实现更新版本管理
This commit is contained in:
@@ -398,13 +398,10 @@ class DatabaseVersionManager:
|
||||
f"+{stats['lines_added']}/-{stats['lines_deleted']} 行")
|
||||
|
||||
# 6. 创建版本记录
|
||||
# 生成下载地址(标准格式:指向实际安装包文件)
|
||||
# 生成下载地址(KS3对象存储 - 广州节点)
|
||||
installer_filename = f"ShuiDi_AI_Assistant_Setup_v{next_version}.exe"
|
||||
download_url = f"https://shuidrop.com/download/gui/{installer_filename}"
|
||||
# 完整示例: https://shuidrop.com/download/gui/ShuiDi_AI_Assistant_Setup_v1.4.12.exe
|
||||
|
||||
# 临时测试可以改为:
|
||||
# download_url = "https://www.baidu.com"
|
||||
download_url = f"https://ks3-cn-guangzhou.ksyuncs.com/shuidrop-chat-server/installers/{installer_filename}"
|
||||
# 完整示例: https://ks3-cn-guangzhou.ksyuncs.com/shuidrop-chat-server/installers/ShuiDi_AI_Assistant_Setup_v1.5.0.exe
|
||||
|
||||
version_record = {
|
||||
'version': next_version,
|
||||
|
||||
141
.gitea/scripts/upload_installer_to_ks3.py
Normal file
141
.gitea/scripts/upload_installer_to_ks3.py
Normal file
@@ -0,0 +1,141 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Upload GUI installer to KS3 object storage
|
||||
- Target directory: installers/
|
||||
- Set proper HTTP headers for browser download
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from ks3.connection import Connection
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s [%(levelname)s] %(message)s'
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# KS3 Configuration (using actual project config)
|
||||
KS3_ACCESS_KEY = 'AKLT0Ey7Nq7ZSXykki0X0RGG'
|
||||
KS3_SECRET_KEY = 'OONxkt9wwJa1FIco21vCbirs1HB6AGzDWdRyV0k2'
|
||||
KS3_ENDPOINT = 'ks3-cn-guangzhou.ksyuncs.com' # Guangzhou region
|
||||
KS3_BUCKET = 'shuidrop-chat-server'
|
||||
KS3_IS_SECURE = True # Use HTTPS
|
||||
KS3_PREFIX = 'installers/'
|
||||
|
||||
|
||||
def get_ks3_connection():
|
||||
"""Get KS3 connection"""
|
||||
try:
|
||||
connection = Connection(
|
||||
KS3_ACCESS_KEY,
|
||||
KS3_SECRET_KEY,
|
||||
host=KS3_ENDPOINT,
|
||||
is_secure=KS3_IS_SECURE,
|
||||
timeout=300,
|
||||
)
|
||||
logger.info(f"KS3 connection established: {KS3_ENDPOINT}")
|
||||
return connection
|
||||
except Exception as e:
|
||||
logger.error(f"KS3 connection failed: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def find_latest_installer():
|
||||
"""Find the latest generated installer"""
|
||||
project_root = Path(__file__).parent.parent.parent
|
||||
installer_dir = project_root / 'installer' / 'output'
|
||||
|
||||
if not installer_dir.exists():
|
||||
logger.error(f"Installer directory not found: {installer_dir}")
|
||||
return None
|
||||
|
||||
installers = list(installer_dir.glob('*.exe'))
|
||||
if not installers:
|
||||
logger.error(f"No installer files found in: {installer_dir}")
|
||||
return None
|
||||
|
||||
latest_installer = max(installers, key=lambda p: p.stat().st_mtime)
|
||||
file_size_mb = latest_installer.stat().st_size / 1024 / 1024
|
||||
|
||||
logger.info(f"Found installer: {latest_installer.name}")
|
||||
logger.info(f"File size: {file_size_mb:.2f} MB")
|
||||
|
||||
return latest_installer
|
||||
|
||||
|
||||
def upload_installer(connection, installer_path):
|
||||
"""Upload installer to KS3"""
|
||||
try:
|
||||
bucket = connection.get_bucket(KS3_BUCKET)
|
||||
ks3_key = f"{KS3_PREFIX}{installer_path.name}"
|
||||
|
||||
logger.info(f"Starting upload to KS3...")
|
||||
logger.info(f"Target path: {ks3_key}")
|
||||
|
||||
key = bucket.new_key(ks3_key)
|
||||
|
||||
with open(installer_path, 'rb') as f:
|
||||
key.set_contents_from_file(
|
||||
f,
|
||||
headers={
|
||||
'Content-Type': 'application/octet-stream',
|
||||
'Content-Disposition': f'attachment; filename="{installer_path.name}"',
|
||||
'Cache-Control': 'public, max-age=3600',
|
||||
'x-kss-storage-class': 'STANDARD'
|
||||
}
|
||||
)
|
||||
|
||||
key.set_acl('public-read')
|
||||
|
||||
# Generate download URL (using HTTPS)
|
||||
protocol = 'https' if KS3_IS_SECURE else 'http'
|
||||
download_url = f"{protocol}://{KS3_ENDPOINT}/{KS3_BUCKET}/{ks3_key}"
|
||||
|
||||
logger.info(f"Upload successful!")
|
||||
logger.info(f"Download URL: {download_url}")
|
||||
|
||||
return download_url
|
||||
except Exception as e:
|
||||
logger.error(f"Upload failed: {e}")
|
||||
import traceback
|
||||
logger.error(traceback.format_exc())
|
||||
return None
|
||||
|
||||
|
||||
def main():
|
||||
"""Main function"""
|
||||
logger.info("=" * 70)
|
||||
logger.info("Starting GUI installer upload to KS3")
|
||||
logger.info(f"KS3 Endpoint: {KS3_ENDPOINT}")
|
||||
logger.info(f"Bucket: {KS3_BUCKET}")
|
||||
logger.info(f"Target directory: {KS3_PREFIX}")
|
||||
logger.info("=" * 70)
|
||||
|
||||
installer_path = find_latest_installer()
|
||||
if not installer_path:
|
||||
return 1
|
||||
|
||||
connection = get_ks3_connection()
|
||||
if not connection:
|
||||
return 1
|
||||
|
||||
download_url = upload_installer(connection, installer_path)
|
||||
if not download_url:
|
||||
return 1
|
||||
|
||||
logger.info("=" * 70)
|
||||
logger.info("Task completed successfully!")
|
||||
logger.info(f"Installer: {installer_path.name}")
|
||||
logger.info(f"Download URL: {download_url}")
|
||||
logger.info("=" * 70)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
|
||||
@@ -131,6 +131,75 @@ jobs:
|
||||
DB_PASSWORD: password_ee2iQ3
|
||||
DB_PORT: 5400
|
||||
|
||||
# Step 4.5: Build production executable
|
||||
- name: Build production executable
|
||||
if: success()
|
||||
shell: powershell
|
||||
run: |
|
||||
Write-Host "==========================================";
|
||||
Write-Host "Step 4.5: Build production executable";
|
||||
Write-Host "==========================================";
|
||||
|
||||
python build_production.py;
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Host "Build failed";
|
||||
exit 1;
|
||||
}
|
||||
|
||||
Write-Host "Production build completed successfully";
|
||||
Write-Host "";
|
||||
|
||||
# Step 4.6: Build NSIS installer
|
||||
- name: Build NSIS installer
|
||||
if: success()
|
||||
shell: powershell
|
||||
run: |
|
||||
Write-Host "==========================================";
|
||||
Write-Host "Step 4.6: Build NSIS installer";
|
||||
Write-Host "==========================================";
|
||||
|
||||
cd installer;
|
||||
python build_installer.py;
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Host "Installer build failed";
|
||||
exit 1;
|
||||
}
|
||||
|
||||
$installers = Get-ChildItem -Path "output" -Filter "*.exe" -ErrorAction SilentlyContinue;
|
||||
if (-not $installers -or $installers.Count -eq 0) {
|
||||
Write-Host "No installer file found";
|
||||
exit 1;
|
||||
}
|
||||
|
||||
$installerName = $installers[0].Name;
|
||||
$installerSize = [math]::Round($installers[0].Length / 1MB, 2);
|
||||
|
||||
Write-Host "Installer built successfully";
|
||||
Write-Host " Filename: $installerName";
|
||||
Write-Host " Size: $installerSize MB";
|
||||
Write-Host "";
|
||||
|
||||
# Step 4.7: Upload installer to KS3
|
||||
- name: Upload installer to KS3
|
||||
if: success()
|
||||
shell: powershell
|
||||
run: |
|
||||
Write-Host "==========================================";
|
||||
Write-Host "Step 4.7: Upload installer to KS3";
|
||||
Write-Host "==========================================";
|
||||
|
||||
pip install ks3 2>$null;
|
||||
|
||||
python .gitea/scripts/upload_installer_to_ks3.py;
|
||||
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Host "KS3 upload failed, but version release continues";
|
||||
Write-Host " You can manually upload the installer later";
|
||||
} else {
|
||||
Write-Host "Installer uploaded to KS3 successfully";
|
||||
}
|
||||
Write-Host "";
|
||||
|
||||
# Step 5: Commit version changes
|
||||
- name: Commit version changes
|
||||
if: success()
|
||||
|
||||
@@ -31,7 +31,6 @@ class BackendClient:
|
||||
self.success_callback: Optional[Callable] = None # 新增:后端连接成功回调
|
||||
self.token_error_callback: Optional[Callable] = None # 新增:token错误回调
|
||||
self.version_callback: Optional[Callable] = None # 新增:版本检查回调
|
||||
self.disconnect_callback: Optional[Callable] = None # 新增:被踢下线回调
|
||||
|
||||
self.is_connected = False
|
||||
|
||||
@@ -266,8 +265,7 @@ class BackendClient:
|
||||
login: Callable = None,
|
||||
success: Callable = None,
|
||||
token_error: Callable = None,
|
||||
version: Callable = None,
|
||||
disconnect: Callable = None):
|
||||
version: Callable = None):
|
||||
"""设置各种消息类型的回调函数"""
|
||||
if store_list:
|
||||
self.store_list_callback = store_list
|
||||
@@ -289,8 +287,6 @@ class BackendClient:
|
||||
self.token_error_callback = token_error
|
||||
if version:
|
||||
self.version_callback = version
|
||||
if disconnect:
|
||||
self.disconnect_callback = disconnect
|
||||
|
||||
def on_connected(self):
|
||||
"""连接成功时的处理"""
|
||||
@@ -447,8 +443,6 @@ class BackendClient:
|
||||
self._handle_staff_list(message)
|
||||
elif msg_type == 'version_response': # 新增:版本检查响应
|
||||
self._handle_version_response(message)
|
||||
elif msg_type == 'disconnect': # 新增:被踢下线
|
||||
self._handle_disconnect(message)
|
||||
else:
|
||||
print(f"未知消息类型: {msg_type}")
|
||||
|
||||
@@ -1235,19 +1229,6 @@ class BackendClient:
|
||||
if self.version_callback:
|
||||
self.version_callback(message)
|
||||
|
||||
def _handle_disconnect(self, message: Dict[str, Any]):
|
||||
"""处理被踢下线消息"""
|
||||
disconnect_message = message.get('message', '您的账号在其他设备登录,当前连接已断开')
|
||||
|
||||
print(f"[断开] 收到后端断开通知: {disconnect_message}")
|
||||
|
||||
# 停止重连机制(不再尝试重连)
|
||||
self.should_stop = True
|
||||
|
||||
# 触发断开回调
|
||||
if self.disconnect_callback:
|
||||
self.disconnect_callback(disconnect_message)
|
||||
|
||||
# ==================== 辅助方法 ====================
|
||||
|
||||
def set_token(self, token: str):
|
||||
|
||||
@@ -54,11 +54,10 @@ class WebSocketManager:
|
||||
'error': None,
|
||||
'platform_connected': None,
|
||||
'token_error': None,
|
||||
'disconnect': None, # 新增:被踢下线回调
|
||||
}
|
||||
|
||||
def set_callbacks(self, log: Callable = None, success: Callable = None, error: Callable = None,
|
||||
platform_connected: Callable = None, token_error: Callable = None, disconnect: Callable = None):
|
||||
platform_connected: Callable = None, token_error: Callable = None):
|
||||
"""设置回调函数"""
|
||||
if log:
|
||||
self.callbacks['log'] = log
|
||||
@@ -70,8 +69,6 @@ class WebSocketManager:
|
||||
self.callbacks['platform_connected'] = platform_connected
|
||||
if token_error:
|
||||
self.callbacks['token_error'] = token_error
|
||||
if disconnect:
|
||||
self.callbacks['disconnect'] = disconnect
|
||||
|
||||
def _log(self, message: str, level: str = "INFO"):
|
||||
"""内部日志方法"""
|
||||
@@ -149,13 +146,8 @@ class WebSocketManager:
|
||||
if self.callbacks['token_error']:
|
||||
self.callbacks['token_error'](error_content)
|
||||
|
||||
def _on_disconnect(disconnect_msg: str):
|
||||
self._log(f"被踢下线: {disconnect_msg}", "WARNING")
|
||||
if self.callbacks['disconnect']:
|
||||
self.callbacks['disconnect'](disconnect_msg)
|
||||
|
||||
backend.set_callbacks(success=_on_backend_success, login=_on_backend_login,
|
||||
token_error=_on_token_error, disconnect=_on_disconnect)
|
||||
token_error=_on_token_error)
|
||||
|
||||
if not backend.is_connected:
|
||||
backend.connect()
|
||||
@@ -190,13 +182,7 @@ class WebSocketManager:
|
||||
if self.callbacks['token_error']:
|
||||
self.callbacks['token_error'](error_content)
|
||||
|
||||
def _on_disconnect(disconnect_msg: str):
|
||||
self._log(f"被踢下线: {disconnect_msg}", "ERROR")
|
||||
if self.callbacks['disconnect']:
|
||||
self.callbacks['disconnect'](disconnect_msg)
|
||||
|
||||
backend.set_callbacks(login=_on_backend_login, success=_on_backend_success,
|
||||
token_error=_on_token_error, disconnect=_on_disconnect)
|
||||
backend.set_callbacks(login=_on_backend_login, success=_on_backend_success, token_error=_on_token_error)
|
||||
backend.connect()
|
||||
|
||||
set_backend_client(backend)
|
||||
|
||||
135
main.py
135
main.py
@@ -25,12 +25,6 @@ class UpdateSignals(QObject):
|
||||
update_available = pyqtSignal(str, str) # (latest_version, download_url)
|
||||
|
||||
|
||||
# 新增: 断开连接信号类(用于线程安全的通知)
|
||||
class DisconnectSignals(QObject):
|
||||
"""断开连接信号"""
|
||||
account_disconnected = pyqtSignal(str) # (disconnect_message)
|
||||
|
||||
|
||||
# 新增: 用户名密码输入对话框类
|
||||
class LoginWindow(QMainWindow):
|
||||
def __init__(self):
|
||||
@@ -56,10 +50,6 @@ class LoginWindow(QMainWindow):
|
||||
self.update_signals = UpdateSignals()
|
||||
self.update_signals.update_available.connect(self._show_update_dialog)
|
||||
|
||||
# 创建断开连接信号对象(线程安全)
|
||||
self.disconnect_signals = DisconnectSignals()
|
||||
self.disconnect_signals.account_disconnected.connect(self._show_disconnect_dialog)
|
||||
|
||||
self.initUI()
|
||||
|
||||
# 延迟设置版本检查器,确保WebSocket连接已建立
|
||||
@@ -447,78 +437,6 @@ class LoginWindow(QMainWindow):
|
||||
except Exception as e:
|
||||
self.add_log(f"处理token错误失败: {e}", "ERROR")
|
||||
|
||||
def on_disconnect(self, disconnect_msg: str):
|
||||
"""处理被踢下线通知(可能在子线程中调用)"""
|
||||
try:
|
||||
self.add_log(f"⚠️ 收到断开通知: {disconnect_msg}", "WARNING")
|
||||
# 通过信号发射到主线程(线程安全)
|
||||
self.disconnect_signals.account_disconnected.emit(disconnect_msg)
|
||||
except Exception as e:
|
||||
self.add_log(f"❌ 处理断开通知失败: {e}", "ERROR")
|
||||
|
||||
def _show_disconnect_dialog(self, disconnect_msg: str):
|
||||
"""显示断开连接对话框(信号槽函数,在主线程中执行)"""
|
||||
try:
|
||||
self.add_log(f"⚠️ 显示断开连接对话框: {disconnect_msg}", "WARNING")
|
||||
|
||||
# 显示警告对话框
|
||||
QMessageBox.warning(
|
||||
self,
|
||||
"连接已断开",
|
||||
f"{disconnect_msg}\n\n请确保用户只在一台设备登录。",
|
||||
QMessageBox.Ok
|
||||
)
|
||||
|
||||
# 断开所有连接
|
||||
self._disconnect_all_connections()
|
||||
|
||||
# 重置 UI 状态(返回登录界面)
|
||||
self._reset_to_login_state()
|
||||
|
||||
self.add_log("✅ 已断开连接,返回登录界面", "INFO")
|
||||
|
||||
except Exception as e:
|
||||
self.add_log(f"❌ 显示断开对话框失败: {e}", "ERROR")
|
||||
import traceback
|
||||
self.add_log(f"详细错误: {traceback.format_exc()}", "ERROR")
|
||||
|
||||
def _disconnect_all_connections(self):
|
||||
"""断开所有连接"""
|
||||
try:
|
||||
self.add_log("🔌 正在断开所有连接...", "INFO")
|
||||
|
||||
# 使用 WebSocket 管理器断开所有连接
|
||||
from WebSocket.backend_singleton import get_websocket_manager
|
||||
ws_manager = get_websocket_manager()
|
||||
ws_manager.disconnect_all()
|
||||
|
||||
self.add_log("✅ 所有连接已断开", "SUCCESS")
|
||||
|
||||
except Exception as e:
|
||||
self.add_log(f"❌ 断开连接失败: {e}", "ERROR")
|
||||
|
||||
def _reset_to_login_state(self):
|
||||
"""重置到登录状态"""
|
||||
try:
|
||||
# 清空已连接平台列表
|
||||
self.connected_platforms.clear()
|
||||
|
||||
# 重置状态标签
|
||||
self.status_label.setText("等待连接...")
|
||||
self.status_label.setStyleSheet(
|
||||
"color: #95a5a6; margin-top: 5px;")
|
||||
|
||||
# 重置按钮状态
|
||||
self.login_btn.setEnabled(True)
|
||||
self.login_btn.setText("重新连接")
|
||||
self.login_btn.setObjectName("loginButton") # 恢复原始样式
|
||||
self.login_btn.setStyleSheet(self.login_btn.styleSheet()) # 刷新样式
|
||||
|
||||
self.add_log("✅ UI 已重置到登录状态", "DEBUG")
|
||||
|
||||
except Exception as e:
|
||||
self.add_log(f"❌ 重置 UI 状态失败: {e}", "ERROR")
|
||||
|
||||
def delayed_platform_summary(self):
|
||||
"""定时器触发的汇总显示更新"""
|
||||
try:
|
||||
@@ -733,8 +651,7 @@ class LoginWindow(QMainWindow):
|
||||
success=lambda: self.add_log("WebSocket连接管理器连接成功", "SUCCESS"),
|
||||
error=lambda error: self.add_log(f"WebSocket连接管理器错误: {error}", "ERROR"),
|
||||
platform_connected=self.on_platform_connected, # 新增:平台连接回调
|
||||
token_error=self.on_token_error, # 新增:token错误回调
|
||||
disconnect=self.on_disconnect # 新增:被踢下线回调
|
||||
token_error=self.on_token_error # 新增:token错误回调
|
||||
)
|
||||
|
||||
# 连接后端
|
||||
@@ -915,8 +832,10 @@ class LoginWindow(QMainWindow):
|
||||
return ""
|
||||
|
||||
def trigger_update(self, download_url, latest_version):
|
||||
"""触发更新下载"""
|
||||
try:
|
||||
"""触发更新下载(不阻塞主程序)"""
|
||||
import webbrowser
|
||||
import threading
|
||||
|
||||
# 检查下载地址是否有效
|
||||
if not download_url or download_url.strip() == "":
|
||||
self.add_log("⚠️ 下载地址为空,无法打开更新页面", "WARNING")
|
||||
@@ -928,34 +847,38 @@ class LoginWindow(QMainWindow):
|
||||
)
|
||||
return
|
||||
|
||||
# 明确提示用户即将下载
|
||||
reply = QMessageBox.information(
|
||||
self,
|
||||
"准备下载更新",
|
||||
f"即将打开浏览器下载 v{latest_version} 安装包。\n\n"
|
||||
f"下载完成后请:\n"
|
||||
f"1. 关闭本程序\n"
|
||||
f"2. 运行下载的安装包\n"
|
||||
f"3. 按照提示完成更新\n\n"
|
||||
f"点击确定继续...",
|
||||
QMessageBox.Ok | QMessageBox.Cancel,
|
||||
QMessageBox.Ok
|
||||
)
|
||||
|
||||
if reply == QMessageBox.Cancel:
|
||||
self.add_log("用户取消下载", "INFO")
|
||||
return
|
||||
|
||||
self.add_log(f"📂 准备打开下载页面: {download_url}", "INFO")
|
||||
|
||||
# 使用线程安全的方式打开浏览器
|
||||
try:
|
||||
import webbrowser
|
||||
import threading
|
||||
|
||||
def open_browser():
|
||||
"""在独立线程中打开浏览器,避免阻塞主线程"""
|
||||
# 在独立线程中打开浏览器,确保不阻塞GUI主线程
|
||||
def open_browser_thread():
|
||||
try:
|
||||
webbrowser.open(download_url)
|
||||
self.add_log("✅ 浏览器已打开", "SUCCESS")
|
||||
self.add_log("✅ 浏览器已打开,下载即将开始", "SUCCESS")
|
||||
except Exception as e:
|
||||
self.add_log(f"❌ 打开浏览器失败: {e}", "ERROR")
|
||||
|
||||
# 在新线程中打开浏览器
|
||||
browser_thread = threading.Thread(target=open_browser, daemon=True)
|
||||
browser_thread.start()
|
||||
thread = threading.Thread(target=open_browser_thread, daemon=True)
|
||||
thread.start()
|
||||
|
||||
self.add_log(f"✅ 已启动浏览器线程", "INFO")
|
||||
|
||||
except Exception as browser_error:
|
||||
self.add_log(f"❌ 浏览器线程启动失败: {browser_error}", "ERROR")
|
||||
|
||||
except Exception as e:
|
||||
self.add_log(f"❌ 触发更新失败: {e}", "ERROR")
|
||||
import traceback
|
||||
self.add_log(f"详细错误: {traceback.format_exc()}", "ERROR")
|
||||
self.add_log("✅ 已启动下载,程序继续运行", "INFO")
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
Reference in New Issue
Block a user