From 30fdeaac1af751b62d7ac2a76422b36d1fee2c1c Mon Sep 17 00:00:00 2001 From: Youzini-afk <13153778771cx@gmail.com> Date: Sat, 28 Mar 2026 14:45:31 +0800 Subject: [PATCH] Recover graph state after missed chat events --- index.js | 104 +++++++++++++++++++++++++++++++++++- panel.js | 1 + tests/graph-persistence.mjs | 78 +++++++++++++++++++++++++++ 3 files changed, 181 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index 0f25a32..8cfee38 100644 --- a/index.js +++ b/index.js @@ -94,6 +94,7 @@ const GRAPH_LOAD_STATES = Object.freeze({ }); const GRAPH_LOAD_PENDING_CHAT_ID = "__pending_chat__"; const GRAPH_SHADOW_SNAPSHOT_STORAGE_PREFIX = `${MODULE_NAME}:graph-shadow:`; +const GRAPH_STARTUP_RECONCILE_DELAYS_MS = [150, 600, 1800, 4000]; function cloneRuntimeDebugValue(value, fallback = null) { if (value == null) { @@ -1607,6 +1608,85 @@ function scheduleGraphLoadRetry( return true; } +function shouldSyncGraphLoadFromLiveContext( + context = getContext(), + { force = false } = {}, +) { + if (force) { + return true; + } + + const chatIdentity = resolveCurrentChatIdentity(context); + const liveChatId = chatIdentity.chatId; + const stateChatId = normalizeChatIdCandidate(graphPersistenceState.chatId); + const liveMetadataReady = isHostChatMetadataReady(context); + + if (liveChatId && liveChatId !== stateChatId) { + return true; + } + + if ( + graphPersistenceState.loadState === GRAPH_LOAD_STATES.NO_CHAT && + (liveChatId || chatIdentity.hasLikelySelectedChat) + ) { + return true; + } + + if ( + graphPersistenceState.loadState === GRAPH_LOAD_STATES.SHADOW_RESTORED && + liveMetadataReady + ) { + return true; + } + + if ( + graphPersistenceState.loadState === GRAPH_LOAD_STATES.LOADING && + liveMetadataReady + ) { + return true; + } + + if ( + graphPersistenceState.loadState === GRAPH_LOAD_STATES.BLOCKED && + (liveChatId || liveMetadataReady) + ) { + return true; + } + + return false; +} + +function syncGraphLoadFromLiveContext(options = {}) { + const { source = "live-context-sync", force = false } = options; + const context = getContext(); + if (!shouldSyncGraphLoadFromLiveContext(context, { force })) { + return { + synced: false, + reason: "no-sync-needed", + loadState: graphPersistenceState.loadState, + chatId: graphPersistenceState.chatId, + }; + } + + const result = loadGraphFromChat({ + source, + }); + return { + synced: true, + ...result, + }; +} + +function scheduleStartupGraphReconciliation() { + for (const delayMs of GRAPH_STARTUP_RECONCILE_DELAYS_MS) { + setTimeout(() => { + syncGraphLoadFromLiveContext({ + source: `startup-reconcile:${delayMs}`, + }); + }, delayMs); + } +} + function clearInjectionState() { lastInjectionContent = ""; lastRecalledItems = []; @@ -4775,12 +4855,21 @@ function onChatChanged() { lastPreGenerationRecallAt = 0; abortAllRunningStages(); dismissAllStageNotices(); - loadGraphFromChat(); + syncGraphLoadFromLiveContext({ + source: "chat-changed", + force: true, + }); clearInjectionState(); clearRecallInputTracking(); installSendIntentHooks(); } +function onChatLoaded() { + syncGraphLoadFromLiveContext({ + source: "chat-loaded", + }); +} + function onMessageSent(messageId) { const context = getContext(); const chat = context?.chat; @@ -5597,6 +5686,9 @@ async function onReembedDirect() { // 注册事件钩子 eventSource.on(event_types.CHAT_CHANGED, onChatChanged); + if (event_types.CHAT_LOADED) { + eventSource.on(event_types.CHAT_LOADED, onChatLoaded); + } if (event_types.MESSAGE_SENT) { eventSource.on(event_types.MESSAGE_SENT, onMessageSent); } @@ -5612,7 +5704,11 @@ async function onReembedDirect() { // 加载当前聊天的图谱 clearPendingGraphLoadRetry(); - loadGraphFromChat(); + syncGraphLoadFromLiveContext({ + source: "initial-load", + force: true, + }); + scheduleStartupGraphReconciliation(); // ==================== 操控面板初始化 ==================== @@ -5650,6 +5746,10 @@ async function onReembedDirect() { return settings; }, actions: { + syncGraphLoad: () => + syncGraphLoadFromLiveContext({ + source: "panel-open-sync", + }), extract: onManualExtract, compress: onManualCompress, sleep: onManualSleep, diff --git a/panel.js b/panel.js index c80c392..f076753 100644 --- a/panel.js +++ b/panel.js @@ -612,6 +612,7 @@ export function openPanel() { if (!overlayEl) return; ensureOverlayMountedAtRoot(); syncViewportCssVars(); + _actionHandlers.syncGraphLoad?.(); overlayEl.classList.add("active"); _restorePanelSize(); diff --git a/tests/graph-persistence.mjs b/tests/graph-persistence.mjs index 0933f04..a04380d 100644 --- a/tests/graph-persistence.mjs +++ b/tests/graph-persistence.mjs @@ -224,6 +224,7 @@ result = { maybeCaptureGraphShadowSnapshot, loadGraphFromChat, saveGraphToChat, + syncGraphLoadFromLiveContext, onMessageReceived, applyGraphLoadState, maybeFlushQueuedGraphPersist, @@ -304,6 +305,83 @@ result = { assert.equal(harness.api.getCurrentGraph().historyState.chatId, "chat-global"); } +{ + const harness = await createGraphPersistenceHarness({ + chatId: "", + globalChatId: "", + chatMetadata: {}, + }); + const lateGraph = createMeaningfulGraph("chat-late", "late"); + harness.api.setChatContext({ + chatId: "chat-late", + chatMetadata: { + integrity: "chat-late-ready", + st_bme_graph: lateGraph, + }, + characterId: "char-late", + groupId: null, + chat: [{ is_user: true, mes: "late load" }], + updateChatMetadata(patch) { + const base = + this.chatMetadata && + typeof this.chatMetadata === "object" && + !Array.isArray(this.chatMetadata) + ? this.chatMetadata + : {}; + this.chatMetadata = { + ...base, + ...(patch || {}), + }; + }, + saveMetadataDebounced() {}, + }); + + const result = harness.api.syncGraphLoadFromLiveContext({ + source: "late-context-sync", + }); + + assert.equal(result.synced, true); + assert.equal(result.loadState, "loaded"); + assert.equal(harness.api.getCurrentGraph().historyState.chatId, "chat-late"); +} + +{ + const harness = await createGraphPersistenceHarness({ + chatId: "", + globalChatId: "", + chatMetadata: {}, + }); + harness.api.setChatContext({ + chatId: "chat-empty-live", + chatMetadata: { + integrity: "chat-empty-live-ready", + }, + characterId: "char-empty-live", + groupId: null, + chat: [{ is_user: true, mes: "hello" }], + updateChatMetadata(patch) { + const base = + this.chatMetadata && + typeof this.chatMetadata === "object" && + !Array.isArray(this.chatMetadata) + ? this.chatMetadata + : {}; + this.chatMetadata = { + ...base, + ...(patch || {}), + }; + }, + saveMetadataDebounced() {}, + }); + + const result = harness.api.syncGraphLoadFromLiveContext({ + source: "late-empty-sync", + }); + + assert.equal(result.synced, true); + assert.equal(result.loadState, "empty-confirmed"); +} + { const harness = await createGraphPersistenceHarness({ chatId: "",