""" 虎牙 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 Any, Optional, Callable, cast 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 = None, logger: Callable[[str], None] | None = None ): self.baseinfo = baseinfo or SHOP_BASEINFO self.ws: Any = 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 requestId,HAR 中 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 业务 UserId(HAR 实证: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=cast(Any, "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: raw_bytes = raw if isinstance(raw, bytes) else raw.encode() msg = WssMessage.decode(raw_bytes) 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_bytes[: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