From 5dd9fb6f97b27058fe84bf62817a5a55e19538ed Mon Sep 17 00:00:00 2001 From: bielie Date: Sun, 1 Feb 2026 02:55:43 +0800 Subject: [PATCH] Update variables UI and parsing --- modules/variables/state2/parser.js | 109 +++++++++++++++++++++------ modules/variables/state2/semantic.js | 1 - modules/variables/variables-panel.js | 78 ++++++++++++------- 3 files changed, 137 insertions(+), 51 deletions(-) diff --git a/modules/variables/state2/parser.js b/modules/variables/state2/parser.js index f42995d..6ddad38 100644 --- a/modules/variables/state2/parser.js +++ b/modules/variables/state2/parser.js @@ -26,27 +26,95 @@ export function computeStateSignature(text) { } /** - * 解析 块内容 -> ops[] - * 单行支持运算符,多行只支持覆盖 set(YAML) + * 解析 块 + * 返回: { 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 }; } diff --git a/modules/variables/state2/semantic.js b/modules/variables/state2/semantic.js index a5549e3..7203e9b 100644 --- a/modules/variables/state2/semantic.js +++ b/modules/variables/state2/semantic.js @@ -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); diff --git a/modules/variables/variables-panel.js b/modules/variables/variables-panel.js index 4aab4dc..dba5c08 100644 --- a/modules/variables/variables-panel.js +++ b/modules/variables/variables-panel.js @@ -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,'"'), out=[]; - if(n.ro) out.push(``); - if((n.objectPolicy&&n.objectPolicy!=='none')||(n.arrayPolicy&&n.arrayPolicy!=='lock')) out.push(``); - 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(``); - return out.length?`${out.join('')}`:''; + +const badgesHtml = (n) => { + if (!hasAnyRule(n)) return ''; + const tip = ruleTip(n).replace(/"/g,'"'); + + const out = []; + if (n.ro) out.push(``); + if (n.lock) out.push(``); + if ((n.min !== undefined || n.max !== undefined) || (n.step !== undefined) || (Array.isArray(n.enum) && n.enum.length)) { + out.push(``); + } + return out.length ? `${out.join('')}` : ''; }; + const debounce=(fn,ms=200)=>{let t;return(...a)=>{clearTimeout(t);t=setTimeout(()=>fn(...a),ms);}}; class VariablesPanel {