新增: Wave8 F-06 导入项目 + F-02 技能联想 + AR-6 Low 失败语义
- F-06 导入历史项目:scan.rs extract_description(README 首段 200 字) + import_project 命令(合并 create+bind,spawn_blocking 探测栈) + 前端 ProjectDetail 导入按钮 + store action;主代理补 lib.rs invoke_handler 注册(agent 未注册) - F-02 技能联想使用:核对发现 sendMessage→ai_chat_send skill 调用链已完整(联想选中→带 skill→注入 SKILL.md),补 Esc 取消选中兜底 - AR-6 Low 失败语义统一(定向B):audit.rs Low 失败 emit AiToolCallCompleted(错误 result) 替代 AiError,消除前端 false/后端 loop 续紊乱,错误回填 tool_result 让 LLM 自处理 cargo/vue-tsc 0 err
This commit is contained in:
@@ -8,7 +8,7 @@ use tauri::State;
|
||||
use df_ai::build_provider;
|
||||
use df_ai::provider::{ChatMessage, CompletionRequest};
|
||||
use df_core::types::new_id;
|
||||
use df_project::scan::{collect_sample, detect_stack, normalize_path};
|
||||
use df_project::scan::{collect_sample, detect_stack, extract_description, normalize_path};
|
||||
use df_storage::models::ProjectRecord;
|
||||
|
||||
use crate::state::AppState;
|
||||
@@ -90,6 +90,99 @@ pub async fn create_project(
|
||||
Ok(record)
|
||||
}
|
||||
|
||||
/// 导入历史项目入参
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ImportProjectInput {
|
||||
/// 待导入的本地目录绝对路径(已存在)
|
||||
pub path: String,
|
||||
/// 项目名(可选,空=用目录名)
|
||||
#[serde(default)]
|
||||
pub name: Option<String>,
|
||||
/// 描述(可选,空=自动读 README 首段)
|
||||
#[serde(default)]
|
||||
pub description: Option<String>,
|
||||
/// 技术栈 JSON 数组字符串(可选,空=自动探测)
|
||||
#[serde(default)]
|
||||
pub stack: Option<String>,
|
||||
}
|
||||
|
||||
/// 导入历史项目 — 用户选已存在的本地目录,复用 scan 探测 + 绑定一步创建项目记录。
|
||||
///
|
||||
/// 与 `create_project` 的区别:import 直接给 path,创建实体 + 绑定目录 + 探测栈 +
|
||||
/// (可选)读 README 首段填 description 一次性完成,无需先建空项目再绑定。
|
||||
///
|
||||
/// 流程:校验目录存在 → normalize_path 防重复绑定 → detect_stack + extract_description
|
||||
/// (spawn_blocking 防 IO 阻塞 tokio runtime)→ 拼记录 insert → 返回。
|
||||
#[tauri::command]
|
||||
pub async fn import_project(
|
||||
state: State<'_, AppState>,
|
||||
input: ImportProjectInput,
|
||||
) -> Result<ProjectRecord, String> {
|
||||
let path = input.path.trim().to_string();
|
||||
if path.is_empty() {
|
||||
return Err("导入路径不能为空".to_string());
|
||||
}
|
||||
if !Path::new(&path).is_dir() {
|
||||
return Err(format!("目录不存在: {path}"));
|
||||
}
|
||||
// 防重复绑定(normalize_path 规范化比较,防正反斜杠/末尾斜杠绕过)
|
||||
if let Some(conflict) = find_binding_conflict(&state, &path, None).await? {
|
||||
return Err(format!("目录已被项目「{}」绑定", conflict.name));
|
||||
}
|
||||
|
||||
// 探测栈 + 读 description(spawn_blocking 防 IO 阻塞 tokio runtime)
|
||||
let root = std::path::PathBuf::from(&path);
|
||||
let want_name = input.name.clone();
|
||||
let want_desc = input.description.clone();
|
||||
let want_stack = input.stack.clone();
|
||||
let (name, description, stack_json) = tokio::task::spawn_blocking(move || -> Result<_, String> {
|
||||
// name: 入参优先,否则取目录名
|
||||
let name = match want_name.as_deref().map(str::trim).filter(|s| !s.is_empty()) {
|
||||
Some(n) => n.to_string(),
|
||||
None => root
|
||||
.file_name()
|
||||
.and_then(|n| n.to_str())
|
||||
.map(|s| s.to_string())
|
||||
.ok_or_else(|| "无法从路径解析项目名".to_string())?,
|
||||
};
|
||||
// description: 入参优先,否则读 README 首段
|
||||
let description = match want_desc.as_deref().map(str::trim).filter(|s| !s.is_empty()) {
|
||||
Some(d) => d.to_string(),
|
||||
None => extract_description(&root).unwrap_or_default(),
|
||||
};
|
||||
// stack: 入参优先,否则自动探测
|
||||
let stack_json = match want_stack.as_deref().map(str::trim).filter(|s| !s.is_empty()) {
|
||||
Some(s) => s.to_string(),
|
||||
None => {
|
||||
let detected = detect_stack(&root).map_err(|e| e.to_string())?;
|
||||
serde_json::to_string(&detected).map_err(|e| e.to_string())?
|
||||
}
|
||||
};
|
||||
Ok((name, description, stack_json))
|
||||
})
|
||||
.await
|
||||
.map_err(|e| e.to_string())??;
|
||||
|
||||
let now = now_millis();
|
||||
let record = ProjectRecord {
|
||||
id: new_id(),
|
||||
name,
|
||||
description,
|
||||
status: "planning".to_string(),
|
||||
idea_id: None,
|
||||
path: Some(path),
|
||||
stack: Some(stack_json),
|
||||
created_at: now.clone(),
|
||||
updated_at: now,
|
||||
};
|
||||
state
|
||||
.projects
|
||||
.insert(record.clone())
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
Ok(record)
|
||||
}
|
||||
|
||||
/// 按 ID 查询项目
|
||||
#[tauri::command]
|
||||
pub async fn get_project(
|
||||
|
||||
Reference in New Issue
Block a user