From 5e1d89b3c50c3aa663973f0a236d8094aa074f73 Mon Sep 17 00:00:00 2001 From: QLHazyCoder <2825305047@qq.com> Date: Sat, 18 Apr 2026 17:40:05 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E7=89=88=E6=9C=AC?= =?UTF-8?q?=E6=AF=94=E8=BE=83=E5=92=8C=E6=8E=92=E5=BA=8F=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=EF=BC=8C=E4=BC=98=E5=8C=96=E8=8E=B7=E5=8F=96=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E5=BF=AB=E7=85=A7=E7=9A=84=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sidepanel/update-service.js | 32 +++++-- tests/update-service.test.js | 173 +++++++++++++++++++++++++++++++++++ 项目完整链路说明.md | 3 +- 项目文件结构说明.md | 3 +- 4 files changed, 203 insertions(+), 8 deletions(-) create mode 100644 tests/update-service.test.js diff --git a/sidepanel/update-service.js b/sidepanel/update-service.js index 5487681..e9e78e2 100644 --- a/sidepanel/update-service.js +++ b/sidepanel/update-service.js @@ -184,6 +184,26 @@ }; } + function getComparableReleaseVersion(release) { + const fallbackFamily = release?.family === VERSION_FAMILY_PRO + ? VERSION_FAMILY_PRO + : VERSION_FAMILY_LEGACY; + const displayVersion = String(release?.displayVersion || '').trim(); + if (displayVersion) { + return displayVersion; + } + return formatDisplayVersion(release?.version || '', fallbackFamily); + } + + function sortReleasesByVersion(releases = []) { + return [...releases].sort((left, right) => ( + compareVersions( + getComparableReleaseVersion(right), + getComparableReleaseVersion(left) + ) + )); + } + function readCache() { try { const raw = localStorage.getItem(CACHE_KEY); @@ -200,7 +220,7 @@ return null; } - return parsed.releases; + return sortReleasesByVersion(parsed.releases).slice(0, MAX_RELEASES); } catch (error) { return null; } @@ -243,12 +263,12 @@ const releases = payload .filter((release) => release && !release.draft && !release.prerelease) .map(sanitizeRelease) - .filter(Boolean) - .sort((left, right) => compareVersions(right.version, left.version)) - .slice(0, MAX_RELEASES); + .filter(Boolean); - writeCache(releases); - return releases; + const sortedReleases = sortReleasesByVersion(releases).slice(0, MAX_RELEASES); + + writeCache(sortedReleases); + return sortedReleases; } catch (error) { if (error?.name === 'AbortError') { throw new Error('GitHub Releases 请求超时'); diff --git a/tests/update-service.test.js b/tests/update-service.test.js new file mode 100644 index 0000000..97000cc --- /dev/null +++ b/tests/update-service.test.js @@ -0,0 +1,173 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); + +const source = fs.readFileSync('sidepanel/update-service.js', 'utf8'); + +function createUpdateService(options = {}) { + const manifest = options.manifest || { + version: '2.0', + version_name: 'Pro2.0', + }; + const cache = new Map(); + const windowObject = {}; + let fetchCalls = 0; + + const localStorage = { + getItem(key) { + return cache.has(key) ? cache.get(key) : null; + }, + setItem(key, value) { + cache.set(key, String(value)); + }, + }; + + if (options.cachedSnapshot) { + cache.set( + 'multipage-release-snapshot-v1', + JSON.stringify(options.cachedSnapshot) + ); + } + + const fetchImpl = options.fetchImpl || (async () => ({ + ok: true, + async json() { + return []; + }, + })); + + const wrappedFetch = async (...args) => { + fetchCalls += 1; + return fetchImpl(...args); + }; + + const api = new Function( + 'window', + 'localStorage', + 'fetch', + 'chrome', + 'AbortController', + 'setTimeout', + 'clearTimeout', + `${source}; return window.SidepanelUpdateService;` + )( + windowObject, + localStorage, + wrappedFetch, + { + runtime: { + getManifest() { + return manifest; + }, + }, + }, + AbortController, + setTimeout, + clearTimeout + ); + + return { + api, + getFetchCalls() { + return fetchCalls; + }, + }; +} + +test('getReleaseSnapshot keeps Pro releases ahead of legacy v releases', async () => { + const { api } = createUpdateService({ + manifest: { + version: '2.0', + version_name: 'Pro2.0', + }, + fetchImpl: async () => ({ + ok: true, + async json() { + return [ + { + tag_name: 'v11.0.0', + name: 'v11.0.0', + html_url: 'https://example.com/v11.0.0', + published_at: '2026-04-17T00:00:00.000Z', + body: '- legacy release', + draft: false, + prerelease: false, + }, + { + tag_name: 'Pro2.4', + name: 'Pro2.4', + html_url: 'https://example.com/Pro2.4', + published_at: '2026-04-18T00:00:00.000Z', + body: '- pro release', + draft: false, + prerelease: false, + }, + { + tag_name: 'Pro2.0', + name: 'Pro2.0', + html_url: 'https://example.com/Pro2.0', + published_at: '2026-04-16T00:00:00.000Z', + body: '- current release', + draft: false, + prerelease: false, + }, + ]; + }, + }), + }); + + const snapshot = await api.getReleaseSnapshot({ force: true }); + + assert.equal(snapshot.status, 'update-available'); + assert.equal(snapshot.localVersion, 'Pro2.0'); + assert.equal(snapshot.latestVersion, 'Pro2.4'); + assert.deepEqual( + snapshot.newerReleases.map((release) => release.displayVersion), + ['Pro2.4'] + ); +}); + +test('getReleaseSnapshot reorders cached releases before choosing latest version', async () => { + const { api, getFetchCalls } = createUpdateService({ + manifest: { + version: '2.0', + version_name: 'Pro2.0', + }, + cachedSnapshot: { + fetchedAt: Date.now(), + releases: [ + { + version: '11.0.0', + displayVersion: 'v11.0.0', + family: 'legacy', + title: '', + url: 'https://example.com/v11.0.0', + publishedAt: '2026-04-17T00:00:00.000Z', + notes: [], + }, + { + version: '2.4', + displayVersion: 'Pro2.4', + family: 'pro', + title: '', + url: 'https://example.com/Pro2.4', + publishedAt: '2026-04-18T00:00:00.000Z', + notes: [], + }, + ], + }, + fetchImpl: async () => { + throw new Error('should not fetch when cache is fresh'); + }, + }); + + const snapshot = await api.getReleaseSnapshot(); + + assert.equal(getFetchCalls(), 0); + assert.equal(snapshot.status, 'update-available'); + assert.equal(snapshot.latestVersion, 'Pro2.4'); + assert.deepEqual( + snapshot.newerReleases.map((release) => release.displayVersion), + ['Pro2.4'] + ); +}); diff --git a/项目完整链路说明.md b/项目完整链路说明.md index 070c067..6ae3520 100644 --- a/项目完整链路说明.md +++ b/项目完整链路说明.md @@ -28,7 +28,7 @@ ### 2.1 Sidepanel -[sidepanel/sidepanel.html](c:/Users/projectf/Downloads/codex注册扩展/sidepanel/sidepanel.html) + [sidepanel/sidepanel.js](c:/Users/projectf/Downloads/codex注册扩展/sidepanel/sidepanel.js) +[sidepanel/sidepanel.html](c:/Users/projectf/Downloads/codex注册扩展/sidepanel/sidepanel.html) + [sidepanel/sidepanel.js](c:/Users/projectf/Downloads/codex注册扩展/sidepanel/sidepanel.js) + [sidepanel/update-service.js](c:/Users/projectf/Downloads/codex注册扩展/sidepanel/update-service.js) 职责: @@ -38,6 +38,7 @@ - 接收后台广播并更新 UI - 动态渲染步骤列表 - 在日志区通过“记录”按钮打开独立的邮箱记录覆盖层,并展示成功/失败/停止/重试统计与分页列表 +- 查询 GitHub Releases 并展示更新卡片;当前更新服务会区分 `Pro` 与 legacy `v` 两个版本族,排序时优先保持版本族语义一致,同时会在读取缓存后重新排序,避免旧缓存把 `v` 版本误显示为比 `Pro` 更新 ### 2.2 Background Service Worker diff --git a/项目文件结构说明.md b/项目文件结构说明.md index 8a094ff..940ec68 100644 --- a/项目文件结构说明.md +++ b/项目文件结构说明.md @@ -116,7 +116,7 @@ - `sidepanel/sidepanel.css`:侧边栏样式文件。 - `sidepanel/sidepanel.html`:侧边栏页面结构;当前步骤列表已改为动态容器,日志区提供“记录”按钮并挂接邮箱记录覆盖层,同时新增共享“验证码重发”次数输入,并加载 `managed-alias-utils.js`,把旧的“邮箱前缀”字段语义改为“别名基邮箱”。 - `sidepanel/sidepanel.js`:侧边栏主入口脚本,负责 UI 状态同步、动态步骤渲染、按钮交互、独立本地同步配置、共享验证码自动重发次数配置与广播接收,并装配 Hotmail / iCloud / LuckMail / 邮箱记录面板 manager;同时把 Gmail / 2925 的基邮箱输入、完整注册邮箱输入、自动生成按钮与兼容性校验统一接到共享别名逻辑上。 -- `sidepanel/update-service.js`:侧边栏更新检查服务,负责 GitHub Releases 查询与版本展示。 +- `sidepanel/update-service.js`:侧边栏更新检查服务,负责 GitHub Releases 查询、`Pro` / `v` 双版本族排序、缓存读取与版本展示。 ## `tests/` @@ -179,5 +179,6 @@ - `tests/step9-cpa-mode.test.js`:测试本地 CPA 平台回调跳过策略判断,当前作用于步骤 10。 - `tests/step9-localhost-cleanup-scope.test.js`:测试 localhost callback 与路径前缀残留页的精确清理范围,当前对应步骤 10 收尾。 - `tests/step9-status-diagnostics.test.js`:测试平台回调成功判定、红色错误态过滤,以及成功徽标与失败提示并存时的优先级,当前对应步骤 10。 +- `tests/update-service.test.js`:测试侧边栏更新检查服务在 `Pro` / `v` 混合版本和旧缓存场景下都能按版本族正确排序,不会把 legacy `v` 版本误显示成最新可升级版本。 - `tests/verification-stop-propagation.test.js`:测试验证码流程在 Stop 场景下的错误传播与不中途降级。 - `tests/verification-flow-polling.test.js`:测试 2925 长轮询参数、验证码提交流程中的 `beforeSubmit` 钩子执行顺序,以及步骤 8 提交验证码后进入手机号页时不会误判成功。