# 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

---

> ## 实施状态(2026-06-18 核对)
>
> **②-⑤ 全部已落地**(原设计 1.2 表中标 ⚠️ 的 crud/白名单缺口已补全，②③④⑤ 已实施)。
>
> **① df-storage 迁移 — 已完成**(原 ⚠️ 项已补)：
> - `TaskRecord.output_json: Option<String>`：`crates/df-storage/src/models.rs:72`。
> - 白名单含 output_json：`crates/df-storage/src/crud.rs:344`(`allowed_columns_for("tasks")` 已含，注释"ai_execute 写产出 / ai_self_review 读产出自审 / human_review 展示对象")。
> - migrations：`crates/df-storage/src/migrations.rs:303`(注释同上)。
>
> **② AiNode 持 db + 写产出 — 已落地**：
> - `AiNode` struct：`crates/df-nodes/src/ai_node.rs:256`(持 db 字段，`AiNode::new(db)` 构造)。
> - state.rs 工厂闭包注入 db：`src-tauri/src/state.rs:351-352`(`registry.register("ai", ... AiNode::new(ai_db.clone()))`)。
> - execute 写产出：`crates/df-nodes/src/ai_node.rs:319-325`(若 config 含 task_id → `repo.update_field(task_id, "output_json", &json_str)`)。
>
> **③ AiSelfReviewNode 独立节点 — 已落地**(原设计 2.3 推荐独立节点路径已采纳，非按 node_id 分支)：
> - `AiSelfReviewNode` struct：`crates/df-nodes/src/ai_node.rs:438`。
> - `REVIEW_SYSTEM_PROMPT`(四维度 prompt 模板)：`crates/df-nodes/src/ai_node.rs:387`。
> - JSON 兜底解析 `parse_review_json`(fn 定义，非 JSON/缺 verdict → verdict=unknown)：`crates/df-nodes/src/ai_node.rs:396`，单测 `:1011-1058`(valid/invalid/fence 三场景)。
> - `build_review_prompt`(fn 定义)：`crates/df-nodes/src/ai_node.rs:449`，单测 `:1060`。
> - state.rs 注册：`src-tauri/src/state.rs:359-360`(`registry.register("ai_self_review", ... AiSelfReviewNode::new(review_db.clone()))`)。
>
> **④ human_review 展示(经 DAG inputs 透传，HumanNode 零改动)— 已落地**：
> - review 摘要塞 NodeOutput.data：`crates/df-nodes/src/ai_node.rs:586`(注释「NodeOutput.data 塞 review 摘要,供下游 human_review 经 inputs["ai_self_review"] 读」)。
> - testing 模板 edge ai_self_review → human_review：`crates/df-nodes/src/task_workflow_templates.rs:80`，单测 `:149` 验证边方向(`assert_eq!(edge.source, "ai_self_review")`)。
> - HumanNode 本身零改动(git 历史核验 human_node.rs 在 AiNode 自审批 commit 中无变动)。
>
> **⑤ 前端展示 — 已落地**：TaskDetail.vue 加 output_json 区块(本轮未逐行核验前端 file:line，但后端 output_json schema 已定型、前端按 schema 渲染)。
>
> **超出原设计、后追加的能力 — gate 闸门**：
> - testing 模板 `ai_self_review` 启用 `gate:true`：`crates/df-nodes/src/task_workflow_templates.rs:54-67`(阶段3 起 verdict=fail → AiSelfReviewNode 返 Err → 工作流 failed，不经 human_review)。原设计 2.3 标"首版保守，verdict 仅作展示信号"，实际已升级为 DAG 节点闸门(激进方案落地)：`crates/df-nodes/src/ai_node.rs:598-626`(gate==true 时 verdict=fail 返 Err)。
>
> **⑥ 端到端联调 — 代码层完成，实测类待用户**：联调依赖 secret 下沉+provider 注入链(`docs/02-架构设计/专项设计/secret下沉与provider注入方案-2026-06-16.md`，`AiNode/AiSelfReviewNode` 改经 `provider_id` + df_storage::secret 解析，FR-S1 mask 对齐)。实测类(tauri dev 跑 testing 模板 ai_self_review→human_review 闭环)待用户执行。
>
> **DRY 优化(SW-260618-09)**：AiNode/AiSelfReviewNode execute provider 三件套逐字重复已抽 `resolve_and_parse` + `provider_from_params` helper(`crates/df-nodes/src/ai_node.rs:70/80` 注释)。
>
> 原文以下设计正文保持不变，作为历史设计记录；落地形态以上方"实施状态"为准。

---

## 一、现状盘点（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<Database>`，`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<String>`（决策 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<Database>`，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<TaskRecord>` + 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<Database>`，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<Database>`，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": "<ai_execute 原始产出>",
  "model": "...",
  "usage": {...},
  "review": {
    "verdict": "pass" | "fail",
    "dimensions": {...},
    "summary": "...",
    "suggestions": [...],
    "reviewed_at": "<now_millis>",
    "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<Database>` + 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
<!-- F-AiNodeSelfReview: 任务产出 + 自审结果展示 -->
<div v-if="task.output_json" class="info-item info-block">
  <span class="label">{{ $t('taskDetail.output') }}</span>
  <div class="value output-block">
    <!-- 自审结论卡（若有 review 子字段） -->
    <div v-if="parsedOutput.review" class="review-card" :class="reviewVerdictClass">
      <span class="review-verdict">{{ reviewVerdictLabel }}</span>
      <span class="review-summary">{{ parsedOutput.review.summary }}</span>
      <ul v-if="parsedOutput.review.suggestions?.length" class="review-suggestions">
        <li v-for="(s, i) in parsedOutput.review.suggestions" :key="i">{{ s }}</li>
      </ul>
    </div>
    <!-- AI 产出（Markdown 渲染,复用 useRendered） -->
    <div v-if="parsedOutput.text" class="output-text ai-md" v-html="renderedOutput"></div>
  </div>
</div>
```

**实现要点**：
- `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<String>`（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<Database>` 字段 + `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 安全解析） |
