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:
2026-03-09 18:45:12 +08:00
parent 50657dd81f
commit 5c5fec52c1
27 changed files with 807 additions and 100 deletions

View File

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