新增: 知识图谱Phase1业务层(父②) + update_task parent_id 校验(P1)
父② 业务层(②.3+②.4+②.5): - IPC: create_task 扩展 queue/parent_id/content_json + create/remove/list_task_links + move_task_queue(一致性约束联动 status) + get_task_tree - 父聚合(②.4): advance_task 后重算父 status(count_children_by_status,set_status_for_aggregation 专用路径绕 D-260616-04 status 收口,父任务=容器模型唯一非状态机写入) - AI 工具(②.5): register_task_graph_tools 分组 6 工具 + create_task 扩展,基线测试 32→38 - state.task_links: TaskLinkRepo + lib.rs 注册 5 新 IPC P1 修复(verify agent 发现,主控修): - update_task 漏 parent_id 校验(可绕 1 级嵌套创建孙任务/悬空 parent_id/自环) - 补 parent_id 特判(对标 create_task: 父存在 + 父自身无 parent_id + 自环拒) df-storage 99 lib + df-ai 331 + 基线测试 38 全过。
This commit is contained in:
@@ -490,12 +490,15 @@ fn register_http_tools(registry: &mut AiToolRegistry) {
|
||||
);
|
||||
}
|
||||
|
||||
/// 数据层 AI 工具注册(18 个持 db 的 CRUD/状态机/工作流工具)——从 build_ai_tool_registry 抽出。
|
||||
/// 数据层 AI 工具注册(24 个持 db 的 CRUD/状态机/工作流/知识图谱工具)——从 build_ai_tool_registry 抽出。
|
||||
///
|
||||
/// 工具闭包捕获 `db: &Arc<Database>` Arc 重建 Repo(列表/创建/更新/删除/状态推进/工作流)。
|
||||
/// 工具闭包捕获 `db: &Arc<Database>` Arc 重建 Repo(列表/创建/更新/删除/状态推进/工作流/任务关联)。
|
||||
/// 例外:run_workflow handler 防御返回 Err(CR-52),不持 db 不 clone,真正执行经
|
||||
/// ai_approve → run_workflow_inner 另走完整 State 路径。
|
||||
///
|
||||
/// 知识图谱 Phase 1(2026-06-26):新增 register_task_graph_tools 子函数(6 工具:task_link CRUD
|
||||
/// + 父子树 + 跨池移动 + content 更新),data 层 18→24。
|
||||
///
|
||||
/// SMELL-P0-2:抽自原 build_ai_tool_registry 1091 行单函数(数据+文件混合)。
|
||||
/// 18 个 register 调用【原样移入】,零行为变更,仅机械搬运。
|
||||
///
|
||||
@@ -505,11 +508,14 @@ fn register_http_tools(registry: &mut AiToolRegistry) {
|
||||
/// - register_workflow_tools(1):run_workflow(High,单独,handler 防御兜底)
|
||||
/// - register_idea_tools(2):list/create
|
||||
/// - register_trash_tools(1):list_trash
|
||||
/// AiToolRegistry 底层 HashMap(注册顺序无关),拆分后工具集合与原一致,
|
||||
/// 行为零变更(基线测试 test_build_ai_tool_registry_baseline_tool_count 守护)。
|
||||
/// 知识图谱 Phase 1(2026-06-26)新增第 6 子函数:
|
||||
/// - register_task_graph_tools(6):task_link CRUD + 父子树 + 跨池移动 + content 更新
|
||||
/// AiToolRegistry 底层 HashMap(注册顺序无关),拆分后工具集合与原一致 + 新增 6,
|
||||
/// 行为零变更(create_task 扩展参数 + 6 新工具,基线测试 test_build_ai_tool_registry_baseline_tool_count 守护)。
|
||||
fn register_data_tools(registry: &mut AiToolRegistry, db: &Arc<Database>) {
|
||||
register_project_tools(registry, db);
|
||||
register_task_tools(registry, db);
|
||||
register_task_graph_tools(registry, db);
|
||||
register_workflow_tools(registry);
|
||||
register_idea_tools(registry, db);
|
||||
register_trash_tools(registry, db);
|
||||
@@ -693,8 +699,14 @@ fn register_task_tools(registry: &mut AiToolRegistry, db: &Arc<Database>) {
|
||||
})},
|
||||
);
|
||||
registry.register(
|
||||
"create_task", "在指定项目下创建新任务,可选传 idea_id 关联灵感(1对1 单向)",
|
||||
df_ai::ai_tools::object_schema(vec![("project_id", "string", true), ("title", "string", true), ("description", "string", false), ("priority", "integer", false), ("idea_id", "string", false)]),
|
||||
"create_task", "在指定项目下创建新任务,可选传 idea_id 关联灵感(1对1 单向)。知识图谱 Phase 1 扩展三个可选参数:queue(管理维度池 backlog/todo/decision,默认 todo,新建不可直接落 active/done 须经 move_task_queue 流转)、parent_id(父任务 ID,限 1 级嵌套,父任务自身不能有 parent_id,IPC 层校验防孙任务)、content_json(结构化需求规格 JSON 字符串 {background,acceptance_criteria[],scope[],technical_design},须合法 JSON)",
|
||||
df_ai::ai_tools::object_schema(vec![
|
||||
("project_id", "string", true), ("title", "string", true),
|
||||
("description", "string", false), ("priority", "integer", false),
|
||||
("idea_id", "string", false),
|
||||
("queue", "string", false), ("parent_id", "string", false),
|
||||
("content_json", "string", false),
|
||||
]),
|
||||
RiskLevel::Medium,
|
||||
{ let db = db.clone(); Box::new(move |args: serde_json::Value| {
|
||||
let db = db.clone();
|
||||
@@ -702,6 +714,59 @@ fn register_task_tools(registry: &mut AiToolRegistry, db: &Arc<Database>) {
|
||||
let project_id = args["project_id"].as_str().ok_or_else(|| anyhow::anyhow!("缺少 project_id"))?;
|
||||
let title = args["title"].as_str().ok_or_else(|| anyhow::anyhow!("缺少 title"))?;
|
||||
let repo = df_storage::crud::TaskRepo::new(&db);
|
||||
|
||||
// ── 知识图谱 Phase 1:queue/parent_id/content_json 校验(对齐 commands::task::create_task IPC)──
|
||||
// queue:白名单 + 空串默认 todo。create_task 时 status 恒 todo,仅 backlog/todo/decision
|
||||
// 合法(active/done 须经 move_task_queue 流转,新建直接落违反一致性约束)。
|
||||
const TASK_QUEUE_VALUES: &[&str] = &["backlog", "todo", "decision", "active", "done"];
|
||||
let queue = match args.get("queue").and_then(|v| v.as_str()) {
|
||||
None | Some("") => "todo".to_string(),
|
||||
Some(q) => {
|
||||
let q = q.trim();
|
||||
if !TASK_QUEUE_VALUES.contains(&q) {
|
||||
anyhow::bail!("非法 queue 值 {:?},合法值: {:?}", q, TASK_QUEUE_VALUES);
|
||||
}
|
||||
// 一致性:create_task 时 status 恒 todo,仅 backlog/todo/decision 合法
|
||||
match q {
|
||||
"backlog" | "todo" | "decision" => q.to_string(),
|
||||
other => anyhow::bail!(
|
||||
"新建任务 queue 不可直接落 {:?}(须经 move_task_queue 流转),当前仅允许 backlog/todo/decision",
|
||||
other
|
||||
),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// parent_id:1 级嵌套铁律(对标设计 §2.1 D2)。空串/缺省=None;
|
||||
// 非空时校验父任务存在 + 父任务自身无 parent_id(防孙任务)。
|
||||
let parent_id = args
|
||||
.get("parent_id")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.trim().to_string())
|
||||
.filter(|s| !s.is_empty());
|
||||
if let Some(pid) = &parent_id {
|
||||
let parent = repo
|
||||
.get_by_id(pid)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow::anyhow!("父任务 {pid} 不存在,无法创建子任务"))?;
|
||||
if parent.parent_id.is_some() {
|
||||
anyhow::bail!(
|
||||
"违反 1 级嵌套铁律:父任务 {pid} 自身是子任务(parent_id={:?}),不允许在其下创建孙任务",
|
||||
parent.parent_id
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// content_json:轻量 JSON 合法性校验(非空时须是合法 JSON),结构细节由 AI 消费层负责。
|
||||
let content_json = match args.get("content_json").and_then(|v| v.as_str()) {
|
||||
Some(c) if !c.trim().is_empty() => {
|
||||
serde_json::from_str::<serde_json::Value>(c)
|
||||
.map_err(|e| anyhow::anyhow!("content_json 不是合法 JSON: {e}"))?;
|
||||
Some(c.to_string())
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let record = TaskRecord {
|
||||
id: new_id(), project_id: project_id.to_string(), title: title.to_string(),
|
||||
description: args["description"].as_str().unwrap_or("").to_string(),
|
||||
@@ -712,12 +777,10 @@ fn register_task_tools(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),
|
||||
// 知识图谱 Phase 1 V29 三列:本批仅数据层迁移,create_task 参数扩展为 Phase 1
|
||||
// 后续 AI 工具任务。此处默认值(queue='todo'/parent_id=None/content_json=None)
|
||||
// 与 DB 列 DEFAULT 及 commands::task::create_task 一致。
|
||||
queue: "todo".to_string(),
|
||||
parent_id: None,
|
||||
content_json: None,
|
||||
// 知识图谱 Phase 1 V29 三列:经上方校验的 queue / parent_id / content_json
|
||||
queue,
|
||||
parent_id,
|
||||
content_json,
|
||||
created_at: now_millis(), updated_at: now_millis(),
|
||||
};
|
||||
let id = record.id.clone();
|
||||
@@ -818,6 +881,221 @@ fn register_task_tools(registry: &mut AiToolRegistry, db: &Arc<Database>) {
|
||||
}
|
||||
|
||||
/// 工作流类 AI 工具注册(1 个:run_workflow)——不持 db(handler 防御兜底,详见注释)。
|
||||
/// 知识图谱 Phase 1 AI 工具注册(6 个:task_link CRUD + 父子树 + 跨池移动 + content 更新)
|
||||
/// —— 持 db:Arc<Database>,对标设计 §五 Phase1 AI 工具表 + §2.1/§2.2。
|
||||
///
|
||||
/// 工具直接调 df_storage::crud Repo(与 register_task_tools 同源模式,经 db Arc 重建 Repo),
|
||||
/// 复用 IPC 层(commands::task)的底层 Repo 方法,不重复实现业务校验:
|
||||
/// - link CRUD / 反查:TaskLinkRepo(create_link 内含 link_type 白名单 + depends_on BFS 环检测)。
|
||||
/// - 父子树:get_children(限 1 级嵌套,设计 D2)。
|
||||
/// - 跨池移动:queue/status 一致性联动(对齐 commands::task::move_task_queue 语义)。
|
||||
/// - content 更新:走通用 update_field(content_json 已在 tasks 白名单) + JSON 合法性校验。
|
||||
///
|
||||
/// 风险等级对标设计 §五:link 写=Medium / 只读=Low / queue 移动=Medium / content 更新=Medium。
|
||||
fn register_task_graph_tools(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 同源)。
|
||||
registry.register(
|
||||
"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",
|
||||
df_ai::ai_tools::object_schema(vec![
|
||||
("task_id", "string", true), ("target_id", "string", true),
|
||||
("link_type", "string", true), ("remark", "string", false),
|
||||
]),
|
||||
RiskLevel::Medium,
|
||||
{ let db = db.clone(); Box::new(move |args: serde_json::Value| {
|
||||
let db = db.clone();
|
||||
Box::pin(async move {
|
||||
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 不存在)。
|
||||
registry.register(
|
||||
"remove_task_link", "解除任务关联(按 link id)。返回 deleted 布尔(是否命中,link 不存在则 false)",
|
||||
df_ai::ai_tools::object_schema(vec![("id", "string", true)]),
|
||||
RiskLevel::Medium,
|
||||
{ let db = db.clone(); Box::new(move |args: serde_json::Value| {
|
||||
let db = db.clone();
|
||||
Box::pin(async move {
|
||||
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 编排调度用:取某任务的全部依赖/阻塞/关联,做拓扑排序。
|
||||
registry.register(
|
||||
"list_task_links", "查询任务关联(AI 编排调度用)。参数:task_id(目标任务)、direction(查询方向 outgoing=作为 source 查我依赖谁 / incoming=作为 target 查谁依赖我 / both=双向合并去重,默认 both)。返回关联记录列表(按 created_at 升序)",
|
||||
df_ai::ai_tools::object_schema(vec![("task_id", "string", true), ("direction", "string", false)]),
|
||||
RiskLevel::Low,
|
||||
{ let db = db.clone(); Box::new(move |args: serde_json::Value| {
|
||||
let db = db.clone();
|
||||
Box::pin(async move {
|
||||
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 用途:查看需求分解结构、子任务进度聚合。
|
||||
registry.register(
|
||||
"get_task_tree", "获取任务父子树(限 1 级嵌套,设计 D2 无孙任务)。参数 task_id(任务 ID,无论叶子还是父任务)。返回 { parent: 父任务记录, children: 直接子任务列表(parent_id 指向 task_id,按 created_at 升序) }。叶子任务 children 为空。AI 用途:查看需求分解结构、子任务进度聚合",
|
||||
df_ai::ai_tools::object_schema(vec![("task_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 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 是合法非状态机路径)。
|
||||
registry.register(
|
||||
"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",
|
||||
df_ai::ai_tools::object_schema(vec![("id", "string", true), ("new_queue", "string", true)]),
|
||||
RiskLevel::Medium,
|
||||
{ let db = db.clone(); Box::new(move |args: serde_json::Value| {
|
||||
let db = db.clone();
|
||||
Box::pin(async move {
|
||||
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.clone()
|
||||
} else {
|
||||
"in_progress".to_string()
|
||||
}
|
||||
}
|
||||
"todo" => "todo".to_string(),
|
||||
"decision" => current.status.clone(),
|
||||
_ => 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 != 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 自检。
|
||||
registry.register(
|
||||
"update_content", "更新任务的结构化需求规格 content_json(JSON 字符串 {background,acceptance_criteria[],scope[],technical_design,custom_fields})。参数 id(任务 ID)、content_json(合法 JSON 字符串,空串清空 content_json)。校验:须是合法 JSON,否则拒。AI 用途:从对话提取信息填充需求规格,供后续 acceptance_criteria 自检",
|
||||
df_ai::ai_tools::object_schema(vec![("id", "string", true), ("content_json", "string", true)]),
|
||||
RiskLevel::Medium,
|
||||
{ let db = db.clone(); Box::new(move |args: serde_json::Value| {
|
||||
let db = db.clone();
|
||||
Box::pin(async move {
|
||||
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 }))
|
||||
})
|
||||
})},
|
||||
);
|
||||
}
|
||||
|
||||
/// 抽自 register_data_tools(SMELL-P0-2 续拆),【原样移入】,零行为变更。
|
||||
fn register_workflow_tools(registry: &mut AiToolRegistry) {
|
||||
// F-260616-07 阶段3(子项2):run_workflow AI 工具注册实装。
|
||||
@@ -2494,11 +2772,11 @@ mod tests {
|
||||
// 工具注册基线测试(SMELL-P0-2 拆分防护)
|
||||
//
|
||||
// 防未来 register_data_tools / register_file_tools 拆分或重构时静默丢工具。
|
||||
// build_ai_tool_registry 经两层 register_* 组装:data(18 持 db) + file(10 不持 db) = 28。
|
||||
// build_ai_tool_registry 经两层 register_* 组装:data(24 持 db) + file(13 不持 db) + http(1) = 38。
|
||||
// 任一层漏移 register 调用,此测试立即红。工具名集合也断言,防 rename 致 LLM tool 突变。
|
||||
// ============================================================
|
||||
|
||||
/// build_ai_tool_registry 应注册恰好 32 个工具(18 data + 13 file + 1 http),且工具名集合稳定。
|
||||
/// build_ai_tool_registry 应注册恰好 38 个工具(24 data + 13 file + 1 http),且工具名集合稳定。
|
||||
///
|
||||
/// 用 in-memory SQLite(Database::open_in_memory 自跑迁移),构造零外部依赖的 db,
|
||||
// 不实际执行任何 handler——仅断言注册阶段的定义完整性,故无需真实数据。
|
||||
@@ -2511,28 +2789,33 @@ mod tests {
|
||||
let allowed_dirs = Arc::new(RwLock::new(AllowedDirs::default_with_root()));
|
||||
let registry = build_ai_tool_registry(&db, &allowed_dirs);
|
||||
|
||||
// 总量基线:32(18 data + 13 file + 1 http)。拆分前后必须一致。
|
||||
// 总量基线:38(24 data + 13 file + 1 http)。拆分前后必须一致。
|
||||
// F-260621: file 层 10→11(新增 grep 跨文件内容搜索工具)。
|
||||
// L1 环境感知(设计 §2.1): file 层 11→12(新增 detect_environment 环境探测工具)。
|
||||
// AST 代码智能(7c2e3b2): file 层 12→13(新增 read_symbol 符号解析,治 read_file 全文回灌 prompt 爆)。
|
||||
// 知识图谱 Phase 1(2026-06-26): data 层 18→24(新增 register_task_graph_tools 6 工具:
|
||||
// create_task_link/remove_task_link/list_task_links/get_task_tree/move_task_queue/update_content)。
|
||||
assert_eq!(
|
||||
registry.len(),
|
||||
32,
|
||||
"工具总数应为 32(18 data + 13 file + 1 http),实际 {}", registry.len()
|
||||
38,
|
||||
"工具总数应为 38(24 data + 13 file + 1 http),实际 {}", registry.len()
|
||||
);
|
||||
|
||||
// 工具名集合基线:防 rename / 漏注册 / 误删除。
|
||||
// data 层 18 个(持 db):CRUD/状态机/工作流
|
||||
// data 层 24 个(持 db):CRUD/状态机/工作流/知识图谱任务关联
|
||||
// file 层 13 个(不持 db):命令/读/列/写/改/元/追加/删/移/搜/grep/环境探测/符号解析
|
||||
// http 层 1 个(不持 db):http_request
|
||||
let mut expected: Vec<&str> = vec![
|
||||
// ── data 层 (18) ──
|
||||
// ── data 层 (24) ──
|
||||
"list_projects", "list_tasks", "list_ideas",
|
||||
"update_project", "create_project", "bind_directory",
|
||||
"create_task", "update_task", "advance_task",
|
||||
"run_workflow", "delete_task", "create_idea",
|
||||
"delete_project", "restore_project", "purge_project",
|
||||
"list_trash", "get_project_count", "get_task_count",
|
||||
// 知识图谱 Phase 1 任务关联工具(register_task_graph_tools 6 个)
|
||||
"create_task_link", "remove_task_link", "list_task_links",
|
||||
"get_task_tree", "move_task_queue", "update_content",
|
||||
// ── file 层 (13) ──(run_command 注册顺序已移至末位降低 LLM 偏好,
|
||||
// 集合断言经 sort 后与顺序无关,仅守护工具名不漂移。grep 新增 F-260621;
|
||||
// detect_environment 新增 L1 环境感知 设计 §2.1;read_symbol 新增 AST 代码智能)
|
||||
|
||||
Reference in New Issue
Block a user