diff --git a/src-tauri/src/commands/ai/tool_registry.rs b/src-tauri/src/commands/ai/tool_registry.rs index bd77951..05fad27 100644 --- a/src-tauri/src/commands/ai/tool_registry.rs +++ b/src-tauri/src/commands/ai/tool_registry.rs @@ -1470,6 +1470,148 @@ fn register_git_tools(registry: &mut AiToolRegistry, db: &Arc) { }) })}, ); + + // ── git_commit (Medium 审批) ── + // 提交工作区改动。需审批(写操作影响代码仓库)。 + // 安全边界:不提交敏感文件(.env/.key/.pem,由 .gitignore 兜底 + 此处检查)。 + registry.register( + "git_commit", "提交工程工作区改动到本地仓库。参数:module_id(工程 ID)、message(提交信息,必填)、add_all(可选 bool,是否添加全部改动到暂存区,默认 true)。中等风险,需审批。提交前检查不提交敏感文件(.env/.key/.pem)", + df_ai::ai_tools::object_schema(vec![ + ("module_id", "string", true), + ("message", "string", true), + ("add_all", "boolean", false), + ]), + RiskLevel::Medium, + { let db = db.clone(); Box::new(move |args: serde_json::Value| { + let db = db.clone(); + Box::pin(async move { + 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 审批) ── + // 分支管理:列表/创建/切换。需审批(影响工作区分支状态)。 + registry.register( + "git_branch", "工程分支管理。参数:module_id(工程 ID)、action(list/create/switch,默认 list)、name(分支名,create/switch 时必填)。中等风险,需审批。list=列出所有分支+当前分支,create=创建新分支,switch=切换分支", + df_ai::ai_tools::object_schema(vec![ + ("module_id", "string", true), + ("action", "string", false), + ("name", "string", false), + ]), + RiskLevel::Medium, + { let db = db.clone(); Box::new(move |args: serde_json::Value| { + let db = db.clone(); + Box::pin(async move { + 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 = 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 审批) ── + // 合并分支。高风险(可能产生冲突,影响代码完整性)。 + registry.register( + "git_merge", "合并指定分支到当前分支。参数:module_id(工程 ID)、branch(要合并的分支名,必填)。高风险,需审批。合并冲突时返回冲突文件列表,需用户手动解决", + df_ai::ai_tools::object_schema(vec![ + ("module_id", "string", true), + ("branch", "string", true), + ]), + RiskLevel::High, + { let db = db.clone(); Box::new(move |args: serde_json::Value| { + let db = db.clone(); + Box::pin(async move { + 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 = 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, + })) + } + }) + })}, + ); } /// 抽自 register_data_tools(SMELL-P0-2 续拆),【原样移入】,零行为变更。 @@ -3196,6 +3338,8 @@ mod tests { // register_task_graph_tools 9→10) // Git 只读工具(2026-06-29): data 层 28→31(新增 git_status/git_diff/git_log, // register_git_tools 3 个) + // Git 写工具(2026-06-29): data 层 31→34(新增 git_commit/git_branch/git_merge, + // register_git_tools 3→6) assert_eq!( registry.len(), 41, @@ -3225,6 +3369,8 @@ mod tests { "list_project_modules", // Git 只读工具 "git_status", "git_diff", "git_log", + // Git 写工具 + "git_commit", "git_branch", "git_merge", // ── file 层 (13) ──(run_command 注册顺序已移至末位降低 LLM 偏好, // 集合断言经 sort 后与顺序无关,仅守护工具名不漂移。grep 新增 F-260621; // detect_environment 新增 L1 环境感知 设计 §2.1;read_symbol 新增 AST 代码智能)