Sync local version

This commit is contained in:
2026-01-26 01:16:35 +08:00
parent 3ad32da21a
commit c1202c2ca2
27 changed files with 16595 additions and 2369 deletions

View File

@@ -0,0 +1,394 @@
// Story Summary - Prompt Injection
// 注入格式:世界状态 → 亲身经历(含闪回) → 相关背景(含闪回) → 记忆碎片 → 人物弧光
import { getContext } from "../../../../../../extensions.js";
import { extension_prompts, extension_prompt_types, extension_prompt_roles } from "../../../../../../../script.js";
import { xbLog } from "../../../core/debug-core.js";
import { getSummaryStore } from "../data/store.js";
import { getVectorConfig, getSummaryPanelConfig, getSettings } from "../data/config.js";
import { recallMemory, buildQueryText } from "../vector/recall.js";
const MODULE_ID = "summaryPrompt";
const SUMMARY_PROMPT_KEY = "LittleWhiteBox_StorySummary";
const BUDGET = { total: 10000, l3Max: 2000, l2Max: 5000 };
const MAX_CHUNKS_PER_EVENT = 2;
const MAX_ORPHAN_CHUNKS = 6;
// ═══════════════════════════════════════════════════════════════════════════
// 工具函数
// ═══════════════════════════════════════════════════════════════════════════
function estimateTokens(text) {
if (!text) return 0;
const s = String(text);
const zh = (s.match(/[\u4e00-\u9fff]/g) || []).length;
return Math.ceil(zh + (s.length - zh) / 4);
}
function pushWithBudget(lines, text, state) {
const t = estimateTokens(text);
if (state.used + t > state.max) return false;
lines.push(text);
state.used += t;
return true;
}
// 从 summary 解析楼层范围:(#321-322) 或 (#321)
function parseFloorRange(summary) {
if (!summary) return null;
// 匹配 (#123-456) 或 (#123)
const match = String(summary).match(/\(#(\d+)(?:-(\d+))?\)/);
if (!match) return null;
const start = parseInt(match[1], 10);
const end = match[2] ? parseInt(match[2], 10) : start;
return { start, end };
}
// 去掉 summary 末尾的楼层标记
function cleanSummary(summary) {
return String(summary || '').replace(/\s*\(#\d+(?:-\d+)?\)\s*$/, '').trim();
}
// ═══════════════════════════════════════════════════════════════════════════
// L1 → L2 归属
// ═══════════════════════════════════════════════════════════════════════════
function attachChunksToEvents(events, chunks) {
const usedChunkIds = new Set();
// 给每个 event 挂载 chunks
for (const e of events) {
e._chunks = [];
const range = parseFloorRange(e.event?.summary);
if (!range) continue;
for (const c of chunks) {
if (c.floor >= range.start && c.floor <= range.end) {
if (!usedChunkIds.has(c.chunkId)) {
e._chunks.push(c);
usedChunkIds.add(c.chunkId);
}
}
}
// 每个事件最多保留 N 条,按相似度排序
e._chunks.sort((a, b) => (b.similarity || 0) - (a.similarity || 0));
e._chunks = e._chunks.slice(0, MAX_CHUNKS_PER_EVENT);
}
// 找出无归属的 chunks记忆碎片
const orphans = chunks
.filter(c => !usedChunkIds.has(c.chunkId))
.sort((a, b) => (b.similarity || 0) - (a.similarity || 0))
.slice(0, MAX_ORPHAN_CHUNKS);
return { events, orphans };
}
// ═══════════════════════════════════════════════════════════════════════════
// 格式化函数
// ═══════════════════════════════════════════════════════════════════════════
function formatWorldLines(world) {
return [...(world || [])]
.sort((a, b) => (b.floor || 0) - (a.floor || 0))
.map(w => `- ${w.topic}${w.content}`);
}
function formatChunkLine(c) {
const text = String(c.text || '');
const preview = text.length > 80 ? text.slice(0, 80) + '...' : text;
return ` #${c.floor} ${preview}`;
}
function formatEventBlock(e, idx) {
const ev = e.event || {};
const time = ev.timeLabel || '';
const people = (ev.participants || []).join(' / ');
const summary = cleanSummary(ev.summary);
const lines = [];
// 标题行
const header = time ? `${idx}.【${time}${people}` : `${idx}. ${people}`;
lines.push(header);
// 摘要
lines.push(` ${summary}`);
// 挂载的闪回
for (const c of (e._chunks || [])) {
lines.push(` ${formatChunkLine(c)}`);
}
return lines.join('\n');
}
function formatArcLine(a) {
const moments = (a.moments || [])
.map(m => typeof m === 'string' ? m : m.text)
.filter(Boolean);
if (moments.length) {
return `- ${a.name}${moments.join(' → ')}(当前:${a.trajectory}`;
}
return `- ${a.name}${a.trajectory}`;
}
// ═══════════════════════════════════════════════════════════════════════════
// 主构建函数
// ═══════════════════════════════════════════════════════════════════════════
function buildMemoryPromptVectorEnabled(store, recallResult) {
const data = store.json || {};
const total = { used: 0, max: BUDGET.total };
const sections = [];
// ─────────────────────────────────────────────────────────────────────
// [世界状态]
// ─────────────────────────────────────────────────────────────────────
const worldLines = formatWorldLines(data.world);
if (worldLines.length) {
const l3 = { used: 0, max: Math.min(BUDGET.l3Max, total.max) };
const l3Lines = [];
for (const line of worldLines) {
if (!pushWithBudget(l3Lines, line, l3)) break;
}
if (l3Lines.length) {
sections.push(`[世界状态] 请严格遵守\n${l3Lines.join('\n')}`);
total.used += l3.used;
}
}
// ─────────────────────────────────────────────────────────────────────
// L1 → L2 归属处理
// ─────────────────────────────────────────────────────────────────────
const events = recallResult?.events || [];
const chunks = recallResult?.chunks || [];
const { events: eventsWithChunks, orphans } = attachChunksToEvents(events, chunks);
// 分离 DIRECT 和 SIMILAR
const directEvents = eventsWithChunks.filter(e => e._recallType === 'DIRECT');
const similarEvents = eventsWithChunks.filter(e => e._recallType !== 'DIRECT');
// ─────────────────────────────────────────────────────────────────────
// [亲身经历] - DIRECT
// ─────────────────────────────────────────────────────────────────────
if (directEvents.length) {
const l2 = { used: 0, max: Math.min(BUDGET.l2Max * 0.7, total.max - total.used) };
const lines = [];
let idx = 1;
for (const e of directEvents) {
const block = formatEventBlock(e, idx);
if (!pushWithBudget(lines, block, l2)) break;
idx++;
}
if (lines.length) {
sections.push(`[亲身经历]\n\n${lines.join('\n\n')}`);
total.used += l2.used;
}
}
// ─────────────────────────────────────────────────────────────────────
// [相关背景] - SIMILAR
// ─────────────────────────────────────────────────────────────────────
if (similarEvents.length) {
const l2s = { used: 0, max: Math.min(BUDGET.l2Max * 0.3, total.max - total.used) };
const lines = [];
let idx = directEvents.length + 1;
for (const e of similarEvents) {
const block = formatEventBlock(e, idx);
if (!pushWithBudget(lines, block, l2s)) break;
idx++;
}
if (lines.length) {
sections.push(`[相关背景]\n\n${lines.join('\n\n')}`);
total.used += l2s.used;
}
}
// ─────────────────────────────────────────────────────────────────────
// [记忆碎片] - 无归属的 chunks
// ─────────────────────────────────────────────────────────────────────
if (orphans.length && total.used < total.max) {
const l1 = { used: 0, max: total.max - total.used };
const lines = [];
for (const c of orphans) {
const line = formatChunkLine(c);
if (!pushWithBudget(lines, line, l1)) break;
}
if (lines.length) {
sections.push(`[记忆碎片]\n${lines.join('\n')}`);
total.used += l1.used;
}
}
// ─────────────────────────────────────────────────────────────────────
// [人物弧光]
// ─────────────────────────────────────────────────────────────────────
if (data.arcs?.length && total.used < total.max) {
const arcLines = data.arcs.map(formatArcLine);
const arcText = `[人物弧光]\n${arcLines.join('\n')}`;
if (total.used + estimateTokens(arcText) <= total.max) {
sections.push(arcText);
}
}
// ─────────────────────────────────────────────────────────────────────
// 组装
// ─────────────────────────────────────────────────────────────────────
if (!sections.length) return '';
return `<剧情记忆>\n\n${sections.join('\n\n')}\n\n</剧情记忆>`;
}
function buildMemoryPromptVectorDisabled(store) {
const data = store.json || {};
const sections = [];
// 世界状态
if (data.world?.length) {
const lines = formatWorldLines(data.world);
sections.push(`[世界状态] 请严格遵守\n${lines.join('\n')}`);
}
// 全部事件(无召回,按时间)
if (data.events?.length) {
const lines = data.events.map((ev, i) => {
const time = ev.timeLabel || '';
const people = (ev.participants || []).join(' / ');
const summary = cleanSummary(ev.summary);
const header = time ? `${i + 1}.【${time}${people}` : `${i + 1}. ${people}`;
return `${header}\n ${summary}`;
});
sections.push(`[剧情记忆]\n\n${lines.join('\n\n')}`);
}
// 弧光
if (data.arcs?.length) {
const lines = data.arcs.map(formatArcLine);
sections.push(`[人物弧光]\n${lines.join('\n')}`);
}
if (!sections.length) return '';
return `<剧情记忆>\n\n${sections.join('\n\n')}\n\n</剧情记忆>`;
}
// ═══════════════════════════════════════════════════════════════════════════
// 导出
// ═══════════════════════════════════════════════════════════════════════════
export function formatPromptWithMemory(store, recallResult) {
const vectorCfg = getVectorConfig();
return vectorCfg?.enabled
? buildMemoryPromptVectorEnabled(store, recallResult)
: buildMemoryPromptVectorDisabled(store);
}
export async function recallAndInjectPrompt(excludeLastAi = false, postToFrame = null) {
if (!getSettings().storySummary?.enabled) {
delete extension_prompts[SUMMARY_PROMPT_KEY];
return;
}
const { chat } = getContext();
const store = getSummaryStore();
if (!store?.json) {
delete extension_prompts[SUMMARY_PROMPT_KEY];
return;
}
const allEvents = store.json.events || [];
const lastIdx = store.lastSummarizedMesId ?? 0;
const length = chat?.length || 0;
if (lastIdx >= length) {
delete extension_prompts[SUMMARY_PROMPT_KEY];
return;
}
const vectorCfg = getVectorConfig();
let recallResult = { events: [], chunks: [] };
if (vectorCfg?.enabled) {
try {
const queryText = buildQueryText(chat, 2, excludeLastAi);
recallResult = await recallMemory(queryText, allEvents, vectorCfg, { excludeLastAi });
postToFrame?.({ type: "RECALL_LOG", text: recallResult.logText || "" });
} catch (e) {
xbLog.error(MODULE_ID, "召回失败", e);
}
}
injectPrompt(store, recallResult, chat);
}
export function updateSummaryExtensionPrompt() {
if (!getSettings().storySummary?.enabled) {
delete extension_prompts[SUMMARY_PROMPT_KEY];
return;
}
const { chat } = getContext();
const store = getSummaryStore();
if (!store?.json || (store.lastSummarizedMesId ?? 0) >= (chat?.length || 0)) {
delete extension_prompts[SUMMARY_PROMPT_KEY];
return;
}
injectPrompt(store, { events: [], chunks: [] }, chat);
}
function injectPrompt(store, recallResult, chat) {
const length = chat?.length || 0;
let text = formatPromptWithMemory(store, recallResult);
const cfg = getSummaryPanelConfig();
if (cfg.trigger?.wrapperHead) {
text = cfg.trigger.wrapperHead + "\n" + text;
}
if (cfg.trigger?.wrapperTail) {
text = text + "\n" + cfg.trigger.wrapperTail;
}
if (!text.trim()) {
delete extension_prompts[SUMMARY_PROMPT_KEY];
return;
}
const lastIdx = store.lastSummarizedMesId ?? 0;
let depth = length - lastIdx - 1;
if (depth < 0) depth = 0;
if (cfg.trigger?.forceInsertAtEnd) {
depth = 10000;
}
extension_prompts[SUMMARY_PROMPT_KEY] = {
value: text,
position: extension_prompt_types.IN_CHAT,
depth,
role: extension_prompt_roles.ASSISTANT,
};
}
export function clearSummaryExtensionPrompt() {
delete extension_prompts[SUMMARY_PROMPT_KEY];
}