Skip to content

Conversation

@cyfung1031
Copy link
Collaborator

@cyfung1031 cyfung1031 commented Dec 9, 2025

这次 PR 对 ScriptCat 的核心通讯机制进行了大幅重构,主要解决以下问题:

  • 原有依赖 tabs.sendMessage 逐个分发的 O(n) 通讯模式,在开启大量分页(例如 50~100+ 个)时会造成 service worker 严重负载,甚至阻塞
  • Firefox Manifest V3 下 runtime.onMessage 无法在 scripting 环境使用,导致通讯不相容
    - 旧有 MessageFlag 设计可被追踪/拦截的风险较高
    - inject / content 层级的类别抽象过度复杂,维护困难

主要改动与设计思路

  1. 通讯方式全面转为 chrome.storage.local 广播

    • service worker 透过 chrome.storage.local.set({ rId: xxx, ... }) 发送指令
    • 每个分页的 scripting 环境监听 chrome.storage.onChanged
    • scripting 再负责将指令转发给 content / inject(使用 postMessage / CustomEvent)
      → 实现近似原生 broadcast 的效果,service worker 不再需要逐一分页呼叫 sendMessage,大幅降低 SW 负载
  2. 适配 Firefox MV3 限制

    • 完全移除对 runtime.onMessage 的依赖(FF MV3 scripting 环境不支援接收)
    • 改用 storage.onChanged + 事件转发机制,确保 Chrome / Firefox 行为一致
  3. 采用不可追踪、每次执行不同的动态同步标志(MessageFlag & EventFlag)

    • 弃用 uuidv5,改用 uuidv4 + 随机 eventFlag 协商(透过 CustomEvent 双向确认)
    • 引入 injectFlagEvtexecutorEnvReadyKey 等一次性/动态 key
    • 结合 build-time 随机 SC_RANDOM_KEY + performance.timeOrigin 进行初始握手
    • 有效防止第三方扩充功能或页面脚本拦截/伪造通讯
  4. inject & content 代码大幅简化

    • 移除原本的类别包装(ImmutableEventTarget、MessageDelivery 等)
    • 改用直接、易读的函数式写法
    • 统一事件监听与发送逻辑,减少抽象层级,提高可维护性
  5. 早期注入(document-start)支援更稳定

    • 移除 early-start 专用的固定 messageFlag
    • 改用动态就绪检测(ReadyWrap + 事件计数),避免 flag 错位
  6. 其他细节优化

    • 移除大量未使用代码(getUspMessageFlag、MessageDelivery、旧版 flag 逻辑等)
    • 修正拼写(「接数」→「接收」)、补充注解
    • 加强 vitest 测试环境相容性
    • 增加 chrome.runtime.lastError 处理
    • GM API、脚本执行、权限校验等相关模组同步更新

效能与相容性

  • 效能:实测 20~30 个分页以上时,service worker 负载明显下降,通讯延迟可接受(storage I/O 比大量 sendMessage 更分散)
  • 相容性:Chrome 稳定,Firefox MV3 scripting 环境已验证可正常执行 GM API 与早期注入
  • 安全性:动态、一次性 flag 设计大幅提升防拦截能力

已知限制 / 未来方向

  • 目前仍保留部分非广播通讯(例如 emitEventToTab),透过 scripting 转发
  • storage.onChanged 会在每个分页产生监听器,极端情况下(数百分页)仍需观察
    - 长期考虑方向:探索 UserScripts API(dynamic)或 file.js?query 方式,进一步减少 content/inject 层

测试重点建议

  • 多分页(30+)开启相同脚本,观察 SW CPU/延迟
  • Firefox MV3:document-start 脚本是否正常执行 GM_* API
  • 早期载入页面(重载、导航)是否正确触发
  • 尝试第三方扩充功能是否能拦截/伪造通讯(应失败)

Aspect tabs.sendMessage to all tabs storage.local.set + onChanged Winner & why
Time complexity (your code) O(n) – you loop & call API n times O(1) – single set call storage
Real wall-clock cost (many tabs) O(n) × ~0.5–5 ms per tab (very bad at 100+ tabs) O(1) + very cheap listener fire (~0.1–1 ms) storage
Background script blocking Can freeze for seconds with 200–1000 tabs Almost never blocks storage
Works when tab is loading / idle Usually yes Yes (event fires when content script is alive) tie
Message arrives reliably Yes (if tab has content script listening) Yes tie
Payload size limit ~64 MiB (but slow) 8 MiB per item (local), usually plenty tie
Can differentiate who needs update Easy (send only to matching tabs) Everyone gets it → filter in content script messaging (sometimes)
Battery / CPU overhead Higher (many API calls) Lower storage
Official recommendation vibe OK for few tabs, problematic for many Designed exactly for this kind of pub-sub storage

@cyfung1031 cyfung1031 requested a review from CodFrm December 9, 2025 03:32
@cyfung1031 cyfung1031 force-pushed the develop/messaging-performance-boost2 branch from a08c0b8 to 2396bb5 Compare December 9, 2025 03:34
@cyfung1031 cyfung1031 changed the title 以类似broadcast机制重构通讯机制 [v1.3?] 以类似broadcast机制重构通讯机制 Dec 9, 2025
@CodFrm
Copy link
Member

CodFrm commented Dec 9, 2025

后台脚本没有测试

我测试了一下,没有发现问题

scripting在 scripting 分离环境直接透过 chrome.storage.onChange 取得 service worker 的broadcast 指示,再转发到 content & inject
这个机制减省了很多不必要的处理
(不用 tabs.query, 不用 tabs.sendMessage, 不用先传到 content 再传到 inject)
(不然的话,我浏览器有100个以上的tab, 每一次 valueUpdate 都要发 100次 sendMessage, 对 serviceWorker 本身处理影响很大)

这个消息投递的次数并没有消失,只是进行了转移,从我们的业务代码转移到了浏览器的内部机制,现在是真真正正的要投递触发100次 chrome.storage.local.onChanged.addListener 事件消息,同时有一次存储消耗,之前的逻辑反而可以根据实际的运行情况只推指定的tab,当然这是一个不错的思路,只是我觉得性能消耗要根据实际情况来看

不用先传到 content 再传到 inject

变成了先传到 scripting 再传到 inject,复杂度并没有消失,如果不是考虑firefox的话,我觉得不需要这一层

scripting.js 无法像 inject 跟 content 把messageFlag封装到代码
而是一个通用脚本直接查 chrome.storage 的 messageFlag 再跟 content 请求对话
因为它是用来被动接收 service worker 的东西,所以不需要一个同步的messageFlag
不会影响 inject 和 content 的同步执行

需要 查 chrome.storage 的 messageFlag 再跟 content 请求对话,early-start 的脚本,在极端情况下,可能丢失message

@CodFrm CodFrm added the P1 🔥 重要但是不紧急的内容 label Dec 9, 2025
@cyfung1031
Copy link
Collaborator Author

cyfung1031 commented Dec 9, 2025

这个消息投递的次数并没有消失,只是进行了转移,从我们的业务代码转移到了浏览器的内部机制

因为每一个 tab 有自己的 scripting
不是 serviceWorker 一个程序发出100个
而是每个 scripting 去处理
这是主要的分别吧

serviceWorker 是单线程。所有 scripts 的后台都是它在处理
尽量把工作能分散都分散处理吧

同时有一次存储消耗

这个还好吧。虽然更进一步是可以直接在 value 储存那边做。现在先这样。

之前的逻辑反而可以根据实际的运行情况只推指定的tab

valueUpdate 没有指定 tab
有指定tab的 emitEvent 还是保留原有的做法

当然这是一个不错的思路,只是我觉得性能消耗要根据实际情况来看

你拿 虚拟机 跑几百个脚本试试吧。应该能测试得出分别。

需要 查 chrome.storage 的 messageFlag 再跟 content 请求对话,early-start 的脚本,在极端情况下,可能丢失message

531ac10 这里有处理。
反正本身 valueChange 就不是同步
现在这个,不会丢失message,得到 messageFlag 后会补回事件
(messageFlag 改变的话,对话就会中断。需要重新载入。)


理想的话,应该是改成 UserScripts API Dynamic

UserScripts API Dynamic:
Uses the browser's UserScripts API to inject both the wrapper code and the userscript code.
The userscript is executed instantly -> document-start is supported.

这样的话后台API全部都经由 scripting 跟 service_worker 对话
不用多出一个 content.js 和 inject.js

但很可惜我不太懂如何改 rspack.config.ts 的设定,让它生成 wrapper 用的js, 然后跟脚本代码一同放在 userScripts API register 里

@CodFrm
Copy link
Member

CodFrm commented Dec 9, 2025

理想的话,应该是改成 UserScripts API Dynamic
这样的话后台API全部都经由 scripting 跟 service_worker 对话
不用多出一个 content.js 和 inject.js

不是很理解这个想法,为什么不用多出 content.js 和 inject.js,难道把 GM、沙盒之类的逻辑也放到 userscript 中?

另外注入的脚本可能是在页面环境中的(inject),也还是要由 content/scripting 转发消息给 service_worker

@cyfung1031
Copy link
Collaborator Author

cyfung1031 commented Dec 9, 2025

理想的话,应该是改成 UserScripts API Dynamic
这样的话后台API全部都经由 scripting 跟 service_worker 对话
不用多出一个 content.js 和 inject.js

不是很理解这个想法,为什么不用多出 content.js 和 inject.js,难道把 GM、沙盒之类的逻辑也放到 userscript 中?

另外注入的脚本可能是在页面环境中的(inject),也还是要由 content/scripting 转发消息给 service_worker

难道把 GM、沙盒之类的逻辑也放到 userscript 中?

对。
userscript 独自能跑
要用 GM API 时,scripting 没准备好的话,可以延迟发消息, scripting 准备好後再交由 scripting 发给 service_worker
(用 dispatchEvent + preventDefault 可以知道 scripting 是否有监听。没有的话 scripting 监听后再发出)
(虽然实际上 scripting 执行应总是优先于 userScripts )

这样就不用等 content.js 和 inject.js 的载入
直接自行做沙盒环境跑
( userscript API 所有都要使用 document-start 来建立沙盒。如果是 document-end 那些就等 body载入好跑 window['flag']

另外注入的脚本可能是在页面环境中的(inject),也还是要由 content/scripting 转发消息给 service_worker

对。转发消息用 dispatchEvent 根据 messageFlag 给 scripting
然后 scripting 再转发消息给 service_worker


脚本环境是 userScript API 的 main / content_script (没 chrome.storage)
转发消息的环境 是 scripting API 的 isolated ( 能跑 extension API. 包括 chrome.storage.local 存取)

@CodFrm
Copy link
Member

CodFrm commented Dec 9, 2025

那这样可能需要打包出一个 模板,service worker读取这个模板,然后将脚本代码放进去,说实话,不是很必要,这样有多少个脚本就要执行多少次初始化代码

TM 的 UserScripts API Dynamic 模式也是有 inject 和 content的

@CodFrm
Copy link
Member

CodFrm commented Dec 9, 2025

我进行了一次测试,storage的总耗时更长,可能因为有IO操作,实际的资源消耗在io操作上,但是接收端接受消息的时间更短


计时有误,storage的耗时都比 chrome.runtime.sendMessage 短,tab越多,差距越大,cpu用的performance.now,忽略掉这个,感觉也不好评估

image

@cyfung1031
Copy link
Collaborator Author

cyfung1031 commented Dec 9, 2025

我进行了一次测试,storage的总耗时更长,可能因为有IO操作,实际的资源消耗在io操作上,但是接收端接受消息的时间更短

计时有误,storage的耗时都比 chrome.runtime.sendMessage 短,tab越多,差距越大,cpu用的performance.now,忽略掉这个,感觉也不好评估

image

没代码 不太清楚实际测试方式

不过建议是,background 那边发一个消息,包含 timestamp
然后在接收那边计一下时间差,最后把时间差加起来做一个指标吧 ( $\sum_{i} (T_{r,i} - T_s)$ )
(尽量贴近后台 background 单线程 跟 多个前台执行环境的情况)
(tabs 那个如果有包含 tabs.query 就更好)

@CodFrm
Copy link
Member

CodFrm commented Dec 9, 2025

代码:归档.zip 里面的时间是消息到达时间

storage的耗时会短一些,但是cpu消耗这个不好评估

@CodFrm
Copy link
Member

CodFrm commented Dec 9, 2025

chrome.tab.query 耗时 1ms 左右,这个差距并不是很大 2x 的样子,但是实际情况会更复杂一些,valueUpdate并不算是广播,会根据实际情况去推送具体的tab,广播所有的tab都会去检查valueUpdate也是一个额外的消耗

我还是比较想保留原来的逻辑,没啥说服力,自己都说服不太了😅,chrome.storage 也可以接受吧

@cyfung1031
Copy link
Collaborator Author

cyfung1031 commented Dec 9, 2025

sc1067-test-code-v2.zip
sc1067-test-code-v3.zip

你拿这个跑跑看结果~
CPU时间那个,不用理会返回结果。 ScriptCat 也只是发出不理返回
预设跑100次。不然不够准
每次跑完后,后台部份要停一下 (20ms),不然一直堆,content_script 那边的处理追不上

看 SW 执行时间 和 latency 就好

例如我这边,23个 tabs ( 其他是未载入的)
结果是这样的
Screenshot 2025-12-10 at 1 45 18

@CodFrm
Copy link
Member

CodFrm commented Dec 10, 2025

image

不过我发现这种方式,页面中会有两个content,实际资源占用情况,我觉得还是不太好说

@cyfung1031
Copy link
Collaborator Author

不过我发现这种方式,页面中会有两个content,实际资源占用情况,我觉得还是不太好说

所以我才說, 把wrapper的部份直接塞到代碼腳本就好
反正沙盒環境也是獨立,互不相干,也是要獨立生成
而且沙盒環境跟inject/content本來就要一致

對話的部份是沒辨法,一定要有scripting /content script

但動態代碼都是userscripts api那邊搞
對話部份用靜態的就可以。直接scripting做


不過這些更進一步的改動可以之後處理
不用一步登天

@CodFrm
Copy link
Member

CodFrm commented Dec 10, 2025

所以我才說, 把wrapper的部份直接塞到代碼腳本就好 反正沙盒環境也是獨立,互不相干,也是要獨立生成 而且沙盒環境跟inject/content本來就要一致

對話的部份是沒辨法,一定要有scripting /content script

但動態代碼都是userscripts api那邊搞 對話部份用靜態的就可以。直接scripting做

不過這些更進一步的改動可以之後處理 不用一步登天

wrapper赛到脚本代码里面,那么每个脚本都要去处理沙盒和一些共用的东西,这样消耗的更多,始终还是要 inject 和 content 的,而且就算是塞进去,那也是另外一种概念上的 inject 和 content,你的脚本始终是要在这两个环境中运行的,不要考虑这个了

不考虑firefox的情况下,我感觉这个scripting还是意义不大,还是不太想引入scripting加大复杂度,光看性能测试,差距不算很大,而且还额外加入了一个页面的消耗,实际情况也不是会去给每个tab发的

或者只在firefox的环境下使用这种模式,chrome使用老的模式

@CodFrm
Copy link
Member

CodFrm commented Dec 15, 2025

我看到了你在mozilla的提问了,本来想等他们回复再决定加不加scripting的,但是没信了?

还是不太想加入scripting,有额外的消耗,且加大了代码和维护的复杂度,至于考虑Firefox的兼容问题,我觉得再等一段时间看看吧,Firefox的mv3缺少好多东西

@cyfung1031
Copy link
Collaborator Author

我看到了你在mozilla的提问了,本来想等他们回复再决定加不加scripting的,但是没信了?

还是不太想加入scripting,有额外的消耗,且加大了代码和维护的复杂度,至于考虑Firefox的兼容问题,我觉得再等一段时间看看吧,Firefox的mv3缺少好多东西

还好吧。用 scripting 解决了, Firefox MV3 版就大致完成了
如果要用 chrome.storage.onChanged.addListener, 还是要 scripting

这个PR的scripting 是在异步协助跟service_worker的沟通
不会加大了代码和维护的复杂度 (你喜欢的还可以搞个什么 scriptingServer scriptingClient )
有额外的消耗是没错。但为了不用在 service_worker 用 tabs.sendMessage 每次valueUpdate 来来回回跑,我觉得是值得
chrome.storage.onChanged.addListener 的好处是,即使资讯量大&频密,浏览器自行分配资源处理,这样就不会影响前台的操作
相反 tabs.sendMessage 的优先度高,在多分页多脚本的操作下, service_worker 不断发消息的话,开支更大,影响前台(UI)的操作。

userScriptAPI 的 content 环境肯定是有限制
那么GM的API呀,跟后台相关的部份,就让 scripting 处理吧。
scripting 弹性大,直接能操作 storage, 日后也许不需要什么都发到 service_worker, scripting 处理也可以

@CodFrm
Copy link
Member

CodFrm commented Dec 17, 2025

我看到了你在mozilla的提问了,本来想等他们回复再决定加不加scripting的,但是没信了?
还是不太想加入scripting,有额外的消耗,且加大了代码和维护的复杂度,至于考虑Firefox的兼容问题,我觉得再等一段时间看看吧,Firefox的mv3缺少好多东西

还好吧。用 scripting 解决了, Firefox MV3 版就大致完成了 如果要用 chrome.storage.onChanged.addListener, 还是要 scripting

这个PR的scripting 是在异步协助跟service_worker的沟通 不会加大了代码和维护的复杂度 (你喜欢的还可以搞个什么 scriptingServer scriptingClient ) 有额外的消耗是没错。但为了不用在 service_worker 用 tabs.sendMessage 每次valueUpdate 来来回回跑,我觉得是值得 chrome.storage.onChanged.addListener 的好处是,即使资讯量大&频密,浏览器自行分配资源处理,这样就不会影响前台的操作 相反 tabs.sendMessage 的优先度高,在多分页多脚本的操作下, service_worker 不断发消息的话,开支更大,影响前台(UI)的操作。

userScriptAPI 的 content 环境肯定是有限制 那么GM的API呀,跟后台相关的部份,就让 scripting 处理吧。 scripting 弹性大,直接能操作 storage, 日后也许不需要什么都发到 service_worker, scripting 处理也可以

勉强可以接受,但是我一直想等firefox的回复,如果支持了在 userScripts 中使用 onMessage,我会毫不犹豫的砍掉 scripting 来保证架构简单

@cyfung1031
Copy link
Collaborator Author

cyfung1031 commented Jan 22, 2026

现在的message flag生成逻辑感觉太弯弯绕绕了,还要进行协商,保持之前的逻辑感觉也没问题,或者以另外的逻辑生成这一块的flag(chrome.runtime.dynamicId+SC_RANDOM_KEY 似乎可以 Firefox没有),不过现在的逻辑是真的绕啊。。。。也没有必要不断的变动,意义不大,做了一堆没必要的动作,初始化时随机的已经够了

以往做法在SW那边搞一堆生成注册
现在不用
early-start 也不用管什么 flag

整个流程简化得多
SW完全不用搞什么 MessageFlag

这玩意是每次跑页面时生成
无论有没有early-start, 是 scripting 还是 inject 还是 content
都会因为 performance.timeOriginprocess.env.SC_RANDOM_KEY 一致而一样的

然后就是 scripting inject content 他们之间真正通讯的token
因为是随机,无法截取的
而且每次都不一样。一定是载入页面时才会得到一致的
脚本内容改了重新注册什么的都不会再次得到同样token


这设计也不需要特别照顾 early-start
这流程跟 pageLoad 没关系
也跟 storage 没关系
未取得token, 那些API通讯就先留著
取得token后,就执行API通讯

@CodFrm
Copy link
Member

CodFrm commented Jan 23, 2026

firefox 可以用 file.js?query 的方案,chrome暂未想到更好的,dynamicId每次重启浏览器都会变动

你终于看到这里了 对。没好办法。MV3的设计就是不给你这样做。只能跑档案。FF这个有可能属于bug. 所以我最后把 MessageFlag 的生成机制都改了

message flag在sw是另外一个生成逻辑,两边不一致导致 early-start 出问题了

是吗? 应该不用依赖sw生成message flag 就是了 我记得都删掉 你试试把脚本猫那个全域脚本开关,关一下开一下看看!?

你自己测试一下就知道了,因为flag没对上,脚本能运行起来,但不是最快的速度了,和 document-start 一样

image

我觉得还是先确定flag的生成逻辑吧,现在的做法算不上随机,完全可以被网站截取的,如果这能认可的话,那为什么不直接使用build的那个随机,反正都会被截取


整个流程简化得多

说实话,我没看出来哪里流程简化了,构建时需要增加环境变量,还需要进行一堆弯弯绕绕复杂的协商


我想还是Firefox和chrome做两套不同的处理,chrome用老的逻辑,Firefox使用 file.js?query 的方案,在scripting处理,虽然我也觉得这是FF的bug,有点担心未来变动。(我看了一下mv2也是可以的,应该也不用太担心,只是这个方法确实有点奇淫巧计)

另外还是舍弃storage.local的方案,毕竟这本来就是用来做存储的,还额外的引入了scripting这一层,valueUpdate也不完全是广播的,另外就是chrome的scripting和content/inject的flag不太好协商,直接舍弃掉


还有一种方法是和MV2一样 scripting 使用 <script> 标签注入inject,可以真随机通信flag,但是也不好协商 early-start 的flag

@CodFrm
Copy link
Member

CodFrm commented Jan 23, 2026

看起来是不会被截取的,因为协商过程会比网页的js运行还快,所以不会被网站截取到,忽略掉我说的这些相关的

说实话,我对chrome这边引入scripting还是很排斥。。。。。valueUpdate也不完全是广播的,每个页面+iframe也都要消耗一个scripting的资源

那现在就是另外一个问题了,early-start如何与页面进行协商,感觉可以直接用SC_RANDOM_KEY作为flag,运行比页面js快,也只需要生效一次,不用考虑截取问题

@cyfung1031
Copy link
Collaborator Author

cyfung1031 commented Jan 23, 2026

early-start如何与页面进行协商

之后我再看看...
应该跟协商无关
单纯是写错了什么吧

直接用SC_RANDOM_KEY作为flag

不進行 协商 的話
载入时的截取不了
但载入后动作的一堆 event 就会被 截取

@cyfung1031
Copy link
Collaborator Author

cyfung1031 commented Jan 24, 2026

那现在就是另外一个问题了,early-start如何与页面进行协商,感觉可以直接用SC_RANDOM_KEY作为flag,运行比页面js快,也只需要生效一次,不用考虑截取问题

是的。改了 d37dee2. 这跟

const MessageFlag = uuidv5(`${performance.timeOrigin}`, process.env.SC_RANDOM_KEY);

无关,也可以另外做一个KEY避免混淆。

@CodFrm
Copy link
Member

CodFrm commented Jan 25, 2026

那现在就是另外一个问题了,early-start如何与页面进行协商,感觉可以直接用SC_RANDOM_KEY作为flag,运行比页面js快,也只需要生效一次,不用考虑截取问题

是的。改了 d37dee2. 这跟

const MessageFlag = uuidv5(`${performance.timeOrigin}`, process.env.SC_RANDOM_KEY);

无关,也可以另外做一个KEY避免混淆。

基于这个思路,其实也不需要 performance.timeOrigin+SC_RANDOM_KEY 了,直接固定一个flag,协商后续的随机flag就行了,我重构一下这块

@cyfung1031
Copy link
Collaborator Author

那现在就是另外一个问题了,early-start如何与页面进行协商,感觉可以直接用SC_RANDOM_KEY作为flag,运行比页面js快,也只需要生效一次,不用考虑截取问题

是的。改了 d37dee2. 这跟

const MessageFlag = uuidv5(`${performance.timeOrigin}`, process.env.SC_RANDOM_KEY);

无关,也可以另外做一个KEY避免混淆。

基于这个思路,其实也不需要 performance.timeOrigin+SC_RANDOM_KEY 了,直接固定一个flag,协商后续的随机flag就行了,我重构一下这块

最初考考慮是避免頁面重載時呼叫之前的event
不過的確不太需要

cyfung1031 and others added 3 commits January 30, 2026 07:15
* 处理service worker没有MouseEvent的问题

* wip

* wip

* wip

* 重构消息机制

* 修复type问题

* 解决冲突

* 单元测试

* ScriptCat代碼 不使用 EventListenerObject

* 刪無關Debug代碼

* 刪未使用 getMessageFlag()

* 注釋修訂

* 统一写法降低维护成本

* ScriptCat代碼 不使用 EventListenerObject

* revised negotiateEventFlag

* Update common.ts

* 修复flag顺序导致的协商问题

* 调整单元测试参数位置

* vitest env fix

* Revert: 调整单元测试参数位置

* lint

* Revert: 调整单元测试参数位置

* 調整代碼

* 加入 readyDeferred 和 isReady

* fix

* fix unit test

* 调整单元测试

* 调整单元测试

* 单元测试

* 单元测试

* 统一大小写

* 刪無用代碼

* 抽取至共通 ReadyWrap

* ReadyWrap 釋放已使用 resolve, promise

* 删除debug日志和调整测试

* 调整日志等级

* 修复测试脚本GM log使用问题

---------

Co-authored-by: cyfung1031 <44498510+cyfung1031@users.noreply.github.com>
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

Copilot reviewed 38 out of 39 changed files in this pull request and generated 7 comments.

Comments suppressed due to low confidence (2)

src/app/service/content/scripting.ts:63

  • 在 storage.local.onChanged 监听器中,如果同时接收到两个 valueUpdate 通知(通过 extServer 和 storage.local),会导致同一个 valueUpdate 被广播两次到 content 和 inject。这可能导致脚本接收到重复的值更新通知。建议添加去重逻辑或明确说明这种行为是预期的。
    src/app/service/content/scripting.ts:93
  • CAT_fetchDocument 的实现中使用了 this.senderToInject 来发送 relatedTarget,但根据第 95 行的 isContent 参数,应该根据来源选择正确的消息桥。如果请求来自 content 脚本,应该使用 this.senderToContent。建议修改为:const msg = data.params[1] ? this.senderToContent : this.senderToInject; 然后使用 msg.sendRelatedTarget(xhr.response)。

@cyfung1031 cyfung1031 changed the title [v1.3] 重构通讯机制 - storage.local 广播、符合 FF MV3 的 scripting 设计、采用不可追踪不断变动的同步 MessageFlag [v1.3] 重构通讯机制:采用 storage.local 广播 + 符合 Firefox MV3 scripting 规范 + 不可追踪的动态同步 MessageFlag Jan 31, 2026
Comment on lines +28 to +31
return Promise.all([
sendMessage(this.senderToContent, "content/" + action, data),
sendMessage(this.senderToInject, "inject/" + action, data),
]).then(() => undefined);
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

如果是 scripting -> content/inject
都没有回传
是单向的
因此可以共用同一个 page event
这样就不用发两次

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

content.js

document.addEventListener("evt-for-broadcast", ()=>{...})

inject.js

document.addEventListener("evt-for-broadcast", ()=>{...})

scripting.js

document.dispatchEvent(new CustomEvent("evt-for-broadcast"))

Copy link
Member

Choose a reason for hiding this comment

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

但是这样的话,content/inject还需要多监听一个广播的事件,问题不大

// flag协商
export function negotiateEventFlag(messageFlag: string, readyCount: number, onInit: (eventFlag: string) => void): void {
const eventFlag = randomMessageFlag();
onInit(eventFlag);
Copy link
Collaborator Author

@cyfung1031 cyfung1031 Jan 31, 2026

Choose a reason for hiding this comment

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

未协商成功已经执行了 onInit
里面会跑 runtime.pageLoad();
里面会跑 client.pageLoad()
里面会跑 contentClient.do("pageLoad", { scripts: contentScriptList, envInfo });
injectClient.do("pageLoad", { scripts: injectScriptList, envInfo });

此时,如inject.js 或 content.js 未载入,即 getEventFlag 的 onReady 未执行,runtime.init(); 未执行,会导致

    this.server.on("runtime/emitEvent", (data: EmitEventRequest) => {
      // 转发给脚本
      this.scriptExecutor.emitEvent(data);
    });
    this.server.on("runtime/valueUpdate", (data: ValueUpdateDataEncoded) => {
      this.scriptExecutor.valueUpdate(data);
    });

    this.server.on("pageLoad", (data: { scripts: TScriptInfo[]; envInfo: GMInfoEnv }) => {
      // 监听事件
      this.startScripts(data.scripts, data.envInfo);
    });

未执行
因为 没有 server.on("pageLoad", ...)

会导致 .do("pageLoad") 报错


参考用

之前的版本(一个月时间)没此问题
https://github.com/scriptscat/scriptcat/blob/ff2538afee6e98dd90768b3c8d38c60f4e438cbf/src/scripting.ts
https://github.com/scriptscat/scriptcat/blob/ff2538afee6e98dd90768b3c8d38c60f4e438cbf/src/inject.ts
https://github.com/scriptscat/scriptcat/blob/ff2538afee6e98dd90768b3c8d38c60f4e438cbf/src/content.ts

Copy link
Member

@CodFrm CodFrm Jan 31, 2026

Choose a reason for hiding this comment

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

现在基于同步的理论和实际情况都是必定执行的,要做的话再增加一个onReady的协商即可,但是说实话不是很想因为这些几乎不可能的情况做没必要的处理,CustomEventMessage 处的 ReadyWrap 也是如此,必不可能存在没有监听的情况

你说的这些情况都只可能在浏览器内核实现发生重大变化时才会出现,如果考虑这些情况那么一切皆有可能

@CodFrm CodFrm merged commit 18c4d81 into release/v1.3 Jan 31, 2026
3 of 4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

P1 🔥 重要但是不紧急的内容

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants