feat: variables 2.0 state + L0 summary integration

This commit is contained in:
2026-01-31 23:06:03 +08:00
parent 201c74dc71
commit 4b0541610b
22 changed files with 1949 additions and 2314 deletions

View File

@@ -0,0 +1,199 @@
import { getContext } from '../../../../../../extensions.js';
import {
lwbResolveVarPath,
lwbAssignVarPath,
lwbAddVarPath,
lwbPushVarPath,
lwbDeleteVarPath,
lwbRemoveArrayItemByValue,
} from '../var-commands.js';
import { lwbSplitPathWithBrackets } from '../../../core/variable-path.js';
import { extractStateBlocks, computeStateSignature, parseStateBlock } from './parser.js';
import { generateSemantic } from './semantic.js';
const LWB_STATE_APPLIED_KEY = 'LWB_STATE_APPLIED_KEY';
/**
* chatMetadata 内记录每楼层 signature防止重复执行
*/
function getAppliedMap() {
const meta = getContext()?.chatMetadata || {};
meta[LWB_STATE_APPLIED_KEY] ||= {};
return meta[LWB_STATE_APPLIED_KEY];
}
export function clearStateAppliedFor(floor) {
try {
const map = getAppliedMap();
delete map[floor];
getContext()?.saveMetadataDebounced?.();
} catch {}
}
export function clearStateAppliedFrom(floorInclusive) {
try {
const map = getAppliedMap();
for (const k of Object.keys(map)) {
const id = Number(k);
if (!Number.isNaN(id) && id >= floorInclusive) delete map[k];
}
getContext()?.saveMetadataDebounced?.();
} catch {}
}
function safeParseAny(str) {
if (str == null || str === '') return undefined;
if (typeof str !== 'string') return str;
const t = str.trim();
if (!t) return undefined;
if (t[0] === '{' || t[0] === '[') {
try { return JSON.parse(t); } catch { return str; }
}
if (/^-?\d+(?:\.\d+)?$/.test(t)) return Number(t);
if (t === 'true') return true;
if (t === 'false') return false;
return str;
}
function isIndexDeleteOp(opItem) {
if (!opItem || opItem.op !== 'del') return false;
const segs = lwbSplitPathWithBrackets(opItem.path);
if (!segs.length) return false;
const last = segs[segs.length - 1];
return typeof last === 'number' && Number.isFinite(last);
}
function buildParentPathFromSegs(segs) {
return segs.reduce((acc, s) => {
if (typeof s === 'number') return `${acc}[${s}]`;
return acc ? `${acc}.${s}` : String(s);
}, '');
}
function buildExecOpsWithIndexDeleteReorder(ops) {
const groups = new Map(); // parentPath -> [{ op, idx }]
const groupOrder = new Map();
let orderCounter = 0;
const normalOps = [];
for (const op of ops) {
if (isIndexDeleteOp(op)) {
const segs = lwbSplitPathWithBrackets(op.path);
const idx = segs[segs.length - 1];
const parentPath = buildParentPathFromSegs(segs.slice(0, -1));
if (!groups.has(parentPath)) {
groups.set(parentPath, []);
groupOrder.set(parentPath, orderCounter++);
}
groups.get(parentPath).push({ op, idx });
} else {
normalOps.push(op);
}
}
const orderedParents = Array.from(groups.keys()).sort(
(a, b) => (groupOrder.get(a) ?? 0) - (groupOrder.get(b) ?? 0)
);
const reorderedIndexDeletes = [];
for (const parent of orderedParents) {
const items = groups.get(parent) || [];
items.sort((a, b) => b.idx - a.idx);
for (const it of items) reorderedIndexDeletes.push(it.op);
}
return [...reorderedIndexDeletes, ...normalOps];
}
/**
* 变量 2.0:执行单条消息里的 <state>,返回 atoms
*/
export function applyStateForMessage(messageId, messageContent) {
const ctx = getContext();
const chatId = ctx?.chatId || '';
const text = String(messageContent ?? '');
const signature = computeStateSignature(text);
// 没有 state清理旧 signature避免“删掉 state 后仍然认为执行过”)
if (!signature) {
clearStateAppliedFor(messageId);
return { atoms: [], errors: [], skipped: false };
}
// 幂等signature 没变就跳过
const appliedMap = getAppliedMap();
if (appliedMap[messageId] === signature) {
return { atoms: [], errors: [], skipped: true };
}
const blocks = extractStateBlocks(text);
const atoms = [];
const errors = [];
let idx = 0;
for (const block of blocks) {
const ops = parseStateBlock(block);
const execOps = buildExecOpsWithIndexDeleteReorder(ops);
for (const opItem of execOps) {
const { path, op, value, delta, warning } = opItem;
if (!path) continue;
if (warning) errors.push(`[${path}] ${warning}`);
const oldValue = safeParseAny(lwbResolveVarPath(path));
try {
switch (op) {
case 'set':
lwbAssignVarPath(path, value);
break;
case 'inc':
lwbAddVarPath(path, delta);
break;
case 'push':
lwbPushVarPath(path, value);
break;
case 'pop':
lwbRemoveArrayItemByValue(path, value);
break;
case 'del':
lwbDeleteVarPath(path);
break;
default:
errors.push(`[${path}] 未知 op=${op}`);
continue;
}
} catch (e) {
errors.push(`[${path}] 执行失败: ${e?.message || e}`);
continue;
}
const newValue = safeParseAny(lwbResolveVarPath(path));
atoms.push({
atomId: `sa-${messageId}-${idx}`,
chatId,
floor: messageId,
idx,
path,
op,
oldValue,
newValue,
delta: op === 'inc' ? delta : undefined,
semantic: generateSemantic(path, op, oldValue, newValue, delta, value),
timestamp: Date.now(),
});
idx++;
}
}
appliedMap[messageId] = signature;
getContext()?.saveMetadataDebounced?.();
return { atoms, errors, skipped: false };
}

View File

@@ -0,0 +1,3 @@
export { applyStateForMessage, clearStateAppliedFor, clearStateAppliedFrom } from './executor.js';
export { parseStateBlock, extractStateBlocks, computeStateSignature, parseInlineValue } from './parser.js';
export { generateSemantic } from './semantic.js';

View File

@@ -0,0 +1,196 @@
import jsyaml from '../../../libs/js-yaml.mjs';
const STATE_TAG_RE = /<\s*state\b[^>]*>([\s\S]*?)<\s*\/\s*state\s*>/gi;
export function extractStateBlocks(text) {
const s = String(text ?? '');
if (!s || s.toLowerCase().indexOf('<state') === -1) return [];
const out = [];
STATE_TAG_RE.lastIndex = 0;
let m;
while ((m = STATE_TAG_RE.exec(s)) !== null) {
const inner = String(m[1] ?? '');
if (inner.trim()) out.push(inner);
}
return out;
}
export function computeStateSignature(text) {
const s = String(text ?? '');
if (!s || s.toLowerCase().indexOf('<state') === -1) return '';
const chunks = [];
STATE_TAG_RE.lastIndex = 0;
let m;
while ((m = STATE_TAG_RE.exec(s)) !== null) chunks.push(String(m[0] ?? '').trim());
return chunks.length ? chunks.join('\n---\n') : '';
}
/**
* 解析 <state> 块内容 -> ops[]
* 单行支持运算符,多行只支持覆盖 setYAML
*/
export function parseStateBlock(content) {
const results = [];
const lines = String(content ?? '').split(/\r?\n/);
let pendingPath = null;
let pendingLines = [];
const flushPending = () => {
if (!pendingPath) return;
// 没有任何缩进行:视为 set 空字符串
if (!pendingLines.length) {
results.push({ path: pendingPath, op: 'set', value: '' });
pendingPath = null;
pendingLines = [];
return;
}
try {
// 去除公共缩进
const nonEmpty = pendingLines.filter(l => l.trim());
const minIndent = nonEmpty.length
? Math.min(...nonEmpty.map(l => l.search(/\S/)))
: 0;
const yamlText = pendingLines
.map(l => (l.trim() ? l.slice(minIndent) : ''))
.join('\n');
const obj = jsyaml.load(yamlText);
results.push({ path: pendingPath, op: 'set', value: obj });
} catch (e) {
results.push({ path: pendingPath, op: 'set', value: null, warning: `YAML 解析失败: ${e.message}` });
} finally {
pendingPath = null;
pendingLines = [];
}
};
for (const raw of lines) {
const trimmed = raw.trim();
if (!trimmed || trimmed.startsWith('#')) continue;
const indent = raw.search(/\S/);
if (indent === 0) {
flushPending();
const colonIdx = findTopLevelColon(trimmed);
if (colonIdx === -1) continue;
const path = trimmed.slice(0, colonIdx).trim();
const rhs = trimmed.slice(colonIdx + 1).trim();
if (!path) continue;
if (!rhs) {
pendingPath = path;
pendingLines = [];
} else {
results.push({ path, ...parseInlineValue(rhs) });
}
} else if (pendingPath) {
pendingLines.push(raw);
}
}
flushPending();
return results;
}
function findTopLevelColon(line) {
let inQuote = false;
let q = '';
let esc = false;
for (let i = 0; i < line.length; i++) {
const ch = line[i];
if (esc) { esc = false; continue; }
if (ch === '\\') { esc = true; continue; }
if (!inQuote && (ch === '"' || ch === "'")) { inQuote = true; q = ch; continue; }
if (inQuote && ch === q) { inQuote = false; q = ''; continue; }
if (!inQuote && ch === ':') return i;
}
return -1;
}
function unescapeString(s) {
return String(s ?? '')
.replace(/\\n/g, '\n')
.replace(/\\t/g, '\t')
.replace(/\\r/g, '\r')
.replace(/\\"/g, '"')
.replace(/\\'/g, "'")
.replace(/\\\\/g, '\\');
}
/**
* 单行内联值解析
*/
export function parseInlineValue(raw) {
const t = String(raw ?? '').trim();
if (t === 'null') return { op: 'del' };
// (负数) 用于强制 set -5而不是 inc -5
const parenNum = t.match(/^\((-?\d+(?:\.\d+)?)\)$/);
if (parenNum) return { op: 'set', value: Number(parenNum[1]) };
// +10 / -20
if (/^\+\d/.test(t) || /^-\d/.test(t)) {
const n = Number(t);
if (Number.isFinite(n)) return { op: 'inc', delta: n };
}
// +"str" / +'str'
const pushD = t.match(/^\+"((?:[^"\\]|\\.)*)"\s*$/);
if (pushD) return { op: 'push', value: unescapeString(pushD[1]) };
const pushS = t.match(/^\+'((?:[^'\\]|\\.)*)'\s*$/);
if (pushS) return { op: 'push', value: unescapeString(pushS[1]) };
// +[...]
if (t.startsWith('+[')) {
try {
const arr = JSON.parse(t.slice(1));
if (Array.isArray(arr)) return { op: 'push', value: arr };
return { op: 'set', value: t, warning: '+[] 不是数组,作为字符串' };
} catch {
return { op: 'set', value: t, warning: '+[] JSON 解析失败,作为字符串' };
}
}
// -"str" / -'str'
const popD = t.match(/^-"((?:[^"\\]|\\.)*)"\s*$/);
if (popD) return { op: 'pop', value: unescapeString(popD[1]) };
const popS = t.match(/^-'((?:[^'\\]|\\.)*)'\s*$/);
if (popS) return { op: 'pop', value: unescapeString(popS[1]) };
// -[...]
if (t.startsWith('-[')) {
try {
const arr = JSON.parse(t.slice(1));
if (Array.isArray(arr)) return { op: 'pop', value: arr };
return { op: 'set', value: t, warning: '-[] 不是数组,作为字符串' };
} catch {
return { op: 'set', value: t, warning: '-[] JSON 解析失败,作为字符串' };
}
}
// 裸数字 set
if (/^-?\d+(?:\.\d+)?$/.test(t)) return { op: 'set', value: Number(t) };
// "str" / 'str'
const strD = t.match(/^"((?:[^"\\]|\\.)*)"\s*$/);
if (strD) return { op: 'set', value: unescapeString(strD[1]) };
const strS = t.match(/^'((?:[^'\\]|\\.)*)'\s*$/);
if (strS) return { op: 'set', value: unescapeString(strS[1]) };
if (t === 'true') return { op: 'set', value: true };
if (t === 'false') return { op: 'set', value: false };
// JSON set
if (t.startsWith('{') || t.startsWith('[')) {
try { return { op: 'set', value: JSON.parse(t) }; }
catch { return { op: 'set', value: t, warning: 'JSON 解析失败,作为字符串' }; }
}
// 兜底 set 原文本
return { op: 'set', value: t };
}

View File

@@ -0,0 +1,42 @@
export function generateSemantic(path, op, oldValue, newValue, delta, operandValue) {
const p = String(path ?? '').replace(/\./g, ' > ');
const fmt = (v) => {
if (v === undefined) return '空';
if (v === null) return 'null';
try {
if (typeof v === 'string') return JSON.stringify(v);
return JSON.stringify(v);
} catch {
return String(v);
}
};
switch (op) {
case 'set':
return oldValue === undefined
? `${p} 设为 ${fmt(newValue)}`
: `${p}${fmt(oldValue)} 变为 ${fmt(newValue)}`;
case 'inc': {
const sign = (delta ?? 0) >= 0 ? '+' : '';
return `${p} ${sign}${delta}${fmt(oldValue)}${fmt(newValue)}`;
}
case 'push': {
const items = Array.isArray(operandValue) ? operandValue : [operandValue];
return `${p} 加入 ${items.map(fmt).join('、')}`;
}
case 'pop': {
const items = Array.isArray(operandValue) ? operandValue : [operandValue];
return `${p} 移除 ${items.map(fmt).join('、')}`;
}
case 'del':
return `${p} 被删除(原值 ${fmt(oldValue)}`;
default:
return `${p} 操作 ${op}`;
}
}

View File

@@ -667,6 +667,62 @@ export function lwbPushVarPath(path, value) {
}
}
export function lwbRemoveArrayItemByValue(path, valuesToRemove) {
try {
const segs = lwbSplitPathWithBrackets(path);
if (!segs.length) return '';
const rootName = String(segs[0]);
const rootRaw = getLocalVariable(rootName);
const rootObj = maybeParseObject(rootRaw);
if (!rootObj) return '';
// 定位到目标数组
let cur = rootObj;
for (let i = 1; i < segs.length; i++) {
cur = cur?.[segs[i]];
if (cur == null) return '';
}
if (!Array.isArray(cur)) return '';
const toRemove = Array.isArray(valuesToRemove) ? valuesToRemove : [valuesToRemove];
if (!toRemove.length) return '';
// 找到索引(每个值只删除一个匹配项)
const indices = [];
for (const v of toRemove) {
const vStr = safeJSONStringify(v);
if (!vStr) continue;
const idx = cur.findIndex(x => safeJSONStringify(x) === vStr);
if (idx !== -1) indices.push(idx);
}
if (!indices.length) return '';
// 倒序删除,且逐个走 guardian 的 delNode 校验(用 index path
indices.sort((a, b) => b - a);
for (const idx of indices) {
const absIndexPath = normalizePath(`${path}[${idx}]`);
try {
if (globalThis.LWB_Guard?.validate) {
const g = globalThis.LWB_Guard.validate('delNode', absIndexPath);
if (!g?.allow) continue;
}
} catch {}
if (idx >= 0 && idx < cur.length) {
cur.splice(idx, 1);
}
}
setLocalVariable(rootName, safeJSONStringify(rootObj));
return '';
} catch {
return '';
}
}
function registerXbGetVarSlashCommand() {
try {
const ctx = getContext();

View File

@@ -1,10 +1,10 @@
/**
* @file modules/variables/variables-core.js
* @description 变量管理核心(受开关控制)
* @description 包含 plot-log 解析、快照回滚、变量守护
* @description Variables core (feature-flag controlled)
* @description Includes plot-log parsing, snapshot rollback, and variable guard
*/
import { getContext } from "../../../../../extensions.js";
import { extension_settings, getContext } from "../../../../../extensions.js";
import { updateMessageBlock } from "../../../../../../script.js";
import { getLocalVariable, setLocalVariable } from "../../../../../variables.js";
import { createModuleEvents, event_types } from "../../core/event-manager.js";
@@ -28,6 +28,7 @@ import {
applyXbGetVarForMessage,
parseValueForSet,
} from "./var-commands.js";
import { applyStateForMessage, clearStateAppliedFrom } from "./state2/index.js";
import {
preprocessBumpAliases,
executeQueuedVareventJsAfterTurn,
@@ -36,17 +37,18 @@ import {
TOP_OP_RE,
} from "./varevent-editor.js";
/* ============= 模块常量 ============= */
/* ============ Module Constants ============= */
const MODULE_ID = 'variablesCore';
const 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 标签正则
// plot-log tag regex
const TAG_RE_PLOTLOG = /<\s*plot-log[^>]*>([\s\S]*?)<\s*\/\s*plot-log\s*>/gi;
// 守护状态
// guardian state
const guardianState = {
table: {},
regexCache: {},
@@ -55,7 +57,8 @@ const guardianState = {
lastMetaSyncAt: 0
};
// 事件管理器
// note
let events = null;
let initialized = false;
let pendingSwipeApply = new Map();
@@ -76,7 +79,7 @@ CacheRegistry.register(MODULE_ID, {
return 0;
}
},
// 新增:估算字节大小(用于 debug-panel 缓存统计)
// estimate bytes for debug panel
getBytes: () => {
try {
let total = 0;
@@ -137,7 +140,7 @@ CacheRegistry.register(MODULE_ID, {
},
});
/* ============= 内部辅助函数 ============= */
/* ============ Internal Helpers ============= */
function getMsgKey(msg) {
return (typeof msg?.mes === 'string') ? 'mes'
@@ -160,7 +163,7 @@ function normalizeOpName(k) {
return OP_MAP[String(k).toLowerCase().trim()] || null;
}
/* ============= 应用签名追踪 ============= */
/* ============ Applied Signature Tracking ============= */
function getAppliedMap() {
const meta = getContext()?.chatMetadata || {};
@@ -206,10 +209,10 @@ function computePlotSignatureFromText(text) {
return chunks.join('\n---\n');
}
/* ============= Plot-Log 解析 ============= */
/* ============ Plot-Log Parsing ============= */
/**
* 提取 plot-log
* Extract plot-log blocks
*/
function extractPlotLogBlocks(text) {
if (!text || typeof text !== 'string') return [];
@@ -224,10 +227,10 @@ function extractPlotLogBlocks(text) {
}
/**
* 解析 plot-log 块内容
* Parse plot-log block content
*/
function parseBlock(innerText) {
// 预处理 bump 别名
// preprocess bump aliases
innerText = preprocessBumpAliases(innerText);
const textForJsonToml = stripLeadingHtmlComments(innerText);
@@ -243,7 +246,7 @@ function parseBlock(innerText) {
};
const norm = (p) => String(p || '').replace(/\[(\d+)\]/g, '.$1');
// 守护指令记录
// guard directive tracking
const guardMap = new Map();
const recordGuardDirective = (path, directives) => {
@@ -292,7 +295,7 @@ function parseBlock(innerText) {
return { directives, curPathRaw, guardTargetRaw, segment: segTrim };
};
// 操作记录函数
// operation record helpers
const putSet = (top, path, value) => {
ops.set[top] ||= {};
ops.set[top][path] = value;
@@ -348,7 +351,7 @@ function parseBlock(innerText) {
return results;
};
// 解码键
// decode key
const decodeKey = (rawKey) => {
const { directives, remainder, original } = extractDirectiveInfo(rawKey);
const path = (remainder || original || String(rawKey)).trim();
@@ -356,7 +359,7 @@ function parseBlock(innerText) {
return path;
};
// 遍历节点
// walk nodes
const walkNode = (op, top, node, basePath = '') => {
if (op === 'set') {
if (node === null || node === undefined) return;
@@ -441,7 +444,7 @@ function parseBlock(innerText) {
}
};
// 处理结构化数据JSON/TOML
// process structured data (json/toml)
const processStructuredData = (data) => {
const process = (d) => {
if (!d || typeof d !== 'object') return;
@@ -507,7 +510,7 @@ function parseBlock(innerText) {
return true;
};
// 尝试 JSON 解析
// try JSON parsing
const tryParseJson = (text) => {
const s = String(text || '').trim();
if (!s || (s[0] !== '{' && s[0] !== '[')) return false;
@@ -563,7 +566,7 @@ function parseBlock(innerText) {
return relaxed !== s && attempt(relaxed);
};
// 尝试 TOML 解析
// try TOML parsing
const tryParseToml = (text) => {
const src = String(text || '').trim();
if (!src || !src.includes('[') || !src.includes('=')) return false;
@@ -638,11 +641,11 @@ function parseBlock(innerText) {
}
};
// 尝试 JSON/TOML
// try JSON/TOML
if (tryParseJson(textForJsonToml)) return finalizeResults();
if (tryParseToml(textForJsonToml)) return finalizeResults();
// YAML 解析
// YAML parsing
let curOp = '';
const stack = [];
@@ -729,7 +732,8 @@ function parseBlock(innerText) {
const curPath = norm(curPathRaw);
if (!curPath) continue;
// 块标量
// note
if (rhs && (rhs[0] === '|' || rhs[0] === '>')) {
const { text, next } = readBlockScalar(i + 1, ind, rhs[0]);
i = next;
@@ -741,7 +745,7 @@ function parseBlock(innerText) {
continue;
}
// 空值(嵌套对象或列表)
// empty value (nested object or list)
if (rhs === '') {
stack.push({
indent: ind,
@@ -791,7 +795,8 @@ function parseBlock(innerText) {
continue;
}
// 普通值
// note
const [top, ...rest] = curPath.split('.');
const rel = rest.join('.');
if (curOp === 'set') {
@@ -817,7 +822,8 @@ function parseBlock(innerText) {
continue;
}
// 顶层列表项del 操作)
// note
const mArr = t.match(/^-+\s*(.+)$/);
if (mArr && stack.length === 0 && curOp === 'del') {
const rawItem = stripQ(stripYamlInlineComment(mArr[1]));
@@ -830,7 +836,8 @@ function parseBlock(innerText) {
continue;
}
// 嵌套列表项
// note
if (mArr && stack.length) {
const curPath = stack[stack.length - 1].path;
const [top, ...rest] = curPath.split('.');
@@ -856,7 +863,7 @@ function parseBlock(innerText) {
return finalizeResults();
}
/* ============= 变量守护与规则集 ============= */
/* ============ Variable Guard & Rules ============= */
function rulesGetTable() {
return guardianState.table || {};
@@ -877,7 +884,7 @@ function rulesLoadFromMeta() {
const raw = meta[LWB_RULES_KEY];
if (raw && typeof raw === 'object') {
rulesSetTable(deepClone(raw));
// 重建正则缓存
// rebuild regex cache
for (const [p, node] of Object.entries(guardianState.table)) {
if (node?.constraints?.regex?.source) {
const src = node.constraints.regex.source;
@@ -1043,7 +1050,7 @@ function getEffectiveParentNode(p) {
}
/**
* 守护验证
* guard validation
*/
export function guardValidate(op, absPath, payload) {
if (guardianState.bypass) return { allow: true, value: payload };
@@ -1057,14 +1064,15 @@ export function guardValidate(op, absPath, payload) {
constraints: {}
};
// 只读检查
// note
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);
// 删除操作
// delete op
if (op === 'delNode') {
if (!parentPath) return { allow: false, reason: 'no-parent' };
@@ -1087,7 +1095,7 @@ export function guardValidate(op, absPath, payload) {
}
}
// 推入操作
// push op
if (op === 'push') {
const arr = getValueAtPath(p);
if (arr === undefined) {
@@ -1124,7 +1132,7 @@ export function guardValidate(op, absPath, payload) {
return { allow: true, value: payload };
}
// 增量操作
// bump op
if (op === 'bump') {
let d = Number(payload);
if (!Number.isFinite(d)) return { allow: false, reason: 'delta-nan' };
@@ -1167,7 +1175,7 @@ export function guardValidate(op, absPath, payload) {
return { allow: true, value: clamped.value };
}
// 设置操作
// set op
if (op === 'set') {
const exists = currentValue !== undefined;
if (!exists) {
@@ -1229,7 +1237,7 @@ export function guardValidate(op, absPath, payload) {
}
/**
* 应用规则增量
* apply rules delta
*/
export function applyRuleDelta(path, delta) {
const p = normalizePath(path);
@@ -1284,7 +1292,7 @@ export function applyRuleDelta(path, delta) {
}
/**
* 从树加载规则
* load rules from tree
*/
export function rulesLoadFromTree(valueTree, basePath) {
const isObj = v => v && typeof v === 'object' && !Array.isArray(v);
@@ -1351,7 +1359,7 @@ export function rulesLoadFromTree(valueTree, basePath) {
}
/**
* 应用规则增量表
* apply rules delta table
*/
export function applyRulesDeltaToTable(delta) {
if (!delta || typeof delta !== 'object') return;
@@ -1362,7 +1370,7 @@ export function applyRulesDeltaToTable(delta) {
}
/**
* 安装变量 API 补丁
* install variable API patch
*/
function installVariableApiPatch() {
try {
@@ -1449,7 +1457,7 @@ function installVariableApiPatch() {
}
/**
* 卸载变量 API 补丁
* uninstall variable API patch
*/
function uninstallVariableApiPatch() {
try {
@@ -1467,7 +1475,7 @@ function uninstallVariableApiPatch() {
} catch {}
}
/* ============= 快照/回滚 ============= */
/* ============ Snapshots / Rollback ============= */
function getSnapMap() {
const meta = getContext()?.chatMetadata || {};
@@ -1488,7 +1496,7 @@ function setVarDict(dict) {
const current = meta.variables || {};
const next = dict || {};
// 清除不存在的变量
// remove missing variables
for (const k of Object.keys(current)) {
if (!(k in next)) {
try { delete current[k]; } catch {}
@@ -1496,7 +1504,8 @@ function setVarDict(dict) {
}
}
// 设置新值
// note
for (const [k, v] of Object.entries(next)) {
let toStore = v;
if (v && typeof v === 'object') {
@@ -1615,6 +1624,13 @@ function rollbackToPreviousOf(messageId) {
const id = Number(messageId);
if (Number.isNaN(id)) return;
clearStateAppliedFrom(id);
if (typeof globalThis.LWB_StateRollbackHook === 'function') {
Promise.resolve(globalThis.LWB_StateRollbackHook(id)).catch((e) => {
console.error('[variablesCore] LWB_StateRollbackHook failed:', e);
});
}
const prevId = id - 1;
if (prevId < 0) return;
@@ -1641,10 +1657,10 @@ function rebuildVariablesFromScratch() {
} catch {}
}
/* ============= 应用变量到消息 ============= */
/* ============ Apply Variables To Message ============= */
/**
* 将对象模式转换
* switch to object mode
*/
function asObject(rec) {
if (rec.mode !== 'object') {
@@ -1658,7 +1674,7 @@ function asObject(rec) {
}
/**
* 增量操作辅助
* bump helper
*/
function bumpAtPath(rec, path, delta) {
const numDelta = Number(delta);
@@ -1715,7 +1731,7 @@ function bumpAtPath(rec, path, delta) {
}
/**
* 解析标量数组
* parse scalar array
*/
function parseScalarArrayMaybe(str) {
try {
@@ -1727,8 +1743,55 @@ function parseScalarArrayMaybe(str) {
}
/**
* 应用变量到消息
* apply variables for message
*/
function readMessageText(msg) {
if (!msg) return '';
if (typeof msg.mes === 'string') return msg.mes;
if (typeof msg.content === 'string') return msg.content;
if (Array.isArray(msg.content)) {
return msg.content
.filter(p => p?.type === 'text' && typeof p.text === 'string')
.map(p => p.text)
.join('\n');
}
return '';
}
function getVariablesMode() {
try {
return extension_settings?.[EXT_ID]?.variablesMode || '1.0';
} catch {
return '1.0';
}
}
async function applyVarsForMessage(messageId) {
const ctx = getContext();
const msg = ctx?.chat?.[messageId];
if (!msg) return;
const text = readMessageText(msg);
const mode = getVariablesMode();
if (mode === '2.0') {
const result = applyStateForMessage(messageId, text);
if (result.errors?.length) {
console.warn('[variablesCore][2.0] warnings:', result.errors);
}
if (result.atoms?.length) {
$(document).trigger('xiaobaix:variables:stateAtomsGenerated', {
messageId,
atoms: result.atoms
});
}
return;
}
await applyVariablesForMessage(messageId);
}
async function applyVariablesForMessage(messageId) {
try {
const ctx = getContext();
@@ -1739,7 +1802,7 @@ async function applyVariablesForMessage(messageId) {
const preview = (text, max = 220) => {
try {
const s = String(text ?? '').replace(/\s+/g, ' ').trim();
return s.length > max ? s.slice(0, max) + '' : s;
return s.length > max ? s.slice(0, max) + '...' : s;
} catch {
return '';
}
@@ -1779,7 +1842,7 @@ async function applyVariablesForMessage(messageId) {
} catch (e) {
parseErrors++;
if (debugOn) {
try { xbLog.error(MODULE_ID, `plot-log 解析失败:楼层=${messageId} 块#${idx + 1} 预览=${preview(b)}`, e); } catch {}
try { xbLog.error(MODULE_ID, `plot-log è§£æž<EFBFBD>失败:楼å±?${messageId} å<EFBFBD>?${idx + 1} 预览=${preview(b)}`, e); } catch {}
}
return;
}
@@ -1810,7 +1873,7 @@ async function applyVariablesForMessage(messageId) {
try {
xbLog.warn(
MODULE_ID,
`plot-log 未产生可执行指令:楼层=${messageId} 块数=${blocks.length} 解析条目=${parsedPartsTotal} 解析失败=${parseErrors} 预览=${preview(blocks[0])}`
`plot-log 未产生å<EFBFBD>¯æ‰§è¡ŒæŒ‡ä»¤ï¼šæ¥¼å±?${messageId} å<EFBFBD>—æ•°=${blocks.length} è§£æž<EFBFBD>æ<EFBFBD>¡ç®=${parsedPartsTotal} è§£æž<EFBFBD>失败=${parseErrors} 预览=${preview(blocks[0])}`
);
} catch {}
}
@@ -1818,7 +1881,7 @@ async function applyVariablesForMessage(messageId) {
return;
}
// 构建变量记录
// build variable records
const byName = new Map();
for (const { name } of ops) {
@@ -1838,9 +1901,9 @@ async function applyVariablesForMessage(messageId) {
const norm = (p) => String(p || '').replace(/\[(\d+)\]/g, '.$1');
// 执行操作
// execute operations
for (const op of ops) {
// 守护指令
// guard directives
if (op.operation === 'guard') {
for (const entry of op.data) {
const path = typeof entry?.path === 'string' ? entry.path.trim() : '';
@@ -1865,7 +1928,7 @@ async function applyVariablesForMessage(messageId) {
const rec = byName.get(root);
if (!rec) continue;
// SET 操作
// set op
if (op.operation === 'setObject') {
for (const [k, v] of Object.entries(op.data)) {
const localPath = joinPath(subPath, k);
@@ -1903,7 +1966,7 @@ async function applyVariablesForMessage(messageId) {
}
}
// DEL 操作
// delete op
else if (op.operation === 'del') {
const obj = asObject(rec);
const pending = [];
@@ -1951,7 +2014,8 @@ async function applyVariablesForMessage(messageId) {
});
}
// 按索引分组(倒序删除)
// note
const arrGroups = new Map();
const objDeletes = [];
@@ -1977,7 +2041,7 @@ async function applyVariablesForMessage(messageId) {
}
}
// PUSH 操作
// push op
else if (op.operation === 'push') {
for (const [k, vals] of Object.entries(op.data)) {
const localPath = joinPath(subPath, k);
@@ -2033,7 +2097,7 @@ async function applyVariablesForMessage(messageId) {
}
}
// BUMP 操作
// bump op
else if (op.operation === 'bump') {
for (const [k, delta] of Object.entries(op.data)) {
const num = Number(delta);
@@ -2077,7 +2141,7 @@ async function applyVariablesForMessage(messageId) {
}
}
// 检查是否有变化
// check for changes
const hasChanges = Array.from(byName.values()).some(rec => rec?.changed === true);
if (!hasChanges && delVarNames.size === 0) {
if (debugOn) {
@@ -2085,7 +2149,7 @@ async function applyVariablesForMessage(messageId) {
const denied = guardDenied ? `,被规则拦截=${guardDenied}` : '';
xbLog.warn(
MODULE_ID,
`plot-log 指令执行后无变化:楼层=${messageId} 指令数=${ops.length}${denied} 示例=${preview(JSON.stringify(guardDeniedSamples))}`
`plot-log 指令执行å<EFBFBD>Žæ— å<EFBFBD>˜åŒï¼šæ¥¼å±?${messageId} 指令æ•?${ops.length}${denied} 示例=${preview(JSON.stringify(guardDeniedSamples))}`
);
} catch {}
}
@@ -2093,7 +2157,7 @@ async function applyVariablesForMessage(messageId) {
return;
}
// 保存变量
// save variables
for (const [name, rec] of byName.entries()) {
if (!rec.changed) continue;
try {
@@ -2105,7 +2169,7 @@ async function applyVariablesForMessage(messageId) {
} catch {}
}
// 删除变量
// delete variables
if (delVarNames.size > 0) {
try {
for (const v of delVarNames) {
@@ -2124,7 +2188,7 @@ async function applyVariablesForMessage(messageId) {
} catch {}
}
/* ============= 事件处理 ============= */
/* ============ Event Handling ============= */
function getMsgIdLoose(payload) {
if (payload && typeof payload === 'object') {
@@ -2150,56 +2214,57 @@ function bindEvents() {
let lastSwipedId;
suppressUpdatedOnce = new Set();
// 消息发送
// note
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);
await applyVarsForMessage(id);
applyXbGetVarForMessage(id, true);
}
} catch {}
});
// 消息接收
// message received
events?.on(event_types.MESSAGE_RECEIVED, async (data) => {
try {
const id = getMsgIdLoose(data);
if (typeof id === 'number') {
await applyVariablesForMessage(id);
await applyVarsForMessage(id);
applyXbGetVarForMessage(id, true);
await executeQueuedVareventJsAfterTurn();
}
} catch {}
});
// 用户消息渲染
// user message rendered
events?.on(event_types.USER_MESSAGE_RENDERED, async (data) => {
try {
const id = getMsgIdLoose(data);
if (typeof id === 'number') {
await applyVariablesForMessage(id);
await applyVarsForMessage(id);
applyXbGetVarForMessage(id, true);
snapshotForMessageId(id);
}
} catch {}
});
// 角色消息渲染
// character message rendered
events?.on(event_types.CHARACTER_MESSAGE_RENDERED, async (data) => {
try {
const id = getMsgIdLoose(data);
if (typeof id === 'number') {
await applyVariablesForMessage(id);
await applyVarsForMessage(id);
applyXbGetVarForMessage(id, true);
snapshotForMessageId(id);
}
} catch {}
});
// 消息更新
// message updated
events?.on(event_types.MESSAGE_UPDATED, async (data) => {
try {
const id = getMsgIdLoose(data);
@@ -2208,13 +2273,13 @@ function bindEvents() {
suppressUpdatedOnce.delete(id);
return;
}
await applyVariablesForMessage(id);
await applyVarsForMessage(id);
applyXbGetVarForMessage(id, true);
}
} catch {}
});
// 消息编辑
// message edited
events?.on(event_types.MESSAGE_EDITED, async (data) => {
try {
const id = getMsgIdLoose(data);
@@ -2223,7 +2288,7 @@ function bindEvents() {
rollbackToPreviousOf(id);
setTimeout(async () => {
await applyVariablesForMessage(id);
await applyVarsForMessage(id);
applyXbGetVarForMessage(id, true);
try {
@@ -2248,7 +2313,7 @@ function bindEvents() {
} catch {}
});
// 消息滑动
// message swiped
events?.on(event_types.MESSAGE_SWIPED, async (data) => {
try {
const id = getMsgIdLoose(data);
@@ -2259,7 +2324,7 @@ function bindEvents() {
const tId = setTimeout(async () => {
pendingSwipeApply.delete(id);
await applyVariablesForMessage(id);
await applyVarsForMessage(id);
await executeQueuedVareventJsAfterTurn();
}, 10);
@@ -2268,7 +2333,7 @@ function bindEvents() {
} catch {}
});
// 消息删除
// message deleted
events?.on(event_types.MESSAGE_DELETED, (data) => {
try {
const id = getMsgIdStrict(data);
@@ -2280,12 +2345,13 @@ function bindEvents() {
} catch {}
});
// 生成开始
// note
events?.on(event_types.GENERATION_STARTED, (data) => {
try {
snapshotPreviousFloor();
// 取消滑动延迟
// cancel swipe delay
const t = (typeof data === 'string' ? data : (data?.type || '')).toLowerCase();
if (t === 'swipe' && lastSwipedId != null) {
const tId = pendingSwipeApply.get(lastSwipedId);
@@ -2297,7 +2363,7 @@ function bindEvents() {
} catch {}
});
// 聊天切换
// chat changed
events?.on(event_types.CHAT_CHANGED, () => {
try {
rulesClearCache();
@@ -2310,29 +2376,31 @@ function bindEvents() {
});
}
/* ============= 初始化与清理 ============= */
/* ============ Init & Cleanup ============= */
/**
* 初始化模块
* init module
*/
export function initVariablesCore() {
try { xbLog.info('variablesCore', <>˜é‡<C3A9>系统å<C5B8>¯åЍ'); } catch {}
if (initialized) return;
initialized = true;
// 创建事件管理器
// init events
events = createModuleEvents(MODULE_ID);
// 加载规则
// load rules
rulesLoadFromMeta();
// 安装 API 补丁
// install API patch
installVariableApiPatch();
// 绑定事件
// bind events
bindEvents();
// 挂载全局函数(供 var-commands.js 使用)
// note
globalThis.LWB_Guard = {
validate: guardValidate,
loadRules: rulesLoadFromTree,
@@ -2343,47 +2411,47 @@ export function initVariablesCore() {
}
/**
* 清理模块
* cleanup module
*/
export function cleanupVariablesCore() {
try { xbLog.info('variablesCore', <>˜é‡<C3A9>系统清ç<E280A6>†'); } catch {}
if (!initialized) return;
// 清理事件
// cleanup events
events?.cleanup();
events = null;
// 卸载 API 补丁
// uninstall API patch
uninstallVariableApiPatch();
// 清理规则
// clear rules
rulesClearCache();
// 清理全局函数
// clear global hooks
delete globalThis.LWB_Guard;
// 清理守护状态
// clear guard state
guardBypass(false);
initialized = false;
}
/* ============= 导出 ============= */
/* ============ Exports ============= */
export {
MODULE_ID,
// 解析
// parsing
parseBlock,
applyVariablesForMessage,
extractPlotLogBlocks,
// 快照
// snapshots
snapshotCurrentLastFloor,
snapshotForMessageId,
rollbackToPreviousOf,
rebuildVariablesFromScratch,
// 规则
// rules
rulesGetTable,
rulesSetTable,
rulesLoadFromMeta,
rulesSaveToMeta,
};
};