Initial commit
This commit is contained in:
1010
modules/variables/var-commands.js
Normal file
1010
modules/variables/var-commands.js
Normal file
File diff suppressed because it is too large
Load Diff
723
modules/variables/varevent-editor.js
Normal file
723
modules/variables/varevent-editor.js
Normal file
@@ -0,0 +1,723 @@
|
||||
/**
|
||||
* @file modules/variables/varevent-editor.js
|
||||
* @description 条件规则编辑器与 varevent 运行时(常驻模块)
|
||||
*/
|
||||
|
||||
import { getContext } from "../../../../../extensions.js";
|
||||
import { getLocalVariable } from "../../../../../variables.js";
|
||||
import { createModuleEvents } from "../../core/event-manager.js";
|
||||
import { replaceXbGetVarInString } from "./var-commands.js";
|
||||
|
||||
const MODULE_ID = 'vareventEditor';
|
||||
const LWB_EXT_ID = 'LittleWhiteBox';
|
||||
const LWB_VAREVENT_PROMPT_KEY = 'LWB_varevent_display';
|
||||
const EDITOR_STYLES_ID = 'lwb-varevent-editor-styles';
|
||||
const TAG_RE_VAREVENT = /<\s*varevent[^>]*>([\s\S]*?)<\s*\/\s*varevent\s*>/gi;
|
||||
|
||||
const OP_ALIASES = {
|
||||
set: ['set', '记下', '記下', '记录', '記錄', '录入', '錄入', 'record'],
|
||||
push: ['push', '添入', '增录', '增錄', '追加', 'append'],
|
||||
bump: ['bump', '推移', '变更', '變更', '调整', '調整', 'adjust'],
|
||||
del: ['del', '遗忘', '遺忘', '抹去', '删除', '刪除', 'erase'],
|
||||
};
|
||||
const OP_MAP = {};
|
||||
for (const [k, arr] of Object.entries(OP_ALIASES)) for (const a of arr) OP_MAP[a.toLowerCase()] = k;
|
||||
const reEscape = (s) => String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
const ALL_OP_WORDS = Object.values(OP_ALIASES).flat();
|
||||
const OP_WORDS_PATTERN = ALL_OP_WORDS.map(reEscape).sort((a, b) => b.length - a.length).join('|');
|
||||
const TOP_OP_RE = new RegExp(`^(${OP_WORDS_PATTERN})\\s*:\\s*$`, 'i');
|
||||
|
||||
let events = null;
|
||||
let initialized = false;
|
||||
let origEmitMap = new WeakMap();
|
||||
|
||||
function debounce(fn, wait = 100) { let t = null; return (...args) => { clearTimeout(t); t = setTimeout(() => fn.apply(null, args), wait); }; }
|
||||
|
||||
function stripYamlInlineComment(s) {
|
||||
const text = String(s ?? ''); if (!text) return '';
|
||||
let inSingle = false, inDouble = false, escaped = false;
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const ch = text[i];
|
||||
if (inSingle) { if (ch === "'") { if (text[i + 1] === "'") { i++; continue; } inSingle = false; } continue; }
|
||||
if (inDouble) { if (escaped) { escaped = false; continue; } if (ch === '\\') { escaped = true; continue; } if (ch === '"') inDouble = false; continue; }
|
||||
if (ch === "'") { inSingle = true; continue; }
|
||||
if (ch === '"') { inDouble = true; continue; }
|
||||
if (ch === '#') { const prev = i > 0 ? text[i - 1] : ''; if (i === 0 || /\s/.test(prev)) return text.slice(0, i); }
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
function readCharExtBumpAliases() {
|
||||
try {
|
||||
const ctx = getContext(); const id = ctx?.characterId ?? ctx?.this_chid; if (id == null) return {};
|
||||
const char = ctx?.getCharacter?.(id) ?? (Array.isArray(ctx?.characters) ? ctx.characters[id] : null);
|
||||
const bump = char?.data?.extensions?.[LWB_EXT_ID]?.variablesCore?.bumpAliases;
|
||||
if (bump && typeof bump === 'object') return bump;
|
||||
const legacy = char?.extensions?.[LWB_EXT_ID]?.variablesCore?.bumpAliases;
|
||||
if (legacy && typeof legacy === 'object') { writeCharExtBumpAliases(legacy); return legacy; }
|
||||
return {};
|
||||
} catch { return {}; }
|
||||
}
|
||||
|
||||
async function writeCharExtBumpAliases(newStore) {
|
||||
try {
|
||||
const ctx = getContext(); const id = ctx?.characterId ?? ctx?.this_chid; if (id == null) return;
|
||||
if (typeof ctx?.writeExtensionField === 'function') {
|
||||
await ctx.writeExtensionField(id, LWB_EXT_ID, { variablesCore: { bumpAliases: structuredClone(newStore || {}) } });
|
||||
const char = ctx?.getCharacter?.(id) ?? (Array.isArray(ctx?.characters) ? ctx.characters[id] : null);
|
||||
if (char) {
|
||||
char.data = char.data && typeof char.data === 'object' ? char.data : {};
|
||||
char.data.extensions = char.data.extensions && typeof char.data.extensions === 'object' ? char.data.extensions : {};
|
||||
const ns = (char.data.extensions[LWB_EXT_ID] ||= {});
|
||||
ns.variablesCore = ns.variablesCore && typeof ns.variablesCore === 'object' ? ns.variablesCore : {};
|
||||
ns.variablesCore.bumpAliases = structuredClone(newStore || {});
|
||||
}
|
||||
typeof ctx?.saveCharacter === 'function' ? await ctx.saveCharacter() : ctx?.saveCharacterDebounced?.();
|
||||
return;
|
||||
}
|
||||
const char = ctx?.getCharacter?.(id) ?? (Array.isArray(ctx?.characters) ? ctx.characters[id] : null);
|
||||
if (char) {
|
||||
char.data = char.data && typeof char.data === 'object' ? char.data : {};
|
||||
char.data.extensions = char.data.extensions && typeof char.data.extensions === 'object' ? char.data.extensions : {};
|
||||
const ns = (char.data.extensions[LWB_EXT_ID] ||= {});
|
||||
ns.variablesCore = ns.variablesCore && typeof ns.variablesCore === 'object' ? ns.variablesCore : {};
|
||||
ns.variablesCore.bumpAliases = structuredClone(newStore || {});
|
||||
}
|
||||
typeof ctx?.saveCharacter === 'function' ? await ctx.saveCharacter() : ctx?.saveCharacterDebounced?.();
|
||||
} catch {}
|
||||
}
|
||||
|
||||
export function getBumpAliasStore() { return readCharExtBumpAliases(); }
|
||||
export async function setBumpAliasStore(newStore) { await writeCharExtBumpAliases(newStore); }
|
||||
export async function clearBumpAliasStore() { await writeCharExtBumpAliases({}); }
|
||||
|
||||
function getBumpAliasMap() { try { return getBumpAliasStore(); } catch { return {}; } }
|
||||
|
||||
function matchAlias(varOrKey, rhs) {
|
||||
const map = getBumpAliasMap();
|
||||
for (const scope of [map._global || {}, map[varOrKey] || {}]) {
|
||||
for (const [k, v] of Object.entries(scope)) {
|
||||
if (k.startsWith('/') && k.lastIndexOf('/') > 0) {
|
||||
const last = k.lastIndexOf('/');
|
||||
try { if (new RegExp(k.slice(1, last), k.slice(last + 1)).test(rhs)) return Number(v); } catch {}
|
||||
} else if (rhs === k) return Number(v);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function preprocessBumpAliases(innerText) {
|
||||
const lines = String(innerText || '').split(/\r?\n/), out = [];
|
||||
let inBump = false; const indentOf = (s) => s.length - s.trimStart().length;
|
||||
const stack = []; let currentVarRoot = '';
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const raw = lines[i], t = raw.trim();
|
||||
if (!t) { out.push(raw); continue; }
|
||||
const ind = indentOf(raw), mTop = TOP_OP_RE.exec(t);
|
||||
if (mTop && ind === 0) { const opKey = OP_MAP[mTop[1].toLowerCase()] || ''; inBump = opKey === 'bump'; stack.length = 0; currentVarRoot = ''; out.push(raw); continue; }
|
||||
if (!inBump) { out.push(raw); continue; }
|
||||
while (stack.length && stack[stack.length - 1].indent >= ind) stack.pop();
|
||||
const mKV = t.match(/^([^:]+):\s*(.*)$/);
|
||||
if (mKV) {
|
||||
const key = mKV[1].trim(), val = String(stripYamlInlineComment(mKV[2])).trim();
|
||||
const parentPath = stack.length ? stack[stack.length - 1].path : '', curPath = parentPath ? `${parentPath}.${key}` : key;
|
||||
if (val === '') { stack.push({ indent: ind, path: curPath }); if (!parentPath) currentVarRoot = key; out.push(raw); continue; }
|
||||
let rhs = val.replace(/^["']|["']$/g, '');
|
||||
const num = matchAlias(key, rhs) ?? matchAlias(currentVarRoot, rhs) ?? matchAlias('', rhs);
|
||||
out.push(num !== null && Number.isFinite(num) ? raw.replace(/:\s*.*$/, `: ${num}`) : raw); continue;
|
||||
}
|
||||
const mArr = t.match(/^-\s*(.+)$/);
|
||||
if (mArr) {
|
||||
let rhs = String(stripYamlInlineComment(mArr[1])).trim().replace(/^["']|["']$/g, '');
|
||||
const leafKey = stack.length ? stack[stack.length - 1].path.split('.').pop() : '';
|
||||
const num = matchAlias(leafKey || currentVarRoot, rhs) ?? matchAlias(currentVarRoot, rhs) ?? matchAlias('', rhs);
|
||||
out.push(num !== null && Number.isFinite(num) ? raw.replace(/-\s*.*$/, `- ${num}`) : raw); continue;
|
||||
}
|
||||
out.push(raw);
|
||||
}
|
||||
return out.join('\n');
|
||||
}
|
||||
|
||||
export function parseVareventEvents(innerText) {
|
||||
const evts = [], lines = String(innerText || '').split(/\r?\n/);
|
||||
let cur = null;
|
||||
const flush = () => { if (cur) { evts.push(cur); cur = null; } };
|
||||
const isStopLine = (t) => !t ? false : /^\[\s*event\.[^\]]+]\s*$/i.test(t) || /^(condition|display|js_execute)\s*:/i.test(t) || /^<\s*\/\s*varevent\s*>/i.test(t);
|
||||
const findUnescapedQuote = (str, q) => { for (let i = 0; i < str.length; i++) if (str[i] === q && str[i - 1] !== '\\') return i; return -1; };
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const raw = lines[i], line = raw.trim(); if (!line) continue;
|
||||
const header = /^\[\s*event\.([^\]]+)]\s*$/i.exec(line);
|
||||
if (header) { flush(); cur = { id: String(header[1]).trim() }; continue; }
|
||||
const m = /^(condition|display|js_execute)\s*:\s*(.*)$/i.exec(line);
|
||||
if (m) {
|
||||
const key = m[1].toLowerCase(); let valPart = m[2] ?? ''; if (!cur) cur = {};
|
||||
let value = ''; const ltrim = valPart.replace(/^\s+/, ''), firstCh = ltrim[0];
|
||||
if (firstCh === '"' || firstCh === "'") {
|
||||
const quote = firstCh; let after = ltrim.slice(1), endIdx = findUnescapedQuote(after, quote);
|
||||
if (endIdx !== -1) value = after.slice(0, endIdx);
|
||||
else { value = after + '\n'; while (++i < lines.length) { const ln = lines[i], pos = findUnescapedQuote(ln, quote); if (pos !== -1) { value += ln.slice(0, pos); break; } value += ln + '\n'; } }
|
||||
value = value.replace(/\\"/g, '"').replace(/\\'/g, "'");
|
||||
} else { value = valPart; let j = i + 1; while (j < lines.length) { const nextTrim = lines[j].trim(); if (isStopLine(nextTrim)) break; value += '\n' + lines[j]; j++; } i = j - 1; }
|
||||
if (key === 'condition') cur.condition = value; else if (key === 'display') cur.display = value; else if (key === 'js_execute') cur.js = value;
|
||||
}
|
||||
}
|
||||
flush(); return evts;
|
||||
}
|
||||
|
||||
export function evaluateCondition(expr) {
|
||||
const isNumericLike = (v) => v != null && /^-?\d+(?:\.\d+)?$/.test(String(v).trim());
|
||||
// Used by eval() expression; keep in scope.
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
function VAR(path) {
|
||||
try {
|
||||
const p = String(path ?? '').replace(/\[(\d+)\]/g, '.$1'), seg = p.split('.').map(s => s.trim()).filter(Boolean);
|
||||
if (!seg.length) return ''; const root = getLocalVariable(seg[0]);
|
||||
if (seg.length === 1) { if (root == null) return ''; return typeof root === 'object' ? JSON.stringify(root) : String(root); }
|
||||
let obj; if (typeof root === 'string') { try { obj = JSON.parse(root); } catch { return undefined; } } else if (root && typeof root === 'object') obj = root; else return undefined;
|
||||
let cur = obj; for (let i = 1; i < seg.length; i++) { cur = cur?.[/^\d+$/.test(seg[i]) ? Number(seg[i]) : seg[i]]; if (cur === undefined) return undefined; }
|
||||
return cur == null ? '' : typeof cur === 'object' ? JSON.stringify(cur) : String(cur);
|
||||
} catch { return undefined; }
|
||||
}
|
||||
// Used by eval() expression; keep in scope.
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const VAL = (t) => String(t ?? '');
|
||||
// Used by eval() expression; keep in scope.
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
function REL(a, op, b) {
|
||||
if (isNumericLike(a) && isNumericLike(b)) { const A = Number(String(a).trim()), B = Number(String(b).trim()); if (op === '>') return A > B; if (op === '>=') return A >= B; if (op === '<') return A < B; if (op === '<=') return A <= B; }
|
||||
else { const A = String(a), B = String(b); if (op === '>') return A > B; if (op === '>=') return A >= B; if (op === '<') return A < B; if (op === '<=') return A <= B; }
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
let processed = expr.replace(/var\(`([^`]+)`\)/g, 'VAR("$1")').replace(/val\(`([^`]+)`\)/g, 'VAL("$1")');
|
||||
processed = processed.replace(/(VAR\(".*?"\)|VAL\(".*?"\))\s*(>=|<=|>|<)\s*(VAR\(".*?"\)|VAL\(".*?"\))/g, 'REL($1,"$2",$3)');
|
||||
// eslint-disable-next-line no-eval -- intentional: user-defined expression evaluation
|
||||
return !!eval(processed);
|
||||
} catch { return false; }
|
||||
}
|
||||
|
||||
export async function runJS(code) {
|
||||
const ctx = getContext();
|
||||
try {
|
||||
const STscriptProxy = async (command) => { if (!command) return; if (command[0] !== '/') command = '/' + command; const { executeSlashCommands, substituteParams } = getContext(); return await executeSlashCommands?.(substituteParams ? substituteParams(command) : command, true); };
|
||||
// eslint-disable-next-line no-new-func -- intentional: user-defined async script
|
||||
const fn = new Function('ctx', 'getVar', 'setVar', 'console', 'STscript', `return (async()=>{ ${code}\n })();`);
|
||||
const getVar = (k) => getLocalVariable(k);
|
||||
const setVar = (k, v) => { getContext()?.variables?.local?.set?.(k, v); };
|
||||
return await fn(ctx, getVar, setVar, console, (typeof window !== 'undefined' && window?.STscript) || STscriptProxy);
|
||||
} catch (err) { console.error('[LWB:runJS]', err); }
|
||||
}
|
||||
|
||||
export async function runST(code) {
|
||||
try { if (!code) return; if (code[0] !== '/') code = '/' + code; const { executeSlashCommands, substituteParams } = getContext() || {}; return await executeSlashCommands?.(substituteParams ? substituteParams(code) : code, true); }
|
||||
catch (err) { console.error('[LWB:runST]', err); }
|
||||
}
|
||||
|
||||
async function buildVareventReplacement(innerText, dryRun, executeJs = false) {
|
||||
try {
|
||||
const evts = parseVareventEvents(innerText); if (!evts.length) return '';
|
||||
let chosen = null;
|
||||
for (let i = evts.length - 1; i >= 0; i--) {
|
||||
const ev = evts[i], condStr = String(ev.condition ?? '').trim(), condOk = condStr ? evaluateCondition(condStr) : true;
|
||||
if (!((ev.display && String(ev.display).trim()) || (ev.js && String(ev.js).trim()))) continue;
|
||||
if (condOk) { chosen = ev; break; }
|
||||
}
|
||||
if (!chosen) return '';
|
||||
let out = chosen.display ? String(chosen.display).replace(/^\n+/, '').replace(/\n+$/, '') : '';
|
||||
if (!dryRun && executeJs && chosen.js && String(chosen.js).trim()) { try { await runJS(chosen.js); } catch {} }
|
||||
return out;
|
||||
} catch { return ''; }
|
||||
}
|
||||
|
||||
export async function replaceVareventInString(text, dryRun = false, executeJs = false) {
|
||||
if (!text || text.indexOf('<varevent') === -1) return text;
|
||||
const replaceAsync = async (input, regex, repl) => { let out = '', last = 0; regex.lastIndex = 0; let m; while ((m = regex.exec(input))) { out += input.slice(last, m.index); out += await repl(...m); last = regex.lastIndex; } return out + input.slice(last); };
|
||||
return await replaceAsync(text, TAG_RE_VAREVENT, (m, inner) => buildVareventReplacement(inner, dryRun, executeJs));
|
||||
}
|
||||
|
||||
export function enqueuePendingVareventBlock(innerText, sourceInfo) {
|
||||
try { const ctx = getContext(), meta = ctx?.chatMetadata || {}, list = (meta.LWB_PENDING_VAREVENT_BLOCKS ||= []); list.push({ inner: String(innerText || ''), source: sourceInfo || 'unknown', turn: (ctx?.chat?.length ?? 0), ts: Date.now() }); ctx?.saveMetadataDebounced?.(); } catch {}
|
||||
}
|
||||
|
||||
export function drainPendingVareventBlocks() {
|
||||
try { const ctx = getContext(), meta = ctx?.chatMetadata || {}, list = Array.isArray(meta.LWB_PENDING_VAREVENT_BLOCKS) ? meta.LWB_PENDING_VAREVENT_BLOCKS.slice() : []; meta.LWB_PENDING_VAREVENT_BLOCKS = []; ctx?.saveMetadataDebounced?.(); return list; } catch { return []; }
|
||||
}
|
||||
|
||||
export async function executeQueuedVareventJsAfterTurn() {
|
||||
const blocks = drainPendingVareventBlocks(); if (!blocks.length) return;
|
||||
for (const item of blocks) {
|
||||
try {
|
||||
const evts = parseVareventEvents(item.inner); if (!evts.length) continue;
|
||||
let chosen = null;
|
||||
for (let j = evts.length - 1; j >= 0; j--) { const ev = evts[j], condStr = String(ev.condition ?? '').trim(); if (!(condStr ? evaluateCondition(condStr) : true)) continue; if (!(ev.js && String(ev.js).trim())) continue; chosen = ev; break; }
|
||||
if (chosen) { try { await runJS(String(chosen.js ?? '').trim()); } catch {} }
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
let _scanRunning = false;
|
||||
async function runImmediateVarEvents() {
|
||||
if (_scanRunning) return; _scanRunning = true;
|
||||
try {
|
||||
const wiList = getContext()?.world_info || [];
|
||||
for (const entry of wiList) {
|
||||
const content = String(entry?.content ?? ''); if (!content || content.indexOf('<varevent') === -1) continue;
|
||||
TAG_RE_VAREVENT.lastIndex = 0; let m;
|
||||
while ((m = TAG_RE_VAREVENT.exec(content)) !== null) {
|
||||
const evts = parseVareventEvents(m[1] ?? '');
|
||||
for (const ev of evts) { if (!(String(ev.condition ?? '').trim() ? evaluateCondition(String(ev.condition ?? '').trim()) : true)) continue; if (String(ev.display ?? '').trim()) await runST(`/sys "${String(ev.display ?? '').trim().replace(/"/g, '\\"')}"`); if (String(ev.js ?? '').trim()) await runJS(String(ev.js ?? '').trim()); }
|
||||
}
|
||||
}
|
||||
} catch {} finally { setTimeout(() => { _scanRunning = false; }, 0); }
|
||||
}
|
||||
const runImmediateVarEventsDebounced = debounce(runImmediateVarEvents, 30);
|
||||
|
||||
function installWIHiddenTagStripper() {
|
||||
const ctx = getContext(), ext = ctx?.extensionSettings; if (!ext) return;
|
||||
ext.regex = Array.isArray(ext.regex) ? ext.regex : [];
|
||||
ext.regex = ext.regex.filter(r => !['lwb-varevent-stripper', 'lwb-varevent-replacer'].includes(r?.id) && !['LWB_VarEventStripper', 'LWB_VarEventReplacer'].includes(r?.scriptName));
|
||||
ctx?.saveSettingsDebounced?.();
|
||||
}
|
||||
|
||||
function registerWIEventSystem() {
|
||||
const { eventSource, event_types: evtTypes } = getContext() || {};
|
||||
if (evtTypes?.CHAT_COMPLETION_PROMPT_READY) {
|
||||
const lateChatReplacementHandler = async (data) => {
|
||||
try {
|
||||
if (data?.dryRun) return;
|
||||
const chat = data?.chat;
|
||||
if (!Array.isArray(chat)) return;
|
||||
for (const msg of chat) {
|
||||
if (typeof msg?.content === 'string') {
|
||||
if (msg.content.includes('<varevent')) {
|
||||
TAG_RE_VAREVENT.lastIndex = 0;
|
||||
let mm;
|
||||
while ((mm = TAG_RE_VAREVENT.exec(msg.content)) !== null) {
|
||||
enqueuePendingVareventBlock(mm[1] ?? '', 'chat.content');
|
||||
}
|
||||
msg.content = await replaceVareventInString(msg.content, false, false);
|
||||
}
|
||||
if (msg.content.indexOf('{{xbgetvar::') !== -1) {
|
||||
msg.content = replaceXbGetVarInString(msg.content);
|
||||
}
|
||||
}
|
||||
if (Array.isArray(msg?.content)) {
|
||||
for (const part of msg.content) {
|
||||
if (part?.type === 'text' && typeof part.text === 'string') {
|
||||
if (part.text.includes('<varevent')) {
|
||||
TAG_RE_VAREVENT.lastIndex = 0;
|
||||
let mm;
|
||||
while ((mm = TAG_RE_VAREVENT.exec(part.text)) !== null) {
|
||||
enqueuePendingVareventBlock(mm[1] ?? '', 'chat.content[].text');
|
||||
}
|
||||
part.text = await replaceVareventInString(part.text, false, false);
|
||||
}
|
||||
if (part.text.indexOf('{{xbgetvar::') !== -1) {
|
||||
part.text = replaceXbGetVarInString(part.text);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (typeof msg?.mes === 'string') {
|
||||
if (msg.mes.includes('<varevent')) {
|
||||
TAG_RE_VAREVENT.lastIndex = 0;
|
||||
let mm;
|
||||
while ((mm = TAG_RE_VAREVENT.exec(msg.mes)) !== null) {
|
||||
enqueuePendingVareventBlock(mm[1] ?? '', 'chat.mes');
|
||||
}
|
||||
msg.mes = await replaceVareventInString(msg.mes, false, false);
|
||||
}
|
||||
if (msg.mes.indexOf('{{xbgetvar::') !== -1) {
|
||||
msg.mes = replaceXbGetVarInString(msg.mes);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
};
|
||||
try {
|
||||
if (eventSource && typeof eventSource.makeLast === 'function') {
|
||||
eventSource.makeLast(evtTypes.CHAT_COMPLETION_PROMPT_READY, lateChatReplacementHandler);
|
||||
} else {
|
||||
events?.on(evtTypes.CHAT_COMPLETION_PROMPT_READY, lateChatReplacementHandler);
|
||||
}
|
||||
} catch {
|
||||
events?.on(evtTypes.CHAT_COMPLETION_PROMPT_READY, lateChatReplacementHandler);
|
||||
}
|
||||
}
|
||||
if (evtTypes?.GENERATE_AFTER_COMBINE_PROMPTS) {
|
||||
events?.on(evtTypes.GENERATE_AFTER_COMBINE_PROMPTS, async (data) => {
|
||||
try {
|
||||
if (data?.dryRun) return;
|
||||
|
||||
if (typeof data?.prompt === 'string') {
|
||||
if (data.prompt.includes('<varevent')) {
|
||||
TAG_RE_VAREVENT.lastIndex = 0;
|
||||
let mm;
|
||||
while ((mm = TAG_RE_VAREVENT.exec(data.prompt)) !== null) {
|
||||
enqueuePendingVareventBlock(mm[1] ?? '', 'prompt');
|
||||
}
|
||||
data.prompt = await replaceVareventInString(data.prompt, false, false);
|
||||
}
|
||||
if (data.prompt.indexOf('{{xbgetvar::') !== -1) {
|
||||
data.prompt = replaceXbGetVarInString(data.prompt);
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
});
|
||||
}
|
||||
if (evtTypes?.GENERATION_ENDED) {
|
||||
events?.on(evtTypes.GENERATION_ENDED, async () => {
|
||||
try {
|
||||
getContext()?.setExtensionPrompt?.(LWB_VAREVENT_PROMPT_KEY, '', 0, 0, false);
|
||||
const ctx = getContext();
|
||||
const chat = ctx?.chat || [];
|
||||
const lastMsg = chat[chat.length - 1];
|
||||
if (lastMsg && !lastMsg.is_user) {
|
||||
await executeQueuedVareventJsAfterTurn();
|
||||
} else {
|
||||
|
||||
drainPendingVareventBlocks();
|
||||
}
|
||||
} catch {}
|
||||
});
|
||||
}
|
||||
if (evtTypes?.CHAT_CHANGED) {
|
||||
events?.on(evtTypes.CHAT_CHANGED, () => {
|
||||
try {
|
||||
getContext()?.setExtensionPrompt?.(LWB_VAREVENT_PROMPT_KEY, '', 0, 0, false);
|
||||
drainPendingVareventBlocks();
|
||||
runImmediateVarEventsDebounced();
|
||||
} catch {}
|
||||
});
|
||||
}
|
||||
if (evtTypes?.APP_READY) {
|
||||
events?.on(evtTypes.APP_READY, () => {
|
||||
try {
|
||||
runImmediateVarEventsDebounced();
|
||||
} catch {}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const LWBVE = { installed: false, obs: null };
|
||||
|
||||
function injectEditorStyles() {
|
||||
if (document.getElementById(EDITOR_STYLES_ID)) return;
|
||||
const style = document.createElement('style'); style.id = EDITOR_STYLES_ID;
|
||||
style.textContent = `.lwb-ve-overlay{position:fixed;inset:0;background:none;z-index:9999;display:flex;align-items:center;justify-content:center;pointer-events:none}.lwb-ve-modal{width:650px;background:var(--SmartThemeBlurTintColor);border:2px solid var(--SmartThemeBorderColor);border-radius:10px;box-shadow:0 8px 16px var(--SmartThemeShadowColor);pointer-events:auto}.lwb-ve-header{display:flex;justify-content:space-between;padding:10px 14px;border-bottom:1px solid var(--SmartThemeBorderColor);font-weight:600;cursor:move}.lwb-ve-tabs{display:flex;gap:6px;padding:8px 14px;border-bottom:1px solid var(--SmartThemeBorderColor)}.lwb-ve-tab{cursor:pointer;border:1px solid var(--SmartThemeBorderColor);background:var(--SmartThemeBlurTintColor);padding:4px 8px;border-radius:6px;opacity:.8}.lwb-ve-tab.active{opacity:1;border-color:var(--crimson70a)}.lwb-ve-page{display:none}.lwb-ve-page.active{display:block}.lwb-ve-body{height:60vh;overflow:auto;padding:10px}.lwb-ve-footer{display:flex;gap:8px;justify-content:flex-end;padding:12px 14px;border-top:1px solid var(--SmartThemeBorderColor)}.lwb-ve-section{margin:12px 0}.lwb-ve-label{font-size:13px;opacity:.7;margin:6px 0}.lwb-ve-row{gap:8px;align-items:center;margin:4px 0;padding-bottom:10px;border-bottom:1px dashed var(--SmartThemeBorderColor);display:flex;flex-wrap:wrap}.lwb-ve-input,.lwb-ve-text{box-sizing:border-box;background:var(--SmartThemeShadowColor);color:inherit;border:1px solid var(--SmartThemeUserMesBlurTintColor);border-radius:6px;padding:6px 8px}.lwb-ve-text{min-height:64px;resize:vertical;width:100%}.lwb-ve-input{width:260px}.lwb-ve-mini{width:70px!important;margin:0}.lwb-ve-op,.lwb-ve-ctype option{text-align:center}.lwb-ve-lop{width:70px!important;text-align:center}.lwb-ve-btn{cursor:pointer;border:1px solid var(--SmartThemeBorderColor);background:var(--SmartThemeBlurTintColor);padding:6px 10px;border-radius:6px}.lwb-ve-btn.primary{background:var(--crimson70a)}.lwb-ve-event{border:1px dashed var(--SmartThemeBorderColor);border-radius:8px;padding:10px;margin:10px 0}.lwb-ve-event-title{font-weight:600;display:flex;align-items:center;gap:8px}.lwb-ve-close{cursor:pointer}.lwb-var-editor-button.right_menu_button{display:inline-flex;align-items:center;margin-left:10px;transform:scale(1.5)}.lwb-ve-vals,.lwb-ve-varrhs{align-items:center;display:inline-flex;gap:6px}.lwb-ve-delval{transform:scale(.5)}.lwb-act-type{width:200px!important}.lwb-ve-condgroups{display:flex;flex-direction:column;gap:10px}.lwb-ve-condgroup{border:1px solid var(--SmartThemeBorderColor);border-radius:8px;padding:8px}.lwb-ve-group-title{display:flex;align-items:center;gap:8px;margin-bottom:6px}.lwb-ve-group-name{font-weight:600}.lwb-ve-group-lop{width:70px!important;text-align:center}.lwb-ve-add-group{margin-top:6px}@media (max-width:999px){.lwb-ve-overlay{position:absolute;inset:0;align-items:flex-start}.lwb-ve-modal{width:100%;max-height:100%;margin:0;border-radius:10px 10px 0 0}}`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
const U = {
|
||||
qa: (root, sel) => Array.from((root || document).querySelectorAll(sel)),
|
||||
// Template-only UI markup.
|
||||
// eslint-disable-next-line no-unsanitized/property
|
||||
el: (tag, cls, html) => { const e = document.createElement(tag); if (cls) e.className = cls; if (html != null) e.innerHTML = html; return e; },
|
||||
setActive(listLike, idx) { (Array.isArray(listLike) ? listLike : U.qa(document, listLike)).forEach((el, i) => el.classList.toggle('active', i === idx)); },
|
||||
toast: { ok: (m) => window?.toastr?.success?.(m), warn: (m) => window?.toastr?.warning?.(m), err: (m) => window?.toastr?.error?.(m) },
|
||||
drag(modal, overlay, header) {
|
||||
try { modal.style.position = 'absolute'; modal.style.left = '50%'; modal.style.top = '50%'; modal.style.transform = 'translate(-50%,-50%)'; } catch {}
|
||||
let dragging = false, sx = 0, sy = 0, sl = 0, st = 0;
|
||||
const onDown = (e) => { if (!(e instanceof PointerEvent) || e.button !== 0) return; dragging = true; const r = modal.getBoundingClientRect(), ro = overlay.getBoundingClientRect(); modal.style.left = (r.left - ro.left) + 'px'; modal.style.top = (r.top - ro.top) + 'px'; modal.style.transform = ''; sx = e.clientX; sy = e.clientY; sl = parseFloat(modal.style.left) || 0; st = parseFloat(modal.style.top) || 0; window.addEventListener('pointermove', onMove, { passive: true }); window.addEventListener('pointerup', onUp, { once: true }); e.preventDefault(); };
|
||||
const onMove = (e) => { if (!dragging) return; let nl = sl + e.clientX - sx, nt = st + e.clientY - sy; const maxL = (overlay.clientWidth || overlay.getBoundingClientRect().width) - modal.offsetWidth, maxT = (overlay.clientHeight || overlay.getBoundingClientRect().height) - modal.offsetHeight; modal.style.left = Math.max(0, Math.min(maxL, nl)) + 'px'; modal.style.top = Math.max(0, Math.min(maxT, nt)) + 'px'; };
|
||||
const onUp = () => { dragging = false; window.removeEventListener('pointermove', onMove); };
|
||||
header.addEventListener('pointerdown', onDown);
|
||||
},
|
||||
mini(innerHTML, title = '编辑器') {
|
||||
const wrap = U.el('div', 'lwb-ve-overlay'), modal = U.el('div', 'lwb-ve-modal'); modal.style.maxWidth = '720px'; modal.style.pointerEvents = 'auto'; modal.style.zIndex = '10010'; wrap.appendChild(modal);
|
||||
const header = U.el('div', 'lwb-ve-header', `<span>${title}</span><span class="lwb-ve-close">✕</span>`), body = U.el('div', 'lwb-ve-body', innerHTML), footer = U.el('div', 'lwb-ve-footer');
|
||||
const btnCancel = U.el('button', 'lwb-ve-btn', '取消'), btnOk = U.el('button', 'lwb-ve-btn primary', '生成');
|
||||
footer.append(btnCancel, btnOk); modal.append(header, body, footer); U.drag(modal, wrap, header);
|
||||
btnCancel.addEventListener('click', () => wrap.remove()); header.querySelector('.lwb-ve-close')?.addEventListener('click', () => wrap.remove());
|
||||
document.body.appendChild(wrap); return { wrap, modal, body, btnOk, btnCancel };
|
||||
},
|
||||
};
|
||||
|
||||
const P = {
|
||||
stripOuter(s) { let t = String(s || '').trim(); if (!t.startsWith('(') || !t.endsWith(')')) return t; let i = 0, d = 0, q = null; while (i < t.length) { const c = t[i]; if (q) { if (c === q && t[i - 1] !== '\\') q = null; i++; continue; } if (c === '"' || c === "'" || c === '`') { q = c; i++; continue; } if (c === '(') d++; else if (c === ')') d--; i++; } return d === 0 ? t.slice(1, -1).trim() : t; },
|
||||
stripOuterWithFlag(s) { let t = String(s || '').trim(); if (!t.startsWith('(') || !t.endsWith(')')) return { text: t, wrapped: false }; let i = 0, d = 0, q = null; while (i < t.length) { const c = t[i]; if (q) { if (c === q && t[i - 1] !== '\\') q = null; i++; continue; } if (c === '"' || c === "'" || c === '`') { q = c; i++; continue; } if (c === '(') d++; else if (c === ')') d--; i++; } return d === 0 ? { text: t.slice(1, -1).trim(), wrapped: true } : { text: t, wrapped: false }; },
|
||||
splitTopWithOps(s) { const out = []; let i = 0, start = 0, d = 0, q = null, pendingOp = null; while (i < s.length) { const c = s[i]; if (q) { if (c === q && s[i - 1] !== '\\') q = null; i++; continue; } if (c === '"' || c === "'" || c === '`') { q = c; i++; continue; } if (c === '(') { d++; i++; continue; } if (c === ')') { d--; i++; continue; } if (d === 0 && (s.slice(i, i + 2) === '&&' || s.slice(i, i + 2) === '||')) { const seg = s.slice(start, i).trim(); if (seg) out.push({ op: pendingOp, expr: seg }); pendingOp = s.slice(i, i + 2); i += 2; start = i; continue; } i++; } const tail = s.slice(start).trim(); if (tail) out.push({ op: pendingOp, expr: tail }); return out; },
|
||||
parseComp(s) { const t = P.stripOuter(s), m = t.match(/^var\(\s*([`'"])([\s\S]*?)\1\s*\)\s*(==|!=|>=|<=|>|<)\s*(val|var)\(\s*([`'"])([\s\S]*?)\5\s*\)$/); if (!m) return null; return { lhs: m[2], op: m[3], rhsIsVar: m[4] === 'var', rhs: m[6] }; },
|
||||
hasBinary: (s) => /\|\||&&/.test(s),
|
||||
paren: (s) => (s.startsWith('(') && s.endsWith(')')) ? s : `(${s})`,
|
||||
wrapBack: (s) => { const t = String(s || '').trim(); return /^([`'"]).*\1$/.test(t) ? t : '`' + t.replace(/`/g, '\\`') + '`'; },
|
||||
buildVar: (name) => `var(${P.wrapBack(name)})`,
|
||||
buildVal(v) { const t = String(v || '').trim(); return /^([`'"]).*\1$/.test(t) ? `val(${t})` : `val(${P.wrapBack(t)})`; },
|
||||
};
|
||||
|
||||
function buildSTscriptFromActions(actionList) {
|
||||
const parts = [], jsEsc = (s) => String(s ?? '').replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$\{/g, '\\${'), plain = (s) => String(s ?? '').trim();
|
||||
for (const a of actionList || []) {
|
||||
switch (a.type) {
|
||||
case 'var.set': parts.push(`/setvar key=${plain(a.key)} ${plain(a.value)}`); break;
|
||||
case 'var.bump': parts.push(`/addvar key=${plain(a.key)} ${Number(a.delta) || 0}`); break;
|
||||
case 'var.del': parts.push(`/flushvar ${plain(a.key)}`); break;
|
||||
case 'wi.enableUID': parts.push(`/setentryfield file=${plain(a.file)} uid=${plain(a.uid)} field=disable 0`); break;
|
||||
case 'wi.disableUID': parts.push(`/setentryfield file=${plain(a.file)} uid=${plain(a.uid)} field=disable 1`); break;
|
||||
case 'wi.setContentUID': parts.push(`/setentryfield file=${plain(a.file)} uid=${plain(a.uid)} field=content ${plain(a.content)}`); break;
|
||||
case 'wi.createContent': parts.push(plain(a.content) ? `/createentry file=${plain(a.file)} key=${plain(a.key)} ${plain(a.content)}` : `/createentry file=${plain(a.file)} key=${plain(a.key)}`); parts.push(`/setentryfield file=${plain(a.file)} uid={{pipe}} field=constant 1`); break;
|
||||
case 'qr.run': parts.push(`/run ${a.preset ? `${plain(a.preset)}.` : ''}${plain(a.label)}`); break;
|
||||
case 'custom.st': if (a.script) parts.push(...a.script.split('\n').map(s => s.trim()).filter(Boolean).map(c => c.startsWith('/') ? c : '/' + c)); break;
|
||||
}
|
||||
}
|
||||
return 'STscript(`' + jsEsc(parts.join(' | ')) + '`)';
|
||||
}
|
||||
|
||||
const UI = {
|
||||
getEventBlockHTML(index) {
|
||||
return `<div class="lwb-ve-event-title">事件 #<span class="lwb-ve-idx">${index}</span><span class="lwb-ve-close" title="删除事件" style="margin-left:auto;">✕</span></div><div class="lwb-ve-section"><div class="lwb-ve-label">执行条件</div><div class="lwb-ve-condgroups"></div><button type="button" class="lwb-ve-btn lwb-ve-add-group"><i class="fa-solid fa-plus"></i>添加条件小组</button></div><div class="lwb-ve-section"><div class="lwb-ve-label">将显示世界书内容(可选)</div><textarea class="lwb-ve-text lwb-ve-display" placeholder="例如:<Info>……</Info>"></textarea></div><div class="lwb-ve-section"><div class="lwb-ve-label">将执行stscript命令或JS代码(可选)</div><textarea class="lwb-ve-text lwb-ve-js" placeholder="stscript:/setvar key=foo 1 | /run SomeQR 或 直接JS"></textarea><div style="margin-top:6px; display:flex; gap:8px; flex-wrap:wrap;"><button type="button" class="lwb-ve-btn lwb-ve-gen-st">常用st控制</button></div></div>`;
|
||||
},
|
||||
getConditionRowHTML() {
|
||||
return `<select class="lwb-ve-input lwb-ve-mini lwb-ve-lop" style="display:none;"><option value="||">或</option><option value="&&" selected>和</option></select><select class="lwb-ve-input lwb-ve-mini lwb-ve-ctype"><option value="vv">比较值</option><option value="vvv">比较变量</option></select><input class="lwb-ve-input lwb-ve-var" placeholder="变量名称"/><select class="lwb-ve-input lwb-ve-mini lwb-ve-op"><option value="==">等于</option><option value="!=">不等于</option><option value=">=">大于或等于</option><option value="<=">小于或等于</option><option value=">">大于</option><option value="<">小于</option></select><span class="lwb-ve-vals"><span class="lwb-ve-valwrap"><input class="lwb-ve-input lwb-ve-val" placeholder="值"/></span></span><span class="lwb-ve-varrhs" style="display:none;"><span class="lwb-ve-valvarwrap"><input class="lwb-ve-input lwb-ve-valvar" placeholder="变量B名称"/></span></span><button type="button" class="lwb-ve-btn ghost lwb-ve-del">删除</button>`;
|
||||
},
|
||||
makeConditionGroup() {
|
||||
const g = U.el('div', 'lwb-ve-condgroup', `<div class="lwb-ve-group-title"><select class="lwb-ve-input lwb-ve-mini lwb-ve-group-lop" style="display:none;"><option value="&&">和</option><option value="||">或</option></select><span class="lwb-ve-group-name">小组</span><span style="flex:1 1 auto;"></span><button type="button" class="lwb-ve-btn ghost lwb-ve-del-group">删除小组</button></div><div class="lwb-ve-conds"></div><button type="button" class="lwb-ve-btn lwb-ve-add-cond"><i class="fa-solid fa-plus"></i>添加条件</button>`);
|
||||
const conds = g.querySelector('.lwb-ve-conds');
|
||||
g.querySelector('.lwb-ve-add-cond')?.addEventListener('click', () => { try { UI.addConditionRow(conds, {}); } catch {} });
|
||||
g.querySelector('.lwb-ve-del-group')?.addEventListener('click', () => g.remove());
|
||||
return g;
|
||||
},
|
||||
refreshLopDisplay(container) { U.qa(container, '.lwb-ve-row').forEach((r, idx) => { const lop = r.querySelector('.lwb-ve-lop'); if (!lop) return; lop.style.display = idx === 0 ? 'none' : ''; if (idx > 0 && !lop.value) lop.value = '&&'; }); },
|
||||
setupConditionRow(row, onRowsChanged) {
|
||||
row.querySelector('.lwb-ve-del')?.addEventListener('click', () => { row.remove(); onRowsChanged?.(); });
|
||||
const ctype = row.querySelector('.lwb-ve-ctype'), vals = row.querySelector('.lwb-ve-vals'), rhs = row.querySelector('.lwb-ve-varrhs');
|
||||
ctype?.addEventListener('change', () => { if (ctype.value === 'vv') { vals.style.display = 'inline-flex'; rhs.style.display = 'none'; } else { vals.style.display = 'none'; rhs.style.display = 'inline-flex'; } });
|
||||
},
|
||||
createConditionRow(params, onRowsChanged) {
|
||||
const { lop, lhs, op, rhsIsVar, rhs } = params || {};
|
||||
const row = U.el('div', 'lwb-ve-row', UI.getConditionRowHTML());
|
||||
const lopSel = row.querySelector('.lwb-ve-lop'); if (lopSel) { if (lop == null) { lopSel.style.display = 'none'; lopSel.value = '&&'; } else { lopSel.style.display = ''; lopSel.value = String(lop || '&&'); } }
|
||||
const varInp = row.querySelector('.lwb-ve-var'); if (varInp && lhs != null) varInp.value = String(lhs);
|
||||
const opSel = row.querySelector('.lwb-ve-op'); if (opSel && op != null) opSel.value = String(op);
|
||||
const ctypeSel = row.querySelector('.lwb-ve-ctype'), valsWrap = row.querySelector('.lwb-ve-vals'), varRhsWrap = row.querySelector('.lwb-ve-varrhs');
|
||||
if (ctypeSel && valsWrap && varRhsWrap && (rhsIsVar != null || rhs != null)) {
|
||||
if (rhsIsVar) { ctypeSel.value = 'vvv'; valsWrap.style.display = 'none'; varRhsWrap.style.display = 'inline-flex'; const rhsInp = row.querySelector('.lwb-ve-varrhs .lwb-ve-valvar'); if (rhsInp && rhs != null) rhsInp.value = String(rhs); }
|
||||
else { ctypeSel.value = 'vv'; valsWrap.style.display = 'inline-flex'; varRhsWrap.style.display = 'none'; const rhsInp = row.querySelector('.lwb-ve-vals .lwb-ve-val'); if (rhsInp && rhs != null) rhsInp.value = String(rhs); }
|
||||
}
|
||||
UI.setupConditionRow(row, onRowsChanged || null); return row;
|
||||
},
|
||||
addConditionRow(container, params) { const row = UI.createConditionRow(params, () => UI.refreshLopDisplay(container)); container.appendChild(row); UI.refreshLopDisplay(container); return row; },
|
||||
parseConditionIntoUI(block, condStr) {
|
||||
try {
|
||||
const groupWrap = block.querySelector('.lwb-ve-condgroups'); if (!groupWrap) return;
|
||||
// Template-only UI markup.
|
||||
// eslint-disable-next-line no-unsanitized/property
|
||||
groupWrap.innerHTML = '';
|
||||
const top = P.splitTopWithOps(condStr);
|
||||
top.forEach((seg, idxSeg) => {
|
||||
const { text } = P.stripOuterWithFlag(seg.expr), g = UI.makeConditionGroup(); groupWrap.appendChild(g);
|
||||
const glopSel = g.querySelector('.lwb-ve-group-lop'); if (glopSel) { glopSel.style.display = idxSeg === 0 ? 'none' : ''; if (idxSeg > 0) glopSel.value = seg.op || '&&'; }
|
||||
const name = g.querySelector('.lwb-ve-group-name'); if (name) name.textContent = ('ABCDEFGHIJKLMNOPQRSTUVWXYZ'[idxSeg] || (idxSeg + 1)) + ' 小组';
|
||||
const rows = P.splitTopWithOps(P.stripOuter(text)); let first = true; const cw = g.querySelector('.lwb-ve-conds');
|
||||
rows.forEach(r => { const comp = P.parseComp(r.expr); if (!comp) return; UI.addConditionRow(cw, { lop: first ? null : (r.op || '&&'), lhs: comp.lhs, op: comp.op, rhsIsVar: comp.rhsIsVar, rhs: comp.rhs }); first = false; });
|
||||
});
|
||||
} catch {}
|
||||
},
|
||||
createEventBlock(index) {
|
||||
const block = U.el('div', 'lwb-ve-event', UI.getEventBlockHTML(index));
|
||||
block.querySelector('.lwb-ve-event-title .lwb-ve-close')?.addEventListener('click', () => { block.remove(); block.dispatchEvent(new CustomEvent('lwb-refresh-idx', { bubbles: true })); });
|
||||
const groupWrap = block.querySelector('.lwb-ve-condgroups'), addGroupBtn = block.querySelector('.lwb-ve-add-group');
|
||||
const refreshGroupOpsAndNames = () => { U.qa(groupWrap, '.lwb-ve-condgroup').forEach((g, i) => { const glop = g.querySelector('.lwb-ve-group-lop'); if (glop) { glop.style.display = i === 0 ? 'none' : ''; if (i > 0 && !glop.value) glop.value = '&&'; } const nm = g.querySelector('.lwb-ve-group-name'); if (nm) nm.textContent = ('ABCDEFGHIJKLMNOPQRSTUVWXYZ'[i] || (i + 1)) + ' 小组'; }); };
|
||||
const createGroup = () => { const g = UI.makeConditionGroup(); UI.addConditionRow(g.querySelector('.lwb-ve-conds'), {}); g.querySelector('.lwb-ve-del-group')?.addEventListener('click', () => { g.remove(); refreshGroupOpsAndNames(); }); return g; };
|
||||
addGroupBtn.addEventListener('click', () => { groupWrap.appendChild(createGroup()); refreshGroupOpsAndNames(); });
|
||||
groupWrap.appendChild(createGroup()); refreshGroupOpsAndNames();
|
||||
block.querySelector('.lwb-ve-gen-st')?.addEventListener('click', () => openActionBuilder(block));
|
||||
return block;
|
||||
},
|
||||
refreshEventIndices(eventsWrap) {
|
||||
U.qa(eventsWrap, '.lwb-ve-event').forEach((el, i) => {
|
||||
const idxEl = el.querySelector('.lwb-ve-idx'); if (!idxEl) return;
|
||||
idxEl.textContent = String(i + 1); idxEl.style.cursor = 'pointer'; idxEl.title = '点击修改显示名称';
|
||||
if (!idxEl.dataset.clickbound) { idxEl.dataset.clickbound = '1'; idxEl.addEventListener('click', () => { const cur = idxEl.textContent || '', name = prompt('输入事件显示名称:', cur) ?? ''; if (name) idxEl.textContent = name; }); }
|
||||
});
|
||||
},
|
||||
processEventBlock(block, idx) {
|
||||
const displayName = String(block.querySelector('.lwb-ve-idx')?.textContent || '').trim();
|
||||
const id = (displayName && /^\w[\w.-]*$/.test(displayName)) ? displayName : String(idx + 1).padStart(4, '0');
|
||||
const lines = [`[event.${id}]`]; let condStr = '', hasAny = false;
|
||||
const groups = U.qa(block, '.lwb-ve-condgroup');
|
||||
for (let gi = 0; gi < groups.length; gi++) {
|
||||
const g = groups[gi], rows = U.qa(g, '.lwb-ve-conds .lwb-ve-row'); let groupExpr = '', groupHas = false;
|
||||
for (const r of rows) {
|
||||
const v = r.querySelector('.lwb-ve-var')?.value?.trim?.() || '', op = r.querySelector('.lwb-ve-op')?.value || '==', ctype = r.querySelector('.lwb-ve-ctype')?.value || 'vv'; if (!v) continue;
|
||||
let rowExpr = '';
|
||||
if (ctype === 'vv') { const ins = U.qa(r, '.lwb-ve-vals .lwb-ve-val'), exprs = []; for (const inp of ins) { const val = (inp?.value || '').trim(); if (!val) continue; exprs.push(`${P.buildVar(v)} ${op} ${P.buildVal(val)}`); } if (exprs.length === 1) rowExpr = exprs[0]; else if (exprs.length > 1) rowExpr = '(' + exprs.join(' || ') + ')'; }
|
||||
else { const ins = U.qa(r, '.lwb-ve-varrhs .lwb-ve-valvar'), exprs = []; for (const inp of ins) { const rhs = (inp?.value || '').trim(); if (!rhs) continue; exprs.push(`${P.buildVar(v)} ${op} ${P.buildVar(rhs)}`); } if (exprs.length === 1) rowExpr = exprs[0]; else if (exprs.length > 1) rowExpr = '(' + exprs.join(' || ') + ')'; }
|
||||
if (!rowExpr) continue;
|
||||
const lop = r.querySelector('.lwb-ve-lop')?.value || '&&';
|
||||
if (!groupHas) { groupExpr = rowExpr; groupHas = true; } else { if (lop === '&&') { const left = P.hasBinary(groupExpr) ? P.paren(groupExpr) : groupExpr, right = P.hasBinary(rowExpr) ? P.paren(rowExpr) : rowExpr; groupExpr = `${left} && ${right}`; } else { groupExpr = `${groupExpr} || ${(P.hasBinary(rowExpr) ? P.paren(rowExpr) : rowExpr)}`; } }
|
||||
}
|
||||
if (!groupHas) continue;
|
||||
const glop = g.querySelector('.lwb-ve-group-lop')?.value || '&&', wrap = P.hasBinary(groupExpr) ? P.paren(groupExpr) : groupExpr;
|
||||
if (!hasAny) { condStr = wrap; hasAny = true; } else condStr = glop === '&&' ? `${condStr} && ${wrap}` : `${condStr} || ${wrap}`;
|
||||
}
|
||||
const disp = block.querySelector('.lwb-ve-display')?.value ?? '', js = block.querySelector('.lwb-ve-js')?.value ?? '', dispCore = String(disp).replace(/^\n+|\n+$/g, '');
|
||||
if (!dispCore && !js) return { lines: [] };
|
||||
if (condStr) lines.push(`condition: ${condStr}`);
|
||||
if (dispCore !== '') lines.push('display: "' + ('\n' + dispCore + '\n').replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '"');
|
||||
if (js !== '') lines.push(`js_execute: ${JSON.stringify(js)}`);
|
||||
return { lines };
|
||||
},
|
||||
};
|
||||
|
||||
export function openVarEditor(entryEl, uid) {
|
||||
const textarea = (uid ? document.getElementById(`world_entry_content_${uid}`) : null) || entryEl?.querySelector?.('textarea[name="content"]');
|
||||
if (!textarea) { U.toast.warn('未找到内容输入框,请先展开该条目的编辑抽屉'); return; }
|
||||
const overlay = U.el('div', 'lwb-ve-overlay'), modal = U.el('div', 'lwb-ve-modal'); overlay.appendChild(modal); modal.style.pointerEvents = 'auto'; modal.style.zIndex = '10010';
|
||||
const header = U.el('div', 'lwb-ve-header', `<span>条件规则编辑器</span><span class="lwb-ve-close">✕</span>`);
|
||||
const tabs = U.el('div', 'lwb-ve-tabs'), tabsCtrl = U.el('div'); tabsCtrl.style.cssText = 'margin-left:auto;display:inline-flex;gap:6px;';
|
||||
const btnAddTab = U.el('button', 'lwb-ve-btn', '+组'), btnDelTab = U.el('button', 'lwb-ve-btn ghost', '-组');
|
||||
tabs.appendChild(tabsCtrl); tabsCtrl.append(btnAddTab, btnDelTab);
|
||||
const body = U.el('div', 'lwb-ve-body'), footer = U.el('div', 'lwb-ve-footer');
|
||||
const btnCancel = U.el('button', 'lwb-ve-btn', '取消'), btnOk = U.el('button', 'lwb-ve-btn primary', '确认');
|
||||
footer.append(btnCancel, btnOk); modal.append(header, tabs, body, footer); U.drag(modal, overlay, header);
|
||||
const pagesWrap = U.el('div'); body.appendChild(pagesWrap);
|
||||
const addEventBtn = U.el('button', 'lwb-ve-btn', '<i class="fa-solid fa-plus"></i> 添加事件'); addEventBtn.type = 'button'; addEventBtn.style.cssText = 'background: var(--SmartThemeBlurTintColor); border: 1px solid var(--SmartThemeBorderColor); cursor: pointer; margin-right: 5px;';
|
||||
const bumpBtn = U.el('button', 'lwb-ve-btn lwb-ve-gen-bump', 'bump数值映射设置');
|
||||
const tools = U.el('div', 'lwb-ve-toolbar'); tools.append(addEventBtn, bumpBtn); body.appendChild(tools);
|
||||
bumpBtn.addEventListener('click', () => openBumpAliasBuilder(null));
|
||||
const wi = document.getElementById('WorldInfo'), wiIcon = document.getElementById('WIDrawerIcon');
|
||||
const wasPinned = !!wi?.classList.contains('pinnedOpen'); let tempPinned = false;
|
||||
if (wi && !wasPinned) { wi.classList.add('pinnedOpen'); tempPinned = true; } if (wiIcon && !wiIcon.classList.contains('drawerPinnedOpen')) wiIcon.classList.add('drawerPinnedOpen');
|
||||
const closeEditor = () => { try { const pinChecked = !!document.getElementById('WI_panel_pin')?.checked; if (tempPinned && !pinChecked) { wi?.classList.remove('pinnedOpen'); wiIcon?.classList.remove('drawerPinnedOpen'); } } catch {} overlay.remove(); };
|
||||
btnCancel.addEventListener('click', closeEditor); header.querySelector('.lwb-ve-close')?.addEventListener('click', closeEditor);
|
||||
const TAG_RE = { varevent: /<varevent>([\s\S]*?)<\/varevent>/gi }, originalText = String(textarea.value || ''), vareventBlocks = [];
|
||||
TAG_RE.varevent.lastIndex = 0; let mm; while ((mm = TAG_RE.varevent.exec(originalText)) !== null) vareventBlocks.push({ inner: mm[1] ?? '' });
|
||||
const pageInitialized = new Set();
|
||||
const makePage = () => { const page = U.el('div', 'lwb-ve-page'), eventsWrap = U.el('div'); page.appendChild(eventsWrap); return { page, eventsWrap }; };
|
||||
const renderPage = (pageIdx) => {
|
||||
const tabEls = U.qa(tabs, '.lwb-ve-tab'); U.setActive(tabEls, pageIdx);
|
||||
const current = vareventBlocks[pageIdx], evts = (current && typeof current.inner === 'string') ? (parseVareventEvents(current.inner) || []) : [];
|
||||
let page = U.qa(pagesWrap, '.lwb-ve-page')[pageIdx]; if (!page) { page = makePage().page; pagesWrap.appendChild(page); }
|
||||
U.qa(pagesWrap, '.lwb-ve-page').forEach(el => el.classList.remove('active')); page.classList.add('active');
|
||||
let eventsWrap = page.querySelector(':scope > div'); if (!eventsWrap) { eventsWrap = U.el('div'); page.appendChild(eventsWrap); }
|
||||
const init = () => {
|
||||
// Template-only UI markup.
|
||||
// eslint-disable-next-line no-unsanitized/property
|
||||
eventsWrap.innerHTML = '';
|
||||
if (!evts.length) eventsWrap.appendChild(UI.createEventBlock(1));
|
||||
else evts.forEach((_ev, i) => { const block = UI.createEventBlock(i + 1); try { const condStr = String(_ev.condition || '').trim(); if (condStr) UI.parseConditionIntoUI(block, condStr); const disp = String(_ev.display || ''), dispEl = block.querySelector('.lwb-ve-display'); if (dispEl) dispEl.value = disp.replace(/^\r?\n/, '').replace(/\r?\n$/, ''); const js = String(_ev.js || ''), jsEl = block.querySelector('.lwb-ve-js'); if (jsEl) jsEl.value = js; } catch {} eventsWrap.appendChild(block); });
|
||||
UI.refreshEventIndices(eventsWrap); eventsWrap.addEventListener('lwb-refresh-idx', () => UI.refreshEventIndices(eventsWrap));
|
||||
};
|
||||
if (!pageInitialized.has(pageIdx)) { init(); pageInitialized.add(pageIdx); } else if (!eventsWrap.querySelector('.lwb-ve-event')) init();
|
||||
};
|
||||
pagesWrap._lwbRenderPage = renderPage;
|
||||
addEventBtn.addEventListener('click', () => { const active = pagesWrap.querySelector('.lwb-ve-page.active'), eventsWrap = active?.querySelector(':scope > div'); if (!eventsWrap) return; eventsWrap.appendChild(UI.createEventBlock(eventsWrap.children.length + 1)); eventsWrap.dispatchEvent(new CustomEvent('lwb-refresh-idx', { bubbles: true })); });
|
||||
if (vareventBlocks.length === 0) { const tab = U.el('div', 'lwb-ve-tab active', '组 1'); tabs.insertBefore(tab, tabsCtrl); const { page, eventsWrap } = makePage(); pagesWrap.appendChild(page); page.classList.add('active'); eventsWrap.appendChild(UI.createEventBlock(1)); UI.refreshEventIndices(eventsWrap); tab.addEventListener('click', () => { U.qa(tabs, '.lwb-ve-tab').forEach(el => el.classList.remove('active')); tab.classList.add('active'); U.qa(pagesWrap, '.lwb-ve-page').forEach(el => el.classList.remove('active')); page.classList.add('active'); }); }
|
||||
else { vareventBlocks.forEach((_b, i) => { const tab = U.el('div', 'lwb-ve-tab' + (i === 0 ? ' active' : ''), `组 ${i + 1}`); tab.addEventListener('click', () => renderPage(i)); tabs.insertBefore(tab, tabsCtrl); }); renderPage(0); }
|
||||
btnAddTab.addEventListener('click', () => { const newIdx = U.qa(tabs, '.lwb-ve-tab').length; vareventBlocks.push({ inner: '' }); const tab = U.el('div', 'lwb-ve-tab', `组 ${newIdx + 1}`); tab.addEventListener('click', () => pagesWrap._lwbRenderPage(newIdx)); tabs.insertBefore(tab, tabsCtrl); pagesWrap._lwbRenderPage(newIdx); });
|
||||
btnDelTab.addEventListener('click', () => { const tabEls = U.qa(tabs, '.lwb-ve-tab'); if (tabEls.length <= 1) { U.toast.warn('至少保留一组'); return; } const activeIdx = tabEls.findIndex(t => t.classList.contains('active')), idx = activeIdx >= 0 ? activeIdx : 0; U.qa(pagesWrap, '.lwb-ve-page')[idx]?.remove(); tabEls[idx]?.remove(); vareventBlocks.splice(idx, 1); const rebind = U.qa(tabs, '.lwb-ve-tab'); rebind.forEach((t, i) => { const nt = t.cloneNode(true); nt.textContent = `组 ${i + 1}`; nt.addEventListener('click', () => pagesWrap._lwbRenderPage(i)); tabs.replaceChild(nt, t); }); pagesWrap._lwbRenderPage(Math.max(0, Math.min(idx, rebind.length - 1))); });
|
||||
btnOk.addEventListener('click', () => {
|
||||
const pageEls = U.qa(pagesWrap, '.lwb-ve-page'); if (pageEls.length === 0) { closeEditor(); return; }
|
||||
const builtBlocks = [], seenIds = new Set();
|
||||
pageEls.forEach((p) => { const wrap = p.querySelector(':scope > div'), blks = wrap ? U.qa(wrap, '.lwb-ve-event') : [], lines = ['<varevent>']; let hasEvents = false; blks.forEach((b, j) => { const r = UI.processEventBlock(b, j); if (r.lines.length > 0) { const idLine = r.lines[0], mm = idLine.match(/^\[\s*event\.([^\]]+)\]/i), id = mm ? mm[1] : `evt_${j + 1}`; let use = id, k = 2; while (seenIds.has(use)) use = `${id}_${k++}`; if (use !== id) r.lines[0] = `[event.${use}]`; seenIds.add(use); lines.push(...r.lines); hasEvents = true; } }); if (hasEvents) { lines.push('</varevent>'); builtBlocks.push(lines.join('\n')); } });
|
||||
const oldVal = textarea.value || '', originals = [], RE = { varevent: /<varevent>([\s\S]*?)<\/varevent>/gi }; RE.varevent.lastIndex = 0; let m; while ((m = RE.varevent.exec(oldVal)) !== null) originals.push({ start: m.index, end: RE.varevent.lastIndex });
|
||||
let acc = '', pos = 0; const minLen = Math.min(originals.length, builtBlocks.length);
|
||||
for (let i = 0; i < originals.length; i++) { const { start, end } = originals[i]; acc += oldVal.slice(pos, start); if (i < minLen) acc += builtBlocks[i]; pos = end; } acc += oldVal.slice(pos);
|
||||
if (builtBlocks.length > originals.length) { const extras = builtBlocks.slice(originals.length).join('\n\n'); acc = acc.replace(/\s*$/, ''); if (acc && !/(?:\r?\n){2}$/.test(acc)) acc += (/\r?\n$/.test(acc) ? '' : '\n') + '\n'; acc += extras; }
|
||||
acc = acc.replace(/(?:\r?\n){3,}/g, '\n\n'); textarea.value = acc; try { window?.jQuery?.(textarea)?.trigger?.('input'); } catch {}
|
||||
U.toast.ok('已更新条件规则到该世界书条目'); closeEditor();
|
||||
});
|
||||
document.body.appendChild(overlay);
|
||||
}
|
||||
|
||||
export function openActionBuilder(block) {
|
||||
const TYPES = [
|
||||
{ value: 'var.set', label: '变量: set', template: `<input class="lwb-ve-input" placeholder="变量名 key"/><input class="lwb-ve-input" placeholder="值 value"/>` },
|
||||
{ value: 'var.bump', label: '变量: bump(+/-)', template: `<input class="lwb-ve-input" placeholder="变量名 key"/><input class="lwb-ve-input" placeholder="增量(整数,可负) delta"/>` },
|
||||
{ value: 'var.del', label: '变量: del', template: `<input class="lwb-ve-input" placeholder="变量名 key"/>` },
|
||||
{ value: 'wi.enableUID', label: '世界书: 启用条目(UID)', template: `<input class="lwb-ve-input" placeholder="世界书文件名 file(必填)"/><input class="lwb-ve-input" placeholder="条目UID(必填)"/>` },
|
||||
{ value: 'wi.disableUID', label: '世界书: 禁用条目(UID)', template: `<input class="lwb-ve-input" placeholder="世界书文件名 file(必填)"/><input class="lwb-ve-input" placeholder="条目UID(必填)"/>` },
|
||||
{ value: 'wi.setContentUID', label: '世界书: 设置内容(UID)', template: `<input class="lwb-ve-input" placeholder="世界书文件名 file(必填)"/><input class="lwb-ve-input" placeholder="条目UID(必填)"/><textarea class="lwb-ve-text" rows="3" placeholder="内容 content(可多行)"></textarea>` },
|
||||
{ value: 'wi.createContent', label: '世界书: 新建条目(仅内容)', template: `<input class="lwb-ve-input" placeholder="世界书文件名 file(必填)"/><input class="lwb-ve-input" placeholder="条目 key(建议填写)"/><textarea class="lwb-ve-text" rows="4" placeholder="新条目内容 content(可留空)"></textarea>` },
|
||||
{ value: 'qr.run', label: '快速回复(/run)', template: `<input class="lwb-ve-input" placeholder="预设名(可空) preset"/><input class="lwb-ve-input" placeholder="标签(label,必填)"/>` },
|
||||
{ value: 'custom.st', label: '自定义ST命令', template: `<textarea class="lwb-ve-text" rows="4" placeholder="每行一条斜杠命令"></textarea>` },
|
||||
];
|
||||
const ui = U.mini(`<div class="lwb-ve-section"><div class="lwb-ve-label">添加动作</div><div id="lwb-action-list"></div><button type="button" class="lwb-ve-btn" id="lwb-add-action">+动作</button></div>`, '常用st控制');
|
||||
const list = ui.body.querySelector('#lwb-action-list'), addBtn = ui.body.querySelector('#lwb-add-action');
|
||||
const addRow = (presetType) => {
|
||||
const row = U.el('div', 'lwb-ve-row');
|
||||
row.style.alignItems = 'flex-start';
|
||||
// Template-only UI markup.
|
||||
// eslint-disable-next-line no-unsanitized/property
|
||||
row.innerHTML = `<select class="lwb-ve-input lwb-ve-mini lwb-act-type"></select><div class="lwb-ve-fields" style="flex:1; display:grid; grid-template-columns: 1fr 1fr; gap:6px;"></div><button type="button" class="lwb-ve-btn ghost lwb-ve-del">??</button>`;
|
||||
const typeSel = row.querySelector('.lwb-act-type');
|
||||
const fields = row.querySelector('.lwb-ve-fields');
|
||||
row.querySelector('.lwb-ve-del').addEventListener('click', () => row.remove());
|
||||
// Template-only UI markup.
|
||||
// eslint-disable-next-line no-unsanitized/property
|
||||
typeSel.innerHTML = TYPES.map(a => `<option value="${a.value}">${a.label}</option>`).join('');
|
||||
const renderFields = () => {
|
||||
const def = TYPES.find(a => a.value === typeSel.value);
|
||||
// Template-only UI markup.
|
||||
// eslint-disable-next-line no-unsanitized/property
|
||||
fields.innerHTML = def ? def.template : '';
|
||||
};
|
||||
typeSel.addEventListener('change', renderFields);
|
||||
if (presetType) typeSel.value = presetType;
|
||||
renderFields();
|
||||
list.appendChild(row);
|
||||
};
|
||||
addBtn.addEventListener('click', () => addRow()); addRow();
|
||||
ui.btnOk.addEventListener('click', () => {
|
||||
const rows = U.qa(list, '.lwb-ve-row'), actions = [];
|
||||
for (const r of rows) { const type = r.querySelector('.lwb-act-type')?.value, inputs = U.qa(r, '.lwb-ve-fields .lwb-ve-input, .lwb-ve-fields .lwb-ve-text').map(i => i.value); if (type === 'var.set' && inputs[0]) actions.push({ type, key: inputs[0], value: inputs[1] || '' }); if (type === 'var.bump' && inputs[0]) actions.push({ type, key: inputs[0], delta: inputs[1] || '0' }); if (type === 'var.del' && inputs[0]) actions.push({ type, key: inputs[0] }); if ((type === 'wi.enableUID' || type === 'wi.disableUID') && inputs[0] && inputs[1]) actions.push({ type, file: inputs[0], uid: inputs[1] }); if (type === 'wi.setContentUID' && inputs[0] && inputs[1]) actions.push({ type, file: inputs[0], uid: inputs[1], content: inputs[2] || '' }); if (type === 'wi.createContent' && inputs[0]) actions.push({ type, file: inputs[0], key: inputs[1] || '', content: inputs[2] || '' }); if (type === 'qr.run' && inputs[1]) actions.push({ type, preset: inputs[0] || '', label: inputs[1] }); if (type === 'custom.st' && inputs[0]) { const cmds = inputs[0].split('\n').map(s => s.trim()).filter(Boolean).map(c => c.startsWith('/') ? c : '/' + c).join(' | '); if (cmds) actions.push({ type, script: cmds }); } }
|
||||
const jsCode = buildSTscriptFromActions(actions), jsBox = block?.querySelector?.('.lwb-ve-js'); if (jsCode && jsBox) jsBox.value = jsCode; ui.wrap.remove();
|
||||
});
|
||||
}
|
||||
|
||||
export function openBumpAliasBuilder(block) {
|
||||
const ui = U.mini(`<div class="lwb-ve-section"><div class="lwb-ve-label">bump数值映射(每行一条:变量名(可空) | 短语或 /regex/flags | 数值)</div><div id="lwb-bump-list"></div><button type="button" class="lwb-ve-btn" id="lwb-add-bump">+映射</button></div>`, 'bump数值映射设置');
|
||||
const list = ui.body.querySelector('#lwb-bump-list'), addBtn = ui.body.querySelector('#lwb-add-bump');
|
||||
const addRow = (scope = '', phrase = '', val = '1') => { const row = U.el('div', 'lwb-ve-row', `<input class="lwb-ve-input" placeholder="变量名(可空=全局)" value="${scope}"/><input class="lwb-ve-input" placeholder="短语 或 /regex(例:/她(很)?开心/i)" value="${phrase}"/><input class="lwb-ve-input" placeholder="数值(整数,可负)" value="${val}"/><button type="button" class="lwb-ve-btn ghost lwb-ve-del">删除</button>`); row.querySelector('.lwb-ve-del').addEventListener('click', () => row.remove()); list.appendChild(row); };
|
||||
addBtn.addEventListener('click', () => addRow());
|
||||
try { const store = getBumpAliasStore() || {}; const addFromBucket = (scope, bucket) => { let n = 0; for (const [phrase, val] of Object.entries(bucket || {})) { addRow(scope, phrase, String(val)); n++; } return n; }; let prefilled = 0; if (store._global) prefilled += addFromBucket('', store._global); for (const [scope, bucket] of Object.entries(store || {})) if (scope !== '_global') prefilled += addFromBucket(scope, bucket); if (prefilled === 0) addRow(); } catch { addRow(); }
|
||||
ui.btnOk.addEventListener('click', async () => { try { const rows = U.qa(list, '.lwb-ve-row'), items = rows.map(r => { const ins = U.qa(r, '.lwb-ve-input').map(i => i.value); return { scope: (ins[0] || '').trim(), phrase: (ins[1] || '').trim(), val: Number(ins[2] || 0) }; }).filter(x => x.phrase), next = {}; for (const it of items) { const bucket = it.scope ? (next[it.scope] ||= {}) : (next._global ||= {}); bucket[it.phrase] = Number.isFinite(it.val) ? it.val : 0; } await setBumpAliasStore(next); U.toast.ok('Bump 映射已保存到角色卡'); ui.wrap.remove(); } catch {} });
|
||||
}
|
||||
|
||||
function tryInjectButtons(root) {
|
||||
const scope = root.closest?.('#WorldInfo') || document.getElementById('WorldInfo') || root;
|
||||
scope.querySelectorAll?.('.world_entry .alignitemscenter.flex-container .editor_maximize')?.forEach((maxBtn) => {
|
||||
const container = maxBtn.parentElement; if (!container || container.querySelector('.lwb-var-editor-button')) return;
|
||||
const entry = container.closest('.world_entry'), uid = entry?.getAttribute('data-uid') || entry?.dataset?.uid || (window?.jQuery ? window.jQuery(entry).data('uid') : undefined);
|
||||
const btn = U.el('div', 'right_menu_button interactable lwb-var-editor-button'); btn.title = '条件规则编辑器'; btn.innerHTML = '<i class="fa-solid fa-pen-ruler"></i>';
|
||||
btn.addEventListener('click', () => openVarEditor(entry || undefined, uid)); container.insertBefore(btn, maxBtn.nextSibling);
|
||||
});
|
||||
}
|
||||
|
||||
function observeWIEntriesForEditorButton() {
|
||||
try { LWBVE.obs?.disconnect(); LWBVE.obs = null; } catch {}
|
||||
const root = document.getElementById('WorldInfo') || document.body;
|
||||
const cb = (() => { let t = null; return () => { clearTimeout(t); t = setTimeout(() => tryInjectButtons(root), 100); }; })();
|
||||
const obs = new MutationObserver(() => cb()); try { obs.observe(root, { childList: true, subtree: true }); } catch {} LWBVE.obs = obs;
|
||||
}
|
||||
|
||||
export function initVareventEditor() {
|
||||
if (initialized) return; initialized = true;
|
||||
events = createModuleEvents(MODULE_ID);
|
||||
injectEditorStyles();
|
||||
installWIHiddenTagStripper();
|
||||
registerWIEventSystem();
|
||||
observeWIEntriesForEditorButton();
|
||||
setTimeout(() => tryInjectButtons(document.body), 600);
|
||||
if (typeof window !== 'undefined') { window.LWBVE = LWBVE; window.openVarEditor = openVarEditor; window.openActionBuilder = openActionBuilder; window.openBumpAliasBuilder = openBumpAliasBuilder; window.parseVareventEvents = parseVareventEvents; window.getBumpAliasStore = getBumpAliasStore; window.setBumpAliasStore = setBumpAliasStore; window.clearBumpAliasStore = clearBumpAliasStore; }
|
||||
LWBVE.installed = true;
|
||||
}
|
||||
|
||||
export function cleanupVareventEditor() {
|
||||
if (!initialized) return;
|
||||
events?.cleanup(); events = null;
|
||||
U.qa(document, '.lwb-ve-overlay').forEach(el => el.remove());
|
||||
U.qa(document, '.lwb-var-editor-button').forEach(el => el.remove());
|
||||
document.getElementById(EDITOR_STYLES_ID)?.remove();
|
||||
try { getContext()?.setExtensionPrompt?.(LWB_VAREVENT_PROMPT_KEY, '', 0, 0, false); } catch {}
|
||||
try { const { eventSource } = getContext() || {}; const orig = eventSource && origEmitMap.get(eventSource); if (orig) { eventSource.emit = orig; origEmitMap.delete(eventSource); } } catch {}
|
||||
try { LWBVE.obs?.disconnect(); LWBVE.obs = null; } catch {}
|
||||
if (typeof window !== 'undefined') LWBVE.installed = false;
|
||||
initialized = false;
|
||||
}
|
||||
|
||||
// 供 variables-core.js 复用的解析工具
|
||||
export { stripYamlInlineComment, OP_MAP, TOP_OP_RE };
|
||||
|
||||
export { MODULE_ID, LWBVE };
|
||||
2389
modules/variables/variables-core.js
Normal file
2389
modules/variables/variables-core.js
Normal file
File diff suppressed because it is too large
Load Diff
680
modules/variables/variables-panel.js
Normal file
680
modules/variables/variables-panel.js
Normal file
@@ -0,0 +1,680 @@
|
||||
import { extension_settings, getContext, saveMetadataDebounced } from "../../../../../extensions.js";
|
||||
import { saveSettingsDebounced, chat_metadata } from "../../../../../../script.js";
|
||||
import { getLocalVariable, setLocalVariable, getGlobalVariable, setGlobalVariable } from "../../../../../variables.js";
|
||||
import { extensionFolderPath } from "../../core/constants.js";
|
||||
import { createModuleEvents, event_types } from "../../core/event-manager.js";
|
||||
|
||||
const CONFIG = {
|
||||
extensionName: "variables-panel",
|
||||
extensionFolderPath,
|
||||
defaultSettings: { enabled: false },
|
||||
watchInterval: 1500, touchTimeout: 4000, longPressDelay: 700,
|
||||
};
|
||||
|
||||
const EMBEDDED_CSS = `
|
||||
.vm-container{color:var(--SmartThemeBodyColor);background:var(--SmartThemeBlurTintColor);flex-direction:column;overflow-y:auto;z-index:3000;position:fixed;display:none}
|
||||
.vm-container:not([style*="display: none"]){display:flex}
|
||||
@media (min-width: 1000px){.vm-container:not([style*="display: none"]){width:calc((100vw - var(--sheldWidth)) / 2);border-left:1px solid var(--SmartThemeBorderColor);right:0;top:0;height:100vh}}
|
||||
@media (max-width: 999px){.vm-container:not([style*="display: none"]){max-height:calc(100svh - var(--topBarBlockSize));top:var(--topBarBlockSize);width:100%;height:100vh;left:0}}
|
||||
.vm-header,.vm-section,.vm-item-content{border-bottom:.5px solid var(--SmartThemeBorderColor)}
|
||||
.vm-header,.vm-section-header{display:flex;justify-content:space-between;align-items:center}
|
||||
.vm-title,.vm-item-name{font-weight:bold}
|
||||
.vm-header{padding:15px}.vm-title{font-size:16px}
|
||||
.vm-section-header{padding:5px 15px;border-bottom:5px solid var(--SmartThemeBorderColor);font-size:14px;color:var(--SmartThemeEmColor)}
|
||||
.vm-close,.vm-btn{background:none;border:none;cursor:pointer;display:inline-flex;align-items:center;justify-content:center}
|
||||
.vm-close{font-size:18px;padding:5px}
|
||||
.vm-btn{border:1px solid var(--SmartThemeBorderColor);border-radius:3px;font-size:12px;padding:2px 4px;color:var(--SmartThemeBodyColor)}
|
||||
.vm-search-container{padding:10px;border-bottom:1px solid var(--SmartThemeBorderColor)}
|
||||
.vm-search-input{width:100%;padding:3px 6px}
|
||||
.vm-clear-all-btn{color:#ff6b6b;border-color:#ff6b6b;opacity:.3}
|
||||
.vm-list{flex:1;overflow-y:auto;padding:10px}
|
||||
.vm-item{border:1px solid var(--SmartThemeBorderColor);opacity:.7}
|
||||
.vm-item.expanded{opacity:1}
|
||||
.vm-item-header{display:flex;justify-content:space-between;align-items:center;cursor:pointer;padding-left:5px}
|
||||
.vm-item-name{font-size:13px}
|
||||
.vm-item-controls{background:var(--SmartThemeChatTintColor);display:flex;gap:5px;position:absolute;right:5px;opacity:0;visibility:hidden}
|
||||
.vm-item-content{border-top:1px solid var(--SmartThemeBorderColor);display:none}
|
||||
.vm-item.expanded>.vm-item-content{display:block}
|
||||
.vm-inline-form{background:var(--SmartThemeChatTintColor);border:1px solid var(--SmartThemeBorderColor);border-top:none;padding:10px;margin:0;display:none}
|
||||
.vm-inline-form.active{display:block;animation:slideDown .2s ease-out}
|
||||
@keyframes slideDown{from{opacity:0;max-height:0;padding-top:0;padding-bottom:0}to{opacity:1;max-height:200px;padding-top:10px;padding-bottom:10px}}
|
||||
@media (hover:hover){.vm-close:hover,.vm-btn:hover{opacity:.8}.vm-close:hover{color:red}.vm-clear-all-btn:hover{opacity:1}.vm-item:hover>.vm-item-header .vm-item-controls{opacity:1;visibility:visible}.vm-list:hover::-webkit-scrollbar-thumb{background:var(--SmartThemeQuoteColor)}.vm-variable-checkbox:hover{background-color:rgba(255,255,255,.1)}}
|
||||
@media (hover:none){.vm-close:active,.vm-btn:active{opacity:.8}.vm-close:active{color:red}.vm-clear-all-btn:active{opacity:1}.vm-item:active>.vm-item-header .vm-item-controls,.vm-item.touched>.vm-item-header .vm-item-controls{opacity:1;visibility:visible}.vm-item.touched>.vm-item-header{background-color:rgba(255,255,255,.05)}.vm-btn:active{background-color:rgba(255,255,255,.1);transform:scale(.95)}.vm-variable-checkbox:active{background-color:rgba(255,255,255,.1)}}
|
||||
.vm-item:not([data-level]).expanded .vm-item[data-level="1"]{--level-color:hsl(36,100%,50%)}
|
||||
.vm-item[data-level="1"].expanded .vm-item[data-level="2"]{--level-color:hsl(60,100%,50%)}
|
||||
.vm-item[data-level="2"].expanded .vm-item[data-level="3"]{--level-color:hsl(120,100%,50%)}
|
||||
.vm-item[data-level="3"].expanded .vm-item[data-level="4"]{--level-color:hsl(180,100%,50%)}
|
||||
.vm-item[data-level="4"].expanded .vm-item[data-level="5"]{--level-color:hsl(240,100%,50%)}
|
||||
.vm-item[data-level="5"].expanded .vm-item[data-level="6"]{--level-color:hsl(280,100%,50%)}
|
||||
.vm-item[data-level="6"].expanded .vm-item[data-level="7"]{--level-color:hsl(320,100%,50%)}
|
||||
.vm-item[data-level="7"].expanded .vm-item[data-level="8"]{--level-color:hsl(200,100%,50%)}
|
||||
.vm-item[data-level="8"].expanded .vm-item[data-level="9"]{--level-color:hsl(160,100%,50%)}
|
||||
.vm-item[data-level]{border-left:2px solid var(--level-color);margin-left:6px}
|
||||
.vm-item[data-level]:last-child{border-bottom:2px solid var(--level-color)}
|
||||
.vm-tree-value,.vm-variable-checkbox span{font-family:monospace;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.vm-tree-value{color:inherit;font-size:12px;flex:1;margin:0 10px}
|
||||
.vm-input,.vm-textarea{border:1px solid var(--SmartThemeBorderColor);border-radius:3px;background-color:var(--SmartThemeChatTintColor);font-size:12px;margin:3px 0}
|
||||
.vm-textarea{min-height:60px;padding:5px;font-family:monospace;resize:vertical}
|
||||
.vm-add-form{padding:10px;border-top:1px solid var(--SmartThemeBorderColor);display:none}
|
||||
.vm-add-form.active{display:block}
|
||||
.vm-form-row{display:flex;gap:10px;margin-bottom:10px;align-items:center}
|
||||
.vm-form-label{min-width:30px;font-size:12px;font-weight:bold}
|
||||
.vm-form-input{flex:1}
|
||||
.vm-form-buttons{display:flex;gap:5px;justify-content:flex-end}
|
||||
.vm-list::-webkit-scrollbar{width:6px}
|
||||
.vm-list::-webkit-scrollbar-track{background:var(--SmartThemeBodyColor)}
|
||||
.vm-list::-webkit-scrollbar-thumb{background:var(--SmartThemeBorderColor);border-radius:3px}
|
||||
.vm-empty-message{padding:20px;text-align:center;color:#888}
|
||||
.vm-item-name-visible{opacity:1}
|
||||
.vm-item-separator{opacity:.3}
|
||||
.vm-null-value{opacity:.6}
|
||||
.mes_btn.mes_variables_panel{opacity:.6}
|
||||
.mes_btn.mes_variables_panel:hover{opacity:1}
|
||||
.vm-badges{display:inline-flex;gap:6px;margin-left:6px;align-items:center}
|
||||
.vm-badge[data-type="ro"]{color:#F9C770}
|
||||
.vm-badge[data-type="struct"]{color:#48B0C7}
|
||||
.vm-badge[data-type="cons"]{color:#D95E37}
|
||||
.vm-badge:hover{opacity:1;filter:saturate(1.2)}
|
||||
:root{--vm-badge-nudge:0.06em}
|
||||
.vm-item-name{display:inline-flex;align-items:center}
|
||||
.vm-badges{display:inline-flex;gap:.35em;margin-left:.35em}
|
||||
.vm-item-name .vm-badge{display:flex;width:1em;position:relative;top:var(--vm-badge-nudge) !important;opacity:.9}
|
||||
.vm-item-name .vm-badge i{display:block;font-size:.8em;line-height:1em}
|
||||
`;
|
||||
|
||||
const EMBEDDED_HTML = `
|
||||
<div id="vm-container" class="vm-container" style="display:none">
|
||||
<div class="vm-header">
|
||||
<div class="vm-title">变量面板</div>
|
||||
<button id="vm-close" class="vm-close"><i class="fa-solid fa-times"></i></button>
|
||||
</div>
|
||||
<div class="vm-content">
|
||||
${['character','global'].map(t=>`
|
||||
<div class="vm-section" id="${t}-variables-section">
|
||||
<div class="vm-section-header">
|
||||
<div class="vm-section-title"><i class="fa-solid ${t==='character'?'fa-user':'fa-globe'}"></i>${t==='character'?' 本地变量':' 全局变量'}</div>
|
||||
<div class="vm-section-controls">
|
||||
${[['import','fa-upload','导入变量'],['export','fa-download','导出变量'],['add','fa-plus','添加变量'],['collapse','fa-chevron-down','展开/折叠所有'],['clear-all','fa-trash','清除所有变量']].map(([a,ic,ti])=>`<button class="vm-btn ${a==='clear-all'?'vm-clear-all-btn':''}" data-type="${t}" data-act="${a}" title="${ti}"><i class="fa-solid ${ic}"></i></button>`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
<div class="vm-search-container"><input type="text" class="vm-input vm-search-input" id="${t}-vm-search" placeholder="搜索${t==='character'?'本地':'全局'}变量..."></div>
|
||||
<div class="vm-list" id="${t}-variables-list"></div>
|
||||
<div class="vm-add-form" id="${t}-vm-add-form">
|
||||
<div class="vm-form-row"><label class="vm-form-label">名称:</label><input type="text" class="vm-input vm-form-input" id="${t}-vm-name" placeholder="变量名称"></div>
|
||||
<div class="vm-form-row"><label class="vm-form-label">值:</label><textarea class="vm-textarea vm-form-input" id="${t}-vm-value" placeholder="变量值 (支持JSON格式)"></textarea></div>
|
||||
<div class="vm-form-buttons">
|
||||
<button class="vm-btn" data-type="${t}" data-act="save-add"><i class="fa-solid fa-floppy-disk"></i>保存</button>
|
||||
<button class="vm-btn" data-type="${t}" data-act="cancel-add">取消</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const VT = {
|
||||
character: { getter: getLocalVariable, setter: setLocalVariable, storage: ()=> chat_metadata?.variables || (chat_metadata.variables = {}), save: saveMetadataDebounced },
|
||||
global: { getter: getGlobalVariable, setter: setGlobalVariable, storage: ()=> extension_settings.variables?.global || ((extension_settings.variables = { global: {} }).global), save: saveSettingsDebounced },
|
||||
};
|
||||
|
||||
const LWB_RULES_KEY='LWB_RULES';
|
||||
const getRulesTable = () => { try { return getContext()?.chatMetadata?.[LWB_RULES_KEY] || {}; } catch { return {}; } };
|
||||
const pathKey = (arr)=>{ try { return (arr||[]).map(String).join('.'); } catch { return ''; } };
|
||||
const getRuleNodeByPath = (arr)=> (pathKey(arr) ? (getRulesTable()||{})[pathKey(arr)] : undefined);
|
||||
const hasAnyRule = (n)=>{
|
||||
if(!n) return false;
|
||||
if(n.ro) return true;
|
||||
if(n.objectPolicy && n.objectPolicy!=='none') return true;
|
||||
if(n.arrayPolicy && n.arrayPolicy!=='lock') return true;
|
||||
const c=n.constraints||{};
|
||||
return ('min'in c)||('max'in c)||('step'in c)||(Array.isArray(c.enum)&&c.enum.length)||(c.regex&&c.regex.source);
|
||||
};
|
||||
const ruleTip = (n)=>{
|
||||
if(!n) return '';
|
||||
const lines=[], c=n.constraints||{};
|
||||
if(n.ro) lines.push('只读:$ro');
|
||||
if(n.objectPolicy){ const m={none:'(默认:不可增删键)',ext:'$ext(可增键)',prune:'$prune(可删键)',free:'$free(可增删键)'}; lines.push(`对象策略:${m[n.objectPolicy]||n.objectPolicy}`); }
|
||||
if(n.arrayPolicy){ const m={lock:'(默认:不可增删项)',grow:'$grow(可增项)',shrink:'$shrink(可删项)',list:'$list(可增删项)'}; lines.push(`数组策略:${m[n.arrayPolicy]||n.arrayPolicy}`); }
|
||||
if('min'in c||'max'in c){ if('min'in c&&'max'in c) lines.push(`范围:$range=[${c.min},${c.max}]`); else if('min'in c) lines.push(`下限:$min=${c.min}`); else lines.push(`上限:$max=${c.max}`); }
|
||||
if('step'in c) lines.push(`步长:$step=${c.step}`);
|
||||
if(Array.isArray(c.enum)&&c.enum.length) lines.push(`枚举:$enum={${c.enum.join(';')}}`);
|
||||
if(c.regex&&c.regex.source) lines.push(`正则:$match=/${c.regex.source}/${c.regex.flags||''}`);
|
||||
return lines.join('\n');
|
||||
};
|
||||
const badgesHtml = (n)=>{
|
||||
if(!hasAnyRule(n)) return '';
|
||||
const tip=ruleTip(n).replace(/"/g,'"'), out=[];
|
||||
if(n.ro) out.push(`<span class="vm-badge" data-type="ro" title="${tip}"><i class="fa-solid fa-shield-halved"></i></span>`);
|
||||
if((n.objectPolicy&&n.objectPolicy!=='none')||(n.arrayPolicy&&n.arrayPolicy!=='lock')) out.push(`<span class="vm-badge" data-type="struct" title="${tip}"><i class="fa-solid fa-diagram-project"></i></span>`);
|
||||
const c=n.constraints||{}; if(('min'in c)||('max'in c)||('step'in c)||(Array.isArray(c.enum)&&c.enum.length)||(c.regex&&c.regex.source)) out.push(`<span class="vm-badge" data-type="cons" title="${tip}"><i class="fa-solid fa-ruler-vertical"></i></span>`);
|
||||
return out.length?`<span class="vm-badges">${out.join('')}</span>`:'';
|
||||
};
|
||||
const debounce=(fn,ms=200)=>{let t;return(...a)=>{clearTimeout(t);t=setTimeout(()=>fn(...a),ms);}};
|
||||
|
||||
class VariablesPanel {
|
||||
constructor(){
|
||||
this.state={isOpen:false,isEnabled:false,container:null,timers:{watcher:null,longPress:null,touch:new Map()},currentInlineForm:null,formState:{},rulesChecksum:''};
|
||||
this.variableSnapshot=null; this.eventHandlers={}; this.savingInProgress=false; this.containerHtml=EMBEDDED_HTML;
|
||||
}
|
||||
|
||||
async init(){
|
||||
this.injectUI(); this.bindControlToggle();
|
||||
const s=this.getSettings(); this.state.isEnabled=s.enabled; this.syncCheckbox();
|
||||
if(s.enabled) this.enable();
|
||||
}
|
||||
|
||||
injectUI(){
|
||||
if(!document.getElementById('variables-panel-css')){
|
||||
const st=document.createElement('style'); st.id='variables-panel-css'; st.textContent=EMBEDDED_CSS; document.head.appendChild(st);
|
||||
}
|
||||
}
|
||||
|
||||
getSettings(){ extension_settings.LittleWhiteBox ??= {}; return extension_settings.LittleWhiteBox.variablesPanel ??= {...CONFIG.defaultSettings}; }
|
||||
vt(t){ return VT[t]; }
|
||||
store(t){ return this.vt(t).storage(); }
|
||||
|
||||
enable(){
|
||||
this.createContainer(); this.bindEvents();
|
||||
['character','global'].forEach(t=>this.normalizeStore(t));
|
||||
this.loadVariables(); this.installMessageButtons();
|
||||
}
|
||||
disable(){ this.cleanup(); }
|
||||
|
||||
cleanup(){
|
||||
this.stopWatcher(); this.unbindEvents(); this.unbindControlToggle(); this.removeContainer(); this.removeAllMessageButtons();
|
||||
const tm=this.state.timers; if(tm.watcher) clearInterval(tm.watcher); if(tm.longPress) clearTimeout(tm.longPress);
|
||||
tm.touch.forEach(x=>clearTimeout(x)); tm.touch.clear();
|
||||
Object.assign(this.state,{isOpen:false,timers:{watcher:null,longPress:null,touch:new Map()},currentInlineForm:null,formState:{},rulesChecksum:''});
|
||||
this.variableSnapshot=null; this.savingInProgress=false;
|
||||
}
|
||||
|
||||
createContainer(){
|
||||
if(!this.state.container?.length){
|
||||
$('body').append(this.containerHtml);
|
||||
this.state.container=$("#vm-container");
|
||||
$("#vm-close").off('click').on('click',()=>this.close());
|
||||
}
|
||||
}
|
||||
removeContainer(){ this.state.container?.remove(); this.state.container=null; }
|
||||
|
||||
open(){
|
||||
if(!this.state.isEnabled) return toastr.warning('请先启用变量面板');
|
||||
this.createContainer(); this.bindEvents(); this.state.isOpen=true; this.state.container.show();
|
||||
this.state.rulesChecksum = JSON.stringify(getRulesTable()||{});
|
||||
this.loadVariables(); this.startWatcher();
|
||||
}
|
||||
close(){ this.state.isOpen=false; this.stopWatcher(); this.unbindEvents(); this.removeContainer(); }
|
||||
|
||||
bindControlToggle(){
|
||||
const id='xiaobaix_variables_panel_enabled';
|
||||
const bind=()=>{
|
||||
const cb=document.getElementById(id); if(!cb) return false;
|
||||
this.handleCheckboxChange && cb.removeEventListener('change',this.handleCheckboxChange);
|
||||
this.handleCheckboxChange=e=> this.toggleEnabled(e.target instanceof HTMLInputElement ? !!e.target.checked : false);
|
||||
cb.addEventListener('change',this.handleCheckboxChange); if(cb instanceof HTMLInputElement) cb.checked=this.state.isEnabled; return true;
|
||||
};
|
||||
if(!bind()) setTimeout(bind,100);
|
||||
}
|
||||
unbindControlToggle(){
|
||||
const cb=document.getElementById('xiaobaix_variables_panel_enabled');
|
||||
if(cb && this.handleCheckboxChange) cb.removeEventListener('change',this.handleCheckboxChange);
|
||||
this.handleCheckboxChange=null;
|
||||
}
|
||||
syncCheckbox(){ const cb=document.getElementById('xiaobaix_variables_panel_enabled'); if(cb instanceof HTMLInputElement) cb.checked=this.state.isEnabled; }
|
||||
|
||||
bindEvents(){
|
||||
if(!this.state.container?.length) return;
|
||||
this.unbindEvents();
|
||||
const ns='.vm';
|
||||
$(document)
|
||||
.on(`click${ns}`,'.vm-section [data-act]',e=>this.onHeaderAction(e))
|
||||
.on(`touchstart${ns}`,'.vm-item>.vm-item-header',e=>this.handleTouch(e))
|
||||
.on(`click${ns}`,'.vm-item>.vm-item-header',e=>this.handleItemClick(e))
|
||||
.on(`click${ns}`,'.vm-item-controls [data-act]',e=>this.onItemAction(e))
|
||||
.on(`click${ns}`,'.vm-inline-form [data-act]',e=>this.onInlineAction(e))
|
||||
.on(`mousedown${ns} touchstart${ns}`,'[data-act="copy"]',e=>this.bindCopyPress(e));
|
||||
['character','global'].forEach(t=> $(`#${t}-vm-search`).on('input',e=>{
|
||||
if(e.currentTarget instanceof HTMLInputElement) this.searchVariables(t,e.currentTarget.value);
|
||||
else this.searchVariables(t,'');
|
||||
}));
|
||||
}
|
||||
unbindEvents(){ $(document).off('.vm'); ['character','global'].forEach(t=> $(`#${t}-vm-search`).off('input')); }
|
||||
|
||||
onHeaderAction(e){
|
||||
e.preventDefault(); e.stopPropagation();
|
||||
const b=$(e.currentTarget), act=b.data('act'), t=b.data('type');
|
||||
({
|
||||
import:()=>this.importVariables(t),
|
||||
export:()=>this.exportVariables(t),
|
||||
add:()=>this.showAddForm(t),
|
||||
collapse:()=>this.collapseAll(t),
|
||||
'clear-all':()=>this.clearAllVariables(t),
|
||||
'save-add':()=>this.saveAddVariable(t),
|
||||
'cancel-add':()=>this.hideAddForm(t),
|
||||
}[act]||(()=>{}))();
|
||||
}
|
||||
|
||||
onItemAction(e){
|
||||
e.preventDefault(); e.stopPropagation();
|
||||
const btn=$(e.currentTarget), act=btn.data('act'), item=btn.closest('.vm-item'),
|
||||
t=this.getVariableType(item), path=this.getItemPath(item);
|
||||
({
|
||||
edit: ()=>this.editAction(item,'edit',t,path),
|
||||
'add-child': ()=>this.editAction(item,'addChild',t,path),
|
||||
delete: ()=>this.handleDelete(item,t,path),
|
||||
copy: ()=>{}
|
||||
}[act]||(()=>{}))();
|
||||
}
|
||||
|
||||
onInlineAction(e){ e.preventDefault(); e.stopPropagation(); const act=$(e.currentTarget).data('act'); act==='inline-save'? this.handleInlineSave($(e.currentTarget).closest('.vm-inline-form')) : this.hideInlineForm(); }
|
||||
|
||||
bindCopyPress(e){
|
||||
e.preventDefault(); e.stopPropagation();
|
||||
const start=Date.now();
|
||||
this.state.timers.longPress=setTimeout(()=>{ this.handleCopy(e,true); this.state.timers.longPress=null; },CONFIG.longPressDelay);
|
||||
const release=(re)=>{
|
||||
if(this.state.timers.longPress){
|
||||
clearTimeout(this.state.timers.longPress); this.state.timers.longPress=null;
|
||||
if(re.type!=='mouseleave' && (Date.now()-start)<CONFIG.longPressDelay) this.handleCopy(e,false);
|
||||
}
|
||||
$(document).off('mouseup.vm touchend.vm mouseleave.vm',release);
|
||||
};
|
||||
$(document).on('mouseup.vm touchend.vm mouseleave.vm',release);
|
||||
}
|
||||
|
||||
stringifyVar(v){ return typeof v==='string'? v : JSON.stringify(v); }
|
||||
makeSnapshotMap(t){ const s=this.store(t), m={}; for(const[k,v] of Object.entries(s)) m[k]=this.stringifyVar(v); return m; }
|
||||
|
||||
startWatcher(){ this.stopWatcher(); this.updateSnapshot(); this.state.timers.watcher=setInterval(()=> this.state.isOpen && this.checkChanges(), CONFIG.watchInterval); }
|
||||
stopWatcher(){ if(this.state.timers.watcher){ clearInterval(this.state.timers.watcher); this.state.timers.watcher=null; } }
|
||||
|
||||
updateSnapshot(){ this.variableSnapshot={ character:this.makeSnapshotMap('character'), global:this.makeSnapshotMap('global') }; }
|
||||
|
||||
expandChangedKeys(changed){
|
||||
['character','global'].forEach(t=>{
|
||||
const set=changed[t]; if(!set?.size) return;
|
||||
setTimeout(()=>{
|
||||
const list=$(`#${t}-variables-list .vm-item[data-key]`);
|
||||
set.forEach(k=> list.filter((_,el)=>$(el).data('key')===k).addClass('expanded'));
|
||||
},10);
|
||||
});
|
||||
}
|
||||
|
||||
checkChanges(){
|
||||
try{
|
||||
const sum=JSON.stringify(getRulesTable()||{});
|
||||
if(sum!==this.state.rulesChecksum){
|
||||
this.state.rulesChecksum=sum;
|
||||
const keep=this.saveAllExpandedStates();
|
||||
this.loadVariables(); this.restoreAllExpandedStates(keep);
|
||||
}
|
||||
const cur={ character:this.makeSnapshotMap('character'), global:this.makeSnapshotMap('global') };
|
||||
const changed={character:new Set(), global:new Set()};
|
||||
['character','global'].forEach(t=>{
|
||||
const prev=this.variableSnapshot?.[t]||{}, now=cur[t];
|
||||
new Set([...Object.keys(prev),...Object.keys(now)]).forEach(k=>{ if(!(k in prev)||!(k in now)||prev[k]!==now[k]) changed[t].add(k);});
|
||||
});
|
||||
if(changed.character.size||changed.global.size){
|
||||
const keep=this.saveAllExpandedStates();
|
||||
this.variableSnapshot=cur; this.loadVariables(); this.restoreAllExpandedStates(keep); this.expandChangedKeys(changed);
|
||||
}
|
||||
}catch{}
|
||||
}
|
||||
|
||||
loadVariables(){
|
||||
['character','global'].forEach(t=>{
|
||||
this.renderVariables(t);
|
||||
$(`#${t}-variables-section [data-act="collapse"] i`).removeClass('fa-chevron-up').addClass('fa-chevron-down');
|
||||
});
|
||||
}
|
||||
|
||||
renderVariables(t){
|
||||
const c=$(`#${t}-variables-list`).empty(), s=this.store(t), root=Object.entries(s);
|
||||
if(!root.length) c.append('<div class="vm-empty-message">暂无变量</div>');
|
||||
else root.forEach(([k,v])=> c.append(this.createVariableItem(t,k,v,0,[k])));
|
||||
}
|
||||
|
||||
createVariableItem(t,k,v,l=0,fullPath=[]){
|
||||
const parsed=this.parseValue(v), hasChildren=typeof parsed==='object' && parsed!==null;
|
||||
const disp = l===0? this.formatTopLevelValue(v) : this.formatValue(v);
|
||||
const ruleNode=getRuleNodeByPath(fullPath);
|
||||
return $(`<div class="vm-item ${l>0?'vm-tree-level-var':''}" data-key="${k}" data-type="${t||''}" ${l>0?`data-level="${l}"`:''} data-path="${this.escape(pathKey(fullPath))}">
|
||||
<div class="vm-item-header">
|
||||
<div class="vm-item-name vm-item-name-visible">${this.escape(k)}${badgesHtml(ruleNode)}<span class="vm-item-separator">:</span></div>
|
||||
<div class="vm-tree-value">${disp}</div>
|
||||
<div class="vm-item-controls">${this.createButtons()}</div>
|
||||
</div>
|
||||
${hasChildren?`<div class="vm-item-content">${this.renderChildren(parsed,l+1,fullPath)}</div>`:''}
|
||||
</div>`);
|
||||
}
|
||||
|
||||
createButtons(){
|
||||
return [
|
||||
['edit','fa-edit','编辑'],
|
||||
['add-child','fa-plus-circle','添加子变量'],
|
||||
['copy','fa-eye-dropper','复制(长按: 宏,单击: 变量路径)'],
|
||||
['delete','fa-trash','删除'],
|
||||
].map(([act,ic,ti])=>`<button class="vm-btn" data-act="${act}" title="${ti}"><i class="fa-solid ${ic}"></i></button>`).join('');
|
||||
}
|
||||
|
||||
createInlineForm(t,target,fs){
|
||||
const fid=`inline-form-${Date.now()}`;
|
||||
const inf=$(`
|
||||
<div class="vm-inline-form" id="${fid}" data-type="${t}">
|
||||
<div class="vm-form-row"><label class="vm-form-label">名称:</label><input type="text" class="vm-input vm-form-input inline-name" placeholder="变量名称"></div>
|
||||
<div class="vm-form-row"><label class="vm-form-label">值:</label><textarea class="vm-textarea vm-form-input inline-value" placeholder="变量值 (支持JSON格式)"></textarea></div>
|
||||
<div class="vm-form-buttons">
|
||||
<button class="vm-btn" data-act="inline-save"><i class="fa-solid fa-floppy-disk"></i>保存</button>
|
||||
<button class="vm-btn" data-act="inline-cancel">取消</button>
|
||||
</div>
|
||||
</div>`);
|
||||
this.state.currentInlineForm?.remove();
|
||||
target.after(inf); this.state.currentInlineForm=inf; this.state.formState={...fs,formId:fid,targetItem:target};
|
||||
const ta=inf.find('.inline-value'); ta.on('input',()=>this.autoResizeTextarea(ta));
|
||||
setTimeout(()=>{ inf.addClass('active'); inf.find('.inline-name').focus(); },10);
|
||||
return inf;
|
||||
}
|
||||
|
||||
renderChildren(obj,level,parentPath){ return Object.entries(obj).map(([k,v])=> this.createVariableItem(null,k,v,level,[...(parentPath||[]),k])[0].outerHTML).join(''); }
|
||||
|
||||
handleTouch(e){
|
||||
if($(e.target).closest('.vm-item-controls').length) return;
|
||||
e.stopPropagation();
|
||||
const item=$(e.currentTarget).closest('.vm-item'); $('.vm-item').removeClass('touched'); item.addClass('touched');
|
||||
this.clearTouchTimer(item);
|
||||
const t=setTimeout(()=>{ item.removeClass('touched'); this.state.timers.touch.delete(item[0]); },CONFIG.touchTimeout);
|
||||
this.state.timers.touch.set(item[0],t);
|
||||
}
|
||||
clearTouchTimer(i){ const t=this.state.timers.touch.get(i[0]); if(t){ clearTimeout(t); this.state.timers.touch.delete(i[0]); } }
|
||||
|
||||
handleItemClick(e){
|
||||
if($(e.target).closest('.vm-item-controls').length) return;
|
||||
e.stopPropagation();
|
||||
$(e.currentTarget).closest('.vm-item').toggleClass('expanded');
|
||||
}
|
||||
|
||||
async writeClipboard(txt){
|
||||
try{
|
||||
if(navigator.clipboard && window.isSecureContext) await navigator.clipboard.writeText(txt);
|
||||
else { const ta=document.createElement('textarea'); Object.assign(ta.style,{position:'fixed',top:0,left:0,width:'2em',height:'2em',padding:0,border:'none',outline:'none',boxShadow:'none',background:'transparent'}); ta.value=txt; document.body.appendChild(ta); ta.focus(); ta.select(); document.execCommand('copy'); document.body.removeChild(ta); }
|
||||
return true;
|
||||
}catch{ return false; }
|
||||
}
|
||||
|
||||
handleCopy(e,longPress){
|
||||
const item=$(e.target).closest('.vm-item'), path=this.getItemPath(item), t=this.getVariableType(item), level=parseInt(item.attr('data-level'))||0;
|
||||
const formatted=this.formatPath(t,path); let cmd='';
|
||||
if(longPress){
|
||||
if(t==='character'){
|
||||
cmd = level===0 ? `{{getvar::${path[0]}}}` : `{{xbgetvar::${formatted}}}`;
|
||||
}else{
|
||||
cmd = `{{getglobalvar::${path[0]}}}`;
|
||||
if(level>0) toastr.info('全局变量宏暂不支持子路径,已复制顶级变量');
|
||||
}
|
||||
}else cmd=formatted;
|
||||
(async()=> (await this.writeClipboard(cmd)) ? toastr.success(`已复制: ${cmd}`) : toastr.error('复制失败'))();
|
||||
}
|
||||
|
||||
editAction(item,action,type,path){
|
||||
const inf=this.createInlineForm(type,item,{action,path,type});
|
||||
if(action==='edit'){
|
||||
const v=this.getValueByPath(type,path);
|
||||
setTimeout(()=>{
|
||||
inf.find('.inline-name').val(path[path.length-1]);
|
||||
const ta=inf.find('.inline-value');
|
||||
const fill=(val)=> Array.isArray(val)? (val.length===1 ? String(val[0]??'') : JSON.stringify(val,null,2)) : (val&&typeof val==='object'? JSON.stringify(val,null,2) : String(val??''));
|
||||
ta.val(fill(v)); this.autoResizeTextarea(ta);
|
||||
},50);
|
||||
}else if(action==='addChild'){
|
||||
inf.find('.inline-name').attr('placeholder',`为 "${path.join('.')}" 添加子变量名称`);
|
||||
inf.find('.inline-value').attr('placeholder','子变量值 (支持JSON格式)');
|
||||
}
|
||||
}
|
||||
|
||||
handleDelete(_item,t,path){
|
||||
const n=path[path.length-1];
|
||||
if(!confirm(`确定要删除 "${n}" 吗?`)) return;
|
||||
this.withGlobalRefresh(()=> this.deleteByPathSilently(t,path));
|
||||
toastr.success('变量已删除');
|
||||
}
|
||||
|
||||
refreshAndKeep(t,states){ this.vt(t).save(); this.loadVariables(); this.updateSnapshot(); states && this.restoreExpandedStates(t,states); }
|
||||
withPreservedExpansion(t,fn){ const s=this.saveExpandedStates(t); fn(); this.refreshAndKeep(t,s); }
|
||||
withGlobalRefresh(fn){ const s=this.saveAllExpandedStates(); fn(); this.loadVariables(); this.updateSnapshot(); this.restoreAllExpandedStates(s); }
|
||||
|
||||
handleInlineSave(form){
|
||||
if(this.savingInProgress) return; this.savingInProgress=true;
|
||||
try{
|
||||
if(!form?.length) return toastr.error('表单未找到');
|
||||
const rawName=form.find('.inline-name').val();
|
||||
const rawValue=form.find('.inline-value').val();
|
||||
const name= typeof rawName==='string'? rawName.trim() : String(rawName ?? '').trim();
|
||||
const value= typeof rawValue==='string'? rawValue.trim() : String(rawValue ?? '').trim();
|
||||
const type=form.data('type');
|
||||
if(!name) return form.find('.inline-name').focus(), toastr.error('请输入变量名称');
|
||||
const val=this.processValue(value), {action,path}=this.state.formState;
|
||||
this.withPreservedExpansion(type,()=>{
|
||||
if(action==='addChild') {
|
||||
this.setValueByPath(type,[...path,name],val);
|
||||
} else if(action==='edit'){
|
||||
const old=path[path.length-1];
|
||||
if(name!==old){
|
||||
this.deleteByPathSilently(type,path);
|
||||
if(path.length===1) {
|
||||
const toSave=(typeof val==='object'&&val!==null)?JSON.stringify(val):val;
|
||||
this.vt(type).setter(name,toSave);
|
||||
} else {
|
||||
this.setValueByPath(type,[...path.slice(0,-1),name],val);
|
||||
}
|
||||
} else {
|
||||
this.setValueByPath(type,path,val);
|
||||
}
|
||||
} else {
|
||||
const toSave=(typeof val==='object'&&val!==null)?JSON.stringify(val):val;
|
||||
this.vt(type).setter(name,toSave);
|
||||
}
|
||||
});
|
||||
this.hideInlineForm(); toastr.success('变量已保存');
|
||||
}catch(e){ toastr.error('JSON格式错误: '+e.message); }
|
||||
finally{ this.savingInProgress=false; }
|
||||
}
|
||||
hideInlineForm(){ if(this.state.currentInlineForm){ this.state.currentInlineForm.removeClass('active'); setTimeout(()=>{ this.state.currentInlineForm?.remove(); this.state.currentInlineForm=null; },200);} this.state.formState={}; }
|
||||
|
||||
showAddForm(t){
|
||||
this.hideInlineForm();
|
||||
$(`#${t}-vm-add-form`).addClass('active');
|
||||
const ta = $(`#${t}-vm-value`);
|
||||
$(`#${t}-vm-name`).val('').attr('placeholder','变量名称').focus();
|
||||
ta.val('').attr('placeholder','变量值 (支持JSON格式)');
|
||||
if(!ta.data('auto-resize-bound')){ ta.on('input',()=>this.autoResizeTextarea(ta)); ta.data('auto-resize-bound',true); }
|
||||
}
|
||||
hideAddForm(t){ $(`#${t}-vm-add-form`).removeClass('active'); $(`#${t}-vm-name, #${t}-vm-value`).val(''); this.state.formState={}; }
|
||||
|
||||
saveAddVariable(t){
|
||||
if(this.savingInProgress) return; this.savingInProgress=true;
|
||||
try{
|
||||
const rawN=$(`#${t}-vm-name`).val();
|
||||
const rawV=$(`#${t}-vm-value`).val();
|
||||
const n= typeof rawN==='string' ? rawN.trim() : String(rawN ?? '').trim();
|
||||
const v= typeof rawV==='string' ? rawV.trim() : String(rawV ?? '').trim();
|
||||
if(!n) return toastr.error('请输入变量名称');
|
||||
const val=this.processValue(v);
|
||||
this.withPreservedExpansion(t,()=> {
|
||||
const toSave=(typeof val==='object'&&val!==null)?JSON.stringify(val):val;
|
||||
this.vt(t).setter(n,toSave);
|
||||
});
|
||||
this.hideAddForm(t); toastr.success('变量已保存');
|
||||
}catch(e){ toastr.error('JSON格式错误: '+e.message); }
|
||||
finally{ this.savingInProgress=false; }
|
||||
}
|
||||
|
||||
getValueByPath(t,p){ if(p.length===1) return this.vt(t).getter(p[0]); let v=this.parseValue(this.vt(t).getter(p[0])); p.slice(1).forEach(k=> v=v?.[k]); return v; }
|
||||
|
||||
setValueByPath(t,p,v){
|
||||
if(p.length===1){
|
||||
const toSave = (typeof v==='object' && v!==null) ? JSON.stringify(v) : v;
|
||||
this.vt(t).setter(p[0], toSave);
|
||||
return;
|
||||
}
|
||||
let root=this.parseValue(this.vt(t).getter(p[0])); if(typeof root!=='object'||root===null) root={};
|
||||
let cur=root; p.slice(1,-1).forEach(k=>{ if(typeof cur[k]!=='object'||cur[k]===null) cur[k]={}; cur=cur[k]; });
|
||||
cur[p[p.length-1]]=v; this.vt(t).setter(p[0], JSON.stringify(root));
|
||||
}
|
||||
|
||||
deleteByPathSilently(t,p){
|
||||
if(p.length===1){ delete this.store(t)[p[0]]; return; }
|
||||
let root=this.parseValue(this.vt(t).getter(p[0])); if(typeof root!=='object'||root===null) return;
|
||||
let cur=root; p.slice(1,-1).forEach(k=>{ if(typeof cur[k]!=='object'||cur[k]===null) cur[k]={}; cur=cur[k]; });
|
||||
delete cur[p[p.length-1]]; this.vt(t).setter(p[0], JSON.stringify(root));
|
||||
}
|
||||
|
||||
formatPath(t,path){
|
||||
if(!Array.isArray(path)||!path.length) return '';
|
||||
let out=String(path[0]), cur=this.parseValue(this.vt(t).getter(path[0]));
|
||||
for(let i=1;i<path.length;i++){
|
||||
const k=String(path[i]), isNum=/^\d+$/.test(k);
|
||||
if(Array.isArray(cur) && isNum){ out+=`[${Number(k)}]`; cur=cur?.[Number(k)]; }
|
||||
else { out+=`.`+k; cur=cur?.[k]; }
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
getVariableType(it){ return it.data('type') || (it.closest('.vm-section').attr('id').includes('character')?'character':'global'); }
|
||||
getItemPath(i){ const p=[]; let c=i; while(c.length&&c.hasClass('vm-item')){ const k=c.data('key'); if(k!==undefined) p.unshift(String(k)); if(!c.attr('data-level')) break; c=c.parent().closest('.vm-item'); } return p; }
|
||||
|
||||
parseValue(v){ try{ return typeof v==='string'? JSON.parse(v) : v; }catch{ return v; } }
|
||||
processValue(v){ if(typeof v!=='string') return v; const s=v.trim(); return (s.startsWith('{')||s.startsWith('['))? JSON.parse(s) : v; }
|
||||
|
||||
formatTopLevelValue(v){ const p=this.parseValue(v); if(typeof p==='object'&&p!==null){ const c=Array.isArray(p)? p.length : Object.keys(p).length; return `<span class="vm-object-count">[${c} items]</span>`; } return this.formatValue(p); }
|
||||
formatValue(v){ if(v==null) return `<span class="vm-null-value">${v}</span>`; const e=this.escape(String(v)); return `<span class="vm-formatted-value">${e.length>50? e.substring(0,50)+'...' : e}</span>`; }
|
||||
escape(t){ const d=document.createElement('div'); d.textContent=t; return d.innerHTML; }
|
||||
autoResizeTextarea(ta){ if(!ta?.length) return; const el=ta[0]; el.style.height='auto'; const sh=el.scrollHeight, max=Math.min(300,window.innerHeight*0.4), fh=Math.max(60,Math.min(max,sh+4)); el.style.height=fh+'px'; el.style.overflowY=sh>max-4?'auto':'hidden'; }
|
||||
searchVariables(t,q){ const l=q.toLowerCase().trim(); $(`#${t}-variables-list .vm-item`).each(function(){ $(this).toggle(!l || $(this).text().toLowerCase().includes(l)); }); }
|
||||
collapseAll(t){ const items=$(`#${t}-variables-list .vm-item`), icon=$(`#${t}-variables-section [data-act="collapse"] i`); const any=items.filter('.expanded').length>0; items.toggleClass('expanded',!any); icon.toggleClass('fa-chevron-up',!any).toggleClass('fa-chevron-down',any); }
|
||||
|
||||
clearAllVariables(t){
|
||||
if(!confirm(`确定要清除所有${t==='character'?'角色':'全局'}变量吗?`)) return;
|
||||
this.withPreservedExpansion(t,()=>{ const s=this.store(t); Object.keys(s).forEach(k=> delete s[k]); });
|
||||
toastr.success('变量已清除');
|
||||
}
|
||||
|
||||
async importVariables(t){
|
||||
const inp=document.createElement('input'); inp.type='file'; inp.accept='.json';
|
||||
inp.onchange=async(e)=>{
|
||||
try{
|
||||
const tgt=e.target;
|
||||
const file = (tgt && 'files' in tgt && tgt.files && tgt.files[0]) ? tgt.files[0] : null;
|
||||
if(!file) throw new Error('未选择文件');
|
||||
const txt=await file.text(), v=JSON.parse(txt);
|
||||
this.withPreservedExpansion(t,()=> {
|
||||
Object.entries(v).forEach(([k,val])=> {
|
||||
const toSave=(typeof val==='object'&&val!==null)?JSON.stringify(val):val;
|
||||
this.vt(t).setter(k,toSave);
|
||||
});
|
||||
});
|
||||
toastr.success(`成功导入 ${Object.keys(v).length} 个变量`);
|
||||
}catch{ toastr.error('文件格式错误'); }
|
||||
};
|
||||
inp.click();
|
||||
}
|
||||
|
||||
exportVariables(t){
|
||||
const v=this.store(t), b=new Blob([JSON.stringify(v,null,2)],{type:'application/json'}), a=document.createElement('a');
|
||||
a.href=URL.createObjectURL(b); a.download=`${t}-variables-${new Date().toISOString().split('T')[0]}.json`; a.click();
|
||||
toastr.success('变量已导出');
|
||||
}
|
||||
|
||||
saveExpandedStates(t){ const s=new Set(); $(`#${t}-variables-list .vm-item.expanded`).each(function(){ const k=$(this).data('key'); if(k!==undefined) s.add(String(k)); }); return s; }
|
||||
saveAllExpandedStates(){ return { character:this.saveExpandedStates('character'), global:this.saveExpandedStates('global') }; }
|
||||
restoreExpandedStates(t,s){ if(!s?.size) return; setTimeout(()=>{ $(`#${t}-variables-list .vm-item`).each(function(){ const k=$(this).data('key'); if(k!==undefined && s.has(String(k))) $(this).addClass('expanded'); }); },50); }
|
||||
restoreAllExpandedStates(st){ Object.entries(st).forEach(([t,s])=> this.restoreExpandedStates(t,s)); }
|
||||
|
||||
toggleEnabled(en){
|
||||
const s=this.getSettings(); s.enabled=this.state.isEnabled=en; saveSettingsDebounced(); this.syncCheckbox();
|
||||
en ? (this.enable(),this.open()) : this.disable();
|
||||
}
|
||||
|
||||
createPerMessageBtn(messageId){
|
||||
const btn=document.createElement('div');
|
||||
btn.className='mes_btn mes_variables_panel';
|
||||
btn.title='变量面板';
|
||||
btn.dataset.mid=messageId;
|
||||
btn.innerHTML='<i class="fa-solid fa-database"></i>';
|
||||
btn.addEventListener('click',(e)=>{ e.preventDefault(); e.stopPropagation(); this.open(); });
|
||||
return btn;
|
||||
}
|
||||
|
||||
addButtonToMessage(messageId){
|
||||
const msg=$(`#chat .mes[mesid="${messageId}"]`);
|
||||
if(!msg.length || msg.find('.mes_btn.mes_variables_panel').length) return;
|
||||
const btn=this.createPerMessageBtn(messageId);
|
||||
const appendToFlex=(m)=>{ const flex=m.find('.flex-container.flex1.alignitemscenter'); if(flex.length) flex.append(btn); };
|
||||
if(typeof window['registerButtonToSubContainer']==='function'){
|
||||
const ok=window['registerButtonToSubContainer'](messageId,btn);
|
||||
if(!ok) appendToFlex(msg);
|
||||
} else appendToFlex(msg);
|
||||
}
|
||||
|
||||
addButtonsToAllMessages(){ $('#chat .mes').each((_,el)=>{ const mid=el.getAttribute('mesid'); if(mid) this.addButtonToMessage(mid); }); }
|
||||
removeAllMessageButtons(){ $('#chat .mes .mes_btn.mes_variables_panel').remove(); }
|
||||
|
||||
installMessageButtons(){
|
||||
const delayedAdd=(id)=> setTimeout(()=>{ if(id!=null) this.addButtonToMessage(id); },120);
|
||||
const delayedScan=()=> setTimeout(()=> this.addButtonsToAllMessages(),150);
|
||||
this.removeMessageButtonsListeners();
|
||||
const idFrom=(d)=> typeof d==='object' ? (d.messageId||d.id) : d;
|
||||
|
||||
if (!this.msgEvents) this.msgEvents = createModuleEvents('variablesPanel:messages');
|
||||
|
||||
this.msgEvents.onMany([
|
||||
event_types.USER_MESSAGE_RENDERED,
|
||||
event_types.CHARACTER_MESSAGE_RENDERED,
|
||||
event_types.MESSAGE_RECEIVED,
|
||||
event_types.MESSAGE_UPDATED,
|
||||
event_types.MESSAGE_SWIPED,
|
||||
event_types.MESSAGE_EDITED
|
||||
].filter(Boolean), (d) => delayedAdd(idFrom(d)));
|
||||
|
||||
this.msgEvents.on(event_types.MESSAGE_SENT, debounce(() => delayedScan(), 300));
|
||||
this.msgEvents.on(event_types.CHAT_CHANGED, () => delayedScan());
|
||||
|
||||
this.addButtonsToAllMessages();
|
||||
}
|
||||
|
||||
removeMessageButtonsListeners(){
|
||||
if (this.msgEvents) {
|
||||
this.msgEvents.cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
removeMessageButtons(){ this.removeMessageButtonsListeners(); this.removeAllMessageButtons(); }
|
||||
|
||||
normalizeStore(t){
|
||||
const s=this.store(t); let changed=0;
|
||||
for(const[k,v] of Object.entries(s)){
|
||||
if(typeof v==='object' && v!==null){
|
||||
try{ s[k]=JSON.stringify(v); changed++; }catch{}
|
||||
}
|
||||
}
|
||||
if(changed) this.vt(t).save?.();
|
||||
}
|
||||
}
|
||||
|
||||
let variablesPanelInstance=null;
|
||||
|
||||
export async function initVariablesPanel(){
|
||||
try{
|
||||
extension_settings.variables ??= { global:{} };
|
||||
if(variablesPanelInstance) variablesPanelInstance.cleanup();
|
||||
variablesPanelInstance=new VariablesPanel();
|
||||
await variablesPanelInstance.init();
|
||||
return variablesPanelInstance;
|
||||
}catch(e){
|
||||
console.error(`[${CONFIG.extensionName}] 加载失败:`,e);
|
||||
toastr?.error?.('Variables Panel加载失败');
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
export function getVariablesPanelInstance(){ return variablesPanelInstance; }
|
||||
export function cleanupVariablesPanel(){ if(variablesPanelInstance){ variablesPanelInstance.removeMessageButtons(); variablesPanelInstance.cleanup(); variablesPanelInstance=null; } }
|
||||
Reference in New Issue
Block a user