fix: persist story-summary relationships and sync local changes
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
// @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";
|
||||||
@@ -51,7 +51,7 @@ 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 { }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -260,7 +260,7 @@ class CallGenerateService {
|
|||||||
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 干跑捕获与组件切换 =====
|
||||||
@@ -352,7 +352,7 @@ class CallGenerateService {
|
|||||||
const order = pm?.getPromptOrderForCharacter(activeChar) ?? [];
|
const order = pm?.getPromptOrderForCharacter(activeChar) ?? [];
|
||||||
const mapSnap = new Map((this._lastToggleSnapshot || snapshot).map(s => [s.identifier, s.enabled]));
|
const mapSnap = new Map((this._lastToggleSnapshot || snapshot).map(s => [s.identifier, s.enabled]));
|
||||||
order.forEach(e => { if (mapSnap.has(e.identifier)) e.enabled = mapSnap.get(e.identifier); });
|
order.forEach(e => { if (mapSnap.has(e.identifier)) e.enabled = mapSnap.get(e.identifier); });
|
||||||
} catch {}
|
} catch { }
|
||||||
this._toggleBusy = false;
|
this._toggleBusy = false;
|
||||||
this._lastToggleSnapshot = null;
|
this._lastToggleSnapshot = null;
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ===== 工具函数:组件与消息辅助 =====
|
// ===== 工具函数:组件与消息辅助 =====
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -466,7 +503,7 @@ class CallGenerateService {
|
|||||||
try {
|
try {
|
||||||
const nameCache = this._getNameCache();
|
const nameCache = this._getNameCache();
|
||||||
if (nameCache.has(nm)) return nameCache.get(nm);
|
if (nameCache.has(nm)) return nameCache.get(nm);
|
||||||
} catch {}
|
} catch { }
|
||||||
|
|
||||||
// 2) 扫描 PromptManager 的订单(显示用)
|
// 2) 扫描 PromptManager 的订单(显示用)
|
||||||
try {
|
try {
|
||||||
@@ -484,7 +521,7 @@ class CallGenerateService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch { }
|
||||||
|
|
||||||
// 3) 扫描 Prompt 集合(运行期合并后的集合)
|
// 3) 扫描 Prompt 集合(运行期合并后的集合)
|
||||||
try {
|
try {
|
||||||
@@ -501,7 +538,7 @@ class CallGenerateService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch { }
|
||||||
|
|
||||||
// 4) 失败时尝试 sanitize 名称与 identifier 的弱匹配
|
// 4) 失败时尝试 sanitize 名称与 identifier 的弱匹配
|
||||||
if (matches.size === 0) {
|
if (matches.size === 0) {
|
||||||
@@ -518,12 +555,12 @@ class CallGenerateService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch { }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (matches.size === 1) {
|
if (matches.size === 1) {
|
||||||
const id = Array.from(matches)[0];
|
const id = Array.from(matches)[0];
|
||||||
try { this._getNameCache().set(nm, id); } catch {}
|
try { this._getNameCache().set(nm, id); } catch { }
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
if (matches.size > 1) {
|
if (matches.size > 1) {
|
||||||
@@ -786,9 +823,9 @@ class CallGenerateService {
|
|||||||
const capture = await this._captureWithEnabledSet(new Set([key]), '', false);
|
const capture = await this._captureWithEnabledSet(new Set([key]), '', false);
|
||||||
const normSet = new Set(capture.map(x => `[${x.role}] ${this._normStrip(x.content)}`));
|
const normSet = new Set(capture.map(x => `[${x.role}] ${this._normStrip(x.content)}`));
|
||||||
footprint.set(key, normSet);
|
footprint.set(key, normSet);
|
||||||
try { fpCache.set(key, normSet); } catch {}
|
try { fpCache.set(key, normSet); } catch { }
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch { }
|
||||||
}
|
}
|
||||||
for (const m of arr) {
|
for (const m of arr) {
|
||||||
if (m?.identifier) continue;
|
if (m?.identifier) continue;
|
||||||
@@ -1018,7 +1055,7 @@ class CallGenerateService {
|
|||||||
try {
|
try {
|
||||||
const re = new RegExp(regex);
|
const re = new RegExp(regex);
|
||||||
out = out.filter(m => re.test(String(m.content)));
|
out = out.filter(m => re.test(String(m.content)));
|
||||||
} catch {}
|
} catch { }
|
||||||
}
|
}
|
||||||
if (fromUserNames && fromUserNames.length) {
|
if (fromUserNames && fromUserNames.length) {
|
||||||
// 仅当 messages 中附带 name 时生效;否则忽略
|
// 仅当 messages 中附带 name 时生效;否则忽略
|
||||||
@@ -1183,104 +1220,148 @@ class CallGenerateService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ===== 主流程 =====
|
// ===== 主流程 =====
|
||||||
async handleRequestInternal(options, requestId, sourceWindow, targetOrigin = null) {
|
async handleRequestInternal(options, requestId, sourceWindow, targetOrigin = null, { assembleOnly = false } = {}) {
|
||||||
// 1) 校验
|
// 1) 校验
|
||||||
this.validateOptions(options);
|
this.validateOptions(options);
|
||||||
|
|
||||||
// 2) 解析组件列表与内联注入
|
const presetName = options?.preset || null;
|
||||||
const list = Array.isArray(options?.components?.list) ? options.components.list.slice() : undefined;
|
|
||||||
let baseStrategy = 'EMPTY'; // EMPTY | ALL | ALL_PREON | SUBSET
|
// 步骤 2~9 的核心逻辑,可能包裹在 _withTemporaryPreset 中执行
|
||||||
let orderedRefs = [];
|
const executeCore = async () => {
|
||||||
let inlineMapped = [];
|
|
||||||
let listLevelOverrides = {};
|
// 2) 解析组件列表与内联注入
|
||||||
const unorderedKeys = new Set();
|
const list = Array.isArray(options?.components?.list) ? options.components.list.slice() : undefined;
|
||||||
if (list && list.length) {
|
let baseStrategy = 'EMPTY'; // EMPTY | ALL | ALL_PREON | SUBSET
|
||||||
const { references, inlineInjections, listOverrides } = this._parseUnifiedList(list);
|
let orderedRefs = [];
|
||||||
listLevelOverrides = listOverrides || {};
|
let inlineMapped = [];
|
||||||
const parsedRefs = references.map(t => this._parseComponentRefToken(t));
|
let listLevelOverrides = {};
|
||||||
const containsAll = parsedRefs.includes('ALL');
|
const unorderedKeys = new Set();
|
||||||
const containsAllPreOn = parsedRefs.includes('ALL_PREON');
|
if (list && list.length) {
|
||||||
if (containsAll) {
|
const { references, inlineInjections, listOverrides } = this._parseUnifiedList(list);
|
||||||
baseStrategy = 'ALL';
|
listLevelOverrides = listOverrides || {};
|
||||||
// ALL 仅作为开关标识,子集重排目标为去除 ALL 后的引用列表
|
const parsedRefs = references.map(t => this._parseComponentRefToken(t));
|
||||||
orderedRefs = parsedRefs.filter(x => x && x !== 'ALL' && x !== 'ALL_PREON');
|
const containsAll = parsedRefs.includes('ALL');
|
||||||
} else if (containsAllPreOn) {
|
const containsAllPreOn = parsedRefs.includes('ALL_PREON');
|
||||||
baseStrategy = 'ALL_PREON';
|
if (containsAll) {
|
||||||
// ALL_PREON:仅启用“预设里已开启”的组件,子集重排目标为去除该标记后的引用列表
|
baseStrategy = 'ALL';
|
||||||
orderedRefs = parsedRefs.filter(x => x && x !== 'ALL' && x !== 'ALL_PREON');
|
// ALL 仅作为开关标识,子集重排目标为去除 ALL 后的引用列表
|
||||||
|
orderedRefs = parsedRefs.filter(x => x && x !== 'ALL' && x !== 'ALL_PREON');
|
||||||
|
} else if (containsAllPreOn) {
|
||||||
|
baseStrategy = 'ALL_PREON';
|
||||||
|
// ALL_PREON:仅启用“预设里已开启”的组件,子集重排目标为去除该标记后的引用列表
|
||||||
|
orderedRefs = parsedRefs.filter(x => x && x !== 'ALL' && x !== 'ALL_PREON');
|
||||||
|
} else {
|
||||||
|
baseStrategy = 'SUBSET';
|
||||||
|
orderedRefs = parsedRefs.filter(Boolean);
|
||||||
|
}
|
||||||
|
inlineMapped = this._mapInlineInjectionsUnified(list, inlineInjections);
|
||||||
|
// 放宽:ALL 可出现在任意位置,作为“启用全部”的标志
|
||||||
|
|
||||||
|
// 解析 order=false:不参与重排
|
||||||
|
for (const rawKey in listLevelOverrides) {
|
||||||
|
if (!Object.prototype.hasOwnProperty.call(listLevelOverrides, rawKey)) continue;
|
||||||
|
const k = this._parseComponentRefToken(rawKey);
|
||||||
|
if (!k) continue;
|
||||||
|
if (listLevelOverrides[rawKey] && listLevelOverrides[rawKey].order === false) unorderedKeys.add(k);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3) 干跑捕获(基座)
|
||||||
|
let captured = [];
|
||||||
|
let enabledIds = []; // assembleOnly 时用于 identifier 标注
|
||||||
|
if (baseStrategy === 'EMPTY') {
|
||||||
|
captured = [];
|
||||||
} else {
|
} else {
|
||||||
baseStrategy = 'SUBSET';
|
// 不将 userInput 作为 quietText 干跑,以免把其注入到历史里
|
||||||
orderedRefs = parsedRefs.filter(Boolean);
|
if (baseStrategy === 'ALL') {
|
||||||
|
// 路径B:ALL 时先全开启用集合再干跑,保证真实组件尽量出现
|
||||||
|
// 读取 promptManager 订单并构造 allow 集合
|
||||||
|
let allow = new Set();
|
||||||
|
try {
|
||||||
|
if (promptManager && typeof promptManager.getPromptOrderForCharacter === 'function') {
|
||||||
|
const pm = promptManager;
|
||||||
|
const activeChar = pm?.activeCharacter ?? null;
|
||||||
|
const order = pm?.getPromptOrderForCharacter(activeChar) ?? [];
|
||||||
|
allow = new Set(order.map(e => e.identifier));
|
||||||
|
}
|
||||||
|
} catch { }
|
||||||
|
enabledIds = Array.from(allow);
|
||||||
|
const run = async () => await this._capturePromptMessages({ includeConfig: null, quietText: '', skipWIAN: false });
|
||||||
|
captured = await this._withPromptEnabledSet(allow, run);
|
||||||
|
} else if (baseStrategy === 'ALL_PREON') {
|
||||||
|
// 仅启用预设里已开启的组件
|
||||||
|
let allow = new Set();
|
||||||
|
try {
|
||||||
|
if (promptManager && typeof promptManager.getPromptOrderForCharacter === 'function') {
|
||||||
|
const pm = promptManager;
|
||||||
|
const activeChar = pm?.activeCharacter ?? null;
|
||||||
|
const order = pm?.getPromptOrderForCharacter(activeChar) ?? [];
|
||||||
|
allow = new Set(order.filter(e => !!e?.enabled).map(e => e.identifier));
|
||||||
|
}
|
||||||
|
} catch { }
|
||||||
|
enabledIds = Array.from(allow);
|
||||||
|
const run = async () => await this._capturePromptMessages({ includeConfig: null, quietText: '', skipWIAN: false });
|
||||||
|
captured = await this._withPromptEnabledSet(allow, run);
|
||||||
|
} else {
|
||||||
|
captured = await this._capturePromptMessages({ includeConfig: null, quietText: '', skipWIAN: false });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
inlineMapped = this._mapInlineInjectionsUnified(list, inlineInjections);
|
|
||||||
// 放宽:ALL 可出现在任意位置,作为“启用全部”的标志
|
|
||||||
|
|
||||||
// 解析 order=false:不参与重排
|
// 4) 依据策略计算启用集合与顺序
|
||||||
for (const rawKey in listLevelOverrides) {
|
let annotateKeys = baseStrategy === 'SUBSET' ? orderedRefs : ((baseStrategy === 'ALL' || baseStrategy === 'ALL_PREON') ? orderedRefs : []);
|
||||||
if (!Object.prototype.hasOwnProperty.call(listLevelOverrides, rawKey)) continue;
|
// assembleOnly 模式下,若无显式排序引用,则用全部启用组件做 identifier 标注
|
||||||
const k = this._parseComponentRefToken(rawKey);
|
if (assembleOnly && annotateKeys.length === 0 && enabledIds.length > 0) {
|
||||||
if (!k) continue;
|
annotateKeys = enabledIds;
|
||||||
if (listLevelOverrides[rawKey] && listLevelOverrides[rawKey].order === false) unorderedKeys.add(k);
|
|
||||||
}
|
}
|
||||||
}
|
let working = await this._annotateIdentifiersIfMissing(captured.slice(), annotateKeys);
|
||||||
|
working = this._applyOrderingStrategy(working, baseStrategy, orderedRefs, unorderedKeys);
|
||||||
|
|
||||||
// 3) 干跑捕获(基座)
|
// 5) 覆写与创建
|
||||||
let captured = [];
|
working = this._applyInlineOverrides(working, listLevelOverrides);
|
||||||
if (baseStrategy === 'EMPTY') {
|
|
||||||
captured = [];
|
// 6) 注入(内联 + 高级)
|
||||||
} else {
|
working = this._applyAllInjections(working, inlineMapped, options?.injections);
|
||||||
// 不将 userInput 作为 quietText 干跑,以免把其注入到历史里
|
|
||||||
if (baseStrategy === 'ALL') {
|
// 7) 用户输入追加
|
||||||
// 路径B:ALL 时先全开启用集合再干跑,保证真实组件尽量出现
|
working = this._appendUserInput(working, options?.userInput);
|
||||||
// 读取 promptManager 订单并构造 allow 集合
|
|
||||||
let allow = new Set();
|
// 8) 调试导出
|
||||||
|
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 {
|
try {
|
||||||
if (promptManager && typeof promptManager.getPromptOrderForCharacter === 'function') {
|
if (promptManager && typeof promptManager.getPromptCollection === 'function') {
|
||||||
const pm = promptManager;
|
const pc = promptManager.getPromptCollection();
|
||||||
const activeChar = pm?.activeCharacter ?? null;
|
const coll = pc?.collection || [];
|
||||||
const order = pm?.getPromptOrderForCharacter(activeChar) ?? [];
|
for (const p of coll) {
|
||||||
allow = new Set(order.map(e => e.identifier));
|
if (p?.identifier) idToName.set(p.identifier, p.name || p.label || p.title || '');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch { }
|
||||||
const run = async () => await this._capturePromptMessages({ includeConfig: null, quietText: '', skipWIAN: false });
|
const messages = working.map(m => {
|
||||||
captured = await this._withPromptEnabledSet(allow, run);
|
const id = m.identifier || undefined;
|
||||||
} else if (baseStrategy === 'ALL_PREON') {
|
const componentName = id ? (idToName.get(id) || undefined) : undefined;
|
||||||
// 仅启用预设里已开启的组件
|
return { role: m.role, content: m.content, identifier: id, name: componentName };
|
||||||
let allow = new Set();
|
});
|
||||||
try {
|
this.postToTarget(sourceWindow, 'assemblePromptResult', {
|
||||||
if (promptManager && typeof promptManager.getPromptOrderForCharacter === 'function') {
|
id: requestId,
|
||||||
const pm = promptManager;
|
messages: messages
|
||||||
const activeChar = pm?.activeCharacter ?? null;
|
}, targetOrigin);
|
||||||
const order = pm?.getPromptOrderForCharacter(activeChar) ?? [];
|
return { messages };
|
||||||
allow = new Set(order.filter(e => !!e?.enabled).map(e => e.identifier));
|
|
||||||
}
|
|
||||||
} catch {}
|
|
||||||
const run = async () => await this._capturePromptMessages({ includeConfig: null, quietText: '', skipWIAN: false });
|
|
||||||
captured = await this._withPromptEnabledSet(allow, run);
|
|
||||||
} else {
|
|
||||||
captured = await this._capturePromptMessages({ includeConfig: null, quietText: '', skipWIAN: false });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 9) 发送
|
||||||
|
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();
|
||||||
// 4) 依据策略计算启用集合与顺序
|
|
||||||
const annotateKeys = baseStrategy === 'SUBSET' ? orderedRefs : ((baseStrategy === 'ALL' || baseStrategy === 'ALL_PREON') ? orderedRefs : []);
|
|
||||||
let working = await this._annotateIdentifiersIfMissing(captured.slice(), annotateKeys);
|
|
||||||
working = this._applyOrderingStrategy(working, baseStrategy, orderedRefs, unorderedKeys);
|
|
||||||
|
|
||||||
// 5) 覆写与创建
|
|
||||||
working = this._applyInlineOverrides(working, listLevelOverrides);
|
|
||||||
|
|
||||||
// 6) 注入(内联 + 高级)
|
|
||||||
working = this._applyAllInjections(working, inlineMapped, options?.injections);
|
|
||||||
|
|
||||||
// 7) 用户输入追加
|
|
||||||
working = this._appendUserInput(working, options?.userInput);
|
|
||||||
|
|
||||||
// 8) 调试导出
|
|
||||||
this._exportDebugData({ sourceWindow, requestId, working, baseStrategy, orderedRefs, inlineMapped, listLevelOverrides, debug: options?.debug, targetOrigin });
|
|
||||||
|
|
||||||
// 9) 发送
|
|
||||||
return await this._sendMessages(working, { ...options, debug: { ...(options?.debug || {}), _exported: true } }, requestId, sourceWindow, targetOrigin);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_applyOrderingStrategy(messages, baseStrategy, orderedRefs, unorderedKeys) {
|
_applyOrderingStrategy(messages, baseStrategy, orderedRefs, unorderedKeys) {
|
||||||
@@ -1352,7 +1433,7 @@ class CallGenerateService {
|
|||||||
overrides: listLevelOverrides || null,
|
overrides: listLevelOverrides || null,
|
||||||
};
|
};
|
||||||
this.postToTarget(sourceWindow, 'blueprint', bp, targetOrigin);
|
this.postToTarget(sourceWindow, 'blueprint', bp, targetOrigin);
|
||||||
} catch {}
|
} catch { }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1370,10 +1451,10 @@ class CallGenerateService {
|
|||||||
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;
|
||||||
}
|
}
|
||||||
@@ -1382,12 +1463,12 @@ class CallGenerateService {
|
|||||||
/** 取消会话 */
|
/** 取消会话 */
|
||||||
cancel(sessionId) {
|
cancel(sessionId) {
|
||||||
const s = this.sessions.get(this.normalizeSessionId(sessionId));
|
const s = this.sessions.get(this.normalizeSessionId(sessionId));
|
||||||
try { s?.abortController?.abort(); } catch {}
|
try { s?.abortController?.abort(); } catch { }
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 清理所有会话 */
|
/** 清理所有会话 */
|
||||||
cleanup() {
|
cleanup() {
|
||||||
this.sessions.forEach(s => { try { s.abortController?.abort(); } catch {} });
|
this.sessions.forEach(s => { try { s.abortController?.abort(); } catch { } });
|
||||||
this.sessions.clear();
|
this.sessions.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1405,51 +1486,84 @@ 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 || {};
|
if (data.type === 'generateRequest') {
|
||||||
await handleGenerateRequest(options, id, event.source || window, event.origin);
|
const id = data.id;
|
||||||
|
const options = data.options || {};
|
||||||
|
await handleGenerateRequest(options, id, event.source || window, event.origin);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
} catch (e) {
|
||||||
try { xbLog.error('callGenerateBridge', 'generateRequest listener error', e); } catch {}
|
try { xbLog.error('callGenerateBridge', 'listener error', e); } catch { }
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// eslint-disable-next-line no-restricted-syntax -- bridge listener; origin can be null for sandboxed iframes.
|
// 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) {}
|
try { window.addEventListener('message', __xb_generate_listener); } catch (e) { }
|
||||||
__xb_generate_listener_attached = true;
|
__xb_generate_listener_attached = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function cleanupCallGenerateHostBridge() {
|
export function cleanupCallGenerateHostBridge() {
|
||||||
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', 'cleanupCallGenerateHostBridge'); } catch {}
|
try { xbLog.info('callGenerateBridge', 'cleanupCallGenerateHostBridge'); } catch { }
|
||||||
try { window.removeEventListener('message', __xb_generate_listener); } catch (e) {}
|
try { window.removeEventListener('message', __xb_generate_listener); } catch (e) { }
|
||||||
__xb_generate_listener_attached = false;
|
__xb_generate_listener_attached = false;
|
||||||
__xb_generate_listener = null;
|
__xb_generate_listener = null;
|
||||||
try { callGenerateService.cleanup(); } catch (e) {}
|
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 });
|
||||||
try { initCallGenerateHostBridge(); } catch (e) {}
|
try { initCallGenerateHostBridge(); } catch (e) { }
|
||||||
try {
|
try {
|
||||||
window.addEventListener('xiaobaixEnabledChanged', (e) => {
|
window.addEventListener('xiaobaixEnabledChanged', (e) => {
|
||||||
try {
|
try {
|
||||||
const enabled = e && e.detail && e.detail.enabled === true;
|
const enabled = e && e.detail && e.detail.enabled === true;
|
||||||
if (enabled) initCallGenerateHostBridge(); else cleanupCallGenerateHostBridge();
|
if (enabled) initCallGenerateHostBridge(); else cleanupCallGenerateHostBridge();
|
||||||
} catch (_) {}
|
} catch (_) { }
|
||||||
});
|
});
|
||||||
document.addEventListener('xiaobaixEnabledChanged', (e) => {
|
document.addEventListener('xiaobaixEnabledChanged', (e) => {
|
||||||
try {
|
try {
|
||||||
const enabled = e && e.detail && e.detail.enabled === true;
|
const enabled = e && e.detail && e.detail.enabled === true;
|
||||||
if (enabled) initCallGenerateHostBridge(); else cleanupCallGenerateHostBridge();
|
if (enabled) initCallGenerateHostBridge(); else cleanupCallGenerateHostBridge();
|
||||||
} catch (_) {}
|
} catch (_) { }
|
||||||
});
|
});
|
||||||
window.addEventListener('beforeunload', () => { try { cleanupCallGenerateHostBridge(); } catch (_) {} });
|
window.addEventListener('beforeunload', () => { try { cleanupCallGenerateHostBridge(); } catch (_) { } });
|
||||||
} catch (_) {}
|
} catch (_) { }
|
||||||
|
|
||||||
// ===== 全局 API 暴露:与 iframe 调用方式完全一致 =====
|
// ===== 全局 API 暴露:与 iframe 调用方式完全一致 =====
|
||||||
// 创建命名空间
|
// 创建命名空间
|
||||||
@@ -1479,7 +1593,7 @@ if (typeof window !== 'undefined') {
|
|||||||
* api: { inherit: true }
|
* api: { inherit: true }
|
||||||
* });
|
* });
|
||||||
*/
|
*/
|
||||||
window.LittleWhiteBox.callGenerate = async function(options) {
|
window.LittleWhiteBox.callGenerate = async function (options) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const requestId = `global-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
const requestId = `global-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
||||||
const streamingEnabled = options?.streaming?.enabled !== false;
|
const streamingEnabled = options?.streaming?.enabled !== false;
|
||||||
@@ -1525,21 +1639,72 @@ 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' 等)
|
||||||
*/
|
*/
|
||||||
window.LittleWhiteBox.callGenerate.cancel = function(sessionId) {
|
window.LittleWhiteBox.callGenerate.cancel = function (sessionId) {
|
||||||
callGenerateService.cancel(sessionId);
|
callGenerateService.cancel(sessionId);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 清理所有会话
|
* 清理所有会话
|
||||||
*/
|
*/
|
||||||
window.LittleWhiteBox.callGenerate.cleanup = function() {
|
window.LittleWhiteBox.callGenerate.cleanup = function () {
|
||||||
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,
|
||||||
|
|||||||
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 { }
|
||||||
|
}
|
||||||
93
index.js
93
index.js
@@ -194,13 +194,13 @@ function addUpdateDownloadButton() {
|
|||||||
totalSwitchDivider.style.display = 'flex';
|
totalSwitchDivider.style.display = 'flex';
|
||||||
totalSwitchDivider.style.alignItems = 'center';
|
totalSwitchDivider.style.alignItems = 'center';
|
||||||
totalSwitchDivider.style.justifyContent = 'flex-start';
|
totalSwitchDivider.style.justifyContent = 'flex-start';
|
||||||
} catch (e) {}
|
} catch (e) { }
|
||||||
totalSwitchDivider.appendChild(updateButton);
|
totalSwitchDivider.appendChild(updateButton);
|
||||||
try {
|
try {
|
||||||
if (window.setupUpdateButtonInSettings) {
|
if (window.setupUpdateButtonInSettings) {
|
||||||
window.setupUpdateButtonInSettings();
|
window.setupUpdateButtonInSettings();
|
||||||
}
|
}
|
||||||
} catch (e) {}
|
} catch (e) { }
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeAllUpdateNotices() {
|
function removeAllUpdateNotices() {
|
||||||
@@ -218,7 +218,7 @@ async function performExtensionUpdateCheck() {
|
|||||||
if (versionData && versionData.isUpToDate === false) {
|
if (versionData && versionData.isUpToDate === false) {
|
||||||
updateExtensionHeaderWithUpdateNotice();
|
updateExtensionHeaderWithUpdateNotice();
|
||||||
}
|
}
|
||||||
} catch (error) {}
|
} catch (error) { }
|
||||||
}
|
}
|
||||||
|
|
||||||
function registerModuleCleanup(moduleName, cleanupFunction) {
|
function registerModuleCleanup(moduleName, cleanupFunction) {
|
||||||
@@ -228,26 +228,26 @@ function registerModuleCleanup(moduleName, cleanupFunction) {
|
|||||||
function removeSkeletonStyles() {
|
function removeSkeletonStyles() {
|
||||||
try {
|
try {
|
||||||
document.querySelectorAll('.xiaobaix-skel').forEach(el => {
|
document.querySelectorAll('.xiaobaix-skel').forEach(el => {
|
||||||
try { el.remove(); } catch (e) {}
|
try { el.remove(); } catch (e) { }
|
||||||
});
|
});
|
||||||
document.getElementById('xiaobaix-skeleton-style')?.remove();
|
document.getElementById('xiaobaix-skeleton-style')?.remove();
|
||||||
} catch (e) {}
|
} catch (e) { }
|
||||||
}
|
}
|
||||||
|
|
||||||
function cleanupAllResources() {
|
function cleanupAllResources() {
|
||||||
try {
|
try {
|
||||||
EventCenter.cleanupAll();
|
EventCenter.cleanupAll();
|
||||||
} catch (e) {}
|
} catch (e) { }
|
||||||
try { window.xbDebugPanelClose?.(); } catch (e) {}
|
try { window.xbDebugPanelClose?.(); } catch (e) { }
|
||||||
moduleCleanupFunctions.forEach((cleanupFn) => {
|
moduleCleanupFunctions.forEach((cleanupFn) => {
|
||||||
try {
|
try {
|
||||||
cleanupFn();
|
cleanupFn();
|
||||||
} catch (e) {}
|
} catch (e) { }
|
||||||
});
|
});
|
||||||
moduleCleanupFunctions.clear();
|
moduleCleanupFunctions.clear();
|
||||||
try {
|
try {
|
||||||
cleanupRenderer();
|
cleanupRenderer();
|
||||||
} catch (e) {}
|
} catch (e) { }
|
||||||
document.querySelectorAll('.memory-button, .mes_history_preview').forEach(btn => btn.remove());
|
document.querySelectorAll('.memory-button, .mes_history_preview').forEach(btn => btn.remove());
|
||||||
document.querySelectorAll('#message_preview_btn').forEach(btn => {
|
document.querySelectorAll('#message_preview_btn').forEach(btn => {
|
||||||
if (btn instanceof HTMLElement) {
|
if (btn instanceof HTMLElement) {
|
||||||
@@ -295,11 +295,11 @@ function toggleSettingsControls(enabled) {
|
|||||||
async function toggleAllFeatures(enabled) {
|
async function toggleAllFeatures(enabled) {
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
toggleSettingsControls(true);
|
toggleSettingsControls(true);
|
||||||
try { window.XB_applyPrevStates && window.XB_applyPrevStates(); } catch (e) {}
|
try { window.XB_applyPrevStates && window.XB_applyPrevStates(); } catch (e) { }
|
||||||
saveSettingsDebounced();
|
saveSettingsDebounced();
|
||||||
initRenderer();
|
initRenderer();
|
||||||
try { initVarCommands(); } catch (e) {}
|
try { initVarCommands(); } catch (e) { }
|
||||||
try { initVareventEditor(); } catch (e) {}
|
try { initVareventEditor(); } catch (e) { }
|
||||||
if (extension_settings[EXT_ID].tasks?.enabled) {
|
if (extension_settings[EXT_ID].tasks?.enabled) {
|
||||||
await initTasks();
|
await initTasks();
|
||||||
}
|
}
|
||||||
@@ -327,29 +327,29 @@ async function toggleAllFeatures(enabled) {
|
|||||||
try {
|
try {
|
||||||
if (isXiaobaixEnabled && settings.wrapperIframe && !document.getElementById('xb-callgen'))
|
if (isXiaobaixEnabled && settings.wrapperIframe && !document.getElementById('xb-callgen'))
|
||||||
document.head.appendChild(Object.assign(document.createElement('script'), { id: 'xb-callgen', type: 'module', src: `${extensionFolderPath}/bridges/call-generate-service.js` }));
|
document.head.appendChild(Object.assign(document.createElement('script'), { id: 'xb-callgen', type: 'module', src: `${extensionFolderPath}/bridges/call-generate-service.js` }));
|
||||||
} catch (e) {}
|
} catch (e) { }
|
||||||
try {
|
try {
|
||||||
if (isXiaobaixEnabled && !document.getElementById('xb-worldbook'))
|
if (isXiaobaixEnabled && !document.getElementById('xb-worldbook'))
|
||||||
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) { }
|
||||||
document.dispatchEvent(new CustomEvent('xiaobaixEnabledChanged', { detail: { enabled: true } }));
|
document.dispatchEvent(new CustomEvent('xiaobaixEnabledChanged', { detail: { enabled: true } }));
|
||||||
$(document).trigger('xiaobaix:enabled:toggle', [true]);
|
$(document).trigger('xiaobaix:enabled:toggle', [true]);
|
||||||
} else {
|
} else {
|
||||||
try { window.XB_captureAndStoreStates && window.XB_captureAndStoreStates(); } catch (e) {}
|
try { window.XB_captureAndStoreStates && window.XB_captureAndStoreStates(); } catch (e) { }
|
||||||
cleanupAllResources();
|
cleanupAllResources();
|
||||||
if (window.messagePreviewCleanup) try { window.messagePreviewCleanup(); } catch (e) {}
|
if (window.messagePreviewCleanup) try { window.messagePreviewCleanup(); } catch (e) { }
|
||||||
if (window.fourthWallCleanup) try { window.fourthWallCleanup(); } catch (e) {}
|
if (window.fourthWallCleanup) try { window.fourthWallCleanup(); } catch (e) { }
|
||||||
if (window.buttonCollapseCleanup) try { window.buttonCollapseCleanup(); } catch (e) {}
|
if (window.buttonCollapseCleanup) try { window.buttonCollapseCleanup(); } catch (e) { }
|
||||||
try { cleanupVariablesPanel(); } catch (e) {}
|
try { cleanupVariablesPanel(); } catch (e) { }
|
||||||
try { cleanupVariablesCore(); } catch (e) {}
|
try { cleanupVariablesCore(); } catch (e) { }
|
||||||
try { cleanupVarCommands(); } catch (e) {}
|
try { cleanupVarCommands(); } catch (e) { }
|
||||||
try { cleanupVareventEditor(); } catch (e) {}
|
try { cleanupVareventEditor(); } catch (e) { }
|
||||||
try { cleanupNovelDraw(); } catch (e) {}
|
try { cleanupNovelDraw(); } catch (e) { }
|
||||||
try { cleanupTts(); } catch (e) {}
|
try { cleanupTts(); } catch (e) { }
|
||||||
try { clearBlobCaches(); } catch (e) {}
|
try { clearBlobCaches(); } catch (e) { }
|
||||||
toggleSettingsControls(false);
|
toggleSettingsControls(false);
|
||||||
try { window.cleanupWorldbookHostBridge && window.cleanupWorldbookHostBridge(); document.getElementById('xb-worldbook')?.remove(); } catch (e) {}
|
try { window.cleanupWorldbookHostBridge && window.cleanupWorldbookHostBridge(); document.getElementById('xb-worldbook')?.remove(); } catch (e) { }
|
||||||
try { window.cleanupCallGenerateHostBridge && window.cleanupCallGenerateHostBridge(); document.getElementById('xb-callgen')?.remove(); } catch (e) {}
|
try { window.cleanupCallGenerateHostBridge && window.cleanupCallGenerateHostBridge(); document.getElementById('xb-callgen')?.remove(); } catch (e) { }
|
||||||
document.dispatchEvent(new CustomEvent('xiaobaixEnabledChanged', { detail: { enabled: false } }));
|
document.dispatchEvent(new CustomEvent('xiaobaixEnabledChanged', { detail: { enabled: false } }));
|
||||||
$(document).trigger('xiaobaix:enabled:toggle', [false]);
|
$(document).trigger('xiaobaix:enabled:toggle', [false]);
|
||||||
}
|
}
|
||||||
@@ -398,13 +398,13 @@ async function setupSettings() {
|
|||||||
if (!isXiaobaixEnabled) return;
|
if (!isXiaobaixEnabled) return;
|
||||||
const enabled = $(this).prop('checked');
|
const enabled = $(this).prop('checked');
|
||||||
if (!enabled && key === 'fourthWall') {
|
if (!enabled && key === 'fourthWall') {
|
||||||
try { fourthWallCleanup(); } catch (e) {}
|
try { fourthWallCleanup(); } catch (e) { }
|
||||||
}
|
}
|
||||||
if (!enabled && key === 'novelDraw') {
|
if (!enabled && key === 'novelDraw') {
|
||||||
try { cleanupNovelDraw(); } catch (e) {}
|
try { cleanupNovelDraw(); } catch (e) { }
|
||||||
}
|
}
|
||||||
if (!enabled && key === 'tts') {
|
if (!enabled && key === 'tts') {
|
||||||
try { cleanupTts(); } catch (e) {}
|
try { cleanupTts(); } catch (e) { }
|
||||||
}
|
}
|
||||||
settings[key] = extension_settings[EXT_ID][key] || {};
|
settings[key] = extension_settings[EXT_ID][key] || {};
|
||||||
settings[key].enabled = enabled;
|
settings[key].enabled = enabled;
|
||||||
@@ -463,7 +463,7 @@ async function setupSettings() {
|
|||||||
settings.wrapperIframe
|
settings.wrapperIframe
|
||||||
? (!document.getElementById('xb-callgen') && document.head.appendChild(Object.assign(document.createElement('script'), { id: 'xb-callgen', type: 'module', src: `${extensionFolderPath}/bridges/call-generate-service.js` })))
|
? (!document.getElementById('xb-callgen') && document.head.appendChild(Object.assign(document.createElement('script'), { id: 'xb-callgen', type: 'module', src: `${extensionFolderPath}/bridges/call-generate-service.js` })))
|
||||||
: (window.cleanupCallGenerateHostBridge && window.cleanupCallGenerateHostBridge(), document.getElementById('xb-callgen')?.remove());
|
: (window.cleanupCallGenerateHostBridge && window.cleanupCallGenerateHostBridge(), document.getElementById('xb-callgen')?.remove());
|
||||||
} catch (e) {}
|
} catch (e) { }
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#xiaobaix_render_enabled").prop("checked", settings.renderEnabled !== false).on("change", async function () {
|
$("#xiaobaix_render_enabled").prop("checked", settings.renderEnabled !== false).on("change", async function () {
|
||||||
@@ -474,7 +474,7 @@ async function setupSettings() {
|
|||||||
if (!settings.renderEnabled && wasEnabled) {
|
if (!settings.renderEnabled && wasEnabled) {
|
||||||
cleanupRenderer();
|
cleanupRenderer();
|
||||||
} else if (settings.renderEnabled && !wasEnabled) {
|
} else if (settings.renderEnabled && !wasEnabled) {
|
||||||
initRenderer();
|
initRenderer();
|
||||||
setTimeout(() => processExistingMessages(), 100);
|
setTimeout(() => processExistingMessages(), 100);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -494,7 +494,7 @@ async function setupSettings() {
|
|||||||
$(this).val(v);
|
$(this).val(v);
|
||||||
settings.maxRenderedMessages = v;
|
settings.maxRenderedMessages = v;
|
||||||
saveSettingsDebounced();
|
saveSettingsDebounced();
|
||||||
try { shrinkRenderedWindowFull(); } catch (e) {}
|
try { shrinkRenderedWindowFull(); } catch (e) { }
|
||||||
});
|
});
|
||||||
|
|
||||||
$(document).off('click.xbreset', '#xiaobaix_reset_btn').on('click.xbreset', '#xiaobaix_reset_btn', function (e) {
|
$(document).off('click.xbreset', '#xiaobaix_reset_btn').on('click.xbreset', '#xiaobaix_reset_btn', function (e) {
|
||||||
@@ -519,16 +519,16 @@ async function setupSettings() {
|
|||||||
const el = document.getElementById(id);
|
const el = document.getElementById(id);
|
||||||
if (el) {
|
if (el) {
|
||||||
el.checked = !!val;
|
el.checked = !!val;
|
||||||
try { $(el).trigger('change'); } catch {}
|
try { $(el).trigger('change'); } catch { }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ON.forEach(k => setChecked(MAP[k], true));
|
ON.forEach(k => setChecked(MAP[k], true));
|
||||||
OFF.forEach(k => setChecked(MAP[k], false));
|
OFF.forEach(k => setChecked(MAP[k], false));
|
||||||
setChecked('xiaobaix_use_blob', false);
|
setChecked('xiaobaix_use_blob', false);
|
||||||
setChecked('Wrapperiframe', true);
|
setChecked('Wrapperiframe', true);
|
||||||
try { saveSettingsDebounced(); } catch (e) {}
|
try { saveSettingsDebounced(); } catch (e) { }
|
||||||
});
|
});
|
||||||
} catch (err) {}
|
} catch (err) { }
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupDebugButtonInSettings() {
|
function setupDebugButtonInSettings() {
|
||||||
@@ -555,7 +555,7 @@ function setupDebugButtonInSettings() {
|
|||||||
try {
|
try {
|
||||||
const mod = await import('./modules/debug-panel/debug-panel.js');
|
const mod = await import('./modules/debug-panel/debug-panel.js');
|
||||||
if (mod?.toggleDebugPanel) await mod.toggleDebugPanel();
|
if (mod?.toggleDebugPanel) await mod.toggleDebugPanel();
|
||||||
} catch (e) {}
|
} catch (e) { }
|
||||||
};
|
};
|
||||||
btn.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); onActivate(); });
|
btn.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); onActivate(); });
|
||||||
btn.addEventListener('keydown', (e) => {
|
btn.addEventListener('keydown', (e) => {
|
||||||
@@ -563,7 +563,7 @@ function setupDebugButtonInSettings() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
row.appendChild(btn);
|
row.appendChild(btn);
|
||||||
} catch (e) {}
|
} catch (e) { }
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupMenuTabs() {
|
function setupMenuTabs() {
|
||||||
@@ -608,7 +608,7 @@ jQuery(async () => {
|
|||||||
|
|
||||||
await setupSettings();
|
await setupSettings();
|
||||||
|
|
||||||
try { initControlAudio(); } catch (e) {}
|
try { initControlAudio(); } catch (e) { }
|
||||||
|
|
||||||
if (isXiaobaixEnabled) {
|
if (isXiaobaixEnabled) {
|
||||||
initRenderer();
|
initRenderer();
|
||||||
@@ -617,20 +617,25 @@ jQuery(async () => {
|
|||||||
try {
|
try {
|
||||||
if (isXiaobaixEnabled && settings.wrapperIframe && !document.getElementById('xb-callgen'))
|
if (isXiaobaixEnabled && settings.wrapperIframe && !document.getElementById('xb-callgen'))
|
||||||
document.head.appendChild(Object.assign(document.createElement('script'), { id: 'xb-callgen', type: 'module', src: `${extensionFolderPath}/bridges/call-generate-service.js` }));
|
document.head.appendChild(Object.assign(document.createElement('script'), { id: 'xb-callgen', type: 'module', src: `${extensionFolderPath}/bridges/call-generate-service.js` }));
|
||||||
} catch (e) {}
|
} catch (e) { }
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (isXiaobaixEnabled && !document.getElementById('xb-worldbook'))
|
if (isXiaobaixEnabled && !document.getElementById('xb-worldbook'))
|
||||||
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);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isXiaobaixEnabled) {
|
if (isXiaobaixEnabled) {
|
||||||
try { initVarCommands(); } catch (e) {}
|
try { initVarCommands(); } catch (e) { }
|
||||||
try { initVareventEditor(); } catch (e) {}
|
try { initVareventEditor(); } catch (e) { }
|
||||||
|
|
||||||
if (settings.tasks?.enabled) {
|
if (settings.tasks?.enabled) {
|
||||||
try { await initTasks(); } catch (e) { console.error('[Tasks] Init failed:', e); }
|
try { await initTasks(); } catch (e) { console.error('[Tasks] Init failed:', e); }
|
||||||
@@ -665,7 +670,7 @@ jQuery(async () => {
|
|||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
if (isXiaobaixEnabled) processExistingMessages();
|
if (isXiaobaixEnabled) processExistingMessages();
|
||||||
}, 30000);
|
}, 30000);
|
||||||
} catch (err) {}
|
} catch (err) { }
|
||||||
});
|
});
|
||||||
|
|
||||||
export { executeSlashCommand };
|
export { executeSlashCommand };
|
||||||
@@ -1028,6 +1028,70 @@ function buildFramePayload(store) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseRelationTargetFromPredicate(predicate) {
|
||||||
|
const text = String(predicate || "").trim();
|
||||||
|
if (!text.startsWith("对")) return null;
|
||||||
|
const idx = text.indexOf("的", 1);
|
||||||
|
if (idx <= 1) return null;
|
||||||
|
return text.slice(1, idx).trim() || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isRelationFactLike(fact) {
|
||||||
|
if (!fact || fact.retracted) return false;
|
||||||
|
return !!parseRelationTargetFromPredicate(fact.p);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNextFactIdValue(facts) {
|
||||||
|
let max = 0;
|
||||||
|
for (const fact of facts || []) {
|
||||||
|
const match = String(fact?.id || "").match(/^f-(\d+)$/);
|
||||||
|
if (match) max = Math.max(max, Number.parseInt(match[1], 10) || 0);
|
||||||
|
}
|
||||||
|
return max + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeCharacterRelationshipsIntoFacts(existingFacts, relationships, floorHint = 0) {
|
||||||
|
const safeFacts = Array.isArray(existingFacts) ? existingFacts : [];
|
||||||
|
const safeRels = Array.isArray(relationships) ? relationships : [];
|
||||||
|
|
||||||
|
const nonRelationFacts = safeFacts.filter((f) => !isRelationFactLike(f));
|
||||||
|
const oldRelationByKey = new Map();
|
||||||
|
|
||||||
|
for (const fact of safeFacts) {
|
||||||
|
const to = parseRelationTargetFromPredicate(fact?.p);
|
||||||
|
const from = String(fact?.s || "").trim();
|
||||||
|
if (!from || !to) continue;
|
||||||
|
oldRelationByKey.set(`${from}->${to}`, fact);
|
||||||
|
}
|
||||||
|
|
||||||
|
let nextFactId = getNextFactIdValue(safeFacts);
|
||||||
|
const newRelationFacts = [];
|
||||||
|
|
||||||
|
for (const rel of safeRels) {
|
||||||
|
const from = String(rel?.from || "").trim();
|
||||||
|
const to = String(rel?.to || "").trim();
|
||||||
|
if (!from || !to) continue;
|
||||||
|
|
||||||
|
const key = `${from}->${to}`;
|
||||||
|
const oldFact = oldRelationByKey.get(key);
|
||||||
|
const label = String(rel?.label || "").trim() || "未知";
|
||||||
|
const trend = String(rel?.trend || "").trim() || "陌生";
|
||||||
|
const id = oldFact?.id || `f-${nextFactId++}`;
|
||||||
|
|
||||||
|
newRelationFacts.push({
|
||||||
|
id,
|
||||||
|
s: from,
|
||||||
|
p: oldFact?.p || `对${to}的关系`,
|
||||||
|
o: label,
|
||||||
|
trend,
|
||||||
|
since: oldFact?.since ?? floorHint,
|
||||||
|
_addedAt: oldFact?._addedAt ?? floorHint,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...nonRelationFacts, ...newRelationFacts];
|
||||||
|
}
|
||||||
|
|
||||||
function openPanelForMessage(mesId) {
|
function openPanelForMessage(mesId) {
|
||||||
createOverlay();
|
createOverlay();
|
||||||
showOverlay();
|
showOverlay();
|
||||||
@@ -1368,6 +1432,11 @@ async function handleFrameMessage(event) {
|
|||||||
if (VALID_SECTIONS.includes(data.section)) {
|
if (VALID_SECTIONS.includes(data.section)) {
|
||||||
store.json[data.section] = data.data;
|
store.json[data.section] = data.data;
|
||||||
}
|
}
|
||||||
|
if (data.section === "characters") {
|
||||||
|
const rels = data?.data?.relationships || [];
|
||||||
|
const floorHint = Math.max(0, Number(store.lastSummarizedMesId) || 0);
|
||||||
|
store.json.facts = mergeCharacterRelationshipsIntoFacts(store.json.facts, rels, floorHint);
|
||||||
|
}
|
||||||
store.updatedAt = Date.now();
|
store.updatedAt = Date.now();
|
||||||
saveSummaryStore();
|
saveSummaryStore();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user