From 3780dbadcdd7c65fd2874b69a2f255313cd34a9c Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 16 May 2026 11:06:53 +0000 Subject: [PATCH] fix(kernel): align isLatest with suffix-aware recommended version MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit buildSnapshot used versionGte against KERNEL_TARGET, which only compares the base date triplet and ignores -zh.N. Users on e.g. 2026.5.12-zh.1 were marked latest when the panel target was 2026.5.12-zh.2, hiding the sidebar upgrade hint and showing a false "latest" badge. Add recommendedIsNewer mirroring Rust config.rs logic; isLatest is now !recommendedIsNewer(target, current). Extend kernel tests accordingly. Co-authored-by: 晴天 <1186258278@users.noreply.github.com> --- src/lib/kernel.js | 52 ++++++++++++++++++++++++++++++++++++++++++-- tests/kernel.test.js | 34 ++++++++++++++++++++++------- 2 files changed, 76 insertions(+), 10 deletions(-) diff --git a/src/lib/kernel.js b/src/lib/kernel.js index 6dced96..3ae9ca1 100644 --- a/src/lib/kernel.js +++ b/src/lib/kernel.js @@ -26,7 +26,7 @@ const _listeners = [] * @property {string|null} target 当前推荐目标版本 * @property {string} floor 硬地板版本 * @property {boolean} aboveFloor 是否 >= floor - * @property {boolean} isLatest 是否 >= target + * @property {boolean} isLatest 是否已达推荐目标(含 -zh.N 等后缀小版本,与后端 recommended 语义一致) * @property {Set} features 当前启用的特性 id 集合 * @property {string} versionLabel 人类可读的版本显示,例如 "2026.5.6 汉化" * @property {number|null} protocol 握手协商出的 Gateway WS 协议版本 (3 或 4),未握手为 null @@ -60,6 +60,54 @@ export function versionGte(a, b) { return true } +/** 与 Rust `base_version` 一致:在首个 `-` 处截断 */ +function baseVersionStr(v) { + const s = String(v || '') + const i = s.indexOf('-') + return i === -1 ? s : s.slice(0, i) +} + +function hasVersionSuffix(v) { + return String(v || '').includes('-') +} + +/** 提取字符串中所有数字段,对齐 Rust `parse_version` 对整串的拆分 */ +function allNumericParts(ver) { + return String(ver || '') + .split(/[^\d]+/) + .filter(Boolean) + .map(n => parseInt(n, 10)) + .filter(n => !Number.isNaN(n)) +} + +function compareLex(a, b) { + if (!a?.length || !b?.length) return 0 + const len = Math.max(a.length, b.length) + for (let i = 0; i < len; i++) { + const ai = a[i] || 0 + const bi = b[i] || 0 + if (ai < bi) return -1 + if (ai > bi) return 1 + } + return 0 +} + +/** + * 与 `src-tauri/src/commands/config.rs` 中 `recommended_is_newer` 对齐: + * 在基础 x.y.z 相同的情况下,继续比较 `-zh.N` / `-nightly.N` 等后缀中的数字序。 + */ +export function recommendedIsNewer(recommended, current) { + const r = parseVersion(baseVersionStr(recommended)) + const c = parseVersion(baseVersionStr(current)) + if (!r || !c) return false + const baseCmp = compareLex(r, c) + if (baseCmp !== 0) return baseCmp > 0 + if (hasVersionSuffix(recommended) && hasVersionSuffix(current)) { + return compareLex(allNumericParts(recommended), allNumericParts(current)) > 0 + } + return false +} + /** * 检测版本是否属于汉化版 */ @@ -100,7 +148,7 @@ export function buildSnapshot(engineId, version) { target, floor, aboveFloor: !!version && versionGte(version, floor), - isLatest: !!version && !!target && versionGte(version, target), + isLatest: !!version && !!target && !recommendedIsNewer(target, version), features, versionLabel: version ? `${versionBase}${variant === 'chinese' ? ' 汉化' : ''}` diff --git a/tests/kernel.test.js b/tests/kernel.test.js index a35b60d..d3b3765 100644 --- a/tests/kernel.test.js +++ b/tests/kernel.test.js @@ -9,8 +9,8 @@ import test from 'node:test' import assert from 'node:assert/strict' -import { parseVersion, versionGte, buildSnapshot } from '../src/lib/kernel.js' -import { FEATURE_CATALOG, KERNEL_FLOOR } from '../src/lib/feature-catalog.js' +import { parseVersion, versionGte, buildSnapshot, recommendedIsNewer } from '../src/lib/kernel.js' +import { FEATURE_CATALOG, KERNEL_FLOOR, KERNEL_TARGET } from '../src/lib/feature-catalog.js' // ============================================================================ // parseVersion @@ -160,12 +160,30 @@ test('buildSnapshot edge case: version slightly below 5.6 feature requirement', }) test('buildSnapshot.isLatest works against KERNEL_TARGET', () => { - const at_target = buildSnapshot('openclaw', '2026.5.6') - const above_target = buildSnapshot('openclaw', '2026.6.0') - const below_target = buildSnapshot('openclaw', '2026.5.5') - assert.equal(at_target.isLatest, true) - assert.equal(above_target.isLatest, true) - assert.equal(below_target.isLatest, false) + const officialT = KERNEL_TARGET.openclaw.official + const atOfficial = buildSnapshot('openclaw', officialT) + const aboveOfficial = buildSnapshot('openclaw', '2026.6.0') + const belowOfficial = buildSnapshot('openclaw', '2026.5.5') + assert.equal(atOfficial.isLatest, true) + assert.equal(aboveOfficial.isLatest, true) + assert.equal(belowOfficial.isLatest, false) + + const chin = KERNEL_TARGET.openclaw.chinese + if (chin && /-zh\.\d+/.test(chin)) { + assert.equal(buildSnapshot('openclaw', chin).isLatest, true) + const m = chin.match(/-zh\.(\d+)$/) + if (m && Number(m[1]) > 1) { + const prevZh = chin.replace(/-zh\.\d+$/, `-zh.${Number(m[1]) - 1}`) + assert.equal(buildSnapshot('openclaw', prevZh).isLatest, false) + } + } +}) + +test('recommendedIsNewer matches suffix patch ordering', () => { + assert.equal(recommendedIsNewer('2026.5.12-zh.2', '2026.5.12-zh.1'), true) + assert.equal(recommendedIsNewer('2026.5.12-zh.1', '2026.5.12-zh.2'), false) + assert.equal(recommendedIsNewer('2026.5.12-zh.2', '2026.5.12-zh.2'), false) + assert.equal(recommendedIsNewer('2026.5.13', '2026.5.12-zh.9'), true) }) // ============================================================================