mirror of
https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology.git
synced 2026-06-13 18:31:16 +08:00
Stabilize recall UI persistence recovery
This commit is contained in:
101
index.js
101
index.js
@@ -1101,7 +1101,7 @@ function updateLastRecalledItems(nodeIds = []) {
|
||||
return;
|
||||
}
|
||||
|
||||
lastRecalledItems = nodeIds
|
||||
lastRecalledItems = normalizeRecallNodeIdList(nodeIds)
|
||||
.map((id) => getNode(currentGraph, id))
|
||||
.filter(Boolean)
|
||||
.slice(0, 8)
|
||||
@@ -1113,6 +1113,61 @@ function updateLastRecalledItems(nodeIds = []) {
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeRecallNodeIdList(nodeIds = []) {
|
||||
if (!Array.isArray(nodeIds)) return [];
|
||||
return nodeIds
|
||||
.map((entry) => {
|
||||
if (typeof entry === "string" || typeof entry === "number") {
|
||||
return String(entry).trim();
|
||||
}
|
||||
if (entry && typeof entry === "object") {
|
||||
return String(entry.id || entry.nodeId || "").trim();
|
||||
}
|
||||
return "";
|
||||
})
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function getLatestPersistedRecallDisplayRecord(chat = getContext()?.chat) {
|
||||
if (!Array.isArray(chat) || chat.length === 0) return null;
|
||||
for (let index = chat.length - 1; index >= 0; index--) {
|
||||
if (!chat[index]?.is_user) continue;
|
||||
const record = readPersistedRecallFromUserMessage(chat, index);
|
||||
if (record?.injectionText) {
|
||||
return {
|
||||
messageIndex: index,
|
||||
record,
|
||||
};
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function restoreRecallUiStateFromPersistence(chat = getContext()?.chat) {
|
||||
const latestPersisted = getLatestPersistedRecallDisplayRecord(chat);
|
||||
const graphRecallNodeIds = normalizeRecallNodeIdList(
|
||||
currentGraph?.lastRecallResult,
|
||||
);
|
||||
const persistedNodeIds = normalizeRecallNodeIdList(
|
||||
latestPersisted?.record?.selectedNodeIds,
|
||||
);
|
||||
const effectiveNodeIds = graphRecallNodeIds.length
|
||||
? graphRecallNodeIds
|
||||
: persistedNodeIds;
|
||||
|
||||
updateLastRecalledItems(effectiveNodeIds);
|
||||
lastInjectionContent = String(latestPersisted?.record?.injectionText || "").trim();
|
||||
|
||||
return {
|
||||
restored: Boolean(lastInjectionContent || effectiveNodeIds.length),
|
||||
latestPersistedMessageIndex: Number.isFinite(latestPersisted?.messageIndex)
|
||||
? latestPersisted.messageIndex
|
||||
: null,
|
||||
selectedNodeIds: effectiveNodeIds,
|
||||
injectionTextLength: lastInjectionContent.length,
|
||||
};
|
||||
}
|
||||
|
||||
function clearRecallInputTracking() {
|
||||
pendingRecallSendIntent = createRecallInputRecord();
|
||||
lastRecallSentUserMessage = createRecallInputRecord();
|
||||
@@ -1309,6 +1364,8 @@ function resolveRecallPersistenceTargetUserMessageIndex(
|
||||
} = {},
|
||||
) {
|
||||
if (!Array.isArray(chat) || chat.length === 0) return null;
|
||||
const normalizedGenerationType =
|
||||
String(generationType || "normal").trim() || "normal";
|
||||
|
||||
const explicitIndex = Number.isFinite(explicitTargetUserMessageIndex)
|
||||
? Math.floor(Number(explicitTargetUserMessageIndex))
|
||||
@@ -1363,8 +1420,28 @@ function resolveRecallPersistenceTargetUserMessageIndex(
|
||||
}
|
||||
}
|
||||
|
||||
// 正常生成阶段里,ST 可能会在真正发送前改写用户文本
|
||||
// (命令展开、包装显示、助手 UI 处理等),导致 hash 已无法精确匹配。
|
||||
// 这时仍应优先回绑到“当前最新 user 楼层”,否则召回记录虽然生成了,
|
||||
// 但 Recall Card 会因为找不到目标楼层而消失。
|
||||
if (
|
||||
String(generationType || "normal").trim() !== "normal" &&
|
||||
normalizedGenerationType === "normal" &&
|
||||
Number.isFinite(latestUserIndex) &&
|
||||
chat[latestUserIndex]?.is_user
|
||||
) {
|
||||
return latestUserIndex;
|
||||
}
|
||||
|
||||
if (
|
||||
normalizedGenerationType === "normal" &&
|
||||
Number.isFinite(preferredMessageId) &&
|
||||
chat[preferredMessageId]?.is_user
|
||||
) {
|
||||
return preferredMessageId;
|
||||
}
|
||||
|
||||
if (
|
||||
normalizedGenerationType !== "normal" &&
|
||||
Number.isFinite(latestUserIndex) &&
|
||||
chat[latestUserIndex]?.is_user
|
||||
) {
|
||||
@@ -3397,8 +3474,9 @@ function applyIndexedDbSnapshotToRuntime(
|
||||
? currentGraph.historyState.extractionCount
|
||||
: 0;
|
||||
lastExtractedItems = [];
|
||||
updateLastRecalledItems(currentGraph.lastRecallResult || []);
|
||||
lastInjectionContent = "";
|
||||
const restoredRecallUi = restoreRecallUiStateFromPersistence(
|
||||
getContext()?.chat,
|
||||
);
|
||||
runtimeStatus = createUiStatus("待命", "已从 IndexedDB 加载聊天图谱", "idle");
|
||||
lastExtractionStatus = createUiStatus(
|
||||
"待命",
|
||||
@@ -3413,7 +3491,9 @@ function applyIndexedDbSnapshotToRuntime(
|
||||
);
|
||||
lastRecallStatus = createUiStatus(
|
||||
"待命",
|
||||
"已从 IndexedDB 加载聊天图谱,等待下一次召回",
|
||||
restoredRecallUi.restored
|
||||
? "已从持久化召回记录恢复显示,等待下一次召回"
|
||||
: "已从 IndexedDB 加载聊天图谱,等待下一次召回",
|
||||
"idle",
|
||||
);
|
||||
|
||||
@@ -3456,6 +3536,7 @@ function applyIndexedDbSnapshotToRuntime(
|
||||
|
||||
removeGraphShadowSnapshot(normalizedChatId);
|
||||
refreshPanelLiveState();
|
||||
schedulePersistedRecallMessageUiRefresh(30);
|
||||
console.debug("[ST-BME] 已从 IndexedDB 加载图谱", {
|
||||
chatId: normalizedChatId,
|
||||
source,
|
||||
@@ -5310,8 +5391,9 @@ function loadGraphFromChat(options = {}) {
|
||||
? currentGraph.historyState.extractionCount
|
||||
: 0;
|
||||
lastExtractedItems = [];
|
||||
updateLastRecalledItems(currentGraph.lastRecallResult || []);
|
||||
lastInjectionContent = "";
|
||||
const restoredRecallUi = restoreRecallUiStateFromPersistence(
|
||||
context?.chat,
|
||||
);
|
||||
runtimeStatus = createUiStatus(
|
||||
"图谱加载中",
|
||||
"已从兼容 metadata 暂载图谱,等待 IndexedDB 权威确认",
|
||||
@@ -5330,7 +5412,9 @@ function loadGraphFromChat(options = {}) {
|
||||
);
|
||||
lastRecallStatus = createUiStatus(
|
||||
"待命",
|
||||
"兼容图谱暂载中,等待 IndexedDB 确认后再执行召回",
|
||||
restoredRecallUi.restored
|
||||
? "已从持久化召回记录恢复显示,等待 IndexedDB 权威确认"
|
||||
: "兼容图谱暂载中,等待 IndexedDB 确认后再执行召回",
|
||||
"idle",
|
||||
);
|
||||
applyGraphLoadState(GRAPH_LOAD_STATES.LOADING, {
|
||||
@@ -5377,6 +5461,7 @@ function loadGraphFromChat(options = {}) {
|
||||
});
|
||||
|
||||
refreshPanelLiveState();
|
||||
schedulePersistedRecallMessageUiRefresh(30);
|
||||
return {
|
||||
success: true,
|
||||
loaded: true,
|
||||
|
||||
@@ -34,6 +34,11 @@ import {
|
||||
getNode,
|
||||
serializeGraph,
|
||||
} from "../graph.js";
|
||||
import {
|
||||
buildPersistedRecallRecord,
|
||||
readPersistedRecallFromUserMessage,
|
||||
} from "../recall-persistence.js";
|
||||
import { getNodeDisplayName } from "../node-labels.js";
|
||||
import { normalizeGraphRuntimeState } from "../runtime-state.js";
|
||||
import {
|
||||
clampFloat,
|
||||
@@ -216,6 +221,7 @@ async function createGraphPersistenceHarness({
|
||||
deserializeGraph,
|
||||
getGraphStats,
|
||||
getNode,
|
||||
getNodeDisplayName,
|
||||
createUiStatus,
|
||||
createGraphPersistenceState,
|
||||
createRecallInputRecord,
|
||||
@@ -229,6 +235,7 @@ async function createGraphPersistenceHarness({
|
||||
clampInt,
|
||||
clampFloat,
|
||||
formatRecallContextLine,
|
||||
readPersistedRecallFromUserMessage,
|
||||
cloneGraphForPersistence,
|
||||
cloneRuntimeDebugValue,
|
||||
onMessageReceivedController,
|
||||
@@ -511,6 +518,12 @@ result = {
|
||||
getCurrentGraph() {
|
||||
return currentGraph;
|
||||
},
|
||||
getLastInjectionContent() {
|
||||
return lastInjectionContent;
|
||||
},
|
||||
getLastRecalledItems() {
|
||||
return lastRecalledItems;
|
||||
},
|
||||
setGraphPersistenceState(patch = {}) {
|
||||
graphPersistenceState = {
|
||||
...graphPersistenceState,
|
||||
@@ -605,6 +618,54 @@ result = {
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
const graph = createMeaningfulGraph("chat-recall-ui", "recall-ui");
|
||||
graph.nodes[0].id = "restore-node";
|
||||
graph.lastRecallResult = [{ id: "restore-node" }];
|
||||
stampPersistedGraph(graph, {
|
||||
revision: 7,
|
||||
chatId: "chat-recall-ui",
|
||||
reason: "recall-ui-restore",
|
||||
});
|
||||
|
||||
const harness = await createGraphPersistenceHarness({
|
||||
chatId: "chat-recall-ui",
|
||||
globalChatId: "chat-recall-ui",
|
||||
indexedDbSnapshot: buildSnapshotFromGraph(graph, {
|
||||
chatId: "chat-recall-ui",
|
||||
revision: 7,
|
||||
}),
|
||||
chat: [
|
||||
{
|
||||
is_user: true,
|
||||
mes: "用户楼层",
|
||||
extra: {
|
||||
bme_recall: buildPersistedRecallRecord({
|
||||
injectionText: "已持久化的召回注入",
|
||||
selectedNodeIds: [],
|
||||
nowIso: "2026-01-01T00:00:00.000Z",
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
is_user: false,
|
||||
mes: "assistant",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const result = harness.api.syncGraphLoadFromLiveContext({
|
||||
source: "indexeddb-recall-ui-restore",
|
||||
});
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
|
||||
assert.equal(result.synced, true);
|
||||
assert.equal(harness.api.getGraphPersistenceState().dbReady, true);
|
||||
assert.equal(harness.api.getLastInjectionContent(), "已持久化的召回注入");
|
||||
assert.equal(harness.api.getLastRecalledItems().length, 1);
|
||||
assert.equal(harness.api.getLastRecalledItems()[0]?.id, "restore-node");
|
||||
}
|
||||
|
||||
{
|
||||
const harness = await createGraphPersistenceHarness({
|
||||
chatId: "",
|
||||
|
||||
@@ -3594,6 +3594,44 @@ async function testGenerationRecallFinalInjectionRebindsLatestMatchingUserFloor(
|
||||
|
||||
assert.equal(resolution.targetUserMessageIndex, 0);
|
||||
}
|
||||
|
||||
{
|
||||
const harness = await createGenerationRecallHarness({ realApplyFinal: true });
|
||||
harness.chat = [
|
||||
{ is_user: true, mes: "酒馆最终写入的用户楼层文本" },
|
||||
{ is_user: false, mes: "assistant-tail" },
|
||||
];
|
||||
harness.result.recordRecallSentUserMessage(0, "发送前捕获的原始文本", "message-sent");
|
||||
|
||||
const resolution =
|
||||
harness.result.applyFinalRecallInjectionForGeneration({
|
||||
generationType: "normal",
|
||||
hookName: "GENERATION_AFTER_COMMANDS",
|
||||
freshRecallResult: {
|
||||
status: "completed",
|
||||
didRecall: true,
|
||||
injectionText: "fresh-memory",
|
||||
sourceCandidates: [
|
||||
{
|
||||
text: "发送前捕获的原始文本",
|
||||
},
|
||||
],
|
||||
},
|
||||
transaction: {
|
||||
frozenRecallOptions: {
|
||||
generationType: "normal",
|
||||
targetUserMessageIndex: null,
|
||||
overrideUserMessage: "发送前捕获的原始文本",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(
|
||||
resolution.targetUserMessageIndex,
|
||||
0,
|
||||
"normal 生成时即便用户文本被宿主改写,也应回绑到最新 user 楼层",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function testRecallSubGraphAndDataLayerEntryPoints() {
|
||||
|
||||
Reference in New Issue
Block a user