[v1.3] 配合 1.3 scripting, 重构 `GM_addElement` (bug 修补 + 功能改进) by cyfung1031 · Pull Request #1233 · scriptscat/scriptcat

🔍 PR #1233 代码审查:重构 GM_addElement

📋 概述

此 PR 对 GM_addElement API 进行了重大重构,主要目的是:

  1. 修复 CSP/TTP 限制下无法插入元素的 bug — 统一由 content.js 处理元素插入
  2. 新增功能 — 支持 native: true 原生创建元素、insertBefore 式的 refNode 参数、更多属性如 innerHTML/outerHTML/innerText/className/value
  3. 移除旧逻辑 — 删除 scripting.tsGM_addElement 的 case 分支,不再走 background 消息中转

✅ 优点

  • 架构改进明显:旧实现通过 syncSendMessage 经 scripting 中转,新实现直接在 inject/content 端利用 dispatchMyEvent 同步事件通讯完成元素创建和插入,更加直接高效
  • 更好的 CSP/TTP 兼容性:文字/数字类属性在 content.js 设置以避开 TrustedTypes 限制
  • 功能扩展合理native: truerefNode、更多属性支持等都是实际需求驱动
  • 注释详尽:代码中有充分的中文注释解释设计意图
  • 测试脚本完备:新增了 example/tests/gm_add_element.js 覆盖多种使用场景

⚠️ 问题和建议

1. 🔴 GM_addStyle 仍使用旧的 syncSendMessage 路径,而 scripting.ts 已删除 GM_addElement 的 case

GM_addStylegm_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. 🟡 dispatchMyEventcancelable 检查可能导致误报错误

if (resFalse !== false && eventInitDict.cancelable === true) {
  throw new Error("Page Message Error");
}

如果 content 端的事件监听器尚未准备好(例如时序问题),preventDefault() 不会被调用,会导致抛出异常。注释说"通讯设置正确的话应不会发生",但在实际环境中初始化时序可能出问题。

建议:考虑添加 fallback 或更友好的错误提示,而不是直接 throw。或者增加重试机制。

6. 🟡 custom_event_message.tsappendOrInsert 处理缺少错误检查

const el = <Element>this.getAndDelRelatedTarget(id1);
const parent = <Node>this.getAndDelRelatedTarget(id2);

如果 getAndDelRelatedTarget 返回 undefined(例如 ID 碰撞导致数据被提前消费),后续的 el.setAttributeparent.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 名称不够描述性,不太符合项目中其他函数的命名风格(如 pageDispatchEventpageDispatchCustomEvent)。

建议:考虑更具描述性的名称,如 dispatchBridgeEventdispatchRelatedTargetEvent

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