Files
live-hub-py/core/huya/shop_structs.py
T
yml2213 a9d711a7e9 fix(huya): 结构体尾部字段容错解析(服务端新增 ZERO 字段不再打挂整个 RPC)
真机现象(2026-10-15,外部 CK 导入后查询角色):checkUserBindGameAccount 响应结构
尾部多了 tag6(ZERO 类型 0x6c),共享的 read_struct 读到非 STRUCT_END 直接抛
「期望 STRUCT_END, 实际 0x0c」,整个 RPC 返回 None —— 工作台「查询角色」永远失败。

- taf_protocol:read_struct 收尾改为按 TAF 语义跳过未建模的尾部字段(上限 32 个,
  防止解析错位时无限吞字节);新增 skip_to_struct_end()
- frame_decoder / shop_structs:抽出 decode_struct_bytes 与 ShopRawRsp.from_payload,
  WSS 与 HTTP 两条通道共用一份通用解码逻辑(wss_client 同步改用)

验证:tests/test_huya_struct_parse_resilience.py 用真机响应字节回归(含 tag6=ZERO 的
响应能解析出角色「泪雨全是谎言」);按 AGENTS.md 反向验证 —— 注掉
skip_to_struct_end() 即复现线上报错「期望 STRUCT_END, 实际 0x0c」,还原后恢复。
全量 pytest 通过。
2026-10-15 15:21:34 +08:00

1407 lines
47 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.
"""
虎牙充值相关 JCE 结构定义
字段顺序 = TAF tag 顺序(从 JS 逆向所得)
来源:m-shop.yaoguo.com 的 jce/ShopFacade.js、api/orderui.ts、api/mall/PlayMallNewHome.ts
"""
from typing import Any
from .taf_protocol import TafInputStream, TafOutputStream, TafStruct, TafType
class OrderType:
HUYA_PHYSICAL = 1
TENCENT_VIRTUAL = 2
MAGIC_BOX = 3
MAGIC_BOX_DELIVERY = 4
PAID_COURSE = 5
HUYA_VIRTUAL = 6
LOTTERY_PICKUP = 7
DELIVERY_WORKBENCH = 11
GAME_ACCOUNT = 12
SHELF_GOODS = 17
class OrderStatus:
DEPOSIT_WAIT_PAY = 10
DEPOSIT_PAID = 20
WAIT_DELIVER = 30
WAIT_RECEIVE = 40
FINISHED = 50
FINISHED_CLOSED = 60
CANCELLED = 90
BALANCE_WAIT_PAY = 1000
CANCELLED_BALANCE_EXPIRED = 1001
def _opt_str(os: TafOutputStream, tag: int, val: str):
"""非空才写字符串(与浏览器一致,省默认值)"""
if val:
os.write_string(tag, val)
def _opt_int(os: TafOutputStream, tag: int, val):
"""非 0 才写整数"""
if val:
os.write_int64(tag, val)
def _opt_struct(os: TafOutputStream, tag: int, val):
"""非 None 才写结构体"""
if val is not None:
os.write_struct(tag, val)
def _opt_list(os: TafOutputStream, tag: int, val):
"""非空才写 list"""
if val:
os.write_list(tag, val)
def _opt_map(os: TafOutputStream, tag: int, val):
"""非空才写 map"""
if val:
os.write_map(tag, val)
def _skip_to_struct_end(ins: TafInputStream):
"""跳过当前结构里未解析的尾部字段,停在 STRUCT_END 前。"""
while True:
pos = ins.buf.tell()
_tag, dtype = ins.read_head()
if dtype == TafType.STRUCT_END:
ins.buf.seek(pos)
return
ins.skip_field(dtype)
# ============================================================
# 基础结构
# ============================================================
class UserId(TafStruct):
"""用户标识(cookie 在这里)"""
def __init__(self):
self.lUid: int = 0 # tag 0
self.sGuid: str = "" # tag 1
self.sToken: str = "" # tag 2
self.sHuYaUA: str = "" # tag 3 "webh5&0.0.1&websocket&&diypc_52775"
self.sCookie: str = "" # tag 4 完整 cookie
self.iTokenType: int = 0 # tag 5
self.sDeviceInfo: str = "" # tag 6
self.sQIMEI: str = "" # tag 7
def write_to(self, os: TafOutputStream):
# HAR2 实证: 浏览器不优化空串/0值, 全写字段
os.write_int64(0, self.lUid)
os.write_string(1, self.sGuid)
os.write_string(2, self.sToken)
os.write_string(3, self.sHuYaUA)
os.write_string(4, self.sCookie)
os.write_int32(5, self.iTokenType)
os.write_string(6, self.sDeviceInfo)
os.write_string(7, self.sQIMEI)
def read_from(self, ins: TafInputStream):
self.lUid = ins.read_int64(0, default=self.lUid)
self.sGuid = ins.read_string(1, default=self.sGuid)
self.sToken = ins.read_string(2, default=self.sToken)
self.sHuYaUA = ins.read_string(3, default=self.sHuYaUA)
self.sCookie = ins.read_string(4, default=self.sCookie)
self.iTokenType = ins.read_int32(5, default=self.iTokenType)
self.sDeviceInfo = ins.read_string(6, default=self.sDeviceInfo)
self.sQIMEI = ins.read_string(7, default=self.sQIMEI)
class ShopAppInfo(TafStruct):
"""应用信息(HAR2 实证字段顺序: tag0 sAppId, tag1 sBizType, tag4 scene, tag5 sourceId)"""
def __init__(self):
self.sAppId: str = "huya" # tag 0
self.sBizType: str = "" # tag 1
self.scene: int = 0 # tag 4
self.sourceId: str = "" # tag 5
def write_to(self, os: TafOutputStream):
# HAR2 实证: 全写字段(含空串/0)
os.write_string(0, self.sAppId)
os.write_string(1, self.sBizType)
os.write_int32(4, self.scene)
os.write_string(5, self.sourceId)
def read_from(self, ins: TafInputStream):
self.sAppId = ins.read_string(0, default=self.sAppId)
self.sBizType = ins.read_string(1, default=self.sBizType)
self.scene = ins.read_int32(4, default=self.scene)
self.sourceId = ins.read_string(5, default=self.sourceId)
# ============================================================
# wsLaunch 初始化
# ============================================================
class WsLaunchSubStruct(TafStruct):
"""wsLaunch tag4 子结构(5个空字符串字段,浏览器强制写)"""
def __init__(self):
self.s0: str = "" # tag 0
self.s1: str = "" # tag 1
self.s2: str = "" # tag 2
self.s3: str = "" # tag 3
self.s4: str = "" # tag 4
def write_to(self, os: TafOutputStream):
# 浏览器写空字符串(STRING1 length0),这里强制写以精确匹配
os.write_string(0, self.s0)
os.write_string(1, self.s1)
os.write_string(2, self.s2)
os.write_string(3, self.s3)
os.write_string(4, self.s4)
def read_from(self, ins: TafInputStream):
pass
class WsLaunchReq(TafStruct):
"""launch.wsLaunch 请求(tReq 是 STRUCT,不是 list)
从 HAR 逆向:
tag0: lUid (int64)
tag1: string ""
tag2: sHuYaUA "webh5&1.0.0&huya"
tag3: appSrc "HUYA&ZH&2052"
tag4: 子struct (5个空字符串)
"""
def __init__(self):
self.lUid: int = 0 # tag 0
self.s1: str = "" # tag 1
self.sHuYaUA: str = "webh5&1.0.0&huya" # tag 2
self.appSrc: str = "HUYA&ZH&2052" # tag 3
self.sub: WsLaunchSubStruct = WsLaunchSubStruct() # tag 4
def write_to(self, os: TafOutputStream):
# 注意: 浏览器不优化空字符串,tag1 强制写(HAR 实证)
_opt_int(os, 0, self.lUid)
os.write_string(1, self.s1) # 强制写空字符串
_opt_str(os, 2, self.sHuYaUA)
_opt_str(os, 3, self.appSrc)
_opt_struct(os, 4, self.sub)
def read_from(self, ins: TafInputStream):
pass
# ============================================================
# 商品查询
# ============================================================
class GetGoodsInfoReqV5(TafStruct):
"""商品查询请求 (shopMiddleUI.getGoodsInfoV5)"""
def __init__(self):
self.userId = UserId() # tag 0
self.shopAppInfo = ShopAppInfo() # tag 1
self.pid: int = 0 # tag 2
self.gameId: str = "" # tag 3
self.spuId: str = "" # tag 4
self.channelStockCode: str = "" # tag 5
self.skuId: int = 0 # tag 6
self.inviterUid: int = 0 # tag 7
self.userModifyPriceId: int = 0 # tag 8
def write_to(self, os: TafOutputStream):
# HAR2 实证: 全写字段
os.write_struct(0, self.userId)
os.write_struct(1, self.shopAppInfo)
os.write_int64(2, self.pid)
os.write_string(3, self.gameId)
os.write_string(4, self.spuId)
os.write_string(5, self.channelStockCode)
os.write_int64(6, self.skuId)
os.write_int64(7, self.inviterUid)
os.write_int64(8, self.userModifyPriceId)
def read_from(self, ins: TafInputStream):
# 请求结构体一般不需要读,但保留
pass
class GoodsInfoRsp(TafStruct):
"""商品查询响应。"""
def __init__(self):
self.code: int = 0 # tag 0
self.message: str = "" # tag 1
self.goodsInfo = None # tag 2
self.selfGoods: int = 0 # tag 3
self.marketStatus: int = 0 # tag 5
self.timestamp: int = 0 # tag 6
def read_from(self, ins: TafInputStream):
self.code = ins.read_int32(0, default=self.code)
self.message = ins.read_string(1, default=self.message)
self.goodsInfo = ins.read_struct(2, GoodsInfoDetail)
self.selfGoods = ins.read_int32(3, default=self.selfGoods)
self.marketStatus = ins.read_int32(5, default=self.marketStatus)
self.timestamp = ins.read_int64(6, default=self.timestamp)
def write_to(self, os: TafOutputStream):
pass
@property
def spu_id(self) -> str:
return self.goodsInfo.baseInfo.spuId if self.goodsInfo else ""
@property
def sku_id(self) -> int:
return self.goodsInfo.priceInfo.first_sku_id if self.goodsInfo else 0
@property
def name(self) -> str:
return self.goodsInfo.baseInfo.name if self.goodsInfo else ""
@property
def price(self) -> int:
return self.goodsInfo.priceInfo.price if self.goodsInfo else 0
@property
def stock(self) -> int:
return self.goodsInfo.priceInfo.stock if self.goodsInfo else 0
@property
def buy_limit(self) -> int:
return self.goodsInfo.priceInfo.buyLimit if self.goodsInfo else 0
def to_dict(self) -> dict:
goods = self.goodsInfo.to_dict() if self.goodsInfo else {}
return {
"code": self.code,
"message": self.message,
"spu_id": goods.get("spu_id", ""),
"sku_id": goods.get("sku_id", 0),
"name": goods.get("name", ""),
"description": goods.get("description", ""),
"icon": goods.get("icon", ""),
"detail_url": goods.get("detail_url", ""),
"price": goods.get("price", 0),
"min_price": goods.get("min_price", 0),
"max_price": goods.get("max_price", 0),
"stock": goods.get("stock", 0),
"buy_limit": goods.get("buy_limit", 0),
"sku_list": goods.get("sku_list", []),
"self_goods": self.selfGoods,
"market_status": self.marketStatus,
"timestamp": self.timestamp,
"goods": goods,
}
class GoodsBaseInfo(TafStruct):
"""商品基础信息(getGoodsInfoV5 tag2.tag0)。"""
def __init__(self):
self.spuId: str = ""
self.appId: str = ""
self.name: str = ""
self.description: str = ""
self.icon: str = ""
self.detailUrl: str = ""
self.detailHtml: str = ""
def read_from(self, ins: TafInputStream):
self.spuId = ins.read_string(0, default=self.spuId)
self.appId = ins.read_string(1, default=self.appId)
self.name = ins.read_string(2, default=self.name)
self.description = ins.read_string(4, default=self.description)
self.icon = ins.read_string(5, default=self.icon)
self.detailUrl = ins.read_string(6, default=self.detailUrl)
self.detailHtml = ins.read_string(7, default=self.detailHtml)
_skip_to_struct_end(ins)
def write_to(self, os: TafOutputStream):
pass
def to_dict(self) -> dict:
return {
"spu_id": self.spuId,
"app_id": self.appId,
"name": self.name,
"description": self.description,
"icon": self.icon,
"detail_url": self.detailUrl,
}
class GoodsSkuItem(TafStruct):
"""商品 SKU 信息(getGoodsInfoV5 tag2.tag4.tag3 map value)。"""
def __init__(self):
self.skuId: int = 0
self.spuId: str = ""
self.price: int = 0
self.stock: int = 0
self.status: int = 0
def read_from(self, ins: TafInputStream):
self.skuId = ins.read_int64(0, default=self.skuId)
self.spuId = ins.read_string(1, default=self.spuId)
self.price = ins.read_int64(2, default=self.price)
self.stock = ins.read_int64(3, default=self.stock)
self.status = ins.read_int32(4, default=self.status)
_skip_to_struct_end(ins)
def write_to(self, os: TafOutputStream):
pass
@staticmethod
def read_map_value(ins: TafInputStream, dtype: int):
if dtype != TafType.STRUCT_BEGIN:
ins.skip_field(dtype)
return None
item = GoodsSkuItem()
item.read_from(ins)
_end_tag, end_type = ins.read_head()
if end_type != TafType.STRUCT_END:
raise ValueError(f"期望 SKU STRUCT_END,实际 0x{end_type:02x}")
return item
def to_dict(self) -> dict:
return {
"sku_id": self.skuId,
"spu_id": self.spuId,
"price": self.price,
"stock": self.stock,
"status": self.status,
}
class GoodsPriceInfo(TafStruct):
"""商品价格与 SKU 信息(getGoodsInfoV5 tag2.tag4)。"""
def __init__(self):
self.spuId: str = ""
self.minPrice: int = 0
self.maxPrice: int = 0
self.skuMap: dict[int, GoodsSkuItem] = {}
self.stock: int = 0
self.buyLimit: int = 0
self.price: int = 0
def _read_sku_map(self, ins: TafInputStream):
found = ins._find_tag(3, required=False)
if not found:
return {}
if found[1] != TafType.MAP:
ins.skip_field(found[1])
return {}
count = ins._read_int_len()
result = {}
for _ in range(count):
_, key_type = ins.read_head()
key = ins._read_int_value(key_type)
_, value_type = ins.read_head()
item = GoodsSkuItem.read_map_value(ins, value_type)
if item is not None:
result[int(key)] = item
return result
def read_from(self, ins: TafInputStream):
self.spuId = ins.read_string(0, default=self.spuId)
self.minPrice = ins.read_int64(1, default=self.minPrice)
self.maxPrice = ins.read_int64(2, default=self.maxPrice)
self.skuMap = self._read_sku_map(ins)
self.stock = ins.read_int64(4, default=self.stock)
self.buyLimit = ins.read_int64(5, default=self.buyLimit)
self.price = ins.read_int64(7, default=self.price)
_skip_to_struct_end(ins)
def write_to(self, os: TafOutputStream):
pass
@property
def first_sku_id(self) -> int:
if not self.skuMap:
return 0
return min(self.skuMap.keys())
@property
def sku_list(self) -> list[dict]:
return [self.skuMap[key].to_dict() for key in sorted(self.skuMap.keys())]
def to_dict(self) -> dict:
return {
"spu_id": self.spuId,
"sku_id": self.first_sku_id,
"price": self.price or self.minPrice,
"min_price": self.minPrice,
"max_price": self.maxPrice,
"stock": self.stock,
"buy_limit": self.buyLimit,
"sku_list": self.sku_list,
}
class GoodsInfoDetail(TafStruct):
"""getGoodsInfoV5 响应里的 goodsInfo 主体。"""
def __init__(self):
self.baseInfo = GoodsBaseInfo()
self.priceInfo = GoodsPriceInfo()
def read_from(self, ins: TafInputStream):
self.baseInfo = ins.read_struct(0, GoodsBaseInfo) or self.baseInfo
self.priceInfo = ins.read_struct(4, GoodsPriceInfo) or self.priceInfo
_skip_to_struct_end(ins)
def write_to(self, os: TafOutputStream):
pass
def to_dict(self) -> dict:
data = self.baseInfo.to_dict()
data.update(self.priceInfo.to_dict())
return data
# ============================================================
# 订单历史
# ============================================================
class OrderListShopInfo(TafStruct):
"""订单明细里的店铺信息(只取展示需要的字段)"""
def __init__(self):
self.shopName: str = "" # tag 0
def read_from(self, ins: TafInputStream):
self.shopName = ins.read_string(0, default=self.shopName)
_skip_to_struct_end(ins)
def write_to(self, os: TafOutputStream):
pass
def to_dict(self) -> dict:
return {
"shop_name": self.shopName,
}
class OrderListGoodsDetail(TafStruct):
"""订单明细(queryUserOrderList 响应 tag16)"""
def __init__(self):
self.spuId: str = "" # tag 4
self.skuId: int = 0 # tag 16
self.buyerUid: int = 0 # tag 18
self.virtualType: int = 0 # tag 19
self.quantity: int = 0 # tag 20
self.shopInfo: OrderListShopInfo | None = None # tag 21
self.points: int = 0 # tag 23
def read_from(self, ins: TafInputStream):
self.spuId = ins.read_string(4, default=self.spuId)
self.skuId = ins.read_int64(16, default=self.skuId)
self.buyerUid = ins.read_int64(18, default=self.buyerUid)
self.virtualType = ins.read_int32(19, default=self.virtualType)
self.quantity = ins.read_int64(20, default=self.quantity)
self.shopInfo = ins.read_struct(21, OrderListShopInfo)
self.points = ins.read_int64(23, default=self.points)
_skip_to_struct_end(ins)
def write_to(self, os: TafOutputStream):
pass
def to_dict(self) -> dict:
return {
"spu_id": self.spuId,
"sku_id": self.skuId,
"buyer_uid": self.buyerUid,
"virtual_type": self.virtualType,
"quantity": self.quantity,
"shop_info": self.shopInfo.to_dict() if self.shopInfo else None,
"points": self.points,
}
class OrderListItem(TafStruct):
"""订单列表项(queryUserOrderList 响应 tag3 的 list 元素)"""
def __init__(self):
self.bizOrderId: str = "" # tag 0 shop10148750
self.appId: str = "" # tag 1 shop
self.orderId: str = "" # tag 2
self.pid: int = 0 # tag 3
self.shopName: str = "" # tag 4
self.orderStatus: int = 0 # tag 5
self.itemName: str = "" # tag 8
self.unitPrice: int = 0 # tag 9 分
self.quantity: int = 0 # tag 10
self.totalPrice: int = 0 # tag 12 分
self.createTime: int = 0 # tag 14 毫秒时间戳
self.payTime: int = 0 # tag 15 毫秒时间戳
self.goodsDetail: OrderListGoodsDetail | None = None # tag 16
def read_from(self, ins: TafInputStream):
self.bizOrderId = ins.read_string(0, default=self.bizOrderId)
self.appId = ins.read_string(1, default=self.appId)
self.orderId = ins.read_string(2, default=self.orderId)
self.pid = ins.read_int64(3, default=self.pid)
self.shopName = ins.read_string(4, default=self.shopName)
self.orderStatus = ins.read_int32(5, default=self.orderStatus)
self.itemName = ins.read_string(8, default=self.itemName)
self.unitPrice = ins.read_int64(9, default=self.unitPrice)
self.quantity = ins.read_int64(10, default=self.quantity)
self.totalPrice = ins.read_int64(12, default=self.totalPrice)
self.createTime = ins.read_int64(14, default=self.createTime)
self.payTime = ins.read_int64(15, default=self.payTime)
self.goodsDetail = ins.read_struct(16, OrderListGoodsDetail)
_skip_to_struct_end(ins)
def write_to(self, os: TafOutputStream):
pass
def to_dict(self) -> dict:
return {
"biz_order_id": self.bizOrderId,
"app_id": self.appId,
"order_id": self.orderId,
"pid": self.pid,
"shop_name": self.shopName,
"order_status": self.orderStatus,
"item_name": self.itemName,
"unit_price": self.unitPrice,
"quantity": self.quantity,
"total_price": self.totalPrice,
"create_time": self.createTime,
"pay_time": self.payTime,
"goods_detail": self.goodsDetail.to_dict() if self.goodsDetail else None,
}
class QueryUserOrderListReq(TafStruct):
"""查询购买历史订单 (revenueWebUI.queryUserOrderList)"""
def __init__(self):
self.userId = UserId() # tag 0
self.offset: int = 0 # tag 1
self.orderType: int = 1 # tag 2
self.pageSize: int = 10 # tag 3
self.status: int = 0 # tag 4
def write_to(self, os: TafOutputStream):
os.write_struct(0, self.userId)
os.write_int32(1, self.offset)
os.write_int32(2, self.orderType)
os.write_int32(3, self.pageSize)
os.write_int32(4, self.status)
def read_from(self, ins: TafInputStream):
pass
class QueryUserOrderListRsp(TafStruct):
"""购买历史订单响应"""
def __init__(self):
self.code: int = 0
self.message: str = ""
self.totalCount: int = 0
self.orders: list[OrderListItem] = []
@staticmethod
def _read_order_item(ins: TafInputStream, _tag: int):
item = OrderListItem()
item.read_from(ins)
_end_tag, dtype = ins.read_head()
if dtype != TafType.STRUCT_END:
raise ValueError(f"期望订单 STRUCT_END,实际 0x{dtype:02x}")
return item
def read_from(self, ins: TafInputStream):
self.code = ins.read_int32(0, default=self.code)
self.message = ins.read_string(1, default=self.message)
self.totalCount = ins.read_int64(2, default=self.totalCount)
self.orders = ins.read_list(3, item_reader=self._read_order_item)
def write_to(self, os: TafOutputStream):
pass
def to_dict(self) -> dict:
return {
"code": self.code,
"message": self.message,
"total_count": self.totalCount,
"orders": [item.to_dict() for item in self.orders],
}
class ShopAppRequest(TafStruct):
"""商城前置 RPC 的公共请求字段。"""
def __init__(self):
self.userId = UserId()
self.shopAppInfo = ShopAppInfo()
def read_from(self, ins: TafInputStream):
self.userId = ins.read_struct(0, UserId) or self.userId
self.shopAppInfo = ins.read_struct(1, ShopAppInfo) or self.shopAppInfo
class ListPayChannelReq(ShopAppRequest):
def __init__(self):
super().__init__()
self.supplierId = 0
self.bizType = 5
self.gameCategoryId = 507
self.spuId = ""
self.skuId = 0
def write_to(self, os: TafOutputStream):
os.write_struct(0, self.userId)
os.write_struct(1, self.shopAppInfo)
os.write_int64(2, self.supplierId)
os.write_int32(3, self.bizType)
os.write_int32(4, self.gameCategoryId)
os.write_string(5, self.spuId)
os.write_int64(6, self.skuId)
class CheckHyProtocolReq(ShopAppRequest):
def __init__(self):
super().__init__()
self.protocolVersion = 1
def write_to(self, os: TafOutputStream):
os.write_struct(0, self.userId)
os.write_struct(1, self.shopAppInfo)
os.write_int32(2, self.protocolVersion)
class CheckUserBuyAuthReq(ShopAppRequest):
def __init__(self):
super().__init__()
self.spuId = ""
self.skuId = 0
self.checkType = 3
self.payChannel = ""
self.itemCount = 0
def write_to(self, os: TafOutputStream):
os.write_struct(0, self.userId)
os.write_struct(1, self.shopAppInfo)
os.write_string(2, self.spuId)
os.write_int64(3, self.skuId)
os.write_int32(4, self.checkType)
os.write_string(5, self.payChannel)
os.write_int32(6, self.itemCount)
class ListPayChannelRsp(TafStruct):
"""shopMiddleUI.listPayChannelV5 响应。"""
def __init__(self):
self.code = 0
self.message = ""
self.channels: list[str] = []
def read_from(self, ins: TafInputStream):
self.channels = ins.read_list(0)
self.code = ins.read_int32(1, default=self.code)
self.message = ins.read_string(2, default=self.message)
_skip_to_struct_end(ins)
def write_to(self, os: TafOutputStream):
os.write_list(0, self.channels)
os.write_int32(1, self.code)
os.write_string(2, self.message)
def to_dict(self) -> dict:
return {"code": self.code, "message": self.message, "channels": self.channels}
class ShopCodeRsp(TafStruct):
"""checkHyProtocolV5/checkUserBuyAuth 的状态响应。"""
def __init__(self):
self.code = 0
self.message = ""
def read_from(self, ins: TafInputStream):
self.code = ins.read_int32(0, default=self.code)
self.message = ins.read_string(1, default=self.message)
_skip_to_struct_end(ins)
def write_to(self, os: TafOutputStream):
os.write_int32(0, self.code)
os.write_string(1, self.message)
def to_dict(self) -> dict:
return {"code": self.code, "message": self.message}
class OrderDetailOrder(TafStruct):
"""orderDetailV5 中的订单摘要;保留支付轮询所需字段。"""
def __init__(self):
self.orderId: int = 0
self.orderStatus: int = 0
self.createTime: int = 0
self.payTime: int = 0
self.totalPrice: int = 0
def read_from(self, ins: TafInputStream):
while True:
pos = ins.buf.tell()
tag, dtype = ins.read_head()
if dtype == TafType.STRUCT_END:
ins.buf.seek(pos)
return
if tag == 0:
ins.buf.seek(pos)
self.orderId = ins.read_int64(tag, default=self.orderId)
elif tag == 3:
ins.buf.seek(pos)
self.orderStatus = ins.read_int32(tag, default=self.orderStatus)
elif tag == 7:
ins.buf.seek(pos)
self.totalPrice = ins.read_int64(tag, default=self.totalPrice)
elif tag == 13:
ins.buf.seek(pos)
self.createTime = ins.read_int64(tag, default=self.createTime)
elif tag == 14:
ins.buf.seek(pos)
self.payTime = ins.read_int64(tag, default=self.payTime)
else:
ins.skip_field(dtype)
def write_to(self, os: TafOutputStream):
os.write_int64(0, self.orderId)
os.write_int32(3, self.orderStatus)
os.write_int64(7, self.totalPrice)
os.write_int64(13, self.createTime)
os.write_int64(14, self.payTime)
def to_dict(self) -> dict:
return {
"order_id": self.orderId,
"order_status": self.orderStatus,
"create_time": self.createTime,
"pay_time": self.payTime,
"total_price": self.totalPrice,
}
class OrderDetailReq(TafStruct):
"""shopMiddleUI.orderDetailV5 请求。"""
def __init__(self):
self.userId = UserId()
self.shopAppInfo = ShopAppInfo()
self.orderId: int = 0
def write_to(self, os: TafOutputStream):
os.write_struct(0, self.userId)
os.write_struct(1, self.shopAppInfo)
os.write_int64(2, self.orderId)
def read_from(self, ins: TafInputStream):
self.userId = ins.read_struct(0, UserId) or self.userId
self.shopAppInfo = ins.read_struct(1, ShopAppInfo) or self.shopAppInfo
self.orderId = ins.read_int64(2, default=self.orderId)
class OrderDetailData(TafStruct):
def __init__(self):
self.order: OrderDetailOrder | None = None
def read_from(self, ins: TafInputStream):
while True:
pos = ins.buf.tell()
tag, dtype = ins.read_head()
if dtype == TafType.STRUCT_END:
ins.buf.seek(pos)
return
if tag == 1:
ins.buf.seek(pos)
self.order = ins.read_struct(1, OrderDetailOrder)
else:
ins.skip_field(dtype)
def write_to(self, os: TafOutputStream):
if self.order:
os.write_struct(1, self.order)
class OrderDetailRsp(TafStruct):
"""shopMiddleUI.orderDetailV5 响应。"""
def __init__(self):
self.code = 0
self.message = ""
self.data: OrderDetailData | None = None
def read_from(self, ins: TafInputStream):
self.code = ins.read_int32(0, default=self.code)
self.message = ins.read_string(1, default=self.message)
self.data = ins.read_struct(2, OrderDetailData)
@property
def order(self) -> OrderDetailOrder | None:
return self.data.order if self.data else None
def write_to(self, os: TafOutputStream):
os.write_int32(0, self.code)
os.write_string(1, self.message)
if self.data:
os.write_struct(2, self.data)
def to_dict(self) -> dict:
return {
"code": self.code,
"message": self.message,
"order": self.order.to_dict() if self.order else None,
}
# ============================================================
# 下单
# ============================================================
class CreateOrderExtraParam(TafStruct):
def __init__(self):
self.freight: int = 0 # tag 0
self.channelStockType: str = "" # tag 1
self.channelStockCode: str = "" # tag 2
self.relatedBizId: str = "" # tag 3
self.bizParams: str = "" # tag 4
self.popupTraceId: str = "" # tag 5
self.supplierUid: int = 0 # tag 6
self.categoryId: str = "" # tag 7
self.ext: str = "" # tag 8
def write_to(self, os: TafOutputStream):
# HAR 实证:下单 extraParam 会强制写默认 0/空串字段
os.write_int64(0, self.freight)
os.write_string(1, self.channelStockType)
os.write_string(2, self.channelStockCode)
os.write_string(3, self.relatedBizId)
os.write_string(4, self.bizParams)
os.write_string(5, self.popupTraceId)
os.write_int64(6, self.supplierUid)
if self.categoryId:
os.write_string(7, self.categoryId)
os.write_string(8, self.ext)
def read_from(self, ins: TafInputStream):
pass
class CreateOrderPromotionParam(TafStruct):
def __init__(self):
self.yxjDeductPrice: int = 0 # tag 0
self.userModifyPriceId: int = 0 # tag 1
self.enablePromotion: int = 1 # tag 2
def write_to(self, os: TafOutputStream):
# HAR 实证:tag0/tag1 为 0,tag2 为 1
os.write_int64(0, self.yxjDeductPrice)
os.write_int64(1, self.userModifyPriceId)
os.write_int32(2, self.enablePromotion)
def read_from(self, ins: TafInputStream):
pass
class CreateOrderAccountParam(TafStruct):
def __init__(self):
self.payoutTypeList: list[int] = [] # tag 0 Vector<INT32>
self.payoutChargeAmount: int = 0 # tag 1
self.cancelPayoutTypeList: list[int] = [] # tag 2
self.recycleSupplierId: int = 0 # tag 3
self.claimPrice: int = 0 # tag 4
def write_to(self, os: TafOutputStream):
# HAR 实证:空 list/0 值也会写出
os.write_list(0, self.payoutTypeList)
os.write_int64(1, self.payoutChargeAmount)
os.write_list(2, self.cancelPayoutTypeList)
os.write_int64(3, self.recycleSupplierId)
os.write_int64(4, self.claimPrice)
def read_from(self, ins: TafInputStream):
pass
class PromotionItem(TafStruct):
def __init__(self):
self.promotionId: int = 0 # tag 0
self.promotionType: int = 0 # tag 1
def write_to(self, os: TafOutputStream):
_opt_int(os, 0, self.promotionId)
_opt_int(os, 1, self.promotionType)
def read_from(self, ins: TafInputStream):
self.promotionId = ins.read_int64(0, default=self.promotionId)
self.promotionType = ins.read_int32(1, default=self.promotionType)
class CreateOrderReqV5(TafStruct):
"""下单请求 (shopMiddleUI.createOrderV5)"""
def __init__(self):
self.userId = UserId() # tag 0
self.shopAppInfo = ShopAppInfo() # tag 1
self.receiveId: int = 0 # tag 2
self.pid: int = 0 # tag 3
self.skuId: int = 0 # tag 4
self.itemCount: int = 1 # tag 5
self.remark: str = "" # tag 6
self.spuId: str = "" # tag 7
self.gameId: str = "" # tag 8
self.orderId: int = 0 # tag 9
self.src: int = 0 # tag 10
self.couponUserIds: list[int] = [] # tag 11 Vector<INT64>
self.orderType: int = 0 # tag 12
self.extraParam: CreateOrderExtraParam | None = None # tag 13
self.scene: int = 0 # tag 14
self.promotionItems: list = [] # tag 15 Vector<PromotionItem>
self.sourceId: str = "" # tag 16
self.env: dict[str, str] = {} # tag 17 Map<STRING,STRING>
self.orderScene: int = 0 # tag 18
self.watchWord: str = "" # tag 19
self.marketingChannel: str = "" # tag 20
self.promotionParam: CreateOrderPromotionParam | None = None # tag 21
self.externalTraceKey: str = "" # tag 22
self.kefuUid: int = 0 # tag 23
self.accountParam: CreateOrderAccountParam | None = None # tag 24
self.parentOrderId: int = 0 # tag 25
self.vendorAccountType: str = "" # tag 26
self.vendorAccountVal: str = "" # tag 27
self.vendorSubAccountVal: str = "" # tag 28
self.vendorSubAccountType: str = "" # tag 29
self.bizType: int = 0 # tag 30
self.gameCategoryId: int = 0 # tag 31
self.ext: str = "" # tag 32
def write_to(self, os: TafOutputStream):
# HAR 实证:createOrderV5 会写出完整字段,即使值为 0/空串/空 list
os.write_struct(0, self.userId)
os.write_struct(1, self.shopAppInfo)
os.write_int64(2, self.receiveId)
os.write_int64(3, self.pid)
os.write_int64(4, self.skuId)
os.write_int64(5, self.itemCount)
os.write_string(6, self.remark)
os.write_string(7, self.spuId)
os.write_string(8, self.gameId)
os.write_int64(9, self.orderId)
os.write_int32(10, self.src)
os.write_list(11, self.couponUserIds)
os.write_int32(12, self.orderType)
os.write_struct(13, self.extraParam or CreateOrderExtraParam())
os.write_int32(14, self.scene)
os.write_list(15, self.promotionItems)
os.write_string(16, self.sourceId)
os.write_map(17, self.env)
os.write_int32(18, self.orderScene)
os.write_string(19, self.watchWord)
os.write_string(20, self.marketingChannel)
os.write_struct(21, self.promotionParam or CreateOrderPromotionParam())
os.write_string(22, self.externalTraceKey)
os.write_int64(23, self.kefuUid)
os.write_struct(24, self.accountParam or CreateOrderAccountParam())
os.write_int64(25, self.parentOrderId)
os.write_string(26, self.vendorAccountType)
os.write_string(27, self.vendorAccountVal)
os.write_string(28, self.vendorSubAccountVal)
os.write_string(29, self.vendorSubAccountType)
os.write_int32(30, self.bizType)
os.write_int32(31, self.gameCategoryId)
os.write_string(32, self.ext)
def read_from(self, ins: TafInputStream):
pass
class CreateOrderRsp(TafStruct):
"""下单响应 (shopMiddleUI.createOrderV5)"""
def __init__(self):
self.code: int = 0 # tag 0
self.message: str = "" # tag 1
self.orderId: int = 0 # tag 2 虎牙订单号
self.subOrderId: int = 0 # tag 4
self.orderStatus: int = 0 # tag 5
self.riskUrl: str = "" # tag 8 风控跳转URL(code!=200时可能有)
def read_from(self, ins: TafInputStream):
self.code = ins.read_int32(0, default=self.code)
self.message = ins.read_string(1, default=self.message)
self.orderId = ins.read_int64(2, default=self.orderId)
self.subOrderId = ins.read_int64(4, default=self.subOrderId)
self.orderStatus = ins.read_int32(5, default=self.orderStatus)
self.riskUrl = ins.read_string(8, default=self.riskUrl)
def write_to(self, os: TafOutputStream):
pass
# ============================================================
# 支付(payOrderSubmitV5)
# ============================================================
class PayOrderRes(TafStruct):
"""
发起支付响应 (shopMiddleUI.payOrderSubmitV5)
结构从 state_shop_ts.js 的 payOrderRes 推断,tag 顺序按出现顺序
"""
def __init__(self):
self.code: int = 0 # tag 0
self.message: str = "" # tag 1
self.orderId: int = 0 # tag 2 虎牙订单号
self.appOrderId: str = "" # tag 3
self.payOrderId: str = "" # tag 4 支付宝 payOrderId
self.payUrl: str = "" # tag 5 支付宝下单URL(含sign)
self.amount: int = 0 # tag 6
def read_from(self, ins: TafInputStream):
self.code = ins.read_int32(0, default=self.code)
self.message = ins.read_string(1, default=self.message)
self.orderId = ins.read_int64(2, default=self.orderId)
self.appOrderId = ins.read_string(3, default=self.appOrderId)
self.payOrderId = ins.read_string(4, default=self.payOrderId)
self.payUrl = ins.read_string(5, default=self.payUrl)
self.amount = ins.read_int64(6, default=self.amount)
def write_to(self, os: TafOutputStream):
pass
def to_dict(self) -> dict:
return {
"code": self.code,
"message": self.message,
"order_id": self.orderId,
"app_order_id": self.appOrderId,
"pay_order_id": self.payOrderId,
"pay_url": self.payUrl,
"amount": self.amount,
}
# ============================================================
# 积分中心(m-shop.huya.com/points.html)新增 RPC
#
# 来源:2026-10-13「开通精英宝典 / 购买幸运币」WSS 抓包逐字段解出。
# 浏览器实际链路顺序:
# getGoodsInfoV5 → getSupplierInfoV5 → listPayChannelV5 → checkHyProtocolV5
# → acceptHyProtocolV5 → calcOrderPromotion → checkUserBuyAuth
# → createOrderV5 → payOrderSubmitV5 → orderDetailV5(轮询)
# 这些接口的响应层次较深且随商品变化,统一用 ShopRawRsp 暴露通用解码结果,
# 不凭空猜测未确认的字段名。
# ============================================================
class ShopRawRsp:
"""未逐字段建模的商城响应:暴露 code/message 与通用解码后的原始结构。"""
def __init__(self, code: int = 0, message: str = "", payload: dict | None = None):
self.code = code
self.message = message
self.payload = payload or {}
def value(self, *path):
"""按通用解码的 tag 路径取值,例如 value("tag2", "tag0", "tag1")。"""
node: Any = self.payload
for key in path:
if not isinstance(node, dict):
return None
node = node.get(key)
return node
@classmethod
def from_payload(cls, payload: dict | None) -> "ShopRawRsp":
"""从通用解码结果构造:code/message 支持扁平与嵌套(tag0.tag0/tag0.tag1)两种形态。"""
data = payload or {}
code = 0
message = ""
head = data.get("tag0")
if isinstance(head, int):
code = int(head)
if isinstance(data.get("tag1"), str):
message = data["tag1"]
elif isinstance(head, dict):
if isinstance(head.get("tag0"), int):
code = int(head["tag0"])
if isinstance(head.get("tag1"), str):
message = head["tag1"]
return cls(code=code, message=message, payload=data)
def to_dict(self) -> dict:
return {"code": self.code, "message": self.message, "payload": self.payload}
class PlayMallAppInfo(TafStruct):
"""PlayMallUI 系接口的应用信息。
与 ShopAppInfo 字段不同(抓包实证):
tag0 sAppId, tag1 渠道号(1001), tag2 0, tag3 "", tag5 scene(7),
tag6 sourceId, tag7 ""
"""
def __init__(self):
self.sAppId: str = "huya" # tag 0
self.channelId: int = 1001 # tag 1 抓包固定 1001
self.flag2: int = 0 # tag 2 抓包固定 0
self.blank3: str = "" # tag 3 抓包为空串
self.scene: int = 7 # tag 5 抓包固定 7
self.sourceId: str = "yellowcarlist" # tag 6
self.blank7: str = "" # tag 7 抓包为空串
def write_to(self, os: TafOutputStream):
os.write_string(0, self.sAppId)
os.write_int32(1, self.channelId)
os.write_int32(2, self.flag2)
os.write_string(3, self.blank3)
os.write_int32(5, self.scene)
os.write_string(6, self.sourceId)
os.write_string(7, self.blank7)
def read_from(self, ins: TafInputStream):
pass
class GetSupplierInfoReqV5(ShopAppRequest):
"""shopMiddleUI.getSupplierInfoV5 请求。"""
def __init__(self):
super().__init__()
self.supplierId: int = 0 # tag 2
def write_to(self, os: TafOutputStream):
os.write_struct(0, self.userId)
os.write_struct(1, self.shopAppInfo)
os.write_int64(2, self.supplierId)
class AcceptHyProtocolReq(ShopAppRequest):
"""shopMiddleUI.acceptHyProtocolV5 请求(同意虎牙商城协议)。"""
def __init__(self):
super().__init__()
self.protocolVersion: int = 1 # tag 2 抓包固定 1
def write_to(self, os: TafOutputStream):
os.write_struct(0, self.userId)
os.write_struct(1, self.shopAppInfo)
os.write_int32(2, self.protocolVersion)
class CalcOrderPromotionExtra(TafStruct):
"""calcOrderPromotion tag12 子结构(抓包全零/空)。"""
def __init__(self):
self.flag0: int = 0 # tag 0
self.flag1: int = 0 # tag 1
self.flag2: int = 0 # tag 2
self.ids: list = [] # tag 3
def write_to(self, os: TafOutputStream):
os.write_int32(0, self.flag0)
os.write_int32(1, self.flag1)
os.write_int32(2, self.flag2)
os.write_list(3, self.ids)
def read_from(self, ins: TafInputStream):
pass
class CalcOrderPromotionReqV5(ShopAppRequest):
"""shopMiddleUI.calcOrderPromotion 请求。
tag7/tag8/tag9/tag10 在抓包中恒为 1/1/1/0,语义未确认,按抓包原值写出。
tag2 与 tag14 都是 spuId 去掉 ``hy-`` 前缀后的数字。
"""
def __init__(self):
super().__init__()
self.spuNum: int = 0 # tag 2
self.skuId: int = 0 # tag 3
self.itemCount: int = 1 # tag 4
self.pid: int = 0 # tag 5
self.couponUserIds: list = [] # tag 6
self.flag7: int = 1 # tag 7 抓包固定 1
self.flag8: int = 1 # tag 8 抓包固定 1
self.flag9: int = 1 # tag 9 抓包固定 1
self.flag10: int = 0 # tag 10 抓包固定 0
self.extra: CalcOrderPromotionExtra | None = None # tag 12
self.flag13: int = 0 # tag 13
self.spuNum2: int = 0 # tag 14 与 tag2 同值
self.gameId: int = 0 # tag 15 游戏 ID(和平精英 9815)
self.list16: list = [] # tag 16
def write_to(self, os: TafOutputStream):
os.write_struct(0, self.userId)
os.write_struct(1, self.shopAppInfo)
os.write_int64(2, self.spuNum)
os.write_int64(3, self.skuId)
os.write_int32(4, self.itemCount)
os.write_int64(5, self.pid)
os.write_list(6, self.couponUserIds)
os.write_int32(7, self.flag7)
os.write_int32(8, self.flag8)
os.write_int32(9, self.flag9)
os.write_int32(10, self.flag10)
os.write_struct(12, self.extra or CalcOrderPromotionExtra())
os.write_int32(13, self.flag13)
os.write_int64(14, self.spuNum2)
os.write_int64(15, self.gameId)
os.write_list(16, self.list16)
def read_from(self, ins: TafInputStream):
pass
class PlayMallQueryAllocationReq(TafStruct):
"""PlayMallUI.queryAllocation 请求(积分中心页面配额查询)。"""
def __init__(self):
self.userId = UserId() # tag 0
self.appInfo = PlayMallAppInfo() # tag 1
self.pageCode: str = "" # tag 2
self.blank3: str = "" # tag 3
self.flag4: int = 0 # tag 4
self.flag5: int = 1 # tag 5 抓包固定 1
self.flag6: int = 24 # tag 6 抓包固定 24
self.blank7: str = "" # tag 7
self.data8: bytes = b"" # tag 8 空字节数组
self.flag9: int = 0 # tag 9
self.flag10: int = 0 # tag 10
self.flags11: list[int] = [6, 19] # tag 11 抓包固定 [6, 19]
self.flag12: int = 0 # tag 12
def write_to(self, os: TafOutputStream):
os.write_struct(0, self.userId)
os.write_struct(1, self.appInfo)
os.write_string(2, self.pageCode)
os.write_string(3, self.blank3)
os.write_int32(4, self.flag4)
os.write_int32(5, self.flag5)
os.write_int32(6, self.flag6)
os.write_string(7, self.blank7)
os.write_bytes(8, self.data8)
os.write_int32(9, self.flag9)
os.write_int32(10, self.flag10)
os.write_list(11, self.flags11)
os.write_int32(12, self.flag12)
def read_from(self, ins: TafInputStream):
pass
class PromotionGoodsKey(TafStruct):
"""getMyPromotion tag6 子结构(供应商 + 商品 + 价格)。"""
def __init__(self):
self.supplierId: int = 0 # tag 0
self.spuNum: int = 0 # tag 1
self.skuId: int = 0 # tag 2
self.price: int = 0 # tag 3
self.flag4: int = 1 # tag 4 抓包固定 1
self.list5: list = [] # tag 5
self.gameId: int = 0 # tag 6
self.spuNum2: int = 0 # tag 7 与 tag1 同值
def write_to(self, os: TafOutputStream):
os.write_int64(0, self.supplierId)
os.write_int64(1, self.spuNum)
os.write_int64(2, self.skuId)
os.write_int64(3, self.price)
os.write_int32(4, self.flag4)
os.write_list(5, self.list5)
os.write_int64(6, self.gameId)
os.write_int64(7, self.spuNum2)
def read_from(self, ins: TafInputStream):
pass
class PlayMallGetMyPromotionReq(TafStruct):
"""PlayMallUI.getMyPromotion 请求(查询用户可用返利/促销)。"""
def __init__(self):
self.userId = UserId() # tag 0
self.appInfo = PlayMallAppInfo() # tag 1
self.flag2: int = 1 # tag 2 抓包固定 1
self.flag3: int = 1 # tag 3 抓包固定 1
self.flag4: int = 10 # tag 4 抓包固定 10
self.flag5: int = 0 # tag 5
self.goods: PromotionGoodsKey | None = None # tag 6
self.list7: list = [] # tag 7
self.scene: str = "shop" # tag 8 抓包固定 "shop"
self.blank9: str = "" # tag 9
self.list10: list = [] # tag 10
def write_to(self, os: TafOutputStream):
os.write_struct(0, self.userId)
os.write_struct(1, self.appInfo)
os.write_int32(2, self.flag2)
os.write_int32(3, self.flag3)
os.write_int32(4, self.flag4)
os.write_int32(5, self.flag5)
os.write_struct(6, self.goods or PromotionGoodsKey())
os.write_list(7, self.list7)
os.write_string(8, self.scene)
os.write_string(9, self.blank9)
os.write_list(10, self.list10)
def read_from(self, ins: TafInputStream):
pass
class GetUserProfileReq(TafStruct):
"""huyauserui.getUserProfile 请求(tag1 为要查询的 uid)。"""
def __init__(self):
self.userId = UserId() # tag 0
self.targetUid: int = 0 # tag 1
def write_to(self, os: TafOutputStream):
os.write_struct(0, self.userId)
os.write_int64(1, self.targetUid)
def read_from(self, ins: TafInputStream):
pass
class FollowWeChatStatusReq(TafStruct):
"""wupui.getUserFollowWeChatOfficialStatus 请求(tag1 抓包固定 1)。"""
def __init__(self):
self.userId = UserId() # tag 0
self.flag1: int = 1 # tag 1 抓包固定 1
def write_to(self, os: TafOutputStream):
os.write_struct(0, self.userId)
os.write_int32(1, self.flag1)
def read_from(self, ins: TafInputStream):
pass
def shop_spu_number(spu_id: str) -> int:
"""把商品 ID(``hy-8328015``)转成接口需要的数字形式(``8328015``)。"""
text = str(spu_id or "").strip()
if "-" in text:
text = text.rsplit("-", 1)[-1]
return int(text) if text.isdigit() else 0