重构: 后端编排拆解与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:
@@ -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 转换 + 去噪音 + 截断全在那)
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,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"))?;
|
||||
|
||||
@@ -26,3 +26,4 @@ pub mod trash;
|
||||
pub mod file;
|
||||
pub mod download_file;
|
||||
pub mod entity_resolve;
|
||||
pub mod read_skill;
|
||||
|
||||
@@ -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 }))
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -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"))?;
|
||||
|
||||
@@ -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"))?;
|
||||
|
||||
Reference in New Issue
Block a user