mirror of
https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology.git
synced 2026-06-13 18:31:16 +08:00
refactor(sync): extract saveGraphToChat + luker cache action (Phase 5e)
This commit is contained in:
251
index.js
251
index.js
@@ -354,7 +354,9 @@ import {
|
||||
buildBmeSyncRuntimeOptionsImpl,
|
||||
loadGraphFromChatImpl,
|
||||
maybeCaptureGraphShadowSnapshotImpl,
|
||||
onRebuildLocalCacheFromLukerSidecarImpl,
|
||||
persistExtractionBatchResultImpl,
|
||||
saveGraphToChatImpl,
|
||||
shouldUseAuthorityGraphStoreImpl,
|
||||
shouldUseAuthorityJobsImpl,
|
||||
syncGraphLoadFromLiveContextImpl,
|
||||
@@ -1392,9 +1394,14 @@ function createGraphLoadPersistRuntime() {
|
||||
isAuthorityJobTypeSupported,
|
||||
isAuthorityVectorConfig,
|
||||
isGraphEffectivelyEmpty,
|
||||
isGraphLoadStateDbReady,
|
||||
isGraphMetadataWriteAllowed,
|
||||
isIndexedDbSnapshotMeaningful,
|
||||
isLukerPrimaryPersistenceHost,
|
||||
loadGraphFromLukerSidecarV2,
|
||||
loadGraphFromChat,
|
||||
loadGraphFromIndexedDb,
|
||||
normalizeIndexedDbRevision,
|
||||
normalizeAuthorityCapabilityState,
|
||||
normalizeAuthorityJobConfig,
|
||||
normalizeAuthoritySettings,
|
||||
@@ -1403,6 +1410,7 @@ function createGraphLoadPersistRuntime() {
|
||||
persistGraphToChatMetadata,
|
||||
persistGraphToConfiguredDurableTier,
|
||||
queueGraphPersist,
|
||||
queueGraphPersistToIndexedDb,
|
||||
readCachedIndexedDbSnapshot,
|
||||
recordLocalPersistEarlyFailure,
|
||||
recordAuthorityBlobSnapshot,
|
||||
@@ -1412,11 +1420,14 @@ function createGraphLoadPersistRuntime() {
|
||||
rememberResolvedGraphIdentityAlias,
|
||||
resolveCompatibleGraphShadowSnapshot,
|
||||
resolveCurrentChatIdentity,
|
||||
resolveCurrentChatStateTarget,
|
||||
resolvePersistRevisionFloor,
|
||||
resolvePersistenceChatId,
|
||||
resolvePreferredGraphLocalStorePresentation,
|
||||
resolveSnapshotGraphStorePresentation,
|
||||
restoreRecallUiStateFromPersistence,
|
||||
runAuthorityConsistencyAudit,
|
||||
scheduleBmeIndexedDbTask,
|
||||
scheduleGraphChatStateProbe,
|
||||
scheduleIndexedDbGraphProbe,
|
||||
schedulePersistedRecallMessageUiRefresh,
|
||||
@@ -1427,6 +1438,7 @@ function createGraphLoadPersistRuntime() {
|
||||
stampGraphPersistenceMeta,
|
||||
syncCommitMarkerToPersistenceState,
|
||||
updateGraphPersistenceState,
|
||||
toastr,
|
||||
writeAuthorityLukerCheckpointBlob,
|
||||
writeGraphShadowSnapshot,
|
||||
};
|
||||
@@ -14505,205 +14517,7 @@ function queueGraphPersistToIndexedDb(
|
||||
}
|
||||
|
||||
function saveGraphToChat(options = {}) {
|
||||
const context = getContext();
|
||||
if (!context || !currentGraph) {
|
||||
return buildGraphPersistResult({
|
||||
saved: false,
|
||||
blocked: true,
|
||||
reason: "missing-context-or-graph",
|
||||
});
|
||||
}
|
||||
const chatId = resolvePersistenceChatId(context, currentGraph);
|
||||
const {
|
||||
reason = "graph-save",
|
||||
markMutation = true,
|
||||
persistMetadata = false,
|
||||
captureShadow = Boolean(persistMetadata),
|
||||
immediate = markMutation,
|
||||
} = options;
|
||||
|
||||
ensureCurrentGraphRuntimeState();
|
||||
currentGraph.historyState.extractionCount = extractionCount;
|
||||
if (!chatId) {
|
||||
recordLocalPersistEarlyFailure("missing-chat-id", {
|
||||
chatId,
|
||||
revision: 0,
|
||||
});
|
||||
return buildGraphPersistResult({
|
||||
saved: false,
|
||||
blocked: true,
|
||||
reason: "missing-chat-id",
|
||||
});
|
||||
}
|
||||
|
||||
const revision = markMutation
|
||||
? allocateRequestedPersistRevision(0, currentGraph)
|
||||
: resolvePersistRevisionFloor(0, currentGraph);
|
||||
const persistenceEnvironment = buildPersistenceEnvironment(
|
||||
context,
|
||||
getPreferredGraphLocalStorePresentationSync(),
|
||||
);
|
||||
|
||||
if (captureShadow) {
|
||||
maybeCaptureGraphShadowSnapshot(reason);
|
||||
}
|
||||
|
||||
const shouldQueueIndexedDbPersist =
|
||||
(persistenceEnvironment.hostProfile !== "luker" ||
|
||||
persistenceEnvironment.primaryStorageTier === "authority-sql") &&
|
||||
(markMutation || !isGraphEffectivelyEmpty(currentGraph));
|
||||
if (shouldQueueIndexedDbPersist) {
|
||||
queueGraphPersistToIndexedDb(chatId, currentGraph, {
|
||||
revision,
|
||||
reason,
|
||||
});
|
||||
}
|
||||
|
||||
const metadataFallbackEnabled =
|
||||
Boolean(persistMetadata) || !ensureBmeChatManager();
|
||||
|
||||
if (!markMutation) {
|
||||
const hasMeaningfulGraphData = !isGraphEffectivelyEmpty(currentGraph);
|
||||
if (
|
||||
!hasMeaningfulGraphData ||
|
||||
graphPersistenceState.loadState === GRAPH_LOAD_STATES.EMPTY_CONFIRMED
|
||||
) {
|
||||
return buildGraphPersistResult({
|
||||
saved: false,
|
||||
blocked: false,
|
||||
reason: hasMeaningfulGraphData
|
||||
? "passive-empty-confirmed-skipped"
|
||||
: "passive-empty-graph-skipped",
|
||||
revision,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (persistenceEnvironment.primaryStorageTier === "luker-chat-state") {
|
||||
const persistGraph = cloneGraphForPersistence(currentGraph, chatId);
|
||||
const chatStateTarget = resolveCurrentChatStateTarget(context);
|
||||
const lastProcessedAssistantFloor = Number.isFinite(
|
||||
Number(persistGraph?.historyState?.lastProcessedAssistantFloor),
|
||||
)
|
||||
? Number(persistGraph.historyState.lastProcessedAssistantFloor)
|
||||
: null;
|
||||
scheduleBmeIndexedDbTask(async () => {
|
||||
const persistResult = await persistGraphToConfiguredDurableTier(
|
||||
context,
|
||||
persistGraph,
|
||||
{
|
||||
chatId,
|
||||
revision,
|
||||
reason,
|
||||
lastProcessedAssistantFloor,
|
||||
chatStateTarget,
|
||||
graphDetached: true,
|
||||
},
|
||||
);
|
||||
if (!persistResult?.accepted) {
|
||||
queueGraphPersist(reason, revision, {
|
||||
immediate,
|
||||
graph: persistGraph,
|
||||
chatId,
|
||||
captureShadow,
|
||||
});
|
||||
}
|
||||
refreshPanelLiveState();
|
||||
});
|
||||
updateGraphPersistenceState({
|
||||
hostProfile: persistenceEnvironment.hostProfile,
|
||||
primaryStorageTier: persistenceEnvironment.primaryStorageTier,
|
||||
cacheStorageTier: persistenceEnvironment.cacheStorageTier,
|
||||
lastPersistReason: String(reason || "graph-save"),
|
||||
lastPersistMode: "luker-chat-state-queued",
|
||||
});
|
||||
return buildGraphPersistResult({
|
||||
saved: false,
|
||||
queued: true,
|
||||
blocked: false,
|
||||
accepted: false,
|
||||
reason: "luker-chat-state-queued",
|
||||
revision,
|
||||
saveMode: "luker-chat-state-queued",
|
||||
storageTier: "luker-chat-state",
|
||||
primaryTier: persistenceEnvironment.primaryStorageTier,
|
||||
cacheTier: persistenceEnvironment.cacheStorageTier,
|
||||
});
|
||||
}
|
||||
|
||||
if (!metadataFallbackEnabled) {
|
||||
const preferredLocalStore = getPreferredGraphLocalStorePresentationSync();
|
||||
const saveMode = shouldQueueIndexedDbPersist
|
||||
? `${preferredLocalStore.reasonPrefix}-queued`
|
||||
: `${preferredLocalStore.reasonPrefix}-skip`;
|
||||
updateGraphPersistenceState({
|
||||
storagePrimary: preferredLocalStore.storagePrimary,
|
||||
storageMode: preferredLocalStore.storageMode,
|
||||
dbReady:
|
||||
graphPersistenceState.dbReady ??
|
||||
isGraphLoadStateDbReady(graphPersistenceState.loadState),
|
||||
lastPersistReason: String(reason || "graph-save"),
|
||||
lastPersistMode: saveMode,
|
||||
pendingPersist: false,
|
||||
queuedPersistChatId: "",
|
||||
queuedPersistMode: "",
|
||||
queuedPersistReason: "",
|
||||
queuedPersistRotateIntegrity: false,
|
||||
dualWriteLastResult: {
|
||||
action: "save",
|
||||
target: preferredLocalStore.storagePrimary,
|
||||
queued: Boolean(shouldQueueIndexedDbPersist),
|
||||
success: true,
|
||||
chatId,
|
||||
revision: normalizeIndexedDbRevision(revision),
|
||||
reason: String(reason || "graph-save"),
|
||||
at: Date.now(),
|
||||
},
|
||||
});
|
||||
return buildGraphPersistResult({
|
||||
saved: false,
|
||||
queued: Boolean(shouldQueueIndexedDbPersist),
|
||||
blocked: false,
|
||||
accepted: false,
|
||||
reason: shouldQueueIndexedDbPersist
|
||||
? `${preferredLocalStore.reasonPrefix}-queued`
|
||||
: `${preferredLocalStore.reasonPrefix}-empty-skip`,
|
||||
revision,
|
||||
saveMode,
|
||||
storageTier: shouldQueueIndexedDbPersist
|
||||
? preferredLocalStore.storagePrimary
|
||||
: "none",
|
||||
});
|
||||
}
|
||||
|
||||
if (!isGraphMetadataWriteAllowed()) {
|
||||
console.warn(
|
||||
`[ST-BME] 图谱写回已被安全保护拦截(chat=${chatId},state=${graphPersistenceState.loadState},reason=${reason})`,
|
||||
);
|
||||
return queueGraphPersist(reason, revision, { immediate });
|
||||
}
|
||||
|
||||
const metadataPersistResult = persistGraphToChatMetadata(context, {
|
||||
reason,
|
||||
revision,
|
||||
immediate,
|
||||
});
|
||||
updateGraphPersistenceState({
|
||||
storageMode: "metadata-full",
|
||||
dualWriteLastResult: {
|
||||
action: "save",
|
||||
target: "metadata",
|
||||
success: Boolean(metadataPersistResult?.saved),
|
||||
queued: Boolean(metadataPersistResult?.queued),
|
||||
blocked: Boolean(metadataPersistResult?.blocked),
|
||||
chatId,
|
||||
revision: normalizeIndexedDbRevision(revision),
|
||||
reason: String(reason || "graph-save"),
|
||||
at: Date.now(),
|
||||
},
|
||||
});
|
||||
|
||||
return metadataPersistResult;
|
||||
return saveGraphToChatImpl(createGraphLoadPersistRuntime(), options);
|
||||
}
|
||||
|
||||
function handleGraphShadowSnapshotPageHide() {
|
||||
@@ -17677,44 +17491,7 @@ async function onProbeGraphLoad() {
|
||||
}
|
||||
|
||||
async function onRebuildLocalCacheFromLukerSidecar() {
|
||||
const context = getContext();
|
||||
const chatStateTarget = resolveCurrentChatStateTarget(context);
|
||||
if (!isLukerPrimaryPersistenceHost(context)) {
|
||||
toastr.info("当前宿主不是 Luker,无需从主 sidecar 重建本地缓存");
|
||||
return { handledToast: true, reason: "not-luker" };
|
||||
}
|
||||
const chatId = getCurrentChatId(context);
|
||||
if (!chatId) {
|
||||
toastr.warning("当前没有聊天上下文");
|
||||
return { handledToast: true, reason: "missing-chat-id" };
|
||||
}
|
||||
|
||||
const loadResult = await loadGraphFromLukerSidecarV2(chatId, {
|
||||
source: "panel-manual-luker-cache-rebuild",
|
||||
allowOverride: true,
|
||||
chatStateTarget,
|
||||
});
|
||||
if (!loadResult?.loaded || !currentGraph) {
|
||||
toastr.warning(
|
||||
`无法从 Luker 主 sidecar 重建本地缓存: ${loadResult?.reason || "sidecar not available"}`,
|
||||
);
|
||||
return { handledToast: true, result: loadResult };
|
||||
}
|
||||
|
||||
queueGraphPersistToIndexedDb(chatId, cloneGraphForPersistence(currentGraph, chatId), {
|
||||
revision: Math.max(
|
||||
Number(graphPersistenceState.lukerManifestRevision || 0),
|
||||
Number(getGraphPersistedRevision(currentGraph) || 0),
|
||||
Number(graphPersistenceState.revision || 0),
|
||||
),
|
||||
reason: "panel-manual-luker-cache-rebuild",
|
||||
persistRole: "cache-mirror",
|
||||
scheduleCloudUpload: false,
|
||||
graphDetached: true,
|
||||
});
|
||||
refreshPanelLiveState();
|
||||
toastr.success("已开始从 Luker 主 sidecar 重建本地缓存");
|
||||
return { handledToast: true, result: loadResult };
|
||||
return await onRebuildLocalCacheFromLukerSidecarImpl(createGraphLoadPersistRuntime());
|
||||
}
|
||||
|
||||
async function onRepairLukerSidecar() {
|
||||
|
||||
@@ -1838,3 +1838,309 @@ export function loadGraphFromChatImpl(runtime, options = {}) {
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
export function saveGraphToChatImpl(runtime, options = {}) {
|
||||
const graphPersistenceState = new Proxy({}, {
|
||||
get(_target, key) {
|
||||
return (runtime.getGraphPersistenceState?.() || {})[key];
|
||||
},
|
||||
set(_target, key, value) {
|
||||
const state = runtime.getGraphPersistenceState?.() || {};
|
||||
state[key] = value;
|
||||
return true;
|
||||
},
|
||||
});
|
||||
let currentGraph = runtime.getCurrentGraph?.() || null;
|
||||
const GRAPH_LOAD_STATES = runtime.GRAPH_LOAD_STATES;
|
||||
const allocateRequestedPersistRevision = runtime.allocateRequestedPersistRevision;
|
||||
const buildGraphPersistResult = runtime.buildGraphPersistResult;
|
||||
const buildPersistenceEnvironment = runtime.buildPersistenceEnvironment;
|
||||
const canPersistGraphToMetadataFallback = runtime.canPersistGraphToMetadataFallback;
|
||||
const cloneGraphForPersistence = runtime.cloneGraphForPersistence;
|
||||
const ensureBmeChatManager = runtime.ensureBmeChatManager;
|
||||
const ensureCurrentGraphRuntimeState = runtime.ensureCurrentGraphRuntimeState;
|
||||
const getContext = runtime.getContext;
|
||||
const getGraphPersistedRevision = runtime.getGraphPersistedRevision;
|
||||
const getPreferredGraphLocalStorePresentationSync = runtime.getPreferredGraphLocalStorePresentationSync;
|
||||
const isGraphEffectivelyEmpty = runtime.isGraphEffectivelyEmpty;
|
||||
const isGraphLoadStateDbReady = runtime.isGraphLoadStateDbReady;
|
||||
const isGraphMetadataWriteAllowed = runtime.isGraphMetadataWriteAllowed;
|
||||
const normalizeIndexedDbRevision = runtime.normalizeIndexedDbRevision;
|
||||
const persistGraphToChatMetadata = runtime.persistGraphToChatMetadata;
|
||||
const persistGraphToConfiguredDurableTier = runtime.persistGraphToConfiguredDurableTier;
|
||||
const queueGraphPersist = runtime.queueGraphPersist;
|
||||
const queueGraphPersistToIndexedDb = runtime.queueGraphPersistToIndexedDb;
|
||||
const recordLocalPersistEarlyFailure = runtime.recordLocalPersistEarlyFailure;
|
||||
const refreshPanelLiveState = runtime.refreshPanelLiveState;
|
||||
const resolveCurrentChatStateTarget = runtime.resolveCurrentChatStateTarget;
|
||||
const resolvePersistRevisionFloor = runtime.resolvePersistRevisionFloor;
|
||||
const resolvePersistenceChatId = runtime.resolvePersistenceChatId;
|
||||
const scheduleBmeIndexedDbTask = runtime.scheduleBmeIndexedDbTask;
|
||||
const updateGraphPersistenceState = runtime.updateGraphPersistenceState;
|
||||
const console = runtime.console || globalThis.console;
|
||||
|
||||
const context = getContext();
|
||||
if (!context || !currentGraph) {
|
||||
return buildGraphPersistResult({
|
||||
saved: false,
|
||||
blocked: true,
|
||||
reason: "missing-context-or-graph",
|
||||
});
|
||||
}
|
||||
const chatId = resolvePersistenceChatId(context, currentGraph);
|
||||
const {
|
||||
reason = "graph-save",
|
||||
markMutation = true,
|
||||
persistMetadata = false,
|
||||
captureShadow = Boolean(persistMetadata),
|
||||
immediate = markMutation,
|
||||
} = options;
|
||||
|
||||
ensureCurrentGraphRuntimeState();
|
||||
currentGraph = runtime.getCurrentGraph?.() || null;
|
||||
currentGraph.historyState.extractionCount = runtime.getExtractionCount?.() || 0;
|
||||
if (!chatId) {
|
||||
recordLocalPersistEarlyFailure("missing-chat-id", {
|
||||
chatId,
|
||||
revision: 0,
|
||||
});
|
||||
return buildGraphPersistResult({
|
||||
saved: false,
|
||||
blocked: true,
|
||||
reason: "missing-chat-id",
|
||||
});
|
||||
}
|
||||
|
||||
const revision = markMutation
|
||||
? allocateRequestedPersistRevision(0, currentGraph)
|
||||
: resolvePersistRevisionFloor(0, currentGraph);
|
||||
const persistenceEnvironment = buildPersistenceEnvironment(
|
||||
context,
|
||||
getPreferredGraphLocalStorePresentationSync(),
|
||||
);
|
||||
|
||||
if (captureShadow) {
|
||||
maybeCaptureGraphShadowSnapshotImpl(runtime, reason);
|
||||
}
|
||||
|
||||
const shouldQueueIndexedDbPersist =
|
||||
(persistenceEnvironment.hostProfile !== "luker" ||
|
||||
persistenceEnvironment.primaryStorageTier === "authority-sql") &&
|
||||
(markMutation || !isGraphEffectivelyEmpty(currentGraph));
|
||||
if (shouldQueueIndexedDbPersist) {
|
||||
queueGraphPersistToIndexedDb(chatId, currentGraph, {
|
||||
revision,
|
||||
reason,
|
||||
});
|
||||
}
|
||||
|
||||
const metadataFallbackEnabled =
|
||||
Boolean(persistMetadata) || !ensureBmeChatManager();
|
||||
|
||||
if (!markMutation) {
|
||||
const hasMeaningfulGraphData = !isGraphEffectivelyEmpty(currentGraph);
|
||||
if (
|
||||
!hasMeaningfulGraphData ||
|
||||
graphPersistenceState.loadState === GRAPH_LOAD_STATES.EMPTY_CONFIRMED
|
||||
) {
|
||||
return buildGraphPersistResult({
|
||||
saved: false,
|
||||
blocked: false,
|
||||
reason: hasMeaningfulGraphData
|
||||
? "passive-empty-confirmed-skipped"
|
||||
: "passive-empty-graph-skipped",
|
||||
revision,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (persistenceEnvironment.primaryStorageTier === "luker-chat-state") {
|
||||
const persistGraph = cloneGraphForPersistence(currentGraph, chatId);
|
||||
const chatStateTarget = resolveCurrentChatStateTarget(context);
|
||||
const lastProcessedAssistantFloor = Number.isFinite(
|
||||
Number(persistGraph?.historyState?.lastProcessedAssistantFloor),
|
||||
)
|
||||
? Number(persistGraph.historyState.lastProcessedAssistantFloor)
|
||||
: null;
|
||||
scheduleBmeIndexedDbTask(async () => {
|
||||
const persistResult = await persistGraphToConfiguredDurableTier(
|
||||
context,
|
||||
persistGraph,
|
||||
{
|
||||
chatId,
|
||||
revision,
|
||||
reason,
|
||||
lastProcessedAssistantFloor,
|
||||
chatStateTarget,
|
||||
graphDetached: true,
|
||||
},
|
||||
);
|
||||
if (!persistResult?.accepted) {
|
||||
queueGraphPersist(reason, revision, {
|
||||
immediate,
|
||||
graph: persistGraph,
|
||||
chatId,
|
||||
captureShadow,
|
||||
});
|
||||
}
|
||||
refreshPanelLiveState();
|
||||
});
|
||||
updateGraphPersistenceState({
|
||||
hostProfile: persistenceEnvironment.hostProfile,
|
||||
primaryStorageTier: persistenceEnvironment.primaryStorageTier,
|
||||
cacheStorageTier: persistenceEnvironment.cacheStorageTier,
|
||||
lastPersistReason: String(reason || "graph-save"),
|
||||
lastPersistMode: "luker-chat-state-queued",
|
||||
});
|
||||
return buildGraphPersistResult({
|
||||
saved: false,
|
||||
queued: true,
|
||||
blocked: false,
|
||||
accepted: false,
|
||||
reason: "luker-chat-state-queued",
|
||||
revision,
|
||||
saveMode: "luker-chat-state-queued",
|
||||
storageTier: "luker-chat-state",
|
||||
primaryTier: persistenceEnvironment.primaryStorageTier,
|
||||
cacheTier: persistenceEnvironment.cacheStorageTier,
|
||||
});
|
||||
}
|
||||
|
||||
if (!metadataFallbackEnabled) {
|
||||
const preferredLocalStore = getPreferredGraphLocalStorePresentationSync();
|
||||
const saveMode = shouldQueueIndexedDbPersist
|
||||
? `${preferredLocalStore.reasonPrefix}-queued`
|
||||
: `${preferredLocalStore.reasonPrefix}-skip`;
|
||||
updateGraphPersistenceState({
|
||||
storagePrimary: preferredLocalStore.storagePrimary,
|
||||
storageMode: preferredLocalStore.storageMode,
|
||||
dbReady:
|
||||
graphPersistenceState.dbReady ??
|
||||
isGraphLoadStateDbReady(graphPersistenceState.loadState),
|
||||
lastPersistReason: String(reason || "graph-save"),
|
||||
lastPersistMode: saveMode,
|
||||
pendingPersist: false,
|
||||
queuedPersistChatId: "",
|
||||
queuedPersistMode: "",
|
||||
queuedPersistReason: "",
|
||||
queuedPersistRotateIntegrity: false,
|
||||
dualWriteLastResult: {
|
||||
action: "save",
|
||||
target: preferredLocalStore.storagePrimary,
|
||||
queued: Boolean(shouldQueueIndexedDbPersist),
|
||||
success: true,
|
||||
chatId,
|
||||
revision: normalizeIndexedDbRevision(revision),
|
||||
reason: String(reason || "graph-save"),
|
||||
at: Date.now(),
|
||||
},
|
||||
});
|
||||
return buildGraphPersistResult({
|
||||
saved: false,
|
||||
queued: Boolean(shouldQueueIndexedDbPersist),
|
||||
blocked: false,
|
||||
accepted: false,
|
||||
reason: shouldQueueIndexedDbPersist
|
||||
? `${preferredLocalStore.reasonPrefix}-queued`
|
||||
: `${preferredLocalStore.reasonPrefix}-empty-skip`,
|
||||
revision,
|
||||
saveMode,
|
||||
storageTier: shouldQueueIndexedDbPersist
|
||||
? preferredLocalStore.storagePrimary
|
||||
: "none",
|
||||
});
|
||||
}
|
||||
|
||||
if (!isGraphMetadataWriteAllowed()) {
|
||||
console.warn(
|
||||
`[ST-BME] 图谱写回已被安全保护拦截(chat=${chatId},state=${graphPersistenceState.loadState},reason=${reason})`,
|
||||
);
|
||||
return queueGraphPersist(reason, revision, { immediate });
|
||||
}
|
||||
|
||||
const metadataPersistResult = persistGraphToChatMetadata(context, {
|
||||
reason,
|
||||
revision,
|
||||
immediate,
|
||||
});
|
||||
updateGraphPersistenceState({
|
||||
storageMode: "metadata-full",
|
||||
dualWriteLastResult: {
|
||||
action: "save",
|
||||
target: "metadata",
|
||||
success: Boolean(metadataPersistResult?.saved),
|
||||
queued: Boolean(metadataPersistResult?.queued),
|
||||
blocked: Boolean(metadataPersistResult?.blocked),
|
||||
chatId,
|
||||
revision: normalizeIndexedDbRevision(revision),
|
||||
reason: String(reason || "graph-save"),
|
||||
at: Date.now(),
|
||||
},
|
||||
});
|
||||
|
||||
return metadataPersistResult;
|
||||
}
|
||||
|
||||
export async function onRebuildLocalCacheFromLukerSidecarImpl(runtime) {
|
||||
const graphPersistenceState = new Proxy({}, {
|
||||
get(_target, key) {
|
||||
return (runtime.getGraphPersistenceState?.() || {})[key];
|
||||
},
|
||||
set(_target, key, value) {
|
||||
const state = runtime.getGraphPersistenceState?.() || {};
|
||||
state[key] = value;
|
||||
return true;
|
||||
},
|
||||
});
|
||||
let currentGraph = runtime.getCurrentGraph?.() || null;
|
||||
const cloneGraphForPersistence = runtime.cloneGraphForPersistence;
|
||||
const getContext = runtime.getContext;
|
||||
const getCurrentChatId = runtime.getCurrentChatId;
|
||||
const getGraphPersistedRevision = runtime.getGraphPersistedRevision;
|
||||
const isLukerPrimaryPersistenceHost = runtime.isLukerPrimaryPersistenceHost;
|
||||
const loadGraphFromLukerSidecarV2 = runtime.loadGraphFromLukerSidecarV2;
|
||||
const queueGraphPersistToIndexedDb = runtime.queueGraphPersistToIndexedDb;
|
||||
const refreshPanelLiveState = runtime.refreshPanelLiveState;
|
||||
const resolveCurrentChatStateTarget = runtime.resolveCurrentChatStateTarget;
|
||||
const toastr = runtime.toastr;
|
||||
|
||||
const context = getContext();
|
||||
const chatStateTarget = resolveCurrentChatStateTarget(context);
|
||||
if (!isLukerPrimaryPersistenceHost(context)) {
|
||||
toastr.info("当前宿主不是 Luker,无需从主 sidecar 重建本地缓存");
|
||||
return { handledToast: true, reason: "not-luker" };
|
||||
}
|
||||
const chatId = getCurrentChatId(context);
|
||||
if (!chatId) {
|
||||
toastr.warning("当前没有聊天上下文");
|
||||
return { handledToast: true, reason: "missing-chat-id" };
|
||||
}
|
||||
|
||||
const loadResult = await loadGraphFromLukerSidecarV2(chatId, {
|
||||
source: "panel-manual-luker-cache-rebuild",
|
||||
allowOverride: true,
|
||||
chatStateTarget,
|
||||
});
|
||||
currentGraph = runtime.getCurrentGraph?.() || null;
|
||||
if (!loadResult?.loaded || !currentGraph) {
|
||||
toastr.warning(
|
||||
`无法从 Luker 主 sidecar 重建本地缓存: ${loadResult?.reason || "sidecar not available"}`,
|
||||
);
|
||||
return { handledToast: true, result: loadResult };
|
||||
}
|
||||
|
||||
queueGraphPersistToIndexedDb(chatId, cloneGraphForPersistence(currentGraph, chatId), {
|
||||
revision: Math.max(
|
||||
Number(graphPersistenceState.lukerManifestRevision || 0),
|
||||
Number(getGraphPersistedRevision(currentGraph) || 0),
|
||||
Number(graphPersistenceState.revision || 0),
|
||||
),
|
||||
reason: "panel-manual-luker-cache-rebuild",
|
||||
persistRole: "cache-mirror",
|
||||
scheduleCloudUpload: false,
|
||||
graphDetached: true,
|
||||
});
|
||||
refreshPanelLiveState();
|
||||
toastr.success("已开始从 Luker 主 sidecar 重建本地缓存");
|
||||
return { handledToast: true, result: loadResult };
|
||||
}
|
||||
|
||||
@@ -192,7 +192,9 @@ import {
|
||||
buildBmeSyncRuntimeOptionsImpl,
|
||||
loadGraphFromChatImpl,
|
||||
maybeCaptureGraphShadowSnapshotImpl,
|
||||
onRebuildLocalCacheFromLukerSidecarImpl,
|
||||
persistExtractionBatchResultImpl,
|
||||
saveGraphToChatImpl,
|
||||
shouldUseAuthorityGraphStoreImpl,
|
||||
shouldUseAuthorityJobsImpl,
|
||||
syncGraphLoadFromLiveContextImpl,
|
||||
@@ -860,7 +862,9 @@ async function createGraphPersistenceHarness({
|
||||
buildBmeSyncRuntimeOptionsImpl,
|
||||
loadGraphFromChatImpl,
|
||||
maybeCaptureGraphShadowSnapshotImpl,
|
||||
onRebuildLocalCacheFromLukerSidecarImpl,
|
||||
persistExtractionBatchResultImpl,
|
||||
saveGraphToChatImpl,
|
||||
shouldUseAuthorityGraphStoreImpl,
|
||||
shouldUseAuthorityJobsImpl,
|
||||
syncGraphLoadFromLiveContextImpl,
|
||||
@@ -1368,6 +1372,39 @@ async function createGraphPersistenceHarness({
|
||||
recordLocalPersistEarlyFailure(reason = "") {
|
||||
runtimeContext.__lastLocalPersistEarlyFailure = String(reason || "");
|
||||
},
|
||||
isGraphLoadStateDbReady(loadState = runtimeContext.graphPersistenceState?.loadState) {
|
||||
return loadState === GRAPH_LOAD_STATES.LOADED || loadState === GRAPH_LOAD_STATES.EMPTY_CONFIRMED;
|
||||
},
|
||||
isGraphMetadataWriteAllowed() {
|
||||
return true;
|
||||
},
|
||||
isLukerPrimaryPersistenceHost(context = runtimeContext.getContext?.()) {
|
||||
return resolveBmeHostProfile(context) === "luker";
|
||||
},
|
||||
async loadGraphFromLukerSidecarV2(chatId, options = {}) {
|
||||
runtimeContext.__lastLukerSidecarLoad = { chatId, options };
|
||||
return { loaded: Boolean(runtimeContext.currentGraph), reason: "test-luker-sidecar" };
|
||||
},
|
||||
normalizeIndexedDbRevision(value, fallbackValue = 0) {
|
||||
const numeric = Number(value);
|
||||
if (Number.isFinite(numeric) && numeric > 0) return Math.floor(numeric);
|
||||
const fallback = Number(fallbackValue);
|
||||
return Number.isFinite(fallback) && fallback > 0 ? Math.floor(fallback) : 0;
|
||||
},
|
||||
resolvePersistRevisionFloor(baseRevision = 0, graph = runtimeContext.currentGraph) {
|
||||
return Math.max(
|
||||
0,
|
||||
Math.floor(Number(baseRevision || 0)),
|
||||
Math.floor(Number(graph?.meta?.revision || 0)),
|
||||
Math.floor(Number(getGraphPersistedRevision(graph) || 0)),
|
||||
);
|
||||
},
|
||||
resolveCurrentChatStateTarget(context = runtimeContext.getContext?.()) {
|
||||
return resolveCurrentBmeChatStateTarget(context);
|
||||
},
|
||||
scheduleBmeIndexedDbTask(task) {
|
||||
return Promise.resolve().then(() => task());
|
||||
},
|
||||
__syncNowCalls: [],
|
||||
async syncNow(chatId, options = {}) {
|
||||
runtimeContext.__syncNowCalls.push({
|
||||
|
||||
Reference in New Issue
Block a user