refactor: batch 5 phase 2 - extract graph-persistence.js (20 exports, ~230 lines) from index.js

This commit is contained in:
Youzini-afk
2026-03-29 15:43:11 +08:00
parent 1e6e5853e7
commit a371150661
4 changed files with 355 additions and 191 deletions

238
graph-persistence.js Normal file
View File

@@ -0,0 +1,238 @@
// ST-BME: 图谱持久化常量与纯工具函数
// 不依赖 index.js 模块级可变状态currentGraph / graphPersistenceState 等)
import {
deserializeGraph,
serializeGraph,
} from "./graph.js";
import { normalizeGraphRuntimeState } from "./runtime-state.js";
// ═══════════════════════════════════════════════════════════
// 常量
// ═══════════════════════════════════════════════════════════
export const MODULE_NAME = "st_bme";
export const GRAPH_METADATA_KEY = "st_bme_graph";
export const GRAPH_PERSISTENCE_META_KEY = "__stBmePersistence";
export const GRAPH_LOAD_STATES = Object.freeze({
NO_CHAT: "no-chat",
LOADING: "loading",
LOADED: "loaded",
SHADOW_RESTORED: "shadow-restored",
EMPTY_CONFIRMED: "empty-confirmed",
BLOCKED: "blocked",
});
export const GRAPH_LOAD_PENDING_CHAT_ID = "__pending_chat__";
export const GRAPH_SHADOW_SNAPSHOT_STORAGE_PREFIX = `${MODULE_NAME}:graph-shadow:`;
export const GRAPH_STARTUP_RECONCILE_DELAYS_MS = [150, 600, 1800, 4000];
// ═══════════════════════════════════════════════════════════
// 纯工具
// ═══════════════════════════════════════════════════════════
export function cloneRuntimeDebugValue(value, fallback = null) {
if (value == null) {
return fallback;
}
try {
return JSON.parse(JSON.stringify(value));
} catch {
return fallback ?? value;
}
}
export function createLocalIntegritySlug() {
const nativeUuid = globalThis.crypto?.randomUUID?.();
if (nativeUuid) return nativeUuid;
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (char) => {
const random = Math.floor(Math.random() * 16);
const value = char === "x" ? random : (random & 0x3) | 0x8;
return value.toString(16);
});
}
export const GRAPH_PERSISTENCE_SESSION_ID = createLocalIntegritySlug();
// ═══════════════════════════════════════════════════════════
// 图谱持久化元数据
// ═══════════════════════════════════════════════════════════
/**
* @param {object} graph
* @returns {object|null}
*/
export function getGraphPersistenceMeta(graph) {
if (!graph || typeof graph !== "object" || Array.isArray(graph)) {
return null;
}
const meta = graph[GRAPH_PERSISTENCE_META_KEY];
if (!meta || typeof meta !== "object" || Array.isArray(meta)) {
return null;
}
return meta;
}
/**
* @param {object} graph
* @returns {number}
*/
export function getGraphPersistedRevision(graph) {
const revision = Number(getGraphPersistenceMeta(graph)?.revision);
return Number.isFinite(revision) && revision > 0 ? revision : 0;
}
/**
* @param {object} graph
* @param {object} opts
* @param {number} [opts.revision]
* @param {string} [opts.reason]
* @param {string} [opts.chatId]
* @param {string} [opts.integrity]
*/
export function stampGraphPersistenceMeta(
graph,
{ revision = 0, reason = "", chatId = "", integrity = "" } = {},
) {
if (!graph || typeof graph !== "object" || Array.isArray(graph)) {
return null;
}
const existingMeta = getGraphPersistenceMeta(graph) || {};
const nextMeta = {
...existingMeta,
revision: Number.isFinite(revision) && revision > 0 ? revision : 0,
updatedAt: new Date().toISOString(),
sessionId: GRAPH_PERSISTENCE_SESSION_ID,
reason: String(reason || ""),
chatId: String(chatId || existingMeta.chatId || ""),
integrity: String(integrity || existingMeta.integrity || ""),
};
graph[GRAPH_PERSISTENCE_META_KEY] = nextMeta;
return nextMeta;
}
// ═══════════════════════════════════════════════════════════
// 聊天元数据
// ═══════════════════════════════════════════════════════════
export function writeChatMetadataPatch(context, patch = {}) {
if (!context) return false;
if (typeof context.updateChatMetadata === "function") {
context.updateChatMetadata(patch);
return true;
}
if (
!context.chatMetadata ||
typeof context.chatMetadata !== "object" ||
Array.isArray(context.chatMetadata)
) {
context.chatMetadata = {};
}
Object.assign(context.chatMetadata, patch || {});
return true;
}
// ═══════════════════════════════════════════════════════════
// Shadow Snapshot会话存储
// ═══════════════════════════════════════════════════════════
export function getGraphShadowSnapshotStorageKey(chatId = "") {
const normalizedChatId = String(chatId || "").trim();
if (!normalizedChatId) return "";
return `${GRAPH_SHADOW_SNAPSHOT_STORAGE_PREFIX}${encodeURIComponent(normalizedChatId)}`;
}
export function readGraphShadowSnapshot(chatId = "") {
const storageKey = getGraphShadowSnapshotStorageKey(chatId);
if (!storageKey) return null;
try {
const raw = globalThis.sessionStorage?.getItem(storageKey);
if (!raw) return null;
const snapshot = JSON.parse(raw);
if (
!snapshot ||
typeof snapshot !== "object" ||
String(snapshot.chatId || "") !== String(chatId || "") ||
typeof snapshot.serializedGraph !== "string" ||
!snapshot.serializedGraph
) {
return null;
}
return {
chatId: String(snapshot.chatId || ""),
revision: Number.isFinite(snapshot.revision) ? snapshot.revision : 0,
serializedGraph: snapshot.serializedGraph,
updatedAt: String(snapshot.updatedAt || ""),
reason: String(snapshot.reason || ""),
};
} catch {
return null;
}
}
/**
* @param {string} chatId
* @param {object} graph
* @param {object} [opts]
* @param {number} [opts.revision]
* @param {string} [opts.reason]
*/
export function writeGraphShadowSnapshot(
chatId,
graph,
{ revision = 0, reason = "" } = {},
) {
const storageKey = getGraphShadowSnapshotStorageKey(chatId);
if (!storageKey || !graph) return false;
try {
const serializedGraph = serializeGraph(graph);
globalThis.sessionStorage?.setItem(
storageKey,
JSON.stringify({
chatId: String(chatId || ""),
revision: Number.isFinite(revision) ? revision : 0,
serializedGraph,
updatedAt: new Date().toISOString(),
reason: String(reason || ""),
}),
);
return true;
} catch (error) {
console.warn("[ST-BME] 写入会话图谱临时快照失败:", error);
return false;
}
}
export function removeGraphShadowSnapshot(chatId = "") {
const storageKey = getGraphShadowSnapshotStorageKey(chatId);
if (!storageKey) return false;
try {
globalThis.sessionStorage?.removeItem(storageKey);
return true;
} catch {
return false;
}
}
// ═══════════════════════════════════════════════════════════
// 图谱克隆 / 比较
// ═══════════════════════════════════════════════════════════
export function cloneGraphForPersistence(graph, chatId = "") {
return normalizeGraphRuntimeState(
deserializeGraph(serializeGraph(graph)),
chatId,
);
}
export function shouldPreferShadowSnapshotOverOfficial(officialGraph, shadowSnapshot) {
if (!shadowSnapshot) return false;
const shadowRevision = Number(shadowSnapshot.revision || 0);
const officialRevision = getGraphPersistedRevision(officialGraph);
return shadowRevision > 0 && shadowRevision > officialRevision;
}

211
index.js
View File

@@ -102,117 +102,39 @@ import {
setBatchStageOutcome,
shouldRunRecallForTransaction,
} from "./ui-status.js";
import {
cloneGraphForPersistence,
cloneRuntimeDebugValue,
getGraphPersistenceMeta,
getGraphPersistedRevision,
getGraphShadowSnapshotStorageKey,
GRAPH_LOAD_PENDING_CHAT_ID,
GRAPH_LOAD_STATES,
GRAPH_METADATA_KEY,
GRAPH_PERSISTENCE_META_KEY,
GRAPH_PERSISTENCE_SESSION_ID,
GRAPH_SHADOW_SNAPSHOT_STORAGE_PREFIX,
GRAPH_STARTUP_RECONCILE_DELAYS_MS,
MODULE_NAME,
readGraphShadowSnapshot,
removeGraphShadowSnapshot,
shouldPreferShadowSnapshotOverOfficial,
stampGraphPersistenceMeta,
writeChatMetadataPatch,
writeGraphShadowSnapshot,
} from "./graph-persistence.js";
// 操控面板模块(动态加载,防止加载失败崩溃整个扩展)
let _panelModule = null;
let _themesModule = null;
const MODULE_NAME = "st_bme";
const GRAPH_METADATA_KEY = "st_bme_graph";
const GRAPH_PERSISTENCE_META_KEY = "__stBmePersistence";
const SERVER_SETTINGS_FILENAME = "st-bme-settings.json";
const SERVER_SETTINGS_URL = `/user/files/${SERVER_SETTINGS_FILENAME}`;
const GRAPH_LOAD_STATES = Object.freeze({
NO_CHAT: "no-chat",
LOADING: "loading",
LOADED: "loaded",
SHADOW_RESTORED: "shadow-restored",
EMPTY_CONFIRMED: "empty-confirmed",
BLOCKED: "blocked",
});
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) {
return fallback;
}
try {
return JSON.parse(JSON.stringify(value));
} catch {
return fallback ?? value;
}
}
function createLocalIntegritySlug() {
const nativeUuid = globalThis.crypto?.randomUUID?.();
if (nativeUuid) return nativeUuid;
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (char) => {
const random = Math.floor(Math.random() * 16);
const value = char === "x" ? random : (random & 0x3) | 0x8;
return value.toString(16);
});
}
const GRAPH_PERSISTENCE_SESSION_ID = createLocalIntegritySlug();
function getGraphPersistenceMeta(graph = currentGraph) {
if (!graph || typeof graph !== "object" || Array.isArray(graph)) {
return null;
}
const meta = graph[GRAPH_PERSISTENCE_META_KEY];
if (!meta || typeof meta !== "object" || Array.isArray(meta)) {
return null;
}
return meta;
}
function getGraphPersistedRevision(graph = currentGraph) {
const revision = Number(getGraphPersistenceMeta(graph)?.revision);
return Number.isFinite(revision) && revision > 0 ? revision : 0;
}
function stampGraphPersistenceMeta(
graph = currentGraph,
{
revision = graphPersistenceState.revision,
reason = "",
chatId = getCurrentChatId(),
integrity = "",
} = {},
) {
if (!graph || typeof graph !== "object" || Array.isArray(graph)) {
return null;
}
const existingMeta = getGraphPersistenceMeta(graph) || {};
const nextMeta = {
...existingMeta,
revision: Number.isFinite(revision) && revision > 0 ? revision : 0,
updatedAt: new Date().toISOString(),
sessionId: GRAPH_PERSISTENCE_SESSION_ID,
reason: String(reason || ""),
chatId: String(chatId || existingMeta.chatId || ""),
integrity: String(integrity || existingMeta.integrity || ""),
};
graph[GRAPH_PERSISTENCE_META_KEY] = nextMeta;
return nextMeta;
}
function getChatMetadataIntegrity(context = getContext()) {
return normalizeChatIdCandidate(context?.chatMetadata?.integrity);
}
function writeChatMetadataPatch(context = getContext(), patch = {}) {
if (!context) return false;
if (typeof context.updateChatMetadata === "function") {
context.updateChatMetadata(patch);
return true;
}
if (
!context.chatMetadata ||
typeof context.chatMetadata !== "object" ||
Array.isArray(context.chatMetadata)
) {
context.chatMetadata = {};
}
Object.assign(context.chatMetadata, patch || {});
return true;
}
function triggerChatMetadataSave(
context = getContext(),
{ immediate = false } = {},
@@ -245,23 +167,6 @@ function triggerChatMetadataSave(
return "debounced";
}
function cloneGraphForPersistence(
graph = currentGraph,
chatId = getCurrentChatId(),
) {
return normalizeGraphRuntimeState(
deserializeGraph(serializeGraph(graph)),
chatId,
);
}
function shouldPreferShadowSnapshotOverOfficial(officialGraph, shadowSnapshot) {
if (!shadowSnapshot) return false;
const shadowRevision = Number(shadowSnapshot.revision || 0);
const officialRevision = getGraphPersistedRevision(officialGraph);
return shadowRevision > 0 && shadowRevision > officialRevision;
}
function getRuntimeDebugState() {
const stateKey = "__stBmeRuntimeDebugState";
if (!globalThis[stateKey] || typeof globalThis[stateKey] !== "object") {
@@ -493,80 +398,6 @@ const stageAbortControllers = {
history: null,
};
function getGraphShadowSnapshotStorageKey(chatId = "") {
const normalizedChatId = String(chatId || "").trim();
if (!normalizedChatId) return "";
return `${GRAPH_SHADOW_SNAPSHOT_STORAGE_PREFIX}${encodeURIComponent(normalizedChatId)}`;
}
function readGraphShadowSnapshot(chatId = "") {
const storageKey = getGraphShadowSnapshotStorageKey(chatId);
if (!storageKey) return null;
try {
const raw = globalThis.sessionStorage?.getItem(storageKey);
if (!raw) return null;
const snapshot = JSON.parse(raw);
if (
!snapshot ||
typeof snapshot !== "object" ||
String(snapshot.chatId || "") !== String(chatId || "") ||
typeof snapshot.serializedGraph !== "string" ||
!snapshot.serializedGraph
) {
return null;
}
return {
chatId: String(snapshot.chatId || ""),
revision: Number.isFinite(snapshot.revision) ? snapshot.revision : 0,
serializedGraph: snapshot.serializedGraph,
updatedAt: String(snapshot.updatedAt || ""),
reason: String(snapshot.reason || ""),
};
} catch {
return null;
}
}
function writeGraphShadowSnapshot(
chatId = "",
graph = currentGraph,
{ revision = graphPersistenceState.revision, reason = "" } = {},
) {
const storageKey = getGraphShadowSnapshotStorageKey(chatId);
if (!storageKey || !graph) return false;
try {
const serializedGraph = serializeGraph(graph);
globalThis.sessionStorage?.setItem(
storageKey,
JSON.stringify({
chatId: String(chatId || ""),
revision: Number.isFinite(revision) ? revision : 0,
serializedGraph,
updatedAt: new Date().toISOString(),
reason: String(reason || ""),
}),
);
return true;
} catch (error) {
console.warn("[ST-BME] 写入会话图谱临时快照失败:", error);
return false;
}
}
function removeGraphShadowSnapshot(chatId = "") {
const storageKey = getGraphShadowSnapshotStorageKey(chatId);
if (!storageKey) return false;
try {
globalThis.sessionStorage?.removeItem(storageKey);
return true;
} catch {
return false;
}
}
function getGraphPersistenceLiveState() {
const snapshot = {
loadState: graphPersistenceState.loadState,

View File

@@ -27,6 +27,27 @@ import {
clampFloat,
formatRecallContextLine,
} from "../ui-status.js";
import {
cloneGraphForPersistence,
cloneRuntimeDebugValue,
getGraphPersistenceMeta,
getGraphPersistedRevision,
getGraphShadowSnapshotStorageKey,
GRAPH_LOAD_PENDING_CHAT_ID,
GRAPH_LOAD_STATES,
GRAPH_METADATA_KEY,
GRAPH_PERSISTENCE_META_KEY,
GRAPH_PERSISTENCE_SESSION_ID,
GRAPH_SHADOW_SNAPSHOT_STORAGE_PREFIX,
GRAPH_STARTUP_RECONCILE_DELAYS_MS,
MODULE_NAME,
readGraphShadowSnapshot,
removeGraphShadowSnapshot,
shouldPreferShadowSnapshotOverOfficial,
stampGraphPersistenceMeta,
writeChatMetadataPatch,
writeGraphShadowSnapshot,
} from "../graph-persistence.js";
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
const indexPath = path.resolve(moduleDir, "../index.js");
@@ -42,7 +63,7 @@ function extractSnippet(startMarker, endMarker) {
}
const persistencePrelude = extractSnippet(
'const MODULE_NAME = "st_bme";',
'const SERVER_SETTINGS_FILENAME = "st-bme-settings.json";',
"function clearInjectionState(options = {}) {",
);
const persistenceCore = extractSnippet(
@@ -204,6 +225,64 @@ async function createGraphPersistenceHarness({
clampInt,
clampFloat,
formatRecallContextLine,
cloneGraphForPersistence,
cloneRuntimeDebugValue,
getGraphPersistenceMeta,
getGraphPersistedRevision,
getGraphShadowSnapshotStorageKey,
GRAPH_LOAD_PENDING_CHAT_ID,
GRAPH_LOAD_STATES,
GRAPH_METADATA_KEY,
GRAPH_PERSISTENCE_META_KEY,
GRAPH_PERSISTENCE_SESSION_ID,
GRAPH_SHADOW_SNAPSHOT_STORAGE_PREFIX,
GRAPH_STARTUP_RECONCILE_DELAYS_MS,
MODULE_NAME,
readGraphShadowSnapshot,
removeGraphShadowSnapshot,
shouldPreferShadowSnapshotOverOfficial,
stampGraphPersistenceMeta,
writeChatMetadataPatch,
writeGraphShadowSnapshot,
// Shadow snapshot functions need VM-local sessionStorage overrides
// because imported versions use the outer globalThis (no sessionStorage)
readGraphShadowSnapshot(chatId = "") {
const key = getGraphShadowSnapshotStorageKey(chatId);
if (!key) return null;
try {
const raw = storage.getItem(key);
if (!raw) return null;
const snap = JSON.parse(raw);
if (!snap || String(snap.chatId || "") !== String(chatId || "") ||
typeof snap.serializedGraph !== "string" || !snap.serializedGraph) return null;
return {
chatId: String(snap.chatId || ""),
revision: Number.isFinite(snap.revision) ? snap.revision : 0,
serializedGraph: snap.serializedGraph,
updatedAt: String(snap.updatedAt || ""),
reason: String(snap.reason || ""),
};
} catch { return null; }
},
writeGraphShadowSnapshot(chatId = "", graph = null, { revision = 0, reason = "" } = {}) {
const key = getGraphShadowSnapshotStorageKey(chatId);
if (!key || !graph) return false;
try {
storage.setItem(key, JSON.stringify({
chatId: String(chatId || ""),
revision: Number.isFinite(revision) ? revision : 0,
serializedGraph: serializeGraph(graph),
updatedAt: new Date().toISOString(),
reason: String(reason || ""),
}));
return true;
} catch { return false; }
},
removeGraphShadowSnapshot(chatId = "") {
const key = getGraphShadowSnapshotStorageKey(chatId);
if (!key) return false;
try { storage.removeItem(key); return true; } catch { return false; }
},
createDefaultTaskProfiles() {
return {
extract: { activeProfileId: "default", profiles: [] },

View File

@@ -30,6 +30,18 @@ import {
setBatchStageOutcome,
shouldRunRecallForTransaction,
} from "../ui-status.js";
import {
cloneRuntimeDebugValue,
GRAPH_LOAD_STATES,
GRAPH_METADATA_KEY,
GRAPH_PERSISTENCE_META_KEY,
GRAPH_PERSISTENCE_SESSION_ID,
MODULE_NAME,
readGraphShadowSnapshot,
stampGraphPersistenceMeta,
writeChatMetadataPatch,
writeGraphShadowSnapshot,
} from "../graph-persistence.js";
const extensionsShimSource = [
"export const extension_settings = globalThis.__p0ExtensionSettings || {};",
@@ -276,6 +288,10 @@ function createGenerationRecallHarness() {
getStageNoticeTitle,
getStageNoticeDuration,
normalizeStageNoticeLevel,
MODULE_NAME,
GRAPH_LOAD_STATES,
GRAPH_METADATA_KEY,
GRAPH_PERSISTENCE_META_KEY,
};
vm.createContext(context);
vm.runInContext(