Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 10 additions & 7 deletions src/main/presenter/configPresenter/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -2992,12 +2992,15 @@ export class ConfigPresenter implements IConfigPresenter {

// 获取更新渠道
getUpdateChannel(): string {
const raw = this.getSetting<string>('updateChannel') || 'stable'
const channel = raw === 'stable' || raw === 'beta' ? raw : 'beta'
if (channel !== raw) {
this.setSetting('updateChannel', channel)
}
return channel
const raw = this.getSetting<string>('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
}

// 设置更新渠道
Expand Down
54 changes: 54 additions & 0 deletions src/main/presenter/upgradePresenter/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
}
}
Comment on lines +291 to +302
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Discard same-channel stale markers as well.

A marker for an older version on the same channel still falls through to _previousUpdateFailed. For example, 1.0.4-beta.4 left behind after a manual install of 1.0.5-beta.5 will now block later auto-updates even though the marker is stale.

Suggested fix
         const markerVersion = typeof updateInfo.version === 'string' ? updateInfo.version : ''
         if (markerVersion) {
+          try {
+            if (compare(markerVersion, currentVersion, '<=')) {
+              console.log('忽略过期的 update marker', { marker: markerVersion, currentVersion })
+              fs.unlinkSync(this._updateMarkerPath)
+              return
+            }
+          } catch (e) {
+            console.warn('marker 版本号无效,删除 update marker', markerVersion, e)
+            fs.unlinkSync(this._updateMarkerPath)
+            return
+          }
+
           const markerIsPre = isPrereleaseVersion(markerVersion)
           const currentIsPre = isPrereleaseVersion(currentVersion)
           if (markerIsPre !== currentIsPre) {
             console.log('忽略跨渠道的旧 update marker', { marker: markerVersion, currentVersion })
             fs.unlinkSync(this._updateMarkerPath)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// 渠道一致性校验: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
}
}
// 渠道一致性校验:marker 中的目标版本若与当前安装包不属于同一渠道(beta vs stable),
// 说明上次的"待完成更新"来自渠道错配,应直接丢弃而不是钉死为 previousUpdateFailed
const markerVersion = typeof updateInfo.version === 'string' ? updateInfo.version : ''
if (markerVersion) {
try {
if (compare(markerVersion, currentVersion, '<=')) {
console.log('忽略过期的 update marker', { marker: markerVersion, currentVersion })
fs.unlinkSync(this._updateMarkerPath)
return
}
} catch (e) {
console.warn('marker 版本号无效,删除 update marker', markerVersion, e)
fs.unlinkSync(this._updateMarkerPath)
return
}
const markerIsPre = isPrereleaseVersion(markerVersion)
const currentIsPre = isPrereleaseVersion(currentVersion)
if (markerIsPre !== currentIsPre) {
console.log('忽略跨渠道的旧 update marker', { marker: markerVersion, currentVersion })
fs.unlinkSync(this._updateMarkerPath)
return
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/presenter/upgradePresenter/index.ts` around lines 291 - 302, The
current marker check only discards cross-channel markers; extend it to also
discard stale same-channel markers by comparing semantic versions: when
updateInfo.version (markerVersion) exists and is on the same channel as
currentVersion (isPrereleaseVersion(markerVersion) ===
isPrereleaseVersion(currentVersion)) and markerVersion is less than
currentVersion (use semver.lt / semver.compare), unlink this._updateMarkerPath
and return so the marker does not fall through to setting _previousUpdateFailed;
change the logic in the block that references updateInfo.version,
isPrereleaseVersion, currentVersion and this._updateMarkerPath accordingly.


// 否则说明上次更新失败,标记为错误状态
console.log('检测到未完成的更新', updateInfo.version)
this._status = 'error'
Expand Down
64 changes: 62 additions & 2 deletions test/main/presenter/upgradePresenter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ const {
setApplicationQuittingMock,
appQuitMock,
appRelaunchMock,
appExitMock
appExitMock,
appGetVersionMock
} = vi.hoisted(() => {
const autoUpdaterState = {
listeners: new Map<string, (...args: unknown[]) => void>(),
Expand All @@ -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
Expand Down Expand Up @@ -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()
})

Expand Down Expand Up @@ -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')
})
})