Files
LittleWhiteBox/modules/variables/state2/executor.js

200 lines
6.1 KiB
JavaScript
Raw Blame History

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