From 5581ccc5d1d33cfe7e263c02ed2f3ba3b64760d4 Mon Sep 17 00:00:00 2001 From: zerob13 Date: Thu, 19 Mar 2026 10:51:58 +0800 Subject: [PATCH 1/7] feat(update): unify update entrypoints --- src/main/events.ts | 3 +- .../hooks/ready/eventListenerSetupHook.ts | 44 +- src/main/presenter/upgradePresenter/index.ts | 17 +- .../settings/components/AboutUsSettings.vue | 209 ++++++---- src/renderer/src/App.vue | 3 - src/renderer/src/components/AppBar.vue | 24 +- .../src/components/ui/UpdateDialog.vue | 95 ----- src/renderer/src/events.ts | 3 +- src/renderer/src/i18n/da-DK/update.json | 4 + src/renderer/src/i18n/en-US/update.json | 4 + src/renderer/src/i18n/fa-IR/update.json | 4 + src/renderer/src/i18n/fr-FR/update.json | 4 + src/renderer/src/i18n/he-IL/update.json | 4 + src/renderer/src/i18n/ja-JP/update.json | 4 + src/renderer/src/i18n/ko-KR/update.json | 4 + src/renderer/src/i18n/pt-BR/update.json | 4 + src/renderer/src/i18n/ru-RU/update.json | 4 + src/renderer/src/i18n/zh-CN/update.json | 4 + src/renderer/src/i18n/zh-HK/update.json | 4 + src/renderer/src/i18n/zh-TW/update.json | 4 + src/renderer/src/stores/upgrade.ts | 392 +++++++++--------- test/renderer/stores/upgradeStore.test.ts | 119 ++++++ 22 files changed, 567 insertions(+), 390 deletions(-) delete mode 100644 src/renderer/src/components/ui/UpdateDialog.vue create mode 100644 test/renderer/stores/upgradeStore.test.ts diff --git a/src/main/events.ts b/src/main/events.ts index d391cf754..435e7eb98 100644 --- a/src/main/events.ts +++ b/src/main/events.ts @@ -119,7 +119,8 @@ export const WINDOW_EVENTS = { // Settings related events export const SETTINGS_EVENTS = { - NAVIGATE: 'settings:navigate' + NAVIGATE: 'settings:navigate', + CHECK_FOR_UPDATES: 'settings:check-for-updates' } // ollama 相关事件 diff --git a/src/main/presenter/lifecyclePresenter/hooks/ready/eventListenerSetupHook.ts b/src/main/presenter/lifecyclePresenter/hooks/ready/eventListenerSetupHook.ts index aa76524b9..d87d045cb 100644 --- a/src/main/presenter/lifecyclePresenter/hooks/ready/eventListenerSetupHook.ts +++ b/src/main/presenter/lifecyclePresenter/hooks/ready/eventListenerSetupHook.ts @@ -7,7 +7,7 @@ import { app } from 'electron' import { optimizer } from '@electron-toolkit/utils' import { LifecycleHook, LifecycleContext } from '@shared/presenter' import { eventBus } from '@/eventbus' -import { WINDOW_EVENTS, TRAY_EVENTS, FLOATING_BUTTON_EVENTS } from '@/events' +import { WINDOW_EVENTS, TRAY_EVENTS, FLOATING_BUTTON_EVENTS, SETTINGS_EVENTS } from '@/events' import { handleShowHiddenWindow } from '@/utils' import { presenter } from '@/presenter' import { LifecyclePhase } from '@shared/lifecycle' @@ -66,16 +66,42 @@ export const eventListenerSetupHook: LifecycleHook = { }) // Tray check for updates - eventBus.on(TRAY_EVENTS.CHECK_FOR_UPDATES, () => { - const allWindows = presenter.windowPresenter.getAllWindows() + eventBus.on(TRAY_EVENTS.CHECK_FOR_UPDATES, async () => { + try { + const settingsWindowId = await presenter.windowPresenter.createSettingsWindow() + if (settingsWindowId == null) { + console.warn('eventListenerSetupHook: Failed to open settings window for update check.') + return + } - // Find target window (focused window or first window) - const targetWindow = presenter.windowPresenter.getFocusedWindow() || allWindows![0] - presenter.windowPresenter.show(targetWindow.id) - targetWindow.focus() // Ensure window is on top + const navigateToAbout = () => { + presenter.windowPresenter.sendToWindow(settingsWindowId, SETTINGS_EVENTS.NAVIGATE, { + routeName: 'settings-about' + }) + } - // Trigger update - presenter.upgradePresenter.checkUpdate() + const triggerUpdateCheck = () => { + presenter.windowPresenter.sendToWindow( + settingsWindowId, + SETTINGS_EVENTS.CHECK_FOR_UPDATES + ) + } + + navigateToAbout() + triggerUpdateCheck() + + setTimeout(() => { + if (presenter.windowPresenter.getSettingsWindowId() === settingsWindowId) { + navigateToAbout() + triggerUpdateCheck() + } + }, 250) + } catch (error) { + console.error( + 'eventListenerSetupHook: Failed to route tray update check to settings window:', + error + ) + } }) // Listen for show/hide window events (triggered from tray or shortcut or floating window) diff --git a/src/main/presenter/upgradePresenter/index.ts b/src/main/presenter/upgradePresenter/index.ts index 737dc5a0e..eabdec253 100644 --- a/src/main/presenter/upgradePresenter/index.ts +++ b/src/main/presenter/upgradePresenter/index.ts @@ -120,6 +120,9 @@ export class UpgradePresenter implements IUpgradePresenter { console.log('无可用更新') this._lock = false this._status = 'not-available' + this._error = null + this._progress = null + this._versionInfo = null eventBus.sendToRenderer(UPDATE_EVENTS.STATUS_CHANGED, SendTarget.ALL_WINDOWS, { status: this._status, type: this._lastCheckType @@ -129,7 +132,10 @@ export class UpgradePresenter implements IUpgradePresenter { // 有可用更新 autoUpdater.on('update-available', (info) => { console.log('检测到新版本', info) + this._lock = false this._versionInfo = toVersionInfo(info) + this._error = null + this._progress = null if (this._previousUpdateFailed) { console.log('上次更新失败,本次不进行自动更新,改为手动更新') @@ -148,8 +154,10 @@ export class UpgradePresenter implements IUpgradePresenter { status: this._status, info: this._versionInfo }) - // 检测到更新后自动开始下载 - this.startDownloadUpdate() + + if (this._lastCheckType === 'autoCheck') { + this.startDownloadUpdate() + } }) // 下载进度 @@ -174,6 +182,7 @@ export class UpgradePresenter implements IUpgradePresenter { console.log('更新下载完成', info) this._lock = false this._status = 'downloaded' + this._error = null if (!this._versionInfo) { this._versionInfo = toVersionInfo(info) @@ -292,7 +301,9 @@ export class UpgradePresenter implements IUpgradePresenter { try { this._status = 'checking' - this._lastCheckType = type + this._error = null + this._progress = null + this._lastCheckType = type ?? 'manualCheck' eventBus.sendToRenderer(UPDATE_EVENTS.STATUS_CHANGED, SendTarget.ALL_WINDOWS, { status: this._status }) diff --git a/src/renderer/settings/components/AboutUsSettings.vue b/src/renderer/settings/components/AboutUsSettings.vue index c7afb4f0d..25652562c 100644 --- a/src/renderer/settings/components/AboutUsSettings.vue +++ b/src/renderer/settings/components/AboutUsSettings.vue @@ -1,17 +1,17 @@ - + - {{ t('update.installNow') }} + {{ upgrade.isRestarting ? t('update.restarting') : t('update.installNow') }} + + + {{ t('update.installUpdate') }} + + + {{ t('settings.about.checking') }} {{ t('about.checkUpdateButton') }} - - - @@ -175,7 +188,7 @@ diff --git a/src/renderer/src/App.vue b/src/renderer/src/App.vue index 8a1794677..9b9ff643a 100644 --- a/src/renderer/src/App.vue +++ b/src/renderer/src/App.vue @@ -1,7 +1,6 @@ diff --git a/src/renderer/src/events.ts b/src/renderer/src/events.ts index 9af09fdc9..03f9f2af3 100644 --- a/src/renderer/src/events.ts +++ b/src/renderer/src/events.ts @@ -75,7 +75,8 @@ export const WINDOW_EVENTS = { // Settings related events export const SETTINGS_EVENTS = { - NAVIGATE: 'settings:navigate' + NAVIGATE: 'settings:navigate', + CHECK_FOR_UPDATES: 'settings:check-for-updates' } // ollama 相关事件 diff --git a/src/renderer/src/i18n/da-DK/update.json b/src/renderer/src/i18n/da-DK/update.json index ac4e65ed5..52a5c14dd 100644 --- a/src/renderer/src/i18n/da-DK/update.json +++ b/src/renderer/src/i18n/da-DK/update.json @@ -9,6 +9,10 @@ "checkUpdate": "Søg efter opdateringer", "downloading": "Downloader", "installNow": "Installer nu", + "installUpdate": "Installer opdatering", + "versionAvailable": "{version} is available", + "autoUpdateFailed": "Automatic update failed. Please use the manual download links below.", + "topbarButton": "Update", "autoUpdate": "Automatisk opdatering", "restarting": "Genstarter", "alreadyUpToDate": "Allerede opdateret", diff --git a/src/renderer/src/i18n/en-US/update.json b/src/renderer/src/i18n/en-US/update.json index ab8b5b475..30f1198a3 100644 --- a/src/renderer/src/i18n/en-US/update.json +++ b/src/renderer/src/i18n/en-US/update.json @@ -9,6 +9,10 @@ "checkUpdate": "Check for Updates", "downloading": "Downloading", "installNow": "Install Now", + "installUpdate": "Install Update", + "versionAvailable": "{version} is available", + "autoUpdateFailed": "Automatic update failed. Please use the manual download links below.", + "topbarButton": "Update", "autoUpdate": "Auto Update", "restarting": "Restarting", "alreadyUpToDate": "Already Up to Date", diff --git a/src/renderer/src/i18n/fa-IR/update.json b/src/renderer/src/i18n/fa-IR/update.json index d86f50cc7..3ac2c90ec 100644 --- a/src/renderer/src/i18n/fa-IR/update.json +++ b/src/renderer/src/i18n/fa-IR/update.json @@ -9,6 +9,10 @@ "checkUpdate": "بررسی به‌روزرسانی‌ها", "downloading": "در حال بارگیری", "installNow": "اکنون نصب کن", + "installUpdate": "Install Update", + "versionAvailable": "{version} is available", + "autoUpdateFailed": "Automatic update failed. Please use the manual download links below.", + "topbarButton": "Update", "autoUpdate": "به‌روزرسانی خودکار", "restarting": "در حال راه‌اندازی دوباره", "alreadyUpToDate": "قبلاً به‌روز شده است", diff --git a/src/renderer/src/i18n/fr-FR/update.json b/src/renderer/src/i18n/fr-FR/update.json index 4c448809f..80d769a69 100644 --- a/src/renderer/src/i18n/fr-FR/update.json +++ b/src/renderer/src/i18n/fr-FR/update.json @@ -10,6 +10,10 @@ "autoUpdate": "Mise à jour automatique", "downloading": "Téléchargement", "installNow": "Installer maintenant", + "installUpdate": "Installer la mise à jour", + "versionAvailable": "{version} is available", + "autoUpdateFailed": "Automatic update failed. Please use the manual download links below.", + "topbarButton": "Update", "restarting": "Redémarrage", "alreadyUpToDate": "Déjà à jour", "alreadyUpToDateDesc": "Votre DeepChat est déjà à jour avec la dernière version, aucune mise à jour n'est nécessaire." diff --git a/src/renderer/src/i18n/he-IL/update.json b/src/renderer/src/i18n/he-IL/update.json index 1ca614070..c3bc1fe18 100644 --- a/src/renderer/src/i18n/he-IL/update.json +++ b/src/renderer/src/i18n/he-IL/update.json @@ -9,6 +9,10 @@ "checkUpdate": "בדוק עדכונים", "downloading": "מוריד", "installNow": "התקן כעת", + "installUpdate": "Install Update", + "versionAvailable": "{version} is available", + "autoUpdateFailed": "Automatic update failed. Please use the manual download links below.", + "topbarButton": "Update", "autoUpdate": "עדכון אוטומטי", "restarting": "מפעיל מחדש", "alreadyUpToDate": "התוכנה מעודכנת", diff --git a/src/renderer/src/i18n/ja-JP/update.json b/src/renderer/src/i18n/ja-JP/update.json index bad3121ce..f556a86fc 100644 --- a/src/renderer/src/i18n/ja-JP/update.json +++ b/src/renderer/src/i18n/ja-JP/update.json @@ -10,6 +10,10 @@ "autoUpdate": "自動更新", "downloading": "ダウンロード", "installNow": "今すぐインストール", + "installUpdate": "アップデートをインストール", + "versionAvailable": "{version} is available", + "autoUpdateFailed": "Automatic update failed. Please use the manual download links below.", + "topbarButton": "Update", "restarting": "再起動", "alreadyUpToDate": "すでに最新です", "alreadyUpToDateDesc": "現在、DeepChatは最新バージョンに更新されており、更新は必要ありません。" diff --git a/src/renderer/src/i18n/ko-KR/update.json b/src/renderer/src/i18n/ko-KR/update.json index 554ac70cc..5e45fc8a4 100644 --- a/src/renderer/src/i18n/ko-KR/update.json +++ b/src/renderer/src/i18n/ko-KR/update.json @@ -10,6 +10,10 @@ "autoUpdate": "자동 업데이트", "downloading": "다운로드", "installNow": "지금 설치", + "installUpdate": "업데이트 설치", + "versionAvailable": "{version} is available", + "autoUpdateFailed": "Automatic update failed. Please use the manual download links below.", + "topbarButton": "Update", "restarting": "다시 시작", "alreadyUpToDate": "이미 최신 버전입니다", "alreadyUpToDateDesc": "현재 DeepChat은 최신 버전으로 업데이트되어 있으며, 업데이트가 필요하지 않습니다." diff --git a/src/renderer/src/i18n/pt-BR/update.json b/src/renderer/src/i18n/pt-BR/update.json index b95fe9f3d..8d4af4b6e 100644 --- a/src/renderer/src/i18n/pt-BR/update.json +++ b/src/renderer/src/i18n/pt-BR/update.json @@ -9,6 +9,10 @@ "checkUpdate": "Verificar Atualizações", "downloading": "Baixando", "installNow": "Instalar Agora", + "installUpdate": "Instalar Atualização", + "versionAvailable": "{version} is available", + "autoUpdateFailed": "Automatic update failed. Please use the manual download links below.", + "topbarButton": "Update", "autoUpdate": "Atualização Automática", "restarting": "Reiniciando", "alreadyUpToDate": "Já Atualizado", diff --git a/src/renderer/src/i18n/ru-RU/update.json b/src/renderer/src/i18n/ru-RU/update.json index e0cf2ac08..418244658 100644 --- a/src/renderer/src/i18n/ru-RU/update.json +++ b/src/renderer/src/i18n/ru-RU/update.json @@ -10,6 +10,10 @@ "autoUpdate": "Автоматическое обновление", "downloading": "Загрузка", "installNow": "Установить", + "installUpdate": "Установить обновление", + "versionAvailable": "{version} is available", + "autoUpdateFailed": "Automatic update failed. Please use the manual download links below.", + "topbarButton": "Update", "restarting": "Перезапуск", "alreadyUpToDate": "Уже обновлено", "alreadyUpToDateDesc": "Ваш DeepChat уже обновлен до последней версии, обновление не требуется." diff --git a/src/renderer/src/i18n/zh-CN/update.json b/src/renderer/src/i18n/zh-CN/update.json index 898f916cf..1a71506c8 100644 --- a/src/renderer/src/i18n/zh-CN/update.json +++ b/src/renderer/src/i18n/zh-CN/update.json @@ -9,6 +9,10 @@ "checkUpdate": "检查更新", "downloading": "正在下载", "installNow": "立即安装", + "installUpdate": "安装更新", + "versionAvailable": "{version} 可用", + "autoUpdateFailed": "自动更新失败,请使用下面的手动下载链接。", + "topbarButton": "更新", "autoUpdate": "自动更新", "restarting": "正在重启", "alreadyUpToDate": "已经是最新版本了", diff --git a/src/renderer/src/i18n/zh-HK/update.json b/src/renderer/src/i18n/zh-HK/update.json index a7ecc0c26..d977b161c 100644 --- a/src/renderer/src/i18n/zh-HK/update.json +++ b/src/renderer/src/i18n/zh-HK/update.json @@ -10,6 +10,10 @@ "autoUpdate": "自動更新", "downloading": "正在下載", "installNow": "立即安裝", + "installUpdate": "安裝更新", + "versionAvailable": "{version} 可用", + "autoUpdateFailed": "自動更新失敗,請使用下方手動下載連結。", + "topbarButton": "更新", "restarting": "正在重啟", "alreadyUpToDate": "已經是最新版本了", "alreadyUpToDateDesc": "您的 DeepChat 目前已經是最新版本,無需更新。" diff --git a/src/renderer/src/i18n/zh-TW/update.json b/src/renderer/src/i18n/zh-TW/update.json index ef3522437..4dfa72919 100644 --- a/src/renderer/src/i18n/zh-TW/update.json +++ b/src/renderer/src/i18n/zh-TW/update.json @@ -10,6 +10,10 @@ "autoUpdate": "自動更新", "downloading": "正在下載", "installNow": "立即安裝", + "installUpdate": "安裝更新", + "versionAvailable": "{version} 可用", + "autoUpdateFailed": "自動更新失敗,請使用下方手動下載連結。", + "topbarButton": "更新", "restarting": "正在重啟", "alreadyUpToDate": "已經是最新版本了", "alreadyUpToDateDesc": "您的 DeepChat 目前已經是最新版本,無需更新。" diff --git a/src/renderer/src/stores/upgrade.ts b/src/renderer/src/stores/upgrade.ts index 3e856b58e..8dab22341 100644 --- a/src/renderer/src/stores/upgrade.ts +++ b/src/renderer/src/stores/upgrade.ts @@ -3,35 +3,154 @@ import { UPDATE_EVENTS } from '@/events' import { defineStore } from 'pinia' import { computed, onMounted, ref } from 'vue' +type PresenterUpdateStatus = + | 'checking' + | 'available' + | 'not-available' + | 'downloading' + | 'downloaded' + | 'error' + | null + +type UpdateState = 'idle' | 'checking' | 'available' | 'downloading' | 'ready_to_install' | 'error' + +type UpdateInfo = { + version: string + releaseDate: string + releaseNotes: string + githubUrl?: string + downloadUrl?: string +} + +type ProgressInfo = { + percent: number + bytesPerSecond: number + transferred: number + total: number +} + +const toUpdateInfo = (info: UpdateInfo | null | undefined): UpdateInfo | null => { + if (!info) return null + + return { + version: info.version, + releaseDate: info.releaseDate, + releaseNotes: info.releaseNotes, + githubUrl: info.githubUrl, + downloadUrl: info.downloadUrl + } +} + export const useUpgradeStore = defineStore('upgrade', () => { const upgradeP = usePresenter('upgradePresenter') const devicePresenter = usePresenter('devicePresenter') - const hasUpdate = ref(false) - const updateInfo = ref<{ - version: string - releaseDate: string - releaseNotes: string - githubUrl?: string - downloadUrl?: string - } | null>(null) - const showUpdateDialog = ref(false) + + const rawStatus = ref(null) + const updateInfo = ref(null) const isUpdating = ref(false) - const isChecking = ref(false) - // 添加在 const updateInfo = ref(null) 附近 - const updateProgress = ref<{ - percent: number - bytesPerSecond: number - transferred: number - total: number - } | null>(null) - const isDownloading = ref(false) - const isReadyToInstall = ref(false) + const updateProgress = ref(null) const isRestarting = ref(false) const updateError = ref(null) - const isSilent = ref(true) // 默认不弹出检查没有最新更新 + const isSilent = ref(true) const platform = ref(null) + const listenersReady = ref(false) + const isWindows = computed(() => platform.value === 'win32') + const hasUpdate = computed(() => Boolean(updateInfo.value)) + + const updateState = computed(() => { + switch (rawStatus.value) { + case 'checking': + return 'checking' + case 'available': + return 'available' + case 'downloading': + return 'downloading' + case 'downloaded': + return 'ready_to_install' + case 'error': + return updateInfo.value ? 'error' : 'idle' + default: + return 'idle' + } + }) + + const isChecking = computed(() => updateState.value === 'checking') + const isDownloading = computed(() => updateState.value === 'downloading') + const isReadyToInstall = computed(() => updateState.value === 'ready_to_install') + const shouldShowUpdateNotes = computed(() => hasUpdate.value) + const shouldShowTopbarInstallButton = computed(() => isReadyToInstall.value) + const showManualDownloadOptions = computed( + () => rawStatus.value === 'error' && Boolean(updateInfo.value) + ) + + const applyStatus = ( + status: PresenterUpdateStatus, + info?: UpdateInfo | null, + error?: string | null + ) => { + rawStatus.value = status + + if (info !== undefined) { + updateInfo.value = toUpdateInfo(info) + } + + if (status === 'checking') { + updateError.value = null + updateProgress.value = null + isRestarting.value = false + return + } + + if (status === 'not-available') { + updateInfo.value = null + updateError.value = null + updateProgress.value = null + isRestarting.value = false + return + } + + if (status === 'available') { + updateError.value = null + updateProgress.value = null + isRestarting.value = false + return + } + + if (status === 'downloading') { + updateError.value = null + isRestarting.value = false + return + } + + if (status === 'downloaded') { + updateError.value = null + isRestarting.value = false + return + } + + if (status === 'error') { + updateError.value = error || '更新出错' + isRestarting.value = false + return + } + } + + const syncFromPresenterStatus = () => { + const status = upgradeP.getUpdateStatus() + applyStatus(status.status, status.updateInfo, status.error) + updateProgress.value = status.progress + ? { + percent: status.progress.percent, + bytesPerSecond: status.progress.bytesPerSecond, + transferred: status.progress.transferred, + total: status.progress.total + } + : null + return status.status + } + const loadDeviceInfo = async () => { try { const deviceInfo = await devicePresenter.getDeviceInfo() @@ -42,38 +161,21 @@ export const useUpgradeStore = defineStore('upgrade', () => { } void loadDeviceInfo() - // 检查更新 + const checkUpdate = async (silent = true) => { isSilent.value = silent - if (isChecking.value) return - isChecking.value = true + if (isChecking.value) return rawStatus.value + try { await upgradeP.checkUpdate() - const status = upgradeP.getUpdateStatus() - hasUpdate.value = status.status === 'available' || status.status === 'downloaded' - if (hasUpdate.value && status.updateInfo) { - updateInfo.value = { - version: status.updateInfo.version, - releaseDate: status.updateInfo.releaseDate, - releaseNotes: status.updateInfo.releaseNotes, - githubUrl: status.updateInfo.githubUrl, - downloadUrl: status.updateInfo.downloadUrl - } - - // 检查是否已经下载完成,只有在下载完成的情况下才打开对话框 - if (status.status === 'downloaded') { - openUpdateDialog() - } - // 否则不打开对话框,让更新在后台静默下载 - } + return syncFromPresenterStatus() } catch (error) { console.error('Failed to check update:', error) - } finally { - isChecking.value = false + applyStatus('error', updateInfo.value, error instanceof Error ? error.message : String(error)) + return 'error' } } - // 开始下载更新 const startUpdate = async (type: 'github' | 'netdisk') => { try { return await upgradeP.goDownloadUpgrade(type) @@ -83,189 +185,97 @@ export const useUpgradeStore = defineStore('upgrade', () => { } } - // 监听更新状态 - const setupUpdateListener = () => { - console.log('setupUpdateListener') - // eslint-disable-next-line @typescript-eslint/no-explicit-any - window.electron.ipcRenderer.on(UPDATE_EVENTS.STATUS_CHANGED, (_, event: any) => { - const { status, type, info, error } = event - console.log(UPDATE_EVENTS.STATUS_CHANGED, status, info, error) - // 根据不同状态更新UI - switch (status) { - case 'available': - hasUpdate.value = true - updateInfo.value = info - ? { - version: info.version, - releaseDate: info.releaseDate, - releaseNotes: info.releaseNotes, - githubUrl: info.githubUrl, - downloadUrl: info.downloadUrl - } - : null - // 不自动弹出对话框,由主进程自动开始下载 - break - case 'not-available': - hasUpdate.value = false - updateInfo.value = null - isDownloading.value = false - isUpdating.value = false - // 当检查到没有更新时,如果是自动检测模式,则不弹出对话框 - if (type !== 'autoCheck') { - openUpdateDialog() - } - break - case 'downloading': - hasUpdate.value = true - isDownloading.value = true - isUpdating.value = true - break - case 'downloaded': - hasUpdate.value = true - isDownloading.value = false - isReadyToInstall.value = true - isUpdating.value = false - if (info) { - updateInfo.value = { - version: info.version, - releaseDate: info.releaseDate, - releaseNotes: info.releaseNotes, - githubUrl: info.githubUrl, - downloadUrl: info.downloadUrl - } - // 下载完成后自动打开安装确认对话框 - openUpdateDialog() - } - break - case 'error': - isDownloading.value = false - isUpdating.value = false - - // 如果有错误,但仍然有更新信息,说明自动更新失败,需要手动下载 - if (info) { - hasUpdate.value = true - updateInfo.value = { - version: info.version, - releaseDate: info.releaseDate, - releaseNotes: info.releaseNotes, - githubUrl: info.githubUrl, - downloadUrl: info.downloadUrl - } - // 自动更新失败,打开手动下载对话框 - openUpdateDialog() - } else { - hasUpdate.value = false - updateInfo.value = null - } - - updateError.value = error || '更新出错' - console.error('Update error:', error) - break - } - }) - - // 监听更新进度 - // eslint-disable-next-line @typescript-eslint/no-explicit-any - window.electron.ipcRenderer.on(UPDATE_EVENTS.PROGRESS, (_, progressData: any) => { - console.log(UPDATE_EVENTS.PROGRESS, progressData) - if (progressData) { - updateProgress.value = { - percent: progressData.percent || 0, - bytesPerSecond: progressData.bytesPerSecond || 0, - transferred: progressData.transferred || 0, - total: progressData.total || 0 - } - } - }) - - // 监听即将重启事件 - window.electron.ipcRenderer.on(UPDATE_EVENTS.WILL_RESTART, () => { - console.log(UPDATE_EVENTS.WILL_RESTART) - isRestarting.value = true - }) - - // 监听更新错误 - // eslint-disable-next-line @typescript-eslint/no-explicit-any - window.electron.ipcRenderer.on(UPDATE_EVENTS.ERROR, (_, errorData: any) => { - console.error(UPDATE_EVENTS.ERROR, errorData.error) - hasUpdate.value = false - updateInfo.value = null - isDownloading.value = false - isUpdating.value = false - updateError.value = errorData.error || '更新出错' - }) - } - - // 打开更新弹窗 - const openUpdateDialog = () => { - // 静默状态下没有更新就别弹了 - if (isSilent.value && !hasUpdate.value) { - return - } - showUpdateDialog.value = true - } - - // 关闭更新弹窗 - const closeUpdateDialog = () => { - showUpdateDialog.value = false - } - - // 处理更新操作 - 修改此方法 const handleUpdate = async (type: 'github' | 'netdisk' | 'auto') => { isUpdating.value = true try { - // 如果更新已下载,执行安装 if (isReadyToInstall.value) { await upgradeP.restartToUpdate() return } - // 如果下载中,不做任何操作 if (isDownloading.value) { return } - // 如果是自动更新模式,启动下载 if (type === 'auto') { const success = await upgradeP.startDownloadUpdate() if (!success) { - // 如果自动更新失败,则使用手动链接 - openUpdateDialog() + applyStatus('error', updateInfo.value, updateError.value) } return } - // 否则进行手动更新 - const success = await startUpdate(type) - if (success) { - closeUpdateDialog() - } + await startUpdate(type) } catch (error) { console.error('Update failed:', error) + applyStatus('error', updateInfo.value, error instanceof Error ? error.message : String(error)) } finally { isUpdating.value = false } } + + const handleStatusChanged = (_: unknown, event: Record) => { + const { status, info, error } = event + applyStatus(status as PresenterUpdateStatus, info, error) + } + + const handleProgress = (_: unknown, progressData: Record) => { + if (!progressData) { + updateProgress.value = null + return + } + + updateProgress.value = { + percent: progressData.percent || 0, + bytesPerSecond: progressData.bytesPerSecond || 0, + transferred: progressData.transferred || 0, + total: progressData.total || 0 + } + } + + const handleWillRestart = () => { + isRestarting.value = true + } + + const handleError = (_: unknown, errorData: Record) => { + applyStatus(updateInfo.value ? 'error' : null, updateInfo.value, errorData?.error || '更新出错') + } + + const setupUpdateListener = () => { + if (listenersReady.value || !window?.electron?.ipcRenderer) { + return + } + + listenersReady.value = true + window.electron.ipcRenderer.on(UPDATE_EVENTS.STATUS_CHANGED, handleStatusChanged) + window.electron.ipcRenderer.on(UPDATE_EVENTS.PROGRESS, handleProgress) + window.electron.ipcRenderer.on(UPDATE_EVENTS.WILL_RESTART, handleWillRestart) + window.electron.ipcRenderer.on(UPDATE_EVENTS.ERROR, handleError) + } + onMounted(() => { setupUpdateListener() + syncFromPresenterStatus() }) + return { - isChecking, - checkUpdate, - startUpdate, - openUpdateDialog, - closeUpdateDialog, - handleUpdate, hasUpdate, updateInfo, - showUpdateDialog, isUpdating, updateProgress, - isDownloading, - isReadyToInstall, isRestarting, updateError, isSilent, - isWindows + isWindows, + updateState, + isChecking, + isDownloading, + isReadyToInstall, + shouldShowUpdateNotes, + shouldShowTopbarInstallButton, + showManualDownloadOptions, + checkUpdate, + startUpdate, + handleUpdate } }) diff --git a/test/renderer/stores/upgradeStore.test.ts b/test/renderer/stores/upgradeStore.test.ts new file mode 100644 index 000000000..32c6608db --- /dev/null +++ b/test/renderer/stores/upgradeStore.test.ts @@ -0,0 +1,119 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { createPinia, setActivePinia } from 'pinia' + +const statusChangedHandlers = vi.hoisted(() => new Map void>()) + +const upgradePresenterMock = vi.hoisted(() => ({ + checkUpdate: vi.fn().mockResolvedValue(undefined), + getUpdateStatus: vi.fn(), + goDownloadUpgrade: vi.fn().mockResolvedValue(undefined), + startDownloadUpdate: vi.fn().mockResolvedValue(true), + restartToUpdate: vi.fn().mockResolvedValue(true) +})) + +const devicePresenterMock = vi.hoisted(() => ({ + getDeviceInfo: vi.fn().mockResolvedValue({ platform: 'darwin' }) +})) + +vi.mock('pinia', async () => { + const actual = await vi.importActual('pinia') + return actual +}) + +vi.mock('vue', async () => { + const actual = await vi.importActual('vue') + return { + ...actual, + onMounted: (callback: () => void) => callback() + } +}) + +vi.mock('@/composables/usePresenter', () => ({ + usePresenter: (name: string) => + name === 'upgradePresenter' ? upgradePresenterMock : devicePresenterMock +})) + +vi.mock('@/events', () => ({ + UPDATE_EVENTS: { + STATUS_CHANGED: 'update:status-changed', + PROGRESS: 'update:progress', + WILL_RESTART: 'update:will-restart', + ERROR: 'update:error' + } +})) + +import { useUpgradeStore } from '@/stores/upgrade' + +const createUpdateInfo = () => ({ + version: '1.0.0-beta.4', + releaseDate: '2026-03-19', + releaseNotes: '- Added floating window', + githubUrl: 'https://github.com/example', + downloadUrl: 'https://download.example.com' +}) + +describe('useUpgradeStore', () => { + beforeEach(() => { + setActivePinia(createPinia()) + vi.clearAllMocks() + statusChangedHandlers.clear() + + upgradePresenterMock.getUpdateStatus.mockReturnValue({ + status: 'not-available', + progress: null, + error: null, + updateInfo: null + }) + + Object.assign(window, { + electron: { + ipcRenderer: { + on: vi.fn((channel: string, handler: (...args: unknown[]) => void) => { + statusChangedHandlers.set(channel, handler) + }) + } + } + }) + }) + + it('keeps manual checks in available state until install is clicked', async () => { + const store = useUpgradeStore() + + upgradePresenterMock.getUpdateStatus.mockReturnValue({ + status: 'available', + progress: null, + error: null, + updateInfo: createUpdateInfo() + }) + + const result = await store.checkUpdate(false) + + expect(result).toBe('available') + expect(store.updateState).toBe('available') + expect(store.hasUpdate).toBe(true) + expect(store.shouldShowTopbarInstallButton).toBe(false) + expect(upgradePresenterMock.startDownloadUpdate).not.toHaveBeenCalled() + }) + + it('shows the topbar entry after the update is downloaded', () => { + const store = useUpgradeStore() + const handler = statusChangedHandlers.get('update:status-changed') + + handler?.({}, { status: 'downloaded', info: createUpdateInfo() }) + + expect(store.updateState).toBe('ready_to_install') + expect(store.shouldShowTopbarInstallButton).toBe(true) + expect(store.hasUpdate).toBe(true) + }) + + it('exposes manual download fallback when update download fails', () => { + const store = useUpgradeStore() + const handler = statusChangedHandlers.get('update:status-changed') + + handler?.({}, { status: 'error', info: createUpdateInfo(), error: 'network failed' }) + + expect(store.updateState).toBe('error') + expect(store.showManualDownloadOptions).toBe(true) + expect(store.updateError).toBe('network failed') + }) +}) From ccfeec37f65b7db022d91ff3734184a46ac83ca8 Mon Sep 17 00:00:00 2001 From: zerob13 Date: Thu, 19 Mar 2026 12:05:47 +0800 Subject: [PATCH 2/7] fix(update): finalize downloaded state --- src/main/presenter/upgradePresenter/index.ts | 62 ++++++++++++++------ 1 file changed, 43 insertions(+), 19 deletions(-) diff --git a/src/main/presenter/upgradePresenter/index.ts b/src/main/presenter/upgradePresenter/index.ts index eabdec253..becf6921a 100644 --- a/src/main/presenter/upgradePresenter/index.ts +++ b/src/main/presenter/upgradePresenter/index.ts @@ -180,24 +180,7 @@ export class UpgradePresenter implements IUpgradePresenter { // 下载完成 autoUpdater.on('update-downloaded', (info) => { console.log('更新下载完成', info) - this._lock = false - this._status = 'downloaded' - this._error = null - - if (!this._versionInfo) { - this._versionInfo = toVersionInfo(info) - } - - // 写入更新标记文件 - this.writeUpdateMarker(this._versionInfo?.version || info.version) - - // 确保保存完整的更新信息 - console.log('使用已保存的版本信息:', this._versionInfo) - - eventBus.sendToRenderer(UPDATE_EVENTS.STATUS_CHANGED, SendTarget.ALL_WINDOWS, { - status: this._status, - info: this._versionInfo // 使用已保存的版本信息 - }) + this.markUpdateDownloaded(info) }) // 监听应用获得焦点事件 @@ -278,6 +261,28 @@ export class UpgradePresenter implements IUpgradePresenter { } } + private markUpdateDownloaded(info?: UpdateInfo): void { + this._lock = false + this._status = 'downloaded' + this._error = null + this._progress = null + + if (!this._versionInfo && info) { + this._versionInfo = toVersionInfo(info) + } + + if (!this._versionInfo) { + console.warn('Downloaded update is missing version info, skipping renderer broadcast.') + return + } + + this.writeUpdateMarker(this._versionInfo.version) + eventBus.sendToRenderer(UPDATE_EVENTS.STATUS_CHANGED, SendTarget.ALL_WINDOWS, { + status: this._status, + info: this._versionInfo + }) + } + // 处理应用获得焦点事件 private handleAppFocus(): void { const now = Date.now() @@ -367,7 +372,26 @@ export class UpgradePresenter implements IUpgradePresenter { status: this._status, info: this._versionInfo // 使用已保存的版本信息 }) - autoUpdater.downloadUpdate() + void autoUpdater + .downloadUpdate() + .then(() => { + if (this._status !== 'downloaded') { + console.log( + 'downloadUpdate resolved before update-downloaded event, applying fallback downloaded status' + ) + this.markUpdateDownloaded() + } + }) + .catch((error: Error | unknown) => { + this._lock = false + this._status = 'error' + this._error = error instanceof Error ? error.message : String(error) + eventBus.sendToRenderer(UPDATE_EVENTS.STATUS_CHANGED, SendTarget.ALL_WINDOWS, { + status: this._status, + error: this._error, + info: this._versionInfo + }) + }) return true } catch (error: Error | unknown) { this._status = 'error' From 520cebbe04016121b9c632fa3375fab2087cedbc Mon Sep 17 00:00:00 2001 From: zerob13 Date: Thu, 19 Mar 2026 12:53:55 +0800 Subject: [PATCH 3/7] fix(update): sync settings status on entry --- .../settings/components/AboutUsSettings.vue | 18 +++++++- src/renderer/src/components/AppBar.vue | 6 +-- src/renderer/src/stores/upgrade.ts | 41 ++++++++++--------- 3 files changed, 42 insertions(+), 23 deletions(-) diff --git a/src/renderer/settings/components/AboutUsSettings.vue b/src/renderer/settings/components/AboutUsSettings.vue index 25652562c..f8ed4628e 100644 --- a/src/renderer/settings/components/AboutUsSettings.vue +++ b/src/renderer/settings/components/AboutUsSettings.vue @@ -188,7 +188,7 @@