diff --git a/bridges/call-generate-service.js b/bridges/call-generate-service.js index a4d0b29..2800a79 100644 --- a/bridges/call-generate-service.js +++ b/bridges/call-generate-service.js @@ -1,23 +1,23 @@ -// @ts-nocheck -import { oai_settings, chat_completion_sources, getChatCompletionModel, promptManager } from "../../../../openai.js"; -import { ChatCompletionService } from "../../../../custom-request.js"; -import { eventSource, event_types } from "../../../../../script.js"; -import { getContext } from "../../../../st-context.js"; -import { xbLog } from "../core/debug-core.js"; +// @ts-nocheck +import { oai_settings, chat_completion_sources, getChatCompletionModel, promptManager, openai_setting_names, openai_settings } from "../../../../openai.js"; +import { ChatCompletionService } from "../../../../custom-request.js"; +import { eventSource, event_types } from "../../../../../script.js"; +import { getContext } from "../../../../st-context.js"; +import { xbLog } from "../core/debug-core.js"; const SOURCE_TAG = 'xiaobaix-host'; -const POSITIONS = Object.freeze({ BEFORE_PROMPT: 'BEFORE_PROMPT', IN_PROMPT: 'IN_PROMPT', IN_CHAT: 'IN_CHAT', AFTER_COMPONENT: 'AFTER_COMPONENT' }); -const KNOWN_KEYS = Object.freeze(new Set([ - 'main', 'chatHistory', 'worldInfo', 'worldInfoBefore', 'worldInfoAfter', - 'charDescription', 'charPersonality', 'scenario', 'personaDescription', - 'dialogueExamples', 'authorsNote', 'vectorsMemory', 'vectorsDataBank', - 'smartContext', 'jailbreak', 'nsfw', 'summary', 'bias', 'impersonate', 'quietPrompt', -])); -const resolveTargetOrigin = (origin) => { - if (typeof origin === 'string' && origin) return origin; - try { return window.location.origin; } catch { return '*'; } -}; +const POSITIONS = Object.freeze({ BEFORE_PROMPT: 'BEFORE_PROMPT', IN_PROMPT: 'IN_PROMPT', IN_CHAT: 'IN_CHAT', AFTER_COMPONENT: 'AFTER_COMPONENT' }); +const KNOWN_KEYS = Object.freeze(new Set([ + 'main', 'chatHistory', 'worldInfo', 'worldInfoBefore', 'worldInfoAfter', + 'charDescription', 'charPersonality', 'scenario', 'personaDescription', + 'dialogueExamples', 'authorsNote', 'vectorsMemory', 'vectorsDataBank', + 'smartContext', 'jailbreak', 'nsfw', 'summary', 'bias', 'impersonate', 'quietPrompt', +])); +const resolveTargetOrigin = (origin) => { + if (typeof origin === 'string' && origin) return origin; + try { return window.location.origin; } catch { return '*'; } +}; // @ts-nocheck class CallGenerateService { @@ -48,11 +48,11 @@ class CallGenerateService { } } - sendError(sourceWindow, requestId, streamingEnabled, err, fallbackCode = 'API_ERROR', details = null, targetOrigin = null) { - const e = this.normalizeError(err, fallbackCode, details); - const type = streamingEnabled ? 'generateStreamError' : 'generateError'; - try { sourceWindow?.postMessage({ source: SOURCE_TAG, type, id: requestId, error: e }, resolveTargetOrigin(targetOrigin)); } catch {} - } + sendError(sourceWindow, requestId, streamingEnabled, err, fallbackCode = 'API_ERROR', details = null, targetOrigin = null) { + const e = this.normalizeError(err, fallbackCode, details); + const type = streamingEnabled ? 'generateStreamError' : 'generateError'; + try { sourceWindow?.postMessage({ source: SOURCE_TAG, type, id: requestId, error: e }, resolveTargetOrigin(targetOrigin)); } catch {} + } /** * @param {string|undefined} rawId @@ -257,11 +257,11 @@ class CallGenerateService { * @param {string} type * @param {object} body */ - postToTarget(target, type, body, targetOrigin = null) { - try { - target?.postMessage({ source: SOURCE_TAG, type, ...body }, resolveTargetOrigin(targetOrigin)); - } catch (e) {} - } + postToTarget(target, type, body, targetOrigin = null) { + try { + target?.postMessage({ source: SOURCE_TAG, type, ...body }, resolveTargetOrigin(targetOrigin)); + } catch (e) {} + } // ===== ST Prompt 干跑捕获与组件切换 ===== @@ -386,6 +386,43 @@ class CallGenerateService { return []; } + /** + * 临时切换到指定 preset 执行 fn,执行完毕后恢复 oai_settings。 + * 模式与 _withPromptToggle 一致:snapshot → 覆写 → fn() → finally 恢复。 + * @param {string} presetName - preset 名称 + * @param {Function} fn - 要在 preset 上下文中执行的异步函数 + */ + async _withTemporaryPreset(presetName, fn) { + if (!presetName) return await fn(); + const idx = openai_setting_names?.[presetName]; + if (idx === undefined || idx === null) { + throw new Error(`Preset "${presetName}" not found`); + } + const preset = openai_settings?.[idx]; + if (!preset || typeof preset !== 'object') { + throw new Error(`Preset "${presetName}" data is invalid`); + } + let snapshot; + try { snapshot = structuredClone(oai_settings); } + catch { snapshot = JSON.parse(JSON.stringify(oai_settings)); } + try { + let presetClone; + try { presetClone = structuredClone(preset); } + catch { presetClone = JSON.parse(JSON.stringify(preset)); } + for (const key of Object.keys(presetClone)) { + oai_settings[key] = presetClone[key]; + } + return await fn(); + } finally { + for (const key of Object.keys(oai_settings)) { + if (!Object.prototype.hasOwnProperty.call(snapshot, key)) { + try { delete oai_settings[key]; } catch {} + } + } + Object.assign(oai_settings, snapshot); + } + } + // ===== 工具函数:组件与消息辅助 ===== /** @@ -1008,7 +1045,7 @@ class CallGenerateService { _applyContentFilter(list, filterCfg) { if (!filterCfg) return list; - const { contains, regex, fromUserNames } = filterCfg; + const { contains, regex, fromUserNames } = filterCfg; let out = list.slice(); if (contains) { const needles = Array.isArray(contains) ? contains : [contains]; @@ -1132,7 +1169,7 @@ class CallGenerateService { // ===== 发送实现(构建后的统一发送) ===== - async _sendMessages(messages, options, requestId, sourceWindow, targetOrigin = null) { + async _sendMessages(messages, options, requestId, sourceWindow, targetOrigin = null) { const sessionId = this.normalizeSessionId(options?.session?.id || 'xb1'); const session = this.ensureSession(sessionId); const streamingEnabled = options?.streaming?.enabled !== false; // 默认开 @@ -1143,11 +1180,11 @@ class CallGenerateService { const shouldExport = !!(options?.debug?.enabled || options?.debug?.exportPrompt); const already = options?.debug?._exported === true; if (shouldExport && !already) { - this.postToTarget(sourceWindow, 'generatePromptPreview', { id: requestId, messages: (messages || []).map(m => ({ role: m.role, content: m.content })) }, targetOrigin); + this.postToTarget(sourceWindow, 'generatePromptPreview', { id: requestId, messages: (messages || []).map(m => ({ role: m.role, content: m.content })) }, targetOrigin); } if (streamingEnabled) { - this.postToTarget(sourceWindow, 'generateStreamStart', { id: requestId, sessionId }, targetOrigin); + this.postToTarget(sourceWindow, 'generateStreamStart', { id: requestId, sessionId }, targetOrigin); const streamFn = await ChatCompletionService.sendRequest(payload, false, session.abortController.signal); let last = ''; const generator = typeof streamFn === 'function' ? streamFn() : null; @@ -1155,7 +1192,7 @@ class CallGenerateService { const chunk = text.slice(last.length); last = text; session.accumulated = text; - this.postToTarget(sourceWindow, 'generateStreamChunk', { id: requestId, chunk, accumulated: text, metadata: {} }, targetOrigin); + this.postToTarget(sourceWindow, 'generateStreamChunk', { id: requestId, chunk, accumulated: text, metadata: {} }, targetOrigin); } const result = { success: true, @@ -1163,7 +1200,7 @@ class CallGenerateService { sessionId, metadata: { duration: Date.now() - session.startedAt, model: apiCfg.model, finishReason: 'stop' }, }; - this.postToTarget(sourceWindow, 'generateStreamComplete', { id: requestId, result }, targetOrigin); + this.postToTarget(sourceWindow, 'generateStreamComplete', { id: requestId, result }, targetOrigin); return result; } else { const extracted = await ChatCompletionService.sendRequest(payload, true, session.abortController.signal); @@ -1173,20 +1210,25 @@ class CallGenerateService { sessionId, metadata: { duration: Date.now() - session.startedAt, model: apiCfg.model, finishReason: 'stop' }, }; - this.postToTarget(sourceWindow, 'generateResult', { id: requestId, result }, targetOrigin); + this.postToTarget(sourceWindow, 'generateResult', { id: requestId, result }, targetOrigin); return result; } } catch (err) { - this.sendError(sourceWindow, requestId, streamingEnabled, err, 'API_ERROR', null, targetOrigin); - return null; - } - } + this.sendError(sourceWindow, requestId, streamingEnabled, err, 'API_ERROR', null, targetOrigin); + return null; + } + } // ===== 主流程 ===== - async handleRequestInternal(options, requestId, sourceWindow, targetOrigin = null) { + async handleRequestInternal(options, requestId, sourceWindow, targetOrigin = null, { assembleOnly = false } = {}) { // 1) 校验 this.validateOptions(options); + const presetName = options?.preset || null; + + // 步骤 2~9 的核心逻辑,可能包裹在 _withTemporaryPreset 中执行 + const executeCore = async () => { + // 2) 解析组件列表与内联注入 const list = Array.isArray(options?.components?.list) ? options.components.list.slice() : undefined; let baseStrategy = 'EMPTY'; // EMPTY | ALL | ALL_PREON | SUBSET @@ -1226,6 +1268,7 @@ class CallGenerateService { // 3) 干跑捕获(基座) let captured = []; + let enabledIds = []; // assembleOnly 时用于 identifier 标注 if (baseStrategy === 'EMPTY') { captured = []; } else { @@ -1242,6 +1285,7 @@ class CallGenerateService { allow = new Set(order.map(e => e.identifier)); } } catch {} + enabledIds = Array.from(allow); const run = async () => await this._capturePromptMessages({ includeConfig: null, quietText: '', skipWIAN: false }); captured = await this._withPromptEnabledSet(allow, run); } else if (baseStrategy === 'ALL_PREON') { @@ -1255,6 +1299,7 @@ class CallGenerateService { allow = new Set(order.filter(e => !!e?.enabled).map(e => e.identifier)); } } catch {} + enabledIds = Array.from(allow); const run = async () => await this._capturePromptMessages({ includeConfig: null, quietText: '', skipWIAN: false }); captured = await this._withPromptEnabledSet(allow, run); } else { @@ -1263,7 +1308,11 @@ class CallGenerateService { } // 4) 依据策略计算启用集合与顺序 - const annotateKeys = baseStrategy === 'SUBSET' ? orderedRefs : ((baseStrategy === 'ALL' || baseStrategy === 'ALL_PREON') ? orderedRefs : []); + let annotateKeys = baseStrategy === 'SUBSET' ? orderedRefs : ((baseStrategy === 'ALL' || baseStrategy === 'ALL_PREON') ? orderedRefs : []); + // assembleOnly 模式下,若无显式排序引用,则用全部启用组件做 identifier 标注 + if (assembleOnly && annotateKeys.length === 0 && enabledIds.length > 0) { + annotateKeys = enabledIds; + } let working = await this._annotateIdentifiersIfMissing(captured.slice(), annotateKeys); working = this._applyOrderingStrategy(working, baseStrategy, orderedRefs, unorderedKeys); @@ -1277,10 +1326,42 @@ class CallGenerateService { working = this._appendUserInput(working, options?.userInput); // 8) 调试导出 - this._exportDebugData({ sourceWindow, requestId, working, baseStrategy, orderedRefs, inlineMapped, listLevelOverrides, debug: options?.debug, targetOrigin }); + this._exportDebugData({ sourceWindow, requestId, working, baseStrategy, orderedRefs, inlineMapped, listLevelOverrides, debug: options?.debug, targetOrigin }); + + // assembleOnly 模式:只返回组装好的 messages,不调 LLM + if (assembleOnly) { + // 构建 identifier → name 映射(从 promptCollection 取,order 里没有 name) + const idToName = new Map(); + try { + if (promptManager && typeof promptManager.getPromptCollection === 'function') { + const pc = promptManager.getPromptCollection(); + const coll = pc?.collection || []; + for (const p of coll) { + if (p?.identifier) idToName.set(p.identifier, p.name || p.label || p.title || ''); + } + } + } catch {} + const messages = working.map(m => { + const id = m.identifier || undefined; + const componentName = id ? (idToName.get(id) || undefined) : undefined; + return { role: m.role, content: m.content, identifier: id, name: componentName }; + }); + this.postToTarget(sourceWindow, 'assemblePromptResult', { + id: requestId, + messages: messages + }, targetOrigin); + return { messages }; + } // 9) 发送 - return await this._sendMessages(working, { ...options, debug: { ...(options?.debug || {}), _exported: true } }, requestId, sourceWindow, targetOrigin); + return await this._sendMessages(working, { ...options, debug: { ...(options?.debug || {}), _exported: true } }, requestId, sourceWindow, targetOrigin); + + }; // end executeCore + + if (presetName) { + return await this._withTemporaryPreset(presetName, executeCore); + } + return await executeCore(); } _applyOrderingStrategy(messages, baseStrategy, orderedRefs, unorderedKeys) { @@ -1340,9 +1421,9 @@ class CallGenerateService { return out; } - _exportDebugData({ sourceWindow, requestId, working, baseStrategy, orderedRefs, inlineMapped, listLevelOverrides, debug, targetOrigin }) { + _exportDebugData({ sourceWindow, requestId, working, baseStrategy, orderedRefs, inlineMapped, listLevelOverrides, debug, targetOrigin }) { const exportPrompt = !!(debug?.enabled || debug?.exportPrompt); - if (exportPrompt) this.postToTarget(sourceWindow, 'generatePromptPreview', { id: requestId, messages: working.map(m => ({ role: m.role, content: m.content })) }, targetOrigin); + if (exportPrompt) this.postToTarget(sourceWindow, 'generatePromptPreview', { id: requestId, messages: working.map(m => ({ role: m.role, content: m.content })) }, targetOrigin); if (debug?.exportBlueprint) { try { const bp = { @@ -1351,7 +1432,7 @@ class CallGenerateService { injections: (debug?.injections || []).concat(inlineMapped || []), overrides: listLevelOverrides || null, }; - this.postToTarget(sourceWindow, 'blueprint', bp, targetOrigin); + this.postToTarget(sourceWindow, 'blueprint', bp, targetOrigin); } catch {} } } @@ -1359,25 +1440,25 @@ class CallGenerateService { /** * 入口:处理 generateRequest(统一入口) */ - async handleGenerateRequest(options, requestId, sourceWindow, targetOrigin = null) { - let streamingEnabled = false; - try { - streamingEnabled = options?.streaming?.enabled !== false; - try { - if (xbLog.isEnabled?.()) { - const comps = options?.components?.list; - const compsCount = Array.isArray(comps) ? comps.length : 0; - const userInputLen = String(options?.userInput || '').length; - xbLog.info('callGenerateBridge', `generateRequest id=${requestId} stream=${!!streamingEnabled} comps=${compsCount} userInputLen=${userInputLen}`); - } - } catch {} - return await this.handleRequestInternal(options, requestId, sourceWindow, targetOrigin); - } catch (err) { - try { xbLog.error('callGenerateBridge', `generateRequest failed id=${requestId}`, err); } catch {} - this.sendError(sourceWindow, requestId, streamingEnabled, err, 'BAD_REQUEST', null, targetOrigin); - return null; - } - } + async handleGenerateRequest(options, requestId, sourceWindow, targetOrigin = null) { + let streamingEnabled = false; + try { + streamingEnabled = options?.streaming?.enabled !== false; + try { + if (xbLog.isEnabled?.()) { + const comps = options?.components?.list; + const compsCount = Array.isArray(comps) ? comps.length : 0; + const userInputLen = String(options?.userInput || '').length; + xbLog.info('callGenerateBridge', `generateRequest id=${requestId} stream=${!!streamingEnabled} comps=${compsCount} userInputLen=${userInputLen}`); + } + } catch {} + return await this.handleRequestInternal(options, requestId, sourceWindow, targetOrigin); + } catch (err) { + try { xbLog.error('callGenerateBridge', `generateRequest failed id=${requestId}`, err); } catch {} + this.sendError(sourceWindow, requestId, streamingEnabled, err, 'BAD_REQUEST', null, targetOrigin); + return null; + } + } /** 取消会话 */ cancel(sessionId) { @@ -1394,43 +1475,76 @@ class CallGenerateService { const callGenerateService = new CallGenerateService(); -export async function handleGenerateRequest(options, requestId, sourceWindow, targetOrigin = null) { - return await callGenerateService.handleGenerateRequest(options, requestId, sourceWindow, targetOrigin); -} +export async function handleGenerateRequest(options, requestId, sourceWindow, targetOrigin = null) { + return await callGenerateService.handleGenerateRequest(options, requestId, sourceWindow, targetOrigin); +} // Host bridge for handling iframe generateRequest → respond via postMessage let __xb_generate_listener_attached = false; let __xb_generate_listener = null; -export function initCallGenerateHostBridge() { - if (typeof window === 'undefined') return; - if (__xb_generate_listener_attached) return; - try { xbLog.info('callGenerateBridge', 'initCallGenerateHostBridge'); } catch {} - __xb_generate_listener = async function (event) { - try { - const data = event && event.data || {}; - if (!data || data.type !== 'generateRequest') return; - const id = data.id; - const options = data.options || {}; - await handleGenerateRequest(options, id, event.source || window, event.origin); - } catch (e) { - try { xbLog.error('callGenerateBridge', 'generateRequest listener error', e); } catch {} - } - }; - // eslint-disable-next-line no-restricted-syntax -- bridge listener; origin can be null for sandboxed iframes. - try { window.addEventListener('message', __xb_generate_listener); } catch (e) {} - __xb_generate_listener_attached = true; -} +export function initCallGenerateHostBridge() { + if (typeof window === 'undefined') return; + if (__xb_generate_listener_attached) return; + try { xbLog.info('callGenerateBridge', 'initCallGenerateHostBridge'); } catch {} + __xb_generate_listener = async function (event) { + try { + const data = event && event.data || {}; + if (!data) return; -export function cleanupCallGenerateHostBridge() { - if (typeof window === 'undefined') return; - if (!__xb_generate_listener_attached) return; - try { xbLog.info('callGenerateBridge', 'cleanupCallGenerateHostBridge'); } catch {} - try { window.removeEventListener('message', __xb_generate_listener); } catch (e) {} - __xb_generate_listener_attached = false; - __xb_generate_listener = null; - try { callGenerateService.cleanup(); } catch (e) {} -} + if (data.type === 'generateRequest') { + const id = data.id; + const options = data.options || {}; + await handleGenerateRequest(options, id, event.source || window, event.origin); + return; + } + + if (data.type === 'listPresetsRequest') { + const id = data.id; + const names = Object.keys(openai_setting_names || {}); + const selected = oai_settings?.preset_settings_openai || ''; + callGenerateService.postToTarget( + event.source || window, + 'listPresetsResult', + { id, presets: names, selected }, + event.origin + ); + return; + } + + if (data.type === 'assemblePromptRequest') { + const id = data.id; + const options = data.options || {}; + try { + await callGenerateService.handleRequestInternal( + options, id, event.source || window, event.origin, + { assembleOnly: true } + ); + } catch (err) { + callGenerateService.sendError( + event.source || window, id, false, err, 'ASSEMBLE_ERROR', null, event.origin + ); + } + return; + } + } catch (e) { + try { xbLog.error('callGenerateBridge', 'listener error', e); } catch {} + } + }; + // eslint-disable-next-line no-restricted-syntax -- bridge listener; origin can be null for sandboxed iframes. + try { window.addEventListener('message', __xb_generate_listener); } catch (e) {} + __xb_generate_listener_attached = true; +} + +export function cleanupCallGenerateHostBridge() { + if (typeof window === 'undefined') return; + if (!__xb_generate_listener_attached) return; + try { xbLog.info('callGenerateBridge', 'cleanupCallGenerateHostBridge'); } catch {} + try { window.removeEventListener('message', __xb_generate_listener); } catch (e) {} + __xb_generate_listener_attached = false; + __xb_generate_listener = null; + try { callGenerateService.cleanup(); } catch (e) {} +} if (typeof window !== 'undefined') { Object.assign(window, { xiaobaixCallGenerateService: callGenerateService, initCallGenerateHostBridge, cleanupCallGenerateHostBridge }); @@ -1514,8 +1628,8 @@ if (typeof window !== 'undefined') { } }; - // eslint-disable-next-line no-restricted-syntax -- local listener for internal request flow. - window.addEventListener('message', listener); + // eslint-disable-next-line no-restricted-syntax -- local listener for internal request flow. + window.addEventListener('message', listener); // 发送请求 handleGenerateRequest(options, requestId, window).catch(err => { @@ -1525,6 +1639,49 @@ if (typeof window !== 'undefined') { }); }; + /** + * 全局 assemblePrompt 函数 + * 只组装提示词,不调用 LLM,返回组装好的 messages 数组 + * + * @param {Object} options - 与 callGenerate 相同的选项格式(api/streaming 字段会被忽略) + * @returns {Promise>} 组装后的 messages 数组 + * + * @example + * const messages = await window.LittleWhiteBox.assemblePrompt({ + * components: { list: ['ALL_PREON'] }, + * userInput: '可选的用户输入' + * }); + * // messages = [{ role: 'system', content: '...' }, ...] + */ + window.LittleWhiteBox.assemblePrompt = async function(options) { + return new Promise((resolve, reject) => { + const requestId = `assemble-${Date.now()}-${Math.random().toString(36).slice(2)}`; + + const listener = (event) => { + const data = event.data; + if (!data || data.source !== SOURCE_TAG || data.id !== requestId) return; + + if (data.type === 'assemblePromptResult') { + window.removeEventListener('message', listener); + resolve(data.messages); + } else if (data.type === 'generateError' || data.type === 'generateStreamError') { + window.removeEventListener('message', listener); + reject(data.error); + } + }; + + // eslint-disable-next-line no-restricted-syntax -- local listener for internal request flow. + window.addEventListener('message', listener); + + callGenerateService.handleRequestInternal( + options, requestId, window, null, { assembleOnly: true } + ).catch(err => { + window.removeEventListener('message', listener); + reject(err); + }); + }); + }; + /** * 取消指定会话 * @param {string} sessionId - 会话 ID(如 'xb1', 'xb2' 等) @@ -1540,6 +1697,14 @@ if (typeof window !== 'undefined') { callGenerateService.cleanup(); }; + window.LittleWhiteBox.listChatCompletionPresets = function() { + return Object.keys(openai_setting_names || {}); + }; + + window.LittleWhiteBox.getSelectedPresetName = function() { + return oai_settings?.preset_settings_openai || ''; + }; + // 保持向后兼容:保留原有的内部接口 window.LittleWhiteBox._internal = { service: callGenerateService, @@ -1547,4 +1712,4 @@ if (typeof window !== 'undefined') { init: initCallGenerateHostBridge, cleanup: cleanupCallGenerateHostBridge }; -} +} diff --git a/bridges/context-bridge.js b/bridges/context-bridge.js new file mode 100644 index 0000000..730f3c2 --- /dev/null +++ b/bridges/context-bridge.js @@ -0,0 +1,293 @@ +// @ts-nocheck +import { event_types, user_avatar, getCurrentChatId } from "../../../../../script.js"; +import { getContext } from "../../../../st-context.js"; +import { power_user } from "../../../../power-user.js"; +import { createModuleEvents } from "../core/event-manager.js"; +import { xbLog } from "../core/debug-core.js"; + +const SOURCE_TAG = 'xiaobaix-host'; + +/** + * Context Bridge — 模板 iframe 上下文桥接服务 + * + * 功能: + * 1. iframe 发送 iframe-ready / request-context → 插件推送上下文快照 + * 2. 酒馆事件实时转发到所有模板 iframe + * 3. 延迟投递队列:iframe 销毁后的事件暂存,待下一个 iframe 连接时投递 + */ +class ContextBridgeService { + constructor() { + this._attached = false; + this._listener = null; + this._previousChatId = null; + /** @type {Array<{type: string, event: string, payload: object}>} */ + this._pendingEvents = []; + this._events = createModuleEvents('contextBridge'); + } + + // ===== 生命周期 ===== + + init() { + if (this._attached) return; + try { xbLog.info('contextBridge', 'init'); } catch {} + + try { + this._previousChatId = getCurrentChatId(); + } catch {} + + const self = this; + this._listener = function (event) { + try { + self._handleMessage(event); + } catch (e) { + try { xbLog.error('contextBridge', 'message handler error', e); } catch {} + } + }; + + // eslint-disable-next-line no-restricted-syntax -- bridge listener for iframe-ready/request-context + window.addEventListener('message', this._listener); + this._attachEventForwarding(); + this._attached = true; + } + + cleanup() { + if (!this._attached) return; + try { xbLog.info('contextBridge', 'cleanup'); } catch {} + try { window.removeEventListener('message', this._listener); } catch {} + this._listener = null; + this._events.cleanup(); + this._pendingEvents.length = 0; + this._previousChatId = null; + this._attached = false; + } + + // ===== 消息处理 ===== + + _handleMessage(event) { + const data = event && event.data; + if (!data || typeof data !== 'object') return; + const type = data.type; + if (type !== 'iframe-ready' && type !== 'request-context') return; + + // 找到发送消息的 iframe 元素 + const iframe = this._findIframeBySource(event.source); + if (!iframe) return; + + const msgIndex = this._getMsgIndexForIframe(iframe); + if (msgIndex < 0) return; + + // iframe-ready 时先投递积压的延迟事件 + if (type === 'iframe-ready') { + while (this._pendingEvents.length > 0) { + const pending = this._pendingEvents.shift(); + // eslint-disable-next-line no-restricted-syntax -- delivering queued events to newly ready iframe + try { event.source?.postMessage(pending, '*'); } catch {} + } + } + + // 推送上下文快照 + const snapshot = this._buildContextSnapshot(msgIndex); + // eslint-disable-next-line no-restricted-syntax -- sending context snapshot to requesting iframe + try { event.source?.postMessage(snapshot, '*'); } catch {} + } + + /** + * 遍历 DOM 查找 contentWindow 匹配的 iframe + * @param {Window} source + * @returns {HTMLIFrameElement|null} + */ + _findIframeBySource(source) { + if (!source) return null; + const iframes = document.querySelectorAll('iframe.xiaobaix-iframe'); + for (const iframe of iframes) { + try { + if (iframe.contentWindow === source) return iframe; + } catch {} + } + return null; + } + + /** + * 从 iframe 的 DOM 位置获取消息楼层索引 + * @param {HTMLIFrameElement} iframe + * @returns {number} + */ + _getMsgIndexForIframe(iframe) { + const mesBlock = iframe.closest('.mes'); + if (!mesBlock) return -1; + const mesid = mesBlock.getAttribute('mesid'); + if (mesid == null) return -1; + return parseInt(mesid, 10); + } + + // ===== 上下文快照 ===== + + /** + * @param {number} msgIndex + * @returns {object} + */ + _buildContextSnapshot(msgIndex) { + const ctx = getContext(); + const chat = ctx.chat || []; + const msg = chat[msgIndex]; + + return { + type: 'st-context', + chatId: getCurrentChatId() || null, + characterId: ctx.characterId ?? null, + characterName: ctx.name2 || '', + userName: ctx.name1 || '', + userPersona: power_user?.persona_description || '', + userAvatar: user_avatar || '', + msgIndex: msgIndex, + swipeId: msg?.swipe_id ?? 0, + totalSwipes: msg?.swipes?.length ?? 1, + totalMessages: chat.length, + isGroupChat: !!ctx.groupId, + groupId: ctx.groupId ?? null, + }; + } + + // ===== 事件广播 ===== + + /** + * 向所有活跃的模板 iframe 广播事件 + * @param {string} eventName + * @param {object} payload + */ + _broadcastToTemplateIframes(eventName, payload) { + const iframes = document.querySelectorAll('.mes iframe.xiaobaix-iframe'); + const message = { type: 'st-event', source: SOURCE_TAG, event: eventName, payload }; + for (const iframe of iframes) { + // eslint-disable-next-line no-restricted-syntax -- broadcasting event to template iframes + try { iframe.contentWindow?.postMessage(message, '*'); } catch {} + } + } + + // ===== 事件转发注册 ===== + + _attachEventForwarding() { + const self = this; + + // ---- 消息级事件 ---- + + // 消息删除(截断式):原生 payload = chat.length(删除后剩余消息数) + this._events.on(event_types.MESSAGE_DELETED, (remainingCount) => { + self._broadcastToTemplateIframes('message_deleted', { + fromIndex: remainingCount, + }); + }); + + // Swipe 切换:原生 payload = chat.length - 1(最后一条消息索引) + this._events.on(event_types.MESSAGE_SWIPED, (msgIndex) => { + const ctx = getContext(); + const msg = ctx.chat?.[msgIndex]; + self._broadcastToTemplateIframes('message_swiped', { + msgIndex: msgIndex, + newSwipeId: msg?.swipe_id ?? 0, + totalSwipes: msg?.swipes?.length ?? 1, + }); + }); + + // 消息发送:原生 payload = insertAt(消息索引) + this._events.on(event_types.MESSAGE_SENT, (msgIndex) => { + self._broadcastToTemplateIframes('message_sent', { msgIndex }); + }); + + // AI 回复完成:原生 payload = chat_id(消息索引) + this._events.on(event_types.MESSAGE_RECEIVED, (msgIndex) => { + self._broadcastToTemplateIframes('message_received', { msgIndex }); + }); + + // 消息编辑:原生 payload = this_edit_mes_id(消息索引) + this._events.on(event_types.MESSAGE_EDITED, (msgIndex) => { + self._broadcastToTemplateIframes('message_edited', { msgIndex }); + }); + + // ---- 聊天级事件 ---- + + // 聊天切换:原生 payload = getCurrentChatId() + this._events.on(event_types.CHAT_CHANGED, (newChatId) => { + self._broadcastToTemplateIframes('chat_id_changed', { + newChatId: newChatId, + previousChatId: self._previousChatId, + }); + self._previousChatId = newChatId; + }); + + // 新聊天创建(含分支检测):原生 payload = 无 + this._events.on(event_types.CHAT_CREATED, () => { + const ctx = getContext(); + const newLength = (ctx.chat || []).length; + const isBranch = newLength > 1; + + self._broadcastToTemplateIframes('chat_created', { + chatId: getCurrentChatId() || null, + isBranch: isBranch, + branchFromChatId: isBranch ? self._previousChatId : null, + branchPointIndex: isBranch ? newLength - 1 : null, + }); + }); + + // ---- 延迟投递事件(入队,不广播)---- + + // 聊天删除:原生 payload = 聊天文件名(不含 .jsonl) + this._events.on(event_types.CHAT_DELETED, (chatFileName) => { + self._pendingEvents.push({ + type: 'st-event', + source: SOURCE_TAG, + event: 'chat_deleted', + payload: { chatId: chatFileName, timestamp: Date.now() }, + }); + }); + + // 群聊删除 + this._events.on(event_types.GROUP_CHAT_DELETED, (chatFileName) => { + self._pendingEvents.push({ + type: 'st-event', + source: SOURCE_TAG, + event: 'group_chat_deleted', + payload: { chatId: chatFileName, timestamp: Date.now() }, + }); + }); + } +} + +// ===== 模块级实例与导出 ===== + +const contextBridgeService = new ContextBridgeService(); + +export function initContextBridge() { + contextBridgeService.init(); +} + +export function cleanupContextBridge() { + contextBridgeService.cleanup(); +} + +// ===== 自初始化(与 call-generate-service.js 模式一致)===== + +if (typeof window !== 'undefined') { + window.LittleWhiteBox = window.LittleWhiteBox || {}; + window.LittleWhiteBox.contextBridge = contextBridgeService; + + try { initContextBridge(); } catch (e) {} + + try { + window.addEventListener('xiaobaixEnabledChanged', (e) => { + try { + const enabled = e && e.detail && e.detail.enabled === true; + if (enabled) initContextBridge(); else cleanupContextBridge(); + } catch {} + }); + document.addEventListener('xiaobaixEnabledChanged', (e) => { + try { + const enabled = e && e.detail && e.detail.enabled === true; + if (enabled) initContextBridge(); else cleanupContextBridge(); + } catch {} + }); + window.addEventListener('beforeunload', () => { + try { cleanupContextBridge(); } catch {} + }); + } catch {} +} diff --git a/index.js b/index.js index 0f9d5a3..fb7014b 100644 --- a/index.js +++ b/index.js @@ -625,6 +625,11 @@ jQuery(async () => { document.head.appendChild(Object.assign(document.createElement('script'), { id: 'xb-worldbook', type: 'module', src: `${extensionFolderPath}/bridges/worldbook-bridge.js` })); } catch (e) {} + try { + if (isXiaobaixEnabled && !document.getElementById('xb-contextbridge')) + document.head.appendChild(Object.assign(document.createElement('script'), { id: 'xb-contextbridge', type: 'module', src: `${extensionFolderPath}/bridges/context-bridge.js` })); + } catch (e) {} + eventSource.on(event_types.APP_READY, () => { setTimeout(performExtensionUpdateCheck, 2000); }); 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 d5e3ec4..6bc5e41 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, @@ -951,10 +951,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({ @@ -966,8 +997,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, }); } @@ -1042,7 +1073,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(); @@ -1050,7 +1082,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}`); @@ -1150,9 +1182,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", @@ -1348,11 +1380,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) { @@ -1365,21 +1393,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; @@ -1453,7 +1479,7 @@ async function handleChatChanged() { const store = getSummaryStore(); - if (store?.hideSummarizedHistory) { + if (getHideUiSettings().hideSummarized) { await applyHideState(); }