From 1f3880d1d921e3b4d2c7c3fe810e0a28f26e7beb Mon Sep 17 00:00:00 2001 From: bielie Date: Tue, 24 Feb 2026 15:26:21 +0800 Subject: [PATCH] fix: persist story-summary relationships and sync local changes --- bridges/call-generate-service.js | 583 ++++++++++++++++--------- bridges/context-bridge.js | 293 +++++++++++++ index.js | 101 +++-- modules/story-summary/story-summary.js | 69 +++ 4 files changed, 789 insertions(+), 257 deletions(-) create mode 100644 bridges/context-bridge.js diff --git a/bridges/call-generate-service.js b/bridges/call-generate-service.js index a4d0b29..7533e72 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 干跑捕获与组件切换 ===== @@ -352,7 +352,7 @@ class CallGenerateService { const order = pm?.getPromptOrderForCharacter(activeChar) ?? []; const mapSnap = new Map((this._lastToggleSnapshot || snapshot).map(s => [s.identifier, s.enabled])); order.forEach(e => { if (mapSnap.has(e.identifier)) e.enabled = mapSnap.get(e.identifier); }); - } catch {} + } catch { } this._toggleBusy = false; this._lastToggleSnapshot = null; } @@ -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); + } + } + // ===== 工具函数:组件与消息辅助 ===== /** @@ -466,7 +503,7 @@ class CallGenerateService { try { const nameCache = this._getNameCache(); if (nameCache.has(nm)) return nameCache.get(nm); - } catch {} + } catch { } // 2) 扫描 PromptManager 的订单(显示用) try { @@ -484,7 +521,7 @@ class CallGenerateService { } } } - } catch {} + } catch { } // 3) 扫描 Prompt 集合(运行期合并后的集合) try { @@ -501,7 +538,7 @@ class CallGenerateService { } } } - } catch {} + } catch { } // 4) 失败时尝试 sanitize 名称与 identifier 的弱匹配 if (matches.size === 0) { @@ -518,12 +555,12 @@ class CallGenerateService { } } } - } catch {} + } catch { } } if (matches.size === 1) { const id = Array.from(matches)[0]; - try { this._getNameCache().set(nm, id); } catch {} + try { this._getNameCache().set(nm, id); } catch { } return id; } if (matches.size > 1) { @@ -786,9 +823,9 @@ class CallGenerateService { const capture = await this._captureWithEnabledSet(new Set([key]), '', false); const normSet = new Set(capture.map(x => `[${x.role}] ${this._normStrip(x.content)}`)); footprint.set(key, normSet); - try { fpCache.set(key, normSet); } catch {} + try { fpCache.set(key, normSet); } catch { } } - } catch {} + } catch { } } for (const m of arr) { if (m?.identifier) continue; @@ -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]; @@ -1018,7 +1055,7 @@ class CallGenerateService { try { const re = new RegExp(regex); out = out.filter(m => re.test(String(m.content))); - } catch {} + } catch { } } if (fromUserNames && fromUserNames.length) { // 仅当 messages 中附带 name 时生效;否则忽略 @@ -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,114 +1210,158 @@ 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); - // 2) 解析组件列表与内联注入 - const list = Array.isArray(options?.components?.list) ? options.components.list.slice() : undefined; - let baseStrategy = 'EMPTY'; // EMPTY | ALL | ALL_PREON | SUBSET - let orderedRefs = []; - let inlineMapped = []; - let listLevelOverrides = {}; - const unorderedKeys = new Set(); - if (list && list.length) { - const { references, inlineInjections, listOverrides } = this._parseUnifiedList(list); - listLevelOverrides = listOverrides || {}; - const parsedRefs = references.map(t => this._parseComponentRefToken(t)); - const containsAll = parsedRefs.includes('ALL'); - const containsAllPreOn = parsedRefs.includes('ALL_PREON'); - if (containsAll) { - baseStrategy = 'ALL'; - // ALL 仅作为开关标识,子集重排目标为去除 ALL 后的引用列表 - orderedRefs = parsedRefs.filter(x => x && x !== 'ALL' && x !== 'ALL_PREON'); - } else if (containsAllPreOn) { - baseStrategy = 'ALL_PREON'; - // ALL_PREON:仅启用“预设里已开启”的组件,子集重排目标为去除该标记后的引用列表 - orderedRefs = parsedRefs.filter(x => x && x !== 'ALL' && x !== 'ALL_PREON'); + 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 + let orderedRefs = []; + let inlineMapped = []; + let listLevelOverrides = {}; + const unorderedKeys = new Set(); + if (list && list.length) { + const { references, inlineInjections, listOverrides } = this._parseUnifiedList(list); + listLevelOverrides = listOverrides || {}; + const parsedRefs = references.map(t => this._parseComponentRefToken(t)); + const containsAll = parsedRefs.includes('ALL'); + const containsAllPreOn = parsedRefs.includes('ALL_PREON'); + if (containsAll) { + baseStrategy = 'ALL'; + // ALL 仅作为开关标识,子集重排目标为去除 ALL 后的引用列表 + orderedRefs = parsedRefs.filter(x => x && x !== 'ALL' && x !== 'ALL_PREON'); + } else if (containsAllPreOn) { + baseStrategy = 'ALL_PREON'; + // ALL_PREON:仅启用“预设里已开启”的组件,子集重排目标为去除该标记后的引用列表 + orderedRefs = parsedRefs.filter(x => x && x !== 'ALL' && x !== 'ALL_PREON'); + } else { + baseStrategy = 'SUBSET'; + orderedRefs = parsedRefs.filter(Boolean); + } + inlineMapped = this._mapInlineInjectionsUnified(list, inlineInjections); + // 放宽:ALL 可出现在任意位置,作为“启用全部”的标志 + + // 解析 order=false:不参与重排 + for (const rawKey in listLevelOverrides) { + if (!Object.prototype.hasOwnProperty.call(listLevelOverrides, rawKey)) continue; + const k = this._parseComponentRefToken(rawKey); + if (!k) continue; + if (listLevelOverrides[rawKey] && listLevelOverrides[rawKey].order === false) unorderedKeys.add(k); + } + } + + // 3) 干跑捕获(基座) + let captured = []; + let enabledIds = []; // assembleOnly 时用于 identifier 标注 + if (baseStrategy === 'EMPTY') { + captured = []; } else { - baseStrategy = 'SUBSET'; - orderedRefs = parsedRefs.filter(Boolean); + // 不将 userInput 作为 quietText 干跑,以免把其注入到历史里 + if (baseStrategy === 'ALL') { + // 路径B:ALL 时先全开启用集合再干跑,保证真实组件尽量出现 + // 读取 promptManager 订单并构造 allow 集合 + let allow = new Set(); + try { + if (promptManager && typeof promptManager.getPromptOrderForCharacter === 'function') { + const pm = promptManager; + const activeChar = pm?.activeCharacter ?? null; + const order = pm?.getPromptOrderForCharacter(activeChar) ?? []; + 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') { + // 仅启用预设里已开启的组件 + let allow = new Set(); + try { + if (promptManager && typeof promptManager.getPromptOrderForCharacter === 'function') { + const pm = promptManager; + const activeChar = pm?.activeCharacter ?? null; + const order = pm?.getPromptOrderForCharacter(activeChar) ?? []; + 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 { + captured = await this._capturePromptMessages({ includeConfig: null, quietText: '', skipWIAN: false }); + } } - inlineMapped = this._mapInlineInjectionsUnified(list, inlineInjections); - // 放宽:ALL 可出现在任意位置,作为“启用全部”的标志 - // 解析 order=false:不参与重排 - for (const rawKey in listLevelOverrides) { - if (!Object.prototype.hasOwnProperty.call(listLevelOverrides, rawKey)) continue; - const k = this._parseComponentRefToken(rawKey); - if (!k) continue; - if (listLevelOverrides[rawKey] && listLevelOverrides[rawKey].order === false) unorderedKeys.add(k); + // 4) 依据策略计算启用集合与顺序 + 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); - // 3) 干跑捕获(基座) - let captured = []; - if (baseStrategy === 'EMPTY') { - captured = []; - } else { - // 不将 userInput 作为 quietText 干跑,以免把其注入到历史里 - if (baseStrategy === 'ALL') { - // 路径B:ALL 时先全开启用集合再干跑,保证真实组件尽量出现 - // 读取 promptManager 订单并构造 allow 集合 - let allow = new Set(); + // 5) 覆写与创建 + working = this._applyInlineOverrides(working, listLevelOverrides); + + // 6) 注入(内联 + 高级) + working = this._applyAllInjections(working, inlineMapped, options?.injections); + + // 7) 用户输入追加 + working = this._appendUserInput(working, options?.userInput); + + // 8) 调试导出 + 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.getPromptOrderForCharacter === 'function') { - const pm = promptManager; - const activeChar = pm?.activeCharacter ?? null; - const order = pm?.getPromptOrderForCharacter(activeChar) ?? []; - allow = new Set(order.map(e => e.identifier)); + 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 run = async () => await this._capturePromptMessages({ includeConfig: null, quietText: '', skipWIAN: false }); - captured = await this._withPromptEnabledSet(allow, run); - } else if (baseStrategy === 'ALL_PREON') { - // 仅启用预设里已开启的组件 - let allow = new Set(); - try { - if (promptManager && typeof promptManager.getPromptOrderForCharacter === 'function') { - const pm = promptManager; - const activeChar = pm?.activeCharacter ?? null; - const order = pm?.getPromptOrderForCharacter(activeChar) ?? []; - allow = new Set(order.filter(e => !!e?.enabled).map(e => e.identifier)); - } - } catch {} - const run = async () => await this._capturePromptMessages({ includeConfig: null, quietText: '', skipWIAN: false }); - captured = await this._withPromptEnabledSet(allow, run); - } else { - captured = await this._capturePromptMessages({ includeConfig: null, quietText: '', skipWIAN: false }); + } 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); + + }; // end executeCore + + if (presetName) { + return await this._withTemporaryPreset(presetName, executeCore); } - - // 4) 依据策略计算启用集合与顺序 - const annotateKeys = baseStrategy === 'SUBSET' ? orderedRefs : ((baseStrategy === 'ALL' || baseStrategy === 'ALL_PREON') ? orderedRefs : []); - let working = await this._annotateIdentifiersIfMissing(captured.slice(), annotateKeys); - working = this._applyOrderingStrategy(working, baseStrategy, orderedRefs, unorderedKeys); - - // 5) 覆写与创建 - working = this._applyInlineOverrides(working, listLevelOverrides); - - // 6) 注入(内联 + 高级) - working = this._applyAllInjections(working, inlineMapped, options?.injections); - - // 7) 用户输入追加 - working = this._appendUserInput(working, options?.userInput); - - // 8) 调试导出 - this._exportDebugData({ sourceWindow, requestId, working, baseStrategy, orderedRefs, inlineMapped, listLevelOverrides, debug: options?.debug, targetOrigin }); - - // 9) 发送 - return await this._sendMessages(working, { ...options, debug: { ...(options?.debug || {}), _exported: true } }, requestId, sourceWindow, targetOrigin); + 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,110 +1432,143 @@ class CallGenerateService { injections: (debug?.injections || []).concat(inlineMapped || []), overrides: listLevelOverrides || null, }; - this.postToTarget(sourceWindow, 'blueprint', bp, targetOrigin); - } catch {} + this.postToTarget(sourceWindow, 'blueprint', bp, targetOrigin); + } catch { } } } /** * 入口:处理 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) { const s = this.sessions.get(this.normalizeSessionId(sessionId)); - try { s?.abortController?.abort(); } catch {} + try { s?.abortController?.abort(); } catch { } } /** 清理所有会话 */ cleanup() { - this.sessions.forEach(s => { try { s.abortController?.abort(); } catch {} }); + this.sessions.forEach(s => { try { s.abortController?.abort(); } catch { } }); this.sessions.clear(); } } 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 }); - try { initCallGenerateHostBridge(); } catch (e) {} + try { initCallGenerateHostBridge(); } catch (e) { } try { window.addEventListener('xiaobaixEnabledChanged', (e) => { try { const enabled = e && e.detail && e.detail.enabled === true; if (enabled) initCallGenerateHostBridge(); else cleanupCallGenerateHostBridge(); - } catch (_) {} + } catch (_) { } }); document.addEventListener('xiaobaixEnabledChanged', (e) => { try { const enabled = e && e.detail && e.detail.enabled === true; if (enabled) initCallGenerateHostBridge(); else cleanupCallGenerateHostBridge(); - } catch (_) {} + } catch (_) { } }); - window.addEventListener('beforeunload', () => { try { cleanupCallGenerateHostBridge(); } catch (_) {} }); - } catch (_) {} + window.addEventListener('beforeunload', () => { try { cleanupCallGenerateHostBridge(); } catch (_) { } }); + } catch (_) { } // ===== 全局 API 暴露:与 iframe 调用方式完全一致 ===== // 创建命名空间 window.LittleWhiteBox = window.LittleWhiteBox || {}; - + /** * 全局 callGenerate 函数 * 使用方式与 iframe 中完全一致:await window.callGenerate(options) @@ -1479,22 +1593,22 @@ if (typeof window !== 'undefined') { * api: { inherit: true } * }); */ - window.LittleWhiteBox.callGenerate = async function(options) { + window.LittleWhiteBox.callGenerate = async function (options) { return new Promise((resolve, reject) => { const requestId = `global-${Date.now()}-${Math.random().toString(36).slice(2)}`; const streamingEnabled = options?.streaming?.enabled !== false; - + // 处理流式回调 let onChunkCallback = null; if (streamingEnabled && typeof options?.streaming?.onChunk === 'function') { onChunkCallback = options.streaming.onChunk; } - + // 监听响应 const listener = (event) => { const data = event.data; if (!data || data.source !== SOURCE_TAG || data.id !== requestId) return; - + if (data.type === 'generateStreamChunk' && onChunkCallback) { // 流式文本块回调 try { @@ -1513,10 +1627,10 @@ if (typeof window !== 'undefined') { reject(data.error); } }; - - // 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 => { window.removeEventListener('message', listener); @@ -1524,22 +1638,73 @@ 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' 等) */ - window.LittleWhiteBox.callGenerate.cancel = function(sessionId) { + window.LittleWhiteBox.callGenerate.cancel = function (sessionId) { callGenerateService.cancel(sessionId); }; - + /** * 清理所有会话 */ - window.LittleWhiteBox.callGenerate.cleanup = function() { + window.LittleWhiteBox.callGenerate.cleanup = function () { 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 }; -} +} \ No newline at end of file diff --git a/bridges/context-bridge.js b/bridges/context-bridge.js new file mode 100644 index 0000000..c5f314d --- /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 { } +} \ No newline at end of file diff --git a/index.js b/index.js index f8b322b..44ab8d6 100644 --- a/index.js +++ b/index.js @@ -55,7 +55,7 @@ if (settings.dynamicPrompt && !settings.fourthWall) settings.fourthWall = settin const DEPRECATED_KEYS = [ 'characterUpdater', - 'promptSections', + 'promptSections', 'promptPresets', 'relationshipGuidelines', 'scriptAssistant' @@ -64,7 +64,7 @@ const DEPRECATED_KEYS = [ function cleanupDeprecatedData() { const s = extension_settings[EXT_ID]; if (!s) return; - + let cleaned = false; for (const key of DEPRECATED_KEYS) { if (key in s) { @@ -73,7 +73,7 @@ function cleanupDeprecatedData() { console.log(`[LittleWhiteBox] Cleaned deprecated data: ${key}`); } } - + if (cleaned) { saveSettingsDebounced(); console.log('[LittleWhiteBox] Deprecated data cleanup complete'); @@ -194,13 +194,13 @@ function addUpdateDownloadButton() { totalSwitchDivider.style.display = 'flex'; totalSwitchDivider.style.alignItems = 'center'; totalSwitchDivider.style.justifyContent = 'flex-start'; - } catch (e) {} + } catch (e) { } totalSwitchDivider.appendChild(updateButton); try { if (window.setupUpdateButtonInSettings) { window.setupUpdateButtonInSettings(); } - } catch (e) {} + } catch (e) { } } function removeAllUpdateNotices() { @@ -218,7 +218,7 @@ async function performExtensionUpdateCheck() { if (versionData && versionData.isUpToDate === false) { updateExtensionHeaderWithUpdateNotice(); } - } catch (error) {} + } catch (error) { } } function registerModuleCleanup(moduleName, cleanupFunction) { @@ -228,26 +228,26 @@ function registerModuleCleanup(moduleName, cleanupFunction) { function removeSkeletonStyles() { try { document.querySelectorAll('.xiaobaix-skel').forEach(el => { - try { el.remove(); } catch (e) {} + try { el.remove(); } catch (e) { } }); document.getElementById('xiaobaix-skeleton-style')?.remove(); - } catch (e) {} + } catch (e) { } } function cleanupAllResources() { try { EventCenter.cleanupAll(); - } catch (e) {} - try { window.xbDebugPanelClose?.(); } catch (e) {} + } catch (e) { } + try { window.xbDebugPanelClose?.(); } catch (e) { } moduleCleanupFunctions.forEach((cleanupFn) => { try { cleanupFn(); - } catch (e) {} + } catch (e) { } }); moduleCleanupFunctions.clear(); try { cleanupRenderer(); - } catch (e) {} + } catch (e) { } document.querySelectorAll('.memory-button, .mes_history_preview').forEach(btn => btn.remove()); document.querySelectorAll('#message_preview_btn').forEach(btn => { if (btn instanceof HTMLElement) { @@ -295,11 +295,11 @@ function toggleSettingsControls(enabled) { async function toggleAllFeatures(enabled) { if (enabled) { toggleSettingsControls(true); - try { window.XB_applyPrevStates && window.XB_applyPrevStates(); } catch (e) {} + try { window.XB_applyPrevStates && window.XB_applyPrevStates(); } catch (e) { } saveSettingsDebounced(); initRenderer(); - try { initVarCommands(); } catch (e) {} - try { initVareventEditor(); } catch (e) {} + try { initVarCommands(); } catch (e) { } + try { initVareventEditor(); } catch (e) { } if (extension_settings[EXT_ID].tasks?.enabled) { await initTasks(); } @@ -327,29 +327,29 @@ async function toggleAllFeatures(enabled) { try { if (isXiaobaixEnabled && settings.wrapperIframe && !document.getElementById('xb-callgen')) document.head.appendChild(Object.assign(document.createElement('script'), { id: 'xb-callgen', type: 'module', src: `${extensionFolderPath}/bridges/call-generate-service.js` })); - } catch (e) {} + } catch (e) { } try { if (isXiaobaixEnabled && !document.getElementById('xb-worldbook')) document.head.appendChild(Object.assign(document.createElement('script'), { id: 'xb-worldbook', type: 'module', src: `${extensionFolderPath}/bridges/worldbook-bridge.js` })); - } catch (e) {} + } catch (e) { } document.dispatchEvent(new CustomEvent('xiaobaixEnabledChanged', { detail: { enabled: true } })); $(document).trigger('xiaobaix:enabled:toggle', [true]); } else { - try { window.XB_captureAndStoreStates && window.XB_captureAndStoreStates(); } catch (e) {} + try { window.XB_captureAndStoreStates && window.XB_captureAndStoreStates(); } catch (e) { } cleanupAllResources(); - if (window.messagePreviewCleanup) try { window.messagePreviewCleanup(); } catch (e) {} - if (window.fourthWallCleanup) try { window.fourthWallCleanup(); } catch (e) {} - if (window.buttonCollapseCleanup) try { window.buttonCollapseCleanup(); } catch (e) {} - try { cleanupVariablesPanel(); } catch (e) {} - try { cleanupVariablesCore(); } catch (e) {} - try { cleanupVarCommands(); } catch (e) {} - try { cleanupVareventEditor(); } catch (e) {} - try { cleanupNovelDraw(); } catch (e) {} - try { cleanupTts(); } catch (e) {} - try { clearBlobCaches(); } catch (e) {} + if (window.messagePreviewCleanup) try { window.messagePreviewCleanup(); } catch (e) { } + if (window.fourthWallCleanup) try { window.fourthWallCleanup(); } catch (e) { } + if (window.buttonCollapseCleanup) try { window.buttonCollapseCleanup(); } catch (e) { } + try { cleanupVariablesPanel(); } catch (e) { } + try { cleanupVariablesCore(); } catch (e) { } + try { cleanupVarCommands(); } catch (e) { } + try { cleanupVareventEditor(); } catch (e) { } + try { cleanupNovelDraw(); } catch (e) { } + try { cleanupTts(); } catch (e) { } + try { clearBlobCaches(); } catch (e) { } toggleSettingsControls(false); - try { window.cleanupWorldbookHostBridge && window.cleanupWorldbookHostBridge(); document.getElementById('xb-worldbook')?.remove(); } catch (e) {} - try { window.cleanupCallGenerateHostBridge && window.cleanupCallGenerateHostBridge(); document.getElementById('xb-callgen')?.remove(); } catch (e) {} + try { window.cleanupWorldbookHostBridge && window.cleanupWorldbookHostBridge(); document.getElementById('xb-worldbook')?.remove(); } catch (e) { } + try { window.cleanupCallGenerateHostBridge && window.cleanupCallGenerateHostBridge(); document.getElementById('xb-callgen')?.remove(); } catch (e) { } document.dispatchEvent(new CustomEvent('xiaobaixEnabledChanged', { detail: { enabled: false } })); $(document).trigger('xiaobaix:enabled:toggle', [false]); } @@ -398,13 +398,13 @@ async function setupSettings() { if (!isXiaobaixEnabled) return; const enabled = $(this).prop('checked'); if (!enabled && key === 'fourthWall') { - try { fourthWallCleanup(); } catch (e) {} + try { fourthWallCleanup(); } catch (e) { } } if (!enabled && key === 'novelDraw') { - try { cleanupNovelDraw(); } catch (e) {} + try { cleanupNovelDraw(); } catch (e) { } } if (!enabled && key === 'tts') { - try { cleanupTts(); } catch (e) {} + try { cleanupTts(); } catch (e) { } } settings[key] = extension_settings[EXT_ID][key] || {}; settings[key].enabled = enabled; @@ -463,7 +463,7 @@ async function setupSettings() { settings.wrapperIframe ? (!document.getElementById('xb-callgen') && document.head.appendChild(Object.assign(document.createElement('script'), { id: 'xb-callgen', type: 'module', src: `${extensionFolderPath}/bridges/call-generate-service.js` }))) : (window.cleanupCallGenerateHostBridge && window.cleanupCallGenerateHostBridge(), document.getElementById('xb-callgen')?.remove()); - } catch (e) {} + } catch (e) { } }); $("#xiaobaix_render_enabled").prop("checked", settings.renderEnabled !== false).on("change", async function () { @@ -474,7 +474,7 @@ async function setupSettings() { if (!settings.renderEnabled && wasEnabled) { cleanupRenderer(); } else if (settings.renderEnabled && !wasEnabled) { - initRenderer(); + initRenderer(); setTimeout(() => processExistingMessages(), 100); } }); @@ -494,7 +494,7 @@ async function setupSettings() { $(this).val(v); settings.maxRenderedMessages = v; saveSettingsDebounced(); - try { shrinkRenderedWindowFull(); } catch (e) {} + try { shrinkRenderedWindowFull(); } catch (e) { } }); $(document).off('click.xbreset', '#xiaobaix_reset_btn').on('click.xbreset', '#xiaobaix_reset_btn', function (e) { @@ -519,16 +519,16 @@ async function setupSettings() { const el = document.getElementById(id); if (el) { el.checked = !!val; - try { $(el).trigger('change'); } catch {} + try { $(el).trigger('change'); } catch { } } } ON.forEach(k => setChecked(MAP[k], true)); OFF.forEach(k => setChecked(MAP[k], false)); setChecked('xiaobaix_use_blob', false); setChecked('Wrapperiframe', true); - try { saveSettingsDebounced(); } catch (e) {} + try { saveSettingsDebounced(); } catch (e) { } }); - } catch (err) {} + } catch (err) { } } function setupDebugButtonInSettings() { @@ -555,7 +555,7 @@ function setupDebugButtonInSettings() { try { const mod = await import('./modules/debug-panel/debug-panel.js'); if (mod?.toggleDebugPanel) await mod.toggleDebugPanel(); - } catch (e) {} + } catch (e) { } }; btn.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); onActivate(); }); btn.addEventListener('keydown', (e) => { @@ -563,7 +563,7 @@ function setupDebugButtonInSettings() { }); row.appendChild(btn); - } catch (e) {} + } catch (e) { } } function setupMenuTabs() { @@ -608,7 +608,7 @@ jQuery(async () => { await setupSettings(); - try { initControlAudio(); } catch (e) {} + try { initControlAudio(); } catch (e) { } if (isXiaobaixEnabled) { initRenderer(); @@ -617,20 +617,25 @@ jQuery(async () => { try { if (isXiaobaixEnabled && settings.wrapperIframe && !document.getElementById('xb-callgen')) document.head.appendChild(Object.assign(document.createElement('script'), { id: 'xb-callgen', type: 'module', src: `${extensionFolderPath}/bridges/call-generate-service.js` })); - } catch (e) {} + } catch (e) { } try { if (isXiaobaixEnabled && !document.getElementById('xb-worldbook')) document.head.appendChild(Object.assign(document.createElement('script'), { id: 'xb-worldbook', type: 'module', src: `${extensionFolderPath}/bridges/worldbook-bridge.js` })); - } catch (e) {} + } 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); }); if (isXiaobaixEnabled) { - try { initVarCommands(); } catch (e) {} - try { initVareventEditor(); } catch (e) {} + try { initVarCommands(); } catch (e) { } + try { initVareventEditor(); } catch (e) { } if (settings.tasks?.enabled) { try { await initTasks(); } catch (e) { console.error('[Tasks] Init failed:', e); } @@ -665,7 +670,7 @@ jQuery(async () => { setInterval(() => { if (isXiaobaixEnabled) processExistingMessages(); }, 30000); - } catch (err) {} + } catch (err) { } }); -export { executeSlashCommand }; +export { executeSlashCommand }; \ No newline at end of file diff --git a/modules/story-summary/story-summary.js b/modules/story-summary/story-summary.js index 4e51394..9787b41 100644 --- a/modules/story-summary/story-summary.js +++ b/modules/story-summary/story-summary.js @@ -1028,6 +1028,70 @@ function buildFramePayload(store) { }; } +function parseRelationTargetFromPredicate(predicate) { + const text = String(predicate || "").trim(); + if (!text.startsWith("对")) return null; + const idx = text.indexOf("的", 1); + if (idx <= 1) return null; + return text.slice(1, idx).trim() || null; +} + +function isRelationFactLike(fact) { + if (!fact || fact.retracted) return false; + return !!parseRelationTargetFromPredicate(fact.p); +} + +function getNextFactIdValue(facts) { + let max = 0; + for (const fact of facts || []) { + const match = String(fact?.id || "").match(/^f-(\d+)$/); + if (match) max = Math.max(max, Number.parseInt(match[1], 10) || 0); + } + return max + 1; +} + +function mergeCharacterRelationshipsIntoFacts(existingFacts, relationships, floorHint = 0) { + const safeFacts = Array.isArray(existingFacts) ? existingFacts : []; + const safeRels = Array.isArray(relationships) ? relationships : []; + + const nonRelationFacts = safeFacts.filter((f) => !isRelationFactLike(f)); + const oldRelationByKey = new Map(); + + for (const fact of safeFacts) { + const to = parseRelationTargetFromPredicate(fact?.p); + const from = String(fact?.s || "").trim(); + if (!from || !to) continue; + oldRelationByKey.set(`${from}->${to}`, fact); + } + + let nextFactId = getNextFactIdValue(safeFacts); + const newRelationFacts = []; + + for (const rel of safeRels) { + const from = String(rel?.from || "").trim(); + const to = String(rel?.to || "").trim(); + if (!from || !to) continue; + + const key = `${from}->${to}`; + const oldFact = oldRelationByKey.get(key); + const label = String(rel?.label || "").trim() || "未知"; + const trend = String(rel?.trend || "").trim() || "陌生"; + const id = oldFact?.id || `f-${nextFactId++}`; + + newRelationFacts.push({ + id, + s: from, + p: oldFact?.p || `对${to}的关系`, + o: label, + trend, + since: oldFact?.since ?? floorHint, + _addedAt: oldFact?._addedAt ?? floorHint, + }); + } + + return [...nonRelationFacts, ...newRelationFacts]; +} + function openPanelForMessage(mesId) { createOverlay(); showOverlay(); @@ -1368,6 +1432,11 @@ async function handleFrameMessage(event) { if (VALID_SECTIONS.includes(data.section)) { store.json[data.section] = data.data; } + if (data.section === "characters") { + const rels = data?.data?.relationships || []; + const floorHint = Math.max(0, Number(store.lastSummarizedMesId) || 0); + store.json.facts = mergeCharacterRelationshipsIntoFacts(store.json.facts, rels, floorHint); + } store.updatedAt = Date.now(); saveSummaryStore();