# AiNode 自审实施方案(②-⑥) > 决策基线:**决策 a 已定** —— `TaskRecord` 加 `output_json` 字段(task 中心,产出跟 task 走)。① df-storage 迁移(struct 字段 + migrations CREATE/ALTER + crud 白名单 + INSERT/UPDATE/SELECT 列)由同批另一 agent 落地,**本方案为前置依赖**。 > > 范围:**仅设计文档(read-only),不改任何 .rs/.ts/.vue 代码**。file:line 证据均独立 grep/Read 核验,未采信文档/会话描述声明。 > > 日期:2026-06-16 | 作者:AiNode 自审调研 agent --- ## 一、现状盘点(file:line 证据) ### 1.1 已就绪(自审闭环地基) | 能力 | 位置 | 状态 | |------|------|------| | `NodeContext` 结构 | `crates/df-workflow/src/node.rs:12-26` | 字段:`node_id / inputs / config / execution_id / event_bus / node_status`,**无 task 数据、无 db 句柄** | | AiNode 通用执行 | `crates/df-nodes/src/ai_node.rs:116-164` | `AiNode.execute` 调 LLM → 返 `NodeOutput{text, model, usage}`(仅内存,不落 task) | | AiNode 参数解析 | `crates/df-nodes/src/ai_node.rs:35-109` | `parse_params(config, inputs)`:prompt 优先上游 inputs["prompt"] > config.prompt;provider 配置从 config 取 | | HumanNode 审批 | `crates/df-nodes/src/human_node.rs:39-176` | 阻塞节点,发 `HumanApprovalRequest` 等响应;reject 关键字(L19-22)→ Err → 工作流 failed | | testing 模板(拓扑) | `crates/df-nodes/src/task_workflow_templates.rs:57-72` | `ai_self_review` → `human_review` 串行;节点级 config 仅含 human title/options,**ai_self_review config 为空 `{}`** | | 模板选择 | `crates/df-nodes/src/task_workflow_templates.rs:28-35` | `template_for(target_status)` 按 in_progress/testing/done 选模板 | | DagDef 选模板 | `src-tauri/src/commands/workflow.rs:84-92` | 空 dag + target_status → `template_for` 自动选 | | **deep_merge(④-1)** | `crates/df-workflow/src/executor.rs:102-108` + `crates/df-workflow/src/dag.rs:169-188` | executor 构建 NodeContext 时 `deep_merge(initial_config, node_config)`,节点级覆盖全局级;**模板节点 config 为空 → 全局 config 原样穿透** | | 工作流联动任务 | `src-tauri/src/commands/workflow.rs:235-308` | `task_id`+`target_status` 都 Some 时,完成推进/失败退回(`regression_target` L46-54) | | **TaskAdvanceNode db 注入先例** | `crates/df-nodes/src/task_advance_node.rs:98-110` + `src-tauri/src/state.rs:265-266` | 节点 struct 持 `db: Arc`,`NodeRegistry::register` 工厂闭包 move 捕获 `db.clone()` 注入 —— **AiNode 可复用同一模式** | | run_workflow 全局 config | `src-tauri/src/commands/workflow.rs:67-76` | 签名含 `config: serde_json::Value`,闭包内 `executor.run(&runtime_dag, config)` 传为 initial_config | ### 1.2 待 ①迁移落地(前置依赖) | 缺口 | 位置 | 说明 | |------|------|------| | struct 字段 | `crates/df-storage/src/models.rs:69-76` | ✅ 已有 `output_json: Option`(决策 a 已落地到模型层) | | migrations CREATE | `crates/df-storage/src/migrations.rs:371` | ✅ CREATE TABLE tasks 含 `output_json TEXT`(新库) | | migrations ALTER(老库) | `crates/df-storage/src/migrations.rs:394-395` 附近 | ⚠️ 未见 `ALTER TABLE tasks ADD COLUMN output_json`(**老库升级缺列**,需 ①迁移补) | | crud SELECT | `crates/df-storage/src/crud.rs:779` | ⚠️ `list_active` SELECT 列未含 output_json | | crud INSERT/UPDATE | `crates/df-storage/src/crud.rs:748-762` | ⚠️ INSERT/UPDATE_FULL 语句未含 output_json 列 | | crud 行映射 | `crates/df-storage/src/crud.rs:544` | ⚠️ `output_json: row.get("output_json")` 存在,但 SELECT 缺列 → 运行时 row.get 会报错(待 ①补 SELECT) | | crud 白名单 | `crates/df-storage/src/crud.rs:342-343` | ⚠️ `allowed_columns_for("tasks")` **未含 output_json**(AiNode 写产出走通用 `update_field` 必需) | **结论**:①迁移尚未完成(struct 已加字段,crud 链路与白名单未跟上)。本方案所有 ②-⑥ 步骤均**强依赖 ①完成**,尤其白名单与 SELECT 列。 --- ## 二、五项实施方案 ### 2.1 方案①:NodeContext 注入 task 机制 **问题**:`ai_execute`/`ai_self_review` 需拿到 `task_id` + task 数据(description / output_json)+ provider 配置;`ai_execute` 还需 db 句柄写产出。当前 `NodeContext`(node.rs:12-26)无此二者。 **三选项对比**: | 选项 | 机制 | 优点 | 缺点 | |------|------|------|------| | **A. config 注入 task_id + 节点持 db 句柄**(推荐) | run_workflow 全局 config 注入 `task_id`+`provider{base_url,api_key,model,protocol}`;AiNode struct 加 `db: Arc`,state.rs 工厂闭包 move 注入(**复用 TaskAdvanceNode 先例**) | 零改 NodeContext(不动 node.rs);db 注入已有先例(state.rs:265);config 经 deep_merge 穿透;AiNode 与 TaskAdvanceNode 注入方式一致 | AiNode 从无状态单例变有状态(state.rs:257-259 工厂闭包需改 move db.clone) | | B. NodeContext 加 task 字段 | 改 node.rs 加 `task: Option` + db 句柄字段 | 节点直接读,省一次 DB 查 | 改 NodeContext 影响所有 Node(HumanNode/SleepNode/test 构造全改,human_node.rs:236 make_ctx 等测试全改);executor.rs:99-113 构造点要读 DB;df-workflow 反向依赖 df-storage/df-task(循环依赖风险) | | C. IPC handler | AiNode 通过 event_bus 发事件,前端/IPC 侧写 task | 解耦 | AiNode 在 DAG 后台线程,无 AppHandle/State;引入跨线程往返;HumanNode 那套 broadcast 模式不适合写 DB;过度复杂 | **推荐:选项 A**。理由: 1. **零侵入 NodeContext** —— 不动 node.rs,所有现有 Node 与测试(human_node.rs:236 make_ctx、executor.rs:99-113)零改动。 2. **先例已验证** —— TaskAdvanceNode(task_advance_node.rs:98-110)就是这套:struct 持 `db: Arc`,state.rs:265-266 工厂闭包 `move |_config| { Box::new(...::new(db.clone())) }` 注入。AiNode 照搬。 3. **config 穿透已就绪** —— run_workflow 全局 config(workflow.rs:72)→ executor deep_merge(executor.rs:102-108)→ 模板节点 config 空 `{}` → 全局 config 原样进 `NodeContext.config`。前端调 `workflowApi.run(name, {}, config, taskId, target)`(workflow.ts:12-26)即可把 task_id/provider 推进去。 4. **DB 句柄天然可得** —— state.rs:205 已有 `db: Arc`,build_registry 入参就是它(state.rs:252)。 **注入路径**: ``` 前端 TaskDetail.handleWorkflowAdvance → workflowApi.run(name, {}, {task_id, provider:{base_url,api_key,model,protocol}}, taskId, "testing") (workflow.ts:12-26) → run_workflow(task_id, target_status, config) (workflow.rs:67) → executor.run(dag, config) // config 作 initial_config (workflow.rs:245) → NodeContext.config = deep_merge(initial_config, node_config={}) (executor.rs:102-108) → AiNode.execute(ctx): ctx.config["task_id"] / ctx.config["provider"] 可读 → AiNode 持 self.db → TaskRepo::new(&self.db).get_by_id(task_id) 读 task ``` **provider 配置来源**:前端从 `ai_list_providers`(api/ai.ts:76)取默认 provider(types.ts:172-178 AiProviderRecord),拼成 `{base_url, api_key, model, protocol}` 注入 config。**api_key 安全**:复用 `src-tauri/src/commands/ai/secret.rs ensure_resolved_key`(ai_node.rs:50 注释已对齐此行为),前端不直接拿明文,由后端在注入 config 前解析(或前端传 provider_id,后端 AiNode 内部解析——更安全,见风险)。 ### 2.2 方案②:ai_execute 写产出 **前置**:方案①A(AiNode 持 db + config 有 task_id)。 **写产出路径**(ai_node.rs:155-163 当前返 NodeOutput 后追加落库): ``` AiNode.execute(ctx): 1. parse_params(&ctx.config, &ctx.inputs) → p (现有,ai_node.rs:119) 2. provider.complete(request) → response (现有,ai_node.rs:146) 3. 【新增】若 ctx.config 有 task_id: let task_id = ctx.config["task_id"].as_str() let repo = TaskRepo::new(&self.db) // 产出 schema: {text, model, usage} 直接序列化(与 NodeOutput.data 一致) let output_json = serde_json::to_string(&json!({text, model, usage}))? repo.update_field(task_id, "output_json", &output_json).await? // 需 ①白名单含 output_json 4. return NodeOutput::from_value(...) (现有,ai_node.rs:155) ``` **关键约束**: - `update_field`(crud.rs:165)走白名单校验 —— **必须 ①把 output_json 加进 tasks 白名单**(crud.rs:342-343)。 - 产出 schema 与 `NodeOutput.data` 一致(ai_node.rs:155-163),下游 `ai_self_review` 既可从 `inputs["ai_execute"].data` 读(DAG 层),也可从 task.output_json 读(跨 DAG/重启)。**推荐读 inputs(DAG 内闭环),task.output_json 仅作持久化与 human_review 展示源**。 **in_progress 模板适用**:in_progress_template(task_workflow_templates.rs:40-50)单 ai_execute 节点,正是写产出处。testing 模板的 ai_self_review 读上游/读 task.output_json。 ### 2.3 方案③:ai_self_review prompt 模板与自审结果 **输入**:`ai_self_review` 节点读上游 `inputs["ai_execute"]`(DAG 层 testing 模板无 ai_execute,故实际读 **task.output_json**)+ task.description(需求)。 **prompt 模板草案**: ``` 【系统提示 system_prompt】 你是严格的代码/产出审查员。审查任务产出是否符合需求,按四维度给出结构化结论。 只输出 JSON,不要任何额外文字。 【用户提示 prompt】 ## 任务需求 {task.description} ## 待审产出 {task.output_json 解析后的 text 字段} ## 审查维度 1. 需求符合度:产出是否覆盖需求描述的所有要点 2. 产出完整性:是否有遗漏、未完成的部分 3. 正确性:逻辑/事实/语法是否正确 4. 边界处理:异常输入、空值、错误路径是否考虑 ## 输出格式(严格 JSON) { "verdict": "pass" | "fail", "dimensions": { "requirement_fit": {"score": 0-10, "issues": ["..."]}, "completeness": {"score": 0-10, "issues": ["..."]}, "correctness": {"score": 0-10, "issues": ["..."]}, "boundary": {"score": 0-10, "issues": ["..."]} }, "summary": "一句话总结", "suggestions": ["改进建议1", "改进建议2"] } verdict=fail 当且仅当任一维度 score < 6 或有阻断性 issue。 ``` **自审结果写回 output_json**:**不覆盖**,加 `review` 子字段(保留 ai_execute 原始产出供 human_review 对照): ```json { "text": "", "model": "...", "usage": {...}, "review": { "verdict": "pass" | "fail", "dimensions": {...}, "summary": "...", "suggestions": [...], "reviewed_at": "", "reviewer_model": "<自审用的 model>" } } ``` **verdict=fail 的处理**:当前 testing 模板(task_workflow_templates.rs:57-72)ai_self_review → human_review 串行,ai_self_review 返 Ok(不 Err)。**设计选择**:ai_self_review **不据 verdict 主动 Err**(自审是辅助,最终决策权在人)。verdict=fail 时: - 写回 output_json(带 review.fail 信号) - 节点返 Ok(让 human_review 节点继续,人看到 fail 结论再定) - human_review 可在审批卡片高亮显示 verdict=fail(前端红标),人选拒绝 → Err → 工作流 failed → regression_target(workflow.rs:46-54 testing→in_review)退回 **替代方案(更激进,不推荐首版)**:ai_self_review verdict=fail 直接返 Err → 工作流 failed → 自动退回 in_review,跳过人工。**风险**:LLM 自审误判直接退回,人无干预机会。首版保守,verdict 仅作展示信号。 **实现要点**: - prompt 模板放节点 config 还是硬编码?—— **放 config(run_workflow 全局 config 注入 `review_prompt_template`),模板可热替换**。但首版可硬编码在 AiNode 逻辑里(按 node_id=="ai_self_review" 分支),简化首版。 - LLM 输出解析:`serde_json::from_str(&response.text)` 解 JSON,失败兜底 `verdict=unknown, summary=原文`(防 LLM 不按要求输出)。 ### 2.4 方案④:human_review 展示 **human_review 节点**(human_node.rs:39)当前从 `ctx.config` 读 title/description/options(human_node.rs:42-56),发 `HumanApprovalRequest`(human_node.rs:74-83)。**问题**:HumanApprovalRequest 事件载荷(df-core events)只有 title/description/options,**不含 task 产出/自审结果**。 **展示方案**:human_review 节点在发 HumanApprovalRequest 前,读 task.output_json,把"产出摘要 + 自审结论"拼进 `description` 字段(最小改动,复用现有事件结构): ``` human_review.execute(ctx): 1. 读 task.output_json(经方案①AiNode 持 db + ctx.config["task_id"]) → 解析 review.verdict / review.summary / review.suggestions → 解析 text(ai_execute 产出) 2. 拼接 description: description = format!(" ## AI 自审结论:{verdict} {summary} 建议:{suggestions} ## 产出 {text_前 N 字} ") 3. ctx.config["description"] = description (覆盖模板默认空串,human_node.rs:46-48) 4. 发 HumanApprovalRequest(现有流程,human_node.rs:74-83) ``` **约束**:HumanNode 当前**不持 db**(human_node.rs:36 `pub struct HumanNode;` 无字段)。需同样按方案①A 给 HumanNode 加 `db: Arc` + state.rs:254-256 工厂闭包改 move db。**或**:human_review 的 description 由 ai_self_review 节点算好,经 DAG outputs 透传(inputs["ai_self_review"].data)—— **更优,HumanNode 零改动**: - ai_self_review 节点把"自审结论 + 产出摘要"塞进 NodeOutput.data - human_review 读 `inputs["ai_self_review"].data["review_summary"]` 拼进 description - HumanNode 不需 db,零改动 **推荐后者**(HumanNode 零改动,db 注入只给 AiNode)。 **审批卡片显示什么**(前端): - title:模板已定「核对 AI 自审结果」(task_workflow_templates.rs:66) - description:自审 verdict(pass/fail 红绿标)+ summary + suggestions + 产出摘要 - options:同意 / 拒绝(task_workflow_templates.rs:67,含拒绝触发退回) ### 2.5 方案⑤:前端展示 output_json **当前 TaskDetail.vue**(src/views/TaskDetail.vue:1-140):展示 title/status/description/priority/branch 等,**无 output_json 展示**。已有工作流推进按钮(L71-91)+ workflow-event 监听(L81-90 轻量进度)+ df-data-changed 自动刷新(L82 注释)。 **UI 方案**(在 TaskDetail.vue 信息区加一个区块,L124 workflowDef 后): ```vue
{{ $t('taskDetail.output') }}
{{ reviewVerdictLabel }} {{ parsedOutput.review.summary }}
  • {{ s }}
``` **实现要点**: - `parsedOutput = computed(() => JSON.parse(task.output_json))`(包 try/catch,解析失败降级显示原文) - `reviewVerdictClass`:pass→绿,fail→红 - `renderedOutput`:复用 `useRendered`(TaskDetail.vue:150, 190-192 已用于 description)渲染 markdown - i18n key:`taskDetail.output` / `taskDetail.review.pass` / `taskDetail.review.fail` - workflow 完成后 task 经 df-data-changed 自动刷新(现有机制,TaskDetail.vue:82 注释),output_json 自动出现 **约束**:output_json 是 `Option`(models.rs:76),前端 `task.output_json` 可能为 undefined(旧任务无产出)—— `v-if="task.output_json"` 守卫。 --- ## 三、②-⑥ 实施步骤拆解(标可并发) > 依赖链:①迁移(另一 agent)→ ②-⑥。②-⑥ 内部: ``` ② AiNode 持 db + 写产出 ──┐ ├─→ ⑤ 前端展示(依赖 output_json schema 定型,即③review schema) ③ ai_self_review prompt ──┤ ├─→ ④ human_review 展示(依赖③ review schema + ② 产出 schema) ┘ ⑥ 端到端联调(依赖②③④⑤) ``` ### 步骤②:ai_execute 写产出(依赖①白名单) - [ ] `crates/df-nodes/src/ai_node.rs`:`AiNode` struct 加 `db: Arc` 字段 + `new(db)` 构造 - [ ] `crates/df-nodes/src/ai_node.rs:execute`:LLM 完成后,若 `ctx.config["task_id"]` 存在 → `TaskRepo::new(&self.db).update_field(task_id, "output_json", &json)` 落库 - [ ] `src-tauri/src/state.rs:257-259`:ai 工厂闭包改 `move |_config| Box::new(AiNode::new(db.clone()))`(对齐 state.rs:265 TaskAdvanceNode 写法) - [ ] 单测:mock db 验证 update_field 调用 + output_json 内容 ### 步骤③:ai_self_review prompt 模板(依赖② schema) - [ ] `crates/df-nodes/src/ai_node.rs`:按 node_id 分支(或新增 `AiReviewNode` 独立节点类型)—— **推荐独立节点** `ai_self_review` 类型,复用 AiNode 大部分逻辑但 prompt 模板固定、输出解析 JSON - [ ] prompt 模板(见 2.3)硬编码或 config 注入 - [ ] LLM 输出 JSON 解析 + 兜底 - [ ] 自审结果写回 task.output_json(加 review 子字段,不覆盖 text) - [ ] 注册 `ai_self_review` 节点类型到 state.rs build_registry - [ ] testing 模板节点类型 "ai" → "ai_self_review"(task_workflow_templates.rs:59) ### 步骤④:human_review 展示(依赖③ review schema) - [ ] `crates/df-nodes/src/ai_node.rs`(ai_self_review 节点):把 review 摘要塞进 NodeOutput.data,供下游 human_review 读 - [ ] **不改 HumanNode**(human_node.rs 零改动)—— human_review 节点 config 的 description 在模板层或 ai_self_review 输出层拼接 - [ ] 方案:testing 模板给 human_review 节点 config 加 description 占位,或新增轻量"描述拼装"逻辑(读 inputs["ai_self_review"]) - [ ] **若选 HumanNode 持 db 方案**:human_node.rs 加 db 字段 + state.rs:254 工厂闭包改 move(不推荐,多改一处) ### 步骤⑤:前端展示(依赖③ schema 定型)—— **可与②③④并发** - [ ] `src/views/TaskDetail.vue`:加 output_json 展示区块(见 2.5) - [ ] `src/api/types.ts`:TaskRecord 类型加 `output_json?: string` - [ ] `src/locales/*.json`:加 taskDetail.output / review.pass / review.fail i18n key - [ ] review.verdict 红绿标样式 ### 步骤⑥:端到端联调(依赖②③④⑤全完成) - [ ] 前端 TaskDetail 点工作流推进(testing)→ run_workflow → ai_self_review 调 LLM → 写 output_json → human_review 审批卡显示自审结论 → 人同意/拒绝 → 任务态推进/退回 - [ ] 验证 regression_target(workflow.rs:46-54):拒绝 → failed → testing 退回 in_review **可并发标注**: - **②③④ 串行**(共享 ai_node.rs / schema) - **⑤ 可与②③④ 并发**(前端独立,仅依赖 schema 文档约定) - **⑥ 必须最后**(全依赖) --- ## 四、风险 | 风险 | 等级 | 说明 | 缓解 | |------|------|------|------| | ①迁移未完成阻塞 ②-⑥ | **P0 阻断** | crud 白名单(crud.rs:342-343)/ SELECT 列(crud.rs:779)/ INSERT-UPDATE 语句(crud.rs:748-762)/ ALTER(migrations.rs)均未含 output_json | 本方案明确标注前置依赖;①agent 须先完成 crud 链路全量(白名单+SELECT+INSERT+UPDATE+ALTER) | | api_key 明文经 config 注入 | **P0 安全** | 前端拼 provider config 含 api_key,经 IPC(明文)+ config(明文)传到 AiNode,前端可拿到明文 key(违背 FR-S1 mask 设计) | 改传 `provider_id`(非 api_key),AiNode 内部调 `secret::ensure_resolved_key`(src-tauri/src/commands/ai/secret.rs)解析;前端 config 只放 provider_id + model + protocol,base_url/api_key 后端解析注入 | | LLM 自审输出不合规 JSON | P1 | LLM 可能不按要求输出纯 JSON | 兜底解析:JSON.parse 失败 → verdict=unknown + summary=原文;prompt 强约束 + temperature=0 | | AiNode 从无状态变有状态 | P2 | state.rs:257-259 当前 `Box::new(AiNode)` 无参,改 `new(db.clone())` 后工厂闭包要 move db(对齐 state.rs:265 已有写法) | 低风险,TaskAdvanceNode 同模式已验证;注意所有 AiNode 测试构造点同步 | | HumanNode 是否持 db 的方案分歧 | P2 | 方案④有两个选项(HumanNode 持 db vs ai_self_review 输出透传) | 推荐后者(HumanNode 零改动),review 摘要经 DAG inputs 透传;减少改动面 | | ai_self_review verdict=fail 是否自动退回 | P2 | 激进方案(fail→Err→自动退回)vs 保守方案(fail→展示,人定) | 首版保守,verdict 仅作展示信号;后续可加 config 开关 `auto_reject_on_fail` | | testing 模板 ai 节点类型改 ai_self_review | P2 | task_workflow_templates.rs:59 当前 `dag.add_node("ai_self_review", "ai", ...)` 类型是 "ai";若新增独立 ai_self_review 节点类型,模板要改 + registry 要注册 | 若复用 AiNode(按 node_id 分支 prompt)则模板不改;若独立节点则模板 + registry 都改(见步骤③) | | output_json schema 演进 | P3 | 未来产出类型多样化(代码/文档/分析),单一 text+review schema 可能不够 | schema 设计预留扩展字段;output_json 本就是自由 JSON 字符串,可演进 | --- ## 五、依赖 - **① df-storage 迁移**(另一 agent,本方案前置):TaskRecord struct(✅ models.rs:76 已加)→ migrations(CREATE ✅ / ALTER ⚠️ 缺)→ crud(SELECT ⚠️ / INSERT ⚠️ / UPDATE ⚠️ / 白名单 ⚠️ 全缺)。**须全量完成 crud 链路 + 白名单含 output_json**。 - **provider 配置注入链**:前端 ai_list_providers(api/ai.ts:76)+ 默认 provider(types.ts:172-178)→ run_workflow config → deep_merge。已就绪,仅需前端拼装逻辑。 - **secret 解析**(若走 provider_id 方案):src-tauri/src/commands/ai/secret.rs ensure_resolved_key 已就绪(ai_node.rs:50 注释对齐)。 --- ## 六、附录:file:line 证据索引 | 文件 | 行 | 内容 | |------|----|------| | crates/df-workflow/src/node.rs | 12-26 | NodeContext 结构(无 task/db) | | crates/df-nodes/src/ai_node.rs | 35-109 | parse_params 参数解析 | | crates/df-nodes/src/ai_node.rs | 112 | `pub struct AiNode;`(无状态) | | crates/df-nodes/src/ai_node.rs | 116-164 | AiNode.execute 调 LLM 返 NodeOutput | | crates/df-nodes/src/ai_node.rs | 155-163 | 产出 schema {text,model,usage} | | crates/df-nodes/src/human_node.rs | 36 | `pub struct HumanNode;`(无状态) | | crates/df-nodes/src/human_node.rs | 39-176 | HumanNode.execute 审批流程 | | crates/df-nodes/src/human_node.rs | 19-22 | REJECT_KEYWORDS | | crates/df-nodes/src/task_workflow_templates.rs | 28-35 | template_for 选模板 | | crates/df-nodes/src/task_workflow_templates.rs | 57-72 | testing 模板(ai_self_review→human_review) | | crates/df-nodes/src/task_advance_node.rs | 98-110 | TaskAdvanceNode db 注入先例 | | crates/df-workflow/src/executor.rs | 99-113 | NodeContext 构造 | | crates/df-workflow/src/executor.rs | 102-108 | deep_merge 节点级覆盖全局 | | crates/df-workflow/src/dag.rs | 169-188 | deep_merge 实现 | | src-tauri/src/commands/workflow.rs | 67-76 | run_workflow 签名(config/task_id/target_status) | | src-tauri/src/commands/workflow.rs | 84-92 | 空 dag + target → template_for | | src-tauri/src/commands/workflow.rs | 235-308 | 工作流联动任务推进/退回 | | src-tauri/src/commands/workflow.rs | 46-54 | regression_target 失败退回映射 | | src-tauri/src/state.rs | 252-266 | build_registry 节点注册(AiNode L257 / TaskAdvanceNode L265) | | crates/df-storage/src/models.rs | 53-79 | TaskRecord(含 output_json L76) | | crates/df-storage/src/migrations.rs | 371 | CREATE TABLE output_json TEXT | | crates/df-storage/src/crud.rs | 165 | update_field 通用写 | | crates/df-storage/src/crud.rs | 342-343 | tasks 白名单(缺 output_json) | | crates/df-storage/src/crud.rs | 748-762 | TaskRepo INSERT/UPDATE(缺 output_json) | | crates/df-storage/src/crud.rs | 779 | list_active SELECT(缺 output_json) | | src/api/workflow.ts | 12-26 | workflowApi.run(含 config/taskId/targetStatus) | | src/api/ai.ts | 76 | ai_list_providers | | src/api/types.ts | 172-178 | AiProviderRecord(api_key/base_url) | | src/views/TaskDetail.vue | 71-91 | 工作流推进按钮 + 轻量进度 | | src/views/TaskDetail.vue | 124-127 | workflowDef 区块(output_json 展示插入点) | | src-tauri/src/commands/ai/secret.rs | - | ensure_resolved_key(api_key 安全解析) |