// HeXi BF2042 捕获桥 - Service Worker // // 两个职责: // 1) 持有到本机 bot 的 WebSocket(自动重连 + 心跳),转发页面捕获的响应; // 2) 执行 bot 下发的取数任务:找/开 tracker.gg 标签页 → 首页 bootstrap → // 站内跳转进 profile 页 → 等页面自己发出 XHR → 上报 job_done。 // // 导航定律(来自插件文档,勿改):地址栏深链会被 Cloudflare 挑战,必须先落首页、 // 再在站内跳转。全程不用任何调试端口,浏览器就是个普通实例。 // // 重试机制:页面撞 CF 挑战/403 时,页面自己的 XHR 不会产出数据,本轮 // ATTEMPT_WAIT_MS 内关键响应收不齐 → 自动重走"首页→站内跳转"(刷新等价), // 最多 MAX_ATTEMPTS 轮;提前收齐即收工。 const WS_URL = "ws://127.0.0.1:39272/"; const WS_TOKEN = "hexi-bf2042"; // 仅本机回环 + 共享 token;修改时与 bot 侧配置同步 const HOMEPAGE = "https://tracker.gg/"; const CAPTURE_PREFIX = "https://api.tracker.gg/api/v2/bf2042/standard/profile/"; const JOB_KEYS = ["profile", "weapon", "vehicle"]; // 齐了即收工(soldier 可选) const SETTLE_MS = 3000; // 首页落稳再跳,镜像 python 通道的 sleep(3) const JOB_TIMEOUT_MS = 150000; // 重试:单轮 X 秒内关键数据没齐 = 多半撞了 CF 挑战/403,重走"首页→站内跳转"刷新 const ATTEMPT_WAIT_MS = 22000; const MAX_ATTEMPTS = 3; let ws = null; let retryMs = 1000; let job = null; // { id, base, tabId, keys:Set, attempts, timer } const log = (...a) => console.log("[bf2042-bridge]", ...a); const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); // ------------------------------------------------------------------ WS function connect() { if (ws) return; // CONNECTING / OPEN 状态都不重复建 try { ws = new WebSocket(`${WS_URL}?token=${encodeURIComponent(WS_TOKEN)}`); } catch (_) { scheduleReconnect(); return; } ws.onopen = () => { retryMs = 1000; log("已连接 bot"); send({ type: "hello", name: "hexi-bf2042-bridge", version: "0.1.0" }); }; ws.onmessage = (ev) => { try { handleMessage(JSON.parse(ev.data)); } catch (_) {} }; ws.onclose = () => scheduleReconnect(); ws.onerror = () => { try { ws.close(); } catch (_) {} }; } function scheduleReconnect() { ws = null; setTimeout(connect, retryMs); retryMs = Math.min(retryMs * 2, 30000); // 1s → 2s → ... → 上限 30s } function send(obj) { if (ws && ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify(obj)); return true; } return false; } // 心跳:兼作 MV3 SW 保活(Chromium 116+ 活跃 WS 本就会保活,alarms 双保险, // 覆盖"bot 重启导致断线、SW 又在重连定时器前被回收"的窗口)。 chrome.alarms.create("keepalive", { periodInMinutes: 0.5 }); chrome.alarms.onAlarm.addListener((alarm) => { if (alarm.name !== "keepalive") return; if (!ws) connect(); else send({ type: "ping" }); }); connect(); // ------------------------------------------------------------------ 工具 function norm(url) { return String(url || "").split("?")[0].replace(/\/+$/, ""); } // 两边都 decode 再比,消除 python quote() 与 JS encodeURIComponent 的转义差异 function canon(url) { try { return decodeURIComponent(norm(url)); } catch (_) { return norm(url); } } function isTracker(url) { return /^https:\/\/(www\.)?tracker\.gg\//.test(String(url || "")); } function keyOf(url) { const u = norm(url); for (const k of ["weapon", "vehicle", "soldier", "gadget"]) { if (u.endsWith(`/segments/${k}`)) return k; } return "profile"; // profile 基础响应(gamemode 等段都在它里面) } // ------------------------------------------------------------------ 任务 function handleMessage(msg) { if (!msg || typeof msg !== "object") return; if (msg.type === "fetch") startJob(msg).catch(() => {}); } async function startJob(msg) { if (job) return send({ type: "job_error", id: msg.id, detail: "busy" }); if (!msg.profileUrl || !msg.apiBase) { return send({ type: "job_error", id: msg.id, detail: "bad_request" }); } job = { id: msg.id, base: canon(msg.apiBase), tabId: null, keys: new Set(), attempts: 0, timer: setTimeout(() => finishJob("timeout"), JOB_TIMEOUT_MS), }; try { const tab = await ensureTab(); job.tabId = tab.id; await chrome.tabs.update(tab.id, { active: true }); for (let attempt = 1; attempt <= MAX_ATTEMPTS && job; attempt++) { job.attempts = attempt; // 严格镜像已验证路径:每轮都先落首页,停顿,再站内跳转 try { await chrome.tabs.update(tab.id, { url: HOMEPAGE }); await waitComplete(tab.id); } catch (_) { break; // 标签页没了 } await sleep(SETTLE_MS); log(`第 ${attempt}/${MAX_ATTEMPTS} 轮,站内跳转:`, msg.profileUrl); // setTimeout 0:让注入脚本先结束再触发跳转,避免 "frame detached" 噪声 chrome.scripting .executeScript({ target: { tabId: tab.id }, world: "MAIN", func: (u) => { setTimeout(() => { window.location.href = u; }, 0); }, args: [msg.profileUrl], }) .catch(() => {}); const ok = await waitForKeys(tab.id, ATTEMPT_WAIT_MS); if (!job) return; // 期间已收工/超时 if (ok) return finishJob("done"); const health = await pageHealth(tab.id); log(`第 ${attempt} 轮未收齐(${health}),${attempt < MAX_ATTEMPTS ? "刷新重试" : "放弃"}`); } if (job) finishJob("no_data", "多轮重试后仍未捕获到数据"); } catch (e) { finishJob("navigate_failed", String(e)); } } // 等待关键响应齐(或任务被外部收工);返回是否收齐 function waitForKeys(tabId, timeoutMs) { return new Promise((resolve) => { const t0 = Date.now(); const timer = setInterval(() => { if (!job) { clearInterval(timer); resolve(false); return; } if (JOB_KEYS.every((k) => job.keys.has(k))) { clearInterval(timer); resolve(true); return; } if (Date.now() - t0 >= timeoutMs) { clearInterval(timer); resolve(false); } }, 500); }); } // 页面健康检查:区分"CF 挑战/封禁"与"页面正常但没数据"(仅供日志) async function pageHealth(tabId) { try { const [res] = await chrome.scripting.executeScript({ target: { tabId }, func: () => { const t = (document.title || "") + " " + (document.body?.innerText || "").slice(0, 300); return t.slice(0, 200); }, }); const t = String(res?.result || ""); if (/just a moment|安全验证|正在进行|verify you are human/i.test(t)) return "CF挑战页"; if (/you'?ve been blocked|been blocked/i.test(t)) return "TRN封禁页"; if (/username sign in|sign in with your tracker/i.test(t)) return "登录过期"; return "页面无挑战"; } catch (_) { return "健康检查失败(标签页可能已关闭)"; } } async function ensureTab() { const tabs = await chrome.tabs.query({ url: ["https://tracker.gg/*", "https://*.tracker.gg/*"], }); if (tabs.length) return tabs[0]; const tab = await chrome.tabs.create({ url: HOMEPAGE, active: true }); await waitComplete(tab.id); return tab; } function waitComplete(tabId, timeoutMs = 30000) { return new Promise((resolve) => { const done = () => { clearTimeout(timer); chrome.tabs.onUpdated.removeListener(listener); resolve(); }; const timer = setTimeout(done, timeoutMs); const listener = (id, info) => { if (id === tabId && info.status === "complete") done(); }; chrome.tabs.onUpdated.addListener(listener); chrome.tabs .get(tabId) .then((t) => { if (t.status === "complete") done(); }) .catch(done); }); } function finishJob(reason, detail) { if (!job) return; clearTimeout(job.timer); const { id, keys } = job; job = null; if (reason === "done") { send({ type: "job_done", id, keys: [...keys] }); } else { send({ type: "job_error", id, keys: [...keys], detail: detail || reason }); } } // ------------------------------------------------------------------ 捕获回传 chrome.runtime.onMessage.addListener((msg, sender) => { if (!msg || msg.type !== "captured") return; const url = String(msg.url || ""); if (!url.startsWith(CAPTURE_PREFIX)) return; const key = keyOf(url); if (job) { // 只认我们驱动的那个标签页,避免把用户同时手动浏览的流量掺进来 if (job.tabId != null && sender.tab && sender.tab.id !== job.tabId) return; if (!canon(url).startsWith(job.base)) return; // 不是这次任务那位玩家的 job.keys.add(key); send({ type: "capture", id: job.id, key, url, status: msg.status, body: msg.body, }); if (JOB_KEYS.every((k) => job.keys.has(k))) finishJob("done"); return; } // 无任务:被动转发(id 为 null),人工浏览也顺手喂数据/便于联调 send({ type: "capture", id: null, key, url, status: msg.status, body: msg.body }); });