新增: Git 写操作 AI 工具(提交/分支/合并)

AI 现在能操作代码仓库的提交、分支和合并,闭环任务推进链。
三个工具按风险分级审批:

- git_commit(中等风险):提交工作区改动
  提交前检查敏感文件(.env/.key/.pem 等),拒绝提交
  支持自定义提交信息和选择性暂存
- git_branch(中等风险):分支管理
  列表所有分支、创建新分支、切换分支
- git_merge(高风险):合并分支
  合并后自动检测冲突,返回冲突文件列表

安全边界:不暴露 push/force/reset --hard(推送远程是
用户决策,避免 AI 自主推到远程或丢失改动)。
This commit is contained in:
2026-06-29 01:55:01 +08:00
parent 7b5b62d3ae
commit 0cfea6da22

View File

@@ -1470,6 +1470,148 @@ fn register_git_tools(registry: &mut AiToolRegistry, db: &Arc<Database>) {
}) })
})}, })},
); );
// ── 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<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 审批) ──
// 合并分支。高风险(可能产生冲突,影响代码完整性)。
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<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,
}))
}
})
})},
);
} }
/// 抽自 register_data_tools(SMELL-P0-2 续拆),【原样移入】,零行为变更。 /// 抽自 register_data_tools(SMELL-P0-2 续拆),【原样移入】,零行为变更。
@@ -3196,6 +3338,8 @@ mod tests {
// register_task_graph_tools 9→10) // register_task_graph_tools 9→10)
// Git 只读工具(2026-06-29): data 层 28→31(新增 git_status/git_diff/git_log, // Git 只读工具(2026-06-29): data 层 28→31(新增 git_status/git_diff/git_log,
// register_git_tools 3 个) // register_git_tools 3 个)
// Git 写工具(2026-06-29): data 层 31→34(新增 git_commit/git_branch/git_merge,
// register_git_tools 3→6)
assert_eq!( assert_eq!(
registry.len(), registry.len(),
41, 41,
@@ -3225,6 +3369,8 @@ mod tests {
"list_project_modules", "list_project_modules",
// Git 只读工具 // Git 只读工具
"git_status", "git_diff", "git_log", "git_status", "git_diff", "git_log",
// Git 写工具
"git_commit", "git_branch", "git_merge",
// ── file 层 (13) ──run_command 注册顺序已移至末位降低 LLM 偏好, // ── file 层 (13) ──run_command 注册顺序已移至末位降低 LLM 偏好,
// 集合断言经 sort 后与顺序无关,仅守护工具名不漂移。grep 新增 F-260621; // 集合断言经 sort 后与顺序无关,仅守护工具名不漂移。grep 新增 F-260621;
// detect_environment 新增 L1 环境感知 设计 §2.1;read_symbol 新增 AST 代码智能) // detect_environment 新增 L1 环境感知 设计 §2.1;read_symbol 新增 AST 代码智能)