Skip to content

✨ 为 GM_xmlhttpRequest / GM.xmlHttpRequest 支持上传进度事件 (GM/TM/VM)#1566

Draft
cyfung1031 wants to merge 11 commits into
mainfrom
agent/gm-xhr-upload-events
Draft

✨ 为 GM_xmlhttpRequest / GM.xmlHttpRequest 支持上传进度事件 (GM/TM/VM)#1566
cyfung1031 wants to merge 11 commits into
mainfrom
agent/gm-xhr-upload-events

Conversation

@cyfung1031

@cyfung1031 cyfung1031 commented Jul 11, 2026

Copy link
Copy Markdown
Collaborator

Checklist / 检查清单

  • Fixes API Compatibility Issue (GM/TM/VM)
  • Code reviewed by human / 代码通过人工检查
  • Changes tested / 已完成测试

背景

当前 GM_xmlhttpRequest / GM.xmlHttpRequest 只能接收主请求下载阶段的事件,无法通过 details.upload 获取上传阶段的开始、进度、完成或异常信息。

这会影响文件上传、表单上传等需要显示上传进度、处理超时或响应用户中止操作的用户脚本,也使 ScriptCat 与原生 XMLHttpRequestUpload 以及常见用户脚本管理器的接口存在差异。

本次改动

  • details.upload 增加以下回调支持:

    • onloadstart
    • onprogress
    • onload
    • onloadend
    • onerror
    • onabort
    • ontimeout
  • 后台原生 XMLHttpRequest 路径监听 xhr.upload,通过现有消息通道转发为 onupload* 事件。

  • 内容脚本侧将 onupload* 消息分发到 details.upload.on*,并统一提供:

    • loaded
    • total
    • lengthComputable
    • done
    • totalSize
  • 请求体编码完成后计算 hasUpload,只在请求确实可能产生上传生命周期时通知后台绑定上传监听器。

  • 对齐原生 XHR 的主动中止行为,处理上传回调与主请求回调的顺序、去重和同步竞态。

  • 更新 GMSend.XHRDetailsGMTypes.XHRDetails,新增 GMTypes.XHRUpload,并同步模板、英文及简体中文声明文件。

  • 新增后台、内容脚本、运行时及浏览器集成测试,覆盖正常上传、异常、超时、主动中止、事件顺序和竞态。

  • 调整 GM API E2E fixture:每个 Playwright worker 只执行一次 user scripts 权限初始化,各测试从预配置 profile 创建独立副本,减少并行 CI 中重复启动 Chrome 导致的 Service Worker 超时,同时保持测试状态隔离。

实现考虑

1. 只在确实需要时绑定 xhr.upload

XMLHttpRequestUpload 注册监听器会改变跨域请求行为,并可能触发额外的 CORS 预检请求。

因此不能无条件为所有 XHR 请求绑定上传监听器。

内容侧会在请求体完成编码后计算 canHaveUploadLifecycle,仅在同时满足以下条件时向后台发送 hasUpload: true

  1. 请求不会切换到 fetch 传输;
  2. 方法不是 GETHEAD
  3. 存在实际请求体;
  4. details.upload 中至少有一个回调值确实是函数。

后台仅在原生 XHR 路径且 details.hasUpload === true 时绑定七类 xhr.upload 事件。

这样不会让未使用上传回调的既有脚本承担额外预检、性能开销和跨域兼容风险。

2. 区分“注册了回调”和“真实存在上传生命周期”

仅传入 details.upload 并不代表浏览器一定会产生上传事件:

  • GET / HEAD 的请求体会被忽略;
  • 没有实际请求体时,上传阶段在发送前就视为完成;
  • fetch 传输路径没有 XMLHttpRequestUpload
  • details.upload 中 truthy 但非函数的值不是有效回调。

因此后台监听开关、本地完成状态以及 abort() 的上传事件补发都统一依赖 canHaveUploadLifecycle

这可以避免在根本不存在上传阶段的请求中错误触发 upload.onabortupload.onloadend

3. 对齐原生 XHR 的中止语义和事件顺序

当上传尚未完成时主动调用返回对象的 abort()

  1. 先触发 upload.onabort
  2. 再触发 upload.onloadend
  3. 然后触发主请求 onabort
  4. 最后触发主请求 onloadend

实现使用 uploadDoneuploadLoadEndCalled 维护上传完成状态并保证 upload.onloadend 只执行一次。

同时处理以下竞态:

  • upload.onload 回调中同步调用 abort() 时,上传已被视为完成,不应误触发 upload.onabort
  • 消息通道断开可能导致后台已发出或即将发出的真实 upload.onloadend 丢失,因此需要本地兜底;
  • 真实 upload.onloadend 和本地兜底只能执行一次;
  • 上传正常完成后再中止主请求时,兜底 upload.onloadend 会保留最后一次真实上传进度;
  • 上传以 erroraborttimeout 结束后,返回对象的 abort() 会成为空操作,让真实的主请求终止事件继续到达,避免被本地合成的 AbortError 覆盖。

4. 上传事件统一使用进度响应结构

主请求 onprogress 与全部上传事件共用同一套进度参数构造逻辑。

上传事件会尽量保留现有 XHR 响应字段,并统一补充:

  • lengthComputable
  • loaded
  • total
  • done
  • totalSize

其中 doneloaded 一致,totalSizetotal 一致,用于兼容现有 ScriptCat 进度回调结构。

5. 只接受函数类型的上传回调

details.upload 中 truthy 但非函数的值不会:

  • 启用后台上传监听器;
  • 触发额外的 CORS 预检;
  • 在事件到达时被尝试调用。

这样既避免无效配置改变请求行为,也避免运行时出现非函数调用异常。

6. 降低 E2E 并行执行的启动开销

GM API 浏览器测试需要先通过 chrome://extensionsdeveloperPrivate 启用 user scripts 权限,再重新启动持久化浏览器上下文。

此前每个测试都会重复执行完整初始化;并行 worker 较多时,大量 Chrome 实例会同时争用 CPU,使扩展 Service Worker 启动时间超过测试超时。

现在权限初始化改为 worker 级 fixture:

  1. 每个 worker 创建并初始化一次基础 profile;
  2. 每个测试复制该 profile 到独立临时目录;
  3. 测试结束后删除副本;
  4. worker 结束后删除基础 profile。

这样可以减少重复启动成本,同时避免脚本、存储和扩展状态在不同测试之间泄漏。

已知限制

1. 强制走 fetch 的配置不支持上传回调

当前以下配置会让 ScriptCat 使用 fetch 传输,因此不会产生 details.upload 事件:

  • fetch: true
  • 设置 redirect
  • anonymous: truemozAnon: true
  • responseType: "stream"

其中 anonymous / mozAnon 切换到 fetch 是现有架构通过 credentials: "omit" 实现不携带 Cookie 的方式。

要让匿名请求继续使用原生 XHR,需要重新设计跨域 Cookie 处理机制,影响范围超出本 PR,因此本次只明确记录限制,不调整现有传输架构。

2. 兼容目标

本实现以原生 XMLHttpRequestUpload 的事件模型和顺序为准,并提供与常见用户脚本管理器相近的 details.upload 接口。

类型声明和说明不承诺与某个特定版本的 Tampermonkey、Greasemonkey 或 Violentmonkey 在所有边缘行为上完全一致。

3. 进度总量由浏览器决定

所有上传回调都会提供 loadedtotallengthComputable,但浏览器不保证每次都能计算上传总大小。

用户脚本仍应在 lengthComputable === false 时使用不确定进度展示,而不能假设 total 始终有效。

建议审查重点

  • hasUpload / canHaveUploadLifecycle 是否完整覆盖所有传输选择条件;
  • 是否只有函数类型的上传回调会改变后台监听和跨域预检行为;
  • 主请求与上传事件之间的完成状态、去重和竞态处理;
  • upload.onload 内同步调用 abort() 时的事件顺序;
  • 上传异常后再次调用 abort() 是否会保留真实的主请求错误类型;
  • 强制 fetch 配置下的限制说明是否足够清晰;
  • 新增公开类型是否符合项目现有 API 命名和兼容策略;
  • worker 级 E2E profile 是否同时保证 CI 稳定性和测试状态隔离。

参考

https://wiki.greasespot.net/GM.xmlHttpRequest

Tampermonkey/tampermonkey#1429

violentmonkey/violentmonkey#2302

cyfung1031 and others added 5 commits July 11, 2026 17:14
对齐 Tampermonkey:details.upload 可挂载 onloadstart/onprogress/onload/
onloadend/onerror/onabort/ontimeout,对应原生 XMLHttpRequestUpload 事件。
仅原生 XHR 路径支持(fetch 模式无法获取上传进度,维持既有限制)。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- 仅在脚本实际注册了 upload 回调时才为 xhr.upload 绑定监听器(GMSend.XHRDetails
  新增 hasUpload 标记):为 xhr.upload 注册任何监听器都会令浏览器对跨域请求强制
  触发 CORS 预检 (OPTIONS),未使用该功能的脚本不应承担这一开销/兼容性风险。
- 调用返回的 abort() 时,若 upload 阶段尚未完成,补发 details.upload.onabort /
  onloadend(对齐原生 XMLHttpRequest.abort() 语义);upload 阶段已完成后再次
  abort 不会重复触发。放宽 abort() 的触发条件,使仅注册了 upload 回调(未注册
  顶层 onabort)的脚本也能正常收到 abort 通知。
- 统一 upload 各回调 (onloadstart/onload/onloadend/onerror/onabort/ontimeout)
  均携带 loaded/total/lengthComputable(对齐 XHR 规范:upload 的事件均为
  ProgressEvent),类型定义同步放宽为 Listener<XHRProgress>。
- 更新/新增测试覆盖以上场景。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- uploadDone 现在于 load/error/abort/timeout 到达时即置位(对齐规范:upload
  complete flag 在这些事件派发前已置位),而不仅仅是 loadend;避免在
  upload.onload 回调内同步调用 abort() 时被误判为"尚未完成"而重复触发
  upload.onabort。
- 新增 uploadLoadEndCalled 去重:upload.onloadend 保证恰好触发一次,无论是
  由真实的 onuploadloadend 消息触发,还是由 abort() 兜底补发触发(两者可能
  竞态:真实消息可能已在通道断开前送出但因断线而无法再被处理)。
- abort() 补发的 upload.onabort / onloadend 现在统一经由
  makeUploadCallbackParam 携带 loaded:0/total:0/lengthComputable:false,
  不再是缺少进度字段的裸响应对象,符合已声明的 XHRProgress 类型契约。
- hasAnyUploadHandler 改为只接受函数值:truthy 但非函数的回调(如
  { onprogress: true })不再被视为已注册,避免既误触发额外 CORS 预检,
  又在事件到达时把非函数值当函数调用而抛出。
- 新增测试覆盖:upload.onload 内同步 abort、补发数据的进度字段、
  abort 时的事件顺序 (upload.abort → upload.loadend → 主 abort → 主 loadend)、
  以及非函数 upload 回调不启用 upload 监听。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- 对齐规范:request error steps 在派发 upload 的 error/abort/timeout 前,
  已将整个请求置为 DONE(原生此时再调用 abort() 不会产生新事件)。新增
  suppressSyntheticAbort 标记,在 onuploaderror/onuploadabort/onuploadtimeout
  到达时置位;置位后返回的 abort() 变为空操作(不再断开通道、不再合成
  AbortError),让真实的主 onerror/ontimeout/onabort 消息能够正常到达并
  驱动正确的回调,而不是被本地合成的 onabort 抢先覆盖。
- 新增 lastUploadEventData,记录 upload 成功完成(onuploadload)时的真实
  loaded/total/lengthComputable;在 upload.onload 回调内同步调用 abort()
  导致真实 onuploadloadend 消息因通道断开而丢失时,兜底补发的 onloadend
  使用这份真实数据,而不是 abort() 合成响应对象里缺省的 undefined。
- 新增测试覆盖:upload.onerror / upload.ontimeout 内调用 abort() 不应覆盖
  真实的主 onerror/ontimeout;upload.onload 内 abort() 导致 loadend 消息
  丢失时,兜底补发的 onloadend 应携带真实的已传输字节数。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- 对齐 XHR 规范:GET/HEAD 请求体会被忽略;无请求体时 upload complete flag
  在发送前已置位;fetch 传输不绑定 xhr.upload——以上情况均不会产生真实的
  原生 upload 阶段。此前 uploadDone 仅依据"是否注册了 upload 回调"初始化,
  与是否存在真实 upload 阶段无关,导致这些场景下调用 abort() 仍会错误地
  合成 upload.onabort / onloadend。
- 新增 canHaveUploadLifecycle:在请求体编码完成、确认 method/body/transport
  后计算(method 非 GET/HEAD、有实际请求体、且不会走 fetch 传输、且注册了
  回调),用其替代 hasAnyUploadHandler(details.upload) 来初始化 uploadDone,
  并据此决定发往后台的 hasUpload 标记——避免此前对 GET/无请求体的原生 XHR
  请求也绑定 xhr.upload 监听器(本可避免的 CORS 预检开销)。
- doAbort 内的 upload 合成逻辑整体收敛到 canHaveUploadLifecycle 判断之下,
  避免"upload 从未开始"与"upload 已成功完成"两种 uploadDone=true 的语义
  被混淆(前者不应触发任何 upload.* 回调,后者才应携带真实数据补发 loadend)。
- 更新/新增测试:GET、HEAD、POST 无 data、fetch/redirect/anonymous/stream
  等场景下 hasUpload 应为 false 且 abort() 不触发 upload 事件;POST 带
  请求体的既有测试同步补上 method/data 以符合真实场景。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@cyfung1031 cyfung1031 changed the title ✨ 为 GM_xmlhttpRequest / GM.xmlHttpRequest 支持上传进度事件 ✨ 为 GM_xmlhttpRequest / GM.xmlHttpRequest 支持上传进度事件 (GM/TM/VM) Jul 11, 2026
cyfung1031 and others added 4 commits July 11, 2026 23:11
CI 下 workers:2 并行启动多个 Chrome 实例造成资源争抢,30s 偶发不足以等到扩展
service worker 就绪,导致 fixture 阶段超时误报(与 gm_xhr.ts 上传事件改动无关,
该测试不涉及任何 GM_xmlhttpRequest 调用)。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
之前每个 test 都要完整走两次 launchPersistentContext(含 chrome://extensions
导航 + developerPrivate 权限调用),CI 下多个 worker 并行时大量并发 Chrome
启动互相抢占 CPU,是 service worker 启动超时误报的根因。

参照 e2e/agent-fixtures.ts 已验证过的模式:新增 worker 级 gmApiProfileDir
fixture,只在每个 worker 里做一次 Phase 1 权限配置,每个 test 改为拷贝该
profile 目录后只做一次轻量 Phase 2 启动。本地验证:整个 spec 文件 7 个测试
2 workers 下 16.8s 全部通过。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@cyfung1031 cyfung1031 added the GM API 支持一下TM/VM/FM的API label Jul 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

GM API 支持一下TM/VM/FM的API

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant