Todo: 修改因exe_token错误导致的不断重连问题
Todo: 修改打包中.bat打包文件与测试打包脚本不一致问题 New: 新增installer安装包环境搭建数据
This commit is contained in:
@@ -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', [])
|
||||
|
||||
@@ -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)
|
||||
|
||||
backend.set_callbacks(success=_on_backend_success, login=_on_backend_login)
|
||||
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)
|
||||
|
||||
if not backend.is_connected:
|
||||
backend.connect()
|
||||
backend.set_callbacks(success=_on_backend_success, login=_on_backend_login, token_error=_on_token_error)
|
||||
|
||||
self.backend_client = backend
|
||||
self._log("令牌已提交,已连接后端。等待后端下发平台cookies后自动连接平台...", "SUCCESS")
|
||||
return True
|
||||
else:
|
||||
if not backend.is_connected:
|
||||
backend.connect()
|
||||
|
||||
self.backend_client = backend
|
||||
self._log("令牌已提交,已连接后端。等待后端下发平台cookies后自动连接平台...", "SUCCESS")
|
||||
return True
|
||||
|
||||
# 如果没有现有客户端或客户端被重置,创建新的
|
||||
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)
|
||||
@@ -403,6 +427,11 @@ class WebSocketManager:
|
||||
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"]:
|
||||
# 如果是特殊状态,说明通知已经在PddLogin中发送了,不需要重复发送
|
||||
|
||||
@@ -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❌ 打包失败")
|
||||
|
||||
|
||||
106
installer/README.md
Normal file
106
installer/README.md
Normal 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 版本信息
|
||||
- 文件复制状态
|
||||
- 编译过程输出
|
||||
- 最终安装包信息
|
||||
|
||||
## 进一步优化
|
||||
|
||||
可以考虑添加以下功能:
|
||||
- 数字签名支持
|
||||
- 多语言支持
|
||||
- 自定义安装选项
|
||||
- 更新检查机制
|
||||
325
installer/build_installer.py
Normal file
325
installer/build_installer.py
Normal 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
26
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错误回调
|
||||
)
|
||||
|
||||
# 连接后端
|
||||
|
||||
@@ -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}")
|
||||
|
||||
print("❌ 未找到有效的打包结果")
|
||||
return False
|
||||
# 检查主程序
|
||||
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
|
||||
|
||||
# 检查_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("<EFBFBD><EFBFBD> 打包结果: dist/main/")
|
||||
print("📁 打包结果: dist/main/")
|
||||
print("🚀 运行方式: cd dist/main && .\\main.exe")
|
||||
print("💡 提示: 可手动重命名为 MultiPlatformGUI")
|
||||
print("⚠️ 建议重命名: dist/main -> dist/MultiPlatformGUI")
|
||||
|
||||
print("=" * 60)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user