新增: 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:
@@ -515,6 +515,7 @@ fn register_data_tools(registry: &mut AiToolRegistry, db: &Arc<Database>) {
|
|||||||
register_workflow_tools(registry);
|
register_workflow_tools(registry);
|
||||||
register_idea_tools(registry, db);
|
register_idea_tools(registry, db);
|
||||||
register_trash_tools(registry, db);
|
register_trash_tools(registry, db);
|
||||||
|
register_git_tools(registry, db);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 项目类 AI 工具注册(8 个:CRUD + 目录绑定 + 探总量)——持 db:Arc<Database>。
|
/// 项目类 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 续拆),【原样移入】,零行为变更。
|
/// 抽自 register_data_tools(SMELL-P0-2 续拆),【原样移入】,零行为变更。
|
||||||
fn register_workflow_tools(registry: &mut AiToolRegistry) {
|
fn register_workflow_tools(registry: &mut AiToolRegistry) {
|
||||||
// F-260616-07 阶段3(子项2):run_workflow AI 工具注册实装。
|
// F-260616-07 阶段3(子项2):run_workflow AI 工具注册实装。
|
||||||
@@ -3025,6 +3194,8 @@ mod tests {
|
|||||||
// 项目基础设施配置 D10 不存凭证,register_task_graph_tools 7→9)。
|
// 项目基础设施配置 D10 不存凭证,register_task_graph_tools 7→9)。
|
||||||
// 工程系统(2026-06-29): data 层 27→28(新增 list_project_modules 工程列表查询,
|
// 工程系统(2026-06-29): data 层 27→28(新增 list_project_modules 工程列表查询,
|
||||||
// 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,
|
||||||
|
// register_git_tools 3 个)
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
registry.len(),
|
registry.len(),
|
||||||
41,
|
41,
|
||||||
@@ -3052,6 +3223,8 @@ mod tests {
|
|||||||
"add_project_service", "list_project_services",
|
"add_project_service", "list_project_services",
|
||||||
// 工程系统:项目工程列表查询
|
// 工程系统:项目工程列表查询
|
||||||
"list_project_modules",
|
"list_project_modules",
|
||||||
|
// Git 只读工具
|
||||||
|
"git_status", "git_diff", "git_log",
|
||||||
// ── 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