- openai_compat: 扫描所有 assistant 消息剥离 orphan tool_calls(原仅查末条) - queue 加 conversationId 字段,按会话精准 drain - regenerate/editMessage 只清本会话排队消息 - newConversation 保留旧会话排队消息 - AiError 只清出错会话的队列项
145 lines
9.8 KiB
Markdown
145 lines
9.8 KiB
Markdown
# aichat 对话功能深度走查报告(2026-07-17)
|
||
|
||
> 触发:用户反馈「对话功能现在存在很多问题,深入检查」。
|
||
> 方法:5 个 general-purpose 代理并行走查 5 维度(发送/流式/渲染/会话侧栏顶栏/审批上下文)→ 主代理独立 grep/read 核验所有 P0 + 关键 P1(不信代理结论,每条标注核验状态)。
|
||
> 性质:**走查任务,未改任何代码**。本报告为问题清单 + 修复方向,待用户确认后实施。
|
||
|
||
## 健康度总评:B-
|
||
|
||
最近一批发改动(BUG-2026-07-17:状态机收敛方向正确)引入 **2 个 P0 回归**;其余 P1 多源自 **F-09 per-conv 改造未完成**的单例债(queue / legacy watchdog / _approvalTimers / modelOverride / _lastDelta 仍是全局单例,多会话并发下串话)。
|
||
|
||
---
|
||
|
||
## P0(功能 bug,建议立即修)
|
||
|
||
### P0-1 流式 watchdog legacy 路径全局误杀并发会话 + 删 running 保护致长工具误杀 ✅核验成立
|
||
|
||
**位置**:`useAiStream.ts:57-110, 73-78` / `useAiSend.ts:156,226`(doSend reset 同款)/ `audit/mod.rs:125-128`
|
||
|
||
**双重回归**(本次改动引入):
|
||
|
||
① **legacy 路径全局 clear**:doSend / regenerate(`useAiSend.ts:156`) / editMessage(`:226`) 调 `resetStreamWatchdog()` **无 convId** → 走 `_legacyWatchdog` fallback(`useAiStream.ts:128-132`)。45s 到期 `onStreamTimeout()` 无参 → `convStates.clear()`(`:77`)清掉**所有并发会话**的生成态。其他会话停止按钮失效、UI 与后端长期不一致。
|
||
|
||
② **删 running 保护**:`onStreamTimeout`(`:69`「不管 stillGenerating 检测」)去掉了原 stillGenerating + running 工具检测。长工具执行(cargo build / 测试套件 / 大文件 patch >45s)必然触发。后端心跳首 tick **30s**(`audit/mod.rs:126` 弃首 tick),AiHeartbeat 到达时前端 `resetStreamWatchdog(convId)` 只动 per-conv Map、**不清 legacy**,保护不了 doSend 挂的 legacy timer。
|
||
|
||
**证据**:`useAiStream.ts:69`「不管 stillGenerating 检测(即使已经 false 也再清理一次)」+ `:77` `convStates.clear()` + `audit/mod.rs:125-128` 心跳 30s。
|
||
|
||
**影响**:多会话并发下其他会话被误杀 + 任何 >45s 的工具执行被前端误判断流(回到 BUG-260624-03 修复前的用户报障状态)。
|
||
|
||
**修复方向**:
|
||
- legacy 路径 `convStates.clear()` → `convStates.delete(state.activeConversationId)`(对齐 legacy 单 timer 只跟踪当前会话语义)
|
||
- 恢复 `onStreamTimeout` 的 running 工具检测(工具执行中跳过 + reset 续等),或 `STREAM_TIMEOUT_MS` 回调 ≥75s
|
||
- 根治:doSend / regenerate / editMessage 的 `resetStreamWatchdog` 传 convId 走 per-conv(F-09 迁移)
|
||
|
||
### P0-2 审批计时器切/删会话不清 + 注释与代码矛盾 ✅核验成立
|
||
|
||
**位置**:`useAiConversations.ts:92-257`(switch 无 clearApprovalTimer)/ `:260-272`(delete 无清)/ `aiShared.ts:118`(默认 15min)vs `useAiConversations.ts:244-246`(注释「Infinity 已决策不超时」)
|
||
|
||
**证据**:
|
||
- switchConversation 全程未调 clearApprovalTimer / clearAllApprovalTimers(已核 90-257)
|
||
- deleteConversation 仅 `clearConvStreamState(id)`,不清 `_approvalTimers` 中属于该会话的 timer
|
||
- `startApprovalTimer` 到点回调(`aiShared.ts:147-163`)调 `aiApi.approve(id,false)` + push 错误气泡到 `getMessages()`(**当前 active 视图**)
|
||
- `:244-246` 注释「APPROVAL_TIMEOUT_MS=Infinity 已决策,不会超时自动拒绝」与 `:118` `APPROVAL_TIMEOUT_DEFAULT_MS=900_000`(15min)**直接矛盾**
|
||
|
||
**影响**:① 切走会话后 15min 到点 → 误拒后台 pending 工具 + 错误气泡 push 到当前(错误)会话视图;② 用户重启恢复审批后离开,回来发现审批已被无声自动拒绝。
|
||
|
||
**修复方向**:switchConversation 开头 `clearAllApprovalTimers()`(对齐 pendingApprovals 单例语义);deleteConversation 清该 conv 的 timer;修正 `:244` 注释。
|
||
|
||
---
|
||
|
||
## P1(明显缺陷)
|
||
|
||
### P1-1 [发送] queue=[] 跨会话丢消息 ✅核验成立
|
||
`useAiSend.ts:155,225` `state.queue = []`,queue 是 store 全局单例。B 会话 regenerate/editMessage 清掉 A 排队的用户消息,静默永久丢失。
|
||
|
||
### P1-2 [发送] drainQueue 无并发互斥 ✅核验成立
|
||
`useAiSend.ts:257-281` fire-and-forget 无锁,AiCompleted 连发 / stop+Completed 竞态致并发 doSend,前端 user/ai 占位双 push(后端 generating 守卫挡大部分但视图已污染)。
|
||
|
||
### P1-3 [会话] TopBar watch(activeConversationId) 无条件重置 modelOverride ✅核验成立
|
||
`TopBar.vue:325-327` 切会话无条件 `modelOverride = enabledModels[0]`,切会话往返丢失用户手选模型;与 `:335` `watch(enabledModels,{immediate})` 叠加语义脆弱。modelOverride 是模块级 ref,不持久化、不 per-conv。
|
||
|
||
### P1-4 [发送] tryForceSend 跨会话错位 + 队列续发 spans 丢失 ✅核验成立
|
||
`useAiSend.ts:323-328` queue item 无 convId / spans 字段,切会话后 forceSend 发到当前 active 而非原会话;mention chip 续发降级为纯文本。
|
||
|
||
### P1-5 [流式] _lastDelta 单值跨会话/跨轮误丢合法 delta ⚠️部分成立
|
||
`useAiEvents.ts:46,292` 单值重复检测,跨会话首 delta 撞同 / 同轮合法重复(Markdown 表格分隔符等)被误丢。难复现。
|
||
|
||
### P1-6 [流式] state.currentText += delta 无 streaming 守卫 ⚠️部分成立
|
||
`useAiEvents.ts:300`,P0-1 触发后超时收尾仍到达的 delta 污染 currentText。修 P0-1 即间接修复。
|
||
|
||
### P1-7 [渲染] MessageItem.vue system/assistant/error 死代码 ✅核验成立
|
||
`MessageItem.vue:94-170`,MessageList 仅 `role==='user'` 引用 MessageItem(`:663-673`),AI 渲染全在 MessageList 内联。MessageItem 内 `.ai-cursor-blink` 是孤岛 class(全仓库无其他定义)。维护陷阱。
|
||
|
||
### P1-8 [渲染] _blockCache 无失效机制 ✅核验成立
|
||
`useStreamRenderer.ts:117` 模块级 Map 无会话切换 / 配置变更失效,长会话膨胀 + Markdown 主题热改无效 + 瞬态错误固化。
|
||
|
||
### P1-9 [渲染] messages deep watch + JSON.stringify 每帧全量遍历 ✅核验成立
|
||
`MessageList.vue:244` deep watch 每帧 O(N×content) 遍历,长会话流式掉帧。
|
||
|
||
### P1-10 [渲染] useToolApproval onApprove confirmDialog 无防重入 ✅核验成立
|
||
`useToolApproval.ts:98-113` High 风险工具 confirmDialog await 期间 approving 未置 true,双击重复审批。
|
||
|
||
### P1-11 [会话] 删当前会话 active=null 无 fallback ✅核验成立
|
||
`useAiConversations.ts:260-272` 删除当前会话后空白无引导,应回落相邻会话。
|
||
|
||
### P1-12 [审批] 审批卡无 conversationId,多会话路由靠 activeConversationId 推断 ⚠️部分成立
|
||
`useAiApproval.ts:65`,F-09 多会话并发下 A 后台审批卡渲染到 B 视图时点击路由不准(依赖多会话并发 + 后台审批叠加才触发)。
|
||
|
||
---
|
||
|
||
## P2(健壮性/可读性)
|
||
|
||
- `useStreamRenderer.ts:139` tailSeq 局部失效 —— **功能没坏**(v-html 值变化兜底更新),仅注释「末块 key 每次不同」与实现(恒为 `tail-1`)相反,误导维护者。
|
||
- `ApprovalOverlay.vue:61` `Date.now()` 在 computed 内,60s 冷却期不响应式过期。
|
||
- `aiShared.ts:288-311` setConvStreaming / setConvCurrentText 收敛不对称,Map 项可能残留。
|
||
- `ConversationSidebar.vue:255` 标题 flash watch 死代码(`if(oldTitle) return` 覆盖前一行)。
|
||
- `useAiWindow.ts:309` maximized 分支 `targetHeight` 三元死代码(高度 vs 宽度比较)。
|
||
- `useToolApproval.ts:29` `emit: any` 类型擦除。
|
||
- `useAiContext.ts:29` `isCompressing` 模块单例,多窗口共享。
|
||
|
||
---
|
||
|
||
## ❌代理误报已否决(独立核验)
|
||
|
||
- **~~[会话 P0] switchConversation currentText accessor 重定向~~**:代理4 称 `:206 state.currentText=''` 在两次 await 间被 accessor 重定向。**否决**——`:113` 设 `activeConversationId=B` 后到 `:206` 是同步连续代码(无 await),`:206` 之后才 `:212 await`。accessor 不会中途重定向,写的是正确的目标会话。代理4 时序分析错误。
|
||
|
||
---
|
||
|
||
## 根源分析
|
||
|
||
所有 P0/P1 的共同根因:**F-09 per-conv 改造未完成**。
|
||
|
||
| 状态源 | per-conv 化状态 | 串话表现 |
|
||
|---|---|---|
|
||
| streaming / currentText | ✅ 已 per-conv(DEC-07a accessor) | — |
|
||
| convStates(conv_state 真相源) | ✅ 已 per-conv(Map) | 但 watchdog legacy fallback 路径 `clear()` 全局误杀(P0-1) |
|
||
| queue | ❌ 全局单例 | 跨会话丢消息(P1-1)、强发错位(P1-4) |
|
||
| watchdog | ⚠️ 双轨(per-conv Map + legacy 单 timer) | legacy 路径不清 by 心跳、全局 clear(P0-1) |
|
||
| _approvalTimers | ❌ 全局 Map | 切/删会话不清(P0-2) |
|
||
| modelOverride | ❌ 模块级 ref | 切会话丢失手选(P1-3) |
|
||
| _lastDelta | ❌ 模块级单值 | 跨会话误丢 delta(P1-5) |
|
||
|
||
---
|
||
|
||
## 建议修复优先级
|
||
|
||
1. **P0-1**(流式误杀,最高频可复现)—— legacy `clear()` 改 `delete(activeConversationId)` + 恢复 running 工具检测
|
||
2. **P0-2**(审批误拒 + 错气泡)—— switch / delete 清 timer + 修正注释
|
||
3. **P1-1 / P1-3 / P1-4**(单例债三连,可攒批修)
|
||
4. 其余 P1(渲染性能/死代码/防重入)按维度推进
|
||
5. P2 择机清理
|
||
|
||
## 相关文件
|
||
|
||
- `src/composables/ai/useAiStream.ts`(P0-1 核心)
|
||
- `src/composables/ai/useAiSend.ts`(P0-1 reset / P1-1,2,4)
|
||
- `src/composables/ai/streamingGuard.ts`(P0-1 forceReset 同款 clear)
|
||
- `src-tauri/src/commands/ai/audit/mod.rs`(P0-1 后端心跳 30s)
|
||
- `src/composables/ai/useAiConversations.ts`(P0-2 / P1-11)
|
||
- `src/composables/ai/aiShared.ts`(P0-2 _approvalTimers / P2 收敛)
|
||
- `src/components/ai/TopBar.vue`(P1-3 双 watch)
|
||
- `src/components/ai/MessageList.vue`(P1-7,9)
|
||
- `src/components/ai/MessageItem.vue`(P1-7 死代码)
|
||
- `src/composables/ai/useStreamRenderer.ts`(P1-8 / P2 tailSeq)
|
||
- `src/composables/ai/useToolApproval.ts`(P1-10)
|