fix: update check误判equal version为available update + about设置页满宽

- ElectronUpdaterService加currentVersion选项,checkForUpdates时比较feed版本
- 自包含的isNewerVersion():忽略prerelease/build后缀,等版本->非更新
- AboutSettings去掉max-w-lg/mx-auto约束,面板左右撑满
- 新增4个版本比较回归测试(等版本/旧版本/新版本场景)

Tested: updater.test 14 pass, generalSettings/updateStore 67 pass, lint通过
This commit is contained in:
你的姓名 2026-06-09 16:55:31 +08:00
parent 78ca7070ad
commit f65ae5b2b9
4 changed files with 82 additions and 3 deletions

View File

@ -109,6 +109,7 @@ function getUpdaterService() {
},
}, {
updateConfigPath: !smokeUpdater && app.isPackaged ? path.join(process.resourcesPath, 'app-update.yml') : undefined,
currentVersion: app.getVersion(),
})
return updaterService
}

View File

@ -1,5 +1,5 @@
import { describe, expect, it, vi } from 'vitest'
import { ElectronUpdaterService, normalizeUpdateInfo, type ElectronUpdaterLike } from './updater'
import { ElectronUpdaterService, normalizeUpdateInfo, isNewerVersion, type ElectronUpdaterLike } from './updater'
import { mkdtempSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
@ -51,6 +51,42 @@ describe('Electron updater service', () => {
expect(normalizeUpdateInfo(undefined)).toBeNull()
})
it('compares release versions, ignoring prerelease and build metadata', () => {
expect(isNewerVersion('0.5.6', '0.5.5')).toBe(true)
expect(isNewerVersion('1.0.0', '0.9.9')).toBe(true)
expect(isNewerVersion('0.6.0', '0.5.9')).toBe(true)
// Equal or older must not count as newer (the safe up-to-date direction).
expect(isNewerVersion('0.5.5', '0.5.5')).toBe(false)
expect(isNewerVersion('0.5.4', '0.5.5')).toBe(false)
expect(isNewerVersion('v0.5.5', '0.5.5')).toBe(false)
expect(isNewerVersion('0.5.5+abc123', '0.5.5')).toBe(false)
expect(isNewerVersion('0.5.5-beta.1', '0.5.5')).toBe(false)
})
it('treats a feed version equal to the running version as no update', async () => {
updater.checkForUpdates.mockResolvedValue({ updateInfo: { version: '0.5.5', body: 'Same release' } })
const service = new ElectronUpdaterService(updater, undefined, { currentVersion: '0.5.5' })
await expect(service.checkForUpdates()).resolves.toBeNull()
await expect(service.downloadUpdate(() => {})).rejects.toThrow(
'No Electron update is available to download',
)
})
it('treats a feed version older than the running version as no update', async () => {
updater.checkForUpdates.mockResolvedValue({ updateInfo: { version: '0.5.4' } })
const service = new ElectronUpdaterService(updater, undefined, { currentVersion: '0.5.5' })
await expect(service.checkForUpdates()).resolves.toBeNull()
})
it('surfaces a feed version newer than the running version as an update', async () => {
updater.checkForUpdates.mockResolvedValue({ updateInfo: { version: '0.5.6', body: 'New' } })
const service = new ElectronUpdaterService(updater, undefined, { currentVersion: '0.5.5' })
await expect(service.checkForUpdates()).resolves.toEqual({ version: '0.5.6', body: 'New' })
})
it('keeps electron-updater autoDownload disabled and emits store-compatible progress', async () => {
updater.checkForUpdates.mockResolvedValue({ updateInfo: { version: '1.2.3', body: 'Fixes' } })
updater.downloadUpdate.mockImplementation(async () => {

View File

@ -36,6 +36,12 @@ export type ElectronUpdaterProxyController = {
export type ElectronUpdaterRuntimeOptions = {
updateConfigPath?: string
/**
* Currently running app version. When provided, a feed entry whose version
* is not strictly newer is treated as "no update", so an already-latest
* release is not surfaced as an available update. Omit to skip comparison.
*/
currentVersion?: string
}
export function normalizeUpdateInfo(info: ElectronUpdateInfo | undefined): ElectronUpdateMetadata | null {
@ -49,6 +55,36 @@ export function normalizeUpdateInfo(info: ElectronUpdateInfo | undefined): Elect
}
}
/**
* Parse the numeric X.Y.Z core of a version string, ignoring any prerelease
* (`-beta`) or build metadata (`+sha`) suffix. Missing segments default to 0.
* Electron release versions are clean semver, so a self-contained comparator
* avoids pulling a semver dependency into the electron main-process bundle.
*/
function parseVersionCore(version: string): [number, number, number] {
const core = version.trim().replace(/^v/i, '').split(/[-+]/, 1)[0] ?? ''
const parts = core.split('.')
const toInt = (value: string | undefined) => {
const parsed = Number.parseInt(value ?? '', 10)
return Number.isFinite(parsed) && parsed >= 0 ? parsed : 0
}
return [toInt(parts[0]), toInt(parts[1]), toInt(parts[2])]
}
/**
* Whether `candidate` is a strictly newer release than `current`. Equal or
* older versions return false so an "already latest" feed entry is not
* mislabeled as an available update. Prerelease/build suffixes are ignored:
* an equal numeric core counts as not newer (the safe up-to-date direction).
*/
export function isNewerVersion(candidate: string, current: string): boolean {
const [aMajor, aMinor, aPatch] = parseVersionCore(candidate)
const [bMajor, bMinor, bPatch] = parseVersionCore(current)
if (aMajor !== bMajor) return aMajor > bMajor
if (aMinor !== bMinor) return aMinor > bMinor
return aPatch > bPatch
}
function isMissingUpdateMetadataError(error: unknown): boolean {
if (!error) return false
const maybeError = typeof error === 'object'
@ -73,6 +109,7 @@ export class ElectronUpdaterService {
private readonly updater: ElectronUpdaterLike
private readonly proxyController?: ElectronUpdaterProxyController
private readonly updateConfigPath?: string
private readonly currentVersion?: string
private pendingUpdate: ElectronUpdateMetadata | null = null
private downloaded = false
private proxyKey: string | null = null
@ -85,6 +122,7 @@ export class ElectronUpdaterService {
this.updater = updater
this.proxyController = proxyController
this.updateConfigPath = runtimeOptions.updateConfigPath
this.currentVersion = runtimeOptions.currentVersion
this.updater.autoDownload = false
this.updater.logger = null
}
@ -113,7 +151,11 @@ export class ElectronUpdaterService {
if (!isMissingUpdateMetadataError(error)) throw error
result = null
}
this.pendingUpdate = normalizeUpdateInfo(result?.updateInfo)
const normalized = normalizeUpdateInfo(result?.updateInfo)
this.pendingUpdate =
normalized && this.currentVersion && !isNewerVersion(normalized.version, this.currentVersion)
? null
: normalized
this.downloaded = false
return this.pendingUpdate
}

View File

@ -3934,7 +3934,7 @@ function AboutSettings() {
})()
return (
<div className="w-full min-w-0 max-w-lg mx-auto flex flex-col items-center py-6">
<div className="w-full min-w-0 flex flex-col items-center py-6">
{/* Logo + App Name + Version */}
<img src={publicAssetPath('app-icon.png')} alt="Claude Code Haha" className="w-20 h-20 mb-4" />
<h1 className="text-xl font-bold text-[var(--color-text-primary)]">Claude Code Haha</h1>