Files
live-hub-py/web/frontend/src/utils/huyaExchange.ts
T
yml2213 4518b726ac feat(huya): 浏览器兑换三目标直达并收窄扩展注入范围
- 操作列改为「直播间 / 精英宝典 / 电竞宝典」三个直达按钮,点哪个就按哪个目标写入登录态并打开,去掉弹窗内选择
- 新增 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
2026-09-11 10:31:46 +08:00

91 lines
3.2 KiB
TypeScript

/** 虎牙浏览器兑换:与内置扩展(web/extension/huya-exchange-helper)的通信桥。 */
import type { HuyaBrowserExchangeKit } from '../api/modules';
/** 扩展注入回执 (huya-exchange-result) */
export interface ExchangeInjectResult {
ok: boolean;
written: number;
failed: number;
error: string;
}
/** 解析后端 Cookie 字符串为 [name, value] 数组,供兑换扩展注入 */
export function parseCookieString(cookieString: string): [string, string][] {
return cookieString
.split(';')
.map((part) => part.trim())
.filter((part) => part.includes('='))
.map((part) => {
const idx = part.indexOf('=');
return [part.slice(0, idx), part.slice(idx + 1)];
});
}
/** 内置扩展的内容脚本会在后台站点白名单页面(见 manifest matches)打上此标记 */
export function huyaExchangeHelperReady(): boolean {
return document.documentElement.dataset.huyaExchangeHelper === '1';
}
function createRequestId(): string {
return `hx-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
}
/**
* 请求内置扩展自动注入登录态并打开目标页,等待扩展回执(4 秒超时)。
* 同时留存到 window 供扩展图标点击兜底读取。
* 注意:事件 detail 必须是 JSON 字符串(跨 world 传递限制),不要改成对象。
*/
export function requestExtensionInject(kit: HuyaBrowserExchangeKit): Promise<ExchangeInjectResult> {
const requestId = createRequestId();
const payload = JSON.stringify({
request_id: requestId,
target_url: kit.target_url,
cookies: parseCookieString(kit.cookie_string),
});
(window as unknown as { __huyaExchangeKit?: string }).__huyaExchangeKit = payload;
return new Promise<ExchangeInjectResult>((resolve) => {
let timer = 0;
let settled = false;
function finish(result: ExchangeInjectResult): void {
if (settled) return;
settled = true;
window.clearTimeout(timer);
window.removeEventListener('huya-exchange-result', onResult);
resolve(result);
}
function onResult(event: Event): void {
const detail = (event as CustomEvent<unknown>).detail;
if (typeof detail !== 'string') return;
let parsed: { request_id?: string; ok?: boolean; written?: number; failed?: number; error?: string };
try {
parsed = JSON.parse(detail) as typeof parsed;
} catch {
return;
}
if (!parsed || parsed.request_id !== requestId) return;
finish({
ok: parsed.ok === true,
written: typeof parsed.written === 'number' ? parsed.written : 0,
failed: typeof parsed.failed === 'number' ? parsed.failed : 0,
error: typeof parsed.error === 'string' ? parsed.error : '',
});
}
timer = window.setTimeout(() => {
finish({ ok: false, written: 0, failed: 0, error: '扩展未响应(未安装或安装后未刷新本页)' });
}, 4000);
window.addEventListener('huya-exchange-result', onResult);
window.dispatchEvent(new CustomEvent('huya-exchange-kit', { detail: payload }));
});
}
/** 在新标签页打开兑换页面(noopener 避免反向标签劫持) */
export function openExchangeTarget(targetUrl: string): void {
window.open(targetUrl, '_blank', 'noopener,noreferrer');
}