Files
DevFlow/src-tauri/src/commands/ai/tools/project.rs
T

185 lines
9.4 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 项目类 AI 工具声明式注册(register_project_tools 剩余 7 个迁入)。
//!
//! 第一步试点已迁 `list_projects`(见 tools/list_projects.rs),本文件迁完
//! `register_project_tools` 的剩余 7 个:update_project/create_project/bind_directory/
//! delete_project/restore_project/purge_project/get_project_count。
//!
//! 迁移策略(handler 逻辑零变更):
//! - handler body 逐字照搬原 `register_project_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 与原手写定义逐字一致。
//! - `bind_dir_to_project`(create_project/bind_directory 共用)原为 tool_registry.rs
//! 私有 helper,本模块跨 super::tool_registry 调用,故该 helper 已改 `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::ProjectRecord;
use df_types::types::{new_id, ProjectStatus};
use crate::commands::now_millis;
use crate::commands::ai::tool_registry::bind_dir_to_project;
/// 注册剩余 7 个项目类工具(update/create/bind/delete/restore/purge/get_count)到 `$registry`。
///
/// 与原手写 register(name, desc, schema, risk, handler) 语义 1:1:
/// - name/desc/schema 字符串与 JSON Schema 逐字照搬原定义
/// - risk 与原一致(update/create/bind=Medium,delete/restore/purge=High,get_count=Low)
/// - handler body 与原 async move 块逐字一致(逻辑零变更)
///
/// 唯一差异:闭包包装改由 `declare_tool!` 宏生成,handler body 直接写业务逻辑。
pub fn register(registry: &mut AiToolRegistry, db: &Arc<Database>) {
// ── update_project (Medium) ──
declare_tool!(
registry,
db: Arc<Database>,
"update_project",
"更新项目的指定字段(name/status/description/path/stack),需要提供项目 ID、字段名和新值。绑定代码目录推荐改用 bind_directory",
RiskLevel::Medium,
schema: object_schema(vec![("id", "string", true), ("field", "string", true), ("value", "string", true)]),
args => {
let id = args["id"].as_str().ok_or_else(|| anyhow::anyhow!("缺少 id"))?;
let field = args["field"].as_str().ok_or_else(|| anyhow::anyhow!("缺少 field"))?;
let value = args["value"].as_str().ok_or_else(|| anyhow::anyhow!("缺少 value"))?;
// 复用 df-storage CRUD 白名单(按表隔离),与 update_field 校验同源
if !df_storage::crud::is_allowed_column("projects", field) {
anyhow::bail!("不允许更新字段 '{}'", field);
}
let repo = df_storage::crud::ProjectRepo::new(&db);
// 透传 affected:0 行(id 不存在 / 已软删)→ updated=false + name 空,
// 让 AI 知道 id 不对而非假成功(对齐 update_idea / update_task)。
let updated = repo.update_field(id, field, value).await?;
// 返回 name 供前端卡片友好展示(对齐 update_task 返 title);update_field 返 bool 不含
// record,单独 get_by_id 取 name;0 行时 get_by_id 返 None→name 空,配合 updated:false。
let name = repo.get_by_id(id).await?.map(|p| p.name).unwrap_or_default();
Ok(serde_json::json!({ "id": id, "name": name, "field": field, "updated": updated }))
}
);
// ── create_project (Medium) ──
declare_tool!(
registry,
db: Arc<Database>,
"create_project",
"创建新项目,可选传 path/stack 一步完成创建+绑定目录(无需再调 bind_directory",
RiskLevel::Medium,
schema: object_schema(vec![
("name", "string", true), ("description", "string", false),
("path", "string", false), ("stack", "string", false),
]),
args => {
let name = args["name"].as_str().ok_or_else(|| anyhow::anyhow!("缺少 name 参数"))?;
let description = args["description"].as_str().unwrap_or("");
let repo = df_storage::crud::ProjectRepo::new(&db);
let record = ProjectRecord {
id: new_id(), name: name.to_string(), description: description.to_string(),
status: ProjectStatus::Planning, idea_id: None,
path: None, stack: None,
created_at: now_millis(), updated_at: now_millis(),
};
let id = record.id.clone();
repo.insert(record).await?;
// path 缺省:保持原行为(仅创建,不绑定);有 path 则复用公共绑定闭包(create_project 与 bind_directory 同源)
let path_opt = args.get("path").and_then(|v| v.as_str()).filter(|s| !s.is_empty());
if let Some(path) = path_opt {
let stack_opt = args.get("stack").and_then(|v| v.as_str()).filter(|s| !s.is_empty());
let stack = bind_dir_to_project(&repo, &id, path, stack_opt).await?;
Ok(serde_json::json!({ "id": id, "name": name, "status": "planning", "path": path, "stack": stack, "bound": true }))
} else {
Ok(serde_json::json!({ "id": id, "name": name, "status": "planning" }))
}
}
);
// ── bind_directory (Medium) ──
declare_tool!(
registry,
db: Arc<Database>,
"bind_directory",
"为项目绑定代码目录(自动探测技术栈,防重复绑定)。path 应为**项目根路径**(单仓库=仓库根;多工程 monorepo=工作区根,非单个子工程路径;子工程信息走 list_project_modules/update_project_module 维护)。多次调用会覆盖 path 字段,monorepo 场景勿为每个子工程重复绑定。可选 stack 手动指定技术栈(如 'Go / Gin / Vue3'),不传则自动探测。",
RiskLevel::Medium,
schema: object_schema(vec![("id", "string", true), ("path", "string", true), ("stack", "string", false)]),
args => {
let id = args["id"].as_str().ok_or_else(|| anyhow::anyhow!("缺少 id 参数"))?;
let path = args["path"].as_str().ok_or_else(|| anyhow::anyhow!("缺少 path 参数"))?;
let stack_opt = args.get("stack").and_then(|v| v.as_str()).filter(|s| !s.is_empty());
let repo = df_storage::crud::ProjectRepo::new(&db);
let stack = bind_dir_to_project(&repo, id, path, stack_opt).await?;
// F-260619-03: 项目绑定目录加 AI 白名单由 reload_allowed_dirs 自动读 projects.bind_directory
// 统一处理(优雅:绑定 = 自动授权,reload 时合并)。bind handler 不直接写 allowed_dirs
// (register_project_tools 无 allowed_dirs 参数,且 reload 集中更易维护)。
// 绑定后触发 reload:通过 app_handle emit 事件让 state reload,或下次工具调用前 reload 兜底。
Ok(serde_json::json!({ "id": id, "path": path, "stack": stack, "bound": true }))
}
);
// ── delete_project (High) ──
declare_tool!(
registry,
db: Arc<Database>,
"delete_project",
"删除项目(移入回收站,可恢复。永久删除用 purge_project",
RiskLevel::High,
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::ProjectRepo::new(&db);
let deleted = repo.soft_delete(id).await?;
Ok(serde_json::json!({ "deleted": deleted, "id": id }))
}
);
// ── restore_project (High) ──
declare_tool!(
registry,
db: Arc<Database>,
"restore_project",
"从回收站恢复已删除项目",
RiskLevel::High,
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::ProjectRepo::new(&db);
let restored = repo.restore(id).await?;
Ok(serde_json::json!({ "restored": restored, "id": id }))
}
);
// ── purge_project (High) ──
declare_tool!(
registry,
db: Arc<Database>,
"purge_project",
"永久删除项目及关联数据(不可恢复)",
RiskLevel::High,
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::ProjectRepo::new(&db);
let purged = repo.purge_with_descendants(id).await?;
Ok(serde_json::json!({ "purged": purged, "id": id }))
}
);
// ── get_project_count (Low risk, F-260616-08) ──
declare_tool!(
registry,
db: Arc<Database>,
"get_project_count",
"获取项目总数(未删除项目),用于分页策略判断。返回 { total: usize }",
RiskLevel::Low,
schema: object_schema(vec![]),
_args => {
let repo = df_storage::crud::ProjectRepo::new(&db);
let items = repo.list_active().await?;
Ok(serde_json::json!({ "total": items.len() }))
}
);
}