新增: Git 只读 AI 工具(状态/改动/历史)

AI 现在能自主查看工程代码仓库的 Git 状态,无需用户审批。
三个低风险只读工具,在工程目录(module.path)执行:

- git_status:查看工作区状态,返回当前分支和改动文件列表
  (结构化输出,每文件含路径和状态标记 M/A/D/??)
- git_diff:查看未提交的代码改动,返回统计和 patch 内容
  (patch 截断到 8000 字符防止 token 爆炸)
- git_log:查看提交历史,返回最近提交的结构化信息
  (哈希/作者/消息/日期,默认最近 20 条,最多 100 条)

所有 git 命令 10 秒超时,无 Git 仓库返回空状态。
AI 据此了解代码变更情况,为后续提交/分支操作做准备。
This commit is contained in:
2026-06-29 01:51:38 +08:00
parent 50aad375eb
commit 7b5b62d3ae

View File

@@ -515,6 +515,7 @@ fn register_data_tools(registry: &mut AiToolRegistry, db: &Arc<Database>) {
register_workflow_tools(registry);
register_idea_tools(registry, db);
register_trash_tools(registry, db);
register_git_tools(registry, db);
}
/// 项目类 AI 工具注册(8 个:CRUD + 目录绑定 + 探总量)——持 db:Arc<Database>。
@@ -1303,6 +1304,174 @@ fn register_task_graph_tools(registry: &mut AiToolRegistry, db: &Arc<Database>)
);
}
// ============================================================
// 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<String> = 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<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 工具注册(3 个:status/diff/log,均 Low 风险自动执行)。
///
/// AI 据此自主查看工程代码仓库状态,无需用户审批。均在工程目录(module.path)执行,
/// 无 .git 返回空状态。
fn register_git_tools(registry: &mut AiToolRegistry, db: &Arc<Database>) {
// ── 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 代码智能)