mirror of
https://github.com/goldenfishs/MRobot.git
synced 2026-03-31 21:07:14 +08:00
v1.0.6
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,3 +1,4 @@
|
||||
import os
|
||||
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout
|
||||
from PyQt5.QtCore import Qt, QUrl, QTimer
|
||||
from PyQt5.QtGui import QDesktopServices
|
||||
@@ -12,8 +13,9 @@ from qfluentwidgets import (
|
||||
from .function_fit_interface import FunctionFitInterface
|
||||
from app.tools.check_update import check_update
|
||||
from app.tools.auto_updater import AutoUpdater, check_update_availability
|
||||
from app.tools.update_check_thread import UpdateCheckThread
|
||||
|
||||
__version__ = "1.0.2"
|
||||
__version__ = "1.0.6"
|
||||
|
||||
class AboutInterface(QWidget):
|
||||
def __init__(self, parent=None):
|
||||
@@ -32,13 +34,7 @@ class AboutInterface(QWidget):
|
||||
layout.setAlignment(Qt.AlignmentFlag.AlignTop)
|
||||
layout.setContentsMargins(20, 30, 20, 20)
|
||||
|
||||
# 页面标题
|
||||
title = SubtitleLabel("MRobot 帮助页面", self)
|
||||
title.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
layout.addWidget(title)
|
||||
layout.addSpacing(10)
|
||||
|
||||
# 版本信息卡片 - 学习AI界面风格
|
||||
# 版本信息卡片
|
||||
version_card = ElevatedCardWidget()
|
||||
version_layout = QVBoxLayout(version_card)
|
||||
version_layout.setContentsMargins(24, 20, 24, 20)
|
||||
@@ -57,7 +53,7 @@ class AboutInterface(QWidget):
|
||||
text="检查更新",
|
||||
icon=FluentIcon.SYNC,
|
||||
title="检查更新",
|
||||
content="检查是否有新版本可用",
|
||||
content="检查是否有新版本可用(需要能够连接github)",
|
||||
)
|
||||
self.check_update_card.clicked.connect(self.check_for_updates)
|
||||
layout.addWidget(self.check_update_card)
|
||||
@@ -119,7 +115,8 @@ class AboutInterface(QWidget):
|
||||
|
||||
self.notes_display = TextEdit()
|
||||
self.notes_display.setReadOnly(True)
|
||||
self.notes_display.setFixedHeight(200)
|
||||
self.notes_display.setMaximumHeight(200)
|
||||
self.notes_display.setMinimumHeight(80)
|
||||
self.notes_display.setText("暂无更新说明")
|
||||
layout.addWidget(self.notes_display)
|
||||
|
||||
@@ -135,6 +132,11 @@ class AboutInterface(QWidget):
|
||||
self.progress_bar.setValue(0)
|
||||
progress_layout.addWidget(self.progress_bar)
|
||||
|
||||
# 详细下载信息
|
||||
self.download_info = BodyLabel("")
|
||||
self.download_info.setWordWrap(True)
|
||||
progress_layout.addWidget(self.download_info)
|
||||
|
||||
self.progress_widget.hide()
|
||||
layout.addWidget(self.progress_widget)
|
||||
|
||||
@@ -170,16 +172,58 @@ class AboutInterface(QWidget):
|
||||
def _perform_check(self):
|
||||
"""执行更新检查"""
|
||||
try:
|
||||
# 获取最新版本信息(包括当前版本的详细信息)
|
||||
latest_info = self._get_latest_release_info()
|
||||
|
||||
# 检查是否有可用更新
|
||||
self.update_info = check_update_availability(__version__)
|
||||
|
||||
if self.update_info:
|
||||
self._show_update_available()
|
||||
else:
|
||||
self._show_no_update()
|
||||
self._show_no_update(latest_info)
|
||||
|
||||
except Exception as e:
|
||||
self._show_error(f"检查更新失败: {str(e)}")
|
||||
|
||||
def _get_latest_release_info(self):
|
||||
"""获取最新发布信息,不论版本是否需要更新"""
|
||||
try:
|
||||
import requests
|
||||
from packaging.version import parse as vparse
|
||||
|
||||
url = f"https://api.github.com/repos/goldenfishs/MRobot/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")
|
||||
|
||||
# 获取下载URL和文件大小
|
||||
assets = release_data.get('assets', [])
|
||||
asset_size = 0
|
||||
download_url = None
|
||||
|
||||
if assets:
|
||||
# 选择第一个资源文件
|
||||
asset = assets[0]
|
||||
asset_size = asset.get('size', 0)
|
||||
download_url = asset.get('browser_download_url', '')
|
||||
|
||||
return {
|
||||
'version': latest_version,
|
||||
'release_notes': release_data.get('body', '暂无更新说明'),
|
||||
'release_date': release_data.get('published_at', ''),
|
||||
'asset_size': asset_size,
|
||||
'download_url': download_url
|
||||
}
|
||||
else:
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
print(f"获取发布信息失败: {e}")
|
||||
return None
|
||||
|
||||
def _show_update_available(self):
|
||||
"""显示发现更新"""
|
||||
# 更新按钮状态
|
||||
@@ -216,11 +260,43 @@ class AboutInterface(QWidget):
|
||||
duration=3000
|
||||
)
|
||||
|
||||
def _show_no_update(self):
|
||||
"""显示无更新"""
|
||||
def _show_no_update(self, latest_info=None):
|
||||
"""显示无更新,但展示最新版本信息"""
|
||||
self.check_update_card.setEnabled(True)
|
||||
self.check_update_card.setContent("已是最新版本")
|
||||
|
||||
# 如果有最新版本信息,显示详情卡片
|
||||
if latest_info:
|
||||
self.update_info_card.show()
|
||||
|
||||
# 显示版本信息(当前版本就是最新版本)
|
||||
self.latest_version_label.setText(f"v{__version__}")
|
||||
|
||||
# 设置文件信息
|
||||
asset_size = latest_info.get('asset_size', 0)
|
||||
file_size = self._format_file_size(asset_size)
|
||||
self.file_size_label.setText(f"文件大小: {file_size}")
|
||||
|
||||
# 设置发布时间
|
||||
release_date = latest_info.get('release_date', '')
|
||||
formatted_date = self._format_date(release_date)
|
||||
self.release_date_label.setText(f"发布时间: {formatted_date}")
|
||||
|
||||
# 设置更新说明
|
||||
notes = latest_info.get('release_notes', '暂无更新说明')
|
||||
self.notes_display.setText(notes[:500] + ('...' if len(notes) > 500 else ''))
|
||||
|
||||
# 修改标题和按钮
|
||||
self.update_title.setText("版本信息")
|
||||
self.update_btn.setText("手动下载")
|
||||
self.update_btn.setIcon(FluentIcon.DOWNLOAD)
|
||||
self.update_btn.setEnabled(True)
|
||||
self.manual_btn.setEnabled(True)
|
||||
|
||||
# 连接手动下载功能
|
||||
self.update_btn.clicked.disconnect()
|
||||
self.update_btn.clicked.connect(self.open_manual_download)
|
||||
|
||||
InfoBar.info(
|
||||
title="已是最新版本",
|
||||
content="当前已是最新版本,无需更新。",
|
||||
@@ -252,9 +328,10 @@ class AboutInterface(QWidget):
|
||||
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.download_progress.connect(self.update_download_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)
|
||||
@@ -266,6 +343,29 @@ class AboutInterface(QWidget):
|
||||
"""更新进度"""
|
||||
self.progress_bar.setValue(value)
|
||||
|
||||
def update_download_progress(self, downloaded: int, total: int, speed: float, remaining: float):
|
||||
"""更新下载进度详情"""
|
||||
if total > 0:
|
||||
downloaded_str = self._format_bytes(downloaded)
|
||||
total_str = self._format_bytes(total)
|
||||
percentage = (downloaded / total) * 100
|
||||
|
||||
info_text = f"已下载: {downloaded_str} / {total_str} ({percentage:.1f}%)"
|
||||
|
||||
self.download_info.setText(info_text)
|
||||
|
||||
def _format_bytes(self, size_bytes: int) -> str:
|
||||
"""格式化字节大小"""
|
||||
if size_bytes == 0:
|
||||
return "0 B"
|
||||
|
||||
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 update_status(self, status: str):
|
||||
"""更新状态"""
|
||||
self.progress_label.setText(status)
|
||||
@@ -276,34 +376,106 @@ class AboutInterface(QWidget):
|
||||
self.update_btn.setEnabled(True)
|
||||
self.manual_btn.setEnabled(True)
|
||||
|
||||
InfoBar.error(
|
||||
title="更新失败",
|
||||
content=error_msg,
|
||||
parent=self,
|
||||
position=InfoBarPosition.TOP,
|
||||
duration=4000
|
||||
)
|
||||
# 如果是平台兼容性问题,提供更友好的提示
|
||||
if "Windows 安装程序" in error_msg and "当前系统是" in error_msg:
|
||||
InfoBar.warning(
|
||||
title="平台不兼容",
|
||||
content="检测到 Windows 安装程序,请点击'手动下载'获取适合 macOS 的版本",
|
||||
parent=self,
|
||||
position=InfoBarPosition.TOP,
|
||||
duration=6000
|
||||
)
|
||||
else:
|
||||
InfoBar.error(
|
||||
title="更新失败",
|
||||
content=error_msg,
|
||||
parent=self,
|
||||
position=InfoBarPosition.TOP,
|
||||
duration=4000
|
||||
)
|
||||
|
||||
def update_completed(self):
|
||||
"""更新完成"""
|
||||
self.progress_label.setText("更新完成,准备重启...")
|
||||
def update_completed(self, file_path=None):
|
||||
"""更新完成 - 显示下载文件位置"""
|
||||
print(f"update_completed called with file_path: {file_path}") # 调试输出
|
||||
|
||||
self.progress_label.setText("下载完成!")
|
||||
self.progress_bar.setValue(100)
|
||||
|
||||
InfoBar.success(
|
||||
title="更新完成",
|
||||
content="更新安装完成,程序将在3秒后重启",
|
||||
parent=self,
|
||||
position=InfoBarPosition.TOP,
|
||||
duration=3000
|
||||
)
|
||||
# 重新启用按钮
|
||||
self.update_btn.setEnabled(True)
|
||||
self.manual_btn.setEnabled(True)
|
||||
|
||||
# 延迟重启
|
||||
QTimer.singleShot(3000, self._restart_app)
|
||||
if file_path and os.path.exists(file_path):
|
||||
print(f"File exists: {file_path}") # 调试输出
|
||||
InfoBar.success(
|
||||
title="下载完成",
|
||||
content="安装文件已下载完成,点击下方按钮打开文件位置",
|
||||
parent=self,
|
||||
position=InfoBarPosition.TOP,
|
||||
duration=5000
|
||||
)
|
||||
|
||||
# 添加打开文件夹按钮
|
||||
self._add_open_folder_button(file_path)
|
||||
else:
|
||||
print(f"File does not exist or file_path is None: {file_path}") # 调试输出
|
||||
InfoBar.success(
|
||||
title="下载完成",
|
||||
content="文件下载完成",
|
||||
parent=self,
|
||||
position=InfoBarPosition.TOP,
|
||||
duration=3000
|
||||
)
|
||||
|
||||
def _add_open_folder_button(self, file_path):
|
||||
"""添加打开文件夹按钮"""
|
||||
def open_file_location():
|
||||
folder_path = os.path.dirname(file_path)
|
||||
# 在 macOS 上使用 Finder 打开文件夹
|
||||
QDesktopServices.openUrl(QUrl.fromLocalFile(folder_path))
|
||||
|
||||
InfoBar.info(
|
||||
title="已打开文件夹",
|
||||
content=f"文件位置: {folder_path}",
|
||||
parent=self,
|
||||
position=InfoBarPosition.TOP,
|
||||
duration=3000
|
||||
)
|
||||
|
||||
# 直接替换更新按钮的文本和功能
|
||||
self.update_btn.setText("打开文件位置")
|
||||
self.update_btn.setIcon(FluentIcon.FOLDER)
|
||||
# 断开原有连接
|
||||
self.update_btn.clicked.disconnect()
|
||||
# 连接新功能
|
||||
self.update_btn.clicked.connect(open_file_location)
|
||||
|
||||
# 修改取消按钮为清理按钮
|
||||
self.cancel_btn.setText("清理临时文件")
|
||||
self.cancel_btn.setIcon(FluentIcon.DELETE)
|
||||
self.cancel_btn.clicked.disconnect()
|
||||
|
||||
def cleanup_temp_files():
|
||||
if self.updater:
|
||||
self.updater.cleanup()
|
||||
InfoBar.success(
|
||||
title="已清理",
|
||||
content="临时文件已清理完成",
|
||||
parent=self,
|
||||
position=InfoBarPosition.TOP,
|
||||
duration=2000
|
||||
)
|
||||
# 重置界面
|
||||
self.update_info_card.hide()
|
||||
self.check_update_card.setContent("检查是否有新版本可用")
|
||||
|
||||
self.cancel_btn.clicked.connect(cleanup_temp_files)
|
||||
|
||||
def cancel_update(self):
|
||||
"""取消更新"""
|
||||
if self.updater and self.updater.isRunning():
|
||||
if hasattr(self, 'updater') and self.updater and self.updater.isRunning():
|
||||
self.updater.cancel_update()
|
||||
self.updater.cleanup()
|
||||
|
||||
self.update_info_card.hide()
|
||||
self.check_update_card.setContent("检查是否有新版本可用")
|
||||
|
||||
Binary file not shown.
Binary file not shown.
BIN
app/tools/__pycache__/auto_updater.cpython-39.pyc
Normal file
BIN
app/tools/__pycache__/auto_updater.cpython-39.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
app/tools/__pycache__/multi_thread_downloader.cpython-39.pyc
Normal file
BIN
app/tools/__pycache__/multi_thread_downloader.cpython-39.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
app/tools/__pycache__/update_check_thread.cpython-39.pyc
Normal file
BIN
app/tools/__pycache__/update_check_thread.cpython-39.pyc
Normal file
Binary file not shown.
Binary file not shown.
@@ -18,13 +18,16 @@ 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)
|
||||
download_progress = pyqtSignal(int, int, float, float) # 下载进度: 已下载字节, 总字节, 速度MB/s, 剩余时间秒
|
||||
status_changed = pyqtSignal(str) # 状态变化信号
|
||||
error_occurred = pyqtSignal(str) # 错误信号
|
||||
update_completed = pyqtSignal() # 更新完成信号
|
||||
update_completed = pyqtSignal(str) # 更新完成信号,可选包含文件路径
|
||||
update_cancelled = pyqtSignal() # 更新取消信号
|
||||
|
||||
|
||||
@@ -43,6 +46,9 @@ class AutoUpdater(QThread):
|
||||
self.app_dir = self._get_app_directory()
|
||||
self.temp_dir = None
|
||||
|
||||
# 多线程下载器
|
||||
self.downloader = None
|
||||
|
||||
def _get_app_directory(self) -> str:
|
||||
"""获取应用程序目录"""
|
||||
if self.is_frozen:
|
||||
@@ -55,6 +61,8 @@ class AutoUpdater(QThread):
|
||||
def cancel_update(self):
|
||||
"""取消更新"""
|
||||
self.cancelled = True
|
||||
if self.downloader:
|
||||
self.downloader.cancel()
|
||||
self.signals.update_cancelled.emit()
|
||||
|
||||
def check_for_updates(self) -> Optional[dict]:
|
||||
@@ -70,12 +78,15 @@ class AutoUpdater(QThread):
|
||||
latest_version = release_data["tag_name"].lstrip("v")
|
||||
|
||||
if vparse(latest_version) > vparse(self.current_version):
|
||||
download_url = self._get_download_url(release_data)
|
||||
asset_size = self._get_asset_size(release_data, download_url) if download_url else 0
|
||||
return {
|
||||
'version': latest_version,
|
||||
'download_url': self._get_download_url(release_data),
|
||||
'download_url': download_url,
|
||||
'release_notes': release_data.get('body', ''),
|
||||
'release_date': release_data.get('published_at', ''),
|
||||
'asset_name': self._get_asset_name(release_data)
|
||||
'asset_name': self._get_asset_name(release_data),
|
||||
'asset_size': asset_size
|
||||
}
|
||||
return None
|
||||
else:
|
||||
@@ -100,17 +111,32 @@ class AutoUpdater(QThread):
|
||||
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'):
|
||||
# 优先选择 dmg 或 zip 文件
|
||||
if name.endswith('.dmg'):
|
||||
return asset['browser_download_url']
|
||||
if name.endswith('.zip') and 'macos' in name:
|
||||
return asset['browser_download_url']
|
||||
elif system == 'linux':
|
||||
if name.endswith('.tar.gz') or name.endswith('.zip'):
|
||||
if name.endswith('.tar.gz') or (name.endswith('.zip') and 'linux' in name):
|
||||
return asset['browser_download_url']
|
||||
|
||||
# 如果没找到特定平台的,返回第一个可用文件
|
||||
# 如果没找到特定平台的,在 macOS 上避免选择 .exe 文件
|
||||
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']
|
||||
if system == 'darwin':
|
||||
# macOS 优先选择非 exe 文件
|
||||
if name.endswith('.zip') or name.endswith('.dmg') or name.endswith('.tar.gz'):
|
||||
return asset['browser_download_url']
|
||||
else:
|
||||
if any(name.endswith(ext) for ext in ['.zip', '.exe', '.dmg', '.tar.gz']):
|
||||
return asset['browser_download_url']
|
||||
|
||||
# 最后才选择 exe 文件(如果没有其他选择)
|
||||
if system == 'darwin':
|
||||
for asset in assets:
|
||||
name = asset['name'].lower()
|
||||
if name.endswith('.exe'):
|
||||
return asset['browser_download_url']
|
||||
|
||||
return None
|
||||
|
||||
@@ -121,46 +147,95 @@ class AutoUpdater(QThread):
|
||||
return os.path.basename(urlparse(download_url).path)
|
||||
return None
|
||||
|
||||
def _get_asset_size(self, release_data: dict, download_url: str) -> int:
|
||||
"""获取资源文件大小"""
|
||||
if not download_url:
|
||||
return 0
|
||||
|
||||
assets = release_data.get('assets', [])
|
||||
for asset in assets:
|
||||
if asset.get('browser_download_url') == download_url:
|
||||
return asset.get('size', 0)
|
||||
return 0
|
||||
|
||||
def download_update(self, download_url: str, filename: str) -> Optional[str]:
|
||||
"""下载更新文件"""
|
||||
"""下载更新文件 - 使用简单的单线程下载"""
|
||||
try:
|
||||
self.signals.status_changed.emit("正在下载更新...")
|
||||
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)
|
||||
print(f"Downloading to: {file_path}")
|
||||
|
||||
# 使用简单的requests下载
|
||||
import requests
|
||||
headers = {
|
||||
'User-Agent': 'MRobot-Updater/1.0',
|
||||
'Accept': '*/*',
|
||||
}
|
||||
|
||||
response = requests.get(download_url, headers=headers, stream=True, timeout=30)
|
||||
response.raise_for_status()
|
||||
|
||||
total_size = int(response.headers.get('content-length', 0))
|
||||
# 获取文件总大小
|
||||
total_size = int(response.headers.get('Content-Length', 0))
|
||||
downloaded_size = 0
|
||||
|
||||
# 确保目录存在
|
||||
os.makedirs(os.path.dirname(file_path), exist_ok=True)
|
||||
|
||||
# 下载文件
|
||||
with open(file_path, 'wb') as f:
|
||||
for chunk in response.iter_content(chunk_size=8192):
|
||||
if self.cancelled:
|
||||
print("Download cancelled by user")
|
||||
return None
|
||||
|
||||
if chunk:
|
||||
f.write(chunk)
|
||||
downloaded_size += len(chunk)
|
||||
|
||||
# 更新进度
|
||||
if total_size > 0:
|
||||
progress = int((downloaded_size / total_size) * 50) # 下载占50%进度
|
||||
progress = int((downloaded_size / total_size) * 100)
|
||||
self.signals.progress_changed.emit(progress)
|
||||
|
||||
# 发送详细进度信息
|
||||
speed = 0 # 简化版本不计算速度
|
||||
remaining = 0
|
||||
self.signals.download_progress.emit(downloaded_size, total_size, speed, remaining)
|
||||
|
||||
self.signals.status_changed.emit("下载完成")
|
||||
return file_path
|
||||
# 验证下载的文件
|
||||
if os.path.exists(file_path) and os.path.getsize(file_path) > 0:
|
||||
print(f"Download completed successfully: {file_path}")
|
||||
self.signals.status_changed.emit("下载完成!")
|
||||
return file_path
|
||||
else:
|
||||
raise Exception("下载的文件不存在或为空")
|
||||
|
||||
except Exception as e:
|
||||
self.signals.error_occurred.emit(f"下载失败: {str(e)}")
|
||||
error_msg = f"下载失败: {str(e)}"
|
||||
print(f"Download error: {error_msg}")
|
||||
self.signals.error_occurred.emit(error_msg)
|
||||
return None
|
||||
|
||||
def _on_download_progress(self, downloaded: int, total: int, speed: float, remaining: float):
|
||||
"""处理下载进度"""
|
||||
# 转发详细下载进度
|
||||
self.signals.download_progress.emit(downloaded, total, speed, remaining)
|
||||
|
||||
# 计算总体进度 (下载占80%,其他操作占20%)
|
||||
if total > 0:
|
||||
download_progress = int((downloaded / total) * 80)
|
||||
self.signals.progress_changed.emit(download_progress)
|
||||
|
||||
def extract_update(self, file_path: str) -> Optional[str]:
|
||||
"""解压更新文件"""
|
||||
try:
|
||||
self.signals.status_changed.emit("正在解压文件...")
|
||||
self.signals.progress_changed.emit(50)
|
||||
self.signals.progress_changed.emit(85)
|
||||
|
||||
if not self.temp_dir:
|
||||
raise Exception("临时目录未初始化")
|
||||
@@ -176,10 +251,19 @@ class AutoUpdater(QThread):
|
||||
import tarfile
|
||||
with tarfile.open(file_path, 'r:gz') as tar_ref:
|
||||
tar_ref.extractall(extract_dir)
|
||||
elif file_path.endswith('.exe'):
|
||||
# 对于 .exe 文件,在非 Windows 系统上提示手动安装
|
||||
current_system = platform.system()
|
||||
if current_system != 'Windows':
|
||||
self.signals.error_occurred.emit(f"下载的是 Windows 安装程序,当前系统是 {current_system}。\n请手动下载适合您系统的版本。")
|
||||
return None
|
||||
else:
|
||||
# Windows 系统直接返回文件路径,由安装函数处理
|
||||
return file_path
|
||||
else:
|
||||
raise Exception(f"不支持的文件格式: {file_path}")
|
||||
|
||||
self.signals.progress_changed.emit(70)
|
||||
self.signals.progress_changed.emit(90)
|
||||
self.signals.status_changed.emit("解压完成")
|
||||
return extract_dir
|
||||
|
||||
@@ -191,7 +275,7 @@ class AutoUpdater(QThread):
|
||||
"""安装更新"""
|
||||
try:
|
||||
self.signals.status_changed.emit("正在安装更新...")
|
||||
self.signals.progress_changed.emit(80)
|
||||
self.signals.progress_changed.emit(95)
|
||||
|
||||
if not self.temp_dir:
|
||||
raise Exception("临时目录未初始化")
|
||||
@@ -206,7 +290,7 @@ class AutoUpdater(QThread):
|
||||
# 复制新文件
|
||||
self._copy_update_files(extract_dir)
|
||||
|
||||
self.signals.progress_changed.emit(95)
|
||||
self.signals.progress_changed.emit(99)
|
||||
self.signals.status_changed.emit("安装完成")
|
||||
|
||||
return True
|
||||
@@ -295,43 +379,81 @@ class AutoUpdater(QThread):
|
||||
|
||||
def cleanup(self):
|
||||
"""清理临时文件"""
|
||||
print(f"cleanup() called, temp_dir: {self.temp_dir}") # 调试输出
|
||||
if self.temp_dir and os.path.exists(self.temp_dir):
|
||||
try:
|
||||
print(f"Removing temp directory: {self.temp_dir}") # 调试输出
|
||||
shutil.rmtree(self.temp_dir)
|
||||
print("Temp directory removed successfully") # 调试输出
|
||||
except Exception as e:
|
||||
print(f"清理临时文件失败: {e}")
|
||||
else:
|
||||
print("No temp directory to clean up") # 调试输出
|
||||
|
||||
def run(self):
|
||||
"""执行更新流程"""
|
||||
try:
|
||||
self.signals.status_changed.emit("开始更新流程...")
|
||||
|
||||
# 检查更新
|
||||
self.signals.status_changed.emit("正在获取更新信息...")
|
||||
update_info = self.check_for_updates()
|
||||
if not update_info or self.cancelled:
|
||||
self.signals.status_changed.emit("未找到更新信息或已取消")
|
||||
return
|
||||
|
||||
self.signals.status_changed.emit(f"准备下载版本 {update_info['version']}")
|
||||
|
||||
# 下载更新
|
||||
downloaded_file = self.download_update(
|
||||
update_info['download_url'],
|
||||
update_info['asset_name']
|
||||
)
|
||||
if not downloaded_file or self.cancelled:
|
||||
self.signals.status_changed.emit("下载失败或已取消")
|
||||
return
|
||||
|
||||
self.signals.status_changed.emit("下载完成!")
|
||||
self.signals.progress_changed.emit(100)
|
||||
|
||||
# 检查是否为exe文件且当前系统非Windows
|
||||
current_system = platform.system()
|
||||
print(f"Downloaded file: {downloaded_file}") # 调试输出
|
||||
print(f"Current system: {current_system}") # 调试输出
|
||||
if downloaded_file.endswith('.exe') and current_system != 'Windows':
|
||||
# 直接完成,返回文件路径
|
||||
print(f"Emitting update_completed signal with file path: {downloaded_file}") # 调试输出
|
||||
self.signals.update_completed.emit(downloaded_file)
|
||||
# 不要立即清理,让用户有时间访问文件
|
||||
print("Skipping cleanup to preserve downloaded file") # 调试输出
|
||||
return
|
||||
|
||||
# 对于其他情况,继续原有流程
|
||||
self.signals.status_changed.emit("下载完成,开始解压...")
|
||||
|
||||
# 解压更新
|
||||
extract_dir = self.extract_update(downloaded_file)
|
||||
if not extract_dir or self.cancelled:
|
||||
self.signals.status_changed.emit("解压失败或已取消")
|
||||
return
|
||||
|
||||
# 安装更新
|
||||
self.signals.status_changed.emit("开始安装更新...")
|
||||
if self.install_update(extract_dir) and not self.cancelled:
|
||||
self.signals.progress_changed.emit(100)
|
||||
self.signals.update_completed.emit()
|
||||
self.signals.status_changed.emit("更新安装完成")
|
||||
self.signals.update_completed.emit("") # 传递空字符串表示正常安装完成
|
||||
else:
|
||||
self.signals.status_changed.emit("安装失败或已取消")
|
||||
|
||||
except Exception as e:
|
||||
self.signals.error_occurred.emit(f"更新过程中发生错误: {str(e)}")
|
||||
error_msg = f"更新过程中发生错误: {str(e)}"
|
||||
print(f"AutoUpdater error: {error_msg}") # 调试输出
|
||||
self.signals.error_occurred.emit(error_msg)
|
||||
finally:
|
||||
# 清理临时文件
|
||||
self.cleanup()
|
||||
# 对于下载完成的情况,延迟清理临时文件
|
||||
# 这样用户有时间访问下载的文件
|
||||
pass # 暂时不在这里清理
|
||||
|
||||
|
||||
def check_update_availability(current_version: str, repo: str = "goldenfishs/MRobot") -> Optional[dict]:
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import requests
|
||||
import platform
|
||||
from packaging.version import parse as vparse
|
||||
from typing import Optional
|
||||
|
||||
def check_update(local_version, repo="goldenfishs/MRobot"):
|
||||
url = f"https://api.github.com/repos/{repo}/releases/latest"
|
||||
@@ -11,4 +13,62 @@ def check_update(local_version, repo="goldenfishs/MRobot"):
|
||||
else:
|
||||
return None
|
||||
else:
|
||||
raise RuntimeError("GitHub API 请求失败")
|
||||
raise RuntimeError("GitHub API 请求失败")
|
||||
|
||||
def check_update_availability(current_version: str, repo: str = "goldenfishs/MRobot") -> Optional[dict]:
|
||||
"""检查更新并返回详细信息"""
|
||||
try:
|
||||
url = f"https://api.github.com/repos/{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(current_version):
|
||||
# 获取适合当前平台的下载链接和文件大小
|
||||
download_url, asset_size, asset_name = _get_platform_asset(release_data)
|
||||
|
||||
return {
|
||||
'version': latest_version,
|
||||
'download_url': download_url,
|
||||
'asset_size': asset_size,
|
||||
'asset_name': asset_name,
|
||||
'release_notes': release_data.get('body', ''),
|
||||
'release_date': release_data.get('published_at', ''),
|
||||
}
|
||||
return None
|
||||
else:
|
||||
raise Exception(f"GitHub API请求失败: {response.status_code}")
|
||||
|
||||
except Exception as e:
|
||||
raise Exception(f"检查更新失败: {str(e)}")
|
||||
|
||||
def _get_platform_asset(release_data: dict) -> tuple:
|
||||
"""获取适合当前平台的资源文件信息"""
|
||||
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'], asset.get('size', 0), asset['name']
|
||||
if name.endswith('.exe') or name.endswith('.zip'):
|
||||
return asset['browser_download_url'], asset.get('size', 0), asset['name']
|
||||
elif system == 'darwin': # macOS
|
||||
if name.endswith('.dmg') or name.endswith('.zip'):
|
||||
return asset['browser_download_url'], asset.get('size', 0), asset['name']
|
||||
elif system == 'linux':
|
||||
if name.endswith('.tar.gz') or name.endswith('.zip'):
|
||||
return asset['browser_download_url'], asset.get('size', 0), asset['name']
|
||||
|
||||
# 如果没找到特定平台的,返回第一个可用文件
|
||||
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'], asset.get('size', 0), asset['name']
|
||||
|
||||
return None, 0, None
|
||||
34
app/tools/update_check_thread.py
Normal file
34
app/tools/update_check_thread.py
Normal file
@@ -0,0 +1,34 @@
|
||||
"""
|
||||
更新检查线程
|
||||
避免阻塞UI界面的更新检查
|
||||
"""
|
||||
|
||||
from PyQt5.QtCore import QThread, pyqtSignal
|
||||
from app.tools.check_update import check_update_availability
|
||||
|
||||
|
||||
class UpdateCheckThread(QThread):
|
||||
"""更新检查线程"""
|
||||
|
||||
# 信号定义
|
||||
update_found = pyqtSignal(dict) # 发现更新
|
||||
no_update = pyqtSignal() # 无更新
|
||||
error_occurred = pyqtSignal(str) # 检查出错
|
||||
|
||||
def __init__(self, current_version: str, repo: str = "goldenfishs/MRobot"):
|
||||
super().__init__()
|
||||
self.current_version = current_version
|
||||
self.repo = repo
|
||||
|
||||
def run(self):
|
||||
"""执行更新检查"""
|
||||
try:
|
||||
update_info = check_update_availability(self.current_version, self.repo)
|
||||
|
||||
if update_info:
|
||||
self.update_found.emit(update_info)
|
||||
else:
|
||||
self.no_update.emit()
|
||||
|
||||
except Exception as e:
|
||||
self.error_occurred.emit(str(e))
|
||||
Reference in New Issue
Block a user