Todo: 修改因exe_token错误导致的不断重连问题

Todo: 修改打包中.bat打包文件与测试打包脚本不一致问题
New: 新增installer安装包环境搭建数据
This commit is contained in:
2025-09-28 17:00:02 +08:00
parent 7f9894908d
commit c58cec750f
7 changed files with 640 additions and 55 deletions

View File

@@ -29,6 +29,7 @@ class BackendClient:
self.error_callback: Optional[Callable] = None self.error_callback: Optional[Callable] = None
self.login_callback: Optional[Callable] = None # 新增平台登录下发cookies回调 self.login_callback: Optional[Callable] = None # 新增平台登录下发cookies回调
self.success_callback: Optional[Callable] = None # 新增:后端连接成功回调 self.success_callback: Optional[Callable] = None # 新增:后端连接成功回调
self.token_error_callback: Optional[Callable] = None # 新增token错误回调
self.is_connected = False self.is_connected = False
@@ -261,7 +262,8 @@ class BackendClient:
close: Callable = None, close: Callable = None,
error: Callable = None, error: Callable = None,
login: Callable = None, login: Callable = None,
success: Callable = None): success: Callable = None,
token_error: Callable = None):
"""设置各种消息类型的回调函数""" """设置各种消息类型的回调函数"""
if store_list: if store_list:
self.store_list_callback = store_list self.store_list_callback = store_list
@@ -279,6 +281,8 @@ class BackendClient:
self.login_callback = login self.login_callback = login
if success: if success:
self.success_callback = success self.success_callback = success
if token_error:
self.token_error_callback = token_error
def on_connected(self): def on_connected(self):
"""连接成功时的处理""" """连接成功时的处理"""
@@ -338,7 +342,7 @@ class BackendClient:
active_store_id = None active_store_id = None
try: try:
from Utils.JD.JdUtils import WebsocketManager as JdManager 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 from Utils.Pdd.PddUtils import WebsocketManager as PddManager
# 检查各平台是否有活跃连接 # 检查各平台是否有活跃连接
@@ -429,6 +433,8 @@ class BackendClient:
self._handle_login(message) self._handle_login(message)
elif msg_type == 'error': elif msg_type == 'error':
self._handle_error_message(message) self._handle_error_message(message)
elif msg_type == 'error_token':
self._handle_token_error(message)
elif msg_type == 'staff_list': elif msg_type == 'staff_list':
self._handle_staff_list(message) self._handle_staff_list(message)
else: else:
@@ -1163,10 +1169,35 @@ class BackendClient:
def _handle_error_message(self, message: Dict[str, Any]): def _handle_error_message(self, message: Dict[str, Any]):
"""处理错误消息""" """处理错误消息"""
error_msg = message.get('error', '未知错误') 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}") print(f"后端连接错误: {error_msg}")
if self.error_callback: if self.error_callback:
self.error_callback(error_msg, message) 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]): def _handle_staff_list(self, message: Dict[str, Any]):
"""处理客服列表更新消息""" """处理客服列表更新消息"""
staff_list = message.get('data', {}).get('staff_list', []) staff_list = message.get('data', {}).get('staff_list', [])

View File

@@ -35,10 +35,11 @@ class WebSocketManager:
'success': None, 'success': None,
'error': None, 'error': None,
'platform_connected': None, 'platform_connected': None,
'token_error': None,
} }
def set_callbacks(self, log: Callable = None, success: Callable = None, error: Callable = 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: if log:
self.callbacks['log'] = log self.callbacks['log'] = log
@@ -48,6 +49,8 @@ class WebSocketManager:
self.callbacks['error'] = error self.callbacks['error'] = error
if platform_connected: # ← 新增 if platform_connected: # ← 新增
self.callbacks['platform_connected'] = 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"): def _log(self, message: str, level: str = "INFO"):
"""内部日志方法""" """内部日志方法"""
@@ -84,33 +87,49 @@ class WebSocketManager:
backend = get_backend_client() backend = get_backend_client()
if backend: if backend:
# 3 如果有客户端更新token并重连 # 检查现有客户端是否因token错误而停止
backend.set_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_login(platform_name: str, store_id: str, cookies: str):
def _on_backend_success(): self._log(
try: f"收到后端登录指令: 平台={platform_name}, 店铺={store_id}, cookies_len={len(cookies) if cookies else 0}",
self._log("连接服务成功", "SUCCESS") "INFO")
if self.callbacks['success']: self._handle_platform_login(platform_name, store_id, cookies)
self.callbacks['success']()
except Exception as e:
self._log(f"成功回调执行失败: {e}", "ERROR")
def _on_backend_login(platform_name: str, store_id: str, cookies: str): def _on_token_error(error_content: str):
self._log( self._log(f"Token验证失败: {error_content}", "ERROR")
f"收到后端登录指令: 平台={platform_name}, 店铺={store_id}, cookies_len={len(cookies) if cookies else 0}", if self.callbacks['token_error']:
"INFO") self.callbacks['token_error'](error_content)
self._handle_platform_login(platform_name, store_id, cookies)
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() if not backend:
self.backend_client = backend
self._log("令牌已提交已连接后端。等待后端下发平台cookies后自动连接平台...", "SUCCESS")
return True
else:
backend = BackendClient.from_exe_token(token) backend = BackendClient.from_exe_token(token)
@@ -128,7 +147,12 @@ class WebSocketManager:
except Exception as e: except Exception as e:
self._log(f"成功回调执行失败: {e}", "ERROR") 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() backend.connect()
set_backend_client(backend) set_backend_client(backend)
@@ -402,6 +426,11 @@ class WebSocketManager:
self._log("🔄 开始执行 start_with_cookies", "DEBUG") self._log("🔄 开始执行 start_with_cookies", "DEBUG")
result = asyncio.run(listener.start_with_cookies(store_id=store_id, cookies=data)) result = asyncio.run(listener.start_with_cookies(store_id=store_id, cookies=data))
self._log(f"📊 start_with_cookies 执行结果: {result}", "DEBUG") 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"]: if self.backend_client and result not in ["need_verification_code", "verification_code_error", "login_failure"]:

View File

@@ -85,9 +85,32 @@ def main():
result = subprocess.run(['python', 'quick_build.py'], capture_output=False) result = subprocess.run(['python', 'quick_build.py'], capture_output=False)
if result.returncode == 0: if result.returncode == 0:
print("\n🎉 生产环境打包成功!") # 确保输出目录正确性
print("📁 输出目录: dist/MultiPlatformGUI/") if os.path.exists('dist/main') and not os.path.exists('dist/MultiPlatformGUI'):
print("🔇 已禁用所有日志功能,客户端不会产生日志文件") 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: else:
print("\n❌ 打包失败") print("\n❌ 打包失败")

106
installer/README.md Normal file
View File

@@ -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 版本信息
- 文件复制状态
- 编译过程输出
- 最终安装包信息
## 进一步优化
可以考虑添加以下功能:
- 数字签名支持
- 多语言支持
- 自定义安装选项
- 更新检查机制

View File

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

26
main.py
View File

@@ -402,6 +402,29 @@ class LoginWindow(QMainWindow):
except Exception as e: except Exception as e:
self.add_log(f"处理平台连接事件失败: {e}", "ERROR") 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): def delayed_platform_summary(self):
"""定时器触发的汇总显示更新""" """定时器触发的汇总显示更新"""
try: try:
@@ -615,7 +638,8 @@ class LoginWindow(QMainWindow):
log=self.add_log, log=self.add_log,
success=lambda: self.add_log("WebSocket连接管理器连接成功", "SUCCESS"), success=lambda: self.add_log("WebSocket连接管理器连接成功", "SUCCESS"),
error=lambda error: self.add_log(f"WebSocket连接管理器错误: {error}", "ERROR"), 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错误回调
) )
# 连接后端 # 连接后端

View File

@@ -7,6 +7,7 @@
import os import os
import subprocess import subprocess
import shutil import shutil
from pathlib import Path
def clean_build(): def clean_build():
@@ -96,17 +97,31 @@ def build_with_command():
print("✅ 打包成功!") print("✅ 打包成功!")
print("📁 打包结果: dist/main/") print("📁 打包结果: dist/main/")
# 重命名目录 # 重命名目录为统一的输出路径
if os.path.exists('dist/main'): if os.path.exists('dist/main'):
try: try:
# 如果目标目录已存在,先删除
if os.path.exists('dist/MultiPlatformGUI'): if os.path.exists('dist/MultiPlatformGUI'):
print("🗑️ 删除旧的 MultiPlatformGUI 目录...")
shutil.rmtree('dist/MultiPlatformGUI') shutil.rmtree('dist/MultiPlatformGUI')
# 重命名
os.rename('dist/main', 'dist/MultiPlatformGUI') os.rename('dist/main', 'dist/MultiPlatformGUI')
print("✅ 已重命名为: 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: except Exception as e:
print(f"⚠️ 重命名失败: {e}") print(f"⚠️ 重命名失败: {e}")
print("💡 可以手动重命名: dist/main -> dist/MultiPlatformGUI") print("💡 可以手动重命名: dist/main -> dist/MultiPlatformGUI")
print("📁 当前可用路径: dist/main/") print("📁 当前可用路径: dist/main/")
else:
print("⚠️ 未找到 dist/main 目录,重命名跳过")
# 创建使用说明 # 创建使用说明
try: try:
@@ -221,31 +236,62 @@ def verify_result():
"""验证打包结果""" """验证打包结果"""
print("\n🔍 验证打包结果...") print("\n🔍 验证打包结果...")
# 先检查MultiPlatformGUI再检查main目录 # 先检查MultiPlatformGUI(标准输出目录
base_dirs = ["dist/MultiPlatformGUI", "dist/main"] target_dir = "dist/MultiPlatformGUI"
fallback_dir = "dist/main"
for base_dir in base_dirs: if os.path.exists(target_dir):
if os.path.exists(base_dir): return _verify_directory(target_dir)
exe_path = f"{base_dir}/main.exe" elif os.path.exists(fallback_dir):
dll_path = f"{base_dir}/_internal/Utils/PythonNew32/SaiNiuApi.dll" print(f"⚠️ 发现备用目录: {fallback_dir}")
print("💡 建议重命名为: dist/MultiPlatformGUI")
return _verify_directory(fallback_dir)
else:
print("❌ 未找到任何打包输出目录")
return False
if os.path.exists(exe_path): def _verify_directory(base_dir):
size = os.path.getsize(exe_path) """验证指定目录的打包结果"""
print(f"✅ 主程序: {exe_path} ({size:,} bytes)") 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): print(f"📁 验证目录: {base_dir}")
dll_size = os.path.getsize(dll_path)
print(f"✅ 千牛DLL: SaiNiuApi.dll ({dll_size:,} bytes)") # 检查主程序
print(f"📁 有效路径: {base_dir}/") if os.path.exists(exe_path):
print("✅ 验证通过") size = os.path.getsize(exe_path) / 1024 / 1024 # MB
return True print(f"✅ 主程序: main.exe ({size:.1f} MB)")
else: else:
print("千牛DLL不存在") print("主程序 main.exe 不存在")
else: return False
print(f"{exe_path} 不存在")
print("❌ 未找到有效的打包结果") # 检查_internal目录
return False 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(): def main():
@@ -296,10 +342,11 @@ def main():
if os.path.exists("dist/MultiPlatformGUI"): if os.path.exists("dist/MultiPlatformGUI"):
print("📁 打包结果: dist/MultiPlatformGUI/") print("📁 打包结果: dist/MultiPlatformGUI/")
print("🚀 运行方式: cd dist/MultiPlatformGUI && .\\main.exe") print("🚀 运行方式: cd dist/MultiPlatformGUI && .\\main.exe")
print("📦 安装包构建: python installer/build_installer.py")
elif os.path.exists("dist/main"): elif os.path.exists("dist/main"):
print("<EFBFBD><EFBFBD> 打包结果: dist/main/") print("📁 打包结果: dist/main/")
print("🚀 运行方式: cd dist/main && .\\main.exe") print("🚀 运行方式: cd dist/main && .\\main.exe")
print("💡 提示: 可手动重命名为 MultiPlatformGUI") print("⚠️ 建议重命名: dist/main -> dist/MultiPlatformGUI")
print("=" * 60) print("=" * 60)