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

250 lines
7.3 KiB
JavaScript
Raw Normal View History

2026-02-01 02:49:35 +08:00
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 {}
}
2026-02-01 21:55:47 +08:00
export function getRuleNode(absPath) {
return matchRuleWithWildcard(absPath);
2026-02-01 02:49:35 +08:00
}
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('.');
}
2026-02-01 21:55:47 +08:00
/**
* 通配符路径匹配
* 例如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;
}
2026-02-01 02:49:35 +08:00
/**
* 验证操作
2026-02-01 21:55:47 +08:00
* @returns {{ allow: boolean, value?: any, reason?: string, note?: string }}
2026-02-01 02:49:35 +08:00
*/
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;
2026-02-01 21:55:47 +08:00
const lastSeg = String(absPath).split('.').pop() || '';
// ===== 1. $schema 白名单检查 =====
if (parentNode?.allowedKeys && Array.isArray(parentNode.allowedKeys)) {
2026-02-01 02:49:35 +08:00
if (isNewKey && (op === 'set' || op === 'push')) {
2026-02-01 21:55:47 +08:00
if (!parentNode.allowedKeys.includes(lastSeg)) {
return { allow: false, reason: `字段不在结构模板中` };
}
2026-02-01 02:49:35 +08:00
}
if (op === 'del') {
2026-02-01 21:55:47 +08:00
if (parentNode.allowedKeys.includes(lastSeg)) {
return { allow: false, reason: `模板定义的字段不能删除` };
}
2026-02-01 02:49:35 +08:00
}
}
2026-02-01 21:55:47 +08:00
// ===== 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;
2026-02-01 02:49:35 +08:00
}
2026-02-01 21:55:47 +08:00
// ===== 4. 数组扩展检查 =====
if (op === 'push') {
if (node && node.typeLock === 'array' && !node.arrayGrow) {
return { allow: false, reason: '数组不允许扩展' };
}
2026-02-01 02:49:35 +08:00
}
2026-02-01 21:55:47 +08:00
// ===== 5. $ro 只读 =====
if (node?.ro && (op === 'set' || op === 'inc')) {
return { allow: false, reason: '只读字段' };
}
// ===== 6. set 操作:数值约束 =====
2026-02-01 02:49:35 +08:00
if (op === 'set') {
const num = Number(payload);
2026-02-01 21:55:47 +08:00
// range 限制
2026-02-01 02:49:35 +08:00
if (Number.isFinite(num) && (node?.min !== undefined || node?.max !== undefined)) {
let v = num;
2026-02-01 21:55:47 +08:00
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,
};
2026-02-01 02:49:35 +08:00
}
2026-02-01 21:55:47 +08:00
// enum 枚举(不自动修正,直接拒绝)
2026-02-01 02:49:35 +08:00
if (node?.enum?.length) {
2026-02-01 21:55:47 +08:00
const s = String(payload ?? '');
if (!node.enum.includes(s)) {
return { allow: false, reason: `枚举不匹配,允许:${node.enum.join(' / ')}` };
2026-02-01 02:49:35 +08:00
}
}
2026-02-01 21:55:47 +08:00
2026-02-01 02:49:35 +08:00
return { allow: true, value: payload };
}
2026-02-01 21:55:47 +08:00
// ===== 7. inc 操作step / range 限制 =====
2026-02-01 02:49:35 +08:00
if (op === 'inc') {
const delta = Number(payload);
2026-02-01 21:55:47 +08:00
if (!Number.isFinite(delta)) return { allow: false, reason: 'delta 不是数字' };
2026-02-01 02:49:35 +08:00
const cur = Number(currentValue) || 0;
let d = delta;
2026-02-01 21:55:47 +08:00
const noteParts = [];
2026-02-01 02:49:35 +08:00
// step 限制
if (node?.step !== undefined && node.step >= 0) {
2026-02-01 21:55:47 +08:00
const before = d;
2026-02-01 02:49:35 +08:00
if (d > node.step) d = node.step;
if (d < -node.step) d = -node.step;
2026-02-01 21:55:47 +08:00
if (d !== before) {
noteParts.push(`超出步长限制,已限制到 ${d >= 0 ? '+' : ''}${d}`);
}
2026-02-01 02:49:35 +08:00
}
let next = cur + d;
// range 限制
2026-02-01 21:55:47 +08:00
const beforeClamp = next;
2026-02-01 02:49:35 +08:00
if (node?.min !== undefined) next = Math.max(next, node.min);
if (node?.max !== undefined) next = Math.min(next, node.max);
2026-02-01 21:55:47 +08:00
if (next !== beforeClamp) {
noteParts.push(`超出范围,已限制到 ${next}`);
}
2026-02-01 02:49:35 +08:00
2026-02-01 21:55:47 +08:00
return {
allow: true,
value: next,
note: noteParts.length ? noteParts.join('') : undefined,
};
2026-02-01 02:49:35 +08:00
}
return { allow: true, value: payload };
}
2026-02-01 21:55:47 +08:00