Files
DevFlow/docs/02-架构设计/aichat审查报告-2026-06-14.md

21 KiB
Raw Blame History

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 currentTextonContentChange → 在底部则 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 卸载不调 stopListenerAiChat.vueonUnmounted。面板 v-if 开关销毁重建 AiChat,但 stopListener(useAiEvents.ts:240)从不调用,Tauri listener 累积泄漏(幂等防住重复注册,但旧 listener 不释放)。

🟢 低危

L1 队列续发失败丢消息useAiSend.ts:25-29 drainQueuequeue.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 toolDisplayName CRUD 类从 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,emit df-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:24ideaCount: '{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-12026-06-15— 自研块级 memo 取代splitBlocks O(末块)+rAF 节流),详见 流式渲染调研 §5 流式态纯文本/增量渲染,完成后再 markdown;rAF 合并
P0 H2 审批态新建对话卡死 已修AR-2commit 057a212 ai_conversation_create 加 generating 守卫
P0 第二章 审批卡片裸 id + reason 模板 已修AR-3commit 36d68dd 后端 reason 拼对象名;前端 id→name
P0 第四章 create_project 双审 已修AR-4commit 057a212— schema 加 path/stack + handler 合并绑定 handler 读 args 的 path/stack,复用 IPC project.rs:43-83 探测逻辑
P1 H3 审批态 stop 无兜底 已修AR-5commit 9e2aeff— stopChat 本地先复位 streaming + clearStreamWatchdog stopChat 本地先复位 streaming
P1 M3 Low 工具失败语义 已修AR-6commit f82dd8b— Low 失败非 AiError错误回填 tool_result 让 LLM 自处理 统一 AiError 后 loop 也退出,或不 emit AiError
P1 第三章 clean 无入口 已修AR-7commit 9e2aeff— clear_messages 真删 + 垃桶按钮二次确认 AiChat 加清空按钮 + 后端真删当前对话消息
P2 M1+M2 delta 节流 + 滚动 🔄 重评降级AR-82026-06-15— 前端 rAF 节流已被 ARC-08 覆盖;剩后端 50ms 合批 + 滚动跟随 后端 50ms 合批 / 前端 rAF
P2 M4 friendlyError i18n 已修AR-9commit 9e2aeff— 全走 i18n.global.t + zh/en 双语补 key 抽 i18n key
P2 第六章 灵感迁移残留 已修AR-10commit 65c475b— 13 文件批量统一 i18n + 后端错误 + LLM 描述统一改
P2 第五章 数据联动 已修AR-11commit dc27e79— 方案 A 后端 emit df-data-changed + store listen 已 attach 方案 A 后端 emit + store 监听

九、write_file 覆盖事故与可靠性风险2026-06-14 实测)

事故经过

会话 3473fcb72026-06-14 22:16AI 拟对 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=50KBconversation.rs:55截断头尾拼接、中段省略AI 重建版仅 17.9KB 残缺。最终靠 git restore PROGRESS.mdHEAD 版本 762行/72KB 完整)恢复。教训:本地文档类资产的实际保护层是 git 而非 DB 会话历史——会话历史是「对话快照」非「文件备份」。

关联待办

  • todo.md FR-S7write_file 覆盖保护)— P0 安全

十、文件工具系统性走查2026-06-143-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) 不校验 parentworkspace 内 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() 跟随 symlinksymlink 目录被当普通目录递归(信息泄露 + 与 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_withWindows 大小写坑)+ canonicalize 只覆盖存在路径(新建漏)。优先级:FR-S8 统一 canonicalize > P1-6/P1-2 symlink 不跟随 > FR-S7 原子写+升审批

关联 todoFR-S7覆盖保护、FR-S8sandbox 逃逸)。


附:相关 memory(指针)

  • devflow-aichat-review-pending.md
  • devflow-idea-inspiration-migration.md
  • devflow-data-change-sync.md

三者在 memory 中仅留指针,详情以本报告为准。