type: 收窄虎牙 HTTP 与 WSS 客户端类型

This commit is contained in:
yml2213
2026-08-30 20:04:08 +08:00
parent ec3c14d0a3
commit 6439dc1942
2 changed files with 445 additions and 201 deletions
+235 -89
View File
@@ -6,6 +6,7 @@
Body: Wup 包 (含4字节长度前缀) Body: Wup 包 (含4字节长度前缀)
Response: Wup 包 (tRsp) Response: Wup 包 (tRsp)
""" """
import base64 import base64
import hashlib import hashlib
import json import json
@@ -13,7 +14,7 @@ import random
import struct import struct
import urllib.parse import urllib.parse
import urllib.request import urllib.request
from typing import Optional, Callable from typing import Any, Optional, Callable
from .cookie_utils import cookie_pairs, normalize_cookie_pairs, normalize_huya_cookie from .cookie_utils import cookie_pairs, normalize_cookie_pairs, normalize_huya_cookie
from .taf_protocol import TafOutputStream, TafInputStream, TafType, TafStruct from .taf_protocol import TafOutputStream, TafInputStream, TafType, TafStruct
@@ -21,9 +22,11 @@ from .wup_protocol import WupRequest, WupResponse
CDNWS_HOST = "cdnws.api.huya.com" CDNWS_HOST = "cdnws.api.huya.com"
LIVELINK_BIND_API_BASE = "https://livelinkbind.game.qq.com" LIVELINK_BIND_API_BASE = "https://livelinkbind.game.qq.com"
PC_UA = ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) " PC_UA = (
"AppleWebKit/537.36 (KHTML, like Gecko) " "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"Chrome/149.0.0.0 Safari/537.36") "AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/149.0.0.0 Safari/537.36"
)
HTTP_HUYA_UA = "webh5&0.0.1&websocket&&diypc_52775" HTTP_HUYA_UA = "webh5&0.0.1&websocket&&diypc_52775"
@@ -42,6 +45,7 @@ class WSConnectParaInfo(TafStruct):
tag9 sTraceId string "hex8:hex8:0:0" tag9 sTraceId string "hex8:hex8:0:0"
tag10 mCustomHeaders Map<string,string> tag10 mCustomHeaders Map<string,string>
""" """
def __init__(self): def __init__(self):
self.lUid: int = 0 self.lUid: int = 0
self.sGuid: str = "" self.sGuid: str = ""
@@ -88,12 +92,13 @@ class WSConnectParaInfo(TafStruct):
def _gen_trace_id() -> str: def _gen_trace_id() -> str:
"""生成 sTraceId (格式 hex8:hex8:0:0HAR 实证)""" """生成 sTraceId (格式 hex8:hex8:0:0HAR 实证)"""
h = '%016x' % random.getrandbits(64) h = "%016x" % random.getrandbits(64)
return f"{h}:{h}:0:0" return f"{h}:{h}:0:0"
def generate_http_baseinfo(uid: int, guid: str, cookie: str, def generate_http_baseinfo(
trace_id: str = None) -> str: uid: int, guid: str, cookie: str, trace_id: str | None = None
) -> str:
""" """
构造 HTTP POST 的 baseinfo URL 参数 构造 HTTP POST 的 baseinfo URL 参数
@@ -111,14 +116,14 @@ def generate_http_baseinfo(uid: int, guid: str, cookie: str,
info.write_to(os) info.write_to(os)
raw = os.get_bytes() raw = os.get_bytes()
# base64 (与 JS window.btoa 一致) # base64 (与 JS window.btoa 一致)
b64 = base64.b64encode(raw).decode('ascii') b64 = base64.b64encode(raw).decode("ascii")
return urllib.parse.quote(b64, safe='') return urllib.parse.quote(b64, safe="")
class HuyaHttpClient: class HuyaHttpClient:
"""虎牙 HTTP POST RPC 客户端""" """虎牙 HTTP POST RPC 客户端"""
def __init__(self, logger: Callable[[str], None] = None): def __init__(self, logger: Callable[[str], None] | None = None):
self.logger = logger or print self.logger = logger or print
@staticmethod @staticmethod
@@ -155,7 +160,11 @@ class HuyaHttpClient:
"""HTTP 业务 UserId.sCookie 需要带 huya_ua 前缀,避免重复写入。""" """HTTP 业务 UserId.sCookie 需要带 huya_ua 前缀,避免重复写入。"""
if not cookie: if not cookie:
return "" return ""
pairs = [(key, value) for key, value in cookie_pairs(normalize_huya_cookie(cookie)) if key != "huya_ua"] pairs = [
(key, value)
for key, value in cookie_pairs(normalize_huya_cookie(cookie))
if key != "huya_ua"
]
if not any(key == "guid" for key, _ in pairs): if not any(key == "guid" for key, _ in pairs):
guid = cls._resolve_cookie_guid(cookie) guid = cls._resolve_cookie_guid(cookie)
if guid: if guid:
@@ -168,7 +177,7 @@ class HuyaHttpClient:
uid: int, uid: int,
guid: str, guid: str,
cookie: str, cookie: str,
trace_id: str = None, trace_id: str | None = None,
) -> str: ) -> str:
"""生成与浏览器 cdnws 调用一致的 baseinfo。""" """生成与浏览器 cdnws 调用一致的 baseinfo。"""
rpc_cookie = cls._normalize_cookie(cookie) rpc_cookie = cls._normalize_cookie(cookie)
@@ -179,6 +188,7 @@ class HuyaHttpClient:
def _build_user(uid: int, guid: str, cookie: str): def _build_user(uid: int, guid: str, cookie: str):
"""构造 HTTP 业务 UserId。""" """构造 HTTP 业务 UserId。"""
from .shop_structs import UserId from .shop_structs import UserId
user = UserId() user = UserId()
user.lUid = uid user.lUid = uid
user.sGuid = guid or HuyaHttpClient._resolve_cookie_guid(cookie) user.sGuid = guid or HuyaHttpClient._resolve_cookie_guid(cookie)
@@ -194,6 +204,7 @@ class HuyaHttpClient:
def _build_shop_app(source_id: str = "yellowcarlist", scene: int = 7): def _build_shop_app(source_id: str = "yellowcarlist", scene: int = 7):
"""构造 HTTP 业务 ShopAppInfo。""" """构造 HTTP 业务 ShopAppInfo。"""
from .shop_structs import ShopAppInfo from .shop_structs import ShopAppInfo
info = ShopAppInfo() info = ShopAppInfo()
info.sAppId = "huya" info.sAppId = "huya"
info.sBizType = "" info.sBizType = ""
@@ -205,6 +216,7 @@ class HuyaHttpClient:
def _build_order_env(item_count: int = 1): def _build_order_env(item_count: int = 1):
"""构造下单/支付环境参数。""" """构造下单/支付环境参数。"""
import time import time
click_time = int(time.time() * 1000) click_time = int(time.time() * 1000)
return { return {
"user_click_seq": f'[{{"x":258,"y":487,"time":{click_time},"id":""}}]', "user_click_seq": f'[{{"x":258,"y":487,"time":{click_time},"id":""}}]',
@@ -215,16 +227,24 @@ class HuyaHttpClient:
def _build_activity_user(uid: int, cookie: str): def _build_activity_user(uid: int, cookie: str):
"""构造活动组件 UserId,与当前积分兑换组件保持一致。""" """构造活动组件 UserId,与当前积分兑换组件保持一致。"""
from .activity_structs import ActivityUserId from .activity_structs import ActivityUserId
user = ActivityUserId() user = ActivityUserId()
user.lUid = uid user.lUid = uid
user.sHuYaUA = HTTP_HUYA_UA user.sHuYaUA = HTTP_HUYA_UA
user.sCookie = HuyaHttpClient._normalize_cookie(cookie) user.sCookie = HuyaHttpClient._normalize_cookie(cookie)
return user return user
def call_rpc(self, service: str, method: str, def call_rpc(
req_struct: TafStruct, rsp_class=None, self,
uid: int = 0, guid: str = "", cookie: str = "", service: str,
timeout: float = 15.0): method: str,
req_struct: TafStruct,
rsp_class=None,
uid: int = 0,
guid: str = "",
cookie: str = "",
timeout: float = 15.0,
):
""" """
HTTP POST RPC 调用 HTTP POST RPC 调用
@@ -249,28 +269,38 @@ class HuyaHttpClient:
self.logger(f"[HTTP] POST {service}.{method}") self.logger(f"[HTTP] POST {service}.{method}")
self.logger(f"[HTTP] URL: https://{CDNWS_HOST}/?baseinfo=<redacted>") self.logger(f"[HTTP] URL: https://{CDNWS_HOST}/?baseinfo=<redacted>")
self.logger(f"[HTTP] 发送 body: {len(wup_data)} 字节, hex前60: {wup_data[:60].hex()}") self.logger(
f"[HTTP] 发送 body: {len(wup_data)} 字节, hex前60: {wup_data[:60].hex()}"
)
# 3. POST # 3. POST
req = urllib.request.Request(url, data=wup_data, method='POST', headers={ req = urllib.request.Request(
'User-Agent': PC_UA, url,
'Origin': 'https://zt.huya.com', data=wup_data,
'Referer': 'https://zt.huya.com/', method="POST",
'Content-Type': 'application/octet-stream', headers={
'Accept-Language': 'zh-CN,zh;q=0.9', "User-Agent": PC_UA,
}) "Origin": "https://zt.huya.com",
"Referer": "https://zt.huya.com/",
"Content-Type": "application/octet-stream",
"Accept-Language": "zh-CN,zh;q=0.9",
},
)
try: try:
with urllib.request.urlopen(req, timeout=timeout) as resp: with urllib.request.urlopen(req, timeout=timeout) as resp:
resp_data = resp.read() resp_data = resp.read()
# 检测 gzip 压缩 (响应 hex 1f8b 开头) # 检测 gzip 压缩 (响应 hex 1f8b 开头)
if resp_data[:2] == b'\x1f\x8b': if resp_data[:2] == b"\x1f\x8b":
import gzip import gzip
resp_data = gzip.decompress(resp_data) resp_data = gzip.decompress(resp_data)
except Exception as e: except Exception as e:
self.logger(f"[HTTP] ❌ 请求失败: {type(e).__name__}: {e}") self.logger(f"[HTTP] ❌ 请求失败: {type(e).__name__}: {e}")
return None return None
self.logger(f"[HTTP] 收到响应: {len(resp_data)} 字节, hex前60: {resp_data[:60].hex()}") self.logger(
f"[HTTP] 收到响应: {len(resp_data)} 字节, hex前60: {resp_data[:60].hex()}"
)
# 4. 解析响应 Wup # 4. 解析响应 Wup
wup_resp = WupResponse() wup_resp = WupResponse()
@@ -289,7 +319,12 @@ class HuyaHttpClient:
if result is None: if result is None:
result = wup_resp.readStruct("tResp", rsp_class) result = wup_resp.readStruct("tResp", rsp_class)
if result is not None and hasattr(result, "to_dict"): if result is not None and hasattr(result, "to_dict"):
log_data = result.to_log_dict() if hasattr(result, "to_log_dict") else result.to_dict() decoded_result: Any = result
log_data = (
decoded_result.to_log_dict()
if hasattr(decoded_result, "to_log_dict")
else decoded_result.to_dict()
)
decoded = json.dumps(log_data, ensure_ascii=False, separators=(",", ":")) decoded = json.dumps(log_data, ensure_ascii=False, separators=(",", ":"))
self.logger(f"[HTTP] 解码 tRsp: {decoded}") self.logger(f"[HTTP] 解码 tRsp: {decoded}")
return result return result
@@ -299,6 +334,7 @@ class HuyaHttpClient:
def query_user_score(self, uid: int, cookie: str, sid: int, timeout: float = 15.0): def query_user_score(self, uid: int, cookie: str, sid: int, timeout: float = 15.0):
"""查询活动可用积分。""" """查询活动可用积分。"""
from .activity_structs import GetUserScoreReq, GetUserScoreResp from .activity_structs import GetUserScoreReq, GetUserScoreResp
req = GetUserScoreReq() req = GetUserScoreReq()
req.userId = self._build_activity_user(uid, cookie) req.userId = self._build_activity_user(uid, cookie)
req.sid = sid req.sid = sid
@@ -312,9 +348,12 @@ class HuyaHttpClient:
timeout=timeout, timeout=timeout,
) )
def get_act_prize_list(self, uid: int, cookie: str, sid: int, timeout: float = 15.0): def get_act_prize_list(
self, uid: int, cookie: str, sid: int, timeout: float = 15.0
):
"""查询活动可兑换商品列表。""" """查询活动可兑换商品列表。"""
from .activity_structs import GetActPrizeListReq, GetActPrizeListResp from .activity_structs import GetActPrizeListReq, GetActPrizeListResp
req = GetActPrizeListReq() req = GetActPrizeListReq()
req.userId = self._build_activity_user(uid, cookie) req.userId = self._build_activity_user(uid, cookie)
req.sid = sid req.sid = sid
@@ -328,9 +367,12 @@ class HuyaHttpClient:
timeout=timeout, timeout=timeout,
) )
def get_user_prize_records(self, uid: int, cookie: str, sid: int, timeout: float = 15.0): def get_user_prize_records(
self, uid: int, cookie: str, sid: int, timeout: float = 15.0
):
"""查询用户兑换记录。""" """查询用户兑换记录。"""
from .activity_structs import GetUserPrizeRecordsReq, GetUserPrizeRecordsResp from .activity_structs import GetUserPrizeRecordsReq, GetUserPrizeRecordsResp
req = GetUserPrizeRecordsReq() req = GetUserPrizeRecordsReq()
req.userId = self._build_activity_user(uid, cookie) req.userId = self._build_activity_user(uid, cookie)
req.sid = sid req.sid = sid
@@ -344,9 +386,12 @@ class HuyaHttpClient:
timeout=timeout, timeout=timeout,
) )
def score_exchange_prize(self, uid: int, cookie: str, sid: int, pid: int, timeout: float = 15.0): def score_exchange_prize(
self, uid: int, cookie: str, sid: int, pid: int, timeout: float = 15.0
):
"""兑换活动积分商品。""" """兑换活动积分商品。"""
from .activity_structs import ScoreExchangePrizeReq, ScoreExchangePrizeResp from .activity_structs import ScoreExchangePrizeReq, ScoreExchangePrizeResp
req = ScoreExchangePrizeReq() req = ScoreExchangePrizeReq()
req.userId = self._build_activity_user(uid, cookie) req.userId = self._build_activity_user(uid, cookie)
req.sid = int(sid or 0) req.sid = int(sid or 0)
@@ -361,9 +406,12 @@ class HuyaHttpClient:
timeout=timeout, timeout=timeout,
) )
def get_act_task_detail(self, uid: int, cookie: str, act_id: int, timeout: float = 15.0): def get_act_task_detail(
self, uid: int, cookie: str, act_id: int, timeout: float = 15.0
):
"""查询活动任务详情,用于发现充值商品 SPU。""" """查询活动任务详情,用于发现充值商品 SPU。"""
from .activity_structs import GetActTaskDetailReq, GetActTaskDetailResp from .activity_structs import GetActTaskDetailReq, GetActTaskDetailResp
req = GetActTaskDetailReq() req = GetActTaskDetailReq()
req.userId = self._build_activity_user(uid, cookie) req.userId = self._build_activity_user(uid, cookie)
req.actId = int(act_id or 0) req.actId = int(act_id or 0)
@@ -389,7 +437,11 @@ class HuyaHttpClient:
timeout: float = 15.0, timeout: float = 15.0,
): ):
"""查询用户游戏账号绑定状态。""" """查询用户游戏账号绑定状态。"""
from .activity_structs import CheckUserBindGameAccountReq, CheckUserBindGameAccountResp from .activity_structs import (
CheckUserBindGameAccountReq,
CheckUserBindGameAccountResp,
)
req = CheckUserBindGameAccountReq() req = CheckUserBindGameAccountReq()
req.userId = self._build_activity_user(uid, cookie) req.userId = self._build_activity_user(uid, cookie)
req.gid = int(gid or 0) req.gid = int(gid or 0)
@@ -418,7 +470,11 @@ class HuyaHttpClient:
timeout: float = 15.0, timeout: float = 15.0,
): ):
"""确认虎牙号与当前活动的游戏账号绑定关系。""" """确认虎牙号与当前活动的游戏账号绑定关系。"""
from .activity_structs import ConfirmBindActAccountReq, ConfirmBindActAccountResp from .activity_structs import (
ConfirmBindActAccountReq,
ConfirmBindActAccountResp,
)
req = ConfirmBindActAccountReq() req = ConfirmBindActAccountReq()
req.userId = self._build_activity_user(uid, cookie) req.userId = self._build_activity_user(uid, cookie)
req.gid = int(gid or 0) req.gid = int(gid or 0)
@@ -446,6 +502,7 @@ class HuyaHttpClient:
): ):
"""获取绑定二维码拉起参数。""" """获取绑定二维码拉起参数。"""
from .activity_structs import GetLiveLinkParamReq, GetLiveLinkParamResp from .activity_structs import GetLiveLinkParamReq, GetLiveLinkParamResp
req = GetLiveLinkParamReq() req = GetLiveLinkParamReq()
req.userId = self._build_activity_user(uid, cookie) req.userId = self._build_activity_user(uid, cookie)
req.gid = int(gid or 0) req.gid = int(gid or 0)
@@ -463,9 +520,12 @@ class HuyaHttpClient:
timeout=timeout, timeout=timeout,
) )
def get_user_profile_batch(self, uid: int, cookie: str, target_uids: list[int], timeout: float = 15.0): def get_user_profile_batch(
self, uid: int, cookie: str, target_uids: list[int], timeout: float = 15.0
):
"""批量查询虎牙用户资料,用于补齐绑定页头像和昵称。""" """批量查询虎牙用户资料,用于补齐绑定页头像和昵称。"""
from .activity_structs import GetUserProfileBatchReq, GetUserProfileBatchResp from .activity_structs import GetUserProfileBatchReq, GetUserProfileBatchResp
req = GetUserProfileBatchReq() req = GetUserProfileBatchReq()
req.userId = self._build_activity_user(0, "") req.userId = self._build_activity_user(0, "")
req.uidList = [int(item) for item in target_uids if int(item or 0)] req.uidList = [int(item) for item in target_uids if int(item or 0)]
@@ -493,7 +553,9 @@ class HuyaHttpClient:
mime = "image/png" if text.startswith("iVBOR") else "image/jpeg" mime = "image/png" if text.startswith("iVBOR") else "image/jpeg"
return f"data:{mime};base64,{text}" return f"data:{mime};base64,{text}"
def get_livelink_mini_qrcode(self, bind_page_url: str, timeout: float = 15.0) -> dict | None: def get_livelink_mini_qrcode(
self, bind_page_url: str, timeout: float = 15.0
) -> dict | None:
"""请求绑定页内部接口,获取真正可扫的小程序码图片。""" """请求绑定页内部接口,获取真正可扫的小程序码图片。"""
import gzip import gzip
import time import time
@@ -522,24 +584,32 @@ class HuyaHttpClient:
self.logger( self.logger(
f"[LIVELINK] POST getMiniAppQrcode plat={live_plat_id or '-'} actId={act_id or '-'}" f"[LIVELINK] POST getMiniAppQrcode plat={live_plat_id or '-'} actId={act_id or '-'}"
) )
req = urllib.request.Request(endpoint, data=payload, method="POST", headers={ req = urllib.request.Request(
"User-Agent": PC_UA, endpoint,
"Origin": "https://livelink.qq.com", data=payload,
"Referer": "https://livelink.qq.com/", method="POST",
"Content-Type": "application/json;charset=UTF-8", headers={
"Accept": "application/json, text/plain, */*", "User-Agent": PC_UA,
"Accept-Language": "zh-CN,zh;q=0.9", "Origin": "https://livelink.qq.com",
"Gap-Plat-Id": live_plat_id, "Referer": "https://livelink.qq.com/",
"Gap-Act-Id": act_id, "Content-Type": "application/json;charset=UTF-8",
"Gap-User-From": "pc", "Accept": "application/json, text/plain, */*",
"Gap-From-Id": from_id, "Accept-Language": "zh-CN,zh;q=0.9",
"Vulcan-AccType": "auth_in_h5", "Gap-Plat-Id": live_plat_id,
"Vulcan-PlatUser-Session": "", "Gap-Act-Id": act_id,
}) "Gap-User-From": "pc",
"Gap-From-Id": from_id,
"Vulcan-AccType": "auth_in_h5",
"Vulcan-PlatUser-Session": "",
},
)
try: try:
with urllib.request.urlopen(req, timeout=timeout) as resp: with urllib.request.urlopen(req, timeout=timeout) as resp:
resp_data = resp.read() resp_data = resp.read()
if resp_data[:2] == b"\x1f\x8b" or resp.headers.get("Content-Encoding") == "gzip": if (
resp_data[:2] == b"\x1f\x8b"
or resp.headers.get("Content-Encoding") == "gzip"
):
resp_data = gzip.decompress(resp_data) resp_data = gzip.decompress(resp_data)
except Exception as e: except Exception as e:
self.logger(f"[LIVELINK] ❌ 小程序码请求失败: {type(e).__name__}: {e}") self.logger(f"[LIVELINK] ❌ 小程序码请求失败: {type(e).__name__}: {e}")
@@ -552,7 +622,9 @@ class HuyaHttpClient:
return None return None
if data.get("iRet") != 0: if data.get("iRet") != 0:
self.logger(f"[LIVELINK] ❌ 小程序码接口失败: iRet={data.get('iRet')} msg={data.get('sMsg') or '-'}") self.logger(
f"[LIVELINK] ❌ 小程序码接口失败: iRet={data.get('iRet')} msg={data.get('sMsg') or '-'}"
)
return None return None
jdata = data.get("jData") if isinstance(data.get("jData"), dict) else {} jdata = data.get("jData") if isinstance(data.get("jData"), dict) else {}
@@ -562,13 +634,17 @@ class HuyaHttpClient:
self.logger("[LIVELINK] ❌ 小程序码接口未返回 qrcode") self.logger("[LIVELINK] ❌ 小程序码接口未返回 qrcode")
return None return None
self.logger(f"[LIVELINK] 小程序码获取成功: image={len(image_src)} 字符 token={'' if jdata.get('qrcodeToken') else ''}") self.logger(
f"[LIVELINK] 小程序码获取成功: image={len(image_src)} 字符 token={'' if jdata.get('qrcodeToken') else ''}"
)
return { return {
"mini_qrcode_image": image_src, "mini_qrcode_image": image_src,
"qrcode_token": jdata.get("qrcodeToken") or "", "qrcode_token": jdata.get("qrcodeToken") or "",
} }
def get_livelink_qrcode_status(self, qrcode_token: str, timeout: float = 15.0) -> dict | None: def get_livelink_qrcode_status(
self, qrcode_token: str, timeout: float = 15.0
) -> dict | None:
"""轮询 livelink 小程序码扫码状态。""" """轮询 livelink 小程序码扫码状态。"""
import gzip import gzip
@@ -582,18 +658,26 @@ class HuyaHttpClient:
separators=(",", ":"), separators=(",", ":"),
).encode("utf-8") ).encode("utf-8")
endpoint = f"{LIVELINK_BIND_API_BASE}/api/qrcode/poolingQrcodeStatus" endpoint = f"{LIVELINK_BIND_API_BASE}/api/qrcode/poolingQrcodeStatus"
req = urllib.request.Request(endpoint, data=payload, method="POST", headers={ req = urllib.request.Request(
"User-Agent": PC_UA, endpoint,
"Origin": "https://livelink.qq.com", data=payload,
"Referer": "https://livelink.qq.com/", method="POST",
"Content-Type": "application/json;charset=UTF-8", headers={
"Accept": "application/json, text/plain, */*", "User-Agent": PC_UA,
"Accept-Language": "zh-CN,zh;q=0.9", "Origin": "https://livelink.qq.com",
}) "Referer": "https://livelink.qq.com/",
"Content-Type": "application/json;charset=UTF-8",
"Accept": "application/json, text/plain, */*",
"Accept-Language": "zh-CN,zh;q=0.9",
},
)
try: try:
with urllib.request.urlopen(req, timeout=timeout) as resp: with urllib.request.urlopen(req, timeout=timeout) as resp:
resp_data = resp.read() resp_data = resp.read()
if resp_data[:2] == b"\x1f\x8b" or resp.headers.get("Content-Encoding") == "gzip": if (
resp_data[:2] == b"\x1f\x8b"
or resp.headers.get("Content-Encoding") == "gzip"
):
resp_data = gzip.decompress(resp_data) resp_data = gzip.decompress(resp_data)
except Exception as e: except Exception as e:
self.logger(f"[LIVELINK] ❌ 二维码状态请求失败: {type(e).__name__}: {e}") self.logger(f"[LIVELINK] ❌ 二维码状态请求失败: {type(e).__name__}: {e}")
@@ -602,11 +686,15 @@ class HuyaHttpClient:
try: try:
data = json.loads(resp_data.decode("utf-8", "replace")) data = json.loads(resp_data.decode("utf-8", "replace"))
except Exception as e: except Exception as e:
self.logger(f"[LIVELINK] ❌ 二维码状态响应解析失败: {type(e).__name__}: {e}") self.logger(
f"[LIVELINK] ❌ 二维码状态响应解析失败: {type(e).__name__}: {e}"
)
return None return None
if data.get("iRet") != 0: if data.get("iRet") != 0:
self.logger(f"[LIVELINK] ❌ 二维码状态接口失败: iRet={data.get('iRet')} msg={data.get('sMsg') or '-'}") self.logger(
f"[LIVELINK] ❌ 二维码状态接口失败: iRet={data.get('iRet')} msg={data.get('sMsg') or '-'}"
)
return None return None
jdata = data.get("jData") if isinstance(data.get("jData"), dict) else {} jdata = data.get("jData") if isinstance(data.get("jData"), dict) else {}
@@ -675,9 +763,20 @@ class HuyaHttpClient:
"exchange_qq_coin_url": f"{huya_base}?{urlencode(exchange_params)}", "exchange_qq_coin_url": f"{huya_base}?{urlencode(exchange_params)}",
} }
def get_goods_info(self, uid, guid, cookie, pid, spu_id, sku_id=0, game_id="", def get_goods_info(
source_id="yellowcarlist", scene=7): self,
uid,
guid,
cookie,
pid,
spu_id,
sku_id=0,
game_id="",
source_id="yellowcarlist",
scene=7,
):
from .shop_structs import GetGoodsInfoReqV5, GoodsInfoRsp from .shop_structs import GetGoodsInfoReqV5, GoodsInfoRsp
req = GetGoodsInfoReqV5() req = GetGoodsInfoReqV5()
req.userId = self._build_user(uid, guid, cookie) req.userId = self._build_user(uid, guid, cookie)
req.shopAppInfo = self._build_shop_app(source_id, scene) req.shopAppInfo = self._build_shop_app(source_id, scene)
@@ -685,11 +784,21 @@ class HuyaHttpClient:
req.spuId = spu_id req.spuId = spu_id
req.skuId = sku_id req.skuId = sku_id
req.gameId = game_id req.gameId = game_id
return self.call_rpc("shopMiddleUI", "getGoodsInfoV5", req, return self.call_rpc(
GoodsInfoRsp, uid, guid, cookie) "shopMiddleUI", "getGoodsInfoV5", req, GoodsInfoRsp, uid, guid, cookie
)
def query_user_order_list(self, uid, guid, cookie, offset=0, page_size=10, def query_user_order_list(
order_type=1, status=0, timeout=15.0): self,
uid,
guid,
cookie,
offset=0,
page_size=10,
order_type=1,
status=0,
timeout=15.0,
):
"""查询用户订单列表,用于支付二维码后的付款状态轮询。""" """查询用户订单列表,用于支付二维码后的付款状态轮询。"""
from .shop_structs import QueryUserOrderListReq, QueryUserOrderListRsp from .shop_structs import QueryUserOrderListReq, QueryUserOrderListRsp
@@ -724,13 +833,28 @@ class HuyaHttpClient:
) )
return fallback if fallback is not None else result return fallback if fallback is not None else result
def create_order(self, uid, guid, cookie, pid, spu_id, sku_id, def create_order(
item_count=1, source_id="yellowcarlist", game_id="", scene=7, self,
order_type=None): uid,
guid,
cookie,
pid,
spu_id,
sku_id,
item_count=1,
source_id="yellowcarlist",
game_id="",
scene=7,
order_type=None,
):
"""order_type=None 时自动尝试从 1 到 10 找到有效值""" """order_type=None 时自动尝试从 1 到 10 找到有效值"""
from .shop_structs import (CreateOrderReqV5, CreateOrderRsp, from .shop_structs import (
CreateOrderExtraParam, CreateOrderPromotionParam, CreateOrderReqV5,
CreateOrderAccountParam) CreateOrderRsp,
CreateOrderExtraParam,
CreateOrderPromotionParam,
CreateOrderAccountParam,
)
# 如果指定了具体值,直接试 # 如果指定了具体值,直接试
types_to_try = [order_type] if order_type is not None else list(range(1, 11)) types_to_try = [order_type] if order_type is not None else list(range(1, 11))
@@ -759,10 +883,13 @@ class HuyaHttpClient:
req.gameCategoryId = 507 req.gameCategoryId = 507
req.ext = "" req.ext = ""
result = self.call_rpc("shopMiddleUI", "createOrderV5", req, result: Any = self.call_rpc(
CreateOrderRsp, uid, guid, cookie) "shopMiddleUI", "createOrderV5", req, CreateOrderRsp, uid, guid, cookie
)
if result is not None: if result is not None:
self.logger(f" orderType={ot}: code={result.code} orderId={result.orderId} msg={result.message}") self.logger(
f" orderType={ot}: code={result.code} orderId={result.orderId} msg={result.message}"
)
if result.code == 200 and result.orderId: if result.code == 200 and result.orderId:
self.logger(f"[✓] 找到有效 orderType={ot}") self.logger(f"[✓] 找到有效 orderType={ot}")
return result return result
@@ -781,11 +908,21 @@ class HuyaHttpClient:
return "Weixin" return "Weixin"
return text return text
def pay_order_submit(self, uid, guid, cookie, order_id, pay_type=1, def pay_order_submit(
pid=0, source_id="yellowcarlist", scene=7, self,
item_count=1): uid,
guid,
cookie,
order_id,
pay_type=1,
pid=0,
source_id="yellowcarlist",
scene=7,
item_count=1,
):
"""发起支付,结构与 WSS HAR 中 payOrderSubmitV5 对齐。""" """发起支付,结构与 WSS HAR 中 payOrderSubmitV5 对齐。"""
from .shop_structs import PayOrderRes from .shop_structs import PayOrderRes
os = TafOutputStream() os = TafOutputStream()
os.write_struct_begin(0) os.write_struct_begin(0)
@@ -817,22 +954,31 @@ class HuyaHttpClient:
self.logger(f"[HTTP] POST shopMiddleUI.payOrderSubmitV5") self.logger(f"[HTTP] POST shopMiddleUI.payOrderSubmitV5")
self.logger(f"[HTTP] 发送 hex前60: {wup_data[:60].hex()}") self.logger(f"[HTTP] 发送 hex前60: {wup_data[:60].hex()}")
req = urllib.request.Request(url, data=wup_data, method='POST', headers={ req = urllib.request.Request(
'User-Agent': PC_UA, 'Origin': 'https://zt.huya.com', url,
'Referer': 'https://zt.huya.com/', data=wup_data,
'Content-Type': 'application/octet-stream', method="POST",
}) headers={
"User-Agent": PC_UA,
"Origin": "https://zt.huya.com",
"Referer": "https://zt.huya.com/",
"Content-Type": "application/octet-stream",
},
)
try: try:
with urllib.request.urlopen(req, timeout=15) as resp: with urllib.request.urlopen(req, timeout=15) as resp:
resp_data = resp.read() resp_data = resp.read()
if resp_data[:2] == b'\x1f\x8b': if resp_data[:2] == b"\x1f\x8b":
import gzip import gzip
resp_data = gzip.decompress(resp_data) resp_data = gzip.decompress(resp_data)
except Exception as e: except Exception as e:
self.logger(f"[HTTP] ❌ payOrderSubmitV5 失败: {e}") self.logger(f"[HTTP] ❌ payOrderSubmitV5 失败: {e}")
return None return None
self.logger(f"[HTTP] payOrderSubmitV5 响应 {len(resp_data)} 字节, hex前60: {resp_data[:60].hex()}") self.logger(
f"[HTTP] payOrderSubmitV5 响应 {len(resp_data)} 字节, hex前60: {resp_data[:60].hex()}"
)
wup_resp = WupResponse() wup_resp = WupResponse()
wup_resp.decode(resp_data) wup_resp.decode(resp_data)
if wup_resp.newdata: if wup_resp.newdata:
+210 -112
View File
@@ -14,13 +14,14 @@
- 服务端响应 seq ≠ 请求 seq → 必须用 FIFO 匹配 - 服务端响应 seq ≠ 请求 seq → 必须用 FIFO 匹配
- 初始化 wsLaunch iRequestId=-1,业务 requestId 从 8 开始递增 - 初始化 wsLaunch iRequestId=-1,业务 requestId 从 8 开始递增
""" """
import asyncio import asyncio
import random import random
import re import re
import struct import struct
import time import time
from collections import deque from collections import deque
from typing import Optional, Callable from typing import Any, Optional, Callable, cast
import websockets import websockets
@@ -41,19 +42,19 @@ WSS_COOKIE_UA = "webh5&0.0.1&websocket&&diypc_52775"
WSS_SHOP_SCENE = 4 WSS_SHOP_SCENE = 4
# seq 按消息类型固定 (HAR 实证,跨两次抓包一致) # seq 按消息类型固定 (HAR 实证,跨两次抓包一致)
SEQ_HEARTBEAT = 0x1d00000d SEQ_HEARTBEAT = 0x1D00000D
SEQ_WSLAUNCH = 0x1d000078 SEQ_WSLAUNCH = 0x1D000078
SEQ_GETCONFIG = 0x1d000076 SEQ_GETCONFIG = 0x1D000076
SEQ_REGISTER = 0x1d00003c SEQ_REGISTER = 0x1D00003C
SEQ_CONFIRM = 0x1d000002 SEQ_CONFIRM = 0x1D000002
SEQ_BUSINESS = 0x1d000106 # 业务 RPC (HAR 7.4 实证:业务帧固定使用该 seq) SEQ_BUSINESS = 0x1D000106 # 业务 RPC (HAR 7.4 实证:业务帧固定使用该 seq)
class WssCommand: class WssCommand:
RPC_REQUEST = 0x03 RPC_REQUEST = 0x03
RPC_RESPONSE = 0x04 RPC_RESPONSE = 0x04
AUTH = 0x0a AUTH = 0x0A
SERVER_PUSH1 = 0x0b SERVER_PUSH1 = 0x0B
HEARTBEAT_SEND = 0x10 HEARTBEAT_SEND = 0x10
HEARTBEAT_RECV = 0x11 HEARTBEAT_RECV = 0x11
CONFIRM_SEND = 0x17 CONFIRM_SEND = 0x17
@@ -68,15 +69,15 @@ class WssMessage:
self.body = body self.body = body
def encode(self) -> bytes: def encode(self) -> bytes:
header = struct.pack('>BBI', 0x00, self.command, self.sequence) header = struct.pack(">BBI", 0x00, self.command, self.sequence)
return header + self.body return header + self.body
@classmethod @classmethod
def decode(cls, data: bytes) -> 'WssMessage': def decode(cls, data: bytes) -> "WssMessage":
if len(data) < 6: if len(data) < 6:
raise ValueError(f"消息太短: {len(data)} bytes") raise ValueError(f"消息太短: {len(data)} bytes")
version, command = struct.unpack('>BB', data[0:2]) version, command = struct.unpack(">BB", data[0:2])
sequence = struct.unpack('>I', data[2:6])[0] sequence = struct.unpack(">I", data[2:6])[0]
body = data[6:] body = data[6:]
return cls(command=command, sequence=sequence, body=body) return cls(command=command, sequence=sequence, body=body)
@@ -84,9 +85,11 @@ class WssMessage:
class HuyaWssClient: class HuyaWssClient:
"""虎牙 WSS 客户端 — 商城通道""" """虎牙 WSS 客户端 — 商城通道"""
def __init__(self, baseinfo: str = None, logger: Callable[[str], None] = None): def __init__(
self, baseinfo: str | None = None, logger: Callable[[str], None] | None = None
):
self.baseinfo = baseinfo or SHOP_BASEINFO self.baseinfo = baseinfo or SHOP_BASEINFO
self.ws = None self.ws: Any = None
self._biz_seq = SEQ_BUSINESS # 业务 RPC 递增用 self._biz_seq = SEQ_BUSINESS # 业务 RPC 递增用
self._wup_req_id = 8 self._wup_req_id = 8
self.rpc_queue = deque() self.rpc_queue = deque()
@@ -135,7 +138,12 @@ class HuyaWssClient:
# 大包头里的长度使用原始 WUP 包长度(含 4B 长度前缀),但正文只放裸 WUP body; # 大包头里的长度使用原始 WUP 包长度(含 4B 长度前缀),但正文只放裸 WUP body;
# 浏览器随后用 trace 尾部的前 4 字节补足该长度窗口。 # 浏览器随后用 trace 尾部的前 4 字节补足该长度窗口。
packet_len = len(wup_data) packet_len = len(wup_data)
return bytes([packet_len & 0xFF]) + packet_len.to_bytes(4, "big") + wup_body + HuyaWssClient._make_big_tail() return (
bytes([packet_len & 0xFF])
+ packet_len.to_bytes(4, "big")
+ wup_body
+ HuyaWssClient._make_big_tail()
)
@staticmethod @staticmethod
def _normalize_biz_cookie(cookie: str) -> str: def _normalize_biz_cookie(cookie: str) -> str:
@@ -143,7 +151,12 @@ class HuyaWssClient:
cookie = (cookie or "").strip() cookie = (cookie or "").strip()
normalized = f"huya_ua={WSS_COOKIE_UA}" normalized = f"huya_ua={WSS_COOKIE_UA}"
if re.search(r"(?:^|;\s*)huya_ua=", cookie): if re.search(r"(?:^|;\s*)huya_ua=", cookie):
return re.sub(r"(^|;\s*)huya_ua=[^;]*", lambda m: f"{m.group(1)}{normalized}", cookie, count=1) return re.sub(
r"(^|;\s*)huya_ua=[^;]*",
lambda m: f"{m.group(1)}{normalized}",
cookie,
count=1,
)
if not cookie: if not cookie:
return normalized return normalized
return f"{normalized}; {cookie}" return f"{normalized}; {cookie}"
@@ -152,6 +165,7 @@ class HuyaWssClient:
def _build_biz_user(uid: int, cookie: str): def _build_biz_user(uid: int, cookie: str):
"""构造 WSS 业务 UserIdHAR 实证:guid 为空,cookie 不额外加前缀)""" """构造 WSS 业务 UserIdHAR 实证:guid 为空,cookie 不额外加前缀)"""
from .shop_structs import UserId from .shop_structs import UserId
user = UserId() user = UserId()
user.lUid = uid user.lUid = uid
user.sGuid = "" user.sGuid = ""
@@ -167,6 +181,7 @@ class HuyaWssClient:
def _build_shop_app(source_id: str = "yellowcarlist", scene: int = WSS_SHOP_SCENE): def _build_shop_app(source_id: str = "yellowcarlist", scene: int = WSS_SHOP_SCENE):
"""构造 WSS 业务 ShopAppInfo""" """构造 WSS 业务 ShopAppInfo"""
from .shop_structs import ShopAppInfo from .shop_structs import ShopAppInfo
info = ShopAppInfo() info = ShopAppInfo()
info.sAppId = "huya" info.sAppId = "huya"
info.sBizType = "" info.sBizType = ""
@@ -183,28 +198,33 @@ class HuyaWssClient:
"input_1": str(item_count), "input_1": str(item_count),
} }
async def connect(self, host: str = SHOP_WS_HOST, timeout: float = 15.0, async def connect(
cookie: str = ""): self, host: str = SHOP_WS_HOST, timeout: float = 15.0, cookie: str = ""
):
from urllib.parse import quote from urllib.parse import quote
url = f"wss://{host}/?baseinfo={quote(self.baseinfo)}" url = f"wss://{host}/?baseinfo={quote(self.baseinfo)}"
self.logger(f"[WSS] 正在连接 {host} ...") self.logger(f"[WSS] 正在连接 {host} ...")
self.logger(f"[WSS] URL: {url[:100]}...") self.logger(f"[WSS] URL: {url[:100]}...")
BROWSER_UA = ("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) " BROWSER_UA = (
"AppleWebKit/537.36 (KHTML, like Gecko) " "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
"Chrome/149.0.0.0 Safari/537.36") "AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/149.0.0.0 Safari/537.36"
)
headers = {"Accept-Language": "zh-CN,zh;q=0.9"} headers = {"Accept-Language": "zh-CN,zh;q=0.9"}
try: try:
self.ws = await asyncio.wait_for( self.ws = await asyncio.wait_for(
websockets.connect( websockets.connect(
url, url,
origin="https://m-shop.yaoguo.com", origin=cast(Any, "https://m-shop.yaoguo.com"),
user_agent_header=BROWSER_UA, user_agent_header=BROWSER_UA,
additional_headers=headers, additional_headers=headers,
open_timeout=timeout, open_timeout=timeout,
), ),
timeout=timeout) timeout=timeout,
)
except asyncio.TimeoutError: except asyncio.TimeoutError:
self.logger(f"[WSS] 连接超时({timeout}s") self.logger(f"[WSS] 连接超时({timeout}s")
raise raise
@@ -230,11 +250,16 @@ class HuyaWssClient:
try: try:
async for raw in self.ws: async for raw in self.ws:
try: try:
msg = WssMessage.decode(raw) raw_bytes = raw if isinstance(raw, bytes) else raw.encode()
self.logger(format_wss_log(msg.body, msg.command, msg.sequence, "")) msg = WssMessage.decode(raw_bytes)
self.logger(
format_wss_log(msg.body, msg.command, msg.sequence, "")
)
await self._handle_message(msg) await self._handle_message(msg)
except Exception as e: except Exception as e:
self.logger(f"[WSS] 解析消息失败: {e} raw_hex={raw[:50].hex()}") self.logger(
f"[WSS] 解析消息失败: {e} raw_hex={raw_bytes[:50].hex()}"
)
except asyncio.CancelledError: except asyncio.CancelledError:
pass pass
except websockets.exceptions.ConnectionClosed as e: except websockets.exceptions.ConnectionClosed as e:
@@ -270,14 +295,17 @@ class HuyaWssClient:
async def send_heartbeat(self): async def send_heartbeat(self):
# body = LIST(tag0,count=1) + 元素{tag0="live:0", tag1=""} + TAIL # body = LIST(tag0,count=1) + 元素{tag0="live:0", tag1=""} + TAIL
os = TafOutputStream() os = TafOutputStream()
os.write_head(0, TafType.LIST) # 09 os.write_head(0, TafType.LIST) # 09
os.write_int32(0, 1) # 00 01 (count=1) os.write_int32(0, 1) # 00 01 (count=1)
os.write_string(0, "live:0") # 06 06 live:0 os.write_string(0, "live:0") # 06 06 live:0
os.buf.write(b'\x16\x00') # 16 00 (tag1 "") os.buf.write(b"\x16\x00") # 16 00 (tag1 "")
body = os.get_bytes() + TAIL_BYTES body = os.get_bytes() + TAIL_BYTES
msg = WssMessage(command=WssCommand.HEARTBEAT_SEND, msg = WssMessage(
sequence=SEQ_HEARTBEAT, body=body) command=WssCommand.HEARTBEAT_SEND, sequence=SEQ_HEARTBEAT, body=body
self.logger(format_wss_log(body, WssCommand.HEARTBEAT_SEND, SEQ_HEARTBEAT, "")) )
self.logger(
format_wss_log(body, WssCommand.HEARTBEAT_SEND, SEQ_HEARTBEAT, "")
)
await self.ws.send(msg.encode()) await self.ws.send(msg.encode())
async def initialize(self, uid: int, guid: str, cookie: str): async def initialize(self, uid: int, guid: str, cookie: str):
@@ -295,10 +323,14 @@ class HuyaWssClient:
if launch_rsp is None: if launch_rsp is None:
self.logger("[✗] wsLaunch 无响应,初始化失败") self.logger("[✗] wsLaunch 无响应,初始化失败")
return False return False
self.logger(f"[初始化] wsLaunch OK guid={self._launch_guid} ip={self._launch_ip}") self.logger(
f"[初始化] wsLaunch OK guid={self._launch_guid} ip={self._launch_ip}"
)
config_rsp = await self.call_get_config_shop() config_rsp = await self.call_get_config_shop()
self.logger(f"[初始化] getConfig {'OK' if config_rsp is not None else '无响应(可继续)'}") self.logger(
f"[初始化] getConfig {'OK' if config_rsp is not None else '无响应(可继续)'}"
)
if self._launch_guid: if self._launch_guid:
await self.send_register(self._launch_guid) await self.send_register(self._launch_guid)
@@ -316,11 +348,11 @@ class HuyaWssClient:
os = TafOutputStream() os = TafOutputStream()
os.write_struct_begin(0) os.write_struct_begin(0)
os.write_int64(0, 0) # lUid = 0 os.write_int64(0, 0) # lUid = 0
os.write_string(1, "") # sGuid = "" os.write_string(1, "") # sGuid = ""
os.write_string(2, ua) # sUA os.write_string(2, ua) # sUA
os.write_string(3, app_src) # sAppSrc os.write_string(3, app_src) # sAppSrc
os.write_struct_begin(4) # tDeviceInfo os.write_struct_begin(4) # tDeviceInfo
for i in range(5): for i in range(5):
os.write_string(i, "") os.write_string(i, "")
os.write_struct_end() os.write_struct_end()
@@ -336,8 +368,7 @@ class HuyaWssClient:
body = self._encode_rpc_body(wup_data) body = self._encode_rpc_body(wup_data)
seq = SEQ_WSLAUNCH seq = SEQ_WSLAUNCH
msg = WssMessage(command=WssCommand.RPC_REQUEST, msg = WssMessage(command=WssCommand.RPC_REQUEST, sequence=seq, body=body)
sequence=seq, body=body)
future = asyncio.Future() future = asyncio.Future()
self.rpc_queue.append(future) self.rpc_queue.append(future)
self.logger(format_wss_log(body, WssCommand.RPC_REQUEST, seq, "")) self.logger(format_wss_log(body, WssCommand.RPC_REQUEST, seq, ""))
@@ -363,11 +394,13 @@ class HuyaWssClient:
wup_resp.decode(wup) wup_resp.decode(wup)
treq = wup_resp.newdata.get("tRsp") or wup_resp.newdata.get("tResp") treq = wup_resp.newdata.get("tRsp") or wup_resp.newdata.get("tResp")
if not treq: if not treq:
self.logger(f"[RPC] wsLaunch 响应无 tRsp, keys={list(wup_resp.newdata.keys())}") self.logger(
f"[RPC] wsLaunch 响应无 tRsp, keys={list(wup_resp.newdata.keys())}"
)
return return
ins = TafInputStream(treq) ins = TafInputStream(treq)
tag, dtype = ins.peek_head() tag, dtype = ins.peek_head()
if dtype != 0x0a: # STRUCT_BEGIN if dtype != 0x0A: # STRUCT_BEGIN
self.logger(f"[RPC] wsLaunch tRsp 非结构体 dtype=0x{dtype:02x}") self.logger(f"[RPC] wsLaunch tRsp 非结构体 dtype=0x{dtype:02x}")
return return
ins.read_head() ins.read_head()
@@ -376,19 +409,29 @@ class HuyaWssClient:
ftag, ftype = ins.peek_head() ftag, ftype = ins.peek_head()
except EOFError: except EOFError:
break break
if ftype == 0x0b: # STRUCT_END if ftype == 0x0B: # STRUCT_END
ins.read_head() ins.read_head()
break break
ins.read_head() ins.read_head()
if ftag == 0 and ftype in (0x06, 0x07): if ftag == 0 and ftype in (0x06, 0x07):
ln = ins.buf.read(1)[0] if ftype == 0x06 else int.from_bytes(ins.buf.read(4), 'big') ln = (
self._launch_guid = ins.buf.read(ln).decode('utf-8', 'replace') ins.buf.read(1)[0]
if ftype == 0x06
else int.from_bytes(ins.buf.read(4), "big")
)
self._launch_guid = ins.buf.read(ln).decode("utf-8", "replace")
elif ftag == 1 and ftype in (0x06, 0x07): elif ftag == 1 and ftype in (0x06, 0x07):
ln = ins.buf.read(1)[0] if ftype == 0x06 else int.from_bytes(ins.buf.read(4), 'big') ln = (
self._launch_ip = ins.buf.read(ln).decode('utf-8', 'replace') ins.buf.read(1)[0]
if ftype == 0x06
else int.from_bytes(ins.buf.read(4), "big")
)
self._launch_ip = ins.buf.read(ln).decode("utf-8", "replace")
else: else:
ins.skip_field(ftype) ins.skip_field(ftype)
self.logger(f"[RPC] wsLaunch 解析: guid={self._launch_guid} ip={self._launch_ip}") self.logger(
f"[RPC] wsLaunch 解析: guid={self._launch_guid} ip={self._launch_ip}"
)
except Exception as e: except Exception as e:
self.logger(f"[RPC] wsLaunch 响应解析失败: {e}") self.logger(f"[RPC] wsLaunch 响应解析失败: {e}")
@@ -403,27 +446,36 @@ class HuyaWssClient:
if len(body) < 5: if len(body) < 5:
return body return body
# 大包格式需优先判断;prefix 可能是 0x00,容易被误判成小包长度 8。 # 大包格式需优先判断;prefix 可能是 0x00,容易被误判成小包长度 8。
wup_len = int.from_bytes(body[1:5], 'big') wup_len = int.from_bytes(body[1:5], "big")
if 5 + wup_len <= len(body) and body[5:7] == b'\x10\x03': if 5 + wup_len <= len(body) and body[5:7] == b"\x10\x03":
return body[5:5 + wup_len] return body[5 : 5 + wup_len]
# 尝试 4B total_len 小包格式 # 尝试 4B total_len 小包格式
total_len = int.from_bytes(body[0:4], 'big') total_len = int.from_bytes(body[0:4], "big")
if 8 <= total_len <= len(body) and body[4:6] == b'\x10\x03': if 8 <= total_len <= len(body) and body[4:6] == b"\x10\x03":
# total_len 包含自身 4B # total_len 包含自身 4B
return body[:total_len] return body[:total_len]
if 5 + wup_len <= len(body): if 5 + wup_len <= len(body):
return body[5:5 + wup_len] return body[5 : 5 + wup_len]
return body return body
async def send_auth(self, cookie: str): async def send_auth(self, cookie: str):
"""cmd 0x0a AUTH — 发送 cookie""" """cmd 0x0a AUTH — 发送 cookie"""
ua = "webh5&0.0.1&websocket&&diypc_52775" ua = "webh5&0.0.1&websocket&&diypc_52775"
auth_text = f"huya_ua={ua}; {cookie}" auth_text = f"huya_ua={ua}; {cookie}"
msg = WssMessage(command=WssCommand.AUTH, msg = WssMessage(
sequence=SEQ_WSLAUNCH, command=WssCommand.AUTH,
body=auth_text.encode('utf-8') + TAIL_BYTES) sequence=SEQ_WSLAUNCH,
body=auth_text.encode("utf-8") + TAIL_BYTES,
)
await self.ws.send(msg.encode()) await self.ws.send(msg.encode())
self.logger(format_wss_log(auth_text.encode('utf-8') + TAIL_BYTES, WssCommand.AUTH, SEQ_WSLAUNCH, "")) self.logger(
format_wss_log(
auth_text.encode("utf-8") + TAIL_BYTES,
WssCommand.AUTH,
SEQ_WSLAUNCH,
"",
)
)
async def call_get_config_shop(self, timeout: float = 10.0): async def call_get_config_shop(self, timeout: float = 10.0):
"""商城版 mobileui.getConfig """商城版 mobileui.getConfig
@@ -441,18 +493,18 @@ class HuyaWssClient:
os.write_struct_begin(0) os.write_struct_begin(0)
# tag0: UserId (匿名) # tag0: UserId (匿名)
os.write_struct_begin(0) os.write_struct_begin(0)
os.write_int64(0, 0) # lUid = 0 os.write_int64(0, 0) # lUid = 0
os.write_string(1, "") # sGuid os.write_string(1, "") # sGuid
os.write_string(2, "") # sUA os.write_string(2, "") # sUA
os.write_string(3, ua) # sHuYaUA os.write_string(3, ua) # sHuYaUA
os.write_string(4, "") # sToken os.write_string(4, "") # sToken
os.write_int32(5, 0) # iTokenType os.write_int32(5, 0) # iTokenType
os.write_string(6, "") # sDeviceInfo os.write_string(6, "") # sDeviceInfo
os.write_string(7, "") # sCookie os.write_string(7, "") # sCookie
os.write_struct_end() os.write_struct_end()
os.write_map(1, {}) # tag1: 空Map os.write_map(1, {}) # tag1: 空Map
os.write_string(2, "huya") # tag2: sAppId os.write_string(2, "huya") # tag2: sAppId
os.write_head(3, TafType.LIST) # tag3: 空List os.write_head(3, TafType.LIST) # tag3: 空List
os.write_int32(0, 0) os.write_int32(0, 0)
os.write_struct_end() os.write_struct_end()
@@ -466,8 +518,7 @@ class HuyaWssClient:
body = self._encode_rpc_body(wup_data) body = self._encode_rpc_body(wup_data)
seq = SEQ_GETCONFIG seq = SEQ_GETCONFIG
msg = WssMessage(command=WssCommand.RPC_REQUEST, msg = WssMessage(command=WssCommand.RPC_REQUEST, sequence=seq, body=body)
sequence=seq, body=body)
future = asyncio.Future() future = asyncio.Future()
self.rpc_queue.append(future) self.rpc_queue.append(future)
self.logger(format_wss_log(body, WssCommand.RPC_REQUEST, seq, "")) self.logger(format_wss_log(body, WssCommand.RPC_REQUEST, seq, ""))
@@ -503,8 +554,7 @@ class HuyaWssClient:
os.write_map(7, {}) os.write_map(7, {})
os.write_int32(8, 0) os.write_int32(8, 0)
body = os.get_bytes() + TAIL_BYTES body = os.get_bytes() + TAIL_BYTES
msg = WssMessage(command=WssCommand.REGISTER, msg = WssMessage(command=WssCommand.REGISTER, sequence=SEQ_REGISTER, body=body)
sequence=SEQ_REGISTER, body=body)
self.logger(format_wss_log(body, WssCommand.REGISTER, SEQ_REGISTER, "")) self.logger(format_wss_log(body, WssCommand.REGISTER, SEQ_REGISTER, ""))
await self.ws.send(msg.encode()) await self.ws.send(msg.encode())
@@ -513,14 +563,20 @@ class HuyaWssClient:
os = TafOutputStream() os = TafOutputStream()
os.write_map(0, {}) os.write_map(0, {})
body = os.get_bytes() + TAIL_BYTES body = os.get_bytes() + TAIL_BYTES
msg = WssMessage(command=WssCommand.CONFIRM_SEND, msg = WssMessage(
sequence=SEQ_CONFIRM, body=body) command=WssCommand.CONFIRM_SEND, sequence=SEQ_CONFIRM, body=body
)
self.logger(format_wss_log(body, WssCommand.CONFIRM_SEND, SEQ_CONFIRM, "")) self.logger(format_wss_log(body, WssCommand.CONFIRM_SEND, SEQ_CONFIRM, ""))
await self.ws.send(msg.encode()) await self.ws.send(msg.encode())
async def call_rpc(self, service: str, method: str, async def call_rpc(
req_struct: TafStruct, rsp_class=None, self,
timeout: float = 10.0): service: str,
method: str,
req_struct: TafStruct,
rsp_class=None,
timeout: float = 10.0,
):
seq = self._next_biz_seq() seq = self._next_biz_seq()
wup = WupRequest() wup = WupRequest()
@@ -532,8 +588,7 @@ class HuyaWssClient:
wup_data = wup.encode() wup_data = wup.encode()
body = self._encode_rpc_body(wup_data) body = self._encode_rpc_body(wup_data)
msg = WssMessage(command=WssCommand.RPC_REQUEST, msg = WssMessage(command=WssCommand.RPC_REQUEST, sequence=seq, body=body)
sequence=seq, body=body)
future = asyncio.Future() future = asyncio.Future()
self.rpc_queue.append(future) self.rpc_queue.append(future)
self.logger(format_wss_log(body, WssCommand.RPC_REQUEST, seq, "")) self.logger(format_wss_log(body, WssCommand.RPC_REQUEST, seq, ""))
@@ -559,7 +614,9 @@ class HuyaWssClient:
if dtype == TafType.STRUCT_BEGIN: if dtype == TafType.STRUCT_BEGIN:
ins.read_head() ins.read_head()
decoded = _decode_taf_struct(ins) decoded = _decode_taf_struct(ins)
self.logger(f"[←] {service}.{method} {key}: {_truncate(decoded)}") self.logger(
f"[←] {service}.{method} {key}: {_truncate(decoded)}"
)
except Exception: except Exception:
pass pass
@@ -571,10 +628,18 @@ class HuyaWssClient:
result = wup_resp.readStruct("tResp", rsp_class) result = wup_resp.readStruct("tResp", rsp_class)
return result return result
async def get_goods_info(self, uid: int, guid: str, cookie: str, async def get_goods_info(
pid: int, spu_id: str, sku_id: int = 0, self,
game_id: str = "", source_id: str = "yellowcarlist", uid: int,
scene: int = WSS_SHOP_SCENE): guid: str,
cookie: str,
pid: int,
spu_id: str,
sku_id: int = 0,
game_id: str = "",
source_id: str = "yellowcarlist",
scene: int = WSS_SHOP_SCENE,
):
from .shop_structs import GetGoodsInfoReqV5, GoodsInfoRsp from .shop_structs import GetGoodsInfoReqV5, GoodsInfoRsp
req = GetGoodsInfoReqV5() req = GetGoodsInfoReqV5()
@@ -585,12 +650,20 @@ class HuyaWssClient:
req.skuId = sku_id req.skuId = sku_id
req.gameId = game_id req.gameId = game_id
return await self.call_rpc("shopMiddleUI", "getGoodsInfoV5", return await self.call_rpc(
req, GoodsInfoRsp, timeout=15.0) "shopMiddleUI", "getGoodsInfoV5", req, GoodsInfoRsp, timeout=15.0
)
async def query_user_order_list(self, uid: int, guid: str, cookie: str, async def query_user_order_list(
offset: int = 0, page_size: int = 10, self,
order_type: int = 1, status: int = 0): uid: int,
guid: str,
cookie: str,
offset: int = 0,
page_size: int = 10,
order_type: int = 1,
status: int = 0,
):
from .shop_structs import QueryUserOrderListReq, QueryUserOrderListRsp from .shop_structs import QueryUserOrderListReq, QueryUserOrderListRsp
req = QueryUserOrderListReq() req = QueryUserOrderListReq()
@@ -600,17 +673,35 @@ class HuyaWssClient:
req.pageSize = page_size req.pageSize = page_size
req.status = status req.status = status
return await self.call_rpc("revenueWebUI", "queryUserOrderList", return await self.call_rpc(
req, QueryUserOrderListRsp, timeout=15.0) "revenueWebUI",
"queryUserOrderList",
req,
QueryUserOrderListRsp,
timeout=15.0,
)
async def create_order(self, uid: int, guid: str, cookie: str, async def create_order(
pid: int, spu_id: str, sku_id: int, self,
item_count: int = 1, source_id: str = "yellowcarlist", uid: int,
game_id: str = "", scene: int = WSS_SHOP_SCENE, guid: str,
order_type: int = 6): cookie: str,
from .shop_structs import (CreateOrderReqV5, CreateOrderRsp, pid: int,
CreateOrderExtraParam, CreateOrderPromotionParam, spu_id: str,
CreateOrderAccountParam) sku_id: int,
item_count: int = 1,
source_id: str = "yellowcarlist",
game_id: str = "",
scene: int = WSS_SHOP_SCENE,
order_type: int = 6,
):
from .shop_structs import (
CreateOrderReqV5,
CreateOrderRsp,
CreateOrderExtraParam,
CreateOrderPromotionParam,
CreateOrderAccountParam,
)
req = CreateOrderReqV5() req = CreateOrderReqV5()
req.userId = self._build_biz_user(uid, cookie) req.userId = self._build_biz_user(uid, cookie)
@@ -635,14 +726,22 @@ class HuyaWssClient:
req.ext = "" req.ext = ""
self.logger(f"[下单] orderType={order_type} (HUYA_VIRTUAL)") self.logger(f"[下单] orderType={order_type} (HUYA_VIRTUAL)")
return await self.call_rpc("shopMiddleUI", "createOrderV5", return await self.call_rpc(
req, CreateOrderRsp, timeout=15.0) "shopMiddleUI", "createOrderV5", req, CreateOrderRsp, timeout=15.0
)
async def pay_order_submit(self, uid: int, guid: str, cookie: str, async def pay_order_submit(
order_id: int, pay_type: int = 1, self,
pid: int = 0, source_id: str = "yellowcarlist", uid: int,
scene: int = WSS_SHOP_SCENE, guid: str,
item_count: int = 1): cookie: str,
order_id: int,
pay_type: int = 1,
pid: int = 0,
source_id: str = "yellowcarlist",
scene: int = WSS_SHOP_SCENE,
item_count: int = 1,
):
from .shop_structs import PayOrderRes from .shop_structs import PayOrderRes
os = TafOutputStream() os = TafOutputStream()
@@ -674,8 +773,7 @@ class HuyaWssClient:
wup_data = wup.encode() wup_data = wup.encode()
body = self._encode_rpc_body(wup_data) body = self._encode_rpc_body(wup_data)
msg = WssMessage(command=WssCommand.RPC_REQUEST, msg = WssMessage(command=WssCommand.RPC_REQUEST, sequence=seq, body=body)
sequence=seq, body=body)
future = asyncio.Future() future = asyncio.Future()
self.rpc_queue.append(future) self.rpc_queue.append(future)
self.logger(format_wss_log(body, WssCommand.RPC_REQUEST, seq, "")) self.logger(format_wss_log(body, WssCommand.RPC_REQUEST, seq, ""))