Skip to content

🐛 页面不属于任何标签页时不再记入后台脚本列表 - #1783

Open
CodFrm wants to merge 3 commits into
mainfrom
fix/1774-tabless-page-load
Open

CodFrm wants to merge 3 commits into
mainfrom
fix/1774-tabless-page-load

Conversation

@CodFrm

@CodFrm CodFrm commented Sep 29, 2026

Copy link
Copy Markdown
Member

Checklist / 检查清单

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

N/A — 第一项:修复针对的是维护者判定的成因,#1774 报告者的实际场景尚未实机复现,所以没有用关闭关键词,是否关闭 issue 由维护者决定。

背景

#1774:一个 @include * 的普通脚本出现在弹窗「开启和运行的后台脚本」里(Android / Quetta,v1.4.0)。

弹窗的后台列表直接读会话缓存 tabScript:-1,而 RuntimeService.pageLoad 用 chromeSender.tab?.id || -1 取标签页 id。页面不属于任何标签页时(sender.tab 缺失,或 tab.id 为 TAB_ID_NONE),id 就落成 -1,和后台脚本的命名空间撞在一起,于是这次页面上运行的普通脚本被记成后台脚本。如果是顶层 frame(frameId 为 0),addScriptRunNumber 还会先清空整个列表,把真正的后台脚本挤掉。另外,普通脚本一旦混进来就移除不掉,因为启用/禁用事件会跳过普通脚本,只能等浏览器重启清空会话缓存。

本次改动

pageLoad 只在页面属于真实标签页(tab.id > 0)时才上报 popupPageLoadUpdate。拿不到所属标签页时,脚本照常下发执行,只是不记弹窗的运行计数。

> 0 沿用了 PopupService 现有的约定:markTabInjected、角标计数、updateScriptMenu 都把 tabId <= 0 视为非真实标签页。getScriptsForTab 本身不使用 tabId,所以脚本在这类页面里的运行不受影响。

已知限制

  • 真实浏览器里的触发场景没有复现出来。在 Quetta 2.0.3(Android 14 模拟器,通过 CDP 驱动)上测了普通浏览、安装页安装、预览页、Custom Tab、冷启动恢复标签页、后台新标签页、标签组、新窗口;在桌面 Chrome 上测了别的扩展的 offscreen 文档里嵌网页。这些场景都能拿到正确的标签页 id,或者页面脚本根本不会注入。所以本 PR 证明的是代码层面的缺陷已经堵上,不是报告者的具体场景已经修好。
  • 截图里脚本名发灰(runNum === 0),这不是页面加载路径会产生的状态。它只可能来自两种情况:数据库里的 type 异常,或者用户在后台列表里点过「运行」。如果更新后仍能复现,需要沿这两条线另查。
  • 已经混进后台列表的条目不需要迁移:chrome.storage.session 在扩展更新或浏览器重启时会被清空(Chrome 文档行为,未在本 PR 中实测)。

验证

  • 新增用例 runtime.test.ts › 「pageLoad 页面运行计数只记到真实标签页」:没有 tab、tab.id 为 -1 这两种情况下,仍返回脚本、不产生弹窗计数记录;真实标签页照常记录。前两条在撤掉修复后失败(2 failed),加上修复后通过。
  • pnpm run typecheck 通过;改动的两个文件 eslint / prettier --check 通过;提交时 pre-commit 钩子通过。
  • pnpm exec vitest run --no-coverage src/app/service/service_worker/(rebase 到 80854540 之后):648 个用例中有 1~2 个失败,出现在 trash_freeze.test.ts / trash_event_partition.test.ts / script.test.ts,每次失败的用例不同。拿 origin/main 版本的两个改动文件跑同样的命令,也会失败(script.test.ts + trash_freeze.test.ts + trash_event_partition.test.ts + runtime.test.ts 连跑 3 次,每次 2/149 失败,用例轮换)。所以这是基线上已有的不稳定,与本改动无关;这几个文件单独跑时都能通过。
  • 最终 diff(origin/main 80854540...b4d7b3cb)只有 runtime.ts 和 runtime.test.ts 两个文件。

关联

Refs #1774

CodFrm and others added 2 commits September 29, 2026 16:09
pageLoad 已在生产者一侧过滤无 tab 的页面加载,这里在真正写 tabScript:<tabId> 的
sink 再挡一层:tabId <= 0 直接返回。旧实现在 frameId 为 0 时会先清空列表,
一旦有别的发送路径带来 -1,后台脚本会被整批挤掉。

新增用例:
- tabId 为 -1 / 0 时 addScriptRunNumber 不改动 tabScript:-1(撤掉守卫后失败)。
- 契约测试:无标签页的页面加载/iframe、菜单注册注销、普通脚本启停与运行状态事件之后,
  tabScript:-1 仍只含后台脚本。

Refs #1774

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@cyfung1031

Copy link
Copy Markdown
Collaborator

在 #1783 上追加了 e773c1b4:在写缓存的 sink 端补第二道防线,并把 tabScript:-1 的所有 writer 审了一遍。结论:#1783 封住的是「页面执行计数污染后台命名空间」这一类 bug,registerMenuCommand 不是第二条污染路径;但 #1774 里「原脚本出现、手动复制的 test 不出现」仍没有解释,所以继续用 Refs #1774,不建议改成 Fixes。

本次提交

  • PopupService.addScriptRunNumber(popup.ts):tabId <= 0 直接返回。生产者(RuntimeService.pageLoad)已过滤,这里是 sink 侧的第二道防线,以后有人新增别的 popupPageLoadUpdate 发送路径也不会再写进 tabScript:-1。旧实现在 frameId 为 0 时会先清空列表,所以受害的不只是「多一个普通脚本」,还有被整批挤掉的后台脚本。
  • popup.test.ts 新增 3 个用例:
    • tabId 为 -1 / 0:addScriptRunNumber(frameId 为 0 / 缺省 / 非 0)之后,tabScript:-1 里预置的后台脚本原样保留(内容与 runNum 均不变);tabId 为 0 时也不会新建 tabScript:0。
    • 命名空间契约:先经 installScript 建好后台脚本 A,再依次触发无标签页的主 frame / iframe 页面加载、普通脚本的 register / unregister 菜单、enableScripts 启停、scriptRunStatus 运行/完成,每一步后 tabScript:-1 都仍然只有 A。
    • 撤掉守卫后这 3 个用例全部失败(3 failed),加回后通过。正常 tab(tabId: 1)的既有用例未改,仍通过。

tabScript:-1 writer 审计

从 CACHE_KEY_TAB_SCRIPT 反查,写这个 key 的只有 popup.ts:

Writer tabId 来源 能否新建条目 能否写 -1
addScriptRunNumber popupPageLoadUpdate 事件 能(runNum = 1) 现在不能(本次守卫)
installScript / enableScripts 订阅 写死 -1 能,但先按 DB type 跳过 SCRIPT_TYPE_NORMAL 只写 -1
scriptRunStatus 订阅 写死 -1 不能,只改已存在条目的 runStatus / runNum(1 或 0),没有 type 检查 只写 -1
updateRegisterMenuCommand 调用方,sender.tab?.id || -1(gm_api.ts:1136) 不能:updateMenuCommand 对不在缓存里的 uuid 直接 continue 只能给已有条目挂菜单
脚本删除清理 / clearData(tab 关闭) 扫描全部 key / 真实 tabId 不能,只删 只删

对计划里几个假设的结论(均为静态分析,没有浏览器实测)

  • registerMenuCommand 路径:排除。 它无法创建条目,只能给已存在的条目加菜单,因此不会产生 runNum === 0 的普通脚本条目。另外,issue 里的 GreasyFork 脚本(462130)头部只 @grant unsafeWindow / GM_getValue / GM_setValue,根本没有 GM_registerMenuCommand。
  • runNum === 0 的来源: scriptToMenu 给普通脚本的初始 runNum 是 0,但 addScriptRunNumber 新增条目时会改成 1,所以刚被污染的条目是 1,不是 0。之后能变成 0 的唯一写入者是 scriptRunStatus(非 RUNNING 一律置 0,popup.ts:729;弹窗前端 usePopupData.ts:185 也有同样的置 0)。
  • 背景列表点「运行」: RuntimeService.runScript(runtime.ts:1724)没有 type 检查,弹窗只对列表里已有的条目提供「运行」;沙盒执行会先后发 RUNNING / COMPLETE。所以它只能操作已经被污染的条目,不能创建条目;点过一次之后条目会停在 runNum = 0,也就是截图里的灰色。这和 PR 描述的「先污染、再手动运行」一致,但没有实测,也不能解释「test 副本为什么不出现」。
  • DB type 异常:不太可能经安装/更新产生。 type 只由 metadata 决定(@crontab → CRONTAB,@background → BACKGROUND,否则 NORMAL,src/pkg/utils/script.ts:164-174),更新时类型变化会被 error_script_type_mismatch 拒绝(同文件 :290)。这个 GreasyFork 脚本头部没有这两个标签,新装必然是 NORMAL。导入 / 同步路径没有单独审。
  • 「没有真实 tab 的 context 是否该执行 userscript」: 静态看,getScriptsForTab 不使用 tabId,行为没有被这个 PR 改变;要不要在 getScriptsForTab 之前就拒绝这类 context,需要真实 sender 证据,所以没动。

没做的

  • 需要真机 / reporter 配合的项(安装来源四组对照、Quetta + Android 15 重现、diagnostic 导出)没有做。
  • 临时 debug 日志与开发环境断言(NORMAL → tabScript:-1 warning)没有提交:计划里本来就是临时手段,放进主干会留下常驻噪音。如果 reporter 能装 debug build,可以另出一个只含这个 warning 的构建。
  • isRealTabId() 的集中化留给单独的 cleanup PR。

验证

  • pnpm run typecheck、改动的两个文件 eslint / prettier --check 通过。
  • popup.test.ts + runtime.test.ts:114 个用例全过。
  • pnpm exec vitest run --no-coverage src/app/service/service_worker/:649 过,2 失败,都在 trash_freeze.test.ts,与 PR 描述里记录的基线不稳定一致;该文件单独跑 2/2 通过。

合并 / 关闭判断

- 契约测试:session 缓存读取不拷贝,expected 直接持有缓存引用时,被测路径原地修改
  (如 menu.push、runNum 赋值)会连 expected 一起改掉,toEqual 恒真。改用
  structuredClone 快照;在 scriptRunStatus 订阅里临时注入原地改 runNum,旧断言通过、
  新断言失败,已验证后还原。
- addScriptRunNumber 守卫由 tabId <= 0 改为 !(tabId > 0),undefined / NaN 这类越过类型
  的运行时数据同样拒绝,并补对应用例(守卫收紧前失败)。

Refs #1774

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@cyfung1031

Copy link
Copy Markdown
Collaborator

#1774 报告者的实际场景尚未实机复现

@CodFrm 这PR好像有点站不住脚...

@CodFrm

CodFrm commented Sep 30, 2026

Copy link
Copy Markdown
Member Author

#1774 报告者的实际场景尚未实机复现

@CodFrm 这PR好像有点站不住脚...

我之前复现过一次,但是当时没在意,都不记得是什么浏览器了,反正是国产的低内核版本的浏览器,现在不知道是修了还是怎么,我猜应该就是tabid返回有问题导致的

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