Skip to content

GM注入优化#517

Merged
CodFrm merged 15 commits intoscriptscat:mainfrom
cyfung1031:fix_gm_content3
Jul 10, 2025
Merged

GM注入优化#517
CodFrm merged 15 commits intoscriptscat:mainfrom
cyfung1031:fix_gm_content3

Conversation

@cyfung1031
Copy link
Collaborator

@cyfung1031 cyfung1031 commented Jul 10, 2025

(首先不好意思,没有及时提交,跟最新的commit会有衝突


之前我在改动时发现了以下问题

在脚本裡可以直接存取变量 EE (修改后也知道了是手动打这些。手动加漏了吧)
研究 GM API 的行为时,发现depend 会导致脚本裡出现其他API
(例如我们只需要 GM.getValues, 但会连同 GM_getValue 都给了出来)

於是重构了几个相关部份

为了容易看,我把他们分开了5个commit


重构 GM Api 内部代码处理
+
methodInject 处理内部化

这个是弃用以往的做法,会在GMContext那裡直接写好API,而不是各处加一堆特别处理(像cookie)
现在定明,GM.cookie.set 裡面做什麼,当@grant 了什麼权限才会有这个之类

这样除了清楚,方便日后修改外,还减少了一堆 nested function 的效能下降隐藏问题

现在我们用TypeScript,用ESM,通用的我们可以直接抽出来,不用grant来Grant去
我不是全改了。只是常用修改了一下。

当然还保留了 depend 的做法。使用方法不变。见 methodInject

针对 EE 那些漏掉的问题
现在会在context出现的都要写在 GM_Base, 然后加一个 @protected
这样的话,就会自动生成一个 不要外漏的清单


优化注入代码

这个改动了Context那个部份

在產生sandbox context 时,直接按protect清单,把context裡的GM或CAT API东西抄到 exposedObject (旧称thisContext)

(终极目标是减少proxy的处理。Proxy用在with上面有效能问题。)

另外,本来是用有名字的变量 context 来做Injection. 实际上我们用arguments就好了
arguments只在顶层scope有效。跑code 那裡就不能存取

error 那个我拿走了
因为现在async 设计,async裡的出错也不会被抓到
如果真的发生了问题,瀏览器本身会报错 (有sourceURL能追踪)
所以不用特别加报错处理吧

要注入的东西在生成sandbox就注入
之后的proxy 不要搞这麼多判断

我看不懂之前 ExecScript .apply时要把 this 都绑定的理由。现在是改成绑定null. 实际使用也没差别

之前绑定了GM_info. 但这个会在context 给出,应该不用绑定吧
跟TM那些一样。用了 @grant none, GM_infoGM 也不会有


这些就是提交的改动

看不懂再提出。

或者需要修改的地方 (我只使用一般TM腳本,沒使用什麼後台腳本。TM腳本有測試過沒問題)

@cyfung1031
Copy link
Collaborator Author

cyfung1031 commented Jul 10, 2025

呀。測試沒通過

因為我主要用 UserScript. 不會有回傳值

  it("global", async () => {
    scriptRes2.code = "window.testObj = 'ok';return window.testObj";
    sandboxExec.scriptFunc = compileScript(compileScriptCode(scriptRes2));
    let ret = await sandboxExec.exec();
    expect(ret).toEqual("ok");
    scriptRes2.code = "window.testObj = 'ok2';return testObj";
    sandboxExec.scriptFunc = compileScript(compileScriptCode(scriptRes2));
    ret = await sandboxExec.exec();
    expect(ret).toEqual("ok2");
  });
  it("this", async () => {
    scriptRes2.code = "this.testObj='ok2';return testObj;";
    sandboxExec.scriptFunc = compileScript(compileScriptCode(scriptRes2));
    const ret = await sandboxExec.exec();
    expect(ret).toEqual("ok2");
  });

這些測試要有回傳值...
測試改就可以?
還是你實際操作上有用到回傳值的部份?


如果要這樣保留傳回值測試,可以這樣改

在測試時指定為測試模式

測試模式的話不使用

  return `with(arguments[0]){
${preCode}
[((async function(){
${code}
})(),0)];
}`;

而使用

  return `with(arguments[0]){
${preCode}
return (function(){
${code}
})();
}`;

try {
return factory.apply(context, []);
} catch (e) {
if (e.message && e.stack) {
Copy link
Member

Choose a reason for hiding this comment

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

这里的错误处理是有意义的,避免一个脚本出错,影响到上层的内容,例如脚本执行顺序是A-B-C,如果A的错误不catch,会导致B、C无法运行

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

喔原來是這樣。這個可以改回來。(最初想法只是不想加一堆代碼做錯誤處理)
(改回來就是上面 // 注釋的部份)

return false;
const exposedProxy = new Proxy(exposedObject, {
defineProperty(target, name, desc) {
return Reflect.defineProperty(target, name, desc);
Copy link
Member

Choose a reason for hiding this comment

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

为什么不是Object.defineProperty

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

一般在Proxy裡面都是用Reflect
你看回傳就懂了吧
Reflect.defineProperty跟Proxy裡面的defineProperty一樣,傳boolean
基本上 Proxy裡面的設定,全部都跟Reflect對應

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

之前exposedProxy (proxy) 的target 不是exposedObject (thisContext),才需要寫defineProperty

    defineProperty(target, name, desc) {
      return Reflect.defineProperty(target, name, desc);
    },

現在參數都是一樣,直接把defineProperty拿掉也可以

Copy link
Member

@CodFrm CodFrm Jul 10, 2025

Choose a reason for hiding this comment

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

好的,听起来没问题,这一块内容从开始到现在没有怎么改变过,担心会影响到脚本的功能,一直都是往上堆🤣

@CodFrm
Copy link
Member

CodFrm commented Jul 10, 2025

ExecScript.apply,这个我也记不清了,似乎是和none的this有关系

TM @grant none,是可以获取到GM_info的:https://github.com/scriptscat/scriptcat/blob/main/example/grant_none.js

建议不要改动单元测试,使用单元测试能通过的模式

@cyfung1031
Copy link
Collaborator Author

cyfung1031 commented Jul 10, 2025

ExecScript.apply,这个我也记不清了,似乎是和none的this有关系

TM @grant none,是可以获取到GM_info的:https://github.com/scriptscat/scriptcat/blob/main/example/grant_none.js

建议不要改动单元测试,使用单元测试能通过的模式

這個範例沒有 @grant none


TM或者其他都是沒GM_info吧
@grant none 不就是為了完全不使用 GM 的東西嗎


Screenshot 2025-07-10 at 18 47 46

ScriptCat現在,@grant none 的處理是不注入任何GM API, 但有 GM_info, 這樣不是很奇怪嗎


660ccc3

剛推了這個commit了,單元測試能過。不用擔心

@CodFrm
Copy link
Member

CodFrm commented Jul 10, 2025

image

但TM事实上就是有的(可以看到this是一个{}),不好意思,我的示例有问题

// ==UserScript==
// @name         Grant None
// @namespace    https://bbs.tampermonkey.net.cn/
// @version      0.1.0
// @description  try to take over the world!
// @author       You
// @match        https://bbs.tampermonkey.net.cn/
// @grant none
// ==/UserScript==

console.log("Grant None", this, GM_info);

(非@grant none)的情况,是一个Proxy

image

*/
exec() {
this.logger.debug("script start");
return this.scriptFunc.apply(this.proxyContent, [this.proxyContent, this.GM_info]);
Copy link
Member

Choose a reason for hiding this comment

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

在之前的实现中,不注入任何GM Api,但是GM_info是通过参数传递的

Copy link
Member

@CodFrm CodFrm Jul 10, 2025

Choose a reason for hiding this comment

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

这里的this,似乎也要传递,参考我上述的脚本

另外有一些脚本会直接使用下面的形式,没有this似乎也会出现问题

onload=()=>{
console.log('load');
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

我不明白。首先這個肯定跟單元測試無關。已經通過了

現在代碼:

return ` with(context){
return ((factory) => {
try {
return factory.apply(context, []);
} catch (e) {
if (e.message && e.stack) {
console.error("ERROR: Execution of script '${name}' failed! " + e.message);
console.log(e.stack);
} else {
console.error(e);
}
}
})(async function(){
${code}
})
}`;

現在的是

async function(){ 
           ${code} 
       }

還有

return factory.apply(context, []); 

這不是已經指定了this 嗎? (雖然我不明白使用了 with(context) 還要再指定 this = context 是有什麼用)

Copy link
Collaborator Author

@cyfung1031 cyfung1031 Jul 10, 2025

Choose a reason for hiding this comment

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

我打算(修改commit) 保留 code 裡面的 this = context ( 油猴是這樣)

但 this.scriptFunc.apply 不用 指定this 吧

@cyfung1031
Copy link
Collaborator Author

@CodFrm 確認了最新版油猴。先不理他那三個undefined define exports module
是有GM和GM_info
(GM那個現時的ScriptCat也漏了吧)

我會加回來(類似現時ScriptCat這樣)

Screenshot 2025-07-10 at 18 52 30 Screenshot 2025-07-10 at 18 52 50

@CodFrm
Copy link
Member

CodFrm commented Jul 10, 2025

似乎是单元测试失效了,我修复了一下,另外不用特意添加testMode吧

this也有问题,我补充单元测试了

@cyfung1031
Copy link
Collaborator Author

似乎是单元测试失效了,我修复了一下,另外不用特意添加testMode吧

this也有问题,我补充单元测试了

實際使用不用回傳值嘛

不是只為了測試才return 嗎

@CodFrm
Copy link
Member

CodFrm commented Jul 10, 2025

后台脚本中,需要用到返回值

后台脚本return了promise,需要拿这个promise做处理,可以参考example中的

https://github.com/scriptscat/scriptcat/blob/main/example/userconfig.js

https://github.com/scriptscat/scriptcat/blob/main/example/gm_bg_menu.js

expect(ret.script.version).toEqual("1.0.0");
expect(ret.GM_info.version).toEqual(ExtVersion);
expect(ret.GM_info.script.version).toEqual("1.0.0");
expect(ret._this).toEqual({});
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

你這個要跟 TM 一樣把 this 指定成 {}?
而不是window (原生) ?

Copy link
Member

Choose a reason for hiding this comment

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

window确实更合理一下,那就和之前保持一致用window吧

@cyfung1031
Copy link
Collaborator Author

@CodFrm 过了单元测试。
你整体再检查一下吧

@CodFrm CodFrm requested a review from Copilot July 10, 2025 11:42
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

This PR overhauls the GM API injection mechanism to centralize and streamline how APIs are exposed in sandboxed scripts, reduce proxy overhead, and improve maintainability.

  • Consolidates API registration and method injection in GMContext/createContext and introduces a @protected decorator for hidden members.
  • Refactors compileScriptCode, proxyContext, and execScript to use argument-based injection and strict equality checks.
  • Removes the custom lodash.has utility and replaces it with an inline has implementation in the content layer.

Reviewed Changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/pkg/utils/lodash.ts Removed legacy has export in favor of inline implementation.
src/app/service/service_worker/runtime.ts Switched to strict !== undefined filters and updated injector.
src/app/service/service_worker/client.ts Replaced != with !== in response code check.
src/app/service/content/utils.ts Added inline has, protect import, and revised code wrapper.
src/app/service/content/types.ts Updated ScriptFunc signature and added follow/fnKey fields.
src/app/service/content/gm_context.ts Changed API map to Map<string, ApiValue[]> and added decorator.
src/app/service/content/gm_api.ts Major refactor: extracted helper functions and unified API calls.
src/app/service/content/exec_script.ts Adjusted constructor and exec to use argument-based injection.
src/app/service/content/exec_script.test.ts Updated tests to align with new exec and compileScriptCode.
src/app/service/content/create_context.ts Redesigned method injection (__methodInject__) and removed legacy patterns.
Comments suppressed due to low confidence (2)

src/app/service/content/gm_context.ts:21

  • [nitpick] Defining a static method named protected can be confusing since protected is a TypeScript access modifier keyword. Consider renaming this decorator to defineProtected or markProtected for clarity.
  public static protected(value: any = undefined) {

src/app/service/content/gm_context.ts:17

  • [nitpick] You’ve introduced a protect list that hides properties from the sandbox. Add unit tests in utils.test.ts or a new test suite to verify that @Protected members are indeed not accessible via proxyContext.
export const protect: { [key: string]: any } = {};

window: {
onurlchange: null,
},
grantSet: new Set()
Copy link

Copilot AI Jul 10, 2025

Choose a reason for hiding this comment

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

The passed-in grantSet argument is not used here; initializing a new empty set will prevent any @grant-based methods from being injected. Replace new Set() with the function parameter grantSet.

Suggested change
grantSet: new Set()
grantSet: grantSet

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.

預計 scripts 通常都有 @grant 而且多於一個。所以 __methodInject__ 裡不放初始化(或判定)。直接預設一個空的Set

Copy link
Collaborator Author

@cyfung1031 cyfung1031 Jul 10, 2025

Choose a reason for hiding this comment

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

兩個 grantSet 是不同的。雖然名字一樣
傳入的是 需要的 grant (不重覆)
context的 grantSet 是用來記錄有沒有注入到 context

Copy link
Member

@CodFrm CodFrm Jul 10, 2025

Choose a reason for hiding this comment

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

我进行了一下修改,全部一个名字有点混淆

}
})
.filter((it) => it != null)
.filter((it) => it !== undefined)
Copy link

Copilot AI Jul 10, 2025

Choose a reason for hiding this comment

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

Changing from != null to !== undefined will no longer filter out null values. If both null and undefined should be removed, consider using it != null or explicitly checking for both.

Suggested change
.filter((it) => it !== undefined)
.filter((it) => it != null)

Copilot uses AI. Check for mistakes.
Copy link
Member

@CodFrm CodFrm Jul 10, 2025

Choose a reason for hiding this comment

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

上述没有返回值,(it) => it != null似乎是合理的

!== 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.

因為代碼裡只有 有物件 和 無物件(沒回傳=undefined)
不會出現null, 反而是undefined
!== 避免類型轉換

Comment on lines 57 to 75
// if (scriptRes.metadata.grant) {
// // 处理GM.与GM_,将GM_与GM.都复制一份
// const grant: string[] = [];
// scriptRes.metadata.grant.forEach((val) => {
// // if (val.startsWith("GM_")) {
// // const t = val.slice(3);
// // grant.push(`GM.${t}`);
// // } else if (val.startsWith("GM.")) {
// // grant.push(val);
// // }
// // grant.push(val);
// grant.push(val);
// });
// // 去重
// const uniqueGrant = new Set(grant);
// for(const grant of uniqueGrant){
// context.__methodInject__(grant);
// }
// }
Copy link

Copilot AI Jul 10, 2025

Choose a reason for hiding this comment

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

[nitpick] There is a large block of commented-out legacy code remaining here. Consider removing it to improve readability and reduce confusion.

Suggested change
// if (scriptRes.metadata.grant) {
// // 处理GM.与GM_,将GM_与GM.都复制一份
// const grant: string[] = [];
// scriptRes.metadata.grant.forEach((val) => {
// // if (val.startsWith("GM_")) {
// // const t = val.slice(3);
// // grant.push(`GM.${t}`);
// // } else if (val.startsWith("GM.")) {
// // grant.push(val);
// // }
// // grant.push(val);
// grant.push(val);
// });
// // 去重
// const uniqueGrant = new Set(grant);
// for(const grant of uniqueGrant){
// context.__methodInject__(grant);
// }
// }
// Removed commented-out legacy code for improved readability.

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.

現在的寫法已經不用把GM. GM_ 重覆grant
depend 那個已經很足夠

只是保留一下舊代碼

Copy link
Member

Choose a reason for hiding this comment

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

旧代码删掉没有关系,我一般也习惯通过git history去查看

@CodFrm CodFrm merged commit 1d6d52a into scriptscat:main Jul 10, 2025
1 of 2 checks passed
@CodFrm
Copy link
Member

CodFrm commented Jul 10, 2025

🙏非常感谢,现在这块的可读性很好了

@GMContext.API({
depend: ["CAT_fetchBlob", "CAT_createBlobUrl", "CAT_fetchDocument"],
})
public ['GM.xmlHttpRequest'](details: GMTypes.XHRDetails) {
Copy link
Member

@CodFrm CodFrm Jul 10, 2025

Choose a reason for hiding this comment

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

应该只存在 GM_xmlhttpRequestGM.xmlHttpRequest 吧,不需要定义另外的两个(没想到可以这样定义)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

原來是這樣! 我以為你方便用家打錯大小寫

Copy link
Member

Choose a reason for hiding this comment

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

这个问题我觉得也很尴尬,命名不规范,GM_getResourceURL与GM.getResourceUrl也是一样

}
let { follow } = param;
if (!follow) follow = key; // follow 是实际 @grant 的权限
GMContextApiSet(follow, key, descriptor.value, param);
Copy link
Member

@CodFrm CodFrm Jul 10, 2025

Choose a reason for hiding this comment

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

GM.saveTab 之类的 API 似乎会出现问题

// ==UserScript==
// @name         New Userscript
// @namespace    https://bbs.tampermonkey.net.cn/
// @version      0.1.0
// @description  try to take over the world!
// @author       You
// @match        https://bbs.tampermonkey.net.cn/
// @grant        GM.getTab
// @grant        GM_getTab
// ==/UserScript==

console.log(GM_getTab, "---", GM.getTab);

Copy link
Collaborator Author

Choose a reason for hiding this comment

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


為什麼呢

它們都沒有depend

裡面的都是直接用sendMessage

Copy link
Member

Choose a reason for hiding this comment

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

没有实现GM.getTab,在此之前是直接替换_=>.去处理的,所以就没这个问题

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

我重現到了。我看一下是哪裡的問題。

Copy link
Collaborator Author

@cyfung1031 cyfung1031 Jul 10, 2025

Choose a reason for hiding this comment

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

呀。對。因為現在要寫清楚每一個
最初ScriptCat以為 . 跟 _ 一樣嘛
然後 GM_cookie 出來後就死掉


反正就是每個用法都要寫出來。
現在要加回 GM.getTab

Copy link
Member

Choose a reason for hiding this comment

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

这样抽出来,放顶层感觉可读性也不是很好,将静态方法作为实际实现是不是也可以:67b1b0d

Copy link
Collaborator Author

@cyfung1031 cyfung1031 Jul 10, 2025

Choose a reason for hiding this comment

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

@CodFrm 剛想到了
可以像你後台腳本那邊做法一樣
加 alias

  @GMContext.API()
  GM_saveTab(obj: object) {
  @GMContext.API({alias: 'GM.saveTab'})
  GM_saveTab(obj: object) {

我等一下提交這個 alias 吧
這樣代碼也比較少

Copy link
Member

@CodFrm CodFrm Jul 10, 2025

Choose a reason for hiding this comment

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

也可以,另外将静态方法作为实际实现,和定义放在一起,不放在顶层,我觉得也是可以的

Copy link
Member

@CodFrm CodFrm Jul 10, 2025

Choose a reason for hiding this comment

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

Copy link
Member

Choose a reason for hiding this comment

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

我处理好了 #518

@cyfung1031 cyfung1031 deleted the fix_gm_content3 branch July 11, 2025 08:41
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