fix: IndexedDB probe 失败后不再永久卡在 loading,重试耗尽后回退到 blocked

- 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
This commit is contained in:
Youzini-afk
2026-04-12 16:49:48 +08:00
parent f192a7d23a
commit b31088cc35
3 changed files with 152 additions and 4 deletions

View File

@@ -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 } = {},

View File

@@ -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",

View File

@@ -9913,7 +9913,7 @@ function _getGraphLoadLabel(loadState = "") {
case "empty-confirmed":
return "当前聊天还没有图谱";
case "blocked":
return "聊天元数据未就绪,已暂停图谱写回以保护旧数据";
return "当前聊天图谱未能完成 IndexedDB 确认,请稍后重试";
case "loaded":
return "聊天图谱已加载";
case "no-chat":