diff --git a/src/stores/aiChat.ts b/src/stores/aiChat.ts index f107ed2e..555e7b3c 100644 --- a/src/stores/aiChat.ts +++ b/src/stores/aiChat.ts @@ -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() diff --git a/src/stores/aiChatNavigation.test.ts b/src/stores/aiChatNavigation.test.ts index dae25dd1..78acb39a 100644 --- a/src/stores/aiChatNavigation.test.ts +++ b/src/stores/aiChatNavigation.test.ts @@ -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 }) + | 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',