重构: 后端编排拆解与DRY债收敛及命名纠偏

阶段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 项纠偏
This commit is contained in:
lxy
2026-10-01 19:35:19 +08:00
parent 3861e13b32
commit 75c5e23523
40 changed files with 2988 additions and 1964 deletions
+5 -1
View File
@@ -10,7 +10,7 @@
use std::sync::Arc;
use df_ai::ai_tools::{AiToolRegistry, RiskLevel};
use df_ai::ai_tools::{AiToolRegistry, RiskLevel, SigMeta, SigValKind, ToolMeta};
use df_ai::declare_tool;
/// 注册 fetch_url 工具到 `$registry`(无 db 捕获,纯网络 GET + htmd 转换)。
@@ -42,6 +42,10 @@ pub fn register(registry: &mut AiToolRegistry) {
"fetch_url",
"获取网页 URL 内容并转为 markdown(HTML 清洗 + 截断),用于高效理解网页文档(API 文档/博客/技术资料)。只读 GET,自动剥离 script/style/nav 等噪声,提取 title,按 max_length 截断。返回 {url, title, markdown, length, truncated, render_mode, ?hint}。安全:仅 http/https,拒绝私网/保留 IP(SSRF 防护含 DNS resolve 后校验),重定向≤3 跳。render=true 用 obscura 渲染 JS(SPA/反爬,需装 obscura,未装自动回退静态)。如需 POST/鉴权/原始响应用 http_request",
RiskLevel::Low,
meta: ToolMeta {
sig: Some(SigMeta { groups: &[(&["url"], SigValKind::Str)] }),
..Default::default()
},
schema: schema,
args => {
// 转调 fetch_url.rs handler(SSRF 防护 + htmd 转换 + 去噪音 + 截断全在那)
+85 -2
View File
@@ -14,7 +14,7 @@
//! validate_path / generate_diff / apply_line_range / resolve_anchor_to_lines /
//! resolve_workspace_path_with_allowed / rename_or_cross_volume_copy / compute_file_hash /
//! FILE_LOCKS / probe_executable / list_dir_recursive / compile_glob_to_regex /
//! grep_recursive / grep_one_file / search_files_recursive / is_noise_dir / is_noise_file /
//! grep_recursive / grep_one_file / search_files_recursive / is_noise_entry / is_noise_file /
//! FileGrepHit / LineMatch。
//!
//! 单捕获宏适配(declare_tool! 宏仅支持单 capture 变量):
@@ -30,7 +30,7 @@
use std::sync::Arc;
use df_ai::ai_tools::{object_schema, AiToolRegistry, RiskLevel};
use df_ai::ai_tools::{object_schema, AiToolRegistry, GoalMeta, GoalStyle, RiskLevel, SigMeta, SigValKind, ToolMeta};
use df_ai::declare_tool;
use tokio::sync::RwLock;
@@ -73,6 +73,14 @@ pub fn register(
"read_file",
"读取文件内容,返回文本内容。支持 offset/limit 分页;传入 search 则在文件内容中搜索匹配行(大小写敏感,字符串包含匹配),返回 matches 数组(限50条)。仅需函数/类型符号的签名或定义体时优先用 read_symbol(骨架/全文,避免 prompt 爆)",
RiskLevel::Low,
meta: ToolMeta {
path_args: &["path"],
readonly: true,
display_verb: Some("读取文件"),
goal: Some(GoalMeta { verb: "查看", keys: &["path"], style: GoalStyle::FileName, fallback: None }),
sig: Some(SigMeta { groups: &[(&["path"], SigValKind::Str), (&["offset"], SigValKind::Json), (&["limit"], SigValKind::Json)] }),
..Default::default()
},
schema: object_schema(vec![("path", "string", true), ("offset", "integer", false), ("limit", "integer", false), ("search", "string", false)]),
args => {
let snap = allowed_dirs.read().await.clone();
@@ -208,6 +216,10 @@ pub fn register(
"read_symbol",
"AST 符号解析(信息密度驱动,治 read_file 全文回灌 prompt 爆)。提取函数/结构体/类等符号:默认返回骨架(签名+行范围+内部调用,极小高密度);传 full=true 取完整定义体。不支持/未找到时回退提示用 grep+read_file。Phase1 支持 .rs/.ts/.tsx/.js/.jsx/.vue。",
RiskLevel::Low,
meta: ToolMeta {
readonly: true,
..Default::default()
},
schema: object_schema(vec![("path", "string", true), ("symbol", "string", true), ("full", "boolean", false), ("kind", "string", false), ("drill", "string", false)]),
args => {
let snap = allowed_dirs.read().await.clone();
@@ -308,6 +320,15 @@ pub fn register(
"list_directory",
"列出目录内容,返回文件和子目录列表(名称、类型、大小)。path 可选,不传时返回引导提示",
RiskLevel::Low,
meta: ToolMeta {
path_args: &["path"],
readonly: true,
cache_path_key: true,
display_verb: Some("列出目录"),
goal: Some(GoalMeta { verb: "浏览", keys: &["path"], style: GoalStyle::Raw, fallback: None }),
sig: Some(SigMeta { groups: &[(&["path"], SigValKind::Str)] }),
..Default::default()
},
schema: object_schema(vec![("path", "string", false), ("recursive", "boolean", false), ("skip_noise_dirs", "boolean", false), ("max_depth", "integer", false)]),
args => {
let p = args["path"].as_str().unwrap_or("");
@@ -335,6 +356,13 @@ pub fn register(
"write_file",
"写入或创建文件,自动创建不存在的父目录。支持 encoding=\"base64\" 写入二进制/非 UTF-8 内容(图片/PDF/Excel 等),content 传 base64 编码字符串;默认 encoding=\"utf-8\" 传纯文本",
RiskLevel::Medium,
meta: ToolMeta {
path_args: &["path"],
entity_action: Some(("file", "update")),
display_verb: Some("写入文件"),
goal: Some(GoalMeta { verb: "修改", keys: &["path"], style: GoalStyle::FileName, fallback: None }),
..Default::default()
},
schema: object_schema(vec![
("path", "string", true),
("content", "string", true),
@@ -453,6 +481,14 @@ pub fn register(
"patch_file",
"局部更新文件内容(三模式互斥)。模式1 old_text:精确匹配原文替换(含空格/缩进,CAS 语义)。模式2 replace_lines:按行号区间 {start,end}(1-based 含首尾)替换,不需原文,配 expected_hash 防行号漂移。模式3 anchor:按首尾子串锚点 {start,end}(大小写敏感,子串匹配)定位行号区间替换,不需完整原文。三模式均需 path+new_text,expected_hash 可选通用。注意:若文件已被外部修改,请先重新 read_file 获取最新内容。所有 patch_file 操作自动放行不阻塞 AI 工作流,写入会落 audit 表可追溯。",
RiskLevel::Medium,
meta: ToolMeta {
path_args: &["path"],
entity_action: Some(("file", "update")),
approval_hint: Some(("修改文件:{}", &["path"])),
display_verb: Some("修改文件"),
goal: Some(GoalMeta { verb: "修改", keys: &["path"], style: GoalStyle::FileName, fallback: None }),
..Default::default()
},
schema: patch_file_schema,
args => {
let snap = allowed_dirs.read().await.clone();
@@ -637,6 +673,11 @@ pub fn register(
"file_info",
"获取文件或目录的元信息(是否存在、大小、行数、修改时间、是否二进制、是否目录),不读取文件内容",
RiskLevel::Low,
meta: ToolMeta {
path_args: &["path"],
goal: Some(GoalMeta { verb: "查看", keys: &["path"], style: GoalStyle::FileName, fallback: None }),
..Default::default()
},
schema: object_schema(vec![("path", "string", true)]),
args => {
let snap = allowed_dirs.read().await.clone();
@@ -691,6 +732,12 @@ pub fn register(
"append_file",
"向文件末尾追加内容,文件不存在则自动创建。返回写入字数和新文件大小",
RiskLevel::Medium,
meta: ToolMeta {
path_args: &["path"],
display_verb: Some("追加写入"),
goal: Some(GoalMeta { verb: "修改", keys: &["path"], style: GoalStyle::FileName, fallback: None }),
..Default::default()
},
schema: object_schema(vec![("path", "string", true), ("content", "string", true)]),
args => {
let snap = allowed_dirs.read().await.clone();
@@ -730,6 +777,13 @@ pub fn register(
"delete_file",
"删除文件。默认(permanent=false)软删除:移到工作区 .trash/ 目录保留可恢复;permanent=true 硬删除不可恢复。删除前备份,回收站机制对齐 list_trash。高风险,须人工批准",
RiskLevel::High,
meta: ToolMeta {
path_args: &["path"],
approval_hint: Some(("删除文件:{}({})", &["path", "permanent"])),
display_verb: Some("删除文件"),
goal: Some(GoalMeta { verb: "处理", keys: &["path"], style: GoalStyle::FileName, fallback: None }),
..Default::default()
},
schema: object_schema(vec![
("path", "string", true),
("permanent", "boolean", false),
@@ -810,6 +864,13 @@ pub fn register(
"rename_file",
"重命名或移动文件(同一工具覆盖 rename 和 move)。参数:from(源路径)+ to(目标路径)+ overwrite(默认 false,目标存在则拒绝)。同卷走 tokio::fs::rename(原子),跨卷自动降级 copy+remove(非原子,失败回滚删 to 保 from 完整)。须人工批准",
RiskLevel::Medium,
meta: ToolMeta {
path_args: &["from", "to"],
approval_hint: Some(("重命名/移动:{} → {}", &["from", "to"])),
display_verb: Some("重命名/移动"),
goal: Some(GoalMeta { verb: "处理", keys: &["path"], style: GoalStyle::FileName, fallback: None }),
..Default::default()
},
schema: object_schema(vec![
("from", "string", true),
("to", "string", true),
@@ -902,6 +963,14 @@ pub fn register(
"grep",
"跨文件内容搜索(grep -rn 模式)。参数:pattern(正则,大小写敏感,无特殊字符时等价字面包含)、path(搜索根,可选,不传时返回引导提示)、glob(可选文件名过滤如 *.rs)、output_mode(content/files_with_matches/count)、-n(行号默认 true)、-i(大小写不敏感默认 false)、-C(上下文行数默认 0)、context_chars(字符级窗口,大单行如 minified JS/CSS 命中行超阈值时截匹配位置 ±N 字符窗口替代整行,默认 0 不截;与 -C 互补:-C 行级前后 N 行,context_chars 同长行内字符级)、max_results(上限默认 50)。跳过噪音目录/噪音文件/symlink/二进制文件。返回 matches(files_with_matches 模式)或 matches(含 file/line/content/context,content 模式)+ total + truncated。授权目录内放行,未授权触发目录授权申请(AiDirAuthRequired)",
RiskLevel::Low,
meta: ToolMeta {
path_args: &["path"],
readonly: true,
display_verb: Some("搜索代码内容"),
goal: Some(GoalMeta { verb: "搜索", keys: &["pattern", "query"], style: GoalStyle::Truncate, fallback: Some("搜索文件") }),
sig: Some(SigMeta { groups: &[(&["pattern", "query"], SigValKind::Str), (&["path", "glob"], SigValKind::Str)] }),
..Default::default()
},
schema: grep_schema,
args => {
let p = args["path"].as_str().unwrap_or("");
@@ -1078,6 +1147,15 @@ pub fn register(
"search_files",
"在指定目录下搜索匹配模式(字符串包含匹配)的文件名,支持 offset/limit 分页。返回 results、total、has_more。默认 limit=50",
RiskLevel::Low,
meta: ToolMeta {
path_args: &["path"],
readonly: true,
deny_unauthorized: true,
display_verb: Some("搜索文件"),
goal: Some(GoalMeta { verb: "搜索", keys: &["pattern", "query"], style: GoalStyle::Truncate, fallback: Some("搜索文件") }),
sig: Some(SigMeta { groups: &[(&["pattern", "query"], SigValKind::Str), (&["path", "glob"], SigValKind::Str)] }),
..Default::default()
},
schema: object_schema(vec![("path", "string", false), ("pattern", "string", true), ("recursive", "boolean", false), ("offset", "integer", false), ("limit", "integer", false)]),
args => {
let p = args["path"].as_str().unwrap_or("");
@@ -1122,6 +1200,11 @@ pub fn register(
"run_command",
"在指定工作目录执行 shell 命令,返回 stdout/stderr/exit_code。仅用于命令执行场景:跑测试套件、构建项目、运行二进制/脚本验证行为。读取文件用 read_file,编辑文件用 patch_file/write_file,列目录用 list_directory,搜索文件名用 search_files——不要用本工具完成这些操作。高风险,须人工批准。命令需自包含(非交互式,避免需用户输入的程序)。默认超时 60 秒。返回含 succeeded 布尔(按 exit_code==0 判定)。注意:部分命令非零退出属正常语义(如 git diff 无差异返回 1),此时以 stdout/stderr 内容为准,勿仅凭 exit_code≠0 判失败。",
RiskLevel::High,
meta: ToolMeta {
goal: Some(GoalMeta { verb: "执行", keys: &["command"], style: GoalStyle::Truncate, fallback: Some("执行命令") }),
sig: Some(SigMeta { groups: &[(&["command"], SigValKind::Str)] }),
..Default::default()
},
schema: object_schema(vec![
("command", "string", true),
("working_dir", "string", false),
+7 -1
View File
@@ -23,7 +23,7 @@
use std::sync::Arc;
use df_ai::ai_tools::{AiToolRegistry, RiskLevel};
use df_ai::ai_tools::{AiToolRegistry, RiskLevel, SigMeta, SigValKind, ToolMeta};
use df_ai::declare_tool;
use tokio::sync::RwLock;
@@ -65,6 +65,12 @@ pub fn register(registry: &mut AiToolRegistry, allowed_dirs: &Arc<RwLock<Allowed
"http_request",
"发起结构化 HTTP 请求(GET/POST/PUT/PATCH/DELETE),用于查询外部 API。参数:method(默认 GET)、url(http/https)、headers(对象 map)、body(请求体字符串)、timeout_secs(默认 30,上限 60)、parse(json/text/auto,默认 auto)、output_file(响应落盘路径,适合大响应,全量写不截断)、max_response_chars(body 截断阈值字节,默认 50KB,clamp [1KB,10MB])。安全:仅 http/https 协议,拒绝私网/保留 IP(SSRF 防护含 DNS resolve 后校验防重绑定),重定向≤3 跳且每跳重校验。响应 body 默认截断 50KB(可 max_response_chars 覆盖或 output_file 落盘)。GET 为只读但触发外发网络,POST/PUT/PATCH/DELETE 有副作用,统一按高风险须人工批准",
RiskLevel::High,
meta: ToolMeta {
approval_hint: Some(("HTTP 请求:{} {}", &["method", "url"])),
display_verb: Some("发起 HTTP 请求"),
sig: Some(SigMeta { groups: &[(&["url"], SigValKind::Str), (&["method"], SigValKind::Str)] }),
..Default::default()
},
schema: schema,
args => {
// allowed_dirs read lock clone(output_file 路径校验用;无 output_file 时校验不触发)
+18 -1
View File
@@ -18,7 +18,7 @@
use std::sync::Arc;
use df_ai::ai_tools::{object_schema, AiToolRegistry, RiskLevel};
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;
@@ -61,6 +61,13 @@ pub fn register(registry: &mut AiToolRegistry, 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),
@@ -99,6 +106,11 @@ pub fn register(registry: &mut AiToolRegistry, 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"))?;
@@ -124,6 +136,11 @@ pub fn register(registry: &mut AiToolRegistry, 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"))?;
+1
View File
@@ -26,3 +26,4 @@ pub mod trash;
pub mod file;
pub mod download_file;
pub mod entity_resolve;
pub mod read_skill;
+38 -1
View File
@@ -17,7 +17,7 @@
use std::sync::Arc;
use df_ai::ai_tools::{object_schema, AiToolRegistry, RiskLevel};
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::ProjectRecord;
@@ -42,6 +42,12 @@ pub fn register(registry: &mut AiToolRegistry, db: &Arc<Database>) {
"update_project",
"更新项目的指定字段(name/status/description/path/stack),需要提供项目 ID、字段名和新值。绑定代码目录推荐改用 bind_directory",
RiskLevel::Medium,
meta: ToolMeta {
entity_action: Some(("project", "update")),
approval_hint: Some(("修改项目{}字段「{}」", &["id", "field"])),
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"))?;
@@ -69,6 +75,13 @@ pub fn register(registry: &mut AiToolRegistry, db: &Arc<Database>) {
"create_project",
"创建新项目,可选传 path/stack 一步完成创建+绑定目录(无需再调 bind_directory)",
RiskLevel::Medium,
meta: ToolMeta {
entity_action: Some(("project", "create")),
approval_hint: Some(("创建项目:「{}」", &["name"])),
display_verb: Some("创建项目"),
goal: Some(GoalMeta { verb: "创建项目", keys: &["name"], style: GoalStyle::Raw, fallback: None }),
..Default::default()
},
schema: object_schema(vec![
("name", "string", true), ("description", "string", false),
("path", "string", false), ("stack", "string", false),
@@ -104,6 +117,12 @@ pub fn register(registry: &mut AiToolRegistry, db: &Arc<Database>) {
"bind_directory",
"为项目绑定代码目录(自动探测技术栈,防重复绑定)。path 应为**项目根路径**(单仓库=仓库根;多工程 monorepo=工作区根,非单个子工程路径;子工程信息走 list_project_modules/update_project_module 维护)。多次调用会覆盖 path 字段,monorepo 场景勿为每个子工程重复绑定。可选 stack 手动指定技术栈(如 'Go / Gin / Vue3'),不传则自动探测。",
RiskLevel::Medium,
meta: ToolMeta {
entity_action: Some(("project", "update")),
approval_hint: Some(("绑定目录:{}(项目{})", &["path", "id"])),
display_verb: Some("绑定目录"),
..Default::default()
},
schema: object_schema(vec![("id", "string", true), ("path", "string", true), ("stack", "string", false)]),
args => {
let id = args["id"].as_str().ok_or_else(|| anyhow::anyhow!("缺少 id 参数"))?;
@@ -126,6 +145,12 @@ pub fn register(registry: &mut AiToolRegistry, db: &Arc<Database>) {
"delete_project",
"删除项目(移入回收站,可恢复。永久删除用 purge_project)",
RiskLevel::High,
meta: ToolMeta {
entity_action: Some(("project", "delete")),
approval_hint: Some(("删除项目{}", &["id"])),
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"))?;
@@ -142,6 +167,12 @@ pub fn register(registry: &mut AiToolRegistry, db: &Arc<Database>) {
"restore_project",
"从回收站恢复已删除项目",
RiskLevel::High,
meta: ToolMeta {
entity_action: Some(("project", "update")),
approval_hint: Some(("从回收站恢复项目{}", &["id"])),
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"))?;
@@ -158,6 +189,12 @@ pub fn register(registry: &mut AiToolRegistry, db: &Arc<Database>) {
"purge_project",
"永久删除项目及关联数据(不可恢复)",
RiskLevel::High,
meta: ToolMeta {
entity_action: Some(("project", "delete")),
approval_hint: Some(("永久删除项目及关联数据,不可恢复{}", &["id"])),
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"))?;
@@ -0,0 +1,48 @@
//! read_skill AI 工具声明式注册(按名读本机 Claude 技能规程正文,只读)。
//!
//! 工具职责:LLM 主动调 read_skill(name) → 查进程内技能缓存(含项目级,PROJECT_SKILLS_ROOT)
//! 命中后读 SKILL.md 剥 frontmatter 正文返回(含 source 来源标注);未命中返回明确错误文案
//! (LLM 可据此向用户澄清技能名,不静默空转)。
//!
//! 风险:Low(纯只读,读 ~/.claude 与项目 .claude 下 SKILL.md,无写副作用)。
//! handler 直调 skills.rs 的 `read_skill_content_stripped` / `skills_cached`(注入链同一
//! 真相源,与 / 技能 SkillResolver 读到相同内容)。
use std::sync::Arc;
use df_ai::ai_tools::{object_schema, AiToolRegistry, RiskLevel};
use df_ai::declare_tool;
/// 注册 read_skill 工具到 `$registry`(无外部捕获,读 skills.rs 进程内缓存 + 单文件)。
pub fn register(registry: &mut AiToolRegistry) {
// 无捕获:占位 Arc<()>(handler 只调 skills.rs 缓存/读文件,不持外部状态)。
let dummy: Arc<()> = Arc::new(());
// schema:name 必填(技能名,如 "review" / "swarm")。
let schema = object_schema(vec![("name", "string", true)]);
declare_tool!(
registry,
dummy: Arc<()>,
"read_skill",
"按名称读取本机已安装的 Claude 技能规程(SKILL.md)完整正文,只读查阅,不执行技能。参数 name 为技能名。命中返回 {name, source, content}(source 为来源 skill/project/command/plugin,content 已剥离 frontmatter);未命中返回明确错误。当用户 / 技能调用失败、用户提到某技能名、或你判断某技能与本任务相关时调用本工具读取其完整规程内容",
RiskLevel::Low,
schema: schema,
args => {
let name = args["name"].as_str().unwrap_or("").trim().to_string();
if name.is_empty() {
return Err(anyhow::anyhow!("参数 name 不能为空:请传入技能名,如 {{\"name\": \"review\"}}"));
}
let content = crate::commands::ai::skills::read_skill_content_stripped(name.clone())
.await
.ok_or_else(|| anyhow::anyhow!(
"技能 \"{name}\" 未找到(未安装/名称不匹配/user_invocable:false)。请核对技能名后重试,或请用户确认该技能是否已安装",
))?;
let source = crate::commands::ai::skills::skills_cached()
.await
.into_iter()
.find(|s| s.name == name)
.map(|s| s.source)
.unwrap_or_else(|| "skill".to_string());
Ok(serde_json::json!({ "name": name, "source": source, "content": content }))
}
);
}
+23 -1
View File
@@ -16,7 +16,7 @@
use std::sync::Arc;
use df_ai::ai_tools::{object_schema, AiToolRegistry, RiskLevel};
use df_ai::ai_tools::{object_schema, AiToolRegistry, RiskLevel, ToolMeta};
use df_ai::declare_tool;
use df_storage::db::Database;
use df_storage::models::TaskRecord;
@@ -67,6 +67,12 @@ pub fn register(registry: &mut AiToolRegistry, db: &Arc<Database>) {
"create_task",
"在指定项目下创建新任务,可选传 idea_id 关联灵感(1对1 单向)。知识图谱 Phase 1 扩展三个可选参数:queue(管理维度池 backlog/todo/decision,默认 todo,新建不可直接落 active/done 须经 move_task_queue 流转)、parent_id(父任务 ID,限 1 级嵌套,父任务自身不能有 parent_id,IPC 层校验防孙任务)、content_json(结构化需求规格 JSON 字符串 {background,acceptance_criteria[],scope[],technical_design},须合法 JSON)",
RiskLevel::Medium,
meta: ToolMeta {
entity_action: Some(("task", "create")),
approval_hint: Some(("创建任务:{}(项目{})", &["title", "project_id"])),
display_verb: Some("创建任务"),
..Default::default()
},
schema: object_schema(vec![
("project_id", "string", true), ("title", "string", true),
("description", "string", false), ("priority", "integer", false),
@@ -129,6 +135,11 @@ pub fn register(registry: &mut AiToolRegistry, db: &Arc<Database>) {
"update_task",
"更新任务的指定字段(title/description/priority/assignee 等),需要提供任务 ID、字段名和新值。注意:status 改动须走 advance_task 工具(状态机推进,8 态 todo/in_progress/in_review/testing/blocked/done/cancelled/deferred,按 target_status 推进不可跳跃),本工具不接受 status 字段(防绕过状态机)",
RiskLevel::Medium,
meta: ToolMeta {
entity_action: Some(("task", "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"))?;
@@ -168,6 +179,12 @@ pub fn register(registry: &mut AiToolRegistry, db: &Arc<Database>) {
"advance_task",
"推进任务状态,必须走状态机(8 态: todo/in_progress/in_review/testing/blocked/done/cancelled/deferred),按 target_status 推进,不可跳跃(非法转换会被状态机拦截)。退回转换(in_review→in_progress 等)自动累加 review_rounds。返回推进后的最新 TaskRecord",
RiskLevel::Medium,
// display_verb 刻意不设:轻量标签缺位让审批文案走模板(含 target_status 详情),与旧表一致
meta: ToolMeta {
entity_action: Some(("task", "update")),
approval_hint: Some(("推进任务状态:{} → {}", &["id", "target_status"])),
..Default::default()
},
schema: object_schema(vec![("id", "string", true), ("target_status", "string", true)]),
args => {
let id = args["id"].as_str().ok_or_else(|| anyhow::anyhow!("缺少 id"))?;
@@ -211,6 +228,11 @@ pub fn register(registry: &mut AiToolRegistry, db: &Arc<Database>) {
"delete_task",
"删除任务(移入回收站,可恢复)",
RiskLevel::High,
meta: ToolMeta {
entity_action: Some(("task", "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"))?;
+7 -1
View File
@@ -15,7 +15,7 @@
use std::sync::Arc;
use df_ai::ai_tools::{object_schema, AiToolRegistry, RiskLevel};
use df_ai::ai_tools::{object_schema, AiToolRegistry, GoalMeta, GoalStyle, RiskLevel, ToolMeta};
use df_ai::declare_tool;
/// 注册 run_workflow 工具到 `$registry`(无 db 捕获,防御兜底)。
@@ -35,6 +35,12 @@ pub fn register(registry: &mut AiToolRegistry) {
"run_workflow",
"按任务 target_status 推进对应工作流(含 AiNode 自审 / HumanNode 核对闸门)。参数 task_id + target_status 同时提供才联动任务推进(完成后按 target_status 推进任务,失败按退回态回滚)。属高风险操作(触发工作流引擎执行),须人工批准。审批通过后由后端直接执行工作流引擎并联动推进任务,返回 execution_id",
RiskLevel::High,
meta: ToolMeta {
approval_hint: Some(("触发工作流:任务{} 推进到 {}", &["task_id", "target_status"])),
display_verb: Some("触发工作流"),
goal: Some(GoalMeta { verb: "执行工作流", keys: &[], style: GoalStyle::Raw, fallback: Some("执行工作流") }),
..Default::default()
},
schema: object_schema(vec![("task_id", "string", true), ("target_status", "string", true)]),
args => {
let task_id = args["task_id"].as_str().ok_or_else(|| anyhow::anyhow!("缺少 task_id"))?;