- 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/ 噪音排除
25 KiB
全栈代码审查报告(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(AI 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:112promote_idea真正调用df_project::manager::ProjectManager::create_from_idea构造项目实体 + 映射ProjectRecord+update_full回写想法。前端走此 IPC,不走 crate。 df-ideas/src/promotion.rs的do_promote(Auto 策略分支)无任何调用方,是 功能决策记录 §想法立项 明确的"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-D1(confirmDialog 重复)即 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-05,Sprint 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 只存引用 id;list 命令返回 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_persist50KB 兜底持久化,但磁盘写入无防护)。
注: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() 或用后端事件自带的 timestamp),computed 只读不改时间。
🟠 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 索引方案——审批替换等场景需命中最新 toolCall,Map 索引易留陈旧引用。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/oneshot,HumanNode 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:200renderMd经 DOMPurify sanitize;ToolCard.vue多处 v-html 均为常量 SVG 非用户输入。 - 事务边界:
purge_with_descendants子表先删父表后删单事务 commit;迁移脚本幂等(PRAGMA 探测 + IF NOT EXISTS)。
8. 建议追加到 todo.md 的项
按严重度给 todo ID(待用户确认是否进看板):
| 建议 ID | 对应 | 优先级 |
|---|---|---|
| FR-S1(api_key 加密) | 本报告 §2 | P0 |
| FR-S2(read_file TOCTOU + write_file 大小限制) | 本报告 §2 | P0 |
| FR-S3(approve decision 白名单) | 本报告 §2 | P0 |
| FR-R1(switchConversation 切换 token) | 本报告 §4 | P1 |
| FR-R2(看门狗多窗口互踩) | 本报告 §4 | P1 |
| FR-C1(formattedEvents 时间漂移) | 本报告 §3 | P1 |
| FR-C2(net_sentiment=0 矛盾) | 本报告 §3 | P1 |
| FR-S4/S5(SKILL.md 注入 / 跨对话审批) | 本报告 §2 | P2 |
| FR-R4(complete() 无超时) | 本报告 §4 | P2 |
| FR-C3/C4/C5(timer 清理 / 迁移常量 / readonly 类型) | 本报告 §3 | P2 |
| FR-P1 |
本报告 §5-6 | P2/长期 |
优先修 3 件(安全 + 明显体验):
- FR-S1 api_key 加密(凭据泄露面最大)
- FR-S3 approve decision 白名单(一行校验,防 HumanNode 卡死)
- FR-C1 formattedEvents 时间漂移(明显体验 bug,改动小)
本报告仅审查 + 文档,不含代码修改。实际修复另起会话。