Skip to content

Conversation

@cyfung1031
Copy link
Collaborator

详细见 #790 . 这PR只用来给Copilot Review

背景 / 动机

现有 GM_registerMenuCommand 的行为与 Tampermonkey(TM)存在差异,导致:

  • menu 内容动态更新不即时(需关闭再打开),与 TM 不一致。
  • 右键 context menu 某些动态项目(如 interval-n-xxxx)不显示。
  • 顶层 window 与 iframe 并单/排序不一致。
  • 相同 id 的注册覆盖规则与期望不一致。

本 PR 针对上述问题进行全面修订,使行为与 TM 对齐,并关闭 #787


变更重点

  1. 稳定化浏览器 contextMenu 的项目 ID

    • 为避免动态新增/删除时出现鬼影或缺失,改为使用具有稳定性的 item id 策略,解决右键菜单缺项问题(interval-n-xxxx)。
  2. 即时与可预期的动态更新

    • 当脚本以秒级变更 menu(intervalChanging = true)时,popup 与 context menu 会即时反映,无需关闭再开。行为与 TM 一致。
  3. 跨 frame(top 与 iframe)的一致性

    • 调整 ID 产生与归并逻辑,避免因不同 frame 的时间差导致项目分裂(例如 interval-m-1001interval-m-1002 在不同 frame 被错误合并/拆分),现在结果与 TM 一致。
  4. 相同 id 的覆盖规则

    • 当以相同 id 重复注册时,只保留最新一个(举例:「MenuReg abc-2」覆盖「MenuReg abc-1」),并确保 GM_unregisterMenuCommand(id) 能正确移除对应项目。
  5. accessKey 与返回值一致性

    • 对齐 TM 的行为:支援 accessKey 更新与同一 id 下的覆盖;注册与反注册的返回值/语意保持一致。

实作概要

  • 重构 GM_registerMenuCommandGM_unregisterMenuCommand 的内部资料结构与索引策略,确保 稳定 ID单一事实来源原子更新
  • 调整 popup 与 context menu 的同步机制,确保 即时刷新跨 frame 一致性
  • 覆盖规则:同 id 注册视为更新;解除注册支援以 id、或以注册返回值识别。

相容性 / 风险

  • 向后相容性:既有仅使用唯一 id 的脚本行为不变;多次注册同 id 的脚本现在会得到 TM 一致的覆盖效果。
  • 风险:若某些脚本依赖「同 id 多重并存」的非预期行为,升级后可能只看到最后一次注册的项目。建议在 release note 提醒。

测试方案(可直接复制以下脚本验证)

将以下 Userscript 于任意页面执行,并依照注解切换三个测试开关:

  • checkSubFrameIdSequence:在有 iframe 的页面设为 true,检查 top 与 iframe 的 ID/顺序。
  • intervalChanging:设为 true 检查「每秒动态变更 menu」是否即时更新
  • skipClickCheck:预设 false 会引导互动测试;设 true 可跳过引导。

此脚本已内嵌于 PR 讨论中(详见原 PR 内容)。

// ==UserScript==
// @name         GM_registerMenuCommand Example
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  Simple demo for GM_registerMenuCommand
// @match        *://*/*
// @grant        GM_registerMenuCommand
// @grant        GM_unregisterMenuCommand
// ==/UserScript==

(async function () {
  'use strict';

  const checkSubFrameIdSequence = false;
  const intervalChanging = false;
  const skipClickCheck = false;

  // ...(完整测试程式码同 PR 讨论中的范例)
})();

预期结果(关键观察点):

  1. 初始同 id 注册仅显示最新一个条目(abc → 只剩 MenuReg abc-2)。
  2. 之后改用不同 id 注册会同时显示两个条目,新增带 accessKey 的版本后,三者并存规则正确。
  3. 呼叫 GM_unregisterMenuCommand 逐一移除后,对应条目正确消失。
  4. intervalChanging = true 时,popup 与右键 context menu 即时刷新,不需重开。
  5. 在有 iframe 的页面,top 与 iframe 的项目数量与归并与 TM 一致(不会错误合并/拆分)。

效能 / 可靠性

  • 稳定 ID 与原子更新降低 menu 重建成本与竞态。
  • 以集中式状态管理避免不同视图(popup/context/frames)之间的同步漂移。

相关议题


检查清单

  • 行为对齐 Tampermonkey(动态更新 / 右键显示 / 同 id 覆盖 / 跨 frame)
  • 单元与手动测试覆盖上述案例
  • 文件与注解补充,降低维护成本
  • 风险与相容性说明

备注:PR 由 cyfung1031: GM_registerMenuCommand_codefix 发至 scriptscat: main,目前变更统计为 +894 / −334,13 个档案。可于「Files changed」分页审查具体改动。


原PR: #790 "[高优先] 重新修订 GM_registerMenuCommand 相关代码设计 by cyfung1031 · Pull Request #790 · scriptscat/scriptcat · GitHub"

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

本 PR 重构了 GM_registerMenuCommand 的设计以解决与 Tampermonkey 的行为差异问题,实现了稳定的菜单项 ID、即时同步更新、跨 iframe 一致性等功能。

  • 将菜单项 ID 从数字改为基于内容的稳定唯一键(groupKey + contentEnvKey)
  • 实现了即时菜单更新机制,无需关闭再打开 popup
  • 统一了主框架和子框架之间的菜单显示与触发逻辑

Reviewed Changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
src/pages/store/AppContext.tsx 增加类型定义和泛型支持,优化消息订阅机制
src/pages/popup/App.tsx 添加实时菜单更新订阅和排序逻辑,支持动态菜单变更
src/pages/options/routes/Setting.tsx 统一消息订阅的类型定义
src/pages/options/routes/ScriptList/index.tsx 统一消息订阅的类型定义
src/pages/components/ScriptMenuList/index.tsx 重构菜单项分组逻辑,实现 groupKey 合并显示
src/app/service/service_worker/types.ts 新增菜单相关类型定义,支持新的菜单项结构
src/app/service/service_worker/popup.ts 核心重构:实现稳定 contextMenu ID 和菜单同步机制
src/app/service/service_worker/gm_api.ts 更新 GM API 参数类型,适配新的菜单键结构
src/app/service/service_worker/client.ts 更新客户端接口,支持新的菜单参数结构
src/app/service/queue.ts 更新队列类型定义,支持新的菜单项键结构
src/app/service/content/gm_api.ts 重构客户端菜单注册逻辑,实现环境隔离和键管理
src/app/service/content/gm_api.test.ts 添加菜单注册和取消注册的测试用例
src/app/cache.ts 添加注释说明缓存返回值的边界情况
Comments suppressed due to low confidence (1)

src/app/service/service_worker/popup.ts:1

  • 使用非空断言操作符 ! 可能导致运行时错误。应该检查 data 是否存在以及 tabId 是否为有效键,例如 return data?.[sender.getExtMessageSender().tabId] || {}
import { type IMessageQueue } from "@Packages/message/message_queue";

];
return () => {
for (const unhook of unhooks) unhook();
unhooks.length = 0;
Copy link

Copilot AI Oct 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

清空数组的操作是多余的。由于 unhooks 是局部变量且在 useEffect 清理函数中只会执行一次,数组会在函数执行完毕后自动被垃圾回收,无需手动清空。

Suggested change
unhooks.length = 0;

Copilot uses AI. Check for mistakes.
Comment on lines +36 to +37
//@ts-ignore
b.enable - a.enable ||
Copy link

Copilot AI Oct 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

使用 @ts-ignore 忽略类型检查不是好的做法。应该通过类型断言或修复类型定义来解决类型错误,例如 Number(b.enable) - Number(a.enable)

Suggested change
//@ts-ignore
b.enable - a.enable ||
Number(b.enable) - Number(a.enable) ||

Copilot uses AI. Check for mistakes.
return () => {
isMounted = false;
for (const unhook of unhooks) unhook();
unhooks.length = 0;
Copy link

Copilot AI Oct 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

清空数组的操作是多余的。由于 unhooks 是局部变量且在 useEffect 清理函数中只会执行一次,数组会在函数执行完毕后自动被垃圾回收,无需手动清空。

Suggested change
unhooks.length = 0;

Copilot uses AI. Check for mistakes.
];
return () => {
for (const unhook of unhooks) unhook();
unhooks.length = 0;
Copy link

Copilot AI Oct 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

清空数组的操作是多余的。由于 unhooks 是局部变量且在 useEffect 清理函数中只会执行一次,数组会在函数执行完毕后自动被垃圾回收,无需手动清空。

Suggested change
unhooks.length = 0;

Copilot uses AI. Check for mistakes.
];
return () => {
for (const unhook of unhooks) unhook();
unhooks.length = 0;
Copy link

Copilot AI Oct 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

清空数组的操作是多余的。由于 unhooks 是局部变量且在 useEffect 清理函数中只会执行一次,数组会在函数执行完毕后自动被垃圾回收,无需手动清空。

Suggested change
unhooks.length = 0;

Copilot uses AI. Check for mistakes.
return () => {
isMounted = false;
unsub();
checkItems.clear();
Copy link

Copilot AI Oct 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

在 useEffect 清理函数中调用 checkItems.clear() 是多余的。Map 实例会在组件卸载后自动被垃圾回收,无需手动清理。

Suggested change
checkItems.clear();

Copilot uses AI. Check for mistakes.
Comment on lines +226 to +227
// @ts-ignore
const exec = new ExecScript(script, "content", mockMessage, nilFn, envInfo);
Copy link

Copilot AI Oct 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

在测试代码中使用 @ts-ignore 并不理想。应该通过正确的类型定义或类型断言来解决类型问题,例如使用 as any 或创建合适的 mock 类型。

Copilot uses AI. Check for mistakes.
Comment on lines +267 to +268
// @ts-ignore
const exec = new ExecScript(script, "content", mockMessage, nilFn, envInfo);
Copy link

Copilot AI Oct 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

在测试代码中使用 @ts-ignore 并不理想。应该通过正确的类型定义或类型断言来解决类型问题,例如使用 as any 或创建合适的 mock 类型。

Suggested change
// @ts-ignore
const exec = new ExecScript(script, "content", mockMessage, nilFn, envInfo);
const exec = new ExecScript(script, "content", mockMessage as any, nilFn, envInfo);

Copilot uses AI. Check for mistakes.
@CodFrm
Copy link
Member

CodFrm commented Oct 9, 2025

似乎也没有太多有营养的内容,和之前差不多

@cyfung1031
Copy link
Collaborator Author

Copilot Review完

@cyfung1031 cyfung1031 closed this Oct 9, 2025
@cyfung1031 cyfung1031 deleted the PR-copilot-check-PR790 branch October 17, 2025 13:45
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.

2 participants