From 7b5b62d3aee3d490e653a707b870e5852f64273d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BB=9D=E5=B0=98?= <237809796@qq.com> Date: Mon, 29 Jun 2026 01:51:38 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E:=20Git=20=E5=8F=AA=E8=AF=BB?= =?UTF-8?q?=20AI=20=E5=B7=A5=E5=85=B7(=E7=8A=B6=E6=80=81/=E6=94=B9?= =?UTF-8?q?=E5=8A=A8/=E5=8E=86=E5=8F=B2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AI 现在能自主查看工程代码仓库的 Git 状态,无需用户审批。 三个低风险只读工具,在工程目录(module.path)执行: - git_status:查看工作区状态,返回当前分支和改动文件列表 (结构化输出,每文件含路径和状态标记 M/A/D/??) - git_diff:查看未提交的代码改动,返回统计和 patch 内容 (patch 截断到 8000 字符防止 token 爆炸) - git_log:查看提交历史,返回最近提交的结构化信息 (哈希/作者/消息/日期,默认最近 20 条,最多 100 条) 所有 git 命令 10 秒超时,无 Git 仓库返回空状态。 AI 据此了解代码变更情况,为后续提交/分支操作做准备。 --- src-tauri/src/commands/ai/tool_registry.rs | 173 +++++++++++++++++++++ 1 file changed, 173 insertions(+) diff --git a/src-tauri/src/commands/ai/tool_registry.rs b/src-tauri/src/commands/ai/tool_registry.rs index fa5a186..bd77951 100644 --- a/src-tauri/src/commands/ai/tool_registry.rs +++ b/src-tauri/src/commands/ai/tool_registry.rs @@ -515,6 +515,7 @@ fn register_data_tools(registry: &mut AiToolRegistry, db: &Arc) { register_workflow_tools(registry); register_idea_tools(registry, db); register_trash_tools(registry, db); + register_git_tools(registry, db); } /// 项目类 AI 工具注册(8 个:CRUD + 目录绑定 + 探总量)——持 db:Arc。 @@ -1303,6 +1304,174 @@ fn register_task_graph_tools(registry: &mut AiToolRegistry, db: &Arc) ); } +// ============================================================ +// Git 只读 AI 工具(工程系统,2026-06-29) +// ============================================================ + +/// 在指定目录执行 git 命令(10s 超时,返回 stdout)。失败返回空字符串(非崩溃)。 +async fn exec_git(working_dir: &str, args: &[&str]) -> String { + let dir = working_dir.to_string(); + let args_vec: Vec = args.iter().map(|s| s.to_string()).collect(); + let result = tokio::task::spawn_blocking(move || { + let mut cmd = std::process::Command::new("git"); + cmd.args(&args_vec).current_dir(&dir); + cmd.stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::null()); + match cmd.output() { + Ok(out) => String::from_utf8_lossy(&out.stdout).to_string(), + Err(_) => String::new(), + } + }) + .await; + result.unwrap_or_default() +} + +/// git status --porcelain 解析为结构化文件列表。 +/// 返回 (当前分支, 改动文件列表 [{path, status}]) +async fn run_git_status(working_dir: &str) -> (String, Vec) { + 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 = raw + .lines() + .filter(|l| !l.is_empty()) + .map(|line| { + // porcelain 格式:"XY filename",XY 为两位状态码 + let status = line.chars().take(2).collect::(); + 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 { + 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 工具注册(3 个:status/diff/log,均 Low 风险自动执行)。 +/// +/// AI 据此自主查看工程代码仓库状态,无需用户审批。均在工程目录(module.path)执行, +/// 无 .git 返回空状态。 +fn register_git_tools(registry: &mut AiToolRegistry, db: &Arc) { + // ── git_status (Low 只读) ── + registry.register( + "git_status", "查看工程 Git 工作区状态。参数:module_id(工程 ID)。返回当前分支、改动文件列表(每个文件含路径+状态 M/A/D/??)、改动总数。只读无副作用", + df_ai::ai_tools::object_schema(vec![ + ("module_id", "string", true), + ]), + RiskLevel::Low, + { 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 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 只读) ── + registry.register( + "git_diff", "查看工程未提交的代码改动。参数:module_id(工程 ID)、staged(可选 bool,仅看已暂存改动,默认 false=全部含未暂存)。返回改动统计 + patch 内容(截断防 token 爆)。只读无副作用", + df_ai::ai_tools::object_schema(vec![ + ("module_id", "string", true), + ("staged", "boolean", false), + ]), + RiskLevel::Low, + { 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 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 只读) ── + registry.register( + "git_log", "查看工程 Git 提交历史。参数:module_id(工程 ID)、limit(可选 int,最近 N 条提交,默认 20,最大 100)。返回提交列表(每个含哈希/作者/消息/日期)。只读无副作用", + df_ai::ai_tools::object_schema(vec![ + ("module_id", "string", true), + ("limit", "integer", false), + ]), + RiskLevel::Low, + { 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 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 })) + }) + })}, + ); +} + /// 抽自 register_data_tools(SMELL-P0-2 续拆),【原样移入】,零行为变更。 fn register_workflow_tools(registry: &mut AiToolRegistry) { // F-260616-07 阶段3(子项2):run_workflow AI 工具注册实装。 @@ -3025,6 +3194,8 @@ mod tests { // 项目基础设施配置 D10 不存凭证,register_task_graph_tools 7→9)。 // 工程系统(2026-06-29): data 层 27→28(新增 list_project_modules 工程列表查询, // register_task_graph_tools 9→10) + // Git 只读工具(2026-06-29): data 层 28→31(新增 git_status/git_diff/git_log, + // register_git_tools 3 个) assert_eq!( registry.len(), 41, @@ -3052,6 +3223,8 @@ mod tests { "add_project_service", "list_project_services", // 工程系统:项目工程列表查询 "list_project_modules", + // Git 只读工具 + "git_status", "git_diff", "git_log", // ── file 层 (13) ──(run_command 注册顺序已移至末位降低 LLM 偏好, // 集合断言经 sort 后与顺序无关,仅守护工具名不漂移。grep 新增 F-260621; // detect_environment 新增 L1 环境感知 设计 §2.1;read_symbol 新增 AST 代码智能)