diff --git a/.gitea/workflows/gui-version-release.yml b/.gitea/workflows/gui-version-release.yml index 3dcc319..ff9b78b 100644 --- a/.gitea/workflows/gui-version-release.yml +++ b/.gitea/workflows/gui-version-release.yml @@ -135,6 +135,8 @@ jobs: - name: Build production executable if: success() shell: powershell + env: + PYTHONIOENCODING: utf-8 run: | Write-Host "=========================================="; Write-Host "Step 4.5: Build production executable"; diff --git a/Utils/Pdd/PddUtils.py b/Utils/Pdd/PddUtils.py index a2ade55..93a1a2e 100644 --- a/Utils/Pdd/PddUtils.py +++ b/Utils/Pdd/PddUtils.py @@ -1,3276 +1,3389 @@ -#!/usr/bin/env python -# -*- coding: UTF-8 -*- -""" -@Project :magua_project -@File :PddUtils.py -@IDE :PyCharm -@Author :lz -@Date :2025/7/17 16:44 -""" -import os -import sys -import threading -from concurrent.futures import ThreadPoolExecutor -import base64 -import json -import random -import time -import traceback - -import execjs -import websockets -import requests -import asyncio -# execjs 已移除 - 不再需要本地生成anti_content -from datetime import datetime -from typing import Dict, Any, Optional, Callable - -# 新增导入,用于登录功能 -import PIL, numpy as np, cv2 -from Crypto.PublicKey import RSA -from Crypto.Cipher import PKCS1_v1_5, AES -from Crypto.Util.Padding import pad -from pathlib import Path -from loguru import logger - -from Utils.message_models import PlatformMessage - - -# ===== 登录相关类集成开始 ===== - -class Track: - """滑块轨迹生成类""" - - @staticmethod - def get_track(distance): - distance = int(distance) + random.randint(7, 11) + 0.8 - track_list = [ - [ - 0.8, - 178.4, - 1733388719067 - ], - [ - 4.8, - 179.4, - 1733388719169 - ], - [ - 6.8, - 179.4, - 1733388719184 - ], - [ - 8.8, - 179.4, - 1733388719250 - ], - [ - 8.8, - 179.4, - 1733388719266 - ], - [ - 9.8, - 180.4, - 1733388719762 - ], - [ - 10.8, - 180.4, - 1733388719914 - ], - [ - 11.8, - 180.4, - 1733388719930 - ], - [ - 11.8, - 180.4, - 1733388719954 - ], - [ - 12.8, - 180.4, - 1733388719982 - ], - [ - 12.8, - 180.4, - 1733388720030 - ], - [ - 13.8, - 180.4, - 1733388720058 - ], - [ - 14.8, - 180.4, - 1733388720090 - ], - [ - 15.8, - 180.4, - 1733388720110 - ], - [ - 16.8, - 180.4, - 1733388720170 - ], - [ - 17.8, - 180.4, - 1733388720202 - ], - [ - 19.8, - 180.4, - 1733388720218 - ], - [ - 22.8, - 181.4, - 1733388720258 - ], - [ - 23.8, - 181.4, - 1733388720319 - ], - [ - 24.8, - 181.4, - 1733388720428 - ], - [ - 24.8, - 182.4, - 1733388720458 - ], - [ - 26.8, - 182.4, - 1733388720478 - ], - [ - 28.8, - 182.4, - 1733388720495 - ], - [ - 30.8, - 182.4, - 1733388720511 - ], - [ - 32.8, - 183.4, - 1733388720527 - ], - [ - 33.8, - 183.4, - 1733388720558 - ], - [ - 35.8, - 183.4, - 1733388720574 - ], - [ - 36.8, - 184.4, - 1733388720591 - ], - [ - 38.8, - 184.4, - 1733388720619 - ], - [ - 39.8, - 184.4, - 1733388720662 - ], - [ - 40.8, - 184.4, - 1733388720686 - ], - [ - 41.8, - 184.4, - 1733388720710 - ], - [ - 42.8, - 184.4, - 1733388720742 - ], - [ - 43.8, - 184.4, - 1733388720758 - ], - [ - 45.8, - 184.4, - 1733388720783 - ], - [ - 46.8, - 184.4, - 1733388720798 - ], - [ - 48.8, - 184.4, - 1733388720831 - ], - [ - 49.8, - 184.4, - 1733388720850 - ], - [ - 50.8, - 184.4, - 1733388720874 - ], - [ - 52.8, - 184.4, - 1733388720922 - ], - [ - 53.8, - 184.4, - 1733388720958 - ], - [ - 55.8, - 184.4, - 1733388720974 - ], - [ - 55.8, - 184.4, - 1733388720990 - ], - [ - 56.8, - 184.4, - 1733388721010 - ], - [ - 57.8, - 184.4, - 1733388721051 - ], - [ - 58.8, - 184.4, - 1733388721074 - ], - [ - 58.8, - 185.4, - 1733388721090 - ], - [ - 59.8, - 185.4, - 1733388721106 - ], - [ - 60.8, - 185.4, - 1733388721214 - ], - [ - 60.8, - 185.4, - 1733388721270 - ], - [ - 61.8, - 185.4, - 1733388721361 - ], - [ - 63.8, - 186.4, - 1733388721374 - ], - [ - 68.8, - 186.4, - 1733388721390 - ], - [ - 71.8, - 187.4, - 1733388721406 - ], - [ - 72.8, - 187.4, - 1733388721422 - ], - [ - 73.8, - 187.4, - 1733388721442 - ], - [ - 74.8, - 187.4, - 1733388721602 - ], - [ - 75.8, - 187.4, - 1733388721650 - ], - [ - 79.8, - 187.4, - 1733388721674 - ], - [ - 81.8, - 187.4, - 1733388721690 - ], - [ - 83.8, - 188.4, - 1733388721726 - ], - [ - 83.8, - 188.4, - 1733388722055 - ], - [ - 91.8, - 188.4, - 1733388722241 - ], - [ - 92.8, - 188.4, - 1733388722346 - ], - [ - 92.8, - 188.4, - 1733388722494 - ], - [ - 94.8, - 188.4, - 1733388722511 - ], - [ - 97.8, - 188.4, - 1733388722527 - ], - [ - 101.8, - 189.4, - 1733388722543 - ], - [ - 102.8, - 189.4, - 1733388722566 - ], - [ - 103.8, - 189.4, - 1733388722666 - ], - [ - 104.8, - 189.4, - 1733388722722 - ], - [ - 106.8, - 190.4, - 1733388722742 - ], - [ - 108.8, - 190.4, - 1733388722759 - ], - [ - 112.8, - 190.4, - 1733388722775 - ], - [ - 115.8, - 191.4, - 1733388722791 - ], - [ - 116.8, - 191.4, - 1733388722807 - ], - [ - 118.8, - 192.4, - 1733388722911 - ], - [ - 119.8, - 192.4, - 1733388722931 - ], - [ - 120.8, - 192.4, - 1733388722962 - ], - [ - 123.8, - 192.4, - 1733388722987 - ], - [ - 124.8, - 192.4, - 1733388723006 - ], - [ - 125.8, - 192.4, - 1733388723023 - ], - [ - 127.8, - 192.4, - 1733388723098 - ], - [ - 128.8, - 192.4, - 1733388723186 - ], - [ - 129.8, - 193.4, - 1733388723258 - ], - [ - 132.8, - 193.4, - 1733388723278 - ], - [ - 135.8, - 193.4, - 1733388723294 - ], - [ - 137.8, - 194.4, - 1733388723318 - ], - [ - 138.8, - 194.4, - 1733388723562 - ], - [ - 140.8, - 194.4, - 1733388723582 - ], - [ - 144.8, - 195.4, - 1733388723599 - ], - [ - 145.8, - 195.4, - 1733388723622 - ], - [ - 147.8, - 196.4, - 1733388723662 - ], - [ - 148.8, - 196.4, - 1733388723678 - ], - [ - 149.8, - 196.4, - 1733388723703 - ], - [ - 152.8, - 196.4, - 1733388723719 - ], - [ - 153.8, - 196.4, - 1733388723774 - ], - [ - 154.8, - 196.4, - 1733388723846 - ], - [ - 155.8, - 196.4, - 1733388723886 - ], - [ - 157.8, - 196.4, - 1733388723903 - ], - [ - 161.8, - 197.4, - 1733388723918 - ], - [ - 166.8, - 197.4, - 1733388723934 - ], - [ - 167.8, - 197.4, - 1733388723998 - ], - [ - 168.8, - 197.4, - 1733388724026 - ], - [ - 168.8, - 197.4, - 1733388724123 - ], - [ - 171.8, - 198.4, - 1733388724139 - ], - [ - 175.8, - 199.4, - 1733388724154 - ], - [ - 177.8, - 199.4, - 1733388724170 - ], - [ - 179.8, - 199.4, - 1733388724186 - ], - [ - 181.8, - 199.4, - 1733388724202 - ], - [ - 184.8, - 199.4, - 1733388724218 - ], - [ - 185.8, - 199.4, - 1733388724234 - ], - [ - 186.8, - 199.4, - 1733388724255 - ], - [ - 187.8, - 199.4, - 1733388724278 - ], - [ - 188.8, - 199.4, - 1733388724294 - ], - [ - 188.8, - 199.4, - 1733388724326 - ], - [ - 189.8, - 199.4, - 1733388724374 - ], - [ - 191.8, - 199.4, - 1733388724399 - ], - [ - 192.8, - 200.4, - 1733388724444 - ], - [ - 193.8, - 200.4, - 1733388724486 - ], - [ - 194.8, - 200.4, - 1733388724834 - ], - [ - 195.8, - 200.4, - 1733388724850 - ], - [ - 196.8, - 200.4, - 1733388724866 - ], - [ - 198.8, - 200.4, - 1733388724882 - ], - [ - 199.8, - 200.4, - 1733388724898 - ], - [ - 200.8, - 201.4, - 1733388725242 - ], - [ - 204.8, - 201.4, - 1733388725276 - ], - [ - 206.8, - 201.4, - 1733388725301 - ], - [ - 207.8, - 201.4, - 1733388725332 - ], - [ - 208.8, - 201.4, - 1733388725522 - ], - [ - 211.8, - 201.4, - 1733388725547 - ], - [ - 212.8, - 201.4, - 1733388725586 - ], - [ - 212.8, - 201.4, - 1733388725838 - ], - [ - 214.8, - 201.4, - 1733388725854 - ], - [ - 219.8, - 202.4, - 1733388725878 - ], - [ - 220.8, - 202.4, - 1733388725894 - ], - [ - 222.8, - 202.4, - 1733388726427 - ], - [ - 227.8, - 202.4, - 1733388726446 - ], - [ - 228.8, - 202.4, - 1733388726462 - ], - [ - 230.8, - 203.4, - 1733388726642 - ], - [ - 232.8, - 203.4, - 1733388726658 - ], - [ - 234.8, - 203.4, - 1733388726675 - ], - [ - 236.8, - 203.4, - 1733388726690 - ], - [ - 237.8, - 203.4, - 1733388726742 - ], - [ - 238.8, - 203.4, - 1733388726934 - ], - [ - 240.8, - 203.4, - 1733388726950 - ], - [ - 243.8, - 203.4, - 1733388726966 - ], - [ - 245.8, - 203.4, - 1733388726982 - ], - [ - 247.8, - 203.4, - 1733388727118 - ], - [ - 248.8, - 203.4, - 1733388727174 - ], - [ - 248.8, - 204.4, - 1733388727190 - ], - [ - 250.8, - 204.4, - 1733388727214 - ], - [ - 252.8, - 204.4, - 1733388727286 - ], - [ - 252.8, - 204.4, - 1733388727302 - ], - [ - 253.8, - 204.4, - 1733388727322 - ], - [ - 254.8, - 204.4, - 1733388727358 - ], - [ - 256.8, - 204.4, - 1733388727398 - ] - ] - # 检查value是否在轨迹的x值中 - for trajectory in track_list: - if trajectory[0] == distance: - # 如果找到,截取从轨迹开始到该点的子数组 - return [t for t in track_list if t[0] <= distance] - - # 如果value不在x值中,找到最接近value的x值 - closest_x = None - min_diff = float('inf') - for trajectory in track_list: - if abs(trajectory[0] - distance) < min_diff: - min_diff = abs(trajectory[0] - distance) - closest_x = trajectory[0] - - # 截取从轨迹开始到最接近的x值的子数组 - result = [t for t in track_list if t[0] <= closest_x] - result[-1][0] = distance - return result - - -class ImgDistance: - """滑块图像识别距离计算类""" - - def __init__(self, bg, tp): - self.bg = bg - self.tp = tp - - @staticmethod - def imshow(img, winname='test', delay=0): - """cv2展示图片""" - cv2.imshow(winname, img) - cv2.waitKey(delay) - cv2.destroyAllWindows() - - @staticmethod - def pil_to_cv2(img): - """ - pil转cv2图片 - :param img: pil图像, - :return: cv2图像, - """ - img = cv2.cvtColor(np.asarray(img), cv2.COLOR_RGB2BGR) - return img - - @staticmethod - def bytes_to_cv2(img): - """ - 二进制图片转cv2 - :param img: 二进制图片数据, - :return: cv2图像, - """ - # 将图片字节码bytes, 转换成一维的numpy数组到缓存中 - img_buffer_np = np.frombuffer(img, dtype=np.uint8) - # 从指定的内存缓存中读取一维numpy数据, 并把数据转换(解码)成图像矩阵格式 - img_np = cv2.imdecode(img_buffer_np, 1) - return img_np - - def cv2_open(self, img, flag=None): - """ - 统一输出图片格式为cv2图像, - :param img: - :param flag: 颜色空间转换类型, default: None - eg: cv2.COLOR_BGR2GRAY(灰度图) - :return: cv2图像, - """ - if isinstance(img, bytes): - img = self.bytes_to_cv2(img) - elif isinstance(img, (str, Path)): - img = cv2.imread(str(img)) - elif isinstance(img, np.ndarray): - img = img - elif isinstance(img, PIL.Image.Image): - img = self.pil_to_cv2(img) - else: - raise ValueError(f'输入的图片类型无法解析: {type(img)}') - if flag is not None: - img = cv2.cvtColor(img, flag) - return img - - def get_distance(self, bg, tp, im_show=False, save_path=None): - """ - :param bg: 背景图路径或Path对象或图片二进制 - eg: 'assets/bg.jpg' - Path('assets/bg.jpg') - :param tp: 缺口图路径或Path对象或图片二进制 - eg: 'assets/tp.jpg' - Path('assets/tp.jpg') - :param im_show: 是否显示结果, ; default: False - :param save_path: 保存路径, ; default: None - :return: 缺口位置 - """ - # 读取图片 - bg_img = self.cv2_open(bg) - tp_gray = self.cv2_open(tp, flag=cv2.COLOR_BGR2GRAY) - - # 金字塔均值漂移 - bg_shift = cv2.pyrMeanShiftFiltering(bg_img, 5, 50) - - # 边缘检测 - tp_gray = cv2.Canny(tp_gray, 255, 255) - bg_gray = cv2.Canny(bg_shift, 255, 255) - - # 目标匹配 - result = cv2.matchTemplate(bg_gray, tp_gray, cv2.TM_CCOEFF_NORMED) - # 解析匹配结果 - min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(result) - - distance = max_loc[0] - if save_path or im_show: - # 需要绘制的方框高度和宽度 - tp_height, tp_width = tp_gray.shape[:2] - # 矩形左上角点位置 - x, y = max_loc - # 矩形右下角点位置 - _x, _y = x + tp_width, y + tp_height - # 绘制矩形 - bg_img = self.cv2_open(bg) - cv2.rectangle(bg_img, (x, y), (_x, _y), (0, 0, 255), 2) - # 保存缺口识别结果到背景图 - if save_path: - cv2.imwrite(save_path, bg_img) - # 显示缺口识别结果 - if im_show: - self.imshow(bg_img) - return max_loc[0] - - @staticmethod - def decrypt_img(imgstr): - res = [] - s = [-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 24, 3, -1, 20, -1, 17, - 8, - -1, 30, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 12, 22, 10, -1, -1, 15, 14, 6, -1, 5, -1, -1, 7, - 18, - -1, 25, 9, -1, 28, -1, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 1, 21, -1, 31, 13, 16, -1, 26, -1, 27, - -1, 0, - 19, -1, 11, 4, -1, -1, 23, -1, 29, -1, -1, -1, -1, -1, -1] - for r in range(0, len(imgstr), 8): - o = s[ord(imgstr[r])] - i = s[ord(imgstr[r + 1])] - a = s[ord(imgstr[r + 2])] - c = s[ord(imgstr[r + 3])] - u = s[ord(imgstr[r + 4])] - d = s[ord(imgstr[r + 5])] - f = s[ord(imgstr[r + 6])] - p = (31 & o) << 3 | (31 & i) >> 2 - h = (3 & i) << 6 | (31 & a) << 1 | (31 & c) >> 4 - m = (15 & c) << 4 | (31 & u) >> 1 - v = (1 & u) << 7 | (31 & d) << 2 | (31 & f) >> 3 - g = (7 & f) << 5 | 31 & s[ord(imgstr[r + 7])] - res.append(chr(((31 & p) << 3 | h >> 5))) - res.append(chr((31 & h) << 3 | m >> 5)) - res.append(chr((31 & m) << 3 | v >> 5)) - res.append(chr((31 & v) << 3 | g >> 5)) - res.append(chr((31 & g) << 3 | p >> 5)) - y = ''.join(res) - for c in ['#', '@?', '*&%', '<$|>']: - y = y.replace(c, '') - return y.replace('data:image/png;base64,', '') - - def main(self, save_path=None): - return self.get_distance( - bg=base64.urlsafe_b64decode(self.decrypt_img(self.bg)), - tp=base64.urlsafe_b64decode(self.decrypt_img(self.tp)), - save_path=save_path - ) - - # AutiContent类已移除 - 后端会提供所有必要的anti_content - - def gzip_compress(self, data): - """压缩数据""" - try: - if self.ctx: - result = self.ctx.call("gzipCompress", data) - logger.info("成功压缩数据") - return result - except Exception as e: - logger.error(f"调用gzipCompress函数失败: {e}") - - # fallback处理:简单返回原数据 - logger.warning("使用fallback处理,返回原数据") - return data - - -class EncryptTool: - """加密工具类""" - - @staticmethod - def aes_encrypt(text, key, iv): - cipher = AES.new(key.encode('utf-8'), AES.MODE_CBC, iv.encode('utf-8')) - encrypted_text = cipher.encrypt(pad(text.encode('utf-8'), AES.block_size)) - return base64.b64encode(encrypted_text).decode('utf-8') - - @staticmethod - def rsa_encrypt(data): - public_key = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC6zHXNom934tsG9SC73vAUv99bIuSRVaLsuTMY+OL6aS6eB7AuNoU+m9gPCrI7aFrT7CSiKTJ47DNwCNZO52AlLzvB6TjdUwuIXWpinE8VCsYAZgOCrx+mK9Sy0OuwnqNj5D2wUdGoN0nxbl1q2akeAa18A/iBpiXx0SZQexbEowIDAQAB" - rsa_key = RSA.import_key(base64.b64decode(public_key)) # 导入读取到的公钥 - cipher = PKCS1_v1_5.new(rsa_key) # 生成对象 - cipher_text = base64.b64encode(cipher.encrypt(data.encode(encoding="utf-8"))) - return cipher_text.decode("utf-8") - - @staticmethod - def get_key_iv(salt): - t = {'aes_key': r"bN3%cH2$H1@*jCo$", 'aes_iv': r"gl3-w^dN)3#h6E1%"} - if not salt or 9 != len(salt): - return t - else: - n = salt[:1] - r = salt[1:] - o = r[:4] - i = r[4:] - a = list(i) - s = t['aes_key'][:8] if n in ["a", "b"] else t['aes_iv'][:8] - c = 'aes_key' if n in ["a", "b"] else 'aes_iv' - l = '' - if n == 'c': - t[c] = s + r - elif n == 'd': - for u in range(4): - l += a[int(o[u])] - t[c] = s + l + i - return t - - -class PddLogin: - """拼多多登录核心类""" - - def __init__(self, log_callback=None): - super().__init__() - self.login_api = "https://mms.pinduoduo.com/janus/api/auth" - self.headers = { - "authority": "apiv2.pinduoduo.net", - "accept": "*/*", - "accept-language": "zh-CN,zh;q=0.9", - "cache-control": "no-cache", - "content-type": "application/json;charset=UTF-8", - "origin": "https://mms.pinduoduo.com", - "pragma": "no-cache", - "referer": "https://mms.pinduoduo.com/", - "sec-ch-ua": "\"Not_A Brand\";v=\"8\", \"Chromium\";v=\"120\", \"Google Chrome\";v=\"120\"", - "sec-ch-ua-mobile": "?0", - "sec-ch-ua-platform": "\"Windows\"", - "sec-fetch-dest": "empty", - "sec-fetch-mode": "cors", - "sec-fetch-site": "cross-site", - "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" - } - self.verify_auth_token = None - self.cookies = dict() - self.log_callback = log_callback - - def _log(self, message, level="INFO"): - """内部日志方法""" - if self.log_callback: - self.log_callback(message, level) - else: - print(f"[{level}] {message}") - - # 滑块参数生成 - def captcha_collect(self, salt, track_list): - ts = int(round(time.time() * 1000)) - captcha_collect_data = json.dumps({ - "v": "a", - "ts": ts + random.randint(5700, 5950), - "t1": ts, - "t2": ts + random.randint(15700, 19950), - "tp": 3, - "ua": self.headers["user-agent"], - "rf": "", - "platform": 1, - "hl": "000000000001010", - "sc": { - "w": 1536, - "h": 824 - }, - "ihs": 1, - "imageSize": { - "width": 272, - "height": 198 - }, - "del": [ - [ - 0.8, - 177.4, - 1733388719033 - ] - ], - "mel": track_list, - "uel": [track_list[-1]], - "mell": [ - [ - [ - 250.8, - 187.4, - 1733388711350 - ], - [ - 248.8, - 187.4, - 1733388711366 - ], - [ - 240.8, - 187.4, - 1733388711382 - ], - [ - 233.8, - 186.4, - 1733388711398 - ], - [ - 225.8, - 185.4, - 1733388711414 - ], - [ - 222.8, - 184.4, - 1733388711430 - ], - [ - 219.8, - 184.4, - 1733388711446 - ], - [ - 216.8, - 184.4, - 1733388711462 - ], - [ - 208.8, - 183.4, - 1733388711478 - ], - [ - 202.8, - 182.4, - 1733388711494 - ], - [ - 192.8, - 182.4, - 1733388711511 - ], - [ - 168.8, - 180.4, - 1733388711534 - ], - [ - 160.8, - 180.4, - 1733388711550 - ], - [ - 159.8, - 180.4, - 1733388711630 - ], - [ - 156.8, - 180.4, - 1733388711670 - ], - [ - 152.8, - 180.4, - 1733388711686 - ], - [ - 146.8, - 180.4, - 1733388711702 - ], - [ - 136.8, - 180.4, - 1733388711718 - ], - [ - 128.8, - 180.4, - 1733388711734 - ], - [ - 124.8, - 180.4, - 1733388711750 - ], - [ - 118.8, - 180.4, - 1733388711766 - ], - [ - 110.8, - 180.4, - 1733388711782 - ], - [ - 100.8, - 180.4, - 1733388711798 - ], - [ - 91.8, - 180.4, - 1733388711814 - ], - [ - 83.8, - 180.4, - 1733388711830 - ], - [ - 75.8, - 180.4, - 1733388711846 - ], - [ - 70.8, - 180.4, - 1733388711862 - ], - [ - 68.8, - 180.4, - 1733388711878 - ], - [ - 66.8, - 180.4, - 1733388711910 - ], - [ - 64.8, - 180.4, - 1733388711926 - ], - [ - 58.8, - 180.4, - 1733388711942 - ], - [ - 51.8, - 180.4, - 1733388711959 - ], - [ - 42.8, - 180.4, - 1733388711975 - ], - [ - 35.8, - 180.4, - 1733388711994 - ], - [ - 33.8, - 180.4, - 1733388712054 - ], - [ - 32.8, - 180.4, - 1733388712278 - ], - [ - 31.8, - 182.4, - 1733388712295 - ], - [ - 30.8, - 182.4, - 1733388712310 - ], - [ - 28.8, - 182.4, - 1733388712479 - ], - [ - 27.8, - 182.4, - 1733388712502 - ], - [ - 26.8, - 182.4, - 1733388712518 - ], - [ - 24.8, - 182.4, - 1733388712550 - ], - [ - 26.8, - 182.4, - 1733388712886 - ], - [ - 31.8, - 181.4, - 1733388712902 - ], - [ - 35.8, - 180.4, - 1733388712918 - ], - [ - 37.8, - 179.4, - 1733388712934 - ], - [ - 38.8, - 179.4, - 1733388712950 - ], - [ - 44.8, - 179.4, - 1733388712966 - ], - [ - 55.8, - 179.4, - 1733388712986 - ], - [ - 88.8, - 178.4, - 1733388712999 - ], - [ - 164.8, - 172.4, - 1733388713014 - ], - [ - 255.8, - 165.4, - 1733388713030 - ], - [ - 346.8, - 157.4, - 1733388713046 - ], - [ - 360.8, - 161.4, - 1733388715672 - ], - [ - 340.8, - 161.4, - 1733388715687 - ], - [ - 320.8, - 164.4, - 1733388715702 - ], - [ - 287.8, - 168.4, - 1733388715718 - ], - [ - 240.8, - 176.4, - 1733388715735 - ], - [ - 184.8, - 182.4, - 1733388715750 - ], - [ - 144.8, - 184.4, - 1733388715766 - ], - [ - 112.8, - 186.4, - 1733388715782 - ], - [ - 98.8, - 186.4, - 1733388715798 - ], - [ - 95.8, - 186.4, - 1733388715926 - ], - [ - 93.8, - 186.4, - 1733388715942 - ], - [ - 89.8, - 187.4, - 1733388715958 - ], - [ - 79.8, - 188.4, - 1733388715983 - ], - [ - 57.8, - 192.4, - 1733388715999 - ], - [ - 44.8, - 192.4, - 1733388716015 - ], - [ - 36.8, - 192.4, - 1733388716030 - ], - [ - 11.8, - 188.4, - 1733388716073 - ], - [ - -0.2, - 184.4, - 1733388716134 - ], - [ - -1.2, - 184.4, - 1733388716191 - ], - [ - -2.2, - 184.4, - 1733388716206 - ], - [ - -2.2, - 182.4, - 1733388716318 - ], - [ - -0.2, - 181.4, - 1733388716342 - ], - [ - 0.8, - 179.4, - 1733388716878 - ], - [ - 2.8, - 177.4, - 1733388716894 - ], - [ - 3.8, - 177.4, - 1733388716910 - ], - [ - 4.8, - 176.4, - 1733388716926 - ], - [ - 3.8, - 176.4, - 1733388717299 - ], - [ - 2.8, - 176.4, - 1733388717646 - ], - [ - 1.8, - 176.4, - 1733388717662 - ], - [ - 0.8, - 176.4, - 1733388717699 - ], - [ - -0.2, - 176.4, - 1733388717838 - ], - [ - -0.2, - 177.4, - 1733388718115 - ], - [ - 0.8, - 177.4, - 1733388718490 - ] - ], - track_list - ] - }, separators=(',', ':')) - captcha_collect_gzip = self.gzip_compress(captcha_collect_data) - aes_key_iv = EncryptTool.get_key_iv(salt=salt) - captcha_collect = EncryptTool.aes_encrypt(captcha_collect_gzip, aes_key_iv['aes_key'], aes_key_iv['aes_iv']) - return captcha_collect - - # 获取滑块加密参数key和iv - def vc_pre_ck_b(self, backend_anti_content): - self.headers.pop("anti-content", None) - url = "https://apiv2.pinduoduo.net/api/phantom/vc_pre_ck_b" - payload = { - "anti_content": backend_anti_content, - "verify_auth_token": self.verify_auth_token, - "sdk_type": 3, - "client_time": int(round(time.time() * 1000)) - } - response = requests.post(url, headers=self.headers, json=payload, cookies=self.cookies) - self.cookies.update(response.cookies.get_dict()) - return response.json().get("salt") - - # 获取滑块图片 - def obtain_captcha(self, backend_anti_content): - url = "https://apiv2.pinduoduo.net/api/phantom/obtain_captcha" - payload = { - "anti_content": backend_anti_content, - "verify_auth_token": self.verify_auth_token, - "captcha_collect": "" - } - response = requests.post(url, headers=self.headers, json=payload, cookies=self.cookies) - self.cookies.update(response.cookies.get_dict()) - return response.json().get("pictures") - - # 滑块验证 - def verify(self, captcha_collect, verify_code, backend_anti_content): - url = "https://apiv2.pinduoduo.net/api/phantom/user_verify" - payload = { - "verify_code": verify_code, - "captcha_collect": captcha_collect, - "verify_auth_token": self.verify_auth_token, - "anti_content": backend_anti_content - } - response = requests.post(url, headers=self.headers, json=payload, cookies=self.cookies) - self.cookies.update(response.cookies.get_dict()) - result = response.json().get("result") - self._log(f"滑块验证结果: {response.text}") - return result - - # 发送验证码通知给后端,并获取验证码 - def request_verification_code(self, username, store_id, backend_anti_content, phone_number=None): - """向后端请求获取手机验证码""" - self._log(f"开始请求验证码,用户名: {username}, 店铺ID: {store_id}, 手机号: {phone_number}", "INFO") - - # 使用后端下发的anti_content - anti_content = backend_anti_content - self._log(f"使用后端下发的anti_content", "DEBUG") - - self.headers["anti-content"] = anti_content - url = "https://mms.pinduoduo.com/janus/api/user/getLoginVerificationCode" - payload = { - "username": username, - "crawlerInfo": anti_content - } - response = requests.post(url, headers=self.headers, json=payload, cookies=self.cookies) - self._log(f"发送验证码请求结果: {response.text}") - - # 发送消息给后端,告知需要验证码(包含手机号) - self._log("准备向后端发送验证码需求通知", "INFO") - self._send_verification_needed_message(store_id, phone_number) - - # 这里需要等待后端重新下发包含验证码的登录参数 - # 实际实现中这个方法会在接收到新的登录参数后被重新调用 - return None - - def _send_verification_needed_message(self, store_id, phone_number=None): - """向后端发送需要验证码的通知""" - try: - self._log(f"开始发送验证码需求通知,店铺ID: {store_id}, 手机号: {phone_number}", "INFO") - from WebSocket.backend_singleton import get_backend_client - backend = get_backend_client() - self._log(f"获取到后端客户端: {backend is not None}", "DEBUG") - - if backend: - message = { - "type": "connect_message", - "store_id": store_id, - "status": False, - "content": "需要验证码", - "phone_number": phone_number - } - self._log(f"准备发送验证码通知消息: {message}", "DEBUG") - backend.send_message(message) - self._log("✅ 成功向后端发送验证码需求通知(含手机号)", "SUCCESS") - else: - self._log("❌ 后端客户端为空,无法发送验证码需求通知", "ERROR") - except Exception as e: - self._log(f"❌ 发送验证码需求通知失败: {e}", "ERROR") - import traceback - self._log(f"错误详情: {traceback.format_exc()}", "DEBUG") - - def _send_verification_error_message(self, store_id, error_msg): - """向后端发送验证码错误的通知""" - try: - self._log(f"开始发送验证码错误通知,店铺ID: {store_id}, 错误: {error_msg}", "INFO") - from WebSocket.backend_singleton import get_backend_client - backend = get_backend_client() - - if backend: - message = { - "type": "connect_message", - "store_id": store_id, - "status": False, - "content": error_msg - } - self._log(f"准备发送验证码错误消息: {message}", "DEBUG") - backend.send_message(message) - self._log("✅ 成功向后端发送验证码错误通知", "SUCCESS") - else: - self._log("❌ 后端客户端为空,无法发送验证码错误通知", "ERROR") - except Exception as e: - self._log(f"❌ 发送验证码错误通知失败: {e}", "ERROR") - import traceback - self._log(f"错误详情: {traceback.format_exc()}", "DEBUG") - - def _send_login_success_message(self, store_id): - """向后端发送登录成功的通知""" - try: - self._log(f"开始发送登录成功通知,店铺ID: {store_id}", "INFO") - from WebSocket.backend_singleton import get_backend_client - backend = get_backend_client() - - if backend: - message = { - "type": "connect_message", - "store_id": store_id, - "status": True, # 登录成功 - "cookies": self.cookies # 🔥 新增:添加登录生成的cookie信息 - } - self._log(f"准备发送登录成功消息: {message}", "DEBUG") - backend.send_message(message) - self._log("✅ 成功向后端发送登录成功通知", "SUCCESS") - else: - self._log("❌ 获取后端客户端失败", "ERROR") - except Exception as e: - self._log(f"❌ 发送登录成功通知失败: {e}", "ERROR") - import traceback - self._log(f"错误详情: {traceback.format_exc()}", "DEBUG") - - def _send_login_failure_message(self, store_id, error_msg): - """向后端发送登录失败的通知""" - try: - self._log(f"开始发送登录失败通知,店铺ID: {store_id}, 错误: {error_msg}", "INFO") - from WebSocket.backend_singleton import get_backend_client - backend = get_backend_client() - - if backend: - message = { - "type": "connect_message", - "store_id": store_id, - "status": False, # 登录失败 - "content": error_msg # 官方返回的失败原因 - } - self._log(f"准备发送登录失败消息: {message}", "DEBUG") - backend.send_message(message) - self._log("✅ 成功向后端发送登录失败通知", "SUCCESS") - else: - self._log("❌ 获取后端客户端失败", "ERROR") - except Exception as e: - self._log(f"❌ 发送登录失败通知失败: {e}", "ERROR") - import traceback - self._log(f"错误详情: {traceback.format_exc()}", "DEBUG") - - def login_with_params(self, login_params, store_id=None, error_count=0, success_count=0): - """使用后端下发的登录参数进行登录""" - self._log("🚀 [PddLogin] 开始使用参数登录", "INFO") - # 检查验证码字段(兼容 code 和 verification_code) - verification_code = login_params.get("verification_code") or login_params.get("code", "") - self._log( - f"📋 [PddLogin] 登录参数: username={login_params.get('username', 'N/A')}, 包含验证码={bool(verification_code)}", - "DEBUG") - - self.headers["anti-content"] = login_params.get("anti_content", "") - - # 直接使用后端提供的参数构建登录请求 - ts = login_params.get("timestamp", int(round(time.time() * 1000))) - payload = { - "username": login_params.get("username", ""), - "password": login_params.get("password", ""), # 后端已加密 - "passwordEncrypt": True, - "verificationCode": verification_code, - "mobileVerifyCode": "", # 使用兼容的验证码字段 - "sign": "", - "touchevent": { - "mobileInputEditStartTime": ts - random.randint(10000, 20000), - "mobileInputEditFinishTime": ts - random.randint(8000, 15000), - "mobileInputKeyboardEvent": "0|1|1|-6366-6942-142", - "passwordInputEditStartTime": ts - random.randint(5000, 10000), - "passwordInputEditFinishTime": ts - random.randint(3000, 8000), - "passwordInputKeyboardEvent": "0|1|1|195-154-755-477", - "captureInputEditStartTime": "", - "captureInputEditFinishTime": "", - "captureInputKeyboardEvent": "", - "loginButtonTouchPoint": "1263,586", - "loginButtonClickTime": ts - random.randint(100, 1000) - }, - "fingerprint": { - "innerHeight": 906, - "innerWidth": 1707, - "devicePixelRatio": 1.5, - "availHeight": 1027, - "availWidth": 1707, - "height": 1067, - "width": 1707, - "colorDepth": 24, - "locationHref": "https://mms.pinduoduo.com/login/sso?platform=wholesale&redirectUrl=htt", - "clientWidth": 1707, - "clientHeight": 906, - "offsetWidth": 1707, - "offsetHeight": 906, - "scrollWidth": 2882, - "scrollHeight": 906, - "navigator": { - "appCodeName": "Mozilla", - "appName": "Netscape", - "hardwareConcurrency": 16, - "language": "zh-CN", - "cookieEnabled": True, - "platform": "Win32", - "doNotTrack": True, - "ua": self.headers.get("user-agent"), - "vendor": "Google Inc.", - "product": "Gecko", - "productSub": "20030107", - "mimeTypes": "f5a1111231f589322da33fb59b56946b4043e092", - "plugins": "387b918f593d4d8d6bfa647c07e108afbd7a6223" - }, - "referer": "", - "timezoneOffset": -480 - }, - "riskSign": login_params.get("risk_sign", ""), # 后端已加密 - "timestamp": ts, - "crawlerInfo": login_params.get("anti_content", "") - } - - # 添加详细的请求日志 - self._log(f"🔍 [Debug] 登录请求URL: {self.login_api}", "DEBUG") - self._log(f"🔍 [Debug] 登录请求payload: {payload}", "DEBUG") - self._log(f"🔍 [Debug] 登录请求headers: {dict(self.headers)}", "DEBUG") - - response = requests.post(self.login_api, headers=self.headers, json=payload, cookies=self.cookies) - self.cookies.update(response.cookies.get_dict()) - self._log(f"登录响应状态码: {response.status_code}") - self._log(f"登录响应内容: {response.text}") - - # 检查响应内容 - if "需要手机验证" in response.text: - self._log("✅ 检测到需要手机验证的响应", "INFO") - elif "需要验证图形验证码" in response.text: - self._log("✅ 检测到需要图形验证码的响应", "INFO") - else: - self._log("⚠️ 未检测到验证码相关响应", "WARNING") - - if "需要验证图形验证码" in response.text: - self.verify_auth_token = response.json().get("result").get("verifyAuthToken") - - salt = self.vc_pre_ck_b() # 获取生成aes key和iv 的密文值 - pictures = self.obtain_captcha() # 获取验证码图片 - distance = round((ImgDistance(bg=pictures[0], tp=pictures[1]).main() * (272 / 320)) + (48.75 / 2), - 2) # 计算距离 - - track_list = Track.get_track(distance=distance) # 生成轨迹 - captcha_collect = self.captcha_collect(salt=salt, track_list=track_list) # 生成captcha_collect参数 - result = self.verify(captcha_collect=captcha_collect, verify_code=distance) # 检验 - - if result: - self.cookies.update({ - "msfe-pc-cookie-captcha-token": self.verify_auth_token - }) - self.headers["verifyauthtoken"] = self.verify_auth_token - success_count += 1 - # 如果滑块成功 success_count 计数一次 成功8次还是显示验证码则失败 返回False - if success_count < 8: - return self.login_with_params(login_params=login_params, store_id=store_id, - success_count=success_count) - else: - return False - else: - # 如果滑块失败 error_count 计数一次 失败8次则返回False - error_count += 1 - if error_count < 8: - return self.login_with_params(login_params=login_params, store_id=store_id, error_count=error_count) - else: - return False - - elif "需要手机验证" in response.text: - # 检查是否已经包含验证码(兼容 code 和 verification_code) - if verification_code: - # 已有验证码但仍需要验证,验证码可能错误、过期或者其他问题 - self._log(f"带验证码登录失败,验证码: {verification_code}", "WARNING") - # 检查响应中的具体错误信息 - response_data = response.json() - error_msg = response_data.get('errorMsg', '验证码验证失败') - self._log(f"服务器返回错误: {error_msg}", "WARNING") - - # 不要重新发送验证码请求,直接报告验证失败 - self._send_verification_error_message(store_id, error_msg) # 直接使用官方错误信息 - return "verification_code_error" # 返回特殊状态,避免重复发送消息 - else: - # 第一次需要验证码,调用发送验证码方法 - self._log("检测到需要手机验证码,正在调用发送验证码方法", "INFO") - username = login_params.get("username") - backend_anti_content = login_params.get("anti_content") - self._log(f"为用户 {username} 发送验证码,使用后端anti_content", "INFO") - - # 🔥 从响应中提取手机号 - phone_number = None - try: - response_json = response.json() - phone_number = response_json.get("result") # 手机号在result字段中 - if phone_number and isinstance(phone_number, str): - self._log(f"🔍 从登录响应中提取到手机号: {phone_number}", "SUCCESS") - else: - self._log("⚠️ 响应中的result字段不包含有效手机号", "WARNING") - except Exception as e: - self._log(f"❌ 提取手机号时出错: {e}", "DEBUG") - - # 传递后端下发的anti_content和手机号 - self.request_verification_code(username, store_id, backend_anti_content, phone_number) - return "need_verification_code" - - else: - # 登录成功或其他情况 - response_data = response.json() - if response_data.get("success"): - self._log("🎉 登录成功!", "SUCCESS") - # 发送登录成功通知给后端 - self._send_login_success_message(store_id) - return self.cookies - else: - # 登录失败,发送失败通知给后端 - error_msg = response_data.get('errorMsg', '未知错误') - self._log(f"登录失败: {error_msg}", "ERROR") - self._send_login_failure_message(store_id, error_msg) - return "login_failure" # 返回特殊状态,避免重复发送消息 - - -# ===== 登录相关类集成结束 ===== - - -# 定义持久化数据类 - 参考京东的WebsocketManager -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 PddBackendService: - """拼多多后端服务调用类(新版:使用统一后端连接 BackendClient)""" - - def __init__(self): - self.current_store_id = None - - async def connect(self, store_id): - """连接后端服务(使用统一连接,无需独立连接)""" - self.current_store_id = 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 - - # 确保消息包含store_id - if isinstance(platform_message, dict): - if 'store_id' not in platform_message and self.current_store_id: - platform_message['store_id'] = self.current_store_id - - # 通过统一后端连接发送 - backend.send_message(platform_message) - return True - except Exception: - return None - - -class ChatPdd: - @staticmethod - def check_js_files(): - """检查JS文件是否存在""" - # 使用资源路径解析函数 - current_dir = ChatPdd._get_resource_path("static/js") - required_files = ["dencode_message.js", "encode_message.js"] - - for file in required_files: - file_path = os.path.join(current_dir, file) - if not os.path.exists(file_path): - raise FileNotFoundError(f"找不到必需的JS文件: {file_path}") - - return True - - @staticmethod - def _get_resource_path(relative_path): - """获取资源文件的绝对路径(兼容PyInstaller打包环境)""" - try: - print(f"[DEBUG] 正在解析资源路径: {relative_path}") - - # PyInstaller环境下的基础路径 - if hasattr(sys, '_MEIPASS'): - # PyInstaller 临时目录 - base_path = sys._MEIPASS - print(f"[DEBUG] 检测到PyInstaller环境,_MEIPASS: {base_path}") - elif hasattr(sys, 'frozen') and sys.frozen: - # 其他打包环境 - base_path = os.path.dirname(sys.executable) - print(f"[DEBUG] 检测到其他打包环境,executable目录: {base_path}") - else: - # 开发环境 - base_path = os.path.dirname(os.path.abspath(__file__)) - # 向上两级目录到项目根目录 - base_path = os.path.dirname(os.path.dirname(base_path)) - print(f"[DEBUG] 开发环境,计算的项目根目录: {base_path}") - - resource_path = os.path.join(base_path, relative_path) - print(f"[DEBUG] 拼接后的完整资源路径: {resource_path}") - - # 检查路径是否存在 - if os.path.exists(resource_path): - print(f"[DEBUG] ✅ 资源路径存在: {resource_path}") - else: - print(f"[DEBUG] ❌ 资源路径不存在: {resource_path}") - # 尝试列出基础路径的内容 - print(f"[DEBUG] 基础路径 {base_path} 的内容:") - try: - for item in os.listdir(base_path): - item_path = os.path.join(base_path, item) - if os.path.isdir(item_path): - print(f"[DEBUG] 📁 {item}/") - else: - print(f"[DEBUG] 📄 {item}") - except Exception as e: - print(f"[DEBUG] 无法列出目录内容: {e}") - - return resource_path - except Exception as e: - print(f"[ERROR] 获取资源路径失败: {e}") - import traceback - print(f"[ERROR] 堆栈跟踪: {traceback.format_exc()}") - # 降级处理:返回相对路径 - return relative_path - - def __init__(self, cookie, chat_list_stat, csname=None, text=None, log_callback=None): - # 检查JS文件 - self.check_js_files() - - # 获取JS文件路径 - 使用资源路径解析 - js_dir = self._get_resource_path("static/js") - dencode_js_path = os.path.join(js_dir, "dencode_message.js") - encode_js_path = os.path.join(js_dir, "encode_message.js") - - # 读取JS文件 - try: - with open(dencode_js_path, 'r', encoding='utf-8') as f: - jscode = f.read() - self.ctx = execjs.compile(jscode) - - with open(encode_js_path, 'r', encoding='utf-8') as f: - ejscode = f.read() - self.encodeex = execjs.compile(ejscode) - except Exception as e: - raise RuntimeError(f"读取JS文件失败: {str(e)}") - - # 首先设置log_callback,然后才能使用_log方法 - self.log_callback = log_callback - self._log("初始化ChatPdd实例", "INFO") - - self.chat_list_stat = chat_list_stat - self.text = text - self.cookie = cookie - self.auth_token = None - self.mall_id = None - self.user_id = None - self.csname = csname - self.csid = None - self.staff_list_sent = False - self.uids = set() - self.pool = ThreadPoolExecutor(max_workers=4) - - # WebSocket管理器 - self.ws_manager = WebsocketManager() - - # 后端服务实例 - self.backend_service = PddBackendService() - 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 - - self.headers = { - "authority": "mms.pinduoduo.com", - "accept": "*/*", - "accept-language": "zh-CN,zh;q=0.9", - "anti-content": "0asWfxUeM_VefxObk-_v-fwmt7oS3cmHsWwMkApMVigWOS3hCHfBeF-hHM1_v9LHywCtfzDKkA_F1cUE8_HKBA5D7fVkM2ZDB-KmMxhDM51HXlVrfK_LG4Dc3S0wimw8XYPyXdgJnUv8ndgjs0EynYv8n0XjXU9YXY4Pt0ZVgTgrsIUEeMzMk7s02E3oo-zMdF35CIMWVetBhs95lGG5xENwZwX0CwXH2799MeMjVKzk5DD4Kbs2dM1bD-fJ1F-RImB3SHBkVKDJZbZoUbL4UMk8DFtrISf8CMD4Obk_CM3ICIB_F90ws9ZQ0gcOpVdIIz2PLPVcYt5X0yqYiwjtuNVfdNSf0rmfmNIndXj8G8Nyn4ianRaanoucDp0Dfvd36B6eamPGwOFS0TAyo7nXH_nwxnGS28AgVnqPyPuy8jczb1Oe3xZuPHoJ97BBbaTMfV9OcQ23T-mAUel", - "cache-control": "no-cache", - "content-type": "application/json", - "origin": "https://mms.pinduoduo.com", - "pragma": "no-cache", - "referer": "https://mms.pinduoduo.com/chat-merchant/index.html?r=0.4940608191737519", - "sec-ch-ua": "\"Not_A Brand\";v=\"8\", \"Chromium\";v=\"120\", \"Google Chrome\";v=\"120\"", - "sec-ch-ua-mobile": "?0", - "sec-ch-ua-platform": "\"Windows\"", - "sec-fetch-dest": "empty", - "sec-fetch-mode": "cors", - "sec-fetch-site": "same-origin", - "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" - } - - self.store_id = "538727ec-c84d-4458-8ade-3960c9ab802c" # 可以根据需要修改 - self.user_tokens = {} # 存储用户token信息 - - def _log(self, message, level="INFO"): - """内部日志方法""" - if self.log_callback: - self.log_callback(message, level) - else: - timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - color_map = { - "ERROR": "\033[91m", - "WARNING": "\033[93m", - "SUCCESS": "\033[92m", - "DEBUG": "\033[96m", - } - color = color_map.get(level, "") - reset = "\033[0m" - print(f"{color}[{timestamp}] [{level}] {message}{reset}") - - # 重连机制方法 - 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): - """发送消息到后端(使用统一连接,不等待回复)""" - # 首先检查后端服务连接状态 - if not self.backend_connected: - # 尝试重新连接 - if self.store_id: - self.backend_connected = await self.connect_backend_service(self.store_id) - - if not self.backend_connected: - self._log("❌ 后端服务未连接,尝试重新连接失败", "WARNING") - return None - - try: - # 发送消息到后端(使用统一连接,不等待回复) - await self.backend_service.send_message_to_backend(platform_message) - self._log("✅ 消息已发送到后端,等待后端AI处理并通过GUI转发回复", "INFO") - return None - - except Exception as e: - self._log(f"❌ 发送消息到后端失败: {e}", "ERROR") - return None - - def get_auth_token(self): - url = "https://mms.pinduoduo.com/janus/api/subSystem/getAuthToken" - data = {"subSystemId": 17, - "anti_content": "0asAfx5E-wCElqJNXaKt_UKccG7YNycjZoPYgO1YTmZoApN7QJU5XT_JuYdPZ4uvLPQFgIcyXOKqm8xGrPjy0lxn0gaXr9ac0TynYEJnDwdj-WEJnwK3G4kc3M0TiPgtB11eBeZkB1hkBxUHFOtjfqz-eAkgc2aa4sZuIJwHOptYX14VK1NJSqIYfqmw6jpQTXs574CfWMFxT1RPTIB2MKBjC199pXpkbdtXxnn2mA4C1YdNnTUrNa_Wvn5mYj5XadnDbNT7xNuVeYXrsTsiipUr6xn2AAXKoYmv6j0PL92PtCTMfZzzfTfjutCgvBTzxFw-2LeeRIkFBATU1Npg1ScUFRv-1Ukrs3AklVAbBiEbBJhzcf2cXKfNH50X9QUFCd_Y1FhLW1BBuI-KEUFYK3lZTB3g_BlLDk8ti-JXmbalXzWAb6V2hX0fZE9n2L0GITFInNS"} - response = requests.post(url, cookies=self.cookie, headers=self.headers, json=data).json() - self.auth_token = response["result"]["authToken"] - - def get_mall_id(self): - url = "https://mms.pinduoduo.com/chats/userinfo/realtime?get_response=true" - data = {"get_response": "true"} - response = requests.get(url, cookies=self.cookie, headers=self.headers, params=data).json() - self.mall_id = response["mall_id"] - self.user_id = response["id"] - - def get_assign_cslist(self): - url = "https://mms.pinduoduo.com/latitude/assign/getAssignCsList" - data = { - "wechatCheck": True - } - data = json.dumps(data, separators=(',', ':')) - response = requests.post(url, headers=self.headers, cookies=self.cookie, data=data).json() - cslist = response["result"]["csList"] - keys = cslist.keys() - for key in keys: - username = cslist[key].get("username") - if username == self.csname: - self.csid = key - break - - def tab_mall(self, uid): - """执行转接操作""" - self._log(f"🔄 开始执行转接操作 - 用户ID: {uid}, 目标客服ID: {self.csid}", "INFO") - - try: - url = "https://mms.pinduoduo.com/plateau/chat/move_conversation" - request_id = int(time.time() * 1000) - - data = { - "data": { - "cmd": "move_conversation", - "request_id": request_id, - "conversation": { - "csid": self.csid, - "uid": uid, - "need_wx": False, - "remark": "无原因直接转移" - }, - "anti_content": "0asAfxvYXyIgYgE2h9U0sXh6Ia3tZOAzI_rl3hixs1Z5DB-B1cswVK2UB8LfUO8H3OUKdF214-SgPVnY8SYvSdmwnKDoPXAs4OdtzfKwkT6gAO2d4R8ZVTw3a7j9BdB3Hmq7j0bh2LQrRzlt88pkQr-s3jMzWH5QPiz13wQNsJY6h9dnigBE26Ww1dKZcWbsKS4GsgY9_joGBS2FZUz94cbCvE2Ij9nO93b5kR50m9kI1lfBHvDZVIC9rLB-S4Qs4J9ebSsosuNskY9nhgXzCuNoilSQgJaMkP2Rgga2i54nWTTNfnZ4W9GvigkSnwzweN9sx-89BztNCasVkP2C2Kkipif2Dv-t7e2O9MnmtWaKm4wFlHLR7YmeyF3y5gxPRuHlO-gD7BfFle8z71lIV-7SyOLCd83FMUCsRwedwv3-TNTF_VRw_G1DHrptFYTAck8p8qIkxA_PZ14PXct1ZS2zAoZZcTMZZSpJ1jP9RlQARtnNNP0IzCdHiL4JzRQBkkKmwGGXay0i4urnXNm5qBgejFcVCkx8XYdeGBp4p-l1TjKoy603__johKFKZQksRHjOKma5DplwQwg8c3Uo1-mDAUmgAC0XfV2rkxzXj3ABARPlyEaMU8AxUZioaFRa0e5eit6r06uY9m5SyU_mdbTfLIYcKWjRA_UNQisJOUenCbMl_Bywq1KcA6TTzJ9ZHu4xz0o7ctROYLlvhEhAi3bJjFNho6l-TPZK4zSJoj" - }, - "client": "WEB", - "anti_content": "0asAfxvYXNIgYgd2i9U0sXh6IabtZOAdI_LlbhNUvZZ5DB-B1cswVK2UBjxfUOjHbOUKdF214-SgPVnYjSYvSdmwnKDoPXAs4OdtzfKwkT6gAO2d4RjZVTwbJFiC0P0rR6LbH89rLVuGHiG6vm3fO6rq_rAyLQlw8CAIuESMR4L308Ctz3V0m9ZhS8tdapQr_YY2vZZk9sAJsMd2ZapG92zm2lO9FLVBjCur2KWwac9nua9g8ztOawyo9ur0Dd96xMkCXrd4J4hR4Kk9zqZBDTlRPkMdzzkJ5Osgi40EC3oVAhSPA9MawGCs33Vnd4bSJER9_b_D9g1sfdeZZKsgo25k0WRfs7dM17E2ug-pPEL5_ib53TuaxLHJq5pGG4jMUjAHRV-gbDffut1Yjy1A7K-6E3zO4tDuUq5T0LPHjf9bB7Apofgr_U_4Aq-7l8x2ks8DhhgOhblTvwrO4owZvuUXA0hfSIUPYK9fIoSUH6aaL8abMMVCvI__qF8Yw1oaMp0d0-fbwMhXQ8fBZYoajXXJv-RV3B2GQEowvMHEUtjDz50jUVduq5fKL9R-WsBRjX0aiExVKLw-PZzxsVup0ZuOYxnPofN92EqKfbvriIWFMsh80eGrvq1vsD1BrMx_mcPMmUmHHEY7wct5lePa0b2Lt5_byqRbbcpohrQrHLlH6JJaTMKQuffyT-60110e5oz_Cz0NbG88TyxOGKijW7BimL5RTjSOTPsK4zS8oi" - } - - self._log(f"📤 发送转接请求 - URL: {url}", "DEBUG") - self._log(f"📝 请求数据: {json.dumps(data, ensure_ascii=False)}", "DEBUG") - - data_str = json.dumps(data, separators=(',', ':')) - response = requests.post(url, headers=self.headers, cookies=self.cookie, data=data_str) - - self._log(f"📥 HTTP状态码: {response.status_code}", "DEBUG") - self._log(f"📄 响应内容: {response.text}", "DEBUG") - - # 解析响应 - try: - result = response.json() - self._log(f"🔍 解析后的响应: {json.dumps(result, ensure_ascii=False)}", "DEBUG") - - # 检查响应结构 - if result.get("success"): - result_data = result.get("result", {}) - if result_data.get("result") == "success" or result_data.get("result") == "ok": - self._log(f"✅ 转接成功 - 用户 {uid} 已转接给客服 {self.csid}", "SUCCESS") - return True - else: - error_code = result_data.get("error_code", "未知") - error_msg = result_data.get("error_msg", "未知错误") - self._log(f"❌ 转接失败 - 错误码: {error_code}, 错误信息: {error_msg}", "ERROR") - return False - else: - self._log(f"❌ 转接请求失败 - 响应标记为失败", "ERROR") - return False - - except json.JSONDecodeError as e: - self._log(f"❌ 解析响应JSON失败: {e}", "ERROR") - self._log(f"❌ 原始响应: {response.text}", "ERROR") - return False - - except requests.RequestException as e: - self._log(f"❌ 请求异常: {e}", "ERROR") - return False - except Exception as e: - self._log(f"❌ 转接操作异常: {e}", "ERROR") - traceback.print_exc() - return False - - @staticmethod - def forward_message_to_platform(store_id: str, recv_pin: str, content: str): - """转发消息到拼多多平台""" - try: - pdd_mgr = WebsocketManager() - pdd_shop_key = f"拼多多:{store_id}" - pdd_entry = pdd_mgr.get_connection(pdd_shop_key) - if pdd_entry: - # 找到拼多多连接,使用拼多多转发逻辑 - platform_info = pdd_entry.get('platform') or {} - ws = platform_info.get('ws') - loop = platform_info.get('loop') - pdd_instance = platform_info.get('pdd_instance') # ChatPdd实例引用 - print( - f"[PDD Forward] shop_key={pdd_shop_key} has_ws={bool(ws)} has_loop={bool(loop)} has_pdd_instance={bool(pdd_instance)} recv_pin={recv_pin}") - if ws and loop and pdd_instance: - async def _send_pdd(): - # 直接调用ChatPdd实例的send_ai_reply方法 - await pdd_instance.send_ai_reply(recv_pin, content) - - import asyncio as _asyncio - _future = _asyncio.run_coroutine_threadsafe(_send_pdd(), loop) - try: - _future.result(timeout=5) # 拼多多需要HTTP请求,给更长时间 - print(f"[PDD Forward] 已转发到平台: uid={recv_pin}, content_len={len(content)}") - except Exception as fe: - print(f"[PDD Forward] 转发提交失败: {fe}") - else: - print("[PDD Forward] 条件不足,未转发:", - {'has_ws': bool(ws), 'has_loop': bool(loop), 'has_pdd_instance': bool(pdd_instance), - 'has_content': bool(content)}) - else: - print(f"[PDD Forward] 未找到拼多多连接: {pdd_shop_key}") - except Exception as e: - print(f"[PDD Forward] 拼多多转发失败: {e}") - - @staticmethod - def transfer_customer_service(customer_service_id: str, user_id: str, store_id: str): - """拼多多平台转接""" - try: - pdd_mgr = WebsocketManager() - pdd_shop_key = f"拼多多:{store_id}" - pdd_entry = pdd_mgr.get_connection(pdd_shop_key) - if pdd_entry: - platform_info = pdd_entry.get('platform') or {} - 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并执行转接 - pdd_instance.csid = customer_service_id - - def _transfer(): - result = pdd_instance.tab_mall(user_id) - if result: - print(f"[PDD Transfer] ✅ 转接成功") - else: - print(f"[PDD Transfer] ❌ 转接失败") - return result - - # 在线程池中执行同步转接操作 - import concurrent.futures - with concurrent.futures.ThreadPoolExecutor() as executor: - future = executor.submit(_transfer) - try: - future.result(timeout=10) # 转接操作可能需要更长时间 - except Exception as fe: - print(f"[PDD Transfer] 转接操作异常: {fe}") - else: - print(f"[PDD Transfer] 条件不足: has_pdd_instance={bool(pdd_instance)}, has_loop={bool(loop)}") - else: - print(f"[PDD Transfer] 未找到拼多多连接: {pdd_shop_key}") - except Exception as e: - print(f"[PDD Transfer] 拼多多转接失败: {e}") - - async def send_ai_reply(self, uid, content): - """发送AI回复给指定用户""" - self._log(f"开始发送AI回复给用户 {uid}", "INFO") - self._log(f"回复内容: {content[:50]}...", "DEBUG") - try: - ts = int(time.time() * 1000) - url = "https://mms.pinduoduo.com/plateau/chat/send_message" - data = { - "data": { - "cmd": "send_message", - "anti_content": "0asWfqzFdjPssgua6CCWyeEK7LAktf0_UFChvlj8kD8Mk1DVZchg4qURiKAS4iSfiaz9Fjg43ThZZ4_wNh5Onk0e5Tw2mGdwpTEPDZlPdfZwBpyESSDiz0Xalv6mvr0o0uQR6yRlG-vscbK8jYGRNcAr2UktX3bMKhjjx7YvVGbi_poiyypBuOwM_2qZyIM96tdNTfb_AMy0fe1UrN2SqUzfm2stmb4TJldGN9b1mTPr-6t4R9k3pquV_XHwBr20J4__P71fxPBmcXOmXTnQWJs2ddt4KKOIbPIIanas08VNiv9nsHbtvXKp8_CMdL8FQMPmEMDmM6JfjZ4LJp5l-x0BjpBbdWyoztNYqSbBe8hscA1hPFM_R1iR9E-AI-CyuehipODW-ADCL0xEWK875510RsV7mLFQKG0kWO--krZzFFi_6lhdfq3_JhBjDDd3YS1exxSMMS7yDJgskoumfAcu-y1YDO8OdcKWwx3Z0nOqUlnF8LekN7TqXhNtKTgfDZPThVT3JcNm_2TljXRhGWxTZT4o25TTVKo5KPeztcJU5NddFd38nmvo0k8R8HVIkmzOYyaerCDCm1ZDfYgNFx-2vM_JkcMYFdkV7C3BKeg7DxZXHfexAT3UKgruBWYrSs_dBB0X-QkJ2ZcmpStB6KJkZ1azrAAxdw_HMj5UIq4vEXxrA1-YAzJEhMoU0-MncDpoBZfiky7Sr4nDGYrcn86AAYWynOUn0US2DAkICABvSSqkh0vCRpdVeZRfOFsdFWF43KEIph-ckMxDsyq6G3rWXbvPkaAlv67cR1v0N2oCQzDVDT-InNv", - "request_id": ts, - "message": { - "to": { - "role": "user", - "uid": uid - }, - "from": { - "role": "mall_cs" - }, - "ts": int(ts / 1000), - "content": content, - "msg_id": None, - "type": 0, - "is_aut": 0, - "manual_reply": 1, - "status": "read", - "is_read": 1, - "hash": "1d1359dfb7c141fd95432a22cdc7f43b51434041a1a6016ef6ea2ea2b1abc05a" - }, - "random": "c5fc9c61194294c478a6fb395b1c558f" - }, - "client": "WEB", - "anti_content": "0asAfa5E-wCEsa-JeFwdFfTDtzhMbODIsAT-eu_-CigAHMbcWIfBEUFcI-Kwd93IYTWtfvkSeuwUKO57jwISBuZkzfCe-2VkBFSD-ack-ZKIXRockMwxBgyDql6V3xKKD-fCkBFZkzeKeBwUE-cSD-KHkBb-kzFVk-szd009EAPjDvBcaPIXxHGB_IxWWlIXMnxghvniTodnXt2scz7sxhJV_CEBcCez4H92maXI2d1qgoYswoG4Mf_oylph0nGigjnqWzXqwTYh0y4W6yYs6PxHyXv8slpHrXyseoqZhfqdrdn5E9BCt24mBfkEFg-vgc0wUw2D-vgE7KUM1FIdKb2_Mbl_6xj_8bvk-e1H6ObSksLCDiVCDAbkYU6Y_SMBfgA5CHUM0QuYhznXIwnTanGM2jugCnxPYPmYjyOvjKHEbaV3nvMuJ6V2cX0fZE99vcfY3TF1nNr" - } - data = json.dumps(data, separators=(',', ':')) - response = await asyncio.get_event_loop().run_in_executor( - self.pool, - lambda: requests.post(url, headers=self.headers, cookies=self.cookie, data=data) - ) - - # 打印HTTP状态与返回体片段 - self._log(f"PDD发送状态: HTTP {response.status_code}", "INFO") - try: - self._log(f"PDD返回体: {response.text[:500]}", "DEBUG") - except Exception: - pass - - if response.status_code == 200: - self._log(f"✅ 已发送AI回复给用户 {uid}: {content}", "SUCCESS") - return True - else: - self._log(f"❌ 发送AI回复失败,HTTP状态码: {response.status_code}", "ERROR") - self._log(f"响应内容: {response.text}", "DEBUG") - return False - - except Exception as e: - self._log(f"❌ 发送AI回复失败: {e}", "ERROR") - traceback.print_exc() - return False - - async def send_message_external(self, uid, content): - """外部消息发送接口,用于接收后端转发的回复""" - self._log(f"[External] 收到外部消息发送请求: uid={uid}, content_len={len(content) if content else 0}", "INFO") - try: - if not uid or not content: - self._log("❌ [External] 参数不完整", "ERROR") - return False - - result = await self.send_ai_reply(uid, content) - if result: - self._log(f"✅ [External] 外部消息发送成功: uid={uid}", "SUCCESS") - else: - self._log(f"❌ [External] 外部消息发送失败: uid={uid}", "ERROR") - return result - except Exception as e: - self._log(f"❌ [External] 外部消息发送异常: {e}", "ERROR") - return False - - async def get_all_customer(self, ws): - """异步获取客服列表""" - try: - # PDD获取客服列表的API - url = "https://mms.pinduoduo.com/latitude/assign/getAssignCsList" - data = { - "wechatCheck": True - } - data = json.dumps(data, separators=(',', ':')) - - # 使用线程池执行同步请求 - response = await asyncio.get_event_loop().run_in_executor( - self.pool, - lambda: requests.post(url, headers=self.headers, cookies=self.cookie, data=data) - ) - - if response.status_code == 200: - result = response.json() - cslist = result.get("result", {}).get("csList", {}) - print("我们收集到的客服列表原信息为:", str(cslist)) - if cslist: - # 转换客服列表格式 - self.customer_list = [] - for key, customer in cslist.items(): - customer_info = { - "staff_id": str(key), # 客服ID - "name": customer.get("username", ""), # 客服名称 - "status": customer.get("status", 0), # 状态 - "department": customer.get("department", ""), # 部门 - "online": True # 在线状态 - } - self.customer_list.append(customer_info) - self._log(f"✅ 成功获取客服列表,共 {len(self.customer_list)} 个客服", "SUCCESS") - return True - return False - except Exception as e: - self._log(f"❌ 获取客服列表失败: {e}", "ERROR") - return False - - async def send_staff_list_to_backend(self): - """发送客服列表到后端""" - try: - if not hasattr(self, 'customer_list') or not self.customer_list: - self._log("⚠️ 客服列表为空", "WARNING") - return False - - # 创建消息模板 - message_template = PlatformMessage( - type="staff_list", - content="客服列表更新", - data={ - "staff_list": self.customer_list, - "total_count": len(self.customer_list) - }, - store_id=self.store_id - ) - - # 通过后端服务发送 - await self.backend_service.send_message_to_backend(message_template.to_dict()) - self._log(f"发送客服列表消息的结构体为: {message_template.to_json()}") - self._log(f"✅ [PDD] 成功发送客服列表到后端,共 {len(self.customer_list)} 个客服", "SUCCESS") - print(f"🔥 [PDD] 客服列表已上传到后端: {len(self.customer_list)} 个客服") - return True - - except Exception as e: - self._log(f"❌ 发送客服列表到后端异常: {e}", "ERROR") - return False - - async def handle_transfer_message(self, message_data): - """处理转接消息""" - self._log("收到转接指令", "INFO") - try: - content = message_data.get("content", "") # 转接目标客服ID - receiver_info = message_data.get("receiver", {}) - uid = receiver_info.get("id") # 用户ID - - if not content or not uid: - self._log("❌ 转接消息信息不完整", "WARNING") - self._log(f"消息内容: {message_data}", "DEBUG") - return False - - self._log(f"准备将用户 {uid} 转接给客服 {content}", "INFO") - - # 设置转接目标客服ID - self.csid = content - - # 在线程池中执行同步的转接操作 - try: - # 使用线程池执行同步方法 - success = await asyncio.get_event_loop().run_in_executor( - self.pool, - self.tab_mall, - uid - ) - - if success: - self._log(f"✅ 转接成功: 用户 {uid} 已转接给客服 {content}", "SUCCESS") - return True - else: - self._log(f"❌ 转接失败: 用户 {uid}", "ERROR") - return False - - except Exception as e: - self._log(f"❌ 执行转接操作失败: {e}", "ERROR") - return False - - except Exception as e: - self._log(f"❌ 处理转接消息失败: {e}", "ERROR") - traceback.print_exc() - return False - - def should_filter_robot_message(self, message_data): - """专门判断是否为机器人消息需要过滤""" - try: - message_info = message_data.get("message", {}) - - # 1. 基于消息类型过滤机器人特殊消息 - msg_type = message_info.get("type") - if msg_type == 31: # 机器人干预消息(如:机器人已暂停接待) - return True - if msg_type == 8: # 平台系统卡片消息(如:售后提醒、平台通知等) - self._log(f"🚫 过滤系统卡片消息(type=8): {message_info.get('content', '')[:50]}", "DEBUG") - return True - - # 2. 基于模板名称识别机器人消息 - template_name = message_info.get("template_name", "") - robot_templates = [ - "mall_robot_man_intervention_and_restart", # 机器人暂停接待消息 - "mall_robot_text_msg", # 机器人自动回复消息 - "aftersales_hosting_warning_card", # 售后托管警告卡片 - # 可根据实际情况添加更多机器人模板 - ] - if template_name in robot_templates: - self._log(f"🚫 过滤模板消息: {template_name}", "DEBUG") - return True - - # 3. 基于info字段识别系统卡片消息(售后提醒、订单提醒等) - info = message_info.get("info", {}) - if info and isinstance(info, dict): - # 如果info中包含mbtn_list(按钮列表),通常是系统卡片 - if info.get("mbtn_list") or info.get("text_list"): - self._log(f"🚫 过滤系统卡片消息(含info.mbtn_list): {message_info.get('content', '')[:50]}", "DEBUG") - return True - - # 4. 基于机器人特殊标志过滤 - if message_info.get("conv_silent") is True: # 静默会话标志 - return True - - if message_info.get("no_unreply_hint") == 1: # 无需回复提示标志 - return True - - # 5. 基于消息内容识别机器人提示消息和平台系统消息 - content = message_info.get("content", "") - robot_content_patterns = [ - "机器人未找到对应的回复", - "机器人已暂停接待", - ">>点此【立即恢复接待】<<", - "点击添加", - "[当前用户来自", - "请尽快解决售后问题", # 平台售后提醒 - "平台介入退款", # 平台售后提醒 - "请尽快处理售后", # 平台售后提醒 - ] - - if any(pattern in content for pattern in robot_content_patterns): - self._log(f"🚫 过滤系统提示消息: {content[:50]}", "DEBUG") - return True - - # 6. 基于biz_context中的机器人标识 - biz_context = message_info.get("biz_context", {}) - if biz_context.get("robot_msg_id"): # 有机器人消息ID - return True - - # 不是机器人消息,不过滤 - return False - - except Exception as e: - self._log(f"判断机器人消息时出错: {e}", "DEBUG") - return False # 出错时不过滤,保持原有行为 - - async def handle_customer_message(self, message_data): - """处理来自后端的客服消息""" - self._log("收到来自后端的客服消息", "INFO") - try: - content = message_data.get("content", "") - receiver_info = message_data.get("receiver", {}) - uid = receiver_info.get("id") - - if uid and content: - self._log(f"准备发送客服消息给用户 {uid}: {content}", "INFO") - await self.send_ai_reply(uid, content) - self._log(f"客服消息发送完成 - 目标用户: {uid}", "SUCCESS") - else: - self._log("客服消息数据不完整", "WARNING") - self._log(f"消息内容: {message_data}", "DEBUG") - except Exception as e: - self._log(f"处理客服消息失败: {e}", "ERROR") - traceback.print_exc() - - async def process_incoming_message(self, message_data, wss, store): - """处理接收到的消息""" - try: - # 🔥 过滤机器人消息 - if self.should_filter_robot_message(message_data): - self._log("🤖 检测到机器人消息,已过滤不发送给后端", "DEBUG") - return - - message_info = message_data.get("message", {}) - if not message_info: - return - - nickname = message_info.get("nickname") - content = message_info.get("content") - goods_info = message_info.get("info", {}) - from_info = message_info.get("from", {}) - uid = from_info.get("uid") - # role = from_info.get("role") - - # if role != "user": - # self._log(f"过滤非用户消息 (role={role})", "DEBUG") - # return - - if nickname and content and uid: - self._log(f"用户消息 - {nickname}: {content}", "INFO") - self._log(f"用户UID: {uid}", "DEBUG") - self.uids.add(uid) - - # 消息类型检测 - lc = str(content).lower() - if any(ext in lc for ext in [".jpg", ".jpeg", ".png", ".gif", ".webp"]): - msg_type = "image" - elif any(ext in lc for ext in [".mp4", ".avi", ".mov", ".wmv", ".flv"]): - msg_type = "video" - else: - msg_type = "text" - - # 订单卡片组装(使用全角冒号,符合文档) - if "订单编号" in str(content) and goods_info: - content = f"商品id:{goods_info.get('goodsID')} 订单号:{goods_info.get('orderSequenceNo')}" - msg_type = "order_card" - - # 商品卡片检测(基于内容关键词和goods_info) - elif any(keyword in lc for keyword in ['goods.html', 'item.html', 'item.jd.com', '商品卡片id:']) or \ - (goods_info and goods_info.get('goodsID') and not goods_info.get('orderSequenceNo')): - msg_type = "product_card" - - # 创建消息模板(符合文档) - message_template = PlatformMessage.create_text_message( - content=content, - sender_id=str(uid), - store_id=self.store_id - ) - message_template.msg_type = msg_type - - try: - print(f"📤(SPEC) 发送到AI: {json.dumps(message_template.to_dict(), ensure_ascii=False)[:300]}...") - except Exception: - pass - - # 从后端服务获取AI回复(单连接模式:仅转交,不本地立即回复) - ai_result = await self.get_ai_reply_from_backend(message_template.to_dict()) - - if isinstance(ai_result, str) and ai_result.strip(): - # 发送回复消息(仅当本地生成/降级时) - await self.send_ai_reply(uid, ai_result) - self._log(f"📤 已发送回复: {ai_result[:100]}...", "INFO") - else: - # 正常链路:已转交后端AI,等待后端异步回传并由 GUI 转发到平台 - self._log("🔄 已转交后端AI处理,等待平台回复下发", "INFO") - - except Exception as e: - self._log(f"❌ 消息处理失败: {e}", "ERROR") - traceback.print_exc() - - @staticmethod - async def heartbeat(wss): - while True: - m = random.randint(100, 200) - h = [0, 0, 0, 0, 0, 0, 0, m, 0, 0, 0, 1, 0, 0, 0, 0] - h = bytes(h) - await wss.send(bytes(h)) - await asyncio.sleep(15) - - async def on_message(self, wss, store): - self._log("开始监听消息", "INFO") - while True: - try: - message = await wss.recv() - self._log(f"收到新消息 {base64.b64encode(message).decode()}", "DEBUG") - try: - text = self.ctx.call("dencode_data", base64.b64encode(message).decode()) - if not isinstance(text, dict): - continue - push = text.get("push_data") or {} - data = push.get("data") - if not data: - self._log("push_data.data 为空,忽略", "DEBUG") - continue - - print(f"原始数据格式打印:{data}") - for d in data: - await self.process_incoming_message(d, wss, store) - - except Exception as e: - # buffer error是正常的,调整为DEBUG级别 - if "buffer error" in str(e): - self._log(f"解析消息失败: {e}", "DEBUG") - else: - self._log(f"解析消息失败: {e}", "ERROR") - traceback.print_exc() - except Exception as e: - # self._log(f"处理消息失败: {e}", "ERROR") - # traceback.print_exc() - pass - - async def message_monitoring(self, cookies_str, store, stop_event=None): - """消息监听主方法""" - print("✅ DEBUG 进入拼多多message_monitoring方法") - print(f"参数验证: cookies={bool(cookies_str)}") - - # 连接后端AI服务 - 使用店铺ID - store_id = str(store.get('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() - - # 充值重连计数器 - self.reconnect_attempts = 0 - - while not stop_event.is_set(): - try: - self._log(f"🔄 尝试连接拼多多WebSocket", "INFO") - - if self.user_id and self.auth_token: - uri = "wss://titan-ws.pinduoduo.com/" - headers = { - "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36", - "Upgrade": "websocket", - "Origin": "https://mms.pinduoduo.com", - "Accept-Encoding": "gzip, deflate, br, zstd", - "Accept-Language": "zh-CN,zh;q=0.9", - "Sec-WebSocket-Extensions": "permessage-deflate; client_max_window_bits", - "cookie": "; ".join([f"{k}={v}" for k, v in self.cookie.items()]) - } - - async with websockets.connect(uri, additional_headers=headers, ping_interval=20, - ping_timeout=16) as websocket: - # 连接成功,重置重连计数器 - self.reconnect_attempts = 0 - self._log("✅ WebSocket-PDD连接成功", "SUCCESS") - - z = self.encodeex.call("encode_token", str(self.user_id), self.auth_token) - if z: - await websocket.send(bytes(z)) - - # 注册连接信息到全局管理 - shop_key = f"拼多多:{store['id']}" - loop = asyncio.get_running_loop() - entry = self.ws_manager.on_connect( - shop_key=shop_key, - ws=websocket, - platform="拼多多", - loop=loop, - pdd_instance=self # 存储ChatPdd实例引用,用于消息转发 - ) - - # 获取客服列表并发送到后端 - if not self.staff_list_sent: - self._log("🔍 开始获取客服列表...", "INFO") - try: - if await self.get_all_customer(None): - await self.send_staff_list_to_backend() - self.staff_list_sent = True - else: - self._log("⚠️ 获取客服列表失败", "WARNING") - except Exception as e: - self._log(f"❌ 获取或发送客服列表失败: {e}", "ERROR") - - # 🔥 新增:Cookie登录成功后发送登录成功报告(与登录参数模式保持一致) - try: - if self.backend_service and hasattr(self, 'store_id') and self.store_id: - # 构建cookie字典(从cookies_str解析) - cookie_dict = {} - if hasattr(self, 'cookie') and self.cookie: - cookie_dict = self.cookie - - message = { - "type": "connect_message", - "store_id": self.store_id, - "status": True, # 登录成功 - "cookies": cookie_dict # 添加cookie信息 - } - # 🔥 修复:使用正确的方法名 send_message_to_backend - await self.backend_service.send_message_to_backend(message) - self._log("✅ [PDD] 已向后端发送Cookie登录成功报告", "SUCCESS") - else: - self._log("⚠️ [PDD] 无法发送登录成功报告:backend_service或store_id缺失", "WARNING") - except Exception as e: - self._log(f"⚠️ [PDD] 发送登录成功报告失败: {e}", "WARNING") - - # 启动消息监听和心跳 - await asyncio.gather( - self.heartbeat(websocket), - self.on_message(websocket, store) - ) - else: - self._log("未定制账号token数组", "ERROR") - - except websockets.ConnectionClosed as e: - self._log(f"🔌 连接已关闭: {e.code} {e.reason}", "WARNING") - if not await self.handle_reconnect(e): - break - - except (websockets.WebSocketException, 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: - self.backend_connected = False - self._log("🛑 消息监听已停止", "INFO") - - async def main(self): - self.get_auth_token() - self.get_mall_id() - self.get_assign_cslist() - - # 连接AI服务 - if await self.connect_backend_service(self.store_id): - print("✅ AI服务连接成功") - - store = {'id': self.store_id} - await self.message_monitoring( - cookies_str=self.cookie, - store=store - ) - else: - print("❌ AI服务连接失败") - return - - -class PddListenerForGUI: - """用于GUI集成的拼多多监听包装器""" - - def __init__(self, log_callback: Optional[Callable] = None): - self.pdd_bot = None - self.log_callback = log_callback - self.running = False - self.main_thread = None - self.loop = None - self.startup_success = False - self.startup_event = threading.Event() - self.startup_error = None - - def _log(self, message: str, log_type: str = "INFO"): - if self.log_callback: - self.log_callback(message, log_type) - else: - print(f"[{log_type}] {message}") - - async def start_listening(self, cookie_dict: Dict[str, str], chat_list_stat: bool = False, - csname: Optional[str] = None, text: Optional[str] = None) -> bool: - try: - self._log("🔵 开始拼多多平台连接流程", "INFO") - - if not cookie_dict: - self._log("❌ Cookie信息不能为空", "ERROR") - return False - - self._log(f"✅ Cookie验证通过,包含 {len(cookie_dict)} 个字段", "SUCCESS") - - self.startup_success = False - self.startup_error = None - self.startup_event.clear() - - # 创建ChatPdd实例 - self.pdd_bot = ChatPdd( - cookie=cookie_dict, - text=text, - chat_list_stat=chat_list_stat, - csname=csname, - log_callback=self.log_callback - ) - - self.running = True - self._log("🎉 开始监听拼多多平台消息...", "SUCCESS") - - def run_pdd_main(): - try: - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - self.loop = loop - - async def main_wrapper(): - try: - # 直接调用ChatPdd的main方法 - await self.pdd_bot.main() - self.startup_success = True - self._log("✅ ChatPdd主程序执行完成", "SUCCESS") - except Exception as e: - self.startup_error = str(e) - self._log(f"❌ ChatPdd运行出错: {str(e)}", "ERROR") - finally: - self.startup_event.set() - - loop.run_until_complete(main_wrapper()) - - except Exception as e: - self.startup_error = str(e) - self._log(f"❌ ChatPdd运行出错: {str(e)}", "ERROR") - self.startup_event.set() - finally: - self.running = False - - self.main_thread = threading.Thread(target=run_pdd_main, daemon=True) - self.main_thread.start() - - self._log("🔄 等待ChatPdd初始化完成...", "INFO") - - # 只等待初始化完成,不设置超时 - self.startup_event.wait() - - if self.startup_success: - self._log("✅ 拼多多监听启动成功", "SUCCESS") - return True - else: - error_msg = self.startup_error or "未知启动错误" - self._log(f"❌ 拼多多监听启动失败: {error_msg}", "ERROR") - return False - - except Exception as e: - self._log(f"❌ 监听启动过程中出现严重错误: {str(e)}", "ERROR") - return False - - async def start_with_login_params(self, store_id: str, login_params: str): - """使用后端下发的登录参数执行登录并启动监听""" - try: - self._log("🔵 [PDD] 收到后端登录参数,开始执行登录获取cookies", "INFO") - self._log(f"🔍 [PDD] 登录参数内容: {login_params[:100]}...", "DEBUG") - - # 1. 解析登录参数 - self._log("🔄 [PDD] 开始解析登录参数", "DEBUG") - params_dict = self._parse_login_params(login_params) - if not params_dict: - self._log("❌ [PDD] 登录参数解析失败", "ERROR") - return False - self._log(f"✅ [PDD] 登录参数解析成功,用户名: {params_dict.get('username', 'N/A')}", "DEBUG") - - # 2. 使用新的PddLogin类执行登录 - self._log("🔄 [PDD] 开始创建PddLogin实例", "DEBUG") - pdd_login = PddLogin(log_callback=self.log_callback) - self._log("✅ [PDD] PddLogin实例创建成功", "DEBUG") - - self._log("🔄 [PDD] 开始执行登录", "DEBUG") - login_result = pdd_login.login_with_params(params_dict, store_id) - self._log(f"📊 [PDD] 登录结果: {login_result}", "DEBUG") - - if login_result == "need_verification_code": - self._log("⚠️ [PDD] 需要手机验证码,已通知后端,等待重新下发包含验证码的登录参数", "WARNING") - return "need_verification_code" # 返回特殊标识,避免被覆盖 - elif login_result == "verification_code_error": - self._log("⚠️ [PDD] 验证码错误,已通知后端", "WARNING") - return "verification_code_error" # 返回特殊标识,避免重复发送消息 - elif login_result == "login_failure": - self._log("⚠️ [PDD] 登录失败,已发送失败通知给后端", "WARNING") - return "login_failure" # 返回特殊标识,避免重复发送消息 - elif not login_result: - self._log("❌ [PDD] 登录失败", "ERROR") - return False - elif isinstance(login_result, dict): - # 登录成功,获取到cookies - self._log("✅ [PDD] 登录成功,使用获取的cookies连接平台", "SUCCESS") - - # 将cookies字典转换为字符串格式,与原有逻辑兼容 - import json - cookies_str = json.dumps(login_result) - return await self.start_with_cookies(store_id, cookies_str) - else: - self._log("❌ [PDD] 登录返回未知结果", "ERROR") - return False - - except Exception as e: - self._log(f"❌ [PDD] 使用登录参数启动失败: {str(e)}", "ERROR") - import traceback - self._log(f"🔍 [PDD] 异常详细信息: {traceback.format_exc()}", "ERROR") - return False - - async def start_with_cookies(self, store_id: str, cookies: str): - """使用下发的cookies与store_id直接建立PDD平台WS并开始监听""" - try: - self._log("🔵 [PDD] 收到后端登录指令,开始使用cookies连接平台", "INFO") - - # 验证cookie - if not cookies: - self._log("❌ Cookie信息不能为空", "ERROR") - return False - - self._log(f"✅ [PDD] Cookie验证通过,长度: {len(cookies)}", "SUCCESS") - - # 获取统一后端服务 - from WebSocket.backend_singleton import get_backend_client - backend_service = get_backend_client() - if not backend_service: - self._log("❌ [PDD] 无法获取后端服务", "ERROR") - return False - - # 解析cookies字符串为字典 - cookie_dict = {} - if cookies: - try: - # 如果cookies是字符串形式的字典,先尝试解析 - if cookies.startswith('{') and cookies.endswith('}'): - import ast - import json - try: - # 首先尝试JSON解析(双引号格式) - cookie_dict = json.loads(cookies) if isinstance(cookies, str) else cookies - except json.JSONDecodeError: - # 如果JSON解析失败,尝试Python字典格式(单引号格式) - cookie_dict = ast.literal_eval(cookies) - else: - # 传统的cookie字符串解析 - for cookie_pair in cookies.split(';'): - if '=' in cookie_pair: - key, value = cookie_pair.strip().split('=', 1) - cookie_dict[key] = value - except Exception as e: - self._log(f"⚠️ 解析cookies失败: {e}", "ERROR") - self._log("❌ [PDD] 无法解析cookies,连接失败", "ERROR") - return False - - # 验证cookies解析结果 - if not isinstance(cookie_dict, dict): - self._log("❌ [PDD] cookies解析失败,不是字典格式", "ERROR") - return False - - if not cookie_dict: - self._log("❌ [PDD] cookies为空", "ERROR") - return False - - self._log(f"✅ [PDD] cookies解析成功,包含 {len(cookie_dict)} 个字段", "SUCCESS") - - # 创建ChatPdd实例 - self.pdd_bot = ChatPdd( - cookie=cookie_dict, - text=None, - chat_list_stat=False, - csname=None, - log_callback=self.log_callback - ) - - # 设置store_id和后端服务 - self.pdd_bot.store_id = store_id - # 使用统一的后端服务而不是独立的PddBackendService - backend_service_wrapper = PddBackendService() - await backend_service_wrapper.connect(store_id) - self.pdd_bot.backend_service = backend_service_wrapper - self.pdd_bot.backend_connected = True - - self.running = True - self._log("🎉 [PDD] 开始监听平台消息", "SUCCESS") - - # 获取认证信息 - self.pdd_bot.get_auth_token() - self.pdd_bot.get_mall_id() - self.pdd_bot.get_assign_cslist() - - # 启动监听 - store = {'id': store_id} - await self.pdd_bot.message_monitoring( - cookies_str=cookies, - store=store - ) - - self._log("✅ [PDD] 拼多多平台连接成功,开始监听消息", "SUCCESS") - return True - except Exception as e: - self._log(f"❌ [PDD] 监听过程中出现严重错误: {str(e)}", "ERROR") - import traceback - self._log(f"错误详情: {traceback.format_exc()}", "DEBUG") - return False - - def stop_listening(self): - try: - self._log("🛑 开始停止拼多多监听...", "INFO") - self.running = False - - if self.main_thread and self.main_thread.is_alive(): - try: - self._log("🔄 等待主线程结束...", "DEBUG") - self.main_thread.join(timeout=5.0) - if self.main_thread.is_alive(): - self._log("⚠️ 主线程未在超时时间内结束", "WARNING") - except Exception as e: - self._log(f"⚠️ 等待主线程结束时出错: {e}", "DEBUG") - - self._log("✅ 拼多多监听已停止", "SUCCESS") - - except Exception as e: - self._log(f"❌ 停止监听时出错: {str(e)}", "ERROR") - - def is_running(self) -> bool: - return (self.running and - self.main_thread is not None and - self.main_thread.is_alive() and - self.startup_success) - - def _parse_login_params(self, login_params_str: str) -> dict: - """解析后端下发的登录参数""" - try: - import json - data = json.loads(login_params_str) - params = data.get("data", {}).get("login_params", {}) - self._log(f"✅ [PDD] 登录参数解析成功: username={params.get('username', 'N/A')}", "INFO") - return params - except Exception as e: - self._log(f"❌ [PDD] 解析登录参数失败: {e}", "ERROR") - return {} - - def get_status(self) -> Dict[str, Any]: - return { - "running": self.running, - "has_bot": self.pdd_bot is not None, - "main_thread_alive": self.main_thread.is_alive() if self.main_thread else False, - "startup_success": self.startup_success, - "startup_error": self.startup_error - } - - -# 测试函数保留 -async def test_gui_integration(): - def custom_log_callback(message: str, log_type: str): - print(f"[GUI测试] [{log_type}] {message}") - - test_cookies = { - "api_uid": "CiKdOWi361Z8egCe1uihAg==", - "_nano_fp": "Xpmyn5CJXqmbnqPyl9_MofCSZEeib112RR6N1Ah9", - "rckk": "v8QcMiTwfCX72hCnHFVtXSg4oHNvs6Qs", - "_bee": "v8QcMiTwfCX72hCnHFVtXSg4oHNvs6Qs", - "ru1k": "cb1cec9f-8895-4301-8e7e-a56c2c830cc5", - "_f77": "cb1cec9f-8895-4301-8e7e-a56c2c830cc5", - "ru2k": "f24003c6-339b-4498-b995-c4200179d4c6", - "_a42": "f24003c6-339b-4498-b995-c4200179d4c6", - "mms_b84d1838": "3616,3523,3660,3614,3599,3621,3588,3254,3532,3642,3474,3475,3477,3479,3482,1202,1203,1204,1205,3417", - "PASS_ID": "1-Ncbwg/naTW6KMMZg7FCtK5ctxvsC84uMK9wM55CJsDJnnoQ8d5jptFdwMkC08e3lKMJw/mGLQXYNL3iCvwTiCQ_109909969_163439093", - "x-visit-time": "1757554980094", - "JSESSIONID": "329E5C258BD7920211D15E263490C895" - } - - print("🧪 开始测试GUI集成类...") - - listener = PddListenerForGUI(log_callback=custom_log_callback) - - try: - print("\n🔵 启动GUI监听...") - success = await listener.start_listening( - cookie_dict=test_cookies, - chat_list_stat=False, - csname=None, - text="测试" - ) - - if success: - print("✅ GUI监听启动成功") - print(f"📊 当前状态: {listener.get_status()}") - - print("\n🟢 监听运行中,将持续运行直到手动停止...") - while True: - if not listener.is_running(): - print("❌ 监听意外停止") - break - await asyncio.sleep(1) - - print("\n🛑 测试完成,开始停止监听...") - listener.stop_listening() - print(f"📊 停止后状态: {listener.get_status()}") - - print("\n✅ GUI集成类测试完成") - print("📋 测试结果:GUI集成类与原main方法逻辑完全一致") - - else: - print("❌ GUI监听启动失败") - if listener.startup_error: - print(f"❌ 错误原因: {listener.startup_error}") - - except KeyboardInterrupt: - print("\n🛑 用户中断测试") - listener.stop_listening() - except Exception as e: - print(f"❌ 测试过程中出错: {e}") - listener.stop_listening() - - -# 同时保留原有的main函数供直接调用 -async def main(): - cookies = { - "api_uid": "CiKdOWi361Z8egCe1uihAg==", - "_nano_fp": "Xpmyn5CJXqmbnqPyl9_MofCSZEeib112RR6N1Ah9", - "rckk": "v8QcMiTwfCX72hCnHFVtXSg4oHNvs6Qs", - "_bee": "v8QcMiTwfCX72hCnHFVtXSg4oHNvs6Qs", - "ru1k": "cb1cec9f-8895-4301-8e7e-a56c2c830cc5", - "_f77": "cb1cec9f-8895-4301-8e7e-a56c2c830cc5", - "ru2k": "f24003c6-339b-4498-b995-c4200179d4c6", - "_a42": "f24003c6-339b-4498-b995-c4200179d4c6", - "mms_b84d1838": "3616,3523,3660,3614,3599,3621,3588,3254,3532,3642,3474,3475,3477,3479,3482,1202,1203,1204,1205,3417", - "PASS_ID": "1-P3kEWMDAT6n7mZjZpx1poVmxfusHa6Qd0s+dhQlG7SiqNA8A5LCsjbbRpck4WHjVSve3G+x0N4ZEL7Dcz2L+vg_109909969_163439093", - "x-visit-time": "1757399096750", - "JSESSIONID": "04601A22B2F955001EFEA28A2F64ED79" - } - - await ChatPdd(cookie=cookies, chat_list_stat=False).main() - - -if __name__ == '__main__': - # 可以选择直接运行main()或者测试GUI集成 - # 测试GUI集成 - asyncio.run(test_gui_integration()) - # asyncio.run(main()) +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +""" +@Project :magua_project +@File :PddUtils.py +@IDE :PyCharm +@Author :lz +@Date :2025/7/17 16:44 +""" +import os +import sys +import threading +from concurrent.futures import ThreadPoolExecutor +import base64 +import json +import random +import time +import traceback + +import execjs +import websockets +import requests +import asyncio +# execjs 已移除 - 不再需要本地生成anti_content +from datetime import datetime +from typing import Dict, Any, Optional, Callable + +# 🔧 尝试导入PyMiniRacer(内置V8引擎,无需外部JavaScript环境) +try: + from py_mini_racer import MiniRacer + PYMINIRACER_AVAILABLE = True +except ImportError: + PYMINIRACER_AVAILABLE = False + MiniRacer = None + +# 新增导入,用于登录功能 +import PIL, numpy as np, cv2 +from Crypto.PublicKey import RSA +from Crypto.Cipher import PKCS1_v1_5, AES +from Crypto.Util.Padding import pad +from pathlib import Path +from loguru import logger + +from Utils.message_models import PlatformMessage + + +# ===== 登录相关类集成开始 ===== + +class Track: + """滑块轨迹生成类""" + + @staticmethod + def get_track(distance): + distance = int(distance) + random.randint(7, 11) + 0.8 + track_list = [ + [ + 0.8, + 178.4, + 1733388719067 + ], + [ + 4.8, + 179.4, + 1733388719169 + ], + [ + 6.8, + 179.4, + 1733388719184 + ], + [ + 8.8, + 179.4, + 1733388719250 + ], + [ + 8.8, + 179.4, + 1733388719266 + ], + [ + 9.8, + 180.4, + 1733388719762 + ], + [ + 10.8, + 180.4, + 1733388719914 + ], + [ + 11.8, + 180.4, + 1733388719930 + ], + [ + 11.8, + 180.4, + 1733388719954 + ], + [ + 12.8, + 180.4, + 1733388719982 + ], + [ + 12.8, + 180.4, + 1733388720030 + ], + [ + 13.8, + 180.4, + 1733388720058 + ], + [ + 14.8, + 180.4, + 1733388720090 + ], + [ + 15.8, + 180.4, + 1733388720110 + ], + [ + 16.8, + 180.4, + 1733388720170 + ], + [ + 17.8, + 180.4, + 1733388720202 + ], + [ + 19.8, + 180.4, + 1733388720218 + ], + [ + 22.8, + 181.4, + 1733388720258 + ], + [ + 23.8, + 181.4, + 1733388720319 + ], + [ + 24.8, + 181.4, + 1733388720428 + ], + [ + 24.8, + 182.4, + 1733388720458 + ], + [ + 26.8, + 182.4, + 1733388720478 + ], + [ + 28.8, + 182.4, + 1733388720495 + ], + [ + 30.8, + 182.4, + 1733388720511 + ], + [ + 32.8, + 183.4, + 1733388720527 + ], + [ + 33.8, + 183.4, + 1733388720558 + ], + [ + 35.8, + 183.4, + 1733388720574 + ], + [ + 36.8, + 184.4, + 1733388720591 + ], + [ + 38.8, + 184.4, + 1733388720619 + ], + [ + 39.8, + 184.4, + 1733388720662 + ], + [ + 40.8, + 184.4, + 1733388720686 + ], + [ + 41.8, + 184.4, + 1733388720710 + ], + [ + 42.8, + 184.4, + 1733388720742 + ], + [ + 43.8, + 184.4, + 1733388720758 + ], + [ + 45.8, + 184.4, + 1733388720783 + ], + [ + 46.8, + 184.4, + 1733388720798 + ], + [ + 48.8, + 184.4, + 1733388720831 + ], + [ + 49.8, + 184.4, + 1733388720850 + ], + [ + 50.8, + 184.4, + 1733388720874 + ], + [ + 52.8, + 184.4, + 1733388720922 + ], + [ + 53.8, + 184.4, + 1733388720958 + ], + [ + 55.8, + 184.4, + 1733388720974 + ], + [ + 55.8, + 184.4, + 1733388720990 + ], + [ + 56.8, + 184.4, + 1733388721010 + ], + [ + 57.8, + 184.4, + 1733388721051 + ], + [ + 58.8, + 184.4, + 1733388721074 + ], + [ + 58.8, + 185.4, + 1733388721090 + ], + [ + 59.8, + 185.4, + 1733388721106 + ], + [ + 60.8, + 185.4, + 1733388721214 + ], + [ + 60.8, + 185.4, + 1733388721270 + ], + [ + 61.8, + 185.4, + 1733388721361 + ], + [ + 63.8, + 186.4, + 1733388721374 + ], + [ + 68.8, + 186.4, + 1733388721390 + ], + [ + 71.8, + 187.4, + 1733388721406 + ], + [ + 72.8, + 187.4, + 1733388721422 + ], + [ + 73.8, + 187.4, + 1733388721442 + ], + [ + 74.8, + 187.4, + 1733388721602 + ], + [ + 75.8, + 187.4, + 1733388721650 + ], + [ + 79.8, + 187.4, + 1733388721674 + ], + [ + 81.8, + 187.4, + 1733388721690 + ], + [ + 83.8, + 188.4, + 1733388721726 + ], + [ + 83.8, + 188.4, + 1733388722055 + ], + [ + 91.8, + 188.4, + 1733388722241 + ], + [ + 92.8, + 188.4, + 1733388722346 + ], + [ + 92.8, + 188.4, + 1733388722494 + ], + [ + 94.8, + 188.4, + 1733388722511 + ], + [ + 97.8, + 188.4, + 1733388722527 + ], + [ + 101.8, + 189.4, + 1733388722543 + ], + [ + 102.8, + 189.4, + 1733388722566 + ], + [ + 103.8, + 189.4, + 1733388722666 + ], + [ + 104.8, + 189.4, + 1733388722722 + ], + [ + 106.8, + 190.4, + 1733388722742 + ], + [ + 108.8, + 190.4, + 1733388722759 + ], + [ + 112.8, + 190.4, + 1733388722775 + ], + [ + 115.8, + 191.4, + 1733388722791 + ], + [ + 116.8, + 191.4, + 1733388722807 + ], + [ + 118.8, + 192.4, + 1733388722911 + ], + [ + 119.8, + 192.4, + 1733388722931 + ], + [ + 120.8, + 192.4, + 1733388722962 + ], + [ + 123.8, + 192.4, + 1733388722987 + ], + [ + 124.8, + 192.4, + 1733388723006 + ], + [ + 125.8, + 192.4, + 1733388723023 + ], + [ + 127.8, + 192.4, + 1733388723098 + ], + [ + 128.8, + 192.4, + 1733388723186 + ], + [ + 129.8, + 193.4, + 1733388723258 + ], + [ + 132.8, + 193.4, + 1733388723278 + ], + [ + 135.8, + 193.4, + 1733388723294 + ], + [ + 137.8, + 194.4, + 1733388723318 + ], + [ + 138.8, + 194.4, + 1733388723562 + ], + [ + 140.8, + 194.4, + 1733388723582 + ], + [ + 144.8, + 195.4, + 1733388723599 + ], + [ + 145.8, + 195.4, + 1733388723622 + ], + [ + 147.8, + 196.4, + 1733388723662 + ], + [ + 148.8, + 196.4, + 1733388723678 + ], + [ + 149.8, + 196.4, + 1733388723703 + ], + [ + 152.8, + 196.4, + 1733388723719 + ], + [ + 153.8, + 196.4, + 1733388723774 + ], + [ + 154.8, + 196.4, + 1733388723846 + ], + [ + 155.8, + 196.4, + 1733388723886 + ], + [ + 157.8, + 196.4, + 1733388723903 + ], + [ + 161.8, + 197.4, + 1733388723918 + ], + [ + 166.8, + 197.4, + 1733388723934 + ], + [ + 167.8, + 197.4, + 1733388723998 + ], + [ + 168.8, + 197.4, + 1733388724026 + ], + [ + 168.8, + 197.4, + 1733388724123 + ], + [ + 171.8, + 198.4, + 1733388724139 + ], + [ + 175.8, + 199.4, + 1733388724154 + ], + [ + 177.8, + 199.4, + 1733388724170 + ], + [ + 179.8, + 199.4, + 1733388724186 + ], + [ + 181.8, + 199.4, + 1733388724202 + ], + [ + 184.8, + 199.4, + 1733388724218 + ], + [ + 185.8, + 199.4, + 1733388724234 + ], + [ + 186.8, + 199.4, + 1733388724255 + ], + [ + 187.8, + 199.4, + 1733388724278 + ], + [ + 188.8, + 199.4, + 1733388724294 + ], + [ + 188.8, + 199.4, + 1733388724326 + ], + [ + 189.8, + 199.4, + 1733388724374 + ], + [ + 191.8, + 199.4, + 1733388724399 + ], + [ + 192.8, + 200.4, + 1733388724444 + ], + [ + 193.8, + 200.4, + 1733388724486 + ], + [ + 194.8, + 200.4, + 1733388724834 + ], + [ + 195.8, + 200.4, + 1733388724850 + ], + [ + 196.8, + 200.4, + 1733388724866 + ], + [ + 198.8, + 200.4, + 1733388724882 + ], + [ + 199.8, + 200.4, + 1733388724898 + ], + [ + 200.8, + 201.4, + 1733388725242 + ], + [ + 204.8, + 201.4, + 1733388725276 + ], + [ + 206.8, + 201.4, + 1733388725301 + ], + [ + 207.8, + 201.4, + 1733388725332 + ], + [ + 208.8, + 201.4, + 1733388725522 + ], + [ + 211.8, + 201.4, + 1733388725547 + ], + [ + 212.8, + 201.4, + 1733388725586 + ], + [ + 212.8, + 201.4, + 1733388725838 + ], + [ + 214.8, + 201.4, + 1733388725854 + ], + [ + 219.8, + 202.4, + 1733388725878 + ], + [ + 220.8, + 202.4, + 1733388725894 + ], + [ + 222.8, + 202.4, + 1733388726427 + ], + [ + 227.8, + 202.4, + 1733388726446 + ], + [ + 228.8, + 202.4, + 1733388726462 + ], + [ + 230.8, + 203.4, + 1733388726642 + ], + [ + 232.8, + 203.4, + 1733388726658 + ], + [ + 234.8, + 203.4, + 1733388726675 + ], + [ + 236.8, + 203.4, + 1733388726690 + ], + [ + 237.8, + 203.4, + 1733388726742 + ], + [ + 238.8, + 203.4, + 1733388726934 + ], + [ + 240.8, + 203.4, + 1733388726950 + ], + [ + 243.8, + 203.4, + 1733388726966 + ], + [ + 245.8, + 203.4, + 1733388726982 + ], + [ + 247.8, + 203.4, + 1733388727118 + ], + [ + 248.8, + 203.4, + 1733388727174 + ], + [ + 248.8, + 204.4, + 1733388727190 + ], + [ + 250.8, + 204.4, + 1733388727214 + ], + [ + 252.8, + 204.4, + 1733388727286 + ], + [ + 252.8, + 204.4, + 1733388727302 + ], + [ + 253.8, + 204.4, + 1733388727322 + ], + [ + 254.8, + 204.4, + 1733388727358 + ], + [ + 256.8, + 204.4, + 1733388727398 + ] + ] + # 检查value是否在轨迹的x值中 + for trajectory in track_list: + if trajectory[0] == distance: + # 如果找到,截取从轨迹开始到该点的子数组 + return [t for t in track_list if t[0] <= distance] + + # 如果value不在x值中,找到最接近value的x值 + closest_x = None + min_diff = float('inf') + for trajectory in track_list: + if abs(trajectory[0] - distance) < min_diff: + min_diff = abs(trajectory[0] - distance) + closest_x = trajectory[0] + + # 截取从轨迹开始到最接近的x值的子数组 + result = [t for t in track_list if t[0] <= closest_x] + result[-1][0] = distance + return result + + +class ImgDistance: + """滑块图像识别距离计算类""" + + def __init__(self, bg, tp): + self.bg = bg + self.tp = tp + + @staticmethod + def imshow(img, winname='test', delay=0): + """cv2展示图片""" + cv2.imshow(winname, img) + cv2.waitKey(delay) + cv2.destroyAllWindows() + + @staticmethod + def pil_to_cv2(img): + """ + pil转cv2图片 + :param img: pil图像, + :return: cv2图像, + """ + img = cv2.cvtColor(np.asarray(img), cv2.COLOR_RGB2BGR) + return img + + @staticmethod + def bytes_to_cv2(img): + """ + 二进制图片转cv2 + :param img: 二进制图片数据, + :return: cv2图像, + """ + # 将图片字节码bytes, 转换成一维的numpy数组到缓存中 + img_buffer_np = np.frombuffer(img, dtype=np.uint8) + # 从指定的内存缓存中读取一维numpy数据, 并把数据转换(解码)成图像矩阵格式 + img_np = cv2.imdecode(img_buffer_np, 1) + return img_np + + def cv2_open(self, img, flag=None): + """ + 统一输出图片格式为cv2图像, + :param img: + :param flag: 颜色空间转换类型, default: None + eg: cv2.COLOR_BGR2GRAY(灰度图) + :return: cv2图像, + """ + if isinstance(img, bytes): + img = self.bytes_to_cv2(img) + elif isinstance(img, (str, Path)): + img = cv2.imread(str(img)) + elif isinstance(img, np.ndarray): + img = img + elif isinstance(img, PIL.Image.Image): + img = self.pil_to_cv2(img) + else: + raise ValueError(f'输入的图片类型无法解析: {type(img)}') + if flag is not None: + img = cv2.cvtColor(img, flag) + return img + + def get_distance(self, bg, tp, im_show=False, save_path=None): + """ + :param bg: 背景图路径或Path对象或图片二进制 + eg: 'assets/bg.jpg' + Path('assets/bg.jpg') + :param tp: 缺口图路径或Path对象或图片二进制 + eg: 'assets/tp.jpg' + Path('assets/tp.jpg') + :param im_show: 是否显示结果, ; default: False + :param save_path: 保存路径, ; default: None + :return: 缺口位置 + """ + # 读取图片 + bg_img = self.cv2_open(bg) + tp_gray = self.cv2_open(tp, flag=cv2.COLOR_BGR2GRAY) + + # 金字塔均值漂移 + bg_shift = cv2.pyrMeanShiftFiltering(bg_img, 5, 50) + + # 边缘检测 + tp_gray = cv2.Canny(tp_gray, 255, 255) + bg_gray = cv2.Canny(bg_shift, 255, 255) + + # 目标匹配 + result = cv2.matchTemplate(bg_gray, tp_gray, cv2.TM_CCOEFF_NORMED) + # 解析匹配结果 + min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(result) + + distance = max_loc[0] + if save_path or im_show: + # 需要绘制的方框高度和宽度 + tp_height, tp_width = tp_gray.shape[:2] + # 矩形左上角点位置 + x, y = max_loc + # 矩形右下角点位置 + _x, _y = x + tp_width, y + tp_height + # 绘制矩形 + bg_img = self.cv2_open(bg) + cv2.rectangle(bg_img, (x, y), (_x, _y), (0, 0, 255), 2) + # 保存缺口识别结果到背景图 + if save_path: + cv2.imwrite(save_path, bg_img) + # 显示缺口识别结果 + if im_show: + self.imshow(bg_img) + return max_loc[0] + + @staticmethod + def decrypt_img(imgstr): + res = [] + s = [-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 24, 3, -1, 20, -1, 17, + 8, + -1, 30, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 12, 22, 10, -1, -1, 15, 14, 6, -1, 5, -1, -1, 7, + 18, + -1, 25, 9, -1, 28, -1, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 1, 21, -1, 31, 13, 16, -1, 26, -1, 27, + -1, 0, + 19, -1, 11, 4, -1, -1, 23, -1, 29, -1, -1, -1, -1, -1, -1] + for r in range(0, len(imgstr), 8): + o = s[ord(imgstr[r])] + i = s[ord(imgstr[r + 1])] + a = s[ord(imgstr[r + 2])] + c = s[ord(imgstr[r + 3])] + u = s[ord(imgstr[r + 4])] + d = s[ord(imgstr[r + 5])] + f = s[ord(imgstr[r + 6])] + p = (31 & o) << 3 | (31 & i) >> 2 + h = (3 & i) << 6 | (31 & a) << 1 | (31 & c) >> 4 + m = (15 & c) << 4 | (31 & u) >> 1 + v = (1 & u) << 7 | (31 & d) << 2 | (31 & f) >> 3 + g = (7 & f) << 5 | 31 & s[ord(imgstr[r + 7])] + res.append(chr(((31 & p) << 3 | h >> 5))) + res.append(chr((31 & h) << 3 | m >> 5)) + res.append(chr((31 & m) << 3 | v >> 5)) + res.append(chr((31 & v) << 3 | g >> 5)) + res.append(chr((31 & g) << 3 | p >> 5)) + y = ''.join(res) + for c in ['#', '@?', '*&%', '<$|>']: + y = y.replace(c, '') + return y.replace('data:image/png;base64,', '') + + def main(self, save_path=None): + return self.get_distance( + bg=base64.urlsafe_b64decode(self.decrypt_img(self.bg)), + tp=base64.urlsafe_b64decode(self.decrypt_img(self.tp)), + save_path=save_path + ) + + # AutiContent类已移除 - 后端会提供所有必要的anti_content + + def gzip_compress(self, data): + """压缩数据""" + try: + if self.ctx: + result = self.ctx.call("gzipCompress", data) + logger.info("成功压缩数据") + return result + except Exception as e: + logger.error(f"调用gzipCompress函数失败: {e}") + + # fallback处理:简单返回原数据 + logger.warning("使用fallback处理,返回原数据") + return data + + +class EncryptTool: + """加密工具类""" + + @staticmethod + def aes_encrypt(text, key, iv): + cipher = AES.new(key.encode('utf-8'), AES.MODE_CBC, iv.encode('utf-8')) + encrypted_text = cipher.encrypt(pad(text.encode('utf-8'), AES.block_size)) + return base64.b64encode(encrypted_text).decode('utf-8') + + @staticmethod + def rsa_encrypt(data): + public_key = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC6zHXNom934tsG9SC73vAUv99bIuSRVaLsuTMY+OL6aS6eB7AuNoU+m9gPCrI7aFrT7CSiKTJ47DNwCNZO52AlLzvB6TjdUwuIXWpinE8VCsYAZgOCrx+mK9Sy0OuwnqNj5D2wUdGoN0nxbl1q2akeAa18A/iBpiXx0SZQexbEowIDAQAB" + rsa_key = RSA.import_key(base64.b64decode(public_key)) # 导入读取到的公钥 + cipher = PKCS1_v1_5.new(rsa_key) # 生成对象 + cipher_text = base64.b64encode(cipher.encrypt(data.encode(encoding="utf-8"))) + return cipher_text.decode("utf-8") + + @staticmethod + def get_key_iv(salt): + t = {'aes_key': r"bN3%cH2$H1@*jCo$", 'aes_iv': r"gl3-w^dN)3#h6E1%"} + if not salt or 9 != len(salt): + return t + else: + n = salt[:1] + r = salt[1:] + o = r[:4] + i = r[4:] + a = list(i) + s = t['aes_key'][:8] if n in ["a", "b"] else t['aes_iv'][:8] + c = 'aes_key' if n in ["a", "b"] else 'aes_iv' + l = '' + if n == 'c': + t[c] = s + r + elif n == 'd': + for u in range(4): + l += a[int(o[u])] + t[c] = s + l + i + return t + + +class PddLogin: + """拼多多登录核心类""" + + def __init__(self, log_callback=None): + super().__init__() + self.login_api = "https://mms.pinduoduo.com/janus/api/auth" + self.headers = { + "authority": "apiv2.pinduoduo.net", + "accept": "*/*", + "accept-language": "zh-CN,zh;q=0.9", + "cache-control": "no-cache", + "content-type": "application/json;charset=UTF-8", + "origin": "https://mms.pinduoduo.com", + "pragma": "no-cache", + "referer": "https://mms.pinduoduo.com/", + "sec-ch-ua": "\"Not_A Brand\";v=\"8\", \"Chromium\";v=\"120\", \"Google Chrome\";v=\"120\"", + "sec-ch-ua-mobile": "?0", + "sec-ch-ua-platform": "\"Windows\"", + "sec-fetch-dest": "empty", + "sec-fetch-mode": "cors", + "sec-fetch-site": "cross-site", + "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" + } + self.verify_auth_token = None + self.cookies = dict() + self.log_callback = log_callback + + def _log(self, message, level="INFO"): + """内部日志方法""" + if self.log_callback: + self.log_callback(message, level) + else: + print(f"[{level}] {message}") + + # 滑块参数生成 + def captcha_collect(self, salt, track_list): + ts = int(round(time.time() * 1000)) + captcha_collect_data = json.dumps({ + "v": "a", + "ts": ts + random.randint(5700, 5950), + "t1": ts, + "t2": ts + random.randint(15700, 19950), + "tp": 3, + "ua": self.headers["user-agent"], + "rf": "", + "platform": 1, + "hl": "000000000001010", + "sc": { + "w": 1536, + "h": 824 + }, + "ihs": 1, + "imageSize": { + "width": 272, + "height": 198 + }, + "del": [ + [ + 0.8, + 177.4, + 1733388719033 + ] + ], + "mel": track_list, + "uel": [track_list[-1]], + "mell": [ + [ + [ + 250.8, + 187.4, + 1733388711350 + ], + [ + 248.8, + 187.4, + 1733388711366 + ], + [ + 240.8, + 187.4, + 1733388711382 + ], + [ + 233.8, + 186.4, + 1733388711398 + ], + [ + 225.8, + 185.4, + 1733388711414 + ], + [ + 222.8, + 184.4, + 1733388711430 + ], + [ + 219.8, + 184.4, + 1733388711446 + ], + [ + 216.8, + 184.4, + 1733388711462 + ], + [ + 208.8, + 183.4, + 1733388711478 + ], + [ + 202.8, + 182.4, + 1733388711494 + ], + [ + 192.8, + 182.4, + 1733388711511 + ], + [ + 168.8, + 180.4, + 1733388711534 + ], + [ + 160.8, + 180.4, + 1733388711550 + ], + [ + 159.8, + 180.4, + 1733388711630 + ], + [ + 156.8, + 180.4, + 1733388711670 + ], + [ + 152.8, + 180.4, + 1733388711686 + ], + [ + 146.8, + 180.4, + 1733388711702 + ], + [ + 136.8, + 180.4, + 1733388711718 + ], + [ + 128.8, + 180.4, + 1733388711734 + ], + [ + 124.8, + 180.4, + 1733388711750 + ], + [ + 118.8, + 180.4, + 1733388711766 + ], + [ + 110.8, + 180.4, + 1733388711782 + ], + [ + 100.8, + 180.4, + 1733388711798 + ], + [ + 91.8, + 180.4, + 1733388711814 + ], + [ + 83.8, + 180.4, + 1733388711830 + ], + [ + 75.8, + 180.4, + 1733388711846 + ], + [ + 70.8, + 180.4, + 1733388711862 + ], + [ + 68.8, + 180.4, + 1733388711878 + ], + [ + 66.8, + 180.4, + 1733388711910 + ], + [ + 64.8, + 180.4, + 1733388711926 + ], + [ + 58.8, + 180.4, + 1733388711942 + ], + [ + 51.8, + 180.4, + 1733388711959 + ], + [ + 42.8, + 180.4, + 1733388711975 + ], + [ + 35.8, + 180.4, + 1733388711994 + ], + [ + 33.8, + 180.4, + 1733388712054 + ], + [ + 32.8, + 180.4, + 1733388712278 + ], + [ + 31.8, + 182.4, + 1733388712295 + ], + [ + 30.8, + 182.4, + 1733388712310 + ], + [ + 28.8, + 182.4, + 1733388712479 + ], + [ + 27.8, + 182.4, + 1733388712502 + ], + [ + 26.8, + 182.4, + 1733388712518 + ], + [ + 24.8, + 182.4, + 1733388712550 + ], + [ + 26.8, + 182.4, + 1733388712886 + ], + [ + 31.8, + 181.4, + 1733388712902 + ], + [ + 35.8, + 180.4, + 1733388712918 + ], + [ + 37.8, + 179.4, + 1733388712934 + ], + [ + 38.8, + 179.4, + 1733388712950 + ], + [ + 44.8, + 179.4, + 1733388712966 + ], + [ + 55.8, + 179.4, + 1733388712986 + ], + [ + 88.8, + 178.4, + 1733388712999 + ], + [ + 164.8, + 172.4, + 1733388713014 + ], + [ + 255.8, + 165.4, + 1733388713030 + ], + [ + 346.8, + 157.4, + 1733388713046 + ], + [ + 360.8, + 161.4, + 1733388715672 + ], + [ + 340.8, + 161.4, + 1733388715687 + ], + [ + 320.8, + 164.4, + 1733388715702 + ], + [ + 287.8, + 168.4, + 1733388715718 + ], + [ + 240.8, + 176.4, + 1733388715735 + ], + [ + 184.8, + 182.4, + 1733388715750 + ], + [ + 144.8, + 184.4, + 1733388715766 + ], + [ + 112.8, + 186.4, + 1733388715782 + ], + [ + 98.8, + 186.4, + 1733388715798 + ], + [ + 95.8, + 186.4, + 1733388715926 + ], + [ + 93.8, + 186.4, + 1733388715942 + ], + [ + 89.8, + 187.4, + 1733388715958 + ], + [ + 79.8, + 188.4, + 1733388715983 + ], + [ + 57.8, + 192.4, + 1733388715999 + ], + [ + 44.8, + 192.4, + 1733388716015 + ], + [ + 36.8, + 192.4, + 1733388716030 + ], + [ + 11.8, + 188.4, + 1733388716073 + ], + [ + -0.2, + 184.4, + 1733388716134 + ], + [ + -1.2, + 184.4, + 1733388716191 + ], + [ + -2.2, + 184.4, + 1733388716206 + ], + [ + -2.2, + 182.4, + 1733388716318 + ], + [ + -0.2, + 181.4, + 1733388716342 + ], + [ + 0.8, + 179.4, + 1733388716878 + ], + [ + 2.8, + 177.4, + 1733388716894 + ], + [ + 3.8, + 177.4, + 1733388716910 + ], + [ + 4.8, + 176.4, + 1733388716926 + ], + [ + 3.8, + 176.4, + 1733388717299 + ], + [ + 2.8, + 176.4, + 1733388717646 + ], + [ + 1.8, + 176.4, + 1733388717662 + ], + [ + 0.8, + 176.4, + 1733388717699 + ], + [ + -0.2, + 176.4, + 1733388717838 + ], + [ + -0.2, + 177.4, + 1733388718115 + ], + [ + 0.8, + 177.4, + 1733388718490 + ] + ], + track_list + ] + }, separators=(',', ':')) + captcha_collect_gzip = self.gzip_compress(captcha_collect_data) + aes_key_iv = EncryptTool.get_key_iv(salt=salt) + captcha_collect = EncryptTool.aes_encrypt(captcha_collect_gzip, aes_key_iv['aes_key'], aes_key_iv['aes_iv']) + return captcha_collect + + # 获取滑块加密参数key和iv + def vc_pre_ck_b(self, backend_anti_content): + self.headers.pop("anti-content", None) + url = "https://apiv2.pinduoduo.net/api/phantom/vc_pre_ck_b" + payload = { + "anti_content": backend_anti_content, + "verify_auth_token": self.verify_auth_token, + "sdk_type": 3, + "client_time": int(round(time.time() * 1000)) + } + response = requests.post(url, headers=self.headers, json=payload, cookies=self.cookies) + self.cookies.update(response.cookies.get_dict()) + return response.json().get("salt") + + # 获取滑块图片 + def obtain_captcha(self, backend_anti_content): + url = "https://apiv2.pinduoduo.net/api/phantom/obtain_captcha" + payload = { + "anti_content": backend_anti_content, + "verify_auth_token": self.verify_auth_token, + "captcha_collect": "" + } + response = requests.post(url, headers=self.headers, json=payload, cookies=self.cookies) + self.cookies.update(response.cookies.get_dict()) + return response.json().get("pictures") + + # 滑块验证 + def verify(self, captcha_collect, verify_code, backend_anti_content): + url = "https://apiv2.pinduoduo.net/api/phantom/user_verify" + payload = { + "verify_code": verify_code, + "captcha_collect": captcha_collect, + "verify_auth_token": self.verify_auth_token, + "anti_content": backend_anti_content + } + response = requests.post(url, headers=self.headers, json=payload, cookies=self.cookies) + self.cookies.update(response.cookies.get_dict()) + result = response.json().get("result") + self._log(f"滑块验证结果: {response.text}") + return result + + # 发送验证码通知给后端,并获取验证码 + def request_verification_code(self, username, store_id, backend_anti_content, phone_number=None): + """向后端请求获取手机验证码""" + self._log(f"开始请求验证码,用户名: {username}, 店铺ID: {store_id}, 手机号: {phone_number}", "INFO") + + # 使用后端下发的anti_content + anti_content = backend_anti_content + self._log(f"使用后端下发的anti_content", "DEBUG") + + self.headers["anti-content"] = anti_content + url = "https://mms.pinduoduo.com/janus/api/user/getLoginVerificationCode" + payload = { + "username": username, + "crawlerInfo": anti_content + } + response = requests.post(url, headers=self.headers, json=payload, cookies=self.cookies) + self._log(f"发送验证码请求结果: {response.text}") + + # 发送消息给后端,告知需要验证码(包含手机号) + self._log("准备向后端发送验证码需求通知", "INFO") + self._send_verification_needed_message(store_id, phone_number) + + # 这里需要等待后端重新下发包含验证码的登录参数 + # 实际实现中这个方法会在接收到新的登录参数后被重新调用 + return None + + def _send_verification_needed_message(self, store_id, phone_number=None): + """向后端发送需要验证码的通知""" + try: + self._log(f"开始发送验证码需求通知,店铺ID: {store_id}, 手机号: {phone_number}", "INFO") + from WebSocket.backend_singleton import get_backend_client + backend = get_backend_client() + self._log(f"获取到后端客户端: {backend is not None}", "DEBUG") + + if backend: + message = { + "type": "connect_message", + "store_id": store_id, + "status": False, + "content": "需要验证码", + "phone_number": phone_number + } + self._log(f"准备发送验证码通知消息: {message}", "DEBUG") + backend.send_message(message) + self._log("✅ 成功向后端发送验证码需求通知(含手机号)", "SUCCESS") + else: + self._log("❌ 后端客户端为空,无法发送验证码需求通知", "ERROR") + except Exception as e: + self._log(f"❌ 发送验证码需求通知失败: {e}", "ERROR") + import traceback + self._log(f"错误详情: {traceback.format_exc()}", "DEBUG") + + def _send_verification_error_message(self, store_id, error_msg): + """向后端发送验证码错误的通知""" + try: + self._log(f"开始发送验证码错误通知,店铺ID: {store_id}, 错误: {error_msg}", "INFO") + from WebSocket.backend_singleton import get_backend_client + backend = get_backend_client() + + if backend: + message = { + "type": "connect_message", + "store_id": store_id, + "status": False, + "content": error_msg + } + self._log(f"准备发送验证码错误消息: {message}", "DEBUG") + backend.send_message(message) + self._log("✅ 成功向后端发送验证码错误通知", "SUCCESS") + else: + self._log("❌ 后端客户端为空,无法发送验证码错误通知", "ERROR") + except Exception as e: + self._log(f"❌ 发送验证码错误通知失败: {e}", "ERROR") + import traceback + self._log(f"错误详情: {traceback.format_exc()}", "DEBUG") + + def _send_login_success_message(self, store_id): + """向后端发送登录成功的通知""" + try: + self._log(f"开始发送登录成功通知,店铺ID: {store_id}", "INFO") + from WebSocket.backend_singleton import get_backend_client + backend = get_backend_client() + + if backend: + message = { + "type": "connect_message", + "store_id": store_id, + "status": True, # 登录成功 + "cookies": self.cookies # 🔥 新增:添加登录生成的cookie信息 + } + self._log(f"准备发送登录成功消息: {message}", "DEBUG") + backend.send_message(message) + self._log("✅ 成功向后端发送登录成功通知", "SUCCESS") + else: + self._log("❌ 获取后端客户端失败", "ERROR") + except Exception as e: + self._log(f"❌ 发送登录成功通知失败: {e}", "ERROR") + import traceback + self._log(f"错误详情: {traceback.format_exc()}", "DEBUG") + + def _send_login_failure_message(self, store_id, error_msg): + """向后端发送登录失败的通知""" + try: + self._log(f"开始发送登录失败通知,店铺ID: {store_id}, 错误: {error_msg}", "INFO") + from WebSocket.backend_singleton import get_backend_client + backend = get_backend_client() + + if backend: + message = { + "type": "connect_message", + "store_id": store_id, + "status": False, # 登录失败 + "content": error_msg # 官方返回的失败原因 + } + self._log(f"准备发送登录失败消息: {message}", "DEBUG") + backend.send_message(message) + self._log("✅ 成功向后端发送登录失败通知", "SUCCESS") + else: + self._log("❌ 获取后端客户端失败", "ERROR") + except Exception as e: + self._log(f"❌ 发送登录失败通知失败: {e}", "ERROR") + import traceback + self._log(f"错误详情: {traceback.format_exc()}", "DEBUG") + + def login_with_params(self, login_params, store_id=None, error_count=0, success_count=0): + """使用后端下发的登录参数进行登录""" + self._log("🚀 [PddLogin] 开始使用参数登录", "INFO") + # 检查验证码字段(兼容 code 和 verification_code) + verification_code = login_params.get("verification_code") or login_params.get("code", "") + self._log( + f"📋 [PddLogin] 登录参数: username={login_params.get('username', 'N/A')}, 包含验证码={bool(verification_code)}", + "DEBUG") + + self.headers["anti-content"] = login_params.get("anti_content", "") + + # 直接使用后端提供的参数构建登录请求 + ts = login_params.get("timestamp", int(round(time.time() * 1000))) + payload = { + "username": login_params.get("username", ""), + "password": login_params.get("password", ""), # 后端已加密 + "passwordEncrypt": True, + "verificationCode": verification_code, + "mobileVerifyCode": "", # 使用兼容的验证码字段 + "sign": "", + "touchevent": { + "mobileInputEditStartTime": ts - random.randint(10000, 20000), + "mobileInputEditFinishTime": ts - random.randint(8000, 15000), + "mobileInputKeyboardEvent": "0|1|1|-6366-6942-142", + "passwordInputEditStartTime": ts - random.randint(5000, 10000), + "passwordInputEditFinishTime": ts - random.randint(3000, 8000), + "passwordInputKeyboardEvent": "0|1|1|195-154-755-477", + "captureInputEditStartTime": "", + "captureInputEditFinishTime": "", + "captureInputKeyboardEvent": "", + "loginButtonTouchPoint": "1263,586", + "loginButtonClickTime": ts - random.randint(100, 1000) + }, + "fingerprint": { + "innerHeight": 906, + "innerWidth": 1707, + "devicePixelRatio": 1.5, + "availHeight": 1027, + "availWidth": 1707, + "height": 1067, + "width": 1707, + "colorDepth": 24, + "locationHref": "https://mms.pinduoduo.com/login/sso?platform=wholesale&redirectUrl=htt", + "clientWidth": 1707, + "clientHeight": 906, + "offsetWidth": 1707, + "offsetHeight": 906, + "scrollWidth": 2882, + "scrollHeight": 906, + "navigator": { + "appCodeName": "Mozilla", + "appName": "Netscape", + "hardwareConcurrency": 16, + "language": "zh-CN", + "cookieEnabled": True, + "platform": "Win32", + "doNotTrack": True, + "ua": self.headers.get("user-agent"), + "vendor": "Google Inc.", + "product": "Gecko", + "productSub": "20030107", + "mimeTypes": "f5a1111231f589322da33fb59b56946b4043e092", + "plugins": "387b918f593d4d8d6bfa647c07e108afbd7a6223" + }, + "referer": "", + "timezoneOffset": -480 + }, + "riskSign": login_params.get("risk_sign", ""), # 后端已加密 + "timestamp": ts, + "crawlerInfo": login_params.get("anti_content", "") + } + + # 添加详细的请求日志 + self._log(f"🔍 [Debug] 登录请求URL: {self.login_api}", "DEBUG") + self._log(f"🔍 [Debug] 登录请求payload: {payload}", "DEBUG") + self._log(f"🔍 [Debug] 登录请求headers: {dict(self.headers)}", "DEBUG") + + response = requests.post(self.login_api, headers=self.headers, json=payload, cookies=self.cookies) + self.cookies.update(response.cookies.get_dict()) + self._log(f"登录响应状态码: {response.status_code}") + self._log(f"登录响应内容: {response.text}") + + # 检查响应内容 + if "需要手机验证" in response.text: + self._log("✅ 检测到需要手机验证的响应", "INFO") + elif "需要验证图形验证码" in response.text: + self._log("✅ 检测到需要图形验证码的响应", "INFO") + else: + self._log("⚠️ 未检测到验证码相关响应", "WARNING") + + if "需要验证图形验证码" in response.text: + self.verify_auth_token = response.json().get("result").get("verifyAuthToken") + + salt = self.vc_pre_ck_b() # 获取生成aes key和iv 的密文值 + pictures = self.obtain_captcha() # 获取验证码图片 + distance = round((ImgDistance(bg=pictures[0], tp=pictures[1]).main() * (272 / 320)) + (48.75 / 2), + 2) # 计算距离 + + track_list = Track.get_track(distance=distance) # 生成轨迹 + captcha_collect = self.captcha_collect(salt=salt, track_list=track_list) # 生成captcha_collect参数 + result = self.verify(captcha_collect=captcha_collect, verify_code=distance) # 检验 + + if result: + self.cookies.update({ + "msfe-pc-cookie-captcha-token": self.verify_auth_token + }) + self.headers["verifyauthtoken"] = self.verify_auth_token + success_count += 1 + # 如果滑块成功 success_count 计数一次 成功8次还是显示验证码则失败 返回False + if success_count < 8: + return self.login_with_params(login_params=login_params, store_id=store_id, + success_count=success_count) + else: + return False + else: + # 如果滑块失败 error_count 计数一次 失败8次则返回False + error_count += 1 + if error_count < 8: + return self.login_with_params(login_params=login_params, store_id=store_id, error_count=error_count) + else: + return False + + elif "需要手机验证" in response.text: + # 检查是否已经包含验证码(兼容 code 和 verification_code) + if verification_code: + # 已有验证码但仍需要验证,验证码可能错误、过期或者其他问题 + self._log(f"带验证码登录失败,验证码: {verification_code}", "WARNING") + # 检查响应中的具体错误信息 + response_data = response.json() + error_msg = response_data.get('errorMsg', '验证码验证失败') + self._log(f"服务器返回错误: {error_msg}", "WARNING") + + # 不要重新发送验证码请求,直接报告验证失败 + self._send_verification_error_message(store_id, error_msg) # 直接使用官方错误信息 + return "verification_code_error" # 返回特殊状态,避免重复发送消息 + else: + # 第一次需要验证码,调用发送验证码方法 + self._log("检测到需要手机验证码,正在调用发送验证码方法", "INFO") + username = login_params.get("username") + backend_anti_content = login_params.get("anti_content") + self._log(f"为用户 {username} 发送验证码,使用后端anti_content", "INFO") + + # 🔥 从响应中提取手机号 + phone_number = None + try: + response_json = response.json() + phone_number = response_json.get("result") # 手机号在result字段中 + if phone_number and isinstance(phone_number, str): + self._log(f"🔍 从登录响应中提取到手机号: {phone_number}", "SUCCESS") + else: + self._log("⚠️ 响应中的result字段不包含有效手机号", "WARNING") + except Exception as e: + self._log(f"❌ 提取手机号时出错: {e}", "DEBUG") + + # 传递后端下发的anti_content和手机号 + self.request_verification_code(username, store_id, backend_anti_content, phone_number) + return "need_verification_code" + + else: + # 登录成功或其他情况 + response_data = response.json() + if response_data.get("success"): + self._log("🎉 登录成功!", "SUCCESS") + # 发送登录成功通知给后端 + self._send_login_success_message(store_id) + return self.cookies + else: + # 登录失败,发送失败通知给后端 + error_msg = response_data.get('errorMsg', '未知错误') + self._log(f"登录失败: {error_msg}", "ERROR") + self._send_login_failure_message(store_id, error_msg) + return "login_failure" # 返回特殊状态,避免重复发送消息 + + +# ===== 登录相关类集成结束 ===== + + +# 定义持久化数据类 - 参考京东的WebsocketManager +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 PddBackendService: + """拼多多后端服务调用类(新版:使用统一后端连接 BackendClient)""" + + def __init__(self): + self.current_store_id = None + + async def connect(self, store_id): + """连接后端服务(使用统一连接,无需独立连接)""" + self.current_store_id = 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 + + # 确保消息包含store_id + if isinstance(platform_message, dict): + if 'store_id' not in platform_message and self.current_store_id: + platform_message['store_id'] = self.current_store_id + + # 通过统一后端连接发送 + backend.send_message(platform_message) + return True + except Exception: + return None + + +class ChatPdd: + @staticmethod + def check_js_files(): + """检查JS文件是否存在""" + # 使用资源路径解析函数 + current_dir = ChatPdd._get_resource_path("static/js") + required_files = ["dencode_message.js", "encode_message.js"] + + for file in required_files: + file_path = os.path.join(current_dir, file) + if not os.path.exists(file_path): + raise FileNotFoundError(f"找不到必需的JS文件: {file_path}") + + return True + + @staticmethod + def _get_resource_path(relative_path): + """获取资源文件的绝对路径(兼容PyInstaller打包环境)""" + try: + print(f"[DEBUG] 正在解析资源路径: {relative_path}") + + # PyInstaller环境下的基础路径 + if hasattr(sys, '_MEIPASS'): + # PyInstaller 临时目录 + base_path = sys._MEIPASS + print(f"[DEBUG] 检测到PyInstaller环境,_MEIPASS: {base_path}") + elif hasattr(sys, 'frozen') and sys.frozen: + # 其他打包环境 + base_path = os.path.dirname(sys.executable) + print(f"[DEBUG] 检测到其他打包环境,executable目录: {base_path}") + else: + # 开发环境 + base_path = os.path.dirname(os.path.abspath(__file__)) + # 向上两级目录到项目根目录 + base_path = os.path.dirname(os.path.dirname(base_path)) + print(f"[DEBUG] 开发环境,计算的项目根目录: {base_path}") + + resource_path = os.path.join(base_path, relative_path) + print(f"[DEBUG] 拼接后的完整资源路径: {resource_path}") + + # 检查路径是否存在 + if os.path.exists(resource_path): + print(f"[DEBUG] ✅ 资源路径存在: {resource_path}") + else: + print(f"[DEBUG] ❌ 资源路径不存在: {resource_path}") + # 尝试列出基础路径的内容 + print(f"[DEBUG] 基础路径 {base_path} 的内容:") + try: + for item in os.listdir(base_path): + item_path = os.path.join(base_path, item) + if os.path.isdir(item_path): + print(f"[DEBUG] 📁 {item}/") + else: + print(f"[DEBUG] 📄 {item}") + except Exception as e: + print(f"[DEBUG] 无法列出目录内容: {e}") + + return resource_path + except Exception as e: + print(f"[ERROR] 获取资源路径失败: {e}") + import traceback + print(f"[ERROR] 堆栈跟踪: {traceback.format_exc()}") + # 降级处理:返回相对路径 + return relative_path + + def __init__(self, cookie, chat_list_stat, csname=None, text=None, log_callback=None): + # 检查JS文件 + self.check_js_files() + + # 获取JS文件路径 - 使用资源路径解析 + js_dir = self._get_resource_path("static/js") + dencode_js_path = os.path.join(js_dir, "dencode_message.js") + encode_js_path = os.path.join(js_dir, "encode_message.js") + + # 首先设置log_callback,以便在初始化过程中使用日志 + self.log_callback = log_callback + + # 读取JS文件 + try: + with open(dencode_js_path, 'r', encoding='utf-8') as f: + jscode = f.read() + + with open(encode_js_path, 'r', encoding='utf-8') as f: + ejscode = f.read() + + # 🔧 优先使用PyMiniRacer(内置V8,无需外部JavaScript环境) + if PYMINIRACER_AVAILABLE: + self._log("✅ 使用PyMiniRacer内置JavaScript引擎(无需Node.js环境)", "INFO") + self.js_engine = "PyMiniRacer" + + # 🔧 关键修复:为PyMiniRacer注入浏览器API(atob/btoa) + browser_api_polyfill = """ + // Base64解码函数(atob polyfill) + if (typeof atob === 'undefined') { + globalThis.atob = function(base64) { + // 简单的Base64解码实现 + var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; + var str = String(base64).replace(/[=]+$/, ''); + var output = ''; + + for (var bc = 0, bs, buffer, idx = 0; buffer = str.charAt(idx++); ~buffer && (bs = bc % 4 ? bs * 64 + buffer : buffer, bc++ % 4) ? output += String.fromCharCode(255 & bs >> (-2 * bc & 6)) : 0) { + buffer = chars.indexOf(buffer); + } + return output; + }; + } + + // Base64编码函数(btoa polyfill) + if (typeof btoa === 'undefined') { + globalThis.btoa = function(str) { + var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; + var output = ''; + + for (var block = 0, charCode, i = 0, map = chars; str.charAt(i | 0) || (map = '=', i % 1); output += map.charAt(63 & block >> 8 - i % 1 * 8)) { + charCode = str.charCodeAt(i += 3/4); + if (charCode > 0xFF) { + throw new Error("'btoa' failed: The string to be encoded contains characters outside of the Latin1 range."); + } + block = block << 8 | charCode; + } + return output; + }; + } + """ + + # 创建PyMiniRacer上下文 + self.ctx_mini = MiniRacer() + # 首先注入浏览器API + self.ctx_mini.eval(browser_api_polyfill) + self._log("✅ 已注入atob/btoa浏览器API", "DEBUG") + # 然后加载实际的JS代码 + self.ctx_mini.eval(jscode) + + self.encode_mini = MiniRacer() + # 同样注入浏览器API + self.encode_mini.eval(browser_api_polyfill) + # 加载实际的JS代码 + self.encode_mini.eval(ejscode) + + # 创建包装对象,统一接口 + self.ctx = self._create_miniracer_wrapper(self.ctx_mini, "dencode_data") + self.encodeex = self._create_miniracer_wrapper(self.encode_mini, "encode_data") + + self._log("✅ PyMiniRacer JavaScript引擎初始化成功(含浏览器API)", "SUCCESS") + else: + # 回退到execjs(需要Node.js或其他外部JavaScript引擎) + self._log("⚠️ PyMiniRacer不可用,回退到execjs(需要Node.js环境)", "WARNING") + self.js_engine = "execjs" + self.ctx = execjs.compile(jscode) + self.encodeex = execjs.compile(ejscode) + try: + engine_name = execjs.get().name + self._log(f"📦 execjs使用的JavaScript引擎: {engine_name}", "INFO") + except: + pass + + except Exception as e: + self._log(f"❌ JavaScript引擎初始化失败: {str(e)}", "ERROR") + raise RuntimeError(f"读取JS文件失败: {str(e)}") + + self._log("初始化ChatPdd实例", "INFO") + + self.chat_list_stat = chat_list_stat + self.text = text + self.cookie = cookie + self.auth_token = None + self.mall_id = None + self.user_id = None + self.csname = csname + self.csid = None + self.staff_list_sent = False + self.uids = set() + self.pool = ThreadPoolExecutor(max_workers=4) + + # WebSocket管理器 + self.ws_manager = WebsocketManager() + + # 后端服务实例 + self.backend_service = PddBackendService() + 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 + + self.headers = { + "authority": "mms.pinduoduo.com", + "accept": "*/*", + "accept-language": "zh-CN,zh;q=0.9", + "anti-content": "0asWfxUeM_VefxObk-_v-fwmt7oS3cmHsWwMkApMVigWOS3hCHfBeF-hHM1_v9LHywCtfzDKkA_F1cUE8_HKBA5D7fVkM2ZDB-KmMxhDM51HXlVrfK_LG4Dc3S0wimw8XYPyXdgJnUv8ndgjs0EynYv8n0XjXU9YXY4Pt0ZVgTgrsIUEeMzMk7s02E3oo-zMdF35CIMWVetBhs95lGG5xENwZwX0CwXH2799MeMjVKzk5DD4Kbs2dM1bD-fJ1F-RImB3SHBkVKDJZbZoUbL4UMk8DFtrISf8CMD4Obk_CM3ICIB_F90ws9ZQ0gcOpVdIIz2PLPVcYt5X0yqYiwjtuNVfdNSf0rmfmNIndXj8G8Nyn4ianRaanoucDp0Dfvd36B6eamPGwOFS0TAyo7nXH_nwxnGS28AgVnqPyPuy8jczb1Oe3xZuPHoJ97BBbaTMfV9OcQ23T-mAUel", + "cache-control": "no-cache", + "content-type": "application/json", + "origin": "https://mms.pinduoduo.com", + "pragma": "no-cache", + "referer": "https://mms.pinduoduo.com/chat-merchant/index.html?r=0.4940608191737519", + "sec-ch-ua": "\"Not_A Brand\";v=\"8\", \"Chromium\";v=\"120\", \"Google Chrome\";v=\"120\"", + "sec-ch-ua-mobile": "?0", + "sec-ch-ua-platform": "\"Windows\"", + "sec-fetch-dest": "empty", + "sec-fetch-mode": "cors", + "sec-fetch-site": "same-origin", + "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" + } + + self.store_id = "538727ec-c84d-4458-8ade-3960c9ab802c" # 可以根据需要修改 + self.user_tokens = {} # 存储用户token信息 + + def _log(self, message, level="INFO"): + """内部日志方法""" + if self.log_callback: + self.log_callback(message, level) + else: + timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + color_map = { + "ERROR": "\033[91m", + "WARNING": "\033[93m", + "SUCCESS": "\033[92m", + "DEBUG": "\033[96m", + } + color = color_map.get(level, "") + reset = "\033[0m" + print(f"{color}[{timestamp}] [{level}] {message}{reset}") + + def _create_miniracer_wrapper(self, mini_racer_ctx, function_name): + """创建PyMiniRacer的包装器,使其接口与execjs兼容""" + class MiniRacerWrapper: + def __init__(self, ctx, func_name): + self.ctx = ctx + self.func_name = func_name + + def call(self, func_name, *args): + """调用JavaScript函数""" + try: + import json + + # 将参数转换为JavaScript可用的格式 + js_args = ', '.join([f'"{arg}"' if isinstance(arg, str) else str(arg) for arg in args]) + + # 🔧 关键修复:使用JSON.stringify包装结果,确保返回可序列化的Python对象 + # 这样可以避免JSObject类型转换问题 + js_code = f"JSON.stringify({func_name}({js_args}))" + + # 执行JavaScript并获取JSON字符串结果 + json_str = self.ctx.eval(js_code) + + # 将JSON字符串解析为Python对象 + if json_str: + return json.loads(json_str) + else: + return None + + except Exception as e: + raise Exception(f"PyMiniRacer执行失败: {e}") + + return MiniRacerWrapper(mini_racer_ctx, function_name) + + # 重连机制方法 + 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): + """发送消息到后端(使用统一连接,不等待回复)""" + # 首先检查后端服务连接状态 + if not self.backend_connected: + # 尝试重新连接 + if self.store_id: + self.backend_connected = await self.connect_backend_service(self.store_id) + + if not self.backend_connected: + self._log("❌ 后端服务未连接,尝试重新连接失败", "WARNING") + return None + + try: + # 发送消息到后端(使用统一连接,不等待回复) + await self.backend_service.send_message_to_backend(platform_message) + self._log("✅ 消息已发送到后端,等待后端AI处理并通过GUI转发回复", "INFO") + return None + + except Exception as e: + self._log(f"❌ 发送消息到后端失败: {e}", "ERROR") + return None + + def get_auth_token(self): + url = "https://mms.pinduoduo.com/janus/api/subSystem/getAuthToken" + data = {"subSystemId": 17, + "anti_content": "0asAfx5E-wCElqJNXaKt_UKccG7YNycjZoPYgO1YTmZoApN7QJU5XT_JuYdPZ4uvLPQFgIcyXOKqm8xGrPjy0lxn0gaXr9ac0TynYEJnDwdj-WEJnwK3G4kc3M0TiPgtB11eBeZkB1hkBxUHFOtjfqz-eAkgc2aa4sZuIJwHOptYX14VK1NJSqIYfqmw6jpQTXs574CfWMFxT1RPTIB2MKBjC199pXpkbdtXxnn2mA4C1YdNnTUrNa_Wvn5mYj5XadnDbNT7xNuVeYXrsTsiipUr6xn2AAXKoYmv6j0PL92PtCTMfZzzfTfjutCgvBTzxFw-2LeeRIkFBATU1Npg1ScUFRv-1Ukrs3AklVAbBiEbBJhzcf2cXKfNH50X9QUFCd_Y1FhLW1BBuI-KEUFYK3lZTB3g_BlLDk8ti-JXmbalXzWAb6V2hX0fZE9n2L0GITFInNS"} + response = requests.post(url, cookies=self.cookie, headers=self.headers, json=data).json() + self.auth_token = response["result"]["authToken"] + + def get_mall_id(self): + url = "https://mms.pinduoduo.com/chats/userinfo/realtime?get_response=true" + data = {"get_response": "true"} + response = requests.get(url, cookies=self.cookie, headers=self.headers, params=data).json() + self.mall_id = response["mall_id"] + self.user_id = response["id"] + + def get_assign_cslist(self): + url = "https://mms.pinduoduo.com/latitude/assign/getAssignCsList" + data = { + "wechatCheck": True + } + data = json.dumps(data, separators=(',', ':')) + response = requests.post(url, headers=self.headers, cookies=self.cookie, data=data).json() + cslist = response["result"]["csList"] + keys = cslist.keys() + for key in keys: + username = cslist[key].get("username") + if username == self.csname: + self.csid = key + break + + def tab_mall(self, uid): + """执行转接操作""" + self._log(f"🔄 开始执行转接操作 - 用户ID: {uid}, 目标客服ID: {self.csid}", "INFO") + + try: + url = "https://mms.pinduoduo.com/plateau/chat/move_conversation" + request_id = int(time.time() * 1000) + + data = { + "data": { + "cmd": "move_conversation", + "request_id": request_id, + "conversation": { + "csid": self.csid, + "uid": uid, + "need_wx": False, + "remark": "无原因直接转移" + }, + "anti_content": "0asAfxvYXyIgYgE2h9U0sXh6Ia3tZOAzI_rl3hixs1Z5DB-B1cswVK2UB8LfUO8H3OUKdF214-SgPVnY8SYvSdmwnKDoPXAs4OdtzfKwkT6gAO2d4R8ZVTw3a7j9BdB3Hmq7j0bh2LQrRzlt88pkQr-s3jMzWH5QPiz13wQNsJY6h9dnigBE26Ww1dKZcWbsKS4GsgY9_joGBS2FZUz94cbCvE2Ij9nO93b5kR50m9kI1lfBHvDZVIC9rLB-S4Qs4J9ebSsosuNskY9nhgXzCuNoilSQgJaMkP2Rgga2i54nWTTNfnZ4W9GvigkSnwzweN9sx-89BztNCasVkP2C2Kkipif2Dv-t7e2O9MnmtWaKm4wFlHLR7YmeyF3y5gxPRuHlO-gD7BfFle8z71lIV-7SyOLCd83FMUCsRwedwv3-TNTF_VRw_G1DHrptFYTAck8p8qIkxA_PZ14PXct1ZS2zAoZZcTMZZSpJ1jP9RlQARtnNNP0IzCdHiL4JzRQBkkKmwGGXay0i4urnXNm5qBgejFcVCkx8XYdeGBp4p-l1TjKoy603__johKFKZQksRHjOKma5DplwQwg8c3Uo1-mDAUmgAC0XfV2rkxzXj3ABARPlyEaMU8AxUZioaFRa0e5eit6r06uY9m5SyU_mdbTfLIYcKWjRA_UNQisJOUenCbMl_Bywq1KcA6TTzJ9ZHu4xz0o7ctROYLlvhEhAi3bJjFNho6l-TPZK4zSJoj" + }, + "client": "WEB", + "anti_content": "0asAfxvYXNIgYgd2i9U0sXh6IabtZOAdI_LlbhNUvZZ5DB-B1cswVK2UBjxfUOjHbOUKdF214-SgPVnYjSYvSdmwnKDoPXAs4OdtzfKwkT6gAO2d4RjZVTwbJFiC0P0rR6LbH89rLVuGHiG6vm3fO6rq_rAyLQlw8CAIuESMR4L308Ctz3V0m9ZhS8tdapQr_YY2vZZk9sAJsMd2ZapG92zm2lO9FLVBjCur2KWwac9nua9g8ztOawyo9ur0Dd96xMkCXrd4J4hR4Kk9zqZBDTlRPkMdzzkJ5Osgi40EC3oVAhSPA9MawGCs33Vnd4bSJER9_b_D9g1sfdeZZKsgo25k0WRfs7dM17E2ug-pPEL5_ib53TuaxLHJq5pGG4jMUjAHRV-gbDffut1Yjy1A7K-6E3zO4tDuUq5T0LPHjf9bB7Apofgr_U_4Aq-7l8x2ks8DhhgOhblTvwrO4owZvuUXA0hfSIUPYK9fIoSUH6aaL8abMMVCvI__qF8Yw1oaMp0d0-fbwMhXQ8fBZYoajXXJv-RV3B2GQEowvMHEUtjDz50jUVduq5fKL9R-WsBRjX0aiExVKLw-PZzxsVup0ZuOYxnPofN92EqKfbvriIWFMsh80eGrvq1vsD1BrMx_mcPMmUmHHEY7wct5lePa0b2Lt5_byqRbbcpohrQrHLlH6JJaTMKQuffyT-60110e5oz_Cz0NbG88TyxOGKijW7BimL5RTjSOTPsK4zS8oi" + } + + self._log(f"📤 发送转接请求 - URL: {url}", "DEBUG") + self._log(f"📝 请求数据: {json.dumps(data, ensure_ascii=False)}", "DEBUG") + + data_str = json.dumps(data, separators=(',', ':')) + response = requests.post(url, headers=self.headers, cookies=self.cookie, data=data_str) + + self._log(f"📥 HTTP状态码: {response.status_code}", "DEBUG") + self._log(f"📄 响应内容: {response.text}", "DEBUG") + + # 解析响应 + try: + result = response.json() + self._log(f"🔍 解析后的响应: {json.dumps(result, ensure_ascii=False)}", "DEBUG") + + # 检查响应结构 + if result.get("success"): + result_data = result.get("result", {}) + if result_data.get("result") == "success" or result_data.get("result") == "ok": + self._log(f"✅ 转接成功 - 用户 {uid} 已转接给客服 {self.csid}", "SUCCESS") + return True + else: + error_code = result_data.get("error_code", "未知") + error_msg = result_data.get("error_msg", "未知错误") + self._log(f"❌ 转接失败 - 错误码: {error_code}, 错误信息: {error_msg}", "ERROR") + return False + else: + self._log(f"❌ 转接请求失败 - 响应标记为失败", "ERROR") + return False + + except json.JSONDecodeError as e: + self._log(f"❌ 解析响应JSON失败: {e}", "ERROR") + self._log(f"❌ 原始响应: {response.text}", "ERROR") + return False + + except requests.RequestException as e: + self._log(f"❌ 请求异常: {e}", "ERROR") + return False + except Exception as e: + self._log(f"❌ 转接操作异常: {e}", "ERROR") + traceback.print_exc() + return False + + @staticmethod + def forward_message_to_platform(store_id: str, recv_pin: str, content: str): + """转发消息到拼多多平台""" + try: + pdd_mgr = WebsocketManager() + pdd_shop_key = f"拼多多:{store_id}" + pdd_entry = pdd_mgr.get_connection(pdd_shop_key) + if pdd_entry: + # 找到拼多多连接,使用拼多多转发逻辑 + platform_info = pdd_entry.get('platform') or {} + ws = platform_info.get('ws') + loop = platform_info.get('loop') + pdd_instance = platform_info.get('pdd_instance') # ChatPdd实例引用 + print( + f"[PDD Forward] shop_key={pdd_shop_key} has_ws={bool(ws)} has_loop={bool(loop)} has_pdd_instance={bool(pdd_instance)} recv_pin={recv_pin}") + if ws and loop and pdd_instance: + async def _send_pdd(): + # 直接调用ChatPdd实例的send_ai_reply方法 + await pdd_instance.send_ai_reply(recv_pin, content) + + import asyncio as _asyncio + _future = _asyncio.run_coroutine_threadsafe(_send_pdd(), loop) + try: + _future.result(timeout=5) # 拼多多需要HTTP请求,给更长时间 + print(f"[PDD Forward] 已转发到平台: uid={recv_pin}, content_len={len(content)}") + except Exception as fe: + print(f"[PDD Forward] 转发提交失败: {fe}") + else: + print("[PDD Forward] 条件不足,未转发:", + {'has_ws': bool(ws), 'has_loop': bool(loop), 'has_pdd_instance': bool(pdd_instance), + 'has_content': bool(content)}) + else: + print(f"[PDD Forward] 未找到拼多多连接: {pdd_shop_key}") + except Exception as e: + print(f"[PDD Forward] 拼多多转发失败: {e}") + + @staticmethod + def transfer_customer_service(customer_service_id: str, user_id: str, store_id: str): + """拼多多平台转接""" + try: + pdd_mgr = WebsocketManager() + pdd_shop_key = f"拼多多:{store_id}" + pdd_entry = pdd_mgr.get_connection(pdd_shop_key) + if pdd_entry: + platform_info = pdd_entry.get('platform') or {} + 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并执行转接 + pdd_instance.csid = customer_service_id + + def _transfer(): + result = pdd_instance.tab_mall(user_id) + if result: + print(f"[PDD Transfer] ✅ 转接成功") + else: + print(f"[PDD Transfer] ❌ 转接失败") + return result + + # 在线程池中执行同步转接操作 + import concurrent.futures + with concurrent.futures.ThreadPoolExecutor() as executor: + future = executor.submit(_transfer) + try: + future.result(timeout=10) # 转接操作可能需要更长时间 + except Exception as fe: + print(f"[PDD Transfer] 转接操作异常: {fe}") + else: + print(f"[PDD Transfer] 条件不足: has_pdd_instance={bool(pdd_instance)}, has_loop={bool(loop)}") + else: + print(f"[PDD Transfer] 未找到拼多多连接: {pdd_shop_key}") + except Exception as e: + print(f"[PDD Transfer] 拼多多转接失败: {e}") + + async def send_ai_reply(self, uid, content): + """发送AI回复给指定用户""" + self._log(f"开始发送AI回复给用户 {uid}", "INFO") + self._log(f"回复内容: {content[:50]}...", "DEBUG") + try: + ts = int(time.time() * 1000) + url = "https://mms.pinduoduo.com/plateau/chat/send_message" + data = { + "data": { + "cmd": "send_message", + "anti_content": "0asWfqzFdjPssgua6CCWyeEK7LAktf0_UFChvlj8kD8Mk1DVZchg4qURiKAS4iSfiaz9Fjg43ThZZ4_wNh5Onk0e5Tw2mGdwpTEPDZlPdfZwBpyESSDiz0Xalv6mvr0o0uQR6yRlG-vscbK8jYGRNcAr2UktX3bMKhjjx7YvVGbi_poiyypBuOwM_2qZyIM96tdNTfb_AMy0fe1UrN2SqUzfm2stmb4TJldGN9b1mTPr-6t4R9k3pquV_XHwBr20J4__P71fxPBmcXOmXTnQWJs2ddt4KKOIbPIIanas08VNiv9nsHbtvXKp8_CMdL8FQMPmEMDmM6JfjZ4LJp5l-x0BjpBbdWyoztNYqSbBe8hscA1hPFM_R1iR9E-AI-CyuehipODW-ADCL0xEWK875510RsV7mLFQKG0kWO--krZzFFi_6lhdfq3_JhBjDDd3YS1exxSMMS7yDJgskoumfAcu-y1YDO8OdcKWwx3Z0nOqUlnF8LekN7TqXhNtKTgfDZPThVT3JcNm_2TljXRhGWxTZT4o25TTVKo5KPeztcJU5NddFd38nmvo0k8R8HVIkmzOYyaerCDCm1ZDfYgNFx-2vM_JkcMYFdkV7C3BKeg7DxZXHfexAT3UKgruBWYrSs_dBB0X-QkJ2ZcmpStB6KJkZ1azrAAxdw_HMj5UIq4vEXxrA1-YAzJEhMoU0-MncDpoBZfiky7Sr4nDGYrcn86AAYWynOUn0US2DAkICABvSSqkh0vCRpdVeZRfOFsdFWF43KEIph-ckMxDsyq6G3rWXbvPkaAlv67cR1v0N2oCQzDVDT-InNv", + "request_id": ts, + "message": { + "to": { + "role": "user", + "uid": uid + }, + "from": { + "role": "mall_cs" + }, + "ts": int(ts / 1000), + "content": content, + "msg_id": None, + "type": 0, + "is_aut": 0, + "manual_reply": 1, + "status": "read", + "is_read": 1, + "hash": "1d1359dfb7c141fd95432a22cdc7f43b51434041a1a6016ef6ea2ea2b1abc05a" + }, + "random": "c5fc9c61194294c478a6fb395b1c558f" + }, + "client": "WEB", + "anti_content": "0asAfa5E-wCEsa-JeFwdFfTDtzhMbODIsAT-eu_-CigAHMbcWIfBEUFcI-Kwd93IYTWtfvkSeuwUKO57jwISBuZkzfCe-2VkBFSD-ack-ZKIXRockMwxBgyDql6V3xKKD-fCkBFZkzeKeBwUE-cSD-KHkBb-kzFVk-szd009EAPjDvBcaPIXxHGB_IxWWlIXMnxghvniTodnXt2scz7sxhJV_CEBcCez4H92maXI2d1qgoYswoG4Mf_oylph0nGigjnqWzXqwTYh0y4W6yYs6PxHyXv8slpHrXyseoqZhfqdrdn5E9BCt24mBfkEFg-vgc0wUw2D-vgE7KUM1FIdKb2_Mbl_6xj_8bvk-e1H6ObSksLCDiVCDAbkYU6Y_SMBfgA5CHUM0QuYhznXIwnTanGM2jugCnxPYPmYjyOvjKHEbaV3nvMuJ6V2cX0fZE99vcfY3TF1nNr" + } + data = json.dumps(data, separators=(',', ':')) + response = await asyncio.get_event_loop().run_in_executor( + self.pool, + lambda: requests.post(url, headers=self.headers, cookies=self.cookie, data=data) + ) + + # 打印HTTP状态与返回体片段 + self._log(f"PDD发送状态: HTTP {response.status_code}", "INFO") + try: + self._log(f"PDD返回体: {response.text[:500]}", "DEBUG") + except Exception: + pass + + if response.status_code == 200: + self._log(f"✅ 已发送AI回复给用户 {uid}: {content}", "SUCCESS") + return True + else: + self._log(f"❌ 发送AI回复失败,HTTP状态码: {response.status_code}", "ERROR") + self._log(f"响应内容: {response.text}", "DEBUG") + return False + + except Exception as e: + self._log(f"❌ 发送AI回复失败: {e}", "ERROR") + traceback.print_exc() + return False + + async def send_message_external(self, uid, content): + """外部消息发送接口,用于接收后端转发的回复""" + self._log(f"[External] 收到外部消息发送请求: uid={uid}, content_len={len(content) if content else 0}", "INFO") + try: + if not uid or not content: + self._log("❌ [External] 参数不完整", "ERROR") + return False + + result = await self.send_ai_reply(uid, content) + if result: + self._log(f"✅ [External] 外部消息发送成功: uid={uid}", "SUCCESS") + else: + self._log(f"❌ [External] 外部消息发送失败: uid={uid}", "ERROR") + return result + except Exception as e: + self._log(f"❌ [External] 外部消息发送异常: {e}", "ERROR") + return False + + async def get_all_customer(self, ws): + """异步获取客服列表""" + try: + # PDD获取客服列表的API + url = "https://mms.pinduoduo.com/latitude/assign/getAssignCsList" + data = { + "wechatCheck": True + } + data = json.dumps(data, separators=(',', ':')) + + # 使用线程池执行同步请求 + response = await asyncio.get_event_loop().run_in_executor( + self.pool, + lambda: requests.post(url, headers=self.headers, cookies=self.cookie, data=data) + ) + + if response.status_code == 200: + result = response.json() + cslist = result.get("result", {}).get("csList", {}) + print("我们收集到的客服列表原信息为:", str(cslist)) + if cslist: + # 转换客服列表格式 + self.customer_list = [] + for key, customer in cslist.items(): + customer_info = { + "staff_id": str(key), # 客服ID + "name": customer.get("username", ""), # 客服名称 + "status": customer.get("status", 0), # 状态 + "department": customer.get("department", ""), # 部门 + "online": True # 在线状态 + } + self.customer_list.append(customer_info) + self._log(f"✅ 成功获取客服列表,共 {len(self.customer_list)} 个客服", "SUCCESS") + return True + return False + except Exception as e: + self._log(f"❌ 获取客服列表失败: {e}", "ERROR") + return False + + async def send_staff_list_to_backend(self): + """发送客服列表到后端""" + try: + if not hasattr(self, 'customer_list') or not self.customer_list: + self._log("⚠️ 客服列表为空", "WARNING") + return False + + # 创建消息模板 + message_template = PlatformMessage( + type="staff_list", + content="客服列表更新", + data={ + "staff_list": self.customer_list, + "total_count": len(self.customer_list) + }, + store_id=self.store_id + ) + + # 通过后端服务发送 + await self.backend_service.send_message_to_backend(message_template.to_dict()) + self._log(f"发送客服列表消息的结构体为: {message_template.to_json()}") + self._log(f"✅ [PDD] 成功发送客服列表到后端,共 {len(self.customer_list)} 个客服", "SUCCESS") + print(f"🔥 [PDD] 客服列表已上传到后端: {len(self.customer_list)} 个客服") + return True + + except Exception as e: + self._log(f"❌ 发送客服列表到后端异常: {e}", "ERROR") + return False + + async def handle_transfer_message(self, message_data): + """处理转接消息""" + self._log("收到转接指令", "INFO") + try: + content = message_data.get("content", "") # 转接目标客服ID + receiver_info = message_data.get("receiver", {}) + uid = receiver_info.get("id") # 用户ID + + if not content or not uid: + self._log("❌ 转接消息信息不完整", "WARNING") + self._log(f"消息内容: {message_data}", "DEBUG") + return False + + self._log(f"准备将用户 {uid} 转接给客服 {content}", "INFO") + + # 设置转接目标客服ID + self.csid = content + + # 在线程池中执行同步的转接操作 + try: + # 使用线程池执行同步方法 + success = await asyncio.get_event_loop().run_in_executor( + self.pool, + self.tab_mall, + uid + ) + + if success: + self._log(f"✅ 转接成功: 用户 {uid} 已转接给客服 {content}", "SUCCESS") + return True + else: + self._log(f"❌ 转接失败: 用户 {uid}", "ERROR") + return False + + except Exception as e: + self._log(f"❌ 执行转接操作失败: {e}", "ERROR") + return False + + except Exception as e: + self._log(f"❌ 处理转接消息失败: {e}", "ERROR") + traceback.print_exc() + return False + + def should_filter_robot_message(self, message_data): + """专门判断是否为机器人消息需要过滤""" + try: + message_info = message_data.get("message", {}) + + # 1. 基于消息类型过滤机器人特殊消息 + msg_type = message_info.get("type") + if msg_type == 31: # 机器人干预消息(如:机器人已暂停接待) + return True + if msg_type == 8: # 平台系统卡片消息(如:售后提醒、平台通知等) + self._log(f"🚫 过滤系统卡片消息(type=8): {message_info.get('content', '')[:50]}", "DEBUG") + return True + + # 2. 基于模板名称识别机器人消息 + template_name = message_info.get("template_name", "") + robot_templates = [ + "mall_robot_man_intervention_and_restart", # 机器人暂停接待消息 + "mall_robot_text_msg", # 机器人自动回复消息 + "aftersales_hosting_warning_card", # 售后托管警告卡片 + # 可根据实际情况添加更多机器人模板 + ] + if template_name in robot_templates: + self._log(f"🚫 过滤模板消息: {template_name}", "DEBUG") + return True + + # 3. 基于info字段识别系统卡片消息(售后提醒、订单提醒等) + info = message_info.get("info", {}) + if info and isinstance(info, dict): + # 如果info中包含mbtn_list(按钮列表),通常是系统卡片 + if info.get("mbtn_list") or info.get("text_list"): + self._log(f"🚫 过滤系统卡片消息(含info.mbtn_list): {message_info.get('content', '')[:50]}", "DEBUG") + return True + + # 4. 基于机器人特殊标志过滤 + if message_info.get("conv_silent") is True: # 静默会话标志 + return True + + if message_info.get("no_unreply_hint") == 1: # 无需回复提示标志 + return True + + # 5. 基于消息内容识别机器人提示消息和平台系统消息 + content = message_info.get("content", "") + robot_content_patterns = [ + "机器人未找到对应的回复", + "机器人已暂停接待", + ">>点此【立即恢复接待】<<", + "点击添加", + "[当前用户来自", + "请尽快解决售后问题", # 平台售后提醒 + "平台介入退款", # 平台售后提醒 + "请尽快处理售后", # 平台售后提醒 + ] + + if any(pattern in content for pattern in robot_content_patterns): + self._log(f"🚫 过滤系统提示消息: {content[:50]}", "DEBUG") + return True + + # 6. 基于biz_context中的机器人标识 + biz_context = message_info.get("biz_context", {}) + if biz_context.get("robot_msg_id"): # 有机器人消息ID + return True + + # 不是机器人消息,不过滤 + return False + + except Exception as e: + self._log(f"判断机器人消息时出错: {e}", "DEBUG") + return False # 出错时不过滤,保持原有行为 + + async def handle_customer_message(self, message_data): + """处理来自后端的客服消息""" + self._log("收到来自后端的客服消息", "INFO") + try: + content = message_data.get("content", "") + receiver_info = message_data.get("receiver", {}) + uid = receiver_info.get("id") + + if uid and content: + self._log(f"准备发送客服消息给用户 {uid}: {content}", "INFO") + await self.send_ai_reply(uid, content) + self._log(f"客服消息发送完成 - 目标用户: {uid}", "SUCCESS") + else: + self._log("客服消息数据不完整", "WARNING") + self._log(f"消息内容: {message_data}", "DEBUG") + except Exception as e: + self._log(f"处理客服消息失败: {e}", "ERROR") + traceback.print_exc() + + async def process_incoming_message(self, message_data, wss, store): + """处理接收到的消息""" + try: + # 🔥 过滤机器人消息 + if self.should_filter_robot_message(message_data): + self._log("🤖 检测到机器人消息,已过滤不发送给后端", "DEBUG") + return + + message_info = message_data.get("message", {}) + if not message_info: + return + + nickname = message_info.get("nickname") + content = message_info.get("content") + goods_info = message_info.get("info", {}) + from_info = message_info.get("from", {}) + uid = from_info.get("uid") + # role = from_info.get("role") + + # if role != "user": + # self._log(f"过滤非用户消息 (role={role})", "DEBUG") + # return + + if nickname and content and uid: + self._log(f"用户消息 - {nickname}: {content}", "INFO") + self._log(f"用户UID: {uid}", "DEBUG") + self.uids.add(uid) + + # 消息类型检测 + lc = str(content).lower() + if any(ext in lc for ext in [".jpg", ".jpeg", ".png", ".gif", ".webp"]): + msg_type = "image" + elif any(ext in lc for ext in [".mp4", ".avi", ".mov", ".wmv", ".flv"]): + msg_type = "video" + else: + msg_type = "text" + + # 订单卡片组装(使用全角冒号,符合文档) + if "订单编号" in str(content) and goods_info: + content = f"商品id:{goods_info.get('goodsID')} 订单号:{goods_info.get('orderSequenceNo')}" + msg_type = "order_card" + + # 商品卡片检测(基于内容关键词和goods_info) + elif any(keyword in lc for keyword in ['goods.html', 'item.html', 'item.jd.com', '商品卡片id:']) or \ + (goods_info and goods_info.get('goodsID') and not goods_info.get('orderSequenceNo')): + msg_type = "product_card" + + # 创建消息模板(符合文档) + message_template = PlatformMessage.create_text_message( + content=content, + sender_id=str(uid), + store_id=self.store_id + ) + message_template.msg_type = msg_type + + try: + print(f"📤(SPEC) 发送到AI: {json.dumps(message_template.to_dict(), ensure_ascii=False)[:300]}...") + except Exception: + pass + + # 从后端服务获取AI回复(单连接模式:仅转交,不本地立即回复) + ai_result = await self.get_ai_reply_from_backend(message_template.to_dict()) + + if isinstance(ai_result, str) and ai_result.strip(): + # 发送回复消息(仅当本地生成/降级时) + await self.send_ai_reply(uid, ai_result) + self._log(f"📤 已发送回复: {ai_result[:100]}...", "INFO") + else: + # 正常链路:已转交后端AI,等待后端异步回传并由 GUI 转发到平台 + self._log("🔄 已转交后端AI处理,等待平台回复下发", "INFO") + + except Exception as e: + self._log(f"❌ 消息处理失败: {e}", "ERROR") + traceback.print_exc() + + @staticmethod + async def heartbeat(wss): + while True: + m = random.randint(100, 200) + h = [0, 0, 0, 0, 0, 0, 0, m, 0, 0, 0, 1, 0, 0, 0, 0] + h = bytes(h) + await wss.send(bytes(h)) + await asyncio.sleep(15) + + async def on_message(self, wss, store): + self._log("开始监听消息", "INFO") + while True: + try: + message = await wss.recv() + self._log(f"收到新消息 {base64.b64encode(message).decode()}", "DEBUG") + try: + text = self.ctx.call("dencode_data", base64.b64encode(message).decode()) + if not isinstance(text, dict): + continue + push = text.get("push_data") or {} + data = push.get("data") + if not data: + self._log("push_data.data 为空,忽略", "DEBUG") + continue + + print(f"原始数据格式打印:{data}") + for d in data: + await self.process_incoming_message(d, wss, store) + + except Exception as e: + # buffer error是正常的,调整为DEBUG级别 + if "buffer error" in str(e): + self._log(f"解析消息失败: {e}", "DEBUG") + else: + self._log(f"解析消息失败: {e}", "ERROR") + traceback.print_exc() + except Exception as e: + # self._log(f"处理消息失败: {e}", "ERROR") + # traceback.print_exc() + pass + + async def message_monitoring(self, cookies_str, store, stop_event=None): + """消息监听主方法""" + print("✅ DEBUG 进入拼多多message_monitoring方法") + print(f"参数验证: cookies={bool(cookies_str)}") + + # 连接后端AI服务 - 使用店铺ID + store_id = str(store.get('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() + + # 充值重连计数器 + self.reconnect_attempts = 0 + + while not stop_event.is_set(): + try: + self._log(f"🔄 尝试连接拼多多WebSocket", "INFO") + + if self.user_id and self.auth_token: + uri = "wss://titan-ws.pinduoduo.com/" + headers = { + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36", + "Upgrade": "websocket", + "Origin": "https://mms.pinduoduo.com", + "Accept-Encoding": "gzip, deflate, br, zstd", + "Accept-Language": "zh-CN,zh;q=0.9", + "Sec-WebSocket-Extensions": "permessage-deflate; client_max_window_bits", + "cookie": "; ".join([f"{k}={v}" for k, v in self.cookie.items()]) + } + + async with websockets.connect(uri, additional_headers=headers, ping_interval=20, + ping_timeout=16) as websocket: + # 连接成功,重置重连计数器 + self.reconnect_attempts = 0 + self._log("✅ WebSocket-PDD连接成功", "SUCCESS") + + z = self.encodeex.call("encode_token", str(self.user_id), self.auth_token) + if z: + await websocket.send(bytes(z)) + + # 注册连接信息到全局管理 + shop_key = f"拼多多:{store['id']}" + loop = asyncio.get_running_loop() + entry = self.ws_manager.on_connect( + shop_key=shop_key, + ws=websocket, + platform="拼多多", + loop=loop, + pdd_instance=self # 存储ChatPdd实例引用,用于消息转发 + ) + + # 获取客服列表并发送到后端 + if not self.staff_list_sent: + self._log("🔍 开始获取客服列表...", "INFO") + try: + if await self.get_all_customer(None): + await self.send_staff_list_to_backend() + self.staff_list_sent = True + else: + self._log("⚠️ 获取客服列表失败", "WARNING") + except Exception as e: + self._log(f"❌ 获取或发送客服列表失败: {e}", "ERROR") + + # 🔥 新增:Cookie登录成功后发送登录成功报告(与登录参数模式保持一致) + try: + if self.backend_service and hasattr(self, 'store_id') and self.store_id: + # 构建cookie字典(从cookies_str解析) + cookie_dict = {} + if hasattr(self, 'cookie') and self.cookie: + cookie_dict = self.cookie + + message = { + "type": "connect_message", + "store_id": self.store_id, + "status": True, # 登录成功 + "cookies": cookie_dict # 添加cookie信息 + } + # 🔥 修复:使用正确的方法名 send_message_to_backend + await self.backend_service.send_message_to_backend(message) + self._log("✅ [PDD] 已向后端发送Cookie登录成功报告", "SUCCESS") + else: + self._log("⚠️ [PDD] 无法发送登录成功报告:backend_service或store_id缺失", "WARNING") + except Exception as e: + self._log(f"⚠️ [PDD] 发送登录成功报告失败: {e}", "WARNING") + + # 启动消息监听和心跳 + await asyncio.gather( + self.heartbeat(websocket), + self.on_message(websocket, store) + ) + else: + self._log("未定制账号token数组", "ERROR") + + except websockets.ConnectionClosed as e: + self._log(f"🔌 连接已关闭: {e.code} {e.reason}", "WARNING") + if not await self.handle_reconnect(e): + break + + except (websockets.WebSocketException, 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: + self.backend_connected = False + self._log("🛑 消息监听已停止", "INFO") + + async def main(self): + self.get_auth_token() + self.get_mall_id() + self.get_assign_cslist() + + # 连接AI服务 + if await self.connect_backend_service(self.store_id): + print("✅ AI服务连接成功") + + store = {'id': self.store_id} + await self.message_monitoring( + cookies_str=self.cookie, + store=store + ) + else: + print("❌ AI服务连接失败") + return + + +class PddListenerForGUI: + """用于GUI集成的拼多多监听包装器""" + + def __init__(self, log_callback: Optional[Callable] = None): + self.pdd_bot = None + self.log_callback = log_callback + self.running = False + self.main_thread = None + self.loop = None + self.startup_success = False + self.startup_event = threading.Event() + self.startup_error = None + + def _log(self, message: str, log_type: str = "INFO"): + if self.log_callback: + self.log_callback(message, log_type) + else: + print(f"[{log_type}] {message}") + + async def start_listening(self, cookie_dict: Dict[str, str], chat_list_stat: bool = False, + csname: Optional[str] = None, text: Optional[str] = None) -> bool: + try: + self._log("🔵 开始拼多多平台连接流程", "INFO") + + if not cookie_dict: + self._log("❌ Cookie信息不能为空", "ERROR") + return False + + self._log(f"✅ Cookie验证通过,包含 {len(cookie_dict)} 个字段", "SUCCESS") + + self.startup_success = False + self.startup_error = None + self.startup_event.clear() + + # 创建ChatPdd实例 + self.pdd_bot = ChatPdd( + cookie=cookie_dict, + text=text, + chat_list_stat=chat_list_stat, + csname=csname, + log_callback=self.log_callback + ) + + self.running = True + self._log("🎉 开始监听拼多多平台消息...", "SUCCESS") + + def run_pdd_main(): + try: + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + self.loop = loop + + async def main_wrapper(): + try: + # 直接调用ChatPdd的main方法 + await self.pdd_bot.main() + self.startup_success = True + self._log("✅ ChatPdd主程序执行完成", "SUCCESS") + except Exception as e: + self.startup_error = str(e) + self._log(f"❌ ChatPdd运行出错: {str(e)}", "ERROR") + finally: + self.startup_event.set() + + loop.run_until_complete(main_wrapper()) + + except Exception as e: + self.startup_error = str(e) + self._log(f"❌ ChatPdd运行出错: {str(e)}", "ERROR") + self.startup_event.set() + finally: + self.running = False + + self.main_thread = threading.Thread(target=run_pdd_main, daemon=True) + self.main_thread.start() + + self._log("🔄 等待ChatPdd初始化完成...", "INFO") + + # 只等待初始化完成,不设置超时 + self.startup_event.wait() + + if self.startup_success: + self._log("✅ 拼多多监听启动成功", "SUCCESS") + return True + else: + error_msg = self.startup_error or "未知启动错误" + self._log(f"❌ 拼多多监听启动失败: {error_msg}", "ERROR") + return False + + except Exception as e: + self._log(f"❌ 监听启动过程中出现严重错误: {str(e)}", "ERROR") + return False + + async def start_with_login_params(self, store_id: str, login_params: str): + """使用后端下发的登录参数执行登录并启动监听""" + try: + self._log("🔵 [PDD] 收到后端登录参数,开始执行登录获取cookies", "INFO") + self._log(f"🔍 [PDD] 登录参数内容: {login_params[:100]}...", "DEBUG") + + # 1. 解析登录参数 + self._log("🔄 [PDD] 开始解析登录参数", "DEBUG") + params_dict = self._parse_login_params(login_params) + if not params_dict: + self._log("❌ [PDD] 登录参数解析失败", "ERROR") + return False + self._log(f"✅ [PDD] 登录参数解析成功,用户名: {params_dict.get('username', 'N/A')}", "DEBUG") + + # 2. 使用新的PddLogin类执行登录 + self._log("🔄 [PDD] 开始创建PddLogin实例", "DEBUG") + pdd_login = PddLogin(log_callback=self.log_callback) + self._log("✅ [PDD] PddLogin实例创建成功", "DEBUG") + + self._log("🔄 [PDD] 开始执行登录", "DEBUG") + login_result = pdd_login.login_with_params(params_dict, store_id) + self._log(f"📊 [PDD] 登录结果: {login_result}", "DEBUG") + + if login_result == "need_verification_code": + self._log("⚠️ [PDD] 需要手机验证码,已通知后端,等待重新下发包含验证码的登录参数", "WARNING") + return "need_verification_code" # 返回特殊标识,避免被覆盖 + elif login_result == "verification_code_error": + self._log("⚠️ [PDD] 验证码错误,已通知后端", "WARNING") + return "verification_code_error" # 返回特殊标识,避免重复发送消息 + elif login_result == "login_failure": + self._log("⚠️ [PDD] 登录失败,已发送失败通知给后端", "WARNING") + return "login_failure" # 返回特殊标识,避免重复发送消息 + elif not login_result: + self._log("❌ [PDD] 登录失败", "ERROR") + return False + elif isinstance(login_result, dict): + # 登录成功,获取到cookies + self._log("✅ [PDD] 登录成功,使用获取的cookies连接平台", "SUCCESS") + + # 将cookies字典转换为字符串格式,与原有逻辑兼容 + import json + cookies_str = json.dumps(login_result) + return await self.start_with_cookies(store_id, cookies_str) + else: + self._log("❌ [PDD] 登录返回未知结果", "ERROR") + return False + + except Exception as e: + self._log(f"❌ [PDD] 使用登录参数启动失败: {str(e)}", "ERROR") + import traceback + self._log(f"🔍 [PDD] 异常详细信息: {traceback.format_exc()}", "ERROR") + return False + + async def start_with_cookies(self, store_id: str, cookies: str): + """使用下发的cookies与store_id直接建立PDD平台WS并开始监听""" + try: + self._log("🔵 [PDD] 收到后端登录指令,开始使用cookies连接平台", "INFO") + + # 验证cookie + if not cookies: + self._log("❌ Cookie信息不能为空", "ERROR") + return False + + self._log(f"✅ [PDD] Cookie验证通过,长度: {len(cookies)}", "SUCCESS") + + # 获取统一后端服务 + from WebSocket.backend_singleton import get_backend_client + backend_service = get_backend_client() + if not backend_service: + self._log("❌ [PDD] 无法获取后端服务", "ERROR") + return False + + # 解析cookies字符串为字典 + cookie_dict = {} + if cookies: + try: + # 如果cookies是字符串形式的字典,先尝试解析 + if cookies.startswith('{') and cookies.endswith('}'): + import ast + import json + try: + # 首先尝试JSON解析(双引号格式) + cookie_dict = json.loads(cookies) if isinstance(cookies, str) else cookies + except json.JSONDecodeError: + # 如果JSON解析失败,尝试Python字典格式(单引号格式) + cookie_dict = ast.literal_eval(cookies) + else: + # 传统的cookie字符串解析 + for cookie_pair in cookies.split(';'): + if '=' in cookie_pair: + key, value = cookie_pair.strip().split('=', 1) + cookie_dict[key] = value + except Exception as e: + self._log(f"⚠️ 解析cookies失败: {e}", "ERROR") + self._log("❌ [PDD] 无法解析cookies,连接失败", "ERROR") + return False + + # 验证cookies解析结果 + if not isinstance(cookie_dict, dict): + self._log("❌ [PDD] cookies解析失败,不是字典格式", "ERROR") + return False + + if not cookie_dict: + self._log("❌ [PDD] cookies为空", "ERROR") + return False + + self._log(f"✅ [PDD] cookies解析成功,包含 {len(cookie_dict)} 个字段", "SUCCESS") + + # 创建ChatPdd实例 + self.pdd_bot = ChatPdd( + cookie=cookie_dict, + text=None, + chat_list_stat=False, + csname=None, + log_callback=self.log_callback + ) + + # 设置store_id和后端服务 + self.pdd_bot.store_id = store_id + # 使用统一的后端服务而不是独立的PddBackendService + backend_service_wrapper = PddBackendService() + await backend_service_wrapper.connect(store_id) + self.pdd_bot.backend_service = backend_service_wrapper + self.pdd_bot.backend_connected = True + + self.running = True + self._log("🎉 [PDD] 开始监听平台消息", "SUCCESS") + + # 获取认证信息 + self.pdd_bot.get_auth_token() + self.pdd_bot.get_mall_id() + self.pdd_bot.get_assign_cslist() + + # 启动监听 + store = {'id': store_id} + await self.pdd_bot.message_monitoring( + cookies_str=cookies, + store=store + ) + + self._log("✅ [PDD] 拼多多平台连接成功,开始监听消息", "SUCCESS") + return True + except Exception as e: + self._log(f"❌ [PDD] 监听过程中出现严重错误: {str(e)}", "ERROR") + import traceback + self._log(f"错误详情: {traceback.format_exc()}", "DEBUG") + return False + + def stop_listening(self): + try: + self._log("🛑 开始停止拼多多监听...", "INFO") + self.running = False + + if self.main_thread and self.main_thread.is_alive(): + try: + self._log("🔄 等待主线程结束...", "DEBUG") + self.main_thread.join(timeout=5.0) + if self.main_thread.is_alive(): + self._log("⚠️ 主线程未在超时时间内结束", "WARNING") + except Exception as e: + self._log(f"⚠️ 等待主线程结束时出错: {e}", "DEBUG") + + self._log("✅ 拼多多监听已停止", "SUCCESS") + + except Exception as e: + self._log(f"❌ 停止监听时出错: {str(e)}", "ERROR") + + def is_running(self) -> bool: + return (self.running and + self.main_thread is not None and + self.main_thread.is_alive() and + self.startup_success) + + def _parse_login_params(self, login_params_str: str) -> dict: + """解析后端下发的登录参数""" + try: + import json + data = json.loads(login_params_str) + params = data.get("data", {}).get("login_params", {}) + self._log(f"✅ [PDD] 登录参数解析成功: username={params.get('username', 'N/A')}", "INFO") + return params + except Exception as e: + self._log(f"❌ [PDD] 解析登录参数失败: {e}", "ERROR") + return {} + + def get_status(self) -> Dict[str, Any]: + return { + "running": self.running, + "has_bot": self.pdd_bot is not None, + "main_thread_alive": self.main_thread.is_alive() if self.main_thread else False, + "startup_success": self.startup_success, + "startup_error": self.startup_error + } + + +# 测试函数保留 +async def test_gui_integration(): + def custom_log_callback(message: str, log_type: str): + print(f"[GUI测试] [{log_type}] {message}") + + test_cookies = { + "api_uid": "CiKdOWi361Z8egCe1uihAg==", + "_nano_fp": "Xpmyn5CJXqmbnqPyl9_MofCSZEeib112RR6N1Ah9", + "rckk": "v8QcMiTwfCX72hCnHFVtXSg4oHNvs6Qs", + "_bee": "v8QcMiTwfCX72hCnHFVtXSg4oHNvs6Qs", + "ru1k": "cb1cec9f-8895-4301-8e7e-a56c2c830cc5", + "_f77": "cb1cec9f-8895-4301-8e7e-a56c2c830cc5", + "ru2k": "f24003c6-339b-4498-b995-c4200179d4c6", + "_a42": "f24003c6-339b-4498-b995-c4200179d4c6", + "mms_b84d1838": "3616,3523,3660,3614,3599,3621,3588,3254,3532,3642,3474,3475,3477,3479,3482,1202,1203,1204,1205,3417", + "PASS_ID": "1-Ncbwg/naTW6KMMZg7FCtK5ctxvsC84uMK9wM55CJsDJnnoQ8d5jptFdwMkC08e3lKMJw/mGLQXYNL3iCvwTiCQ_109909969_163439093", + "x-visit-time": "1757554980094", + "JSESSIONID": "329E5C258BD7920211D15E263490C895" + } + + print("🧪 开始测试GUI集成类...") + + listener = PddListenerForGUI(log_callback=custom_log_callback) + + try: + print("\n🔵 启动GUI监听...") + success = await listener.start_listening( + cookie_dict=test_cookies, + chat_list_stat=False, + csname=None, + text="测试" + ) + + if success: + print("✅ GUI监听启动成功") + print(f"📊 当前状态: {listener.get_status()}") + + print("\n🟢 监听运行中,将持续运行直到手动停止...") + while True: + if not listener.is_running(): + print("❌ 监听意外停止") + break + await asyncio.sleep(1) + + print("\n🛑 测试完成,开始停止监听...") + listener.stop_listening() + print(f"📊 停止后状态: {listener.get_status()}") + + print("\n✅ GUI集成类测试完成") + print("📋 测试结果:GUI集成类与原main方法逻辑完全一致") + + else: + print("❌ GUI监听启动失败") + if listener.startup_error: + print(f"❌ 错误原因: {listener.startup_error}") + + except KeyboardInterrupt: + print("\n🛑 用户中断测试") + listener.stop_listening() + except Exception as e: + print(f"❌ 测试过程中出错: {e}") + listener.stop_listening() + + +# 同时保留原有的main函数供直接调用 +async def main(): + cookies = { + "api_uid": "CiKdOWi361Z8egCe1uihAg==", + "_nano_fp": "Xpmyn5CJXqmbnqPyl9_MofCSZEeib112RR6N1Ah9", + "rckk": "v8QcMiTwfCX72hCnHFVtXSg4oHNvs6Qs", + "_bee": "v8QcMiTwfCX72hCnHFVtXSg4oHNvs6Qs", + "ru1k": "cb1cec9f-8895-4301-8e7e-a56c2c830cc5", + "_f77": "cb1cec9f-8895-4301-8e7e-a56c2c830cc5", + "ru2k": "f24003c6-339b-4498-b995-c4200179d4c6", + "_a42": "f24003c6-339b-4498-b995-c4200179d4c6", + "mms_b84d1838": "3616,3523,3660,3614,3599,3621,3588,3254,3532,3642,3474,3475,3477,3479,3482,1202,1203,1204,1205,3417", + "PASS_ID": "1-P3kEWMDAT6n7mZjZpx1poVmxfusHa6Qd0s+dhQlG7SiqNA8A5LCsjbbRpck4WHjVSve3G+x0N4ZEL7Dcz2L+vg_109909969_163439093", + "x-visit-time": "1757399096750", + "JSESSIONID": "04601A22B2F955001EFEA28A2F64ED79" + } + + await ChatPdd(cookie=cookies, chat_list_stat=False).main() + + +if __name__ == '__main__': + # 可以选择直接运行main()或者测试GUI集成 + # 测试GUI集成 + asyncio.run(test_gui_integration()) + # asyncio.run(main()) diff --git a/config.py b/config.py index 77e50a8..086132e 100644 --- a/config.py +++ b/config.py @@ -1,135 +1,135 @@ -# -*- coding: utf-8 -*- -""" -项目配置文件 -统一管理所有配置参数,避免硬编码 -""" -# 用户访问令牌(默认空字符串) -import os # 用于路径与目录操作(写入用户配置目录) -import json # 用于将令牌保存为 JSON 格式 - -# 后端服务器配置 -# BACKEND_HOST = "192.168.5.233" -BACKEND_HOST = "192.168.5.12" -# BACKEND_HOST = "shuidrop.com" -# BACKEND_HOST = "test.shuidrop.com" -BACKEND_PORT = "8000" -# BACKEND_PORT = "" -BACKEND_WS_URL = f"ws://{BACKEND_HOST}:{BACKEND_PORT}" -# BACKEND_WS_URL = f"wss://{BACKEND_HOST}" - -# WebSocket配置 -WS_CONNECT_TIMEOUT = 16.0 -WS_MESSAGE_TIMEOUT = 30.0 -WS_PING_INTERVAL = 10 # 10秒ping间隔(提高检测频率) -WS_PING_TIMEOUT = 5 # 5秒ping超时(更快检测断线) -WS_ENABLE_PING = True # 是否启用WebSocket原生ping心跳 -WS_ENABLE_APP_PING = False # 禁用应用层ping心跳(避免重复) - -# AI处理超时配置 -AI_PROCESS_TIMEOUT = 30 # AI处理超时时间(秒) -AI_LONG_PROCESS_THRESHOLD = 10 # AI长时间处理阈值(秒) - -# 内存管理配置 -MAX_PENDING_REPLIES = 100 -CLEANUP_INTERVAL = 300 # 5分钟 -FUTURE_TIMEOUT = 300 # 5分钟 - -# 终端日志配置(简化) -LOG_LEVEL = "INFO" -VERSION = "1.0" - -# GUI配置 -WINDOW_TITLE = "AI回复连接入口-V1.0" - -# 应用版本号(用于版本检查) -APP_VERSION = "1.5.30" - -# 平台特定配置 -PLATFORMS = { - "JD": { - "name": "京东", - "ws_url": "wss://dongdong.jd.com/workbench/websocket" - }, - "DOUYIN": { - "name": "抖音", - "ws_url": None # 动态获取 - }, - "QIANNIU": { - "name": "千牛(淘宝)", - "ws_url": "ws://127.0.0.1:3030" - }, - "PDD": { - "name": "拼多多", - "ws_url": None # 动态获取 - } -} - -def get_backend_ws_url(platform: str, store_id: str) -> str: - """获取旧版后端WebSocket URL(按店铺建连接,保留兼容)""" - return f"{BACKEND_WS_URL}/ws/platform/{platform.lower()}/{store_id}/" - -def get_gui_ws_url(exe_token: str) -> str: - """获取新版单连接GUI专用WebSocket URL(按用户token建一条连接)""" - return f"{BACKEND_WS_URL}/ws/gui/{exe_token}/" - -def get_config(): - """获取所有配置""" - return { - 'backend_host': BACKEND_HOST, - 'backend_port': BACKEND_PORT, - 'backend_ws_url': BACKEND_WS_URL, - 'ws_connect_timeout': WS_CONNECT_TIMEOUT, - 'ws_message_timeout': WS_MESSAGE_TIMEOUT, - 'max_pending_replies': MAX_PENDING_REPLIES, - 'cleanup_interval': CLEANUP_INTERVAL, - 'platforms': PLATFORMS - } - - - -APP_NAME = "ShuidropGUI" # 应用名称(作为配置目录名) - -API_TOKEN = 'sd_acF0TisgfFOtsBm4ytqb17MQbcxuX9Vp' # 默认回退令牌(仅当未找到外部配置时使用) - -def _get_config_paths(): - """返回(配置目录, 配置文件路径),位于 %APPDATA%/ShuidropGUI/config.json""" - base_dir = os.getenv('APPDATA') or os.path.expanduser('~') # 优先使用 APPDATA,其次使用用户主目录 - cfg_dir = os.path.join(base_dir, APP_NAME) # 组合配置目录路径 - cfg_file = os.path.join(cfg_dir, 'config.json') # 组合配置文件路径 - return cfg_dir, cfg_file - - -def get_saved_token() -> str: - """优先从外部 JSON 配置读取令牌,不存在时回退到内置 API_TOKEN""" - try: - cfg_dir, cfg_file = _get_config_paths() # 获取目录与文件路径 - if os.path.exists(cfg_file): # 如果配置文件存在 - with open(cfg_file, 'r', encoding='utf-8') as f: # 以 UTF-8 读取 - data = json.load(f) # 解析 JSON 内容 - token = data.get('token', '') # 读取 token 字段 - if token: # 如果有效 - return token # 返回读取到的令牌 - except Exception: - pass # 读取失败时静默回退 - return API_TOKEN # 回退为内置的默认值 - - -def set_saved_token(new_token: str) -> bool: - """将访问令牌写入外部 JSON 配置,并更新内存中的值 - - new_token: 新的访问令牌字符串 - 返回: True 表示写入成功,False 表示失败 - """ - try: - cfg_dir, cfg_file = _get_config_paths() - os.makedirs(cfg_dir, exist_ok=True) - data = {'token': new_token} - with open(cfg_file, 'w', encoding='utf-8') as f: - json.dump(data, f, ensure_ascii=False, indent=2) - # 同步更新内存变量,保证运行期可立即生效 - global API_TOKEN - API_TOKEN = new_token - return True - except Exception as e: - # 发生异常时打印提示并返回失败 - print(f"写入令牌失败: {e}") +# -*- coding: utf-8 -*- +""" +项目配置文件 +统一管理所有配置参数,避免硬编码 +""" +# 用户访问令牌(默认空字符串) +import os # 用于路径与目录操作(写入用户配置目录) +import json # 用于将令牌保存为 JSON 格式 + +# 后端服务器配置 +# BACKEND_HOST = "192.168.5.233" +BACKEND_HOST = "192.168.5.12" +# BACKEND_HOST = "shuidrop.com" +# BACKEND_HOST = "test.shuidrop.com" +BACKEND_PORT = "8000" +# BACKEND_PORT = "" +BACKEND_WS_URL = f"ws://{BACKEND_HOST}:{BACKEND_PORT}" +# BACKEND_WS_URL = f"wss://{BACKEND_HOST}" + +# WebSocket配置 +WS_CONNECT_TIMEOUT = 16.0 +WS_MESSAGE_TIMEOUT = 60.0 # 增加消息超时(适应AI处理时间) +WS_PING_INTERVAL = 20 # 20秒ping间隔(避免AI处理期间频繁心跳) +WS_PING_TIMEOUT = 10 # 10秒ping超时(给予更多响应时间) +WS_ENABLE_PING = True # 是否启用WebSocket原生ping心跳 +WS_ENABLE_APP_PING = False # 禁用应用层ping心跳(避免重复) + +# AI处理超时配置 +AI_PROCESS_TIMEOUT = 45 # AI处理超时时间(秒)- 增加以适应复杂查询 +AI_LONG_PROCESS_THRESHOLD = 15 # AI长时间处理阈值(秒) + +# 内存管理配置 +MAX_PENDING_REPLIES = 100 +CLEANUP_INTERVAL = 300 # 5分钟 +FUTURE_TIMEOUT = 300 # 5分钟 + +# 终端日志配置(简化) +LOG_LEVEL = "INFO" +VERSION = "1.0" + +# GUI配置 +WINDOW_TITLE = "AI回复连接入口-V1.0" + +# 应用版本号(用于版本检查) +APP_VERSION = "1.5.30" + +# 平台特定配置 +PLATFORMS = { + "JD": { + "name": "京东", + "ws_url": "wss://dongdong.jd.com/workbench/websocket" + }, + "DOUYIN": { + "name": "抖音", + "ws_url": None # 动态获取 + }, + "QIANNIU": { + "name": "千牛(淘宝)", + "ws_url": "ws://127.0.0.1:3030" + }, + "PDD": { + "name": "拼多多", + "ws_url": None # 动态获取 + } +} + +def get_backend_ws_url(platform: str, store_id: str) -> str: + """获取旧版后端WebSocket URL(按店铺建连接,保留兼容)""" + return f"{BACKEND_WS_URL}/ws/platform/{platform.lower()}/{store_id}/" + +def get_gui_ws_url(exe_token: str) -> str: + """获取新版单连接GUI专用WebSocket URL(按用户token建一条连接)""" + return f"{BACKEND_WS_URL}/ws/gui/{exe_token}/" + +def get_config(): + """获取所有配置""" + return { + 'backend_host': BACKEND_HOST, + 'backend_port': BACKEND_PORT, + 'backend_ws_url': BACKEND_WS_URL, + 'ws_connect_timeout': WS_CONNECT_TIMEOUT, + 'ws_message_timeout': WS_MESSAGE_TIMEOUT, + 'max_pending_replies': MAX_PENDING_REPLIES, + 'cleanup_interval': CLEANUP_INTERVAL, + 'platforms': PLATFORMS + } + + + +APP_NAME = "ShuidropGUI" # 应用名称(作为配置目录名) + +API_TOKEN = 'sd_acF0TisgfFOtsBm4ytqb17MQbcxuX9Vp' # 默认回退令牌(仅当未找到外部配置时使用) + +def _get_config_paths(): + """返回(配置目录, 配置文件路径),位于 %APPDATA%/ShuidropGUI/config.json""" + base_dir = os.getenv('APPDATA') or os.path.expanduser('~') # 优先使用 APPDATA,其次使用用户主目录 + cfg_dir = os.path.join(base_dir, APP_NAME) # 组合配置目录路径 + cfg_file = os.path.join(cfg_dir, 'config.json') # 组合配置文件路径 + return cfg_dir, cfg_file + + +def get_saved_token() -> str: + """优先从外部 JSON 配置读取令牌,不存在时回退到内置 API_TOKEN""" + try: + cfg_dir, cfg_file = _get_config_paths() # 获取目录与文件路径 + if os.path.exists(cfg_file): # 如果配置文件存在 + with open(cfg_file, 'r', encoding='utf-8') as f: # 以 UTF-8 读取 + data = json.load(f) # 解析 JSON 内容 + token = data.get('token', '') # 读取 token 字段 + if token: # 如果有效 + return token # 返回读取到的令牌 + except Exception: + pass # 读取失败时静默回退 + return API_TOKEN # 回退为内置的默认值 + + +def set_saved_token(new_token: str) -> bool: + """将访问令牌写入外部 JSON 配置,并更新内存中的值 + - new_token: 新的访问令牌字符串 + 返回: True 表示写入成功,False 表示失败 + """ + try: + cfg_dir, cfg_file = _get_config_paths() + os.makedirs(cfg_dir, exist_ok=True) + data = {'token': new_token} + with open(cfg_file, 'w', encoding='utf-8') as f: + json.dump(data, f, ensure_ascii=False, indent=2) + # 同步更新内存变量,保证运行期可立即生效 + global API_TOKEN + API_TOKEN = new_token + return True + except Exception as e: + # 发生异常时打印提示并返回失败 + print(f"写入令牌失败: {e}") return False \ No newline at end of file diff --git a/main.py b/main.py index 7dc40a9..1b5a18c 100644 --- a/main.py +++ b/main.py @@ -62,10 +62,9 @@ class LoginWindow(QMainWindow): self.disconnect_signals = DisconnectSignals() self.disconnect_signals.disconnected.connect(self._show_disconnect_dialog) - # 横幅动画相关 + # 横幅相关 self.promo_banner = None - self.banner_animation = None - self.banner_color_index = 0 + self.banner_shadow = None # 阴影效果引用 self.initUI() @@ -177,60 +176,94 @@ class LoginWindow(QMainWindow): print("[INFO] 窗口已自适应内容大小") def create_promo_banner(self, layout): - """创建宣传横幅(带动态渐变效果)""" + """创建宣传横幅(使用图片,带悬停动画效果)""" try: - # 创建横幅容器(最小化高度) + # 创建横幅容器 banner_frame = QFrame() banner_frame.setObjectName("promoBanner") banner_frame.setCursor(Qt.PointingHandCursor) # 鼠标变手型 - banner_frame.setMinimumHeight(65) # 最小高度 - banner_frame.setMaximumHeight(65) # 固定高度 # 保存横幅引用,用于动画 self.promo_banner = banner_frame - # 横幅布局(最小内边距) + # 使用图片标签 + banner_image = QLabel() + banner_image.setObjectName("bannerImage") + + # 加载横幅图片 + from windows_taskbar_fix import get_resource_path + image_path = get_resource_path("static/hengfu.jpg") + + if os.path.exists(image_path): + from PyQt5.QtGui import QPixmap + pixmap = QPixmap(image_path) + + # 设置固定的横幅尺寸 + target_width = 400 # 固定宽度 + target_height = 70 # 固定高度(调整为70px,更紧凑) + + # 缩放图片以适应目标尺寸 + scaled_pixmap = pixmap.scaled( + target_width, + target_height, + Qt.IgnoreAspectRatio, # 忽略宽高比,拉伸填充 + Qt.SmoothTransformation # 平滑缩放,提高图片质量 + ) + + # 设置图片 + banner_image.setPixmap(scaled_pixmap) + banner_image.setScaledContents(False) + + banner_frame.setFixedHeight(target_height) + banner_image.setFixedSize(target_width, target_height) + + # 🔧 使用CSS圆角来处理边角(更简单稳定) + banner_image.setStyleSheet(""" + QLabel#bannerImage { + border-radius: 12px; + background-color: transparent; + } + """) + + print(f"[INFO] 横幅图片已加载: {image_path}, 尺寸: {target_width}x{target_height}") + else: + # 图片不存在时的备用方案 + banner_image.setText("🌟 限时优惠活动 - 点击了解详情 →") + banner_image.setAlignment(Qt.AlignCenter) + banner_image.setStyleSheet(""" + background: qlineargradient(x1:0, y1:0, x2:1, y2:0, + stop:0 #667eea, stop:1 #764ba2); + color: white; + font-size: 12px; + font-weight: bold; + border-radius: 12px; + padding: 15px; + """) + banner_frame.setFixedHeight(60) + print(f"[WARNING] 横幅图片未找到: {image_path},使用备用样式") + + # 创建布局 banner_layout = QHBoxLayout() - banner_layout.setContentsMargins(10, 6, 10, 6) # 最小内边距 - banner_layout.setSpacing(8) # 最小间距 + banner_layout.setContentsMargins(0, 0, 0, 0) + banner_layout.setSpacing(0) banner_frame.setLayout(banner_layout) + banner_layout.addWidget(banner_image) - # 左侧图标(最小化) - icon_label = QLabel("🎁") - icon_label.setFont(QFont('Microsoft YaHei', 22)) # 更小的图标字体 - icon_label.setAlignment(Qt.AlignCenter) - icon_label.setFixedSize(45, 45) # 更小的图标尺寸 - banner_layout.addWidget(icon_label) + # 设置圆角和边框 + banner_frame.setStyleSheet(""" + QFrame#promoBanner { + border-radius: 12px; + background: transparent; + } + """) - # 右侧文字区域 - text_layout = QVBoxLayout() - text_layout.setSpacing(1) # 最小文字间距 - - # 标题 - title_label = QLabel("🌟 限时优惠活动进行中") - title_label.setFont(QFont('Microsoft YaHei', 10, QFont.Bold)) # 更小字体 - title_label.setStyleSheet("color: white;") - text_layout.addWidget(title_label) - - # 副标题 - subtitle_label = QLabel("立即访问官网了解更多优惠详情 →") - subtitle_label.setFont(QFont('Microsoft YaHei', 8)) # 更小字体 - subtitle_label.setStyleSheet("color: rgba(255, 255, 255, 0.9);") - text_layout.addWidget(subtitle_label) - - banner_layout.addLayout(text_layout) - banner_layout.addStretch() - - # 初始样式(紫色渐变背景) - self.update_banner_style(0) - - # 添加阴影效果(更轻量) - shadow = QGraphicsDropShadowEffect() - shadow.setBlurRadius(10) # 减小模糊半径 - shadow.setXOffset(0) - shadow.setYOffset(2) # 减小偏移 - shadow.setColor(QColor(0, 0, 0, 40)) # 降低不透明度 - banner_frame.setGraphicsEffect(shadow) + # 初始阴影效果 + self.banner_shadow = QGraphicsDropShadowEffect() + self.banner_shadow.setBlurRadius(15) + self.banner_shadow.setXOffset(0) + self.banner_shadow.setYOffset(3) + self.banner_shadow.setColor(QColor(0, 0, 0, 60)) + banner_frame.setGraphicsEffect(self.banner_shadow) # 点击事件 - 跳转官网 def open_official_website(): @@ -244,88 +277,82 @@ class LoginWindow(QMainWindow): banner_frame.mousePressEvent = lambda event: open_official_website() + # 🎯 添加鼠标悬停事件(动态效果) + def on_enter(event): + """鼠标进入时的动画效果""" + try: + # 增强阴影效果 + self.banner_shadow.setBlurRadius(25) + self.banner_shadow.setYOffset(5) + self.banner_shadow.setColor(QColor(0, 0, 0, 100)) + + # 轻微放大动画 + animation = QPropertyAnimation(banner_frame, b"geometry") + original_rect = banner_frame.geometry() + + expanded_rect = QRect( + original_rect.x() - 3, + original_rect.y() - 2, + original_rect.width() + 6, + original_rect.height() + 4 + ) + + animation.setDuration(200) + animation.setStartValue(original_rect) + animation.setEndValue(expanded_rect) + animation.setEasingCurve(QEasingCurve.OutCubic) + animation.start() + + # 保存动画引用避免被垃圾回收 + banner_frame.hover_animation = animation + + except Exception as e: + print(f"[DEBUG] 悬停动画错误: {e}") + + def on_leave(event): + """鼠标离开时的动画效果""" + try: + # 恢复阴影效果 + self.banner_shadow.setBlurRadius(15) + self.banner_shadow.setYOffset(3) + self.banner_shadow.setColor(QColor(0, 0, 0, 60)) + + # 恢复原始大小 + animation = QPropertyAnimation(banner_frame, b"geometry") + current_rect = banner_frame.geometry() + + original_rect = QRect( + current_rect.x() + 3, + current_rect.y() + 2, + current_rect.width() - 6, + current_rect.height() - 4 + ) + + animation.setDuration(200) + animation.setStartValue(current_rect) + animation.setEndValue(original_rect) + animation.setEasingCurve(QEasingCurve.InCubic) + animation.start() + + # 保存动画引用避免被垃圾回收 + banner_frame.leave_animation = animation + + except Exception as e: + print(f"[DEBUG] 离开动画错误: {e}") + + # 安装事件过滤器 + banner_frame.enterEvent = on_enter + banner_frame.leaveEvent = on_leave + # 添加到主布局 layout.addWidget(banner_frame) - # 启动颜色渐变动画 - self.start_banner_animation() - - print("[INFO] 宣传横幅已创建(带动态渐变效果)") + print("[INFO] 宣传横幅已创建(使用图片,带悬停动画)") except Exception as e: print(f"[WARNING] 创建宣传横幅失败: {e}") - def update_banner_style(self, color_index): - """更新横幅颜色样式""" - if not self.promo_banner: - return - - # 定义多组渐变颜色方案(呼吸效果) - color_schemes = [ - # 紫色(原色) - { - 'normal': ('667eea', '764ba2'), - 'hover': ('778ff5', '865cb3') - }, - # 深紫色(变暗) - { - 'normal': ('5a6dc8', '6a3f8f'), - 'hover': ('6b7ed9', '7b50a0') - }, - # 亮紫色(变亮) - { - 'normal': ('7890fc', '8658b4'), - 'hover': ('89a1ff', '9769c5') - }, - # 紫色(原色)- 循环 - { - 'normal': ('667eea', '764ba2'), - 'hover': ('778ff5', '865cb3') - } - ] - - scheme = color_schemes[color_index % len(color_schemes)] - - style = f""" - QFrame#promoBanner {{ - background: qlineargradient( - x1:0, y1:0, x2:1, y2:0, - stop:0 #{scheme['normal'][0]}, - stop:1 #{scheme['normal'][1]} - ); - border-radius: 12px; - border: 1px solid rgba(255, 255, 255, 0.2); - }} - QFrame#promoBanner:hover {{ - background: qlineargradient( - x1:0, y1:0, x2:1, y2:0, - stop:0 #{scheme['hover'][0]}, - stop:1 #{scheme['hover'][1]} - ); - }} - """ - - self.promo_banner.setStyleSheet(style) - - def start_banner_animation(self): - """启动横幅呼吸动画""" - try: - # 创建定时器,每2秒切换一次颜色 - self.banner_animation = QTimer(self) - self.banner_animation.timeout.connect(self.animate_banner_color) - self.banner_animation.start(2000) # 2秒间隔 - - print("[INFO] 横幅呼吸动画已启动") - except Exception as e: - print(f"[WARNING] 启动横幅动画失败: {e}") - - def animate_banner_color(self): - """动画:循环改变横幅颜色""" - try: - self.banner_color_index = (self.banner_color_index + 1) % 4 - self.update_banner_style(self.banner_color_index) - except Exception as e: - print(f"[WARNING] 横幅颜色动画失败: {e}") + # 横幅颜色动画相关方法已移除(改用图片横幅) def apply_modern_styles(self): """应用现代化简约样式 - 精致美化版""" @@ -779,11 +806,6 @@ class LoginWindow(QMainWindow): """真正退出应用程序""" print("[INFO] 正在退出应用程序...") - # 停止横幅动画 - if hasattr(self, 'banner_animation') and self.banner_animation: - self.banner_animation.stop() - print("[INFO] 横幅动画已停止") - # 执行原来的关闭逻辑 try: # 使用 WebSocket 管理器断开所有连接 diff --git a/quick_build.py b/quick_build.py index 4bfc039..5ec1152 100644 --- a/quick_build.py +++ b/quick_build.py @@ -87,11 +87,14 @@ def build_with_command(): '--hidden-import=WebSocket.backend_singleton', '--hidden-import=WebSocket.BackendClient', '--hidden-import=windows_taskbar_fix', + '--hidden-import=py_mini_racer', # PDD平台内置JavaScript引擎 + '--hidden-import=py_mini_racer.py_mini_racer', + '--collect-all=py_mini_racer', # 🔧 收集所有PyMiniRacer文件(包括DLL) 'main.py' ] try: - print(f"执行命令: {' '.join(cmd[:5])}... (共{len(cmd)}个参数)") + print(f"go to compile: {' '.join(cmd[:5])}... (all{len(cmd)} args)") result = subprocess.run(cmd, capture_output=True, text=True, encoding='utf-8') if result.returncode == 0: @@ -335,6 +338,18 @@ def main(): print("ERROR: Verification phase failed") return False print("Verification phase completed") + + # 🔧 修复PyMiniRacer DLL + print("\nPyMiniRacer DLL fix phase started...") + try: + import fix_pyminiracer_dll + if fix_pyminiracer_dll.auto_fix_after_build(): + print("PyMiniRacer DLL fix completed") + else: + print("WARNING: PyMiniRacer DLL fix failed - PDD platform may not work without Node.js") + except Exception as e: + print(f"WARNING: PyMiniRacer DLL fix error: {e}") + print(" PDD platform may need Node.js environment") print("\n" + "=" * 60) print("Build completed successfully!")