mirror of
https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology.git
synced 2026-05-15 22:30:38 +08:00
187 lines
5.8 KiB
JavaScript
187 lines
5.8 KiB
JavaScript
// ST-BME: 持久化召回记录纯函数
|
||
|
||
export const BME_RECALL_EXTRA_KEY = "bme_recall";
|
||
export const BME_RECALL_VERSION = 1;
|
||
|
||
function toIsoString(value) {
|
||
if (typeof value === "string" && value.trim()) return value;
|
||
return new Date().toISOString();
|
||
}
|
||
|
||
function cloneStringArray(value) {
|
||
return Array.isArray(value)
|
||
? value
|
||
.map((item) => String(item || "").trim())
|
||
.filter(Boolean)
|
||
: [];
|
||
}
|
||
|
||
function cloneRecord(value) {
|
||
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
||
return { ...value };
|
||
}
|
||
|
||
export function readPersistedRecallFromUserMessage(chat, userMessageIndex) {
|
||
if (!Array.isArray(chat) || !Number.isFinite(userMessageIndex)) return null;
|
||
const message = chat[userMessageIndex];
|
||
const raw = message?.extra?.[BME_RECALL_EXTRA_KEY];
|
||
const record = cloneRecord(raw);
|
||
if (!record) return null;
|
||
|
||
const injectionText = String(record.injectionText || "").trim();
|
||
if (!injectionText) return null;
|
||
|
||
return {
|
||
version: Number.isFinite(Number(record.version))
|
||
? Number(record.version)
|
||
: BME_RECALL_VERSION,
|
||
injectionText,
|
||
selectedNodeIds: cloneStringArray(record.selectedNodeIds),
|
||
recallInput: String(record.recallInput || ""),
|
||
recallSource: String(record.recallSource || ""),
|
||
hookName: String(record.hookName || ""),
|
||
tokenEstimate: Number.isFinite(Number(record.tokenEstimate))
|
||
? Number(record.tokenEstimate)
|
||
: 0,
|
||
createdAt: toIsoString(record.createdAt),
|
||
updatedAt: toIsoString(record.updatedAt),
|
||
generationCount: Math.max(0, Number.parseInt(record.generationCount, 10) || 0),
|
||
manuallyEdited: Boolean(record.manuallyEdited),
|
||
};
|
||
}
|
||
|
||
export function buildPersistedRecallRecord(payload = {}, existingRecord = null) {
|
||
const nowIso = toIsoString(payload.nowIso);
|
||
const previous = cloneRecord(existingRecord) || {};
|
||
const injectionText = String(payload.injectionText || "").trim();
|
||
|
||
return {
|
||
version: BME_RECALL_VERSION,
|
||
injectionText,
|
||
selectedNodeIds: cloneStringArray(payload.selectedNodeIds),
|
||
recallInput: String(payload.recallInput || ""),
|
||
recallSource: String(payload.recallSource || ""),
|
||
hookName: String(payload.hookName || ""),
|
||
tokenEstimate: Number.isFinite(Number(payload.tokenEstimate))
|
||
? Number(payload.tokenEstimate)
|
||
: 0,
|
||
createdAt: toIsoString(previous.createdAt || nowIso),
|
||
updatedAt: nowIso,
|
||
generationCount: 0,
|
||
manuallyEdited: Boolean(payload.manuallyEdited),
|
||
};
|
||
}
|
||
|
||
export function writePersistedRecallToUserMessage(chat, userMessageIndex, record) {
|
||
if (!Array.isArray(chat) || !Number.isFinite(userMessageIndex)) return false;
|
||
const message = chat[userMessageIndex];
|
||
if (!message || !message.is_user) return false;
|
||
|
||
const normalized = cloneRecord(record);
|
||
if (!normalized || !String(normalized.injectionText || "").trim()) return false;
|
||
|
||
message.extra ||= {};
|
||
message.extra[BME_RECALL_EXTRA_KEY] = normalized;
|
||
return true;
|
||
}
|
||
|
||
export function removePersistedRecallFromUserMessage(chat, userMessageIndex) {
|
||
if (!Array.isArray(chat) || !Number.isFinite(userMessageIndex)) return false;
|
||
const message = chat[userMessageIndex];
|
||
if (!message?.extra || typeof message.extra !== "object") return false;
|
||
if (!(BME_RECALL_EXTRA_KEY in message.extra)) return false;
|
||
delete message.extra[BME_RECALL_EXTRA_KEY];
|
||
return true;
|
||
}
|
||
|
||
export function markPersistedRecallManualEdit(
|
||
chat,
|
||
userMessageIndex,
|
||
manuallyEdited = true,
|
||
nowIso = new Date().toISOString(),
|
||
) {
|
||
const current = readPersistedRecallFromUserMessage(chat, userMessageIndex);
|
||
if (!current) return null;
|
||
const nextRecord = {
|
||
...current,
|
||
manuallyEdited: Boolean(manuallyEdited),
|
||
updatedAt: toIsoString(nowIso),
|
||
};
|
||
if (!writePersistedRecallToUserMessage(chat, userMessageIndex, nextRecord)) {
|
||
return null;
|
||
}
|
||
return nextRecord;
|
||
}
|
||
|
||
export function bumpPersistedRecallGenerationCount(chat, userMessageIndex) {
|
||
const current = readPersistedRecallFromUserMessage(chat, userMessageIndex);
|
||
if (!current) return null;
|
||
const nextRecord = {
|
||
...current,
|
||
generationCount: Math.max(0, Number(current.generationCount || 0)) + 1,
|
||
};
|
||
if (!writePersistedRecallToUserMessage(chat, userMessageIndex, nextRecord)) {
|
||
return null;
|
||
}
|
||
return nextRecord;
|
||
}
|
||
|
||
export function resolveGenerationTargetUserMessageIndex(
|
||
chat,
|
||
{ generationType = "normal" } = {},
|
||
) {
|
||
if (!Array.isArray(chat) || chat.length === 0) return null;
|
||
|
||
const normalizedType = String(generationType || "normal").trim() || "normal";
|
||
|
||
// normal:取「最后一条非系统用户楼层」。若直接 return 末条非 user(常见为刚追加的助手回合),
|
||
// 会得到 null,导致持久化无法回绑到本轮 user,`hasRecordForLatest` 长期为 false。
|
||
if (normalizedType === "normal") {
|
||
for (let index = chat.length - 1; index >= 0; index--) {
|
||
const message = chat[index];
|
||
if (message?.is_system) continue;
|
||
if (message?.is_user) return index;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
for (let index = chat.length - 1; index >= 0; index--) {
|
||
if (chat[index]?.is_user) return index;
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
export function resolveFinalRecallInjectionSource({
|
||
freshRecallResult = null,
|
||
persistedRecord = null,
|
||
} = {}) {
|
||
const freshInjection = String(freshRecallResult?.injectionText || "").trim();
|
||
if (
|
||
freshRecallResult?.status === "completed" &&
|
||
freshRecallResult?.didRecall &&
|
||
freshInjection
|
||
) {
|
||
return {
|
||
source: "fresh",
|
||
injectionText: freshInjection,
|
||
record: null,
|
||
};
|
||
}
|
||
|
||
const persistedInjection = String(persistedRecord?.injectionText || "").trim();
|
||
if (persistedInjection) {
|
||
return {
|
||
source: "persisted",
|
||
injectionText: persistedInjection,
|
||
record: persistedRecord,
|
||
};
|
||
}
|
||
|
||
return {
|
||
source: "none",
|
||
injectionText: "",
|
||
record: null,
|
||
};
|
||
}
|