2390 lines
82 KiB
JavaScript
2390 lines
82 KiB
JavaScript
/**
|
||
* @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,
|
||
};
|