diff --git a/app/about_interface.py b/app/about_interface.py index 2dac91a..b1ddc73 100644 --- a/app/about_interface.py +++ b/app/about_interface.py @@ -1,65 +1,350 @@ -from PyQt5.QtWidgets import QWidget, QVBoxLayout, QMessageBox -from PyQt5.QtCore import Qt, QUrl +from PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout +from PyQt5.QtCore import Qt, QUrl, QTimer from PyQt5.QtGui import QDesktopServices -from qfluentwidgets import PrimaryPushSettingCard, FluentIcon -from qfluentwidgets import InfoBar, InfoBarPosition, SubtitleLabel +from qfluentwidgets import ( + PrimaryPushSettingCard, FluentIcon, InfoBar, InfoBarPosition, + SubtitleLabel, BodyLabel, CaptionLabel, StrongBodyLabel, + ElevatedCardWidget, PrimaryPushButton, PushButton, + ProgressBar, TextEdit +) from .function_fit_interface import FunctionFitInterface 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): def __init__(self, parent=None): super().__init__(parent) self.setObjectName("aboutInterface") - + + # 初始化更新相关变量 + self.updater = None + self.update_info = None + + self._setup_ui() + + def _setup_ui(self): + """设置用户界面""" layout = QVBoxLayout(self) - layout.setAlignment(Qt.AlignTop) - layout.setContentsMargins(20, 30, 20, 20) # 添加边距 + layout.setAlignment(Qt.AlignmentFlag.AlignTop) + layout.setContentsMargins(20, 30, 20, 20) + # 页面标题 title = SubtitleLabel("MRobot 帮助页面", self) - title.setAlignment(Qt.AlignCenter) + title.setAlignment(Qt.AlignmentFlag.AlignCenter) layout.addWidget(title) - # 添加空间隔 layout.addSpacing(10) - card = PrimaryPushSettingCard( - text="检查更新", - icon=FluentIcon.DOWNLOAD, - title="更新", - content=f"MRobot_Toolbox 当前版本:{__version__}", - ) - card.clicked.connect(self.on_check_update_clicked) - layout.addWidget(card) + # 版本信息卡片 - 学习AI界面风格 + version_card = ElevatedCardWidget() + version_layout = QVBoxLayout(version_card) + version_layout.setContentsMargins(24, 20, 24, 20) + + version_title = StrongBodyLabel("版本信息") + version_layout.addWidget(version_title) + + 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: - latest = check_update(__version__) - if latest: - # 直接用浏览器打开下载链接 - QDesktopServices.openUrl(QUrl("https://github.com/goldenfishs/MRobot/releases/latest")) - InfoBar.success( - title="发现新版本", - content=f"检测到新版本:{latest},已为你打开下载页面。", - parent=self, - position=InfoBarPosition.TOP, - duration=5000 - ) - elif latest is None: - InfoBar.info( - title="已是最新版本", - content="当前已是最新版本,无需更新。", - parent=self, - position=InfoBarPosition.TOP, - duration=3000 - ) - except Exception: - InfoBar.error( - title="检查更新失败", - content="无法获取最新版本,请检查网络连接。", - parent=self, - position=InfoBarPosition.TOP, - duration=4000 - ) \ No newline at end of file + self.update_info = check_update_availability(__version__) + + if self.update_info: + self._show_update_available() + else: + self._show_no_update() + + except Exception as e: + self._show_error(f"检查更新失败: {str(e)}") + + def _show_update_available(self): + """显示发现更新""" + # 更新按钮状态 + self.check_update_card.setEnabled(True) + self.check_update_card.setContent("发现新版本!") + + # 显示更新信息卡片 + self.update_info_card.show() + + # 设置版本信息 + if self.update_info: + version = self.update_info.get('version', 'Unknown') + self.latest_version_label.setText(f"v{version}") + + # 设置文件信息 + 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 \ No newline at end of file diff --git a/app/main_window.py b/app/main_window.py index 89057c0..0080e83 100644 --- a/app/main_window.py +++ b/app/main_window.py @@ -25,9 +25,8 @@ class MainWindow(FluentWindow): self.initInterface() self.initNavigation() - # 检查更新 - # checkUpdate(self, flag=True) - # checkAnnouncement(self) # 检查公告 + # 后台检查更新(不弹窗,只显示通知) + # self.check_updates_in_background() def initWindow(self): self.setMicaEffectEnabled(False) @@ -74,6 +73,14 @@ class MainWindow(FluentWindow): None, NavigationItemPosition.BOTTOM ) + + def check_updates_in_background(self): + """后台检查更新""" + try: + # 后台更新检查已移至关于页面手动触发 + pass + except Exception as e: + print(f"初始化完成: {e}") # main_window.py 只需修改关闭事件 def closeEvent(self, e): diff --git a/app/tools/auto_updater.py b/app/tools/auto_updater.py new file mode 100644 index 0000000..18f174e --- /dev/null +++ b/app/tools/auto_updater.py @@ -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() \ No newline at end of file diff --git a/check_releases.py b/check_releases.py new file mode 100644 index 0000000..d2e8209 --- /dev/null +++ b/check_releases.py @@ -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() \ No newline at end of file diff --git a/demo_auto_update.py b/demo_auto_update.py new file mode 100644 index 0000000..f6d5b03 --- /dev/null +++ b/demo_auto_update.py @@ -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演示完成!") \ No newline at end of file diff --git a/test_auto_update.py b/test_auto_update.py new file mode 100644 index 0000000..7a81353 --- /dev/null +++ b/test_auto_update.py @@ -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) \ No newline at end of file diff --git a/test_fix_verification.py b/test_fix_verification.py new file mode 100644 index 0000000..852a0d2 --- /dev/null +++ b/test_fix_verification.py @@ -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() \ No newline at end of file diff --git a/test_fluent_design.py b/test_fluent_design.py new file mode 100644 index 0000000..aefb122 --- /dev/null +++ b/test_fluent_design.py @@ -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() \ No newline at end of file diff --git a/test_fluent_update.py b/test_fluent_update.py new file mode 100644 index 0000000..05641f7 --- /dev/null +++ b/test_fluent_update.py @@ -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() \ No newline at end of file diff --git a/test_modern_update.py b/test_modern_update.py new file mode 100644 index 0000000..5fce270 --- /dev/null +++ b/test_modern_update.py @@ -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() \ No newline at end of file diff --git a/test_new_ui.py b/test_new_ui.py new file mode 100644 index 0000000..c69fc51 --- /dev/null +++ b/test_new_ui.py @@ -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() \ No newline at end of file diff --git a/test_simple_update.py b/test_simple_update.py new file mode 100644 index 0000000..f562322 --- /dev/null +++ b/test_simple_update.py @@ -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("测试完成!") \ No newline at end of file diff --git a/test_stable_update.py b/test_stable_update.py new file mode 100644 index 0000000..08a2ca1 --- /dev/null +++ b/test_stable_update.py @@ -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() \ No newline at end of file diff --git a/test_update_fix.py b/test_update_fix.py new file mode 100644 index 0000000..69d5fa1 --- /dev/null +++ b/test_update_fix.py @@ -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) \ No newline at end of file diff --git a/test_version_check.py b/test_version_check.py new file mode 100644 index 0000000..d056819 --- /dev/null +++ b/test_version_check.py @@ -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") \ No newline at end of file diff --git a/自动更新功能说明.md b/自动更新功能说明.md new file mode 100644 index 0000000..5b25ced --- /dev/null +++ b/自动更新功能说明.md @@ -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+ \ No newline at end of file