Files
live-hub-py/core/huya/wss_client.py
T

710 lines
27 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
虎牙 WSS 客户端 — 商城通道 (77bc035c-ws.va.huya.com)
初始化流程 (Chrome WS 帧实证):
1. HEARTBEAT_SEND (cmd 0x10)
2. RPC: launch.wsLaunch (cmd 0x03) — 商城版无 lUid
3. RPC: mobileui.getConfig (cmd 0x03)
4. 等待 wsLaunch/getConfig 响应
5. REGISTER (cmd 0x21) — 回传 wsLaunch 返回的 sGuid
6. CONFIRM_SEND (cmd 0x17)
7. 业务 RPC ...
关键发现 (Chrome WS 帧分析):
- 服务端响应 seq ≠ 请求 seq → 必须用 FIFO 匹配
- 初始化 wsLaunch iRequestId=-1,业务 requestId 从 8 开始递增
"""
import asyncio
import random
import re
import struct
import time
from collections import deque
from typing import Optional, Callable
import websockets
from .wup_protocol import WupRequest, WupResponse
from .taf_protocol import TafOutputStream, TafInputStream, TafType, TafStruct
from .frame_decoder import format_wss_log, _decode_taf_struct, _truncate
SHOP_WS_HOST = "77bc035c-ws.va.huya.com"
# 商城端点 baseinfo (conn4, h5_/index.html) — 商城业务 shopMiddleUI 走此通道
# wsLaunch tReq: lUid=0, sGuid="", sUA="webh5&0.0.1&websocket&&h5_/index.html"
SHOP_BASEINFO = "DBYAJiV3ZWJoNSYwLjAuMSZ3ZWJzb2NrZXQmJmg1Xy9pbmRleC5odG1sNgxIVVlBJlpIJjIwNTJGAFYAbHYAhgCWAKgM"
TAIL_BYTES = bytes.fromhex("2c36004c5c6600")
BIG_TAIL_SUFFIX = bytes.fromhex("4c5c6600")
WSS_BIZ_UA = "web&1.0.0&huya"
WSS_COOKIE_UA = "webh5&0.0.1&websocket&&diypc_52775"
WSS_SHOP_SCENE = 4
# seq 按消息类型固定 (HAR 实证,跨两次抓包一致)
SEQ_HEARTBEAT = 0x1d00000d
SEQ_WSLAUNCH = 0x1d000078
SEQ_GETCONFIG = 0x1d000076
SEQ_REGISTER = 0x1d00003c
SEQ_CONFIRM = 0x1d000002
SEQ_BUSINESS = 0x1d000106 # 业务 RPC (HAR 7.4 实证:业务帧固定使用该 seq)
class WssCommand:
RPC_REQUEST = 0x03
RPC_RESPONSE = 0x04
AUTH = 0x0a
SERVER_PUSH1 = 0x0b
HEARTBEAT_SEND = 0x10
HEARTBEAT_RECV = 0x11
CONFIRM_SEND = 0x17
SERVER_PUSH2 = 0x18
REGISTER = 0x21
class WssMessage:
def __init__(self, command: int, sequence: int, body: bytes):
self.command = command
self.sequence = sequence
self.body = body
def encode(self) -> bytes:
header = struct.pack('>BBI', 0x00, self.command, self.sequence)
return header + self.body
@classmethod
def decode(cls, data: bytes) -> 'WssMessage':
if len(data) < 6:
raise ValueError(f"消息太短: {len(data)} bytes")
version, command = struct.unpack('>BB', data[0:2])
sequence = struct.unpack('>I', data[2:6])[0]
body = data[6:]
return cls(command=command, sequence=sequence, body=body)
class HuyaWssClient:
"""虎牙 WSS 客户端 — 商城通道"""
def __init__(self, baseinfo: str = None, logger: Callable[[str], None] = None):
self.baseinfo = baseinfo or SHOP_BASEINFO
self.ws = None
self._biz_seq = SEQ_BUSINESS # 业务 RPC 递增用
self._wup_req_id = 8
self.rpc_queue = deque()
self.logger = logger or print
self._recv_task = None
self._heartbeat_task = None
self._launch_guid = ""
self._launch_ip = ""
def _next_biz_seq(self) -> int:
"""业务 RPC 的 frame seq。
浏览器在商城 WSS 上连续发送多个业务 RPC 时,frame seq 都保持
0x1d000106;真正区分请求依赖 WUP requestId。
"""
return SEQ_BUSINESS
def _next_wup_req_id(self) -> int:
"""业务 WUP requestIdHAR 中 getGoodsInfoV5 从 8 开始递增"""
req_id = self._wup_req_id
self._wup_req_id += 1
return req_id
@staticmethod
def _make_big_tail() -> bytes:
"""生成 WSS 大包尾部 trace(格式与浏览器 HAR 一致)"""
trace = f"{random.getrandbits(64):016x}"
return f",6%{trace}:{trace}:0:3".encode("ascii") + BIG_TAIL_SUFFIX
@staticmethod
def _encode_rpc_body(wup_data: bytes) -> bytes:
"""按浏览器规则封装 WSS RPC body
小包: [4B WUP长度][WUP body] + TAIL_BYTES
大包: [1B len低字节][4B WUP body长度][裸WUP body] + trace尾部
"""
if len(wup_data) < 4:
return wup_data + TAIL_BYTES
declared_len = int.from_bytes(wup_data[:4], "big")
if declared_len == len(wup_data):
wup_body = wup_data[4:]
else:
wup_body = wup_data
if len(wup_body) <= 0xFF:
return wup_data + TAIL_BYTES
# 大包头里的长度使用原始 WUP 包长度(含 4B 长度前缀),但正文只放裸 WUP body;
# 浏览器随后用 trace 尾部的前 4 字节补足该长度窗口。
packet_len = len(wup_data)
return bytes([packet_len & 0xFF]) + packet_len.to_bytes(4, "big") + wup_body + HuyaWssClient._make_big_tail()
@staticmethod
def _normalize_biz_cookie(cookie: str) -> str:
"""业务 RPC 的 huya_ua Cookie 值按浏览器 WSS 抓包修正"""
cookie = (cookie or "").strip()
normalized = f"huya_ua={WSS_COOKIE_UA}"
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)
if not cookie:
return normalized
return f"{normalized}; {cookie}"
@staticmethod
def _build_biz_user(uid: int, cookie: str):
"""构造 WSS 业务 UserIdHAR 实证:guid 为空,cookie 不额外加前缀)"""
from .shop_structs import UserId
user = UserId()
user.lUid = uid
user.sGuid = ""
user.sToken = ""
user.sHuYaUA = WSS_BIZ_UA
user.sCookie = HuyaWssClient._normalize_biz_cookie(cookie)
user.iTokenType = 0
user.sDeviceInfo = ""
user.sQIMEI = ""
return user
@staticmethod
def _build_shop_app(source_id: str = "yellowcarlist", scene: int = WSS_SHOP_SCENE):
"""构造 WSS 业务 ShopAppInfo"""
from .shop_structs import ShopAppInfo
info = ShopAppInfo()
info.sAppId = "huya"
info.sBizType = ""
info.scene = scene
info.sourceId = source_id
return info
@staticmethod
def _build_order_env(item_count: int = 1):
"""构造下单/支付环境参数"""
click_time = int(time.time() * 1000)
return {
"user_click_seq": f'[{{"x":258,"y":487,"time":{click_time},"id":""}}]',
"input_1": str(item_count),
}
async def connect(self, host: str = SHOP_WS_HOST, timeout: float = 15.0,
cookie: str = ""):
from urllib.parse import quote
url = f"wss://{host}/?baseinfo={quote(self.baseinfo)}"
self.logger(f"[WSS] 正在连接 {host} ...")
self.logger(f"[WSS] URL: {url[:100]}...")
BROWSER_UA = ("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/149.0.0.0 Safari/537.36")
headers = {"Accept-Language": "zh-CN,zh;q=0.9"}
try:
self.ws = await asyncio.wait_for(
websockets.connect(
url,
origin="https://m-shop.yaoguo.com",
user_agent_header=BROWSER_UA,
additional_headers=headers,
open_timeout=timeout,
),
timeout=timeout)
except asyncio.TimeoutError:
self.logger(f"[WSS] 连接超时({timeout}s")
raise
except Exception as e:
self.logger(f"[WSS] 连接失败: {type(e).__name__}: {e}")
raise
self.logger(f"[WSS] 握手成功, 实际连接: {self.ws.remote_address}")
self._recv_task = asyncio.create_task(self._recv_loop())
self._heartbeat_task = asyncio.create_task(self._heartbeat_loop())
async def disconnect(self):
if self._heartbeat_task:
self._heartbeat_task.cancel()
if self._recv_task:
self._recv_task.cancel()
if self.ws:
await self.ws.close()
self.ws = None
self.logger("[WSS] 已断开")
async def _recv_loop(self):
try:
async for raw in self.ws:
try:
msg = WssMessage.decode(raw)
self.logger(format_wss_log(msg.body, msg.command, msg.sequence, "收"))
await self._handle_message(msg)
except Exception as e:
self.logger(f"[WSS] 解析消息失败: {e} raw_hex={raw[:50].hex()}")
except asyncio.CancelledError:
pass
except websockets.exceptions.ConnectionClosed as e:
self.logger(f"[WSS] 连接关闭: code={e.code} reason={e.reason}")
except Exception as e:
self.logger(f"[WSS] 接收循环异常: {type(e).__name__}: {e}")
async def _handle_message(self, msg: WssMessage):
if msg.command == WssCommand.RPC_RESPONSE:
if self.rpc_queue:
future = self.rpc_queue.popleft()
if not future.done():
future.set_result(msg.body)
else:
self.logger(f"[WSS] 未匹配的 RPC 响应 seq={msg.sequence}")
elif msg.command == WssCommand.HEARTBEAT_RECV:
pass # 心跳已在上层 _recv_loop 打过日志
elif msg.command in (WssCommand.SERVER_PUSH1, WssCommand.SERVER_PUSH2):
pass # 推送已在上层打过日志
else:
pass # 未知消息已在上层打过日志
async def _heartbeat_loop(self):
try:
while True:
await asyncio.sleep(8)
await self.send_heartbeat()
except asyncio.CancelledError:
pass
except Exception as e:
self.logger(f"[WSS] 心跳循环异常: {e}")
async def send_heartbeat(self):
# body = LIST(tag0,count=1) + 元素{tag0="live:0", tag1=""} + TAIL
os = TafOutputStream()
os.write_head(0, TafType.LIST) # 09
os.write_int32(0, 1) # 00 01 (count=1)
os.write_string(0, "live:0") # 06 06 live:0
os.buf.write(b'\x16\x00') # 16 00 (tag1 "")
body = os.get_bytes() + TAIL_BYTES
msg = WssMessage(command=WssCommand.HEARTBEAT_SEND,
sequence=SEQ_HEARTBEAT, body=body)
self.logger(format_wss_log(body, WssCommand.HEARTBEAT_SEND, SEQ_HEARTBEAT, "发"))
await self.ws.send(msg.encode())
async def initialize(self, uid: int, guid: str, cookie: str):
"""商城端点初始化: heartbeat → wsLaunch → getConfig → confirm
商城 wsLaunch 无 lUid/sGuid (匿名), tReq:
tag0 lUid=0, tag1 sGuid="", tag2 sUA="webh5&0.0.1&websocket&&h5_/index.html",
tag3 sAppSrc="HUYA&ZH&2052", tag4 tDeviceInfo={5空串}
"""
self.logger("[初始化] 开始 WSS 初始化流程 (商城端点)...")
await self.send_heartbeat()
launch_rsp = await self.call_ws_launch_shop()
if launch_rsp is None:
self.logger("[✗] wsLaunch 无响应,初始化失败")
return False
self.logger(f"[初始化] wsLaunch OK guid={self._launch_guid} ip={self._launch_ip}")
config_rsp = await self.call_get_config_shop()
self.logger(f"[初始化] getConfig {'OK' if config_rsp is not None else '无响应(可继续)'}")
if self._launch_guid:
await self.send_register(self._launch_guid)
await self.send_confirm()
await asyncio.sleep(0.3)
self.logger("[初始化] 完成 ✓")
return True
async def call_ws_launch_shop(self, timeout: float = 10.0):
"""商城版 wsLaunch (匿名, 无 lUid/sGuid, 与 conn4 格式一致)"""
ua = "webh5&0.0.1&websocket&&h5_/index.html"
app_src = "HUYA&ZH&2052"
os = TafOutputStream()
os.write_struct_begin(0)
os.write_int64(0, 0) # lUid = 0
os.write_string(1, "") # sGuid = ""
os.write_string(2, ua) # sUA
os.write_string(3, app_src) # sAppSrc
os.write_struct_begin(4) # tDeviceInfo
for i in range(5):
os.write_string(i, "")
os.write_struct_end()
os.write_struct_end()
wup = WupRequest()
wup.setServant("launch")
wup.setFunc("wsLaunch")
wup.setRequestId(-1)
wup.iTimeout = 0
wup.newdata["tReq"] = os.get_bytes()
wup_data = wup.encode()
body = self._encode_rpc_body(wup_data)
seq = SEQ_WSLAUNCH
msg = WssMessage(command=WssCommand.RPC_REQUEST,
sequence=seq, body=body)
future = asyncio.Future()
self.rpc_queue.append(future)
self.logger(format_wss_log(body, WssCommand.RPC_REQUEST, seq, "发"))
await self.ws.send(msg.encode())
try:
body = await asyncio.wait_for(future, timeout)
except asyncio.TimeoutError:
if future in self.rpc_queue:
self.rpc_queue.remove(future)
self.logger("[✗] wsLaunch 超时")
return None
self._parse_launch_response(body)
return body
def _parse_launch_response(self, body: bytes):
"""解析 wsLaunch 响应 (WSLaunchRsp: tag0=sGuid, tag1=sClientIp)"""
try:
# 从 body 中提取干净的 WUP 包(去掉尾部垃圾字节)
wup = self._extract_wup(body)
wup_resp = WupResponse()
wup_resp.decode(wup)
treq = wup_resp.newdata.get("tRsp") or wup_resp.newdata.get("tResp")
if not treq:
self.logger(f"[RPC] wsLaunch 响应无 tRsp, keys={list(wup_resp.newdata.keys())}")
return
ins = TafInputStream(treq)
tag, dtype = ins.peek_head()
if dtype != 0x0a: # STRUCT_BEGIN
self.logger(f"[RPC] wsLaunch tRsp 非结构体 dtype=0x{dtype:02x}")
return
ins.read_head()
while True:
try:
ftag, ftype = ins.peek_head()
except EOFError:
break
if ftype == 0x0b: # STRUCT_END
ins.read_head()
break
ins.read_head()
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')
self._launch_guid = ins.buf.read(ln).decode('utf-8', 'replace')
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')
self._launch_ip = ins.buf.read(ln).decode('utf-8', 'replace')
else:
ins.skip_field(ftype)
self.logger(f"[RPC] wsLaunch 解析: guid={self._launch_guid} ip={self._launch_ip}")
except Exception as e:
self.logger(f"[RPC] wsLaunch 响应解析失败: {e}")
@staticmethod
def _extract_wup(body: bytes) -> bytes:
"""从 WSS 响应 body 中提取干净的 WUP 包
响应 body 格式:
小包: [4B BE total_len][wup_body][tail]
大包: [1B prefix][4B BE wup_body_len][wup_body][tail]
"""
if len(body) < 5:
return body
# 大包格式需优先判断;prefix 可能是 0x00,容易被误判成小包长度 8。
wup_len = int.from_bytes(body[1:5], 'big')
if 5 + wup_len <= len(body) and body[5:7] == b'\x10\x03':
return body[5:5 + wup_len]
# 尝试 4B total_len 小包格式
total_len = int.from_bytes(body[0:4], 'big')
if 8 <= total_len <= len(body) and body[4:6] == b'\x10\x03':
# total_len 包含自身 4B
return body[:total_len]
if 5 + wup_len <= len(body):
return body[5:5 + wup_len]
return body
async def send_auth(self, cookie: str):
"""cmd 0x0a AUTH — 发送 cookie"""
ua = "webh5&0.0.1&websocket&&diypc_52775"
auth_text = f"huya_ua={ua}; {cookie}"
msg = WssMessage(command=WssCommand.AUTH,
sequence=SEQ_WSLAUNCH,
body=auth_text.encode('utf-8') + TAIL_BYTES)
await self.ws.send(msg.encode())
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):
"""商城版 mobileui.getConfig
tReq 结构 (HAR 实证):
tag0: UserId {lUid=0, sGuid="", sUA="", sHuYaUA="webh5&0.0.1&websocket&&h5_/index.html",
sToken="", iTokenType=0, sDeviceInfo="", sCookie=""}
tag1: 空Map
tag2: "huya" (sAppId)
tag3: 空List
"""
ua = "webh5&0.0.1&websocket&&h5_/index.html"
os = TafOutputStream()
os.write_struct_begin(0)
# tag0: UserId (匿名)
os.write_struct_begin(0)
os.write_int64(0, 0) # lUid = 0
os.write_string(1, "") # sGuid
os.write_string(2, "") # sUA
os.write_string(3, ua) # sHuYaUA
os.write_string(4, "") # sToken
os.write_int32(5, 0) # iTokenType
os.write_string(6, "") # sDeviceInfo
os.write_string(7, "") # sCookie
os.write_struct_end()
os.write_map(1, {}) # tag1: 空Map
os.write_string(2, "huya") # tag2: sAppId
os.write_head(3, TafType.LIST) # tag3: 空List
os.write_int32(0, 0)
os.write_struct_end()
wup = WupRequest()
wup.setServant("mobileui")
wup.setFunc("getConfig")
wup.setRequestId(7)
wup.iTimeout = 0
wup.newdata["tReq"] = os.get_bytes()
wup_data = wup.encode()
body = self._encode_rpc_body(wup_data)
seq = SEQ_GETCONFIG
msg = WssMessage(command=WssCommand.RPC_REQUEST,
sequence=seq, body=body)
future = asyncio.Future()
self.rpc_queue.append(future)
self.logger(format_wss_log(body, WssCommand.RPC_REQUEST, seq, "发"))
await self.ws.send(msg.encode())
try:
body = await asyncio.wait_for(future, timeout)
except asyncio.TimeoutError:
if future in self.rpc_queue:
self.rpc_queue.remove(future)
self.logger("[✗] getConfig 超时")
return None
return body
async def send_register(self, guid_from_launch: str):
"""cmd 0x21 REGISTER — 回传 wsLaunch 的 sGuid
body = tag0("HUYA&ZH&2052") + tag1(sGuid) + tag2=0 + tag3=0
+ tag6 struct{tag0空Map, tag1空Map, tag3=0}
+ tag7空Map + tag8=0 + TAIL
"""
os = TafOutputStream()
os.write_string(0, "HUYA&ZH&2052")
os.write_string(1, guid_from_launch)
os.write_int32(2, 0)
os.write_int32(3, 0)
os.write_struct_begin(6)
os.write_map(0, {})
os.write_map(1, {})
os.write_int32(3, 0)
os.write_struct_end()
os.write_map(7, {})
os.write_int32(8, 0)
body = os.get_bytes() + TAIL_BYTES
msg = WssMessage(command=WssCommand.REGISTER,
sequence=SEQ_REGISTER, body=body)
self.logger(format_wss_log(body, WssCommand.REGISTER, SEQ_REGISTER, "发"))
await self.ws.send(msg.encode())
async def send_confirm(self):
"""cmd 0x17 CONFIRM_SEND — body = 空Map(tag0) + TAIL"""
os = TafOutputStream()
os.write_map(0, {})
body = os.get_bytes() + TAIL_BYTES
msg = WssMessage(command=WssCommand.CONFIRM_SEND,
sequence=SEQ_CONFIRM, body=body)
self.logger(format_wss_log(body, WssCommand.CONFIRM_SEND, SEQ_CONFIRM, "发"))
await self.ws.send(msg.encode())
async def call_rpc(self, service: str, method: str,
req_struct: TafStruct, rsp_class=None,
timeout: float = 10.0):
seq = self._next_biz_seq()
wup = WupRequest()
wup.setServant(service)
wup.setFunc(method)
wup.setRequestId(self._next_wup_req_id())
wup.iTimeout = 0
wup.writeStruct("tReq", req_struct)
wup_data = wup.encode()
body = self._encode_rpc_body(wup_data)
msg = WssMessage(command=WssCommand.RPC_REQUEST,
sequence=seq, body=body)
future = asyncio.Future()
self.rpc_queue.append(future)
self.logger(format_wss_log(body, WssCommand.RPC_REQUEST, seq, "发"))
await self.ws.send(msg.encode())
try:
body = await asyncio.wait_for(future, timeout)
except asyncio.TimeoutError:
if future in self.rpc_queue:
self.rpc_queue.remove(future)
self.logger(f"[✗] {service}.{method} 超时")
return None
wup_resp = WupResponse()
wup_resp.decode(self._extract_wup(body))
# 响应日志已由 _recv_loop 打出,此处只打解码后的业务字段
for key in ("tRsp", "tResp"):
data = wup_resp.newdata.get(key)
if data and isinstance(data, bytes) and len(data) > 0:
try:
ins = TafInputStream(data)
tag, dtype = ins.peek_head()
if dtype == TafType.STRUCT_BEGIN:
ins.read_head()
decoded = _decode_taf_struct(ins)
self.logger(f"[←] {service}.{method} {key}: {_truncate(decoded)}")
except Exception:
pass
if rsp_class is None:
return body
result = wup_resp.readStruct("tRsp", rsp_class)
if result is None:
result = wup_resp.readStruct("tResp", rsp_class)
return result
async def get_goods_info(self, uid: int, 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
req = GetGoodsInfoReqV5()
req.userId = self._build_biz_user(uid, cookie)
req.shopAppInfo = self._build_shop_app(source_id, scene)
req.pid = pid
req.spuId = spu_id
req.skuId = sku_id
req.gameId = game_id
return await self.call_rpc("shopMiddleUI", "getGoodsInfoV5",
req, GoodsInfoRsp, timeout=15.0)
async def query_user_order_list(self, 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
req = QueryUserOrderListReq()
req.userId = self._build_biz_user(uid, cookie)
req.offset = offset
req.orderType = order_type
req.pageSize = page_size
req.status = status
return await self.call_rpc("revenueWebUI", "queryUserOrderList",
req, QueryUserOrderListRsp, timeout=15.0)
async def create_order(self, uid: int, guid: str, cookie: str,
pid: int, spu_id: str, 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.userId = self._build_biz_user(uid, cookie)
req.shopAppInfo = self._build_shop_app(source_id, scene)
req.receiveId = 0
req.pid = pid
req.spuId = spu_id
req.skuId = sku_id
req.itemCount = item_count
req.gameId = game_id or "0"
req.src = scene
req.scene = 0
req.sourceId = source_id
req.orderType = order_type
req.extraParam = CreateOrderExtraParam()
req.env = self._build_order_env(item_count)
req.orderScene = scene
req.promotionParam = CreateOrderPromotionParam()
req.accountParam = CreateOrderAccountParam()
req.bizType = 5
req.gameCategoryId = 507
req.ext = ""
self.logger(f"[下单] orderType={order_type} (HUYA_VIRTUAL)")
return await self.call_rpc("shopMiddleUI", "createOrderV5",
req, CreateOrderRsp, timeout=15.0)
async def pay_order_submit(self, uid: int, guid: str, 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
os = TafOutputStream()
os.write_struct_begin(0)
os.write_struct(0, self._build_biz_user(uid, cookie))
os.write_struct(1, self._build_shop_app(source_id, scene))
os.write_int64(2, order_id)
os.write_string(3, "Zfb" if pay_type == 1 else str(pay_type))
os.write_string(4, "QrCode")
callback_url = (
f"https://m-shop.yaoguo.com/index.html#/consumer/paycallback"
f"?orderId={order_id}&pid={pid}&sourceId={source_id}"
)
os.write_string(5, callback_url)
os.write_map(6, self._build_order_env(item_count))
os.write_string(7, "null")
os.write_struct_end()
seq = self._next_biz_seq()
wup = WupRequest()
wup.setServant("shopMiddleUI")
wup.setFunc("payOrderSubmitV5")
wup.setRequestId(self._next_wup_req_id())
wup.iTimeout = 0
wup.newdata["tReq"] = os.get_bytes()
wup_data = wup.encode()
body = self._encode_rpc_body(wup_data)
msg = WssMessage(command=WssCommand.RPC_REQUEST,
sequence=seq, body=body)
future = asyncio.Future()
self.rpc_queue.append(future)
self.logger(format_wss_log(body, WssCommand.RPC_REQUEST, seq, "发"))
await self.ws.send(msg.encode())
try:
body = await asyncio.wait_for(future, 15.0)
except asyncio.TimeoutError:
if future in self.rpc_queue:
self.rpc_queue.remove(future)
self.logger("[✗] payOrderSubmitV5 超时")
return None
wup_resp = WupResponse()
wup_resp.decode(self._extract_wup(body))
for key in ("tRsp", "tResp"):
data = wup_resp.newdata.get(key)
if data and isinstance(data, bytes) and len(data) > 0:
try:
ins = TafInputStream(data)
tag, dtype = ins.peek_head()
if dtype == TafType.STRUCT_BEGIN:
ins.read_head()
decoded = _decode_taf_struct(ins)
self.logger(f"[←] payOrderSubmitV5 {key}: {_truncate(decoded)}")
except Exception:
pass
result = wup_resp.readStruct("tRsp", PayOrderRes)
if result is None:
result = wup_resp.readStruct("tResp", PayOrderRes)
return result