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: ` -
/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: `
- /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: `
+ /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: `
+ /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,
+};