Skip to content
106 changes: 106 additions & 0 deletions example/tests/gm_add_element.js
Original file line number Diff line number Diff line change
@@ -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: '<div id="test777"></div>',
});

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");
26 changes: 24 additions & 2 deletions packages/message/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = <T = any>(eventType: string, detail: T) => {
if (detailClone && detail) detail = <T>detailClone(detail, performanceClone);
const ev = new CustomEventClone(eventType, {
detail,
cancelable: true,
Expand Down Expand Up @@ -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 = <T extends Record<string, TNestedPrimitive>>(
type: string,
eventInitDict: MouseEventInit | Omit<T, "movementX" | "relatedTarget">
) => {
Comment on lines +95 to +98
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

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

dispatchMyEvent 的类型定义不够严格。Omit<T, "movementX" | "relatedTarget"> 允许 eventInitDict 包含这些字段,但实际检查是使用 "in" 操作符。这可能导致类型安全问题。建议使用更严格的类型约束,或者在运行时添加更明确的验证。

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

Choose a reason for hiding this comment

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

typescript 要用 in 呀

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");
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

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

dispatchMyEvent 函数对于失败的情况(resFalse !== false && cancelable === true)会抛出通用错误 "Page Message Error"。这个错误消息不够具体,无法帮助开发者诊断问题。建议提供更详细的错误信息,包括 event type 和可能的失败原因。

Suggested change
throw new Error("Page Message Error");
let eventInitDebug = "";
try {
eventInitDebug = JSON.stringify(eventInitDict);
} catch {
eventInitDebug = "[unserializable eventInitDict]";
}
throw new Error(
`Page Message Error: dispatchMyEvent expected event "${type}" (cancelable: true) to be canceled, but dispatch returned ${String(
resFalse
)}. eventInitDict: ${eventInitDebug}`
);

Copilot uses AI. Check for mistakes.
}
};
26 changes: 25 additions & 1 deletion packages/message/custom_event_message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,34 @@ 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 = <Element>this.getAndDelRelatedTarget(id1);
const parent = <Node>this.getAndDelRelatedTarget(id2);
const refNode = id3 ? <Node>this.getAndDelRelatedTarget(id3) : null;
const attrs = (event.detail?.attrs ?? {}) as Record<string, string | number>;
const props = new Set(["textContent", "innerHTML", "innerText", "outerHTML", "className", "value"] as const);
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

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

属性设置逻辑中的 props Set 包含了 "outerHTML",但设置 outerHTML 会替换整个元素,这可能导致刚创建的元素被替换掉,后续的 appendChild/insertBefore 操作会失败。建议从 props 中移除 "outerHTML",或者在文档中明确说明此行为。

Suggested change
const props = new Set(["textContent", "innerHTML", "innerText", "outerHTML", "className", "value"] as const);
const props = new Set(["textContent", "innerHTML", "innerText", "className", "value"] as const);

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

Choose a reason for hiding this comment

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

改了 outerHTML 还是用一个element呀

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 = <DocumentFragment>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) {
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));
Expand Down
2 changes: 2 additions & 0 deletions src/app/service/content/global.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

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

使用单例 DocumentFragment(ownFragment)会导致并发问题。当多个 GM_addElement 调用同时执行时,它们会共享同一个 fragment,导致元素互相覆盖。应该移除这个单例,改为在需要时创建新的 DocumentFragment 实例。

Suggested change
ownFragment: new DocumentFragment(),
get ownFragment() {
return new DocumentFragment();
},

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

Choose a reason for hiding this comment

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

同步操作。不会有这情况

} as const;

export const customClone = (o: any) => {
Expand Down
131 changes: 110 additions & 21 deletions src/app/service/content/gm_api/gm_api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -758,44 +760,131 @@ export default class GMApi extends GM_Base {
public GM_addElement(
parentNode: Node | string,
tagName: string | Record<string, string | number | boolean>,
attrs: Record<string, string | number | boolean> = {}
attrs: Record<string, string | number | boolean> | Node | null = {},
refNode: Node | null = null
): Element | undefined {
Comment on lines 760 to 765
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

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

TypeScript 类型定义需要更新以支持新的 API 签名。当前的类型定义缺少:1) 第四个参数 refNode 用于 insertBefore 功能;2) attrs 中的 native 选项;3) 新支持的属性如 innerHTML、innerText、outerHTML、className、value。建议更新 src/types/scriptcat.d.ts 文件。

Copilot uses AI. Check for mistakes.
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 = (<CustomEventMessage>this.message).sendRelatedTarget(parentNode);
parentNodeId = id;
sParentNode = parentNode as Node;
attrs = (attrs || {}) as Record<string, string | number | boolean>;
} else {
parentNodeId = null;
refNode = attrs as Node | null;
attrs = (tagName || {}) as Record<string, string | number | boolean>;
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 = (<CustomEventMessage>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 (<CustomEventMessage>this.message).getAndDelRelatedTarget(resp.data) as Element;

refNode = refNode instanceof Node && refNode.parentNode === sParentNode ? refNode : null;
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

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

refNode 验证逻辑(refNode.parentNode === sParentNode)会在 refNode 还未插入到 DOM 时将其设为 null。这限制了 API 的灵活性,因为用户可能想在元素插入前就指定插入位置。建议移除此验证,让 insertBefore 在 refNode 不在 parent 中时自然失败,或者提供更清晰的错误消息。

Suggested change
refNode = refNode instanceof Node && refNode.parentNode === sParentNode ? refNode : null;
// 仅在 refNode 为合法 DOM 节点时保留,其是否属于 sParentNode 交由后续 DOM 操作自行校验
refNode = refNode instanceof Node ? refNode : null;

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

Choose a reason for hiding this comment

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

暂不考虑。日后再算


// 不需要 incremental. 这个值只是在用来作一次性同步处理
// 最小值为 1000000000 避免与其他 related Id 操作冲突
let randInt = Math.floor(Math.random() * 1147483647 + 1000000000); // 32-bit signed int
Comment on lines +795 to +796
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

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

代码注释中提到 "32-bit signed int",但 JavaScript 的 Number 类型是 64-bit 浮点数。Math.random() * 1147483647 的结果可以精确表示,但注释可能会误导读者。建议澄清这是为了避免溢出还是其他原因。

Suggested change
// 最小值为 1000000000 避免与其他 related Id 操作冲突
let randInt = Math.floor(Math.random() * 1147483647 + 1000000000); // 32-bit signed int
// 在 10^9 ~ 2.1×10^9 区间内生成一次性随机 ID,用于与其他 related Id 的数值空间错开
let randInt = Math.floor(Math.random() * 1147483647 + 1000000000);

Copilot uses AI. Check for mistakes.
randInt -= randInt % 100; // 用此方法可以生成不重复的 id
Comment on lines +794 to +797
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

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

随机 ID 生成存在潜在的冲突风险。虽然使用了大范围(1000000000 到 2147483647)并取整到 100 的倍数,但在高并发场景下仍可能产生冲突。建议使用递增的 ID 生成器或添加冲突检测机制。

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

Choose a reason for hiding this comment

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

只是一次性用途。不需要避免冲突


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);
Comment on lines +804 to +807
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

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

通过字符串操作(split 和 slice)从 receiveFlag 反推 eventFlag 是脆弱的实现。如果 DefinedFlags 的格式发生变化,这段代码会静默失败。建议提供一个明确的方法来获取 eventFlag,或者在 CustomEventMessage 中存储 eventFlag 以便直接访问。

Copilot uses AI. Check for mistakes.

// 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 = <Element>Native.createElement.call(document, tagName as string);
} catch {
// 避免元素生成失败时无法执行。此情况应 fallback
console.warn("GM API: Native.createElement failed");
}
}
if (!el) {
// 一般情况(非 isNative) 或 元素生成失败 (报错或回传null/undefined)
Comment on lines +820 to +826
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

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

在 isNative 模式下,如果 Native.createElement 失败(抛出异常或返回 null/undefined),代码会 fallback 到 content 环境创建元素。但这可能违背用户的预期 - 如果用户明确指定了 native: true,可能是因为需要在页面环境创建特殊元素(如 Custom Elements)。Fallback 到 content 环境可能导致功能异常。建议在 isNative 失败时直接抛出错误,或至少记录更明确的警告信息。

Suggested change
} catch {
// 避免元素生成失败时无法执行。此情况应 fallback
console.warn("GM API: Native.createElement failed");
}
}
if (!el) {
// 一般情况(非 isNative) 或 元素生成失败 (报错或回传null/undefined)
} catch (err) {
// 在 native 模式下元素创建失败时,不应静默 fallback 到 content,以免违背用户预期
console.error("GM API: Native.createElement failed in native mode", err);
throw new Error("GM API: Native.createElement failed in native mode");
}
// Native.createElement 未抛异常但返回了 null/undefined,同样视为 native 模式下的致命错误
if (!el) {
console.error("GM API: Native.createElement returned no element in native mode");
throw new Error("GM API: Native.createElement returned no element in native mode");
}
} else {
// 一般情况(非 isNative) 使用 content 环境创建元素

Copilot uses AI. Check for mistakes.
const frag = Native.ownFragment;
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

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

Native.ownFragment 使用单例 DocumentFragment,这在多次调用 GM_addElement 时会导致问题。当一个元素被 appendChild 到 fragment 后,后续调用会共享同一个 fragment,可能导致元素被意外移除或覆盖。应该为每次调用创建新的 DocumentFragment 实例。

Suggested change
const frag = Native.ownFragment;
const frag = document.createDocumentFragment();

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

Choose a reason for hiding this comment

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

同步操作。不会有这情况

// 设置 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");
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

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

在元素创建失败时抛出的错误消息 "GM API: createElement failed" 不够具体。建议包含更多上下文信息,例如标签名称和失败原因,以便用户调试。

Copilot uses AI. Check for mistakes.
}

// 控制传送参数,避免参数出现 non-json-selizable
const attrsCT = {} as Record<string, string | number>;
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 无法跨环境
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

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

属性处理逻辑存在安全风险:对于非字符串/数字的值(如 Function、Symbol、boolean 等),代码直接设置为元素的属性(line 848: (el as any)[key] = value)。这可能导致原型污染或其他安全问题。建议添加白名单验证,只允许已知安全的属性名称。

Suggested change
// Function, Symbol 无法跨环境
// Function, Symbol 无法跨环境
// 为避免原型污染等风险,这里显式跳过若干危险属性名
if (key === "__proto__" || key === "prototype" || key === "constructor") {
// 记录一条警告,方便开发者排查潜在问题
console.warn("GM API: unsafe property key ignored on element:", key);
continue;
}

Copilot uses AI. Check for mistakes.
(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 -> refNode
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"] })
public "GM.addElement"(
parentNode: Node | string,
tagName: string | Record<string, string | number | boolean>,
attrs: Record<string, string | number | boolean> = {}
attrs: Record<string, string | number | boolean> | Node | null = {},
refNode: Node | null = null
): Promise<Element | undefined> {
return new Promise<Element | undefined>((resolve) => {
const ret = this.GM_addElement(parentNode, tagName, attrs);
const ret = this.GM_addElement(parentNode, tagName, attrs, refNode);
resolve(ret);
});
}
Expand Down
33 changes: 0 additions & 33 deletions src/app/service/content/scripting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,39 +95,6 @@ export default class ScriptingRuntime {
xhr.send();
});
}
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 = <Element>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;
}
case "GM_log":
// 拦截 GM_log:直接打印到控制台(某些页面可能劫持 console.log)
switch (data.params.length) {
Expand Down
Loading