From ca9b3187b592bfccdca657d3c04f3d7f7659f6be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BB=9D=E5=B0=98?= <237809796@qq.com> Date: Wed, 17 Jun 2026 13:00:37 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D:=20sendQueuedNow=E7=AB=8B?= =?UTF-8?q?=E5=8D=B3=E5=8F=91=E9=80=81=E9=80=80=E5=8C=96=E5=85=A5=E9=98=9F?= =?UTF-8?q?+=E8=A3=B8diff=E5=9B=B4=E6=A0=8F=E6=B8=B2=E6=9F=93+=E5=B7=A5?= =?UTF-8?q?=E5=85=B7=E5=8D=A1=E8=BD=AE=E6=AC=A1=E8=87=AA=E5=8A=A8=E6=94=B6?= =?UTF-8?q?=E8=B5=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AI聊天组件极端数据场景分析-2026-06-17.md | 285 +++++++++++++++ docs/todo.md | 345 +++++++++++++++++- src/composables/ai/useAiSend.ts | 8 +- src/composables/useMarkdown.ts | 96 ++++- 4 files changed, 727 insertions(+), 7 deletions(-) create mode 100644 docs/05-代码审查/AI聊天组件极端数据场景分析-2026-06-17.md diff --git a/docs/05-代码审查/AI聊天组件极端数据场景分析-2026-06-17.md b/docs/05-代码审查/AI聊天组件极端数据场景分析-2026-06-17.md new file mode 100644 index 0000000..75a7539 --- /dev/null +++ b/docs/05-代码审查/AI聊天组件极端数据场景分析-2026-06-17.md @@ -0,0 +1,285 @@ +# AI 聊天组件极端数据场景多角度分析报告 — 2026-06-17 + +> 分析范围:`AiChat.vue`(3917行/166KB)、`useMarkdown.ts`(192行)、`useAiVirtualScroll.ts`(162行)、`ToolCard.vue`(1177行) +> 方法:六维边界值矩阵 × 代码走查 × 性能估算 +> 来源:AI Chat 上下文会话中逐文件走读后交付 + +--- + +## 一、超长文本场景 + +### P0-A:单条 AI 消息完成态 >100K 字符 — 无高度约束,DOM 爆炸 + +**代码路径**: +``` +renderContent(msg) → renderMd(msg.content) → _marked.parse(text) → _purify.sanitize → v-html +``` + +**定位**:`AiChat.vue:468` `
` + `useMarkdown.ts:182-190 renderMd()` + +**风险论证**: +- `.ai-msg-bubble--ai`(L3260-3278)仅有 `max-width: var(--df-msg-max-width)`(90%)和 `overflow-wrap: anywhere`,**无 `max-height` 限制**。 +- 100K 字符 markdown → marked.parse 可能耗时 >200ms 阻塞主线程(marked 是同步解析)。 +- DOMPurify 对 100K+ HTML 字符串的 sanitize 同样 O(n) 开销。 +- `_mdCache` 以**完整原始文本为 key**,100K 文本作为 Map key 本身就是内存开销。 +- 缓存上限 200 条但无 LRU,`size>200` 时**全量 clear**(L189),频繁切换消息可能导致缓存抖动。 + +**复现条件**:AI 返回超长代码审查报告 / 全文翻译 / 大段文档。 + +**修复方向**:`.ai-msg-bubble--ai` 加 `max-height:600px` + `overflow-y:auto` + "内容过长,展开查看"折叠机制。 + +--- + +### P0-B:流式生成超长消息 — 每帧全文 lexer,O(n) 随长度线性增长 + +**代码路径**: +``` +scheduleStreamParse(text) → rAF → renderStreamingBlocks(lastStreamText) → splitBlocks(text) → marked.lexer(text) 全文解析 +``` + +**定位**:`AiChat.vue:821-851 splitBlocks()` 每帧对全文调 `marked.lexer(text)` + +**风险论证**: +- `splitBlocks` 每 rAF 调用 `getMarked()!.lexer(text)`,解析**整个流式文本**而非增量。 +- 流式到 50K 字符、200+ token blocks 时,每帧 lexer 开销显著(marked lexer 是 O(n) 的)。 +- 更致命的子场景:**无换行的超长连续文本**(如 LLM 返回无段落分隔的巨段)→ `splitBlocks` 输出为单一巨大 block → 该 block 作为"尾块"每帧调 `parseBlockNoCache`(marked.parse + DOMPurify)→ 每帧全量重渲染。 +- `_blockCache` 虽缓存已完成块,但**尾块始终重算**,流式越长越痛。 + +**性能估算**:100K 文本 × 60fps rAF → 每帧 `marked.lexer(100K)` → 假设 1μs/char → ~100ms/frame → **严重掉帧**。 + +**修复方向**:`splitBlocks` 内对全文长度 >50K 做采样或限制 blocks 数量;尾块超过阈值(如 5K 字符)改为增量 append 而非全文重算。 + +--- + +## 二、特殊字符场景 + +### P2-A:XSS 注入 — 三层防护完备 ✅ + +- `escapeFallback`(useMarkdown.ts:175):&/ HTML 实体转义 → marked 未就绪时纯文本安全。 +- `DOMPurify.sanitize`(useMarkdown.ts:188):marked 输出后过滤 → 所有 `v-html` 输出必经此层。 +- `highlightCode`(useMarkdown.ts:99-107):catch 异常降级 HTML 转义。 + +**结论**:无问题,无需修复。 + +--- + +### P2-B:JSON 解析注入 — ToolCard parseResult 安全 ✅ + +`ToolCard.vue:287-293 parseResult()` 包 `try-catch`,非法 JSON 返回 null → 模板 `v-if parsed` 不渲染。 + +**结论**:无问题,无需修复。 + +--- + +### P2-C:simpleHash 碰撞 — 流式块 key 冲突 + +**定位**:`AiChat.vue:884-888 simpleHash()` 使用 DJB2 算法(32-bit 输出) + +**风险论证**: +- 已完成块的 key 用 `b-{simpleHash(blockText)}`,key 稳定性依赖 hash 唯一性。 +- 32-bit 哈希在 300 条缓存内有碰撞概率(生日悖论:~10⁻⁵ 级别,低但不为零)。 +- **碰撞后果**:两块不同文本有相同 key → Vue 复用 DOM → 渲染错误内容。 +- 实际影响极小(需要两块不同文本产生相同 32-bit hash + 同时出现在同一流式会话中),但零概率保证。 + +**修复方向**:改用 `block.text.slice(0, 40) + block.text.length` 做 key(确定性唯一,无碰撞风险)。 + +--- + +## 三、空值/缺失场景 + +### P3-A:核心函数空值防护 — 完备 ✅ + +| 函数 | 空值处理 | 判定 | +|------|----------|------| +| `renderMd('')` | L181 提前返回 '' | ✅ | +| `renderStreamingBlocks('')` | L868 返回 [] | ✅ | +| `formatBytes(null/undefined/NaN)` | ToolCard L341 isFinite 兜底 '0 B' | ✅ | +| `formatArgValue(undefined/null)` | ToolCard L278 返回 '' / 特殊显示 | ✅ | +| `parseResult(null)` | ToolCard L288 返回 null | ✅ | +| `isNearBottom()` | 容器 ref 为空返回 true(兜底认为在底部) | ✅ | + +--- + +### P3-B:`messagesContainer` undefined 时序 — 有守卫,但 watch 触发过早 + +**定位**:`AiChat.vue:1978` `watch(() => store.state.messages.length, onContentChange)` 在 onMounted 前可能触发(store 初始化时 messages 已有数据)。 + +`onContentChange → isNearBottom() → el = undefined → return true → scrollToBottom() → messagesContainer.value 为 undefined → 静默无操作`。 + +**结论**:不会崩溃,但首次加载时自动滚底不生效(需等 onMounted 容器就绪后用户手动滚或新消息触发)。影响轻微,可不修复。 + +--- + +## 四、大量数据场景 + +### P4-A:1000+ 条消息 — 多次全量遍历 + +**代码路径**: +``` +messageSegments computed → O(n) 遍历 messages +renderItems computed → O(n) 遍历 segments +buildActiveToolIds → O(n×t) 遍历 messages + toolCalls +lastMsgSnapshot watch → JSON.stringify O(n×t) +``` + +**定位**: +- `AiChat.vue:2455 messageSegments` — 每条消息一次遍历 +- `AiChat.vue:2511 renderItems` — 每条 segment 一次遍历 +- `AiChat.vue:2052 buildActiveToolIds` — 双重循环 messages × toolCalls +- `AiChat.vue:2071-2079` deep watch — `JSON.stringify({n, tc: [...所有toolCalls的id+status...]})` — **每次 deep watch 触发都全量序列化** + +**已有优化**:B-260616-13 已剔除 content.length,流式 delta 不再触发 watch body 重算(snap 不变)。✅ + +**剩余风险**:初始加载 1000+ 条消息时 `messageSegments` + `renderItems` 仍全量计算。虚拟滚动裁剪了渲染,但 computed 链仍全量算。 + +**修复方向**:messageSegments 改为分页/窗口计算(配合虚拟滚动只算可见窗口附近)。 + +--- + +### P4-B:虚拟滚动 — IntersectionObserver entries 批量处理 ✅ + +**定位**:`useAiVirtualScroll.ts:67-90` + +IO 回调中遍历 entries → `next.delete(key)` 操作 Set → `renderedKeys.value = next` 触发响应式更新。大批量 entries(快速滚屏)时一次性处理多条目,但每条目是 O(1) Set 操作。 + +**结论**:合理,无性能瓶颈。唯一关注点:`pinnedKeys` 无上限但当前恒为单 key(流式末条)。 + +--- + +## 五、数值边界场景 + +### P5-A:`_mdCache` 全量 clear — 缓存雪崩 + +**定位**:`useMarkdown.ts:189` `if (_mdCache.size > 200) _mdCache.clear()` + +**风险论证**: +- 不是 LRU 淘汰,而是**全部清空**。 +- 场景:201 条不同消息文本 → 第 201 条触发全清 → 随后任何历史消息再次渲染需重新 marked.parse + sanitize。 +- 200 条的阈值对于长对话偏低(200 轮往返=400 条消息)。 + +**修复方向**:改为 LRU 淘汰(Map 已保证插入顺序,`_mdCache.delete(_mdCache.keys().next().value)` 就是 FIFO 淘汰一条而非全清)。 + +--- + +### P5-B:`_blockCache` 逻辑 — FIFO 逐条淘汰,优于 _mdCache ✅ + +**定位**:`AiChat.vue:856` `_blockCache.delete(_blockCache.keys().next().value!)` + +这里用的是**逐条 FIFO 淘汰**(只删最老一条),比 `_mdCache` 的全清更合理。但 `BLOCK_CACHE_LIMIT=300` 且无 LRU,高频文本块可能被误淘汰。影响轻微。 + +--- + +### P5-C:其他边界值 — 均有兜底 ✅ + +| 项目 | 上限 | 状态 | +|------|------|------| +| `pendingImages` | 6 张 | ✅ | +| `mentionItems` | 各 20 条 | ✅ | +| `inputText` 无 maxlength | **无限制** | ⚠️ 可粘贴 1MB 文本,虽不崩溃但无提示 | +| `autoResize` | 120px max-height | ✅ | + +--- + +## 六、深层嵌套场景 + +### P6-A:模板条件嵌套深度 — 可维护性差但功能正确 + +**定位**:AiChat.vue template L362-567 + +``` +v-for renderItems + → sentinel div (.ai-msg-slot) + → v-if kind==='sep' (分隔条,始终渲染) + → v-else-if shouldRenderMsg (消息体) + → v-if role==='user' + → 用户气泡+图片+编辑+时间戳 + → v-else (AI) + → v-if streaming + currentText + → v-for streamingBlocks (分块渲染) + → v-else v-html renderContent (完成态) + → v-else-if 流式无文本光标 + → 操作栏(复制/重试/重生成) + → Token 用量 + → ToolCardList 工具卡 + → 时间戳 +``` + +嵌套深度:6 层。功能正确,但 3917 行单文件严重违反单一职责。 + +**修复方向**:建议拆分子组件: +- `AiMessageBubble.vue` — 单条消息气泡(user/ai 双分支) +- `AiMessageStreaming.vue` — 流式渲染 + 选区保存恢复 +- `AiConversationSidebar.vue` — 对话侧栏 + +--- + +## 七、ToolCard 特有问题 + +### P7-A:read_file 展开全部 — 50K 行全量 DOM + +**定位**:`ToolCard.vue:81` `
{{ parsed?.content }}
` + +折叠态 `max-height:180px` + `overflow-y:auto` + fade 遮罩 → ✅ 安全。 +**展开态**无任何限制 → 50K 行文件内容全部塞进单个 `` 文本节点 → 布局计算耗时。 + +**修复方向**:展开态也加 `content.slice(0, 50000)` 截断 + "内容过长已截断"提示。 + +--- + +### P7-B:list_directory 无条目上限 + +**定位**:`ToolCard.vue:92-100` `v-for="(entry, i) in parsed?.entries"` 无上限。 + +5000+ 条目 = 5000+ DOM 节点(每项含图标 SVG + 名称 + 大小)。 + +**修复方向**:截断至 500 项 + "还有 N 项未显示"提示。 + +--- + +### P7-C:diffLines 全量 v-for + +**定位**:`ToolCard.vue:45 + 567-577` + +10000 行 diff → `diffLines` computed 全量 split + map → 10000 个 `` DOM 节点。CSS `max-height:240px` 仅遮视觉,DOM 全在内存。 + +**修复方向**:`diffLines` computed 内截断至 1000 行 + 截断提示行。 + +--- + +## 八、问题汇总与优先级 + +| 编号 | 场景 | 等级 | 现象 | 修复方向 | +|------|------|------|------|----------| +| P0-A | 单条消息 >100K 字符完成态 | 🔴 | DOM 爆炸 + marked 同步阻塞 | `.ai-msg-bubble--ai` 加 `max-height:600px` + `overflow-y:auto` + "内容过长,展开查看"折叠 | +| P0-B | 流式生成超长消息 | 🔴 | 每帧全文 lexer + 尾块全量 parse | `splitBlocks` 内对全文长度 >50K 做采样或限制 blocks 数量 | +| P4-A | 1000+ 条消息初始化 | 🔴 | computed 链全量遍历 | messageSegments 改为分页/窗口计算(配合虚拟滚动只算可见窗口) | +| P7-A | read_file 展开全部 | 🟡 | 50K 行全量 DOM | 展开态截断 `content.slice(0,50000)` | +| P7-B | list_directory 无上限 | 🟡 | 5000+ DOM 节点 | `entries.slice(0,500)` | +| P7-C | diffLines 全量渲染 | 🟡 | 10000 行 DOM 节点 | `diffLines.slice(0,1000)` | +| P5-A | _mdCache 全量 clear | 🟡 | 缓存雪崩 | 改 LRU 逐条淘汰 | +| P2-C | simpleHash 碰撞 | 🟡 | 块级 key 冲突(极低概率) | 改用 `block.slice(0,40)+length` | +| P6-A | 3917 行巨型组件 | 🟡 | 可维护性差 | 拆分子组件 | +| P3-B | messagesContainer undefined 时序 | 🟢 | 首次加载不自动滚底 | 影响轻微,可不修复 | + +--- + +## 九、已有防护措施汇总(无需修复) + +| 措施 | 位置 | +|------|------| +| DOMPurify XSS 防护 | useMarkdown.ts:188 | +| escapeFallback 纯文本降级 | useMarkdown.ts:174-176 | +| formatBytes NaN/Infinity 兜底 | ToolCard.vue:341 | +| formatArgValue undefined/null 兜底 | ToolCard.vue:276-282 | +| parseResult try-catch | ToolCard.vue:287-293 | +| highlightCode try-catch | useMarkdown.ts:99-107 | +| read_file 折叠态 max-height:180px | ToolCard.vue:1074 | +| inputText autoResize max 120px | AiChat.vue:1802 | +| 虚拟滚动 sentinel 占位防塌 | useAiVirtualScroll.ts:79-82 | +| rAF 节流防掉帧 | AiChat.vue:897 | +| 流式末条 pinned 保活 | AiChat.vue:2539-2549 | + +--- + +*分析完成日期:2026-06-17* +*相关任务:DevFlow AI Chat 组件极端数据场景多角度分析* diff --git a/docs/todo.md b/docs/todo.md index 81ef53e..97edf7f 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -113,11 +113,247 @@ **已审文件清单(本轮)**:crates/df-ai-core/src/provider.rs · crates/df-ai/src/anthropic_compat.rs · crates/df-ai/src/openai_compat.rs · src-tauri/src/commands/ai/conversation.rs · src-tauri/src/commands/ai/commands.rs · src/api/ai.ts · src/api/types.ts · src/composables/ai/useAiSend.ts · src/composables/ai/useAiConversations.ts · src/stores/ai.ts · src/components/AiChat.vue(图片构建+渲染) · src/components/settings/ProviderPanel.vue **新登记 todo**:B-260617-07 · B-260617-08 +### 🔧 2026-06-17 aichat 消息全量重叠(5角度深入分析·未实施) + +> 用户报 aichat 对话「大量重叠」「不单间距问题」。session-role-diagnose-only,5 角度并行论证后记录。 + +**现象确认(基于截图)**: +| 现象 | 判定 | +|---|---| +| 顶部出现 `` 等模板源码文本 | ✅ 正常 — AI 回复中 read_file 读取 AiChat.vue 源码后 v-html 展示,非渲染泄漏 | +| 消息/工具卡大量重叠堆叠 | 🔴 异常 — 布局层叠问题 | +| 整体挤压、间距消失 | 🔴 异常 | + +**已排除项**: + +| 检查点 | 结论 | +|---|---| +| CSS 声明链路 | ✅ `.ai-messages` 有 `display:flex;flex-direction:column;gap:14px`(:3109-3116),无外部覆盖 | +| scoped 样式冲突 | ✅ 单 `