修复: DeepSeek 400 全量扫描 + 队列 per-conv 隔离
- openai_compat: 扫描所有 assistant 消息剥离 orphan tool_calls(原仅查末条) - queue 加 conversationId 字段,按会话精准 drain - regenerate/editMessage 只清本会话排队消息 - newConversation 保留旧会话排队消息 - AiError 只清出错会话的队列项
This commit is contained in:
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user