Files
LittleWhiteBox/modules/variables/var-commands.js
2026-01-28 00:28:01 +08:00

1062 lines
36 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* @file modules/variables/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 jsYaml from "../../libs/js-yaml.mjs";
import {
lwbSplitPathWithBrackets,
lwbSplitPathAndValue,
normalizePath,
ensureDeepContainer,
safeJSONStringify,
maybeParseObject,
valueToString,
deepClone,
} from "../../core/variable-path.js";
const MODULE_ID = 'varCommands';
const TAG_RE_XBGETVAR = /\{\{xbgetvar::([^}]+)\}\}/gi;
const TAG_RE_XBGETVAR_YAML = /\{\{xbgetvar_yaml::([^}]+)\}\}/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 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];
const nextSeg = segs[i + 1];
const wantArray = (typeof nextSeg === 'number');
// 已存在且类型正确:继续深入
if (wantArray && Array.isArray(nxt)) {
cur = nxt;
continue;
}
if (!wantArray && (nxt && typeof nxt === 'object') && !Array.isArray(nxt)) {
cur = nxt;
continue;
}
// 不存在或类型不匹配:创建正确的容器
cur[key] = wantArray ? [] : {};
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));
}
/**
* 将 {{xbgetvar_yaml::路径}} 替换为 YAML 格式的值
* @param {string} s
* @returns {string}
*/
export function replaceXbGetVarYamlInString(s) {
s = String(s ?? '');
if (!s || s.indexOf('{{xbgetvar_yaml::') === -1) return s;
TAG_RE_XBGETVAR_YAML.lastIndex = 0;
return s.replace(TAG_RE_XBGETVAR_YAML, (_, p) => {
const value = lwbResolveVarPath(p);
if (!value) return '';
// 尝试解析为对象/数组,然后转 YAML
try {
const parsed = JSON.parse(value);
if (typeof parsed === 'object' && parsed !== null) {
return jsYaml.dump(parsed, {
indent: 2,
lineWidth: -1,
noRefs: true,
quotingType: '"',
}).trim();
}
return value;
} catch {
return value;
}
});
}
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] ?? '');
const hasJson = old.indexOf('{{xbgetvar::') !== -1;
const hasYaml = old.indexOf('{{xbgetvar_yaml::') !== -1;
if (!hasJson && !hasYaml) continue;
let result = hasJson ? replaceXbGetVarInString(old) : old;
result = hasYaml ? replaceXbGetVarYamlInString(result) : result;
msg[key] = result;
} 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] ?? '');
const hasJson = old.indexOf('{{xbgetvar::') !== -1;
const hasYaml = old.indexOf('{{xbgetvar_yaml::') !== -1;
if (!hasJson && !hasYaml) return;
let out = hasJson ? replaceXbGetVarInString(old) : old;
out = hasYaml ? replaceXbGetVarYamlInString(out) : out;
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 && 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);
}
}
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: `
<div>通过点/中括号路径获取嵌套的本地变量值</div>
<div>支持 ["0"] 强制字符串键、[0] 数组索引</div>
<div><strong>示例:</strong></div>
<pre><code>/xbgetvar 人物状态.姓名</code></pre>
<pre><code>/xbgetvar A[0].name | /echo {{pipe}}</code></pre>
`,
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: `
<div>设置嵌套本地变量</div>
<div>支持指令前缀:$ro, $min=, $max=, $list 等</div>
<div><strong>示例:</strong></div>
<pre><code>/xbsetvar A.B.C 123</code></pre>
<pre><code>/xbsetvar key="$list 情节小结" ["item1"]</code></pre>
`,
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: `
<div>通过点路径增加变量值</div>
<div>两者都为数字时执行加法,否则执行字符串拼接</div>
<div><strong>示例:</strong></div>
<pre><code>/xbaddvar key=人物状态.金币 100</code></pre>
<pre><code>/xbaddvar A.B.count 1</code></pre>
<pre><code>/xbaddvar 名字 _后缀</code></pre>
`,
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: `
<div>删除变量或深层属性</div>
<div><strong>示例:</strong></div>
<pre><code>/xbdelvar 临时变量</code></pre>
<pre><code>/xbdelvar 角色状态.临时buff</code></pre>
<pre><code>/xbdelvar 背包[0]</code></pre>
`,
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: `
<div>向数组推入值</div>
<div>返回新数组长度</div>
<div><strong>示例:</strong></div>
<pre><code>/xbpushvar key=背包 苹果</code></pre>
<pre><code>/xbpushvar 角色.技能列表 火球术</code></pre>
`,
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,
};