Improve story summary assembly and UI
This commit is contained in:
@@ -31,9 +31,12 @@ export function getKeepVisibleCount() {
|
|||||||
return store?.keepVisibleCount ?? 3;
|
return store?.keepVisibleCount ?? 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function calcHideRange(lastSummarized) {
|
// boundary:隐藏边界(由调用方决定语义:LLM总结边界 or 向量边界)
|
||||||
|
export function calcHideRange(boundary) {
|
||||||
|
if (boundary == null || boundary < 0) return null;
|
||||||
|
|
||||||
const keepCount = getKeepVisibleCount();
|
const keepCount = getKeepVisibleCount();
|
||||||
const hideEnd = lastSummarized - keepCount;
|
const hideEnd = boundary - keepCount;
|
||||||
if (hideEnd < 0) return null;
|
if (hideEnd < 0) return null;
|
||||||
return { start: 0, end: hideEnd };
|
return { start: 0, end: hideEnd };
|
||||||
}
|
}
|
||||||
@@ -285,4 +288,4 @@ export async function clearSummaryData(chatId) {
|
|||||||
export function getWorldSnapshot() {
|
export function getWorldSnapshot() {
|
||||||
const store = getSummaryStore();
|
const store = getSummaryStore();
|
||||||
return store?.json?.world || [];
|
return store?.json?.world || [];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -209,6 +209,7 @@ Before generating, observe the USER and analyze carefully:
|
|||||||
- cleared: true 表示该条目已失效需删除(不填 content)
|
- cleared: true 表示该条目已失效需删除(不填 content)
|
||||||
- status/knowledge/relation 的 topic 必须包含「::」分隔符
|
- status/knowledge/relation 的 topic 必须包含「::」分隔符
|
||||||
- 硬约束才记录,避免叙事化,确保少、硬、稳定、可覆盖
|
- 硬约束才记录,避免叙事化,确保少、硬、稳定、可覆盖
|
||||||
|
- 动态清理:若发现已有条目中存在不适合作为硬约束的内容(如衣着打扮、临时情绪、琐碎动作),本次输出中用 cleared: true 删除
|
||||||
|
|
||||||
## CRITICAL NOTES
|
## CRITICAL NOTES
|
||||||
- events.id 从 evt-{nextEventId} 开始编号
|
- events.id 从 evt-{nextEventId} 开始编号
|
||||||
@@ -220,7 +221,7 @@ Before generating, observe the USER and analyze carefully:
|
|||||||
- 必须引用“已存在事件”或“本次新输出事件”(允许引用本次 JSON 内较早出现的事件)
|
- 必须引用“已存在事件”或“本次新输出事件”(允许引用本次 JSON 内较早出现的事件)
|
||||||
- worldUpdate 可为空数组
|
- worldUpdate 可为空数组
|
||||||
- 合法JSON,字符串值内部避免英文双引号
|
- 合法JSON,字符串值内部避免英文双引号
|
||||||
- 用小说家的细腻笔触记录,带烟火气
|
- 用朴实、白描、有烟火气的笔触记录,避免比喻和意象
|
||||||
</meta_protocol>`,
|
</meta_protocol>`,
|
||||||
|
|
||||||
assistantCheck: `Content review initiated...
|
assistantCheck: `Content review initiated...
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import { xbLog } from "../../../core/debug-core.js";
|
|||||||
import { getSummaryStore } from "../data/store.js";
|
import { getSummaryStore } from "../data/store.js";
|
||||||
import { getVectorConfig, getSummaryPanelConfig, getSettings } from "../data/config.js";
|
import { getVectorConfig, getSummaryPanelConfig, getSettings } from "../data/config.js";
|
||||||
import { recallMemory, buildQueryText } from "../vector/recall.js";
|
import { recallMemory, buildQueryText } from "../vector/recall.js";
|
||||||
import { getChunksByFloors, getAllChunkVectors, getAllEventVectors } from "../vector/chunk-store.js";
|
import { getChunksByFloors, getAllChunkVectors, getAllEventVectors, getMeta } from "../vector/chunk-store.js";
|
||||||
|
|
||||||
const MODULE_ID = "summaryPrompt";
|
const MODULE_ID = "summaryPrompt";
|
||||||
const SUMMARY_PROMPT_KEY = "LittleWhiteBox_StorySummary";
|
const SUMMARY_PROMPT_KEY = "LittleWhiteBox_StorySummary";
|
||||||
@@ -39,9 +39,11 @@ function canNotifyRecallFail() {
|
|||||||
// 预算常量(向量模式使用)
|
// 预算常量(向量模式使用)
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
const BUDGET = { total: 10000 };
|
const MAIN_BUDGET_MAX = 10000; // 主装配预算(世界/事件/远期/弧光)
|
||||||
const L3_MAX = Math.floor(BUDGET.total * 0.20); // 2000
|
const RECENT_ORPHAN_MAX = 5000; // [待整理] 独立预算
|
||||||
const ARCS_MAX = Math.floor(BUDGET.total * 0.15); // 1500
|
const TOTAL_BUDGET_MAX = 15000; // 总预算(用于日志显示)
|
||||||
|
const L3_MAX = 2000;
|
||||||
|
const ARCS_MAX = 1500;
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
// 工具函数
|
// 工具函数
|
||||||
@@ -173,7 +175,7 @@ function formatCausalEventLine(causalItem, causalById) {
|
|||||||
// 装配日志(开发调试用)
|
// 装配日志(开发调试用)
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function formatInjectionLog(stats, details) {
|
function formatInjectionLog(stats, details, recentOrphanStats = null) {
|
||||||
const pct = (n, d) => (d > 0 ? Math.round((n / d) * 100) : 0);
|
const pct = (n, d) => (d > 0 ? Math.round((n / d) * 100) : 0);
|
||||||
|
|
||||||
const lines = [
|
const lines = [
|
||||||
@@ -190,45 +192,52 @@ function formatInjectionLog(stats, details) {
|
|||||||
|
|
||||||
// 世界状态
|
// 世界状态
|
||||||
lines.push("┌─────────────────────────────────────────────────────────────┐");
|
lines.push("┌─────────────────────────────────────────────────────────────┐");
|
||||||
lines.push("│ [1] 世界状态 (上限 20% = 2000) │");
|
lines.push("│ [1] 世界约束 (上限 2000) │");
|
||||||
lines.push("└─────────────────────────────────────────────────────────────┘");
|
lines.push("└─────────────────────────────────────────────────────────────┘");
|
||||||
lines.push(` 注入: ${stats.world.count} 条 | ${stats.world.tokens} tokens`);
|
lines.push(` 注入: ${stats.world.count} 条 | ${stats.world.tokens} tokens`);
|
||||||
lines.push("");
|
lines.push("");
|
||||||
|
|
||||||
// 事件
|
// 核心经历 + 过往背景
|
||||||
lines.push("┌─────────────────────────────────────────────────────────────┐");
|
lines.push("┌─────────────────────────────────────────────────────────────┐");
|
||||||
lines.push("│ [2] 事件(含证据) │");
|
lines.push("│ [2] 核心经历 + 过往背景(含证据) │");
|
||||||
lines.push("└─────────────────────────────────────────────────────────────┘");
|
lines.push("└─────────────────────────────────────────────────────────────┘");
|
||||||
lines.push(` 选入: ${stats.events.selected} 条 | 事件本体: ${stats.events.tokens} tokens`);
|
lines.push(` 选入: ${stats.events.selected} 条 | 事件本体: ${stats.events.tokens} tokens`);
|
||||||
lines.push(` 挂载证据: ${stats.evidence.attached} 条 | 证据: ${stats.evidence.tokens} tokens`);
|
lines.push(` 挂载证据: ${stats.evidence.attached} 条 | 证据: ${stats.evidence.tokens} tokens`);
|
||||||
lines.push(` DIRECT: ${details.directCount || 0} | SIMILAR: ${details.similarCount || 0}`);
|
lines.push(` 核心: ${details.directCount || 0} | 过往: ${details.similarCount || 0}`);
|
||||||
if (details.eventList?.length) {
|
if (details.eventList?.length) {
|
||||||
lines.push(" ────────────────────────────────────────");
|
lines.push(" ────────────────────────────────────────");
|
||||||
details.eventList.slice(0, 20).forEach((ev, i) => {
|
details.eventList.slice(0, 20).forEach((ev, i) => {
|
||||||
const type = ev.isDirect ? "D" : "S";
|
const type = ev.isDirect ? "核心" : "过往";
|
||||||
const hasE = ev.hasEvidence ? " +E" : "";
|
const hasE = ev.hasEvidence ? " +E" : "";
|
||||||
const title = (ev.title || "(无标题)").slice(0, 32);
|
const title = (ev.title || "(无标题)").slice(0, 32);
|
||||||
lines.push(` ${String(i + 1).padStart(2)}. [${type}${hasE}] ${title} (${ev.tokens} tok, sim=${ev.similarity.toFixed(3)})`);
|
lines.push(` ${String(i + 1).padStart(2)}. [${type}${hasE}] ${title} (${ev.tokens}tok)`);
|
||||||
});
|
});
|
||||||
if (details.eventList.length > 20) lines.push(` ... 还有 ${details.eventList.length - 20} 条`);
|
if (details.eventList.length > 20) lines.push(` ... 还有 ${details.eventList.length - 20} 条`);
|
||||||
}
|
}
|
||||||
lines.push("");
|
lines.push("");
|
||||||
|
|
||||||
// 碎片
|
// 远期片段
|
||||||
lines.push("┌─────────────────────────────────────────────────────────────┐");
|
lines.push("┌─────────────────────────────────────────────────────────────┐");
|
||||||
lines.push("│ [3] 记忆碎片(按楼层从早到晚) │");
|
lines.push("│ [3] 远期片段(已总结范围) │");
|
||||||
lines.push("└─────────────────────────────────────────────────────────────┘");
|
lines.push("└─────────────────────────────────────────────────────────────┘");
|
||||||
lines.push(` 注入: ${stats.orphans.injected} 条 | ${stats.orphans.tokens} tokens`);
|
lines.push(` 注入: ${stats.orphans.injected} 条 | ${stats.orphans.tokens} tokens`);
|
||||||
lines.push("");
|
lines.push("");
|
||||||
|
|
||||||
// 弧光
|
// 待整理
|
||||||
lines.push("┌─────────────────────────────────────────────────────────────┐");
|
lines.push("┌─────────────────────────────────────────────────────────────┐");
|
||||||
lines.push("│ [4] 人物弧光(上限 15% = 1500,放在最底) │");
|
lines.push("│ [4] 待整理(未总结范围,独立预算 5000) │");
|
||||||
|
lines.push("└─────────────────────────────────────────────────────────────┘");
|
||||||
|
lines.push(` 注入: ${recentOrphanStats?.injected || 0} 条 | ${recentOrphanStats?.tokens || 0} tokens`);
|
||||||
|
lines.push(` 楼层范围: ${recentOrphanStats?.floorRange || "N/A"}`);
|
||||||
|
lines.push("");
|
||||||
|
|
||||||
|
lines.push("┌─────────────────────────────────────────────────────────────┐");
|
||||||
|
lines.push("│ [5] 人物弧光(上限 1500) │");
|
||||||
lines.push("└─────────────────────────────────────────────────────────────┘");
|
lines.push("└─────────────────────────────────────────────────────────────┘");
|
||||||
lines.push(` 注入: ${stats.arcs.count} 条 | ${stats.arcs.tokens} tokens`);
|
lines.push(` 注入: ${stats.arcs.count} 条 | ${stats.arcs.tokens} tokens`);
|
||||||
lines.push("");
|
lines.push("");
|
||||||
|
|
||||||
// 预算条形
|
// 预算条形图
|
||||||
lines.push("┌─────────────────────────────────────────────────────────────┐");
|
lines.push("┌─────────────────────────────────────────────────────────────┐");
|
||||||
lines.push("│ 【预算分布】 │");
|
lines.push("│ 【预算分布】 │");
|
||||||
lines.push("└─────────────────────────────────────────────────────────────┘");
|
lines.push("└─────────────────────────────────────────────────────────────┘");
|
||||||
@@ -238,10 +247,11 @@ function formatInjectionLog(stats, details) {
|
|||||||
const pctStr = pct(tokens, total) + "%";
|
const pctStr = pct(tokens, total) + "%";
|
||||||
return ` ${label.padEnd(6)} ${"█".repeat(width).padEnd(40)} ${String(tokens).padStart(5)} (${pctStr})`;
|
return ` ${label.padEnd(6)} ${"█".repeat(width).padEnd(40)} ${String(tokens).padStart(5)} (${pctStr})`;
|
||||||
};
|
};
|
||||||
lines.push(bar(stats.world.tokens, "世界"));
|
lines.push(bar(stats.world.tokens, "约束"));
|
||||||
lines.push(bar(stats.events.tokens, "事件"));
|
lines.push(bar(stats.events.tokens, "经历"));
|
||||||
lines.push(bar(stats.evidence.tokens, "证据"));
|
lines.push(bar(stats.evidence.tokens, "证据"));
|
||||||
lines.push(bar(stats.orphans.tokens, "碎片"));
|
lines.push(bar(stats.orphans.tokens, "远期"));
|
||||||
|
lines.push(bar(recentOrphanStats?.tokens || 0, "待整理"));
|
||||||
lines.push(bar(stats.arcs.tokens, "弧光"));
|
lines.push(bar(stats.arcs.tokens, "弧光"));
|
||||||
lines.push(bar(stats.budget.max - stats.budget.used, "剩余"));
|
lines.push(bar(stats.budget.max - stats.budget.used, "剩余"));
|
||||||
lines.push("");
|
lines.push("");
|
||||||
@@ -260,7 +270,7 @@ function buildNonVectorPrompt(store) {
|
|||||||
|
|
||||||
if (data.world?.length) {
|
if (data.world?.length) {
|
||||||
const lines = formatWorldLines(data.world);
|
const lines = formatWorldLines(data.world);
|
||||||
sections.push(`[世界状态] 请严格遵守\n${lines.join("\n")}`);
|
sections.push(`[世界约束] 规则手册,请严格遵守\n${lines.join("\n")}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.events?.length) {
|
if (data.events?.length) {
|
||||||
@@ -334,13 +344,24 @@ export async function injectNonVectorPrompt(postToFrame = null) {
|
|||||||
// 向量模式:预算装配(世界 → 事件(带证据) → 碎片 → 弧光)
|
// 向量模式:预算装配(世界 → 事件(带证据) → 碎片 → 弧光)
|
||||||
// ─────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
async function buildVectorPrompt(store, recallResult, causalById, queryEntities = []) {
|
async function buildVectorPrompt(store, recallResult, causalById, queryEntities = [], meta = null) {
|
||||||
const data = store.json || {};
|
const data = store.json || {};
|
||||||
const total = { used: 0, max: BUDGET.total };
|
const total = { used: 0, max: MAIN_BUDGET_MAX };
|
||||||
const sections = [];
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
// 预装配各层内容(先计算预算,后按顺序拼接)
|
||||||
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
const assembled = {
|
||||||
|
world: { lines: [], tokens: 0 },
|
||||||
|
arcs: { lines: [], tokens: 0 },
|
||||||
|
events: { direct: [], similar: [] },
|
||||||
|
orphans: { lines: [], tokens: 0 },
|
||||||
|
recentOrphans: { lines: [], tokens: 0 },
|
||||||
|
};
|
||||||
|
|
||||||
const injectionStats = {
|
const injectionStats = {
|
||||||
budget: { max: BUDGET.total, used: 0 },
|
budget: { max: TOTAL_BUDGET_MAX, used: 0 },
|
||||||
world: { count: 0, tokens: 0 },
|
world: { count: 0, tokens: 0 },
|
||||||
arcs: { count: 0, tokens: 0 },
|
arcs: { count: 0, tokens: 0 },
|
||||||
events: { selected: 0, tokens: 0 },
|
events: { selected: 0, tokens: 0 },
|
||||||
@@ -348,29 +369,67 @@ async function buildVectorPrompt(store, recallResult, causalById, queryEntities
|
|||||||
orphans: { injected: 0, tokens: 0 },
|
orphans: { injected: 0, tokens: 0 },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const recentOrphanStats = {
|
||||||
|
injected: 0,
|
||||||
|
tokens: 0,
|
||||||
|
floorRange: "N/A",
|
||||||
|
};
|
||||||
const details = {
|
const details = {
|
||||||
eventList: [],
|
eventList: [],
|
||||||
directCount: 0,
|
directCount: 0,
|
||||||
similarCount: 0,
|
similarCount: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
// [世界状态](20%)
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
// [优先级 1] 世界约束 - 最高优先级
|
||||||
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
const worldLines = formatWorldLines(data.world);
|
const worldLines = formatWorldLines(data.world);
|
||||||
if (worldLines.length) {
|
if (worldLines.length) {
|
||||||
const l3 = { used: 0, max: Math.min(L3_MAX, total.max - total.used) };
|
const l3Budget = { used: 0, max: Math.min(L3_MAX, total.max - total.used) };
|
||||||
const lines = [];
|
|
||||||
for (const line of worldLines) {
|
for (const line of worldLines) {
|
||||||
if (!pushWithBudget(lines, line, l3)) break;
|
if (!pushWithBudget(assembled.world.lines, line, l3Budget)) break;
|
||||||
}
|
}
|
||||||
if (lines.length) {
|
assembled.world.tokens = l3Budget.used;
|
||||||
sections.push(`[世界状态] 请严格遵守\n${lines.join("\n")}`);
|
total.used += l3Budget.used;
|
||||||
total.used += l3.used;
|
injectionStats.world.count = assembled.world.lines.length;
|
||||||
injectionStats.world.count = lines.length;
|
injectionStats.world.tokens = l3Budget.used;
|
||||||
injectionStats.world.tokens = l3.used;
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
// [优先级 2] 人物弧光 - 预留预算(稍后再拼接到末尾)
|
||||||
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
if (data.arcs?.length && total.used < total.max) {
|
||||||
|
const { name1 } = getContext();
|
||||||
|
const userName = String(name1 || "").trim();
|
||||||
|
|
||||||
|
const relevant = new Set(
|
||||||
|
[userName, ...(queryEntities || [])]
|
||||||
|
.map(s => String(s || "").trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
);
|
||||||
|
|
||||||
|
const filtered = (data.arcs || []).filter(a => {
|
||||||
|
const n = String(a?.name || "").trim();
|
||||||
|
return n && relevant.has(n);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (filtered.length) {
|
||||||
|
const arcBudget = { used: 0, max: Math.min(ARCS_MAX, total.max - total.used) };
|
||||||
|
for (const a of filtered) {
|
||||||
|
const line = formatArcLine(a);
|
||||||
|
if (!pushWithBudget(assembled.arcs.lines, line, arcBudget)) break;
|
||||||
|
}
|
||||||
|
assembled.arcs.tokens = arcBudget.used;
|
||||||
|
total.used += arcBudget.used;
|
||||||
|
injectionStats.arcs.count = assembled.arcs.lines.length;
|
||||||
|
injectionStats.arcs.tokens = arcBudget.used;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 事件(含证据)
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
// [优先级 3] 事件 + 证据
|
||||||
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
const recalledEvents = (recallResult?.events || []).filter(e => e?.event?.summary);
|
const recalledEvents = (recallResult?.events || []).filter(e => e?.event?.summary);
|
||||||
const chunks = recallResult?.chunks || [];
|
const chunks = recallResult?.chunks || [];
|
||||||
const usedChunkIds = new Set();
|
const usedChunkIds = new Set();
|
||||||
@@ -483,70 +542,104 @@ async function buildVectorPrompt(store, recallResult, causalById, queryEntities
|
|||||||
|
|
||||||
details.directCount = selectedDirectTexts.length;
|
details.directCount = selectedDirectTexts.length;
|
||||||
details.similarCount = selectedSimilarTexts.length;
|
details.similarCount = selectedSimilarTexts.length;
|
||||||
|
assembled.events.direct = selectedDirectTexts;
|
||||||
|
assembled.events.similar = selectedSimilarTexts;
|
||||||
|
|
||||||
if (selectedDirectTexts.length) {
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
sections.push(`[亲身经历]\n\n${selectedDirectTexts.join("\n\n")}`);
|
// [优先级 4] 远期片段(已总结范围的 orphan chunks)
|
||||||
}
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
if (selectedSimilarTexts.length) {
|
const lastSummarized = store.lastSummarizedMesId ?? -1;
|
||||||
sections.push(`[相关背景]\n\n${selectedSimilarTexts.join("\n\n")}`);
|
const lastChunkFloor = meta?.lastChunkFloor ?? -1;
|
||||||
}
|
const keepVisible = store.keepVisibleCount ?? 3;
|
||||||
|
|
||||||
// [记忆碎片]:orphans 按楼层从早到晚,完整 chunk
|
|
||||||
if (chunks.length && total.used < total.max) {
|
if (chunks.length && total.used < total.max) {
|
||||||
const orphans = chunks
|
const orphans = chunks
|
||||||
.filter(c => !usedChunkIds.has(c.chunkId))
|
.filter(c => !usedChunkIds.has(c.chunkId))
|
||||||
|
.filter(c => c.floor <= lastSummarized)
|
||||||
.sort((a, b) => (a.floor - b.floor) || ((a.chunkIdx ?? 0) - (b.chunkIdx ?? 0)));
|
.sort((a, b) => (a.floor - b.floor) || ((a.chunkIdx ?? 0) - (b.chunkIdx ?? 0)));
|
||||||
|
|
||||||
const l1 = { used: 0, max: total.max - total.used };
|
const l1Budget = { used: 0, max: total.max - total.used };
|
||||||
const lines = [];
|
|
||||||
|
|
||||||
for (const c of orphans) {
|
for (const c of orphans) {
|
||||||
const line = formatChunkFullLine(c);
|
const line = formatChunkFullLine(c);
|
||||||
if (!pushWithBudget(lines, line, l1)) break;
|
if (!pushWithBudget(assembled.orphans.lines, line, l1Budget)) break;
|
||||||
injectionStats.orphans.injected++;
|
injectionStats.orphans.injected++;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lines.length) {
|
assembled.orphans.tokens = l1Budget.used;
|
||||||
sections.push(`[记忆碎片]\n${lines.join("\n")}`);
|
total.used += l1Budget.used;
|
||||||
total.used += l1.used;
|
injectionStats.orphans.tokens = l1Budget.used;
|
||||||
injectionStats.orphans.tokens = l1.used;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// [人物弧光]:放最底,且上限 15%(只保留 user + queryEntities)
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
if (data.arcs?.length && total.used < total.max) {
|
// [独立预算] 待整理(未总结范围,独立 5000)
|
||||||
const { name1 } = getContext();
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
const userName = String(name1 || "").trim();
|
|
||||||
|
|
||||||
const relevant = new Set(
|
// 近期范围:(lastSummarized, lastChunkFloor - keepVisible]
|
||||||
[userName, ...(queryEntities || [])]
|
const recentStart = lastSummarized + 1;
|
||||||
.map(s => String(s || "").trim())
|
const recentEnd = lastChunkFloor - keepVisible;
|
||||||
.filter(Boolean)
|
|
||||||
);
|
|
||||||
|
|
||||||
const filtered = (data.arcs || []).filter(a => {
|
if (chunks.length && recentEnd >= recentStart) {
|
||||||
const n = String(a?.name || "").trim();
|
const recentOrphans = chunks
|
||||||
return n && relevant.has(n);
|
.filter(c => !usedChunkIds.has(c.chunkId))
|
||||||
});
|
.filter(c => c.floor >= recentStart && c.floor <= recentEnd)
|
||||||
|
.sort((a, b) => (a.floor - b.floor) || ((a.chunkIdx ?? 0) - (b.chunkIdx ?? 0)));
|
||||||
|
|
||||||
if (filtered.length) {
|
const recentBudget = { used: 0, max: RECENT_ORPHAN_MAX };
|
||||||
const arcBudget = { used: 0, max: Math.min(ARCS_MAX, total.max - total.used) };
|
|
||||||
const arcLines = [];
|
|
||||||
for (const a of filtered) {
|
|
||||||
const line = formatArcLine(a);
|
|
||||||
if (!pushWithBudget(arcLines, line, arcBudget)) break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (arcLines.length) {
|
for (const c of recentOrphans) {
|
||||||
sections.push(`[人物弧光]\n${arcLines.join("\n")}`);
|
const line = formatChunkFullLine(c);
|
||||||
total.used += arcBudget.used;
|
if (!pushWithBudget(assembled.recentOrphans.lines, line, recentBudget)) break;
|
||||||
injectionStats.arcs.count = arcLines.length;
|
recentOrphanStats.injected++;
|
||||||
injectionStats.arcs.tokens = arcBudget.used;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
assembled.recentOrphans.tokens = recentBudget.used;
|
||||||
|
recentOrphanStats.tokens = recentBudget.used;
|
||||||
|
recentOrphanStats.floorRange = `${recentStart + 1}~${recentEnd + 1}楼`;
|
||||||
}
|
}
|
||||||
|
|
||||||
injectionStats.budget.used = total.used;
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
// 按注入顺序拼接 sections
|
||||||
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
const sections = [];
|
||||||
|
|
||||||
|
// 1. 世界约束
|
||||||
|
if (assembled.world.lines.length) {
|
||||||
|
sections.push(`[世界约束] 规则手册,请严格遵守\n${assembled.world.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. 人物弧光(最后注入,但预算已在优先级 2 预留)
|
||||||
|
if (assembled.arcs.lines.length) {
|
||||||
|
sections.push(`[人物弧光]\n${assembled.arcs.lines.join("\n")}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
// 统计 & 返回
|
||||||
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// 总预算 = 主装配 + 待整理
|
||||||
|
injectionStats.budget.used = total.used + (recentOrphanStats.tokens || 0);
|
||||||
|
|
||||||
if (!sections.length) {
|
if (!sections.length) {
|
||||||
return { promptText: "", injectionLogText: "", injectionStats };
|
return { promptText: "", injectionLogText: "", injectionStats };
|
||||||
@@ -557,7 +650,7 @@ async function buildVectorPrompt(store, recallResult, causalById, queryEntities
|
|||||||
`<剧情记忆>\n\n${sections.join("\n\n")}\n\n</剧情记忆>\n` +
|
`<剧情记忆>\n\n${sections.join("\n\n")}\n\n</剧情记忆>\n` +
|
||||||
`${buildPostscript()}`;
|
`${buildPostscript()}`;
|
||||||
|
|
||||||
const injectionLogText = formatInjectionLog(injectionStats, details);
|
const injectionLogText = formatInjectionLog(injectionStats, details, recentOrphanStats);
|
||||||
|
|
||||||
return { promptText, injectionLogText, injectionStats };
|
return { promptText, injectionLogText, injectionStats };
|
||||||
}
|
}
|
||||||
@@ -643,6 +736,10 @@ export async function recallAndInjectPrompt(excludeLastAi = false, hooks = {}) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { chatId } = getContext();
|
||||||
|
// meta 用于 lastChunkFloor(供 buildVectorPrompt 分桶)
|
||||||
|
const meta = chatId ? await getMeta(chatId) : null;
|
||||||
|
|
||||||
let recallResult = null;
|
let recallResult = null;
|
||||||
let causalById = new Map();
|
let causalById = new Map();
|
||||||
|
|
||||||
@@ -743,7 +840,8 @@ export async function recallAndInjectPrompt(excludeLastAi = false, hooks = {}) {
|
|||||||
store,
|
store,
|
||||||
recallResult,
|
recallResult,
|
||||||
causalById,
|
causalById,
|
||||||
recallResult?.queryEntities || []
|
recallResult?.queryEntities || [],
|
||||||
|
meta
|
||||||
);
|
);
|
||||||
|
|
||||||
// 写入 extension_prompts(真正注入)
|
// 写入 extension_prompts(真正注入)
|
||||||
|
|||||||
@@ -178,7 +178,10 @@ h1 span {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#keep-visible-count {
|
#keep-visible-count {
|
||||||
width: 32px;
|
width: 3ch; /* 可稳定显示 3 位数字:0-50 足够 */
|
||||||
|
min-width: 3ch;
|
||||||
|
max-width: 4ch;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
padding: 2px 4px;
|
padding: 2px 4px;
|
||||||
margin: 0 2px;
|
margin: 0 2px;
|
||||||
background: var(--bg2);
|
background: var(--bg2);
|
||||||
|
|||||||
@@ -92,6 +92,9 @@ let eventsRegistered = false;
|
|||||||
let vectorGenerating = false;
|
let vectorGenerating = false;
|
||||||
let vectorCancelled = false;
|
let vectorCancelled = false;
|
||||||
|
|
||||||
|
let hideApplyTimer = null;
|
||||||
|
const HIDE_APPLY_DEBOUNCE_MS = 250;
|
||||||
|
|
||||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
@@ -583,11 +586,12 @@ async function sendSavedConfigToFrame() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendFrameBaseData(store, totalFloors) {
|
async function sendFrameBaseData(store, totalFloors) {
|
||||||
const lastSummarized = store?.lastSummarizedMesId ?? -1;
|
const boundary = await getHideBoundaryFloor(store);
|
||||||
const range = calcHideRange(lastSummarized);
|
const range = calcHideRange(boundary);
|
||||||
const hiddenCount = range ? range.end + 1 : 0;
|
const hiddenCount = (store?.hideSummarizedHistory && range) ? (range.end + 1) : 0;
|
||||||
|
|
||||||
|
const lastSummarized = store?.lastSummarizedMesId ?? -1;
|
||||||
postToFrame({
|
postToFrame({
|
||||||
type: "SUMMARY_BASE_DATA",
|
type: "SUMMARY_BASE_DATA",
|
||||||
stats: {
|
stats: {
|
||||||
@@ -629,7 +633,7 @@ function openPanelForMessage(mesId) {
|
|||||||
const store = getSummaryStore();
|
const store = getSummaryStore();
|
||||||
const totalFloors = chat.length;
|
const totalFloors = chat.length;
|
||||||
|
|
||||||
sendFrameBaseData(store, totalFloors);
|
sendFrameBaseData(store, totalFloors); // 不需要 await,fire-and-forget
|
||||||
sendFrameFullData(store, totalFloors);
|
sendFrameFullData(store, totalFloors);
|
||||||
setSummaryGenerating(summaryGenerating);
|
setSummaryGenerating(summaryGenerating);
|
||||||
|
|
||||||
@@ -641,6 +645,53 @@ function openPanelForMessage(mesId) {
|
|||||||
sendLocalModelStatusToFrame(modelId);
|
sendLocalModelStatusToFrame(modelId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
// Hide/Unhide:向量模式联动("已总结"的定义切换)
|
||||||
|
// - 非向量:boundary = lastSummarizedMesId(LLM总结边界)
|
||||||
|
// - 向量:boundary = meta.lastChunkFloor(已向量化)
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
async function getHideBoundaryFloor(store) {
|
||||||
|
const vectorCfg = getVectorConfig();
|
||||||
|
if (!vectorCfg?.enabled) {
|
||||||
|
return store?.lastSummarizedMesId ?? -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { chatId } = getContext();
|
||||||
|
if (!chatId) return -1;
|
||||||
|
|
||||||
|
const meta = await getMeta(chatId);
|
||||||
|
return meta?.lastChunkFloor ?? -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function applyHideState() {
|
||||||
|
const store = getSummaryStore();
|
||||||
|
if (!store?.hideSummarizedHistory) return;
|
||||||
|
|
||||||
|
const boundary = await getHideBoundaryFloor(store);
|
||||||
|
if (boundary < 0) return;
|
||||||
|
|
||||||
|
const range = calcHideRange(boundary);
|
||||||
|
if (!range) return;
|
||||||
|
|
||||||
|
await executeSlashCommand(`/hide ${range.start}-${range.end}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyHideStateDebounced() {
|
||||||
|
clearTimeout(hideApplyTimer);
|
||||||
|
hideApplyTimer = setTimeout(() => {
|
||||||
|
applyHideState().catch((e) => xbLog.warn(MODULE_ID, "applyHideState failed", e));
|
||||||
|
}, HIDE_APPLY_DEBOUNCE_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function clearHideState() {
|
||||||
|
const store = getSummaryStore();
|
||||||
|
const boundary = await getHideBoundaryFloor(store);
|
||||||
|
if (boundary < 0) return;
|
||||||
|
|
||||||
|
await executeSlashCommand(`/unhide 0-${boundary}`);
|
||||||
|
}
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
// 自动总结(保持原逻辑;不做 prompt 注入)
|
// 自动总结(保持原逻辑;不做 prompt 注入)
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
@@ -849,18 +900,17 @@ function handleFrameMessage(event) {
|
|||||||
case "TOGGLE_HIDE_SUMMARIZED": {
|
case "TOGGLE_HIDE_SUMMARIZED": {
|
||||||
const store = getSummaryStore();
|
const store = getSummaryStore();
|
||||||
if (!store) break;
|
if (!store) break;
|
||||||
const lastSummarized = store.lastSummarizedMesId ?? -1;
|
|
||||||
if (lastSummarized < 0) break;
|
|
||||||
|
|
||||||
store.hideSummarizedHistory = !!data.enabled;
|
store.hideSummarizedHistory = !!data.enabled;
|
||||||
saveSummaryStore();
|
saveSummaryStore();
|
||||||
|
|
||||||
if (data.enabled) {
|
(async () => {
|
||||||
const range = calcHideRange(lastSummarized);
|
if (data.enabled) {
|
||||||
if (range) executeSlashCommand(`/hide ${range.start}-${range.end}`);
|
await applyHideState();
|
||||||
} else {
|
} else {
|
||||||
executeSlashCommand(`/unhide 0-${lastSummarized}`);
|
await clearHideState();
|
||||||
}
|
}
|
||||||
|
})();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -876,20 +926,15 @@ function handleFrameMessage(event) {
|
|||||||
store.keepVisibleCount = newCount;
|
store.keepVisibleCount = newCount;
|
||||||
saveSummaryStore();
|
saveSummaryStore();
|
||||||
|
|
||||||
const lastSummarized = store.lastSummarizedMesId ?? -1;
|
(async () => {
|
||||||
|
// 先清掉原隐藏,再按新 keepCount 重算隐藏
|
||||||
if (store.hideSummarizedHistory && lastSummarized >= 0) {
|
if (store.hideSummarizedHistory) {
|
||||||
(async () => {
|
await clearHideState();
|
||||||
await executeSlashCommand(`/unhide 0-${lastSummarized}`);
|
await applyHideState();
|
||||||
const range = calcHideRange(lastSummarized);
|
}
|
||||||
if (range) await executeSlashCommand(`/hide ${range.start}-${range.end}`);
|
|
||||||
const { chat } = getContext();
|
|
||||||
sendFrameBaseData(store, Array.isArray(chat) ? chat.length : 0);
|
|
||||||
})();
|
|
||||||
} else {
|
|
||||||
const { chat } = getContext();
|
const { chat } = getContext();
|
||||||
sendFrameBaseData(store, Array.isArray(chat) ? chat.length : 0);
|
await sendFrameBaseData(store, Array.isArray(chat) ? chat.length : 0);
|
||||||
}
|
})();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -924,8 +969,6 @@ async function handleManualGenerate(mesId, config) {
|
|||||||
onStatus: (text) => postToFrame({ type: "SUMMARY_STATUS", statusText: text }),
|
onStatus: (text) => postToFrame({ type: "SUMMARY_STATUS", statusText: text }),
|
||||||
onError: (msg) => postToFrame({ type: "SUMMARY_ERROR", message: msg }),
|
onError: (msg) => postToFrame({ type: "SUMMARY_ERROR", message: msg }),
|
||||||
onComplete: ({ merged, endMesId }) => {
|
onComplete: ({ merged, endMesId }) => {
|
||||||
const store = getSummaryStore();
|
|
||||||
|
|
||||||
postToFrame({
|
postToFrame({
|
||||||
type: "SUMMARY_FULL_DATA",
|
type: "SUMMARY_FULL_DATA",
|
||||||
payload: {
|
payload: {
|
||||||
@@ -943,12 +986,8 @@ async function handleManualGenerate(mesId, config) {
|
|||||||
statusText: `已更新至 ${endMesId + 1} 楼 · ${merged.events?.length || 0} 个事件`,
|
statusText: `已更新至 ${endMesId + 1} 楼 · ${merged.events?.length || 0} 个事件`,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 隐藏逻辑(与注入无关)
|
// 隐藏逻辑:统一走 boundary(vector on/off 自动切换定义)
|
||||||
const lastSummarized = store?.lastSummarizedMesId ?? -1;
|
applyHideStateDebounced();
|
||||||
if (store?.hideSummarizedHistory && lastSummarized >= 0) {
|
|
||||||
const range = calcHideRange(lastSummarized);
|
|
||||||
if (range) executeSlashCommand(`/hide ${range.start}-${range.end}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
updateFrameStatsAfterSummary(endMesId, merged);
|
updateFrameStatsAfterSummary(endMesId, merged);
|
||||||
},
|
},
|
||||||
@@ -969,15 +1008,10 @@ async function handleChatChanged() {
|
|||||||
initButtonsForAll();
|
initButtonsForAll();
|
||||||
|
|
||||||
const store = getSummaryStore();
|
const store = getSummaryStore();
|
||||||
const lastSummarized = store?.lastSummarizedMesId ?? -1;
|
applyHideStateDebounced();
|
||||||
|
|
||||||
if (lastSummarized >= 0 && store?.hideSummarizedHistory === true) {
|
|
||||||
const range = calcHideRange(lastSummarized);
|
|
||||||
if (range) executeSlashCommand(`/hide ${range.start}-${range.end}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (frameReady) {
|
if (frameReady) {
|
||||||
sendFrameBaseData(store, newLength);
|
await sendFrameBaseData(store, newLength);
|
||||||
sendFrameFullData(store, newLength);
|
sendFrameFullData(store, newLength);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1009,6 +1043,9 @@ async function handleMessageReceived() {
|
|||||||
await syncOnMessageReceived(chatId, lastFloor, message, vectorConfig);
|
await syncOnMessageReceived(chatId, lastFloor, message, vectorConfig);
|
||||||
await maybeAutoBuildChunks();
|
await maybeAutoBuildChunks();
|
||||||
|
|
||||||
|
// 向量模式下,lastChunkFloor 会持续推进;如果勾选隐藏,自动扩展隐藏范围
|
||||||
|
applyHideStateDebounced();
|
||||||
|
|
||||||
setTimeout(() => maybeAutoRunSummary("after_ai"), 1000);
|
setTimeout(() => maybeAutoRunSummary("after_ai"), 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user