From 5e72557c77546247414a5588b38d250b3fac4235 Mon Sep 17 00:00:00 2001
From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com>
Date: Sat, 7 Feb 2026 14:34:15 +0900
Subject: [PATCH 1/6] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=20`@unwrap`?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/app/service/content/utils.ts | 4 ++++
src/app/service/service_worker/utils.ts | 18 ++++++++++++------
2 files changed, 16 insertions(+), 6 deletions(-)
diff --git a/src/app/service/content/utils.ts b/src/app/service/content/utils.ts
index 6fe01c634..b56f7e044 100644
--- a/src/app/service/content/utils.ts
+++ b/src/app/service/content/utils.ts
@@ -186,6 +186,10 @@ export function isEarlyStartScript(metadata: SCMetadata): boolean {
return metadataBlankOrTrue(metadata, "early-start") && metadata["run-at"]?.[0] === "document-start";
}
+export function isScriptletUnwrap(metadata: SCMetadata): boolean {
+ return metadataBlankOrTrue(metadata, "unwrap");
+}
+
export function isInjectIntoContent(metadata: SCMetadata): boolean {
return metadata["inject-into"]?.[0] === "content";
}
diff --git a/src/app/service/service_worker/utils.ts b/src/app/service/service_worker/utils.ts
index 9e7adfeba..6688f7f9f 100644
--- a/src/app/service/service_worker/utils.ts
+++ b/src/app/service/service_worker/utils.ts
@@ -1,6 +1,6 @@
export const BrowserNoSupport = new Error("browserNoSupport");
import type { SCMetadata, Script, ScriptLoadInfo, ScriptRunResource } from "@App/app/repo/scripts";
-import { getMetadataStr, getUserConfigStr } from "@App/pkg/utils/utils";
+import { getMetadataStr, getUserConfigStr, sourceMapTo } from "@App/pkg/utils/utils";
import type { ScriptMatchInfo } from "./types";
import {
compileInjectScript,
@@ -9,6 +9,7 @@ import {
getScriptFlag,
isEarlyStartScript,
isInjectIntoContent,
+ isScriptletUnwrap,
} from "../content/utils";
import {
extractUrlPatterns,
@@ -173,13 +174,18 @@ export function compileInjectionCode(
scriptCode: string,
scriptUrlPatterns: URLRuleEntry[]
): string {
- const preDocumentStartScript = isEarlyStartScript(scriptRes.metadata);
let scriptInjectCode;
- scriptCode = compileScriptCode(scriptRes, scriptCode);
- if (preDocumentStartScript) {
- scriptInjectCode = compilePreInjectScript(parseScriptLoadInfo(scriptRes, scriptUrlPatterns), scriptCode);
+ if (isScriptletUnwrap(scriptRes.metadata)) {
+ // 在window[flag]注册一个空脚本让原本的脚本管理器知道并记录脚本成功执行
+ const codeBody = `${scriptCode}\nwindow['${scriptRes.flag}'] = function(){};`;
+ scriptInjectCode = `${codeBody}${sourceMapTo(`${scriptRes.name}.user.js`)}\n`;
} else {
- scriptInjectCode = compileInjectScript(scriptRes, scriptCode);
+ scriptCode = compileScriptCode(scriptRes, scriptCode);
+ if (isEarlyStartScript(scriptRes.metadata)) {
+ scriptInjectCode = compilePreInjectScript(parseScriptLoadInfo(scriptRes, scriptUrlPatterns), scriptCode);
+ } else {
+ scriptInjectCode = compileInjectScript(scriptRes, scriptCode);
+ }
}
return scriptInjectCode;
}
From 5f178136d74dd9933776caf887ea72b683b5e5c3 Mon Sep 17 00:00:00 2001
From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com>
Date: Sat, 7 Feb 2026 14:52:59 +0900
Subject: [PATCH 2/6] =?UTF-8?q?=E5=8A=A0=20example=20js=20=E5=8A=A0=20mona?=
=?UTF-8?q?co=20editor=20hint?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
example/tests/unwrap_test.js | 13 +++++++++++++
src/pkg/utils/monaco-editor/index.ts | 14 ++++++++++++++
2 files changed, 27 insertions(+)
create mode 100644 example/tests/unwrap_test.js
diff --git a/example/tests/unwrap_test.js b/example/tests/unwrap_test.js
new file mode 100644
index 000000000..42f248b85
--- /dev/null
+++ b/example/tests/unwrap_test.js
@@ -0,0 +1,13 @@
+// ==UserScript==
+// @name A Scriptlet for @grant unwrap test
+// @namespace none
+// @version 2026-02-07
+// @description try to take over the world!
+// @author You
+// @match https://*/*?test_grant_unwrap
+// @grant GM_setValue
+// @unwrap
+// ==/UserScript==
+
+var test_global_injection = "success"; // User can access the variable "test_global_injection" directly in DevTools
+console.log(`Expected Result: typeof GM = ${typeof GM} = undefined; typeof GM_setValue = ${typeof GM_setValue} = undefined`);
diff --git a/src/pkg/utils/monaco-editor/index.ts b/src/pkg/utils/monaco-editor/index.ts
index 6c91de174..bc06a138f 100644
--- a/src/pkg/utils/monaco-editor/index.ts
+++ b/src/pkg/utils/monaco-editor/index.ts
@@ -49,6 +49,8 @@ const langs = {
"脚本注入环境
`content`:脚本注入到 content 环境
`page`:脚本注入到网页环境(默认)
注:SC 不支持以 CSP 判断是否需要脚本注入到 content 环境的 `inject-into: auto` 设计。",
"early-start":
"配合 `run-at: document-start` 的声明,使用 `early-start` 可以比网页更快地加载并执行脚本,但存在一定性能问题与 GM API 使用限制。(SC 独有)",
+ unwrap:
+ "让用户脚本不经过沙箱封装,直接注入并运行在页面的原生全局作用域中。
脚本可直接访问和修改页面真实的全局变量,但将无法使用 GM.* 等用户脚本特权 API。
常用于需要与页面原生脚本深度交互或从普通页面脚本迁移的场景。",
definition: "ScriptCat 特有功能:一个 `.d.ts` 文件的引用地址,能够自动补全编辑器的提示",
// https://bbs.tampermonkey.net.cn/thread-3036-1-1.html#%40antifeature%E8%A7%84%E5%88%99
antifeature: `与脚本市场有关,不受欢迎的功能需要加上此描述值
@@ -118,6 +120,8 @@ tracking:该脚本会追踪你的用户信息`.replace(/\n/g, "
"),
"Script injection context
`content`: inject into content context
`page`: inject into page context (default)
Note: SC does not support `inject-into: auto`, which chooses context based on CSP.",
"early-start":
"Used with `run-at: document-start`. `early-start` lets the script execute even earlier than the page, but may affect performance and limit GM APIs. (SC only)",
+ unwrap:
+ "Makes the user script bypass sandbox wrapping and be injected and executed directly in the page’s native global scope.
The script can directly access and modify the page’s real global variables, but will not be able to use user script privileged APIs such as GM.*.
Commonly used in scenarios that require deep interaction with native page scripts or when migrating from regular page scripts.",
definition: "ScriptCat-only: URL of a `.d.ts` file used for editor auto-completion",
antifeature: "For script markets: describe any unwanted or controversial features",
updateURL: "URL used to check for script updates",
@@ -180,6 +184,8 @@ tracking:该脚本会追踪你的用户信息`.replace(/\n/g, "
"),
"腳本注入環境
`content`:將腳本注入 content 環境
`page`:將腳本注入網頁環境(預設)
註:SC 不支援依據 CSP 判斷是否注入 content 環境的 `inject-into: auto`。",
"early-start":
"配合 `run-at: document-start` 使用,`early-start` 可以比網頁更早載入並執行腳本,但可能造成效能問題與 GM API 限制。(SC 獨有)",
+ unwrap:
+ "讓使用者腳本不經過沙箱封裝,直接注入並執行於頁面的原生全域作用域中。
腳本可直接存取並修改頁面真實的全域變數,但將無法使用 GM.* 等使用者腳本的特權 API。
常用於需要與頁面原生腳本深度互動,或從一般頁面腳本遷移的場景。",
definition: "ScriptCat 特有功能:一個 `.d.ts` 檔案的引用網址,可啟用編輯器自動提示",
antifeature: "與腳本市場相關,不受歡迎的功能需要在此描述",
updateURL: "腳本檢查更新的 url",
@@ -242,6 +248,8 @@ tracking:该脚本会追踪你的用户信息`.replace(/\n/g, "
"),
"スクリプトの注入コンテキスト
`content`:コンテンツスクリプト環境に注入
`page`:ページコンテキストに注入(既定)
注:SC は CSP に基づき自動でコンテキストを切り替える `inject-into: auto` には対応していません。",
"early-start":
"`run-at: document-start` と併用します。`early-start` を指定するとページよりも早くスクリプトを実行できますが、パフォーマンスへの影響や GM API の制限が発生する場合があります(SC 独自機能)。",
+ unwrap:
+ "ユーザースクリプトをサンドボックスでラップせず、ページのネイティブなグローバルスコープに直接注入して実行します。
スクリプトはページの実際のグローバル変数に直接アクセスおよび変更できますが、GM.* などのユーザースクリプトの特権 API は使用できなくなります。
ページのネイティブスクリプトとの深い連携が必要な場合や、通常のページスクリプトから移行する際によく使用されます。",
definition: "ScriptCat 専用機能:`.d.ts` ファイルの URL。エディタの補完を有効にします。",
antifeature: "スクリプトマーケット向け:好まれない機能がある場合、ここに説明を記載します。",
updateURL: "スクリプト更新を確認する URL",
@@ -304,6 +312,8 @@ tracking:该脚本会追踪你的用户信息`.replace(/\n/g, "
"),
"Skript-Injektionskontext
`content`: in den Content-Kontext injizieren
`page`: in den Seitenkontext injizieren (Standard)
Hinweis: SC unterstützt `inject-into: auto` nicht, bei dem der Kontext über CSP gewählt wird.",
"early-start":
"Wird mit `run-at: document-start` verwendet. `early-start` lässt das Skript noch vor der Seite laufen, kann aber die Leistung beeinträchtigen und GM-APIs einschränken. (Nur in SC)",
+ unwrap:
+ "Ermöglicht es, das Benutzerskript ohne Sandbox-Kapselung direkt in den nativen globalen Gültigkeitsbereich der Seite zu injizieren und auszuführen.
Das Skript kann direkt auf die tatsächlichen globalen Variablen der Seite zugreifen und diese verändern, kann jedoch keine privilegierten Benutzerskript-APIs wie GM.* verwenden.
Wird häufig in Szenarien eingesetzt, die eine tiefe Interaktion mit nativen Seitenskripten erfordern oder bei der Migration von normalen Seitenskripten.",
definition: "Nur für ScriptCat: URL zu einer `.d.ts`-Datei für Editor-Autovervollständigung",
antifeature: "Für Script-Marktplätze: hier unerwünschte oder kontroverse Funktionen beschreiben",
updateURL: "URL zur Aktualisierungsprüfung des Skripts",
@@ -366,6 +376,8 @@ tracking:该脚本会追踪你的用户信息`.replace(/\n/g, "
"),
"Ngữ cảnh chèn script
`content`: chèn vào ngữ cảnh content
`page`: chèn vào ngữ cảnh trang (mặc định)
Lưu ý: SC không hỗ trợ `inject-into: auto`, lựa chọn ngữ cảnh dựa trên CSP.",
"early-start":
"Dùng cùng với `run-at: document-start`. `early-start` cho phép script chạy sớm hơn cả trang, nhưng có thể gây ảnh hưởng hiệu năng và giới hạn một số GM API. (Chỉ có trong SC)",
+ unwrap:
+ "Cho phép script người dùng bỏ qua sandbox và được chèn, thực thi trực tiếp trong phạm vi toàn cục gốc của trang.
Script có thể trực tiếp truy cập và chỉnh sửa các biến toàn cục thực sự của trang, nhưng sẽ không thể sử dụng các API đặc quyền của user script như GM.*.
Thường được dùng trong các trường hợp cần tương tác sâu với script gốc của trang hoặc khi chuyển đổi từ script trang thông thường.",
definition: "Tính năng riêng của ScriptCat: URL tới tệp `.d.ts` giúp bật gợi ý tự động trong trình soạn thảo",
antifeature: "Dùng cho chợ script: mô tả các tính năng không được người dùng ưa thích",
updateURL: "URL dùng để kiểm tra cập nhật script",
@@ -428,6 +440,8 @@ tracking:该脚本会追踪你的用户信息`.replace(/\n/g, "
"),
"Контекст внедрения скрипта
`content`: внедрить в контекст content
`page`: внедрить в контекст страницы (по умолчанию)
Примечание: SC не поддерживает `inject-into: auto`, когда контекст выбирается по CSP.",
"early-start":
"Используется совместно с `run-at: document-start`. `early-start` позволяет выполнять скрипт раньше загрузки страницы, но может ухудшать производительность и ограничивать некоторые GM API. (Только в SC)",
+ unwrap:
+ "Позволяет пользовательскому скрипту обходить песочницу и напрямую внедряться и выполняться в нативной глобальной области видимости страницы.
Скрипт может напрямую получать доступ к реальным глобальным переменным страницы и изменять их, однако не сможет использовать привилегированные API пользовательских скриптов, такие как GM.*.
Обычно используется в сценариях, требующих глубокой интеграции с нативными скриптами страницы или при миграции с обычных скриптов страницы.",
definition: "Особенность ScriptCat: URL файла `.d.ts`, используемого для автодополнения в редакторе",
antifeature: "Для маркетплейсов скриптов: опишите здесь нежелательные / спорные функции",
updateURL: "URL для проверки обновлений скрипта",
From 2e43972044ece85205acbb819ea7eb5a077bee72 Mon Sep 17 00:00:00 2001
From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com>
Date: Sat, 7 Feb 2026 15:13:05 +0900
Subject: [PATCH 3/6] =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E4=BB=A3=E7=A0=81?=
=?UTF-8?q?=E5=8F=8A=E6=B5=8B=E8=AF=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
example/tests/unwrap_test.js | 57 +++++++++++++++++++++++--
src/app/service/content/utils.ts | 16 +++++++
src/app/service/service_worker/utils.ts | 7 ++-
3 files changed, 72 insertions(+), 8 deletions(-)
diff --git a/example/tests/unwrap_test.js b/example/tests/unwrap_test.js
index 42f248b85..1af8ec787 100644
--- a/example/tests/unwrap_test.js
+++ b/example/tests/unwrap_test.js
@@ -1,13 +1,62 @@
// ==UserScript==
-// @name A Scriptlet for @grant unwrap test
+// @name A Scriptlet for @unwrap test
// @namespace none
// @version 2026-02-07
// @description try to take over the world!
// @author You
-// @match https://*/*?test_grant_unwrap
+// @match https://*/*?test_unwrap
// @grant GM_setValue
+// @require https://cdn.jsdelivr.net/npm/jquery@3.6.0/dist/jquery.min.js#sha384-vtXRMe3mGCbOeY7l30aIg8H9p3GdeSe4IFlP6G8JMa7o7lXvnz3GFKzPxzJdPfGK
// @unwrap
// ==/UserScript==
-var test_global_injection = "success"; // User can access the variable "test_global_injection" directly in DevTools
-console.log(`Expected Result: typeof GM = ${typeof GM} = undefined; typeof GM_setValue = ${typeof GM_setValue} = undefined`);
+var test_global_injection = "success";
+// User can access the variable "test_global_injection" directly in DevTools
+
+(function () {
+ const results = {
+ GM: {
+ expected: "undefined",
+ actual: typeof GM,
+ },
+ GM_setValue: {
+ expected: "undefined",
+ actual: typeof GM_setValue,
+ },
+ jQuery: {
+ expected: "function",
+ actual: typeof jQuery,
+ },
+ };
+
+ console.group(
+ "%c@unwrap Test",
+ "color:#0aa;font-weight:bold"
+ );
+
+ const table = {};
+ let allPass = true;
+
+ for (const key in results) {
+ const { expected, actual } = results[key];
+ const pass = expected === actual;
+ allPass &&= pass;
+
+ table[key] = {
+ Expected: expected,
+ Actual: actual,
+ Result: pass ? "✅ PASS" : "❌ FAIL",
+ };
+ }
+
+ console.table(table);
+
+ console.log(
+ allPass
+ ? "%cAll tests passed ✔"
+ : "%cSome tests failed ✘",
+ `font-weight:bold;color:${allPass ? "green" : "red"}`
+ );
+
+ console.groupEnd();
+})();
diff --git a/src/app/service/content/utils.ts b/src/app/service/content/utils.ts
index b56f7e044..99b2f1cdb 100644
--- a/src/app/service/content/utils.ts
+++ b/src/app/service/content/utils.ts
@@ -25,6 +25,22 @@ export function getScriptRequire(scriptRes: ScriptRunResource): CompileScriptCod
return resourceArray;
}
+/**
+ * 构建unwrap脚本运行代码
+ * @see {@link ExecScript}
+ * @param scriptRes
+ * @param scriptCode
+ * @returns
+ */
+export function compileScriptletCode(scriptRes: ScriptRunResource, scriptCode?: string): string {
+ scriptCode = scriptCode ?? scriptRes.code;
+ const requireArray = getScriptRequire(scriptRes);
+ const requireCode = requireArray.map((r) => r.content).join("\n;");
+ // 在window[flag]注册一个空脚本让原本的脚本管理器知道并记录脚本成功执行
+ const codeBody = `${requireCode}\n${scriptCode}\nwindow['${scriptRes.flag}'] = function(){};`;
+ return `${codeBody}${sourceMapTo(`${scriptRes.name}.user.js`)}\n`;
+}
+
/**
* 构建脚本运行代码
* @see {@link ExecScript}
diff --git a/src/app/service/service_worker/utils.ts b/src/app/service/service_worker/utils.ts
index 6688f7f9f..de42a41c6 100644
--- a/src/app/service/service_worker/utils.ts
+++ b/src/app/service/service_worker/utils.ts
@@ -1,11 +1,12 @@
export const BrowserNoSupport = new Error("browserNoSupport");
import type { SCMetadata, Script, ScriptLoadInfo, ScriptRunResource } from "@App/app/repo/scripts";
-import { getMetadataStr, getUserConfigStr, sourceMapTo } from "@App/pkg/utils/utils";
+import { getMetadataStr, getUserConfigStr } from "@App/pkg/utils/utils";
import type { ScriptMatchInfo } from "./types";
import {
compileInjectScript,
compilePreInjectScript,
compileScriptCode,
+ compileScriptletCode,
getScriptFlag,
isEarlyStartScript,
isInjectIntoContent,
@@ -176,9 +177,7 @@ export function compileInjectionCode(
): string {
let scriptInjectCode;
if (isScriptletUnwrap(scriptRes.metadata)) {
- // 在window[flag]注册一个空脚本让原本的脚本管理器知道并记录脚本成功执行
- const codeBody = `${scriptCode}\nwindow['${scriptRes.flag}'] = function(){};`;
- scriptInjectCode = `${codeBody}${sourceMapTo(`${scriptRes.name}.user.js`)}\n`;
+ scriptInjectCode = compileScriptletCode(scriptRes, scriptCode);
} else {
scriptCode = compileScriptCode(scriptRes, scriptCode);
if (isEarlyStartScript(scriptRes.metadata)) {
From e850758ac5586bd6612fd6473b0b25e9bb5a933a Mon Sep 17 00:00:00 2001
From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com>
Date: Sat, 7 Feb 2026 15:39:30 +0900
Subject: [PATCH 4/6] =?UTF-8?q?=E4=BF=AE=E6=AD=A3=20restoreJSCodeFromCompi?=
=?UTF-8?q?ledResource?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/app/service/service_worker/runtime.ts | 12 ++++++++++--
src/app/service/service_worker/utils.ts | 1 +
2 files changed, 11 insertions(+), 2 deletions(-)
diff --git a/src/app/service/service_worker/runtime.ts b/src/app/service/service_worker/runtime.ts
index 4fa3acbbc..0204551bd 100644
--- a/src/app/service/service_worker/runtime.ts
+++ b/src/app/service/service_worker/runtime.ts
@@ -31,8 +31,10 @@ import type { CompileScriptCodeResource } from "../content/utils";
import {
compileInjectScriptByFlag,
compileScriptCodeByResource,
+ compileScriptletCode,
isEarlyStartScript,
isInjectIntoContent,
+ isScriptletUnwrap,
trimScriptInfo,
} from "../content/utils";
import LoggerCore from "@App/app/logger/core";
@@ -727,9 +729,15 @@ export class RuntimeService {
// 从CompiledResource中还原脚本代码
async restoreJSCodeFromCompiledResource(script: Script, result: CompiledResource) {
- const earlyScript = isEarlyStartScript(script.metadata);
+ // 如果是 Scriptlet (unwrap) 脚本,需要另外的处理方式
+ if (isScriptletUnwrap(script.metadata)) {
+ const scriptRes = await this.script.buildScriptRunResource(script);
+ if (!scriptRes) return "";
+ return compileScriptletCode(scriptRes, scriptRes.code);
+ }
+
// 如果是预加载脚本,需要另外的处理方式
- if (earlyScript) {
+ if (isEarlyStartScript(script.metadata)) {
const scriptRes = await this.script.buildScriptRunResource(script);
if (!scriptRes) return "";
return compileInjectionCode(scriptRes, scriptRes.code, result.scriptUrlPatterns);
diff --git a/src/app/service/service_worker/utils.ts b/src/app/service/service_worker/utils.ts
index de42a41c6..5336ecbb6 100644
--- a/src/app/service/service_worker/utils.ts
+++ b/src/app/service/service_worker/utils.ts
@@ -175,6 +175,7 @@ export function compileInjectionCode(
scriptCode: string,
scriptUrlPatterns: URLRuleEntry[]
): string {
+ // 注意! restoreJSCodeFromCompiledResource 跟 compileInjectionCode 的处理是不同的!
let scriptInjectCode;
if (isScriptletUnwrap(scriptRes.metadata)) {
scriptInjectCode = compileScriptletCode(scriptRes, scriptCode);
From 937b0aeb1a154d82aa89d1a556bf2c7900273e18 Mon Sep 17 00:00:00 2001
From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com>
Date: Sat, 7 Feb 2026 23:48:35 +0900
Subject: [PATCH 5/6] =?UTF-8?q?=E5=8A=A0=E5=85=A5=20`embeddedPatternChecke?=
=?UTF-8?q?rString`=20=E8=AE=A9=20`@unwrap`=20=E5=8F=AF=E4=BB=A5=E5=9C=A8?=
=?UTF-8?q?=20`@exclude`=20=E6=8E=92=E9=99=A4?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/app/service/content/utils.ts | 11 +++-
src/app/service/service_worker/runtime.ts | 2 +-
src/app/service/service_worker/utils.ts | 2 +-
src/pkg/utils/url_matcher.ts | 66 ++++++++++++++++++-----
4 files changed, 65 insertions(+), 16 deletions(-)
diff --git a/src/app/service/content/utils.ts b/src/app/service/content/utils.ts
index 99b2f1cdb..ba4ac386e 100644
--- a/src/app/service/content/utils.ts
+++ b/src/app/service/content/utils.ts
@@ -4,6 +4,7 @@ import type { ScriptLoadInfo } from "../service_worker/types";
import { DefinedFlags } from "../service_worker/runtime.consts";
import { sourceMapTo } from "@App/pkg/utils/utils";
import { ScriptEnvTag } from "@Packages/message/consts";
+import { embeddedPatternCheckerString, type URLRuleEntry } from "@App/pkg/utils/url_matcher";
export type CompileScriptCodeResource = {
name: string;
@@ -32,12 +33,18 @@ export function getScriptRequire(scriptRes: ScriptRunResource): CompileScriptCod
* @param scriptCode
* @returns
*/
-export function compileScriptletCode(scriptRes: ScriptRunResource, scriptCode?: string): string {
+export function compileScriptletCode(
+ scriptRes: ScriptRunResource,
+ scriptCode: string,
+ scriptUrlPatterns: URLRuleEntry[]
+): string {
scriptCode = scriptCode ?? scriptRes.code;
const requireArray = getScriptRequire(scriptRes);
const requireCode = requireArray.map((r) => r.content).join("\n;");
// 在window[flag]注册一个空脚本让原本的脚本管理器知道并记录脚本成功执行
- const codeBody = `${requireCode}\n${scriptCode}\nwindow['${scriptRes.flag}'] = function(){};`;
+ const reducedPatterns = scriptUrlPatterns.map(({ ruleType, ruleContent }) => ({ ruleType, ruleContent }));
+ const urlCondition = embeddedPatternCheckerString("location.href", JSON.stringify(reducedPatterns));
+ const codeBody = `if(${urlCondition}){\n${requireCode}\n${scriptCode}\nwindow['${scriptRes.flag}']=function(){};\n}`;
return `${codeBody}${sourceMapTo(`${scriptRes.name}.user.js`)}\n`;
}
diff --git a/src/app/service/service_worker/runtime.ts b/src/app/service/service_worker/runtime.ts
index 0204551bd..f7b09339d 100644
--- a/src/app/service/service_worker/runtime.ts
+++ b/src/app/service/service_worker/runtime.ts
@@ -733,7 +733,7 @@ export class RuntimeService {
if (isScriptletUnwrap(script.metadata)) {
const scriptRes = await this.script.buildScriptRunResource(script);
if (!scriptRes) return "";
- return compileScriptletCode(scriptRes, scriptRes.code);
+ return compileScriptletCode(scriptRes, scriptRes.code, result.scriptUrlPatterns);
}
// 如果是预加载脚本,需要另外的处理方式
diff --git a/src/app/service/service_worker/utils.ts b/src/app/service/service_worker/utils.ts
index 5336ecbb6..979f6c863 100644
--- a/src/app/service/service_worker/utils.ts
+++ b/src/app/service/service_worker/utils.ts
@@ -178,7 +178,7 @@ export function compileInjectionCode(
// 注意! restoreJSCodeFromCompiledResource 跟 compileInjectionCode 的处理是不同的!
let scriptInjectCode;
if (isScriptletUnwrap(scriptRes.metadata)) {
- scriptInjectCode = compileScriptletCode(scriptRes, scriptCode);
+ scriptInjectCode = compileScriptletCode(scriptRes, scriptCode, scriptUrlPatterns);
} else {
scriptCode = compileScriptCode(scriptRes, scriptCode);
if (isEarlyStartScript(scriptRes.metadata)) {
diff --git a/src/pkg/utils/url_matcher.ts b/src/pkg/utils/url_matcher.ts
index 08f5f37f2..7806b3487 100644
--- a/src/pkg/utils/url_matcher.ts
+++ b/src/pkg/utils/url_matcher.ts
@@ -24,7 +24,7 @@ const URL_MATCH_CACHE_MAX_SIZE = 512; // 用来做简单缓存,512 算是足
// 检查 @match @include @exclude 是否按照MV3的 match pattern
// export 只用于测试,不要在外部直接引用 checkUrlMatch
-export function checkUrlMatch(s: string) {
+export const checkUrlMatch = (s: string): string[] | null => {
s = s.trim();
const idx1 = s.indexOf("://");
@@ -54,9 +54,9 @@ export function checkUrlMatch(s: string) {
}
}
return extMatch;
-}
+};
-const globSplit = (text: string) => {
+const globSplit = (text: string): string[] => {
text = text.replace(/\*{2,}/g, "*"); // api定义的 glob * 是等价于 glob **
text = text.replace(/\*(\?+)/g, "$1*"); // "*????" 改成 "????*",避免 backward 处理
return text.split(/([*?])/g);
@@ -260,7 +260,7 @@ export const isUrlMatch = (url: string, rule: URLRuleEntry) => {
return ret;
};
-function isUrlMatchPattern(s: string, m: string[]) {
+const isUrlMatchPattern = (s: string, m: string[]): boolean => {
let url;
try {
url = new URL(s);
@@ -303,9 +303,9 @@ function isUrlMatchPattern(s: string, m: string[]) {
// 用于处理类似 "http://example.com/path?" 这样的 URL,
// 确保在其余部分匹配时,这类 URL 也会被认为是匹配。
return idx === path.length || (idx === path.length - 1 && path[idx] === "?");
-}
+};
-function isUrlMatchGlob(s: string, gs: string[]) {
+const isUrlMatchGlob = (s: string, gs: string[]): boolean => {
let hashPos = s.indexOf("#");
if (hashPos >= 0) {
const hashPos2 = s.indexOf("#", hashPos + 1);
@@ -368,13 +368,13 @@ function isUrlMatchGlob(s: string, gs: string[]) {
// 用于处理类似 "http://example.com/path?" 这样的 URL,
// 确保在其余部分匹配时,这类 URL 也会被认为是匹配。
return idx === path.length || (idx === path.length - 1 && path[idx] === "?");
-}
+};
-function isUrlMatchRegEx(s: string, ruleContent: [string, string]) {
+const isUrlMatchRegEx = (s: string, ruleContent: [string, string]): boolean => {
return new RegExp(ruleContent[0], ruleContent[1] || "i").test(s);
-}
+};
-export const addMatchesToGlobs = (matches: URLRuleEntry[], globs: string[]) => {
+export const addMatchesToGlobs = (matches: URLRuleEntry[], globs: string[]): void => {
for (const rule of matches) {
if (rule.ruleType !== 1) continue;
const [scheme0, host, path] = rule.ruleContent as string[];
@@ -389,7 +389,7 @@ export const addMatchesToGlobs = (matches: URLRuleEntry[], globs: string[]) => {
}
};
-export const extractMatchPatternsFromGlobs = (globs: string[]) => {
+export const extractMatchPatternsFromGlobs = (globs: string[]): (string | null)[] => {
return globs.map((glob) => {
if (glob.startsWith("http*://")) {
glob = `*://${glob.substring(8)}`;
@@ -403,7 +403,7 @@ export const extractMatchPatternsFromGlobs = (globs: string[]) => {
});
};
-export const extractSchemesOfGlobs = (globs: string[]) => {
+export const extractSchemesOfGlobs = (globs: string[]): string[] => {
const set = new Set(["*://*/*"]);
for (const glob of globs) {
const m = /^([-\w]+):\/\//.exec(glob);
@@ -512,3 +512,45 @@ export const getApiMatchesAndGlobs = (scriptUrlPatterns: URLRuleEntry[]) => {
includeGlobs: apiIncludeGlobs, // includeGlobs applied after matches
};
};
+
+export const embeddedPatternChecker = (
+ url: string,
+ scriptUrlPatterns: URLRuleEntry[],
+ isUrlMatchPattern: (s: string, m: string[]) => boolean,
+ isUrlMatchGlob: (s: string, gs: string[]) => boolean,
+ isUrlMatchRegEx: (s: string, ruleContent: [string, string]) => boolean
+): boolean => {
+ // 這個會直接轉換成Function代碼於網頁環境執行。請不要在這裡引入任何外部代碼
+ let included = false;
+ let excluded = false;
+ for (const rule of scriptUrlPatterns) {
+ switch (rule.ruleType) {
+ case RuleType.MATCH_INCLUDE:
+ included ||= isUrlMatchPattern(url, rule.ruleContent as string[]);
+ break;
+ case RuleType.MATCH_EXCLUDE:
+ excluded ||= isUrlMatchPattern(url, rule.ruleContent as string[]);
+ if (excluded) return false;
+ break;
+ case RuleType.GLOB_INCLUDE:
+ included ||= isUrlMatchGlob(url, rule.ruleContent as string[]);
+ break;
+ case RuleType.GLOB_EXCLUDE:
+ excluded ||= isUrlMatchGlob(url, rule.ruleContent as string[]);
+ if (excluded) return false;
+ break;
+ case RuleType.REGEX_INCLUDE:
+ included ||= isUrlMatchRegEx(url, rule.ruleContent as [string, string]);
+ break;
+ case RuleType.REGEX_EXCLUDE:
+ excluded ||= isUrlMatchRegEx(url, rule.ruleContent as [string, string]);
+ if (excluded) return false;
+ break;
+ }
+ }
+ return included;
+};
+
+export const embeddedPatternCheckerString = (url: string, patternArray: string): string => {
+ return `(${embeddedPatternChecker})(${url},${patternArray},${isUrlMatchPattern},${isUrlMatchGlob},${isUrlMatchRegEx})`;
+};
From f95cb8fab787a6d1ec8883b51bf7274f11f53b78 Mon Sep 17 00:00:00 2001
From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com>
Date: Sat, 7 Feb 2026 23:52:05 +0900
Subject: [PATCH 6/6] update test js
---
example/tests/unwrap_test.js | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/example/tests/unwrap_test.js b/example/tests/unwrap_test.js
index 1af8ec787..7577c53f8 100644
--- a/example/tests/unwrap_test.js
+++ b/example/tests/unwrap_test.js
@@ -4,12 +4,16 @@
// @version 2026-02-07
// @description try to take over the world!
// @author You
-// @match https://*/*?test_unwrap
+// @match https://*/*?test_unwrap*
+// @exclude /test_\w+_excluded/
// @grant GM_setValue
// @require https://cdn.jsdelivr.net/npm/jquery@3.6.0/dist/jquery.min.js#sha384-vtXRMe3mGCbOeY7l30aIg8H9p3GdeSe4IFlP6G8JMa7o7lXvnz3GFKzPxzJdPfGK
// @unwrap
// ==/UserScript==
+// include: https://example.com/?test_unwrap_123
+// exclude: https://example.com/?test_unwrap_excluded
+
var test_global_injection = "success";
// User can access the variable "test_global_injection" directly in DevTools