Update variables UI and parsing

This commit is contained in:
2026-02-01 02:55:43 +08:00
parent bcf664e9a0
commit 5dd9fb6f97
3 changed files with 137 additions and 51 deletions

View File

@@ -26,27 +26,95 @@ export function computeStateSignature(text) {
}
/**
* 解析 <state> 块内容 -> ops[]
* 单行支持运算符,多行只支持覆盖 setYAML
* 解析 <state> 块
* 返回: { rules: [{path, rule}], ops: [{path, op, value, ...}] }
*/
export function parseStateBlock(content) {
const results = [];
const lines = String(content ?? '').split(/\r?\n/);
const rules = [];
const dataLines = [];
// 第一遍:分离规则行和数据行
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#')) continue;
// 规则行:以 $ 开头
if (trimmed.startsWith('$')) {
const parsed = parseRuleLineInternal(trimmed);
if (parsed) rules.push(parsed);
} else {
dataLines.push(line);
}
}
// 第二遍:解析数据
const ops = parseDataLines(dataLines);
return { rules, ops };
}
function parseRuleLineInternal(line) {
const tokens = line.trim().split(/\s+/);
const directives = [];
let pathStart = 0;
for (let i = 0; i < tokens.length; i++) {
if (tokens[i].startsWith('$')) {
directives.push(tokens[i]);
pathStart = i + 1;
} else {
break;
}
}
const path = tokens.slice(pathStart).join(' ').trim();
if (!path || !directives.length) return null;
const rule = {};
for (const tok of directives) {
if (tok === '$ro') { rule.ro = true; continue; }
if (tok === '$lock') { rule.lock = true; continue; }
const rangeMatch = tok.match(/^\$range=\[\s*(-?\d+(?:\.\d+)?)\s*,\s*(-?\d+(?:\.\d+)?)\s*\]$/);
if (rangeMatch) {
rule.min = Math.min(Number(rangeMatch[1]), Number(rangeMatch[2]));
rule.max = Math.max(Number(rangeMatch[1]), Number(rangeMatch[2]));
continue;
}
const stepMatch = tok.match(/^\$step=(\d+(?:\.\d+)?)$/);
if (stepMatch) { rule.step = Math.abs(Number(stepMatch[1])); continue; }
const enumMatch = tok.match(/^\$enum=\{([^}]+)\}$/);
if (enumMatch) {
rule.enum = enumMatch[1].split(/[;]/).map(s => s.trim()).filter(Boolean);
continue;
}
}
return { path, rule };
}
function parseDataLines(lines) {
const results = [];
let pendingPath = null;
let pendingLines = [];
const flushPending = () => {
if (!pendingPath) return;
// 没有任何缩进行:视为 set 空字符串
if (!pendingLines.length) {
results.push({ path: pendingPath, op: 'set', value: '' });
pendingPath = null;
pendingLines = [];
return;
}
try {
// 去除公共缩进
const nonEmpty = pendingLines.filter(l => l.trim());
const minIndent = nonEmpty.length
? Math.min(...nonEmpty.map(l => l.search(/\S/)))
@@ -121,25 +189,22 @@ function unescapeString(s) {
.replace(/\\\\/g, '\\');
}
/**
* 单行内联值解析
*/
export function parseInlineValue(raw) {
const t = String(raw ?? '').trim();
if (t === 'null') return { op: 'del' };
// (负数) 用于强制 set -5而不是 inc -5
// (负数) 强制 set
const parenNum = t.match(/^\((-?\d+(?:\.\d+)?)\)$/);
if (parenNum) return { op: 'set', value: Number(parenNum[1]) };
// +10 / -20
// +N / -N
if (/^\+\d/.test(t) || /^-\d/.test(t)) {
const n = Number(t);
if (Number.isFinite(n)) return { op: 'inc', delta: n };
}
// +"str" / +'str'
// +"str"
const pushD = t.match(/^\+"((?:[^"\\]|\\.)*)"\s*$/);
if (pushD) return { op: 'push', value: unescapeString(pushD[1]) };
const pushS = t.match(/^\+'((?:[^'\\]|\\.)*)'\s*$/);
@@ -150,13 +215,11 @@ export function parseInlineValue(raw) {
try {
const arr = JSON.parse(t.slice(1));
if (Array.isArray(arr)) return { op: 'push', value: arr };
return { op: 'set', value: t, warning: '+[] 不是数组,作为字符串' };
} catch {
return { op: 'set', value: t, warning: '+[] JSON 解析失败,作为字符串' };
}
} catch {}
return { op: 'set', value: t, warning: '+[] 解析失败' };
}
// -"str" / -'str'
// -"str"
const popD = t.match(/^-"((?:[^"\\]|\\.)*)"\s*$/);
if (popD) return { op: 'pop', value: unescapeString(popD[1]) };
const popS = t.match(/^-'((?:[^'\\]|\\.)*)'\s*$/);
@@ -167,13 +230,11 @@ export function parseInlineValue(raw) {
try {
const arr = JSON.parse(t.slice(1));
if (Array.isArray(arr)) return { op: 'pop', value: arr };
return { op: 'set', value: t, warning: '-[] 不是数组,作为字符串' };
} catch {
return { op: 'set', value: t, warning: '-[] JSON 解析失败,作为字符串' };
}
} catch {}
return { op: 'set', value: t, warning: '-[] 解析失败' };
}
// 裸数字 set
// 裸数字
if (/^-?\d+(?:\.\d+)?$/.test(t)) return { op: 'set', value: Number(t) };
// "str" / 'str'
@@ -185,12 +246,12 @@ export function parseInlineValue(raw) {
if (t === 'true') return { op: 'set', value: true };
if (t === 'false') return { op: 'set', value: false };
// JSON set
// JSON array/object
if (t.startsWith('{') || t.startsWith('[')) {
try { return { op: 'set', value: JSON.parse(t) }; }
catch { return { op: 'set', value: t, warning: 'JSON 解析失败,作为字符串' }; }
catch { return { op: 'set', value: t, warning: 'JSON 解析失败' }; }
}
// 兜底 set 原文本
// 兜底
return { op: 'set', value: t };
}

View File

@@ -5,7 +5,6 @@ export function generateSemantic(path, op, oldValue, newValue, delta, operandVal
if (v === undefined) return '空';
if (v === null) return 'null';
try {
if (typeof v === 'string') return JSON.stringify(v);
return JSON.stringify(v);
} catch {
return String(v);

View File

@@ -117,38 +117,64 @@ const VT = {
global: { getter: getGlobalVariable, setter: setGlobalVariable, storage: ()=> extension_settings.variables?.global || ((extension_settings.variables = { global: {} }).global), save: saveSettingsDebounced },
};
const LWB_RULES_KEY='LWB_RULES';
const getRulesTable = () => { try { return getContext()?.chatMetadata?.[LWB_RULES_KEY] || {}; } catch { return {}; } };
const EXT_ID = 'LittleWhiteBox';
const LWB_RULES_V1_KEY = 'LWB_RULES';
const LWB_RULES_V2_KEY = 'LWB_RULES_V2';
const getRulesTable = () => {
try {
const ctx = getContext();
const mode = extension_settings?.[EXT_ID]?.variablesMode || '1.0';
const meta = ctx?.chatMetadata || {};
return mode === '2.0'
? (meta[LWB_RULES_V2_KEY] || {})
: (meta[LWB_RULES_V1_KEY] || {});
} catch {
return {};
}
};
const pathKey = (arr)=>{ try { return (arr||[]).map(String).join('.'); } catch { return ''; } };
const getRuleNodeByPath = (arr)=> (pathKey(arr) ? (getRulesTable()||{})[pathKey(arr)] : undefined);
const hasAnyRule = (n)=>{
if(!n) return false;
if(n.ro) return true;
if(n.objectPolicy && n.objectPolicy!=='none') return true;
if(n.arrayPolicy && n.arrayPolicy!=='lock') return true;
const c=n.constraints||{};
return ('min'in c)||('max'in c)||('step'in c)||(Array.isArray(c.enum)&&c.enum.length)||(c.regex&&c.regex.source);
const hasAnyRule = (n) => {
if (!n) return false;
if (n.ro) return true;
if (n.lock) return true;
if (n.min !== undefined || n.max !== undefined) return true;
if (n.step !== undefined) return true;
if (Array.isArray(n.enum) && n.enum.length) return true;
return false;
};
const ruleTip = (n)=>{
if(!n) return '';
const lines=[], c=n.constraints||{};
if(n.ro) lines.push('只读:$ro');
if(n.objectPolicy){ const m={none:'(默认:不可增删键)',ext:'$ext可增键',prune:'$prune可删键',free:'$free可增删键'}; lines.push(`对象策略:${m[n.objectPolicy]||n.objectPolicy}`); }
if(n.arrayPolicy){ const m={lock:'(默认:不可增删项)',grow:'$grow可增项',shrink:'$shrink可删项',list:'$list可增删项'}; lines.push(`数组策略:${m[n.arrayPolicy]||n.arrayPolicy}`); }
if('min'in c||'max'in c){ if('min'in c&&'max'in c) lines.push(`范围:$range=[${c.min},${c.max}]`); else if('min'in c) lines.push(`下限:$min=${c.min}`); else lines.push(`上限:$max=${c.max}`); }
if('step'in c) lines.push(`步长:$step=${c.step}`);
if(Array.isArray(c.enum)&&c.enum.length) lines.push(`枚举:$enum={${c.enum.join(';')}}`);
if(c.regex&&c.regex.source) lines.push(`正则:$match=/${c.regex.source}/${c.regex.flags||''}`);
const ruleTip = (n) => {
if (!n) return '';
const lines = [];
if (n.ro) lines.push('只读:$ro');
if (n.lock) lines.push('结构锁:$lock禁止增删该层 key/项)');
if (n.min !== undefined || n.max !== undefined) {
const a = n.min !== undefined ? n.min : '-∞';
const b = n.max !== undefined ? n.max : '+∞';
lines.push(`范围:$range=[${a},${b}]`);
}
if (n.step !== undefined) lines.push(`步长:$step=${n.step}`);
if (Array.isArray(n.enum) && n.enum.length) lines.push(`枚举:$enum={${n.enum.join(';')}}`);
return lines.join('\n');
};
const badgesHtml = (n)=>{
if(!hasAnyRule(n)) return '';
const tip=ruleTip(n).replace(/"/g,'&quot;'), out=[];
if(n.ro) out.push(`<span class="vm-badge" data-type="ro" title="${tip}"><i class="fa-solid fa-shield-halved"></i></span>`);
if((n.objectPolicy&&n.objectPolicy!=='none')||(n.arrayPolicy&&n.arrayPolicy!=='lock')) out.push(`<span class="vm-badge" data-type="struct" title="${tip}"><i class="fa-solid fa-diagram-project"></i></span>`);
const c=n.constraints||{}; if(('min'in c)||('max'in c)||('step'in c)||(Array.isArray(c.enum)&&c.enum.length)||(c.regex&&c.regex.source)) out.push(`<span class="vm-badge" data-type="cons" title="${tip}"><i class="fa-solid fa-ruler-vertical"></i></span>`);
return out.length?`<span class="vm-badges">${out.join('')}</span>`:'';
const badgesHtml = (n) => {
if (!hasAnyRule(n)) return '';
const tip = ruleTip(n).replace(/"/g,'&quot;');
const out = [];
if (n.ro) out.push(`<span class="vm-badge" data-type="ro" title="${tip}"><i class="fa-solid fa-shield-halved"></i></span>`);
if (n.lock) out.push(`<span class="vm-badge" data-type="struct" title="${tip}"><i class="fa-solid fa-diagram-project"></i></span>`);
if ((n.min !== undefined || n.max !== undefined) || (n.step !== undefined) || (Array.isArray(n.enum) && n.enum.length)) {
out.push(`<span class="vm-badge" data-type="cons" title="${tip}"><i class="fa-solid fa-ruler-vertical"></i></span>`);
}
return out.length ? `<span class="vm-badges">${out.join('')}</span>` : '';
};
const debounce=(fn,ms=200)=>{let t;return(...a)=>{clearTimeout(t);t=setTimeout(()=>fn(...a),ms);}};
class VariablesPanel {