拼多多集成

This commit is contained in:
jjz
2025-09-12 20:42:00 +08:00
commit e9361b4c87
25 changed files with 22234 additions and 0 deletions

13
.gitignore vendored Normal file
View File

@@ -0,0 +1,13 @@
# 排除虚拟环境目录
.venv/
# 排除test.py文件
test.py
# 排除test1.py文件
test1.py
# 排除.idea文件
.idea
Utils/PythonNew32/

BIN
.projectroot Normal file

Binary file not shown.

217
README.md Normal file
View File

@@ -0,0 +1,217 @@
# GUI 单连接多店铺 WebSocket 通信协议
---
## 1. 连接地址与认证
```
ws://<host>/ws/gui/<exe_token>/
```
- `exe_token`:用户侧令牌,用于鉴权。连接成功后,后端会立即下发一次连接成功消息(不包含店铺 ID
### 1.1 连接成功(初次,仅鉴权)
```json
{
"type": "success",
"content": "connected"
}
```
说明:该消息仅表示“用户级连接成功”,不包含店铺上下文。后续业务消息按店铺维度进行(见下)。
---
## 2. 总体约定(多店铺)
- 单条 WS 连接可处理 N 个店铺消息。除心跳 `ping`GUI → 后端的业务消息必须携带 `store_id`UUID
- 后端在收到带有 `store_id` 的首条消息时,会按需初始化该店铺上下文并进行处理、转发与存储。
- 当后台需要为某店铺下发平台连接所需 cookie 时,会通过该用户的这条 WS 连接发送携带 `store_id``connect_success`
---
## 3. 字段规范
### 3.1 GUI → 后端(必填)
| 字段 | 类型 | 说明 |
|---------------|---------|----------------------------------------------------------|
| `type` | string | "message" \| "staff_list" \| "ping" \| "transfer" 等 |
| `content` | string | 消息内容(文本/链接/卡片文本等)。`ping` 无需此字段。 |
| `sender.id` | string | 平台用户 pin。`ping` 无需此字段。 |
| `store_id` | string | 店铺 UUID。`ping` 无需此字段。 |
| `msg_type` | string | 消息类型text/image/video/product_card/order_card。 |
说明:
- 文本/卡片可自动识别,但建议显式传 `msg_type`,图片与视频必须传。
- 可选字段:`pin_image`(头像 URL`timestamp`(毫秒),`message_id` 等。
### 3.2 后端 → GUI通用
| 字段 | 类型 | 说明 |
|---------------|---------|---------------------------------------------------------------------------|
| `type` | string | 消息类型connect_success/message/transfer/error/pong 等 |
| `content` | string | 具体内容cookie 文本、AI 回复、错误说明等 |
| `msg_type` | string | 当 `type=message` 时需要,如 text/image/video/product_card/order_card |
| `receiver.id` | string | 当后端主动发消息/AI 回复/转接时指定接收用户 pin |
| `store_id` | string | 关联店铺 UUID初次连接成功不带店铺级 cookie 下发时必须携带) |
---
## 4. GUI → 后端:消息格式示例
### 4.1 文本消息
```json
{
"type": "message",
"content": "你好,请问在吗?",
"msg_type": "text",
"sender": { "id": "jd_user_001" },
"store_id": "93a5c3d2-efe1-4ab5-ada3-d8c1d1212b31"
}
```
### 4.2 图片消息
```json
{
"type": "message",
"content": "https://example.com/a.jpg",
"msg_type": "image",
"sender": { "id": "jd_user_001" },
"store_id": "93a5c3d2-efe1-4ab5-ada3-d8c1d1212b31"
}
```
### 4.3 视频消息
```json
{
"type": "message",
"content": "https://example.com/a.mp4",
"msg_type": "video",
"sender": { "id": "jd_user_001" },
"store_id": "93a5c3d2-efe1-4ab5-ada3-d8c1d1212b31"
}
```
### 4.4 商品卡片(链接)
```json
{
"type": "message",
"content": "https://item.jd.com/100123456789.html",
"msg_type": "product_card",
"sender": { "id": "jd_user_001" },
"store_id": "93a5c3d2-efe1-4ab5-ada3-d8c1d1212b31"
}
```
### 4.5 订单卡片
```json
{
"type": "message",
"content": "商品id100123456789 订单号250904-518080458310434",
"msg_type": "order_card",
"sender": { "id": "jd_user_001" },
"store_id": "93a5c3d2-efe1-4ab5-ada3-d8c1d1212b31"
}
```
### 4.6 客服列表staff_list
```json
{
"type": "staff_list",
"content": "客服列表更新",
"data": {
"staff_list": [
{ "staff_id": "7545667615256944155", "name": "售后客服01", "status": 1, "department": "", "online": true },
{ "staff_id": "1216524360102748", "name": "水滴智能优品", "status": 1, "department": "", "online": true }
]
},
"store_id": "93a5c3d2-efe1-4ab5-ada3-d8c1d1212b31"
}
```
### 4.7 心跳ping
```json
{ "type": "ping", "uuid": "connection_test_123" }
```
---
## 5. 后端 → GUI消息格式示例
### 5.1 后台点击登录后下发cookies给gui进行连接平台
```json
{
"type": "connect_success",
"content": "<cookie_string>",
"store_id": "93a5c3d2-efe1-4ab5-ada3-d8c1d1212b31",
"platform_name": "京东"
}
```
说明当后台触发店铺登录后由后端推送GUI 收到后使用该 cookie 连接对应平台。
### 5.2 错误消息
```json
{
"type": "error",
"content": "图形验证码校验失败,请刷新重试!",
"receiver": { "id": "gui_12345678" },
"data": { "verify_link": "https://safe.jd.com/verify.html?xxx" },
"store_id": "93a5c3d2-efe1-4ab5-ada3-d8c1d1212b31"
}
```
### 5.3 AI 回复
```json
{
"type": "message",
"content": "您好!我是智能客服,很高兴为您服务。",
"msg_type": "text",
"receiver": { "id": "jd_user_001" },
"store_id": "93a5c3d2-efe1-4ab5-ada3-d8c1d1212b31"
}
```
### 5.4 后台客服消息转发
```json
{
"type": "message",
"content": "好的,我来为您查询订单状态",
"msg_type": "text",
"receiver": { "id": "jd_user_001" },
"store_id": "93a5c3d2-efe1-4ab5-ada3-d8c1d1212b31"
}
```
### 5.5 客服转接
```json
{
"type": "transfer",
"content": "客服小王",
"receiver": { "id": "jd_user_001" },
"store_id": "93a5c3d2-efe1-4ab5-ada3-d8c1d1212b31"
}
```
### 5.6 自动催单(示例)
```json
{
"type": "message",
"content": "亲,您的订单还在等待您的确认哦~如有任何疑问请及时联系我们!",
"msg_type": "text",
"receiver": { "id": "jd_user_001" },
"store_id": "93a5c3d2-efe1-4ab5-ada3-d8c1d1212b31"
}
```
### 5.7 心跳回复pong
```json
{ "type": "pong", "uuid": "connection_test_123" }
```
---
## 6. Cookie 下发策略
- 抖音、拼多多:直接使用后台请求携带的 cookiepass-through
- 京东、淘宝由后端插件生成或获取plugin后台在店铺登录时通过本 WS 下发店铺级 `connect_success`(带 `store_id`)给 GUI。

1946
Utils/Dy/DyUtils.py Normal file

File diff suppressed because it is too large Load Diff

0
Utils/Dy/__init__.py Normal file
View File

375
Utils/Dy/message_arg.py Normal file
View File

@@ -0,0 +1,375 @@
# -*- coding: utf-8 -*-
# python let's go
# 编辑人:kris思成
# coding=utf-8
import time
import uuid
import json
# 发送消息
def send_message(pigeon_sign: str, token: str, receiver_id: str, shop_id: str, talk_id: int, session_did: str, p_id: int, user_code: str, text: str):
"""
构造发送消息消息体
:param pigeon_sign: 接口返回
:param token: 接口返回
:param receiver_id: wss消息返回 对方用户id
:param shop_id: cookie自带
:param talk_id: wss消息返回 激活窗口id
:param session_did: cookie自带
:param p_id: wss消息返回
:param user_code: 用户token
:param text: 文本内容
:return:
"""
value = {
'1': 11778,
'2': int(time.time() * 1000),
'3': 10001,
'4': 1,
'5': [
{'1': b'pigeon_source', '2': b'web'},
{'1': b'PIGEON_BIZ_TYPE', '2': b'2'},
{'1': b'pigeon_sign', '2': pigeon_sign.encode()},
],
'7': {'14': 98},
'8': {
'1': 100,
'2': 11778,
'3': b'1.0.4-beta.2',
'4': token.encode(),
'5': 3,
'6': 3,
'7': b'2d97ea6:feat/add_init_callback',
'8': {
'100': {
'1': f"{receiver_id}:{shop_id}::2:1:pigeon".encode(),
'2': 11,
'3': p_id,
'4': text.encode(),
'5': [
{'1': b'type', '2': b'text'},
{'1': b'shop_id', '2': shop_id.encode()},
{'1': b'sender_role', '2': b'2'},
{'1': b'PIGEON_BIZ_TYPE', '2': b'2'},
{'1': b'src', '2': b'pc'},
{'1': b'srcType', '2': b'1'},
{'1': b'source', '2': b'pc-web'},
{'1': b'receiver_id', '2': str(receiver_id).encode()},
{'1': b'hierarchical_dimension', '2': b'{"dynamic_dimension":"4541_1131_9042_6599_9420_6832_4050_3823_3994_8564_1528_0388_8667_2179_7948_1870_1949_0989_8012_6240_7898_7548_8852_6245_9393_3650_8570_4026_4034_4057_6537_8632_2068_8958_0363_2387_9033_3425_2238_0982_1935_8188_3817_8557_7931_3278_4065_1893_6049_6961_3814_4883_4401_6637_7282_3652_9354_0437_4769_4815_9572_7230_5054_3951_4852_2188_3505_6813_2570_5394_0729","goofy_id":"1.0.1.1508","desk_version":"0.0.0","open_stores":"0","memL":"","cpuL":"","session_throughput":0,"message_throughput_send":0,"message_throughput_revice":0}'},
{'1': b'p:check_Send', '2': str(uuid.uuid4()).encode()},
{'1': b'track_info','2': json.dumps({"send_time": int(time.time() * 1000), "_send_delta": "77","_send_delta_2": "216"}).encode()},
{'1': b'user_agent', '2': b'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36'},
{'1': b'sender_id', '2': b''},
{'1': b'biz_ext', '2': b'{}'},
{'1': b'p:from_source', '2': b'web'},
{'1': b's:mentioned_users', '2': b''},
{'1': b's:client_message_id', '2': str(uuid.uuid4()).encode()}
],
'6': 1000,
'7': user_code.encode(),
'8': str(uuid.uuid4()).encode(),
'14': talk_id # 激活聊天窗口id
}
},
'9': session_did.encode(),
'11': b'web',
'15': [
{'1': b'pigeon_source', '2': b'web'},
{'1': b'PIGEON_BIZ_TYPE', '2': b'2'},
{'1': b'pigeon_sign', '2': b'MIG6BAz2BNUON43WdlOBuGYEgZcsIho9ZjVP4yyExLShzXgAZtsvUMj2e3jZWeMZv+6+TNVQQMq3xSLrqiwcs2cCaOVBDuS6zGsWm5gBlGtlvOOLM5td2/9OS8P37t1sdkjN4BSH2mB7FlGItioZIsTh1sodn6pYCGj+45mtId3Itenufgai3Mnkpt573uoWJmagF8J3jVPHMFtdwd25Qf5vsWC2kB30glpQBBCbk2VO2ubMqctqQSzhI6uD'},
{'1': b'session_aid', '2': b'1383'},
{'1': b'session_did', '2': session_did.encode()},
{'1': b'app_name', '2': b'im'},
{'1': b'priority_region', '2': b'cn'},
{'1': b'user_agent','2': b'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36'},
{'1': b'cookie_enabled', '2': b'true'},
{'1': b'browser_language', '2': b'zh-CN'},
{'1': b'browser_platform', '2': b'Win32'},
{'1': b'browser_name', '2': b'Mozilla'},
{'1': b'browser_version', '2': b'5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36'},
{'1': b'browser_online', '2': b'true'},
{'1': b'screen_width', '2': b'1707'},
{'1': b'screen_height', '2': b'1067'},
{'1': b'referer', '2': b''},
{'1': b'timezone_name', '2': b'Asia/Shanghai'}
],
'18': 2}
}
message_type = {
"1": {
"type": "int",
"name": ""
},
"2": {
"type": "int",
"name": ""
},
"3": {
"type": "int",
"name": ""
},
"4": {
"type": "int",
"name": ""
},
"5": {
"type": "message",
"message_typedef": {
"1": {
"type": "bytes",
"name": ""
},
"2": {
"type": "bytes",
"name": ""
}
},
"name": ""
},
"7": {
"type": "message",
"message_typedef": {
"14": {
"type": "int",
"name": ""
}
},
"name": ""
},
"8": {
"type": "message",
"message_typedef": {
"1": {
"type": "int",
"name": ""
},
"2": {
"type": "int",
"name": ""
},
"3": {
"type": "bytes",
"name": ""
},
"4": {
"type": "bytes",
"name": ""
},
"5": {
"type": "int",
"name": ""
},
"6": {
"type": "int",
"name": ""
},
"7": {
"type": "bytes",
"name": ""
},
"8": {
"type": "message",
"message_typedef": {
"100": {
"type": "message",
"message_typedef": {
"1": {
"type": "bytes",
"name": ""
},
"2": {
"type": "int",
"name": ""
},
"3": {
"type": "int",
"name": ""
},
"4": {
"type": "bytes",
"name": ""
},
"5": {
"type": "message",
"message_typedef": {
"1": {
"type": "bytes",
"name": ""
},
"2": {
"type": "bytes",
"name": ""
}
},
"name": ""
},
"6": {
"type": "int",
"name": ""
},
"7": {
"type": "bytes",
"name": ""
},
"8": {
"type": "bytes",
"name": ""
},
"14": {
"type": "int",
"name": ""
}
},
"name": ""
}
},
"name": ""
},
"9": {
"type": "bytes",
"name": ""
},
"11": {
"type": "bytes",
"name": ""
},
"15": {
"type": "message",
"message_typedef": {
"1": {
"type": "bytes",
"name": ""
},
"2": {
"type": "bytes",
"name": ""
}
},
"name": ""
},
"18": {
"type": "int",
"name": ""
}
},
"name": ""
}
}
return value, message_type
# 获取token
def get_user_code(pigeon_sign: str, token: str, receiver_id: str, shop_id: str, session_did: str, p_id: int):
value = {'1': 10109, '2': int(time.time() * 1000), '3': 10001, '4': 1,
'5': [{'1': b'pigeon_source', '2': b'web'}, {'1': b'PIGEON_BIZ_TYPE', '2': b'2'}, {'1': b'pigeon_sign',
'2': pigeon_sign.encode()}],
'7': {'14': 98}, '8': {'1': 610, '2': 10109, '3': b'1.0.4-beta.2',
'4': token.encode(), '5': 3, '6': 3,
'7': b'2d97ea6:feat/add_init_callback', '8': {
'610': {'1': {'1': f"{receiver_id}:{shop_id}::2:1:pigeon".encode(), '2': p_id, '3': 10}}},
'9': session_did.encode(), '11': b'web',
'15': [{'1': b'pigeon_source', '2': b'web'}, {'1': b'PIGEON_BIZ_TYPE', '2': b'2'},
{'1': b'pigeon_sign',
'2': pigeon_sign.encode()},
{'1': b'session_aid', '2': b'1383'},
{'1': b'session_did', '2': session_did.encode()},
{'1': b'app_name', '2': b'im'}, {'1': b'priority_region', '2': b'cn'},
{'1': b'user_agent',
'2': b'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36'},
{'1': b'cookie_enabled', '2': b'true'},
{'1': b'browser_language', '2': b'zh-CN'},
{'1': b'browser_platform', '2': b'Win32'},
{'1': b'browser_name', '2': b'Mozilla'}, {'1': b'browser_version',
'2': b'5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36'},
{'1': b'browser_online', '2': b'true'}, {'1': b'screen_width', '2': b'1707'},
{'1': b'screen_height', '2': b'1067'}, {'1': b'referer', '2': b''},
{'1': b'timezone_name', '2': b'Asia/Shanghai'}], '18': 2}}
message_type = {'1': {'type': 'int', 'name': ''}, '2': {'type': 'int', 'name': ''},
'3': {'type': 'int', 'name': ''}, '4': {'type': 'int', 'name': ''}, '5': {'type': 'message',
'message_typedef': {
'1': {'type': 'bytes',
'name': ''},
'2': {'type': 'bytes',
'name': ''}},
'name': ''},
'7': {'type': 'message', 'message_typedef': {'14': {'type': 'int', 'name': ''}}, 'name': ''},
'8': {'type': 'message',
'message_typedef': {'1': {'type': 'int', 'name': ''}, '2': {'type': 'int', 'name': ''},
'3': {'type': 'bytes', 'name': ''}, '4': {'type': 'bytes', 'name': ''},
'5': {'type': 'int', 'name': ''}, '6': {'type': 'int', 'name': ''},
'7': {'type': 'bytes', 'name': ''}, '8': {'type': 'message',
'message_typedef': {
'610': {'type': 'message',
'message_typedef': {
'1': {
'type': 'message',
'message_typedef': {
'1': {
'type': 'bytes',
'name': ''},
'2': {
'type': 'int',
'name': ''},
'3': {
'type': 'int',
'name': ''}},
'name': ''}},
'name': ''}},
'name': ''},
'9': {'type': 'bytes', 'name': ''}, '11': {'type': 'bytes', 'name': ''},
'15': {'type': 'message',
'message_typedef': {'1': {'type': 'bytes', 'name': ''},
'2': {'type': 'bytes', 'name': ''}},
'name': ''}, '18': {'type': 'int', 'name': ''}}, 'name': ''}}
return value, message_type
# 心跳包
def heartbeat_message(pigeon_sign: str, token: str, session_did: str):
value = {
'1': 11028,
'2': int(time.time() * 1000),
'3': 10001,
'4': 1,
'5': [
{'1': b'pigeon_source', '2': b'web'},
{'1': b'PIGEON_BIZ_TYPE', '2': b'2'},
{'1': b'pigeon_sign','2': pigeon_sign.encode()}
],
'7': {'14': 98},
'8': {
'1': 200,
'2': 11028,
'3': b'1.0.4-beta.2',
'4': token.encode(),
'5': 3,
'6': 3,
'7': b'2d97ea6:feat/add_init_callback',
'8': {'200': {'1': int(time.time() * 1000), '2': 50}},
'9': session_did.encode(),
'11': b'web',
'15': [
{'1': b'pigeon_source', '2': b'web'},
{'1': b'PIGEON_BIZ_TYPE', '2': b'2'},
{'1': b'pigeon_sign', '2': pigeon_sign.encode()},
{'1': b'session_aid', '2': b'1383'},
{'1': b'session_did', '2': session_did.encode()},
{'1': b'app_name', '2': b'im'},
{'1': b'priority_region', '2': b'cn'},
{'1': b'user_agent', '2': b'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36'},
{'1': b'cookie_enabled', '2': b'true'},
{'1': b'browser_language', '2': b'zh-CN'},
{'1': b'browser_platform', '2': b'Win32'}, {'1': b'browser_name', '2': b'Mozilla'},
{'1': b'browser_version', '2': b'5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36'},
{'1': b'browser_online', '2': b'true'}, {'1': b'screen_width', '2': b'1707'},
{'1': b'screen_height', '2': b'1067'}, {'1': b'referer', '2': b''},
{'1': b'timezone_name', '2': b'Asia/Shanghai'}
],
'18': 2
}
}
message_type = {'1': {'type': 'int', 'name': ''}, '2': {'type': 'int', 'name': ''}, '3': {'type': 'int', 'name': ''}, '4': {'type': 'int', 'name': ''}, '5': {'type': 'message', 'message_typedef': {'1': {'type': 'bytes', 'name': ''}, '2': {'type': 'bytes', 'name': ''}}, 'name': ''}, '7': {'type': 'message', 'message_typedef': {'14': {'type': 'int', 'name': ''}}, 'name': ''}, '8': {'type': 'message', 'message_typedef': {'1': {'type': 'int', 'name': ''}, '2': {'type': 'int', 'name': ''}, '3': {'type': 'bytes', 'name': ''}, '4': {'type': 'bytes', 'name': ''}, '5': {'type': 'int', 'name': ''}, '6': {'type': 'int', 'name': ''}, '7': {'type': 'bytes', 'name': ''}, '8': {'type': 'message', 'message_typedef': {'200': {'type': 'message', 'message_typedef': {'1': {'type': 'int', 'name': ''}, '2': {'type': 'int', 'name': ''}}, 'name': ''}}, 'name': ''}, '9': {'type': 'bytes', 'name': ''}, '11': {'type': 'bytes', 'name': ''}, '15': {'type': 'message', 'message_typedef': {'1': {'type': 'bytes', 'name': ''}, '2': {'type': 'bytes', 'name': ''}}, 'name': ''}, '18': {'type': 'int', 'name': ''}}, 'name': ''}}
return value, message_type

981
Utils/JD/JdUtils.py Normal file
View File

@@ -0,0 +1,981 @@
# -*- coding: utf-8 -*-
# python let's go
# 编辑人:kris思成
import asyncio
import hashlib
import traceback
from datetime import datetime
import aiohttp
import jsonpath
import websockets
from loguru import logger
import requests
import json
import time
import threading
# 定义持久化数据类
class WebsocketManager:
_instance = None
_lock = threading.Lock()
def __new__(cls):
if cls._instance is None:
with cls._lock:
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance._store = {}
cls._instance._lock = threading.RLock()
return cls._instance
def on_connect(self, shop_key, ws, **kwargs):
"""完全保持原有数据结构"""
with self._lock:
entry = self._store.setdefault(shop_key, {
'platform': None,
'customers': [],
'user_assignments': {}
})
entry['platform'] = {
'ws': ws, # 注意:这里存储的是强引用
'last_heartbeat': datetime.now(),
**kwargs
}
return entry
def get_connection(self, shop_key):
with self._lock:
return self._store.get(shop_key)
def remove_connection(self, shop_key):
with self._lock:
if shop_key in self._store:
del self._store[shop_key]
class JDBackendService:
"""京东后端服务调用类(已废弃:使用单后端连接 BackendClient 代替)"""
def __init__(self, *args, **kwargs):
self.current_store_id = ""
async def connect(self, store_id, *args, **kwargs):
try:
self.current_store_id = str(store_id or "")
except Exception:
self.current_store_id = ""
return True
async def send_message_to_backend(self, platform_message):
"""改为通过单后端连接发送需携带store_id"""
try:
from WebSocket.backend_singleton import get_backend_client
backend = get_backend_client()
if not backend:
return None
# 从platform_message中构造统一上行结构并附加store_id
body = platform_message.get('body', {}) if isinstance(platform_message, dict) else {}
sender_pin = platform_message.get('from', {}).get('pin', '') if isinstance(platform_message, dict) else ''
# 优先取消息内的store_id其次取body内再次退回当前会话store_id
store_id = (platform_message.get('store_id')
or body.get('store_id')
or self.current_store_id
or '')
body_type = body.get('type', 'text')
content_for_backend = body.get('content', '')
if body_type in ('image', 'video'):
# 统一从常见字段取URL
content_for_backend = (
body.get('url')
or body.get('imageUrl')
or body.get('videoUrl')
or body.get('picUrl')
or content_for_backend
)
# 检测文本中的商品卡片/订单卡片
if body_type == 'text' and isinstance(content_for_backend, str):
try:
import re as _re
text = content_for_backend.strip()
# 商品卡片JD商品URL
if _re.search(r"https?://item(\.m)?\.jd\.com/\d+\.html(\?.*)?", text):
body_type = 'product_card'
else:
# 订单卡片多样式匹配
# 1) 订单在前:咨询/查询/订单号:<order>[,]? 商品(ID|id|号|编号)<product>
m1 = _re.search(
r"(?:咨询订单号|查询订单号|订单号)\s*[:]\s*(\d+)[,]?\s*商品(?:ID|id|号|编号)\s*[:]\s*(\d+)",
text)
# 2) 商品在前:商品(ID|id|号|编号)<product>[,]? (咨询/查询)?订单号:<order>
m2 = _re.search(
r"商品(?:ID|id|号|编号)\s*[:]\s*(\d+)[,]?\s*(?:咨询订单号|查询订单号|订单号)\s*[:]\s*(\d+)",
text)
if m1 or m2:
body_type = 'order_card'
if m1:
order_number, product_id = m1.group(1), m1.group(2)
else:
product_id, order_number = m2.group(1), m2.group(2)
# 归一化 content
content_for_backend = f"商品id{product_id} 订单号:{order_number}"
except Exception:
pass
msg = {
'type': 'message',
'content': content_for_backend,
'msg_type': body_type,
'sender': {'id': sender_pin},
'store_id': store_id
}
backend.send_message(msg)
return None
except Exception:
return None
class FixJdCookie:
def __init__(self, log_callback=None):
# 定义一些常用参数
super().__init__() # 继承一些父类的初始化参数
self.ws_manager = WebsocketManager()
self.log_callback = log_callback # 存储日志回调
# 新增后端服务实例
self.backend_service = JDBackendService()
self.backend_connected = False
# 新增重连参数
self.reconnect_attempts = 0
self.max_reconnect_attempts = 10 # 最大重连次数
self.base_reconnect_delay = 1.0 # 基础重连延迟
self.max_reconnect_delay = 60.0 # 最大重连延迟
self.reconnect_backoff = 1.5 # 退避系数
def _log(self, message, log_type="INFO"):
"""内部日志方法"""
if self.log_callback:
self.log_callback(message, log_type)
else:
print(f"[{log_type}] {message}")
def get_config(self, cookies_str):
"""获取配置"""
headers = {
"cookie": cookies_str,
"user-agent": "Mozilla/5.0",
"referer": "https://dongdong.jd.com/",
}
response = requests.get("https://dongdong.jd.com/workbench/checkin.json", headers=headers,
params={"version": "2.6.3", "client": "openweb"})
return response.json()
async def init_wss(self, ws, aid, pin_zj):
"""初始化 socket"""
await self.send_heartbeat(ws, aid, pin_zj)
print("开始监听初始化")
auth = {
"id": hashlib.md5(str(int(time.time() * 1000)).encode()).hexdigest(),
"aid": aid,
"lang": "zh_CN",
"timestamp": int(time.time() * 1000),
"type": "auth",
"body": {"presence": 1, "clientVersion": "2.6.3"},
"to": {"app": "im.waiter"},
"from": {"app": "im.waiter", "pin": pin_zj, "clientType": "comet", "dvc": "device1234"}
}
await ws.send(json.dumps(auth))
async def waiter_status_switch(self, ws, aid, pin_zj):
"""设置接待状态"""
message = {
"id": hashlib.md5(str(int(time.time() * 1000)).encode()).hexdigest(),
"aid": aid,
"lang": "zh_CN",
"timestamp": int(time.time() * 1000),
"from": {
"app": "im.waiter",
"pin": pin_zj,
"art": "customerGroupMsg",
"clientType": "comet"
},
"type": "waiter_status_switch",
"body": {
"s": 1
}
}
await ws.send(json.dumps(message))
async def transfer_customer(self, ws, aid, pin, pin_zj, chat_name):
"""异步客服转接 在发送的消息为客服转接的关键词的时候"""
message = {
"id": hashlib.md5(str(int(time.time() * 1000)).encode()).hexdigest(),
"aid": aid,
"lang": "zh_CN",
"timestamp": int(time.time() * 1000),
"from": {
"app": "im.waiter", "pin": pin_zj, "art": "customerGroupMsg", "clientType": "comet"
},
"type": "chat_transfer_partern",
"body": {
"customer": pin, "cappId": "im.customer", "waiter": chat_name, "reason": "",
"ext": {"pid": ""}, "pid": ""
}
}
try:
await ws.send(json.dumps(message))
return True
except Exception:
traceback.print_exc()
return False
async def send_message(self, ws, pin, aid, pin_zj, vender_id, content):
"""异步发送单条消息"""
try:
print('本地发送消息')
message = {
"ver": "4.3",
"type": "chat_message",
"from": {"pin": pin_zj, "app": "im.waiter", "clientType": "comet"},
"to": {"app": "im.customer", "pin": pin},
"id": hashlib.md5(str(int(time.time() * 1000)).encode()).hexdigest(),
"lang": "zh_CN",
"aid": aid,
"timestamp": int(time.time() * 1000),
"readFlag": 0,
"body": {
"content": content,
"translated": False,
"param": {"cusVenderId": vender_id},
"type": "text"
}
}
await ws.send(json.dumps(message))
logger.info(f"消息已经发送到客户端[info] {pin}: {content[:20]} ...")
except websockets.ConnectionClosed:
logger.error('本地发送消息失败 连接关闭')
raise
except Exception as e:
# 同时这里也要及时进行raise抛出 这样比较好让系统可以看出 异常了可以抛出信息不至于后续被认为
logger.error(f"消息发送过程中出现特殊异常异常信息为: {e}")
raise
def get_userinfo(self, response_text):
"""获取用户 pin 并存入 pins 列表"""
pin = jsonpath.jsonpath(json.loads(response_text), "$..from.pin")
return pin[0] if pin else None
def is_merchants(self, store: object, response):
"""判断消息是否来自顾客"""
send = response['from']['pin']
# 补充: 方法lower()是转化为小写 upper()是转化为大写 title()是每个单词首字母大写其余小写
if send.lower() == "KLD测试".lower():
return False
return True
async def send_heartbeat(self, ws, aid, pin_zj):
"""发送心跳包"""
msg = {
"id": hashlib.md5(str(int(time.time() * 1000)).encode()).hexdigest(),
"type": "client_heartbeat",
"aid": aid,
"ver": "4.1",
"lang": "zh_CN",
"from": {"pin": pin_zj, "app": "im.waiter", "art": "customerGroupMsg", "clientType": "comet"}
}
await ws.send(json.dumps(msg, separators=(',', ':'))) # 使用最简心跳包模式 来节约部分性能 减少传输压力
async def heartbeat_loop(self, websocket, aid, pin_zj):
"""独立的心跳循环"""
"""
优化心跳 循环 新增改进
1. 心跳间隔动态调整
2. 异常重试机制
3. 心跳超时控制
4. 状态监控
"""
retry_count = 0
max_retries = 3
base_interval = 3.0 # 基础间隔1s
backoff_factor = 1.5 # 退避系数
timeout = 10.0
while True:
try:
start_time = time.monotonic()
# 使用websocket原生的ping/pong机制
pong_waiter = await websocket.ping()
await asyncio.wait_for(pong_waiter, timeout=timeout)
# 如果需要发送自定义心跳包
await self.send_heartbeat(websocket, aid, pin_zj)
# 计算剩余等待时间
elapsed = time.monotonic() - start_time
sleep_time = max(0.0, base_interval - elapsed)
if sleep_time > 0:
await asyncio.sleep(sleep_time)
retry_count = 0 # 重置重试计数
except asyncio.TimeoutError:
logger.warning(f'心跳超时,已用时: {time.monotonic() - start_time:.2f}')
retry_count += 1
except websockets.ConnectionClosed:
logger.error("连接关闭,心跳已停止")
break
except Exception as e:
logger.error(f"心跳异常: {e}", exc_info=True)
retry_count += 1
if retry_count >= max_retries:
logger.error(f"心跳连续失败{retry_count}次,终止循环")
break
# 退避策略
if retry_count > 0:
backoff_time = min(
base_interval * (backoff_factor ** (retry_count - 1)),
60.0 # 最大等待60秒
)
logger.debug(f"心跳失败,等待{backoff_time:.2f}秒后重试")
await asyncio.sleep(backoff_time)
''' 整理重连方法 '''
async def calculate_reconnect_delay(self):
"""计算指数退避的重连延迟时间"""
delay = self.base_reconnect_delay * (self.reconnect_backoff ** self.reconnect_attempts)
return min(delay, self.max_reconnect_delay)
async def should_reconnect(self):
"""判断是否应该继续重连"""
if self.reconnect_attempts >= self.max_reconnect_attempts:
self._log(f"已达到最大重连次数({self.max_reconnect_attempts}),停止重连", "ERROR")
return False
return True
async def handle_reconnect(self, exception=None):
"""处理重连逻辑"""
if exception:
error_type = type(exception).__name__
error_msg = str(exception)
self._log(f"连接异常[{error_type}]: {error_msg}", "WARNING")
if not await self.should_reconnect():
return False
delay = await self.calculate_reconnect_delay()
self._log(f"{self.reconnect_attempts + 1}次重连尝试,等待{delay:.1f}秒...", "WARNING")
await asyncio.sleep(delay)
self.reconnect_attempts += 1
return True
''' 后端服务调用方法 '''
async def connect_backend_service(self, store_id):
"""连接后端AI服务"""
try:
success = await self.backend_service.connect(store_id)
if success:
self.backend_connected = True
self._log("✅ 后端AI服务连接成功", "SUCCESS")
return success
except Exception as e:
self._log(f"❌ 连接后端AI服务失败: {e}", "ERROR")
return False
async def get_ai_reply_from_backend(self, platform_message):
"""从后端服务获取AI回复"""
# 首先检查后端服务连接状态
if not self.backend_connected:
# 尝试重新连接
store_id = str(platform_message.get('body', {}).get('chatinfo', {}).get('venderId', ''))
if store_id:
self.backend_connected = await self.connect_backend_service(store_id)
if not self.backend_connected:
self._log("❌ 后端服务未连接尝试重新连接失败使用本地AI回复", "WARNING")
# 降级到本地AI回复
customer_sid = platform_message.get('body', {}).get('chatinfo', {}).get('sid', '')
customer_message = platform_message.get('body', {}).get('content', '')
return
try:
# 推送给后端由后端异步回传AI消息到GUI此处不进行本地立即回复
await self.backend_service.send_message_to_backend(platform_message)
return None
except Exception as e:
self._log(f"❌ 获取AI回复失败: {e}使用本地AI", "ERROR")
customer_sid = platform_message.get('body', {}).get('chatinfo', {}).get('sid', '')
customer_message = platform_message.get('body', {}).get('content', '')
return
async def process_incoming_message(self, response, ws, aid, pin_zj, vender_id, store):
"""处理接收到的消息"""
try:
# 解析消息
json_resp = json.loads(response) if isinstance(response, (str, bytes)) else response
# 验证消息格式
if not all([
json_resp,
json_resp.get('type') == "chat_message",
json_resp.get('ver') == "4.2",
isinstance(json_resp.get("body"), dict)
]):
return
# 过滤非客户消息
pin = self.get_userinfo(response)
if not self.is_merchants(store, json_resp):
return
# 提取消息内容
message_type = json_resp['body']['type']
if message_type == 'text':
customer_message = json_resp['body']['content']
elif message_type == 'image':
customer_message = f"[图片] {json_resp['body'].get('url') or json_resp['body'].get('imageUrl') or json_resp['body'].get('picUrl') or ''}"
elif message_type == 'video':
customer_message = f"[视频] {json_resp['body'].get('url') or json_resp['body'].get('videoUrl') or ''}"
else:
return
self._log(f"📩 收到客户消息: {pin}: {customer_message[:100]}...", "INFO")
# 从后端服务获取AI回复单连接模式仅转交不本地立即回复
ai_result = await self.get_ai_reply_from_backend(json_resp)
if isinstance(ai_result, str) and ai_result.strip():
# 发送回复消息(仅当本地生成/降级时)
await self.send_message(
ws=ws, pin=pin, aid=aid,
pin_zj=pin_zj, vender_id=vender_id,
content=ai_result
)
self._log(f"📤 已发送回复: {ai_result[:100]}...", "INFO")
else:
# 正常链路已转交后端AI等待后端异步回传并由 GUI 转发到平台
self._log("🔄 已转交后端AI处理等待平台回复下发", "INFO")
except json.JSONDecodeError:
self._log("❌ 消息JSON解析失败", "ERROR")
except Exception as e:
self._log(f"❌ 消息处理失败: {e}", "ERROR")
async def message_monitoring(self, cookies_str, aid, pin_zj, vender_id, store, sleep_time=0.5, stop_event=None):
print("✅ DEBUG 进入message_monitoring方法")
print(f"参数验证: cookies={bool(cookies_str)}, aid={aid}, pin_zj={pin_zj}")
# 连接后端AI服务 - 使用店铺ID或venderId
store_id = str(store.get('id', '')) or str(vender_id)
self._log(f"🔗 尝试连接后端服务店铺ID: {store_id}", "DEBUG")
backend_connected = await self.connect_backend_service(store_id)
if not backend_connected:
self._log("⚠️ 后端服务连接失败将使用本地AI回复", "WARNING")
else:
self._log("✅ 后端服务连接成功", "SUCCESS")
stop_event = stop_event or asyncio.Event() # 如果外部没传,自己创建
uri = "wss://dongdong.jd.com/workbench/websocket"
headers = {"cookie": cookies_str}
self._log(f"🔵 开始连接WebSocket: {uri}", "INFO")
# 充值重连计数器
self.reconnect_attempts = 0
while not stop_event.is_set():
try:
self._log(f"🔄 尝试连接WebSocket", "INFO")
async with websockets.connect(uri, additional_headers=headers, ping_interval=6, ping_timeout=10, close_timeout=1, max_queue=1024) as ws:
# 连接成功,重置重连计数器
self.reconnect_attempts = 0
self._log("✅ WebSocket-JD连接成功", "SUCCESS")
await self.init_wss(ws, aid, pin_zj)
# === 验证连接成功的核心指标 ===
# print(f"✅ 连接状态: open={ws.open}, closed={ws.closed}")
print(f"🖥️ 服务端地址: {ws.remote_address}")
# --- 注册连接信息到全局管理
shop_key = f"京东:{store['id']}"
loop = asyncio.get_running_loop()
entry = self.ws_manager.on_connect(
shop_key=shop_key,
ws=ws,
vender_id=vender_id,
aid=aid,
pin_zj=pin_zj,
platform="京东",
loop=loop
)
await self.waiter_status_switch(ws=ws, aid=aid, pin_zj=pin_zj)
heartbeat_task = asyncio.create_task(self.heartbeat_loop(ws, aid, pin_zj))
message_tasks = set()
try:
while not stop_event.is_set(): # 检查是否收到中止信号
try:
print(f"等待监听消息-{datetime.now()}")
response = await asyncio.wait_for(ws.recv(), timeout=1)
print(f"原始消息类型:{type(response)}, 消息体为: {response}")
await self.process_incoming_message(response, ws, aid, pin_zj, vender_id, store)
# 安全解析消息
json_resp = json.loads(response) if isinstance(response, (str, bytes)) else response
print(json_resp)
ver = json_resp.get("ver")
print(f"版本{ver}")
except asyncio.TimeoutError:
continue
except websockets.ConnectionClosed:
# 连接关闭 跳出内层循环进行重连
break
except Exception as e:
self._log(f"消息处理异常: {e}", "ERROR")
continue
await asyncio.sleep(sleep_time)
###
##{'ver': '4.2', 'mid': 366566937, 'body': {'chatinfo': {'venderId': '11961298', 'isJdSuperMarket': '0', 'pid': '10143502227300', 'source': 'jimitwo_service_smart_sdk', 'deviceNo': 'dd_dvc_aes_30EA91E824A2F36365F7B7193C10B76A8842E4E08C0DA687EC9BEB307FCF7195', 'label': 1, 'IMService': False, 'distinguishPersonJimi': 2, 'proVer': 'smart_android_15.2.0', 'sid': 'ce7f35b51a9c00ac898b0fe08608674a', 'entry': 'sdk_item', 'leaveMsgTable': 1, 'venderName': 'TYBOY康路达数字科技专卖店', 'disputeId': -1, 'ddSessionType': '1', 'appId': 'im.waiter', 'systemVer': 'android_13_V2239A', 'eidtoken': 'jdd01C5XRGQHGNJBSFYWWA7RF54QXYHF2N34TM32NGLZV6YAAPPNKWLAIQ2MZV25T4QZ2TJE4HRF6UZ2L3THX7I2BLLB37YVAWKR2BYGRRPA01234567', 'auctionType': '0', 'region': 'CN', 'verification': 'slide'}, 'param': {'venderId': '11961298', 'isJdSuperMarket': '0', 'pid': '10143502227300', 'source': 'jimitwo_service_smart_sdk', 'deviceNo': 'dd_dvc_aes_30EA91E824A2F36365F7B7193C10B76A8842E4E08C0DA687EC9BEB307FCF7195', 'label': 1, 'IMService': False, 'distinguishPersonJimi': 2, 'proVer': 'smart_android_15.2.0', 'sid': 'ce7f35b51a9c00ac898b0fe08608674a', 'entry': 'sdk_item', 'leaveMsgTable': 1, 'venderName': 'TYBOY康路达数字科技专卖店', 'disputeId': -1, 'ddSessionType': '1', 'appId': 'im.waiter', 'systemVer': 'android_13_V2239A', 'eidtoken': 'jdd01C5XRGQHGNJBSFYWWA7RF54QXYHF2N34TM32NGLZV6YAAPPNKWLAIQ2MZV25T4QZ2TJE4HRF6UZ2L3THX7I2BLLB37YVAWKR2BYGRRPA01234567', 'auctionType': '0', 'region': 'CN', 'verification': 'slide'}, 'type': 'text', 'requestData': {'entry': 'sdk_item', 'venderId': '11961298'}, 'content': '你好', 'sid': 'ce7f35b51a9c00ac898b0fe08608674a'}, 'type': 'chat_message', 'clientTime': 1755076542320, 'datetime': '2025-08-13 17:15:42', 'len': 0, 'from': {'app': 'im.customer', 'art': '', 'clientType': 'android', 'pin': 'jd_thpotntctwys'}, 'subType': 'text', 'id': 'bc3c42a706fa4fa482ff565304c7dfbb', 'to': {'app': 'im.waiter', 'clientType': 'comet', 'pin': 'KLD测试'}, 'lang': 'zh_CN', 'timestamp': 1755076542671}
# {'ver': '4.2', 'mid': 366566966, 'body': {'chatinfo': {'venderId': '11961298', 'isJdSuperMarket': '0', 'pid': '10052172306055', 'source': 'jimitwo_service_smart_sdk', 'deviceNo': 'dd_dvc_aes_30EA91E824A2F36365F7B7193C10B76A8842E4E08C0DA687EC9BEB307FCF7195', 'IMService': False, 'distinguishPersonJimi': 2, 'proVer': 'smart_android_15.2.0', 'sid': '538fc3761474ea693812ceed4b39639c', 'entry': 'sdk_item', 'leaveMsgTable': 1, 'venderName': 'TYBOY康路达数字科技专卖店', 'disputeId': -1, 'ddSessionType': '1', 'appId': 'im.waiter', 'systemVer': 'android_13_V2239A', 'eidtoken': 'jdd014TJZZMUXFLXYGJJJ4M3HY3PTU4PBA22SPDIXDWOGZ7XTUTOWTY4LU3VWN6OKDOJPJOVDINMCJXCSPSG5X2K6KWHNQJQOROB57LDG5EY01234567', 'auctionType': '0', 'region': 'CN', 'verification': 'slide'}, 'param': {'venderId': '11961298', 'isJdSuperMarket': '0', 'pid': '10052172306055', 'source': 'jimitwo_service_smart_sdk', 'deviceNo': 'dd_dvc_aes_30EA91E824A2F36365F7B7193C10B76A8842E4E08C0DA687EC9BEB307FCF7195', 'IMService': False, 'distinguishPersonJimi': 2, 'proVer': 'smart_android_15.2.0', 'sid': '538fc3761474ea693812ceed4b39639c', 'entry': 'sdk_item', 'leaveMsgTable': 1, 'venderName': 'TYBOY康路达数字科技专卖店', 'disputeId': -1, 'ddSessionType': '1', 'appId': 'im.waiter', 'systemVer': 'android_13_V2239A', 'eidtoken': 'jdd014TJZZMUXFLXYGJJJ4M3HY3PTU4PBA22SPDIXDWOGZ7XTUTOWTY4LU3VWN6OKDOJPJOVDINMCJXCSPSG5X2K6KWHNQJQOROB57LDG5EY01234567', 'auctionType': '0', 'region': 'CN', 'verification': 'slide'}, 'type': 'text', 'requestData': {'entry': 'sdk_item', 'venderId': '11961298'}, 'content': '你好啊', 'sid': '538fc3761474ea693812ceed4b39639c'}, 'type': 'chat_message', 'clientTime': 1755592905140, 'datetime': '2025-08-19 16:41:45', 'len': 0, 'from': {'app': 'im.customer', 'art': '', 'clientType': 'android', 'pin': 'jd_thpotntctwys'}, 'subType': 'text', 'id': 'd31d369f17f24b05abbe2b7c334f340e', 'to': {'app': 'im.waiter', 'clientType': 'comet', 'pin': 'KLD测试'}, 'lang': 'zh_CN', 'timestamp': 1755592905232}
# {'ver': '4.2', 'mid': 366566984, 'body': {'chatinfo': {'venderId': '11961298', 'isJdSuperMarket': '0', 'pid': '10143502227300', 'source': 'jimitwo_service_smart_sdk', 'deviceNo': 'dd_dvc_aes_30EA91E824A2F36365F7B7193C10B76A8842E4E08C0DA687EC9BEB307FCF7195', 'label': 1, 'IMService': False, 'distinguishPersonJimi': 2, 'proVer': 'smart_android_15.2.0', 'sid': '5b5e044c73ee243b7e67eeca5b11393c', 'entry': 'sdk_item', 'leaveMsgTable': 1, 'venderName': 'TYBOY康路达数字科技专卖店', 'disputeId': -1, 'ddSessionType': '1', 'appId': 'im.waiter', 'systemVer': 'android_13_V2239A', 'eidtoken': 'jdd01ZC7NRD2EX45UN4Q5IZUQC4VLKXIKR7LWP2HC45VQRBDXEYHACT6U5KFBIAI52JBHYCO5BLMOHXTYUYU35RQPB2XA37HL4MPP7CXET7Q01234567', 'auctionType': '0', 'region': 'CN', 'verification': 'slide'}, 'param': {'venderId': '11961298', 'isJdSuperMarket': '0', 'pid': '10143502227300', 'source': 'jimitwo_service_smart_sdk', 'deviceNo': 'dd_dvc_aes_30EA91E824A2F36365F7B7193C10B76A8842E4E08C0DA687EC9BEB307FCF7195', 'label': 1, 'IMService': False, 'distinguishPersonJimi': 2, 'proVer': 'smart_android_15.2.0', 'sid': '5b5e044c73ee243b7e67eeca5b11393c', 'entry': 'sdk_item', 'leaveMsgTable': 1, 'venderName': 'TYBOY康路达数字科技专卖店', 'disputeId': -1, 'ddSessionType': '1', 'appId': 'im.waiter', 'systemVer': 'android_13_V2239A', 'eidtoken': 'jdd01ZC7NRD2EX45UN4Q5IZUQC4VLKXIKR7LWP2HC45VQRBDXEYHACT6U5KFBIAI52JBHYCO5BLMOHXTYUYU35RQPB2XA37HL4MPP7CXET7Q01234567', 'auctionType': '0', 'region': 'CN', 'verification': 'slide'}, 'type': 'text', 'requestData': {'entry': 'sdk_item', 'venderId': '11961298'}, 'content': '您好', 'sid': '5b5e044c73ee243b7e67eeca5b11393c'}, 'type': 'chat_message', 'clientTime': 1755595443735, 'datetime': '2025-08-19 17:24:03', 'len': 0, 'from': {'app': 'im.customer', 'art': '', 'clientType': 'android', 'pin': 'jd_thpotntctwys'}, 'subType': 'text', 'id': '800deaf802f9436b98937bb084ee2a56', 'to': {'app': 'im.waiter', 'clientType': 'comet', 'pin': 'KLD测试'}, 'lang': 'zh_CN', 'timestamp': 1755595443839}
# {'ver': '4.2', 'mid': 366566986, 'body': {'chatinfo': {'venderId': '11961298', 'isJdSuperMarket': '0', 'pid': '10143502227300', 'source': 'jimitwo_service_smart_sdk', 'deviceNo': 'dd_dvc_aes_30EA91E824A2F36365F7B7193C10B76A8842E4E08C0DA687EC9BEB307FCF7195', 'label': 1, 'IMService': False, 'distinguishPersonJimi': 2, 'proVer': 'smart_android_15.2.0', 'sid': '5b5e044c73ee243b7e67eeca5b11393c', 'entry': 'sdk_item', 'leaveMsgTable': 1, 'venderName': 'TYBOY康路达数字科技专卖店', 'disputeId': -1, 'ddSessionType': '1', 'appId': 'im.waiter', 'systemVer': 'android_13_V2239A', 'eidtoken': 'jdd01ZC7NRD2EX45UN4Q5IZUQC4VLKXIKR7LWP2HC45VQRBDXEYHACT6U5KFBIAI52JBHYCO5BLMOHXTYUYU35RQPB2XA37HL4MPP7CXET7Q01234567', 'auctionType': '0', 'region': 'CN', 'verification': 'slide'}, 'param': {'venderId': '11961298', 'isJdSuperMarket': '0', 'pid': '10143502227300', 'source': 'jimitwo_service_smart_sdk', 'deviceNo': 'dd_dvc_aes_30EA91E824A2F36365F7B7193C10B76A8842E4E08C0DA687EC9BEB307FCF7195', 'label': 1, 'IMService': False, 'distinguishPersonJimi': 2, 'proVer': 'smart_android_15.2.0', 'sid': '5b5e044c73ee243b7e67eeca5b11393c', 'entry': 'sdk_item', 'leaveMsgTable': 1, 'venderName': 'TYBOY康路达数字科技专卖店', 'disputeId': -1, 'ddSessionType': '1', 'appId': 'im.waiter', 'systemVer': 'android_13_V2239A', 'eidtoken': 'jdd01ZC7NRD2EX45UN4Q5IZUQC4VLKXIKR7LWP2HC45VQRBDXEYHACT6U5KFBIAI52JBHYCO5BLMOHXTYUYU35RQPB2XA37HL4MPP7CXET7Q01234567', 'auctionType': '0', 'region': 'CN', 'verification': 'slide'}, 'type': 'text', 'requestData': {'entry': 'sdk_item', 'venderId': '11961298'}, 'content': '我先自己看看谢谢您', 'sid': '5b5e044c73ee243b7e67eeca5b11393c'}, 'type': 'chat_message', 'clientTime': 1755595460153, 'datetime': '2025-08-19 17:24:20', 'len': 0, 'from': {'app': 'im.customer', 'art': '', 'clientType': 'android', 'pin': 'jd_thpotntctwys'}, 'subType': 'text', 'id': 'a3d4de984f2d4811952f8ba872850bc2', 'to': {'app': 'im.waiter', 'clientType': 'comet', 'pin': 'KLD测试'}, 'lang': 'zh_CN', 'timestamp': 1755595460274}
except Exception as e:
logger.error(f"接收对应监控消息时候产生特殊错误 , 错误信息为{e}")
finally:
# 清理资源
heartbeat_task.cancel()
try:
await heartbeat_task
except asyncio.CancelledError:
pass
self._log("🔄 连接断开,准备重连", "INFO")
except websockets.ConnectionClosed as e:
self._log(f"🔌 连接已关闭: {e.code} {e.reason}", "WARNING")
if not await self.handle_reconnect(e):
break
except (websockets.WebSocketException, aiohttp.ClientError, OSError) as e:
self._log(f"🌐 网络异常: {type(e).__name__} - {str(e)}", "WARNING")
if not await self.handle_reconnect(e):
break
except Exception as e:
self._log(f"⚠️ 未知异常: {type(e).__name__} - {str(e)}", "ERROR")
if not await self.handle_reconnect(e):
break
# 关闭后端服务连接
if self.backend_connected:
await self.backend_service.close()
self.backend_connected = False
self._log("🛑 消息监听已停止", "INFO")
# 新增: GUI集成包装器类
# class JDListenerForGUI:
# """用于GUI集成的JD监听包装器 ()"""
#
# def __init__(self, log_callback=None):
# self.fix_jd_util = FixJdCookie(log_callback)
# self.log_callback = log_callback
# self.running = False
# self.stop_event = None
# self.username = None
# self.password = None
#
# def _log(self, message, log_type="INFO"):
# """处理日志输出"""
# if self.log_callback:
# self.log_callback(message, log_type)
# else:
# print(f"[{log_type}] {message}")
#
# async def start_listening(self, username, password):
# """启动监听的主方法"""
# try:
# # 存储用户名和密码
# self.username = username
# self.password = password
#
# self._log("🔵 开始JD平台连接流程", "INFO")
# print("🔵 开始JD平台连接流程 - 调试断点1")
#
# # 1. 获取店铺信息
# self._log("🔵 步骤2: 获取店铺信息...", "INFO")
# print("🔵 步骤2: 获取店铺信息... - 调试断点2")
# store_today = self.fix_jd_util.get_today_store()
# if not store_today:
# self._log("❌ 未找到店铺信息", "ERROR")
# print("❌ 未找到店铺信息")
# return False
# self._log(f"✅ 获取到店铺信息: {store_today.get("id", '未知')}", "SUCCESS")
# print(f"✅ 获取到店铺信息: {store_today.get("id", '未知')}")
#
# # 2. 连接后端服务获取cookie
# self._log("🔵 步骤3: 连接后端服务获取cookie...", "INFO")
# store_id = str(store_today.get('id', ''))
#
# jd_cookie = await self.fix_jd_util.connect_backend_service(store_id, username, password)
#
# if not jd_cookie or not jd_cookie.get('status'):
# self._log("❌ 从后端服务获取cookie失败", "ERROR")
# if jd_cookie and jd_cookie.get('verify_link'):
# self._log(f"❌ 需要验证登录,验证链接: {jd_cookie.get('verify_link')}", "ERROR")
# return False
#
# self._log("✅ 从后端服务获取cookie成功", "SUCCESS")
# cookies_str = jd_cookie.get('cookie')
# self._log(f"📦 获取到cookie: {cookies_str[:50] + '...' if cookies_str else '无'}", "DEBUG")
#
# # 3. 获取配置信息
# self._log("🔵 步骤4: 获取配置信息...", "INFO")
# config = None
# for i in range(3):
# try:
# config = self.fix_jd_util.get_config(cookies_str)
# if config and config.get('data'):
# self._log(f"✅ 第{i + 1}次尝试获取配置成功", "SUCCESS")
# break
# else:
# self._log(f"⚠️ 第{i + 1}次尝试获取配置返回空数据", "WARNING")
# except Exception as e:
# self._log(f"获取配置异常({i + 1}/3): {str(e)}", "WARNING")
# await asyncio.sleep(3)
#
# if not config or not config.get('data'):
# self._log("获取配置失败", "ERROR")
# return False
#
# # 4. 提取必要参数
# self._log("🔵 步骤5: 提取配置参数...", "INFO")
# aid = config["data"].get("aid")
# vender_id = config["data"].get("venderId")
# pin_zj = config["data"].get("pin", "").lower()
#
# if not all([aid, vender_id, pin_zj]):
# self._log("❌ 登录信息不完整", "ERROR")
# return False
#
# self._log(f"获取到配置: aid={aid}, vender_id={vender_id}, pin_zj={pin_zj}", "INFO")
#
# # 5. 启动监听
# self._log("🔵 步骤6: 启动消息监听...", "INFO")
# self.stop_event = asyncio.Event()
# self.running = True
#
# self._log("🎉开始监听JD平台消息...", "SUCCESS")
#
# # 调用实际的监听方法
# await self.fix_jd_util.message_monitoring(
# cookies_str=cookies_str,
# aid=aid,
# pin_zj=pin_zj,
# vender_id=vender_id,
# store=store_today,
# stop_event=self.stop_event,
# username=username,
# password=password
# )
#
# return True
#
# except Exception as e:
# self._log(f"监听过程中出现严重错误: {str(e)}", "ERROR")
# import traceback
# self._log(f"错误详情: {traceback.format_exc()}", "DEBUG")
# return False
#
# def stop_listening(self):
# """停止监听"""
# if self.stop_event:
# self.stop_event.set()
# self.running = False
# self._log("JD监听已停止", "INFO")
# 新增: GUI集成包装器类
class JDListenerForGUI:
"""用于GUI集成的JD监听包装器 ()"""
def __init__(self, log_callback=None):
self.fix_jd_util = FixJdCookie(log_callback)
self.log_callback = log_callback
self.running = False
self.stop_event = None
def _log(self, message, log_type="INFO"):
"""处理日志输出"""
if self.log_callback:
self.log_callback(message, log_type)
else:
print(f"[{log_type}] {message}")
async def start_listening(self, username, password):
"""启动监听的主方法"""
try:
self._log("🔵 开始JD平台连接流程", "INFO")
print("🔵 开始JD平台连接流程 - 调试断点1")
# 1. 获取店铺信息
self._log("🔵 步骤2: 获取店铺信息...", "INFO")
print("🔵 步骤2: 获取店铺信息... - 调试断点2")
store_today = self.fix_jd_util.get_today_store()
if not store_today:
self._log("❌ 未找到店铺信息", "ERROR")
print("❌ 未找到店铺信息")
return False
self._log(f"✅ 获取到店铺信息: {store_today.get("id", '未知')}", "SUCCESS")
print(f"✅ 获取到店铺信息: {store_today.get("id", '未知')}")
cookie_str = store_today.get('platform_cookie')
self._log(f"📦 当前存储的cookie: {cookie_str[:50] + '...' if cookie_str else ''}", "DEBUG")
# 2. 获取或更新cookie - 在这里设置断点②
self._log("🔵 步骤3: 获取或更新JD cookie...", "INFO")
# 2. 获取或更新cookie
jd_login_cookie = self.fix_jd_util.get_cookies(
username=username,
password=password,
cookies_str=cookie_str
)
if not jd_login_cookie['status']:
fail_status = jd_login_cookie.get('verify_link', None)
if fail_status:
self._log(f"❌ JD登录失败: , 失败类型为二次验证(手机验证码) 对应url:{fail_status}", "ERROR")
else:
# 表示没有返回verify_url 未知错误
self._log(f"❌ JD登录失败: , 失败类型为:{fail_status} 未知错误 请单独测试login方法")
return False
self._log("✅ JD登录成功", "SUCCESS")
self._log(f"📦 获取到cookie: {jd_login_cookie['cookie'][:50] + '...'}", "DEBUG")
self._log("🔵 步骤4: 获取配置信息...", "INFO")
# 3. 获取配置信息
config = None
for i in range(3):
try:
config = self.fix_jd_util.get_config(jd_login_cookie['cookie'])
if config and config.get('data'):
self._log(f"✅ 第{i + 1}次尝试获取配置成功", "SUCCESS")
break
else:
self._log(f"⚠️ 第{i + 1}次尝试获取配置返回空数据", "WARNING")
except Exception as e:
self._log(f"获取配置异常({i + 1}/3): {str(e)}", "WARNING")
await asyncio.sleep(3)
if not config or not config.get('data'):
self._log("获取配置失败", "ERROR")
return False
# 4. 提取必要参数
self._log("🔵 步骤5: 提取配置参数...", "INFO")
aid = config["data"].get("aid")
vender_id = config["data"].get("venderId")
pin_zj = config["data"].get("pin", "").lower()
if not all([aid, vender_id, pin_zj]):
self._log("❌ 登录信息不完整", "ERROR")
return False
self._log(f"获取到配置: aid={aid}, vender_id={vender_id}, pin_zj={pin_zj}", "INFO")
# 5. 启动监听
self._log("🔵 步骤6: 启动消息监听...", "INFO")
self.stop_event = asyncio.Event()
self.running = True
self._log("🎉开始监听JD平台消息...", "SUCCESS")
# 调用实际的监听方法
await self.fix_jd_util.message_monitoring(
cookies_str=jd_login_cookie['cookie'],
aid=aid,
pin_zj=pin_zj,
vender_id=vender_id,
store=store_today,
stop_event=self.stop_event
)
return True
except Exception as e:
self._log(f"监听过程中出现严重错误: {str(e)}", "ERROR")
import traceback
self._log(f"错误详情: {traceback.format_exc()}", "DEBUG")
return False
async def start_with_cookies(self, store_id: str, cookies: str):
"""使用下发的cookies与store_id直接建立JD平台WS并开始监听"""
try:
self._log("🔵 [JD] 收到后端登录指令开始使用cookies连接平台", "INFO")
# 获取平台配置
config = None
for i in range(3):
try:
config = self.fix_jd_util.get_config(cookies)
if config and config.get('data'):
self._log(f"✅ 第{i + 1}次尝试获取配置成功", "SUCCESS")
break
else:
self._log(f"⚠️ 第{i + 1}次尝试获取配置返回空数据", "WARNING")
except Exception as e:
self._log(f"获取配置异常({i + 1}/3): {str(e)}", "WARNING")
await asyncio.sleep(3)
if not config or not config.get('data'):
self._log("获取配置失败", "ERROR")
return False
aid = config["data"].get("aid")
vender_id = config["data"].get("venderId")
pin_zj = config["data"].get("pin", "").lower()
if not all([aid, vender_id, pin_zj]):
self._log("❌ 登录信息不完整", "ERROR")
return False
# 建立与后端的AI通道确保使用GUI的store_id
await self.fix_jd_util.connect_backend_service(store_id)
# 启动监听
self.stop_event = asyncio.Event()
self.running = True
store = {'id': store_id}
self._log("🎉 [JD] 开始监听平台消息", "SUCCESS")
await self.fix_jd_util.message_monitoring(
cookies_str=cookies,
aid=aid,
pin_zj=pin_zj,
vender_id=vender_id,
store=store,
stop_event=self.stop_event
)
return True
except Exception as e:
self._log(f"[JD] 监听过程中出现错误: {str(e)}", "ERROR")
import traceback
self._log(f"错误详情: {traceback.format_exc()}", "DEBUG")
return False
def stop_listening(self):
"""停止监听"""
if self.stop_event:
self.stop_event.set()
self.running = False
self._log("JD监听已停止", "INFO")
async def main():
username = "KLD测试"
password = "kld168168"
fix_jd_util = FixJdCookie()
store_today = fix_jd_util.get_today_store()
# 检查店铺信息
if not store_today:
logger.error("❌ 未找到店铺信息")
return
store_id = str(store_today.get('id', ''))
logger.info(f"✅ 获取到店铺信息: {store_id}")
try:
# 1. 直接连接后端服务获取 cookie
logger.info("🔵 步骤1: 连接后端服务获取 cookie...")
jd_cookie = await fix_jd_util.connect_backend_service(store_id, username, password)
print(f"完整的cookie数据: {jd_cookie}")
if not jd_cookie or not jd_cookie.get('status'):
logger.error("❌ 从后端服务获取 cookie 失败")
if jd_cookie and jd_cookie.get('verify_link'):
logger.error(f"❌ 需要验证登录,验证链接: {jd_cookie.get('verify_link')}")
return
cookies_str = jd_cookie.get('cookie')
logger.info("✅ 从后端服务获取 cookie 成功")
logger.debug(f"📦 获取到 cookie: {cookies_str[:50] + '...' if cookies_str else ''}")
# 测试cookie后端生成的有效性
# cookies_str = "shshshfpa=112082d7-6f59-f35d-093a-e4c035938ab8-1754961318; shshshfpx=112082d7-6f59-f35d-093a-e4c035938ab8-1754961318; __jdu=17549613198151684402245; user-key=13d41f1d-5d8a-447e-a3d1-19c964e2fa13; __jdv=76161171|direct|-|none|-|1756259208627; areaId=18; ipLoc-djd=18-1482-48942-49052; pinId=EifU7rHmf-gwaaxnNveDFw; _tp=%2Bw%2Fs5xFMS9g0SkUd93EGxqh4USt1M5LwIyzTR4TbgCM%3D; _pst=KLD%E6%B5%8B%E8%AF%95; pin=KLD%E6%B5%8B%E8%AF%95; unick=u40d47u5ckctnz; PCSYCityID=CN_430000_430100_0; ceshi3.com=000; sdtoken=AAbEsBpEIOVjqTAKCQtvQu17tqE2au0-pftQOStCUKRw9JX4gfXHESNiN_EgrTvDv8qS5IZHipleuQxIX9JXWojJS8sKju6Vh1Qqt5LjakfSeWqqAOL-HhXeBn9m; shshshfpb=BApXS1gvPG_xA1izHnR4d6bvzjbeOVTtiBhbXFDdg9xJ1Mh6uh462; 3AB9D23F7A4B3CSS=jdd03OFWZGTHWHQYLKSX5BEWHXMTLYAHXEEXC7HBOIZDBIPLMQZT5LTHKICALRZU5ZDL6X6T3NMHRNSJJDX6BTEI6AVJS5IAAAAMZDDKTS5YAAAAACJZVYQP7FYJA3UX; _gia_d=1; wlfstk_smdl=0whbpvk7ixj27lbykeeb8rh6iulon3fo; __jda=95931165.17549613198151684402245.1754961320.1757039732.1757053789.21; __jdb=95931165.30.17549613198151684402245|21.1757053789; __jdc=95931165; 3AB9D23F7A4B3C9B=OFWZGTHWHQYLKSX5BEWHXMTLYAHXEEXC7HBOIZDBIPLMQZT5LTHKICALRZU5ZDL6X6T3NMHRNSJJDX6BTEI6AVJS5I; TrackID=1C24wlDX-QPSyJRaB_1YHqCLhBW6qIo2stht_nwl5g9fGI-lJpP-CZT3TaCr9QVppcvhilhcCe1VhgXMKGWao6Fd3wJ5bQhJ9w9VwWcYOySsXfbCTQbteFWevVN1ZQYp9; thor=4E136F9D9458703D01BE17544D30601F9649F79B89E5FC150CA91054C788CAA1C82670080466F219573AE7FD6EB9ABDF9F52D520671373DAD721CC3B78613FABC99ADA1FAC8E92CDC42F5131682B1F008727F1BA49783B055AFED9349D0B79E53A51F059A1DDE3FC181DD38D1B388D829CE8ADD775D0D30C38A8CAD0519DCD0C; flash=3_KFKaImBVn2spH8stTd9wjKlZQTxgYcZCPXXP_axvJMphR3w29aJNU2c2qPReKxWHRX1lzJ7MfD9iQmHQI-2cKp0dYzs6YsH9eDyB3lQxuu6MtkM8jCiBynVSdRBnr21oDrLKGMeYG6yYlcEsAsbe8OC-yKO69758MJYyMZd_4soV; light_key=AASBKE7rOxgWQziEhC_QY6yakCROyWTrRIF9K9uCpw_IcR8gGNaL7IM6AQuVa-3pJoC9wTze; logining=1; rita=A1EA9FF92ADE7FC61C825E83F126B9E97EF9243BEED9B77E4F7110D6081254A8EEAA66B26BFA00E08CBD8B0C88DD3D292CAD14839A50184501755B761A11F679F63D9DAA76E6785799D2F78AE378F76F32E05C1914C1132995B15CC5F79AFB9314A9D6FE7911DAFE1D958906C016E724"
# 2. 获取配置信息
logger.info("🔵 步骤2: 获取配置信息...")
config = None
for i in range(3):
try:
config = fix_jd_util.get_config(cookies_str)
if config and config.get('data'):
logger.info(f"✅ 第{i + 1}次尝试获取配置成功")
break
else:
logger.warning(f"⚠️ 第{i + 1}次尝试获取配置返回空数据")
except Exception as e:
logger.error(f"获取配置异常({i + 1}/3): {e}")
if i == 2:
return
await asyncio.sleep(3) # 使用异步等待
if not config or not config.get('data'):
logger.error("❌ 获取配置失败")
return
# 3. 提取必要参数
logger.info("🔵 步骤3: 提取配置参数...")
aid = config["data"].get("aid")
vender_id = config["data"].get("venderId")
pin_zj = config["data"].get("pin", "").lower()
if not all([aid, vender_id, pin_zj]):
logger.error("❌ 登录信息不完整,需要重新登录")
return
logger.info(f"✅ 获取到配置: aid={aid}, vender_id={vender_id}, pin_zj={pin_zj}")
# 4. 启动监听
logger.info("🔵 步骤4: 启动消息监听...")
stop_event = asyncio.Event()
try:
await fix_jd_util.message_monitoring(
cookies_str=cookies_str,
aid=aid,
pin_zj=pin_zj,
vender_id=vender_id,
store=store_today,
stop_event=stop_event,
username=username,
password=password
)
except Exception as e:
logger.error(f"❌ 监听过程中出现错误: {e}")
import traceback
logger.error(f"错误详情: {traceback.format_exc()}")
except Exception as e:
logger.error(f"❌ 主程序执行过程中出现错误: {e}")
import traceback
logger.error(f"错误详情: {traceback.format_exc()}")
if __name__ == '__main__':
asyncio.run(main())
# asyncio.run(new_test_login())

0
Utils/JD/__init__.py Normal file
View File

1178
Utils/Pdd/PddUtils.py Normal file

File diff suppressed because it is too large Load Diff

0
Utils/Pdd/__init__.py Normal file
View File

File diff suppressed because it is too large Load Diff

View File

0
Utils/__init__.py Normal file
View File

170
Utils/message_models.py Normal file
View File

@@ -0,0 +1,170 @@
import json
from dataclasses import dataclass, asdict
from typing import Optional, Dict, Any
@dataclass
class PlatformMessage:
"""规范消息结构体 - 严格按照WebSocket文档v2格式"""
# 必填字段
type: str # 消息类型:"message" | "staff_list" | "ping" | "pong" | "transfer" | "connect_success" | "error"
# 条件必填字段(根据消息类型)
content: Optional[str] = None # 消息内容
msg_type: Optional[str] = None # 消息实际类型:"text" | "image" | "video" | "product_card" | "order_card"
pin_image: Optional[str] = None # 用户头像URL
sender: Optional[Dict] = None # 发送者信息必须包含id字段
store_id: Optional[str] = None # 店铺ID
# 可选字段
receiver: Optional[Dict] = None # 接收者信息
data: Optional[Dict] = None # 扩展数据(如客服列表、验证链接等)
uuid: Optional[str] = None # 心跳包UUID
token: Optional[str] = None # 认证令牌(心跳包可能需要)
def __post_init__(self):
"""初始化后处理"""
# 自动设置msg_type仅对message类型
if self.type == "message" and self.msg_type is None and self.content:
self.msg_type = self._detect_msg_type()
def _detect_msg_type(self) -> str:
"""自动检测消息类型"""
if not self.content:
return "text"
content = self.content.lower()
# 图片检测
if any(ext in content for ext in ['.jpg', '.jpeg', '.png', '.gif', '.webp']):
return "image"
# 视频检测
if any(ext in content for ext in ['.mp4', '.avi', '.mov', '.wmv', '.flv']):
return "video"
# 订单卡片检测 - 支持多种格式
if ("商品id" in self.content and "订单号:" in self.content) or \
("商品ID" in self.content and "订单号:" in self.content) or \
("咨询订单号:" in self.content and "商品ID" in self.content):
return "order_card"
# 商品卡片检测
if any(keyword in content for keyword in ['goods.html', 'item.html', 'item.jd.com', '商品卡片id']):
return "product_card"
# 默认文本消息
return "text"
def to_json(self) -> str:
"""序列化为JSON字符串用于存储"""
return json.dumps(self.to_dict(), ensure_ascii=False)
def to_dict(self) -> Dict[str, Any]:
"""转换为字典过滤None值"""
result = {}
for key, value in asdict(self).items():
if value is not None:
result[key] = value
return result
@classmethod
def from_json(cls, json_str: str):
"""从JSON字符串恢复结构体用于读取"""
data = json.loads(json_str)
return cls(**data)
@classmethod
def from_dict(cls, data: Dict[str, Any]):
"""从字典创建实例"""
return cls(**data)
@classmethod
def create_text_message(cls, content: str, sender_id: str, store_id: str, pin_image: str = None):
"""创建文本消息"""
return cls(
type="message",
content=content,
msg_type="text",
pin_image=pin_image,
sender={"id": sender_id},
store_id=store_id
)
@classmethod
def create_image_message(cls, image_url: str, sender_id: str, store_id: str, pin_image: str = None):
"""创建图片消息"""
return cls(
type="message",
content=image_url,
msg_type="image",
pin_image=pin_image,
sender={"id": sender_id},
store_id=store_id
)
@classmethod
def create_video_message(cls, video_url: str, sender_id: str, store_id: str, pin_image: str = None):
"""创建视频消息"""
return cls(
type="message",
content=video_url,
msg_type="video",
pin_image=pin_image,
sender={"id": sender_id},
store_id=store_id
)
@classmethod
def create_order_card_message(cls, product_id: str, order_number: str, sender_id: str, store_id: str,
pin_image: str = None):
"""创建订单卡片消息"""
return cls(
type="message",
content=f"商品id{product_id} 订单号:{order_number}",
msg_type="order_card",
pin_image=pin_image,
sender={"id": sender_id},
store_id=store_id
)
@classmethod
def create_product_card_message(cls, product_url: str, sender_id: str, store_id: str, pin_image: str = None):
"""创建商品卡片消息"""
return cls(
type="message",
content=product_url,
msg_type="product_card",
pin_image=pin_image,
sender={"id": sender_id},
store_id=store_id
)
@classmethod
def create_staff_list_message(cls, staff_list: list, store_id: str):
"""创建客服列表消息"""
return cls(
type="staff_list",
content="客服列表更新",
data={"staff_list": staff_list},
store_id=store_id
)
@classmethod
def create_ping_message(cls, uuid_str: str, token: str = None):
"""创建心跳检测消息"""
message = cls(
type="ping",
uuid=uuid_str
)
if token:
message.token = token
return message
@classmethod
def create_pong_message(cls, uuid_str: str):
"""创建心跳回复消息"""
return cls(
type="pong",
uuid=uuid_str
)

809
WebSocket/BackendClient.py Normal file
View File

@@ -0,0 +1,809 @@
# WebSocket/BackendClient.py
import json
import threading
import websockets
import uuid
import asyncio
from typing import List, Dict, Any, Optional, Callable
from config import get_gui_ws_url
class BackendClient:
"""后端WebSocket客户端"""
def __init__(self, url: str, token: str = None):
self.token = token
self.uuid = str(uuid.uuid4())
self.url = url
# 消息处理回调函数
self.store_list_callback: Optional[Callable] = None
self.customers_callback: Optional[Callable] = None
self.message_callback: Optional[Callable] = None
self.transfer_callback: Optional[Callable] = None
self.close_callback: Optional[Callable] = None
self.error_callback: Optional[Callable] = None
self.login_callback: Optional[Callable] = None # 新增平台登录下发cookies回调
self.success_callback: Optional[Callable] = None # 新增:后端连接成功回调
self.is_connected = False
def connect(self):
"""连接到WebSocket服务器"""
if self.is_connected:
return
self.thread = threading.Thread(target=self._run_loop, daemon=True)
self.thread.start()
def disconnect(self):
"""断开WebSocket连接"""
if self.loop and self.loop.is_running():
asyncio.run_coroutine_threadsafe(self._close(), self.loop)
if self.thread and self.thread.is_alive():
self.thread.join(timeout=3)
self.is_connected = False
async def _close(self):
"""异步关闭连接"""
if self.websocket:
await self.websocket.close()
self.is_connected = False
def _run_loop(self):
"""在新线程中运行事件循环"""
self.loop = asyncio.new_event_loop()
asyncio.set_event_loop(self.loop)
try:
self.loop.run_until_complete(self._connect_and_listen())
except Exception as e:
print(f"WebSocket异常: {e}")
finally:
self.loop.close()
async def _connect_and_listen(self):
"""连接并监听消息"""
try:
self.websocket = await websockets.connect(self.url)
self.is_connected = True
self.on_connected()
async for message in self.websocket:
try:
# 打印原始文本帧与长度
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))
@classmethod
def from_exe_token(cls, exe_token: str):
"""使用 exe_token 构造单连接客户端ws/gui/<token>/"""
url = get_gui_ws_url(exe_token)
return cls(url=url, token=exe_token)
def set_callbacks(self,
store_list: Callable = None,
customers: Callable = None,
message: Callable = None,
transfer: Callable = None,
close: Callable = None,
error: Callable = None,
login: Callable = None,
success: Callable = None):
"""设置各种消息类型的回调函数"""
if store_list:
self.store_list_callback = store_list
if customers:
self.customers_callback = customers
if message:
self.message_callback = message
if transfer:
self.transfer_callback = transfer
if close:
self.close_callback = close
if error:
self.error_callback = error
if login:
self.login_callback = login
if success:
self.success_callback = success
def on_connected(self):
"""连接成功时的处理"""
print("后端WebSocket连接成功")
# 不再主动请求 get_store避免与后端不兼容导致协程未完成
def on_message_received(self, message: Dict[str, Any]):
"""处理接收到的消息 - 根据WebSocket文档v2更新"""
# 统一打印后端下发的完整消息结构体
try:
import json as _json
print("=== Backend -> GUI Message ===")
print(_json.dumps(message, ensure_ascii=False, indent=2))
print("=== End Message ===")
except Exception:
pass
msg_type = message.get('type')
try:
if msg_type == 'get_store':
self._handle_store_list(message)
elif msg_type == 'get_customers':
self._handle_customers_list(message)
elif msg_type == 'success':
print("后端连接服务成功")
# 可在此触发上层UI通知
if self.success_callback:
try:
self.success_callback()
except Exception:
pass
elif msg_type == 'message':
self._handle_message(message)
elif msg_type == 'transfer':
self._handle_transfer(message)
elif msg_type == 'close':
self._handle_close(message)
elif msg_type == 'pong':
self._handle_pong(message)
elif msg_type == 'connect_success': # 兼容旧版
self._handle_connect_success(message)
elif msg_type == 'login': # 新版后台下发平台cookies
self._handle_login(message)
elif msg_type == 'error':
self._handle_error_message(message)
elif msg_type == 'staff_list':
self._handle_staff_list(message)
else:
print(f"未知消息类型: {msg_type}")
except Exception as e:
error_msg = f"处理消息异常: {e}"
print(error_msg)
if self.error_callback:
self.error_callback(error_msg, message)
def on_error(self, error: str):
"""错误处理"""
print(f"后端连接错误: {error}")
if self.error_callback:
self.error_callback(error, None)
# ==================== 发送消息方法 ====================
def send_message(self, message: Dict[str, Any]):
"""
发送消息到后端
Args:
message: 要发送的消息字典
"""
if not self.is_connected or not self.loop:
raise Exception("WebSocket未连接")
future = asyncio.run_coroutine_threadsafe(
self._send_to_backend(message), self.loop
)
return future.result(timeout=8)
async def _send_to_backend(self, message: Dict[str, Any]):
"""异步发送消息到后端"""
if not self.websocket:
raise Exception("WebSocket连接不存在")
import json
message_str = json.dumps(message, ensure_ascii=False)
await self.websocket.send(message_str)
print(f"发送消息到后端: {message}")
def send_ping(self, custom_uuid: str = None, custom_token: str = None):
"""
发送心跳包
如果接收到关闭的消息后心跳包要带上token
"""
ping_message = {
'type': 'ping',
'uuid': custom_uuid or self.uuid
}
token = custom_token or self.token
if token:
ping_message['token'] = token
return self.send_message(ping_message)
def get_store(self):
"""获取店铺信息"""
message = {
'type': 'get_store',
'token': self.token or ''
}
return self.send_message(message)
def send_text_message(self, content: str, sender_id: str, store_id: str, pin_image: str = None):
"""发送文本消息 - 根据WebSocket文档v2更新"""
message = {
'type': 'message',
'content': content,
'msg_type': 'text',
'sender': {'id': sender_id},
'store_id': store_id
}
if pin_image:
message['pin_image'] = pin_image
return self.send_message(message)
def send_image_message(self, image_url: str, sender_id: str, store_id: str, pin_image: str = None):
"""发送图片消息 - 根据WebSocket文档v2更新"""
message = {
'type': 'message',
'content': image_url,
'msg_type': 'image',
'sender': {'id': sender_id},
'store_id': store_id
}
if pin_image:
message['pin_image'] = pin_image
return self.send_message(message)
def send_video_message(self, video_url: str, sender_id: str, store_id: str, pin_image: str = None):
"""发送视频消息 - 根据WebSocket文档v2更新"""
message = {
'type': 'message',
'content': video_url,
'msg_type': 'video',
'sender': {'id': sender_id},
'store_id': store_id
}
if pin_image:
message['pin_image'] = pin_image
return self.send_message(message)
def send_order_card(self, product_id: str, order_number: str, sender_id: str, store_id: str, pin_image: str = None):
"""发送订单卡片 - 根据WebSocket文档v2更新"""
message = {
'type': 'message',
'content': f'商品id{product_id} 订单号:{order_number}',
'msg_type': 'order_card',
'sender': {'id': sender_id},
'store_id': store_id
}
if pin_image:
message['pin_image'] = pin_image
return self.send_message(message)
def send_product_card(self, product_url: str, sender_id: str, store_id: str, pin_image: str = None):
"""发送商品卡片 - 根据WebSocket文档v2更新"""
message = {
'type': 'message',
'content': product_url,
'msg_type': 'product_card',
'sender': {'id': sender_id},
'store_id': store_id
}
if pin_image:
message['pin_image'] = pin_image
return self.send_message(message)
def send_staff_list(self, staff_list: List[Dict], store_id: str):
"""发送客服列表 - 根据WebSocket文档v2更新"""
message = {
'type': 'staff_list',
'content': '客服列表更新',
'data': {'staff_list': staff_list},
'store_id': store_id
}
return self.send_message(message)
# 保持向后兼容的旧方法(标记为已废弃)
def send_file_message(self, content: str, uid: str, pin: str, store_id: str):
"""发送文件消息 - 已废弃请使用send_video_message或send_image_message"""
print("警告: send_file_message已废弃请根据文件类型使用send_video_message或send_image_message")
# 尝试自动检测文件类型
content_lower = content.lower()
if any(ext in content_lower for ext in ['.mp4', '.avi', '.mov', '.wmv', '.flv']):
return self.send_video_message(content, uid, store_id)
elif any(ext in content_lower for ext in ['.jpg', '.jpeg', '.png', '.gif', '.webp']):
return self.send_image_message(content, uid, store_id)
else:
# 默认作为文本消息发送
return self.send_text_message(content, uid, store_id)
def send_transfer(self, customer: str, pin: str, store_id: str):
"""发送转接消息"""
message = {
'type': 'transfer',
'customer': customer, # 客服名称
'pin': pin, # 顾客名称
'store_id': store_id
}
return self.send_message(message)
def close_store(self, store_id: str):
"""关闭店铺"""
message = {
'type': 'close',
'store_id': store_id
}
return self.send_message(message)
# ==================== 消息处理方法 ====================
def _handle_store_list(self, message: Dict[str, Any]):
"""处理店铺列表"""
store_list = message.get('store_list', [])
print(f"获取到{len(store_list)}个店铺:")
for store in store_list:
merchant_name = store.get('merchant_name', '')
store_id = store.get('store_id', '')
store_platform = store.get('store_platform', '')
print(f" - {merchant_name} (ID: {store_id}, 平台: {store_platform})")
if self.store_list_callback:
self.store_list_callback(store_list)
def _handle_customers_list(self, message: Dict[str, Any]):
"""处理客服列表"""
customers = message.get('customers', [])
store_id = message.get('store_id', '')
print(f"店铺{store_id}的客服列表,共{len(customers)}个客服:")
for customer in customers:
pin = customer.get('pin', '')
nickname = customer.get('nickname', '')
print(f" - {nickname} ({pin})")
if self.customers_callback:
self.customers_callback(customers, store_id)
def _handle_message(self, message: Dict[str, Any]):
"""处理消息"""
store_id = message.get('store_id', '')
data = message.get('data')
content = message.get('content', '')
print(f"[{store_id}] [{message.get('msg_type', 'unknown')}] : {content}")
# 尝试将后端AI/客服回复转发到对应平台
try:
receiver = message.get('receiver') or (data.get('receiver') if isinstance(data, dict) else None) or {}
recv_pin = receiver.get('id')
if recv_pin and store_id:
# 根据store_id动态确定平台类型
platform_type = self._get_platform_by_store_id(store_id)
if platform_type == "京东":
self._forward_to_jd(store_id, recv_pin, content)
elif platform_type == "抖音":
self._forward_to_douyin(store_id, recv_pin, content)
elif platform_type == "千牛":
self._forward_to_qianniu(store_id, recv_pin, content)
elif platform_type == "拼多多":
self._forward_to_pdd(store_id, recv_pin, content)
else:
print(f"[Forward] 未知平台类型或未找到店铺: {platform_type}, store_id={store_id}")
except Exception as e:
print(f"转发到平台失败: {e}")
if self.message_callback:
self.message_callback(data if data else message, store_id)
def _get_platform_by_store_id(self, store_id: str) -> str:
"""根据店铺ID获取平台类型"""
try:
# 从WebSocket管理器获取平台信息
from WebSocket.backend_singleton import get_websocket_manager
manager = get_websocket_manager()
if manager and hasattr(manager, 'platform_listeners'):
for key, listener_info in manager.platform_listeners.items():
if listener_info.get('store_id') == store_id:
return listener_info.get('platform', '')
return ""
except Exception as e:
print(f"获取平台类型失败: {e}")
return ""
def _forward_to_jd(self, store_id: str, recv_pin: str, content: str):
"""转发消息到京东平台"""
try:
from Utils.JD.JdUtils import WebsocketManager as JDWSManager
jd_mgr = JDWSManager()
shop_key = f"京东:{store_id}"
entry = jd_mgr.get_connection(shop_key)
if not entry:
print(f"[JD Forward] 未找到连接: {shop_key}")
return
platform_info = (entry or {}).get('platform') or {}
ws = platform_info.get('ws')
aid = platform_info.get('aid')
pin_zj = platform_info.get('pin_zj')
vender_id = platform_info.get('vender_id')
loop = platform_info.get('loop')
print(f"[JD Forward] shop_key={shop_key} has_ws={bool(ws)} aid={aid} pin_zj={pin_zj} vender_id={vender_id} has_loop={bool(loop)} recv_pin={recv_pin}")
if ws and aid and pin_zj and vender_id and loop and content:
async def _send():
import hashlib as _hashlib
import time as _time
import json as _json
msg = {
"ver": "4.3",
"type": "chat_message",
"from": {"pin": pin_zj, "app": "im.waiter", "clientType": "comet"},
"to": {"app": "im.customer", "pin": recv_pin},
"id": _hashlib.md5(str(int(_time.time() * 1000)).encode()).hexdigest(),
"lang": "zh_CN",
"aid": aid,
"timestamp": int(_time.time() * 1000),
"readFlag": 0,
"body": {
"content": content,
"translated": False,
"param": {"cusVenderId": vender_id},
"type": "text"
}
}
await ws.send(_json.dumps(msg))
import asyncio as _asyncio
_future = _asyncio.run_coroutine_threadsafe(_send(), loop)
try:
_future.result(timeout=2)
print(f"[JD Forward] 已转发到平台: pin={recv_pin}, content_len={len(content)}")
except Exception as fe:
print(f"[JD Forward] 转发提交失败: {fe}")
else:
print("[JD Forward] 条件不足,未转发:",
{
'has_ws': bool(ws), 'has_aid': bool(aid), 'has_pin_zj': bool(pin_zj),
'has_vender_id': bool(vender_id), 'has_loop': bool(loop), 'has_content': bool(content)
})
except Exception as e:
print(f"[JD Forward] 转发失败: {e}")
def _forward_to_douyin(self, store_id: str, recv_pin: str, content: str):
"""转发消息到抖音平台"""
try:
from Utils.Dy.DyUtils import DouYinWebsocketManager
dy_mgr = DouYinWebsocketManager()
shop_key = f"抖音:{store_id}"
entry = dy_mgr.get_connection(shop_key)
if not entry:
print(f"[DY Forward] 未找到连接: {shop_key}")
return
platform_info = entry.get('platform', {})
douyin_bot = platform_info.get('douyin_bot')
message_handler = platform_info.get('message_handler')
print(f"[DY Forward] shop_key={shop_key} has_bot={bool(douyin_bot)} has_handler={bool(message_handler)} recv_pin={recv_pin}")
if douyin_bot and message_handler and content:
# 在消息处理器的事件循环中发送消息
def send_in_loop():
try:
# 获取消息处理器的事件循环
loop = message_handler._loop
if loop and not loop.is_closed():
# 在事件循环中执行发送
future = asyncio.run_coroutine_threadsafe(
message_handler.send_message_external(recv_pin, content),
loop
)
# 等待结果
try:
result = future.result(timeout=5)
if result:
print(f"[DY Forward] 已转发到平台: pin={recv_pin}, content_len={len(content)}")
else:
print(f"[DY Forward] 转发失败: 消息处理器返回False")
except Exception as fe:
print(f"[DY Forward] 转发执行失败: {fe}")
else:
print(f"[DY Forward] 事件循环不可用")
except Exception as e:
print(f"[DY Forward] 发送过程异常: {e}")
# 在新线程中执行发送操作
import threading
send_thread = threading.Thread(target=send_in_loop, daemon=True)
send_thread.start()
else:
print("[DY Forward] 条件不足,未转发:",
{
'has_bot': bool(douyin_bot),
'has_handler': bool(message_handler),
'has_content': bool(content)
})
except Exception as e:
print(f"[DY Forward] 转发失败: {e}")
def _forward_to_qianniu(self, store_id: str, recv_pin: str, content: str):
"""转发消息到千牛平台"""
try:
# TODO: 实现千牛平台的消息转发逻辑
print(f"[QN Forward] 千牛平台消息转发功能待实现: store_id={store_id}, recv_pin={recv_pin}, content={content}")
except Exception as e:
print(f"[QN Forward] 转发失败: {e}")
def _forward_to_pdd(self, store_id: str, recv_pin: str, content: str):
"""转发消息到拼多多平台"""
try:
from Utils.Pdd.PddUtils import WebsocketManager as PDDWSManager
pdd_mgr = PDDWSManager()
shop_key = f"拼多多:{store_id}"
entry = pdd_mgr.get_connection(shop_key)
if not entry:
print(f"[PDD Forward] 未找到连接: {shop_key}")
return
platform_info = entry.get('platform', {})
pdd_instance = platform_info.get('pdd_instance')
loop = platform_info.get('loop')
print(f"[PDD Forward] shop_key={shop_key} has_pdd_instance={bool(pdd_instance)} has_loop={bool(loop)} recv_pin={recv_pin}")
if pdd_instance and loop and content:
# 在拼多多实例的事件循环中发送消息
def send_in_loop():
try:
# 在事件循环中执行发送
future = asyncio.run_coroutine_threadsafe(
pdd_instance.send_message_external(recv_pin, content),
loop
)
# 等待结果
try:
result = future.result(timeout=10) # 拼多多可能需要更长时间
if result:
print(f"[PDD Forward] 已转发到平台: uid={recv_pin}, content_len={len(content)}")
else:
print(f"[PDD Forward] 转发失败: 拼多多实例返回False")
except Exception as fe:
print(f"[PDD Forward] 转发执行失败: {fe}")
except Exception as e:
print(f"[PDD Forward] 发送过程异常: {e}")
# 在新线程中执行发送操作
import threading
send_thread = threading.Thread(target=send_in_loop, daemon=True)
send_thread.start()
else:
print("[PDD Forward] 条件不足,未转发:",
{
'has_pdd_instance': bool(pdd_instance),
'has_loop': bool(loop),
'has_content': bool(content)
})
except Exception as e:
print(f"[PDD Forward] 转发失败: {e}")
def _transfer_to_pdd(self, customer_service_id: str, user_id: str, store_id: str):
"""执行拼多多平台转接操作"""
try:
from Utils.Pdd.PddUtils import WebsocketManager as PDDWSManager
pdd_mgr = PDDWSManager()
shop_key = f"拼多多:{store_id}"
entry = pdd_mgr.get_connection(shop_key)
if not entry:
print(f"[PDD Transfer] 未找到拼多多连接: {shop_key}")
return
platform_info = entry.get('platform', {})
pdd_instance = platform_info.get('pdd_instance')
loop = platform_info.get('loop')
print(f"[PDD Transfer] 找到拼多多连接,准备执行转接: user_id={user_id}, cs_id={customer_service_id}")
if pdd_instance and loop:
# 设置目标客服ID并执行转接
def transfer_in_loop():
try:
# 设置目标客服ID
pdd_instance.csid = customer_service_id
# 在事件循环中执行转接
future = asyncio.run_coroutine_threadsafe(
pdd_instance.handle_transfer_message({
"content": customer_service_id,
"receiver": {"id": user_id}
}),
loop
)
# 等待转接结果
try:
result = future.result(timeout=15) # 转接可能需要更长时间
if result:
print(f"[PDD Transfer] ✅ 转接成功: user_id={user_id} -> cs_id={customer_service_id}")
else:
print(f"[PDD Transfer] ❌ 转接失败: user_id={user_id}")
except Exception as fe:
print(f"[PDD Transfer] 转接执行失败: {fe}")
except Exception as e:
print(f"[PDD Transfer] 转接过程异常: {e}")
# 在新线程中执行转接操作
import threading
transfer_thread = threading.Thread(target=transfer_in_loop, daemon=True)
transfer_thread.start()
else:
print(f"[PDD Transfer] 条件不足: has_pdd_instance={bool(pdd_instance)}, has_loop={bool(loop)}")
except Exception as e:
print(f"[PDD Transfer] 拼多多转接失败: {e}")
def _handle_transfer(self, message: Dict[str, Any]):
"""处理转接消息"""
# 新版转接消息格式: {"type": "transfer", "content": "客服ID", "receiver": {"id": "用户ID"}, "store_id": "店铺ID"}
customer_service_id = message.get('content', '') # 目标客服ID
receiver_info = message.get('receiver', {})
user_id = receiver_info.get('id', '') # 用户ID
store_id = message.get('store_id', '')
print(f"转接消息: 顾客{user_id}已转接给客服{customer_service_id} (店铺: {store_id})")
# 根据店铺ID确定平台类型并执行转接
try:
platform_type = self._get_platform_by_store_id(store_id)
if platform_type == "京东":
# 京东转接逻辑 - 待实现
print(f"[JD Transfer] 京东平台转接功能待实现")
elif platform_type == "抖音":
# 抖音转接逻辑 - 待实现
print(f"[DY Transfer] 抖音平台转接功能待实现")
elif platform_type == "千牛":
# 千牛转接逻辑 - 待实现
print(f"[QN Transfer] 千牛平台转接功能待实现")
elif platform_type == "拼多多":
self._transfer_to_pdd(customer_service_id, user_id, store_id)
else:
print(f"[Transfer] 未知平台类型或未找到店铺: {platform_type}, store_id={store_id}")
except Exception as e:
print(f"执行转接操作失败: {e}")
# 保持旧版回调兼容性
if self.transfer_callback:
self.transfer_callback(customer_service_id, user_id, store_id)
def _handle_close(self, message: Dict[str, Any]):
"""处理店铺关闭"""
store_id = message.get('store_id', '')
print(f"店铺{store_id}已关闭")
if self.close_callback:
self.close_callback(store_id)
def _handle_pong(self, message: Dict[str, Any]):
"""处理心跳响应"""
uuid_received = message.get('uuid', '')
print(f"收到心跳响应: {uuid_received}")
def _handle_connect_success(self, message: Dict[str, Any]):
"""处理连接成功消息(旧版兼容)"""
print("后端连接成功(connect_success)")
if self.token:
self.get_store()
def _handle_login(self, message: Dict[str, Any]):
"""处理平台登录消息新版type=login, cookies, store_id, platform_name"""
cookies = message.get('cookies', '')
store_id = message.get('store_id', '')
platform_name = message.get('platform_name', '')
print(f"收到登录指令: 平台={platform_name}, 店铺={store_id}, cookies_len={len(cookies) if cookies else 0}")
if self.login_callback:
self.login_callback(platform_name, store_id, cookies)
def _handle_error_message(self, message: Dict[str, Any]):
"""处理错误消息"""
error_msg = message.get('error', '未知错误')
print(f"后端连接错误: {error_msg}")
if self.error_callback:
self.error_callback(error_msg, message)
def _handle_staff_list(self, message: Dict[str, Any]):
"""处理客服列表更新消息"""
staff_list = message.get('data', {}).get('staff_list', [])
store_id = message.get('store_id', '')
print(f"店铺{store_id}的客服列表已更新,共{len(staff_list)}个客服:")
for staff in staff_list:
pin = staff.get('pin', '')
nickname = staff.get('nickname', '')
print(f" - {nickname} ({pin})")
if self.customers_callback: # 假设客服列表更新也触发客服列表回调
self.customers_callback(staff_list, store_id)
# ==================== 辅助方法 ====================
def set_token(self, token: str):
"""设置或更新令牌"""
self.token = token
def get_connection_info(self) -> Dict[str, Any]:
"""获取连接信息"""
return {
'url': self.url,
'token': self.token,
'uuid': self.uuid,
'is_connected': self.is_connected
}
# 使用示例
if __name__ == '__main__':
pass
# import time
#
# def on_store_list(stores):
# print(f"回调: 收到{len(stores)}个店铺")
#
# def on_message(data, store_id):
# print(f"回调: 店铺{store_id}收到消息: {data.get('content')}")
#
# def on_error(error, message):
# print(f"回调: 发生错误: {error}")
#
# def on_login(platform_name, store_id, cookies):
# print(f"回调: 登录指令 平台={platform_name}, 店铺={store_id}, cookies_len={len(cookies) if cookies else 0}")
# # 此处触发对应平台的WS连接/更新cookies逻辑并将该平台WS与store_id绑定
#
# def on_success():
# print("回调: 后端连接成功")
#
# # 创建客户端(新版:单连接)
# client = BackendClient.from_exe_token("your_exe_token_here")
#
# # 设置回调
# client.set_callbacks(
# store_list=on_store_list,
# message=on_message,
# error=on_error,
# login=on_login,
# success=on_success
# )
#
# try:
# # 连接
# client.connect()
#
# # 等待连接
# time.sleep(2)
#
# # 发送心跳
# client.send_ping()
#
# # 保持运行
# while True:
# time.sleep(30)
# client.send_ping()
#
# except KeyboardInterrupt:
# print("用户中断")
# finally:
# client.disconnect()

9
WebSocket/__init__.py Normal file
View File

@@ -0,0 +1,9 @@
"""
WebSocket模块
处理WebSocket连接和消息传输
"""
from .BackendClient import BackendClient
__all__ = ['BackendClient']

View File

@@ -0,0 +1,347 @@
# backend_singleton.py
# 共享后端单连接客户端实例和WebSocket连接管理
from typing import Optional, Callable
import threading
import asyncio
from threading import Thread
# 创建新的后端客户端
from WebSocket.BackendClient import BackendClient
from Utils.JD.JdUtils import JDListenerForGUI as JDListenerForGUI_WS
from Utils.Dy.DyUtils import DouYinListenerForGUI as DYListenerForGUI_WS
from Utils.Pdd.PddUtils import PddListenerForGUI as PDDListenerForGUI_WS
_backend_client = None
def set_backend_client(client: BackendClient) -> None:
global _backend_client
_backend_client = client
def get_backend_client() -> Optional[BackendClient]:
return _backend_client
class WebSocketManager:
"""WebSocket连接管理器统一处理后端连接和平台监听"""
def __init__(self):
self.backend_client = None
self.platform_listeners = {} # 存储各平台的监听器
self.callbacks = {
'log': None,
'success': None,
'error': None
}
def set_callbacks(self, log: Callable = None, success: Callable = None, error: Callable = None):
"""设置回调函数"""
if log:
self.callbacks['log'] = log
if success:
self.callbacks['success'] = success
if error:
self.callbacks['error'] = error
def _log(self, message: str, level: str = "INFO"):
"""内部日志方法"""
if self.callbacks['log']:
self.callbacks['log'](message, level)
else:
print(f"[{level}] {message}")
def connect_backend(self, token: str) -> bool:
"""连接后端WebSocket"""
try:
# 1 保存token到配置
try:
from config import set_saved_token
set_saved_token(token)
except Exception:
pass
# 2 获取或创建后端客户端
backend = get_backend_client()
if backend:
# 3 如果有客户端更新token并重连
backend.set_token(token)
# 设置回调函数
def _on_backend_success():
try:
self._log("连接服务成功", "SUCCESS")
if self.callbacks['success']:
self.callbacks['success']()
except Exception as e:
self._log(f"成功回调执行失败: {e}", "ERROR")
def _on_backend_login(platform_name: str, store_id: str, cookies: str):
self._log(
f"收到后端登录指令: 平台={platform_name}, 店铺={store_id}, cookies_len={len(cookies) if cookies else 0}",
"INFO")
self._handle_platform_login(platform_name, store_id, cookies)
backend.set_callbacks(success=_on_backend_success, login=_on_backend_login)
if not backend.is_connected:
backend.connect()
self.backend_client = backend
self._log("令牌已提交已连接后端。等待后端下发平台cookies后自动连接平台...", "SUCCESS")
return True
else:
backend = BackendClient.from_exe_token(token)
def _on_backend_login(platform_name: str, store_id: str, cookies: str):
self._log(
f"收到后端登录指令: 平台={platform_name}, 店铺={store_id}, cookies_len={len(cookies) if cookies else 0}",
"INFO")
self._handle_platform_login(platform_name, store_id, cookies)
def _on_backend_success():
try:
self._log("连接服务成功", "SUCCESS")
if self.callbacks['success']:
self.callbacks['success']()
except Exception as e:
self._log(f"成功回调执行失败: {e}", "ERROR")
backend.set_callbacks(login=_on_backend_login, success=_on_backend_success)
backend.connect()
set_backend_client(backend)
self.backend_client = backend
self._log("已创建后端客户端并连接。等待后端下发平台cookies...", "SUCCESS")
return True
except Exception as e:
self._log(f"连接后端失败: {e}", "ERROR")
if self.callbacks['error']:
self.callbacks['error'](str(e))
return False
def _handle_platform_login(self, platform_name: str, store_id: str, cookies: str):
"""处理平台登录逻辑"""
try:
if platform_name == "京东" and store_id and cookies:
self._start_jd_listener(store_id, cookies)
elif platform_name == "抖音" and store_id and cookies:
self._start_douyin_listener(store_id, cookies)
elif platform_name == "千牛" and store_id and cookies:
self._start_qianniu_listener(store_id, cookies)
elif platform_name == "拼多多" and store_id and cookies:
self._start_pdd_listener(store_id, cookies)
else:
self._log(f"不支持的平台或参数不全: {platform_name}", "WARNING")
except Exception as e:
self._log(f"启动平台监听失败: {e}", "ERROR")
def _start_jd_listener(self, store_id: str, cookies: str):
"""启动京东平台监听"""
try:
def _runner():
try:
listener = JDListenerForGUI_WS()
asyncio.run(listener.start_with_cookies(store_id, cookies))
except Exception as e:
self._log(f"京东监听器运行异常: {e}", "ERROR")
# 在新线程中启动监听器
thread = Thread(target=_runner, daemon=True)
thread.start()
# 保存监听器引用
self.platform_listeners[f"京东:{store_id}"] = {
'thread': thread,
'platform': '京东',
'store_id': store_id
}
# 上报连接状态给后端
if self.backend_client:
try:
self.backend_client.send_message({
"type": "connect_message",
"store_id": store_id,
"status": True
})
except Exception as e:
self._log(f"上报连接状态失败: {e}", "WARNING")
self._log("已启动京东平台监听", "SUCCESS")
except Exception as e:
self._log(f"启动京东平台监听失败: {e}", "ERROR")
# 确保失败时也上报状态
if self.backend_client:
try:
self.backend_client.send_message({
"type": "connect_message",
"store_id": store_id,
"status": False
})
except Exception as send_e:
self._log(f"失败状态下报连接状态也失败: {send_e}", "ERROR")
def _start_douyin_listener(self, store_id: str, cookies: str):
"""启动抖音平台监听"""
try:
def _runner():
try:
import json
listener = DYListenerForGUI_WS()
# 将JSON字符串格式的cookies解析为字典
try:
cookie_dict = json.loads(cookies) if isinstance(cookies, str) else cookies
except json.JSONDecodeError as e:
self._log(f"❌ Cookie JSON解析失败: {e}", "ERROR")
return False
result = asyncio.run(listener.start_with_cookies(store_id=store_id, cookie_dict=cookie_dict))
return result
except Exception as e:
self._log(f"抖音监听器运行异常: {e}", "ERROR")
return False
# 在新线程中启动监听器
thread = Thread(target=_runner, daemon=True)
thread.start()
# 保存监听器引用
self.platform_listeners[f"抖音:{store_id}"] = {
'thread': thread,
'platform': '抖音',
'store_id': store_id,
}
# 更新监听器状态
if f"抖音:{store_id}" in self.platform_listeners:
self.platform_listeners[f"抖音:{store_id}"]['status'] = 'success'
# 上报连接状态给后端
if self.backend_client:
try:
self.backend_client.send_message({
"type": "connect_message",
"store_id": store_id,
"status": True
})
self._log("已上报抖音平台连接状态: 成功", "INFO")
except Exception as e:
self._log(f"上报抖音平台连接状态失败: {e}", "WARNING")
self._log("已启动抖音平台监听", "SUCCESS")
except Exception as e:
self._log(f"启动抖音平台监听失败: {e}", "ERROR")
# 确保失败时也上报状态
if self.backend_client:
try:
self.backend_client.send_message({
"type": "connect_message",
"store_id": store_id,
"status": False
})
except Exception as send_e:
self._log(f"失败状态下报抖音平台连接状态也失败: {send_e}", "ERROR")
def _start_qianniu_listener(self, store_id: str, cookies: str):
"""启动千牛平台监听"""
try:
# 这里可以添加千牛监听逻辑
self._log("千牛平台监听功能待实现", "INFO")
except Exception as e:
self._log(f"启动千牛平台监听失败: {e}", "ERROR")
def _start_pdd_listener(self, store_id: str, cookies: str):
"""启动拼多多平台监听"""
try:
def _runner():
try:
listener = PDDListenerForGUI_WS(log_callback=self._log)
result = asyncio.run(listener.start_with_cookies(store_id=store_id, cookies=cookies))
return result
except Exception as e:
self._log(f"拼多多监听器运行异常: {e}", "ERROR")
return False
# 在新线程中启动监听器
thread = Thread(target=_runner, daemon=True)
thread.start()
# 保存监听器引用
self.platform_listeners[f"拼多多:{store_id}"] = {
'thread': thread,
'platform': '拼多多',
'store_id': store_id,
}
# 更新监听器状态
if f"拼多多:{store_id}" in self.platform_listeners:
self.platform_listeners[f"拼多多:{store_id}"]['status'] = 'success'
# 上报连接状态给后端
if self.backend_client:
try:
self.backend_client.send_message({
"type": "connect_message",
"store_id": store_id,
"status": True
})
self._log("已上报拼多多平台连接状态: 成功", "INFO")
except Exception as e:
self._log(f"上报拼多多平台连接状态失败: {e}", "WARNING")
self._log("已启动拼多多平台监听", "SUCCESS")
except Exception as e:
self._log(f"启动拼多多平台监听失败: {e}", "ERROR")
# 确保失败时也上报状态
if self.backend_client:
try:
self.backend_client.send_message({
"type": "connect_message",
"store_id": store_id,
"status": False
})
except Exception as send_e:
self._log(f"失败状态下报拼多多平台连接状态也失败: {send_e}", "ERROR")
def send_message(self, message: dict):
"""发送消息到后端"""
if self.backend_client and self.backend_client.is_connected:
return self.backend_client.send_message(message)
else:
raise Exception("后端未连接")
def disconnect_all(self):
"""断开所有连接"""
try:
# 断开后端连接
if self.backend_client:
self.backend_client.disconnect()
self.backend_client = None
# 清理平台监听器
self.platform_listeners.clear()
self._log("所有连接已断开", "INFO")
except Exception as e:
self._log(f"断开连接时发生错误: {e}", "ERROR")
# 全局WebSocket管理器实例
_websocket_manager = None
def get_websocket_manager() -> WebSocketManager:
"""获取全局WebSocket管理器实例"""
global _websocket_manager
if _websocket_manager is None:
_websocket_manager = WebSocketManager()
return _websocket_manager

124
config.py Normal file
View File

@@ -0,0 +1,124 @@
# -*- coding: utf-8 -*-
"""
项目配置文件
统一管理所有配置参数,避免硬编码
"""
# 用户访问令牌(默认空字符串)
import os # 用于路径与目录操作(写入用户配置目录)
import json # 用于将令牌保存为 JSON 格式
# 后端服务器配置
# BACKEND_HOST = "192.168.5.155"
BACKEND_HOST = "test.shuidrop.com"
BACKEND_PORT = ""
BACKEND_BASE_URL = f"http://{BACKEND_HOST}:{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 = 20
WS_PING_TIMEOUT = 10
# 内存管理配置
MAX_PENDING_REPLIES = 100
CLEANUP_INTERVAL = 300 # 5分钟
FUTURE_TIMEOUT = 300 # 5分钟
# 终端日志配置(简化)
LOG_LEVEL = "INFO"
VERSION = "1.0"
# GUI配置
WINDOW_TITLE = "AI回复连接入口-V1.0"
# 平台特定配置
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_base_url': BACKEND_BASE_URL,
'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

387
main.py Normal file
View File

@@ -0,0 +1,387 @@
import sys
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QFont, QPalette, QColor
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
QHBoxLayout, QLabel, QPushButton, QLineEdit,
QTextEdit, QFrame, QDialog, QDialogButtonBox, QComboBox)
import config
from WebSocket.backend_singleton import get_websocket_manager
# 新增: 用户名密码输入对话框类
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.platform_combo_connected = False
self.last_login_time = 0
self.login_cooldown = 2 # 登录冷却时间(秒)
# 日志管理相关变量已删除
self.initUI()
# 设置当前平台为ComboBox的当前值
self.current_platform = self.platform_combo.currentText()
def initUI(self):
# 设置窗口基本属性
self.setWindowTitle('AI回复连接入口-V1.0')
self.setGeometry(300, 300, 800, 600) # 增大窗口尺寸
self.setMinimumSize(700, 500) # 设置最小尺寸保证显示完整
# 创建中央widget
central_widget = QWidget()
self.setCentralWidget(central_widget)
# 创建主布局
main_layout = QVBoxLayout()
main_layout.setSpacing(25) # 增加间距
main_layout.setContentsMargins(40, 40, 40, 40) # 增加边距
central_widget.setLayout(main_layout)
# 添加标题
title_label = QLabel('AI回复连接入口')
title_label.setAlignment(Qt.AlignCenter)
title_label.setFont(QFont('Microsoft YaHei', 24, QFont.Bold)) # 使用系统自带字体
title_label.setStyleSheet("color: #2c3e50;")
main_layout.addWidget(title_label)
# 添加分隔线
line = QFrame()
line.setFrameShape(QFrame.HLine)
line.setFrameShadow(QFrame.Sunken)
line.setStyleSheet("color: #bdc3c7;")
main_layout.addWidget(line)
# 在token_input之前添加平台选择区域
platform_layout = QHBoxLayout()
platform_label = QLabel("选择平台:")
self.platform_combo = QComboBox()
self.platform_combo.addItems(["JD", "抖音", "千牛(淘宝)", "拼多多"])
# 防止重复连接信号 - 更强的保护
if not hasattr(self, 'platform_combo_connected') or not self.platform_combo_connected:
try:
self.platform_combo.currentIndexChanged.disconnect()
except TypeError:
pass # 如果没有连接则忽略
self.platform_combo.currentIndexChanged.connect(self.on_platform_changed)
self.platform_combo_connected = True
platform_layout.addWidget(platform_label)
platform_layout.addWidget(self.platform_combo)
# 将platform_layout添加到主布局中在token_layout之前
main_layout.addLayout(platform_layout)
# 创建令牌输入区域
token_layout = QHBoxLayout()
token_layout.setSpacing(15)
token_label = QLabel('令牌:')
token_label.setFont(QFont('Microsoft YaHei', 12))
token_label.setFixedWidth(80)
self.token_input = QLineEdit()
self.token_input.setPlaceholderText('请输入您的访问令牌')
self.token_input.setEchoMode(QLineEdit.Password)
self.token_input.setFont(QFont('Microsoft YaHei', 11))
# noinspection PyUnresolvedReferences
self.token_input.returnPressed.connect(self.login) # 表示回车提交
self.token_input.setMinimumHeight(40) # 增加输入框高度
# 预填已保存的令牌(如果存在)
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', 12, QFont.Bold))
self.login_btn.setMinimumHeight(45) # 增大按钮高度
self.login_btn.clicked.connect(self.login) # 表示点击提交
main_layout.addWidget(self.login_btn)
# 添加分隔线
line = QFrame()
line.setFrameShape(QFrame.HLine)
line.setFrameShadow(QFrame.Sunken)
line.setStyleSheet("color: #bdc3c7;")
main_layout.addWidget(line)
# 日志框已永久删除,只使用终端输出
self.log_display = None
# 应用现代化样式
self.apply_modern_styles()
# 系统初始化日志输出到终端
print("[INFO] 系统初始化完成")
# # 在平台选择改变时添加调试日志
# self.platform_combo.currentIndexChanged.connect(self.on_platform_changed)
# 设置默认平台
self.platform_combo.setCurrentText("JD")
print(f"🔥 设置默认平台为: {self.platform_combo.currentText()}")
def apply_modern_styles(self):
"""应用现代化样式兼容各Windows版本"""
self.setStyleSheet("""
QMainWindow {
background-color: #f9f9f9;
}
QLabel {
color: #34495e;
}
QLineEdit {
padding: 10px;
border: 2px solid #dfe6e9;
border-radius: 6px;
font-size: 14px;
background-color: white;
selection-background-color: #3498db;
selection-color: white;
}
QLineEdit:focus {
border-color: #3498db;
}
QPushButton {
background-color: #3498db;
border: none;
color: white;
padding: 12px 24px;
border-radius: 6px;
font-size: 14px;
min-width: 120px;
}
QPushButton:hover {
background-color: #2980b9;
}
QPushButton:pressed {
background-color: #1a6a9c;
}
QPushButton:disabled {
background-color: #bdc3c7;
color: #7f8c8d;
}
QTextEdit {
border: 2px solid #dfe6e9;
border-radius: 6px;
background-color: white;
padding: 8px;
font-family: 'Consolas', 'Microsoft YaHei', monospace;
}
QFrame[frameShape="4"] {
color: #dfe6e9;
}
QGroupBox {
font-weight: bold;
border: 2px solid #dfe6e9;
border-radius: 6px;
margin-top: 10px;
padding-top: 10px;
}
QComboBox {
padding: 8px;
border: 2px solid #dfe6e9;
border-radius: 6px;
font-size: 14px;
background-color: white;
min-width: 100px;
}
QComboBox::drop-down {
border: none;
width: 20px;
}
QComboBox QAbstractItemView {
border: 2px solid #dfe6e9;
selection-background-color: #3498db;
selection-color: white;
}
""")
# 设置全局字体确保各Windows版本显示一致
font = QFont('Microsoft YaHei', 10) # Windows系统自带字体
QApplication.setFont(font)
# 设置调色板确保颜色一致性
palette = QPalette()
palette.setColor(QPalette.Window, QColor(249, 249, 249))
palette.setColor(QPalette.WindowText, QColor(52, 73, 94))
palette.setColor(QPalette.Base, QColor(255, 255, 255))
palette.setColor(QPalette.AlternateBase, QColor(233, 235, 239))
palette.setColor(QPalette.ToolTipBase, QColor(255, 255, 255))
palette.setColor(QPalette.ToolTipText, QColor(52, 73, 94))
palette.setColor(QPalette.Text, QColor(52, 73, 94))
palette.setColor(QPalette.Button, QColor(52, 152, 219))
palette.setColor(QPalette.ButtonText, QColor(255, 255, 255))
palette.setColor(QPalette.BrightText, QColor(255, 255, 255))
palette.setColor(QPalette.Highlight, QColor(52, 152, 219))
palette.setColor(QPalette.HighlightedText, QColor(255, 255, 255))
QApplication.setPalette(palette)
def on_platform_changed(self, index):
"""平台选择改变时的处理 - 新增方法"""
platform = self.platform_combo.currentText()
self.current_platform = platform
self.add_log(f"已选择平台: {platform}", "INFO")
print(f"🔥 平台已更改为: {platform}")
def add_log(self, message, log_type="INFO"):
"""添加日志信息 - 只输出到终端"""
from datetime import datetime
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# 根据日志类型设置颜色ANSI颜色码
colors = {
"ERROR": "\033[91m", # 红色
"SUCCESS": "\033[92m", # 绿色
"WARNING": "\033[93m", # 黄色
"INFO": "\033[94m", # 蓝色
"DEBUG": "\033[95m" # 紫色
}
reset = "\033[0m" # 重置颜色
color = colors.get(log_type, colors["INFO"])
log_entry = f"{color}[{timestamp}] [{log_type}] {message}{reset}"
print(log_entry)
def login(self):
"""处理连接逻辑"""
# 防止重复快速点击
import time
current_time = time.time()
if current_time - self.last_login_time < self.login_cooldown:
self.add_log(f"请等待 {self.login_cooldown} 秒后再试", "WARNING")
return
self.last_login_time = current_time
token = self.token_input.text().strip()
if not token:
self.add_log("请输入有效的连接令牌", "ERROR")
return
# 禁用连接按钮,防止重复点击
self.login_btn.setEnabled(False)
self.login_btn.setText("连接中...")
try:
# 使用 WebSocket 管理器处理连接
ws_manager = get_websocket_manager()
# 设置回调函数
ws_manager.set_callbacks(
log=self.add_log,
success=lambda: self.add_log("WebSocket连接管理器连接成功", "SUCCESS"),
error=lambda error: self.add_log(f"WebSocket连接管理器错误: {error}", "ERROR")
)
# 连接后端
success = ws_manager.connect_backend(token)
if success:
self.add_log("已启动WebSocket连接管理器", "SUCCESS")
else:
self.add_log("WebSocket连接管理器启动失败", "ERROR")
except Exception as e:
self.add_log(f"连接失败: {e}", "ERROR")
finally:
self.login_btn.setEnabled(True)
self.login_btn.setText("开始连接")
def verify_token(self, token):
"""简化的令牌校验:非空即通过(实际校验由后端承担)"""
return bool(token)
def closeEvent(self, event):
"""窗口关闭事件处理"""
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:
event.accept()
def main():
"""主函数,用于测试界面"""
app = QApplication(sys.argv)
# 设置应用程序属性
app.setApplicationName(config.WINDOW_TITLE)
app.setApplicationVersion(config.VERSION)
# 创建主窗口
window = LoginWindow()
window.show() # 程序启动断点
# 运行应用程序
sys.exit(app.exec_())
if __name__ == '__main__':
main() # sd_aAoHIO9fDRIkePZEhW6oaFgK6IzAPxuB 测试令牌(token)
# username = "KLD测试"
# password = "kld168168"
# taobao nickname = "tb420723827:redboat"

2558
static/js/data_encrypt.js Normal file

File diff suppressed because it is too large Load Diff

3717
static/js/dencode_message.js Normal file

File diff suppressed because it is too large Load Diff

1766
static/js/encode_message.js Normal file

File diff suppressed because it is too large Load Diff

77
static/js/get_uuid.js Normal file
View File

@@ -0,0 +1,77 @@
function n(n, o) {
var r = {bw: 1707, bh: 924}
, a = "https://sz.jd.com/szweb/sz/view/productAnalysis/productDetail.html"
,
d = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36"
,
c = "__jdv=122270672|direct|-|none|-|1692254561099; __jdu=1692254561098125451041; wlfstk_smdl=rn7jesxgevs0yntcfsrvjfjbf9aj2dsu; 3AB9D23F7A4B3C9B=BYLR4QCYOYVQIQXRCKH3XB4O527FC3NKDAZ5PPDA5IJXSVN4CP5EFUHUOGB5PH3EXMGZ22JDTYS4DYHMPBHQVEYMG4; TrackID=1PQ9rNMOyjMYOPORod42pcTTsm8qhbKU7seozK_5G2_kuSCeqHl8UUPY3sAceTY_7QTKUv5aNu389rgQyTIrARUZi05BEmI_p9bHTugk8-KU; pinId=MOUYfPJVXga1ffsfn98I-w; pin=%E4%B8%B8%E7%BE%8EIT; unick=%E4%B8%B8%E7%BE%8EIT; ceshi3.com=000; _tp=FSAH7V3VJiXcG2VMtstR1QteqtxBRgaiAg%2FWx4nnI2A%3D; b-sec=JFN3AQWC6KONG2C2JXN4V6Y3D5SHVFAJBDTEQIVPR3QXUB6C6YUBAQXVYUWZI4PW; is_sz_old_version=false; __jda=251704139.1692254561098125451041.1692254561.1692254561.1692254561.1; __jdc=251704139; __jdb=251704139.5.1692254561098125451041|1.1692254561"
, s = 1e9 * Math.random() << 0
, p = (new Date).getTime()
, m = t(e([r.bw + "" + r.bh, a, d, c, s, p, n].join("-"))).slice(20);
return o ? m : m + "-" + p.toString(16)
}
function t(n) {
for (var o = "", e = 0; e < n.length; e++) {
var t = n[e].toString(16);
o += t = 1 === t.length ? "0" + t : t
}
return o
}
function e(n) {
function o(n, o) {
return n << o | n >>> 32 - o
}
var e = [1518500249, 1859775393, 2400959708, 3395469782]
, t = [1732584193, 4023233417, 2562383102, 271733878, 3285377520];
if ("string" == typeof n) {
var i = unescape(encodeURIComponent(n));
n = new Array(i.length);
for (c = 0; c < i.length; c++)
n[c] = i.charCodeAt(c)
}
n.push(128);
for (var r = n.length / 4 + 2, a = Math.ceil(r / 16), d = new Array(a), c = 0; c < a; c++) {
d[c] = new Array(16);
for (var s = 0; s < 16; s++)
d[c][s] = n[64 * c + 4 * s] << 24 | n[64 * c + 4 * s + 1] << 16 | n[64 * c + 4 * s + 2] << 8 | n[64 * c + 4 * s + 3]
}
d[a - 1][14] = 8 * (n.length - 1) / Math.pow(2, 32),
d[a - 1][14] = Math.floor(d[a - 1][14]),
d[a - 1][15] = 8 * (n.length - 1) & 4294967295;
for (c = 0; c < a; c++) {
for (var p = new Array(80), m = 0; m < 16; m++)
p[m] = d[c][m];
for (m = 16; m < 80; m++)
p[m] = o(p[m - 3] ^ p[m - 8] ^ p[m - 14] ^ p[m - 16], 1);
for (var w = t[0], f = t[1], g = t[2], u = t[3], _ = t[4], m = 0; m < 80; m++) {
var h = Math.floor(m / 20)
, l = o(w, 5) + function (n, o, e, t) {
switch (n) {
case 0:
return o & e ^ ~o & t;
case 1:
return o ^ e ^ t;
case 2:
return o & e ^ o & t ^ e & t;
case 3:
return o ^ e ^ t
}
}(h, f, g, u) + _ + e[h] + p[m] >>> 0;
_ = u,
u = g,
g = o(f, 30) >>> 0,
f = w,
w = l
}
t[0] = t[0] + w >>> 0,
t[1] = t[1] + f >>> 0,
t[2] = t[2] + g >>> 0,
t[3] = t[3] + u >>> 0,
t[4] = t[4] + _ >>> 0
}
return [t[0] >> 24 & 255, t[0] >> 16 & 255, t[0] >> 8 & 255, 255 & t[0], t[1] >> 24 & 255, t[1] >> 16 & 255, t[1] >> 8 & 255, 255 & t[1], t[2] >> 24 & 255, t[2] >> 16 & 255, t[2] >> 8 & 255, 255 & t[2], t[3] >> 24 & 255, t[3] >> 16 & 255, t[3] >> 8 & 255, 255 & t[3], t[4] >> 24 & 255, t[4] >> 16 & 255, t[4] >> 8 & 255, 255 & t[4]]
}

3867
static/js/pwd_encrypt.js Normal file

File diff suppressed because it is too large Load Diff

1371
static/js/track_encrypt.js Normal file

File diff suppressed because it is too large Load Diff