新增: 推进链阶段3收口(advance_task/run_workflow工具+status状态机收口)

This commit is contained in:
2026-06-16 21:20:41 +08:00
parent dfe0096498
commit b94e74a96d
6 changed files with 126 additions and 15 deletions

View File

@@ -359,7 +359,7 @@ pub fn build_ai_tool_registry(db: &Arc<Database>) -> AiToolRegistry {
})},
);
registry.register(
"update_task", "更新任务的指定字段title/description/status/priority/assignee 等),需要提供任务 ID、字段名和新值",
"update_task", "更新任务的指定字段title/description/priority/assignee 等),需要提供任务 ID、字段名和新值。注意status 改动须走 advance_task 工具状态机推进7 态 todo/in_progress/in_review/testing/blocked/done/cancelled按 target_status 推进不可跳跃),本工具不接受 status 字段(防绕过状态机)",
df_ai::ai_tools::object_schema(vec![("id", "string", true), ("field", "string", true), ("value", "string", true)]),
RiskLevel::Medium,
{ let db = db.clone(); Box::new(move |args: serde_json::Value| {
@@ -368,6 +368,14 @@ pub fn build_ai_tool_registry(db: &Arc<Database>) -> AiToolRegistry {
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"))?;
// F-03 收口(子项3):status 改动必须走 advance_task 工具(状态机推进),
// 本工具显式拒绝 status 字段,防 AI 绕过状态机直接改库。
// 对齐 Agent B df-storage tasks 白名单移 status——双重防御(schema 拒绝 + 白名单拒绝)。
if field == "status" {
anyhow::bail!(
"status 改动须走 advance_task 工具(状态机推进,7 态不可跳跃),本工具不接受 status 字段"
);
}
// 复用 df-storage CRUD 白名单(按表隔离),与 update_field 校验同源
if !df_storage::crud::is_allowed_column("tasks", field) {
anyhow::bail!("不允许更新字段 '{}'", field);
@@ -378,6 +386,65 @@ pub fn build_ai_tool_registry(db: &Arc<Database>) -> AiToolRegistry {
})
})},
);
// F-03 收口(子项1):advance_task AI 工具 — 推进任务状态的唯一合法路径。
// D-260616-03:推进链业务逻辑(状态机校验 + 原子 CAS + review_rounds 累加)落
// df-nodes::task_advance_node::advance_task_atomic,本 handler 直接调它(与 IPC
// commands::task::advance_task 同源,thin 转发),无需 AppHandle/State/EventBus。
// 风险 Medium:status 变更属状态机推进(非破坏),但改变任务流转语义需人工知情。
registry.register(
"advance_task", "推进任务状态,必须走状态机(7 态: todo/in_progress/in_review/testing/blocked/done/cancelled),按 target_status 推进,不可跳跃(非法转换会被状态机拦截)。退回转换(in_review→in_progress 等)自动累加 review_rounds。返回推进后的最新 TaskRecord",
df_ai::ai_tools::object_schema(vec![("id", "string", true), ("target_status", "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 target_status = args["target_status"].as_str()
.ok_or_else(|| anyhow::anyhow!("缺少 target_status"))?;
// 复用 df-nodes 推进链核心逻辑(状态机 + 原子 CAS + review_rounds),
// 与 commands::task::advance_task IPC 同源,避免双轨。
let repo = df_storage::crud::TaskRepo::new(&db);
let updated = df_nodes::task_advance_node::advance_task_atomic(
&repo, id, target_status,
).await?;
// 返回推进后的 TaskRecord(含新 status / 累加后的 review_rounds),供 LLM 确认推进结果。
Ok(serde_json::to_value(&updated)?)
})
})},
);
// F-260616-07 阶段3(子项2):run_workflow AI 工具注册实装。
// 实施路径文档 §三 列为阶段3 必做项(tool_registry.rs 此前无此工具连空壳都没有)。
// 描述明确按任务 target_status 推进对应工作流(含 AiNode 自审 / HumanNode 核对闸门)。
//
// handler 约束说明:run_workflow 真正执行需要 AppHandle(转发 workflow-event 到前端) +
// AppState(registry 构建 DAG / event_bus 订阅 / workflows Repo 落库 / workflow_state_registry
// 注销)等 Tauri 注入态,这些在 tool handler(仅持有 db: Arc<Database>)中无法构造。
// 现阶段仅注册 ToolDefinition(schema + risk + 描述)让 LLM 知晓此能力并产出 tool_call;
// 真正触发须走 Tauri IPC(经 invoke_handler 注册的 run_workflow 命令,持有完整 State)。
// handler 显式报错引导走 IPC,避免在 handler 内重放 DAG 执行引擎(违反单一执行路径原则)。
// 后续若需 AI 直驱完整工作流,需扩展 build_ai_tool_registry 注入 AppState 句柄(改 state.rs,
// 留待推进链后续批次)。
registry.register(
"run_workflow", "按任务 target_status 推进对应工作流(含 AiNode 自审 / HumanNode 核对闸门)。参数 task_id + target_status 同时提供才联动任务推进(完成后按 target_status 推进任务,失败按退回态回滚)。属高风险操作(触发工作流引擎执行),须人工批准",
df_ai::ai_tools::object_schema(vec![("task_id", "string", true), ("target_status", "string", true)]),
RiskLevel::High,
{ 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_status = args["target_status"].as_str()
.ok_or_else(|| anyhow::anyhow!("缺少 target_status"))?;
// handler 无法访问 AppHandle/State(registry/event_bus/workflows Repo),
// 真正执行须走 run_workflow Tauri IPC(持有完整 State)。
// 这里返回明确错误引导前端走 IPC,而非在 handler 内重放 DAG 引擎。
Err(anyhow::anyhow!(
"run_workflow 工具需经 Tauri IPC 执行(持有 AppHandle/State),handler 无 State 句柄。\
请前端收到此 tool_call 后转调 invoke('run_workflow', {{ task_id: {}, target_status: {} }})。",
task_id, target_status
))
})
})},
);
// FR-D6:delete_task 对齐 commands::task::delete_task(D-260616-02 软删除)
// tasks 表 V14 加 deleted_at,delete_task 改调 soft_delete 移入回收站(对标 delete_project)
registry.register(
@@ -1189,6 +1256,9 @@ pub(crate) fn display_hint_for_tool(name: &str) -> Option<(&'static str, &'stati
"patch_file" => ("修改文件:{}", &["path"]),
"delete_file" => ("删除文件:{}{}", &["path", "permanent"]),
"rename_file" => ("重命名/移动:{} → {}", &["from", "to"]),
// F-03 收口:推进链工具审批文案(advance_task/run_workflow)
"advance_task" => ("推进任务状态:{} → {}", &["id", "target_status"]),
"run_workflow" => ("触发工作流:任务{} 推进到 {}", &["task_id", "target_status"]),
_ => return None,
};
Some((template, keys))
@@ -1213,6 +1283,8 @@ pub(crate) fn tool_display_hint(name: &str) -> Option<&'static str> {
"bind_directory" => Some("绑定目录"),
"create_idea" => Some("捕获灵感"),
"update_task" => Some("修改任务"),
"advance_task" => Some("推进任务状态"),
"run_workflow" => Some("触发工作流"),
"delete_task" => Some("删除任务"),
"update_idea" => Some("修改灵感"),
"delete_idea" => Some("删除灵感"),