Skip to content

🐛 修复 @run-at context-menu:设置覆写不生效、菜单注册不上、脚本体自己的菜单被屏蔽 - #1718

Open
CodFrm wants to merge 3 commits into
mainfrom
fix/self-metadata-run-at-restore
Open

🐛 修复 @run-at context-menu:设置覆写不生效、菜单注册不上、脚本体自己的菜单被屏蔽#1718
CodFrm wants to merge 3 commits into
mainfrom
fix/self-metadata-run-at-restore

Conversation

@CodFrm

@CodFrm CodFrm commented Sep 3, 2026

Copy link
Copy Markdown
Member

Checklist / 检查清单

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

背景

脚本设置面板的「运行时机」下拉可以把一个原本自动执行的脚本改成 context-menu(手动/菜单触发),
#1649 反馈实测无效:菜单里不出现执行项,脚本仍然照常自动执行。

设置面板改运行时机只写 script.selfMetadata,脚本自带 script.metadata 不变,生效值要靠
getCombinedMeta 合并出来。排查后确认有两处代码直接读了未合并的 script.metadata
因此脚本自己在 metadata 里写 @run-at context-menu 是好的,只有「UI 覆写」这条路会坏
(这也是用 demo 脚本复现不出来的原因)。

本次改动

1. RuntimeService.restoreJSCodeFromCompiledResourceruntime.ts

全量重新注册时会从 CompiledResource 缓存还原脚本代码,选编译分支用的是脚本自带 metadata。
用户覆写的 run-at / early-start 在这里被丢弃:

  • context-menu 覆写 → 还原出的是裸代码,没有 GM_registerMenuCommand(...) 包装 → 脚本恢复自动执行;
  • early-start 覆写 → 退化成普通注入。

改为先算 getCombinedMeta(script.metadata, script.selfMetadata),三个分支判断(isScriptletUnwrap
/ isEarlyStartScript / isContextMenuScript)都基于生效 metadata。

触发这条路径的是任何一次全量重新注册:扩展更新(CompiledResourceNamespace 变更强制清理重建)、
切换「启用脚本」、修改黑名单、userScripts 权限重新授予。所以表现是「刚设置完像是生效了,之后就永久失效」。

顺带修掉同文件 pushValueUpdate 里同源的一处:覆写而来的 early-start 脚本在 GM 值变更后不会重新编译,
预注入代码里的值会过期。

2. GMApi.parseRequestgm_api/gm_api.ts

GMApiRequest.script 直接用 scriptDAO.get() 的原始 Script,metadata 未合并。
PermissionVerify.verify 对 context-menu 脚本的 GM_registerMenuCommand@grant 豁免
permission_verify.ts:135)因此判不出来,浏览器里直接报
verify error {"api":"GM_registerMenuCommand","error":"permission not requested"},菜单项注册不上。
在从 DAO 取出脚本后合并一次 selfMetadata,位置在订阅 connect 覆盖之前,订阅覆盖仍然优先。

3. compileScriptCodeByResourcecontent/utils.ts

@run-at context-menu 的包装把脚本体塞进菜单回调时,回调开头把 GM_registerMenuCommand 连同
window. / GM. 上的引用一起置为 undefined。任何在脚本体里注册菜单的脚本,点菜单执行就会
TypeError: GM_registerMenuCommand is not a function 当场中断,它自己的菜单项也永远注册不上 ——
表现为「GM_registerMenuCommand 的菜单显示不出来」。该置空还会污染页面 window 与该脚本的 GM 物件,
且是持久的(不止这一次调用)。

这一处与 selfMetadata 无关:脚本直接在 metadata 里写 @run-at context-menu 同样中招,已实测确认。
去掉这行后脚本体里的菜单注册照常工作。

4. getCombinedMetaservice_worker/utils.ts 第二参数放宽为 SCMetadata | undefined
函数体本来就有 if (!metaCustom) return metaRet 守卫,此前所有调用点都在外面先判一次,该分支实际是死代码。

实现考虑

  • 两处都选择在「读到 Script 之后、做判断之前」合并,而不是在每个判断点各打一次补丁 —— 根因是
    「该用生效 metadata 的地方用了原始 metadata」,在源头合并可以覆盖同一对象的其它消费者
    (如 getConnectMatched(request.script.metadata.connect, …))。
  • parseRequest 里合并时新建对象({...script, metadata: …}),不就地改写 DAO 返回的对象。
  • 今天 UI 能写进 selfMetadata 的只有 match/include/exclude/run-in/run-at/early-start/tag,
    其中只有 run-at 影响 GM 权限校验,所以第 2 点在当前 UI 下等价于只修 context-menu 豁免,
    但按生效 metadata 走的语义与 runtime 其余部分一致。

已知限制

  • 已经被错误注册过的脚本,需要再发生一次重新注册(或改动脚本触发 installScript)才会重编译成正确代码。
    没有加迁移/一次性修复逻辑 —— 扩展更新本身就会触发全量重新注册。
  • 浏览器验证里「点菜单执行脚本」这一步用的是 popup 与 chrome.contextMenus.onClicked 共用的
    serviceWorker/popup/menuClick 消息,未覆盖 chrome.contextMenus 条目创建与浏览器原生点击派发本身。
  • 去掉包装里的置空后,脚本体每次被点执行都会重新注册一次自己的菜单,内部条目累积。
    显示层按 groupKey 去重,不会出现重复菜单项;但同名项的回调会被触发多次(实测:脚本体跑过 2 次后,
    点它注册的菜单项回调执行 2 次)。这是「脚本体按次执行」的固有结果,本 PR 未额外去重。
  • [Feature] 增加手动执行脚本的功能 #1649 里另一条反馈「菜单状态只认第一个窗口的分页」(chrome.contextMenus 是全局的、按活动标签页渲染)
    不在本 PR 范围内,未处理。

建议审查重点

  • restoreJSCodeFromCompiledResource 的三个分支现在都基于合并后的 metadata,
    其中前两个分支内部又调用 buildScriptRunResource(本身会再合并一次),确认没有语义漂移。
  • parseRequest 合并的位置:在订阅 connect 覆盖之前,保证订阅声明的 connect 仍然覆盖脚本自身的。
  • 非 context-menu 脚本的 GM 权限校验路径没有被这次合并改变行为。
  • 包装里去掉 GM_registerMenuCommand=undefined 后是否有其它依赖该屏蔽的行为(我没找到,但这是行为契约变更)。

关联

close #1649

验证

Base/head:61164f6920e736fd3a03b269fb907e7aa7975432...1e811242c6ecb67225a008ea43fb5b2921856203
git diff --stat 共 7 个文件(4 个生产文件 + 3 个测试文件),无其它清理。

单元测试(先 RED 后 GREEN)

新增 4 条回归测试。先 git stash 掉生产改动确认全部失败,恢复后全部通过:

$ npx vitest run src/app/service/service_worker/runtime.test.ts      # 52 passed
$ npx vitest run src/app/service/service_worker/gm_api/              # 61 passed
$ npx vitest run src/app/service/content/utils.test.ts               # 38 passed
$ npx vitest run src/app/service/service_worker/                     # 586 tests, 2 failing 文件单独跑全过(本机负载所致)

RED 时的失败输出与浏览器现象一致,例如 gm_api_self_metadata.test.ts 报的就是
Error: permission not requested

真实浏览器验证e2e/session.mjs,Chrome + dist/ext,探针脚本 @run-at document-idle + @grant none
匹配本地 127.0.0.1:8765 页面)

修复前(用 origin/main 的两份文件构建)复现了 issue 的两个症状:

01:51:42 [error] (src/service_worker.js) verify error
         {"api":"GM_registerMenuCommand","error":"permission not requested"}   ← 菜单注册不上
01:53:24 sw getScripts() → [{"id":"aaa16bad-…","hasMenuWrapper":false}]        ← 切「启用脚本」关→开后包装丢失
01:53:32 [log] (http://127.0.0.1:8765/?after-reregister) SC-PROBE-EXECUTED     ← 脚本恢复自动执行

修复后同一套操作:

sw getScripts() → [{"id":"88af7526-…","hasMenuWrapper":true}]                  ← 重新注册后包装仍在
sw storage.session['tabScript:…'] → menus:["run-at override probe"]            ← 菜单项注册成功
goto http://127.0.0.1:8765/?r2-after-reregister → 之后无 SC-PROBE-EXECUTED,无权限报错
触发 menuClick 后 → 02:04:39 SC-PROBE-EXECUTED                                 ← 点菜单才执行

第三处(包装屏蔽菜单注册)同样是浏览器里先复现的:装一个脚本体里注册
Own Item A / Own Item B 的脚本,改成 context-menu 后

menus: ["own menu probe"]                                  ← 自己的两项没了
点菜单 → TypeError: GM_registerMenuCommand is not a function ← 脚本体从这行中断

改完之后:

点菜单 → OWN-MENU-RUN,无报错
menus: ["own menu probe","Own Item A","Own Item B"]
contextMenus.create: ScriptCat → own menu probe → Own Item A / Own Item B

同样的复现在「脚本自己声明 @run-at context-menu」时也成立,证实与 selfMetadata 那条路无关。

未跑完的检查pnpm test 全量在本次会话中被同机其它项目的构建占满 CPU(load average 141),
跑出大量 UI 用例超时失败,单独跑均通过;改动前在同一机器空闲时有 4505/4505 全绿基线。
prettier --check / tsc --noEmit / check:i18n / check:issue-templates / eslint 均 exit 0。

restoreJSCodeFromCompiledResource 用脚本自带 metadata 选择编译分支,
而设置面板改运行时机/early-start 只写 selfMetadata,导致全量重新注册
(扩展更新、切换启用脚本、改黑名单等)后覆写被丢弃:context-menu 脚本
恢复自动执行且不注册菜单项,early-start 退化为普通注入。

pushValueUpdate 判断 early-start 时同样只看自带 metadata,覆写而来的
early-start 脚本在 GM 值变更后不会重新编译,预注入代码里的值会过期。

close #1649
GMApi.parseRequest 直接把 scriptDAO 里的原始 Script 放进 GMApiRequest,
metadata 没有合并 selfMetadata。PermissionVerify 对 context-menu 脚本的
GM_registerMenuCommand 免 @grant 豁免因此判不出来,浏览器里表现为
verify error {"api":"GM_registerMenuCommand","error":"permission not requested"},
菜单项注册不上 —— 即 #1649 里「上下文菜单中没有出现执行选项」。

真实浏览器验证记录见 e2e/scratch/run-at-override/report.md(未入库)。
@run-at context-menu 的包装把脚本体塞进菜单回调时,回调开头把
GM_registerMenuCommand 连同 window./GM. 上的引用一起置为 undefined。
于是任何在脚本体里注册菜单的脚本,点菜单执行就会
TypeError: GM_registerMenuCommand is not a function 当场中断,
它自己的菜单项也永远注册不上——用户看到的是「GM_registerMenu 的菜单显示不出来」。
该置空还会污染页面 window 与该脚本的 GM 物件,且是持久的。

去掉这行,脚本体里的菜单注册照常工作。代价是脚本体每次被点执行都会重新注册
一次,内部条目累积(显示层按 groupKey 去重,不会出现重复菜单项,但同名项的
回调会被触发多次)。

真实浏览器验证记录见 e2e/scratch/ctx-menu-{display,fix}/(未入库)。
@CodFrm CodFrm changed the title 🐛 修复脚本设置里覆写运行时机为 context-menu 不生效 🐛 修复 @run-at context-menu:设置覆写不生效、菜单注册不上、脚本体自己的菜单被屏蔽 Sep 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature] 增加手动执行脚本的功能

1 participant