[v1.3] 配合 1.3 scripting, 重构 GM_addElement (bug 修补 + 功能改进)#1233
[v1.3] 配合 1.3 scripting, 重构 GM_addElement (bug 修补 + 功能改进)#1233cyfung1031 wants to merge 8 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>


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