Files
LittleWhiteBox/modules/variables/variables-core.js
2026-01-17 16:34:39 +08:00

2390 lines
82 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* @file modules/variables/variables-core.js
* @description 变量管理核心(受开关控制)
* @description 包含 plot-log 解析、快照回滚、变量守护
*/
import { getContext } from "../../../../../extensions.js";
import { updateMessageBlock } from "../../../../../../script.js";
import { getLocalVariable, setLocalVariable } from "../../../../../variables.js";
import { createModuleEvents, event_types } from "../../core/event-manager.js";
import { xbLog, CacheRegistry } from "../../core/debug-core.js";
import {
normalizePath,
lwbSplitPathWithBrackets,
splitPathSegments,
ensureDeepContainer,
setDeepValue,
pushDeepValue,
deleteDeepKey,
getRootAndPath,
joinPath,
safeJSONStringify,
maybeParseObject,
deepClone,
} from "../../core/variable-path.js";
import {
parseDirectivesTokenList,
applyXbGetVarForMessage,
parseValueForSet,
} from "./var-commands.js";
import {
preprocessBumpAliases,
executeQueuedVareventJsAfterTurn,
stripYamlInlineComment,
OP_MAP,
TOP_OP_RE,
} from "./varevent-editor.js";
/* ============= 模块常量 ============= */
const MODULE_ID = 'variablesCore';
const LWB_RULES_KEY = 'LWB_RULES';
const LWB_SNAP_KEY = 'LWB_SNAP';
const LWB_PLOT_APPLIED_KEY = 'LWB_PLOT_APPLIED_KEY';
// plot-log 标签正则
const TAG_RE_PLOTLOG = /<\s*plot-log[^>]*>([\s\S]*?)<\s*\/\s*plot-log\s*>/gi;
// 守护状态
const guardianState = {
table: {},
regexCache: {},
bypass: false,
origVarApi: null,
lastMetaSyncAt: 0
};
// 事件管理器
let events = null;
let initialized = false;
let pendingSwipeApply = new Map();
let suppressUpdatedOnce = new Set();
CacheRegistry.register(MODULE_ID, {
name: '变量系统缓存',
getSize: () => {
try {
const applied = Object.keys(getAppliedMap() || {}).length;
const snaps = Object.keys(getSnapMap() || {}).length;
const rules = Object.keys(guardianState.table || {}).length;
const regex = Object.keys(guardianState.regexCache || {}).length;
const swipe = (typeof pendingSwipeApply !== 'undefined' && pendingSwipeApply?.size) ? pendingSwipeApply.size : 0;
const sup = (typeof suppressUpdatedOnce !== 'undefined' && suppressUpdatedOnce?.size) ? suppressUpdatedOnce.size : 0;
return applied + snaps + rules + regex + swipe + sup;
} catch {
return 0;
}
},
// 新增:估算字节大小(用于 debug-panel 缓存统计)
getBytes: () => {
try {
let total = 0;
const snaps = getSnapMap();
if (snaps && typeof snaps === 'object') {
total += JSON.stringify(snaps).length * 2; // UTF-16
}
const applied = getAppliedMap();
if (applied && typeof applied === 'object') {
total += JSON.stringify(applied).length * 2; // UTF-16
}
const rules = guardianState.table;
if (rules && typeof rules === 'object') {
total += JSON.stringify(rules).length * 2; // UTF-16
}
const regex = guardianState.regexCache;
if (regex && typeof regex === 'object') {
total += JSON.stringify(regex).length * 2; // UTF-16
}
if (typeof pendingSwipeApply !== 'undefined' && pendingSwipeApply?.size) {
total += JSON.stringify(Array.from(pendingSwipeApply)).length * 2; // UTF-16
}
if (typeof suppressUpdatedOnce !== 'undefined' && suppressUpdatedOnce?.size) {
total += JSON.stringify(Array.from(suppressUpdatedOnce)).length * 2; // UTF-16
}
return total;
} catch {
return 0;
}
},
clear: () => {
try {
const meta = getContext()?.chatMetadata || {};
try { delete meta[LWB_PLOT_APPLIED_KEY]; } catch {}
try { delete meta[LWB_SNAP_KEY]; } catch {}
} catch {}
try { guardianState.regexCache = {}; } catch {}
try { pendingSwipeApply?.clear?.(); } catch {}
try { suppressUpdatedOnce?.clear?.(); } catch {}
},
getDetail: () => {
try {
return {
appliedSignatures: Object.keys(getAppliedMap() || {}).length,
snapshots: Object.keys(getSnapMap() || {}).length,
rulesTableKeys: Object.keys(guardianState.table || {}).length,
rulesRegexCacheKeys: Object.keys(guardianState.regexCache || {}).length,
pendingSwipeApply: (typeof pendingSwipeApply !== 'undefined' && pendingSwipeApply?.size) ? pendingSwipeApply.size : 0,
suppressUpdatedOnce: (typeof suppressUpdatedOnce !== 'undefined' && suppressUpdatedOnce?.size) ? suppressUpdatedOnce.size : 0,
};
} catch {
return {};
}
},
});
/* ============= 内部辅助函数 ============= */
function getMsgKey(msg) {
return (typeof msg?.mes === 'string') ? 'mes'
: (typeof msg?.content === 'string' ? 'content' : null);
}
function stripLeadingHtmlComments(s) {
let t = String(s ?? '');
t = t.replace(/^\uFEFF/, '');
while (true) {
const m = t.match(/^\s*<!--[\s\S]*?-->\s*/);
if (!m) break;
t = t.slice(m[0].length);
}
return t;
}
function normalizeOpName(k) {
if (!k) return null;
return OP_MAP[String(k).toLowerCase().trim()] || null;
}
/* ============= 应用签名追踪 ============= */
function getAppliedMap() {
const meta = getContext()?.chatMetadata || {};
const m = meta[LWB_PLOT_APPLIED_KEY];
if (m && typeof m === 'object') return m;
meta[LWB_PLOT_APPLIED_KEY] = {};
return meta[LWB_PLOT_APPLIED_KEY];
}
function setAppliedSignature(messageId, sig) {
const map = getAppliedMap();
if (sig) map[messageId] = sig;
else delete map[messageId];
getContext()?.saveMetadataDebounced?.();
}
function clearAppliedFrom(messageIdInclusive) {
const map = getAppliedMap();
for (const k of Object.keys(map)) {
const id = Number(k);
if (!Number.isNaN(id) && id >= messageIdInclusive) {
delete map[k];
}
}
getContext()?.saveMetadataDebounced?.();
}
function clearAppliedFor(messageId) {
const map = getAppliedMap();
delete map[messageId];
getContext()?.saveMetadataDebounced?.();
}
function computePlotSignatureFromText(text) {
if (!text || typeof text !== 'string') return '';
TAG_RE_PLOTLOG.lastIndex = 0;
const chunks = [];
let m;
while ((m = TAG_RE_PLOTLOG.exec(text)) !== null) {
chunks.push((m[0] || '').trim());
}
if (!chunks.length) return '';
return chunks.join('\n---\n');
}
/* ============= Plot-Log 解析 ============= */
/**
* 提取 plot-log 块
*/
function extractPlotLogBlocks(text) {
if (!text || typeof text !== 'string') return [];
const out = [];
TAG_RE_PLOTLOG.lastIndex = 0;
let m;
while ((m = TAG_RE_PLOTLOG.exec(text)) !== null) {
const inner = m[1] ?? '';
if (inner.trim()) out.push(inner);
}
return out;
}
/**
* 解析 plot-log 块内容
*/
function parseBlock(innerText) {
// 预处理 bump 别名
innerText = preprocessBumpAliases(innerText);
const textForJsonToml = stripLeadingHtmlComments(innerText);
const ops = { set: {}, push: {}, bump: {}, del: {} };
const lines = String(innerText || '').split(/\r?\n/);
const indentOf = (s) => s.length - s.trimStart().length;
const stripQ = (s) => {
let t = String(s ?? '').trim();
if ((t.startsWith('"') && t.endsWith('"')) || (t.startsWith("'") && t.endsWith("'"))) {
t = t.slice(1, -1);
}
return t;
};
const norm = (p) => String(p || '').replace(/\[(\d+)\]/g, '.$1');
// 守护指令记录
const guardMap = new Map();
const recordGuardDirective = (path, directives) => {
const tokens = Array.isArray(directives)
? directives.map(t => String(t || '').trim()).filter(Boolean)
: [];
if (!tokens.length) return;
const normalizedPath = norm(path);
if (!normalizedPath) return;
let bag = guardMap.get(normalizedPath);
if (!bag) {
bag = new Set();
guardMap.set(normalizedPath, bag);
}
for (const tok of tokens) {
if (tok) bag.add(tok);
}
};
const extractDirectiveInfo = (rawKey) => {
const text = String(rawKey || '').trim().replace(/:$/, '');
if (!text) return { directives: [], remainder: '', original: '' };
const directives = [];
let idx = 0;
while (idx < text.length) {
while (idx < text.length && /\s/.test(text[idx])) idx++;
if (idx >= text.length) break;
if (text[idx] !== '$') break;
const start = idx;
idx++;
while (idx < text.length && !/\s/.test(text[idx])) idx++;
directives.push(text.slice(start, idx));
}
const remainder = text.slice(idx).trim();
const seg = remainder || text;
return { directives, remainder: seg, original: text };
};
const buildPathInfo = (rawKey, parentPath) => {
const parent = String(parentPath || '').trim();
const { directives, remainder, original } = extractDirectiveInfo(rawKey);
const segTrim = String(remainder || original || '').trim();
const curPathRaw = segTrim ? (parent ? `${parent}.${segTrim}` : segTrim) : parent;
const guardTargetRaw = directives.length ? (segTrim ? curPathRaw : parent || curPathRaw) : '';
return { directives, curPathRaw, guardTargetRaw, segment: segTrim };
};
// 操作记录函数
const putSet = (top, path, value) => {
ops.set[top] ||= {};
ops.set[top][path] = value;
};
const putPush = (top, path, value) => {
ops.push[top] ||= {};
const arr = (ops.push[top][path] ||= []);
Array.isArray(value) ? arr.push(...value) : arr.push(value);
};
const putBump = (top, path, delta) => {
const n = Number(String(delta).replace(/^\+/, ''));
if (!Number.isFinite(n)) return;
ops.bump[top] ||= {};
ops.bump[top][path] = (ops.bump[top][path] ?? 0) + n;
};
const putDel = (top, path) => {
ops.del[top] ||= [];
ops.del[top].push(path);
};
const finalizeResults = () => {
const results = [];
for (const [top, flat] of Object.entries(ops.set)) {
if (flat && Object.keys(flat).length) {
results.push({ name: top, operation: 'setObject', data: flat });
}
}
for (const [top, flat] of Object.entries(ops.push)) {
if (flat && Object.keys(flat).length) {
results.push({ name: top, operation: 'push', data: flat });
}
}
for (const [top, flat] of Object.entries(ops.bump)) {
if (flat && Object.keys(flat).length) {
results.push({ name: top, operation: 'bump', data: flat });
}
}
for (const [top, list] of Object.entries(ops.del)) {
if (Array.isArray(list) && list.length) {
results.push({ name: top, operation: 'del', data: list });
}
}
if (guardMap.size) {
const guardList = [];
for (const [path, tokenSet] of guardMap.entries()) {
const directives = Array.from(tokenSet).filter(Boolean);
if (directives.length) guardList.push({ path, directives });
}
if (guardList.length) {
results.push({ operation: 'guard', data: guardList });
}
}
return results;
};
// 解码键
const decodeKey = (rawKey) => {
const { directives, remainder, original } = extractDirectiveInfo(rawKey);
const path = (remainder || original || String(rawKey)).trim();
if (directives && directives.length) recordGuardDirective(path, directives);
return path;
};
// 遍历节点
const walkNode = (op, top, node, basePath = '') => {
if (op === 'set') {
if (node === null || node === undefined) return;
if (typeof node !== 'object' || Array.isArray(node)) {
putSet(top, norm(basePath), node);
return;
}
for (const [rawK, v] of Object.entries(node)) {
const k = decodeKey(rawK);
const p = norm(basePath ? `${basePath}.${k}` : k);
if (Array.isArray(v)) putSet(top, p, v);
else if (v && typeof v === 'object') walkNode(op, top, v, p);
else putSet(top, p, v);
}
} else if (op === 'push') {
if (!node || typeof node !== 'object' || Array.isArray(node)) return;
for (const [rawK, v] of Object.entries(node)) {
const k = decodeKey(rawK);
const p = norm(basePath ? `${basePath}.${k}` : k);
if (Array.isArray(v)) {
for (const it of v) putPush(top, p, it);
} else if (v && typeof v === 'object') {
walkNode(op, top, v, p);
} else {
putPush(top, p, v);
}
}
} else if (op === 'bump') {
if (!node || typeof node !== 'object' || Array.isArray(node)) return;
for (const [rawK, v] of Object.entries(node)) {
const k = decodeKey(rawK);
const p = norm(basePath ? `${basePath}.${k}` : k);
if (v && typeof v === 'object' && !Array.isArray(v)) {
walkNode(op, top, v, p);
} else {
putBump(top, p, v);
}
}
} else if (op === 'del') {
const acc = new Set();
const collect = (n, base = '') => {
if (Array.isArray(n)) {
for (const it of n) {
if (typeof it === 'string' || typeof it === 'number') {
const seg = typeof it === 'number' ? String(it) : decodeKey(it);
const full = base ? `${base}.${seg}` : seg;
if (full) acc.add(norm(full));
} else if (it && typeof it === 'object') {
collect(it, base);
}
}
} else if (n && typeof n === 'object') {
for (const [rawK, v] of Object.entries(n)) {
const k = decodeKey(rawK);
const nextBase = base ? `${base}.${k}` : k;
if (v && typeof v === 'object') {
collect(v, nextBase);
} else {
const valStr = (v !== null && v !== undefined)
? String(v).trim()
: '';
if (valStr) {
const full = nextBase ? `${nextBase}.${valStr}` : valStr;
acc.add(norm(full));
} else if (nextBase) {
acc.add(norm(nextBase));
}
}
}
} else if (base) {
acc.add(norm(base));
}
};
collect(node, basePath);
for (const p of acc) {
const std = p.replace(/\[(\d+)\]/g, '.$1');
const parts = std.split('.').filter(Boolean);
const t = parts.shift();
const rel = parts.join('.');
if (t) putDel(t, rel);
}
}
};
// 处理结构化数据JSON/TOML
const processStructuredData = (data) => {
const process = (d) => {
if (!d || typeof d !== 'object') return;
for (const [k, v] of Object.entries(d)) {
const op = normalizeOpName(k);
if (!op || v == null) continue;
if (op === 'del' && Array.isArray(v)) {
for (const it of v) {
const std = String(it).replace(/\[(\d+)\]/g, '.$1');
const parts = std.split('.').filter(Boolean);
const top = parts.shift();
const rel = parts.join('.');
if (top) putDel(top, rel);
}
continue;
}
if (typeof v !== 'object') continue;
for (const [rawTop, payload] of Object.entries(v)) {
const top = decodeKey(rawTop);
if (op === 'push') {
if (Array.isArray(payload)) {
for (const it of payload) putPush(top, '', it);
} else if (payload && typeof payload === 'object') {
walkNode(op, top, payload);
} else {
putPush(top, '', payload);
}
} else if (op === 'bump' && (typeof payload !== 'object' || Array.isArray(payload))) {
putBump(top, '', payload);
} else if (op === 'del') {
if (Array.isArray(payload) || (payload && typeof payload === 'object')) {
walkNode(op, top, payload, top);
} else {
const base = norm(top);
if (base) {
const hasValue = payload !== undefined && payload !== null
&& String(payload).trim() !== '';
const full = hasValue ? norm(`${base}.${payload}`) : base;
const std = full.replace(/\[(\d+)\]/g, '.$1');
const parts = std.split('.').filter(Boolean);
const t = parts.shift();
const rel = parts.join('.');
if (t) putDel(t, rel);
}
}
} else {
walkNode(op, top, payload);
}
}
}
};
if (Array.isArray(data)) {
for (const entry of data) {
if (entry && typeof entry === 'object') process(entry);
}
} else {
process(data);
}
return true;
};
// 尝试 JSON 解析
const tryParseJson = (text) => {
const s = String(text || '').trim();
if (!s || (s[0] !== '{' && s[0] !== '[')) return false;
const relaxJson = (src) => {
let out = '', i = 0, inStr = false, q = '', esc = false;
const numRe = /^[+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?$/;
const bareRe = /[A-Za-z_$]|[^\x00-\x7F]/;
while (i < src.length) {
const ch = src[i];
if (inStr) {
out += ch;
if (esc) esc = false;
else if (ch === '\\') esc = true;
else if (ch === q) { inStr = false; q = ''; }
i++;
continue;
}
if (ch === '"' || ch === "'") { inStr = true; q = ch; out += ch; i++; continue; }
if (ch === ':') {
out += ch; i++;
let j = i;
while (j < src.length && /\s/.test(src[j])) { out += src[j]; j++; }
if (j >= src.length || !bareRe.test(src[j])) { i = j; continue; }
let k = j;
while (k < src.length && !/[,}\]\s:]/.test(src[k])) k++;
const tok = src.slice(j, k), low = tok.toLowerCase();
if (low === 'true' || low === 'false' || low === 'null' || numRe.test(tok)) {
out += tok;
} else {
out += `"${tok.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
}
i = k;
continue;
}
out += ch; i++;
}
return out;
};
const attempt = (src) => {
try {
const parsed = JSON.parse(src);
return processStructuredData(parsed);
} catch {
return false;
}
};
if (attempt(s)) return true;
const relaxed = relaxJson(s);
return relaxed !== s && attempt(relaxed);
};
// 尝试 TOML 解析
const tryParseToml = (text) => {
const src = String(text || '').trim();
if (!src || !src.includes('[') || !src.includes('=')) return false;
try {
const parseVal = (raw) => {
const v = String(raw ?? '').trim();
if (v === 'true') return true;
if (v === 'false') return false;
if (/^-?\d+$/.test(v)) return parseInt(v, 10);
if (/^-?\d+\.\d+$/.test(v)) return parseFloat(v);
if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) {
const inner = v.slice(1, -1);
return v.startsWith('"')
? inner.replace(/\\n/g, '\n').replace(/\\t/g, '\t').replace(/\\"/g, '"').replace(/\\'/g, "'").replace(/\\\\/g, '\\')
: inner;
}
if (v.startsWith('[') && v.endsWith(']')) {
try { return JSON.parse(v.replace(/'/g, '"')); } catch { return v; }
}
return v;
};
const L = src.split(/\r?\n/);
let i = 0, curOp = '';
while (i < L.length) {
let line = L[i].trim();
i++;
if (!line || line.startsWith('#')) continue;
const sec = line.match(/\[\s*([^\]]+)\s*\]$/);
if (sec) {
curOp = normalizeOpName(sec[1]) || '';
continue;
}
if (!curOp) continue;
const kv = line.match(/^([^=]+)=(.*)$/);
if (!kv) continue;
const keyRaw = kv[1].trim();
const rhsRaw = kv[2];
const hasTriple = rhsRaw.includes('"""') || rhsRaw.includes("'''");
const rhs = hasTriple ? rhsRaw : stripYamlInlineComment(rhsRaw);
const cleaned = stripQ(keyRaw);
const { directives, remainder, original } = extractDirectiveInfo(cleaned);
const core = remainder || original || cleaned;
const segs = core.split('.').map(seg => stripQ(String(seg).trim())).filter(Boolean);
if (!segs.length) continue;
const top = segs[0];
const rest = segs.slice(1);
const relNorm = norm(rest.join('.'));
if (directives && directives.length) {
recordGuardDirective(norm(segs.join('.')), directives);
}
if (!hasTriple) {
const value = parseVal(rhs);
if (curOp === 'set') putSet(top, relNorm, value);
else if (curOp === 'push') putPush(top, relNorm, value);
else if (curOp === 'bump') putBump(top, relNorm, value);
else if (curOp === 'del') putDel(top, relNorm || norm(segs.join('.')));
}
}
return true;
} catch {
return false;
}
};
// 尝试 JSON/TOML
if (tryParseJson(textForJsonToml)) return finalizeResults();
if (tryParseToml(textForJsonToml)) return finalizeResults();
// YAML 解析
let curOp = '';
const stack = [];
const readList = (startIndex, parentIndent) => {
const out = [];
let i = startIndex;
for (; i < lines.length; i++) {
const raw = lines[i];
const t = raw.trim();
if (!t) continue;
const ind = indentOf(raw);
if (ind <= parentIndent) break;
const m = t.match(/^-+\s*(.+)$/);
if (m) out.push(stripQ(stripYamlInlineComment(m[1])));
else break;
}
return { arr: out, next: i - 1 };
};
const readBlockScalar = (startIndex, parentIndent, ch) => {
const out = [];
let i = startIndex;
for (; i < lines.length; i++) {
const raw = lines[i];
const t = raw.trimEnd();
const tt = raw.trim();
const ind = indentOf(raw);
if (!tt) { out.push(''); continue; }
if (ind <= parentIndent) {
const isKey = /^[^\s-][^:]*:\s*(?:\||>.*|.*)?$/.test(tt);
const isListSibling = tt.startsWith('- ');
const isTopOp = (parentIndent === 0) && TOP_OP_RE.test(tt);
if (isKey || isListSibling || isTopOp) break;
out.push(t);
continue;
}
out.push(raw.slice(parentIndent + 2));
}
let text = out.join('\n');
if (text.startsWith('\n')) text = text.slice(1);
if (ch === '>') text = text.replace(/\n(?!\n)/g, ' ');
return { text, next: i - 1 };
};
for (let i = 0; i < lines.length; i++) {
const raw = lines[i];
const t = raw.trim();
if (!t || t.startsWith('#')) continue;
const ind = indentOf(raw);
const mTop = TOP_OP_RE.exec(t);
if (mTop && ind === 0) {
curOp = OP_MAP[mTop[1].toLowerCase()] || '';
stack.length = 0;
continue;
}
if (!curOp) continue;
while (stack.length && stack[stack.length - 1].indent >= ind) {
stack.pop();
}
const mKV = t.match(/^([^:]+):\s*(.*)$/);
if (mKV) {
const key = mKV[1].trim();
const rhs = String(stripYamlInlineComment(mKV[2])).trim();
const parentInfo = stack.length ? stack[stack.length - 1] : null;
const parentPath = parentInfo ? parentInfo.path : '';
const inheritedDirs = parentInfo?.directives || [];
const inheritedForChildren = parentInfo?.directivesForChildren || inheritedDirs;
const info = buildPathInfo(key, parentPath);
const combinedDirs = [...inheritedDirs, ...info.directives];
const nextInherited = info.directives.length ? info.directives : inheritedForChildren;
const effectiveGuardDirs = info.directives.length ? info.directives : inheritedDirs;
if (effectiveGuardDirs.length && info.guardTargetRaw) {
recordGuardDirective(info.guardTargetRaw, effectiveGuardDirs);
}
const curPathRaw = info.curPathRaw;
const curPath = norm(curPathRaw);
if (!curPath) continue;
// 块标量
if (rhs && (rhs[0] === '|' || rhs[0] === '>')) {
const { text, next } = readBlockScalar(i + 1, ind, rhs[0]);
i = next;
const [top, ...rest] = curPath.split('.');
const rel = rest.join('.');
if (curOp === 'set') putSet(top, rel, text);
else if (curOp === 'push') putPush(top, rel, text);
else if (curOp === 'bump') putBump(top, rel, Number(text));
continue;
}
// 空值(嵌套对象或列表)
if (rhs === '') {
stack.push({
indent: ind,
path: curPath,
directives: combinedDirs,
directivesForChildren: nextInherited
});
let j = i + 1;
while (j < lines.length && !lines[j].trim()) j++;
let handledList = false;
let hasDeeper = false;
if (j < lines.length) {
const t2 = lines[j].trim();
const ind2 = indentOf(lines[j]);
if (ind2 > ind && t2) {
hasDeeper = true;
if (/^-+\s+/.test(t2)) {
const { arr, next } = readList(j, ind);
i = next;
const [top, ...rest] = curPath.split('.');
const rel = rest.join('.');
if (curOp === 'set') putSet(top, rel, arr);
else if (curOp === 'push') putPush(top, rel, arr);
else if (curOp === 'del') {
for (const item of arr) putDel(top, rel ? `${rel}.${item}` : item);
}
else if (curOp === 'bump') {
for (const item of arr) putBump(top, rel, Number(item));
}
stack.pop();
handledList = true;
hasDeeper = false;
}
}
}
if (!handledList && !hasDeeper && curOp === 'del') {
const [top, ...rest] = curPath.split('.');
const rel = rest.join('.');
putDel(top, rel);
stack.pop();
}
continue;
}
// 普通值
const [top, ...rest] = curPath.split('.');
const rel = rest.join('.');
if (curOp === 'set') {
putSet(top, rel, stripQ(rhs));
} else if (curOp === 'push') {
putPush(top, rel, stripQ(rhs));
} else if (curOp === 'del') {
const val = stripQ(rhs);
const normRel = norm(rel);
const segs = normRel.split('.').filter(Boolean);
const lastSeg = segs.length > 0 ? segs[segs.length - 1] : '';
const pathEndsWithIndex = /^\d+$/.test(lastSeg);
if (pathEndsWithIndex) {
putDel(top, normRel);
} else {
const target = normRel ? `${normRel}.${val}` : val;
putDel(top, target);
}
} else if (curOp === 'bump') {
putBump(top, rel, Number(stripQ(rhs)));
}
continue;
}
// 顶层列表项del 操作)
const mArr = t.match(/^-+\s*(.+)$/);
if (mArr && stack.length === 0 && curOp === 'del') {
const rawItem = stripQ(stripYamlInlineComment(mArr[1]));
if (rawItem) {
const std = String(rawItem).replace(/\[(\d+)\]/g, '.$1');
const [top, ...rest] = std.split('.');
const rel = rest.join('.');
if (top) putDel(top, rel);
}
continue;
}
// 嵌套列表项
if (mArr && stack.length) {
const curPath = stack[stack.length - 1].path;
const [top, ...rest] = curPath.split('.');
const rel = rest.join('.');
const val = stripQ(stripYamlInlineComment(mArr[1]));
if (curOp === 'set') {
const bucket = (ops.set[top] ||= {});
const prev = bucket[rel];
if (Array.isArray(prev)) prev.push(val);
else if (prev !== undefined) bucket[rel] = [prev, val];
else bucket[rel] = [val];
} else if (curOp === 'push') {
putPush(top, rel, val);
} else if (curOp === 'del') {
putDel(top, rel ? `${rel}.${val}` : val);
} else if (curOp === 'bump') {
putBump(top, rel, Number(val));
}
}
}
return finalizeResults();
}
/* ============= 变量守护与规则集 ============= */
function rulesGetTable() {
return guardianState.table || {};
}
function rulesSetTable(t) {
guardianState.table = t || {};
}
function rulesClearCache() {
guardianState.table = {};
guardianState.regexCache = {};
}
function rulesLoadFromMeta() {
try {
const meta = getContext()?.chatMetadata || {};
const raw = meta[LWB_RULES_KEY];
if (raw && typeof raw === 'object') {
rulesSetTable(deepClone(raw));
// 重建正则缓存
for (const [p, node] of Object.entries(guardianState.table)) {
if (node?.constraints?.regex?.source) {
const src = node.constraints.regex.source;
const flg = node.constraints.regex.flags || '';
try {
guardianState.regexCache[p] = new RegExp(src, flg);
} catch {}
}
}
} else {
rulesSetTable({});
}
} catch {
rulesSetTable({});
}
}
function rulesSaveToMeta() {
try {
const meta = getContext()?.chatMetadata || {};
meta[LWB_RULES_KEY] = deepClone(guardianState.table || {});
guardianState.lastMetaSyncAt = Date.now();
getContext()?.saveMetadataDebounced?.();
} catch {}
}
export function guardBypass(on) {
guardianState.bypass = !!on;
}
function getRootValue(rootName) {
try {
const raw = getLocalVariable(rootName);
if (raw == null) return undefined;
if (typeof raw === 'string') {
const s = raw.trim();
if (s && (s[0] === '{' || s[0] === '[')) {
try { return JSON.parse(s); } catch { return raw; }
}
return raw;
}
return raw;
} catch {
return undefined;
}
}
function getValueAtPath(absPath) {
try {
const segs = lwbSplitPathWithBrackets(absPath);
if (!segs.length) return undefined;
const rootName = String(segs[0]);
let cur = getRootValue(rootName);
if (segs.length === 1) return cur;
if (typeof cur === 'string') {
const s = cur.trim();
if (s && (s[0] === '{' || s[0] === '[')) {
try { cur = JSON.parse(s); } catch { return undefined; }
} else {
return undefined;
}
}
for (let i = 1; i < segs.length; i++) {
cur = cur?.[segs[i]];
if (cur === undefined) return undefined;
}
return cur;
} catch {
return undefined;
}
}
function typeOfValue(v) {
if (Array.isArray(v)) return 'array';
const t = typeof v;
if (t === 'object' && v !== null) return 'object';
if (t === 'number') return 'number';
if (t === 'string') return 'string';
if (t === 'boolean') return 'boolean';
if (v === null) return 'null';
return 'scalar';
}
function ensureRuleNode(path) {
const tbl = rulesGetTable();
const p = normalizePath(path);
const node = tbl[p] || (tbl[p] = {
typeLock: 'unknown',
ro: false,
objectPolicy: 'none',
arrayPolicy: 'lock',
constraints: {},
elementConstraints: null
});
return node;
}
function getRuleNode(path) {
const tbl = rulesGetTable();
return tbl[normalizePath(path)];
}
function setTypeLockIfUnknown(path, v) {
const n = ensureRuleNode(path);
if (!n.typeLock || n.typeLock === 'unknown') {
n.typeLock = typeOfValue(v);
rulesSaveToMeta();
}
}
function clampNumberWithConstraints(v, node) {
let out = Number(v);
if (!Number.isFinite(out)) return { ok: false };
const c = node?.constraints || {};
if (Number.isFinite(c.min)) out = Math.max(out, c.min);
if (Number.isFinite(c.max)) out = Math.min(out, c.max);
return { ok: true, value: out };
}
function checkStringWithConstraints(v, node) {
const s = String(v);
const c = node?.constraints || {};
if (Array.isArray(c.enum) && c.enum.length) {
if (!c.enum.includes(s)) return { ok: false };
}
if (c.regex && c.regex.source) {
let re = guardianState.regexCache[normalizePath(node.__path || '')];
if (!re) {
try {
re = new RegExp(c.regex.source, c.regex.flags || '');
guardianState.regexCache[normalizePath(node.__path || '')] = re;
} catch {}
}
if (re && !re.test(s)) return { ok: false };
}
return { ok: true, value: s };
}
function getParentPath(absPath) {
const segs = lwbSplitPathWithBrackets(absPath);
if (segs.length <= 1) return '';
return segs.slice(0, -1).map(s => String(s)).join('.');
}
function getEffectiveParentNode(p) {
let parentPath = getParentPath(p);
while (parentPath) {
const pNode = getRuleNode(parentPath);
if (pNode && (pNode.objectPolicy !== 'none' || pNode.arrayPolicy !== 'lock')) {
return pNode;
}
parentPath = getParentPath(parentPath);
}
return null;
}
/**
* 守护验证
*/
export function guardValidate(op, absPath, payload) {
if (guardianState.bypass) return { allow: true, value: payload };
const p = normalizePath(absPath);
const node = getRuleNode(p) || {
typeLock: 'unknown',
ro: false,
objectPolicy: 'none',
arrayPolicy: 'lock',
constraints: {}
};
// 只读检查
if (node.ro) return { allow: false, reason: 'ro' };
const parentPath = getParentPath(p);
const parentNode = parentPath ? (getEffectiveParentNode(p) || { objectPolicy: 'none', arrayPolicy: 'lock' }) : null;
const currentValue = getValueAtPath(p);
// 删除操作
if (op === 'delNode') {
if (!parentPath) return { allow: false, reason: 'no-parent' };
const parentValue = getValueAtPath(parentPath);
const parentIsArray = Array.isArray(parentValue);
const pp = getRuleNode(parentPath) || { objectPolicy: 'none', arrayPolicy: 'lock' };
const lastSeg = p.split('.').pop() || '';
const isIndex = /^\d+$/.test(lastSeg);
if (parentIsArray || isIndex) {
if (!(pp.arrayPolicy === 'shrink' || pp.arrayPolicy === 'list')) {
return { allow: false, reason: 'array-no-shrink' };
}
return { allow: true };
} else {
if (!(pp.objectPolicy === 'prune' || pp.objectPolicy === 'free')) {
return { allow: false, reason: 'object-no-prune' };
}
return { allow: true };
}
}
// 推入操作
if (op === 'push') {
const arr = getValueAtPath(p);
if (arr === undefined) {
const lastSeg = p.split('.').pop() || '';
const isIndex = /^\d+$/.test(lastSeg);
if (parentPath) {
const parentVal = getValueAtPath(parentPath);
const pp = parentNode || { objectPolicy: 'none', arrayPolicy: 'lock' };
if (isIndex) {
if (!Array.isArray(parentVal)) return { allow: false, reason: 'parent-not-array' };
if (!(pp.arrayPolicy === 'grow' || pp.arrayPolicy === 'list')) {
return { allow: false, reason: 'array-no-grow' };
}
} else {
if (!(pp.objectPolicy === 'ext' || pp.objectPolicy === 'free')) {
return { allow: false, reason: 'object-no-ext' };
}
}
}
const nn = ensureRuleNode(p);
nn.typeLock = 'array';
rulesSaveToMeta();
return { allow: true, value: payload };
}
if (!Array.isArray(arr)) {
if (node.typeLock !== 'unknown' && node.typeLock !== 'array') {
return { allow: false, reason: 'type-locked-not-array' };
}
return { allow: false, reason: 'not-array' };
}
if (!(node.arrayPolicy === 'grow' || node.arrayPolicy === 'list')) {
return { allow: false, reason: 'array-no-grow' };
}
return { allow: true, value: payload };
}
// 增量操作
if (op === 'bump') {
let d = Number(payload);
if (!Number.isFinite(d)) return { allow: false, reason: 'delta-nan' };
if (currentValue === undefined) {
if (parentPath) {
const lastSeg = p.split('.').pop() || '';
const isIndex = /^\d+$/.test(lastSeg);
if (isIndex) {
if (!(parentNode && (parentNode.arrayPolicy === 'grow' || parentNode.arrayPolicy === 'list'))) {
return { allow: false, reason: 'array-no-grow' };
}
} else {
if (!(parentNode && (parentNode.objectPolicy === 'ext' || parentNode.objectPolicy === 'free'))) {
return { allow: false, reason: 'object-no-ext' };
}
}
}
}
const c = node?.constraints || {};
const step = Number.isFinite(c.step) ? Math.abs(c.step) : Infinity;
if (isFinite(step)) {
if (d > step) d = step;
if (d < -step) d = -step;
}
const cur = Number(currentValue);
if (!Number.isFinite(cur)) {
const base = 0 + d;
const cl = clampNumberWithConstraints(base, node);
if (!cl.ok) return { allow: false, reason: 'number-constraint' };
setTypeLockIfUnknown(p, base);
return { allow: true, value: cl.value };
}
const next = cur + d;
const clamped = clampNumberWithConstraints(next, node);
if (!clamped.ok) return { allow: false, reason: 'number-constraint' };
return { allow: true, value: clamped.value };
}
// 设置操作
if (op === 'set') {
const exists = currentValue !== undefined;
if (!exists) {
if (parentNode) {
const lastSeg = p.split('.').pop() || '';
const isIndex = /^\d+$/.test(lastSeg);
if (isIndex) {
if (!(parentNode.arrayPolicy === 'grow' || parentNode.arrayPolicy === 'list')) {
return { allow: false, reason: 'array-no-grow' };
}
} else {
if (!(parentNode.objectPolicy === 'ext' || parentNode.objectPolicy === 'free')) {
return { allow: false, reason: 'object-no-ext' };
}
}
}
}
const incomingType = typeOfValue(payload);
if (node.typeLock !== 'unknown' && node.typeLock !== incomingType) {
return { allow: false, reason: 'type-locked-mismatch' };
}
if (incomingType === 'number') {
let incoming = Number(payload);
if (!Number.isFinite(incoming)) return { allow: false, reason: 'number-constraint' };
const c = node?.constraints || {};
const step = Number.isFinite(c.step) ? Math.abs(c.step) : Infinity;
const curNum = Number(currentValue);
const base = Number.isFinite(curNum) ? curNum : 0;
if (isFinite(step)) {
let diff = incoming - base;
if (diff > step) diff = step;
if (diff < -step) diff = -step;
incoming = base + diff;
}
const clamped = clampNumberWithConstraints(incoming, node);
if (!clamped.ok) return { allow: false, reason: 'number-constraint' };
setTypeLockIfUnknown(p, incoming);
return { allow: true, value: clamped.value };
}
if (incomingType === 'string') {
const n2 = { ...node, __path: p };
const ok = checkStringWithConstraints(payload, n2);
if (!ok.ok) return { allow: false, reason: 'string-constraint' };
setTypeLockIfUnknown(p, payload);
return { allow: true, value: ok.value };
}
setTypeLockIfUnknown(p, payload);
return { allow: true, value: payload };
}
return { allow: true, value: payload };
}
/**
* 应用规则增量
*/
export function applyRuleDelta(path, delta) {
const p = normalizePath(path);
if (delta?.clear) {
try {
const tbl = rulesGetTable();
if (tbl && Object.prototype.hasOwnProperty.call(tbl, p)) {
delete tbl[p];
}
if (guardianState?.regexCache) {
delete guardianState.regexCache[p];
}
} catch {}
}
const hasOther = !!(delta && (
delta.ro ||
delta.objectPolicy ||
delta.arrayPolicy ||
(delta.constraints && Object.keys(delta.constraints).length)
));
if (hasOther) {
const node = ensureRuleNode(p);
if (delta.ro) node.ro = true;
if (delta.objectPolicy) node.objectPolicy = delta.objectPolicy;
if (delta.arrayPolicy) node.arrayPolicy = delta.arrayPolicy;
if (delta.constraints) {
const c = node.constraints || {};
if (delta.constraints.min != null) c.min = Number(delta.constraints.min);
if (delta.constraints.max != null) c.max = Number(delta.constraints.max);
if (delta.constraints.enum) c.enum = delta.constraints.enum.slice();
if (delta.constraints.regex) {
c.regex = {
source: delta.constraints.regex.source,
flags: delta.constraints.regex.flags || ''
};
try {
guardianState.regexCache[p] = new RegExp(c.regex.source, c.regex.flags || '');
} catch {}
}
if (delta.constraints.step != null) {
c.step = Math.max(0, Math.abs(Number(delta.constraints.step)));
}
node.constraints = c;
}
}
rulesSaveToMeta();
}
/**
* 从树加载规则
*/
export function rulesLoadFromTree(valueTree, basePath) {
const isObj = v => v && typeof v === 'object' && !Array.isArray(v);
function stripDollarKeysDeep(val) {
if (Array.isArray(val)) return val.map(stripDollarKeysDeep);
if (isObj(val)) {
const out = {};
for (const k in val) {
if (!Object.prototype.hasOwnProperty.call(val, k)) continue;
if (String(k).trim().startsWith('$')) continue;
out[k] = stripDollarKeysDeep(val[k]);
}
return out;
}
return val;
}
const rulesDelta = {};
function walk(node, curAbs) {
if (!isObj(node)) return;
for (const key in node) {
if (!Object.prototype.hasOwnProperty.call(node, key)) continue;
const v = node[key];
const keyStr = String(key).trim();
if (!keyStr.startsWith('$')) {
const childPath = curAbs ? `${curAbs}.${keyStr}` : keyStr;
if (isObj(v)) walk(v, childPath);
continue;
}
const rest = keyStr.slice(1).trim();
if (!rest) continue;
const parts = rest.split(/\s+/).filter(Boolean);
if (!parts.length) continue;
const targetToken = parts.pop();
const dirs = parts.map(t =>
String(t).trim().startsWith('$') ? String(t).trim() : ('$' + String(t).trim())
);
const baseNorm = normalizePath(curAbs || '');
const tokenNorm = normalizePath(targetToken);
const targetPath = (baseNorm && (tokenNorm === baseNorm || tokenNorm.startsWith(baseNorm + '.')))
? tokenNorm
: (curAbs ? `${curAbs}.${targetToken}` : targetToken);
const absPath = normalizePath(targetPath);
const delta = parseDirectivesTokenList(dirs);
if (!rulesDelta[absPath]) rulesDelta[absPath] = {};
Object.assign(rulesDelta[absPath], delta);
if (isObj(v)) walk(v, absPath);
}
}
walk(valueTree, basePath || '');
const cleanValue = stripDollarKeysDeep(valueTree);
return { cleanValue, rulesDelta };
}
/**
* 应用规则增量表
*/
export function applyRulesDeltaToTable(delta) {
if (!delta || typeof delta !== 'object') return;
for (const [p, d] of Object.entries(delta)) {
applyRuleDelta(p, d);
}
rulesSaveToMeta();
}
/**
* 安装变量 API 补丁
*/
function installVariableApiPatch() {
try {
const ctx = getContext();
const api = ctx?.variables?.local;
if (!api || guardianState.origVarApi) return;
guardianState.origVarApi = {
set: api.set?.bind(api),
add: api.add?.bind(api),
inc: api.inc?.bind(api),
dec: api.dec?.bind(api),
del: api.del?.bind(api)
};
if (guardianState.origVarApi.set) {
api.set = (name, value) => {
try {
if (guardianState.bypass) return guardianState.origVarApi.set(name, value);
let finalValue = value;
if (value && typeof value === 'object' && !Array.isArray(value)) {
const hasRuleKey = Object.keys(value).some(k => k.startsWith('$'));
if (hasRuleKey) {
const { cleanValue, rulesDelta } = rulesLoadFromTree(value, normalizePath(name));
finalValue = cleanValue;
applyRulesDeltaToTable(rulesDelta);
}
}
const res = guardValidate('set', normalizePath(name), finalValue);
if (!res.allow) return;
return guardianState.origVarApi.set(name, res.value);
} catch {
return;
}
};
}
if (guardianState.origVarApi.add) {
api.add = (name, delta) => {
try {
if (guardianState.bypass) return guardianState.origVarApi.add(name, delta);
const res = guardValidate('bump', normalizePath(name), delta);
if (!res.allow) return;
const cur = Number(getValueAtPath(normalizePath(name)));
if (!Number.isFinite(cur)) {
return guardianState.origVarApi.set(name, res.value);
}
const next = res.value;
const diff = Number(next) - cur;
return guardianState.origVarApi.add(name, diff);
} catch {
return;
}
};
}
if (guardianState.origVarApi.inc) {
api.inc = (name) => api.add?.(name, 1);
}
if (guardianState.origVarApi.dec) {
api.dec = (name) => api.add?.(name, -1);
}
if (guardianState.origVarApi.del) {
api.del = (name) => {
try {
if (guardianState.bypass) return guardianState.origVarApi.del(name);
const res = guardValidate('delNode', normalizePath(name));
if (!res.allow) return;
return guardianState.origVarApi.del(name);
} catch {
return;
}
};
}
} catch {}
}
/**
* 卸载变量 API 补丁
*/
function uninstallVariableApiPatch() {
try {
const ctx = getContext();
const api = ctx?.variables?.local;
if (!api || !guardianState.origVarApi) return;
if (guardianState.origVarApi.set) api.set = guardianState.origVarApi.set;
if (guardianState.origVarApi.add) api.add = guardianState.origVarApi.add;
if (guardianState.origVarApi.inc) api.inc = guardianState.origVarApi.inc;
if (guardianState.origVarApi.dec) api.dec = guardianState.origVarApi.dec;
if (guardianState.origVarApi.del) api.del = guardianState.origVarApi.del;
guardianState.origVarApi = null;
} catch {}
}
/* ============= 快照/回滚 ============= */
function getSnapMap() {
const meta = getContext()?.chatMetadata || {};
if (!meta[LWB_SNAP_KEY]) meta[LWB_SNAP_KEY] = {};
return meta[LWB_SNAP_KEY];
}
function getVarDict() {
const meta = getContext()?.chatMetadata || {};
return deepClone(meta.variables || {});
}
function setVarDict(dict) {
try {
guardBypass(true);
const ctx = getContext();
const meta = ctx?.chatMetadata || {};
const current = meta.variables || {};
const next = dict || {};
// 清除不存在的变量
for (const k of Object.keys(current)) {
if (!(k in next)) {
try { delete current[k]; } catch {}
try { setLocalVariable(k, ''); } catch {}
}
}
// 设置新值
for (const [k, v] of Object.entries(next)) {
let toStore = v;
if (v && typeof v === 'object') {
try { toStore = JSON.stringify(v); } catch { toStore = ''; }
}
try { setLocalVariable(k, toStore); } catch {}
}
meta.variables = deepClone(next);
getContext()?.saveMetadataDebounced?.();
} catch {} finally {
guardBypass(false);
}
}
function cloneRulesTableForSnapshot() {
try {
const table = rulesGetTable();
if (!table || typeof table !== 'object') return {};
return deepClone(table);
} catch {
return {};
}
}
function applyRulesSnapshot(tableLike) {
const safe = (tableLike && typeof tableLike === 'object') ? tableLike : {};
rulesSetTable(deepClone(safe));
if (guardianState?.regexCache) guardianState.regexCache = {};
try {
for (const [p, node] of Object.entries(guardianState.table || {})) {
const c = node?.constraints?.regex;
if (c && c.source) {
try {
guardianState.regexCache[p] = new RegExp(c.source, c.flags || '');
} catch {}
}
}
} catch {}
rulesSaveToMeta();
}
function normalizeSnapshotRecord(raw) {
if (!raw || typeof raw !== 'object') return { vars: {}, rules: {} };
if (Object.prototype.hasOwnProperty.call(raw, 'vars') || Object.prototype.hasOwnProperty.call(raw, 'rules')) {
return {
vars: (raw.vars && typeof raw.vars === 'object') ? raw.vars : {},
rules: (raw.rules && typeof raw.rules === 'object') ? raw.rules : {}
};
}
return { vars: raw, rules: {} };
}
function setSnapshot(messageId, snapDict) {
if (messageId == null || messageId < 0) return;
const snaps = getSnapMap();
snaps[messageId] = deepClone(snapDict || {});
getContext()?.saveMetadataDebounced?.();
}
function getSnapshot(messageId) {
if (messageId == null || messageId < 0) return undefined;
const snaps = getSnapMap();
const snap = snaps[messageId];
if (!snap) return undefined;
return deepClone(snap);
}
function clearSnapshotsFrom(startIdInclusive) {
if (startIdInclusive == null) return;
try {
guardBypass(true);
const snaps = getSnapMap();
for (const k of Object.keys(snaps)) {
const id = Number(k);
if (!Number.isNaN(id) && id >= startIdInclusive) {
delete snaps[k];
}
}
getContext()?.saveMetadataDebounced?.();
} finally {
guardBypass(false);
}
}
function snapshotCurrentLastFloor() {
try {
const ctx = getContext();
const chat = ctx?.chat || [];
const lastId = chat.length ? chat.length - 1 : -1;
if (lastId < 0) return;
const dict = getVarDict();
const rules = cloneRulesTableForSnapshot();
setSnapshot(lastId, { vars: dict, rules });
} catch {}
}
function snapshotPreviousFloor() {
snapshotCurrentLastFloor();
}
function snapshotForMessageId(currentId) {
try {
if (typeof currentId !== 'number' || currentId < 0) return;
const dict = getVarDict();
const rules = cloneRulesTableForSnapshot();
setSnapshot(currentId, { vars: dict, rules });
} catch {}
}
function rollbackToPreviousOf(messageId) {
const id = Number(messageId);
if (Number.isNaN(id)) return;
const prevId = id - 1;
if (prevId < 0) return;
const snap = getSnapshot(prevId);
if (snap) {
const normalized = normalizeSnapshotRecord(snap);
try {
guardBypass(true);
setVarDict(normalized.vars || {});
applyRulesSnapshot(normalized.rules || {});
} finally {
guardBypass(false);
}
}
}
function rebuildVariablesFromScratch() {
try {
setVarDict({});
const chat = getContext()?.chat || [];
for (let i = 0; i < chat.length; i++) {
applyVariablesForMessage(i);
}
} catch {}
}
/* ============= 应用变量到消息 ============= */
/**
* 将对象模式转换
*/
function asObject(rec) {
if (rec.mode !== 'object') {
rec.mode = 'object';
rec.base = {};
rec.next = {};
rec.changed = true;
delete rec.scalar;
}
return rec.next ?? (rec.next = {});
}
/**
* 增量操作辅助
*/
function bumpAtPath(rec, path, delta) {
const numDelta = Number(delta);
if (!Number.isFinite(numDelta)) return false;
if (!path) {
if (rec.mode === 'scalar') {
let base = Number(rec.scalar);
if (!Number.isFinite(base)) base = 0;
const next = base + numDelta;
const nextStr = String(next);
if (rec.scalar !== nextStr) {
rec.scalar = nextStr;
rec.changed = true;
return true;
}
}
return false;
}
const obj = asObject(rec);
const segs = splitPathSegments(path);
const { parent, lastKey } = ensureDeepContainer(obj, segs);
const prev = parent?.[lastKey];
if (Array.isArray(prev)) {
if (prev.length === 0) {
prev.push(numDelta);
rec.changed = true;
return true;
}
let base = Number(prev[0]);
if (!Number.isFinite(base)) base = 0;
const next = base + numDelta;
if (prev[0] !== next) {
prev[0] = next;
rec.changed = true;
return true;
}
return false;
}
if (prev && typeof prev === 'object') return false;
let base = Number(prev);
if (!Number.isFinite(base)) base = 0;
const next = base + numDelta;
if (prev !== next) {
parent[lastKey] = next;
rec.changed = true;
return true;
}
return false;
}
/**
* 解析标量数组
*/
function parseScalarArrayMaybe(str) {
try {
const v = JSON.parse(String(str ?? ''));
return Array.isArray(v) ? v : null;
} catch {
return null;
}
}
/**
* 应用变量到消息
*/
async function applyVariablesForMessage(messageId) {
try {
const ctx = getContext();
const msg = ctx?.chat?.[messageId];
if (!msg) return;
const debugOn = !!xbLog.isEnabled?.();
const preview = (text, max = 220) => {
try {
const s = String(text ?? '').replace(/\s+/g, ' ').trim();
return s.length > max ? s.slice(0, max) + '…' : s;
} catch {
return '';
}
};
const rawKey = getMsgKey(msg);
const rawTextForSig = rawKey ? String(msg[rawKey] ?? '') : '';
const curSig = computePlotSignatureFromText(rawTextForSig);
if (!curSig) {
clearAppliedFor(messageId);
return;
}
const appliedMap = getAppliedMap();
if (appliedMap[messageId] === curSig) return;
const raw = rawKey ? String(msg[rawKey] ?? '') : '';
const blocks = extractPlotLogBlocks(raw);
if (blocks.length === 0) {
clearAppliedFor(messageId);
return;
}
const ops = [];
const delVarNames = new Set();
let parseErrors = 0;
let parsedPartsTotal = 0;
let guardDenied = 0;
const guardDeniedSamples = [];
blocks.forEach((b, idx) => {
let parts = [];
try {
parts = parseBlock(b);
} catch (e) {
parseErrors++;
if (debugOn) {
try { xbLog.error(MODULE_ID, `plot-log 解析失败:楼层=${messageId} 块#${idx + 1} 预览=${preview(b)}`, e); } catch {}
}
return;
}
parsedPartsTotal += Array.isArray(parts) ? parts.length : 0;
for (const p of parts) {
if (p.operation === 'guard' && Array.isArray(p.data) && p.data.length > 0) {
ops.push({ operation: 'guard', data: p.data });
continue;
}
const name = p.name?.trim() || `varevent_${idx + 1}`;
if (p.operation === 'setObject' && p.data && Object.keys(p.data).length) {
ops.push({ name, operation: 'setObject', data: p.data });
} else if (p.operation === 'del' && Array.isArray(p.data) && p.data.length) {
ops.push({ name, operation: 'del', data: p.data });
} else if (p.operation === 'push' && p.data && Object.keys(p.data).length) {
ops.push({ name, operation: 'push', data: p.data });
} else if (p.operation === 'bump' && p.data && Object.keys(p.data).length) {
ops.push({ name, operation: 'bump', data: p.data });
} else if (p.operation === 'delVar') {
delVarNames.add(name);
}
}
});
if (ops.length === 0 && delVarNames.size === 0) {
if (debugOn) {
try {
xbLog.warn(
MODULE_ID,
`plot-log 未产生可执行指令:楼层=${messageId} 块数=${blocks.length} 解析条目=${parsedPartsTotal} 解析失败=${parseErrors} 预览=${preview(blocks[0])}`
);
} catch {}
}
setAppliedSignature(messageId, curSig);
return;
}
// 构建变量记录
const byName = new Map();
for (const { name } of ops) {
if (!name || typeof name !== 'string') continue;
const { root } = getRootAndPath(name);
if (!byName.has(root)) {
const curRaw = getLocalVariable(root);
const obj = maybeParseObject(curRaw);
if (obj) {
byName.set(root, { mode: 'object', base: obj, next: { ...obj }, changed: false });
} else {
byName.set(root, { mode: 'scalar', scalar: (curRaw ?? ''), changed: false });
}
}
}
const norm = (p) => String(p || '').replace(/\[(\d+)\]/g, '.$1');
// 执行操作
for (const op of ops) {
// 守护指令
if (op.operation === 'guard') {
for (const entry of op.data) {
const path = typeof entry?.path === 'string' ? entry.path.trim() : '';
const tokens = Array.isArray(entry?.directives)
? entry.directives.map(t => String(t || '').trim()).filter(Boolean)
: [];
if (!path || !tokens.length) continue;
try {
const delta = parseDirectivesTokenList(tokens);
if (delta) {
applyRuleDelta(normalizePath(path), delta);
}
} catch {}
}
rulesSaveToMeta();
continue;
}
const { root, subPath } = getRootAndPath(op.name);
const rec = byName.get(root);
if (!rec) continue;
// SET 操作
if (op.operation === 'setObject') {
for (const [k, v] of Object.entries(op.data)) {
const localPath = joinPath(subPath, k);
const absPath = localPath ? `${root}.${localPath}` : root;
const stdPath = normalizePath(absPath);
let allow = true;
let newVal = parseValueForSet(v);
const res = guardValidate('set', stdPath, newVal);
allow = !!res?.allow;
if ('value' in res) newVal = res.value;
if (!allow) {
guardDenied++;
if (debugOn && guardDeniedSamples.length < 8) guardDeniedSamples.push({ op: 'set', path: stdPath });
continue;
}
if (!localPath) {
if (newVal !== null && typeof newVal === 'object') {
rec.mode = 'object';
rec.next = deepClone(newVal);
rec.changed = true;
} else {
rec.mode = 'scalar';
rec.scalar = String(newVal ?? '');
rec.changed = true;
}
continue;
}
const obj = asObject(rec);
if (setDeepValue(obj, norm(localPath), newVal)) rec.changed = true;
}
}
// DEL 操作
else if (op.operation === 'del') {
const obj = asObject(rec);
const pending = [];
for (const key of op.data) {
const localPath = joinPath(subPath, key);
if (!localPath) {
const res = guardValidate('delNode', normalizePath(root));
if (!res?.allow) {
guardDenied++;
if (debugOn && guardDeniedSamples.length < 8) guardDeniedSamples.push({ op: 'delNode', path: normalizePath(root) });
continue;
}
if (rec.mode === 'scalar') {
if (rec.scalar !== '') { rec.scalar = ''; rec.changed = true; }
} else {
if (rec.next && (Array.isArray(rec.next) ? rec.next.length > 0 : Object.keys(rec.next || {}).length > 0)) {
rec.next = Array.isArray(rec.next) ? [] : {};
rec.changed = true;
}
}
continue;
}
const absPath = `${root}.${localPath}`;
const res = guardValidate('delNode', normalizePath(absPath));
if (!res?.allow) {
guardDenied++;
if (debugOn && guardDeniedSamples.length < 8) guardDeniedSamples.push({ op: 'delNode', path: normalizePath(absPath) });
continue;
}
const normLocal = norm(localPath);
const segs = splitPathSegments(normLocal);
const last = segs[segs.length - 1];
const parentKey = segs.slice(0, -1).join('.');
pending.push({
normLocal,
isIndex: typeof last === 'number',
parentKey,
index: typeof last === 'number' ? last : null,
});
}
// 按索引分组(倒序删除)
const arrGroups = new Map();
const objDeletes = [];
for (const it of pending) {
if (it.isIndex) {
const g = arrGroups.get(it.parentKey) || [];
g.push(it);
arrGroups.set(it.parentKey, g);
} else {
objDeletes.push(it);
}
}
for (const [, list] of arrGroups.entries()) {
list.sort((a, b) => b.index - a.index);
for (const it of list) {
if (deleteDeepKey(obj, it.normLocal)) rec.changed = true;
}
}
for (const it of objDeletes) {
if (deleteDeepKey(obj, it.normLocal)) rec.changed = true;
}
}
// PUSH 操作
else if (op.operation === 'push') {
for (const [k, vals] of Object.entries(op.data)) {
const localPath = joinPath(subPath, k);
const absPathBase = localPath ? `${root}.${localPath}` : root;
let incoming = Array.isArray(vals) ? vals : [vals];
const filtered = [];
for (const v of incoming) {
const res = guardValidate('push', normalizePath(absPathBase), v);
if (!res?.allow) {
guardDenied++;
if (debugOn && guardDeniedSamples.length < 8) guardDeniedSamples.push({ op: 'push', path: normalizePath(absPathBase) });
continue;
}
filtered.push('value' in res ? res.value : v);
}
if (filtered.length === 0) continue;
if (!localPath) {
let arrRef = null;
if (rec.mode === 'object') {
if (Array.isArray(rec.next)) {
arrRef = rec.next;
} else if (rec.next && typeof rec.next === 'object' && Object.keys(rec.next).length === 0) {
rec.next = [];
arrRef = rec.next;
} else if (Array.isArray(rec.base)) {
rec.next = [...rec.base];
arrRef = rec.next;
} else {
rec.next = [];
arrRef = rec.next;
}
} else {
const parsed = parseScalarArrayMaybe(rec.scalar);
rec.mode = 'object';
rec.next = parsed ?? [];
arrRef = rec.next;
}
let changed = false;
for (const v of filtered) {
if (!arrRef.includes(v)) { arrRef.push(v); changed = true; }
}
if (changed) rec.changed = true;
continue;
}
const obj = asObject(rec);
if (pushDeepValue(obj, norm(localPath), filtered)) rec.changed = true;
}
}
// BUMP 操作
else if (op.operation === 'bump') {
for (const [k, delta] of Object.entries(op.data)) {
const num = Number(delta);
if (!Number.isFinite(num)) continue;
const localPath = joinPath(subPath, k);
const absPath = localPath ? `${root}.${localPath}` : root;
const stdPath = normalizePath(absPath);
let allow = true;
let useDelta = num;
const res = guardValidate('bump', stdPath, num);
allow = !!res?.allow;
if (allow && 'value' in res && Number.isFinite(res.value)) {
let curr;
try {
const pth = norm(localPath || '');
if (!pth) {
if (rec.mode === 'scalar') curr = Number(rec.scalar);
} else {
const segs = splitPathSegments(pth);
const obj = asObject(rec);
const { parent, lastKey } = ensureDeepContainer(obj, segs);
curr = parent?.[lastKey];
}
} catch {}
const baseNum = Number(curr);
const targetNum = Number(res.value);
useDelta = (Number.isFinite(targetNum) ? targetNum : num) - (Number.isFinite(baseNum) ? baseNum : 0);
}
if (!allow) {
guardDenied++;
if (debugOn && guardDeniedSamples.length < 8) guardDeniedSamples.push({ op: 'bump', path: stdPath });
continue;
}
bumpAtPath(rec, norm(localPath || ''), useDelta);
}
}
}
// 检查是否有变化
const hasChanges = Array.from(byName.values()).some(rec => rec?.changed === true);
if (!hasChanges && delVarNames.size === 0) {
if (debugOn) {
try {
const denied = guardDenied ? `,被规则拦截=${guardDenied}` : '';
xbLog.warn(
MODULE_ID,
`plot-log 指令执行后无变化:楼层=${messageId} 指令数=${ops.length}${denied} 示例=${preview(JSON.stringify(guardDeniedSamples))}`
);
} catch {}
}
setAppliedSignature(messageId, curSig);
return;
}
// 保存变量
for (const [name, rec] of byName.entries()) {
if (!rec.changed) continue;
try {
if (rec.mode === 'scalar') {
setLocalVariable(name, rec.scalar ?? '');
} else {
setLocalVariable(name, safeJSONStringify(rec.next ?? {}));
}
} catch {}
}
// 删除变量
if (delVarNames.size > 0) {
try {
for (const v of delVarNames) {
try { setLocalVariable(v, ''); } catch {}
}
const meta = ctx?.chatMetadata;
if (meta?.variables) {
for (const v of delVarNames) delete meta.variables[v];
ctx?.saveMetadataDebounced?.();
ctx?.saveSettingsDebounced?.();
}
} catch {}
}
setAppliedSignature(messageId, curSig);
} catch {}
}
/* ============= 事件处理 ============= */
function getMsgIdLoose(payload) {
if (payload && typeof payload === 'object') {
if (typeof payload.messageId === 'number') return payload.messageId;
if (typeof payload.id === 'number') return payload.id;
}
if (typeof payload === 'number') return payload;
const chat = getContext()?.chat || [];
return chat.length ? chat.length - 1 : undefined;
}
function getMsgIdStrict(payload) {
if (payload && typeof payload === 'object') {
if (typeof payload.id === 'number') return payload.id;
if (typeof payload.messageId === 'number') return payload.messageId;
}
if (typeof payload === 'number') return payload;
return undefined;
}
function bindEvents() {
pendingSwipeApply = new Map();
let lastSwipedId;
suppressUpdatedOnce = new Set();
// 消息发送
events?.on(event_types.MESSAGE_SENT, async () => {
try {
snapshotCurrentLastFloor();
const chat = getContext()?.chat || [];
const id = chat.length ? chat.length - 1 : undefined;
if (typeof id === 'number') {
await applyVariablesForMessage(id);
applyXbGetVarForMessage(id, true);
}
} catch {}
});
// 消息接收
events?.on(event_types.MESSAGE_RECEIVED, async (data) => {
try {
const id = getMsgIdLoose(data);
if (typeof id === 'number') {
await applyVariablesForMessage(id);
applyXbGetVarForMessage(id, true);
await executeQueuedVareventJsAfterTurn();
}
} catch {}
});
// 用户消息渲染
events?.on(event_types.USER_MESSAGE_RENDERED, async (data) => {
try {
const id = getMsgIdLoose(data);
if (typeof id === 'number') {
await applyVariablesForMessage(id);
applyXbGetVarForMessage(id, true);
snapshotForMessageId(id);
}
} catch {}
});
// 角色消息渲染
events?.on(event_types.CHARACTER_MESSAGE_RENDERED, async (data) => {
try {
const id = getMsgIdLoose(data);
if (typeof id === 'number') {
await applyVariablesForMessage(id);
applyXbGetVarForMessage(id, true);
snapshotForMessageId(id);
}
} catch {}
});
// 消息更新
events?.on(event_types.MESSAGE_UPDATED, async (data) => {
try {
const id = getMsgIdLoose(data);
if (typeof id === 'number') {
if (suppressUpdatedOnce.has(id)) {
suppressUpdatedOnce.delete(id);
return;
}
await applyVariablesForMessage(id);
applyXbGetVarForMessage(id, true);
}
} catch {}
});
// 消息编辑
events?.on(event_types.MESSAGE_EDITED, async (data) => {
try {
const id = getMsgIdLoose(data);
if (typeof id === 'number') {
clearAppliedFor(id);
rollbackToPreviousOf(id);
setTimeout(async () => {
await applyVariablesForMessage(id);
applyXbGetVarForMessage(id, true);
try {
const ctx = getContext();
const msg = ctx?.chat?.[id];
if (msg) updateMessageBlock(id, msg, { rerenderMessage: true });
} catch {}
try {
const ctx = getContext();
const es = ctx?.eventSource;
const et = ctx?.event_types;
if (es?.emit && et?.MESSAGE_UPDATED) {
suppressUpdatedOnce.add(id);
await es.emit(et.MESSAGE_UPDATED, id);
}
} catch {}
await executeQueuedVareventJsAfterTurn();
}, 10);
}
} catch {}
});
// 消息滑动
events?.on(event_types.MESSAGE_SWIPED, async (data) => {
try {
const id = getMsgIdLoose(data);
if (typeof id === 'number') {
lastSwipedId = id;
clearAppliedFor(id);
rollbackToPreviousOf(id);
const tId = setTimeout(async () => {
pendingSwipeApply.delete(id);
await applyVariablesForMessage(id);
await executeQueuedVareventJsAfterTurn();
}, 10);
pendingSwipeApply.set(id, tId);
}
} catch {}
});
// 消息删除
events?.on(event_types.MESSAGE_DELETED, (data) => {
try {
const id = getMsgIdStrict(data);
if (typeof id === 'number') {
rollbackToPreviousOf(id);
clearSnapshotsFrom(id);
clearAppliedFrom(id);
}
} catch {}
});
// 生成开始
events?.on(event_types.GENERATION_STARTED, (data) => {
try {
snapshotPreviousFloor();
// 取消滑动延迟
const t = (typeof data === 'string' ? data : (data?.type || '')).toLowerCase();
if (t === 'swipe' && lastSwipedId != null) {
const tId = pendingSwipeApply.get(lastSwipedId);
if (tId) {
clearTimeout(tId);
pendingSwipeApply.delete(lastSwipedId);
}
}
} catch {}
});
// 聊天切换
events?.on(event_types.CHAT_CHANGED, () => {
try {
rulesClearCache();
rulesLoadFromMeta();
const meta = getContext()?.chatMetadata || {};
meta[LWB_PLOT_APPLIED_KEY] = {};
getContext()?.saveMetadataDebounced?.();
} catch {}
});
}
/* ============= 初始化与清理 ============= */
/**
* 初始化模块
*/
export function initVariablesCore() {
try { xbLog.info('variablesCore', '变量系统启动'); } catch {}
if (initialized) return;
initialized = true;
// 创建事件管理器
events = createModuleEvents(MODULE_ID);
// 加载规则
rulesLoadFromMeta();
// 安装 API 补丁
installVariableApiPatch();
// 绑定事件
bindEvents();
// 挂载全局函数(供 var-commands.js 使用)
globalThis.LWB_Guard = {
validate: guardValidate,
loadRules: rulesLoadFromTree,
applyDelta: applyRuleDelta,
applyDeltaTable: applyRulesDeltaToTable,
save: rulesSaveToMeta,
};
}
/**
* 清理模块
*/
export function cleanupVariablesCore() {
try { xbLog.info('variablesCore', '变量系统清理'); } catch {}
if (!initialized) return;
// 清理事件
events?.cleanup();
events = null;
// 卸载 API 补丁
uninstallVariableApiPatch();
// 清理规则
rulesClearCache();
// 清理全局函数
delete globalThis.LWB_Guard;
// 清理守护状态
guardBypass(false);
initialized = false;
}
/* ============= 导出 ============= */
export {
MODULE_ID,
// 解析
parseBlock,
applyVariablesForMessage,
extractPlotLogBlocks,
// 快照
snapshotCurrentLastFloor,
snapshotForMessageId,
rollbackToPreviousOf,
rebuildVariablesFromScratch,
// 规则
rulesGetTable,
rulesSetTable,
rulesLoadFromMeta,
rulesSaveToMeta,
};