- 操作列改为「直播间 / 精英宝典 / 电竞宝典」三个直达按钮,点哪个就按哪个目标写入登录态并打开,去掉弹窗内选择
- 新增 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
91 lines
3.2 KiB
TypeScript
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');
|
|
}
|