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

25 KiB
Raw Blame History

全栈代码审查报告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.mdAI Chat 专项)、工作流审批审查报告-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.rsdo_promoteAuto 策略分支)无任何调用方,是 功能决策记录 §想法立项 明确的"crate 留纯决策 TODO"——crate 不依赖 df-storage/df-project 以避循环依赖,副作用放 IPC 组合。
  • todo.md:145 已标注 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-457ai_providers.api_key TEXT NOT NULL 明文列)
  • crates/df-storage/src/models.rs:138api_key: String 裸字段)
  • src-tauri/src/commands/ai/commands.rs:299-318ai_save_provider 直接 update_full 落盘)
  • src/views/Settings.vue:53listProviders 返回完整 key前端 maskKey 仅展示层)

问题三个环节都明文——DB 文件 app_data_dir/devflow.db 明文落盘全部 provider 密钥list 命令经 IPC 把明文 key 传到前端 webview前端仅做视觉 mask。任何能读 DB 文件的进程(备份、同步盘、其他本机进程)直接拿全部密钥。

严重度:高(桌面应用典型安全缺口,密钥=成本+越权)。

建议OS 级凭据存储Windows Credential Manager / macOS Keychain / Linux Secret Servicekeyring crate 包一层DB 只存引用 idlist 命令返回 mask 后的占位,仅编辑保存时按需取明文。

🔴 FR-S2 — read_file TOCTOU + write_file 无大小限制 [已核实]

位置src-tauri/src/commands/ai/tool_registry.rs:364-417

问题

  • read_file:364-380tokio::fs::metadata(path) 检查大小 ≤1MBtokio::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_filecontent.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_idsession.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 对未登记表返回 Nonevalidate_column_name 返回 Ok(()) 放行。当前 7 个 Repo 内部安全(表名是宏编译期常量),但 is_allowed_columnpub 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(模板)+ :368sentimentClass

问题:模板 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

问题:组件有 _concurrencyTimer660_knowledgeSaveTimer717_toastTimer390三个 setTimeoutonUnmountedclearTimeout(_concurrencyTimer)。另两个泄漏unmount 后若 fire 会写已销毁 reactivetoast/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 — AiConversationDetailreadonly 字段 [已核实]

位置src/api/types.ts:242-246缺字段vs src-tauri/src/commands/ai/commands.rs:450-455(后端生成中返回 readonly:true

问题:后端 ai_conversation_switchsession.generating 时返回 readonly: true前端类型定义未声明。store 层读 detail.readonly 会 TS 报错或被忽略,生成中切换对话时前端无法感知只读态。

严重度TS 类型缺口,影响开发期类型安全 + 一个未利用的只读信号)。

建议AiConversationDetailreadonly?: boolean


4. 并发 / 竞态(前端高频路径)

🟠 FR-R1 — switchConversation 无切换 token竞态致历史错配 [仅代理报告待复核]

位置src/composables/ai/useAiConversations.ts:44-121

问题switchConversation 含多个 awaitaiApi.switchConversationaiApi.pendingToolCalls)。快速连点 A→B若 B 网络更快先返回,state.activeConversationId=B 但 A 后返回又把 state.messages 覆盖成 A 的历史——最终 activeId=B、messages=A视图与激活会话不一致。B 的流式 delta 也可能被 isCurrent 判定错配,串话到 A 视图。

严重度:中(用户高频操作路径,错配难复现但体验崩坏)。

建议:加切换 tokenlatestSwitchIdawait 后比对,过期响应丢弃;或用 generatingConvId 而非 activeConversationId 路由流式事件。

🟠 FR-R2 — 全局单看门狗 timer 多窗口互踩 [仅代理报告待复核]

位置src/composables/ai/useAiStream.ts:18-44 + useAiWindow.ts(共享 state 单例)

问题_streamWatchdog 模块级单 timer。分离窗口模式主/分离窗口共享同一 state,两侧 handleEventresetStreamWatchdog()。主窗口进入审批等待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 > 500shift 最旧)或改环形缓冲。

🟡 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/predecessorsself.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_atembedding 单独另取。消除隐式依赖字段级精简待单独立项。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。每条 CRUDlist_all/search)都 spawn_blocking + blocking_lock。全局热点锁并发查询互相阻塞。WAL 已开但读路径未享并发红利。代码注释已有 r2d2 TODO 未做。

严重度:低(单用户桌面应用,并发量小,当前无感)。

建议r2d2/deadpool-sqlite 连接池(写单连接+Mutex读多连接并发最低限度换 std::sync::Mutexspawn_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_promptstate.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.tsProjects/ProjectDetail/Ideas/Settings 四视图改用 composable各削减约 14 行重复commit 4a95f6a该 commit 内编号为 FR-D5

FR-D2 — parseTags/parseScores/parseStack JSON 解析散落 [已核实]

同类"JSON 字符串→数组"解析在 Ideas/Knowledge/Projects/ProjectDetail 多处独立实现。收敛到 utils/json.tsparseJsonArray/parseJsonObject

FR-D3 — set_waiting/set_skipped/emit_human_approval_request 死代码 [仅代理报告待复核]

df-workflow/state.rs:87-111set_waiting/set_skipped 无调用方)+ eventbus.rs:44emit_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.rsset_* 系列审计:保留 set_cancelledcancel 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_fieldvalidate_column_name 白名单 + params![] 参数绑定,无 value 拼接注入FR-S6 是潜在面非现行漏洞)。
  • tool_registry 无运行时注册入口AiToolRegistry 只在 build_ai_tool_registry 编译期注册,前端无法注入恶意工具。
  • v-html 安全AiChat.vue:200 renderMd 经 DOMPurify sanitizeToolCard.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-P1P6 / FR-D1D5性能 + 重构) 本报告 §5-6 P2/长期

优先修 3 件(安全 + 明显体验):

  1. FR-S1 api_key 加密(凭据泄露面最大)
  2. FR-S3 approve decision 白名单(一行校验,防 HumanNode 卡死)
  3. FR-C1 formattedEvents 时间漂移(明显体验 bug改动小

本报告仅审查 + 文档,不含代码修改。实际修复另起会话。