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