From e56cfa6a180f2e9881982db40e2284adc5610c8b Mon Sep 17 00:00:00 2001 From: RT15548 Date: Wed, 21 Jan 2026 11:40:37 +0800 Subject: [PATCH] =?UTF-8?q?=E8=B0=83=E6=95=B4=E4=B8=BA=E9=BB=98=E8=AE=A4?= =?UTF-8?q?=E4=B8=8D=E4=BC=A0=E5=8F=82=E6=95=B0=E7=9A=84=E6=A8=A1=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/streaming-generation.js | 2847 +++++++++++++++---------------- 1 file changed, 1417 insertions(+), 1430 deletions(-) diff --git a/modules/streaming-generation.js b/modules/streaming-generation.js index 6ec3258..551e8ac 100644 --- a/modules/streaming-generation.js +++ b/modules/streaming-generation.js @@ -1,1430 +1,1417 @@ -// 删掉: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 - }); -} +// 删掉: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__'; + // 只使用命令参数,不从 UI 设置读取 + const effectiveTemperature = isUnset('temperature') ? undefined : num(baseOptions?.temperature); + const effectivePresence = isUnset('presence_penalty') ? undefined : num(baseOptions?.presence_penalty); + const effectiveFrequency = isUnset('frequency_penalty') ? undefined : num(baseOptions?.frequency_penalty); + const effectiveTopP = isUnset('top_p') ? undefined : num(baseOptions?.top_p); + const effectiveTopK = isUnset('top_k') ? undefined : num(baseOptions?.top_k); + const effectiveMaxT = isUnset('max_tokens') ? undefined : num(baseOptions?.max_tokens); + + 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 + }); +}