新增: AiNode自审实施方案设计(NodeContext注入task+prompt模板+5项实施)

This commit is contained in:
2026-06-16 22:29:25 +08:00
parent 539b5ed63e
commit 25409e5329

View File

@@ -0,0 +1,345 @@
# 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.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 安全解析) |