Upload LittleWhiteBox extension
This commit is contained in:
249
modules/variables/state2/guard.js
Normal file
249
modules/variables/state2/guard.js
Normal file
@@ -0,0 +1,249 @@
|
||||
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 };
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user