Files
LittleWhiteBox/modules/streaming-generation.js

1431 lines
67 KiB
JavaScript
Raw Normal View History

2026-01-17 16:34:39 +08:00
// 删掉getRequestHeaders, extractMessageFromData, getStreamingReply, tryParseStreamingError, getEventSourceStream
import { eventSource, event_types, chat, name1, activateSendButtons, deactivateSendButtons } from "../../../../../script.js";
import { chat_completion_sources, oai_settings, promptManager, getChatCompletionModel } from "../../../../openai.js";
import { ChatCompletionService } from "../../../../custom-request.js";
import { getContext } from "../../../../st-context.js";
import { SlashCommandParser } from "../../../../slash-commands/SlashCommandParser.js";
import { SlashCommand } from "../../../../slash-commands/SlashCommand.js";
import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from "../../../../slash-commands/SlashCommandArgument.js";
import { SECRET_KEYS, writeSecret } from "../../../../secrets.js";
import { power_user } from "../../../../power-user.js";
import { world_info } from "../../../../world-info.js";
import { xbLog, CacheRegistry } from "../core/debug-core.js";
import { getTrustedOrigin } from "../core/iframe-messaging.js";
const EVT_DONE = 'xiaobaix_streaming_completed';
const PROXY_SUPPORTED = new Set([
chat_completion_sources.OPENAI, chat_completion_sources.CLAUDE,
chat_completion_sources.MAKERSUITE, chat_completion_sources.COHERE,
chat_completion_sources.DEEPSEEK,
]);
class StreamingGeneration {
constructor() {
this.tempreply = '';
this.isInitialized = false;
this.isStreaming = false;
this.sessions = new Map();
this.lastSessionId = null;
this.activeCount = 0;
this._toggleBusy = false;
this._toggleQueue = Promise.resolve();
}
init() {
if (this.isInitialized) return;
try { localStorage.removeItem('xbgen:lastToggleSnap'); } catch {}
this.registerCommands();
try { xbLog.info('streamingGeneration', 'init'); } catch {}
this.isInitialized = true;
}
_getSlotId(id) {
if (!id) return 1;
const m = String(id).match(/^xb(\d+)$/i);
if (m && +m[1] >= 1 && +m[1] <= 10) return `xb${m[1]}`;
const n = parseInt(id, 10);
return (!isNaN(n) && n >= 1 && n <= 10) ? n : 1;
}
_ensureSession(id, prompt) {
const slotId = this._getSlotId(id);
if (!this.sessions.has(slotId)) {
if (this.sessions.size >= 10) this._cleanupOldestSessions();
this.sessions.set(slotId, {
id: slotId, text: '', isStreaming: false, prompt: prompt || '',
updatedAt: Date.now(), abortController: null
});
}
this.lastSessionId = slotId;
return this.sessions.get(slotId);
}
_cleanupOldestSessions() {
const sorted = [...this.sessions.entries()].sort((a, b) => a[1].updatedAt - b[1].updatedAt);
sorted.slice(0, Math.max(0, sorted.length - 9)).forEach(([sid, s]) => {
try { s.abortController?.abort(); } catch {}
this.sessions.delete(sid);
});
}
updateTempReply(value, sessionId) {
const text = String(value || '');
if (sessionId !== undefined) {
const sid = this._getSlotId(sessionId);
const s = this.sessions.get(sid) || {
id: sid, text: '', isStreaming: false, prompt: '',
updatedAt: 0, abortController: null
};
s.text = text;
s.updatedAt = Date.now();
this.sessions.set(sid, s);
this.lastSessionId = sid;
}
this.tempreply = text;
}
postToFrames(name, payload) {
try {
const frames = window?.frames;
if (frames?.length) {
const msg = { type: name, payload, from: 'xiaobaix' };
const targetOrigin = getTrustedOrigin();
let fail = 0;
for (let i = 0; i < frames.length; i++) {
try { frames[i].postMessage(msg, targetOrigin); } catch { fail++; }
}
if (fail) {
try { xbLog.warn('streamingGeneration', `postToFrames fail=${fail} total=${frames.length} type=${name}`); } catch {}
}
}
} catch {}
}
resolveCurrentApiAndModel(apiOptions = {}) {
if (apiOptions.api && apiOptions.model) return apiOptions;
const source = oai_settings?.chat_completion_source;
const model = getChatCompletionModel();
const map = {
[chat_completion_sources.OPENAI]: 'openai',
[chat_completion_sources.CLAUDE]: 'claude',
[chat_completion_sources.MAKERSUITE]: 'gemini',
[chat_completion_sources.COHERE]: 'cohere',
[chat_completion_sources.DEEPSEEK]: 'deepseek',
[chat_completion_sources.CUSTOM]: 'custom',
};
const api = map[source] || 'openai';
return { api, model };
}
async callAPI(generateData, abortSignal, stream = true) {
const messages = Array.isArray(generateData) ? generateData :
(generateData?.prompt || generateData?.messages || generateData);
const baseOptions = (!Array.isArray(generateData) && generateData?.apiOptions) ? generateData.apiOptions : {};
const opts = { ...baseOptions, ...this.resolveCurrentApiAndModel(baseOptions) };
const modelLower = String(opts.model || '').toLowerCase();
const isClaudeThinkingModel =
modelLower.includes('claude') &&
modelLower.includes('thinking') &&
!modelLower.includes('nothinking');
if (isClaudeThinkingModel && Array.isArray(messages) && messages.length > 0) {
const lastMsg = messages[messages.length - 1];
if (lastMsg?.role === 'assistant') {
console.log('[xbgen] Claude Thinking 模型:移除 assistant prefill');
messages.pop();
}
}
const source = {
openai: chat_completion_sources.OPENAI,
claude: chat_completion_sources.CLAUDE,
gemini: chat_completion_sources.MAKERSUITE,
google: chat_completion_sources.MAKERSUITE,
cohere: chat_completion_sources.COHERE,
deepseek: chat_completion_sources.DEEPSEEK,
custom: chat_completion_sources.CUSTOM,
}[String(opts.api || '').toLowerCase()];
if (!source) {
console.error('[xbgen:callAPI] 不支持的 api:', opts.api);
try { xbLog.error('streamingGeneration', `unsupported api: ${opts.api}`, null); } catch {}
}
if (!source) throw new Error(`不支持的 api: ${opts.api}`);
const model = String(opts.model || '').trim();
if (!model) {
try { xbLog.error('streamingGeneration', 'missing model', null); } catch {}
}
if (!model) throw new Error('未检测到当前模型,请在聊天面板选择模型或在插件设置中为分析显式指定模型。');
try {
try {
if (xbLog.isEnabled?.()) {
const msgCount = Array.isArray(messages) ? messages.length : null;
xbLog.info('streamingGeneration', `callAPI stream=${!!stream} api=${String(opts.api || '')} model=${model} messages=${msgCount ?? '-'}`);
}
} catch {}
const provider = String(opts.api || '').toLowerCase();
const reverseProxyConfigured = String(opts.apiurl || '').trim().length > 0;
const pwd = String(opts.apipassword || '').trim();
if (!reverseProxyConfigured && pwd) {
const providerToSecretKey = {
openai: SECRET_KEYS.OPENAI,
gemini: SECRET_KEYS.MAKERSUITE,
google: SECRET_KEYS.MAKERSUITE,
cohere: SECRET_KEYS.COHERE,
deepseek: SECRET_KEYS.DEEPSEEK,
custom: SECRET_KEYS.CUSTOM,
};
const secretKey = providerToSecretKey[provider];
if (secretKey) {
await writeSecret(secretKey, pwd, 'xbgen-inline');
}
}
} catch {}
const num = (v) => {
const n = Number(v);
return Number.isFinite(n) ? n : undefined;
};
const isUnset = (k) => baseOptions?.[k] === '__unset__';
const tUser = num(baseOptions?.temperature);
const ppUser = num(baseOptions?.presence_penalty);
const fpUser = num(baseOptions?.frequency_penalty);
const tpUser = num(baseOptions?.top_p);
const tkUser = num(baseOptions?.top_k);
const mtUser = num(baseOptions?.max_tokens);
const tUI = num(oai_settings?.temp_openai);
const ppUI = num(oai_settings?.pres_pen_openai);
const fpUI = num(oai_settings?.freq_pen_openai);
const tpUI_OpenAI = num(oai_settings?.top_p_openai ?? oai_settings?.top_p);
const mtUI_OpenAI = num(oai_settings?.openai_max_tokens ?? oai_settings?.max_tokens);
const tpUI_Gemini = num(oai_settings?.makersuite_top_p ?? oai_settings?.top_p);
const tkUI_Gemini = num(oai_settings?.makersuite_top_k ?? oai_settings?.top_k);
const mtUI_Gemini = num(oai_settings?.makersuite_max_tokens ?? oai_settings?.max_output_tokens ?? oai_settings?.openai_max_tokens ?? oai_settings?.max_tokens);
const effectiveTemperature = isUnset('temperature') ? undefined : (tUser ?? tUI);
const effectivePresence = isUnset('presence_penalty') ? undefined : (ppUser ?? ppUI);
const effectiveFrequency = isUnset('frequency_penalty') ? undefined : (fpUser ?? fpUI);
const effectiveTopP = isUnset('top_p') ? undefined : (tpUser ?? (source === chat_completion_sources.MAKERSUITE ? tpUI_Gemini : tpUI_OpenAI));
const effectiveTopK = isUnset('top_k') ? undefined : (tkUser ?? (source === chat_completion_sources.MAKERSUITE ? tkUI_Gemini : undefined));
const effectiveMaxT = isUnset('max_tokens') ? undefined : (mtUser ?? (source === chat_completion_sources.MAKERSUITE ? (mtUI_Gemini ?? mtUI_OpenAI) : mtUI_OpenAI) ?? 4000);
const body = {
messages, model, stream,
chat_completion_source: source,
temperature: effectiveTemperature,
presence_penalty: effectivePresence,
frequency_penalty: effectiveFrequency,
top_p: effectiveTopP,
max_tokens: effectiveMaxT,
stop: Array.isArray(generateData?.stop) ? generateData.stop : undefined,
use_makersuite_sysprompt: false,
claude_use_sysprompt: oai_settings?.claude_use_sysprompt ?? false,
custom_prompt_post_processing: undefined,
// thinking 模型支持
include_reasoning: oai_settings?.show_thoughts ?? true,
reasoning_effort: oai_settings?.reasoning_effort || 'medium',
};
// Claude 专用top_k
if (source === chat_completion_sources.CLAUDE) {
body.top_k = Number(oai_settings?.top_k_openai) || undefined;
}
if (source === chat_completion_sources.MAKERSUITE) {
if (effectiveTopK !== undefined) body.top_k = effectiveTopK;
body.max_output_tokens = effectiveMaxT;
}
const useNet = !!opts.enableNet;
if (source === chat_completion_sources.MAKERSUITE && useNet) {
body.tools = Array.isArray(body.tools) ? body.tools : [];
if (!body.tools.some(t => t && t.google_search_retrieval)) {
body.tools.push({ google_search_retrieval: {} });
}
body.enable_web_search = true;
body.makersuite_use_google_search = true;
}
let reverseProxy = String(opts.apiurl || oai_settings?.reverse_proxy || '').trim();
let proxyPassword = String(oai_settings?.proxy_password || '').trim();
const cmdApiUrl = String(opts.apiurl || '').trim();
const cmdApiPwd = String(opts.apipassword || '').trim();
if (cmdApiUrl) {
if (cmdApiPwd) proxyPassword = cmdApiPwd;
} else if (cmdApiPwd) {
reverseProxy = '';
proxyPassword = '';
}
if (PROXY_SUPPORTED.has(source) && reverseProxy) {
body.reverse_proxy = reverseProxy.replace(/\/?$/, '');
if (proxyPassword) body.proxy_password = proxyPassword;
}
if (source === chat_completion_sources.CUSTOM) {
const customUrl = String(cmdApiUrl || oai_settings?.custom_url || '').trim();
if (customUrl) {
body.custom_url = customUrl;
} else {
throw new Error('未配置自定义后端URL请在命令中提供 apiurl 或在设置中填写 custom_url');
}
if (oai_settings?.custom_include_headers) body.custom_include_headers = oai_settings.custom_include_headers;
if (oai_settings?.custom_include_body) body.custom_include_body = oai_settings.custom_include_body;
if (oai_settings?.custom_exclude_body) body.custom_exclude_body = oai_settings.custom_exclude_body;
}
if (stream) {
const payload = ChatCompletionService.createRequestData(body);
const streamFactory = await ChatCompletionService.sendRequest(payload, false, abortSignal);
const generator = (typeof streamFactory === 'function') ? streamFactory() : streamFactory;
return (async function* () {
let last = '';
try {
for await (const item of (generator || [])) {
if (abortSignal?.aborted) {
return;
}
let accumulated = '';
if (typeof item === 'string') {
accumulated = item;
} else if (item && typeof item === 'object') {
// 尝试多种字段
accumulated = (typeof item.text === 'string' ? item.text : '') ||
(typeof item.content === 'string' ? item.content : '') || '';
// thinking 相关字段
if (!accumulated) {
const thinking = item?.delta?.thinking || item?.thinking;
if (typeof thinking === 'string') {
accumulated = thinking;
}
}
if (!accumulated) {
const rc = item?.reasoning_content || item?.reasoning;
if (typeof rc === 'string') {
accumulated = rc;
}
}
if (!accumulated) {
const rc = item?.choices?.[0]?.delta?.reasoning_content;
if (typeof rc === 'string') accumulated = rc;
}
}
if (!accumulated) {
continue;
}
if (accumulated.startsWith(last)) {
last = accumulated;
} else {
last += accumulated;
}
yield last;
}
} catch (err) {
console.error('[xbgen:stream] 流式错误:', err);
console.error('[xbgen:stream] err.name:', err?.name);
console.error('[xbgen:stream] err.message:', err?.message);
if (err?.name === 'AbortError') return;
try { xbLog.error('streamingGeneration', 'Stream error', err); } catch {}
throw err;
}
})();
} else {
const payload = ChatCompletionService.createRequestData(body);
const extracted = await ChatCompletionService.sendRequest(payload, false, abortSignal);
let result = '';
if (extracted && typeof extracted === 'object') {
const msg = extracted?.choices?.[0]?.message;
result = String(
msg?.content ??
msg?.reasoning_content ??
extracted?.choices?.[0]?.text ??
extracted?.content ??
extracted?.reasoning_content ??
''
);
} else {
result = String(extracted ?? '');
}
return result;
}
}
async _emitPromptReady(chatArray) {
try {
if (Array.isArray(chatArray)) {
await eventSource?.emit?.(event_types.CHAT_COMPLETION_PROMPT_READY, { chat: chatArray, dryRun: false });
}
} catch {}
}
async processGeneration(generateData, prompt, sessionId, stream = true) {
const session = this._ensureSession(sessionId, prompt);
const abortController = new AbortController();
session.abortController = abortController;
try {
try { xbLog.info('streamingGeneration', `processGeneration start sid=${session.id} stream=${!!stream} promptLen=${String(prompt || '').length}`); } catch {}
this.isStreaming = true;
this.activeCount++;
session.isStreaming = true;
session.text = '';
session.updatedAt = Date.now();
this.tempreply = '';
if (stream) {
const generator = await this.callAPI(generateData, abortController.signal, true);
for await (const chunk of generator) {
if (abortController.signal.aborted) {
break;
}
this.updateTempReply(chunk, session.id);
}
} else {
const result = await this.callAPI(generateData, abortController.signal, false);
this.updateTempReply(result, session.id);
}
const payload = { finalText: session.text, originalPrompt: prompt, sessionId: session.id };
try { eventSource?.emit?.(EVT_DONE, payload); } catch { }
this.postToFrames(EVT_DONE, payload);
try { window?.postMessage?.({ type: EVT_DONE, payload, from: 'xiaobaix' }, getTrustedOrigin()); } catch { }
try { xbLog.info('streamingGeneration', `processGeneration done sid=${session.id} outLen=${String(session.text || '').length}`); } catch {}
return String(session.text || '');
} catch (err) {
if (err?.name === 'AbortError') {
try { xbLog.warn('streamingGeneration', `processGeneration aborted sid=${session.id}`); } catch {}
return String(session.text || '');
}
console.error('[StreamingGeneration] Generation error:', err);
console.error('[StreamingGeneration] error.error =', err?.error);
try { xbLog.error('streamingGeneration', `processGeneration error sid=${session.id}`, err); } catch {}
let errorMessage = '生成失败';
if (err && typeof err === 'object' && err.error && typeof err.error === 'object') {
const detail = err.error;
const rawMsg = String(detail.message || '').trim();
const code = String(detail.code || '').trim().toLowerCase();
if (
/input is too long/i.test(rawMsg) ||
/context length/i.test(rawMsg) ||
/maximum context length/i.test(rawMsg) ||
/too many tokens/i.test(rawMsg)
) {
errorMessage =
'输入过长:当前对话内容超过了所选模型或代理的上下文长度限制。\n' +
`原始信息:${rawMsg}`;
} else if (
/quota/i.test(rawMsg) ||
/rate limit/i.test(rawMsg) ||
code === 'insufficient_quota'
) {
errorMessage =
'请求被配额或限流拒绝:当前 API 额度可能已用尽,或触发了限流。\n' +
`原始信息:${rawMsg || code}`;
} else if (code === 'bad_request') {
errorMessage =
'请求被上游 API 以 Bad Request 拒绝。\n' +
'可能原因:参数格式不符合要求、模型名错误,或输入内容不被当前通道接受。\n\n' +
`原始信息:${rawMsg || code}`;
} else {
errorMessage = rawMsg || code || JSON.stringify(detail);
}
} else if (err && typeof err === 'object' && err.message) {
errorMessage = err.message;
} else if (typeof err === 'string') {
errorMessage = err;
}
throw new Error(errorMessage);
} finally {
session.isStreaming = false;
this.activeCount = Math.max(0, this.activeCount - 1);
this.isStreaming = this.activeCount > 0;
try { session.abortController = null; } catch { }
}
}
_normalize = (s) => String(s || '').replace(/[\r\t\u200B\u00A0]/g, '').replace(/\s+/g, ' ').replace(/^["'""'']+|["'""'']+$/g, '').trim();
_stripNamePrefix = (s) => String(s || '').replace(/^\s*[^:]{1,32}:\s*/, '');
_normStrip = (s) => this._normalize(this._stripNamePrefix(s));
_parseCompositeParam(param) {
const input = String(param || '').trim();
if (!input) return [];
try {
const parsed = JSON.parse(input);
if (Array.isArray(parsed)) {
const normRole = (r) => {
const x = String(r || '').trim().toLowerCase();
if (x === 'sys' || x === 'system') return 'system';
if (x === 'assistant' || x === 'asst' || x === 'ai') return 'assistant';
if (x === 'user' || x === 'u') return 'user';
return '';
};
const result = parsed
.filter(m => m && typeof m === 'object')
.map(m => ({ role: normRole(m.role), content: String(m.content || '') }))
.filter(m => m.role);
if (result.length > 0) {
return result;
}
}
} catch {
}
const parts = [];
let buf = '';
let depth = 0;
for (let i = 0; i < input.length; i++) {
const ch = input[i];
if (ch === '{') depth++;
if (ch === '}') depth = Math.max(0, depth - 1);
if (ch === ';' && depth === 0) {
parts.push(buf);
buf = '';
} else {
buf += ch;
}
}
if (buf) parts.push(buf);
const normRole = (r) => {
const x = String(r || '').trim().toLowerCase();
if (x === 'sys' || x === 'system') return 'system';
if (x === 'assistant' || x === 'asst' || x === 'ai') return 'assistant';
if (x === 'user' || x === 'u') return 'user';
return '';
};
const extractValue = (v) => {
let s = String(v || '').trim();
if ((s.startsWith('{') && s.endsWith('}')) ||
(s.startsWith('"') && s.endsWith('"')) ||
(s.startsWith('\'') && s.endsWith('\''))) {
s = s.slice(1, -1);
}
return s.trim();
};
const result = [];
for (const seg of parts) {
const idx = seg.indexOf('=');
if (idx === -1) continue;
const role = normRole(seg.slice(0, idx));
if (!role) continue;
const content = extractValue(seg.slice(idx + 1));
if (content || content === '') result.push({ role, content });
}
return result;
}
_createIsFromChat() {
const chatNorms = chat.map(m => this._normStrip(m?.mes)).filter(Boolean);
const chatSet = new Set(chatNorms);
return (content) => {
const n = this._normStrip(content);
if (!n || chatSet.has(n)) return !n ? false : true;
for (const c of chatNorms) {
const [a, b] = [n.length, c.length];
const [minL, maxL] = [Math.min(a, b), 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;
};
}
async _runToggleTask(task) {
const prev = this._toggleQueue;
let release;
this._toggleQueue = new Promise(r => (release = r));
await prev;
try { return await task(); }
finally { release(); }
}
async _withTemporaryPromptToggles(addonSet, fn) {
return this._runToggleTask(async () => {
const pm = promptManager;
if (!pm || typeof pm.getPromptOrderForCharacter !== 'function') {
return await fn();
}
// 记录原始状态
const hadOwn = Object.prototype.hasOwnProperty.call(pm, 'getPromptOrderForCharacter');
const original = pm.getPromptOrderForCharacter;
const PRESET_EXCLUDES = new Set([
'chatHistory',
'worldInfoBefore', 'worldInfoAfter',
'charDescription', 'charPersonality', 'scenario', 'personaDescription',
]);
const wrapper = (...args) => {
const list = original.call(pm, ...args) || [];
const enableIds = new Set();
if (addonSet.has('preset')) {
for (const e of list) {
if (e?.identifier && e.enabled && !PRESET_EXCLUDES.has(e.identifier)) {
enableIds.add(e.identifier);
}
}
}
if (addonSet.has('chatHistory')) enableIds.add('chatHistory');
if (addonSet.has('worldInfo')) {
enableIds.add('worldInfoBefore');
enableIds.add('worldInfoAfter');
}
if (addonSet.has('charDescription')) enableIds.add('charDescription');
if (addonSet.has('charPersonality')) enableIds.add('charPersonality');
if (addonSet.has('scenario')) enableIds.add('scenario');
if (addonSet.has('personaDescription')) enableIds.add('personaDescription');
if (addonSet.has('worldInfo') && !addonSet.has('chatHistory')) {
enableIds.add('chatHistory');
}
return list.map(e => {
if (!e?.identifier) return e;
return { ...e, enabled: enableIds.has(e.identifier) };
});
};
pm.getPromptOrderForCharacter = wrapper;
try {
return await fn();
} finally {
if (pm.getPromptOrderForCharacter === wrapper) {
if (hadOwn) {
pm.getPromptOrderForCharacter = original;
} else {
try {
delete pm.getPromptOrderForCharacter;
} catch {
pm.getPromptOrderForCharacter = original;
}
}
}
}
});
}
async _captureWorldInfoText(prompt) {
const addonSet = new Set(['worldInfo', 'chatHistory']);
const context = getContext();
let capturedData = null;
const dataListener = (data) => {
capturedData = (data && typeof data === 'object' && Array.isArray(data.prompt))
? { ...data, prompt: data.prompt.slice() }
: (Array.isArray(data) ? data.slice() : data);
};
eventSource.on(event_types.GENERATE_AFTER_DATA, dataListener);
const activatedUids = new Set();
const wiListener = (payload) => {
try {
const list = Array.isArray(payload?.entries)
? payload.entries
: (Array.isArray(payload) ? payload : (payload?.entry ? [payload.entry] : []));
for (const it of list) {
const uid = it?.uid || it?.id || it?.entry?.uid || it?.entry?.id;
if (uid) activatedUids.add(uid);
}
} catch {}
};
eventSource.on(event_types.WORLD_INFO_ACTIVATED, wiListener);
try {
await this._withTemporaryPromptToggles(addonSet, async () => {
await context.generate('normal', {
quiet_prompt: String(prompt || '').trim(),
quietToLoud: false,
skipWIAN: false,
force_name2: true,
}, true);
});
} finally {
eventSource.removeListener(event_types.GENERATE_AFTER_DATA, dataListener);
eventSource.removeListener(event_types.WORLD_INFO_ACTIVATED, wiListener);
}
try {
if (activatedUids.size > 0 && Array.isArray(world_info)) {
const seen = new Set();
const pieces = [];
for (const wi of world_info) {
const uid = wi?.uid || wi?.id;
if (!uid || !activatedUids.has(uid) || seen.has(uid)) continue;
seen.add(uid);
const content = String(wi?.content || '').trim();
if (content) pieces.push(content);
}
const text = pieces.join('\n\n').replace(/\n{3,}/g, '\n\n').trim();
if (text) return text;
}
} catch {}
let src = [];
const cd = capturedData;
if (Array.isArray(cd)) {
src = cd.slice();
} else if (cd && typeof cd === 'object' && Array.isArray(cd.prompt)) {
src = cd.prompt.slice();
}
const isFromChat = this._createIsFromChat();
const pieces = [];
for (const m of src) {
if (!m || typeof m.content !== 'string') continue;
if (m.role === 'system') {
pieces.push(m.content);
} else if ((m.role === 'user' || m.role === 'assistant') && isFromChat(m.content)) {
continue;
}
}
let text = pieces.map(s => String(s || '').trim()).filter(Boolean).join('\n\n').replace(/\n{3,}/g, '\n\n').trim();
text = text.replace(/\n{0,2}\s*\[Start a new Chat\]\s*\n?/ig, '\n');
text = text.replace(/\n{3,}/g, '\n\n').trim();
return text;
}
parseOpt(args, key) {
const v = args?.[key];
if (v === undefined) return undefined;
const s = String(v).trim().toLowerCase();
if (s === 'undefined' || s === 'none' || s === 'null' || s === 'off') return '__unset__';
const n = Number(v);
return Number.isFinite(n) ? n : undefined;
}
getActiveCharFields() {
const ctx = getContext();
const char = (ctx?.getCharacter?.(ctx?.characterId)) || (Array.isArray(ctx?.characters) ? ctx.characters[ctx.characterId] : null) || {};
const data = char.data || char || {};
const personaText =
(typeof power_user?.persona_description === 'string' ? power_user.persona_description : '') ||
String((ctx?.extensionSettings?.personas?.current?.description) || '').trim();
const mesExamples =
String(data.mes_example || data.mesExample || data.example_dialogs || '').trim();
return {
description: String(data.description || '').trim(),
personality: String(data.personality || '').trim(),
scenario: String(data.scenario || '').trim(),
persona: String(personaText || '').trim(),
mesExamples,
};
}
_extractTextFromMessage(msg) {
if (!msg) return '';
if (typeof msg.mes === 'string') return msg.mes.replace(/\r\n/g, '\n');
if (typeof msg.content === 'string') return msg.content.replace(/\r\n/g, '\n');
if (Array.isArray(msg.content)) {
return msg.content
.filter(p => p && p.type === 'text' && typeof p.text === 'string')
.map(p => p.text.replace(/\r\n/g, '\n')).join('\n');
}
return '';
}
_getLastMessagesSnapshot() {
const ctx = getContext();
const list = Array.isArray(ctx?.chat) ? ctx.chat : [];
let lastMessage = '';
let lastUserMessage = '';
let lastCharMessage = '';
for (let i = list.length - 1; i >= 0; i--) {
const m = list[i];
const text = this._extractTextFromMessage(m).trim();
if (!lastMessage && text) lastMessage = text;
if (!lastUserMessage && m?.is_user && text) lastUserMessage = text;
if (!lastCharMessage && !m?.is_user && !m?.is_system && text) lastCharMessage = text;
if (lastMessage && lastUserMessage && lastCharMessage) break;
}
return { lastMessage, lastUserMessage, lastCharMessage };
}
async expandInline(text) {
let out = String(text ?? '');
if (!out) return out;
const f = this.getActiveCharFields();
const dict = {
'{{description}}': f.description,
'{{personality}}': f.personality,
'{{scenario}}': f.scenario,
'{{persona}}': f.persona,
'{{mesexamples}}': f.mesExamples,
};
for (const [k, v] of Object.entries(dict)) {
if (!k) continue;
const re = new RegExp(k.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi');
out = out.replace(re, v || '');
}
const ctx = getContext();
out = String(out)
.replace(/\{\{user\}\}/gi, String(ctx?.name1 || 'User'))
.replace(/\{\{char\}\}/gi, String(ctx?.name2 || 'Assistant'))
.replace(/\{\{newline\}\}/gi, '\n');
out = out
.replace(/<\s*user\s*>/gi, String(ctx?.name1 || 'User'))
.replace(/<\s*(char|character)\s*>/gi, String(ctx?.name2 || 'Assistant'))
.replace(/<\s*persona\s*>/gi, String(f.persona || ''));
const snap = this._getLastMessagesSnapshot();
const lastDict = {
'{{lastmessage}}': snap.lastMessage,
'{{lastusermessage}}': snap.lastUserMessage,
'{{lastcharmessage}}': snap.lastCharMessage,
};
for (const [k, v] of Object.entries(lastDict)) {
const re = new RegExp(k.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi');
out = out.replace(re, (m) => (v && v.length ? v : ''));
}
const expandVarMacros = async (s) => {
if (typeof window?.STscript !== 'function') return s;
let txt = String(s);
const escapeForCmd = (v) => {
const escaped = String(v).replace(/\\/g, '\\\\').replace(/"/g, '\\"');
return `"${escaped}"`;
};
const apply = async (macroRe, getCmdForRoot) => {
const found = [];
let m;
macroRe.lastIndex = 0;
while ((m = macroRe.exec(txt)) !== null) {
const full = m[0];
const path = m[1]?.trim();
if (!path) continue;
found.push({ full, path });
}
if (!found.length) return;
const cache = new Map();
const getRootAndTail = (p) => {
const idx = p.indexOf('.');
return idx === -1 ? [p, ''] : [p.slice(0, idx), p.slice(idx + 1)];
};
const dig = (val, tail) => {
if (!tail) return val;
const parts = tail.split('.').filter(Boolean);
let cur = val;
for (const key of parts) {
if (cur && typeof cur === 'object' && key in cur) cur = cur[key];
else return '';
}
return cur;
};
const roots = [...new Set(found.map(item => getRootAndTail(item.path)[0]))];
await Promise.all(roots.map(async (root) => {
try {
const cmd = getCmdForRoot(root);
const result = await window.STscript(cmd);
let parsed = result;
try { parsed = JSON.parse(result); } catch {}
cache.set(root, parsed);
} catch {
cache.set(root, '');
}
}));
for (const item of found) {
const [root, tail] = getRootAndTail(item.path);
const rootVal = cache.get(root);
const val = tail ? dig(rootVal, tail) : rootVal;
const finalStr = typeof val === 'string' ? val : (val == null ? '' : JSON.stringify(val));
txt = txt.split(item.full).join(finalStr);
}
};
await apply(
/\{\{getvar::([\s\S]*?)\}\}/gi,
(root) => `/getvar key=${escapeForCmd(root)}`
);
await apply(
/\{\{getglobalvar::([\s\S]*?)\}\}/gi,
(root) => `/getglobalvar ${escapeForCmd(root)}`
);
return txt;
};
out = await expandVarMacros(out);
return out;
}
async xbgenrawCommand(args, prompt) {
const hasScaffolding = Boolean(String(
args?.top || args?.top64 ||
args?.topsys || args?.topuser || args?.topassistant ||
args?.bottom || args?.bottom64 ||
args?.bottomsys || args?.bottomuser || args?.bottomassistant ||
args?.addon || ''
).trim());
if (!prompt?.trim() && !hasScaffolding) return '';
const role = ['user', 'system', 'assistant'].includes(args?.as) ? args.as : 'user';
const sessionId = this._getSlotId(args?.id);
const lockArg = String(args?.lock || '').toLowerCase();
const lock = lockArg === 'on' || lockArg === 'true' || lockArg === '1';
const apiOptions = {
api: args?.api, apiurl: args?.apiurl,
apipassword: args?.apipassword, model: args?.model,
enableNet: ['on','true','1','yes'].includes(String(args?.net ?? '').toLowerCase()),
top_p: this.parseOpt(args, 'top_p'),
top_k: this.parseOpt(args, 'top_k'),
max_tokens: this.parseOpt(args, 'max_tokens'),
temperature: this.parseOpt(args, 'temperature'),
presence_penalty: this.parseOpt(args, 'presence_penalty'),
frequency_penalty: this.parseOpt(args, 'frequency_penalty'),
};
let parsedStop;
try {
if (args?.stop) {
const s = String(args.stop).trim();
if (s) {
const j = JSON.parse(s);
parsedStop = Array.isArray(j) ? j : (typeof j === 'string' ? [j] : undefined);
}
}
} catch {}
const nonstream = String(args?.nonstream || '').toLowerCase() === 'true';
const b64dUtf8 = (s) => {
try {
let str = String(s).trim().replace(/-/g, '+').replace(/_/g, '/');
const pad = str.length % 4 ? '='.repeat(4 - (str.length % 4)) : '';
str += pad;
const bin = atob(str);
const u8 = Uint8Array.from(bin, c => c.charCodeAt(0));
return new TextDecoder().decode(u8);
} catch { return ''; }
};
const topComposite = args?.top64 ? b64dUtf8(args.top64) : String(args?.top || '').trim();
const bottomComposite = args?.bottom64 ? b64dUtf8(args.bottom64) : String(args?.bottom || '').trim();
const createMsgs = (prefix) => {
const msgs = [];
['sys', 'user', 'assistant'].forEach(r => {
const content = String(args?.[`${prefix}${r === 'sys' ? 'sys' : r}`] || '').trim();
if (content) msgs.push({ role: r === 'sys' ? 'system' : r, content });
});
return msgs;
};
const historyPlaceholderRegex = /\{\$history(\d{1,3})\}/ig;
const resolveHistoryPlaceholder = async (text) => {
if (!text || typeof text !== 'string') return text;
const ctx = getContext();
const chatArr = Array.isArray(ctx?.chat) ? ctx.chat : [];
if (!chatArr.length) return text;
const extractText = (msg) => {
if (typeof msg?.mes === 'string') return msg.mes.replace(/\r\n/g, '\n');
if (typeof msg?.content === 'string') return msg.content.replace(/\r\n/g, '\n');
if (Array.isArray(msg?.content)) {
return msg.content
.filter(p => p && p.type === 'text' && typeof p.text === 'string')
.map(p => p.text.replace(/\r\n/g, '\n')).join('\n');
}
return '';
};
const replaceFn = (match, countStr) => {
const count = Math.max(1, Math.min(200, Number(countStr)));
const start = Math.max(0, chatArr.length - count);
const lines = [];
for (let i = start; i < chatArr.length; i++) {
const msg = chatArr[i];
const isUser = !!msg?.is_user;
const speaker = isUser
? ((msg?.name && String(msg.name).trim()) || (ctx?.name1 && String(ctx.name1).trim()) || 'USER')
: ((msg?.name && String(msg.name).trim()) || (ctx?.name2 && String(ctx.name2).trim()) || 'ASSISTANT');
lines.push(`${speaker}`);
const textContent = (extractText(msg) || '').trim();
if (textContent) lines.push(textContent);
lines.push('');
}
return lines.join('\n').replace(/\n{3,}/g, '\n\n').trim();
};
return text.replace(historyPlaceholderRegex, replaceFn);
};
const mapHistoryPlaceholders = async (messages) => {
const out = [];
for (const m of messages) {
if (!m) continue;
const content = await resolveHistoryPlaceholder(m.content);
out.push({ ...m, content });
}
return out;
};
let topMsgs = await mapHistoryPlaceholders(
[]
.concat(topComposite ? this._parseCompositeParam(topComposite) : [])
.concat(createMsgs('top'))
);
let bottomMsgs = await mapHistoryPlaceholders(
[]
.concat(bottomComposite ? this._parseCompositeParam(bottomComposite) : [])
.concat(createMsgs('bottom'))
);
const expandSegmentInline = async (arr) => {
for (const m of arr) {
if (m && typeof m.content === 'string') {
const before = m.content;
const after = await this.expandInline(before);
m.content = after && after.length ? after : before;
}
}
};
await expandSegmentInline(topMsgs);
await expandSegmentInline(bottomMsgs);
if (typeof prompt === 'string' && prompt.trim()) {
const beforeP = await resolveHistoryPlaceholder(prompt);
const afterP = await this.expandInline(beforeP);
prompt = afterP && afterP.length ? afterP : beforeP;
}
try {
const needsWI = [...topMsgs, ...bottomMsgs].some(m => m && typeof m.content === 'string' && m.content.includes('{$worldInfo}')) || (typeof prompt === 'string' && prompt.includes('{$worldInfo}'));
if (needsWI) {
const wiText = await this._captureWorldInfoText(prompt || '');
const wiTrim = String(wiText || '').trim();
if (wiTrim) {
const wiRegex = /\{\$worldInfo\}/ig;
const applyWI = (arr) => {
for (const m of arr) {
if (m && typeof m.content === 'string') {
m.content = m.content.replace(wiRegex, wiTrim);
}
}
};
applyWI(topMsgs);
applyWI(bottomMsgs);
if (typeof prompt === 'string') prompt = prompt.replace(wiRegex, wiTrim);
}
}
} catch {}
const addonSetStr = String(args?.addon || '').trim();
const shouldUsePM = addonSetStr.length > 0;
if (!shouldUsePM) {
const messages = []
.concat(topMsgs.filter(m => typeof m?.content === 'string' && m.content.trim().length))
.concat(prompt && prompt.trim().length ? [{ role, content: prompt.trim() }] : [])
.concat(bottomMsgs.filter(m => typeof m?.content === 'string' && m.content.trim().length));
const common = { messages, apiOptions, stop: parsedStop };
if (nonstream) {
try { if (lock) deactivateSendButtons(); } catch {}
try {
await this._emitPromptReady(messages);
const finalText = await this.processGeneration(common, prompt || '', sessionId, false);
return String(finalText ?? '');
} finally {
try { if (lock) activateSendButtons(); } catch {}
}
} else {
try { if (lock) deactivateSendButtons(); } catch {}
await this._emitPromptReady(messages);
const p = this.processGeneration(common, prompt || '', sessionId, true);
p.finally(() => { try { if (lock) activateSendButtons(); } catch {} });
p.catch(() => {});
return String(sessionId);
}
}
const addonSet = new Set(addonSetStr.split(',').map(s => s.trim()).filter(Boolean));
const buildAddonFinalMessages = async () => {
const context = getContext();
let capturedData = null;
const dataListener = (data) => {
capturedData = (data && typeof data === 'object' && Array.isArray(data.prompt))
? { ...data, prompt: data.prompt.slice() }
: (Array.isArray(data) ? data.slice() : data);
};
eventSource.on(event_types.GENERATE_AFTER_DATA, dataListener);
const skipWIAN = addonSet.has('worldInfo') ? false : true;
await this._withTemporaryPromptToggles(addonSet, async () => {
const sandboxed = addonSet.has('worldInfo') && !addonSet.has('chatHistory');
let chatBackup = null;
if (sandboxed) {
try {
chatBackup = chat.slice();
chat.length = 0;
chat.push({ name: name1 || 'User', is_user: true, is_system: false, mes: '[hist]', send_date: new Date().toISOString() });
} catch {}
}
try {
await context.generate('normal', {
quiet_prompt: (prompt || '').trim(), quietToLoud: false,
skipWIAN, force_name2: true
}, true);
} finally {
if (sandboxed && Array.isArray(chatBackup)) {
chat.length = 0;
chat.push(...chatBackup);
}
}
});
eventSource.removeListener(event_types.GENERATE_AFTER_DATA, dataListener);
let src = [];
const cd = capturedData;
if (Array.isArray(cd)) src = cd.slice();
else if (cd && typeof cd === 'object' && Array.isArray(cd.prompt)) src = cd.prompt.slice();
const sandboxedAfter = addonSet.has('worldInfo') && !addonSet.has('chatHistory');
const isFromChat = this._createIsFromChat();
const finalPromptMessages = src.filter(m => {
if (!sandboxedAfter) return true;
if (!m) return false;
if (m.role === 'system') return true;
if ((m.role === 'user' || m.role === 'assistant') && isFromChat(m.content)) return false;
return true;
});
const norm = this._normStrip;
const position = ['history', 'after_history', 'afterhistory', 'chathistory']
.includes(String(args?.position || '').toLowerCase()) ? 'history' : 'bottom';
const targetIdx = finalPromptMessages.findIndex(m => m && typeof m.content === 'string' && norm(m.content) === norm(prompt || ''));
if (targetIdx !== -1) {
finalPromptMessages.splice(targetIdx, 1);
}
if (prompt?.trim()) {
const centerMsg = { role: (args?.as || 'assistant'), content: prompt.trim() };
if (position === 'history') {
let lastHistoryIndex = -1;
const isFromChat2 = this._createIsFromChat();
for (let i = 0; i < finalPromptMessages.length; i++) {
const m = finalPromptMessages[i];
if (m && (m.role === 'user' || m.role === 'assistant') && isFromChat2(m.content)) {
lastHistoryIndex = i;
}
}
if (lastHistoryIndex >= 0) finalPromptMessages.splice(lastHistoryIndex + 1, 0, centerMsg);
else {
let lastSystemIndex = -1;
for (let i = 0; i < finalPromptMessages.length; i++) {
if (finalPromptMessages[i]?.role === 'system') lastSystemIndex = i;
}
if (lastSystemIndex >= 0) finalPromptMessages.splice(lastSystemIndex + 1, 0, centerMsg);
else finalPromptMessages.push(centerMsg);
}
} else {
finalPromptMessages.push(centerMsg);
}
}
const mergedOnce = ([]).concat(topMsgs).concat(finalPromptMessages).concat(bottomMsgs);
const seenKey = new Set();
const finalMessages = [];
for (const m of mergedOnce) {
if (!m || !m.content || !String(m.content).trim().length) continue;
const key = `${m.role}:${this._normStrip(m.content)}`;
if (seenKey.has(key)) continue;
seenKey.add(key);
finalMessages.push(m);
}
return finalMessages;
};
if (nonstream) {
try { if (lock) deactivateSendButtons(); } catch {}
try {
const finalMessages = await buildAddonFinalMessages();
const common = { messages: finalMessages, apiOptions, stop: parsedStop };
await this._emitPromptReady(finalMessages);
const finalText = await this.processGeneration(common, prompt || '', sessionId, false);
return String(finalText ?? '');
} finally {
try { if (lock) activateSendButtons(); } catch {}
}
} else {
(async () => {
try {
try { if (lock) deactivateSendButtons(); } catch {}
const finalMessages = await buildAddonFinalMessages();
const common = { messages: finalMessages, apiOptions, stop: parsedStop };
await this._emitPromptReady(finalMessages);
await this.processGeneration(common, prompt || '', sessionId, true);
} catch {} finally {
try { if (lock) activateSendButtons(); } catch {}
}
})();
return String(sessionId);
}
}
async xbgenCommand(args, prompt) {
if (!prompt?.trim()) return '';
const role = ['user', 'system', 'assistant'].includes(args?.as) ? args.as : 'system';
const sessionId = this._getSlotId(args?.id);
const lockArg = String(args?.lock || '').toLowerCase();
const lock = lockArg === 'on' || lockArg === 'true' || lockArg === '1';
const nonstream = String(args?.nonstream || '').toLowerCase() === 'true';
const buildGenDataWithOptions = async () => {
const context = getContext();
const tempMessage = {
name: role === 'user' ? (name1 || 'User') : 'System',
is_user: role === 'user',
is_system: role === 'system',
mes: prompt.trim(),
send_date: new Date().toISOString(),
};
const originalLength = chat.length;
chat.push(tempMessage);
let capturedData = null;
const dataListener = (data) => {
if (data?.prompt && Array.isArray(data.prompt)) {
let messages = [...data.prompt];
const promptText = prompt.trim();
for (let i = messages.length - 1; i >= 0; i--) {
const m = messages[i];
if (m.content === promptText &&
((role !== 'system' && m.role === 'system') ||
(role === 'system' && m.role === 'user'))) {
messages.splice(i, 1);
break;
}
}
capturedData = { ...data, prompt: messages };
} else {
capturedData = data;
}
};
eventSource.on(event_types.GENERATE_AFTER_DATA, dataListener);
try {
await context.generate('normal', {
quiet_prompt: prompt.trim(), quietToLoud: false,
skipWIAN: false, force_name2: true
}, true);
} finally {
eventSource.removeListener(event_types.GENERATE_AFTER_DATA, dataListener);
chat.length = originalLength;
}
const apiOptions = {
api: args?.api, apiurl: args?.apiurl,
apipassword: args?.apipassword, model: args?.model,
enableNet: ['on','true','1','yes'].includes(String(args?.net ?? '').toLowerCase()),
top_p: this.parseOpt(args, 'top_p'),
top_k: this.parseOpt(args, 'top_k'),
max_tokens: this.parseOpt(args, 'max_tokens'),
temperature: this.parseOpt(args, 'temperature'),
presence_penalty: this.parseOpt(args, 'presence_penalty'),
frequency_penalty: this.parseOpt(args, 'frequency_penalty'),
};
const cd = capturedData;
let finalPromptMessages = [];
if (cd && typeof cd === 'object' && Array.isArray(cd.prompt)) {
finalPromptMessages = cd.prompt.slice();
} else if (Array.isArray(cd)) {
finalPromptMessages = cd.slice();
}
const norm = this._normStrip;
const promptNorm = norm(prompt);
for (let i = finalPromptMessages.length - 1; i >= 0; i--) {
if (norm(finalPromptMessages[i]?.content) === promptNorm) {
finalPromptMessages.splice(i, 1);
}
}
const messageToInsert = { role, content: prompt.trim() };
const position = ['history', 'after_history', 'afterhistory', 'chathistory']
.includes(String(args?.position || '').toLowerCase()) ? 'history' : 'bottom';
if (position === 'history') {
const isFromChat = this._createIsFromChat();
let lastHistoryIndex = -1;
for (let i = 0; i < finalPromptMessages.length; i++) {
const m = finalPromptMessages[i];
if (m && (m.role === 'user' || m.role === 'assistant') && isFromChat(m.content)) {
lastHistoryIndex = i;
}
}
if (lastHistoryIndex >= 0) {
finalPromptMessages.splice(lastHistoryIndex + 1, 0, messageToInsert);
} else {
finalPromptMessages.push(messageToInsert);
}
} else {
finalPromptMessages.push(messageToInsert);
}
const cd2 = capturedData;
let dataWithOptions;
if (cd2 && typeof cd2 === 'object' && !Array.isArray(cd2)) {
dataWithOptions = Object.assign({}, cd2, { prompt: finalPromptMessages, apiOptions });
} else {
dataWithOptions = { messages: finalPromptMessages, apiOptions };
}
return dataWithOptions;
};
if (nonstream) {
try { if (lock) deactivateSendButtons(); } catch {}
try {
const dataWithOptions = await buildGenDataWithOptions();
const chatMsgs = Array.isArray(dataWithOptions?.prompt) ? dataWithOptions.prompt
: (Array.isArray(dataWithOptions?.messages) ? dataWithOptions.messages : []);
await this._emitPromptReady(chatMsgs);
const finalText = await this.processGeneration(dataWithOptions, prompt, sessionId, false);
return String(finalText ?? '');
} finally {
try { if (lock) activateSendButtons(); } catch {}
}
}
(async () => {
try {
try { if (lock) deactivateSendButtons(); } catch {}
const dataWithOptions = await buildGenDataWithOptions();
const chatMsgs = Array.isArray(dataWithOptions?.prompt) ? dataWithOptions.prompt
: (Array.isArray(dataWithOptions?.messages) ? dataWithOptions.messages : []);
await this._emitPromptReady(chatMsgs);
const finalText = await this.processGeneration(dataWithOptions, prompt, sessionId, true);
try { if (args && args._scope) args._scope.pipe = String(finalText ?? ''); } catch {}
} catch {}
finally {
try { if (lock) activateSendButtons(); } catch {}
}
})();
return String(sessionId);
}
registerCommands() {
const commonArgs = [
{ name: 'id', description: '会话ID', typeList: [ARGUMENT_TYPE.STRING] },
{ name: 'api', description: '后端: openai/claude/gemini/cohere/deepseek/custom', typeList: [ARGUMENT_TYPE.STRING] },
{ name: 'net', description: '联网 on/off', typeList: [ARGUMENT_TYPE.STRING], enumList: ['on','off'] },
{ name: 'apiurl', description: '自定义后端URL', typeList: [ARGUMENT_TYPE.STRING] },
{ name: 'apipassword', description: '后端密码', typeList: [ARGUMENT_TYPE.STRING] },
{ name: 'model', description: '模型名', typeList: [ARGUMENT_TYPE.STRING] },
{ name: 'position', description: '插入位置bottom/history', typeList: [ARGUMENT_TYPE.STRING], enumList: ['bottom', 'history'] },
{ name: 'temperature', description: '温度', typeList: [ARGUMENT_TYPE.STRING] },
{ name: 'presence_penalty', description: '存在惩罚', typeList: [ARGUMENT_TYPE.STRING] },
{ name: 'frequency_penalty', description: '频率惩罚', typeList: [ARGUMENT_TYPE.STRING] },
{ name: 'top_p', description: 'Top P', typeList: [ARGUMENT_TYPE.STRING] },
{ name: 'top_k', description: 'Top K', typeList: [ARGUMENT_TYPE.STRING] },
{ name: 'max_tokens', description: '最大回复长度', typeList: [ARGUMENT_TYPE.STRING] },
];
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'xbgen',
callback: (args, prompt) => this.xbgenCommand(args, prompt),
namedArgumentList: [
{ name: 'as', description: '消息角色', typeList: [ARGUMENT_TYPE.STRING], defaultValue: 'system', enumList: ['user', 'system', 'assistant'] },
{ name: 'nonstream', description: '非流式true/false', typeList: [ARGUMENT_TYPE.STRING], enumList: ['true', 'false'] },
{ name: 'lock', description: '生成时锁定输入 on/off', typeList: [ARGUMENT_TYPE.STRING], enumList: ['on', 'off'] },
...commonArgs
].map(SlashCommandNamedArgument.fromProps),
unnamedArgumentList: [SlashCommandArgument.fromProps({
description: '生成提示文本', typeList: [ARGUMENT_TYPE.STRING], isRequired: true
})],
helpString: '使用完整上下文进行流式生成',
returns: 'session ID'
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'xbgenraw',
callback: (args, prompt) => this.xbgenrawCommand(args, prompt),
namedArgumentList: [
{ name: 'as', description: '消息角色', typeList: [ARGUMENT_TYPE.STRING], defaultValue: 'user', enumList: ['user', 'system', 'assistant'] },
{ name: 'nonstream', description: '非流式true/false', typeList: [ARGUMENT_TYPE.STRING], enumList: ['true', 'false'] },
{ name: 'lock', description: '生成时锁定输入 on/off', typeList: [ARGUMENT_TYPE.STRING], enumList: ['on', 'off'] },
{ name: 'addon', description: '附加上下文', typeList: [ARGUMENT_TYPE.STRING] },
{ name: 'topsys', description: '置顶 system', typeList: [ARGUMENT_TYPE.STRING] },
{ name: 'topuser', description: '置顶 user', typeList: [ARGUMENT_TYPE.STRING] },
{ name: 'topassistant', description: '置顶 assistant', typeList: [ARGUMENT_TYPE.STRING] },
{ name: 'bottomsys', description: '置底 system', typeList: [ARGUMENT_TYPE.STRING] },
{ name: 'bottomuser', description: '置底 user', typeList: [ARGUMENT_TYPE.STRING] },
{ name: 'bottomassistant', description: '置底 assistant', typeList: [ARGUMENT_TYPE.STRING] },
{ name: 'top', description: '复合置顶: assistant={A};user={B};sys={C}', typeList: [ARGUMENT_TYPE.STRING] },
{ name: 'bottom', description: '复合置底: assistant={C};sys={D1}', typeList: [ARGUMENT_TYPE.STRING] },
{ name: 'top64', description: '复合置顶(base64-url安全编码)', typeList: [ARGUMENT_TYPE.STRING] },
{ name: 'bottom64', description: '复合置底(base64-url安全编码)', typeList: [ARGUMENT_TYPE.STRING] },
...commonArgs
].map(SlashCommandNamedArgument.fromProps),
unnamedArgumentList: [SlashCommandArgument.fromProps({
description: '原始提示文本', typeList: [ARGUMENT_TYPE.STRING], isRequired: false
})],
helpString: '使用原始提示进行流式生成',
returns: 'session ID'
}));
}
getLastGeneration = (sessionId) => sessionId !== undefined ?
(this.sessions.get(this._getSlotId(sessionId))?.text || '') : this.tempreply;
getStatus = (sessionId) => {
if (sessionId !== undefined) {
const sid = this._getSlotId(sessionId);
const s = this.sessions.get(sid);
return s ? { isStreaming: !!s.isStreaming, text: s.text, sessionId: sid }
: { isStreaming: false, text: '', sessionId: sid };
}
return { isStreaming: !!this.isStreaming, text: this.tempreply };
};
startSession = (id, prompt) => this._ensureSession(id, prompt).id;
getLastSessionId = () => this.lastSessionId;
cancel(sessionId) {
const s = this.sessions.get(this._getSlotId(sessionId));
s?.abortController?.abort();
}
cleanup() {
this.sessions.forEach(s => s.abortController?.abort());
Object.assign(this, {
sessions: new Map(), tempreply: '', lastSessionId: null,
activeCount: 0, isInitialized: false, isStreaming: false
});
}
}
const streamingGeneration = new StreamingGeneration();
CacheRegistry.register('streamingGeneration', {
name: '流式生成会话',
getSize: () => streamingGeneration?.sessions?.size || 0,
getBytes: () => {
try {
let bytes = String(streamingGeneration?.tempreply || '').length * 2; // UTF-16
streamingGeneration?.sessions?.forEach?.((s) => {
bytes += (String(s?.prompt || '').length + String(s?.text || '').length) * 2; // UTF-16
});
return bytes;
} catch {
return 0;
}
},
clear: () => {
try { streamingGeneration.cleanup(); } catch {}
},
getDetail: () => {
try {
const sessions = Array.from(streamingGeneration.sessions?.values?.() || []);
return sessions.map(s => ({
id: s?.id,
isStreaming: !!s?.isStreaming,
promptLen: String(s?.prompt || '').length,
textLen: String(s?.text || '').length,
updatedAt: s?.updatedAt || 0,
}));
} catch {
return [];
}
},
});
export function initStreamingGeneration() {
const w = window;
if ((w)?.isXiaobaixEnabled === false) return;
try { xbLog.info('streamingGeneration', 'initStreamingGeneration'); } catch {}
streamingGeneration.init();
(w)?.registerModuleCleanup?.('streamingGeneration', () => streamingGeneration.cleanup());
}
export { streamingGeneration };
if (typeof window !== 'undefined') {
Object.assign(window, {
xiaobaixStreamingGeneration: streamingGeneration,
eventSource: (window)?.eventSource || eventSource
});
}