import { defineStore } from 'pinia' import { ref } from 'vue' import { Message } from '@arco-design/web-vue' import { formatBytes as formatFileSize } from '@/utils/fileUtils' import { parseWailsEventData } from '@/utils/wailsEvent' import { CheckUpdate, GetUpdateConfig, DownloadUpdate, InstallUpdate, InstallUpdateWithHash } from '../wailsjs/v3-bindings/u-desk/app' import { Events } from '@wailsio/runtime' const { On, Off } = Events // 本次应用运行内已自动弹过的版本(升级后自动恢复提醒) let sessionNotifiedVersion = '' // 下载完成事件携带的远端声明哈希,安装时用于完整性校验 let lastExpectedSha256 = '' /** * 更新管理 Store * 统一管理版本检查、下载、安装等更新相关逻辑 */ export const useUpdateStore = defineStore('update', () => { // ==================== 状态 ==================== const updateInfo = ref(null) const showUpdate = ref(false) const checking = ref(false) const downloading = ref(false) const installing = ref(false) const downloadProgress = ref(0) const downloadStatus = ref<'active' | 'exception' | 'success'>('active') const progressInfo = ref({ speed: 0, downloaded: 0, total: 0 }) // 节流:防止过度更新 let lastUpdateTime = 0 const UPDATE_THROTTLE = 100 // 100ms 最小更新间隔 // 最小显示时间:确保进度条至少显示 5 秒 let downloadStartTime = 0 const MIN_DISPLAY_TIME = 5000 // 5 秒最小显示时间 // ==================== 工具函数 ==================== const formatSpeed = (bytesPerSecond: number): string => { return formatFileSize(bytesPerSecond) + '/s' } // ==================== 核心方法 ==================== // 用户显式跳过的版本(localStorage):该版本不再自动弹窗,手动检查不受限 const SKIPPED_VERSION_KEY = 'udesk-update-skipped-version' /** * 展示更新弹窗。自动路径(静默检查/后端广播)按版本去重(用户已跳过或本次运行已弹过则不弹),手动检查强制弹 */ const tryShowUpdate = (data: UpdateInfo, force = false) => { const version = String(data?.latest_version || '') if (!force && version) { if (localStorage.getItem(SKIPPED_VERSION_KEY) === version) return if (sessionNotifiedVersion === version) return } updateInfo.value = data showUpdate.value = true if (version) { sessionNotifiedVersion = version } } /** * 稍后提醒:记录用户显式跳过的版本,该版本不再自动弹窗(手动检查仍强制弹) */ const skipVersion = (version: string) => { if (version) { localStorage.setItem(SKIPPED_VERSION_KEY, version) } showUpdate.value = false } /** * 检查更新 * @param silent 是否静默模式(不显示消息) * @param manual 是否手动触发(跳过 auto_check_enabled 门禁) * @returns 检查是否实际完成(false=后端未就绪或网络失败,调用方可重试) */ const checkForUpdates = async (silent = false, manual = false): Promise => { if (checking.value) return true checking.value = true try { const configResult = await GetUpdateConfig() if (!configResult.success) return false const { auto_check_enabled } = configResult.data || {} if (!manual && !auto_check_enabled) return true const result = await CheckUpdate() if (result.success && result.data?.has_update) { tryShowUpdate(result.data, !silent) if (!silent) { Message.success('发现新版本!') } } else if (!silent) { Message.success('已是最新版本') } return result.success === true } catch (error) { if (!silent) { console.error('检查更新失败:', error) Message.error('检查更新失败:' + (error as Error).message) } return false } finally { checking.value = false } } /** * 下载更新 */ const downloadUpdate = async () => { const url = updateInfo.value?.download_url if (!url) { Message.warning('下载地址不存在') return } // 记录开始时间 downloadStartTime = Date.now() // 重置下载状态 downloading.value = true downloadProgress.value = 1 // 设置为 1 而不是 0,确保进度条显示 downloadStatus.value = 'active' progressInfo.value = { speed: 0, downloaded: 0, total: 0 } try { const result = await DownloadUpdate(url) if (!result.success) { downloadStatus.value = 'exception' downloading.value = false Message.error(result.message || '下载启动失败') return } Message.success('下载请求已发送,等待后端发送进度事件...') } catch (error) { console.error('下载失败:', error) downloadStatus.value = 'exception' downloading.value = false Message.error('下载失败:' + (error as Error).message) } } /** * 安装更新 */ const installUpdate = async (filePath: string) => { if (!filePath) { Message.warning('请先下载更新包') return } installing.value = true try { // 优先带哈希校验安装(期望哈希来自下载完成事件的远端声明) const result = lastExpectedSha256 ? await InstallUpdateWithHash(filePath, true, lastExpectedSha256, 'sha256') : await InstallUpdate(filePath, true) if (result.success) { Message.success({ content: '安装成功!应用将在几秒后重启...', duration: 3000 }) return } Message.error(result.message || '安装失败') } catch (error) { Message.error('安装失败:' + (error as Error).message) } finally { installing.value = false } } /** * 下载进度处理 */ const onDownloadProgress = (event: unknown) => { const now = Date.now() if (now - lastUpdateTime < UPDATE_THROTTLE) { return } lastUpdateTime = now const data = parseWailsEventData(event) progressInfo.value = { speed: (data.speed as number) || 0, downloaded: (data.downloaded as number) || 0, total: (data.total as number) || 0 } const rawProgress = Number(data.progress) || 0 const safeProgress = Math.min(100, Math.max(0, Math.round(rawProgress))) downloadProgress.value = safeProgress } /** * 下载完成处理 */ const onDownloadComplete = (event: unknown) => { const data = parseWailsEventData(event) // 错误处理 if (data.error) { console.error('下载失败:', data.error) downloadStatus.value = 'exception' downloading.value = false Message.error('下载失败:' + data.error) return } // 数据验证 if (!data.success || !data.file_path) { console.error('下载数据不完整:', data) downloadStatus.value = 'exception' downloading.value = false Message.error('下载完成但数据不完整') return } // 完成下载 downloadProgress.value = 100 downloadStatus.value = 'success' // 记录远端声明的期望哈希,安装时用于校验 lastExpectedSha256 = String(data.expected_sha256 || '') const fileSize = (data.file_size as number) || 0 progressInfo.value = { speed: 0, downloaded: fileSize, total: fileSize } // 计算已经显示的时间 const elapsed = Date.now() - downloadStartTime const remainingTime = Math.max(0, MIN_DISPLAY_TIME - elapsed) // 确保进度条至少显示 3 秒 setTimeout(() => { downloading.value = false // 安装前才关闭下载状态 installUpdate(data.file_path as string) }, remainingTime) } /** * 后端广播"发现新版本"处理(启动即查与周期检查命中时后端都会发) */ const onUpdateAvailable = (event: unknown) => { const data = parseWailsEventData(event) as UpdateInfo if (!data?.latest_version) { return } tryShowUpdate(data) } /** * 设置事件监听 */ const setupEventListeners = () => { On('download-progress', onDownloadProgress) On('download-complete', onDownloadComplete) On('update-available', onUpdateAvailable) } /** * 移除事件监听 */ const removeEventListeners = () => { Off('download-progress') Off('download-complete') Off('update-available') } // ==================== 返回 ==================== return { // 状态 updateInfo, showUpdate, checking, downloading, installing, downloadProgress, downloadStatus, progressInfo, // 方法 checkForUpdates, downloadUpdate, installUpdate, skipVersion, setupEventListeners, removeEventListeners, formatFileSize, formatSpeed } }) // ==================== 类型定义 ==================== interface UpdateInfo { has_update: boolean current_version: string latest_version: string download_url: string changelog: string force_update: boolean release_date: string file_size: number }