fix(extraction): add SSE stream idle timeout and vector sync timeout to prevent extraction hang

Root cause analysis: extraction can appear stuck at '正在提取xx-xx楼层' indefinitely
when the LLM API connection goes half-open (server stops sending data but keeps the
TCP connection alive). The SSE stream reader.read() would block forever with no
per-chunk idle timeout.

Changes:
1. llm/llm.js: Add LLM_STREAM_IDLE_TIMEOUT_MS (90s default) to
   parseDedicatedStreamingResponse. When no SSE data is received for 90 seconds,
   the read loop aborts with a clear timeout error instead of hanging forever.
   The idle timeout is configurable per-request (defaults to 30% of config timeout,
   minimum 30s).

2. index.js: Add EXTRACTION_VECTOR_SYNC_TIMEOUT_MS (120s) timeout wrapper around
   syncVectorState in handleExtractionSuccess. Vector sync now uses a combined
   AbortSignal (extraction signal + timeout) so that either user abort or 120s
   timeout will break out. Vector sync timeout is treated as non-fatal (doesn't
   abort the entire extraction batch).
This commit is contained in:
Youzini-afk
2026-04-29 02:42:50 +08:00
parent cb7ce45572
commit 0749ef29c5
2 changed files with 85 additions and 4 deletions

View File

@@ -1274,6 +1274,7 @@ let isRecoveringHistory = false;
let lastRecallFallbackNoticeAt = 0;
let lastExtractionWarningAt = 0;
const LOCAL_VECTOR_TIMEOUT_MS = 300000;
const EXTRACTION_VECTOR_SYNC_TIMEOUT_MS = 120000;
const STATUS_TOAST_THROTTLE_MS = 1500;
const RECALL_INPUT_RECORD_TTL_MS = 60000;
const TRIVIAL_GENERATION_SKIP_TTL_MS = 60000;
@@ -19953,9 +19954,36 @@ async function handleExtractionSuccess(
"向量同步中",
"正在同步本批提取后的向量索引",
);
vectorSync = await syncVectorState({ signal });
const vectorSyncTimeoutController = new AbortController();
const vectorSyncTimeout = setTimeout(
() => vectorSyncTimeoutController.abort(
new DOMException(
`向量同步超时 (${Math.round(EXTRACTION_VECTOR_SYNC_TIMEOUT_MS / 1000)}s)`,
"AbortError",
),
),
EXTRACTION_VECTOR_SYNC_TIMEOUT_MS,
);
let vectorSyncSignal = vectorSyncTimeoutController.signal;
if (signal) {
try {
if (typeof AbortSignal.any === "function") {
vectorSyncSignal = AbortSignal.any([signal, vectorSyncTimeoutController.signal]);
}
} catch {}
}
try {
vectorSync = await syncVectorState({ signal: vectorSyncSignal });
} finally {
clearTimeout(vectorSyncTimeout);
}
} catch (error) {
if (isAbortError(error)) throw error;
if (isAbortError(error)) {
const isVectorSyncTimeout = error?.name === "AbortError" &&
typeof error?.message === "string" &&
error.message.includes("向量同步超时");
if (!isVectorSyncTimeout) throw error;
}
const message = error?.message || String(error) || "向量同步阶段失败";
setBatchStageOutcome(
status,