From b31088cc35f7499641ebdc92b596a09c9fded736 Mon Sep 17 00:00:00 2001 From: Youzini-afk <13153778771cx@gmail.com> Date: Sun, 12 Apr 2026 16:49:48 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20IndexedDB=20probe=20=E5=A4=B1=E8=B4=A5?= =?UTF-8?q?=E5=90=8E=E4=B8=8D=E5=86=8D=E6=B0=B8=E4=B9=85=E5=8D=A1=E5=9C=A8?= =?UTF-8?q?=20loading=EF=BC=8C=E9=87=8D=E8=AF=95=E8=80=97=E5=B0=BD?= =?UTF-8?q?=E5=90=8E=E5=9B=9E=E9=80=80=E5=88=B0=20blocked?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - index.js: 新增 reconcileIndexedDbProbeFailureState,后台 probe 失败时先有限重试,耗尽后切到 blocked - index.js: scheduleIndexedDbGraphProbe 的 .then/.catch 均接入 reconcile 逻辑 - index.js: createGraphLoadUiStatus blocked 文案更新 - ui/panel.js: _getGraphLoadLabel blocked 文案更新,不再误导为元数据未就绪 - tests/graph-persistence.mjs: 新增 manager-unavailable / read-failed 回归 - tests/graph-persistence.mjs: harness 支持 __indexedDbExportSnapshotShouldThrow / __indexedDbGetCurrentDbShouldThrow --- index.js | 89 ++++++++++++++++++++++++++++++++++++- tests/graph-persistence.mjs | 65 ++++++++++++++++++++++++++- ui/panel.js | 2 +- 3 files changed, 152 insertions(+), 4 deletions(-) diff --git a/index.js b/index.js index 9d00f88..d9b3d4c 100644 --- a/index.js +++ b/index.js @@ -1029,7 +1029,7 @@ function createGraphLoadUiStatus() { case GRAPH_LOAD_STATES.BLOCKED: return createUiStatus( "图谱加载受阻", - "当前图谱尚未完成 IndexedDB 初始加载", + "当前图谱未能完成 IndexedDB 确认,请稍后重试", "warning", ); case GRAPH_LOAD_STATES.LOADED: @@ -6182,6 +6182,7 @@ async function loadGraphFromIndexedDb( function scheduleIndexedDbGraphProbe(chatId, options = {}) { const normalizedChatId = normalizeChatIdCandidate(chatId); + const attemptIndex = Math.max(0, Math.floor(Number(options?.attemptIndex) || 0)); if ( !normalizedChatId || bmeIndexedDbLoadInFlightByChatId.has(normalizedChatId) @@ -6191,8 +6192,27 @@ function scheduleIndexedDbGraphProbe(chatId, options = {}) { scheduleBmeIndexedDbTask(() => { const loadPromise = loadGraphFromIndexedDb(normalizedChatId, options) + .then((result) => + reconcileIndexedDbProbeFailureState(normalizedChatId, result, { + attemptIndex, + }), + ) .catch((error) => { console.warn("[ST-BME] IndexedDB 后台加载失败:", error); + return reconcileIndexedDbProbeFailureState( + normalizedChatId, + { + success: false, + loaded: false, + reason: "indexeddb-read-failed", + chatId: normalizedChatId, + attemptIndex, + error, + }, + { + attemptIndex, + }, + ); }) .finally(() => { if ( @@ -7852,6 +7872,73 @@ function scheduleGraphLoadRetry( return true; } +function reconcileIndexedDbProbeFailureState( + chatId, + result = {}, + { attemptIndex = 0 } = {}, +) { + if (result?.loaded || result?.emptyConfirmed) { + clearPendingGraphLoadRetry(); + return result; + } + + const normalizedChatId = normalizeChatIdCandidate(chatId || result?.chatId); + const normalizedReason = String(result?.reason || "").trim(); + if (!normalizedChatId || !normalizedReason) { + return result; + } + + if ( + !normalizedReason.startsWith("indexeddb-") || + normalizedReason === "indexeddb-stale" || + normalizedReason === "indexeddb-chat-switched" + ) { + return result; + } + + if (graphPersistenceState.loadState !== GRAPH_LOAD_STATES.LOADING) { + return result; + } + + const stateChatId = normalizeChatIdCandidate(graphPersistenceState.chatId); + if (stateChatId && stateChatId !== normalizedChatId) { + return result; + } + + const currentChatId = getCurrentChatId(); + if (currentChatId && currentChatId !== normalizedChatId) { + return result; + } + + if ( + scheduleGraphLoadRetry(normalizedChatId, normalizedReason, attemptIndex, { + expectedChatId: normalizedChatId, + }) + ) { + return { + ...result, + retryScheduled: true, + }; + } + + applyGraphLoadState(GRAPH_LOAD_STATES.BLOCKED, { + chatId: normalizedChatId, + reason: normalizedReason, + attemptIndex, + dbReady: false, + writesBlocked: true, + }); + runtimeStatus = createGraphLoadUiStatus(); + refreshPanelLiveState(); + + return { + ...result, + loadState: GRAPH_LOAD_STATES.BLOCKED, + blocked: true, + reason: normalizedReason, + }; +} + function shouldSyncGraphLoadFromLiveContext( context = getContext(), { force = false } = {}, diff --git a/tests/graph-persistence.mjs b/tests/graph-persistence.mjs index 66b5933..d57f93b 100644 --- a/tests/graph-persistence.mjs +++ b/tests/graph-persistence.mjs @@ -912,6 +912,9 @@ async function createGraphPersistenceHarness({ _createDb(dbChatId = "") { return { async exportSnapshot() { + if (runtimeContext.__indexedDbExportSnapshotShouldThrow) { + throw new Error("indexeddb-export-failed"); + } return getIndexedDbSnapshotForChat(dbChatId); }, async commitDelta(delta, options = {}) { @@ -993,6 +996,9 @@ async function createGraphPersistenceHarness({ runtimeContext.__indexedDbSnapshot = getIndexedDbSnapshotForChat( this._currentChatId, ); + if (runtimeContext.__indexedDbGetCurrentDbShouldThrow) { + throw new Error("indexeddb-get-current-db-failed"); + } return this._createDb(this._currentChatId); } async switchChat(dbChatId = "") { @@ -1224,7 +1230,6 @@ result = { harness.api.setChatContext({ chatId: "chat-late", chatMetadata: { - integrity: "chat-late-ready", st_bme_graph: lateGraph, }, characterId: "char-late", @@ -1258,7 +1263,7 @@ result = { assert.equal(result.loadState, "loading"); assert.equal( harness.api.getCurrentGraph().historyState.chatId, - "chat-late-ready", + "chat-late", ); assert.equal(harness.api.getGraphPersistenceState().dbReady, true); assert.equal( @@ -2096,6 +2101,62 @@ result = { ); } +{ + const harness = await createGraphPersistenceHarness({ + chatId: "chat-manager-unavailable-fallback", + globalChatId: "chat-manager-unavailable-fallback", + chatMetadata: { + integrity: "meta-manager-unavailable-fallback", + }, + }); + harness.runtimeContext.BmeChatManager = null; + + const result = harness.api.loadGraphFromChat({ + attemptIndex: harness.api.GRAPH_LOAD_RETRY_DELAYS_MS.length, + source: "manager-unavailable-fallback", + }); + await new Promise((resolve) => setTimeout(resolve, 0)); + + assert.equal(result.loadState, "loading"); + assert.equal( + harness.api.getGraphPersistenceState().loadState, + "blocked", + "IndexedDB manager 不可用时,重试耗尽后不应永久停留在 loading", + ); + assert.equal( + harness.api.getGraphPersistenceState().reason, + "indexeddb-manager-unavailable", + ); +} + +{ + const harness = await createGraphPersistenceHarness({ + chatId: "chat-indexeddb-read-failed-fallback", + globalChatId: "chat-indexeddb-read-failed-fallback", + chatMetadata: { + integrity: "meta-indexeddb-read-failed-fallback", + }, + }); + harness.runtimeContext.__indexedDbExportSnapshotShouldThrow = true; + + const result = harness.api.loadGraphFromChat({ + attemptIndex: harness.api.GRAPH_LOAD_RETRY_DELAYS_MS.length, + source: "indexeddb-read-failed-fallback", + }); + await new Promise((resolve) => setTimeout(resolve, 0)); + + assert.equal(result.loadState, "loading"); + assert.equal( + harness.api.getGraphPersistenceState().loadState, + "blocked", + "IndexedDB 读取失败时,重试耗尽后不应永久停留在 loading", + ); + assert.equal( + harness.api.getGraphPersistenceState().reason, + "indexeddb-read-failed", + ); +} + { const harness = await createGraphPersistenceHarness({ chatId: "chat-create-first-graph", diff --git a/ui/panel.js b/ui/panel.js index 3f5722b..acaf473 100644 --- a/ui/panel.js +++ b/ui/panel.js @@ -9913,7 +9913,7 @@ function _getGraphLoadLabel(loadState = "") { case "empty-confirmed": return "当前聊天还没有图谱"; case "blocked": - return "聊天元数据未就绪,已暂停图谱写回以保护旧数据"; + return "当前聊天图谱未能完成 IndexedDB 确认,请稍后重试"; case "loaded": return "聊天图谱已加载"; case "no-chat":