Todo: 集成多平台 解决因SaiNiu线程抢占资源问题 本地提交测试环境打包 和 正式打包脚本与正式环境打包bat

This commit is contained in:
2025-09-17 17:48:35 +08:00
parent 256e6c21a5
commit 8b9fc925fa
11 changed files with 2020 additions and 610 deletions

File diff suppressed because it is too large Load Diff

View File

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

745
main.py
View File

@@ -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,104 +279,256 @@ 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)
ws_manager.set_callbacks( shadow.setYOffset(5)
log=self.add_log, shadow.setColor(QColor(0, 0, 0, 40))
success=lambda: self.add_log("WebSocket连接管理器连接成功", "SUCCESS"),
error=lambda error: self.add_log(f"WebSocket连接管理器错误: {error}", "ERROR") # 为输入框添加阴影
) input_shadow = QGraphicsDropShadowEffect()
input_shadow.setBlurRadius(8)
# 连接后端 input_shadow.setXOffset(0)
success = ws_manager.connect_backend(token) input_shadow.setYOffset(2)
input_shadow.setColor(QColor(0, 0, 0, 20))
if success: self.token_input.setGraphicsEffect(input_shadow)
self.add_log("已启动WebSocket连接管理器", "SUCCESS")
else: # 为按钮添加阴影
self.add_log("WebSocket连接管理器启动失败", "ERROR") 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)
# 设置对象名称以应用特定样式
self.token_input.setObjectName("tokenInput")
self.login_btn.setObjectName("loginButton")
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
ws_manager = get_websocket_manager() ws_manager = get_websocket_manager()
ws_manager.disconnect_all() ws_manager.disconnect_all()
# 停止所有工作线程(向后兼容) # 停止所有工作线程(向后兼容)
workers = [] workers = []
if hasattr(self, 'jd_worker') and self.jd_worker: if hasattr(self, 'jd_worker') and self.jd_worker:
@@ -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
View 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❌ 请检查错误信息并重试")

Binary file not shown.

After

Width:  |  Height:  |  Size: 727 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

113
windows_taskbar_fix.py Normal file
View 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_())