- 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/ 噪音排除
360 lines
25 KiB
Markdown
360 lines
25 KiB
Markdown
# 全栈代码审查报告(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-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_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()` 或用后端事件自带的 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:200` `renderMd` 经 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~P6 / FR-D1~D5(性能 + 重构) | 本报告 §5-6 | P2/长期 |
|
||
|
||
**优先修 3 件**(安全 + 明显体验):
|
||
1. **FR-S1** api_key 加密(凭据泄露面最大)
|
||
2. **FR-S3** approve decision 白名单(一行校验,防 HumanNode 卡死)
|
||
3. **FR-C1** formattedEvents 时间漂移(明显体验 bug,改动小)
|
||
|
||
> 本报告仅审查 + 文档,不含代码修改。实际修复另起会话。
|