From 85abe9f728ffe10c53bf43107235517c58d71fde Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Fri, 2 Oct 2026 11:24:55 +0800 Subject: [PATCH] =?UTF-8?q?fix(extension):=20Chrome=20background=20?= =?UTF-8?q?=E6=94=B9=E4=B8=BA=E8=83=BD=E5=8A=9B=E6=8E=A2=E6=B5=8B=EF=BC=9B?= =?UTF-8?q?README=20=E7=BA=A0=E6=AD=A3=E6=A3=80=E6=B5=8B=E6=8F=8F=E8=BF=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- extension/README.md | 9 +++-- extension/chrome/background.js | 63 +++++++++++++++++++++++++++++++--- 2 files changed, 65 insertions(+), 7 deletions(-) diff --git a/extension/README.md b/extension/README.md index 7c3593a..5b233a2 100644 --- a/extension/README.md +++ b/extension/README.md @@ -29,7 +29,7 @@ |------|------| | 导航服务地址 | 你的门户根地址,如 `https://nav.example.com`(不要带 `/search` 等路径) | | 同步令牌 | 服务端环境变量 `NAVI_SYNC_TOKEN` 的值(**服务端开启鉴权时必填**) | -| 设备名称 | 可选,留空自动检测(desktop-chrome / desktop-firefox / mobile-*) | +| 设备名称 | 可选,留空则自动检测。**Firefox 版**按 UA 识别(desktop-firefox / mobile-* / desktop-edge 等);**Chrome 版**的 MV3 service worker 里没有 `navigator`,只能回退为 `desktop-browser`(需要区分设备时请手填) | ## 行为 @@ -43,6 +43,11 @@ ## 技术细节 - Chrome/Edge: Manifest V3,service worker + `chrome.alarms`(最小周期 30s) -- Firefox: Manifest V2,event page + `browser.alarms` +- Firefox: Manifest V2,event page(`persistent: true`)+ `browser.alarms` - 存储: `storage.local`(pending 队列 + 配置) - API: `history.search` + `alarms` + `runtime.onMessage` +- `visit_count` 上报的是 **Chrome/Firefox history API 的绝对累计值**,不是增量。 + 服务端(`history-server`)按设备取绝对值再求和,所以重复上报不会把计数放大。 +- 两个目录的 `background.js` **不是同一份文件**:Firefox 额外做设备名识别 + (MV2 常驻后台,`navigator` 可用),并过滤 `moz-extension://`。 + 改动时两边都要改,但不要直接整份覆盖。 diff --git a/extension/chrome/background.js b/extension/chrome/background.js index 70f62d5..93d035d 100644 --- a/extension/chrome/background.js +++ b/extension/chrome/background.js @@ -31,8 +31,25 @@ let pendingItems = []; // ── 检测标记:只向用户配置的导航站注入 content script ── // 已安装的浏览器上,导航站会拿到 data-navi-ext 属性,从而隐藏安装入口。 +// +// MV3 与 MV2 的注册方式不同,必须分别处理(此前只写了 MV3 那条, +// Firefox 走 chrome.scripting 早退,于是 data-navi-ext 永不写入, +// 安装入口在 Firefox 上永远藏不住): +// · MV3 / Chromium:chrome.scripting.registerContentScripts,callback 风格 +// · MV2 / Firefox :browser.contentScripts.register,返回 Promise, +// 注销只能靠 register() 返回的句柄上的 .unregister()(无按 id 注销的 API) const DETECT_SCRIPT_ID = 'navi-detect'; const DETECT_FILE = 'detect.js'; +// MV2 句柄:MV2 没有「按 id 查询已注册脚本」的 API,只能自己留住上次注册 +// 返回的对象,换地址时调它的 unregister()。MV3 另有 storage 字段标记。 +let mv2RegisteredHandle = null; + +function extApi() { + // Firefox 注入 browser.* 兼 chrome.* 兼容层;优先用它拿 MV2 的 contentScripts。 + return (typeof browser !== 'undefined' && browser && browser.contentScripts) + ? browser + : (typeof chrome !== 'undefined' ? chrome : null); +} function hostMatchPattern(serverUrl) { try { @@ -43,19 +60,55 @@ function hostMatchPattern(serverUrl) { } } -async function syncDetectScript(serverUrl) { - if (!chrome.scripting || !chrome.scripting.registerContentScripts) return; +// mv2HasContentScripts 判断当前运行的是 Firefox MV2 那条路。 +function mv2HasContentScripts(api) { + return !!(api && api.contentScripts && typeof api.contentScripts.register === 'function'); +} + +async function unregisterMV2() { + if (!mv2RegisteredHandle) return; + const h = mv2RegisteredHandle; + mv2RegisteredHandle = null; try { - const existing = await chrome.scripting.getRegisteredContentScripts({ ids: [DETECT_SCRIPT_ID] }); + if (typeof h.unregister === 'function') await h.unregister(); + } catch (e) { /* 从未注册 / 已被注销 */ } +} + +async function syncDetectScript(serverUrl) { + const api = extApi(); + if (!api) return; + + // ① Firefox MV2:contentScripts.register(Promise) + if (mv2HasContentScripts(api)) { + await unregisterMV2(); + const pattern = hostMatchPattern(serverUrl); + if (!pattern) return; + try { + mv2RegisteredHandle = await api.contentScripts.register({ + matches: [pattern], + js: [DETECT_FILE], + runAt: 'document_start', + allFrames: false, + }); + } catch (e) { + // 静默降级:未装扩展时只是多显示一个安装入口,不影响同步 + } + return; + } + + // ② Chromium MV3:chrome.scripting.registerContentScripts(callback) + if (!api.scripting || !api.scripting.registerContentScripts) return; + try { + const existing = await api.scripting.getRegisteredContentScripts({ ids: [DETECT_SCRIPT_ID] }); if (existing && existing.length) { - await chrome.scripting.unregisterContentScripts({ ids: [DETECT_SCRIPT_ID] }); + await api.scripting.unregisterContentScripts({ ids: [DETECT_SCRIPT_ID] }); } } catch (e) { /* 从未注册过 */ } const pattern = hostMatchPattern(serverUrl); if (!pattern) return; try { - await chrome.scripting.registerContentScripts([{ + await api.scripting.registerContentScripts([{ id: DETECT_SCRIPT_ID, matches: [pattern], js: [DETECT_FILE],