# aichat 审查报告 — 2026-06-14 > 性质:只审查不改代码。本报告汇总本次会话对 devflow AI chat 全链路的核对发现。 ## 审查范围 - **前端**:`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 双审 / 数据联动方案 / 想法→灵感迁移。 --- ## 一、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`): ```rust 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(用户实测痛点) ### 根因:AI 工具 schema 比 IPC 接口窄 | 层 | create_project 参数 | 建带目录项目 | |----|---------------------|-------------| | IPC `create_project`(`project.rs:43-83`) | name, description, idea_id, **path, stack** | ✅ 一步完成(校验+防重复+探测 stack) | | AI 工具 `create_project`(`tool_registry.rs:148`) | name, description(**无 path**) | ❌ 只能建空项目,handler 写死 path=None(:159) | → LLM 建带目录项目被逼拆两步:`create_project`(Medium 审1 + LLM 轮次1)→ `bind_directory`(Medium 审2 + LLM 轮次2)= **双审批双 API**。IPC 本可一步完成,AI 工具没用上。 ### 方案 `create_project` AI 工具 schema 加可选 `path`/`stack`(对齐 IPC `CreateProjectInput`),handler 复用 IPC 的绑定+探测逻辑。`bind_directory` 保留用于后期改绑(relocate)。 ### 关联 问题 1 修好后 `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: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-模块文档/想法探索-对抗式评估.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 重解析 | 流式态纯文本/增量渲染,完成后再 markdown;rAF 合并 | | P0 | H2 审批态新建对话卡死 | `ai_conversation_create` 加 generating 守卫 | | P0 | 第二章 审批卡片裸 id + reason 模板 | 后端 reason 拼对象名;前端 id→name | | P0 | 第四章 create_project 双审 | AI 工具 schema 加 path/stack | | P1 | H3 审批态 stop 无兜底 | stopChat 本地先复位 streaming | | P1 | M3 Low 工具失败语义 | 统一 AiError 后 loop 也退出,或不 emit AiError | | P1 | 第三章 clean 无入口 | AiChat 加清空按钮 + 后端真删当前对话消息 | | P2 | M1+M2 delta 节流 + 滚动 | 后端 50ms 合批 / 前端 rAF | | P2 | M4 friendlyError i18n | 抽 i18n key | | P2 | 第六章 灵感迁移残留 | i18n + 后端错误 + LLM 描述统一改 | | P2 | 第五章 数据联动 | 方案 A 后端 emit + store 监听 | --- ## 附:相关 memory(指针) - `devflow-aichat-review-pending.md` - `devflow-idea-inspiration-migration.md` - `devflow-data-change-sync.md` 三者在 memory 中仅留指针,详情以本报告为准。