feat(chat): handle retry SSE event
parent
cb115f1a88
commit
b6090f0897
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 }}>
|
||||
推理过程
|
||||
|
|
|
|||
Loading…
Reference in New Issue