Skip to content

[v1.3] 配合 1.3 scripting, 重构 GM_addElement (bug 修补 + 功能改进)#1233

Open
cyfung1031 wants to merge 8 commits intoscriptscat:release/v1.3from
cyfung1031:pr-fix-addElement_bug-301
Open

[v1.3] 配合 1.3 scripting, 重构 GM_addElement (bug 修补 + 功能改进)#1233
cyfung1031 wants to merge 8 commits intoscriptscat:release/v1.3from
cyfung1031:pr-fix-addElement_bug-301

Conversation

@cyfung1031
Copy link
Collaborator

@cyfung1031 cyfung1031 commented Feb 11, 2026

事原

  • 1.3 scripting - scripting.js 不能在 CSP TTP 插入元素执行代码
  • GM_addElement - 最近支持了 onload onerror 等 function value

改善

  • 统一以 content.js 处理 CSP插入元素
  • 【新功能】可使用 content.js 或 inject.js 创造元素 ( native: true )
  • 【新功能】 追加多一个 parameter 让 GM_addElement 可以像 insertBefore 一样插在特定位置
  • 文字 / 数字 均使用 content.js 设置 (避开 TTP )
  • 除了 textContent, 还支持了 innerHTML, outerHTML, innerText
  • 针对 input element 常有的 value,也支持了
  • 针对 className, 也支持了
  • Function 等使用 页面环境 - inject.js / content.js (视乎 @inject-into 有否指定了 content)
  • 设计上兼容了在 content环境 (@inject-into content) 执行 ,即 自己呼叫自己

测试环境

  • github.com (CSP)
  • content-security-policy.com (CSP)
  • youtube.com (TTP)
Screenshot 2026-02-12 at 12 23 40

Test

// ==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");

@cyfung1031 cyfung1031 changed the title 配合 1.3 scripting, 重构 GM_addElement [v1.3] 配合 1.3 scripting, 重构 GM_addElement (bug 修补 + 功能改进) Feb 12, 2026
@cyfung1031 cyfung1031 linked an issue Feb 12, 2026 that may be closed by this pull request
@cyfung1031 cyfung1031 added enhancement New feature or request hotfix 需要尽快更新到扩展商店 labels Feb 12, 2026
@cyfung1031 cyfung1031 marked this pull request as ready for review February 12, 2026 10:41
@CodFrm
Copy link
Member

CodFrm commented Feb 12, 2026

1.3 scripting - scripting.js 不能在CSP插入元素

我看没问题啊

@cyfung1031

This comment was marked as outdated.

@cyfung1031
Copy link
Collaborator Author

cyfung1031 commented Feb 12, 2026

1.3 scripting - scripting.js 不能在CSP插入元素

我看没问题啊

呀。我打錯&記錯了
是TTP

// ==UserScript==
// @name         New Userscript MWNB-1
// @namespace    https://docs.scriptcat.org/
// @version      0.1.0
// @description  try to take over the world!
// @author       You
// @match        https://scriptcat.org/zh-CN?_TTP_GM_addElement
// @grant        GM_addElement
// @noframes
// ==/UserScript==

(function () {
  'use strict';

  trustedTypes.createPolicy('abc', {
    createHTML: (string) => { throw new Error('Unsafe HTML') },
    createScript: (string) => { throw new Error('Unsafe script') },
    createScriptURL: (string) => { throw new Error('Unsafe URL') }
  });

  console.log("START");
  const elm = GM_addElement("script", { textContent: "console.log('userscript running')" });
  console.log("END", elm);

  // 成功的话会打印 userscript running
})();

未修好的 v1.3

Screenshot 2026-02-12 at 19 59 02

PR修正后

Screenshot 2026-02-12 at 20 00 17

@cyfung1031 cyfung1031 added the P1 🔥 重要但是不紧急的内容 label Feb 15, 2026
@CodFrm CodFrm requested a review from Copilot February 15, 2026 08:02
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

这个 PR 重构了 GM_addElement API 以解决 v1.3 scripting 中的 CSP (Content Security Policy) 和 TTP (Trusted Types Policy) 限制问题。主要改进包括:

Changes:

  • 移除了 scripting.ts 中通过消息传递处理 GM_addElement 的旧实现
  • 在 gm_api.ts 中实现了新的 GM_addElement,直接在 content 环境处理 DOM 操作以绕过 CSP 限制
  • 添加了 native 选项支持在页面环境创建元素(用于 Custom Elements)
  • 新增第四个参数 refNode 支持 insertBefore 功能
  • 扩展了属性支持(innerHTML, innerText, outerHTML, className, value)
  • 在 custom_event_message.ts 中添加了新的消息处理逻辑
  • 添加了 dispatchMyEvent 辅助函数简化事件分发

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 15 comments.

Show a summary per file
File Description
src/app/service/content/scripting.ts 移除了旧的 GM_addElement 消息处理代码(38行)
src/app/service/content/gm_api/gm_api.ts 重写 GM_addElement 实现,新增 120 行代码支持 CSP/TTP 绕过、native 模式和 insertBefore 功能
src/app/service/content/global.ts 在 Native 对象中添加 createElement 和 ownFragment 以防止页面篡改
packages/message/custom_event_message.ts 添加消息处理逻辑以支持在 content 环境创建和插入元素
packages/message/common.ts 新增 dispatchMyEvent 辅助函数和相关类型定义
example/tests/gm_add_element.js 添加测试脚本验证新功能(native、insertBefore、各种属性)
Comments suppressed due to low confidence (1)

src/app/service/content/gm_api/gm_api.ts:889

  • GM.addElement 的 Promise 版本缺少第四个参数 refNode 的支持。这导致 GM.addElement 无法使用新增的 insertBefore 功能。建议更新签名以支持完整的参数列表,或者至少在文档中说明 GM.addElement 不支持此功能。
  @GMContext.API({ depend: ["GM_addElement"] })
  public "GM.addElement"(
    parentNode: Node | string,
    tagName: string | Record<string, string | number | boolean>,
    attrs: Record<string, string | number | boolean> = {}
  ): Promise<Element | undefined> {
    return new Promise<Element | undefined>((resolve) => {
      const ret = this.GM_addElement(parentNode, tagName, attrs);
      resolve(ret);
    });
  }

Comment on lines +795 to +796
// 最小值为 1000000000 避免与其他 related Id 操作冲突
let randInt = Math.floor(Math.random() * 1147483647 + 1000000000); // 32-bit signed int
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.
Comment on lines +804 to +807
// 目前未有直接取得 eventFlag 的方法。通过 page/content 的 receiveFlag 反推 eventFlag
const eventFlag = (this.message as CustomEventMessage).receiveFlag
.split(`${DefinedFlags.outboundFlag}${DefinedFlags.domEvent}`)[0]
.slice(0, -2);
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.
Comment on lines 760 to 765
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 {
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.
}
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.

暂不考虑。日后再算

}
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.
Comment on lines +794 to +797
// 不需要 incremental. 这个值只是在用来作一次性同步处理
// 最小值为 1000000000 避免与其他 related Id 操作冲突
let randInt = Math.floor(Math.random() * 1147483647 + 1000000000); // 32-bit signed int
randInt -= randInt % 100; // 用此方法可以生成不重复的 id
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.

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

Comment on lines +95 to +98
export const dispatchMyEvent = <T extends Record<string, TNestedPrimitive>>(
type: string,
eventInitDict: MouseEventInit | Omit<T, "movementX" | "relatedTarget">
) => {
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 呀

}
if (!el) {
// 一般情况(非 isNative) 或 元素生成失败 (报错或回传null/undefined)
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.

同步操作。不会有这情况

Comment on lines +820 to +826
} catch {
// 避免元素生成失败时无法执行。此情况应 fallback
console.warn("GM API: Native.createElement failed");
}
}
if (!el) {
// 一般情况(非 isNative) 或 元素生成失败 (报错或回传null/undefined)
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request hotfix 需要尽快更新到扩展商店 P1 🔥 重要但是不紧急的内容

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] GM_addElement Bug

2 participants