Files
LittleWhiteBox/bridges/call-generate-service.js
2026-01-17 16:34:39 +08:00

1551 lines
70 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// @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<string, { id: string, abortController: AbortController, accumulated: string, startedAt: number }>} */
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-* 归并为 chatHistorydialogueExamples 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<any>} 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<any>} 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<any>} messages
* @param {Set<string>} wantedKeys
* @returns {Array<any>}
*/
_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;
// 仅实现 counttokenBudget 预留
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') {
// 路径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 {}
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<Object>} 生成结果
*
* @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
};
}