Update variables v2 state handling
This commit is contained in:
@@ -1,22 +1,329 @@
|
|||||||
import { getContext } from '../../../../../../extensions.js';
|
import { getContext } from '../../../../../../extensions.js';
|
||||||
import {
|
import { getLocalVariable, setLocalVariable } from '../../../../../../variables.js';
|
||||||
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 { extractStateBlocks, computeStateSignature, parseStateBlock } from './parser.js';
|
||||||
import { generateSemantic } from './semantic.js';
|
import { generateSemantic } from './semantic.js';
|
||||||
|
import { validate, setRule, loadRulesFromMeta, saveRulesToMeta } from './guard.js';
|
||||||
const LWB_STATE_APPLIED_KEY = 'LWB_STATE_APPLIED_KEY';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* chatMetadata 内记录每楼层 signature,防止重复执行
|
* =========================
|
||||||
|
* 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 LOG_KEY = 'stateLogV2';
|
||||||
|
const CKPT_KEY = 'stateCkptV2';
|
||||||
|
|
||||||
|
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() {
|
function getAppliedMap() {
|
||||||
const meta = getContext()?.chatMetadata || {};
|
const meta = getContext()?.chatMetadata || {};
|
||||||
meta[LWB_STATE_APPLIED_KEY] ||= {};
|
meta[LWB_STATE_APPLIED_KEY] ||= {};
|
||||||
@@ -25,8 +332,7 @@ function getAppliedMap() {
|
|||||||
|
|
||||||
export function clearStateAppliedFor(floor) {
|
export function clearStateAppliedFor(floor) {
|
||||||
try {
|
try {
|
||||||
const map = getAppliedMap();
|
delete getAppliedMap()[floor];
|
||||||
delete map[floor];
|
|
||||||
getContext()?.saveMetadataDebounced?.();
|
getContext()?.saveMetadataDebounced?.();
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
@@ -35,44 +341,24 @@ export function clearStateAppliedFrom(floorInclusive) {
|
|||||||
try {
|
try {
|
||||||
const map = getAppliedMap();
|
const map = getAppliedMap();
|
||||||
for (const k of Object.keys(map)) {
|
for (const k of Object.keys(map)) {
|
||||||
const id = Number(k);
|
if (Number(k) >= floorInclusive) delete map[k];
|
||||||
if (!Number.isNaN(id) && id >= floorInclusive) delete map[k];
|
|
||||||
}
|
}
|
||||||
getContext()?.saveMetadataDebounced?.();
|
getContext()?.saveMetadataDebounced?.();
|
||||||
} catch {}
|
} 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) {
|
function isIndexDeleteOp(opItem) {
|
||||||
if (!opItem || opItem.op !== 'del') return false;
|
if (!opItem || opItem.op !== 'del') return false;
|
||||||
const segs = lwbSplitPathWithBrackets(opItem.path);
|
const segs = splitPath(opItem.path);
|
||||||
if (!segs.length) return false;
|
if (!segs.length) return false;
|
||||||
const last = segs[segs.length - 1];
|
const last = segs[segs.length - 1];
|
||||||
return typeof last === 'number' && Number.isFinite(last);
|
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) {
|
function buildExecOpsWithIndexDeleteReorder(ops) {
|
||||||
const groups = new Map(); // parentPath -> [{ op, idx }]
|
// 同一个数组的 index-del:按 parentPath 分组,组内 index 倒序
|
||||||
|
// 其它操作:保持原顺序
|
||||||
|
const groups = new Map(); // parentPath -> { order, items: [{...opItem, index}] }
|
||||||
const groupOrder = new Map();
|
const groupOrder = new Map();
|
||||||
let orderCounter = 0;
|
let orderCounter = 0;
|
||||||
|
|
||||||
@@ -80,9 +366,12 @@ function buildExecOpsWithIndexDeleteReorder(ops) {
|
|||||||
|
|
||||||
for (const op of ops) {
|
for (const op of ops) {
|
||||||
if (isIndexDeleteOp(op)) {
|
if (isIndexDeleteOp(op)) {
|
||||||
const segs = lwbSplitPathWithBrackets(op.path);
|
const segs = splitPath(op.path);
|
||||||
const idx = segs[segs.length - 1];
|
const idx = segs[segs.length - 1];
|
||||||
const parentPath = buildParentPathFromSegs(segs.slice(0, -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)) {
|
if (!groups.has(parentPath)) {
|
||||||
groups.set(parentPath, []);
|
groups.set(parentPath, []);
|
||||||
@@ -94,85 +383,137 @@ function buildExecOpsWithIndexDeleteReorder(ops) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const orderedParents = Array.from(groups.keys()).sort(
|
// 按“该数组第一次出现的顺序”输出各组(可预测)
|
||||||
(a, b) => (groupOrder.get(a) ?? 0) - (groupOrder.get(b) ?? 0)
|
const orderedParents = Array.from(groups.keys()).sort((a, b) => (groupOrder.get(a) ?? 0) - (groupOrder.get(b) ?? 0));
|
||||||
);
|
|
||||||
|
|
||||||
const reorderedIndexDeletes = [];
|
const reorderedIndexDeletes = [];
|
||||||
for (const parent of orderedParents) {
|
for (const parent of orderedParents) {
|
||||||
const items = groups.get(parent) || [];
|
const items = groups.get(parent) || [];
|
||||||
|
// 关键:倒序
|
||||||
items.sort((a, b) => b.idx - a.idx);
|
items.sort((a, b) => b.idx - a.idx);
|
||||||
for (const it of items) reorderedIndexDeletes.push(it.op);
|
for (const it of items) reorderedIndexDeletes.push(it.op);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ✅ 我们把“索引删除”放在最前面执行:这样它们永远按“原索引”删
|
||||||
|
// (避免在同一轮里先删后 push 导致索引变化)
|
||||||
return [...reorderedIndexDeletes, ...normalOps];
|
return [...reorderedIndexDeletes, ...normalOps];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 变量 2.0:执行单条消息里的 <state>,返回 atoms
|
* =========================
|
||||||
|
* Core: apply one message text (<state>...) => update vars + rules + wal + checkpoint
|
||||||
|
* =========================
|
||||||
*/
|
*/
|
||||||
export function applyStateForMessage(messageId, messageContent) {
|
export function applyStateForMessage(messageId, messageContent) {
|
||||||
const ctx = getContext();
|
const ctx = getContext();
|
||||||
const chatId = ctx?.chatId || '';
|
const chatId = ctx?.chatId || '';
|
||||||
|
|
||||||
|
loadRulesFromMeta();
|
||||||
|
|
||||||
const text = String(messageContent ?? '');
|
const text = String(messageContent ?? '');
|
||||||
const signature = computeStateSignature(text);
|
const signature = computeStateSignature(text);
|
||||||
|
const blocks = extractStateBlocks(text);
|
||||||
// 没有 state:清理旧 signature(避免“删掉 state 后仍然认为执行过”)
|
// ✅ 统一:只要没有可执行 blocks,就视为本层 state 被移除
|
||||||
if (!signature) {
|
if (!signature || blocks.length === 0) {
|
||||||
clearStateAppliedFor(messageId);
|
clearStateAppliedFor(messageId);
|
||||||
|
// 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 };
|
return { atoms: [], errors: [], skipped: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
// 幂等:signature 没变就跳过
|
|
||||||
const appliedMap = getAppliedMap();
|
const appliedMap = getAppliedMap();
|
||||||
if (appliedMap[messageId] === signature) {
|
if (appliedMap[messageId] === signature) {
|
||||||
return { atoms: [], errors: [], skipped: true };
|
return { atoms: [], errors: [], skipped: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
const blocks = extractStateBlocks(text);
|
|
||||||
const atoms = [];
|
const atoms = [];
|
||||||
const errors = [];
|
const errors = [];
|
||||||
|
|
||||||
let idx = 0;
|
let idx = 0;
|
||||||
|
|
||||||
|
const mergedRules = [];
|
||||||
|
const mergedOps = [];
|
||||||
|
|
||||||
for (const block of blocks) {
|
for (const block of blocks) {
|
||||||
const ops = parseStateBlock(block);
|
const parsed = parseStateBlock(block);
|
||||||
const execOps = buildExecOpsWithIndexDeleteReorder(ops);
|
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) {
|
for (const opItem of execOps) {
|
||||||
const { path, op, value, delta, warning } = opItem;
|
const { path, op, value, delta, warning } = opItem;
|
||||||
if (!path) continue;
|
if (!path) continue;
|
||||||
if (warning) errors.push(`[${path}] ${warning}`);
|
if (warning) errors.push(`[${path}] ${warning}`);
|
||||||
|
|
||||||
const oldValue = safeParseAny(lwbResolveVarPath(path));
|
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}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let execOk = true;
|
||||||
|
let execReason = '';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
switch (op) {
|
switch (op) {
|
||||||
case 'set':
|
case 'set':
|
||||||
lwbAssignVarPath(path, value);
|
setVar(path, guard.value);
|
||||||
break;
|
break;
|
||||||
case 'inc':
|
case 'inc':
|
||||||
lwbAddVarPath(path, delta);
|
// guard.value 对 inc 是最终 nextValue
|
||||||
|
setVar(path, guard.value);
|
||||||
break;
|
break;
|
||||||
case 'push':
|
case 'push': {
|
||||||
lwbPushVarPath(path, value);
|
const result = pushVar(path, guard.value);
|
||||||
|
if (!result.ok) { execOk = false; execReason = result.reason; }
|
||||||
break;
|
break;
|
||||||
case 'pop':
|
}
|
||||||
lwbRemoveArrayItemByValue(path, value);
|
case 'pop': {
|
||||||
|
const result = popVar(path, guard.value);
|
||||||
|
if (!result.ok) { execOk = false; execReason = result.reason; }
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
case 'del':
|
case 'del':
|
||||||
lwbDeleteVarPath(path);
|
delVar(path);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
errors.push(`[${path}] 未知 op=${op}`);
|
execOk = false;
|
||||||
continue;
|
execReason = `未知 op=${op}`;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
errors.push(`[${path}] 执行失败: ${e?.message || e}`);
|
execOk = false;
|
||||||
|
execReason = e?.message || String(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!execOk) {
|
||||||
|
errors.push(`[${path}] 失败: ${execReason}`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const newValue = safeParseAny(lwbResolveVarPath(path));
|
const newValue = getVar(path);
|
||||||
|
|
||||||
atoms.push({
|
atoms.push({
|
||||||
atomId: `sa-${messageId}-${idx}`,
|
atomId: `sa-${messageId}-${idx}`,
|
||||||
@@ -195,5 +536,182 @@ export function applyStateForMessage(messageId, messageContent) {
|
|||||||
appliedMap[messageId] = signature;
|
appliedMap[messageId] = signature;
|
||||||
getContext()?.saveMetadataDebounced?.();
|
getContext()?.saveMetadataDebounced?.();
|
||||||
|
|
||||||
|
// ✅ checkpoint:执行完该楼后,可选存一次全量
|
||||||
|
saveCheckpointIfNeeded(messageId);
|
||||||
|
|
||||||
return { atoms, errors, skipped: false };
|
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 signature:floor 之后都要重新计算
|
||||||
|
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 signatures(floor>=start 都要重新算)
|
||||||
|
try {
|
||||||
|
clearStateAppliedFrom(start);
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
ctx?.saveMetadataDebounced?.();
|
||||||
|
return { ok: true };
|
||||||
|
}
|
||||||
|
|||||||
123
modules/variables/state2/guard.js
Normal file
123
modules/variables/state2/guard.js
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
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(path) {
|
||||||
|
return rulesTable[path] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
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('.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证操作
|
||||||
|
* @param {string} op - set/inc/push/pop/del
|
||||||
|
* @param {string} absPath - 规范化路径
|
||||||
|
* @param {*} payload - set的值 / inc的delta / push的值 / pop的值
|
||||||
|
* @param {*} currentValue - 当前值
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
|
||||||
|
// 父层 $lock:不允许新增/删除 key
|
||||||
|
if (parentNode?.lock) {
|
||||||
|
if (isNewKey && (op === 'set' || op === 'push')) {
|
||||||
|
return { allow: false, reason: 'parent-locked' };
|
||||||
|
}
|
||||||
|
if (op === 'del') {
|
||||||
|
return { allow: false, reason: 'parent-locked' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 当前 $ro:不允许改值
|
||||||
|
if (node?.ro && (op === 'set' || op === 'inc')) {
|
||||||
|
return { allow: false, reason: 'ro' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 当前 $lock:不允许改结构
|
||||||
|
if (node?.lock && (op === 'push' || op === 'pop' || op === 'del')) {
|
||||||
|
return { allow: false, reason: 'lock' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// set 数值约束
|
||||||
|
if (op === 'set') {
|
||||||
|
const num = Number(payload);
|
||||||
|
if (Number.isFinite(num) && (node?.min !== undefined || node?.max !== undefined)) {
|
||||||
|
let v = num;
|
||||||
|
if (node.min !== undefined) v = Math.max(v, node.min);
|
||||||
|
if (node.max !== undefined) v = Math.min(v, node.max);
|
||||||
|
return { allow: true, value: v };
|
||||||
|
}
|
||||||
|
// enum
|
||||||
|
if (node?.enum?.length) {
|
||||||
|
if (!node.enum.includes(String(payload ?? ''))) {
|
||||||
|
return { allow: false, reason: 'enum' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { allow: true, value: payload };
|
||||||
|
}
|
||||||
|
|
||||||
|
// inc:返回最终值
|
||||||
|
if (op === 'inc') {
|
||||||
|
const delta = Number(payload);
|
||||||
|
if (!Number.isFinite(delta)) return { allow: false, reason: 'delta-nan' };
|
||||||
|
|
||||||
|
const cur = Number(currentValue) || 0;
|
||||||
|
let d = delta;
|
||||||
|
|
||||||
|
// step 限制
|
||||||
|
if (node?.step !== undefined && node.step >= 0) {
|
||||||
|
if (d > node.step) d = node.step;
|
||||||
|
if (d < -node.step) d = -node.step;
|
||||||
|
}
|
||||||
|
|
||||||
|
let next = cur + d;
|
||||||
|
|
||||||
|
// range 限制
|
||||||
|
if (node?.min !== undefined) next = Math.max(next, node.min);
|
||||||
|
if (node?.max !== undefined) next = Math.min(next, node.max);
|
||||||
|
|
||||||
|
return { allow: true, value: next };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { allow: true, value: payload };
|
||||||
|
}
|
||||||
@@ -1,3 +1,21 @@
|
|||||||
export { applyStateForMessage, clearStateAppliedFor, clearStateAppliedFrom } from './executor.js';
|
export {
|
||||||
|
applyStateForMessage,
|
||||||
|
clearStateAppliedFor,
|
||||||
|
clearStateAppliedFrom,
|
||||||
|
restoreStateV2ToFloor,
|
||||||
|
trimStateV2FromFloor,
|
||||||
|
} from './executor.js';
|
||||||
|
|
||||||
export { parseStateBlock, extractStateBlocks, computeStateSignature, parseInlineValue } from './parser.js';
|
export { parseStateBlock, extractStateBlocks, computeStateSignature, parseInlineValue } from './parser.js';
|
||||||
export { generateSemantic } from './semantic.js';
|
export { generateSemantic } from './semantic.js';
|
||||||
|
|
||||||
|
export {
|
||||||
|
validate,
|
||||||
|
setRule,
|
||||||
|
clearRule,
|
||||||
|
clearAllRules,
|
||||||
|
loadRulesFromMeta,
|
||||||
|
saveRulesToMeta,
|
||||||
|
getRuleNode,
|
||||||
|
getParentPath,
|
||||||
|
} from './guard.js';
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
const MODULE_ID = 'varCommands';
|
const MODULE_ID = 'varCommands';
|
||||||
const TAG_RE_XBGETVAR = /\{\{xbgetvar::([^}]+)\}\}/gi;
|
const TAG_RE_XBGETVAR = /\{\{xbgetvar::([^}]+)\}\}/gi;
|
||||||
const TAG_RE_XBGETVAR_YAML = /\{\{xbgetvar_yaml::([^}]+)\}\}/gi;
|
const TAG_RE_XBGETVAR_YAML = /\{\{xbgetvar_yaml::([^}]+)\}\}/gi;
|
||||||
|
const TAG_RE_XBGETVAR_YAML_IDX = /\{\{xbgetvar_yaml_idx::([^}]+)\}\}/gi;
|
||||||
|
|
||||||
let events = null;
|
let events = null;
|
||||||
let initialized = false;
|
let initialized = false;
|
||||||
@@ -183,6 +184,121 @@ export function replaceXbGetVarYamlInString(s) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将 {{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) {
|
export function replaceXbGetVarInChat(chat) {
|
||||||
if (!Array.isArray(chat)) return;
|
if (!Array.isArray(chat)) return;
|
||||||
|
|
||||||
@@ -194,10 +310,12 @@ export function replaceXbGetVarInChat(chat) {
|
|||||||
const old = String(msg[key] ?? '');
|
const old = String(msg[key] ?? '');
|
||||||
const hasJson = old.indexOf('{{xbgetvar::') !== -1;
|
const hasJson = old.indexOf('{{xbgetvar::') !== -1;
|
||||||
const hasYaml = old.indexOf('{{xbgetvar_yaml::') !== -1;
|
const hasYaml = old.indexOf('{{xbgetvar_yaml::') !== -1;
|
||||||
if (!hasJson && !hasYaml) continue;
|
const hasYamlIdx = old.indexOf('{{xbgetvar_yaml_idx::') !== -1;
|
||||||
|
if (!hasJson && !hasYaml && !hasYamlIdx) continue;
|
||||||
|
|
||||||
let result = hasJson ? replaceXbGetVarInString(old) : old;
|
let result = hasJson ? replaceXbGetVarInString(old) : old;
|
||||||
result = hasYaml ? replaceXbGetVarYamlInString(result) : result;
|
result = hasYaml ? replaceXbGetVarYamlInString(result) : result;
|
||||||
|
result = hasYamlIdx ? replaceXbGetVarYamlIdxInString(result) : result;
|
||||||
msg[key] = result;
|
msg[key] = result;
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
@@ -215,10 +333,12 @@ export function applyXbGetVarForMessage(messageId, writeback = true) {
|
|||||||
const old = String(msg[key] ?? '');
|
const old = String(msg[key] ?? '');
|
||||||
const hasJson = old.indexOf('{{xbgetvar::') !== -1;
|
const hasJson = old.indexOf('{{xbgetvar::') !== -1;
|
||||||
const hasYaml = old.indexOf('{{xbgetvar_yaml::') !== -1;
|
const hasYaml = old.indexOf('{{xbgetvar_yaml::') !== -1;
|
||||||
if (!hasJson && !hasYaml) return;
|
const hasYamlIdx = old.indexOf('{{xbgetvar_yaml_idx::') !== -1;
|
||||||
|
if (!hasJson && !hasYaml && !hasYamlIdx) return;
|
||||||
|
|
||||||
let out = hasJson ? replaceXbGetVarInString(old) : old;
|
let out = hasJson ? replaceXbGetVarInString(old) : old;
|
||||||
out = hasYaml ? replaceXbGetVarYamlInString(out) : out;
|
out = hasYaml ? replaceXbGetVarYamlInString(out) : out;
|
||||||
|
out = hasYamlIdx ? replaceXbGetVarYamlIdxInString(out) : out;
|
||||||
if (writeback && out !== old) {
|
if (writeback && out !== old) {
|
||||||
msg[key] = out;
|
msg[key] = out;
|
||||||
}
|
}
|
||||||
@@ -1111,7 +1231,9 @@ export function cleanupVarCommands() {
|
|||||||
|
|
||||||
initialized = false;
|
initialized = false;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* 按值从数组中删除元素(2.0 pop 操作)
|
||||||
|
*/
|
||||||
export {
|
export {
|
||||||
MODULE_ID,
|
MODULE_ID,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
import { getContext } from "../../../../../extensions.js";
|
import { getContext } from "../../../../../extensions.js";
|
||||||
import { getLocalVariable } from "../../../../../variables.js";
|
import { getLocalVariable } from "../../../../../variables.js";
|
||||||
import { createModuleEvents } from "../../core/event-manager.js";
|
import { createModuleEvents } from "../../core/event-manager.js";
|
||||||
import { replaceXbGetVarInString, replaceXbGetVarYamlInString } from "./var-commands.js";
|
import { replaceXbGetVarInString, replaceXbGetVarYamlInString, replaceXbGetVarYamlIdxInString } from "./var-commands.js";
|
||||||
|
|
||||||
const MODULE_ID = 'vareventEditor';
|
const MODULE_ID = 'vareventEditor';
|
||||||
const LWB_EXT_ID = 'LittleWhiteBox';
|
const LWB_EXT_ID = 'LittleWhiteBox';
|
||||||
@@ -303,6 +303,9 @@ function installWIHiddenTagStripper() {
|
|||||||
if (msg.content.indexOf('{{xbgetvar_yaml::') !== -1) {
|
if (msg.content.indexOf('{{xbgetvar_yaml::') !== -1) {
|
||||||
msg.content = replaceXbGetVarYamlInString(msg.content);
|
msg.content = replaceXbGetVarYamlInString(msg.content);
|
||||||
}
|
}
|
||||||
|
if (msg.content.indexOf('{{xbgetvar_yaml_idx::') !== -1) {
|
||||||
|
msg.content = replaceXbGetVarYamlIdxInString(msg.content);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (Array.isArray(msg?.content)) {
|
if (Array.isArray(msg?.content)) {
|
||||||
for (const part of msg.content) {
|
for (const part of msg.content) {
|
||||||
@@ -321,6 +324,9 @@ function installWIHiddenTagStripper() {
|
|||||||
if (part.text.indexOf('{{xbgetvar_yaml::') !== -1) {
|
if (part.text.indexOf('{{xbgetvar_yaml::') !== -1) {
|
||||||
part.text = replaceXbGetVarYamlInString(part.text);
|
part.text = replaceXbGetVarYamlInString(part.text);
|
||||||
}
|
}
|
||||||
|
if (part.text.indexOf('{{xbgetvar_yaml_idx::') !== -1) {
|
||||||
|
part.text = replaceXbGetVarYamlIdxInString(part.text);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -339,6 +345,9 @@ function installWIHiddenTagStripper() {
|
|||||||
if (msg.mes.indexOf('{{xbgetvar_yaml::') !== -1) {
|
if (msg.mes.indexOf('{{xbgetvar_yaml::') !== -1) {
|
||||||
msg.mes = replaceXbGetVarYamlInString(msg.mes);
|
msg.mes = replaceXbGetVarYamlInString(msg.mes);
|
||||||
}
|
}
|
||||||
|
if (msg.mes.indexOf('{{xbgetvar_yaml_idx::') !== -1) {
|
||||||
|
msg.mes = replaceXbGetVarYamlIdxInString(msg.mes);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
@@ -373,6 +382,9 @@ function installWIHiddenTagStripper() {
|
|||||||
if (data.prompt.indexOf('{{xbgetvar_yaml::') !== -1) {
|
if (data.prompt.indexOf('{{xbgetvar_yaml::') !== -1) {
|
||||||
data.prompt = replaceXbGetVarYamlInString(data.prompt);
|
data.prompt = replaceXbGetVarYamlInString(data.prompt);
|
||||||
}
|
}
|
||||||
|
if (data.prompt.indexOf('{{xbgetvar_yaml_idx::') !== -1) {
|
||||||
|
data.prompt = replaceXbGetVarYamlIdxInString(data.prompt);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import {
|
|||||||
applyXbGetVarForMessage,
|
applyXbGetVarForMessage,
|
||||||
parseValueForSet,
|
parseValueForSet,
|
||||||
} from "./var-commands.js";
|
} from "./var-commands.js";
|
||||||
import { applyStateForMessage, clearStateAppliedFrom } from "./state2/index.js";
|
import { applyStateForMessage } from "./state2/index.js";
|
||||||
import {
|
import {
|
||||||
preprocessBumpAliases,
|
preprocessBumpAliases,
|
||||||
executeQueuedVareventJsAfterTurn,
|
executeQueuedVareventJsAfterTurn,
|
||||||
@@ -1624,16 +1624,10 @@ function rollbackToPreviousOf(messageId) {
|
|||||||
const id = Number(messageId);
|
const id = Number(messageId);
|
||||||
if (Number.isNaN(id)) return;
|
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;
|
const prevId = id - 1;
|
||||||
if (prevId < 0) return;
|
if (prevId < 0) return;
|
||||||
|
|
||||||
|
// ???? 1.0 ???????
|
||||||
const snap = getSnapshot(prevId);
|
const snap = getSnapshot(prevId);
|
||||||
if (snap) {
|
if (snap) {
|
||||||
const normalized = normalizeSnapshotRecord(snap);
|
const normalized = normalizeSnapshotRecord(snap);
|
||||||
@@ -1647,12 +1641,52 @@ function rollbackToPreviousOf(messageId) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function rebuildVariablesFromScratch() {
|
async function rollbackToPreviousOfAsync(messageId) {
|
||||||
|
const id = Number(messageId);
|
||||||
|
if (Number.isNaN(id)) return;
|
||||||
|
|
||||||
|
// ???????? floor>=id ? L0
|
||||||
|
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 ???
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[variablesCore][2.0] restoreStateV2ToFloor failed:', e);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// mode === '1.0'
|
||||||
|
rollbackToPreviousOf(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function rebuildVariablesFromScratch() {
|
||||||
try {
|
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 旧逻辑
|
||||||
setVarDict({});
|
setVarDict({});
|
||||||
const chat = getContext()?.chat || [];
|
const chat = getContext()?.chat || [];
|
||||||
for (let i = 0; i < chat.length; i++) {
|
for (let i = 0; i < chat.length; i++) {
|
||||||
applyVariablesForMessage(i);
|
await applyVariablesForMessage(i);
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
@@ -1842,7 +1876,7 @@ async function applyVariablesForMessage(messageId) {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
parseErrors++;
|
parseErrors++;
|
||||||
if (debugOn) {
|
if (debugOn) {
|
||||||
try { xbLog.error(MODULE_ID, `plot-log è§£æž<EFBFBD>失败:楼å±?${messageId} å<EFBFBD>?${idx + 1} 预览=${preview(b)}`, e); } catch {}
|
try { xbLog.error(MODULE_ID, `plot-log 解析失败:楼<EFBFBD>?${messageId} <20>?${idx + 1} 预览=${preview(b)}`, e); } catch {}
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1873,7 +1907,7 @@ async function applyVariablesForMessage(messageId) {
|
|||||||
try {
|
try {
|
||||||
xbLog.warn(
|
xbLog.warn(
|
||||||
MODULE_ID,
|
MODULE_ID,
|
||||||
`plot-log 未产生å<EFBFBD>¯æ‰§è¡ŒæŒ‡ä»¤ï¼šæ¥¼å±?${messageId} å<EFBFBD>—æ•°=${blocks.length} è§£æž<EFBFBD>æ<EFBFBD>¡ç›®=${parsedPartsTotal} è§£æž<EFBFBD>失败=${parseErrors} 预览=${preview(blocks[0])}`
|
`plot-log 未产生可执行指令:楼<EFBFBD>?${messageId} 块数=${blocks.length} 解析条目=${parsedPartsTotal} 解析失败=${parseErrors} 预览=${preview(blocks[0])}`
|
||||||
);
|
);
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
@@ -2149,7 +2183,7 @@ async function applyVariablesForMessage(messageId) {
|
|||||||
const denied = guardDenied ? `,被规则拦截=${guardDenied}` : '';
|
const denied = guardDenied ? `,被规则拦截=${guardDenied}` : '';
|
||||||
xbLog.warn(
|
xbLog.warn(
|
||||||
MODULE_ID,
|
MODULE_ID,
|
||||||
`plot-log 指令执行å<EFBFBD>Žæ— å<EFBFBD>˜åŒ–:楼å±?${messageId} 指令æ•?${ops.length}${denied} 示例=${preview(JSON.stringify(guardDeniedSamples))}`
|
`plot-log 指令执行后无变化:楼<EFBFBD>?${messageId} 指令<EFBFBD>?${ops.length}${denied} 示例=${preview(JSON.stringify(guardDeniedSamples))}`
|
||||||
);
|
);
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
@@ -2218,7 +2252,7 @@ function bindEvents() {
|
|||||||
|
|
||||||
events?.on(event_types.MESSAGE_SENT, async () => {
|
events?.on(event_types.MESSAGE_SENT, async () => {
|
||||||
try {
|
try {
|
||||||
snapshotCurrentLastFloor();
|
if (getVariablesMode() !== '2.0') snapshotCurrentLastFloor();
|
||||||
const chat = getContext()?.chat || [];
|
const chat = getContext()?.chat || [];
|
||||||
const id = chat.length ? chat.length - 1 : undefined;
|
const id = chat.length ? chat.length - 1 : undefined;
|
||||||
if (typeof id === 'number') {
|
if (typeof id === 'number') {
|
||||||
@@ -2247,7 +2281,7 @@ function bindEvents() {
|
|||||||
if (typeof id === 'number') {
|
if (typeof id === 'number') {
|
||||||
await applyVarsForMessage(id);
|
await applyVarsForMessage(id);
|
||||||
applyXbGetVarForMessage(id, true);
|
applyXbGetVarForMessage(id, true);
|
||||||
snapshotForMessageId(id);
|
if (getVariablesMode() !== '2.0') snapshotForMessageId(id);
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
});
|
});
|
||||||
@@ -2259,7 +2293,7 @@ function bindEvents() {
|
|||||||
if (typeof id === 'number') {
|
if (typeof id === 'number') {
|
||||||
await applyVarsForMessage(id);
|
await applyVarsForMessage(id);
|
||||||
applyXbGetVarForMessage(id, true);
|
applyXbGetVarForMessage(id, true);
|
||||||
snapshotForMessageId(id);
|
if (getVariablesMode() !== '2.0') snapshotForMessageId(id);
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
});
|
});
|
||||||
@@ -2283,33 +2317,35 @@ function bindEvents() {
|
|||||||
events?.on(event_types.MESSAGE_EDITED, async (data) => {
|
events?.on(event_types.MESSAGE_EDITED, async (data) => {
|
||||||
try {
|
try {
|
||||||
const id = getMsgIdLoose(data);
|
const id = getMsgIdLoose(data);
|
||||||
if (typeof id === 'number') {
|
if (typeof id !== 'number') return;
|
||||||
clearAppliedFor(id);
|
|
||||||
rollbackToPreviousOf(id);
|
|
||||||
|
|
||||||
setTimeout(async () => {
|
if (getVariablesMode() !== '2.0') clearAppliedFor(id);
|
||||||
await applyVarsForMessage(id);
|
|
||||||
applyXbGetVarForMessage(id, true);
|
|
||||||
|
|
||||||
try {
|
// ? ?? await????? apply ????????????
|
||||||
const ctx = getContext();
|
await rollbackToPreviousOfAsync(id);
|
||||||
const msg = ctx?.chat?.[id];
|
|
||||||
if (msg) updateMessageBlock(id, msg, { rerenderMessage: true });
|
|
||||||
} catch {}
|
|
||||||
|
|
||||||
try {
|
setTimeout(async () => {
|
||||||
const ctx = getContext();
|
await applyVarsForMessage(id);
|
||||||
const es = ctx?.eventSource;
|
applyXbGetVarForMessage(id, true);
|
||||||
const et = ctx?.event_types;
|
|
||||||
if (es?.emit && et?.MESSAGE_UPDATED) {
|
|
||||||
suppressUpdatedOnce.add(id);
|
|
||||||
await es.emit(et.MESSAGE_UPDATED, id);
|
|
||||||
}
|
|
||||||
} catch {}
|
|
||||||
|
|
||||||
await executeQueuedVareventJsAfterTurn();
|
try {
|
||||||
}, 10);
|
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 {}
|
} catch {}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -2317,28 +2353,44 @@ function bindEvents() {
|
|||||||
events?.on(event_types.MESSAGE_SWIPED, async (data) => {
|
events?.on(event_types.MESSAGE_SWIPED, async (data) => {
|
||||||
try {
|
try {
|
||||||
const id = getMsgIdLoose(data);
|
const id = getMsgIdLoose(data);
|
||||||
if (typeof id === 'number') {
|
if (typeof id !== 'number') return;
|
||||||
lastSwipedId = id;
|
|
||||||
clearAppliedFor(id);
|
|
||||||
rollbackToPreviousOf(id);
|
|
||||||
|
|
||||||
const tId = setTimeout(async () => {
|
lastSwipedId = id;
|
||||||
pendingSwipeApply.delete(id);
|
if (getVariablesMode() !== '2.0') clearAppliedFor(id);
|
||||||
await applyVarsForMessage(id);
|
|
||||||
await executeQueuedVareventJsAfterTurn();
|
|
||||||
}, 10);
|
|
||||||
|
|
||||||
pendingSwipeApply.set(id, tId);
|
// ? ?? await???????????????
|
||||||
}
|
await rollbackToPreviousOfAsync(id);
|
||||||
|
|
||||||
|
const tId = setTimeout(async () => {
|
||||||
|
pendingSwipeApply.delete(id);
|
||||||
|
await applyVarsForMessage(id);
|
||||||
|
await executeQueuedVareventJsAfterTurn();
|
||||||
|
}, 10);
|
||||||
|
|
||||||
|
pendingSwipeApply.set(id, tId);
|
||||||
} catch {}
|
} catch {}
|
||||||
});
|
});
|
||||||
|
|
||||||
// message deleted
|
// message deleted
|
||||||
events?.on(event_types.MESSAGE_DELETED, (data) => {
|
events?.on(event_types.MESSAGE_DELETED, async (data) => {
|
||||||
try {
|
try {
|
||||||
const id = getMsgIdStrict(data);
|
const id = getMsgIdStrict(data);
|
||||||
if (typeof id === 'number') {
|
if (typeof id !== 'number') return;
|
||||||
rollbackToPreviousOf(id);
|
|
||||||
|
// ? ????????await ???????
|
||||||
|
await rollbackToPreviousOfAsync(id);
|
||||||
|
|
||||||
|
// ✅ 2.0:物理删除消息 => 同步清理 WAL/ckpt,避免膨胀
|
||||||
|
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);
|
clearSnapshotsFrom(id);
|
||||||
clearAppliedFrom(id);
|
clearAppliedFrom(id);
|
||||||
}
|
}
|
||||||
@@ -2349,7 +2401,7 @@ function bindEvents() {
|
|||||||
|
|
||||||
events?.on(event_types.GENERATION_STARTED, (data) => {
|
events?.on(event_types.GENERATION_STARTED, (data) => {
|
||||||
try {
|
try {
|
||||||
snapshotPreviousFloor();
|
if (getVariablesMode() !== '2.0') snapshotPreviousFloor();
|
||||||
|
|
||||||
// cancel swipe delay
|
// cancel swipe delay
|
||||||
const t = (typeof data === 'string' ? data : (data?.type || '')).toLowerCase();
|
const t = (typeof data === 'string' ? data : (data?.type || '')).toLowerCase();
|
||||||
@@ -2364,7 +2416,7 @@ function bindEvents() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// chat changed
|
// chat changed
|
||||||
events?.on(event_types.CHAT_CHANGED, () => {
|
events?.on(event_types.CHAT_CHANGED, async () => {
|
||||||
try {
|
try {
|
||||||
rulesClearCache();
|
rulesClearCache();
|
||||||
rulesLoadFromMeta();
|
rulesLoadFromMeta();
|
||||||
@@ -2372,6 +2424,13 @@ function bindEvents() {
|
|||||||
const meta = getContext()?.chatMetadata || {};
|
const meta = getContext()?.chatMetadata || {};
|
||||||
meta[LWB_PLOT_APPLIED_KEY] = {};
|
meta[LWB_PLOT_APPLIED_KEY] = {};
|
||||||
getContext()?.saveMetadataDebounced?.();
|
getContext()?.saveMetadataDebounced?.();
|
||||||
|
|
||||||
|
if (getVariablesMode() === '2.0') {
|
||||||
|
try {
|
||||||
|
const mod = await import('./state2/index.js');
|
||||||
|
mod.clearStateAppliedFrom(0);
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -2408,6 +2467,33 @@ export function initVariablesCore() {
|
|||||||
applyDeltaTable: applyRulesDeltaToTable,
|
applyDeltaTable: applyRulesDeltaToTable,
|
||||||
save: rulesSaveToMeta,
|
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;
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -2429,6 +2515,7 @@ export function cleanupVariablesCore() {
|
|||||||
|
|
||||||
// clear global hooks
|
// clear global hooks
|
||||||
delete globalThis.LWB_Guard;
|
delete globalThis.LWB_Guard;
|
||||||
|
delete globalThis.LWB_StateV2;
|
||||||
|
|
||||||
// clear guard state
|
// clear guard state
|
||||||
guardBypass(false);
|
guardBypass(false);
|
||||||
|
|||||||
Reference in New Issue
Block a user