[minor] 新增自动更新功能 模块Test
This commit is contained in:
311
auto_updater.py
Normal file
311
auto_updater.py
Normal file
@@ -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("❌ 重启启动器创建失败")
|
||||||
|
|
||||||
233
main.py
233
main.py
@@ -1324,21 +1324,111 @@ class LoginWindow(QMainWindow):
|
|||||||
return ""
|
return ""
|
||||||
|
|
||||||
def trigger_update(self, download_url, latest_version):
|
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 webbrowser
|
||||||
import threading
|
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(
|
reply = QMessageBox.information(
|
||||||
self,
|
self,
|
||||||
@@ -1354,12 +1444,12 @@ class LoginWindow(QMainWindow):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if reply == QMessageBox.Cancel:
|
if reply == QMessageBox.Cancel:
|
||||||
self.add_log("用户取消下载", "INFO")
|
self.add_log("用户取消手动下载", "INFO")
|
||||||
return
|
return
|
||||||
|
|
||||||
self.add_log(f"📂 准备打开下载页面: {download_url}", "INFO")
|
self.add_log(f"📂 准备打开下载页面: {download_url}", "INFO")
|
||||||
|
|
||||||
# 在独立线程中打开浏览器,确保不阻塞GUI主线程
|
# 在独立线程中打开浏览器
|
||||||
def open_browser_thread():
|
def open_browser_thread():
|
||||||
try:
|
try:
|
||||||
webbrowser.open(download_url)
|
webbrowser.open(download_url)
|
||||||
@@ -1370,7 +1460,120 @@ class LoginWindow(QMainWindow):
|
|||||||
thread = threading.Thread(target=open_browser_thread, daemon=True)
|
thread = threading.Thread(target=open_browser_thread, daemon=True)
|
||||||
thread.start()
|
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():
|
def main():
|
||||||
|
|||||||
@@ -60,6 +60,9 @@ def build_with_command():
|
|||||||
'--add-data=config.py;.',
|
'--add-data=config.py;.',
|
||||||
'--add-data=exe_file_logger.py;.',
|
'--add-data=exe_file_logger.py;.',
|
||||||
'--add-data=windows_taskbar_fix.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/PythonNew32;Utils/PythonNew32',
|
||||||
'--add-data=Utils/JD;Utils/JD',
|
'--add-data=Utils/JD;Utils/JD',
|
||||||
'--add-data=Utils/Dy;Utils/Dy',
|
'--add-data=Utils/Dy;Utils/Dy',
|
||||||
|
|||||||
290
update_dialog.py
Normal file
290
update_dialog.py
Normal file
@@ -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_())
|
||||||
|
|
||||||
Reference in New Issue
Block a user