新增: 任务关联工程(module_id 数据层V41 + AI module 写工具6个 + 任务构造补字段)
This commit is contained in:
@@ -1361,18 +1361,22 @@ mod tests {
|
||||
// mongo_update/mongo_delete/redis_get/redis_set/redis_del,声明式注册在 tools/local_proxy.rs,
|
||||
// build_ai_tool_registry 加第 5 参 LocalProxyCtx)。
|
||||
// 66 = 36 data + 14 file + 1 http + 1 fetch_url + 1 fetch_search + 1 generate_image + 1 get_app_config + 11 local_proxy。
|
||||
// 工程模块写工具(2026-08-08): data 层 36→42(新增 create_module/update_module/delete_module +
|
||||
// list_module_dependencies/add_module_dependency/remove_module_dependency,扩展 tools/task_graph.rs,
|
||||
// 复用 module.rs CRUD 逻辑;delete_module 级联清理依赖边防外键约束)。
|
||||
// 72 = 42 data + 14 file + 1 http + 1 fetch_url + 1 fetch_search + 1 generate_image + 1 get_app_config + 11 local_proxy。
|
||||
assert_eq!(
|
||||
registry.len(),
|
||||
66,
|
||||
"工具总数应为 66(36 data + 14 file + 1 http + 1 fetch_url + 1 fetch_search + 1 generate_image + 1 get_app_config + 11 local_proxy),实际 {}", registry.len()
|
||||
72,
|
||||
"工具总数应为 72(42 data + 14 file + 1 http + 1 fetch_url + 1 fetch_search + 1 generate_image + 1 get_app_config + 11 local_proxy),实际 {}", registry.len()
|
||||
);
|
||||
|
||||
// 工具名集合基线:防 rename / 漏注册 / 误删除。
|
||||
// data 层 36 个(持 db):CRUD/状态机/工作流/知识图谱任务关联 + 项目事件流 + 基础设施配置 + git 工具
|
||||
// data 层 42 个(持 db):CRUD/状态机/工作流/知识图谱任务关联 + 项目事件流 + 基础设施配置 + git 工具 + 工程模块写工具
|
||||
// file 层 14 个(不持 db):命令/读/列/写/改/元/追加/删/移/搜/grep/环境探测/符号解析/下载
|
||||
// http 层 1 个(不持 db):http_request
|
||||
let mut expected: Vec<&str> = vec![
|
||||
// ── data 层 (36) ──
|
||||
// ── data 层 (42) ──
|
||||
"list_projects", "list_tasks", "list_ideas",
|
||||
"update_project", "create_project", "bind_directory",
|
||||
"create_task", "update_task", "advance_task",
|
||||
@@ -1389,6 +1393,9 @@ mod tests {
|
||||
"add_project_service", "list_project_services",
|
||||
// 工程系统:项目工程列表查询
|
||||
"list_project_modules",
|
||||
// 工程模块写工具(2026-08-08):create/update/delete 工程 + 依赖关系管理
|
||||
"create_module", "update_module", "delete_module",
|
||||
"list_module_dependencies", "add_module_dependency", "remove_module_dependency",
|
||||
// Git 只读工具
|
||||
"git_status", "git_diff", "git_log",
|
||||
// Git 写工具
|
||||
|
||||
@@ -141,6 +141,7 @@ pub fn register(registry: &mut AiToolRegistry, db: &Arc<Database>) {
|
||||
output_json: None,
|
||||
// F-260619-01 可选关联灵感(空字符串/缺省视为不关联)
|
||||
idea_id: args.get("idea_id").and_then(|v| v.as_str()).filter(|s| !s.is_empty()).map(String::from),
|
||||
module_id: args.get("module_id").and_then(|v| v.as_str()).filter(|s| !s.is_empty()).map(String::from),
|
||||
// 知识图谱 Phase 1 V29 三列:经上方校验的 queue / parent_id / content_json
|
||||
queue,
|
||||
parent_id,
|
||||
|
||||
@@ -1,24 +1,27 @@
|
||||
//! 知识图谱 / 工程系统类 AI 工具声明式注册(register_task_graph_tools 10 个迁入)。
|
||||
//! 知识图谱 / 工程系统类 AI 工具声明式注册(register_task_graph_tools 16 个迁入)。
|
||||
//!
|
||||
//! 迁自 `tool_registry.rs::register_task_graph_tools`(原 10 个:task_link CRUD + 父子树
|
||||
//! + 跨池移动 + content 更新 + 项目事件流 + 项目基础设施配置 + 工程列表),改用 `declare_tool!` 宏。
|
||||
//! 后续在声明式注册中新增 6 个工程模块写工具(create_module/update_module/delete_module +
|
||||
//! list/add/remove_module_dependency),共 16 个。
|
||||
//!
|
||||
//! 迁移策略(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) 来源,与原同源)。
|
||||
//! - 复用类型:new_id/ProjectServiceRecord/ProjectModuleRecord/ModuleDependencyRecord/now_millis
|
||||
//! (各 pub(crate) 来源,与原同源)。
|
||||
//!
|
||||
//! 等价性验证:基线测试 `test_build_ai_tool_registry_baseline_tool_count` 仍断言 48 总量 +
|
||||
//! 工具名集合稳定(防 rename / 漏注册)。
|
||||
//! 等价性验证:基线测试 `test_build_ai_tool_registry_baseline_tool_count` 断言工具名集合稳定
|
||||
//! (防 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_storage::models::{ModuleDependencyRecord, ProjectModuleRecord, ProjectServiceRecord};
|
||||
use df_types::types::new_id;
|
||||
|
||||
use crate::commands::now_millis;
|
||||
@@ -410,4 +413,318 @@ pub fn register(registry: &mut AiToolRegistry, db: &Arc<Database>) {
|
||||
}))
|
||||
}
|
||||
);
|
||||
|
||||
// ── create_module (Medium 写库,工程系统) ──
|
||||
// 创建工程(module)。复用 module.rs::add_project_module 的创建逻辑:
|
||||
// trim + 非空校验 + sort_order 取当前项目工程数末尾追加,stack/description 空串归一 None。
|
||||
// AI 用途:多工程场景 AI 主动登记新工程(发现新代码仓库 / 初始化子项目)。
|
||||
declare_tool!(
|
||||
registry,
|
||||
db: Arc<Database>,
|
||||
"create_module",
|
||||
"创建工程(module,独立代码仓库)。参数:project_id(项目 ID)、name(工程名)、path(工程目录绝对路径)、stack(可选,技术栈,如 rust/vue)、description(可选,工程职责描述)。返回新建的完整工程记录(含 id/sort_order/status)。中等风险,写库需审批",
|
||||
RiskLevel::Medium,
|
||||
schema: object_schema(vec![
|
||||
("project_id", "string", true),
|
||||
("name", "string", true),
|
||||
("path", "string", true),
|
||||
("stack", "string", false),
|
||||
("description", "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 path = args["path"].as_str()
|
||||
.ok_or_else(|| anyhow::anyhow!("缺少 path"))?
|
||||
.trim()
|
||||
.to_string();
|
||||
if project_id.is_empty() {
|
||||
anyhow::bail!("project_id 不能为空");
|
||||
}
|
||||
if name.is_empty() {
|
||||
anyhow::bail!("name 不能为空");
|
||||
}
|
||||
if path.is_empty() {
|
||||
anyhow::bail!("path 不能为空");
|
||||
}
|
||||
// 可选字段 trim + 空串归一 None(对齐 module.rs trim_opt 语义)。
|
||||
let stack = args.get("stack")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(str::trim)
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(|s| s.to_string());
|
||||
let description = args.get("description")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(str::trim)
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(|s| s.to_string());
|
||||
|
||||
let repo = df_storage::crud::ProjectModuleRepo::new(&db);
|
||||
// sort_order 取当前项目下工程数(末尾追加,新工程排末位,对齐 add_project_module)。
|
||||
let existing = repo.list_by_project(&project_id).await?;
|
||||
let sort_order = existing.len() as i32;
|
||||
let record = ProjectModuleRecord {
|
||||
id: new_id(),
|
||||
project_id,
|
||||
name,
|
||||
path,
|
||||
git_url: None,
|
||||
stack,
|
||||
auto_detected: false,
|
||||
sort_order,
|
||||
created_at: now_millis(),
|
||||
updated_at: now_millis(),
|
||||
description,
|
||||
// 未传 status → None(应用层视为 active,对齐 GUI add_project_module 语义)。
|
||||
status: None,
|
||||
};
|
||||
let record_id = record.id.clone();
|
||||
let ok = repo.insert(record).await?;
|
||||
if !ok {
|
||||
anyhow::bail!("插入工程记录失败");
|
||||
}
|
||||
let inserted = repo.get_by_id(&record_id).await?
|
||||
.ok_or_else(|| anyhow::anyhow!("插入后回读失败"))?;
|
||||
Ok(serde_json::to_value(&inserted)?)
|
||||
}
|
||||
);
|
||||
|
||||
// ── update_module (Medium 写库,工程系统) ──
|
||||
// 更新工程(部分更新:仅传字段被覆盖,其余保留)。复用 module.rs::update_project_module
|
||||
// 的部分更新语义:name/path 传空串拒绝;stack/description 传空串清空。
|
||||
declare_tool!(
|
||||
registry,
|
||||
db: Arc<Database>,
|
||||
"update_module",
|
||||
"更新工程(module,部分更新:仅传入字段被覆盖,其余保留)。参数:id(工程 ID)、name/path/stack/description(可选,至少提供一个)。约束:name/path 不能为空串;stack/description 传空串清空。返回更新后的完整工程记录",
|
||||
RiskLevel::Medium,
|
||||
schema: object_schema(vec![
|
||||
("id", "string", true),
|
||||
("name", "string", false),
|
||||
("path", "string", false),
|
||||
("stack", "string", false),
|
||||
("description", "string", false),
|
||||
]),
|
||||
args => {
|
||||
let id = args["id"].as_str()
|
||||
.ok_or_else(|| anyhow::anyhow!("缺少 id"))?
|
||||
.trim()
|
||||
.to_string();
|
||||
if id.is_empty() {
|
||||
anyhow::bail!("id 不能为空");
|
||||
}
|
||||
// 解析待更新字段(args.get 区分「未提供」vs「提供空串」:未提供保留原值)。
|
||||
let name = args.get("name").and_then(|v| v.as_str());
|
||||
let path = args.get("path").and_then(|v| v.as_str());
|
||||
let stack = args.get("stack").and_then(|v| v.as_str());
|
||||
let description = args.get("description").and_then(|v| v.as_str());
|
||||
if name.is_none() && path.is_none() && stack.is_none() && description.is_none() {
|
||||
anyhow::bail!("至少提供一个待更新字段 (name/path/stack/description)");
|
||||
}
|
||||
|
||||
let repo = df_storage::crud::ProjectModuleRepo::new(&db);
|
||||
// 先校验存在(404 友好错误),再整体更新(保留不可变字段,对齐 update_project_module)。
|
||||
let mut existing = repo.get_by_id(&id).await?
|
||||
.ok_or_else(|| anyhow::anyhow!("工程 {id} 不存在"))?;
|
||||
if let Some(n) = name {
|
||||
let n = n.trim().to_string();
|
||||
if n.is_empty() {
|
||||
anyhow::bail!("name 不能为空");
|
||||
}
|
||||
existing.name = n;
|
||||
}
|
||||
if let Some(p) = path {
|
||||
let p = p.trim().to_string();
|
||||
if p.is_empty() {
|
||||
anyhow::bail!("path 不能为空");
|
||||
}
|
||||
existing.path = p;
|
||||
}
|
||||
// stack/description 仅在传入时覆盖;空串归一 None(清空,对齐 trim_opt 语义)。
|
||||
if let Some(s) = stack {
|
||||
existing.stack = Some(s.trim().to_string())
|
||||
.filter(|v| !v.is_empty());
|
||||
}
|
||||
if let Some(d) = description {
|
||||
existing.description = Some(d.trim().to_string())
|
||||
.filter(|v| !v.is_empty());
|
||||
}
|
||||
|
||||
let hit = repo.update_full(&existing).await?;
|
||||
if !hit {
|
||||
anyhow::bail!("工程 {id} 不存在(更新未命中)");
|
||||
}
|
||||
let updated = repo.get_by_id(&id).await?
|
||||
.ok_or_else(|| anyhow::anyhow!("更新后回读失败"))?;
|
||||
Ok(serde_json::to_value(&updated)?)
|
||||
}
|
||||
);
|
||||
|
||||
// ── delete_module (High 破坏性,工程系统) ──
|
||||
// 删除工程(硬删,对齐 GUI remove_project_module 语义)。
|
||||
// 谨慎处理依赖关系:module_dependencies 表 from_module_id/to_module_id 均有外键
|
||||
// REFERENCES project_modules(id),且 SQLite PRAGMA foreign_keys=ON——直接删被依赖
|
||||
// 的工程会撞外键约束。故先级联删除引用该工程的依赖边,再删工程本体。
|
||||
declare_tool!(
|
||||
registry,
|
||||
db: Arc<Database>,
|
||||
"delete_module",
|
||||
"删除工程(module,硬删,不可恢复)。参数:id(工程 ID)。高风险,需审批。若该工程被依赖关系引用(from 或 to),先级联删除相关依赖边再删除工程本体(防外键约束阻塞)。返回 { deleted, removed_dependencies, id }",
|
||||
RiskLevel::High,
|
||||
schema: object_schema(vec![
|
||||
("id", "string", true),
|
||||
]),
|
||||
args => {
|
||||
let id = args["id"].as_str()
|
||||
.ok_or_else(|| anyhow::anyhow!("缺少 id"))?
|
||||
.trim()
|
||||
.to_string();
|
||||
if id.is_empty() {
|
||||
anyhow::bail!("id 不能为空");
|
||||
}
|
||||
let module_repo = df_storage::crud::ProjectModuleRepo::new(&db);
|
||||
let module = module_repo.get_by_id(&id).await?
|
||||
.ok_or_else(|| anyhow::anyhow!("工程 {id} 不存在"))?;
|
||||
// 级联清理依赖边:先列项目全部依赖,过滤引用本工程的边逐条删除(外键约束先清引用)。
|
||||
let dep_repo = df_storage::crud::ModuleDependencyRepo::new(&db);
|
||||
let deps = dep_repo.list_by_field("project_id", &module.project_id).await?;
|
||||
let mut removed_dependencies = 0usize;
|
||||
for d in deps {
|
||||
if d.from_module_id == id || d.to_module_id == id {
|
||||
if dep_repo.delete(&d.id).await? {
|
||||
removed_dependencies += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
let deleted = module_repo.delete(&id).await?;
|
||||
Ok(serde_json::json!({
|
||||
"deleted": deleted,
|
||||
"removed_dependencies": removed_dependencies,
|
||||
"id": id,
|
||||
}))
|
||||
}
|
||||
);
|
||||
|
||||
// ── list_module_dependencies (Low 只读,工程系统) ──
|
||||
// 列出项目工程依赖关系(AI 了解工程间调用/依赖拓扑,决定构建/部署顺序)。
|
||||
declare_tool!(
|
||||
registry,
|
||||
db: Arc<Database>,
|
||||
"list_module_dependencies",
|
||||
"列出项目全部工程依赖关系(只读)。参数:project_id(项目 ID)。返回 { items: 依赖边列表(每条含 id/from_module_id/to_module_id/dep_type/label), total, project_id }。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::ModuleDependencyRepo::new(&db);
|
||||
let items = repo.list_by_field("project_id", &project_id).await?;
|
||||
let total = items.len();
|
||||
Ok(serde_json::json!({
|
||||
"items": items,
|
||||
"total": total,
|
||||
"project_id": project_id,
|
||||
}))
|
||||
}
|
||||
);
|
||||
|
||||
// ── add_module_dependency (Medium 写库,工程系统) ──
|
||||
// 添加工程依赖边(对齐 GUI add_module_dependency:dep_type 默认 library,label 可选)。
|
||||
// AI 用途:登记工程间依赖(如 API 调用 / 共享库),供依赖图 / 拓扑排序使用。
|
||||
declare_tool!(
|
||||
registry,
|
||||
db: Arc<Database>,
|
||||
"add_module_dependency",
|
||||
"添加工程依赖关系(module_dependencies 边,登记 from 依赖 to)。参数:project_id(项目 ID)、from_module_id(依赖方工程 ID)、to_module_id(被依赖方工程 ID)、dep_type(可选,依赖类型 library/api/mq/shared/custom,默认 library)、label(可选,备注)。返回新增完整记录。中等风险,写库需审批",
|
||||
RiskLevel::Medium,
|
||||
schema: object_schema(vec![
|
||||
("project_id", "string", true),
|
||||
("from_module_id", "string", true),
|
||||
("to_module_id", "string", true),
|
||||
("dep_type", "string", false),
|
||||
("label", "string", false),
|
||||
]),
|
||||
args => {
|
||||
let project_id = args["project_id"].as_str()
|
||||
.ok_or_else(|| anyhow::anyhow!("缺少 project_id"))?
|
||||
.trim()
|
||||
.to_string();
|
||||
let from_module_id = args["from_module_id"].as_str()
|
||||
.ok_or_else(|| anyhow::anyhow!("缺少 from_module_id"))?
|
||||
.trim()
|
||||
.to_string();
|
||||
let to_module_id = args["to_module_id"].as_str()
|
||||
.ok_or_else(|| anyhow::anyhow!("缺少 to_module_id"))?
|
||||
.trim()
|
||||
.to_string();
|
||||
if project_id.is_empty() || from_module_id.is_empty() || to_module_id.is_empty() {
|
||||
anyhow::bail!("project_id / from_module_id / to_module_id 不能为空");
|
||||
}
|
||||
let dep_type = args.get("dep_type")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(str::trim)
|
||||
.filter(|s| !s.is_empty())
|
||||
.unwrap_or("library")
|
||||
.to_string();
|
||||
let label = args.get("label")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(str::trim)
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(|s| s.to_string());
|
||||
|
||||
let record = ModuleDependencyRecord {
|
||||
id: new_id(),
|
||||
project_id,
|
||||
from_module_id,
|
||||
to_module_id,
|
||||
dep_type,
|
||||
label,
|
||||
created_at: now_millis(),
|
||||
};
|
||||
let repo = df_storage::crud::ModuleDependencyRepo::new(&db);
|
||||
let ok = repo.insert(record.clone()).await?;
|
||||
if !ok {
|
||||
anyhow::bail!("插入依赖记录失败");
|
||||
}
|
||||
Ok(serde_json::to_value(&record)?)
|
||||
}
|
||||
);
|
||||
|
||||
// ── remove_module_dependency (Medium 写库,工程系统) ──
|
||||
// 删除工程依赖边(按依赖记录 id)。对齐 GUI remove_module_dependency 语义。
|
||||
declare_tool!(
|
||||
registry,
|
||||
db: Arc<Database>,
|
||||
"remove_module_dependency",
|
||||
"删除工程依赖关系(按依赖记录 id)。参数:id(依赖记录 ID)。返回 deleted(布尔,是否命中,不存在则 false)。中等风险,写库需审批",
|
||||
RiskLevel::Medium,
|
||||
schema: object_schema(vec![
|
||||
("id", "string", true),
|
||||
]),
|
||||
args => {
|
||||
let id = args["id"].as_str()
|
||||
.ok_or_else(|| anyhow::anyhow!("缺少 id"))?
|
||||
.trim()
|
||||
.to_string();
|
||||
if id.is_empty() {
|
||||
anyhow::bail!("id 不能为空");
|
||||
}
|
||||
let repo = df_storage::crud::ModuleDependencyRepo::new(&db);
|
||||
let deleted = repo.delete(&id).await?;
|
||||
Ok(serde_json::json!({ "deleted": deleted, "id": id }))
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -69,6 +69,10 @@ pub struct CreateTaskInput {
|
||||
/// 关联灵感 ID(F-260619-01,1对1 单向,可空=不关联)。
|
||||
/// 空字符串视为不关联(与 AI 工具层一致)。
|
||||
pub idea_id: Option<String>,
|
||||
/// 关联工程 ID(多工程:任务属于某个 module,可空=不关联)。
|
||||
/// 空字符串视为 None(向后兼容)。
|
||||
#[serde(default)]
|
||||
pub module_id: Option<String>,
|
||||
/// 管理维度池(知识图谱 Phase 1 V29,对标设计 §2.1)。默认 "todo"(待办池)。
|
||||
/// 合法值:backlog / todo / decision / active / done。非法值在 IPC 层兜底校验。
|
||||
/// 空字符串视为默认 todo(向后兼容,与 idea_id 一致处理)。
|
||||
@@ -304,6 +308,8 @@ pub async fn create_task(
|
||||
output_json: None,
|
||||
// F-260619-01:空字符串视为不关联(与 AI 工具层一致)
|
||||
idea_id: input.idea_id.filter(|s| !s.is_empty()),
|
||||
// 多工程:可选关联到具体工程(module)
|
||||
module_id: input.module_id.filter(|s| !s.is_empty()),
|
||||
// 知识图谱 Phase 1 V29 三列:经上方校验的 queue / parent_id / content_json
|
||||
queue,
|
||||
parent_id,
|
||||
|
||||
Reference in New Issue
Block a user