fix: browser API chrome.downloads.download 代码及Mock修正#1410
Conversation
There was a problem hiding this comment.
Pull request overview
该 PR 旨在修复 chrome.downloads.download 在存在 onDeterminingFilename 监听器时可能忽略 filename 的 Chromium 已知问题(Close #1409),通过在 service worker 侧引入统一的下载封装来稳定处理文件名覆盖,并同步完善 chrome.downloads 的 mock 与单元测试,确保行为可回归验证。
Changes:
- 新增
startDownload下载封装:集中处理onDeterminingFilename+onChanged,并为调用方提供完成/中断回调。 - 将 service worker 中相关下载调用(如备份导出、GM_download)改为使用新封装,以规避文件名覆盖失效问题。
- 重写
chrome.downloadsmock,补齐 Promise/callback 形态与事件模拟,并新增对应单测覆盖。
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| src/app/service/service_worker/synchronize.ts | 备份导出下载改为走 startDownload 封装 |
| src/app/service/service_worker/gm_api/gm_api.ts | GM_download 下载流程改为使用 startDownload + 回调通知 |
| src/app/service/service_worker/download.ts | 新增下载封装:注册/卸载监听、文件名覆盖与完成/中断回调 |
| src/app/service/service_worker/download.test.ts | 为下载封装与 mock 下载行为新增单测覆盖 |
| src/app/service/content/gm_api/gm_api.ts | 优化 native->browser 下载链路的 Object URL 释放时机(覆盖更多回包分支) |
| packages/chrome-extension-mock/index.ts | chromeMock.init 时重置 downloads mock 状态 |
| packages/chrome-extension-mock/downloads.ts | 重写 downloads mock:支持 Promise/callback、事件与文件名决策流程 |
| @@ -1060,30 +1061,44 @@ export default class GMApi extends GM_Base { | |||
| } as GMTypes.DownloadDetails<string>, | |||
| ]); | |||
| if (aborted) return; | |||
| let released = false; | |||
| const releaseResources = () => { | |||
| if (released) return; | |||
| released = true; | |||
| setTimeout(() => { | |||
| // 释放不需要的 URL | |||
| URL.revokeObjectURL(url); | |||
| }, 1); | |||
| }; | |||
|
先研究一下 Copilot 的说法 DRAFT PR |
Code reviewFound 1 functional issue + a few CLAUDE.md hygiene points worth a look:
scriptcat/src/app/service/content/gm_api/gm_api.ts Lines 1012 to 1038 in fb25aa3 而 SW 是无差别给两条 conn 都发 scriptcat/src/app/service/service_worker/gm_api/gm_api.ts Lines 1290 to 1300 in fb25aa3
scriptcat/src/app/service/service_worker/download.ts Lines 43 to 52 in fb25aa3
scriptcat/src/app/service/service_worker/gm_api/gm_api.ts Lines 1310 to 1320 in fb25aa3
scriptcat/src/app/service/service_worker/download.ts Lines 106 to 150 in fb25aa3
🤖 Generated with Claude Code - If this code review was useful, please react with 👍. Otherwise, react with 👎. |
CodFrm
left a comment
There was a problem hiding this comment.
几个具体修改建议附在 inline 评论里。其中第一条是功能回归,建议合并前一并补上。
| retPromiseResolve?.(data.data); | ||
| releaseResources(); | ||
| break; | ||
| case "save_cancelled": // saveAs cancelled by user |
There was a problem hiding this comment.
功能回归:直接 browser 路径漏了同样的 save_cancelled case。
本文件里 GM_download 有两条独立的 connect.onMessage switch:
- 这里(native xhr → blob 内层,L1102-)—— ✅ PR 已加
save_cancelled - 上面 L1012-1037 的直连分支(
downloadMode === "browser"或blob:URL)—— ❌ 没加,会落到default:触发retPromiseReject(new Error("Unexpected Internal ERROR"))+onerror
SW 现在是无差别给两条 conn 都发 save_cancelled 的(src/app/service/service_worker/gm_api/gm_api.ts 新增的 else if (o.state === "save_cancelled")),所以走 downloadMode: "browser" + saveAs: true 然后点取消的脚本会拿到错误回调,正好和这条 PR 想保证的契约相反。example/tests/gm_download_test.js 的 "Cancel via chrome://downloads (browser mode)" 用例覆盖到的就是这条会出 bug 的路径。
建议在 L1014 同样加一条(直连分支没有 releaseResources,所以更短):
case "onload":
case "save_cancelled": // saveAs cancelled by user,TM 兼容契约:视作下载成功
details.onload?.(makeCallbackParam({ ...data.data }));
retPromiseResolve?.(data.data);
break;| if (!entry) return; | ||
| const state = downloadDelta.state?.current; | ||
| if (state === STATE.COMPLETE || state === STATE.INTERRUPTED) { | ||
| detachDownloadCallback(id); |
There was a problem hiding this comment.
缓存路径恒失效。
detachDownloadCallback(id) 内部会 responseMap.delete(downloadId)(L177-181),紧接着 L126 !responseMap.has(id) 永远为 true,onDeterminingFilename 之前缓存的 bytesReceived/fileSize/totalBytes 在这里其实拿不到,每次都得调 chrome.downloads.search 兜底;若 search 拿不到 item,最终 callback 的 loaded/total 是 undefined。
建议把 detach 挪到 notifyDownloadCallback 之后(L155 处),并去掉 L156 重复的 responseMap.delete(id):
await notifyDownloadCallback(entry.callback, {
downloadId: id,
state: isSaveCancelled ? "save_cancelled" : state,
loaded: downloadItem?.downloadItem?.totalBytes,
total: downloadItem?.downloadItem?.totalBytes,
});
detachDownloadCallback(id); // 通知完成后再清理,避免 responseMap 缓存被提前删除| if (cDownloadId! > 0 && !reqCompleteWith) { | ||
| reqCompleteWith = "disconnected"; | ||
| chrome.downloads.cancel(cDownloadId, () => { | ||
| chrome.downloads.cancel(cDownloadId!, () => { |
There was a problem hiding this comment.
非空断言绕过收窄(CLAUDE.md: "Fix root causes, not symptoms. No as any / // @ts-ignore / try-catch swallow ...")。
用 typeof 检查替代 !,运行时等价,TS 也能自动收窄:
| if (cDownloadId! > 0 && !reqCompleteWith) { | |
| reqCompleteWith = "disconnected"; | |
| chrome.downloads.cancel(cDownloadId, () => { | |
| chrome.downloads.cancel(cDownloadId!, () => { | |
| if (typeof cDownloadId === "number" && cDownloadId > 0 && !reqCompleteWith) { | |
| reqCompleteWith = "disconnected"; | |
| chrome.downloads.cancel(cDownloadId, () => { |
Checklist / 检查清单
Description / 描述
Close #1409
chrome.downloads.onDeterminingFilename。这是官方已知的 Chromium Bug。 https://issues.chromium.org/issues/40706258 。实作起来可以解决问题,所以就先这样吧。chrome.downloads.onDeterminingFilename,重新包装了chrome.downloads.download相关的操作。chrome.downloads的 mockScreenshots / 截图
Filename for chrome.downloads.download is ignored if onDeterminingFilename listener exists
https://issues.chromium.org/issues/40734759