fix(ai): preserve stream errors through completion

中文:错误流的 done 只终止传输,不再覆盖错误状态或误报发送成功。

English: Treat done as stream termination after an error, preserving the error phase and returning failed sends correctly.
This commit is contained in:
Sanssssssssssssssss authored and digua committed 2026-08-26 22:23:21 +08:00
1 parent cb7ba1f3cc
commit 702311cf96
2 files changed
+85 -5

No files matched your search

+11 -3
View File
@@ -1379,7 +1379,11 @@ export const useAIChatStore = defineStore('aiChatRuntime', () => {
cacheWriteTokens: state.sessionTokenUsage.cacheWriteTokens + chunk.usage.cacheWriteTokens,
}
}
setAgentPhase(state, 'completed', chunk.usage ? { totalUsage: chunk.usage } : undefined)
setAgentPhase(
state,
hasStreamError ? 'error' : 'completed',
chunk.usage ? { totalUsage: chunk.usage } : undefined
)
break
case 'error':
@@ -1496,7 +1500,7 @@ export const useAIChatStore = defineStore('aiChatRuntime', () => {
}
}
return { success: true }
return result.success ? { success: true } : { success: false, reason: 'error' }
} catch (error) {
console.error('[AI] 处理失败:', error)
state.agentStatus = null
@@ -1952,7 +1956,11 @@ export const useAIChatStore = defineStore('aiChatRuntime', () => {
state.currentToolStatus = null
if (!hasStreamError) updatePlanBlockStatus('done')
if (chunk.usage) lastDoneUsage = { ...chunk.usage }
setAgentPhase(state, 'completed', chunk.usage ? { totalUsage: chunk.usage } : undefined)
setAgentPhase(
state,
hasStreamError ? 'error' : 'completed',
chunk.usage ? { totalUsage: chunk.usage } : undefined
)
break
case 'error': {
settleProcessDuration()
+74 -2
View File
@@ -1,10 +1,15 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import type { AgentStreamChunk, AgentStreamResult } from '@/services/ai-stream/types'
import { createPinia, setActivePinia } from 'pinia'
import { ref } from 'vue'
test('restores the latest valid AI chat without leaking stale navigation state', async (t) => {
let hasLLMConfig = false
let agentStreamRun:
| ((onChunk?: (chunk: AgentStreamChunk) => void) => { requestId: string; promise: Promise<AgentStreamResult> })
| null = null
let savedMessageId = 0
const makeConversation = (id: string, sessionId = 'session-one') => ({
id,
sessionId,
@@ -56,6 +61,24 @@ test('restores the latest valid AI chat without leaking stale navigation state',
createAIChat: async () => {
throw new Error('create failed')
},
addMessage: async (
aiChatId: string,
role: 'user' | 'assistant',
content: string,
_dataKeywords?: string[],
_dataMessageCount?: number,
contentBlocks?: unknown,
_tokenUsage?: unknown,
entityRefs?: unknown
) => ({
id: `saved-${++savedMessageId}`,
aiChatId,
role,
content,
timestamp: 1,
contentBlocks,
entityRefs,
}),
}
const assistantStore = {
isLoaded: true,
@@ -69,7 +92,20 @@ test('restores the latest valid AI chat without leaking stale navigation state',
namedExports: { useSessionStore: () => ({ sessions: [] }) },
}),
t.mock.module('@/stores/settings', {
namedExports: { useSettingsStore: () => ({ aiPreprocessConfig: {} }) },
namedExports: {
useSettingsStore: () => ({
aiPreprocessConfig: {
dataCleaning: false,
mergeConsecutive: false,
mergeWindowSeconds: 180,
blacklistKeywords: [],
denoise: false,
desensitize: false,
desensitizeRules: [],
anonymizeNames: false,
},
}),
},
}),
t.mock.module('@/stores/assistant', {
namedExports: { useAssistantStore: () => assistantStore },
@@ -88,7 +124,14 @@ test('restores the latest valid AI chat without leaking stale navigation state',
},
}),
t.mock.module('@/services/ai-stream/service', {
namedExports: { useAgentStreamService: () => ({}) },
namedExports: {
useAgentStreamService: () => ({
runStream: (_params: unknown, onChunk?: (chunk: AgentStreamChunk) => void) => {
if (!agentStreamRun) throw new Error('agent stream is not configured')
return agentStreamRun(onChunk)
},
}),
},
}),
])
@@ -183,6 +226,35 @@ test('restores the latest valid AI chat without leaking stale navigation state',
true
)
const streamError = { name: 'ProviderError', message: 'provider unavailable', stack: null }
agentStreamRun = (onChunk) => {
onChunk?.({ type: 'error', error: streamError })
onChunk?.({
type: 'done',
isFinished: true,
usage: { promptTokens: 1, completionTokens: 2, totalTokens: 3, cacheReadTokens: 0, cacheWriteTokens: 0 },
})
return {
requestId: 'stream-error',
promise: Promise.resolve({ success: false, error: streamError }),
}
}
const streamFailureResult = await store.sendMessage(first.chatKey, 'Trigger provider failure')
const failedAssistant = first.state.messages.findLast((message) => message.role === 'assistant')
assert.deepEqual(
{
result: streamFailureResult,
phase: first.state.agentStatus?.phase,
errorBlocks: failedAssistant?.contentBlocks?.filter((block) => block.type === 'error').length,
},
{
result: { success: false, reason: 'error' },
phase: 'error',
errorBlocks: 1,
}
)
const global = store.ensureGlobalState('zh-CN')
store.activeTask = {
requestId: 'global-request',