From b6090f0897ca4bd3623a7e882dc92149c502e3c9 Mon Sep 17 00:00:00 2001 From: sp mac bookpro 2605 Date: Fri, 29 May 2026 20:56:40 +0800 Subject: [PATCH] feat(chat): handle retry SSE event --- src/api.ts | 4 ++ src/pages/ChatPage.tsx | 111 ++++++++++++++++++++++++++++++++++++++--- 2 files changed, 109 insertions(+), 6 deletions(-) diff --git a/src/api.ts b/src/api.ts index 20a8db5..493d518 100644 --- a/src/api.ts +++ b/src/api.ts @@ -541,6 +541,7 @@ export interface StreamEvents { onMeta?: (data: any) => void; onReasoningDelta?: (text: string) => void; onDelta?: (text: string) => void; + onRetry?: (data: any) => void; onToolCall?: (data: { id: string; name: string; args: any }) => void; onToolResult?: (data: { id: string; name: string; result: any }) => void; onDone?: (data: { user: ChatMessage; assistant: ChatMessage }) => void; @@ -641,6 +642,9 @@ async function consumeSSE(resp: Response, h: StreamEvents, signal?: AbortSignal) case 'meta': h.onMeta?.(data); break; + case 'retry': + h.onRetry?.(data); + break; case 'reasoning_delta': h.onReasoningDelta?.(data.content || ''); break; diff --git a/src/pages/ChatPage.tsx b/src/pages/ChatPage.tsx index 7f1f296..7d702d0 100644 --- a/src/pages/ChatPage.tsx +++ b/src/pages/ChatPage.tsx @@ -28,6 +28,7 @@ interface StreamingState { reasoningText: string; answerText: string; errorMessage: string | null; + retryInfo: any | null; retrieved: RetrievedSnippet[]; toolCalls: ToolCallTrace[]; } @@ -66,6 +67,7 @@ export default function ChatPage() { reasoningText: '', answerText: '', errorMessage: null, + retryInfo: null, retrieved: [], toolCalls: [] }); @@ -209,6 +211,7 @@ export default function ChatPage() { reasoningText: '', answerText: '', errorMessage: null, + retryInfo: null, retrieved: [], toolCalls: [] }); @@ -228,6 +231,12 @@ export default function ChatPage() { content, { onMeta: (m) => setStreaming((s) => ({ ...s, retrieved: m.retrieved || [] })), + onRetry: (data) => { + setStreaming((s) => ({ ...s, retryInfo: data })); + if (data?.stage === 'fallback_model' && data?.toModel) { + setOverrides((o) => ({ ...o, model: String(data.toModel) })); + } + }, onReasoningDelta: (chunk) => setStreaming((s) => { const next = { ...s, reasoningText: s.reasoningText + chunk }; @@ -274,7 +283,15 @@ export default function ChatPage() { } }; setMessages((m) => [...(m || []).filter((x) => x.id !== tempUser.id), data.user, nextAssistant]); - return { active: false, reasoningText: '', answerText: '', errorMessage: null, retrieved: [], toolCalls: [] }; + return { + active: false, + reasoningText: '', + answerText: '', + errorMessage: null, + retryInfo: null, + retrieved: [], + toolCalls: [] + }; }); setSessionRefresh((t) => t + 1); setAttachments([]); // 用完即清 @@ -295,7 +312,15 @@ export default function ChatPage() { { ...tempUser, id: 'u-' + data.assistant.id }, // 占位 assistant ]); - setStreaming({ active: false, reasoningText: '', answerText: '', errorMessage: null, retrieved: [], toolCalls: [] }); + setStreaming({ + active: false, + reasoningText: '', + answerText: '', + errorMessage: null, + retryInfo: null, + retrieved: [], + toolCalls: [] + }); setSessionRefresh((t) => t + 1); // 重新拉一次确保和后端一致(user 真实 id 在那边) loadMessages(); @@ -303,7 +328,15 @@ export default function ChatPage() { onError: (errMsg) => { msg.error('流式失败:' + errMsg); setMessages((m) => (m || []).filter((x) => x.id !== tempUser.id)); - setStreaming({ active: false, reasoningText: '', answerText: '', errorMessage: errMsg, retrieved: [], toolCalls: [] }); + setStreaming({ + active: false, + reasoningText: '', + answerText: '', + errorMessage: errMsg, + retryInfo: null, + retrieved: [], + toolCalls: [] + }); } }, ctrl.signal, @@ -321,6 +354,7 @@ export default function ChatPage() { reasoningText: '', answerText: '', errorMessage: e?.message ?? String(e), + retryInfo: null, retrieved: [], toolCalls: [] }); @@ -393,6 +427,7 @@ export default function ChatPage() { reasoningText: '', answerText: '', errorMessage: null, + retryInfo: null, retrieved: [], toolCalls: [] }); @@ -405,6 +440,12 @@ export default function ChatPage() { assistantId, { onMeta: (m) => setStreaming((s) => ({ ...s, retrieved: m.retrieved || [] })), + onRetry: (data) => { + setStreaming((s) => ({ ...s, retryInfo: data })); + if (data?.stage === 'fallback_model' && data?.toModel) { + setOverrides((o) => ({ ...o, model: String(data.toModel) })); + } + }, onReasoningDelta: (chunk) => setStreaming((s) => { const next = { ...s, reasoningText: s.reasoningText + chunk }; @@ -434,16 +475,40 @@ export default function ChatPage() { return { ...s, toolCalls: list }; }), onDone: () => { - setStreaming({ active: false, reasoningText: '', answerText: '', errorMessage: null, retrieved: [], toolCalls: [] }); + setStreaming({ + active: false, + reasoningText: '', + answerText: '', + errorMessage: null, + retryInfo: null, + retrieved: [], + toolCalls: [] + }); loadMessages(); }, onAborted: () => { - setStreaming({ active: false, reasoningText: '', answerText: '', errorMessage: null, retrieved: [], toolCalls: [] }); + setStreaming({ + active: false, + reasoningText: '', + answerText: '', + errorMessage: null, + retryInfo: null, + retrieved: [], + toolCalls: [] + }); loadMessages(); }, onError: (errMsg) => { msg.error('重新生成失败:' + errMsg); - setStreaming({ active: false, reasoningText: '', answerText: '', errorMessage: errMsg, retrieved: [], toolCalls: [] }); + setStreaming({ + active: false, + reasoningText: '', + answerText: '', + errorMessage: errMsg, + retryInfo: null, + retrieved: [], + toolCalls: [] + }); loadMessages(); } }, @@ -655,6 +720,40 @@ export default function ChatPage() {
+ {!!streaming.retryInfo?.message && ( +
+
+ + {streaming.retryInfo.stage === 'fallback_model' ? '自动切换模型' : '自动重试'} + + {streaming.retryInfo.stage === 'fallback_model' ? ( + + {String(streaming.retryInfo.fromModel || '')} → {String(streaming.retryInfo.toModel || '')} + + ) : ( + + {String(streaming.retryInfo.model || '')} + {streaming.retryInfo.attempt ? ` · 第${streaming.retryInfo.attempt}次` : ''} + + )} +
+
+ {String(streaming.retryInfo.message)} +
+ {!!streaming.retryInfo.reason && ( +
+ {String(streaming.retryInfo.reason)} +
+ )} +
+ )}
推理过程