Files

2390 lines
82 KiB
JavaScript
Raw Permalink Normal View History

2026-01-17 16:34:39 +08:00
/**
* @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,
};