mirror of
https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology.git
synced 2026-06-13 18:31:16 +08:00
feat(planner): persist structured plot history
This commit is contained in:
@@ -34,6 +34,7 @@ export function applyPlannerResultAndSend({
|
||||
rawUserInput = '',
|
||||
filtered = '',
|
||||
plannerRecall = null,
|
||||
plannerPlotRecord = null,
|
||||
runtime = null,
|
||||
plannerState = null,
|
||||
} = {}) {
|
||||
@@ -48,10 +49,18 @@ export function applyPlannerResultAndSend({
|
||||
|
||||
let handoffPrepared = false;
|
||||
if (runtime?.preparePlannerRecallHandoff && plannerRecall?.result) {
|
||||
const plotRecordPayload = plannerPlotRecord && typeof plannerPlotRecord === 'object'
|
||||
? {
|
||||
...plannerPlotRecord,
|
||||
rawUserInput: raw,
|
||||
plannerAugmentedMessage: merged,
|
||||
}
|
||||
: null;
|
||||
runtime.preparePlannerRecallHandoff({
|
||||
rawUserInput: raw,
|
||||
plannerAugmentedMessage: merged,
|
||||
plannerRecall,
|
||||
plannerPlotRecord: plotRecordPayload,
|
||||
});
|
||||
handoffPrepared = true;
|
||||
}
|
||||
|
||||
@@ -3,9 +3,8 @@ import { getRequestHeaders, saveSettingsDebounced, substituteParamsExtended } fr
|
||||
import { EnaPlannerStorage, migrateFromLWBIfNeeded } from './ena-planner-storage.js';
|
||||
import {
|
||||
applyPlannerResultAndSend,
|
||||
extractLastNPlots,
|
||||
formatPlotsBlock,
|
||||
} from './ena-planner-runtime-utils.js';
|
||||
import { readPlannerPlotHistory } from './planner-plot-history.js';
|
||||
import { DEFAULT_PROMPT_BLOCKS, BUILTIN_TEMPLATES } from './ena-planner-presets.js';
|
||||
import {
|
||||
createBuiltinPromptBlock,
|
||||
@@ -1743,7 +1742,7 @@ async function buildPlannerMessages(rawUserInput) {
|
||||
// a little continuity even when memory recall returns empty.
|
||||
const recentChatRaw = collectRecentChatSnippet(chat, 2);
|
||||
|
||||
const plotsRaw = formatPlotsBlock(extractLastNPlots(chat, s.plotCount));
|
||||
const plotsRaw = readPlannerPlotHistory(chat, { count: s.plotCount }).block;
|
||||
|
||||
// Build scanText for worldbook keyword activation
|
||||
const scanText = [charBlockRaw, recentChatRaw, plotsRaw, rawUserInput].join('\n\n');
|
||||
@@ -1923,6 +1922,10 @@ async function doInterceptAndPlanThenSend() {
|
||||
rawUserInput: raw,
|
||||
filtered,
|
||||
plannerRecall,
|
||||
plannerPlotRecord: {
|
||||
rawUserInput: raw,
|
||||
plotText: filtered,
|
||||
},
|
||||
runtime: _bmeRuntime,
|
||||
plannerState: state,
|
||||
});
|
||||
|
||||
132
ena-planner/planner-plot-history.js
Normal file
132
ena-planner/planner-plot-history.js
Normal file
@@ -0,0 +1,132 @@
|
||||
import {
|
||||
extractLastNPlots,
|
||||
formatPlotsBlock,
|
||||
} from './ena-planner-runtime-utils.js';
|
||||
|
||||
export const ST_BME_PLOT_HISTORY_KEY = 'st_bme_plot';
|
||||
export const ST_BME_PLOT_HISTORY_VERSION = 1;
|
||||
|
||||
export function hashPlannerPlotInput(text = '') {
|
||||
let hash = 2166136261;
|
||||
for (const char of String(text || '')) {
|
||||
hash ^= char.charCodeAt(0);
|
||||
hash = Math.imul(hash, 16777619);
|
||||
}
|
||||
return String(Math.abs(hash >>> 0));
|
||||
}
|
||||
|
||||
export function createStructuredPlotRecord({
|
||||
rawUserInput = '',
|
||||
plannerAugmentedMessage = '',
|
||||
plotText = '',
|
||||
plotBlocks = null,
|
||||
promptProfileId = '',
|
||||
recallHandoffId = '',
|
||||
taskResults = [],
|
||||
createdAt = Date.now(),
|
||||
inputHash = '',
|
||||
} = {}) {
|
||||
const normalizedRaw = String(rawUserInput || '').trim();
|
||||
const normalizedPlot = String(plotText || '').trim();
|
||||
const blocks = Array.isArray(plotBlocks)
|
||||
? plotBlocks.map((item) => String(item || '').trim()).filter(Boolean)
|
||||
: extractLastNPlots([{ mes: normalizedPlot }], 99);
|
||||
return {
|
||||
version: ST_BME_PLOT_HISTORY_VERSION,
|
||||
inputHash: String(inputHash || hashPlannerPlotInput(normalizedRaw)),
|
||||
rawUserInput: normalizedRaw,
|
||||
plannerAugmentedMessage: String(plannerAugmentedMessage || '').trim(),
|
||||
plotText: normalizedPlot,
|
||||
plotBlocks: blocks,
|
||||
promptProfileId: String(promptProfileId || ''),
|
||||
recallHandoffId: String(recallHandoffId || ''),
|
||||
taskResults: Array.isArray(taskResults) ? taskResults : [],
|
||||
createdAt: Number.isFinite(Number(createdAt)) ? Number(createdAt) : Date.now(),
|
||||
};
|
||||
}
|
||||
|
||||
export function normalizeStructuredPlotRecord(value) {
|
||||
if (!value || typeof value !== 'object') return null;
|
||||
if (Number(value.version) !== ST_BME_PLOT_HISTORY_VERSION) return null;
|
||||
const plotText = String(value.plotText || '').trim();
|
||||
const plotBlocks = Array.isArray(value.plotBlocks)
|
||||
? value.plotBlocks.map((item) => String(item || '').trim()).filter(Boolean)
|
||||
: [];
|
||||
if (!plotText && plotBlocks.length === 0) return null;
|
||||
return createStructuredPlotRecord({
|
||||
...value,
|
||||
plotText,
|
||||
plotBlocks,
|
||||
createdAt: value.createdAt,
|
||||
});
|
||||
}
|
||||
|
||||
export function readStructuredPlotRecordFromMessage(message) {
|
||||
return normalizeStructuredPlotRecord(message?.extra?.[ST_BME_PLOT_HISTORY_KEY]);
|
||||
}
|
||||
|
||||
export function collectStructuredPlotRecords(chat, count = 2) {
|
||||
if (!Array.isArray(chat) || chat.length === 0) return [];
|
||||
const want = Math.max(0, Number(count) || 0);
|
||||
if (!want) return [];
|
||||
const records = [];
|
||||
for (let index = chat.length - 1; index >= 0; index--) {
|
||||
const record = readStructuredPlotRecordFromMessage(chat[index]);
|
||||
if (!record) continue;
|
||||
records.push(record);
|
||||
if (records.length >= want) break;
|
||||
}
|
||||
return records;
|
||||
}
|
||||
|
||||
export function readPlannerPlotHistory(chat, { count = 2 } = {}) {
|
||||
const structuredRecords = collectStructuredPlotRecords(chat, count);
|
||||
if (structuredRecords.length > 0) {
|
||||
const plots = structuredRecords.map((record) => record.plotText || record.plotBlocks.join('\n')).filter(Boolean);
|
||||
return {
|
||||
source: 'structured',
|
||||
records: structuredRecords,
|
||||
plots,
|
||||
block: formatPlotsBlock(plots),
|
||||
};
|
||||
}
|
||||
|
||||
const plots = extractLastNPlots(chat, count);
|
||||
return {
|
||||
source: plots.length > 0 ? 'legacy' : 'empty',
|
||||
records: [],
|
||||
plots,
|
||||
block: formatPlotsBlock(plots),
|
||||
};
|
||||
}
|
||||
|
||||
export function writeStructuredPlotRecordToMessage(message, recordInput) {
|
||||
if (!message || typeof message !== 'object' || !message.is_user) return false;
|
||||
const record = normalizeStructuredPlotRecord(
|
||||
recordInput?.version ? recordInput : createStructuredPlotRecord(recordInput),
|
||||
);
|
||||
if (!record) return false;
|
||||
message.extra = message.extra && typeof message.extra === 'object'
|
||||
? message.extra
|
||||
: {};
|
||||
message.extra[ST_BME_PLOT_HISTORY_KEY] = record;
|
||||
return true;
|
||||
}
|
||||
|
||||
export function writeStructuredPlotRecordToMatchingUserMessage(chat, recordInput) {
|
||||
if (!Array.isArray(chat)) return null;
|
||||
const record = normalizeStructuredPlotRecord(
|
||||
recordInput?.version ? recordInput : createStructuredPlotRecord(recordInput),
|
||||
);
|
||||
if (!record) return null;
|
||||
const inputHash = String(record.inputHash || hashPlannerPlotInput(record.rawUserInput));
|
||||
for (let index = chat.length - 1; index >= 0; index--) {
|
||||
const message = chat[index];
|
||||
if (!message?.is_user) continue;
|
||||
if (hashPlannerPlotInput(message.mes || '') !== inputHash) continue;
|
||||
if (writeStructuredPlotRecordToMessage(message, record)) {
|
||||
return { index, record };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -327,6 +327,7 @@ export function onMessageSentController(runtime, messageId) {
|
||||
resolvedMessageId,
|
||||
message.mes || "",
|
||||
);
|
||||
runtime.persistPlannerPlotRecordToUserMessage?.(resolvedMessageId);
|
||||
// GENERATION_AFTER_COMMANDS 在 sendMessageAsUser 之前触发,此时新用户消息
|
||||
// 尚未进入 chat,recall 记录会被写到上一条 user 上。这里用户消息刚入场,
|
||||
// transaction 仍在桥接窗口内,立即把记录重新绑定到正确的楼层。
|
||||
|
||||
29
index.js
29
index.js
@@ -111,6 +111,7 @@ import {
|
||||
registerGenerationAfterCommandsController,
|
||||
scheduleSendIntentHookRetryController,
|
||||
} from "./host/event-binding.js";
|
||||
import { writeStructuredPlotRecordToMessage } from "./ena-planner/planner-plot-history.js";
|
||||
import {
|
||||
BME_HOST_PROFILE_LUKER,
|
||||
getBmeHostAdapter,
|
||||
@@ -14734,16 +14735,43 @@ function preparePlannerRecallHandoff({
|
||||
rawUserInput = "",
|
||||
plannerAugmentedMessage = "",
|
||||
plannerRecall = null,
|
||||
plannerPlotRecord = null,
|
||||
chatId = getCurrentChatId(),
|
||||
} = {}) {
|
||||
return rerollRecallInput.preparePlannerRecallHandoff({
|
||||
rawUserInput,
|
||||
plannerAugmentedMessage,
|
||||
plannerRecall,
|
||||
plannerPlotRecord,
|
||||
chatId,
|
||||
});
|
||||
}
|
||||
|
||||
function persistPlannerPlotRecordToUserMessage(newUserMessageIndex) {
|
||||
const context = getContext();
|
||||
const chat = context?.chat;
|
||||
if (
|
||||
!Array.isArray(chat) ||
|
||||
!Number.isFinite(newUserMessageIndex) ||
|
||||
!chat[newUserMessageIndex]?.is_user
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
const handoff = peekPlannerRecallHandoff(context?.chatId || getCurrentChatId());
|
||||
const plannerPlotRecord = handoff?.plannerPlotRecord;
|
||||
if (!plannerPlotRecord || typeof plannerPlotRecord !== "object") {
|
||||
return false;
|
||||
}
|
||||
const wrote = writeStructuredPlotRecordToMessage(chat[newUserMessageIndex], {
|
||||
...plannerPlotRecord,
|
||||
recallHandoffId: handoff.id || plannerPlotRecord.recallHandoffId || "",
|
||||
});
|
||||
if (wrote) {
|
||||
triggerChatMetadataSave(context, { immediate: false });
|
||||
}
|
||||
return wrote;
|
||||
}
|
||||
|
||||
function buildPreGenerationRecallKey(type, options = {}) {
|
||||
return generationRecallTransactionRuntime.buildPreGenerationRecallKey(
|
||||
type,
|
||||
@@ -16051,6 +16079,7 @@ function onMessageSent(messageId) {
|
||||
getContext,
|
||||
isTrivialUserInput,
|
||||
markCurrentGenerationTrivialSkip,
|
||||
persistPlannerPlotRecordToUserMessage,
|
||||
recordRecallSentUserMessage,
|
||||
rebindRecallRecordToNewUserMessage,
|
||||
refreshPersistedRecallMessageUi: schedulePersistedRecallMessageUiRefresh,
|
||||
|
||||
@@ -309,6 +309,7 @@ export function createRerollRecallInput(deps = {}) {
|
||||
rawUserInput = "",
|
||||
plannerAugmentedMessage = "",
|
||||
plannerRecall = null,
|
||||
plannerPlotRecord = null,
|
||||
chatId = getCurrentChatId(),
|
||||
} = {}) {
|
||||
const normalizedChatId = normalizeChatIdCandidate(chatId);
|
||||
@@ -340,6 +341,10 @@ export function createRerollRecallInput(deps = {}) {
|
||||
? plannerRecall.recentMessages.map((item) => String(item || ""))
|
||||
: [],
|
||||
injectionText,
|
||||
plannerPlotRecord:
|
||||
plannerPlotRecord && typeof plannerPlotRecord === "object"
|
||||
? { ...plannerPlotRecord }
|
||||
: null,
|
||||
source: "planner-handoff",
|
||||
sourceLabel: "Planner handoff",
|
||||
createdAt,
|
||||
|
||||
@@ -5,6 +5,12 @@ import {
|
||||
extractLastNPlots,
|
||||
formatPlotsBlock,
|
||||
} from '../ena-planner/ena-planner-runtime-utils.js';
|
||||
import {
|
||||
createStructuredPlotRecord,
|
||||
readPlannerPlotHistory,
|
||||
writeStructuredPlotRecordToMatchingUserMessage,
|
||||
writeStructuredPlotRecordToMessage,
|
||||
} from '../ena-planner/planner-plot-history.js';
|
||||
|
||||
{
|
||||
const chat = [
|
||||
@@ -44,6 +50,11 @@ import {
|
||||
assert.equal(payload.rawUserInput, 'raw input');
|
||||
assert.equal(payload.plannerAugmentedMessage, 'raw input\n\n<plot>next</plot>');
|
||||
assert.equal(payload.plannerRecall, plannerRecall);
|
||||
assert.deepEqual(payload.plannerPlotRecord, {
|
||||
rawUserInput: 'raw input',
|
||||
plannerAugmentedMessage: 'raw input\n\n<plot>next</plot>',
|
||||
plotText: '<plot>next</plot>',
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
@@ -53,6 +64,7 @@ import {
|
||||
rawUserInput: 'raw input',
|
||||
filtered: '<plot>next</plot>',
|
||||
plannerRecall,
|
||||
plannerPlotRecord: { plotText: '<plot>next</plot>' },
|
||||
runtime,
|
||||
plannerState,
|
||||
});
|
||||
@@ -65,6 +77,51 @@ import {
|
||||
assert.equal(plannerState.bypassNextSend, true);
|
||||
}
|
||||
|
||||
{
|
||||
const chat = [
|
||||
{ is_user: true, mes: 'raw old', extra: {} },
|
||||
{ is_user: false, mes: '<plot>legacy stale</plot>' },
|
||||
{ is_user: true, mes: 'raw latest', extra: {} },
|
||||
];
|
||||
writeStructuredPlotRecordToMessage(chat[2], createStructuredPlotRecord({
|
||||
rawUserInput: 'raw latest',
|
||||
plannerAugmentedMessage: 'raw latest\n\n<plot>structured</plot>',
|
||||
plotText: '<plot>structured</plot>',
|
||||
}));
|
||||
const history = readPlannerPlotHistory(chat, { count: 2 });
|
||||
assert.equal(history.source, 'structured');
|
||||
assert.deepEqual(history.plots, ['<plot>structured</plot>']);
|
||||
assert.ok(history.block.includes('<plot>structured</plot>'));
|
||||
assert.ok(!history.block.includes('legacy stale'));
|
||||
}
|
||||
|
||||
{
|
||||
const chat = [
|
||||
{ is_user: true, mes: 'raw old', extra: {} },
|
||||
{ is_user: false, mes: '<plot>legacy old</plot>' },
|
||||
];
|
||||
chat[0].extra.st_bme_plot = { version: 999, plotText: '<plot>bad</plot>' };
|
||||
const history = readPlannerPlotHistory(chat, { count: 1 });
|
||||
assert.equal(history.source, 'legacy');
|
||||
assert.deepEqual(history.plots, ['<plot>legacy old</plot>']);
|
||||
}
|
||||
|
||||
{
|
||||
const chat = [
|
||||
{ is_user: true, mes: 'first input', extra: {} },
|
||||
{ is_user: false, mes: 'assistant' },
|
||||
{ is_user: true, mes: 'second input', extra: {} },
|
||||
];
|
||||
const result = writeStructuredPlotRecordToMatchingUserMessage(chat, {
|
||||
rawUserInput: 'first input',
|
||||
plannerAugmentedMessage: 'first input\n\n<plot>first plan</plot>',
|
||||
plotText: '<plot>first plan</plot>',
|
||||
});
|
||||
assert.equal(result.index, 0);
|
||||
assert.equal(chat[0].extra.st_bme_plot.plotText, '<plot>first plan</plot>');
|
||||
assert.equal(chat[2].extra.st_bme_plot, undefined);
|
||||
}
|
||||
|
||||
{
|
||||
const order = [];
|
||||
const result = applyPlannerResultAndSend({
|
||||
|
||||
Reference in New Issue
Block a user