Update story summary recall and prompt
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// Story Summary - Prompt Injection (Final Clean Version)
|
||||
// - 仅负责“构建注入文本”,不负责写入 extension_prompts
|
||||
// - 仅负责"构建注入文本",不负责写入 extension_prompts
|
||||
// - 注入发生在 story-summary.js:GENERATION_STARTED 时写入 extension_prompts(IN_CHAT + depth)
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
@@ -8,8 +8,8 @@ import { getContext } from "../../../../../../extensions.js";
|
||||
import { xbLog } from "../../../core/debug-core.js";
|
||||
import { getSummaryStore, getFacts, isRelationFact } from "../data/store.js";
|
||||
import { getVectorConfig, getSummaryPanelConfig, getSettings } from "../data/config.js";
|
||||
import { recallMemory, buildQueryText } from "../vector/recall.js";
|
||||
import { getChunksByFloors, getAllChunkVectors, getAllEventVectors, getMeta } from "../vector/chunk-store.js";
|
||||
import { recallMemory, buildQueryText } from "../vector/retrieval/recall.js";
|
||||
import { getChunksByFloors, getAllChunkVectors, getAllEventVectors, getMeta } from "../vector/storage/chunk-store.js";
|
||||
|
||||
const MODULE_ID = "summaryPrompt";
|
||||
|
||||
@@ -85,6 +85,49 @@ function cleanSummary(summary) {
|
||||
.trim();
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// 上下文配对工具函数
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* 获取chunk的配对楼层
|
||||
* USER楼层 → 下一楼(AI回复)
|
||||
* AI楼层 → 上一楼(USER发言)
|
||||
*/
|
||||
function getContextFloor(chunk) {
|
||||
if (chunk.isL0) return -1; // L0虚拟chunk不需要配对
|
||||
return chunk.isUser ? chunk.floor + 1 : chunk.floor - 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从候选chunks中选择最佳配对
|
||||
* 策略:优先选择相反角色的第一个chunk
|
||||
*/
|
||||
function pickContextChunk(candidates, mainChunk) {
|
||||
if (!candidates?.length) return null;
|
||||
const targetIsUser = !mainChunk.isUser;
|
||||
// 优先相反角色
|
||||
const opposite = candidates.find(c => c.isUser === targetIsUser);
|
||||
if (opposite) return opposite;
|
||||
// 否则选第一个
|
||||
return candidates[0];
|
||||
}
|
||||
/**
|
||||
* 格式化配对chunk(完整显示,带缩进和方向符号)
|
||||
*/
|
||||
function formatContextChunkLine(chunk, isAbove) {
|
||||
const { name1, name2 } = getContext();
|
||||
const speaker = chunk.isUser ? (name1 || "用户") : (chunk.speaker || name2 || "角色");
|
||||
const text = String(chunk.text || "").trim();
|
||||
const symbol = isAbove ? "┌" : "└";
|
||||
return ` ${symbol} #${chunk.floor + 1} [${speaker}] ${text}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化配对chunk(缩进,简短摘要)
|
||||
*/
|
||||
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// 系统前导与后缀
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
@@ -150,7 +193,31 @@ function formatChunkFullLine(c) {
|
||||
return `› #${c.floor + 1} [${speaker}] ${String(c.text || "").trim()}`;
|
||||
}
|
||||
|
||||
// 因果事件格式(仅作为“前因线索”展示,仍保留楼层提示)
|
||||
/**
|
||||
* 格式化chunk及其配对上下文
|
||||
* 返回数组:[配对行(如果在前), 主chunk行, 配对行(如果在后)]
|
||||
*/
|
||||
function formatChunkWithContext(mainChunk, contextChunk) {
|
||||
const lines = [];
|
||||
const mainLine = formatChunkFullLine(mainChunk);
|
||||
|
||||
if (!contextChunk) {
|
||||
lines.push(mainLine);
|
||||
return lines;
|
||||
}
|
||||
|
||||
if (contextChunk.floor < mainChunk.floor) {
|
||||
lines.push(formatContextChunkLine(contextChunk, true));
|
||||
lines.push(mainLine);
|
||||
} else {
|
||||
lines.push(mainLine);
|
||||
lines.push(formatContextChunkLine(contextChunk, false));
|
||||
}
|
||||
|
||||
return lines;
|
||||
}
|
||||
|
||||
// 因果事件格式(仅作为"前因线索"展示,仍保留楼层提示)
|
||||
function formatCausalEventLine(causalItem, causalById) {
|
||||
const ev = causalItem?.event || {};
|
||||
const depth = Math.max(1, Math.min(9, causalItem?._causalDepth || 1));
|
||||
@@ -172,9 +239,8 @@ function formatCausalEventLine(causalItem, causalById) {
|
||||
const evidence = causalItem._evidenceChunk;
|
||||
if (evidence) {
|
||||
const speaker = evidence.speaker || "角色";
|
||||
const preview = String(evidence.text || "");
|
||||
const clip = preview.length > 60 ? preview.slice(0, 60) + "..." : preview;
|
||||
lines.push(`${indent} › #${evidence.floor + 1} [${speaker}] ${clip}`);
|
||||
const text = String(evidence.text || "").trim();
|
||||
lines.push(`${indent} › #${evidence.floor + 1} [${speaker}] ${text}`);
|
||||
}
|
||||
|
||||
return lines.join("\n");
|
||||
@@ -216,11 +282,13 @@ function formatInjectionLog(stats, details, recentOrphanStats = null) {
|
||||
const l1OrphanCount = (stats.orphans.injected || 0) - l0OrphanCount;
|
||||
lines.push(` [3] 远期片段 (已总结范围)`);
|
||||
lines.push(` 选入: ${stats.orphans.injected} 条 (L0: ${l0OrphanCount}, L1: ${l1OrphanCount}) | 消耗: ${stats.orphans.tokens} tokens`);
|
||||
lines.push(` 配对: ${stats.orphans.contextPairs || 0} 条`);
|
||||
lines.push('');
|
||||
|
||||
// [4] 待整理
|
||||
lines.push(` [4] 待整理 (独立预算 5000)`);
|
||||
lines.push(` 选入: ${recentOrphanStats?.injected || 0} 条 | 消耗: ${recentOrphanStats?.tokens || 0} tokens`);
|
||||
lines.push(` 配对: ${recentOrphanStats?.contextPairs || 0} 条`);
|
||||
lines.push(` 楼层: ${recentOrphanStats?.floorRange || 'N/A'}`);
|
||||
lines.push('');
|
||||
|
||||
@@ -248,7 +316,7 @@ function formatInjectionLog(stats, details, recentOrphanStats = null) {
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
// 重写事件文本里的序号前缀:把 “{idx}. ” 或 “{idx}.【...】” 的 idx 替换
|
||||
// 重写事件文本里的序号前缀:把 "{idx}. " 或 "{idx}.【...】" 的 idx 替换
|
||||
function renumberEventText(text, newIndex) {
|
||||
const s = String(text || "");
|
||||
// 匹配行首: "12." 或 "12.【"
|
||||
@@ -325,11 +393,12 @@ export function buildNonVectorPromptText() {
|
||||
return text;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// 向量模式:预算装配(世界 → 事件(带证据) → 碎片 → 弧光)
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
async function buildVectorPrompt(store, recallResult, causalById, queryEntities = [], meta = null) {
|
||||
const { chatId } = getContext();
|
||||
const data = store.json || {};
|
||||
const total = { used: 0, max: MAIN_BUDGET_MAX };
|
||||
|
||||
@@ -351,13 +420,14 @@ async function buildVectorPrompt(store, recallResult, causalById, queryEntities
|
||||
arcs: { count: 0, tokens: 0 },
|
||||
events: { selected: 0, tokens: 0 },
|
||||
evidence: { attached: 0, tokens: 0 },
|
||||
orphans: { injected: 0, tokens: 0 },
|
||||
orphans: { injected: 0, tokens: 0, l0Count: 0, contextPairs: 0 },
|
||||
};
|
||||
|
||||
const recentOrphanStats = {
|
||||
injected: 0,
|
||||
tokens: 0,
|
||||
floorRange: "N/A",
|
||||
contextPairs: 0,
|
||||
};
|
||||
const details = {
|
||||
eventList: [],
|
||||
@@ -473,14 +543,14 @@ async function buildVectorPrompt(store, recallResult, causalById, queryEntities
|
||||
|
||||
const bestChunk = pickBestChunkForEvent(e.event);
|
||||
|
||||
// 先尝试“带证据”
|
||||
// 先尝试"带证据"
|
||||
// idx 先占位写 0,后面统一按时间线重排后再改号
|
||||
let text = formatEventWithEvidence(e, 0, bestChunk);
|
||||
let cost = estimateTokens(text);
|
||||
let hasEvidence = !!bestChunk;
|
||||
let chosenChunk = bestChunk || null;
|
||||
|
||||
// 塞不下就退化成“不带证据”
|
||||
// 塞不下就退化成"不带证据"
|
||||
if (total.used + cost > total.max) {
|
||||
text = formatEventWithEvidence(e, 0, null);
|
||||
cost = estimateTokens(text);
|
||||
@@ -549,33 +619,90 @@ async function buildVectorPrompt(store, recallResult, causalById, queryEntities
|
||||
assembled.events.similar = selectedSimilarTexts;
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// [优先级 4] 远期片段(已总结范围的 orphan chunks)
|
||||
// [优先级 4] 远期片段(已总结范围的 orphan chunks)- 带上下文配对
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
const lastSummarized = store.lastSummarizedMesId ?? -1;
|
||||
const lastChunkFloor = meta?.lastChunkFloor ?? -1;
|
||||
const keepVisible = store.keepVisibleCount ?? 3;
|
||||
|
||||
if (chunks.length && total.used < total.max) {
|
||||
const orphans = chunks
|
||||
.filter(c => !usedChunkIds.has(c.chunkId))
|
||||
.filter(c => c.floor <= lastSummarized)
|
||||
// 收集需要配对的楼层
|
||||
const orphanContextFloors = new Set();
|
||||
const orphanCandidates = chunks
|
||||
.filter(c => !usedChunkIds.has(c.chunkId))
|
||||
.filter(c => c.floor <= lastSummarized);
|
||||
|
||||
for (const c of orphanCandidates) {
|
||||
if (c.isL0) continue;
|
||||
const pairFloor = getContextFloor(c);
|
||||
if (pairFloor >= 0) orphanContextFloors.add(pairFloor);
|
||||
}
|
||||
|
||||
// 批量获取配对楼层的chunks
|
||||
let contextChunksByFloor = new Map();
|
||||
if (chatId && orphanContextFloors.size > 0) {
|
||||
try {
|
||||
const contextChunks = await getChunksByFloors(chatId, Array.from(orphanContextFloors));
|
||||
for (const pc of contextChunks) {
|
||||
if (!contextChunksByFloor.has(pc.floor)) {
|
||||
contextChunksByFloor.set(pc.floor, []);
|
||||
}
|
||||
contextChunksByFloor.get(pc.floor).push(pc);
|
||||
}
|
||||
} catch (e) {
|
||||
xbLog.warn(MODULE_ID, "获取配对chunks失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
if (orphanCandidates.length && total.used < total.max) {
|
||||
const orphans = orphanCandidates
|
||||
.sort((a, b) => (a.floor - b.floor) || ((a.chunkIdx ?? 0) - (b.chunkIdx ?? 0)));
|
||||
|
||||
const l1Budget = { used: 0, max: total.max - total.used };
|
||||
let l0Count = 0;
|
||||
let contextPairsCount = 0;
|
||||
|
||||
for (const c of orphans) {
|
||||
const line = formatChunkFullLine(c);
|
||||
if (!pushWithBudget(assembled.orphans.lines, line, l1Budget)) break;
|
||||
// L0 不需要配对
|
||||
if (c.isL0) {
|
||||
const line = formatChunkFullLine(c);
|
||||
if (!pushWithBudget(assembled.orphans.lines, line, l1Budget)) break;
|
||||
injectionStats.orphans.injected++;
|
||||
l0Count++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 获取配对chunk
|
||||
const pairFloor = getContextFloor(c);
|
||||
const candidates = contextChunksByFloor.get(pairFloor) || [];
|
||||
const contextChunk = pickContextChunk(candidates, c);
|
||||
|
||||
// 格式化(带配对)
|
||||
const formattedLines = formatChunkWithContext(c, contextChunk);
|
||||
|
||||
// 尝试添加所有行
|
||||
let allAdded = true;
|
||||
for (const line of formattedLines) {
|
||||
if (!pushWithBudget(assembled.orphans.lines, line, l1Budget)) {
|
||||
allAdded = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!allAdded) break;
|
||||
|
||||
injectionStats.orphans.injected++;
|
||||
if (contextChunk) contextPairsCount++;
|
||||
}
|
||||
|
||||
assembled.orphans.tokens = l1Budget.used;
|
||||
total.used += l1Budget.used;
|
||||
injectionStats.orphans.tokens = l1Budget.used;
|
||||
injectionStats.orphans.l0Count = l0Count;
|
||||
injectionStats.orphans.contextPairs = contextPairsCount;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// [独立预算] 待整理(未总结范围,独立 5000)
|
||||
// [独立预算] 待整理(未总结范围,独立 5000)- 带上下文配对
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
// 近期范围:(lastSummarized, lastChunkFloor - keepVisible]
|
||||
@@ -583,55 +710,113 @@ async function buildVectorPrompt(store, recallResult, causalById, queryEntities
|
||||
const recentEnd = lastChunkFloor - keepVisible;
|
||||
|
||||
if (chunks.length && recentEnd >= recentStart) {
|
||||
const recentOrphans = chunks
|
||||
const recentOrphanCandidates = chunks
|
||||
.filter(c => !usedChunkIds.has(c.chunkId))
|
||||
.filter(c => c.floor >= recentStart && c.floor <= recentEnd)
|
||||
.filter(c => c.floor >= recentStart && c.floor <= recentEnd);
|
||||
|
||||
// 收集近期范围需要配对的楼层
|
||||
const recentContextFloors = new Set();
|
||||
for (const c of recentOrphanCandidates) {
|
||||
if (c.isL0) continue;
|
||||
const pairFloor = getContextFloor(c);
|
||||
if (pairFloor >= 0) recentContextFloors.add(pairFloor);
|
||||
}
|
||||
|
||||
// 批量获取(复用已有的 or 新获取)
|
||||
let recentContextChunksByFloor = new Map();
|
||||
if (chatId && recentContextFloors.size > 0) {
|
||||
// 过滤掉已经获取过的
|
||||
const newFloors = Array.from(recentContextFloors).filter(f => !contextChunksByFloor.has(f));
|
||||
if (newFloors.length > 0) {
|
||||
try {
|
||||
const newContextChunks = await getChunksByFloors(chatId, newFloors);
|
||||
for (const pc of newContextChunks) {
|
||||
if (!contextChunksByFloor.has(pc.floor)) {
|
||||
contextChunksByFloor.set(pc.floor, []);
|
||||
}
|
||||
contextChunksByFloor.get(pc.floor).push(pc);
|
||||
}
|
||||
} catch (e) {
|
||||
xbLog.warn(MODULE_ID, "获取近期配对chunks失败", e);
|
||||
}
|
||||
}
|
||||
recentContextChunksByFloor = contextChunksByFloor;
|
||||
}
|
||||
|
||||
const recentOrphans = recentOrphanCandidates
|
||||
.sort((a, b) => (a.floor - b.floor) || ((a.chunkIdx ?? 0) - (b.chunkIdx ?? 0)));
|
||||
|
||||
const recentBudget = { used: 0, max: RECENT_ORPHAN_MAX };
|
||||
let recentContextPairsCount = 0;
|
||||
|
||||
for (const c of recentOrphans) {
|
||||
const line = formatChunkFullLine(c);
|
||||
if (!pushWithBudget(assembled.recentOrphans.lines, line, recentBudget)) break;
|
||||
// L0 不需要配对
|
||||
if (c.isL0) {
|
||||
const line = formatChunkFullLine(c);
|
||||
if (!pushWithBudget(assembled.recentOrphans.lines, line, recentBudget)) break;
|
||||
recentOrphanStats.injected++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 获取配对chunk
|
||||
const pairFloor = getContextFloor(c);
|
||||
const candidates = recentContextChunksByFloor.get(pairFloor) || [];
|
||||
const contextChunk = pickContextChunk(candidates, c);
|
||||
|
||||
// 格式化(带配对)
|
||||
const formattedLines = formatChunkWithContext(c, contextChunk);
|
||||
|
||||
// 尝试添加所有行
|
||||
let allAdded = true;
|
||||
for (const line of formattedLines) {
|
||||
if (!pushWithBudget(assembled.recentOrphans.lines, line, recentBudget)) {
|
||||
allAdded = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!allAdded) break;
|
||||
|
||||
recentOrphanStats.injected++;
|
||||
if (contextChunk) recentContextPairsCount++;
|
||||
}
|
||||
|
||||
assembled.recentOrphans.tokens = recentBudget.used;
|
||||
recentOrphanStats.tokens = recentBudget.used;
|
||||
recentOrphanStats.floorRange = `${recentStart + 1}~${recentEnd + 1}楼`;
|
||||
recentOrphanStats.contextPairs = recentContextPairsCount;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// 按注入顺序拼接 sections
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
const sections = [];
|
||||
// 1. 世界约束 → 定了的事
|
||||
if (assembled.facts.lines.length) {
|
||||
sections.push(`[定了的事] 已确立的事实\n${assembled.facts.lines.join("\n")}`);
|
||||
}
|
||||
// 2. 核心经历 → 印象深的事
|
||||
if (assembled.events.direct.length) {
|
||||
sections.push(`[印象深的事] 记得很清楚\n\n${assembled.events.direct.join("\n\n")}`);
|
||||
}
|
||||
// 3. 过往背景 → 好像有关的事
|
||||
if (assembled.events.similar.length) {
|
||||
sections.push(`[好像有关的事] 听说过或有点模糊\n\n${assembled.events.similar.join("\n\n")}`);
|
||||
}
|
||||
// 4. 远期片段 → 更早以前
|
||||
if (assembled.orphans.lines.length) {
|
||||
sections.push(`[更早以前] 记忆里残留的老画面\n${assembled.orphans.lines.join("\n")}`);
|
||||
}
|
||||
// 5. 待整理 → 刚发生的
|
||||
if (assembled.recentOrphans.lines.length) {
|
||||
sections.push(`[刚发生的] 还没来得及想明白\n${assembled.recentOrphans.lines.join("\n")}`);
|
||||
}
|
||||
// 6. 人物弧光 → 这些人
|
||||
if (assembled.arcs.lines.length) {
|
||||
sections.push(`[这些人] 他们现在怎样了\n${assembled.arcs.lines.join("\n")}`);
|
||||
}
|
||||
// 按注入顺序拼接 sections
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
const sections = [];
|
||||
// 1. 世界约束 → 定了的事
|
||||
if (assembled.facts.lines.length) {
|
||||
sections.push(`[定了的事] 已确立的事实\n${assembled.facts.lines.join("\n")}`);
|
||||
}
|
||||
// 2. 核心经历 → 印象深的事
|
||||
if (assembled.events.direct.length) {
|
||||
sections.push(`[印象深的事] 记得很清楚\n\n${assembled.events.direct.join("\n\n")}`);
|
||||
}
|
||||
// 3. 过往背景 → 好像有关的事
|
||||
if (assembled.events.similar.length) {
|
||||
sections.push(`[好像有关的事] 听说过或有点模糊\n\n${assembled.events.similar.join("\n\n")}`);
|
||||
}
|
||||
// 4. 远期片段 → 更早以前
|
||||
if (assembled.orphans.lines.length) {
|
||||
sections.push(`[更早以前] 记忆里残留的老画面\n${assembled.orphans.lines.join("\n")}`);
|
||||
}
|
||||
// 5. 待整理 → 刚发生的
|
||||
if (assembled.recentOrphans.lines.length) {
|
||||
sections.push(`[刚发生的] 还没来得及想明白\n${assembled.recentOrphans.lines.join("\n")}`);
|
||||
}
|
||||
// 6. 人物弧光 → 这些人
|
||||
if (assembled.arcs.lines.length) {
|
||||
sections.push(`[这些人] 他们现在怎样了\n${assembled.arcs.lines.join("\n")}`);
|
||||
}
|
||||
|
||||
if (!sections.length) {
|
||||
if (!sections.length) {
|
||||
return { promptText: "", injectionLogText: "", injectionStats };
|
||||
}
|
||||
|
||||
@@ -846,3 +1031,4 @@ export async function buildVectorPromptText(excludeLastAi = false, hooks = {}) {
|
||||
|
||||
return { text: finalText, logText: (recallResult.logText || "") + (injectionLogText || "") };
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user