- 操作列改为「直播间 / 精英宝典 / 电竞宝典」三个直达按钮,点哪个就按哪个目标写入登录态并打开,去掉弹窗内选择
- 新增 services/huya_exchange_targets.py 统一三个目标地址,接口支持可选 body {"target": "..."}(不传=精英宝典)
- 扩展内容脚本从 <all_urls> 收窄到后台站点白名单,service worker 校验 sender.tab.url,拒绝其它来源的注入
- 注入改为 Cookie 写完后回执(ok/written/failed/error),前端按回执提示成功或失败,不再静默
- 扩展图标兜底改用 world:'MAIN' 读取;一个 Cookie 都没写成功时不打开目标页
- 兑换材料签发写审计 huya:exchange:kit(含账号与目标)
- 控制台脚本增加 huya.com 域校验与 Secure,与扩展路径保持一致
- 修复兑换记录轮询的跨账号串扰与卸载后 setState;兑换请求加 epoch 防止过期结果覆盖弹窗
- 新增 tests/test_huya_browser_exchange.py(13 项)并同步 docs/HUYA_BROWSER_EXCHANGE.md
107 lines
3.5 KiB
Python
107 lines
3.5 KiB
Python
"""虎牙 Cookie 规范化工具。"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from collections.abc import Iterable, Mapping
|
|
|
|
from requests.cookies import RequestsCookieJar
|
|
|
|
|
|
def cookie_pairs(cookie: str) -> list[tuple[str, str]]:
|
|
"""把浏览器 Cookie 字符串拆成 key/value 对。"""
|
|
pairs: list[tuple[str, str]] = []
|
|
for part in (cookie or "").split(";"):
|
|
item = part.strip()
|
|
if not item or "=" not in item:
|
|
continue
|
|
key, value = item.split("=", 1)
|
|
key = key.strip()
|
|
if not key:
|
|
continue
|
|
pairs.append((key, value.strip()))
|
|
return pairs
|
|
|
|
|
|
def normalize_cookie_pairs(pairs: Iterable[tuple[str, str]]) -> str:
|
|
"""按 Cookie key 去重,保留最后一次出现的值。"""
|
|
ordered_keys: list[str] = []
|
|
values: dict[str, str] = {}
|
|
for raw_key, raw_value in pairs:
|
|
key = str(raw_key).strip()
|
|
if not key:
|
|
continue
|
|
if key not in values:
|
|
ordered_keys.append(key)
|
|
values[key] = "" if raw_value is None else str(raw_value).strip()
|
|
return "; ".join(f"{key}={values[key]}" for key in ordered_keys)
|
|
|
|
|
|
def normalize_huya_cookie(
|
|
cookie: str | Mapping[str, str] | RequestsCookieJar | None,
|
|
) -> str:
|
|
"""把 Cookie 字符串、dict 或 CookieJar 转成去重后的浏览器 Cookie 字符串。"""
|
|
if not cookie:
|
|
return ""
|
|
if isinstance(cookie, str):
|
|
return normalize_cookie_pairs(cookie_pairs(cookie))
|
|
return normalize_cookie_pairs(
|
|
[
|
|
(str(key), "" if value is None else str(value))
|
|
for key, value in cookie.items()
|
|
]
|
|
)
|
|
|
|
|
|
def cookie_value(
|
|
cookie: str | Mapping[str, str] | RequestsCookieJar | None, key: str
|
|
) -> str:
|
|
"""从 Cookie 中读取 key;有重复时以最后一次出现为准。"""
|
|
if not cookie or not key:
|
|
return ""
|
|
if isinstance(cookie, str):
|
|
pairs = cookie_pairs(cookie)
|
|
else:
|
|
pairs = list(cookie.items())
|
|
value = ""
|
|
for item_key, item_value in pairs:
|
|
if item_key == key:
|
|
value = "" if item_value is None else str(item_value).strip()
|
|
return value
|
|
|
|
|
|
def browser_login_script(
|
|
pairs: Mapping[str, str],
|
|
target_url: str,
|
|
*,
|
|
domain: str = ".huya.com",
|
|
ttl_days: int = 180,
|
|
secure: bool = True,
|
|
) -> str:
|
|
"""生成在用户自己浏览器控制台执行的登录脚本。
|
|
|
|
脚本把 Cookie 写入指定域后跳转目标页;Cookie 值以 JSON 数组内联,
|
|
保证引号、反斜杠等字符安全转义。属性与扩展注入保持一致(默认 Secure),
|
|
并先校验当前页面确实在 huya.com 域下——否则 document.cookie 会静默失效,
|
|
操作者会以为已登录。
|
|
"""
|
|
payload = json.dumps(
|
|
[[key, value] for key, value in pairs.items()], ensure_ascii=False
|
|
)
|
|
target = json.dumps(target_url, ensure_ascii=False)
|
|
ttl_ms = max(1, ttl_days) * 86_400_000
|
|
suffix = ";secure" if secure else ""
|
|
return (
|
|
f"(function(){{"
|
|
f"if(!/(^|\\.)huya\\.com$/i.test(location.hostname)){{"
|
|
f"alert('请在 huya.com 域下的页面执行本脚本,当前页面:'+location.hostname);"
|
|
f"return;}}"
|
|
f"var c={payload};"
|
|
f"var e=new Date(Date.now()+{ttl_ms}).toUTCString();"
|
|
f"for(var i=0;i<c.length;i++){{"
|
|
f"document.cookie=c[i][0]+'='+c[i][1]+';domain={domain};path=/;expires='+e+'{suffix}';"
|
|
f"}}"
|
|
f"location.href={target};"
|
|
f"}})();"
|
|
)
|