[v1.3] 配合 1.3 scripting, 重构 GM_addElement (bug 修补 + 功能改进)#1233
[v1.3] 配合 1.3 scripting, 重构 GM_addElement (bug 修补 + 功能改进)#1233cyfung1031 wants to merge 9 commits intoscriptscat:release/v1.3from
GM_addElement (bug 修补 + 功能改进)#1233Conversation
GM_addElementGM_addElement (bug 修补 + 功能改进)
我看没问题啊 |
This comment was marked as outdated.
This comment was marked as outdated.
There was a problem hiding this comment.
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);
});
}
| // 最小值为 1000000000 避免与其他 related Id 操作冲突 | ||
| let randInt = Math.floor(Math.random() * 1147483647 + 1000000000); // 32-bit signed int |
There was a problem hiding this comment.
代码注释中提到 "32-bit signed int",但 JavaScript 的 Number 类型是 64-bit 浮点数。Math.random() * 1147483647 的结果可以精确表示,但注释可能会误导读者。建议澄清这是为了避免溢出还是其他原因。
| // 最小值为 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); |
| // 目前未有直接取得 eventFlag 的方法。通过 page/content 的 receiveFlag 反推 eventFlag | ||
| const eventFlag = (this.message as CustomEventMessage).receiveFlag | ||
| .split(`${DefinedFlags.outboundFlag}${DefinedFlags.domEvent}`)[0] | ||
| .slice(0, -2); |
There was a problem hiding this comment.
通过字符串操作(split 和 slice)从 receiveFlag 反推 eventFlag 是脆弱的实现。如果 DefinedFlags 的格式发生变化,这段代码会静默失败。建议提供一个明确的方法来获取 eventFlag,或者在 CustomEventMessage 中存储 eventFlag 以便直接访问。
| 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 { |
There was a problem hiding this comment.
TypeScript 类型定义需要更新以支持新的 API 签名。当前的类型定义缺少:1) 第四个参数 refNode 用于 insertBefore 功能;2) attrs 中的 native 选项;3) 新支持的属性如 innerHTML、innerText、outerHTML、className、value。建议更新 src/types/scriptcat.d.ts 文件。
| } | ||
| return (<CustomEventMessage>this.message).getAndDelRelatedTarget(resp.data) as Element; | ||
|
|
||
| refNode = refNode instanceof Node && refNode.parentNode === sParentNode ? refNode : null; |
There was a problem hiding this comment.
refNode 验证逻辑(refNode.parentNode === sParentNode)会在 refNode 还未插入到 DOM 时将其设为 null。这限制了 API 的灵活性,因为用户可能想在元素插入前就指定插入位置。建议移除此验证,让 insertBefore 在 refNode 不在 parent 中时自然失败,或者提供更清晰的错误消息。
| refNode = refNode instanceof Node && refNode.parentNode === sParentNode ? refNode : null; | |
| // 仅在 refNode 为合法 DOM 节点时保留,其是否属于 sParentNode 交由后续 DOM 操作自行校验 | |
| refNode = refNode instanceof Node ? refNode : null; |
| } | ||
| if (resFalse !== false && eventInitDict.cancelable === true) { | ||
| // 通讯设置正确的话应不会发生 | ||
| throw new Error("Page Message Error"); |
There was a problem hiding this comment.
dispatchMyEvent 函数对于失败的情况(resFalse !== false && cancelable === true)会抛出通用错误 "Page Message Error"。这个错误消息不够具体,无法帮助开发者诊断问题。建议提供更详细的错误信息,包括 event type 和可能的失败原因。
| 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}` | |
| ); |
| // 不需要 incremental. 这个值只是在用来作一次性同步处理 | ||
| // 最小值为 1000000000 避免与其他 related Id 操作冲突 | ||
| let randInt = Math.floor(Math.random() * 1147483647 + 1000000000); // 32-bit signed int | ||
| randInt -= randInt % 100; // 用此方法可以生成不重复的 id |
There was a problem hiding this comment.
随机 ID 生成存在潜在的冲突风险。虽然使用了大范围(1000000000 到 2147483647)并取整到 100 的倍数,但在高并发场景下仍可能产生冲突。建议使用递增的 ID 生成器或添加冲突检测机制。
There was a problem hiding this comment.
只是一次性用途。不需要避免冲突
| export const dispatchMyEvent = <T extends Record<string, TNestedPrimitive>>( | ||
| type: string, | ||
| eventInitDict: MouseEventInit | Omit<T, "movementX" | "relatedTarget"> | ||
| ) => { |
There was a problem hiding this comment.
dispatchMyEvent 的类型定义不够严格。Omit<T, "movementX" | "relatedTarget"> 允许 eventInitDict 包含这些字段,但实际检查是使用 "in" 操作符。这可能导致类型安全问题。建议使用更严格的类型约束,或者在运行时添加更明确的验证。
There was a problem hiding this comment.
typescript 要用 in 呀
| } | ||
| if (!el) { | ||
| // 一般情况(非 isNative) 或 元素生成失败 (报错或回传null/undefined) | ||
| const frag = Native.ownFragment; |
There was a problem hiding this comment.
Native.ownFragment 使用单例 DocumentFragment,这在多次调用 GM_addElement 时会导致问题。当一个元素被 appendChild 到 fragment 后,后续调用会共享同一个 fragment,可能导致元素被意外移除或覆盖。应该为每次调用创建新的 DocumentFragment 实例。
| const frag = Native.ownFragment; | |
| const frag = document.createDocumentFragment(); |
| } catch { | ||
| // 避免元素生成失败时无法执行。此情况应 fallback | ||
| console.warn("GM API: Native.createElement failed"); | ||
| } | ||
| } | ||
| if (!el) { | ||
| // 一般情况(非 isNative) 或 元素生成失败 (报错或回传null/undefined) |
There was a problem hiding this comment.
在 isNative 模式下,如果 Native.createElement 失败(抛出异常或返回 null/undefined),代码会 fallback 到 content 环境创建元素。但这可能违背用户的预期 - 如果用户明确指定了 native: true,可能是因为需要在页面环境创建特殊元素(如 Custom Elements)。Fallback 到 content 环境可能导致功能异常。建议在 isNative 失败时直接抛出错误,或至少记录更明确的警告信息。
| } 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 环境创建元素 |
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
🔍 PR #1233 代码审查:重构 GM_addElement
📋 概述
此 PR 对 GM_addElement API 进行了重大重构,主要目的是:
- 修复 CSP/TTP 限制下无法插入元素的 bug — 统一由 content.js 处理元素插入
- 新增功能 — 支持
native: true原生创建元素、insertBefore式的refNode参数、更多属性如innerHTML/outerHTML/innerText/className/value - 移除旧逻辑 — 删除
scripting.ts中GM_addElement的 case 分支,不再走 background 消息中转
✅ 优点
- 架构改进明显:旧实现通过
syncSendMessage经 scripting 中转,新实现直接在 inject/content 端利用dispatchMyEvent同步事件通讯完成元素创建和插入,更加直接高效 - 更好的 CSP/TTP 兼容性:文字/数字类属性在 content.js 设置以避开 TrustedTypes 限制
- 功能扩展合理:
native: true、refNode、更多属性支持等都是实际需求驱动 - 注释详尽:代码中有充分的中文注释解释设计意图
- 测试脚本完备:新增了
example/tests/gm_add_element.js覆盖多种使用场景
⚠️ 问题和建议
1. 🔴 GM_addStyle 仍使用旧的 syncSendMessage 路径,而 scripting.ts 已删除 GM_addElement 的 case
GM_addStyle 仍使用旧的 syncSendMessage 路径,而 scripting.ts 已删除 GM_addElement 的 caseGM_addStyle(gm_api.ts 第 724–748 行)仍然通过 syncSendMessage 调用 scripting.ts 中的 GM_addElement case,但该 case 已在此 PR 中被删除。这将导致 GM_addStyle 完全失效!
// gm_api.ts — GM_addStyle 仍旧使用已删除的路径
const resp = (<CustomEventMessage>this.message).syncSendMessage({
action: `${this.prefix}/runtime/gmApi`,
data: {
uuid: this.scriptRes.uuid,
api: "GM_addElement", // ← scripting.ts 中此 case 已被删除
params: [null, "style", { textContent: css }, isContent],
},
});建议:将 GM_addStyle 也重构为使用新的 GM_addElement 路径,例如直接调用 this.GM_addElement('style', { textContent: css })。
2. 🔴 随机 ID 碰撞风险
let randInt = Math.floor(Math.random() * 1147483647 + 1000000000);
randInt -= randInt % 100; // 用此方法可以生成不重复的 id注释说"可以生成不重复的 id",但 Math.random() 并不保证唯一性。如果两个脚本或同一脚本快速连续调用 GM_addElement,理论上可能产生相同的 randInt,导致 relatedTargetMap 中的数据被覆盖。
建议:使用递增计数器(类似现有的 relateId)或在现有 relateId 基础上扩展,确保 ID 唯一性。例如:
// 在类或模块级别维护一个递增计数器
const baseId = (relateId = relateId + 100); // 每次递增 100,留出 id0~id3 的空间3. 🟡 eventFlag 的反推方式十分脆弱
const eventFlag = (this.message as CustomEventMessage).receiveFlag
.split(`${DefinedFlags.outboundFlag}${DefinedFlags.domEvent}`)[0]
.slice(0, -2);通过字符串分割和截取来反推 eventFlag 依赖于 flag 的内部格式,如果 DefinedFlags 的格式发生变化,此处会无声地出错。PR 描述中也提到"目前未有直接取得 eventFlag 的方法"。
建议:在 CustomEventMessage 类中暴露一个 getter 方法(如 getEventFlag()),或将 eventFlag 作为构造参数保存,避免外部依赖内部格式。
4. 🟡 Native.ownFragment 作为单例的线程安全问题
// global.ts
ownFragment: new DocumentFragment(),Native.ownFragment 是一个全局共享的 DocumentFragment。如果在极端情况下(如事件监听器中触发了另一次 GM_addElement 调用),frag.lastChild 可能返回错误的元素。
建议:每次调用时创建新的 DocumentFragment,或者在使用后立即清空:
const frag = new DocumentFragment(); // 每次新建5. 🟡 dispatchMyEvent 中 cancelable 检查可能导致误报错误
if (resFalse !== false && eventInitDict.cancelable === true) {
throw new Error("Page Message Error");
}如果 content 端的事件监听器尚未准备好(例如时序问题),preventDefault() 不会被调用,会导致抛出异常。注释说"通讯设置正确的话应不会发生",但在实际环境中初始化时序可能出问题。
建议:考虑添加 fallback 或更友好的错误提示,而不是直接 throw。或者增加重试机制。
6. 🟡 custom_event_message.ts 中 appendOrInsert 处理缺少错误检查
const el = <Element>this.getAndDelRelatedTarget(id1);
const parent = <Node>this.getAndDelRelatedTarget(id2);如果 getAndDelRelatedTarget 返回 undefined(例如 ID 碰撞导致数据被提前消费),后续的 el.setAttribute 和 parent.appendChild 会抛出不明确的运行时错误。
建议:添加空值检查并提供有意义的错误信息:
if (!el || !parent) throw new Error("GM_addElement: relatedTarget not found");7. 🟡 innerHTML/outerHTML 的安全风险
在 content 环境中直接设置 innerHTML/outerHTML 可能绕过页面的 CSP 或 TrustedTypes 策略(这正是此 PR 的目的之一),但也意味着用户脚本可以注入任意 HTML。虽然用户脚本本身就有高权限,但仍需注意这可能被恶意脚本利用。
建议:在文档中明确说明此行为的安全影响,确保用户理解权限范围。
8. 🟢 小问题:dispatchMyEvent 函数命名
dispatchMyEvent 名称不够描述性,不太符合项目中其他函数的命名风格(如 pageDispatchEvent、pageDispatchCustomEvent)。
建议:考虑更具描述性的名称,如 dispatchBridgeEvent 或 dispatchRelatedTargetEvent。
9. 🟢 类型标注可以更精确
attrs: Record<string, string | number | boolean> | Node | null = {}attrs 参数的类型联合了 Record<...> | Node | null,使得函数签名较为混乱。这是因为 GM_addElement 支持两种调用模式(有/无 parentNode),建议使用函数重载让类型更清晰。
📊 总结
| 维度 | 评价 |
|---|---|
| 代码正确性 | GM_addStyle 路径断裂是严重 bug |
| 架构设计 | ✅ 方向正确,但 eventFlag 反推和 ID 生成需改进 |
| 安全性 | 🟡 innerHTML/outerHTML 需注意,但在用户脚本场景下可接受 |
| 测试覆盖 | ✅ 有手动测试脚本,但缺少单元测试 |
| 代码风格 | 🟡 函数命名可改进,PR 作者自己也提到风格可能需重构 |
总体评价:功能设计思路好,解决了实际问题(CSP/TTP 绕过),但 GM_addStyle 的回归 bug 必须在合并前修复,且 ID 唯一性和 eventFlag 反推方式需要加固。建议修复关键问题后再合并。
🤖 Generated with Claude Code
|
确实写得有点随便 |
简单看了一下,那确实还是得整理一下,我这两天处理 |


事原
scripting.js不能在CSPTTP 插入元素执行代码GM_addElement- 最近支持了 onload onerror 等 function value改善
native: true)@inject-into有否指定了 content)@inject-into content) 执行 ,即 自己呼叫自己测试环境
注
api.bind(this)(优先度:低) #1212 有冲突。看哪个先合并Test