diff --git a/modules/variables/var-commands.js b/modules/variables/var-commands.js index f93b177..3694c9f 100644 --- a/modules/variables/var-commands.js +++ b/modules/variables/var-commands.js @@ -1,1009 +1,1015 @@ -/** - * @file modules/variables/var-commands.js - * @description 变量斜杠命令与宏替换,常驻模块 - */ - -import { getContext } from "../../../../../extensions.js"; -import { getLocalVariable, setLocalVariable } from "../../../../../variables.js"; -import { createModuleEvents, event_types } from "../../core/event-manager.js"; -import { - lwbSplitPathWithBrackets, - lwbSplitPathAndValue, - normalizePath, - ensureDeepContainer, - safeJSONStringify, - maybeParseObject, - valueToString, - deepClone, -} from "../../core/variable-path.js"; - -const MODULE_ID = 'varCommands'; -const TAG_RE_XBGETVAR = /\{\{xbgetvar::([^}]+)\}\}/gi; - -let events = null; -let initialized = false; - -function getMsgKey(msg) { - return (typeof msg?.mes === 'string') ? 'mes' - : (typeof msg?.content === 'string' ? 'content' : null); -} - -export function parseValueForSet(value) { - try { - const t = String(value ?? '').trim(); - - if (t.startsWith('{') || t.startsWith('[')) { - try { return JSON.parse(t); } catch {} - } - - const looksLikeJson = (t[0] === '{' || t[0] === '[') && /[:\],}]/.test(t); - if (looksLikeJson && !t.includes('"') && t.includes("'")) { - try { return JSON.parse(t.replace(/'/g, '"')); } catch {} - } - - if (t === 'true' || t === 'false' || t === 'null') { - return JSON.parse(t); - } - - if (/^-?\d+(\.\d+)?$/.test(t)) { - return JSON.parse(t); - } - - return value; - } catch { - return value; - } -} - -function extractPathFromArgs(namedArgs, unnamedArgs) { - try { - if (namedArgs && typeof namedArgs.key === 'string' && namedArgs.key.trim()) { - return String(namedArgs.key).trim(); - } - const arr = Array.isArray(unnamedArgs) ? unnamedArgs : [unnamedArgs]; - const first = String(arr[0] ?? '').trim(); - const m = /^key\s*=\s*(.+)$/i.exec(first); - return m ? m[1].trim() : first; - } catch { - return ''; - } -} - -function hasTopLevelRuleKey(obj) { - if (!obj || typeof obj !== 'object' || Array.isArray(obj)) return false; - return Object.keys(obj).some(k => String(k).trim().startsWith('$')); -} - -function ensureAbsTargetPath(basePath, token) { - const t = String(token || '').trim(); - if (!t) return String(basePath || ''); - const base = String(basePath || ''); - if (t === base || t.startsWith(base + '.')) return t; - return base ? (base + '.' + t) : t; -} - -function segmentsRelativeToBase(absPath, basePath) { - const segs = lwbSplitPathWithBrackets(absPath); - const baseSegs = lwbSplitPathWithBrackets(basePath); - if (!segs.length || !baseSegs.length) return segs || []; - const matches = baseSegs.every((b, i) => String(segs[i]) === String(b)); - return matches ? segs.slice(baseSegs.length) : segs; -} - -function setDeepBySegments(target, segs, value) { - let cur = target; - for (let i = 0; i < segs.length; i++) { - const isLast = i === segs.length - 1; - const key = segs[i]; - if (isLast) { - cur[key] = value; - } else { - const nxt = cur[key]; - if (typeof nxt === 'object' && nxt && !Array.isArray(nxt)) { - cur = nxt; - } else { - cur[key] = {}; - cur = cur[key]; - } - } - } -} - -export function lwbResolveVarPath(path) { - try { - const segs = lwbSplitPathWithBrackets(path); - if (!segs.length) return ''; - - const rootName = String(segs[0]); - const rootRaw = getLocalVariable(rootName); - - if (segs.length === 1) { - return valueToString(rootRaw); - } - - const obj = maybeParseObject(rootRaw); - if (!obj) return ''; - - let cur = obj; - for (let i = 1; i < segs.length; i++) { - cur = cur?.[segs[i]]; - if (cur === undefined) return ''; - } - - return valueToString(cur); - } catch { - return ''; - } -} - -export function replaceXbGetVarInString(s) { - s = String(s ?? ''); - if (!s || s.indexOf('{{xbgetvar::') === -1) return s; - - TAG_RE_XBGETVAR.lastIndex = 0; - return s.replace(TAG_RE_XBGETVAR, (_, p) => lwbResolveVarPath(p)); -} - -export function replaceXbGetVarInChat(chat) { - if (!Array.isArray(chat)) return; - - for (const msg of chat) { - try { - const key = getMsgKey(msg); - if (!key) continue; - - const old = String(msg[key] ?? ''); - if (old.indexOf('{{xbgetvar::') === -1) continue; - - msg[key] = replaceXbGetVarInString(old); - } catch {} - } -} - -export function applyXbGetVarForMessage(messageId, writeback = true) { - try { - const ctx = getContext(); - const msg = ctx?.chat?.[messageId]; - if (!msg) return; - - const key = getMsgKey(msg); - if (!key) return; - - const old = String(msg[key] ?? ''); - if (old.indexOf('{{xbgetvar::') === -1) return; - - const out = replaceXbGetVarInString(old); - if (writeback && out !== old) { - msg[key] = out; - } - } catch {} -} - -export function parseDirectivesTokenList(tokens) { - const out = { - ro: false, - objectPolicy: null, - arrayPolicy: null, - constraints: {}, - clear: false - }; - - for (const tok of tokens) { - const t = String(tok || '').trim(); - if (!t) continue; - - if (t === '$ro') { out.ro = true; continue; } - if (t === '$ext') { out.objectPolicy = 'ext'; continue; } - if (t === '$prune') { out.objectPolicy = 'prune'; continue; } - if (t === '$free') { out.objectPolicy = 'free'; continue; } - if (t === '$grow') { out.arrayPolicy = 'grow'; continue; } - if (t === '$shrink') { out.arrayPolicy = 'shrink'; continue; } - if (t === '$list') { out.arrayPolicy = 'list'; continue; } - - if (t.startsWith('$min=')) { - const num = Number(t.slice(5)); - if (Number.isFinite(num)) out.constraints.min = num; - continue; - } - if (t.startsWith('$max=')) { - const num = Number(t.slice(5)); - if (Number.isFinite(num)) out.constraints.max = num; - continue; - } - if (t.startsWith('$range=')) { - const m = t.match(/^\$range=\[\s*(-?\d+(?:\.\d+)?)\s*,\s*(-?\d+(?:\.\d+)?)\s*\]$/); - if (m) { - const a = Number(m[1]), b = Number(m[2]); - if (Number.isFinite(a) && Number.isFinite(b)) { - out.constraints.min = Math.min(a, b); - out.constraints.max = Math.max(a, b); - } - } - continue; - } - if (t.startsWith('$step=')) { - const num = Number(t.slice(6)); - if (Number.isFinite(num)) { - out.constraints.step = Math.max(0, Math.abs(num)); - } - continue; - } - - if (t.startsWith('$enum=')) { - const m = t.match(/^\$enum=\{\s*([^}]+)\s*\}$/); - if (m) { - const vals = m[1].split(/[;;]/).map(s => s.trim()).filter(Boolean); - if (vals.length) out.constraints.enum = vals; - } - continue; - } - - if (t.startsWith('$match=')) { - const raw = t.slice(7); - if (raw.startsWith('/') && raw.lastIndexOf('/') > 0) { - const last = raw.lastIndexOf('/'); - const pattern = raw.slice(1, last).replace(/\\\//g, '/'); - const flags = raw.slice(last + 1) || ''; - out.constraints.regex = { source: pattern, flags }; - } - continue; - } - - if (t === '$clear') { out.clear = true; continue; } - } - - return out; -} - -export function expandShorthandRuleObject(basePath, valueObj) { - try { - const base = String(basePath || ''); - const isObj = v => v && typeof v === 'object' && !Array.isArray(v); - - if (!isObj(valueObj)) return null; - - 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; - } - - function formatPathWithBrackets(pathStr) { - const segs = lwbSplitPathWithBrackets(String(pathStr || '')); - let out = ''; - for (const s of segs) { - if (typeof s === 'number') out += `[${s}]`; - else out += out ? `.${s}` : `${s}`; - } - return out; - } - - function assignDeep(dst, src) { - for (const k in src) { - if (!Object.prototype.hasOwnProperty.call(src, k)) continue; - const v = src[k]; - if (v && typeof v === 'object' && !Array.isArray(v)) { - if (!dst[k] || typeof dst[k] !== 'object' || Array.isArray(dst[k])) { - dst[k] = {}; - } - assignDeep(dst[k], v); - } else { - dst[k] = v; - } - } - } - - const rulesTop = {}; - const dataTree = {}; - - function writeDataAt(relPathStr, val) { - const abs = ensureAbsTargetPath(base, relPathStr); - const relSegs = segmentsRelativeToBase(abs, base); - if (relSegs.length) { - setDeepBySegments(dataTree, relSegs, val); - } else { - if (val && typeof val === 'object' && !Array.isArray(val)) { - assignDeep(dataTree, val); - } else { - dataTree['$root'] = val; - } - } - } - - function walk(node, currentRelPathStr) { - if (Array.isArray(node)) { - const cleanedArr = node.map(stripDollarKeysDeep); - if (currentRelPathStr) writeDataAt(currentRelPathStr, cleanedArr); - for (let i = 0; i < node.length; i++) { - const el = node[i]; - if (el && typeof el === 'object') { - const childRel = currentRelPathStr ? `${currentRelPathStr}.${i}` : String(i); - walk(el, childRel); - } - } - return; - } - - if (!isObj(node)) { - if (currentRelPathStr) writeDataAt(currentRelPathStr, node); - return; - } - - const cleaned = stripDollarKeysDeep(node); - if (currentRelPathStr) writeDataAt(currentRelPathStr, cleaned); - else assignDeep(dataTree, cleaned); - - for (const key in node) { - if (!Object.prototype.hasOwnProperty.call(node, key)) continue; - const v = node[key]; - const keyStr = String(key).trim(); - const isRule = keyStr.startsWith('$'); - - if (!isRule) { - const childRel = currentRelPathStr ? `${currentRelPathStr}.${keyStr}` : keyStr; - if (v && typeof v === 'object') walk(v, childRel); - 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 fullRelTarget = currentRelPathStr - ? `${currentRelPathStr}.${targetToken}` - : targetToken; - - const absTarget = ensureAbsTargetPath(base, fullRelTarget); - const absDisplay = formatPathWithBrackets(absTarget); - const ruleKey = `$ ${dirs.join(' ')} ${absDisplay}`.trim(); - rulesTop[ruleKey] = {}; - - if (v !== undefined) { - const cleanedVal = stripDollarKeysDeep(v); - writeDataAt(fullRelTarget, cleanedVal); - if (v && typeof v === 'object') { - walk(v, fullRelTarget); - } - } - } - } - - walk(valueObj, ''); - - const out = {}; - assignDeep(out, rulesTop); - assignDeep(out, dataTree); - return out; - } catch { - return null; - } -} - -export function lwbAssignVarPath(path, value) { - try { - const segs = lwbSplitPathWithBrackets(path); - if (!segs.length) return ''; - - const rootName = String(segs[0]); - let vParsed = parseValueForSet(value); - - if (vParsed && typeof vParsed === 'object') { - try { +/** + * @file modules/variables/var-commands.js + * @description 变量斜杠命令与宏替换,常驻模块 + */ + +import { getContext } from "../../../../../extensions.js"; +import { getLocalVariable, setLocalVariable } from "../../../../../variables.js"; +import { createModuleEvents, event_types } from "../../core/event-manager.js"; +import { + lwbSplitPathWithBrackets, + lwbSplitPathAndValue, + normalizePath, + ensureDeepContainer, + safeJSONStringify, + maybeParseObject, + valueToString, + deepClone, +} from "../../core/variable-path.js"; + +const MODULE_ID = 'varCommands'; +const TAG_RE_XBGETVAR = /\{\{xbgetvar::([^}]+)\}\}/gi; + +let events = null; +let initialized = false; + +function getMsgKey(msg) { + return (typeof msg?.mes === 'string') ? 'mes' + : (typeof msg?.content === 'string' ? 'content' : null); +} + +export function parseValueForSet(value) { + try { + const t = String(value ?? '').trim(); + + if (t.startsWith('{') || t.startsWith('[')) { + try { return JSON.parse(t); } catch {} + } + + const looksLikeJson = (t[0] === '{' || t[0] === '[') && /[:\],}]/.test(t); + if (looksLikeJson && !t.includes('"') && t.includes("'")) { + try { return JSON.parse(t.replace(/'/g, '"')); } catch {} + } + + if (t === 'true' || t === 'false' || t === 'null') { + return JSON.parse(t); + } + + if (/^-?\d+(\.\d+)?$/.test(t)) { + return JSON.parse(t); + } + + return value; + } catch { + return value; + } +} + +function extractPathFromArgs(namedArgs, unnamedArgs) { + try { + if (namedArgs && typeof namedArgs.key === 'string' && namedArgs.key.trim()) { + return String(namedArgs.key).trim(); + } + const arr = Array.isArray(unnamedArgs) ? unnamedArgs : [unnamedArgs]; + const first = String(arr[0] ?? '').trim(); + const m = /^key\s*=\s*(.+)$/i.exec(first); + return m ? m[1].trim() : first; + } catch { + return ''; + } +} + +function hasTopLevelRuleKey(obj) { + if (!obj || typeof obj !== 'object' || Array.isArray(obj)) return false; + return Object.keys(obj).some(k => String(k).trim().startsWith('$')); +} + +function ensureAbsTargetPath(basePath, token) { + const t = String(token || '').trim(); + if (!t) return String(basePath || ''); + const base = String(basePath || ''); + if (t === base || t.startsWith(base + '.')) return t; + return base ? (base + '.' + t) : t; +} + +function segmentsRelativeToBase(absPath, basePath) { + const segs = lwbSplitPathWithBrackets(absPath); + const baseSegs = lwbSplitPathWithBrackets(basePath); + if (!segs.length || !baseSegs.length) return segs || []; + const matches = baseSegs.every((b, i) => String(segs[i]) === String(b)); + return matches ? segs.slice(baseSegs.length) : segs; +} + +function setDeepBySegments(target, segs, value) { + let cur = target; + for (let i = 0; i < segs.length; i++) { + const isLast = i === segs.length - 1; + const key = segs[i]; + if (isLast) { + cur[key] = value; + } else { + const nxt = cur[key]; + if (typeof nxt === 'object' && nxt && !Array.isArray(nxt)) { + cur = nxt; + } else { + cur[key] = {}; + cur = cur[key]; + } + } + } +} + +export function lwbResolveVarPath(path) { + try { + const segs = lwbSplitPathWithBrackets(path); + if (!segs.length) return ''; + + const rootName = String(segs[0]); + const rootRaw = getLocalVariable(rootName); + + if (segs.length === 1) { + return valueToString(rootRaw); + } + + const obj = maybeParseObject(rootRaw); + if (!obj) return ''; + + let cur = obj; + for (let i = 1; i < segs.length; i++) { + cur = cur?.[segs[i]]; + if (cur === undefined) return ''; + } + + return valueToString(cur); + } catch { + return ''; + } +} + +export function replaceXbGetVarInString(s) { + s = String(s ?? ''); + if (!s || s.indexOf('{{xbgetvar::') === -1) return s; + + TAG_RE_XBGETVAR.lastIndex = 0; + return s.replace(TAG_RE_XBGETVAR, (_, p) => lwbResolveVarPath(p)); +} + +export function replaceXbGetVarInChat(chat) { + if (!Array.isArray(chat)) return; + + for (const msg of chat) { + try { + const key = getMsgKey(msg); + if (!key) continue; + + const old = String(msg[key] ?? ''); + if (old.indexOf('{{xbgetvar::') === -1) continue; + + msg[key] = replaceXbGetVarInString(old); + } catch {} + } +} + +export function applyXbGetVarForMessage(messageId, writeback = true) { + try { + const ctx = getContext(); + const msg = ctx?.chat?.[messageId]; + if (!msg) return; + + const key = getMsgKey(msg); + if (!key) return; + + const old = String(msg[key] ?? ''); + if (old.indexOf('{{xbgetvar::') === -1) return; + + const out = replaceXbGetVarInString(old); + if (writeback && out !== old) { + msg[key] = out; + } + } catch {} +} + +export function parseDirectivesTokenList(tokens) { + const out = { + ro: false, + objectPolicy: null, + arrayPolicy: null, + constraints: {}, + clear: false + }; + + for (const tok of tokens) { + const t = String(tok || '').trim(); + if (!t) continue; + + if (t === '$ro') { out.ro = true; continue; } + if (t === '$ext') { out.objectPolicy = 'ext'; continue; } + if (t === '$prune') { out.objectPolicy = 'prune'; continue; } + if (t === '$free') { out.objectPolicy = 'free'; continue; } + if (t === '$grow') { out.arrayPolicy = 'grow'; continue; } + if (t === '$shrink') { out.arrayPolicy = 'shrink'; continue; } + if (t === '$list') { out.arrayPolicy = 'list'; continue; } + + if (t.startsWith('$min=')) { + const num = Number(t.slice(5)); + if (Number.isFinite(num)) out.constraints.min = num; + continue; + } + if (t.startsWith('$max=')) { + const num = Number(t.slice(5)); + if (Number.isFinite(num)) out.constraints.max = num; + continue; + } + if (t.startsWith('$range=')) { + const m = t.match(/^\$range=\[\s*(-?\d+(?:\.\d+)?)\s*,\s*(-?\d+(?:\.\d+)?)\s*\]$/); + if (m) { + const a = Number(m[1]), b = Number(m[2]); + if (Number.isFinite(a) && Number.isFinite(b)) { + out.constraints.min = Math.min(a, b); + out.constraints.max = Math.max(a, b); + } + } + continue; + } + if (t.startsWith('$step=')) { + const num = Number(t.slice(6)); + if (Number.isFinite(num)) { + out.constraints.step = Math.max(0, Math.abs(num)); + } + continue; + } + + if (t.startsWith('$enum=')) { + const m = t.match(/^\$enum=\{\s*([^}]+)\s*\}$/); + if (m) { + const vals = m[1].split(/[;;]/).map(s => s.trim()).filter(Boolean); + if (vals.length) out.constraints.enum = vals; + } + continue; + } + + if (t.startsWith('$match=')) { + const raw = t.slice(7); + if (raw.startsWith('/') && raw.lastIndexOf('/') > 0) { + const last = raw.lastIndexOf('/'); + const pattern = raw.slice(1, last).replace(/\\\//g, '/'); + const flags = raw.slice(last + 1) || ''; + out.constraints.regex = { source: pattern, flags }; + } + continue; + } + + if (t === '$clear') { out.clear = true; continue; } + } + + return out; +} + +export function expandShorthandRuleObject(basePath, valueObj) { + try { + const base = String(basePath || ''); + const isObj = v => v && typeof v === 'object' && !Array.isArray(v); + + if (!isObj(valueObj)) return null; + + 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; + } + + function formatPathWithBrackets(pathStr) { + const segs = lwbSplitPathWithBrackets(String(pathStr || '')); + let out = ''; + for (const s of segs) { + if (typeof s === 'number') out += `[${s}]`; + else out += out ? `.${s}` : `${s}`; + } + return out; + } + + function assignDeep(dst, src) { + for (const k in src) { + if (!Object.prototype.hasOwnProperty.call(src, k)) continue; + const v = src[k]; + if (v && typeof v === 'object' && !Array.isArray(v)) { + if (!dst[k] || typeof dst[k] !== 'object' || Array.isArray(dst[k])) { + dst[k] = {}; + } + assignDeep(dst[k], v); + } else { + dst[k] = v; + } + } + } + + const rulesTop = {}; + const dataTree = {}; + + function writeDataAt(relPathStr, val) { + const abs = ensureAbsTargetPath(base, relPathStr); + const relSegs = segmentsRelativeToBase(abs, base); + if (relSegs.length) { + setDeepBySegments(dataTree, relSegs, val); + } else { + if (val && typeof val === 'object' && !Array.isArray(val)) { + assignDeep(dataTree, val); + } else { + dataTree['$root'] = val; + } + } + } + + function walk(node, currentRelPathStr) { + if (Array.isArray(node)) { + const cleanedArr = node.map(stripDollarKeysDeep); + if (currentRelPathStr) writeDataAt(currentRelPathStr, cleanedArr); + for (let i = 0; i < node.length; i++) { + const el = node[i]; + if (el && typeof el === 'object') { + const childRel = currentRelPathStr ? `${currentRelPathStr}.${i}` : String(i); + walk(el, childRel); + } + } + return; + } + + if (!isObj(node)) { + if (currentRelPathStr) writeDataAt(currentRelPathStr, node); + return; + } + + const cleaned = stripDollarKeysDeep(node); + if (currentRelPathStr) writeDataAt(currentRelPathStr, cleaned); + else assignDeep(dataTree, cleaned); + + for (const key in node) { + if (!Object.prototype.hasOwnProperty.call(node, key)) continue; + const v = node[key]; + const keyStr = String(key).trim(); + const isRule = keyStr.startsWith('$'); + + if (!isRule) { + const childRel = currentRelPathStr ? `${currentRelPathStr}.${keyStr}` : keyStr; + if (v && typeof v === 'object') walk(v, childRel); + 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 fullRelTarget = currentRelPathStr + ? `${currentRelPathStr}.${targetToken}` + : targetToken; + + const absTarget = ensureAbsTargetPath(base, fullRelTarget); + const absDisplay = formatPathWithBrackets(absTarget); + const ruleKey = `$ ${dirs.join(' ')} ${absDisplay}`.trim(); + rulesTop[ruleKey] = {}; + + if (v !== undefined) { + const cleanedVal = stripDollarKeysDeep(v); + writeDataAt(fullRelTarget, cleanedVal); + if (v && typeof v === 'object') { + walk(v, fullRelTarget); + } + } + } + } + + walk(valueObj, ''); + + const out = {}; + assignDeep(out, rulesTop); + assignDeep(out, dataTree); + return out; + } catch { + return null; + } +} + +export function lwbAssignVarPath(path, value) { + try { + const segs = lwbSplitPathWithBrackets(path); + if (!segs.length) return ''; + + const rootName = String(segs[0]); + let vParsed = parseValueForSet(value); + + if (vParsed && typeof vParsed === 'object') { + try { if (globalThis.LWB_Guard?.loadRules) { const res = globalThis.LWB_Guard.loadRules(vParsed, rootName); if (res?.cleanValue !== undefined) vParsed = res.cleanValue; - if (res?.rulesDelta && globalThis.LWB_Guard?.applyDelta) { - globalThis.LWB_Guard.applyDelta(res.rulesDelta); - globalThis.LWB_Guard.save?.(); - } - } - } catch {} - } - - const absPath = normalizePath(path); - - let guardOk = true; - let guardVal = vParsed; - try { - if (globalThis.LWB_Guard?.validate) { - const g = globalThis.LWB_Guard.validate('set', absPath, vParsed); - guardOk = !!g?.allow; - if ('value' in g) guardVal = g.value; - } - } catch {} - - if (!guardOk) return ''; - - if (segs.length === 1) { - if (guardVal && typeof guardVal === 'object') { - setLocalVariable(rootName, safeJSONStringify(guardVal)); - } else { - setLocalVariable(rootName, String(guardVal ?? '')); - } - return ''; - } - - const rootRaw = getLocalVariable(rootName); - let obj; - const parsed = maybeParseObject(rootRaw); - if (parsed) { - obj = deepClone(parsed); - } else { - // 若根变量不存在:A[0].x 这类路径期望根为数组 - obj = (typeof segs[1] === 'number') ? [] : {}; - } - - const { parent, lastKey } = ensureDeepContainer(obj, segs.slice(1)); - parent[lastKey] = guardVal; - - setLocalVariable(rootName, safeJSONStringify(obj)); - return ''; - } catch { - return ''; - } -} - -export function lwbAddVarPath(path, increment) { - try { - const segs = lwbSplitPathWithBrackets(path); - if (!segs.length) return ''; - - const currentStr = lwbResolveVarPath(path); - const incStr = String(increment ?? ''); - - const currentNum = Number(currentStr); - const incNum = Number(incStr); - const bothNumeric = currentStr !== '' && incStr !== '' - && Number.isFinite(currentNum) && Number.isFinite(incNum); - - const newValue = bothNumeric - ? (currentNum + incNum) - : (currentStr + incStr); - - lwbAssignVarPath(path, newValue); - - return valueToString(newValue); - } catch { - return ''; - } -} - -/** - * 删除变量或深层属性(支持点路径/中括号路径) - * @param {string} path - * @returns {string} 空字符串 - */ -export function lwbDeleteVarPath(path) { - try { - const segs = lwbSplitPathWithBrackets(path); - if (!segs.length) return ''; - - const rootName = String(segs[0]); - const absPath = normalizePath(path); - - // 只有根变量:对齐 /flushvar 的“清空”语义 - if (segs.length === 1) { - try { - if (globalThis.LWB_Guard?.validate) { - const g = globalThis.LWB_Guard.validate('delNode', absPath); - if (!g?.allow) return ''; - } - } catch {} - - setLocalVariable(rootName, ''); - return ''; - } - - const rootRaw = getLocalVariable(rootName); - const parsed = maybeParseObject(rootRaw); - if (!parsed) return ''; - - const obj = deepClone(parsed); - const subSegs = segs.slice(1); - - let cur = obj; - for (let i = 0; i < subSegs.length - 1; i++) { - cur = cur?.[subSegs[i]]; - if (cur == null || typeof cur !== 'object') return ''; - } - - try { - if (globalThis.LWB_Guard?.validate) { - const g = globalThis.LWB_Guard.validate('delNode', absPath); - if (!g?.allow) return ''; - } - } catch {} - - const lastKey = subSegs[subSegs.length - 1]; - if (Array.isArray(cur)) { - if (typeof lastKey === 'number' && lastKey >= 0 && lastKey < cur.length) { - cur.splice(lastKey, 1); - } else { - const equal = (a, b) => a === b || a == b || String(a) === String(b); - for (let i = cur.length - 1; i >= 0; i--) { - if (equal(cur[i], lastKey)) cur.splice(i, 1); - } - } - } else { - try { delete cur[lastKey]; } catch {} - } - - setLocalVariable(rootName, safeJSONStringify(obj)); - return ''; - } catch { - return ''; - } -} - -/** - * 向数组推入值(支持点路径/中括号路径) - * @param {string} path - * @param {*} value - * @returns {string} 新数组长度(字符串) - */ -export function lwbPushVarPath(path, value) { - try { - const segs = lwbSplitPathWithBrackets(path); - if (!segs.length) return ''; - - const rootName = String(segs[0]); - const absPath = normalizePath(path); - const vParsed = parseValueForSet(value); - - // 仅根变量:将 root 视为数组 - if (segs.length === 1) { - try { - if (globalThis.LWB_Guard?.validate) { - const g = globalThis.LWB_Guard.validate('push', absPath, vParsed); - if (!g?.allow) return ''; - } - } catch {} - - const rootRaw = getLocalVariable(rootName); - let arr; - try { arr = JSON.parse(rootRaw); } catch { arr = undefined; } - if (!Array.isArray(arr)) arr = rootRaw != null && rootRaw !== '' ? [rootRaw] : []; - arr.push(vParsed); - setLocalVariable(rootName, safeJSONStringify(arr)); - return String(arr.length); - } - - const rootRaw = getLocalVariable(rootName); - let obj; - const parsed = maybeParseObject(rootRaw); - if (parsed) { - obj = deepClone(parsed); - } else { - const firstSubSeg = segs[1]; - obj = typeof firstSubSeg === 'number' ? [] : {}; - } - - const { parent, lastKey } = ensureDeepContainer(obj, segs.slice(1)); - let arr = parent[lastKey]; - - if (!Array.isArray(arr)) { - arr = arr != null ? [arr] : []; - } - - try { - if (globalThis.LWB_Guard?.validate) { - const g = globalThis.LWB_Guard.validate('push', absPath, vParsed); - if (!g?.allow) return ''; - } - } catch {} - - arr.push(vParsed); - parent[lastKey] = arr; - - setLocalVariable(rootName, safeJSONStringify(obj)); - return String(arr.length); - } catch { - return ''; - } -} - -function registerXbGetVarSlashCommand() { - try { - const ctx = getContext(); - const { SlashCommandParser, SlashCommand, SlashCommandArgument, ARGUMENT_TYPE } = ctx || {}; - - if (!SlashCommandParser?.addCommandObject || !SlashCommand?.fromProps || !SlashCommandArgument?.fromProps) { - return; - } - - SlashCommandParser.addCommandObject(SlashCommand.fromProps({ - name: 'xbgetvar', - returns: 'string', - helpString: ` -
通过点/中括号路径获取嵌套的本地变量值
-
支持 ["0"] 强制字符串键、[0] 数组索引
-
示例:
-
/xbgetvar 人物状态.姓名
-
/xbgetvar A[0].name | /echo {{pipe}}
- `, - unnamedArgumentList: [ - SlashCommandArgument.fromProps({ - description: '变量路径', - typeList: [ARGUMENT_TYPE.STRING], - isRequired: true, - acceptsMultiple: false, - }), - ], - callback: (namedArgs, unnamedArgs) => { - try { - const path = extractPathFromArgs(namedArgs, unnamedArgs); - return lwbResolveVarPath(String(path || '')); - } catch { - return ''; - } - }, - })); - } catch {} -} - -function registerXbSetVarSlashCommand() { - try { - const ctx = getContext(); - const { SlashCommandParser, SlashCommand, SlashCommandArgument, ARGUMENT_TYPE } = ctx || {}; - - if (!SlashCommandParser?.addCommandObject || !SlashCommand?.fromProps || !SlashCommandArgument?.fromProps) { - return; - } - - function joinUnnamed(args) { - if (Array.isArray(args)) { - return args.filter(v => v != null).map(v => String(v)).join(' ').trim(); - } - return String(args ?? '').trim(); - } - - function splitTokensBySpace(s) { - return String(s || '').split(/\s+/).filter(Boolean); - } - - function isDirectiveToken(tok) { - const t = String(tok || '').trim(); - if (!t) return false; - if (['$ro', '$ext', '$prune', '$free', '$grow', '$shrink', '$list', '$clear'].includes(t)) { - return true; - } - if (/^\$(min|max|range|enum|match|step)=/.test(t)) { - return true; - } - return false; - } - - function parseKeyAndValue(namedArgs, unnamedArgs) { - const unnamedJoined = joinUnnamed(unnamedArgs); - const hasNamedKey = typeof namedArgs?.key === 'string' && namedArgs.key.trim().length > 0; - - if (hasNamedKey) { - const keyRaw = namedArgs.key.trim(); - const keyParts = splitTokensBySpace(keyRaw); - - if (keyParts.length > 1 && keyParts.every((p, i) => - isDirectiveToken(p) || i === keyParts.length - 1 - )) { - const directives = keyParts.slice(0, -1); - const realPath = keyParts[keyParts.length - 1]; - return { directives, realPath, valueText: unnamedJoined }; - } - - if (isDirectiveToken(keyRaw)) { - const m = unnamedJoined.match(/^\S+/); - const realPath = m ? m[0] : ''; - const valueText = realPath ? unnamedJoined.slice(realPath.length).trim() : ''; - return { directives: [keyRaw], realPath, valueText }; - } - - return { directives: [], realPath: keyRaw, valueText: unnamedJoined }; - } - - const firstRaw = joinUnnamed(unnamedArgs); - if (!firstRaw) return { directives: [], realPath: '', valueText: '' }; - - const sp = lwbSplitPathAndValue(firstRaw); - let head = String(sp.path || '').trim(); - let rest = String(sp.value || '').trim(); - const parts = splitTokensBySpace(head); - - if (parts.length > 1 && parts.every((p, i) => - isDirectiveToken(p) || i === parts.length - 1 - )) { - const directives = parts.slice(0, -1); - const realPath = parts[parts.length - 1]; - return { directives, realPath, valueText: rest }; - } - - if (isDirectiveToken(head)) { - const m = rest.match(/^\S+/); - const realPath = m ? m[0] : ''; - const valueText = realPath ? rest.slice(realPath.length).trim() : ''; - return { directives: [head], realPath, valueText }; - } - - return { directives: [], realPath: head, valueText: rest }; - } - - SlashCommandParser.addCommandObject(SlashCommand.fromProps({ - name: 'xbsetvar', - returns: 'string', - helpString: ` -
设置嵌套本地变量
-
支持指令前缀:$ro, $min=, $max=, $list 等
-
示例:
-
/xbsetvar A.B.C 123
-
/xbsetvar key="$list 情节小结" ["item1"]
- `, - unnamedArgumentList: [ - SlashCommandArgument.fromProps({ - description: '变量路径或(指令 + 路径)', - typeList: [ARGUMENT_TYPE.STRING], - isRequired: true, - acceptsMultiple: false, - }), - SlashCommandArgument.fromProps({ - description: '要设置的值', - typeList: [ARGUMENT_TYPE.STRING], - isRequired: false, - acceptsMultiple: false, - }), - ], - callback: (namedArgs, unnamedArgs) => { - try { - const parsed = parseKeyAndValue(namedArgs, unnamedArgs); - const directives = parsed.directives || []; - const realPath = String(parsed.realPath || '').trim(); - let rest = String(parsed.valueText || '').trim(); - - if (!realPath) return ''; - - if (directives.length > 0 && globalThis.LWB_Guard?.applyDelta) { - const delta = parseDirectivesTokenList(directives); - const absPath = normalizePath(realPath); - globalThis.LWB_Guard.applyDelta(absPath, delta); - globalThis.LWB_Guard.save?.(); - } - - let toSet = rest; - try { - const parsedVal = parseValueForSet(rest); - if (parsedVal && typeof parsedVal === 'object' && !Array.isArray(parsedVal)) { - const expanded = expandShorthandRuleObject(realPath, parsedVal); - if (expanded && typeof expanded === 'object') { - toSet = safeJSONStringify(expanded) || rest; + if (res?.rulesDelta && typeof res.rulesDelta === 'object') { + if (globalThis.LWB_Guard?.applyDeltaTable) { + globalThis.LWB_Guard.applyDeltaTable(res.rulesDelta); + } else if (globalThis.LWB_Guard?.applyDelta) { + for (const [p, d] of Object.entries(res.rulesDelta)) { + globalThis.LWB_Guard.applyDelta(p, d); } } - } catch {} - - lwbAssignVarPath(realPath, toSet); - return ''; - } catch { - return ''; - } - }, - })); - } catch {} -} - -function registerXbAddVarSlashCommand() { - try { - const ctx = getContext(); - const { SlashCommandParser, SlashCommand, SlashCommandArgument, SlashCommandNamedArgument, ARGUMENT_TYPE } = ctx || {}; - - if (!SlashCommandParser?.addCommandObject || !SlashCommand?.fromProps || !SlashCommandArgument?.fromProps) { - return; - } - - SlashCommandParser.addCommandObject(SlashCommand.fromProps({ - name: 'xbaddvar', - returns: 'string', - helpString: ` -
通过点路径增加变量值
-
两者都为数字时执行加法,否则执行字符串拼接
-
示例:
-
/xbaddvar key=人物状态.金币 100
-
/xbaddvar A.B.count 1
-
/xbaddvar 名字 _后缀
- `, - namedArgumentList: [ - SlashCommandNamedArgument.fromProps({ - name: 'key', - description: '变量路径', - typeList: [ARGUMENT_TYPE.STRING], - isRequired: false, - }), - ], - unnamedArgumentList: [ - SlashCommandArgument.fromProps({ - description: '路径+增量 或 仅增量', - typeList: [ARGUMENT_TYPE.STRING], - isRequired: true, - }), - ], - callback: (namedArgs, unnamedArgs) => { - try { - let path, increment; - - const unnamedJoined = Array.isArray(unnamedArgs) - ? unnamedArgs.filter(v => v != null).map(String).join(' ').trim() - : String(unnamedArgs ?? '').trim(); - - if (namedArgs?.key && String(namedArgs.key).trim()) { - path = String(namedArgs.key).trim(); - increment = unnamedJoined; - } else { - const sp = lwbSplitPathAndValue(unnamedJoined); - path = sp.path; - increment = sp.value; + globalThis.LWB_Guard.save?.(); } - - if (!path) return ''; - - return lwbAddVarPath(path, increment); - } catch { - return ''; } - }, - })); - } catch {} -} - -function registerXbDelVarSlashCommand() { - try { - const ctx = getContext(); - const { SlashCommandParser, SlashCommand, SlashCommandArgument, ARGUMENT_TYPE } = ctx || {}; - - if (!SlashCommandParser?.addCommandObject || !SlashCommand?.fromProps || !SlashCommandArgument?.fromProps) { - return; + } catch {} } - - SlashCommandParser.addCommandObject(SlashCommand.fromProps({ - name: 'xbdelvar', - returns: 'string', - helpString: ` -
删除变量或深层属性
-
示例:
-
/xbdelvar 临时变量
-
/xbdelvar 角色状态.临时buff
-
/xbdelvar 背包[0]
- `, - unnamedArgumentList: [ - SlashCommandArgument.fromProps({ - description: '变量路径', - typeList: [ARGUMENT_TYPE.STRING], - isRequired: true, - }), - ], - callback: (namedArgs, unnamedArgs) => { - try { - const path = extractPathFromArgs(namedArgs, unnamedArgs); - if (!path) return ''; - return lwbDeleteVarPath(path); - } catch { - return ''; - } - }, - })); - } catch {} -} - -function registerXbPushVarSlashCommand() { - try { - const ctx = getContext(); - const { SlashCommandParser, SlashCommand, SlashCommandArgument, SlashCommandNamedArgument, ARGUMENT_TYPE } = ctx || {}; - - if (!SlashCommandParser?.addCommandObject || !SlashCommand?.fromProps || !SlashCommandArgument?.fromProps) { - return; - } - - SlashCommandParser.addCommandObject(SlashCommand.fromProps({ - name: 'xbpushvar', - returns: 'string', - helpString: ` -
向数组推入值
-
返回新数组长度
-
示例:
-
/xbpushvar key=背包 苹果
-
/xbpushvar 角色.技能列表 火球术
- `, - namedArgumentList: [ - SlashCommandNamedArgument.fromProps({ - name: 'key', - description: '数组路径', - typeList: [ARGUMENT_TYPE.STRING], - isRequired: false, - }), - ], - unnamedArgumentList: [ - SlashCommandArgument.fromProps({ - description: '路径+值 或 仅值', - typeList: [ARGUMENT_TYPE.STRING], - isRequired: true, - }), - ], - callback: (namedArgs, unnamedArgs) => { - try { - let path, value; - - const unnamedJoined = Array.isArray(unnamedArgs) - ? unnamedArgs.filter(v => v != null).map(String).join(' ').trim() - : String(unnamedArgs ?? '').trim(); - - if (namedArgs?.key && String(namedArgs.key).trim()) { - path = String(namedArgs.key).trim(); - value = unnamedJoined; - } else { - const sp = lwbSplitPathAndValue(unnamedJoined); - path = sp.path; - value = sp.value; - } - - if (!path) return ''; - return lwbPushVarPath(path, value); - } catch { - return ''; - } - }, - })); - } catch {} -} - -function onMessageRendered(data) { - try { - if (globalThis.LWB_Guard?.validate) return; - - const id = typeof data === 'object' && data !== null - ? (data.messageId ?? data.id ?? data) - : data; - - if (typeof id === 'number') { - applyXbGetVarForMessage(id, true); - } - } catch {} -} - -export function initVarCommands() { - if (initialized) return; - initialized = true; - - events = createModuleEvents(MODULE_ID); - - registerXbGetVarSlashCommand(); - registerXbSetVarSlashCommand(); - registerXbAddVarSlashCommand(); - registerXbDelVarSlashCommand(); - registerXbPushVarSlashCommand(); - - events.on(event_types.USER_MESSAGE_RENDERED, onMessageRendered); - events.on(event_types.CHARACTER_MESSAGE_RENDERED, onMessageRendered); - events.on(event_types.MESSAGE_UPDATED, onMessageRendered); - events.on(event_types.MESSAGE_EDITED, onMessageRendered); - events.on(event_types.MESSAGE_SWIPED, onMessageRendered); -} - -export function cleanupVarCommands() { - if (!initialized) return; - - events?.cleanup(); - events = null; - - initialized = false; -} - -export { - MODULE_ID, -}; + + const absPath = normalizePath(path); + + let guardOk = true; + let guardVal = vParsed; + try { + if (globalThis.LWB_Guard?.validate) { + const g = globalThis.LWB_Guard.validate('set', absPath, vParsed); + guardOk = !!g?.allow; + if ('value' in g) guardVal = g.value; + } + } catch {} + + if (!guardOk) return ''; + + if (segs.length === 1) { + if (guardVal && typeof guardVal === 'object') { + setLocalVariable(rootName, safeJSONStringify(guardVal)); + } else { + setLocalVariable(rootName, String(guardVal ?? '')); + } + return ''; + } + + const rootRaw = getLocalVariable(rootName); + let obj; + const parsed = maybeParseObject(rootRaw); + if (parsed) { + obj = deepClone(parsed); + } else { + // 若根变量不存在:A[0].x 这类路径期望根为数组 + obj = (typeof segs[1] === 'number') ? [] : {}; + } + + const { parent, lastKey } = ensureDeepContainer(obj, segs.slice(1)); + parent[lastKey] = guardVal; + + setLocalVariable(rootName, safeJSONStringify(obj)); + return ''; + } catch { + return ''; + } +} + +export function lwbAddVarPath(path, increment) { + try { + const segs = lwbSplitPathWithBrackets(path); + if (!segs.length) return ''; + + const currentStr = lwbResolveVarPath(path); + const incStr = String(increment ?? ''); + + const currentNum = Number(currentStr); + const incNum = Number(incStr); + const bothNumeric = currentStr !== '' && incStr !== '' + && Number.isFinite(currentNum) && Number.isFinite(incNum); + + const newValue = bothNumeric + ? (currentNum + incNum) + : (currentStr + incStr); + + lwbAssignVarPath(path, newValue); + + return valueToString(newValue); + } catch { + return ''; + } +} + +/** + * 删除变量或深层属性(支持点路径/中括号路径) + * @param {string} path + * @returns {string} 空字符串 + */ +export function lwbDeleteVarPath(path) { + try { + const segs = lwbSplitPathWithBrackets(path); + if (!segs.length) return ''; + + const rootName = String(segs[0]); + const absPath = normalizePath(path); + + // 只有根变量:对齐 /flushvar 的“清空”语义 + if (segs.length === 1) { + try { + if (globalThis.LWB_Guard?.validate) { + const g = globalThis.LWB_Guard.validate('delNode', absPath); + if (!g?.allow) return ''; + } + } catch {} + + setLocalVariable(rootName, ''); + return ''; + } + + const rootRaw = getLocalVariable(rootName); + const parsed = maybeParseObject(rootRaw); + if (!parsed) return ''; + + const obj = deepClone(parsed); + const subSegs = segs.slice(1); + + let cur = obj; + for (let i = 0; i < subSegs.length - 1; i++) { + cur = cur?.[subSegs[i]]; + if (cur == null || typeof cur !== 'object') return ''; + } + + try { + if (globalThis.LWB_Guard?.validate) { + const g = globalThis.LWB_Guard.validate('delNode', absPath); + if (!g?.allow) return ''; + } + } catch {} + + const lastKey = subSegs[subSegs.length - 1]; + if (Array.isArray(cur)) { + if (typeof lastKey === 'number' && lastKey >= 0 && lastKey < cur.length) { + cur.splice(lastKey, 1); + } else { + const equal = (a, b) => a === b || a == b || String(a) === String(b); + for (let i = cur.length - 1; i >= 0; i--) { + if (equal(cur[i], lastKey)) cur.splice(i, 1); + } + } + } else { + try { delete cur[lastKey]; } catch {} + } + + setLocalVariable(rootName, safeJSONStringify(obj)); + return ''; + } catch { + return ''; + } +} + +/** + * 向数组推入值(支持点路径/中括号路径) + * @param {string} path + * @param {*} value + * @returns {string} 新数组长度(字符串) + */ +export function lwbPushVarPath(path, value) { + try { + const segs = lwbSplitPathWithBrackets(path); + if (!segs.length) return ''; + + const rootName = String(segs[0]); + const absPath = normalizePath(path); + const vParsed = parseValueForSet(value); + + // 仅根变量:将 root 视为数组 + if (segs.length === 1) { + try { + if (globalThis.LWB_Guard?.validate) { + const g = globalThis.LWB_Guard.validate('push', absPath, vParsed); + if (!g?.allow) return ''; + } + } catch {} + + const rootRaw = getLocalVariable(rootName); + let arr; + try { arr = JSON.parse(rootRaw); } catch { arr = undefined; } + if (!Array.isArray(arr)) arr = rootRaw != null && rootRaw !== '' ? [rootRaw] : []; + arr.push(vParsed); + setLocalVariable(rootName, safeJSONStringify(arr)); + return String(arr.length); + } + + const rootRaw = getLocalVariable(rootName); + let obj; + const parsed = maybeParseObject(rootRaw); + if (parsed) { + obj = deepClone(parsed); + } else { + const firstSubSeg = segs[1]; + obj = typeof firstSubSeg === 'number' ? [] : {}; + } + + const { parent, lastKey } = ensureDeepContainer(obj, segs.slice(1)); + let arr = parent[lastKey]; + + if (!Array.isArray(arr)) { + arr = arr != null ? [arr] : []; + } + + try { + if (globalThis.LWB_Guard?.validate) { + const g = globalThis.LWB_Guard.validate('push', absPath, vParsed); + if (!g?.allow) return ''; + } + } catch {} + + arr.push(vParsed); + parent[lastKey] = arr; + + setLocalVariable(rootName, safeJSONStringify(obj)); + return String(arr.length); + } catch { + return ''; + } +} + +function registerXbGetVarSlashCommand() { + try { + const ctx = getContext(); + const { SlashCommandParser, SlashCommand, SlashCommandArgument, ARGUMENT_TYPE } = ctx || {}; + + if (!SlashCommandParser?.addCommandObject || !SlashCommand?.fromProps || !SlashCommandArgument?.fromProps) { + return; + } + + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'xbgetvar', + returns: 'string', + helpString: ` +
通过点/中括号路径获取嵌套的本地变量值
+
支持 ["0"] 强制字符串键、[0] 数组索引
+
示例:
+
/xbgetvar 人物状态.姓名
+
/xbgetvar A[0].name | /echo {{pipe}}
+ `, + unnamedArgumentList: [ + SlashCommandArgument.fromProps({ + description: '变量路径', + typeList: [ARGUMENT_TYPE.STRING], + isRequired: true, + acceptsMultiple: false, + }), + ], + callback: (namedArgs, unnamedArgs) => { + try { + const path = extractPathFromArgs(namedArgs, unnamedArgs); + return lwbResolveVarPath(String(path || '')); + } catch { + return ''; + } + }, + })); + } catch {} +} + +function registerXbSetVarSlashCommand() { + try { + const ctx = getContext(); + const { SlashCommandParser, SlashCommand, SlashCommandArgument, ARGUMENT_TYPE } = ctx || {}; + + if (!SlashCommandParser?.addCommandObject || !SlashCommand?.fromProps || !SlashCommandArgument?.fromProps) { + return; + } + + function joinUnnamed(args) { + if (Array.isArray(args)) { + return args.filter(v => v != null).map(v => String(v)).join(' ').trim(); + } + return String(args ?? '').trim(); + } + + function splitTokensBySpace(s) { + return String(s || '').split(/\s+/).filter(Boolean); + } + + function isDirectiveToken(tok) { + const t = String(tok || '').trim(); + if (!t) return false; + if (['$ro', '$ext', '$prune', '$free', '$grow', '$shrink', '$list', '$clear'].includes(t)) { + return true; + } + if (/^\$(min|max|range|enum|match|step)=/.test(t)) { + return true; + } + return false; + } + + function parseKeyAndValue(namedArgs, unnamedArgs) { + const unnamedJoined = joinUnnamed(unnamedArgs); + const hasNamedKey = typeof namedArgs?.key === 'string' && namedArgs.key.trim().length > 0; + + if (hasNamedKey) { + const keyRaw = namedArgs.key.trim(); + const keyParts = splitTokensBySpace(keyRaw); + + if (keyParts.length > 1 && keyParts.every((p, i) => + isDirectiveToken(p) || i === keyParts.length - 1 + )) { + const directives = keyParts.slice(0, -1); + const realPath = keyParts[keyParts.length - 1]; + return { directives, realPath, valueText: unnamedJoined }; + } + + if (isDirectiveToken(keyRaw)) { + const m = unnamedJoined.match(/^\S+/); + const realPath = m ? m[0] : ''; + const valueText = realPath ? unnamedJoined.slice(realPath.length).trim() : ''; + return { directives: [keyRaw], realPath, valueText }; + } + + return { directives: [], realPath: keyRaw, valueText: unnamedJoined }; + } + + const firstRaw = joinUnnamed(unnamedArgs); + if (!firstRaw) return { directives: [], realPath: '', valueText: '' }; + + const sp = lwbSplitPathAndValue(firstRaw); + let head = String(sp.path || '').trim(); + let rest = String(sp.value || '').trim(); + const parts = splitTokensBySpace(head); + + if (parts.length > 1 && parts.every((p, i) => + isDirectiveToken(p) || i === parts.length - 1 + )) { + const directives = parts.slice(0, -1); + const realPath = parts[parts.length - 1]; + return { directives, realPath, valueText: rest }; + } + + if (isDirectiveToken(head)) { + const m = rest.match(/^\S+/); + const realPath = m ? m[0] : ''; + const valueText = realPath ? rest.slice(realPath.length).trim() : ''; + return { directives: [head], realPath, valueText }; + } + + return { directives: [], realPath: head, valueText: rest }; + } + + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'xbsetvar', + returns: 'string', + helpString: ` +
设置嵌套本地变量
+
支持指令前缀:$ro, $min=, $max=, $list 等
+
示例:
+
/xbsetvar A.B.C 123
+
/xbsetvar key="$list 情节小结" ["item1"]
+ `, + unnamedArgumentList: [ + SlashCommandArgument.fromProps({ + description: '变量路径或(指令 + 路径)', + typeList: [ARGUMENT_TYPE.STRING], + isRequired: true, + acceptsMultiple: false, + }), + SlashCommandArgument.fromProps({ + description: '要设置的值', + typeList: [ARGUMENT_TYPE.STRING], + isRequired: false, + acceptsMultiple: false, + }), + ], + callback: (namedArgs, unnamedArgs) => { + try { + const parsed = parseKeyAndValue(namedArgs, unnamedArgs); + const directives = parsed.directives || []; + const realPath = String(parsed.realPath || '').trim(); + let rest = String(parsed.valueText || '').trim(); + + if (!realPath) return ''; + + if (directives.length > 0 && globalThis.LWB_Guard?.applyDelta) { + const delta = parseDirectivesTokenList(directives); + const absPath = normalizePath(realPath); + globalThis.LWB_Guard.applyDelta(absPath, delta); + globalThis.LWB_Guard.save?.(); + } + + let toSet = rest; + try { + const parsedVal = parseValueForSet(rest); + if (parsedVal && typeof parsedVal === 'object' && !Array.isArray(parsedVal)) { + const expanded = expandShorthandRuleObject(realPath, parsedVal); + if (expanded && typeof expanded === 'object') { + toSet = safeJSONStringify(expanded) || rest; + } + } + } catch {} + + lwbAssignVarPath(realPath, toSet); + return ''; + } catch { + return ''; + } + }, + })); + } catch {} +} + +function registerXbAddVarSlashCommand() { + try { + const ctx = getContext(); + const { SlashCommandParser, SlashCommand, SlashCommandArgument, SlashCommandNamedArgument, ARGUMENT_TYPE } = ctx || {}; + + if (!SlashCommandParser?.addCommandObject || !SlashCommand?.fromProps || !SlashCommandArgument?.fromProps) { + return; + } + + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'xbaddvar', + returns: 'string', + helpString: ` +
通过点路径增加变量值
+
两者都为数字时执行加法,否则执行字符串拼接
+
示例:
+
/xbaddvar key=人物状态.金币 100
+
/xbaddvar A.B.count 1
+
/xbaddvar 名字 _后缀
+ `, + namedArgumentList: [ + SlashCommandNamedArgument.fromProps({ + name: 'key', + description: '变量路径', + typeList: [ARGUMENT_TYPE.STRING], + isRequired: false, + }), + ], + unnamedArgumentList: [ + SlashCommandArgument.fromProps({ + description: '路径+增量 或 仅增量', + typeList: [ARGUMENT_TYPE.STRING], + isRequired: true, + }), + ], + callback: (namedArgs, unnamedArgs) => { + try { + let path, increment; + + const unnamedJoined = Array.isArray(unnamedArgs) + ? unnamedArgs.filter(v => v != null).map(String).join(' ').trim() + : String(unnamedArgs ?? '').trim(); + + if (namedArgs?.key && String(namedArgs.key).trim()) { + path = String(namedArgs.key).trim(); + increment = unnamedJoined; + } else { + const sp = lwbSplitPathAndValue(unnamedJoined); + path = sp.path; + increment = sp.value; + } + + if (!path) return ''; + + return lwbAddVarPath(path, increment); + } catch { + return ''; + } + }, + })); + } catch {} +} + +function registerXbDelVarSlashCommand() { + try { + const ctx = getContext(); + const { SlashCommandParser, SlashCommand, SlashCommandArgument, ARGUMENT_TYPE } = ctx || {}; + + if (!SlashCommandParser?.addCommandObject || !SlashCommand?.fromProps || !SlashCommandArgument?.fromProps) { + return; + } + + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'xbdelvar', + returns: 'string', + helpString: ` +
删除变量或深层属性
+
示例:
+
/xbdelvar 临时变量
+
/xbdelvar 角色状态.临时buff
+
/xbdelvar 背包[0]
+ `, + unnamedArgumentList: [ + SlashCommandArgument.fromProps({ + description: '变量路径', + typeList: [ARGUMENT_TYPE.STRING], + isRequired: true, + }), + ], + callback: (namedArgs, unnamedArgs) => { + try { + const path = extractPathFromArgs(namedArgs, unnamedArgs); + if (!path) return ''; + return lwbDeleteVarPath(path); + } catch { + return ''; + } + }, + })); + } catch {} +} + +function registerXbPushVarSlashCommand() { + try { + const ctx = getContext(); + const { SlashCommandParser, SlashCommand, SlashCommandArgument, SlashCommandNamedArgument, ARGUMENT_TYPE } = ctx || {}; + + if (!SlashCommandParser?.addCommandObject || !SlashCommand?.fromProps || !SlashCommandArgument?.fromProps) { + return; + } + + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'xbpushvar', + returns: 'string', + helpString: ` +
向数组推入值
+
返回新数组长度
+
示例:
+
/xbpushvar key=背包 苹果
+
/xbpushvar 角色.技能列表 火球术
+ `, + namedArgumentList: [ + SlashCommandNamedArgument.fromProps({ + name: 'key', + description: '数组路径', + typeList: [ARGUMENT_TYPE.STRING], + isRequired: false, + }), + ], + unnamedArgumentList: [ + SlashCommandArgument.fromProps({ + description: '路径+值 或 仅值', + typeList: [ARGUMENT_TYPE.STRING], + isRequired: true, + }), + ], + callback: (namedArgs, unnamedArgs) => { + try { + let path, value; + + const unnamedJoined = Array.isArray(unnamedArgs) + ? unnamedArgs.filter(v => v != null).map(String).join(' ').trim() + : String(unnamedArgs ?? '').trim(); + + if (namedArgs?.key && String(namedArgs.key).trim()) { + path = String(namedArgs.key).trim(); + value = unnamedJoined; + } else { + const sp = lwbSplitPathAndValue(unnamedJoined); + path = sp.path; + value = sp.value; + } + + if (!path) return ''; + return lwbPushVarPath(path, value); + } catch { + return ''; + } + }, + })); + } catch {} +} + +function onMessageRendered(data) { + try { + if (globalThis.LWB_Guard?.validate) return; + + const id = typeof data === 'object' && data !== null + ? (data.messageId ?? data.id ?? data) + : data; + + if (typeof id === 'number') { + applyXbGetVarForMessage(id, true); + } + } catch {} +} + +export function initVarCommands() { + if (initialized) return; + initialized = true; + + events = createModuleEvents(MODULE_ID); + + registerXbGetVarSlashCommand(); + registerXbSetVarSlashCommand(); + registerXbAddVarSlashCommand(); + registerXbDelVarSlashCommand(); + registerXbPushVarSlashCommand(); + + events.on(event_types.USER_MESSAGE_RENDERED, onMessageRendered); + events.on(event_types.CHARACTER_MESSAGE_RENDERED, onMessageRendered); + events.on(event_types.MESSAGE_UPDATED, onMessageRendered); + events.on(event_types.MESSAGE_EDITED, onMessageRendered); + events.on(event_types.MESSAGE_SWIPED, onMessageRendered); +} + +export function cleanupVarCommands() { + if (!initialized) return; + + events?.cleanup(); + events = null; + + initialized = false; +} + +export { + MODULE_ID, +}; diff --git a/modules/variables/variables-core.js b/modules/variables/variables-core.js index 91942f0..12b5955 100644 --- a/modules/variables/variables-core.js +++ b/modules/variables/variables-core.js @@ -1,2385 +1,2389 @@ -/** - * @file modules/variables/variables-core.js - * @description 变量管理核心(受开关控制) - * @description 包含 plot-log 解析、快照回滚、变量守护 - */ - -import { getContext, extension_settings } 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, - drainPendingVareventBlocks, - stripYamlInlineComment, - OP_MAP, - TOP_OP_RE, -} from "./varevent-editor.js"; - -/* ============= 模块常量 ============= */ - -const MODULE_ID = 'variablesCore'; -const LWB_EXT_ID = 'LittleWhiteBox'; -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; - -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*/); - 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; - +/** + * @file modules/variables/variables-core.js + * @description 变量管理核心(受开关控制) + * @description 包含 plot-log 解析、快照回滚、变量守护 + */ + +import { getContext, extension_settings } 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, + drainPendingVareventBlocks, + stripYamlInlineComment, + OP_MAP, + TOP_OP_RE, +} from "./varevent-editor.js"; + +/* ============= 模块常量 ============= */ + +const MODULE_ID = 'variablesCore'; +const LWB_EXT_ID = 'LittleWhiteBox'; +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; + +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*/); + 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 targetPath = curAbs ? `${curAbs}.${targetToken}` : targetToken; + 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() { - const pendingSwipeApply = new Map(); - let lastSwipedId; - const 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, -}; + 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() { + const pendingSwipeApply = new Map(); + let lastSwipedId; + const 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, +};