重构: tool_registry拆分及多批改进
This commit is contained in:
@@ -22,6 +22,8 @@ const MAX_LIST_RESULTS: usize = 50;
|
||||
/// run_command 默认超时(秒)。LLM 可在 args timeout_secs 覆盖此默认值。
|
||||
/// 提取为常量便于在超时标注处引用同一来源(F-260616-04)。
|
||||
const DEFAULT_RUN_COMMAND_TIMEOUT_SECS: u64 = 60;
|
||||
/// run_command 超时上限(秒):防 LLM 传超大 timeout_secs 冻结会话(允许长构建,封顶 10min)
|
||||
const MAX_RUN_COMMAND_TIMEOUT_SECS: u64 = 600;
|
||||
|
||||
/// 生成行级 unified diff(无外部依赖,基于 LCS)。
|
||||
/// 仅标 +/- 前缀,不做 hunk header(足够审批卡/审计留痕可读)。
|
||||
@@ -358,11 +360,26 @@ async fn bind_dir_to_project(
|
||||
|
||||
/// 构建 AI 工具注册表 — handler 即唯一执行路径(schema+risk+实现同源,消除双轨)
|
||||
///
|
||||
/// CRUD 工具闭包捕获 `db` Arc 重建 Repo;文件系统工具复用 resolve_workspace_path /
|
||||
/// list_dir_recursive。新增工具只改这里一处,定义与实现同源,编译期保证一致。
|
||||
/// SMELL-P0-2 拆分:原 1091 行单函数拆为两层注册。
|
||||
/// - data 层(register_data_tools):18 个持 db 的 CRUD/状态机/工作流工具,闭包捕获 db Arc 重建 Repo。
|
||||
/// - file 层(register_file_tools):10 个纯文件系统工具,不持 db。
|
||||
/// 新增工具改对应 register_* 一处,定义与实现同源,编译期保证一致。
|
||||
pub fn build_ai_tool_registry(db: &Arc<Database>) -> AiToolRegistry {
|
||||
let mut registry = AiToolRegistry::new();
|
||||
register_data_tools(&mut registry, db);
|
||||
register_file_tools(&mut registry);
|
||||
registry
|
||||
}
|
||||
|
||||
/// 数据层 AI 工具注册(18 个持 db 的 CRUD/状态机/工作流工具)——从 build_ai_tool_registry 抽出。
|
||||
///
|
||||
/// 工具闭包捕获 `db: &Arc<Database>` Arc 重建 Repo(列表/创建/更新/删除/状态推进/工作流)。
|
||||
/// 例外:run_workflow handler 防御返回 Err(CR-52),不持 db 不 clone,真正执行经
|
||||
/// ai_approve → run_workflow_inner 另走完整 State 路径。
|
||||
///
|
||||
/// SMELL-P0-2:抽自原 build_ai_tool_registry 1091 行单函数(数据+文件混合)。
|
||||
/// 18 个 register 调用【原样移入】,零行为变更,仅机械搬运。
|
||||
fn register_data_tools(registry: &mut AiToolRegistry, db: &Arc<Database>) {
|
||||
// ── 只读 (Low) ──
|
||||
registry.register(
|
||||
"list_projects", "列出所有项目,支持 offset/limit 分页。返回 items(项目列表)、total(总量)、has_more(是否有更多页)。默认 limit=50",
|
||||
@@ -713,6 +730,41 @@ pub fn build_ai_tool_registry(db: &Arc<Database>) -> AiToolRegistry {
|
||||
})
|
||||
})},
|
||||
);
|
||||
|
||||
// ── 探总量工具 (Low risk, F-260616-08) ──
|
||||
registry.register(
|
||||
"get_project_count", "获取项目总数(未删除项目),用于分页策略判断。返回 { total: usize }",
|
||||
df_ai::ai_tools::object_schema(vec![]), RiskLevel::Low,
|
||||
{ let db = db.clone(); Box::new(move |_args: serde_json::Value| {
|
||||
let db = db.clone();
|
||||
Box::pin(async move {
|
||||
let repo = df_storage::crud::ProjectRepo::new(&db);
|
||||
let items = repo.list_active().await?;
|
||||
Ok(serde_json::json!({ "total": items.len() }))
|
||||
})
|
||||
})},
|
||||
);
|
||||
registry.register(
|
||||
"get_task_count", "获取任务总数(未删除任务),用于分页策略判断。返回 { total: usize }",
|
||||
df_ai::ai_tools::object_schema(vec![("project_id", "string", false)]), RiskLevel::Low,
|
||||
{ let db = db.clone(); Box::new(move |args: serde_json::Value| {
|
||||
let db = db.clone();
|
||||
Box::pin(async move {
|
||||
let repo = df_storage::crud::TaskRepo::new(&db);
|
||||
let tasks = if let Some(pid) = args.get("project_id").and_then(|v| v.as_str()) {
|
||||
repo.query("project_id", pid).await?
|
||||
} else {
|
||||
repo.list_all().await?
|
||||
};
|
||||
Ok(serde_json::json!({ "total": tasks.len() }))
|
||||
})
|
||||
})},
|
||||
);
|
||||
}
|
||||
|
||||
/// 文件系统类 AI 工具注册(命令执行/读/列/写/改/元/追加/删/移/搜)——不持 db,纯文件系统操作。
|
||||
/// 从 build_ai_tool_registry 抽出(SMELL-P0-2:1091行单函数拆分,build 1090→~390 行)。
|
||||
fn register_file_tools(registry: &mut AiToolRegistry) {
|
||||
registry.register(
|
||||
"run_command", "在指定工作目录执行 shell 命令(跑测试/构建/查看运行结果),返回 stdout/stderr/exit_code。高风险,须人工批准。命令需自包含(非交互式,避免需用户输入的程序)。默认超时 60 秒。用于验证刚写入的代码能否运行、跑测试、看报错迭代修改。",
|
||||
df_ai::ai_tools::object_schema(vec![
|
||||
@@ -736,7 +788,8 @@ pub fn build_ai_tool_registry(db: &Arc<Database>) -> AiToolRegistry {
|
||||
None => workspace_root().to_string_lossy().to_string(),
|
||||
};
|
||||
// timeout 默认 60s:防 hang(交互式命令/死循环/大构建),LLM 可通过 args timeout_secs 覆盖
|
||||
let timeout_secs = args["timeout_secs"].as_u64().unwrap_or(DEFAULT_RUN_COMMAND_TIMEOUT_SECS);
|
||||
// clamp 封顶 MAX:防 LLM 传超大 timeout_secs 冻结会话(需更长命令应拆分而非无限等)
|
||||
let timeout_secs = args["timeout_secs"].as_u64().unwrap_or(DEFAULT_RUN_COMMAND_TIMEOUT_SECS).min(MAX_RUN_COMMAND_TIMEOUT_SECS);
|
||||
|
||||
let request = ShellRequest {
|
||||
command: command.to_string(),
|
||||
@@ -1418,38 +1471,6 @@ pub fn build_ai_tool_registry(db: &Arc<Database>) -> AiToolRegistry {
|
||||
Ok(serde_json::json!({ "path": path, "pattern": pattern, "results": page_results, "total": total, "has_more": has_more }))
|
||||
})),
|
||||
);
|
||||
|
||||
// ── 探总量工具 (Low risk, F-260616-08) ──
|
||||
registry.register(
|
||||
"get_project_count", "获取项目总数(未删除项目),用于分页策略判断。返回 { total: usize }",
|
||||
df_ai::ai_tools::object_schema(vec![]), RiskLevel::Low,
|
||||
{ let db = db.clone(); Box::new(move |_args: serde_json::Value| {
|
||||
let db = db.clone();
|
||||
Box::pin(async move {
|
||||
let repo = df_storage::crud::ProjectRepo::new(&db);
|
||||
let items = repo.list_active().await?;
|
||||
Ok(serde_json::json!({ "total": items.len() }))
|
||||
})
|
||||
})},
|
||||
);
|
||||
registry.register(
|
||||
"get_task_count", "获取任务总数(未删除任务),用于分页策略判断。返回 { total: usize }",
|
||||
df_ai::ai_tools::object_schema(vec![("project_id", "string", false)]), RiskLevel::Low,
|
||||
{ let db = db.clone(); Box::new(move |args: serde_json::Value| {
|
||||
let db = db.clone();
|
||||
Box::pin(async move {
|
||||
let repo = df_storage::crud::TaskRepo::new(&db);
|
||||
let tasks = if let Some(pid) = args.get("project_id").and_then(|v| v.as_str()) {
|
||||
repo.query("project_id", pid).await?
|
||||
} else {
|
||||
repo.list_all().await?
|
||||
};
|
||||
Ok(serde_json::json!({ "total": tasks.len() }))
|
||||
})
|
||||
})},
|
||||
);
|
||||
|
||||
registry
|
||||
}
|
||||
|
||||
/// 递归列出目录内容(最多 max_depth 层,最多 max_entries 条)
|
||||
@@ -1625,6 +1646,59 @@ mod tests {
|
||||
use super::*;
|
||||
use std::fs;
|
||||
|
||||
// ============================================================
|
||||
// 工具注册基线测试(SMELL-P0-2 拆分防护)
|
||||
//
|
||||
// 防未来 register_data_tools / register_file_tools 拆分或重构时静默丢工具。
|
||||
// build_ai_tool_registry 经两层 register_* 组装:data(18 持 db) + file(10 不持 db) = 28。
|
||||
// 任一层漏移 register 调用,此测试立即红。工具名集合也断言,防 rename 致 LLM tool 突变。
|
||||
// ============================================================
|
||||
|
||||
/// build_ai_tool_registry 应注册恰好 28 个工具(18 data + 10 file),且工具名集合稳定。
|
||||
///
|
||||
/// 用 in-memory SQLite(Database::open_in_memory 自跑迁移),构造零外部依赖的 db,
|
||||
// 不实际执行任何 handler——仅断言注册阶段的定义完整性,故无需真实数据。
|
||||
#[tokio::test]
|
||||
async fn test_build_ai_tool_registry_baseline_tool_count() {
|
||||
let db = Database::open_in_memory().await.expect("in-memory db 初始化失败");
|
||||
let db = Arc::new(db);
|
||||
let registry = build_ai_tool_registry(&db);
|
||||
|
||||
// 总量基线:28(18 data + 10 file)。拆分前后必须一致。
|
||||
assert_eq!(
|
||||
registry.len(),
|
||||
28,
|
||||
"工具总数应为 28(18 data + 10 file),实际 {}", registry.len()
|
||||
);
|
||||
|
||||
// 工具名集合基线:防 rename / 漏注册 / 误删除。
|
||||
// data 层 18 个(持 db):CRUD/状态机/工作流
|
||||
// file 层 10 个(不持 db):命令/读/列/写/改/元/追加/删/移/搜
|
||||
let mut expected: Vec<&str> = vec![
|
||||
// ── data 层 (18) ──
|
||||
"list_projects", "list_tasks", "list_ideas",
|
||||
"update_project", "create_project", "bind_directory",
|
||||
"create_task", "update_task", "advance_task",
|
||||
"run_workflow", "delete_task", "create_idea",
|
||||
"delete_project", "restore_project", "purge_project",
|
||||
"list_trash", "get_project_count", "get_task_count",
|
||||
// ── file 层 (10) ──
|
||||
"run_command", "read_file", "list_directory",
|
||||
"write_file", "patch_file", "file_info",
|
||||
"append_file", "delete_file", "rename_file", "search_files",
|
||||
];
|
||||
expected.sort_unstable();
|
||||
|
||||
let mut actual: Vec<String> = registry.tool_names();
|
||||
actual.sort();
|
||||
|
||||
assert_eq!(
|
||||
actual,
|
||||
expected.iter().map(|s| s.to_string()).collect::<Vec<_>>(),
|
||||
"工具名集合与基线不一致(可能漏注册或 rename)"
|
||||
);
|
||||
}
|
||||
|
||||
/// is_noise_dir 纯函数:覆盖 .git/.gitignore 区分(目录是噪音,.gitignore 文件名不是)
|
||||
#[test]
|
||||
fn test_is_noise_dir_distinguishes_dir_and_gitignore_file() {
|
||||
|
||||
Reference in New Issue
Block a user