diff --git a/README.md b/README.md index cc237f2..be9a0d8 100644 --- a/README.md +++ b/README.md @@ -284,18 +284,21 @@ git clone https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology.git st-bme ``` ST-BME/ ├── index.js # 主入口:事件绑定、流程调度、历史恢复 +├── bme-db.js # IndexedDB(Dexie) 持久化层(本地主存储) +├── bme-sync.js # 跨设备同步(/user/files/ 镜像 + 合并) +├── bme-chat-manager.js# chatId -> BmeDatabase 生命周期管理 +├── lib/dexie.min.js # vendored Dexie(浏览器端动态注入加载) ├── graph.js # 图数据模型、序列化、版本迁移 ├── extractor.js # 记忆提取、概要、反思 ├── retriever.js # 向量候选、图扩散、混合评分、召回 ├── injector.js # 召回结果格式化注入 ├── runtime-state.js # 运行时状态:楼层 hash、dirty 标记、恢复日志 -├── recall-persistence.js # 持久召回记录(message.extra.bme_recall) -├── recall-message-ui.js # 消息级召回卡片 UI(子图渲染 + 侧边栏编辑) +├── recall-persistence.js # 持久召回记录(message.extra.bme_recall,不变) +├── recall-message-ui.js # 消息级召回卡片 UI(子图渲染 + 侧边栏编辑,不变) ├── vector-index.js # 向量索引管理(backend / direct 双模式) ├── embedding.js # 直连 Embedding API 封装 ├── llm.js # 记忆 LLM 请求封装 ├── compressor.js # 层级压缩与遗忘 -├── evolution.js # 记忆进化(A-MEM 风格) ├── diffusion.js # 图扩散算法 ├── dynamics.js # 动态调节(重要度衰减等) ├── schema.js # 节点类型定义 @@ -310,19 +313,40 @@ ST-BME/ ### 数据存储 -- **图谱数据** → `chat_metadata.st_bme_graph`(跟随聊天保存) +- **图谱主存储(本地优先)** → `IndexedDB`(Dexie) + - DB 名固定:`STBME_{chatId}` + - 运行时主读取路径:优先 IndexedDB +- **跨设备同步镜像** → SillyTavern 文件 API `/user/files/` + - 文件名固定:`ST-BME_sync_{sanitizedChatId}.json` + - 冲突合并:`updatedAt` 新者胜;tombstone `deletedAt` 优先;`lastProcessedFloor/extractionCount` 取 `max` + - `meta` 为同步 JSON 顶层首字段,`revision` 全程单调递增 +- **兼容兜底(迁移窗口)** → `chat_metadata.st_bme_graph` + - 仅用于 legacy 兼容与迁移,不再是主路径 +- **墓碑(tombstones)** → 保留期固定 30 天 - **插件设置** → SillyTavern 的 `extension_settings.st_bme` - **向量索引** → 后端模式走酒馆 API;直连模式存在节点内 -- **召回持久注入** → `chat[x].extra.bme_recall`(消息级) +- **召回持久注入(不变)** → `chat[x].extra.bme_recall`(消息级) + +### 兼容迁移策略(legacy metadata → IndexedDB) + +- 触发:聊天加载/切换后,若目标 `STBME_{chatId}` 为空且存在 legacy `chat_metadata` 图谱 +- 行为:自动一次性迁移到 IndexedDB,并立即尝试同步到 `/user/files/` +- 幂等: + - 若 `migrationCompletedAt > 0`,跳过 + - 若 IndexedDB 已非空,跳过 +- 迁移记录: + - `migrationCompletedAt` + - `migrationSource`(默认 `chat_metadata`) + - `legacyRetentionUntil`(30 天) ### 事件挂载 | SillyTavern 事件 | 做什么 | |---|---| -| `CHAT_CHANGED` | 加载对应聊天的图谱 | +| `CHAT_CHANGED` | IndexedDB 优先加载 + 自动同步 | | `GENERATION_AFTER_COMMANDS` | AI 回复后提取记忆 | | `GENERATE_BEFORE_COMBINE_PROMPTS` | 生成前召回并注入 | -| `MESSAGE_RECEIVED` | 保存图谱状态 | +| `MESSAGE_RECEIVED` | 触发图谱持久化(IndexedDB 主写) | | 删除 / 编辑 / Swipe | 触发历史变动检测与恢复 | ### 召回流水线 diff --git a/index.js b/index.js index a848e02..f3dd7c5 100644 --- a/index.js +++ b/index.js @@ -3836,6 +3836,7 @@ function loadGraphFromChat(options = {}) { attemptIndex = 0, expectedChatId = "", source = "direct-load", + allowMetadataFallback = true, } = options; const context = getContext(); const chatIdentity = resolveCurrentChatIdentity(context); @@ -3861,28 +3862,7 @@ function loadGraphFromChat(options = {}) { }; } - if (chatId) { - const cachedSnapshot = readCachedIndexedDbSnapshot(chatId); - if (isIndexedDbSnapshotMeaningful(cachedSnapshot)) { - const cachedResult = applyIndexedDbSnapshotToRuntime(chatId, cachedSnapshot, { - source: `${source}:indexeddb-cache`, - attemptIndex, - }); - if (cachedResult?.loaded) { - clearPendingGraphLoadRetry(); - return cachedResult; - } - } - - scheduleIndexedDbGraphProbe(chatId, { - source: `${source}:indexeddb-probe`, - attemptIndex, - allowOverride: false, - }); - } - if (!chatId) { - const shouldRetry = attemptIndex < GRAPH_LOAD_RETRY_DELAYS_MS.length; if (chatIdentity.hasLikelySelectedChat) { currentGraph = normalizeGraphRuntimeState(createEmptyGraph(), ""); extractionCount = 0; @@ -3891,63 +3871,46 @@ function loadGraphFromChat(options = {}) { lastInjectionContent = ""; runtimeStatus = createUiStatus( "图谱加载中", - "正在等待当前聊天会话 ID 与元数据就绪", - shouldRetry ? "running" : "warning", + "正在等待当前聊天会话 ID 就绪", + "running", ); lastExtractionStatus = createUiStatus( "待命", - shouldRetry - ? "正在等待当前聊天会话 ID 就绪" - : "当前聊天会话 ID 长时间未就绪,已暂停修改图谱", - shouldRetry ? "idle" : "warning", + "正在等待当前聊天会话 ID 就绪", + "idle", ); lastVectorStatus = createUiStatus( "待命", - shouldRetry - ? "正在等待当前聊天会话 ID 就绪" - : "当前聊天会话 ID 长时间未就绪,已暂停修改图谱", - shouldRetry ? "idle" : "warning", + "正在等待当前聊天会话 ID 就绪", + "idle", ); lastRecallStatus = createUiStatus( "待命", - shouldRetry - ? "正在等待当前聊天会话 ID 就绪" - : "当前聊天会话 ID 长时间未就绪,已暂停图谱写回", - shouldRetry ? "idle" : "warning", + "正在等待当前聊天会话 ID 就绪", + "idle", ); - applyGraphLoadState( - shouldRetry ? GRAPH_LOAD_STATES.LOADING : GRAPH_LOAD_STATES.BLOCKED, - { - chatId: "", - reason: shouldRetry ? "chat-id-missing" : "chat-id-timeout", - attemptIndex, - revision: 0, - lastPersistedRevision: 0, - queuedPersistRevision: 0, - queuedPersistChatId: "", - pendingPersist: false, - shadowSnapshotUsed: false, - shadowSnapshotRevision: 0, - shadowSnapshotUpdatedAt: "", - shadowSnapshotReason: "", - writesBlocked: true, - }, - ); - if (shouldRetry) { - scheduleGraphLoadRetry("", "chat-id-missing", attemptIndex, { - allowPendingChat: true, - }); - } else { - clearPendingGraphLoadRetry(); - } + applyGraphLoadState(GRAPH_LOAD_STATES.LOADING, { + chatId: "", + reason: "chat-id-missing", + attemptIndex, + revision: 0, + lastPersistedRevision: 0, + queuedPersistRevision: 0, + queuedPersistChatId: "", + pendingPersist: false, + shadowSnapshotUsed: false, + shadowSnapshotRevision: 0, + shadowSnapshotUpdatedAt: "", + shadowSnapshotReason: "", + dbReady: false, + writesBlocked: true, + }); refreshPanelLiveState(); return { success: false, loaded: false, - loadState: shouldRetry - ? GRAPH_LOAD_STATES.LOADING - : GRAPH_LOAD_STATES.BLOCKED, - reason: shouldRetry ? "chat-id-missing" : "chat-id-timeout", + loadState: GRAPH_LOAD_STATES.LOADING, + reason: "chat-id-missing", chatId: "", attemptIndex, }; @@ -3978,6 +3941,7 @@ function loadGraphFromChat(options = {}) { shadowSnapshotReason: "", writesBlocked: true, }); + refreshPanelLiveState(); return { success: false, @@ -3989,351 +3953,51 @@ function loadGraphFromChat(options = {}) { }; } - const hasChatMetadata = - context?.chatMetadata && - typeof context.chatMetadata === "object" && - !Array.isArray(context.chatMetadata); - const metadataReady = isHostChatMetadataReady(context); - const savedData = hasChatMetadata - ? context.chatMetadata[GRAPH_METADATA_KEY] - : undefined; - const hasOfficialGraph = savedData != null && savedData !== ""; - const shadowSnapshot = readGraphShadowSnapshot(chatId); - const shouldRetry = attemptIndex < GRAPH_LOAD_RETRY_DELAYS_MS.length; - - if (hasOfficialGraph) { - const officialGraph = cloneGraphForPersistence( - normalizeGraphRuntimeState(deserializeGraph(savedData), chatId), - chatId, - ); - const officialRevision = Math.max( - 1, - getGraphPersistedRevision(officialGraph), - ); - const metadataIntegrity = getChatMetadataIntegrity(context); - - if (shouldPreferShadowSnapshotOverOfficial(officialGraph, shadowSnapshot)) { + const cachedSnapshot = readCachedIndexedDbSnapshot(chatId); + if (isIndexedDbSnapshotMeaningful(cachedSnapshot)) { + const cachedResult = applyIndexedDbSnapshotToRuntime(chatId, cachedSnapshot, { + source: `${source}:indexeddb-cache`, + attemptIndex, + }); + if (cachedResult?.loaded) { clearPendingGraphLoadRetry(); - currentGraph = cloneGraphForPersistence( - normalizeGraphRuntimeState( - deserializeGraph(shadowSnapshot.serializedGraph), - chatId, - ), + return cachedResult; + } + } + + const savedData = allowMetadataFallback + ? context?.chatMetadata?.[GRAPH_METADATA_KEY] + : undefined; + if (savedData != null && savedData !== "") { + try { + const officialGraph = cloneGraphForPersistence( + normalizeGraphRuntimeState(deserializeGraph(savedData), chatId), chatId, ); - extractionCount = Number.isFinite( - currentGraph?.historyState?.extractionCount, - ) + const officialRevision = Math.max(1, getGraphPersistedRevision(officialGraph)); + + clearPendingGraphLoadRetry(); + currentGraph = officialGraph; + extractionCount = Number.isFinite(currentGraph?.historyState?.extractionCount) ? currentGraph.historyState.extractionCount : 0; lastExtractedItems = []; updateLastRecalledItems(currentGraph.lastRecallResult || []); lastInjectionContent = ""; - runtimeStatus = createUiStatus( - "待命", - "已恢复本次会话里较新的图谱,正在补写正式聊天元数据", - "warning", - ); - lastExtractionStatus = createUiStatus( - "待命", - "检测到本次会话里有更新的图谱快照,正在补写正式元数据", - "warning", - ); + runtimeStatus = createUiStatus("待命", "已从兼容 metadata 加载图谱", "idle"); + lastExtractionStatus = createUiStatus("待命", "已加载聊天图谱,等待下一次提取", "idle"); lastVectorStatus = createUiStatus( "待命", - "检测到本次会话里有更新的图谱快照,正在补写正式元数据", - "warning", - ); - lastRecallStatus = createUiStatus( - "待命", - "检测到本次会话里有更新的图谱快照,正在补写正式元数据", - "warning", + currentGraph.vectorIndexState?.lastWarning || "已加载聊天图谱,等待下一次向量任务", + "idle", ); + lastRecallStatus = createUiStatus("待命", "已加载聊天图谱,等待下一次召回", "idle"); applyGraphLoadState(GRAPH_LOAD_STATES.LOADED, { chatId, - reason: "shadow-snapshot-newer-than-official", + reason: `${source}:metadata-compat`, attemptIndex, - revision: Math.max(shadowSnapshot.revision || 0, 1), + revision: officialRevision, lastPersistedRevision: officialRevision, - queuedPersistRevision: Math.max(shadowSnapshot.revision || 0, 1), - pendingPersist: true, - shadowSnapshotUsed: true, - shadowSnapshotRevision: Math.max(shadowSnapshot.revision || 0, 1), - shadowSnapshotUpdatedAt: shadowSnapshot.updatedAt, - shadowSnapshotReason: shadowSnapshot.reason, - writesBlocked: false, - }); - updateGraphPersistenceState({ - metadataIntegrity, - queuedPersistMode: "immediate", - queuedPersistRotateIntegrity: false, - queuedPersistReason: "shadow-snapshot-newer-than-official", - storagePrimary: "metadata", - storageMode: "metadata", - indexedDbRevision: Math.max(graphPersistenceState.indexedDbRevision || 0, officialRevision), - indexedDbLastError: "", - }); - const persistResult = maybeFlushQueuedGraphPersist( - "shadow-snapshot-newer-than-official", - ); - refreshPanelLiveState(); - return { - success: Boolean(persistResult.saved), - loaded: true, - loadState: GRAPH_LOAD_STATES.LOADED, - reason: "shadow-snapshot-newer-than-official", - chatId, - attemptIndex, - shadowSnapshotUsed: true, - }; - } - - clearPendingGraphLoadRetry(); - currentGraph = officialGraph; - extractionCount = Number.isFinite( - currentGraph?.historyState?.extractionCount, - ) - ? currentGraph.historyState.extractionCount - : 0; - lastExtractedItems = []; - updateLastRecalledItems(currentGraph.lastRecallResult || []); - lastInjectionContent = ""; - runtimeStatus = createUiStatus( - "待命", - "已加载聊天图谱,等待下一次任务", - "idle", - ); - lastExtractionStatus = createUiStatus( - "待命", - "已加载聊天图谱,等待下一次提取", - "idle", - ); - lastVectorStatus = createUiStatus( - "待命", - currentGraph.vectorIndexState?.lastWarning || - "已加载聊天图谱,等待下一次向量任务", - "idle", - ); - lastRecallStatus = createUiStatus( - "待命", - "已加载聊天图谱,等待下一次召回", - "idle", - ); - applyGraphLoadState(GRAPH_LOAD_STATES.LOADED, { - chatId, - reason: source, - attemptIndex, - revision: officialRevision, - lastPersistedRevision: officialRevision, - queuedPersistRevision: 0, - queuedPersistChatId: "", - pendingPersist: false, - shadowSnapshotUsed: false, - shadowSnapshotRevision: 0, - shadowSnapshotUpdatedAt: "", - shadowSnapshotReason: "", - writesBlocked: false, - }); - updateGraphPersistenceState({ - metadataIntegrity, - lastPersistMode: "", - queuedPersistMode: "", - queuedPersistRotateIntegrity: false, - queuedPersistReason: "", - storagePrimary: "metadata", - storageMode: "metadata", - indexedDbLastError: "", - }); - removeGraphShadowSnapshot(chatId); - - console.log("[ST-BME] 从聊天数据加载图谱:", { - chatId, - source, - attemptIndex, - ...getGraphStats(currentGraph), - }); - refreshPanelLiveState(); - return { - success: true, - loaded: true, - loadState: GRAPH_LOAD_STATES.LOADED, - reason: source, - chatId, - attemptIndex, - shadowSnapshotUsed: false, - }; - } - - if (shadowSnapshot) { - currentGraph = normalizeGraphRuntimeState( - deserializeGraph(shadowSnapshot.serializedGraph), - chatId, - ); - extractionCount = Number.isFinite( - currentGraph?.historyState?.extractionCount, - ) - ? currentGraph.historyState.extractionCount - : 0; - lastExtractedItems = []; - updateLastRecalledItems(currentGraph.lastRecallResult || []); - lastInjectionContent = ""; - runtimeStatus = createUiStatus("待命", "已从本次会话临时恢复图谱", "idle"); - lastExtractionStatus = createUiStatus( - "待命", - "图谱处于临时恢复状态,等待正式元数据", - "warning", - ); - lastVectorStatus = createUiStatus( - "待命", - "图谱处于临时恢复状态,等待正式元数据", - "warning", - ); - lastRecallStatus = createUiStatus( - "待命", - "图谱处于临时恢复状态,等待正式元数据", - "warning", - ); - - if (metadataReady) { - applyGraphLoadState(GRAPH_LOAD_STATES.LOADED, { - chatId, - reason: "shadow-snapshot-promoted", - attemptIndex, - revision: Math.max(shadowSnapshot.revision || 0, 1), - lastPersistedRevision: 0, - queuedPersistRevision: Math.max(shadowSnapshot.revision || 0, 1), - pendingPersist: true, - shadowSnapshotUsed: true, - shadowSnapshotRevision: Math.max(shadowSnapshot.revision || 0, 1), - shadowSnapshotUpdatedAt: shadowSnapshot.updatedAt, - shadowSnapshotReason: shadowSnapshot.reason, - writesBlocked: false, - }); - updateGraphPersistenceState({ - metadataIntegrity: getChatMetadataIntegrity(context), - queuedPersistMode: "immediate", - queuedPersistRotateIntegrity: false, - queuedPersistReason: "shadow-snapshot-promoted", - storagePrimary: "metadata", - storageMode: "metadata", - indexedDbLastError: "", - }); - const persistResult = maybeFlushQueuedGraphPersist( - "shadow-snapshot-promoted", - ); - refreshPanelLiveState(); - return { - success: Boolean(persistResult.saved), - loaded: true, - loadState: GRAPH_LOAD_STATES.LOADED, - reason: "shadow-snapshot-promoted", - chatId, - attemptIndex, - shadowSnapshotUsed: true, - }; - } - - const shadowState = shouldRetry - ? GRAPH_LOAD_STATES.SHADOW_RESTORED - : GRAPH_LOAD_STATES.BLOCKED; - applyGraphLoadState(shadowState, { - chatId, - reason: shouldRetry - ? "shadow-snapshot-restored" - : "shadow-snapshot-blocked", - attemptIndex, - revision: Math.max(shadowSnapshot.revision || 0, 1), - lastPersistedRevision: 0, - queuedPersistRevision: Math.max(shadowSnapshot.revision || 0, 1), - pendingPersist: true, - shadowSnapshotUsed: true, - shadowSnapshotRevision: Math.max(shadowSnapshot.revision || 0, 1), - shadowSnapshotUpdatedAt: shadowSnapshot.updatedAt, - shadowSnapshotReason: shadowSnapshot.reason, - writesBlocked: true, - }); - updateGraphPersistenceState({ - queuedPersistMode: "immediate", - queuedPersistRotateIntegrity: false, - queuedPersistReason: shouldRetry - ? "shadow-snapshot-restored" - : "shadow-snapshot-blocked", - storagePrimary: "metadata", - storageMode: "metadata", - indexedDbLastError: "", - }); - if (shouldRetry) { - scheduleGraphLoadRetry( - chatId, - hasChatMetadata - ? "official-graph-missing-shadow" - : "chat-metadata-missing-shadow", - attemptIndex, - ); - } else { - clearPendingGraphLoadRetry(); - } - refreshPanelLiveState(); - return { - success: false, - loaded: false, - loadState: shadowState, - reason: graphPersistenceState.reason, - chatId, - attemptIndex, - shadowSnapshotUsed: true, - }; - } - - currentGraph = normalizeGraphRuntimeState(createEmptyGraph(), chatId); - extractionCount = 0; - lastExtractedItems = []; - lastRecalledItems = []; - lastInjectionContent = ""; - - if (!metadataReady) { - runtimeStatus = createUiStatus( - "待命", - shouldRetry - ? "正在加载当前聊天图谱,暂不写回图谱元数据" - : "聊天元数据未就绪,已暂停图谱写回", - shouldRetry ? "idle" : "warning", - ); - lastExtractionStatus = createUiStatus( - "待命", - shouldRetry - ? "正在等待聊天图谱元数据加载" - : "聊天元数据未就绪,暂不允许修改图谱", - shouldRetry ? "idle" : "warning", - ); - lastVectorStatus = createUiStatus( - "待命", - shouldRetry - ? "正在等待聊天图谱元数据加载" - : "聊天元数据未就绪,暂不允许修改图谱", - shouldRetry ? "idle" : "warning", - ); - lastRecallStatus = createUiStatus( - "待命", - shouldRetry - ? "正在等待聊天图谱元数据加载" - : "聊天元数据未就绪,图谱处于保护状态", - shouldRetry ? "idle" : "warning", - ); - applyGraphLoadState( - shouldRetry ? GRAPH_LOAD_STATES.LOADING : GRAPH_LOAD_STATES.BLOCKED, - { - chatId, - reason: hasChatMetadata - ? shouldRetry - ? "graph-metadata-missing" - : "graph-metadata-timeout" - : shouldRetry - ? "chat-metadata-missing" - : "chat-metadata-timeout", - attemptIndex, - revision: 0, - lastPersistedRevision: 0, queuedPersistRevision: 0, queuedPersistChatId: "", pendingPersist: false, @@ -4341,72 +4005,66 @@ function loadGraphFromChat(options = {}) { shadowSnapshotRevision: 0, shadowSnapshotUpdatedAt: "", shadowSnapshotReason: "", - writesBlocked: true, - }, - ); - if (shouldRetry) { - scheduleGraphLoadRetry( - chatId, - hasChatMetadata ? "graph-metadata-missing" : "chat-metadata-missing", + dbReady: true, + writesBlocked: false, + }); + updateGraphPersistenceState({ + metadataIntegrity: getChatMetadataIntegrity(context), + storagePrimary: "metadata", + storageMode: "metadata", + dbReady: true, + indexedDbLastError: "", + }); + + scheduleIndexedDbGraphProbe(chatId, { + source: `${source}:indexeddb-probe`, attemptIndex, - ); - } else { - clearPendingGraphLoadRetry(); + allowOverride: true, + applyEmptyState: true, + }); + + refreshPanelLiveState(); + return { + success: true, + loaded: true, + loadState: GRAPH_LOAD_STATES.LOADED, + reason: `${source}:metadata-compat`, + chatId, + attemptIndex, + }; + } catch (error) { + console.warn("[ST-BME] 兼容 metadata 图谱读取失败,将回退 IndexedDB:", error); } - refreshPanelLiveState(); - return { - success: false, - loaded: false, - loadState: shouldRetry - ? GRAPH_LOAD_STATES.LOADING - : GRAPH_LOAD_STATES.BLOCKED, - reason: graphPersistenceState.reason, - chatId, - attemptIndex, - shadowSnapshotUsed: false, - }; } - clearPendingGraphLoadRetry(); - const confirmedState = GRAPH_LOAD_STATES.EMPTY_CONFIRMED; - runtimeStatus = createUiStatus("待命", "当前聊天还没有图谱", "idle"); - lastExtractionStatus = createUiStatus("待命", "当前聊天尚未执行提取", "idle"); - lastVectorStatus = createUiStatus("待命", "当前聊天尚未执行向量任务", "idle"); - lastRecallStatus = createUiStatus("待命", "当前聊天尚未建立记忆图谱", "idle"); - applyGraphLoadState(confirmedState, { + applyGraphLoadState(GRAPH_LOAD_STATES.LOADING, { chatId, - reason: "metadata-confirmed-empty", + reason: `indexeddb-probe-pending:${String(source || "direct-load")}`, attemptIndex, - revision: 0, - lastPersistedRevision: 0, - queuedPersistRevision: 0, - queuedPersistChatId: "", - pendingPersist: false, - shadowSnapshotUsed: false, - shadowSnapshotRevision: 0, - shadowSnapshotUpdatedAt: "", - shadowSnapshotReason: "", - writesBlocked: false, + dbReady: false, + writesBlocked: true, }); updateGraphPersistenceState({ - metadataIntegrity: getChatMetadataIntegrity(context), - queuedPersistMode: "", - queuedPersistRotateIntegrity: false, - queuedPersistReason: "", - storagePrimary: "metadata", - storageMode: "metadata", + storagePrimary: "indexeddb", + storageMode: "indexeddb", + dbReady: false, indexedDbLastError: "", }); - removeGraphShadowSnapshot(chatId); + scheduleIndexedDbGraphProbe(chatId, { + source: `${source}:indexeddb-probe`, + attemptIndex, + allowOverride: true, + applyEmptyState: true, + }); refreshPanelLiveState(); + return { success: false, loaded: false, - loadState: confirmedState, - reason: graphPersistenceState.reason, + loadState: GRAPH_LOAD_STATES.LOADING, + reason: "indexeddb-probe-pending", chatId, attemptIndex, - shadowSnapshotUsed: false, }; } diff --git a/package.json b/package.json index 4a1b3c4..6f2f781 100644 --- a/package.json +++ b/package.json @@ -1,4 +1,14 @@ { + "scripts": { + "test:p0": "node tests/p0-regressions.mjs", + "test:graph-persistence": "node tests/graph-persistence.mjs", + "test:indexeddb-persistence": "node tests/indexeddb-persistence.mjs", + "test:indexeddb-sync": "node tests/indexeddb-sync.mjs", + "test:indexeddb-migration": "node tests/indexeddb-migration.mjs", + "test:indexeddb": "npm run test:indexeddb-persistence && npm run test:indexeddb-sync && npm run test:indexeddb-migration", + "test:all": "npm run test:p0 && npm run test:graph-persistence && npm run test:indexeddb", + "check": "node --check index.js && node --check bme-db.js && node --check panel.js && node --check ui-status.js && node --check event-binding.js" + }, "dependencies": { "triviumdb": "^0.4.41" }, diff --git a/tests/graph-persistence.mjs b/tests/graph-persistence.mjs index 7891a49..78da861 100644 --- a/tests/graph-persistence.mjs +++ b/tests/graph-persistence.mjs @@ -651,12 +651,12 @@ result = { assert.equal( result.loadState, "loading", - "无 integrity 的占位 metadata 不能视作 ready", + "无图谱数据时应进入 IndexedDB 探测等待态", ); assert.equal( result.reason, - "graph-metadata-missing", - "应继续等待正式 graph metadata", + "indexeddb-probe-pending", + "应继续等待 IndexedDB 探测结果", ); assert.equal(live.writesBlocked, true); } @@ -673,11 +673,11 @@ result = { source: "metadata-chatid-ready", }); - assert.equal(result.loadState, "empty-confirmed"); + assert.equal(result.loadState, "loading"); assert.equal( harness.api.getGraphPersistenceLiveState().writesBlocked, - false, - "当 metadata 提供 chatId/sessionId 等强信号时,可进入 ready-empty", + true, + "无 IndexedDB 命中时应维持 loading 等待探测结果", ); } @@ -862,11 +862,11 @@ result = { source: "shadow-test", }); - assert.equal(result.loadState, "shadow-restored"); - assert.equal(reader.api.getCurrentGraph().nodes.length, 1); + assert.equal(result.loadState, "loading"); + assert.equal(reader.api.getCurrentGraph(), null); assert.equal( reader.api.getGraphPersistenceLiveState().shadowSnapshotUsed, - true, + false, ); assert.equal(reader.api.getGraphPersistenceLiveState().writesBlocked, true); } @@ -907,9 +907,9 @@ result = { "事件-official", ); assert.equal( - reader.api.readGraphShadowSnapshot("chat-official"), - null, - "正式元数据到位后应清理影子快照", + reader.api.readGraphShadowSnapshot("chat-official")?.reason, + "stale-shadow", + "metadata 兼容加载时保留影子快照仅作为兼容数据,不参与主链路", ); } @@ -944,26 +944,21 @@ result = { }); assert.equal(result.loadState, "loaded"); - assert.equal(result.reason, "shadow-snapshot-newer-than-official"); + assert.equal(result.reason, "official-older-than-shadow:metadata-compat"); assert.equal( reader.api.getCurrentGraph().nodes[0]?.fields?.title, - "事件-shadow-newer", + "事件-official-older", ); - assert.equal(reader.runtimeContext.__contextImmediateSaveCalls, 1); + assert.equal(reader.runtimeContext.__contextImmediateSaveCalls, 0); assert.equal( reader.runtimeContext.__chatContext.chatMetadata?.st_bme_graph?.nodes?.[0] ?.fields?.title, - "事件-shadow-newer", + "事件-official-older", ); assert.equal( - reader.runtimeContext.__chatContext.chatMetadata?.integrity, - "integrity-official-older", - "影子快照补写正式图谱时不能改写宿主 metadata.integrity", - ); - assert.equal( - reader.api.readGraphShadowSnapshot("chat-shadow-newer"), - null, - "影子快照补写成功后应被清理", + reader.api.readGraphShadowSnapshot("chat-shadow-newer")?.reason, + "pagehide-refresh", + "metadata 兼容加载后影子快照可保留,但不作为主链路恢复来源", ); } @@ -980,13 +975,13 @@ result = { }); const live = harness.api.getGraphPersistenceLiveState(); - assert.equal(result.loadState, "empty-confirmed"); - assert.equal(live.writesBlocked, false); - assert.equal(live.canWriteToMetadata, true); - assert.equal(harness.api.getCurrentGraph().nodes.length, 0); + assert.equal(result.loadState, "loading"); + assert.equal(result.reason, "indexeddb-probe-pending"); + assert.equal(live.writesBlocked, true); + assert.equal(harness.api.getCurrentGraph(), null); assert.equal( harness.api.readRuntimeDebugSnapshot().graphPersistence?.loadState, - "empty-confirmed", + "loading", ); } @@ -1017,7 +1012,7 @@ result = { assert.equal( harness.runtimeContext.__chatContext.chatMetadata?.st_bme_graph, undefined, - "empty-confirmed 状态下不能把空图被动写回 metadata", + "loading 状态下不能把空图被动写回 metadata", ); } @@ -1088,20 +1083,16 @@ result = { }); const live = reader.api.getGraphPersistenceLiveState(); - assert.equal(result.loadState, "loaded"); + assert.equal(result.loadState, "loading"); assert.equal( reader.runtimeContext.__chatContext.chatMetadata?.st_bme_graph?.nodes ?.length, - 1, + undefined, ); - assert.equal( - reader.runtimeContext.__chatContext.chatMetadata?.integrity, - "meta-ready-promote", - "metadata 就绪后提升影子快照时不能改写宿主 metadata.integrity", - ); - assert.equal(reader.runtimeContext.__contextImmediateSaveCalls, 1); + assert.equal(reader.runtimeContext.__chatContext.chatMetadata?.integrity, "meta-ready-promote"); + assert.equal(reader.runtimeContext.__contextImmediateSaveCalls, 0); assert.equal(reader.runtimeContext.__contextSaveCalls, 0); - assert.equal(live.lastPersistedRevision, 9); + assert.equal(live.lastPersistedRevision, 0); assert.equal(live.pendingPersist, false); } @@ -1255,8 +1246,8 @@ result = { runtimeGraph.nodes[0].fields.title = "runtime-shadow-mutated"; assert.equal( persistedGraph.nodes[0].fields.title, - "事件-shadow", - "shadow 恢复后的运行时修改不能污染已补写 metadata", + "事件-official-older", + "metadata 兼容加载后的运行时修改不能污染已保存 metadata", ); }