This commit is contained in:
RT15548
2025-12-28 18:55:41 +08:00
committed by GitHub
parent a693c55e50
commit 937e9f6386

View File

@@ -160,57 +160,6 @@ function normalizeOpName(k) {
return OP_MAP[String(k).toLowerCase().trim()] || null; return OP_MAP[String(k).toLowerCase().trim()] || null;
} }
function parseBumpSpec(payload) {
try {
if (payload && typeof payload === 'object' && payload.kind && Number.isFinite(Number(payload.value))) {
return { kind: String(payload.kind), value: Number(payload.value), raw: payload.raw };
}
if (typeof payload === 'number' && Number.isFinite(payload)) {
return { kind: 'delta', value: payload, raw: payload };
}
const raw = String(payload ?? '').trim();
if (!raw) return null;
const num = Number(raw);
if (Number.isFinite(num) && !/[*/^%]/.test(raw)) {
return { kind: 'delta', value: num, raw };
}
const mPct = raw.match(/^([+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?)\s*%$/);
if (mPct) return { kind: 'percent', value: Number(mPct[1]), raw };
const mOp = raw.match(/^([*/^])\s*([+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?)$/);
if (mOp) {
const op = mOp[1] === '*' ? 'mul' : (mOp[1] === '/' ? 'div' : 'pow');
return { kind: op, value: Number(mOp[2]), raw };
}
} catch {}
return null;
}
function computeBumpResult(payload, currentValue) {
const spec = parseBumpSpec(payload);
if (!spec) return { ok: false, reason: 'delta-nan' };
const baseRaw = Number(currentValue);
const base = Number.isFinite(baseRaw) ? baseRaw : 0;
let next;
if (spec.kind === 'delta') next = base + spec.value;
else if (spec.kind === 'percent') next = base + (base * (spec.value / 100));
else if (spec.kind === 'mul') next = base * spec.value;
else if (spec.kind === 'div') {
if (!Number.isFinite(spec.value) || spec.value === 0) return { ok: false, reason: 'div-zero', spec };
next = base / spec.value;
} else if (spec.kind === 'pow') next = Math.pow(base, spec.value);
else return { ok: false, reason: 'delta-nan', spec };
if (!Number.isFinite(next)) return { ok: false, reason: 'result-nan', spec };
return { ok: true, base, next, delta: next - base, spec };
}
/* ============= 应用签名追踪 ============= */ /* ============= 应用签名追踪 ============= */
function getAppliedMap() { function getAppliedMap() {
@@ -354,12 +303,10 @@ function parseBlock(innerText) {
Array.isArray(value) ? arr.push(...value) : arr.push(value); Array.isArray(value) ? arr.push(...value) : arr.push(value);
}; };
const putBump = (top, path, delta) => { const putBump = (top, path, delta) => {
const spec = parseBumpSpec(delta); const n = Number(String(delta).replace(/^\+/, ''));
if (!spec) return; if (!Number.isFinite(n)) return;
ops.bump[top] ||= {}; ops.bump[top] ||= {};
const list = (ops.bump[top][path] ||= []); ops.bump[top][path] = (ops.bump[top][path] ?? 0) + n;
if (spec.kind === 'delta') list.push(spec.value);
else list.push(spec);
}; };
const putDel = (top, path) => { const putDel = (top, path) => {
ops.del[top] ||= []; ops.del[top] ||= [];
@@ -790,7 +737,7 @@ function parseBlock(innerText) {
const rel = rest.join('.'); const rel = rest.join('.');
if (curOp === 'set') putSet(top, rel, text); if (curOp === 'set') putSet(top, rel, text);
else if (curOp === 'push') putPush(top, rel, text); else if (curOp === 'push') putPush(top, rel, text);
else if (curOp === 'bump') putBump(top, rel, text); else if (curOp === 'bump') putBump(top, rel, Number(text));
continue; continue;
} }
@@ -826,7 +773,7 @@ function parseBlock(innerText) {
for (const item of arr) putDel(top, rel ? `${rel}.${item}` : item); for (const item of arr) putDel(top, rel ? `${rel}.${item}` : item);
} }
else if (curOp === 'bump') { else if (curOp === 'bump') {
for (const item of arr) putBump(top, rel, item); for (const item of arr) putBump(top, rel, Number(item));
} }
stack.pop(); stack.pop();
handledList = true; handledList = true;
@@ -865,7 +812,7 @@ function parseBlock(innerText) {
putDel(top, target); putDel(top, target);
} }
} else if (curOp === 'bump') { } else if (curOp === 'bump') {
putBump(top, rel, stripQ(rhs)); putBump(top, rel, Number(stripQ(rhs)));
} }
continue; continue;
} }
@@ -901,7 +848,7 @@ function parseBlock(innerText) {
} else if (curOp === 'del') { } else if (curOp === 'del') {
putDel(top, rel ? `${rel}.${val}` : val); putDel(top, rel ? `${rel}.${val}` : val);
} else if (curOp === 'bump') { } else if (curOp === 'bump') {
putBump(top, rel, val); putBump(top, rel, Number(val));
} }
} }
} }
@@ -1098,7 +1045,7 @@ function getEffectiveParentNode(p) {
/** /**
* 守护验证 * 守护验证
*/ */
export function guardValidate(op, absPath, payload, currentOverride) { export function guardValidate(op, absPath, payload) {
if (guardianState.bypass) return { allow: true, value: payload }; if (guardianState.bypass) return { allow: true, value: payload };
const p = normalizePath(absPath); const p = normalizePath(absPath);
@@ -1179,19 +1126,10 @@ export function guardValidate(op, absPath, payload, currentOverride) {
// 增量操作 // 增量操作
if (op === 'bump') { if (op === 'bump') {
const effectiveCurrent = (currentOverride !== undefined) ? currentOverride : currentValue; let d = Number(payload);
const computed = computeBumpResult(payload, effectiveCurrent); if (!Number.isFinite(d)) return { allow: false, reason: 'delta-nan' };
if (!computed?.ok) {
if (xbLog.isEnabled?.()) {
try { xbLog.warn(MODULE_ID, `bump payload rejected: path=${p} reason=${computed?.reason || 'delta-nan'}`); } catch {}
}
return { allow: false, reason: computed?.reason || 'delta-nan' };
}
let d = computed.delta; if (currentValue === undefined) {
const base = computed.base;
if (effectiveCurrent === undefined) {
if (parentPath) { if (parentPath) {
const lastSeg = p.split('.').pop() || ''; const lastSeg = p.split('.').pop() || '';
const isIndex = /^\d+$/.test(lastSeg); const isIndex = /^\d+$/.test(lastSeg);
@@ -1214,16 +1152,16 @@ export function guardValidate(op, absPath, payload, currentOverride) {
if (d < -step) d = -step; if (d < -step) d = -step;
} }
const cur = Number(effectiveCurrent); const cur = Number(currentValue);
if (!Number.isFinite(cur)) { if (!Number.isFinite(cur)) {
const baseValue = 0 + d; const base = 0 + d;
const cl = clampNumberWithConstraints(baseValue, node); const cl = clampNumberWithConstraints(base, node);
if (!cl.ok) return { allow: false, reason: 'number-constraint' }; if (!cl.ok) return { allow: false, reason: 'number-constraint' };
setTypeLockIfUnknown(p, cl.value); setTypeLockIfUnknown(p, base);
return { allow: true, value: cl.value }; return { allow: true, value: cl.value };
} }
const next = Number.isFinite(base) ? (base + d) : (cur + d); const next = cur + d;
const clamped = clampNumberWithConstraints(next, node); const clamped = clampNumberWithConstraints(next, node);
if (!clamped.ok) return { allow: false, reason: 'number-constraint' }; if (!clamped.ok) return { allow: false, reason: 'number-constraint' };
return { allow: true, value: clamped.value }; return { allow: true, value: clamped.value };
@@ -1391,7 +1329,11 @@ export function rulesLoadFromTree(valueTree, basePath) {
String(t).trim().startsWith('$') ? String(t).trim() : ('$' + String(t).trim()) String(t).trim().startsWith('$') ? String(t).trim() : ('$' + String(t).trim())
); );
const targetPath = curAbs ? `${curAbs}.${targetToken}` : targetToken; const baseNorm = normalizePath(curAbs || '');
const tokenNorm = normalizePath(targetToken);
const targetPath = (baseNorm && (tokenNorm === baseNorm || tokenNorm.startsWith(baseNorm + '.')))
? tokenNorm
: (curAbs ? `${curAbs}.${targetToken}` : targetToken);
const absPath = normalizePath(targetPath); const absPath = normalizePath(targetPath);
const delta = parseDirectivesTokenList(dirs); const delta = parseDirectivesTokenList(dirs);
@@ -2094,15 +2036,19 @@ async function applyVariablesForMessage(messageId) {
// BUMP 操作 // BUMP 操作
else if (op.operation === 'bump') { else if (op.operation === 'bump') {
for (const [k, delta] of Object.entries(op.data)) { for (const [k, delta] of Object.entries(op.data)) {
const num = Number(delta);
if (!Number.isFinite(num)) continue;
const localPath = joinPath(subPath, k); const localPath = joinPath(subPath, k);
const absPath = localPath ? `${root}.${localPath}` : root; const absPath = localPath ? `${root}.${localPath}` : root;
const stdPath = normalizePath(absPath); const stdPath = normalizePath(absPath);
const deltaList = Array.isArray(delta) ? delta : [delta];
for (const entry of deltaList) { let allow = true;
const spec = parseBumpSpec(entry); let useDelta = num;
if (!spec) continue;
const res = guardValidate('bump', stdPath, num);
allow = !!res?.allow;
if (allow && 'value' in res && Number.isFinite(res.value)) {
let curr; let curr;
try { try {
const pth = norm(localPath || ''); const pth = norm(localPath || '');
@@ -2116,42 +2062,20 @@ async function applyVariablesForMessage(messageId) {
} }
} catch {} } catch {}
const res = guardValidate('bump', stdPath, spec, curr); const baseNum = Number(curr);
const allow = !!res?.allow; const targetNum = Number(res.value);
useDelta = (Number.isFinite(targetNum) ? targetNum : num) - (Number.isFinite(baseNum) ? baseNum : 0);
}
if (!allow) { if (!allow) {
guardDenied++; guardDenied++;
if (debugOn && guardDeniedSamples.length < 8) guardDeniedSamples.push({ op: 'bump', path: stdPath }); if (debugOn && guardDeniedSamples.length < 8) guardDeniedSamples.push({ op: 'bump', path: stdPath });
continue; continue;
} }
const baseNum = Number(curr);
const targetNum = Number(res.value);
const useDelta = Number.isFinite(targetNum)
? (targetNum - (Number.isFinite(baseNum) ? baseNum : 0))
: (spec.kind === 'delta' ? spec.value : NaN);
if (!Number.isFinite(useDelta)) {
guardDenied++;
if (debugOn && guardDeniedSamples.length < 8) guardDeniedSamples.push({ op: 'bump', path: stdPath });
continue;
}
if (debugOn && spec.kind !== 'delta') {
try {
const baseVal = Number.isFinite(baseNum) ? baseNum : 0;
const nextVal = Number.isFinite(targetNum) ? targetNum : (baseVal + useDelta);
const expr = (spec.raw !== undefined && spec.raw !== null && String(spec.raw).trim() !== '')
? String(spec.raw)
: `${spec.kind}:${spec.value}`;
xbLog.info(MODULE_ID, `plot-log bump-math path=${stdPath} base=${baseVal} expr=${expr} next=${nextVal}`);
} catch {}
}
bumpAtPath(rec, norm(localPath || ''), useDelta); bumpAtPath(rec, norm(localPath || ''), useDelta);
} }
} }
} }
}
// 检查是否有变化 // 检查是否有变化
const hasChanges = Array.from(byName.values()).some(rec => rec?.changed === true); const hasChanges = Array.from(byName.values()).some(rec => rec?.changed === true);