🐛 修复 @run-at context-menu:设置覆写不生效、菜单注册不上、脚本体自己的菜单被屏蔽 - #1718
Merged
Conversation
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}/(未入库)。
Collaborator
|
脚本体 lol |
CodFrm
added a commit
that referenced
this pull request
Sep 7, 2026
* 🐛 restore ScriptCat registration health checks (#1724) * 🐛 修复 @run-at context-menu:设置覆写不生效、菜单注册不上、脚本体自己的菜单被屏蔽 (#1718) * 🐛 修复设置面板覆写运行时机在重新注册后失效 restoreJSCodeFromCompiledResource 用脚本自带 metadata 选择编译分支, 而设置面板改运行时机/early-start 只写 selfMetadata,导致全量重新注册 (扩展更新、切换启用脚本、改黑名单等)后覆写被丢弃:context-menu 脚本 恢复自动执行且不注册菜单项,early-start 退化为普通注入。 pushValueUpdate 判断 early-start 时同样只看自带 metadata,覆写而来的 early-start 脚本在 GM 值变更后不会重新编译,预注入代码里的值会过期。 close #1649 * 🐛 修复 GM API 权限校验忽略用户覆写的运行时机 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(未入库)。 * 🐛 context-menu 包装不再屏蔽脚本体自己的 GM_registerMenuCommand @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}/(未入库)。 * ✨ 统一 example/tests 测试结果与人工验证反馈 (#1717) * ✨ 统一 example/tests 测试结果与人工验证反馈 * 🔒 固定 sctest CDN 引用到框架提交 * 🎨 统一 userscript 诊断面板与测试描述 * 🔒 固定 sctest CDN 引用到最新框架提交 * Update sctest.js * 🔒 固定 sctest CDN 引用到最新框架提交 * ✨ 增强统一测试诊断与面板反馈 * 🔒 固定 sctest CDN 引用到最终框架提交 * ✨ 优化 sctest 诊断面板与 frame 反馈 * 🔒 固定 sctest CDN 依赖版本 * ♿ 优化 sctest 面板可访问性反馈 * 🔒 重新固定 sctest CDN 引用 * 🎨 优化 sctest 面板指标与布局 * 🔒 固定指标优化后的 sctest CDN 引用 * 🐛 固定 sctest 耗时显示宽度 * ✅ 加固 UI 异步断言与 E2E 保存结果观察 (#1727) * test: stabilize heavy network rules UI cases * test: isolate UI files and await observable state * ✅ 保留 UI 测试原有隔离配置 * ✅ harden async test observations * 🐛 align E2E save expectations with failure cases * 🔍 tighten test guard binding and toast observation * 📄 补充 Agent 自主操作边界、指令冲突裁决与人类可读写作规范 (#1728) 现有 agent 文档规定了改动的质量门槛,但缺三层:agent 在两次人工决策之间可以自行做什么、 指令冲突如何裁决、以及写给人看的东西该怎么写。静态审查的依据: 整套文档没有一处把 agent 写成决策者(`decide` 的主语全是分类表或原则),因而产出「建议生成器」; `material` 作为门槛术语被引用九次却从未定义,且在 pull-request.md 内有两种含义; 测试失败例外在 AGENTS.md 概括成两条而 owner 定义了六种;路由表是一次性分类; 人工指令能覆盖什么没有成文;以及全套文档没有任何一条关于行文的规范, 而 pull-request.md 提供的九级标题骨架会被当成表格来填。 本次补齐:范围内自行决策、交还决定须指名归属与阻塞点、不写可查证却不查的保留意见、 指令冲突裁决与人工指令覆盖边界、连续路由、`material` 定义、测试失败例外改交 owner 裁决、 自主操作边界、不稳定结果报告口径、面向人类读者的写作原则、文档集自身的指令预算。 PR 模板补一条不渲染注释;pull-request.md 明确其结构是待考虑项而非待填表格。 Co-authored-by: Claude Opus 5 <noreply@anthropic.com> * 🐛 批量更新页打开更新详情复用检查缓存并挡住重复点击 (#1719) * 🐛 批量更新页打开更新详情复用检查缓存并挡住重复点击 点击脚本名查看更新时,openUpdatePageByUUID 会重新 fetch 一次脚本代码, 而这份新版代码在检查更新阶段已经存进 scriptUpdateCheck 的记录缓存里 (行内「更新」按钮装的就是它)。用户因此要为每次点击白等一次网络往返, 期间页面又没有任何反馈,连点几下就会开出多个安装页。 - SW: 拆出 prepareUpdateOrInstallPage,openUpdatePage 命中缓存代码时 跳过 fetchScriptBody;openUpdatePageByUUID 改为回报 boolean - 页面: 打开期间行内转圈并同步挡住重复点击,失败弹 toast, 点击脚本名同样取消自动关闭倒计时 * 🐛 更新页与安装页补齐骨架屏与异步中间态,失败不再被渲染成成功 (#1721) * 🐛 打开更新详情区分静默更新,忽略动作补齐逐条回执 页面此前无从判断服务端到底做了什么:openUpdatePageByUUID 在命中静默更新时 不开安装页却同样返回 true,用户点完脚本名只看到转一圈、什么都没发生; IGNORE 分支根本没有返回值,页面只能 fire-and-forget。 - openUpdatePageByUUID / openUpdatePage 返回 "opened" | "silent" | "failed" - IGNORE 逐条回报结果。忽略写的是脚本自身的 ignoreVersion,与检查缓存无关, 因此缓存随 Service Worker 回收后忽略照样生效,这里如实回报而不是谎报失效 - checkScriptUpdate 的结果收敛成 TCheckScriptUpdateResult 并用 reason 区分 「已有检查在跑」与真正的失败,页面才能分别提示 * 🐛 安装页补齐加载分档、代码骨架与提交忙态 从批量更新页点脚本名进来的必然是「更新」,加载屏却把上下文 chip 写死成 「脚本安装」,几百毫秒后再闪成「脚本更新」;描述写着「正在从来源下载」, 但这条入口的代码 Service Worker 早已备好,根本不下载。 - 状态屏按来路分档,未确知场景不渲染 chip(不猜),并补一条与就绪态操作栏 等高的底部占位,避免就绪瞬间内容区高度再跳一次 - 暂存代码被定时清理回收时落到专属终态,出口换成「重新检查更新」—— 原来的「重试」在这个最常见的失败原因下重试多少次都是同一结果 - Monaco 实例就绪前渲染代码骨架,替代此前 340px 的纯空白 - toggleWatch / rejectExternalAccess 补忙态,install 加重入守卫: 这两个动作全程不置忙态,连点会发出两次安装/两次决定 * 🐛 批量更新页补齐取数失败、检查空窗期与忽略/批量的中间态 取数失败时记录仍是空的,页面直接走到空态,把一次加载失败渲染成 「所有脚本均为最新(已检查 0 个脚本)」这条与事实相反的成功终态; 点「检查更新」到服务端广播回来之间页面完全静止,期间可以连点。 - 取数失败落错误终态:等宽 detail 框 + 重试 / 脚本列表出口 - 主动检查由本地 pending 立刻接管忙态,并把服务端的「正忙」「结果够新已跳过」 「通道异常」三条回执分别说出来;跳过时就地清掉待反馈标记, 否则会在下一次后台检查完成时冒出一条用户没点过的 toast - 忽略复用与更新相同的行级阶段(working → success → 退场),不再 fire-and-forget - 批量进行中互斥(行内勾选、两个批量按钮、全部恢复),避免两条进度互相覆盖; 被「结果失效」中断时保留已完成条数,不把汇总抹掉 - 骨架补齐工具条(桌面)与顶部选择栏/底部操作栏(移动)占位,消除数据到达时的 布局跳动,并加 role="status" / aria-busy;空态下重新检查不再整页闪回骨架 - 脚本名改用 aria-disabled + onClick 早退:disabled 会让浏览器不派发指针事件, 正好在名字被截断、最需要看全名时把 tooltip 一起关掉,键盘触发后焦点还会掉到 body --------- Co-authored-by: wangyizhi <yz@ggnb.top> Co-authored-by: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Checklist / 检查清单
背景
脚本设置面板的「运行时机」下拉可以把一个原本自动执行的脚本改成
context-menu(手动/菜单触发),但 #1649 反馈实测无效:菜单里不出现执行项,脚本仍然照常自动执行。
设置面板改运行时机只写
script.selfMetadata,脚本自带script.metadata不变,生效值要靠getCombinedMeta合并出来。排查后确认有两处代码直接读了未合并的script.metadata,因此脚本自己在 metadata 里写
@run-at context-menu是好的,只有「UI 覆写」这条路会坏(这也是用 demo 脚本复现不出来的原因)。
本次改动
1.
RuntimeService.restoreJSCodeFromCompiledResource(runtime.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.parseRequest(gm_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.
compileScriptCodeByResource(content/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.
getCombinedMeta(service_worker/utils.ts) 第二参数放宽为SCMetadata | undefined。函数体本来就有
if (!metaCustom) return metaRet守卫,此前所有调用点都在外面先判一次,该分支实际是死代码。实现考虑
「该用生效 metadata 的地方用了原始 metadata」,在源头合并可以覆盖同一对象的其它消费者
(如
getConnectMatched(request.script.metadata.connect, …))。parseRequest里合并时新建对象({...script, metadata: …}),不就地改写 DAO 返回的对象。selfMetadata的只有 match/include/exclude/run-in/run-at/early-start/tag,其中只有
run-at影响 GM 权限校验,所以第 2 点在当前 UI 下等价于只修 context-menu 豁免,但按生效 metadata 走的语义与 runtime 其余部分一致。
已知限制
installScript)才会重编译成正确代码。没有加迁移/一次性修复逻辑 —— 扩展更新本身就会触发全量重新注册。
chrome.contextMenus.onClicked共用的serviceWorker/popup/menuClick消息,未覆盖chrome.contextMenus条目创建与浏览器原生点击派发本身。显示层按
groupKey去重,不会出现重复菜单项;但同名项的回调会被触发多次(实测:脚本体跑过 2 次后,点它注册的菜单项回调执行 2 次)。这是「脚本体按次执行」的固有结果,本 PR 未额外去重。
chrome.contextMenus是全局的、按活动标签页渲染)不在本 PR 范围内,未处理。
建议审查重点
restoreJSCodeFromCompiledResource的三个分支现在都基于合并后的 metadata,其中前两个分支内部又调用
buildScriptRunResource(本身会再合并一次),确认没有语义漂移。parseRequest合并的位置:在订阅connect覆盖之前,保证订阅声明的 connect 仍然覆盖脚本自身的。GM_registerMenuCommand=undefined后是否有其它依赖该屏蔽的行为(我没找到,但这是行为契约变更)。关联
close #1649
验证
Base/head:
61164f6920e736fd3a03b269fb907e7aa7975432...1e811242c6ecb67225a008ea43fb5b2921856203,git diff --stat共 7 个文件(4 个生产文件 + 3 个测试文件),无其它清理。单元测试(先 RED 后 GREEN)
新增 4 条回归测试。先
git stash掉生产改动确认全部失败,恢复后全部通过: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 的两个症状:修复后同一套操作:
第三处(包装屏蔽菜单注册)同样是浏览器里先复现的:装一个脚本体里注册
Own Item A/Own Item B的脚本,改成 context-menu 后改完之后:
同样的复现在「脚本自己声明
@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。