Skip to content

🐛 修复同一 document 内重复注册的同组菜单被重复触发 - #1695

Closed
CodFrm wants to merge 1 commit into
mainfrom
fix/popup-menu-click-dedupe
Closed

🐛 修复同一 document 内重复注册的同组菜单被重复触发#1695
CodFrm wants to merge 1 commit into
mainfrom
fix/popup-menu-click-dedupe

Conversation

@CodFrm

@CodFrm CodFrm commented Aug 28, 2026

Copy link
Copy Markdown
Member

Checklist / 检查清单

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

N/A — Code reviewed by human:尚未有人工复核。

背景

PopupService.menuClick() 收到 Popup 传来的整组菜单后,逐条向对应的 key 派发 menuClick 事件:

await Promise.allSettled(menus.map((menu) => this.runtime.emitEventToTab({ tabId, frameId, documentId }, { eventId: menu.key, ... })));

Popup 点击时传的是整组:script.menus.filter((m) => m.groupKey === menuItem.groupKey && !m.options?.inputType)
src/pages/popup/App.tsx)。

groupKeyname + options(含 mIndividualKey)生成,用意写在 updateMenuCommand() 的注释里:
mainframe 与 subframe 注册的同一个菜单在 UI 只显示一条,但点击时两边都要执行。

问题在于「同一个 document 内的重复注册」也会落进同一个 groupKey:脚本切换状态时重新
GM_registerMenuCommand 而没有 unregisterMenuCommand,或者每次 SPA 换页都注册一遍,
都会在 tabScript:<tabId> 里留下多条 groupKey 相同、key 不同、documentId 相同的记录。
点击时这些残留 listener 会在同一个 document 里被一起触发——开关型菜单(例如「翻译网页 / 显示原文」)
等于连按两次,用户看到的是「点了没反应」。

本次改动

menuClick() 派发前按 tabId + documentId(无 documentId 时退回 frameId)+ groupKey 收敛,
每个 key 只保留最后一条(最新注册的 listener):

const targets = new Map<string, ScriptMenuItem>();
for (const menu of menus) {
  const documentKey = menu.documentId || `frame:${menu.frameId || 0}`;
  targets.set(`${menu.tabId}:${documentKey}:${menu.groupKey}`, menu);
}

跨 document / 跨 frame 的同组菜单仍然各自触发,updateMenuCommand() 注释里
「subframe 和 mainframe 都会执行」的语义不变。

实现考虑

  • 收敛键为什么带 documentIdScriptMenuItem 同时带 frameIddocumentId,后者能区分
    同一 frameId 上的前后两个文档。没有 documentId 的记录退回 frame:<frameId>,避免把不同 frame
    的菜单误并成一条。
  • 为什么保留最后一条updateMenuCommand() 按到达顺序 push,最后一条即最新注册的 listener;
    同一 document 内重复注册时,先前那条通常是脚本已经放弃的旧闭包。
  • individual 菜单不受影响options.mIndividualKey 参与 groupKey 生成,显式声明不合并的
    菜单项本来就拿到不同的 groupKey

已知限制

  • 如果某个脚本有意在同一个 document 里注册两条 name/options 完全相同的菜单,并期待点击时
    两个回调都执行,改动后只会触发最新的那条。这种用法无法与「残留的旧 listener」区分;
    需要各自触发时应使用 individual 选项让它们拿到不同的 groupKey
  • 只改了派发侧。重复注册的记录本身仍留在 tabScript:<tabId> 里(UI 层早就按 groupKey 去重显示,
    所以不影响观感);是否在注册侧一并清理属于另一件事,本次不扩大范围。

建议审查重点

  • 收敛键的选择:tabId + documentId|frameId + groupKey 是否恰好切在「同 document 重复注册」
    与「跨 frame 同名菜单」之间。
  • 「保留最后一条」是否符合 updateMenuCommand() 里 REGISTER/UNREGISTER 的实际顺序语义。
  • 上面「已知限制」第一条是否可接受。

参考

  • src/app/service/service_worker/popup.tsmenuClick() / updateMenuCommand()(groupKey 生成与合并语义)
  • src/pages/popup/App.tsx:点击时按 groupKey 取整组菜单

验证

npx vitest run src/app/service/service_worker/popup.test.ts   → 51 passed (51)
npx tsc --noEmit                                              → 0 error
npx eslint src/app/service/service_worker/popup.{ts,test.ts}  → 0 problem

新增单元测试 PopupService 菜单点击分发 > 同一 document 重复注册的同组菜单只触发最新监听,跨 document 仍分别触发
三条同 groupKey 记录(两条 main-document、一条 frame-document)→ 期望 emitEventToTab
只被调用 2 次,分别命中 main-2(最新)与 frame-1

未做浏览器复现:改动依据是上述代码路径与单元测试,没有在真实浏览器里重放
「脚本重复注册同名菜单 → 点击无反应」的场景。若需要该级别证据请提出,我再补一份会话验证。

menuClick 收到 Popup 传来的整组菜单(`script.menus.filter(m => m.groupKey === ...)`)
后逐条 emitEventToTab。groupKey 按「name + options」生成,用意是让 mainframe 与
subframe 注册的同一个菜单在 UI 只显示一条、点击时各 frame 都执行。

但同一个 document 内重复注册同名菜单(脚本切换状态时重新 register 而没有
unregister,或每次 SPA 换页都注册一遍)也会落进同一个 groupKey,且各自持有
不同的 key。点击时这些残留 listener 会在同一个 document 里被一起触发,
开关型菜单等于连按两次,表现为点了没反应。

按 tabId + documentId(或 frameId) + groupKey 收敛,只保留最后注册的那条,
跨 document 仍分别触发,保持原本「各 frame 都执行」的语义。
@CodFrm CodFrm closed this Aug 28, 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.

1 participant