Todo: 集成多平台 解决PDD打包后js文件位置检索问题
Todo: 集成多平台 解决打包日志规划问题 Todo: 集成多平台 解决后端连接心跳与重连管理问题
This commit is contained in:
55
README.md
55
README.md
@@ -208,9 +208,62 @@ ws://<host>/ws/gui/<exe_token>/
|
|||||||
{ "type": "pong", "uuid": "connection_test_123" }
|
{ "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 下发策略
|
||||||
|
|
||||||
- 抖音、拼多多:直接使用后台请求携带的 cookie(pass-through)。
|
- 抖音、拼多多:直接使用后台请求携带的 cookie(pass-through)。
|
||||||
- 京东、淘宝:由后端插件生成或获取(plugin),后台在店铺登录时通过本 WS 下发店铺级 `connect_success`(带 `store_id`)给 GUI。
|
- 京东、淘宝:由后端插件生成或获取(plugin),后台在店铺登录时通过本 WS 下发店铺级 `connect_success`(带 `store_id`)给 GUI。
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
@Date :2025/7/17 16:44
|
@Date :2025/7/17 16:44
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
import threading
|
import threading
|
||||||
from concurrent.futures import ThreadPoolExecutor
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
import base64
|
import base64
|
||||||
@@ -1959,7 +1960,8 @@ class ChatPdd:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def check_js_files():
|
def check_js_files():
|
||||||
"""检查JS文件是否存在"""
|
"""检查JS文件是否存在"""
|
||||||
current_dir = "static/js"
|
# 使用资源路径解析函数
|
||||||
|
current_dir = ChatPdd._get_resource_path("static/js")
|
||||||
required_files = ["dencode_message.js", "encode_message.js"]
|
required_files = ["dencode_message.js", "encode_message.js"]
|
||||||
|
|
||||||
for file in required_files:
|
for file in required_files:
|
||||||
@@ -1969,14 +1971,64 @@ class ChatPdd:
|
|||||||
|
|
||||||
return True
|
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):
|
def __init__(self, cookie, chat_list_stat, csname=None, text=None, log_callback=None):
|
||||||
# 检查JS文件
|
# 检查JS文件
|
||||||
self.check_js_files()
|
self.check_js_files()
|
||||||
|
|
||||||
# 获取JS文件路径
|
# 获取JS文件路径 - 使用资源路径解析
|
||||||
current_dir = "static/js"
|
js_dir = self._get_resource_path("static/js")
|
||||||
dencode_js_path = os.path.join(current_dir, "dencode_message.js")
|
dencode_js_path = os.path.join(js_dir, "dencode_message.js")
|
||||||
encode_js_path = os.path.join(current_dir, "encode_message.js")
|
encode_js_path = os.path.join(js_dir, "encode_message.js")
|
||||||
|
|
||||||
# 读取JS文件
|
# 读取JS文件
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
# WebSocket/BackendClient.py
|
# WebSocket/BackendClient.py
|
||||||
import json
|
import json
|
||||||
import threading
|
import threading
|
||||||
|
import time
|
||||||
|
|
||||||
import websockets
|
import websockets
|
||||||
|
|
||||||
@@ -31,16 +32,31 @@ class BackendClient:
|
|||||||
|
|
||||||
self.is_connected = False
|
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):
|
def connect(self):
|
||||||
"""连接到WebSocket服务器"""
|
"""连接到WebSocket服务器"""
|
||||||
if self.is_connected:
|
if self.is_connected:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
self.should_stop = False
|
||||||
self.thread = threading.Thread(target=self._run_loop, daemon=True)
|
self.thread = threading.Thread(target=self._run_loop, daemon=True)
|
||||||
self.thread.start()
|
self.thread.start()
|
||||||
|
|
||||||
def disconnect(self):
|
def disconnect(self):
|
||||||
"""断开WebSocket连接"""
|
"""断开WebSocket连接"""
|
||||||
|
self.should_stop = True
|
||||||
|
|
||||||
if self.loop and self.loop.is_running():
|
if self.loop and self.loop.is_running():
|
||||||
asyncio.run_coroutine_threadsafe(self._close(), self.loop)
|
asyncio.run_coroutine_threadsafe(self._close(), self.loop)
|
||||||
|
|
||||||
@@ -68,12 +84,49 @@ class BackendClient:
|
|||||||
self.loop.close()
|
self.loop.close()
|
||||||
|
|
||||||
async def _connect_and_listen(self):
|
async def _connect_and_listen(self):
|
||||||
"""连接并监听消息"""
|
"""连接并监听消息 - 带重连机制"""
|
||||||
|
while not self.should_stop:
|
||||||
try:
|
try:
|
||||||
self.websocket = await websockets.connect(self.url)
|
print(f"正在连接后端WebSocket: {self.url}")
|
||||||
|
|
||||||
|
# 建立连接(可配置的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.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()
|
self.on_connected()
|
||||||
|
|
||||||
|
# 消息循环
|
||||||
async for message in self.websocket:
|
async for message in self.websocket:
|
||||||
try:
|
try:
|
||||||
# 打印原始文本帧与长度
|
# 打印原始文本帧与长度
|
||||||
@@ -87,9 +140,112 @@ class BackendClient:
|
|||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
print(f"JSON解析错误: {message}")
|
print(f"JSON解析错误: {message}")
|
||||||
|
|
||||||
|
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:
|
except Exception as e:
|
||||||
self.is_connected = False
|
self.is_connected = False
|
||||||
self.on_error(str(e))
|
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
|
@classmethod
|
||||||
def from_exe_token(cls, exe_token: str):
|
def from_exe_token(cls, exe_token: str):
|
||||||
@@ -126,9 +282,114 @@ class BackendClient:
|
|||||||
|
|
||||||
def on_connected(self):
|
def on_connected(self):
|
||||||
"""连接成功时的处理"""
|
"""连接成功时的处理"""
|
||||||
|
if self.reconnect_attempts > 0:
|
||||||
|
print(f"[重连] 后端WebSocket重连成功!(第{self.reconnect_attempts}次尝试)")
|
||||||
|
else:
|
||||||
print("后端WebSocket连接成功")
|
print("后端WebSocket连接成功")
|
||||||
|
|
||||||
|
# 重连成功后可选择上报状态给后端
|
||||||
|
if self.reconnect_attempts > 0:
|
||||||
|
self._report_reconnect_status()
|
||||||
|
|
||||||
# 不再主动请求 get_store,避免与后端不兼容导致协程未完成
|
# 不再主动请求 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]):
|
def on_message_received(self, message: Dict[str, Any]):
|
||||||
"""处理接收到的消息 - 根据WebSocket文档v2更新"""
|
"""处理接收到的消息 - 根据WebSocket文档v2更新"""
|
||||||
# 统一打印后端下发的完整消息结构体
|
# 统一打印后端下发的完整消息结构体
|
||||||
@@ -195,12 +456,21 @@ class BackendClient:
|
|||||||
message: 要发送的消息字典
|
message: 要发送的消息字典
|
||||||
"""
|
"""
|
||||||
if not self.is_connected or not self.loop:
|
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)
|
||||||
|
|
||||||
|
try:
|
||||||
future = asyncio.run_coroutine_threadsafe(
|
future = asyncio.run_coroutine_threadsafe(
|
||||||
self._send_to_backend(message), self.loop
|
self._send_to_backend(message), self.loop
|
||||||
)
|
)
|
||||||
return future.result(timeout=8)
|
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]):
|
async def _send_to_backend(self, message: Dict[str, Any]):
|
||||||
"""异步发送消息到后端"""
|
"""异步发送消息到后端"""
|
||||||
@@ -212,20 +482,18 @@ class BackendClient:
|
|||||||
await self.websocket.send(message_str)
|
await self.websocket.send(message_str)
|
||||||
print(f"发送消息到后端: {message}")
|
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 = {
|
ping_message = {
|
||||||
'type': 'ping',
|
'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)
|
return self.send_message(ping_message)
|
||||||
|
|
||||||
def get_store(self):
|
def get_store(self):
|
||||||
@@ -748,7 +1016,8 @@ class BackendClient:
|
|||||||
# 获取实际的抖音店铺ID(从cookie中获取)
|
# 获取实际的抖音店铺ID(从cookie中获取)
|
||||||
shop_id = cookie_dict.get('SHOP_ID', store_id) # 优先使用cookie中的SHOP_ID
|
shop_id = cookie_dict.get('SHOP_ID', store_id) # 优先使用cookie中的SHOP_ID
|
||||||
print(f"[DY Transfer] 使用shop_id: {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:
|
try:
|
||||||
@@ -767,10 +1036,12 @@ class BackendClient:
|
|||||||
break
|
break
|
||||||
|
|
||||||
if target_staff:
|
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:
|
else:
|
||||||
print(f"[DY Transfer] ⚠️ 未找到目标客服ID: {customer_service_id}")
|
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:
|
else:
|
||||||
print(f"[DY Transfer] ⚠️ 无法获取客服列表")
|
print(f"[DY Transfer] ⚠️ 无法获取客服列表")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -787,7 +1058,8 @@ class BackendClient:
|
|||||||
print(f"[DY Transfer] ✅ 转接成功: user_id={user_id} -> cs_id={customer_service_id}")
|
print(f"[DY Transfer] ✅ 转接成功: user_id={user_id} -> cs_id={customer_service_id}")
|
||||||
else:
|
else:
|
||||||
print(f"[DY Transfer] ❌ 转接失败: user_id={user_id}")
|
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:
|
else:
|
||||||
print(f"[DY Transfer] ⚠️ 抖音实例或message_handler不可用")
|
print(f"[DY Transfer] ⚠️ 抖音实例或message_handler不可用")
|
||||||
|
|
||||||
|
|||||||
18
config.py
18
config.py
@@ -8,10 +8,10 @@ import os # 用于路径与目录操作(写入用户配置目录)
|
|||||||
import json # 用于将令牌保存为 JSON 格式
|
import json # 用于将令牌保存为 JSON 格式
|
||||||
|
|
||||||
# 后端服务器配置
|
# 后端服务器配置
|
||||||
# BACKEND_HOST = "192.168.5.197"
|
# BACKEND_HOST = "192.168.5.233"
|
||||||
# BACKEND_HOST = "192.168.5.197"
|
# BACKEND_HOST = "192.168.5.12"
|
||||||
# BACKEND_HOST = "shuidrop.com"
|
BACKEND_HOST = "shuidrop.com"
|
||||||
BACKEND_HOST = "test.shuidrop.com"
|
# BACKEND_HOST = "test.shuidrop.com"
|
||||||
# BACKEND_PORT = "8000"
|
# BACKEND_PORT = "8000"
|
||||||
BACKEND_PORT = ""
|
BACKEND_PORT = ""
|
||||||
# BACKEND_WS_URL = f"ws://{BACKEND_HOST}:{BACKEND_PORT}"
|
# BACKEND_WS_URL = f"ws://{BACKEND_HOST}:{BACKEND_PORT}"
|
||||||
@@ -20,8 +20,14 @@ BACKEND_WS_URL = f"wss://{BACKEND_HOST}"
|
|||||||
# WebSocket配置
|
# WebSocket配置
|
||||||
WS_CONNECT_TIMEOUT = 16.0
|
WS_CONNECT_TIMEOUT = 16.0
|
||||||
WS_MESSAGE_TIMEOUT = 30.0
|
WS_MESSAGE_TIMEOUT = 30.0
|
||||||
WS_PING_INTERVAL = 20
|
WS_PING_INTERVAL = 10 # 10秒ping间隔(提高检测频率)
|
||||||
WS_PING_TIMEOUT = 10
|
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
|
MAX_PENDING_REPLIES = 100
|
||||||
|
|||||||
@@ -24,14 +24,14 @@ class FileLogger:
|
|||||||
|
|
||||||
# 创建日志文件
|
# 创建日志文件
|
||||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
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.lock = threading.Lock()
|
||||||
|
|
||||||
# 初始化日志文件
|
# 初始化日志文件
|
||||||
self.write_log("=" * 80)
|
self.write_log("=" * 80)
|
||||||
self.write_log("千牛EXE日志开始")
|
self.write_log("多平台客服GUI日志开始")
|
||||||
self.write_log(f"Python版本: {sys.version}")
|
self.write_log(f"Python版本: {sys.version}")
|
||||||
self.write_log(f"是否打包环境: {getattr(sys, 'frozen', False)}")
|
self.write_log(f"是否打包环境: {getattr(sys, 'frozen', False)}")
|
||||||
self.write_log(f"日志文件: {self.log_file}")
|
self.write_log(f"日志文件: {self.log_file}")
|
||||||
|
|||||||
8
main.py
8
main.py
@@ -11,12 +11,12 @@ import config
|
|||||||
from WebSocket.backend_singleton import get_websocket_manager
|
from WebSocket.backend_singleton import get_websocket_manager
|
||||||
from windows_taskbar_fix import setup_windows_taskbar_icon
|
from windows_taskbar_fix import setup_windows_taskbar_icon
|
||||||
import os
|
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() # 已禁用自动日志功能
|
setup_file_logging() # 生产环境启用自动日志功能
|
||||||
# print("文件日志系统已在main.py中初始化")
|
print("文件日志系统已在main.py中初始化")
|
||||||
|
|
||||||
|
|
||||||
# 新增: 用户名密码输入对话框类
|
# 新增: 用户名密码输入对话框类
|
||||||
|
|||||||
Reference in New Issue
Block a user