diff --git a/core/huya/app_login.py b/core/huya/app_login.py index 2f2a676..4e648cc 100644 --- a/core/huya/app_login.py +++ b/core/huya/app_login.py @@ -475,6 +475,30 @@ class HuyaAppPasswordLogin: present = {key for key, value in cookie_pairs(cookie) if value} return tuple(key for key in WEB_DEVICE_COOKIE_KEYS if key not in present) + @staticmethod + def _supplement_anonymous_device( + device_info: Mapping[str, object], + *, + proxies: Mapping[str, str] | None = None, + ) -> dict: + """用匿名设备态协议尽力补齐 4 个字段(udb_deviceid/guiddata/biztoken/anouid)。 + + 只 ``setdefault``,不覆盖 hydevice 或显式传入的真实值;获取失败只 + 告警,不阻断登录(字段不全仍保存,风控风险由调用方承担)。 + """ + from .anon_device import AnonymousDeviceError, fetch_anonymous_device_state + + merged = dict(device_info) + try: + anon = fetch_anonymous_device_state(proxies=proxies) + except AnonymousDeviceError as exc: + logger.warning("[huya-app] 匿名设备态获取失败(不阻断登录): {}", exc) + return merged + for key, value in anon.items(): + merged.setdefault(key, value) + logger.info("[huya-app] 匿名设备态协议补齐字段: {}", ", ".join(sorted(anon))) + return merged + @staticmethod def _merge_web_cookie( session: requests.Session, @@ -667,6 +691,11 @@ class HuyaAppPasswordLogin: "udb_anobiztoken", "udb_deviceid", }: merge_device_info.setdefault(key, value) + # 匿名设备态协议补齐(middle + anonymousLogin 签发 4 字段), + # 只 setdefault 不覆盖真实值;失败不阻断。 + merge_device_info = self._supplement_anonymous_device( + merge_device_info, proxies=self.proxies + ) logger.info("[huya-app] cred 已获取,开始二维码绑定流程") pc = QrRole(pc=True, sdid=sdid, proxies=self.proxies) ph = QrRole( @@ -793,15 +822,9 @@ class HuyaAppPasswordLogin: missing_web = self._missing_web_device_cookie(cookie_str) if missing_web: missing_text = ", ".join(missing_web) - logger.error( - f"[huya-app] 账号 {acct} 登录凭据有效,但网页设备态不完整,缺少: {missing_text}" - ) - return HuyaLoginResult( - success=False, - message=f"COOKIE_INCOMPLETE: 缺少网页设备态({missing_text}),请在真实虎牙网页/WebView完成一次访问后重试", - code="COOKIE_INCOMPLETE", - sdid=sdid, - context=pc.context, + logger.warning( + f"[huya-app] 账号 {acct} 登录凭据有效,但网页设备态不完整," + f"缺少: {missing_text}(字段不全仍保存,风控风险自担)" ) if "udb_cred" not in cookie_str or "yyuid" not in cookie_str: return HuyaLoginResult( diff --git a/docs/HUYA_COOKIE合并审计与来源矩阵.md b/docs/HUYA_COOKIE合并审计与来源矩阵.md index 0ef0659..14c6e8e 100644 --- a/docs/HUYA_COOKIE合并审计与来源矩阵.md +++ b/docs/HUYA_COOKIE合并审计与来源矩阵.md @@ -56,7 +56,11 @@ 因此 App 的 `/web/cookie/verify` 不能兑换出完整网页 CK。网页设备字段来自浏览器此前 持久化的 Cookie/Storage 和页面脚本运行态;当前 Python App 登录链不应声称能凭空补齐它们, -也不在脚本中外挂浏览器。缺失时返回 `COOKIE_INCOMPLETE`,精英宝典任务不再回退 HTTP。 +也不在脚本中外挂浏览器。2026-09-01 起接入匿名设备态协议 +(`core/huya/anon_device.py`,middle + anonymousLogin)补 `udb_deviceid`/ +`udb_guiddata`/`udb_anobiztoken`/`udb_anouid` 四个字段;其余脚本字段(`guid`/ +`game_did`/`_qimei_uuid42`/`__yamid_new`)缺失时不再拒绝登录,改为警告并保存 +(风控风险自担);精英宝典任务不再回退 HTTP。 ### 运行时复核(2026-09-01) @@ -67,7 +71,8 @@ - `__SDID__` 和 `__HDID__` 正常返回; - Cookie 桥只观察到脚本自身的 `udb_appid`/临时 `sdid`,没有生成 `guid`、QIMEI、 `udb_guiddata`、`udb_deviceid` 或 `udb_anobiztoken`; -- 因此这次改动不会改变 `COOKIE_INCOMPLETE` 判定,也不会用随机值补齐字段。 +- 因此这次改动此前不会改变 `COOKIE_INCOMPLETE` 判定,也不会用随机值补齐字段; + 2026-09-01 登录链接入匿名设备态协议后可补齐 4 个服务端签发字段(见下节)。 从线上下载的 `hydevice-prod-1.2.51-min.js` 在现有 Node 仿真环境中还依赖未实现的 浏览器 API(首个错误为 `window[t(...)] is not a function`),没有将其替换进生产链。 @@ -82,8 +87,8 @@ 成功响应;`df/collect` 只下发 `sdid`、`hdid`。 - `7.5-短信登录-滑块.har`:`web/cookie/verify` 前后的请求也没有新增这 7 个字段。 -因此当前日志中的缺失列表是协议事实,不是 Cookie 合并阶段丢失;下一步只有取得 -同一网页会话的真实持久化设备态,或找到服务端正式签发接口,才能继续补齐。 +因此当前日志中的缺失列表是协议事实,不是 Cookie 合并阶段丢失;服务端正式签发 +接口已找到并接入登录链(middle + anonymousLogin,见“匿名流程解密”节)。 ### 真实浏览器实测(2026-09-01,未登录) diff --git a/docs/HUYA精英宝典联调指南.md b/docs/HUYA精英宝典联调指南.md index eb2bbf5..64ee88f 100644 --- a/docs/HUYA精英宝典联调指南.md +++ b/docs/HUYA精英宝典联调指南.md @@ -17,8 +17,10 @@ 旧登录结果中的 Cookie 缺少网页设备态,已不再作为成功登录或任务链路使用。 纯 App/HTTP 登录不会由 `web/cookie/verify` 下发网页设备 Cookie。浏览器只用于 -人工调试和抓取来源,未集成到脚本;当前 App 登录缺少这些字段时直接返回 -`COOKIE_INCOMPLETE`,不会伪造或自动启动浏览器。 +人工调试和抓取来源,未集成到脚本;登录链已接入匿名设备态协议 +(`core/huya/anon_device.py`)补 `udb_deviceid`/`udb_guiddata`/`udb_anobiztoken`/ +`udb_anouid`,剩余脚本字段(`guid`/`game_did`/`_qimei_uuid42`/`__yamid_new`) +缺失时警告并保存,不伪造、不启动浏览器。 ## 推荐联调顺序 @@ -38,16 +40,19 @@ 执行器只检查字段是否存在,不记录字段值: ```text -必要字段:udb_cred、udb_biztoken、udb_uid、yyuid、sdid、guid、udb_guiddata、udb_deviceid、game_did、_qimei_uuid42、udb_anobiztoken、__yamid_new +必要字段:udb_cred、udb_biztoken、udb_uid、yyuid、sdid、udb_deviceid、udb_guiddata、udb_anobiztoken ``` - 字段齐全:尝试活动或商城 WSS。 -- 缺少字段:App 登录返回 `COOKIE_INCOMPLETE`,任务日志列出缺失字段并停止,不创建 HTTP 客户端,例如: +- 缺少字段:任务日志列出缺失字段并停止,不创建 HTTP 客户端,例如: ```text -Cookie 缺少网页设备态(guid, udb_guiddata),已停止活动 HTTP 回退 +Cookie 缺少网页设备态(udb_deviceid, udb_guiddata, udb_anobiztoken),已停止活动 HTTP 回退 ``` +`guid`/`game_did`/`_qimei_uuid42`/`__yamid_new` 四个脚本字段无服务端签发接口, +登录时缺失只警告不阻断保存,WSS 判定也不再要求(风控风险自担)。 + - WSS 握手、`wsLaunch`、`getConfig` 或业务 RPC 任一步失败:任务直接失败并记录具体阶段,便于修复 Cookie 或协议参数。 ## WSS 专项验收 diff --git a/tests/test_huya_app_login.py b/tests/test_huya_app_login.py index 70a83a5..f546658 100644 --- a/tests/test_huya_app_login.py +++ b/tests/test_huya_app_login.py @@ -147,6 +147,46 @@ def test_app_login_cookie_contains_web_session_shape(): assert expected <= keys +def test_supplement_anonymous_device_adds_four_fields(monkeypatch): + anon = { + "udb_deviceid": "w_1147246591246680065", + "udb_guiddata": "a" * 32, + "udb_anobiztoken": "B" * 344, + "udb_anouid": "1471255709907", + } + monkeypatch.setattr( + "core.huya.anon_device.fetch_anonymous_device_state", lambda **kw: anon + ) + merged = HuyaAppPasswordLogin._supplement_anonymous_device({"guid": "g" * 32}) + assert merged["udb_deviceid"] == anon["udb_deviceid"] + assert merged["udb_guiddata"] == anon["udb_guiddata"] + assert merged["udb_anobiztoken"] == anon["udb_anobiztoken"] + assert merged["udb_anouid"] == anon["udb_anouid"] + assert merged["guid"] == "g" * 32 # 已有显式值不被覆盖 + + +def test_supplement_anonymous_device_keeps_explicit_values(monkeypatch): + anon = {"udb_deviceid": "w_9999999999999999999", "udb_anobiztoken": "B" * 344} + monkeypatch.setattr( + "core.huya.anon_device.fetch_anonymous_device_state", lambda **kw: anon + ) + merged = HuyaAppPasswordLogin._supplement_anonymous_device( + {"udb_deviceid": "w_1111111111111111111"} + ) + assert merged["udb_deviceid"] == "w_1111111111111111111" # setdefault 语义 + + +def test_supplement_anonymous_device_failure_does_not_raise(monkeypatch): + def boom(**kw): + from core.huya.anon_device import AnonymousDeviceError + + raise AnonymousDeviceError("network down") + + monkeypatch.setattr("core.huya.anon_device.fetch_anonymous_device_state", boom) + merged = HuyaAppPasswordLogin._supplement_anonymous_device({"guid": "g"}) + assert merged == {"guid": "g"} + + class TestHuyaAppLogin: def setup_method(self): self.engine = create_engine("sqlite://") diff --git a/tests/test_huya_runner_transport.py b/tests/test_huya_runner_transport.py index abbe39f..a8edec9 100644 --- a/tests/test_huya_runner_transport.py +++ b/tests/test_huya_runner_transport.py @@ -10,8 +10,7 @@ def test_minimal_app_cookie_reports_only_missing_web_guid(): ) assert HuyaBatchRunnerCore._web_wss_cookie_missing(cookie) == ( - "guid", "udb_guiddata", "udb_deviceid", "game_did", - "_qimei_uuid42", "udb_anobiztoken", "__yamid_new", + "udb_deviceid", "udb_guiddata", "udb_anobiztoken", ) assert not HuyaBatchRunnerCore._has_web_wss_cookie(cookie) @@ -26,3 +25,16 @@ def test_complete_web_cookie_enables_wss_without_logging_values(): assert HuyaBatchRunnerCore._web_wss_cookie_missing(cookie) == () assert HuyaBatchRunnerCore._has_web_wss_cookie(cookie) + + +def test_script_only_fields_do_not_block_wss(): + """guid/_qimei_uuid42/__yamid_new/game_did 无服务端签发时不再阻断 WSS。""" + cookie = ( + "udb_cred=cred; udb_biztoken=biz; udb_uid=1199659425268; " + "yyuid=1199659425268; sdid=sdid; " + "udb_deviceid=w_1234567890123456789; udb_guiddata=guiddata; " + "udb_anobiztoken=anobiz" + ) + + assert HuyaBatchRunnerCore._web_wss_cookie_missing(cookie) == () + assert HuyaBatchRunnerCore._has_web_wss_cookie(cookie) \ No newline at end of file diff --git a/web/backend/services/huya_runner_core.py b/web/backend/services/huya_runner_core.py index dc6bc90..3fb0377 100644 --- a/web/backend/services/huya_runner_core.py +++ b/web/backend/services/huya_runner_core.py @@ -84,11 +84,16 @@ class HuyaBatchRunnerCore: @staticmethod def _web_wss_cookie_missing(cookie: str) -> tuple[str, ...]: - """返回 WSS 前置字段中缺失的名称,不输出任何 Cookie 值。""" + """返回 WSS 前置字段中缺失的名称,不输出任何 Cookie 值。 + + 必填集 = 认证字段 + sdid + 匿名设备态协议可签发的字段 + (udb_deviceid/udb_guiddata/udb_anobiztoken,见 core/huya/anon_device.py)。 + guid/_qimei_uuid42/__yamid_new/game_did 目前无服务端签发接口, + 不阻断 WSS(风控风险自担)。 + """ required = ( "udb_cred", "udb_biztoken", "udb_uid", "yyuid", "sdid", - "guid", "udb_guiddata", "udb_deviceid", "game_did", - "_qimei_uuid42", "udb_anobiztoken", "__yamid_new", + "udb_deviceid", "udb_guiddata", "udb_anobiztoken", ) return tuple(key for key in required if not cookie_value(cookie, key))