修复: DeepSeek 400 全量扫描 + 队列 per-conv 隔离

- openai_compat: 扫描所有 assistant 消息剥离 orphan tool_calls(原仅查末条)
- queue 加 conversationId 字段,按会话精准 drain
- regenerate/editMessage 只清本会话排队消息
- newConversation 保留旧会话排队消息
- AiError 只清出错会话的队列项
This commit is contained in:
lxy
2026-07-20 00:19:50 +08:00
parent 42efb31bbf
commit e9e3578d26
59 changed files with 2875 additions and 1330 deletions
+29 -12
View File
@@ -24,7 +24,7 @@ import { t } from '@/i18n/i18n-helpers'
import { resetStreamWatchdog, clearStreamWatchdog } from './useAiStream'
import { setStreaming } from './streamingGuard'
import { startListener } from './useAiEvents'
import { nextMsgId, startApprovalTimer, clearApprovalTimer, clearAllApprovalTimers, resolveAiLang } from './aiShared'
import { nextMsgId, startApprovalTimer, clearApprovalTimer, clearAllApprovalTimers, resolveAiLang, convStates } from './aiShared'
/// 待发送队列上限(超过抛错提示用户)
const QUEUE_LIMIT = 10
@@ -90,7 +90,6 @@ async function doSend(text: string, skill?: string, force = false, parts?: Conte
content: '',
timestamp: Date.now(),
})
setStreaming(true, { convId: state.activeConversationId, reason: 'doSend' })
state.currentText = ''
resetStreamWatchdog() // 启动流式看门狗,无数据超时兜底
@@ -112,8 +111,11 @@ async function doSend(text: string, skill?: string, force = false, parts?: Conte
} catch (e) {
// IPC 失败(spawn 前/provider 配置错等):回滚 streaming 防光标卡死 + 移除 user 消息与空气泡占位;
// 重新抛出由 handleSend 回填输入框,用户可重试
setStreaming(false, { convId: state.activeConversationId, reason: 'doSend-ipc-fail' })
clearStreamWatchdog() // 清看门狗,防 130s 后 onStreamTimeout 误推错误气泡
// BUG-2026-07-17:彻底清理 convStates,避免按钮永久红色
const convId = state.activeConversationId
setStreaming(false, { convId, reason: 'doSend-ipc-fail' })
clearStreamWatchdog(convId || undefined)
if (convId) convStates.delete(convId)
state.messages = state.messages.filter(m => m.id !== aiMsgId && m.id !== userMsgId)
throw e
}
@@ -149,7 +151,9 @@ async function regenerate() {
setStreaming(true, { convId: state.activeConversationId, reason: 'regenerate' })
state.currentText = ''
state.queue = [] // 重生成视同新发,清残留队列
// 只清当前会话的排队消息,不误伤其他会话
const regenConvId = state.activeConversationId
state.queue = state.queue.filter(q => q.conversationId !== regenConvId)
resetStreamWatchdog()
await startListener()
@@ -219,7 +223,9 @@ async function editMessage(newMessage: string) {
setStreaming(true, { convId: state.activeConversationId, reason: 'editMessage' })
state.currentText = ''
state.queue = [] // 编辑重生成视同新发,清残留队列
// 只清当前会话的排队消息,不误伤其他会话
const editConvId = state.activeConversationId
state.queue = state.queue.filter(q => q.conversationId !== editConvId)
resetStreamWatchdog()
await startListener()
@@ -251,15 +257,22 @@ async function editMessage(newMessage: string) {
* 回填失败消息到队首并停 drain(不静默丢用户输入,保留剩余队列供手动重试/编辑/取消)。
* 成功路径不受影响——doSend 成功后端会再 emit AiCompleted 续 drain 链路。
*/
export function drainQueue() {
export function drainQueue(convId?: string | null) {
if (state.queue.length === 0) return
const next = state.queue.shift()!
// 找队首属于目标 conv 的消息;若穿越(非当前 conv 的项堵在队首)则跳过整条队列
const targetId = convId ?? state.activeConversationId
const idx = state.queue.findIndex(q => q.conversationId === targetId)
if (idx === -1) return
const next = state.queue.splice(idx, 1)[0]!
const activeConvId = state.activeConversationId
void (async () => {
try {
await sendMessage(next.text, next.skill, false, next.parts)
} catch (e) {
setStreaming(false, { convId: state.activeConversationId, reason: 'drainQueue-fail' })
clearStreamWatchdog()
// BUG-2026-07-17: 失败时彻底收尾,带 convId 精准清理 per-conv watchdog + convStates
setStreaming(false, { convId: activeConvId, reason: 'drainQueue-fail' })
clearStreamWatchdog(activeConvId || undefined)
if (activeConvId) convStates.delete(activeConvId)
const errMsg = e instanceof Error ? e.message : String(e)
state.queue.unshift(next) // 回填失败消息到队首,保留剩余队列(不静默丢用户输入)
state.messages.push({
@@ -313,12 +326,14 @@ async function sendMessage(text: string, skill?: string, forceMode = false, part
throw new Error(t('ai.queueFull', { limit: QUEUE_LIMIT }))
}
setStreaming(true, { convId: state.activeConversationId, reason: 'L1-enqueue' })
// F-260614-05 Phase 2b: 入队项挂 parts(供 drainQueue 续发时本地 user 消息渲染图)
// F-260614-05 Phase 2b: 入队项挂 parts(供 drainQueue/续发时本地 user 消息渲染图)
// 同时挂 conversationId,供 drainQueue 按会话精准续发(防跨会话串话)
state.queue.push({
text: text.trim(),
skill: skill || undefined,
enqueuedAt: Date.now(),
parts: parts && parts.length > 0 ? parts : undefined,
conversationId: state.activeConversationId,
})
return
}
@@ -481,5 +496,7 @@ export function useAiSend() {
let _drainUnlisten: (() => void) | null = null
export async function initDrainQueueListener(): Promise<void> {
if (_drainUnlisten) return
_drainUnlisten = await listen('ai-drain-queue', () => { drainQueue() })
_drainUnlisten = await listen('ai-drain-queue', (event: any) => {
drainQueue(event?.payload?.conversationId ?? null)
})
}