feat(chat): handle retry SSE event

main
sp mac bookpro 2605 2026-05-29 20:56:40 +08:00
parent cb115f1a88
commit b6090f0897
2 changed files with 109 additions and 6 deletions

View File

@ -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;

View File

@ -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() {
<div style={{ marginBottom: 24 }}>
<div className="bubble assistant">
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
{!!streaming.retryInfo?.message && (
<div
style={{
padding: '8px 10px',
borderRadius: 10,
background: 'rgba(59, 130, 246, 0.08)',
border: '1px solid rgba(59, 130, 246, 0.18)'
}}
>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 10 }}>
<span style={{ fontSize: 12.5, color: 'var(--color-text)', fontWeight: 600 }}>
{streaming.retryInfo.stage === 'fallback_model' ? '自动切换模型' : '自动重试'}
</span>
{streaming.retryInfo.stage === 'fallback_model' ? (
<Tag color="processing" style={{ marginInlineEnd: 0 }}>
{String(streaming.retryInfo.fromModel || '')} {String(streaming.retryInfo.toModel || '')}
</Tag>
) : (
<Tag color="processing" style={{ marginInlineEnd: 0 }}>
{String(streaming.retryInfo.model || '')}
{streaming.retryInfo.attempt ? ` · 第${streaming.retryInfo.attempt}` : ''}
</Tag>
)}
</div>
<div style={{ marginTop: 4, fontSize: 12.5, color: 'var(--color-text-secondary)', lineHeight: 1.55 }}>
{String(streaming.retryInfo.message)}
</div>
{!!streaming.retryInfo.reason && (
<div style={{ marginTop: 4, fontSize: 12, color: 'var(--color-text-tertiary)', lineHeight: 1.5 }}>
{String(streaming.retryInfo.reason)}
</div>
)}
</div>
)}
<div>
<div style={{ fontSize: 12, color: 'var(--color-text-tertiary)', marginBottom: 4 }}>