fix: persist story-summary relationships and sync local changes

This commit is contained in:
2026-02-24 15:26:21 +08:00
parent 8dc7ba5fae
commit 1f3880d1d9
4 changed files with 789 additions and 257 deletions

View File

@@ -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') {
// 路径BALL 时先全开启用集合再干跑,保证真实组件尽量出现
// 读取 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') {
// 路径BALL 时先全开启用集合再干跑,保证真实组件尽量出现
// 读取 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<Array<{role: string, content: string}>>} 组装后的 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
};
}
}