refactor(bf-bot): 插件重构为 handlers/services/utils 分层,资源归入 res/
- 旧平铺模块(data/data_utils/database_op/image_builder*/get_bf6_data/ gametools_bf6/bf6_data/img_utils)拆分为 handlers(绑定/查询)、 services(fetchers/processing/query)、utils(渲染/参数)分层; 新增 repository.py / config.py / paths.py - 字体/图片/模板资源迁入 res/,新增 BF2042 卡面素材(bg/object_icon 等) - browser_ext/:BF2042 扩展捕获桥(WS 直连兜底通道)源码入库 - 新增 hexi/web/src/plugins/bf_bot 前端管理页 - 新增 tests:test_bf_bot_helpers / repository / tracker_api Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
1 parent
614e1d7e00
commit
a31aab00e8
752 files changed
+6241
-2506
No files matched your search
@@ -0,0 +1,80 @@
|
||||
# HeXi BF2042 捕获桥(浏览器扩展)
|
||||
|
||||
被动捕获 tracker.gg 的 BF2042 API 响应,经 WebSocket 转发给本机 bot。
|
||||
服务端有两个等价实现:bot 插件内的 `services/fetchers/tracker_bridge.py`(正式,
|
||||
随 bot 启停,端口/token 是 /hub 插件配置页里的 `bf_ext_ws_port`/`bf_ext_ws_token`),
|
||||
以及 `debug/bf2042_bridge_server.py`(独立联调用,不启 bot)。两侧默认
|
||||
`ws://127.0.0.1:39272/?token=hexi-bf2042`。
|
||||
|
||||
## 原理
|
||||
|
||||
历史通道要走 `--remote-debugging-port` + Playwright `connect_over_cdp`。本扩展把捕获
|
||||
搬进浏览器内部,参照 FetchV / 猫抓一类捕获扩展的通行模式:
|
||||
|
||||
- `inject.js`(主世界,document_start)用 Proxy 包住 `fetch` / `XMLHttpRequest`,
|
||||
被动观察**页面自己发出**的请求(不改写、不重放,只读 clone 的响应体);
|
||||
- `content.js`(隔离世界)把捕获经 `chrome.runtime` 转给 SW
|
||||
(内容脚本连 `ws://127.0.0.1` 会被页面 CSP 的 connect-src 拦掉,所以 WS 必须放 SW);
|
||||
- `background.js`(SW)持有到 bot 的 WebSocket,转发捕获、执行取数任务:
|
||||
找/开 tracker.gg 标签页 → 回首页落稳 → 站内跳转进 profile 页 → 等页面发 XHR。
|
||||
- **失败重试**:单轮 22s 内关键响应没齐(多半撞了 CF 挑战/403,页面 XHR 拿不到
|
||||
数据),自动重走"首页→站内跳转"刷新,最多 3 轮;每轮结束做页面健康检查
|
||||
(挑战页/封禁页/登录过期)并打日志。
|
||||
|
||||
**不需要任何调试端口**,浏览器就是个普通 Edge 实例,可以常驻不关。
|
||||
|
||||
## 安装(一次性)
|
||||
|
||||
1. 用专用配置开 Edge(登录态 + CF 信任都在里面):
|
||||
```
|
||||
msedge.exe --user-data-dir="<repo>\hexi\data\nonebot_plugin_bf_bot\edge_profile"
|
||||
```
|
||||
2. 地址栏开 `edge://extensions` → 打开"开发人员模式" → "加载解压缩的扩展" → 选本目录。
|
||||
3. 若有"开发者模式扩展"启动提示,保留即可(纯提醒,不影响)。
|
||||
|
||||
代码改动后在 `edge://extensions` 点该扩展的"刷新"按钮生效;SW 日志在扩展详情页点
|
||||
"Service Worker" 检查。
|
||||
|
||||
## 联调(不启 bot)
|
||||
|
||||
```
|
||||
.venv\Scripts\python.exe debug\bf2042_bridge_server.py # 手动浏览观察
|
||||
.venv\Scripts\python.exe debug\bf2042_bridge_server.py --job EAID # 自动取数全流程
|
||||
```
|
||||
|
||||
预期输出:先 "扩展已连接";手动打开任意 BF2042 profile 页会打印被动捕获;
|
||||
`--job` 时扩展自动完成首页→站内跳转,打印 4 个响应,最后 "任务完成 keys=..."。
|
||||
|
||||
注意:bot 在跑时 39272 已被 bot 自己的扩展桥占用——要跑独立联调服务端得先停
|
||||
bot(或临时改扩展 `background.js` 的端口常量)。bot 在跑时直接看 bot 日志里
|
||||
"BF2042 扩展桥" 相关行即可,不用另起服务端。
|
||||
|
||||
## WebSocket 协议(v0)
|
||||
|
||||
服务端:`ws://127.0.0.1:39272/?token=hexi-bf2042`(仅回环 + 共享 token;
|
||||
常量在 `background.js` 顶部,bot 侧对应配置项为 `bf_ext_ws_port`/`bf_ext_ws_token`,
|
||||
两边保持一致)。
|
||||
|
||||
| 方向 | 消息 |
|
||||
|---|---|
|
||||
| ext→bot | `{"type":"hello","name":"hexi-bf2042-bridge","version":"0.1.0"}` |
|
||||
| bot→ext | `{"type":"fetch","id":"...","player":"X","platform":"origin","profileUrl":"https://tracker.gg/bf2042/profile/origin/X/overview","apiBase":"https://api.tracker.gg/api/v2/bf2042/standard/profile/origin/X"}` |
|
||||
| ext→bot | `{"type":"capture","id":null 或任务id,"key":"profile\|weapon\|vehicle\|soldier","url":"...","status":200,"body":{...}}` |
|
||||
| ext→bot | `{"type":"job_done","id":"...","keys":["profile","weapon","vehicle"]}` |
|
||||
| ext→bot | `{"type":"job_error","id":"...","keys":[...],"detail":"..."}` |
|
||||
| 双向 | `{"type":"ping"}` |
|
||||
|
||||
- 无任务时的捕获以 `id: null` 被动转发。
|
||||
- 任务的 URL 匹配"两遍 decode 再比",消除 python `quote()` 与 JS
|
||||
`encodeURIComponent` 的转义差异。
|
||||
- 任务期间只认自己驱动的标签页(`tabId` 过滤)。
|
||||
|
||||
## 注意
|
||||
|
||||
- 导航定律保留:首页 bootstrap → 站内跳转(`location.href`);深链直开会被挑战。
|
||||
- MV3 SW 保活:活跃 WebSocket(Chromium 116+)+ `chrome.alarms`(30s)双保险,
|
||||
bot 重启后扩展自动重连。
|
||||
- 若手动浏览时页面有数据、扩展却捕获不到 → 该请求可能不经过页面 `fetch`/`XHR`
|
||||
(如 Worker 发起)。届时把捕获后端换成 `chrome.debugger`(只开 Network 域),
|
||||
管道其余部分不用动。
|
||||
- 本目录不要放 `.py`(它是插件目录子目录的一部分,保持纯前端文件)。
|
||||
@@ -0,0 +1,283 @@
|
||||
// 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 });
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
// 隔离世界桥(document_start):把主世界钩子 postMessage 上来的捕获转发给 SW。
|
||||
//
|
||||
// WS 由 SW 持有 —— 内容脚本直接连 ws://127.0.0.1 会被页面 CSP 的 connect-src 拦掉。
|
||||
|
||||
(() => {
|
||||
"use strict";
|
||||
|
||||
const MARKER = "__hexi_bf2042_capture__";
|
||||
|
||||
window.addEventListener("message", (event) => {
|
||||
if (event.source !== window) return;
|
||||
const data = event.data;
|
||||
if (!data || data[MARKER] !== true) return;
|
||||
try {
|
||||
chrome.runtime.sendMessage({
|
||||
type: "captured",
|
||||
url: String(data.url || ""),
|
||||
status: data.status,
|
||||
body: data.body,
|
||||
});
|
||||
} catch (_) {
|
||||
// SW 正在重启等场景,丢弃即可(数据可重查,不值得重试)
|
||||
}
|
||||
});
|
||||
})();
|
||||
@@ -0,0 +1,102 @@
|
||||
// 主世界钩子(document_start):被动观察页面自己发出的请求,把命中的响应体副本
|
||||
// postMessage 给 content.js。参照 FetchV / 猫抓一类捕获扩展的通行模式。
|
||||
//
|
||||
// 铁律:只观察,不改写请求与响应;任何异常都必须静默,绝不能影响页面本身。
|
||||
|
||||
(() => {
|
||||
"use strict";
|
||||
|
||||
const MARKER = "__hexi_bf2042_capture__";
|
||||
const API_PREFIX =
|
||||
"https://api.tracker.gg/api/v2/bf2042/standard/profile/";
|
||||
|
||||
function match(url) {
|
||||
return typeof url === "string" && url.startsWith(API_PREFIX);
|
||||
}
|
||||
|
||||
function report(url, status, body) {
|
||||
try {
|
||||
window.postMessage({ [MARKER]: true, url, status, body }, "*");
|
||||
} catch (_) {
|
||||
/* 忽略:上报失败不能影响页面 */
|
||||
}
|
||||
}
|
||||
|
||||
// ---- fetch ----
|
||||
// 用 Proxy 包一层:Function.prototype.toString.call() 显示为 native
|
||||
// (Proxy 的 target 是原生函数),比"改写后补 toString"更难被识破。
|
||||
if (typeof window.fetch === "function") {
|
||||
const orig = window.fetch;
|
||||
window.fetch = new Proxy(orig, {
|
||||
apply(target, thisArg, args) {
|
||||
const input = args[0];
|
||||
const url =
|
||||
typeof input === "string" ? input : (input && input.url) || "";
|
||||
const result = Reflect.apply(target, thisArg, args);
|
||||
if (match(url)) {
|
||||
result
|
||||
.then((resp) => {
|
||||
try {
|
||||
const ctype = resp.headers.get("content-type") || "";
|
||||
if (!ctype.includes("json")) return;
|
||||
// 立刻 clone,免得页面随后消费了原响应体
|
||||
resp
|
||||
.clone()
|
||||
.json()
|
||||
.then((body) => report(url, resp.status, body))
|
||||
.catch(() => {});
|
||||
} catch (_) {}
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
return result;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// ---- XMLHttpRequest ----
|
||||
const XHR = window.XMLHttpRequest;
|
||||
if (XHR && XHR.prototype) {
|
||||
const origOpen = XHR.prototype.open;
|
||||
const origSend = XHR.prototype.send;
|
||||
const urls = new WeakMap(); // 挂在实例上会留可见的自定义属性,用 WeakMap 更干净
|
||||
|
||||
XHR.prototype.open = new Proxy(origOpen, {
|
||||
apply(target, thisArg, args) {
|
||||
try {
|
||||
urls.set(thisArg, String(args[1] ?? ""));
|
||||
} catch (_) {}
|
||||
return Reflect.apply(target, thisArg, args);
|
||||
},
|
||||
});
|
||||
|
||||
XHR.prototype.send = new Proxy(origSend, {
|
||||
apply(target, thisArg, args) {
|
||||
try {
|
||||
const url = urls.get(thisArg);
|
||||
if (match(url)) {
|
||||
thisArg.addEventListener(
|
||||
"loadend",
|
||||
() => {
|
||||
try {
|
||||
const rt = thisArg.responseType;
|
||||
let body = null;
|
||||
if (rt === "" || rt === "text") {
|
||||
body = JSON.parse(thisArg.responseText);
|
||||
} else if (rt === "json") {
|
||||
body = thisArg.response;
|
||||
} else {
|
||||
return; // arraybuffer / blob 等类型不处理
|
||||
}
|
||||
if (body != null) report(url, thisArg.status, body);
|
||||
} catch (_) {}
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
}
|
||||
} catch (_) {}
|
||||
return Reflect.apply(target, thisArg, args);
|
||||
},
|
||||
});
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "HeXi BF2042 捕获桥",
|
||||
"version": "0.1.0",
|
||||
"description": "被动捕获 tracker.gg 的 BF2042 响应并经 WebSocket 转发给本机禾汐 bot。浏览器无需开调试端口。",
|
||||
"minimum_chrome_version": "116",
|
||||
"permissions": ["tabs", "scripting", "alarms"],
|
||||
"host_permissions": [
|
||||
"https://tracker.gg/*",
|
||||
"https://*.tracker.gg/*",
|
||||
"http://127.0.0.1/*"
|
||||
],
|
||||
"background": {
|
||||
"service_worker": "background.js"
|
||||
},
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": ["https://tracker.gg/*", "https://*.tracker.gg/*"],
|
||||
"js": ["inject.js"],
|
||||
"run_at": "document_start",
|
||||
"world": "MAIN",
|
||||
"all_frames": true
|
||||
},
|
||||
{
|
||||
"matches": ["https://tracker.gg/*", "https://*.tracker.gg/*"],
|
||||
"js": ["content.js"],
|
||||
"run_at": "document_start",
|
||||
"all_frames": true
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in new issue
Block a user