好
This commit is contained in:
parent
697104d1ce
commit
e5d5afb1a8
@ -1,65 +1,350 @@
|
|||||||
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QMessageBox
|
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout
|
||||||
from PyQt5.QtCore import Qt, QUrl
|
from PyQt5.QtCore import Qt, QUrl, QTimer
|
||||||
from PyQt5.QtGui import QDesktopServices
|
from PyQt5.QtGui import QDesktopServices
|
||||||
|
|
||||||
from qfluentwidgets import PrimaryPushSettingCard, FluentIcon
|
from qfluentwidgets import (
|
||||||
from qfluentwidgets import InfoBar, InfoBarPosition, SubtitleLabel
|
PrimaryPushSettingCard, FluentIcon, InfoBar, InfoBarPosition,
|
||||||
|
SubtitleLabel, BodyLabel, CaptionLabel, StrongBodyLabel,
|
||||||
|
ElevatedCardWidget, PrimaryPushButton, PushButton,
|
||||||
|
ProgressBar, TextEdit
|
||||||
|
)
|
||||||
|
|
||||||
from .function_fit_interface import FunctionFitInterface
|
from .function_fit_interface import FunctionFitInterface
|
||||||
from app.tools.check_update import check_update
|
from app.tools.check_update import check_update
|
||||||
|
from app.tools.auto_updater import AutoUpdater, check_update_availability
|
||||||
|
|
||||||
__version__ = "1.0.5"
|
__version__ = "1.0.2"
|
||||||
|
|
||||||
class AboutInterface(QWidget):
|
class AboutInterface(QWidget):
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.setObjectName("aboutInterface")
|
self.setObjectName("aboutInterface")
|
||||||
|
|
||||||
|
# 初始化更新相关变量
|
||||||
|
self.updater = None
|
||||||
|
self.update_info = None
|
||||||
|
|
||||||
|
self._setup_ui()
|
||||||
|
|
||||||
|
def _setup_ui(self):
|
||||||
|
"""设置用户界面"""
|
||||||
layout = QVBoxLayout(self)
|
layout = QVBoxLayout(self)
|
||||||
layout.setAlignment(Qt.AlignTop)
|
layout.setAlignment(Qt.AlignmentFlag.AlignTop)
|
||||||
layout.setContentsMargins(20, 30, 20, 20) # 添加边距
|
layout.setContentsMargins(20, 30, 20, 20)
|
||||||
|
|
||||||
|
# 页面标题
|
||||||
title = SubtitleLabel("MRobot 帮助页面", self)
|
title = SubtitleLabel("MRobot 帮助页面", self)
|
||||||
title.setAlignment(Qt.AlignCenter)
|
title.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
layout.addWidget(title)
|
layout.addWidget(title)
|
||||||
# 添加空间隔
|
|
||||||
layout.addSpacing(10)
|
layout.addSpacing(10)
|
||||||
|
|
||||||
card = PrimaryPushSettingCard(
|
# 版本信息卡片 - 学习AI界面风格
|
||||||
text="检查更新",
|
version_card = ElevatedCardWidget()
|
||||||
icon=FluentIcon.DOWNLOAD,
|
version_layout = QVBoxLayout(version_card)
|
||||||
title="更新",
|
version_layout.setContentsMargins(24, 20, 24, 20)
|
||||||
content=f"MRobot_Toolbox 当前版本:{__version__}",
|
|
||||||
)
|
version_title = StrongBodyLabel("版本信息")
|
||||||
card.clicked.connect(self.on_check_update_clicked)
|
version_layout.addWidget(version_title)
|
||||||
layout.addWidget(card)
|
|
||||||
|
current_version_label = BodyLabel(f"当前版本:v{__version__}")
|
||||||
|
version_layout.addWidget(current_version_label)
|
||||||
|
|
||||||
def on_check_update_clicked(self):
|
|
||||||
|
layout.addWidget(version_card)
|
||||||
|
|
||||||
|
# 检查更新按钮
|
||||||
|
self.check_update_card = PrimaryPushSettingCard(
|
||||||
|
text="检查更新",
|
||||||
|
icon=FluentIcon.SYNC,
|
||||||
|
title="检查更新",
|
||||||
|
content="检查是否有新版本可用",
|
||||||
|
)
|
||||||
|
self.check_update_card.clicked.connect(self.check_for_updates)
|
||||||
|
layout.addWidget(self.check_update_card)
|
||||||
|
|
||||||
|
# 更新信息卡片(初始隐藏)
|
||||||
|
self.update_info_card = ElevatedCardWidget()
|
||||||
|
self.update_info_card.hide()
|
||||||
|
self._setup_update_info_card()
|
||||||
|
layout.addWidget(self.update_info_card)
|
||||||
|
|
||||||
|
layout.addStretch()
|
||||||
|
|
||||||
|
def _setup_update_info_card(self):
|
||||||
|
"""设置更新信息卡片"""
|
||||||
|
layout = QVBoxLayout(self.update_info_card)
|
||||||
|
layout.setContentsMargins(24, 20, 24, 20)
|
||||||
|
layout.setSpacing(16)
|
||||||
|
|
||||||
|
# 标题
|
||||||
|
self.update_title = StrongBodyLabel("发现新版本")
|
||||||
|
layout.addWidget(self.update_title)
|
||||||
|
|
||||||
|
# 版本对比
|
||||||
|
version_layout = QHBoxLayout()
|
||||||
|
|
||||||
|
current_layout = QVBoxLayout()
|
||||||
|
current_layout.addWidget(CaptionLabel("当前版本"))
|
||||||
|
self.current_version_label = SubtitleLabel(f"v{__version__}")
|
||||||
|
current_layout.addWidget(self.current_version_label)
|
||||||
|
|
||||||
|
arrow_label = SubtitleLabel("→")
|
||||||
|
arrow_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
arrow_label.setFixedWidth(30)
|
||||||
|
|
||||||
|
latest_layout = QVBoxLayout()
|
||||||
|
latest_layout.addWidget(CaptionLabel("最新版本"))
|
||||||
|
self.latest_version_label = SubtitleLabel("v--")
|
||||||
|
latest_layout.addWidget(self.latest_version_label)
|
||||||
|
|
||||||
|
version_layout.addLayout(current_layout)
|
||||||
|
version_layout.addWidget(arrow_label)
|
||||||
|
version_layout.addLayout(latest_layout)
|
||||||
|
|
||||||
|
layout.addLayout(version_layout)
|
||||||
|
|
||||||
|
# 更新信息
|
||||||
|
info_layout = QHBoxLayout()
|
||||||
|
self.file_size_label = BodyLabel("文件大小: --")
|
||||||
|
self.release_date_label = BodyLabel("发布时间: --")
|
||||||
|
|
||||||
|
info_layout.addWidget(self.file_size_label)
|
||||||
|
info_layout.addStretch()
|
||||||
|
info_layout.addWidget(self.release_date_label)
|
||||||
|
|
||||||
|
layout.addLayout(info_layout)
|
||||||
|
|
||||||
|
# 更新说明
|
||||||
|
layout.addWidget(CaptionLabel("更新说明:"))
|
||||||
|
|
||||||
|
self.notes_display = TextEdit()
|
||||||
|
self.notes_display.setReadOnly(True)
|
||||||
|
self.notes_display.setFixedHeight(200)
|
||||||
|
self.notes_display.setText("暂无更新说明")
|
||||||
|
layout.addWidget(self.notes_display)
|
||||||
|
|
||||||
|
# 进度条(初始隐藏)
|
||||||
|
self.progress_widget = QWidget()
|
||||||
|
progress_layout = QVBoxLayout(self.progress_widget)
|
||||||
|
|
||||||
|
self.progress_label = BodyLabel("准备更新...")
|
||||||
|
progress_layout.addWidget(self.progress_label)
|
||||||
|
|
||||||
|
self.progress_bar = ProgressBar()
|
||||||
|
self.progress_bar.setRange(0, 100)
|
||||||
|
self.progress_bar.setValue(0)
|
||||||
|
progress_layout.addWidget(self.progress_bar)
|
||||||
|
|
||||||
|
self.progress_widget.hide()
|
||||||
|
layout.addWidget(self.progress_widget)
|
||||||
|
|
||||||
|
# 按钮区域
|
||||||
|
button_layout = QHBoxLayout()
|
||||||
|
|
||||||
|
self.manual_btn = PushButton("手动下载")
|
||||||
|
self.manual_btn.setIcon(FluentIcon.LINK)
|
||||||
|
self.manual_btn.clicked.connect(self.open_manual_download)
|
||||||
|
|
||||||
|
self.update_btn = PrimaryPushButton("开始更新")
|
||||||
|
self.update_btn.setIcon(FluentIcon.DOWNLOAD)
|
||||||
|
self.update_btn.clicked.connect(self.start_update)
|
||||||
|
|
||||||
|
self.cancel_btn = PushButton("取消")
|
||||||
|
self.cancel_btn.clicked.connect(self.cancel_update)
|
||||||
|
|
||||||
|
button_layout.addWidget(self.manual_btn)
|
||||||
|
button_layout.addStretch()
|
||||||
|
button_layout.addWidget(self.update_btn)
|
||||||
|
button_layout.addWidget(self.cancel_btn)
|
||||||
|
|
||||||
|
layout.addLayout(button_layout)
|
||||||
|
|
||||||
|
def check_for_updates(self):
|
||||||
|
"""检查更新"""
|
||||||
|
self.check_update_card.setEnabled(False)
|
||||||
|
self.check_update_card.setContent("正在检查更新...")
|
||||||
|
|
||||||
|
# 延迟执行检查,避免阻塞UI
|
||||||
|
QTimer.singleShot(100, self._perform_check)
|
||||||
|
|
||||||
|
def _perform_check(self):
|
||||||
|
"""执行更新检查"""
|
||||||
try:
|
try:
|
||||||
latest = check_update(__version__)
|
self.update_info = check_update_availability(__version__)
|
||||||
if latest:
|
|
||||||
# 直接用浏览器打开下载链接
|
if self.update_info:
|
||||||
QDesktopServices.openUrl(QUrl("https://github.com/goldenfishs/MRobot/releases/latest"))
|
self._show_update_available()
|
||||||
InfoBar.success(
|
else:
|
||||||
title="发现新版本",
|
self._show_no_update()
|
||||||
content=f"检测到新版本:{latest},已为你打开下载页面。",
|
|
||||||
parent=self,
|
except Exception as e:
|
||||||
position=InfoBarPosition.TOP,
|
self._show_error(f"检查更新失败: {str(e)}")
|
||||||
duration=5000
|
|
||||||
)
|
def _show_update_available(self):
|
||||||
elif latest is None:
|
"""显示发现更新"""
|
||||||
InfoBar.info(
|
# 更新按钮状态
|
||||||
title="已是最新版本",
|
self.check_update_card.setEnabled(True)
|
||||||
content="当前已是最新版本,无需更新。",
|
self.check_update_card.setContent("发现新版本!")
|
||||||
parent=self,
|
|
||||||
position=InfoBarPosition.TOP,
|
# 显示更新信息卡片
|
||||||
duration=3000
|
self.update_info_card.show()
|
||||||
)
|
|
||||||
except Exception:
|
# 设置版本信息
|
||||||
InfoBar.error(
|
if self.update_info:
|
||||||
title="检查更新失败",
|
version = self.update_info.get('version', 'Unknown')
|
||||||
content="无法获取最新版本,请检查网络连接。",
|
self.latest_version_label.setText(f"v{version}")
|
||||||
parent=self,
|
|
||||||
position=InfoBarPosition.TOP,
|
# 设置文件信息
|
||||||
duration=4000
|
asset_size = self.update_info.get('asset_size', 0)
|
||||||
)
|
file_size = self._format_file_size(asset_size)
|
||||||
|
self.file_size_label.setText(f"文件大小: {file_size}")
|
||||||
|
|
||||||
|
# 设置发布时间
|
||||||
|
release_date = self.update_info.get('release_date', '')
|
||||||
|
formatted_date = self._format_date(release_date)
|
||||||
|
self.release_date_label.setText(f"发布时间: {formatted_date}")
|
||||||
|
|
||||||
|
# 设置更新说明
|
||||||
|
notes = self.update_info.get('release_notes', '暂无更新说明')
|
||||||
|
self.notes_display.setText(notes[:500] + ('...' if len(notes) > 500 else ''))
|
||||||
|
|
||||||
|
InfoBar.success(
|
||||||
|
title="发现新版本",
|
||||||
|
content=f"检测到新版本 v{version}",
|
||||||
|
parent=self,
|
||||||
|
position=InfoBarPosition.TOP,
|
||||||
|
duration=3000
|
||||||
|
)
|
||||||
|
|
||||||
|
def _show_no_update(self):
|
||||||
|
"""显示无更新"""
|
||||||
|
self.check_update_card.setEnabled(True)
|
||||||
|
self.check_update_card.setContent("已是最新版本")
|
||||||
|
|
||||||
|
InfoBar.info(
|
||||||
|
title="已是最新版本",
|
||||||
|
content="当前已是最新版本,无需更新。",
|
||||||
|
parent=self,
|
||||||
|
position=InfoBarPosition.TOP,
|
||||||
|
duration=3000
|
||||||
|
)
|
||||||
|
|
||||||
|
def _show_error(self, error_msg: str):
|
||||||
|
"""显示错误"""
|
||||||
|
self.check_update_card.setEnabled(True)
|
||||||
|
self.check_update_card.setContent("检查更新失败")
|
||||||
|
|
||||||
|
InfoBar.error(
|
||||||
|
title="检查更新失败",
|
||||||
|
content=error_msg,
|
||||||
|
parent=self,
|
||||||
|
position=InfoBarPosition.TOP,
|
||||||
|
duration=4000
|
||||||
|
)
|
||||||
|
|
||||||
|
def start_update(self):
|
||||||
|
"""开始更新"""
|
||||||
|
if not self.update_info:
|
||||||
|
return
|
||||||
|
|
||||||
|
# 显示进度UI
|
||||||
|
self.progress_widget.show()
|
||||||
|
self.update_btn.setEnabled(False)
|
||||||
|
self.manual_btn.setEnabled(False)
|
||||||
|
|
||||||
|
# 启动更新器
|
||||||
|
self.updater = AutoUpdater(__version__)
|
||||||
|
self.updater.signals.progress_changed.connect(self.update_progress)
|
||||||
|
self.updater.signals.status_changed.connect(self.update_status)
|
||||||
|
self.updater.signals.error_occurred.connect(self.update_error)
|
||||||
|
self.updater.signals.update_completed.connect(self.update_completed)
|
||||||
|
|
||||||
|
# 开始更新流程
|
||||||
|
self.updater.start()
|
||||||
|
|
||||||
|
def update_progress(self, value: int):
|
||||||
|
"""更新进度"""
|
||||||
|
self.progress_bar.setValue(value)
|
||||||
|
|
||||||
|
def update_status(self, status: str):
|
||||||
|
"""更新状态"""
|
||||||
|
self.progress_label.setText(status)
|
||||||
|
|
||||||
|
def update_error(self, error_msg: str):
|
||||||
|
"""更新错误"""
|
||||||
|
self.progress_widget.hide()
|
||||||
|
self.update_btn.setEnabled(True)
|
||||||
|
self.manual_btn.setEnabled(True)
|
||||||
|
|
||||||
|
InfoBar.error(
|
||||||
|
title="更新失败",
|
||||||
|
content=error_msg,
|
||||||
|
parent=self,
|
||||||
|
position=InfoBarPosition.TOP,
|
||||||
|
duration=4000
|
||||||
|
)
|
||||||
|
|
||||||
|
def update_completed(self):
|
||||||
|
"""更新完成"""
|
||||||
|
self.progress_label.setText("更新完成,准备重启...")
|
||||||
|
self.progress_bar.setValue(100)
|
||||||
|
|
||||||
|
InfoBar.success(
|
||||||
|
title="更新完成",
|
||||||
|
content="更新安装完成,程序将在3秒后重启",
|
||||||
|
parent=self,
|
||||||
|
position=InfoBarPosition.TOP,
|
||||||
|
duration=3000
|
||||||
|
)
|
||||||
|
|
||||||
|
# 延迟重启
|
||||||
|
QTimer.singleShot(3000, self._restart_app)
|
||||||
|
|
||||||
|
def cancel_update(self):
|
||||||
|
"""取消更新"""
|
||||||
|
if self.updater and self.updater.isRunning():
|
||||||
|
self.updater.cancel_update()
|
||||||
|
|
||||||
|
self.update_info_card.hide()
|
||||||
|
self.check_update_card.setContent("检查是否有新版本可用")
|
||||||
|
|
||||||
|
def open_manual_download(self):
|
||||||
|
"""打开手动下载页面"""
|
||||||
|
QDesktopServices.openUrl(QUrl("https://github.com/goldenfishs/MRobot/releases/latest"))
|
||||||
|
|
||||||
|
InfoBar.info(
|
||||||
|
title="手动下载",
|
||||||
|
content="已为您打开下载页面",
|
||||||
|
parent=self,
|
||||||
|
position=InfoBarPosition.TOP,
|
||||||
|
duration=2000
|
||||||
|
)
|
||||||
|
|
||||||
|
def _restart_app(self):
|
||||||
|
"""重启应用程序"""
|
||||||
|
if self.updater:
|
||||||
|
self.updater.restart_application()
|
||||||
|
|
||||||
|
def _format_file_size(self, size_bytes: int) -> str:
|
||||||
|
"""格式化文件大小"""
|
||||||
|
if size_bytes == 0:
|
||||||
|
return "--"
|
||||||
|
|
||||||
|
size = float(size_bytes)
|
||||||
|
for unit in ['B', 'KB', 'MB', 'GB']:
|
||||||
|
if size < 1024.0:
|
||||||
|
return f"{size:.1f} {unit}"
|
||||||
|
size /= 1024.0
|
||||||
|
return f"{size:.1f} TB"
|
||||||
|
|
||||||
|
def _format_date(self, date_str: str) -> str:
|
||||||
|
"""格式化日期"""
|
||||||
|
if not date_str:
|
||||||
|
return "--"
|
||||||
|
|
||||||
|
try:
|
||||||
|
from datetime import datetime
|
||||||
|
date_obj = datetime.fromisoformat(date_str.replace('Z', '+00:00'))
|
||||||
|
return date_obj.strftime('%Y-%m-%d')
|
||||||
|
except:
|
||||||
|
return date_str[:10] if len(date_str) >= 10 else date_str
|
||||||
@ -25,9 +25,8 @@ class MainWindow(FluentWindow):
|
|||||||
self.initInterface()
|
self.initInterface()
|
||||||
self.initNavigation()
|
self.initNavigation()
|
||||||
|
|
||||||
# 检查更新
|
# 后台检查更新(不弹窗,只显示通知)
|
||||||
# checkUpdate(self, flag=True)
|
# self.check_updates_in_background()
|
||||||
# checkAnnouncement(self) # 检查公告
|
|
||||||
|
|
||||||
def initWindow(self):
|
def initWindow(self):
|
||||||
self.setMicaEffectEnabled(False)
|
self.setMicaEffectEnabled(False)
|
||||||
@ -74,6 +73,14 @@ class MainWindow(FluentWindow):
|
|||||||
None,
|
None,
|
||||||
NavigationItemPosition.BOTTOM
|
NavigationItemPosition.BOTTOM
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def check_updates_in_background(self):
|
||||||
|
"""后台检查更新"""
|
||||||
|
try:
|
||||||
|
# 后台更新检查已移至关于页面手动触发
|
||||||
|
pass
|
||||||
|
except Exception as e:
|
||||||
|
print(f"初始化完成: {e}")
|
||||||
|
|
||||||
# main_window.py 只需修改关闭事件
|
# main_window.py 只需修改关闭事件
|
||||||
def closeEvent(self, e):
|
def closeEvent(self, e):
|
||||||
|
|||||||
340
app/tools/auto_updater.py
Normal file
340
app/tools/auto_updater.py
Normal file
@ -0,0 +1,340 @@
|
|||||||
|
"""
|
||||||
|
自动更新模块
|
||||||
|
实现软件的自动更新功能,包括下载、解压、安装等完整流程
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import shutil
|
||||||
|
import tempfile
|
||||||
|
import zipfile
|
||||||
|
import subprocess
|
||||||
|
import platform
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional, Callable
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from packaging.version import parse as vparse
|
||||||
|
from PyQt5.QtCore import QThread, pyqtSignal, QObject
|
||||||
|
|
||||||
|
|
||||||
|
class UpdaterSignals(QObject):
|
||||||
|
"""更新器信号类"""
|
||||||
|
progress_changed = pyqtSignal(int) # 进度变化信号 (0-100)
|
||||||
|
status_changed = pyqtSignal(str) # 状态变化信号
|
||||||
|
error_occurred = pyqtSignal(str) # 错误信号
|
||||||
|
update_completed = pyqtSignal() # 更新完成信号
|
||||||
|
update_cancelled = pyqtSignal() # 更新取消信号
|
||||||
|
|
||||||
|
|
||||||
|
class AutoUpdater(QThread):
|
||||||
|
"""自动更新器类"""
|
||||||
|
|
||||||
|
def __init__(self, current_version: str, repo: str = "goldenfishs/MRobot"):
|
||||||
|
super().__init__()
|
||||||
|
self.current_version = current_version
|
||||||
|
self.repo = repo
|
||||||
|
self.signals = UpdaterSignals()
|
||||||
|
self.cancelled = False
|
||||||
|
|
||||||
|
# 获取当前程序信息
|
||||||
|
self.is_frozen = getattr(sys, 'frozen', False)
|
||||||
|
self.app_dir = self._get_app_directory()
|
||||||
|
self.temp_dir = None
|
||||||
|
|
||||||
|
def _get_app_directory(self) -> str:
|
||||||
|
"""获取应用程序目录"""
|
||||||
|
if self.is_frozen:
|
||||||
|
# 如果是打包的exe,返回exe所在目录
|
||||||
|
return os.path.dirname(sys.executable)
|
||||||
|
else:
|
||||||
|
# 如果是Python脚本,返回项目根目录
|
||||||
|
return os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
|
||||||
|
|
||||||
|
def cancel_update(self):
|
||||||
|
"""取消更新"""
|
||||||
|
self.cancelled = True
|
||||||
|
self.signals.update_cancelled.emit()
|
||||||
|
|
||||||
|
def check_for_updates(self) -> Optional[dict]:
|
||||||
|
"""检查是否有新版本可用"""
|
||||||
|
try:
|
||||||
|
self.signals.status_changed.emit("正在检查更新...")
|
||||||
|
|
||||||
|
url = f"https://api.github.com/repos/{self.repo}/releases/latest"
|
||||||
|
response = requests.get(url, timeout=10)
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
release_data = response.json()
|
||||||
|
latest_version = release_data["tag_name"].lstrip("v")
|
||||||
|
|
||||||
|
if vparse(latest_version) > vparse(self.current_version):
|
||||||
|
return {
|
||||||
|
'version': latest_version,
|
||||||
|
'download_url': self._get_download_url(release_data),
|
||||||
|
'release_notes': release_data.get('body', ''),
|
||||||
|
'release_date': release_data.get('published_at', ''),
|
||||||
|
'asset_name': self._get_asset_name(release_data)
|
||||||
|
}
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
raise Exception(f"GitHub API请求失败: {response.status_code}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.signals.error_occurred.emit(f"检查更新失败: {str(e)}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _get_download_url(self, release_data: dict) -> Optional[str]:
|
||||||
|
"""从release数据中获取适合当前平台的下载链接"""
|
||||||
|
assets = release_data.get('assets', [])
|
||||||
|
system = platform.system().lower()
|
||||||
|
|
||||||
|
# 根据操作系统选择合适的安装包
|
||||||
|
for asset in assets:
|
||||||
|
name = asset['name'].lower()
|
||||||
|
|
||||||
|
if system == 'windows':
|
||||||
|
if 'installer' in name and name.endswith('.exe'):
|
||||||
|
return asset['browser_download_url']
|
||||||
|
if name.endswith('.exe') or name.endswith('.zip'):
|
||||||
|
return asset['browser_download_url']
|
||||||
|
elif system == 'darwin': # macOS
|
||||||
|
if name.endswith('.dmg') or name.endswith('.zip'):
|
||||||
|
return asset['browser_download_url']
|
||||||
|
elif system == 'linux':
|
||||||
|
if name.endswith('.tar.gz') or name.endswith('.zip'):
|
||||||
|
return asset['browser_download_url']
|
||||||
|
|
||||||
|
# 如果没找到特定平台的,返回第一个可用文件
|
||||||
|
for asset in assets:
|
||||||
|
name = asset['name'].lower()
|
||||||
|
if any(name.endswith(ext) for ext in ['.zip', '.exe', '.dmg', '.tar.gz']):
|
||||||
|
return asset['browser_download_url']
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _get_asset_name(self, release_data: dict) -> Optional[str]:
|
||||||
|
"""获取资源文件名"""
|
||||||
|
download_url = self._get_download_url(release_data)
|
||||||
|
if download_url:
|
||||||
|
return os.path.basename(urlparse(download_url).path)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def download_update(self, download_url: str, filename: str) -> Optional[str]:
|
||||||
|
"""下载更新文件"""
|
||||||
|
try:
|
||||||
|
self.signals.status_changed.emit("正在下载更新...")
|
||||||
|
|
||||||
|
# 创建临时目录
|
||||||
|
self.temp_dir = tempfile.mkdtemp(prefix="MRobot_update_")
|
||||||
|
file_path = os.path.join(self.temp_dir, filename)
|
||||||
|
|
||||||
|
response = requests.get(download_url, stream=True, timeout=30)
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
total_size = int(response.headers.get('content-length', 0))
|
||||||
|
downloaded_size = 0
|
||||||
|
|
||||||
|
with open(file_path, 'wb') as f:
|
||||||
|
for chunk in response.iter_content(chunk_size=8192):
|
||||||
|
if self.cancelled:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if chunk:
|
||||||
|
f.write(chunk)
|
||||||
|
downloaded_size += len(chunk)
|
||||||
|
|
||||||
|
if total_size > 0:
|
||||||
|
progress = int((downloaded_size / total_size) * 50) # 下载占50%进度
|
||||||
|
self.signals.progress_changed.emit(progress)
|
||||||
|
|
||||||
|
self.signals.status_changed.emit("下载完成")
|
||||||
|
return file_path
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.signals.error_occurred.emit(f"下载失败: {str(e)}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def extract_update(self, file_path: str) -> Optional[str]:
|
||||||
|
"""解压更新文件"""
|
||||||
|
try:
|
||||||
|
self.signals.status_changed.emit("正在解压文件...")
|
||||||
|
self.signals.progress_changed.emit(50)
|
||||||
|
|
||||||
|
if not self.temp_dir:
|
||||||
|
raise Exception("临时目录未初始化")
|
||||||
|
|
||||||
|
extract_dir = os.path.join(self.temp_dir, "extracted")
|
||||||
|
os.makedirs(extract_dir, exist_ok=True)
|
||||||
|
|
||||||
|
# 根据文件扩展名选择解压方法
|
||||||
|
if file_path.endswith('.zip'):
|
||||||
|
with zipfile.ZipFile(file_path, 'r') as zip_ref:
|
||||||
|
zip_ref.extractall(extract_dir)
|
||||||
|
elif file_path.endswith('.tar.gz'):
|
||||||
|
import tarfile
|
||||||
|
with tarfile.open(file_path, 'r:gz') as tar_ref:
|
||||||
|
tar_ref.extractall(extract_dir)
|
||||||
|
else:
|
||||||
|
raise Exception(f"不支持的文件格式: {file_path}")
|
||||||
|
|
||||||
|
self.signals.progress_changed.emit(70)
|
||||||
|
self.signals.status_changed.emit("解压完成")
|
||||||
|
return extract_dir
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.signals.error_occurred.emit(f"解压失败: {str(e)}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def install_update(self, extract_dir: str) -> bool:
|
||||||
|
"""安装更新"""
|
||||||
|
try:
|
||||||
|
self.signals.status_changed.emit("正在安装更新...")
|
||||||
|
self.signals.progress_changed.emit(80)
|
||||||
|
|
||||||
|
if not self.temp_dir:
|
||||||
|
raise Exception("临时目录未初始化")
|
||||||
|
|
||||||
|
# 创建备份目录
|
||||||
|
backup_dir = os.path.join(self.temp_dir, "backup")
|
||||||
|
os.makedirs(backup_dir, exist_ok=True)
|
||||||
|
|
||||||
|
# 备份当前程序文件
|
||||||
|
self._backup_current_files(backup_dir)
|
||||||
|
|
||||||
|
# 复制新文件
|
||||||
|
self._copy_update_files(extract_dir)
|
||||||
|
|
||||||
|
self.signals.progress_changed.emit(95)
|
||||||
|
self.signals.status_changed.emit("安装完成")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.signals.error_occurred.emit(f"安装失败: {str(e)}")
|
||||||
|
# 尝试恢复备份
|
||||||
|
self._restore_backup(backup_dir)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _backup_current_files(self, backup_dir: str):
|
||||||
|
"""备份当前程序文件"""
|
||||||
|
important_files = ['MRobot.py', 'MRobot.exe', 'app/', 'assets/']
|
||||||
|
|
||||||
|
for item in important_files:
|
||||||
|
src_path = os.path.join(self.app_dir, item)
|
||||||
|
if os.path.exists(src_path):
|
||||||
|
dst_path = os.path.join(backup_dir, item)
|
||||||
|
if os.path.isdir(src_path):
|
||||||
|
shutil.copytree(src_path, dst_path, dirs_exist_ok=True)
|
||||||
|
else:
|
||||||
|
os.makedirs(os.path.dirname(dst_path), exist_ok=True)
|
||||||
|
shutil.copy2(src_path, dst_path)
|
||||||
|
|
||||||
|
def _copy_update_files(self, extract_dir: str):
|
||||||
|
"""复制更新文件到应用程序目录"""
|
||||||
|
# 查找解压目录中的主要文件/文件夹
|
||||||
|
extract_contents = os.listdir(extract_dir)
|
||||||
|
|
||||||
|
# 如果解压后只有一个文件夹,进入该文件夹
|
||||||
|
if len(extract_contents) == 1 and os.path.isdir(os.path.join(extract_dir, extract_contents[0])):
|
||||||
|
extract_dir = os.path.join(extract_dir, extract_contents[0])
|
||||||
|
|
||||||
|
# 复制文件到应用程序目录
|
||||||
|
for item in os.listdir(extract_dir):
|
||||||
|
src_path = os.path.join(extract_dir, item)
|
||||||
|
dst_path = os.path.join(self.app_dir, item)
|
||||||
|
|
||||||
|
if os.path.isdir(src_path):
|
||||||
|
if os.path.exists(dst_path):
|
||||||
|
shutil.rmtree(dst_path)
|
||||||
|
shutil.copytree(src_path, dst_path)
|
||||||
|
else:
|
||||||
|
shutil.copy2(src_path, dst_path)
|
||||||
|
|
||||||
|
def _restore_backup(self, backup_dir: str):
|
||||||
|
"""恢复备份文件"""
|
||||||
|
try:
|
||||||
|
for item in os.listdir(backup_dir):
|
||||||
|
src_path = os.path.join(backup_dir, item)
|
||||||
|
dst_path = os.path.join(self.app_dir, item)
|
||||||
|
|
||||||
|
if os.path.isdir(src_path):
|
||||||
|
if os.path.exists(dst_path):
|
||||||
|
shutil.rmtree(dst_path)
|
||||||
|
shutil.copytree(src_path, dst_path)
|
||||||
|
else:
|
||||||
|
shutil.copy2(src_path, dst_path)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"恢复备份失败: {e}")
|
||||||
|
|
||||||
|
def restart_application(self):
|
||||||
|
"""重启应用程序"""
|
||||||
|
try:
|
||||||
|
self.signals.status_changed.emit("正在重启应用程序...")
|
||||||
|
|
||||||
|
if self.is_frozen:
|
||||||
|
# 如果是打包的exe
|
||||||
|
executable = sys.executable
|
||||||
|
else:
|
||||||
|
# 如果是Python脚本
|
||||||
|
executable = sys.executable
|
||||||
|
script_path = os.path.join(self.app_dir, "MRobot.py")
|
||||||
|
|
||||||
|
# 启动新进程
|
||||||
|
if platform.system() == 'Windows':
|
||||||
|
subprocess.Popen([executable] + ([script_path] if not self.is_frozen else []))
|
||||||
|
else:
|
||||||
|
subprocess.Popen([executable] + ([script_path] if not self.is_frozen else []))
|
||||||
|
|
||||||
|
# 退出当前进程
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.signals.error_occurred.emit(f"重启失败: {str(e)}")
|
||||||
|
|
||||||
|
def cleanup(self):
|
||||||
|
"""清理临时文件"""
|
||||||
|
if self.temp_dir and os.path.exists(self.temp_dir):
|
||||||
|
try:
|
||||||
|
shutil.rmtree(self.temp_dir)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"清理临时文件失败: {e}")
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
"""执行更新流程"""
|
||||||
|
try:
|
||||||
|
# 检查更新
|
||||||
|
update_info = self.check_for_updates()
|
||||||
|
if not update_info or self.cancelled:
|
||||||
|
return
|
||||||
|
|
||||||
|
# 下载更新
|
||||||
|
downloaded_file = self.download_update(
|
||||||
|
update_info['download_url'],
|
||||||
|
update_info['asset_name']
|
||||||
|
)
|
||||||
|
if not downloaded_file or self.cancelled:
|
||||||
|
return
|
||||||
|
|
||||||
|
# 解压更新
|
||||||
|
extract_dir = self.extract_update(downloaded_file)
|
||||||
|
if not extract_dir or self.cancelled:
|
||||||
|
return
|
||||||
|
|
||||||
|
# 安装更新
|
||||||
|
if self.install_update(extract_dir) and not self.cancelled:
|
||||||
|
self.signals.progress_changed.emit(100)
|
||||||
|
self.signals.update_completed.emit()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.signals.error_occurred.emit(f"更新过程中发生错误: {str(e)}")
|
||||||
|
finally:
|
||||||
|
# 清理临时文件
|
||||||
|
self.cleanup()
|
||||||
|
|
||||||
|
|
||||||
|
def check_update_availability(current_version: str, repo: str = "goldenfishs/MRobot") -> Optional[dict]:
|
||||||
|
"""快速检查是否有新版本可用(不下载)"""
|
||||||
|
updater = AutoUpdater(current_version, repo)
|
||||||
|
return updater.check_for_updates()
|
||||||
48
check_releases.py
Normal file
48
check_releases.py
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
检查GitHub Releases API响应结构
|
||||||
|
"""
|
||||||
|
|
||||||
|
import requests
|
||||||
|
import json
|
||||||
|
|
||||||
|
def check_releases_structure():
|
||||||
|
"""检查GitHub releases的API响应结构"""
|
||||||
|
try:
|
||||||
|
url = "https://api.github.com/repos/goldenfishs/MRobot/releases/latest"
|
||||||
|
response = requests.get(url, timeout=10)
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
data = response.json()
|
||||||
|
|
||||||
|
print("Release信息:")
|
||||||
|
print(f"标签: {data.get('tag_name')}")
|
||||||
|
print(f"名称: {data.get('name')}")
|
||||||
|
print(f"发布时间: {data.get('published_at')}")
|
||||||
|
print(f"是否为预发布: {data.get('prerelease')}")
|
||||||
|
print(f"是否为草稿: {data.get('draft')}")
|
||||||
|
|
||||||
|
print("\n可用的资源文件:")
|
||||||
|
assets = data.get('assets', [])
|
||||||
|
|
||||||
|
if not assets:
|
||||||
|
print("❌ 没有找到任何资源文件")
|
||||||
|
print("建议在GitHub Release中上传安装包文件")
|
||||||
|
else:
|
||||||
|
for i, asset in enumerate(assets):
|
||||||
|
print(f" {i+1}. {asset['name']}")
|
||||||
|
print(f" 大小: {asset['size']} 字节")
|
||||||
|
print(f" 下载链接: {asset['browser_download_url']}")
|
||||||
|
print(f" 内容类型: {asset.get('content_type', 'unknown')}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
print(f"\n更新说明:\n{data.get('body', '无')}")
|
||||||
|
|
||||||
|
else:
|
||||||
|
print(f"❌ API请求失败,状态码: {response.status_code}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ 检查失败: {e}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
check_releases_structure()
|
||||||
114
demo_auto_update.py
Normal file
114
demo_auto_update.py
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
自动更新功能演示
|
||||||
|
展示如何使用自动更新功能的完整示例
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
# 添加项目路径
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
|
||||||
|
|
||||||
|
from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QPushButton, QLabel
|
||||||
|
from PyQt5.QtCore import Qt
|
||||||
|
|
||||||
|
def demo_auto_update():
|
||||||
|
"""演示自动更新功能"""
|
||||||
|
from app.tools.update_dialog import UpdateDialog
|
||||||
|
|
||||||
|
app = QApplication(sys.argv)
|
||||||
|
|
||||||
|
# 创建主窗口
|
||||||
|
window = QWidget()
|
||||||
|
window.setWindowTitle("自动更新演示")
|
||||||
|
window.setFixedSize(300, 200)
|
||||||
|
|
||||||
|
layout = QVBoxLayout(window)
|
||||||
|
|
||||||
|
# 标题
|
||||||
|
title = QLabel("MRobot 自动更新演示")
|
||||||
|
title.setAlignment(Qt.AlignCenter)
|
||||||
|
title.setStyleSheet("font-size: 16px; font-weight: bold; margin: 20px;")
|
||||||
|
layout.addWidget(title)
|
||||||
|
|
||||||
|
# 当前版本显示
|
||||||
|
version_label = QLabel("当前版本: v1.0.0")
|
||||||
|
version_label.setAlignment(Qt.AlignCenter)
|
||||||
|
layout.addWidget(version_label)
|
||||||
|
|
||||||
|
# 更新按钮
|
||||||
|
update_btn = QPushButton("检查并更新")
|
||||||
|
update_btn.clicked.connect(lambda: show_update_dialog(window))
|
||||||
|
layout.addWidget(update_btn)
|
||||||
|
|
||||||
|
# 说明
|
||||||
|
info_label = QLabel("点击按钮体验自动更新功能")
|
||||||
|
info_label.setAlignment(Qt.AlignCenter)
|
||||||
|
info_label.setStyleSheet("color: gray; font-size: 12px;")
|
||||||
|
layout.addWidget(info_label)
|
||||||
|
|
||||||
|
window.show()
|
||||||
|
|
||||||
|
def show_update_dialog(parent):
|
||||||
|
"""显示更新对话框"""
|
||||||
|
dialog = UpdateDialog("1.0.0", parent)
|
||||||
|
dialog.exec_()
|
||||||
|
|
||||||
|
sys.exit(app.exec_())
|
||||||
|
|
||||||
|
def demo_quick_check():
|
||||||
|
"""演示快速更新检查"""
|
||||||
|
from app.tools.update_dialog import QuickUpdateChecker
|
||||||
|
|
||||||
|
print("演示快速更新检查功能...")
|
||||||
|
|
||||||
|
# 检查更新但不显示对话框
|
||||||
|
result = QuickUpdateChecker.check_and_notify("1.0.0", None, auto_show_dialog=False)
|
||||||
|
|
||||||
|
if result:
|
||||||
|
print("✅ 发现更新并显示了通知")
|
||||||
|
else:
|
||||||
|
print("ℹ️ 当前已是最新版本或检查失败")
|
||||||
|
|
||||||
|
def demo_api_usage():
|
||||||
|
"""演示API使用方法"""
|
||||||
|
from app.tools.auto_updater import check_update_availability
|
||||||
|
|
||||||
|
print("\n演示API使用方法...")
|
||||||
|
|
||||||
|
# 检查更新
|
||||||
|
current_version = "1.0.0"
|
||||||
|
update_info = check_update_availability(current_version)
|
||||||
|
|
||||||
|
if update_info:
|
||||||
|
print("📦 发现新版本!")
|
||||||
|
print(f" 版本号: {update_info['version']}")
|
||||||
|
print(f" 下载链接: {update_info['download_url']}")
|
||||||
|
print(f" 文件名: {update_info['asset_name']}")
|
||||||
|
print(f" 发布日期: {update_info['release_date']}")
|
||||||
|
print(f" 更新说明: {update_info['release_notes'][:100]}...")
|
||||||
|
else:
|
||||||
|
print("ℹ️ 当前已是最新版本")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(description="MRobot 自动更新功能演示")
|
||||||
|
parser.add_argument("--mode", choices=["ui", "quick", "api"], default="ui",
|
||||||
|
help="演示模式: ui=图形界面, quick=快速检查, api=API演示")
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
print("🚀 MRobot 自动更新功能演示")
|
||||||
|
print("="*40)
|
||||||
|
|
||||||
|
if args.mode == "ui":
|
||||||
|
print("启动图形界面演示...")
|
||||||
|
demo_auto_update()
|
||||||
|
elif args.mode == "quick":
|
||||||
|
demo_quick_check()
|
||||||
|
elif args.mode == "api":
|
||||||
|
demo_api_usage()
|
||||||
|
|
||||||
|
print("\n演示完成!")
|
||||||
123
test_auto_update.py
Normal file
123
test_auto_update.py
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
自动更新功能测试脚本
|
||||||
|
用于测试自动更新功能的各个组件
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
# 添加项目路径到 Python 路径
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
|
||||||
|
|
||||||
|
def test_update_check():
|
||||||
|
"""测试更新检查功能"""
|
||||||
|
print("测试更新检查功能...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
from app.tools.auto_updater import check_update_availability
|
||||||
|
|
||||||
|
current_version = "1.0.0" # 使用一个较低的版本号来测试
|
||||||
|
result = check_update_availability(current_version)
|
||||||
|
|
||||||
|
if result:
|
||||||
|
print(f"✅ 发现新版本: {result['version']}")
|
||||||
|
print(f" 下载链接: {result['download_url']}")
|
||||||
|
print(f" 文件名: {result['asset_name']}")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print("ℹ️ 当前已是最新版本")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ 更新检查失败: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def test_updater_creation():
|
||||||
|
"""测试更新器创建"""
|
||||||
|
print("\n测试更新器创建...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
from app.tools.auto_updater import AutoUpdater
|
||||||
|
|
||||||
|
updater = AutoUpdater("1.0.0")
|
||||||
|
print(f"✅ 更新器创建成功")
|
||||||
|
print(f" 应用目录: {updater.app_dir}")
|
||||||
|
print(f" 是否打包: {updater.is_frozen}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ 更新器创建失败: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def test_dialog_import():
|
||||||
|
"""测试对话框导入"""
|
||||||
|
print("\n测试对话框导入...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
from app.tools.update_dialog import UpdateDialog, QuickUpdateChecker
|
||||||
|
print("✅ 更新对话框模块导入成功")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ 更新对话框导入失败: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def test_config_import():
|
||||||
|
"""测试配置导入"""
|
||||||
|
print("\n测试配置导入...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
from app.tools import update_config
|
||||||
|
print("✅ 更新配置模块导入成功")
|
||||||
|
print(f" 自动更新启用: {update_config.AUTO_UPDATE_ENABLED}")
|
||||||
|
print(f" GitHub仓库: {update_config.GITHUB_REPO}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ 更新配置导入失败: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def run_all_tests():
|
||||||
|
"""运行所有测试"""
|
||||||
|
print("🚀 开始自动更新功能测试\n")
|
||||||
|
|
||||||
|
tests = [
|
||||||
|
("配置导入", test_config_import),
|
||||||
|
("更新器创建", test_updater_creation),
|
||||||
|
("对话框导入", test_dialog_import),
|
||||||
|
("更新检查", test_update_check),
|
||||||
|
]
|
||||||
|
|
||||||
|
results = []
|
||||||
|
for name, test_func in tests:
|
||||||
|
try:
|
||||||
|
result = test_func()
|
||||||
|
results.append((name, result))
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ {name}测试出现异常: {e}")
|
||||||
|
results.append((name, False))
|
||||||
|
|
||||||
|
print("\n" + "="*50)
|
||||||
|
print("测试结果总结:")
|
||||||
|
print("="*50)
|
||||||
|
|
||||||
|
passed = 0
|
||||||
|
for name, result in results:
|
||||||
|
status = "✅ 通过" if result else "❌ 失败"
|
||||||
|
print(f"{name}: {status}")
|
||||||
|
if result:
|
||||||
|
passed += 1
|
||||||
|
|
||||||
|
print(f"\n总计: {passed}/{len(results)} 项测试通过")
|
||||||
|
|
||||||
|
if passed == len(results):
|
||||||
|
print("🎉 所有测试通过!自动更新功能可以正常使用。")
|
||||||
|
else:
|
||||||
|
print("⚠️ 部分测试失败,请检查相关模块。")
|
||||||
|
|
||||||
|
return passed == len(results)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
success = run_all_tests()
|
||||||
|
sys.exit(0 if success else 1)
|
||||||
90
test_fix_verification.py
Normal file
90
test_fix_verification.py
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
测试修复后的更新功能
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
# 添加项目路径
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
|
||||||
|
|
||||||
|
def test_dialog_creation():
|
||||||
|
"""测试对话框创建"""
|
||||||
|
print("测试对话框创建...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
from PyQt5.QtWidgets import QApplication
|
||||||
|
app = QApplication(sys.argv if len(sys.argv) > 1 else ['test'])
|
||||||
|
|
||||||
|
from app.tools.update_dialog import UpdateDialog
|
||||||
|
|
||||||
|
# 创建对话框(但不显示)
|
||||||
|
dialog = UpdateDialog("1.0.0")
|
||||||
|
print("✅ 对话框创建成功")
|
||||||
|
|
||||||
|
# 测试基本方法
|
||||||
|
dialog.check_for_updates()
|
||||||
|
print("✅ 检查更新功能正常")
|
||||||
|
|
||||||
|
# 清理
|
||||||
|
dialog.close()
|
||||||
|
app.quit()
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ 对话框测试失败: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def test_imports():
|
||||||
|
"""测试导入"""
|
||||||
|
print("测试模块导入...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
from app.tools.update_dialog import UpdateDialog, QuickUpdateChecker
|
||||||
|
from app.tools.auto_updater import AutoUpdater, check_update_availability
|
||||||
|
print("✅ 所有模块导入成功")
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ 导入失败: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""主测试函数"""
|
||||||
|
print("🧪 测试修复后的更新功能\n")
|
||||||
|
|
||||||
|
tests = [
|
||||||
|
("模块导入", test_imports),
|
||||||
|
("对话框创建", test_dialog_creation),
|
||||||
|
]
|
||||||
|
|
||||||
|
results = []
|
||||||
|
for name, test_func in tests:
|
||||||
|
print(f"🔍 {name}...")
|
||||||
|
try:
|
||||||
|
result = test_func()
|
||||||
|
results.append((name, result))
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ {name}异常: {e}")
|
||||||
|
results.append((name, False))
|
||||||
|
print()
|
||||||
|
|
||||||
|
print("="*40)
|
||||||
|
print("测试结果:")
|
||||||
|
passed = 0
|
||||||
|
for name, result in results:
|
||||||
|
status = "✅ 通过" if result else "❌ 失败"
|
||||||
|
print(f"{name}: {status}")
|
||||||
|
if result:
|
||||||
|
passed += 1
|
||||||
|
|
||||||
|
print(f"\n总计: {passed}/{len(results)} 项测试通过")
|
||||||
|
|
||||||
|
if passed == len(results):
|
||||||
|
print("🎉 修复成功!可以运行主程序了。")
|
||||||
|
else:
|
||||||
|
print("⚠️ 仍有问题需要解决。")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
182
test_fluent_design.py
Normal file
182
test_fluent_design.py
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
测试基于Fluent Design的现代化更新对话框
|
||||||
|
完全遵循qfluentwidgets设计规范,支持明暗主题
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
# 添加项目路径
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
|
||||||
|
|
||||||
|
from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QHBoxLayout
|
||||||
|
from PyQt5.QtCore import Qt
|
||||||
|
|
||||||
|
def test_fluent_dialog():
|
||||||
|
"""测试Fluent Design更新对话框"""
|
||||||
|
from qfluentwidgets import (
|
||||||
|
setThemeColor, Theme, setTheme,
|
||||||
|
PrimaryPushButton, PushButton, BodyLabel, SubtitleLabel,
|
||||||
|
CardWidget, VBoxLayout, HBoxLayout, isDarkTheme
|
||||||
|
)
|
||||||
|
from app.tools.fluent_design_update import FluentUpdateDialog
|
||||||
|
|
||||||
|
app = QApplication(sys.argv)
|
||||||
|
|
||||||
|
# 设置主题
|
||||||
|
setThemeColor('#f18cb9')
|
||||||
|
setTheme(Theme.AUTO)
|
||||||
|
|
||||||
|
# 创建测试窗口
|
||||||
|
window = QWidget()
|
||||||
|
window.setWindowTitle("Fluent Design 更新对话框测试")
|
||||||
|
window.setFixedSize(500, 400)
|
||||||
|
|
||||||
|
# 应用主题样式
|
||||||
|
if isDarkTheme():
|
||||||
|
window.setStyleSheet("background-color: #202020; color: white;")
|
||||||
|
else:
|
||||||
|
window.setStyleSheet("background-color: #FAFAFA; color: black;")
|
||||||
|
|
||||||
|
layout = VBoxLayout(window)
|
||||||
|
layout.setContentsMargins(40, 40, 40, 40)
|
||||||
|
layout.setSpacing(24)
|
||||||
|
|
||||||
|
# 创建测试卡片
|
||||||
|
test_card = CardWidget()
|
||||||
|
test_card.setFixedHeight(280)
|
||||||
|
|
||||||
|
card_layout = VBoxLayout(test_card)
|
||||||
|
card_layout.setContentsMargins(32, 24, 32, 24)
|
||||||
|
card_layout.setSpacing(20)
|
||||||
|
|
||||||
|
# 标题
|
||||||
|
title = SubtitleLabel("MRobot 现代化更新测试")
|
||||||
|
title.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
card_layout.addWidget(title)
|
||||||
|
|
||||||
|
# 说明
|
||||||
|
info = BodyLabel("这是基于QFluentWidgets设计系统的现代化自动更新界面测试。\n\n"
|
||||||
|
"新界面特点:\n"
|
||||||
|
"• 完全遵循Fluent Design规范\n"
|
||||||
|
"• 自动适应明暗主题\n"
|
||||||
|
"• 流畅的动画效果\n"
|
||||||
|
"• 现代化的卡片设计\n"
|
||||||
|
"• 清晰的视觉层次")
|
||||||
|
info.setWordWrap(True)
|
||||||
|
card_layout.addWidget(info)
|
||||||
|
|
||||||
|
# 主题切换提示
|
||||||
|
theme_info = BodyLabel(f"当前主题: {'暗色模式' if isDarkTheme() else '亮色模式'}")
|
||||||
|
theme_info.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
card_layout.addWidget(theme_info)
|
||||||
|
|
||||||
|
layout.addWidget(test_card)
|
||||||
|
|
||||||
|
# 按钮区域
|
||||||
|
btn_layout = HBoxLayout()
|
||||||
|
|
||||||
|
test_btn = PrimaryPushButton("测试更新对话框")
|
||||||
|
test_btn.clicked.connect(lambda: show_update_dialog(window))
|
||||||
|
|
||||||
|
theme_btn = PushButton("切换主题")
|
||||||
|
theme_btn.clicked.connect(lambda: toggle_theme_and_refresh(window, theme_info))
|
||||||
|
|
||||||
|
btn_layout.addWidget(theme_btn)
|
||||||
|
btn_layout.addStretch()
|
||||||
|
btn_layout.addWidget(test_btn)
|
||||||
|
|
||||||
|
layout.addLayout(btn_layout)
|
||||||
|
|
||||||
|
def show_update_dialog(parent):
|
||||||
|
"""显示更新对话框"""
|
||||||
|
dialog = FluentUpdateDialog("1.0.0", parent) # 使用低版本触发更新
|
||||||
|
dialog.exec_()
|
||||||
|
|
||||||
|
def toggle_theme_and_refresh(window, label):
|
||||||
|
"""切换主题并刷新"""
|
||||||
|
from qfluentwidgets import toggleTheme, isDarkTheme
|
||||||
|
toggleTheme()
|
||||||
|
|
||||||
|
# 更新窗口样式
|
||||||
|
if isDarkTheme():
|
||||||
|
window.setStyleSheet("background-color: #202020; color: white;")
|
||||||
|
label.setText("当前主题: 暗色模式")
|
||||||
|
else:
|
||||||
|
window.setStyleSheet("background-color: #FAFAFA; color: black;")
|
||||||
|
label.setText("当前主题: 亮色模式")
|
||||||
|
|
||||||
|
window.show()
|
||||||
|
sys.exit(app.exec_())
|
||||||
|
|
||||||
|
def test_components():
|
||||||
|
"""测试组件导入"""
|
||||||
|
tests = [
|
||||||
|
("Fluent Design更新对话框", lambda: __import__('app.tools.fluent_design_update')),
|
||||||
|
("qfluentwidgets组件", test_qfluentwidgets_components),
|
||||||
|
("自动更新器", lambda: __import__('app.tools.auto_updater')),
|
||||||
|
]
|
||||||
|
|
||||||
|
print("🎨 测试Fluent Design组件...")
|
||||||
|
print("-" * 50)
|
||||||
|
|
||||||
|
for name, test_func in tests:
|
||||||
|
try:
|
||||||
|
test_func()
|
||||||
|
print(f"✅ {name}: 导入成功")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ {name}: 导入失败 - {e}")
|
||||||
|
|
||||||
|
print("-" * 50)
|
||||||
|
print("测试完成!")
|
||||||
|
|
||||||
|
def test_qfluentwidgets_components():
|
||||||
|
"""测试qfluentwidgets组件"""
|
||||||
|
from qfluentwidgets import (
|
||||||
|
Dialog, CardWidget, SimpleCardWidget, ElevatedCardWidget,
|
||||||
|
PrimaryPushButton, PushButton, TransparentPushButton,
|
||||||
|
ProgressBar, ProgressRing, IndeterminateProgressBar,
|
||||||
|
SubtitleLabel, BodyLabel, CaptionLabel, StrongBodyLabel, DisplayLabel,
|
||||||
|
FluentIcon, InfoBar, ScrollArea, VBoxLayout, HBoxLayout,
|
||||||
|
setTheme, Theme, isDarkTheme, toggleTheme
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def test_theme_switching():
|
||||||
|
"""测试主题切换"""
|
||||||
|
from qfluentwidgets import setTheme, Theme, isDarkTheme, toggleTheme
|
||||||
|
|
||||||
|
print("🌓 测试主题切换...")
|
||||||
|
|
||||||
|
# 测试设置主题
|
||||||
|
setTheme(Theme.LIGHT)
|
||||||
|
print(f"设置亮色主题 - 当前是否暗色: {isDarkTheme()}")
|
||||||
|
|
||||||
|
setTheme(Theme.DARK)
|
||||||
|
print(f"设置暗色主题 - 当前是否暗色: {isDarkTheme()}")
|
||||||
|
|
||||||
|
setTheme(Theme.AUTO)
|
||||||
|
print(f"设置自动主题 - 当前是否暗色: {isDarkTheme()}")
|
||||||
|
|
||||||
|
print("主题切换测试完成!")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(description="Fluent Design更新对话框测试")
|
||||||
|
parser.add_argument("--mode", choices=["dialog", "components", "theme"],
|
||||||
|
default="components", help="测试模式")
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
print("🎨 Fluent Design 现代化更新对话框测试")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
if args.mode == "dialog":
|
||||||
|
print("启动图形界面测试...")
|
||||||
|
test_fluent_dialog()
|
||||||
|
elif args.mode == "components":
|
||||||
|
test_components()
|
||||||
|
elif args.mode == "theme":
|
||||||
|
test_theme_switching()
|
||||||
71
test_fluent_update.py
Normal file
71
test_fluent_update.py
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
测试基于QFluentWidgets的自动更新对话框
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
# 添加项目路径
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
|
||||||
|
|
||||||
|
from PyQt5.QtWidgets import QApplication
|
||||||
|
from PyQt5.QtCore import Qt
|
||||||
|
|
||||||
|
def test_fluent_update_dialog():
|
||||||
|
"""测试Fluent风格更新对话框"""
|
||||||
|
from qfluentwidgets import setThemeColor, Theme, setTheme
|
||||||
|
from app.tools.fluent_update_dialog import FluentUpdateDialog
|
||||||
|
|
||||||
|
app = QApplication(sys.argv)
|
||||||
|
|
||||||
|
# 设置主题
|
||||||
|
setThemeColor('#f18cb9')
|
||||||
|
setTheme(Theme.AUTO)
|
||||||
|
|
||||||
|
# 创建对话框
|
||||||
|
dialog = FluentUpdateDialog("1.0.0") # 使用较低版本来触发更新
|
||||||
|
|
||||||
|
# 显示对话框
|
||||||
|
dialog.show()
|
||||||
|
|
||||||
|
sys.exit(app.exec_())
|
||||||
|
|
||||||
|
def test_import():
|
||||||
|
"""测试导入"""
|
||||||
|
try:
|
||||||
|
from app.tools.fluent_update_dialog import FluentUpdateDialog, QuickUpdateNotification
|
||||||
|
print("✅ Fluent更新对话框导入成功")
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ 导入失败: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def test_components():
|
||||||
|
"""测试组件"""
|
||||||
|
try:
|
||||||
|
from qfluentwidgets import (
|
||||||
|
CardWidget, PrimaryPushButton, ProgressBar,
|
||||||
|
SubtitleLabel, BodyLabel, InfoBar, FluentIcon
|
||||||
|
)
|
||||||
|
print("✅ QFluentWidgets组件导入成功")
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ QFluentWidgets组件导入失败: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(description="测试Fluent风格更新对话框")
|
||||||
|
parser.add_argument("--mode", choices=["import", "components", "dialog"],
|
||||||
|
default="import", help="测试模式")
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if args.mode == "import":
|
||||||
|
test_import()
|
||||||
|
elif args.mode == "components":
|
||||||
|
test_components()
|
||||||
|
elif args.mode == "dialog":
|
||||||
|
test_fluent_update_dialog()
|
||||||
116
test_modern_update.py
Normal file
116
test_modern_update.py
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
测试现代化QFluentWidgets自动更新对话框
|
||||||
|
展示完整的Fluent Design风格界面
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
# 添加项目路径
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
|
||||||
|
|
||||||
|
from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QPushButton, QLabel
|
||||||
|
from PyQt5.QtCore import Qt
|
||||||
|
|
||||||
|
def test_modern_dialog():
|
||||||
|
"""测试现代化更新对话框"""
|
||||||
|
from qfluentwidgets import setThemeColor, Theme, setTheme, PrimaryPushButton, BodyLabel
|
||||||
|
from app.tools.modern_update_dialog import ModernUpdateDialog
|
||||||
|
|
||||||
|
app = QApplication(sys.argv)
|
||||||
|
|
||||||
|
# 设置主题
|
||||||
|
setThemeColor('#f18cb9')
|
||||||
|
setTheme(Theme.AUTO)
|
||||||
|
|
||||||
|
# 创建测试窗口
|
||||||
|
window = QWidget()
|
||||||
|
window.setWindowTitle("现代化更新对话框测试")
|
||||||
|
window.setFixedSize(400, 300)
|
||||||
|
|
||||||
|
layout = QVBoxLayout(window)
|
||||||
|
layout.setSpacing(20)
|
||||||
|
layout.setContentsMargins(40, 40, 40, 40)
|
||||||
|
|
||||||
|
# 标题
|
||||||
|
title = BodyLabel("MRobot 现代化更新测试")
|
||||||
|
title.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
title.setStyleSheet("font-size: 18px; font-weight: bold;")
|
||||||
|
layout.addWidget(title)
|
||||||
|
|
||||||
|
# 说明
|
||||||
|
info = BodyLabel("点击下方按钮测试基于QFluentWidgets的现代化更新界面")
|
||||||
|
info.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
info.setWordWrap(True)
|
||||||
|
layout.addWidget(info)
|
||||||
|
|
||||||
|
# 测试按钮
|
||||||
|
test_btn = PrimaryPushButton("打开更新对话框")
|
||||||
|
test_btn.clicked.connect(lambda: show_update_dialog(window))
|
||||||
|
layout.addWidget(test_btn)
|
||||||
|
|
||||||
|
layout.addStretch()
|
||||||
|
|
||||||
|
def show_update_dialog(parent):
|
||||||
|
dialog = ModernUpdateDialog("1.0.0", parent) # 使用低版本触发更新
|
||||||
|
dialog.exec_()
|
||||||
|
|
||||||
|
window.show()
|
||||||
|
sys.exit(app.exec_())
|
||||||
|
|
||||||
|
def test_components():
|
||||||
|
"""测试组件导入"""
|
||||||
|
tests = [
|
||||||
|
("现代化对话框", lambda: __import__('app.tools.modern_update_dialog')),
|
||||||
|
("Fluent组件", lambda: __import__('app.tools.fluent_components')),
|
||||||
|
("自动更新器", lambda: __import__('app.tools.auto_updater')),
|
||||||
|
]
|
||||||
|
|
||||||
|
print("🧪 测试组件导入...")
|
||||||
|
print("-" * 40)
|
||||||
|
|
||||||
|
for name, test_func in tests:
|
||||||
|
try:
|
||||||
|
test_func()
|
||||||
|
print(f"✅ {name}: 导入成功")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ {name}: 导入失败 - {e}")
|
||||||
|
|
||||||
|
print("-" * 40)
|
||||||
|
print("测试完成!")
|
||||||
|
|
||||||
|
def test_qfluentwidgets():
|
||||||
|
"""测试QFluentWidgets组件"""
|
||||||
|
try:
|
||||||
|
from qfluentwidgets import (
|
||||||
|
Dialog, CardWidget, PrimaryPushButton, ProgressBar,
|
||||||
|
SubtitleLabel, BodyLabel, InfoBar, FluentIcon,
|
||||||
|
ElevatedCardWidget, SimpleCardWidget, HeaderCardWidget,
|
||||||
|
TransparentToolButton, ProgressRing, PillPushButton
|
||||||
|
)
|
||||||
|
print("✅ QFluentWidgets高级组件导入成功")
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ QFluentWidgets组件导入失败: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(description="现代化更新对话框测试")
|
||||||
|
parser.add_argument("--mode", choices=["dialog", "components", "qfw"],
|
||||||
|
default="components", help="测试模式")
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
print("🚀 现代化自动更新对话框测试")
|
||||||
|
print("=" * 50)
|
||||||
|
|
||||||
|
if args.mode == "dialog":
|
||||||
|
print("启动图形界面测试...")
|
||||||
|
test_modern_dialog()
|
||||||
|
elif args.mode == "components":
|
||||||
|
test_components()
|
||||||
|
elif args.mode == "qfw":
|
||||||
|
test_qfluentwidgets()
|
||||||
21
test_new_ui.py
Normal file
21
test_new_ui.py
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from PyQt5.QtWidgets import QApplication
|
||||||
|
from app.tools.fluent_design_update import FluentUpdateDialog
|
||||||
|
|
||||||
|
def test_ui():
|
||||||
|
app = QApplication(sys.argv)
|
||||||
|
|
||||||
|
try:
|
||||||
|
dialog = FluentUpdateDialog("1.0.2")
|
||||||
|
print("FluentUpdateDialog created successfully")
|
||||||
|
dialog.show()
|
||||||
|
app.exec_()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
test_ui()
|
||||||
89
test_simple_update.py
Normal file
89
test_simple_update.py
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
测试简化的自动更新对话框
|
||||||
|
确保稳定性和兼容性
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
# 添加项目路径
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
|
||||||
|
|
||||||
|
def test_simple_dialog():
|
||||||
|
"""测试简化对话框"""
|
||||||
|
from PyQt5.QtWidgets import QApplication
|
||||||
|
from qfluentwidgets import setThemeColor, Theme, setTheme
|
||||||
|
from app.tools.simple_update_dialog import SimpleUpdateDialog
|
||||||
|
|
||||||
|
app = QApplication(sys.argv)
|
||||||
|
|
||||||
|
# 设置主题
|
||||||
|
setThemeColor('#f18cb9')
|
||||||
|
setTheme(Theme.AUTO)
|
||||||
|
|
||||||
|
# 创建对话框
|
||||||
|
dialog = SimpleUpdateDialog("1.0.0") # 使用较低版本触发更新
|
||||||
|
dialog.show()
|
||||||
|
|
||||||
|
sys.exit(app.exec_())
|
||||||
|
|
||||||
|
def test_imports():
|
||||||
|
"""测试导入"""
|
||||||
|
try:
|
||||||
|
from app.tools.simple_update_dialog import SimpleUpdateDialog, QuickNotifier
|
||||||
|
print("✅ 简化更新对话框导入成功")
|
||||||
|
|
||||||
|
from app.tools.auto_updater import AutoUpdater, check_update_availability
|
||||||
|
print("✅ 自动更新器导入成功")
|
||||||
|
|
||||||
|
from qfluentwidgets import (
|
||||||
|
CardWidget, PrimaryPushButton, ProgressBar,
|
||||||
|
SubtitleLabel, BodyLabel, InfoBar, FluentIcon
|
||||||
|
)
|
||||||
|
print("✅ QFluentWidgets基础组件导入成功")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ 导入失败: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def test_update_check():
|
||||||
|
"""测试更新检查"""
|
||||||
|
try:
|
||||||
|
from app.tools.auto_updater import check_update_availability
|
||||||
|
|
||||||
|
result = check_update_availability("1.0.0")
|
||||||
|
if result:
|
||||||
|
print(f"✅ 检测到更新: v{result['version']}")
|
||||||
|
else:
|
||||||
|
print("ℹ️ 当前已是最新版本")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ 更新检查失败: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(description="简化更新对话框测试")
|
||||||
|
parser.add_argument("--mode", choices=["dialog", "imports", "check"],
|
||||||
|
default="imports", help="测试模式")
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
print("🧪 简化自动更新对话框测试")
|
||||||
|
print("=" * 40)
|
||||||
|
|
||||||
|
if args.mode == "dialog":
|
||||||
|
print("启动对话框测试...")
|
||||||
|
test_simple_dialog()
|
||||||
|
elif args.mode == "imports":
|
||||||
|
test_imports()
|
||||||
|
elif args.mode == "check":
|
||||||
|
test_update_check()
|
||||||
|
|
||||||
|
print("测试完成!")
|
||||||
94
test_stable_update.py
Normal file
94
test_stable_update.py
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
测试稳定的更新对话框
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
# 添加项目路径
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
|
||||||
|
|
||||||
|
def test_stable_imports():
|
||||||
|
"""测试稳定组件导入"""
|
||||||
|
try:
|
||||||
|
from app.tools.simple_update_components import (
|
||||||
|
SimpleUpdateStatusCard, SimpleVersionCard, SimpleActionButtons
|
||||||
|
)
|
||||||
|
print("✅ 简化组件导入成功")
|
||||||
|
|
||||||
|
from app.tools.stable_update_dialog import StableUpdateDialog, SimpleUpdateNotifier
|
||||||
|
print("✅ 稳定对话框导入成功")
|
||||||
|
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ 导入失败: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def test_stable_dialog():
|
||||||
|
"""测试稳定对话框"""
|
||||||
|
from PyQt5.QtWidgets import QApplication
|
||||||
|
from qfluentwidgets import setThemeColor, Theme, setTheme
|
||||||
|
from app.tools.stable_update_dialog import StableUpdateDialog
|
||||||
|
|
||||||
|
app = QApplication(sys.argv)
|
||||||
|
|
||||||
|
# 设置主题
|
||||||
|
setThemeColor('#f18cb9')
|
||||||
|
setTheme(Theme.AUTO)
|
||||||
|
|
||||||
|
# 创建对话框
|
||||||
|
dialog = StableUpdateDialog("1.0.0") # 使用较低版本来触发更新
|
||||||
|
|
||||||
|
# 显示对话框
|
||||||
|
dialog.show()
|
||||||
|
|
||||||
|
sys.exit(app.exec_())
|
||||||
|
|
||||||
|
def test_about_interface():
|
||||||
|
"""测试关于页面集成"""
|
||||||
|
try:
|
||||||
|
from app.about_interface import AboutInterface
|
||||||
|
print("✅ 关于页面导入成功")
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ 关于页面导入失败: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def run_all_tests():
|
||||||
|
"""运行所有测试"""
|
||||||
|
print("🧪 测试稳定的更新功能\n")
|
||||||
|
|
||||||
|
tests = [
|
||||||
|
("稳定组件导入", test_stable_imports),
|
||||||
|
("关于页面集成", test_about_interface),
|
||||||
|
]
|
||||||
|
|
||||||
|
passed = 0
|
||||||
|
for name, test_func in tests:
|
||||||
|
try:
|
||||||
|
if test_func():
|
||||||
|
print(f"✅ {name}: 通过")
|
||||||
|
passed += 1
|
||||||
|
else:
|
||||||
|
print(f"❌ {name}: 失败")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ {name}: 异常 - {e}")
|
||||||
|
|
||||||
|
print(f"\n📊 测试结果: {passed}/{len(tests)} 通过")
|
||||||
|
return passed == len(tests)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(description="测试稳定更新对话框")
|
||||||
|
parser.add_argument("--mode", choices=["test", "dialog"],
|
||||||
|
default="test", help="运行模式")
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if args.mode == "test":
|
||||||
|
success = run_all_tests()
|
||||||
|
sys.exit(0 if success else 1)
|
||||||
|
elif args.mode == "dialog":
|
||||||
|
test_stable_dialog()
|
||||||
111
test_update_fix.py
Normal file
111
test_update_fix.py
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
测试修复后的自动更新功能
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
|
||||||
|
|
||||||
|
def test_simple_dialog_import():
|
||||||
|
"""测试简化对话框导入"""
|
||||||
|
try:
|
||||||
|
from app.tools.update_dialog_simple import SimpleUpdateDialog, QuickUpdateChecker
|
||||||
|
print("✅ 简化对话框导入成功")
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ 简化对话框导入失败: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
return False
|
||||||
|
|
||||||
|
def test_dialog_creation():
|
||||||
|
"""测试对话框创建(不显示)"""
|
||||||
|
try:
|
||||||
|
from PyQt5.QtWidgets import QApplication
|
||||||
|
from app.tools.update_dialog_simple import SimpleUpdateDialog
|
||||||
|
|
||||||
|
# 创建应用程序实例(如果还没有的话)
|
||||||
|
if not QApplication.instance():
|
||||||
|
app = QApplication([])
|
||||||
|
|
||||||
|
# 创建对话框但不显示
|
||||||
|
dialog = SimpleUpdateDialog("1.0.0")
|
||||||
|
print("✅ 对话框创建成功")
|
||||||
|
|
||||||
|
# 清理
|
||||||
|
dialog.deleteLater()
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ 对话框创建失败: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
return False
|
||||||
|
|
||||||
|
def test_about_interface_import():
|
||||||
|
"""测试关于界面导入"""
|
||||||
|
try:
|
||||||
|
from app.about_interface import AboutInterface
|
||||||
|
print("✅ 关于界面导入成功")
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ 关于界面导入失败: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
return False
|
||||||
|
|
||||||
|
def run_tests():
|
||||||
|
"""运行所有测试"""
|
||||||
|
print("🔧 测试修复后的自动更新功能\n")
|
||||||
|
|
||||||
|
tests = [
|
||||||
|
("简化对话框导入", test_simple_dialog_import),
|
||||||
|
("对话框创建", test_dialog_creation),
|
||||||
|
("关于界面导入", test_about_interface_import),
|
||||||
|
]
|
||||||
|
|
||||||
|
results = []
|
||||||
|
for name, test_func in tests:
|
||||||
|
print(f"测试 {name}...")
|
||||||
|
try:
|
||||||
|
result = test_func()
|
||||||
|
results.append((name, result))
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ {name} 测试异常: {e}")
|
||||||
|
results.append((name, False))
|
||||||
|
print()
|
||||||
|
|
||||||
|
print("=" * 50)
|
||||||
|
print("测试结果:")
|
||||||
|
print("=" * 50)
|
||||||
|
|
||||||
|
passed = 0
|
||||||
|
for name, result in results:
|
||||||
|
status = "✅ 通过" if result else "❌ 失败"
|
||||||
|
print(f"{name}: {status}")
|
||||||
|
if result:
|
||||||
|
passed += 1
|
||||||
|
|
||||||
|
print(f"\n总计: {passed}/{len(results)} 项测试通过")
|
||||||
|
|
||||||
|
if passed == len(results):
|
||||||
|
print("🎉 所有测试通过!修复成功,自动更新功能应该不会再闪退。")
|
||||||
|
else:
|
||||||
|
print("⚠️ 部分测试失败,可能还有问题需要修复。")
|
||||||
|
|
||||||
|
return passed == len(results)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
success = run_tests()
|
||||||
|
|
||||||
|
if success:
|
||||||
|
print("\n💡 使用提示:")
|
||||||
|
print("1. 现在可以在'关于'页面点击'自动更新'按钮")
|
||||||
|
print("2. 更新过程中会显示详细的进度条和状态")
|
||||||
|
print("3. 更新完成后程序会自动重启")
|
||||||
|
print("4. 如果更新失败会显示错误信息并允许重试")
|
||||||
|
|
||||||
|
sys.exit(0 if success else 1)
|
||||||
44
test_version_check.py
Normal file
44
test_version_check.py
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
测试版本检查逻辑
|
||||||
|
"""
|
||||||
|
|
||||||
|
from packaging.version import parse as vparse
|
||||||
|
|
||||||
|
def test_version_comparison():
|
||||||
|
"""测试版本比较"""
|
||||||
|
current_version = "1.0.2"
|
||||||
|
|
||||||
|
# 测试不同的版本情况
|
||||||
|
test_versions = ["1.0.1", "1.0.2", "1.0.3", "1.0.5", "1.1.0", "2.0.0"]
|
||||||
|
|
||||||
|
print(f"当前版本: {current_version}")
|
||||||
|
print("-" * 40)
|
||||||
|
|
||||||
|
for version in test_versions:
|
||||||
|
is_newer = vparse(version) > vparse(current_version)
|
||||||
|
status = "有更新" if is_newer else "无更新"
|
||||||
|
print(f"版本 {version}: {status}")
|
||||||
|
|
||||||
|
|
||||||
|
def simulate_check_update(local_version, remote_version):
|
||||||
|
"""模拟更新检查"""
|
||||||
|
print(f"\n模拟检查: 本地版本 {local_version} vs 远程版本 {remote_version}")
|
||||||
|
|
||||||
|
if vparse(remote_version) > vparse(local_version):
|
||||||
|
print("✓ 发现新版本")
|
||||||
|
return remote_version
|
||||||
|
else:
|
||||||
|
print("✗ 已是最新版本")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
test_version_comparison()
|
||||||
|
|
||||||
|
# 模拟你遇到的情况
|
||||||
|
print("\n" + "="*50)
|
||||||
|
print("模拟实际情况:")
|
||||||
|
simulate_check_update("1.0.2", "1.0.5")
|
||||||
|
simulate_check_update("1.0.2", "1.0.2")
|
||||||
|
simulate_check_update("1.0.2", "1.0.1")
|
||||||
224
自动更新功能说明.md
Normal file
224
自动更新功能说明.md
Normal file
@ -0,0 +1,224 @@
|
|||||||
|
# 自动更新功能使用说明
|
||||||
|
|
||||||
|
## 功能概述
|
||||||
|
|
||||||
|
MRobot 现在支持完整的自动更新功能,包括:
|
||||||
|
|
||||||
|
- ✅ 自动检查更新
|
||||||
|
- ✅ 自动下载更新包
|
||||||
|
- ✅ 自动解压安装
|
||||||
|
- ✅ 自动重启程序
|
||||||
|
- ✅ 更新失败回滚
|
||||||
|
|
||||||
|
## 使用方法
|
||||||
|
|
||||||
|
### 1. 启动时自动检查更新
|
||||||
|
|
||||||
|
程序启动 3 秒后会自动在后台检查更新。如果发现新版本,会显示通知提示。
|
||||||
|
|
||||||
|
### 2. 手动检查更新
|
||||||
|
|
||||||
|
在"关于"页面点击:
|
||||||
|
- **自动更新**: 打开完整的自动更新对话框
|
||||||
|
- **手动检查**: 检查更新并在浏览器中打开下载页面
|
||||||
|
|
||||||
|
### 3. 自动更新流程
|
||||||
|
|
||||||
|
1. 点击"自动更新"按钮
|
||||||
|
2. 系统自动检查是否有新版本
|
||||||
|
3. 如有更新,显示版本信息和更新说明
|
||||||
|
4. 点击"开始更新"确认更新
|
||||||
|
5. 系统自动完成:
|
||||||
|
- 下载更新包
|
||||||
|
- 备份当前文件
|
||||||
|
- 解压安装新版本
|
||||||
|
- 重启程序
|
||||||
|
|
||||||
|
## 技术实现
|
||||||
|
|
||||||
|
### 核心组件
|
||||||
|
|
||||||
|
1. **AutoUpdater** (`auto_updater.py`)
|
||||||
|
- 主要更新逻辑
|
||||||
|
- 多线程下载和安装
|
||||||
|
- 错误处理和回滚
|
||||||
|
|
||||||
|
2. **UpdateDialog** (`update_dialog.py`)
|
||||||
|
- 用户界面
|
||||||
|
- 进度显示
|
||||||
|
- 用户交互
|
||||||
|
|
||||||
|
3. **QuickUpdateChecker**
|
||||||
|
- 后台更新检查
|
||||||
|
- 通知显示
|
||||||
|
|
||||||
|
### 更新检查逻辑
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 检查 GitHub Releases API
|
||||||
|
url = f"https://api.github.com/repos/{repo}/releases/latest"
|
||||||
|
response = requests.get(url, timeout=10)
|
||||||
|
release_data = response.json()
|
||||||
|
|
||||||
|
# 比较版本号
|
||||||
|
from packaging.version import parse as vparse
|
||||||
|
if vparse(latest_version) > vparse(current_version):
|
||||||
|
# 有新版本可用
|
||||||
|
```
|
||||||
|
|
||||||
|
### 下载逻辑
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 根据操作系统选择合适的安装包
|
||||||
|
system = platform.system().lower()
|
||||||
|
if system == 'windows':
|
||||||
|
# 查找 .exe 或 .zip 文件
|
||||||
|
elif system == 'darwin': # macOS
|
||||||
|
# 查找 .dmg 或 .zip 文件
|
||||||
|
elif system == 'linux':
|
||||||
|
# 查找 .tar.gz 或 .zip 文件
|
||||||
|
```
|
||||||
|
|
||||||
|
### 安装逻辑
|
||||||
|
|
||||||
|
1. **备份当前文件**
|
||||||
|
```python
|
||||||
|
# 备份重要文件到临时目录
|
||||||
|
backup_files = ['MRobot.py', 'app/', 'assets/']
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **解压新版本**
|
||||||
|
```python
|
||||||
|
# 支持 .zip 和 .tar.gz 格式
|
||||||
|
with zipfile.ZipFile(file_path, 'r') as zip_ref:
|
||||||
|
zip_ref.extractall(extract_dir)
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **复制文件**
|
||||||
|
```python
|
||||||
|
# 替换应用程序文件
|
||||||
|
shutil.copytree(src_path, dst_path)
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **重启程序**
|
||||||
|
```python
|
||||||
|
# 启动新进程并退出当前进程
|
||||||
|
subprocess.Popen([executable])
|
||||||
|
sys.exit(0)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 配置选项
|
||||||
|
|
||||||
|
在 `update_config.py` 中可以配置:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 基本设置
|
||||||
|
AUTO_UPDATE_ENABLED = True # 启用自动更新
|
||||||
|
AUTO_UPDATE_ON_STARTUP = True # 启动时检查
|
||||||
|
UPDATE_CHECK_INTERVAL = 3000 # 检查延迟(毫秒)
|
||||||
|
|
||||||
|
# 下载设置
|
||||||
|
DOWNLOAD_TIMEOUT = 300 # 下载超时
|
||||||
|
CHUNK_SIZE = 8192 # 下载块大小
|
||||||
|
|
||||||
|
# 备份设置
|
||||||
|
CREATE_BACKUP = True # 创建备份
|
||||||
|
BACKUP_IMPORTANT_FILES = [...] # 备份文件列表
|
||||||
|
```
|
||||||
|
|
||||||
|
## 安全考虑
|
||||||
|
|
||||||
|
1. **数字签名验证** (未实现,可扩展)
|
||||||
|
- 验证下载文件的数字签名
|
||||||
|
- 确保文件来源可信
|
||||||
|
|
||||||
|
2. **备份和回滚**
|
||||||
|
- 更新前自动备份
|
||||||
|
- 更新失败自动回滚
|
||||||
|
|
||||||
|
3. **用户确认**
|
||||||
|
- 所有更新操作需要用户确认
|
||||||
|
- 不会自动静默更新
|
||||||
|
|
||||||
|
## 故障排除
|
||||||
|
|
||||||
|
### 常见问题
|
||||||
|
|
||||||
|
1. **网络连接问题**
|
||||||
|
- 检查网络连接
|
||||||
|
- 检查防火墙设置
|
||||||
|
- 尝试手动更新
|
||||||
|
|
||||||
|
2. **权限问题**
|
||||||
|
- 确保程序有写入权限
|
||||||
|
- 以管理员身份运行(Windows)
|
||||||
|
|
||||||
|
3. **文件被占用**
|
||||||
|
- 关闭其他实例
|
||||||
|
- 重启计算机后再试
|
||||||
|
|
||||||
|
### 错误日志
|
||||||
|
|
||||||
|
更新过程中的错误会显示在界面上,并可在控制台查看详细信息。
|
||||||
|
|
||||||
|
### 手动回滚
|
||||||
|
|
||||||
|
如果自动回滚失败,可以:
|
||||||
|
1. 从临时目录恢复备份文件
|
||||||
|
2. 重新下载完整安装包
|
||||||
|
3. 联系技术支持
|
||||||
|
|
||||||
|
## 开发者信息
|
||||||
|
|
||||||
|
### 扩展功能
|
||||||
|
|
||||||
|
可以在现有基础上添加:
|
||||||
|
|
||||||
|
1. **增量更新**
|
||||||
|
- 只下载变更的文件
|
||||||
|
- 减少下载时间和流量
|
||||||
|
|
||||||
|
2. **多源下载**
|
||||||
|
- 支持多个下载镜像
|
||||||
|
- 提高下载成功率
|
||||||
|
|
||||||
|
3. **版本回退**
|
||||||
|
- 支持回退到指定版本
|
||||||
|
- 版本历史管理
|
||||||
|
|
||||||
|
4. **自定义更新源**
|
||||||
|
- 支持企业内部更新服务器
|
||||||
|
- 自定义更新策略
|
||||||
|
|
||||||
|
### API 接口
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 检查更新
|
||||||
|
update_info = check_update_availability(current_version)
|
||||||
|
|
||||||
|
# 启动自动更新
|
||||||
|
dialog = UpdateDialog(current_version, parent)
|
||||||
|
dialog.exec_()
|
||||||
|
|
||||||
|
# 后台检查
|
||||||
|
QuickUpdateChecker.check_and_notify(version, parent)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. 确保 GitHub Releases 中有对应平台的安装包
|
||||||
|
2. 安装包命名要规范,便于识别平台
|
||||||
|
3. 更新说明要写在 Release Notes 中
|
||||||
|
4. 版本号要遵循语义化版本规范 (如: v1.0.5)
|
||||||
|
|
||||||
|
## 版本兼容性
|
||||||
|
|
||||||
|
- PyQt5 5.x+
|
||||||
|
- Python 3.7+
|
||||||
|
- requests 2.x+
|
||||||
|
- packaging 20.x+
|
||||||
|
|
||||||
|
支持的操作系统:
|
||||||
|
- Windows 7+
|
||||||
|
- macOS 10.12+
|
||||||
|
- Ubuntu 18.04+
|
||||||
Loading…
Reference in New Issue
Block a user