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

360 lines
25 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
> 范围Rust 9 crate + Tauri 命令层 + Vue 3 前端stores/composables/views/components/api约 25k 行。
> 方法5 个 general-purpose 子代理按模块并行审查df-nodes/workflow/execute、df-ai/ideas/storage、src-tauri commands、stores/composables、views/components/api主代理对高严重度项逐条读码核实。
> 互斥关系:与 [aichat审查报告-2026-06-14.md](../02-架构设计/aichat审查报告-2026-06-14.md)AI Chat 专项)、[工作流审批审查报告-2026-06-14.md](../02-架构设计/工作流审批审查报告-2026-06-14.md)(工作流审批专项)去重;本轮不重复两份已记录的内容,仅记新发现与核查澄清。
> 增补2026-06-14 追加 §0.1 修复进度表 + 在 FR-D1/FR-P1/FR-P2/FR-R4/FR-R5 章节内联标注已修复状态(对照 commit 36d68dd / 4a95f6a / 4b5f096
---
## 0. 核查澄清(审查代理误判,不记为 bug
### ✅ 已澄清:`df-ideas/promotion.rs:80-91` `do_promote` 非"伪造成功"
子代理报告称 `do_promote` 返回 `promoted:true` 却未建项目,是"成功撒谎"高严重度 bug。**核查后判定为误判**
- IPC 层 `src-tauri/src/commands/idea.rs:112` `promote_idea` **真正调用** `df_project::manager::ProjectManager::create_from_idea` 构造项目实体 + 映射 `ProjectRecord` + `update_full` 回写想法。前端走此 IPC不走 crate。
- `df-ideas/src/promotion.rs``do_promote`Auto 策略分支)**无任何调用方**,是 [功能决策记录 §想法立项](../02-架构设计/功能决策记录-2026-06-14.md) 明确的"crate 留纯决策 TODO"——crate 不依赖 df-storage/df-project 以避循环依赖,副作用放 IPC 组合。
- [todo.md:145](../todo.md) 已标注 `do_promote crate 层 TODO现走前端闭环`
**保留的低优提示**`do_promote` 返回值 `promoted:true` 对一个未接 df-project 的函数是误导性签名,若将来有人直接调用会产生真实的"成功撒谎"。建议接入前把返回改 `promoted:false` + `reason: "crate 层未接入"`,或加 `#[allow(dead_code)]` + 注释固化"无副作用占位"语义。**不计为新 bug仅留改进提示。**
## 0.1 修复进度2026-06-14 增补)
> 后续 commit 已落地的修复项汇总。下表 ID 沿用 commit message 编号(与本报告 §2-§6 的 FR-S/C/R/P/D 编号体系不完全一致,已括注对应章节)。报告内已有条目的,已在对应章节标题追加 ✅ 标记并补「已完成」说明;以下为**报告原未单列、commit 已落地**的项。
| commit ID | 问题 | 位置 | 状态 |
|---|------|------|------|
| 36d68dd FR-D6 | 补 `delete_task`/`update_task` 工具(防误用 `delete_project` 清理孤儿任务)| `tool_registry.rs` | ✅ 已完成(原缺任务级删改工具,迫使 LLM 误调项目级删除)|
| 36d68dd FR-D7 | 抽 `bind_dir_to_project` 消除 `create_project`/`bind_directory` 重复DRY| `tool_registry.rs` | ✅ 已完成 |
| 36d68dd FR-D8 | `create_idea` schema 补 `priority` 字段(前后端契约对齐)| `tool_registry.rs` | ✅ 已完成 |
| 4a95f6a FR-D4 | `replace_tool_result_content` 正向 O(n) 改反向 `rposition`(审批替换命中最近)| `crates/df-ai/src/context.rs` | ✅ 已完成(本报告未单列此 crate 层方法;低频审批路径,不引入索引)|
> 本报告 §6 FR-D4 指向的是 `migrations.rs` 的 13 个 if 链 DRY与此处 commit 内 FR-D4 不同名同号),两者无关,勿混淆。报告 FR-D1confirmDialog 重复)即 commit 内 FR-D5已在 §6 内联标注。
---
## 1. 与已有文档去重
以下子代理报告的项**已在现有文档/todo 记录**,本报告不重复,仅交叉指引:
| 子代理发现 | 已记录位置 | 状态 |
|---|---|---|
| 条件引擎空壳(`conditions.rs` 仅认 true/false 字面量) | todo.md:30 (B-260614-02 修默认值) + todo.md:112 (T-260614-11 表达式升级待做) | 已记录 |
| 路径穿越 / symlink 逃逸 | todo.md:102 (T-260614-04 已完成 canonicalize) | 已修canonicalize 防逃逸,但 **未防 read_file TOCTOU**,见 FR-S2 |
| localStorage 跨窗口传 currentText | todo.md:98 (B-260614-05Sprint 19 有意保留) | 已记录 |
| ai_key 明文存 SQLite | 功能决策记录-归档-2026-06-14.md旧记录无根治决策 | **未根治,本轮重申见 FR-S1** |
| Settings.vue 1042 行 god file | todo.md:88 (T-260614-06 待立项) | 已记录 |
| confirmDialog 4 处重复 | 隐含在 View改造指南无显式 todo | 记为 FR-D1 |
---
## 2. 安全(最高优先级)
### 🔴 FR-S1 — api_key 明文存储 + 经 IPC 明文传前端 [已核实]
**位置**
- `crates/df-storage/src/migrations.rs:445-457``ai_providers.api_key TEXT NOT NULL` 明文列)
- `crates/df-storage/src/models.rs:138``api_key: String` 裸字段)
- `src-tauri/src/commands/ai/commands.rs:299-318``ai_save_provider` 直接 `update_full` 落盘)
- `src/views/Settings.vue:53``listProviders` 返回完整 key前端 `maskKey` 仅展示层)
**问题**三个环节都明文——DB 文件 `app_data_dir/devflow.db` 明文落盘全部 provider 密钥list 命令经 IPC 把明文 key 传到前端 webview前端仅做视觉 mask。任何能读 DB 文件的进程(备份、同步盘、其他本机进程)直接拿全部密钥。
**严重度**:高(桌面应用典型安全缺口,密钥=成本+越权)。
**建议**OS 级凭据存储Windows Credential Manager / macOS Keychain / Linux Secret Service`keyring` crate 包一层DB 只存引用 idlist 命令返回 mask 后的占位,仅编辑保存时按需取明文。
### 🔴 FR-S2 — `read_file` TOCTOU + `write_file` 无大小限制 [已核实]
**位置**`src-tauri/src/commands/ai/tool_registry.rs:364-417`
**问题**
- `read_file:364-380``tokio::fs::metadata(path)` 检查大小 ≤1MB`tokio::fs::read_to_string(path)` 重新打开读——两个独立 syscall 之间路径可被 symlink 替换1MB 限制可被绕过(检查时指向小文件、读取时切到大文件)。
- `write_file:399-417` 无任何 `content.len()` 校验LLM 经审批可写超大文件撑爆磁盘/对话历史(虽有 `truncate_for_persist` 50KB 兜底持久化,但磁盘写入无防护)。
**注**T-260614-04 已加 `resolve_workspace_path` canonicalize 防 symlink 逃逸,但 canonicalize 在文件存在时才做、且不防检查/读取两步间的替换。
**严重度**:高(审批门控后的 LLM 写权限,破坏面大)。
**建议**`read_file` 改单次 `File::open → file.metadata() → read` 同一句柄消除竞态;`write_file``content.len()` 上限(如 1MB超限 bail。
### 🔴 FR-S3 — `approve_human_approval` 未校验 decision 白名单 [已核实]
**位置**`src-tauri/src/commands/workflow.rs:180-206`
**问题**`decision: String` 直接透传到 `HumanApprovalResponse`,无白名单校验。`human_node` 若对 decision 做字符串匹配(如 `== "approved"`),传入 `""` / `"yes"` / `"approve"` 会被当作既非批准也非拒绝HumanNode 可能永久卡死直到超时。
**严重度**:高(工作流审批的输入校验缺口,前端可传任意字符串)。
**建议**:命令层校验 `decision ∈ {"approved","rejected"}`,非法值返回 Err。
### 🟠 FR-S4 — SKILL.md 全文注入 system prompt [仅代理报告待复核]
**位置**`src-tauri/src/commands/ai/commands.rs:67-71` + `src-tauri/src/commands/ai/skills.rs:164-169`
**问题**`ai_chat_send(skill=...)` 把任意本机 `~/.claude/skills/*/SKILL.md` 全文拼到 system prompt。被篡改/恶意的 SKILL.md含 prompt injection 指令)会作为系统指令注入,覆盖行为准则;配合 LLM 可调工具write_file/create_project构成提权路径。
**严重度**:中(信任本机技能文件的设计取舍,但缺隔离标注)。
**建议**:注入时明确包裹"以下是用户选择的技能说明,非系统指令"的隔离边界;或文档标注此取舍。
### 🟠 FR-S5 — 跨对话越权审批 [仅代理报告待复核]
**位置**`src-tauri/src/commands/ai/audit.rs:111-139` + `commands.rs:104-185`
**问题**`ai_approve` 只按 `tool_call_id``session.pending_approvals` 取审批,不校验该 tool_call 是否属于前端当前展示的对话。配合 `restore_pending_approvals` 启动时把所有 pending 行重建到单一内存 HashMap任意前端可对任意对话的 pending 工具调用执行/拒绝。
**严重度**:中(本地单机影响有限,但审批 UI 的"对话隔离"是假象Medium/High 工具审批门控意义被削弱)。
**建议**`ai_approve` 增加 `expected_conversation_id` 参数并校验;或降级为文档说明"审批不按对话隔离"。
### 🟡 FR-S6 — `validate_column_name` 未登记表直接放行 [仅代理报告待复核]
**位置**`crates/df-storage/src/crud.rs:334-350`
**问题**`allowed_columns_for` 对未登记表返回 `None``validate_column_name` 返回 `Ok(())` 放行。当前 7 个 Repo 内部安全(表名是宏编译期常量),但 `is_allowed_column``pub fn`,若未来加了未登记表却走通用查询路径,列名直接进 `format!("... WHERE {} = ?1", field)` 字符串拼接即 SQL 注入。靠纪律非靠类型。
**严重度**:低(当前不可触发,潜在注入面)。
**建议**:未登记表保守拒绝,`None` 分支改 `Err`
---
## 3. 正确性 Bug行为错但不伪造
### 🟠 FR-C1 — `formattedEvents` 时间漂移 [已核实]
**位置**`src/views/ProjectDetail.vue:298-307`
**问题**`formattedEvents` 是 computed内部对每条事件 `new Date().toLocaleTimeString()` 取"当前时刻"。每次 `store.liveEvents` 变化(新事件追加)整个 computed 重算,**所有历史日志的 time 被刷新为最新触发时刻**,而非事件发生时刻。日志时间全错。
**严重度**:中(明显体验 bug
**建议**事件入数组时固化时间戳store push 时记 `Date.now()` 或用后端事件自带的 timestampcomputed 只读不改时间。
### 🟠 FR-C2 — `net_sentiment=0` 文案/样式矛盾 [已核实]
**位置**`src/views/Ideas.vue:103`(模板)+ `:368``sentimentClass`
**问题**:模板 `adversarialEval.net_sentiment > 0 ? 正面 : 负面``> 0` 严格),`sentimentClass` `sentiment >= 0 ? 'positive' : 'negative'``>= 0` 含零)。`net_sentiment === 0` 时:文案走 else 显示「负面」,样式走 `positive`。视觉与文案矛盾。
**严重度**:中(评估卡片核心展示,矛盾误导用户)。
**建议**:统一三档 `> 0 / < 0 / === 0`(中性),文案 + class 同源判定。
### 🟡 FR-C3 — Settings.vue 三个 timer 只清一个 [已核实]
**位置**`src/views/Settings.vue:749-751`
**问题**:组件有 `_concurrencyTimer`660`_knowledgeSaveTimer`717`_toastTimer`390三个 setTimeout`onUnmounted``clearTimeout(_concurrencyTimer)`。另两个泄漏unmount 后若 fire 会写已销毁 reactive`toast`/`knowledgeConfig`)。
**严重度**3000ms/300ms 短 timer实际触发概率低但属明确的资源管理缺口
**建议**`onUnmounted` 清全部三个 timer。
### 🟡 FR-C4 — `MIGRATION_VERSION` 死常量与逻辑错位 [已核实]
**位置**`crates/df-storage/src/migrations.rs:7`
**问题**`const MIGRATION_VERSION: i32 = 12`,但 `run()` 的 if 链已调到 `migrate_v13`(实际版本 13。常量全仓未被引用dead constant却留在文件头作"当前迁移版本"误导。新加 v14 迁移时若有人据此常量判断,会踩坑。
**严重度**:低(当前无功能影响,纯误导 + 易踩坑)。
**建议**:删除未引用常量;或改成数组驱动的迁移注册(`&[(version, migrate_fn)]` 循环),让编译器/结构保证版本号与执行链同步。
### 🟡 FR-C5 — `AiConversationDetail` 缺 `readonly` 字段 [已核实]
**位置**`src/api/types.ts:242-246`缺字段vs `src-tauri/src/commands/ai/commands.rs:450-455`(后端生成中返回 `readonly:true`
**问题**:后端 `ai_conversation_switch``session.generating` 时返回 `readonly: true`前端类型定义未声明。store 层读 `detail.readonly` 会 TS 报错或被忽略,生成中切换对话时前端无法感知只读态。
**严重度**TS 类型缺口,影响开发期类型安全 + 一个未利用的只读信号)。
**建议**`AiConversationDetail``readonly?: boolean`
---
## 4. 并发 / 竞态(前端高频路径)
### 🟠 FR-R1 — `switchConversation` 无切换 token竞态致历史错配 [仅代理报告待复核]
**位置**`src/composables/ai/useAiConversations.ts:44-121`
**问题**`switchConversation` 含多个 await`aiApi.switchConversation``aiApi.pendingToolCalls`)。快速连点 A→B若 B 网络更快先返回,`state.activeConversationId=B` 但 A 后返回又把 `state.messages` 覆盖成 A 的历史——最终 activeId=B、messages=A视图与激活会话不一致。B 的流式 delta 也可能被 `isCurrent` 判定错配,串话到 A 视图。
**严重度**:中(用户高频操作路径,错配难复现但体验崩坏)。
**建议**:加切换 token`latestSwitchId`await 后比对,过期响应丢弃;或用 `generatingConvId` 而非 `activeConversationId` 路由流式事件。
### 🟠 FR-R2 — 全局单看门狗 timer 多窗口互踩 [仅代理报告待复核]
**位置**`src/composables/ai/useAiStream.ts:18-44` + `useAiWindow.ts`(共享 state 单例)
**问题**`_streamWatchdog` 模块级单 timer。分离窗口模式主/分离窗口共享同一 `state`,两侧 `handleEvent``resetStreamWatchdog()`。主窗口进入审批等待clear timer分离窗口的一个 delta 又 `resetStreamWatchdog()` 重启,审批态被误触发 130s 超时强制收尾。
**严重度**:中(隐蔽难复现,审批态被误中断)。
**建议**:按 `conversation_id` 维护 `Map<convId, timer>`,或分离窗口模式下由一方独占看门狗。
### 🟡 FR-R3 — `liveEvents` 数组无限增长 [仅代理报告待复核]
**位置**`src/stores/project.ts:213`
**问题**`state.liveEvents.push(payload)` 从不裁剪。长时间运行的工作流让数组无限膨胀,每次 push 触发整个数组的响应式依赖重算(含 `formattedEvents` computed 全量 map
**严重度**:低(需超长工作流才显现,但内存+响应式开销随时间线性增长)。
**建议**:加上限(如 `length > 500``shift` 最旧)或改环形缓冲。
### 🟡 FR-R4 — LLM `complete()` 同步路径无超时无重试 [仅代理报告待复核] ✅ 已完成commit 36d68dd
**位置**`crates/df-ai/src/openai_compat.rs:230-243` + `anthropic_compat.rs:226-232`
**问题**`Client::builder()` 只设 `connect_timeout(30s)` 刻意不设总 timeout。`complete()`(同步调用,非流式)路径无整体超时、无重试、无取消令牌。远端建连成功但响应慢(网络静默、限流排队)时 `send().await` 无限挂起,卡死调用方任务。流式路径注释说由上层 idle timeout 兜底,但 complete() 无此兜底。
**严重度**:中(一次网络抖动挂死一轮对话)。
**建议**`complete()``tokio::time::timeout` 包整体调用;加重试(指数退避 2-3 次)。
> **已完成**`complete()` 路径用 `RequestBuilder::timeout(Duration::from_secs(60))` 设 60s 单请求超时;不影响 `stream` 流式路径(仍靠上层 idle timeout 兜底。重试未加当前需求仅防挂死。commit 36d68dd。
### 🟡 FR-R5 — `findToolCall` / `flatMap` 全量线性扫描 [仅代理报告待复核] ✅ 已完成commit 4b5f096
**位置**`src/composables/ai/useAiEvents.ts:63-71` + `useAiSend.ts:82-84`
**问题**:每个 `AiToolCallCompleted`/`AiApprovalRequired`/`AiApprovalResult`/`approveToolCall` 都遍历全部 messages 再遍历每条 toolCalls。长对话数百消息高频工具调用 O(n²)。
**严重度**:低(性能,长对话显现)。
**建议**:维护 `Map<toolCallId, AiToolCallInfo>` 索引,`AiToolCallStarted` 登记,事件直接查 Map。
> **已完成**`useAiEvents::findToolCall` 改为反向遍历(命中最近一条同 id 工具调用)。**放弃 Map 索引方案**——审批替换等场景需命中最新 toolCallMap 索引易留陈旧引用。commit 4b5f096。
---
## 5. 性能(热路径)
### 🟡 FR-P1 — `dag.rs` 拓扑排序 O(V·E) + 入度 unwrap panic 风险 [仅代理报告待复核] ✅ 已完成commit 4b5f096 / commit 内编号 FR-D1
**位置**`crates/df-workflow/src/dag.rs:65,74,114`
**问题**`successors`/`predecessors``self.edges.iter().filter(...)` 全表扫描。`topological_layers` 对每个出队节点调 `successors`,整体 O(V·E)`executor.rs:81` 又对每个节点调 `predecessors`,再叠加 O(V·E)。DAG 变大(几十节点上百边)明显变慢。另 `dag.rs:115` `in_degree.get_mut(&succ).unwrap()`,若 edges 含 target 不在 nodes 的野节点successors 不过滤succ 不在 in_degree 表 → unwrap panic。
**严重度**:低(当前 DAG 规模小无感)。
**建议**:预建 `adjacency_out`/`adjacency_in: HashMap<NodeId, Vec<NodeId>>``add_edge*` 维护,拓扑降 O(V+E)`successors` 过滤野节点或 unwrap 改 `if let Some`
> **已完成**`dag.rs::topological_layers` 一次性遍历边建 `adjacency_out`(出边表)+ 入度表BFS 分层走索引;`executor.rs::run` 预建 `adjacency_in`入边索引取前驱。O(V·E) → O(V+E)14 测试全绿(含 executor 3 测。commit 4b5f096该 commit 内编号为 FR-D1
### 🟡 FR-P2 — `search_vector` 全表扫 + SELECT * [仅代理报告待复核] ✅ 已完成commit 4a95f6a / commit 内编号 FR-D2
**位置**`crates/df-storage/src/crud.rs:1176-1209`
**问题**:每次向量检索 `SELECT * FROM knowledges WHERE status='published' AND embedding IS NOT NULL` 拉全部已发布记录的完整 BLOB含 content/title 大文本Rust 端算余弦。仅用了 embedding 却拉了全部字段。注释自评"<50k <50ms",但无 LIMIT 上限,行数无界增长。
**严重度**:低(知识检索热路径,当前数据量可接受)。
**建议**SELECT 只取 id+embedding算完 top-N 的 id 再回表取详情;长期换 sqlite-vec注释已预告
> **已完成**`SELECT *` 改为显式 14 列id, kind, title, content, tags, status, confidence, reuse_count, verified, source_project, source_ref, reasoning, created_at, updated_at`embedding` 单独另取。消除隐式依赖字段级精简待单独立项。commit 4a95f6a该 commit 内编号为 FR-D2
### 🟡 FR-P3 — 全局单 SQLite 连接串行瓶颈 [仅代理报告待复核]
**位置**`crates/df-storage/src/db.rs:13-60` + `crud.rs` 宏内 `spawn_blocking`+`blocking_lock`
**问题**:整个数据库一条 `Connection`,所有读写串行抢 `tokio::sync::Mutex`。每条 CRUD`list_all`/`search`)都 spawn_blocking + blocking_lock。全局热点锁并发查询互相阻塞。WAL 已开但读路径未享并发红利。代码注释已有 r2d2 TODO 未做。
**严重度**:低(单用户桌面应用,并发量小,当前无感)。
**建议**r2d2/deadpool-sqlite 连接池(写单连接+Mutex读多连接并发最低限度换 `std::sync::Mutex`spawn_blocking 同步上下文内开销更低,不跨 await
### 🟡 FR-P4 — `human_node` 500ms 忙轮询取消状态 [已核实]
**位置**`crates/df-nodes/src/human_node.rs:54`
**问题**`cancel_tick = interval(500ms)`select! 内每 500ms 检查 `is_cancelled`。最长 3600s 阻塞节点 = 7200 次无谓锁竞争StateMachine HashMap 锁)。魔法数字无注释。
**严重度**:低(开销小但模式不佳)。
**建议**:取消走独立 `tokio::sync::Notify`/oneshotHumanNode `notify.notified().await` 零轮询。注todo.md:142 已记"停止生成 idle 用 Notify 替代轮询",此为同类不同位置(审批节点 vs stopChat
### 🟡 FR-P5 — `build_system_prompt` 每次发消息全表扫项目 [仅代理报告待复核]
**位置**`src-tauri/src/commands/ai/prompt.rs:70-86`
**问题**`ai_chat_send` 每次调 `build_system_prompt``state.projects.list_active()``take(20)` 拼 format! 字符串。热路径每对话重扫全表+重拼。
**严重度**项目表小SQL 本身快format! 重复分配是主要开销)。
**建议**:项目列表变动时刷新缓存(已有 path 绑定事件可触发),或注入前比对 updated_at 决定是否重算。
### 🟡 FR-P6 — `AiChat.vue` watch messages 用 JSON.stringify 全量快照 [仅代理报告待复核]
**位置**`src/components/AiChat.vue:670-678`
**问题**`watch(() => store.state.messages, ..., { deep: true })` 内对整个 messages 做 `JSON.stringify(msgs.map(...))`。流式生成期间 currentText 每个 delta 触发 messages 对象变更deep watch 每秒数十次全量序列化。长对话主线程阻塞。
**严重度**:低(与 aichat-review AR-1 流式 Markdown 全量重解析叠加放大)。
**建议**watch 具体派生信号(如 toolCalls 的 status 集合)而非整个 messages deep 快照。
---
## 6. 可读性 / DRY重构项
### FR-D1 — confirmDialog 逻辑 4 处重复 [已核实] ✅ 已完成commit 4a95f6a / commit 内编号 FR-D5
`Ideas.vue:217` / `Projects.vue:137` / `ProjectDetail.vue:226` / `Settings.vue:403` 各一份 `confirmState + confirmDialog + answerConfirm`,代码几乎逐字相同。抽 `useConfirmDialog()` composable 配合现有 `ConfirmDialog.vue`
> **已完成**:抽 `src/composables/useConfirm.ts`Projects/ProjectDetail/Ideas/Settings 四视图改用 composable各削减约 14 行重复commit 4a95f6a该 commit 内编号为 FR-D5
### FR-D2 — `parseTags`/`parseScores`/`parseStack` JSON 解析散落 [已核实]
同类"JSON 字符串→数组"解析在 Ideas/Knowledge/Projects/ProjectDetail 多处独立实现。收敛到 `utils/json.ts``parseJsonArray`/`parseJsonObject`
### FR-D3 — `set_waiting`/`set_skipped`/`emit_human_approval_request` 死代码 [仅代理报告待复核]
`df-workflow/state.rs:87-111`set_waiting/set_skipped 无调用方)+ `eventbus.rs:44`emit_human_approval_request 无调用方)。`set_cancelled` 绕过状态机转换校验直接 insert。建议清理死代码 + `set_cancelled` 纳入合法转换白名单。
### FR-D4 — migrations 13 个手写 if 链 DRY 反例 [已核实]
`migrations.rs:38-90` 13 个 `if current_version < N { migrate_vN()? }` 手写重复,新加迁移易漏注册且编译器不报。改数组驱动 `&[(version, fn)]` 循环(与 FR-C4 同根,一并修)。
### FR-D5 — 状态机后门 + 死代码清理集合 [仅代理报告待复核]
合并 FR-D3 + `state.rs``set_*` 系列审计:保留 `set_cancelled`cancel IPC 必需)但纳入 `is_legal` 转换图;删除 `set_waiting`/`set_skipped` 未用方法。
---
## 7. 核对通过的项(无问题)
- **R6 await 缺失是孤立 bug**:其余 10 处 send/emit/channel 操作全部正确 await已逐条核查见 workflow-approval-review 结论)。
- **执行链完整**`run_workflow → spawn → DagExecutor::run → join_all(node.execute) → HumanNode select! → approve IPC → Response → HumanNode 返回 → executor 继续`无断裂cancel 通路 `cancel_workflow_node → set_cancelled → HumanNode 500ms 轮询 is_cancelled → Err` 也完整。
- **SQL 参数化扎实**`query`/`update_field``validate_column_name` 白名单 + `params![]` 参数绑定,无 value 拼接注入FR-S6 是潜在面非现行漏洞)。
- **tool_registry 无运行时注册入口**`AiToolRegistry` 只在 `build_ai_tool_registry` 编译期注册,前端无法注入恶意工具。
- **v-html 安全**`AiChat.vue:200` `renderMd` 经 DOMPurify sanitize`ToolCard.vue` 多处 v-html 均为常量 SVG 非用户输入。
- **事务边界**`purge_with_descendants` 子表先删父表后删单事务 commit迁移脚本幂等PRAGMA 探测 + IF NOT EXISTS
---
## 8. 建议追加到 todo.md 的项
按严重度给 todo ID待用户确认是否进看板
| 建议 ID | 对应 | 优先级 |
|---|---|---|
| FR-S1api_key 加密) | 本报告 §2 | P0 |
| FR-S2read_file TOCTOU + write_file 大小限制) | 本报告 §2 | P0 |
| FR-S3approve decision 白名单) | 本报告 §2 | P0 |
| FR-R1switchConversation 切换 token | 本报告 §4 | P1 |
| FR-R2看门狗多窗口互踩 | 本报告 §4 | P1 |
| FR-C1formattedEvents 时间漂移) | 本报告 §3 | P1 |
| FR-C2net_sentiment=0 矛盾) | 本报告 §3 | P1 |
| FR-S4/S5SKILL.md 注入 / 跨对话审批) | 本报告 §2 | P2 |
| FR-R4complete() 无超时) | 本报告 §4 | P2 |
| FR-C3/C4/C5timer 清理 / 迁移常量 / readonly 类型) | 本报告 §3 | P2 |
| FR-P1~P6 / FR-D1~D5性能 + 重构) | 本报告 §5-6 | P2/长期 |
**优先修 3 件**(安全 + 明显体验):
1. **FR-S1** api_key 加密(凭据泄露面最大)
2. **FR-S3** approve decision 白名单(一行校验,防 HumanNode 卡死)
3. **FR-C1** formattedEvents 时间漂移(明显体验 bug改动小
> 本报告仅审查 + 文档,不含代码修改。实际修复另起会话。