From bd363d0e3329de03eaeb7282baf1462260063151 Mon Sep 17 00:00:00 2001 From: haosicheng Date: Tue, 28 Oct 2025 15:59:18 +0800 Subject: [PATCH] =?UTF-8?q?[minor]=20=E6=96=B0=E5=A2=9E=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=E5=8A=9F=E8=83=BD=20=E6=A8=A1=E5=9D=97Test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- auto_updater.py | 311 +++++++++++++++++++++++++++++++++++++++++++++++ main.py | 245 +++++++++++++++++++++++++++++++++---- quick_build.py | 3 + update_dialog.py | 290 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 828 insertions(+), 21 deletions(-) create mode 100644 auto_updater.py create mode 100644 update_dialog.py diff --git a/auto_updater.py b/auto_updater.py new file mode 100644 index 0000000..815dcca --- /dev/null +++ b/auto_updater.py @@ -0,0 +1,311 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +自动更新模块 - 支持后台下载、静默安装和自动重启 +优化版:带重试、超时、自动重启功能 +""" + +import os +import sys +import subprocess +import urllib.request +import tempfile +import time +import socket +from pathlib import Path +from PyQt5.QtCore import QThread, pyqtSignal + + +class UpdateDownloader(QThread): + """ + 优化版更新下载器线程 + 特性: + - 自动重试3次 + - 30秒网络超时 + - 文件大小验证 + - 进度实时更新 + """ + + # 信号定义 + progress = pyqtSignal(int, int) # (已下载字节, 总字节) + finished = pyqtSignal(str) # 下载完成,返回文件路径 + error = pyqtSignal(str) # 下载失败,返回错误信息 + retry_info = pyqtSignal(int, int) # 重试信息 (当前重试次数, 总重试次数) + + def __init__(self, download_url, version, max_retries=3): + super().__init__() + self.download_url = download_url + self.version = version + self.max_retries = max_retries # 最大重试次数 + self.is_cancelled = False + + def run(self): + """执行下载(带重试机制)""" + for attempt in range(self.max_retries): + try: + if attempt > 0: + # 显示重试信息 + self.retry_info.emit(attempt + 1, self.max_retries) + time.sleep(2) # 等待2秒后重试 + + # 执行实际下载 + self._download_file() + return # 下载成功,退出 + + except Exception as e: + error_msg = str(e) + print(f"[UpdateDownloader] 下载失败 (尝试 {attempt + 1}/{self.max_retries}): {error_msg}") + + if self.is_cancelled: + # 用户取消,立即退出 + self.error.emit("下载已取消") + return + + if attempt < self.max_retries - 1: + # 还有重试机会,继续 + continue + else: + # 所有重试都失败了 + self.error.emit(f"下载失败(已重试{self.max_retries}次)\n错误: {error_msg}") + + def _download_file(self): + """实际下载逻辑(带超时和验证)""" + # 设置网络超时 + socket.setdefaulttimeout(30) # 30秒超时 + + # 准备临时目录和文件名 + temp_dir = tempfile.gettempdir() + filename = f"ShuiDi_AI_Assistant_Setup_v{self.version}.exe" + file_path = os.path.join(temp_dir, filename) + + print(f"[UpdateDownloader] 开始下载到: {file_path}") + print(f"[UpdateDownloader] 下载地址: {self.download_url}") + + # 删除旧文件(如果存在) + if os.path.exists(file_path): + try: + os.remove(file_path) + print(f"[UpdateDownloader] 已删除旧文件") + except Exception as e: + print(f"[UpdateDownloader] 删除旧文件失败: {e}") + + # 下载文件(带进度回调) + def progress_callback(block_num, block_size, total_size): + if self.is_cancelled: + raise Exception("用户取消下载") + + downloaded = block_num * block_size + # 避免超过总大小 + if total_size > 0 and downloaded > total_size: + downloaded = total_size + + self.progress.emit(downloaded, total_size) + + # 执行下载 + urllib.request.urlretrieve( + self.download_url, + file_path, + reporthook=progress_callback + ) + + # 验证下载的文件 + if not os.path.exists(file_path): + raise Exception("下载的文件不存在") + + file_size = os.path.getsize(file_path) + print(f"[UpdateDownloader] 下载完成,文件大小: {file_size / (1024*1024):.2f} MB") + + # 验证文件大小(至少1MB,避免下载了错误页面) + min_file_size = 1024 * 1024 # 1MB(正式环境) + # 🧪 测试小文件时可以临时改为:min_file_size = 10 * 1024 # 10KB + + if file_size < min_file_size: + raise Exception(f"下载的文件大小异常: {file_size} bytes(可能不是有效的安装包)") + + # 发送完成信号 + self.finished.emit(file_path) + + def cancel(self): + """取消下载""" + print("[UpdateDownloader] 用户取消下载") + self.is_cancelled = True + + +def install_update_and_restart(installer_path): + """ + 启动静默安装并自动重启程序 + + 工作流程: + 1. 启动NSIS安装包(静默模式) + 2. 启动重启启动器(等待安装完成后重启) + 3. 当前程序退出 + + Args: + installer_path: 安装包路径 + + Returns: + bool: 是否成功启动安装 + """ + try: + # 获取当前程序信息 + if getattr(sys, 'frozen', False): + # 打包后的exe + current_exe = sys.executable + current_dir = os.path.dirname(current_exe) + exe_name = os.path.basename(current_exe) + else: + # 开发环境 + current_exe = os.path.abspath("main.py") + current_dir = os.getcwd() + exe_name = "main.exe" + + print(f"[Updater] 当前程序: {current_exe}") + print(f"[Updater] 安装目录: {current_dir}") + print(f"[Updater] 主程序名: {exe_name}") + + # 创建重启启动器脚本 + restart_script_path = create_restart_launcher(current_dir, exe_name) + + # 启动NSIS安装包 + # 参数说明: + # /S - 静默安装 + # /D=目录 - 指定安装目录(必须是最后一个参数) + install_cmd = [ + installer_path, + '/S', # 静默安装 + f'/D={current_dir}' # 安装到当前目录(覆盖) + ] + + print(f"[Updater] 启动安装命令: {' '.join(install_cmd)}") + + # 启动安装进程(独立进程,不受父进程影响) + subprocess.Popen( + install_cmd, + creationflags=subprocess.CREATE_NO_WINDOW | subprocess.DETACHED_PROCESS, + close_fds=True + ) + + print(f"[Updater] ✅ 安装包已启动") + + # 同时启动重启启动器(延迟15秒启动,确保安装完成) + subprocess.Popen( + ['cmd', '/c', restart_script_path], + creationflags=subprocess.CREATE_NO_WINDOW | subprocess.DETACHED_PROCESS, + close_fds=True + ) + + print(f"[Updater] ✅ 重启启动器已启动") + + return True + + except Exception as e: + print(f"[Updater] ❌ 启动安装失败: {e}") + import traceback + traceback.print_exc() + return False + + +def create_restart_launcher(install_dir, exe_name): + """ + 创建重启启动器脚本 + + 该脚本会: + 1. 等待当前GUI进程退出 + 2. 等待NSIS安装完成(15秒) + 3. 启动新版本GUI + 4. 自我删除 + + Args: + install_dir: 安装目录 + exe_name: 主程序文件名 + + Returns: + str: 脚本路径 + """ + script_path = os.path.join(tempfile.gettempdir(), "shuidrop_restart_launcher.bat") + + # 构建批处理脚本 + script_content = f"""@echo off +chcp 65001 > nul +title 水滴AI客服智能助手 - 更新助手 + +echo [%time%] 正在更新水滴AI客服智能助手... +echo. + +REM 等待主程序退出(最多等待10秒) +echo [%time%] 等待主程序退出... +set count=0 +:wait_main_exit +tasklist /FI "IMAGENAME eq {exe_name}" 2>NUL | find /I "{exe_name}" >NUL +if NOT ERRORLEVEL 1 ( + if %count% LSS 10 ( + timeout /t 1 /nobreak > nul + set /a count+=1 + goto wait_main_exit + ) +) +echo [%time%] 主程序已退出 + +REM 等待安装完成(15秒) +echo [%time%] 等待安装完成... +timeout /t 15 /nobreak > nul + +REM 清理旧的安装包 +echo [%time%] 清理临时文件... +del /f /q "%TEMP%\\ShuiDi_AI_Assistant_Setup_*.exe" 2>nul + +REM 启动新版本程序 +echo [%time%] 正在启动新版本... +cd /d "{install_dir}" +if exist "{exe_name}" ( + start "" "{install_dir}\\{exe_name}" + echo [%time%] ✅ 程序已启动 +) else ( + echo [%time%] ❌ 错误: 找不到程序文件 {exe_name} + pause +) + +REM 延迟后删除自己 +timeout /t 2 /nobreak > nul +del /f /q "%~f0" 2>nul +exit +""" + + # 写入脚本文件(UTF-8 BOM编码,避免中文乱码) + try: + with open(script_path, 'w', encoding='utf-8-sig') as f: + f.write(script_content) + print(f"[Updater] 重启启动器已创建: {script_path}") + except Exception as e: + print(f"[Updater] 创建重启启动器失败: {e}") + + return script_path + + +# 兼容性函数(保留旧接口) +def install_update_silently(installer_path): + """ + 启动静默安装(不自动重启) + 兼容旧接口,建议使用 install_update_and_restart + """ + return install_update_and_restart(installer_path) + + +if __name__ == "__main__": + # 测试代码 + print("自动更新模块测试") + print("=" * 50) + + # 测试重启启动器创建 + script_path = create_restart_launcher("C:\\Program Files\\ShuiDi AI Assistant", "main.exe") + print(f"测试脚本路径: {script_path}") + + if os.path.exists(script_path): + print("✅ 重启启动器创建成功") + with open(script_path, 'r', encoding='utf-8-sig') as f: + print("\n脚本内容预览:") + print("-" * 50) + print(f.read()) + else: + print("❌ 重启启动器创建失败") + diff --git a/main.py b/main.py index 7e1f72e..fc6a176 100644 --- a/main.py +++ b/main.py @@ -1324,21 +1324,111 @@ class LoginWindow(QMainWindow): return "" def trigger_update(self, download_url, latest_version): - """触发更新下载(不阻塞主程序)""" + """触发更新下载(支持自动更新和手动下载)""" + try: + # 检查下载地址是否有效 + if not download_url or download_url.strip() == "": + self.add_log("⚠️ 下载地址为空,无法更新", "WARNING") + QMessageBox.warning( + self, + "下载地址缺失", + f"版本 {latest_version} 的下载地址暂未配置。\n\n请联系管理员或稍后再试。", + QMessageBox.Ok + ) + return + + # 获取更新内容 + update_content = self._get_update_content(latest_version) + if update_content: + content_preview = update_content[:100] + "..." if len(update_content) > 100 else update_content + else: + content_preview = "(暂无更新说明)" + + # 🔥 新增:弹出三选一对话框(自动更新/手动下载/取消) + msg_box = QMessageBox(self) + msg_box.setWindowTitle("版本更新") + msg_box.setText(f"发现新版本 v{latest_version},是否更新?") + msg_box.setInformativeText( + f"更新内容:\n{content_preview}\n\n" + f"推荐使用「自动更新」,程序将自动下载并安装新版本。" + ) + msg_box.setIcon(QMessageBox.Information) + + # 添加三个按钮 + auto_button = msg_box.addButton("自动更新", QMessageBox.YesRole) + manual_button = msg_box.addButton("手动下载", QMessageBox.NoRole) + cancel_button = msg_box.addButton("取消", QMessageBox.RejectRole) + msg_box.setDefaultButton(auto_button) + + msg_box.exec_() + clicked_button = msg_box.clickedButton() + + if clicked_button == auto_button: + # 🔥 自动更新:下载并安装 + self.add_log("用户选择自动更新", "INFO") + self.start_auto_update(download_url, latest_version) + elif clicked_button == manual_button: + # 🔥 手动下载:打开浏览器 + self.add_log("用户选择手动下载", "INFO") + self.start_manual_download(download_url, latest_version) + else: + # 取消更新 + self.add_log("用户取消更新", "INFO") + + except Exception as e: + self.add_log(f"更新失败: {e}", "ERROR") + import traceback + self.add_log(f"详细错误: {traceback.format_exc()}", "ERROR") + + def start_auto_update(self, download_url, latest_version): + """启动自动更新流程(下载 → 安装 → 重启)""" + try: + self.add_log("🚀 启动自动更新流程", "INFO") + + # 导入自动更新模块 + from update_dialog import UpdateProgressDialog + from auto_updater import UpdateDownloader + + # 创建进度对话框 + progress_dialog = UpdateProgressDialog(latest_version, self) + + # 创建下载线程 + downloader = UpdateDownloader(download_url, latest_version, max_retries=3) + progress_dialog.downloader = downloader + + # 连接信号 + downloader.progress.connect(progress_dialog.update_progress) + downloader.retry_info.connect(progress_dialog.show_retry_info) + downloader.finished.connect(lambda path: self.on_download_finished(path, progress_dialog)) + downloader.error.connect(lambda err: self.on_download_error(err, progress_dialog, download_url, latest_version)) + + # 开始下载 + self.add_log(f"📥 开始下载: {download_url}", "INFO") + downloader.start() + progress_dialog.exec_() + + except Exception as e: + self.add_log(f"启动自动更新失败: {e}", "ERROR") + import traceback + self.add_log(f"详细错误: {traceback.format_exc()}", "ERROR") + + # 失败后降级到手动下载 + reply = QMessageBox.critical( + self, + "自动更新失败", + f"自动更新启动失败: {str(e)}\n\n是否改为手动下载?", + QMessageBox.Yes | QMessageBox.No, + QMessageBox.Yes + ) + + if reply == QMessageBox.Yes: + self.start_manual_download(download_url, latest_version) + + def start_manual_download(self, download_url, latest_version): + """启动手动下载(打开浏览器)""" import webbrowser import threading - - # 检查下载地址是否有效 - if not download_url or download_url.strip() == "": - self.add_log("⚠️ 下载地址为空,无法打开更新页面", "WARNING") - QMessageBox.warning( - self, - "下载地址缺失", - f"版本 {latest_version} 的下载地址暂未配置。\n\n请联系管理员或稍后再试。", - QMessageBox.Ok - ) - return - + # 明确提示用户即将下载 reply = QMessageBox.information( self, @@ -1352,25 +1442,138 @@ class LoginWindow(QMainWindow): QMessageBox.Ok | QMessageBox.Cancel, QMessageBox.Ok ) - + if reply == QMessageBox.Cancel: - self.add_log("用户取消下载", "INFO") + self.add_log("用户取消手动下载", "INFO") return - + self.add_log(f"📂 准备打开下载页面: {download_url}", "INFO") - - # 在独立线程中打开浏览器,确保不阻塞GUI主线程 + + # 在独立线程中打开浏览器 def open_browser_thread(): try: webbrowser.open(download_url) self.add_log("✅ 浏览器已打开,下载即将开始", "SUCCESS") except Exception as e: self.add_log(f"❌ 打开浏览器失败: {e}", "ERROR") - + thread = threading.Thread(target=open_browser_thread, daemon=True) thread.start() - - self.add_log("✅ 已启动下载,程序继续运行", "INFO") + + self.add_log("✅ 已启动手动下载", "INFO") + + def on_download_finished(self, installer_path, progress_dialog): + """下载完成后的处理""" + try: + self.add_log(f"✅ 下载完成: {installer_path}", "SUCCESS") + + # 更新进度对话框状态 + progress_dialog.download_finished(installer_path) + + # 延迟1秒,让用户看到"下载完成"提示 + QTimer.singleShot(1000, lambda: self.prepare_install(installer_path, progress_dialog)) + + except Exception as e: + self.add_log(f"处理下载完成事件失败: {e}", "ERROR") + + def prepare_install(self, installer_path, progress_dialog): + """准备安装""" + try: + # 关闭进度对话框 + progress_dialog.accept() + + # 提示即将安装 + reply = QMessageBox.information( + self, + "准备安装", + "新版本下载完成!\n\n" + "程序将自动退出并安装新版本,\n" + "安装完成后会自动重启。\n\n" + "是否立即安装?", + QMessageBox.Yes | QMessageBox.No, + QMessageBox.Yes + ) + + if reply == QMessageBox.Yes: + from auto_updater import install_update_and_restart + + # 启动静默安装(带自动重启) + if install_update_and_restart(installer_path): + self.add_log("✅ 更新安装已启动,程序即将退出", "SUCCESS") + self.add_log("程序将在安装完成后自动重启", "INFO") + + # 显示提示 + QMessageBox.information( + self, + "正在更新", + "程序即将退出并开始安装。\n\n" + "安装完成后会自动重启,请稍候...", + QMessageBox.Ok + ) + + # 延迟1秒后退出(让用户看到提示) + QTimer.singleShot(1000, self.quit_for_update) + else: + self.add_log("❌ 启动安装失败", "ERROR") + QMessageBox.critical( + self, + "安装失败", + "无法启动安装程序,请手动运行安装包:\n\n" + installer_path, + QMessageBox.Ok + ) + else: + self.add_log("用户取消安装", "INFO") + QMessageBox.information( + self, + "安装包已保存", + f"安装包已保存到:\n{installer_path}\n\n您可以稍后手动运行安装。", + QMessageBox.Ok + ) + + except Exception as e: + self.add_log(f"准备安装失败: {e}", "ERROR") + import traceback + self.add_log(f"详细错误: {traceback.format_exc()}", "ERROR") + + def on_download_error(self, error_msg, progress_dialog, download_url, latest_version): + """下载失败处理""" + try: + self.add_log(f"❌ 下载失败: {error_msg}", "ERROR") + + # 更新进度对话框状态 + progress_dialog.download_error(error_msg) + + # 延迟2秒后显示降级选项 + QTimer.singleShot(2000, lambda: self.handle_download_failure(progress_dialog, download_url, latest_version, error_msg)) + + except Exception as e: + self.add_log(f"处理下载失败事件异常: {e}", "ERROR") + + def handle_download_failure(self, progress_dialog, download_url, latest_version, error_msg): + """处理下载失败,提供降级选项""" + try: + # 关闭进度对话框 + progress_dialog.reject() + + # 提示用户选择手动下载 + reply = QMessageBox.critical( + self, + "自动下载失败", + f"自动下载失败:\n{error_msg}\n\n是否改为手动下载?", + QMessageBox.Yes | QMessageBox.No, + QMessageBox.Yes + ) + + if reply == QMessageBox.Yes: + self.start_manual_download(download_url, latest_version) + + except Exception as e: + self.add_log(f"处理下载失败异常: {e}", "ERROR") + + def quit_for_update(self): + """为更新而退出程序""" + self.add_log("正在退出程序以进行更新...", "INFO") + self.quit_application() def main(): diff --git a/quick_build.py b/quick_build.py index 5ec1152..2922cd6 100644 --- a/quick_build.py +++ b/quick_build.py @@ -60,6 +60,9 @@ def build_with_command(): '--add-data=config.py;.', '--add-data=exe_file_logger.py;.', '--add-data=windows_taskbar_fix.py;.', + '--add-data=auto_updater.py;.', + '--add-data=update_dialog.py;.', + '--add-data=version_checker.py;.', '--add-data=Utils/PythonNew32;Utils/PythonNew32', '--add-data=Utils/JD;Utils/JD', '--add-data=Utils/Dy;Utils/Dy', diff --git a/update_dialog.py b/update_dialog.py new file mode 100644 index 0000000..7309ab1 --- /dev/null +++ b/update_dialog.py @@ -0,0 +1,290 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +更新进度对话框 +显示下载进度、重试信息和状态提示 +""" + +from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QLabel, + QPushButton, QProgressBar, QHBoxLayout) +from PyQt5.QtCore import Qt +from PyQt5.QtGui import QFont + + +class UpdateProgressDialog(QDialog): + """ + 更新下载进度对话框 + + 特性: + - 实时显示下载进度 + - 显示重试状态 + - 支持用户取消 + - 现代化UI设计 + """ + + def __init__(self, version, parent=None): + super().__init__(parent) + self.version = version + self.downloader = None + self.init_ui() + + def init_ui(self): + """初始化UI""" + self.setWindowTitle("正在更新") + self.setFixedWidth(450) + # 禁止关闭按钮(只能通过取消按钮关闭) + self.setWindowFlags(Qt.Dialog | Qt.WindowTitleHint | Qt.CustomizeWindowHint) + + layout = QVBoxLayout() + layout.setSpacing(15) + layout.setContentsMargins(25, 20, 25, 20) + + # 标题 + self.title_label = QLabel(f"正在下载 v{self.version}") + self.title_label.setFont(QFont('Microsoft YaHei', 12, QFont.Bold)) + self.title_label.setAlignment(Qt.AlignCenter) + layout.addWidget(self.title_label) + + # 进度条 + self.progress_bar = QProgressBar() + self.progress_bar.setMinimum(0) + self.progress_bar.setMaximum(100) + self.progress_bar.setValue(0) + self.progress_bar.setTextVisible(True) + self.progress_bar.setFormat("%p%") + layout.addWidget(self.progress_bar) + + # 状态文本 + self.status_label = QLabel("准备下载...") + self.status_label.setAlignment(Qt.AlignCenter) + self.status_label.setStyleSheet("color: #666; font-size: 10px;") + self.status_label.setMinimumHeight(20) + layout.addWidget(self.status_label) + + # 重试信息标签(默认隐藏) + self.retry_label = QLabel("") + self.retry_label.setAlignment(Qt.AlignCenter) + self.retry_label.setStyleSheet("color: #ff9800; font-size: 10px; font-weight: bold;") + self.retry_label.setMinimumHeight(20) + self.retry_label.hide() + layout.addWidget(self.retry_label) + + # 按钮布局 + button_layout = QHBoxLayout() + button_layout.addStretch() + + self.cancel_button = QPushButton("取消") + self.cancel_button.setFixedWidth(100) + self.cancel_button.clicked.connect(self.cancel_download) + button_layout.addWidget(self.cancel_button) + + button_layout.addStretch() + layout.addLayout(button_layout) + + self.setLayout(layout) + + # 应用样式 + self.apply_styles() + + def apply_styles(self): + """应用现代化样式""" + self.setStyleSheet(""" + QDialog { + background: qlineargradient( + x1:0, y1:0, x2:0, y2:1, + stop:0 #f8fafb, + stop:1 #e8ecef + ); + } + QLabel { + background: transparent; + } + QProgressBar { + border: 2px solid #ddd; + border-radius: 8px; + text-align: center; + height: 28px; + background: white; + font-size: 11px; + font-weight: bold; + } + QProgressBar::chunk { + background: qlineargradient(x1:0, y1:0, x2:1, y2:0, + stop:0 #4285f4, + stop:0.5 #1976d2, + stop:1 #1565c0); + border-radius: 6px; + } + QPushButton { + background: qlineargradient( + x1:0, y1:0, x2:0, y2:1, + stop:0 #f5f5f5, + stop:1 #e0e0e0 + ); + border: 1px solid #ccc; + border-radius: 6px; + padding: 8px 20px; + font-size: 11px; + font-family: 'Microsoft YaHei'; + } + QPushButton:hover { + background: qlineargradient( + x1:0, y1:0, x2:0, y2:1, + stop:0 #e8e8e8, + stop:1 #d0d0d0 + ); + border: 1px solid #999; + } + QPushButton:pressed { + background: #d0d0d0; + } + """) + + def update_progress(self, downloaded, total): + """ + 更新下载进度 + + Args: + downloaded: 已下载字节数 + total: 总字节数 + """ + if total > 0: + # 计算百分比 + progress = int((downloaded / total) * 100) + self.progress_bar.setValue(progress) + + # 转换为MB显示 + downloaded_mb = downloaded / (1024 * 1024) + total_mb = total / (1024 * 1024) + + # 更新状态文本 + self.status_label.setText(f"已下载 {downloaded_mb:.1f} MB / {total_mb:.1f} MB") + + # 更新进度条格式 + self.progress_bar.setFormat(f"%p% ({downloaded_mb:.1f}/{total_mb:.1f} MB)") + else: + # 无法获取总大小时显示不确定进度 + self.progress_bar.setMaximum(0) + self.progress_bar.setFormat("正在下载...") + self.status_label.setText("正在下载,请稍候...") + + def show_retry_info(self, current_retry, max_retries): + """ + 显示重试信息 + + Args: + current_retry: 当前重试次数 + max_retries: 最大重试次数 + """ + self.retry_label.setText(f"⚠️ 下载失败,正在重试... ({current_retry}/{max_retries})") + self.retry_label.show() + + # 重置进度条 + self.progress_bar.setValue(0) + self.status_label.setText("等待重试...") + + # 更新标题 + self.title_label.setText(f"正在重试下载 v{self.version}") + + def download_finished(self, file_path): + """ + 下载完成 + + Args: + file_path: 下载的文件路径 + """ + self.title_label.setText(f"✅ 下载完成 v{self.version}") + self.status_label.setText("下载完成,正在准备安装...") + self.status_label.setStyleSheet("color: #28a745; font-size: 10px; font-weight: bold;") + self.progress_bar.setValue(100) + self.progress_bar.setFormat("100% (完成)") + self.cancel_button.setEnabled(False) + self.retry_label.hide() + + def download_error(self, error_msg): + """ + 下载失败 + + Args: + error_msg: 错误信息 + """ + self.title_label.setText(f"❌ 下载失败") + + # 错误信息截断(避免太长) + if len(error_msg) > 80: + display_msg = error_msg[:80] + "..." + else: + display_msg = error_msg + + self.status_label.setText(display_msg) + self.status_label.setStyleSheet("color: #dc3545; font-size: 10px; font-weight: bold;") + self.cancel_button.setText("关闭") + self.retry_label.hide() + + # 进度条变红 + self.progress_bar.setStyleSheet(""" + QProgressBar { + border: 2px solid #dc3545; + border-radius: 8px; + text-align: center; + height: 28px; + background: white; + } + QProgressBar::chunk { + background: #dc3545; + border-radius: 6px; + } + """) + + def cancel_download(self): + """取消下载""" + if self.downloader and self.downloader.isRunning(): + # 确认取消 + from PyQt5.QtWidgets import QMessageBox + reply = QMessageBox.question( + self, + "确认取消", + "确定要取消下载吗?", + QMessageBox.Yes | QMessageBox.No, + QMessageBox.No + ) + + if reply == QMessageBox.Yes: + self.downloader.cancel() + self.downloader.wait() + self.status_label.setText("下载已取消") + self.reject() + else: + # 下载已完成或出错,直接关闭 + self.reject() + + +if __name__ == "__main__": + # 测试代码 + import sys + from PyQt5.QtWidgets import QApplication + + app = QApplication(sys.argv) + + # 创建测试对话框 + dialog = UpdateProgressDialog("1.5.53") + + # 模拟下载进度 + dialog.show() + + # 测试进度更新 + import time + from PyQt5.QtCore import QTimer + + def test_progress(): + for i in range(0, 101, 10): + dialog.update_progress(i * 1024 * 1024, 100 * 1024 * 1024) + QApplication.processEvents() + time.sleep(0.2) + + dialog.download_finished("/tmp/test.exe") + + QTimer.singleShot(500, test_progress) + + sys.exit(app.exec_()) +