Files
DevFlow/docs/02-架构设计/专项设计/AiNode自审实施方案-2026-06-16.md
绝尘 998a2f243d 文档: 架构方案文档(意图识别论证+多主题愿景/论证+文档物理分类+边界清晰化)
squash合并:
- 意图识别层论证(8维度+10业界佐证)
- 多主题上下文管理愿景+并存论证+补充论证(多轮agentic)
- 架构设计文档物理分类(四子目录+INDEX+命名规范+引用同步+边界清晰化)
- 前端架构技术债清单归档
2026-06-19 15:04:04 +08:00

385 lines
28 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.promptprovider 配置从 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.rsdb 注入已有先例state.rs:265config 经 deep_merge 穿透AiNode 与 TaskAdvanceNode 注入方式一致 | AiNode 从无状态单例变有状态state.rs:257-259 工厂闭包需改 move db.clone |
| B. NodeContext 加 task 字段 | 改 node.rs 加 `task: Option<TaskRecord>` + db 句柄字段 | 节点直接读,省一次 DB 查 | 改 NodeContext 影响所有 NodeHumanNode/SleepNode/test 构造全改human_node.rs:236 make_ctx 等测试全改executor.rs:99-113 构造点要读 DBdf-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. **先例已验证** —— TaskAdvanceNodetask_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 全局 configworkflow.rs:72→ executor deep_mergeexecutor.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取默认 providertypes.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 写产出
**前置**方案①AAiNode 持 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/重启)。**推荐读 inputsDAG 内闭环task.output_json 仅作持久化与 human_review 展示源**。
**in_progress 模板适用**in_progress_templatetask_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-72ai_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_targetworkflow.rs:46-54 testing→in_review退回
**替代方案(更激进,不推荐首版)**ai_self_review verdict=fail 直接返 Err → 工作流 failed → 自动退回 in_review跳过人工。**风险**LLM 自审误判直接退回人无干预机会。首版保守verdict 仅作展示信号。
**实现要点**
- prompt 模板放节点 config 还是硬编码?—— **放 configrun_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/optionshuman_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
→ 解析 textai_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自审 verdictpass/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_targetworkflow.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/ ALTERmigrations.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_keyAiNode 内部调 `secret::ensure_resolved_key`src-tauri/src/commands/ai/secret.rs解析前端 config 只放 provider_id + model + protocolbase_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 已加)→ migrationsCREATE ✅ / ALTER ⚠️ 缺)→ crudSELECT ⚠️ / INSERT ⚠️ / UPDATE ⚠️ / 白名单 ⚠️ 全缺)。**须全量完成 crud 链路 + 白名单含 output_json**。
- **provider 配置注入链**:前端 ai_list_providersapi/ai.ts:76+ 默认 providertypes.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 | AiProviderRecordapi_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_keyapi_key 安全解析) |