Files
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

148 lines
4.8 KiB
JavaScript

// 虎牙兑换助手后台:接收后台页面 relay 转发的登录材料,
// 覆盖写入 huya.com 同名 Cookie 后打开所选目标页(直播间/精英宝典/电竞宝典)。
const HUYA_COOKIE_URL = 'https://www.huya.com/';
const TTL_SECONDS = 180 * 86400;
// 允许触发注入的来源站点:直接从 manifest 内容脚本白名单推导,避免两处配置漂移。
const ALLOWED_ORIGINS = (() => {
const manifest = chrome.runtime.getManifest();
const patterns = (manifest.content_scripts || []).flatMap((entry) => entry.matches || []);
const origins = new Set();
for (const pattern of patterns) {
try {
origins.add(new URL(pattern.replace(/\/\*$/, '/')).origin);
} catch (e) { /* 形如 *://*.example.com/* 的通配主机无法解析,忽略 */ }
}
return origins;
})();
function originAllowed(url) {
if (!url) return false;
try {
return ALLOWED_ORIGINS.has(new URL(url).origin);
} catch (e) {
return false;
}
}
function badge(tabId, text) {
if (!tabId) return;
chrome.action.setBadgeText({ tabId, text });
setTimeout(() => {
chrome.action.setBadgeText({ tabId, text: '' });
}, 2500);
}
async function injectKit(kit) {
const cookies = Array.isArray(kit && kit.cookies) ? kit.cookies : [];
const wanted = new Set(cookies.map(([name]) => String(name)).filter(Boolean));
// 只清理与登录态同名的旧 Cookie(覆盖所有域/路径变体),其余 huya Cookie 保持不动,
// 行为与控制台脚本方案一致,避免误伤浏览器里已有的其他虎牙状态。
const existing = await chrome.cookies.getAll({ domain: 'huya.com' });
for (const cookie of existing) {
if (!wanted.has(cookie.name)) continue;
const url = 'https://' + cookie.domain.replace(/^\./, '') + (cookie.path || '/');
try {
await chrome.cookies.remove({ url, name: cookie.name });
} catch (e) { /* 单个失败不阻断 */ }
}
const expirationDate = Math.floor(Date.now() / 1000) + TTL_SECONDS;
let written = 0;
const failedNames = [];
for (const [name, value] of cookies) {
if (!name) continue;
try {
await chrome.cookies.set({
url: HUYA_COOKIE_URL,
domain: '.huya.com',
path: '/',
name: String(name),
value: value == null ? '' : String(value),
secure: true,
expirationDate,
});
written += 1;
} catch (e) {
failedNames.push(String(name));
}
}
// 一个都没写进去时不要打开目标页:否则操作者在新标签页里看到的是未登录或上一个账号
if (written > 0 && kit && kit.target_url) {
chrome.tabs.create({ url: kit.target_url });
}
const failed = failedNames.length;
return {
ok: written > 0 && failed === 0,
written,
failed,
error: failed
? `有 ${failed} 个 Cookie 写入失败:${failedNames.slice(0, 5).join('、')}`
: written
? ''
: '没有可写入的 Cookie,请重新获取兑换材料',
};
}
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
if (!msg || msg.type !== 'inject' || !msg.kit) return undefined;
const tab = sender && sender.tab;
if (!originAllowed(tab && tab.url)) {
// 非后台站点派发的注入请求一律拒绝,页面脚本无法伪造 sender.tab.url
sendResponse({
ok: false,
written: 0,
failed: 0,
error: '来源未授权:仅允许在后台管理页面触发注入',
});
return undefined;
}
injectKit(msg.kit)
.then((result) => {
badge(tab && tab.id, result.ok ? 'OK' : '失败');
sendResponse(result);
})
.catch((error) => {
badge(tab && tab.id, '失败');
sendResponse({
ok: false,
written: 0,
failed: 0,
error: String((error && error.message) || error),
});
});
return true; // 等 Cookie 真正写完再回执,页面才能显示真实结果
});
// 兜底:后台页面把材料存到 window.__huyaExchangeKit,点击扩展图标时读取。
// 该变量由页面主世界写入,必须用 world: 'MAIN' 读取(默认 isolated world 读不到)。
chrome.action.onClicked.addListener(async (tab) => {
if (!tab || !tab.id) return;
if (!originAllowed(tab.url)) {
badge(tab.id, '非后台页');
return;
}
let payload = null;
try {
const [result] = await chrome.scripting.executeScript({
target: { tabId: tab.id },
world: 'MAIN',
func: () => window.__huyaExchangeKit || null,
});
payload = result ? result.result : null;
} catch (e) {
// 注入失败(无权限、或浏览器不支持 world: 'MAIN')不再静默
badge(tab.id, '失败');
return;
}
if (!payload) {
badge(tab.id, '无任务');
return;
}
try {
const result = await injectKit(JSON.parse(payload));
badge(tab.id, result.ok ? 'OK' : '失败');
} catch (e) {
badge(tab.id, '失败');
}
});