feat: updates to test branch
This commit is contained in:
@@ -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;
|
||||||
}
|
}
|
||||||
@@ -416,7 +416,7 @@ class CallGenerateService {
|
|||||||
} finally {
|
} finally {
|
||||||
for (const key of Object.keys(oai_settings)) {
|
for (const key of Object.keys(oai_settings)) {
|
||||||
if (!Object.prototype.hasOwnProperty.call(snapshot, key)) {
|
if (!Object.prototype.hasOwnProperty.call(snapshot, key)) {
|
||||||
try { delete oai_settings[key]; } catch {}
|
try { delete oai_settings[key]; } catch { }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Object.assign(oai_settings, snapshot);
|
Object.assign(oai_settings, snapshot);
|
||||||
@@ -503,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 {
|
||||||
@@ -521,7 +521,7 @@ class CallGenerateService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch { }
|
||||||
|
|
||||||
// 3) 扫描 Prompt 集合(运行期合并后的集合)
|
// 3) 扫描 Prompt 集合(运行期合并后的集合)
|
||||||
try {
|
try {
|
||||||
@@ -538,7 +538,7 @@ class CallGenerateService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch { }
|
||||||
|
|
||||||
// 4) 失败时尝试 sanitize 名称与 identifier 的弱匹配
|
// 4) 失败时尝试 sanitize 名称与 identifier 的弱匹配
|
||||||
if (matches.size === 0) {
|
if (matches.size === 0) {
|
||||||
@@ -555,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) {
|
||||||
@@ -823,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;
|
||||||
@@ -1055,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 时生效;否则忽略
|
||||||
@@ -1229,132 +1229,132 @@ class CallGenerateService {
|
|||||||
// 步骤 2~9 的核心逻辑,可能包裹在 _withTemporaryPreset 中执行
|
// 步骤 2~9 的核心逻辑,可能包裹在 _withTemporaryPreset 中执行
|
||||||
const executeCore = async () => {
|
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
|
||||||
let orderedRefs = [];
|
let orderedRefs = [];
|
||||||
let inlineMapped = [];
|
let inlineMapped = [];
|
||||||
let listLevelOverrides = {};
|
let listLevelOverrides = {};
|
||||||
const unorderedKeys = new Set();
|
const unorderedKeys = new Set();
|
||||||
if (list && list.length) {
|
if (list && list.length) {
|
||||||
const { references, inlineInjections, listOverrides } = this._parseUnifiedList(list);
|
const { references, inlineInjections, listOverrides } = this._parseUnifiedList(list);
|
||||||
listLevelOverrides = listOverrides || {};
|
listLevelOverrides = listOverrides || {};
|
||||||
const parsedRefs = references.map(t => this._parseComponentRefToken(t));
|
const parsedRefs = references.map(t => this._parseComponentRefToken(t));
|
||||||
const containsAll = parsedRefs.includes('ALL');
|
const containsAll = parsedRefs.includes('ALL');
|
||||||
const containsAllPreOn = parsedRefs.includes('ALL_PREON');
|
const containsAllPreOn = parsedRefs.includes('ALL_PREON');
|
||||||
if (containsAll) {
|
if (containsAll) {
|
||||||
baseStrategy = 'ALL';
|
baseStrategy = 'ALL';
|
||||||
// ALL 仅作为开关标识,子集重排目标为去除 ALL 后的引用列表
|
// ALL 仅作为开关标识,子集重排目标为去除 ALL 后的引用列表
|
||||||
orderedRefs = parsedRefs.filter(x => x && x !== 'ALL' && x !== 'ALL_PREON');
|
orderedRefs = parsedRefs.filter(x => x && x !== 'ALL' && x !== 'ALL_PREON');
|
||||||
} else if (containsAllPreOn) {
|
} else if (containsAllPreOn) {
|
||||||
baseStrategy = 'ALL_PREON';
|
baseStrategy = 'ALL_PREON';
|
||||||
// ALL_PREON:仅启用“预设里已开启”的组件,子集重排目标为去除该标记后的引用列表
|
// ALL_PREON:仅启用“预设里已开启”的组件,子集重排目标为去除该标记后的引用列表
|
||||||
orderedRefs = parsedRefs.filter(x => x && x !== 'ALL' && x !== 'ALL_PREON');
|
orderedRefs = parsedRefs.filter(x => x && x !== 'ALL' && x !== 'ALL_PREON');
|
||||||
} else {
|
} else {
|
||||||
baseStrategy = 'SUBSET';
|
baseStrategy = 'SUBSET';
|
||||||
orderedRefs = parsedRefs.filter(Boolean);
|
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 {
|
|
||||||
// 不将 userInput 作为 quietText 干跑,以免把其注入到历史里
|
|
||||||
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 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4) 依据策略计算启用集合与顺序
|
|
||||||
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);
|
|
||||||
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 });
|
|
||||||
|
|
||||||
// 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 {}
|
inlineMapped = this._mapInlineInjectionsUnified(list, inlineInjections);
|
||||||
const messages = working.map(m => {
|
// 放宽:ALL 可出现在任意位置,作为“启用全部”的标志
|
||||||
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) 发送
|
// 解析 order=false:不参与重排
|
||||||
return await this._sendMessages(working, { ...options, debug: { ...(options?.debug || {}), _exported: true } }, requestId, sourceWindow, targetOrigin);
|
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 {
|
||||||
|
// 不将 userInput 作为 quietText 干跑,以免把其注入到历史里
|
||||||
|
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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4) 依据策略计算启用集合与顺序
|
||||||
|
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);
|
||||||
|
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 });
|
||||||
|
|
||||||
|
// 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) 发送
|
||||||
|
return await this._sendMessages(working, { ...options, debug: { ...(options?.debug || {}), _exported: true } }, requestId, sourceWindow, targetOrigin);
|
||||||
|
|
||||||
}; // end executeCore
|
}; // end executeCore
|
||||||
|
|
||||||
@@ -1433,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 { }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1451,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;
|
||||||
}
|
}
|
||||||
@@ -1463,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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1486,7 +1486,7 @@ 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 || {};
|
||||||
@@ -1528,42 +1528,42 @@ export function initCallGenerateHostBridge() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
try { xbLog.error('callGenerateBridge', '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 调用方式完全一致 =====
|
||||||
// 创建命名空间
|
// 创建命名空间
|
||||||
@@ -1593,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;
|
||||||
@@ -1653,7 +1653,7 @@ if (typeof window !== 'undefined') {
|
|||||||
* });
|
* });
|
||||||
* // messages = [{ role: 'system', content: '...' }, ...]
|
* // messages = [{ role: 'system', content: '...' }, ...]
|
||||||
*/
|
*/
|
||||||
window.LittleWhiteBox.assemblePrompt = async function(options) {
|
window.LittleWhiteBox.assemblePrompt = async function (options) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const requestId = `assemble-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
const requestId = `assemble-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
||||||
|
|
||||||
@@ -1686,22 +1686,22 @@ if (typeof window !== 'undefined') {
|
|||||||
* 取消指定会话
|
* 取消指定会话
|
||||||
* @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() {
|
window.LittleWhiteBox.listChatCompletionPresets = function () {
|
||||||
return Object.keys(openai_setting_names || {});
|
return Object.keys(openai_setting_names || {});
|
||||||
};
|
};
|
||||||
|
|
||||||
window.LittleWhiteBox.getSelectedPresetName = function() {
|
window.LittleWhiteBox.getSelectedPresetName = function () {
|
||||||
return oai_settings?.preset_settings_openai || '';
|
return oai_settings?.preset_settings_openai || '';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -29,18 +29,18 @@ class ContextBridgeService {
|
|||||||
|
|
||||||
init() {
|
init() {
|
||||||
if (this._attached) return;
|
if (this._attached) return;
|
||||||
try { xbLog.info('contextBridge', 'init'); } catch {}
|
try { xbLog.info('contextBridge', 'init'); } catch { }
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this._previousChatId = getCurrentChatId();
|
this._previousChatId = getCurrentChatId();
|
||||||
} catch {}
|
} catch { }
|
||||||
|
|
||||||
const self = this;
|
const self = this;
|
||||||
this._listener = function (event) {
|
this._listener = function (event) {
|
||||||
try {
|
try {
|
||||||
self._handleMessage(event);
|
self._handleMessage(event);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
try { xbLog.error('contextBridge', 'message handler error', e); } catch {}
|
try { xbLog.error('contextBridge', 'message handler error', e); } catch { }
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -52,8 +52,8 @@ class ContextBridgeService {
|
|||||||
|
|
||||||
cleanup() {
|
cleanup() {
|
||||||
if (!this._attached) return;
|
if (!this._attached) return;
|
||||||
try { xbLog.info('contextBridge', 'cleanup'); } catch {}
|
try { xbLog.info('contextBridge', 'cleanup'); } catch { }
|
||||||
try { window.removeEventListener('message', this._listener); } catch {}
|
try { window.removeEventListener('message', this._listener); } catch { }
|
||||||
this._listener = null;
|
this._listener = null;
|
||||||
this._events.cleanup();
|
this._events.cleanup();
|
||||||
this._pendingEvents.length = 0;
|
this._pendingEvents.length = 0;
|
||||||
@@ -81,14 +81,14 @@ class ContextBridgeService {
|
|||||||
while (this._pendingEvents.length > 0) {
|
while (this._pendingEvents.length > 0) {
|
||||||
const pending = this._pendingEvents.shift();
|
const pending = this._pendingEvents.shift();
|
||||||
// eslint-disable-next-line no-restricted-syntax -- delivering queued events to newly ready iframe
|
// eslint-disable-next-line no-restricted-syntax -- delivering queued events to newly ready iframe
|
||||||
try { event.source?.postMessage(pending, '*'); } catch {}
|
try { event.source?.postMessage(pending, '*'); } catch { }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 推送上下文快照
|
// 推送上下文快照
|
||||||
const snapshot = this._buildContextSnapshot(msgIndex);
|
const snapshot = this._buildContextSnapshot(msgIndex);
|
||||||
// eslint-disable-next-line no-restricted-syntax -- sending context snapshot to requesting iframe
|
// eslint-disable-next-line no-restricted-syntax -- sending context snapshot to requesting iframe
|
||||||
try { event.source?.postMessage(snapshot, '*'); } catch {}
|
try { event.source?.postMessage(snapshot, '*'); } catch { }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -102,7 +102,7 @@ class ContextBridgeService {
|
|||||||
for (const iframe of iframes) {
|
for (const iframe of iframes) {
|
||||||
try {
|
try {
|
||||||
if (iframe.contentWindow === source) return iframe;
|
if (iframe.contentWindow === source) return iframe;
|
||||||
} catch {}
|
} catch { }
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -160,7 +160,7 @@ class ContextBridgeService {
|
|||||||
const message = { type: 'st-event', source: SOURCE_TAG, event: eventName, payload };
|
const message = { type: 'st-event', source: SOURCE_TAG, event: eventName, payload };
|
||||||
for (const iframe of iframes) {
|
for (const iframe of iframes) {
|
||||||
// eslint-disable-next-line no-restricted-syntax -- broadcasting event to template iframes
|
// eslint-disable-next-line no-restricted-syntax -- broadcasting event to template iframes
|
||||||
try { iframe.contentWindow?.postMessage(message, '*'); } catch {}
|
try { iframe.contentWindow?.postMessage(message, '*'); } catch { }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -271,23 +271,23 @@ if (typeof window !== 'undefined') {
|
|||||||
window.LittleWhiteBox = window.LittleWhiteBox || {};
|
window.LittleWhiteBox = window.LittleWhiteBox || {};
|
||||||
window.LittleWhiteBox.contextBridge = contextBridgeService;
|
window.LittleWhiteBox.contextBridge = contextBridgeService;
|
||||||
|
|
||||||
try { initContextBridge(); } catch (e) {}
|
try { initContextBridge(); } 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) initContextBridge(); else cleanupContextBridge();
|
if (enabled) initContextBridge(); else cleanupContextBridge();
|
||||||
} 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) initContextBridge(); else cleanupContextBridge();
|
if (enabled) initContextBridge(); else cleanupContextBridge();
|
||||||
} catch {}
|
} catch { }
|
||||||
});
|
});
|
||||||
window.addEventListener('beforeunload', () => {
|
window.addEventListener('beforeunload', () => {
|
||||||
try { cleanupContextBridge(); } catch {}
|
try { cleanupContextBridge(); } catch { }
|
||||||
});
|
});
|
||||||
} catch {}
|
} catch { }
|
||||||
}
|
}
|
||||||
@@ -182,5 +182,6 @@ export const TasksStorage = new StorageFile('LittleWhiteBox_Tasks.json');
|
|||||||
export const StoryOutlineStorage = new StorageFile('LittleWhiteBox_StoryOutline.json');
|
export const StoryOutlineStorage = new StorageFile('LittleWhiteBox_StoryOutline.json');
|
||||||
export const NovelDrawStorage = new StorageFile('LittleWhiteBox_NovelDraw.json', { debounceMs: 800 });
|
export const NovelDrawStorage = new StorageFile('LittleWhiteBox_NovelDraw.json', { debounceMs: 800 });
|
||||||
export const TtsStorage = new StorageFile('LittleWhiteBox_TTS.json', { debounceMs: 800 });
|
export const TtsStorage = new StorageFile('LittleWhiteBox_TTS.json', { debounceMs: 800 });
|
||||||
|
export const EnaPlannerStorage = new StorageFile('LittleWhiteBox_EnaPlanner.json', { debounceMs: 800 });
|
||||||
export const CommonSettingStorage = new StorageFile('LittleWhiteBox_CommonSettings.json', { debounceMs: 1000 });
|
export const CommonSettingStorage = new StorageFile('LittleWhiteBox_CommonSettings.json', { debounceMs: 1000 });
|
||||||
export const VectorStorage = new StorageFile('LittleWhiteBox_Vectors.json', { debounceMs: 3000 });
|
export const VectorStorage = new StorageFile('LittleWhiteBox_Vectors.json', { debounceMs: 3000 });
|
||||||
|
|||||||
120
index.js
120
index.js
@@ -27,7 +27,7 @@ import { initNovelDraw, cleanupNovelDraw } from "./modules/novel-draw/novel-draw
|
|||||||
import "./modules/story-summary/story-summary.js";
|
import "./modules/story-summary/story-summary.js";
|
||||||
import "./modules/story-outline/story-outline.js";
|
import "./modules/story-outline/story-outline.js";
|
||||||
import { initTts, cleanupTts } from "./modules/tts/tts.js";
|
import { initTts, cleanupTts } from "./modules/tts/tts.js";
|
||||||
import { initEnaPlanner } from "./modules/ena-planner/ena-planner.js";
|
import { initEnaPlanner, cleanupEnaPlanner } from "./modules/ena-planner/ena-planner.js";
|
||||||
|
|
||||||
extension_settings[EXT_ID] = extension_settings[EXT_ID] || {
|
extension_settings[EXT_ID] = extension_settings[EXT_ID] || {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
@@ -45,6 +45,7 @@ extension_settings[EXT_ID] = extension_settings[EXT_ID] || {
|
|||||||
storyOutline: { enabled: false },
|
storyOutline: { enabled: false },
|
||||||
novelDraw: { enabled: false },
|
novelDraw: { enabled: false },
|
||||||
tts: { enabled: false },
|
tts: { enabled: false },
|
||||||
|
enaPlanner: { enabled: false },
|
||||||
useBlob: false,
|
useBlob: false,
|
||||||
wrapperIframe: true,
|
wrapperIframe: true,
|
||||||
renderEnabled: true,
|
renderEnabled: true,
|
||||||
@@ -195,13 +196,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() {
|
||||||
@@ -219,7 +220,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) {
|
||||||
@@ -229,26 +230,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) {
|
||||||
@@ -277,7 +278,8 @@ function toggleSettingsControls(enabled) {
|
|||||||
'xiaobaix_use_blob', 'xiaobaix_variables_core_enabled', 'xiaobaix_variables_mode', 'Wrapperiframe', 'xiaobaix_render_enabled',
|
'xiaobaix_use_blob', 'xiaobaix_variables_core_enabled', 'xiaobaix_variables_mode', 'Wrapperiframe', 'xiaobaix_render_enabled',
|
||||||
'xiaobaix_max_rendered', 'xiaobaix_story_outline_enabled', 'xiaobaix_story_summary_enabled',
|
'xiaobaix_max_rendered', 'xiaobaix_story_outline_enabled', 'xiaobaix_story_summary_enabled',
|
||||||
'xiaobaix_novel_draw_enabled', 'xiaobaix_novel_draw_open_settings',
|
'xiaobaix_novel_draw_enabled', 'xiaobaix_novel_draw_open_settings',
|
||||||
'xiaobaix_tts_enabled', 'xiaobaix_tts_open_settings'
|
'xiaobaix_tts_enabled', 'xiaobaix_tts_open_settings',
|
||||||
|
'xiaobaix_ena_planner_enabled', 'xiaobaix_ena_planner_open_settings'
|
||||||
];
|
];
|
||||||
controls.forEach(id => {
|
controls.forEach(id => {
|
||||||
$(`#${id}`).prop('disabled', !enabled).closest('.flex-container').toggleClass('disabled-control', !enabled);
|
$(`#${id}`).prop('disabled', !enabled).closest('.flex-container').toggleClass('disabled-control', !enabled);
|
||||||
@@ -296,11 +298,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();
|
||||||
}
|
}
|
||||||
@@ -312,6 +314,7 @@ async function toggleAllFeatures(enabled) {
|
|||||||
{ condition: extension_settings[EXT_ID].variablesCore?.enabled, init: initVariablesCore },
|
{ condition: extension_settings[EXT_ID].variablesCore?.enabled, init: initVariablesCore },
|
||||||
{ condition: extension_settings[EXT_ID].novelDraw?.enabled, init: initNovelDraw },
|
{ condition: extension_settings[EXT_ID].novelDraw?.enabled, init: initNovelDraw },
|
||||||
{ condition: extension_settings[EXT_ID].tts?.enabled, init: initTts },
|
{ condition: extension_settings[EXT_ID].tts?.enabled, init: initTts },
|
||||||
|
{ condition: extension_settings[EXT_ID].enaPlanner?.enabled, init: initEnaPlanner },
|
||||||
{ condition: true, init: initStreamingGeneration },
|
{ condition: true, init: initStreamingGeneration },
|
||||||
{ condition: true, init: initButtonCollapse }
|
{ condition: true, init: initButtonCollapse }
|
||||||
];
|
];
|
||||||
@@ -328,29 +331,30 @@ 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 { cleanupEnaPlanner(); } 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]);
|
||||||
}
|
}
|
||||||
@@ -391,7 +395,8 @@ async function setupSettings() {
|
|||||||
{ id: 'xiaobaix_story_summary_enabled', key: 'storySummary' },
|
{ id: 'xiaobaix_story_summary_enabled', key: 'storySummary' },
|
||||||
{ id: 'xiaobaix_story_outline_enabled', key: 'storyOutline' },
|
{ id: 'xiaobaix_story_outline_enabled', key: 'storyOutline' },
|
||||||
{ id: 'xiaobaix_novel_draw_enabled', key: 'novelDraw', init: initNovelDraw },
|
{ id: 'xiaobaix_novel_draw_enabled', key: 'novelDraw', init: initNovelDraw },
|
||||||
{ id: 'xiaobaix_tts_enabled', key: 'tts', init: initTts }
|
{ id: 'xiaobaix_tts_enabled', key: 'tts', init: initTts },
|
||||||
|
{ id: 'xiaobaix_ena_planner_enabled', key: 'enaPlanner', init: initEnaPlanner }
|
||||||
];
|
];
|
||||||
|
|
||||||
moduleConfigs.forEach(({ id, key, init }) => {
|
moduleConfigs.forEach(({ id, key, init }) => {
|
||||||
@@ -399,13 +404,16 @@ 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) { }
|
||||||
|
}
|
||||||
|
if (!enabled && key === 'enaPlanner') {
|
||||||
|
try { cleanupEnaPlanner(); } catch (e) { }
|
||||||
}
|
}
|
||||||
settings[key] = extension_settings[EXT_ID][key] || {};
|
settings[key] = extension_settings[EXT_ID][key] || {};
|
||||||
settings[key].enabled = enabled;
|
settings[key].enabled = enabled;
|
||||||
@@ -450,6 +458,15 @@ async function setupSettings() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$("#xiaobaix_ena_planner_open_settings").on("click", function () {
|
||||||
|
if (!isXiaobaixEnabled) return;
|
||||||
|
if (settings.enaPlanner?.enabled && window.xiaobaixEnaPlanner?.openSettings) {
|
||||||
|
window.xiaobaixEnaPlanner.openSettings();
|
||||||
|
} else {
|
||||||
|
toastr.warning('请先启用剧情规划模块');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
$("#xiaobaix_use_blob").prop("checked", !!settings.useBlob).on("change", async function () {
|
$("#xiaobaix_use_blob").prop("checked", !!settings.useBlob).on("change", async function () {
|
||||||
if (!isXiaobaixEnabled) return;
|
if (!isXiaobaixEnabled) return;
|
||||||
settings.useBlob = $(this).prop("checked");
|
settings.useBlob = $(this).prop("checked");
|
||||||
@@ -464,7 +481,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 () {
|
||||||
@@ -475,7 +492,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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -495,7 +512,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) {
|
||||||
@@ -512,24 +529,25 @@ async function setupSettings() {
|
|||||||
variablesPanel: 'xiaobaix_variables_panel_enabled',
|
variablesPanel: 'xiaobaix_variables_panel_enabled',
|
||||||
variablesCore: 'xiaobaix_variables_core_enabled',
|
variablesCore: 'xiaobaix_variables_core_enabled',
|
||||||
novelDraw: 'xiaobaix_novel_draw_enabled',
|
novelDraw: 'xiaobaix_novel_draw_enabled',
|
||||||
tts: 'xiaobaix_tts_enabled'
|
tts: 'xiaobaix_tts_enabled',
|
||||||
|
enaPlanner: 'xiaobaix_ena_planner_enabled'
|
||||||
};
|
};
|
||||||
const ON = ['templateEditor', 'tasks', 'variablesCore', 'audio', 'storySummary', 'recorded'];
|
const ON = ['templateEditor', 'tasks', 'variablesCore', 'audio', 'storySummary', 'recorded'];
|
||||||
const OFF = ['preview', 'immersive', 'variablesPanel', 'fourthWall', 'storyOutline', 'novelDraw', 'tts'];
|
const OFF = ['preview', 'immersive', 'variablesPanel', 'fourthWall', 'storyOutline', 'novelDraw', 'tts', 'enaPlanner'];
|
||||||
function setChecked(id, val) {
|
function setChecked(id, val) {
|
||||||
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() {
|
||||||
@@ -556,7 +574,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) => {
|
||||||
@@ -564,7 +582,7 @@ function setupDebugButtonInSettings() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
row.appendChild(btn);
|
row.appendChild(btn);
|
||||||
} catch (e) {}
|
} catch (e) { }
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupMenuTabs() {
|
function setupMenuTabs() {
|
||||||
@@ -609,7 +627,7 @@ jQuery(async () => {
|
|||||||
|
|
||||||
await setupSettings();
|
await setupSettings();
|
||||||
|
|
||||||
try { initControlAudio(); } catch (e) {}
|
try { initControlAudio(); } catch (e) { }
|
||||||
|
|
||||||
if (isXiaobaixEnabled) {
|
if (isXiaobaixEnabled) {
|
||||||
initRenderer();
|
initRenderer();
|
||||||
@@ -618,25 +636,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 {
|
try {
|
||||||
if (isXiaobaixEnabled && !document.getElementById('xb-contextbridge'))
|
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` }));
|
document.head.appendChild(Object.assign(document.createElement('script'), { id: 'xb-contextbridge', type: 'module', src: `${extensionFolderPath}/bridges/context-bridge.js` }));
|
||||||
} catch (e) {}
|
} 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); }
|
||||||
@@ -650,11 +668,11 @@ jQuery(async () => {
|
|||||||
{ condition: settings.variablesCore?.enabled, init: initVariablesCore },
|
{ condition: settings.variablesCore?.enabled, init: initVariablesCore },
|
||||||
{ condition: settings.novelDraw?.enabled, init: initNovelDraw },
|
{ condition: settings.novelDraw?.enabled, init: initNovelDraw },
|
||||||
{ condition: settings.tts?.enabled, init: initTts },
|
{ condition: settings.tts?.enabled, init: initTts },
|
||||||
|
{ condition: settings.enaPlanner?.enabled, init: initEnaPlanner },
|
||||||
{ condition: true, init: initStreamingGeneration },
|
{ condition: true, init: initStreamingGeneration },
|
||||||
{ condition: true, init: initButtonCollapse }
|
{ condition: true, init: initButtonCollapse }
|
||||||
];
|
];
|
||||||
moduleInits.forEach(({ condition, init }) => { if (condition) init(); });
|
moduleInits.forEach(({ condition, init }) => { if (condition) init(); });
|
||||||
try { initEnaPlanner(); } catch (e) { console.error('[EnaPlanner] Init failed:', e); }
|
|
||||||
|
|
||||||
if (settings.preview?.enabled || settings.recorded?.enabled) {
|
if (settings.preview?.enabled || settings.recorded?.enabled) {
|
||||||
setTimeout(initMessagePreview, 1500);
|
setTimeout(initMessagePreview, 1500);
|
||||||
@@ -672,7 +690,7 @@ jQuery(async () => {
|
|||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
if (isXiaobaixEnabled) processExistingMessages();
|
if (isXiaobaixEnabled) processExistingMessages();
|
||||||
}, 30000);
|
}, 30000);
|
||||||
} catch (err) {}
|
} catch (err) { }
|
||||||
});
|
});
|
||||||
|
|
||||||
export { executeSlashCommand };
|
export { executeSlashCommand };
|
||||||
|
|||||||
59
modules/ena-planner/ena-planner-presets.js
Normal file
59
modules/ena-planner/ena-planner-presets.js
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
export const DEFAULT_PROMPT_BLOCKS = [
|
||||||
|
{
|
||||||
|
id: 'ena-default-system-001',
|
||||||
|
role: 'system',
|
||||||
|
name: 'Ena Planner System',
|
||||||
|
content: `你是一位剧情规划师(Story Planner)。你的工作是在幕后为互动叙事提供方向指引,而不是直接扮演角色或撰写正文。
|
||||||
|
|
||||||
|
## 你会收到的信息
|
||||||
|
|
||||||
|
- 角色卡:当前角色的设定(描述、性格、场景)
|
||||||
|
- 世界书:世界观设定和规则
|
||||||
|
- 剧情摘要:此前发生过的重要事件
|
||||||
|
- 聊天历史:最近的 AI 回复片段
|
||||||
|
- 向量召回:与当前情境相关的记忆片段
|
||||||
|
- 历史规划:之前生成的 <plot> 块
|
||||||
|
- 玩家输入:玩家刚刚发出的指令或行动
|
||||||
|
|
||||||
|
## 你的任务
|
||||||
|
|
||||||
|
根据以上所有信息,为下一轮 AI 回复规划剧情走向。
|
||||||
|
|
||||||
|
## 输出格式(严格遵守)
|
||||||
|
|
||||||
|
只输出以下两个标签,不要输出任何其他内容:
|
||||||
|
|
||||||
|
<plot>
|
||||||
|
(剧情走向指引:接下来应该发生什么。包括场景推进、NPC 反应、事件触发、伏笔推进等。
|
||||||
|
写给 AI 看的导演指令,不是给玩家看的正文。简洁、具体、可执行。)
|
||||||
|
</plot>
|
||||||
|
|
||||||
|
<note>
|
||||||
|
(写作注意事项:这一轮回复应该怎么写。包括叙事节奏、情绪基调、应避免的问题、需要保持的连贯性等。
|
||||||
|
同样是给 AI 的元指令,不是正文。)
|
||||||
|
</note>
|
||||||
|
|
||||||
|
## 规划原则
|
||||||
|
|
||||||
|
1. 尊重玩家意图:玩家的输入是最高优先级,规划应围绕玩家的行动展开
|
||||||
|
2. 保持连贯:与历史 plot 和剧情摘要保持因果一致,不要凭空引入矛盾设定
|
||||||
|
3. 推进而非重复:每次规划应让故事向前推进,避免原地踏步
|
||||||
|
4. 留有空间:给出方向但不要过度规定细节,让主 AI 有创作余地
|
||||||
|
5. 遵守世界观:世界书中的规则和设定是硬约束,不可违反
|
||||||
|
|
||||||
|
如有思考过程,请放在 <thinking> 中(会被自动剔除)。`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ena-default-assistant-001',
|
||||||
|
role: 'assistant',
|
||||||
|
name: 'Assistant Seed',
|
||||||
|
content: `<think>
|
||||||
|
让我分析当前情境,梳理玩家意图、已有伏笔和世界观约束,然后规划下一步走向...
|
||||||
|
规划结果输出在<plot>...</plot>和<note>...</note>两个块中
|
||||||
|
</think>`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const BUILTIN_TEMPLATES = {
|
||||||
|
'默认模板': DEFAULT_PROMPT_BLOCKS,
|
||||||
|
};
|
||||||
File diff suppressed because it is too large
Load Diff
948
modules/ena-planner/ena-planner.html
Normal file
948
modules/ena-planner/ena-planner.html
Normal file
@@ -0,0 +1,948 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
|
<meta name="mobile-web-app-capable" content="yes">
|
||||||
|
<title>Ena Planner</title>
|
||||||
|
<link rel="stylesheet" href="./ena-planner.css">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
|
||||||
|
<header>
|
||||||
|
<div class="header-left">
|
||||||
|
<h1>Ena<span>Planner</span></h1>
|
||||||
|
<div class="subtitle">Story Planning · LLM Integration</div>
|
||||||
|
</div>
|
||||||
|
<div class="stats">
|
||||||
|
<div class="stat">
|
||||||
|
<div class="stat-val" id="ep_badge"><span class="hl">未启用</span></div>
|
||||||
|
<div class="stat-lbl">状态</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat">
|
||||||
|
<div class="stat-val" id="ep_save_status">就绪</div>
|
||||||
|
<div class="stat-lbl">保存</div>
|
||||||
|
</div>
|
||||||
|
<button class="modal-close" id="ep_close" title="关闭">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<line x1="18" y1="6" x2="6" y2="18" />
|
||||||
|
<line x1="6" y1="6" x2="18" y2="18" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Desktop tabs -->
|
||||||
|
<div class="nav-tabs">
|
||||||
|
<div class="nav-item active" data-view="quickstart">快速开始</div>
|
||||||
|
<div class="nav-item" data-view="api">API 配置</div>
|
||||||
|
<div class="nav-item" data-view="prompt">提示词</div>
|
||||||
|
<div class="nav-item" data-view="context">上下文</div>
|
||||||
|
<div class="nav-item" data-view="debug">调试</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<main class="app-main">
|
||||||
|
|
||||||
|
<!-- ── 快速开始 ── -->
|
||||||
|
<div id="view-quickstart" class="view active">
|
||||||
|
<div class="tip-box">
|
||||||
|
<div class="tip-icon">ℹ</div>
|
||||||
|
<div class="tip-text">
|
||||||
|
<strong>工作流程:</strong>点击发送 → 拦截 → 收集上下文(角色卡、世界书、摘要、历史 plot、最近 AI 回复)→ 发给规划 LLM → 提取 <plot> 和
|
||||||
|
<note> → 追加到你的输入 → 放行发送
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section class="card">
|
||||||
|
<div class="card-title">基本设置</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">启用规划器</label>
|
||||||
|
<select id="ep_enabled" class="input">
|
||||||
|
<option value="true">开启</option>
|
||||||
|
<option value="false">关闭</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">跳过已有规划的输入</label>
|
||||||
|
<select id="ep_skip_plot" class="input">
|
||||||
|
<option value="true">是</option>
|
||||||
|
<option value="false">否</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="form-hint">输入中已有 <plot> 标签时跳过自动规划。</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="card">
|
||||||
|
<div class="card-title">快速测试</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">测试输入(留空使用默认)</label>
|
||||||
|
<textarea id="ep_test_input" class="input" rows="3" placeholder="输入一段剧情描述,测试规划器输出..."></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="btn-group">
|
||||||
|
<button id="ep_run_test" class="btn btn-p">运行规划测试</button>
|
||||||
|
</div>
|
||||||
|
<div id="ep_test_status" class="status-text"></div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ── API 配置 ── -->
|
||||||
|
<div id="view-api" class="view">
|
||||||
|
<section class="card">
|
||||||
|
<div class="card-title">连接设置</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">渠道类型</label>
|
||||||
|
<select id="ep_api_channel" class="input">
|
||||||
|
<option value="openai">OpenAI 兼容</option>
|
||||||
|
<option value="gemini">Gemini 兼容</option>
|
||||||
|
<option value="claude">Claude 兼容</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">路径前缀</label>
|
||||||
|
<select id="ep_prefix_mode" class="input">
|
||||||
|
<option value="auto">自动 (如 /v1)</option>
|
||||||
|
<option value="custom">自定义</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">API 地址</label>
|
||||||
|
<input id="ep_api_base" type="text" class="input" placeholder="https://api.openai.com">
|
||||||
|
</div>
|
||||||
|
<div class="form-group hidden" id="ep_custom_prefix_group">
|
||||||
|
<label class="form-label">自定义前缀</label>
|
||||||
|
<input id="ep_prefix_custom" type="text" class="input" placeholder="/v1">
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">API Key</label>
|
||||||
|
<div class="input-row">
|
||||||
|
<input id="ep_api_key" type="password" class="input" placeholder="sk-...">
|
||||||
|
<button id="ep_toggle_key" class="btn">显示</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">模型</label>
|
||||||
|
<input id="ep_model" type="text" class="input" placeholder="gpt-4o, claude-3-5-sonnet...">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="ep_model_selector" class="hidden" style="margin-top:12px;">
|
||||||
|
<label class="form-label">选择模型</label>
|
||||||
|
<select id="ep_model_select" class="input">
|
||||||
|
<option value="">-- 从列表选择 --</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="btn-group" style="margin-top:16px;">
|
||||||
|
<button id="ep_fetch_models" class="btn">拉取模型列表</button>
|
||||||
|
<button id="ep_test_conn" class="btn">测试连接</button>
|
||||||
|
</div>
|
||||||
|
<div id="ep_api_status" class="status-text"></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="card">
|
||||||
|
<div class="card-title">生成参数</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">流式输出</label>
|
||||||
|
<select id="ep_stream" class="input">
|
||||||
|
<option value="true">开启</option>
|
||||||
|
<option value="false">关闭</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Temperature</label>
|
||||||
|
<input id="ep_temp" type="number" class="input" step="0.1" min="0" max="2">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Top P</label>
|
||||||
|
<input id="ep_top_p" type="number" class="input" step="0.05" min="0" max="1">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Top K</label>
|
||||||
|
<input id="ep_top_k" type="number" class="input" step="1" min="0">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Presence penalty</label>
|
||||||
|
<input id="ep_pp" type="text" class="input" placeholder="-2 ~ 2">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Frequency penalty</label>
|
||||||
|
<input id="ep_fp" type="text" class="input" placeholder="-2 ~ 2">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">最大 Token 数</label>
|
||||||
|
<input id="ep_mt" type="text" class="input" placeholder="留空则不限制">
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ── 提示词 ── -->
|
||||||
|
<div id="view-prompt" class="view">
|
||||||
|
<div class="tip-box">
|
||||||
|
<div class="tip-icon">💡</div>
|
||||||
|
<div class="tip-text">
|
||||||
|
系统会自动在提示词之后注入:角色卡、世界书、剧情摘要、聊天历史、向量召回等上下文。你只需专注编写"规划指令"。
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section class="card">
|
||||||
|
<div class="card-title">模板管理</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group" style="flex:2;">
|
||||||
|
<select id="ep_tpl_select" class="input">
|
||||||
|
<option value="">-- 选择模板 --</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group" style="flex:3;">
|
||||||
|
<div class="btn-group">
|
||||||
|
<button id="ep_tpl_save" class="btn btn-p">保存</button>
|
||||||
|
<button id="ep_tpl_saveas" class="btn">另存为</button>
|
||||||
|
<button id="ep_tpl_delete" class="btn btn-del">删除</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="ep_tpl_undo" class="undo-bar hidden">
|
||||||
|
<span>模板 <strong id="ep_tpl_undo_name"></strong> 已删除</span>
|
||||||
|
<button id="ep_tpl_undo_btn" class="btn btn-p btn-sm">撤销</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="card">
|
||||||
|
<div class="card-title">提示词块</div>
|
||||||
|
<div id="ep_prompt_list"></div>
|
||||||
|
<div class="prompt-empty" id="ep_prompt_empty" style="display:none;">暂无提示词块</div>
|
||||||
|
<div class="btn-group" style="margin-top:16px;">
|
||||||
|
<button id="ep_add_prompt" class="btn">添加区块</button>
|
||||||
|
<button id="ep_reset_prompt" class="btn btn-del">恢复默认</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ── 上下文 ── -->
|
||||||
|
<div id="view-context" class="view">
|
||||||
|
<section class="card">
|
||||||
|
<div class="card-title">世界书</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">读取全局世界书</label>
|
||||||
|
<select id="ep_include_global_wb" class="input">
|
||||||
|
<option value="false">否</option>
|
||||||
|
<option value="true">是</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">排除 position=4 的条目</label>
|
||||||
|
<select id="ep_wb_pos4" class="input">
|
||||||
|
<option value="true">是</option>
|
||||||
|
<option value="false">否</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">排除的条目名称关键词(逗号分隔)</label>
|
||||||
|
<input id="ep_wb_exclude_names" type="text" class="input" placeholder="mvu_update, system, ...">
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="card">
|
||||||
|
<div class="card-title">聊天与历史</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">保留的规划输出标签(逗号分隔)</label>
|
||||||
|
<input id="ep_keep_tags" type="text" class="input" placeholder="plot, note, plot-log, state">
|
||||||
|
<p class="form-hint">仅支持英文标签(如 plot, note, memory)。留空表示不按标签过滤(仅去除 think)。无效标签会自动忽略。</p>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">清理 AI 回复中的干扰标签(逗号分隔)</label>
|
||||||
|
<input id="ep_exclude_tags" type="text" class="input"
|
||||||
|
placeholder="行动选项, UpdateVariable, StatusPlaceHolderImpl">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">携带最近 N 条历史 plot</label>
|
||||||
|
<input id="ep_plot_n" type="number" class="input" min="0" max="10" step="1">
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ── 调试 ── -->
|
||||||
|
<div id="view-debug" class="view">
|
||||||
|
<section class="card">
|
||||||
|
<div class="card-title">诊断工具</div>
|
||||||
|
<div class="btn-group">
|
||||||
|
<button id="ep_debug_worldbook" class="btn">诊断世界书</button>
|
||||||
|
<button id="ep_debug_char" class="btn">诊断角色卡</button>
|
||||||
|
<button id="ep_test_planner" class="btn btn-p">运行规划测试</button>
|
||||||
|
</div>
|
||||||
|
<pre id="ep_debug_output" class="debug-output"></pre>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="card">
|
||||||
|
<div class="card-title">日志</div>
|
||||||
|
<div class="form-row" style="margin-bottom:16px;">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">持久化日志</label>
|
||||||
|
<select id="ep_logs_persist" class="input">
|
||||||
|
<option value="true">是</option>
|
||||||
|
<option value="false">否</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">最大日志条数</label>
|
||||||
|
<input id="ep_logs_max" type="number" class="input" min="1" max="200" step="1">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="btn-group" style="margin-bottom:16px;">
|
||||||
|
<button id="ep_open_logs" class="btn">刷新</button>
|
||||||
|
<button id="ep_log_export" class="btn">导出 JSON</button>
|
||||||
|
<button id="ep_log_clear" class="btn btn-del">清空日志</button>
|
||||||
|
</div>
|
||||||
|
<div id="ep_log_body" class="log-list">
|
||||||
|
<div class="log-empty">暂无日志</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- Mobile bottom nav -->
|
||||||
|
<nav class="mobile-nav">
|
||||||
|
<div class="mobile-nav-inner">
|
||||||
|
<div class="mobile-nav-item active" data-view="quickstart">
|
||||||
|
<div class="nav-dot"></div><span>开始</span>
|
||||||
|
</div>
|
||||||
|
<div class="mobile-nav-item" data-view="api">
|
||||||
|
<div class="nav-dot"></div><span>API</span>
|
||||||
|
</div>
|
||||||
|
<div class="mobile-nav-item" data-view="prompt">
|
||||||
|
<div class="nav-dot"></div><span>提示词</span>
|
||||||
|
</div>
|
||||||
|
<div class="mobile-nav-item" data-view="context">
|
||||||
|
<div class="nav-dot"></div><span>上下文</span>
|
||||||
|
</div>
|
||||||
|
<div class="mobile-nav-item" data-view="debug">
|
||||||
|
<div class="nav-dot"></div><span>调试</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const PARENT_ORIGIN = (() => {
|
||||||
|
try { return new URL(document.referrer).origin; } catch { return window.location.origin; }
|
||||||
|
})();
|
||||||
|
|
||||||
|
const post = (type, payload) => parent.postMessage({ type, payload }, PARENT_ORIGIN);
|
||||||
|
const $ = id => document.getElementById(id);
|
||||||
|
const $$ = sel => document.querySelectorAll(sel);
|
||||||
|
|
||||||
|
function genId() {
|
||||||
|
try { return crypto.randomUUID(); } catch { return `${Date.now()}_${Math.random().toString(36).slice(2, 8)}`; }
|
||||||
|
}
|
||||||
|
|
||||||
|
let cfg = null;
|
||||||
|
let logs = [];
|
||||||
|
let pendingSave = null;
|
||||||
|
let undoState = null;
|
||||||
|
let undoPending = false;
|
||||||
|
let fetchedModels = [];
|
||||||
|
|
||||||
|
/* ── Save indicator ── */
|
||||||
|
|
||||||
|
function setSaveIndicator(state, text) {
|
||||||
|
const el = $('ep_save_status');
|
||||||
|
if (!el) return;
|
||||||
|
if (state === 'saving') {
|
||||||
|
el.innerHTML = `<span style="color:var(--warn)">${text || '保存中…'}</span>`;
|
||||||
|
} else if (state === 'saved') {
|
||||||
|
el.innerHTML = `<span style="color:var(--success)">${text || '已保存'}</span>`;
|
||||||
|
} else if (state === 'error') {
|
||||||
|
el.innerHTML = `<span style="color:var(--error)">${text || '保存失败'}</span>`;
|
||||||
|
} else {
|
||||||
|
el.textContent = '就绪';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function startPendingSave(requestId) {
|
||||||
|
pendingSave = {
|
||||||
|
requestId,
|
||||||
|
timer: setTimeout(() => {
|
||||||
|
if (!pendingSave || pendingSave.requestId !== requestId) return;
|
||||||
|
pendingSave = null;
|
||||||
|
setSaveIndicator('error', '保存超时');
|
||||||
|
}, 5000)
|
||||||
|
};
|
||||||
|
setSaveIndicator('saving');
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolvePendingSave(requestId) {
|
||||||
|
if (!pendingSave || pendingSave.requestId !== requestId) return;
|
||||||
|
clearTimeout(pendingSave.timer);
|
||||||
|
pendingSave = null;
|
||||||
|
setSaveIndicator('saved');
|
||||||
|
setTimeout(() => setSaveIndicator(''), 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function rejectPendingSave(requestId, msg) {
|
||||||
|
if (!pendingSave || pendingSave.requestId !== requestId) return;
|
||||||
|
clearTimeout(pendingSave.timer);
|
||||||
|
pendingSave = null;
|
||||||
|
setSaveIndicator('error', msg || '保存失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Auto-save ── */
|
||||||
|
|
||||||
|
let autoSaveTimer = null;
|
||||||
|
|
||||||
|
function scheduleSave() {
|
||||||
|
if (undoPending) return;
|
||||||
|
if (autoSaveTimer) clearTimeout(autoSaveTimer);
|
||||||
|
autoSaveTimer = setTimeout(doSave, 600);
|
||||||
|
}
|
||||||
|
|
||||||
|
function doSave() {
|
||||||
|
if (pendingSave) return;
|
||||||
|
const requestId = `ena_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
||||||
|
const patch = collectPatch();
|
||||||
|
startPendingSave(requestId);
|
||||||
|
post('xb-ena:save-config', { requestId, patch });
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── UI helpers ── */
|
||||||
|
|
||||||
|
function setLocalStatus(elId, text, type) {
|
||||||
|
const el = $(elId);
|
||||||
|
if (!el) return;
|
||||||
|
el.textContent = text || '';
|
||||||
|
el.className = 'status-text' + (type ? ' ' + type : '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function setBadge(enabled) {
|
||||||
|
const badge = $('ep_badge');
|
||||||
|
badge.innerHTML = enabled
|
||||||
|
? '<span class="hl">已启用</span>'
|
||||||
|
: '<span style="color:var(--txt3)">未启用</span>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function activateTab(viewId) {
|
||||||
|
$$('.nav-item, .mobile-nav-item').forEach(n => {
|
||||||
|
n.classList.toggle('active', n.dataset.view === viewId);
|
||||||
|
});
|
||||||
|
$$('.view').forEach(v => {
|
||||||
|
v.classList.toggle('active', v.id === `view-${viewId}`);
|
||||||
|
});
|
||||||
|
if (viewId === 'debug') post('xb-ena:logs-request');
|
||||||
|
}
|
||||||
|
|
||||||
|
function updatePrefixModeUI() {
|
||||||
|
$('ep_custom_prefix_group').classList.toggle('hidden', $('ep_prefix_mode').value !== 'custom');
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Type conversion ── */
|
||||||
|
|
||||||
|
function toBool(v, fallback = false) {
|
||||||
|
if (v === true || v === false) return v;
|
||||||
|
if (v === 'true') return true;
|
||||||
|
if (v === 'false') return false;
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toNum(v, fallback = 0) {
|
||||||
|
const n = Number(v);
|
||||||
|
return Number.isFinite(n) ? n : fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
function arrToCsv(arr) { return Array.isArray(arr) ? arr.join(', ') : ''; }
|
||||||
|
|
||||||
|
function csvToArr(text) {
|
||||||
|
return String(text || '').split(/[,,]/).map(x => x.trim()).filter(Boolean);
|
||||||
|
}
|
||||||
|
function normalizeKeepTagsInput(text) {
|
||||||
|
const src = csvToArr(text);
|
||||||
|
const out = [];
|
||||||
|
src.forEach(item => {
|
||||||
|
const tag = String(item || '').replace(/^<+|>+$/g, '').toLowerCase();
|
||||||
|
if (!/^[a-z][a-z0-9_-]*$/.test(tag)) return;
|
||||||
|
if (!out.includes(tag)) out.push(tag);
|
||||||
|
});
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(str) {
|
||||||
|
return String(str || '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Prompt blocks ── */
|
||||||
|
|
||||||
|
function createPromptBlockElement(block, idx, total) {
|
||||||
|
const wrap = document.createElement('div');
|
||||||
|
wrap.className = 'prompt-block';
|
||||||
|
|
||||||
|
const head = document.createElement('div');
|
||||||
|
head.className = 'prompt-head';
|
||||||
|
|
||||||
|
const left = document.createElement('div');
|
||||||
|
left.className = 'prompt-head-left';
|
||||||
|
|
||||||
|
const nameInput = document.createElement('input');
|
||||||
|
nameInput.type = 'text';
|
||||||
|
nameInput.className = 'input';
|
||||||
|
nameInput.placeholder = '块名称';
|
||||||
|
nameInput.value = block.name || '';
|
||||||
|
nameInput.addEventListener('change', () => { block.name = nameInput.value; scheduleSave(); });
|
||||||
|
|
||||||
|
const roleSelect = document.createElement('select');
|
||||||
|
roleSelect.className = 'input';
|
||||||
|
['system', 'user', 'assistant'].forEach(r => {
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = r;
|
||||||
|
opt.textContent = r;
|
||||||
|
opt.selected = (block.role || 'system') === r;
|
||||||
|
roleSelect.appendChild(opt);
|
||||||
|
});
|
||||||
|
roleSelect.addEventListener('change', () => { block.role = roleSelect.value; scheduleSave(); });
|
||||||
|
|
||||||
|
left.append(nameInput, roleSelect);
|
||||||
|
|
||||||
|
const right = document.createElement('div');
|
||||||
|
right.className = 'prompt-head-right';
|
||||||
|
|
||||||
|
const upBtn = document.createElement('button');
|
||||||
|
upBtn.className = 'btn btn-sm';
|
||||||
|
upBtn.textContent = '↑';
|
||||||
|
upBtn.disabled = idx === 0;
|
||||||
|
upBtn.addEventListener('click', () => {
|
||||||
|
if (idx === 0) return;
|
||||||
|
[cfg.promptBlocks[idx - 1], cfg.promptBlocks[idx]] = [cfg.promptBlocks[idx], cfg.promptBlocks[idx - 1]];
|
||||||
|
renderPromptList(); scheduleSave();
|
||||||
|
});
|
||||||
|
|
||||||
|
const downBtn = document.createElement('button');
|
||||||
|
downBtn.className = 'btn btn-sm';
|
||||||
|
downBtn.textContent = '↓';
|
||||||
|
downBtn.disabled = idx === total - 1;
|
||||||
|
downBtn.addEventListener('click', () => {
|
||||||
|
if (idx >= total - 1) return;
|
||||||
|
[cfg.promptBlocks[idx], cfg.promptBlocks[idx + 1]] = [cfg.promptBlocks[idx + 1], cfg.promptBlocks[idx]];
|
||||||
|
renderPromptList(); scheduleSave();
|
||||||
|
});
|
||||||
|
|
||||||
|
const delBtn = document.createElement('button');
|
||||||
|
delBtn.className = 'btn btn-sm btn-del';
|
||||||
|
delBtn.textContent = '删除';
|
||||||
|
delBtn.addEventListener('click', () => {
|
||||||
|
cfg.promptBlocks.splice(idx, 1);
|
||||||
|
renderPromptList(); scheduleSave();
|
||||||
|
});
|
||||||
|
|
||||||
|
right.append(upBtn, downBtn, delBtn);
|
||||||
|
|
||||||
|
const content = document.createElement('textarea');
|
||||||
|
content.className = 'input';
|
||||||
|
content.placeholder = '提示词内容...';
|
||||||
|
content.value = block.content || '';
|
||||||
|
content.addEventListener('change', () => { block.content = content.value; scheduleSave(); });
|
||||||
|
|
||||||
|
head.append(left, right);
|
||||||
|
wrap.append(head, content);
|
||||||
|
return wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPromptList() {
|
||||||
|
const list = $('ep_prompt_list');
|
||||||
|
const empty = $('ep_prompt_empty');
|
||||||
|
const blocks = cfg?.promptBlocks || [];
|
||||||
|
list.innerHTML = '';
|
||||||
|
if (!blocks.length) { empty.style.display = ''; return; }
|
||||||
|
empty.style.display = 'none';
|
||||||
|
blocks.forEach((block, idx) => {
|
||||||
|
list.appendChild(createPromptBlockElement(block, idx, blocks.length));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTemplateSelect(selected = '') {
|
||||||
|
const sel = $('ep_tpl_select');
|
||||||
|
sel.innerHTML = '<option value="">-- 选择模板 --</option>';
|
||||||
|
const names = Object.keys(cfg?.promptTemplates || {});
|
||||||
|
const selectedName = names.includes(selected) ? selected : '';
|
||||||
|
names.forEach(name => {
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = name;
|
||||||
|
opt.textContent = name;
|
||||||
|
opt.selected = name === selectedName;
|
||||||
|
sel.appendChild(opt);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Undo ── */
|
||||||
|
|
||||||
|
function showUndoBar(name, blocks) {
|
||||||
|
clearUndo();
|
||||||
|
undoPending = true;
|
||||||
|
undoState = {
|
||||||
|
name, blocks,
|
||||||
|
timer: setTimeout(() => {
|
||||||
|
hideUndoBar(); undoPending = false; scheduleSave();
|
||||||
|
}, 5000)
|
||||||
|
};
|
||||||
|
$('ep_tpl_undo_name').textContent = name;
|
||||||
|
$('ep_tpl_undo').classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideUndoBar() {
|
||||||
|
$('ep_tpl_undo').classList.add('hidden');
|
||||||
|
undoState = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearUndo() {
|
||||||
|
if (undoState?.timer) clearTimeout(undoState.timer);
|
||||||
|
hideUndoBar();
|
||||||
|
undoPending = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Model selector ── */
|
||||||
|
|
||||||
|
function showModelSelector(models) {
|
||||||
|
fetchedModels = models;
|
||||||
|
const sel = $('ep_model_select');
|
||||||
|
const cur = $('ep_model').value.trim();
|
||||||
|
sel.innerHTML = '<option value="">-- 从列表选择 --</option>';
|
||||||
|
models.forEach(m => {
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = m; opt.textContent = m; opt.selected = m === cur;
|
||||||
|
sel.appendChild(opt);
|
||||||
|
});
|
||||||
|
$('ep_model_selector').classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Logs ── */
|
||||||
|
|
||||||
|
function renderLogs() {
|
||||||
|
const body = $('ep_log_body');
|
||||||
|
if (!Array.isArray(logs) || !logs.length) {
|
||||||
|
body.innerHTML = '<div class="log-empty">暂无日志</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
body.innerHTML = logs.map(item => {
|
||||||
|
const time = item.time ? new Date(item.time).toLocaleString() : '-';
|
||||||
|
const cls = item.ok ? 'success' : 'error';
|
||||||
|
const label = item.ok ? '成功' : '失败';
|
||||||
|
return `
|
||||||
|
<div class="log-item">
|
||||||
|
<div class="log-meta">
|
||||||
|
<span>${escapeHtml(time)} · <span class="${cls}">${label}</span></span>
|
||||||
|
<span>${escapeHtml(item.model || '-')}</span>
|
||||||
|
</div>
|
||||||
|
${item.error ? `<div class="log-error">${escapeHtml(item.error)}</div>` : ''}
|
||||||
|
<details><summary>请求消息</summary>
|
||||||
|
<pre class="log-pre">${escapeHtml(JSON.stringify(item.requestMessages || [], null, 2))}</pre>
|
||||||
|
</details>
|
||||||
|
<details><summary>原始回复</summary>
|
||||||
|
<pre class="log-pre">${escapeHtml(item.rawReply || '')}</pre>
|
||||||
|
</details>
|
||||||
|
<details open><summary>过滤后回复</summary>
|
||||||
|
<pre class="log-pre">${escapeHtml(item.filteredReply || '')}</pre>
|
||||||
|
</details>
|
||||||
|
</div>`;
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Apply / Collect ── */
|
||||||
|
|
||||||
|
function applyConfig(nextCfg) {
|
||||||
|
cfg = nextCfg || {};
|
||||||
|
logs = Array.isArray(cfg.logs) ? cfg.logs : [];
|
||||||
|
|
||||||
|
$('ep_enabled').value = String(toBool(cfg.enabled, true));
|
||||||
|
$('ep_skip_plot').value = String(toBool(cfg.skipIfPlotPresent, true));
|
||||||
|
|
||||||
|
const api = cfg.api || {};
|
||||||
|
$('ep_api_channel').value = api.channel || 'openai';
|
||||||
|
$('ep_prefix_mode').value = api.prefixMode || 'auto';
|
||||||
|
$('ep_api_base').value = api.baseUrl || '';
|
||||||
|
$('ep_prefix_custom').value = api.customPrefix || '';
|
||||||
|
$('ep_api_key').value = api.apiKey || '';
|
||||||
|
$('ep_model').value = api.model || '';
|
||||||
|
$('ep_stream').value = String(toBool(api.stream, false));
|
||||||
|
$('ep_temp').value = String(toNum(api.temperature, 1));
|
||||||
|
$('ep_top_p').value = String(toNum(api.top_p, 1));
|
||||||
|
$('ep_top_k').value = String(toNum(api.top_k, 0));
|
||||||
|
$('ep_pp').value = api.presence_penalty ?? '';
|
||||||
|
$('ep_fp').value = api.frequency_penalty ?? '';
|
||||||
|
$('ep_mt').value = api.max_tokens ?? '';
|
||||||
|
|
||||||
|
$('ep_include_global_wb').value = String(toBool(cfg.includeGlobalWorldbooks, false));
|
||||||
|
$('ep_wb_pos4').value = String(toBool(cfg.excludeWorldbookPosition4, true));
|
||||||
|
$('ep_wb_exclude_names').value = arrToCsv(cfg.worldbookExcludeNames);
|
||||||
|
$('ep_plot_n').value = String(toNum(cfg.plotCount, 2));
|
||||||
|
$('ep_keep_tags').value = arrToCsv(cfg.responseKeepTags || ['plot', 'note', 'plot-log', 'state']);
|
||||||
|
$('ep_exclude_tags').value = arrToCsv(cfg.chatExcludeTags);
|
||||||
|
|
||||||
|
$('ep_logs_persist').value = String(toBool(cfg.logsPersist, true));
|
||||||
|
$('ep_logs_max').value = String(toNum(cfg.logsMax, 20));
|
||||||
|
|
||||||
|
setBadge(toBool(cfg.enabled, true));
|
||||||
|
updatePrefixModeUI();
|
||||||
|
const keepSelectedTemplate = cfg?.activePromptTemplate || $('ep_tpl_select')?.value || '';
|
||||||
|
renderTemplateSelect(keepSelectedTemplate);
|
||||||
|
renderPromptList();
|
||||||
|
renderLogs();
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectPatch() {
|
||||||
|
const p = {};
|
||||||
|
|
||||||
|
p.enabled = toBool($('ep_enabled').value, true);
|
||||||
|
p.skipIfPlotPresent = toBool($('ep_skip_plot').value, true);
|
||||||
|
|
||||||
|
p.api = {
|
||||||
|
channel: $('ep_api_channel').value,
|
||||||
|
prefixMode: $('ep_prefix_mode').value,
|
||||||
|
baseUrl: $('ep_api_base').value.trim(),
|
||||||
|
customPrefix: $('ep_prefix_custom').value.trim(),
|
||||||
|
apiKey: $('ep_api_key').value,
|
||||||
|
model: $('ep_model').value.trim(),
|
||||||
|
stream: toBool($('ep_stream').value, false),
|
||||||
|
temperature: toNum($('ep_temp').value, 1),
|
||||||
|
top_p: toNum($('ep_top_p').value, 1),
|
||||||
|
top_k: Math.floor(toNum($('ep_top_k').value, 0)),
|
||||||
|
presence_penalty: $('ep_pp').value.trim(),
|
||||||
|
frequency_penalty: $('ep_fp').value.trim(),
|
||||||
|
max_tokens: $('ep_mt').value.trim()
|
||||||
|
};
|
||||||
|
|
||||||
|
p.includeGlobalWorldbooks = toBool($('ep_include_global_wb').value, false);
|
||||||
|
p.excludeWorldbookPosition4 = toBool($('ep_wb_pos4').value, true);
|
||||||
|
p.worldbookExcludeNames = csvToArr($('ep_wb_exclude_names').value);
|
||||||
|
p.plotCount = Math.max(0, Math.floor(toNum($('ep_plot_n').value, 2)));
|
||||||
|
p.responseKeepTags = normalizeKeepTagsInput($('ep_keep_tags').value);
|
||||||
|
p.chatExcludeTags = csvToArr($('ep_exclude_tags').value);
|
||||||
|
|
||||||
|
p.logsPersist = toBool($('ep_logs_persist').value, true);
|
||||||
|
p.logsMax = Math.max(1, Math.min(200, Math.floor(toNum($('ep_logs_max').value, 20))));
|
||||||
|
|
||||||
|
p.promptBlocks = cfg?.promptBlocks || [];
|
||||||
|
p.promptTemplates = cfg?.promptTemplates || {};
|
||||||
|
p.activePromptTemplate = $('ep_tpl_select')?.value || '';
|
||||||
|
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Event bindings ── */
|
||||||
|
|
||||||
|
function bindEvents() {
|
||||||
|
$$('.nav-item, .mobile-nav-item').forEach(item => {
|
||||||
|
item.addEventListener('click', () => activateTab(item.dataset.view));
|
||||||
|
});
|
||||||
|
|
||||||
|
$('ep_close').addEventListener('click', () => post('xb-ena:close'));
|
||||||
|
|
||||||
|
$('ep_enabled').addEventListener('change', () => setBadge(toBool($('ep_enabled').value, true)));
|
||||||
|
|
||||||
|
$('ep_run_test').addEventListener('click', () => {
|
||||||
|
const text = $('ep_test_input').value.trim() || '(测试输入)我想让你帮我规划下一步剧情。';
|
||||||
|
post('xb-ena:run-test', { text });
|
||||||
|
setLocalStatus('ep_test_status', '测试中…', 'loading');
|
||||||
|
});
|
||||||
|
|
||||||
|
$('ep_toggle_key').addEventListener('click', () => {
|
||||||
|
const input = $('ep_api_key');
|
||||||
|
const btn = $('ep_toggle_key');
|
||||||
|
if (input.type === 'password') {
|
||||||
|
input.type = 'text'; btn.textContent = '隐藏';
|
||||||
|
} else {
|
||||||
|
input.type = 'password'; btn.textContent = '显示';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$('ep_prefix_mode').addEventListener('change', updatePrefixModeUI);
|
||||||
|
|
||||||
|
$('ep_fetch_models').addEventListener('click', () => {
|
||||||
|
post('xb-ena:fetch-models');
|
||||||
|
setLocalStatus('ep_api_status', '拉取中…', 'loading');
|
||||||
|
});
|
||||||
|
$('ep_test_conn').addEventListener('click', () => {
|
||||||
|
post('xb-ena:fetch-models');
|
||||||
|
setLocalStatus('ep_api_status', '测试中…', 'loading');
|
||||||
|
});
|
||||||
|
|
||||||
|
$('ep_model_select').addEventListener('change', () => {
|
||||||
|
const val = $('ep_model_select').value;
|
||||||
|
if (val) { $('ep_model').value = val; scheduleSave(); }
|
||||||
|
});
|
||||||
|
$('ep_keep_tags').addEventListener('change', () => {
|
||||||
|
const normalized = normalizeKeepTagsInput($('ep_keep_tags').value);
|
||||||
|
$('ep_keep_tags').value = normalized.join(', ');
|
||||||
|
});
|
||||||
|
|
||||||
|
$('ep_add_prompt').addEventListener('click', () => {
|
||||||
|
cfg.promptBlocks = cfg.promptBlocks || [];
|
||||||
|
cfg.promptBlocks.push({ id: genId(), role: 'system', name: '新块', content: '' });
|
||||||
|
renderPromptList(); scheduleSave();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('ep_reset_prompt').addEventListener('click', () => {
|
||||||
|
if (!confirm('确定恢复默认提示词块?当前提示词块将被覆盖。')) return;
|
||||||
|
if (pendingSave) return;
|
||||||
|
const requestId = `ena_reset_${Date.now()}`;
|
||||||
|
startPendingSave(requestId);
|
||||||
|
post('xb-ena:reset-prompt-default', { requestId });
|
||||||
|
});
|
||||||
|
|
||||||
|
$('ep_tpl_select').addEventListener('change', () => {
|
||||||
|
const name = $('ep_tpl_select').value;
|
||||||
|
cfg.activePromptTemplate = name;
|
||||||
|
if (!name) return;
|
||||||
|
const blocks = cfg?.promptTemplates?.[name];
|
||||||
|
if (!Array.isArray(blocks)) return;
|
||||||
|
cfg.promptBlocks = structuredClone(blocks);
|
||||||
|
renderPromptList(); scheduleSave();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('ep_tpl_save').addEventListener('click', () => {
|
||||||
|
const name = $('ep_tpl_select').value;
|
||||||
|
if (!name) { setSaveIndicator('error', '请先选择或创建模板'); return; }
|
||||||
|
cfg.promptTemplates = cfg.promptTemplates || {};
|
||||||
|
cfg.promptTemplates[name] = structuredClone(cfg.promptBlocks || []);
|
||||||
|
cfg.activePromptTemplate = name;
|
||||||
|
renderTemplateSelect(name); scheduleSave();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('ep_tpl_saveas').addEventListener('click', () => {
|
||||||
|
const name = prompt('新模板名称');
|
||||||
|
if (!name) return;
|
||||||
|
cfg.promptTemplates = cfg.promptTemplates || {};
|
||||||
|
cfg.promptTemplates[name] = structuredClone(cfg.promptBlocks || []);
|
||||||
|
cfg.activePromptTemplate = name;
|
||||||
|
renderTemplateSelect(name); scheduleSave();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('ep_tpl_delete').addEventListener('click', () => {
|
||||||
|
const name = $('ep_tpl_select').value;
|
||||||
|
if (!name) return;
|
||||||
|
const backup = structuredClone(cfg.promptTemplates[name]);
|
||||||
|
delete cfg.promptTemplates[name];
|
||||||
|
cfg.activePromptTemplate = '';
|
||||||
|
renderTemplateSelect('');
|
||||||
|
showUndoBar(name, backup);
|
||||||
|
});
|
||||||
|
|
||||||
|
$('ep_tpl_undo_btn').addEventListener('click', () => {
|
||||||
|
if (!undoState) return;
|
||||||
|
cfg.promptTemplates = cfg.promptTemplates || {};
|
||||||
|
cfg.promptTemplates[undoState.name] = undoState.blocks;
|
||||||
|
cfg.activePromptTemplate = undoState.name;
|
||||||
|
renderTemplateSelect(undoState.name);
|
||||||
|
clearUndo(); scheduleSave();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('ep_debug_worldbook').addEventListener('click', () => {
|
||||||
|
$('ep_debug_output').classList.add('visible');
|
||||||
|
$('ep_debug_output').textContent = '诊断中…';
|
||||||
|
post('xb-ena:debug-worldbook');
|
||||||
|
});
|
||||||
|
$('ep_debug_char').addEventListener('click', () => {
|
||||||
|
$('ep_debug_output').classList.add('visible');
|
||||||
|
$('ep_debug_output').textContent = '诊断中…';
|
||||||
|
post('xb-ena:debug-char');
|
||||||
|
});
|
||||||
|
$('ep_test_planner').addEventListener('click', () => {
|
||||||
|
post('xb-ena:run-test', { text: '(测试输入)请规划下一步剧情走向。' });
|
||||||
|
$('ep_debug_output').classList.add('visible');
|
||||||
|
$('ep_debug_output').textContent = '规划测试中…';
|
||||||
|
});
|
||||||
|
|
||||||
|
$('ep_open_logs').addEventListener('click', () => post('xb-ena:logs-request'));
|
||||||
|
$('ep_log_clear').addEventListener('click', () => {
|
||||||
|
if (!confirm('确定清空所有日志?')) return;
|
||||||
|
post('xb-ena:logs-clear');
|
||||||
|
});
|
||||||
|
$('ep_log_export').addEventListener('click', () => {
|
||||||
|
const blob = new Blob([JSON.stringify(logs || [], null, 2)], { type: 'application/json' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url; a.download = `ena-planner-logs-${Date.now()}.json`;
|
||||||
|
a.click(); URL.revokeObjectURL(url);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('.card .input').forEach(el => {
|
||||||
|
if (el.closest('.prompt-block')) return;
|
||||||
|
if (el.id === 'ep_test_input') return;
|
||||||
|
el.addEventListener('change', scheduleSave);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Message handler ── */
|
||||||
|
|
||||||
|
window.addEventListener('message', ev => {
|
||||||
|
if (ev.origin !== PARENT_ORIGIN) return;
|
||||||
|
const { type, payload } = ev.data || {};
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 'xb-ena:config':
|
||||||
|
applyConfig(payload || {});
|
||||||
|
break;
|
||||||
|
case 'xb-ena:config-saved':
|
||||||
|
applyConfig(payload || {});
|
||||||
|
resolvePendingSave(payload?.requestId || '');
|
||||||
|
break;
|
||||||
|
case 'xb-ena:config-save-error':
|
||||||
|
rejectPendingSave(payload?.requestId || '', payload?.message);
|
||||||
|
break;
|
||||||
|
case 'xb-ena:test-done': {
|
||||||
|
setLocalStatus('ep_test_status', '规划测试完成', 'success');
|
||||||
|
const d = $('ep_debug_output');
|
||||||
|
if (d.classList.contains('visible') && d.textContent.includes('测试中'))
|
||||||
|
d.textContent = '测试完成,请查看下方日志';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'xb-ena:test-error': {
|
||||||
|
const msg = payload?.message || '规划测试失败';
|
||||||
|
setLocalStatus('ep_test_status', msg, 'error');
|
||||||
|
const d = $('ep_debug_output');
|
||||||
|
if (d.classList.contains('visible')) d.textContent = '测试失败: ' + msg;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'xb-ena:logs':
|
||||||
|
logs = Array.isArray(payload?.logs) ? payload.logs : [];
|
||||||
|
renderLogs();
|
||||||
|
break;
|
||||||
|
case 'xb-ena:models': {
|
||||||
|
const models = Array.isArray(payload?.models) ? payload.models : [];
|
||||||
|
if (models.length) {
|
||||||
|
showModelSelector(models);
|
||||||
|
setLocalStatus('ep_api_status', `获取到 ${models.length} 个模型`, 'success');
|
||||||
|
} else {
|
||||||
|
setLocalStatus('ep_api_status', '未获取到模型', 'error');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'xb-ena:models-error':
|
||||||
|
setLocalStatus('ep_api_status', payload?.message || '拉取模型失败', 'error');
|
||||||
|
break;
|
||||||
|
case 'xb-ena:debug-output': {
|
||||||
|
const out = $('ep_debug_output');
|
||||||
|
out.classList.add('visible');
|
||||||
|
out.textContent = String(payload?.output || '');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ── Init ── */
|
||||||
|
|
||||||
|
bindEvents();
|
||||||
|
post('xb-ena:ready');
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -679,6 +679,9 @@ async function executeTaskJS(jsCode, taskName = 'AnonymousTask') {
|
|||||||
const listeners = new Set();
|
const listeners = new Set();
|
||||||
const createdNodes = new Set();
|
const createdNodes = new Set();
|
||||||
const waiters = new Set();
|
const waiters = new Set();
|
||||||
|
let suppressTimerTracking = false;
|
||||||
|
const originalToastrFns = {};
|
||||||
|
const toastrMethods = ['info', 'success', 'warning', 'error'];
|
||||||
|
|
||||||
const notifyActivityChange = () => {
|
const notifyActivityChange = () => {
|
||||||
if (waiters.size === 0) return;
|
if (waiters.size === 0) return;
|
||||||
@@ -689,16 +692,36 @@ async function executeTaskJS(jsCode, taskName = 'AnonymousTask') {
|
|||||||
|
|
||||||
window.setTimeout = function(fn, t, ...args) {
|
window.setTimeout = function(fn, t, ...args) {
|
||||||
const id = originals.setTimeout(function(...inner) {
|
const id = originals.setTimeout(function(...inner) {
|
||||||
try { fn?.(...inner); } finally { timeouts.delete(id); notifyActivityChange(); }
|
try { fn?.(...inner); }
|
||||||
|
finally {
|
||||||
|
if (timeouts.delete(id)) notifyActivityChange();
|
||||||
|
}
|
||||||
}, t, ...args);
|
}, t, ...args);
|
||||||
timeouts.add(id);
|
if (!suppressTimerTracking) {
|
||||||
notifyActivityChange();
|
timeouts.add(id);
|
||||||
|
notifyActivityChange();
|
||||||
|
}
|
||||||
return id;
|
return id;
|
||||||
};
|
};
|
||||||
window.clearTimeout = function(id) { originals.clearTimeout(id); timeouts.delete(id); notifyActivityChange(); };
|
window.clearTimeout = function(id) {
|
||||||
|
originals.clearTimeout(id);
|
||||||
|
if (timeouts.delete(id)) notifyActivityChange();
|
||||||
|
};
|
||||||
window.setInterval = function(fn, t, ...args) { const id = originals.setInterval(fn, t, ...args); intervals.add(id); notifyActivityChange(); return id; };
|
window.setInterval = function(fn, t, ...args) { const id = originals.setInterval(fn, t, ...args); intervals.add(id); notifyActivityChange(); return id; };
|
||||||
window.clearInterval = function(id) { originals.clearInterval(id); intervals.delete(id); notifyActivityChange(); };
|
window.clearInterval = function(id) { originals.clearInterval(id); intervals.delete(id); notifyActivityChange(); };
|
||||||
|
|
||||||
|
if (window.toastr) {
|
||||||
|
for (const method of toastrMethods) {
|
||||||
|
if (typeof window.toastr[method] !== 'function') continue;
|
||||||
|
originalToastrFns[method] = window.toastr[method];
|
||||||
|
window.toastr[method] = function(...fnArgs) {
|
||||||
|
suppressTimerTracking = true;
|
||||||
|
try { return originalToastrFns[method].apply(window.toastr, fnArgs); }
|
||||||
|
finally { suppressTimerTracking = false; }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const addListenerEntry = (entry) => { listeners.add(entry); notifyActivityChange(); };
|
const addListenerEntry = (entry) => { listeners.add(entry); notifyActivityChange(); };
|
||||||
const removeListenerEntry = (target, type, listener, options) => {
|
const removeListenerEntry = (target, type, listener, options) => {
|
||||||
let removed = false;
|
let removed = false;
|
||||||
@@ -736,6 +759,13 @@ async function executeTaskJS(jsCode, taskName = 'AnonymousTask') {
|
|||||||
Node.prototype.appendChild = originals.appendChild;
|
Node.prototype.appendChild = originals.appendChild;
|
||||||
Node.prototype.insertBefore = originals.insertBefore;
|
Node.prototype.insertBefore = originals.insertBefore;
|
||||||
Node.prototype.replaceChild = originals.replaceChild;
|
Node.prototype.replaceChild = originals.replaceChild;
|
||||||
|
if (window.toastr) {
|
||||||
|
for (const method of toastrMethods) {
|
||||||
|
if (typeof originalToastrFns[method] === 'function') {
|
||||||
|
window.toastr[method] = originalToastrFns[method];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const hardCleanup = () => {
|
const hardCleanup = () => {
|
||||||
|
|||||||
@@ -1029,6 +1029,12 @@ function buildFramePayload(store) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Compatibility export for ena-planner.
|
||||||
|
// Returns a compact plain-text snapshot of story-summary memory.
|
||||||
|
export function getStorySummaryForEna() {
|
||||||
|
return _lastBuiltPromptText;
|
||||||
|
}
|
||||||
|
|
||||||
function parseRelationTargetFromPredicate(predicate) {
|
function parseRelationTargetFromPredicate(predicate) {
|
||||||
const text = String(predicate || "").trim();
|
const text = String(predicate || "").trim();
|
||||||
if (!text.startsWith("对")) return null;
|
if (!text.startsWith("对")) return null;
|
||||||
@@ -1093,6 +1099,57 @@ function mergeCharacterRelationshipsIntoFacts(existingFacts, relationships, floo
|
|||||||
return [...nonRelationFacts, ...newRelationFacts];
|
return [...nonRelationFacts, ...newRelationFacts];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getCurrentFloorHint() {
|
||||||
|
const { chat } = getContext();
|
||||||
|
const lastFloor = (Array.isArray(chat) ? chat.length : 0) - 1;
|
||||||
|
return Math.max(0, lastFloor);
|
||||||
|
}
|
||||||
|
|
||||||
|
function factKeyBySubjectPredicate(fact) {
|
||||||
|
const s = String(fact?.s || "").trim();
|
||||||
|
const p = String(fact?.p || "").trim();
|
||||||
|
return `${s}::${p}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeEditedFactsWithTimestamps(existingFacts, editedFacts, floorHint = 0) {
|
||||||
|
const currentFacts = Array.isArray(existingFacts) ? existingFacts : [];
|
||||||
|
const incomingFacts = Array.isArray(editedFacts) ? editedFacts : [];
|
||||||
|
const oldMap = new Map(currentFacts.map((f) => [factKeyBySubjectPredicate(f), f]));
|
||||||
|
|
||||||
|
let nextFactId = getNextFactIdValue(currentFacts);
|
||||||
|
const merged = [];
|
||||||
|
|
||||||
|
for (const fact of incomingFacts) {
|
||||||
|
const s = String(fact?.s || "").trim();
|
||||||
|
const p = String(fact?.p || "").trim();
|
||||||
|
const o = String(fact?.o || "").trim();
|
||||||
|
if (!s || !p || !o) continue;
|
||||||
|
|
||||||
|
const key = `${s}::${p}`;
|
||||||
|
const oldFact = oldMap.get(key);
|
||||||
|
const since = oldFact?.since ?? fact?.since ?? floorHint;
|
||||||
|
const addedAt = oldFact?._addedAt ?? fact?._addedAt ?? floorHint;
|
||||||
|
|
||||||
|
const out = {
|
||||||
|
id: oldFact?.id || fact?.id || `f-${nextFactId++}`,
|
||||||
|
s,
|
||||||
|
p,
|
||||||
|
o,
|
||||||
|
since,
|
||||||
|
_addedAt: addedAt,
|
||||||
|
};
|
||||||
|
if (oldFact?._isState != null) out._isState = oldFact._isState;
|
||||||
|
|
||||||
|
const mergedTrend = fact?.trend ?? oldFact?.trend;
|
||||||
|
if (mergedTrend != null && String(mergedTrend).trim()) {
|
||||||
|
out.trend = String(mergedTrend).trim();
|
||||||
|
}
|
||||||
|
merged.push(out);
|
||||||
|
}
|
||||||
|
|
||||||
|
return merged;
|
||||||
|
}
|
||||||
|
|
||||||
function openPanelForMessage(mesId) {
|
function openPanelForMessage(mesId) {
|
||||||
createOverlay();
|
createOverlay();
|
||||||
showOverlay();
|
showOverlay();
|
||||||
@@ -1429,13 +1486,17 @@ async function handleFrameMessage(event) {
|
|||||||
|
|
||||||
// 如果是 events,先记录旧数据用于同步向量
|
// 如果是 events,先记录旧数据用于同步向量
|
||||||
const oldEvents = data.section === "events" ? [...(store.json.events || [])] : null;
|
const oldEvents = data.section === "events" ? [...(store.json.events || [])] : null;
|
||||||
|
const oldFacts = data.section === "facts" ? [...(store.json.facts || [])] : null;
|
||||||
|
|
||||||
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 === "facts") {
|
||||||
|
store.json.facts = mergeEditedFactsWithTimestamps(oldFacts, data.data, getCurrentFloorHint());
|
||||||
|
}
|
||||||
if (data.section === "characters") {
|
if (data.section === "characters") {
|
||||||
const rels = data?.data?.relationships || [];
|
const rels = data?.data?.relationships || [];
|
||||||
const floorHint = Math.max(0, Number(store.lastSummarizedMesId) || 0);
|
const floorHint = getCurrentFloorHint();
|
||||||
store.json.facts = mergeCharacterRelationshipsIntoFacts(store.json.facts, rels, floorHint);
|
store.json.facts = mergeCharacterRelationshipsIntoFacts(store.json.facts, rels, floorHint);
|
||||||
}
|
}
|
||||||
store.updatedAt = Date.now();
|
store.updatedAt = Date.now();
|
||||||
@@ -1757,13 +1818,14 @@ async function handleGenerationStarted(type, _params, isDryRun) {
|
|||||||
} else {
|
} else {
|
||||||
text = buildNonVectorPromptText() || "";
|
text = buildNonVectorPromptText() || "";
|
||||||
}
|
}
|
||||||
|
_lastBuiltPromptText = text;
|
||||||
if (!text.trim()) return;
|
if (!text.trim()) return;
|
||||||
|
|
||||||
// 获取用户配置的 role
|
// 获取用户配置的 role
|
||||||
const cfg = getSummaryPanelConfig();
|
const cfg = getSummaryPanelConfig();
|
||||||
const roleKey = cfg.trigger?.role || 'system';
|
const roleKey = cfg.trigger?.role || 'system';
|
||||||
const role = ROLE_MAP[roleKey] || extension_prompt_roles.SYSTEM;
|
const role = ROLE_MAP[roleKey] || extension_prompt_roles.SYSTEM;
|
||||||
_lastBuiltPromptText = text;
|
|
||||||
// 写入 extension_prompts
|
// 写入 extension_prompts
|
||||||
extension_prompts[EXT_PROMPT_KEY] = {
|
extension_prompts[EXT_PROMPT_KEY] = {
|
||||||
value: text,
|
value: text,
|
||||||
@@ -1877,6 +1939,3 @@ jQuery(() => {
|
|||||||
|
|
||||||
maybePreloadTokenizer();
|
maybePreloadTokenizer();
|
||||||
});
|
});
|
||||||
export function getStorySummaryForEna() {
|
|
||||||
return _lastBuiltPromptText;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,23 +1,33 @@
|
|||||||
/**
|
/**
|
||||||
* 火山引擎 TTS API 封装
|
* 火山引擎 TTS API 封装
|
||||||
* V3 单向流式 + V1试用
|
* V3 单向流式 + V1试用
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const V3_URL = 'https://openspeech.bytedance.com/api/v3/tts/unidirectional';
|
const V3_URL = 'https://openspeech.bytedance.com/api/v3/tts/unidirectional';
|
||||||
const FREE_V1_URL = 'https://hstts.velure.codes';
|
const FREE_V1_URL = 'https://edgetts.velure.codes';
|
||||||
|
|
||||||
export const FREE_VOICES = [
|
export const FREE_VOICES = [
|
||||||
{ key: 'female_1', name: '桃夭', tag: '甜蜜仙子', gender: 'female' },
|
{ key: 'female_1', name: '晓晓', tag: '温暖百变', gender: 'female' },
|
||||||
{ key: 'female_2', name: '霜华', tag: '清冷仙子', gender: 'female' },
|
{ key: 'female_2', name: '晓伊', tag: '清冷知性', gender: 'female' },
|
||||||
{ key: 'female_3', name: '顾姐', tag: '御姐烟嗓', gender: 'female' },
|
{ key: 'female_3', name: '小北', tag: '东北甜妹', gender: 'female' },
|
||||||
{ key: 'female_4', name: '苏菲', tag: '优雅知性', gender: 'female' },
|
{ key: 'female_4', name: '小妮', tag: '陕西姑娘', gender: 'female' },
|
||||||
{ key: 'female_5', name: '嘉欣', tag: '港风甜心', gender: 'female' },
|
{ key: 'hk_female_1', name: '曉佳', tag: '粤语女声', gender: 'female' },
|
||||||
{ key: 'female_6', name: '青梅', tag: '清秀少年音', gender: 'female' },
|
{ key: 'hk_female_2', name: '曉曼', tag: '粤语温柔', gender: 'female' },
|
||||||
{ key: 'female_7', name: '可莉', tag: '奶音萝莉', gender: 'female' },
|
{ key: 'hk_male_1', name: '雲龍', tag: '粤语男声', gender: 'male' },
|
||||||
{ key: 'male_1', name: '夜枭', tag: '磁性低音', gender: 'male' },
|
{ key: 'tw_female_1', name: '曉臻', tag: '台灣女聲', gender: 'female' },
|
||||||
{ key: 'male_2', name: '君泽', tag: '温润公子', gender: 'male' },
|
{ key: 'tw_female_2', name: '曉雨', tag: '台灣温柔', gender: 'female' },
|
||||||
{ key: 'male_3', name: '沐阳', tag: '沉稳暖男', gender: 'male' },
|
{ key: 'tw_male_1', name: '雲哲', tag: '台灣男聲', gender: 'male' },
|
||||||
{ key: 'male_4', name: '梓辛', tag: '青春少年', gender: 'male' },
|
{ key: 'male_1', name: '云希', tag: '少年温暖', gender: 'male' },
|
||||||
|
{ key: 'male_2', name: '云健', tag: '阳刚有力', gender: 'male' },
|
||||||
|
{ key: 'male_3', name: '云扬', tag: '专业播报', gender: 'male' },
|
||||||
|
{ key: 'male_4', name: '云夏', tag: '少年活力', gender: 'male' },
|
||||||
|
{ key: 'en_female_1', name: 'Jenny', tag: '美式甜美', gender: 'female' },
|
||||||
|
{ key: 'en_female_2', name: 'Aria', tag: '美式知性', gender: 'female' },
|
||||||
|
{ key: 'en_female_3', name: 'Sonia', tag: '英式优雅', gender: 'female' },
|
||||||
|
{ key: 'en_male_1', name: 'Guy', tag: '美式磁性', gender: 'male' },
|
||||||
|
{ key: 'en_male_2', name: 'Ryan', tag: '英式绅士', gender: 'male' },
|
||||||
|
{ key: 'ja_female_1', name: '七海', tag: '日式温柔', gender: 'female' },
|
||||||
|
{ key: 'ja_male_1', name: '圭太', tag: '日式少年', gender: 'male' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const FREE_DEFAULT_VOICE = 'female_1';
|
export const FREE_DEFAULT_VOICE = 'female_1';
|
||||||
@@ -333,3 +343,4 @@ export async function synthesizeFreeV1(params, options = {}) {
|
|||||||
|
|
||||||
return { audioBase64: data.data };
|
return { audioBase64: data.data };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
@@ -1275,7 +1275,7 @@ select.input { cursor: pointer; }
|
|||||||
<div class="tip-box" style="margin-bottom: 16px;">
|
<div class="tip-box" style="margin-bottom: 16px;">
|
||||||
<i class="fa-solid fa-info-circle"></i>
|
<i class="fa-solid fa-info-circle"></i>
|
||||||
<div>
|
<div>
|
||||||
<strong>试用音色</strong> — 无需配置,使用插件服务器(11个音色)<br>
|
<strong>试用音色</strong> — 无需配置,使用插件服务器(21个音色)<br>
|
||||||
<strong>鉴权音色</strong> — 需配置火山引擎 API(200+ 音色 + 复刻)
|
<strong>鉴权音色</strong> — 需配置火山引擎 API(200+ 音色 + 复刻)
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1719,19 +1719,30 @@ let selectedTrialVoiceValue = '';
|
|||||||
let selectedAuthVoiceValue = '';
|
let selectedAuthVoiceValue = '';
|
||||||
let editingVoiceValue = null;
|
let editingVoiceValue = null;
|
||||||
let activeSaveBtn = null;
|
let activeSaveBtn = null;
|
||||||
|
let pendingSaveRequest = null;
|
||||||
|
|
||||||
const TRIAL_VOICES = [
|
const TRIAL_VOICES = [
|
||||||
{ key: 'female_1', name: '桃夭', tag: '甜蜜仙子', gender: 'female' },
|
{ key: 'female_1', name: '晓晓', tag: '温暖百变', gender: 'female' },
|
||||||
{ key: 'female_2', name: '霜华', tag: '清冷仙子', gender: 'female' },
|
{ key: 'female_2', name: '晓伊', tag: '清冷知性', gender: 'female' },
|
||||||
{ key: 'female_3', name: '顾姐', tag: '御姐烟嗓', gender: 'female' },
|
{ key: 'female_3', name: '小北', tag: '东北甜妹', gender: 'female' },
|
||||||
{ key: 'female_4', name: '苏菲', tag: '优雅知性', gender: 'female' },
|
{ key: 'female_4', name: '小妮', tag: '陕西姑娘', gender: 'female' },
|
||||||
{ key: 'female_5', name: '嘉欣', tag: '港风甜心', gender: 'female' },
|
{ key: 'hk_female_1', name: '曉佳', tag: '粤语女声', gender: 'female' },
|
||||||
{ key: 'female_6', name: '青梅', tag: '清秀少年音', gender: 'female' },
|
{ key: 'hk_female_2', name: '曉曼', tag: '粤语温柔', gender: 'female' },
|
||||||
{ key: 'female_7', name: '可莉', tag: '奶音萝莉', gender: 'female' },
|
{ key: 'hk_male_1', name: '雲龍', tag: '粤语男声', gender: 'male' },
|
||||||
{ key: 'male_1', name: '夜枭', tag: '磁性低音', gender: 'male' },
|
{ key: 'tw_female_1', name: '曉臻', tag: '台灣女聲', gender: 'female' },
|
||||||
{ key: 'male_2', name: '君泽', tag: '温润公子', gender: 'male' },
|
{ key: 'tw_female_2', name: '曉雨', tag: '台灣温柔', gender: 'female' },
|
||||||
{ key: 'male_3', name: '沐阳', tag: '沉稳暖男', gender: 'male' },
|
{ key: 'tw_male_1', name: '雲哲', tag: '台灣男聲', gender: 'male' },
|
||||||
{ key: 'male_4', name: '梓辛', tag: '青春少年', gender: 'male' },
|
{ key: 'male_1', name: '云希', tag: '少年温暖', gender: 'male' },
|
||||||
|
{ key: 'male_2', name: '云健', tag: '阳刚有力', gender: 'male' },
|
||||||
|
{ key: 'male_3', name: '云扬', tag: '专业播报', gender: 'male' },
|
||||||
|
{ key: 'male_4', name: '云夏', tag: '少年活力', gender: 'male' },
|
||||||
|
{ key: 'en_female_1', name: 'Jenny', tag: '美式甜美', gender: 'female' },
|
||||||
|
{ key: 'en_female_2', name: 'Aria', tag: '美式知性', gender: 'female' },
|
||||||
|
{ key: 'en_female_3', name: 'Sonia', tag: '英式优雅', gender: 'female' },
|
||||||
|
{ key: 'en_male_1', name: 'Guy', tag: '美式磁性', gender: 'male' },
|
||||||
|
{ key: 'en_male_2', name: 'Ryan', tag: '英式绅士', gender: 'male' },
|
||||||
|
{ key: 'ja_female_1', name: '七海', tag: '日式温柔', gender: 'female' },
|
||||||
|
{ key: 'ja_male_1', name: '圭太', tag: '日式少年', gender: 'male' },
|
||||||
];
|
];
|
||||||
const TRIAL_VOICE_KEYS = new Set(TRIAL_VOICES.map(v => v.key));
|
const TRIAL_VOICE_KEYS = new Set(TRIAL_VOICES.map(v => v.key));
|
||||||
|
|
||||||
@@ -1781,6 +1792,25 @@ function handleSaveResult(success) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function requestSaveConfig(form, btn = null) {
|
||||||
|
const requestId = `tts_save_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
||||||
|
|
||||||
|
if (pendingSaveRequest?.timer) clearTimeout(pendingSaveRequest.timer);
|
||||||
|
if (btn) setSavingState(btn);
|
||||||
|
|
||||||
|
pendingSaveRequest = {
|
||||||
|
requestId,
|
||||||
|
timer: setTimeout(() => {
|
||||||
|
if (!pendingSaveRequest || pendingSaveRequest.requestId !== requestId) return;
|
||||||
|
pendingSaveRequest = null;
|
||||||
|
handleSaveResult(false);
|
||||||
|
post('xb-tts:toast', { type: 'error', message: '保存超时(3秒)' });
|
||||||
|
}, 3000),
|
||||||
|
};
|
||||||
|
|
||||||
|
post('xb-tts:save-config', { requestId, patch: form });
|
||||||
|
}
|
||||||
|
|
||||||
function setTestStatus(elId, status, text) {
|
function setTestStatus(elId, status, text) {
|
||||||
const el = $(elId);
|
const el = $(elId);
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
@@ -2050,7 +2080,7 @@ function bindMyVoiceEvents(listEl) {
|
|||||||
const input = btn.closest('.voice-item').querySelector('.voice-edit-input');
|
const input = btn.closest('.voice-item').querySelector('.voice-edit-input');
|
||||||
if (item && input?.value?.trim()) {
|
if (item && input?.value?.trim()) {
|
||||||
item.name = input.value.trim();
|
item.name = input.value.trim();
|
||||||
post('xb-tts:save-config', collectForm());
|
requestSaveConfig(collectForm());
|
||||||
}
|
}
|
||||||
editingVoiceValue = null;
|
editingVoiceValue = null;
|
||||||
renderMyVoiceList();
|
renderMyVoiceList();
|
||||||
@@ -2080,7 +2110,7 @@ function bindMyVoiceEvents(listEl) {
|
|||||||
renderTrialVoiceList();
|
renderTrialVoiceList();
|
||||||
renderAuthVoiceList();
|
renderAuthVoiceList();
|
||||||
updateCurrentVoiceDisplay();
|
updateCurrentVoiceDisplay();
|
||||||
post('xb-tts:save-config', collectForm());
|
requestSaveConfig(collectForm());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -2163,7 +2193,12 @@ function normalizeMySpeakers(list) {
|
|||||||
value: String(item?.value || '').trim(),
|
value: String(item?.value || '').trim(),
|
||||||
source: item?.source || getVoiceSource(item?.value || ''),
|
source: item?.source || getVoiceSource(item?.value || ''),
|
||||||
resourceId: item?.resourceId || null,
|
resourceId: item?.resourceId || null,
|
||||||
})).filter(item => item.value);
|
})).filter(item => {
|
||||||
|
if (!item.value) return false;
|
||||||
|
// Keep UI behavior aligned with runtime: remove unsupported legacy free voices.
|
||||||
|
if (item.source === 'free' && !TRIAL_VOICE_KEYS.has(item.value)) return false;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyCacheStats(stats = {}) {
|
function applyCacheStats(stats = {}) {
|
||||||
@@ -2298,11 +2333,17 @@ window.addEventListener('message', ev => {
|
|||||||
fillForm(payload);
|
fillForm(payload);
|
||||||
break;
|
break;
|
||||||
case 'xb-tts:config-saved':
|
case 'xb-tts:config-saved':
|
||||||
|
if (pendingSaveRequest?.requestId && payload?.requestId && pendingSaveRequest.requestId !== payload.requestId) break;
|
||||||
|
if (pendingSaveRequest?.timer) clearTimeout(pendingSaveRequest.timer);
|
||||||
|
pendingSaveRequest = null;
|
||||||
fillForm(payload);
|
fillForm(payload);
|
||||||
handleSaveResult(true);
|
handleSaveResult(true);
|
||||||
post('xb-tts:toast', { type: 'success', message: '配置已保存' });
|
post('xb-tts:toast', { type: 'success', message: '配置已保存' });
|
||||||
break;
|
break;
|
||||||
case 'xb-tts:config-save-error':
|
case 'xb-tts:config-save-error':
|
||||||
|
if (pendingSaveRequest?.requestId && payload?.requestId && pendingSaveRequest.requestId !== payload.requestId) break;
|
||||||
|
if (pendingSaveRequest?.timer) clearTimeout(pendingSaveRequest.timer);
|
||||||
|
pendingSaveRequest = null;
|
||||||
handleSaveResult(false);
|
handleSaveResult(false);
|
||||||
post('xb-tts:toast', { type: 'error', message: payload?.message || '保存失败' });
|
post('xb-tts:toast', { type: 'error', message: payload?.message || '保存失败' });
|
||||||
break;
|
break;
|
||||||
@@ -2417,7 +2458,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
$$('.voice-tab')[0].classList.add('active');
|
$$('.voice-tab')[0].classList.add('active');
|
||||||
$('panel-myVoice').classList.add('active');
|
$('panel-myVoice').classList.add('active');
|
||||||
|
|
||||||
post('xb-tts:save-config', collectForm());
|
requestSaveConfig(collectForm());
|
||||||
post('xb-tts:toast', { type: 'success', message: `已添加:${name}` });
|
post('xb-tts:toast', { type: 'success', message: `已添加:${name}` });
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -2441,7 +2482,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
$$('.voice-tab')[0].classList.add('active');
|
$$('.voice-tab')[0].classList.add('active');
|
||||||
$('panel-myVoice').classList.add('active');
|
$('panel-myVoice').classList.add('active');
|
||||||
|
|
||||||
post('xb-tts:save-config', collectForm());
|
requestSaveConfig(collectForm());
|
||||||
post('xb-tts:toast', { type: 'success', message: `已添加:${name}` });
|
post('xb-tts:toast', { type: 'success', message: `已添加:${name}` });
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -2460,12 +2501,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
renderMyVoiceList();
|
renderMyVoiceList();
|
||||||
updateCurrentVoiceDisplay();
|
updateCurrentVoiceDisplay();
|
||||||
post('xb-tts:save-config', collectForm());
|
requestSaveConfig(collectForm());
|
||||||
post('xb-tts:toast', { type: 'success', message: `已添加:${name || id}` });
|
post('xb-tts:toast', { type: 'success', message: `已添加:${name || id}` });
|
||||||
});
|
});
|
||||||
|
|
||||||
['saveConfigBtn', 'saveVoiceBtn', 'saveAdvancedBtn', 'saveCacheBtn'].forEach(id => {
|
['saveConfigBtn', 'saveVoiceBtn', 'saveAdvancedBtn', 'saveCacheBtn'].forEach(id => {
|
||||||
$(id)?.addEventListener('click', () => { setSavingState($(id)); post('xb-tts:save-config', collectForm()); });
|
$(id)?.addEventListener('click', () => { requestSaveConfig(collectForm(), $(id)); });
|
||||||
});
|
});
|
||||||
|
|
||||||
$('cacheRefreshBtn').addEventListener('click', () => post('xb-tts:cache-refresh'));
|
$('cacheRefreshBtn').addEventListener('click', () => post('xb-tts:cache-refresh'));
|
||||||
@@ -2477,3 +2518,4 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|||||||
@@ -169,7 +169,7 @@ export function parseTtsSegments(text) {
|
|||||||
|
|
||||||
// ============ 非鉴权分段切割 ============
|
// ============ 非鉴权分段切割 ============
|
||||||
|
|
||||||
const FREE_MAX_TEXT = 200;
|
const FREE_MAX_TEXT = 1000;
|
||||||
const FREE_MIN_TEXT = 50;
|
const FREE_MIN_TEXT = 50;
|
||||||
const FREE_SENTENCE_DELIMS = new Set(['。', '!', '?', '!', '?', ';', ';', '…', '.', ',', ',', '、', ':', ':']);
|
const FREE_SENTENCE_DELIMS = new Set(['。', '!', '?', '!', '?', ';', ';', '…', '.', ',', ',', '、', ':', ':']);
|
||||||
|
|
||||||
@@ -218,20 +218,98 @@ function splitTextForFree(text, maxLength = FREE_MAX_TEXT) {
|
|||||||
const chunks = [];
|
const chunks = [];
|
||||||
const paragraphs = String(text || '').split(/\n\s*\n/).map(s => s.replace(/\n+/g, '\n').trim()).filter(Boolean);
|
const paragraphs = String(text || '').split(/\n\s*\n/).map(s => s.replace(/\n+/g, '\n').trim()).filter(Boolean);
|
||||||
|
|
||||||
|
let current = '';
|
||||||
|
const pushCurrent = () => {
|
||||||
|
if (!current) return;
|
||||||
|
chunks.push(current);
|
||||||
|
current = '';
|
||||||
|
};
|
||||||
|
|
||||||
for (const para of paragraphs) {
|
for (const para of paragraphs) {
|
||||||
if (para.length <= maxLength) {
|
if (!para) continue;
|
||||||
chunks.push(para);
|
|
||||||
|
if (para.length > maxLength) {
|
||||||
|
// Flush buffered short paragraphs before handling a long paragraph.
|
||||||
|
pushCurrent();
|
||||||
|
const longParts = splitLongTextBySentence(para, maxLength);
|
||||||
|
for (const part of longParts) {
|
||||||
|
const t = String(part || '').trim();
|
||||||
|
if (!t) continue;
|
||||||
|
if (!current) {
|
||||||
|
current = t;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (current.length + t.length + 2 <= maxLength) {
|
||||||
|
current += `\n\n${t}`;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
pushCurrent();
|
||||||
|
current = t;
|
||||||
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
chunks.push(...splitLongTextBySentence(para, maxLength));
|
|
||||||
|
if (!current) {
|
||||||
|
current = para;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cross-paragraph merge: keep fewer requests while preserving paragraph boundary.
|
||||||
|
if (current.length + para.length + 2 <= maxLength) {
|
||||||
|
current += `\n\n${para}`;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
pushCurrent();
|
||||||
|
current = para;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pushCurrent();
|
||||||
return chunks;
|
return chunks;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function splitTtsSegmentsForFree(segments, maxLength = FREE_MAX_TEXT) {
|
export function splitTtsSegmentsForFree(segments, maxLength = FREE_MAX_TEXT) {
|
||||||
if (!Array.isArray(segments) || !segments.length) return [];
|
if (!Array.isArray(segments) || !segments.length) return [];
|
||||||
const out = [];
|
const normalizedSegments = [];
|
||||||
|
|
||||||
|
// In free mode, only explicit speaker directives are semantic split points.
|
||||||
|
// Adjacent segments without speaker= are merged to reduce request count.
|
||||||
|
let mergeBuffer = null;
|
||||||
|
const flushMergeBuffer = () => {
|
||||||
|
if (!mergeBuffer) return;
|
||||||
|
normalizedSegments.push(mergeBuffer);
|
||||||
|
mergeBuffer = null;
|
||||||
|
};
|
||||||
|
|
||||||
for (const seg of segments) {
|
for (const seg of segments) {
|
||||||
|
const hasExplicitSpeaker = !!String(seg?.speaker || '').trim();
|
||||||
|
const text = String(seg?.text || '').trim();
|
||||||
|
if (!text) continue;
|
||||||
|
|
||||||
|
if (hasExplicitSpeaker) {
|
||||||
|
flushMergeBuffer();
|
||||||
|
normalizedSegments.push({
|
||||||
|
...seg,
|
||||||
|
text,
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mergeBuffer) {
|
||||||
|
mergeBuffer = {
|
||||||
|
...seg,
|
||||||
|
text,
|
||||||
|
speaker: '',
|
||||||
|
};
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
mergeBuffer.text += `\n${text}`;
|
||||||
|
}
|
||||||
|
flushMergeBuffer();
|
||||||
|
|
||||||
|
const out = [];
|
||||||
|
for (const seg of normalizedSegments) {
|
||||||
const parts = splitTextForFree(seg.text, maxLength);
|
const parts = splitTextForFree(seg.text, maxLength);
|
||||||
if (!parts.length) continue;
|
if (!parts.length) continue;
|
||||||
let buffer = '';
|
let buffer = '';
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// ============ 导入 ============
|
// ============ 导入 ============
|
||||||
|
|
||||||
import { event_types } from "../../../../../../script.js";
|
import { event_types } from "../../../../../../script.js";
|
||||||
import { extension_settings, getContext } from "../../../../../extensions.js";
|
import { extension_settings, getContext } from "../../../../../extensions.js";
|
||||||
@@ -42,8 +42,12 @@ const HTML_PATH = `${extensionFolderPath}/modules/tts/tts-overlay.html`;
|
|||||||
const TTS_DIRECTIVE_REGEX = /\[tts:([^\]]*)\]/gi;
|
const TTS_DIRECTIVE_REGEX = /\[tts:([^\]]*)\]/gi;
|
||||||
|
|
||||||
const FREE_VOICE_KEYS = new Set([
|
const FREE_VOICE_KEYS = new Set([
|
||||||
'female_1', 'female_2', 'female_3', 'female_4', 'female_5', 'female_6', 'female_7',
|
'female_1', 'female_2', 'female_3', 'female_4',
|
||||||
'male_1', 'male_2', 'male_3', 'male_4'
|
'hk_female_1', 'hk_female_2', 'hk_male_1',
|
||||||
|
'tw_female_1', 'tw_female_2', 'tw_male_1',
|
||||||
|
'male_1', 'male_2', 'male_3', 'male_4',
|
||||||
|
'en_female_1', 'en_female_2', 'en_female_3', 'en_male_1', 'en_male_2',
|
||||||
|
'ja_female_1', 'ja_male_1',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// ============ NovelDraw 兼容 ============
|
// ============ NovelDraw 兼容 ============
|
||||||
@@ -913,11 +917,26 @@ async function loadConfig() {
|
|||||||
config = await TtsStorage.load();
|
config = await TtsStorage.load();
|
||||||
config.volc = config.volc || {};
|
config.volc = config.volc || {};
|
||||||
|
|
||||||
|
let legacyPurged = false;
|
||||||
if (Array.isArray(config.volc.mySpeakers)) {
|
if (Array.isArray(config.volc.mySpeakers)) {
|
||||||
config.volc.mySpeakers = config.volc.mySpeakers.map(s => ({
|
const normalized = config.volc.mySpeakers.map(s => ({
|
||||||
...s,
|
...s,
|
||||||
source: s.source || getVoiceSource(s.value)
|
source: s.source || getVoiceSource(s.value)
|
||||||
}));
|
}));
|
||||||
|
const filtered = normalized.filter(s => {
|
||||||
|
// Purge legacy free voices that are no longer supported by the current free voice map.
|
||||||
|
if (s.source === 'free' && !FREE_VOICE_KEYS.has(s.value)) {
|
||||||
|
legacyPurged = true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
config.volc.mySpeakers = filtered;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.volc.defaultSpeaker && getVoiceSource(config.volc.defaultSpeaker) === 'free' && !FREE_VOICE_KEYS.has(config.volc.defaultSpeaker)) {
|
||||||
|
config.volc.defaultSpeaker = FREE_DEFAULT_VOICE;
|
||||||
|
legacyPurged = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
config.volc.disableMarkdownFilter = config.volc.disableMarkdownFilter !== false;
|
config.volc.disableMarkdownFilter = config.volc.disableMarkdownFilter !== false;
|
||||||
@@ -943,6 +962,12 @@ async function loadConfig() {
|
|||||||
config.showFloorButton = config.showFloorButton !== false;
|
config.showFloorButton = config.showFloorButton !== false;
|
||||||
config.showFloatingButton = config.showFloatingButton === true;
|
config.showFloatingButton = config.showFloatingButton === true;
|
||||||
|
|
||||||
|
if (legacyPurged) {
|
||||||
|
await TtsStorage.set('volc', config.volc);
|
||||||
|
await TtsStorage.saveNow({ silent: true });
|
||||||
|
console.info('[TTS] Purged legacy free voices from mySpeakers.');
|
||||||
|
}
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1054,15 +1079,17 @@ async function handleIframeMessage(ev) {
|
|||||||
closeSettings();
|
closeSettings();
|
||||||
break;
|
break;
|
||||||
case 'xb-tts:save-config': {
|
case 'xb-tts:save-config': {
|
||||||
const ok = await saveConfig(payload);
|
const requestId = payload?.requestId || '';
|
||||||
|
const patch = (payload && typeof payload.patch === 'object') ? payload.patch : payload;
|
||||||
|
const ok = await saveConfig(patch);
|
||||||
if (ok) {
|
if (ok) {
|
||||||
const cacheStats = await getCacheStatsSafe();
|
const cacheStats = await getCacheStatsSafe();
|
||||||
postToIframe(iframe, { type: 'xb-tts:config-saved', payload: { ...config, cacheStats } });
|
postToIframe(iframe, { type: 'xb-tts:config-saved', payload: { ...config, cacheStats, requestId } });
|
||||||
updateAutoSpeakAll();
|
updateAutoSpeakAll();
|
||||||
updateSpeedAll();
|
updateSpeedAll();
|
||||||
updateVoiceAll();
|
updateVoiceAll();
|
||||||
} else {
|
} else {
|
||||||
postToIframe(iframe, { type: 'xb-tts:config-save-error', payload: { message: '保存失败' } });
|
postToIframe(iframe, { type: 'xb-tts:config-save-error', payload: { message: '保存失败', requestId } });
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -1472,3 +1499,4 @@ export function cleanupTts() {
|
|||||||
cacheCounters.misses = 0;
|
cacheCounters.misses = 0;
|
||||||
delete window.xiaobaixTts;
|
delete window.xiaobaixTts;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -206,6 +206,14 @@
|
|||||||
<input type="checkbox" id="xiaobaix_story_outline_enabled" />
|
<input type="checkbox" id="xiaobaix_story_outline_enabled" />
|
||||||
<label for="xiaobaix_story_outline_enabled" class="has-tooltip" data-tooltip="在X按钮区域添加地图图标,点击可打开可视化剧情地图编辑器">小白板</label>
|
<label for="xiaobaix_story_outline_enabled" class="has-tooltip" data-tooltip="在X按钮区域添加地图图标,点击可打开可视化剧情地图编辑器">小白板</label>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex-container">
|
||||||
|
<input type="checkbox" id="xiaobaix_ena_planner_enabled" />
|
||||||
|
<label for="xiaobaix_ena_planner_enabled" class="has-tooltip" data-tooltip="发送前剧情规划,自动注入 plot/note">剧情规划</label>
|
||||||
|
<button id="xiaobaix_ena_planner_open_settings" class="menu_button menu_button_icon" type="button" style="margin-left:auto;" title="打开剧情规划设置">
|
||||||
|
<i class="fa-solid fa-compass-drafting"></i>
|
||||||
|
<small>规划设置</small>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<br>
|
<br>
|
||||||
<div class="section-divider">变量控制</div>
|
<div class="section-divider">变量控制</div>
|
||||||
<hr class="sysHR" />
|
<hr class="sysHR" />
|
||||||
@@ -519,14 +527,15 @@
|
|||||||
audio: 'xiaobaix_audio_enabled',
|
audio: 'xiaobaix_audio_enabled',
|
||||||
storySummary: 'xiaobaix_story_summary_enabled',
|
storySummary: 'xiaobaix_story_summary_enabled',
|
||||||
tts: 'xiaobaix_tts_enabled',
|
tts: 'xiaobaix_tts_enabled',
|
||||||
|
enaPlanner: 'xiaobaix_ena_planner_enabled',
|
||||||
storyOutline: 'xiaobaix_story_outline_enabled',
|
storyOutline: 'xiaobaix_story_outline_enabled',
|
||||||
useBlob: 'xiaobaix_use_blob',
|
useBlob: 'xiaobaix_use_blob',
|
||||||
wrapperIframe: 'Wrapperiframe',
|
wrapperIframe: 'Wrapperiframe',
|
||||||
renderEnabled: 'xiaobaix_render_enabled',
|
renderEnabled: 'xiaobaix_render_enabled',
|
||||||
};
|
};
|
||||||
const DEFAULTS_ON = ['templateEditor', 'tasks', 'variablesCore', 'audio', 'storySummary', 'recorded'];
|
const DEFAULTS_ON = ['templateEditor', 'tasks', 'variablesCore', 'audio', 'storySummary', 'recorded'];
|
||||||
const DEFAULTS_OFF = ['preview', 'scriptAssistant', 'immersive', 'variablesPanel', 'fourthWall', 'storyOutline', 'novelDraw', 'tts'];
|
const DEFAULTS_OFF = ['preview', 'scriptAssistant', 'immersive', 'variablesPanel', 'fourthWall', 'storyOutline', 'novelDraw', 'tts', 'enaPlanner'];
|
||||||
const MODULE_KEYS = ['templateEditor', 'tasks', 'fourthWall', 'variablesCore', 'recorded', 'preview', 'scriptAssistant', 'immersive', 'variablesPanel', 'audio', 'storySummary', 'storyOutline', 'novelDraw', 'tts'];
|
const MODULE_KEYS = ['templateEditor', 'tasks', 'fourthWall', 'variablesCore', 'recorded', 'preview', 'scriptAssistant', 'immersive', 'variablesPanel', 'audio', 'storySummary', 'storyOutline', 'novelDraw', 'tts', 'enaPlanner'];
|
||||||
function setModuleEnabled(key, enabled) {
|
function setModuleEnabled(key, enabled) {
|
||||||
try {
|
try {
|
||||||
if (!extension_settings[EXT_ID][key]) extension_settings[EXT_ID][key] = {};
|
if (!extension_settings[EXT_ID][key]) extension_settings[EXT_ID][key] = {};
|
||||||
|
|||||||
Reference in New Issue
Block a user