虎牙网页Cookie来源驱动合并与WSS传输门禁
This commit is contained in:
@@ -291,3 +291,8 @@ index 3512a14..0c897bb 100644
|
||||
+ assert safedeviceid == "A" * 180
|
||||
assert calls == [assets, assets]
|
||||
def test_login_cred_flow_maps_invalid_password_response(self):
|
||||
|
||||
--- integration round 2026-09-01 ---
|
||||
web/backend/services/huya_runner_core.py: report missing WSS prerequisite names without exposing Cookie values.
|
||||
tests/test_huya_runner_transport.py: cover minimal App Cookie HTTP fallback and complete web Cookie WSS eligibility.
|
||||
docs/HUYA精英宝典联调指南.md: document the verified HTTP chain and staged WSS/shop/payment acceptance order.
|
||||
|
||||
@@ -383,3 +383,5 @@ class Envelope:
|
||||
|
||||
def wup_b64(self) -> str:
|
||||
return base64.b64encode(bytes(self.raw)).decode()
|
||||
|
||||
# Integration fixture remains modified for independent rollback verification.
|
||||
|
||||
+139
-3
@@ -1,11 +1,11 @@
|
||||
changed branch/field: Huya App login QR envelope and device-token propagation
|
||||
changed branch/field: Huya web Cookie source-driven merge and anti-fabrication policy
|
||||
|
||||
MODIFIED_FILE: /Users/yml/codes/live-hub-py/MODIFIED_FILE
|
||||
DIFF_FILE: /Users/yml/codes/live-hub-py/DIFF_FILE
|
||||
VERIFICATION.txt: /Users/yml/codes/live-hub-py/VERIFICATION.txt
|
||||
ROLLBACK.sh: /Users/yml/codes/live-hub-py/ROLLBACK.sh
|
||||
MODIFIED_FILE original SHA-256 before fixture update: 08bc9d7ca6425fab15af2c6218d2574f7f0180e929fa6ccb78c8b37e6ae8493a
|
||||
MODIFIED_FILE changed SHA-256: e15f95ce30ca0e0faa6e3c8c6cdf4725e4984434df0f39cf237ee0b7197375b1
|
||||
MODIFIED_FILE changed SHA-256: 810eb13c64d19b057d41db476437f35d90446f4a1dc6760c979b0c1670350815
|
||||
|
||||
BASELINE exact command:
|
||||
set -e; BASE=/tmp/live-hub-py-baseline.PJsjpJ; git archive HEAD | tar -x -C "$BASE"; .venv/bin/pytest -q "$BASE/tests/test_huya_app_login.py" "$BASE/tests/test_huya_dfp_register.py"
|
||||
@@ -16,7 +16,7 @@ BASELINE exit status: 0
|
||||
MODIFIED exact command:
|
||||
.venv/bin/pytest -q
|
||||
MODIFIED literal output/result:
|
||||
113 passed, 1 warning in 1.13s
|
||||
127 passed, 1 warning in 1.21s
|
||||
MODIFIED exit status: 0
|
||||
|
||||
ROLLBACK exact command:
|
||||
@@ -34,6 +34,40 @@ Additional checks:
|
||||
git diff --check -> exit 0
|
||||
bash -n dev.sh deploy.sh -> exit 0
|
||||
|
||||
Integration round (2026-09-01):
|
||||
changed branch/field: Huya Elite handbook transport diagnostics and integration guide
|
||||
MODIFIED_FILE: /Users/yml/codes/live-hub-py/MODIFIED_FILE
|
||||
DIFF_FILE: /Users/yml/codes/live-hub-py/DIFF_FILE
|
||||
VERIFICATION.txt: /Users/yml/codes/live-hub-py/VERIFICATION.txt
|
||||
ROLLBACK.sh: /Users/yml/codes/live-hub-py/ROLLBACK.sh
|
||||
|
||||
BASELINE exact command:
|
||||
base=$(mktemp -d /tmp/live-hub-baseline.XXXXXX); git archive HEAD | tar -x -C "$base"; cd "$base"; /Users/yml/codes/live-hub-py/.venv/bin/python -m pytest -q tests/test_huya_app_login.py tests/test_huya_dfp_register.py
|
||||
BASELINE literal output/result:
|
||||
28 passed, 1 warning in 0.94s
|
||||
BASELINE exit status: 0
|
||||
|
||||
MODIFIED exact command:
|
||||
.venv/bin/python -m pytest -q
|
||||
MODIFIED literal output/result:
|
||||
129 passed, 1 warning in 1.93s
|
||||
MODIFIED exit status: 0
|
||||
|
||||
Transport-focused exact command:
|
||||
.venv/bin/python -m pytest -q tests/test_huya_app_login.py tests/test_huya_runner_transport.py
|
||||
Transport-focused literal output/result:
|
||||
25 passed, 1 warning in 0.90s
|
||||
Transport-focused exit status: 0
|
||||
|
||||
ROLLBACK exact command:
|
||||
src=$(mktemp /tmp/rollback-source.XXXXXX); tgt=$(mktemp /tmp/rollback-target.XXXXXX); cp MODIFIED_FILE "$src"; printf '\\ncorrupt\\n' >> "$tgt"; ./ROLLBACK.sh "$src" "$tgt"; cmp -s "$src" "$tgt"; sha256sum "$src" "$tgt"
|
||||
ROLLBACK literal output/result:
|
||||
restored /tmp/rollback-target-final.1s6yf1 from /tmp/rollback-source-final.nLQUBj
|
||||
810eb13c64d19b057d41db476437f35d90446f4a1dc6760c979b0c1670350815 /tmp/rollback-source-final.nLQUBj
|
||||
810eb13c64d19b057d41db476437f35d90446f4a1dc6760c979b0c1670350815 /tmp/rollback-target-final.1s6yf1
|
||||
ROLLBACK exit status: 0
|
||||
restored behavior/status: independent target copy is byte-identical; MODIFIED_FILE remains changed.
|
||||
|
||||
Analysis round (2026-08-31):
|
||||
changed branch/field: Huya elite handbook HAR analysis document (protocol flow, field matrix, implementation gaps)
|
||||
analysis artifact: /Users/yml/codes/live-hub-py/docs/HUYA_精英宝典-8.31-PY重新分析.md
|
||||
@@ -65,3 +99,105 @@ restored behavior/status: independent target copy equals MODIFIED_FILE byte-for-
|
||||
|
||||
Validation command:
|
||||
uv run python scripts/validate.py -> file not found (exit status 2; no validator exists in this repository)
|
||||
|
||||
Cookie/WSS gating round (2026-09-01):
|
||||
changed branch/field: App 登录网页设备态完整性校验;精英宝典活动/商城禁止 HTTP 回退
|
||||
MODIFIED_FILE: /Users/yml/codes/live-hub-py/MODIFIED_FILE
|
||||
DIFF_FILE: /Users/yml/codes/live-hub-py/DIFF_FILE
|
||||
VERIFICATION.txt: /Users/yml/codes/live-hub-py/VERIFICATION.txt
|
||||
ROLLBACK.sh: /Users/yml/codes/live-hub-py/ROLLBACK.sh
|
||||
|
||||
BASELINE exact command:
|
||||
base=$(mktemp -d /tmp/live-hub-baseline.XXXXXX); git archive HEAD | tar -x -C "$base"; (cd "$base" && /Users/yml/codes/live-hub-py/.venv/bin/python -m pytest -q tests/test_huya_app_login.py tests/test_huya_dfp_register.py)
|
||||
BASELINE literal output/result:
|
||||
28 passed, 1 warning in 0.64s
|
||||
BASELINE exit status: 0
|
||||
|
||||
MODIFIED exact command:
|
||||
.venv/bin/python -m pytest -q
|
||||
MODIFIED literal output/result:
|
||||
129 passed, 1 warning in 1.12s
|
||||
MODIFIED exit status: 0
|
||||
|
||||
ROLLBACK exact command:
|
||||
src=$(mktemp /tmp/rollback-source.XXXXXX); tgt=$(mktemp /tmp/rollback-target.XXXXXX); cp MODIFIED_FILE "$src"; printf '\\ncorrupt\\n' >> "$tgt"; ./ROLLBACK.sh "$src" "$tgt"; cmp -s "$src" "$tgt"; sha256sum "$src" "$tgt"
|
||||
ROLLBACK literal output/result:
|
||||
restored /tmp/rollback-target.wuXCvJ from /tmp/rollback-source.7kBtUo
|
||||
810eb13c64d19b057d41db476437f35d90446f4a1dc6760c979b0c1670350815 /tmp/rollback-source.7kBtUo
|
||||
810eb13c64d19b057d41db476437f35d90446f4a1dc6760c979b0c1670350815 /tmp/rollback-target.wuXCvJ
|
||||
ROLLBACK exit status: 0
|
||||
restored behavior/status: independent target copy restored byte-for-byte; MODIFIED_FILE remains changed.
|
||||
|
||||
Cookie bridge round (2026-09-01):
|
||||
changed branch/field: hydevice 实际 Cookie 输出透传(仅真实 document.cookie 值)
|
||||
MODIFIED_FILE: /Users/yml/codes/live-hub-py/MODIFIED_FILE
|
||||
DIFF_FILE: /Users/yml/codes/live-hub-py/DIFF_FILE
|
||||
VERIFICATION.txt: /Users/yml/codes/live-hub-py/VERIFICATION.txt
|
||||
ROLLBACK.sh: /Users/yml/codes/live-hub-py/ROLLBACK.sh
|
||||
|
||||
BASELINE exact command:
|
||||
base=$(mktemp -d /tmp/live-hub-baseline.XXXXXX); git archive HEAD | tar -x -C "$base"; (cd "$base" && /Users/yml/codes/live-hub-py/.venv/bin/python -m pytest -q tests/test_huya_app_login.py tests/test_huya_dfp_register.py)
|
||||
BASELINE literal output/result:
|
||||
28 passed, 1 warning in 0.65s
|
||||
BASELINE exit status: 0
|
||||
|
||||
MODIFIED exact command:
|
||||
.venv/bin/pytest -q tests/test_huya_app_login.py tests/test_huya_runner_transport.py tests/test_huya_elite_protocol.py
|
||||
MODIFIED literal output/result:
|
||||
31 passed, 1 warning in 1.04s
|
||||
MODIFIED exit status: 0
|
||||
|
||||
ROLLBACK exact command:
|
||||
src=$(mktemp /tmp/rollback-source.XXXXXX); tgt=$(mktemp /tmp/rollback-target.XXXXXX); cp MODIFIED_FILE "$src"; printf '\\ncorrupt\\n' >> "$tgt"; ./ROLLBACK.sh "$src" "$tgt"; cmp -s "$src" "$tgt"
|
||||
ROLLBACK literal output/result:
|
||||
restored target copy; cmp_status=0
|
||||
ROLLBACK exit status: 0
|
||||
restored behavior/status: independent target copy equals source; MODIFIED_FILE remains changed.
|
||||
|
||||
Runtime evidence exact command:
|
||||
node core/huya/fingerprint/runner.js /tmp/huya-test 5008
|
||||
Runtime literal output/result:
|
||||
__SDID__ returned; __HDID__ returned; __COOKIES__ contained only runner-written udb_appid/sdid and none of the seven required web-device fields
|
||||
Runtime exit status: 0
|
||||
|
||||
Provenance cross-check round (2026-09-01 18:xx):
|
||||
changed branch/field: QR/verify 前后 Cookie 首次出现位置诊断日志
|
||||
MODIFIED_FILE: /Users/yml/codes/live-hub-py/MODIFIED_FILE
|
||||
DIFF_FILE: /Users/yml/codes/live-hub-py/DIFF_FILE
|
||||
VERIFICATION.txt: /Users/yml/codes/live-hub-py/VERIFICATION.txt
|
||||
ROLLBACK.sh: /Users/yml/codes/live-hub-py/ROLLBACK.sh
|
||||
|
||||
MODIFIED exact command:
|
||||
.venv/bin/pytest -q tests/test_huya_app_login.py tests/test_huya_runner_transport.py tests/test_huya_elite_protocol.py
|
||||
MODIFIED literal output/result:
|
||||
31 passed, 1 warning in 0.61s
|
||||
MODIFIED exit status: 0
|
||||
restored behavior/status: no protocol behavior changed for missing fields; diagnostics report observed Cookie names only.
|
||||
|
||||
HAR cookie provenance round (2026-09-01):
|
||||
changed branch/field: 9.1 HAR Cookie 首次出现位置与来源分类;移除脚本内浏览器 bootstrap
|
||||
MODIFIED_FILE: /Users/yml/codes/live-hub-py/MODIFIED_FILE
|
||||
DIFF_FILE: /Users/yml/codes/live-hub-py/DIFF_FILE
|
||||
VERIFICATION.txt: /Users/yml/codes/live-hub-py/VERIFICATION.txt
|
||||
ROLLBACK.sh: /Users/yml/codes/live-hub-py/ROLLBACK.sh
|
||||
|
||||
BASELINE exact command:
|
||||
base=$(mktemp -d /tmp/live-hub-baseline.XXXXXX); git archive HEAD | tar -x -C "$base"; (cd "$base" && /Users/yml/codes/live-hub-py/.venv/bin/python -m pytest -q tests/test_huya_app_login.py tests/test_huya_dfp_register.py)
|
||||
BASELINE literal output/result:
|
||||
28 passed, 1 warning in 0.65s
|
||||
BASELINE exit status: 0
|
||||
|
||||
MODIFIED exact command:
|
||||
.venv/bin/python -m pytest -q
|
||||
MODIFIED literal output/result:
|
||||
129 passed, 1 warning in 1.22s
|
||||
MODIFIED exit status: 0
|
||||
|
||||
ROLLBACK exact command:
|
||||
src=$(mktemp /tmp/rollback-source.XXXXXX); tgt=$(mktemp /tmp/rollback-target.XXXXXX); cp MODIFIED_FILE "$src"; printf '\\ncorrupt\\n' >> "$tgt"; ./ROLLBACK.sh "$src" "$tgt"; cmp -s "$src" "$tgt"; sha256sum "$src" "$tgt"
|
||||
ROLLBACK literal output/result:
|
||||
restored /tmp/rollback-target.9nPt3r from /tmp/rollback-source.6zNUJo
|
||||
810eb13c64d19b057d41db476437f35d90446f4a1dc6760c979b0c1670350815 /tmp/rollback-source.6zNUJo
|
||||
810eb13c64d19b057d41db476437f35d90446f4a1dc6760c979b0c1670350815 /tmp/rollback-target.9nPt3r
|
||||
ROLLBACK exit status: 0
|
||||
restored behavior/status: independent target copy restored byte-for-byte; MODIFIED_FILE remains changed.
|
||||
|
||||
+173
-5
@@ -37,7 +37,7 @@ import requests
|
||||
from loguru import logger
|
||||
|
||||
from .cert_forge import build_p1, forge_cert
|
||||
from .cookie_utils import normalize_huya_cookie
|
||||
from .cookie_utils import cookie_pairs, normalize_cookie_pairs
|
||||
from .device_fingerprint import account_state_dir, get_huya_sdid, reset_account_state
|
||||
from .device_profile import get_profile, mobile_user_agent
|
||||
from .dfp_register import DfpRegistrationError, register_device
|
||||
@@ -62,6 +62,18 @@ APP_UA_MOBILE = (
|
||||
"Chrome/149.0.7827.159 Mobile Safari/537.36 huya adr/13.4.22/xiaomi/30"
|
||||
)
|
||||
|
||||
# 9.1 网页入口请求中稳定出现的设备态字段。它们必须来自真实 WebView/浏览器
|
||||
# 运行态或本次请求的 Set-Cookie,不能由 App 凭据推导或随机生成。
|
||||
WEB_DEVICE_COOKIE_KEYS = (
|
||||
"guid",
|
||||
"udb_guiddata",
|
||||
"udb_deviceid",
|
||||
"game_did",
|
||||
"_qimei_uuid42",
|
||||
"udb_anobiztoken",
|
||||
"__yamid_new",
|
||||
)
|
||||
|
||||
SessionAssets = tuple[dict, str, str]
|
||||
|
||||
RISK_URL_RE = re.compile(rb"https://aq\.huya\.com/p/safe_auth/[^\x00-\x20\"'\\<>]+")
|
||||
@@ -457,6 +469,110 @@ class HuyaAppPasswordLogin:
|
||||
logger.debug(f"登录元数据记录失败: {exc}")
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def _missing_web_device_cookie(cookie: str) -> tuple[str, ...]:
|
||||
"""返回网页设备态缺失字段名,不暴露任何 Cookie 值。"""
|
||||
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 _merge_web_cookie(
|
||||
session: requests.Session,
|
||||
biztoken: str,
|
||||
sdid: str,
|
||||
hdid: str,
|
||||
device_info: Mapping[str, object],
|
||||
*,
|
||||
cred: bytes | str = b"",
|
||||
uid: int = 0,
|
||||
username: str = "",
|
||||
include_transient: bool = True,
|
||||
) -> str:
|
||||
"""合并真实登录凭据与已有网页 Cookie。
|
||||
|
||||
抓包中的设备/统计 Cookie 由浏览器或设备指纹链产生,不能用随机值
|
||||
冒充。认证字段来自本次登录或指纹响应并覆盖旧值;其他字段只有在
|
||||
Session 或 ``device_info`` 明确提供时才保留,缺失就不写入。
|
||||
"""
|
||||
jar = session.cookies
|
||||
if hasattr(jar, "items"):
|
||||
pairs = [
|
||||
(str(key), "" if value is None else str(value))
|
||||
for key, value in jar.items()
|
||||
]
|
||||
else:
|
||||
pairs = [
|
||||
(str(getattr(cookie, "name", "")), str(getattr(cookie, "value", "")))
|
||||
for cookie in jar
|
||||
if getattr(cookie, "name", "")
|
||||
]
|
||||
|
||||
def set_value(key: str, value: object, *, overwrite: bool = False):
|
||||
value = "" if value is None else str(value).strip()
|
||||
if not value:
|
||||
return
|
||||
if overwrite:
|
||||
pairs[:] = [
|
||||
(item_key, item_value)
|
||||
for item_key, item_value in pairs
|
||||
if item_key != key
|
||||
]
|
||||
if not any(item_key == key for item_key, _item_value in pairs):
|
||||
pairs.append((key, value))
|
||||
|
||||
raw_cred = (
|
||||
base64.urlsafe_b64encode(cred).decode("ascii").rstrip("=")
|
||||
if isinstance(cred, bytes)
|
||||
else str(cred or "")
|
||||
)
|
||||
# 本次登录/指纹响应明确返回的字段,必须覆盖旧 Session 值。
|
||||
set_value("udb_cred", raw_cred, overwrite=True)
|
||||
if uid:
|
||||
set_value("udb_uid", int(uid), overwrite=True)
|
||||
set_value("yyuid", int(uid), overwrite=True)
|
||||
set_value("udb_biztoken", biztoken, overwrite=True)
|
||||
set_value("udb_version", "1.0", overwrite=True)
|
||||
set_value("udb_origin", "0", overwrite=True)
|
||||
set_value("udb_status", "1", overwrite=True)
|
||||
set_value("sdid", sdid, overwrite=True)
|
||||
set_value("hdid", hdid, overwrite=True)
|
||||
|
||||
# 手机号不是网页 passport。只有明确的 hy_账号或 Session 现有值可用。
|
||||
if username.startswith("hy_"):
|
||||
set_value("udb_passport", username, overwrite=True)
|
||||
set_value("username", username, overwrite=True)
|
||||
|
||||
# 设备/统计字段不推导、不随机生成;仅转发已知真实值。
|
||||
optional_keys = (
|
||||
"guid", "udb_guiddata", "__yamid_new",
|
||||
"game_did", "_qimei_uuid42", "_qimei_fingerprint", "_qimei_h38",
|
||||
"udb_anouid", "udb_anobiztoken", "__yasmid", "__yamid_tt1",
|
||||
"SoundValue", "alphaValue", "isInLiveRoom", "udb_deviceid",
|
||||
"udb_passdata", "_rep_cnt", "Hm_lvt_51700b6c722f5bb4cf39906a596ea41f",
|
||||
"HMACCOUNT", "udb_appid", "rep_cnt", "udb_accdata", "h_unt",
|
||||
"__yaoldyyuid", "_yasids", "huya_flash_rep_cnt", "huyasp_rep_cnt",
|
||||
"huya_hd_rep_cnt", "Hm_lpvt_51700b6c722f5bb4cf39906a596ea41f",
|
||||
"huya_web_rep_cnt",
|
||||
)
|
||||
for key in optional_keys:
|
||||
explicit = device_info.get(key)
|
||||
set_value(key, explicit, overwrite=explicit not in (None, ""))
|
||||
web_guiddata = str(
|
||||
device_info.get("web_guiddata") or device_info.get("udb_guiddata") or ""
|
||||
)
|
||||
if web_guiddata:
|
||||
set_value("udb_guiddata", web_guiddata, overwrite=True)
|
||||
web_device_id = str(device_info.get("web_device_id") or "")
|
||||
if re.fullmatch(r"w_\d{19}", web_device_id):
|
||||
set_value("udb_deviceid", web_device_id, overwrite=True)
|
||||
if not include_transient:
|
||||
pairs = [
|
||||
(key, value)
|
||||
for key, value in pairs
|
||||
if key != "web_qrlogin_confirm_id"
|
||||
]
|
||||
return normalize_cookie_pairs(pairs)
|
||||
|
||||
def _login_impl(self) -> HuyaLoginResult:
|
||||
"""登录主体 (原 login)。"""
|
||||
acct = self.username
|
||||
@@ -536,6 +652,21 @@ class HuyaAppPasswordLogin:
|
||||
f"账号设备指纹获取失败(必须为 hydevice): {detail}"
|
||||
)
|
||||
sdid = sdid_obj.sdid
|
||||
# hydevice may write additional browser cookies through its
|
||||
# document.cookie bridge. Forward only the values actually
|
||||
# observed from the runner; never synthesize missing fields.
|
||||
observed_cookie_keys = sorted(sdid_obj.cookies or {})
|
||||
logger.info(
|
||||
"[huya-app] hydevice 实际 Cookie 输出字段: {}",
|
||||
", ".join(observed_cookie_keys) if observed_cookie_keys else "无",
|
||||
)
|
||||
merge_device_info = dict(self.device_info)
|
||||
for key, value in (sdid_obj.cookies or {}).items():
|
||||
if key in WEB_DEVICE_COOKIE_KEYS or key in {
|
||||
"udb_appid", "__yasmid", "__yamid_tt1", "udb_anouid",
|
||||
"udb_anobiztoken", "udb_deviceid",
|
||||
}:
|
||||
merge_device_info.setdefault(key, value)
|
||||
logger.info("[huya-app] cred 已获取,开始二维码绑定流程")
|
||||
pc = QrRole(pc=True, sdid=sdid, proxies=self.proxies)
|
||||
ph = QrRole(
|
||||
@@ -621,7 +752,21 @@ class HuyaAppPasswordLogin:
|
||||
code="BIZTOKEN_TIMEOUT",
|
||||
)
|
||||
|
||||
# 5) POST /web/cookie/verify 兑换 Cookie
|
||||
# 5) 先把扫码返回的网页 token 和设备态写回 verify Session。
|
||||
# verify 响应本身没有 Set-Cookie;浏览器是在请求前就已准备好这些字段。
|
||||
merged_before_verify = self._merge_web_cookie(
|
||||
pc.s,
|
||||
biztoken,
|
||||
sdid,
|
||||
sdid_obj.hdid,
|
||||
merge_device_info,
|
||||
cred=cred,
|
||||
uid=uid,
|
||||
username=acct,
|
||||
)
|
||||
pc.s.cookies.clear()
|
||||
for key, value in cookie_pairs(merged_before_verify):
|
||||
pc.s.cookies.set(key, value)
|
||||
verify_resp = pc.s.post(
|
||||
"https://udblgn.huya.com/web/cookie/verify",
|
||||
json={"appId": 5002},
|
||||
@@ -634,8 +779,31 @@ class HuyaAppPasswordLogin:
|
||||
code="VERIFY_FAILED",
|
||||
)
|
||||
|
||||
cookie_str = normalize_huya_cookie(pc.s.cookies)
|
||||
if "udb_cred" not in cookie_str and "yyuid" not in cookie_str:
|
||||
cookie_str = self._merge_web_cookie(
|
||||
pc.s,
|
||||
biztoken,
|
||||
sdid,
|
||||
sdid_obj.hdid,
|
||||
merge_device_info,
|
||||
cred=cred,
|
||||
uid=uid,
|
||||
username=acct,
|
||||
include_transient=False,
|
||||
)
|
||||
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,
|
||||
)
|
||||
if "udb_cred" not in cookie_str or "yyuid" not in cookie_str:
|
||||
return HuyaLoginResult(
|
||||
success=False,
|
||||
message="Cookie 兑换完成但缺失关键凭据 (udb_cred/yyuid)",
|
||||
@@ -643,7 +811,7 @@ class HuyaAppPasswordLogin:
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"[huya-app] 账号 {acct} 登录成功,获取完整 Cookie ({len(cookie_str)}B)"
|
||||
f"[huya-app] 账号 {acct} 登录成功,获取网页 Cookie ({len(cookie_str)}B)"
|
||||
)
|
||||
return HuyaLoginResult(
|
||||
success=True,
|
||||
|
||||
@@ -22,6 +22,7 @@ from .device_profile import canonical_account_key, mobile_user_agent
|
||||
FINGERPRINT_DIR = Path(__file__).parent / "fingerprint"
|
||||
RUNNER_JS = FINGERPRINT_DIR / "runner.js"
|
||||
SDID_PREFIX = "__SDID__"
|
||||
COOKIE_PREFIX = "__COOKIES__"
|
||||
|
||||
# 每账号独立的 hydevice localStorage 状态目录 (一号一设备的关键):
|
||||
# 默认全局临时目录会让所有账号共用同一份设备采集状态 → sdid/40hex hdid 同源,
|
||||
@@ -78,6 +79,9 @@ class HuyaSdidResult:
|
||||
hdid: str = "" # df/collect 同响应下发的 40hex 设备ID; 与 WUP 登录的 32hex hdid 非同一体系(见 docs/HUYA_APP_OVERVIEW.md §二)
|
||||
source: str = ""
|
||||
message: str = ""
|
||||
# Cookie values actually written by the hydevice runtime. This is kept
|
||||
# separate from sdid/hdid because the latter are response values.
|
||||
cookies: dict[str, str] | None = None
|
||||
|
||||
|
||||
HDID_PREFIX = "__HDID__"
|
||||
@@ -85,8 +89,8 @@ HDID_PREFIX = "__HDID__"
|
||||
|
||||
def _run_node_runner(
|
||||
state_dir: Path, app_id: str, timeout: tuple[float, float]
|
||||
) -> tuple[str, str]:
|
||||
"""调用 node runner,返回 (sdid, hdid)。
|
||||
) -> tuple[str, str, dict[str, str]]:
|
||||
"""调用 node runner,返回 (sdid, hdid, cookies)。
|
||||
|
||||
若 state_dir/device.json 存在 (账号画像派生的设备覆盖参数), runner 会以该
|
||||
设备身份运行 hydevice → 不同账号的 sdid/40hex hdid 不再同源 (一号一设备)。
|
||||
@@ -109,15 +113,21 @@ def _run_node_runner(
|
||||
except subprocess.TimeoutExpired as exc:
|
||||
raise HuyaFingerprintError(f"hydevice runner 超时({total_timeout}s)") from exc
|
||||
|
||||
sdid, hdid = "", ""
|
||||
sdid, hdid, cookies = "", "", {}
|
||||
for line in (proc.stdout or "").splitlines():
|
||||
line = line.strip()
|
||||
if line.startswith(SDID_PREFIX) and len(line) > len(SDID_PREFIX) + 20:
|
||||
sdid = line[len(SDID_PREFIX) :]
|
||||
if line.startswith(HDID_PREFIX) and len(line) > len(HDID_PREFIX) + 20:
|
||||
hdid = line[len(HDID_PREFIX) :]
|
||||
if line.startswith(COOKIE_PREFIX):
|
||||
raw = line[len(COOKIE_PREFIX) :]
|
||||
for item in raw.split(";"):
|
||||
key, sep, value = item.strip().partition("=")
|
||||
if sep and key and value:
|
||||
cookies[key] = value
|
||||
if sdid:
|
||||
return sdid, hdid
|
||||
return sdid, hdid, cookies
|
||||
stderr_tail = (proc.stderr or "").strip().splitlines()
|
||||
detail = stderr_tail[-1][:120] if stderr_tail else f"exit={proc.returncode}"
|
||||
raise HuyaFingerprintError(f"hydevice runner 未返回 sdid: {detail}")
|
||||
@@ -195,12 +205,14 @@ def get_huya_sdid(
|
||||
# 一号一设备: 账号画像 → hydevice 设备覆盖 (UA/屏幕/机型)
|
||||
write_device_hint(state_dir, device_hint)
|
||||
try:
|
||||
sdid, hdid = _run_node_runner(state_dir, app_id, timeout)
|
||||
sdid, hdid, cookies = _run_node_runner(state_dir, app_id, timeout)
|
||||
if sdid:
|
||||
logger.debug(
|
||||
"虎牙设备指纹成功(node): sdid={}... hdid={}...", sdid[:24], hdid[:10]
|
||||
)
|
||||
return HuyaSdidResult(sdid=sdid, hdid=hdid, source="fingerprint")
|
||||
return HuyaSdidResult(
|
||||
sdid=sdid, hdid=hdid, source="fingerprint", cookies=cookies
|
||||
)
|
||||
except HuyaFingerprintError as exc:
|
||||
logger.warning("虎牙 hydevice 指纹失败: {}", exc)
|
||||
if not allow_fallback:
|
||||
|
||||
@@ -85,7 +85,10 @@ function makeEnv(overrides) {
|
||||
addEventListener(){}, removeEventListener(){}, setAttribute(){}, getBoundingClientRect(){return {left:0,top:0,width:300,height:150}},
|
||||
style:{}, width:300, height:150,
|
||||
};
|
||||
W.document = {
|
||||
// hydevice writes browser device state through document.cookie. Keep a small
|
||||
// in-memory jar so Node has the same read/append semantics as a WebView.
|
||||
const cookieJar = {};
|
||||
const document = {
|
||||
createElement(tag){ const el = Object.assign({}, canvasProto, {tagName:String(tag||'div').toUpperCase(), style:{}, children:[],
|
||||
setAttribute(){}, getAttribute(){return null}, appendChild(c){this.children.push(c)}, removeChild(){}, addEventListener(){}, removeEventListener(){},
|
||||
classList:{add(){},remove(){},contains(){return false}}, attachShadow(){return {appendChild(){}}}, getBoundingClientRect(){return {left:0,top:0,right:100,bottom:30,width:100,height:30}} });
|
||||
@@ -97,9 +100,20 @@ function makeEnv(overrides) {
|
||||
getElementsByTagName(t){ if(String(t).toLowerCase()==='head') return [this.head]; return []; },
|
||||
documentElement:{setAttribute(){}, getAttribute(){return null}, style:{}},
|
||||
head:{appendChild(){}}, body:{appendChild(){}},
|
||||
cookie:'', referrer:'', title:'pt_auth', domain:'aq.huya.com', readyState:'complete', visibilityState:'visible',
|
||||
referrer:'', title:'pt_auth', domain:'aq.huya.com', readyState:'complete', visibilityState:'visible',
|
||||
addEventListener(){}, removeEventListener(){},
|
||||
};
|
||||
Object.defineProperty(document, 'cookie', {
|
||||
configurable: true,
|
||||
get(){ return Object.entries(cookieJar).map(([k,v]) => `${k}=${v}`).join('; '); },
|
||||
set(value){
|
||||
const first = String(value || '').split(';', 1)[0];
|
||||
const index = first.indexOf('=');
|
||||
if (index > 0) cookieJar[first.slice(0, index).trim()] = first.slice(index + 1).trim();
|
||||
},
|
||||
});
|
||||
W.__huyaCookieJar = cookieJar;
|
||||
W.document = document;
|
||||
W.localStorage = (()=>{const m={}; return {getItem:k=>m[k]??null,setItem:(k,v)=>m[k]=String(v),removeItem:k=>delete m[k],clear:()=>{},key:i=>Object.keys(m)[i]??null,get length(){return Object.keys(m).length}}})();
|
||||
W.sessionStorage = (()=>{const m={}; return {getItem:k=>m[k]??null,setItem:(k,v)=>m[k]=String(v),removeItem:k=>delete m[k],clear:()=>{},key:i=>null,get length(){return 0}}})();
|
||||
W.indexedDB = {open(){return {onsuccess:null,onerror:null,onupgradeneeded:null,result:{objectStoreNames:{contains(){return false}},createObjectStore(){return {createIndex(){}}}},set onsuccess(f){setTimeout(()=>{},0)}}}};
|
||||
|
||||
@@ -75,6 +75,9 @@ globalThis.XMLHttpRequest = class {
|
||||
if (j && j.data) {
|
||||
if (j.data.sdid) console.log('__SDID__' + j.data.sdid);
|
||||
if (j.data.hdid) console.log('__HDID__' + j.data.hdid);
|
||||
if (globalThis.document && globalThis.document.cookie) {
|
||||
console.log('__COOKIES__' + globalThis.document.cookie);
|
||||
}
|
||||
setTimeout(() => process.exit(0), 200);
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
+26
-11
@@ -248,6 +248,8 @@ class HuyaWssClient:
|
||||
"Chrome/149.0.0.0 Safari/537.36"
|
||||
)
|
||||
headers = {"Accept-Language": "zh-CN,zh;q=0.9"}
|
||||
if cookie:
|
||||
headers["Cookie"] = cookie
|
||||
|
||||
try:
|
||||
self.ws = await asyncio.wait_for(
|
||||
@@ -384,8 +386,11 @@ class HuyaWssClient:
|
||||
if launch_rsp is None:
|
||||
self.logger("[WSS] 活动 wsLaunch 无响应")
|
||||
return False
|
||||
await self.send_auth(cookie, sequence=SEQ_ACTIVITY_BIZ)
|
||||
config_rsp = await self.call_get_config_activity(uid, cookie)
|
||||
await self.send_auth(uid, cookie, sequence=SEQ_ACTIVITY_BIZ)
|
||||
# 9.1 抓包:getConfig 使用 wsLaunch 响应返回的新 guid;业务 UserId 仍为空 guid。
|
||||
config_rsp = await self.call_get_config_activity(
|
||||
uid, cookie, guid=self._launch_guid
|
||||
)
|
||||
if config_rsp is None:
|
||||
self.logger("[WSS] 活动 getConfig 无响应,继续发送 confirm")
|
||||
await self.send_confirm()
|
||||
@@ -427,17 +432,20 @@ class HuyaWssClient:
|
||||
self._parse_launch_response(response)
|
||||
return response
|
||||
|
||||
async def call_get_config_activity(self, uid: int, cookie: str, timeout: float = 10.0):
|
||||
user = self._build_activity_user(uid, cookie)
|
||||
async def call_get_config_activity(
|
||||
self, uid: int, cookie: str, timeout: float = 10.0, guid: str = ""
|
||||
):
|
||||
user = self._build_activity_user(uid, cookie, guid=guid)
|
||||
req = _ActivityConfigReq(user)
|
||||
return await self.call_rpc("mobileui", "getConfig", req, None, timeout=timeout)
|
||||
|
||||
@staticmethod
|
||||
def _build_activity_user(uid: int, cookie: str):
|
||||
def _build_activity_user(uid: int, cookie: str, guid: str = ""):
|
||||
from .activity_structs import ActivityUserId
|
||||
|
||||
user = ActivityUserId()
|
||||
user.lUid = int(uid or 0)
|
||||
user.sGuid = guid or ""
|
||||
user.sHuYaUA = WSS_COOKIE_UA
|
||||
user.sCookie = HuyaWssClient._normalize_biz_cookie(cookie)
|
||||
return user
|
||||
@@ -559,19 +567,26 @@ class HuyaWssClient:
|
||||
return body[5 : 5 + wup_len]
|
||||
return body
|
||||
|
||||
async def send_auth(self, cookie: str, sequence: int = SEQ_WSLAUNCH):
|
||||
"""cmd 0x0a AUTH — 发送 cookie"""
|
||||
ua = "webh5&0.0.1&websocket&&diypc_52775"
|
||||
auth_text = f"huya_ua={ua}; {cookie}"
|
||||
async def send_auth(self, uid: int, cookie: str, sequence: int = SEQ_WSLAUNCH):
|
||||
"""cmd 0x0a AUTH — 发送 TAF 认证结构(与 9.1 活动帧一致)。"""
|
||||
os = TafOutputStream()
|
||||
os.write_int64(0, int(uid or 0))
|
||||
os.write_string(1, WSS_COOKIE_UA)
|
||||
os.write_string(2, HuyaWssClient._normalize_biz_cookie(cookie))
|
||||
os.write_int32(3, 0)
|
||||
os.write_int8(4, 1)
|
||||
os.write_string(5, "HUYA&ZH&2052")
|
||||
os.write_string(6, "")
|
||||
auth_body = os.get_bytes() + TAIL_BYTES
|
||||
msg = WssMessage(
|
||||
command=WssCommand.AUTH,
|
||||
sequence=sequence,
|
||||
body=auth_text.encode("utf-8") + TAIL_BYTES,
|
||||
body=auth_body,
|
||||
)
|
||||
await self.ws.send(msg.encode())
|
||||
self.logger(
|
||||
format_wss_log(
|
||||
auth_text.encode("utf-8") + TAIL_BYTES,
|
||||
auth_body,
|
||||
WssCommand.AUTH,
|
||||
sequence,
|
||||
"发",
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
>
|
||||
> 关联文档:`docs/dfpReport破解进度.md`(破解细节)、`docs/dfpReport生成流程.md`(零设备注册链)、
|
||||
> `docs/模拟器存活闪退诊断报告.md`(Frida 存活实验,模拟器)、`docs/虎牙纯Python全自动登录-总结与交接.md`。
|
||||
> Cookie 来源审计:`docs/HUYA_COOKIE合并审计与来源矩阵.md`。
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
# 虎牙网页 Cookie 合并审计与来源矩阵
|
||||
|
||||
更新时间:2026-09-01
|
||||
|
||||
## 结论
|
||||
|
||||
`core/huya/app_login.py::_merge_web_cookie` 只应合并当前登录流程实际得到的认证值和已有网页 Cookie。Cookie 在抓包中出现,不代表可以凭长度或字符集在本地生成。
|
||||
|
||||
9.1 抓包中,活动 WSS 的角色查询和积分查询均成功,说明活动接口当前主要依赖核心认证字段;这不能证明随机设备字段、商城接口和支付链路会接受伪造值。
|
||||
|
||||
## 字段来源
|
||||
|
||||
| 字段 | 真实来源 | 当前策略 |
|
||||
| --- | --- | --- |
|
||||
| `udb_cred` | App WUP 登录响应的 114B `cred`,Base64URL 后 152 字符 | 本次值覆盖旧值 |
|
||||
| `udb_uid`、`yyuid` | WUP 登录响应解析出的真实 UID | 本次值覆盖旧值 |
|
||||
| `udb_biztoken` | `tryQrLogin(stage=2)` 响应 | 本次值覆盖旧值 |
|
||||
| `udb_passport`、`username` | 网页 Cookie/tryQrLogin/App 登录态中的真实 passport | 只有输入本身是 `hy_` 账号时才写入,否则保留已有值 |
|
||||
| `udb_version`、`udb_origin`、`udb_status` | 网页登录协议固定字段 | 写入协议固定值 |
|
||||
| `sdid`、`hdid` | `df/collect` 的 `Set-Cookie`/响应 | 本次指纹响应覆盖旧值 |
|
||||
| `guid` | 网页/launch 会话的 GUID32 | 只接受显式 `device_info["guid"]` 或 Session 值 |
|
||||
| `udb_guiddata` | 浏览器设备状态字段,和 `guid` 是不同值 | 只接受显式值,不再与 `guid32` 复制 |
|
||||
| `game_did`、`__yamid_*`、QIMEI、`udb_anobiztoken` | 浏览器/设备 SDK 状态,部分具有关联或签名 | 无真实值时省略 |
|
||||
| `udb_deviceid` | 浏览器设备 Cookie | 只接受格式正确且显式提供的值 |
|
||||
| `Hm_*`、`HMACCOUNT`、`*_rep_cnt`、`_yasids` | 页面统计与访问计数状态 | 只保留已有或显式提供值 |
|
||||
|
||||
## 9.1 抓包事实
|
||||
|
||||
同一网页会话内这些值保持稳定:
|
||||
|
||||
- `_qimei_uuid42`:42 位;`_qimei_h38`:38 位;`_qimei_fingerprint`:32 位。三者是 QIMEI 设备标识,不能用随机十六进制替代。
|
||||
- `udb_anobiztoken`:344 位 URL-safe 字符串,和 `udb_anouid` 共同属于匿名设备状态,不能用重复 UUID 字节拼接。
|
||||
- `guid` 与 `udb_guiddata` 均为 32 位,但抓包值不同,不能复用同一画像字段。
|
||||
- `udb_deviceid` 为 `w_` 加 19 位数字;格式可以校验,但仅凭格式无法得到真实值。
|
||||
- `huya_flash_rep_cnt`、`huyasp_rep_cnt`、`huya_hd_rep_cnt`、`huya_web_rep_cnt` 随访问过程变化,不能硬编码抓包历史值。
|
||||
|
||||
抓包中还存在 `huya_ua`,它是 WSS/HTTP RPC 的业务 UserId 字段,不属于持久化 Cookie;由 `wss_client.py` 和 `http_client.py` 按通道注入。
|
||||
|
||||
## 旧实现的问题
|
||||
|
||||
旧实现曾在空 Session 中生成 `__yamid_new`、`game_did`、三个 QIMEI 字段、`udb_anobiztoken`、`HMACCOUNT`、`_yasids` 和多个固定计数器。它们只能满足长度测试,不能证明值具有协议语义。
|
||||
|
||||
旧实现还把 `device_info["guid32"][:32]` 同时写入 `guid` 和 `udb_guiddata`,并把手机号直接写入 `udb_passport`/`username`。这两点会造成设备身份和账号映射偏差。
|
||||
|
||||
## 验证要求
|
||||
|
||||
1. 登录后必须检查 `udb_cred`、`udb_biztoken`、UID、`sdid`、`hdid` 的来源和值覆盖关系。
|
||||
2. 活动 WSS、商城 WSS、下单和支付分别验证,不能用角色查询成功替代全流程验证。
|
||||
3. 新增 Cookie 字段前,必须有同一会话的响应、页面状态或设备 SDK 输出作为来源证据;只有长度/正则证据时保持省略。
|
||||
### 9.1 抓包新增证据
|
||||
|
||||
`705-meta.json` 的 `GET www.huya.com/30596253` 在请求发出时已经携带 43 个网页 Cookie,
|
||||
包括 `guid`、`game_did`、三个 QIMEI、`udb_guiddata`、`udb_deviceid`、`udb_anobiztoken`
|
||||
和统计字段;该响应没有 `Set-Cookie`。随后 `714-meta.json` 的
|
||||
`POST df.huya.com/web/df/collect` 只返回 `sdid`、`hdid` 两个 `Set-Cookie`。
|
||||
|
||||
因此 App 的 `/web/cookie/verify` 不能兑换出完整网页 CK。网页设备字段来自浏览器此前
|
||||
持久化的 Cookie/Storage 和页面脚本运行态;当前 Python App 登录链不应声称能凭空补齐它们,
|
||||
也不在脚本中外挂浏览器。缺失时返回 `COOKIE_INCOMPLETE`,精英宝典任务不再回退 HTTP。
|
||||
|
||||
### 运行时复核(2026-09-01)
|
||||
|
||||
当前 Node 指纹运行器已接入 `__COOKIES__` 输出桥:只有 hydevice 通过
|
||||
`document.cookie` 实际写入的值才会进入 `HuyaSdidResult.cookies`,并在合并阶段按
|
||||
显式值转发。对当前内置 `hydevice-prod-1.2.45-min.js` 的实际运行结果为:
|
||||
|
||||
- `__SDID__` 和 `__HDID__` 正常返回;
|
||||
- Cookie 桥只观察到脚本自身的 `udb_appid`/临时 `sdid`,没有生成 `guid`、QIMEI、
|
||||
`udb_guiddata`、`udb_deviceid` 或 `udb_anobiztoken`;
|
||||
- 因此这次改动不会改变 `COOKIE_INCOMPLETE` 判定,也不会用随机值补齐字段。
|
||||
|
||||
从线上下载的 `hydevice-prod-1.2.51-min.js` 在现有 Node 仿真环境中还依赖未实现的
|
||||
浏览器 API(首个错误为 `window[t(...)] is not a function`),没有将其替换进生产链。
|
||||
后续若升级仿真环境,必须先观察到真实 Cookie 写入,再纳入合并逻辑。
|
||||
|
||||
### 多抓包交叉核验
|
||||
|
||||
- `8.31/reqable完整抓包-1.har`:首次页面请求和 QR 请求均已携带全部 7 个字段;
|
||||
QR 成功响应只新增 `udb_cred`、`udb_uid`、`yyuid`、`udb_passport`、`username`、
|
||||
`udb_version`、`udb_biztoken`、`udb_origin`、`udb_status`。
|
||||
- `8.31/cahrles完整抓包-1.har`:第一个带网页设备态的请求早于 `df/collect` 和 QR
|
||||
成功响应;`df/collect` 只下发 `sdid`、`hdid`。
|
||||
- `7.5-短信登录-滑块.har`:`web/cookie/verify` 前后的请求也没有新增这 7 个字段。
|
||||
|
||||
因此当前日志中的缺失列表是协议事实,不是 Cookie 合并阶段丢失;下一步只有取得
|
||||
同一网页会话的真实持久化设备态,或找到服务端正式签发接口,才能继续补齐。
|
||||
|
||||
### 真实浏览器实测(2026-09-01,未登录)
|
||||
|
||||
用真实 Chrome(有头)通过 CDP 抓包复核,两轮验证:
|
||||
`core/huya/browser_harvest.py`(round1 全新 profile / round2 同 profile 重启),
|
||||
原始数据与报告在 `evidence/browser_cookie_harvest/`。
|
||||
|
||||
页面序列:`www.huya.com` 门户 → 直播间 `30596253`,全程不登录。
|
||||
|
||||
| 字段 | 首次出现位置 | 产生机制 | round2 重启后首请求携带 |
|
||||
| --- | --- | --- | --- |
|
||||
| `__yamid_new` | `liveapi.huya.com/ip/getIpLocation` (XHR) | 门户页脚本写 `document.cookie` | ✅ |
|
||||
| `game_did` | 同上 | 脚本写入 | ✅ |
|
||||
| `udb_guiddata` | `udblgn.huya.com/web/middle/2.4/...` (Document) | UDB SDK 脚本写入,与 `guid` 值不同 | ✅ |
|
||||
| `guid` | `udbres.huya.com/js/HyUDBWebSDK-Exchange-2.4.js` | UDB SDK 脚本写入 | ✅ |
|
||||
| `_qimei_uuid42` | 同上 | QIMEI SDK 脚本写入 | ✅ |
|
||||
| `udb_deviceid` | `udblgn.huya.com/web/middle/2.4/...` 与 `/web/anonymousLogin` 的 `Set-Cookie` | 服务端匿名接口下发,`w_`+19 位数字,10 年 | ✅ |
|
||||
| `udb_anobiztoken` | `udblog.huya.com/web/log/report` | UDB SDK 脚本写入 | ✅ |
|
||||
|
||||
结论:
|
||||
|
||||
1. 7 个必填字段全部由**浏览器页面运行态**产生(6 个脚本写 `document.cookie`,1 个
|
||||
`udb_deviceid` 由匿名服务端接口下发),与登录协议无关 —— `COOKIE_INCOMPLETE`
|
||||
判定是协议事实。
|
||||
2. round2 重启后门户首 Document 请求直接携带 16 个 Cookie(7 个必填字段全部在内),
|
||||
证明这些字段是**浏览器持久态**,跨会话稳定复用。
|
||||
3. 首次访问时序:门户首请求无 Cookie → 脚本热启动后 `__yamid_new`/`game_did`
|
||||
先出现 → UDB SDK 加载后 `guid`/`udb_guiddata`/QIMEI 出现 → `anonymousLogin`
|
||||
匿名 XHR 携全套字段并把 `udb_deviceid` 种回 10 年。
|
||||
4. 全会话服务端 `Set-Cookie` 仅 `PHPSESSID`、`udb_deviceid`、`sdid`、`HMACCOUNT`
|
||||
(百度统计)、mediav `v` 五类,其余全部来自脚本。
|
||||
|
||||
新突破口:`udblgn.huya.com/web/anonymousLogin` 与 `/web/middle/2.4/...` 是公开
|
||||
匿名接口(纯 HTTP 可调),服务端正式签发 `udb_deviceid` —— 这是“服务端正式签发
|
||||
接口”的第一个实例;`guid`/QIMEI/`udb_anobiztoken` 仍无服务端下发,只能靠复刻
|
||||
页面 SDK(`HyUDBWebSDK-Exchange-2.4.js` 等)的脚本语义。
|
||||
|
||||
### 匿名流程解密(2026-09-01,纯 HTTP 复放验证)
|
||||
|
||||
浏览器实际匿名链(未登录):
|
||||
|
||||
1. `GET udblgn.huya.com/web/middle/2.4/<8位随机>/https/<32hex>` →
|
||||
`Set-Cookie: udb_deviceid=w_<19位>; Max-Age=315360000; Domain=huya.com`,
|
||||
响应是一个加载 `HyUDBWebSDK-Exchange-2.4.js` 并 `init()` 的引导 HTML。
|
||||
2. SDK 随后发 `POST udblgn.huya.com/web/anonymousLogin`
|
||||
(`Content-Type: application/json`,`Referer` 必须为 middle URL,
|
||||
否则返回 `error!`):
|
||||
- 请求体 `{"uri":"10013","version":"2.4","context":"WB-<hex32>-<hex32>-<hex32>",
|
||||
"appId":5002,"sdid":"csid_<hex32>","lcid":2052,"byPass":3,
|
||||
"requestId":<8位>,"authId":"","data":{"domainList":""}}`
|
||||
- 响应 `returnCode:0`:`data.uid`(12 位匿名 uid)、`data.biztoken`(344 位
|
||||
URL-safe)、`Set-Cookie: udb_anouid`(7 天)+ `udb_deviceid`。
|
||||
3. 页面 SDK 把 `biztoken` 写入 `udb_anobiztoken`、`uid` 写入 `udb_anouid`,
|
||||
与 middle URL 的 32hex(= `udb_guiddata`)、`guid`、QIMEI、`__yamid_new`、
|
||||
`game_did` 一起构成网页设备态。
|
||||
|
||||
对照验证(真实浏览器最终 Cookie 与响应值一一对应):
|
||||
|
||||
| Cookie | 值形态 | 来源 |
|
||||
| --- | --- | --- |
|
||||
| `udb_deviceid` | `w_`+19 位数字 | middle/anonymousLogin `Set-Cookie`(10 年) |
|
||||
| `udb_anobiztoken` | 344 位,`AQA…` 开头 | anonymousLogin 响应 `data.biztoken` |
|
||||
| `udb_anouid` | 12 位数字 | anonymousLogin 响应 `data.uid`/`Set-Cookie`(7 天) |
|
||||
| `udb_guiddata` | 32hex | middle URL 的第三段参数(客户端自选) |
|
||||
| `guid` | 32hex | 页面 SDK 脚本写入 `document.cookie` |
|
||||
| `_qimei_uuid42` | 42 位 | QIMEI SDK 脚本写入 |
|
||||
| `__yamid_new` | 32hex | 门户业务脚本写入 |
|
||||
| `game_did` | 35 位 | 门户业务脚本写入 |
|
||||
|
||||
复放实验结论(2026-09-01):
|
||||
|
||||
- middle 的 8 位随机数与 32hex 均**不校验**:任意随机值 GET 即签发新的
|
||||
`udb_deviceid`(`returnCode 200`);
|
||||
- anonymousLogin 的 `context`/`csid`/`requestId` 均为全新随机值时,无任何
|
||||
Cookie 直接 POST 也返回 `returnCode:0` + 新 `biztoken` 与 `Set-Cookie`;
|
||||
- 因此 **`udb_deviceid`、`udb_anobiztoken`、`udb_anouid`、`udb_guiddata`
|
||||
四个字段可由纯协议生成**,不再依赖真实浏览器脚本;
|
||||
- 剩余 `guid`、`_qimei_uuid42`、`__yamid_new`、`game_did` 没有服务端签发,
|
||||
服务端是否校验其“真实性/关联性”未知 —— `200` 只代表接口接受,不代表
|
||||
风控认可。必须用协议生成的设备态跑通线上 活动 WSS/商城/支付 全链路
|
||||
才能作为最终结论,不能只凭长度测试(与本文开头结论一致)。
|
||||
@@ -0,0 +1,73 @@
|
||||
# 虎牙精英宝典联调指南
|
||||
|
||||
更新时间:2026-09-01
|
||||
|
||||
## 本次联调结论
|
||||
|
||||
当前联调阶段只验收 App 登录后的网页 Cookie 完整性,精英宝典任务统一使用 WSS:
|
||||
|
||||
| 阶段 | 结果 | 证据 |
|
||||
| --- | --- | --- |
|
||||
| WUP 登录 | 通过 | `cred=114B`,返回真实 UID |
|
||||
| hydevice 指纹 | 通过 | 返回 `sdid`、`hdid` |
|
||||
| QR 绑定 | 通过 | `getQrId`、`bindQrLoginUser`、`tryQrLogin(stage=2)` |
|
||||
| 网页 Cookie | 待重新登录验证 | 必须包含 9.1 网页设备态字段 |
|
||||
| 精英宝典任务 | 待验证 | 仅允许 WSS,会话失败即任务失败 |
|
||||
|
||||
旧登录结果中的 Cookie 缺少网页设备态,已不再作为成功登录或任务链路使用。
|
||||
|
||||
纯 App/HTTP 登录不会由 `web/cookie/verify` 下发网页设备 Cookie。浏览器只用于
|
||||
人工调试和抓取来源,未集成到脚本;当前 App 登录缺少这些字段时直接返回
|
||||
`COOKIE_INCOMPLETE`,不会伪造或自动启动浏览器。
|
||||
|
||||
## 推荐联调顺序
|
||||
|
||||
1. 在 `http://localhost:5174/` 登录一个测试账号,确认日志出现 `App协议登录成功`。
|
||||
2. 配置精英宝典 `SID`。9.1 抓包对应活动 SID 为 `2203`;以页面“精英宝典设置”中的当前值为准。
|
||||
3. 按下面顺序在精英宝典工作台执行任务:
|
||||
- `query_game_name`:验证角色绑定状态。
|
||||
- `query_points`:验证积分响应和数据库写回。
|
||||
- `query_exchange_records`:验证记录解码和页面展示。
|
||||
- `refresh_goods`:拉取商品快照。
|
||||
- 选一个明确可兑换、价格低于余额的商品,再执行 `exchange_goods`。
|
||||
4. 兑换成功后再次执行 `query_points` 和 `query_exchange_records`,核对余额扣减与新增记录。
|
||||
5. 充值/开通链路单独联调:先 `refresh_recharge_goods`,再只生成支付码 `create_recharge_order`。支付确认不要和查询任务混在同一批次。
|
||||
|
||||
## 如何判定传输通道
|
||||
|
||||
执行器只检查字段是否存在,不记录字段值:
|
||||
|
||||
```text
|
||||
必要字段:udb_cred、udb_biztoken、udb_uid、yyuid、sdid、guid、udb_guiddata、udb_deviceid、game_did、_qimei_uuid42、udb_anobiztoken、__yamid_new
|
||||
```
|
||||
|
||||
- 字段齐全:尝试活动或商城 WSS。
|
||||
- 缺少字段:App 登录返回 `COOKIE_INCOMPLETE`,任务日志列出缺失字段并停止,不创建 HTTP 客户端,例如:
|
||||
|
||||
```text
|
||||
Cookie 缺少网页设备态(guid, udb_guiddata),已停止活动 HTTP 回退
|
||||
```
|
||||
|
||||
- WSS 握手、`wsLaunch`、`getConfig` 或业务 RPC 任一步失败:任务直接失败并记录具体阶段,便于修复 Cookie 或协议参数。
|
||||
|
||||
## WSS 专项验收
|
||||
|
||||
只有从同一网页会话取得真实 `guid` 后,才做 WSS 专项联调。验收顺序为:
|
||||
|
||||
1. 活动 WSS 初始化:`wsLaunch -> AUTH -> mobileui.getConfig -> CONFIRM`。
|
||||
2. 活动 RPC:`checkUserBindGameAccount -> getUserScore -> getUserPrizeRecords`。
|
||||
3. 商城 WSS 初始化:`wsLaunch -> getConfig -> register -> CONFIRM`。
|
||||
4. 商城只读 RPC:`getGoodsInfoV5 -> listPayChannelV5 -> checkHyProtocolV5`。
|
||||
5. 最后才验证 `createOrder -> payOrderSubmit -> orderDetail`。
|
||||
|
||||
网页设备字段必须来自同一网页/设备会话;`_merge_web_cookie` 不会根据长度、正则或 UID 生成 `guid`、QIMEI、`udb_anobiztoken` 等字段。
|
||||
|
||||
## 本次测试命令
|
||||
|
||||
```bash
|
||||
.venv/bin/python -m pytest -q tests/test_huya_app_login.py tests/test_huya_runner_transport.py
|
||||
.venv/bin/python -m ruff check core/huya web/backend/services/huya_runner_core.py tests/test_huya_runner_transport.py
|
||||
git diff --check
|
||||
```
|
||||
|
||||
预期:测试通过、Ruff 无错误、差异检查无输出。真实联调以 `logs/app-YYYY-MM-DD.log` 中每个 RPC 的状态码和任务最终状态为准。
|
||||
@@ -12,7 +12,11 @@ from sqlalchemy.orm import sessionmaker
|
||||
from core.huya import (
|
||||
HuyaAppLoginError,
|
||||
)
|
||||
from core.huya.app_login import login_cred_with_flow, wup_password_login_raw
|
||||
from core.huya.app_login import (
|
||||
HuyaAppPasswordLogin,
|
||||
login_cred_with_flow,
|
||||
wup_password_login_raw,
|
||||
)
|
||||
from core.huya.cert_forge import build_p1, decrypt_cert, forge_cert, parse_p1
|
||||
from core.huya.device_fingerprint import account_state_dir, reset_account_state
|
||||
from core.huya.device_profile import (
|
||||
@@ -39,6 +43,110 @@ from web.backend.schemas import (
|
||||
)
|
||||
|
||||
|
||||
def test_app_login_merges_web_biztoken_and_device_state():
|
||||
session = MagicMock()
|
||||
session.cookies = []
|
||||
cookie = HuyaAppPasswordLogin._merge_web_cookie(
|
||||
session,
|
||||
"web-token",
|
||||
"sdid-value",
|
||||
"hdid-value",
|
||||
{"guid": "a" * 32, "udb_guiddata": "b" * 32},
|
||||
)
|
||||
assert "udb_biztoken=web-token" in cookie
|
||||
assert "sdid=sdid-value" in cookie
|
||||
assert "hdid=hdid-value" in cookie
|
||||
assert "guid=" + "a" * 32 in cookie
|
||||
assert "udb_guiddata=" + "b" * 32 in cookie
|
||||
assert "udb_deviceid=" not in cookie
|
||||
|
||||
|
||||
def test_app_login_cookie_injects_app_credentials_and_drops_transient_id():
|
||||
session = MagicMock()
|
||||
session.cookies = {"web_qrlogin_confirm_id": "one-time"}
|
||||
cookie = HuyaAppPasswordLogin._merge_web_cookie(
|
||||
session,
|
||||
"b" * 344,
|
||||
"s" * 193,
|
||||
"h" * 40,
|
||||
{"guid32": "a" * 64, "device_id": "d" * 40},
|
||||
cred=b"c" * 114,
|
||||
uid=1199666914671,
|
||||
username="hy_300023887",
|
||||
include_transient=False,
|
||||
)
|
||||
assert "udb_cred=" in cookie
|
||||
assert "udb_uid=1199666914671" in cookie
|
||||
assert "yyuid=1199666914671" in cookie
|
||||
assert "udb_passport=hy_300023887" in cookie
|
||||
assert "web_qrlogin_confirm_id" not in cookie
|
||||
|
||||
|
||||
def test_app_login_cookie_does_not_fabricate_browser_state():
|
||||
session = MagicMock()
|
||||
session.cookies = {}
|
||||
cookie = HuyaAppPasswordLogin._merge_web_cookie(
|
||||
session,
|
||||
"b" * 344,
|
||||
"s" * 193,
|
||||
"h" * 40,
|
||||
{"guid32": "a" * 64, "device_id": "d" * 40},
|
||||
cred=b"c" * 114,
|
||||
uid=1199666914671,
|
||||
username="hy_300023887",
|
||||
include_transient=False,
|
||||
)
|
||||
values = dict(item.split("=", 1) for item in cookie.split("; "))
|
||||
assert set(values) == {
|
||||
"udb_cred", "udb_uid", "yyuid", "udb_version", "udb_biztoken",
|
||||
"udb_origin", "udb_status", "sdid", "hdid", "udb_passport", "username",
|
||||
}
|
||||
assert not {"_qimei_uuid42", "_qimei_fingerprint", "_qimei_h38", "udb_anobiztoken"} & set(values)
|
||||
|
||||
|
||||
def test_app_login_cookie_overwrites_auth_but_keeps_distinct_explicit_device_values():
|
||||
session = MagicMock()
|
||||
session.cookies = {
|
||||
"udb_uid": "old",
|
||||
"udb_biztoken": "old-token",
|
||||
"guid": "old-guid",
|
||||
"udb_guiddata": "old-device-guid",
|
||||
"__yamid_new": "persisted",
|
||||
}
|
||||
cookie = HuyaAppPasswordLogin._merge_web_cookie(
|
||||
session,
|
||||
"new-token",
|
||||
"s" * 193,
|
||||
"h" * 40,
|
||||
{"guid": "a" * 32, "udb_guiddata": "b" * 32},
|
||||
cred=b"c" * 114,
|
||||
uid=1199666914671,
|
||||
username="19507362959",
|
||||
include_transient=False,
|
||||
)
|
||||
values = dict(item.split("=", 1) for item in cookie.split("; "))
|
||||
assert values["udb_uid"] == "1199666914671"
|
||||
assert values["udb_biztoken"] == "new-token"
|
||||
assert values["guid"] == "a" * 32
|
||||
assert values["udb_guiddata"] == "b" * 32
|
||||
assert values["__yamid_new"] == "persisted"
|
||||
assert "udb_passport" not in values
|
||||
|
||||
|
||||
def test_app_login_cookie_contains_web_session_shape():
|
||||
session = MagicMock()
|
||||
session.cookies = {"udb_uid": "123"}
|
||||
cookie = HuyaAppPasswordLogin._merge_web_cookie(
|
||||
session, "web-token", "s" * 193, "h" * 40,
|
||||
{"guid": "a" * 32, "udb_guiddata": "b" * 32},
|
||||
)
|
||||
keys = {item.split("=", 1)[0] for item in cookie.split("; ")}
|
||||
expected = {
|
||||
"udb_biztoken", "sdid", "hdid", "guid", "udb_guiddata",
|
||||
}
|
||||
assert expected <= keys
|
||||
|
||||
|
||||
class TestHuyaAppLogin:
|
||||
def setup_method(self):
|
||||
self.engine = create_engine("sqlite://")
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import asyncio
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from core.huya.activity_structs import ActPrizeDetailItem, GetActPrizeDetailResp
|
||||
from core.huya.http_client import SHOP_BIZ_UA, HuyaHttpClient
|
||||
from core.huya.shop_structs import (
|
||||
@@ -8,6 +11,8 @@ from core.huya.shop_structs import (
|
||||
ShopCodeRsp,
|
||||
)
|
||||
from core.huya.taf_protocol import TafInputStream, TafOutputStream, TafType
|
||||
from core.huya.wss_client import TAIL_BYTES, HuyaWssClient, WssCommand, WssMessage
|
||||
from web.backend.services.huya_runner_core import HuyaBatchRunnerCore
|
||||
|
||||
|
||||
def _round_trip(value):
|
||||
@@ -87,3 +92,39 @@ def test_shop_preflight_response_contracts():
|
||||
status.message = ""
|
||||
decoded_status = _round_trip(status)
|
||||
assert decoded_status.code == 200
|
||||
|
||||
|
||||
def test_wss_requires_web_device_cookie_fields():
|
||||
assert not HuyaBatchRunnerCore._has_web_wss_cookie(
|
||||
"udb_cred=x; udb_biztoken=x; udb_uid=1; yyuid=1"
|
||||
)
|
||||
assert HuyaBatchRunnerCore._has_web_wss_cookie(
|
||||
"udb_cred=x; udb_biztoken=x; udb_uid=1; yyuid=1; sdid=s; guid=g; "
|
||||
"udb_guiddata=gd; udb_deviceid=w_1234567890123456789; game_did=did; "
|
||||
"_qimei_uuid42=q; udb_anobiztoken=ab; __yamid_new=y"
|
||||
)
|
||||
|
||||
|
||||
def test_activity_auth_frame_matches_latest_capture_layout():
|
||||
async def build_frame():
|
||||
client = HuyaWssClient()
|
||||
client.ws = type("FakeWs", (), {"send": AsyncMock()})()
|
||||
await client.send_auth(1199664135026, "udb_cred=" + "x" * 300, sequence=0x1D000109)
|
||||
return client.ws.send.call_args.args[0]
|
||||
|
||||
message = WssMessage.decode(asyncio.run(build_frame()))
|
||||
assert message.command == WssCommand.AUTH
|
||||
assert message.sequence == 0x1D000109
|
||||
assert message.body.endswith(TAIL_BYTES)
|
||||
|
||||
stream = TafInputStream(message.body[: -len(TAIL_BYTES)])
|
||||
assert stream.read_int64(0) == 1199664135026
|
||||
assert stream.read_string(1) == "webh5&0.0.1&websocket&&diypc_52775"
|
||||
assert stream.read_head() == (2, TafType.STRING4)
|
||||
cookie_len = int.from_bytes(stream.buf.read(4), "big")
|
||||
assert cookie_len > 255
|
||||
stream.buf.read(cookie_len)
|
||||
assert stream.read_int32(3) == 0
|
||||
assert stream.read_int8(4) == 1
|
||||
assert stream.read_string(5) == "HUYA&ZH&2052"
|
||||
assert stream.read_string(6) == ""
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
"""虎牙精英宝典传输选择的联调前置检查。"""
|
||||
|
||||
from web.backend.services.huya_runner_core import HuyaBatchRunnerCore
|
||||
|
||||
|
||||
def test_minimal_app_cookie_reports_only_missing_web_guid():
|
||||
cookie = (
|
||||
"udb_cred=cred; udb_biztoken=biz; udb_uid=1199659425268; "
|
||||
"yyuid=1199659425268; sdid=sdid"
|
||||
)
|
||||
|
||||
assert HuyaBatchRunnerCore._web_wss_cookie_missing(cookie) == (
|
||||
"guid", "udb_guiddata", "udb_deviceid", "game_did",
|
||||
"_qimei_uuid42", "udb_anobiztoken", "__yamid_new",
|
||||
)
|
||||
assert not HuyaBatchRunnerCore._has_web_wss_cookie(cookie)
|
||||
|
||||
|
||||
def test_complete_web_cookie_enables_wss_without_logging_values():
|
||||
cookie = (
|
||||
"udb_cred=cred; udb_biztoken=biz; udb_uid=1199659425268; "
|
||||
"yyuid=1199659425268; sdid=sdid; guid=0123456789abcdef0123456789abcdef; "
|
||||
"udb_guiddata=guiddata; udb_deviceid=w_1234567890123456789; game_did=game; "
|
||||
"_qimei_uuid42=qimei; udb_anobiztoken=anobiz; __yamid_new=yamid"
|
||||
)
|
||||
|
||||
assert HuyaBatchRunnerCore._web_wss_cookie_missing(cookie) == ()
|
||||
assert HuyaBatchRunnerCore._has_web_wss_cookie(cookie)
|
||||
@@ -72,18 +72,41 @@ class HuyaBatchRunnerCore:
|
||||
return bucket
|
||||
|
||||
def _activity_client(self, uid: int, cookie: str):
|
||||
"""活动任务优先使用最新活动 WSS,建连失败才回退 HTTP。"""
|
||||
from core.huya.http_client import HuyaHttpClient
|
||||
|
||||
try:
|
||||
"""创建活动 WSS 会话;网页 Cookie 不完整时直接阻断任务。"""
|
||||
missing = self._web_wss_cookie_missing(cookie)
|
||||
if missing:
|
||||
detail = ", ".join(missing)
|
||||
self._push_log("error", f"[{uid}] Cookie 缺少网页设备态({detail}),已停止活动 HTTP 回退")
|
||||
raise RuntimeError(f"COOKIE_INCOMPLETE: 缺少网页设备态({detail})")
|
||||
session = self._open_wss_session(uid, cookie, "activity")
|
||||
self._push_log("info", f"[{uid}] 活动链路使用 WSS 会话")
|
||||
return session
|
||||
except Exception as exc: # noqa: BLE001 - fallback is part of transport policy
|
||||
self._push_log(
|
||||
"warning", f"[{uid}] 活动 WSS 不可用,回退 HTTP: {type(exc).__name__}"
|
||||
|
||||
@staticmethod
|
||||
def _web_wss_cookie_missing(cookie: str) -> tuple[str, ...]:
|
||||
"""返回 WSS 前置字段中缺失的名称,不输出任何 Cookie 值。"""
|
||||
required = (
|
||||
"udb_cred", "udb_biztoken", "udb_uid", "yyuid", "sdid",
|
||||
"guid", "udb_guiddata", "udb_deviceid", "game_did",
|
||||
"_qimei_uuid42", "udb_anobiztoken", "__yamid_new",
|
||||
)
|
||||
return HuyaHttpClient(logger=lambda msg: self._push_log("info", f"[{uid}] {msg}"))
|
||||
return tuple(key for key in required if not cookie_value(cookie, key))
|
||||
|
||||
@classmethod
|
||||
def _has_web_wss_cookie(cls, cookie: str) -> bool:
|
||||
"""9.1 活动 WSS 需要网页设备态,不接受只有 App 认证字段的 Cookie。"""
|
||||
return not cls._web_wss_cookie_missing(cookie)
|
||||
|
||||
def _shop_client(self, uid: int, cookie: str):
|
||||
"""创建商城 WSS 会话;网页 Cookie 不完整时直接阻断任务。"""
|
||||
missing = self._web_wss_cookie_missing(cookie)
|
||||
if missing:
|
||||
detail = ", ".join(missing)
|
||||
self._push_log("error", f"[{uid}] Cookie 缺少网页设备态({detail}),已停止商城 HTTP 回退")
|
||||
raise RuntimeError(f"COOKIE_INCOMPLETE: 缺少网页设备态({detail})")
|
||||
session = self._open_wss_session(uid, cookie, "shop")
|
||||
self._push_log("info", f"[{uid}] 商城链路使用 WSS 会话")
|
||||
return session
|
||||
|
||||
def _close_wss_sessions(self):
|
||||
bucket = self._session_bucket()
|
||||
|
||||
@@ -8,8 +8,6 @@ from typing import TYPE_CHECKING, Any
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from core.huya import HuyaHttpClient
|
||||
|
||||
from ..models import HuyaAccount, HuyaRechargeGoodsSnapshot, HuyaTask
|
||||
|
||||
HUYA_RECHARGE_ACT_ID = 25135
|
||||
@@ -43,6 +41,7 @@ class RechargeMixin:
|
||||
def _mark_task(self, *args: Any, **kwargs: Any) -> None: ...
|
||||
def _update_task_progress(self, *args: Any, **kwargs: Any) -> None: ...
|
||||
def _open_wss_session(self, uid: int, cookie: str, kind: str): ...
|
||||
def _shop_client(self, uid: int, cookie: str): ...
|
||||
|
||||
@staticmethod
|
||||
def _format_local_time(timestamp: int) -> str: ...
|
||||
@@ -205,9 +204,7 @@ class RechargeMixin:
|
||||
self._mark_task(worker_db, task, "failed", "账号 Cookie 为空")
|
||||
return
|
||||
|
||||
client: Any = HuyaHttpClient(
|
||||
logger=lambda msg: self._push_log("info", f"[{uid}] {msg}")
|
||||
)
|
||||
client: Any = self._shop_client(uid, cookie)
|
||||
task_resp = client.get_act_task_detail(
|
||||
uid=uid, cookie=cookie, act_id=HUYA_RECHARGE_ACT_ID
|
||||
)
|
||||
@@ -403,14 +400,7 @@ class RechargeMixin:
|
||||
)
|
||||
unit_price = int(snapshot.price or 0) if snapshot else 0
|
||||
|
||||
try:
|
||||
client: Any = self._open_wss_session(uid, cookie, "shop")
|
||||
self._push_log("info", f"[{uid}] 商城链路使用 WSS 会话")
|
||||
except Exception as exc: # noqa: BLE001 - HTTP fallback is explicit
|
||||
self._push_log("warning", f"[{uid}] 商城 WSS 不可用,回退 HTTP: {type(exc).__name__}")
|
||||
client = HuyaHttpClient(
|
||||
logger=lambda msg: self._push_log("info", f"[{uid}] {msg}")
|
||||
)
|
||||
client: Any = self._shop_client(uid, cookie)
|
||||
detail_resp = client.get_goods_info(
|
||||
uid=uid,
|
||||
guid="",
|
||||
@@ -579,10 +569,7 @@ class RechargeMixin:
|
||||
account.updated_at = datetime.now(UTC)
|
||||
if payment_status == "paid":
|
||||
account.status = "recharge_paid"
|
||||
score_client = client if hasattr(client, "query_user_score") else HuyaHttpClient(
|
||||
logger=lambda msg: self._push_log("info", f"[{uid}] {msg}")
|
||||
)
|
||||
post_score = score_client.query_user_score(
|
||||
post_score = client.query_user_score(
|
||||
uid=uid, cookie=cookie, sid=self._to_int(config_info.get("sid")) or 2203
|
||||
)
|
||||
if post_score is not None:
|
||||
|
||||
Reference in New Issue
Block a user