mirror of
https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology.git
synced 2026-05-15 22:30:38 +08:00
refactor: deepen indexeddb-first load path and finalize phase ef docs/scripts
This commit is contained in:
38
README.md
38
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 | 触发历史变动检测与恢复 |
|
||||
|
||||
### 召回流水线
|
||||
|
||||
468
index.js
468
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,35 +3871,27 @@ 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,
|
||||
{
|
||||
applyGraphLoadState(GRAPH_LOAD_STATES.LOADING, {
|
||||
chatId: "",
|
||||
reason: shouldRetry ? "chat-id-missing" : "chat-id-timeout",
|
||||
reason: "chat-id-missing",
|
||||
attemptIndex,
|
||||
revision: 0,
|
||||
lastPersistedRevision: 0,
|
||||
@@ -3930,24 +3902,15 @@ function loadGraphFromChat(options = {}) {
|
||||
shadowSnapshotRevision: 0,
|
||||
shadowSnapshotUpdatedAt: "",
|
||||
shadowSnapshotReason: "",
|
||||
dbReady: false,
|
||||
writesBlocked: true,
|
||||
},
|
||||
);
|
||||
if (shouldRetry) {
|
||||
scheduleGraphLoadRetry("", "chat-id-missing", attemptIndex, {
|
||||
allowPendingChat: true,
|
||||
});
|
||||
} else {
|
||||
clearPendingGraphLoadRetry();
|
||||
}
|
||||
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,139 +3953,48 @@ 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;
|
||||
const cachedSnapshot = readCachedIndexedDbSnapshot(chatId);
|
||||
if (isIndexedDbSnapshotMeaningful(cachedSnapshot)) {
|
||||
const cachedResult = applyIndexedDbSnapshotToRuntime(chatId, cachedSnapshot, {
|
||||
source: `${source}:indexeddb-cache`,
|
||||
attemptIndex,
|
||||
});
|
||||
if (cachedResult?.loaded) {
|
||||
clearPendingGraphLoadRetry();
|
||||
return cachedResult;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasOfficialGraph) {
|
||||
const savedData = allowMetadataFallback
|
||||
? context?.chatMetadata?.[GRAPH_METADATA_KEY]
|
||||
: undefined;
|
||||
if (savedData != null && savedData !== "") {
|
||||
try {
|
||||
const officialGraph = cloneGraphForPersistence(
|
||||
normalizeGraphRuntimeState(deserializeGraph(savedData), chatId),
|
||||
chatId,
|
||||
);
|
||||
const officialRevision = Math.max(
|
||||
1,
|
||||
getGraphPersistedRevision(officialGraph),
|
||||
);
|
||||
const metadataIntegrity = getChatMetadataIntegrity(context);
|
||||
|
||||
if (shouldPreferShadowSnapshotOverOfficial(officialGraph, shadowSnapshot)) {
|
||||
clearPendingGraphLoadRetry();
|
||||
currentGraph = cloneGraphForPersistence(
|
||||
normalizeGraphRuntimeState(
|
||||
deserializeGraph(shadowSnapshot.serializedGraph),
|
||||
chatId,
|
||||
),
|
||||
chatId,
|
||||
);
|
||||
extractionCount = Number.isFinite(
|
||||
currentGraph?.historyState?.extractionCount,
|
||||
)
|
||||
? currentGraph.historyState.extractionCount
|
||||
: 0;
|
||||
lastExtractedItems = [];
|
||||
updateLastRecalledItems(currentGraph.lastRecallResult || []);
|
||||
lastInjectionContent = "";
|
||||
runtimeStatus = createUiStatus(
|
||||
"待命",
|
||||
"已恢复本次会话里较新的图谱,正在补写正式聊天元数据",
|
||||
"warning",
|
||||
);
|
||||
lastExtractionStatus = createUiStatus(
|
||||
"待命",
|
||||
"检测到本次会话里有更新的图谱快照,正在补写正式元数据",
|
||||
"warning",
|
||||
);
|
||||
lastVectorStatus = createUiStatus(
|
||||
"待命",
|
||||
"检测到本次会话里有更新的图谱快照,正在补写正式元数据",
|
||||
"warning",
|
||||
);
|
||||
lastRecallStatus = createUiStatus(
|
||||
"待命",
|
||||
"检测到本次会话里有更新的图谱快照,正在补写正式元数据",
|
||||
"warning",
|
||||
);
|
||||
applyGraphLoadState(GRAPH_LOAD_STATES.LOADED, {
|
||||
chatId,
|
||||
reason: "shadow-snapshot-newer-than-official",
|
||||
attemptIndex,
|
||||
revision: Math.max(shadowSnapshot.revision || 0, 1),
|
||||
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,
|
||||
};
|
||||
}
|
||||
const officialRevision = Math.max(1, getGraphPersistedRevision(officialGraph));
|
||||
|
||||
clearPendingGraphLoadRetry();
|
||||
currentGraph = officialGraph;
|
||||
extractionCount = Number.isFinite(
|
||||
currentGraph?.historyState?.extractionCount,
|
||||
)
|
||||
extractionCount = Number.isFinite(currentGraph?.historyState?.extractionCount)
|
||||
? currentGraph.historyState.extractionCount
|
||||
: 0;
|
||||
lastExtractedItems = [];
|
||||
updateLastRecalledItems(currentGraph.lastRecallResult || []);
|
||||
lastInjectionContent = "";
|
||||
runtimeStatus = createUiStatus(
|
||||
"待命",
|
||||
"已加载聊天图谱,等待下一次任务",
|
||||
"idle",
|
||||
);
|
||||
lastExtractionStatus = createUiStatus(
|
||||
"待命",
|
||||
"已加载聊天图谱,等待下一次提取",
|
||||
"idle",
|
||||
);
|
||||
runtimeStatus = createUiStatus("待命", "已从兼容 metadata 加载图谱", "idle");
|
||||
lastExtractionStatus = createUiStatus("待命", "已加载聊天图谱,等待下一次提取", "idle");
|
||||
lastVectorStatus = createUiStatus(
|
||||
"待命",
|
||||
currentGraph.vectorIndexState?.lastWarning ||
|
||||
"已加载聊天图谱,等待下一次向量任务",
|
||||
"idle",
|
||||
);
|
||||
lastRecallStatus = createUiStatus(
|
||||
"待命",
|
||||
"已加载聊天图谱,等待下一次召回",
|
||||
currentGraph.vectorIndexState?.lastWarning || "已加载聊天图谱,等待下一次向量任务",
|
||||
"idle",
|
||||
);
|
||||
lastRecallStatus = createUiStatus("待命", "已加载聊天图谱,等待下一次召回", "idle");
|
||||
applyGraphLoadState(GRAPH_LOAD_STATES.LOADED, {
|
||||
chatId,
|
||||
reason: source,
|
||||
reason: `${source}:metadata-compat`,
|
||||
attemptIndex,
|
||||
revision: officialRevision,
|
||||
lastPersistedRevision: officialRevision,
|
||||
@@ -4132,281 +4005,66 @@ function loadGraphFromChat(options = {}) {
|
||||
shadowSnapshotRevision: 0,
|
||||
shadowSnapshotUpdatedAt: "",
|
||||
shadowSnapshotReason: "",
|
||||
dbReady: true,
|
||||
writesBlocked: false,
|
||||
});
|
||||
updateGraphPersistenceState({
|
||||
metadataIntegrity,
|
||||
lastPersistMode: "",
|
||||
queuedPersistMode: "",
|
||||
queuedPersistRotateIntegrity: false,
|
||||
queuedPersistReason: "",
|
||||
metadataIntegrity: getChatMetadataIntegrity(context),
|
||||
storagePrimary: "metadata",
|
||||
storageMode: "metadata",
|
||||
dbReady: true,
|
||||
indexedDbLastError: "",
|
||||
});
|
||||
removeGraphShadowSnapshot(chatId);
|
||||
|
||||
console.log("[ST-BME] 从聊天数据加载图谱:", {
|
||||
chatId,
|
||||
source,
|
||||
scheduleIndexedDbGraphProbe(chatId, {
|
||||
source: `${source}:indexeddb-probe`,
|
||||
attemptIndex,
|
||||
...getGraphStats(currentGraph),
|
||||
allowOverride: true,
|
||||
applyEmptyState: true,
|
||||
});
|
||||
|
||||
refreshPanelLiveState();
|
||||
return {
|
||||
success: true,
|
||||
loaded: true,
|
||||
loadState: GRAPH_LOAD_STATES.LOADED,
|
||||
reason: source,
|
||||
reason: `${source}:metadata-compat`,
|
||||
chatId,
|
||||
attemptIndex,
|
||||
shadowSnapshotUsed: false,
|
||||
};
|
||||
} catch (error) {
|
||||
console.warn("[ST-BME] 兼容 metadata 图谱读取失败,将回退 IndexedDB:", error);
|
||||
}
|
||||
}
|
||||
|
||||
if (shadowSnapshot) {
|
||||
currentGraph = normalizeGraphRuntimeState(
|
||||
deserializeGraph(shadowSnapshot.serializedGraph),
|
||||
applyGraphLoadState(GRAPH_LOAD_STATES.LOADING, {
|
||||
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",
|
||||
reason: `indexeddb-probe-pending:${String(source || "direct-load")}`,
|
||||
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,
|
||||
dbReady: false,
|
||||
writesBlocked: true,
|
||||
});
|
||||
updateGraphPersistenceState({
|
||||
queuedPersistMode: "immediate",
|
||||
queuedPersistRotateIntegrity: false,
|
||||
queuedPersistReason: shouldRetry
|
||||
? "shadow-snapshot-restored"
|
||||
: "shadow-snapshot-blocked",
|
||||
storagePrimary: "metadata",
|
||||
storageMode: "metadata",
|
||||
storagePrimary: "indexeddb",
|
||||
storageMode: "indexeddb",
|
||||
dbReady: false,
|
||||
indexedDbLastError: "",
|
||||
});
|
||||
if (shouldRetry) {
|
||||
scheduleGraphLoadRetry(
|
||||
chatId,
|
||||
hasChatMetadata
|
||||
? "official-graph-missing-shadow"
|
||||
: "chat-metadata-missing-shadow",
|
||||
scheduleIndexedDbGraphProbe(chatId, {
|
||||
source: `${source}:indexeddb-probe`,
|
||||
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,
|
||||
shadowSnapshotUsed: false,
|
||||
shadowSnapshotRevision: 0,
|
||||
shadowSnapshotUpdatedAt: "",
|
||||
shadowSnapshotReason: "",
|
||||
writesBlocked: true,
|
||||
},
|
||||
);
|
||||
if (shouldRetry) {
|
||||
scheduleGraphLoadRetry(
|
||||
chatId,
|
||||
hasChatMetadata ? "graph-metadata-missing" : "chat-metadata-missing",
|
||||
attemptIndex,
|
||||
);
|
||||
} else {
|
||||
clearPendingGraphLoadRetry();
|
||||
}
|
||||
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, {
|
||||
chatId,
|
||||
reason: "metadata-confirmed-empty",
|
||||
attemptIndex,
|
||||
revision: 0,
|
||||
lastPersistedRevision: 0,
|
||||
queuedPersistRevision: 0,
|
||||
queuedPersistChatId: "",
|
||||
pendingPersist: false,
|
||||
shadowSnapshotUsed: false,
|
||||
shadowSnapshotRevision: 0,
|
||||
shadowSnapshotUpdatedAt: "",
|
||||
shadowSnapshotReason: "",
|
||||
writesBlocked: false,
|
||||
allowOverride: true,
|
||||
applyEmptyState: true,
|
||||
});
|
||||
updateGraphPersistenceState({
|
||||
metadataIntegrity: getChatMetadataIntegrity(context),
|
||||
queuedPersistMode: "",
|
||||
queuedPersistRotateIntegrity: false,
|
||||
queuedPersistReason: "",
|
||||
storagePrimary: "metadata",
|
||||
storageMode: "metadata",
|
||||
indexedDbLastError: "",
|
||||
});
|
||||
removeGraphShadowSnapshot(chatId);
|
||||
refreshPanelLiveState();
|
||||
|
||||
return {
|
||||
success: false,
|
||||
loaded: false,
|
||||
loadState: confirmedState,
|
||||
reason: graphPersistenceState.reason,
|
||||
loadState: GRAPH_LOAD_STATES.LOADING,
|
||||
reason: "indexeddb-probe-pending",
|
||||
chatId,
|
||||
attemptIndex,
|
||||
shadowSnapshotUsed: false,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
10
package.json
10
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"
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user