2.0变量 , 向量总结正式推送

This commit is contained in:
RT15548
2026-02-16 00:30:59 +08:00
parent 17b1fe9091
commit cd9fe53f84
75 changed files with 48287 additions and 12186 deletions

View File

@@ -0,0 +1,746 @@
import { getContext } from '../../../../../../extensions.js';
import { getLocalVariable, setLocalVariable } from '../../../../../../variables.js';
import { extractStateBlocks, computeStateSignature, parseStateBlock } from './parser.js';
import { generateSemantic } from './semantic.js';
import { validate, setRule, loadRulesFromMeta, saveRulesToMeta } from './guard.js';
/**
* =========================
* Path / JSON helpers
* =========================
*/
function splitPath(path) {
const s = String(path || '');
const segs = [];
let buf = '';
let i = 0;
while (i < s.length) {
const ch = s[i];
if (ch === '.') {
if (buf) { segs.push(/^\d+$/.test(buf) ? Number(buf) : buf); buf = ''; }
i++;
} else if (ch === '[') {
if (buf) { segs.push(/^\d+$/.test(buf) ? Number(buf) : buf); buf = ''; }
i++;
let val = '';
if (s[i] === '"' || s[i] === "'") {
const q = s[i++];
while (i < s.length && s[i] !== q) val += s[i++];
i++;
} else {
while (i < s.length && s[i] !== ']') val += s[i++];
}
if (s[i] === ']') i++;
segs.push(/^\d+$/.test(val.trim()) ? Number(val.trim()) : val.trim());
} else {
buf += ch;
i++;
}
}
if (buf) segs.push(/^\d+$/.test(buf) ? Number(buf) : buf);
return segs;
}
function normalizePath(path) {
return splitPath(path).map(String).join('.');
}
function safeJSON(v) {
try { return JSON.stringify(v); } catch { return ''; }
}
function safeParse(s) {
if (s == null || s === '') return undefined;
if (typeof s !== 'string') return s;
const t = s.trim();
if (!t) return undefined;
if (t[0] === '{' || t[0] === '[') {
try { return JSON.parse(t); } catch { return s; }
}
if (/^-?\d+(?:\.\d+)?$/.test(t)) return Number(t);
if (t === 'true') return true;
if (t === 'false') return false;
return s;
}
function deepClone(obj) {
try { return structuredClone(obj); } catch {
try { return JSON.parse(JSON.stringify(obj)); } catch { return obj; }
}
}
/**
* =========================
* Variable getters/setters (local vars)
* =========================
*/
function getVar(path) {
const segs = splitPath(path);
if (!segs.length) return undefined;
const rootRaw = getLocalVariable(String(segs[0]));
if (segs.length === 1) return safeParse(rootRaw);
let obj = safeParse(rootRaw);
if (!obj || typeof obj !== 'object') return undefined;
for (let i = 1; i < segs.length; i++) {
obj = obj?.[segs[i]];
if (obj === undefined) return undefined;
}
return obj;
}
function setVar(path, value) {
const segs = splitPath(path);
if (!segs.length) return;
const rootName = String(segs[0]);
if (segs.length === 1) {
const toStore = (value && typeof value === 'object') ? safeJSON(value) : String(value ?? '');
setLocalVariable(rootName, toStore);
return;
}
let root = safeParse(getLocalVariable(rootName));
if (!root || typeof root !== 'object') {
root = typeof segs[1] === 'number' ? [] : {};
}
let cur = root;
for (let i = 1; i < segs.length - 1; i++) {
const key = segs[i];
const nextKey = segs[i + 1];
if (cur[key] == null || typeof cur[key] !== 'object') {
cur[key] = typeof nextKey === 'number' ? [] : {};
}
cur = cur[key];
}
cur[segs[segs.length - 1]] = value;
setLocalVariable(rootName, safeJSON(root));
}
function delVar(path) {
const segs = splitPath(path);
if (!segs.length) return;
const rootName = String(segs[0]);
if (segs.length === 1) {
setLocalVariable(rootName, '');
return;
}
let root = safeParse(getLocalVariable(rootName));
if (!root || typeof root !== 'object') return;
let cur = root;
for (let i = 1; i < segs.length - 1; i++) {
cur = cur?.[segs[i]];
if (!cur || typeof cur !== 'object') return;
}
const lastKey = segs[segs.length - 1];
if (Array.isArray(cur) && typeof lastKey === 'number') {
cur.splice(lastKey, 1);
} else {
delete cur[lastKey];
}
setLocalVariable(rootName, safeJSON(root));
}
function pushVar(path, value) {
const segs = splitPath(path);
if (!segs.length) return { ok: false, reason: 'invalid-path' };
const rootName = String(segs[0]);
if (segs.length === 1) {
let arr = safeParse(getLocalVariable(rootName));
// ✅ 类型检查:必须是数组或不存在
if (arr !== undefined && !Array.isArray(arr)) {
return { ok: false, reason: 'not-array' };
}
if (!Array.isArray(arr)) arr = [];
const items = Array.isArray(value) ? value : [value];
arr.push(...items);
setLocalVariable(rootName, safeJSON(arr));
return { ok: true };
}
let root = safeParse(getLocalVariable(rootName));
if (!root || typeof root !== 'object') {
root = typeof segs[1] === 'number' ? [] : {};
}
let cur = root;
for (let i = 1; i < segs.length - 1; i++) {
const key = segs[i];
const nextKey = segs[i + 1];
if (cur[key] == null || typeof cur[key] !== 'object') {
cur[key] = typeof nextKey === 'number' ? [] : {};
}
cur = cur[key];
}
const lastKey = segs[segs.length - 1];
let arr = cur[lastKey];
// ✅ 类型检查:必须是数组或不存在
if (arr !== undefined && !Array.isArray(arr)) {
return { ok: false, reason: 'not-array' };
}
if (!Array.isArray(arr)) arr = [];
const items = Array.isArray(value) ? value : [value];
arr.push(...items);
cur[lastKey] = arr;
setLocalVariable(rootName, safeJSON(root));
return { ok: true };
}
function popVar(path, value) {
const segs = splitPath(path);
if (!segs.length) return { ok: false, reason: 'invalid-path' };
const rootName = String(segs[0]);
let root = safeParse(getLocalVariable(rootName));
if (segs.length === 1) {
if (!Array.isArray(root)) {
return { ok: false, reason: 'not-array' };
}
const toRemove = Array.isArray(value) ? value : [value];
for (const v of toRemove) {
const vStr = safeJSON(v);
const idx = root.findIndex(x => safeJSON(x) === vStr);
if (idx !== -1) root.splice(idx, 1);
}
setLocalVariable(rootName, safeJSON(root));
return { ok: true };
}
if (!root || typeof root !== 'object') {
return { ok: false, reason: 'not-array' };
}
let cur = root;
for (let i = 1; i < segs.length - 1; i++) {
cur = cur?.[segs[i]];
if (!cur || typeof cur !== 'object') {
return { ok: false, reason: 'path-not-found' };
}
}
const lastKey = segs[segs.length - 1];
let arr = cur[lastKey];
if (!Array.isArray(arr)) {
return { ok: false, reason: 'not-array' };
}
const toRemove = Array.isArray(value) ? value : [value];
for (const v of toRemove) {
const vStr = safeJSON(v);
const idx = arr.findIndex(x => safeJSON(x) === vStr);
if (idx !== -1) arr.splice(idx, 1);
}
setLocalVariable(rootName, safeJSON(root));
return { ok: true };
}
/**
* =========================
* Storage (chat_metadata.extensions.LittleWhiteBox)
* =========================
*/
const EXT_ID = 'LittleWhiteBox';
const ERR_VAR_NAME = 'LWB_STATE_ERRORS';
const LOG_KEY = 'stateLogV2';
const CKPT_KEY = 'stateCkptV2';
/**
* 写入状态错误到本地变量(覆盖写入)
*/
function writeStateErrorsToLocalVar(lines) {
try {
const text = Array.isArray(lines) && lines.length
? lines.map(s => `- ${String(s)}`).join('\n')
: '';
setLocalVariable(ERR_VAR_NAME, text);
} catch {}
}
function getLwbExtMeta() {
const ctx = getContext();
const meta = ctx?.chatMetadata || (ctx.chatMetadata = {});
meta.extensions ||= {};
meta.extensions[EXT_ID] ||= {};
return meta.extensions[EXT_ID];
}
function getStateLog() {
const ext = getLwbExtMeta();
ext[LOG_KEY] ||= { version: 1, floors: {} };
return ext[LOG_KEY];
}
function getCheckpointStore() {
const ext = getLwbExtMeta();
ext[CKPT_KEY] ||= { version: 1, every: 50, points: {} };
return ext[CKPT_KEY];
}
function saveWalRecord(floor, signature, rules, ops) {
const log = getStateLog();
log.floors[String(floor)] = {
signature: String(signature || ''),
rules: Array.isArray(rules) ? deepClone(rules) : [],
ops: Array.isArray(ops) ? deepClone(ops) : [],
ts: Date.now(),
};
getContext()?.saveMetadataDebounced?.();
}
/**
* checkpoint = 执行完 floor 后的全量变量+规则
*/
function saveCheckpointIfNeeded(floor) {
const ckpt = getCheckpointStore();
const every = Number(ckpt.every) || 50;
// floor=0 也可以存,但一般没意义;你可按需调整
if (floor < 0) return;
if (every <= 0) return;
if (floor % every !== 0) return;
const ctx = getContext();
const meta = ctx?.chatMetadata || {};
const vars = deepClone(meta.variables || {});
// 2.0 rules 存在 chatMetadata 里guard.js 写入的位置)
const rules = deepClone(meta.LWB_RULES_V2 || {});
ckpt.points[String(floor)] = { vars, rules, ts: Date.now() };
ctx?.saveMetadataDebounced?.();
}
/**
* =========================
* Applied signature map (idempotent)
* =========================
*/
const LWB_STATE_APPLIED_KEY = 'LWB_STATE_APPLIED_KEY';
function getAppliedMap() {
const meta = getContext()?.chatMetadata || {};
meta[LWB_STATE_APPLIED_KEY] ||= {};
return meta[LWB_STATE_APPLIED_KEY];
}
export function clearStateAppliedFor(floor) {
try {
delete getAppliedMap()[floor];
getContext()?.saveMetadataDebounced?.();
} catch {}
}
export function clearStateAppliedFrom(floorInclusive) {
try {
const map = getAppliedMap();
for (const k of Object.keys(map)) {
if (Number(k) >= floorInclusive) delete map[k];
}
getContext()?.saveMetadataDebounced?.();
} catch {}
}
function isIndexDeleteOp(opItem) {
if (!opItem || opItem.op !== 'del') return false;
const segs = splitPath(opItem.path);
if (!segs.length) return false;
const last = segs[segs.length - 1];
return typeof last === 'number' && Number.isFinite(last);
}
function buildExecOpsWithIndexDeleteReorder(ops) {
// 同一个数组的 index-del按 parentPath 分组,组内 index 倒序
// 其它操作:保持原顺序
const groups = new Map(); // parentPath -> { order, items: [{...opItem, index}] }
const groupOrder = new Map();
let orderCounter = 0;
const normalOps = [];
for (const op of ops) {
if (isIndexDeleteOp(op)) {
const segs = splitPath(op.path);
const idx = segs[segs.length - 1];
const parentPath = segs.slice(0, -1).reduce((acc, s) => {
if (typeof s === 'number') return acc + `[${s}]`;
return acc ? `${acc}.${s}` : String(s);
}, '');
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);
}
// ✅ 我们把“索引删除”放在最前面执行:这样它们永远按“原索引”删
// (避免在同一轮里先删后 push 导致索引变化)
return [...reorderedIndexDeletes, ...normalOps];
}
/**
* =========================
* Core: apply one message text (<state>...) => update vars + rules + wal + checkpoint
* =========================
*/
export function applyStateForMessage(messageId, messageContent) {
const ctx = getContext();
const chatId = ctx?.chatId || '';
loadRulesFromMeta();
const text = String(messageContent ?? '');
const signature = computeStateSignature(text);
const blocks = extractStateBlocks(text);
// ✅ 统一:只要没有可执行 blocks就视为本层 state 被移除
if (!signature || blocks.length === 0) {
clearStateAppliedFor(messageId);
writeStateErrorsToLocalVar([]);
// delete WAL record
try {
const ext = getLwbExtMeta();
const log = ext[LOG_KEY];
if (log?.floors) delete log.floors[String(messageId)];
getContext()?.saveMetadataDebounced?.();
} catch {}
return { atoms: [], errors: [], skipped: false };
}
const appliedMap = getAppliedMap();
if (appliedMap[messageId] === signature) {
return { atoms: [], errors: [], skipped: true };
}
const atoms = [];
const errors = [];
let idx = 0;
const mergedRules = [];
const mergedOps = [];
for (const block of blocks) {
const parsed = parseStateBlock(block);
mergedRules.push(...(parsed?.rules || []));
mergedOps.push(...(parsed?.ops || []));
}
if (blocks.length) {
// ✅ WAL一次写入完整的 rules/ops
saveWalRecord(messageId, signature, mergedRules, mergedOps);
// ✅ rules 一次性注册
let rulesTouched = false;
for (const { path, rule } of mergedRules) {
if (path && rule && Object.keys(rule).length) {
setRule(normalizePath(path), rule);
rulesTouched = true;
}
}
if (rulesTouched) saveRulesToMeta();
const execOps = buildExecOpsWithIndexDeleteReorder(mergedOps);
// 执行操作(用 execOps
for (const opItem of execOps) {
const { path, op, value, delta, warning } = opItem;
if (!path) continue;
if (warning) errors.push(`[${path}] ${warning}`);
const absPath = normalizePath(path);
const oldValue = getVar(path);
const guard = validate(op, absPath, op === 'inc' ? delta : value, oldValue);
if (!guard.allow) {
errors.push(`${path}: ${guard.reason || '\u88ab\u89c4\u5219\u62d2\u7edd'}`);
continue;
}
// 记录修正信息
if (guard.note) {
if (op === 'inc') {
const raw = Number(delta);
const rawTxt = Number.isFinite(raw) ? `${raw >= 0 ? '+' : ''}${raw}` : String(delta ?? '');
errors.push(`${path}: ${rawTxt} ${guard.note}`);
} else {
errors.push(`${path}: ${guard.note}`);
}
}
let execOk = true;
let execReason = '';
try {
switch (op) {
case 'set':
setVar(path, guard.value);
break;
case 'inc':
// guard.value 对 inc 是最终 nextValue
setVar(path, guard.value);
break;
case 'push': {
const result = pushVar(path, guard.value);
if (!result.ok) { execOk = false; execReason = result.reason; }
break;
}
case 'pop': {
const result = popVar(path, guard.value);
if (!result.ok) { execOk = false; execReason = result.reason; }
break;
}
case 'del':
delVar(path);
break;
default:
execOk = false;
execReason = `未知 op=${op}`;
}
} catch (e) {
execOk = false;
execReason = e?.message || String(e);
}
if (!execOk) {
errors.push(`[${path}] 失败: ${execReason}`);
continue;
}
const newValue = getVar(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?.();
// ✅ checkpoint执行完该楼后可选存一次全量
saveCheckpointIfNeeded(messageId);
// Write error list to local variable
writeStateErrorsToLocalVar(errors);
return { atoms, errors, skipped: false };
}
/**
* =========================
* Restore / Replay (for rollback & rebuild)
* =========================
*/
/**
* 恢复到 targetFloor 执行完成后的变量状态(含规则)
* - 使用最近 checkpoint然后 replay WAL
* - 不依赖消息文本 <state>(避免被正则清掉)
*/
export async function restoreStateV2ToFloor(targetFloor) {
const ctx = getContext();
const meta = ctx?.chatMetadata || {};
const floor = Number(targetFloor);
if (!Number.isFinite(floor) || floor < 0) {
// floor < 0 => 清空
meta.variables = {};
meta.LWB_RULES_V2 = {};
ctx?.saveMetadataDebounced?.();
return { ok: true, usedCheckpoint: null };
}
const log = getStateLog();
const ckpt = getCheckpointStore();
const points = ckpt.points || {};
const available = Object.keys(points)
.map(Number)
.filter(n => Number.isFinite(n) && n <= floor)
.sort((a, b) => b - a);
const ck = available.length ? available[0] : null;
// 1) 恢复 checkpoint 或清空基线
if (ck != null) {
const snap = points[String(ck)];
meta.variables = deepClone(snap?.vars || {});
meta.LWB_RULES_V2 = deepClone(snap?.rules || {});
} else {
meta.variables = {};
meta.LWB_RULES_V2 = {};
}
ctx?.saveMetadataDebounced?.();
// 2) 从 meta 载入规则到内存guard.js 的内存表)
loadRulesFromMeta();
let rulesTouchedAny = false;
// 3) replay WAL: (ck+1 .. floor)
const start = ck == null ? 0 : (ck + 1);
for (let f = start; f <= floor; f++) {
const rec = log.floors?.[String(f)];
if (!rec) continue;
// 先应用 rules
const rules = Array.isArray(rec.rules) ? rec.rules : [];
let touched = false;
for (const r of rules) {
const p = r?.path;
const rule = r?.rule;
if (p && rule && typeof rule === 'object') {
setRule(normalizePath(p), rule);
touched = true;
}
}
if (touched) rulesTouchedAny = true;
// 再应用 ops不产出 atoms、不写 wal
const ops = Array.isArray(rec.ops) ? rec.ops : [];
const execOps = buildExecOpsWithIndexDeleteReorder(ops);
for (const opItem of execOps) {
const path = opItem?.path;
const op = opItem?.op;
if (!path || !op) continue;
const absPath = normalizePath(path);
const oldValue = getVar(path);
const payload = (op === 'inc') ? opItem.delta : opItem.value;
const guard = validate(op, absPath, payload, oldValue);
if (!guard.allow) continue;
try {
switch (op) {
case 'set':
setVar(path, guard.value);
break;
case 'inc':
setVar(path, guard.value);
break;
case 'push': {
const result = pushVar(path, guard.value);
if (!result.ok) {/* ignore */}
break;
}
case 'pop': {
const result = popVar(path, guard.value);
if (!result.ok) {/* ignore */}
break;
}
case 'del':
delVar(path);
break;
}
} catch {
// ignore replay errors
}
}
}
if (rulesTouchedAny) {
saveRulesToMeta();
}
// 4) 清理 applied signaturefloor 之后都要重新计算
clearStateAppliedFrom(floor + 1);
ctx?.saveMetadataDebounced?.();
return { ok: true, usedCheckpoint: ck };
}
/**
* 删除 floor >= fromFloor 的 2.0 持久化数据:
* - WAL: stateLogV2.floors
* - checkpoint: stateCkptV2.points
* - applied signature: LWB_STATE_APPLIED_KEY
*
* 用于 MESSAGE_DELETED 等“物理删除消息”场景,避免 WAL/ckpt 无限膨胀。
*/
export async function trimStateV2FromFloor(fromFloor) {
const start = Number(fromFloor);
if (!Number.isFinite(start)) return { ok: false };
const ctx = getContext();
const meta = ctx?.chatMetadata || {};
meta.extensions ||= {};
meta.extensions[EXT_ID] ||= {};
const ext = meta.extensions[EXT_ID];
// 1) WAL
const log = ext[LOG_KEY];
if (log?.floors && typeof log.floors === 'object') {
for (const k of Object.keys(log.floors)) {
const f = Number(k);
if (Number.isFinite(f) && f >= start) {
delete log.floors[k];
}
}
}
// 2) Checkpoints
const ckpt = ext[CKPT_KEY];
if (ckpt?.points && typeof ckpt.points === 'object') {
for (const k of Object.keys(ckpt.points)) {
const f = Number(k);
if (Number.isFinite(f) && f >= start) {
delete ckpt.points[k];
}
}
}
// 3) Applied signaturesfloor>=start 都要重新算)
try {
clearStateAppliedFrom(start);
} catch {}
ctx?.saveMetadataDebounced?.();
return { ok: true };
}

View File

@@ -0,0 +1,249 @@
import { getContext } from '../../../../../../extensions.js';
const LWB_RULES_V2_KEY = 'LWB_RULES_V2';
let rulesTable = {};
export function loadRulesFromMeta() {
try {
const meta = getContext()?.chatMetadata || {};
rulesTable = meta[LWB_RULES_V2_KEY] || {};
} catch {
rulesTable = {};
}
}
export function saveRulesToMeta() {
try {
const meta = getContext()?.chatMetadata || {};
meta[LWB_RULES_V2_KEY] = { ...rulesTable };
getContext()?.saveMetadataDebounced?.();
} catch {}
}
export function getRuleNode(absPath) {
return matchRuleWithWildcard(absPath);
}
export function setRule(path, rule) {
rulesTable[path] = { ...(rulesTable[path] || {}), ...rule };
}
export function clearRule(path) {
delete rulesTable[path];
saveRulesToMeta();
}
export function clearAllRules() {
rulesTable = {};
saveRulesToMeta();
}
export function getParentPath(absPath) {
const parts = String(absPath).split('.').filter(Boolean);
if (parts.length <= 1) return '';
return parts.slice(0, -1).join('.');
}
/**
* 通配符路径匹配
* 例如data.同行者.张三.HP 可以匹配 data.同行者.*.HP
*/
function matchRuleWithWildcard(absPath) {
// 1. 精确匹配
if (rulesTable[absPath]) return rulesTable[absPath];
const segs = String(absPath).split('.').filter(Boolean);
const n = segs.length;
// 2. 尝试各种 * 替换组合(从少到多)
for (let starCount = 1; starCount <= n; starCount++) {
const patterns = generateStarPatterns(segs, starCount);
for (const pattern of patterns) {
if (rulesTable[pattern]) return rulesTable[pattern];
}
}
// 3. 尝试 [*] 匹配(数组元素模板)
for (let i = 0; i < n; i++) {
if (/^\d+$/.test(segs[i])) {
const trySegs = [...segs];
trySegs[i] = '[*]';
const tryPath = trySegs.join('.');
if (rulesTable[tryPath]) return rulesTable[tryPath];
}
}
return null;
}
/**
* 生成恰好有 starCount 个 * 的所有模式
*/
function generateStarPatterns(segs, starCount) {
const n = segs.length;
const results = [];
function backtrack(idx, stars, path) {
if (idx === n) {
if (stars === starCount) results.push(path.join('.'));
return;
}
// 用原值
if (n - idx > starCount - stars) {
backtrack(idx + 1, stars, [...path, segs[idx]]);
}
// 用 *
if (stars < starCount) {
backtrack(idx + 1, stars + 1, [...path, '*']);
}
}
backtrack(0, 0, []);
return results;
}
function getValueType(v) {
if (Array.isArray(v)) return 'array';
if (v === null) return 'null';
return typeof v;
}
/**
* 验证操作
* @returns {{ allow: boolean, value?: any, reason?: string, note?: string }}
*/
export function validate(op, absPath, payload, currentValue) {
const node = getRuleNode(absPath);
const parentPath = getParentPath(absPath);
const parentNode = parentPath ? getRuleNode(parentPath) : null;
const isNewKey = currentValue === undefined;
const lastSeg = String(absPath).split('.').pop() || '';
// ===== 1. $schema 白名单检查 =====
if (parentNode?.allowedKeys && Array.isArray(parentNode.allowedKeys)) {
if (isNewKey && (op === 'set' || op === 'push')) {
if (!parentNode.allowedKeys.includes(lastSeg)) {
return { allow: false, reason: `字段不在结构模板中` };
}
}
if (op === 'del') {
if (parentNode.allowedKeys.includes(lastSeg)) {
return { allow: false, reason: `模板定义的字段不能删除` };
}
}
}
// ===== 2. 父层结构锁定(无 objectExt / 无 allowedKeys / 无 hasWildcard =====
if (parentNode && parentNode.typeLock === 'object') {
if (!parentNode.objectExt && !parentNode.allowedKeys && !parentNode.hasWildcard) {
if (isNewKey && (op === 'set' || op === 'push')) {
return { allow: false, reason: '父层结构已锁定,不允许新增字段' };
}
}
}
// ===== 3. 类型锁定 =====
if (node?.typeLock && op === 'set') {
let finalPayload = payload;
// 宽松:数字字符串 => 数字
if (node.typeLock === 'number' && typeof payload === 'string') {
if (/^-?\d+(?:\.\d+)?$/.test(payload.trim())) {
finalPayload = Number(payload);
}
}
const finalType = getValueType(finalPayload);
if (node.typeLock !== finalType) {
return { allow: false, reason: `类型不匹配,期望 ${node.typeLock},实际 ${finalType}` };
}
payload = finalPayload;
}
// ===== 4. 数组扩展检查 =====
if (op === 'push') {
if (node && node.typeLock === 'array' && !node.arrayGrow) {
return { allow: false, reason: '数组不允许扩展' };
}
}
// ===== 5. $ro 只读 =====
if (node?.ro && (op === 'set' || op === 'inc')) {
return { allow: false, reason: '只读字段' };
}
// ===== 6. set 操作:数值约束 =====
if (op === 'set') {
const num = Number(payload);
// range 限制
if (Number.isFinite(num) && (node?.min !== undefined || node?.max !== undefined)) {
let v = num;
const min = node?.min;
const max = node?.max;
if (min !== undefined) v = Math.max(v, min);
if (max !== undefined) v = Math.min(v, max);
const clamped = v !== num;
return {
allow: true,
value: v,
note: clamped ? `超出范围,已限制到 ${v}` : undefined,
};
}
// enum 枚举(不自动修正,直接拒绝)
if (node?.enum?.length) {
const s = String(payload ?? '');
if (!node.enum.includes(s)) {
return { allow: false, reason: `枚举不匹配,允许:${node.enum.join(' / ')}` };
}
}
return { allow: true, value: payload };
}
// ===== 7. inc 操作step / range 限制 =====
if (op === 'inc') {
const delta = Number(payload);
if (!Number.isFinite(delta)) return { allow: false, reason: 'delta 不是数字' };
const cur = Number(currentValue) || 0;
let d = delta;
const noteParts = [];
// step 限制
if (node?.step !== undefined && node.step >= 0) {
const before = d;
if (d > node.step) d = node.step;
if (d < -node.step) d = -node.step;
if (d !== before) {
noteParts.push(`超出步长限制,已限制到 ${d >= 0 ? '+' : ''}${d}`);
}
}
let next = cur + d;
// range 限制
const beforeClamp = next;
if (node?.min !== undefined) next = Math.max(next, node.min);
if (node?.max !== undefined) next = Math.min(next, node.max);
if (next !== beforeClamp) {
noteParts.push(`超出范围,已限制到 ${next}`);
}
return {
allow: true,
value: next,
note: noteParts.length ? noteParts.join('') : undefined,
};
}
return { allow: true, value: payload };
}

View File

@@ -0,0 +1,21 @@
export {
applyStateForMessage,
clearStateAppliedFor,
clearStateAppliedFrom,
restoreStateV2ToFloor,
trimStateV2FromFloor,
} from './executor.js';
export { parseStateBlock, extractStateBlocks, computeStateSignature, parseInlineValue } from './parser.js';
export { generateSemantic } from './semantic.js';
export {
validate,
setRule,
clearRule,
clearAllRules,
loadRulesFromMeta,
saveRulesToMeta,
getRuleNode,
getParentPath,
} from './guard.js';

View File

@@ -0,0 +1,514 @@
import jsyaml from '../../../libs/js-yaml.mjs';
/**
* Robust <state> block matcher (no regex)
* - Pairs each </state> with the nearest preceding <state ...>
* - Ignores unclosed <state>
*/
function isValidOpenTagAt(s, i) {
if (s[i] !== '<') return false;
const head = s.slice(i, i + 6).toLowerCase();
if (head !== '<state') return false;
const next = s[i + 6] ?? '';
if (next && !(next === '>' || next === '/' || /\s/.test(next))) return false;
return true;
}
function isValidCloseTagAt(s, i) {
if (s[i] !== '<') return false;
if (s[i + 1] !== '/') return false;
const head = s.slice(i, i + 7).toLowerCase();
if (head !== '</state') return false;
let j = i + 7;
while (j < s.length && /\s/.test(s[j])) j++;
return s[j] === '>';
}
function findTagEnd(s, openIndex) {
const end = s.indexOf('>', openIndex);
return end === -1 ? -1 : end;
}
function findStateBlockSpans(text) {
const s = String(text ?? '');
const closes = [];
for (let i = 0; i < s.length; i++) {
if (s[i] !== '<') continue;
if (isValidCloseTagAt(s, i)) closes.push(i);
}
if (!closes.length) return [];
const spans = [];
let searchEnd = s.length;
for (let cIdx = closes.length - 1; cIdx >= 0; cIdx--) {
const closeStart = closes[cIdx];
if (closeStart >= searchEnd) continue;
let closeEnd = closeStart + 7;
while (closeEnd < s.length && s[closeEnd] !== '>') closeEnd++;
if (s[closeEnd] !== '>') continue;
closeEnd += 1;
let openStart = -1;
for (let i = closeStart - 1; i >= 0; i--) {
if (s[i] !== '<') continue;
if (!isValidOpenTagAt(s, i)) continue;
const tagEnd = findTagEnd(s, i);
if (tagEnd === -1) continue;
if (tagEnd >= closeStart) continue;
openStart = i;
break;
}
if (openStart === -1) continue;
const openTagEnd = findTagEnd(s, openStart);
if (openTagEnd === -1) continue;
spans.push({
openStart,
openTagEnd: openTagEnd + 1,
closeStart,
closeEnd,
});
searchEnd = openStart;
}
spans.reverse();
return spans;
}
export function extractStateBlocks(text) {
const s = String(text ?? '');
const spans = findStateBlockSpans(s);
const out = [];
for (const sp of spans) {
const inner = s.slice(sp.openTagEnd, sp.closeStart);
if (inner.trim()) out.push(inner);
}
return out;
}
export function computeStateSignature(text) {
const s = String(text ?? '');
const spans = findStateBlockSpans(s);
if (!spans.length) return '';
const chunks = spans.map(sp => s.slice(sp.openStart, sp.closeEnd).trim());
return chunks.join('\n---\n');
}
/**
* Parse $schema block
*/
function parseSchemaBlock(basePath, schemaLines) {
const rules = [];
const nonEmpty = schemaLines.filter(l => l.trim());
if (!nonEmpty.length) return rules;
const minIndent = Math.min(...nonEmpty.map(l => l.search(/\S/)));
const yamlText = schemaLines
.map(l => (l.trim() ? l.slice(minIndent) : ''))
.join('\n');
let schemaObj;
try {
schemaObj = jsyaml.load(yamlText);
} catch (e) {
console.warn('[parser] $schema YAML parse failed:', e.message);
return rules;
}
if (!schemaObj || typeof schemaObj !== 'object') return rules;
function walk(obj, curPath) {
if (obj === null || obj === undefined) return;
if (Array.isArray(obj)) {
if (obj.length === 0) {
rules.push({
path: curPath,
rule: { typeLock: 'array', arrayGrow: true },
});
} else {
rules.push({
path: curPath,
rule: { typeLock: 'array', arrayGrow: true },
});
walk(obj[0], curPath ? `${curPath}.[*]` : '[*]');
}
return;
}
if (typeof obj !== 'object') {
const t = typeof obj;
if (t === 'string' || t === 'number' || t === 'boolean') {
rules.push({
path: curPath,
rule: { typeLock: t },
});
}
return;
}
const keys = Object.keys(obj);
if (keys.length === 0) {
rules.push({
path: curPath,
rule: { typeLock: 'object', objectExt: true },
});
return;
}
const hasWildcard = keys.includes('*');
if (hasWildcard) {
rules.push({
path: curPath,
rule: { typeLock: 'object', objectExt: true, hasWildcard: true },
});
const wildcardTemplate = obj['*'];
if (wildcardTemplate !== undefined) {
walk(wildcardTemplate, curPath ? `${curPath}.*` : '*');
}
for (const k of keys) {
if (k === '*') continue;
const childPath = curPath ? `${curPath}.${k}` : k;
walk(obj[k], childPath);
}
return;
}
rules.push({
path: curPath,
rule: { typeLock: 'object', allowedKeys: keys },
});
for (const k of keys) {
const childPath = curPath ? `${curPath}.${k}` : k;
walk(obj[k], childPath);
}
}
walk(schemaObj, basePath);
return rules;
}
/**
* Parse rule line ($ro, $range, $step, $enum)
*/
function parseRuleLine(line) {
const tokens = line.trim().split(/\s+/);
const directives = [];
let pathStart = 0;
for (let i = 0; i < tokens.length; i++) {
if (tokens[i].startsWith('$')) {
directives.push(tokens[i]);
pathStart = i + 1;
} else {
break;
}
}
const path = tokens.slice(pathStart).join(' ').trim();
if (!path || !directives.length) return null;
const rule = {};
for (const tok of directives) {
if (tok === '$ro') { rule.ro = true; continue; }
const rangeMatch = tok.match(/^\$range=\[\s*(-?\d+(?:\.\d+)?)\s*,\s*(-?\d+(?:\.\d+)?)\s*\]$/);
if (rangeMatch) {
rule.min = Math.min(Number(rangeMatch[1]), Number(rangeMatch[2]));
rule.max = Math.max(Number(rangeMatch[1]), Number(rangeMatch[2]));
continue;
}
const stepMatch = tok.match(/^\$step=(\d+(?:\.\d+)?)$/);
if (stepMatch) { rule.step = Math.abs(Number(stepMatch[1])); continue; }
const enumMatch = tok.match(/^\$enum=\{([^}]+)\}$/);
if (enumMatch) {
rule.enum = enumMatch[1].split(/[,、;]/).map(s => s.trim()).filter(Boolean);
continue;
}
}
return { path, rule };
}
export function parseStateBlock(content) {
const lines = String(content ?? '').split(/\r?\n/);
const rules = [];
const dataLines = [];
let inSchema = false;
let schemaPath = '';
let schemaLines = [];
let schemaBaseIndent = -1;
const flushSchema = () => {
if (schemaLines.length) {
const parsed = parseSchemaBlock(schemaPath, schemaLines);
rules.push(...parsed);
}
inSchema = false;
schemaPath = '';
schemaLines = [];
schemaBaseIndent = -1;
};
for (let i = 0; i < lines.length; i++) {
const raw = lines[i];
const trimmed = raw.trim();
const indent = raw.search(/\S/);
if (!trimmed || trimmed.startsWith('#')) {
if (inSchema && schemaBaseIndent >= 0) schemaLines.push(raw);
continue;
}
// $schema 开始
if (trimmed.startsWith('$schema')) {
flushSchema();
const rest = trimmed.slice(7).trim();
schemaPath = rest || '';
inSchema = true;
schemaBaseIndent = -1;
continue;
}
if (inSchema) {
if (schemaBaseIndent < 0) {
schemaBaseIndent = indent;
}
// 缩进回退 => schema 结束
if (indent < schemaBaseIndent && indent >= 0 && trimmed) {
flushSchema();
i--;
continue;
}
schemaLines.push(raw);
continue;
}
// 普通 $rule$ro, $range, $step, $enum
if (trimmed.startsWith('$')) {
const parsed = parseRuleLine(trimmed);
if (parsed) rules.push(parsed);
continue;
}
dataLines.push(raw);
}
flushSchema();
const ops = parseDataLines(dataLines);
return { rules, ops };
}
/**
* 解析数据行
*/
function stripYamlInlineComment(s) {
const text = String(s ?? '');
if (!text) return '';
let inSingle = false;
let inDouble = false;
let escaped = false;
for (let i = 0; i < text.length; i++) {
const ch = text[i];
if (inSingle) {
if (ch === "'") {
if (text[i + 1] === "'") { i++; continue; }
inSingle = false;
}
continue;
}
if (inDouble) {
if (escaped) { escaped = false; continue; }
if (ch === '\\') { escaped = true; continue; }
if (ch === '"') inDouble = false;
continue;
}
if (ch === "'") { inSingle = true; continue; }
if (ch === '"') { inDouble = true; continue; }
if (ch === '#') {
const prev = i > 0 ? text[i - 1] : '';
if (i === 0 || /\s/.test(prev)) {
return text.slice(0, i).trimEnd();
}
}
}
return text.trimEnd();
}
function parseDataLines(lines) {
const results = [];
let pendingPath = null;
let pendingLines = [];
const flushPending = () => {
if (!pendingPath) return;
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();
let rhs = trimmed.slice(colonIdx + 1).trim();
rhs = stripYamlInlineComment(rhs);
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' };
const parenNum = t.match(/^\((-?\d+(?:\.\d+)?)\)$/);
if (parenNum) return { op: 'set', value: Number(parenNum[1]) };
if (/^\+\d/.test(t) || /^-\d/.test(t)) {
const n = Number(t);
if (Number.isFinite(n)) return { op: 'inc', delta: n };
}
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 };
} catch {}
return { op: 'set', value: t, warning: '+[] 解析失败' };
}
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 };
} catch {}
return { op: 'set', value: t, warning: '-[] 解析失败' };
}
if (/^-?\d+(?:\.\d+)?$/.test(t)) return { op: 'set', value: Number(t) };
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 };
if (t.startsWith('{') || t.startsWith('[')) {
try { return { op: 'set', value: JSON.parse(t) }; }
catch { return { op: 'set', value: t, warning: 'JSON 解析失败' }; }
}
return { op: 'set', value: t };
}

View File

@@ -0,0 +1,41 @@
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 {
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

@@ -6,6 +6,7 @@
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,
@@ -19,6 +20,8 @@ import {
const MODULE_ID = 'varCommands';
const TAG_RE_XBGETVAR = /\{\{xbgetvar::([^}]+)\}\}/gi;
const TAG_RE_XBGETVAR_YAML = /\{\{xbgetvar_yaml::([^}]+)\}\}/gi;
const TAG_RE_XBGETVAR_YAML_IDX = /\{\{xbgetvar_yaml_idx::([^}]+)\}\}/gi;
let events = null;
let initialized = false;
@@ -94,12 +97,22 @@ function setDeepBySegments(target, segs, value) {
cur[key] = value;
} else {
const nxt = cur[key];
if (typeof nxt === 'object' && nxt && !Array.isArray(nxt)) {
const nextSeg = segs[i + 1];
const wantArray = (typeof nextSeg === 'number');
// 已存在且类型正确:继续深入
if (wantArray && Array.isArray(nxt)) {
cur = nxt;
} else {
cur[key] = {};
cur = cur[key];
continue;
}
if (!wantArray && (nxt && typeof nxt === 'object') && !Array.isArray(nxt)) {
cur = nxt;
continue;
}
// 不存在或类型不匹配:创建正确的容器
cur[key] = wantArray ? [] : {};
cur = cur[key];
}
}
}
@@ -139,6 +152,153 @@ export function replaceXbGetVarInString(s) {
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;
}
});
}
/**
* 将 {{xbgetvar_yaml_idx::路径}} 替换为带索引注释的 YAML
*/
export function replaceXbGetVarYamlIdxInString(s) {
s = String(s ?? '');
if (!s || s.indexOf('{{xbgetvar_yaml_idx::') === -1) return s;
TAG_RE_XBGETVAR_YAML_IDX.lastIndex = 0;
return s.replace(TAG_RE_XBGETVAR_YAML_IDX, (_, p) => {
const value = lwbResolveVarPath(p);
if (!value) return '';
try {
const parsed = JSON.parse(value);
if (typeof parsed === 'object' && parsed !== null) {
return formatYamlWithIndex(parsed, 0).trim();
}
return value;
} catch {
return value;
}
});
}
function formatYamlWithIndex(obj, indent) {
const pad = ' '.repeat(indent);
if (Array.isArray(obj)) {
if (obj.length === 0) return `${pad}[]`;
const lines = [];
obj.forEach((item, idx) => {
if (item && typeof item === 'object' && !Array.isArray(item)) {
const keys = Object.keys(item);
if (keys.length === 0) {
lines.push(`${pad}- {} # [${idx}]`);
} else {
const firstKey = keys[0];
const firstVal = item[firstKey];
const firstFormatted = formatValue(firstVal, indent + 2);
if (typeof firstVal === 'object' && firstVal !== null) {
lines.push(`${pad}- ${firstKey}: # [${idx}]`);
lines.push(firstFormatted);
} else {
lines.push(`${pad}- ${firstKey}: ${firstFormatted} # [${idx}]`);
}
for (let i = 1; i < keys.length; i++) {
const k = keys[i];
const v = item[k];
const vFormatted = formatValue(v, indent + 2);
if (typeof v === 'object' && v !== null) {
lines.push(`${pad} ${k}:`);
lines.push(vFormatted);
} else {
lines.push(`${pad} ${k}: ${vFormatted}`);
}
}
}
} else if (Array.isArray(item)) {
lines.push(`${pad}- # [${idx}]`);
lines.push(formatYamlWithIndex(item, indent + 1));
} else {
lines.push(`${pad}- ${formatScalar(item)} # [${idx}]`);
}
});
return lines.join('\n');
}
if (obj && typeof obj === 'object') {
if (Object.keys(obj).length === 0) return `${pad}{}`;
const lines = [];
for (const [key, val] of Object.entries(obj)) {
const vFormatted = formatValue(val, indent + 1);
if (typeof val === 'object' && val !== null) {
lines.push(`${pad}${key}:`);
lines.push(vFormatted);
} else {
lines.push(`${pad}${key}: ${vFormatted}`);
}
}
return lines.join('\n');
}
return `${pad}${formatScalar(obj)}`;
}
function formatValue(val, indent) {
if (Array.isArray(val)) return formatYamlWithIndex(val, indent);
if (val && typeof val === 'object') return formatYamlWithIndex(val, indent);
return formatScalar(val);
}
function formatScalar(v) {
if (v === null) return 'null';
if (v === undefined) return '';
if (typeof v === 'boolean') return String(v);
if (typeof v === 'number') return String(v);
if (typeof v === 'string') {
const needsQuote =
v === '' ||
/^\s|\s$/.test(v) || // 首尾空格
/[:[]\]{}&*!|>'"%@`#,]/.test(v) || // YAML 易歧义字符
/^(?:true|false|null)$/i.test(v) || // YAML 关键字
/^-?(?:\d+(?:\.\d+)?|\.\d+)$/.test(v); // 纯数字字符串
if (needsQuote) {
return `"${v.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
}
return v;
}
return String(v);
}
export function replaceXbGetVarInChat(chat) {
if (!Array.isArray(chat)) return;
@@ -148,9 +308,15 @@ export function replaceXbGetVarInChat(chat) {
if (!key) continue;
const old = String(msg[key] ?? '');
if (old.indexOf('{{xbgetvar::') === -1) continue;
const hasJson = old.indexOf('{{xbgetvar::') !== -1;
const hasYaml = old.indexOf('{{xbgetvar_yaml::') !== -1;
const hasYamlIdx = old.indexOf('{{xbgetvar_yaml_idx::') !== -1;
if (!hasJson && !hasYaml && !hasYamlIdx) continue;
msg[key] = replaceXbGetVarInString(old);
let result = hasJson ? replaceXbGetVarInString(old) : old;
result = hasYaml ? replaceXbGetVarYamlInString(result) : result;
result = hasYamlIdx ? replaceXbGetVarYamlIdxInString(result) : result;
msg[key] = result;
} catch {}
}
}
@@ -165,9 +331,14 @@ export function applyXbGetVarForMessage(messageId, writeback = true) {
if (!key) return;
const old = String(msg[key] ?? '');
if (old.indexOf('{{xbgetvar::') === -1) return;
const hasJson = old.indexOf('{{xbgetvar::') !== -1;
const hasYaml = old.indexOf('{{xbgetvar_yaml::') !== -1;
const hasYamlIdx = old.indexOf('{{xbgetvar_yaml_idx::') !== -1;
if (!hasJson && !hasYaml && !hasYamlIdx) return;
const out = replaceXbGetVarInString(old);
let out = hasJson ? replaceXbGetVarInString(old) : old;
out = hasYaml ? replaceXbGetVarYamlInString(out) : out;
out = hasYamlIdx ? replaceXbGetVarYamlIdxInString(out) : out;
if (writeback && out !== old) {
msg[key] = out;
}
@@ -616,6 +787,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();
@@ -1004,7 +1231,9 @@ export function cleanupVarCommands() {
initialized = false;
}
/**
* 按值从数组中删除元素2.0 pop 操作)
*/
export {
MODULE_ID,
};

View File

@@ -6,7 +6,7 @@
import { getContext } from "../../../../../extensions.js";
import { getLocalVariable } from "../../../../../variables.js";
import { createModuleEvents } from "../../core/event-manager.js";
import { replaceXbGetVarInString } from "./var-commands.js";
import { replaceXbGetVarInString, replaceXbGetVarYamlInString, replaceXbGetVarYamlIdxInString } from "./var-commands.js";
const MODULE_ID = 'vareventEditor';
const LWB_EXT_ID = 'LittleWhiteBox';
@@ -297,12 +297,18 @@ function installWIHiddenTagStripper() {
}
msg.content = await replaceVareventInString(msg.content, false, false);
}
if (msg.content.indexOf('{{xbgetvar::') !== -1) {
msg.content = replaceXbGetVarInString(msg.content);
}
}
if (Array.isArray(msg?.content)) {
for (const part of msg.content) {
if (msg.content.indexOf('{{xbgetvar::') !== -1) {
msg.content = replaceXbGetVarInString(msg.content);
}
if (msg.content.indexOf('{{xbgetvar_yaml::') !== -1) {
msg.content = replaceXbGetVarYamlInString(msg.content);
}
if (msg.content.indexOf('{{xbgetvar_yaml_idx::') !== -1) {
msg.content = replaceXbGetVarYamlIdxInString(msg.content);
}
}
if (Array.isArray(msg?.content)) {
for (const part of msg.content) {
if (part?.type === 'text' && typeof part.text === 'string') {
if (part.text.includes('<varevent')) {
TAG_RE_VAREVENT.lastIndex = 0;
@@ -312,12 +318,18 @@ function installWIHiddenTagStripper() {
}
part.text = await replaceVareventInString(part.text, false, false);
}
if (part.text.indexOf('{{xbgetvar::') !== -1) {
part.text = replaceXbGetVarInString(part.text);
if (part.text.indexOf('{{xbgetvar::') !== -1) {
part.text = replaceXbGetVarInString(part.text);
}
if (part.text.indexOf('{{xbgetvar_yaml::') !== -1) {
part.text = replaceXbGetVarYamlInString(part.text);
}
if (part.text.indexOf('{{xbgetvar_yaml_idx::') !== -1) {
part.text = replaceXbGetVarYamlIdxInString(part.text);
}
}
}
}
}
}
if (typeof msg?.mes === 'string') {
if (msg.mes.includes('<varevent')) {
TAG_RE_VAREVENT.lastIndex = 0;
@@ -327,12 +339,18 @@ function installWIHiddenTagStripper() {
}
msg.mes = await replaceVareventInString(msg.mes, false, false);
}
if (msg.mes.indexOf('{{xbgetvar::') !== -1) {
msg.mes = replaceXbGetVarInString(msg.mes);
if (msg.mes.indexOf('{{xbgetvar::') !== -1) {
msg.mes = replaceXbGetVarInString(msg.mes);
}
if (msg.mes.indexOf('{{xbgetvar_yaml::') !== -1) {
msg.mes = replaceXbGetVarYamlInString(msg.mes);
}
if (msg.mes.indexOf('{{xbgetvar_yaml_idx::') !== -1) {
msg.mes = replaceXbGetVarYamlIdxInString(msg.mes);
}
}
}
}
}
} catch {}
} catch {}
};
try {
if (eventSource && typeof eventSource.makeLast === 'function') {
@@ -361,6 +379,12 @@ function installWIHiddenTagStripper() {
if (data.prompt.indexOf('{{xbgetvar::') !== -1) {
data.prompt = replaceXbGetVarInString(data.prompt);
}
if (data.prompt.indexOf('{{xbgetvar_yaml::') !== -1) {
data.prompt = replaceXbGetVarYamlInString(data.prompt);
}
if (data.prompt.indexOf('{{xbgetvar_yaml_idx::') !== -1) {
data.prompt = replaceXbGetVarYamlIdxInString(data.prompt);
}
}
} catch {}
});

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 } 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') {
@@ -1618,6 +1627,7 @@ function rollbackToPreviousOf(messageId) {
const prevId = id - 1;
if (prevId < 0) return;
// 1.0: restore from snapshot if available
const snap = getSnapshot(prevId);
if (snap) {
const normalized = normalizeSnapshotRecord(snap);
@@ -1631,20 +1641,60 @@ function rollbackToPreviousOf(messageId) {
}
}
function rebuildVariablesFromScratch() {
async function rollbackToPreviousOfAsync(messageId) {
const id = Number(messageId);
if (Number.isNaN(id)) return;
// Notify L0 rollback hook for floor >= id
if (typeof globalThis.LWB_StateRollbackHook === 'function') {
try {
await globalThis.LWB_StateRollbackHook(id);
} catch (e) {
console.error('[variablesCore] LWB_StateRollbackHook failed:', e);
}
}
const prevId = id - 1;
const mode = getVariablesMode();
if (mode === '2.0') {
try {
const mod = await import('./state2/index.js');
await mod.restoreStateV2ToFloor(prevId); // prevId < 0 handled by implementation
} catch (e) {
console.error('[variablesCore][2.0] restoreStateV2ToFloor failed:', e);
}
return;
}
// mode === '1.0'
rollbackToPreviousOf(id);
}
async function rebuildVariablesFromScratch() {
try {
const mode = getVariablesMode();
if (mode === '2.0') {
const mod = await import('./state2/index.js');
const chat = getContext()?.chat || [];
const lastId = chat.length ? chat.length - 1 : -1;
await mod.restoreStateV2ToFloor(lastId);
return;
}
// 1.0 legacy logic
setVarDict({});
const chat = getContext()?.chat || [];
for (let i = 0; i < chat.length; i++) {
applyVariablesForMessage(i);
await applyVariablesForMessage(i);
}
} catch {}
}
/* ============= 应用变量到消息 ============= */
/* ============ Apply Variables To Message ============= */
/**
* 将对象模式转换
* switch to object mode
*/
function asObject(rec) {
if (rec.mode !== 'object') {
@@ -1658,7 +1708,7 @@ function asObject(rec) {
}
/**
* 增量操作辅助
* bump helper
*/
function bumpAtPath(rec, path, delta) {
const numDelta = Number(delta);
@@ -1715,7 +1765,7 @@ function bumpAtPath(rec, path, delta) {
}
/**
* 解析标量数组
* parse scalar array
*/
function parseScalarArrayMaybe(str) {
try {
@@ -1727,8 +1777,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 +1836,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 +1876,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 解析失败:楼层${messageId}${idx + 1} 预览=${preview(b)}`, e); } catch {}
}
return;
}
@@ -1810,7 +1907,7 @@ async function applyVariablesForMessage(messageId) {
try {
xbLog.warn(
MODULE_ID,
`plot-log 未产生可执行指令:楼层=${messageId} 块数=${blocks.length} 解析条目=${parsedPartsTotal} 解析失败=${parseErrors} 预览=${preview(blocks[0])}`
`plot-log 未产生可执行指令:楼层${messageId} 块数=${blocks.length} 解析条目=${parsedPartsTotal} 解析失败=${parseErrors} 预览=${preview(blocks[0])}`
);
} catch {}
}
@@ -1818,7 +1915,7 @@ async function applyVariablesForMessage(messageId) {
return;
}
// 构建变量记录
// build variable records
const byName = new Map();
for (const { name } of ops) {
@@ -1838,9 +1935,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 +1962,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 +2000,7 @@ async function applyVariablesForMessage(messageId) {
}
}
// DEL 操作
// delete op
else if (op.operation === 'del') {
const obj = asObject(rec);
const pending = [];
@@ -1951,7 +2048,8 @@ async function applyVariablesForMessage(messageId) {
});
}
// 按索引分组(倒序删除)
// note
const arrGroups = new Map();
const objDeletes = [];
@@ -1977,7 +2075,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 +2131,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 +2175,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 +2183,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 指令执行后无变化:楼层${messageId} 指令数${ops.length}${denied} 示例=${preview(JSON.stringify(guardDeniedSamples))}`
);
} catch {}
}
@@ -2093,7 +2191,7 @@ async function applyVariablesForMessage(messageId) {
return;
}
// 保存变量
// save variables
for (const [name, rec] of byName.entries()) {
if (!rec.changed) continue;
try {
@@ -2105,7 +2203,7 @@ async function applyVariablesForMessage(messageId) {
} catch {}
}
// 删除变量
// delete variables
if (delVarNames.size > 0) {
try {
for (const v of delVarNames) {
@@ -2124,7 +2222,7 @@ async function applyVariablesForMessage(messageId) {
} catch {}
}
/* ============= 事件处理 ============= */
/* ============ Event Handling ============= */
function getMsgIdLoose(payload) {
if (payload && typeof payload === 'object') {
@@ -2150,56 +2248,57 @@ function bindEvents() {
let lastSwipedId;
suppressUpdatedOnce = new Set();
// 消息发送
// note
events?.on(event_types.MESSAGE_SENT, async () => {
try {
snapshotCurrentLastFloor();
if (getVariablesMode() !== '2.0') 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);
if (getVariablesMode() !== '2.0') 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);
if (getVariablesMode() !== '2.0') snapshotForMessageId(id);
}
} catch {}
});
// 消息更新
// message updated
events?.on(event_types.MESSAGE_UPDATED, async (data) => {
try {
const id = getMsgIdLoose(data);
@@ -2208,84 +2307,103 @@ 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);
if (typeof id === 'number') {
clearAppliedFor(id);
rollbackToPreviousOf(id);
if (typeof id !== 'number') return;
setTimeout(async () => {
await applyVariablesForMessage(id);
applyXbGetVarForMessage(id, true);
if (getVariablesMode() !== '2.0') clearAppliedFor(id);
try {
const ctx = getContext();
const msg = ctx?.chat?.[id];
if (msg) updateMessageBlock(id, msg, { rerenderMessage: true });
} catch {}
// Roll back first so re-apply uses the edited message
await rollbackToPreviousOfAsync(id);
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 {}
setTimeout(async () => {
await applyVarsForMessage(id);
applyXbGetVarForMessage(id, true);
await executeQueuedVareventJsAfterTurn();
}, 10);
}
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 {}
});
// 消息滑动
// message swiped
events?.on(event_types.MESSAGE_SWIPED, async (data) => {
try {
const id = getMsgIdLoose(data);
if (typeof id === 'number') {
lastSwipedId = id;
clearAppliedFor(id);
rollbackToPreviousOf(id);
if (typeof id !== 'number') return;
const tId = setTimeout(async () => {
pendingSwipeApply.delete(id);
await applyVariablesForMessage(id);
await executeQueuedVareventJsAfterTurn();
}, 10);
lastSwipedId = id;
if (getVariablesMode() !== '2.0') clearAppliedFor(id);
pendingSwipeApply.set(id, tId);
}
// Roll back first so swipe applies cleanly
await rollbackToPreviousOfAsync(id);
const tId = setTimeout(async () => {
pendingSwipeApply.delete(id);
await applyVarsForMessage(id);
await executeQueuedVareventJsAfterTurn();
}, 10);
pendingSwipeApply.set(id, tId);
} catch {}
});
// 消息删除
events?.on(event_types.MESSAGE_DELETED, (data) => {
// message deleted
events?.on(event_types.MESSAGE_DELETED, async (data) => {
try {
const id = getMsgIdStrict(data);
if (typeof id === 'number') {
rollbackToPreviousOf(id);
if (typeof id !== 'number') return;
// Roll back first before delete handling
await rollbackToPreviousOfAsync(id);
// 2.0: physical delete -> trim WAL/ckpt to avoid bloat
if (getVariablesMode() === '2.0') {
try {
const mod = await import('./state2/index.js');
await mod.trimStateV2FromFloor(id);
} catch (e) {
console.error('[variablesCore][2.0] trimStateV2FromFloor failed:', e);
}
}
if (getVariablesMode() !== '2.0') {
clearSnapshotsFrom(id);
clearAppliedFrom(id);
}
} catch {}
});
// 生成开始
// note
events?.on(event_types.GENERATION_STARTED, (data) => {
try {
snapshotPreviousFloor();
if (getVariablesMode() !== '2.0') snapshotPreviousFloor();
// 取消滑动延迟
// cancel swipe delay
const t = (typeof data === 'string' ? data : (data?.type || '')).toLowerCase();
if (t === 'swipe' && lastSwipedId != null) {
const tId = pendingSwipeApply.get(lastSwipedId);
@@ -2297,8 +2415,8 @@ function bindEvents() {
} catch {}
});
// 聊天切换
events?.on(event_types.CHAT_CHANGED, () => {
// chat changed
events?.on(event_types.CHAT_CHANGED, async () => {
try {
rulesClearCache();
rulesLoadFromMeta();
@@ -2306,33 +2424,42 @@ function bindEvents() {
const meta = getContext()?.chatMetadata || {};
meta[LWB_PLOT_APPLIED_KEY] = {};
getContext()?.saveMetadataDebounced?.();
if (getVariablesMode() === '2.0') {
try {
const mod = await import('./state2/index.js');
mod.clearStateAppliedFrom(0);
} catch {}
}
} catch {}
});
}
/* ============= 初始化与清理 ============= */
/* ============ Init & Cleanup ============= */
/**
* 初始化模块
* init module
*/
export function initVariablesCore() {
try { xbLog.info('variablesCore', '变量系统启动'); } 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,
@@ -2340,48 +2467,76 @@ export function initVariablesCore() {
applyDeltaTable: applyRulesDeltaToTable,
save: rulesSaveToMeta,
};
globalThis.LWB_StateV2 = {
/**
* @param {string} text - 包含 <state>...</state> 的文本
* @param {{ floor?: number, silent?: boolean }} [options]
* - floor: 指定写入/记录用楼层(默认:最后一楼)
* - silent: true 时不触发 stateAtomsGenerated初始化用
*/
applyText: async (text, options = {}) => {
const { applyStateForMessage } = await import('./state2/index.js');
const ctx = getContext();
const floor =
Number.isFinite(options.floor)
? Number(options.floor)
: Math.max(0, (ctx?.chat?.length || 1) - 1);
const result = applyStateForMessage(floor, String(text || ''));
// ✅ 默认会触发(当作事件)
// ✅ 初始化时 silent=true不触发当作基线写入
if (!options.silent && result?.atoms?.length) {
$(document).trigger('xiaobaix:variables:stateAtomsGenerated', {
messageId: floor,
atoms: result.atoms,
});
}
return result;
},
};
}
/**
* 清理模块
* cleanup module
*/
export function cleanupVariablesCore() {
try { xbLog.info('variablesCore', '变量系统清理'); } 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;
delete globalThis.LWB_StateV2;
// 清理守护状态
// clear guard state
guardBypass(false);
initialized = false;
}
/* ============= 导出 ============= */
/* ============ Exports ============= */
export {
MODULE_ID,
// 解析
// parsing
parseBlock,
applyVariablesForMessage,
extractPlotLogBlocks,
// 快照
// snapshots
snapshotCurrentLastFloor,
snapshotForMessageId,
rollbackToPreviousOf,
rebuildVariablesFromScratch,
// 规则
// rules
rulesGetTable,
rulesSetTable,
rulesLoadFromMeta,

View File

@@ -117,38 +117,64 @@ const VT = {
global: { getter: getGlobalVariable, setter: setGlobalVariable, storage: ()=> extension_settings.variables?.global || ((extension_settings.variables = { global: {} }).global), save: saveSettingsDebounced },
};
const LWB_RULES_KEY='LWB_RULES';
const getRulesTable = () => { try { return getContext()?.chatMetadata?.[LWB_RULES_KEY] || {}; } catch { return {}; } };
const EXT_ID = 'LittleWhiteBox';
const LWB_RULES_V1_KEY = 'LWB_RULES';
const LWB_RULES_V2_KEY = 'LWB_RULES_V2';
const getRulesTable = () => {
try {
const ctx = getContext();
const mode = extension_settings?.[EXT_ID]?.variablesMode || '1.0';
const meta = ctx?.chatMetadata || {};
return mode === '2.0'
? (meta[LWB_RULES_V2_KEY] || {})
: (meta[LWB_RULES_V1_KEY] || {});
} catch {
return {};
}
};
const pathKey = (arr)=>{ try { return (arr||[]).map(String).join('.'); } catch { return ''; } };
const getRuleNodeByPath = (arr)=> (pathKey(arr) ? (getRulesTable()||{})[pathKey(arr)] : undefined);
const hasAnyRule = (n)=>{
if(!n) return false;
if(n.ro) return true;
if(n.objectPolicy && n.objectPolicy!=='none') return true;
if(n.arrayPolicy && n.arrayPolicy!=='lock') return true;
const c=n.constraints||{};
return ('min'in c)||('max'in c)||('step'in c)||(Array.isArray(c.enum)&&c.enum.length)||(c.regex&&c.regex.source);
const hasAnyRule = (n) => {
if (!n) return false;
if (n.ro) return true;
if (n.lock) return true;
if (n.min !== undefined || n.max !== undefined) return true;
if (n.step !== undefined) return true;
if (Array.isArray(n.enum) && n.enum.length) return true;
return false;
};
const ruleTip = (n)=>{
if(!n) return '';
const lines=[], c=n.constraints||{};
if(n.ro) lines.push('只读:$ro');
if(n.objectPolicy){ const m={none:'(默认:不可增删键)',ext:'$ext可增键',prune:'$prune可删键',free:'$free可增删键'}; lines.push(`对象策略:${m[n.objectPolicy]||n.objectPolicy}`); }
if(n.arrayPolicy){ const m={lock:'(默认:不可增删项)',grow:'$grow可增项',shrink:'$shrink可删项',list:'$list可增删项'}; lines.push(`数组策略:${m[n.arrayPolicy]||n.arrayPolicy}`); }
if('min'in c||'max'in c){ if('min'in c&&'max'in c) lines.push(`范围:$range=[${c.min},${c.max}]`); else if('min'in c) lines.push(`下限:$min=${c.min}`); else lines.push(`上限:$max=${c.max}`); }
if('step'in c) lines.push(`步长:$step=${c.step}`);
if(Array.isArray(c.enum)&&c.enum.length) lines.push(`枚举:$enum={${c.enum.join(';')}}`);
if(c.regex&&c.regex.source) lines.push(`正则:$match=/${c.regex.source}/${c.regex.flags||''}`);
const ruleTip = (n) => {
if (!n) return '';
const lines = [];
if (n.ro) lines.push('只读:$ro');
if (n.lock) lines.push('结构锁:$lock禁止增删该层 key/项)');
if (n.min !== undefined || n.max !== undefined) {
const a = n.min !== undefined ? n.min : '-∞';
const b = n.max !== undefined ? n.max : '+∞';
lines.push(`范围:$range=[${a},${b}]`);
}
if (n.step !== undefined) lines.push(`步长:$step=${n.step}`);
if (Array.isArray(n.enum) && n.enum.length) lines.push(`枚举:$enum={${n.enum.join(';')}}`);
return lines.join('\n');
};
const badgesHtml = (n)=>{
if(!hasAnyRule(n)) return '';
const tip=ruleTip(n).replace(/"/g,'&quot;'), out=[];
if(n.ro) out.push(`<span class="vm-badge" data-type="ro" title="${tip}"><i class="fa-solid fa-shield-halved"></i></span>`);
if((n.objectPolicy&&n.objectPolicy!=='none')||(n.arrayPolicy&&n.arrayPolicy!=='lock')) out.push(`<span class="vm-badge" data-type="struct" title="${tip}"><i class="fa-solid fa-diagram-project"></i></span>`);
const c=n.constraints||{}; if(('min'in c)||('max'in c)||('step'in c)||(Array.isArray(c.enum)&&c.enum.length)||(c.regex&&c.regex.source)) out.push(`<span class="vm-badge" data-type="cons" title="${tip}"><i class="fa-solid fa-ruler-vertical"></i></span>`);
return out.length?`<span class="vm-badges">${out.join('')}</span>`:'';
const badgesHtml = (n) => {
if (!hasAnyRule(n)) return '';
const tip = ruleTip(n).replace(/"/g,'&quot;');
const out = [];
if (n.ro) out.push(`<span class="vm-badge" data-type="ro" title="${tip}"><i class="fa-solid fa-shield-halved"></i></span>`);
if (n.lock) out.push(`<span class="vm-badge" data-type="struct" title="${tip}"><i class="fa-solid fa-diagram-project"></i></span>`);
if ((n.min !== undefined || n.max !== undefined) || (n.step !== undefined) || (Array.isArray(n.enum) && n.enum.length)) {
out.push(`<span class="vm-badge" data-type="cons" title="${tip}"><i class="fa-solid fa-ruler-vertical"></i></span>`);
}
return out.length ? `<span class="vm-badges">${out.join('')}</span>` : '';
};
const debounce=(fn,ms=200)=>{let t;return(...a)=>{clearTimeout(t);t=setTimeout(()=>fn(...a),ms);}};
class VariablesPanel {