✨ 为 GM_xmlhttpRequest / GM.xmlHttpRequest 支持上传进度事件 (GM/TM/VM) - #1566
✨ 为 GM_xmlhttpRequest / GM.xmlHttpRequest 支持上传进度事件 (GM/TM/VM)#1566cyfung1031 wants to merge 14 commits into
Conversation
对齐 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>
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>
|
按 PickInvariant 的 DELTA_AUDIT,对 base 结论:正常 native XHR 上传事件、字段映射、声明和去重逻辑基本覆盖,但以下跨层接缝仍有缺口:
已检查:函数回调启用谓词、native XHR 监听绑定、事件名/进度字段、upload/main abort 顺序、loadend 去重、模板/英文/中文声明。未验证:真实浏览器的 CORS 预检、native upload error/timeout/loadend 时序及 E2E;PR 中的 worker profile 初始化调整不在本上传语义结论内。 |
Checklist / 检查清单
背景
当前
GM_xmlhttpRequest/GM.xmlHttpRequest只能接收主请求下载阶段的事件,无法通过details.upload获取上传阶段的开始、进度、完成或异常信息。这会影响文件上传、表单上传等需要显示上传进度、处理超时或响应用户中止操作的用户脚本,也使 ScriptCat 与原生
XMLHttpRequestUpload以及常见用户脚本管理器的接口存在差异。本次改动
为
details.upload增加以下回调支持:onloadstartonprogressonloadonloadendonerroronabortontimeout后台原生
XMLHttpRequest路径监听xhr.upload,通过现有消息通道转发为onupload*事件。内容脚本侧将
onupload*消息分发到details.upload.on*,并统一提供:loadedtotallengthComputabledonetotalSize请求体编码完成后计算
hasUpload,只在请求确实可能产生上传生命周期时通知后台绑定上传监听器。对齐原生 XHR 的主动中止行为,处理上传回调与主请求回调的顺序、去重和同步竞态。
更新
GMSend.XHRDetails、GMTypes.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:fetch传输;GET或HEAD;details.upload中至少有一个回调值确实是函数。后台仅在原生 XHR 路径且
details.hasUpload === true时绑定七类xhr.upload事件。这样不会让未使用上传回调的既有脚本承担额外预检、性能开销和跨域兼容风险。
2. 区分“注册了回调”和“真实存在上传生命周期”
仅传入
details.upload并不代表浏览器一定会产生上传事件:GET/HEAD的请求体会被忽略;fetch传输路径没有XMLHttpRequestUpload;details.upload中 truthy 但非函数的值不是有效回调。因此后台监听开关、本地完成状态以及
abort()的上传事件补发都统一依赖canHaveUploadLifecycle。这可以避免在根本不存在上传阶段的请求中错误触发
upload.onabort或upload.onloadend。3. 对齐原生 XHR 的中止语义和事件顺序
当上传尚未完成时主动调用返回对象的
abort():upload.onabort;upload.onloadend;onabort;onloadend。实现使用
uploadDone和uploadLoadEndCalled维护上传完成状态并保证upload.onloadend只执行一次。同时处理以下竞态:
upload.onload回调中同步调用abort()时,上传已被视为完成,不应误触发upload.onabort;upload.onloadend丢失,因此需要本地兜底;upload.onloadend和本地兜底只能执行一次;upload.onloadend会保留最后一次真实上传进度;error、abort或timeout结束后,返回对象的abort()会成为空操作,让真实的主请求终止事件继续到达,避免被本地合成的AbortError覆盖。4. 上传事件统一使用进度响应结构
主请求
onprogress与全部上传事件共用同一套进度参数构造逻辑。上传事件会尽量保留现有 XHR 响应字段,并统一补充:
lengthComputableloadedtotaldonetotalSize其中
done与loaded一致,totalSize与total一致,用于兼容现有 ScriptCat 进度回调结构。5. 只接受函数类型的上传回调
details.upload中 truthy 但非函数的值不会:这样既避免无效配置改变请求行为,也避免运行时出现非函数调用异常。
6. 降低 E2E 并行执行的启动开销
GM API 浏览器测试需要先通过
chrome://extensions和developerPrivate启用 user scripts 权限,再重新启动持久化浏览器上下文。此前每个测试都会重复执行完整初始化;并行 worker 较多时,大量 Chrome 实例会同时争用 CPU,使扩展 Service Worker 启动时间超过测试超时。
现在权限初始化改为 worker 级 fixture:
这样可以减少重复启动成本,同时避免脚本、存储和扩展状态在不同测试之间泄漏。
已知限制
1. 强制走
fetch的配置不支持上传回调当前以下配置会让 ScriptCat 使用
fetch传输,因此不会产生details.upload事件:fetch: trueredirectanonymous: true或mozAnon: trueresponseType: "stream"其中
anonymous/mozAnon切换到fetch是现有架构通过credentials: "omit"实现不携带 Cookie 的方式。要让匿名请求继续使用原生 XHR,需要重新设计跨域 Cookie 处理机制,影响范围超出本 PR,因此本次只明确记录限制,不调整现有传输架构。
2. 兼容目标
本实现以原生
XMLHttpRequestUpload的事件模型和顺序为准,并提供与常见用户脚本管理器相近的details.upload接口。类型声明和说明不承诺与某个特定版本的 Tampermonkey、Greasemonkey 或 Violentmonkey 在所有边缘行为上完全一致。
3. 进度总量由浏览器决定
所有上传回调都会提供
loaded、total和lengthComputable,但浏览器不保证每次都能计算上传总大小。用户脚本仍应在
lengthComputable === false时使用不确定进度展示,而不能假设total始终有效。建议审查重点
hasUpload/canHaveUploadLifecycle是否完整覆盖所有传输选择条件;upload.onload内同步调用abort()时的事件顺序;abort()是否会保留真实的主请求错误类型;fetch配置下的限制说明是否足够清晰;参考
https://wiki.greasespot.net/GM.xmlHttpRequest
Tampermonkey/tampermonkey#1429
violentmonkey/violentmonkey#2302