重构: tool_registry 剩余 21 工具迁声明式(task_graph/git/http/workflow/idea/trash)
This commit is contained in:
@@ -0,0 +1,340 @@
|
||||
//! Git 类 AI 工具声明式注册(register_git_tools 6 个迁入)。
|
||||
//!
|
||||
//! 迁自 `tool_registry.rs::register_git_tools`(原 6 个:git_status/git_diff/git_log 只读 +
|
||||
//! git_commit/git_branch/git_merge 写),改用 `declare_tool!` 宏。
|
||||
//!
|
||||
//! 迁移策略(handler 逻辑零变更):
|
||||
//! - handler body 逐字照搬原 `register_git_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 与原手写定义逐字一致。
|
||||
//! - 配套私有 helper(exec_git/run_git_status/run_git_diff/run_git_log)原样搬入本文件,
|
||||
//! 仅 register_git_tools 使用(external module.rs::run_git_status 是同名不同签名的另一函数)。
|
||||
//!
|
||||
//! 等价性验证:基线测试 `test_build_ai_tool_registry_baseline_tool_count` 仍断言 48 总量 +
|
||||
//! 工具名集合稳定(防 rename / 漏注册)。
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use df_ai::ai_tools::{object_schema, AiToolRegistry, RiskLevel};
|
||||
use df_ai::declare_tool;
|
||||
use df_storage::db::Database;
|
||||
|
||||
// ============================================================
|
||||
// Git 私有 helper(原 tool_registry.rs 986-1080 行原样搬入,仅本模块使用)
|
||||
// ============================================================
|
||||
|
||||
/// 在指定目录执行 git 命令(10s 超时,返回 stdout)。失败/超时返回空字符串(非崩溃)。
|
||||
///
|
||||
/// BUG-2026-07-18: 原实现 spawn_blocking 内裸 std::process::Command::output() 无 timeout
|
||||
/// (注释谎称"10s 超时")。git 在 OneDrive/网盘/挂载盘/lfs/大仓库场景会卡数十秒到无限,
|
||||
/// spawn_blocking 线程永不返回 → 累计耗尽 tokio blocking 池 → 间接卡死单线程 runtime
|
||||
/// (与 env_snapshot::probe_version 同型病根)。AI 的 git_status/log/diff 工具在会话内高频触发。
|
||||
///
|
||||
/// 改用 tokio::process + tokio::time::timeout(10s) + kill_on_drop:超时 drop 时 child 进程
|
||||
/// 被 kill,不泄漏线程/进程,对齐 shell.rs execute 同源封装。
|
||||
async fn exec_git(working_dir: &str, args: &[&str]) -> String {
|
||||
let mut cmd = tokio::process::Command::new("git");
|
||||
cmd.args(args)
|
||||
.current_dir(working_dir)
|
||||
.stdout(std::process::Stdio::piped())
|
||||
.stderr(std::process::Stdio::null())
|
||||
.kill_on_drop(true);
|
||||
match tokio::time::timeout(std::time::Duration::from_secs(10), cmd.output()).await {
|
||||
Ok(Ok(out)) => String::from_utf8_lossy(&out.stdout).to_string(),
|
||||
Ok(Err(_)) => String::new(),
|
||||
Err(_elapsed) => String::new(), // 超时:child 被 kill_on_drop 终止
|
||||
}
|
||||
}
|
||||
|
||||
/// git status --porcelain 解析为结构化文件列表。
|
||||
/// 返回 (当前分支, 改动文件列表 [{path, status}])
|
||||
async fn run_git_status(working_dir: &str) -> (String, Vec<serde_json::Value>) {
|
||||
let branch = exec_git(working_dir, &["branch", "--show-current"]).await;
|
||||
let branch = branch.trim().to_string();
|
||||
let raw = exec_git(working_dir, &["status", "--porcelain"]).await;
|
||||
let files: Vec<serde_json::Value> = raw
|
||||
.lines()
|
||||
.filter(|l| !l.is_empty())
|
||||
.map(|line| {
|
||||
// porcelain 格式:"XY filename",XY 为两位状态码
|
||||
let status = line.chars().take(2).collect::<String>();
|
||||
let path = line.get(3..).unwrap_or("").trim().to_string();
|
||||
// 简化状态码:M/A/D/??/R
|
||||
let simple = if status.starts_with("??") { "??" }
|
||||
else if status.contains('A') { "A" }
|
||||
else if status.contains('D') { "D" }
|
||||
else if status.contains('R') { "R" }
|
||||
else { "M" };
|
||||
serde_json::json!({ "path": path, "status": simple })
|
||||
})
|
||||
.collect();
|
||||
(branch, files)
|
||||
}
|
||||
|
||||
/// git diff 解析为结构化文件列表(每文件统计 + patch 截断)。
|
||||
async fn run_git_diff(working_dir: &str, staged: bool) -> serde_json::Value {
|
||||
let mut args = vec!["diff", "--stat"];
|
||||
if staged { args.push("--cached"); }
|
||||
let stat_raw = exec_git(working_dir, &args).await;
|
||||
|
||||
// 每文件 patch(截断防 token 爆)
|
||||
let mut patch_args = vec!["diff" ];
|
||||
if staged { patch_args.push("--cached"); }
|
||||
let patch_raw = exec_git(working_dir, &patch_args).await;
|
||||
// 截断到 8000 字符(防大体量 diff)
|
||||
let patch_truncated = if patch_raw.len() > 8000 {
|
||||
format!("{}\n... (diff 截断,共 {} 字符)", &patch_raw[..8000], patch_raw.len())
|
||||
} else {
|
||||
patch_raw
|
||||
};
|
||||
|
||||
serde_json::json!({
|
||||
"stat": stat_raw,
|
||||
"patch": patch_truncated,
|
||||
})
|
||||
}
|
||||
|
||||
/// git log 解析为结构化提交列表。
|
||||
async fn run_git_log(working_dir: &str, limit: usize) -> Vec<serde_json::Value> {
|
||||
let format = "%H|%an|%ad|%s";
|
||||
let limit_str = format!("-{}", limit);
|
||||
let raw = exec_git(working_dir, &["log", "--oneline", &format!("--format={}", format), &limit_str, "--date=short"]).await;
|
||||
raw.lines()
|
||||
.filter(|l| !l.is_empty())
|
||||
.filter_map(|line| {
|
||||
let parts: Vec<&str> = line.splitn(4, '|').collect();
|
||||
if parts.len() == 4 {
|
||||
Some(serde_json::json!({
|
||||
"hash": parts[0],
|
||||
"author": parts[1],
|
||||
"date": parts[2],
|
||||
"message": parts[3],
|
||||
}))
|
||||
} else { None }
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Git AI 工具注册(6 个:status/diff/log 只读 Low + commit/branch 写 Medium + merge High)。
|
||||
///
|
||||
/// 与原手写 register(name, desc, schema, risk, handler) 语义 1:1:
|
||||
/// - name/desc/schema 字符串与 JSON Schema 逐字照搬原定义
|
||||
/// - risk 与原一致(status/diff/log=Low,commit/branch=Medium,merge=High)
|
||||
/// - handler body 与原 async move 块逐字一致(逻辑零变更)
|
||||
///
|
||||
/// 唯一差异:闭包包装改由 `declare_tool!` 宏生成,handler body 直接写业务逻辑。
|
||||
pub fn register(registry: &mut AiToolRegistry, db: &Arc<Database>) {
|
||||
// ── git_status (Low 只读) ──
|
||||
declare_tool!(
|
||||
registry,
|
||||
db: Arc<Database>,
|
||||
"git_status",
|
||||
"查看工程 Git 工作区状态。参数:module_id(工程 ID)。返回当前分支、改动文件列表(每个文件含路径+状态 M/A/D/??)、改动总数。只读无副作用",
|
||||
RiskLevel::Low,
|
||||
schema: object_schema(vec![
|
||||
("module_id", "string", true),
|
||||
]),
|
||||
args => {
|
||||
let module_id = args["module_id"].as_str()
|
||||
.ok_or_else(|| anyhow::anyhow!("缺少 module_id"))?;
|
||||
let repo = df_storage::crud::ProjectModuleRepo::new(&db);
|
||||
let module = repo.get_by_id(module_id).await?
|
||||
.ok_or_else(|| anyhow::anyhow!("工程不存在: {}", module_id))?;
|
||||
let (branch, files) = run_git_status(&module.path).await;
|
||||
Ok(serde_json::json!({
|
||||
"branch": branch,
|
||||
"files": files,
|
||||
"total_changes": files.len(),
|
||||
}))
|
||||
}
|
||||
);
|
||||
|
||||
// ── git_diff (Low 只读) ──
|
||||
declare_tool!(
|
||||
registry,
|
||||
db: Arc<Database>,
|
||||
"git_diff",
|
||||
"查看工程未提交的代码改动。参数:module_id(工程 ID)、staged(可选 bool,仅看已暂存改动,默认 false=全部含未暂存)。返回改动统计 + patch 内容(截断防 token 爆)。只读无副作用",
|
||||
RiskLevel::Low,
|
||||
schema: object_schema(vec![
|
||||
("module_id", "string", true),
|
||||
("staged", "boolean", false),
|
||||
]),
|
||||
args => {
|
||||
let module_id = args["module_id"].as_str()
|
||||
.ok_or_else(|| anyhow::anyhow!("缺少 module_id"))?;
|
||||
let staged = args.get("staged").and_then(|v| v.as_bool()).unwrap_or(false);
|
||||
let repo = df_storage::crud::ProjectModuleRepo::new(&db);
|
||||
let module = repo.get_by_id(module_id).await?
|
||||
.ok_or_else(|| anyhow::anyhow!("工程不存在: {}", module_id))?;
|
||||
let diff = run_git_diff(&module.path, staged).await;
|
||||
Ok(diff)
|
||||
}
|
||||
);
|
||||
|
||||
// ── git_log (Low 只读) ──
|
||||
declare_tool!(
|
||||
registry,
|
||||
db: Arc<Database>,
|
||||
"git_log",
|
||||
"查看工程 Git 提交历史。参数:module_id(工程 ID)、limit(可选 int,最近 N 条提交,默认 20,最大 100)。返回提交列表(每个含哈希/作者/消息/日期)。只读无副作用",
|
||||
RiskLevel::Low,
|
||||
schema: object_schema(vec![
|
||||
("module_id", "string", true),
|
||||
("limit", "integer", false),
|
||||
]),
|
||||
args => {
|
||||
let module_id = args["module_id"].as_str()
|
||||
.ok_or_else(|| anyhow::anyhow!("缺少 module_id"))?;
|
||||
let limit = args.get("limit").and_then(|v| v.as_u64()).unwrap_or(20).min(100) as usize;
|
||||
let repo = df_storage::crud::ProjectModuleRepo::new(&db);
|
||||
let module = repo.get_by_id(module_id).await?
|
||||
.ok_or_else(|| anyhow::anyhow!("工程不存在: {}", module_id))?;
|
||||
let commits = run_git_log(&module.path, limit).await;
|
||||
Ok(serde_json::json!({ "commits": commits }))
|
||||
}
|
||||
);
|
||||
|
||||
// ── git_commit (Medium 审批) ──
|
||||
// 提交工作区改动。需审批(写操作影响代码仓库)。
|
||||
// 安全边界:不提交敏感文件(.env/.key/.pem,由 .gitignore 兜底 + 此处检查)。
|
||||
declare_tool!(
|
||||
registry,
|
||||
db: Arc<Database>,
|
||||
"git_commit",
|
||||
"提交工程工作区改动到本地仓库。参数:module_id(工程 ID)、message(提交信息,必填)、add_all(可选 bool,是否添加全部改动到暂存区,默认 true)。中等风险,需审批。提交前检查不提交敏感文件(.env/.key/.pem)",
|
||||
RiskLevel::Medium,
|
||||
schema: object_schema(vec![
|
||||
("module_id", "string", true),
|
||||
("message", "string", true),
|
||||
("add_all", "boolean", false),
|
||||
]),
|
||||
args => {
|
||||
let module_id = args["module_id"].as_str()
|
||||
.ok_or_else(|| anyhow::anyhow!("缺少 module_id"))?;
|
||||
let message = args["message"].as_str()
|
||||
.ok_or_else(|| anyhow::anyhow!("缺少 message"))?;
|
||||
let add_all = args.get("add_all").and_then(|v| v.as_bool()).unwrap_or(true);
|
||||
if message.trim().is_empty() {
|
||||
anyhow::bail!("提交信息不能为空");
|
||||
}
|
||||
let repo = df_storage::crud::ProjectModuleRepo::new(&db);
|
||||
let module = repo.get_by_id(module_id).await?
|
||||
.ok_or_else(|| anyhow::anyhow!("工程不存在: {}", module_id))?;
|
||||
// 敏感文件检查:提交前查看待提交文件列表,如有 .env/.key/.pem 则拒绝
|
||||
let status_raw = exec_git(&module.path, &["status", "--porcelain"]).await;
|
||||
let sensitive_patterns = [".env", ".key", ".pem", "id_rsa", ".htpasswd"];
|
||||
for line in status_raw.lines() {
|
||||
let path = line.get(3..).unwrap_or("").trim().to_lowercase();
|
||||
if sensitive_patterns.iter().any(|p| path.contains(p)) {
|
||||
anyhow::bail!("检测到敏感文件在待提交列表中: {}。请手动检查 .gitignore 或移除该文件后重试", line.get(3..).unwrap_or(""));
|
||||
}
|
||||
}
|
||||
// git add
|
||||
if add_all {
|
||||
let _ = exec_git(&module.path, &["add", "-A"]).await;
|
||||
}
|
||||
// git commit
|
||||
let commit_out = exec_git(&module.path, &["commit", "-m", message]).await;
|
||||
// 提交后取最新 commit hash
|
||||
let hash = exec_git(&module.path, &["log", "-1", "--format=%H"]).await.trim().to_string();
|
||||
Ok(serde_json::json!({
|
||||
"committed": !hash.is_empty(),
|
||||
"hash": hash,
|
||||
"message": message,
|
||||
"output": commit_out,
|
||||
}))
|
||||
}
|
||||
);
|
||||
|
||||
// ── git_branch (Medium 审批) ──
|
||||
// 分支管理:列表/创建/切换。需审批(影响工作区分支状态)。
|
||||
declare_tool!(
|
||||
registry,
|
||||
db: Arc<Database>,
|
||||
"git_branch",
|
||||
"工程分支管理。参数:module_id(工程 ID)、action(list/create/switch,默认 list)、name(分支名,create/switch 时必填)。中等风险,需审批。list=列出所有分支+当前分支,create=创建新分支,switch=切换分支",
|
||||
RiskLevel::Medium,
|
||||
schema: object_schema(vec![
|
||||
("module_id", "string", true),
|
||||
("action", "string", false),
|
||||
("name", "string", false),
|
||||
]),
|
||||
args => {
|
||||
let module_id = args["module_id"].as_str()
|
||||
.ok_or_else(|| anyhow::anyhow!("缺少 module_id"))?;
|
||||
let action = args.get("action").and_then(|v| v.as_str()).unwrap_or("list");
|
||||
let name = args.get("name").and_then(|v| v.as_str()).unwrap_or("");
|
||||
let repo = df_storage::crud::ProjectModuleRepo::new(&db);
|
||||
let module = repo.get_by_id(module_id).await?
|
||||
.ok_or_else(|| anyhow::anyhow!("工程不存在: {}", module_id))?;
|
||||
match action {
|
||||
"list" => {
|
||||
let raw = exec_git(&module.path, &["branch", "--list"]).await;
|
||||
let current = exec_git(&module.path, &["branch", "--show-current"]).await.trim().to_string();
|
||||
let branches: Vec<String> = raw.lines()
|
||||
.map(|l| l.trim_start_matches("* ").trim().to_string())
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect();
|
||||
Ok(serde_json::json!({ "current": current, "branches": branches }))
|
||||
}
|
||||
"create" => {
|
||||
if name.is_empty() { anyhow::bail!("create 需要 name 参数"); }
|
||||
let _ = exec_git(&module.path, &["branch", name]).await;
|
||||
Ok(serde_json::json!({ "created": name }))
|
||||
}
|
||||
"switch" => {
|
||||
if name.is_empty() { anyhow::bail!("switch 需要 name 参数"); }
|
||||
let _ = exec_git(&module.path, &["checkout", name]).await;
|
||||
let current = exec_git(&module.path, &["branch", "--show-current"]).await.trim().to_string();
|
||||
Ok(serde_json::json!({ "switched_to": current }))
|
||||
}
|
||||
_ => anyhow::bail!("未知 action: {},合法值: list/create/switch", action),
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// ── git_merge (High 审批) ──
|
||||
// 合并分支。高风险(可能产生冲突,影响代码完整性)。
|
||||
declare_tool!(
|
||||
registry,
|
||||
db: Arc<Database>,
|
||||
"git_merge",
|
||||
"合并指定分支到当前分支。参数:module_id(工程 ID)、branch(要合并的分支名,必填)。高风险,需审批。合并冲突时返回冲突文件列表,需用户手动解决",
|
||||
RiskLevel::High,
|
||||
schema: object_schema(vec![
|
||||
("module_id", "string", true),
|
||||
("branch", "string", true),
|
||||
]),
|
||||
args => {
|
||||
let module_id = args["module_id"].as_str()
|
||||
.ok_or_else(|| anyhow::anyhow!("缺少 module_id"))?;
|
||||
let branch = args["branch"].as_str()
|
||||
.ok_or_else(|| anyhow::anyhow!("缺少 branch"))?;
|
||||
let repo = df_storage::crud::ProjectModuleRepo::new(&db);
|
||||
let module = repo.get_by_id(module_id).await?
|
||||
.ok_or_else(|| anyhow::anyhow!("工程不存在: {}", module_id))?;
|
||||
let merge_out = exec_git(&module.path, &["merge", branch]).await;
|
||||
let has_conflict = merge_out.contains("CONFLICT") || merge_out.contains("Merge conflict");
|
||||
if has_conflict {
|
||||
let status_raw = exec_git(&module.path, &["status", "--porcelain"]).await;
|
||||
let conflicts: Vec<String> = status_raw.lines()
|
||||
.filter(|l| l.starts_with("UU") || l.starts_with("AA") || l.starts_with("DD"))
|
||||
.map(|l| l.get(3..).unwrap_or("").trim().to_string())
|
||||
.collect();
|
||||
Ok(serde_json::json!({
|
||||
"merged": false,
|
||||
"conflicts": conflicts,
|
||||
"message": "合并冲突,需手动解决",
|
||||
}))
|
||||
} else {
|
||||
Ok(serde_json::json!({
|
||||
"merged": true,
|
||||
"branch": branch,
|
||||
"output": merge_out,
|
||||
}))
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user