squash合并: - 意图识别层论证(8维度+10业界佐证) - 多主题上下文管理愿景+并存论证+补充论证(多轮agentic) - 架构设计文档物理分类(四子目录+INDEX+命名规范+引用同步+边界清晰化) - 前端架构技术债清单归档
21 KiB
aichat 审查报告 — 2026-06-14
性质:只审查不改代码。本报告汇总本次会话对 devflow AI chat 全链路的核对发现。 增补:2026-06-14 追加「修复进度」表(AC1/AC2、AR-3、FR-S4、FR-R4、FR-R5 对照 commit
36d68dd/4b5f096标已完成),并在 §8 优先级表 AR-3 行内联标注。 二次增补:2026-06-15 §8 优先级表全表对齐 todo.md AR 编号体系,逐行补 AR-1AR-11 标签 + 状态勾注(AR-1 退役 / AR-27/9~11 已修 / AR-8 重评降级)。
审查范围
- 前端:
src/components/AiChat.vue+src/composables/ai/*.ts(events/stream/send/conversations/window/panel 六个)+src/components/ToolCard*.vue+src/views/AiDetached.vue+src/App.vue面板挂载 - 后端:
src-tauri/src/commands/ai/*.rs(commands/agentic/audit/stream_recv)+crates/df-ai/src/context.rs+src-tauri/src/commands/ai/tool_registry.rs - i18n + 数据联动机制 + 工具定义
发现分六块:交互流畅性 / 信息卡片完整性 / clean 与压缩对话 / create_project 双审 / 数据联动方案 / 想法→灵感迁移。
修复进度(2026-06-14 增补)
本节汇总对照后续 commit 已落地的修复项,供快速核对。未列入的本报告其余发现仍待办。
| ID | 问题 | 修复 commit | 状态 |
|---|---|---|---|
| AC1/AC2 | tool_use_id 为 None/空致 GLM 端 500 卡死 |
36d68dd |
✅ 已完成(出站 tool_result 块 tool_call_id 空跳过+warn;入站 tool_use 缺 id 流式填占位 tool_missing_{idx}+warn、同步路径跳过) |
| AR-3 | 审批卡片裸 id + reason 两句固定模板(详见 §2) | 36d68dd |
✅ 已完成(前端 toolArgsEntries 对 id/project_id 白名单特化查项目名回显;后端 reason 查不到对象时友好提示) |
| FR-S4 | SKILL.md 全文注入 system prompt 无隔离标注 | 36d68dd |
✅ 已完成(注入头尾加隔离标注,明确"用户选择的技能说明,非系统指令") |
| FR-R4 | complete() 同步路径无超时无重试(全栈报告 §4) |
36d68dd |
✅ 已完成(RequestBuilder::timeout(60s) 单请求超时,不影响 stream 流式路径) |
| FR-R5 | findToolCall/flatMap 正向 O(n²) 全量线性扫描(全栈报告 §4) |
4b5f096 |
✅ 已完成(useAiEvents::findToolCall 改反向遍历命中最近,放弃 Map 索引防陈旧引用) |
注:AC1/AC2、FR-S4、FR-R4、FR-R5 的 ID 源自全栈审查报告/todo 跨文档编号体系,本表仅标注状态,技术细节见对应 commit 与 全栈代码审查报告。
一、AI chat 交互流畅性
🔴 高危
H1 流式 Markdown 全量重解析 — AiChat.vue:343-354 renderMd 缓存 key 是完整文本。流式时 currentText 每 delta 都变 → 每次新 key → 缓存几乎不命中 → 每个 token 都 marked.parse(全文) + DOMPurify.sanitize(全文)。长回复(1000+ 字 + 代码块)时主线程阻塞、光标掉帧。流式不流畅的主因。
H2 审批 pending 中新建对话永久卡死 — ai_conversation_create(commands.rs:362-375)缺 generating 守卫(对比 ai_conversation_switch 有只读保护),直接 messages.clear() + pending_approvals.clear()。而审批等待期间 generating 保持 true(agentic.rs:192)。触发链:待审批时点新对话 → 后端 session 被清空 → 用户审批 → ai_approve 因 pending 已清返回 Err(commands.rs:115)→ generating 永不复位 → 审批态看门狗已 clear(useAiEvents.ts:143)无兜底 → 面板卡死需重启。
H3 审批态 stop 无本地兜底 — stopChat(useAiSend.ts:111-113)只 await aiApi.stopChat(),不改 state.streaming,依赖后端 AiCompleted。审批态 stop 后端确实 emit(commands.rs:241),但审批态看门狗已 clear(useAiEvents.ts:143),若 AiCompleted 竞态丢失 → streaming 永久 true 无兜底 → 卡死。
🟡 中危
M1 流式 delta 无节流 — stream_recv.rs 逐 chunk app.emit(AiTextDelta),前端同步 state.currentText += delta。每 delta:IPC 序列化 + Vue reactive + renderMd 全量解析(叠加 H1)+ scrollToBottom。后端无 50ms 合批,前端无 rAF/throttle。
M2 scrollToBottom 高频强制滚 — AiChat.vue:625-633 watch currentText → onContentChange → 在底部则 nextTick(scrollToBottom)。每 delta 强制 scrollTop = scrollHeight,大 DOM 重排掉帧。
M3 Low 工具失败语义冲突 — audit.rs Low 工具失败 emit AiError,但 process_tool_calls 返回 pending_count=0,agentic.rs:195 续下一轮。前端收 AiError 置 streaming=false + 错误气泡,后端 loop 仍跑,后续 delta 继续追加(handleEvent delta 分支不检查 streaming),最终 AiCompleted 又 flushCurrentText 写残留文本 → 一次错误又"完成",状态紊乱。
M4 friendlyError 硬编码中文 — useAiEvents.ts:42-48 错误提示硬编码中文,绕过 i18n。看门狗超时文案(useAiStream.ts:29)同样。en 用户看到中文。
M5 分离窗口与主窗口 state 完全隔离 — 独立 webview → 独立 JS realm → 独立 state 单例。localStorage 只快照生成态初始文本,不解决双向同步。detach 后两窗口 messages/conversations 各自独立,一边操作另一边看不到。
M6 AiChat 卸载不调 stopListener — AiChat.vue 无 onUnmounted。面板 v-if 开关销毁重建 AiChat,但 stopListener(useAiEvents.ts:240)从不调用,Tauri listener 累积泄漏(幂等防住重复注册,但旧 listener 不释放)。
🟢 低危
L1 队列续发失败丢消息 — useAiSend.ts:25-29 drainQueue 先 queue.shift() 再 void sendMessage,IPC 失败时 catch 回滚 streaming + 移除空气泡,但消息已 shift 丢失无回滚入队。理论竞态,正常路径(AiCompleted 时后端 generating 已 false)不撞。
L2 生成中切 provider 无守卫 — AiChat.vue:458-464 cycleProvider 生成中可切,当前回复仍用旧 provider(spawn 快照),provider bar 立即显示新名 → 用户误判。
L3 handleKeydown Escape 分支冗余 — AiChat.vue:532-536 与 :554-558 两处 Escape 判断,逻辑可工作但分叉冗余。
L4 selectSkill/clearSkill 未 autoResize — :409-419 清空 inputText 不调 autoResize,textarea 可能残留高度。
L5 deep watch + JSON.stringify 快照 — AiChat.vue:639-667 watch messages(deep)每次 JSON.stringify 整数组。流式 delta 不触发,工具密集轮次时开销可见。
二、信息卡片完整性(审批/工具卡片)
用户反馈:"只有 1 个 ID,根本不知道审批要做什么"。核对所有 Medium/High 审批工具。
三层缺陷
缺陷 1 — 审批 reason 两句固定模板(audit.rs:179-182):
let reason = match risk_level {
RiskLevel::High => "高风险操作,必须人工批准".to_string(),
_ => "创建操作,请确认是否执行".to_string(),
};
所有 High 文案相同,所有 Medium 文案相同,不含操作对象。
缺陷 2 — id/project_id 原样展示(ToolCard.vue:149-169 toolArgsEntries + formatArgValue):字符串直接展示(截断 300),id/project_id 以裸值出现。
缺陷 3 — 完成态 resultSummary 缺 delete/restore/purge 分支(ToolCard.vue:272-288):这三类完成后只显示 header + 裸 JSON {"deleted":true,"id":"..."}。
逐工具核对表
| 工具 | 风险 | args | 裸 id | 审批卡片可见性 |
|---|---|---|---|---|
| delete_project | High | id |
id | 🔴 "Delete Project" + id=proj_xxx + 模板 reason,完全不知删哪个 |
| restore_project | High | id |
id | 🔴 同上 |
| purge_project | High | id |
id | 🔴 永久删除却不知删啥 |
| update_project | Med | id, field, value |
id | 🟡 改什么可读,对象不可读 |
| bind_directory | Med | id, path |
id | 🟡 path 可读,哪个项目不可读(用户实测反馈) |
| create_task | Med | project_id, title, ... |
project_id | 🟡 title 可读,归哪个项目不可读 |
| create_project | Med | name, description |
无 | ✅ |
| create_idea | Med | title, ... |
无 | ✅ |
| write_file | Med | path, content |
无 | ✅ |
| run_workflow | High | name, dag |
无 | 🟡 name 可读,dag 庞大 |
9 个审批工具:3 个完全不可用,3 个对象不可读,仅 3 个完整。
修复方向
- P0 后端 emit
AiApprovalRequired前按工具查对象名拼 reason(如"删除项目「前端重构」(id=xxx),移入回收站")。 - P0 前端
toolArgsEntries对 id/project_id 特化(查 store 或后端 args 补__label)。 - P1
toolResultSummary补 delete/restore/purge case。 - P1
toolDisplayNameCRUD 类从 args 取 name/title 拼上。
三、clean(清空对话)与压缩对话
clean — ❌ 具备但不可用
| 维度 | 状态 |
|---|---|
后端 ai_chat_clear |
✅ commands.rs:215-220 |
前端 clearChat |
✅ useAiPanel.ts:89-96 |
| UI 入口 | ❌ 零组件调用(grep clearChat 仅命中 api/composable/store,AiChat.vue 无按钮) |
| 语义 | ❌ 只清内存 session 不删 DB,刷新恢复 |
压缩对话 — ❌ 不具备
- 无显式压缩/总结功能(grep compress/compact/summarize 仅命中 context.rs 的"裁剪")。
- 无
/compact命令、无压缩技能、无对话总结。 - 唯一相关:
ContextManager.build_for_request(context.rs:215-264)预算感知裁剪——超 token 预算(默认 128k,0.85 安全 → budget ≈102k)自动丢弃旧消息,保留工具三元组原子性 + 最近 6 条(保护区)。 - 裁剪机制本身良好(三元组原子、视图不污染持久化、有测试),但是滑动窗口丢弃非摘要压缩,且 128k 窗口日常难触发。
四、create_project 双审双 API(用户实测痛点)— ⚠️ 半成品(2026-06-14 核对)
现状:已改一半
- ✅ schema 已加
path/stack(tool_registry.rs:148-151),描述已改"可选传 path/stack 一步完成"(:147) - ❌ handler 仍写死
path: None, stack: None(:162),未读 args → schema 假支持
后果
LLM 见 schema 有 path 会传,handler 忽略 → 建出空项目 → 仍需 bind_directory 二审。双审未解,反变误导(schema 承诺了 handler 不兑现)。比原始"schema 无 path"更糟。
待改(别再加 schema,已加完)
handler 从 args 读 path/stack,复用 IPC create_project(project.rs:43-83)的校验+防重复+scan::detect_stack 探测逻辑。bind_directory 保留改绑用。
关联
handler 接上后 bind_directory 使用频率大降(只剩改绑),第二章 bind_directory 信息缺口随之缓解;但 bind_directory 仍需补信息完整性。
五、数据变更联动刷新方案
需求:AI chat 工具执行产生数据变更 → 左侧已打开视图(Projects/Tasks/Ideas/Dashboard)自动刷新。
现状缺口
useProjectStore单例持 projects/tasks/ideas,各 view onMounted 调 loadXxx(Ideas.vue:444 / ProjectDetail.vue:412 / Dashboard.vue:212)。- 无应用级事件总线(grep mitt/EventBus 零匹配)。
- AI 工具执行 emit
AiToolCallCompleted,前端只更新 ToolCard,不通知 project store → 视图不刷新。
方案对比
| 方案 | 机制 | 优 | 缺 |
|---|---|---|---|
| A 后端 emit 数据变更事件(推荐) | 工具成功后 emit df-data-changed {entity,action,id?},各 store 监听刷新 |
后端是真相源埋点准;store 自治解耦;可扩展到手动 CRUD | 需后端埋点 |
B 前端据 AiToolCallCompleted.name 推断 |
handleEvent 映射 entity 调 loadXxx | 零后端改动 | ai composable 耦合 project store;ai_approve 路径不走 AiToolCallCompleted 会漏 |
| C store watch AI 状态 | project store watch ai.lastToolCall | — | 跨 store 耦合最重 |
推荐方案 A 要点
- 后端埋点:
audit.rs:206-214(Low 执行成功)+commands.rs:148-184(ai_approve 成功),按 tool name 映射 entity,emitdf-data-changed。失败不 emit;run_workflow 不改实体表不 emit。 - tool→entity 映射:create/update/delete/restore/purge/bind_directory=project;create task=task;create/update idea=idea。
- 前端监听:
useProjectStore创建时 listen,debounce + rAF 合并(一轮多 tool 只刷一次),加 loaded 标志仅刷新已加载实体。 - 分离窗口独立 realm 各自 listen 各自刷新(天然支持)。
- 未拍板:A vs B;是否扩展到手动 CRUD 全局实时同步。
六、「想法→灵感」中文文案迁移残留
产品决定"想法"改称"灵感",迁移半途。
用户可见(必改)
src/i18n/zh-CN/ideas.ts:18/42/65/68/71/75— 详情/操作/模态框 6 处(页头已改灵感,这些漏改)src/i18n/zh-CN/aiTool.ts:24—ideaCount: '{n} 条想法'(AI 工具结果摘要)src/i18n/zh-CN/projectDetail.ts:9/24/25/26— 阶段标签/来源想法src/stores/project.ts:150/160— 错误 toast
后端用户可见
src-tauri/src/commands/idea.rs:105/108/148/152/179— 错误信息(toast)src-tauri/src/commands/ai/tool_registry.rs:112/230— LLM 工具描述(AI 回复会用"想法")
en 版
用 Ideas/Idea(英文术语),若产品要求统一 Inspiration 也需改,待定。
docs + crates 注释
大量"想法"(低优先),含文件名 docs/03-模块文档/想法探索-对抗式评估-2026-06-12.md。
根因
上次迁移只改 ideas.ts 页头区,详情/操作/模态框 + 其他 i18n + 后端错误 + LLM 工具描述未跟进。
七、设计亮点(明确无问题)
| 机制 | 评价 |
|---|---|
流式看门狗(useAiStream.ts) |
无数据超时兜底,130s > 后端 120s 留余量 |
generating 复位先于 emit(agentic.rs:74/170/228) |
保证前端收 AiCompleted 时后端已可接下条 |
| 错误路径 AiError 覆盖全 | idle/流中断/chunk error/provider 失败,无静默失败 |
| 工具卡片折叠自治 + auto-collapse | shouldKeepOpen 保留 running/pending/rejected/write_file |
| 审批乐观更新 + IPC 失败不回滚 | useAiSend.ts:80-98 防按钮卡死 |
| Markdown 懒加载 + 历史消息缓存 | 首屏不阻塞,已完成消息命中缓存 |
| 回到底部智能判断 | isNearBottom 避免上滑被打断 |
| ContextManager 裁剪 | 三元组原子、视图不污染持久化、测试覆盖 |
八、修复优先级
| 优先 | 问题 | 方向 |
|---|---|---|
| P0 | H1 流式 Markdown 重解析 ✅ 退役(AR-1,2026-06-15)— 自研块级 memo 取代(splitBlocks O(末块)+rAF 节流),详见 流式渲染调研 §5 | 流式态纯文本/增量渲染,完成后再 markdown;rAF 合并 |
| P0 | H2 审批态新建对话卡死 ✅ 已修(AR-2,commit 057a212) | ai_conversation_create 加 generating 守卫 |
| P0 | 第二章 审批卡片裸 id + reason 模板 ✅ 已修(AR-3,commit 36d68dd) | 后端 reason 拼对象名;前端 id→name |
| P0 | 第四章 create_project 双审 ✅ 已修(AR-4,commit 057a212)— schema 加 path/stack + handler 合并绑定 | handler 读 args 的 path/stack,复用 IPC project.rs:43-83 探测逻辑 |
| P1 | H3 审批态 stop 无兜底 ✅ 已修(AR-5,commit 9e2aeff)— stopChat 本地先复位 streaming + clearStreamWatchdog | stopChat 本地先复位 streaming |
| P1 | M3 Low 工具失败语义 ✅ 已修(AR-6,commit f82dd8b)— Low 失败非 AiError,错误回填 tool_result 让 LLM 自处理 | 统一 AiError 后 loop 也退出,或不 emit AiError |
| P1 | 第三章 clean 无入口 ✅ 已修(AR-7,commit 9e2aeff)— clear_messages 真删 + 垃桶按钮二次确认 | AiChat 加清空按钮 + 后端真删当前对话消息 |
| P2 | M1+M2 delta 节流 + 滚动 🔄 重评降级(AR-8,2026-06-15)— 前端 rAF 节流已被 ARC-08 覆盖;剩后端 50ms 合批 + 滚动跟随 | 后端 50ms 合批 / 前端 rAF |
| P2 | M4 friendlyError i18n ✅ 已修(AR-9,commit 9e2aeff)— 全走 i18n.global.t + zh/en 双语补 key | 抽 i18n key |
| P2 | 第六章 灵感迁移残留 ✅ 已修(AR-10,commit 65c475b)— 13 文件批量统一 | i18n + 后端错误 + LLM 描述统一改 |
| P2 | 第五章 数据联动 ✅ 已修(AR-11,commit dc27e79)— 方案 A 后端 emit df-data-changed + store listen 已 attach |
方案 A 后端 emit + store 监听 |
九、write_file 覆盖事故与可靠性风险(2026-06-14 实测)
事故经过
会话 3473fcb7(2026-06-14 22:16)AI 拟对 PROGRESS.md 做 3 处精准更新(头部当前阶段、全局问题 #9 状态、新增 #10),但误用 write_file(全文覆盖语义)只传了头部 3 行 content,把原 762行/72KB 覆盖成 248字节。AI 自查发现(msg[59-61])尝试凭记忆重建恢复(write_file 17956 字符),但该恢复写入未生效(会话中断),用户无感知「恢复失败」。
暴露的潜在问题
| # | 问题 | 性质 | 修法方向 |
|---|---|---|---|
| FR-S7 | write_file 覆盖已有非空文件无确认/备份 | 根因 | 覆盖非空文件前自动备份 .bak;或检测目标存在强制走 edit_file |
| 关联-1 | 写入后无「预期 vs 实际」校验 | 可靠性 | write_file 返回新旧大小,差异巨大(如原 72KB→新 248B)时 warn/阻断 |
| 关联-2 | AI 自恢复失败无感知 | agent 可靠性 | 工具失败/中断需明确通知用户,不静默吞掉 |
| 备份 | DB 50KB 截断致无法从 DB 完整恢复 | 备份策略 | 长文档丢失仅 git 可救;考虑关键文件写前 git 快照 |
恢复方式与教训
DB 中 PROGRESS.md 原文已被 TRUNCATE_THRESHOLD=50KB(conversation.rs:55)截断(头尾拼接、中段省略),AI 重建版仅 17.9KB 残缺。最终靠 git restore PROGRESS.md(HEAD 版本 762行/72KB 完整)恢复。教训:本地文档类资产的实际保护层是 git 而非 DB 会话历史——会话历史是「对话快照」非「文件备份」。
关联待办
todo.mdFR-S7(write_file 覆盖保护)— P0 安全
十、文件工具系统性走查(2026-06-14,3-agent review 之外的补充走查)
tool_registry.rs 实际注册 3 个文件工具:read_file(:394) / list_directory(:428) / write_file(:444)。其余 edit_file/delete_file/rename_file/move_file/search_content/append_file 均未实现——功能缺口,迫使 LLM 滥用 write_file 全量覆写,放大 FR-S7 危害。
P0 — 阻断
| # | 问题 | 位置 | 修法方向 |
|---|---|---|---|
| FR-S8 | 路径 sandbox 系统性逃逸:①validate_path 子串 .. 检测对绝对路径无效(C:\Windows\... 不含 .. 绕过);②canonicalize 仅对已存在路径跑,write_file 新建文件 + symlink 父目录场景失效;③Windows Path::starts_with 大小写敏感而文件系统不敏感,可误判/漏判 |
:19-21, :53-69 | 统一 canonicalize(不存在路径取最长存在前缀)+ 大小写不敏感 prefix 比较 + parent 也校验 |
| FR-S7(放大) | write_file 定级 Medium 可自动批准 + 非原子覆写,覆写任意已存在非空文件即数据丢失(见 §9) | :446, :461 | 覆写非空文件升 High 审批 + .bak 备份 + 原子写(tmp→rename) |
P1 — 重要
| # | 问题 | 位置 |
|---|---|---|
| P1-1 | write_file 非原子写:中途崩溃留半截文件丢原内容(叠加 FR-S7 数据彻底丢失) | :461 |
| P1-2 | write_file create_dir_all(parent) 不校验 parent,workspace 内 symlink 父目录可写逃逸 |
:457-460 |
| P1-3 | read_file 1MB 按字节 + read_to_string 对非 UTF-8/二进制直接失败无降级 |
:409, :413 |
| P1-4 | read_file offset 无上限校验(超范围静默返空),limit 无硬上限 | :415-419 |
| P1-5 | list_directory 噪音目录仍作为 entry 返回(仅不深入),max_depth=2 写死无文档 | :437, :439, :500 |
| P1-6 | list_directory DirEntry::metadata() 跟随 symlink,symlink 目录被当普通目录递归(信息泄露 + 与 P1-2/FR-S8 形成逃逸组合拳) |
:491-492 |
| P1-7 | validate_path 黑名单子串匹配:易误伤(appdata-collector 项目)易绕过(漏 .config/.kube/Program Files),冗余弱层 |
:22-29 |
P2 — 次要
list_directory 子目录无权限读整层 bail(:484)/ read_file 错误回显完整绝对路径泄露(:406)/ write_file bytes_written 字节非字符易误导(:463)/ max_entries off-by-one(:486)/ to_str 非 UTF-8 路径笼统报错(:401)/ 未实现工具缺口(edit/delete/rename/move/search/append 迫使滥用 write_file)。
总结
框架方向正确(schema+risk+handler 同源、双层校验、FR-S2 TOCTOU+1MB 已修),但 sandbox 实现层有系统性缺口:子串黑名单(弱)+ 词法 starts_with(Windows 大小写坑)+ canonicalize 只覆盖存在路径(新建漏)。优先级:FR-S8 统一 canonicalize > P1-6/P1-2 symlink 不跟随 > FR-S7 原子写+升审批。
关联 todo:FR-S7(覆盖保护)、FR-S8(sandbox 逃逸)。
附:相关 memory(指针)
devflow-aichat-review-pending.mddevflow-idea-inspiration-migration.mddevflow-data-change-sync.md
三者在 memory 中仅留指针,详情以本报告为准。