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

130 lines
8.2 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.
# 工作流审批子系统对抗审查报告
> 2026-06-14 · 多代理审查(31 agents / 5 维度 × 对抗验证) + 主代理独立复核 + 二轮对抗论证
> 范围: df-workflow/{executor,state,eventbus}.rs · df-nodes/human_node.rs · df-core/events.rs · src-tauri/{commands/workflow,state}.rs · src/stores/project.ts · src/api/{types,workflow}.ts · src/views/ProjectDetail.vue
> 方法: workflow fan-out 审查 → 每发现独立对抗验证(默认反驳) → 主代理读源码复核 criticals → 二轮对抗论证(存在性/可达性/危害三维)
## TL;DR
1. **头号 bug [P0 阻断]**: `human_node.rs:41``HumanApprovalRequest``.await``EventBus::send` 是 async fn`let _ = async_fn()` 丢弃 Future 未 poll → body 不执行 → **Request 从未进入 channel**。审批链最上游断裂。
2. **①②(前端契约失配) 被头号遮蔽**: Request 没发 → 前端 `onEvent` 收不到 → type 匹配分支不可达。修 41 行后 ①② 才显形为 critical。
3. **当前 UI 无 human 节点 DAG 入口**: `demoDag` 仅 script 节点AI 工具 `run_workflow` 返回提示不执行。审批相关 9 项发现现实触发率=0全部潜伏。
4. **根因**: 审批功能前端从未端到端跑通41 行 await 漏掉即铁证),单测绿但不覆盖 human→前端弹窗→审批→返回链路。
---
## 0. 头号发现 [P0]
### 0.1 缺陷定位
`crates/df-nodes/src/human_node.rs:41-47`
```rust
let _ = ctx.event_bus.send(WorkflowEvent::HumanApprovalRequest {
execution_id: ctx.execution_id.clone(),
node_id: ctx.node_id.clone(),
title: title.to_string(),
description: description.to_string(),
options: options.clone(),
}); // ← 无 .await
```
### 0.2 机制
- `EventBus::send` 签名 (`eventbus.rs:33`): `pub async fn send(&self, event: WorkflowEvent)` — async fn。
- `let _ = async_fn()` 求值得 Future绑定 `_` 后语句结束立即 drop**Future 零 poll**。
- async fn body (`self.sender.send(event)`) 仅在 Future 被 poll 时执行 → 此处永不执行 → Request 未进 broadcast channel。
### 0.3 对抗自检(排除假阳)
| 质疑 | 核实 |
|------|------|
| 测试为何 pass? | `normal_approval_returns_decision` 的 helper `send_response``send(...).await`(`human_node.rs:172` 有 await) 发的是 **Response**HumanNode 的 Request 那行没 await。测试只断言 Response 被 rx 收到并返回 decision**不验证 Request 是否发出**。绿测不能证伪。 |
| 编译器为何不报? | `let _ = expr` 合法通配符绑定async fn 生成的 Future 默认无 `#[must_use]`,零 warning。 |
| 是否误用同步 fn? | `eventbus.rs:44` `emit_human_approval_request` 是同步 fn(返 Result),但 HumanNode 未用它,用的是 async `send`。 |
| 多代理为何漏? | 审查聚焦前端契约(①②)与串扰(③),未逐行核 HumanNode 内 send 调用点是否 await。主代理二轮读 human_node.rs 全文才发现。 |
### 0.4 触发路径
`human_node.rs:38 subscribe → :41 send(无await)` → Request 未发 → `workflow.rs` 转发任务 rx 收不到 → 前端 `onEvent` 不触发 `HumanApprovalRequest` 分支 → `pendingApproval` 恒 null → 弹窗不开 → HumanNode `select!` 阻塞至 `:86 sleep_until(deadline)` 默认 3600s 超时 → Err → 工作流 failed。
### 0.5 修复
```diff
- let _ = ctx.event_bus.send(WorkflowEvent::HumanApprovalRequest { ... });
+ ctx.event_bus.send(WorkflowEvent::HumanApprovalRequest { ... }).await;
```
---
## 1. 反转结论
### 1.1 ①② 被头号遮蔽(降级: 当前不可达)
-`project.ts:214` `payload.event?.type === 'HumanApprovalRequest'`大驼峰vs 后端 `events.rs:18` `#[serde(tag="type", rename_all="snake_case")]` → 序列化值 `'human_approval_request'`,永不匹配。
-`project.ts:215` `payload.event.data` —— event 是 `WorkflowEvent` 本体扁平结构(`{type, execution_id, node_id, title, description, options}`),无 `data` 包装层 → undefined。
- 两者代码层面确凿,但 Request 未发(§0 遮蔽) → 前端收不到事件 → 分支不可达。**修 41 行后 ①② 立即变 critical**。
### 1.2 当前 UI 无 human 节点入口(多数发现现实触发率=0
| 证据 | 位置 |
|------|------|
| 前端唯一 DAG = demoDag仅 3 个 script 节点 | `ProjectDetail.vue:271-273` |
| AI 工具 run_workflow 返回提示不执行 DAG | `tool_registry.rs:346-350` |
| runDemoWorkflow 按钮 disabled=workflowRunning 防重入 | `ProjectDetail.vue:281-289` |
→ 审批相关发现(①②④⑤⑦⑧⑨⑩)全部潜伏在未接入路径。
---
## 2. 逐条对抗裁定
| # | 定位 | 存在性(代码层) | 现实可达性 | 危害校正 | 裁定 |
|---|------|---------------|-----------|---------|------|
| **0** | human_node.rs:41 | 确凿 缺await | human DAG 运行即必现UI 无入口 | Request 不发,链最上游断 | **真·潜伏头号 P0** |
| ① | project.ts:214 | 确凿 snake_case | 被0遮蔽+无human入口 | 潜伏修0后才显形 | 降级: 当前不可达 |
| ② | project.ts:215 | 确凿 flat无data | 同① 双重遮蔽 | 潜伏 | 降级: 同① |
| ③ | workflow.rs:67-97 | 确凿 全局bus+Node*无exec_id+matches!只看变体 | 需并发工作流UI防重入+AI不执行 | 审批路由不坏(Request自带exec_id),仅日志串扰+提前break | 真实架构缺陷现实触发0危害被夸大 |
| ④ | project.ts:15-21 | 确凿 单槽 | 三重遮蔽(0+无入口+单流) | 潜伏 | 真实,潜伏 |
| ⑤ | project.ts:208-261 | 确凿 无终态监听 | 同④;"稍后"按钮可视觉关 | UX退化非卡死 | 真实,危害偏低 |
| ⑥ | state.rs:106 | 确凿 直接insert | 需⑦竞态命中 | snapshot()零调用方(死代码)DB不受影响 | **真实但无害** |
| ⑦ | project.ts:228 | 确凿 无互斥 | 需0+①②+入口全通+双击+500ms窗口 | 窄窗口 | 真实,潜伏 |
| ⑧ | workflow.rs:171 | 确凿 无校验(对比cancel有registry守卫) | 需human+超时窗口 | 诊断损失非功能损坏 | 真实,潜伏 |
| ⑨ | human_node.rs:73-81 | 确凿 warn+continue | 需256积压当前无节点发NodeProgress每节点≤2事件需128+并发节点 | 概率近0 | 真实,当前不可达 |
| ⑩ | workflow.rs:91-97 | 确凿 Lagged+Closed死代码(bus持于AppState全程) | 同⑨ 需256积压 | DB终态仍正确仅task泄漏 | 真实,当前不可达 |
| ⑪ | workflow.rs:140 | 确凿 String::new() | demoDag失败即触发 | 低危: error字段含first_err.context"节点X失败"failed_node冗余空 | **真实且当前可达,危害低** |
---
## 3. 根因
审批功能前端从未端到端验证。证据链:
1. `B-260614-03a` 重写 HumanNode `execute`(subscribe→send→select!) 时引入 `:41` await 缺失7 单测全绿未抓(单测不覆盖 Request 发出)。
2. 前端无 human 节点 DAG 入口,无任何集成测试覆盖 human→弹窗→审批→返回链路。
3. ①② 是前后端契约层断裂,`types.ts:127` `event.type: string` 弱类型无编译期拦截。
→ 单测绿 + 无端到端测试 = 这批潜伏 bug 的存活土壤。
---
## 4. 修复优先级
| 序 | 动作 | 优先级 | 依赖 | 会暴露 |
|---|------|--------|------|--------|
| 1 | 补 human 节点端到端集成测试(含human的DAG→运行→断言前端收到Request+弹窗开) | P0 | 无 | 0+①+② |
| 2 | human_node.rs:41 加 .await | P0 | 测试暴露后修 | — |
| 3 | project.ts:214 type→snake_case; :215 取 event 本体字段; types.ts event.type 收窄字面量联合 | P0(修2后) | 2 | — |
| 4 | ③ Node*事件补 execution_id + 转发过滤 | P2 | 并发工作流时 | — |
| 5 | ④⑤ pendingApproval 改 Map + 终态清空 | P2 | 2③后 | — |
| 6 | ⑥ set_cancelled 查终态 no-op | P3 | 无(无消费者零危害) | — |
| 7 | ⑦⑧⑨⑩⑪ | P2-P3 | 见§2 | — |
---
## 附: 多代理 workflow 元数据
- 31 agents / 5 维度(concurrency / state-machine / error-handling / lifecycle / frontend-contract) × 对抗验证
- 26 原始发现 → 12 确认 / 11 反驳 / 3 验证代理因 API 限流未跑(主代理手动补判)
- 验证阶段正确识别 `executor.rs:124` 三条为 not-a-bug(B-03b-R1 已修,`:127` 有 is_cancelled guard + test_cancelled_node_skips_set_failed)
- 漏抓头号(§0): 因未核 send await主代理二轮复核补