Files
DevFlow/docs/05-代码审查/架构与缺陷复核报告-2026-06-14.md
绝尘 04032a2a8d 重构: 文档汇总+进度看板+孤儿任务清理脚本+gitignore 噪音排除
- docs/02 架构设计: 新增 aichat审查/异步审批构想/流式渲染调研/generating状态机/密钥迁移健壮性/工作流脚本执行边界/条件表达式引擎/F-07 trait下沉/Agent架构说明/任务推进构想/功能创意池;更新功能决策记录+归档/对抗论证/文档记录规范/经验记录
- docs/03 模块文档: 新增 AI对话引擎/DAG引擎详解;更新 df-knowledge/df-nodes/df-storage/df-workflow/df-ai
- docs/05 代码审查: 新增 全栈审查/全局review/架构审查/近期改动审查/工作区多角度走查/自研memo流式渲染审查
- docs/09 问题排查: 新增 aichat-apikey-401
- docs/INDEX+README 索引同步;docs/todo 待办看板(2026-06-15 汇总)
- PROGRESS.md Sprint 22-25;URGENT.md 加急清单快照(5 项 P0 已全修)
- scripts/cleanup_orphan_tasks.{py,sh} 孤儿任务清理工具
- .gitignore 补 *.broken.bak + tmp/ 噪音排除
2026-06-15 05:14:21 +08:00

215 lines
16 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
> 范围:在 [全栈代码审查报告-2026-06-14.md](./全栈代码审查报告-2026-06-14.md) 基础上的**复核 + 回归审计 + 架构层补充**。
> 方法4 路并行(安全/并发性能/架构/工作流),其中安全路子代理失控(误改任务表无产出),改由**主代理直读 5 文件**完成tool_registry / crud / workflow / commands / audit。其余 3 路子代理产出完整。
> 模式:**dry — 仅审查 + 文档,不改代码**。
> 互斥:与全栈报告去重;本报告只记**复核结论confirm/refute/已修)+ 新发现 + 报告修正**,不重复已记录的项。
---
## 0. 复核总览
| 类别 | 数量 | 说明 |
|---|---|---|
| 已修确认(最近提交修复正确) | 8 | FR-S2/S3/S4/S6、AR-3、FR-R1(主路径)/R3、FR-C1 |
| confirm复核证实仍存在 | 9 | FR-S1/S5、FR-R4/R5、FR-P2/P3/P5/P6、FR-P1(O(V·E)部分) |
| refute原报告误判/已澄清) | 3 | FR-R2 降级成立、FR-P1 panic 担忧不成立、FR-P4 严重度偏高 |
| **新发现(不在原报告/todo** | **11** | 见 §5其中 5 项 🟠 |
| 死代码确认 | 5 | set_waiting/set_skipped、emit_human_approval_request、ModelRouter、StreamCollector、IdeaPromoter |
**最高优先级(建议本轮接手)**§5 新发现 ②③④⑤FR-R1 闭环缺口 / client 降级丢 timeout / 取消事件语义双标 / cancel 无终态校验)。
---
## 1. 安全复核(主代理直读)
### ✅ 已修确认
**① FR-S2 — read_file/write_file [已核实修复正确]**
- `tool_registry.rs:367-376` read_file 改单次 `File::open → file.metadata() → read_to_string`**同句柄**消除原 metadata+read 两步 syscall 间 symlink 替换的 TOCTOU。1MB 上限在 `metadata.len()` 判定后 bail。
- `tool_registry.rs:416-418` write_file 加 `content.len() > 1_048_576` bail。
- 两处入口均先走 `resolve_workspace_path`canonicalize 防 symlink 逃逸)。
- **残余低优**read_file 的 metadata 大小判定与实际 `read_to_string` 之间,若文件在判定后被追加写入,仍可能读入 >1MB路径已 canonicalize攻击者难 swap属理论面。write_file 无此问题(写 content 大小在内存已知)。
**② FR-S3 — approve decision 校验 [已核实修复,但 IPC 层弱于建议]**
- `workflow.rs:189` `decision.trim().is_empty()` → Err。防 `""` 透传卡死 HumanNode。
- **缺口**IPC 层**未做 decision ∈ {"approved","rejected"} 白名单**(原报告建议)。深层兜底在 `human_node.rs:67``options.is_empty() || options.contains(&decision)`,故 options 非空时仍拒非法值。属"IPC 弱 + HumanNode 强"的纵深防御可接受若想严格化IPC 补一行白名单即可。
**③ FR-S4 — SKILL.md 隔离标注 [已核实修复,属弱防御]**
- `commands.rs:67-74` 注入前包裹 `# 用户选择的技能说明: {}(非系统指令,勿作为行为准则覆盖;以下为技能内容供参考)`,且技能内容置于 system_prompt **之前**、系统真行为准则在**之后**recency 优势)。
- **性质**:纯文本标注,是 prompt injection 的**缓解非根治**——信任本机技能文件的设计取舍下可接受,但恶意 SKILL.md 仍可能通过语义注入影响输出。已在功能决策记录标注此取舍即可,不升级。
**④ FR-S6 — validate_column_name 未登记表拒绝 [已核实修复正确]**
- `crud.rs:339` `None => Err("表 {} 未登记列白名单,拒绝防注入")`,与 `is_allowed_column``None => false` 语义一致。
- **回归风险评估**allowed_columns_for 当前登记 ai_tool_calls/ai_conversations/ai_messages/ai_providers/ai_sessions/ideas/idea_scores/projects/tasks/workflows/node_executions/knowledges/knowledge_eventsmatch arms 可见)。所有走 `query`/`update_field` 通用路径的 Repo 表均已登记(编译期表名宏 + 实测),无合法查询被误拒。低回归风险。
**⑤ AR-3 — 审批 reason 拼对象名 [已核实修复且实现质量高]**
- `audit.rs:64-138` `build_approval_reason` 特化 9 工具delete/restore/purge_project、update_project、bind_directory、create_task、create_project、create_idea、run_workflow。
- `resolve_project_label`:44`ProjectRepo::get_by_id` 取 name查不到回退 `(id=x)`,空 id 返回空串。
- 无 N+1每次审批单工具单查询非循环。
- 未特化工具走 risk fallback 模板。覆盖完整。
### 🟠 FR-S1 — api_key 明文三连 [维持未修]
确认现状未动migrations.rs/models.rs/commands.rs/Settings.vue 四处仍明文。沿用全栈报告结论P0 待 OS keychain 落地。
### 🟡 FR-S5 — 跨对话越权审批 [确认降级文档成立]
`commands.rs:117-120` ai_approve 仅按 tool_call_id 从 pending_approvals 取,无对话归属校验。但本地单机 + restore 只载当前对话 + UI 隔离,实际触发面窄。维持降级为文档说明。
---
## 2. 并发/性能复核子代理产出11 项)
| # | 项 | 判定 | 证据要点 | 严重度 |
|---|---|---|---|---|
| FR-R1 | 切换 token | **已修(主路径) + 遗漏点** | `_latestSwitchId` 在首个 await 后比对正确,**但 pendingToolCalls(:108) await 后无二次比对**A→B 快切可覆写 B 的 pendingApprovals | 🟠(见新发现①) |
| FR-R2 | 看门狗多窗口互踩 | **refute降级成立** | 分离窗口=独立 WebviewWindow=独立 JS realm模块级 state 各持一份不共享 | 🟢 |
| FR-R3 | liveEvents 限长 | **已修正确** | `project.ts:215` splice(0, len-200),无 off-by-one配合 `_ts` | 🟢 |
| FR-R4 | complete 无超时无重试 | **confirm** | openai/anthropic_compat 仅 `connect_timeout(30s)`complete() 同步路径无总 timeout远端建连后挂起则永久 hang | 🔴 |
| FR-R5 | findToolCall O(n²) | **confirm** | useAiEvents.ts:63 双层 for+find4 处事件触发点 | 🟡 |
| FR-P1 | dag O(V·E) + panic | **confirm(O(V·E)) / refute(panic)** | successors/predecessors iter().filter 全表扫;但 in_degree 已对全部 node_ids 初始化且 contains 过滤野节点,`unwrap()` 不会 panic | 🟠→🟡 |
| FR-P2 | search_vector SELECT * | **confirm** | crud.rs:1183 `SELECT *` 拉 content/reasoning 大文本,无 LIMITRust 算余弦 | 🟠 |
| FR-P3 | 单连接 Mutex | **confirm** | db.rs:13 `Arc<Mutex<Connection>>`,每 CRUD spawn_blocking+blocking_lock 串行 | 🟠 |
| FR-P4 | 500ms 轮询锁竞争 | **confirm(轮询) / refute(严重度)** | interval(500ms) 确实存在,但 std Mutex 无竞争纳秒级,"7200 次锁竞争"表述偏重 | 🟡 |
| FR-P5 | build_system_prompt 全表扫 | **confirm** | prompt.rs:75 `list_active()` 全表 + take(20) + format!,每消息+每 agentic 轮触发list_active 无 LIMIT | 🟠 |
| FR-P6 | watch messages JSON.stringify | **confirm** | AiChat.vue:650 `{deep:true}` + 全量 stringifysnapshot 比对只省后续逻辑不省 stringify 本身 | 🟠 |
---
## 3. 架构层(子代理产出,最大结构性风险)
**架构健康度:🟢 基本健康7/10**。依赖图无环、df-core 严格叶子、IPC 无 SQL 泄漏、前后端类型对齐度高、僵尸 crate 清理彻底grep df_evolve/plugin/stages/task/traceability 零残留)。
**最大结构性风险**:🟠 **crate 层与 IPC 层职责边界双向错位**——领域逻辑(评估编排/晋升事务补偿/AI 扫描编排)沉淀在 IPC commands 层,而对应的 crate 模块df-ideas::promotion、df-project::manager、df-ai::router/stream反而是空壳或死代码。
### 3.1 依赖图与职责
- `df-nodes` 同时依赖 df-core/df-execute/df-workflow/df-ai 四个,是唯一多父汇聚点。结构正确(节点需调执行器/工作流/AI非阻塞。
- `df-workflow/src/conditions.rs:16-33` ConditionEngine 仅识别 true/false 字面量,其余默认 false保守拒绝安全。文档承诺的 JSON Path/数值/逻辑组合语法全 TODO。todo.md:137 T-260614-11 已记。
### 3.2 空壳/死代码5 处,建议清理或显式标注)
| # | 位置 | 判定 | 严重度 |
|---|---|---|---|
| ⑥ | `df-workflow/state.rs:87-100` set_waiting/set_skipped | 全仓零调用方,纯死代码 | 🟡 |
| ⑦ | `df-workflow/eventbus.rs:43-46` emit_human_approval_request | 全仓零调用方HumanNode 用 async send | 🟡 |
| ⑧ | `df-ai/router.rs` ModelRouter | **route() 全返回 default_modellib.rs 未导出,零调用方** | 🟠 |
| ⑨ | `df-ai/stream.rs` StreamCollector | 流式接收走 IPC 层 stream_recv.rs此模块零调用方 | 🟠 |
| ⑩ | `df-ideas/promotion.rs:45-91` IdeaPromoter/try_promote/do_promote | 全 TODO 占位IPC 层 idea.rs 重写了晋升逻辑crate 层零调用 | 🟠 |
> coordinator.rsdf-ai是 B 路线**有意占位**(文件头注释+文档对齐),非死代码,建议 run() 改 `bail!("未实现")` 更安全。
### 3.3 IPC 层 carry 领域逻辑3 处)
- `idea.rs:184-231` evaluate_idea评估编排 + 结果组装 + `scores*10` 缩放 + recommendation/action_items 扁平化映射,整段领域逻辑落 IPC。
- `idea.rs:140-153` promote_idea跨表事务补偿回写失败→补偿删 project落 IPC。
- `project.rs:335-377` scan_project_with_aibuild_scan_prompt/parse_scan_result 纯函数级 AI 编排落 IPC。
后果:领域逻辑绑 Tauri State 无法 crate 级单测AI 工具 create_* 与 IPC create_* 各写一份字段默认值tool_registry.rs vs project.rs/task.rs字段增删易漏改分叉。
### 3.4 前后端类型 drift3 处注释错误)
- `types.ts:88` TaskStatus 注释 `review_ready/merged/abandoned` 三值后端枚举不存在(实际 todo/in_progress/in_review/testing/done/blocked/cancelled。🟠
- `types.ts:89` Priority 注释方向反(前端 `0=critical/3=low`,后端 df-core `Low=0/Critical=3`)。🟠
- `types.ts:39` ProjectStatus 注释漏 testing/releasing。🟡
- 类型本身 string 不阻断,但注释误导前端排序/展示逻辑写反。
### 3.5 错误处理双轨
- df-storage 用强类型 `df-core::Error`thiserror其余 cratedf-ai/df-ideas/df-project/df-workflow/df-execute全 anyhow。`df-core::Error` 的 Workflow/Execution/AiProvider/Plugin 等变体零使用=死变体。🟠
- IPC 层统一 `.to_string()` 压平成中文消息,前端无法区分 404/500。🟡Tauri 限制下折中,可接受)
---
## 4. 工作流审批链复核(子代理产出)
### 4.1 死代码confirm 3 处)
set_waiting/set_skipped、emit_human_approval_request 零调用(见 §3.2 ⑥⑦。set_cancelled 绕 is_legal 直接 insertstate.rs:106todo 决策采"修法 B 绕而非纳入白名单",与全栈报告建议分歧,属待统一项。
### 4.2 回归审计(最近 5 提交无 P0/P1 回归)
- ✅ R6 human_node send 加 awaithuman_node.rs:42测试 `request_is_emitted_to_bus` 验证。
- ✅ select! 三分支Response 双键过滤 / sleep_until 无漂移 / cancel 首 tick 丢弃)正确。
- ✅ Lagged(n) continue 容忍正确。
- ✅ decision 校验human_node.rs:67 三态 + IPC workflow.rs:189 双层)。
- ✅ executor Err 处理 is_cancelled guardexecutor.rs:123-141跳 set_failed避开 Cancelled→Failed 非法 transition。
- ✅ cancel 链端到端通IPC→registry→共享 StateMachine→HumanNode 500ms 轮询→Err→executor
### 4.3 集成测试缺口confirm
前端无 human DAG 入口demoDag 仅 3 个 script 节点),现有测试用 mock `CancelSelfNode` 验证取消,**非真 HumanNode 路径**。select! cancel 分支 + interval tick 逻辑无端到端覆盖。todo.md:90 B-03b-R8 已记。
---
## 5. 新发现汇总11 项,不在原报告/todo
### 🟠 高优(建议本轮处理)
**新-① [src/composables/ai/useAiConversations.ts:108-126] FR-R1 闭环不完整pendingToolCalls await 后无 token 比对**
A→B 快切时,首个 await 已挡住 messages/activeConversationId 覆写,但 `aiApi.pendingToolCalls`(:108) 慢返回仍会覆写 B 的 `state.pendingApprovals`(:120)。建议补 `if (mySwitchId !== _latestSwitchId) return`
**新-② [crates/df-ai/src/{openai,anthropic}_compat.rs] client builder 降级丢 connect_timeout**
`Client::builder().connect_timeout(30s).build().unwrap_or_else(|_| Client::new())` — build 失败降级到 `Client::new()` 丢失连接阶段超时。建议降级分支显式 `Client::builder().build()`
**新-③ [crates/df-workflow/src/executor.rs:130-135] 取消节点状态保 Cancelled 但仍发 NodeFailed 事件,语义双标**
已取消节点状态机保 Cancelled但事件总线仍 emit `NodeFailed { error: "人工审批被取消" }`。前端若按 NodeFailed 事件分支判断会误归类"失败"而非"取消"。建议加 `NodeCancelled` 事件 variant 或 NodeFailed 加 `cancelled: bool` 字段。
**新-④ [src-tauri/src/commands/workflow.rs:218-236] cancel_workflow_node 无节点终态前置校验**
仅校验 execution_id 在注册表,不校验 node_id 状态。对已 Completed/Failed/Cancelled 节点调 set_cancelled 会静默覆盖终态set_cancelled 绕 is_legal。建议加 `match sm.get(&node_id) { Running|Waiting => {}, _ => Err }` 守卫。
**新-⑤ [前端无 human DAG 入口] 端到端 human 审批测试缺口**
见 §4.3CancelSelfNode 自取消与 HumanNode 走 select! cancel 分支是两条代码路径,后者无覆盖。
### 🟠 中优
**新-⑥ [df-ai/router.rs] ModelRouter 空壳死代码** — 见 §3.2 ⑧route() 全返回 default_model零调用方。
**新-⑦ [df-ai/stream.rs] StreamCollector 死代码** — 见 §3.2 ⑨,流式逻辑全在 IPC stream_recv.rs。
**新-⑧ [df-ideas/promotion.rs] IdeaPromoter 死代码** — 见 §3.2 ⑩IPC 层重写了晋升。
**新-⑨ [types.ts:88-89] TaskStatus/Priority 注释 drift** — 见 §3.4,误导前端排序逻辑。
**新-⑩ [df-core/error.rs] 错误类型双轨,半数变体死代码** — 见 §3.5。
### 🟡 低优
**新-⑪ [useAiWindow.ts:79-95 + useAiStream.ts:18] 主窗口看门狗在分离窗口接管生成时仍跑**
detach 时主窗口 streaming/generatingConvId 未清,看门狗续计 130s 后往主窗口 state 补幽灵错误消息,但用户已切走无实际危害,仅主窗口面板重开时看到一条幽灵错误。
---
## 6. 报告修正refute 原 report 误判)
| 原报告项 | 修正 |
|---|---|
| FR-R2 看门狗多窗口互踩 | **应标 refute**:分离窗口独立 JS realm 不共享 state 单例降级理由成立todo 已记"评估维持",复核确认) |
| FR-P1 in_degree.unwrap() panic 风险 | **refute**in_degree 对全部 node_ids 初始化 + contains 过滤野节点unwrap 不 panic |
| FR-P4 "7200 次锁竞争" | 严重度偏高std Mutex 无竞争纳秒级,实为微秒级开销;保留"500ms 轮询模式不佳"批评,建议改 broadcast/Notify |
---
## 7. 建议进 todo 的项(去重后)
> 已在 todo 的不重复;下列为本复核**新产出**,待用户确认进看板。
| 建议 ID | 对应 | 优先级 |
|---|---|---|
| 复核-新① FR-R1 pendingToolCalls 闭环 | useAiConversations.ts:108 补 token 比对 | P1 |
| 复核-新② client 降级丢 timeout | openai/anthropic_compat 降级分支显式重建 | P2 |
| 复核-新③ 取消事件语义双标 | 加 NodeCancelled variant 或 cancelled 字段 | P2 |
| 复核-新④ cancel 无终态校验 | workflow.rs:223 加状态前置守卫 | P2 |
| 复核-新⑤ human 端到端测试缺口 | = todo B-03b-R8已记提优先级 | P1 |
| 复核-新⑥⑦⑧ 死代码三连 | ModelRouter/StreamCollector/IdeaPromoter 删或标注 | P2 |
| 复核-新⑨ types.ts 注释 drift | TaskStatus/Priority 注释对齐 df-core 枚举 | P2 |
| 复核-新⑩ 错误类型双轨 | df-core::Error 瘦身或各 crate 收敛 | P2 长期 |
| FR-R4 complete 无 timeout | (已在 todo复核确认 🔴,建议提 P1 | P1 |
| FR-S1 api_key 明文 | (已在 todo P0复核确认未动 | P0 |
**优先处理 3 件**(影响实际行为):
1. **复核-新①** FR-R1 闭环缺口(审批卡片可见性错配,一行修)
2. **复核-新③④** 取消事件语义 + 终态校验(审批链语义正确性)
3. **FR-R4** complete 无 timeout一次网络抖动挂死整轮对话提 P1
---
> 本会话**只做代码审查 + 文档记录**,不含代码变更。实际修复另起会话。