Skip to content

🦊 启用 Firefox MV3 构建并完善 sandbox/offscreen 通信#1561

Open
cyfung1031 wants to merge 39 commits into
scriptscat:mainfrom
cyfung1031:pr/ff3-mv3-002
Open

🦊 启用 Firefox MV3 构建并完善 sandbox/offscreen 通信#1561
cyfung1031 wants to merge 39 commits into
scriptscat:mainfrom
cyfung1031:pr/ff3-mv3-002

Conversation

@cyfung1031

@cyfung1031 cyfung1031 commented Jul 11, 2026

Copy link
Copy Markdown
Collaborator

Checklist / 检查清单

  • Fixes mentioned issues / 修复已提及的问题(无关联 Issue)
  • Code reviewed by human / 代码通过人工检查
  • Changes tested / 已完成测试

Description / 描述

本 PR 启用 ScriptCat 的 Firefox MV3 构建,并修复 Firefox 下 sandbox、event page 与 Service Worker 之间的启动时序和跨上下文通信问题。

主要改动

  • 启用 Firefox MV3 打包

    • 开启 Firefox 构建产物生成。
    • 保留 Firefox manifest 中的 sandbox 配置,并补充 sandbox CSP。
    • 将 Firefox 最低版本设为 154.0a1,以匹配 manifest sandbox 支持。
    • Chrome 构建继续移除 Firefox 专用的 sandbox CSP,避免影响现有 Chromium 路径。
  • 重构 sandbox/offscreen 就绪流程

    • 由 sandbox 在自身初始化完成后主动发送 preparationSandbox,不再由父层猜测、轮询或伪造就绪状态。
    • offscreen/event page 收到信号后再通知 Service Worker 环境已就绪。
    • 增加 15 秒兜底超时,sandbox 未能上报时仍可继续启动,并输出明确日志,避免永久阻塞。
    • 复用已有的 getExtensionEnv 请求执行非阻塞通道自检,并将检测结果上报到父层日志。
  • 修复 Firefox 下的消息通道问题

    • Firefox MV3 中 event page 与 Service Worker 运行在同一脚本/进程内,新增 InProcessMessage 处理 offscreen → Service Worker 通信,避免 chrome.runtime.sendMessage 向自身发送时出现 Receiving end does not exist
    • WindowMessage 支持惰性解析目标窗口,避免 iframe 尚处于 about:blank 时过早缓存 contentWindow,导致导航到跨源 sandbox 页面后消息来源匹配失败。
    • MessageQueue.publish() 主动捕获无接收方时 Firefox 返回的 rejected Promise,避免产生未处理的 Promise rejection。
  • 补充测试与文档

    • 新增或更新 sandbox 就绪、进程内消息桥接、惰性窗口目标及无接收方广播等单元测试。
    • 更新架构文档,说明 Firefox MV3 下 sandbox/offscreen 的初始化和通信机制。

兼容性说明

  • Firefox 构建要求 154.0a1 或更高版本。
  • Chromium 继续使用真实 offscreen document,现有通信路径保持不变。
  • 本 PR 不包含界面改动。

验证

  • tsc --noEmit
  • eslint
  • prettier --check
  • 完整测试套件通过:292 个测试文件、3099 个测试用例

Screenshots / 截图

不适用:本 PR 仅涉及构建配置、启动协议和内部消息通信。

@cyfung1031 cyfung1031 changed the title ♻️ 修复 Firefox MV3 sandbox 就绪握手与 offscreen↔SW 通信问题 ♻️ ScriptCat MV3 正式支持 Firefox - 修复 Firefox MV3 sandbox 就绪握手与 offscreen↔SW 通信问题 Jul 11, 2026
@cyfung1031 cyfung1031 changed the title ♻️ ScriptCat MV3 正式支持 Firefox - 修复 Firefox MV3 sandbox 就绪握手与 offscreen↔SW 通信问题 🦊 启用 Firefox MV3 构建并完善 sandbox/offscreen 通信 Jul 11, 2026
@cyfung1031

cyfung1031 commented Jul 11, 2026

Copy link
Copy Markdown
Collaborator Author

b6329d4 修正了 定时脚本 没有自动执行的问题


RuntimeService(service worker 侧)和 ScriptService(offscreen 侧)各自持有一个独立的 MessageQueue 实例。两者之间的跨上下文消息传递,完全依赖 MessageQueue.publish() 通过 chrome.runtime.sendMessage() 广播。

在 Chrome 中,这一机制可以正常工作,因为 offscreen document 运行在独立进程中。

但在 Firefox 中,EventPageOffscreenManager 与 service worker 代码运行在同一个脚本和 Frame 内。根据 WebExtensions 的 runtime.onMessage 规则,广播会发送到扩展中的其他 Frame,但不会触发发送者所在 Frame 的监听器。

因此,当 service worker 侧调用:

publish("enableScripts", ...)

消息无法到达 offscreen 侧队列的监听器,因为两个队列都位于同一个发送者 Frame 中。

cron 调度机制本身没有问题。使用 createCronJob* * * * * * 进行独立测试时,3.5 秒内按计划触发了 3 次,说明问题不在 cron 包的集成。

两种执行方式的差异如下:

手动运行通过 runScript() 执行,使用 this.msgSender 进行点对点 RPC,也就是 IOffscreenSend / InProcessMessage 桥接。这条路径可以正常工作。

自动调度依赖 RuntimeServicepreparationOffscreen 订阅者发布:

this.mq.publish("enableScripts", res)

该消息需要触发 ScriptService"enableScripts" 的订阅,然后才能在 sandbox 中执行:

enableScript()crontabScript()

但在 Firefox 中,这条广播无法到达,因此 crontabScript() 根本不会被调用。

实际问题不是“任务执行一次后停止”,而是任务从一开始就没有完成调度。

同一问题还会导致以下消息无法同步到 Firefox 的 sandbox:

  • deleteScripts
  • installScript
  • setSandboxLanguage

这些失败不会产生明显错误,因此容易被忽略。

修复方案

BackgroundEnvManagerBase 增加可注入的 messageQueue 构造参数,默认值仍为 new MessageQueue()

这样不会影响 Chrome 的 OffscreenManager。在 Chrome 中,offscreen 和 service worker 本来就运行在不同进程中,因此仍然需要各自独立的队列。

EventPageOffscreenManager 现在要求通过第二个构造参数传入队列。service_worker.ts 会将现有的 messageQueue 实例传给它,避免基类再创建一个互不连通的新实例。

共享同一个 MessageQueue 实例后,publish() 内部的本地 this.EE.emit() 就可以直接完成两侧通信,无需再通过 chrome.runtime.sendMessage() 中转。

新增测试覆盖以下场景:

  • 注入的 MessageQueue 实例会被正确保留,不会被新的实例覆盖。
  • 在共享队列上调用 publish() 后,offscreen 侧订阅能够正常收到消息。

@cyfung1031 cyfung1031 added P0 🚑 需要紧急处理的内容 Pre-FF-MV3 labels Jul 11, 2026
@cyfung1031

cyfung1031 commented Jul 11, 2026

Copy link
Copy Markdown
Collaborator Author

Firefox retains WebRequest blocking in MV3

https://blog.mozilla.org/addons/2022/10/31/begin-your-mv3-migration-by-implementing-new-features-today/

因此 Firefox MV3版 用了 WebRequest blocking 来避免 Event Page 被 kill 掉 (定时脚本需要这个设计)

mozilla 不接受再处理吧
例如先关掉这功能,之后再换成 Alarms

@cyfung1031
cyfung1031 requested a review from CodFrm July 11, 2026 04:29
@cyfung1031

cyfung1031 commented Jul 11, 2026

Copy link
Copy Markdown
Collaborator Author

在使用 startFirefoxEventPageKeepAliveLoop 情况下,// @crontab * * * * * 顺利按时执行

Screenshot 2026-07-12 at 0 32 36

@cyfung1031

Copy link
Copy Markdown
Collaborator Author

@cyfung1031

Copy link
Copy Markdown
Collaborator Author

唯一是 keep_alive 那个实现 可能要再改良一下
但不做 keep_alive 的话定时脚本跑不动

@cyfung1031

Copy link
Copy Markdown
Collaborator Author

我在用 Fable 5 解決 vitest 問題
你試一下這個 keep_alive 有沒有解決 #1343
我這邊已經重視不了

fdfb04e 把 startFirefoxEventPageKeepAliveLoop 移入 keep_alive.ts 并在模块顶层
加上 isFirefox() 门控;测试环境(happy-dom)非 Firefox,保活分支变成 no-op,
event_page_manager.test.ts 中 4 个用例失败。另外 77ac32f 起探测 <img> 不再
挂载到 DOM,且探测 URL 需由 runtime.getURL 派生。

- vi.mock isFirefox(与 RuntimeSection.test.tsx 同一模式)配合 resetModules 生效
- 用可追踪的 Image 替身取代 DOM img 查询来断言探测请求
- 测试内探测 URL 改用与 keep_alive.ts 相同的派生规则

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@cyfung1031

Copy link
Copy Markdown
Collaborator Author

vitest 失败根因与修复(ddd90d6f)

分支最近几次提交曾导致 src/app/service/offscreen/event_page_manager.test.ts 中「Firefox event page 保活权限门控」的 4 个用例失败,已在 ddd90d6 修复,现全套件 292 文件 / 3111 用例全部通过。

根因

  1. fdfb04e 起保活循环增加运行时 Firefox 门控。 startFirefoxEventPageKeepAliveLoop 被移入 src/app/service/offscreen/keep_alive.ts,并在模块顶层以 const boolFirefox = isFirefox() 固化门控。vitest 的 happy-dom 环境没有 mozInnerScreenX / navigator.mozGetUserMediaisFirefox() 返回 false,整个保活循环在测试中编译为 no-op:不注册 webRequest.onBeforeRequest blocking listener,也不注册 permissions.onAdded,对应断言全部落空。
  2. 77ac32f 起探测 <img> 不再挂载 DOM。 新实现仅靠变量引用防止 GC(new Image() 发起网络请求无需附加到 DOM),测试里 document.documentElement.querySelectorAll("img") 的断言因此过时。
  3. 测试内探测 URL 与派生规则不一致。「移除后恢复」用例硬编码 https://--extensions-test.invalid/...,与 keep_alive.tschrome.runtime.getURL() 派生的探测域名(mock 扩展 id 为 kfjdomqetnlhbgxasrwzypcviueotmlr)不匹配,listener 的 URL 过滤会直接放行而非返回 Promise。该用例与门控改动同批加入,从未在生效状态下运行过。

修复方式(仅测试侧,产品行为为有意变更、保持不动)

  • RuntimeSection.test.tsx 的既有模式 vi.mock("@App/pkg/utils/utils") 注入 isFirefoxMock,配合已有的 vi.resetModules() 动态导入,使保活分支在测试中按 Firefox 语义生效。
  • 用可追踪的 Image 替身记录探测请求,取代 DOM <img> 查询;并补充断言探测 URL 携带 __network_delay=
  • 「移除后恢复」用例的探测 URL 改用与 keep_alive.ts 相同的 runtime.getURL 派生规则,URL 匹配路径得到真实覆盖;恢复后断言共发起 2 次探测(首拍 + 重新授权)。

验证

  • npx vitest run src/app/service/offscreen/event_page_manager.test.ts:9/9 通过
  • pnpm test:292 文件 / 3111 用例全部通过
  • tsc --noEmiteslintprettier --check:通过

🤖 Generated with Claude Code

@CodFrm

CodFrm commented Jul 14, 2026

Copy link
Copy Markdown
Member

@CodFrm

讓scriptcat追上firefox的154 發佈吧

https://www.neowin.net/news/mozilla-firefox-to-follow-google-chrome-microsoft-edge-with-new-release-cadence/

我处理完回收站和cloud sync的问题和pr就来处理ff

@CodFrm

CodFrm commented Jul 17, 2026

Copy link
Copy Markdown
Member

Firefox MV3 真浏览器 E2E 验证(最终更新)

本评论记录 PR #1561 的 Firefox 真浏览器验证。本轮只做验证和记录,未修改产品代码,也未提交修复。

  • 验证 commit:2e8e623b0587938d239549a4c43dbee05103e9a2
  • 版本策略:Firefox Nightly >= 154.0a1,优先使用验证时 latest;本次 Mozilla latest 实际为 154.0a1
  • Firefox:Nightly 154.0a1
  • geckodriver:0.37.0,参数 --allow-system-access
  • 产物:pnpm run pack 生成的 dist/scriptcat-v1.5.0-beta-firefox.zip
  • 安装:WebDriver moz/addon/install,temporary unsigned addon
  • 特权检查:Marionette chrome context + WebExtensionPolicy.getByID()

为什么仓库现有 E2E 不能替代本次验证

当前 Playwright fixture 和 CI 是 Chromium 专用,直接使用:

  • chromium.launchPersistentContext()
  • --load-extension
  • chrome-extension://
  • context.serviceWorkers()
  • keep-alive 用例中的 CDP Target.*

GitHub Actions 也只安装 Playwright Chromium。因此现有四组 E2E 全绿是重要回归信号,但不能证明 Firefox 扩展实际可用。本次验证单独使用 Firefox Nightly + geckodriver + Selenium。

最终结果矩阵

范围 结果 真浏览器观察
Firefox MV3 ZIP 安装 / policy PASS WebExtensionPolicy.active=true,真实 moz-extension:// base URL
Options 页面 / 16 个路由 PASS 稳定 DOM anchor 全部渲染
@background event page → sandbox PASS ×2 status=1runStatus=completeerror=nulltype=3
UI 安装 / 搜索 / 启停 / 编辑 / 删除 PASS ×2 两个全新 Firefox session 均通过;编辑保存到 1.1.0,删除后 active record 消失
普通 userscript 注入 PASS ×2 真实 HTTP 页面产生预期 DOM mutation
GM value / style API PASS ×2 GM_setValue/getValue/listValues/deleteValue、持久化、GM_addStyle 均通过
GM XHR / @connect PASS ×2 声明域请求 200 且自定义 Header 到达;未声明域触发 onerror,服务端收到 0 次请求
Firefox 站点访问权限 PASS ScriptCat confirm 页 → Firefox 原生权限面板 → Allow → origin permission → XHR 完成
Popup / standalone 页面 PASS popup、install、import error、batchupdate empty、confirm expired 均正常
Settings / UserConfig YAML PASS 设置刷新后保持;正常 YAML 持久化;__proto__ hostile key 被拒绝且未污染原型
Backup ZIP 导出 / 导入 PASS Firefox 生成真实 ZIP,unzip -t 通过,导入页正确列出备份脚本
@crontab 自动调度 PASS */5 * * * * *,未调用手动 run API;证据服务器收到 26 次 tick
keep-alive UI / 权限 / probe PASS UI 开启后 webRequestBlocking=true、配置为 true;3 个 __network_delay=10000 probe 间隔约 10 秒
无 ScriptCat 页面时持续运行 PASS(观察) 导航到 about:blank 后观察 55 秒;background 一直 running,cron 持续执行;开启阶段总观察约 91 秒
关闭 keep-alive 后配置恢复 PASS UI 关闭后权限移除,配置变为 false
关闭后强制证明一定 suspend INCONCLUSIVE / tooling limitation 关闭后 35 秒 background 仍 running。Marionette 没有等价于 CDP Target.closeTarget 的 browser-managed suspend 触发方式;强杀 context 会绕过产品逻辑,因此未做,不归类为产品失败

关键证据

@background

{
  "status": 1,
  "runStatus": "complete",
  "error": null,
  "type": 3
}

真实链路为:扩展页面 chrome.runtime.sendMessage → Firefox event page → in-process/window message → sandbox → 后台脚本 → storage 回写。

@crontab / keep-alive

{
  "enabledTicks": 19,
  "postDisableTicks": 7,
  "totalTicks": 26,
  "cronIntervalMinMs": 4843,
  "cronIntervalMaxMs": 5081,
  "cronIntervalAvgMs": 5000,
  "probesStartedEnabled": 3,
  "probeIntervalsMs": [10005, 10005],
  "finalScriptState": {
    "status": 1,
    "type": 2,
    "runStatus": "complete",
    "error": null
  }
}

最终日志未发现 ScriptCat 的 Receiving end does not exist、sandbox、crontab 或 keep-alive 错误。浏览器控制台中的其余 warning 来自首次安装打开的文档/analytics 页面,与验证链路无关。

Firefox-specific harness 说明

  1. Firefox chrome.userScripts 需要可选 userScripts 权限。一次性测试 profile 通过 Firefox 自身 ExtensionPermissions 授权并 reload addon;未修改 ZIP 或产品源码。
  2. 原生站点权限 doorhanger 不是 WebDriver JS alert;本次在 Marionette chrome context 中观察并点击真实 Allow
  3. Backup 的 saveAs 原生对话框不适合 headless WebDriver;测试使用产品页面真实提供的 manual-download blob link,随后校验 ZIP 并走真实导入 UI。
  4. Chromium keep-alive spec 的 CDP Target.* 无法等价迁移到 Firefox;只将无法制造 browser-managed suspend 的关闭对照标为 tooling limitation。
  5. 另外使用 headed Firefox + Xvfb 保存了带完整 Firefox 标签栏、地址栏和 moz-extension:// URL 的浏览器壳截图;页面级 screenshot 与浏览器壳 screenshot 分开保留。

可复跑流程

统一原则:scratch harness 和证据均放在 git-ignored 目录,不改正式 fixture。

NODE_PATH=/root/.cache/scriptcat-selenium-node/node_modules \
  node e2e/scratch/firefox/<verification-script>.js

验证脚本覆盖:

  • 基础 MV3 / background sandbox smoke
  • UI core
  • normal userscript + GM API + site access
  • pages + data / backup
  • crontab + keep-alive
  • headed Firefox browser-shell capture

本地附加门禁:typecheck 通过;相关 Vitest 7 files / 62 tests 通过;build 通过;Firefox pack 通过。

结论

在当前验证范围内,PR #1561 在 Firefox Nightly 154.0a1 的真实 MV3 环境中,扩展加载、event page、sandbox 初始化、跨上下文通信、普通脚本、核心 GM API、权限、页面、数据和 crontab/keep-alive 主链路均通过。未发现需要在本轮修改产品代码的 Firefox 功能失败。

唯一未形成强结论的是“关闭 keep-alive 后强制让 Firefox 自动 suspend event page”的对照,因为 Selenium/Marionette 没有可靠且不绕过浏览器生命周期的触发方法。

image

scriptcat-pr1561-firefox-evidence-20260718-0200.zip


最新 Firefox MV3 最终验收补充(HEAD cc4dd7fd

上文保留的是早期验证记录;PR 最新提交已重新完成整套 Firefox headed E2E:

  • Tested commit:cc4dd7fda2c6cee53cc844a31fbfaf8d53b6e3de
  • Required scenarios:10/10 PASS
  • 11 段 H.264/yuv420p、1440×900 MP4:全部 video probe PASS
  • 119 张截图,包含 Firefox chrome/address bar 的 browser-shell 证据
  • 测试前后 tracked tree 均 clean
  • Final recommendation:MERGE RECOMMENDED

公开验收报告、关键截图及原始附件:

其中早期 normal/GM 两轮红项已确认是 Firefox/Xvfb 下通知 registry 很快消失造成的验收时序假阴性;最终使用 GM_notificationoncreate 回调与有效 notification ID 作为创建证据,两轮均通过,未修改产品代码,也未削弱功能断言。

@cyfung1031 cyfung1031 added this to the 2026七月 Milestone milestone Jul 18, 2026
@CodFrm

CodFrm commented Jul 18, 2026

Copy link
Copy Markdown
Member

更正:Codex 复核后发现上一版把 harness 早退误判为 XHR pending。

本次更正仅针对仓库两份完整 GM API Example 的证据口径。复核环境如下:

  • HEAD:cc4dd7fda2c6cee53cc844a31fbfaf8d53b6e3de
  • Firefox Nightly:154.0a1
  • geckodriver:0.37.0
  • 平台:Linux x64
  • Firefox MV3 包:dist/scriptcat-v1.5.0-beta-firefox.zip
  • 扩展包 SHA-256:86a06318cae4ee528dde8578fb6a5d5599a6766c6c70b92524bfccbfcac75d97

已确认的事实:

  • 同步源码为未修改的 example/tests/gm_api_sync_test.js,源码与安装后存储代码的 SHA-256 均为 dee214fd91ef25850eba84545ce36667137d911099c9283d03beb98d1984b41a
  • 异步源码为未修改的 example/tests/gm_api_async_test.js,源码与安装后存储代码的 SHA-256 均为 7b3597c56e699b12199668a87255c94c60142d090f51af4068e17bd5fe5723d2
  • 两份结构化 evidence 中的预期检查清单均为 29 项,与源码测试项一致。
  • 同步版采集到 13 条 PASS、1 条明确 FAIL,另有 15 项未采集到结果;失败项是 GM_addElement - 创建元素
  • 异步版采集到 13 条 PASS、0 条明确 FAIL,另有 16 项未采集到结果。两版均未生成最终结果卡片。

同步版的确定失败可以直接由页面 CSP 解释:测试插入的 inline script 内容为 window.foo = "bar";,其 SHA-256 为 sha256-3Ix5X7n8Um9e8ysSYO+Qgu0ily2jAfQViRcdYI1fe8g=,与 Firefox CSP 报错建议的哈希完全一致。因此可以写为:该次失败直接由页面 CSP 拒绝 inline script 解释。 异步版的 GM.addElement 用例没有 inline script 执行断言,所以异步版该项通过与同步版的 CSP 失败并不矛盾。

通知方面,两份源码都把通知测试串行放在首个 XHR 用例之后;当前日志中既没有“GM 通知 API 测试”标题,也没有 GM_notification / GM.notification 用例日志。因此现有证据只能说明:本轮采集窗口内未观察到执行到通知测试。 不能据此证明通知成功或失败,也不能说通知被 CSP 阻止。

最关键的证据纠错是等待时间:harness 虽然报称等待 120000ms,此前版本还报称 150000ms,但结构化 JSON 显示“等待结果卡片”步骤实际仅运行了:

  • 同步版:3022ms
  • 异步版:1829ms

geckodriver 日志也显示只查询了一次结果卡片,随后即关闭 session。两次采集都没有真正等待超过 Example 自带的 XHR timeout: 10000。因此当前 evidence 不能证明 XHR 一直 pending、请求没有 settle、ontimeout 未触发,也不足以归因于 ScriptCat 的 Firefox MV3 XHR 产品缺陷。

网络证据同样需要限定:BiDi 网络侧只订阅了 network.responseCompletednetwork.fetchError,且未必完整覆盖扩展后台发起的请求。日志中没有发现 httpbun.com,只能表述为“当前 BiDi 捕获中未见”,不能据此证明请求没有发出。

权限方面,当前结果为 confirmations=[],并且只进行了一次窗口观察。预授予 httpbun.comexample.com origins 的脚本返回了 ok,但没有再单独调用 chrome.permissions.contains({ origins: [...] }) 复核实际权限状态。因此权限不是当前首选解释,但现有材料也不能将其完全排除。

综上,当前可靠结论是:

  1. 两份完整 Example 都未被 harness 有效运行到最终结果,XHR 与后续用例的产品行为尚未完成验证。
  2. 同步版另有一个确定的 inline script CSP 失败。
  3. GM_notification / GM.notification 的成功或失败仍未得到本轮 evidence 验证。
  4. 现阶段不应将结果定性为 Firefox MV3 XHR pending 或 ScriptCat XHR 产品缺陷。

下一步应先修正 harness:使用 monotonic clock 计算 timeout,记录每一次结果卡片与窗口轮询,并确保实际等待至少 15~20 秒后再重跑。重跑时还需要补充扩展后台/扩展 context 的网络及错误证据,独立复核 origin permissions,并将 notification 拆出,通过 oncreate 单独验证创建行为。在这些证据补齐前,两份完整 Example 应标记为“验证未完成”,而不是“已确认 XHR 失败”。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

P0 🚑 需要紧急处理的内容 Pre-FF-MV3

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants