diff --git a/.gitea/scripts/gui_version_creator.py b/.gitea/scripts/gui_version_creator.py index 7b0f74f..b6aa2f6 100644 --- a/.gitea/scripts/gui_version_creator.py +++ b/.gitea/scripts/gui_version_creator.py @@ -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, diff --git a/.gitea/scripts/upload_installer_to_ks3.py b/.gitea/scripts/upload_installer_to_ks3.py new file mode 100644 index 0000000..ac9c870 --- /dev/null +++ b/.gitea/scripts/upload_installer_to_ks3.py @@ -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()) + diff --git a/.gitea/workflows/gui-version-release.yml b/.gitea/workflows/gui-version-release.yml index 2050371..02fa668 100644 --- a/.gitea/workflows/gui-version-release.yml +++ b/.gitea/workflows/gui-version-release.yml @@ -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() diff --git a/WebSocket/BackendClient.py b/WebSocket/BackendClient.py index c1b9f84..31972bd 100644 --- a/WebSocket/BackendClient.py +++ b/WebSocket/BackendClient.py @@ -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}") @@ -1234,19 +1228,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) # ==================== 辅助方法 ==================== diff --git a/WebSocket/backend_singleton.py b/WebSocket/backend_singleton.py index e44edcd..92c324d 100644 --- a/WebSocket/backend_singleton.py +++ b/WebSocket/backend_singleton.py @@ -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"): """内部日志方法""" @@ -148,14 +145,9 @@ class WebSocketManager: self._log(f"Token验证失败: {error_content}", "ERROR") 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() @@ -189,14 +181,8 @@ class WebSocketManager: self._log(f"Token验证失败: {error_content}", "ERROR") 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) diff --git a/main.py b/main.py index f335e1b..418eb61 100644 --- a/main.py +++ b/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): @@ -55,10 +49,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() @@ -446,78 +436,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): """定时器触发的汇总显示更新""" @@ -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,47 +832,53 @@ class LoginWindow(QMainWindow): return "" def trigger_update(self, download_url, latest_version): - """触发更新下载""" - try: - # 检查下载地址是否有效 - if not download_url or download_url.strip() == "": - self.add_log("⚠️ 下载地址为空,无法打开更新页面", "WARNING") - QMessageBox.warning( - self, - "下载地址缺失", - f"版本 {latest_version} 的下载地址暂未配置。\n\n请联系管理员或稍后再试。", - QMessageBox.Ok - ) - return - - self.add_log(f"📂 准备打开下载页面: {download_url}", "INFO") - - # 使用线程安全的方式打开浏览器 + """触发更新下载(不阻塞主程序)""" + import webbrowser + import threading + + # 检查下载地址是否有效 + if not download_url or download_url.strip() == "": + self.add_log("⚠️ 下载地址为空,无法打开更新页面", "WARNING") + QMessageBox.warning( + self, + "下载地址缺失", + f"版本 {latest_version} 的下载地址暂未配置。\n\n请联系管理员或稍后再试。", + QMessageBox.Ok + ) + 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") + + # 在独立线程中打开浏览器,确保不阻塞GUI主线程 + def open_browser_thread(): try: - import webbrowser - import threading - - def open_browser(): - """在独立线程中打开浏览器,避免阻塞主线程""" - try: - webbrowser.open(download_url) - 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() - - 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") + webbrowser.open(download_url) + self.add_log("✅ 浏览器已打开,下载即将开始", "SUCCESS") + except Exception as e: + self.add_log(f"❌ 打开浏览器失败: {e}", "ERROR") + + thread = threading.Thread(target=open_browser_thread, daemon=True) + thread.start() + + self.add_log("✅ 已启动下载,程序继续运行", "INFO") def main():