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