From 24e03b7ebf96d0c075769210f1a1f22107566820 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Thu, 12 Feb 2026 04:56:20 +0900 Subject: [PATCH 01/13] =?UTF-8?q?=E9=85=8D=E5=90=88=201.3=20scripting,=20?= =?UTF-8?q?=E9=87=8D=E6=9E=84=20`GM=5FaddElement`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/message/common.ts | 26 ++++- packages/message/custom_event_message.ts | 23 ++++- src/app/service/content/global.ts | 2 + src/app/service/content/gm_api/gm_api.ts | 126 +++++++++++++++++++---- src/app/service/content/scripting.ts | 62 +++++------ 5 files changed, 186 insertions(+), 53 deletions(-) diff --git a/packages/message/common.ts b/packages/message/common.ts index 8b00e74cf..062692933 100644 --- a/packages/message/common.ts +++ b/packages/message/common.ts @@ -14,8 +14,8 @@ export const pageDispatchEvent = performanceClone.dispatchEvent.bind(performance export const pageAddEventListener = performanceClone.addEventListener.bind(performanceClone); export const pageRemoveEventListener = performanceClone.removeEventListener.bind(performanceClone); const detailClone = typeof cloneInto === "function" ? cloneInto : null; -export const pageDispatchCustomEvent = (eventType: string, detail: any) => { - if (detailClone && detail) detail = detailClone(detail, performanceClone); +export const pageDispatchCustomEvent = (eventType: string, detail: T) => { + if (detailClone && detail) detail = detailClone(detail, performanceClone); const ev = new CustomEventClone(eventType, { detail, cancelable: true, @@ -85,3 +85,25 @@ export const createMouseEvent = : (type: string, eventInitDict?: MouseEventInit | undefined): MouseEvent => { return new MouseEventClone(type, eventInitDict); }; + +type TPrimitive = string | number | boolean; +interface INestedPrimitive { + [key: string]: TPrimitive | INestedPrimitive; +} +type TNestedPrimitive = TPrimitive | INestedPrimitive; + +export const dispatchMyEvent = >( + type: string, + eventInitDict: MouseEventInit | Omit +) => { + let resFalse; + if ("movementX" in eventInitDict) { + resFalse = pageDispatchEvent(createMouseEvent(type, eventInitDict)); + } else { + resFalse = pageDispatchCustomEvent(type, eventInitDict); + } + if (resFalse !== false && eventInitDict.cancelable === true) { + // 通讯设置正确的话应不会发生 + throw new Error("Page Message Error"); + } +}; diff --git a/packages/message/custom_event_message.ts b/packages/message/custom_event_message.ts index 5943bb632..084670db7 100644 --- a/packages/message/custom_event_message.ts +++ b/packages/message/custom_event_message.ts @@ -47,10 +47,31 @@ export class CustomEventMessage implements Message { this.receiveFlag = `${messageFlag}${isInbound ? DefinedFlags.inboundFlag : DefinedFlags.outboundFlag}${DefinedFlags.domEvent}`; this.sendFlag = `${messageFlag}${isInbound ? DefinedFlags.outboundFlag : DefinedFlags.inboundFlag}${DefinedFlags.domEvent}`; pageAddEventListener(this.receiveFlag, (event: Event) => { - if (event instanceof MouseEventClone && event.movementX === 0 && event.cancelable) { + if (event instanceof CustomEventClone && event.detail?.appendOrInsert === true) { + const id1 = event.detail?.id1 as number; + const id2 = event.detail?.id2 as number; + const id3 = event.detail?.id3 as number | undefined | null; + const el = this.getAndDelRelatedTarget(id1); + const parent = this.getAndDelRelatedTarget(id2); + const refNode = id3 ? this.getAndDelRelatedTarget(id3) : null; + const attrs = (event.detail?.attrs ?? {}) as Record; + const props = new Set(["textContent", "innerHTML", "innerText", "outerHTML", "className", "value"] as const); + for (const [key, value] of Object.entries(attrs)) { + if (props.has(key as any)) (el as any)[key] = value; + else el.setAttribute(key, value as string); + } + refNode ? parent.insertBefore(el, refNode) : parent.appendChild(el); + event.preventDefault(); + } else if (event instanceof CustomEventClone && typeof event.detail?.createElement === "string") { + const id0 = event.detail?.id0 as number; + const frag = this.getAndDelRelatedTarget(id0); + frag.appendChild(document.createElement(event.detail?.createElement)); + event.preventDefault(); + } else if (event instanceof MouseEventClone && event.movementX === 0 && event.cancelable) { event.preventDefault(); // 告知另一端这边已准备好 this.readyWrap.setReady(); // 两端已准备好,则 setReady() } else if (event instanceof MouseEventClone && event.movementX && event.relatedTarget) { + if (event.cancelable) event.preventDefault(); // 告知另一端 relatedTargetMap.set(event.movementX, event.relatedTarget); } else if (event instanceof CustomEventClone) { this.messageHandle(event.detail, new CustomEventPostMessage(this)); diff --git a/src/app/service/content/global.ts b/src/app/service/content/global.ts index e932f8c3c..5562212c6 100644 --- a/src/app/service/content/global.ts +++ b/src/app/service/content/global.ts @@ -7,6 +7,8 @@ export const Native = { structuredClone: typeof structuredClone === "function" ? structuredClone : unsupportedAPI, jsonStringify: JSON.stringify.bind(JSON), jsonParse: JSON.parse.bind(JSON), + createElement: Document.prototype.createElement, + ownFragment: new DocumentFragment(), } as const; export const customClone = (o: any) => { diff --git a/src/app/service/content/gm_api/gm_api.ts b/src/app/service/content/gm_api/gm_api.ts index 874c4c1ff..25003d00a 100644 --- a/src/app/service/content/gm_api/gm_api.ts +++ b/src/app/service/content/gm_api/gm_api.ts @@ -18,13 +18,15 @@ import GMContext from "./gm_context"; import { type ScriptRunResource } from "@App/app/repo/scripts"; import type { ValueUpdateDataEncoded } from "../types"; import { connect, sendMessage } from "@Packages/message/client"; -import { isContent } from "@Packages/message/common"; +import { dispatchMyEvent, isContent } from "@Packages/message/common"; import { getStorageName } from "@App/pkg/utils/utils"; import { ListenerManager } from "../listener_manager"; import { decodeRValue, encodeRValue, type REncoded } from "@App/pkg/utils/message_value"; import { type TGMKeyValue } from "@App/app/repo/value"; import type { ContextType } from "./gm_xhr"; import { convObjectToURL, GM_xmlhttpRequest, toBlobURL, urlToDocumentInContentPage } from "./gm_xhr"; +import { DefinedFlags } from "../../service_worker/runtime.consts"; +import { ScriptEnvTag } from "@Packages/message/consts"; // 内部函数呼叫定义 export interface IGM_Base { @@ -758,34 +760,120 @@ export default class GMApi extends GM_Base { public GM_addElement( parentNode: Node | string, tagName: string | Record, - attrs: Record = {} + attrs: Record | Node | null = {}, + refNode: Node | null = null ): Element | undefined { if (!this.message || !this.scriptRes) return; // 与content页的消息通讯实际是同步,此方法不需要经过background // 这里直接使用同步的方式去处理, 不要有promise - let parentNodeId: number | null; + // 在content脚本执行的话,与直接 DOM 无异 + // TrustedTypes 限制了对 DOM 的 innerHTML/outerHTML 的操作 (TrustedHTML) + // TrustedTypes 限制了对 script 的 innerHTML/outerHTML/textContent/innerText 的操作 (TrustedScript) + // CSP 限制了对 appendChild/insertChild/replaceChild/insertAdjacentElement ... 等DOM插入移除操作 + + // let parentNodeId: number | null; + let sParentNode: Node | null = null; if (typeof parentNode !== "string") { - const id = (this.message).sendRelatedTarget(parentNode); - parentNodeId = id; + sParentNode = parentNode as Node; + attrs = (attrs || {}) as Record; } else { - parentNodeId = null; + refNode = attrs as Node | null; attrs = (tagName || {}) as Record; tagName = parentNode as string; } - if (typeof tagName !== "string") throw new Error("The parameter 'tagName' of GM_addElement shall be a string."); - if (typeof attrs !== "object") throw new Error("The parameter 'attrs' of GM_addElement shall be an object."); - const resp = (this.message).syncSendMessage({ - action: `${this.prefix}/runtime/gmApi`, - data: { - uuid: this.scriptRes.uuid, - api: "GM_addElement", - params: [parentNodeId, tagName, attrs, isContent], - }, - }); - if (resp.code) { - throw new Error(resp.message); + + // 决定 parentNode + if (!sParentNode) { + sParentNode = document.head || document.body || document.documentElement || document.querySelector("*"); + // MV3 应该都至少有一个元素 (document.documentElement), 这个错误应该不会发生 + if (!sParentNode) throw new Error("Page Element Error"); } - return (this.message).getAndDelRelatedTarget(resp.data) as Element; + + refNode = refNode instanceof Node && refNode.parentNode === sParentNode ? refNode : null; + + // 不需要 incremental. 这个值只是在用来作一次性同步处理 + // 最小值为 1000000000 避免与其他 related Id 操作冲突 + let randInt = Math.floor(Math.random() * 1147483647 + 1000000000); // 32-bit signed int + randInt -= randInt % 100; // 用此方法可以生成不重复的 id + + const id0 = randInt; + const id1 = randInt + 1; + const id2 = randInt + 2; + let id3; + + // 目前未有直接取得 eventFlag 的方法。通过 page/content 的 receiveFlag 反推 eventFlag + const eventFlag = (this.message as CustomEventMessage).receiveFlag + .split(`${DefinedFlags.outboundFlag}${DefinedFlags.domEvent}`)[0] + .slice(0, -2); + + // content 的 receiveFlag + const ctReceiveFlag = `${eventFlag}${ScriptEnvTag.content}${DefinedFlags.outboundFlag}${DefinedFlags.domEvent}`; + + let el; + + const isNative = attrs.native === true; + if (isNative) { + // 直接使用页面的元素生成方法。某些情况例如 Custom Elements 用户可能需要直接在页面环境生成元素 + // CSP 或 TrustedTypes 目前未有对 document.createElement 做出任何限制。 + try { + el = Native.createElement.call(document, tagName as string); + } catch { + // 避免元素生成失败时无法执行。此情况应 fallback + console.warn("GM API: Native.createElement failed"); + } + } + if (!el) { + // 一般情况(非 isNative) 或 元素生成失败 (报错或回传null/undefined) + const frag = Native.ownFragment; + // 设置 fragment + dispatchMyEvent(ctReceiveFlag, { cancelable: true, movementX: id0, relatedTarget: frag }); + // 执行 createElement 并放入 fragment + dispatchMyEvent(ctReceiveFlag, { cancelable: true, createElement: `${tagName}`, id0: id0 }); + // 从 fragment 取回新增的 Element + el = frag.lastChild as Element | null; + // 如特殊情况导致无法创建元素,则报错。 + if (!el) throw new Error("GM API: createElement failed"); + } + + // 控制传送参数,避免参数出现 non-json-selizable + const attrsCT = {} as Record; + for (const [key, value] of Object.entries(attrs)) { + if (key === "native") continue; + if (typeof value === "string" || typeof value === "number") { + // 数字不是标准的 attribute value type, 但常见于实际使用 + attrsCT[key] = value; + } else { + // property setter for non attribute (e.g. Function, Symbol, boolean, etc) + // Function, Symbol 无法跨环境 + (el as any)[key] = value; + } + } + + // 设置 id1 -> el + dispatchMyEvent(ctReceiveFlag, { cancelable: true, movementX: id1, relatedTarget: el }); + + // 设置 id2 -> parentNode + dispatchMyEvent(ctReceiveFlag, { cancelable: true, movementX: id2, relatedTarget: sParentNode }); + + // 执行 attrsCT 设置并 appendChild + + if (refNode) { + id3 = randInt + 3; + // 设置 id3 -> parentNode + dispatchMyEvent(ctReceiveFlag, { cancelable: true, movementX: id3, relatedTarget: refNode }); + } + + dispatchMyEvent(ctReceiveFlag, { + cancelable: true, + appendOrInsert: true, + id1: id1, + id2: id2, + id3: id3, + attrs: attrsCT, + }); + + // 回传元素 + return el; } @GMContext.API({ depend: ["GM_addElement"] }) diff --git a/src/app/service/content/scripting.ts b/src/app/service/content/scripting.ts index 17d1b0a84..e552dcc05 100644 --- a/src/app/service/content/scripting.ts +++ b/src/app/service/content/scripting.ts @@ -95,37 +95,37 @@ export default class ScriptingRuntime { }); } case "GM_addElement": { - const [parentNodeId, tagName, tmpAttr, isContent] = data.params; - - // 根据来源选择不同的消息桥(content / inject) - const msg = isContent ? this.senderToContent : this.senderToInject; - - // 取回 parentNode(如果存在) - let parentNode: Node | undefined; - if (parentNodeId) { - parentNode = msg.getAndDelRelatedTarget(parentNodeId) as Node | undefined; - } - - // 创建元素并设置属性 - const el = document.createElement(tagName); - const attr = tmpAttr ? { ...tmpAttr } : {}; - let textContent = ""; - if (attr.textContent) { - textContent = attr.textContent; - delete attr.textContent; - } - for (const key of Object.keys(attr)) { - el.setAttribute(key, attr[key]); - } - if (textContent) el.textContent = textContent; - - // 优先挂到 parentNode,否则挂到 head/body/任意节点 - const node = parentNode || document.head || document.body || document.querySelector("*"); - node.appendChild(el); - - // 返回节点引用 id,供另一侧再取回 - const nodeId = msg.sendRelatedTarget(el); - return nodeId; + // const [parentNodeId, nodeId, tmpAttr, isContent] = data.params; + // // 根据来源选择不同的消息桥(content / inject) + // const msg = isContent ? this.senderToContent : this.senderToInject; + // // 取回 parentNode(如果存在) + // let parentNode: Node | undefined; + // if (parentNodeId) { + // parentNode = msg.getAndDelRelatedTarget(parentNodeId) as Node | undefined; + // } + // const node = msg.getAndDelRelatedTarget(nodeId) as Element | undefined; + // if (!node || !parentNode) return 0; + // // 创建元素并设置属性 + // // const el = document.createElement(tagName); + // const attr = tmpAttr ? { ...tmpAttr } : {}; + // let textContent = ""; + // if (attr.textContent) { + // textContent = attr.textContent; + // delete attr.textContent; + // } + // for (const key of Object.keys(attr)) { + // node.setAttribute(key, attr[key]); + // } + // if (textContent) node.textContent = textContent; + // parentNode?.appendChild(node); + // // 优先挂到 parentNode,否则挂到 head/body/任意节点 + // // const node = parentNode || document.head || document.body || document.querySelector("*"); + // // node.appendChild(el); + // // 返回节点引用 id,供另一侧再取回 + // // const nodeId = msg.sendRelatedTarget(el); + // // return nodeId; + // return 100; + break; } case "GM_log": // 拦截 GM_log:直接打印到控制台(某些页面可能劫持 console.log) From a9a8a9261e0a6b8041d20d956fd8c4fa4850143c Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Thu, 12 Feb 2026 19:40:47 +0900 Subject: [PATCH 02/13] Update scripting.ts --- src/app/service/content/scripting.ts | 33 ---------------------------- 1 file changed, 33 deletions(-) diff --git a/src/app/service/content/scripting.ts b/src/app/service/content/scripting.ts index e552dcc05..bd33f18d4 100644 --- a/src/app/service/content/scripting.ts +++ b/src/app/service/content/scripting.ts @@ -94,39 +94,6 @@ export default class ScriptingRuntime { xhr.send(); }); } - case "GM_addElement": { - // const [parentNodeId, nodeId, tmpAttr, isContent] = data.params; - // // 根据来源选择不同的消息桥(content / inject) - // const msg = isContent ? this.senderToContent : this.senderToInject; - // // 取回 parentNode(如果存在) - // let parentNode: Node | undefined; - // if (parentNodeId) { - // parentNode = msg.getAndDelRelatedTarget(parentNodeId) as Node | undefined; - // } - // const node = msg.getAndDelRelatedTarget(nodeId) as Element | undefined; - // if (!node || !parentNode) return 0; - // // 创建元素并设置属性 - // // const el = document.createElement(tagName); - // const attr = tmpAttr ? { ...tmpAttr } : {}; - // let textContent = ""; - // if (attr.textContent) { - // textContent = attr.textContent; - // delete attr.textContent; - // } - // for (const key of Object.keys(attr)) { - // node.setAttribute(key, attr[key]); - // } - // if (textContent) node.textContent = textContent; - // parentNode?.appendChild(node); - // // 优先挂到 parentNode,否则挂到 head/body/任意节点 - // // const node = parentNode || document.head || document.body || document.querySelector("*"); - // // node.appendChild(el); - // // 返回节点引用 id,供另一侧再取回 - // // const nodeId = msg.sendRelatedTarget(el); - // // return nodeId; - // return 100; - break; - } case "GM_log": // 拦截 GM_log:直接打印到控制台(某些页面可能劫持 console.log) switch (data.params.length) { From d499ab8a3e2096f90a3e2cb83e68c5ec338e63b2 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Thu, 12 Feb 2026 19:44:48 +0900 Subject: [PATCH 03/13] Create gm_add_element.js --- example/tests/gm_add_element.js | 106 ++++++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 example/tests/gm_add_element.js diff --git a/example/tests/gm_add_element.js b/example/tests/gm_add_element.js new file mode 100644 index 000000000..6f8499b6a --- /dev/null +++ b/example/tests/gm_add_element.js @@ -0,0 +1,106 @@ +// ==UserScript== +// @name GM_addElement test +// @match *://*/*?test_GM_addElement +// @grant GM_addElement +// @version 0 +// ==/UserScript== + +/* +### Example Sites +* https://content-security-policy.com/?test_GM_addElement (CSP) +* https://github.com/scriptscat/scriptcat/?test_GM_addElement (CSP) +* https://www.youtube.com/account_playback/?test_GM_addElement (TTP) +*/ + +const logSection = (title) => { + console.log(`\n=== ${title} ===`); +}; + +const logStep = (message, data) => { + if (data !== undefined) { + console.log(`→ ${message}:`, data); + } else { + console.log(`→ ${message}`); + } +}; + + +// ───────────────────────────────────────────── +// Native textarea insertion +// ───────────────────────────────────────────── +logSection("Native textarea insertion - BEGIN"); + +const textarea = GM_addElement('textarea', { + native: true, + value: "myText", +}); + +logStep("Textarea value", textarea.value); +logSection("Native textarea insertion - END"); + + +// ───────────────────────────────────────────── +// Div insertion +// ───────────────────────────────────────────── +logSection("Div insertion - BEGIN"); + +GM_addElement('div', { + innerHTML: '
', +}); + +logSection("Div insertion - END"); + + +// ───────────────────────────────────────────── +// Span insertion +// ───────────────────────────────────────────── +logSection("Span insertion - BEGIN"); + +GM_addElement(document.getElementById("test777"), 'span', { + className: "test777-span", + textContent: 'Hello World!', +}); + +logStep( + "Span content", + document.querySelector("span.test777-span").textContent +); + +logSection("Span insertion - END"); + + +// ───────────────────────────────────────────── +// Image insertion +// ───────────────────────────────────────────── +logSection("Image insertion - BEGIN"); + +let img; +await new Promise((resolve, reject) => { + img = GM_addElement(document.body, 'img', { + src: 'https://www.tampermonkey.net/favicon.ico', + onload: resolve, + onerror: reject + }); + + logStep("Image element inserted"); +}); + +logStep("Image loaded"); +logSection("Image insertion - END"); + + +// ───────────────────────────────────────────── +// Script insertion +// ───────────────────────────────────────────── +logSection("Script insertion - BEGIN"); + +GM_addElement(document.body, 'script', { + textContent: "window.myCustomFlag = true; console.log('script run ok');", +}, img); + +logStep( + "Script inserted before image", + img.previousSibling?.nodeName === "SCRIPT" +); + +logSection("Script insertion - END"); From d9401ba5256f5f490a70099a446d9b46969d3472 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Sun, 15 Feb 2026 17:12:13 +0900 Subject: [PATCH 04/13] Update src/app/service/content/gm_api/gm_api.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/app/service/content/gm_api/gm_api.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/service/content/gm_api/gm_api.ts b/src/app/service/content/gm_api/gm_api.ts index 25003d00a..bf8c92684 100644 --- a/src/app/service/content/gm_api/gm_api.ts +++ b/src/app/service/content/gm_api/gm_api.ts @@ -859,7 +859,7 @@ export default class GMApi extends GM_Base { if (refNode) { id3 = randInt + 3; - // 设置 id3 -> parentNode + // 设置 id3 -> refNode dispatchMyEvent(ctReceiveFlag, { cancelable: true, movementX: id3, relatedTarget: refNode }); } From ec8d59d80b8d4d325d1f4d4b90d42b8b3095788f Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Sun, 15 Feb 2026 17:14:41 +0900 Subject: [PATCH 05/13] API parameter for GM.addElement --- src/app/service/content/gm_api/gm_api.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/app/service/content/gm_api/gm_api.ts b/src/app/service/content/gm_api/gm_api.ts index bf8c92684..3c35fe30b 100644 --- a/src/app/service/content/gm_api/gm_api.ts +++ b/src/app/service/content/gm_api/gm_api.ts @@ -880,10 +880,11 @@ export default class GMApi extends GM_Base { public "GM.addElement"( parentNode: Node | string, tagName: string | Record, - attrs: Record = {} + attrs: Record | Node | null = {}, + refNode: Node | null = null ): Promise { return new Promise((resolve) => { - const ret = this.GM_addElement(parentNode, tagName, attrs); + const ret = this.GM_addElement(parentNode, tagName, attrs, refNode); resolve(ret); }); } From 4d8a1fc59571f54c902b766df2c38386c8010d7b Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Sun, 15 Feb 2026 17:18:24 +0900 Subject: [PATCH 06/13] event.detail.createElement as string --- packages/message/custom_event_message.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/message/custom_event_message.ts b/packages/message/custom_event_message.ts index 084670db7..4defa018e 100644 --- a/packages/message/custom_event_message.ts +++ b/packages/message/custom_event_message.ts @@ -65,7 +65,7 @@ export class CustomEventMessage implements Message { } else if (event instanceof CustomEventClone && typeof event.detail?.createElement === "string") { const id0 = event.detail?.id0 as number; const frag = this.getAndDelRelatedTarget(id0); - frag.appendChild(document.createElement(event.detail?.createElement)); + frag.appendChild(document.createElement(event.detail.createElement as string)); event.preventDefault(); } else if (event instanceof MouseEventClone && event.movementX === 0 && event.cancelable) { event.preventDefault(); // 告知另一端这边已准备好 From 0ec847e6d1522485ea5c5a93c4bc42197c8ae7c1 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Sun, 15 Feb 2026 17:20:43 +0900 Subject: [PATCH 07/13] if (!(frag instanceof DocumentFragment)) throw Error --- packages/message/custom_event_message.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/message/custom_event_message.ts b/packages/message/custom_event_message.ts index 4defa018e..af5986665 100644 --- a/packages/message/custom_event_message.ts +++ b/packages/message/custom_event_message.ts @@ -65,6 +65,9 @@ export class CustomEventMessage implements Message { } else if (event instanceof CustomEventClone && typeof event.detail?.createElement === "string") { const id0 = event.detail?.id0 as number; const frag = this.getAndDelRelatedTarget(id0); + if (!(frag instanceof DocumentFragment)) { + throw new Error("Unexpected Error in createElement"); + } frag.appendChild(document.createElement(event.detail.createElement as string)); event.preventDefault(); } else if (event instanceof MouseEventClone && event.movementX === 0 && event.cancelable) { From ad4d72378c7ae9d373a0d764086ff1f3ab8cc066 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Wed, 18 Feb 2026 08:03:23 +0900 Subject: [PATCH 08/13] =?UTF-8?q?=E6=B5=8B=E8=AF=95=E6=94=B9=E4=B8=80?= =?UTF-8?q?=E4=B8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- example/gm_add_element.js | 80 ++++++++++++++ ...gm_add_element.js => gm_add_element_SC.js} | 0 example/tests/gm_add_element_TM.js | 103 ++++++++++++++++++ 3 files changed, 183 insertions(+) rename example/tests/{gm_add_element.js => gm_add_element_SC.js} (100%) create mode 100644 example/tests/gm_add_element_TM.js diff --git a/example/gm_add_element.js b/example/gm_add_element.js index b71ebf5a7..aedb1d5d7 100644 --- a/example/gm_add_element.js +++ b/example/gm_add_element.js @@ -22,9 +22,89 @@ * 2. 元素标签名 * 3. 属性对象 */ + +// ------------- 基础用法(TM) B1 ---------------- + const el = GM_addElement(document.querySelector('.BorderGrid-cell'), "img", { src: "https://bbs.tampermonkey.net.cn/uc_server/avatar.php?uid=4&size=small&ts=1" }); // 打印创建出来的 DOM 元素 console.log(el); + +// ------------- 基础用法(TM) B2 - textContent ---------------- + + +const span3 = GM_addElement('span', { + textContent: 'Hello', +}); + +console.log(`span text: ${span3.textContent}`); + +// ------------- 基础用法(TM) B3 - onload & onerror ---------------- + + +new Promise((resolve, reject) => { + img = GM_addElement(document.body, 'img', { + src: 'https://www.tampermonkey.net/favicon.ico', + onload: resolve, + onerror: reject + }); +}).then(() => { + console.log("img insert ok"); +}).catch(() => { + console.log("img insert failed") +}); + + +if (GM?.info.scriptHandler === "ScriptCat") { + + // ------------- 額外用法(SC) E1 - value ---------------- + + + const textarea = GM_addElement('textarea', { + value: "myText", + }); + + console.log(`Textarea Value: ${textarea.value}`); + + // ------------- 額外用法(SC) E2 - innerHTML ---------------- + + const div3 = GM_addElement('div', { + innerHTML: '
World
', + }); + + console.log(`div text: ${div3.textContent}`); + + + // ------------- 額外用法(SC) E3 - className ---------------- + + + const span4 = GM_addElement(document.getElementById("test777"), 'span', { + className: "test777-span", + textContent: 'Hello World!', + }); + + console.log(`span class: ${span4.classList.contains("test777-span")}`) + + + + // ------------- 額外用法(SC) E4 - native ---------------- + + // 在目前环境生成元素 + + const elementA = GM_addElement('div', { + native: true, + textContent: "DEF", + }); + + + // ------------- 額外用法(SC) E5 - insertBefore ---------------- + + // 插入在某元素前面 = parentNdoe.insertBefore(node, referenceNode) + + const elementB = GM_addElement('textarea', { + value: "ABC", + }, elementA); + +} diff --git a/example/tests/gm_add_element.js b/example/tests/gm_add_element_SC.js similarity index 100% rename from example/tests/gm_add_element.js rename to example/tests/gm_add_element_SC.js diff --git a/example/tests/gm_add_element_TM.js b/example/tests/gm_add_element_TM.js new file mode 100644 index 000000000..3b7830f97 --- /dev/null +++ b/example/tests/gm_add_element_TM.js @@ -0,0 +1,103 @@ +// ==UserScript== +// @name GM_addElement test +// @match *://*/*?test_GM_addElement +// @grant GM_addElement +// @version 0 +// ==/UserScript== + +/* +### Example Sites +* https://content-security-policy.com/?test_GM_addElement (CSP) +* https://github.com/scriptscat/scriptcat/?test_GM_addElement (CSP) +* https://www.youtube.com/account_playback/?test_GM_addElement (TTP) +*/ + +const logSection = (title) => { + console.log(`\n=== ${title} ===`); +}; + +const logStep = (message, data) => { + if (data !== undefined) { + console.log(`→ ${message}:`, data); + } else { + console.log(`→ ${message}`); + } +}; + + +// ───────────────────────────────────────────── +// Native textarea insertion +// ───────────────────────────────────────────── +logSection("Native textarea insertion - BEGIN"); + +const textarea = GM_addElement('textarea', { + value: "myText", +}); + +logStep("Textarea value", textarea.value); +logSection("Native textarea insertion - END"); + + +// ───────────────────────────────────────────── +// Div insertion +// ───────────────────────────────────────────── +logSection("Div insertion - BEGIN"); + +GM_addElement('div', { + textContent: 'DIV TEXT', +}); + +logSection("Div insertion - END"); + + +// ───────────────────────────────────────────── +// Span insertion +// ───────────────────────────────────────────── +logSection("Span insertion - BEGIN"); + +GM_addElement(document.getElementById("test777"), 'span', { + textContent: 'Hello World!', +}); + +logStep( + "Span content", + document.querySelector("span.test777-span").textContent +); + +logSection("Span insertion - END"); + + +// ───────────────────────────────────────────── +// Image insertion +// ───────────────────────────────────────────── +logSection("Image insertion - BEGIN"); + +let img; +await new Promise((resolve, reject) => { + img = GM_addElement(document.body, 'img', { + src: 'https://www.tampermonkey.net/favicon.ico', + onload: resolve, + onerror: reject + }); + + logStep("Image element inserted"); +}); + +logStep("Image loaded"); +logSection("Image insertion - END"); + + +// ───────────────────────────────────────────── +// Script insertion +// ───────────────────────────────────────────── +logSection("Script insertion - BEGIN"); + +GM_addElement(document.body, 'script', { + textContent: "window.myCustomFlag = true; console.log('script run ok');", +}); + +logStep( + "Script inserted before image" +); + +logSection("Script insertion - END"); From 6606e59edf13d7e2ee852e24d9001537c1e2fefe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E4=B8=80=E4=B9=8B?= Date: Wed, 18 Feb 2026 13:17:32 +0800 Subject: [PATCH 09/13] =?UTF-8?q?=E6=95=B4=E5=90=88=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=E7=94=A8=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- example/tests/gm_api_test.js | 135 +++++++++++++++++++++-------------- 1 file changed, 82 insertions(+), 53 deletions(-) diff --git a/example/tests/gm_api_test.js b/example/tests/gm_api_test.js index af8d7fa8d..e45b7f31a 100644 --- a/example/tests/gm_api_test.js +++ b/example/tests/gm_api_test.js @@ -33,7 +33,7 @@ // @run-at document-start // ==/UserScript== -(async function () { +(function () { "use strict"; console.log("%c=== ScriptCat GM API 测试开始 ===", "color: blue; font-size: 16px; font-weight: bold;"); @@ -145,8 +145,44 @@ assert("not_found", GM_getValue("test_delete", "not_found"), "值应该被删除"); }); - // ============ GM_addValueChangeListener 测试 ============ - await (async () => { + // ============ GM_addStyle 测试 ============ + console.log("\n%c--- GM 样式 API 测试 ---", "color: orange; font-weight: bold;"); + + test("GM_addStyle - CSS字符串", () => { + const css = ` + .scriptcat-test { + color: red; + font-weight: bold; + } + `; + const element = GM_addStyle(css); + assert(true, element && element.tagName === "STYLE", "应该返回 style 元素"); + console.log("添加的样式元素:", element); + }); + + // ============ GM_getResourceText/URL 测试 ============ + console.log("\n%c--- GM 资源 API 测试 ---", "color: orange; font-weight: bold;"); + + test("GM_getResourceText", () => { + assert("function", typeof GM_getResourceText, "GM_getResourceText 应该是函数"); + + const css = GM_getResourceText("testCSS"); + assert("string", typeof css, "应该返回字符串"); + assert(163870, css.length, "资源内容长度应该是 163870"); + console.log("资源文本长度:", css.length); + }); + + test("GM_getResourceURL", () => { + assert("function", typeof GM_getResourceURL, "GM_getResourceURL 应该是函数"); + + const url = GM_getResourceURL("testCSS"); + assert("string", typeof url, "应该返回字符串"); + assert(true, url.startsWith("data:") || url.startsWith("blob:"), "应该返回 data URL 或 blob URL"); + console.log("资源 URL:", url.substring(0, 50) + "..."); + }); + + (async () => { + // ============ GM_addValueChangeListener 测试 ============ await testAsync("GM_addValueChangeListener", () => { return new Promise(async (resolve, reject) => { let listenerId = null; @@ -216,63 +252,56 @@ }, 50); }); }); - })(); - // ============ GM_addStyle 测试 ============ - console.log("\n%c--- GM 样式 API 测试 ---", "color: orange; font-weight: bold;"); + // ============ GM_addElement 测试 ============ + await testAsync("GM_addElement - 创建元素", async () => { + assert("function", typeof GM_addElement, "GM_addElement 应该是函数"); - test("GM_addStyle - CSS字符串", () => { - const css = ` - .scriptcat-test { - color: red; - font-weight: bold; - } - `; - const element = GM_addStyle(css); - assert(true, element && element.tagName === "STYLE", "应该返回 style 元素"); - console.log("添加的样式元素:", element); - }); + const div = GM_addElement("div", { + textContent: "ScriptCat GM_addElement 测试", + style: "position: fixed; top: 10px; right: 10px; background: yellow; padding: 10px; z-index: 9999;", + }); + assert(true, div && div.tagName === "DIV", "应该返回 div 元素"); + console.log("添加的元素:", div); - // ============ GM_addElement 测试 ============ - test("GM_addElement - 创建元素", () => { - assert("function", typeof GM_addElement, "GM_addElement 应该是函数"); + // 创建脚本元素测试 + const script = GM_addElement("script", { + textContent: 'window.foo = "bar";', + }); + assert(true, script && script.tagName === "SCRIPT", "应该返回 script 元素"); + assert("bar", unsafeWindow.foo, "脚本内容应该执行,unsafeWindow.foo 应该是 'bar'"); + console.log("添加的脚本元素:", script); + + document.querySelector(".container").insertBefore(script, document.querySelector(".masthead")); + + // onload 和 onerror 测试 - 插入图片元素 + let img; + await new Promise((resolve, reject) => { + img = GM_addElement(document.body, "img", { + src: "https://www.tampermonkey.net/favicon.ico", + onload: () => { + console.log("图片加载成功"); + resolve(); + }, + onerror: (error) => { + reject(new Error("图片加载失败: " + error)); + }, + }); + }); + assert(true, img && img.tagName === "IMG", "应该返回 img 元素"); + console.log("添加的图片元素:", img); - const div = GM_addElement("div", { - textContent: "ScriptCat GM_addElement 测试", - style: "position: fixed; top: 10px; right: 10px; background: yellow; padding: 10px; z-index: 9999;", + // 3秒后移除 + setTimeout(() => { + script.remove(); + div.remove(); + img.remove(); + }, 3000); }); - assert(true, div && div.tagName === "DIV", "应该返回 div 元素"); - console.log("添加的元素:", div); - - // 3秒后移除 - setTimeout(() => div.remove(), 3000); - }); - // ============ GM_getResourceText/URL 测试 ============ - console.log("\n%c--- GM 资源 API 测试 ---", "color: orange; font-weight: bold;"); + // ============ GM_xmlhttpRequest 测试 ============ + console.log("\n%c--- GM 网络请求 API 测试 ---", "color: orange; font-weight: bold;"); - test("GM_getResourceText", () => { - assert("function", typeof GM_getResourceText, "GM_getResourceText 应该是函数"); - - const css = GM_getResourceText("testCSS"); - assert("string", typeof css, "应该返回字符串"); - assert(163870, css.length, "资源内容长度应该是 163870"); - console.log("资源文本长度:", css.length); - }); - - test("GM_getResourceURL", () => { - assert("function", typeof GM_getResourceURL, "GM_getResourceURL 应该是函数"); - - const url = GM_getResourceURL("testCSS"); - assert("string", typeof url, "应该返回字符串"); - assert(true, url.startsWith("data:") || url.startsWith("blob:"), "应该返回 data URL 或 blob URL"); - console.log("资源 URL:", url.substring(0, 50) + "..."); - }); - - // ============ GM_xmlhttpRequest 测试 ============ - console.log("\n%c--- GM 网络请求 API 测试 ---", "color: orange; font-weight: bold;"); - - (async () => { await testAsync("GM_xmlhttpRequest - GET 请求", () => { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ From b97bc12e189e40532ddc5f9f8155ac14a431eaae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E4=B8=80=E4=B9=8B?= Date: Wed, 18 Feb 2026 14:25:01 +0800 Subject: [PATCH 10/13] =?UTF-8?q?=E9=87=8D=E6=9E=84=EF=BC=88=E5=88=A0?= =?UTF-8?q?=E9=99=A4=E4=BA=86native=E3=80=81insertBefore=E3=80=81=E5=92=8C?= =?UTF-8?q?=E7=89=B9=E5=AE=9A=E5=B1=9E=E6=80=A7=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/message/common.ts | 22 --- packages/message/custom_event_message.ts | 25 +--- src/app/service/content/create_context.ts | 2 + src/app/service/content/exec_script.test.ts | 22 +-- src/app/service/content/exec_script.ts | 3 +- src/app/service/content/gm_api/gm_api.test.ts | 50 +++---- src/app/service/content/gm_api/gm_api.ts | 136 +++++------------- src/app/service/content/script_executor.ts | 7 +- src/app/service/content/script_runtime.ts | 40 +++++- src/content.ts | 5 +- src/inject.ts | 4 +- 11 files changed, 128 insertions(+), 188 deletions(-) diff --git a/packages/message/common.ts b/packages/message/common.ts index 062692933..3806f53f4 100644 --- a/packages/message/common.ts +++ b/packages/message/common.ts @@ -85,25 +85,3 @@ export const createMouseEvent = : (type: string, eventInitDict?: MouseEventInit | undefined): MouseEvent => { return new MouseEventClone(type, eventInitDict); }; - -type TPrimitive = string | number | boolean; -interface INestedPrimitive { - [key: string]: TPrimitive | INestedPrimitive; -} -type TNestedPrimitive = TPrimitive | INestedPrimitive; - -export const dispatchMyEvent = >( - type: string, - eventInitDict: MouseEventInit | Omit -) => { - let resFalse; - if ("movementX" in eventInitDict) { - resFalse = pageDispatchEvent(createMouseEvent(type, eventInitDict)); - } else { - resFalse = pageDispatchCustomEvent(type, eventInitDict); - } - if (resFalse !== false && eventInitDict.cancelable === true) { - // 通讯设置正确的话应不会发生 - throw new Error("Page Message Error"); - } -}; diff --git a/packages/message/custom_event_message.ts b/packages/message/custom_event_message.ts index af5986665..32872d1e5 100644 --- a/packages/message/custom_event_message.ts +++ b/packages/message/custom_event_message.ts @@ -47,30 +47,7 @@ export class CustomEventMessage implements Message { this.receiveFlag = `${messageFlag}${isInbound ? DefinedFlags.inboundFlag : DefinedFlags.outboundFlag}${DefinedFlags.domEvent}`; this.sendFlag = `${messageFlag}${isInbound ? DefinedFlags.outboundFlag : DefinedFlags.inboundFlag}${DefinedFlags.domEvent}`; pageAddEventListener(this.receiveFlag, (event: Event) => { - if (event instanceof CustomEventClone && event.detail?.appendOrInsert === true) { - const id1 = event.detail?.id1 as number; - const id2 = event.detail?.id2 as number; - const id3 = event.detail?.id3 as number | undefined | null; - const el = this.getAndDelRelatedTarget(id1); - const parent = this.getAndDelRelatedTarget(id2); - const refNode = id3 ? this.getAndDelRelatedTarget(id3) : null; - const attrs = (event.detail?.attrs ?? {}) as Record; - const props = new Set(["textContent", "innerHTML", "innerText", "outerHTML", "className", "value"] as const); - for (const [key, value] of Object.entries(attrs)) { - if (props.has(key as any)) (el as any)[key] = value; - else el.setAttribute(key, value as string); - } - refNode ? parent.insertBefore(el, refNode) : parent.appendChild(el); - event.preventDefault(); - } else if (event instanceof CustomEventClone && typeof event.detail?.createElement === "string") { - const id0 = event.detail?.id0 as number; - const frag = this.getAndDelRelatedTarget(id0); - if (!(frag instanceof DocumentFragment)) { - throw new Error("Unexpected Error in createElement"); - } - frag.appendChild(document.createElement(event.detail.createElement as string)); - event.preventDefault(); - } else if (event instanceof MouseEventClone && event.movementX === 0 && event.cancelable) { + if (event instanceof MouseEventClone && event.movementX === 0 && event.cancelable) { event.preventDefault(); // 告知另一端这边已准备好 this.readyWrap.setReady(); // 两端已准备好,则 setReady() } else if (event instanceof MouseEventClone && event.movementX && event.relatedTarget) { diff --git a/src/app/service/content/create_context.ts b/src/app/service/content/create_context.ts index ea445e6ec..1bbe7f151 100644 --- a/src/app/service/content/create_context.ts +++ b/src/app/service/content/create_context.ts @@ -14,6 +14,7 @@ export const createContext = ( GMInfo: any, envPrefix: string, message: Message, + contentMsg: Message, scriptGrants: Set ) => { // 按照GMApi构建 @@ -31,6 +32,7 @@ export const createContext = ( const context = createGMBase({ prefix: envPrefix, message, + contentMsg, scriptRes, valueChangeListener, EE, diff --git a/src/app/service/content/exec_script.test.ts b/src/app/service/content/exec_script.test.ts index 4f927db46..8665f5721 100644 --- a/src/app/service/content/exec_script.test.ts +++ b/src/app/service/content/exec_script.test.ts @@ -29,7 +29,7 @@ const envInfo: GMInfoEnv = { }; // @ts-ignore -const noneExec = new ExecScript(scriptRes, undefined, undefined, nilFn, envInfo); +const noneExec = new ExecScript(scriptRes, undefined, undefined, undefined, nilFn, envInfo); const scriptRes2 = { id: 0, @@ -43,7 +43,7 @@ const scriptRes2 = { } as unknown as ScriptLoadInfo; // @ts-ignore -const sandboxExec = new ExecScript(scriptRes2, undefined, undefined, nilFn, envInfo); +const sandboxExec = new ExecScript(scriptRes2, undefined, undefined, undefined, nilFn, envInfo); describe.concurrent("GM_info", () => { it.concurrent("none", async () => { @@ -504,7 +504,7 @@ describe("沙盒环境测试", async () => { it.concurrent("RegExp", async () => { const script = Object.assign({}, scriptRes2) as ScriptLoadInfo; // @ts-ignore - const exec = new ExecScript(script, undefined, undefined, nilFn, envInfo); + const exec = new ExecScript(script, undefined, undefined, undefined, nilFn, envInfo); script.code = `const str = "12345"; const reg = /(123)/; return [str.match(reg), RegExp.$1];`; @@ -517,7 +517,7 @@ return [str.match(reg), RegExp.$1];`; const script = Object.assign({}, scriptRes2) as ScriptLoadInfo; script.code = `this.testVar = "ok"; ttest1 = "ok"; return {testVar: this.testVar, testVar2: this.testVar2, ttest1: typeof ttest1, ttest2: typeof ttest2};`; // @ts-ignore - const exec1 = new ExecScript(script, undefined, undefined, nilFn, envInfo); + const exec1 = new ExecScript(script, undefined, undefined, undefined, nilFn, envInfo); exec1.scriptFunc = compileScript(compileScriptCode(script)); const ret1 = await exec1.exec(); expect(ret1).toEqual({ testVar: "ok", testVar2: undefined, ttest1: "string", ttest2: "number" }); @@ -525,7 +525,7 @@ return [str.match(reg), RegExp.$1];`; const script2 = Object.assign({}, scriptRes2) as ScriptLoadInfo; script2.code = `this.testVar2 = "ok"; ttest2 = "ok"; return {testVar: this.testVar, testVar2: this.testVar2, ttest1: typeof ttest1, ttest2: typeof ttest2};`; // @ts-ignore - const exec2 = new ExecScript(script2, undefined, undefined, nilFn, envInfo); + const exec2 = new ExecScript(script2, undefined, undefined, undefined, nilFn, envInfo); exec2.scriptFunc = compileScript(compileScriptCode(script2)); const ret2 = await exec2.exec(); expect(ret2).toEqual({ testVar: undefined, testVar2: "ok", ttest1: "number", ttest2: "string" }); @@ -533,7 +533,7 @@ return [str.match(reg), RegExp.$1];`; const script3 = Object.assign({}, scriptRes2) as ScriptLoadInfo; script3.code = `onload = function (){return 123}; return {onload, thisOnload: this.onload, winOnload: window.onload};`; // @ts-ignore - const exec3 = new ExecScript(script3, undefined, undefined, nilFn, envInfo); + const exec3 = new ExecScript(script3, undefined, undefined, undefined, nilFn, envInfo); exec3.scriptFunc = compileScript(compileScriptCode(script3)); const ret3 = await exec3.exec(); expect(ret3.onload).toEqual(expect.any(Function)); @@ -546,7 +546,7 @@ return [str.match(reg), RegExp.$1];`; const script4 = Object.assign({}, scriptRes2) as ScriptLoadInfo; script4.code = `onload = function (){return 456}; return {onload, thisOnload: this.onload, winOnload: window.onload};`; // @ts-ignore - const exec4 = new ExecScript(script4, undefined, undefined, nilFn, envInfo); + const exec4 = new ExecScript(script4, undefined, undefined, undefined, nilFn, envInfo); exec4.scriptFunc = compileScript(compileScriptCode(script4)); const ret4 = await exec4.exec(); expect(ret4.onload).toEqual(expect.any(Function)); @@ -565,7 +565,7 @@ return [str.match(reg), RegExp.$1];`; const script = Object.assign({}, scriptRes2) as ScriptLoadInfo; script.code = `unsafeWindow.testSVar1 = "shareA"; ggaa1 = "ok"; return {testSVar1: unsafeWindow.testSVar1, testSVar2: unsafeWindow.testSVar2, ggaa1: typeof ggaa1, ggaa2: typeof ggaa2};`; // @ts-ignore - const exec1 = new ExecScript(script, undefined, undefined, nilFn, envInfo); + const exec1 = new ExecScript(script, undefined, undefined, undefined, nilFn, envInfo); exec1.scriptFunc = compileScript(compileScriptCode(script)); const ret1 = await exec1.exec(); expect(ret1).toEqual({ testSVar1: "shareA", testSVar2: undefined, ggaa1: "string", ggaa2: "undefined" }); @@ -573,7 +573,7 @@ return [str.match(reg), RegExp.$1];`; const script2 = Object.assign({}, scriptRes2) as ScriptLoadInfo; script2.code = `unsafeWindow.testSVar2 = "shareB"; ggaa2 = "ok"; return {testSVar1: unsafeWindow.testSVar1, testSVar2: unsafeWindow.testSVar2, ggaa1: typeof ggaa1, ggaa2: typeof ggaa2};`; // @ts-ignore - const exec2 = new ExecScript(script2, undefined, undefined, nilFn, envInfo); + const exec2 = new ExecScript(script2, undefined, undefined, undefined, nilFn, envInfo); exec2.scriptFunc = compileScript(compileScriptCode(script2)); const ret2 = await exec2.exec(); expect(ret2).toEqual({ testSVar1: "shareA", testSVar2: "shareB", ggaa1: "string", ggaa2: "string" }); @@ -583,7 +583,7 @@ return [str.match(reg), RegExp.$1];`; const script1 = Object.assign({}, scriptRes2) as ScriptLoadInfo; script1.code = `onfocus = function(){}; onresize = 123; onblur = "123"; const ret = {onfocus, onresize, onblur}; onfocus = null; onresize = null; onblur = null; return ret;`; // @ts-ignore - const exec1 = new ExecScript(script1, undefined, undefined, nilFn, envInfo); + const exec1 = new ExecScript(script1, undefined, undefined, undefined, nilFn, envInfo); exec1.scriptFunc = compileScript(compileScriptCode(script1)); const ret1 = await exec1.exec(); expect(ret1.onfocus).toEqual(expect.any(Function)); @@ -593,7 +593,7 @@ return [str.match(reg), RegExp.$1];`; const script2 = Object.assign({}, scriptRes2) as ScriptLoadInfo; script2.code = `window.onfocus = function(){}; window.onresize = 123; window.onblur = "123"; const {onfocus, onresize, onblur} = window; const ret = {onfocus, onresize, onblur}; window.onfocus = null; window.onresize = null; window.onblur = null; return ret;`; // @ts-ignore - const exec2 = new ExecScript(script2, undefined, undefined, nilFn, envInfo); + const exec2 = new ExecScript(script2, undefined, undefined, undefined, nilFn, envInfo); exec2.scriptFunc = compileScript(compileScriptCode(script2)); const ret2 = await exec2.exec(); expect(ret2.onfocus).toEqual(expect.any(Function)); diff --git a/src/app/service/content/exec_script.ts b/src/app/service/content/exec_script.ts index 4c252e5b1..4e19ed67e 100644 --- a/src/app/service/content/exec_script.ts +++ b/src/app/service/content/exec_script.ts @@ -27,6 +27,7 @@ export default class ExecScript { scriptRes: TScriptInfo, envPrefix: "scripting" | "offscreen", message: Message, + contentMsg: Message, code: string | ScriptFunc, envInfo: GMInfoEnv, globalInjection?: { [key: string]: any } // 主要是全域API. @grant none 时无效 @@ -52,7 +53,7 @@ export default class ExecScript { this.named = { GM: { info: GM_info }, GM_info }; } else { // 构建脚本GM上下文 - this.sandboxContext = createContext(scriptRes, GM_info, envPrefix, message, grantSet); + this.sandboxContext = createContext(scriptRes, GM_info, envPrefix, message, contentMsg, grantSet); if (globalInjection) { Object.assign(this.sandboxContext, globalInjection); } diff --git a/src/app/service/content/gm_api/gm_api.test.ts b/src/app/service/content/gm_api/gm_api.test.ts index 3a7b61e59..d57f4143c 100644 --- a/src/app/service/content/gm_api/gm_api.test.ts +++ b/src/app/service/content/gm_api/gm_api.test.ts @@ -45,7 +45,7 @@ describe.concurrent("@grant GM", () => { "GM_notification", ]; // @ts-ignore - const exec = new ExecScript(script, undefined, undefined, nilFn, envInfo); + const exec = new ExecScript(script, undefined, undefined, undefined, nilFn, envInfo); script.code = `return { GM_getValue: this.GM_getValue, GM_getTab: this.GM_getTab, @@ -103,7 +103,7 @@ describe.concurrent("@grant GM", () => { "GM.notification", ]; // @ts-ignore - const exec = new ExecScript(script, undefined, undefined, nilFn, envInfo); + const exec = new ExecScript(script, undefined, undefined, undefined, nilFn, envInfo); script.code = `return { ["GM.getValue"]: GM.getValue, ["GM.getTab"]: GM.getTab, @@ -153,7 +153,7 @@ describe.concurrent("window.*", () => { script.metadata.grant = ["window.close"]; script.code = `return window.close;`; // @ts-ignore - const exec = new ExecScript(script, undefined, undefined, nilFn, envInfo); + const exec = new ExecScript(script, undefined, undefined, undefined, nilFn, envInfo); exec.scriptFunc = compileScript(compileScriptCode(script)); const ret = await exec.exec(); expect(ret).toEqual(expect.any(Function)); @@ -167,7 +167,7 @@ describe.concurrent("GM Api", () => { script.metadata.grant = ["GM_getValue"]; script.code = `return GM_getValue("test");`; // @ts-ignore - const exec = new ExecScript(script, undefined, undefined, nilFn, envInfo); + const exec = new ExecScript(script, undefined, undefined, undefined, nilFn, envInfo); exec.scriptFunc = compileScript(compileScriptCode(script)); const ret = await exec.exec(); expect(ret).toEqual("ok"); @@ -178,7 +178,7 @@ describe.concurrent("GM Api", () => { script.metadata.grant = ["GM.getValue"]; script.code = `return GM.getValue("test").then(v=>v+"!");`; // @ts-ignore - const exec = new ExecScript(script, undefined, undefined, nilFn, envInfo); + const exec = new ExecScript(script, undefined, undefined, undefined, nilFn, envInfo); exec.scriptFunc = compileScript(compileScriptCode(script)); const ret = await exec.exec(); expect(ret).toEqual("ok!"); @@ -190,7 +190,7 @@ describe.concurrent("GM Api", () => { script.metadata.grant = ["GM_listValues"]; script.code = `return GM_listValues().join("-");`; // @ts-ignore - const exec = new ExecScript(script, undefined, undefined, nilFn, envInfo); + const exec = new ExecScript(script, undefined, undefined, undefined, nilFn, envInfo); exec.scriptFunc = compileScript(compileScriptCode(script)); const ret = await exec.exec(); expect(ret).toEqual("test1-test2-test3"); @@ -206,7 +206,7 @@ describe.concurrent("GM Api", () => { script.metadata.grant = ["GM_listValues"]; script.code = `return GM_listValues().join("-");`; // @ts-ignore - const exec = new ExecScript(script, undefined, undefined, nilFn, envInfo); + const exec = new ExecScript(script, undefined, undefined, undefined, nilFn, envInfo); exec.scriptFunc = compileScript(compileScriptCode(script)); const ret = await exec.exec(); expect(ret).toEqual("test5-test2-test3-test1"); // TM也没有sort @@ -218,7 +218,7 @@ describe.concurrent("GM Api", () => { script.metadata.grant = ["GM.listValues"]; script.code = `return GM.listValues().then(v=>v.join("-"));`; // @ts-ignore - const exec = new ExecScript(script, undefined, undefined, nilFn, envInfo); + const exec = new ExecScript(script, undefined, undefined, undefined, nilFn, envInfo); exec.scriptFunc = compileScript(compileScriptCode(script)); const ret = await exec.exec(); expect(ret).toEqual("test1-test2-test3"); @@ -234,7 +234,7 @@ describe.concurrent("GM Api", () => { script.metadata.grant = ["GM.listValues"]; script.code = `return GM.listValues().then(v=>v.join("-"));`; // @ts-ignore - const exec = new ExecScript(script, undefined, undefined, nilFn, envInfo); + const exec = new ExecScript(script, undefined, undefined, undefined, nilFn, envInfo); exec.scriptFunc = compileScript(compileScriptCode(script)); const ret = await exec.exec(); expect(ret).toEqual("test5-test2-test3-test1"); // TM也没有sort @@ -246,7 +246,7 @@ describe.concurrent("GM Api", () => { script.metadata.grant = ["GM_getValues"]; script.code = `return GM_getValues(["test2", "test3", "test1"]);`; // @ts-ignore - const exec = new ExecScript(script, undefined, undefined, nilFn, envInfo); + const exec = new ExecScript(script, undefined, undefined, undefined, nilFn, envInfo); exec.scriptFunc = compileScript(compileScriptCode(script)); const ret = await exec.exec(); expect(ret.test1).toEqual("23"); @@ -267,7 +267,7 @@ describe.concurrent("GM Api", () => { script.metadata.grant = ["GM.getValues"]; script.code = `return GM.getValues(["test2", "test3", "test1"]).then(v=>v);`; // @ts-ignore - const exec = new ExecScript(script, undefined, undefined, nilFn, envInfo); + const exec = new ExecScript(script, undefined, undefined, undefined, nilFn, envInfo); exec.scriptFunc = compileScript(compileScriptCode(script)); const ret = await exec.exec(); expect(ret.test1).toEqual("23"); @@ -284,7 +284,7 @@ describe.concurrent("early-script", () => { script.metadata["grant"] = ["CAT_scriptLoaded"]; script.code = `return CAT_scriptLoaded().then(()=>123);`; // @ts-ignore - const exec = new ExecScript(script, undefined, undefined, nilFn, envInfo); + const exec = new ExecScript(script, undefined, undefined, undefined, nilFn, envInfo); exec.scriptFunc = compileScript(compileScriptCode(script)); // 抛出错误 await expect(exec.exec()).rejects.toThrowError(); @@ -297,7 +297,7 @@ describe.concurrent("early-script", () => { script.metadata["grant"] = ["CAT_scriptLoaded"]; script.code = `return CAT_scriptLoaded().then(()=>123);`; // @ts-ignore - const exec = new ExecScript(script, undefined, undefined, nilFn, envInfo); + const exec = new ExecScript(script, undefined, undefined, undefined, nilFn, envInfo); exec.scriptFunc = compileScript(compileScriptCode(script)); const ret = exec.exec(); // 触发envInfo @@ -318,7 +318,7 @@ describe.concurrent("GM_menu", () => { sendMessage: mockSendMessage, } as unknown as Message; // @ts-ignore - const exec = new ExecScript(script, "scripting", mockMessage, nilFn, envInfo); + const exec = new ExecScript(script, "scripting", mockMessage, undefined, nilFn, envInfo); exec.scriptFunc = compileScript(compileScriptCode(script)); const retPromise = exec.exec(); @@ -368,7 +368,7 @@ describe.concurrent("GM_menu", () => { sendMessage: mockSendMessage, } as unknown as Message; // @ts-ignore - const exec = new ExecScript(script, "scripting", mockMessage, nilFn, envInfo); + const exec = new ExecScript(script, "content", mockMessage, undefined, nilFn, envInfo); exec.scriptFunc = compileScript(compileScriptCode(script)); const ret = exec.exec(); // 验证 sendMessage 是否被调用 @@ -390,7 +390,7 @@ describe.concurrent("GM_menu", () => { sendMessage: mockSendMessage, } as unknown as Message; // @ts-ignore - const exec = new ExecScript(script, "scripting", mockMessage, nilFn, envInfo); + const exec = new ExecScript(script, "scripting", mockMessage, undefined, nilFn, envInfo); exec.scriptFunc = compileScript(compileScriptCode(script)); const retPromise = exec.exec(); @@ -453,7 +453,7 @@ describe.concurrent("GM_menu", () => { sendMessage: mockSendMessage, } as unknown as Message; // @ts-ignore - const exec = new ExecScript(script, "scripting", mockMessage, nilFn, envInfo); + const exec = new ExecScript(script, "content", mockMessage, undefined, nilFn, envInfo); exec.scriptFunc = compileScript(compileScriptCode(script)); const ret = await exec.exec(); expect(ret).toEqual({ id1: "abc", id2: "abc", id3: 1, id4: 2, id5: "3", id6: 3, id7: 3, id8: 4 }); @@ -482,7 +482,7 @@ describe.concurrent("GM_value", () => { sendMessage: mockSendMessage, } as unknown as Message; // @ts-ignore - const exec = new ExecScript(script, "scripting", mockMessage, nilFn, envInfo); + const exec = new ExecScript(script, "scripting", mockMessage, undefined, nilFn, envInfo); exec.scriptFunc = compileScript(compileScriptCode(script)); const ret = await exec.exec(); @@ -612,7 +612,7 @@ return { value1, value2, value3, values1,values2, allValues1, allValues2, value4 sendMessage: mockSendMessage, } as unknown as Message; // @ts-ignore - const exec = new ExecScript(script, "content", mockMessage, nilFn, envInfo); + const exec = new ExecScript(script, "content", mockMessage, undefined, nilFn, envInfo); exec.scriptFunc = compileScript(compileScriptCode(script)); const ret = await exec.exec(); @@ -727,7 +727,7 @@ return { value1, value2, value3, values1,values2, allValues1, allValues2, value4 sendMessage: mockSendMessage, } as unknown as Message; // @ts-ignore - const exec = new ExecScript(script, "scripting", mockMessage, nilFn, envInfo); + const exec = new ExecScript(script, "scripting", mockMessage, undefined, nilFn, envInfo); exec.scriptFunc = compileScript(compileScriptCode(script)); const ret = await exec.exec(); @@ -850,7 +850,7 @@ return { value1, value2, value3, values1,values2, allValues1, allValues2, value4 sendMessage: mockSendMessage, } as unknown as Message; // @ts-ignore - const exec = new ExecScript(script, "scripting", mockMessage, nilFn, envInfo); + const exec = new ExecScript(script, "scripting", mockMessage, undefined, nilFn, envInfo); exec.scriptFunc = compileScript(compileScriptCode(script)); const ret = await exec.exec(); @@ -919,7 +919,7 @@ return { value1, value2, value3, values1,values2, allValues1, allValues2, value4 sendMessage: mockSendMessage, } as unknown as Message; // @ts-ignore - const exec = new ExecScript(script, "scripting", mockMessage, nilFn, envInfo); + const exec = new ExecScript(script, "scripting", mockMessage, undefined, nilFn, envInfo); exec.scriptFunc = compileScript(compileScriptCode(script)); const ret = await exec.exec(); @@ -995,7 +995,7 @@ return { value1, value2, value3, values1,values2, allValues1, allValues2, value4 sendMessage: mockSendMessage, } as unknown as Message; // @ts-ignore - const exec = new ExecScript(script, "scripting", mockMessage, nilFn, envInfo); + const exec = new ExecScript(script, "scripting", mockMessage, undefined, nilFn, envInfo); exec.scriptFunc = compileScript(compileScriptCode(script)); const retPromise = exec.exec(); expect(mockSendMessage).toHaveBeenCalledTimes(1); @@ -1029,7 +1029,7 @@ return { value1, value2, value3, values1,values2, allValues1, allValues2, value4 sendMessage: mockSendMessage, } as unknown as Message; // @ts-ignore - const exec = new ExecScript(script, "scripting", mockMessage, nilFn, envInfo); + const exec = new ExecScript(script, "scripting", mockMessage, undefined, nilFn, envInfo); exec.scriptFunc = compileScript(compileScriptCode(script)); // remote = true const retPromise = exec.exec(); @@ -1055,7 +1055,7 @@ return { value1, value2, value3, values1,values2, allValues1, allValues2, value4 sendMessage: mockSendMessage, } as unknown as Message; // @ts-ignore - const exec = new ExecScript(script, "scripting", mockMessage, nilFn, envInfo); + const exec = new ExecScript(script, "scripting", mockMessage, undefined, nilFn, envInfo); exec.scriptFunc = compileScript(compileScriptCode(script)); const retPromise = exec.exec(); diff --git a/src/app/service/content/gm_api/gm_api.ts b/src/app/service/content/gm_api/gm_api.ts index 3c35fe30b..000cf5986 100644 --- a/src/app/service/content/gm_api/gm_api.ts +++ b/src/app/service/content/gm_api/gm_api.ts @@ -18,15 +18,13 @@ import GMContext from "./gm_context"; import { type ScriptRunResource } from "@App/app/repo/scripts"; import type { ValueUpdateDataEncoded } from "../types"; import { connect, sendMessage } from "@Packages/message/client"; -import { dispatchMyEvent, isContent } from "@Packages/message/common"; +import { isContent } from "@Packages/message/common"; import { getStorageName } from "@App/pkg/utils/utils"; import { ListenerManager } from "../listener_manager"; import { decodeRValue, encodeRValue, type REncoded } from "@App/pkg/utils/message_value"; import { type TGMKeyValue } from "@App/app/repo/value"; import type { ContextType } from "./gm_xhr"; import { convObjectToURL, GM_xmlhttpRequest, toBlobURL, urlToDocumentInContentPage } from "./gm_xhr"; -import { DefinedFlags } from "../../service_worker/runtime.consts"; -import { ScriptEnvTag } from "@Packages/message/consts"; // 内部函数呼叫定义 export interface IGM_Base { @@ -71,6 +69,9 @@ class GM_Base implements IGM_Base { @GMContext.protected() protected message?: Message | null; + @GMContext.protected() + protected contentMsg!: Message; + // Extension Context 无效时释放 scriptRes @GMContext.protected() protected scriptRes?: ScriptRunResource | null; @@ -203,7 +204,8 @@ export default class GMApi extends GM_Base { constructor( public prefix: string, public message: Message | undefined, - public scriptRes: ScriptRunResource | undefined + public scriptRes: ScriptRunResource | undefined, + public contentMsg: Message ) { // testing only 仅供测试用 const valueChangeListener = new ListenerManager(); @@ -727,25 +729,22 @@ export default class GMApi extends GM_Base { if (typeof css !== "string") throw new Error("The parameter 'css' of GM_addStyle shall be a string."); // 与content页的消息通讯实际是同步,此方法不需要经过background // 这里直接使用同步的方式去处理, 不要有promise - const resp = (this.message).syncSendMessage({ - action: `${this.prefix}/runtime/gmApi`, + const resp = (this.contentMsg).syncSendMessage({ + action: `content/runtime/addElement`, data: { - uuid: this.scriptRes.uuid, - api: "GM_addElement", params: [ null, "style", { textContent: css, }, - isContent, ], }, }); if (resp.code) { throw new Error(resp.message); } - return (this.message).getAndDelRelatedTarget(resp.data) as Element; + return (this.contentMsg).getAndDelRelatedTarget(resp.data) as Element; } @GMContext.API({ depend: ["GM_addStyle"] }) @@ -760,117 +759,61 @@ export default class GMApi extends GM_Base { public GM_addElement( parentNode: Node | string, tagName: string | Record, - attrs: Record | Node | null = {}, - refNode: Node | null = null + attrs: Record | Node | null = {} ): Element | undefined { if (!this.message || !this.scriptRes) return; - // 与content页的消息通讯实际是同步,此方法不需要经过background + // 与content页的消息通讯实际是同步, 此方法不需要经过background // 这里直接使用同步的方式去处理, 不要有promise // 在content脚本执行的话,与直接 DOM 无异 // TrustedTypes 限制了对 DOM 的 innerHTML/outerHTML 的操作 (TrustedHTML) // TrustedTypes 限制了对 script 的 innerHTML/outerHTML/textContent/innerText 的操作 (TrustedScript) // CSP 限制了对 appendChild/insertChild/replaceChild/insertAdjacentElement ... 等DOM插入移除操作 - // let parentNodeId: number | null; - let sParentNode: Node | null = null; + let parentNodeId: number | null; if (typeof parentNode !== "string") { - sParentNode = parentNode as Node; - attrs = (attrs || {}) as Record; + const id = (this.contentMsg).sendRelatedTarget(parentNode); + parentNodeId = id; } else { - refNode = attrs as Node | null; + parentNodeId = null; attrs = (tagName || {}) as Record; tagName = parentNode as string; } - // 决定 parentNode - if (!sParentNode) { - sParentNode = document.head || document.body || document.documentElement || document.querySelector("*"); - // MV3 应该都至少有一个元素 (document.documentElement), 这个错误应该不会发生 - if (!sParentNode) throw new Error("Page Element Error"); - } - - refNode = refNode instanceof Node && refNode.parentNode === sParentNode ? refNode : null; - - // 不需要 incremental. 这个值只是在用来作一次性同步处理 - // 最小值为 1000000000 避免与其他 related Id 操作冲突 - let randInt = Math.floor(Math.random() * 1147483647 + 1000000000); // 32-bit signed int - randInt -= randInt % 100; // 用此方法可以生成不重复的 id - - const id0 = randInt; - const id1 = randInt + 1; - const id2 = randInt + 2; - let id3; - - // 目前未有直接取得 eventFlag 的方法。通过 page/content 的 receiveFlag 反推 eventFlag - const eventFlag = (this.message as CustomEventMessage).receiveFlag - .split(`${DefinedFlags.outboundFlag}${DefinedFlags.domEvent}`)[0] - .slice(0, -2); - - // content 的 receiveFlag - const ctReceiveFlag = `${eventFlag}${ScriptEnvTag.content}${DefinedFlags.outboundFlag}${DefinedFlags.domEvent}`; - - let el; - - const isNative = attrs.native === true; - if (isNative) { - // 直接使用页面的元素生成方法。某些情况例如 Custom Elements 用户可能需要直接在页面环境生成元素 - // CSP 或 TrustedTypes 目前未有对 document.createElement 做出任何限制。 - try { - el = Native.createElement.call(document, tagName as string); - } catch { - // 避免元素生成失败时无法执行。此情况应 fallback - console.warn("GM API: Native.createElement failed"); - } - } - if (!el) { - // 一般情况(非 isNative) 或 元素生成失败 (报错或回传null/undefined) - const frag = Native.ownFragment; - // 设置 fragment - dispatchMyEvent(ctReceiveFlag, { cancelable: true, movementX: id0, relatedTarget: frag }); - // 执行 createElement 并放入 fragment - dispatchMyEvent(ctReceiveFlag, { cancelable: true, createElement: `${tagName}`, id0: id0 }); - // 从 fragment 取回新增的 Element - el = frag.lastChild as Element | null; - // 如特殊情况导致无法创建元素,则报错。 - if (!el) throw new Error("GM API: createElement failed"); - } + if (typeof tagName !== "string") throw new Error("The parameter 'tagName' of GM_addElement shall be a string."); + if (typeof attrs !== "object") throw new Error("The parameter 'attrs' of GM_addElement shall be an object."); // 控制传送参数,避免参数出现 non-json-selizable const attrsCT = {} as Record; - for (const [key, value] of Object.entries(attrs)) { - if (key === "native") continue; + const setAttr = {} as Record; + for (const [key, value] of Object.entries(attrs as Record)) { if (typeof value === "string" || typeof value === "number") { // 数字不是标准的 attribute value type, 但常见于实际使用 attrsCT[key] = value; } else { // property setter for non attribute (e.g. Function, Symbol, boolean, etc) - // Function, Symbol 无法跨环境 - (el as any)[key] = value; + // Function, Symbol 无法跨环境传递 + setAttr[key] = value; } } - // 设置 id1 -> el - dispatchMyEvent(ctReceiveFlag, { cancelable: true, movementX: id1, relatedTarget: el }); - - // 设置 id2 -> parentNode - dispatchMyEvent(ctReceiveFlag, { cancelable: true, movementX: id2, relatedTarget: sParentNode }); - - // 执行 attrsCT 设置并 appendChild - - if (refNode) { - id3 = randInt + 3; - // 设置 id3 -> refNode - dispatchMyEvent(ctReceiveFlag, { cancelable: true, movementX: id3, relatedTarget: refNode }); + // 使用contentMsg同步发送消息到content脚本,由content脚本创建元素并返回 + // 不使用message,因为message是在scripting环境处理的,会因为扩展的 CSP 而无法操作 DOM + const resp = (this.contentMsg).syncSendMessage({ + action: `content/runtime/addElement`, + data: { + el: null, + params: [parentNodeId, tagName, attrsCT], + }, + }); + if (resp.code) { + throw new Error(resp.message); } - dispatchMyEvent(ctReceiveFlag, { - cancelable: true, - appendOrInsert: true, - id1: id1, - id2: id2, - id3: id3, - attrs: attrsCT, - }); + const el = (this.contentMsg).getAndDelRelatedTarget(resp.data) as Element; + // 设置属性 + for (const [key, value] of Object.entries(setAttr)) { + (el as any)[key] = value; + } // 回传元素 return el; @@ -880,11 +823,10 @@ export default class GMApi extends GM_Base { public "GM.addElement"( parentNode: Node | string, tagName: string | Record, - attrs: Record | Node | null = {}, - refNode: Node | null = null + attrs: Record | Node | null = {} ): Promise { return new Promise((resolve) => { - const ret = this.GM_addElement(parentNode, tagName, attrs, refNode); + const ret = this.GM_addElement(parentNode, tagName, attrs); resolve(ret); }); } diff --git a/src/app/service/content/script_executor.ts b/src/app/service/content/script_executor.ts index 21b005450..d5cce540c 100644 --- a/src/app/service/content/script_executor.ts +++ b/src/app/service/content/script_executor.ts @@ -32,7 +32,10 @@ export class ScriptExecutor { earlyScriptFlag: Set = new Set(); execScriptMap: Map = new Map(); - constructor(private msg: Message) {} + constructor( + private msg: Message, + private contentMsg: Message // 用于 content <-> content/inject 通讯 + ) {} emitEvent(data: EmitEventRequest) { // 转发给脚本 @@ -132,7 +135,7 @@ export class ScriptExecutor { execScriptEntry(scriptEntry: ExecScriptEntry) { const { scriptLoadInfo, scriptFunc, envInfo } = scriptEntry; - const execScript = new ExecScript(scriptLoadInfo, "scripting", this.msg, scriptFunc, envInfo); + const execScript = new ExecScript(scriptLoadInfo, "scripting", this.msg, this.contentMsg, scriptFunc, envInfo); this.execScriptMap.set(scriptLoadInfo.uuid, execScript); const metadata = scriptLoadInfo.metadata || {}; const resource = scriptLoadInfo.resource; diff --git a/src/app/service/content/script_runtime.ts b/src/app/service/content/script_runtime.ts index e2aa00fb4..b93aa515d 100644 --- a/src/app/service/content/script_runtime.ts +++ b/src/app/service/content/script_runtime.ts @@ -6,16 +6,52 @@ import type { EmitEventRequest } from "../service_worker/types"; import type { GMInfoEnv, ValueUpdateDataEncoded } from "./types"; import type { ScriptEnvTag } from "@Packages/message/consts"; import { onInjectPageLoaded } from "./external"; +import type { CustomEventMessage } from "@Packages/message/custom_event_message"; export class ScriptRuntime { constructor( private readonly scripEnvTag: ScriptEnvTag, private readonly server: Server, private readonly msg: Message, - private readonly scriptExecutor: ScriptExecutor, - private readonly messageFlag: string + private readonly scriptExecutor: ScriptExecutor ) {} + // content环境的特殊初始化 + contentInit() { + this.server.on("runtime/addElement", (data: { params: [number | null, string, Record | null] }) => { + const [parentNodeId, tagName, tmpAttr] = data.params; + + const msg = this.msg as CustomEventMessage; + + // 取回 parentNode(如果存在) + let parentNode: Node | undefined; + if (parentNodeId) { + parentNode = msg.getAndDelRelatedTarget(parentNodeId) as Node | undefined; + } + + // 创建元素并设置属性 + const el = document.createElement(tagName); + const attr = tmpAttr ? { ...tmpAttr } : {}; + let textContent = ""; + if (attr.textContent) { + textContent = attr.textContent; + delete attr.textContent; + } + for (const key of Object.keys(attr)) { + el.setAttribute(key, attr[key]); + } + if (textContent) el.textContent = textContent; + + // 优先挂到 parentNode,否则挂到 head/body/任意节点 + const node = parentNode || document.head || document.body || document.querySelector("*"); + node.appendChild(el); + + // 返回节点引用 id,供另一侧再取回 + const nodeId = msg.sendRelatedTarget(el); + return nodeId; + }); + } + init() { this.server.on("runtime/emitEvent", (data: EmitEventRequest) => { // 转发给脚本 diff --git a/src/content.ts b/src/content.ts index 64c84ec13..397f99dc6 100644 --- a/src/content.ts +++ b/src/content.ts @@ -25,7 +25,8 @@ getEventFlag(messageFlag, (eventFlag: string) => { logger.logger().debug("content start"); const server = new Server("content", msg); - const scriptExecutor = new ScriptExecutor(msg); - const runtime = new ScriptRuntime(scriptEnvTag, server, msg, scriptExecutor, messageFlag); + const scriptExecutor = new ScriptExecutor(msg, new CustomEventMessage(`${eventFlag}${scriptEnvTag}`, true)); + const runtime = new ScriptRuntime(scriptEnvTag, server, msg, scriptExecutor); + runtime.contentInit(); runtime.init(); }); diff --git a/src/inject.ts b/src/inject.ts index 82d391e4e..17ecaf9fd 100644 --- a/src/inject.ts +++ b/src/inject.ts @@ -25,8 +25,8 @@ getEventFlag(messageFlag, (eventFlag: string) => { logger.logger().debug("inject start"); const server = new Server("inject", msg); - const scriptExecutor = new ScriptExecutor(msg); - const runtime = new ScriptRuntime(scriptEnvTag, server, msg, scriptExecutor, messageFlag); + const scriptExecutor = new ScriptExecutor(msg, new CustomEventMessage(`${eventFlag}${ScriptEnvTag.content}`, true)); + const runtime = new ScriptRuntime(scriptEnvTag, server, msg, scriptExecutor); runtime.init(); // inject环境,直接判断白名单,注入对外接口 From ce89dd1a3a69aeafb142795d8e04cdf207ac0355 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E4=B8=80=E4=B9=8B?= Date: Wed, 18 Feb 2026 14:37:54 +0800 Subject: [PATCH 11/13] =?UTF-8?q?=E4=BC=98=E5=8C=96lint=E5=92=8C=E6=B5=8B?= =?UTF-8?q?=E8=AF=95=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/service/content/exec_script.test.ts | 99 ++++++-- src/app/service/content/exec_script.ts | 15 +- src/app/service/content/exec_warp.ts | 9 +- src/app/service/content/gm_api/gm_api.test.ts | 225 ++++++++++++++---- src/app/service/content/gm_api/gm_api.ts | 4 +- src/app/service/content/script_executor.ts | 8 +- tests/runtime/gm_api.test.ts | 6 +- 7 files changed, 281 insertions(+), 85 deletions(-) diff --git a/src/app/service/content/exec_script.test.ts b/src/app/service/content/exec_script.test.ts index 8665f5721..3b82103e7 100644 --- a/src/app/service/content/exec_script.test.ts +++ b/src/app/service/content/exec_script.test.ts @@ -28,8 +28,13 @@ const envInfo: GMInfoEnv = { isIncognito: false, }; -// @ts-ignore -const noneExec = new ExecScript(scriptRes, undefined, undefined, undefined, nilFn, envInfo); +const noneExec = new ExecScript(scriptRes, { + envPrefix: "scripting", + message: undefined as any, + contentMsg: undefined as any, + code: nilFn, + envInfo, +}); const scriptRes2 = { id: 0, @@ -42,8 +47,13 @@ const scriptRes2 = { value: {}, } as unknown as ScriptLoadInfo; -// @ts-ignore -const sandboxExec = new ExecScript(scriptRes2, undefined, undefined, undefined, nilFn, envInfo); +const sandboxExec = new ExecScript(scriptRes2, { + envPrefix: "scripting", + message: undefined as any, + contentMsg: undefined as any, + code: nilFn, + envInfo, +}); describe.concurrent("GM_info", () => { it.concurrent("none", async () => { @@ -503,8 +513,13 @@ describe("沙盒环境测试", async () => { it.concurrent("RegExp", async () => { const script = Object.assign({}, scriptRes2) as ScriptLoadInfo; - // @ts-ignore - const exec = new ExecScript(script, undefined, undefined, undefined, nilFn, envInfo); + const exec = new ExecScript(script, { + envPrefix: "scripting", + message: undefined as any, + contentMsg: undefined as any, + code: nilFn, + envInfo, + }); script.code = `const str = "12345"; const reg = /(123)/; return [str.match(reg), RegExp.$1];`; @@ -516,24 +531,39 @@ return [str.match(reg), RegExp.$1];`; it.concurrent("沙盒之间不应该共享变量", async () => { const script = Object.assign({}, scriptRes2) as ScriptLoadInfo; script.code = `this.testVar = "ok"; ttest1 = "ok"; return {testVar: this.testVar, testVar2: this.testVar2, ttest1: typeof ttest1, ttest2: typeof ttest2};`; - // @ts-ignore - const exec1 = new ExecScript(script, undefined, undefined, undefined, nilFn, envInfo); + const exec1 = new ExecScript(script, { + envPrefix: "scripting", + message: undefined as any, + contentMsg: undefined as any, + code: nilFn, + envInfo, + }); exec1.scriptFunc = compileScript(compileScriptCode(script)); const ret1 = await exec1.exec(); expect(ret1).toEqual({ testVar: "ok", testVar2: undefined, ttest1: "string", ttest2: "number" }); const script2 = Object.assign({}, scriptRes2) as ScriptLoadInfo; script2.code = `this.testVar2 = "ok"; ttest2 = "ok"; return {testVar: this.testVar, testVar2: this.testVar2, ttest1: typeof ttest1, ttest2: typeof ttest2};`; - // @ts-ignore - const exec2 = new ExecScript(script2, undefined, undefined, undefined, nilFn, envInfo); + const exec2 = new ExecScript(script2, { + envPrefix: "scripting", + message: undefined as any, + contentMsg: undefined as any, + code: nilFn, + envInfo, + }); exec2.scriptFunc = compileScript(compileScriptCode(script2)); const ret2 = await exec2.exec(); expect(ret2).toEqual({ testVar: undefined, testVar2: "ok", ttest1: "number", ttest2: "string" }); const script3 = Object.assign({}, scriptRes2) as ScriptLoadInfo; script3.code = `onload = function (){return 123}; return {onload, thisOnload: this.onload, winOnload: window.onload};`; - // @ts-ignore - const exec3 = new ExecScript(script3, undefined, undefined, undefined, nilFn, envInfo); + const exec3 = new ExecScript(script3, { + envPrefix: "scripting", + message: undefined as any, + contentMsg: undefined as any, + code: nilFn, + envInfo, + }); exec3.scriptFunc = compileScript(compileScriptCode(script3)); const ret3 = await exec3.exec(); expect(ret3.onload).toEqual(expect.any(Function)); @@ -545,8 +575,13 @@ return [str.match(reg), RegExp.$1];`; const script4 = Object.assign({}, scriptRes2) as ScriptLoadInfo; script4.code = `onload = function (){return 456}; return {onload, thisOnload: this.onload, winOnload: window.onload};`; - // @ts-ignore - const exec4 = new ExecScript(script4, undefined, undefined, undefined, nilFn, envInfo); + const exec4 = new ExecScript(script4, { + envPrefix: "scripting", + message: undefined as any, + contentMsg: undefined as any, + code: nilFn, + envInfo, + }); exec4.scriptFunc = compileScript(compileScriptCode(script4)); const ret4 = await exec4.exec(); expect(ret4.onload).toEqual(expect.any(Function)); @@ -564,16 +599,26 @@ return [str.match(reg), RegExp.$1];`; it.concurrent("沙盒之间能用unsafeWindow(及全局作用域)共享变量", async () => { const script = Object.assign({}, scriptRes2) as ScriptLoadInfo; script.code = `unsafeWindow.testSVar1 = "shareA"; ggaa1 = "ok"; return {testSVar1: unsafeWindow.testSVar1, testSVar2: unsafeWindow.testSVar2, ggaa1: typeof ggaa1, ggaa2: typeof ggaa2};`; - // @ts-ignore - const exec1 = new ExecScript(script, undefined, undefined, undefined, nilFn, envInfo); + const exec1 = new ExecScript(script, { + envPrefix: "scripting", + message: undefined as any, + contentMsg: undefined as any, + code: nilFn, + envInfo, + }); exec1.scriptFunc = compileScript(compileScriptCode(script)); const ret1 = await exec1.exec(); expect(ret1).toEqual({ testSVar1: "shareA", testSVar2: undefined, ggaa1: "string", ggaa2: "undefined" }); const script2 = Object.assign({}, scriptRes2) as ScriptLoadInfo; script2.code = `unsafeWindow.testSVar2 = "shareB"; ggaa2 = "ok"; return {testSVar1: unsafeWindow.testSVar1, testSVar2: unsafeWindow.testSVar2, ggaa1: typeof ggaa1, ggaa2: typeof ggaa2};`; - // @ts-ignore - const exec2 = new ExecScript(script2, undefined, undefined, undefined, nilFn, envInfo); + const exec2 = new ExecScript(script2, { + envPrefix: "scripting", + message: undefined as any, + contentMsg: undefined as any, + code: nilFn, + envInfo, + }); exec2.scriptFunc = compileScript(compileScriptCode(script2)); const ret2 = await exec2.exec(); expect(ret2).toEqual({ testSVar1: "shareA", testSVar2: "shareB", ggaa1: "string", ggaa2: "string" }); @@ -582,8 +627,13 @@ return [str.match(reg), RegExp.$1];`; it.concurrent("测试SC沙盒与TM沙盒有相近的特殊处理", async () => { const script1 = Object.assign({}, scriptRes2) as ScriptLoadInfo; script1.code = `onfocus = function(){}; onresize = 123; onblur = "123"; const ret = {onfocus, onresize, onblur}; onfocus = null; onresize = null; onblur = null; return ret;`; - // @ts-ignore - const exec1 = new ExecScript(script1, undefined, undefined, undefined, nilFn, envInfo); + const exec1 = new ExecScript(script1, { + envPrefix: "scripting", + message: undefined as any, + contentMsg: undefined as any, + code: nilFn, + envInfo, + }); exec1.scriptFunc = compileScript(compileScriptCode(script1)); const ret1 = await exec1.exec(); expect(ret1.onfocus).toEqual(expect.any(Function)); @@ -592,8 +642,13 @@ return [str.match(reg), RegExp.$1];`; const script2 = Object.assign({}, scriptRes2) as ScriptLoadInfo; script2.code = `window.onfocus = function(){}; window.onresize = 123; window.onblur = "123"; const {onfocus, onresize, onblur} = window; const ret = {onfocus, onresize, onblur}; window.onfocus = null; window.onresize = null; window.onblur = null; return ret;`; - // @ts-ignore - const exec2 = new ExecScript(script2, undefined, undefined, undefined, nilFn, envInfo); + const exec2 = new ExecScript(script2, { + envPrefix: "scripting", + message: undefined as any, + contentMsg: undefined as any, + code: nilFn, + envInfo, + }); exec2.scriptFunc = compileScript(compileScriptCode(script2)); const ret2 = await exec2.exec(); expect(ret2.onfocus).toEqual(expect.any(Function)); diff --git a/src/app/service/content/exec_script.ts b/src/app/service/content/exec_script.ts index 4e19ed67e..b00ca4c9d 100644 --- a/src/app/service/content/exec_script.ts +++ b/src/app/service/content/exec_script.ts @@ -25,13 +25,16 @@ export default class ExecScript { constructor( scriptRes: TScriptInfo, - envPrefix: "scripting" | "offscreen", - message: Message, - contentMsg: Message, - code: string | ScriptFunc, - envInfo: GMInfoEnv, - globalInjection?: { [key: string]: any } // 主要是全域API. @grant none 时无效 + options: { + envPrefix: string; + message: Message; + contentMsg: Message; + code: string | ScriptFunc; + envInfo: GMInfoEnv; + globalInjection?: { [key: string]: any }; // 主要是全域API. @grant none 时无效 + } ) { + const { envPrefix, message, contentMsg, code, envInfo, globalInjection } = options; this.scriptRes = scriptRes; this.logger = LoggerCore.getInstance().logger({ component: "exec", diff --git a/src/app/service/content/exec_warp.ts b/src/app/service/content/exec_warp.ts index defffa41c..a965e91bc 100644 --- a/src/app/service/content/exec_warp.ts +++ b/src/app/service/content/exec_warp.ts @@ -73,7 +73,14 @@ export class BgExecScriptWarp extends ExecScript { }, isIncognito: false, }; - super(scriptRes, "offscreen", message, scriptRes.code, envInfo, thisContext); + super(scriptRes, { + envPrefix: "offscreen", + message: message, + contentMsg: message, + code: scriptRes.code, + envInfo, + globalInjection: thisContext, + }); this.setTimeout = setTimeout; this.setInterval = setInterval; } diff --git a/src/app/service/content/gm_api/gm_api.test.ts b/src/app/service/content/gm_api/gm_api.test.ts index d57f4143c..2a76ed845 100644 --- a/src/app/service/content/gm_api/gm_api.test.ts +++ b/src/app/service/content/gm_api/gm_api.test.ts @@ -44,8 +44,13 @@ describe.concurrent("@grant GM", () => { "GM_log", "GM_notification", ]; - // @ts-ignore - const exec = new ExecScript(script, undefined, undefined, undefined, nilFn, envInfo); + const exec = new ExecScript(script, { + envPrefix: "scripting", + message: undefined as any, + contentMsg: undefined as any, + code: nilFn, + envInfo, + }); script.code = `return { GM_getValue: this.GM_getValue, GM_getTab: this.GM_getTab, @@ -102,8 +107,13 @@ describe.concurrent("@grant GM", () => { "GM.log", "GM.notification", ]; - // @ts-ignore - const exec = new ExecScript(script, undefined, undefined, undefined, nilFn, envInfo); + const exec = new ExecScript(script, { + envPrefix: "scripting", + message: undefined as any, + contentMsg: undefined as any, + code: nilFn, + envInfo, + }); script.code = `return { ["GM.getValue"]: GM.getValue, ["GM.getTab"]: GM.getTab, @@ -152,8 +162,13 @@ describe.concurrent("window.*", () => { const script = Object.assign({}, scriptRes) as ScriptLoadInfo; script.metadata.grant = ["window.close"]; script.code = `return window.close;`; - // @ts-ignore - const exec = new ExecScript(script, undefined, undefined, undefined, nilFn, envInfo); + const exec = new ExecScript(script, { + envPrefix: "scripting", + message: undefined as any, + contentMsg: undefined as any, + code: nilFn, + envInfo, + }); exec.scriptFunc = compileScript(compileScriptCode(script)); const ret = await exec.exec(); expect(ret).toEqual(expect.any(Function)); @@ -166,8 +181,13 @@ describe.concurrent("GM Api", () => { script.value = { test: "ok" }; script.metadata.grant = ["GM_getValue"]; script.code = `return GM_getValue("test");`; - // @ts-ignore - const exec = new ExecScript(script, undefined, undefined, undefined, nilFn, envInfo); + const exec = new ExecScript(script, { + envPrefix: "scripting", + message: undefined as any, + contentMsg: undefined as any, + code: nilFn, + envInfo, + }); exec.scriptFunc = compileScript(compileScriptCode(script)); const ret = await exec.exec(); expect(ret).toEqual("ok"); @@ -177,8 +197,13 @@ describe.concurrent("GM Api", () => { script.value = { test: "ok" }; script.metadata.grant = ["GM.getValue"]; script.code = `return GM.getValue("test").then(v=>v+"!");`; - // @ts-ignore - const exec = new ExecScript(script, undefined, undefined, undefined, nilFn, envInfo); + const exec = new ExecScript(script, { + envPrefix: "scripting", + message: undefined as any, + contentMsg: undefined as any, + code: nilFn, + envInfo, + }); exec.scriptFunc = compileScript(compileScriptCode(script)); const ret = await exec.exec(); expect(ret).toEqual("ok!"); @@ -189,8 +214,13 @@ describe.concurrent("GM Api", () => { script.value = { test1: "23", test2: "45", test3: "67" }; script.metadata.grant = ["GM_listValues"]; script.code = `return GM_listValues().join("-");`; - // @ts-ignore - const exec = new ExecScript(script, undefined, undefined, undefined, nilFn, envInfo); + const exec = new ExecScript(script, { + envPrefix: "scripting", + message: undefined as any, + contentMsg: undefined as any, + code: nilFn, + envInfo, + }); exec.scriptFunc = compileScript(compileScriptCode(script)); const ret = await exec.exec(); expect(ret).toEqual("test1-test2-test3"); @@ -205,8 +235,13 @@ describe.concurrent("GM Api", () => { script.value.test1 = "40"; script.metadata.grant = ["GM_listValues"]; script.code = `return GM_listValues().join("-");`; - // @ts-ignore - const exec = new ExecScript(script, undefined, undefined, undefined, nilFn, envInfo); + const exec = new ExecScript(script, { + envPrefix: "scripting", + message: undefined as any, + contentMsg: undefined as any, + code: nilFn, + envInfo, + }); exec.scriptFunc = compileScript(compileScriptCode(script)); const ret = await exec.exec(); expect(ret).toEqual("test5-test2-test3-test1"); // TM也没有sort @@ -217,8 +252,13 @@ describe.concurrent("GM Api", () => { script.value = { test1: "23", test2: "45", test3: "67" }; script.metadata.grant = ["GM.listValues"]; script.code = `return GM.listValues().then(v=>v.join("-"));`; - // @ts-ignore - const exec = new ExecScript(script, undefined, undefined, undefined, nilFn, envInfo); + const exec = new ExecScript(script, { + envPrefix: "scripting", + message: undefined as any, + contentMsg: undefined as any, + code: nilFn, + envInfo, + }); exec.scriptFunc = compileScript(compileScriptCode(script)); const ret = await exec.exec(); expect(ret).toEqual("test1-test2-test3"); @@ -233,8 +273,13 @@ describe.concurrent("GM Api", () => { script.value.test1 = "40"; script.metadata.grant = ["GM.listValues"]; script.code = `return GM.listValues().then(v=>v.join("-"));`; - // @ts-ignore - const exec = new ExecScript(script, undefined, undefined, undefined, nilFn, envInfo); + const exec = new ExecScript(script, { + envPrefix: "scripting", + message: undefined as any, + contentMsg: undefined as any, + code: nilFn, + envInfo, + }); exec.scriptFunc = compileScript(compileScriptCode(script)); const ret = await exec.exec(); expect(ret).toEqual("test5-test2-test3-test1"); // TM也没有sort @@ -245,8 +290,13 @@ describe.concurrent("GM Api", () => { script.value = { test1: "23", test2: 45, test3: "67" }; script.metadata.grant = ["GM_getValues"]; script.code = `return GM_getValues(["test2", "test3", "test1"]);`; - // @ts-ignore - const exec = new ExecScript(script, undefined, undefined, undefined, nilFn, envInfo); + const exec = new ExecScript(script, { + envPrefix: "scripting", + message: undefined as any, + contentMsg: undefined as any, + code: nilFn, + envInfo, + }); exec.scriptFunc = compileScript(compileScriptCode(script)); const ret = await exec.exec(); expect(ret.test1).toEqual("23"); @@ -266,8 +316,13 @@ describe.concurrent("GM Api", () => { script.value = { test1: "23", test2: 45, test3: "67" }; script.metadata.grant = ["GM.getValues"]; script.code = `return GM.getValues(["test2", "test3", "test1"]).then(v=>v);`; - // @ts-ignore - const exec = new ExecScript(script, undefined, undefined, undefined, nilFn, envInfo); + const exec = new ExecScript(script, { + envPrefix: "scripting", + message: undefined as any, + contentMsg: undefined as any, + code: nilFn, + envInfo, + }); exec.scriptFunc = compileScript(compileScriptCode(script)); const ret = await exec.exec(); expect(ret.test1).toEqual("23"); @@ -283,8 +338,13 @@ describe.concurrent("early-script", () => { script.metadata["early-start"] = [""]; script.metadata["grant"] = ["CAT_scriptLoaded"]; script.code = `return CAT_scriptLoaded().then(()=>123);`; - // @ts-ignore - const exec = new ExecScript(script, undefined, undefined, undefined, nilFn, envInfo); + const exec = new ExecScript(script, { + envPrefix: "scripting", + message: undefined as any, + contentMsg: undefined as any, + code: nilFn, + envInfo, + }); exec.scriptFunc = compileScript(compileScriptCode(script)); // 抛出错误 await expect(exec.exec()).rejects.toThrowError(); @@ -296,8 +356,13 @@ describe.concurrent("early-script", () => { script.metadata["run-at"] = ["document-start"]; script.metadata["grant"] = ["CAT_scriptLoaded"]; script.code = `return CAT_scriptLoaded().then(()=>123);`; - // @ts-ignore - const exec = new ExecScript(script, undefined, undefined, undefined, nilFn, envInfo); + const exec = new ExecScript(script, { + envPrefix: "scripting", + message: undefined as any, + contentMsg: undefined as any, + code: nilFn, + envInfo, + }); exec.scriptFunc = compileScript(compileScriptCode(script)); const ret = exec.exec(); // 触发envInfo @@ -317,8 +382,13 @@ describe.concurrent("GM_menu", () => { const mockMessage = { sendMessage: mockSendMessage, } as unknown as Message; - // @ts-ignore - const exec = new ExecScript(script, "scripting", mockMessage, undefined, nilFn, envInfo); + const exec = new ExecScript(script, { + envPrefix: "scripting", + message: mockMessage, + contentMsg: undefined as any, + code: nilFn, + envInfo, + }); exec.scriptFunc = compileScript(compileScriptCode(script)); const retPromise = exec.exec(); @@ -367,8 +437,13 @@ describe.concurrent("GM_menu", () => { const mockMessage = { sendMessage: mockSendMessage, } as unknown as Message; - // @ts-ignore - const exec = new ExecScript(script, "content", mockMessage, undefined, nilFn, envInfo); + const exec = new ExecScript(script, { + envPrefix: "content", + message: mockMessage, + contentMsg: undefined as any, + code: nilFn, + envInfo, + }); exec.scriptFunc = compileScript(compileScriptCode(script)); const ret = exec.exec(); // 验证 sendMessage 是否被调用 @@ -389,8 +464,13 @@ describe.concurrent("GM_menu", () => { const mockMessage = { sendMessage: mockSendMessage, } as unknown as Message; - // @ts-ignore - const exec = new ExecScript(script, "scripting", mockMessage, undefined, nilFn, envInfo); + const exec = new ExecScript(script, { + envPrefix: "scripting", + message: mockMessage, + contentMsg: undefined as any, + code: nilFn, + envInfo, + }); exec.scriptFunc = compileScript(compileScriptCode(script)); const retPromise = exec.exec(); @@ -452,8 +532,13 @@ describe.concurrent("GM_menu", () => { const mockMessage = { sendMessage: mockSendMessage, } as unknown as Message; - // @ts-ignore - const exec = new ExecScript(script, "content", mockMessage, undefined, nilFn, envInfo); + const exec = new ExecScript(script, { + envPrefix: "content", + message: mockMessage, + contentMsg: undefined as any, + code: nilFn, + envInfo, + }); exec.scriptFunc = compileScript(compileScriptCode(script)); const ret = await exec.exec(); expect(ret).toEqual({ id1: "abc", id2: "abc", id3: 1, id4: 2, id5: "3", id6: 3, id7: 3, id8: 4 }); @@ -481,8 +566,13 @@ describe.concurrent("GM_value", () => { const mockMessage = { sendMessage: mockSendMessage, } as unknown as Message; - // @ts-ignore - const exec = new ExecScript(script, "scripting", mockMessage, undefined, nilFn, envInfo); + const exec = new ExecScript(script, { + envPrefix: "scripting", + message: mockMessage, + contentMsg: undefined as any, + code: nilFn, + envInfo, + }); exec.scriptFunc = compileScript(compileScriptCode(script)); const ret = await exec.exec(); @@ -611,8 +701,13 @@ return { value1, value2, value3, values1,values2, allValues1, allValues2, value4 const mockMessage = { sendMessage: mockSendMessage, } as unknown as Message; - // @ts-ignore - const exec = new ExecScript(script, "content", mockMessage, undefined, nilFn, envInfo); + const exec = new ExecScript(script, { + envPrefix: "content", + message: mockMessage, + contentMsg: undefined as any, + code: nilFn, + envInfo, + }); exec.scriptFunc = compileScript(compileScriptCode(script)); const ret = await exec.exec(); @@ -726,8 +821,13 @@ return { value1, value2, value3, values1,values2, allValues1, allValues2, value4 const mockMessage = { sendMessage: mockSendMessage, } as unknown as Message; - // @ts-ignore - const exec = new ExecScript(script, "scripting", mockMessage, undefined, nilFn, envInfo); + const exec = new ExecScript(script, { + envPrefix: "scripting", + message: mockMessage, + contentMsg: undefined as any, + code: nilFn, + envInfo, + }); exec.scriptFunc = compileScript(compileScriptCode(script)); const ret = await exec.exec(); @@ -849,8 +949,13 @@ return { value1, value2, value3, values1,values2, allValues1, allValues2, value4 const mockMessage = { sendMessage: mockSendMessage, } as unknown as Message; - // @ts-ignore - const exec = new ExecScript(script, "scripting", mockMessage, undefined, nilFn, envInfo); + const exec = new ExecScript(script, { + envPrefix: "scripting", + message: mockMessage, + contentMsg: undefined as any, + code: nilFn, + envInfo, + }); exec.scriptFunc = compileScript(compileScriptCode(script)); const ret = await exec.exec(); @@ -918,8 +1023,13 @@ return { value1, value2, value3, values1,values2, allValues1, allValues2, value4 const mockMessage = { sendMessage: mockSendMessage, } as unknown as Message; - // @ts-ignore - const exec = new ExecScript(script, "scripting", mockMessage, undefined, nilFn, envInfo); + const exec = new ExecScript(script, { + envPrefix: "scripting", + message: mockMessage, + contentMsg: undefined as any, + code: nilFn, + envInfo, + }); exec.scriptFunc = compileScript(compileScriptCode(script)); const ret = await exec.exec(); @@ -994,8 +1104,13 @@ return { value1, value2, value3, values1,values2, allValues1, allValues2, value4 const mockMessage = { sendMessage: mockSendMessage, } as unknown as Message; - // @ts-ignore - const exec = new ExecScript(script, "scripting", mockMessage, undefined, nilFn, envInfo); + const exec = new ExecScript(script, { + envPrefix: "scripting", + message: mockMessage, + contentMsg: undefined as any, + code: nilFn, + envInfo, + }); exec.scriptFunc = compileScript(compileScriptCode(script)); const retPromise = exec.exec(); expect(mockSendMessage).toHaveBeenCalledTimes(1); @@ -1028,8 +1143,13 @@ return { value1, value2, value3, values1,values2, allValues1, allValues2, value4 const mockMessage = { sendMessage: mockSendMessage, } as unknown as Message; - // @ts-ignore - const exec = new ExecScript(script, "scripting", mockMessage, undefined, nilFn, envInfo); + const exec = new ExecScript(script, { + envPrefix: "scripting", + message: mockMessage, + contentMsg: undefined as any, + code: nilFn, + envInfo, + }); exec.scriptFunc = compileScript(compileScriptCode(script)); // remote = true const retPromise = exec.exec(); @@ -1054,8 +1174,13 @@ return { value1, value2, value3, values1,values2, allValues1, allValues2, value4 const mockMessage = { sendMessage: mockSendMessage, } as unknown as Message; - // @ts-ignore - const exec = new ExecScript(script, "scripting", mockMessage, undefined, nilFn, envInfo); + const exec = new ExecScript(script, { + envPrefix: "scripting", + message: mockMessage, + contentMsg: undefined as any, + code: nilFn, + envInfo, + }); exec.scriptFunc = compileScript(compileScriptCode(script)); const retPromise = exec.exec(); diff --git a/src/app/service/content/gm_api/gm_api.ts b/src/app/service/content/gm_api/gm_api.ts index 000cf5986..0759ea9ce 100644 --- a/src/app/service/content/gm_api/gm_api.ts +++ b/src/app/service/content/gm_api/gm_api.ts @@ -204,8 +204,8 @@ export default class GMApi extends GM_Base { constructor( public prefix: string, public message: Message | undefined, - public scriptRes: ScriptRunResource | undefined, - public contentMsg: Message + public contentMsg: Message, + public scriptRes: ScriptRunResource | undefined ) { // testing only 仅供测试用 const valueChangeListener = new ListenerManager(); diff --git a/src/app/service/content/script_executor.ts b/src/app/service/content/script_executor.ts index d5cce540c..c231213f6 100644 --- a/src/app/service/content/script_executor.ts +++ b/src/app/service/content/script_executor.ts @@ -135,7 +135,13 @@ export class ScriptExecutor { execScriptEntry(scriptEntry: ExecScriptEntry) { const { scriptLoadInfo, scriptFunc, envInfo } = scriptEntry; - const execScript = new ExecScript(scriptLoadInfo, "scripting", this.msg, this.contentMsg, scriptFunc, envInfo); + const execScript = new ExecScript(scriptLoadInfo, { + envPrefix: "scripting", + message: this.msg, + contentMsg: this.contentMsg, + code: scriptFunc, + envInfo, + }); this.execScriptMap.set(scriptLoadInfo.uuid, execScript); const metadata = scriptLoadInfo.metadata || {}; const resource = scriptLoadInfo.resource; diff --git a/tests/runtime/gm_api.test.ts b/tests/runtime/gm_api.test.ts index acfdc137c..e1840b447 100644 --- a/tests/runtime/gm_api.test.ts +++ b/tests/runtime/gm_api.test.ts @@ -135,7 +135,7 @@ describe.concurrent("测试GMApi环境 - XHR", async () => { addTestPermission(script.uuid); await new ScriptDAO().save(script); - const gmApi = new GMApi("serviceWorker", msg, { + const gmApi = new GMApi("serviceWorker", msg, undefined as any, { uuid: script.uuid, }); it.concurrent("test GM xhr - plain text", async () => { @@ -341,7 +341,7 @@ describe.concurrent("测试GMApi环境 - XHR", async () => { describe.concurrent("GM xmlHttpRequest", () => { const msg = initTestGMApi(); - const gmApi = new GMApi("serviceWorker", msg, { + const gmApi = new GMApi("serviceWorker", msg, undefined as any, { uuid: script.uuid, }); it.concurrent("get", () => { @@ -424,7 +424,7 @@ describe.concurrent("GM xmlHttpRequest", () => { describe("GM download", () => { const msg = initTestGMApi(); - const gmApi = new GMApi("serviceWorker", msg, { + const gmApi = new GMApi("serviceWorker", msg, undefined as any, { uuid: script.uuid, }); it("simple download", async () => { From b9fc495f86fa6cb8969e198a1e355c06d6800e3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E4=B8=80=E4=B9=8B?= Date: Wed, 18 Feb 2026 14:45:26 +0800 Subject: [PATCH 12/13] =?UTF-8?q?=E5=88=A0=E9=99=A4=E4=B8=8D=E5=85=BC?= =?UTF-8?q?=E5=AE=B9=E7=9A=84=E7=A4=BA=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- example/gm_add_element.js | 89 ++++--------------- example/tests/gm_add_element_SC.js | 106 ----------------------- example/tests/gm_add_element_TM.js | 103 ---------------------- src/app/service/content/gm_api/gm_api.ts | 5 +- 4 files changed, 20 insertions(+), 283 deletions(-) delete mode 100644 example/tests/gm_add_element_SC.js delete mode 100644 example/tests/gm_add_element_TM.js diff --git a/example/gm_add_element.js b/example/gm_add_element.js index aedb1d5d7..878d7ba13 100644 --- a/example/gm_add_element.js +++ b/example/gm_add_element.js @@ -23,88 +23,35 @@ * 3. 属性对象 */ -// ------------- 基础用法(TM) B1 ---------------- +// ------------- 基础用法 ---------------- -const el = GM_addElement(document.querySelector('.BorderGrid-cell'), "img", { - src: "https://bbs.tampermonkey.net.cn/uc_server/avatar.php?uid=4&size=small&ts=1" +const el = GM_addElement(document.querySelector(".BorderGrid-cell"), "img", { + src: "https://bbs.tampermonkey.net.cn/uc_server/avatar.php?uid=4&size=small&ts=1", }); // 打印创建出来的 DOM 元素 console.log(el); -// ------------- 基础用法(TM) B2 - textContent ---------------- +// ------------- 基础用法 - textContent ---------------- - -const span3 = GM_addElement('span', { - textContent: 'Hello', +const span3 = GM_addElement("span", { + textContent: "Hello", }); console.log(`span text: ${span3.textContent}`); -// ------------- 基础用法(TM) B3 - onload & onerror ---------------- - +// ------------- 基础用法 - onload & onerror ---------------- new Promise((resolve, reject) => { - img = GM_addElement(document.body, 'img', { - src: 'https://www.tampermonkey.net/favicon.ico', - onload: resolve, - onerror: reject - }); -}).then(() => { + img = GM_addElement(document.body, "img", { + src: "https://www.tampermonkey.net/favicon.ico", + onload: resolve, + onerror: reject, + }); +}) + .then(() => { console.log("img insert ok"); -}).catch(() => { - console.log("img insert failed") -}); - - -if (GM?.info.scriptHandler === "ScriptCat") { - - // ------------- 額外用法(SC) E1 - value ---------------- - - - const textarea = GM_addElement('textarea', { - value: "myText", - }); - - console.log(`Textarea Value: ${textarea.value}`); - - // ------------- 額外用法(SC) E2 - innerHTML ---------------- - - const div3 = GM_addElement('div', { - innerHTML: '
World
', - }); - - console.log(`div text: ${div3.textContent}`); - - - // ------------- 額外用法(SC) E3 - className ---------------- - - - const span4 = GM_addElement(document.getElementById("test777"), 'span', { - className: "test777-span", - textContent: 'Hello World!', - }); - - console.log(`span class: ${span4.classList.contains("test777-span")}`) - - - - // ------------- 額外用法(SC) E4 - native ---------------- - - // 在目前环境生成元素 - - const elementA = GM_addElement('div', { - native: true, - textContent: "DEF", - }); - - - // ------------- 額外用法(SC) E5 - insertBefore ---------------- - - // 插入在某元素前面 = parentNdoe.insertBefore(node, referenceNode) - - const elementB = GM_addElement('textarea', { - value: "ABC", - }, elementA); - -} + }) + .catch(() => { + console.log("img insert failed"); + }); diff --git a/example/tests/gm_add_element_SC.js b/example/tests/gm_add_element_SC.js deleted file mode 100644 index 6f8499b6a..000000000 --- a/example/tests/gm_add_element_SC.js +++ /dev/null @@ -1,106 +0,0 @@ -// ==UserScript== -// @name GM_addElement test -// @match *://*/*?test_GM_addElement -// @grant GM_addElement -// @version 0 -// ==/UserScript== - -/* -### Example Sites -* https://content-security-policy.com/?test_GM_addElement (CSP) -* https://github.com/scriptscat/scriptcat/?test_GM_addElement (CSP) -* https://www.youtube.com/account_playback/?test_GM_addElement (TTP) -*/ - -const logSection = (title) => { - console.log(`\n=== ${title} ===`); -}; - -const logStep = (message, data) => { - if (data !== undefined) { - console.log(`→ ${message}:`, data); - } else { - console.log(`→ ${message}`); - } -}; - - -// ───────────────────────────────────────────── -// Native textarea insertion -// ───────────────────────────────────────────── -logSection("Native textarea insertion - BEGIN"); - -const textarea = GM_addElement('textarea', { - native: true, - value: "myText", -}); - -logStep("Textarea value", textarea.value); -logSection("Native textarea insertion - END"); - - -// ───────────────────────────────────────────── -// Div insertion -// ───────────────────────────────────────────── -logSection("Div insertion - BEGIN"); - -GM_addElement('div', { - innerHTML: '
', -}); - -logSection("Div insertion - END"); - - -// ───────────────────────────────────────────── -// Span insertion -// ───────────────────────────────────────────── -logSection("Span insertion - BEGIN"); - -GM_addElement(document.getElementById("test777"), 'span', { - className: "test777-span", - textContent: 'Hello World!', -}); - -logStep( - "Span content", - document.querySelector("span.test777-span").textContent -); - -logSection("Span insertion - END"); - - -// ───────────────────────────────────────────── -// Image insertion -// ───────────────────────────────────────────── -logSection("Image insertion - BEGIN"); - -let img; -await new Promise((resolve, reject) => { - img = GM_addElement(document.body, 'img', { - src: 'https://www.tampermonkey.net/favicon.ico', - onload: resolve, - onerror: reject - }); - - logStep("Image element inserted"); -}); - -logStep("Image loaded"); -logSection("Image insertion - END"); - - -// ───────────────────────────────────────────── -// Script insertion -// ───────────────────────────────────────────── -logSection("Script insertion - BEGIN"); - -GM_addElement(document.body, 'script', { - textContent: "window.myCustomFlag = true; console.log('script run ok');", -}, img); - -logStep( - "Script inserted before image", - img.previousSibling?.nodeName === "SCRIPT" -); - -logSection("Script insertion - END"); diff --git a/example/tests/gm_add_element_TM.js b/example/tests/gm_add_element_TM.js deleted file mode 100644 index 3b7830f97..000000000 --- a/example/tests/gm_add_element_TM.js +++ /dev/null @@ -1,103 +0,0 @@ -// ==UserScript== -// @name GM_addElement test -// @match *://*/*?test_GM_addElement -// @grant GM_addElement -// @version 0 -// ==/UserScript== - -/* -### Example Sites -* https://content-security-policy.com/?test_GM_addElement (CSP) -* https://github.com/scriptscat/scriptcat/?test_GM_addElement (CSP) -* https://www.youtube.com/account_playback/?test_GM_addElement (TTP) -*/ - -const logSection = (title) => { - console.log(`\n=== ${title} ===`); -}; - -const logStep = (message, data) => { - if (data !== undefined) { - console.log(`→ ${message}:`, data); - } else { - console.log(`→ ${message}`); - } -}; - - -// ───────────────────────────────────────────── -// Native textarea insertion -// ───────────────────────────────────────────── -logSection("Native textarea insertion - BEGIN"); - -const textarea = GM_addElement('textarea', { - value: "myText", -}); - -logStep("Textarea value", textarea.value); -logSection("Native textarea insertion - END"); - - -// ───────────────────────────────────────────── -// Div insertion -// ───────────────────────────────────────────── -logSection("Div insertion - BEGIN"); - -GM_addElement('div', { - textContent: 'DIV TEXT', -}); - -logSection("Div insertion - END"); - - -// ───────────────────────────────────────────── -// Span insertion -// ───────────────────────────────────────────── -logSection("Span insertion - BEGIN"); - -GM_addElement(document.getElementById("test777"), 'span', { - textContent: 'Hello World!', -}); - -logStep( - "Span content", - document.querySelector("span.test777-span").textContent -); - -logSection("Span insertion - END"); - - -// ───────────────────────────────────────────── -// Image insertion -// ───────────────────────────────────────────── -logSection("Image insertion - BEGIN"); - -let img; -await new Promise((resolve, reject) => { - img = GM_addElement(document.body, 'img', { - src: 'https://www.tampermonkey.net/favicon.ico', - onload: resolve, - onerror: reject - }); - - logStep("Image element inserted"); -}); - -logStep("Image loaded"); -logSection("Image insertion - END"); - - -// ───────────────────────────────────────────── -// Script insertion -// ───────────────────────────────────────────── -logSection("Script insertion - BEGIN"); - -GM_addElement(document.body, 'script', { - textContent: "window.myCustomFlag = true; console.log('script run ok');", -}); - -logStep( - "Script inserted before image" -); - -logSection("Script insertion - END"); diff --git a/src/app/service/content/gm_api/gm_api.ts b/src/app/service/content/gm_api/gm_api.ts index 0759ea9ce..aa795026d 100644 --- a/src/app/service/content/gm_api/gm_api.ts +++ b/src/app/service/content/gm_api/gm_api.ts @@ -759,7 +759,7 @@ export default class GMApi extends GM_Base { public GM_addElement( parentNode: Node | string, tagName: string | Record, - attrs: Record | Node | null = {} + attrs: Record | null = {} ): Element | undefined { if (!this.message || !this.scriptRes) return; // 与content页的消息通讯实际是同步, 此方法不需要经过background @@ -801,7 +801,6 @@ export default class GMApi extends GM_Base { const resp = (this.contentMsg).syncSendMessage({ action: `content/runtime/addElement`, data: { - el: null, params: [parentNodeId, tagName, attrsCT], }, }); @@ -823,7 +822,7 @@ export default class GMApi extends GM_Base { public "GM.addElement"( parentNode: Node | string, tagName: string | Record, - attrs: Record | Node | null = {} + attrs: Record | null = {} ): Promise { return new Promise((resolve) => { const ret = this.GM_addElement(parentNode, tagName, attrs); From 01230aa8f6da4efb8dbac14d32116be81e4eb7b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E4=B8=80=E4=B9=8B?= Date: Wed, 18 Feb 2026 15:37:41 +0800 Subject: [PATCH 13/13] =?UTF-8?q?=E6=A0=B9=E6=8D=AEcopilot=E7=9A=84?= =?UTF-8?q?=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- example/tests/gm_api_test.js | 168 ++++++++++++----------- src/app/service/content/gm_api/gm_api.ts | 8 +- 2 files changed, 90 insertions(+), 86 deletions(-) diff --git a/example/tests/gm_api_test.js b/example/tests/gm_api_test.js index e45b7f31a..a563ea5e7 100644 --- a/example/tests/gm_api_test.js +++ b/example/tests/gm_api_test.js @@ -1,7 +1,7 @@ // ==UserScript== // @name GM API 完整测试 // @namespace https://docs.scriptcat.org/ -// @version 1.0.0 +// @version 1.1.0 // @description 全面测试ScriptCat的所有GM API功能 // @author ScriptCat // @match https://content-security-policy.com/ @@ -33,7 +33,7 @@ // @run-at document-start // ==/UserScript== -(function () { +(async function () { "use strict"; console.log("%c=== ScriptCat GM API 测试开始 ===", "color: blue; font-size: 16px; font-weight: bold;"); @@ -145,44 +145,8 @@ assert("not_found", GM_getValue("test_delete", "not_found"), "值应该被删除"); }); - // ============ GM_addStyle 测试 ============ - console.log("\n%c--- GM 样式 API 测试 ---", "color: orange; font-weight: bold;"); - - test("GM_addStyle - CSS字符串", () => { - const css = ` - .scriptcat-test { - color: red; - font-weight: bold; - } - `; - const element = GM_addStyle(css); - assert(true, element && element.tagName === "STYLE", "应该返回 style 元素"); - console.log("添加的样式元素:", element); - }); - - // ============ GM_getResourceText/URL 测试 ============ - console.log("\n%c--- GM 资源 API 测试 ---", "color: orange; font-weight: bold;"); - - test("GM_getResourceText", () => { - assert("function", typeof GM_getResourceText, "GM_getResourceText 应该是函数"); - - const css = GM_getResourceText("testCSS"); - assert("string", typeof css, "应该返回字符串"); - assert(163870, css.length, "资源内容长度应该是 163870"); - console.log("资源文本长度:", css.length); - }); - - test("GM_getResourceURL", () => { - assert("function", typeof GM_getResourceURL, "GM_getResourceURL 应该是函数"); - - const url = GM_getResourceURL("testCSS"); - assert("string", typeof url, "应该返回字符串"); - assert(true, url.startsWith("data:") || url.startsWith("blob:"), "应该返回 data URL 或 blob URL"); - console.log("资源 URL:", url.substring(0, 50) + "..."); - }); - - (async () => { - // ============ GM_addValueChangeListener 测试 ============ + // ============ GM_addValueChangeListener 测试 ============ + await (async () => { await testAsync("GM_addValueChangeListener", () => { return new Promise(async (resolve, reject) => { let listenerId = null; @@ -252,56 +216,94 @@ }, 50); }); }); + })(); - // ============ GM_addElement 测试 ============ - await testAsync("GM_addElement - 创建元素", async () => { - assert("function", typeof GM_addElement, "GM_addElement 应该是函数"); + // ============ GM_addStyle 测试 ============ + console.log("\n%c--- GM 样式 API 测试 ---", "color: orange; font-weight: bold;"); - const div = GM_addElement("div", { - textContent: "ScriptCat GM_addElement 测试", - style: "position: fixed; top: 10px; right: 10px; background: yellow; padding: 10px; z-index: 9999;", - }); - assert(true, div && div.tagName === "DIV", "应该返回 div 元素"); - console.log("添加的元素:", div); + test("GM_addStyle - CSS字符串", () => { + const css = ` + .scriptcat-test { + color: red; + font-weight: bold; + } + `; + const element = GM_addStyle(css); + assert(true, element && element.tagName === "STYLE", "应该返回 style 元素"); + console.log("添加的样式元素:", element); + }); - // 创建脚本元素测试 - const script = GM_addElement("script", { - textContent: 'window.foo = "bar";', - }); - assert(true, script && script.tagName === "SCRIPT", "应该返回 script 元素"); - assert("bar", unsafeWindow.foo, "脚本内容应该执行,unsafeWindow.foo 应该是 'bar'"); - console.log("添加的脚本元素:", script); - - document.querySelector(".container").insertBefore(script, document.querySelector(".masthead")); - - // onload 和 onerror 测试 - 插入图片元素 - let img; - await new Promise((resolve, reject) => { - img = GM_addElement(document.body, "img", { - src: "https://www.tampermonkey.net/favicon.ico", - onload: () => { - console.log("图片加载成功"); - resolve(); - }, - onerror: (error) => { - reject(new Error("图片加载失败: " + error)); - }, - }); - }); - assert(true, img && img.tagName === "IMG", "应该返回 img 元素"); - console.log("添加的图片元素:", img); + // ============ GM_addElement 测试 ============ + await testAsync("GM_addElement - 创建元素", async () => { + assert("function", typeof GM_addElement, "GM_addElement 应该是函数"); + + const div = GM_addElement("div", { + textContent: "ScriptCat GM_addElement 测试", + style: "position: fixed; top: 10px; right: 10px; background: yellow; padding: 10px; z-index: 9999;", + }); + assert(true, div && div.tagName === "DIV", "应该返回 div 元素"); + console.log("添加的元素:", div); - // 3秒后移除 - setTimeout(() => { - script.remove(); - div.remove(); - img.remove(); - }, 3000); + // 创建脚本元素测试 + const script = GM_addElement("script", { + textContent: 'window.foo = "bar";', }); + assert(true, script && script.tagName === "SCRIPT", "应该返回 script 元素"); + assert("bar", unsafeWindow.foo, "脚本内容应该执行,unsafeWindow.foo 应该是 'bar'"); + console.log("添加的脚本元素:", script); - // ============ GM_xmlhttpRequest 测试 ============ - console.log("\n%c--- GM 网络请求 API 测试 ---", "color: orange; font-weight: bold;"); + document.querySelector(".container").insertBefore(script, document.querySelector(".masthead")); + // onload 和 onerror 测试 - 插入图片元素 + let img; + await new Promise((resolve, reject) => { + img = GM_addElement(document.body, "img", { + src: "https://www.tampermonkey.net/favicon.ico", + onload: () => { + console.log("图片加载成功"); + resolve(); + }, + onerror: (error) => { + reject(new Error("图片加载失败: " + error)); + }, + }); + }); + assert(true, img && img.tagName === "IMG", "应该返回 img 元素"); + console.log("添加的图片元素:", img); + + // 3秒后移除 + setTimeout(() => { + script.remove(); + div.remove(); + img.remove(); + }, 3000); + }); + + // ============ GM_getResourceText/URL 测试 ============ + console.log("\n%c--- GM 资源 API 测试 ---", "color: orange; font-weight: bold;"); + + test("GM_getResourceText", () => { + assert("function", typeof GM_getResourceText, "GM_getResourceText 应该是函数"); + + const css = GM_getResourceText("testCSS"); + assert("string", typeof css, "应该返回字符串"); + assert(163870, css.length, "资源内容长度应该是 163870"); + console.log("资源文本长度:", css.length); + }); + + test("GM_getResourceURL", () => { + assert("function", typeof GM_getResourceURL, "GM_getResourceURL 应该是函数"); + + const url = GM_getResourceURL("testCSS"); + assert("string", typeof url, "应该返回字符串"); + assert(true, url.startsWith("data:") || url.startsWith("blob:"), "应该返回 data URL 或 blob URL"); + console.log("资源 URL:", url.substring(0, 50) + "..."); + }); + + // ============ GM_xmlhttpRequest 测试 ============ + console.log("\n%c--- GM 网络请求 API 测试 ---", "color: orange; font-weight: bold;"); + + (async () => { await testAsync("GM_xmlhttpRequest - GET 请求", () => { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ diff --git a/src/app/service/content/gm_api/gm_api.ts b/src/app/service/content/gm_api/gm_api.ts index aa795026d..61ff146be 100644 --- a/src/app/service/content/gm_api/gm_api.ts +++ b/src/app/service/content/gm_api/gm_api.ts @@ -203,9 +203,9 @@ export default class GMApi extends GM_Base { constructor( public prefix: string, - public message: Message | undefined, + public message: Message, public contentMsg: Message, - public scriptRes: ScriptRunResource | undefined + public scriptRes: ScriptRunResource ) { // testing only 仅供测试用 const valueChangeListener = new ListenerManager(); @@ -780,7 +780,9 @@ export default class GMApi extends GM_Base { } if (typeof tagName !== "string") throw new Error("The parameter 'tagName' of GM_addElement shall be a string."); - if (typeof attrs !== "object") throw new Error("The parameter 'attrs' of GM_addElement shall be an object."); + if (attrs !== null && typeof attrs !== "object") { + throw new Error("The parameter 'attrs' of GM_addElement shall be an object."); + } // 控制传送参数,避免参数出现 non-json-selizable const attrsCT = {} as Record;