diff --git a/WebSocket/BackendClient.py b/WebSocket/BackendClient.py index 31972bd..23d5c6e 100644 --- a/WebSocket/BackendClient.py +++ b/WebSocket/BackendClient.py @@ -31,6 +31,7 @@ class BackendClient: self.success_callback: Optional[Callable] = None # 新增:后端连接成功回调 self.token_error_callback: Optional[Callable] = None # 新增:token错误回调 self.version_callback: Optional[Callable] = None # 新增:版本检查回调 + self.disconnect_callback: Optional[Callable] = None # 新增:被踢下线回调 self.is_connected = False @@ -265,7 +266,8 @@ class BackendClient: login: Callable = None, success: Callable = None, token_error: Callable = None, - version: Callable = None): + version: Callable = None, + disconnect: Callable = None): """设置各种消息类型的回调函数""" if store_list: self.store_list_callback = store_list @@ -287,6 +289,8 @@ class BackendClient: self.token_error_callback = token_error if version: self.version_callback = version + if disconnect: + self.disconnect_callback = disconnect def on_connected(self): """连接成功时的处理""" @@ -443,6 +447,8 @@ class BackendClient: self._handle_staff_list(message) elif msg_type == 'version_response': # 新增:版本检查响应 self._handle_version_response(message) + elif msg_type == 'disconnect': # 新增:被踢下线 + self._handle_disconnect(message) else: print(f"未知消息类型: {msg_type}") @@ -1229,6 +1235,19 @@ class BackendClient: if self.version_callback: self.version_callback(message) + def _handle_disconnect(self, message: Dict[str, Any]): + """处理被踢下线消息""" + disconnect_message = message.get('message', '您的账号在其他设备登录,当前连接已断开') + + print(f"[断开] 收到后端断开通知: {disconnect_message}") + + # 停止重连机制(不再尝试重连) + self.should_stop = True + + # 触发断开回调 + if self.disconnect_callback: + self.disconnect_callback(disconnect_message) + # ==================== 辅助方法 ==================== def set_token(self, token: str): diff --git a/WebSocket/backend_singleton.py b/WebSocket/backend_singleton.py index 92c324d..ae4f7ed 100644 --- a/WebSocket/backend_singleton.py +++ b/WebSocket/backend_singleton.py @@ -54,10 +54,11 @@ class WebSocketManager: 'error': None, 'platform_connected': None, 'token_error': None, + 'disconnect': None, # 新增:被踢下线回调 } def set_callbacks(self, log: Callable = None, success: Callable = None, error: Callable = None, - platform_connected: Callable = None, token_error: Callable = None): + platform_connected: Callable = None, token_error: Callable = None, disconnect: Callable = None): """设置回调函数""" if log: self.callbacks['log'] = log @@ -69,6 +70,8 @@ class WebSocketManager: self.callbacks['platform_connected'] = platform_connected if token_error: self.callbacks['token_error'] = token_error + if disconnect: + self.callbacks['disconnect'] = disconnect def _log(self, message: str, level: str = "INFO"): """内部日志方法""" @@ -146,8 +149,13 @@ class WebSocketManager: if self.callbacks['token_error']: self.callbacks['token_error'](error_content) + def _on_disconnect(disconnect_msg: str): + self._log(f"被踢下线: {disconnect_msg}", "WARNING") + if self.callbacks['disconnect']: + self.callbacks['disconnect'](disconnect_msg) + backend.set_callbacks(success=_on_backend_success, login=_on_backend_login, - token_error=_on_token_error) + token_error=_on_token_error, disconnect=_on_disconnect) if not backend.is_connected: backend.connect() @@ -182,7 +190,13 @@ class WebSocketManager: if self.callbacks['token_error']: self.callbacks['token_error'](error_content) - backend.set_callbacks(login=_on_backend_login, success=_on_backend_success, token_error=_on_token_error) + def _on_disconnect(disconnect_msg: str): + self._log(f"被踢下线: {disconnect_msg}", "ERROR") + if self.callbacks['disconnect']: + self.callbacks['disconnect'](disconnect_msg) + + backend.set_callbacks(login=_on_backend_login, success=_on_backend_success, + token_error=_on_token_error, disconnect=_on_disconnect) backend.connect() set_backend_client(backend) diff --git a/config.py b/config.py index 8a81fca..14ca835 100644 --- a/config.py +++ b/config.py @@ -1,135 +1,135 @@ -# -*- coding: utf-8 -*- -""" -项目配置文件 -统一管理所有配置参数,避免硬编码 -""" -# 用户访问令牌(默认空字符串) -import os # 用于路径与目录操作(写入用户配置目录) -import json # 用于将令牌保存为 JSON 格式 - -# 后端服务器配置 -# BACKEND_HOST = "192.168.5.233" -BACKEND_HOST = "192.168.5.12" -# BACKEND_HOST = "shuidrop.com" -# BACKEND_HOST = "test.shuidrop.com" -BACKEND_PORT = "8000" -# BACKEND_PORT = "" -BACKEND_WS_URL = f"ws://{BACKEND_HOST}:{BACKEND_PORT}" -# BACKEND_WS_URL = f"wss://{BACKEND_HOST}" - -# WebSocket配置 -WS_CONNECT_TIMEOUT = 16.0 -WS_MESSAGE_TIMEOUT = 30.0 -WS_PING_INTERVAL = 10 # 10秒ping间隔(提高检测频率) -WS_PING_TIMEOUT = 5 # 5秒ping超时(更快检测断线) -WS_ENABLE_PING = True # 是否启用WebSocket原生ping心跳 -WS_ENABLE_APP_PING = False # 禁用应用层ping心跳(避免重复) - -# AI处理超时配置 -AI_PROCESS_TIMEOUT = 30 # AI处理超时时间(秒) -AI_LONG_PROCESS_THRESHOLD = 10 # AI长时间处理阈值(秒) - -# 内存管理配置 -MAX_PENDING_REPLIES = 100 -CLEANUP_INTERVAL = 300 # 5分钟 -FUTURE_TIMEOUT = 300 # 5分钟 - -# 终端日志配置(简化) -LOG_LEVEL = "INFO" -VERSION = "1.0" - -# GUI配置 -WINDOW_TITLE = "AI回复连接入口-V1.0" - -# 应用版本号(用于版本检查) -APP_VERSION = "1.5.13" - -# 平台特定配置 -PLATFORMS = { - "JD": { - "name": "京东", - "ws_url": "wss://dongdong.jd.com/workbench/websocket" - }, - "DOUYIN": { - "name": "抖音", - "ws_url": None # 动态获取 - }, - "QIANNIU": { - "name": "千牛(淘宝)", - "ws_url": "ws://127.0.0.1:3030" - }, - "PDD": { - "name": "拼多多", - "ws_url": None # 动态获取 - } -} - -def get_backend_ws_url(platform: str, store_id: str) -> str: - """获取旧版后端WebSocket URL(按店铺建连接,保留兼容)""" - return f"{BACKEND_WS_URL}/ws/platform/{platform.lower()}/{store_id}/" - -def get_gui_ws_url(exe_token: str) -> str: - """获取新版单连接GUI专用WebSocket URL(按用户token建一条连接)""" - return f"{BACKEND_WS_URL}/ws/gui/{exe_token}/" - -def get_config(): - """获取所有配置""" - return { - 'backend_host': BACKEND_HOST, - 'backend_port': BACKEND_PORT, - 'backend_ws_url': BACKEND_WS_URL, - 'ws_connect_timeout': WS_CONNECT_TIMEOUT, - 'ws_message_timeout': WS_MESSAGE_TIMEOUT, - 'max_pending_replies': MAX_PENDING_REPLIES, - 'cleanup_interval': CLEANUP_INTERVAL, - 'platforms': PLATFORMS - } - - - -APP_NAME = "ShuidropGUI" # 应用名称(作为配置目录名) - -API_TOKEN = 'sd_acF0TisgfFOtsBm4ytqb17MQbcxuX9Vp' # 默认回退令牌(仅当未找到外部配置时使用) - -def _get_config_paths(): - """返回(配置目录, 配置文件路径),位于 %APPDATA%/ShuidropGUI/config.json""" - base_dir = os.getenv('APPDATA') or os.path.expanduser('~') # 优先使用 APPDATA,其次使用用户主目录 - cfg_dir = os.path.join(base_dir, APP_NAME) # 组合配置目录路径 - cfg_file = os.path.join(cfg_dir, 'config.json') # 组合配置文件路径 - return cfg_dir, cfg_file - - -def get_saved_token() -> str: - """优先从外部 JSON 配置读取令牌,不存在时回退到内置 API_TOKEN""" - try: - cfg_dir, cfg_file = _get_config_paths() # 获取目录与文件路径 - if os.path.exists(cfg_file): # 如果配置文件存在 - with open(cfg_file, 'r', encoding='utf-8') as f: # 以 UTF-8 读取 - data = json.load(f) # 解析 JSON 内容 - token = data.get('token', '') # 读取 token 字段 - if token: # 如果有效 - return token # 返回读取到的令牌 - except Exception: - pass # 读取失败时静默回退 - return API_TOKEN # 回退为内置的默认值 - - -def set_saved_token(new_token: str) -> bool: - """将访问令牌写入外部 JSON 配置,并更新内存中的值 - - new_token: 新的访问令牌字符串 - 返回: True 表示写入成功,False 表示失败 - """ - try: - cfg_dir, cfg_file = _get_config_paths() - os.makedirs(cfg_dir, exist_ok=True) - data = {'token': new_token} - with open(cfg_file, 'w', encoding='utf-8') as f: - json.dump(data, f, ensure_ascii=False, indent=2) - # 同步更新内存变量,保证运行期可立即生效 - global API_TOKEN - API_TOKEN = new_token - return True - except Exception as e: - # 发生异常时打印提示并返回失败 - print(f"写入令牌失败: {e}") +# -*- coding: utf-8 -*- +""" +项目配置文件 +统一管理所有配置参数,避免硬编码 +""" +# 用户访问令牌(默认空字符串) +import os # 用于路径与目录操作(写入用户配置目录) +import json # 用于将令牌保存为 JSON 格式 + +# 后端服务器配置 +# BACKEND_HOST = "192.168.5.233" +BACKEND_HOST = "192.168.5.12" +# BACKEND_HOST = "shuidrop.com" +# BACKEND_HOST = "test.shuidrop.com" +BACKEND_PORT = "8000" +# BACKEND_PORT = "" +BACKEND_WS_URL = f"ws://{BACKEND_HOST}:{BACKEND_PORT}" +# BACKEND_WS_URL = f"wss://{BACKEND_HOST}" + +# WebSocket配置 +WS_CONNECT_TIMEOUT = 16.0 +WS_MESSAGE_TIMEOUT = 30.0 +WS_PING_INTERVAL = 10 # 10秒ping间隔(提高检测频率) +WS_PING_TIMEOUT = 5 # 5秒ping超时(更快检测断线) +WS_ENABLE_PING = True # 是否启用WebSocket原生ping心跳 +WS_ENABLE_APP_PING = False # 禁用应用层ping心跳(避免重复) + +# AI处理超时配置 +AI_PROCESS_TIMEOUT = 30 # AI处理超时时间(秒) +AI_LONG_PROCESS_THRESHOLD = 10 # AI长时间处理阈值(秒) + +# 内存管理配置 +MAX_PENDING_REPLIES = 100 +CLEANUP_INTERVAL = 300 # 5分钟 +FUTURE_TIMEOUT = 300 # 5分钟 + +# 终端日志配置(简化) +LOG_LEVEL = "INFO" +VERSION = "1.0" + +# GUI配置 +WINDOW_TITLE = "AI回复连接入口-V1.0" + +# 应用版本号(用于版本检查) +APP_VERSION = "1.5.13" + +# 平台特定配置 +PLATFORMS = { + "JD": { + "name": "京东", + "ws_url": "wss://dongdong.jd.com/workbench/websocket" + }, + "DOUYIN": { + "name": "抖音", + "ws_url": None # 动态获取 + }, + "QIANNIU": { + "name": "千牛(淘宝)", + "ws_url": "ws://127.0.0.1:3030" + }, + "PDD": { + "name": "拼多多", + "ws_url": None # 动态获取 + } +} + +def get_backend_ws_url(platform: str, store_id: str) -> str: + """获取旧版后端WebSocket URL(按店铺建连接,保留兼容)""" + return f"{BACKEND_WS_URL}/ws/platform/{platform.lower()}/{store_id}/" + +def get_gui_ws_url(exe_token: str) -> str: + """获取新版单连接GUI专用WebSocket URL(按用户token建一条连接)""" + return f"{BACKEND_WS_URL}/ws/gui/{exe_token}/" + +def get_config(): + """获取所有配置""" + return { + 'backend_host': BACKEND_HOST, + 'backend_port': BACKEND_PORT, + 'backend_ws_url': BACKEND_WS_URL, + 'ws_connect_timeout': WS_CONNECT_TIMEOUT, + 'ws_message_timeout': WS_MESSAGE_TIMEOUT, + 'max_pending_replies': MAX_PENDING_REPLIES, + 'cleanup_interval': CLEANUP_INTERVAL, + 'platforms': PLATFORMS + } + + + +APP_NAME = "ShuidropGUI" # 应用名称(作为配置目录名) + +API_TOKEN = 'sd_acF0TisgfFOtsBm4ytqb17MQbcxuX9Vp' # 默认回退令牌(仅当未找到外部配置时使用) + +def _get_config_paths(): + """返回(配置目录, 配置文件路径),位于 %APPDATA%/ShuidropGUI/config.json""" + base_dir = os.getenv('APPDATA') or os.path.expanduser('~') # 优先使用 APPDATA,其次使用用户主目录 + cfg_dir = os.path.join(base_dir, APP_NAME) # 组合配置目录路径 + cfg_file = os.path.join(cfg_dir, 'config.json') # 组合配置文件路径 + return cfg_dir, cfg_file + + +def get_saved_token() -> str: + """优先从外部 JSON 配置读取令牌,不存在时回退到内置 API_TOKEN""" + try: + cfg_dir, cfg_file = _get_config_paths() # 获取目录与文件路径 + if os.path.exists(cfg_file): # 如果配置文件存在 + with open(cfg_file, 'r', encoding='utf-8') as f: # 以 UTF-8 读取 + data = json.load(f) # 解析 JSON 内容 + token = data.get('token', '') # 读取 token 字段 + if token: # 如果有效 + return token # 返回读取到的令牌 + except Exception: + pass # 读取失败时静默回退 + return API_TOKEN # 回退为内置的默认值 + + +def set_saved_token(new_token: str) -> bool: + """将访问令牌写入外部 JSON 配置,并更新内存中的值 + - new_token: 新的访问令牌字符串 + 返回: True 表示写入成功,False 表示失败 + """ + try: + cfg_dir, cfg_file = _get_config_paths() + os.makedirs(cfg_dir, exist_ok=True) + data = {'token': new_token} + with open(cfg_file, 'w', encoding='utf-8') as f: + json.dump(data, f, ensure_ascii=False, indent=2) + # 同步更新内存变量,保证运行期可立即生效 + global API_TOKEN + API_TOKEN = new_token + return True + except Exception as e: + # 发生异常时打印提示并返回失败 + print(f"写入令牌失败: {e}") return False \ No newline at end of file diff --git a/main.py b/main.py index f2fe9ae..6a994f5 100644 --- a/main.py +++ b/main.py @@ -25,6 +25,12 @@ class UpdateSignals(QObject): update_available = pyqtSignal(str, str) # (latest_version, download_url) +# 新增: 断开连接信号类(用于线程安全的断开提示) +class DisconnectSignals(QObject): + """断开连接信号""" + disconnected = pyqtSignal(str) # (disconnect_message) + + # 新增: 用户名密码输入对话框类 class LoginWindow(QMainWindow): def __init__(self): @@ -50,6 +56,10 @@ class LoginWindow(QMainWindow): self.update_signals = UpdateSignals() self.update_signals.update_available.connect(self._show_update_dialog) + # 创建断开连接信号对象(线程安全) + self.disconnect_signals = DisconnectSignals() + self.disconnect_signals.disconnected.connect(self._show_disconnect_dialog) + self.initUI() # 延迟设置版本检查器,确保WebSocket连接已建立 @@ -437,6 +447,54 @@ class LoginWindow(QMainWindow): except Exception as e: self.add_log(f"处理token错误失败: {e}", "ERROR") + def on_disconnect(self, disconnect_msg: str): + """处理被踢下线 - 发射信号到主线程(可在任何线程中调用)""" + try: + self.add_log(f"📡 收到断开通知,准备发射信号: {disconnect_msg}", "INFO") + # 发射信号(Qt 自动调度到主线程) + self.disconnect_signals.disconnected.emit(disconnect_msg) + self.add_log(f"✅ 断开信号已发射", "DEBUG") + except Exception as e: + self.add_log(f"❌ 发射断开信号失败: {e}", "ERROR") + import traceback + self.add_log(f"详细错误: {traceback.format_exc()}", "ERROR") + + def _show_disconnect_dialog(self, disconnect_msg: str): + """显示断开连接提示框(信号槽函数,始终在主线程中执行)""" + try: + self.add_log(f"🎯 主线程收到断开信号: {disconnect_msg}", "INFO") + + # 在状态标签显示警告信息 + self.status_label.setText(f"⚠️ 账号在其他设备登录") + self.status_label.setStyleSheet( + "color: #ff9800; background: rgba(255, 152, 0, 0.1); border-radius: 12px; padding: 5px 10px; font-weight: bold;") + + # 重置按钮状态 + self.login_btn.setEnabled(True) + self.login_btn.setText("重新连接") + self.login_btn.setObjectName("loginButton") # 恢复原始样式 + self.login_btn.setStyleSheet(self.login_btn.styleSheet()) # 刷新样式 + + # 清空已连接平台列表 + self.connected_platforms.clear() + + # 显示友好的提示对话框(主线程中执行,不会卡顿) + QMessageBox.warning( + self, + "连接已断开", + f"{disconnect_msg}\n\n" + "如果是您本人在其他设备登录,可以忽略此提示。\n" + "如需继续使用,请点击\"重新连接\"按钮。", + QMessageBox.Ok + ) + + self.add_log("连接已断开,您可以重新连接", "INFO") + + except Exception as e: + self.add_log(f"❌ 显示断开提示框失败: {e}", "ERROR") + import traceback + self.add_log(f"详细错误: {traceback.format_exc()}", "ERROR") + def delayed_platform_summary(self): """定时器触发的汇总显示更新""" try: @@ -651,7 +709,8 @@ class LoginWindow(QMainWindow): success=lambda: self.add_log("WebSocket连接管理器连接成功", "SUCCESS"), error=lambda error: self.add_log(f"WebSocket连接管理器错误: {error}", "ERROR"), platform_connected=self.on_platform_connected, # 新增:平台连接回调 - token_error=self.on_token_error # 新增:token错误回调 + token_error=self.on_token_error, # 新增:token错误回调 + disconnect=self.on_disconnect # 新增:被踢下线回调 ) # 连接后端