From a35d8b83a86cff3faa085db9077250b6e5ded24a Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Sat, 28 Mar 2026 10:53:45 +0900 Subject: [PATCH 1/7] =?UTF-8?q?=E4=BB=A5=20Navigation=20API=20=E5=AE=9E?= =?UTF-8?q?=E7=8E=B0=20TM=20=E7=9A=84=20window.onurlchange?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/service/content/create_context.ts | 48 ++++++++++++++++++++++- 1 file changed, 46 insertions(+), 2 deletions(-) diff --git a/src/app/service/content/create_context.ts b/src/app/service/content/create_context.ts index 1bbe7f151..353a92af8 100644 --- a/src/app/service/content/create_context.ts +++ b/src/app/service/content/create_context.ts @@ -8,6 +8,46 @@ import { isEarlyStartScript } from "./utils"; import { ListenerManager } from "./listener_manager"; import { createGMBase } from "./gm_api/gm_api"; +class UrlChangeEvent extends Event { + url: string; + constructor(type: string, eventInitDict?: EventInit) { + super(type, eventInitDict); + this.url = ""; + } +} + +// Chrome 102+, Firefox 147+ +// https://developer.chrome.com/docs/web-platform/navigation-api +// https://developer.mozilla.org/en-US/docs/Web/API/Navigation_API#browser_compatibility +const attachNavigateHandler = (win: Window & { navigation: EventTarget }) => { + // 以 location.href 判断避免 replaceState/pushState 重复执行重复触发 + const location = win.location; + const getUrl = Object.getOwnPropertyDescriptor(location, "href")?.get?.bind(location); + const dispatcher = win.dispatchEvent.bind(win); + let lastUrl = getUrl?.(); + const handler = async (ev: Event): Promise => { + let newUrl = getUrl?.(); // 取得当前 location.href + const destinationUrl = (ev as any).destination?.url; + if (destinationUrl !== newUrl && newUrl === lastUrl) { + // 某些情况,location.href 未更新就触发了 + // 用 postMessage 推迟到下一个 marcoEvent 阶段 + await new Promise((resolve) => { + window.addEventListener("message", resolve, { once: true }); + window.postMessage({ [`${Math.random()}`]: {} }); // 传一个 dummy message + }); + // 注:等待时,或已经触发了其他 navigate + newUrl = getUrl?.(); // 再次取得当前 location.href + } + if (newUrl === lastUrl) return; + lastUrl = newUrl; + const dispatchEvent = new UrlChangeEvent("urlchange"); + dispatchEvent.url = (destinationUrl || newUrl) as string; // info.url + dispatcher(dispatchEvent); + }; + win.navigation?.addEventListener("navigate", handler, false); + return handler; +}; + // 构建沙盒上下文 export const createContext = ( scriptRes: TScriptInfo, @@ -102,6 +142,10 @@ export const createContext = ( } } context.unsafeWindow = window; + if (scriptGrants.has("window.onurlchange") && context.onurlchange === undefined) { + context.onurlchange = null; + attachNavigateHandler(window as any); + } return context; }; @@ -389,7 +433,7 @@ export const createProxyContext = (context // 把 GM context物件的 window属性内容移至exposedWindow // 由于目前只有 window.close, window.open, window.onurlchange, 不需要循环 window - const cWindow = context.window; + const cWindow = context.window as (Window & Record) | undefined; // @grant window.close if (cWindow?.close) { @@ -403,7 +447,7 @@ export const createProxyContext = (context // @grant window.onurlchange if (cWindow?.onurlchange === null) { - // 目前 TM 只支援 null. ScriptCat不需要grant预设启用? + // 目前 TM 只支援 null mySandbox.onurlchange = null; } From 8070269787888a474479aee648e2ef42ac9f9d1f Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Sat, 28 Mar 2026 10:59:54 +0900 Subject: [PATCH 2/7] =?UTF-8?q?=E6=95=B4=E7=90=86=E6=A1=A3=E6=A1=88?= =?UTF-8?q?=E6=9E=B6=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/service/content/create_context.ts | 41 +------------------ .../content/gm_api/navigation_handle.ts | 39 ++++++++++++++++++ 2 files changed, 40 insertions(+), 40 deletions(-) create mode 100644 src/app/service/content/gm_api/navigation_handle.ts diff --git a/src/app/service/content/create_context.ts b/src/app/service/content/create_context.ts index 353a92af8..0fd494699 100644 --- a/src/app/service/content/create_context.ts +++ b/src/app/service/content/create_context.ts @@ -7,46 +7,7 @@ import { protect } from "./gm_api/gm_context"; import { isEarlyStartScript } from "./utils"; import { ListenerManager } from "./listener_manager"; import { createGMBase } from "./gm_api/gm_api"; - -class UrlChangeEvent extends Event { - url: string; - constructor(type: string, eventInitDict?: EventInit) { - super(type, eventInitDict); - this.url = ""; - } -} - -// Chrome 102+, Firefox 147+ -// https://developer.chrome.com/docs/web-platform/navigation-api -// https://developer.mozilla.org/en-US/docs/Web/API/Navigation_API#browser_compatibility -const attachNavigateHandler = (win: Window & { navigation: EventTarget }) => { - // 以 location.href 判断避免 replaceState/pushState 重复执行重复触发 - const location = win.location; - const getUrl = Object.getOwnPropertyDescriptor(location, "href")?.get?.bind(location); - const dispatcher = win.dispatchEvent.bind(win); - let lastUrl = getUrl?.(); - const handler = async (ev: Event): Promise => { - let newUrl = getUrl?.(); // 取得当前 location.href - const destinationUrl = (ev as any).destination?.url; - if (destinationUrl !== newUrl && newUrl === lastUrl) { - // 某些情况,location.href 未更新就触发了 - // 用 postMessage 推迟到下一个 marcoEvent 阶段 - await new Promise((resolve) => { - window.addEventListener("message", resolve, { once: true }); - window.postMessage({ [`${Math.random()}`]: {} }); // 传一个 dummy message - }); - // 注:等待时,或已经触发了其他 navigate - newUrl = getUrl?.(); // 再次取得当前 location.href - } - if (newUrl === lastUrl) return; - lastUrl = newUrl; - const dispatchEvent = new UrlChangeEvent("urlchange"); - dispatchEvent.url = (destinationUrl || newUrl) as string; // info.url - dispatcher(dispatchEvent); - }; - win.navigation?.addEventListener("navigate", handler, false); - return handler; -}; +import { attachNavigateHandler } from "./gm_api/navigation_handle"; // 构建沙盒上下文 export const createContext = ( diff --git a/src/app/service/content/gm_api/navigation_handle.ts b/src/app/service/content/gm_api/navigation_handle.ts new file mode 100644 index 000000000..4e9502fbd --- /dev/null +++ b/src/app/service/content/gm_api/navigation_handle.ts @@ -0,0 +1,39 @@ +class UrlChangeEvent extends Event { + url: string; + constructor(type: string, eventInitDict?: EventInit) { + super(type, eventInitDict); + this.url = ""; + } +} + +// Chrome 102+, Firefox 147+ +// https://developer.chrome.com/docs/web-platform/navigation-api +// https://developer.mozilla.org/en-US/docs/Web/API/Navigation_API#browser_compatibility +export const attachNavigateHandler = (win: Window & { navigation: EventTarget }) => { + // 以 location.href 判断避免 replaceState/pushState 重复执行重复触发 + const location = win.location; + const getUrl = Object.getOwnPropertyDescriptor(location, "href")?.get?.bind(location); + const dispatcher = win.dispatchEvent.bind(win); + let lastUrl = getUrl?.(); + const handler = async (ev: Event): Promise => { + let newUrl = getUrl?.(); // 取得当前 location.href + const destinationUrl = (ev as any).destination?.url; + if (destinationUrl !== newUrl && newUrl === lastUrl) { + // 某些情况,location.href 未更新就触发了 + // 用 postMessage 推迟到下一个 marcoEvent 阶段 + await new Promise((resolve) => { + window.addEventListener("message", resolve, { once: true }); + window.postMessage({ [`${Math.random()}`]: {} }); // 传一个 dummy message + }); + // 注:等待时,或已经触发了其他 navigate + newUrl = getUrl?.(); // 再次取得当前 location.href + } + if (newUrl === lastUrl) return; + lastUrl = newUrl; + const dispatchEvent = new UrlChangeEvent("urlchange"); + dispatchEvent.url = (destinationUrl || newUrl) as string; // info.url + dispatcher(dispatchEvent); + }; + win.navigation?.addEventListener("navigate", handler, false); + return handler; +}; From 7a5ba76c3e861558782a48202a82a50e7e0b4373 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Sat, 28 Mar 2026 11:18:34 +0900 Subject: [PATCH 3/7] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=20seq=E5=88=A4=E6=96=AD?= =?UTF-8?q?=20=E9=81=BF=E5=85=8D=E9=87=8D=E5=A4=8D=E8=A7=A6=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../content/gm_api/navigation_handle.ts | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/app/service/content/gm_api/navigation_handle.ts b/src/app/service/content/gm_api/navigation_handle.ts index 4e9502fbd..13942e6bf 100644 --- a/src/app/service/content/gm_api/navigation_handle.ts +++ b/src/app/service/content/gm_api/navigation_handle.ts @@ -11,29 +11,31 @@ class UrlChangeEvent extends Event { // https://developer.mozilla.org/en-US/docs/Web/API/Navigation_API#browser_compatibility export const attachNavigateHandler = (win: Window & { navigation: EventTarget }) => { // 以 location.href 判断避免 replaceState/pushState 重复执行重复触发 - const location = win.location; - const getUrl = Object.getOwnPropertyDescriptor(location, "href")?.get?.bind(location); - const dispatcher = win.dispatchEvent.bind(win); + const loc = win.location; + const getUrl = Object.getOwnPropertyDescriptor(loc, "href")?.get?.bind(loc); + const dispatch = win.dispatchEvent.bind(win); let lastUrl = getUrl?.(); + let callSeq = 0; const handler = async (ev: Event): Promise => { + callSeq = callSeq > 512 ? 1 : callSeq + 1; + const seq = callSeq; let newUrl = getUrl?.(); // 取得当前 location.href - const destinationUrl = (ev as any).destination?.url; - if (destinationUrl !== newUrl && newUrl === lastUrl) { + const destUrl = (ev as any).destination?.url; + if (destUrl !== newUrl && newUrl === lastUrl) { // 某些情况,location.href 未更新就触发了 // 用 postMessage 推迟到下一个 marcoEvent 阶段 await new Promise((resolve) => { window.addEventListener("message", resolve, { once: true }); window.postMessage({ [`${Math.random()}`]: {} }); // 传一个 dummy message }); - // 注:等待时,或已经触发了其他 navigate + if (seq !== callSeq) return; // 等待时,或许已经触发了其他 navigate newUrl = getUrl?.(); // 再次取得当前 location.href } if (newUrl === lastUrl) return; lastUrl = newUrl; - const dispatchEvent = new UrlChangeEvent("urlchange"); - dispatchEvent.url = (destinationUrl || newUrl) as string; // info.url - dispatcher(dispatchEvent); + const urlChangeEv = new UrlChangeEvent("urlchange"); + urlChangeEv.url = (destUrl || newUrl) as string; // info.url + dispatch(urlChangeEv); }; win.navigation?.addEventListener("navigate", handler, false); - return handler; }; From 6359474f8253dd98e0a73a5a8b1561433fa816a0 Mon Sep 17 00:00:00 2001 From: wangyizhi Date: Sun, 29 Mar 2026 20:20:04 +0800 Subject: [PATCH 4/7] Update src/app/service/content/gm_api/navigation_handle.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/app/service/content/gm_api/navigation_handle.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/service/content/gm_api/navigation_handle.ts b/src/app/service/content/gm_api/navigation_handle.ts index 13942e6bf..8c4442839 100644 --- a/src/app/service/content/gm_api/navigation_handle.ts +++ b/src/app/service/content/gm_api/navigation_handle.ts @@ -23,7 +23,7 @@ export const attachNavigateHandler = (win: Window & { navigation: EventTarget }) const destUrl = (ev as any).destination?.url; if (destUrl !== newUrl && newUrl === lastUrl) { // 某些情况,location.href 未更新就触发了 - // 用 postMessage 推迟到下一个 marcoEvent 阶段 + // 用 postMessage 推迟到下一个 macrotask 阶段 await new Promise((resolve) => { window.addEventListener("message", resolve, { once: true }); window.postMessage({ [`${Math.random()}`]: {} }); // 传一个 dummy message From c36ee37153f5e114498e515103c6658cb8f7860a Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Mon, 30 Mar 2026 08:05:38 +0900 Subject: [PATCH 5/7] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../content/gm_api/navigation_handle.ts | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/src/app/service/content/gm_api/navigation_handle.ts b/src/app/service/content/gm_api/navigation_handle.ts index 8c4442839..07b655526 100644 --- a/src/app/service/content/gm_api/navigation_handle.ts +++ b/src/app/service/content/gm_api/navigation_handle.ts @@ -6,13 +6,31 @@ class UrlChangeEvent extends Event { } } +let attached = false; + +const getPropGetter = (obj: any, key: string) => { + // 避免直接 obj[key] 读取。或会被 hack + let t = obj; + let pd: PropertyDescriptor | undefined; + while (t) { + pd = Object.getOwnPropertyDescriptor(t, key); + if (pd) break; + t = Object.getPrototypeOf(t); + } + if (pd) { + return pd?.get?.bind(obj); + } +}; + // Chrome 102+, Firefox 147+ // https://developer.chrome.com/docs/web-platform/navigation-api // https://developer.mozilla.org/en-US/docs/Web/API/Navigation_API#browser_compatibility export const attachNavigateHandler = (win: Window & { navigation: EventTarget }) => { + if (attached) return; + attached = true; // 以 location.href 判断避免 replaceState/pushState 重复执行重复触发 const loc = win.location; - const getUrl = Object.getOwnPropertyDescriptor(loc, "href")?.get?.bind(loc); + const getUrl = getPropGetter(loc, "href"); const dispatch = win.dispatchEvent.bind(win); let lastUrl = getUrl?.(); let callSeq = 0; @@ -26,7 +44,7 @@ export const attachNavigateHandler = (win: Window & { navigation: EventTarget }) // 用 postMessage 推迟到下一个 macrotask 阶段 await new Promise((resolve) => { window.addEventListener("message", resolve, { once: true }); - window.postMessage({ [`${Math.random()}`]: {} }); // 传一个 dummy message + window.postMessage({ [`${Math.random()}`]: {} }, "*"); // 传一个 dummy message }); if (seq !== callSeq) return; // 等待时,或许已经触发了其他 navigate newUrl = getUrl?.(); // 再次取得当前 location.href From cc72cc48749aa9f33f061478a4c2ee209537aba1 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Mon, 30 Mar 2026 22:50:14 +0900 Subject: [PATCH 6/7] Update navigation_handle.ts --- src/app/service/content/gm_api/navigation_handle.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/service/content/gm_api/navigation_handle.ts b/src/app/service/content/gm_api/navigation_handle.ts index 07b655526..0ce1f08de 100644 --- a/src/app/service/content/gm_api/navigation_handle.ts +++ b/src/app/service/content/gm_api/navigation_handle.ts @@ -43,8 +43,8 @@ export const attachNavigateHandler = (win: Window & { navigation: EventTarget }) // 某些情况,location.href 未更新就触发了 // 用 postMessage 推迟到下一个 macrotask 阶段 await new Promise((resolve) => { - window.addEventListener("message", resolve, { once: true }); - window.postMessage({ [`${Math.random()}`]: {} }, "*"); // 传一个 dummy message + self.addEventListener("message", resolve, { once: true }); + self.postMessage({ [`${Math.random()}`]: {} }, "*"); // 传一个 dummy message }); if (seq !== callSeq) return; // 等待时,或许已经触发了其他 navigate newUrl = getUrl?.(); // 再次取得当前 location.href From e26af0a85919fe5165bd8d3f14ca917ea6720156 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Tue, 31 Mar 2026 04:36:59 +0900 Subject: [PATCH 7/7] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/service/content/create_context.ts | 27 ++++++++++++++++--- .../content/gm_api/navigation_handle.ts | 13 +++++---- 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/src/app/service/content/create_context.ts b/src/app/service/content/create_context.ts index 0fd494699..999a78b3a 100644 --- a/src/app/service/content/create_context.ts +++ b/src/app/service/content/create_context.ts @@ -7,7 +7,7 @@ import { protect } from "./gm_api/gm_context"; import { isEarlyStartScript } from "./utils"; import { ListenerManager } from "./listener_manager"; import { createGMBase } from "./gm_api/gm_api"; -import { attachNavigateHandler } from "./gm_api/navigation_handle"; +import { attachNavigateHandler, type UrlChangeEvent } from "./gm_api/navigation_handle"; // 构建沙盒上下文 export const createContext = ( @@ -375,6 +375,23 @@ export const createProxyContext = (context }, }; + // @grant window.onurlchange + if (context?.onurlchange === null) { + let currentValue: ((this: GlobalEventHandlers, ev: UrlChangeEvent) => any) | null = null; + ownDescs.onurlchange = { + enumerable: true, + configurable: true, + get() { + return currentValue; + }, + set(nv) { + if (typeof nv !== "function") nv = null; + currentValue = nv; + return true; + }, + }; + } + // 把初始Copy加上特殊变量后,生成一份新Copy mySandbox = Object.create(Object.getPrototypeOf(sharedInitCopy), ownDescs); @@ -407,9 +424,11 @@ export const createProxyContext = (context } // @grant window.onurlchange - if (cWindow?.onurlchange === null) { - // 目前 TM 只支援 null - mySandbox.onurlchange = null; + if (context?.onurlchange === null) { + const handle = function (this: Window & Record, e: UrlChangeEvent) { + this.onurlchange?.(e); + } as EventListener; + (window).addEventListener("urlchange", handle.bind(mySandbox), false); } // 从网页 console 隔离出来的沙盒 console diff --git a/src/app/service/content/gm_api/navigation_handle.ts b/src/app/service/content/gm_api/navigation_handle.ts index 0ce1f08de..f64b581d4 100644 --- a/src/app/service/content/gm_api/navigation_handle.ts +++ b/src/app/service/content/gm_api/navigation_handle.ts @@ -1,8 +1,8 @@ -class UrlChangeEvent extends Event { - url: string; - constructor(type: string, eventInitDict?: EventInit) { - super(type, eventInitDict); - this.url = ""; +export class UrlChangeEvent extends Event { + readonly url: string; + constructor(type: string, url: string) { + super(type); + this.url = url; } } @@ -51,8 +51,7 @@ export const attachNavigateHandler = (win: Window & { navigation: EventTarget }) } if (newUrl === lastUrl) return; lastUrl = newUrl; - const urlChangeEv = new UrlChangeEvent("urlchange"); - urlChangeEv.url = (destUrl || newUrl) as string; // info.url + const urlChangeEv = new UrlChangeEvent("urlchange", (destUrl || newUrl) as string); dispatch(urlChangeEv); }; win.navigation?.addEventListener("navigate", handler, false);