Files

250 lines
7.3 KiB
JavaScript
Raw Permalink 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';
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 };
}