1813 lines
75 KiB
Python
1813 lines
75 KiB
Python
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 BalanceInsufficientSignals(QObject):
|
||
"""余额不足信号"""
|
||
balance_insufficient = pyqtSignal(str) # (balance_message)
|
||
|
||
|
||
# 新增: 会话超时信号类(用于线程安全的超时重试提示)
|
||
class SessionTimeoutSignals(QObject):
|
||
"""会话超时信号"""
|
||
session_timeout = pyqtSignal(dict) # (retry_info)
|
||
|
||
|
||
# 新增: 用户名密码输入对话框类
|
||
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.balance_insufficient_signals = BalanceInsufficientSignals()
|
||
self.balance_insufficient_signals.balance_insufficient.connect(self._show_balance_insufficient_dialog)
|
||
|
||
# 创建会话超时信号对象(线程安全)
|
||
self.session_timeout_signals = SessionTimeoutSignals()
|
||
self.session_timeout_signals.session_timeout.connect(self._show_session_timeout_dialog)
|
||
|
||
# 横幅相关
|
||
self.promo_banner = None
|
||
self.banner_shadow = None # 阴影效果引用
|
||
|
||
# 🔥 版本更新提示状态管理
|
||
self.notified_version = None # 已提示过的版本号(本次运行期间)
|
||
self.pending_update_info = None # 待更新的版本信息 {version, url, content}
|
||
|
||
self.initUI()
|
||
|
||
# 延迟设置版本检查器,确保WebSocket连接已建立
|
||
QTimer.singleShot(5000, self.setup_version_checker)
|
||
|
||
def initUI(self):
|
||
# 设置窗口基本属性
|
||
self.setWindowTitle(f'水滴AI客服智能助手 v{config.APP_VERSION}')
|
||
# 只设置宽度,高度自适应内容
|
||
self.setFixedWidth(450) # 固定宽度
|
||
# 不设置固定高度,让窗口根据内容自适应
|
||
|
||
# 窗口图标将由统一的任务栏修复模块设置,这里只做备用检查
|
||
print("[INFO] 窗口图标将由统一模块设置")
|
||
|
||
# 创建中央widget
|
||
central_widget = QWidget()
|
||
self.setCentralWidget(central_widget)
|
||
|
||
# 创建主布局(最小化间距)
|
||
main_layout = QVBoxLayout()
|
||
main_layout.setSpacing(8) # 最小间距
|
||
main_layout.setContentsMargins(25, 12, 25, 12) # 最小边距
|
||
central_widget.setLayout(main_layout)
|
||
|
||
# 添加标题和副标题
|
||
title_label = QLabel('水滴AI客服智能助手')
|
||
title_label.setObjectName("title")
|
||
title_label.setAlignment(Qt.AlignCenter)
|
||
title_label.setFont(QFont('Microsoft YaHei', 16, QFont.Bold)) # 稍微减小字体
|
||
title_label.setStyleSheet("color: #2c3e50; margin-bottom: 2px;")
|
||
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: 5px;")
|
||
main_layout.addWidget(subtitle_label)
|
||
|
||
# 创建令牌输入区域(移除额外间距)
|
||
token_layout = QVBoxLayout()
|
||
token_layout.setSpacing(6) # 最小间距
|
||
|
||
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(34) # 最小输入框高度
|
||
# 预填已保存的令牌(如果存在)
|
||
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(36) # 最小按钮高度
|
||
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: 2px;")
|
||
self.status_label.setCursor(Qt.ArrowCursor) # 默认鼠标样式
|
||
|
||
# 🔥 让状态标签可点击(用于更新提示)
|
||
self.status_label.mousePressEvent = self.on_status_label_clicked
|
||
|
||
main_layout.addWidget(self.status_label)
|
||
|
||
# 添加最小间距(为横幅留出位置)
|
||
main_layout.addSpacing(8)
|
||
|
||
# ============ 添加宣传横幅 ============
|
||
self.create_promo_banner(main_layout)
|
||
|
||
# 日志框已永久删除,只使用终端输出
|
||
self.log_display = None
|
||
|
||
# 应用现代化样式
|
||
self.apply_modern_styles()
|
||
|
||
# 添加视觉效果
|
||
self.add_visual_effects()
|
||
|
||
# 初始化系统托盘
|
||
self.init_system_tray()
|
||
|
||
# 系统初始化日志输出到终端
|
||
print("[INFO] 系统初始化完成")
|
||
|
||
# 让窗口自适应内容高度(最小化)
|
||
self.adjustSize()
|
||
print("[INFO] 窗口已自适应内容大小")
|
||
|
||
def create_promo_banner(self, layout):
|
||
"""创建宣传横幅(使用图片,带悬停动画效果)"""
|
||
try:
|
||
# 创建横幅容器
|
||
banner_frame = QFrame()
|
||
banner_frame.setObjectName("promoBanner")
|
||
banner_frame.setCursor(Qt.PointingHandCursor) # 鼠标变手型
|
||
|
||
# 保存横幅引用,用于动画
|
||
self.promo_banner = banner_frame
|
||
|
||
# 使用图片标签
|
||
banner_image = QLabel()
|
||
banner_image.setObjectName("bannerImage")
|
||
|
||
# 加载横幅图片
|
||
from windows_taskbar_fix import get_resource_path
|
||
image_path = get_resource_path("static/hengfu.jpg")
|
||
|
||
if os.path.exists(image_path):
|
||
from PyQt5.QtGui import QPixmap
|
||
pixmap = QPixmap(image_path)
|
||
|
||
# 设置固定的横幅尺寸
|
||
target_width = 400 # 固定宽度
|
||
target_height = 70 # 固定高度(调整为70px,更紧凑)
|
||
|
||
# 缩放图片以适应目标尺寸
|
||
scaled_pixmap = pixmap.scaled(
|
||
target_width,
|
||
target_height,
|
||
Qt.IgnoreAspectRatio, # 忽略宽高比,拉伸填充
|
||
Qt.SmoothTransformation # 平滑缩放,提高图片质量
|
||
)
|
||
|
||
# 设置图片
|
||
banner_image.setPixmap(scaled_pixmap)
|
||
banner_image.setScaledContents(False)
|
||
|
||
banner_frame.setFixedHeight(target_height)
|
||
banner_image.setFixedSize(target_width, target_height)
|
||
|
||
# 🔧 使用CSS圆角来处理边角(更简单稳定)
|
||
banner_image.setStyleSheet("""
|
||
QLabel#bannerImage {
|
||
border-radius: 12px;
|
||
background-color: transparent;
|
||
}
|
||
""")
|
||
|
||
print(f"[INFO] 横幅图片已加载: {image_path}, 尺寸: {target_width}x{target_height}")
|
||
else:
|
||
# 图片不存在时的备用方案
|
||
banner_image.setText("🌟 限时优惠活动 - 点击了解详情 →")
|
||
banner_image.setAlignment(Qt.AlignCenter)
|
||
banner_image.setStyleSheet("""
|
||
background: qlineargradient(x1:0, y1:0, x2:1, y2:0,
|
||
stop:0 #667eea, stop:1 #764ba2);
|
||
color: white;
|
||
font-size: 12px;
|
||
font-weight: bold;
|
||
border-radius: 12px;
|
||
padding: 15px;
|
||
""")
|
||
banner_frame.setFixedHeight(60)
|
||
print(f"[WARNING] 横幅图片未找到: {image_path},使用备用样式")
|
||
|
||
# 创建布局
|
||
banner_layout = QHBoxLayout()
|
||
banner_layout.setContentsMargins(0, 0, 0, 0)
|
||
banner_layout.setSpacing(0)
|
||
banner_frame.setLayout(banner_layout)
|
||
banner_layout.addWidget(banner_image)
|
||
|
||
# 设置圆角和边框
|
||
banner_frame.setStyleSheet("""
|
||
QFrame#promoBanner {
|
||
border-radius: 12px;
|
||
background: transparent;
|
||
}
|
||
""")
|
||
|
||
# 初始阴影效果
|
||
self.banner_shadow = QGraphicsDropShadowEffect()
|
||
self.banner_shadow.setBlurRadius(15)
|
||
self.banner_shadow.setXOffset(0)
|
||
self.banner_shadow.setYOffset(3)
|
||
self.banner_shadow.setColor(QColor(0, 0, 0, 60))
|
||
banner_frame.setGraphicsEffect(self.banner_shadow)
|
||
|
||
# 点击事件 - 跳转官网
|
||
def open_official_website():
|
||
try:
|
||
import webbrowser
|
||
official_url = "https://shuidrop.com/"
|
||
webbrowser.open(official_url)
|
||
self.add_log(f"已打开官网: {official_url}", "INFO")
|
||
except Exception as e:
|
||
self.add_log(f"打开官网失败: {e}", "ERROR")
|
||
|
||
banner_frame.mousePressEvent = lambda event: open_official_website()
|
||
|
||
# 🎯 添加鼠标悬停事件(动态效果)
|
||
def on_enter(event):
|
||
"""鼠标进入时的动画效果"""
|
||
try:
|
||
# 增强阴影效果
|
||
self.banner_shadow.setBlurRadius(25)
|
||
self.banner_shadow.setYOffset(5)
|
||
self.banner_shadow.setColor(QColor(0, 0, 0, 100))
|
||
|
||
# 轻微放大动画
|
||
animation = QPropertyAnimation(banner_frame, b"geometry")
|
||
original_rect = banner_frame.geometry()
|
||
|
||
expanded_rect = QRect(
|
||
original_rect.x() - 3,
|
||
original_rect.y() - 2,
|
||
original_rect.width() + 6,
|
||
original_rect.height() + 4
|
||
)
|
||
|
||
animation.setDuration(200)
|
||
animation.setStartValue(original_rect)
|
||
animation.setEndValue(expanded_rect)
|
||
animation.setEasingCurve(QEasingCurve.OutCubic)
|
||
animation.start()
|
||
|
||
# 保存动画引用避免被垃圾回收
|
||
banner_frame.hover_animation = animation
|
||
|
||
except Exception as e:
|
||
print(f"[DEBUG] 悬停动画错误: {e}")
|
||
|
||
def on_leave(event):
|
||
"""鼠标离开时的动画效果"""
|
||
try:
|
||
# 恢复阴影效果
|
||
self.banner_shadow.setBlurRadius(15)
|
||
self.banner_shadow.setYOffset(3)
|
||
self.banner_shadow.setColor(QColor(0, 0, 0, 60))
|
||
|
||
# 恢复原始大小
|
||
animation = QPropertyAnimation(banner_frame, b"geometry")
|
||
current_rect = banner_frame.geometry()
|
||
|
||
original_rect = QRect(
|
||
current_rect.x() + 3,
|
||
current_rect.y() + 2,
|
||
current_rect.width() - 6,
|
||
current_rect.height() - 4
|
||
)
|
||
|
||
animation.setDuration(200)
|
||
animation.setStartValue(current_rect)
|
||
animation.setEndValue(original_rect)
|
||
animation.setEasingCurve(QEasingCurve.InCubic)
|
||
animation.start()
|
||
|
||
# 保存动画引用避免被垃圾回收
|
||
banner_frame.leave_animation = animation
|
||
|
||
except Exception as e:
|
||
print(f"[DEBUG] 离开动画错误: {e}")
|
||
|
||
# 安装事件过滤器
|
||
banner_frame.enterEvent = on_enter
|
||
banner_frame.leaveEvent = on_leave
|
||
|
||
# 添加到主布局
|
||
layout.addWidget(banner_frame)
|
||
|
||
print("[INFO] 宣传横幅已创建(使用图片,带悬停动画)")
|
||
|
||
except Exception as e:
|
||
print(f"[WARNING] 创建宣传横幅失败: {e}")
|
||
|
||
# 横幅颜色动画相关方法已移除(改用图片横幅)
|
||
|
||
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_platform_kicked(self, platform_name: str, store_name: str, reason: str):
|
||
"""处理平台被踢下线 - 显示弹窗警告"""
|
||
try:
|
||
self.add_log(f"⚠️ {platform_name}平台被踢下线: {store_name}, 原因: {reason}", "WARNING")
|
||
|
||
# 显示弹窗提示
|
||
message_text = (
|
||
f"【{store_name}】连接已断开\n\n"
|
||
f"原因:{reason}\n\n"
|
||
f"请确保:\n"
|
||
f"1. 关闭网页版{platform_name}客户端\n"
|
||
f"2. 关闭其他{platform_name}客户端\n"
|
||
f"3. 确认只有本程序连接\n\n"
|
||
f"处理完成后,请重新登录平台。"
|
||
)
|
||
QMessageBox.warning(
|
||
self,
|
||
f"{platform_name}连接已断开",
|
||
message_text,
|
||
QMessageBox.Ok
|
||
)
|
||
|
||
# 更新状态显示
|
||
self.status_label.setText(f"⚠️ {platform_name}已断开")
|
||
self.status_label.setStyleSheet(
|
||
"color: #ff6b6b; background: rgba(255, 107, 107, 0.1); border-radius: 12px; padding: 5px 10px;")
|
||
|
||
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 on_balance_insufficient(self, balance_msg: str):
|
||
"""处理余额不足 - 发射信号到主线程(可在任何线程中调用)"""
|
||
try:
|
||
self.add_log(f"📡 收到余额不足通知,准备发射信号: {balance_msg}", "INFO")
|
||
# 发射信号(Qt 自动调度到主线程)
|
||
self.balance_insufficient_signals.balance_insufficient.emit(balance_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 _show_session_timeout_dialog(self, retry_info: dict):
|
||
"""显示会话超时重试对话框(信号槽函数,始终在主线程中执行)"""
|
||
try:
|
||
elapsed_time = retry_info.get('elapsed_time', 0)
|
||
store_id = retry_info.get('store_id', 'unknown')
|
||
pdd_instance = retry_info.get('pdd_instance')
|
||
|
||
self.add_log(f"🎯 主线程收到会话超时信号: 已等待{elapsed_time}秒", "INFO")
|
||
|
||
# 显示重试对话框
|
||
reply = QMessageBox.question(
|
||
self,
|
||
"拼多多会话过期",
|
||
f"拼多多会话已过期,等待后端处理超时(已等待{elapsed_time}秒)。\n\n"
|
||
f"可能原因:\n"
|
||
f"1. 后端正在处理验证码,需要更多时间\n"
|
||
f"2. 网络连接问题\n"
|
||
f"3. 后端服务异常\n\n"
|
||
f"是否重新发送通知给后端?",
|
||
QMessageBox.Yes | QMessageBox.No,
|
||
QMessageBox.Yes
|
||
)
|
||
|
||
if reply == QMessageBox.Yes:
|
||
self.add_log("✅ [GUI] 用户选择重试", "INFO")
|
||
# 调用重试方法
|
||
if pdd_instance and hasattr(pdd_instance, 'retry_session_recovery'):
|
||
try:
|
||
# 在pdd_instance的事件循环中执行重试
|
||
import asyncio
|
||
import threading
|
||
|
||
def retry_in_thread():
|
||
try:
|
||
# 创建新的事件循环或使用现有的
|
||
try:
|
||
loop = asyncio.get_event_loop()
|
||
except RuntimeError:
|
||
loop = asyncio.new_event_loop()
|
||
asyncio.set_event_loop(loop)
|
||
|
||
# 执行重试
|
||
loop.run_until_complete(pdd_instance.retry_session_recovery())
|
||
self.add_log("✅ [GUI] 重试请求已发送", "SUCCESS")
|
||
except Exception as e:
|
||
self.add_log(f"❌ [GUI] 执行重试失败: {e}", "ERROR")
|
||
|
||
# 在新线程中执行异步操作
|
||
retry_thread = threading.Thread(target=retry_in_thread, daemon=True)
|
||
retry_thread.start()
|
||
|
||
except Exception as e:
|
||
self.add_log(f"❌ [GUI] 启动重试失败: {e}", "ERROR")
|
||
else:
|
||
self.add_log("ℹ️ [GUI] 用户取消重试", "INFO")
|
||
|
||
except Exception as e:
|
||
self.add_log(f"❌ 显示会话超时对话框失败: {e}", "ERROR")
|
||
import traceback
|
||
self.add_log(f"详细错误: {traceback.format_exc()}", "ERROR")
|
||
|
||
def _show_balance_insufficient_dialog(self, balance_msg: str):
|
||
"""显示余额不足提示框(信号槽函数,始终在主线程中执行)"""
|
||
try:
|
||
self.add_log(f"🎯 主线程收到余额不足信号: {balance_msg}", "INFO")
|
||
|
||
# 1. 断开所有连接(异步,不阻塞)
|
||
ws_manager = get_websocket_manager()
|
||
if ws_manager:
|
||
self.add_log("🔴 开始断开所有连接...", "INFO")
|
||
ws_manager.disconnect_all_async()
|
||
self.add_log("✅ 断开连接指令已发送", "INFO")
|
||
|
||
# 2. 更新UI状态
|
||
self.status_label.setText("🔴 余额不足")
|
||
self.status_label.setStyleSheet(
|
||
"color: #dc3545; background: rgba(220, 53, 69, 0.1); border-radius: 12px; padding: 5px 10px; font-weight: bold;")
|
||
|
||
# 3. 重置按钮状态
|
||
self.login_btn.setEnabled(True)
|
||
self.login_btn.setText("重新连接")
|
||
self.login_btn.setObjectName("loginButton")
|
||
self.login_btn.setStyleSheet(self.login_btn.styleSheet()) # 刷新样式
|
||
|
||
# 4. 清空已连接平台列表
|
||
self.connected_platforms.clear()
|
||
|
||
# 5. 显示弹窗提示(主线程中执行,不会卡顿)
|
||
QMessageBox.critical(
|
||
self,
|
||
"余额不足",
|
||
f"{balance_msg}\n\n"
|
||
f"已断开所有平台连接和后端连接。\n\n"
|
||
f"请充值后,点击「连接服务」按钮重新连接。",
|
||
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))
|
||
|
||
# 创建托盘菜单
|
||
self.tray_menu = QMenu()
|
||
|
||
# 连接菜单显示信号,实时更新状态
|
||
self.tray_menu.aboutToShow.connect(self.update_tray_menu)
|
||
|
||
# 初始化菜单内容
|
||
self.update_tray_menu()
|
||
|
||
# 设置托盘菜单
|
||
self.tray_icon.setContextMenu(self.tray_menu)
|
||
|
||
# 设置托盘提示
|
||
self.tray_icon.setToolTip("水滴AI客服智能助手")
|
||
|
||
# 双击托盘图标显示窗口
|
||
self.tray_icon.activated.connect(self.tray_icon_activated)
|
||
|
||
# 显示托盘图标
|
||
self.tray_icon.show()
|
||
|
||
print("[INFO] 系统托盘已初始化")
|
||
|
||
def update_tray_menu(self):
|
||
"""实时更新托盘菜单,显示连接状态"""
|
||
try:
|
||
# 清空现有菜单
|
||
self.tray_menu.clear()
|
||
|
||
# 获取WebSocket管理器
|
||
ws_manager = get_websocket_manager()
|
||
|
||
# 1. 显示后端连接状态
|
||
if ws_manager and ws_manager.backend_client and ws_manager.backend_client.is_connected:
|
||
backend_status = QAction("✅ AI服务已连接", self)
|
||
backend_status.setEnabled(False) # 不可点击
|
||
self.tray_menu.addAction(backend_status)
|
||
|
||
# 2. 显示已连接的平台信息
|
||
platform_listeners = ws_manager.platform_listeners
|
||
if platform_listeners:
|
||
# 按平台分组店铺
|
||
platform_stores = {}
|
||
for key, info in platform_listeners.items():
|
||
platform = info.get('platform', '')
|
||
store_name = info.get('store_name', '')
|
||
store_id = info.get('store_id', '')
|
||
|
||
if platform not in platform_stores:
|
||
platform_stores[platform] = []
|
||
|
||
# 优先显示店铺名称,如果没有则显示 store_id 前8位
|
||
if store_name:
|
||
display_name = store_name
|
||
else:
|
||
display_name = store_id[:8] + "..." if len(store_id) > 8 else store_id
|
||
|
||
platform_stores[platform].append(display_name)
|
||
|
||
# 显示每个平台的店铺
|
||
for platform, stores in platform_stores.items():
|
||
stores_text = ", ".join(stores)
|
||
platform_info = QAction(f"📊 {platform}: {stores_text}", self)
|
||
platform_info.setEnabled(False) # 不可点击
|
||
self.tray_menu.addAction(platform_info)
|
||
else:
|
||
# 后端已连接但没有平台连接
|
||
no_platform = QAction("⚠️ 暂无平台连接", self)
|
||
no_platform.setEnabled(False)
|
||
self.tray_menu.addAction(no_platform)
|
||
else:
|
||
# 后端未连接
|
||
backend_disconnected = QAction("❌ 后端未连接", self)
|
||
backend_disconnected.setEnabled(False)
|
||
self.tray_menu.addAction(backend_disconnected)
|
||
|
||
# 添加分隔线
|
||
self.tray_menu.addSeparator()
|
||
|
||
# 3. 显示主窗口
|
||
show_action = QAction("显示主窗口", self)
|
||
show_action.triggered.connect(self.show_window)
|
||
self.tray_menu.addAction(show_action)
|
||
|
||
# 添加分隔线
|
||
self.tray_menu.addSeparator()
|
||
|
||
# 4. 退出程序
|
||
quit_action = QAction("退出程序", self)
|
||
quit_action.triggered.connect(self.quit_application)
|
||
self.tray_menu.addAction(quit_action)
|
||
|
||
except Exception as e:
|
||
print(f"[ERROR] 更新托盘菜单失败: {e}")
|
||
# 如果更新失败,至少保证基本功能可用
|
||
self.tray_menu.clear()
|
||
show_action = QAction("显示主窗口", self)
|
||
show_action.triggered.connect(self.show_window)
|
||
self.tray_menu.addAction(show_action)
|
||
|
||
quit_action = QAction("退出程序", self)
|
||
quit_action.triggered.connect(self.quit_application)
|
||
self.tray_menu.addAction(quit_action)
|
||
|
||
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, # 新增:平台连接回调
|
||
platform_disconnected=self.on_platform_kicked, # 新增:平台被踢回调
|
||
token_error=self.on_token_error, # 新增:token错误回调
|
||
disconnect=self.on_disconnect, # 新增:被踢下线回调
|
||
balance_insufficient=self.on_balance_insufficient # 新增:余额不足回调
|
||
)
|
||
|
||
# 🔥 设置会话超时信号(用于拼多多会话过期重试)
|
||
ws_manager.session_timeout_signal = self.session_timeout_signals.session_timeout
|
||
|
||
# 连接后端
|
||
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 on_status_label_clicked(self, event):
|
||
"""状态标签被点击时的处理"""
|
||
try:
|
||
# 如果有待更新信息,点击打开更新对话框
|
||
if self.pending_update_info:
|
||
self.add_log("📱 用户点击状态标签,打开更新对话框", "INFO")
|
||
self.show_update_dialog_manual()
|
||
except Exception as e:
|
||
self.add_log(f"处理状态标签点击失败: {e}", "ERROR")
|
||
|
||
def show_update_dialog_manual(self):
|
||
"""手动打开更新对话框(用户点击状态标签时)"""
|
||
if not self.pending_update_info:
|
||
return
|
||
|
||
version = self.pending_update_info['version']
|
||
url = self.pending_update_info['url']
|
||
content = self.pending_update_info.get('content', '')
|
||
|
||
self.add_log(f"显示更新对话框: v{version}", "INFO")
|
||
self._show_update_dialog_internal(version, url, content)
|
||
|
||
def update_status_label_for_pending_update(self, latest_version, download_url):
|
||
"""更新状态标签显示待更新信息"""
|
||
try:
|
||
# 保存待更新信息
|
||
update_content = self._get_update_content(latest_version)
|
||
self.pending_update_info = {
|
||
'version': latest_version,
|
||
'url': download_url,
|
||
'content': update_content
|
||
}
|
||
|
||
# 更新状态标签
|
||
self.status_label.setText(f"🔔 新版本 v{latest_version} 可用(点击更新)")
|
||
self.status_label.setStyleSheet(
|
||
"color: #ff9800; background: rgba(255, 152, 0, 0.1); "
|
||
"border-radius: 12px; padding: 5px 10px; font-weight: bold; "
|
||
"cursor: pointer;" # 鼠标变手型
|
||
)
|
||
self.status_label.setCursor(Qt.PointingHandCursor) # 手型光标
|
||
|
||
self.add_log(f"✅ 状态标签已更新为待更新状态: v{latest_version}", "INFO")
|
||
|
||
except Exception as e:
|
||
self.add_log(f"更新状态标签失败: {e}", "ERROR")
|
||
|
||
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")
|
||
|
||
# 🔥 检查是否已经提示过这个版本
|
||
if self.notified_version == latest_version:
|
||
self.add_log(f"ℹ️ 版本 {latest_version} 已提示过,本次运行不再弹窗", "INFO")
|
||
# 只更新状态标签,不弹窗
|
||
self.update_status_label_for_pending_update(latest_version, download_url)
|
||
return
|
||
|
||
# 🔥 首次提示这个版本,显示对话框
|
||
self.add_log(f"🔔 首次提示版本 {latest_version},显示更新对话框", "INFO")
|
||
self.trigger_update(download_url, latest_version)
|
||
|
||
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):
|
||
"""触发更新提示(首次检测到新版本时调用)"""
|
||
try:
|
||
# 🔥 获取更新内容(如果没有则传空字符串,对话框会自动隐藏该区域)
|
||
update_content = self._get_update_content(latest_version)
|
||
if not update_content:
|
||
update_content = "" # 确保是空字符串而非None
|
||
|
||
# 调用内部方法显示对话框
|
||
self._show_update_dialog_internal(latest_version, download_url, update_content)
|
||
|
||
except Exception as e:
|
||
self.add_log(f"触发更新失败: {e}", "ERROR")
|
||
import traceback
|
||
self.add_log(f"详细错误: {traceback.format_exc()}", "ERROR")
|
||
|
||
def _show_update_dialog_internal(self, latest_version, download_url, update_content):
|
||
"""内部方法:显示更新对话框(使用自定义对话框)"""
|
||
try:
|
||
# 检查下载地址是否有效
|
||
if not download_url or download_url.strip() == "":
|
||
self.add_log("⚠️ 下载地址为空,无法更新", "WARNING")
|
||
QMessageBox.warning(
|
||
self,
|
||
"下载地址缺失",
|
||
f"版本 {latest_version} 的下载地址暂未配置。\n\n请联系管理员或稍后再试。",
|
||
QMessageBox.Ok
|
||
)
|
||
return
|
||
|
||
# 🔥 使用自定义对话框(美观布局)
|
||
from custom_update_dialog import UpdateNotificationDialog
|
||
|
||
dialog = UpdateNotificationDialog(
|
||
version=latest_version,
|
||
download_url=download_url,
|
||
update_content=update_content,
|
||
parent=self
|
||
)
|
||
|
||
dialog.exec_()
|
||
choice = dialog.get_user_choice()
|
||
|
||
# 处理用户选择
|
||
if choice == UpdateNotificationDialog.CHOICE_UPDATE_NOW:
|
||
# 🔥 立即更新:启动自动更新流程
|
||
self.add_log("用户选择立即更新", "INFO")
|
||
self.notified_version = latest_version # 标记已提示
|
||
self.start_auto_update(download_url, latest_version)
|
||
|
||
elif choice == UpdateNotificationDialog.CHOICE_UPDATE_LATER:
|
||
# 🔥 稍后更新:更新状态标签,保留提示
|
||
self.add_log("用户选择稍后更新", "INFO")
|
||
self.notified_version = latest_version # 标记已提示(避免重复弹窗)
|
||
self.update_status_label_for_pending_update(latest_version, download_url)
|
||
|
||
elif choice == UpdateNotificationDialog.CHOICE_IGNORE:
|
||
# 🔥 忽略此版本:标记已提示,清除待更新信息
|
||
self.add_log(f"用户忽略版本 {latest_version}", "INFO")
|
||
self.notified_version = latest_version # 标记已提示
|
||
self.pending_update_info = None # 清除待更新信息
|
||
else:
|
||
# 用户直接关闭对话框(等同于稍后更新)
|
||
self.add_log("用户关闭更新对话框", "INFO")
|
||
self.notified_version = latest_version
|
||
self.update_status_label_for_pending_update(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 start_auto_update(self, download_url, latest_version):
|
||
"""启动自动更新流程(下载 → 安装 → 重启)"""
|
||
try:
|
||
self.add_log("🚀 启动自动更新流程", "INFO")
|
||
|
||
# 导入自动更新模块
|
||
from update_dialog import UpdateProgressDialog
|
||
from auto_updater import UpdateDownloader
|
||
|
||
# 创建进度对话框
|
||
progress_dialog = UpdateProgressDialog(latest_version, self)
|
||
|
||
# 创建下载线程
|
||
downloader = UpdateDownloader(download_url, latest_version, max_retries=3)
|
||
progress_dialog.downloader = downloader
|
||
|
||
# 连接信号
|
||
downloader.progress.connect(progress_dialog.update_progress)
|
||
downloader.retry_info.connect(progress_dialog.show_retry_info)
|
||
downloader.finished.connect(lambda path: self.on_download_finished(path, progress_dialog))
|
||
downloader.error.connect(lambda err: self.on_download_error(err, progress_dialog, download_url, latest_version))
|
||
|
||
# 开始下载
|
||
self.add_log(f"📥 开始下载: {download_url}", "INFO")
|
||
downloader.start()
|
||
progress_dialog.exec_()
|
||
|
||
except Exception as e:
|
||
self.add_log(f"启动自动更新失败: {e}", "ERROR")
|
||
import traceback
|
||
self.add_log(f"详细错误: {traceback.format_exc()}", "ERROR")
|
||
|
||
# 失败后降级到手动下载
|
||
reply = QMessageBox.critical(
|
||
self,
|
||
"自动更新失败",
|
||
f"自动更新启动失败: {str(e)}\n\n是否改为手动下载?",
|
||
QMessageBox.Yes | QMessageBox.No,
|
||
QMessageBox.Yes
|
||
)
|
||
|
||
if reply == QMessageBox.Yes:
|
||
self.start_manual_download(download_url, latest_version)
|
||
|
||
def start_manual_download(self, download_url, latest_version):
|
||
"""启动手动下载(打开浏览器)"""
|
||
import webbrowser
|
||
import threading
|
||
|
||
# 明确提示用户即将下载
|
||
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")
|
||
|
||
# 在独立线程中打开浏览器
|
||
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 on_download_finished(self, installer_path, progress_dialog):
|
||
"""下载完成后的处理"""
|
||
try:
|
||
self.add_log(f"✅ 下载完成: {installer_path}", "SUCCESS")
|
||
|
||
# 🎯 更新进度对话框到UAC等待阶段(不关闭对话框)
|
||
progress_dialog.download_finished(installer_path)
|
||
|
||
# 延迟2秒,让用户看到"下载完成"提示,然后自动开始安装
|
||
QTimer.singleShot(2000, lambda: self.prepare_install(installer_path, progress_dialog))
|
||
|
||
except Exception as e:
|
||
self.add_log(f"处理下载完成事件失败: {e}", "ERROR")
|
||
|
||
def prepare_install(self, installer_path, progress_dialog):
|
||
"""准备安装(不再弹窗询问,直接开始)"""
|
||
try:
|
||
# 🔥 不再关闭进度对话框,也不再弹窗询问,直接开始安装
|
||
self.add_log("开始执行自动安装流程", "INFO")
|
||
|
||
from auto_updater import install_update_and_restart
|
||
|
||
# 🔥 启动静默安装(带UAC提升)
|
||
result = install_update_and_restart(installer_path)
|
||
|
||
if result:
|
||
# ✅ 用户授权成功,脚本已启动(但在等待GUI退出)
|
||
self.add_log("✅ 用户已授权UAC,更新脚本已启动", "SUCCESS")
|
||
self.add_log("进度条将执行到100%后提示用户", "INFO")
|
||
|
||
# 🎯 更新进度对话框到UAC授权完成阶段
|
||
progress_dialog.uac_authorized()
|
||
|
||
# 🔥 新增:等待进度条执行到100%后再处理
|
||
# 通过定时器循环检查进度是否到100%
|
||
self.progress_check_timer = QTimer()
|
||
|
||
def check_progress_complete():
|
||
if progress_dialog.restart_progress >= 100:
|
||
# 进度已到100%,停止检查并弹窗询问用户
|
||
self.progress_check_timer.stop()
|
||
self.show_update_complete_dialog(progress_dialog)
|
||
|
||
self.progress_check_timer.timeout.connect(check_progress_complete)
|
||
self.progress_check_timer.start(1000) # 每1秒检查一次
|
||
else:
|
||
# ⚠️ 用户取消了UAC授权,或启动失败
|
||
self.add_log("⚠️ 更新已取消(用户取消UAC授权或启动失败)", "WARNING")
|
||
|
||
# 关闭进度对话框
|
||
progress_dialog.reject()
|
||
|
||
# 显示友好提示
|
||
QMessageBox.warning(
|
||
self,
|
||
"更新已取消",
|
||
"未能启动自动更新。\n\n"
|
||
"可能原因:\n"
|
||
"• 您取消了管理员权限授权\n"
|
||
"• 系统安全策略限制\n\n"
|
||
f"安装包已保存到:\n{installer_path}\n\n"
|
||
"您可以稍后手动运行安装包进行更新。",
|
||
QMessageBox.Ok
|
||
)
|
||
|
||
except Exception as e:
|
||
self.add_log(f"准备安装失败: {e}", "ERROR")
|
||
import traceback
|
||
self.add_log(f"详细错误: {traceback.format_exc()}", "ERROR")
|
||
|
||
# 关闭进度对话框
|
||
progress_dialog.reject()
|
||
|
||
def on_download_error(self, error_msg, progress_dialog, download_url, latest_version):
|
||
"""下载失败处理"""
|
||
try:
|
||
self.add_log(f"❌ 下载失败: {error_msg}", "ERROR")
|
||
|
||
# 更新进度对话框状态
|
||
progress_dialog.download_error(error_msg)
|
||
|
||
# 延迟2秒后显示降级选项
|
||
QTimer.singleShot(2000, lambda: self.handle_download_failure(progress_dialog, download_url, latest_version, error_msg))
|
||
|
||
except Exception as e:
|
||
self.add_log(f"处理下载失败事件异常: {e}", "ERROR")
|
||
|
||
def handle_download_failure(self, progress_dialog, download_url, latest_version, error_msg):
|
||
"""处理下载失败,提供降级选项"""
|
||
try:
|
||
# 关闭进度对话框
|
||
progress_dialog.reject()
|
||
|
||
# 提示用户选择手动下载
|
||
reply = QMessageBox.critical(
|
||
self,
|
||
"自动下载失败",
|
||
f"自动下载失败:\n{error_msg}\n\n是否改为手动下载?",
|
||
QMessageBox.Yes | QMessageBox.No,
|
||
QMessageBox.Yes
|
||
)
|
||
|
||
if reply == QMessageBox.Yes:
|
||
self.start_manual_download(download_url, latest_version)
|
||
|
||
except Exception as e:
|
||
self.add_log(f"处理下载失败异常: {e}", "ERROR")
|
||
|
||
def show_update_complete_dialog(self, progress_dialog):
|
||
"""显示更新完成对话框,询问是否立即启动"""
|
||
try:
|
||
self.add_log("🎉 更新流程已完成,准备提示用户", "SUCCESS")
|
||
|
||
# 清理检查定时器
|
||
if hasattr(self, 'progress_check_timer') and self.progress_check_timer:
|
||
self.progress_check_timer.stop()
|
||
self.progress_check_timer = None
|
||
|
||
# 🔥 不立即关闭进度对话框,先弹窗询问用户
|
||
# 弹窗询问用户
|
||
reply = QMessageBox.question(
|
||
self,
|
||
"更新完成",
|
||
"🎉 恭喜!新版本安装成功!\n\n"
|
||
"是否立即启动新版本?\n\n"
|
||
"• 点击「是」:立即启动新版本\n"
|
||
"• 点击「否」:稍后手动启动",
|
||
QMessageBox.Yes | QMessageBox.No,
|
||
QMessageBox.Yes
|
||
)
|
||
|
||
# 关闭进度对话框
|
||
progress_dialog.accept()
|
||
|
||
if reply == QMessageBox.Yes:
|
||
self.add_log("✅ 用户选择立即启动新版本", "INFO")
|
||
self.add_log("🚀 程序立即退出,让批处理脚本完成安装", "INFO")
|
||
# 立即退出,让批处理脚本启动新版本(无延迟)
|
||
QApplication.instance().quit()
|
||
else:
|
||
self.add_log("ℹ️ 用户选择稍后手动启动", "INFO")
|
||
# 用户选择稍后启动,显示提示后退出
|
||
QMessageBox.information(
|
||
self,
|
||
"提示",
|
||
"程序即将退出。\n\n"
|
||
"您可以随时从开始菜单或桌面快捷方式\n"
|
||
"启动新版本的水滴AI客服智能助手。",
|
||
QMessageBox.Ok
|
||
)
|
||
self.add_log("🚀 程序退出", "INFO")
|
||
# 立即退出(无延迟)
|
||
QApplication.instance().quit()
|
||
|
||
except Exception as e:
|
||
self.add_log(f"❌ 显示更新完成对话框失败: {e}", "ERROR")
|
||
import traceback
|
||
self.add_log(f"详细错误: {traceback.format_exc()}", "ERROR")
|
||
# 关闭进度对话框
|
||
try:
|
||
progress_dialog.reject()
|
||
except:
|
||
pass
|
||
# 出错也要立即退出
|
||
QApplication.instance().quit()
|
||
|
||
def quit_for_update(self):
|
||
"""为更新而退出程序"""
|
||
self.add_log("正在退出程序以进行更新...", "INFO")
|
||
self.quit_application()
|
||
|
||
|
||
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() # 程序启动断点
|
||
|
||
# 🔥 检查是否是更新后首次启动,如果是则置顶显示
|
||
if '--after-update' in sys.argv:
|
||
print("[INFO] 检测到更新后启动标志,将窗口置顶显示")
|
||
try:
|
||
# Qt方法:将窗口提升到最前面
|
||
window.raise_()
|
||
window.activateWindow()
|
||
|
||
# Windows API:强制窗口前置(确保万无一失)
|
||
import ctypes
|
||
import win32gui
|
||
import win32con
|
||
|
||
# 获取窗口句柄
|
||
hwnd = int(window.winId())
|
||
|
||
# 方法1:设置窗口为前台窗口
|
||
win32gui.SetForegroundWindow(hwnd)
|
||
|
||
# 方法2:显示窗口并激活
|
||
win32gui.ShowWindow(hwnd, win32con.SW_SHOW)
|
||
win32gui.ShowWindow(hwnd, win32con.SW_RESTORE)
|
||
|
||
# 方法3:设置窗口Z序到顶部
|
||
win32gui.SetWindowPos(
|
||
hwnd,
|
||
win32con.HWND_TOPMOST, # 临时置顶
|
||
0, 0, 0, 0,
|
||
win32con.SWP_NOMOVE | win32con.SWP_NOSIZE
|
||
)
|
||
# 取消置顶(只是确保显示出来)
|
||
win32gui.SetWindowPos(
|
||
hwnd,
|
||
win32con.HWND_NOTOPMOST,
|
||
0, 0, 0, 0,
|
||
win32con.SWP_NOMOVE | win32con.SWP_NOSIZE | win32con.SWP_SHOWWINDOW
|
||
)
|
||
|
||
print("[INFO] ✅ 窗口已置顶显示")
|
||
except Exception as e:
|
||
print(f"[WARNING] 窗口置顶失败: {e},但程序仍正常运行")
|
||
|
||
# 运行应用程序
|
||
sys.exit(app.exec_())
|
||
|
||
|
||
if __name__ == '__main__':
|
||
main() # sd_acF0TisgfFOtsBm4ytqb17MQbcxuX9Vp 测试令牌(token)
|
||
# username = "KLD测试"
|
||
# password = "kld168168"
|
||
# taobao nickname = "tb420723827:redboat"
|
||
|