200 lines
6.1 KiB
JavaScript
200 lines
6.1 KiB
JavaScript
|
|
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 };
|
|||
|
|
}
|