重构: tool_registry 剩余 21 工具迁声明式(task_graph/git/http/workflow/idea/trash)
This commit is contained in:
@@ -0,0 +1,340 @@
|
||||
//! Git 类 AI 工具声明式注册(register_git_tools 6 个迁入)。
|
||||
//!
|
||||
//! 迁自 `tool_registry.rs::register_git_tools`(原 6 个:git_status/git_diff/git_log 只读 +
|
||||
//! git_commit/git_branch/git_merge 写),改用 `declare_tool!` 宏。
|
||||
//!
|
||||
//! 迁移策略(handler 逻辑零变更):
|
||||
//! - handler body 逐字照搬原 `register_git_tools` 内 async move 块(逻辑等价),
|
||||
//! 仅闭包包装(`{ let db = db.clone(); Box::new(move |args| { let db = db.clone();
|
||||
//! Box::pin(async move { ... }) }) }`)改由 `declare_tool!` 宏生成。
|
||||
//! - name/desc/schema/risk 与原手写定义逐字一致。
|
||||
//! - 配套私有 helper(exec_git/run_git_status/run_git_diff/run_git_log)原样搬入本文件,
|
||||
//! 仅 register_git_tools 使用(external module.rs::run_git_status 是同名不同签名的另一函数)。
|
||||
//!
|
||||
//! 等价性验证:基线测试 `test_build_ai_tool_registry_baseline_tool_count` 仍断言 48 总量 +
|
||||
//! 工具名集合稳定(防 rename / 漏注册)。
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use df_ai::ai_tools::{object_schema, AiToolRegistry, RiskLevel};
|
||||
use df_ai::declare_tool;
|
||||
use df_storage::db::Database;
|
||||
|
||||
// ============================================================
|
||||
// Git 私有 helper(原 tool_registry.rs 986-1080 行原样搬入,仅本模块使用)
|
||||
// ============================================================
|
||||
|
||||
/// 在指定目录执行 git 命令(10s 超时,返回 stdout)。失败/超时返回空字符串(非崩溃)。
|
||||
///
|
||||
/// BUG-2026-07-18: 原实现 spawn_blocking 内裸 std::process::Command::output() 无 timeout
|
||||
/// (注释谎称"10s 超时")。git 在 OneDrive/网盘/挂载盘/lfs/大仓库场景会卡数十秒到无限,
|
||||
/// spawn_blocking 线程永不返回 → 累计耗尽 tokio blocking 池 → 间接卡死单线程 runtime
|
||||
/// (与 env_snapshot::probe_version 同型病根)。AI 的 git_status/log/diff 工具在会话内高频触发。
|
||||
///
|
||||
/// 改用 tokio::process + tokio::time::timeout(10s) + kill_on_drop:超时 drop 时 child 进程
|
||||
/// 被 kill,不泄漏线程/进程,对齐 shell.rs execute 同源封装。
|
||||
async fn exec_git(working_dir: &str, args: &[&str]) -> String {
|
||||
let mut cmd = tokio::process::Command::new("git");
|
||||
cmd.args(args)
|
||||
.current_dir(working_dir)
|
||||
.stdout(std::process::Stdio::piped())
|
||||
.stderr(std::process::Stdio::null())
|
||||
.kill_on_drop(true);
|
||||
match tokio::time::timeout(std::time::Duration::from_secs(10), cmd.output()).await {
|
||||
Ok(Ok(out)) => String::from_utf8_lossy(&out.stdout).to_string(),
|
||||
Ok(Err(_)) => String::new(),
|
||||
Err(_elapsed) => String::new(), // 超时:child 被 kill_on_drop 终止
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 工具注册(6 个:status/diff/log 只读 Low + commit/branch 写 Medium + merge High)。
|
||||
///
|
||||
/// 与原手写 register(name, desc, schema, risk, handler) 语义 1:1:
|
||||
/// - name/desc/schema 字符串与 JSON Schema 逐字照搬原定义
|
||||
/// - risk 与原一致(status/diff/log=Low,commit/branch=Medium,merge=High)
|
||||
/// - handler body 与原 async move 块逐字一致(逻辑零变更)
|
||||
///
|
||||
/// 唯一差异:闭包包装改由 `declare_tool!` 宏生成,handler body 直接写业务逻辑。
|
||||
pub fn register(registry: &mut AiToolRegistry, db: &Arc<Database>) {
|
||||
// ── git_status (Low 只读) ──
|
||||
declare_tool!(
|
||||
registry,
|
||||
db: Arc<Database>,
|
||||
"git_status",
|
||||
"查看工程 Git 工作区状态。参数:module_id(工程 ID)。返回当前分支、改动文件列表(每个文件含路径+状态 M/A/D/??)、改动总数。只读无副作用",
|
||||
RiskLevel::Low,
|
||||
schema: object_schema(vec![
|
||||
("module_id", "string", true),
|
||||
]),
|
||||
args => {
|
||||
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 只读) ──
|
||||
declare_tool!(
|
||||
registry,
|
||||
db: Arc<Database>,
|
||||
"git_diff",
|
||||
"查看工程未提交的代码改动。参数:module_id(工程 ID)、staged(可选 bool,仅看已暂存改动,默认 false=全部含未暂存)。返回改动统计 + patch 内容(截断防 token 爆)。只读无副作用",
|
||||
RiskLevel::Low,
|
||||
schema: object_schema(vec![
|
||||
("module_id", "string", true),
|
||||
("staged", "boolean", false),
|
||||
]),
|
||||
args => {
|
||||
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 只读) ──
|
||||
declare_tool!(
|
||||
registry,
|
||||
db: Arc<Database>,
|
||||
"git_log",
|
||||
"查看工程 Git 提交历史。参数:module_id(工程 ID)、limit(可选 int,最近 N 条提交,默认 20,最大 100)。返回提交列表(每个含哈希/作者/消息/日期)。只读无副作用",
|
||||
RiskLevel::Low,
|
||||
schema: object_schema(vec![
|
||||
("module_id", "string", true),
|
||||
("limit", "integer", false),
|
||||
]),
|
||||
args => {
|
||||
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 }))
|
||||
}
|
||||
);
|
||||
|
||||
// ── git_commit (Medium 审批) ──
|
||||
// 提交工作区改动。需审批(写操作影响代码仓库)。
|
||||
// 安全边界:不提交敏感文件(.env/.key/.pem,由 .gitignore 兜底 + 此处检查)。
|
||||
declare_tool!(
|
||||
registry,
|
||||
db: Arc<Database>,
|
||||
"git_commit",
|
||||
"提交工程工作区改动到本地仓库。参数:module_id(工程 ID)、message(提交信息,必填)、add_all(可选 bool,是否添加全部改动到暂存区,默认 true)。中等风险,需审批。提交前检查不提交敏感文件(.env/.key/.pem)",
|
||||
RiskLevel::Medium,
|
||||
schema: object_schema(vec![
|
||||
("module_id", "string", true),
|
||||
("message", "string", true),
|
||||
("add_all", "boolean", false),
|
||||
]),
|
||||
args => {
|
||||
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 审批) ──
|
||||
// 分支管理:列表/创建/切换。需审批(影响工作区分支状态)。
|
||||
declare_tool!(
|
||||
registry,
|
||||
db: Arc<Database>,
|
||||
"git_branch",
|
||||
"工程分支管理。参数:module_id(工程 ID)、action(list/create/switch,默认 list)、name(分支名,create/switch 时必填)。中等风险,需审批。list=列出所有分支+当前分支,create=创建新分支,switch=切换分支",
|
||||
RiskLevel::Medium,
|
||||
schema: object_schema(vec![
|
||||
("module_id", "string", true),
|
||||
("action", "string", false),
|
||||
("name", "string", false),
|
||||
]),
|
||||
args => {
|
||||
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 审批) ──
|
||||
// 合并分支。高风险(可能产生冲突,影响代码完整性)。
|
||||
declare_tool!(
|
||||
registry,
|
||||
db: Arc<Database>,
|
||||
"git_merge",
|
||||
"合并指定分支到当前分支。参数:module_id(工程 ID)、branch(要合并的分支名,必填)。高风险,需审批。合并冲突时返回冲突文件列表,需用户手动解决",
|
||||
RiskLevel::High,
|
||||
schema: object_schema(vec![
|
||||
("module_id", "string", true),
|
||||
("branch", "string", true),
|
||||
]),
|
||||
args => {
|
||||
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,
|
||||
}))
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
//! 网络层 AI 工具声明式注册(register_http_tools 1 个:http_request 迁入)。
|
||||
//!
|
||||
//! 迁自 `tool_registry.rs::register_http_tools`(原 1 个 http_request),改用 `declare_tool!` 宏。
|
||||
//!
|
||||
//! 迁移策略(handler 逻辑零变更):
|
||||
//! - handler body 逐字照搬原 async move 块(转调 `crate::commands::ai::http::execute_http_request`),
|
||||
//! 仅闭包包装改由 `declare_tool!` 宏生成。
|
||||
//! - name/desc/schema/risk 与原手写定义逐字一致(headers 是对象 map,object_schema 仅支持扁平
|
||||
//! 标量三元组,故保留原手工拼 serde_json::Map schema 表达式)。
|
||||
//! - 无捕获:原 handler 不 clone db(handler 仅转调 http.rs),用占位 `Arc<()>` 捕获(宏要求 capture 形参)。
|
||||
//!
|
||||
//! 风险:GET=Medium(只读但触发外发)/ 写方法=High(须人工批准),一个工具名两种 risk 不支持(register
|
||||
//! 单一 risk_level),故按最高风险 High 注册(写方法 High 兜底;GET 也走 High 审批更保守)。
|
||||
//!
|
||||
//! 等价性验证:基线测试 `test_build_ai_tool_registry_baseline_tool_count` 仍断言 48 总量 +
|
||||
//! 工具名集合稳定(防 rename / 漏注册)。
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use df_ai::ai_tools::{AiToolRegistry, RiskLevel};
|
||||
use df_ai::declare_tool;
|
||||
|
||||
/// 注册 http_request 工具到 `$registry`(无 db 捕获,纯 reqwest 调用)。
|
||||
///
|
||||
/// 与原手写 register(name, desc, schema, risk, handler) 语义 1:1:
|
||||
/// - name/desc/schema 字符串与 JSON Schema 逐字照搬原定义
|
||||
/// - risk=High(写方法兜底,GET 同走审批更保守)
|
||||
/// - handler body 与原 async move 块逐字一致(逻辑零变更)
|
||||
///
|
||||
/// 唯一差异:闭包包装改由 `declare_tool!` 宏生成,handler body 直接写业务逻辑。
|
||||
pub fn register(registry: &mut AiToolRegistry) {
|
||||
// 无捕获:占位 Arc<()>(原 handler 不持 db,仅转调 http.rs)。
|
||||
let dummy: Arc<()> = Arc::new(());
|
||||
// schema:headers 是对象 map,object_schema 仅支持扁平标量三元组,故手工拼 serde_json::Map。
|
||||
// 提取为 let 绑定:declare_tool! 的 schema: $schema:expr 形参对花括号块表达式解析有歧义,
|
||||
// 先求值到局部变量再传入,语义等价(原手写 register 亦以此 Map 作为 schema 实参)。
|
||||
let schema = {
|
||||
let mut props = serde_json::Map::new();
|
||||
props.insert("method".into(), serde_json::json!({ "type": "string", "description": "HTTP 方法:GET/POST/PUT/PATCH/DELETE(默认 GET)", "enum": ["GET", "POST", "PUT", "PATCH", "DELETE"] }));
|
||||
props.insert("url".into(), serde_json::json!({ "type": "string", "description": "请求 URL,仅 http/https,拒私网/localhost(SSRF 防护)" }));
|
||||
props.insert("headers".into(), serde_json::json!({ "type": "object", "description": "请求头 map<string,string>,如 {\"Authorization\":\"Bearer xxx\",\"Content-Type\":\"application/json\"}", "additionalProperties": { "type": "string" } }));
|
||||
props.insert("body".into(), serde_json::json!({ "type": "string", "description": "请求体(POST/PUT/PATCH 用),原样发送,Content-Type 须在 headers 显式指定" }));
|
||||
props.insert("timeout_secs".into(), serde_json::json!({ "type": "integer", "description": "超时秒数(默认 30,上限 60)", "minimum": 1, "maximum": 60 }));
|
||||
props.insert("parse".into(), serde_json::json!({ "type": "string", "description": "响应 body 解析:json(pretty 格式化)/text(原样)/auto(按 Content-Type 自动,默认)", "enum": ["json", "text", "auto"] }));
|
||||
serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": props,
|
||||
"required": ["url"],
|
||||
})
|
||||
};
|
||||
declare_tool!(
|
||||
registry,
|
||||
dummy: Arc<()>,
|
||||
"http_request",
|
||||
"发起结构化 HTTP 请求(GET/POST/PUT/PATCH/DELETE),用于查询外部 API。参数:method(默认 GET)、url(http/https)、headers(对象 map)、body(请求体字符串)、timeout_secs(默认 30,上限 60)、parse(json/text/auto,默认 auto)。安全:仅 http/https 协议,拒绝私网/保留 IP(SSRF 防护含 DNS resolve 后校验防重绑定),重定向≤3 跳且每跳重校验。响应 body 截断 50KB。GET 为只读但触发外发网络,POST/PUT/PATCH/DELETE 有副作用,统一按高风险须人工批准",
|
||||
RiskLevel::High,
|
||||
schema: schema,
|
||||
args => {
|
||||
// 转调 http.rs handler(SSRF 防护 + 重定向 + 截断全在那)
|
||||
crate::commands::ai::http::execute_http_request(args).await
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
//! 灵感类 AI 工具声明式注册(register_idea_tools 2 个:list + create 迁入)。
|
||||
//!
|
||||
//! 迁自 `tool_registry.rs::register_idea_tools`(原 2 个 list_ideas/create_idea),
|
||||
//! 改用 `declare_tool!` 宏。
|
||||
//!
|
||||
//! 迁移策略(handler 逻辑零变更):
|
||||
//! - handler body 逐字照搬原 `register_idea_tools` 内 async move 块(逻辑等价),
|
||||
//! 仅闭包包装(`{ let db = db.clone(); Box::new(move |args| { let db = db.clone();
|
||||
//! Box::pin(async move { ... }) }) }`)改由 `declare_tool!` 宏生成。
|
||||
//! - name/desc/schema/risk 与原手写定义逐字一致。
|
||||
//! - 复用类型/常量:new_id/IdeaStatus/IdeaRecord/now_millis(各 pub(crate) 来源,与原同源) +
|
||||
//! MAX_LIST_RESULTS(super::tool_registry 单真相源)。
|
||||
//!
|
||||
//! 等价性验证:基线测试 `test_build_ai_tool_registry_baseline_tool_count` 仍断言 48 总量 +
|
||||
//! 工具名集合稳定(防 rename / 漏注册)。
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use df_ai::ai_tools::{object_schema, AiToolRegistry, RiskLevel};
|
||||
use df_ai::declare_tool;
|
||||
use df_storage::db::Database;
|
||||
use df_storage::models::IdeaRecord;
|
||||
use df_types::types::{new_id, IdeaStatus};
|
||||
|
||||
use crate::commands::now_millis;
|
||||
use crate::commands::ai::tool_registry::MAX_LIST_RESULTS;
|
||||
|
||||
/// 注册 2 个灵感类工具(list/create)到 `$registry`。
|
||||
///
|
||||
/// 与原手写 register(name, desc, schema, risk, handler) 语义 1:1:
|
||||
/// - name/desc/schema 字符串与 JSON Schema 逐字照搬原定义
|
||||
/// - risk 与原一致(list=Low,create=Medium)
|
||||
/// - handler body 与原 async move 块逐字一致(逻辑零变更)
|
||||
///
|
||||
/// 唯一差异:闭包包装改由 `declare_tool!` 宏生成,handler body 直接写业务逻辑。
|
||||
pub fn register(registry: &mut AiToolRegistry, db: &Arc<Database>) {
|
||||
declare_tool!(
|
||||
registry,
|
||||
db: Arc<Database>,
|
||||
"list_ideas",
|
||||
"列出所有灵感,支持 offset/limit 分页。返回 items、total、has_more。默认 limit=50",
|
||||
RiskLevel::Low,
|
||||
schema: object_schema(vec![("offset", "integer", false), ("limit", "integer", false)]),
|
||||
args => {
|
||||
let repo = df_storage::crud::IdeaRepo::new(&db);
|
||||
let items = repo.list_all().await?;
|
||||
let total = items.len();
|
||||
let offset = args["offset"].as_u64().unwrap_or(0) as usize;
|
||||
let limit = args["limit"].as_u64().unwrap_or(MAX_LIST_RESULTS as u64).min(MAX_LIST_RESULTS as u64) as usize;
|
||||
let page_items: Vec<_> = items.into_iter().skip(offset).take(limit).collect();
|
||||
let has_more = (offset + page_items.len()) < total;
|
||||
Ok(serde_json::json!({ "items": page_items, "total": total, "has_more": has_more }))
|
||||
}
|
||||
);
|
||||
|
||||
declare_tool!(
|
||||
registry,
|
||||
db: Arc<Database>,
|
||||
"create_idea",
|
||||
"捕获一个新灵感",
|
||||
RiskLevel::Medium,
|
||||
schema: object_schema(vec![
|
||||
("title", "string", true), ("description", "string", false),
|
||||
("tags", "string", false), ("source", "string", false),
|
||||
// priority:与 commands::idea::CreateIdeaInput 默认值一致(=1,灵感默认普通优先级)
|
||||
("priority", "integer", false),
|
||||
]),
|
||||
args => {
|
||||
let title = args["title"].as_str().ok_or_else(|| anyhow::anyhow!("缺少 title"))?;
|
||||
let repo = df_storage::crud::IdeaRepo::new(&db);
|
||||
let record = IdeaRecord {
|
||||
id: new_id(), title: title.to_string(),
|
||||
description: args["description"].as_str().unwrap_or("").to_string(),
|
||||
// priority 默认 1:灵感默认普通优先级(与 commands::idea::default_priority 及 tasks 表 SQL DEFAULT 1 对齐)
|
||||
status: IdeaStatus::Draft, priority: args["priority"].as_i64().unwrap_or(1) as i32,
|
||||
score: None, tags: args["tags"].as_str().map(|s| s.to_string()),
|
||||
source: args["source"].as_str().map(|s| s.to_string()),
|
||||
promoted_to: None, ai_analysis: None, scores: None,
|
||||
related_ids: None,
|
||||
created_at: now_millis(), updated_at: now_millis(),
|
||||
};
|
||||
let id = record.id.clone();
|
||||
repo.insert(record).await?;
|
||||
Ok(serde_json::json!({ "id": id, "title": title, "status": "draft" }))
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -11,4 +11,10 @@
|
||||
pub mod list_projects;
|
||||
pub mod project;
|
||||
pub mod task;
|
||||
pub mod task_graph;
|
||||
pub mod git;
|
||||
pub mod http;
|
||||
pub mod workflow;
|
||||
pub mod idea;
|
||||
pub mod trash;
|
||||
pub mod file;
|
||||
|
||||
@@ -0,0 +1,447 @@
|
||||
//! 知识图谱 / 工程系统类 AI 工具声明式注册(register_task_graph_tools 10 个迁入)。
|
||||
//!
|
||||
//! 迁自 `tool_registry.rs::register_task_graph_tools`(原 10 个:task_link CRUD + 父子树
|
||||
//! + 跨池移动 + content 更新 + 项目事件流 + 项目基础设施配置 + 工程列表),改用 `declare_tool!` 宏。
|
||||
//!
|
||||
//! 迁移策略(handler 逻辑零变更):
|
||||
//! - handler body 逐字照搬原 `register_task_graph_tools` 内 async move 块(逻辑等价),
|
||||
//! 仅闭包包装(`{ let db = db.clone(); Box::new(move |args| { let db = db.clone();
|
||||
//! Box::pin(async move { ... }) }) }`)改由 `declare_tool!` 宏生成。
|
||||
//! - name/desc/schema/risk 与原手写定义逐字一致。
|
||||
//! - 复用类型:new_id/ProjectServiceRecord/now_millis(各 pub(crate) 来源,与原同源)。
|
||||
//!
|
||||
//! 等价性验证:基线测试 `test_build_ai_tool_registry_baseline_tool_count` 仍断言 48 总量 +
|
||||
//! 工具名集合稳定(防 rename / 漏注册)。
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use df_ai::ai_tools::{object_schema, AiToolRegistry, RiskLevel};
|
||||
use df_ai::declare_tool;
|
||||
use df_storage::db::Database;
|
||||
use df_storage::models::ProjectServiceRecord;
|
||||
use df_types::types::new_id;
|
||||
|
||||
use crate::commands::now_millis;
|
||||
|
||||
/// 注册 10 个知识图谱 / 工程系统类工具到 `$registry`。
|
||||
///
|
||||
/// 与原手写 register(name, desc, schema, risk, handler) 语义 1:1:
|
||||
/// - name/desc/schema 字符串与 JSON Schema 逐字照搬原定义
|
||||
/// - risk 与原一致(link 写/queue 移动/content 更新/add_service=Medium,只读=Low)
|
||||
/// - handler body 与原 async move 块逐字一致(逻辑零变更)
|
||||
///
|
||||
/// 唯一差异:闭包包装改由 `declare_tool!` 宏生成,handler body 直接写业务逻辑。
|
||||
pub fn register(registry: &mut AiToolRegistry, db: &Arc<Database>) {
|
||||
// ── create_task_link (Medium,设计 §2.2 + §五) ──
|
||||
// 建立任务横向关联。TaskLinkRepo::create_link 内含:
|
||||
// link_type 白名单(depends_on/blocks/relates_to)+ 自环拒绝 + depends_on BFS 循环依赖检测。
|
||||
// handler 仅转发参数 + new_id,业务校验全部下沉 Repo 层(单一真相源,与 IPC create_task_link 同源)。
|
||||
declare_tool!(
|
||||
registry,
|
||||
db: Arc<Database>,
|
||||
"create_task_link",
|
||||
"建立任务横向关联(AI 拓扑排序编排调度基础)。参数:task_id(source 任务,关联发起方)、target_id(target 任务,关联指向方)、link_type(depends_on 依赖/blocks 阻塞/relates_to 弱关联)、remark(可选备注)。约束:link_type 白名单校验;source==target 自环拒绝;depends_on 链 BFS 循环依赖检测(A→B→A 拒绝);跨项目允许。返回 link id",
|
||||
RiskLevel::Medium,
|
||||
schema: object_schema(vec![
|
||||
("task_id", "string", true), ("target_id", "string", true),
|
||||
("link_type", "string", true), ("remark", "string", false),
|
||||
]),
|
||||
args => {
|
||||
let task_id = args["task_id"].as_str().ok_or_else(|| anyhow::anyhow!("缺少 task_id"))?;
|
||||
let target_id = args["target_id"].as_str().ok_or_else(|| anyhow::anyhow!("缺少 target_id"))?;
|
||||
let link_type = args["link_type"].as_str().ok_or_else(|| anyhow::anyhow!("缺少 link_type"))?;
|
||||
let remark = args.get("remark").and_then(|v| v.as_str()).filter(|s| !s.is_empty());
|
||||
let repo = df_storage::crud::TaskLinkRepo::new(&db);
|
||||
let id = new_id();
|
||||
let link_id = repo.create_link(&id, task_id, target_id, link_type, remark).await?;
|
||||
Ok(serde_json::json!({ "id": link_id, "task_id": task_id, "target_id": target_id, "link_type": link_type }))
|
||||
}
|
||||
);
|
||||
|
||||
// ── remove_task_link (Medium,设计 §2.2 + §五) ──
|
||||
// 解除任务关联(按 link id)。返回是否命中(deleted=false=link 不存在)。
|
||||
declare_tool!(
|
||||
registry,
|
||||
db: Arc<Database>,
|
||||
"remove_task_link",
|
||||
"解除任务关联(按 link id)。返回 deleted 布尔(是否命中,link 不存在则 false)",
|
||||
RiskLevel::Medium,
|
||||
schema: object_schema(vec![("id", "string", true)]),
|
||||
args => {
|
||||
let id = args["id"].as_str().ok_or_else(|| anyhow::anyhow!("缺少 id"))?;
|
||||
let repo = df_storage::crud::TaskLinkRepo::new(&db);
|
||||
let deleted = repo.delete(id).await?;
|
||||
Ok(serde_json::json!({ "deleted": deleted, "id": id }))
|
||||
}
|
||||
);
|
||||
|
||||
// ── list_task_links (Low 只读,设计 §2.2 + §五) ──
|
||||
// 查询任务关联(方向:outgoing 作为 source / incoming 作为 target / both 双向合并去重)。
|
||||
// AI 编排调度用:取某任务的全部依赖/阻塞/关联,做拓扑排序。
|
||||
declare_tool!(
|
||||
registry,
|
||||
db: Arc<Database>,
|
||||
"list_task_links",
|
||||
"查询任务关联(AI 编排调度用)。参数:task_id(目标任务)、direction(查询方向 outgoing=作为 source 查我依赖谁 / incoming=作为 target 查谁依赖我 / both=双向合并去重,默认 both)。返回关联记录列表(按 created_at 升序)",
|
||||
RiskLevel::Low,
|
||||
schema: object_schema(vec![("task_id", "string", true), ("direction", "string", false)]),
|
||||
args => {
|
||||
let task_id = args["task_id"].as_str().ok_or_else(|| anyhow::anyhow!("缺少 task_id"))?;
|
||||
// 方向默认 both(对齐 commands::task::LinkDirection::default)。空串/未传走 both。
|
||||
let direction = args.get("direction").and_then(|v| v.as_str()).unwrap_or("both").to_lowercase();
|
||||
let repo = df_storage::crud::TaskLinkRepo::new(&db);
|
||||
let links = match direction.as_str() {
|
||||
"outgoing" => repo.get_by_source(task_id).await?,
|
||||
"incoming" => repo.get_by_target(task_id).await?,
|
||||
// both:Outgoing + Incoming 合并后按 id 去重(同一 link 双向查询可能重复)
|
||||
_ => {
|
||||
let mut outgoing = repo.get_by_source(task_id).await?;
|
||||
let incoming = repo.get_by_target(task_id).await?;
|
||||
let mut seen: std::collections::HashSet<String> =
|
||||
outgoing.iter().map(|l| l.id.clone()).collect();
|
||||
for l in incoming {
|
||||
if seen.insert(l.id.clone()) {
|
||||
outgoing.push(l);
|
||||
}
|
||||
}
|
||||
outgoing.sort_by(|a, b| a.created_at.cmp(&b.created_at));
|
||||
outgoing
|
||||
}
|
||||
};
|
||||
let total = links.len();
|
||||
Ok(serde_json::json!({ "items": links, "total": total, "task_id": task_id, "direction": direction }))
|
||||
}
|
||||
);
|
||||
|
||||
// ── get_task_tree (Low 只读,设计 §2.1 + §五) ──
|
||||
// 获取父子任务树(限 1 级嵌套,设计 D2:无孙任务)。返回 parent + 直接 children 列表。
|
||||
// AI 用途:查看需求分解结构、子任务进度聚合。
|
||||
declare_tool!(
|
||||
registry,
|
||||
db: Arc<Database>,
|
||||
"get_task_tree",
|
||||
"获取任务父子树(限 1 级嵌套,设计 D2 无孙任务)。参数 task_id(任务 ID,无论叶子还是父任务)。返回 { parent: 父任务记录, children: 直接子任务列表(parent_id 指向 task_id,按 created_at 升序) }。叶子任务 children 为空。AI 用途:查看需求分解结构、子任务进度聚合",
|
||||
RiskLevel::Low,
|
||||
schema: object_schema(vec![("task_id", "string", true)]),
|
||||
args => {
|
||||
let task_id = args["task_id"].as_str().ok_or_else(|| anyhow::anyhow!("缺少 task_id"))?;
|
||||
let repo = df_storage::crud::TaskRepo::new(&db);
|
||||
let parent = repo
|
||||
.get_by_id(task_id)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow::anyhow!("任务 {task_id} 不存在"))?;
|
||||
let children = repo.get_children(task_id).await?;
|
||||
let child_count = children.len();
|
||||
Ok(serde_json::json!({ "parent": parent, "children": children, "child_count": child_count }))
|
||||
}
|
||||
);
|
||||
|
||||
// ── move_task_queue (Medium,设计 §2.1 + §五) ──
|
||||
// 跨池移动任务(backlog/todo/decision/active/done),按一致性约束联动 status。
|
||||
// 对齐 commands::task::move_task_queue 语义:
|
||||
// - done → status 强制 done / backlog → status 强制 todo
|
||||
// - active → status 若不在执行中三态则强制 in_progress / todo → status 强制 todo
|
||||
// - decision → status 不变(待决策池保留执行态)
|
||||
// status 写入走专用 set_status_for_aggregation(绕过 status 收口,move_task_queue 是合法非状态机路径)。
|
||||
declare_tool!(
|
||||
registry,
|
||||
db: Arc<Database>,
|
||||
"move_task_queue",
|
||||
"跨池移动任务(管理维度池 backlog/todo/decision/active/done,与 status 执行维度正交)。参数 id(任务 ID)、new_queue(目标池)。一致性约束联动 status:done→status=done / backlog→status=todo / active→status 若非执行中三态强制 in_progress / todo→status=todo / decision→status 不变。返回移动后的最新 TaskRecord",
|
||||
RiskLevel::Medium,
|
||||
schema: object_schema(vec![("id", "string", true), ("new_queue", "string", true)]),
|
||||
args => {
|
||||
let id = args["id"].as_str().ok_or_else(|| anyhow::anyhow!("缺少 id"))?;
|
||||
let new_queue = args["new_queue"].as_str()
|
||||
.ok_or_else(|| anyhow::anyhow!("缺少 new_queue"))?
|
||||
.trim()
|
||||
.to_string();
|
||||
// queue 白名单校验(对标 commands::task::validate_queue)
|
||||
const TASK_QUEUE_VALUES: &[&str] = &["backlog", "todo", "decision", "active", "done"];
|
||||
if !TASK_QUEUE_VALUES.contains(&new_queue.as_str()) {
|
||||
anyhow::bail!("非法 new_queue 值 {:?},合法值: {:?}", new_queue, TASK_QUEUE_VALUES);
|
||||
}
|
||||
|
||||
let repo = df_storage::crud::TaskRepo::new(&db);
|
||||
let current = repo
|
||||
.get_by_id(id)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow::anyhow!("任务 {id} 不存在"))?;
|
||||
|
||||
// 一致性约束联动:根据 new_queue 决定 status 是否需调整
|
||||
// (对标 commands::task::move_task_queue 同源逻辑,单一真相源)
|
||||
const ACTIVE_OK_STATUSES: &[&str] = &["in_progress", "in_review", "testing"];
|
||||
let new_status = match new_queue.as_str() {
|
||||
"done" => "done".to_string(),
|
||||
"backlog" => "todo".to_string(),
|
||||
"active" => {
|
||||
if ACTIVE_OK_STATUSES.contains(¤t.status.as_str()) {
|
||||
current.status.as_str().to_string()
|
||||
} else {
|
||||
"in_progress".to_string()
|
||||
}
|
||||
}
|
||||
"todo" => "todo".to_string(),
|
||||
"decision" => current.status.as_str().to_string(),
|
||||
_ => unreachable!("queue 白名单已收口"),
|
||||
};
|
||||
|
||||
// 写 queue(queue 已在 tasks 白名单);与 current 不同才写(避免无谓 updated_at 抖动)
|
||||
if current.queue != new_queue {
|
||||
repo.update_field(id, "queue", &new_queue).await?;
|
||||
}
|
||||
// 写 status(专用 set_status_for_aggregation 绕过 status 收口,合法非状态机路径)
|
||||
if current.status.as_str() != new_status {
|
||||
repo.set_status_for_aggregation(id, &new_status).await?;
|
||||
}
|
||||
|
||||
// 回读最新记录返回
|
||||
let updated = repo
|
||||
.get_by_id(id)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow::anyhow!("任务 {id} 不存在(移动后回读失败)"))?;
|
||||
Ok(serde_json::to_value(&updated)?)
|
||||
}
|
||||
);
|
||||
|
||||
// ── update_content (Medium,设计 §五) ──
|
||||
// 更新 content_json 结构化需求规格({background,acceptance_criteria[],scope[],technical_design})。
|
||||
// 走通用 update_field(content_json 已在 tasks 白名单) + JSON 合法性校验(对齐 create_task 校验)。
|
||||
// AI 从对话中提取信息填充 content_json,人确认后 AI 执行中用 acceptance_criteria 自检。
|
||||
declare_tool!(
|
||||
registry,
|
||||
db: Arc<Database>,
|
||||
"update_content",
|
||||
"更新任务的结构化需求规格 content_json(JSON 字符串 {background,acceptance_criteria[],scope[],technical_design,custom_fields})。参数 id(任务 ID)、content_json(合法 JSON 字符串,空串清空 content_json)。校验:须是合法 JSON,否则拒。AI 用途:从对话提取信息填充需求规格,供后续 acceptance_criteria 自检",
|
||||
RiskLevel::Medium,
|
||||
schema: object_schema(vec![("id", "string", true), ("content_json", "string", true)]),
|
||||
args => {
|
||||
let id = args["id"].as_str().ok_or_else(|| anyhow::anyhow!("缺少 id"))?;
|
||||
let content_json = args["content_json"].as_str()
|
||||
.ok_or_else(|| anyhow::anyhow!("缺少 content_json"))?;
|
||||
// JSON 合法性校验(对齐 commands::task::create_task 的 content_json 校验)
|
||||
// 非空串须是合法 JSON;空串视为清空(写 NULL)——但 update_field 收 string,
|
||||
// 用空串写库后会留空字符串而非 NULL(可接受,content_json 为 Option<String> 但
|
||||
// 通用 update_field 走 TEXT 列接受空串)。这里对非空串做校验,空串直接放行清空。
|
||||
if !content_json.trim().is_empty() {
|
||||
serde_json::from_str::<serde_json::Value>(content_json)
|
||||
.map_err(|e| anyhow::anyhow!("content_json 不是合法 JSON: {e}"))?;
|
||||
}
|
||||
let repo = df_storage::crud::TaskRepo::new(&db);
|
||||
repo.update_field(id, "content_json", content_json).await?;
|
||||
let title = repo.get_by_id(id).await?.map(|t| t.title).unwrap_or_default();
|
||||
Ok(serde_json::json!({ "id": id, "title": title, "field": "content_json", "updated": true }))
|
||||
}
|
||||
);
|
||||
|
||||
// ── get_project_timeline (Low 只读,设计 §五 + §2.4) ──
|
||||
// 查询项目事件流(project_events 统一事件流)。AI 精准检索基础:
|
||||
// 回答"上周做了什么 / 这个任务为何 blocked / 决策何时做出"。
|
||||
// 参数 project_id(必填) + event_type(可选过滤) + limit(默认 50,上限 200)。
|
||||
// source 标 "ai"(AI 工具路径,区别于 IPC 层的 "human")。
|
||||
declare_tool!(
|
||||
registry,
|
||||
db: Arc<Database>,
|
||||
"get_project_timeline",
|
||||
"查询项目事件流(AI 精准检索基础,回答\"上周做了什么/这个任务为何 blocked/决策何时做出\")。参数 project_id(必填,项目 ID)、event_type(可选,按事件类型精确过滤,如 task_created/task_advanced/idea_promoted/idea_evaluated/decision_made 等,空=不过滤)、limit(可选,返回条数上限,默认 50,内部钳制 ≤ 200)。返回 { items: 事件列表(时间倒序), total, project_id }。事件含 event_type/entity_type/entity_id/from_state/to_state/source/created_at",
|
||||
RiskLevel::Low,
|
||||
schema: object_schema(vec![
|
||||
("project_id", "string", true),
|
||||
("event_type", "string", false),
|
||||
("limit", "integer", false),
|
||||
]),
|
||||
args => {
|
||||
let project_id = args["project_id"].as_str()
|
||||
.ok_or_else(|| anyhow::anyhow!("缺少 project_id"))?
|
||||
.trim()
|
||||
.to_string();
|
||||
if project_id.is_empty() {
|
||||
anyhow::bail!("project_id 不能为空");
|
||||
}
|
||||
let event_filter = args.get("event_type")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(str::trim)
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(|s| s.to_string());
|
||||
// limit 默认 50(对齐 commands::events::default_limit);负数/异常值兜底 50。
|
||||
let limit = args.get("limit")
|
||||
.and_then(|v| v.as_u64())
|
||||
.map(|n| n as u32)
|
||||
.unwrap_or(50);
|
||||
|
||||
let repo = df_storage::crud::ProjectEventRepo::new(&db);
|
||||
let mut events = repo.get_by_project(&project_id, limit).await?;
|
||||
// 应用层按 event_type 过滤(单用户事件量小,保持 Repo 方法精简)。
|
||||
if let Some(et) = &event_filter {
|
||||
events.retain(|e| e.event_type == *et);
|
||||
}
|
||||
let total = events.len();
|
||||
Ok(serde_json::json!({
|
||||
"items": events,
|
||||
"total": total,
|
||||
"project_id": project_id,
|
||||
}))
|
||||
}
|
||||
);
|
||||
|
||||
// ── add_project_service (Medium,设计 §2.3 + §五 + D10) ──
|
||||
// 添加项目依赖的基础设施配置(数据库/缓存/MQ/API 等)。为 AI 执行任务时提供基础设施上下文:
|
||||
// "这项目用了什么数据库、Redis 在哪、有没有 MQ"。
|
||||
// service_type 白名单 + D10 凭证审查下沉 Repo::insert_validated(单一真相源,与 IPC
|
||||
// add_project_service 同源)。
|
||||
//
|
||||
// ⚠️ D10 安全边界:config_json / endpoint / remark 不含敏感凭证(密码/密钥/Token)。
|
||||
// Repo 层审查 password/secret/token/api_key 子串,命中即拒。凭证走环境变量。
|
||||
declare_tool!(
|
||||
registry,
|
||||
db: Arc<Database>,
|
||||
"add_project_service",
|
||||
"为项目添加基础设施配置(为 AI 执行任务时提供\"用了什么数据库/缓存/MQ/API\"上下文)。参数:project_id(项目 ID)、name(服务名,如 主库/Redis 缓存)、service_type(类型,白名单 mysql/postgresql/sqlite/redis/mongodb/mq/api/other)、environment(环境 development/staging/production,默认 development)、endpoint(可选,连接地址 localhost:3306 或 URL,不含凭证)、config_json(可选,类型相关配置 JSON 字符串,如 {\"pool_size\":10},⚠️不含密码字段,凭证走环境变量)、remark(可选,备注)。返回新增完整记录。约束:service_type 白名单;D10 安全边界 config_json/endpoint/remark 含 password/secret/token/api_key 子串拒绝",
|
||||
RiskLevel::Medium,
|
||||
schema: object_schema(vec![
|
||||
("project_id", "string", true),
|
||||
("name", "string", true),
|
||||
("service_type", "string", true),
|
||||
("environment", "string", false),
|
||||
("endpoint", "string", false),
|
||||
("config_json", "string", false),
|
||||
("remark", "string", false),
|
||||
]),
|
||||
args => {
|
||||
let project_id = args["project_id"].as_str()
|
||||
.ok_or_else(|| anyhow::anyhow!("缺少 project_id"))?
|
||||
.trim()
|
||||
.to_string();
|
||||
let name = args["name"].as_str()
|
||||
.ok_or_else(|| anyhow::anyhow!("缺少 name"))?
|
||||
.trim()
|
||||
.to_string();
|
||||
let service_type = args["service_type"].as_str()
|
||||
.ok_or_else(|| anyhow::anyhow!("缺少 service_type"))?
|
||||
.trim()
|
||||
.to_string();
|
||||
if project_id.is_empty() {
|
||||
anyhow::bail!("project_id 不能为空");
|
||||
}
|
||||
if name.is_empty() {
|
||||
anyhow::bail!("name 不能为空");
|
||||
}
|
||||
if service_type.is_empty() {
|
||||
anyhow::bail!("service_type 不能为空");
|
||||
}
|
||||
let endpoint = args.get("endpoint")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(str::trim)
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(|s| s.to_string());
|
||||
let config_json = args.get("config_json")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(str::trim)
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(|s| s.to_string());
|
||||
let environment = args.get("environment")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(str::trim)
|
||||
.filter(|s| !s.is_empty())
|
||||
.unwrap_or("development")
|
||||
.to_string();
|
||||
let remark = args.get("remark")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(str::trim)
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(|s| s.to_string());
|
||||
|
||||
let record = ProjectServiceRecord {
|
||||
id: new_id(),
|
||||
project_id: project_id.clone(),
|
||||
name,
|
||||
service_type,
|
||||
endpoint,
|
||||
config_json,
|
||||
environment,
|
||||
remark,
|
||||
created_at: now_millis(),
|
||||
updated_at: now_millis(),
|
||||
};
|
||||
let repo = df_storage::crud::ProjectServiceRepo::new(&db);
|
||||
// insert_validated 内含 service_type 白名单 + D10 凭证审查(密码/token/api_key 子串拒绝)。
|
||||
let id = repo.insert_validated(record).await?;
|
||||
let inserted = repo.get_by_id(&id).await?
|
||||
.ok_or_else(|| anyhow::anyhow!("插入后回读失败"))?;
|
||||
Ok(serde_json::to_value(&inserted)?)
|
||||
}
|
||||
);
|
||||
|
||||
// ── list_project_services (Low 只读,设计 §2.3 + §五) ──
|
||||
// 查询项目基础设施配置。AI 执行任务时按需取基础设施上下文:
|
||||
// 部署 production 任务只取 production 服务,开发调试取 development(环境隔离避免误连生产库)。
|
||||
declare_tool!(
|
||||
registry,
|
||||
db: Arc<Database>,
|
||||
"list_project_services",
|
||||
"查询项目基础设施配置(AI 执行任务时取\"用了什么数据库/缓存/MQ/API\"上下文)。参数:project_id(项目 ID)、environment(可选,按环境过滤 development/staging/production,空=返回所有环境)。返回 { items: 配置列表(时间倒序), total, project_id }。环境隔离:production 部署任务取 production 服务,开发调试取 development,避免误连生产库",
|
||||
RiskLevel::Low,
|
||||
schema: object_schema(vec![
|
||||
("project_id", "string", true),
|
||||
("environment", "string", false),
|
||||
]),
|
||||
args => {
|
||||
let project_id = args["project_id"].as_str()
|
||||
.ok_or_else(|| anyhow::anyhow!("缺少 project_id"))?
|
||||
.trim()
|
||||
.to_string();
|
||||
if project_id.is_empty() {
|
||||
anyhow::bail!("project_id 不能为空");
|
||||
}
|
||||
let env_filter = args.get("environment")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(str::trim)
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(|s| s.to_string());
|
||||
|
||||
let repo = df_storage::crud::ProjectServiceRepo::new(&db);
|
||||
let items = if let Some(env) = &env_filter {
|
||||
repo.list_by_project_env(&project_id, env).await?
|
||||
} else {
|
||||
repo.list_by_project(&project_id).await?
|
||||
};
|
||||
let total = items.len();
|
||||
Ok(serde_json::json!({
|
||||
"items": items,
|
||||
"total": total,
|
||||
"project_id": project_id,
|
||||
}))
|
||||
}
|
||||
);
|
||||
|
||||
// ── list_project_modules (Low 只读,工程系统) ──
|
||||
// 查询项目工程列表(AI 取项目结构上下文:有哪些代码仓库、各自目录/技术栈/Git 地址)。
|
||||
// 多工程场景(Monorepo/微服务/前后端分离)AI 据此决定在哪个工程执行 git/构建操作。
|
||||
declare_tool!(
|
||||
registry,
|
||||
db: Arc<Database>,
|
||||
"list_project_modules",
|
||||
"查询项目工程列表(每个工程是独立代码仓库)。参数:project_id(项目 ID)。返回 { modules: 工程列表(含 name/path/git_url/stack), total }。AI 据此了解项目有哪些代码仓库及各自技术栈",
|
||||
RiskLevel::Low,
|
||||
schema: object_schema(vec![
|
||||
("project_id", "string", true),
|
||||
]),
|
||||
args => {
|
||||
let project_id = args["project_id"].as_str()
|
||||
.ok_or_else(|| anyhow::anyhow!("缺少 project_id"))?
|
||||
.trim()
|
||||
.to_string();
|
||||
if project_id.is_empty() {
|
||||
anyhow::bail!("project_id 不能为空");
|
||||
}
|
||||
let repo = df_storage::crud::ProjectModuleRepo::new(&db);
|
||||
let modules = repo.list_by_project(&project_id).await?;
|
||||
let total = modules.len();
|
||||
Ok(serde_json::json!({
|
||||
"modules": modules,
|
||||
"total": total,
|
||||
"project_id": project_id,
|
||||
}))
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
//! 回收站类 AI 工具声明式注册(register_trash_tools 1 个:list_trash 迁入)。
|
||||
//!
|
||||
//! 迁自 `tool_registry.rs::register_trash_tools`(原 1 个 list_trash),改用 `declare_tool!` 宏。
|
||||
//!
|
||||
//! 迁移策略(handler 逻辑零变更):
|
||||
//! - handler body 逐字照搬原 `register_trash_tools` 内 async move 块(逻辑等价),
|
||||
//! 仅闭包包装(`{ let db = db.clone(); Box::new(move |args| { let db = db.clone();
|
||||
//! Box::pin(async move { ... }) }) }`)改由 `declare_tool!` 宏生成。
|
||||
//! - name/desc/schema/risk 与原手写定义逐字一致。
|
||||
//! - 复用常量:MAX_LIST_RESULTS(super::tool_registry 单真相源)。
|
||||
//!
|
||||
//! 等价性验证:基线测试 `test_build_ai_tool_registry_baseline_tool_count` 仍断言 48 总量 +
|
||||
//! 工具名集合稳定(防 rename / 漏注册)。
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use df_ai::ai_tools::{object_schema, AiToolRegistry, RiskLevel};
|
||||
use df_ai::declare_tool;
|
||||
use df_storage::db::Database;
|
||||
|
||||
use crate::commands::ai::tool_registry::MAX_LIST_RESULTS;
|
||||
|
||||
/// 注册 list_trash 工具到 `$registry`。
|
||||
///
|
||||
/// 与原手写 register(name, desc, schema, risk, handler) 语义 1:1:
|
||||
/// - name/desc/schema 字符串与 JSON Schema 逐字照搬原定义
|
||||
/// - risk=Low(只读)
|
||||
/// - handler body 与原 async move 块逐字一致(逻辑零变更)
|
||||
///
|
||||
/// 唯一差异:闭包包装改由 `declare_tool!` 宏生成,handler body 直接写业务逻辑。
|
||||
pub fn register(registry: &mut AiToolRegistry, db: &Arc<Database>) {
|
||||
declare_tool!(
|
||||
registry,
|
||||
db: Arc<Database>,
|
||||
"list_trash",
|
||||
"列出回收站已删除项目,支持 offset/limit 分页。返回 items、total、has_more。默认 limit=50",
|
||||
RiskLevel::Low,
|
||||
schema: object_schema(vec![("offset", "integer", false), ("limit", "integer", false)]),
|
||||
args => {
|
||||
let repo = df_storage::crud::ProjectRepo::new(&db);
|
||||
let items = repo.list_deleted().await?;
|
||||
let total = items.len();
|
||||
let offset = args["offset"].as_u64().unwrap_or(0) as usize;
|
||||
let limit = args["limit"].as_u64().unwrap_or(MAX_LIST_RESULTS as u64).min(MAX_LIST_RESULTS as u64) as usize;
|
||||
let page_items: Vec<_> = items.into_iter().skip(offset).take(limit).collect();
|
||||
let has_more = (offset + page_items.len()) < total;
|
||||
Ok(serde_json::json!({ "items": page_items, "total": total, "has_more": has_more }))
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
//! 工作流类 AI 工具声明式注册(register_workflow_tools 1 个:run_workflow 迁入)。
|
||||
//!
|
||||
//! 迁自 `tool_registry.rs::register_workflow_tools`(原 1 个 run_workflow),改用 `declare_tool!` 宏。
|
||||
//!
|
||||
//! 迁移策略(handler 逻辑零变更):
|
||||
//! - handler body 逐字照搬原 async move 块(防御兜底返 Err,正常流程不可达),仅闭包包装改由
|
||||
//! `declare_tool!` 宏生成。
|
||||
//! - name/desc/schema/risk 与原手写定义逐字一致。
|
||||
//! - 无捕获:CR-52 删死代码后 handler 不 clone db(防御兜底,run_workflow 真正执行经
|
||||
//! ai_approve → execute_run_workflow_for_tool → workflow.rs::run_workflow_inner),用占位
|
||||
//! `Arc<()>` 捕获(宏要求 capture 形参)。
|
||||
//!
|
||||
//! 等价性验证:基线测试 `test_build_ai_tool_registry_baseline_tool_count` 仍断言 48 总量 +
|
||||
//! 工具名集合稳定(防 rename / 漏注册)。
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use df_ai::ai_tools::{object_schema, AiToolRegistry, RiskLevel};
|
||||
use df_ai::declare_tool;
|
||||
|
||||
/// 注册 run_workflow 工具到 `$registry`(无 db 捕获,防御兜底)。
|
||||
///
|
||||
/// 与原手写 register(name, desc, schema, risk, handler) 语义 1:1:
|
||||
/// - name/desc/schema 字符串与 JSON Schema 逐字照搬原定义
|
||||
/// - risk=High(触发工作流引擎执行,须人工批准)
|
||||
/// - handler body 与原 async move 块逐字一致(逻辑零变更,仅返 Err 兜底)
|
||||
///
|
||||
/// 唯一差异:闭包包装改由 `declare_tool!` 宏生成,handler body 直接写业务逻辑。
|
||||
pub fn register(registry: &mut AiToolRegistry) {
|
||||
// 无捕获:占位 Arc<()>(CR-52 后 handler 不持 db,仅返 Err 防御)。
|
||||
let dummy: Arc<()> = Arc::new(());
|
||||
declare_tool!(
|
||||
registry,
|
||||
dummy: Arc<()>,
|
||||
"run_workflow",
|
||||
"按任务 target_status 推进对应工作流(含 AiNode 自审 / HumanNode 核对闸门)。参数 task_id + target_status 同时提供才联动任务推进(完成后按 target_status 推进任务,失败按退回态回滚)。属高风险操作(触发工作流引擎执行),须人工批准。审批通过后由后端直接执行工作流引擎并联动推进任务,返回 execution_id",
|
||||
RiskLevel::High,
|
||||
schema: object_schema(vec![("task_id", "string", true), ("target_status", "string", true)]),
|
||||
args => {
|
||||
let task_id = args["task_id"].as_str().ok_or_else(|| anyhow::anyhow!("缺少 task_id"))?;
|
||||
let target_status = args["target_status"].as_str()
|
||||
.ok_or_else(|| anyhow::anyhow!("缺少 target_status"))?;
|
||||
// 防御兜底(正常流程不可达):run_workflow 经 ai_approve 分支执行,不经此 handler。
|
||||
// 误入此路径说明调用方异常(非 ai_approve 直接 execute),返回明确错误勿盲目重试
|
||||
// (对齐 run_command 超时标注模式 L700-712),High risk 去重缓存会断 LLM 重试循环。
|
||||
Err(anyhow::anyhow!(
|
||||
"run_workflow 须经人工审批后由后端 ai_approve 分支执行(转调 run_workflow_inner,\
|
||||
持完整 State)。本 handler 经 ai_tools.execute 调用属异常路径(无 AppHandle/State),\
|
||||
勿盲目重试同调用(task_id={}, target_status={});\
|
||||
若需推进任务,重新发起 run_workflow tool_call 走审批流程。",
|
||||
task_id, target_status
|
||||
))
|
||||
}
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user