虎牙人工兑换改用本机浏览器并新增扩展自动注入登录态

移除服务器浏览器方案(容器/Xvfb/noVNC/CDP 会话管理),后端仅下发
兑换材料;新增内置 Chrome/Edge 扩展一键写入 huya.com 登录态,
未安装扩展时回退控制台脚本手动注入,兑换始终由用户手动完成。
This commit is contained in:
yml2213
2026-09-05 17:55:31 +08:00
parent 95ad900ef0
commit 230d6c1918
15 changed files with 477 additions and 4 deletions
@@ -0,0 +1,78 @@
// 虎牙兑换助手后台:接收后台页面 relay 转发的登录材料,
// 覆盖写入 huya.com 同名 Cookie 后打开已登录的兑换页。
const HUYA_COOKIE_URL = 'https://www.huya.com/';
const TTL_SECONDS = 180 * 86400;
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;
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,
});
} catch (e) { /* 单个失败不阻断 */ }
}
if (kit && kit.target_url) {
chrome.tabs.create({ url: kit.target_url });
}
}
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
if (msg && msg.type === 'inject' && msg.kit) {
injectKit(msg.kit).then(() => badge(sender.tab && sender.tab.id, 'OK'));
sendResponse({ ok: true });
}
return undefined;
});
// 兜底:后台页面把材料存到 window.__huyaExchangeKit,点击扩展图标时读取。
chrome.action.onClicked.addListener(async (tab) => {
if (!tab || !tab.id) return;
let payload = null;
try {
const [result] = await chrome.scripting.executeScript({
target: { tabId: tab.id },
func: () => window.__huyaExchangeKit || null,
});
payload = result ? result.result : null;
} catch (e) {
return; // 无权访问当前页面
}
if (!payload) {
badge(tab.id, '无任务');
return;
}
try {
await injectKit(JSON.parse(payload));
badge(tab.id, 'OK');
} catch (e) {
badge(tab.id, '失败');
}
});
@@ -0,0 +1,69 @@
"""生成虎牙兑换助手扩展图标:橙色渐变圆角底 + 白色礼盒。
128/48 用完整图案;16px 细节会糊,改用去掉蝴蝶结的加粗简化版。
用法: uv run python web/extension/huya-exchange-helper/generate_icons.py
"""
from PIL import Image, ImageDraw
S = 1024 # 超采样画布,最后缩到目标尺寸
RADIUS = 200
TOP = (255, 156, 71) # 渐变顶部
BOTTOM = (240, 84, 0) # 渐变底部
BOX = (255, 255, 255) # 礼盒
RIBBON = (234, 74, 8) # 丝带(比背景深,压在白色上可见)
def gradient_rounded(size: int, radius: int) -> Image.Image:
grad = Image.new('RGBA', (size, size))
g = ImageDraw.Draw(grad)
for y in range(size):
t = y / (size - 1)
color = tuple(round(TOP[i] + (BOTTOM[i] - TOP[i]) * t) for i in range(3)) + (255,)
g.line([(0, y), (size, y)], fill=color)
mask = Image.new('L', (size, size), 0)
ImageDraw.Draw(mask).rounded_rectangle([0, 0, size, size], radius=radius, fill=255)
out = Image.new('RGBA', (size, size), (0, 0, 0, 0))
out.paste(grad, (0, 0), mask)
return out
def draw_gift(img: Image.Image) -> None:
d = ImageDraw.Draw(img)
cx = S // 2
# 蝴蝶结:左右两个椭圆
d.ellipse([348, 235, 512, 365], fill=BOX)
d.ellipse([512, 235, 676, 365], fill=BOX)
# 盒盖(比盒身宽)
d.rounded_rectangle([cx - 235, 375, cx + 235, 485], radius=30, fill=BOX)
# 盒身
d.rounded_rectangle([cx - 200, 509, cx + 200, 789], radius=36, fill=BOX)
# 竖向丝带贯穿盖与盒身
d.rectangle([cx - 44, 375, cx + 44, 789], fill=RIBBON)
# 结口圆点,压在蝴蝶结与盒盖交界
d.ellipse([cx - 46, 330, cx + 46, 422], fill=RIBBON)
def draw_gift_simple(img: Image.Image) -> None:
"""小尺寸版:去掉蝴蝶结,盒体加粗。"""
d = ImageDraw.Draw(img)
cx = img.width // 2
scale = img.width / 512
def rect(x1, y1, x2, y2, radius):
d.rounded_rectangle([x1 * scale, y1 * scale, x2 * scale, y2 * scale],
radius=radius * scale, fill=BOX)
rect(cx / scale - 144, 96, cx / scale + 144, 206, 24) # 盖
rect(cx / scale - 112, 230, cx / scale + 112, 420, 28) # 盒身
d.rectangle([cx - 34 * scale, 96 * scale, cx + 34 * scale, 420 * scale], fill=RIBBON)
full = gradient_rounded(S, RADIUS)
draw_gift(full)
base = 'web/extension/huya-exchange-helper/'
for size in (128, 48):
full.resize((size, size), Image.LANCZOS).save(f'{base}icon{size}.png')
simple = gradient_rounded(512, 100)
draw_gift_simple(simple)
simple.resize((16, 16), Image.LANCZOS).save(f'{base}icon16.png')
print('icons regenerated')
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 653 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

@@ -0,0 +1,26 @@
{
"manifest_version": 3,
"name": "虎牙兑换助手",
"version": "1.0.0",
"description": "配合直播后台使用:一键把所选虎牙账号的登录态写入本机浏览器(仅 huya.com 域),兑换操作仍由用户手动完成。",
"permissions": ["cookies", "scripting", "activeTab"],
"host_permissions": ["*://*.huya.com/*"],
"background": {
"service_worker": "background.js"
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["relay.js"],
"run_at": "document_start"
}
],
"action": {
"default_title": "虎牙兑换助手"
},
"icons": {
"16": "icon16.png",
"48": "icon48.png",
"128": "icon128.png"
}
}
@@ -0,0 +1,17 @@
// 内容脚本:在所有页面标记扩展存在,并把后台页面派发的兑换材料转发给 service worker。
// 后台页面通过 documentElement.dataset.huyaExchangeHelper === '1' 检测扩展是否可用。
document.documentElement.dataset.huyaExchangeHelper = '1';
window.addEventListener('huya-exchange-kit', (event) => {
const detail = event.detail;
if (typeof detail !== 'string') return;
try {
const kit = JSON.parse(detail);
if (kit && kit.target_url && Array.isArray(kit.cookies)) {
const sending = chrome.runtime.sendMessage({ type: 'inject', kit });
if (sending && typeof sending.catch === 'function') {
sending.catch(() => { /* 扩展上下文失效等场景忽略 */ });
}
}
} catch (e) { /* 忽略非法消息 */ }
}, true);