新增: Git 写操作 AI 工具(提交/分支/合并)
AI 现在能操作代码仓库的提交、分支和合并,闭环任务推进链。 三个工具按风险分级审批: - git_commit(中等风险):提交工作区改动 提交前检查敏感文件(.env/.key/.pem 等),拒绝提交 支持自定义提交信息和选择性暂存 - git_branch(中等风险):分支管理 列表所有分支、创建新分支、切换分支 - git_merge(高风险):合并分支 合并后自动检测冲突,返回冲突文件列表 安全边界:不暴露 push/force/reset --hard(推送远程是 用户决策,避免 AI 自主推到远程或丢失改动)。
This commit is contained in:
@@ -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 代码智能)
|
||||||
|
|||||||
Reference in New Issue
Block a user