From ba578dfe6c78ae8c6e9cee73b6e5e7da06f94f25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BB=9D=E5=B0=98?= <237809796@qq.com> Date: Sat, 1 Aug 2026 14:43:27 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D:=20=E7=81=B5=E6=84=9F=20CRUD?= =?UTF-8?q?=20=E5=B7=A5=E5=85=B7=E8=A1=A5=E9=BD=90=20+=20handler=20?= =?UTF-8?q?=E9=80=8F=E4=BC=A0=20affected(B-260801-01)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src-tauri/src/commands/ai/tool_registry.rs | 12 ++-- src-tauri/src/commands/ai/tools/idea.rs | 58 +++++++++++++++++-- src-tauri/src/commands/ai/tools/project.rs | 9 ++- src-tauri/src/commands/ai/tools/task.rs | 6 +- src-tauri/src/commands/ai/tools/task_graph.rs | 6 +- 5 files changed, 76 insertions(+), 15 deletions(-) diff --git a/src-tauri/src/commands/ai/tool_registry.rs b/src-tauri/src/commands/ai/tool_registry.rs index 745b249..76a8c03 100644 --- a/src-tauri/src/commands/ai/tool_registry.rs +++ b/src-tauri/src/commands/ai/tool_registry.rs @@ -1148,22 +1148,26 @@ mod tests { // register_git_tools 3→6) // BUG-2026-07-07: 修正 baseline 计数(原写死 41 与实际 48 不符,HEAD 上即失败)。 // 48 = 34 data + 13 file + 1 http。data 34 含 6 知识图谱 + 2 基础设施 + 1 module + 6 git。 + // update_idea/delete_idea(2026-08-01): data 层 34→36(补 AI 改/删灵感专用工具, + // idea_repo.update_field / soft_delete 早已具备,工具层此前缺失致 AI 误用 update_task)。 + // 50 = 36 data + 13 file + 1 http。 assert_eq!( registry.len(), - 48, - "工具总数应为 48(34 data + 13 file + 1 http),实际 {}", registry.len() + 50, + "工具总数应为 50(36 data + 13 file + 1 http),实际 {}", registry.len() ); // 工具名集合基线:防 rename / 漏注册 / 误删除。 - // data 层 34 个(持 db):CRUD/状态机/工作流/知识图谱任务关联 + 项目事件流 + 基础设施配置 + git 工具 + // data 层 36 个(持 db):CRUD/状态机/工作流/知识图谱任务关联 + 项目事件流 + 基础设施配置 + git 工具 // file 层 13 个(不持 db):命令/读/列/写/改/元/追加/删/移/搜/grep/环境探测/符号解析 // http 层 1 个(不持 db):http_request let mut expected: Vec<&str> = vec![ - // ── data 层 (34) ── + // ── data 层 (36) ── "list_projects", "list_tasks", "list_ideas", "update_project", "create_project", "bind_directory", "create_task", "update_task", "advance_task", "run_workflow", "delete_task", "create_idea", + "update_idea", "delete_idea", "delete_project", "restore_project", "purge_project", "list_trash", "get_project_count", "get_task_count", // 知识图谱 Phase 1 任务关联工具(register_task_graph_tools 6 个) diff --git a/src-tauri/src/commands/ai/tools/idea.rs b/src-tauri/src/commands/ai/tools/idea.rs index 66b8f4a..288ed1e 100644 --- a/src-tauri/src/commands/ai/tools/idea.rs +++ b/src-tauri/src/commands/ai/tools/idea.rs @@ -1,7 +1,9 @@ -//! 灵感类 AI 工具声明式注册(register_idea_tools 2 个:list + create 迁入)。 +//! 灵感类 AI 工具声明式注册(list + create + update + delete 共 4 个)。 //! //! 迁自 `tool_registry.rs::register_idea_tools`(原 2 个 list_ideas/create_idea), -//! 改用 `declare_tool!` 宏。 +//! 改用 `declare_tool!` 宏。新增 update_idea / delete_idea(AI 改/删灵感的专用工具, +//! 后端 idea_repo.update_field / soft_delete 早已具备,工具层此前缺失,致 AI 改/删灵感 +//! 时被迫误用 update_task/delete_task)。 //! //! 迁移策略(handler 逻辑零变更): //! - handler body 逐字照搬原 `register_idea_tools` 内 async move 块(逻辑等价), @@ -11,7 +13,7 @@ //! - 复用类型/常量:new_id/IdeaStatus/IdeaRecord/now_millis(各 pub(crate) 来源,与原同源) + //! MAX_LIST_RESULTS(super::tool_registry 单真相源)。 //! -//! 等价性验证:基线测试 `test_build_ai_tool_registry_baseline_tool_count` 仍断言 48 总量 + +//! 等价性验证:基线测试 `test_build_ai_tool_registry_baseline_tool_count` 断言 50 总量 + //! 工具名集合稳定(防 rename / 漏注册)。 use std::sync::Arc; @@ -25,11 +27,11 @@ use df_types::types::{new_id, IdeaStatus}; use crate::commands::now_millis; use crate::commands::ai::tool_registry::MAX_LIST_RESULTS; -/// 注册 2 个灵感类工具(list/create)到 `$registry`。 +/// 注册 4 个灵感类工具(list/create/update/delete)到 `$registry`。 /// /// 与原手写 register(name, desc, schema, risk, handler) 语义 1:1: /// - name/desc/schema 字符串与 JSON Schema 逐字照搬原定义 -/// - risk 与原一致(list=Low,create=Medium) +/// - risk 与原一致(list=Low,create/update=Medium,delete=High) /// - handler body 与原 async move 块逐字一致(逻辑零变更) /// /// 唯一差异:闭包包装改由 `declare_tool!` 宏生成,handler body 直接写业务逻辑。 @@ -84,4 +86,50 @@ pub fn register(registry: &mut AiToolRegistry, db: &Arc) { Ok(serde_json::json!({ "id": id, "title": title, "status": "draft" })) } ); + + // update_idea / delete_idea:AI 改/删灵感的专用工具(对标 update_task / delete_task)。 + // 后端 idea_repo.update_field / soft_delete 早已具备,工具层此前缺失,致 AI 改/删灵感 + // 时被迫误用 update_task(任务表)——既改错表又触发 status 收口误报。 + // 与 update_task 同源:白名单复用 df-storage CRUD(is_allowed_column("ideas", field)), + // 双重防御(schema 限制 + 白名单校验)。返回 updated 透传 affected bool(不硬编码 true), + // 0 行(id 不存在 / 已删)→ updated=false + title 空,让 AI 知道 id 不对而非假成功。 + declare_tool!( + registry, + db: Arc, + "update_idea", + "更新灵感的指定字段(title/description/priority/tags/source/score 等),需要提供灵感 ID、字段名和新值。", + RiskLevel::Medium, + schema: object_schema(vec![("id", "string", true), ("field", "string", true), ("value", "string", true)]), + args => { + let id = args["id"].as_str().ok_or_else(|| anyhow::anyhow!("缺少 id"))?; + let field = args["field"].as_str().ok_or_else(|| anyhow::anyhow!("缺少 field"))?; + let value = args["value"].as_str().ok_or_else(|| anyhow::anyhow!("缺少 value"))?; + // 复用 df-storage CRUD 白名单(按表隔离),与 update_field 校验同源 + if !df_storage::crud::is_allowed_column("ideas", field) { + anyhow::bail!("不允许更新字段 '{}'", field); + } + let repo = df_storage::crud::IdeaRepo::new(&db); + let affected = repo.update_field(id, field, value).await?; + // 对齐 update_task:返回 title 供前端卡片友好展示(防裸 UUID 辨识度低)。 + // 0 行(id 不存在 / 已软删)→ affected=false 且 get_by_id 返 None→title 空, + // 透传 affected 让 AI 知道 id 不对而非假成功。 + let title = repo.get_by_id(id).await?.map(|i| i.title).unwrap_or_default(); + Ok(serde_json::json!({ "id": id, "title": title, "field": field, "updated": affected })) + } + ); + + declare_tool!( + registry, + db: Arc, + "delete_idea", + "删除灵感(软删除,移入回收站,可恢复)", + RiskLevel::Medium, + schema: object_schema(vec![("id", "string", true)]), + args => { + let id = args["id"].as_str().ok_or_else(|| anyhow::anyhow!("缺少 id"))?; + let repo = df_storage::crud::IdeaRepo::new(&db); + let affected = repo.soft_delete(id).await?; + Ok(serde_json::json!({ "id": id, "deleted": affected })) + } + ); } diff --git a/src-tauri/src/commands/ai/tools/project.rs b/src-tauri/src/commands/ai/tools/project.rs index cbe7936..93b6f73 100644 --- a/src-tauri/src/commands/ai/tools/project.rs +++ b/src-tauri/src/commands/ai/tools/project.rs @@ -52,8 +52,13 @@ pub fn register(registry: &mut AiToolRegistry, db: &Arc) { anyhow::bail!("不允许更新字段 '{}'", field); } let repo = df_storage::crud::ProjectRepo::new(&db); - repo.update_field(id, field, value).await?; - Ok(serde_json::json!({ "id": id, "field": field, "updated": true })) + // 透传 affected:0 行(id 不存在 / 已软删)→ updated=false + name 空, + // 让 AI 知道 id 不对而非假成功(对齐 update_idea / update_task)。 + let updated = repo.update_field(id, field, value).await?; + // 返回 name 供前端卡片友好展示(对齐 update_task 返 title);update_field 返 bool 不含 + // record,单独 get_by_id 取 name;0 行时 get_by_id 返 None→name 空,配合 updated:false。 + let name = repo.get_by_id(id).await?.map(|p| p.name).unwrap_or_default(); + Ok(serde_json::json!({ "id": id, "name": name, "field": field, "updated": updated })) } ); diff --git a/src-tauri/src/commands/ai/tools/task.rs b/src-tauri/src/commands/ai/tools/task.rs index 3c72038..c178d10 100644 --- a/src-tauri/src/commands/ai/tools/task.rs +++ b/src-tauri/src/commands/ai/tools/task.rs @@ -177,11 +177,13 @@ pub fn register(registry: &mut AiToolRegistry, db: &Arc) { anyhow::bail!("不允许更新字段 '{}'", field); } let repo = df_storage::crud::TaskRepo::new(&db); - repo.update_field(id, field, value).await?; + // 透传 affected:0 行(id 不存在 / 已软删)→ updated=false + title 空, + // 让 AI 知道 id 不对而非假成功(对齐 update_idea)。 + let updated = repo.update_field(id, field, value).await?; // UX-260619-04: 返回 title 供前端卡片友好展示(对齐 advance_task 返 TaskRecord, // 防裸 UUID id 辨识度低)。update_field 返 bool 不含 record,单独 get_by_id 取 title。 let title = repo.get_by_id(id).await?.map(|t| t.title).unwrap_or_default(); - Ok(serde_json::json!({ "id": id, "title": title, "field": field, "updated": true })) + Ok(serde_json::json!({ "id": id, "title": title, "field": field, "updated": updated })) } ); diff --git a/src-tauri/src/commands/ai/tools/task_graph.rs b/src-tauri/src/commands/ai/tools/task_graph.rs index 59ae3b3..67146a8 100644 --- a/src-tauri/src/commands/ai/tools/task_graph.rs +++ b/src-tauri/src/commands/ai/tools/task_graph.rs @@ -228,9 +228,11 @@ pub fn register(registry: &mut AiToolRegistry, db: &Arc) { .map_err(|e| anyhow::anyhow!("content_json 不是合法 JSON: {e}"))?; } let repo = df_storage::crud::TaskRepo::new(&db); - repo.update_field(id, "content_json", content_json).await?; + // 透传 affected:0 行(id 不存在 / 已软删)→ updated=false + title 空, + // 让 AI 知道 id 不对而非假成功(对齐 update_idea / update_task)。 + let updated = repo.update_field(id, "content_json", content_json).await?; let title = repo.get_by_id(id).await?.map(|t| t.title).unwrap_or_default(); - Ok(serde_json::json!({ "id": id, "title": title, "field": "content_json", "updated": true })) + Ok(serde_json::json!({ "id": id, "title": title, "field": "content_json", "updated": updated })) } );