代码提交
This commit is contained in:
1 parent
f2e81d8875
commit
cebc44ff82
4 files changed
+231
-78
No files matched your search
@@ -244,6 +244,11 @@ const TERMINAL_RUN_STATUSES = new Set([
|
||||
// for a while, then reconcile it from the authoritative run/state endpoints.
|
||||
const STREAM_RECONCILIATION_IDLE_MS = 12_000;
|
||||
const STREAM_RECONCILIATION_POLL_MS = 5_000;
|
||||
// A run can legitimately be quiet while a long tool call is in progress, so
|
||||
// reconnect more conservatively than the terminal-status check above. The
|
||||
// reconnection only replaces the local SSE reader; the backend run keeps going.
|
||||
const STREAM_RECONNECT_IDLE_MS = 30_000;
|
||||
const STREAM_RECONNECT_MAX_ATTEMPTS = 3;
|
||||
|
||||
async function fetchThreadTitleRefreshState(
|
||||
threadId: string,
|
||||
@@ -466,9 +471,15 @@ export function useThreadStream({
|
||||
const startedRef = useRef(false);
|
||||
const lastStreamActivityAtRef = useRef(0);
|
||||
const streamReconciliationInFlightRef = useRef(false);
|
||||
const streamReconnectAttemptsRef = useRef(0);
|
||||
const streamReconnectToastShownRef = useRef(false);
|
||||
const streamReconnectFailureToastShownRef = useRef(false);
|
||||
|
||||
const markStreamActivity = useCallback(() => {
|
||||
lastStreamActivityAtRef.current = Date.now();
|
||||
streamReconnectAttemptsRef.current = 0;
|
||||
streamReconnectToastShownRef.current = false;
|
||||
streamReconnectFailureToastShownRef.current = false;
|
||||
}, []);
|
||||
|
||||
const listeners = useRef({
|
||||
@@ -761,13 +772,6 @@ export function useThreadStream({
|
||||
threadStreamRef.current = thread;
|
||||
|
||||
useEffect(() => {
|
||||
if (!thread.isLoading) {
|
||||
// A normal terminal event reached the browser. A later run may use the
|
||||
// same thread, so it must be eligible for recovery again.
|
||||
streamReconciliationInFlightRef.current = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const activeThreadId = threadIdRef.current ?? onStreamThreadId ?? null;
|
||||
if (!activeThreadId || typeof window === "undefined") {
|
||||
return;
|
||||
@@ -776,7 +780,7 @@ export function useThreadStream({
|
||||
let disposed = false;
|
||||
let refreshTimer: number | null = null;
|
||||
|
||||
const reconcileCompletedRun = async () => {
|
||||
const reconcileRun = async () => {
|
||||
if (
|
||||
disposed ||
|
||||
streamReconciliationInFlightRef.current ||
|
||||
@@ -793,7 +797,51 @@ export function useThreadStream({
|
||||
try {
|
||||
const client = getAPIClient(isMock);
|
||||
const run = await client.runs.get(activeThreadId, runId);
|
||||
if (disposed || !TERMINAL_RUN_STATUSES.has(run.status)) {
|
||||
if (disposed) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!TERMINAL_RUN_STATUSES.has(run.status)) {
|
||||
// ``useStream`` retries one failed connection on its own. If that
|
||||
// reader quietly stalls, however, its internal queue never gets a
|
||||
// terminal error and the page remains frozen despite the backend
|
||||
// continuing the run. Abort only the local reader, then join the
|
||||
// same run so no work or tokens are lost server-side.
|
||||
if (Date.now() - lastStreamActivityAtRef.current < STREAM_RECONNECT_IDLE_MS) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (streamReconnectAttemptsRef.current >= STREAM_RECONNECT_MAX_ATTEMPTS) {
|
||||
if (!streamReconnectFailureToastShownRef.current) {
|
||||
streamReconnectFailureToastShownRef.current = true;
|
||||
toast.error("回答仍在后台处理中,但页面连接暂未恢复;请稍后返回此会话查看结果。");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
streamReconnectAttemptsRef.current += 1;
|
||||
// Avoid enqueueing another reconnect before this new reader has had
|
||||
// enough time to receive its first event. A genuine event resets
|
||||
// the attempt counter through ``markStreamActivity``.
|
||||
lastStreamActivityAtRef.current = Date.now();
|
||||
if (!streamReconnectToastShownRef.current) {
|
||||
streamReconnectToastShownRef.current = true;
|
||||
toast.info("回答连接出现波动,正在自动恢复…");
|
||||
}
|
||||
|
||||
const stream = threadStreamRef.current;
|
||||
void Promise.resolve(stream.stop())
|
||||
.catch(() => {
|
||||
// Stopping a stale local reader is best-effort; joining is still
|
||||
// useful if the SDK already considers the stream stopped.
|
||||
})
|
||||
.finally(() => {
|
||||
if (disposed || threadIdRef.current !== activeThreadId) return;
|
||||
void stream.joinStream(runId).catch(() => {
|
||||
// The status poll will either reconcile a terminal run or
|
||||
// schedule the next bounded reconnect attempt.
|
||||
});
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -801,13 +849,17 @@ export function useThreadStream({
|
||||
// receive any more useful events. Load the checkpoint directly before
|
||||
// resetting the SDK stream; this preserves the normal onFinish side
|
||||
// effects (e.g. marking a position delivery complete) even when the
|
||||
// browser missed the terminal event.
|
||||
// browser missed the terminal event or already reported a transport
|
||||
// error and dropped ``isLoading``.
|
||||
const finalState = await client.threads.getState(activeThreadId);
|
||||
if (disposed || !threadStreamRef.current.isLoading) {
|
||||
if (disposed) {
|
||||
return;
|
||||
}
|
||||
|
||||
storage?.removeItem(`lg:stream:${activeThreadId}`);
|
||||
streamReconnectAttemptsRef.current = 0;
|
||||
streamReconnectToastShownRef.current = false;
|
||||
streamReconnectFailureToastShownRef.current = false;
|
||||
if (run.status === "success") {
|
||||
listeners.current.onFinish?.(finalState.values as AgentThreadState);
|
||||
void queryClient.invalidateQueries({ queryKey: ["threads", "search"] });
|
||||
@@ -836,7 +888,7 @@ export function useThreadStream({
|
||||
};
|
||||
|
||||
const interval = window.setInterval(
|
||||
() => void reconcileCompletedRun(),
|
||||
() => void reconcileRun(),
|
||||
STREAM_RECONCILIATION_POLL_MS,
|
||||
);
|
||||
|
||||
|
||||
Reference in new issue
Block a user