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