feat(chat): handle retry SSE event
parent
cb115f1a88
commit
b6090f0897
|
|
@ -541,6 +541,7 @@ export interface StreamEvents {
|
||||||
onMeta?: (data: any) => void;
|
onMeta?: (data: any) => void;
|
||||||
onReasoningDelta?: (text: string) => void;
|
onReasoningDelta?: (text: string) => void;
|
||||||
onDelta?: (text: string) => void;
|
onDelta?: (text: string) => void;
|
||||||
|
onRetry?: (data: any) => void;
|
||||||
onToolCall?: (data: { id: string; name: string; args: any }) => void;
|
onToolCall?: (data: { id: string; name: string; args: any }) => void;
|
||||||
onToolResult?: (data: { id: string; name: string; result: any }) => void;
|
onToolResult?: (data: { id: string; name: string; result: any }) => void;
|
||||||
onDone?: (data: { user: ChatMessage; assistant: ChatMessage }) => void;
|
onDone?: (data: { user: ChatMessage; assistant: ChatMessage }) => void;
|
||||||
|
|
@ -641,6 +642,9 @@ async function consumeSSE(resp: Response, h: StreamEvents, signal?: AbortSignal)
|
||||||
case 'meta':
|
case 'meta':
|
||||||
h.onMeta?.(data);
|
h.onMeta?.(data);
|
||||||
break;
|
break;
|
||||||
|
case 'retry':
|
||||||
|
h.onRetry?.(data);
|
||||||
|
break;
|
||||||
case 'reasoning_delta':
|
case 'reasoning_delta':
|
||||||
h.onReasoningDelta?.(data.content || '');
|
h.onReasoningDelta?.(data.content || '');
|
||||||
break;
|
break;
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ interface StreamingState {
|
||||||
reasoningText: string;
|
reasoningText: string;
|
||||||
answerText: string;
|
answerText: string;
|
||||||
errorMessage: string | null;
|
errorMessage: string | null;
|
||||||
|
retryInfo: any | null;
|
||||||
retrieved: RetrievedSnippet[];
|
retrieved: RetrievedSnippet[];
|
||||||
toolCalls: ToolCallTrace[];
|
toolCalls: ToolCallTrace[];
|
||||||
}
|
}
|
||||||
|
|
@ -66,6 +67,7 @@ export default function ChatPage() {
|
||||||
reasoningText: '',
|
reasoningText: '',
|
||||||
answerText: '',
|
answerText: '',
|
||||||
errorMessage: null,
|
errorMessage: null,
|
||||||
|
retryInfo: null,
|
||||||
retrieved: [],
|
retrieved: [],
|
||||||
toolCalls: []
|
toolCalls: []
|
||||||
});
|
});
|
||||||
|
|
@ -209,6 +211,7 @@ export default function ChatPage() {
|
||||||
reasoningText: '',
|
reasoningText: '',
|
||||||
answerText: '',
|
answerText: '',
|
||||||
errorMessage: null,
|
errorMessage: null,
|
||||||
|
retryInfo: null,
|
||||||
retrieved: [],
|
retrieved: [],
|
||||||
toolCalls: []
|
toolCalls: []
|
||||||
});
|
});
|
||||||
|
|
@ -228,6 +231,12 @@ export default function ChatPage() {
|
||||||
content,
|
content,
|
||||||
{
|
{
|
||||||
onMeta: (m) => setStreaming((s) => ({ ...s, retrieved: m.retrieved || [] })),
|
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) =>
|
onReasoningDelta: (chunk) =>
|
||||||
setStreaming((s) => {
|
setStreaming((s) => {
|
||||||
const next = { ...s, reasoningText: s.reasoningText + chunk };
|
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]);
|
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);
|
setSessionRefresh((t) => t + 1);
|
||||||
setAttachments([]); // 用完即清
|
setAttachments([]); // 用完即清
|
||||||
|
|
@ -295,7 +312,15 @@ export default function ChatPage() {
|
||||||
{ ...tempUser, id: 'u-' + data.assistant.id }, // 占位
|
{ ...tempUser, id: 'u-' + data.assistant.id }, // 占位
|
||||||
assistant
|
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);
|
setSessionRefresh((t) => t + 1);
|
||||||
// 重新拉一次确保和后端一致(user 真实 id 在那边)
|
// 重新拉一次确保和后端一致(user 真实 id 在那边)
|
||||||
loadMessages();
|
loadMessages();
|
||||||
|
|
@ -303,7 +328,15 @@ export default function ChatPage() {
|
||||||
onError: (errMsg) => {
|
onError: (errMsg) => {
|
||||||
msg.error('流式失败:' + errMsg);
|
msg.error('流式失败:' + errMsg);
|
||||||
setMessages((m) => (m || []).filter((x) => x.id !== tempUser.id));
|
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,
|
ctrl.signal,
|
||||||
|
|
@ -321,6 +354,7 @@ export default function ChatPage() {
|
||||||
reasoningText: '',
|
reasoningText: '',
|
||||||
answerText: '',
|
answerText: '',
|
||||||
errorMessage: e?.message ?? String(e),
|
errorMessage: e?.message ?? String(e),
|
||||||
|
retryInfo: null,
|
||||||
retrieved: [],
|
retrieved: [],
|
||||||
toolCalls: []
|
toolCalls: []
|
||||||
});
|
});
|
||||||
|
|
@ -393,6 +427,7 @@ export default function ChatPage() {
|
||||||
reasoningText: '',
|
reasoningText: '',
|
||||||
answerText: '',
|
answerText: '',
|
||||||
errorMessage: null,
|
errorMessage: null,
|
||||||
|
retryInfo: null,
|
||||||
retrieved: [],
|
retrieved: [],
|
||||||
toolCalls: []
|
toolCalls: []
|
||||||
});
|
});
|
||||||
|
|
@ -405,6 +440,12 @@ export default function ChatPage() {
|
||||||
assistantId,
|
assistantId,
|
||||||
{
|
{
|
||||||
onMeta: (m) => setStreaming((s) => ({ ...s, retrieved: m.retrieved || [] })),
|
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) =>
|
onReasoningDelta: (chunk) =>
|
||||||
setStreaming((s) => {
|
setStreaming((s) => {
|
||||||
const next = { ...s, reasoningText: s.reasoningText + chunk };
|
const next = { ...s, reasoningText: s.reasoningText + chunk };
|
||||||
|
|
@ -434,16 +475,40 @@ export default function ChatPage() {
|
||||||
return { ...s, toolCalls: list };
|
return { ...s, toolCalls: list };
|
||||||
}),
|
}),
|
||||||
onDone: () => {
|
onDone: () => {
|
||||||
setStreaming({ active: false, reasoningText: '', answerText: '', errorMessage: null, retrieved: [], toolCalls: [] });
|
setStreaming({
|
||||||
|
active: false,
|
||||||
|
reasoningText: '',
|
||||||
|
answerText: '',
|
||||||
|
errorMessage: null,
|
||||||
|
retryInfo: null,
|
||||||
|
retrieved: [],
|
||||||
|
toolCalls: []
|
||||||
|
});
|
||||||
loadMessages();
|
loadMessages();
|
||||||
},
|
},
|
||||||
onAborted: () => {
|
onAborted: () => {
|
||||||
setStreaming({ active: false, reasoningText: '', answerText: '', errorMessage: null, retrieved: [], toolCalls: [] });
|
setStreaming({
|
||||||
|
active: false,
|
||||||
|
reasoningText: '',
|
||||||
|
answerText: '',
|
||||||
|
errorMessage: null,
|
||||||
|
retryInfo: null,
|
||||||
|
retrieved: [],
|
||||||
|
toolCalls: []
|
||||||
|
});
|
||||||
loadMessages();
|
loadMessages();
|
||||||
},
|
},
|
||||||
onError: (errMsg) => {
|
onError: (errMsg) => {
|
||||||
msg.error('重新生成失败:' + 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();
|
loadMessages();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -655,6 +720,40 @@ export default function ChatPage() {
|
||||||
<div style={{ marginBottom: 24 }}>
|
<div style={{ marginBottom: 24 }}>
|
||||||
<div className="bubble assistant">
|
<div className="bubble assistant">
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
|
<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>
|
||||||
<div style={{ fontSize: 12, color: 'var(--color-text-tertiary)', marginBottom: 4 }}>
|
<div style={{ fontSize: 12, color: 'var(--color-text-tertiary)', marginBottom: 4 }}>
|
||||||
推理过程
|
推理过程
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue