Merge branch 'main' of https://github.com/RT15548/LittleWhiteBox
This commit is contained in:
@@ -1,23 +1,23 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import { oai_settings, chat_completion_sources, getChatCompletionModel, promptManager } from "../../../../openai.js";
|
import { oai_settings, chat_completion_sources, getChatCompletionModel, promptManager, openai_setting_names, openai_settings } from "../../../../openai.js";
|
||||||
import { ChatCompletionService } from "../../../../custom-request.js";
|
import { ChatCompletionService } from "../../../../custom-request.js";
|
||||||
import { eventSource, event_types } from "../../../../../script.js";
|
import { eventSource, event_types } from "../../../../../script.js";
|
||||||
import { getContext } from "../../../../st-context.js";
|
import { getContext } from "../../../../st-context.js";
|
||||||
import { xbLog } from "../core/debug-core.js";
|
import { xbLog } from "../core/debug-core.js";
|
||||||
|
|
||||||
const SOURCE_TAG = 'xiaobaix-host';
|
const SOURCE_TAG = 'xiaobaix-host';
|
||||||
|
|
||||||
const POSITIONS = Object.freeze({ BEFORE_PROMPT: 'BEFORE_PROMPT', IN_PROMPT: 'IN_PROMPT', IN_CHAT: 'IN_CHAT', AFTER_COMPONENT: 'AFTER_COMPONENT' });
|
const POSITIONS = Object.freeze({ BEFORE_PROMPT: 'BEFORE_PROMPT', IN_PROMPT: 'IN_PROMPT', IN_CHAT: 'IN_CHAT', AFTER_COMPONENT: 'AFTER_COMPONENT' });
|
||||||
const KNOWN_KEYS = Object.freeze(new Set([
|
const KNOWN_KEYS = Object.freeze(new Set([
|
||||||
'main', 'chatHistory', 'worldInfo', 'worldInfoBefore', 'worldInfoAfter',
|
'main', 'chatHistory', 'worldInfo', 'worldInfoBefore', 'worldInfoAfter',
|
||||||
'charDescription', 'charPersonality', 'scenario', 'personaDescription',
|
'charDescription', 'charPersonality', 'scenario', 'personaDescription',
|
||||||
'dialogueExamples', 'authorsNote', 'vectorsMemory', 'vectorsDataBank',
|
'dialogueExamples', 'authorsNote', 'vectorsMemory', 'vectorsDataBank',
|
||||||
'smartContext', 'jailbreak', 'nsfw', 'summary', 'bias', 'impersonate', 'quietPrompt',
|
'smartContext', 'jailbreak', 'nsfw', 'summary', 'bias', 'impersonate', 'quietPrompt',
|
||||||
]));
|
]));
|
||||||
const resolveTargetOrigin = (origin) => {
|
const resolveTargetOrigin = (origin) => {
|
||||||
if (typeof origin === 'string' && origin) return origin;
|
if (typeof origin === 'string' && origin) return origin;
|
||||||
try { return window.location.origin; } catch { return '*'; }
|
try { return window.location.origin; } catch { return '*'; }
|
||||||
};
|
};
|
||||||
|
|
||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
class CallGenerateService {
|
class CallGenerateService {
|
||||||
@@ -48,11 +48,11 @@ class CallGenerateService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sendError(sourceWindow, requestId, streamingEnabled, err, fallbackCode = 'API_ERROR', details = null, targetOrigin = null) {
|
sendError(sourceWindow, requestId, streamingEnabled, err, fallbackCode = 'API_ERROR', details = null, targetOrigin = null) {
|
||||||
const e = this.normalizeError(err, fallbackCode, details);
|
const e = this.normalizeError(err, fallbackCode, details);
|
||||||
const type = streamingEnabled ? 'generateStreamError' : 'generateError';
|
const type = streamingEnabled ? 'generateStreamError' : 'generateError';
|
||||||
try { sourceWindow?.postMessage({ source: SOURCE_TAG, type, id: requestId, error: e }, resolveTargetOrigin(targetOrigin)); } catch {}
|
try { sourceWindow?.postMessage({ source: SOURCE_TAG, type, id: requestId, error: e }, resolveTargetOrigin(targetOrigin)); } catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string|undefined} rawId
|
* @param {string|undefined} rawId
|
||||||
@@ -257,11 +257,11 @@ class CallGenerateService {
|
|||||||
* @param {string} type
|
* @param {string} type
|
||||||
* @param {object} body
|
* @param {object} body
|
||||||
*/
|
*/
|
||||||
postToTarget(target, type, body, targetOrigin = null) {
|
postToTarget(target, type, body, targetOrigin = null) {
|
||||||
try {
|
try {
|
||||||
target?.postMessage({ source: SOURCE_TAG, type, ...body }, resolveTargetOrigin(targetOrigin));
|
target?.postMessage({ source: SOURCE_TAG, type, ...body }, resolveTargetOrigin(targetOrigin));
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== ST Prompt 干跑捕获与组件切换 =====
|
// ===== ST Prompt 干跑捕获与组件切换 =====
|
||||||
|
|
||||||
@@ -386,6 +386,43 @@ class CallGenerateService {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 临时切换到指定 preset 执行 fn,执行完毕后恢复 oai_settings。
|
||||||
|
* 模式与 _withPromptToggle 一致:snapshot → 覆写 → fn() → finally 恢复。
|
||||||
|
* @param {string} presetName - preset 名称
|
||||||
|
* @param {Function} fn - 要在 preset 上下文中执行的异步函数
|
||||||
|
*/
|
||||||
|
async _withTemporaryPreset(presetName, fn) {
|
||||||
|
if (!presetName) return await fn();
|
||||||
|
const idx = openai_setting_names?.[presetName];
|
||||||
|
if (idx === undefined || idx === null) {
|
||||||
|
throw new Error(`Preset "${presetName}" not found`);
|
||||||
|
}
|
||||||
|
const preset = openai_settings?.[idx];
|
||||||
|
if (!preset || typeof preset !== 'object') {
|
||||||
|
throw new Error(`Preset "${presetName}" data is invalid`);
|
||||||
|
}
|
||||||
|
let snapshot;
|
||||||
|
try { snapshot = structuredClone(oai_settings); }
|
||||||
|
catch { snapshot = JSON.parse(JSON.stringify(oai_settings)); }
|
||||||
|
try {
|
||||||
|
let presetClone;
|
||||||
|
try { presetClone = structuredClone(preset); }
|
||||||
|
catch { presetClone = JSON.parse(JSON.stringify(preset)); }
|
||||||
|
for (const key of Object.keys(presetClone)) {
|
||||||
|
oai_settings[key] = presetClone[key];
|
||||||
|
}
|
||||||
|
return await fn();
|
||||||
|
} finally {
|
||||||
|
for (const key of Object.keys(oai_settings)) {
|
||||||
|
if (!Object.prototype.hasOwnProperty.call(snapshot, key)) {
|
||||||
|
try { delete oai_settings[key]; } catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Object.assign(oai_settings, snapshot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ===== 工具函数:组件与消息辅助 =====
|
// ===== 工具函数:组件与消息辅助 =====
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1008,7 +1045,7 @@ class CallGenerateService {
|
|||||||
|
|
||||||
_applyContentFilter(list, filterCfg) {
|
_applyContentFilter(list, filterCfg) {
|
||||||
if (!filterCfg) return list;
|
if (!filterCfg) return list;
|
||||||
const { contains, regex, fromUserNames } = filterCfg;
|
const { contains, regex, fromUserNames } = filterCfg;
|
||||||
let out = list.slice();
|
let out = list.slice();
|
||||||
if (contains) {
|
if (contains) {
|
||||||
const needles = Array.isArray(contains) ? contains : [contains];
|
const needles = Array.isArray(contains) ? contains : [contains];
|
||||||
@@ -1132,7 +1169,7 @@ class CallGenerateService {
|
|||||||
|
|
||||||
// ===== 发送实现(构建后的统一发送) =====
|
// ===== 发送实现(构建后的统一发送) =====
|
||||||
|
|
||||||
async _sendMessages(messages, options, requestId, sourceWindow, targetOrigin = null) {
|
async _sendMessages(messages, options, requestId, sourceWindow, targetOrigin = null) {
|
||||||
const sessionId = this.normalizeSessionId(options?.session?.id || 'xb1');
|
const sessionId = this.normalizeSessionId(options?.session?.id || 'xb1');
|
||||||
const session = this.ensureSession(sessionId);
|
const session = this.ensureSession(sessionId);
|
||||||
const streamingEnabled = options?.streaming?.enabled !== false; // 默认开
|
const streamingEnabled = options?.streaming?.enabled !== false; // 默认开
|
||||||
@@ -1143,11 +1180,11 @@ class CallGenerateService {
|
|||||||
const shouldExport = !!(options?.debug?.enabled || options?.debug?.exportPrompt);
|
const shouldExport = !!(options?.debug?.enabled || options?.debug?.exportPrompt);
|
||||||
const already = options?.debug?._exported === true;
|
const already = options?.debug?._exported === true;
|
||||||
if (shouldExport && !already) {
|
if (shouldExport && !already) {
|
||||||
this.postToTarget(sourceWindow, 'generatePromptPreview', { id: requestId, messages: (messages || []).map(m => ({ role: m.role, content: m.content })) }, targetOrigin);
|
this.postToTarget(sourceWindow, 'generatePromptPreview', { id: requestId, messages: (messages || []).map(m => ({ role: m.role, content: m.content })) }, targetOrigin);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (streamingEnabled) {
|
if (streamingEnabled) {
|
||||||
this.postToTarget(sourceWindow, 'generateStreamStart', { id: requestId, sessionId }, targetOrigin);
|
this.postToTarget(sourceWindow, 'generateStreamStart', { id: requestId, sessionId }, targetOrigin);
|
||||||
const streamFn = await ChatCompletionService.sendRequest(payload, false, session.abortController.signal);
|
const streamFn = await ChatCompletionService.sendRequest(payload, false, session.abortController.signal);
|
||||||
let last = '';
|
let last = '';
|
||||||
const generator = typeof streamFn === 'function' ? streamFn() : null;
|
const generator = typeof streamFn === 'function' ? streamFn() : null;
|
||||||
@@ -1155,7 +1192,7 @@ class CallGenerateService {
|
|||||||
const chunk = text.slice(last.length);
|
const chunk = text.slice(last.length);
|
||||||
last = text;
|
last = text;
|
||||||
session.accumulated = text;
|
session.accumulated = text;
|
||||||
this.postToTarget(sourceWindow, 'generateStreamChunk', { id: requestId, chunk, accumulated: text, metadata: {} }, targetOrigin);
|
this.postToTarget(sourceWindow, 'generateStreamChunk', { id: requestId, chunk, accumulated: text, metadata: {} }, targetOrigin);
|
||||||
}
|
}
|
||||||
const result = {
|
const result = {
|
||||||
success: true,
|
success: true,
|
||||||
@@ -1163,7 +1200,7 @@ class CallGenerateService {
|
|||||||
sessionId,
|
sessionId,
|
||||||
metadata: { duration: Date.now() - session.startedAt, model: apiCfg.model, finishReason: 'stop' },
|
metadata: { duration: Date.now() - session.startedAt, model: apiCfg.model, finishReason: 'stop' },
|
||||||
};
|
};
|
||||||
this.postToTarget(sourceWindow, 'generateStreamComplete', { id: requestId, result }, targetOrigin);
|
this.postToTarget(sourceWindow, 'generateStreamComplete', { id: requestId, result }, targetOrigin);
|
||||||
return result;
|
return result;
|
||||||
} else {
|
} else {
|
||||||
const extracted = await ChatCompletionService.sendRequest(payload, true, session.abortController.signal);
|
const extracted = await ChatCompletionService.sendRequest(payload, true, session.abortController.signal);
|
||||||
@@ -1173,20 +1210,25 @@ class CallGenerateService {
|
|||||||
sessionId,
|
sessionId,
|
||||||
metadata: { duration: Date.now() - session.startedAt, model: apiCfg.model, finishReason: 'stop' },
|
metadata: { duration: Date.now() - session.startedAt, model: apiCfg.model, finishReason: 'stop' },
|
||||||
};
|
};
|
||||||
this.postToTarget(sourceWindow, 'generateResult', { id: requestId, result }, targetOrigin);
|
this.postToTarget(sourceWindow, 'generateResult', { id: requestId, result }, targetOrigin);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.sendError(sourceWindow, requestId, streamingEnabled, err, 'API_ERROR', null, targetOrigin);
|
this.sendError(sourceWindow, requestId, streamingEnabled, err, 'API_ERROR', null, targetOrigin);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== 主流程 =====
|
// ===== 主流程 =====
|
||||||
async handleRequestInternal(options, requestId, sourceWindow, targetOrigin = null) {
|
async handleRequestInternal(options, requestId, sourceWindow, targetOrigin = null, { assembleOnly = false } = {}) {
|
||||||
// 1) 校验
|
// 1) 校验
|
||||||
this.validateOptions(options);
|
this.validateOptions(options);
|
||||||
|
|
||||||
|
const presetName = options?.preset || null;
|
||||||
|
|
||||||
|
// 步骤 2~9 的核心逻辑,可能包裹在 _withTemporaryPreset 中执行
|
||||||
|
const executeCore = async () => {
|
||||||
|
|
||||||
// 2) 解析组件列表与内联注入
|
// 2) 解析组件列表与内联注入
|
||||||
const list = Array.isArray(options?.components?.list) ? options.components.list.slice() : undefined;
|
const list = Array.isArray(options?.components?.list) ? options.components.list.slice() : undefined;
|
||||||
let baseStrategy = 'EMPTY'; // EMPTY | ALL | ALL_PREON | SUBSET
|
let baseStrategy = 'EMPTY'; // EMPTY | ALL | ALL_PREON | SUBSET
|
||||||
@@ -1226,6 +1268,7 @@ class CallGenerateService {
|
|||||||
|
|
||||||
// 3) 干跑捕获(基座)
|
// 3) 干跑捕获(基座)
|
||||||
let captured = [];
|
let captured = [];
|
||||||
|
let enabledIds = []; // assembleOnly 时用于 identifier 标注
|
||||||
if (baseStrategy === 'EMPTY') {
|
if (baseStrategy === 'EMPTY') {
|
||||||
captured = [];
|
captured = [];
|
||||||
} else {
|
} else {
|
||||||
@@ -1242,6 +1285,7 @@ class CallGenerateService {
|
|||||||
allow = new Set(order.map(e => e.identifier));
|
allow = new Set(order.map(e => e.identifier));
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
|
enabledIds = Array.from(allow);
|
||||||
const run = async () => await this._capturePromptMessages({ includeConfig: null, quietText: '', skipWIAN: false });
|
const run = async () => await this._capturePromptMessages({ includeConfig: null, quietText: '', skipWIAN: false });
|
||||||
captured = await this._withPromptEnabledSet(allow, run);
|
captured = await this._withPromptEnabledSet(allow, run);
|
||||||
} else if (baseStrategy === 'ALL_PREON') {
|
} else if (baseStrategy === 'ALL_PREON') {
|
||||||
@@ -1255,6 +1299,7 @@ class CallGenerateService {
|
|||||||
allow = new Set(order.filter(e => !!e?.enabled).map(e => e.identifier));
|
allow = new Set(order.filter(e => !!e?.enabled).map(e => e.identifier));
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
|
enabledIds = Array.from(allow);
|
||||||
const run = async () => await this._capturePromptMessages({ includeConfig: null, quietText: '', skipWIAN: false });
|
const run = async () => await this._capturePromptMessages({ includeConfig: null, quietText: '', skipWIAN: false });
|
||||||
captured = await this._withPromptEnabledSet(allow, run);
|
captured = await this._withPromptEnabledSet(allow, run);
|
||||||
} else {
|
} else {
|
||||||
@@ -1263,7 +1308,11 @@ class CallGenerateService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 4) 依据策略计算启用集合与顺序
|
// 4) 依据策略计算启用集合与顺序
|
||||||
const annotateKeys = baseStrategy === 'SUBSET' ? orderedRefs : ((baseStrategy === 'ALL' || baseStrategy === 'ALL_PREON') ? orderedRefs : []);
|
let annotateKeys = baseStrategy === 'SUBSET' ? orderedRefs : ((baseStrategy === 'ALL' || baseStrategy === 'ALL_PREON') ? orderedRefs : []);
|
||||||
|
// assembleOnly 模式下,若无显式排序引用,则用全部启用组件做 identifier 标注
|
||||||
|
if (assembleOnly && annotateKeys.length === 0 && enabledIds.length > 0) {
|
||||||
|
annotateKeys = enabledIds;
|
||||||
|
}
|
||||||
let working = await this._annotateIdentifiersIfMissing(captured.slice(), annotateKeys);
|
let working = await this._annotateIdentifiersIfMissing(captured.slice(), annotateKeys);
|
||||||
working = this._applyOrderingStrategy(working, baseStrategy, orderedRefs, unorderedKeys);
|
working = this._applyOrderingStrategy(working, baseStrategy, orderedRefs, unorderedKeys);
|
||||||
|
|
||||||
@@ -1277,10 +1326,42 @@ class CallGenerateService {
|
|||||||
working = this._appendUserInput(working, options?.userInput);
|
working = this._appendUserInput(working, options?.userInput);
|
||||||
|
|
||||||
// 8) 调试导出
|
// 8) 调试导出
|
||||||
this._exportDebugData({ sourceWindow, requestId, working, baseStrategy, orderedRefs, inlineMapped, listLevelOverrides, debug: options?.debug, targetOrigin });
|
this._exportDebugData({ sourceWindow, requestId, working, baseStrategy, orderedRefs, inlineMapped, listLevelOverrides, debug: options?.debug, targetOrigin });
|
||||||
|
|
||||||
|
// assembleOnly 模式:只返回组装好的 messages,不调 LLM
|
||||||
|
if (assembleOnly) {
|
||||||
|
// 构建 identifier → name 映射(从 promptCollection 取,order 里没有 name)
|
||||||
|
const idToName = new Map();
|
||||||
|
try {
|
||||||
|
if (promptManager && typeof promptManager.getPromptCollection === 'function') {
|
||||||
|
const pc = promptManager.getPromptCollection();
|
||||||
|
const coll = pc?.collection || [];
|
||||||
|
for (const p of coll) {
|
||||||
|
if (p?.identifier) idToName.set(p.identifier, p.name || p.label || p.title || '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
const messages = working.map(m => {
|
||||||
|
const id = m.identifier || undefined;
|
||||||
|
const componentName = id ? (idToName.get(id) || undefined) : undefined;
|
||||||
|
return { role: m.role, content: m.content, identifier: id, name: componentName };
|
||||||
|
});
|
||||||
|
this.postToTarget(sourceWindow, 'assemblePromptResult', {
|
||||||
|
id: requestId,
|
||||||
|
messages: messages
|
||||||
|
}, targetOrigin);
|
||||||
|
return { messages };
|
||||||
|
}
|
||||||
|
|
||||||
// 9) 发送
|
// 9) 发送
|
||||||
return await this._sendMessages(working, { ...options, debug: { ...(options?.debug || {}), _exported: true } }, requestId, sourceWindow, targetOrigin);
|
return await this._sendMessages(working, { ...options, debug: { ...(options?.debug || {}), _exported: true } }, requestId, sourceWindow, targetOrigin);
|
||||||
|
|
||||||
|
}; // end executeCore
|
||||||
|
|
||||||
|
if (presetName) {
|
||||||
|
return await this._withTemporaryPreset(presetName, executeCore);
|
||||||
|
}
|
||||||
|
return await executeCore();
|
||||||
}
|
}
|
||||||
|
|
||||||
_applyOrderingStrategy(messages, baseStrategy, orderedRefs, unorderedKeys) {
|
_applyOrderingStrategy(messages, baseStrategy, orderedRefs, unorderedKeys) {
|
||||||
@@ -1340,9 +1421,9 @@ class CallGenerateService {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
_exportDebugData({ sourceWindow, requestId, working, baseStrategy, orderedRefs, inlineMapped, listLevelOverrides, debug, targetOrigin }) {
|
_exportDebugData({ sourceWindow, requestId, working, baseStrategy, orderedRefs, inlineMapped, listLevelOverrides, debug, targetOrigin }) {
|
||||||
const exportPrompt = !!(debug?.enabled || debug?.exportPrompt);
|
const exportPrompt = !!(debug?.enabled || debug?.exportPrompt);
|
||||||
if (exportPrompt) this.postToTarget(sourceWindow, 'generatePromptPreview', { id: requestId, messages: working.map(m => ({ role: m.role, content: m.content })) }, targetOrigin);
|
if (exportPrompt) this.postToTarget(sourceWindow, 'generatePromptPreview', { id: requestId, messages: working.map(m => ({ role: m.role, content: m.content })) }, targetOrigin);
|
||||||
if (debug?.exportBlueprint) {
|
if (debug?.exportBlueprint) {
|
||||||
try {
|
try {
|
||||||
const bp = {
|
const bp = {
|
||||||
@@ -1351,7 +1432,7 @@ class CallGenerateService {
|
|||||||
injections: (debug?.injections || []).concat(inlineMapped || []),
|
injections: (debug?.injections || []).concat(inlineMapped || []),
|
||||||
overrides: listLevelOverrides || null,
|
overrides: listLevelOverrides || null,
|
||||||
};
|
};
|
||||||
this.postToTarget(sourceWindow, 'blueprint', bp, targetOrigin);
|
this.postToTarget(sourceWindow, 'blueprint', bp, targetOrigin);
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1359,25 +1440,25 @@ class CallGenerateService {
|
|||||||
/**
|
/**
|
||||||
* 入口:处理 generateRequest(统一入口)
|
* 入口:处理 generateRequest(统一入口)
|
||||||
*/
|
*/
|
||||||
async handleGenerateRequest(options, requestId, sourceWindow, targetOrigin = null) {
|
async handleGenerateRequest(options, requestId, sourceWindow, targetOrigin = null) {
|
||||||
let streamingEnabled = false;
|
let streamingEnabled = false;
|
||||||
try {
|
try {
|
||||||
streamingEnabled = options?.streaming?.enabled !== false;
|
streamingEnabled = options?.streaming?.enabled !== false;
|
||||||
try {
|
try {
|
||||||
if (xbLog.isEnabled?.()) {
|
if (xbLog.isEnabled?.()) {
|
||||||
const comps = options?.components?.list;
|
const comps = options?.components?.list;
|
||||||
const compsCount = Array.isArray(comps) ? comps.length : 0;
|
const compsCount = Array.isArray(comps) ? comps.length : 0;
|
||||||
const userInputLen = String(options?.userInput || '').length;
|
const userInputLen = String(options?.userInput || '').length;
|
||||||
xbLog.info('callGenerateBridge', `generateRequest id=${requestId} stream=${!!streamingEnabled} comps=${compsCount} userInputLen=${userInputLen}`);
|
xbLog.info('callGenerateBridge', `generateRequest id=${requestId} stream=${!!streamingEnabled} comps=${compsCount} userInputLen=${userInputLen}`);
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
return await this.handleRequestInternal(options, requestId, sourceWindow, targetOrigin);
|
return await this.handleRequestInternal(options, requestId, sourceWindow, targetOrigin);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
try { xbLog.error('callGenerateBridge', `generateRequest failed id=${requestId}`, err); } catch {}
|
try { xbLog.error('callGenerateBridge', `generateRequest failed id=${requestId}`, err); } catch {}
|
||||||
this.sendError(sourceWindow, requestId, streamingEnabled, err, 'BAD_REQUEST', null, targetOrigin);
|
this.sendError(sourceWindow, requestId, streamingEnabled, err, 'BAD_REQUEST', null, targetOrigin);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 取消会话 */
|
/** 取消会话 */
|
||||||
cancel(sessionId) {
|
cancel(sessionId) {
|
||||||
@@ -1394,43 +1475,76 @@ class CallGenerateService {
|
|||||||
|
|
||||||
const callGenerateService = new CallGenerateService();
|
const callGenerateService = new CallGenerateService();
|
||||||
|
|
||||||
export async function handleGenerateRequest(options, requestId, sourceWindow, targetOrigin = null) {
|
export async function handleGenerateRequest(options, requestId, sourceWindow, targetOrigin = null) {
|
||||||
return await callGenerateService.handleGenerateRequest(options, requestId, sourceWindow, targetOrigin);
|
return await callGenerateService.handleGenerateRequest(options, requestId, sourceWindow, targetOrigin);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Host bridge for handling iframe generateRequest → respond via postMessage
|
// Host bridge for handling iframe generateRequest → respond via postMessage
|
||||||
let __xb_generate_listener_attached = false;
|
let __xb_generate_listener_attached = false;
|
||||||
let __xb_generate_listener = null;
|
let __xb_generate_listener = null;
|
||||||
|
|
||||||
export function initCallGenerateHostBridge() {
|
export function initCallGenerateHostBridge() {
|
||||||
if (typeof window === 'undefined') return;
|
if (typeof window === 'undefined') return;
|
||||||
if (__xb_generate_listener_attached) return;
|
if (__xb_generate_listener_attached) return;
|
||||||
try { xbLog.info('callGenerateBridge', 'initCallGenerateHostBridge'); } catch {}
|
try { xbLog.info('callGenerateBridge', 'initCallGenerateHostBridge'); } catch {}
|
||||||
__xb_generate_listener = async function (event) {
|
__xb_generate_listener = async function (event) {
|
||||||
try {
|
try {
|
||||||
const data = event && event.data || {};
|
const data = event && event.data || {};
|
||||||
if (!data || data.type !== 'generateRequest') return;
|
if (!data) return;
|
||||||
const id = data.id;
|
|
||||||
const options = data.options || {};
|
|
||||||
await handleGenerateRequest(options, id, event.source || window, event.origin);
|
|
||||||
} catch (e) {
|
|
||||||
try { xbLog.error('callGenerateBridge', 'generateRequest listener error', e); } catch {}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
// eslint-disable-next-line no-restricted-syntax -- bridge listener; origin can be null for sandboxed iframes.
|
|
||||||
try { window.addEventListener('message', __xb_generate_listener); } catch (e) {}
|
|
||||||
__xb_generate_listener_attached = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function cleanupCallGenerateHostBridge() {
|
if (data.type === 'generateRequest') {
|
||||||
if (typeof window === 'undefined') return;
|
const id = data.id;
|
||||||
if (!__xb_generate_listener_attached) return;
|
const options = data.options || {};
|
||||||
try { xbLog.info('callGenerateBridge', 'cleanupCallGenerateHostBridge'); } catch {}
|
await handleGenerateRequest(options, id, event.source || window, event.origin);
|
||||||
try { window.removeEventListener('message', __xb_generate_listener); } catch (e) {}
|
return;
|
||||||
__xb_generate_listener_attached = false;
|
}
|
||||||
__xb_generate_listener = null;
|
|
||||||
try { callGenerateService.cleanup(); } catch (e) {}
|
if (data.type === 'listPresetsRequest') {
|
||||||
}
|
const id = data.id;
|
||||||
|
const names = Object.keys(openai_setting_names || {});
|
||||||
|
const selected = oai_settings?.preset_settings_openai || '';
|
||||||
|
callGenerateService.postToTarget(
|
||||||
|
event.source || window,
|
||||||
|
'listPresetsResult',
|
||||||
|
{ id, presets: names, selected },
|
||||||
|
event.origin
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.type === 'assemblePromptRequest') {
|
||||||
|
const id = data.id;
|
||||||
|
const options = data.options || {};
|
||||||
|
try {
|
||||||
|
await callGenerateService.handleRequestInternal(
|
||||||
|
options, id, event.source || window, event.origin,
|
||||||
|
{ assembleOnly: true }
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
callGenerateService.sendError(
|
||||||
|
event.source || window, id, false, err, 'ASSEMBLE_ERROR', null, event.origin
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
try { xbLog.error('callGenerateBridge', 'listener error', e); } catch {}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// eslint-disable-next-line no-restricted-syntax -- bridge listener; origin can be null for sandboxed iframes.
|
||||||
|
try { window.addEventListener('message', __xb_generate_listener); } catch (e) {}
|
||||||
|
__xb_generate_listener_attached = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function cleanupCallGenerateHostBridge() {
|
||||||
|
if (typeof window === 'undefined') return;
|
||||||
|
if (!__xb_generate_listener_attached) return;
|
||||||
|
try { xbLog.info('callGenerateBridge', 'cleanupCallGenerateHostBridge'); } catch {}
|
||||||
|
try { window.removeEventListener('message', __xb_generate_listener); } catch (e) {}
|
||||||
|
__xb_generate_listener_attached = false;
|
||||||
|
__xb_generate_listener = null;
|
||||||
|
try { callGenerateService.cleanup(); } catch (e) {}
|
||||||
|
}
|
||||||
|
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
Object.assign(window, { xiaobaixCallGenerateService: callGenerateService, initCallGenerateHostBridge, cleanupCallGenerateHostBridge });
|
Object.assign(window, { xiaobaixCallGenerateService: callGenerateService, initCallGenerateHostBridge, cleanupCallGenerateHostBridge });
|
||||||
@@ -1514,8 +1628,8 @@ if (typeof window !== 'undefined') {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// eslint-disable-next-line no-restricted-syntax -- local listener for internal request flow.
|
// eslint-disable-next-line no-restricted-syntax -- local listener for internal request flow.
|
||||||
window.addEventListener('message', listener);
|
window.addEventListener('message', listener);
|
||||||
|
|
||||||
// 发送请求
|
// 发送请求
|
||||||
handleGenerateRequest(options, requestId, window).catch(err => {
|
handleGenerateRequest(options, requestId, window).catch(err => {
|
||||||
@@ -1525,6 +1639,49 @@ if (typeof window !== 'undefined') {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 全局 assemblePrompt 函数
|
||||||
|
* 只组装提示词,不调用 LLM,返回组装好的 messages 数组
|
||||||
|
*
|
||||||
|
* @param {Object} options - 与 callGenerate 相同的选项格式(api/streaming 字段会被忽略)
|
||||||
|
* @returns {Promise<Array<{role: string, content: string}>>} 组装后的 messages 数组
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const messages = await window.LittleWhiteBox.assemblePrompt({
|
||||||
|
* components: { list: ['ALL_PREON'] },
|
||||||
|
* userInput: '可选的用户输入'
|
||||||
|
* });
|
||||||
|
* // messages = [{ role: 'system', content: '...' }, ...]
|
||||||
|
*/
|
||||||
|
window.LittleWhiteBox.assemblePrompt = async function(options) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const requestId = `assemble-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
||||||
|
|
||||||
|
const listener = (event) => {
|
||||||
|
const data = event.data;
|
||||||
|
if (!data || data.source !== SOURCE_TAG || data.id !== requestId) return;
|
||||||
|
|
||||||
|
if (data.type === 'assemblePromptResult') {
|
||||||
|
window.removeEventListener('message', listener);
|
||||||
|
resolve(data.messages);
|
||||||
|
} else if (data.type === 'generateError' || data.type === 'generateStreamError') {
|
||||||
|
window.removeEventListener('message', listener);
|
||||||
|
reject(data.error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-syntax -- local listener for internal request flow.
|
||||||
|
window.addEventListener('message', listener);
|
||||||
|
|
||||||
|
callGenerateService.handleRequestInternal(
|
||||||
|
options, requestId, window, null, { assembleOnly: true }
|
||||||
|
).catch(err => {
|
||||||
|
window.removeEventListener('message', listener);
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 取消指定会话
|
* 取消指定会话
|
||||||
* @param {string} sessionId - 会话 ID(如 'xb1', 'xb2' 等)
|
* @param {string} sessionId - 会话 ID(如 'xb1', 'xb2' 等)
|
||||||
@@ -1540,6 +1697,14 @@ if (typeof window !== 'undefined') {
|
|||||||
callGenerateService.cleanup();
|
callGenerateService.cleanup();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
window.LittleWhiteBox.listChatCompletionPresets = function() {
|
||||||
|
return Object.keys(openai_setting_names || {});
|
||||||
|
};
|
||||||
|
|
||||||
|
window.LittleWhiteBox.getSelectedPresetName = function() {
|
||||||
|
return oai_settings?.preset_settings_openai || '';
|
||||||
|
};
|
||||||
|
|
||||||
// 保持向后兼容:保留原有的内部接口
|
// 保持向后兼容:保留原有的内部接口
|
||||||
window.LittleWhiteBox._internal = {
|
window.LittleWhiteBox._internal = {
|
||||||
service: callGenerateService,
|
service: callGenerateService,
|
||||||
@@ -1547,4 +1712,4 @@ if (typeof window !== 'undefined') {
|
|||||||
init: initCallGenerateHostBridge,
|
init: initCallGenerateHostBridge,
|
||||||
cleanup: cleanupCallGenerateHostBridge
|
cleanup: cleanupCallGenerateHostBridge
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
293
bridges/context-bridge.js
Normal file
293
bridges/context-bridge.js
Normal file
@@ -0,0 +1,293 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import { event_types, user_avatar, getCurrentChatId } from "../../../../../script.js";
|
||||||
|
import { getContext } from "../../../../st-context.js";
|
||||||
|
import { power_user } from "../../../../power-user.js";
|
||||||
|
import { createModuleEvents } from "../core/event-manager.js";
|
||||||
|
import { xbLog } from "../core/debug-core.js";
|
||||||
|
|
||||||
|
const SOURCE_TAG = 'xiaobaix-host';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Context Bridge — 模板 iframe 上下文桥接服务
|
||||||
|
*
|
||||||
|
* 功能:
|
||||||
|
* 1. iframe 发送 iframe-ready / request-context → 插件推送上下文快照
|
||||||
|
* 2. 酒馆事件实时转发到所有模板 iframe
|
||||||
|
* 3. 延迟投递队列:iframe 销毁后的事件暂存,待下一个 iframe 连接时投递
|
||||||
|
*/
|
||||||
|
class ContextBridgeService {
|
||||||
|
constructor() {
|
||||||
|
this._attached = false;
|
||||||
|
this._listener = null;
|
||||||
|
this._previousChatId = null;
|
||||||
|
/** @type {Array<{type: string, event: string, payload: object}>} */
|
||||||
|
this._pendingEvents = [];
|
||||||
|
this._events = createModuleEvents('contextBridge');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 生命周期 =====
|
||||||
|
|
||||||
|
init() {
|
||||||
|
if (this._attached) return;
|
||||||
|
try { xbLog.info('contextBridge', 'init'); } catch {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this._previousChatId = getCurrentChatId();
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
const self = this;
|
||||||
|
this._listener = function (event) {
|
||||||
|
try {
|
||||||
|
self._handleMessage(event);
|
||||||
|
} catch (e) {
|
||||||
|
try { xbLog.error('contextBridge', 'message handler error', e); } catch {}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-syntax -- bridge listener for iframe-ready/request-context
|
||||||
|
window.addEventListener('message', this._listener);
|
||||||
|
this._attachEventForwarding();
|
||||||
|
this._attached = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
if (!this._attached) return;
|
||||||
|
try { xbLog.info('contextBridge', 'cleanup'); } catch {}
|
||||||
|
try { window.removeEventListener('message', this._listener); } catch {}
|
||||||
|
this._listener = null;
|
||||||
|
this._events.cleanup();
|
||||||
|
this._pendingEvents.length = 0;
|
||||||
|
this._previousChatId = null;
|
||||||
|
this._attached = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 消息处理 =====
|
||||||
|
|
||||||
|
_handleMessage(event) {
|
||||||
|
const data = event && event.data;
|
||||||
|
if (!data || typeof data !== 'object') return;
|
||||||
|
const type = data.type;
|
||||||
|
if (type !== 'iframe-ready' && type !== 'request-context') return;
|
||||||
|
|
||||||
|
// 找到发送消息的 iframe 元素
|
||||||
|
const iframe = this._findIframeBySource(event.source);
|
||||||
|
if (!iframe) return;
|
||||||
|
|
||||||
|
const msgIndex = this._getMsgIndexForIframe(iframe);
|
||||||
|
if (msgIndex < 0) return;
|
||||||
|
|
||||||
|
// iframe-ready 时先投递积压的延迟事件
|
||||||
|
if (type === 'iframe-ready') {
|
||||||
|
while (this._pendingEvents.length > 0) {
|
||||||
|
const pending = this._pendingEvents.shift();
|
||||||
|
// eslint-disable-next-line no-restricted-syntax -- delivering queued events to newly ready iframe
|
||||||
|
try { event.source?.postMessage(pending, '*'); } catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 推送上下文快照
|
||||||
|
const snapshot = this._buildContextSnapshot(msgIndex);
|
||||||
|
// eslint-disable-next-line no-restricted-syntax -- sending context snapshot to requesting iframe
|
||||||
|
try { event.source?.postMessage(snapshot, '*'); } catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 遍历 DOM 查找 contentWindow 匹配的 iframe
|
||||||
|
* @param {Window} source
|
||||||
|
* @returns {HTMLIFrameElement|null}
|
||||||
|
*/
|
||||||
|
_findIframeBySource(source) {
|
||||||
|
if (!source) return null;
|
||||||
|
const iframes = document.querySelectorAll('iframe.xiaobaix-iframe');
|
||||||
|
for (const iframe of iframes) {
|
||||||
|
try {
|
||||||
|
if (iframe.contentWindow === source) return iframe;
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从 iframe 的 DOM 位置获取消息楼层索引
|
||||||
|
* @param {HTMLIFrameElement} iframe
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
_getMsgIndexForIframe(iframe) {
|
||||||
|
const mesBlock = iframe.closest('.mes');
|
||||||
|
if (!mesBlock) return -1;
|
||||||
|
const mesid = mesBlock.getAttribute('mesid');
|
||||||
|
if (mesid == null) return -1;
|
||||||
|
return parseInt(mesid, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 上下文快照 =====
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} msgIndex
|
||||||
|
* @returns {object}
|
||||||
|
*/
|
||||||
|
_buildContextSnapshot(msgIndex) {
|
||||||
|
const ctx = getContext();
|
||||||
|
const chat = ctx.chat || [];
|
||||||
|
const msg = chat[msgIndex];
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'st-context',
|
||||||
|
chatId: getCurrentChatId() || null,
|
||||||
|
characterId: ctx.characterId ?? null,
|
||||||
|
characterName: ctx.name2 || '',
|
||||||
|
userName: ctx.name1 || '',
|
||||||
|
userPersona: power_user?.persona_description || '',
|
||||||
|
userAvatar: user_avatar || '',
|
||||||
|
msgIndex: msgIndex,
|
||||||
|
swipeId: msg?.swipe_id ?? 0,
|
||||||
|
totalSwipes: msg?.swipes?.length ?? 1,
|
||||||
|
totalMessages: chat.length,
|
||||||
|
isGroupChat: !!ctx.groupId,
|
||||||
|
groupId: ctx.groupId ?? null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 事件广播 =====
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 向所有活跃的模板 iframe 广播事件
|
||||||
|
* @param {string} eventName
|
||||||
|
* @param {object} payload
|
||||||
|
*/
|
||||||
|
_broadcastToTemplateIframes(eventName, payload) {
|
||||||
|
const iframes = document.querySelectorAll('.mes iframe.xiaobaix-iframe');
|
||||||
|
const message = { type: 'st-event', source: SOURCE_TAG, event: eventName, payload };
|
||||||
|
for (const iframe of iframes) {
|
||||||
|
// eslint-disable-next-line no-restricted-syntax -- broadcasting event to template iframes
|
||||||
|
try { iframe.contentWindow?.postMessage(message, '*'); } catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 事件转发注册 =====
|
||||||
|
|
||||||
|
_attachEventForwarding() {
|
||||||
|
const self = this;
|
||||||
|
|
||||||
|
// ---- 消息级事件 ----
|
||||||
|
|
||||||
|
// 消息删除(截断式):原生 payload = chat.length(删除后剩余消息数)
|
||||||
|
this._events.on(event_types.MESSAGE_DELETED, (remainingCount) => {
|
||||||
|
self._broadcastToTemplateIframes('message_deleted', {
|
||||||
|
fromIndex: remainingCount,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Swipe 切换:原生 payload = chat.length - 1(最后一条消息索引)
|
||||||
|
this._events.on(event_types.MESSAGE_SWIPED, (msgIndex) => {
|
||||||
|
const ctx = getContext();
|
||||||
|
const msg = ctx.chat?.[msgIndex];
|
||||||
|
self._broadcastToTemplateIframes('message_swiped', {
|
||||||
|
msgIndex: msgIndex,
|
||||||
|
newSwipeId: msg?.swipe_id ?? 0,
|
||||||
|
totalSwipes: msg?.swipes?.length ?? 1,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 消息发送:原生 payload = insertAt(消息索引)
|
||||||
|
this._events.on(event_types.MESSAGE_SENT, (msgIndex) => {
|
||||||
|
self._broadcastToTemplateIframes('message_sent', { msgIndex });
|
||||||
|
});
|
||||||
|
|
||||||
|
// AI 回复完成:原生 payload = chat_id(消息索引)
|
||||||
|
this._events.on(event_types.MESSAGE_RECEIVED, (msgIndex) => {
|
||||||
|
self._broadcastToTemplateIframes('message_received', { msgIndex });
|
||||||
|
});
|
||||||
|
|
||||||
|
// 消息编辑:原生 payload = this_edit_mes_id(消息索引)
|
||||||
|
this._events.on(event_types.MESSAGE_EDITED, (msgIndex) => {
|
||||||
|
self._broadcastToTemplateIframes('message_edited', { msgIndex });
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---- 聊天级事件 ----
|
||||||
|
|
||||||
|
// 聊天切换:原生 payload = getCurrentChatId()
|
||||||
|
this._events.on(event_types.CHAT_CHANGED, (newChatId) => {
|
||||||
|
self._broadcastToTemplateIframes('chat_id_changed', {
|
||||||
|
newChatId: newChatId,
|
||||||
|
previousChatId: self._previousChatId,
|
||||||
|
});
|
||||||
|
self._previousChatId = newChatId;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 新聊天创建(含分支检测):原生 payload = 无
|
||||||
|
this._events.on(event_types.CHAT_CREATED, () => {
|
||||||
|
const ctx = getContext();
|
||||||
|
const newLength = (ctx.chat || []).length;
|
||||||
|
const isBranch = newLength > 1;
|
||||||
|
|
||||||
|
self._broadcastToTemplateIframes('chat_created', {
|
||||||
|
chatId: getCurrentChatId() || null,
|
||||||
|
isBranch: isBranch,
|
||||||
|
branchFromChatId: isBranch ? self._previousChatId : null,
|
||||||
|
branchPointIndex: isBranch ? newLength - 1 : null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---- 延迟投递事件(入队,不广播)----
|
||||||
|
|
||||||
|
// 聊天删除:原生 payload = 聊天文件名(不含 .jsonl)
|
||||||
|
this._events.on(event_types.CHAT_DELETED, (chatFileName) => {
|
||||||
|
self._pendingEvents.push({
|
||||||
|
type: 'st-event',
|
||||||
|
source: SOURCE_TAG,
|
||||||
|
event: 'chat_deleted',
|
||||||
|
payload: { chatId: chatFileName, timestamp: Date.now() },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 群聊删除
|
||||||
|
this._events.on(event_types.GROUP_CHAT_DELETED, (chatFileName) => {
|
||||||
|
self._pendingEvents.push({
|
||||||
|
type: 'st-event',
|
||||||
|
source: SOURCE_TAG,
|
||||||
|
event: 'group_chat_deleted',
|
||||||
|
payload: { chatId: chatFileName, timestamp: Date.now() },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 模块级实例与导出 =====
|
||||||
|
|
||||||
|
const contextBridgeService = new ContextBridgeService();
|
||||||
|
|
||||||
|
export function initContextBridge() {
|
||||||
|
contextBridgeService.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function cleanupContextBridge() {
|
||||||
|
contextBridgeService.cleanup();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 自初始化(与 call-generate-service.js 模式一致)=====
|
||||||
|
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
window.LittleWhiteBox = window.LittleWhiteBox || {};
|
||||||
|
window.LittleWhiteBox.contextBridge = contextBridgeService;
|
||||||
|
|
||||||
|
try { initContextBridge(); } catch (e) {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
window.addEventListener('xiaobaixEnabledChanged', (e) => {
|
||||||
|
try {
|
||||||
|
const enabled = e && e.detail && e.detail.enabled === true;
|
||||||
|
if (enabled) initContextBridge(); else cleanupContextBridge();
|
||||||
|
} catch {}
|
||||||
|
});
|
||||||
|
document.addEventListener('xiaobaixEnabledChanged', (e) => {
|
||||||
|
try {
|
||||||
|
const enabled = e && e.detail && e.detail.enabled === true;
|
||||||
|
if (enabled) initContextBridge(); else cleanupContextBridge();
|
||||||
|
} catch {}
|
||||||
|
});
|
||||||
|
window.addEventListener('beforeunload', () => {
|
||||||
|
try { cleanupContextBridge(); } catch {}
|
||||||
|
});
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
5
index.js
5
index.js
@@ -625,6 +625,11 @@ jQuery(async () => {
|
|||||||
document.head.appendChild(Object.assign(document.createElement('script'), { id: 'xb-worldbook', type: 'module', src: `${extensionFolderPath}/bridges/worldbook-bridge.js` }));
|
document.head.appendChild(Object.assign(document.createElement('script'), { id: 'xb-worldbook', type: 'module', src: `${extensionFolderPath}/bridges/worldbook-bridge.js` }));
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (isXiaobaixEnabled && !document.getElementById('xb-contextbridge'))
|
||||||
|
document.head.appendChild(Object.assign(document.createElement('script'), { id: 'xb-contextbridge', type: 'module', src: `${extensionFolderPath}/bridges/context-bridge.js` }));
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
eventSource.on(event_types.APP_READY, () => {
|
eventSource.on(event_types.APP_READY, () => {
|
||||||
setTimeout(performExtensionUpdateCheck, 2000);
|
setTimeout(performExtensionUpdateCheck, 2000);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -19,6 +19,12 @@ export function getSettings() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getSummaryPanelConfig() {
|
export function getSummaryPanelConfig() {
|
||||||
|
const clampKeepVisibleCount = (value) => {
|
||||||
|
const n = Number.parseInt(value, 10);
|
||||||
|
if (!Number.isFinite(n)) return 6;
|
||||||
|
return Math.max(0, Math.min(50, n));
|
||||||
|
};
|
||||||
|
|
||||||
const defaults = {
|
const defaults = {
|
||||||
api: { provider: "st", url: "", key: "", model: "", modelCache: [] },
|
api: { provider: "st", url: "", key: "", model: "", modelCache: [] },
|
||||||
gen: { temperature: null, top_p: null, top_k: null, presence_penalty: null, frequency_penalty: null },
|
gen: { temperature: null, top_p: null, top_k: null, presence_penalty: null, frequency_penalty: null },
|
||||||
@@ -33,6 +39,10 @@ export function getSummaryPanelConfig() {
|
|||||||
wrapperTail: "",
|
wrapperTail: "",
|
||||||
forceInsertAtEnd: false,
|
forceInsertAtEnd: false,
|
||||||
},
|
},
|
||||||
|
ui: {
|
||||||
|
hideSummarized: true,
|
||||||
|
keepVisibleCount: 6,
|
||||||
|
},
|
||||||
textFilterRules: [...DEFAULT_FILTER_RULES],
|
textFilterRules: [...DEFAULT_FILTER_RULES],
|
||||||
vector: null,
|
vector: null,
|
||||||
};
|
};
|
||||||
@@ -52,12 +62,15 @@ export function getSummaryPanelConfig() {
|
|||||||
api: { ...defaults.api, ...(parsed.api || {}) },
|
api: { ...defaults.api, ...(parsed.api || {}) },
|
||||||
gen: { ...defaults.gen, ...(parsed.gen || {}) },
|
gen: { ...defaults.gen, ...(parsed.gen || {}) },
|
||||||
trigger: { ...defaults.trigger, ...(parsed.trigger || {}) },
|
trigger: { ...defaults.trigger, ...(parsed.trigger || {}) },
|
||||||
|
ui: { ...defaults.ui, ...(parsed.ui || {}) },
|
||||||
textFilterRules,
|
textFilterRules,
|
||||||
vector: parsed.vector || null,
|
vector: parsed.vector || null,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (result.trigger.timing === "manual") result.trigger.enabled = false;
|
if (result.trigger.timing === "manual") result.trigger.enabled = false;
|
||||||
if (result.trigger.useStream === undefined) result.trigger.useStream = true;
|
if (result.trigger.useStream === undefined) result.trigger.useStream = true;
|
||||||
|
result.ui.hideSummarized = !!result.ui.hideSummarized;
|
||||||
|
result.ui.keepVisibleCount = clampKeepVisibleCount(result.ui.keepVisibleCount);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@@ -48,13 +48,15 @@ export function saveSummaryStore() {
|
|||||||
|
|
||||||
export function getKeepVisibleCount() {
|
export function getKeepVisibleCount() {
|
||||||
const store = getSummaryStore();
|
const store = getSummaryStore();
|
||||||
return store?.keepVisibleCount ?? 3;
|
return store?.keepVisibleCount ?? 6;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function calcHideRange(boundary) {
|
export function calcHideRange(boundary, keepCountOverride = null) {
|
||||||
if (boundary == null || boundary < 0) return null;
|
if (boundary == null || boundary < 0) return null;
|
||||||
|
|
||||||
const keepCount = getKeepVisibleCount();
|
const keepCount = Number.isFinite(keepCountOverride)
|
||||||
|
? Math.max(0, Math.min(50, Number(keepCountOverride)))
|
||||||
|
: getKeepVisibleCount();
|
||||||
const hideEnd = boundary - keepCount;
|
const hideEnd = boundary - keepCount;
|
||||||
if (hideEnd < 0) return null;
|
if (hideEnd < 0) return null;
|
||||||
return { start: 0, end: hideEnd };
|
return { start: 0, end: hideEnd };
|
||||||
|
|||||||
@@ -210,11 +210,13 @@ Before generating, observe the USER and analyze carefully:
|
|||||||
## CRITICAL NOTES
|
## CRITICAL NOTES
|
||||||
- events.id 从 evt-{nextEventId} 开始编号
|
- events.id 从 evt-{nextEventId} 开始编号
|
||||||
- 仅输出【增量】内容,已有事件绝不重复
|
- 仅输出【增量】内容,已有事件绝不重复
|
||||||
|
- /地点、通过什么方式、对谁、做了什么事、结果如何。如果原文有具体道具(如一把枪、一封信),必须在总结中提及。
|
||||||
- keywords 是全局关键词,综合已有+新增
|
- keywords 是全局关键词,综合已有+新增
|
||||||
- causedBy 仅在因果明确时填写,允许为[],0-2个
|
- causedBy 仅在因果明确时填写,允许为[],0-2个
|
||||||
- factUpdates 可为空数组
|
- factUpdates 可为空数组
|
||||||
- 合法JSON,字符串值内部避免英文双引号
|
- 合法JSON,字符串值内部避免英文双引号
|
||||||
- 用朴实、白描、有烟火气的笔触记录,避免比喻和意象
|
- 用朴实、白描、有烟火气的笔触记录事实,避免比喻和意象
|
||||||
|
- 严谨、注重细节,避免使用模糊的概括性语言,应用具体的动词描述动作,例:谁,在什么时间/地点,通过什么方式,对谁,做了什么事,出现了什么道具,结果如何。
|
||||||
</meta_protocol>`,
|
</meta_protocol>`,
|
||||||
|
|
||||||
assistantCheck: `Content review initiated...
|
assistantCheck: `Content review initiated...
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import { getSummaryStore, getFacts, isRelationFact } from "../data/store.js";
|
|||||||
import { getVectorConfig, getSummaryPanelConfig, getSettings } from "../data/config.js";
|
import { getVectorConfig, getSummaryPanelConfig, getSettings } from "../data/config.js";
|
||||||
import { recallMemory } from "../vector/retrieval/recall.js";
|
import { recallMemory } from "../vector/retrieval/recall.js";
|
||||||
import { getMeta } from "../vector/storage/chunk-store.js";
|
import { getMeta } from "../vector/storage/chunk-store.js";
|
||||||
|
import { getStateAtoms } from "../vector/storage/state-store.js";
|
||||||
import { getEngineFingerprint } from "../vector/utils/embedder.js";
|
import { getEngineFingerprint } from "../vector/utils/embedder.js";
|
||||||
import { buildTrustedCharacters } from "../vector/retrieval/entity-lexicon.js";
|
import { buildTrustedCharacters } from "../vector/retrieval/entity-lexicon.js";
|
||||||
|
|
||||||
@@ -540,6 +541,34 @@ function groupL0ByFloor(l0List) {
|
|||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all available L0 atoms in recent window and normalize to evidence shape.
|
||||||
|
* @param {number} recentStart
|
||||||
|
* @param {number} recentEnd
|
||||||
|
* @returns {object[]}
|
||||||
|
*/
|
||||||
|
function getRecentWindowL0Atoms(recentStart, recentEnd) {
|
||||||
|
if (!Number.isFinite(recentStart) || !Number.isFinite(recentEnd) || recentEnd < recentStart) return [];
|
||||||
|
const atoms = getStateAtoms() || [];
|
||||||
|
const out = [];
|
||||||
|
for (const atom of atoms) {
|
||||||
|
const floor = atom?.floor;
|
||||||
|
const atomId = atom?.atomId;
|
||||||
|
const semantic = String(atom?.semantic || '').trim();
|
||||||
|
if (!Number.isFinite(floor)) continue;
|
||||||
|
if (floor < recentStart || floor > recentEnd) continue;
|
||||||
|
if (!atomId || !semantic) continue;
|
||||||
|
out.push({
|
||||||
|
id: atomId,
|
||||||
|
floor,
|
||||||
|
atom,
|
||||||
|
similarity: 0,
|
||||||
|
rerankScore: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
// EvidenceGroup(per-floor:N个L0 + 共享一对L1)
|
// EvidenceGroup(per-floor:N个L0 + 共享一对L1)
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
@@ -585,6 +614,21 @@ function buildEvidenceGroup(floor, l0AtomsForFloor, l1ByFloor) {
|
|||||||
return { floor, l0Atoms: l0AtomsForFloor, userL1, aiL1, totalTokens };
|
return { floor, l0Atoms: l0AtomsForFloor, userL1, aiL1, totalTokens };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build recent-evidence group (L0 only, no L1 attachment).
|
||||||
|
* @param {number} floor
|
||||||
|
* @param {object[]} l0AtomsForFloor
|
||||||
|
* @returns {object}
|
||||||
|
*/
|
||||||
|
function buildRecentEvidenceGroup(floor, l0AtomsForFloor) {
|
||||||
|
let totalTokens = 0;
|
||||||
|
for (const l0 of l0AtomsForFloor) {
|
||||||
|
totalTokens += estimateTokens(buildL0DisplayText(l0));
|
||||||
|
}
|
||||||
|
totalTokens += 10;
|
||||||
|
return { floor, l0Atoms: l0AtomsForFloor, userL1: null, aiL1: null, totalTokens };
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 格式化一个证据组为文本行数组
|
* 格式化一个证据组为文本行数组
|
||||||
*
|
*
|
||||||
@@ -1114,7 +1158,11 @@ async function buildVectorPrompt(store, recallResult, causalById, focusCharacter
|
|||||||
|
|
||||||
const lastSummarized = store.lastSummarizedMesId ?? -1;
|
const lastSummarized = store.lastSummarizedMesId ?? -1;
|
||||||
const lastChunkFloor = meta?.lastChunkFloor ?? -1;
|
const lastChunkFloor = meta?.lastChunkFloor ?? -1;
|
||||||
const keepVisible = store.keepVisibleCount ?? 3;
|
const uiCfg = getSummaryPanelConfig()?.ui || {};
|
||||||
|
const parsedKeepVisible = Number.parseInt(uiCfg.keepVisibleCount, 10);
|
||||||
|
const keepVisible = Number.isFinite(parsedKeepVisible)
|
||||||
|
? Math.max(0, Math.min(50, parsedKeepVisible))
|
||||||
|
: 6;
|
||||||
|
|
||||||
// 收集未被事件消费的 L0,按 rerankScore 降序
|
// 收集未被事件消费的 L0,按 rerankScore 降序
|
||||||
const focusSetForEvidence = new Set((focusCharacters || []).map(normalize).filter(Boolean));
|
const focusSetForEvidence = new Set((focusCharacters || []).map(normalize).filter(Boolean));
|
||||||
@@ -1171,22 +1219,22 @@ async function buildVectorPrompt(store, recallResult, causalById, focusCharacter
|
|||||||
const recentEnd = lastChunkFloor - keepVisible;
|
const recentEnd = lastChunkFloor - keepVisible;
|
||||||
|
|
||||||
if (recentEnd >= recentStart) {
|
if (recentEnd >= recentStart) {
|
||||||
const recentL0 = remainingL0
|
const recentAllL0 = getRecentWindowL0Atoms(recentStart, recentEnd);
|
||||||
|
const recentL0 = recentAllL0
|
||||||
.filter(l0 => !usedL0Ids.has(l0.id))
|
.filter(l0 => !usedL0Ids.has(l0.id))
|
||||||
.filter(l0 => l0.floor >= recentStart && l0.floor <= recentEnd);
|
.filter(l0 => l0.floor >= recentStart && l0.floor <= recentEnd);
|
||||||
|
|
||||||
if (recentL0.length) {
|
if (recentL0.length) {
|
||||||
const recentBudget = { used: 0, max: UNSUMMARIZED_EVIDENCE_MAX };
|
const recentBudget = { used: 0, max: UNSUMMARIZED_EVIDENCE_MAX };
|
||||||
|
|
||||||
// 先按分数挑组(高分优先),再按时间输出(楼层升序)
|
// Pick newest floors first, then output in chronological order.
|
||||||
const recentFloorMap = groupL0ByFloor(recentL0);
|
const recentFloorMap = groupL0ByFloor(recentL0);
|
||||||
const recentRanked = [];
|
const recentRanked = [];
|
||||||
for (const [floor, l0s] of recentFloorMap) {
|
for (const [floor, l0s] of recentFloorMap) {
|
||||||
const group = buildEvidenceGroup(floor, l0s, l1ByFloor);
|
const group = buildRecentEvidenceGroup(floor, l0s);
|
||||||
const bestScore = Math.max(...l0s.map(l0 => (l0.rerankScore ?? l0.similarity ?? 0)));
|
recentRanked.push({ group });
|
||||||
recentRanked.push({ group, bestScore });
|
|
||||||
}
|
}
|
||||||
recentRanked.sort((a, b) => (b.bestScore - a.bestScore) || (a.group.floor - b.group.floor));
|
recentRanked.sort((a, b) => b.group.floor - a.group.floor);
|
||||||
|
|
||||||
const acceptedRecentGroups = [];
|
const acceptedRecentGroups = [];
|
||||||
for (const item of recentRanked) {
|
for (const item of recentRanked) {
|
||||||
@@ -1277,6 +1325,8 @@ async function buildVectorPrompt(store, recallResult, causalById, focusCharacter
|
|||||||
};
|
};
|
||||||
|
|
||||||
metrics.evidence.tokens = injectionStats.distantEvidence.tokens + injectionStats.recentEvidence.tokens;
|
metrics.evidence.tokens = injectionStats.distantEvidence.tokens + injectionStats.recentEvidence.tokens;
|
||||||
|
metrics.evidence.recentSource = 'all_l0_window';
|
||||||
|
metrics.evidence.recentL1Attached = 0;
|
||||||
metrics.evidence.assemblyTime = Math.round(
|
metrics.evidence.assemblyTime = Math.round(
|
||||||
performance.now() - T_Start - (metrics.timing.constraintFilter || 0) - metrics.formatting.time
|
performance.now() - T_Start - (metrics.timing.constraintFilter || 0) - metrics.formatting.time
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -87,6 +87,7 @@
|
|||||||
api: { provider: 'st', url: '', key: '', model: '', modelCache: [] },
|
api: { provider: 'st', url: '', key: '', model: '', modelCache: [] },
|
||||||
gen: { temperature: null, top_p: null, top_k: null, presence_penalty: null, frequency_penalty: null },
|
gen: { temperature: null, top_p: null, top_k: null, presence_penalty: null, frequency_penalty: null },
|
||||||
trigger: { enabled: false, interval: 20, timing: 'before_user', role: 'system', useStream: true, maxPerRun: 100, wrapperHead: '', wrapperTail: '', forceInsertAtEnd: false },
|
trigger: { enabled: false, interval: 20, timing: 'before_user', role: 'system', useStream: true, maxPerRun: 100, wrapperHead: '', wrapperTail: '', forceInsertAtEnd: false },
|
||||||
|
ui: { hideSummarized: true, keepVisibleCount: 6 },
|
||||||
textFilterRules: [...DEFAULT_FILTER_RULES],
|
textFilterRules: [...DEFAULT_FILTER_RULES],
|
||||||
vector: { enabled: false, engine: 'online', local: { modelId: 'bge-small-zh' }, online: { provider: 'siliconflow', url: '', key: '', model: '' } }
|
vector: { enabled: false, engine: 'online', local: { modelId: 'bge-small-zh' }, online: { provider: 'siliconflow', url: '', key: '', model: '' } }
|
||||||
};
|
};
|
||||||
@@ -124,6 +125,7 @@
|
|||||||
Object.assign(config.api, p.api || {});
|
Object.assign(config.api, p.api || {});
|
||||||
Object.assign(config.gen, p.gen || {});
|
Object.assign(config.gen, p.gen || {});
|
||||||
Object.assign(config.trigger, p.trigger || {});
|
Object.assign(config.trigger, p.trigger || {});
|
||||||
|
Object.assign(config.ui, p.ui || {});
|
||||||
config.textFilterRules = Array.isArray(p.textFilterRules)
|
config.textFilterRules = Array.isArray(p.textFilterRules)
|
||||||
? p.textFilterRules
|
? p.textFilterRules
|
||||||
: (Array.isArray(p.vector?.textFilterRules) ? p.vector.textFilterRules : [...DEFAULT_FILTER_RULES]);
|
: (Array.isArray(p.vector?.textFilterRules) ? p.vector.textFilterRules : [...DEFAULT_FILTER_RULES]);
|
||||||
@@ -141,6 +143,7 @@
|
|||||||
Object.assign(config.api, cfg.api || {});
|
Object.assign(config.api, cfg.api || {});
|
||||||
Object.assign(config.gen, cfg.gen || {});
|
Object.assign(config.gen, cfg.gen || {});
|
||||||
Object.assign(config.trigger, cfg.trigger || {});
|
Object.assign(config.trigger, cfg.trigger || {});
|
||||||
|
Object.assign(config.ui, cfg.ui || {});
|
||||||
config.textFilterRules = Array.isArray(cfg.textFilterRules)
|
config.textFilterRules = Array.isArray(cfg.textFilterRules)
|
||||||
? cfg.textFilterRules
|
? cfg.textFilterRules
|
||||||
: (Array.isArray(cfg.vector?.textFilterRules)
|
: (Array.isArray(cfg.vector?.textFilterRules)
|
||||||
@@ -1599,7 +1602,8 @@
|
|||||||
// Hide summarized
|
// Hide summarized
|
||||||
$('hide-summarized').onchange = e => postMsg('TOGGLE_HIDE_SUMMARIZED', { enabled: e.target.checked });
|
$('hide-summarized').onchange = e => postMsg('TOGGLE_HIDE_SUMMARIZED', { enabled: e.target.checked });
|
||||||
$('keep-visible-count').onchange = e => {
|
$('keep-visible-count').onchange = e => {
|
||||||
const c = Math.max(0, Math.min(50, parseInt(e.target.value) || 3));
|
const parsedCount = Number.parseInt(e.target.value, 10);
|
||||||
|
const c = Number.isFinite(parsedCount) ? Math.max(0, Math.min(50, parsedCount)) : 6;
|
||||||
e.target.value = c;
|
e.target.value = c;
|
||||||
postMsg('UPDATE_KEEP_VISIBLE', { count: c });
|
postMsg('UPDATE_KEEP_VISIBLE', { count: c });
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import { postToIframe, isTrustedMessage } from "../../core/iframe-messaging.js";
|
|||||||
import { CommonSettingStorage } from "../../core/server-storage.js";
|
import { CommonSettingStorage } from "../../core/server-storage.js";
|
||||||
|
|
||||||
// config/store
|
// config/store
|
||||||
import { getSettings, getSummaryPanelConfig, getVectorConfig, saveVectorConfig } from "./data/config.js";
|
import { getSettings, getSummaryPanelConfig, getVectorConfig, saveVectorConfig, saveSummaryPanelConfig } from "./data/config.js";
|
||||||
import {
|
import {
|
||||||
getSummaryStore,
|
getSummaryStore,
|
||||||
saveSummaryStore,
|
saveSummaryStore,
|
||||||
@@ -951,10 +951,41 @@ async function sendSavedConfigToFrame() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getHideUiSettings() {
|
||||||
|
const cfg = getSummaryPanelConfig() || {};
|
||||||
|
const ui = cfg.ui || {};
|
||||||
|
const parsedKeep = Number.parseInt(ui.keepVisibleCount, 10);
|
||||||
|
const keepVisibleCount = Number.isFinite(parsedKeep) ? Math.max(0, Math.min(50, parsedKeep)) : 6;
|
||||||
|
return {
|
||||||
|
hideSummarized: !!ui.hideSummarized,
|
||||||
|
keepVisibleCount,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function setHideUiSettings(patch = {}) {
|
||||||
|
const cfg = getSummaryPanelConfig() || {};
|
||||||
|
const current = getHideUiSettings();
|
||||||
|
const next = {
|
||||||
|
...cfg,
|
||||||
|
ui: {
|
||||||
|
hideSummarized: patch.hideSummarized !== undefined ? !!patch.hideSummarized : current.hideSummarized,
|
||||||
|
keepVisibleCount: patch.keepVisibleCount !== undefined
|
||||||
|
? (() => {
|
||||||
|
const parsedKeep = Number.parseInt(patch.keepVisibleCount, 10);
|
||||||
|
return Number.isFinite(parsedKeep) ? Math.max(0, Math.min(50, parsedKeep)) : 6;
|
||||||
|
})()
|
||||||
|
: current.keepVisibleCount,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
saveSummaryPanelConfig(next);
|
||||||
|
return next.ui;
|
||||||
|
}
|
||||||
|
|
||||||
async function sendFrameBaseData(store, totalFloors) {
|
async function sendFrameBaseData(store, totalFloors) {
|
||||||
|
const ui = getHideUiSettings();
|
||||||
const boundary = await getHideBoundaryFloor(store);
|
const boundary = await getHideBoundaryFloor(store);
|
||||||
const range = calcHideRange(boundary);
|
const range = calcHideRange(boundary, ui.keepVisibleCount);
|
||||||
const hiddenCount = (store?.hideSummarizedHistory && range) ? (range.end + 1) : 0;
|
const hiddenCount = (ui.hideSummarized && range) ? (range.end + 1) : 0;
|
||||||
|
|
||||||
const lastSummarized = store?.lastSummarizedMesId ?? -1;
|
const lastSummarized = store?.lastSummarizedMesId ?? -1;
|
||||||
postToFrame({
|
postToFrame({
|
||||||
@@ -966,8 +997,8 @@ async function sendFrameBaseData(store, totalFloors) {
|
|||||||
pendingFloors: totalFloors - lastSummarized - 1,
|
pendingFloors: totalFloors - lastSummarized - 1,
|
||||||
hiddenCount,
|
hiddenCount,
|
||||||
},
|
},
|
||||||
hideSummarized: store?.hideSummarizedHistory || false,
|
hideSummarized: ui.hideSummarized,
|
||||||
keepVisibleCount: store?.keepVisibleCount ?? 3,
|
keepVisibleCount: ui.keepVisibleCount,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1042,7 +1073,8 @@ async function getHideBoundaryFloor(store) {
|
|||||||
|
|
||||||
async function applyHideState() {
|
async function applyHideState() {
|
||||||
const store = getSummaryStore();
|
const store = getSummaryStore();
|
||||||
if (!store?.hideSummarizedHistory) return;
|
const ui = getHideUiSettings();
|
||||||
|
if (!ui.hideSummarized) return;
|
||||||
|
|
||||||
// 先全量 unhide,杜绝历史残留
|
// 先全量 unhide,杜绝历史残留
|
||||||
await unhideAllMessages();
|
await unhideAllMessages();
|
||||||
@@ -1050,7 +1082,7 @@ async function applyHideState() {
|
|||||||
const boundary = await getHideBoundaryFloor(store);
|
const boundary = await getHideBoundaryFloor(store);
|
||||||
if (boundary < 0) return;
|
if (boundary < 0) return;
|
||||||
|
|
||||||
const range = calcHideRange(boundary);
|
const range = calcHideRange(boundary, ui.keepVisibleCount);
|
||||||
if (!range) return;
|
if (!range) return;
|
||||||
|
|
||||||
await executeSlashCommand(`/hide ${range.start}-${range.end}`);
|
await executeSlashCommand(`/hide ${range.start}-${range.end}`);
|
||||||
@@ -1150,9 +1182,9 @@ async function autoRunSummaryWithRetry(targetMesId, configForRun) {
|
|||||||
function updateFrameStatsAfterSummary(endMesId, merged) {
|
function updateFrameStatsAfterSummary(endMesId, merged) {
|
||||||
const { chat } = getContext();
|
const { chat } = getContext();
|
||||||
const totalFloors = Array.isArray(chat) ? chat.length : 0;
|
const totalFloors = Array.isArray(chat) ? chat.length : 0;
|
||||||
const store = getSummaryStore();
|
const ui = getHideUiSettings();
|
||||||
const range = calcHideRange(endMesId);
|
const range = calcHideRange(endMesId, ui.keepVisibleCount);
|
||||||
const hiddenCount = store?.hideSummarizedHistory && range ? range.end + 1 : 0;
|
const hiddenCount = ui.hideSummarized && range ? range.end + 1 : 0;
|
||||||
|
|
||||||
postToFrame({
|
postToFrame({
|
||||||
type: "SUMMARY_BASE_DATA",
|
type: "SUMMARY_BASE_DATA",
|
||||||
@@ -1348,11 +1380,7 @@ async function handleFrameMessage(event) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case "TOGGLE_HIDE_SUMMARIZED": {
|
case "TOGGLE_HIDE_SUMMARIZED": {
|
||||||
const store = getSummaryStore();
|
setHideUiSettings({ hideSummarized: !!data.enabled });
|
||||||
if (!store) break;
|
|
||||||
|
|
||||||
store.hideSummarizedHistory = !!data.enabled;
|
|
||||||
saveSummaryStore();
|
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
if (data.enabled) {
|
if (data.enabled) {
|
||||||
@@ -1365,21 +1393,19 @@ async function handleFrameMessage(event) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case "UPDATE_KEEP_VISIBLE": {
|
case "UPDATE_KEEP_VISIBLE": {
|
||||||
const store = getSummaryStore();
|
const oldCount = getHideUiSettings().keepVisibleCount;
|
||||||
if (!store) break;
|
const parsedCount = Number.parseInt(data.count, 10);
|
||||||
|
const newCount = Number.isFinite(parsedCount) ? Math.max(0, Math.min(50, parsedCount)) : 6;
|
||||||
const oldCount = store.keepVisibleCount ?? 3;
|
|
||||||
const newCount = Math.max(0, Math.min(50, parseInt(data.count) || 3));
|
|
||||||
if (newCount === oldCount) break;
|
if (newCount === oldCount) break;
|
||||||
|
|
||||||
store.keepVisibleCount = newCount;
|
setHideUiSettings({ keepVisibleCount: newCount });
|
||||||
saveSummaryStore();
|
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
if (store.hideSummarizedHistory) {
|
if (getHideUiSettings().hideSummarized) {
|
||||||
await applyHideState();
|
await applyHideState();
|
||||||
}
|
}
|
||||||
const { chat } = getContext();
|
const { chat } = getContext();
|
||||||
|
const store = getSummaryStore();
|
||||||
await sendFrameBaseData(store, Array.isArray(chat) ? chat.length : 0);
|
await sendFrameBaseData(store, Array.isArray(chat) ? chat.length : 0);
|
||||||
})();
|
})();
|
||||||
break;
|
break;
|
||||||
@@ -1453,7 +1479,7 @@ async function handleChatChanged() {
|
|||||||
|
|
||||||
const store = getSummaryStore();
|
const store = getSummaryStore();
|
||||||
|
|
||||||
if (store?.hideSummarizedHistory) {
|
if (getHideUiSettings().hideSummarized) {
|
||||||
await applyHideState();
|
await applyHideState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user