拼多多集成
This commit is contained in:
13
.gitignore
vendored
Normal file
13
.gitignore
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
# 排除虚拟环境目录
|
||||
.venv/
|
||||
|
||||
# 排除test.py文件
|
||||
test.py
|
||||
|
||||
# 排除test1.py文件
|
||||
test1.py
|
||||
|
||||
# 排除.idea文件
|
||||
.idea
|
||||
|
||||
Utils/PythonNew32/
|
||||
BIN
.projectroot
Normal file
BIN
.projectroot
Normal file
Binary file not shown.
217
README.md
Normal file
217
README.md
Normal 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": "商品id:100123456789 订单号: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 下发策略
|
||||
|
||||
- 抖音、拼多多:直接使用后台请求携带的 cookie(pass-through)。
|
||||
- 京东、淘宝:由后端插件生成或获取(plugin),后台在店铺登录时通过本 WS 下发店铺级 `connect_success`(带 `store_id`)给 GUI。
|
||||
|
||||
1946
Utils/Dy/DyUtils.py
Normal file
1946
Utils/Dy/DyUtils.py
Normal file
File diff suppressed because it is too large
Load Diff
0
Utils/Dy/__init__.py
Normal file
0
Utils/Dy/__init__.py
Normal file
375
Utils/Dy/message_arg.py
Normal file
375
Utils/Dy/message_arg.py
Normal 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
981
Utils/JD/JdUtils.py
Normal 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
0
Utils/JD/__init__.py
Normal file
1178
Utils/Pdd/PddUtils.py
Normal 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
0
Utils/Pdd/__init__.py
Normal file
2322
Utils/QianNiu/QianNiuUtils.py
Normal file
2322
Utils/QianNiu/QianNiuUtils.py
Normal file
File diff suppressed because it is too large
Load Diff
0
Utils/QianNiu/__init__.py
Normal file
0
Utils/QianNiu/__init__.py
Normal file
0
Utils/__init__.py
Normal file
0
Utils/__init__.py
Normal file
170
Utils/message_models.py
Normal file
170
Utils/message_models.py
Normal 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
809
WebSocket/BackendClient.py
Normal 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
9
WebSocket/__init__.py
Normal file
@@ -0,0 +1,9 @@
|
||||
"""
|
||||
WebSocket模块
|
||||
处理WebSocket连接和消息传输
|
||||
"""
|
||||
|
||||
from .BackendClient import BackendClient
|
||||
|
||||
|
||||
__all__ = ['BackendClient']
|
||||
347
WebSocket/backend_singleton.py
Normal file
347
WebSocket/backend_singleton.py
Normal 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
124
config.py
Normal 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
387
main.py
Normal 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
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
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
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
77
static/js/get_uuid.js
Normal 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
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
1371
static/js/track_encrypt.js
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user