From b5d8d5b5d8eaffd9ed29bdb07d8bffca765be166 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Thu, 15 Oct 2026 15:21:51 +0800 Subject: [PATCH] =?UTF-8?q?feat(huya):=20=E7=BC=BA=E7=BD=91=E9=A1=B5?= =?UTF-8?q?=E8=AE=BE=E5=A4=87=E6=80=81=E7=9A=84=20CK=20=E5=9B=9E=E9=80=80?= =?UTF-8?q?=20HTTP=20cdnws=20=E9=80=9A=E9=81=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 问题:外部 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。 --- core/huya/http_client.py | 350 ++++++++++++++++++----- tests/test_huya_http_shop_flow.py | 95 ++++++ tests/test_huya_runner_transport.py | 63 +++- web/backend/services/huya_runner_core.py | 30 +- 4 files changed, 463 insertions(+), 75 deletions(-) create mode 100644 tests/test_huya_http_shop_flow.py diff --git a/core/huya/http_client.py b/core/huya/http_client.py index 1915937..e5acf4c 100644 --- a/core/huya/http_client.py +++ b/core/huya/http_client.py @@ -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 diff --git a/tests/test_huya_http_shop_flow.py b/tests/test_huya_http_shop_flow.py new file mode 100644 index 0000000..4bd8582 --- /dev/null +++ b/tests/test_huya_http_shop_flow.py @@ -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 diff --git a/tests/test_huya_runner_transport.py b/tests/test_huya_runner_transport.py index 46fa711..667c65e 100644 --- a/tests/test_huya_runner_transport.py +++ b/tests/test_huya_runner_transport.py @@ -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"] diff --git a/web/backend/services/huya_runner_core.py b/web/backend/services/huya_runner_core.py index 2ddda68..847ab4f 100644 --- a/web/backend/services/huya_runner_core.py +++ b/web/backend/services/huya_runner_core.py @@ -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