This commit is contained in:
Robofish 2025-10-13 11:20:19 +08:00
parent 697104d1ce
commit e5d5afb1a8
16 changed files with 2008 additions and 49 deletions

View File

@ -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

View File

@ -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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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+