Files
shuidrop_gui/main.py

1730 lines
71 KiB
Python
Raw Normal View History

2025-09-12 20:42:00 +08:00
import sys
from PyQt5.QtCore import Qt, pyqtSignal, QObject
2025-09-12 20:42:00 +08:00
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
2025-09-12 20:42:00 +08:00
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中初始化") # 开发环境启用
2025-09-12 20:42:00 +08:00
# 新增: 版本更新信号类(用于线程安全的 GUI 通知)
class UpdateSignals(QObject):
"""版本更新信号"""
update_available = pyqtSignal(str, str) # (latest_version, download_url)
2025-09-12 20:42:00 +08:00
# 新增: 断开连接信号类(用于线程安全的断开提示)
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)
# 新增: 用户名密码输入对话框类
2025-09-12 20:42:00 +08:00
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 # 记录最后一次平台连接时间
2025-09-12 20:42:00 +08:00
# 日志管理相关变量已删除
# 创建版本更新信号对象(线程安全)
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 # 阴影效果引用
2025-09-12 20:42:00 +08:00
self.initUI()
# 延迟设置版本检查器确保WebSocket连接已建立
QTimer.singleShot(5000, self.setup_version_checker)
2025-09-12 20:42:00 +08:00
def initUI(self):
# 设置窗口基本属性
self.setWindowTitle(f'水滴AI客服智能助手 v{config.APP_VERSION}')
# 只设置宽度,高度自适应内容
self.setFixedWidth(450) # 固定宽度
# 不设置固定高度,让窗口根据内容自适应
# 窗口图标将由统一的任务栏修复模块设置,这里只做备用检查
print("[INFO] 窗口图标将由统一模块设置")
2025-09-12 20:42:00 +08:00
# 创建中央widget
central_widget = QWidget()
self.setCentralWidget(central_widget)
# 创建主布局(最小化间距)
2025-09-12 20:42:00 +08:00
main_layout = QVBoxLayout()
main_layout.setSpacing(8) # 最小间距
main_layout.setContentsMargins(25, 12, 25, 12) # 最小边距
2025-09-12 20:42:00 +08:00
central_widget.setLayout(main_layout)
# 添加标题和副标题
title_label = QLabel('水滴AI客服智能助手')
title_label.setObjectName("title")
2025-09-12 20:42:00 +08:00
title_label.setAlignment(Qt.AlignCenter)
title_label.setFont(QFont('Microsoft YaHei', 16, QFont.Bold)) # 稍微减小字体
title_label.setStyleSheet("color: #2c3e50; margin-bottom: 2px;")
2025-09-12 20:42:00 +08:00
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) # 最小间距
2025-09-12 20:42:00 +08:00
token_label = QLabel('访问令牌')
token_label.setFont(QFont('Microsoft YaHei', 11, QFont.Bold)) # 减小字体
token_label.setStyleSheet("color: #34495e;")
2025-09-12 20:42:00 +08:00
self.token_input = QLineEdit()
self.token_input.setPlaceholderText('请输入您的访问令牌以连接服务')
2025-09-12 20:42:00 +08:00
self.token_input.setEchoMode(QLineEdit.Password)
self.token_input.setFont(QFont('Microsoft YaHei', 10))
2025-09-12 20:42:00 +08:00
# noinspection PyUnresolvedReferences
self.token_input.returnPressed.connect(self.login) # 表示回车提交
self.token_input.setMinimumHeight(34) # 最小输入框高度
2025-09-12 20:42:00 +08:00
# 预填已保存的令牌(如果存在)
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) # 最小按钮高度
2025-09-12 20:42:00 +08:00
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;")
main_layout.addWidget(self.status_label)
# 添加最小间距(为横幅留出位置)
main_layout.addSpacing(8)
# ============ 添加宣传横幅 ============
self.create_promo_banner(main_layout)
2025-09-12 20:42:00 +08:00
# 日志框已永久删除,只使用终端输出
self.log_display = None
# 应用现代化样式
self.apply_modern_styles()
# 添加视觉效果
self.add_visual_effects()
2025-09-12 20:42:00 +08:00
# 初始化系统托盘
self.init_system_tray()
2025-09-12 20:42:00 +08:00
# 系统初始化日志输出到终端
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}")
# 横幅颜色动画相关方法已移除(改用图片横幅)
2025-09-12 20:42:00 +08:00
def apply_modern_styles(self):
"""应用现代化简约样式 - 精致美化版"""
2025-09-12 20:42:00 +08:00
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);
2025-09-12 20:42:00 +08:00
}
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;
2025-09-12 20:42:00 +08:00
}
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;
2025-09-12 20:42:00 +08:00
selection-color: white;
color: #2c3e50;
2025-09-12 20:42:00 +08:00
}
QLineEdit:focus {
border: 2px solid #007bff;
background: white;
outline: none;
}
QLineEdit:hover {
border: 2px solid #6c757d;
background: white;
2025-09-12 20:42:00 +08:00
}
QPushButton {
background: qlineargradient(
x1:0, y1:0, x2:0, y2:1,
stop:0 #4285f4,
stop:0.5 #1976d2,
stop:1 #1565c0
);
2025-09-12 20:42:00 +08:00
border: none;
color: white;
padding: 12px 24px;
border-radius: 12px;
font-size: 13px;
font-weight: 600;
min-width: 180px;
letter-spacing: 0.5px;
2025-09-12 20:42:00 +08:00
}
QPushButton:hover {
background: qlineargradient(
x1:0, y1:0, x2:0, y2:1,
stop:0 #5294f5,
stop:0.5 #2986d3,
stop:1 #1e74c1
);
2025-09-12 20:42:00 +08:00
}
QPushButton:pressed {
background: qlineargradient(
x1:0, y1:0, x2:0, y2:1,
stop:0 #3274d4,
stop:0.5 #1666c2,
stop:1 #1455b0
);
2025-09-12 20:42:00 +08:00
}
QPushButton:disabled {
background: qlineargradient(
x1:0, y1:0, x2:0, y2:1,
stop:0 #f8f9fa,
stop:1 #e9ecef
);
color: #6c757d;
border: 1px solid #dee2e6;
2025-09-12 20:42:00 +08:00
}
/* 连接成功状态的按钮 */
QPushButton[objectName="connected"] {
background: qlineargradient(
x1:0, y1:0, x2:0, y2:1,
stop:0 #28a745,
stop:0.5 #20963b,
stop:1 #1e7e34
);
2025-09-12 20:42:00 +08:00
}
QPushButton[objectName="connected"]:hover {
background: qlineargradient(
x1:0, y1:0, x2:0, y2:1,
stop:0 #34ce57,
stop:0.5 #28a745,
stop:1 #20963b
);
2025-09-12 20:42:00 +08:00
}
""")
# 设置全局字体确保各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))
2025-09-12 20:42:00 +08:00
palette.setColor(QPalette.Base, QColor(255, 255, 255))
palette.setColor(QPalette.AlternateBase, QColor(241, 244, 246))
2025-09-12 20:42:00 +08:00
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))
2025-09-12 20:42:00 +08:00
palette.setColor(QPalette.ButtonText, QColor(255, 255, 255))
palette.setColor(QPalette.BrightText, QColor(255, 255, 255))
palette.setColor(QPalette.Highlight, QColor(66, 133, 244))
2025-09-12 20:42:00 +08:00
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()
2025-09-12 20:42:00 +08:00
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", # 红色
2025-09-12 20:42:00 +08:00
"SUCCESS": "\033[92m", # 绿色
"WARNING": "\033[93m", # 黄色
"INFO": "\033[94m", # 蓝色
"DEBUG": "\033[95m" # 紫色
2025-09-12 20:42:00 +08:00
}
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;")
2025-09-12 20:42:00 +08:00
success = False
2025-09-12 20:42:00 +08:00
try:
# 使用 WebSocket 管理器处理连接
ws_manager = get_websocket_manager()
2025-09-12 20:42:00 +08:00
# 设置回调函数
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 # 新增:余额不足回调
2025-09-12 20:42:00 +08:00
)
# 🔥 设置会话超时信号(用于拼多多会话过期重试)
ws_manager.session_timeout_signal = self.session_timeout_signals.session_timeout
2025-09-12 20:42:00 +08:00
# 连接后端
success = ws_manager.connect_backend(token)
2025-09-12 20:42:00 +08:00
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()
2025-09-12 20:42:00 +08:00
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;")
2025-09-12 20:42:00 +08:00
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;")
2025-09-12 20:42:00 +08:00
finally:
self.login_btn.setEnabled(True)
if not success:
self.login_btn.setText("重新连接")
self.login_btn.setObjectName("loginButton") # 恢复原始样式
2025-09-12 20:42:00 +08:00
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()
2025-09-12 20:42:00 +08:00
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")
# 🔥 修改调用新的trigger_update方法支持自动更新
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:
# 检查下载地址是否有效
if not download_url or download_url.strip() == "":
self.add_log("⚠️ 下载地址为空,无法更新", "WARNING")
QMessageBox.warning(
self,
"下载地址缺失",
f"版本 {latest_version} 的下载地址暂未配置。\n\n请联系管理员或稍后再试。",
QMessageBox.Ok
)
return
# 获取更新内容
update_content = self._get_update_content(latest_version)
if update_content:
content_preview = update_content[:100] + "..." if len(update_content) > 100 else update_content
else:
content_preview = "(暂无更新说明)"
# 🔥 新增:弹出三选一对话框(自动更新/手动下载/取消)
msg_box = QMessageBox(self)
msg_box.setWindowTitle("版本更新")
msg_box.setText(f"发现新版本 v{latest_version},是否更新?")
msg_box.setInformativeText(
f"更新内容:\n{content_preview}\n\n"
f"推荐使用「自动更新」,程序将自动下载并安装新版本。"
)
msg_box.setIcon(QMessageBox.Information)
# 添加三个按钮
auto_button = msg_box.addButton("自动更新", QMessageBox.YesRole)
manual_button = msg_box.addButton("手动下载", QMessageBox.NoRole)
cancel_button = msg_box.addButton("取消", QMessageBox.RejectRole)
msg_box.setDefaultButton(auto_button)
msg_box.exec_()
clicked_button = msg_box.clickedButton()
if clicked_button == auto_button:
# 🔥 自动更新:下载并安装
self.add_log("用户选择自动更新", "INFO")
self.start_auto_update(download_url, latest_version)
elif clicked_button == manual_button:
# 🔥 手动下载:打开浏览器
self.add_log("用户选择手动下载", "INFO")
self.start_manual_download(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 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(
2025-10-11 15:46:39 +08:00
self,
"自动更新失败",
f"自动更新启动失败: {str(e)}\n\n是否改为手动下载?",
QMessageBox.Yes | QMessageBox.No,
QMessageBox.Yes
2025-10-11 15:46:39 +08:00
)
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
2025-10-11 15:46:39 +08:00
# 明确提示用户即将下载
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
)
2025-10-11 15:46:39 +08:00
if reply == QMessageBox.Cancel:
self.add_log("用户取消手动下载", "INFO")
2025-10-11 15:46:39 +08:00
return
2025-10-11 15:46:39 +08:00
self.add_log(f"📂 准备打开下载页面: {download_url}", "INFO")
# 在独立线程中打开浏览器
2025-10-11 15:46:39 +08:00
def open_browser_thread():
try:
2025-10-11 15:46:39 +08:00
webbrowser.open(download_url)
self.add_log("✅ 浏览器已打开,下载即将开始", "SUCCESS")
except Exception as e:
self.add_log(f"❌ 打开浏览器失败: {e}", "ERROR")
2025-10-11 15:46:39 +08:00
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()
2025-09-12 20:42:00 +08:00
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}")
2025-09-12 20:42:00 +08:00
# 创建主窗口
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}")
2025-09-12 20:42:00 +08:00
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},但程序仍正常运行")
2025-09-12 20:42:00 +08:00
# 运行应用程序
sys.exit(app.exec_())
if __name__ == '__main__':
main() # sd_acF0TisgfFOtsBm4ytqb17MQbcxuX9Vp 测试令牌(token)
2025-09-12 20:42:00 +08:00
# username = "KLD测试"
# password = "kld168168"
# taobao nickname = "tb420723827:redboat"