feat(huya): 缺网页设备态的 CK 回退 HTTP cdnws 通道
问题:外部 CK(只带登录态)导入工作台后,任何任务都被「Cookie 缺少网页设备态 (sdid/udb_deviceid/udb_guiddata/udb_anobiztoken)」直接拦下,查询角色/开通都用不了。 实测结论(2026-10-15,同一张外部 CK): - 这些字段是 WSS 通道的门禁,不是业务必需 —— 同一条 CK 走 HTTP cdnws 可以正常查询 角色、商品,并走完整条开通链路(商品/小店/渠道/协议/返利/试算/同意/购买权限) - A/B 对照(同一账号只抽掉 sdid):含 sdid 服务端回包,去掉即完全不回包,说明 sdid 是 WSS 的硬门槛,而 HTTP 通道不需要 - 因此改为按能力选通道:设备态齐全走 WSS(行为不变),缺设备态回退 HTTP 并记日志 - http_client:补齐积分中心链路(get_supplier_info / accept_hy_protocol / calc_order_promotion / play_mall_get_my_promotion / play_mall_query_allocation / get_user_profile / get_follow_wechat_status);下单与支付参数对齐 2026-10-13 抓包 (appInfo.scene=7、订单 src/orderScene=4、orderType=6、jifencenter 标记、空点击序列、 m-shop.huya.com/points.html 回调),并去掉 orderType 1→10 的重试循环(有重复下单风险) - huya_runner_core:_activity_client/_shop_client 不再抛 COOKIE_INCOMPLETE,改为 返回 HTTP cdnws 客户端(方法签名与 WSS 会话保持一致,任务侧无感) 验证:tests/test_huya_http_shop_flow.py(下单/支付参数与回调域名)、 tests/test_huya_runner_transport.py(通道选择:缺设备态走 HTTP、齐全仍走 WSS); 真机 HTTP 开通链路 8 步全通(末步服务端返回业务提示「已超过该商品的购买上限」, 说明通道可用而非被丢弃)。反向验证:关掉回退即复现原报错。全量 pytest 717 passed。
This commit is contained in:
+286
-64
@@ -19,7 +19,18 @@ from collections.abc import Callable
|
||||
from typing import Any
|
||||
|
||||
from .cookie_utils import cookie_pairs, normalize_cookie_pairs, normalize_huya_cookie
|
||||
from .frame_decoder import decode_struct_bytes
|
||||
from .taf_protocol import TafInputStream, TafOutputStream, TafStruct
|
||||
|
||||
# 积分中心链路常量与 WSS 通道共用(wss_client 不反向依赖本模块,无循环导入)
|
||||
from .wss_client import (
|
||||
GAME_CATEGORY_ID_HUYA,
|
||||
JIFENCENTER_BIZ_PARAMS,
|
||||
ORDER_BIZ_TYPE,
|
||||
ORDER_SCENE,
|
||||
POINTS_PAY_CALLBACK,
|
||||
SHOP_APP_SCENE,
|
||||
)
|
||||
from .wup_protocol import WupRequest, WupResponse
|
||||
|
||||
CDNWS_HOST = "cdnws.api.huya.com"
|
||||
@@ -246,8 +257,8 @@ class HuyaHttpClient:
|
||||
return user
|
||||
|
||||
@staticmethod
|
||||
def _build_shop_app(source_id: str = "yellowcarlist", scene: int = 7):
|
||||
"""构造 HTTP 业务 ShopAppInfo。"""
|
||||
def _build_shop_app(source_id: str = "yellowcarlist", scene: int = SHOP_APP_SCENE):
|
||||
"""构造 HTTP 业务 ShopAppInfo(积分中心抓包 scene=7)。"""
|
||||
from .shop_structs import ShopAppInfo
|
||||
|
||||
info = ShopAppInfo()
|
||||
@@ -258,15 +269,12 @@ class HuyaHttpClient:
|
||||
return info
|
||||
|
||||
@staticmethod
|
||||
def _build_order_env(item_count: int = 1):
|
||||
"""构造下单/支付环境参数。"""
|
||||
import time
|
||||
def _build_order_env():
|
||||
"""构造下单/支付环境参数。
|
||||
|
||||
click_time = int(time.time() * 1000)
|
||||
return {
|
||||
"user_click_seq": f'[{{"x":258,"y":487,"time":{click_time},"id":""}}]',
|
||||
"input_1": str(item_count),
|
||||
}
|
||||
2026-10-13 积分中心抓包:仅一个空点击序列,没有旧版坐标数组与 input_1。
|
||||
"""
|
||||
return {"user_click_seq": "[]"}
|
||||
|
||||
@staticmethod
|
||||
def _build_activity_user(uid: int, cookie: str):
|
||||
@@ -913,13 +921,13 @@ class HuyaHttpClient:
|
||||
sku_id=0,
|
||||
game_id="",
|
||||
source_id="yellowcarlist",
|
||||
scene=7,
|
||||
app_scene=SHOP_APP_SCENE,
|
||||
):
|
||||
from .shop_structs import GetGoodsInfoReqV5, GoodsInfoRsp
|
||||
|
||||
req = GetGoodsInfoReqV5()
|
||||
req.userId = self._build_shop_user(uid, cookie)
|
||||
req.shopAppInfo = self._build_shop_app(source_id, scene)
|
||||
req.shopAppInfo = self._build_shop_app(source_id, app_scene)
|
||||
req.pid = pid
|
||||
req.spuId = spu_id
|
||||
req.skuId = sku_id
|
||||
@@ -942,7 +950,7 @@ class HuyaHttpClient:
|
||||
|
||||
req = ListPayChannelReq()
|
||||
req.userId = self._build_shop_user(uid, cookie)
|
||||
req.shopAppInfo = self._build_shop_app("yellowcarlist", 4)
|
||||
req.shopAppInfo = self._build_shop_app("yellowcarlist", SHOP_APP_SCENE)
|
||||
req.supplierId = int(supplier_id or 0)
|
||||
req.spuId = spu_id
|
||||
req.skuId = int(sku_id or 0)
|
||||
@@ -964,7 +972,7 @@ class HuyaHttpClient:
|
||||
|
||||
req = CheckHyProtocolReq()
|
||||
req.userId = self._build_shop_user(uid, cookie)
|
||||
req.shopAppInfo = self._build_shop_app("yellowcarlist", 4)
|
||||
req.shopAppInfo = self._build_shop_app("yellowcarlist", SHOP_APP_SCENE)
|
||||
return self.call_rpc(
|
||||
"shopMiddleUI",
|
||||
"checkHyProtocolV5",
|
||||
@@ -989,7 +997,7 @@ class HuyaHttpClient:
|
||||
|
||||
req = CheckUserBuyAuthReq()
|
||||
req.userId = self._build_shop_user(uid, cookie)
|
||||
req.shopAppInfo = self._build_shop_app("yellowcarlist", 4)
|
||||
req.shopAppInfo = self._build_shop_app("yellowcarlist", SHOP_APP_SCENE)
|
||||
req.spuId = spu_id
|
||||
req.skuId = int(sku_id or 0)
|
||||
return self.call_rpc(
|
||||
@@ -1056,7 +1064,7 @@ class HuyaHttpClient:
|
||||
|
||||
req = OrderDetailReq()
|
||||
req.userId = self._build_shop_user(uid, cookie)
|
||||
req.shopAppInfo = self._build_shop_app("yellowcarlist", 4)
|
||||
req.shopAppInfo = self._build_shop_app("yellowcarlist", SHOP_APP_SCENE)
|
||||
req.orderId = int(order_id or 0)
|
||||
return self.call_rpc(
|
||||
"shopMiddleUI",
|
||||
@@ -1080,10 +1088,16 @@ class HuyaHttpClient:
|
||||
item_count=1,
|
||||
source_id="yellowcarlist",
|
||||
game_id="",
|
||||
scene=7,
|
||||
order_type=None,
|
||||
app_scene=SHOP_APP_SCENE,
|
||||
order_scene=ORDER_SCENE,
|
||||
order_type=6,
|
||||
biz_params=JIFENCENTER_BIZ_PARAMS,
|
||||
):
|
||||
"""order_type=None 时自动尝试从 1 到 10 找到有效值"""
|
||||
"""下单(HTTP 通道),参数与 2026-10-13 积分中心抓包一致。
|
||||
|
||||
旧实现会从 orderType=1 试到 10,失败重试有重复下单风险;抓包已确认
|
||||
和平精英虚拟商品固定 orderType=6,这里改为单次调用。
|
||||
"""
|
||||
from .shop_structs import (
|
||||
CreateOrderAccountParam,
|
||||
CreateOrderExtraParam,
|
||||
@@ -1092,47 +1106,40 @@ class HuyaHttpClient:
|
||||
CreateOrderRsp,
|
||||
)
|
||||
|
||||
# 如果指定了具体值,直接试
|
||||
types_to_try = [order_type] if order_type is not None else list(range(1, 11))
|
||||
req = CreateOrderReqV5()
|
||||
req.userId = self._build_shop_user(uid, cookie)
|
||||
req.shopAppInfo = self._build_shop_app(source_id, app_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 = order_scene
|
||||
req.scene = 0
|
||||
req.sourceId = source_id
|
||||
req.orderType = order_type
|
||||
extra = CreateOrderExtraParam()
|
||||
extra.bizParams = biz_params
|
||||
req.extraParam = extra
|
||||
req.env = self._build_order_env()
|
||||
req.orderScene = order_scene
|
||||
req.promotionParam = CreateOrderPromotionParam()
|
||||
req.accountParam = CreateOrderAccountParam()
|
||||
req.bizType = ORDER_BIZ_TYPE
|
||||
req.gameCategoryId = GAME_CATEGORY_ID_HUYA
|
||||
req.ext = ""
|
||||
|
||||
last_result = None
|
||||
for ot in types_to_try:
|
||||
req = CreateOrderReqV5()
|
||||
req.userId = self._build_shop_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 = ot
|
||||
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 = ""
|
||||
|
||||
result: Any = self.call_rpc(
|
||||
"shopMiddleUI", "createOrderV5", req, CreateOrderRsp, uid, guid, cookie
|
||||
result: Any = self.call_rpc(
|
||||
"shopMiddleUI", "createOrderV5", req, CreateOrderRsp, uid, guid, cookie
|
||||
)
|
||||
if result is not None:
|
||||
self.logger(
|
||||
f" createOrderV5: code={result.code} orderId={result.orderId} msg={result.message}"
|
||||
)
|
||||
if result is not None:
|
||||
self.logger(
|
||||
f" orderType={ot}: code={result.code} orderId={result.orderId} msg={result.message}"
|
||||
)
|
||||
if result.code == 200 and result.orderId:
|
||||
self.logger(f"[✓] 找到有效 orderType={ot}")
|
||||
return result
|
||||
last_result = result
|
||||
else:
|
||||
self.logger(f" orderType={ot}: 无响应")
|
||||
return last_result
|
||||
else:
|
||||
self.logger(" createOrderV5: 无响应")
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def _normalize_pay_channel(pay_type) -> str:
|
||||
@@ -1153,27 +1160,27 @@ class HuyaHttpClient:
|
||||
pay_type=1,
|
||||
pid=0,
|
||||
source_id="yellowcarlist",
|
||||
scene=7,
|
||||
app_scene=SHOP_APP_SCENE,
|
||||
item_count=1,
|
||||
):
|
||||
"""发起支付,结构与 WSS HAR 中 payOrderSubmitV5 对齐。"""
|
||||
"""发起支付(HTTP 通道),字段与 2026-10-13 积分中心抓包一致。"""
|
||||
from .shop_structs import PayOrderRes
|
||||
|
||||
os = TafOutputStream()
|
||||
os.write_struct_begin(0)
|
||||
|
||||
os.write_struct(0, self._build_shop_user(uid, cookie))
|
||||
os.write_struct(1, self._build_shop_app(source_id, scene))
|
||||
os.write_struct(1, self._build_shop_app(source_id, app_scene))
|
||||
os.write_int64(2, order_id)
|
||||
os.write_string(3, self._normalize_pay_channel(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}"
|
||||
f"{POINTS_PAY_CALLBACK}?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_map(6, self._build_order_env())
|
||||
os.write_string(7, "null")
|
||||
os.write_string(8, "")
|
||||
|
||||
os.write_struct_end()
|
||||
|
||||
@@ -1226,6 +1233,221 @@ class HuyaHttpClient:
|
||||
result = wup_resp.readStruct("tResp", PayOrderRes)
|
||||
return result
|
||||
|
||||
# ============================================================
|
||||
# 积分中心链路新增 RPC(HTTP 通道版本,参数与 WSS 侧一致)
|
||||
# ============================================================
|
||||
|
||||
@staticmethod
|
||||
def _raw_from_wup(wup_resp: WupResponse):
|
||||
"""已解码的 WUP 响应 → ShopRawRsp(未逐字段建模的接口用)。"""
|
||||
from .shop_structs import ShopRawRsp
|
||||
|
||||
data = wup_resp.newdata.get("tRsp") or wup_resp.newdata.get("tResp")
|
||||
payload = decode_struct_bytes(data) if isinstance(data, bytes) else {}
|
||||
return ShopRawRsp.from_payload(payload)
|
||||
|
||||
def get_supplier_info(
|
||||
self,
|
||||
uid: int,
|
||||
guid: str,
|
||||
cookie: str,
|
||||
supplier_id: int = 87401,
|
||||
timeout: float = 15.0,
|
||||
):
|
||||
"""shopMiddleUI.getSupplierInfoV5(供应商/小店信息)。"""
|
||||
from .shop_structs import GetSupplierInfoReqV5
|
||||
|
||||
req = GetSupplierInfoReqV5()
|
||||
req.userId = self._build_shop_user(uid, cookie)
|
||||
req.shopAppInfo = self._build_shop_app("yellowcarlist", SHOP_APP_SCENE)
|
||||
req.supplierId = int(supplier_id or 0)
|
||||
resp = self.call_rpc(
|
||||
"shopMiddleUI",
|
||||
"getSupplierInfoV5",
|
||||
req,
|
||||
None,
|
||||
uid,
|
||||
guid,
|
||||
cookie,
|
||||
timeout=timeout,
|
||||
)
|
||||
return self._raw_from_wup(resp) if isinstance(resp, WupResponse) else None
|
||||
|
||||
def accept_hy_protocol(
|
||||
self, uid: int, guid: str, cookie: str, timeout: float = 15.0
|
||||
):
|
||||
"""shopMiddleUI.acceptHyProtocolV5(同意商城协议)。"""
|
||||
from .shop_structs import AcceptHyProtocolReq, ShopCodeRsp
|
||||
|
||||
req = AcceptHyProtocolReq()
|
||||
req.userId = self._build_shop_user(uid, cookie)
|
||||
req.shopAppInfo = self._build_shop_app("yellowcarlist", SHOP_APP_SCENE)
|
||||
return self.call_rpc(
|
||||
"shopMiddleUI",
|
||||
"acceptHyProtocolV5",
|
||||
req,
|
||||
ShopCodeRsp,
|
||||
uid,
|
||||
guid,
|
||||
cookie,
|
||||
timeout=timeout,
|
||||
)
|
||||
|
||||
def calc_order_promotion(
|
||||
self,
|
||||
uid: int,
|
||||
guid: str,
|
||||
cookie: str,
|
||||
pid: int,
|
||||
spu_id: str,
|
||||
sku_id: int = 0,
|
||||
item_count: int = 1,
|
||||
game_id: int = 9815,
|
||||
timeout: float = 15.0,
|
||||
):
|
||||
"""shopMiddleUI.calcOrderPromotion(下单前试算)。"""
|
||||
from .shop_structs import CalcOrderPromotionReqV5, shop_spu_number
|
||||
|
||||
spu_num = shop_spu_number(spu_id)
|
||||
req = CalcOrderPromotionReqV5()
|
||||
req.userId = self._build_shop_user(uid, cookie)
|
||||
req.shopAppInfo = self._build_shop_app("yellowcarlist", SHOP_APP_SCENE)
|
||||
req.spuNum = spu_num
|
||||
req.skuId = int(sku_id or 0)
|
||||
req.itemCount = int(item_count or 1)
|
||||
req.pid = int(pid or 0)
|
||||
req.spuNum2 = spu_num
|
||||
req.gameId = int(game_id or 0)
|
||||
resp = self.call_rpc(
|
||||
"shopMiddleUI",
|
||||
"calcOrderPromotion",
|
||||
req,
|
||||
None,
|
||||
uid,
|
||||
guid,
|
||||
cookie,
|
||||
timeout=timeout,
|
||||
)
|
||||
return self._raw_from_wup(resp) if isinstance(resp, WupResponse) else None
|
||||
|
||||
def play_mall_query_allocation(
|
||||
self,
|
||||
uid: int,
|
||||
guid: str,
|
||||
cookie: str,
|
||||
source_id: str = "yellowcarlist",
|
||||
page_code: str = "heping_baodian_credit_center_page",
|
||||
timeout: float = 15.0,
|
||||
):
|
||||
"""PlayMallUI.queryAllocation(积分中心配额查询)。"""
|
||||
from .shop_structs import PlayMallQueryAllocationReq
|
||||
|
||||
req = PlayMallQueryAllocationReq()
|
||||
req.userId = self._build_shop_user(uid, cookie)
|
||||
req.pageCode = page_code
|
||||
req.appInfo.sourceId = source_id
|
||||
resp = self.call_rpc(
|
||||
"PlayMallUI",
|
||||
"queryAllocation",
|
||||
req,
|
||||
None,
|
||||
uid,
|
||||
guid,
|
||||
cookie,
|
||||
timeout=timeout,
|
||||
)
|
||||
return self._raw_from_wup(resp) if isinstance(resp, WupResponse) else None
|
||||
|
||||
def play_mall_get_my_promotion(
|
||||
self,
|
||||
uid: int,
|
||||
guid: str,
|
||||
cookie: str,
|
||||
spu_id: str,
|
||||
sku_id: int = 0,
|
||||
price: int = 0,
|
||||
supplier_id: int = 87401,
|
||||
game_id: int = 9815,
|
||||
source_id: str = "yellowcarlist",
|
||||
timeout: float = 15.0,
|
||||
):
|
||||
"""PlayMallUI.getMyPromotion(返利/促销查询)。"""
|
||||
from .shop_structs import (
|
||||
PlayMallGetMyPromotionReq,
|
||||
PromotionGoodsKey,
|
||||
shop_spu_number,
|
||||
)
|
||||
|
||||
spu_num = shop_spu_number(spu_id)
|
||||
req = PlayMallGetMyPromotionReq()
|
||||
req.userId = self._build_shop_user(uid, cookie)
|
||||
req.appInfo.sourceId = source_id
|
||||
goods = PromotionGoodsKey()
|
||||
goods.supplierId = int(supplier_id or 0)
|
||||
goods.spuNum = spu_num
|
||||
goods.skuId = int(sku_id or 0)
|
||||
goods.price = int(price or 0)
|
||||
goods.gameId = int(game_id or 0)
|
||||
goods.spuNum2 = spu_num
|
||||
req.goods = goods
|
||||
resp = self.call_rpc(
|
||||
"PlayMallUI",
|
||||
"getMyPromotion",
|
||||
req,
|
||||
None,
|
||||
uid,
|
||||
guid,
|
||||
cookie,
|
||||
timeout=timeout,
|
||||
)
|
||||
return self._raw_from_wup(resp) if isinstance(resp, WupResponse) else None
|
||||
|
||||
def get_user_profile(
|
||||
self,
|
||||
uid: int,
|
||||
guid: str,
|
||||
cookie: str,
|
||||
target_uid: int = 0,
|
||||
timeout: float = 15.0,
|
||||
):
|
||||
"""huyauserui.getUserProfile(单个用户资料,资料在 tag0.tag0)。"""
|
||||
from .shop_structs import GetUserProfileReq
|
||||
|
||||
req = GetUserProfileReq()
|
||||
req.userId = self._build_shop_user(uid, cookie)
|
||||
req.targetUid = int(target_uid or uid or 0)
|
||||
resp = self.call_rpc(
|
||||
"huyauserui",
|
||||
"getUserProfile",
|
||||
req,
|
||||
None,
|
||||
uid,
|
||||
guid,
|
||||
cookie,
|
||||
timeout=timeout,
|
||||
)
|
||||
return self._raw_from_wup(resp) if isinstance(resp, WupResponse) else None
|
||||
|
||||
def get_follow_wechat_status(
|
||||
self, uid: int, guid: str, cookie: str, timeout: float = 15.0
|
||||
):
|
||||
"""wupui.getUserFollowWeChatOfficialStatus(是否关注公众号)。"""
|
||||
from .shop_structs import FollowWeChatStatusReq
|
||||
|
||||
req = FollowWeChatStatusReq()
|
||||
req.userId = self._build_shop_user(uid, cookie)
|
||||
resp = self.call_rpc(
|
||||
"wupui",
|
||||
"getUserFollowWeChatOfficialStatus",
|
||||
req,
|
||||
None,
|
||||
uid,
|
||||
guid,
|
||||
cookie,
|
||||
timeout=timeout,
|
||||
)
|
||||
return self._raw_from_wup(resp) if isinstance(resp, WupResponse) else None
|
||||
|
||||
|
||||
def _self_check():
|
||||
uid = 1199647239697
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
"""HTTP cdnws 通道的下单/支付参数对齐(2026-10-13 积分中心抓包)。
|
||||
|
||||
外部 CK(只有登录态、无网页设备态)会走 HTTP 通道,因此 HTTP 侧的参数必须与
|
||||
抓包一致:jifencenter 标记、空点击序列、m-shop.huya.com 回调、appInfo.scene 与
|
||||
订单 src/orderScene 分离。
|
||||
"""
|
||||
|
||||
from core.huya.http_client import HuyaHttpClient
|
||||
|
||||
|
||||
def _recording_client():
|
||||
client = HuyaHttpClient(logger=lambda _message: None)
|
||||
recorded: list[tuple[str, str, object]] = []
|
||||
|
||||
def fake_call_rpc(
|
||||
service, method, req_struct, rsp_class=None, uid=0, guid="", cookie="", **kwargs
|
||||
):
|
||||
recorded.append((service, method, req_struct))
|
||||
|
||||
client.call_rpc = fake_call_rpc # type: ignore[method-assign]
|
||||
return client, recorded
|
||||
|
||||
|
||||
def test_http_create_order_matches_points_capture():
|
||||
client, recorded = _recording_client()
|
||||
|
||||
client.create_order(
|
||||
uid=1199665170519,
|
||||
guid="",
|
||||
cookie="yyuid=1199665170519",
|
||||
pid=1199650619883,
|
||||
spu_id="hy-8328015",
|
||||
sku_id=7756811,
|
||||
)
|
||||
|
||||
service, method, req = recorded[-1]
|
||||
assert (service, method) == ("shopMiddleUI", "createOrderV5")
|
||||
assert req.shopAppInfo.scene == 7 # appInfo scene
|
||||
assert req.src == 4 # 订单侧 scene
|
||||
assert req.orderScene == 4
|
||||
assert req.orderType == 6
|
||||
assert req.extraParam.bizParams == '{ "jifencenter": 1 }'
|
||||
assert req.env == {"user_click_seq": "[]"}
|
||||
assert req.bizType == 5
|
||||
assert req.gameCategoryId == 507
|
||||
|
||||
|
||||
def test_http_get_goods_info_uses_app_scene():
|
||||
client, recorded = _recording_client()
|
||||
|
||||
client.get_goods_info(
|
||||
uid=1199665170519,
|
||||
guid="",
|
||||
cookie="yyuid=1199665170519",
|
||||
pid=1199650619883,
|
||||
spu_id="hy-8328015",
|
||||
)
|
||||
|
||||
service, method, req = recorded[-1]
|
||||
assert (service, method) == ("shopMiddleUI", "getGoodsInfoV5")
|
||||
assert req.shopAppInfo.scene == 7
|
||||
assert req.pid == 1199650619883
|
||||
assert req.spuId == "hy-8328015"
|
||||
|
||||
|
||||
def test_http_pay_order_submit_points_callback_and_env(monkeypatch):
|
||||
captured: dict[str, bytes] = {}
|
||||
|
||||
class _StopAfterCapture(Exception):
|
||||
pass
|
||||
|
||||
def fake_urlopen(request, timeout=None):
|
||||
captured["body"] = request.data
|
||||
raise _StopAfterCapture
|
||||
|
||||
monkeypatch.setattr("urllib.request.urlopen", fake_urlopen)
|
||||
client = HuyaHttpClient(logger=lambda _message: None)
|
||||
|
||||
result = client.pay_order_submit(
|
||||
uid=1199665170519,
|
||||
guid="",
|
||||
cookie="yyuid=1199665170519",
|
||||
order_id=11339649,
|
||||
pay_type="Zfb",
|
||||
pid=1199650619883,
|
||||
)
|
||||
|
||||
assert result is None # 捕获到请求即中断,不关心响应
|
||||
body = captured["body"]
|
||||
assert (
|
||||
b"https://m-shop.huya.com/points.html#/consumer/paycallback"
|
||||
b"?orderId=11339649&pid=1199650619883&sourceId=yellowcarlist"
|
||||
) in body
|
||||
assert b"user_click_seq" in body
|
||||
assert b"input_1" not in body
|
||||
@@ -1,7 +1,29 @@
|
||||
"""虎牙精英宝典传输选择的联调前置检查。"""
|
||||
"""虎牙传输选择:网页设备态齐全走 WSS,缺设备态回退 HTTP cdnws。
|
||||
|
||||
背景(2026-10-15 实测,外部 CK 导入后):只带登录态、缺 sdid/udb_deviceid/
|
||||
udb_guiddata/udb_anobiztoken 的 Cookie 在 WSS 上会被服务端静默丢弃业务请求
|
||||
(wsLaunch / 业务 RPC 无响应),同一条 Cookie 走 HTTP cdnws 通道可正常查询。
|
||||
"""
|
||||
|
||||
from core.huya.http_client import HuyaHttpClient
|
||||
from web.backend.services.huya_runner_core import HuyaBatchRunnerCore
|
||||
|
||||
LOGIN_ONLY_COOKIE = (
|
||||
"udb_cred=cred; udb_biztoken=biz; udb_uid=1199659425268; yyuid=1199659425268"
|
||||
)
|
||||
COMPLETE_COOKIE = (
|
||||
"udb_cred=cred; udb_biztoken=biz; udb_uid=1199659425268; yyuid=1199659425268; "
|
||||
"sdid=sdid; udb_deviceid=w_1234567890123456789; udb_guiddata=guiddata; "
|
||||
"udb_anobiztoken=anobiz"
|
||||
)
|
||||
|
||||
|
||||
def _runner() -> HuyaBatchRunnerCore:
|
||||
# 这两个方法只用 db 无关的状态,直接构造即可
|
||||
return HuyaBatchRunnerCore(
|
||||
db=None, batch_id="test-batch", task_type="query_game_name"
|
||||
)
|
||||
|
||||
|
||||
def test_minimal_app_cookie_reports_only_missing_web_guid():
|
||||
cookie = (
|
||||
@@ -40,3 +62,42 @@ def test_script_only_fields_do_not_block_wss():
|
||||
|
||||
assert HuyaBatchRunnerCore._web_wss_cookie_missing(cookie) == ()
|
||||
assert HuyaBatchRunnerCore._has_web_wss_cookie(cookie)
|
||||
|
||||
|
||||
def test_login_only_cookie_falls_back_to_http_channel():
|
||||
"""缺网页设备态不再抛 COOKIE_INCOMPLETE,改用 HTTP cdnws 客户端。"""
|
||||
runner = _runner()
|
||||
|
||||
activity = runner._activity_client(1199659425268, LOGIN_ONLY_COOKIE)
|
||||
shop = runner._shop_client(1199659425268, LOGIN_ONLY_COOKIE)
|
||||
|
||||
assert isinstance(activity, HuyaHttpClient)
|
||||
assert isinstance(shop, HuyaHttpClient)
|
||||
# HTTP 客户端具备任务侧需要的同名方法(含新增的积分中心 RPC)
|
||||
for method in (
|
||||
"check_user_bind_game_account",
|
||||
"get_act_task_detail",
|
||||
"query_user_score",
|
||||
"get_goods_info",
|
||||
"get_supplier_info",
|
||||
"accept_hy_protocol",
|
||||
"calc_order_promotion",
|
||||
"create_order",
|
||||
"pay_order_submit",
|
||||
):
|
||||
assert callable(getattr(activity, method)), method
|
||||
|
||||
|
||||
def test_complete_cookie_keeps_using_wss(monkeypatch):
|
||||
opened: list[str] = []
|
||||
|
||||
def fake_open(self, uid: int, cookie: str, kind: str):
|
||||
opened.append(kind)
|
||||
return f"wss-{kind}"
|
||||
|
||||
monkeypatch.setattr(HuyaBatchRunnerCore, "_open_wss_session", fake_open)
|
||||
runner = _runner()
|
||||
|
||||
assert runner._activity_client(1199659425268, COMPLETE_COOKIE) == "wss-activity"
|
||||
assert runner._shop_client(1199659425268, COMPLETE_COOKIE) == "wss-shop"
|
||||
assert opened == ["activity", "shop"]
|
||||
|
||||
@@ -94,19 +94,30 @@ class HuyaBatchRunnerCore:
|
||||
return bucket
|
||||
|
||||
def _activity_client(self, uid: int, cookie: str):
|
||||
"""创建活动 WSS 会话;网页 Cookie 不完整时直接阻断任务。"""
|
||||
"""活动通道客户端:网页设备态齐全走 WSS,否则回退 HTTP cdnws。
|
||||
|
||||
2026-10-15 实测(外部 CK 导入后):只带登录态、缺 sdid/udb_deviceid/
|
||||
udb_guiddata/udb_anobiztoken 的 Cookie 在 WSS 上会被服务端静默丢弃业务请求
|
||||
(wsLaunch 或业务 RPC 无响应);同一条 Cookie 走 HTTP cdnws 通道可以正常
|
||||
查询角色与商品。因此这里不再直接失败,而是按能力选通道。
|
||||
"""
|
||||
missing = self._web_wss_cookie_missing(cookie)
|
||||
if missing:
|
||||
detail = ", ".join(missing)
|
||||
self._push_log(
|
||||
"error",
|
||||
f"[{uid}] Cookie 缺少网页设备态({detail}),已停止活动 HTTP 回退",
|
||||
"info",
|
||||
f"[{uid}] Cookie 缺少网页设备态({', '.join(missing)}),改用 HTTP cdnws 通道",
|
||||
)
|
||||
raise RuntimeError(f"COOKIE_INCOMPLETE: 缺少网页设备态({detail})")
|
||||
return self._http_client()
|
||||
session = self._open_wss_session(uid, cookie, "activity")
|
||||
self._push_log("info", f"[{uid}] 活动链路使用 WSS 会话")
|
||||
return session
|
||||
|
||||
def _http_client(self):
|
||||
"""HTTP cdnws 通道客户端(无需网页设备态,方法签名与 WSS 会话保持一致)。"""
|
||||
from core.huya.http_client import HuyaHttpClient
|
||||
|
||||
return HuyaHttpClient(logger=lambda message: self._push_log("info", message))
|
||||
|
||||
@staticmethod
|
||||
def _web_wss_cookie_missing(cookie: str) -> tuple[str, ...]:
|
||||
"""返回 WSS 前置字段中缺失的名称,不输出任何 Cookie 值。
|
||||
@@ -134,15 +145,14 @@ class HuyaBatchRunnerCore:
|
||||
return not cls._web_wss_cookie_missing(cookie)
|
||||
|
||||
def _shop_client(self, uid: int, cookie: str):
|
||||
"""创建商城 WSS 会话;网页 Cookie 不完整时直接阻断任务。"""
|
||||
"""商城通道客户端:网页设备态齐全走 WSS,否则回退 HTTP cdnws(原因同活动通道)。"""
|
||||
missing = self._web_wss_cookie_missing(cookie)
|
||||
if missing:
|
||||
detail = ", ".join(missing)
|
||||
self._push_log(
|
||||
"error",
|
||||
f"[{uid}] Cookie 缺少网页设备态({detail}),已停止商城 HTTP 回退",
|
||||
"info",
|
||||
f"[{uid}] Cookie 缺少网页设备态({', '.join(missing)}),改用 HTTP cdnws 通道",
|
||||
)
|
||||
raise RuntimeError(f"COOKIE_INCOMPLETE: 缺少网页设备态({detail})")
|
||||
return self._http_client()
|
||||
session = self._open_wss_session(uid, cookie, "shop")
|
||||
self._push_log("info", f"[{uid}] 商城链路使用 WSS 会话")
|
||||
return session
|
||||
|
||||
Reference in New Issue
Block a user