diff --git a/call-generate-service.js b/call-generate-service.js deleted file mode 100644 index a4d0b29..0000000 --- a/call-generate-service.js +++ /dev/null @@ -1,1550 +0,0 @@ -// @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 '*'; } -}; - -// @ts-nocheck -class CallGenerateService { - constructor() { - /** @type {Map} */ - this.sessions = new Map(); - this._toggleBusy = false; - this._lastToggleSnapshot = null; - this._toggleQueue = Promise.resolve(); - } - - // ===== 通用错误处理 ===== - normalizeError(err, fallbackCode = 'API_ERROR', details = null) { - try { - if (!err) return { code: fallbackCode, message: 'Unknown error', details }; - if (typeof err === 'string') return { code: fallbackCode, message: err, details }; - const msg = err?.message || String(err); - // Map known cases - if (msg === 'INVALID_OPTIONS') return { code: 'INVALID_OPTIONS', message: 'Invalid options', details }; - if (msg === 'MISSING_MESSAGES') return { code: 'MISSING_MESSAGES', message: 'Missing messages', details }; - if (msg === 'INVALID_COMPONENT_REF') return { code: 'INVALID_COMPONENT_REF', message: 'Invalid component reference', details }; - if (msg === 'AMBIGUOUS_COMPONENT_NAME') return { code: 'AMBIGUOUS_COMPONENT_NAME', message: 'Ambiguous component name', details }; - if (msg === 'Unsupported provider') return { code: 'PROVIDER_UNSUPPORTED', message: msg, details }; - if (err?.name === 'AbortError') return { code: 'CANCELLED', message: 'Request cancelled', details }; - return { code: fallbackCode, message: msg, details }; - } catch { - return { code: fallbackCode, message: 'Error serialization failed', details }; - } - } - - 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 - * @returns {string} - */ - normalizeSessionId(rawId) { - if (!rawId) return 'xb1'; - const m = String(rawId).match(/^xb(\d{1,2})$/i); - if (m) { - const n = Math.max(1, Math.min(10, Number(m[1]) || 1)); - return `xb${n}`; - } - const n = Math.max(1, Math.min(10, parseInt(String(rawId), 10) || 1)); - return `xb${n}`; - } - - /** - * @param {string} sessionId - */ - ensureSession(sessionId) { - const id = this.normalizeSessionId(sessionId); - if (!this.sessions.has(id)) { - this.sessions.set(id, { - id, - abortController: new AbortController(), - accumulated: '', - startedAt: Date.now(), - }); - } - return this.sessions.get(id); - } - - /** - * 选项校验(宽松)。 - * 支持仅 injections 或仅 userInput 构建场景。 - * @param {Object} options - * @throws {Error} INVALID_OPTIONS 当 options 非对象 - */ - validateOptions(options) { - if (!options || typeof options !== 'object') throw new Error('INVALID_OPTIONS'); - // 允许仅凭 injections 或 userInput 构建 - const hasComponents = options.components && Array.isArray(options.components.list); - const hasInjections = Array.isArray(options.injections) && options.injections.length > 0; - const hasUserInput = typeof options.userInput === 'string' && options.userInput.length >= 0; - if (!hasComponents && !hasInjections && !hasUserInput) { - // 仍允许空配置,但会构建空 + userInput - return; - } - } - - /** - * @param {string} provider - */ - mapProviderToSource(provider) { - const p = String(provider || '').toLowerCase(); - const map = { - openai: chat_completion_sources.OPENAI, - claude: chat_completion_sources.CLAUDE, - gemini: chat_completion_sources.MAKERSUITE, - google: chat_completion_sources.MAKERSUITE, - vertexai: chat_completion_sources.VERTEXAI, - cohere: chat_completion_sources.COHERE, - deepseek: chat_completion_sources.DEEPSEEK, - xai: chat_completion_sources.XAI, - groq: chat_completion_sources.GROQ, - openrouter: chat_completion_sources.OPENROUTER, - custom: chat_completion_sources.CUSTOM, - }; - return map[p] || null; - } - - /** - * 解析 API 与模型的继承/覆写,并注入代理/自定义地址 - * @param {any} api - */ - resolveApiConfig(api) { - const inherit = api?.inherit !== false; - let source = oai_settings?.chat_completion_source; - let model = getChatCompletionModel ? getChatCompletionModel() : undefined; - let overrides = api?.overrides || {}; - - if (!inherit) { - if (api?.provider) source = this.mapProviderToSource(api.provider); - if (api?.model) model = api.model; - } else { - if (overrides?.provider) source = this.mapProviderToSource(overrides.provider); - if (overrides?.model) model = overrides.model; - } - - if (!source) throw new Error(`Unsupported provider`); - if (!model) throw new Error('Model not specified'); - - const temperature = inherit ? Number(oai_settings?.temp_openai ?? '') : undefined; - const max_tokens = inherit ? (Number(oai_settings?.openai_max_tokens ?? 0) || 1024) : undefined; - const top_p = inherit ? Number(oai_settings?.top_p_openai ?? '') : undefined; - const frequency_penalty = inherit ? Number(oai_settings?.freq_pen_openai ?? '') : undefined; - const presence_penalty = inherit ? Number(oai_settings?.pres_pen_openai ?? '') : undefined; - - const resolved = { - chat_completion_source: source, - model, - temperature, - max_tokens, - top_p, - frequency_penalty, - presence_penalty, - // 代理/自定义地址占位 - reverse_proxy: undefined, - proxy_password: undefined, - custom_url: undefined, - custom_include_body: undefined, - custom_exclude_body: undefined, - custom_include_headers: undefined, - }; - - // 继承代理/自定义配置 - if (inherit) { - const proxySupported = new Set([ - chat_completion_sources.CLAUDE, - chat_completion_sources.OPENAI, - chat_completion_sources.MISTRALAI, - chat_completion_sources.MAKERSUITE, - chat_completion_sources.VERTEXAI, - chat_completion_sources.DEEPSEEK, - chat_completion_sources.XAI, - ]); - if (proxySupported.has(source) && oai_settings?.reverse_proxy) { - resolved.reverse_proxy = String(oai_settings.reverse_proxy).replace(/\/?$/, ''); - if (oai_settings?.proxy_password) resolved.proxy_password = String(oai_settings.proxy_password); - } - if (source === chat_completion_sources.CUSTOM) { - if (oai_settings?.custom_url) resolved.custom_url = String(oai_settings.custom_url); - if (oai_settings?.custom_include_body) resolved.custom_include_body = oai_settings.custom_include_body; - if (oai_settings?.custom_exclude_body) resolved.custom_exclude_body = oai_settings.custom_exclude_body; - if (oai_settings?.custom_include_headers) resolved.custom_include_headers = oai_settings.custom_include_headers; - } - } - - // 显式 baseURL 覆写 - const baseURL = overrides?.baseURL || api?.baseURL; - if (baseURL) { - if (resolved.chat_completion_source === chat_completion_sources.CUSTOM) { - resolved.custom_url = String(baseURL); - } else { - resolved.reverse_proxy = String(baseURL).replace(/\/?$/, ''); - } - } - - const ovw = inherit ? (api?.overrides || {}) : api || {}; - ['temperature', 'maxTokens', 'topP', 'topK', 'frequencyPenalty', 'presencePenalty', 'repetitionPenalty', 'stop', 'responseFormat', 'seed'] - .forEach((k) => { - const keyMap = { - maxTokens: 'max_tokens', - topP: 'top_p', - topK: 'top_k', - frequencyPenalty: 'frequency_penalty', - presencePenalty: 'presence_penalty', - repetitionPenalty: 'repetition_penalty', - responseFormat: 'response_format', - }; - const targetKey = keyMap[k] || k; - if (ovw[k] !== undefined) resolved[targetKey] = ovw[k]; - }); - - return resolved; - } - - /** - * @param {any[]} messages - * @param {any} apiCfg - * @param {boolean} stream - */ - buildChatPayload(messages, apiCfg, stream) { - const payload = { - stream: !!stream, - messages, - model: apiCfg.model, - chat_completion_source: apiCfg.chat_completion_source, - max_tokens: apiCfg.max_tokens, - temperature: apiCfg.temperature, - top_p: apiCfg.top_p, - top_k: apiCfg.top_k, - frequency_penalty: apiCfg.frequency_penalty, - presence_penalty: apiCfg.presence_penalty, - repetition_penalty: apiCfg.repetition_penalty, - stop: Array.isArray(apiCfg.stop) ? apiCfg.stop : undefined, - response_format: apiCfg.response_format, - seed: apiCfg.seed, - // 代理/自定义地址透传 - reverse_proxy: apiCfg.reverse_proxy, - proxy_password: apiCfg.proxy_password, - custom_url: apiCfg.custom_url, - custom_include_body: apiCfg.custom_include_body, - custom_exclude_body: apiCfg.custom_exclude_body, - custom_include_headers: apiCfg.custom_include_headers, - }; - return ChatCompletionService.createRequestData(payload); - } - - /** - * @param {Window} target - * @param {string} type - * @param {object} body - */ - postToTarget(target, type, body, targetOrigin = null) { - try { - target?.postMessage({ source: SOURCE_TAG, type, ...body }, resolveTargetOrigin(targetOrigin)); - } catch (e) {} - } - - // ===== ST Prompt 干跑捕获与组件切换 ===== - - _computeEnableIds(includeConfig) { - const ids = new Set(); - if (!includeConfig || typeof includeConfig !== 'object') return ids; - const c = includeConfig; - if (c.chatHistory?.enabled) ids.add('chatHistory'); - if (c.worldInfo?.enabled || c.worldInfo?.beforeHistory || c.worldInfo?.afterHistory) { - if (c.worldInfo?.beforeHistory !== false) ids.add('worldInfoBefore'); - if (c.worldInfo?.afterHistory !== false) ids.add('worldInfoAfter'); - } - if (c.character?.description) ids.add('charDescription'); - if (c.character?.personality) ids.add('charPersonality'); - if (c.character?.scenario) ids.add('scenario'); - if (c.persona?.description) ids.add('personaDescription'); - return ids; - } - - async _withTemporaryPromptToggles(includeConfig, fn) { return await this._withPromptToggle({ includeConfig }, fn); } - - async _capturePromptMessages({ includeConfig = null, quietText = '', skipWIAN = false }) { - const ctx = getContext(); - /** @type {any} */ - let capturedData = null; - const listener = (data) => { - if (data && typeof data === 'object' && Array.isArray(data.prompt)) { - capturedData = { ...data, prompt: data.prompt.slice() }; - } else if (Array.isArray(data)) { - capturedData = data.slice(); - } - }; - eventSource.on(event_types.GENERATE_AFTER_DATA, listener); - try { - const run = async () => { - await ctx.generate('normal', { quiet_prompt: String(quietText || ''), quietToLoud: false, skipWIAN, force_name2: true }, true); - }; - if (includeConfig) { - await this._withTemporaryPromptToggles(includeConfig, run); - } else { - await run(); - } - } finally { - eventSource.removeListener(event_types.GENERATE_AFTER_DATA, listener); - } - if (!capturedData) return []; - if (capturedData && typeof capturedData === 'object' && Array.isArray(capturedData.prompt)) return capturedData.prompt.slice(); - if (Array.isArray(capturedData)) return capturedData.slice(); - return []; - } - - /** 使用 identifier 集合进行临时启停捕获 */ - async _withPromptEnabledSet(enableSet, fn) { return await this._withPromptToggle({ enableSet }, fn); } - - /** 统一启停切换:支持 includeConfig(标识集)或 enableSet(组件键集合) */ - async _withPromptToggle({ includeConfig = null, enableSet = null } = {}, fn) { - if (!promptManager || typeof promptManager.getPromptOrderForCharacter !== 'function') { - return await fn(); - } - // 使用队列保证串行执行,避免忙等 - const runExclusive = async () => { - this._toggleBusy = true; - let snapshot = []; - try { - const pm = promptManager; - const activeChar = pm?.activeCharacter ?? null; - const order = pm?.getPromptOrderForCharacter(activeChar) ?? []; - snapshot = order.map(e => ({ identifier: e.identifier, enabled: !!e.enabled })); - this._lastToggleSnapshot = snapshot.map(s => ({ ...s })); - - if (includeConfig) { - const enableIds = this._computeEnableIds(includeConfig); - order.forEach(e => { e.enabled = enableIds.has(e.identifier); }); - } else if (enableSet) { - const allow = enableSet instanceof Set ? enableSet : new Set(enableSet); - order.forEach(e => { - let ok = false; - for (const k of allow) { if (this._identifierMatchesKey(e.identifier, k)) { ok = true; break; } } - e.enabled = ok; - }); - } - - return await fn(); - } finally { - try { - const pm = promptManager; - const activeChar = pm?.activeCharacter ?? null; - 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 {} - this._toggleBusy = false; - this._lastToggleSnapshot = null; - } - }; - this._toggleQueue = this._toggleQueue.then(runExclusive, runExclusive); - return await this._toggleQueue; - } - - async _captureWithEnabledSet(enableSet, quietText = '', skipWIAN = false) { - const ctx = getContext(); - /** @type {any} */ - let capturedData = null; - const listener = (data) => { - if (data && typeof data === 'object' && Array.isArray(data.prompt)) { - capturedData = { ...data, prompt: data.prompt.slice() }; - } else if (Array.isArray(data)) { - capturedData = data.slice(); - } - }; - eventSource.on(event_types.GENERATE_AFTER_DATA, listener); - try { - await this._withPromptToggle({ enableSet }, async () => { - await ctx.generate('normal', { quiet_prompt: String(quietText || ''), quietToLoud: false, skipWIAN, force_name2: true }, true); - }); - } finally { - eventSource.removeListener(event_types.GENERATE_AFTER_DATA, listener); - } - if (!capturedData) return []; - if (capturedData && typeof capturedData === 'object' && Array.isArray(capturedData.prompt)) return capturedData.prompt.slice(); - if (Array.isArray(capturedData)) return capturedData.slice(); - return []; - } - - // ===== 工具函数:组件与消息辅助 ===== - - /** - * 获取消息的 component key(用于匹配与排序)。 - * chatHistory-* 归并为 chatHistory;dialogueExamples x-y 归并为 dialogueExamples。 - * @param {string} identifier - * @returns {string} - */ - _getComponentKeyFromIdentifier(identifier) { - const id = String(identifier || ''); - if (id.startsWith('chatHistory')) return 'chatHistory'; - if (id.startsWith('dialogueExamples')) return 'dialogueExamples'; - return id; - } - - /** - * 判断具体 identifier 是否匹配某组件 key(处理聚合键)。 - * @param {string} identifier - * @param {string} key - * @returns {boolean} - */ - _identifierMatchesKey(identifier, key) { - const id = String(identifier || ''); - const k = String(key || ''); - if (!k || !id) return false; - if (k === 'dialogueExamples') return id.startsWith('dialogueExamples'); - if (k === 'worldInfo') return id === 'worldInfoBefore' || id === 'worldInfoAfter'; - if (k === 'chatHistory') return id === 'chatHistory' || id.startsWith('chatHistory'); - return id === k; - } - - /** 将组件键映射到创建锚点与角色,并生成稳定 identifier */ - _mapCreateAnchorForKey(key) { - const k = String(key || ''); - const sys = { position: POSITIONS.IN_PROMPT, role: 'system' }; - const asst = { position: POSITIONS.IN_PROMPT, role: 'assistant' }; - if (k === 'bias') return { ...asst, identifier: 'bias' }; - if (k === 'worldInfo' || k === 'worldInfoBefore') return { ...sys, identifier: 'worldInfoBefore' }; - if (k === 'worldInfoAfter') return { ...sys, identifier: 'worldInfoAfter' }; - if (k === 'charDescription') return { ...sys, identifier: 'charDescription' }; - if (k === 'charPersonality') return { ...sys, identifier: 'charPersonality' }; - if (k === 'scenario') return { ...sys, identifier: 'scenario' }; - if (k === 'personaDescription') return { ...sys, identifier: 'personaDescription' }; - if (k === 'quietPrompt') return { ...sys, identifier: 'quietPrompt' }; - if (k === 'impersonate') return { ...sys, identifier: 'impersonate' }; - if (k === 'authorsNote') return { ...sys, identifier: 'authorsNote' }; - if (k === 'vectorsMemory') return { ...sys, identifier: 'vectorsMemory' }; - if (k === 'vectorsDataBank') return { ...sys, identifier: 'vectorsDataBank' }; - if (k === 'smartContext') return { ...sys, identifier: 'smartContext' }; - if (k === 'summary') return { ...sys, identifier: 'summary' }; - if (k === 'dialogueExamples') return { ...sys, identifier: 'dialogueExamples 0-0' }; - // 默认走 system+IN_PROMPT,并使用 key 作为 identifier - return { ...sys, identifier: k }; - } - - /** - * 将 name 解析为唯一 identifier。 - * 规则: - * 1) 先快速命中已知原生键(直接返回同名 identifier) - * 2) 扫描 PromptManager 的“订单列表”和“集合”,按 name/label/title 精确匹配(大小写不敏感),唯一命中返回其 identifier - * 3) 失败时做一步 sanitize 对比(将非单词字符转为下划线) - * 4) 多命中抛出 AMBIGUOUS_COMPONENT_NAME,零命中返回 null - */ - _resolveNameToIdentifier(rawName) { - try { - const nm = String(rawName || '').trim(); - if (!nm) return null; - - // 1) 原生与常见聚合键的快速命中(支持用户用 name 指代这些键) - if (KNOWN_KEYS.has(nm)) return nm; - - const eq = (a, b) => String(a || '').trim() === String(b || '').trim(); - const sanitize = (s) => String(s || '').replace(/\W/g, '_'); - - const matches = new Set(); - - // 缓存命中 - try { - const nameCache = this._getNameCache(); - if (nameCache.has(nm)) return nameCache.get(nm); - } catch {} - - // 2) 扫描 PromptManager 的订单(显示用) - try { - if (promptManager && typeof promptManager.getPromptOrderForCharacter === 'function') { - const pm = promptManager; - const activeChar = pm?.activeCharacter ?? null; - const order = pm.getPromptOrderForCharacter(activeChar) || []; - for (const e of order) { - const id = e?.identifier; - if (!id) continue; - const candidates = [e?.name, e?.label, e?.title, id].filter(Boolean); - if (candidates.some(x => eq(x, nm))) { - matches.add(id); - continue; - } - } - } - } catch {} - - // 3) 扫描 Prompt 集合(运行期合并后的集合) - try { - if (promptManager && typeof promptManager.getPromptCollection === 'function') { - const pc = promptManager.getPromptCollection(); - const coll = pc?.collection || []; - for (const p of coll) { - const id = p?.identifier; - if (!id) continue; - const candidates = [p?.name, p?.label, p?.title, id].filter(Boolean); - if (candidates.some(x => eq(x, nm))) { - matches.add(id); - continue; - } - } - } - } catch {} - - // 4) 失败时尝试 sanitize 名称与 identifier 的弱匹配 - if (matches.size === 0) { - const nmSan = sanitize(nm); - try { - if (promptManager && typeof promptManager.getPromptCollection === 'function') { - const pc = promptManager.getPromptCollection(); - const coll = pc?.collection || []; - for (const p of coll) { - const id = p?.identifier; - if (!id) continue; - if (sanitize(id) === nmSan) { - matches.add(id); - } - } - } - } catch {} - } - - if (matches.size === 1) { - const id = Array.from(matches)[0]; - try { this._getNameCache().set(nm, id); } catch {} - return id; - } - if (matches.size > 1) { - const err = new Error('AMBIGUOUS_COMPONENT_NAME'); - throw err; - } - return null; - } catch (e) { - // 透传歧义错误,其它情况视为未命中 - if (String(e?.message) === 'AMBIGUOUS_COMPONENT_NAME') throw e; - return null; - } - } - - /** - * 解析组件引用 token: - * - 'ALL' → 特殊标记 - * - 'id:identifier' → 直接返回 identifier - * - 'name:xxx' → 通过名称解析为 identifier(大小写敏感) - * - 'xxx' → 先按 name 精确匹配,未命中回退为 identifier - * @param {string} token - * @returns {string|null} - */ - _parseComponentRefToken(token) { - if (!token) return null; - if (typeof token !== 'string') return null; - const raw = token.trim(); - if (!raw) return null; - if (raw.toLowerCase() === 'all') return 'ALL'; - // 特殊模式:仅启用预设中已开启的组件 - if (raw.toLowerCase() === 'all_preon') return 'ALL_PREON'; - if (raw.startsWith('id:')) return raw.slice(3).trim(); - if (raw.startsWith('name:')) { - const nm = raw.slice(5).trim(); - const id = this._resolveNameToIdentifier(nm); - if (id) return id; - const err = new Error('INVALID_COMPONENT_REF'); - throw err; - } - // 默认按 name 精确匹配;未命中则回退当作 identifier 使用 - try { - const byName = this._resolveNameToIdentifier(raw); - if (byName) return byName; - } catch (e) { - if (String(e?.message) === 'AMBIGUOUS_COMPONENT_NAME') throw e; - } - return raw; - } - - // ===== 轻量缓存:按 activeCharacter 维度缓存 name→identifier 与 footprint ===== - _getActiveCharacterIdSafe() { - try { - return promptManager?.activeCharacter ?? 'default'; - } catch { return 'default'; } - } - - _getNameCache() { - if (!this._nameCache) this._nameCache = new Map(); - const key = this._getActiveCharacterIdSafe(); - if (!this._nameCache.has(key)) this._nameCache.set(key, new Map()); - return this._nameCache.get(key); - } - - _getFootprintCache() { - if (!this._footprintCache) this._footprintCache = new Map(); - const key = this._getActiveCharacterIdSafe(); - if (!this._footprintCache.has(key)) this._footprintCache.set(key, new Map()); - return this._footprintCache.get(key); - } - - /** - * 解析统一 list:返回三元组 - * - references: 组件引用序列 - * - inlineInjections: 内联注入项(含原始索引) - * - listOverrides: 行内覆写(以组件引用为键) - * @param {Array} list - * @returns {{references:string[], inlineInjections:Array<{index:number,item:any}>, listOverrides:Object}} - */ - _parseUnifiedList(list) { - const references = []; - const inlineInjections = []; - const listOverrides = {}; - for (let i = 0; i < list.length; i++) { - const item = list[i]; - if (typeof item === 'string') { - references.push(item); - continue; - } - if (item && typeof item === 'object' && item.role && item.content) { - inlineInjections.push({ index: i, item }); - continue; - } - if (item && typeof item === 'object') { - const keys = Object.keys(item); - for (const k of keys) { - // k 是组件引用,如 'id:charDescription' / 'scenario' / 'chatHistory' / 'main' - references.push(k); - const cfg = item[k]; - if (cfg && typeof cfg === 'object') { - listOverrides[k] = Object.assign({}, listOverrides[k] || {}, cfg); - } - } - } - } - return { references, inlineInjections, listOverrides }; - } - - /** - * 基于原始 list 计算内联注入的邻接规则,映射到 position/depth。 - * 默认:紧跟前一组件(AFTER_COMPONENT);首项+attach=prev → BEFORE_PROMPT;邻接 chatHistory → IN_CHAT。 - * @param {Array} rawList - * @param {Array<{index:number,item:any}>} inlineInjections - * @returns {Array<{role:string,content:string,position:string,depth?:number,_afterRef?:string}>} - */ - _mapInlineInjectionsUnified(rawList, inlineInjections) { - const result = []; - const getRefAt = (idx, dir) => { - let j = idx + (dir < 0 ? -1 : 1); - while (j >= 0 && j < rawList.length) { - const it = rawList[j]; - if (typeof it === 'string') { - const token = this._parseComponentRefToken(it); - if (token && token !== 'ALL') return token; - } else if (it && typeof it === 'object') { - if (it.role && it.content) { - // inline injection, skip - } else { - const ks = Object.keys(it); - if (ks.length) { - const tk = this._parseComponentRefToken(ks[0]); - if (tk) return tk; - } - } - } - j += (dir < 0 ? -1 : 1); - } - return null; - }; - for (const { index, item } of inlineInjections) { - const prevRef = getRefAt(index, -1); - const nextRef = getRefAt(index, +1); - const attach = item.attach === 'prev' || item.attach === 'next' ? item.attach : 'auto'; - // 显式 position 优先 - if (item.position && typeof item.position === 'string') { - result.push({ role: item.role, content: item.content, position: item.position, depth: item.depth || 0 }); - continue; - } - // 有前邻组件 → 默认插到该组件之后(满足示例:位于 charDescription 之后、main 之前) - if (prevRef) { - result.push({ role: item.role, content: item.content, position: POSITIONS.AFTER_COMPONENT, _afterRef: prevRef }); - continue; - } - if (index === 0 && attach === 'prev') { - result.push({ role: item.role, content: item.content, position: POSITIONS.BEFORE_PROMPT }); - continue; - } - if (prevRef === 'chatHistory' || nextRef === 'chatHistory') { - result.push({ role: item.role, content: item.content, position: POSITIONS.IN_CHAT, depth: 0, _attach: attach === 'prev' ? 'before' : 'after' }); - continue; - } - result.push({ role: item.role, content: item.content, position: POSITIONS.IN_PROMPT }); - } - return result; - } - - /** - * 根据组件集合过滤消息(当 list 不含 ALL)。 - * @param {Array} messages - * @param {Set} wantedKeys - * @returns {Array} - */ - _filterMessagesByComponents(messages, wantedKeys) { - if (!wantedKeys || !wantedKeys.size) return []; - return messages.filter(m => wantedKeys.has(this._getComponentKeyFromIdentifier(m?.identifier))); - } - - /** 稳定重排:对目标子集按给定顺序排序,其他保持相对不变 */ - _stableReorderSubset(messages, orderedKeys) { - if (!Array.isArray(messages) || !orderedKeys || !orderedKeys.length) return messages; - const orderIndex = new Map(); - orderedKeys.forEach((k, i) => orderIndex.set(k, i)); - // 提取目标子集的元素与其原索引 - const targetIndices = []; - const targetMessages = []; - messages.forEach((m, idx) => { - const key = this._getComponentKeyFromIdentifier(m?.identifier); - if (orderIndex.has(key)) { - targetIndices.push(idx); - targetMessages.push({ m, ord: orderIndex.get(key) }); - } - }); - if (!targetIndices.length) return messages; - // 对目标子集按 ord 稳定排序 - targetMessages.sort((a, b) => a.ord - b.ord); - // 将排序后的目标消息放回原有“子集槽位”,非目标元素完全不动 - const out = messages.slice(); - for (let i = 0; i < targetIndices.length; i++) { - out[targetIndices[i]] = targetMessages[i].m; - } - return out; - } - - // ===== 缺失 identifier 的兜底标注 ===== - _normalizeText(s) { - return String(s || '').replace(/[\r\t\u200B\u00A0]/g, '').replace(/\s+/g, ' ').replace(/^[("']+|[("']+$/g, '').trim(); - } - - _stripNamePrefix(s) { - return String(s || '').replace(/^\s*[^:]{1,32}:\s*/, ''); - } - - _normStrip(s) { return this._normalizeText(this._stripNamePrefix(s)); } - - _createIsFromChat() { - try { - const ctx = getContext(); - const chatArr = Array.isArray(ctx?.chat) ? ctx.chat : []; - const chatNorms = chatArr.map(m => this._normStrip(m?.mes)).filter(Boolean); - const chatSet = new Set(chatNorms); - return (content) => { - const n = this._normStrip(content); - if (!n) return false; - if (chatSet.has(n)) return true; - for (const c of chatNorms) { - const a = n.length, b = c.length; - const minL = Math.min(a, b), maxL = Math.max(a, b); - if (minL < 20) continue; - if (((a >= b && n.includes(c)) || (b >= a && c.includes(n))) && minL / maxL >= 0.8) return true; - } - return false; - }; - } catch { - return () => false; - } - } - - async _annotateIdentifiersIfMissing(messages, targetKeys) { - const arr = Array.isArray(messages) ? messages.map(m => ({ ...m })) : []; - if (!arr.length) return arr; - // 标注 chatHistory:依据 role + 来源判断 - const isFromChat = this._createIsFromChat(); - for (const m of arr) { - if (!m?.identifier && (m?.role === 'user' || m?.role === 'assistant') && isFromChat(m.content)) { - m.identifier = 'chatHistory-annotated'; - } - } - // 即使部分已有 identifier,也继续尝试为缺失者做 footprint 标注 - // 若仍缺失,按目标 keys 单独捕获来反向标注 - const keys = Array.from(new Set((Array.isArray(targetKeys) ? targetKeys : []).filter(Boolean))); - if (!keys.length) return arr; - const footprint = new Map(); // key -> Set of norm strings - for (const key of keys) { - try { - if (key === 'chatHistory') continue; // 已在上面标注 - // footprint 缓存命中 - const fpCache = this._getFootprintCache(); - if (fpCache.has(key)) { - footprint.set(key, fpCache.get(key)); - } else { - 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 {} - } - } catch {} - } - for (const m of arr) { - if (m?.identifier) continue; - const sig = `[${m?.role}] ${this._normStrip(m?.content)}`; - for (const [key, set] of footprint.entries()) { - if (set.has(sig)) { m.identifier = key; break; } - } - } - return arr; - } - - /** 覆写:通用组件 disable/replace(文本级),不影响采样参数 */ - _applyGeneralOverrides(messages, overridesByComponent) { - if (!overridesByComponent) return messages; - let out = messages.slice(); - for (const ref in overridesByComponent) { - if (!Object.prototype.hasOwnProperty.call(overridesByComponent, ref)) continue; - const cfg = overridesByComponent[ref]; - if (!cfg || typeof cfg !== 'object') continue; - const key = this._parseComponentRefToken(ref); - if (!key) continue; - if (key === 'chatHistory') continue; // 历史专属逻辑另行处理 - const disable = !!cfg.disable; - const replace = typeof cfg.replace === 'string' ? cfg.replace : null; - if (disable) { - out = out.filter(m => this._getComponentKeyFromIdentifier(m?.identifier) !== key); - continue; - } - if (replace != null) { - out = out.map(m => this._getComponentKeyFromIdentifier(m?.identifier) === key ? { ...m, content: replace } : m); - } - } - return out; - } - - /** 仅对 chatHistory 应用 selector/replaceAll/replace */ - _applyChatHistoryOverride(messages, historyCfg) { - if (!historyCfg) return messages; - const all = messages.slice(); - const indexes = []; - for (let i = 0; i < all.length; i++) { - const m = all[i]; - if (this._getComponentKeyFromIdentifier(m?.identifier) === 'chatHistory') indexes.push(i); - } - if (indexes.length === 0) return messages; - if (historyCfg.disable) { - // 直接移除全部历史 - return all.filter((m, idx) => !indexes.includes(idx)); - } - const history = indexes.map(i => all[i]); - - // selector 过滤 - let selected = history.slice(); - if (historyCfg.selector) { - // 在历史子集上应用 selector - selected = this.applyChatHistorySelector(history, historyCfg.selector); - } - - // 替换逻辑 - let replaced = selected.slice(); - if (historyCfg.replaceAll && Array.isArray(historyCfg.with)) { - replaced = (historyCfg.with || []).map((w, idx) => ({ role: w.role, content: w.content, identifier: `chatHistory-replaceAll-${idx}` })); - } - if (Array.isArray(historyCfg.replace)) { - // 在 replaced 上按顺序执行多段替换 - for (const step of historyCfg.replace) { - const withArr = Array.isArray(step?.with) ? step.with : []; - const newMsgs = withArr.map((w, idx) => ({ role: w.role, content: w.content, identifier: `chatHistory-replace-${Date.now()}-${idx}` })); - let indices = []; - if (step?.indices?.values && Array.isArray(step.indices.values) && step.indices.values.length) { - const n = replaced.length; - indices = step.indices.values.map(v0 => { - let v = Number(v0); - if (Number.isNaN(v)) return -1; - if (v < 0) v = n + v; - return (v >= 0 && v < n) ? v : -1; - }).filter(v => v >= 0); - } else if (step?.range && (step.range.start !== undefined || step.range.end !== undefined)) { - let { start = 0, end = replaced.length - 1 } = step.range; - const n = replaced.length; - start = Number(start); end = Number(end); - if (Number.isNaN(start)) start = 0; - if (Number.isNaN(end)) end = n - 1; - if (start < 0) start = n + start; - if (end < 0) end = n + end; - start = Math.max(0, start); end = Math.min(n - 1, end); - if (start <= end) indices = Array.from({ length: end - start + 1 }, (_, k) => start + k); - } else if (step?.last != null) { - const k = Math.max(0, Number(step.last) || 0); - const n = replaced.length; - indices = k > 0 ? Array.from({ length: Math.min(k, n) }, (_, j) => n - k + j) : []; - } - if (indices.length) { - // 按出现顺序处理:先删除这些索引,再按同位置插入(采用最小索引处插入) - const set = new Set(indices); - const kept = replaced.filter((_, idx) => !set.has(idx)); - const insertAt = Math.min(...indices); - replaced = kept.slice(0, insertAt).concat(newMsgs).concat(kept.slice(insertAt)); - } - } - } - - // 将 replaced 合并回全量:找到历史的第一个索引,替换整个历史窗口 - const first = Math.min(...indexes); - const last = Math.max(...indexes); - const before = all.slice(0, first); - const after = all.slice(last + 1); - return before.concat(replaced).concat(after); - } - - /** 将高级 injections 应用到 messages */ - _applyAdvancedInjections(messages, injections = []) { - if (!Array.isArray(injections) || injections.length === 0) return messages; - const out = messages.slice(); - // 计算 chatHistory 边界 - const historyIdx = []; - for (let i = 0; i < out.length; i++) if (this._getComponentKeyFromIdentifier(out[i]?.identifier) === 'chatHistory') historyIdx.push(i); - const hasHistory = historyIdx.length > 0; - const historyStart = hasHistory ? Math.min(...historyIdx) : -1; - const historyEnd = hasHistory ? Math.max(...historyIdx) : -1; - for (const inj of injections) { - const role = inj?.role; const content = inj?.content; - if (!role || typeof content !== 'string') continue; - const forcedId = inj && typeof inj.identifier === 'string' && inj.identifier.trim() ? String(inj.identifier).trim() : null; - const msg = { role, content, identifier: forcedId || `injection-${inj.position || POSITIONS.IN_PROMPT}-${Date.now()}-${Math.random().toString(36).slice(2)}` }; - if (inj.position === POSITIONS.BEFORE_PROMPT) { - out.splice(0, 0, msg); - continue; - } - if (inj.position === POSITIONS.AFTER_COMPONENT) { - const ref = inj._afterRef || null; - let inserted = false; - if (ref) { - for (let i = out.length - 1; i >= 0; i--) { - const id = out[i]?.identifier; - if (this._identifierMatchesKey(id, ref) || this._getComponentKeyFromIdentifier(id) === ref) { - out.splice(i + 1, 0, msg); - inserted = true; break; - } - } - } - if (!inserted) { - // 回退同 IN_PROMPT - if (hasHistory) { - const depth = Math.max(0, Number(inj.depth) || 0); - const insertPos = Math.max(0, historyStart - depth); - out.splice(insertPos, 0, msg); - } else { - out.splice(0, 0, msg); - } - } - continue; - } - if (inj.position === POSITIONS.IN_CHAT && hasHistory) { - // depth=0 → 历史末尾后;depth>0 → 进入历史内部; - const depth = Math.max(0, Number(inj.depth) || 0); - if (inj._attach === 'before') { - const insertPos = Math.max(historyStart - depth, 0); - out.splice(insertPos, 0, msg); - } else { - const insertPos = Math.min(out.length, historyEnd + 1 - depth); - out.splice(Math.max(historyStart, insertPos), 0, msg); - } - continue; - } - // IN_PROMPT 或无历史:在 chatHistory 之前插入,否则置顶后 - if (hasHistory) { - const depth = Math.max(0, Number(inj.depth) || 0); - const insertPos = Math.max(0, historyStart - depth); - out.splice(insertPos, 0, msg); - } else { - out.splice(0, 0, msg); - } - } - return out; - } - - _mergeMessages(baseMessages, extraMessages) { - const out = []; - const seen = new Set(); - const norm = (s) => String(s || '').replace(/[\r\t\u200B\u00A0]/g, '').replace(/\s+/g, ' ').replace(/^[("']+|[("']+$/g, '').trim(); - const push = (m) => { - if (!m || !m.content) return; - const key = `${m.role}:${norm(m.content)}`; - if (seen.has(key)) return; - seen.add(key); - out.push({ role: m.role, content: m.content }); - }; - baseMessages.forEach(push); - (extraMessages || []).forEach(push); - return out; - } - - _splitMessagesForHistoryOps(messages) { - // history: user/assistant; systemOther: 其余 - const history = []; - const systemOther = []; - for (const m of messages) { - if (!m || typeof m.content !== 'string') continue; - if (m.role === 'user' || m.role === 'assistant') history.push(m); - else systemOther.push(m); - } - return { history, systemOther }; - } - - _applyRolesFilter(list, rolesCfg) { - if (!rolesCfg || (!rolesCfg.include && !rolesCfg.exclude)) return list; - const inc = Array.isArray(rolesCfg.include) && rolesCfg.include.length ? new Set(rolesCfg.include) : null; - const exc = Array.isArray(rolesCfg.exclude) && rolesCfg.exclude.length ? new Set(rolesCfg.exclude) : null; - return list.filter(m => { - const r = m.role; - if (inc && !inc.has(r)) return false; - if (exc && exc.has(r)) return false; - return true; - }); - } - - _applyContentFilter(list, filterCfg) { - if (!filterCfg) return list; - const { contains, regex, fromUserNames } = filterCfg; - let out = list.slice(); - if (contains) { - const needles = Array.isArray(contains) ? contains : [contains]; - out = out.filter(m => needles.some(k => String(m.content).includes(String(k)))); - } - if (regex) { - try { - const re = new RegExp(regex); - out = out.filter(m => re.test(String(m.content))); - } catch {} - } - if (fromUserNames && fromUserNames.length) { - // 仅当 messages 中附带 name 时生效;否则忽略 - out = out.filter(m => !m.name || fromUserNames.includes(m.name)); - } - // 时间戳过滤需要原始数据支持,这里忽略(占位) - return out; - } - - _applyAnchorWindow(list, anchorCfg) { - if (!anchorCfg || !list.length) return list; - const { anchor = 'lastUser', before = 0, after = 0 } = anchorCfg; - // 找到锚点索引 - let idx = -1; - if (anchor === 'lastUser') { - for (let i = list.length - 1; i >= 0; i--) if (list[i].role === 'user') { idx = i; break; } - } else if (anchor === 'lastAssistant') { - for (let i = list.length - 1; i >= 0; i--) if (list[i].role === 'assistant') { idx = i; break; } - } else if (anchor === 'lastSystem') { - for (let i = list.length - 1; i >= 0; i--) if (list[i].role === 'system') { idx = i; break; } - } - if (idx === -1) return list; - const start = Math.max(0, idx - Number(before || 0)); - const end = Math.min(list.length - 1, idx + Number(after || 0)); - return list.slice(start, end + 1); - } - - _applyIndicesRange(list, selector) { - let result = list.slice(); - // indices 优先 - if (Array.isArray(selector?.indices?.values) && selector.indices.values.length) { - const vals = selector.indices.values; - const picked = []; - const n = list.length; - for (const v0 of vals) { - let v = Number(v0); - if (Number.isNaN(v)) continue; - if (v < 0) v = n + v; // 负索引 - if (v >= 0 && v < n) picked.push(list[v]); - } - result = picked; - return result; - } - if (selector?.range && (selector.range.start !== undefined || selector.range.end !== undefined)) { - let { start = 0, end = list.length - 1 } = selector.range; - const n = list.length; - start = Number(start); end = Number(end); - if (Number.isNaN(start)) start = 0; - if (Number.isNaN(end)) end = n - 1; - if (start < 0) start = n + start; - if (end < 0) end = n + end; - start = Math.max(0, start); end = Math.min(n - 1, end); - if (start > end) return []; - return list.slice(start, end + 1); - } - if (selector?.last !== undefined && selector.last !== null) { - const k = Math.max(0, Number(selector.last) || 0); - if (k === 0) return []; - const n = list.length; - return list.slice(Math.max(0, n - k)); - } - return result; - } - - _applyTakeEvery(list, step) { - const s = Math.max(1, Number(step) || 1); - if (s === 1) return list; - const out = []; - for (let i = 0; i < list.length; i += s) out.push(list[i]); - return out; - } - - _applyLimit(list, limitCfg) { - if (!limitCfg) return list; - // 仅实现 count,tokenBudget 预留 - const count = Number(limitCfg.count || 0); - if (count > 0 && list.length > count) { - const how = limitCfg.truncateStrategy || 'last'; - if (how === 'first') return list.slice(0, count); - if (how === 'middle') { - const left = Math.floor(count / 2); - const right = count - left; - return list.slice(0, left).concat(list.slice(-right)); - } - if (how === 'even') { - const step = Math.ceil(list.length / count); - const out = []; - for (let i = 0; i < list.length && out.length < count; i += step) out.push(list[i]); - return out; - } - // default: 'last' → 取末尾 - return list.slice(-count); - } - return list; - } - - applyChatHistorySelector(messages, selector) { - if (!selector || !Array.isArray(messages) || !messages.length) return messages; - const { history, systemOther } = this._splitMessagesForHistoryOps(messages); - let list = history; - // roles/filter/anchor → indices/range/last → takeEvery → limit - list = this._applyRolesFilter(list, selector.roles); - list = this._applyContentFilter(list, selector.filter); - list = this._applyAnchorWindow(list, selector.anchorWindow); - list = this._applyIndicesRange(list, selector); - list = this._applyTakeEvery(list, selector.takeEvery); - list = this._applyLimit(list, selector.limit || (selector.last ? { count: Number(selector.last) } : null)); - // 合并非历史部分 - return systemOther.concat(list); - } - - // ===== 发送实现(构建后的统一发送) ===== - - 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; // 默认开 - const apiCfg = this.resolveApiConfig(options?.api || {}); - const payload = this.buildChatPayload(messages, apiCfg, streamingEnabled); - - try { - 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); - } - - if (streamingEnabled) { - 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; - for await (const { text } of (generator || [])) { - const chunk = text.slice(last.length); - last = text; - session.accumulated = text; - this.postToTarget(sourceWindow, 'generateStreamChunk', { id: requestId, chunk, accumulated: text, metadata: {} }, targetOrigin); - } - const result = { - success: true, - result: session.accumulated, - sessionId, - metadata: { duration: Date.now() - session.startedAt, model: apiCfg.model, finishReason: 'stop' }, - }; - this.postToTarget(sourceWindow, 'generateStreamComplete', { id: requestId, result }, targetOrigin); - return result; - } else { - const extracted = await ChatCompletionService.sendRequest(payload, true, session.abortController.signal); - const result = { - success: true, - result: String((extracted && extracted.content) || ''), - sessionId, - metadata: { duration: Date.now() - session.startedAt, model: apiCfg.model, finishReason: 'stop' }, - }; - this.postToTarget(sourceWindow, 'generateResult', { id: requestId, result }, targetOrigin); - return result; - } - } catch (err) { - this.sendError(sourceWindow, requestId, streamingEnabled, err, 'API_ERROR', null, targetOrigin); - return null; - } - } - - // ===== 主流程 ===== - async handleRequestInternal(options, requestId, sourceWindow, targetOrigin = null) { - // 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'); - } 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 = []; - if (baseStrategy === 'EMPTY') { - captured = []; - } else { - // 不将 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 {} - 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 }); - } - } - - // 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); - } - - _applyOrderingStrategy(messages, baseStrategy, orderedRefs, unorderedKeys) { - let out = messages.slice(); - if (baseStrategy === 'SUBSET') { - const want = new Set(orderedRefs); - out = this._filterMessagesByComponents(out, want); - } else if ((baseStrategy === 'ALL' || baseStrategy === 'ALL_PREON') && orderedRefs.length) { - const targets = orderedRefs.filter(k => !unorderedKeys.has(k)); - if (targets.length) out = this._stableReorderSubset(out, targets); - } - return out; - } - - _applyInlineOverrides(messages, byComp) { - let out = messages.slice(); - if (!byComp) return out; - out = this._applyGeneralOverrides(out, byComp); - const ensureInjections = []; - for (const ref in byComp) { - if (!Object.prototype.hasOwnProperty.call(byComp, ref)) continue; - const key = this._parseComponentRefToken(ref); - if (!key || key === 'chatHistory') continue; - const cfg = byComp[ref]; - if (!cfg || typeof cfg.replace !== 'string') continue; - const exists = out.some(m => this._identifierMatchesKey(m?.identifier, key) || this._getComponentKeyFromIdentifier(m?.identifier) === key); - if (exists) continue; - const map = this._mapCreateAnchorForKey(key); - ensureInjections.push({ position: map.position, role: map.role, content: cfg.replace, identifier: map.identifier }); - } - if (ensureInjections.length) { - out = this._applyAdvancedInjections(out, ensureInjections); - } - if (byComp['id:chatHistory'] || byComp['chatHistory']) { - const cfg = byComp['id:chatHistory'] || byComp['chatHistory']; - out = this._applyChatHistoryOverride(out, cfg); - } - return out; - } - - _applyAllInjections(messages, inlineMapped, advancedInjections) { - let out = messages.slice(); - if (inlineMapped && inlineMapped.length) { - out = this._applyAdvancedInjections(out, inlineMapped); - } - if (Array.isArray(advancedInjections) && advancedInjections.length) { - out = this._applyAdvancedInjections(out, advancedInjections); - } - return out; - } - - _appendUserInput(messages, userInput) { - const out = messages.slice(); - if (typeof userInput === 'string' && userInput.length >= 0) { - out.push({ role: 'user', content: String(userInput || ''), identifier: 'userInput' }); - } - return out; - } - - _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 (debug?.exportBlueprint) { - try { - const bp = { - id: requestId, - components: { strategy: baseStrategy, order: orderedRefs }, - injections: (debug?.injections || []).concat(inlineMapped || []), - overrides: listLevelOverrides || null, - }; - 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; - } - } - - /** 取消会话 */ - cancel(sessionId) { - const s = this.sessions.get(this.normalizeSessionId(sessionId)); - try { s?.abortController?.abort(); } catch {} - } - - /** 清理所有会话 */ - cleanup() { - 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); -} - -// 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 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 { - window.addEventListener('xiaobaixEnabledChanged', (e) => { - try { - const enabled = e && e.detail && e.detail.enabled === true; - if (enabled) initCallGenerateHostBridge(); else cleanupCallGenerateHostBridge(); - } catch (_) {} - }); - document.addEventListener('xiaobaixEnabledChanged', (e) => { - try { - const enabled = e && e.detail && e.detail.enabled === true; - if (enabled) initCallGenerateHostBridge(); else cleanupCallGenerateHostBridge(); - } catch (_) {} - }); - window.addEventListener('beforeunload', () => { try { cleanupCallGenerateHostBridge(); } catch (_) {} }); - } catch (_) {} - - // ===== 全局 API 暴露:与 iframe 调用方式完全一致 ===== - // 创建命名空间 - window.LittleWhiteBox = window.LittleWhiteBox || {}; - - /** - * 全局 callGenerate 函数 - * 使用方式与 iframe 中完全一致:await window.callGenerate(options) - * - * @param {Object} options - 生成选项 - * @returns {Promise} 生成结果 - * - * @example - * // iframe 中的调用方式: - * const res = await window.callGenerate({ - * components: { list: ['ALL_PREON'] }, - * userInput: '你好', - * streaming: { enabled: true }, - * api: { inherit: true } - * }); - * - * // 全局调用方式(完全一致): - * const res = await window.LittleWhiteBox.callGenerate({ - * components: { list: ['ALL_PREON'] }, - * userInput: '你好', - * streaming: { enabled: true }, - * api: { inherit: true } - * }); - */ - 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 { - onChunkCallback(data.chunk, data.accumulated); - } catch (err) { - console.error('[callGenerate] onChunk callback error:', err); - } - } else if (data.type === 'generateStreamComplete') { - window.removeEventListener('message', listener); - resolve(data.result); - } else if (data.type === 'generateResult') { - window.removeEventListener('message', listener); - resolve(data.result); - } else if (data.type === 'generateStreamError' || data.type === 'generateError') { - window.removeEventListener('message', listener); - reject(data.error); - } - }; - - // 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); - reject(err); - }); - }); - }; - - /** - * 取消指定会话 - * @param {string} sessionId - 会话 ID(如 'xb1', 'xb2' 等) - */ - window.LittleWhiteBox.callGenerate.cancel = function(sessionId) { - callGenerateService.cancel(sessionId); - }; - - /** - * 清理所有会话 - */ - window.LittleWhiteBox.callGenerate.cleanup = function() { - callGenerateService.cleanup(); - }; - - // 保持向后兼容:保留原有的内部接口 - window.LittleWhiteBox._internal = { - service: callGenerateService, - handleGenerateRequest, - init: initCallGenerateHostBridge, - cleanup: cleanupCallGenerateHostBridge - }; -}