fix(aia,ssa,asl,infra): harden SSE transport and stabilize attachment context
Deliver SSE protocol hardening for SAE/HTTP2 paths, add graceful shutdown health behavior, and improve SSA retry UX for transient stream failures. For AIA, persist attachment extraction results in database with cache read-through fallback, plus production cache safety guard to prevent memory-cache drift in multi-instance deployments; also restore ASL SR page scrolling behavior. Made-with: Cursor
This commit is contained in:
@@ -77,6 +77,38 @@ export interface UseSSAChatReturn {
|
||||
loadHistory: (sessionId: string) => Promise<void>;
|
||||
abort: () => void;
|
||||
clearMessages: () => void;
|
||||
retryLastMessage: () => Promise<void>;
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────
|
||||
// Helpers
|
||||
// ────────────────────────────────────────────
|
||||
|
||||
const MAX_AUTO_RETRY = 2;
|
||||
|
||||
function retryDelay(attempt: number): number {
|
||||
return Math.min(1000 * (2 ** attempt), 5000);
|
||||
}
|
||||
|
||||
function toFriendlyError(err: any): string {
|
||||
const msg = (err?.message || '').toLowerCase();
|
||||
|
||||
if (msg.includes('failed to fetch') || msg.includes('networkerror') || msg.includes('network'))
|
||||
return '网络连接不稳定,请稍后重试。';
|
||||
|
||||
if (msg.includes('http2') || msg.includes('protocol'))
|
||||
return '网络链路出现瞬时波动,请重新发送消息。';
|
||||
|
||||
if (msg.includes('timeout') || msg.includes('timed out'))
|
||||
return '请求超时,服务器响应较慢,请稍后重试。';
|
||||
|
||||
if (msg.includes('502') || msg.includes('503') || msg.includes('504'))
|
||||
return '服务暂时不可用,可能正在更新中,请稍后重试。';
|
||||
|
||||
if (msg.includes('401') || msg.includes('登录'))
|
||||
return '登录已过期,请刷新页面重新登录。';
|
||||
|
||||
return err?.message || '请求失败,请重试。';
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────
|
||||
@@ -95,6 +127,8 @@ export function useSSAChat(): UseSSAChatReturn {
|
||||
const [pendingQuestion, setPendingQuestion] = useState<AskUserEventData | null>(null);
|
||||
|
||||
const abortRef = useRef<AbortController | null>(null);
|
||||
const lastRequestRef = useRef<{ sessionId: string; content: string; metadata?: Record<string, any> } | null>(null);
|
||||
const retryCountRef = useRef(0);
|
||||
|
||||
const ensureFreshToken = useCallback(async (): Promise<string> => {
|
||||
if (isTokenExpired()) {
|
||||
@@ -186,6 +220,7 @@ export function useSSAChat(): UseSSAChatReturn {
|
||||
* 发送消息并接收 SSE 流式响应
|
||||
*/
|
||||
const sendChatMessage = useCallback(async (sessionId: string, content: string, metadata?: Record<string, any>) => {
|
||||
lastRequestRef.current = { sessionId, content, metadata };
|
||||
setError(null);
|
||||
setIsGenerating(true);
|
||||
setThinkingContent('');
|
||||
@@ -454,11 +489,32 @@ export function useSSAChat(): UseSSAChatReturn {
|
||||
? { ...m, content: fullContent || '(已中断)', status: 'complete' }
|
||||
: m
|
||||
));
|
||||
retryCountRef.current = 0;
|
||||
} else {
|
||||
const errMsg = err.message || '请求失败';
|
||||
setError(errMsg);
|
||||
const isNetworkError = /failed to fetch|networkerror|network|http2|protocol/i.test(err.message || '');
|
||||
|
||||
// 瞬时网络错误自动重试一次
|
||||
if (isNetworkError && retryCountRef.current < MAX_AUTO_RETRY) {
|
||||
retryCountRef.current += 1;
|
||||
const delay = retryDelay(retryCountRef.current);
|
||||
setChatMessages(prev => prev.map(m =>
|
||||
m.id === assistantMsgId
|
||||
? { ...m, content: `⏳ 网络波动,${(delay / 1000).toFixed(1)}s 后自动重试(第 ${retryCountRef.current}/${MAX_AUTO_RETRY} 次)...`, status: 'generating' }
|
||||
: m
|
||||
));
|
||||
setIsGenerating(false);
|
||||
abortRef.current = null;
|
||||
await new Promise(r => setTimeout(r, delay));
|
||||
return sendChatMessage(sessionId, content, metadata);
|
||||
}
|
||||
|
||||
retryCountRef.current = 0;
|
||||
const friendlyMsg = toFriendlyError(err);
|
||||
setError(friendlyMsg);
|
||||
setChatMessages(prev => prev.map(m =>
|
||||
m.id === assistantMsgId ? { ...m, content: errMsg, status: 'error' } : m
|
||||
m.id === assistantMsgId
|
||||
? { ...m, content: `⚠️ ${friendlyMsg}`, status: 'error' }
|
||||
: m
|
||||
));
|
||||
}
|
||||
} finally {
|
||||
@@ -535,6 +591,20 @@ export function useSSAChat(): UseSSAChatReturn {
|
||||
await sendChatMessage(sessionId, '已跳过此问题', { askUserResponse: skipResponse });
|
||||
}, [sendChatMessage]);
|
||||
|
||||
const retryLastMessage = useCallback(async () => {
|
||||
const last = lastRequestRef.current;
|
||||
if (!last) return;
|
||||
retryCountRef.current = 0;
|
||||
setChatMessages(prev => {
|
||||
let lastErrIdx = -1;
|
||||
for (let i = prev.length - 1; i >= 0; i--) {
|
||||
if (prev[i].status === 'error') { lastErrIdx = i; break; }
|
||||
}
|
||||
return lastErrIdx >= 0 ? prev.filter((_: ChatMessage, i: number) => i !== lastErrIdx) : prev;
|
||||
});
|
||||
await sendChatMessage(last.sessionId, last.content, last.metadata);
|
||||
}, [sendChatMessage]);
|
||||
|
||||
return {
|
||||
chatMessages,
|
||||
isGenerating,
|
||||
@@ -552,6 +622,7 @@ export function useSSAChat(): UseSSAChatReturn {
|
||||
loadHistory,
|
||||
abort,
|
||||
clearMessages,
|
||||
retryLastMessage,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user