diff --git a/bridges/call-generate-service.js b/bridges/call-generate-service.js index a4d0b29..677cef7 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 } 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 干跑捕获与组件切换 ===== @@ -1008,7 +1008,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 +1132,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 +1143,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 +1155,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 +1163,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,17 +1173,17 @@ 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); @@ -1226,6 +1226,7 @@ class CallGenerateService { // 3) 干跑捕获(基座) let captured = []; + let enabledIds = []; // assembleOnly 时用于 identifier 标注 if (baseStrategy === 'EMPTY') { captured = []; } else { @@ -1242,6 +1243,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 +1257,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 +1266,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 +1284,35 @@ 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); } _applyOrderingStrategy(messages, baseStrategy, orderedRefs, unorderedKeys) { @@ -1340,9 +1372,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 +1383,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 +1391,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 +1426,63 @@ 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 === '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 +1566,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 +1577,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' 等) @@ -1547,4 +1642,4 @@ if (typeof window !== 'undefined') { init: initCallGenerateHostBridge, cleanup: cleanupCallGenerateHostBridge }; -} +}