diff --git a/src/main/presenter/configPresenter/index.ts b/src/main/presenter/configPresenter/index.ts index fc79b50df..adb7ecb00 100644 --- a/src/main/presenter/configPresenter/index.ts +++ b/src/main/presenter/configPresenter/index.ts @@ -438,7 +438,7 @@ export class ConfigPresenter implements IConfigPresenter { skillsPath: path.join(app.getPath('home'), '.deepchat', 'skills'), enableSkills: true, skillDraftSuggestionsEnabled: false, - updateChannel: 'stable', // Default to stable version + // updateChannel 不预填,首次由 getUpdateChannel() 根据当前应用版本号推断(避免 beta 安装包被默认推入 stable 渠道) appVersion: this.currentAppVersion, hooksNotifications: createDefaultHooksNotificationsConfig(), scheduledTasks: createDefaultScheduledTasksSettings() @@ -2992,12 +2992,15 @@ export class ConfigPresenter implements IConfigPresenter { // 获取更新渠道 getUpdateChannel(): string { - const raw = this.getSetting('updateChannel') || 'stable' - const channel = raw === 'stable' || raw === 'beta' ? raw : 'beta' - if (channel !== raw) { - this.setSetting('updateChannel', channel) - } - return channel + const raw = this.getSetting('updateChannel') + if (raw === 'stable' || raw === 'beta') { + return raw + } + // 首次启动或值非法时,按当前应用版本号推断:含 -alpha/-beta/-rc/-canary 等预发后缀的安装包默认进入 beta 渠道 + const isPrerelease = /-(?:alpha|beta|rc|canary)(?:[.-]\d+)?$/i.test(this.currentAppVersion) + const inferred = isPrerelease ? 'beta' : 'stable' + this.setSetting('updateChannel', inferred) + return inferred } // 设置更新渠道 diff --git a/src/main/presenter/upgradePresenter/index.ts b/src/main/presenter/upgradePresenter/index.ts index f787ece9d..ee28752c0 100644 --- a/src/main/presenter/upgradePresenter/index.ts +++ b/src/main/presenter/upgradePresenter/index.ts @@ -11,6 +11,7 @@ import { presenter } from '@/presenter' import { publishDeepchatEvent } from '@/routes/publishDeepchatEvent' import electronUpdater from 'electron-updater' import type { UpdateInfo } from 'electron-updater' +import { compare } from 'compare-versions' import fs from 'fs' import path from 'path' @@ -21,6 +22,11 @@ const GITHUB_REPO = 'deepchat' const OFFICIAL_DOWNLOAD_URL = 'https://deepchatai.cn/#/download' const UPDATE_CHANNEL_STABLE = 'stable' const UPDATE_CHANNEL_BETA = 'beta' +const PRERELEASE_VERSION_REGEX = /-(?:alpha|beta|rc|canary)(?:[.-]\d+)?$/i + +const isPrereleaseVersion = (version: string): boolean => { + return PRERELEASE_VERSION_REGEX.test(version) +} type ReleaseNoteItem = { version?: string | null @@ -174,6 +180,41 @@ export class UpgradePresenter implements IUpgradePresenter { autoUpdater.on('update-available', (info) => { console.log('检测到新版本', info) this._lock = false + + // 版本号兜底保护:electron-updater 在 channel 错配时可能把当前 beta 安装包"更新"成更旧的正式版。 + // 严格按 semver 判定——只要远端版本 <= 当前版本就拒绝。这里不再单独以"channel 是否同源"为拒绝条件, + // 以免误伤"beta → 同版本号 stable 正式发布"这类合法的渠道收敛升级。 + const currentVersion = app.getVersion() + const remoteVersion = info?.version || '' + + let isDowngradeOrSame = false + try { + if (!remoteVersion) { + isDowngradeOrSame = true + } else if (compare(remoteVersion, currentVersion, '<=')) { + isDowngradeOrSame = true + } + } catch (e) { + console.warn('版本号对比失败,忽略此次更新提示', currentVersion, remoteVersion, e) + isDowngradeOrSame = true + } + + if (isDowngradeOrSame) { + console.log('忽略降级或同版本的更新提示', { + current: currentVersion, + remote: remoteVersion + }) + this._status = 'not-available' + this._error = null + this._progress = null + this._versionInfo = null + this.emitStatusChanged({ + status: this._status, + type: this._lastCheckType + }) + return + } + this._versionInfo = toVersionInfo(info) this._error = null this._progress = null @@ -247,6 +288,19 @@ export class UpgradePresenter implements IUpgradePresenter { return } + // 渠道一致性校验:marker 中的目标版本若与当前安装包不属于同一渠道(beta vs stable), + // 说明上次的"待完成更新"来自渠道错配,应直接丢弃而不是钉死为 previousUpdateFailed + const markerVersion = typeof updateInfo.version === 'string' ? updateInfo.version : '' + if (markerVersion) { + const markerIsPre = isPrereleaseVersion(markerVersion) + const currentIsPre = isPrereleaseVersion(currentVersion) + if (markerIsPre !== currentIsPre) { + console.log('忽略跨渠道的旧 update marker', { marker: markerVersion, currentVersion }) + fs.unlinkSync(this._updateMarkerPath) + return + } + } + // 否则说明上次更新失败,标记为错误状态 console.log('检测到未完成的更新', updateInfo.version) this._status = 'error' diff --git a/test/main/presenter/upgradePresenter.test.ts b/test/main/presenter/upgradePresenter.test.ts index 7d6d44144..34c973ab5 100644 --- a/test/main/presenter/upgradePresenter.test.ts +++ b/test/main/presenter/upgradePresenter.test.ts @@ -10,7 +10,8 @@ const { setApplicationQuittingMock, appQuitMock, appRelaunchMock, - appExitMock + appExitMock, + appGetVersionMock } = vi.hoisted(() => { const autoUpdaterState = { listeners: new Map void>(), @@ -28,13 +29,15 @@ const { setApplicationQuittingMock: vi.fn(), appQuitMock: vi.fn(), appRelaunchMock: vi.fn(), - appExitMock: vi.fn() + appExitMock: vi.fn(), + appGetVersionMock: vi.fn(() => '1.0.0') } }) vi.mock('electron', () => ({ app: { getPath: vi.fn(() => '/tmp/deepchat-test'), + getVersion: appGetVersionMock, quit: appQuitMock, relaunch: appRelaunchMock, exit: appExitMock @@ -100,6 +103,8 @@ describe('UpgradePresenter', () => { appQuitMock.mockReset() appRelaunchMock.mockReset() appExitMock.mockReset() + appGetVersionMock.mockReset() + appGetVersionMock.mockReturnValue('1.0.0') vi.mocked(electronUpdater.autoUpdater.checkForUpdates).mockReset() }) @@ -181,4 +186,59 @@ describe('UpgradePresenter', () => { expect(electronUpdater.autoUpdater.checkForUpdates).toHaveBeenCalledTimes(1) }) + + it('ignores cross-channel downgrades when current install is a prerelease', () => { + appGetVersionMock.mockReturnValue('1.0.5-beta.5') + const configPresenter = { + getUpdateChannel: vi.fn(() => 'stable'), + getPrivacyModeEnabled: vi.fn(() => false) + } as any + + const presenter = new UpgradePresenter(configPresenter) + const handler = autoUpdaterState.listeners.get('update-available') + expect(handler).toBeDefined() + + // 模拟 electron-updater 在 channel 错配下推送的旧正式版 + handler!({ version: '1.0.4', releaseDate: '2026-05-01', releaseNotes: '' }) + + expect((presenter as any)._status).toBe('not-available') + expect((presenter as any)._versionInfo).toBeNull() + // 不应触发自动下载 + expect(electronUpdater.autoUpdater.downloadUpdate).not.toHaveBeenCalled() + }) + + it('accepts in-channel upgrades from one beta to a newer beta', () => { + appGetVersionMock.mockReturnValue('1.0.5-beta.2') + const configPresenter = { + getUpdateChannel: vi.fn(() => 'beta'), + getPrivacyModeEnabled: vi.fn(() => false) + } as any + + const presenter = new UpgradePresenter(configPresenter) + const handler = autoUpdaterState.listeners.get('update-available') + expect(handler).toBeDefined() + + handler!({ version: '1.0.5-beta.5', releaseDate: '2026-05-15', releaseNotes: '' }) + + expect((presenter as any)._status).toBe('available') + expect((presenter as any)._versionInfo?.version).toBe('1.0.5-beta.5') + }) + + it('accepts beta to same-version stable release as a legitimate channel convergence', () => { + // beta 测试完成,1.0.5 正式版发布;用户从 1.0.5-beta.5 升级到 1.0.5 应被允许 + appGetVersionMock.mockReturnValue('1.0.5-beta.5') + const configPresenter = { + getUpdateChannel: vi.fn(() => 'stable'), + getPrivacyModeEnabled: vi.fn(() => false) + } as any + + const presenter = new UpgradePresenter(configPresenter) + const handler = autoUpdaterState.listeners.get('update-available') + expect(handler).toBeDefined() + + handler!({ version: '1.0.5', releaseDate: '2026-06-01', releaseNotes: '' }) + + expect((presenter as any)._status).toBe('available') + expect((presenter as any)._versionInfo?.version).toBe('1.0.5') + }) })