mirror of
https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology.git
synced 2026-05-15 22:30:38 +08:00
fix: 重构生成前注入与历史回滚链路
This commit is contained in:
20
graph.js
20
graph.js
@@ -11,7 +11,7 @@ import {
|
|||||||
/**
|
/**
|
||||||
* 图状态版本号
|
* 图状态版本号
|
||||||
*/
|
*/
|
||||||
const GRAPH_VERSION = 4;
|
const GRAPH_VERSION = 5;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 生成 UUID v4
|
* 生成 UUID v4
|
||||||
@@ -510,6 +510,20 @@ export function deserializeGraph(json) {
|
|||||||
: createDefaultBatchJournal();
|
: createDefaultBatchJournal();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (data.version < 5) {
|
||||||
|
data.historyState = {
|
||||||
|
...createDefaultHistoryState(),
|
||||||
|
...(data.historyState || {}),
|
||||||
|
extractionCount: Number.isFinite(data?.historyState?.extractionCount)
|
||||||
|
? data.historyState.extractionCount
|
||||||
|
: 0,
|
||||||
|
lastMutationSource: String(data?.historyState?.lastMutationSource || ""),
|
||||||
|
};
|
||||||
|
data.batchJournal = Array.isArray(data.batchJournal)
|
||||||
|
? data.batchJournal
|
||||||
|
: createDefaultBatchJournal();
|
||||||
|
}
|
||||||
|
|
||||||
data.version = GRAPH_VERSION;
|
data.version = GRAPH_VERSION;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -550,6 +564,10 @@ export function deserializeGraph(json) {
|
|||||||
)
|
)
|
||||||
? data.historyState.lastProcessedAssistantFloor
|
? data.historyState.lastProcessedAssistantFloor
|
||||||
: data.lastProcessedSeq,
|
: data.lastProcessedSeq,
|
||||||
|
extractionCount: Number.isFinite(data?.historyState?.extractionCount)
|
||||||
|
? data.historyState.extractionCount
|
||||||
|
: 0,
|
||||||
|
lastMutationSource: String(data?.historyState?.lastMutationSource || ""),
|
||||||
};
|
};
|
||||||
data.vectorIndexState = {
|
data.vectorIndexState = {
|
||||||
...createDefaultVectorIndexState(data?.historyState?.chatId || ""),
|
...createDefaultVectorIndexState(data?.historyState?.chatId || ""),
|
||||||
|
|||||||
555
index.js
555
index.js
@@ -44,11 +44,13 @@ import {
|
|||||||
findJournalRecoveryPoint,
|
findJournalRecoveryPoint,
|
||||||
markHistoryDirty,
|
markHistoryDirty,
|
||||||
normalizeGraphRuntimeState,
|
normalizeGraphRuntimeState,
|
||||||
|
rollbackBatch,
|
||||||
snapshotProcessedMessageHashes,
|
snapshotProcessedMessageHashes,
|
||||||
} from "./runtime-state.js";
|
} from "./runtime-state.js";
|
||||||
import { DEFAULT_NODE_SCHEMA, validateSchema } from "./schema.js";
|
import { DEFAULT_NODE_SCHEMA, validateSchema } from "./schema.js";
|
||||||
import {
|
import {
|
||||||
BACKEND_VECTOR_SOURCES,
|
BACKEND_VECTOR_SOURCES,
|
||||||
|
deleteBackendVectorHashesForRecovery,
|
||||||
getVectorConfigFromSettings,
|
getVectorConfigFromSettings,
|
||||||
getVectorIndexStats,
|
getVectorIndexStats,
|
||||||
isBackendVectorConfig,
|
isBackendVectorConfig,
|
||||||
@@ -192,6 +194,9 @@ let sendIntentHookRetryTimer = null;
|
|||||||
let pendingHistoryRecoveryTimer = null;
|
let pendingHistoryRecoveryTimer = null;
|
||||||
let pendingHistoryRecoveryTrigger = "";
|
let pendingHistoryRecoveryTrigger = "";
|
||||||
let pendingHistoryMutationCheckTimers = [];
|
let pendingHistoryMutationCheckTimers = [];
|
||||||
|
let skipBeforeCombineRecallUntil = 0;
|
||||||
|
let lastPreGenerationRecallKey = "";
|
||||||
|
let lastPreGenerationRecallAt = 0;
|
||||||
const stageNoticeHandles = {
|
const stageNoticeHandles = {
|
||||||
extraction: null,
|
extraction: null,
|
||||||
vector: null,
|
vector: null,
|
||||||
@@ -559,6 +564,17 @@ function registerBeforeCombinePrompts(listener) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function registerGenerationAfterCommands(listener) {
|
||||||
|
const makeFirst = globalThis.eventMakeFirst;
|
||||||
|
if (typeof makeFirst === "function") {
|
||||||
|
return makeFirst(event_types.GENERATION_AFTER_COMMANDS, listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.warn("[ST-BME] eventMakeFirst 不可用,GENERATION_AFTER_COMMANDS 回退到普通事件注册");
|
||||||
|
eventSource.on(event_types.GENERATION_AFTER_COMMANDS, listener);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
function installSendIntentHooks() {
|
function installSendIntentHooks() {
|
||||||
for (const cleanup of sendIntentHookCleanup.splice(0, sendIntentHookCleanup.length)) {
|
for (const cleanup of sendIntentHookCleanup.splice(0, sendIntentHookCleanup.length)) {
|
||||||
try {
|
try {
|
||||||
@@ -839,6 +855,7 @@ async function recordGraphMutation({
|
|||||||
artifactTags = [],
|
artifactTags = [],
|
||||||
syncRange = null,
|
syncRange = null,
|
||||||
signal = undefined,
|
signal = undefined,
|
||||||
|
extractionCountBefore = extractionCount,
|
||||||
} = {}) {
|
} = {}) {
|
||||||
ensureCurrentGraphRuntimeState();
|
ensureCurrentGraphRuntimeState();
|
||||||
const vectorSync = await syncVectorState({
|
const vectorSync = await syncVectorState({
|
||||||
@@ -865,6 +882,7 @@ async function recordGraphMutation({
|
|||||||
artifactTags,
|
artifactTags,
|
||||||
),
|
),
|
||||||
vectorHashesInserted: vectorSync?.insertedHashes || [],
|
vectorHashesInserted: vectorSync?.insertedHashes || [],
|
||||||
|
extractionCountBefore,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
saveGraphToChat();
|
saveGraphToChat();
|
||||||
@@ -1195,7 +1213,9 @@ function loadGraphFromChat() {
|
|||||||
currentGraph = normalizeGraphRuntimeState(createEmptyGraph(), chatId);
|
currentGraph = normalizeGraphRuntimeState(createEmptyGraph(), chatId);
|
||||||
}
|
}
|
||||||
|
|
||||||
extractionCount = 0;
|
extractionCount = Number.isFinite(currentGraph?.historyState?.extractionCount)
|
||||||
|
? currentGraph.historyState.extractionCount
|
||||||
|
: 0;
|
||||||
lastExtractedItems = [];
|
lastExtractedItems = [];
|
||||||
updateLastRecalledItems(currentGraph.lastRecallResult || []);
|
updateLastRecalledItems(currentGraph.lastRecallResult || []);
|
||||||
lastInjectionContent = "";
|
lastInjectionContent = "";
|
||||||
@@ -1218,6 +1238,7 @@ function saveGraphToChat() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ensureCurrentGraphRuntimeState();
|
ensureCurrentGraphRuntimeState();
|
||||||
|
currentGraph.historyState.extractionCount = extractionCount;
|
||||||
context.chatMetadata[GRAPH_METADATA_KEY] = currentGraph;
|
context.chatMetadata[GRAPH_METADATA_KEY] = currentGraph;
|
||||||
saveMetadataDebounced();
|
saveMetadataDebounced();
|
||||||
return true;
|
return true;
|
||||||
@@ -1463,6 +1484,56 @@ function resolveRecallInput(chat, recentContextMessageLimit, override = null) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildGenerationAfterCommandsRecallInput(type, params = {}, chat) {
|
||||||
|
if (params?.automatic_trigger || params?.quiet_prompt) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const generationType = String(type || "").trim() || "normal";
|
||||||
|
if (!["normal", "continue", "regenerate", "swipe"].includes(generationType)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (generationType === "normal") {
|
||||||
|
const lastNonSystemMessage = getLastNonSystemChatMessage(chat);
|
||||||
|
const tailUserText = lastNonSystemMessage?.is_user
|
||||||
|
? normalizeRecallInputText(lastNonSystemMessage?.mes || "")
|
||||||
|
: "";
|
||||||
|
const textareaText = normalizeRecallInputText(
|
||||||
|
pendingRecallSendIntent.text || getSendTextareaValue(),
|
||||||
|
);
|
||||||
|
const userMessage = tailUserText || textareaText;
|
||||||
|
if (!userMessage) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
overrideUserMessage: userMessage,
|
||||||
|
overrideSource: tailUserText ? "chat-tail-user" : "send-intent",
|
||||||
|
overrideSourceLabel: tailUserText ? "当前用户楼层" : "发送意图",
|
||||||
|
includeSyntheticUserMessage: !tailUserText,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const latestUserText = normalizeRecallInputText(
|
||||||
|
getLatestUserChatMessage(chat)?.mes || lastRecallSentUserMessage.text,
|
||||||
|
);
|
||||||
|
if (!latestUserText) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
overrideUserMessage: latestUserText,
|
||||||
|
overrideSource: "chat-last-user",
|
||||||
|
overrideSourceLabel: "历史最后用户楼层",
|
||||||
|
includeSyntheticUserMessage: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildPreGenerationRecallKey(type, options = {}) {
|
||||||
|
return [
|
||||||
|
getCurrentChatId(),
|
||||||
|
String(type || "normal").trim() || "normal",
|
||||||
|
hashRecallInput(options.overrideUserMessage || ""),
|
||||||
|
].join(":");
|
||||||
|
}
|
||||||
|
|
||||||
function getCurrentChatSeq(context = getContext()) {
|
function getCurrentChatSeq(context = getContext()) {
|
||||||
const chat = context?.chat;
|
const chat = context?.chat;
|
||||||
if (Array.isArray(chat) && chat.length > 0) {
|
if (Array.isArray(chat) && chat.length > 0) {
|
||||||
@@ -1476,6 +1547,8 @@ async function handleExtractionSuccess(result, endIdx, settings, signal = undefi
|
|||||||
const warnings = [];
|
const warnings = [];
|
||||||
throwIfAborted(signal, "提取已终止");
|
throwIfAborted(signal, "提取已终止");
|
||||||
extractionCount++;
|
extractionCount++;
|
||||||
|
ensureCurrentGraphRuntimeState();
|
||||||
|
currentGraph.historyState.extractionCount = extractionCount;
|
||||||
updateLastExtractedItems(result.newNodeIds || []);
|
updateLastExtractedItems(result.newNodeIds || []);
|
||||||
|
|
||||||
if (settings.enableEvolution && result.newNodeIds?.length > 0) {
|
if (settings.enableEvolution && result.newNodeIds?.length > 0) {
|
||||||
@@ -1606,6 +1679,96 @@ function buildExtractionMessages(chat, startIdx, endIdx, settings) {
|
|||||||
return messages;
|
return messages;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getChatIndexForPlayableSeq(chat, playableSeq) {
|
||||||
|
if (!Array.isArray(chat) || !Number.isFinite(playableSeq)) return null;
|
||||||
|
|
||||||
|
let currentSeq = -1;
|
||||||
|
for (let index = 0; index < chat.length; index++) {
|
||||||
|
const message = chat[index];
|
||||||
|
if (message?.is_system) continue;
|
||||||
|
currentSeq++;
|
||||||
|
if (currentSeq >= playableSeq) {
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return chat.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getChatIndexForAssistantSeq(chat, assistantSeq) {
|
||||||
|
if (!Array.isArray(chat) || !Number.isFinite(assistantSeq)) return null;
|
||||||
|
|
||||||
|
let currentSeq = -1;
|
||||||
|
for (let index = 0; index < chat.length; index++) {
|
||||||
|
if (!isAssistantChatMessage(chat[index])) continue;
|
||||||
|
currentSeq++;
|
||||||
|
if (currentSeq >= assistantSeq) {
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return chat.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveDirtyFloorFromMutationMeta(trigger, primaryArg, meta, chat) {
|
||||||
|
if (!meta || typeof meta !== "object") return null;
|
||||||
|
|
||||||
|
const candidates = [];
|
||||||
|
if (Number.isFinite(meta.messageId)) {
|
||||||
|
candidates.push({
|
||||||
|
floor: meta.messageId,
|
||||||
|
source: `${trigger}-meta`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (Number.isFinite(meta.deletedPlayableSeqFrom)) {
|
||||||
|
const floor = getChatIndexForPlayableSeq(chat, meta.deletedPlayableSeqFrom);
|
||||||
|
if (Number.isFinite(floor)) {
|
||||||
|
candidates.push({
|
||||||
|
floor,
|
||||||
|
source: `${trigger}-meta`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (Number.isFinite(meta.deletedAssistantSeqFrom)) {
|
||||||
|
const floor = getChatIndexForAssistantSeq(chat, meta.deletedAssistantSeqFrom);
|
||||||
|
if (Number.isFinite(floor)) {
|
||||||
|
candidates.push({
|
||||||
|
floor,
|
||||||
|
source: `${trigger}-meta`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (Number.isFinite(meta.playableSeq)) {
|
||||||
|
const floor = getChatIndexForPlayableSeq(chat, meta.playableSeq);
|
||||||
|
if (Number.isFinite(floor)) {
|
||||||
|
candidates.push({
|
||||||
|
floor,
|
||||||
|
source: `${trigger}-meta`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (Number.isFinite(meta.assistantSeq)) {
|
||||||
|
const floor = getChatIndexForAssistantSeq(chat, meta.assistantSeq);
|
||||||
|
if (Number.isFinite(floor)) {
|
||||||
|
candidates.push({
|
||||||
|
floor,
|
||||||
|
source: `${trigger}-meta`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (trigger !== "message-deleted" && Number.isFinite(primaryArg)) {
|
||||||
|
candidates.push({
|
||||||
|
floor: primaryArg,
|
||||||
|
source: `${trigger}-meta`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (candidates.length === 0) return null;
|
||||||
|
return candidates.reduce((earliest, current) =>
|
||||||
|
current.floor < earliest.floor ? current : earliest,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function getLastProcessedAssistantFloor() {
|
function getLastProcessedAssistantFloor() {
|
||||||
ensureCurrentGraphRuntimeState();
|
ensureCurrentGraphRuntimeState();
|
||||||
return Number.isFinite(currentGraph?.historyState?.lastProcessedAssistantFloor)
|
return Number.isFinite(currentGraph?.historyState?.lastProcessedAssistantFloor)
|
||||||
@@ -1675,7 +1838,7 @@ function scheduleImmediateHistoryRecovery(
|
|||||||
}, delayMs);
|
}, delayMs);
|
||||||
}
|
}
|
||||||
|
|
||||||
function scheduleHistoryMutationRecheck(trigger = "history-change") {
|
function scheduleHistoryMutationRecheck(trigger = "history-change", primaryArg = null, meta = null) {
|
||||||
if (!getSettings().enabled) return;
|
if (!getSettings().enabled) return;
|
||||||
|
|
||||||
clearPendingHistoryMutationChecks();
|
clearPendingHistoryMutationChecks();
|
||||||
@@ -1701,7 +1864,7 @@ function scheduleHistoryMutationRecheck(trigger = "history-change") {
|
|||||||
);
|
);
|
||||||
if (!getSettings().enabled) return;
|
if (!getSettings().enabled) return;
|
||||||
|
|
||||||
const detection = inspectHistoryMutation(`settled:${trigger}`);
|
const detection = inspectHistoryMutation(`settled:${trigger}`, primaryArg, meta);
|
||||||
if (
|
if (
|
||||||
detection.dirty ||
|
detection.dirty ||
|
||||||
Number.isFinite(currentGraph?.historyState?.historyDirtyFrom)
|
Number.isFinite(currentGraph?.historyState?.historyDirtyFrom)
|
||||||
@@ -1718,12 +1881,39 @@ function scheduleHistoryMutationRecheck(trigger = "history-change") {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function inspectHistoryMutation(trigger = "history-change") {
|
function inspectHistoryMutation(trigger = "history-change", primaryArg = null, meta = null) {
|
||||||
if (!currentGraph) return { dirty: false, earliestAffectedFloor: null, reason: "" };
|
if (!currentGraph) return { dirty: false, earliestAffectedFloor: null, reason: "" };
|
||||||
|
|
||||||
ensureCurrentGraphRuntimeState();
|
ensureCurrentGraphRuntimeState();
|
||||||
const context = getContext();
|
const context = getContext();
|
||||||
const chat = context?.chat;
|
const chat = context?.chat;
|
||||||
|
const metaDetection = resolveDirtyFloorFromMutationMeta(
|
||||||
|
trigger,
|
||||||
|
primaryArg,
|
||||||
|
meta,
|
||||||
|
chat,
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
metaDetection &&
|
||||||
|
Number.isFinite(metaDetection.floor) &&
|
||||||
|
metaDetection.floor <= getLastProcessedAssistantFloor()
|
||||||
|
) {
|
||||||
|
clearInjectionState();
|
||||||
|
markHistoryDirty(
|
||||||
|
currentGraph,
|
||||||
|
metaDetection.floor,
|
||||||
|
`${trigger} 元数据检测到楼层变动`,
|
||||||
|
metaDetection.source,
|
||||||
|
);
|
||||||
|
saveGraphToChat();
|
||||||
|
notifyHistoryDirty(metaDetection.floor, `${trigger} 元数据检测到楼层变动`);
|
||||||
|
return {
|
||||||
|
dirty: true,
|
||||||
|
earliestAffectedFloor: metaDetection.floor,
|
||||||
|
reason: `${trigger} 元数据检测到楼层变动`,
|
||||||
|
source: metaDetection.source,
|
||||||
|
};
|
||||||
|
}
|
||||||
const detection = detectHistoryMutation(chat, currentGraph.historyState);
|
const detection = detectHistoryMutation(chat, currentGraph.historyState);
|
||||||
|
|
||||||
if (detection.dirty) {
|
if (detection.dirty) {
|
||||||
@@ -1732,10 +1922,14 @@ function inspectHistoryMutation(trigger = "history-change") {
|
|||||||
currentGraph,
|
currentGraph,
|
||||||
detection.earliestAffectedFloor,
|
detection.earliestAffectedFloor,
|
||||||
detection.reason || trigger,
|
detection.reason || trigger,
|
||||||
|
"hash-recheck",
|
||||||
);
|
);
|
||||||
saveGraphToChat();
|
saveGraphToChat();
|
||||||
notifyHistoryDirty(detection.earliestAffectedFloor, detection.reason);
|
notifyHistoryDirty(detection.earliestAffectedFloor, detection.reason);
|
||||||
return detection;
|
return {
|
||||||
|
...detection,
|
||||||
|
source: "hash-recheck",
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (trigger === "message-edited" || trigger === "message-swiped") {
|
if (trigger === "message-edited" || trigger === "message-swiped") {
|
||||||
@@ -1763,23 +1957,31 @@ async function purgeCurrentVectorCollection(signal = undefined) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function prepareVectorStateForReplay(fullReset = false, signal = undefined) {
|
async function prepareVectorStateForReplay(
|
||||||
|
fullReset = false,
|
||||||
|
signal = undefined,
|
||||||
|
{ skipBackendPurge = false } = {},
|
||||||
|
) {
|
||||||
ensureCurrentGraphRuntimeState();
|
ensureCurrentGraphRuntimeState();
|
||||||
const config = getEmbeddingConfig();
|
const config = getEmbeddingConfig();
|
||||||
|
|
||||||
if (isBackendVectorConfig(config)) {
|
if (isBackendVectorConfig(config)) {
|
||||||
try {
|
if (!skipBackendPurge) {
|
||||||
await purgeCurrentVectorCollection(signal);
|
try {
|
||||||
} catch (error) {
|
await purgeCurrentVectorCollection(signal);
|
||||||
if (isAbortError(error)) {
|
} catch (error) {
|
||||||
throw error;
|
if (isAbortError(error)) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
console.warn("[ST-BME] 清理后端向量索引失败,继续本地恢复:", error);
|
||||||
}
|
}
|
||||||
console.warn("[ST-BME] 清理后端向量索引失败,继续本地恢复:", error);
|
currentGraph.vectorIndexState.hashToNodeId = {};
|
||||||
|
currentGraph.vectorIndexState.nodeToHash = {};
|
||||||
}
|
}
|
||||||
currentGraph.vectorIndexState.hashToNodeId = {};
|
|
||||||
currentGraph.vectorIndexState.nodeToHash = {};
|
|
||||||
currentGraph.vectorIndexState.dirty = true;
|
currentGraph.vectorIndexState.dirty = true;
|
||||||
currentGraph.vectorIndexState.lastWarning = "历史恢复后需要重建后端向量索引";
|
currentGraph.vectorIndexState.lastWarning = skipBackendPurge
|
||||||
|
? "历史恢复后需要修复受影响后缀的后端向量索引"
|
||||||
|
: "历史恢复后需要重建后端向量索引";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1802,6 +2004,7 @@ async function executeExtractionBatch({
|
|||||||
ensureCurrentGraphRuntimeState();
|
ensureCurrentGraphRuntimeState();
|
||||||
throwIfAborted(signal, "提取已终止");
|
throwIfAborted(signal, "提取已终止");
|
||||||
const lastProcessed = getLastProcessedAssistantFloor();
|
const lastProcessed = getLastProcessedAssistantFloor();
|
||||||
|
const extractionCountBefore = extractionCount;
|
||||||
const beforeSnapshot = cloneGraphSnapshot(currentGraph);
|
const beforeSnapshot = cloneGraphSnapshot(currentGraph);
|
||||||
const messages = buildExtractionMessages(chat, startIdx, endIdx, settings);
|
const messages = buildExtractionMessages(chat, startIdx, endIdx, settings);
|
||||||
|
|
||||||
@@ -1852,6 +2055,7 @@ async function executeExtractionBatch({
|
|||||||
processedRange: [startIdx, endIdx],
|
processedRange: [startIdx, endIdx],
|
||||||
postProcessArtifacts,
|
postProcessArtifacts,
|
||||||
vectorHashesInserted: effects?.vectorHashesInserted || [],
|
vectorHashesInserted: effects?.vectorHashesInserted || [],
|
||||||
|
extractionCountBefore,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
saveGraphToChat();
|
saveGraphToChat();
|
||||||
@@ -1901,6 +2105,29 @@ async function replayExtractionFromHistory(chat, settings, signal = undefined) {
|
|||||||
return replayedBatches;
|
return replayedBatches;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function collectAffectedInsertedHashes(affectedJournals = []) {
|
||||||
|
const hashes = new Set();
|
||||||
|
for (const journal of affectedJournals) {
|
||||||
|
const insertedHashes =
|
||||||
|
journal?.vectorDelta?.insertedHashes ||
|
||||||
|
journal?.vectorHashesInserted ||
|
||||||
|
[];
|
||||||
|
for (const hash of insertedHashes) {
|
||||||
|
if (hash) hashes.add(hash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [...hashes];
|
||||||
|
}
|
||||||
|
|
||||||
|
function rollbackAffectedJournals(graph, affectedJournals = []) {
|
||||||
|
for (let index = affectedJournals.length - 1; index >= 0; index--) {
|
||||||
|
rollbackBatch(graph, affectedJournals[index]);
|
||||||
|
}
|
||||||
|
graph.batchJournal = Array.isArray(graph.batchJournal)
|
||||||
|
? graph.batchJournal.slice(0, Math.max(0, graph.batchJournal.length - affectedJournals.length))
|
||||||
|
: [];
|
||||||
|
}
|
||||||
|
|
||||||
async function recoverHistoryIfNeeded(trigger = "history-recovery") {
|
async function recoverHistoryIfNeeded(trigger = "history-recovery") {
|
||||||
if (!currentGraph || isRecoveringHistory) {
|
if (!currentGraph || isRecoveringHistory) {
|
||||||
return !isRecoveringHistory;
|
return !isRecoveringHistory;
|
||||||
@@ -1927,6 +2154,8 @@ async function recoverHistoryIfNeeded(trigger = "history-recovery") {
|
|||||||
: detection.earliestAffectedFloor;
|
: detection.earliestAffectedFloor;
|
||||||
let replayedBatches = 0;
|
let replayedBatches = 0;
|
||||||
let usedFullRebuild = false;
|
let usedFullRebuild = false;
|
||||||
|
let recoveryPath = "full-rebuild";
|
||||||
|
let affectedBatchCount = 0;
|
||||||
const historyController = beginStageAbortController("history");
|
const historyController = beginStageAbortController("history");
|
||||||
const historySignal = historyController.signal;
|
const historySignal = historyController.signal;
|
||||||
|
|
||||||
@@ -1946,33 +2175,71 @@ async function recoverHistoryIfNeeded(trigger = "history-recovery") {
|
|||||||
try {
|
try {
|
||||||
throwIfAborted(historySignal, "历史恢复已终止");
|
throwIfAborted(historySignal, "历史恢复已终止");
|
||||||
const recoveryPoint = findJournalRecoveryPoint(currentGraph, initialDirtyFrom);
|
const recoveryPoint = findJournalRecoveryPoint(currentGraph, initialDirtyFrom);
|
||||||
if (recoveryPoint) {
|
if (recoveryPoint?.path === "reverse-journal") {
|
||||||
|
recoveryPath = "reverse-journal";
|
||||||
|
affectedBatchCount = recoveryPoint.affectedBatchCount || 0;
|
||||||
|
const config = getEmbeddingConfig();
|
||||||
|
const insertedHashes = collectAffectedInsertedHashes(
|
||||||
|
recoveryPoint.affectedJournals,
|
||||||
|
);
|
||||||
|
rollbackAffectedJournals(currentGraph, recoveryPoint.affectedJournals);
|
||||||
|
currentGraph = normalizeGraphRuntimeState(currentGraph, chatId);
|
||||||
|
extractionCount = currentGraph.historyState.extractionCount || 0;
|
||||||
|
|
||||||
|
if (isBackendVectorConfig(config) && insertedHashes.length > 0) {
|
||||||
|
await deleteBackendVectorHashesForRecovery(
|
||||||
|
currentGraph.vectorIndexState.collectionId,
|
||||||
|
config,
|
||||||
|
insertedHashes,
|
||||||
|
historySignal,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
await prepareVectorStateForReplay(false, historySignal, {
|
||||||
|
skipBackendPurge: isBackendVectorConfig(config),
|
||||||
|
});
|
||||||
|
} else if (recoveryPoint?.path === "legacy-snapshot") {
|
||||||
|
recoveryPath = "legacy-snapshot";
|
||||||
|
affectedBatchCount = recoveryPoint.affectedBatchCount || 0;
|
||||||
currentGraph = normalizeGraphRuntimeState(
|
currentGraph = normalizeGraphRuntimeState(
|
||||||
recoveryPoint.snapshotBefore,
|
recoveryPoint.snapshotBefore,
|
||||||
chatId,
|
chatId,
|
||||||
);
|
);
|
||||||
|
extractionCount = currentGraph.historyState.extractionCount || 0;
|
||||||
|
await prepareVectorStateForReplay(false, historySignal);
|
||||||
} else {
|
} else {
|
||||||
|
recoveryPath = "full-rebuild";
|
||||||
currentGraph = normalizeGraphRuntimeState(createEmptyGraph(), chatId);
|
currentGraph = normalizeGraphRuntimeState(createEmptyGraph(), chatId);
|
||||||
usedFullRebuild = true;
|
usedFullRebuild = true;
|
||||||
|
extractionCount = 0;
|
||||||
|
await prepareVectorStateForReplay(true, historySignal);
|
||||||
}
|
}
|
||||||
|
|
||||||
await prepareVectorStateForReplay(usedFullRebuild, historySignal);
|
|
||||||
replayedBatches = await replayExtractionFromHistory(chat, settings, historySignal);
|
replayedBatches = await replayExtractionFromHistory(chat, settings, historySignal);
|
||||||
|
|
||||||
clearHistoryDirty(
|
clearHistoryDirty(
|
||||||
currentGraph,
|
currentGraph,
|
||||||
buildRecoveryResult(usedFullRebuild ? "full-rebuild" : "replayed", {
|
buildRecoveryResult(
|
||||||
|
usedFullRebuild ? "full-rebuild" : "replayed",
|
||||||
|
{
|
||||||
fromFloor: initialDirtyFrom,
|
fromFloor: initialDirtyFrom,
|
||||||
batches: replayedBatches,
|
batches: replayedBatches,
|
||||||
|
path: recoveryPath,
|
||||||
|
detectionSource:
|
||||||
|
detection.source ||
|
||||||
|
currentGraph?.historyState?.lastMutationSource ||
|
||||||
|
"hash-recheck",
|
||||||
|
affectedBatchCount,
|
||||||
|
replayedBatchCount: replayedBatches,
|
||||||
reason: detection.reason || currentGraph?.historyState?.lastMutationReason || trigger,
|
reason: detection.reason || currentGraph?.historyState?.lastMutationReason || trigger,
|
||||||
}),
|
},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
saveGraphToChat();
|
saveGraphToChat();
|
||||||
refreshPanelLiveState();
|
refreshPanelLiveState();
|
||||||
updateStageNotice(
|
updateStageNotice(
|
||||||
"history",
|
"history",
|
||||||
usedFullRebuild ? "历史恢复完成(全量重建)" : "历史恢复完成",
|
usedFullRebuild ? "历史恢复完成(全量重建)" : "历史恢复完成",
|
||||||
`起点楼层 ${initialDirtyFrom} · 回放 ${replayedBatches} 批`,
|
`path ${recoveryPath} · 起点楼层 ${initialDirtyFrom} · 受影响 ${affectedBatchCount} 批 · 回放 ${replayedBatches} 批`,
|
||||||
usedFullRebuild ? "warning" : "success",
|
usedFullRebuild ? "warning" : "success",
|
||||||
{
|
{
|
||||||
busy: false,
|
busy: false,
|
||||||
@@ -2005,6 +2272,7 @@ async function recoverHistoryIfNeeded(trigger = "history-recovery") {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
currentGraph = normalizeGraphRuntimeState(createEmptyGraph(), chatId);
|
currentGraph = normalizeGraphRuntimeState(createEmptyGraph(), chatId);
|
||||||
|
extractionCount = 0;
|
||||||
await prepareVectorStateForReplay(true, historySignal);
|
await prepareVectorStateForReplay(true, historySignal);
|
||||||
replayedBatches = await replayExtractionFromHistory(chat, settings, historySignal);
|
replayedBatches = await replayExtractionFromHistory(chat, settings, historySignal);
|
||||||
clearHistoryDirty(
|
clearHistoryDirty(
|
||||||
@@ -2012,6 +2280,13 @@ async function recoverHistoryIfNeeded(trigger = "history-recovery") {
|
|||||||
buildRecoveryResult("full-rebuild", {
|
buildRecoveryResult("full-rebuild", {
|
||||||
fromFloor: 0,
|
fromFloor: 0,
|
||||||
batches: replayedBatches,
|
batches: replayedBatches,
|
||||||
|
path: "full-rebuild",
|
||||||
|
detectionSource:
|
||||||
|
detection.source ||
|
||||||
|
currentGraph?.historyState?.lastMutationSource ||
|
||||||
|
"hash-recheck",
|
||||||
|
affectedBatchCount,
|
||||||
|
replayedBatchCount: replayedBatches,
|
||||||
reason: `恢复失败后兜底全量重建: ${error?.message || error}`,
|
reason: `恢复失败后兜底全量重建: ${error?.message || error}`,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@@ -2020,7 +2295,7 @@ async function recoverHistoryIfNeeded(trigger = "history-recovery") {
|
|||||||
updateStageNotice(
|
updateStageNotice(
|
||||||
"history",
|
"history",
|
||||||
"历史恢复已退化为全量重建",
|
"历史恢复已退化为全量重建",
|
||||||
`起点楼层 ${initialDirtyFrom} · 回放 ${replayedBatches} 批`,
|
`path full-rebuild · 起点楼层 ${initialDirtyFrom} · 回放 ${replayedBatches} 批`,
|
||||||
"warning",
|
"warning",
|
||||||
{
|
{
|
||||||
busy: false,
|
busy: false,
|
||||||
@@ -2032,6 +2307,13 @@ async function recoverHistoryIfNeeded(trigger = "history-recovery") {
|
|||||||
} catch (fallbackError) {
|
} catch (fallbackError) {
|
||||||
currentGraph.historyState.lastRecoveryResult = buildRecoveryResult("failed", {
|
currentGraph.historyState.lastRecoveryResult = buildRecoveryResult("failed", {
|
||||||
fromFloor: initialDirtyFrom,
|
fromFloor: initialDirtyFrom,
|
||||||
|
path: recoveryPath,
|
||||||
|
detectionSource:
|
||||||
|
detection.source ||
|
||||||
|
currentGraph?.historyState?.lastMutationSource ||
|
||||||
|
"hash-recheck",
|
||||||
|
affectedBatchCount,
|
||||||
|
replayedBatchCount: replayedBatches,
|
||||||
reason: String(fallbackError),
|
reason: String(fallbackError),
|
||||||
});
|
});
|
||||||
saveGraphToChat();
|
saveGraphToChat();
|
||||||
@@ -2145,23 +2427,115 @@ async function runExtraction() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getRecallHookLabel(hookName = "") {
|
||||||
|
switch (hookName) {
|
||||||
|
case "GENERATION_AFTER_COMMANDS":
|
||||||
|
return "hook GENERATION_AFTER_COMMANDS";
|
||||||
|
case "GENERATE_BEFORE_COMBINE_PROMPTS":
|
||||||
|
return "hook GENERATE_BEFORE_COMBINE_PROMPTS";
|
||||||
|
default:
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyRecallInjection(context, settings, recallInput, recentMessages, result) {
|
||||||
|
const injectionText = formatInjection(result, getSchema()).trim();
|
||||||
|
lastInjectionContent = injectionText;
|
||||||
|
const retrievalMeta = result?.meta?.retrieval || {};
|
||||||
|
const llmMeta = retrievalMeta.llm || {
|
||||||
|
status: settings.recallEnableLLM ? "unknown" : "disabled",
|
||||||
|
reason: settings.recallEnableLLM ? "未提供 LLM 状态" : "LLM 精排已关闭",
|
||||||
|
candidatePool: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (injectionText) {
|
||||||
|
const tokens = estimateTokens(injectionText);
|
||||||
|
console.log(
|
||||||
|
`[ST-BME] 注入 ${tokens} 估算 tokens, Core=${result.stats.coreCount}, Recall=${result.stats.recallCount}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
context.setExtensionPrompt(
|
||||||
|
MODULE_NAME,
|
||||||
|
injectionText,
|
||||||
|
extension_prompt_types.IN_CHAT,
|
||||||
|
clampInt(settings.injectDepth, 9999, 0, 9999),
|
||||||
|
);
|
||||||
|
|
||||||
|
currentGraph.lastRecallResult = result.selectedNodeIds;
|
||||||
|
updateLastRecalledItems(result.selectedNodeIds || []);
|
||||||
|
saveGraphToChat();
|
||||||
|
|
||||||
|
const llmLabel =
|
||||||
|
llmMeta.status === "llm"
|
||||||
|
? "LLM 精排完成"
|
||||||
|
: llmMeta.status === "fallback"
|
||||||
|
? "LLM 回退评分"
|
||||||
|
: llmMeta.status === "disabled"
|
||||||
|
? "仅评分排序"
|
||||||
|
: "召回完成";
|
||||||
|
const hookLabel = getRecallHookLabel(recallInput.hookName);
|
||||||
|
setLastRecallStatus(
|
||||||
|
llmLabel,
|
||||||
|
[
|
||||||
|
hookLabel,
|
||||||
|
recallInput.sourceLabel,
|
||||||
|
`ctx ${recentMessages.length}`,
|
||||||
|
`vector ${retrievalMeta.vectorHits ?? 0}`,
|
||||||
|
`diffusion ${retrievalMeta.diffusionHits ?? 0}`,
|
||||||
|
`llm pool ${llmMeta.candidatePool ?? 0}`,
|
||||||
|
`recall ${result.stats.recallCount}`,
|
||||||
|
].filter(Boolean).join(" · "),
|
||||||
|
llmMeta.status === "fallback" ? "warning" : "success",
|
||||||
|
{
|
||||||
|
syncRuntime: true,
|
||||||
|
toastKind: "",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (llmMeta.status === "fallback") {
|
||||||
|
const now = Date.now();
|
||||||
|
if (now - lastRecallFallbackNoticeAt > 15000) {
|
||||||
|
lastRecallFallbackNoticeAt = now;
|
||||||
|
toastr.warning(
|
||||||
|
llmMeta.reason || "LLM 精排未返回有效结果,已回退到评分排序",
|
||||||
|
"ST-BME 召回提示",
|
||||||
|
{ timeOut: 4500 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { injectionText, retrievalMeta, llmMeta };
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 召回管线:检索并注入记忆
|
* 召回管线:检索并注入记忆
|
||||||
*/
|
*/
|
||||||
async function runRecall(options = {}) {
|
async function runRecall(options = {}) {
|
||||||
if (isRecalling || !currentGraph) return;
|
if (isRecalling || !currentGraph) return false;
|
||||||
|
|
||||||
const settings = getSettings();
|
const settings = getSettings();
|
||||||
if (!settings.enabled || !settings.recallEnabled) return;
|
if (!settings.enabled || !settings.recallEnabled) return false;
|
||||||
if (!(await recoverHistoryIfNeeded("pre-recall"))) return;
|
if (!(await recoverHistoryIfNeeded("pre-recall"))) return false;
|
||||||
|
|
||||||
const context = getContext();
|
const context = getContext();
|
||||||
const chat = context.chat;
|
const chat = context.chat;
|
||||||
if (!chat || chat.length === 0) return;
|
if (!chat || chat.length === 0) return false;
|
||||||
|
|
||||||
isRecalling = true;
|
isRecalling = true;
|
||||||
const recallController = beginStageAbortController("recall");
|
const recallController = beginStageAbortController("recall");
|
||||||
const recallSignal = recallController.signal;
|
const recallSignal = recallController.signal;
|
||||||
|
if (options.signal) {
|
||||||
|
if (options.signal.aborted) {
|
||||||
|
recallController.abort(options.signal.reason || createAbortError("宿主已终止生成"));
|
||||||
|
} else {
|
||||||
|
options.signal.addEventListener(
|
||||||
|
"abort",
|
||||||
|
() => recallController.abort(options.signal.reason || createAbortError("宿主已终止生成")),
|
||||||
|
{ once: true },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await ensureVectorReadyIfNeeded("pre-recall", recallSignal);
|
await ensureVectorReadyIfNeeded("pre-recall", recallSignal);
|
||||||
@@ -2175,17 +2549,25 @@ async function runRecall(options = {}) {
|
|||||||
const userMessage = recallInput.userMessage;
|
const userMessage = recallInput.userMessage;
|
||||||
const recentMessages = recallInput.recentMessages;
|
const recentMessages = recallInput.recentMessages;
|
||||||
|
|
||||||
if (!userMessage) return;
|
if (!userMessage) return false;
|
||||||
|
|
||||||
|
recallInput.hookName = options.hookName || "";
|
||||||
|
|
||||||
console.log("[ST-BME] 开始召回", {
|
console.log("[ST-BME] 开始召回", {
|
||||||
source: recallInput.source,
|
source: recallInput.source,
|
||||||
sourceLabel: recallInput.sourceLabel,
|
sourceLabel: recallInput.sourceLabel,
|
||||||
|
hookName: recallInput.hookName,
|
||||||
userMessageLength: userMessage.length,
|
userMessageLength: userMessage.length,
|
||||||
recentMessages: recentMessages.length,
|
recentMessages: recentMessages.length,
|
||||||
});
|
});
|
||||||
setLastRecallStatus(
|
setLastRecallStatus(
|
||||||
"召回中",
|
"召回中",
|
||||||
`来源 ${recallInput.sourceLabel} · 上下文 ${recentMessages.length} 条 · 当前用户消息长度 ${userMessage.length}`,
|
[
|
||||||
|
getRecallHookLabel(recallInput.hookName),
|
||||||
|
`来源 ${recallInput.sourceLabel}`,
|
||||||
|
`上下文 ${recentMessages.length} 条`,
|
||||||
|
`当前用户消息长度 ${userMessage.length}`,
|
||||||
|
].filter(Boolean).join(" · "),
|
||||||
"running",
|
"running",
|
||||||
{ syncRuntime: true },
|
{ syncRuntime: true },
|
||||||
);
|
);
|
||||||
@@ -2223,71 +2605,14 @@ async function runRecall(options = {}) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// 格式化注入文本
|
applyRecallInjection(context, settings, recallInput, recentMessages, result);
|
||||||
const injectionText = formatInjection(result, getSchema()).trim();
|
return true;
|
||||||
lastInjectionContent = injectionText;
|
|
||||||
const retrievalMeta = result?.meta?.retrieval || {};
|
|
||||||
const llmMeta = retrievalMeta.llm || {
|
|
||||||
status: settings.recallEnableLLM ? "unknown" : "disabled",
|
|
||||||
reason: settings.recallEnableLLM ? "未提供 LLM 状态" : "LLM 精排已关闭",
|
|
||||||
candidatePool: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (injectionText) {
|
|
||||||
const tokens = estimateTokens(injectionText);
|
|
||||||
console.log(
|
|
||||||
`[ST-BME] 注入 ${tokens} 估算 tokens, Core=${result.stats.coreCount}, Recall=${result.stats.recallCount}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 无结果时也要清空旧注入,避免脏 prompt 残留
|
|
||||||
context.setExtensionPrompt(
|
|
||||||
MODULE_NAME,
|
|
||||||
injectionText,
|
|
||||||
extension_prompt_types.IN_CHAT, // 当前注入走 IN_CHAT@Depth
|
|
||||||
clampInt(settings.injectDepth, 9999, 0, 9999),
|
|
||||||
);
|
|
||||||
|
|
||||||
// 保存召回结果和访问强化
|
|
||||||
currentGraph.lastRecallResult = result.selectedNodeIds;
|
|
||||||
updateLastRecalledItems(result.selectedNodeIds || []);
|
|
||||||
saveGraphToChat();
|
|
||||||
|
|
||||||
const llmLabel =
|
|
||||||
llmMeta.status === "llm"
|
|
||||||
? "LLM 精排完成"
|
|
||||||
: llmMeta.status === "fallback"
|
|
||||||
? "LLM 回退评分"
|
|
||||||
: llmMeta.status === "disabled"
|
|
||||||
? "仅评分排序"
|
|
||||||
: "召回完成";
|
|
||||||
setLastRecallStatus(
|
|
||||||
llmLabel,
|
|
||||||
`${recallInput.sourceLabel} · ctx ${recentMessages.length} · vector ${retrievalMeta.vectorHits ?? 0} · diffusion ${retrievalMeta.diffusionHits ?? 0} · llm pool ${llmMeta.candidatePool ?? 0} · recall ${result.stats.recallCount}`,
|
|
||||||
llmMeta.status === "fallback" ? "warning" : "success",
|
|
||||||
{
|
|
||||||
syncRuntime: true,
|
|
||||||
toastKind: "",
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (llmMeta.status === "fallback") {
|
|
||||||
const now = Date.now();
|
|
||||||
if (now - lastRecallFallbackNoticeAt > 15000) {
|
|
||||||
lastRecallFallbackNoticeAt = now;
|
|
||||||
toastr.warning(
|
|
||||||
llmMeta.reason || "LLM 精排未返回有效结果,已回退到评分排序",
|
|
||||||
"ST-BME 召回提示",
|
|
||||||
{ timeOut: 4500 },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (isAbortError(e)) {
|
if (isAbortError(e)) {
|
||||||
setLastRecallStatus("召回已终止", e?.message || "已手动终止当前召回", "warning", {
|
setLastRecallStatus("召回已终止", e?.message || "已手动终止当前召回", "warning", {
|
||||||
syncRuntime: true,
|
syncRuntime: true,
|
||||||
});
|
});
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
console.error("[ST-BME] 召回失败:", e);
|
console.error("[ST-BME] 召回失败:", e);
|
||||||
const message = e?.message || String(e);
|
const message = e?.message || String(e);
|
||||||
@@ -2296,6 +2621,7 @@ async function runRecall(options = {}) {
|
|||||||
toastKind: "",
|
toastKind: "",
|
||||||
});
|
});
|
||||||
toastr.error(`召回失败: ${message}`);
|
toastr.error(`召回失败: ${message}`);
|
||||||
|
return false;
|
||||||
} finally {
|
} finally {
|
||||||
finishStageAbortController("recall", recallController);
|
finishStageAbortController("recall", recallController);
|
||||||
isRecalling = false;
|
isRecalling = false;
|
||||||
@@ -2310,6 +2636,9 @@ function onChatChanged() {
|
|||||||
clearTimeout(pendingHistoryRecoveryTimer);
|
clearTimeout(pendingHistoryRecoveryTimer);
|
||||||
pendingHistoryRecoveryTimer = null;
|
pendingHistoryRecoveryTimer = null;
|
||||||
pendingHistoryRecoveryTrigger = "";
|
pendingHistoryRecoveryTrigger = "";
|
||||||
|
skipBeforeCombineRecallUntil = 0;
|
||||||
|
lastPreGenerationRecallKey = "";
|
||||||
|
lastPreGenerationRecallAt = 0;
|
||||||
abortAllRunningStages();
|
abortAllRunningStages();
|
||||||
dismissAllStageNotices();
|
dismissAllStageNotices();
|
||||||
loadGraphFromChat();
|
loadGraphFromChat();
|
||||||
@@ -2328,23 +2657,56 @@ function onMessageSent(messageId) {
|
|||||||
recordRecallSentUserMessage(messageId, message.mes || "");
|
recordRecallSentUserMessage(messageId, message.mes || "");
|
||||||
}
|
}
|
||||||
|
|
||||||
function onMessageDeleted() {
|
function onMessageDeleted(chatLengthOrMessageId, meta = null) {
|
||||||
clearInjectionState();
|
clearInjectionState();
|
||||||
scheduleHistoryMutationRecheck("message-deleted");
|
scheduleHistoryMutationRecheck("message-deleted", chatLengthOrMessageId, meta);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onMessageEdited() {
|
function onMessageEdited(messageId, meta = null) {
|
||||||
clearInjectionState();
|
clearInjectionState();
|
||||||
scheduleHistoryMutationRecheck("message-edited");
|
scheduleHistoryMutationRecheck("message-edited", messageId, meta);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onMessageSwiped() {
|
function onMessageSwiped(messageId, meta = null) {
|
||||||
clearInjectionState();
|
clearInjectionState();
|
||||||
scheduleHistoryMutationRecheck("message-swiped");
|
scheduleHistoryMutationRecheck("message-swiped", messageId, meta);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onGenerationAfterCommands(type, params = {}, dryRun = false) {
|
||||||
|
if (dryRun) return;
|
||||||
|
|
||||||
|
const context = getContext();
|
||||||
|
const chat = context?.chat;
|
||||||
|
const recallOptions = buildGenerationAfterCommandsRecallInput(type, params, chat);
|
||||||
|
if (!recallOptions?.overrideUserMessage) return;
|
||||||
|
|
||||||
|
const recallKey = buildPreGenerationRecallKey(type, recallOptions);
|
||||||
|
const recentlyHandled =
|
||||||
|
lastPreGenerationRecallKey === recallKey &&
|
||||||
|
Date.now() - lastPreGenerationRecallAt < 1500;
|
||||||
|
if (recentlyHandled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const didRecall = await runRecall({
|
||||||
|
...recallOptions,
|
||||||
|
hookName: "GENERATION_AFTER_COMMANDS",
|
||||||
|
signal: params?.signal,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (didRecall) {
|
||||||
|
lastPreGenerationRecallKey = recallKey;
|
||||||
|
lastPreGenerationRecallAt = Date.now();
|
||||||
|
skipBeforeCombineRecallUntil = Date.now() + 1500;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onBeforeCombinePrompts() {
|
async function onBeforeCombinePrompts() {
|
||||||
await runRecall();
|
if (skipBeforeCombineRecallUntil > Date.now()) {
|
||||||
|
skipBeforeCombineRecallUntil = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await runRecall({ hookName: "GENERATE_BEFORE_COMBINE_PROMPTS" });
|
||||||
}
|
}
|
||||||
|
|
||||||
function onMessageReceived() {
|
function onMessageReceived() {
|
||||||
@@ -2426,6 +2788,10 @@ async function onRebuild() {
|
|||||||
buildRecoveryResult("full-rebuild", {
|
buildRecoveryResult("full-rebuild", {
|
||||||
fromFloor: 0,
|
fromFloor: 0,
|
||||||
batches: replayedBatches,
|
batches: replayedBatches,
|
||||||
|
path: "full-rebuild",
|
||||||
|
detectionSource: "manual-rebuild",
|
||||||
|
affectedBatchCount: currentGraph.batchJournal?.length || 0,
|
||||||
|
replayedBatchCount: replayedBatches,
|
||||||
reason: "用户手动触发全量重建",
|
reason: "用户手动触发全量重建",
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@@ -2824,6 +3190,7 @@ async function onReembedDirect() {
|
|||||||
if (event_types.MESSAGE_SENT) {
|
if (event_types.MESSAGE_SENT) {
|
||||||
eventSource.on(event_types.MESSAGE_SENT, onMessageSent);
|
eventSource.on(event_types.MESSAGE_SENT, onMessageSent);
|
||||||
}
|
}
|
||||||
|
registerGenerationAfterCommands(onGenerationAfterCommands);
|
||||||
registerBeforeCombinePrompts(onBeforeCombinePrompts);
|
registerBeforeCombinePrompts(onBeforeCombinePrompts);
|
||||||
eventSource.on(event_types.MESSAGE_RECEIVED, onMessageReceived);
|
eventSource.on(event_types.MESSAGE_RECEIVED, onMessageReceived);
|
||||||
eventSource.on(event_types.MESSAGE_DELETED, onMessageDeleted);
|
eventSource.on(event_types.MESSAGE_DELETED, onMessageDeleted);
|
||||||
|
|||||||
10
panel.js
10
panel.js
@@ -372,7 +372,15 @@ function _refreshDashboard() {
|
|||||||
_setText(
|
_setText(
|
||||||
"bme-status-recovery",
|
"bme-status-recovery",
|
||||||
recovery
|
recovery
|
||||||
? `${recovery.status} · from ${recovery.fromFloor ?? "—"} · ${recovery.reason || "—"}`
|
? [
|
||||||
|
recovery.status || "—",
|
||||||
|
recovery.path ? `path ${recovery.path}` : "",
|
||||||
|
recovery.detectionSource ? `src ${recovery.detectionSource}` : "",
|
||||||
|
recovery.fromFloor != null ? `from ${recovery.fromFloor}` : "",
|
||||||
|
recovery.affectedBatchCount != null ? `affected ${recovery.affectedBatchCount}` : "",
|
||||||
|
recovery.replayedBatchCount != null ? `replayed ${recovery.replayedBatchCount}` : "",
|
||||||
|
recovery.reason || "",
|
||||||
|
].filter(Boolean).join(" · ")
|
||||||
: "暂无恢复记录",
|
: "暂无恢复记录",
|
||||||
);
|
);
|
||||||
_setText(
|
_setText(
|
||||||
|
|||||||
208
runtime-state.js
208
runtime-state.js
@@ -1,6 +1,7 @@
|
|||||||
// ST-BME: 运行时状态与历史恢复辅助
|
// ST-BME: 运行时状态与历史恢复辅助
|
||||||
|
|
||||||
const BATCH_JOURNAL_LIMIT = 24;
|
const BATCH_JOURNAL_LIMIT = 96;
|
||||||
|
export const BATCH_JOURNAL_VERSION = 2;
|
||||||
|
|
||||||
export function buildVectorCollectionId(chatId) {
|
export function buildVectorCollectionId(chatId) {
|
||||||
return `st-bme::${chatId || "unknown-chat"}`;
|
return `st-bme::${chatId || "unknown-chat"}`;
|
||||||
@@ -13,6 +14,8 @@ export function createDefaultHistoryState(chatId = "") {
|
|||||||
processedMessageHashes: {},
|
processedMessageHashes: {},
|
||||||
historyDirtyFrom: null,
|
historyDirtyFrom: null,
|
||||||
lastMutationReason: "",
|
lastMutationReason: "",
|
||||||
|
lastMutationSource: "",
|
||||||
|
extractionCount: 0,
|
||||||
lastRecoveryResult: null,
|
lastRecoveryResult: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -61,6 +64,12 @@ export function normalizeGraphRuntimeState(graph, chatId = "") {
|
|||||||
? graph.lastProcessedSeq
|
? graph.lastProcessedSeq
|
||||||
: -1;
|
: -1;
|
||||||
}
|
}
|
||||||
|
if (!Number.isFinite(historyState.extractionCount)) {
|
||||||
|
historyState.extractionCount = 0;
|
||||||
|
}
|
||||||
|
if (typeof historyState.lastMutationSource !== "string") {
|
||||||
|
historyState.lastMutationSource = "";
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!historyState.processedMessageHashes ||
|
!historyState.processedMessageHashes ||
|
||||||
@@ -207,7 +216,7 @@ export function detectHistoryMutation(chat, historyState) {
|
|||||||
return { dirty: false, earliestAffectedFloor: null, reason: "" };
|
return { dirty: false, earliestAffectedFloor: null, reason: "" };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function markHistoryDirty(graph, floor, reason = "") {
|
export function markHistoryDirty(graph, floor, reason = "", source = "") {
|
||||||
normalizeGraphRuntimeState(graph, graph?.historyState?.chatId || "");
|
normalizeGraphRuntimeState(graph, graph?.historyState?.chatId || "");
|
||||||
const currentDirtyFrom = graph.historyState.historyDirtyFrom;
|
const currentDirtyFrom = graph.historyState.historyDirtyFrom;
|
||||||
|
|
||||||
@@ -219,11 +228,13 @@ export function markHistoryDirty(graph, floor, reason = "") {
|
|||||||
? Math.min(currentDirtyFrom, floor)
|
? Math.min(currentDirtyFrom, floor)
|
||||||
: floor;
|
: floor;
|
||||||
graph.historyState.lastMutationReason = String(reason || "").trim();
|
graph.historyState.lastMutationReason = String(reason || "").trim();
|
||||||
|
graph.historyState.lastMutationSource = String(source || "").trim();
|
||||||
graph.historyState.lastRecoveryResult = {
|
graph.historyState.lastRecoveryResult = {
|
||||||
status: "pending",
|
status: "pending",
|
||||||
at: Date.now(),
|
at: Date.now(),
|
||||||
fromFloor: graph.historyState.historyDirtyFrom,
|
fromFloor: graph.historyState.historyDirtyFrom,
|
||||||
reason: graph.historyState.lastMutationReason,
|
reason: graph.historyState.lastMutationReason,
|
||||||
|
detectionSource: graph.historyState.lastMutationSource || "",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -231,6 +242,7 @@ export function clearHistoryDirty(graph, result = null) {
|
|||||||
normalizeGraphRuntimeState(graph, graph?.historyState?.chatId || "");
|
normalizeGraphRuntimeState(graph, graph?.historyState?.chatId || "");
|
||||||
graph.historyState.historyDirtyFrom = null;
|
graph.historyState.historyDirtyFrom = null;
|
||||||
graph.historyState.lastMutationReason = "";
|
graph.historyState.lastMutationReason = "";
|
||||||
|
graph.historyState.lastMutationSource = "";
|
||||||
if (result) {
|
if (result) {
|
||||||
graph.historyState.lastRecoveryResult = result;
|
graph.historyState.lastRecoveryResult = result;
|
||||||
}
|
}
|
||||||
@@ -252,6 +264,32 @@ function hasMeaningfulEdgeChange(beforeEdge, afterEdge) {
|
|||||||
return JSON.stringify(beforeEdge) !== JSON.stringify(afterEdge);
|
return JSON.stringify(beforeEdge) !== JSON.stringify(afterEdge);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function clonePlain(value) {
|
||||||
|
return JSON.parse(JSON.stringify(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildJournalStateBefore(snapshotBefore, meta = {}) {
|
||||||
|
return {
|
||||||
|
lastProcessedAssistantFloor:
|
||||||
|
snapshotBefore?.historyState?.lastProcessedAssistantFloor ??
|
||||||
|
snapshotBefore?.lastProcessedSeq ??
|
||||||
|
-1,
|
||||||
|
processedMessageHashes: clonePlain(
|
||||||
|
snapshotBefore?.historyState?.processedMessageHashes || {},
|
||||||
|
),
|
||||||
|
historyDirtyFrom: Number.isFinite(snapshotBefore?.historyState?.historyDirtyFrom)
|
||||||
|
? snapshotBefore.historyState.historyDirtyFrom
|
||||||
|
: null,
|
||||||
|
vectorIndexState: clonePlain(snapshotBefore?.vectorIndexState || {}),
|
||||||
|
lastRecallResult: Array.isArray(snapshotBefore?.lastRecallResult)
|
||||||
|
? [...snapshotBefore.lastRecallResult]
|
||||||
|
: null,
|
||||||
|
extractionCount: Number.isFinite(meta.extractionCountBefore)
|
||||||
|
? meta.extractionCountBefore
|
||||||
|
: snapshotBefore?.historyState?.extractionCount ?? 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function createBatchJournalEntry(snapshotBefore, snapshotAfter, meta = {}) {
|
export function createBatchJournalEntry(snapshotBefore, snapshotAfter, meta = {}) {
|
||||||
const beforeNodes = buildNodeMap(snapshotBefore?.nodes || []);
|
const beforeNodes = buildNodeMap(snapshotBefore?.nodes || []);
|
||||||
const afterNodes = buildNodeMap(snapshotAfter?.nodes || []);
|
const afterNodes = buildNodeMap(snapshotAfter?.nodes || []);
|
||||||
@@ -260,9 +298,8 @@ export function createBatchJournalEntry(snapshotBefore, snapshotAfter, meta = {}
|
|||||||
|
|
||||||
const createdNodeIds = [];
|
const createdNodeIds = [];
|
||||||
const createdEdgeIds = [];
|
const createdEdgeIds = [];
|
||||||
const updatedNodeSnapshots = [];
|
const previousNodeSnapshots = [];
|
||||||
const archivedNodeSnapshots = [];
|
const previousEdgeSnapshots = [];
|
||||||
const invalidatedEdgeSnapshots = [];
|
|
||||||
|
|
||||||
for (const [nodeId, afterNode] of afterNodes.entries()) {
|
for (const [nodeId, afterNode] of afterNodes.entries()) {
|
||||||
if (!beforeNodes.has(nodeId)) {
|
if (!beforeNodes.has(nodeId)) {
|
||||||
@@ -272,11 +309,7 @@ export function createBatchJournalEntry(snapshotBefore, snapshotAfter, meta = {}
|
|||||||
|
|
||||||
const beforeNode = beforeNodes.get(nodeId);
|
const beforeNode = beforeNodes.get(nodeId);
|
||||||
if (!hasMeaningfulNodeChange(beforeNode, afterNode)) continue;
|
if (!hasMeaningfulNodeChange(beforeNode, afterNode)) continue;
|
||||||
updatedNodeSnapshots.push(cloneGraphSnapshot(beforeNode));
|
previousNodeSnapshots.push(cloneGraphSnapshot(beforeNode));
|
||||||
|
|
||||||
if (beforeNode.archived !== afterNode.archived) {
|
|
||||||
archivedNodeSnapshots.push(cloneGraphSnapshot(beforeNode));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const [edgeId, afterEdge] of afterEdges.entries()) {
|
for (const [edgeId, afterEdge] of afterEdges.entries()) {
|
||||||
@@ -287,31 +320,34 @@ export function createBatchJournalEntry(snapshotBefore, snapshotAfter, meta = {}
|
|||||||
|
|
||||||
const beforeEdge = beforeEdges.get(edgeId);
|
const beforeEdge = beforeEdges.get(edgeId);
|
||||||
if (!hasMeaningfulEdgeChange(beforeEdge, afterEdge)) continue;
|
if (!hasMeaningfulEdgeChange(beforeEdge, afterEdge)) continue;
|
||||||
if (
|
previousEdgeSnapshots.push(cloneGraphSnapshot(beforeEdge));
|
||||||
beforeEdge.invalidAt !== afterEdge.invalidAt ||
|
|
||||||
beforeEdge.expiredAt !== afterEdge.expiredAt
|
|
||||||
) {
|
|
||||||
invalidatedEdgeSnapshots.push(cloneGraphSnapshot(beforeEdge));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
const entry = {
|
||||||
id: `batch-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
id: `batch-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
||||||
|
journalVersion: BATCH_JOURNAL_VERSION,
|
||||||
createdAt: Date.now(),
|
createdAt: Date.now(),
|
||||||
processedRange: meta.processedRange || [-1, -1],
|
processedRange: meta.processedRange || [-1, -1],
|
||||||
createdNodeIds,
|
createdNodeIds,
|
||||||
createdEdgeIds,
|
createdEdgeIds,
|
||||||
updatedNodeSnapshots,
|
previousNodeSnapshots,
|
||||||
archivedNodeSnapshots,
|
previousEdgeSnapshots,
|
||||||
invalidatedEdgeSnapshots,
|
stateBefore: buildJournalStateBefore(snapshotBefore, meta),
|
||||||
vectorHashesInserted: Array.isArray(meta.vectorHashesInserted)
|
vectorDelta: {
|
||||||
? [...new Set(meta.vectorHashesInserted)]
|
insertedHashes: Array.isArray(meta.vectorHashesInserted)
|
||||||
: [],
|
? [...new Set(meta.vectorHashesInserted)]
|
||||||
|
: [],
|
||||||
|
},
|
||||||
postProcessArtifacts: Array.isArray(meta.postProcessArtifacts)
|
postProcessArtifacts: Array.isArray(meta.postProcessArtifacts)
|
||||||
? meta.postProcessArtifacts
|
? meta.postProcessArtifacts
|
||||||
: [],
|
: [],
|
||||||
snapshotBefore,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (meta.includeLegacySnapshotBefore) {
|
||||||
|
entry.snapshotBefore = snapshotBefore;
|
||||||
|
}
|
||||||
|
|
||||||
|
return entry;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function appendBatchJournal(graph, entry) {
|
export function appendBatchJournal(graph, entry) {
|
||||||
@@ -322,6 +358,99 @@ export function appendBatchJournal(graph, entry) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function upsertById(list, item) {
|
||||||
|
const index = list.findIndex((entry) => entry.id === item.id);
|
||||||
|
if (index >= 0) {
|
||||||
|
list[index] = item;
|
||||||
|
} else {
|
||||||
|
list.push(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function sanitizeGraphReferences(graph) {
|
||||||
|
const nodeIds = new Set((graph?.nodes || []).map((node) => node.id));
|
||||||
|
graph.nodes = (graph.nodes || []).map((node) => ({
|
||||||
|
...node,
|
||||||
|
parentId: nodeIds.has(node.parentId) ? node.parentId : null,
|
||||||
|
childIds: Array.isArray(node.childIds)
|
||||||
|
? node.childIds.filter((id) => nodeIds.has(id))
|
||||||
|
: [],
|
||||||
|
prevId: nodeIds.has(node.prevId) ? node.prevId : null,
|
||||||
|
nextId: nodeIds.has(node.nextId) ? node.nextId : null,
|
||||||
|
}));
|
||||||
|
graph.edges = (graph.edges || []).filter(
|
||||||
|
(edge) => nodeIds.has(edge.fromId) && nodeIds.has(edge.toId),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyJournalStateBefore(graph, stateBefore = {}) {
|
||||||
|
const historyState = {
|
||||||
|
...createDefaultHistoryState(graph?.historyState?.chatId || ""),
|
||||||
|
...(graph.historyState || {}),
|
||||||
|
};
|
||||||
|
historyState.lastProcessedAssistantFloor = Number.isFinite(
|
||||||
|
stateBefore.lastProcessedAssistantFloor,
|
||||||
|
)
|
||||||
|
? stateBefore.lastProcessedAssistantFloor
|
||||||
|
: historyState.lastProcessedAssistantFloor;
|
||||||
|
historyState.processedMessageHashes = clonePlain(
|
||||||
|
stateBefore.processedMessageHashes || {},
|
||||||
|
);
|
||||||
|
historyState.historyDirtyFrom = Number.isFinite(stateBefore.historyDirtyFrom)
|
||||||
|
? stateBefore.historyDirtyFrom
|
||||||
|
: null;
|
||||||
|
historyState.extractionCount = Number.isFinite(stateBefore.extractionCount)
|
||||||
|
? stateBefore.extractionCount
|
||||||
|
: historyState.extractionCount;
|
||||||
|
graph.historyState = historyState;
|
||||||
|
|
||||||
|
graph.vectorIndexState = {
|
||||||
|
...createDefaultVectorIndexState(graph?.historyState?.chatId || ""),
|
||||||
|
...clonePlain(stateBefore.vectorIndexState || {}),
|
||||||
|
};
|
||||||
|
graph.lastRecallResult = Array.isArray(stateBefore.lastRecallResult)
|
||||||
|
? [...stateBefore.lastRecallResult]
|
||||||
|
: null;
|
||||||
|
graph.lastProcessedSeq = historyState.lastProcessedAssistantFloor;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function rollbackBatch(graph, journal) {
|
||||||
|
if (!graph || !journal) return graph;
|
||||||
|
|
||||||
|
normalizeGraphRuntimeState(graph, graph?.historyState?.chatId || "");
|
||||||
|
|
||||||
|
const createdNodeIds = new Set(journal.createdNodeIds || []);
|
||||||
|
const createdEdgeIds = new Set(journal.createdEdgeIds || []);
|
||||||
|
const previousNodeSnapshots =
|
||||||
|
journal.previousNodeSnapshots ||
|
||||||
|
journal.updatedNodeSnapshots ||
|
||||||
|
journal.archivedNodeSnapshots ||
|
||||||
|
[];
|
||||||
|
const previousEdgeSnapshots =
|
||||||
|
journal.previousEdgeSnapshots ||
|
||||||
|
journal.invalidatedEdgeSnapshots ||
|
||||||
|
[];
|
||||||
|
|
||||||
|
graph.edges = (graph.edges || []).filter(
|
||||||
|
(edge) =>
|
||||||
|
!createdEdgeIds.has(edge.id) &&
|
||||||
|
!createdNodeIds.has(edge.fromId) &&
|
||||||
|
!createdNodeIds.has(edge.toId),
|
||||||
|
);
|
||||||
|
graph.nodes = (graph.nodes || []).filter((node) => !createdNodeIds.has(node.id));
|
||||||
|
|
||||||
|
for (const nodeSnapshot of previousNodeSnapshots) {
|
||||||
|
upsertById(graph.nodes, cloneGraphSnapshot(nodeSnapshot));
|
||||||
|
}
|
||||||
|
for (const edgeSnapshot of previousEdgeSnapshots) {
|
||||||
|
upsertById(graph.edges, cloneGraphSnapshot(edgeSnapshot));
|
||||||
|
}
|
||||||
|
|
||||||
|
applyJournalStateBefore(graph, journal.stateBefore || {});
|
||||||
|
sanitizeGraphReferences(graph);
|
||||||
|
return graph;
|
||||||
|
}
|
||||||
|
|
||||||
export function findJournalRecoveryPoint(graph, dirtyFromFloor) {
|
export function findJournalRecoveryPoint(graph, dirtyFromFloor) {
|
||||||
const journals = Array.isArray(graph?.batchJournal) ? graph.batchJournal : [];
|
const journals = Array.isArray(graph?.batchJournal) ? graph.batchJournal : [];
|
||||||
const affectedIndex = journals.findIndex((journal) => {
|
const affectedIndex = journals.findIndex((journal) => {
|
||||||
@@ -333,14 +462,31 @@ export function findJournalRecoveryPoint(graph, dirtyFromFloor) {
|
|||||||
|
|
||||||
if (affectedIndex < 0) return null;
|
if (affectedIndex < 0) return null;
|
||||||
|
|
||||||
const journal = journals[affectedIndex];
|
const affectedJournals = journals.slice(affectedIndex);
|
||||||
if (!journal?.snapshotBefore) return null;
|
const canReverse = affectedJournals.every(
|
||||||
|
(journal) => Number(journal?.journalVersion || 0) >= BATCH_JOURNAL_VERSION,
|
||||||
|
);
|
||||||
|
if (canReverse) {
|
||||||
|
return {
|
||||||
|
path: "reverse-journal",
|
||||||
|
affectedIndex,
|
||||||
|
affectedJournals: affectedJournals.map((journal) => cloneGraphSnapshot(journal)),
|
||||||
|
affectedBatchCount: affectedJournals.length,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
const journal = journals[affectedIndex];
|
||||||
affectedIndex,
|
if (journal?.snapshotBefore) {
|
||||||
journal,
|
return {
|
||||||
snapshotBefore: cloneGraphSnapshot(journal.snapshotBefore),
|
path: "legacy-snapshot",
|
||||||
};
|
affectedIndex,
|
||||||
|
journal: cloneGraphSnapshot(journal),
|
||||||
|
snapshotBefore: cloneGraphSnapshot(journal.snapshotBefore),
|
||||||
|
affectedBatchCount: affectedJournals.length,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildRecoveryResult(status, extra = {}) {
|
export function buildRecoveryResult(status, extra = {}) {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
createBatchJournalEntry,
|
createBatchJournalEntry,
|
||||||
detectHistoryMutation,
|
detectHistoryMutation,
|
||||||
findJournalRecoveryPoint,
|
findJournalRecoveryPoint,
|
||||||
|
rollbackBatch,
|
||||||
snapshotProcessedMessageHashes,
|
snapshotProcessedMessageHashes,
|
||||||
} from "../runtime-state.js";
|
} from "../runtime-state.js";
|
||||||
import { createEmptyGraph } from "../graph.js";
|
import { createEmptyGraph } from "../graph.js";
|
||||||
@@ -43,8 +44,29 @@ assert.equal(truncatedDetection.earliestAffectedFloor, 2);
|
|||||||
const graph = createEmptyGraph();
|
const graph = createEmptyGraph();
|
||||||
graph.historyState.chatId = "chat-history-test";
|
graph.historyState.chatId = "chat-history-test";
|
||||||
const beforeSnapshot = cloneGraphSnapshot(graph);
|
const beforeSnapshot = cloneGraphSnapshot(graph);
|
||||||
|
graph.nodes.push({
|
||||||
|
id: "node-1",
|
||||||
|
type: "event",
|
||||||
|
fields: { title: "旧事件", summary: "旧摘要" },
|
||||||
|
seq: 1,
|
||||||
|
seqRange: [1, 1],
|
||||||
|
archived: false,
|
||||||
|
embedding: null,
|
||||||
|
importance: 5,
|
||||||
|
accessCount: 0,
|
||||||
|
lastAccessTime: Date.now(),
|
||||||
|
createdTime: Date.now(),
|
||||||
|
level: 0,
|
||||||
|
parentId: null,
|
||||||
|
childIds: [],
|
||||||
|
prevId: null,
|
||||||
|
nextId: null,
|
||||||
|
clusters: [],
|
||||||
|
});
|
||||||
graph.lastProcessedSeq = 3;
|
graph.lastProcessedSeq = 3;
|
||||||
graph.historyState.lastProcessedAssistantFloor = 3;
|
graph.historyState.lastProcessedAssistantFloor = 3;
|
||||||
|
graph.historyState.processedMessageHashes = hashes;
|
||||||
|
graph.historyState.extractionCount = 4;
|
||||||
const afterSnapshot = cloneGraphSnapshot(graph);
|
const afterSnapshot = cloneGraphSnapshot(graph);
|
||||||
appendBatchJournal(
|
appendBatchJournal(
|
||||||
graph,
|
graph,
|
||||||
@@ -52,15 +74,18 @@ appendBatchJournal(
|
|||||||
processedRange: [1, 3],
|
processedRange: [1, 3],
|
||||||
postProcessArtifacts: ["compression"],
|
postProcessArtifacts: ["compression"],
|
||||||
vectorHashesInserted: [1234],
|
vectorHashesInserted: [1234],
|
||||||
|
extractionCountBefore: 0,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const recoveryPoint = findJournalRecoveryPoint(graph, 2);
|
const recoveryPoint = findJournalRecoveryPoint(graph, 2);
|
||||||
assert.ok(recoveryPoint);
|
assert.ok(recoveryPoint);
|
||||||
assert.equal(recoveryPoint.journal.processedRange[1], 3);
|
assert.equal(recoveryPoint.path, "reverse-journal");
|
||||||
assert.equal(
|
assert.equal(recoveryPoint.affectedJournals[0].processedRange[1], 3);
|
||||||
recoveryPoint.snapshotBefore.historyState.lastProcessedAssistantFloor,
|
|
||||||
-1,
|
rollbackBatch(graph, recoveryPoint.affectedJournals[0]);
|
||||||
);
|
assert.equal(graph.nodes.length, 0);
|
||||||
|
assert.equal(graph.historyState.lastProcessedAssistantFloor, -1);
|
||||||
|
assert.equal(graph.historyState.extractionCount, 0);
|
||||||
|
|
||||||
console.log("runtime-history tests passed");
|
console.log("runtime-history tests passed");
|
||||||
|
|||||||
@@ -386,6 +386,16 @@ async function deleteVectorHashes(collectionId, config, hashes, signal) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function deleteBackendVectorHashesForRecovery(
|
||||||
|
collectionId,
|
||||||
|
config,
|
||||||
|
hashes,
|
||||||
|
signal = undefined,
|
||||||
|
) {
|
||||||
|
if (!collectionId || !isBackendVectorConfig(config)) return;
|
||||||
|
await deleteVectorHashes(collectionId, config, hashes, signal);
|
||||||
|
}
|
||||||
|
|
||||||
async function insertVectorEntries(collectionId, config, entries, signal) {
|
async function insertVectorEntries(collectionId, config, entries, signal) {
|
||||||
if (!Array.isArray(entries) || entries.length === 0) return;
|
if (!Array.isArray(entries) || entries.length === 0) return;
|
||||||
throwIfAborted(signal);
|
throwIfAborted(signal);
|
||||||
|
|||||||
Reference in New Issue
Block a user