修复: force_send单锁竞态+classify unknown不重试(撤销误判)+UX交互批

This commit is contained in:
2026-06-17 14:31:47 +08:00
parent 0633aa6614
commit 924158cff2
8 changed files with 256 additions and 31 deletions

View File

@@ -34,6 +34,20 @@ import type { AiChatEvent, AiConversationSummary, AiMessage, AiProviderConfig, A
* - 现有 push 逻辑(composables 内)零改动,上限由本模块 watch 统一兜底。
*/
const MESSAGE_CAP = 200
/**
* 单对话 messages 的 parts 体积软上限(UX-260617-20)。
*
* 背景:MESSAGE_CAP 仅按"条数"裁剪,不感知单条体积。多模态 base64 图片(F-260614-05)
* 单条可达数 MB,极端 200 条 × 5MB ≈ 1GB 会撑爆渲染内存。
*
* 设计:
* - 仅度量 image 片的 base64/url 字符串长度(其余 text/diff 普通字符串可忽略量级);
* base64 char 数 ≈ 编码后字节数(3/4 编码开销),量纲稳定可作预算代理。
* - 超出预算时,从最旧消息起逐条裁掉其 parts(置空数组),保条数与文本可见、丢历史图片。
* - 与 MESSAGE_CAP 互补:条数先裁体积小场景,体积裁多模态大体积场景;两 watch 各司其职。
*/
const PARTS_SIZE_BUDGET = 50 * 1024 * 1024 // 50MB(base64 char 计)
import { useAiEvents } from '@/composables/ai/useAiEvents'
import { useAiStream } from '@/composables/ai/useAiStream'
import { useAiSend, initDrainQueueListener } from '@/composables/ai/useAiSend'
@@ -106,6 +120,49 @@ watch(
},
)
/**
* 估算单条消息 parts 的字符体积(base64/url 字符串长度之和,UX-260617-20)。
*
* 只计 image 片的 base64 与 url(大体量载荷);text 片(正文/工具结果文本)不在此预算口径内,
* 避免对常规长文本消息误伤。无 parts 或非 image 片返回 0。
*/
function estimatePartsSize(parts: ContentPart[] | undefined): number {
if (!parts || parts.length === 0) return 0
let size = 0
for (const p of parts) {
if (p.type === 'image') {
if (p.base64) size += p.base64.length
if (p.url) size += p.url.length
}
}
return size
}
/**
* parts 体积软上限守卫(UX-260617-20):push 式增长使总 parts 体积超 PARTS_SIZE_BUDGET 时,
* 从最旧消息起逐条置空其 parts(丢历史多模态图,保文本/条数)。
*
* 仅拦截 push(同上 length watch 的判定),避免切换对话整体替换时误裁后端历史图。
*/
watch(
() => state.messages.length,
(newLen, oldLen) => {
if (oldLen <= 0 || newLen - oldLen > 2) return
let total = 0
for (const m of state.messages) total += estimatePartsSize(m.parts)
if (total <= PARTS_SIZE_BUDGET) return
// 超预算:从最旧起逐条清 parts,直到总量回落到预算内
for (const m of state.messages) {
if (total <= PARTS_SIZE_BUDGET) break
const partSize = estimatePartsSize(m.parts)
if (partSize > 0) {
m.parts = []
total -= partSize
}
}
},
)
/**
* 对话搜索过滤结果(UX-06 §3.1)。
*