Todo: 集成多平台 解决PDD打包后js文件位置检索问题

Todo: 集成多平台 解决打包日志规划问题
Todo: 集成多平台 解决后端连接心跳与重连管理问题
This commit is contained in:
2025-09-20 16:13:23 +08:00
parent 7cfc0c22b7
commit 8ad02b4416
6 changed files with 447 additions and 64 deletions

View File

@@ -208,9 +208,62 @@ ws://<host>/ws/gui/<exe_token>/
{ "type": "pong", "uuid": "connection_test_123" }
```
### 5.8 GUI重连状态上报
```json
{
"type": "connect_message",
"store_id": "93a5c3d2-efe1-4ab5-ada3-d8c1d1212b31",
"status": true,
"content": "GUI重连成功京东平台状态正常"
}
```
说明GUI重连成功后会自动为每个活跃平台发送状态上报消息。
---
## 6. Cookie 下发策略
## 6. GUI重连机制
### 6.1 重连触发条件
- WebSocket连接关闭如ping timeout
- 网络连接异常
- 服务器重启或维护
- 消息发送失败
### 6.2 重连策略
- **最大重连次数**10次
- **退避策略**指数退避2秒 → 3秒 → 4.5秒 → ... → 最大60秒
- **心跳配置**可在config.py中配置
- `WS_ENABLE_PING = True`:是否启用心跳(默认启用)
- `WS_PING_INTERVAL = 30`心跳间隔30秒
- `WS_PING_TIMEOUT = 15`心跳超时15秒
### 6.3 心跳机制说明
**WebSocket ping/pong帧 vs JSON ping消息**
- **WebSocket ping帧**协议层面的心跳由websockets库自动处理
- **JSON ping消息**:应用层面的心跳,格式:`{"type":"ping","uuid":"ping_123"}`
**配置建议**
- **生产环境**:建议启用心跳(默认配置)
- **测试环境**:可设置`WS_ENABLE_PING = False`对比测试稳定性
### 6.4 重连过程中的消息处理
1. **重连期间**:新消息发送会抛出异常,提示"正在重连中..."
2. **重连成功**:自动上报各平台连接状态
3. **重连失败**达到最大次数后停止重连需要手动重启GUI
### 6.5 重连日志示例
```
[重连] 检测到ping超时这是常见的网络问题
[重连] 第1次重连尝试等待2.0秒...
正在连接后端WebSocket: wss://shuidrop.com/ws/gui/your_token/
[重连] 后端WebSocket重连成功(第1次尝试)
[重连] 已上报京东平台状态
[重连] 已上报抖音平台状态
```
---
## 7. Cookie 下发策略
- 抖音、拼多多:直接使用后台请求携带的 cookiepass-through
- 京东、淘宝由后端插件生成或获取plugin后台在店铺登录时通过本 WS 下发店铺级 `connect_success`(带 `store_id`)给 GUI。

View File

@@ -8,6 +8,7 @@
@Date 2025/7/17 16:44
"""
import os
import sys
import threading
from concurrent.futures import ThreadPoolExecutor
import base64
@@ -1959,7 +1960,8 @@ class ChatPdd:
@staticmethod
def check_js_files():
"""检查JS文件是否存在"""
current_dir = "static/js"
# 使用资源路径解析函数
current_dir = ChatPdd._get_resource_path("static/js")
required_files = ["dencode_message.js", "encode_message.js"]
for file in required_files:
@@ -1969,14 +1971,64 @@ class ChatPdd:
return True
@staticmethod
def _get_resource_path(relative_path):
"""获取资源文件的绝对路径兼容PyInstaller打包环境"""
try:
print(f"[DEBUG] 正在解析资源路径: {relative_path}")
# PyInstaller环境下的基础路径
if hasattr(sys, '_MEIPASS'):
# PyInstaller 临时目录
base_path = sys._MEIPASS
print(f"[DEBUG] 检测到PyInstaller环境_MEIPASS: {base_path}")
elif hasattr(sys, 'frozen') and sys.frozen:
# 其他打包环境
base_path = os.path.dirname(sys.executable)
print(f"[DEBUG] 检测到其他打包环境executable目录: {base_path}")
else:
# 开发环境
base_path = os.path.dirname(os.path.abspath(__file__))
# 向上两级目录到项目根目录
base_path = os.path.dirname(os.path.dirname(base_path))
print(f"[DEBUG] 开发环境,计算的项目根目录: {base_path}")
resource_path = os.path.join(base_path, relative_path)
print(f"[DEBUG] 拼接后的完整资源路径: {resource_path}")
# 检查路径是否存在
if os.path.exists(resource_path):
print(f"[DEBUG] ✅ 资源路径存在: {resource_path}")
else:
print(f"[DEBUG] ❌ 资源路径不存在: {resource_path}")
# 尝试列出基础路径的内容
print(f"[DEBUG] 基础路径 {base_path} 的内容:")
try:
for item in os.listdir(base_path):
item_path = os.path.join(base_path, item)
if os.path.isdir(item_path):
print(f"[DEBUG] 📁 {item}/")
else:
print(f"[DEBUG] 📄 {item}")
except Exception as e:
print(f"[DEBUG] 无法列出目录内容: {e}")
return resource_path
except Exception as e:
print(f"[ERROR] 获取资源路径失败: {e}")
import traceback
print(f"[ERROR] 堆栈跟踪: {traceback.format_exc()}")
# 降级处理:返回相对路径
return relative_path
def __init__(self, cookie, chat_list_stat, csname=None, text=None, log_callback=None):
# 检查JS文件
self.check_js_files()
# 获取JS文件路径
current_dir = "static/js"
dencode_js_path = os.path.join(current_dir, "dencode_message.js")
encode_js_path = os.path.join(current_dir, "encode_message.js")
# 获取JS文件路径 - 使用资源路径解析
js_dir = self._get_resource_path("static/js")
dencode_js_path = os.path.join(js_dir, "dencode_message.js")
encode_js_path = os.path.join(js_dir, "encode_message.js")
# 读取JS文件
try:

View File

@@ -1,6 +1,7 @@
# WebSocket/BackendClient.py
import json
import threading
import time
import websockets
@@ -31,16 +32,31 @@ class BackendClient:
self.is_connected = False
# 新增:重连机制相关属性
self.reconnect_attempts = 0
self.max_reconnect_attempts = 10
self.base_reconnect_delay = 2.0
self.max_reconnect_delay = 60.0
self.reconnect_backoff = 1.5
self.is_reconnecting = False
self.should_stop = False
self.websocket = None
self.loop = None
self.thread = None
def connect(self):
"""连接到WebSocket服务器"""
if self.is_connected:
return
self.should_stop = False
self.thread = threading.Thread(target=self._run_loop, daemon=True)
self.thread.start()
def disconnect(self):
"""断开WebSocket连接"""
self.should_stop = True
if self.loop and self.loop.is_running():
asyncio.run_coroutine_threadsafe(self._close(), self.loop)
@@ -68,28 +84,168 @@ class BackendClient:
self.loop.close()
async def _connect_and_listen(self):
"""连接并监听消息"""
try:
self.websocket = await websockets.connect(self.url)
self.is_connected = True
self.on_connected()
"""连接并监听消息 - 带重连机制"""
while not self.should_stop:
try:
print(f"正在连接后端WebSocket: {self.url}")
async for message in self.websocket:
try:
# 打印原始文本帧与长度
# 建立连接可配置的ping设置
from config import WS_PING_INTERVAL, WS_PING_TIMEOUT, WS_ENABLE_PING
if WS_ENABLE_PING:
self.websocket = await websockets.connect(
self.url,
ping_interval=WS_PING_INTERVAL, # 可配置ping间隔
ping_timeout=WS_PING_TIMEOUT, # 可配置ping超时
close_timeout=10, # 10秒关闭超时
# 增加TCP keepalive配置
max_size=2**20, # 1MB最大消息大小
max_queue=32, # 最大队列大小
compression=None # 禁用压缩以提高性能
)
print(f"[连接] 已启用心跳ping_interval={WS_PING_INTERVAL}s, ping_timeout={WS_PING_TIMEOUT}s")
else:
self.websocket = await websockets.connect(
self.url,
max_size=2**20,
max_queue=32,
compression=None
)
print("[连接] 已禁用心跳机制")
self.is_connected = True
self.reconnect_attempts = 0 # 重置重连计数
self.is_reconnecting = False
print("后端WebSocket连接成功")
# 等待连接稳定后再发送状态通知
await asyncio.sleep(0.5)
# 发送连接状态通知给后端
self._notify_connection_status(True)
self.on_connected()
# 消息循环
async for message in self.websocket:
try:
raw_len = len(message.encode('utf-8')) if isinstance(message, str) else len(message)
print(f"后端发送消息体内容:{message}")
except Exception:
pass
data = json.loads(message)
self.on_message_received(data)
except json.JSONDecodeError:
print(f"JSON解析错误: {message}")
# 打印原始文本帧与长度
try:
raw_len = len(message.encode('utf-8')) if isinstance(message, str) else len(message)
print(f"后端发送消息体内容:{message}")
except Exception:
pass
data = json.loads(message)
self.on_message_received(data)
except json.JSONDecodeError:
print(f"JSON解析错误: {message}")
except Exception as e:
self.is_connected = False
self.on_error(str(e))
except websockets.ConnectionClosed as e:
self.is_connected = False
self._notify_connection_status(False) # 通知断开
# 详细分析断开原因
if e.code == 1006:
print(f"[重连] WebSocket异常关闭 (1006): 可能是心跳超时或网络问题")
elif e.code == 1000:
print(f"[重连] WebSocket正常关闭 (1000): 服务端主动断开")
elif e.code == 1001:
print(f"[重连] WebSocket关闭 (1001): 端点离开")
else:
print(f"[重连] WebSocket关闭 ({e.code}): {e.reason}")
self._handle_connection_closed(e)
if not await self._should_reconnect():
break
await self._wait_before_reconnect()
except websockets.InvalidURI as e:
self.is_connected = False
print(f"无效的WebSocket URI: {e}")
self.on_error(f"无效的WebSocket URI: {e}")
break
except (websockets.WebSocketException, OSError, ConnectionError) as e:
self.is_connected = False
self._handle_network_error(e)
if not await self._should_reconnect():
break
await self._wait_before_reconnect()
except Exception as e:
self.is_connected = False
self._handle_general_error(e)
if not await self._should_reconnect():
break
await self._wait_before_reconnect()
def _handle_connection_closed(self, error):
"""处理连接关闭"""
error_msg = f"WebSocket连接已关闭: {error.code} {error.reason if hasattr(error, 'reason') else ''}"
print(f"[重连] {error_msg}")
# 特殊处理ping超时等情况
if hasattr(error, 'code'):
if error.code == 1011: # Internal error (ping timeout)
print("[重连] 检测到ping超时这是常见的网络问题")
elif error.code == 1006: # Abnormal closure
print("[重连] 检测到异常关闭,可能是网络中断")
if not self.is_reconnecting:
self.on_error(error_msg)
def _handle_network_error(self, error):
"""处理网络错误"""
error_msg = f"网络连接错误: {type(error).__name__} - {str(error)}"
print(f"[重连] {error_msg}")
if not self.is_reconnecting:
self.on_error(error_msg)
def _handle_general_error(self, error):
"""处理一般错误"""
error_msg = f"WebSocket连接异常: {type(error).__name__} - {str(error)}"
print(f"[重连] {error_msg}")
if not self.is_reconnecting:
self.on_error(error_msg)
async def _should_reconnect(self) -> bool:
"""判断是否应该重连"""
if self.should_stop:
print("[重连] 程序正在关闭,停止重连")
return False
if self.reconnect_attempts >= self.max_reconnect_attempts:
print(f"[重连] 已达到最大重连次数({self.max_reconnect_attempts}),停止重连")
# 通知上层重连失败
if not self.is_reconnecting:
self.on_error(f"重连失败:已达到最大重连次数({self.max_reconnect_attempts})")
return False
return True
async def _wait_before_reconnect(self):
"""重连前等待(指数退避)"""
delay = min(
self.base_reconnect_delay * (self.reconnect_backoff ** self.reconnect_attempts),
self.max_reconnect_delay
)
self.reconnect_attempts += 1
self.is_reconnecting = True
print(f"[重连] 第{self.reconnect_attempts}次重连尝试,等待{delay:.1f}秒...")
# 分割等待时间,支持快速退出
wait_steps = max(1, int(delay))
for i in range(wait_steps):
if self.should_stop:
return
await asyncio.sleep(1)
# 处理小数部分
remaining = delay - wait_steps
if remaining > 0 and not self.should_stop:
await asyncio.sleep(remaining)
@classmethod
def from_exe_token(cls, exe_token: str):
@@ -126,9 +282,114 @@ class BackendClient:
def on_connected(self):
"""连接成功时的处理"""
print("后端WebSocket连接成功")
if self.reconnect_attempts > 0:
print(f"[重连] 后端WebSocket重连成功(第{self.reconnect_attempts}次尝试)")
else:
print("后端WebSocket连接成功")
# 重连成功后可选择上报状态给后端
if self.reconnect_attempts > 0:
self._report_reconnect_status()
# 不再主动请求 get_store避免与后端不兼容导致协程未完成
def _report_reconnect_status(self):
"""重连成功后上报当前状态(可选)"""
try:
# 获取当前已连接的平台列表
from WebSocket.backend_singleton import get_websocket_manager
manager = get_websocket_manager()
if hasattr(manager, 'platform_listeners') and manager.platform_listeners:
for platform_key, listener_info in manager.platform_listeners.items():
store_id = listener_info.get('store_id')
platform = listener_info.get('platform')
if store_id and platform:
# 上报平台仍在连接状态
try:
reconnect_message = {
"type": "connect_message",
"store_id": store_id,
"status": True,
"content": f"GUI重连成功{platform}平台状态正常"
}
# 异步发送,不阻塞连接过程
asyncio.run_coroutine_threadsafe(
self._send_to_backend(reconnect_message),
self.loop
)
print(f"[重连] 已上报{platform}平台状态")
except Exception as e:
print(f"[重连] 上报{platform}平台状态失败: {e}")
else:
print("[重连] 当前无活跃平台连接,跳过状态上报")
except Exception as e:
print(f"[重连] 状态上报过程异常: {e}")
def _notify_connection_status(self, connected: bool):
"""通知后端连接状态变化"""
try:
if not self.loop:
return
# 获取当前活跃平台的store_id
active_store_id = None
try:
from Utils.JD.JdUtils import WebsocketManager as JdManager
from Utils.Dy.DyUtils import WebsocketManager as DyManager
from Utils.Pdd.PddUtils import WebsocketManager as PddManager
# 检查各平台是否有活跃连接
for mgr_class, platform_name in [(JdManager, "京东"), (DyManager, "抖音"), (PddManager, "拼多多")]:
try:
mgr = mgr_class()
if hasattr(mgr, '_store') and mgr._store:
for shop_key, entry in mgr._store.items():
if entry and entry.get('platform'):
# 从shop_key中提取store_id格式平台:store_id
if ':' in shop_key:
_, store_id = shop_key.split(':', 1)
active_store_id = store_id
print(f"[状态] 检测到活跃{platform_name}平台: {store_id}")
break
if active_store_id:
break
except Exception as e:
print(f"[状态] 检查{platform_name}平台失败: {e}")
continue
except Exception as e:
print(f"[状态] 获取活跃平台信息失败: {e}")
status_message = {
"type": "connection_status",
"status": connected,
"timestamp": int(time.time()),
"client_uuid": self.uuid
}
# 如果有活跃平台添加store_id
if active_store_id:
status_message["store_id"] = active_store_id
print(f"[状态] 添加store_id到状态消息: {active_store_id}")
else:
print(f"[状态] 未检测到活跃平台不添加store_id")
# 异步发送状态通知
asyncio.run_coroutine_threadsafe(
self._send_to_backend(status_message),
self.loop
)
status_text = "连接" if connected else "断开"
print(f"[状态] 已通知后端GUI客户端{status_text}")
except Exception as e:
print(f"[状态] 发送状态通知失败: {e}")
import traceback
print(f"[状态] 详细错误: {traceback.format_exc()}")
def on_message_received(self, message: Dict[str, Any]):
"""处理接收到的消息 - 根据WebSocket文档v2更新"""
# 统一打印后端下发的完整消息结构体
@@ -195,12 +456,21 @@ class BackendClient:
message: 要发送的消息字典
"""
if not self.is_connected or not self.loop:
raise Exception("WebSocket未连接")
error_msg = "WebSocket未连接"
if self.is_reconnecting:
error_msg += "(正在重连中..."
raise Exception(error_msg)
future = asyncio.run_coroutine_threadsafe(
self._send_to_backend(message), self.loop
)
return future.result(timeout=8)
try:
future = asyncio.run_coroutine_threadsafe(
self._send_to_backend(message), self.loop
)
return future.result(timeout=8)
except Exception as e:
# 发送失败时检查连接状态
if not self.is_connected:
print(f"[重连] 消息发送失败,连接已断开: {e}")
raise e
async def _send_to_backend(self, message: Dict[str, Any]):
"""异步发送消息到后端"""
@@ -212,20 +482,18 @@ class BackendClient:
await self.websocket.send(message_str)
print(f"发送消息到后端: {message}")
def send_ping(self, custom_uuid: str = None, custom_token: str = None):
def send_ping(self, custom_uuid: str = None):
"""
发送心跳包
如果接收到关闭的消息后心跳包要带上token
"""
# 生成简单的ping UUID
ping_uuid = custom_uuid or f"ping_{int(time.time())}"
ping_message = {
'type': 'ping',
'uuid': custom_uuid or self.uuid
'uuid': ping_uuid
}
token = custom_token or self.token
if token:
ping_message['token'] = token
return self.send_message(ping_message)
def get_store(self):
@@ -748,7 +1016,8 @@ class BackendClient:
# 获取实际的抖音店铺ID从cookie中获取
shop_id = cookie_dict.get('SHOP_ID', store_id) # 优先使用cookie中的SHOP_ID
print(f"[DY Transfer] 使用shop_id: {shop_id}")
print(f"[DY Transfer] 转接参数: receiver_id={user_id}, shop_id={shop_id}, staff_id={customer_service_id}")
print(
f"[DY Transfer] 转接参数: receiver_id={user_id}, shop_id={shop_id}, staff_id={customer_service_id}")
# 检查是否是自己转给自己的情况
try:
@@ -767,10 +1036,12 @@ class BackendClient:
break
if target_staff:
print(f"[DY Transfer] 找到目标客服: {target_staff.get('staffName', 'Unknown')} (ID: {customer_service_id})")
print(
f"[DY Transfer] 找到目标客服: {target_staff.get('staffName', 'Unknown')} (ID: {customer_service_id})")
else:
print(f"[DY Transfer] ⚠️ 未找到目标客服ID: {customer_service_id}")
print(f"[DY Transfer] 可用客服列表: {[{'id': s.get('staffId'), 'name': s.get('staffName')} for s in staff_list]}")
print(
f"[DY Transfer] 可用客服列表: {[{'id': s.get('staffId'), 'name': s.get('staffName')} for s in staff_list]}")
else:
print(f"[DY Transfer] ⚠️ 无法获取客服列表")
except Exception as e:
@@ -787,7 +1058,8 @@ class BackendClient:
print(f"[DY Transfer] ✅ 转接成功: user_id={user_id} -> cs_id={customer_service_id}")
else:
print(f"[DY Transfer] ❌ 转接失败: user_id={user_id}")
print(f"[DY Transfer] 💡 可能原因1) 只有一个客服无法转接 2) 客服ID不存在 3) 权限不足 4) 会话状态不允许转接")
print(
f"[DY Transfer] 💡 可能原因1) 只有一个客服无法转接 2) 客服ID不存在 3) 权限不足 4) 会话状态不允许转接")
else:
print(f"[DY Transfer] ⚠️ 抖音实例或message_handler不可用")

View File

@@ -8,10 +8,10 @@ import os # 用于路径与目录操作(写入用户配置目录)
import json # 用于将令牌保存为 JSON 格式
# 后端服务器配置
# BACKEND_HOST = "192.168.5.197"
# BACKEND_HOST = "192.168.5.197"
# BACKEND_HOST = "shuidrop.com"
BACKEND_HOST = "test.shuidrop.com"
# 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}"
@@ -20,8 +20,14 @@ BACKEND_WS_URL = f"wss://{BACKEND_HOST}"
# WebSocket配置
WS_CONNECT_TIMEOUT = 16.0
WS_MESSAGE_TIMEOUT = 30.0
WS_PING_INTERVAL = 20
WS_PING_TIMEOUT = 10
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

View File

@@ -24,14 +24,14 @@ class FileLogger:
# 创建日志文件
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
self.log_file = os.path.join(self.log_dir, f"qianniu_exe_{timestamp}.log")
self.log_file = os.path.join(self.log_dir, f"MultiPlatformGUI_{timestamp}.log")
# 线程锁,确保多线程写入安全
self.lock = threading.Lock()
# 初始化日志文件
self.write_log("=" * 80)
self.write_log("千牛EXE日志开始")
self.write_log("多平台客服GUI日志开始")
self.write_log(f"Python版本: {sys.version}")
self.write_log(f"是否打包环境: {getattr(sys, 'frozen', False)}")
self.write_log(f"日志文件: {self.log_file}")

View File

@@ -11,12 +11,12 @@ 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
from exe_file_logger import setup_file_logging, log_to_file
# setup_file_logging() # 已禁用自动日志功能
# print("文件日志系统已在main.py中初始化")
setup_file_logging() # 生产环境启用自动日志功能
print("文件日志系统已在main.py中初始化")
# 新增: 用户名密码输入对话框类