From c58cec750faadce6f3b03794ceb24b213c350849 Mon Sep 17 00:00:00 2001 From: haosicheng Date: Sun, 28 Sep 2025 17:00:02 +0800 Subject: [PATCH] =?UTF-8?q?Todo:=20=E4=BF=AE=E6=94=B9=E5=9B=A0exe=5Ftoken?= =?UTF-8?q?=E9=94=99=E8=AF=AF=E5=AF=BC=E8=87=B4=E7=9A=84=E4=B8=8D=E6=96=AD?= =?UTF-8?q?=E9=87=8D=E8=BF=9E=E9=97=AE=E9=A2=98=20Todo:=20=E4=BF=AE?= =?UTF-8?q?=E6=94=B9=E6=89=93=E5=8C=85=E4=B8=AD.bat=E6=89=93=E5=8C=85?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E4=B8=8E=E6=B5=8B=E8=AF=95=E6=89=93=E5=8C=85?= =?UTF-8?q?=E8=84=9A=E6=9C=AC=E4=B8=8D=E4=B8=80=E8=87=B4=E9=97=AE=E9=A2=98?= =?UTF-8?q?=20New:=20=E6=96=B0=E5=A2=9Einstaller=E5=AE=89=E8=A3=85?= =?UTF-8?q?=E5=8C=85=E7=8E=AF=E5=A2=83=E6=90=AD=E5=BB=BA=E6=95=B0=E6=8D=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- WebSocket/BackendClient.py | 35 +++- WebSocket/backend_singleton.py | 79 +++++--- build_production.py | 29 ++- installer/README.md | 106 +++++++++++ installer/build_installer.py | 325 +++++++++++++++++++++++++++++++++ main.py | 26 ++- quick_build.py | 95 +++++++--- 7 files changed, 640 insertions(+), 55 deletions(-) create mode 100644 installer/README.md create mode 100644 installer/build_installer.py diff --git a/WebSocket/BackendClient.py b/WebSocket/BackendClient.py index a503928..95bf09c 100644 --- a/WebSocket/BackendClient.py +++ b/WebSocket/BackendClient.py @@ -29,6 +29,7 @@ class BackendClient: self.error_callback: Optional[Callable] = None self.login_callback: Optional[Callable] = None # 新增:平台登录(下发cookies)回调 self.success_callback: Optional[Callable] = None # 新增:后端连接成功回调 + self.token_error_callback: Optional[Callable] = None # 新增:token错误回调 self.is_connected = False @@ -261,7 +262,8 @@ class BackendClient: close: Callable = None, error: Callable = None, login: Callable = None, - success: Callable = None): + success: Callable = None, + token_error: Callable = None): """设置各种消息类型的回调函数""" if store_list: self.store_list_callback = store_list @@ -279,6 +281,8 @@ class BackendClient: self.login_callback = login if success: self.success_callback = success + if token_error: + self.token_error_callback = token_error def on_connected(self): """连接成功时的处理""" @@ -338,7 +342,7 @@ class BackendClient: active_store_id = None try: from Utils.JD.JdUtils import WebsocketManager as JdManager - from Utils.Dy.DyUtils import WebsocketManager as DyManager + from Utils.Dy.DyUtils import DouYinWebsocketManager as DyManager from Utils.Pdd.PddUtils import WebsocketManager as PddManager # 检查各平台是否有活跃连接 @@ -429,6 +433,8 @@ class BackendClient: self._handle_login(message) elif msg_type == 'error': self._handle_error_message(message) + elif msg_type == 'error_token': + self._handle_token_error(message) elif msg_type == 'staff_list': self._handle_staff_list(message) else: @@ -1163,10 +1169,35 @@ class BackendClient: def _handle_error_message(self, message: Dict[str, Any]): """处理错误消息""" error_msg = message.get('error', '未知错误') + content = message.get('content', '') + + # 检查是否为token错误(无论type是error还是error_token) + if content == "无效的exe_token" or "无效的exe_token" in content: + print(f"[错误] 检测到token错误: {content}") + self._handle_token_error(message) + return + print(f"后端连接错误: {error_msg}") if self.error_callback: self.error_callback(error_msg, message) + def _handle_token_error(self, message: Dict[str, Any]): + """处理token错误消息 - 无效token时停止重连并显示错误""" + error_content = message.get('content', '无效的exe_token') + print(f"[错误] Token验证失败: {error_content}") + + # 停止重连机制 + self.should_stop = True + self.is_reconnecting = False + + # 触发token错误回调 + if self.token_error_callback: + self.token_error_callback(error_content) + + # 主动关闭连接 + if self.websocket: + asyncio.run_coroutine_threadsafe(self.websocket.close(), self.loop) + def _handle_staff_list(self, message: Dict[str, Any]): """处理客服列表更新消息""" staff_list = message.get('data', {}).get('staff_list', []) diff --git a/WebSocket/backend_singleton.py b/WebSocket/backend_singleton.py index 82032a5..581d39f 100644 --- a/WebSocket/backend_singleton.py +++ b/WebSocket/backend_singleton.py @@ -35,10 +35,11 @@ class WebSocketManager: 'success': None, 'error': None, 'platform_connected': None, + 'token_error': None, } def set_callbacks(self, log: Callable = None, success: Callable = None, error: Callable = None, - platform_connected: Callable = None): # ← 新增参数 + platform_connected: Callable = None, token_error: Callable = None): """设置回调函数""" if log: self.callbacks['log'] = log @@ -48,6 +49,8 @@ class WebSocketManager: self.callbacks['error'] = error if platform_connected: # ← 新增 self.callbacks['platform_connected'] = platform_connected + if token_error: + self.callbacks['token_error'] = token_error def _log(self, message: str, level: str = "INFO"): """内部日志方法""" @@ -84,33 +87,49 @@ class WebSocketManager: backend = get_backend_client() if backend: - # 3 如果有客户端更新token并重连 - backend.set_token(token) + # 检查现有客户端是否因token错误而停止 + if backend.should_stop: + self._log("检测到客户端因token错误已停止,创建新的客户端", "INFO") + # 断开旧客户端 + backend.disconnect() + # 清除旧客户端引用 + set_backend_client(None) + backend = None + else: + # 3 如果有客户端更新token并重连 + backend.set_token(token) + + # 设置回调函数 + def _on_backend_success(): + try: + self._log("连接服务成功", "SUCCESS") + if self.callbacks['success']: + self.callbacks['success']() + except Exception as e: + self._log(f"成功回调执行失败: {e}", "ERROR") - # 设置回调函数 - def _on_backend_success(): - try: - self._log("连接服务成功", "SUCCESS") - if self.callbacks['success']: - self.callbacks['success']() - except Exception as e: - self._log(f"成功回调执行失败: {e}", "ERROR") + def _on_backend_login(platform_name: str, store_id: str, cookies: str): + self._log( + f"收到后端登录指令: 平台={platform_name}, 店铺={store_id}, cookies_len={len(cookies) if cookies else 0}", + "INFO") + self._handle_platform_login(platform_name, store_id, cookies) - def _on_backend_login(platform_name: str, store_id: str, cookies: str): - self._log( - f"收到后端登录指令: 平台={platform_name}, 店铺={store_id}, cookies_len={len(cookies) if cookies else 0}", - "INFO") - self._handle_platform_login(platform_name, store_id, cookies) + def _on_token_error(error_content: str): + self._log(f"Token验证失败: {error_content}", "ERROR") + if self.callbacks['token_error']: + self.callbacks['token_error'](error_content) - backend.set_callbacks(success=_on_backend_success, login=_on_backend_login) + backend.set_callbacks(success=_on_backend_success, login=_on_backend_login, token_error=_on_token_error) + + if not backend.is_connected: + backend.connect() + + self.backend_client = backend + self._log("令牌已提交,已连接后端。等待后端下发平台cookies后自动连接平台...", "SUCCESS") + return True - if not backend.is_connected: - backend.connect() - - self.backend_client = backend - self._log("令牌已提交,已连接后端。等待后端下发平台cookies后自动连接平台...", "SUCCESS") - return True - else: + # 如果没有现有客户端或客户端被重置,创建新的 + if not backend: backend = BackendClient.from_exe_token(token) @@ -128,7 +147,12 @@ class WebSocketManager: except Exception as e: self._log(f"成功回调执行失败: {e}", "ERROR") - backend.set_callbacks(login=_on_backend_login, success=_on_backend_success) + def _on_token_error(error_content: str): + self._log(f"Token验证失败: {error_content}", "ERROR") + if self.callbacks['token_error']: + self.callbacks['token_error'](error_content) + + backend.set_callbacks(login=_on_backend_login, success=_on_backend_success, token_error=_on_token_error) backend.connect() set_backend_client(backend) @@ -402,6 +426,11 @@ class WebSocketManager: self._log("🔄 开始执行 start_with_cookies", "DEBUG") result = asyncio.run(listener.start_with_cookies(store_id=store_id, cookies=data)) self._log(f"📊 start_with_cookies 执行结果: {result}", "DEBUG") + + # Cookie启动成功时也要通知GUI + if result: + self._log("✅ [PDD] Cookie启动成功,平台连接已建立", "SUCCESS") + self._notify_platform_connected("拼多多") # 根据实际登录结果上报状态给后端 if self.backend_client and result not in ["need_verification_code", "verification_code_error", "login_failure"]: diff --git a/build_production.py b/build_production.py index b6ee6a6..16130da 100644 --- a/build_production.py +++ b/build_production.py @@ -85,9 +85,32 @@ def main(): result = subprocess.run(['python', 'quick_build.py'], capture_output=False) if result.returncode == 0: - print("\n🎉 生产环境打包成功!") - print("📁 输出目录: dist/MultiPlatformGUI/") - print("🔇 已禁用所有日志功能,客户端不会产生日志文件") + # 确保输出目录正确性 + if os.path.exists('dist/main') and not os.path.exists('dist/MultiPlatformGUI'): + print("🔄 修正输出目录名称...") + try: + os.rename('dist/main', 'dist/MultiPlatformGUI') + print("✅ 已重命名为: dist/MultiPlatformGUI/") + except Exception as e: + print(f"⚠️ 重命名失败: {e}") + + # 验证最终输出 + if os.path.exists('dist/MultiPlatformGUI'): + print("\n🎉 生产环境打包成功!") + print("📁 输出目录: dist/MultiPlatformGUI/") + print("🔇 已禁用所有日志功能,客户端不会产生日志文件") + + # 验证关键文件 + exe_path = 'dist/MultiPlatformGUI/main.exe' + if os.path.exists(exe_path): + size = os.path.getsize(exe_path) / 1024 / 1024 # MB + print(f"✅ 主程序: main.exe ({size:.1f} MB)") + else: + print("⚠️ 主程序文件未找到") + else: + print("\n❌ 输出目录验证失败") + if os.path.exists('dist/main'): + print("💡 发现: dist/main/ 目录,请手动重命名为 MultiPlatformGUI") else: print("\n❌ 打包失败") diff --git a/installer/README.md b/installer/README.md new file mode 100644 index 0000000..95f9809 --- /dev/null +++ b/installer/README.md @@ -0,0 +1,106 @@ +# GUI 安装包构建工具 + +这个目录包含了使用 NSIS 构建 Windows 安装包的完整解决方案。 + +## 目录结构 + +``` +installer/ +├── build_installer.py # 主要的构建脚本 +├── installer.nsi # 自动生成的 NSIS 脚本 +├── assets/ # 安装包资源文件 +│ ├── icon.png # 应用程序图标 +│ └── license.txt # 软件许可协议 +└── output/ # 生成的安装包输出目录 +``` + +## 使用方法 + +### 方法一:使用批处理脚本(推荐) +```bash +# 在 GUI_main 目录下运行 +build_installer.bat +``` + +### 方法二:直接运行Python脚本 +```bash +# 在 GUI_main/installer 目录下运行 +python build_installer.py +``` + +## 前提条件 + +1. **NSIS 安装** + - 下载地址:https://nsis.sourceforge.io/Download + - 安装后确保 `makensis` 命令在 PATH 中 + +2. **应用程序构建** + - 必须先运行 `build_production.py` 构建应用程序 + - 确保 `dist/MultiPlatformGUI/` 目录存在 + +## 构建流程 + +1. **环境检查**:验证 NSIS 安装和应用程序构建 +2. **准备资源**:复制图标文件和创建许可证 +3. **生成脚本**:自动生成 NSIS 安装脚本 +4. **编译安装包**:使用 makensis 编译最终的安装程序 + +## 输出结果 + +安装包将生成在 `installer/output/` 目录下,文件名格式: +``` +ShuiDi_AI_Assistant_Setup_1.0.0_YYYYMMDD_HHMMSS.exe +``` + +## 安装包功能 + +- ✅ 完整的中文界面 +- ✅ 自动创建开始菜单快捷方式 +- ✅ 自动创建桌面快捷方式 +- ✅ 注册表集成 +- ✅ 完整的卸载功能 +- ✅ 版本信息显示 + +## 自定义配置 + +可以修改 `build_installer.py` 中的以下配置: + +```python +# 应用程序信息 +self.app_name = "水滴AI客服智能助手" +self.app_name_en = "ShuiDi AI Assistant" +self.app_version = "1.0.0" +self.app_publisher = "水滴科技" +self.app_url = "https://www.shuidi.tech" +``` + +## 故障排除 + +### 常见问题 + +1. **找不到 makensis 命令** + - 确保 NSIS 已正确安装 + - 将 NSIS 安装目录添加到 PATH 环境变量 + +2. **找不到 dist 目录** + - 先运行 `build_production.py` 构建应用程序 + +3. **图标文件问题** + - 确保 `static/` 目录下有图标文件 + - 建议准备 ICO 格式的图标文件 + +### 日志和调试 + +构建过程中的详细信息会在控制台显示,包括: +- NSIS 版本信息 +- 文件复制状态 +- 编译过程输出 +- 最终安装包信息 + +## 进一步优化 + +可以考虑添加以下功能: +- 数字签名支持 +- 多语言支持 +- 自定义安装选项 +- 更新检查机制 \ No newline at end of file diff --git a/installer/build_installer.py b/installer/build_installer.py new file mode 100644 index 0000000..0258431 --- /dev/null +++ b/installer/build_installer.py @@ -0,0 +1,325 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +NSIS 安装包构建脚本 +用于创建 GUI 应用程序的Windows安装包 +""" + +import os +import sys +import shutil +import subprocess +import datetime +from pathlib import Path + +class NSISInstaller: + def __init__(self): + self.script_dir = Path(__file__).parent + self.project_root = self.script_dir.parent + self.dist_dir = self.project_root / "dist" / "MultiPlatformGUI" + self.output_dir = self.script_dir / "output" + self.assets_dir = self.script_dir / "assets" + self.nsis_script = self.script_dir / "installer.nsi" + + # 应用程序信息 + self.app_name = "水滴AI客服智能助手" + self.app_name_en = "ShuiDi AI Assistant" + self.app_version = "1.0.0" + self.app_publisher = "水滴智能科技" + self.app_url = "https://shuidrop.com/" + self.exe_name = "main.exe" + + def check_prerequisites(self): + """检查构建前提条件""" + print("🔍 检查构建前提条件...") + + # 检查NSIS安装 + try: + result = subprocess.run(['makensis', '/VERSION'], + capture_output=True, text=True, check=True) + nsis_version = result.stdout.strip() + print(f"✅ NSIS 版本: {nsis_version}") + except (subprocess.CalledProcessError, FileNotFoundError): + print("❌ 错误: 未找到NSIS或makensis命令") + print(" 请从 https://nsis.sourceforge.io/Download 下载并安装NSIS") + print(" 确保将NSIS添加到系统PATH环境变量") + return False + + # 检查dist目录 + if not self.dist_dir.exists(): + print(f"❌ 错误: 未找到构建输出目录 {self.dist_dir}") + print(" 请先运行 build_production.py 构建应用程序") + return False + + # 检查主程序文件 + exe_path = self.dist_dir / self.exe_name + if not exe_path.exists(): + print(f"❌ 错误: 未找到主程序文件 {exe_path}") + return False + + print(f"✅ 找到主程序: {exe_path}") + return True + + def prepare_assets(self): + """准备安装包资源文件""" + print("📁 准备资源文件...") + + # 创建assets目录 + self.assets_dir.mkdir(exist_ok=True) + + # 复制图标文件 + icon_sources = [ + self.project_root / "static" / "ai_assistant_icon_64.png", + self.project_root / "static" / "ai_assistant_icon_32.png", + self.project_root / "static" / "ai_assistant_icon_16.png" + ] + + # 寻找可用的图标文件 + icon_found = False + for icon_source in icon_sources: + if icon_source.exists(): + # 如果有PNG图标,我们需要转换为ICO格式 + # 这里暂时复制PNG,实际项目中建议准备ICO格式图标 + icon_dest = self.assets_dir / "icon.ico" + try: + # 尝试使用PIL转换PNG为ICO格式 + from PIL import Image + img = Image.open(icon_source) + ico_path = self.assets_dir / "icon.ico" + img.save(ico_path, format='ICO', sizes=[(16,16), (32,32), (48,48), (64,64)]) + print(f"✅ 转换图标: {icon_source.name} -> icon.ico") + icon_found = True + break + except ImportError: + print("⚠️ 未安装PIL库,跳过图标转换") + break + except Exception as e: + print(f"⚠️ 图标转换失败: {e}") + continue + + if not icon_found: + print("⚠️ 未找到图标文件,将使用默认图标") + + return icon_found + + def generate_nsis_script(self, has_icon=False): + """生成NSIS安装脚本""" + print("📝 生成NSIS安装脚本...") + + timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") + installer_name = f"{self.app_name_en}_Setup_{self.app_version}_{timestamp}.exe" + + nsis_content = f'''# 水滴AI客服智能助手 NSIS 安装脚本 +# 自动生成于 {datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")} + +Unicode True + +# 定义应用程序信息 +!define APP_NAME "{self.app_name}" +!define APP_NAME_EN "{self.app_name_en}" +!define APP_VERSION "{self.app_version}" +!define APP_PUBLISHER "{self.app_publisher}" +!define APP_URL "{self.app_url}" +!define APP_EXE "{self.exe_name}" + +# 安装程序信息 +Name "${{APP_NAME}} v${{APP_VERSION}}" +OutFile "output\\{installer_name}" +InstallDir "$PROGRAMFILES\\${{APP_NAME_EN}}" +InstallDirRegKey HKLM "Software\\${{APP_PUBLISHER}}\\${{APP_NAME_EN}}" "InstallPath" + +# 界面设置 +!include "MUI2.nsh" +!define MUI_ABORTWARNING +{f'!define MUI_ICON "assets\\\\icon.ico"' if has_icon else ''} +{f'!define MUI_UNICON "assets\\\\icon.ico"' if has_icon else ''} + +# 页面配置 +!insertmacro MUI_PAGE_WELCOME +!insertmacro MUI_PAGE_LICENSE "assets\\license.txt" +!insertmacro MUI_PAGE_DIRECTORY +!insertmacro MUI_PAGE_INSTFILES +!insertmacro MUI_PAGE_FINISH + +!insertmacro MUI_UNPAGE_WELCOME +!insertmacro MUI_UNPAGE_CONFIRM +!insertmacro MUI_UNPAGE_INSTFILES +!insertmacro MUI_UNPAGE_FINISH + +# 语言 +!insertmacro MUI_LANGUAGE "SimpChinese" + +# 版本信息 +VIProductVersion "{self.app_version}.0" +VIAddVersionKey /LANG=2052 "ProductName" "${{APP_NAME}}" +VIAddVersionKey /LANG=2052 "Comments" "水滴AI客服智能助手" +VIAddVersionKey /LANG=2052 "CompanyName" "${{APP_PUBLISHER}}" +VIAddVersionKey /LANG=2052 "FileDescription" "${{APP_NAME}} 安装程序" +VIAddVersionKey /LANG=2052 "FileVersion" "${{APP_VERSION}}" +VIAddVersionKey /LANG=2052 "ProductVersion" "${{APP_VERSION}}" +VIAddVersionKey /LANG=2052 "OriginalFilename" "{installer_name}" +VIAddVersionKey /LANG=2052 "LegalCopyright" "© 2024 ${{APP_PUBLISHER}}" + +# 安装段 +Section "MainSection" SEC01 + SetOutPath "$INSTDIR" + SetOverwrite on + + # 复制所有文件 + File /r "..\\dist\\MultiPlatformGUI\\*.*" + + # 复制图标文件到安装目录(如果有的话) + {f'File "assets\\\\icon.ico"' if has_icon else ''} + + # 创建开始菜单快捷方式 + CreateDirectory "$SMPROGRAMS\\${{APP_NAME}}" + {f'CreateShortCut "$SMPROGRAMS\\\\${{APP_NAME}}\\\\${{APP_NAME}}.lnk" "$INSTDIR\\\\${{APP_EXE}}" "" "$INSTDIR\\\\icon.ico" 0' if has_icon else 'CreateShortCut "$SMPROGRAMS\\\\${{APP_NAME}}\\\\${{APP_NAME}}.lnk" "$INSTDIR\\\\${{APP_EXE}}"'} + {f'CreateShortCut "$SMPROGRAMS\\\\${{APP_NAME}}\\\\卸载${{APP_NAME}}.lnk" "$INSTDIR\\\\uninstall.exe" "" "$INSTDIR\\\\icon.ico" 0' if has_icon else 'CreateShortCut "$SMPROGRAMS\\\\${{APP_NAME}}\\\\卸载${{APP_NAME}}.lnk" "$INSTDIR\\\\uninstall.exe"'} + + # 创建桌面快捷方式 + {f'CreateShortCut "$DESKTOP\\\\${{APP_NAME}}.lnk" "$INSTDIR\\\\${{APP_EXE}}" "" "$INSTDIR\\\\icon.ico" 0' if has_icon else 'CreateShortCut "$DESKTOP\\\\${{APP_NAME}}.lnk" "$INSTDIR\\\\${{APP_EXE}}"'} + + # 写入注册表 + WriteRegStr HKLM "Software\\${{APP_PUBLISHER}}\\${{APP_NAME_EN}}" "InstallPath" "$INSTDIR" + WriteRegStr HKLM "Software\\${{APP_PUBLISHER}}\\${{APP_NAME_EN}}" "Version" "${{APP_VERSION}}" + + # 写入卸载信息 + WriteRegStr HKLM "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\${{APP_NAME_EN}}" "DisplayName" "${{APP_NAME}}" + WriteRegStr HKLM "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\${{APP_NAME_EN}}" "UninstallString" "$INSTDIR\\uninstall.exe" + WriteRegStr HKLM "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\${{APP_NAME_EN}}" "DisplayVersion" "${{APP_VERSION}}" + WriteRegStr HKLM "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\${{APP_NAME_EN}}" "Publisher" "${{APP_PUBLISHER}}" + WriteRegStr HKLM "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\${{APP_NAME_EN}}" "URLInfoAbout" "${{APP_URL}}" + WriteRegDWORD HKLM "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\${{APP_NAME_EN}}" "NoModify" 1 + WriteRegDWORD HKLM "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\${{APP_NAME_EN}}" "NoRepair" 1 + + # 创建卸载程序 + WriteUninstaller "$INSTDIR\\uninstall.exe" +SectionEnd + +# 卸载段 +Section "Uninstall" + # 删除快捷方式 + Delete "$SMPROGRAMS\\${{APP_NAME}}\\${{APP_NAME}}.lnk" + Delete "$SMPROGRAMS\\${{APP_NAME}}\\卸载${{APP_NAME}}.lnk" + RMDir "$SMPROGRAMS\\${{APP_NAME}}" + Delete "$DESKTOP\\${{APP_NAME}}.lnk" + + # 删除安装目录 + RMDir /r "$INSTDIR" + + # 删除注册表项 + DeleteRegKey HKLM "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\${{APP_NAME_EN}}" + DeleteRegKey HKLM "Software\\${{APP_PUBLISHER}}\\${{APP_NAME_EN}}" +SectionEnd +''' + + # 写入NSIS脚本文件 + # 使用UTF-8 BOM编码,NSIS可以正确识别 + with open(self.nsis_script, 'w', encoding='utf-8-sig') as f: + f.write(nsis_content) + + print(f"✅ NSIS脚本已生成: {self.nsis_script}") + return installer_name + + def create_license_file(self): + """创建许可证文件""" + license_file = self.assets_dir / "license.txt" + license_content = f"""软件许可协议 + +{self.app_name} v{self.app_version} + +版权所有 © 2025 {self.app_publisher} + +使用本软件即表示您同意以下条款: + +1. 本软件仅供合法用途使用 +2. 不得对软件进行反向工程、反编译或反汇编 +3. 软件按"原样"提供,不提供任何明示或暗示的保证 +4. 在任何情况下,软件提供者均不对因使用本软件而产生的任何损害承担责任 + +如有疑问,请联系:{self.app_url} +""" + + with open(license_file, 'w', encoding='utf-8-sig') as f: + f.write(license_content) + + print(f"✅ 许可证文件已创建: {license_file}") + + def build_installer(self): + """构建安装包""" + print("🚀 开始构建NSIS安装包...") + + # 创建输出目录 + self.output_dir.mkdir(exist_ok=True) + + # 执行NSIS编译 + try: + cmd = ['makensis', str(self.nsis_script)] + result = subprocess.run(cmd, cwd=str(self.script_dir), + capture_output=True, text=True, check=True) + + print("✅ NSIS编译成功") + if result.stdout: + print("NSIS输出:") + print(result.stdout) + + return True + + except subprocess.CalledProcessError as e: + print("❌ NSIS编译失败") + print(f"错误信息: {e.stderr}") + return False + + def run(self): + """执行完整的构建流程""" + print("=" * 60) + print(f"🔧 {self.app_name} 安装包构建工具") + print("=" * 60) + + try: + # 1. 检查前提条件 + if not self.check_prerequisites(): + return False + + # 2. 准备资源文件 + has_icon = self.prepare_assets() + + # 3. 创建许可证文件 + self.create_license_file() + + # 4. 生成NSIS脚本 + installer_name = self.generate_nsis_script(has_icon) + + # 5. 构建安装包 + if not self.build_installer(): + return False + + # 6. 显示结果 + installer_path = self.output_dir / installer_name + if installer_path.exists(): + installer_size = installer_path.stat().st_size / (1024 * 1024) + print("\n" + "=" * 60) + print("🎉 安装包构建成功!") + print(f"📁 安装包位置: {installer_path}") + print(f"📏 文件大小: {installer_size:.1f} MB") + print("🚀 可以直接分发给用户使用") + print("=" * 60) + return True + else: + print("❌ 安装包文件未找到") + return False + + except Exception as e: + print(f"❌ 构建过程出错: {e}") + return False + +def main(): + """主函数""" + installer = NSISInstaller() + success = installer.run() + + if not success: + sys.exit(1) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/main.py b/main.py index ae81ce9..e9f42c4 100644 --- a/main.py +++ b/main.py @@ -402,6 +402,29 @@ class LoginWindow(QMainWindow): except Exception as e: self.add_log(f"处理平台连接事件失败: {e}", "ERROR") + def on_token_error(self, error_content: str): + """处理token错误 - 显示红色错误信息并停止所有操作""" + try: + self.add_log(f"Token验证失败: {error_content}", "ERROR") + + # 在状态标签显示红色错误信息 + self.status_label.setText(f"🔴 {error_content}") + self.status_label.setStyleSheet( + "color: #dc3545; background: rgba(220, 53, 69, 0.1); border-radius: 12px; padding: 5px 10px; font-weight: bold;") + + # 重置按钮状态 + self.login_btn.setEnabled(True) + self.login_btn.setText("重新连接") + self.login_btn.setObjectName("loginButton") # 恢复原始样式 + + # 清空已连接平台列表 + self.connected_platforms.clear() + + self.add_log("由于token无效,已停止所有连接操作", "ERROR") + + except Exception as e: + self.add_log(f"处理token错误失败: {e}", "ERROR") + def delayed_platform_summary(self): """定时器触发的汇总显示更新""" try: @@ -615,7 +638,8 @@ class LoginWindow(QMainWindow): log=self.add_log, success=lambda: self.add_log("WebSocket连接管理器连接成功", "SUCCESS"), error=lambda error: self.add_log(f"WebSocket连接管理器错误: {error}", "ERROR"), - platform_connected=self.on_platform_connected # 新增:平台连接回调 + platform_connected=self.on_platform_connected, # 新增:平台连接回调 + token_error=self.on_token_error # 新增:token错误回调 ) # 连接后端 diff --git a/quick_build.py b/quick_build.py index 472d087..21ff5a4 100644 --- a/quick_build.py +++ b/quick_build.py @@ -7,6 +7,7 @@ import os import subprocess import shutil +from pathlib import Path def clean_build(): @@ -96,17 +97,31 @@ def build_with_command(): print("✅ 打包成功!") print("📁 打包结果: dist/main/") - # 重命名目录 + # 重命名目录为统一的输出路径 if os.path.exists('dist/main'): try: + # 如果目标目录已存在,先删除 if os.path.exists('dist/MultiPlatformGUI'): + print("🗑️ 删除旧的 MultiPlatformGUI 目录...") shutil.rmtree('dist/MultiPlatformGUI') + + # 重命名 os.rename('dist/main', 'dist/MultiPlatformGUI') print("✅ 已重命名为: dist/MultiPlatformGUI/") + + # 验证重命名结果 + if os.path.exists('dist/MultiPlatformGUI/main.exe'): + exe_size = os.path.getsize('dist/MultiPlatformGUI/main.exe') / 1024 / 1024 + print(f"✅ 主程序验证通过: main.exe ({exe_size:.1f} MB)") + else: + print("⚠️ 重命名后主程序文件验证失败") + except Exception as e: print(f"⚠️ 重命名失败: {e}") print("💡 可以手动重命名: dist/main -> dist/MultiPlatformGUI") print("📁 当前可用路径: dist/main/") + else: + print("⚠️ 未找到 dist/main 目录,重命名跳过") # 创建使用说明 try: @@ -221,31 +236,62 @@ def verify_result(): """验证打包结果""" print("\n🔍 验证打包结果...") - # 先检查MultiPlatformGUI,再检查main目录 - base_dirs = ["dist/MultiPlatformGUI", "dist/main"] + # 优先检查MultiPlatformGUI(标准输出目录) + target_dir = "dist/MultiPlatformGUI" + fallback_dir = "dist/main" - for base_dir in base_dirs: - if os.path.exists(base_dir): - exe_path = f"{base_dir}/main.exe" - dll_path = f"{base_dir}/_internal/Utils/PythonNew32/SaiNiuApi.dll" + if os.path.exists(target_dir): + return _verify_directory(target_dir) + elif os.path.exists(fallback_dir): + print(f"⚠️ 发现备用目录: {fallback_dir}") + print("💡 建议重命名为: dist/MultiPlatformGUI") + return _verify_directory(fallback_dir) + else: + print("❌ 未找到任何打包输出目录") + return False - if os.path.exists(exe_path): - size = os.path.getsize(exe_path) - print(f"✅ 主程序: {exe_path} ({size:,} bytes)") +def _verify_directory(base_dir): + """验证指定目录的打包结果""" + exe_path = f"{base_dir}/main.exe" + internal_dir = f"{base_dir}/_internal" + dll_path = f"{base_dir}/_internal/Utils/PythonNew32/SaiNiuApi.dll" + static_dir = f"{base_dir}/_internal/static" - if os.path.exists(dll_path): - dll_size = os.path.getsize(dll_path) - print(f"✅ 千牛DLL: SaiNiuApi.dll ({dll_size:,} bytes)") - print(f"📁 有效路径: {base_dir}/") - print("✅ 验证通过") - return True - else: - print("❌ 千牛DLL不存在") - else: - print(f"❌ {exe_path} 不存在") + print(f"📁 验证目录: {base_dir}") + + # 检查主程序 + if os.path.exists(exe_path): + size = os.path.getsize(exe_path) / 1024 / 1024 # MB + print(f"✅ 主程序: main.exe ({size:.1f} MB)") + else: + print("❌ 主程序 main.exe 不存在") + return False - print("❌ 未找到有效的打包结果") - return False + # 检查_internal目录 + if os.path.exists(internal_dir): + file_count = len([f for f in os.listdir(internal_dir) if os.path.isfile(os.path.join(internal_dir, f))]) + dir_count = len([d for d in os.listdir(internal_dir) if os.path.isdir(os.path.join(internal_dir, d))]) + print(f"✅ 依赖目录: _internal/ ({file_count} 个文件, {dir_count} 个子目录)") + else: + print("❌ _internal 目录不存在") + return False + + # 检查关键DLL(可选) + if os.path.exists(dll_path): + dll_size = os.path.getsize(dll_path) / 1024 # KB + print(f"✅ 千牛DLL: SaiNiuApi.dll ({dll_size:.1f} KB)") + else: + print("⚠️ 千牛DLL不存在(可能正常)") + + # 检查静态资源 + if os.path.exists(static_dir): + icon_files = list(Path(static_dir).glob("*.png")) + print(f"✅ 静态资源: static/ ({len(icon_files)} 个图标文件)") + else: + print("⚠️ static 目录不存在") + + print(f"✅ 目录 {base_dir} 验证通过") + return True def main(): @@ -296,10 +342,11 @@ def main(): if os.path.exists("dist/MultiPlatformGUI"): print("📁 打包结果: dist/MultiPlatformGUI/") print("🚀 运行方式: cd dist/MultiPlatformGUI && .\\main.exe") + print("📦 安装包构建: python installer/build_installer.py") elif os.path.exists("dist/main"): - print("�� 打包结果: dist/main/") + print("📁 打包结果: dist/main/") print("🚀 运行方式: cd dist/main && .\\main.exe") - print("💡 提示: 可手动重命名为 MultiPlatformGUI") + print("⚠️ 建议重命名: dist/main -> dist/MultiPlatformGUI") print("=" * 60)