Files
shuidrop_gui/main.py

997 lines
39 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import sys
from PyQt5.QtCore import Qt, pyqtSignal, QObject
from PyQt5.QtGui import QFont, QPalette, QColor
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
QHBoxLayout, QLabel, QPushButton, QLineEdit,
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
from WebSocket.backend_singleton import get_websocket_manager
from windows_taskbar_fix import setup_windows_taskbar_icon
import os
# ===================== 文件日志系统 - 生产环境启用 =====================
# 重定向所有输出到文件,确保有日志记录
# from exe_file_logger import setup_file_logging, log_to_file # 生产环境禁用
# setup_file_logging() # 生产环境禁用 # 生产环境启用自动日志功能
# print("文件日志系统已在main.py中初始化") # 生产环境禁用
# 新增: 版本更新信号类(用于线程安全的 GUI 通知)
class UpdateSignals(QObject):
"""版本更新信号"""
update_available = pyqtSignal(str, str) # (latest_version, download_url)
# 新增: 断开连接信号类(用于线程安全的断开提示)
class DisconnectSignals(QObject):
"""断开连接信号"""
disconnected = pyqtSignal(str) # (disconnect_message)
# 新增: 用户名密码输入对话框类
class LoginWindow(QMainWindow):
def __init__(self):
super().__init__()
self.jd_worker = None
self.progress_dialog = None
self.douyin_worker = None
self.qian_niu_worker = None
self.pdd_worker = None
# 重复执行防护
self.last_login_time = 0
self.login_cooldown = 2 # 登录冷却时间(秒)
# 平台连接状态管理
self.connected_platforms = []
self.status_timer = None
self.last_platform_connect_time = 0 # 记录最后一次平台连接时间
# 日志管理相关变量已删除
# 创建版本更新信号对象(线程安全)
self.update_signals = UpdateSignals()
self.update_signals.update_available.connect(self._show_update_dialog)
# 创建断开连接信号对象(线程安全)
self.disconnect_signals = DisconnectSignals()
self.disconnect_signals.disconnected.connect(self._show_disconnect_dialog)
self.initUI()
# 延迟设置版本检查器确保WebSocket连接已建立
QTimer.singleShot(5000, self.setup_version_checker)
def initUI(self):
# 设置窗口基本属性
self.setWindowTitle(f'AI客服智能助手 v{config.APP_VERSION}')
self.setGeometry(300, 300, 450, 300) # 进一步减小窗口尺寸
self.setMinimumSize(420, 280) # 设置更小的最小尺寸
self.setMaximumSize(500, 350) # 设置最大尺寸,保持紧凑
# 窗口图标将由统一的任务栏修复模块设置,这里只做备用检查
print("[INFO] 窗口图标将由统一模块设置")
# 创建中央widget
central_widget = QWidget()
self.setCentralWidget(central_widget)
# 创建主布局
main_layout = QVBoxLayout()
main_layout.setSpacing(15) # 减小间距
main_layout.setContentsMargins(30, 20, 30, 20) # 减小边距
central_widget.setLayout(main_layout)
# 添加标题和副标题
title_label = QLabel('AI客服智能助手')
title_label.setObjectName("title")
title_label.setAlignment(Qt.AlignCenter)
title_label.setFont(QFont('Microsoft YaHei', 18, QFont.Bold)) # 减小字体
title_label.setStyleSheet("color: #2c3e50; margin-bottom: 3px;")
main_layout.addWidget(title_label)
# 添加副标题说明
subtitle_label = QLabel('智能连接多平台客服提供AI自动回复服务')
subtitle_label.setObjectName("subtitle")
subtitle_label.setAlignment(Qt.AlignCenter)
subtitle_label.setFont(QFont('Microsoft YaHei', 9)) # 减小字体
subtitle_label.setStyleSheet("color: #7f8c8d; margin-bottom: 10px;")
main_layout.addWidget(subtitle_label)
# 添加少量垂直空间
main_layout.addStretch(1)
# 创建令牌输入区域
token_layout = QVBoxLayout()
token_layout.setSpacing(8) # 减小间距
token_label = QLabel('访问令牌')
token_label.setFont(QFont('Microsoft YaHei', 11, QFont.Bold)) # 减小字体
token_label.setStyleSheet("color: #34495e;")
self.token_input = QLineEdit()
self.token_input.setPlaceholderText('请输入您的访问令牌以连接服务')
self.token_input.setEchoMode(QLineEdit.Password)
self.token_input.setFont(QFont('Microsoft YaHei', 10)) # 减小字体
# noinspection PyUnresolvedReferences
self.token_input.returnPressed.connect(self.login) # 表示回车提交
self.token_input.setMinimumHeight(38) # 减小输入框高度
# 预填已保存的令牌(如果存在)
try:
from config import get_saved_token
saved = get_saved_token()
if saved:
self.token_input.setText(saved)
except Exception:
pass
token_layout.addWidget(token_label)
token_layout.addWidget(self.token_input)
main_layout.addLayout(token_layout)
# 创建连接按钮
self.login_btn = QPushButton('连接服务')
self.login_btn.setFont(QFont('Microsoft YaHei', 11, QFont.Bold)) # 减小字体
self.login_btn.setMinimumHeight(40) # 减小按钮高度
self.login_btn.clicked.connect(self.login) # 表示点击提交
main_layout.addWidget(self.login_btn)
# 添加连接状态提示
self.status_label = QLabel('等待连接...')
self.status_label.setObjectName("status")
self.status_label.setAlignment(Qt.AlignCenter)
self.status_label.setFont(QFont('Microsoft YaHei', 9)) # 减小字体
self.status_label.setStyleSheet("color: #95a5a6; margin-top: 5px;")
main_layout.addWidget(self.status_label)
# 添加少量底部空间
main_layout.addStretch(1)
# 日志框已永久删除,只使用终端输出
self.log_display = None
# 应用现代化样式
self.apply_modern_styles()
# 添加视觉效果
self.add_visual_effects()
# 初始化系统托盘
self.init_system_tray()
# 系统初始化日志输出到终端
print("[INFO] 系统初始化完成")
def apply_modern_styles(self):
"""应用现代化简约样式 - 精致美化版"""
self.setStyleSheet("""
QMainWindow {
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 {
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 {
padding: 12px 16px;
border: 2px solid transparent;
border-radius: 12px;
font-size: 13px;
background: qlineargradient(
x1:0, y1:0, x2:0, y2:1,
stop:0 #ffffff,
stop:1 #f8fafb
);
selection-background-color: #007bff;
selection-color: white;
color: #2c3e50;
}
QLineEdit:focus {
border: 2px solid #007bff;
background: white;
outline: none;
}
QLineEdit:hover {
border: 2px solid #6c757d;
background: white;
}
QPushButton {
background: qlineargradient(
x1:0, y1:0, x2:0, y2:1,
stop:0 #4285f4,
stop:0.5 #1976d2,
stop:1 #1565c0
);
border: none;
color: white;
padding: 12px 24px;
border-radius: 12px;
font-size: 13px;
font-weight: 600;
min-width: 180px;
letter-spacing: 0.5px;
}
QPushButton:hover {
background: qlineargradient(
x1:0, y1:0, x2:0, y2:1,
stop:0 #5294f5,
stop:0.5 #2986d3,
stop:1 #1e74c1
);
}
QPushButton:pressed {
background: qlineargradient(
x1:0, y1:0, x2:0, y2:1,
stop:0 #3274d4,
stop:0.5 #1666c2,
stop:1 #1455b0
);
}
QPushButton:disabled {
background: qlineargradient(
x1:0, y1:0, x2:0, y2:1,
stop:0 #f8f9fa,
stop:1 #e9ecef
);
color: #6c757d;
border: 1px solid #dee2e6;
}
/* 连接成功状态的按钮 */
QPushButton[objectName="connected"] {
background: qlineargradient(
x1:0, y1:0, x2:0, y2:1,
stop:0 #28a745,
stop:0.5 #20963b,
stop:1 #1e7e34
);
}
QPushButton[objectName="connected"]:hover {
background: qlineargradient(
x1:0, y1:0, x2:0, y2:1,
stop:0 #34ce57,
stop:0.5 #28a745,
stop:1 #20963b
);
}
""")
# 设置全局字体确保各Windows版本显示一致
font = QFont('Microsoft YaHei', 10) # Windows系统自带字体
QApplication.setFont(font)
# 设置调色板确保颜色一致性
palette = QPalette()
palette.setColor(QPalette.Window, QColor(248, 250, 251))
palette.setColor(QPalette.WindowText, QColor(44, 62, 80))
palette.setColor(QPalette.Base, QColor(255, 255, 255))
palette.setColor(QPalette.AlternateBase, QColor(241, 244, 246))
palette.setColor(QPalette.ToolTipBase, QColor(255, 255, 255))
palette.setColor(QPalette.ToolTipText, QColor(44, 62, 80))
palette.setColor(QPalette.Text, QColor(44, 62, 80))
palette.setColor(QPalette.Button, QColor(66, 133, 244))
palette.setColor(QPalette.ButtonText, QColor(255, 255, 255))
palette.setColor(QPalette.BrightText, QColor(255, 255, 255))
palette.setColor(QPalette.Highlight, QColor(66, 133, 244))
palette.setColor(QPalette.HighlightedText, QColor(255, 255, 255))
QApplication.setPalette(palette)
def add_visual_effects(self):
"""添加视觉效果"""
try:
# 为主窗口添加阴影效果
shadow = QGraphicsDropShadowEffect()
shadow.setBlurRadius(20)
shadow.setXOffset(0)
shadow.setYOffset(5)
shadow.setColor(QColor(0, 0, 0, 40))
# 为输入框添加阴影
input_shadow = QGraphicsDropShadowEffect()
input_shadow.setBlurRadius(8)
input_shadow.setXOffset(0)
input_shadow.setYOffset(2)
input_shadow.setColor(QColor(0, 0, 0, 20))
self.token_input.setGraphicsEffect(input_shadow)
# 为按钮添加阴影
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:
print(f"[WARNING] 添加视觉效果失败: {e}")
def animate_success(self):
"""连接成功的动画效果"""
try:
# 按钮轻微放大动画
self.button_animation = QPropertyAnimation(self.login_btn, b"geometry")
original_rect = self.login_btn.geometry()
# 计算放大后的位置(居中放大)
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 on_token_error(self, error_content: str):
"""处理token错误 - 显示红色错误信息并停止所有操作"""
try:
self.add_log(f"Token验证失败: {error_content}", "ERROR")
# 在状态标签显示红色错误信息
self.status_label.setText(f"🔴 {error_content}")
self.status_label.setStyleSheet(
"color: #dc3545; background: rgba(220, 53, 69, 0.1); border-radius: 12px; padding: 5px 10px; font-weight: bold;")
# 重置按钮状态
self.login_btn.setEnabled(True)
self.login_btn.setText("重新连接")
self.login_btn.setObjectName("loginButton") # 恢复原始样式
# 清空已连接平台列表
self.connected_platforms.clear()
self.add_log("由于token无效已停止所有连接操作", "ERROR")
except Exception as e:
self.add_log(f"处理token错误失败: {e}", "ERROR")
def on_disconnect(self, disconnect_msg: str):
"""处理被踢下线 - 发射信号到主线程(可在任何线程中调用)"""
try:
self.add_log(f"📡 收到断开通知,准备发射信号: {disconnect_msg}", "INFO")
# 发射信号Qt 自动调度到主线程)
self.disconnect_signals.disconnected.emit(disconnect_msg)
self.add_log(f"✅ 断开信号已发射", "DEBUG")
except Exception as e:
self.add_log(f"❌ 发射断开信号失败: {e}", "ERROR")
import traceback
self.add_log(f"详细错误: {traceback.format_exc()}", "ERROR")
def _show_disconnect_dialog(self, disconnect_msg: str):
"""显示断开连接提示框(信号槽函数,始终在主线程中执行)"""
try:
self.add_log(f"🎯 主线程收到断开信号: {disconnect_msg}", "INFO")
# 在状态标签显示警告信息
self.status_label.setText(f"⚠️ 账号在其他设备登录")
self.status_label.setStyleSheet(
"color: #ff9800; background: rgba(255, 152, 0, 0.1); border-radius: 12px; padding: 5px 10px; font-weight: bold;")
# 重置按钮状态
self.login_btn.setEnabled(True)
self.login_btn.setText("重新连接")
self.login_btn.setObjectName("loginButton") # 恢复原始样式
self.login_btn.setStyleSheet(self.login_btn.styleSheet()) # 刷新样式
# 清空已连接平台列表
self.connected_platforms.clear()
# 显示友好的提示对话框(主线程中执行,不会卡顿)
QMessageBox.warning(
self,
"连接已断开",
f"{disconnect_msg}\n\n"
"如果是您本人在其他设备登录,可以忽略此提示。\n"
"如需继续使用,请点击\"重新连接\"按钮。",
QMessageBox.Ok
)
self.add_log("连接已断开,您可以重新连接", "INFO")
except Exception as e:
self.add_log(f"❌ 显示断开提示框失败: {e}", "ERROR")
import traceback
self.add_log(f"详细错误: {traceback.format_exc()}", "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:
# 导入路径处理函数
from windows_taskbar_fix import get_resource_path
tray_icon_path = get_resource_path("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:
# 使用 WebSocket 管理器断开所有连接
from WebSocket.backend_singleton import get_websocket_manager
ws_manager = get_websocket_manager()
ws_manager.disconnect_all()
# 停止所有工作线程(向后兼容)
workers = []
if hasattr(self, 'jd_worker') and self.jd_worker:
workers.append(self.jd_worker)
if hasattr(self, 'douyin_worker') and self.douyin_worker:
workers.append(self.douyin_worker)
if hasattr(self, 'qian_niu_worker') and self.qian_niu_worker:
workers.append(self.qian_niu_worker)
if hasattr(self, 'pdd_worker') and self.pdd_worker:
workers.append(self.pdd_worker)
# 停止所有线程
for worker in workers:
if worker.isRunning():
worker.stop()
worker.quit()
worker.wait(1000)
# 强制关闭事件循环
for worker in workers:
if hasattr(worker, 'loop') and worker.loop and not worker.loop.is_closed():
worker.loop.close()
except Exception as e:
print(f"关闭时发生错误: {str(e)}")
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, # 新增:平台连接回调
token_error=self.on_token_error, # 新增token错误回调
disconnect=self.on_disconnect # 新增:被踢下线回调
)
# 连接后端
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()
def setup_version_checker(self):
"""设置版本检查器的GUI回调"""
try:
from WebSocket.backend_singleton import get_websocket_manager
ws_manager = get_websocket_manager()
if ws_manager:
# 设置回调为信号发射函数(线程安全)
ws_manager.gui_update_callback = self.emit_update_signal
self.add_log("✅ 版本检查器GUI回调已设置", "SUCCESS")
else:
self.add_log("⚠️ WebSocket管理器未初始化", "WARNING")
except Exception as e:
self.add_log(f"❌ 设置版本检查器失败: {e}", "ERROR")
def emit_update_signal(self, latest_version, download_url):
"""发射更新信号(可以在任何线程中调用)"""
try:
self.add_log(f"📡 收到更新通知,准备发射信号: v{latest_version}", "INFO")
# 发射信号Qt 自动调度到主线程)
self.update_signals.update_available.emit(latest_version, download_url)
self.add_log(f"✅ 更新信号已发射", "DEBUG")
except Exception as e:
self.add_log(f"❌ 发射更新信号失败: {e}", "ERROR")
import traceback
self.add_log(f"详细错误: {traceback.format_exc()}", "ERROR")
def _show_update_dialog(self, latest_version, download_url):
"""显示更新对话框(信号槽函数,始终在主线程中执行)"""
try:
self.add_log(f"🎯 主线程收到更新信号: v{latest_version}", "INFO")
self.show_update_notification(latest_version, download_url)
except Exception as e:
self.add_log(f"❌ 显示更新对话框失败: {e}", "ERROR")
import traceback
self.add_log(f"详细错误: {traceback.format_exc()}", "ERROR")
def show_update_notification(self, latest_version, download_url):
"""显示版本更新通知"""
try:
self.add_log(f"🔔 准备显示更新通知: v{latest_version}", "INFO")
self.add_log(f" 下载地址: {download_url if download_url else '(空)'}", "INFO")
# 读取更新内容
update_content = self._get_update_content(latest_version)
# 检查下载地址
if not download_url or download_url.strip() == "":
# 下载地址为空,只显示通知,不提供下载
message = f"发现新版本 {latest_version}\n\n"
if update_content:
message += f"更新内容:\n{update_content}\n\n"
message += "下载地址暂未配置,请稍后再试或联系管理员。"
QMessageBox.information(
self,
"版本更新",
message,
QMessageBox.Ok
)
self.add_log(f"⚠️ 新版本 {latest_version} 的下载地址为空,已通知用户", "WARNING")
return # 安全返回,不崩溃
# 下载地址有效,显示更新对话框
message = f"发现新版本 {latest_version},是否立即更新?\n\n"
if update_content:
message += f"更新内容:\n{update_content}\n\n"
message += "点击确定将打开下载页面。"
reply = QMessageBox.question(
self,
"版本更新",
message,
QMessageBox.Yes | QMessageBox.No,
QMessageBox.Yes
)
if reply == QMessageBox.Yes:
self.add_log("用户选择立即更新", "INFO")
self.trigger_update(download_url, latest_version)
else:
self.add_log("用户选择稍后更新", "INFO")
# 用户点击"否",不做任何操作,程序继续运行
except Exception as e:
self.add_log(f"❌ 显示更新通知失败: {e}", "ERROR")
import traceback
self.add_log(f"详细错误: {traceback.format_exc()}", "ERROR")
def _get_update_content(self, version):
"""获取版本更新内容"""
try:
import json
version_file = "version_history.json"
if not os.path.exists(version_file):
return ""
with open(version_file, 'r', encoding='utf-8') as f:
history = json.load(f)
# 查找对应版本
for record in history:
if record.get('version') == version:
content = record.get('content', '')
# 去掉 [patch]/[minor]/[major] 标记
import re
content = re.sub(r'\[(patch|minor|major)\]\s*', '', content, flags=re.IGNORECASE)
# 限制长度
if len(content) > 150:
content = content[:150] + "..."
return content
return ""
except Exception as e:
self.add_log(f"⚠️ 读取更新内容失败: {e}", "DEBUG")
return ""
def trigger_update(self, download_url, latest_version):
"""触发更新下载(不阻塞主程序)"""
import webbrowser
import threading
# 检查下载地址是否有效
if not download_url or download_url.strip() == "":
self.add_log("⚠️ 下载地址为空,无法打开更新页面", "WARNING")
QMessageBox.warning(
self,
"下载地址缺失",
f"版本 {latest_version} 的下载地址暂未配置。\n\n请联系管理员或稍后再试。",
QMessageBox.Ok
)
return
# 明确提示用户即将下载
reply = QMessageBox.information(
self,
"准备下载更新",
f"即将打开浏览器下载 v{latest_version} 安装包。\n\n"
f"下载完成后请:\n"
f"1. 关闭本程序\n"
f"2. 运行下载的安装包\n"
f"3. 按照提示完成更新\n\n"
f"点击确定继续...",
QMessageBox.Ok | QMessageBox.Cancel,
QMessageBox.Ok
)
if reply == QMessageBox.Cancel:
self.add_log("用户取消下载", "INFO")
return
self.add_log(f"📂 准备打开下载页面: {download_url}", "INFO")
# 在独立线程中打开浏览器确保不阻塞GUI主线程
def open_browser_thread():
try:
webbrowser.open(download_url)
self.add_log("✅ 浏览器已打开,下载即将开始", "SUCCESS")
except Exception as e:
self.add_log(f"❌ 打开浏览器失败: {e}", "ERROR")
thread = threading.Thread(target=open_browser_thread, daemon=True)
thread.start()
self.add_log("✅ 已启动下载,程序继续运行", "INFO")
def main():
"""主函数,用于测试界面"""
app = QApplication(sys.argv)
# 设置应用程序属性
app.setApplicationName(config.WINDOW_TITLE)
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()
# 统一设置所有图标(使用任务栏修复模块)
try:
app_icon = setup_windows_taskbar_icon(app, window)
print("[INFO] 所有图标设置完成")
except Exception as e:
print(f"[WARNING] 图标设置失败: {e}")
# 备用方案:使用简单的图标设置
try:
# 导入路径处理函数
from windows_taskbar_fix import get_resource_path
icon_path = get_resource_path("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() # 程序启动断点
# 运行应用程序
sys.exit(app.exec_())
if __name__ == '__main__':
main() # sd_acF0TisgfFOtsBm4ytqb17MQbcxuX9Vp 测试令牌(token)
# username = "KLD测试"
# password = "kld168168"
# taobao nickname = "tb420723827:redboat"