From a86aa999c3b7863f3fdd71aeacf321a53777274b Mon Sep 17 00:00:00 2001 From: RT15548 <168917470+RT15548@users.noreply.github.com> Date: Tue, 24 Feb 2026 13:53:18 +0800 Subject: [PATCH] feat(story-summary): default hide summarized to enabled --- modules/story-summary/data/config.js | 13 ++++ modules/story-summary/data/store.js | 8 ++- modules/story-summary/generate/llm.js | 4 +- modules/story-summary/generate/prompt.js | 64 +++++++++++++++++--- modules/story-summary/story-summary-ui.js | 6 +- modules/story-summary/story-summary.js | 74 +++++++++++++++-------- 6 files changed, 133 insertions(+), 36 deletions(-) diff --git a/modules/story-summary/data/config.js b/modules/story-summary/data/config.js index fea6aaf..ce19296 100644 --- a/modules/story-summary/data/config.js +++ b/modules/story-summary/data/config.js @@ -19,6 +19,12 @@ export function getSettings() { } export function getSummaryPanelConfig() { + const clampKeepVisibleCount = (value) => { + const n = Number.parseInt(value, 10); + if (!Number.isFinite(n)) return 6; + return Math.max(0, Math.min(50, n)); + }; + const defaults = { api: { provider: "st", url: "", key: "", model: "", modelCache: [] }, gen: { temperature: null, top_p: null, top_k: null, presence_penalty: null, frequency_penalty: null }, @@ -33,6 +39,10 @@ export function getSummaryPanelConfig() { wrapperTail: "", forceInsertAtEnd: false, }, + ui: { + hideSummarized: true, + keepVisibleCount: 6, + }, textFilterRules: [...DEFAULT_FILTER_RULES], vector: null, }; @@ -52,12 +62,15 @@ export function getSummaryPanelConfig() { api: { ...defaults.api, ...(parsed.api || {}) }, gen: { ...defaults.gen, ...(parsed.gen || {}) }, trigger: { ...defaults.trigger, ...(parsed.trigger || {}) }, + ui: { ...defaults.ui, ...(parsed.ui || {}) }, textFilterRules, vector: parsed.vector || null, }; if (result.trigger.timing === "manual") result.trigger.enabled = false; if (result.trigger.useStream === undefined) result.trigger.useStream = true; + result.ui.hideSummarized = !!result.ui.hideSummarized; + result.ui.keepVisibleCount = clampKeepVisibleCount(result.ui.keepVisibleCount); return result; } catch { diff --git a/modules/story-summary/data/store.js b/modules/story-summary/data/store.js index 0429d49..5f289ed 100644 --- a/modules/story-summary/data/store.js +++ b/modules/story-summary/data/store.js @@ -48,13 +48,15 @@ export function saveSummaryStore() { export function getKeepVisibleCount() { const store = getSummaryStore(); - return store?.keepVisibleCount ?? 3; + return store?.keepVisibleCount ?? 6; } -export function calcHideRange(boundary) { +export function calcHideRange(boundary, keepCountOverride = null) { if (boundary == null || boundary < 0) return null; - const keepCount = getKeepVisibleCount(); + const keepCount = Number.isFinite(keepCountOverride) + ? Math.max(0, Math.min(50, Number(keepCountOverride))) + : getKeepVisibleCount(); const hideEnd = boundary - keepCount; if (hideEnd < 0) return null; return { start: 0, end: hideEnd }; diff --git a/modules/story-summary/generate/llm.js b/modules/story-summary/generate/llm.js index ca84ae2..d55a3ff 100644 --- a/modules/story-summary/generate/llm.js +++ b/modules/story-summary/generate/llm.js @@ -210,11 +210,13 @@ Before generating, observe the USER and analyze carefully: ## CRITICAL NOTES - events.id 从 evt-{nextEventId} 开始编号 - 仅输出【增量】内容,已有事件绝不重复 +- /地点、通过什么方式、对谁、做了什么事、结果如何。如果原文有具体道具(如一把枪、一封信),必须在总结中提及。 - keywords 是全局关键词,综合已有+新增 - causedBy 仅在因果明确时填写,允许为[],0-2个 - factUpdates 可为空数组 - 合法JSON,字符串值内部避免英文双引号 -- 用朴实、白描、有烟火气的笔触记录,避免比喻和意象 +- 用朴实、白描、有烟火气的笔触记录事实,避免比喻和意象 +- 严谨、注重细节,避免使用模糊的概括性语言,应用具体的动词描述动作,例:谁,在什么时间/地点,通过什么方式,对谁,做了什么事,出现了什么道具,结果如何。 `, assistantCheck: `Content review initiated... diff --git a/modules/story-summary/generate/prompt.js b/modules/story-summary/generate/prompt.js index 8ac6d98..fe4c59d 100644 --- a/modules/story-summary/generate/prompt.js +++ b/modules/story-summary/generate/prompt.js @@ -18,6 +18,7 @@ import { getSummaryStore, getFacts, isRelationFact } from "../data/store.js"; import { getVectorConfig, getSummaryPanelConfig, getSettings } from "../data/config.js"; import { recallMemory } from "../vector/retrieval/recall.js"; import { getMeta } from "../vector/storage/chunk-store.js"; +import { getStateAtoms } from "../vector/storage/state-store.js"; import { getEngineFingerprint } from "../vector/utils/embedder.js"; import { buildTrustedCharacters } from "../vector/retrieval/entity-lexicon.js"; @@ -540,6 +541,34 @@ function groupL0ByFloor(l0List) { return map; } +/** + * Get all available L0 atoms in recent window and normalize to evidence shape. + * @param {number} recentStart + * @param {number} recentEnd + * @returns {object[]} + */ +function getRecentWindowL0Atoms(recentStart, recentEnd) { + if (!Number.isFinite(recentStart) || !Number.isFinite(recentEnd) || recentEnd < recentStart) return []; + const atoms = getStateAtoms() || []; + const out = []; + for (const atom of atoms) { + const floor = atom?.floor; + const atomId = atom?.atomId; + const semantic = String(atom?.semantic || '').trim(); + if (!Number.isFinite(floor)) continue; + if (floor < recentStart || floor > recentEnd) continue; + if (!atomId || !semantic) continue; + out.push({ + id: atomId, + floor, + atom, + similarity: 0, + rerankScore: 0, + }); + } + return out; +} + // ───────────────────────────────────────────────────────────────────────────── // EvidenceGroup(per-floor:N个L0 + 共享一对L1) // ───────────────────────────────────────────────────────────────────────────── @@ -585,6 +614,21 @@ function buildEvidenceGroup(floor, l0AtomsForFloor, l1ByFloor) { return { floor, l0Atoms: l0AtomsForFloor, userL1, aiL1, totalTokens }; } +/** + * Build recent-evidence group (L0 only, no L1 attachment). + * @param {number} floor + * @param {object[]} l0AtomsForFloor + * @returns {object} + */ +function buildRecentEvidenceGroup(floor, l0AtomsForFloor) { + let totalTokens = 0; + for (const l0 of l0AtomsForFloor) { + totalTokens += estimateTokens(buildL0DisplayText(l0)); + } + totalTokens += 10; + return { floor, l0Atoms: l0AtomsForFloor, userL1: null, aiL1: null, totalTokens }; +} + /** * 格式化一个证据组为文本行数组 * @@ -1114,7 +1158,11 @@ async function buildVectorPrompt(store, recallResult, causalById, focusCharacter const lastSummarized = store.lastSummarizedMesId ?? -1; const lastChunkFloor = meta?.lastChunkFloor ?? -1; - const keepVisible = store.keepVisibleCount ?? 3; + const uiCfg = getSummaryPanelConfig()?.ui || {}; + const parsedKeepVisible = Number.parseInt(uiCfg.keepVisibleCount, 10); + const keepVisible = Number.isFinite(parsedKeepVisible) + ? Math.max(0, Math.min(50, parsedKeepVisible)) + : 6; // 收集未被事件消费的 L0,按 rerankScore 降序 const focusSetForEvidence = new Set((focusCharacters || []).map(normalize).filter(Boolean)); @@ -1171,22 +1219,22 @@ async function buildVectorPrompt(store, recallResult, causalById, focusCharacter const recentEnd = lastChunkFloor - keepVisible; if (recentEnd >= recentStart) { - const recentL0 = remainingL0 + const recentAllL0 = getRecentWindowL0Atoms(recentStart, recentEnd); + const recentL0 = recentAllL0 .filter(l0 => !usedL0Ids.has(l0.id)) .filter(l0 => l0.floor >= recentStart && l0.floor <= recentEnd); if (recentL0.length) { const recentBudget = { used: 0, max: UNSUMMARIZED_EVIDENCE_MAX }; - // 先按分数挑组(高分优先),再按时间输出(楼层升序) + // Pick newest floors first, then output in chronological order. const recentFloorMap = groupL0ByFloor(recentL0); const recentRanked = []; for (const [floor, l0s] of recentFloorMap) { - const group = buildEvidenceGroup(floor, l0s, l1ByFloor); - const bestScore = Math.max(...l0s.map(l0 => (l0.rerankScore ?? l0.similarity ?? 0))); - recentRanked.push({ group, bestScore }); + const group = buildRecentEvidenceGroup(floor, l0s); + recentRanked.push({ group }); } - recentRanked.sort((a, b) => (b.bestScore - a.bestScore) || (a.group.floor - b.group.floor)); + recentRanked.sort((a, b) => b.group.floor - a.group.floor); const acceptedRecentGroups = []; for (const item of recentRanked) { @@ -1277,6 +1325,8 @@ async function buildVectorPrompt(store, recallResult, causalById, focusCharacter }; metrics.evidence.tokens = injectionStats.distantEvidence.tokens + injectionStats.recentEvidence.tokens; + metrics.evidence.recentSource = 'all_l0_window'; + metrics.evidence.recentL1Attached = 0; metrics.evidence.assemblyTime = Math.round( performance.now() - T_Start - (metrics.timing.constraintFilter || 0) - metrics.formatting.time ); diff --git a/modules/story-summary/story-summary-ui.js b/modules/story-summary/story-summary-ui.js index 461667f..65142ea 100644 --- a/modules/story-summary/story-summary-ui.js +++ b/modules/story-summary/story-summary-ui.js @@ -87,6 +87,7 @@ api: { provider: 'st', url: '', key: '', model: '', modelCache: [] }, gen: { temperature: null, top_p: null, top_k: null, presence_penalty: null, frequency_penalty: null }, trigger: { enabled: false, interval: 20, timing: 'before_user', role: 'system', useStream: true, maxPerRun: 100, wrapperHead: '', wrapperTail: '', forceInsertAtEnd: false }, + ui: { hideSummarized: true, keepVisibleCount: 6 }, textFilterRules: [...DEFAULT_FILTER_RULES], vector: { enabled: false, engine: 'online', local: { modelId: 'bge-small-zh' }, online: { provider: 'siliconflow', url: '', key: '', model: '' } } }; @@ -124,6 +125,7 @@ Object.assign(config.api, p.api || {}); Object.assign(config.gen, p.gen || {}); Object.assign(config.trigger, p.trigger || {}); + Object.assign(config.ui, p.ui || {}); config.textFilterRules = Array.isArray(p.textFilterRules) ? p.textFilterRules : (Array.isArray(p.vector?.textFilterRules) ? p.vector.textFilterRules : [...DEFAULT_FILTER_RULES]); @@ -141,6 +143,7 @@ Object.assign(config.api, cfg.api || {}); Object.assign(config.gen, cfg.gen || {}); Object.assign(config.trigger, cfg.trigger || {}); + Object.assign(config.ui, cfg.ui || {}); config.textFilterRules = Array.isArray(cfg.textFilterRules) ? cfg.textFilterRules : (Array.isArray(cfg.vector?.textFilterRules) @@ -1599,7 +1602,8 @@ // Hide summarized $('hide-summarized').onchange = e => postMsg('TOGGLE_HIDE_SUMMARIZED', { enabled: e.target.checked }); $('keep-visible-count').onchange = e => { - const c = Math.max(0, Math.min(50, parseInt(e.target.value) || 3)); + const parsedCount = Number.parseInt(e.target.value, 10); + const c = Number.isFinite(parsedCount) ? Math.max(0, Math.min(50, parsedCount)) : 6; e.target.value = c; postMsg('UPDATE_KEEP_VISIBLE', { count: c }); }; diff --git a/modules/story-summary/story-summary.js b/modules/story-summary/story-summary.js index 4f95d0b..4e51394 100644 --- a/modules/story-summary/story-summary.js +++ b/modules/story-summary/story-summary.js @@ -22,7 +22,7 @@ import { postToIframe, isTrustedMessage } from "../../core/iframe-messaging.js"; import { CommonSettingStorage } from "../../core/server-storage.js"; // config/store -import { getSettings, getSummaryPanelConfig, getVectorConfig, saveVectorConfig } from "./data/config.js"; +import { getSettings, getSummaryPanelConfig, getVectorConfig, saveVectorConfig, saveSummaryPanelConfig } from "./data/config.js"; import { getSummaryStore, saveSummaryStore, @@ -950,10 +950,41 @@ async function sendSavedConfigToFrame() { } } +function getHideUiSettings() { + const cfg = getSummaryPanelConfig() || {}; + const ui = cfg.ui || {}; + const parsedKeep = Number.parseInt(ui.keepVisibleCount, 10); + const keepVisibleCount = Number.isFinite(parsedKeep) ? Math.max(0, Math.min(50, parsedKeep)) : 6; + return { + hideSummarized: !!ui.hideSummarized, + keepVisibleCount, + }; +} + +function setHideUiSettings(patch = {}) { + const cfg = getSummaryPanelConfig() || {}; + const current = getHideUiSettings(); + const next = { + ...cfg, + ui: { + hideSummarized: patch.hideSummarized !== undefined ? !!patch.hideSummarized : current.hideSummarized, + keepVisibleCount: patch.keepVisibleCount !== undefined + ? (() => { + const parsedKeep = Number.parseInt(patch.keepVisibleCount, 10); + return Number.isFinite(parsedKeep) ? Math.max(0, Math.min(50, parsedKeep)) : 6; + })() + : current.keepVisibleCount, + }, + }; + saveSummaryPanelConfig(next); + return next.ui; +} + async function sendFrameBaseData(store, totalFloors) { + const ui = getHideUiSettings(); const boundary = await getHideBoundaryFloor(store); - const range = calcHideRange(boundary); - const hiddenCount = (store?.hideSummarizedHistory && range) ? (range.end + 1) : 0; + const range = calcHideRange(boundary, ui.keepVisibleCount); + const hiddenCount = (ui.hideSummarized && range) ? (range.end + 1) : 0; const lastSummarized = store?.lastSummarizedMesId ?? -1; postToFrame({ @@ -965,8 +996,8 @@ async function sendFrameBaseData(store, totalFloors) { pendingFloors: totalFloors - lastSummarized - 1, hiddenCount, }, - hideSummarized: store?.hideSummarizedHistory || false, - keepVisibleCount: store?.keepVisibleCount ?? 3, + hideSummarized: ui.hideSummarized, + keepVisibleCount: ui.keepVisibleCount, }); } @@ -1041,7 +1072,8 @@ async function getHideBoundaryFloor(store) { async function applyHideState() { const store = getSummaryStore(); - if (!store?.hideSummarizedHistory) return; + const ui = getHideUiSettings(); + if (!ui.hideSummarized) return; // 先全量 unhide,杜绝历史残留 await unhideAllMessages(); @@ -1049,7 +1081,7 @@ async function applyHideState() { const boundary = await getHideBoundaryFloor(store); if (boundary < 0) return; - const range = calcHideRange(boundary); + const range = calcHideRange(boundary, ui.keepVisibleCount); if (!range) return; await executeSlashCommand(`/hide ${range.start}-${range.end}`); @@ -1149,9 +1181,9 @@ async function autoRunSummaryWithRetry(targetMesId, configForRun) { function updateFrameStatsAfterSummary(endMesId, merged) { const { chat } = getContext(); const totalFloors = Array.isArray(chat) ? chat.length : 0; - const store = getSummaryStore(); - const range = calcHideRange(endMesId); - const hiddenCount = store?.hideSummarizedHistory && range ? range.end + 1 : 0; + const ui = getHideUiSettings(); + const range = calcHideRange(endMesId, ui.keepVisibleCount); + const hiddenCount = ui.hideSummarized && range ? range.end + 1 : 0; postToFrame({ type: "SUMMARY_BASE_DATA", @@ -1347,11 +1379,7 @@ async function handleFrameMessage(event) { } case "TOGGLE_HIDE_SUMMARIZED": { - const store = getSummaryStore(); - if (!store) break; - - store.hideSummarizedHistory = !!data.enabled; - saveSummaryStore(); + setHideUiSettings({ hideSummarized: !!data.enabled }); (async () => { if (data.enabled) { @@ -1364,21 +1392,19 @@ async function handleFrameMessage(event) { } case "UPDATE_KEEP_VISIBLE": { - const store = getSummaryStore(); - if (!store) break; - - const oldCount = store.keepVisibleCount ?? 3; - const newCount = Math.max(0, Math.min(50, parseInt(data.count) || 3)); + const oldCount = getHideUiSettings().keepVisibleCount; + const parsedCount = Number.parseInt(data.count, 10); + const newCount = Number.isFinite(parsedCount) ? Math.max(0, Math.min(50, parsedCount)) : 6; if (newCount === oldCount) break; - store.keepVisibleCount = newCount; - saveSummaryStore(); + setHideUiSettings({ keepVisibleCount: newCount }); (async () => { - if (store.hideSummarizedHistory) { + if (getHideUiSettings().hideSummarized) { await applyHideState(); } const { chat } = getContext(); + const store = getSummaryStore(); await sendFrameBaseData(store, Array.isArray(chat) ? chat.length : 0); })(); break; @@ -1452,7 +1478,7 @@ async function handleChatChanged() { const store = getSummaryStore(); - if (store?.hideSummarizedHistory) { + if (getHideUiSettings().hideSummarized) { await applyHideState(); }