重构: 后端编排拆解与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
+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),