diff --git a/src-tauri/src/commands/ai/tool_registry.rs b/src-tauri/src/commands/ai/tool_registry.rs index 2b1512a..ea1a823 100644 --- a/src-tauri/src/commands/ai/tool_registry.rs +++ b/src-tauri/src/commands/ai/tool_registry.rs @@ -379,8 +379,27 @@ pub fn build_ai_tool_registry(db: &Arc) -> AiToolRegistry { /// /// SMELL-P0-2:抽自原 build_ai_tool_registry 1091 行单函数(数据+文件混合)。 /// 18 个 register 调用【原样移入】,零行为变更,仅机械搬运。 +/// +/// SMELL-P0-2 续拆(2026-06-19):18 工具按业务表细分为 5 子函数: +/// - register_project_tools(8):list/update/create/bind/delete/restore/purge/get_count +/// - register_task_tools(6):list/create/update/advance/delete/get_count +/// - register_workflow_tools(1):run_workflow(High,单独,handler 防御兜底) +/// - register_idea_tools(2):list/create +/// - register_trash_tools(1):list_trash +/// AiToolRegistry 底层 HashMap(注册顺序无关),拆分后工具集合与原一致, +/// 行为零变更(基线测试 test_build_ai_tool_registry_baseline_tool_count 守护)。 fn register_data_tools(registry: &mut AiToolRegistry, db: &Arc) { - // ── 只读 (Low) ── + register_project_tools(registry, db); + register_task_tools(registry, db); + register_workflow_tools(registry); + register_idea_tools(registry, db); + register_trash_tools(registry, db); +} + +/// 项目类 AI 工具注册(8 个:CRUD + 目录绑定 + 探总量)——持 db:Arc。 +/// 抽自 register_data_tools(SMELL-P0-2 续拆),【原样移入】,零行为变更。 +/// 组内顺序保留原相对顺序(list/update/create/bind/delete/restore/purge/get_count)。 +fn register_project_tools(registry: &mut AiToolRegistry, db: &Arc) { registry.register( "list_projects", "列出所有项目,支持 offset/limit 分页。返回 items(项目列表)、total(总量)、has_more(是否有更多页)。默认 limit=50", df_ai::ai_tools::object_schema(vec![("offset", "integer", false), ("limit", "integer", false)]), RiskLevel::Low, @@ -398,46 +417,6 @@ fn register_data_tools(registry: &mut AiToolRegistry, db: &Arc) { }) })}, ); - registry.register( - "list_tasks", "列出任务,可按 project_id 筛选,支持 offset/limit 分页。返回 items、total、has_more。默认 limit=50", - df_ai::ai_tools::object_schema(vec![("project_id", "string", false), ("offset", "integer", false), ("limit", "integer", false)]), RiskLevel::Low, - { let db = db.clone(); Box::new(move |args: serde_json::Value| { - let db = db.clone(); - Box::pin(async move { - let repo = df_storage::crud::TaskRepo::new(&db); - let tasks = if let Some(pid) = args.get("project_id").and_then(|v| v.as_str()) { - repo.query("project_id", pid).await? - } else { - repo.list_all().await? - }; - let total = tasks.len(); - let offset = args["offset"].as_u64().unwrap_or(0) as usize; - let limit = args["limit"].as_u64().unwrap_or(MAX_LIST_RESULTS as u64).min(MAX_LIST_RESULTS as u64) as usize; - let page_items: Vec<_> = tasks.into_iter().skip(offset).take(limit).collect(); - let has_more = (offset + page_items.len()) < total; - Ok(serde_json::json!({ "items": page_items, "total": total, "has_more": has_more })) - }) - })}, - ); - registry.register( - "list_ideas", "列出所有灵感,支持 offset/limit 分页。返回 items、total、has_more。默认 limit=50", - df_ai::ai_tools::object_schema(vec![("offset", "integer", false), ("limit", "integer", false)]), RiskLevel::Low, - { let db = db.clone(); Box::new(move |args: serde_json::Value| { - let db = db.clone(); - Box::pin(async move { - let repo = df_storage::crud::IdeaRepo::new(&db); - let items = repo.list_all().await?; - let total = items.len(); - let offset = args["offset"].as_u64().unwrap_or(0) as usize; - let limit = args["limit"].as_u64().unwrap_or(MAX_LIST_RESULTS as u64).min(MAX_LIST_RESULTS as u64) as usize; - let page_items: Vec<_> = items.into_iter().skip(offset).take(limit).collect(); - let has_more = (offset + page_items.len()) < total; - Ok(serde_json::json!({ "items": page_items, "total": total, "has_more": has_more })) - }) - })}, - ); - - // ── 创建 (Medium) ── registry.register( "update_project", "更新项目的指定字段(name/status/description/path/stack),需要提供项目 ID、字段名和新值。绑定代码目录推荐改用 bind_directory", df_ai::ai_tools::object_schema(vec![("id", "string", true), ("field", "string", true), ("value", "string", true)]), @@ -507,6 +486,85 @@ fn register_data_tools(registry: &mut AiToolRegistry, db: &Arc) { }) })}, ); + registry.register( + "delete_project", "删除项目(移入回收站,可恢复。永久删除用 purge_project)", + df_ai::ai_tools::object_schema(vec![("id", "string", true)]), RiskLevel::High, + { let db = db.clone(); Box::new(move |args: serde_json::Value| { + let db = db.clone(); + Box::pin(async move { + let id = args["id"].as_str().ok_or_else(|| anyhow::anyhow!("缺少 id"))?; + let repo = df_storage::crud::ProjectRepo::new(&db); + let deleted = repo.soft_delete(id).await?; + Ok(serde_json::json!({ "deleted": deleted, "id": id })) + }) + })}, + ); + registry.register( + "restore_project", "从回收站恢复已删除项目", + df_ai::ai_tools::object_schema(vec![("id", "string", true)]), RiskLevel::High, + { let db = db.clone(); Box::new(move |args: serde_json::Value| { + let db = db.clone(); + Box::pin(async move { + let id = args["id"].as_str().ok_or_else(|| anyhow::anyhow!("缺少 id"))?; + let repo = df_storage::crud::ProjectRepo::new(&db); + let restored = repo.restore(id).await?; + Ok(serde_json::json!({ "restored": restored, "id": id })) + }) + })}, + ); + registry.register( + "purge_project", "永久删除项目及关联数据(不可恢复)", + df_ai::ai_tools::object_schema(vec![("id", "string", true)]), RiskLevel::High, + { let db = db.clone(); Box::new(move |args: serde_json::Value| { + let db = db.clone(); + Box::pin(async move { + let id = args["id"].as_str().ok_or_else(|| anyhow::anyhow!("缺少 id"))?; + let repo = df_storage::crud::ProjectRepo::new(&db); + let purged = repo.purge_with_descendants(id).await?; + Ok(serde_json::json!({ "purged": purged, "id": id })) + }) + })}, + ); + // ── 探总量工具 (Low risk, F-260616-08) ── + registry.register( + "get_project_count", "获取项目总数(未删除项目),用于分页策略判断。返回 { total: usize }", + df_ai::ai_tools::object_schema(vec![]), RiskLevel::Low, + { let db = db.clone(); Box::new(move |_args: serde_json::Value| { + let db = db.clone(); + Box::pin(async move { + let repo = df_storage::crud::ProjectRepo::new(&db); + let items = repo.list_active().await?; + Ok(serde_json::json!({ "total": items.len() })) + }) + })}, + ); +} + +/// 任务类 AI 工具注册(6 个:CRUD + 状态机推进 + 探总量)——持 db:Arc。 +/// 抽自 register_data_tools(SMELL-P0-2 续拆),【原样移入】,零行为变更。 +/// 组内顺序保留原相对顺序(list/create/update/advance/delete/get_count)。 +fn register_task_tools(registry: &mut AiToolRegistry, db: &Arc) { + registry.register( + "list_tasks", "列出任务,可按 project_id 筛选,支持 offset/limit 分页。返回 items、total、has_more。默认 limit=50", + df_ai::ai_tools::object_schema(vec![("project_id", "string", false), ("offset", "integer", false), ("limit", "integer", false)]), RiskLevel::Low, + { let db = db.clone(); Box::new(move |args: serde_json::Value| { + let db = db.clone(); + Box::pin(async move { + let repo = df_storage::crud::TaskRepo::new(&db); + let tasks = if let Some(pid) = args.get("project_id").and_then(|v| v.as_str()) { + repo.query("project_id", pid).await? + } else { + repo.list_all().await? + }; + let total = tasks.len(); + let offset = args["offset"].as_u64().unwrap_or(0) as usize; + let limit = args["limit"].as_u64().unwrap_or(MAX_LIST_RESULTS as u64).min(MAX_LIST_RESULTS as u64) as usize; + let page_items: Vec<_> = tasks.into_iter().skip(offset).take(limit).collect(); + let has_more = (offset + page_items.len()) < total; + Ok(serde_json::json!({ "items": page_items, "total": total, "has_more": has_more })) + }) + })}, + ); registry.register( "create_task", "在指定项目下创建新任务", df_ai::ai_tools::object_schema(vec![("project_id", "string", true), ("title", "string", true), ("description", "string", false), ("priority", "integer", false)]), @@ -587,6 +645,43 @@ fn register_data_tools(registry: &mut AiToolRegistry, db: &Arc) { }) })}, ); + // FR-D6:delete_task 对齐 commands::task::delete_task(D-260616-02 软删除) + // tasks 表 V14 加 deleted_at,delete_task 改调 soft_delete 移入回收站(对标 delete_project) + registry.register( + "delete_task", "删除任务(移入回收站,可恢复)", + df_ai::ai_tools::object_schema(vec![("id", "string", true)]), RiskLevel::High, + { let db = db.clone(); Box::new(move |args: serde_json::Value| { + let db = db.clone(); + Box::pin(async move { + let id = args["id"].as_str().ok_or_else(|| anyhow::anyhow!("缺少 id"))?; + let repo = df_storage::crud::TaskRepo::new(&db); + let deleted = repo.soft_delete(id).await?; + Ok(serde_json::json!({ "deleted": deleted, "id": id })) + }) + })}, + ); + // ── 探总量工具 (Low risk, F-260616-08) ── + registry.register( + "get_task_count", "获取任务总数(未删除任务),用于分页策略判断。返回 { total: usize }", + df_ai::ai_tools::object_schema(vec![("project_id", "string", false)]), RiskLevel::Low, + { let db = db.clone(); Box::new(move |args: serde_json::Value| { + let db = db.clone(); + Box::pin(async move { + let repo = df_storage::crud::TaskRepo::new(&db); + let tasks = if let Some(pid) = args.get("project_id").and_then(|v| v.as_str()) { + repo.query("project_id", pid).await? + } else { + repo.list_all().await? + }; + Ok(serde_json::json!({ "total": tasks.len() })) + }) + })}, + ); +} + +/// 工作流类 AI 工具注册(1 个:run_workflow)——不持 db(handler 防御兜底,详见注释)。 +/// 抽自 register_data_tools(SMELL-P0-2 续拆),【原样移入】,零行为变更。 +fn register_workflow_tools(registry: &mut AiToolRegistry) { // F-260616-07 阶段3(子项2):run_workflow AI 工具注册实装。 // 实施路径文档 §三 列为阶段3 必做项(tool_registry.rs 此前无此工具连空壳都没有)。 // 描述明确按任务 target_status 推进对应工作流(含 AiNode 自审 / HumanNode 核对闸门)。 @@ -627,18 +722,25 @@ fn register_data_tools(registry: &mut AiToolRegistry, db: &Arc) { }) })}, ); - // FR-D6:delete_task 对齐 commands::task::delete_task(D-260616-02 软删除) - // tasks 表 V14 加 deleted_at,delete_task 改调 soft_delete 移入回收站(对标 delete_project) +} + +/// 灵感类 AI 工具注册(2 个:list + create)——持 db:Arc。 +/// 抽自 register_data_tools(SMELL-P0-2 续拆),【原样移入】,零行为变更。 +fn register_idea_tools(registry: &mut AiToolRegistry, db: &Arc) { registry.register( - "delete_task", "删除任务(移入回收站,可恢复)", - df_ai::ai_tools::object_schema(vec![("id", "string", true)]), RiskLevel::High, + "list_ideas", "列出所有灵感,支持 offset/limit 分页。返回 items、total、has_more。默认 limit=50", + df_ai::ai_tools::object_schema(vec![("offset", "integer", false), ("limit", "integer", false)]), RiskLevel::Low, { let db = db.clone(); Box::new(move |args: serde_json::Value| { let db = db.clone(); Box::pin(async move { - let id = args["id"].as_str().ok_or_else(|| anyhow::anyhow!("缺少 id"))?; - let repo = df_storage::crud::TaskRepo::new(&db); - let deleted = repo.soft_delete(id).await?; - Ok(serde_json::json!({ "deleted": deleted, "id": id })) + let repo = df_storage::crud::IdeaRepo::new(&db); + let items = repo.list_all().await?; + let total = items.len(); + let offset = args["offset"].as_u64().unwrap_or(0) as usize; + let limit = args["limit"].as_u64().unwrap_or(MAX_LIST_RESULTS as u64).min(MAX_LIST_RESULTS as u64) as usize; + let page_items: Vec<_> = items.into_iter().skip(offset).take(limit).collect(); + let has_more = (offset + page_items.len()) < total; + Ok(serde_json::json!({ "items": page_items, "total": total, "has_more": has_more })) }) })}, ); @@ -672,47 +774,11 @@ fn register_data_tools(registry: &mut AiToolRegistry, db: &Arc) { }) })}, ); +} - // ── 高风险 (High) ── - registry.register( - "delete_project", "删除项目(移入回收站,可恢复。永久删除用 purge_project)", - df_ai::ai_tools::object_schema(vec![("id", "string", true)]), RiskLevel::High, - { let db = db.clone(); Box::new(move |args: serde_json::Value| { - let db = db.clone(); - Box::pin(async move { - let id = args["id"].as_str().ok_or_else(|| anyhow::anyhow!("缺少 id"))?; - let repo = df_storage::crud::ProjectRepo::new(&db); - let deleted = repo.soft_delete(id).await?; - Ok(serde_json::json!({ "deleted": deleted, "id": id })) - }) - })}, - ); - registry.register( - "restore_project", "从回收站恢复已删除项目", - df_ai::ai_tools::object_schema(vec![("id", "string", true)]), RiskLevel::High, - { let db = db.clone(); Box::new(move |args: serde_json::Value| { - let db = db.clone(); - Box::pin(async move { - let id = args["id"].as_str().ok_or_else(|| anyhow::anyhow!("缺少 id"))?; - let repo = df_storage::crud::ProjectRepo::new(&db); - let restored = repo.restore(id).await?; - Ok(serde_json::json!({ "restored": restored, "id": id })) - }) - })}, - ); - registry.register( - "purge_project", "永久删除项目及关联数据(不可恢复)", - df_ai::ai_tools::object_schema(vec![("id", "string", true)]), RiskLevel::High, - { let db = db.clone(); Box::new(move |args: serde_json::Value| { - let db = db.clone(); - Box::pin(async move { - let id = args["id"].as_str().ok_or_else(|| anyhow::anyhow!("缺少 id"))?; - let repo = df_storage::crud::ProjectRepo::new(&db); - let purged = repo.purge_with_descendants(id).await?; - Ok(serde_json::json!({ "purged": purged, "id": id })) - }) - })}, - ); +/// 回收站类 AI 工具注册(1 个:list_trash)——持 db:Arc。 +/// 抽自 register_data_tools(SMELL-P0-2 续拆),【原样移入】,零行为变更。 +fn register_trash_tools(registry: &mut AiToolRegistry, db: &Arc) { registry.register( "list_trash", "列出回收站已删除项目,支持 offset/limit 分页。返回 items、total、has_more。默认 limit=50", df_ai::ai_tools::object_schema(vec![("offset", "integer", false), ("limit", "integer", false)]), RiskLevel::Low, @@ -730,36 +796,6 @@ fn register_data_tools(registry: &mut AiToolRegistry, db: &Arc) { }) })}, ); - - // ── 探总量工具 (Low risk, F-260616-08) ── - registry.register( - "get_project_count", "获取项目总数(未删除项目),用于分页策略判断。返回 { total: usize }", - df_ai::ai_tools::object_schema(vec![]), RiskLevel::Low, - { let db = db.clone(); Box::new(move |_args: serde_json::Value| { - let db = db.clone(); - Box::pin(async move { - let repo = df_storage::crud::ProjectRepo::new(&db); - let items = repo.list_active().await?; - Ok(serde_json::json!({ "total": items.len() })) - }) - })}, - ); - registry.register( - "get_task_count", "获取任务总数(未删除任务),用于分页策略判断。返回 { total: usize }", - df_ai::ai_tools::object_schema(vec![("project_id", "string", false)]), RiskLevel::Low, - { let db = db.clone(); Box::new(move |args: serde_json::Value| { - let db = db.clone(); - Box::pin(async move { - let repo = df_storage::crud::TaskRepo::new(&db); - let tasks = if let Some(pid) = args.get("project_id").and_then(|v| v.as_str()) { - repo.query("project_id", pid).await? - } else { - repo.list_all().await? - }; - Ok(serde_json::json!({ "total": tasks.len() })) - }) - })}, - ); } /// 文件系统类 AI 工具注册(命令执行/读/列/写/改/元/追加/删/移/搜)——不持 db,纯文件系统操作。