Todo: 集成多平台 解决因SaiNiu线程抢占资源问题 本地提交测试环境打包 和 正式打包脚本与正式环境打包bat
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -29,13 +29,16 @@ class WebSocketManager:
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.backend_client = None
|
self.backend_client = None
|
||||||
self.platform_listeners = {} # 存储各平台的监听器
|
self.platform_listeners = {} # 存储各平台的监听器
|
||||||
|
self.connected_platforms = [] # 存储已连接的平台列表 # <- 新增
|
||||||
self.callbacks = {
|
self.callbacks = {
|
||||||
'log': None,
|
'log': None,
|
||||||
'success': None,
|
'success': None,
|
||||||
'error': None
|
'error': None,
|
||||||
|
'platform_connected': 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): # ← 新增参数
|
||||||
"""设置回调函数"""
|
"""设置回调函数"""
|
||||||
if log:
|
if log:
|
||||||
self.callbacks['log'] = log
|
self.callbacks['log'] = log
|
||||||
@@ -43,6 +46,8 @@ class WebSocketManager:
|
|||||||
self.callbacks['success'] = success
|
self.callbacks['success'] = success
|
||||||
if error:
|
if error:
|
||||||
self.callbacks['error'] = error
|
self.callbacks['error'] = error
|
||||||
|
if platform_connected: # ← 新增
|
||||||
|
self.callbacks['platform_connected'] = platform_connected
|
||||||
|
|
||||||
def _log(self, message: str, level: str = "INFO"):
|
def _log(self, message: str, level: str = "INFO"):
|
||||||
"""内部日志方法"""
|
"""内部日志方法"""
|
||||||
@@ -51,6 +56,18 @@ class WebSocketManager:
|
|||||||
else:
|
else:
|
||||||
print(f"[{level}] {message}")
|
print(f"[{level}] {message}")
|
||||||
|
|
||||||
|
def _notify_platform_connected(self, platform_name: str):
|
||||||
|
"""通知GUI平台连接成功"""
|
||||||
|
try:
|
||||||
|
if platform_name not in self.connected_platforms:
|
||||||
|
self.connected_platforms.append(platform_name)
|
||||||
|
|
||||||
|
if self.callbacks['platform_connected']:
|
||||||
|
self.callbacks['platform_connected'](platform_name, self.connected_platforms.copy())
|
||||||
|
self._log(f"已通知GUI平台连接: {platform_name}", "INFO")
|
||||||
|
except Exception as e:
|
||||||
|
self._log(f"通知平台连接失败: {e}", "ERROR")
|
||||||
|
|
||||||
def connect_backend(self, token: str) -> bool:
|
def connect_backend(self, token: str) -> bool:
|
||||||
"""连接后端WebSocket"""
|
"""连接后端WebSocket"""
|
||||||
try:
|
try:
|
||||||
@@ -202,6 +219,7 @@ class WebSocketManager:
|
|||||||
self._log(f"上报连接状态失败: {e}", "WARNING")
|
self._log(f"上报连接状态失败: {e}", "WARNING")
|
||||||
|
|
||||||
self._log("已启动京东平台监听", "SUCCESS")
|
self._log("已启动京东平台监听", "SUCCESS")
|
||||||
|
self._notify_platform_connected("京东") # ← 新增
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self._log(f"启动京东平台监听失败: {e}", "ERROR")
|
self._log(f"启动京东平台监听失败: {e}", "ERROR")
|
||||||
@@ -265,6 +283,7 @@ class WebSocketManager:
|
|||||||
self._log(f"上报抖音平台连接状态失败: {e}", "WARNING")
|
self._log(f"上报抖音平台连接状态失败: {e}", "WARNING")
|
||||||
|
|
||||||
self._log("已启动抖音平台监听", "SUCCESS")
|
self._log("已启动抖音平台监听", "SUCCESS")
|
||||||
|
self._notify_platform_connected("抖音") # ← 新增
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self._log(f"启动抖音平台监听失败: {e}", "ERROR")
|
self._log(f"启动抖音平台监听失败: {e}", "ERROR")
|
||||||
@@ -324,6 +343,7 @@ class WebSocketManager:
|
|||||||
self._log(f"上报连接状态失败: {e}", "WARNING")
|
self._log(f"上报连接状态失败: {e}", "WARNING")
|
||||||
|
|
||||||
self._log("已启动千牛平台监听(单连接多店铺架构)", "SUCCESS")
|
self._log("已启动千牛平台监听(单连接多店铺架构)", "SUCCESS")
|
||||||
|
self._notify_platform_connected("千牛") # ← 新增
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self._log(f"启动千牛平台监听失败: {e}", "ERROR")
|
self._log(f"启动千牛平台监听失败: {e}", "ERROR")
|
||||||
@@ -363,6 +383,7 @@ class WebSocketManager:
|
|||||||
self._log("⚠️ [PDD] 验证码错误,已发送错误通知给后端", "WARNING")
|
self._log("⚠️ [PDD] 验证码错误,已发送错误通知给后端", "WARNING")
|
||||||
elif result:
|
elif result:
|
||||||
self._log("✅ [PDD] 登录成功,平台连接已建立", "SUCCESS")
|
self._log("✅ [PDD] 登录成功,平台连接已建立", "SUCCESS")
|
||||||
|
self._notify_platform_connected("拼多多")
|
||||||
else:
|
else:
|
||||||
self._log("❌ [PDD] 登录失败", "ERROR")
|
self._log("❌ [PDD] 登录失败", "ERROR")
|
||||||
else:
|
else:
|
||||||
|
|||||||
21
build_production.bat
Normal file
21
build_production.bat
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
@echo off
|
||||||
|
chcp 65001 >nul
|
||||||
|
echo.
|
||||||
|
echo =====================================================
|
||||||
|
echo 🔇 生产环境打包工具(无日志版本)
|
||||||
|
echo =====================================================
|
||||||
|
echo.
|
||||||
|
|
||||||
|
echo 🔧 正在生成无日志版本...
|
||||||
|
python build_production.py
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo =====================================================
|
||||||
|
echo 🎉 生产环境打包完成!
|
||||||
|
echo.
|
||||||
|
echo 📁 输出目录: dist/MultiPlatformGUI/
|
||||||
|
echo 🔇 客户端运行时不会产生任何日志文件
|
||||||
|
echo 🚀 可直接交付给客户使用
|
||||||
|
echo.
|
||||||
|
echo =====================================================
|
||||||
|
pause >nul
|
||||||
106
build_production.py
Normal file
106
build_production.py
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
生产环境打包脚本 - 自动禁用日志功能
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
def disable_logging():
|
||||||
|
"""自动禁用所有日志功能"""
|
||||||
|
print("🔧 正在禁用日志功能...")
|
||||||
|
|
||||||
|
# 备份原文件
|
||||||
|
files_to_backup = ['main.py', 'exe_file_logger.py']
|
||||||
|
for file_name in files_to_backup:
|
||||||
|
if os.path.exists(file_name):
|
||||||
|
shutil.copy(file_name, f'{file_name}.backup')
|
||||||
|
print(f"✅ 已备份 {file_name}")
|
||||||
|
|
||||||
|
# 1. 修改 main.py - 注释掉日志初始化
|
||||||
|
if os.path.exists('main.py'):
|
||||||
|
with open('main.py', 'r', encoding='utf-8') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
# 注释掉日志相关的导入和初始化
|
||||||
|
content = content.replace(
|
||||||
|
'from exe_file_logger import setup_file_logging, log_to_file',
|
||||||
|
'# from exe_file_logger import setup_file_logging, log_to_file # 生产环境禁用'
|
||||||
|
).replace(
|
||||||
|
'setup_file_logging()',
|
||||||
|
'# setup_file_logging() # 生产环境禁用'
|
||||||
|
).replace(
|
||||||
|
'print("文件日志系统已在main.py中初始化")',
|
||||||
|
'# print("文件日志系统已在main.py中初始化") # 生产环境禁用'
|
||||||
|
)
|
||||||
|
|
||||||
|
with open('main.py', 'w', encoding='utf-8') as f:
|
||||||
|
f.write(content)
|
||||||
|
print("✅ 已禁用 main.py 中的日志初始化")
|
||||||
|
|
||||||
|
# 2. 修改 exe_file_logger.py - 让所有函数变成空操作
|
||||||
|
if os.path.exists('exe_file_logger.py'):
|
||||||
|
with open('exe_file_logger.py', 'r', encoding='utf-8') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
# 将 setup_file_logging 函数替换为空函数
|
||||||
|
content = content.replace(
|
||||||
|
'def setup_file_logging():',
|
||||||
|
'def setup_file_logging():\n """生产环境 - 禁用文件日志"""\n return None\n\ndef setup_file_logging_original():'
|
||||||
|
).replace(
|
||||||
|
'def log_to_file(message):',
|
||||||
|
'def log_to_file(message):\n """生产环境 - 禁用文件日志"""\n pass\n\ndef log_to_file_original(message):'
|
||||||
|
)
|
||||||
|
|
||||||
|
with open('exe_file_logger.py', 'w', encoding='utf-8') as f:
|
||||||
|
f.write(content)
|
||||||
|
print("✅ 已禁用 exe_file_logger.py 中的日志功能")
|
||||||
|
|
||||||
|
print("✅ 所有日志功能已禁用")
|
||||||
|
|
||||||
|
def restore_logging():
|
||||||
|
"""恢复日志功能"""
|
||||||
|
files_to_restore = ['main.py', 'exe_file_logger.py']
|
||||||
|
for file_name in files_to_restore:
|
||||||
|
backup_file = f'{file_name}.backup'
|
||||||
|
if os.path.exists(backup_file):
|
||||||
|
shutil.copy(backup_file, file_name)
|
||||||
|
os.remove(backup_file)
|
||||||
|
print(f"✅ 已恢复 {file_name}")
|
||||||
|
print("✅ 所有文件已恢复到开发环境配置")
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""主函数"""
|
||||||
|
print("🔥 生产环境打包工具(无日志版本)")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 1. 禁用日志功能
|
||||||
|
disable_logging()
|
||||||
|
|
||||||
|
# 2. 执行打包
|
||||||
|
print("\n🚀 开始打包...")
|
||||||
|
result = subprocess.run(['python', 'quick_build.py'], capture_output=False)
|
||||||
|
|
||||||
|
if result.returncode == 0:
|
||||||
|
print("\n🎉 生产环境打包成功!")
|
||||||
|
print("📁 输出目录: dist/MultiPlatformGUI/")
|
||||||
|
print("🔇 已禁用所有日志功能,客户端不会产生日志文件")
|
||||||
|
else:
|
||||||
|
print("\n❌ 打包失败")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ 打包过程出错: {e}")
|
||||||
|
|
||||||
|
finally:
|
||||||
|
# 3. 恢复日志功能(用于开发)
|
||||||
|
print("\n🔄 恢复开发环境配置...")
|
||||||
|
restore_logging()
|
||||||
|
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
input("按Enter键退出...")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
129
exe_file_logger.py
Normal file
129
exe_file_logger.py
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
EXE文件日志器 - 将所有输出重定向到文件
|
||||||
|
适用于 pyinstaller -w 打包的exe
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
from datetime import datetime
|
||||||
|
import threading
|
||||||
|
|
||||||
|
class FileLogger:
|
||||||
|
"""文件日志器类"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
# 确定日志文件路径
|
||||||
|
if getattr(sys, 'frozen', False):
|
||||||
|
# 打包环境
|
||||||
|
self.log_dir = os.path.dirname(sys.executable)
|
||||||
|
else:
|
||||||
|
# 开发环境
|
||||||
|
self.log_dir = os.getcwd()
|
||||||
|
|
||||||
|
# 创建日志文件
|
||||||
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||||
|
self.log_file = os.path.join(self.log_dir, f"qianniu_exe_{timestamp}.log")
|
||||||
|
|
||||||
|
# 线程锁,确保多线程写入安全
|
||||||
|
self.lock = threading.Lock()
|
||||||
|
|
||||||
|
# 初始化日志文件
|
||||||
|
self.write_log("=" * 80)
|
||||||
|
self.write_log("千牛EXE日志开始")
|
||||||
|
self.write_log(f"Python版本: {sys.version}")
|
||||||
|
self.write_log(f"是否打包环境: {getattr(sys, 'frozen', False)}")
|
||||||
|
self.write_log(f"日志文件: {self.log_file}")
|
||||||
|
self.write_log("=" * 80)
|
||||||
|
|
||||||
|
def write_log(self, message):
|
||||||
|
"""写入日志到文件"""
|
||||||
|
try:
|
||||||
|
with self.lock:
|
||||||
|
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
|
||||||
|
log_entry = f"[{timestamp}] {message}\n"
|
||||||
|
|
||||||
|
with open(self.log_file, 'a', encoding='utf-8') as f:
|
||||||
|
f.write(log_entry)
|
||||||
|
f.flush()
|
||||||
|
os.fsync(f.fileno()) # 强制写入磁盘
|
||||||
|
except Exception as e:
|
||||||
|
# 如果写入失败,至少尝试输出到stderr
|
||||||
|
try:
|
||||||
|
sys.stderr.write(f"LOG_ERROR: {message} | Error: {e}\n")
|
||||||
|
sys.stderr.flush()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def write(self, text):
|
||||||
|
"""实现write方法以支持作为sys.stdout使用"""
|
||||||
|
if text.strip(): # 只记录非空内容
|
||||||
|
self.write_log(text.strip())
|
||||||
|
|
||||||
|
def flush(self):
|
||||||
|
"""实现flush方法"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
class TeeOutput:
|
||||||
|
"""同时输出到原始输出和文件的类"""
|
||||||
|
|
||||||
|
def __init__(self, original, file_logger):
|
||||||
|
self.original = original
|
||||||
|
self.file_logger = file_logger
|
||||||
|
|
||||||
|
def write(self, text):
|
||||||
|
# 写入原始输出(如果存在)
|
||||||
|
if self.original:
|
||||||
|
try:
|
||||||
|
self.original.write(text)
|
||||||
|
self.original.flush()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 写入文件日志
|
||||||
|
if text.strip():
|
||||||
|
self.file_logger.write_log(text.strip())
|
||||||
|
|
||||||
|
def flush(self):
|
||||||
|
if self.original:
|
||||||
|
try:
|
||||||
|
self.original.flush()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 全局文件日志器实例
|
||||||
|
_file_logger = None
|
||||||
|
|
||||||
|
def setup_file_logging():
|
||||||
|
"""设置文件日志记录"""
|
||||||
|
global _file_logger
|
||||||
|
|
||||||
|
if _file_logger is None:
|
||||||
|
_file_logger = FileLogger()
|
||||||
|
|
||||||
|
# 保存原始的stdout和stderr
|
||||||
|
original_stdout = sys.stdout
|
||||||
|
original_stderr = sys.stderr
|
||||||
|
|
||||||
|
# 创建Tee输出,同时输出到原始输出和文件
|
||||||
|
sys.stdout = TeeOutput(original_stdout, _file_logger)
|
||||||
|
sys.stderr = TeeOutput(original_stderr, _file_logger)
|
||||||
|
|
||||||
|
_file_logger.write_log("文件日志系统已设置完成")
|
||||||
|
|
||||||
|
return _file_logger
|
||||||
|
|
||||||
|
def log_to_file(message):
|
||||||
|
"""直接写入文件日志的函数"""
|
||||||
|
global _file_logger
|
||||||
|
if _file_logger:
|
||||||
|
_file_logger.write_log(message)
|
||||||
|
else:
|
||||||
|
# 如果还没有初始化,先初始化
|
||||||
|
setup_file_logging()
|
||||||
|
_file_logger.write_log(message)
|
||||||
|
|
||||||
|
# 注释掉自动初始化,改为手动调用
|
||||||
|
# if getattr(sys, 'frozen', False):
|
||||||
|
# setup_file_logging()
|
||||||
735
main.py
735
main.py
@@ -3,10 +3,20 @@ from PyQt5.QtCore import Qt
|
|||||||
from PyQt5.QtGui import QFont, QPalette, QColor
|
from PyQt5.QtGui import QFont, QPalette, QColor
|
||||||
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
|
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
|
||||||
QHBoxLayout, QLabel, QPushButton, QLineEdit,
|
QHBoxLayout, QLabel, QPushButton, QLineEdit,
|
||||||
QTextEdit, QFrame, QDialog, QDialogButtonBox, QComboBox)
|
QTextEdit, QFrame, QDialog, QDialogButtonBox, QComboBox,
|
||||||
|
QSystemTrayIcon, QMenu, QAction, QMessageBox, QGraphicsDropShadowEffect)
|
||||||
|
from PyQt5.QtCore import QPropertyAnimation, QEasingCurve, QRect, QTimer, pyqtProperty
|
||||||
|
from PyQt5.QtGui import QColor
|
||||||
import config
|
import config
|
||||||
from WebSocket.backend_singleton import get_websocket_manager
|
from WebSocket.backend_singleton import get_websocket_manager
|
||||||
|
from windows_taskbar_fix import setup_windows_taskbar_icon
|
||||||
|
import os
|
||||||
|
# ===================== 文件日志系统 - 同时支持开发和exe环境 =====================
|
||||||
|
# 重定向所有输出到文件,确保有日志记录
|
||||||
|
from exe_file_logger import setup_file_logging, log_to_file
|
||||||
|
|
||||||
|
setup_file_logging()
|
||||||
|
print("文件日志系统已在main.py中初始化")
|
||||||
|
|
||||||
|
|
||||||
# 新增: 用户名密码输入对话框类
|
# 新增: 用户名密码输入对话框类
|
||||||
@@ -21,22 +31,27 @@ class LoginWindow(QMainWindow):
|
|||||||
self.pdd_worker = None
|
self.pdd_worker = None
|
||||||
|
|
||||||
# 重复执行防护
|
# 重复执行防护
|
||||||
self.platform_combo_connected = False
|
|
||||||
self.last_login_time = 0
|
self.last_login_time = 0
|
||||||
self.login_cooldown = 2 # 登录冷却时间(秒)
|
self.login_cooldown = 2 # 登录冷却时间(秒)
|
||||||
|
|
||||||
|
# 平台连接状态管理
|
||||||
|
self.connected_platforms = []
|
||||||
|
self.status_timer = None
|
||||||
|
self.last_platform_connect_time = 0 # 记录最后一次平台连接时间
|
||||||
|
|
||||||
# 日志管理相关变量已删除
|
# 日志管理相关变量已删除
|
||||||
|
|
||||||
self.initUI()
|
self.initUI()
|
||||||
|
|
||||||
# 设置当前平台为ComboBox的当前值
|
|
||||||
self.current_platform = self.platform_combo.currentText()
|
|
||||||
|
|
||||||
def initUI(self):
|
def initUI(self):
|
||||||
# 设置窗口基本属性
|
# 设置窗口基本属性
|
||||||
self.setWindowTitle('AI回复连接入口-V1.0')
|
self.setWindowTitle('AI客服智能助手')
|
||||||
self.setGeometry(300, 300, 800, 600) # 增大窗口尺寸
|
self.setGeometry(300, 300, 450, 300) # 进一步减小窗口尺寸
|
||||||
self.setMinimumSize(700, 500) # 设置最小尺寸保证显示完整
|
self.setMinimumSize(420, 280) # 设置更小的最小尺寸
|
||||||
|
self.setMaximumSize(500, 350) # 设置最大尺寸,保持紧凑
|
||||||
|
|
||||||
|
# 窗口图标将由统一的任务栏修复模块设置,这里只做备用检查
|
||||||
|
print("[INFO] 窗口图标将由统一模块设置")
|
||||||
|
|
||||||
# 创建中央widget
|
# 创建中央widget
|
||||||
central_widget = QWidget()
|
central_widget = QWidget()
|
||||||
@@ -44,60 +59,44 @@ class LoginWindow(QMainWindow):
|
|||||||
|
|
||||||
# 创建主布局
|
# 创建主布局
|
||||||
main_layout = QVBoxLayout()
|
main_layout = QVBoxLayout()
|
||||||
main_layout.setSpacing(25) # 增加间距
|
main_layout.setSpacing(15) # 减小间距
|
||||||
main_layout.setContentsMargins(40, 40, 40, 40) # 增加边距
|
main_layout.setContentsMargins(30, 20, 30, 20) # 减小边距
|
||||||
central_widget.setLayout(main_layout)
|
central_widget.setLayout(main_layout)
|
||||||
|
|
||||||
# 添加标题
|
# 添加标题和副标题
|
||||||
title_label = QLabel('AI回复连接入口')
|
title_label = QLabel('AI客服智能助手')
|
||||||
|
title_label.setObjectName("title")
|
||||||
title_label.setAlignment(Qt.AlignCenter)
|
title_label.setAlignment(Qt.AlignCenter)
|
||||||
title_label.setFont(QFont('Microsoft YaHei', 24, QFont.Bold)) # 使用系统自带字体
|
title_label.setFont(QFont('Microsoft YaHei', 18, QFont.Bold)) # 减小字体
|
||||||
title_label.setStyleSheet("color: #2c3e50;")
|
title_label.setStyleSheet("color: #2c3e50; margin-bottom: 3px;")
|
||||||
main_layout.addWidget(title_label)
|
main_layout.addWidget(title_label)
|
||||||
|
|
||||||
# 添加分隔线
|
# 添加副标题说明
|
||||||
line = QFrame()
|
subtitle_label = QLabel('智能连接多平台客服,提供AI自动回复服务')
|
||||||
line.setFrameShape(QFrame.HLine)
|
subtitle_label.setObjectName("subtitle")
|
||||||
line.setFrameShadow(QFrame.Sunken)
|
subtitle_label.setAlignment(Qt.AlignCenter)
|
||||||
line.setStyleSheet("color: #bdc3c7;")
|
subtitle_label.setFont(QFont('Microsoft YaHei', 9)) # 减小字体
|
||||||
main_layout.addWidget(line)
|
subtitle_label.setStyleSheet("color: #7f8c8d; margin-bottom: 10px;")
|
||||||
|
main_layout.addWidget(subtitle_label)
|
||||||
|
|
||||||
# 在token_input之前添加平台选择区域
|
# 添加少量垂直空间
|
||||||
platform_layout = QHBoxLayout()
|
main_layout.addStretch(1)
|
||||||
platform_label = QLabel("选择平台:")
|
|
||||||
self.platform_combo = QComboBox()
|
|
||||||
self.platform_combo.addItems(["JD", "抖音", "千牛(淘宝)", "拼多多"])
|
|
||||||
|
|
||||||
# 防止重复连接信号 - 更强的保护
|
|
||||||
if not hasattr(self, 'platform_combo_connected') or not self.platform_combo_connected:
|
|
||||||
try:
|
|
||||||
self.platform_combo.currentIndexChanged.disconnect()
|
|
||||||
except TypeError:
|
|
||||||
pass # 如果没有连接则忽略
|
|
||||||
|
|
||||||
self.platform_combo.currentIndexChanged.connect(self.on_platform_changed)
|
|
||||||
self.platform_combo_connected = True
|
|
||||||
platform_layout.addWidget(platform_label)
|
|
||||||
platform_layout.addWidget(self.platform_combo)
|
|
||||||
|
|
||||||
# 将platform_layout添加到主布局中,在token_layout之前
|
|
||||||
main_layout.addLayout(platform_layout)
|
|
||||||
|
|
||||||
# 创建令牌输入区域
|
# 创建令牌输入区域
|
||||||
token_layout = QHBoxLayout()
|
token_layout = QVBoxLayout()
|
||||||
token_layout.setSpacing(15)
|
token_layout.setSpacing(8) # 减小间距
|
||||||
|
|
||||||
token_label = QLabel('令牌:')
|
token_label = QLabel('访问令牌')
|
||||||
token_label.setFont(QFont('Microsoft YaHei', 12))
|
token_label.setFont(QFont('Microsoft YaHei', 11, QFont.Bold)) # 减小字体
|
||||||
token_label.setFixedWidth(80)
|
token_label.setStyleSheet("color: #34495e;")
|
||||||
|
|
||||||
self.token_input = QLineEdit()
|
self.token_input = QLineEdit()
|
||||||
self.token_input.setPlaceholderText('请输入您的访问令牌')
|
self.token_input.setPlaceholderText('请输入您的访问令牌以连接服务')
|
||||||
self.token_input.setEchoMode(QLineEdit.Password)
|
self.token_input.setEchoMode(QLineEdit.Password)
|
||||||
self.token_input.setFont(QFont('Microsoft YaHei', 11))
|
self.token_input.setFont(QFont('Microsoft YaHei', 10)) # 减小字体
|
||||||
# noinspection PyUnresolvedReferences
|
# noinspection PyUnresolvedReferences
|
||||||
self.token_input.returnPressed.connect(self.login) # 表示回车提交
|
self.token_input.returnPressed.connect(self.login) # 表示回车提交
|
||||||
self.token_input.setMinimumHeight(40) # 增加输入框高度
|
self.token_input.setMinimumHeight(38) # 减小输入框高度
|
||||||
# 预填已保存的令牌(如果存在)
|
# 预填已保存的令牌(如果存在)
|
||||||
try:
|
try:
|
||||||
from config import get_saved_token
|
from config import get_saved_token
|
||||||
@@ -112,18 +111,22 @@ class LoginWindow(QMainWindow):
|
|||||||
main_layout.addLayout(token_layout)
|
main_layout.addLayout(token_layout)
|
||||||
|
|
||||||
# 创建连接按钮
|
# 创建连接按钮
|
||||||
self.login_btn = QPushButton('是否连接')
|
self.login_btn = QPushButton('连接服务')
|
||||||
self.login_btn.setFont(QFont('Microsoft YaHei', 12, QFont.Bold))
|
self.login_btn.setFont(QFont('Microsoft YaHei', 11, QFont.Bold)) # 减小字体
|
||||||
self.login_btn.setMinimumHeight(45) # 增大按钮高度
|
self.login_btn.setMinimumHeight(40) # 减小按钮高度
|
||||||
self.login_btn.clicked.connect(self.login) # 表示点击提交
|
self.login_btn.clicked.connect(self.login) # 表示点击提交
|
||||||
main_layout.addWidget(self.login_btn)
|
main_layout.addWidget(self.login_btn)
|
||||||
|
|
||||||
# 添加分隔线
|
# 添加连接状态提示
|
||||||
line = QFrame()
|
self.status_label = QLabel('等待连接...')
|
||||||
line.setFrameShape(QFrame.HLine)
|
self.status_label.setObjectName("status")
|
||||||
line.setFrameShadow(QFrame.Sunken)
|
self.status_label.setAlignment(Qt.AlignCenter)
|
||||||
line.setStyleSheet("color: #bdc3c7;")
|
self.status_label.setFont(QFont('Microsoft YaHei', 9)) # 减小字体
|
||||||
main_layout.addWidget(line)
|
self.status_label.setStyleSheet("color: #95a5a6; margin-top: 5px;")
|
||||||
|
main_layout.addWidget(self.status_label)
|
||||||
|
|
||||||
|
# 添加少量底部空间
|
||||||
|
main_layout.addStretch(1)
|
||||||
|
|
||||||
# 日志框已永久删除,只使用终端输出
|
# 日志框已永久删除,只使用终端输出
|
||||||
self.log_display = None
|
self.log_display = None
|
||||||
@@ -131,102 +134,142 @@ class LoginWindow(QMainWindow):
|
|||||||
# 应用现代化样式
|
# 应用现代化样式
|
||||||
self.apply_modern_styles()
|
self.apply_modern_styles()
|
||||||
|
|
||||||
|
# 添加视觉效果
|
||||||
|
self.add_visual_effects()
|
||||||
|
|
||||||
|
# 初始化系统托盘
|
||||||
|
self.init_system_tray()
|
||||||
|
|
||||||
# 系统初始化日志输出到终端
|
# 系统初始化日志输出到终端
|
||||||
print("[INFO] 系统初始化完成")
|
print("[INFO] 系统初始化完成")
|
||||||
|
|
||||||
# # 在平台选择改变时添加调试日志
|
|
||||||
# self.platform_combo.currentIndexChanged.connect(self.on_platform_changed)
|
|
||||||
|
|
||||||
# 设置默认平台
|
|
||||||
self.platform_combo.setCurrentText("JD")
|
|
||||||
print(f"🔥 设置默认平台为: {self.platform_combo.currentText()}")
|
|
||||||
|
|
||||||
def apply_modern_styles(self):
|
def apply_modern_styles(self):
|
||||||
"""应用现代化样式,兼容各Windows版本"""
|
"""应用现代化简约样式 - 精致美化版"""
|
||||||
self.setStyleSheet("""
|
self.setStyleSheet("""
|
||||||
QMainWindow {
|
QMainWindow {
|
||||||
background-color: #f9f9f9;
|
background: qlineargradient(
|
||||||
|
x1:0, y1:0, x2:0, y2:1,
|
||||||
|
stop:0 #f8fafb,
|
||||||
|
stop:0.5 #f1f4f6,
|
||||||
|
stop:1 #e8ecef
|
||||||
|
);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
QLabel {
|
QLabel {
|
||||||
color: #34495e;
|
color: #2c3e50;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
QLabel[objectName="title"] {
|
||||||
|
color: qlineargradient(
|
||||||
|
x1:0, y1:0, x2:1, y2:0,
|
||||||
|
stop:0 #2c3e50,
|
||||||
|
stop:1 #34495e
|
||||||
|
);
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
QLabel[objectName="subtitle"] {
|
||||||
|
color: #7f8c8d;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
QLabel[objectName="status"] {
|
||||||
|
padding: 5px 10px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: rgba(149, 165, 166, 0.1);
|
||||||
|
font-size: 9px;
|
||||||
}
|
}
|
||||||
|
|
||||||
QLineEdit {
|
QLineEdit {
|
||||||
padding: 10px;
|
padding: 12px 16px;
|
||||||
border: 2px solid #dfe6e9;
|
border: 2px solid transparent;
|
||||||
border-radius: 6px;
|
border-radius: 12px;
|
||||||
font-size: 14px;
|
font-size: 13px;
|
||||||
background-color: white;
|
background: qlineargradient(
|
||||||
selection-background-color: #3498db;
|
x1:0, y1:0, x2:0, y2:1,
|
||||||
|
stop:0 #ffffff,
|
||||||
|
stop:1 #f8fafb
|
||||||
|
);
|
||||||
|
selection-background-color: #007bff;
|
||||||
selection-color: white;
|
selection-color: white;
|
||||||
|
color: #2c3e50;
|
||||||
}
|
}
|
||||||
|
|
||||||
QLineEdit:focus {
|
QLineEdit:focus {
|
||||||
border-color: #3498db;
|
border: 2px solid #007bff;
|
||||||
|
background: white;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
QLineEdit:hover {
|
||||||
|
border: 2px solid #6c757d;
|
||||||
|
background: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
QPushButton {
|
QPushButton {
|
||||||
background-color: #3498db;
|
background: qlineargradient(
|
||||||
|
x1:0, y1:0, x2:0, y2:1,
|
||||||
|
stop:0 #4285f4,
|
||||||
|
stop:0.5 #1976d2,
|
||||||
|
stop:1 #1565c0
|
||||||
|
);
|
||||||
border: none;
|
border: none;
|
||||||
color: white;
|
color: white;
|
||||||
padding: 12px 24px;
|
padding: 12px 24px;
|
||||||
border-radius: 6px;
|
border-radius: 12px;
|
||||||
font-size: 14px;
|
font-size: 13px;
|
||||||
min-width: 120px;
|
font-weight: 600;
|
||||||
|
min-width: 180px;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
QPushButton:hover {
|
QPushButton:hover {
|
||||||
background-color: #2980b9;
|
background: qlineargradient(
|
||||||
|
x1:0, y1:0, x2:0, y2:1,
|
||||||
|
stop:0 #5294f5,
|
||||||
|
stop:0.5 #2986d3,
|
||||||
|
stop:1 #1e74c1
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
QPushButton:pressed {
|
QPushButton:pressed {
|
||||||
background-color: #1a6a9c;
|
background: qlineargradient(
|
||||||
|
x1:0, y1:0, x2:0, y2:1,
|
||||||
|
stop:0 #3274d4,
|
||||||
|
stop:0.5 #1666c2,
|
||||||
|
stop:1 #1455b0
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
QPushButton:disabled {
|
QPushButton:disabled {
|
||||||
background-color: #bdc3c7;
|
background: qlineargradient(
|
||||||
color: #7f8c8d;
|
x1:0, y1:0, x2:0, y2:1,
|
||||||
|
stop:0 #f8f9fa,
|
||||||
|
stop:1 #e9ecef
|
||||||
|
);
|
||||||
|
color: #6c757d;
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
}
|
}
|
||||||
|
|
||||||
QTextEdit {
|
/* 连接成功状态的按钮 */
|
||||||
border: 2px solid #dfe6e9;
|
QPushButton[objectName="connected"] {
|
||||||
border-radius: 6px;
|
background: qlineargradient(
|
||||||
background-color: white;
|
x1:0, y1:0, x2:0, y2:1,
|
||||||
padding: 8px;
|
stop:0 #28a745,
|
||||||
font-family: 'Consolas', 'Microsoft YaHei', monospace;
|
stop:0.5 #20963b,
|
||||||
|
stop:1 #1e7e34
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
QFrame[frameShape="4"] {
|
QPushButton[objectName="connected"]:hover {
|
||||||
color: #dfe6e9;
|
background: qlineargradient(
|
||||||
}
|
x1:0, y1:0, x2:0, y2:1,
|
||||||
|
stop:0 #34ce57,
|
||||||
QGroupBox {
|
stop:0.5 #28a745,
|
||||||
font-weight: bold;
|
stop:1 #20963b
|
||||||
border: 2px solid #dfe6e9;
|
);
|
||||||
border-radius: 6px;
|
|
||||||
margin-top: 10px;
|
|
||||||
padding-top: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
QComboBox {
|
|
||||||
padding: 8px;
|
|
||||||
border: 2px solid #dfe6e9;
|
|
||||||
border-radius: 6px;
|
|
||||||
font-size: 14px;
|
|
||||||
background-color: white;
|
|
||||||
min-width: 100px;
|
|
||||||
}
|
|
||||||
|
|
||||||
QComboBox::drop-down {
|
|
||||||
border: none;
|
|
||||||
width: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
QComboBox QAbstractItemView {
|
|
||||||
border: 2px solid #dfe6e9;
|
|
||||||
selection-background-color: #3498db;
|
|
||||||
selection-color: white;
|
|
||||||
}
|
}
|
||||||
""")
|
""")
|
||||||
|
|
||||||
@@ -236,98 +279,250 @@ class LoginWindow(QMainWindow):
|
|||||||
|
|
||||||
# 设置调色板确保颜色一致性
|
# 设置调色板确保颜色一致性
|
||||||
palette = QPalette()
|
palette = QPalette()
|
||||||
palette.setColor(QPalette.Window, QColor(249, 249, 249))
|
palette.setColor(QPalette.Window, QColor(248, 250, 251))
|
||||||
palette.setColor(QPalette.WindowText, QColor(52, 73, 94))
|
palette.setColor(QPalette.WindowText, QColor(44, 62, 80))
|
||||||
palette.setColor(QPalette.Base, QColor(255, 255, 255))
|
palette.setColor(QPalette.Base, QColor(255, 255, 255))
|
||||||
palette.setColor(QPalette.AlternateBase, QColor(233, 235, 239))
|
palette.setColor(QPalette.AlternateBase, QColor(241, 244, 246))
|
||||||
palette.setColor(QPalette.ToolTipBase, QColor(255, 255, 255))
|
palette.setColor(QPalette.ToolTipBase, QColor(255, 255, 255))
|
||||||
palette.setColor(QPalette.ToolTipText, QColor(52, 73, 94))
|
palette.setColor(QPalette.ToolTipText, QColor(44, 62, 80))
|
||||||
palette.setColor(QPalette.Text, QColor(52, 73, 94))
|
palette.setColor(QPalette.Text, QColor(44, 62, 80))
|
||||||
palette.setColor(QPalette.Button, QColor(52, 152, 219))
|
palette.setColor(QPalette.Button, QColor(66, 133, 244))
|
||||||
palette.setColor(QPalette.ButtonText, QColor(255, 255, 255))
|
palette.setColor(QPalette.ButtonText, QColor(255, 255, 255))
|
||||||
palette.setColor(QPalette.BrightText, QColor(255, 255, 255))
|
palette.setColor(QPalette.BrightText, QColor(255, 255, 255))
|
||||||
palette.setColor(QPalette.Highlight, QColor(52, 152, 219))
|
palette.setColor(QPalette.Highlight, QColor(66, 133, 244))
|
||||||
palette.setColor(QPalette.HighlightedText, QColor(255, 255, 255))
|
palette.setColor(QPalette.HighlightedText, QColor(255, 255, 255))
|
||||||
QApplication.setPalette(palette)
|
QApplication.setPalette(palette)
|
||||||
|
|
||||||
def on_platform_changed(self, index):
|
def add_visual_effects(self):
|
||||||
"""平台选择改变时的处理 - 新增方法"""
|
"""添加视觉效果"""
|
||||||
platform = self.platform_combo.currentText()
|
|
||||||
self.current_platform = platform
|
|
||||||
self.add_log(f"已选择平台: {platform}", "INFO")
|
|
||||||
print(f"🔥 平台已更改为: {platform}")
|
|
||||||
|
|
||||||
def add_log(self, message, log_type="INFO"):
|
|
||||||
"""添加日志信息 - 只输出到终端"""
|
|
||||||
from datetime import datetime
|
|
||||||
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
||||||
|
|
||||||
# 根据日志类型设置颜色(ANSI颜色码)
|
|
||||||
colors = {
|
|
||||||
"ERROR": "\033[91m", # 红色
|
|
||||||
"SUCCESS": "\033[92m", # 绿色
|
|
||||||
"WARNING": "\033[93m", # 黄色
|
|
||||||
"INFO": "\033[94m", # 蓝色
|
|
||||||
"DEBUG": "\033[95m" # 紫色
|
|
||||||
}
|
|
||||||
reset = "\033[0m" # 重置颜色
|
|
||||||
|
|
||||||
color = colors.get(log_type, colors["INFO"])
|
|
||||||
log_entry = f"{color}[{timestamp}] [{log_type}] {message}{reset}"
|
|
||||||
print(log_entry)
|
|
||||||
|
|
||||||
|
|
||||||
def login(self):
|
|
||||||
"""处理连接逻辑"""
|
|
||||||
# 防止重复快速点击
|
|
||||||
import time
|
|
||||||
current_time = time.time()
|
|
||||||
if current_time - self.last_login_time < self.login_cooldown:
|
|
||||||
self.add_log(f"请等待 {self.login_cooldown} 秒后再试", "WARNING")
|
|
||||||
return
|
|
||||||
|
|
||||||
self.last_login_time = current_time
|
|
||||||
|
|
||||||
token = self.token_input.text().strip()
|
|
||||||
if not token:
|
|
||||||
self.add_log("请输入有效的连接令牌", "ERROR")
|
|
||||||
return
|
|
||||||
|
|
||||||
# 禁用连接按钮,防止重复点击
|
|
||||||
self.login_btn.setEnabled(False)
|
|
||||||
self.login_btn.setText("连接中...")
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 使用 WebSocket 管理器处理连接
|
# 为主窗口添加阴影效果
|
||||||
ws_manager = get_websocket_manager()
|
shadow = QGraphicsDropShadowEffect()
|
||||||
|
shadow.setBlurRadius(20)
|
||||||
|
shadow.setXOffset(0)
|
||||||
|
shadow.setYOffset(5)
|
||||||
|
shadow.setColor(QColor(0, 0, 0, 40))
|
||||||
|
|
||||||
# 设置回调函数
|
# 为输入框添加阴影
|
||||||
ws_manager.set_callbacks(
|
input_shadow = QGraphicsDropShadowEffect()
|
||||||
log=self.add_log,
|
input_shadow.setBlurRadius(8)
|
||||||
success=lambda: self.add_log("WebSocket连接管理器连接成功", "SUCCESS"),
|
input_shadow.setXOffset(0)
|
||||||
error=lambda error: self.add_log(f"WebSocket连接管理器错误: {error}", "ERROR")
|
input_shadow.setYOffset(2)
|
||||||
)
|
input_shadow.setColor(QColor(0, 0, 0, 20))
|
||||||
|
self.token_input.setGraphicsEffect(input_shadow)
|
||||||
|
|
||||||
# 连接后端
|
# 为按钮添加阴影
|
||||||
success = ws_manager.connect_backend(token)
|
button_shadow = QGraphicsDropShadowEffect()
|
||||||
|
button_shadow.setBlurRadius(12)
|
||||||
|
button_shadow.setXOffset(0)
|
||||||
|
button_shadow.setYOffset(4)
|
||||||
|
button_shadow.setColor(QColor(66, 133, 244, 80))
|
||||||
|
self.login_btn.setGraphicsEffect(button_shadow)
|
||||||
|
|
||||||
if success:
|
# 设置对象名称以应用特定样式
|
||||||
self.add_log("已启动WebSocket连接管理器", "SUCCESS")
|
self.token_input.setObjectName("tokenInput")
|
||||||
else:
|
self.login_btn.setObjectName("loginButton")
|
||||||
self.add_log("WebSocket连接管理器启动失败", "ERROR")
|
|
||||||
|
print("[INFO] 视觉效果已添加")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.add_log(f"连接失败: {e}", "ERROR")
|
print(f"[WARNING] 添加视觉效果失败: {e}")
|
||||||
finally:
|
|
||||||
self.login_btn.setEnabled(True)
|
|
||||||
self.login_btn.setText("开始连接")
|
|
||||||
|
|
||||||
def verify_token(self, token):
|
def animate_success(self):
|
||||||
"""简化的令牌校验:非空即通过(实际校验由后端承担)"""
|
"""连接成功的动画效果"""
|
||||||
return bool(token)
|
try:
|
||||||
|
# 按钮轻微放大动画
|
||||||
|
self.button_animation = QPropertyAnimation(self.login_btn, b"geometry")
|
||||||
|
original_rect = self.login_btn.geometry()
|
||||||
|
|
||||||
def closeEvent(self, event):
|
# 计算放大后的位置(居中放大)
|
||||||
"""窗口关闭事件处理"""
|
expanded_rect = QRect(
|
||||||
|
original_rect.x() - 5,
|
||||||
|
original_rect.y() - 2,
|
||||||
|
original_rect.width() + 10,
|
||||||
|
original_rect.height() + 4
|
||||||
|
)
|
||||||
|
|
||||||
|
self.button_animation.setDuration(200)
|
||||||
|
self.button_animation.setStartValue(original_rect)
|
||||||
|
self.button_animation.setEndValue(expanded_rect)
|
||||||
|
self.button_animation.setEasingCurve(QEasingCurve.OutBack)
|
||||||
|
|
||||||
|
# 动画完成后恢复原始大小
|
||||||
|
def restore_size():
|
||||||
|
restore_animation = QPropertyAnimation(self.login_btn, b"geometry")
|
||||||
|
restore_animation.setDuration(150)
|
||||||
|
restore_animation.setStartValue(expanded_rect)
|
||||||
|
restore_animation.setEndValue(original_rect)
|
||||||
|
restore_animation.setEasingCurve(QEasingCurve.InBack)
|
||||||
|
restore_animation.start()
|
||||||
|
|
||||||
|
self.button_animation.finished.connect(restore_size)
|
||||||
|
self.button_animation.start()
|
||||||
|
|
||||||
|
print("[INFO] 成功动画已启动")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[WARNING] 动画效果失败: {e}")
|
||||||
|
|
||||||
|
def on_platform_connected(self, platform_name: str, all_platforms: list):
|
||||||
|
"""处理平台连接成功事件"""
|
||||||
|
try:
|
||||||
|
import time
|
||||||
|
self.connected_platforms = all_platforms.copy()
|
||||||
|
self.last_platform_connect_time = time.time()
|
||||||
|
|
||||||
|
# 短暂显示平台连接成功提示
|
||||||
|
self.status_label.setText(f"🟢 {platform_name}平台连接成功!")
|
||||||
|
self.status_label.setStyleSheet(
|
||||||
|
"color: #28a745; background: rgba(40, 167, 69, 0.15); border-radius: 12px; padding: 5px 10px; font-weight: bold;")
|
||||||
|
|
||||||
|
# 停止之前的定时器
|
||||||
|
if self.status_timer:
|
||||||
|
self.status_timer.stop()
|
||||||
|
self.status_timer = None
|
||||||
|
|
||||||
|
self.add_log(f"GUI收到平台连接通知: {platform_name}, 当前已连接: {', '.join(all_platforms)}", "INFO")
|
||||||
|
|
||||||
|
# 使用线程定时器,更可靠(资源消耗极小)
|
||||||
|
import threading
|
||||||
|
|
||||||
|
def delayed_update():
|
||||||
|
import time
|
||||||
|
time.sleep(3) # 等待3秒
|
||||||
|
# 使用QTimer.singleShot确保在主线程中执行GUI更新
|
||||||
|
QTimer.singleShot(0, self.delayed_platform_summary)
|
||||||
|
|
||||||
|
# 启动轻量级守护线程(3秒后自动销毁)
|
||||||
|
timer_thread = threading.Thread(target=delayed_update, daemon=True)
|
||||||
|
timer_thread.start()
|
||||||
|
|
||||||
|
self.add_log("3秒线程定时器已启动,将自动切换到汇总显示", "INFO")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.add_log(f"处理平台连接事件失败: {e}", "ERROR")
|
||||||
|
|
||||||
|
def delayed_platform_summary(self):
|
||||||
|
"""定时器触发的汇总显示更新"""
|
||||||
|
try:
|
||||||
|
self.add_log("🎯 定时器触发!开始执行汇总显示更新", "INFO")
|
||||||
|
self.update_platform_summary()
|
||||||
|
self.add_log("🎯 定时器执行完成", "INFO")
|
||||||
|
except Exception as e:
|
||||||
|
self.add_log(f"定时器执行失败: {e}", "ERROR")
|
||||||
|
|
||||||
|
def check_and_update_summary(self):
|
||||||
|
"""检查是否应该更新汇总显示"""
|
||||||
|
try:
|
||||||
|
import time
|
||||||
|
elapsed = time.time() - self.last_platform_connect_time
|
||||||
|
self.add_log(f"检查汇总更新: 距离最后连接 {elapsed:.1f} 秒", "INFO")
|
||||||
|
|
||||||
|
# 检查是否在最近1秒内还有新的平台连接
|
||||||
|
if elapsed >= 2.8: # 距离最后连接超过2.8秒
|
||||||
|
self.add_log("满足条件,开始更新汇总显示", "INFO")
|
||||||
|
self.update_platform_summary()
|
||||||
|
else:
|
||||||
|
# 如果还有新连接,再延迟一会儿
|
||||||
|
self.add_log(f"还需等待 {2.8 - elapsed:.1f} 秒,延迟更新", "INFO")
|
||||||
|
QTimer.singleShot(1000, self.check_and_update_summary)
|
||||||
|
except Exception as e:
|
||||||
|
self.add_log(f"检查汇总更新失败: {e}", "ERROR")
|
||||||
|
|
||||||
|
def update_platform_summary(self):
|
||||||
|
"""更新平台连接汇总显示"""
|
||||||
|
try:
|
||||||
|
self.add_log(f"开始更新汇总显示,当前连接平台: {self.connected_platforms}", "INFO")
|
||||||
|
|
||||||
|
if self.connected_platforms:
|
||||||
|
platforms_text = "、".join(self.connected_platforms)
|
||||||
|
summary_text = f"🟢 已连接: {platforms_text}"
|
||||||
|
self.status_label.setText(summary_text)
|
||||||
|
self.status_label.setStyleSheet(
|
||||||
|
"color: #28a745; background: rgba(40, 167, 69, 0.1); border-radius: 12px; padding: 5px 10px;")
|
||||||
|
self.add_log(f"汇总显示已更新: {summary_text}", "INFO")
|
||||||
|
else:
|
||||||
|
self.status_label.setText("🟢 连接成功!等待平台指令...")
|
||||||
|
self.status_label.setStyleSheet(
|
||||||
|
"color: #28a745; background: rgba(40, 167, 69, 0.1); border-radius: 12px; padding: 5px 10px;")
|
||||||
|
self.add_log("已更新为等待平台指令状态", "INFO")
|
||||||
|
except Exception as e:
|
||||||
|
self.add_log(f"更新平台汇总显示失败: {e}", "ERROR")
|
||||||
|
|
||||||
|
def init_system_tray(self):
|
||||||
|
"""初始化系统托盘"""
|
||||||
|
# 检查系统是否支持托盘
|
||||||
|
if not QSystemTrayIcon.isSystemTrayAvailable():
|
||||||
|
QMessageBox.critical(None, "系统托盘",
|
||||||
|
"系统托盘不可用")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 创建托盘图标
|
||||||
|
self.tray_icon = QSystemTrayIcon(self)
|
||||||
|
|
||||||
|
# 使用自定义的美观图标
|
||||||
|
try:
|
||||||
|
tray_icon_path = os.path.join(os.path.dirname(__file__), "static", "ai_assistant_icon_16.png")
|
||||||
|
if os.path.exists(tray_icon_path):
|
||||||
|
from PyQt5.QtGui import QIcon
|
||||||
|
self.tray_icon.setIcon(QIcon(tray_icon_path))
|
||||||
|
print(f"[INFO] 已加载托盘图标: {tray_icon_path}")
|
||||||
|
else:
|
||||||
|
print(f"[WARNING] 托盘图标文件不存在: {tray_icon_path}")
|
||||||
|
# 备用方案:使用系统图标
|
||||||
|
self.tray_icon.setIcon(self.style().standardIcon(self.style().SP_ComputerIcon))
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[WARNING] 加载托盘图标失败: {e}, 使用默认图标")
|
||||||
|
# 备用方案:使用系统图标
|
||||||
|
self.tray_icon.setIcon(self.style().standardIcon(self.style().SP_ComputerIcon))
|
||||||
|
|
||||||
|
# 创建托盘菜单
|
||||||
|
tray_menu = QMenu()
|
||||||
|
|
||||||
|
# 显示窗口动作
|
||||||
|
show_action = QAction("显示主窗口", self)
|
||||||
|
show_action.triggered.connect(self.show_window)
|
||||||
|
tray_menu.addAction(show_action)
|
||||||
|
|
||||||
|
# 分隔线
|
||||||
|
tray_menu.addSeparator()
|
||||||
|
|
||||||
|
# 退出动作
|
||||||
|
quit_action = QAction("退出程序", self)
|
||||||
|
quit_action.triggered.connect(self.quit_application)
|
||||||
|
tray_menu.addAction(quit_action)
|
||||||
|
|
||||||
|
# 设置托盘菜单
|
||||||
|
self.tray_icon.setContextMenu(tray_menu)
|
||||||
|
|
||||||
|
# 设置托盘提示
|
||||||
|
self.tray_icon.setToolTip("AI客服智能助手")
|
||||||
|
|
||||||
|
# 双击托盘图标显示窗口
|
||||||
|
self.tray_icon.activated.connect(self.tray_icon_activated)
|
||||||
|
|
||||||
|
# 显示托盘图标
|
||||||
|
self.tray_icon.show()
|
||||||
|
|
||||||
|
print("[INFO] 系统托盘已初始化")
|
||||||
|
|
||||||
|
def tray_icon_activated(self, reason):
|
||||||
|
"""托盘图标被激活时的处理"""
|
||||||
|
if reason == QSystemTrayIcon.DoubleClick:
|
||||||
|
self.show_window()
|
||||||
|
|
||||||
|
def show_window(self):
|
||||||
|
"""显示主窗口"""
|
||||||
|
self.show()
|
||||||
|
self.raise_()
|
||||||
|
self.activateWindow()
|
||||||
|
print("[INFO] 主窗口已显示")
|
||||||
|
|
||||||
|
def quit_application(self):
|
||||||
|
"""真正退出应用程序"""
|
||||||
|
print("[INFO] 正在退出应用程序...")
|
||||||
|
|
||||||
|
# 执行原来的关闭逻辑
|
||||||
try:
|
try:
|
||||||
# 使用 WebSocket 管理器断开所有连接
|
# 使用 WebSocket 管理器断开所有连接
|
||||||
from WebSocket.backend_singleton import get_websocket_manager
|
from WebSocket.backend_singleton import get_websocket_manager
|
||||||
@@ -358,8 +553,124 @@ class LoginWindow(QMainWindow):
|
|||||||
worker.loop.close()
|
worker.loop.close()
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"关闭窗口时发生错误: {str(e)}")
|
print(f"关闭时发生错误: {str(e)}")
|
||||||
finally:
|
finally:
|
||||||
|
# 隐藏托盘图标
|
||||||
|
if hasattr(self, 'tray_icon'):
|
||||||
|
self.tray_icon.hide()
|
||||||
|
|
||||||
|
# 退出应用程序
|
||||||
|
QApplication.quit()
|
||||||
|
|
||||||
|
def add_log(self, message, log_type="INFO"):
|
||||||
|
"""添加日志信息 - 只输出到终端"""
|
||||||
|
from datetime import datetime
|
||||||
|
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
|
||||||
|
# 根据日志类型设置颜色(ANSI颜色码)
|
||||||
|
colors = {
|
||||||
|
"ERROR": "\033[91m", # 红色
|
||||||
|
"SUCCESS": "\033[92m", # 绿色
|
||||||
|
"WARNING": "\033[93m", # 黄色
|
||||||
|
"INFO": "\033[94m", # 蓝色
|
||||||
|
"DEBUG": "\033[95m" # 紫色
|
||||||
|
}
|
||||||
|
reset = "\033[0m" # 重置颜色
|
||||||
|
|
||||||
|
color = colors.get(log_type, colors["INFO"])
|
||||||
|
log_entry = f"{color}[{timestamp}] [{log_type}] {message}{reset}"
|
||||||
|
print(log_entry)
|
||||||
|
|
||||||
|
def login(self):
|
||||||
|
"""处理连接逻辑"""
|
||||||
|
# 防止重复快速点击
|
||||||
|
import time
|
||||||
|
current_time = time.time()
|
||||||
|
if current_time - self.last_login_time < self.login_cooldown:
|
||||||
|
self.add_log(f"请等待 {self.login_cooldown} 秒后再试", "WARNING")
|
||||||
|
return
|
||||||
|
|
||||||
|
self.last_login_time = current_time
|
||||||
|
|
||||||
|
token = self.token_input.text().strip()
|
||||||
|
if not token:
|
||||||
|
self.add_log("请输入有效的连接令牌", "ERROR")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 禁用连接按钮,防止重复点击
|
||||||
|
self.login_btn.setEnabled(False)
|
||||||
|
self.login_btn.setText("连接中...")
|
||||||
|
self.status_label.setText("正在连接服务...")
|
||||||
|
self.status_label.setStyleSheet("color: #ffc107;")
|
||||||
|
|
||||||
|
success = False
|
||||||
|
try:
|
||||||
|
# 使用 WebSocket 管理器处理连接
|
||||||
|
ws_manager = get_websocket_manager()
|
||||||
|
|
||||||
|
# 设置回调函数
|
||||||
|
ws_manager.set_callbacks(
|
||||||
|
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 # 新增:平台连接回调
|
||||||
|
)
|
||||||
|
|
||||||
|
# 连接后端
|
||||||
|
success = ws_manager.connect_backend(token)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
self.add_log("已启动WebSocket连接管理器", "SUCCESS")
|
||||||
|
self.status_label.setText("🟢 连接成功!等待平台指令...")
|
||||||
|
self.status_label.setStyleSheet(
|
||||||
|
"color: #28a745; background: rgba(40, 167, 69, 0.1); border-radius: 12px; padding: 5px 10px;")
|
||||||
|
self.login_btn.setText("✓ 已连接")
|
||||||
|
self.login_btn.setObjectName("connected")
|
||||||
|
self.login_btn.setStyleSheet(self.login_btn.styleSheet()) # 刷新样式
|
||||||
|
|
||||||
|
# 添加成功动画效果
|
||||||
|
self.animate_success()
|
||||||
|
else:
|
||||||
|
self.add_log("WebSocket连接管理器启动失败", "ERROR")
|
||||||
|
self.status_label.setText("🔴 连接失败,请检查令牌")
|
||||||
|
self.status_label.setStyleSheet(
|
||||||
|
"color: #dc3545; background: rgba(220, 53, 69, 0.1); border-radius: 12px; padding: 5px 10px;")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.add_log(f"连接失败: {e}", "ERROR")
|
||||||
|
self.status_label.setText(f"🔴 连接失败: {str(e)}")
|
||||||
|
self.status_label.setStyleSheet(
|
||||||
|
"color: #dc3545; background: rgba(220, 53, 69, 0.1); border-radius: 12px; padding: 5px 10px;")
|
||||||
|
finally:
|
||||||
|
self.login_btn.setEnabled(True)
|
||||||
|
if not success:
|
||||||
|
self.login_btn.setText("重新连接")
|
||||||
|
self.login_btn.setObjectName("loginButton") # 恢复原始样式
|
||||||
|
|
||||||
|
def verify_token(self, token):
|
||||||
|
"""简化的令牌校验:非空即通过(实际校验由后端承担)"""
|
||||||
|
return bool(token)
|
||||||
|
|
||||||
|
def closeEvent(self, event):
|
||||||
|
"""窗口关闭事件处理 - 最小化到托盘"""
|
||||||
|
if hasattr(self, 'tray_icon') and self.tray_icon.isVisible():
|
||||||
|
# 首次最小化时显示提示消息
|
||||||
|
if not hasattr(self, '_tray_message_shown'):
|
||||||
|
self.tray_icon.showMessage(
|
||||||
|
"AI客服智能助手",
|
||||||
|
"程序已最小化到系统托盘。双击托盘图标可重新显示窗口。",
|
||||||
|
QSystemTrayIcon.Information,
|
||||||
|
3000
|
||||||
|
)
|
||||||
|
self._tray_message_shown = True
|
||||||
|
|
||||||
|
# 隐藏窗口而不是关闭
|
||||||
|
self.hide()
|
||||||
|
event.ignore()
|
||||||
|
print("[INFO] 窗口已最小化到系统托盘")
|
||||||
|
else:
|
||||||
|
# 如果托盘不可用,则正常退出
|
||||||
|
self.quit_application()
|
||||||
event.accept()
|
event.accept()
|
||||||
|
|
||||||
|
|
||||||
@@ -371,8 +682,36 @@ def main():
|
|||||||
app.setApplicationName(config.WINDOW_TITLE)
|
app.setApplicationName(config.WINDOW_TITLE)
|
||||||
app.setApplicationVersion(config.VERSION)
|
app.setApplicationVersion(config.VERSION)
|
||||||
|
|
||||||
|
# 设置应用程序唯一ID(重要:避免和Python默认图标混淆)
|
||||||
|
try:
|
||||||
|
import ctypes
|
||||||
|
# 设置应用程序用户模型ID,让Windows识别为独立应用
|
||||||
|
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID("ShuidropAI.CustomerService.1.0")
|
||||||
|
print("[INFO] 已设置应用程序唯一ID")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[WARNING] 设置应用程序ID失败: {e}")
|
||||||
|
|
||||||
# 创建主窗口
|
# 创建主窗口
|
||||||
window = LoginWindow()
|
window = LoginWindow()
|
||||||
|
|
||||||
|
# 统一设置所有图标(使用任务栏修复模块)
|
||||||
|
try:
|
||||||
|
app_icon = setup_windows_taskbar_icon(app, window)
|
||||||
|
print("[INFO] 所有图标设置完成")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[WARNING] 图标设置失败: {e}")
|
||||||
|
# 备用方案:使用简单的图标设置
|
||||||
|
try:
|
||||||
|
icon_path = os.path.join(os.path.dirname(__file__), "static", "ai_assistant_icon_32.png")
|
||||||
|
if os.path.exists(icon_path):
|
||||||
|
from PyQt5.QtGui import QIcon
|
||||||
|
backup_icon = QIcon(icon_path)
|
||||||
|
app.setWindowIcon(backup_icon)
|
||||||
|
window.setWindowIcon(backup_icon)
|
||||||
|
print(f"[INFO] 已使用备用图标: {icon_path}")
|
||||||
|
except Exception as e2:
|
||||||
|
print(f"[ERROR] 备用图标也设置失败: {e2}")
|
||||||
|
|
||||||
window.show() # 程序启动断点
|
window.show() # 程序启动断点
|
||||||
|
|
||||||
# 运行应用程序
|
# 运行应用程序
|
||||||
@@ -380,7 +719,7 @@ def main():
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
main() # sd_aAoHIO9fDRIkePZEhW6oaFgK6IzAPxuB 测试令牌(token)
|
main() # sd_acF0TisgfFOtsBm4ytqb17MQbcxuX9Vp 测试令牌(token)
|
||||||
# username = "KLD测试"
|
# username = "KLD测试"
|
||||||
# password = "kld168168"
|
# password = "kld168168"
|
||||||
# taobao nickname = "tb420723827:redboat"
|
# taobao nickname = "tb420723827:redboat"
|
||||||
|
|||||||
300
quick_build.py
Normal file
300
quick_build.py
Normal file
@@ -0,0 +1,300 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
快速打包脚本 - 避免spec文件缩进问题
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
|
||||||
|
def clean_build():
|
||||||
|
"""清理构建目录"""
|
||||||
|
print("🧹 清理旧的构建文件...")
|
||||||
|
|
||||||
|
# 跳过进程结束步骤,避免影响当前脚本运行
|
||||||
|
print("🔄 跳过进程结束步骤(避免脚本自终止)...")
|
||||||
|
|
||||||
|
# 等待少量时间确保文件句柄释放
|
||||||
|
import time
|
||||||
|
print("⏳ 等待文件句柄释放...")
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
dirs_to_clean = ['dist', 'build']
|
||||||
|
for dir_name in dirs_to_clean:
|
||||||
|
print(f"🔍 检查目录: {dir_name}")
|
||||||
|
if os.path.exists(dir_name):
|
||||||
|
try:
|
||||||
|
print(f"🗑️ 正在删除: {dir_name}")
|
||||||
|
shutil.rmtree(dir_name)
|
||||||
|
print(f"✅ 已删除: {dir_name}")
|
||||||
|
except PermissionError as e:
|
||||||
|
print(f"⚠️ {dir_name} 被占用,尝试强制删除... 错误: {e}")
|
||||||
|
try:
|
||||||
|
subprocess.run(['rd', '/s', '/q', dir_name],
|
||||||
|
shell=True, check=True)
|
||||||
|
print(f"✅ 强制删除成功: {dir_name}")
|
||||||
|
except Exception as e2:
|
||||||
|
print(f"❌ 无法删除 {dir_name}: {e2}")
|
||||||
|
print("💡 请手动删除或运行 force_clean_dist.bat")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ 删除 {dir_name} 时出错: {e}")
|
||||||
|
else:
|
||||||
|
print(f"ℹ️ {dir_name} 不存在,跳过")
|
||||||
|
|
||||||
|
print("✅ 清理阶段完成")
|
||||||
|
|
||||||
|
|
||||||
|
def build_with_command():
|
||||||
|
"""使用命令行参数直接打包"""
|
||||||
|
print("🚀 开始打包...")
|
||||||
|
|
||||||
|
cmd = [
|
||||||
|
'pyinstaller',
|
||||||
|
'--name=main',
|
||||||
|
'--onedir', # 相当于 --exclude-binaries
|
||||||
|
'--windowed', # 相当于 -w
|
||||||
|
'--add-data=config.py;.',
|
||||||
|
'--add-data=exe_file_logger.py;.',
|
||||||
|
'--add-data=Utils/PythonNew32;Utils/PythonNew32',
|
||||||
|
'--add-data=Utils/JD;Utils/JD',
|
||||||
|
'--add-data=Utils/Dy;Utils/Dy',
|
||||||
|
'--add-data=Utils/Pdd;Utils/Pdd',
|
||||||
|
'--add-data=Utils/QianNiu;Utils/QianNiu',
|
||||||
|
'--add-data=Utils/message_models.py;Utils',
|
||||||
|
'--add-data=Utils/__init__.py;Utils',
|
||||||
|
'--add-data=WebSocket;WebSocket',
|
||||||
|
'--add-data=static;static',
|
||||||
|
'--add-binary=Utils/PythonNew32/SaiNiuApi.dll;Utils/PythonNew32',
|
||||||
|
'--add-binary=Utils/PythonNew32/SaiNiuSys.dll;Utils/PythonNew32',
|
||||||
|
'--add-binary=Utils/PythonNew32/SaiNiuServer.dll;Utils/PythonNew32',
|
||||||
|
'--add-binary=Utils/PythonNew32/python32.exe;Utils/PythonNew32',
|
||||||
|
'--add-binary=Utils/PythonNew32/python313.dll;Utils/PythonNew32',
|
||||||
|
'--add-binary=Utils/PythonNew32/vcruntime140.dll;Utils/PythonNew32',
|
||||||
|
'--hidden-import=PyQt5.QtCore',
|
||||||
|
'--hidden-import=PyQt5.QtGui',
|
||||||
|
'--hidden-import=PyQt5.QtWidgets',
|
||||||
|
'--hidden-import=websockets',
|
||||||
|
'--hidden-import=asyncio',
|
||||||
|
'--hidden-import=Utils.QianNiu.QianNiuUtils',
|
||||||
|
'--hidden-import=Utils.JD.JdUtils',
|
||||||
|
'--hidden-import=Utils.Dy.DyUtils',
|
||||||
|
'--hidden-import=Utils.Pdd.PddUtils',
|
||||||
|
'--hidden-import=WebSocket.backend_singleton',
|
||||||
|
'--hidden-import=WebSocket.BackendClient',
|
||||||
|
'main.py'
|
||||||
|
]
|
||||||
|
|
||||||
|
try:
|
||||||
|
print(f"执行命令: {' '.join(cmd[:5])}... (共{len(cmd)}个参数)")
|
||||||
|
result = subprocess.run(cmd, capture_output=True, text=True, encoding='utf-8')
|
||||||
|
|
||||||
|
if result.returncode == 0:
|
||||||
|
print("✅ 打包成功!")
|
||||||
|
print("📁 打包结果: dist/main/")
|
||||||
|
|
||||||
|
# 重命名目录
|
||||||
|
if os.path.exists('dist/main'):
|
||||||
|
try:
|
||||||
|
if os.path.exists('dist/MultiPlatformGUI'):
|
||||||
|
shutil.rmtree('dist/MultiPlatformGUI')
|
||||||
|
os.rename('dist/main', 'dist/MultiPlatformGUI')
|
||||||
|
print("✅ 已重命名为: dist/MultiPlatformGUI/")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"⚠️ 重命名失败: {e}")
|
||||||
|
print("💡 可以手动重命名: dist/main -> dist/MultiPlatformGUI")
|
||||||
|
print("📁 当前可用路径: dist/main/")
|
||||||
|
|
||||||
|
# 创建使用说明
|
||||||
|
try:
|
||||||
|
create_usage_guide()
|
||||||
|
except:
|
||||||
|
create_usage_guide_fallback()
|
||||||
|
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print("❌ 打包失败")
|
||||||
|
if result.stderr:
|
||||||
|
print("错误信息:")
|
||||||
|
print(result.stderr)
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ 打包过程出错: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def create_usage_guide():
|
||||||
|
"""创建使用说明"""
|
||||||
|
guide_content = r"""# 多平台客服GUI使用说明
|
||||||
|
|
||||||
|
## 🚀 运行方式
|
||||||
|
```
|
||||||
|
cd dist/MultiPlatformGUI
|
||||||
|
.\main.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📝 支持平台
|
||||||
|
- 千牛 (淘宝)
|
||||||
|
- 京东 (JD)
|
||||||
|
- 抖音 (DY)
|
||||||
|
- 拼多多 (PDD)
|
||||||
|
|
||||||
|
## 🎯 特点
|
||||||
|
- 支持多平台同时监听
|
||||||
|
- 无平台间冲突
|
||||||
|
- 自动生成日志文件
|
||||||
|
|
||||||
|
## 📁 文件结构
|
||||||
|
```
|
||||||
|
MultiPlatformGUI/
|
||||||
|
├── main.exe # 主程序
|
||||||
|
├── qianniu_exe_*.log # 运行日志
|
||||||
|
└── _internal/ # 程序依赖文件
|
||||||
|
├── Utils/
|
||||||
|
│ ├── PythonNew32/ # 千牛DLL文件
|
||||||
|
│ ├── JD/ # 京东模块
|
||||||
|
│ ├── Dy/ # 抖音模块
|
||||||
|
│ └── Pdd/ # 拼多多模块
|
||||||
|
└── ...
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open('dist/MultiPlatformGUI/使用说明.txt', 'w', encoding='utf-8') as f:
|
||||||
|
f.write(guide_content)
|
||||||
|
print("✅ 已生成使用说明文件")
|
||||||
|
except:
|
||||||
|
print("⚠️ 使用说明生成失败")
|
||||||
|
|
||||||
|
|
||||||
|
def create_usage_guide_fallback():
|
||||||
|
"""备用使用说明生成"""
|
||||||
|
guide_content = r"""# 多平台客服GUI使用说明
|
||||||
|
|
||||||
|
## 🚀 运行方式
|
||||||
|
```
|
||||||
|
cd dist/main
|
||||||
|
.\main.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📝 支持平台
|
||||||
|
- 千牛 (淘宝)
|
||||||
|
- 京东 (JD)
|
||||||
|
- 抖音 (DY)
|
||||||
|
- 拼多多 (PDD)
|
||||||
|
|
||||||
|
## 🎯 特点
|
||||||
|
- 支持多平台同时监听
|
||||||
|
- 无平台间冲突
|
||||||
|
- 自动生成日志文件
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open('dist/main/使用说明.txt', 'w', encoding='utf-8') as f:
|
||||||
|
f.write(guide_content)
|
||||||
|
print("✅ 已生成使用说明文件 (备用路径)")
|
||||||
|
except:
|
||||||
|
print("⚠️ 使用说明生成失败")
|
||||||
|
|
||||||
|
|
||||||
|
def verify_result():
|
||||||
|
"""验证打包结果"""
|
||||||
|
print("\n🔍 验证打包结果...")
|
||||||
|
|
||||||
|
# 先检查MultiPlatformGUI,再检查main目录
|
||||||
|
base_dirs = ["dist/MultiPlatformGUI", "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(exe_path):
|
||||||
|
size = os.path.getsize(exe_path)
|
||||||
|
print(f"✅ 主程序: {exe_path} ({size:,} bytes)")
|
||||||
|
|
||||||
|
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("❌ 未找到有效的打包结果")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""主函数"""
|
||||||
|
print("🔥 多平台客服GUI快速打包工具")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 检查依赖
|
||||||
|
print("🔍 检查打包依赖...")
|
||||||
|
try:
|
||||||
|
import subprocess
|
||||||
|
result = subprocess.run(['pyinstaller', '--version'],
|
||||||
|
capture_output=True, text=True, check=False)
|
||||||
|
if result.returncode == 0:
|
||||||
|
print(f"✅ PyInstaller 版本: {result.stdout.strip()}")
|
||||||
|
else:
|
||||||
|
print("❌ PyInstaller 未安装或不可用")
|
||||||
|
print("💡 请运行: pip install pyinstaller")
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ 检查PyInstaller时出错: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 清理
|
||||||
|
print("\n📍 开始清理阶段...")
|
||||||
|
clean_build()
|
||||||
|
print("📍 清理阶段完成")
|
||||||
|
|
||||||
|
# 打包
|
||||||
|
print("\n📍 开始打包阶段...")
|
||||||
|
if not build_with_command():
|
||||||
|
print("❌ 打包阶段失败")
|
||||||
|
return False
|
||||||
|
print("📍 打包阶段完成")
|
||||||
|
|
||||||
|
# 验证
|
||||||
|
print("\n📍 开始验证阶段...")
|
||||||
|
if not verify_result():
|
||||||
|
print("❌ 验证阶段失败")
|
||||||
|
return False
|
||||||
|
print("📍 验证阶段完成")
|
||||||
|
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print("🎉 打包完成!")
|
||||||
|
|
||||||
|
# 智能显示可用路径
|
||||||
|
if os.path.exists("dist/MultiPlatformGUI"):
|
||||||
|
print("📁 打包结果: dist/MultiPlatformGUI/")
|
||||||
|
print("🚀 运行方式: cd dist/MultiPlatformGUI && .\\main.exe")
|
||||||
|
elif os.path.exists("dist/main"):
|
||||||
|
print("📁 打包结果: dist/main/")
|
||||||
|
print("🚀 运行方式: cd dist/main && .\\main.exe")
|
||||||
|
print("💡 提示: 可手动重命名为 MultiPlatformGUI")
|
||||||
|
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ 打包失败: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
success = main()
|
||||||
|
if success:
|
||||||
|
print("\n✅ 可以开始测试了!")
|
||||||
|
else:
|
||||||
|
print("\n❌ 请检查错误信息并重试")
|
||||||
BIN
static/ai_assistant_icon_16.png
Normal file
BIN
static/ai_assistant_icon_16.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 727 B |
BIN
static/ai_assistant_icon_32.png
Normal file
BIN
static/ai_assistant_icon_32.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.1 KiB |
BIN
static/ai_assistant_icon_64.png
Normal file
BIN
static/ai_assistant_icon_64.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.5 KiB |
113
windows_taskbar_fix.py
Normal file
113
windows_taskbar_fix.py
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Windows任务栏图标修复模块
|
||||||
|
解决PyQt5应用在任务栏显示Python图标的问题
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from PyQt5.QtGui import QIcon, QPixmap
|
||||||
|
from PyQt5.QtCore import QSize
|
||||||
|
|
||||||
|
def set_windows_app_id(app_id="ShuidropAI.CustomerService.1.0"):
|
||||||
|
"""设置Windows应用程序用户模型ID"""
|
||||||
|
try:
|
||||||
|
import ctypes
|
||||||
|
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(app_id)
|
||||||
|
print(f"[INFO] Windows应用ID已设置: {app_id}")
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[WARNING] 设置Windows应用ID失败: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def create_multi_size_icon(base_path="static"):
|
||||||
|
"""创建包含多种尺寸的QIcon对象"""
|
||||||
|
try:
|
||||||
|
icon = QIcon()
|
||||||
|
|
||||||
|
# 我们有的图标文件
|
||||||
|
available_files = [
|
||||||
|
("ai_assistant_icon_16.png", 16),
|
||||||
|
("ai_assistant_icon_32.png", 32),
|
||||||
|
("ai_assistant_icon_64.png", 64),
|
||||||
|
("ai_assistant_icon_128.png", 128)
|
||||||
|
]
|
||||||
|
|
||||||
|
icon_loaded = False
|
||||||
|
for filename, size in available_files:
|
||||||
|
icon_path = os.path.join(base_path, filename)
|
||||||
|
if os.path.exists(icon_path):
|
||||||
|
icon.addFile(icon_path, QSize(size, size))
|
||||||
|
print(f"[INFO] 添加图标 {size}x{size}: {filename}")
|
||||||
|
icon_loaded = True
|
||||||
|
|
||||||
|
# 如果没有加载任何图标,尝试加载单个文件
|
||||||
|
if not icon_loaded:
|
||||||
|
fallback_path = os.path.join(base_path, "ai_assistant_icon_32.png")
|
||||||
|
if os.path.exists(fallback_path):
|
||||||
|
icon = QIcon(fallback_path)
|
||||||
|
print(f"[INFO] 使用备用图标: {fallback_path}")
|
||||||
|
else:
|
||||||
|
print("[WARNING] 没有找到任何图标文件")
|
||||||
|
|
||||||
|
return icon
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[ERROR] 创建多尺寸图标失败: {e}")
|
||||||
|
return QIcon()
|
||||||
|
|
||||||
|
def setup_windows_taskbar_icon(app, window=None):
|
||||||
|
"""完整设置Windows任务栏图标"""
|
||||||
|
print("[INFO] 开始设置Windows任务栏图标...")
|
||||||
|
|
||||||
|
# 1. 设置应用程序唯一ID
|
||||||
|
set_windows_app_id()
|
||||||
|
|
||||||
|
# 2. 创建多尺寸图标
|
||||||
|
icon = create_multi_size_icon()
|
||||||
|
|
||||||
|
# 3. 设置应用程序级别图标
|
||||||
|
app.setWindowIcon(icon)
|
||||||
|
print("[INFO] 应用程序图标已设置")
|
||||||
|
|
||||||
|
# 4. 如果提供了窗口,也设置窗口图标
|
||||||
|
if window:
|
||||||
|
window.setWindowIcon(icon)
|
||||||
|
print("[INFO] 窗口图标已设置")
|
||||||
|
|
||||||
|
# 5. 强制刷新应用程序图标缓存
|
||||||
|
try:
|
||||||
|
if window and hasattr(window, 'winId'):
|
||||||
|
# 等待窗口完全初始化
|
||||||
|
from PyQt5.QtCore import QTimer
|
||||||
|
def delayed_icon_refresh():
|
||||||
|
try:
|
||||||
|
window.setWindowIcon(icon)
|
||||||
|
print("[INFO] 已延迟刷新窗口图标")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[WARNING] 延迟图标刷新失败: {e}")
|
||||||
|
|
||||||
|
# 延迟100ms再次设置图标
|
||||||
|
QTimer.singleShot(100, delayed_icon_refresh)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[WARNING] 图标刷新失败: {e}")
|
||||||
|
|
||||||
|
print("[INFO] Windows任务栏图标设置完成")
|
||||||
|
return icon
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# 测试模块
|
||||||
|
from PyQt5.QtWidgets import QApplication, QMainWindow, QLabel
|
||||||
|
|
||||||
|
app = QApplication(sys.argv)
|
||||||
|
window = QMainWindow()
|
||||||
|
window.setWindowTitle("任务栏图标测试")
|
||||||
|
window.setCentralWidget(QLabel("测试Windows任务栏图标"))
|
||||||
|
|
||||||
|
# 设置任务栏图标
|
||||||
|
setup_windows_taskbar_icon(app, window)
|
||||||
|
|
||||||
|
window.show()
|
||||||
|
sys.exit(app.exec_())
|
||||||
Reference in New Issue
Block a user