From add06e880fed10a0d5505f9dd9447f19f4626dad Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Mon, 20 Jul 2026 07:47:49 +0900 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20=E4=BF=AE=E5=A4=8D=20GM=5Fdownlo?= =?UTF-8?q?ad=20=E4=BC=A0=E5=85=A5=E7=A9=BA=20url=20=E6=97=B6=E6=9C=AA?= =?UTF-8?q?=E8=A7=A6=E5=8F=91=20onerror=20=E7=9A=84=E5=85=BC=E5=AE=B9?= =?UTF-8?q?=E6=80=A7=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit new URL("", location.href) 不会抛错而是解析为当前页面地址,导致空 url 被当作有效地址发起下载,与 TM 触发 onerror 的行为不一致。 Co-Authored-By: Claude Sonnet 5 --- src/app/service/content/gm_api/gm_api.test.ts | 47 +++++++++++++++++++ src/app/service/content/gm_api/gm_api.ts | 9 ++++ 2 files changed, 56 insertions(+) diff --git a/src/app/service/content/gm_api/gm_api.test.ts b/src/app/service/content/gm_api/gm_api.test.ts index 8a94e1b66..60ec1b1a8 100644 --- a/src/app/service/content/gm_api/gm_api.test.ts +++ b/src/app/service/content/gm_api/gm_api.test.ts @@ -1209,6 +1209,53 @@ return { value1, value2, value3, values1,values2, allValues1, allValues2, value4 }); }); +describe("@grant GM_download", () => { + it("空 url 应触发 onerror 而不是发起下载(GM_download)", async () => { + const script = Object.assign({}, scriptRes) as ScriptLoadInfo; + script.metadata.grant = ["GM_download"]; + const exec = new ExecScript(script, { + envPrefix: "scripting", + message: undefined as any, + contentMsg: undefined as any, + code: nilFn, + envInfo, + }); + script.code = `return new Promise((resolve) => { + let onloadCalled = false; + GM_download({ + url: "", + name: "empty-url-test.bin", + onload: () => { onloadCalled = true; }, + onerror: (e) => resolve({ onloadCalled, error: e && e.error }), + }); + setTimeout(() => resolve({ onloadCalled, error: "TIMEOUT" }), 100); + })`; + exec.scriptFunc = compileScript(compileScriptCode(script)); + const ret = await exec.exec(); + expect(ret.onloadCalled).toEqual(false); + expect(ret.error).toEqual("unknown"); + }); + + it("空 url 应 reject(GM.download)", async () => { + const script = Object.assign({}, scriptRes) as ScriptLoadInfo; + script.metadata.grant = ["GM.download"]; + const exec = new ExecScript(script, { + envPrefix: "scripting", + message: undefined as any, + contentMsg: undefined as any, + code: nilFn, + envInfo, + }); + script.code = `return GM.download({ url: "", name: "empty-url-test.bin" }).then( + () => ({ resolved: true }), + () => ({ resolved: false }) + )`; + exec.scriptFunc = compileScript(compileScriptCode(script)); + const ret = await exec.exec(); + expect(ret.resolved).toEqual(false); + }); +}); + describe("@grant CAT.agent.conversation", () => { it("CAT.agent.conversation 应该在沙盒中可访问", async () => { const script = Object.assign({}, scriptRes) as ScriptLoadInfo; diff --git a/src/app/service/content/gm_api/gm_api.ts b/src/app/service/content/gm_api/gm_api.ts index 945d5e936..2ea570d90 100644 --- a/src/app/service/content/gm_api/gm_api.ts +++ b/src/app/service/content/gm_api/gm_api.ts @@ -993,6 +993,15 @@ export default class GMApi extends GM_Base { }; const handle = async () => { const url = await urlPromiseLike; + if (!url) { + // TM 对空 url 会同步报错/触发 onerror,而非发起请求; + // new URL("", base) 不会抛错而是解析为当前页面地址,因此需在此显式拦截,避免误下载当前页面。 + if (!aborted) { + details.onerror?.(makeCallbackParam({ error: "unknown" }) as GMTypes.DownloadError); + retPromiseReject?.(new Error("GM_download: url is empty")); + } + return; + } const downloadMode = details.downloadMode || "native"; // native = sc_default; browser = chrome api details.url = url; if (downloadMode === "browser" || url.startsWith("blob:")) {