fix: surface rerun preparation failures

This commit is contained in:
Youzini-afk
2026-04-16 01:27:07 +08:00
parent 69ab525cc7
commit fbaadddae3
2 changed files with 167 additions and 2 deletions

145
index.js
View File

@@ -16089,14 +16089,30 @@ async function rollbackGraphForReroll(targetFloor, context = getContext()) {
isBackendVectorConfig(config) && isBackendVectorConfig(config) &&
recoveryPlan.backendDeleteHashes.length > 0 recoveryPlan.backendDeleteHashes.length > 0
) { ) {
setRuntimeStatus(
"重新提取准备中",
`正在整理向量恢复状态(${recoveryPlan.backendDeleteHashes.length} 项)`,
"running",
);
assertRecoveryChatStillActive(chatId, "reroll-pre-vector"); assertRecoveryChatStillActive(chatId, "reroll-pre-vector");
await deleteBackendVectorHashesForRecovery( await tryDeleteBackendVectorHashesForRecovery(
currentGraph.vectorIndexState.collectionId, currentGraph.vectorIndexState.collectionId,
config, config,
recoveryPlan.backendDeleteHashes, recoveryPlan.backendDeleteHashes,
undefined,
{
source: "reroll",
},
); );
} }
if (isBackendVectorConfig(config)) {
setRuntimeStatus(
"重新提取准备中",
"正在准备向量回放状态",
"running",
);
}
assertRecoveryChatStillActive(chatId, "reroll-pre-prepare"); assertRecoveryChatStillActive(chatId, "reroll-pre-prepare");
await prepareVectorStateForReplay(false, undefined, { await prepareVectorStateForReplay(false, undefined, {
skipBackendPurge: isBackendVectorConfig(config), skipBackendPurge: isBackendVectorConfig(config),
@@ -16185,6 +16201,106 @@ async function rollbackGraphForReroll(targetFloor, context = getContext()) {
}; };
} }
const VECTOR_RECOVERY_PREP_TIMEOUT_MS = 15000;
async function tryDeleteBackendVectorHashesForRecovery(
collectionId,
config,
hashes,
signal = undefined,
{ source = "recovery" } = {},
) {
if (
!collectionId ||
!isBackendVectorConfig(config) ||
!Array.isArray(hashes) ||
hashes.length === 0
) {
return {
ok: true,
skipped: true,
reason: "no-backend-hashes",
};
}
const canAbortWithTimeout =
typeof AbortController !== "undefined" &&
typeof DOMException !== "undefined";
const controller = canAbortWithTimeout ? new AbortController() : null;
const timeout = controller
? setTimeout(
() =>
controller.abort(
new DOMException(
`向量恢复准备超时 (${Math.round(VECTOR_RECOVERY_PREP_TIMEOUT_MS / 1000)}s)`,
"AbortError",
),
),
VECTOR_RECOVERY_PREP_TIMEOUT_MS,
)
: null;
let combinedSignal = controller?.signal;
if (signal && controller) {
if (
typeof AbortSignal !== "undefined" &&
typeof AbortSignal.any === "function"
) {
combinedSignal = AbortSignal.any([signal, controller.signal]);
} else {
combinedSignal = controller.signal;
signal.addEventListener(
"abort",
() => controller.abort(signal.reason),
{ once: true },
);
}
} else if (signal) {
combinedSignal = signal;
}
try {
await deleteBackendVectorHashesForRecovery(
collectionId,
config,
hashes,
combinedSignal,
);
return {
ok: true,
skipped: false,
reason: "",
};
} catch (error) {
if (isAbortError(error) && signal?.aborted) {
throw error;
}
console.warn("[ST-BME] 向量恢复预清理失败,已降级为后续修复:", {
source,
collectionId,
hashCount: hashes.length,
error,
});
if (currentGraph?.vectorIndexState) {
currentGraph.vectorIndexState.dirty = true;
currentGraph.vectorIndexState.dirtyReason =
currentGraph.vectorIndexState.dirtyReason ||
"history-recovery-replay";
currentGraph.vectorIndexState.lastWarning =
"向量恢复预清理失败,已跳过并标记为后续修复";
}
return {
ok: false,
skipped: false,
reason: error?.message || String(error),
error,
};
} finally {
if (timeout) {
clearTimeout(timeout);
}
}
}
async function recoverHistoryIfNeeded(trigger = "history-recovery") { async function recoverHistoryIfNeeded(trigger = "history-recovery") {
if (!currentGraph || isRecoveringHistory) { if (!currentGraph || isRecoveringHistory) {
return !isRecoveringHistory; return !isRecoveringHistory;
@@ -16264,12 +16380,37 @@ async function recoverHistoryIfNeeded(trigger = "history-recovery") {
isBackendVectorConfig(config) && isBackendVectorConfig(config) &&
recoveryPlan.backendDeleteHashes.length > 0 recoveryPlan.backendDeleteHashes.length > 0
) { ) {
updateStageNotice(
"history",
"历史恢复中",
`正在整理向量恢复状态(${recoveryPlan.backendDeleteHashes.length} 项)`,
"running",
{
persist: true,
busy: true,
},
);
assertRecoveryChatStillActive(chatId, "pre-backend-delete"); assertRecoveryChatStillActive(chatId, "pre-backend-delete");
await deleteBackendVectorHashesForRecovery( await tryDeleteBackendVectorHashesForRecovery(
currentGraph.vectorIndexState.collectionId, currentGraph.vectorIndexState.collectionId,
config, config,
recoveryPlan.backendDeleteHashes, recoveryPlan.backendDeleteHashes,
historySignal, historySignal,
{
source: "history-recovery",
},
);
}
if (isBackendVectorConfig(config)) {
updateStageNotice(
"history",
"历史恢复中",
"正在准备向量回放状态",
"running",
{
persist: true,
busy: true,
},
); );
} }
await prepareVectorStateForReplay(false, historySignal, { await prepareVectorStateForReplay(false, historySignal, {

View File

@@ -1187,6 +1187,30 @@ export async function onExtractionTaskController(runtime, options = {}) {
context, context,
); );
if (!rollbackResult?.success) { if (!rollbackResult?.success) {
const rollbackError = String(
rollbackResult?.error ||
rollbackResult?.reason ||
rollbackResult?.recoveryPath ||
"回滚失败",
).trim() || "回滚失败";
setExtractionProgressStatus(
runtime,
"重新提取失败",
rollbackError,
"warning",
{
syncRuntime: true,
toastKind: "",
toastTitle: "ST-BME 重新提取",
},
);
runtime.toastr?.warning?.(
`重新提取未开始:${rollbackError}`,
"ST-BME 重新提取",
{
timeOut: 4500,
},
);
return { return {
...rollbackResult, ...rollbackResult,
rerunPerformed: false, rerunPerformed: false,