Skip to content

Conversation

@cyfung1031
Copy link
Collaborator

@cyfung1031 cyfung1031 commented Dec 9, 2025

概述 Descriptions

已测试在Chrome版和Firefox版都能够正常执行
后台脚本没有测试。
(VSCodeConnect以外的功能,Firefox MV3 能完全执行。可以正常在Firefox跑)

不会被浏览器截取。inject 要发一个 id key, content 要配合这个 key 发另一个沟通key (同步处理)
沟通key = uuidv5(inject/scripting发出的requestId, content的messageFlagId)
双方要用同一个沟通key才能对话

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

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

同时,也解决了 Firefox MV3 中,runtime 没有 onMessage 的问题
(根据官方文档,这个看起来不像 Bug. userscriptAPI 的 runtime 就是只能发不能接收)

这个改动里, early-start 处理不变。
因为改的部份是 extServer 的接收处理。

非broadcast类的通讯,例如 emitEventToTab 那些有留著。
在 scripting 中接收然后转发。

变更内容 Changes

inject.js 和 content.js 也改动了,不再是一层套一层的 class 设计
代码不会重用。
直接写代码就好。方便理解和维护

为了方便使用 uuidv5, messageFlag 改成 uuidv4 格式

截图 Screenshots

@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 来保证架构简单

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 实施了一次重大的通讯机制重构,主要目的是支持 Firefox MV3 并优化性能。

Changes:

  • 引入新的 scripting.ts 作为通信中间层,使用 chrome.storage.local 广播机制替代 tabs.sendMessage
  • 采用动态 MessageFlag(基于 performance.timeOrigin 和编译时随机密钥生成),提高安全性和唯一性
  • 简化 inject.ts 和 content.ts,移除复杂的类层次结构,改用函数式设计

Reviewed changes

Copilot reviewed 32 out of 33 changed files in this pull request and generated 16 comments.

Show a summary per file
File Description
src/scripting.ts 新增 scripting 层,作为 service_worker 和 inject/content 之间的桥梁
src/inject.ts 完全重写,移除 InjectRuntime 类,改用函数式设计并直接处理握手
src/content.ts 完全重写,移除 ContentRuntime 类,简化为直接消息处理
src/message-delivery.ts 新增 MessageDelivery 类,用于缓冲和分发消息
packages/message/custom_event_message.ts 重构为使用 PageMessaging 对象,支持动态 token 绑定
packages/message/common.ts 新增公共工具函数,提取 pageDispatchEvent 等避免页面篡改
src/app/service/service_worker/value.ts 改用 chrome.storage.local 广播 valueUpdate,移除 tabs.query 循环
src/app/service/service_worker/runtime.ts 调整脚本注册逻辑,移除 Firefox 特殊处理,统一使用 scripting.js
rspack.config.ts 添加 SC_RANDOM_KEY 编译时常量,用于 MessageFlag 生成
其他文件 更新 uuid 导入路径,调整测试用例以适配新架构

function createScriptcatExpose(pageMsg: Message) {
const scriptExpose: App.ExternalScriptCat = {
isInstalled(name: string, namespace: string, callback: (res: App.IsInstalledResponse | undefined) => unknown) {
sendMessage<App.IsInstalledResponse>(pageMsg, "scripting/script/isInstalled", { name, namespace }).then(callback);
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

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

sendMessage 返回的 Promise 如果被 reject(例如通信失败),callback 将不会被调用,这可能导致调用方一直等待回调。建议添加 .catch() 处理,在发生错误时也调用 callback,传递 undefined 或错误信息。

Suggested change
sendMessage<App.IsInstalledResponse>(pageMsg, "scripting/script/isInstalled", { name, namespace }).then(callback);
sendMessage<App.IsInstalledResponse>(pageMsg, "scripting/script/isInstalled", { name, namespace })
.then(callback)
.catch(() => callback(undefined));

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

应该不用吧。这个改动跟PR没直接关系。有需要可另外处理

Comment on lines +81 to +83
chrome.storage.local.set({
valueUpdateDelivery: {
rId: `${Date.now()}.${Math.random()}`,
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

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

变量 rId 使用 Date.now()Math.random() 组合生成唯一标识,但这种方式在高并发场景下可能产生重复值(虽然概率极低)。建议改用 uuidv4() 以确保唯一性,与项目其他地方的ID生成方式保持一致。例如:rId: uuidv4()

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Date.now() 和 Math.random() 的组合就够了
uuidv4() 速度比较慢,不需要这么搞

Comment on lines +133 to +134
const node = parentNode || document.head || document.body || document.querySelector("*");
node.appendChild(el);
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

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

GM_addElement 中,如果 document.querySelector("*") 返回 null(例如在空文档中),则 node.appendChild(el) 将抛出异常。虽然这种情况极为罕见,但建议增加空值检查或至少添加 try-catch 处理,以避免整个 scripting 环境崩溃。

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

ScriptCat 作者假設總是有document.documentElement 嘛

Comment on lines 194 to 230
// readyFlag 位运算:inject=1,content=2,凑齐 3 表示都 ready. ready 后设为 4 避免再触发
let readyFlag = 0;

const finalizeWhenReady = () => {
if (readyFlag === 3) {
readyFlag = 4;

// 统一设置 token
scriptingMessaging.et = injectFlagEvt;
scriptExecutorMsgIT.et = `${injectFlagEvt}_${ScriptEnvTag.inject}`;
scriptExecutorMsgCT.et = `${injectFlagEvt}_${ScriptEnvTag.content}`;

// 绑定 receiver(允许 inject/content 发消息给 scripting)
scriptExecutorMsgTxIT.bindReceiver();
scriptExecutorMsgTxCT.bindReceiver();

// 建立 server:inject/content -> scripting 通道
const server = new Server("scripting", [scriptExecutorMsgTxIT, scriptExecutorMsgTxCT]);
prepareServer(server, senderToExt, scriptExecutorMsgTxIT, scriptExecutorMsgTxCT);

// 建立向页面投递消息的 delivery 通道
setupDeliveryChannel();
}
};

// 接收 inject/content 的 ready 回执
pageAddEventListener(`${injectFlagEvt}`, (ev) => {
if (!(ev instanceof CustomEvent)) return;

const key = `emitterKeyFor${injectFlagEvt}`;
let value = ev.detail?.[key];
if (!value) return;

if (value !== ScriptEnvType.content) value = ScriptEnvType.inject; // 使 value 必定为 1 或 2
readyFlag |= value;
finalizeWhenReady();
});
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

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

握手过程中,如果只有 inject 或只有 content 一方响应,readyFlag 将永远无法达到 3,导致 finalizeWhenReady 永远不会执行,scripting 通道无法建立。建议添加超时机制,在一定时间后即使未收到全部响应也继续初始化,或者根据页面类型(是否为 userScripts 环境)判断是否需要等待 content 响应。

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

如果只有一方响应,整个ScriptCat设计就跑不动啦
不需要加超时强制触发 readyFlag 3
如果跑不动,自然会有Bug Report

Comment on lines 80 to 111
// const { storageName } = sendData;
chrome.storage.local.set({
valueUpdateDelivery: {
rId: `${Date.now()}.${Math.random()}`,
sendData,
},
});
// chrome.tabs.query({}, (tabs) => {
// const lastError = chrome.runtime.lastError;
// if (lastError) {
// console.error("chrome.runtime.lastError in chrome.tabs.query:", lastError);
// // 没有 tabs 资讯,无法发推送到 tabs
// return;
// }
// // 推送到所有加载了本脚本的tab中
// for (const tab of tabs) {
// const tabId = tab.id!;
// this.popup!.getScriptMenu(tabId).then((scriptMenu) => {
// if (scriptMenu.find((item) => item.storageName === storageName)) {
// this.runtime!.sendMessageToTab(
// {
// tabId,
// },
// "valueUpdate",
// sendData
// );
// }
// });
// }
// });
// 推送到offscreen中
this.runtime!.sendMessageToTab(
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

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

使用 chrome.storage.local 广播 valueUpdate 时,每次更新都会触发所有 tabs 的 onChanged 监听器,即使某些 tabs 不包含该脚本。这可能导致性能问题,尤其是在有大量 tabs 时。虽然注释提到"注意资讯量不要过大",但建议考虑添加额外的过滤机制(例如基于 storageName 的白名单),或者在注释中明确说明这种设计权衡的原因。

Suggested change
// const { storageName } = sendData;
chrome.storage.local.set({
valueUpdateDelivery: {
rId: `${Date.now()}.${Math.random()}`,
sendData,
},
});
// chrome.tabs.query({}, (tabs) => {
// const lastError = chrome.runtime.lastError;
// if (lastError) {
// console.error("chrome.runtime.lastError in chrome.tabs.query:", lastError);
// // 没有 tabs 资讯,无法发推送到 tabs
// return;
// }
// // 推送到所有加载了本脚本的tab中
// for (const tab of tabs) {
// const tabId = tab.id!;
// this.popup!.getScriptMenu(tabId).then((scriptMenu) => {
// if (scriptMenu.find((item) => item.storageName === storageName)) {
// this.runtime!.sendMessageToTab(
// {
// tabId,
// },
// "valueUpdate",
// sendData
// );
// }
// });
// }
// });
// 推送到offscreen中
this.runtime!.sendMessageToTab(
// 为避免使用 chrome.storage.local 广播导致所有 tabs 的 storage.onChanged 被触发,
// 这里通过 chrome.tabs.query + runtime.sendMessageToTab 仅向实际加载了对应脚本的 tab 推送。
const { storageName } = sendData;
chrome.tabs.query({}, (tabs) => {
const lastError = chrome.runtime.lastError;
if (lastError) {
// 没有 tabs 资讯或查询失败,无法发推送到 tabs
this.logger.error("chrome.tabs.query error in pushValueToTab", lastError);
return;
}
// 推送到所有加载了本脚本的 tab 中
for (const tab of tabs) {
const tabId = tab.id;
if (tabId == null) {
continue;
}
this.popup
?.getScriptMenu(tabId)
.then((scriptMenu) => {
if (!scriptMenu) {
return;
}
if (scriptMenu.find((item) => item.storageName === storageName)) {
this.runtime?.sendMessageToTab(
{
tabId,
},
"valueUpdate",
sendData
);
}
})
.catch((err) => {
this.logger.error("getScriptMenu error in pushValueToTab", err);
});
}
});
// 推送到 offscreen 中
this.runtime?.sendMessageToTab(

Copilot uses AI. Check for mistakes.
Comment on lines +258 to +271
chrome.storage.local.onChanged.addListener((changes) => {
if (changes["localStorage:scriptInjectMessageFlag"]?.newValue) {
messageDeliveryToPage.dispatch({
tag: "localStorage:scriptInjectMessageFlag",
value: changes["localStorage:scriptInjectMessageFlag"]?.newValue,
});
}
if (changes["valueUpdateDelivery"]?.newValue) {
messageDeliveryToPage.dispatch({
tag: "valueUpdateDelivery",
value: changes["valueUpdateDelivery"]?.newValue,
});
}
});
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

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

chrome.storage.local.onChanged 监听器缺少错误处理。如果 messageDeliveryToPage.dispatch 抛出异常,将导致后续的 storage 变更无法被处理。建议添加 try-catch 包裹 dispatch 调用,或者在 MessageDelivery.dispatch 内部处理异常。

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

这个不需要考虑处理异常

chrome.userScripts?.unregister(),
chrome.scripting.unregisterContentScripts(),
this.localStorageDAO.save({ key: "scriptInjectMessageFlag", value: runtimeGlobal.messageFlag }),
chrome.storage.session.set({ unregisterUserscriptsFlag: `${Date.now()}.${Math.random()}` }),
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

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

同样的问题:使用 Date.now()Math.random() 组合生成 flag。建议改用 uuidv4() 以确保唯一性和一致性。

Suggested change
chrome.storage.session.set({ unregisterUserscriptsFlag: `${Date.now()}.${Math.random()}` }),
chrome.storage.session.set({ unregisterUserscriptsFlag: uuidv4() }),

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Date.now() 和 Math.random() 的组合就够了
uuidv4() 速度比较慢,不需要这么搞

@CodFrm
Copy link
Member

CodFrm commented Jan 19, 2026

想重构一下,每次来都没review完整

@cyfung1031
Copy link
Collaborator Author

想重构一下,每次来都没review完整

? 什麼意思?
你指copilot的review?

@CodFrm
Copy link
Member

CodFrm commented Jan 20, 2026

想重构一下,每次来都没review完整

? 什麼意思? 你指copilot的review?

太难阅读了,每次都花费很多时间,看到后面越来越没有耐心 😧

@cyfung1031
Copy link
Collaborator Author

想重构一下,每次来都没review完整

? 什麼意思? 你指copilot的review?

太难阅读了,每次都花费很多时间,看到后面越来越没有耐心 😧

新年放假才看吧
这个是重构,没什么能拆细。没办法

@CodFrm
Copy link
Member

CodFrm commented Jan 22, 2026

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

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


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

import type { TClientPageLoadInfo } from "./app/repo/scripts";

//@ts-ignore
const MessageFlag = uuidv5(`${performance.timeOrigin}`, process.env.SC_RANDOM_KEY);
Copy link
Member

Choose a reason for hiding this comment

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

似乎不算随机,对网站来说,performance.timeOrigin和SC_RANDOM_KEY都是固定值,可以获取到的,协商过程也会被网站监听到。这个方案也可以判死刑了🧐

Copy link
Collaborator Author

@cyfung1031 cyfung1031 Jan 22, 2026

Choose a reason for hiding this comment

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

不会不会

首先 SC_RANDOM_KEY 是每次 build 都不一样
除非那些网站故意把ScriptCat每一次的build 都记录起来吧
这个是单向加密。所以网站记起来也要一个个试!?

重点是 scripting 这个
Screenshot 2026-01-22 at 22 06 37

先假设你说的情况会发生
MessageFlag被截取了
executorEnvReadyKey 能生成出来

我刚刚 commit 了 10941fe

在 content.js inject.js 执行后 readyFlag 会变成 3/4
scripting 不会再处理 executorEnvReadyKey
content/inject 也因为已经处理过所以不会再理会 executorEnvReadyKey

scripting 和 content/inject 之间通讯用的 "injectFlagEvt" 不会被截取出来
(这玩意是随机生成。由 scripting 告诉 content/inject )


MessageFlag 是 ScriptCat 共通的接口
而只有最初载入时用这个接口沟通,才能获得真正的 token "injectFlagEvt" (完全随机)

Copy link
Member

@CodFrm CodFrm Jan 23, 2026

Choose a reason for hiding this comment

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

除非那些网站故意把ScriptCat每一次的build 都记录起来吧

我的意思就是这个,每个版本都是固定的值,你的初始化值是固定的,你后续的随机又有什么意义呢?还不如直接用 SC_RANDOM_KEY

Copy link
Member

Choose a reason for hiding this comment

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

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

@cyfung1031
Copy link
Collaborator Author

cyfung1031 commented Jan 22, 2026

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

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

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

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

@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
不過的確不太需要

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