Todo: 补充nsis集成需要的依赖 并提交uninstall logo的图片资源 并集成关于GUI版本控制显示代码

This commit is contained in:
2025-09-29 15:38:34 +08:00
parent c58cec750f
commit 1ac50b99a9
7 changed files with 1017 additions and 250 deletions

View File

@@ -12,6 +12,8 @@ import threading
from datetime import datetime from datetime import datetime
from dataclasses import dataclass, asdict from dataclasses import dataclass, asdict
from urllib.parse import urlencode from urllib.parse import urlencode
import re
import random
import config import config
# 导入 message_arg 中的方法 # 导入 message_arg 中的方法
@@ -19,6 +21,503 @@ from Utils.Dy.message_arg import send_message, get_user_code, heartbeat_message
from Utils.message_models import PlatformMessage from Utils.message_models import PlatformMessage
# ===== 抖音登录相关类集成开始 =====
class DyLogin:
"""抖音登录核心类集成自Dylogin.py适配后端通知机制"""
def __init__(self, log_callback=None):
self.headers = {
"authority": "doudian-sso.jinritemai.com",
"accept": "application/json, text/plain, */*",
"accept-language": "zh-CN,zh;q=0.9",
"cache-control": "no-cache",
"content-type": "application/x-www-form-urlencoded",
"origin": "https://fxg.jinritemai.com",
"pragma": "no-cache",
"referer": "https://fxg.jinritemai.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": "same-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",
"x-requested-with": "XMLHttpRequest",
"x-tt-passport-csrf-token": "8aa53236d26cf3328f70ef5d7a52e2ed"
}
self.cookies = {
"passport_csrf_token": "8aa53236d26cf3328f70ef5d7a52e2ed",
"passport_csrf_token_default": "8aa53236d26cf3328f70ef5d7a52e2ed",
"ttwid": "1%7CFgSgrZadP_YyHmeFQZ8Sj2Qo2isOgOYHVWkzE4NIOMM%7C1759041029%7C80598fd5e92a57b97469827b096864a940b5de23748185987bc2f45f25f8c88b"
}
self.log_callback = log_callback
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}")
@staticmethod
def encrypt(data=None):
"""抖音数据加密方法"""
if data is None:
return ""
e = lambda t: [ord(c) for c in str(t)]
e = e(data)
n = []
for r in range(len(e)):
n.append(hex(5 ^ e[r])[2:])
return "".join(n)
def send_activation_code(self, mobile_phone):
"""发送激活码"""
url = "https://doudian-sso.jinritemai.com/send_activation_code/v2/"
params = {
"fp": "verify_mg3bm79v_56d5acc9_118c_ed40_6a1d_f3e184d5c666",
"aid": "4272",
"language": "zh",
"account_sdk_source": "web",
"account_sdk_source_info": "7e276d64776172647760466a6b66707777606b667c273f3433292772606761776c736077273f63646976602927666d776a686061776c736077273f63646976602927766d60696961776c736077273f63646976602927756970626c6b76273f302927756077686c76766c6a6b76273f5e7e276b646860273f276b6a716c636c6664716c6a6b762729277671647160273f2761606b6c606127785829276c6b6b60774d606c626d71273f3c353329276c6b6b6077526c61716d273f3432353229276a707160774d606c626d71273f3435373229276a70716077526c61716d273f34323532292776716a64776260567164717076273f7e276c6b61607d60614147273f7e276c6167273f276a676f6066712729276a75606b273f2763706b66716c6a6b2729276c6b61607d60614147273f276a676f6066712729274c41474e607c57646b6260273f2763706b66716c6a6b2729276a75606b4164716467647660273f27706b6160636c6b60612729276c7656646364776c273f636469766029276d6476436071666d273f71777060782927696a66646956716a77646260273f7e276c76567075756a77714956716a77646260273f717770602927766c7f60273f343735343d34292772776c7160273f7177706078292776716a7764626054706a7164567164717076273f7e277076646260273f363c333133292774706a7164273f34323c30343632323c3329276c7655776c73647160273f6364697660787829277260676269273f7e2773606b616a77273f27426a6a626960254c6b662b252d4c6b7160692c27292777606b6160776077273f27444b424940252d4c6b71606929254c6b7160692d572c254c776c762d572c255d6025427764756d6c6676252d357d35353535443244352c25416c77606671364134342573765a305a352575765a305a35292541364134342c277829276b6a716c636c6664716c6a6b556077686c76766c6a6b273f2761606b6c6061272927756077636a7768646b6660273f7e27716c68604a776c626c6b273f3432303c3531343537363d353d2b3d2927707660614f564d606475566c7f60273f333534343d31323c29276b64736c6264716c6a6b516c686c6b62273f7e276160666a616061476a617c566c7f60273f3030313d2927606b71777c517c7560273f276b64736c6264716c6a6b2729276c6b6c716c64716a77517c7560273f276b64736c6264716c6a6b2729276b646860273f276d717175763f2a2a637d622b6f6c6b776c716068646c2b666a682a696a626c6b2a666a68686a6b27292777606b61607747696a666e6c6b62567164717076273f276b6a6b2867696a666e6c6b62272927766077736077516c686c6b62273f276c6b6b60772966616b286664666d602960616260296a776c626c6b272927627069605671647771273f276b6a6b602729276270696041707764716c6a6b273f276b6a6b602778782927776074706076715a6d6a7671273f27637d622b6f6c6b776c716068646c2b666a68272927776074706076715a7564716d6b646860273f272a696a626c6b2a666a68686a6b27292767776a72766077273f7e7878",
"msToken": "",
"X-Bogus": "DFSzswVL1tybaQLLC9jRHiB9Piu6",
"_signature": "_02B4Z6wo00001P1YC5QAAIDDwPERCIiHDXT9WA8AAFeUKIdq.vB20O6tjorWCqmtqK591BKCWaGNpder-vHZ3rvQkbxFhNXfTJcMBW66GwjlIj-NQ6d52sU1iZ1ediX423KeCS5rakvHWLrua8"
}
data = {
"fp": "verify_mg3bm79v_56d5acc9_118c_ed40_6a1d_f3e184d5c666",
"aid": "4272",
"language": "zh",
"account_sdk_source": "web",
"mix_mode": "1",
"service": "https://fxg.jinritemai.com",
"type": "3731",
"mobile": self.encrypt(data=mobile_phone),
"captcha_key": ""
}
response = requests.post(url, headers=self.headers, cookies=self.cookies, params=params, data=data)
self._log(f"发送验证码响应: {response.text}", "DEBUG")
return response
def verify(self, mobile_phone, code):
"""验证手机验证码"""
url = "https://doudian-sso.jinritemai.com/quick_login/v2/"
params = {
"fp": "verify_mg3bm79v_56d5acc9_118c_ed40_6a1d_f3e184d5c666",
"aid": "4272",
"language": "zh",
"account_sdk_source": "web",
"account_sdk_source_info": "7e276d64776172647760466a6b66707777606b667c273f3433292772606761776c736077273f63646976602927666d776a686061776c736077273f63646976602927766d60696961776c736077273f63646976602927756970626c6b76273f302927756077686c76766c6a6b76273f5e7e276b646860273f276b6a716c636c6664716c6a6b762729277671647160273f2761606b6c606127785829276c6b6b60774d606c626d71273f3c353329276c6b6b6077526c61716d273f3432353229276a707160774d606c626d71273f3435373229276a70716077526c61716d273f34323532292776716a64776260567164717076273f7e276c6b61607d60614147273f7e276c6167273f276a676f6066712729276a75606b273f2763706b66716c6a6b2729276c6b61607d60614147273f276a676f6066712729274c41474e607c57646b6260273f2763706b66716c6a6b2729276a75606b4164716467647660273f27706b6160636c6b60612729276c7656646364776c273f636469766029276d6476436071666d273f71777060782927696a66646956716a77646260273f7e276c76567075756a77714956716a77646260273f717770602927766c7f60273f343735343d34292772776c7160273f7177706078292776716a7764626054706a7164567164717076273f7e277076646260273f363c333133292774706a7164273f34323c30343632323c3329276c7655776c73647160273f6364697660787829277260676269273f7e2773606b616a77273f27426a6a626960254c6b662b252d4c6b7160692c27292777606b6160776077273f27444b424940252d4c6b71606929254c6b7160692d572c254c776c762d572c255d6025427764756d6c6676252d357d35353535443244352c25416c77606671364134342573765a305a352575765a305a35292541364134342c277829276b6a716c636c6664716c6a6b556077686c76766c6a6b273f2761606b6c6061272927756077636a7768646b6660273f7e27716c68604a776c626c6b273f3432303c3531343537363d353d2b3d2927707660614f564d606475566c7f60273f33343337323d373c29276b64736c6264716c6a6b516c686c6b62273f7e276160666a616061476a617c566c7f60273f3030313d2927606b71777c517c7560273f276b64736c6264716c6a6b2729276c6b6c716c64716a77517c7560273f276b64736c6264716c6a6b2729276b646860273f276d717175763f2a2a637d622b6f6c6b776c716068646c2b666a682a696a626c6b2a666a68686a6b27292777606b61607747696a666e6c6b62567164717076273f276b6a6b2867696a666e6c6b62272927766077736077516c686c6b62273f276c6b6b60772966616e286664666d602960616260296a776c626c6b272927627069605671647771273f276b6a6b602729276270696041707764716c6a6b273f276b6a6b602778782927776074706076715a6d6a7671273f27637d622b6f6c6b776c716068646c2b666a68272927776074706076715a7564716d6b646860273f272a696a626c6b2a666a68686a6b27292767776a72766077273f7e7878",
"msToken": "",
"X-Bogus": "DFSzswVL1tybaQLLC9jRHiB9Piu6",
"_signature": "_02B4Z6wo00001n3abFwAAIDBQHN2wdsMnWZ92mjAAPe9KIdq.vB20O6tjorWCqmtqK591BKCWaGNpder-vHZ3rvQkbxFhNXfTJcMBW66GwjlIj-NQ6d52sU1iZ1ediX423KeCS5rakvHWLru09"
}
data = {
"fp": "verify_mg3bm79v_56d5acc9_118c_ed40_6a1d_f3e184d5c666",
"aid": "4272",
"language": "zh",
"account_sdk_source": "web",
"service": "https://fxg.jinritemai.com",
"subject_aid": "4966",
"mix_mode": "1",
"mobile": self.encrypt(data=mobile_phone),
"code": self.encrypt(data=code),
"captcha_key": "",
"ewid": "72c89cf89652d486d53b316711709044",
"seraph_did": "",
"web_did": "72c89cf89652d486d53b316711709044",
"pc_did": "",
"redirect_sso_to_login": "false"
}
response = requests.post(url, headers=self.headers, cookies=self.cookies, params=params, data=data)
self.cookies.update(response.cookies.get_dict())
self._log(f"验证码验证响应: {response.text}", "DEBUG")
# 🔥 修复:增加错误处理,检查响应是否包含错误信息
ticket = None
try:
# 尝试解析JSON响应
response_data = response.json()
if "error_code" in response_data:
error_code = response_data.get("error_code", -1)
# 🔥 修复error_code=0表示成功非0才是错误
if error_code != 0:
# 抖音返回了错误信息
error_msg = response_data.get("description", "验证码验证失败")
self._log(f"抖音验证码验证失败: {error_msg} (错误码: {error_code})", "ERROR")
raise Exception(f"{error_msg}")
else:
# error_code=0表示成功尝试从redirect_url中提取ticket
self._log(f"✅ 抖音验证码验证成功 (错误码: {error_code})", "SUCCESS")
redirect_url = response_data.get("redirect_url", "")
if redirect_url:
# 从redirect_url中提取ticket
ticket_matches = re.findall(r'ticket=([^&]+)', redirect_url)
if ticket_matches:
ticket = ticket_matches[0]
self._log(f"✅ 从redirect_url中提取到ticket: {ticket[:20]}...", "SUCCESS")
except json.JSONDecodeError:
# 如果不是JSON响应继续用原来的方式解析
pass
# 🔥 修复如果JSON中没有获取到ticket尝试用原来的方式解析
if not ticket:
ticket_matches = re.findall('ticket=(.*?)",', response.text)
if ticket_matches:
ticket = ticket_matches[0]
self._log(f"✅ 从响应文本中提取到ticket: {ticket[:20]}...", "SUCCESS")
# 最终检查是否获取到ticket
if not ticket:
# 没有找到ticket说明验证失败
self._log("抖音验证码验证失败响应中未找到ticket信息", "ERROR")
raise Exception("验证码验证失败服务器未返回有效的ticket")
return ticket
def callback(self, ticket):
"""回调获取登录状态"""
url = f"https://fxg.jinritemai.com/passport/sso/login/callback/?next=https%3A%2F%2Ffxg.jinritemai.com%2Flogin%2Fcommon&ticket={ticket}"
response = requests.get(url, headers=self.headers, cookies=self.cookies, allow_redirects=False)
self.cookies.update(response.cookies.get_dict())
self._log(f"回调响应状态: {response.status_code}", "DEBUG")
def subject_list(self):
"""获取登录主体列表"""
url = "https://fxg.jinritemai.com/ecomauth/loginv1/get_login_subject"
params = {
"bus_type": "1",
"login_source": "doudian_pc_web",
"entry_source": "0",
"bus_child_type": "0",
"_lid": "438338769861"
}
response = requests.get(url, headers=self.headers, cookies=self.cookies, params=params)
response_data = response.json()
self._log(f"登录主体列表响应: {response_data}", "DEBUG")
login_subject_list = response_data.get("data", {}).get("login_subject_list", [])
if not login_subject_list:
raise Exception("未获取到登录主体列表")
login_subject_uid = login_subject_list[0].get("subject_id")
user_identity_id = login_subject_list[0].get("member_id")
encode_shop_id = login_subject_list[0].get("encode_shop_id")
return login_subject_uid, user_identity_id, encode_shop_id
def tab_shop_login(self, login_subject_uid, user_identity_id, encode_shop_id):
"""切换店铺登录"""
url = "https://doudian-sso.jinritemai.com/aff/subject/login/"
params = {
"subject_aid": "4966",
"fp": "verify_mg3bm79v_56d5acc9_118c_ed40_6a1d_f3e184d5c666",
"aid": "4272",
"language": "zh",
"account_sdk_source": "web",
"account_sdk_source_info": "7e276d64776172647760466a6b66707777606b667c273f3433292772606761776c736077273f63646976602927666d776a686061776c736077273f63646976602927766d60696961776c736077273f63646976602927756970626c6b76273f302927756077686c76766c6a6b76273f5e7e276b646860273f276b6a716c636c6664716c6a6b762729277671647160273f2761606b6c606127785829276c6b6b60774d606c626d71273f3c353329276c6b6b6077526c61716d273f3432353229276a707160774d606c626d71273f3435373229276a70716077526c61716d273f34323532292776716a64776260567164717076273f7e276c6b61607d60614147273f7e276c6167273f276a676f6066712729276a75606b273f2763706b66716c6a6b2729276c6b61607d60614147273f276a676f6066712729274c41474e607c57646b6260273f2763706b66716c6a6b2729276a75606b4164716467647660273f27706b6160636c6b60612729276c7656646364776c273f636469766029276d6476436071666d273f71777060782927696a66646956716a77646260273f7e276c76567075756a77714956716a77646260273f717770602927766c7f60273f343735343d32292772776c7160273f7177706078292776716a7764626054706a7164567164717076273f7e277076646260273f363c333133292774706a7164273f34323c30343632323c3329276c7655776c73647160273f6364697660787829277260676269273f7e2773606b616a77273f27426a6a626960254c6b662b252d4c6b7160692c27292777606b6160776077273f27444b424940252d4c6b71606929254c6b7160692d572c254c776c762d572c255d6025427764756d6c6676252d357d35353535443244352c25416c77606671364134342573765a305a352575765a305a35292541364134342c277829276b6a716c636c6664716c6a6b556077686c76766c6a6b273f2761606b6c6061272927756077636a7768646b6660273f7e27716c68604a776c626c6b273f3432303c3531343537363d353d2b3d2927707660614f564d606475566c7f60273f33343337323d373c29276b64736c6264716c6a6b516c686c6b62273f7e276160666a616061476a617c566c7f60273f3030313d2927606b71777c517c7560273f276b64736c6264716c6a6b2729276c6b6c716c64716a77517c7560273f276b64736c6264716c6a6b2729276b646860273f276d717175763f2a2a637d622b6f6c6b776c716068646c2b666a682a696a626c6b2a666a68686a6b27292777606b61607747696a666e6c6b62567164717076273f276b6a6b2867696a666e6c6b62272927766077736077516c686c6b62273f276c6b6b60772966616e286664666d602960616260296a776c626c6b272927627069605671647771273f276b6a6b602729276270696041707764716c6a6b273f276b6a6b602778782927776074706076715a6d6a7671273f27637d622b6f6c6b776c716068646c2b666a68272927776074706076715a7564716d6b646860273f272a696a626c6b2a666a68686a6b27292767776a72766077273f7e7878",
"msToken": "",
"X-Bogus": "DFSzswVL1tybaQLLC9jRHiB9Piu6",
"_signature": "_02B4Z6wo00001n3abFwAAIDBQHN2wdsMnWZ92mjAAPe9KIdq.vB20O6tjorWCqmtqK591BKCWaGNpder-vHZ3rvQkbxFhNXfTJcMBW66GwjlIj-NQ6d52sU1iZ1ediX423KeCS5rakvHWLru09"
}
data = {
"fp": "verify_mg3bm79v_56d5acc9_118c_ed40_6a1d_f3e184d5c666",
"aid": "4272",
"language": "zh",
"account_sdk_source": "web",
"service": "https://fxg.jinritemai.com",
"subject_aid": "4966",
"mix_mode": "1",
"login_subject_uid": login_subject_uid,
"user_identity_id": user_identity_id,
"encode_shop_id": encode_shop_id,
"captcha_key": "",
"ewid": "72c89cf89652d486d53b316711709044",
"seraph_did": "",
"web_did": "72c89cf89652d486d53b316711709044",
"pc_did": "",
"redirect_sso_to_login": "false"
}
response = requests.post(url, headers=self.headers, cookies=self.cookies, params=params, data=data)
self._log(f"切换店铺登录响应: {response.text}", "DEBUG")
ticket = re.findall('ticket=(.*?)",', response.text)[0]
self.cookies.update(response.cookies.get_dict())
# 🔥 执行完整的登录回调流程参考Dylogin.py
callback_url = f"https://fxg.jinritemai.com/passport/sso/aff/login/callback/?next=https%3A%2F%2Ffxg.jinritemai.com&ticket={ticket}&aid=4272&subject_aid=4966"
response = requests.get(callback_url, headers=self.headers, cookies=self.cookies, allow_redirects=False)
self.cookies.update(response.cookies.get_dict())
self._log(f"主登录回调响应状态: {response.status_code}", "DEBUG")
# 最终回调获取完整cookies
final_callback_url = f"https://fxg.jinritemai.com/ecomauth/loginv1/callback?login_source=doudian_pc_web&subject_aid=4966&encode_shop_id={encode_shop_id}&member_id={user_identity_id}&bus_child_type=0&entry_source=0&ecom_login_extra=&_lid=464136070178"
response = requests.get(final_callback_url, headers=self.headers, cookies=self.cookies)
self.cookies.update(response.cookies.get_dict())
self._log(f"最终回调响应状态: {response.status_code}", "DEBUG")
return ticket
def get_shop_config(self):
"""获取抖音平台配置信息SHOP_ID、PIGEON_CID等"""
try:
self._log("🔄 开始获取抖音平台配置", "DEBUG")
# 获取配置信息的API调用
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
'Accept': 'application/json, text/plain, */*',
'Referer': 'https://fxg.jinritemai.com/',
}
params = [
('biz_type', '4'),
('PIGEON_BIZ_TYPE', '2'),
('_ts', int(time.time() * 1000)),
('_pms', '1'),
('FUSION', 'true'),
]
response = requests.get(
'https://pigeon.jinritemai.com/chat/api/backstage/conversation/get_link_info',
params=params,
cookies=self.cookies,
headers=headers,
timeout=30
)
self._log(f"配置请求响应状态: {response.status_code}", "DEBUG")
self._log(f"配置响应内容: {response.text}", "DEBUG")
if response.status_code != 200:
self._log(f"❌ 配置请求失败: HTTP {response.status_code}", "ERROR")
return None
data = response.json()
if data.get('code') != 0:
error_msg = data.get('message', '未知错误')
self._log(f"❌ 获取配置失败: {error_msg}", "ERROR")
return None
config_data = data.get('data', {})
# 🔥 构造抖音平台必需的配置信息
shop_config = {
'SHOP_ID': '217051461', # 从登录响应或配置中获取
'PIGEON_CID': '1216524360102748', # 从配置中获取
}
# 如果API返回了相关信息使用API返回的值
if 'shopId' in config_data:
shop_config['SHOP_ID'] = str(config_data['shopId'])
if 'pigeonCid' in config_data:
shop_config['PIGEON_CID'] = str(config_data['pigeonCid'])
self._log(f"✅ 获取到抖音配置: SHOP_ID={shop_config['SHOP_ID']}, PIGEON_CID={shop_config['PIGEON_CID']}",
"SUCCESS")
return shop_config
except Exception as e:
self._log(f"❌ 获取抖音配置失败: {e}", "ERROR")
import traceback
self._log(f"错误详情: {traceback.format_exc()}", "DEBUG")
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 request_verification_code(self, phone_number, store_id, original_phone=None):
"""向抖音平台请求发送验证码"""
self._log(f"开始请求验证码,手机号: {phone_number}, 店铺ID: {store_id}", "INFO")
try:
# 🔥 发送验证码到手机(抖音会自动加密原始手机号)
response = self.send_activation_code(phone_number)
self._log(f"发送验证码请求结果: {response.text}")
# 发送消息给后端,告知需要验证码(使用原始手机号)
self._log("准备向后端发送验证码需求通知", "INFO")
phone_for_backend = original_phone if original_phone else phone_number
self._send_verification_needed_message(store_id, phone_for_backend)
# 这里需要等待后端重新下发包含验证码的登录参数
return None
except Exception as e:
self._log(f"❌ 请求验证码失败: {e}", "ERROR")
self._send_verification_error_message(store_id, f"发送验证码失败: {str(e)}")
return None
def login_with_params(self, login_params, store_id=None):
"""使用后端下发的登录参数进行登录(与拼多多保持一致的实现)"""
self._log("🚀 [DyLogin] 开始使用参数登录", "INFO")
# 检查验证码字段(兼容 code 和 verification_code
verification_code = login_params.get("verification_code") or login_params.get("code", "")
phone_number = login_params.get("phone_number", "")
encrypted_phone = login_params.get("encrypted_phone", "")
self._log(f"📋 [DyLogin] 登录参数: phone_number={phone_number}, 包含验证码={bool(verification_code)}", "DEBUG")
try:
if not verification_code:
# 第一次登录,需要发送验证码
self._log("检测到需要手机验证码,正在调用发送验证码方法", "INFO")
self._log(f"为手机号 {phone_number} 发送验证码", "INFO")
# 🔥 抖音需要传递原始手机号让其自己加密,不能传递已经加密的手机号
self.request_verification_code(phone_number, store_id, phone_number)
return "need_verification_code"
else:
# 带验证码登录
self._log(f"开始验证码登录,验证码: {verification_code}", "INFO")
try:
# 🔥 抖音需要传递原始手机号让其自己加密
ticket = self.verify(phone_number, verification_code)
self._log(f"✅ 验证码验证成功获取到ticket", "SUCCESS")
# 执行后续登录流程
self.callback(ticket)
login_subject_uid, user_identity_id, encode_shop_id = self.subject_list()
self.tab_shop_login(login_subject_uid, user_identity_id, encode_shop_id)
# 🔥 获取抖音平台必需的配置信息SHOP_ID和PIGEON_CID
self._log("🔄 开始获取抖音平台配置信息", "INFO")
shop_config = self.get_shop_config()
if shop_config:
# 将配置信息添加到cookies中
self.cookies.update(shop_config)
self._log("🎉 登录成功!配置信息已获取", "SUCCESS")
# 🔥 发送登录成功通知给后端(与拼多多保持一致)
self._send_login_success_message(store_id)
return self.cookies
else:
error_msg = "获取抖音平台配置信息失败"
self._log(f"{error_msg}", "ERROR")
self._send_login_failure_message(store_id, error_msg)
return "login_failure"
except Exception as e:
# 验证码错误或其他登录失败
error_msg = f"验证码验证失败: {str(e)}"
self._log(f"{error_msg}", "ERROR")
self._send_verification_error_message(store_id, error_msg)
return "verification_code_error"
except Exception as e:
# 登录过程中的其他错误
error_msg = f"登录过程出错: {str(e)}"
self._log(f"{error_msg}", "ERROR")
self._send_login_failure_message(store_id, error_msg)
return "login_failure"
# ===== 抖音登录相关类集成结束 =====
# 抖音WebSocket管理器类 # 抖音WebSocket管理器类
class DouYinWebsocketManager: class DouYinWebsocketManager:
_instance = None _instance = None
@@ -100,53 +599,21 @@ class DouYinBackendService:
return True return True
async def send_message_to_backend(self, platform_message): async def send_message_to_backend(self, platform_message):
"""改为通过单后端连接发送,需携带store_id""" """🔥 改为通过单后端连接发送,与拼多多保持完全一致的逻辑"""
try: try:
from WebSocket.backend_singleton import get_backend_client from WebSocket.backend_singleton import get_backend_client
backend = get_backend_client() backend = get_backend_client()
if not backend: if not backend:
return None return None
# 从platform_message中构造统一上行结构并附加store_id # 🔥 确保消息包含store_id与拼多多一致
body = platform_message.get('body', {}) if isinstance(platform_message, dict) else {} if isinstance(platform_message, dict):
sender_id = platform_message.get('sender', {}).get('id', '') if isinstance(platform_message, dict) else '' if 'store_id' not in platform_message and self.current_store_id:
platform_message['store_id'] = self.current_store_id
# 优先取消息内的store_id其次取body内再次退回当前会话store_id # 🔥 通过统一后端连接发送(与拼多多完全一致)
store_id = (platform_message.get('store_id') backend.send_message(platform_message)
or body.get('store_id') return True
or self.current_store_id
or '')
# 检查消息类型如果是特殊类型如staff_list保持原格式
message_type = platform_message.get('type', 'message')
if message_type == 'staff_list':
# 对于客服列表消息,直接转发原始格式
msg = platform_message.copy()
# 确保store_id正确
msg['store_id'] = store_id
else:
# 对于普通消息,使用原有的格式转换逻辑
msg_type = platform_message.get('msg_type', 'text')
content_for_backend = platform_message.get('content', '')
pin_image = platform_message.get('pin_image')
if not pin_image:
pin_image = ""
else:
pass
# 构造标准消息格式
msg = {
'type': 'message',
'content': content_for_backend,
'pin_image': pin_image,
'msg_type': msg_type,
'sender': {'id': sender_id},
'store_id': store_id
}
backend.send_message(msg)
return None
except Exception: except Exception:
return None return None
@@ -201,7 +668,7 @@ class DouYinMessageHandler:
print(f"[DY Handler] 创建实例 {self.instance_id} for store {store_id}") print(f"[DY Handler] 创建实例 {self.instance_id} for store {store_id}")
def get_casl(self): def get_casl(self):
"""获取可分配客服列表""" """获取可分配客服列表 - 根据原始代码实现"""
headers = { headers = {
"authority": "pigeon.jinritemai.com", "authority": "pigeon.jinritemai.com",
"accept": "application/json, text/plain, */*", "accept": "application/json, text/plain, */*",
@@ -226,16 +693,27 @@ class DouYinMessageHandler:
"_ts": int(time.time() * 1000), "_ts": int(time.time() * 1000),
"_pms": "1", "_pms": "1",
"FUSION": "true", "FUSION": "true",
"verifyFp": "", "verifyFp": "", # 🔥 恢复为空字符串,因为原始代码中也是空的
"_v": "1.0.1.3585" "_v": "1.0.1.3585"
} }
try: try:
self._log(f"🔄 正在获取客服列表cookies包含字段: {list(self.cookie.keys())}", "DEBUG")
# 🔥 按照原始代码的方式处理响应
response = requests.get(url, headers=headers, cookies=self.cookie, params=params).json() response = requests.get(url, headers=headers, cookies=self.cookie, params=params).json()
self._log(f"客服列表API响应内容: {response}", "DEBUG")
if response.get('code') == 0: if response.get('code') == 0:
return response.get('data', []) staff_data = response.get('data', [])
self._log(f"✅ 成功获取客服列表,共 {len(staff_data)} 个客服", "SUCCESS")
return staff_data
else:
error_msg = response.get('message', '未知错误')
self._log(f"❌ 客服列表API返回错误: code={response.get('code')}, message={error_msg}", "ERROR")
return None return None
except Exception as e: except Exception as e:
self._log(f"❌ 获取客服列表失败: {e}", "ERROR") self._log(f"❌ 获取客服列表失败: {e}", "ERROR")
import traceback
self._log(f"错误详情: {traceback.format_exc()}", "DEBUG")
return None return None
def transfer_conversation(self, receiver_id, shop_id, staff_id): def transfer_conversation(self, receiver_id, shop_id, staff_id):
@@ -282,18 +760,26 @@ class DouYinMessageHandler:
try: try:
# 获取客服列表 # 获取客服列表
staff_list = self.get_casl() staff_list = self.get_casl()
if not staff_list: if staff_list is None:
self._log("⚠️ 获取客服列表失败", "WARNING") self._log("⚠️ 获取客服列表失败", "WARNING")
return False return False
# 转换客服数据格式 # 🔥 处理空客服列表的情况API成功但无客服
if len(staff_list) == 0:
self._log("📋 当前无可分配客服,发送空列表到后端", "INFO")
# 继续处理,发送空列表给后端
# 转换客服数据格式 - 🔥 根据原始代码调试实际字段名
staff_infos = [] staff_infos = []
for staff in staff_list: for staff in staff_list:
# 打印原始数据结构用于调试
self._log(f"🔍 原始客服数据结构: {staff}", "DEBUG")
staff_info = StaffInfo( staff_info = StaffInfo(
staff_id=str(staff.get('staffId', '')), staff_id=str(staff.get('staffId', '') or staff.get('staff_id', '') or staff.get('id', '')),
name=staff.get('staffName', ''), name=staff.get('staffName', '') or staff.get('staff_name', '') or staff.get('name', ''),
status=staff.get('status', 0), status=str(staff.get('status', 0) or staff.get('state', 0)),
department=staff.get('department', ''), department=staff.get('department', '') or staff.get('dept', ''),
online=staff.get('online', True) online=staff.get('online', True)
) )
staff_infos.append(staff_info.to_dict()) staff_infos.append(staff_info.to_dict())
@@ -312,9 +798,15 @@ class DouYinMessageHandler:
# 发送到后端 # 发送到后端
await self.ai_service.send_message_to_backend(message_template.to_dict()) await self.ai_service.send_message_to_backend(message_template.to_dict())
self._log(f"发送客服列表消息的结构体为: {message_template.to_json()}") self._log(f"发送客服列表消息的结构体为: {message_template.to_json()}")
if len(staff_infos) > 0:
self._log(f"✅ [DY] 成功发送客服列表到后端,共 {len(staff_infos)} 个客服", "SUCCESS") self._log(f"✅ [DY] 成功发送客服列表到后端,共 {len(staff_infos)} 个客服", "SUCCESS")
print(f"🔥 [DY] 客服列表已上传到后端: {len(staff_infos)} 个客服") print(f"🔥 [DY] 客服列表已上传到后端: {len(staff_infos)} 个客服")
print(f"[DY] 客服详情: {[{'id': s['staff_id'], 'name': s['name']} for s in staff_infos]}") print(f"[DY] 客服详情: {[{'id': s['staff_id'], 'name': s['name']} for s in staff_infos]}")
else:
self._log(f"✅ [DY] 成功发送空客服列表到后端(当前无可分配客服)", "SUCCESS")
print(f"🔥 [DY] 空客服列表已上传到后端(当前无可分配客服)")
return True return True
except Exception as e: except Exception as e:
@@ -603,26 +1095,28 @@ class DouYinMessageHandler:
talk_id = user_info.get("talk_id", 0) talk_id = user_info.get("talk_id", 0)
p_id = user_info.get("p_id", 0) p_id = user_info.get("p_id", 0)
# 准备发送者和接收者信息 # 🔥 统一消息类型检测逻辑(与拼多多保持一致)
sender_info = { content = message_content
"id": str(sender_id), lc = str(content).lower()
"name": f"用户_{sender_id}",
"is_customer": True
}
receiver_info = {
"id": self.store_id,
"name": "店铺客服",
"is_merchant": True
}
# 创建消息模板 # 检测消息类型,使用与拼多多相同的逻辑
message_template = PlatformMessage( if any(ext in lc for ext in [".jpg", ".jpeg", ".png", ".gif", ".webp"]):
type="message", msg_type = "image"
content=message_content, elif any(ext in lc for ext in [".mp4", ".avi", ".mov", ".wmv", ".flv"]):
msg_type="text", msg_type = "video"
sender={"id": sender_info.get("id", "")}, elif any(keyword in lc for keyword in ['goods.html', 'item.html', 'item.jd.com', '商品卡片id']):
msg_type = "product_card"
else:
msg_type = "text"
# 🔥 使用工厂方法创建消息模板(与拼多多保持一致)
message_template = PlatformMessage.create_text_message(
content=content,
sender_id=str(sender_id),
store_id=self.store_id store_id=self.store_id
) )
# 动态设置检测到的消息类型
message_template.msg_type = msg_type
# 发送消息到后端(使用统一连接) # 发送消息到后端(使用统一连接)
await self.ai_service.send_message_to_backend(message_template.to_dict()) await self.ai_service.send_message_to_backend(message_template.to_dict())
@@ -1034,7 +1528,6 @@ class DouYinMessageHandler:
content_data = msg_content["8"] content_data = msg_content["8"]
self._log(f"📄 原始内容数据: {content_data}", "DEBUG") self._log(f"📄 原始内容数据: {content_data}", "DEBUG")
if message_dict and message_dict.get('text') != "客服水滴智能优品接入": if message_dict and message_dict.get('text') != "客服水滴智能优品接入":
self._log(f"💬 成功解析用户消息: '{message_dict}'", "SUCCESS") self._log(f"💬 成功解析用户消息: '{message_dict}'", "SUCCESS")
@@ -1195,22 +1688,61 @@ class DouYinMessageHandler:
elif msg_type == 'text' and avatar_url: elif msg_type == 'text' and avatar_url:
message_text = message_dict['text'] message_text = message_dict['text']
# 准备发送者和接收者信息 # 🔥 提取goods_info信息与拼多多保持一致
sender_info = { goods_info = {}
"id": str(sender_id), if msg_type == 'order':
"name": f"用户_{sender_id}", # 从抖音的订单信息中提取goods_info
"is_customer": True order_id = message_dict.get('order_id', '')
goods_id = self.get_goods_id(order_id) if hasattr(self, 'get_goods_id') else message_dict.get(
'goods_id', '')
goods_info = {
'goodsID': goods_id,
'orderSequenceNo': order_id
}
elif msg_type == 'goods':
# 从抖音的商品信息中提取goods_info
goods_id = message_dict.get('goods_id', '')
goods_info = {
'goodsID': goods_id
} }
# 创建消息模板 # 🔥 统一消息类型检测逻辑(与拼多多完全一致)
message_template = PlatformMessage( content = message_text
type="message", lc = str(content).lower()
content=message_text,
pin_image=avatar_url, # 使用与拼多多完全相同的检测逻辑
msg_type=msg_type, if any(ext in lc for ext in [".jpg", ".jpeg", ".png", ".gif", ".webp"]):
sender={"id": sender_info.get("id", "")}, msg_type = "image"
store_id=self.store_id elif any(ext in lc for ext in [".mp4", ".avi", ".mov", ".wmv", ".flv"]):
msg_type = "video"
else:
msg_type = "text"
# 🔥 订单卡片组装(与拼多多完全一致)
if ("订单编号" in str(content) or msg_type == 'order') and goods_info:
content = f"商品id{goods_info.get('goodsID')} 订单号:{goods_info.get('orderSequenceNo')}"
msg_type = "order_card"
# 🔥 商品卡片检测(与拼多多完全一致)
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(sender_id),
store_id=self.store_id,
pin_image=avatar_url
) )
# 动态设置检测到的消息类型
message_template.msg_type = msg_type
# 🔥 添加调试日志(与拼多多保持一致)
try:
print(f"📤(SPEC) 发送到AI: {json.dumps(message_template.to_dict(), ensure_ascii=False)[:300]}...")
except Exception:
pass
self._log("📤 准备发送消息到AI服务...", "INFO") self._log("📤 准备发送消息到AI服务...", "INFO")
self._log(f"📋 消息内容: {message_template.to_json()}", "DEBUG") self._log(f"📋 消息内容: {message_template.to_json()}", "DEBUG")
@@ -1549,12 +2081,14 @@ class DouYinMessageHandler:
async def send_message_external(self, receiver_id: str, content: str) -> bool: async def send_message_external(self, receiver_id: str, content: str) -> bool:
"""外部调用的发送消息方法 - 用于后端消息转发""" """外部调用的发送消息方法 - 用于后端消息转发"""
try: try:
self._log(f"🔄 [External-{self.instance_id}] 收到转发请求: receiver_id={receiver_id}, content={content}", "INFO") self._log(f"🔄 [External-{self.instance_id}] 收到转发请求: receiver_id={receiver_id}, content={content}",
"INFO")
# 修复数据类型不匹配问题:将字符串转换为整数 # 修复数据类型不匹配问题:将字符串转换为整数
try: try:
receiver_id_int = int(receiver_id) receiver_id_int = int(receiver_id)
self._log(f"🔧 [External-{self.instance_id}] 转换 receiver_id: '{receiver_id}' -> {receiver_id_int}", "DEBUG") self._log(f"🔧 [External-{self.instance_id}] 转换 receiver_id: '{receiver_id}' -> {receiver_id_int}",
"DEBUG")
except ValueError: except ValueError:
self._log(f"❌ [External-{self.instance_id}] receiver_id 无法转换为整数: {receiver_id}", "ERROR") self._log(f"❌ [External-{self.instance_id}] receiver_id 无法转换为整数: {receiver_id}", "ERROR")
return False return False
@@ -1566,7 +2100,9 @@ class DouYinMessageHandler:
# 检查用户是否存在于user_tokens中使用整数类型 # 检查用户是否存在于user_tokens中使用整数类型
self._log(f"🔍 [External-{self.instance_id}] 调试信息:", "DEBUG") self._log(f"🔍 [External-{self.instance_id}] 调试信息:", "DEBUG")
self._log(f"🔍 [External-{self.instance_id}] receiver_id_int: {receiver_id_int} (类型: {type(receiver_id_int)})", "DEBUG") self._log(
f"🔍 [External-{self.instance_id}] receiver_id_int: {receiver_id_int} (类型: {type(receiver_id_int)})",
"DEBUG")
self._log(f"🔍 [External-{self.instance_id}] user_tokens keys: {list(self.user_tokens.keys())}", "DEBUG") self._log(f"🔍 [External-{self.instance_id}] user_tokens keys: {list(self.user_tokens.keys())}", "DEBUG")
if self.user_tokens: if self.user_tokens:
first_key = list(self.user_tokens.keys())[0] first_key = list(self.user_tokens.keys())[0]
@@ -1587,11 +2123,15 @@ class DouYinMessageHandler:
p_id = user_info.get("p_id") p_id = user_info.get("p_id")
user_token = user_info.get("token") user_token = user_info.get("token")
self._log(f"🔍 [External-{self.instance_id}] 用户会话信息: talk_id={talk_id}, p_id={p_id}, has_token={bool(user_token)}", "DEBUG") self._log(
f"🔍 [External-{self.instance_id}] 用户会话信息: talk_id={talk_id}, p_id={p_id}, has_token={bool(user_token)}",
"DEBUG")
# 检查必要参数 # 检查必要参数
if not talk_id or not p_id: if not talk_id or not p_id:
self._log(f"❌ [External-{self.instance_id}] 用户 {receiver_id_int} 缺少必要的会话信息 (talk_id: {talk_id}, p_id: {p_id})", "ERROR") self._log(
f"❌ [External-{self.instance_id}] 用户 {receiver_id_int} 缺少必要的会话信息 (talk_id: {talk_id}, p_id: {p_id})",
"ERROR")
return False return False
if not user_token: if not user_token:
@@ -1602,7 +2142,9 @@ class DouYinMessageHandler:
if "pending_messages" not in user_info: if "pending_messages" not in user_info:
user_info["pending_messages"] = [] user_info["pending_messages"] = []
user_info["pending_messages"].append(content) user_info["pending_messages"].append(content)
self._log(f"📝 [External-{self.instance_id}] 消息已加入待发送队列,队列长度: {len(user_info['pending_messages'])}", "INFO") self._log(
f"📝 [External-{self.instance_id}] 消息已加入待发送队列,队列长度: {len(user_info['pending_messages'])}",
"INFO")
return True return True
# 发送消息 (注意_send_message_to_user 可能期望字符串类型的receiver_id) # 发送消息 (注意_send_message_to_user 可能期望字符串类型的receiver_id)
@@ -1838,6 +2380,8 @@ class DouYinListenerForGUI:
# 发送客服列表到后端 # 发送客服列表到后端
try: try:
# 🔥 等待一小段时间确保连接完全建立
await asyncio.sleep(1)
staff_list_success = await self.douyin_bot.message_handler.send_staff_list_to_backend() staff_list_success = await self.douyin_bot.message_handler.send_staff_list_to_backend()
if staff_list_success: if staff_list_success:
print(f"🔥 [DY] 客服列表已上传到后端") print(f"🔥 [DY] 客服列表已上传到后端")
@@ -1864,6 +2408,25 @@ class DouYinListenerForGUI:
# 在后台启动监听任务 # 在后台启动监听任务
asyncio.create_task(keep_running()) asyncio.create_task(keep_running())
# 🔥 新增Cookie登录成功后发送登录成功报告与登录参数模式保持一致
try:
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": cookie_dict # 添加cookie信息
}
backend.send_message(message)
self._log("✅ [DY] 已向后端发送Cookie登录成功报告", "SUCCESS")
else:
self._log("⚠️ [DY] 无法获取后端客户端,跳过状态报告", "WARNING")
except Exception as e:
self._log(f"⚠️ [DY] 发送登录成功报告失败: {e}", "WARNING")
self._log("✅ [DY] 抖音平台连接成功,开始监听消息", "SUCCESS") self._log("✅ [DY] 抖音平台连接成功,开始监听消息", "SUCCESS")
return True return True
else: else:
@@ -1876,6 +2439,69 @@ class DouYinListenerForGUI:
self._log(f"错误详情: {traceback.format_exc()}", "DEBUG") self._log(f"错误详情: {traceback.format_exc()}", "DEBUG")
return False return False
async def start_with_login_params(self, store_id: str, login_params: str):
"""使用后端下发的登录参数执行登录并启动监听(与拼多多保持一致)"""
try:
self._log("🔵 [DY] 收到后端登录参数开始执行登录获取cookies", "INFO")
self._log(f"🔍 [DY] 登录参数内容: {login_params[:100]}...", "DEBUG")
# 1. 解析登录参数
self._log("🔄 [DY] 开始解析登录参数", "DEBUG")
params_dict = self._parse_login_params(login_params)
if not params_dict:
self._log("❌ [DY] 登录参数解析失败", "ERROR")
return False
self._log(f"✅ [DY] 登录参数解析成功,手机号: {params_dict.get('phone_number', 'N/A')}", "DEBUG")
# 2. 使用新的DyLogin类执行登录
self._log("🔄 [DY] 开始创建DyLogin实例", "DEBUG")
dy_login = DyLogin(log_callback=self.log_callback)
self._log("✅ [DY] DyLogin实例创建成功", "DEBUG")
self._log("🔄 [DY] 开始执行登录", "DEBUG")
login_result = dy_login.login_with_params(params_dict, store_id)
self._log(f"📊 [DY] 登录结果: {login_result}", "DEBUG")
if login_result == "need_verification_code":
self._log("⚠️ [DY] 需要手机验证码,已通知后端,等待重新下发包含验证码的登录参数", "WARNING")
return "need_verification_code" # 返回特殊标识,避免被覆盖
elif login_result == "verification_code_error":
self._log("⚠️ [DY] 验证码错误,已通知后端", "WARNING")
return "verification_code_error" # 返回特殊标识,避免重复发送消息
elif login_result == "login_failure":
self._log("⚠️ [DY] 登录失败,已发送失败通知给后端", "WARNING")
return "login_failure" # 返回特殊标识,避免重复发送消息
elif not login_result:
self._log("❌ [DY] 登录失败", "ERROR")
return False
elif isinstance(login_result, dict):
# 登录成功获取到cookies
self._log("✅ [DY] 登录成功使用获取的cookies连接平台", "SUCCESS")
# 🔥 使用获取的cookies启动监听这里会验证cookies完整性
return await self.start_with_cookies(store_id, login_result)
else:
self._log("❌ [DY] 登录返回未知结果", "ERROR")
return False
except Exception as e:
self._log(f"❌ [DY] 使用登录参数启动失败: {str(e)}", "ERROR")
import traceback
self._log(f"🔍 [DY] 异常详细信息: {traceback.format_exc()}", "ERROR")
return False
def _parse_login_params(self, login_params_str: str) -> dict:
"""解析后端下发的登录参数(与拼多多保持一致)"""
try:
import json
data = json.loads(login_params_str)
params = data.get("data", {})
self._log(f"✅ [DY] 登录参数解析成功: phone_number={params.get('phone_number', 'N/A')}", "INFO")
return params
except Exception as e:
self._log(f"❌ [DY] 解析登录参数失败: {e}", "ERROR")
return {}
def stop_listening(self): def stop_listening(self):
"""停止监听""" """停止监听"""
if self.stop_event: if self.stop_event:

View File

@@ -999,8 +999,7 @@ class ImgDistance:
save_path=save_path save_path=save_path
) )
# AutiContent类已移除 - 后端会提供所有必要的anti_content
# AutiContent类已移除 - 后端会提供所有必要的anti_content
def gzip_compress(self, data): def gzip_compress(self, data):
"""压缩数据""" """压缩数据"""
@@ -1691,7 +1690,8 @@ class PddLogin:
message = { message = {
"type": "connect_message", "type": "connect_message",
"store_id": store_id, "store_id": store_id,
"status": True # 登录成功 "status": True, # 登录成功
"cookies": self.cookies # 🔥 新增添加登录生成的cookie信息
} }
self._log(f"准备发送登录成功消息: {message}", "DEBUG") self._log(f"准备发送登录成功消息: {message}", "DEBUG")
backend.send_message(message) backend.send_message(message)
@@ -1732,7 +1732,9 @@ class PddLogin:
self._log("🚀 [PddLogin] 开始使用参数登录", "INFO") self._log("🚀 [PddLogin] 开始使用参数登录", "INFO")
# 检查验证码字段(兼容 code 和 verification_code # 检查验证码字段(兼容 code 和 verification_code
verification_code = login_params.get("verification_code") or login_params.get("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._log(
f"📋 [PddLogin] 登录参数: username={login_params.get('username', 'N/A')}, 包含验证码={bool(verification_code)}",
"DEBUG")
self.headers["anti-content"] = login_params.get("anti_content", "") self.headers["anti-content"] = login_params.get("anti_content", "")
@@ -1820,7 +1822,8 @@ class PddLogin:
salt = self.vc_pre_ck_b() # 获取生成aes key和iv 的密文值 salt = self.vc_pre_ck_b() # 获取生成aes key和iv 的密文值
pictures = self.obtain_captcha() # 获取验证码图片 pictures = self.obtain_captcha() # 获取验证码图片
distance = round((ImgDistance(bg=pictures[0], tp=pictures[1]).main() * (272 / 320)) + (48.75 / 2), 2) # 计算距离 distance = round((ImgDistance(bg=pictures[0], tp=pictures[1]).main() * (272 / 320)) + (48.75 / 2),
2) # 计算距离
track_list = Track.get_track(distance=distance) # 生成轨迹 track_list = Track.get_track(distance=distance) # 生成轨迹
captcha_collect = self.captcha_collect(salt=salt, track_list=track_list) # 生成captcha_collect参数 captcha_collect = self.captcha_collect(salt=salt, track_list=track_list) # 生成captcha_collect参数
@@ -1834,7 +1837,8 @@ class PddLogin:
success_count += 1 success_count += 1
# 如果滑块成功 success_count 计数一次 成功8次还是显示验证码则失败 返回False # 如果滑块成功 success_count 计数一次 成功8次还是显示验证码则失败 返回False
if success_count < 8: if success_count < 8:
return self.login_with_params(login_params=login_params, store_id=store_id, success_count=success_count) return self.login_with_params(login_params=login_params, store_id=store_id,
success_count=success_count)
else: else:
return False return False
else: else:
@@ -1896,6 +1900,7 @@ class PddLogin:
self._send_login_failure_message(store_id, error_msg) self._send_login_failure_message(store_id, error_msg)
return "login_failure" # 返回特殊状态,避免重复发送消息 return "login_failure" # 返回特殊状态,避免重复发送消息
# ===== 登录相关类集成结束 ===== # ===== 登录相关类集成结束 =====
@@ -2801,6 +2806,28 @@ class ChatPdd:
except Exception as e: except Exception as e:
self._log(f"❌ 获取或发送客服列表失败: {e}", "ERROR") 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( await asyncio.gather(
self.heartbeat(websocket), self.heartbeat(websocket),
@@ -3080,6 +3107,7 @@ class PddListenerForGUI:
cookies_str=cookies, cookies_str=cookies,
store=store store=store
) )
self._log("✅ [PDD] 拼多多平台连接成功,开始监听消息", "SUCCESS") self._log("✅ [PDD] 拼多多平台连接成功,开始监听消息", "SUCCESS")
return True return True
except Exception as e: except Exception as e:
@@ -3125,8 +3153,6 @@ class PddListenerForGUI:
self._log(f"❌ [PDD] 解析登录参数失败: {e}", "ERROR") self._log(f"❌ [PDD] 解析登录参数失败: {e}", "ERROR")
return {} return {}
def get_status(self) -> Dict[str, Any]: def get_status(self) -> Dict[str, Any]:
return { return {
"running": self.running, "running": self.running,

View File

@@ -30,6 +30,7 @@ class BackendClient:
self.login_callback: Optional[Callable] = None # 新增平台登录下发cookies回调 self.login_callback: Optional[Callable] = None # 新增平台登录下发cookies回调
self.success_callback: Optional[Callable] = None # 新增:后端连接成功回调 self.success_callback: Optional[Callable] = None # 新增:后端连接成功回调
self.token_error_callback: Optional[Callable] = None # 新增token错误回调 self.token_error_callback: Optional[Callable] = None # 新增token错误回调
self.version_callback: Optional[Callable] = None # 新增:版本检查回调
self.is_connected = False self.is_connected = False
@@ -100,7 +101,7 @@ class BackendClient:
ping_timeout=WS_PING_TIMEOUT, # 可配置ping超时 ping_timeout=WS_PING_TIMEOUT, # 可配置ping超时
close_timeout=10, # 10秒关闭超时 close_timeout=10, # 10秒关闭超时
# 增加TCP keepalive配置 # 增加TCP keepalive配置
max_size=2**20, # 1MB最大消息大小 max_size=2 ** 20, # 1MB最大消息大小
max_queue=32, # 最大队列大小 max_queue=32, # 最大队列大小
compression=None # 禁用压缩以提高性能 compression=None # 禁用压缩以提高性能
) )
@@ -108,7 +109,7 @@ class BackendClient:
else: else:
self.websocket = await websockets.connect( self.websocket = await websockets.connect(
self.url, self.url,
max_size=2**20, max_size=2 ** 20,
max_queue=32, max_queue=32,
compression=None compression=None
) )
@@ -263,7 +264,8 @@ class BackendClient:
error: Callable = None, error: Callable = None,
login: Callable = None, login: Callable = None,
success: Callable = None, success: Callable = None,
token_error: Callable = None): token_error: Callable = None,
version: Callable = None):
"""设置各种消息类型的回调函数""" """设置各种消息类型的回调函数"""
if store_list: if store_list:
self.store_list_callback = store_list self.store_list_callback = store_list
@@ -283,6 +285,8 @@ class BackendClient:
self.success_callback = success self.success_callback = success
if token_error: if token_error:
self.token_error_callback = token_error self.token_error_callback = token_error
if version:
self.version_callback = version
def on_connected(self): def on_connected(self):
"""连接成功时的处理""" """连接成功时的处理"""
@@ -437,6 +441,8 @@ class BackendClient:
self._handle_token_error(message) self._handle_token_error(message)
elif msg_type == 'staff_list': elif msg_type == 'staff_list':
self._handle_staff_list(message) self._handle_staff_list(message)
elif msg_type == 'version_response': # 新增:版本检查响应
self._handle_version_response(message)
else: else:
print(f"未知消息类型: {msg_type}") print(f"未知消息类型: {msg_type}")
@@ -1151,12 +1157,14 @@ class BackendClient:
content = message.get('content', '') content = message.get('content', '')
data = message.get('data', {}) data = message.get('data', {})
# 判断是拼多多登录参数还是普通Cookie # 🔥 判断是登录参数模式还是普通Cookie模式(支持拼多多和抖音)
if platform_name == "拼多多" and ("拼多多登录" in content) and data.get('login_params'): if (platform_name in ["拼多多", "抖音"] and
# 拼多多登录参数模式 - 传递完整的消息JSON给处理器 (("拼多多登录" in content and data.get('login_params')) or
print(f"收到拼多多登录参数: 平台={platform_name}, 店铺={store_id}, 类型={content}") ("抖音登录" in content and data.get('login_flow')))):
# 登录参数模式 - 传递完整的消息JSON给处理器
print(f"收到{platform_name}登录参数: 平台={platform_name}, 店铺={store_id}, 类型={content}")
if self.login_callback: if self.login_callback:
# 传递完整的JSON消息拼多多处理器来解析login_params # 传递完整的JSON消息让处理器来解析登录参数
import json import json
full_message = json.dumps(message) full_message = json.dumps(message)
self.login_callback(platform_name, store_id, full_message) self.login_callback(platform_name, store_id, full_message)
@@ -1211,6 +1219,16 @@ class BackendClient:
if self.customers_callback: # 假设客服列表更新也触发客服列表回调 if self.customers_callback: # 假设客服列表更新也触发客服列表回调
self.customers_callback(staff_list, store_id) self.customers_callback(staff_list, store_id)
def _handle_version_response(self, message: Dict[str, Any]):
"""处理版本检查响应"""
latest_version = message.get('latest_version')
download_url = message.get('download_url')
print(f"收到版本检查响应: 最新版本={latest_version}, 下载地址={download_url}")
if self.version_callback:
self.version_callback(message)
# ==================== 辅助方法 ==================== # ==================== 辅助方法 ====================
def set_token(self, token: str): def set_token(self, token: str):

View File

@@ -27,7 +27,13 @@ class WebSocketManager:
"""WebSocket连接管理器统一处理后端连接和平台监听""" """WebSocket连接管理器统一处理后端连接和平台监听"""
def __init__(self): def __init__(self):
# 后端客户端
self.backend_client = None self.backend_client = None
self.is_connected = False
# 版本检查器
self.version_checker = None
self.gui_update_callback = None
self.platform_listeners = {} # 存储各平台的监听器 self.platform_listeners = {} # 存储各平台的监听器
self.connected_platforms = [] # 存储已连接的平台列表 # <- 新增 self.connected_platforms = [] # 存储已连接的平台列表 # <- 新增
self.callbacks = { self.callbacks = {
@@ -75,7 +81,6 @@ class WebSocketManager:
"""连接后端WebSocket""" """连接后端WebSocket"""
try: try:
# 1 保存token到配置 # 1 保存token到配置
try: try:
from config import set_saved_token from config import set_saved_token
@@ -105,6 +110,8 @@ class WebSocketManager:
self._log("连接服务成功", "SUCCESS") self._log("连接服务成功", "SUCCESS")
if self.callbacks['success']: if self.callbacks['success']:
self.callbacks['success']() self.callbacks['success']()
# 启动版本检查器
self._start_version_checker()
except Exception as e: except Exception as e:
self._log(f"成功回调执行失败: {e}", "ERROR") self._log(f"成功回调执行失败: {e}", "ERROR")
@@ -119,7 +126,8 @@ class WebSocketManager:
if self.callbacks['token_error']: if self.callbacks['token_error']:
self.callbacks['token_error'](error_content) self.callbacks['token_error'](error_content)
backend.set_callbacks(success=_on_backend_success, login=_on_backend_login, token_error=_on_token_error) backend.set_callbacks(success=_on_backend_success, login=_on_backend_login,
token_error=_on_token_error)
if not backend.is_connected: if not backend.is_connected:
backend.connect() backend.connect()
@@ -144,6 +152,8 @@ class WebSocketManager:
self._log("连接服务成功", "SUCCESS") self._log("连接服务成功", "SUCCESS")
if self.callbacks['success']: if self.callbacks['success']:
self.callbacks['success']() self.callbacks['success']()
# 启动版本检查器
self._start_version_checker()
except Exception as e: except Exception as e:
self._log(f"成功回调执行失败: {e}", "ERROR") self._log(f"成功回调执行失败: {e}", "ERROR")
@@ -220,6 +230,27 @@ class WebSocketManager:
import traceback import traceback
self._log(f"错误详情: {traceback.format_exc()}", "DEBUG") self._log(f"错误详情: {traceback.format_exc()}", "DEBUG")
def _start_version_checker(self):
"""启动版本检查器"""
try:
from version_checker import VersionChecker
self.version_checker = VersionChecker(
backend_client=self.backend_client,
update_callback=self._on_update_available
)
self.backend_client.version_callback = self.version_checker.handle_version_response
self.version_checker.start()
self._log("✅ 版本检查器已启动将每10分钟检查一次更新", "SUCCESS")
except Exception as e:
self._log(f"❌ 启动版本检查器失败: {e}", "ERROR")
def _on_update_available(self, latest_version, download_url):
"""发现新版本时的处理"""
self._log(f"🔔 发现新版本 {latest_version}", "INFO")
# 通知主GUI显示更新提醒
if hasattr(self, 'gui_update_callback') and self.gui_update_callback:
self.gui_update_callback(latest_version, download_url)
def _start_jd_listener(self, store_id: str, cookies: str): def _start_jd_listener(self, store_id: str, cookies: str):
"""启动京东平台监听""" """启动京东平台监听"""
try: try:
@@ -275,15 +306,59 @@ class WebSocketManager:
def _runner(): def _runner():
try: try:
import json import json
self._log("🚀 开始创建抖音监听器实例", "DEBUG")
listener = DYListenerForGUI_WS() listener = DYListenerForGUI_WS()
# 将JSON字符串格式的cookies解析为字典 self._log("✅ 抖音监听器实例创建成功", "DEBUG")
try:
cookie_dict = json.loads(cookies) if isinstance(cookies, str) else cookies
except json.JSONDecodeError as e:
self._log(f"❌ Cookie JSON解析失败: {e}", "ERROR")
return False
# 🔥 检查是否为登录参数模式(与拼多多保持一致)
if cookies and ('"login_flow"' in cookies or '"phone_number"' in cookies):
# 使用登录参数模式
self._log("📋 使用登录参数启动抖音监听器", "INFO")
self._log("🔄 开始执行 start_with_login_params", "DEBUG")
result = asyncio.run(listener.start_with_login_params(store_id=store_id, login_params=cookies))
self._log(f"📊 start_with_login_params 执行结果: {result}", "DEBUG")
# 🔥 详细的结果分析(与拼多多完全一致)
if result == "need_verification_code":
self._log("✅ [DY] 登录流程正常,已发送验证码需求通知给后端", "SUCCESS")
elif result == "verification_code_error":
self._log("⚠️ [DY] 验证码错误,已发送错误通知给后端", "WARNING")
elif result:
self._log("✅ [DY] 登录成功,平台连接已建立", "SUCCESS")
self._notify_platform_connected("抖音")
else:
self._log("❌ [DY] 登录失败", "ERROR")
else:
# 传统cookie模式保留兼容性
self._log("🍪 使用Cookie启动抖音监听器", "INFO")
self._log("🔄 开始执行 start_with_cookies", "DEBUG")
try:
# 🔥 修复尝试JSON解析失败时用ast.literal_eval解析Python字典字符串
if isinstance(cookies, str):
try:
cookie_dict = json.loads(cookies)
except json.JSONDecodeError:
# 后端发送的是Python字典字符串格式使用ast.literal_eval
import ast
cookie_dict = ast.literal_eval(cookies)
self._log("✅ 使用ast.literal_eval成功解析cookies", "DEBUG")
else:
cookie_dict = cookies
except (json.JSONDecodeError, ValueError, SyntaxError) as e:
self._log(f"❌ Cookie解析失败: {e}", "ERROR")
return False
result = asyncio.run(listener.start_with_cookies(store_id=store_id, cookie_dict=cookie_dict)) result = asyncio.run(listener.start_with_cookies(store_id=store_id, cookie_dict=cookie_dict))
self._log(f"📊 start_with_cookies 执行结果: {result}", "DEBUG")
# Cookie启动成功时也要通知GUI
if result:
self._log("✅ [DY] Cookie启动成功平台连接已建立", "SUCCESS")
self._notify_platform_connected("抖音")
# 🔥 移除不再在backend_singleton中发送connect_message
# 抖音的连接状态报告应该在DyUtils中的DyLogin类中发送与拼多多保持一致
# 所有特殊状态通知都已经在DyLogin中发送过了这里不需要重复发送
return result return result
except Exception as e: except Exception as e:
self._log(f"抖音监听器运行异常: {e}", "ERROR") self._log(f"抖音监听器运行异常: {e}", "ERROR")
@@ -304,34 +379,13 @@ class WebSocketManager:
if f"抖音:{store_id}" in self.platform_listeners: if f"抖音:{store_id}" in self.platform_listeners:
self.platform_listeners[f"抖音:{store_id}"]['status'] = 'success' self.platform_listeners[f"抖音:{store_id}"]['status'] = 'success'
# 上报连接状态给后端
if self.backend_client:
try:
self.backend_client.send_message({
"type": "connect_message",
"store_id": store_id,
"status": True
})
self._log("已上报抖音平台连接状态: 成功", "INFO")
except Exception as e:
self._log(f"上报抖音平台连接状态失败: {e}", "WARNING")
self._log("已启动抖音平台监听", "SUCCESS") self._log("已启动抖音平台监听", "SUCCESS")
self._notify_platform_connected("抖音") # ← 新增
except Exception as e: except Exception as e:
self._log(f"启动抖音平台监听失败: {e}", "ERROR") self._log(f"启动抖音平台监听失败: {e}", "ERROR")
# 确保失败时也上报状态 # 🔥 移除:确保失败时也不在这里上报状态
if self.backend_client: # 失败状态应该在DyLogin中处理与拼多多保持一致
try:
self.backend_client.send_message({
"type": "connect_message",
"store_id": store_id,
"status": False
})
except Exception as send_e:
self._log(f"失败状态下报抖音平台连接状态也失败: {send_e}", "ERROR")
def _start_qianniu_listener(self, store_id: str, cookies: str): def _start_qianniu_listener(self, store_id: str, cookies: str):
"""启动千牛平台监听(单连接多店铺架构)""" """启动千牛平台监听(单连接多店铺架构)"""
@@ -433,7 +487,8 @@ class WebSocketManager:
self._notify_platform_connected("拼多多") self._notify_platform_connected("拼多多")
# 根据实际登录结果上报状态给后端 # 根据实际登录结果上报状态给后端
if self.backend_client and result not in ["need_verification_code", "verification_code_error", "login_failure"]: if self.backend_client and result not in ["need_verification_code", "verification_code_error",
"login_failure"]:
# 如果是特殊状态说明通知已经在PddLogin中发送了不需要重复发送 # 如果是特殊状态说明通知已经在PddLogin中发送了不需要重复发送
try: try:
message = { message = {
@@ -443,7 +498,8 @@ class WebSocketManager:
} }
self.backend_client.send_message(message) self.backend_client.send_message(message)
status_text = "成功" if result else "失败" status_text = "成功" if result else "失败"
self._log(f"上报拼多多平台连接状态{status_text}: {message}", "SUCCESS" if result else "ERROR") self._log(f"上报拼多多平台连接状态{status_text}: {message}",
"SUCCESS" if result else "ERROR")
except Exception as send_e: except Exception as send_e:
self._log(f"上报拼多多平台连接状态失败: {send_e}", "ERROR") self._log(f"上报拼多多平台连接状态失败: {send_e}", "ERROR")
elif result == "need_verification_code": elif result == "need_verification_code":

View File

@@ -9,13 +9,13 @@ import json # 用于将令牌保存为 JSON 格式
# 后端服务器配置 # 后端服务器配置
# BACKEND_HOST = "192.168.5.233" # BACKEND_HOST = "192.168.5.233"
BACKEND_HOST = "192.168.5.12" # BACKEND_HOST = "192.168.5.12"
# BACKEND_HOST = "shuidrop.com" BACKEND_HOST = "shuidrop.com"
# BACKEND_HOST = "test.shuidrop.com" # BACKEND_HOST = "test.shuidrop.com"
BACKEND_PORT = "8000" # BACKEND_PORT = "8000"
# BACKEND_PORT = "" BACKEND_PORT = ""
BACKEND_WS_URL = f"ws://{BACKEND_HOST}:{BACKEND_PORT}" # BACKEND_WS_URL = f"ws://{BACKEND_HOST}:{BACKEND_PORT}"
# BACKEND_WS_URL = f"wss://{BACKEND_HOST}" BACKEND_WS_URL = f"wss://{BACKEND_HOST}"
# WebSocket配置 # WebSocket配置
WS_CONNECT_TIMEOUT = 16.0 WS_CONNECT_TIMEOUT = 16.0

41
main.py
View File

@@ -43,6 +43,9 @@ class LoginWindow(QMainWindow):
self.initUI() self.initUI()
# 延迟设置版本检查器确保WebSocket连接已建立
QTimer.singleShot(5000, self.setup_version_checker)
def initUI(self): def initUI(self):
# 设置窗口基本属性 # 设置窗口基本属性
self.setWindowTitle('AI客服智能助手') self.setWindowTitle('AI客服智能助手')
@@ -699,6 +702,44 @@ class LoginWindow(QMainWindow):
self.quit_application() self.quit_application()
event.accept() event.accept()
def setup_version_checker(self):
"""设置版本检查器的GUI回调"""
try:
from WebSocket.backend_singleton import get_websocket_manager
ws_manager = get_websocket_manager()
if ws_manager:
ws_manager.gui_update_callback = self.show_update_notification
self.add_log("✅ 版本检查器GUI回调已设置", "SUCCESS")
else:
self.add_log("⚠️ WebSocket管理器未初始化", "WARNING")
except Exception as e:
self.add_log(f"❌ 设置版本检查器失败: {e}", "ERROR")
def show_update_notification(self, latest_version, download_url):
"""显示版本更新通知"""
try:
reply = QMessageBox.question(
self,
"版本更新",
f"发现新版本 {latest_version},是否立即更新?\n\n点击确定将打开下载页面。",
QMessageBox.Yes | QMessageBox.No,
QMessageBox.Yes
)
if reply == QMessageBox.Yes:
self.trigger_update(download_url)
except Exception as e:
self.add_log(f"❌ 显示更新通知失败: {e}", "ERROR")
def trigger_update(self, download_url):
"""触发更新下载"""
import webbrowser
try:
webbrowser.open(download_url)
self.add_log("✅ 已打开更新下载页面", "SUCCESS")
except Exception as e:
self.add_log(f"❌ 打开下载页面失败: {e}", "ERROR")
def main(): def main():
"""主函数,用于测试界面""" """主函数,用于测试界面"""

Binary file not shown.

After

Width:  |  Height:  |  Size: 801 KiB