阶段1拆解: stream 系外移 stream_relay.rs,mod.rs 3048→2607,loop_body 502→83 技能收尾: regenerate/edit 回溯补技能,新增 read_skill 工具,项目级技能扫描 DRY: emit 双写 46 处收敛 event_bus 正典,KV 引号归一收口,LOCK-SLOW 33 处单行化 警示骨架 4→1 与压缩兜底 2→1,噪音目录档位收敛,truncate 同名陷阱消除 工具元数据落 ToolMeta 八维,7 张 match 表改查 registry,parity 测试锁等价 命名: is_noise_entry/teardown_round/probe_missed_terminal_event 等 6 项纠偏
153 lines
7.8 KiB
Rust
153 lines
7.8 KiB
Rust
//! 灵感类 AI 工具声明式注册(list + create + update + delete 共 4 个)。
|
|
//!
|
|
//! 迁自 `tool_registry.rs::register_idea_tools`(原 2 个 list_ideas/create_idea),
|
|
//! 改用 `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 块(逻辑等价),
|
|
//! 仅闭包包装(`{ let db = db.clone(); Box::new(move |args| { let db = db.clone();
|
|
//! Box::pin(async move { ... }) }) }`)改由 `declare_tool!` 宏生成。
|
|
//! - name/desc/schema/risk 与原手写定义逐字一致。
|
|
//! - 复用类型/常量:new_id/IdeaStatus/IdeaRecord/now_millis(各 pub(crate) 来源,与原同源) +
|
|
//! MAX_LIST_RESULTS(super::tool_registry 单真相源)。
|
|
//!
|
|
//! 等价性验证:基线测试 `test_build_ai_tool_registry_baseline_tool_count` 断言 50 总量 +
|
|
//! 工具名集合稳定(防 rename / 漏注册)。
|
|
|
|
use std::sync::Arc;
|
|
|
|
use df_ai::ai_tools::{object_schema, AiToolRegistry, GoalMeta, GoalStyle, RiskLevel, ToolMeta};
|
|
use df_ai::declare_tool;
|
|
use df_storage::db::Database;
|
|
use df_storage::models::IdeaRecord;
|
|
use df_types::types::{new_id, IdeaStatus};
|
|
|
|
use crate::commands::now_millis;
|
|
use crate::commands::ai::tool_registry::MAX_LIST_RESULTS;
|
|
|
|
/// 注册 4 个灵感类工具(list/create/update/delete)到 `$registry`。
|
|
///
|
|
/// 与原手写 register(name, desc, schema, risk, handler) 语义 1:1:
|
|
/// - name/desc/schema 字符串与 JSON Schema 逐字照搬原定义
|
|
/// - risk 与原一致(list=Low,create/update=Medium,delete=High)
|
|
/// - handler body 与原 async move 块逐字一致(逻辑零变更)
|
|
///
|
|
/// 唯一差异:闭包包装改由 `declare_tool!` 宏生成,handler body 直接写业务逻辑。
|
|
pub fn register(registry: &mut AiToolRegistry, db: &Arc<Database>) {
|
|
declare_tool!(
|
|
registry,
|
|
db: Arc<Database>,
|
|
"list_ideas",
|
|
"列出所有灵感,支持 offset/limit 分页。返回 items、total、has_more。默认 limit=50",
|
|
RiskLevel::Low,
|
|
schema: object_schema(vec![("offset", "integer", false), ("limit", "integer", false)]),
|
|
args => {
|
|
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 }))
|
|
}
|
|
);
|
|
|
|
declare_tool!(
|
|
registry,
|
|
db: Arc<Database>,
|
|
"create_idea",
|
|
"捕获一个新灵感",
|
|
RiskLevel::Medium,
|
|
meta: ToolMeta {
|
|
entity_action: Some(("idea", "create")),
|
|
approval_hint: Some(("捕获灵感:{}", &["title"])),
|
|
display_verb: Some("捕获灵感"),
|
|
goal: Some(GoalMeta { verb: "捕获灵感", keys: &["title"], style: GoalStyle::Raw, fallback: None }),
|
|
..Default::default()
|
|
},
|
|
schema: object_schema(vec![
|
|
("title", "string", true), ("description", "string", false),
|
|
("tags", "string", false), ("source", "string", false),
|
|
// priority:与 commands::idea::CreateIdeaInput 默认值一致(=1,灵感默认普通优先级)
|
|
("priority", "integer", false),
|
|
]),
|
|
args => {
|
|
let title = args["title"].as_str().ok_or_else(|| anyhow::anyhow!("缺少 title"))?;
|
|
let repo = df_storage::crud::IdeaRepo::new(&db);
|
|
let record = IdeaRecord {
|
|
id: new_id(), title: title.to_string(),
|
|
description: args["description"].as_str().unwrap_or("").to_string(),
|
|
// priority 默认 1:灵感默认普通优先级(与 commands::idea::default_priority 及 tasks 表 SQL DEFAULT 1 对齐)
|
|
status: IdeaStatus::Draft, priority: args["priority"].as_i64().unwrap_or(1) as i32,
|
|
score: None, tags: args["tags"].as_str().map(|s| s.to_string()),
|
|
source: args["source"].as_str().map(|s| s.to_string()),
|
|
promoted_to: None, ai_analysis: None, scores: None,
|
|
related_ids: None,
|
|
created_at: now_millis(), updated_at: now_millis(),
|
|
};
|
|
let id = record.id.clone();
|
|
repo.insert(record).await?;
|
|
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<Database>,
|
|
"update_idea",
|
|
"更新灵感的指定字段(title/description/priority/tags/source/score 等),需要提供灵感 ID、字段名和新值。",
|
|
RiskLevel::Medium,
|
|
meta: ToolMeta {
|
|
entity_action: Some(("idea", "update")),
|
|
display_verb: Some("修改灵感"),
|
|
..Default::default()
|
|
},
|
|
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<Database>,
|
|
"delete_idea",
|
|
"删除灵感(软删除,移入回收站,可恢复)",
|
|
RiskLevel::Medium,
|
|
meta: ToolMeta {
|
|
entity_action: Some(("idea", "delete")),
|
|
display_verb: Some("删除灵感"),
|
|
..Default::default()
|
|
},
|
|
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 }))
|
|
}
|
|
);
|
|
}
|