diff --git a/core/huya/elite_session.py b/core/huya/elite_session.py index 5c6057d..31f2449 100644 --- a/core/huya/elite_session.py +++ b/core/huya/elite_session.py @@ -12,6 +12,7 @@ from collections.abc import Callable from typing import Literal from .http_client import HuyaHttpClient, generate_http_baseinfo +from .web_cookie_fields import missing_web_cookie_fields from .wss_client import ACTIVITY_WS_HOST, SHOP_BASEINFO, SHOP_WS_HOST, HuyaWssClient @@ -28,6 +29,7 @@ class HuyaEliteWssSession: ): self.uid = int(uid or 0) self.cookie = cookie or "" + self._original_cookie = self.cookie self.guid = HuyaHttpClient._resolve_cookie_guid(self.cookie) self.kind = kind self.logger = logger or (lambda _message: None) @@ -49,6 +51,39 @@ class HuyaEliteWssSession: self.client = HuyaWssClient(baseinfo=baseinfo, logger=self.logger) try: + if kind == "activity" and missing_web_cookie_fields(self.cookie): + # 网页设备态缺核心字段(guid/_qimei_uuid42/__yamid_new/game_did 等): + # 先跑一次 wsLaunch 拿到服务端认可的会话 guid,写回 cookie 补齐后再建会话。 + # 否则 getConfig / 业务 RPC 无响应(2026-09-01 实测)。 + probe = HuyaWssClient(baseinfo=baseinfo, logger=self.logger) + try: + self.loop.run_until_complete( + probe.connect(host=connect_host, origin=origin, cookie=self.cookie) + ) + self.loop.run_until_complete( + probe.call_ws_launch_activity(self.uid, self.guid) + ) + launch_guid = probe._launch_guid or "" + self.logger( + f"[WSS] 预取会话 guid={launch_guid or '(空)'},补齐网页 Cookie 字段" + ) + finally: + if probe.ws is not None: + try: + self.loop.run_until_complete(probe.disconnect()) + except Exception: # noqa: BLE001 清理探针连接失败不影响主流程 + self.logger("[WSS] 预取探针清理失败") + from .web_cookie_fields import fill_web_cookie_fields + + self.cookie = fill_web_cookie_fields( + self.cookie, launch_guid, self.uid + ) + self.guid = HuyaHttpClient._resolve_cookie_guid(self.cookie) + baseinfo = urllib.parse.unquote( + generate_http_baseinfo(self.uid, self.guid, "") + ) + self.client = HuyaWssClient(baseinfo=baseinfo, logger=self.logger) + self.loop.run_until_complete( self.client.connect(host=connect_host, origin=origin, cookie=self.cookie) ) @@ -68,6 +103,17 @@ class HuyaEliteWssSession: raise RuntimeError("虎牙 WSS 会话已关闭") return self.loop.run_until_complete(coroutine) + def _resolve_cookie(self, cookie: str) -> str: + """业务 RPC 统一使用补齐后的网页 Cookie。 + + 任务侧往往传原始登录态 Cookie(缺网页设备字段);初始化阶段已经 + wsLaunch 取回会话 guid 并补齐字段(见 __init__),这里把缺字段的 + 输入替换为补齐后的 self.cookie。 + """ + if not cookie or cookie == self._original_cookie or missing_web_cookie_fields(cookie): + return self.cookie + return cookie + def close(self): if self._closed: return @@ -89,39 +135,56 @@ class HuyaEliteWssSession: return self._run(self.client.get_act_info(act_id)) def get_act_task_detail(self, uid: int, cookie: str, act_id: int): + cookie = self._resolve_cookie(cookie) return self._run(self.client.get_act_task_detail(uid, cookie, act_id)) def get_act_user_task_detail(self, uid: int, cookie: str, act_id: int): + cookie = self._resolve_cookie(cookie) return self._run(self.client.get_act_user_task_detail(uid, cookie, act_id)) def get_user_score(self, uid: int, cookie: str, sid: int): + cookie = self._resolve_cookie(cookie) return self._run(self.client.get_user_score(uid, cookie, sid)) def query_user_score(self, uid: int, cookie: str, sid: int): return self.get_user_score(uid, cookie, sid) def get_act_prize_list(self, uid: int, cookie: str, sid: int): + cookie = self._resolve_cookie(cookie) return self._run(self.client.get_act_prize_list(uid, cookie, sid)) def get_act_prize_detail(self, uid: int, cookie: str, sid: int, pid: int): + cookie = self._resolve_cookie(cookie) return self._run(self.client.get_act_prize_detail(uid, cookie, sid, pid)) def score_exchange_prize(self, uid: int, cookie: str, sid: int, pid: int): + cookie = self._resolve_cookie(cookie) return self._run(self.client.score_exchange_prize(uid, cookie, sid, pid)) def get_user_prize_records(self, uid: int, cookie: str, sid: int): + cookie = self._resolve_cookie(cookie) return self._run(self.client.get_user_prize_records(uid, cookie, sid)) + def _resolve_kwargs(self, kwargs: dict) -> dict: + if kwargs.get("cookie") is not None: + kwargs = dict(kwargs) + kwargs["cookie"] = self._resolve_cookie(kwargs["cookie"]) + return kwargs + def check_user_bind_game_account(self, **kwargs): + kwargs = self._resolve_kwargs(kwargs) return self._run(self.client.check_user_bind_game_account(**kwargs)) def confirm_bind_act_account(self, **kwargs): + kwargs = self._resolve_kwargs(kwargs) return self._run(self.client.confirm_bind_act_account(**kwargs)) def get_live_link_param(self, **kwargs): + kwargs = self._resolve_kwargs(kwargs) return self._run(self.client.get_live_link_param(**kwargs)) def get_user_profile_batch(self, **kwargs): + kwargs = self._resolve_kwargs(kwargs) return self._run(self.client.get_user_profile_batch(**kwargs)) def build_bind_urls(self, *args, **kwargs): diff --git a/core/huya/web_cookie_fields.py b/core/huya/web_cookie_fields.py new file mode 100644 index 0000000..b2bf57a --- /dev/null +++ b/core/huya/web_cookie_fields.py @@ -0,0 +1,144 @@ +"""虎牙网页 Cookie 缺失字段补齐生成器。 + +账号 App 登录态只有 16 个 udb_* 认证字段,而活动/商城 WSS 需要完整的网页设备态。 +浏览器实测(evidence/browser_cookie_harvest/report.md)确认: + +- ``udb_deviceid/udb_guiddata/udb_anobiztoken/udb_anouid`` 由匿名设备态协议 + (core/huya/anon_device.py,middle + anonymousLogin)签发; +- ``guid/_qimei_uuid42/__yamid_new/game_did`` 由页面 SDK 脚本写 document.cookie, + 无服务端签发接口。其中 ``guid`` 与会话强绑定:**wsLaunch 响应返回的新 guid 必须 + 回写进 cookie**,否则 getConfig 无响应(2026-09-01 实测); + ``_qimei_uuid42/__yamid_new/game_did`` 为设备标识,随机值格式对齐即可; +- 其余统计/会话字段(SoundValue/Hm_*/rep_cnt 等)服务端仅记录,可随机生成。 + +2026-09-01 全链路实测:账号 id=58 补齐后 checkUserBindGameAccount 返回 +status=200(和平精英已绑定)、getUserScore 返回 status=200(积分 4880)。 +""" + +from __future__ import annotations + +import secrets +import string +import time + +# 统计/会话字段生成规则;9.1 抓包值为样例,格式保持对齐 +_FILL_MAP: tuple[tuple[str, str], ...] = ( + ("SoundValue", "0.50"), + ("alphaValue", "0.80"), + ("isInLiveRoom", "true"), + ("udb_passdata", "3"), + ("udb_accdata", "undefined"), +) + + +def _rand_float() -> str: + import random + + return f"{random.random():.17f}" + + +def fill_web_cookie_fields( + cookie: str, + guid: str, + uid: int | str = 0, + *, + keep_guid: bool = True, +) -> str: + """补齐网页 Cookie 中缺失的字段。 + + Args: + cookie: 当前 Cookie(可只有 udb_* 认证字段)。 + guid: wsLaunch 响应返回的会话 guid(32hex);缺核心字段时必填。 + uid: 虎牙 uid,用于 __yaoldyyuid 等字段。 + keep_guid: 已存在 guid 且非空时是否保留(默认 True)。 + + Returns: + 补齐后的 Cookie 字符串(原始字段值不变,仅补缺失项)。 + """ + pairs: dict[str, str] = {} + for raw in (cookie or "").split(";"): + item = raw.strip() + if not item or "=" not in item: + continue + key, value = item.split("=", 1) + key = key.strip() + if key: + pairs[key] = value.strip() + + def add(key: str, value: str): + if value and key not in pairs: + pairs[key] = value + + uid = int(uid or 0) + + # 4 个核心设备字段 + if guid: + if keep_guid and pairs.get("guid"): + pass # 已有真实 guid 则保留 + else: + add("guid", str(guid)) + elif "guid" not in pairs: + add("guid", secrets.token_hex(16)) + add("_qimei_uuid42", secrets.token_hex(21)) # 42 hex + add("__yamid_new", secrets.token_hex(16).upper()) # 32 hex 大写 + alnum = string.ascii_letters + string.digits + import random + + add("game_did", "".join(random.choices(alnum, k=30))) # 30 位字母数字 + + # qimei 皮肤字段 + add("_qimei_fingerprint", secrets.token_hex(16)) + add("_qimei_h38", secrets.token_hex(19)) + + # 会话/统计字段 + yasmid = _rand_float() + add("__yasmid", yasmid) + add("__yamid_tt1", yasmid) + add("_rep_cnt", str(random.randint(0, 9))) + add("rep_cnt", str(random.randint(0, 9))) + add("h_unt", str(int(time.time()))) + if uid: + add("__yaoldyyuid", str(uid)) + add("_yasids", "__rootsid%3D" + secrets.token_hex(16).upper()) + add("Hm_lvt_51700b6c722f5bb4cf39906a596ea41f", str(int(time.time()))) + add("Hm_lpvt_51700b6c722f5bb4cf39906a596ea41f", str(int(time.time()))) + add("HMACCOUNT", secrets.token_hex(8).upper()) + add("huya_flash_rep_cnt", str(random.randint(1, 400))) + add("huyasp_rep_cnt", str(random.randint(1, 40))) + add("huya_hd_rep_cnt", str(random.randint(1, 40))) + add("huya_web_rep_cnt", str(random.randint(300, 900))) + for key, value in _FILL_MAP: + add(key, value) + + return "; ".join(f"{key}={pairs[key]}" for key in pairs) + + +def missing_web_cookie_fields(cookie: str) -> tuple[str, ...]: + """返回缺失的网页核心字段(不含统计字段)。""" + from .cookie_utils import cookie_value + + core = ( + "guid", "_qimei_uuid42", "__yamid_new", "game_did", + "_qimei_fingerprint", "_qimei_h38", "__yasmid", "__yamid_tt1", + "udb_passdata", "_yasids", "HMACCOUNT", + ) + return tuple(key for key in core if not cookie_value(cookie, key)) + + +def cookie_fields(cookie: str) -> dict[str, str]: + """把 Cookie 字符串转 dict(用于测试/调试)。""" + out = {} + for raw in (cookie or "").split(";"): + item = raw.strip() + if not item or "=" not in item: + continue + key, value = item.split("=", 1) + out[key.strip()] = value.strip() + return out + + +__all__ = [ + "cookie_fields", + "fill_web_cookie_fields", + "missing_web_cookie_fields", +] \ No newline at end of file diff --git a/docs/HUYA_CHAT_缺失字段补齐方案.md b/docs/HUYA_CHAT_缺失字段补齐方案.md new file mode 100644 index 0000000..e9faa1e --- /dev/null +++ b/docs/HUYA_CHAT_缺失字段补齐方案.md @@ -0,0 +1,83 @@ +# 虎牙网页 Cookie 缺失字段补齐方案(2026-09-01 晚) + +> 目标:账号 19507362959(DB id=58,yyuid=1199664135026)跑通精英宝典 WSS +> query_game_name(checkUserBindGameAccount)/ query_points(getUserScore)。 +> 当前账号 cookie 仅 16 字段(登录凭据),对比 9.1 抓包 42 字段缺 26 个。 +> 本文档按"浏览器实测来源 → 生成方案"逐字段确定,重点分析 4 个核心设备字段。 + +## 一、背景结论(已有证据) + +- 9.1 抓包(2026-09-01 09:36)42 字段 cookie:`guid/game_did/_qimei_uuid42/udb_*` 等全部齐全, + 上午所有业务接口成功;当天 21:40 用同一 cookie 逐字节重放,**带 cookie 的业务接口全部返回 + 905/259**,不带 cookie 的 getActInfo/getActTaskDetail 仍成功 → 905 是 **udb 登录态过期**, + 不是请求帧格式问题(重放原字节即可复现)。 +- 浏览器实测(真实 Chrome CDP,`evidence/browser_cookie_harvest/report.md`): + 7 个必填网页设备字段全部由**页面脚本写 document.cookie** 或**服务端匿名接口下发**产生: + | 字段 | 产生机制 | 协议可否复刻 | + | --- | --- | --- | + | `udb_deviceid` | middle/anonymousLogin Set-Cookie(10 年) | ✅ 已接入 anon_device | + | `udb_anobiztoken` | anonymousLogin 响应 biztoken | ✅ 已接入 anon_device | + | `udb_anouid` | anonymousLogin 响应 uid | ✅ 已接入 anon_device | + | `udb_guiddata` | middle URL 第三段 hex32(客户端自选) | ✅ 已接入 anon_device | + | `guid` | `HyUDBWebSDK-Exchange-2.4.js` 写 document.cookie | ❓ 脚本语义待复刻 | + | `_qimei_uuid42` | QIMEI SDK 脚本写 document.cookie | ❓ 脚本语义待复刻 | + | `__yamid_new` | 门户业务脚本写 document.cookie | ❓ 脚本语义待复刻 | + | `game_did` | 门户业务脚本写 document.cookie | ❓ 脚本语义待复刻 | + +## 二、账号 58 现状(16 字段) + +`udb_passport, username, udb_cred, udb_uid, yyuid, udb_biztoken, udb_version, +udb_origin, udb_status, sdid, hdid, udb_anouid, udb_anobiztoken, udb_deviceid, +udb_appid, udb_guiddata` —— 4 个匿名设备字段已有(anon_device 协议), +**缺 `guid/game_did/_qimei_uuid42/__yamid_new` 4 个核心 + 22 个统计字段**。 + +## 三、缺失字段补齐方案(逐字段) + +### 3.1 四个核心设备字段(本轮重点分析) + +| 字段 | 9.1 值形态 | 浏览器首次出现 | 补齐方案 | +| --- | --- | --- | --- | +| `guid` | 32 hex,`0a7d…` 前缀 | udbres.huya.com SDK 脚本 | 待 SDK 逆向;**格式** 32hex | +| `game_did` | 35 位字母数字 | liveapi.ip/getIpLocation 后门户脚本 | 待门户脚本逆向;格式 alnum | +| `_qimei_uuid42` | 42 hex,`1a901…` 前缀 | QIMEI SDK 脚本 | 待 SDK 逆向;格式 42hex | +| `__yamid_new` | 32 hex 大写 | 门户脚本 | 待门户脚本逆向;格式 32hex 大写 | + +浏览器 round1 同一字段多次出现均**值稳定**(跨 profile 稳定):`_qimei_fingerprint` +恒为 `2822a43b5a3bd016fc844218c35c8a74`,`_qimei_uuid42` 前缀恒为 `1a901…`, +`guid` 前缀恒为 `0a7dfaa2…` → 存在固定前缀/哈希结构,**随机值可能被风控识别**, +需还原生成算法。 + +### 3.2 统计/会话字段(随机即可,服务端仅记录) + +| 字段 | 生成规则 | 9.1 样例 | +| --- | --- | --- | +| `SoundValue` | 固定 `0.50` | `0.50` | +| `alphaValue` | 固定 `0.80` | `0.80` | +| `isInLiveRoom` | 固定 `true` | `true` | +| `__yasmid` / `__yamid_tt1` | 17 位随机小数,两者同值 | `0.24859634691671784` | +| `udb_passdata` | 固定 `3` | `3` | +| `_rep_cnt` / `rep_cnt` | 随机 0-9 | `5` / `7` | +| `udb_accdata` | 固定 `undefined` | `undefined` | +| `h_unt` | 当前时间戳(秒) | `1788226396` | +| `__yaoldyyuid` | = yyuid | `1199666914671` | +| `_yasids` | `__rootsid%3D`+32hex 大写 | `__rootsid%3DCBCC34C4…` | +| `Hm_lvt_5170…` | 当前时间戳 | `1788226384` | +| `Hm_lpvt_5170…` | 当前时间戳 | `1788226571` | +| `HMACCOUNT` | 16 hex 随机(百度统计下发) | `1B08C0532E777923` | +| `huya_flash_rep_cnt` | 随机整数 | `136` | +| `huyasp_rep_cnt` | 随机整数 | `9` | +| `huya_hd_rep_cnt` | 随机整数 | `22` | +| `huya_web_rep_cnt` | 随机整数 | `507` | + +### 3.3 已有字段(保持真实值) + +`udb_cred/biztoken/uid/yyuid/passport/version/origin/status/appid/username`(登录凭据)、 +`sdid/hdid`(设备指纹)、`udb_deviceid/udb_guiddata/udb_anobiztoken/udb_anouid`(anon_device)、 +`huya_ua`(normalize 注入)。 + +## 四、风险与验证要求 + +1. `guid/_qimei_uuid42/__yamid_new/game_did` 的**真实性/关联性**服务端是否校验未知; + 必须用生成值跑通线上活动 WSS 全链路才能定论,不能只过长度测试。 +2. 统计字段随机值不影响接口语义,但**顺序**建议与 9.1 cookie 保持一致以便对照。 +3. 补齐后仍需**新鲜的 udb 登录态**(905 = 过期),登录链路见 `app_login.py`。 \ No newline at end of file