mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-10-25 17:54:34 +08:00
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:
1 parent
cb7ba1f3cc
commit
702311cf96
2 files changed
+85
-5
No files matched your search
+11
-3
@@ -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()
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in new issue
Block a user