Skip to content

[Proposal] 在原沙盒基礎上修正Firefox Spec問題 #1701

Description

@cyfung1031

背景问题

本issue 在原沙盒基礎上修正Firefox Spec問題

考慮但不一定 revert #1692
PR #1692 沒有實際能重視問題的針對性腳本,也沒有TDD的測試代碼
這樣對程式員或agent來說都很危險

#1697 給出了更進一步的問題,也有了針對性腳本
可是PR的代碼更動不一定是最理想的


agent 或程式員應參考以下分析進一步在代碼層面深入考慮修改部份,以相容於Firefox Spec但又不影響Chrome Spec.
以下分析或具体方案僅用於參考。不能視為必須遵循。Agent 或程式員應制定詳細方案包括代碼測試驗收標準等才能正式提交PR。

具体方案

我把你上傳的 scriptcat-1.5.0-beta.2.zip 解開後,核心執行鏈已經追清楚了。這次 Firefox 的 10 個失敗不是單點 bug,而是 ScriptCat 現有 sandbox 建模隱含假設了 global === window;這個假設在 Chrome USER_SCRIPT world 大致成立,在 Firefox 明確不成立。

Mozilla 對這件事其實寫得很直接:Firefox content script 中 globalThis 是一個不同於 window、並繼承自 window 的物件。(MDN Web Docs) Firefox USER_SCRIPT 的實作也是建立 Cu.Sandbox,並指定 sandboxPrototype: contentWindowwantXrays: true。(Searchfox)

所以目標不應是「讓 Firefox 自己變成 Chrome」,而應是讓 ScriptCat 明確處理三個不同物件:

realmGlobal = globalThis
    Firefox: Cu.Sandbox global
    Chrome: 通常近似 Window global

hostWindow = window
    真正提供 DOM / Window / EventTarget 的 Window

scriptGlobal = mySandbox
    ScriptCat 每個 userscript 自己看到的 synthetic global

userscript 最終應看到:

window ──────┐
self ────────┼──> scriptGlobal
globalThis ──┘

unsafeWindow ───> hostWindow

而不是現在 Firefox 下的混合狀態。


一、你這份 beta.2 的真正問題在哪裡

最重要的是 src/app/service/content/create_context.ts

問題 1:整套 descriptor 系統從 global 開始

beta.2 約 172 行:

const initOwnDescs = Object.getOwnPropertyDescriptors(global);

接著約 186 行:

getAllPropertyDescriptors(global, ([key, desc]) => {

這在 Chrome 比較碰巧,因為 global/window 的差異小。

Firefox USER_SCRIPT 下:

globalThis !== window

所以這裡其實正在遍歷:

Cu.Sandbox global
   ↓ prototype
Xray Window
   ↓
...

但後面的程式把整條鏈都當成「同一個 Window global」。

這是根本錯誤。


二、10 個測試為什麼逐項失敗

你現在 alias 處理在約 360 行:

for (const key of ["window", "self", "globalThis", "top", "parent", "frames"]) {
  const desc = ownDescs[key];

  if (desc?.value === global) {
    desc.get = function () {
      return mySandbox;
    };

這段實際只對「值剛好等於 Firefox sandbox global」的屬性有效。

Firefox 中大致是:

globalThis -> sandbox global
window     -> contentWindow
self       -> contentWindow

因此:

globalThis -> mySandbox
window     -> host Window
self       -> host Window

於是直接造成:

測試 根因
window === globalThis window 沒被映射到 mySandbox
self === globalThis 同上
window.GM_getValue GM API 在 mySandbox,但 window 指到 hostWindow
self.xglobalThis.x 寫在 hostWindow,讀的是 mySandbox

所以 ① 的四個失敗,本質上是同一件事。


三、Node / NodeFilter / XMLHttpRequest 的 bug 更具體

你們其實已經知道 constructor 不能亂 bind。

shouldFnBind() 有:

if ("prototype" in f) return false;

而且還有:

// 要求函数名字小写字头 能筛选掉 NodeFilter 之类 Interface

這個方向是對的。

但是後面約 205–214 行又把保護完全繞掉了:

} else if (!(key in initOwnDescs) && !Object.hasOwn(global, key)) {
  if (!protoBaseDescs[key]) {
    if (typeof value === "function") {
      const boundValue = value.bind(global);
      protoBaseDescs[key] = {
        ...desc,
        value: boundValue,
      };

也就是:

shouldFnBind(NodeFilter) -> false
                         ↓
fallback
typeof NodeFilter === function
                         ↓
NodeFilter.bind(global)

結果還是 bind。

而:

const B = NodeFilter.bind(...);

B.SHOW_TEXT
// undefined

同樣:

XMLHttpRequest.bind(...).DONE
// undefined

Node.bind(...).ELEMENT_NODE
// undefined

HTMLBodyElement.bind(...).prototype
// undefined

這就是你的 ②。

所以這裡不是抽象性的「可能有問題」,而是我會直接判斷:

protoBaseDescstypeof value === "function" => bind() 必須取消。


四、EventTarget 也綁錯 receiver 了

現在很多地方用:

value.bind(global)

例如約 199 行。

Firefox 的 global 是 Cu.Sandbox object。

但是:

EventTarget.prototype.addEventListener
EventTarget.prototype.removeEventListener
EventTarget.prototype.dispatchEvent

需要作用的 EventTarget 是:

window

不是 Cu.Sandbox global。

所以理想操作是:

Reflect.apply(window.addEventListener, window, args)

而不是:

window.addEventListener.bind(global)

這也解釋了你的:

globalThis.addEventListener(...)
globalThis.dispatchEvent(...)
globalThis.removeEventListener(...)

為什麼整個 lifecycle 壞掉。


五、還有幾個 beta.2 裡很值得一起修的點

createFuncWrapper() 現在是:

const ret = f.call(global);
if (ret === global) return mySandbox;

Firefox 應該用 window 作 Window getter receiver,而且應判斷:

ret === hostWindow

而不是:

ret === realmGlobal

否則 self/parent/top/frames 都會出問題。

createEventProp() 也全部在:

global.addEventListener(...)
global.removeEventListener(...)

應該改成 hostWindow

PseudoWindowPrototype 現在又取:

global[Symbol.toStringTag]
global.constructor
global.__proto__

如果想模擬 Window,來源也應該是 hostWindow,不是 Firefox Cu.Sandbox。


六、不能簡單做 global -> window 全域替換

這點非常重要。

直覺 patch 可能是:

const global = window;

或把所有:

global

改成:

window

我不建議。

原因是 Firefox sandbox global 裡還有屬於 userscript JS realm 自己的 ECMAScript intrinsics

例如:

Object
Array
Promise
RegExp
Number
Math
Proxy
Error

這些最好保留 Firefox sandbox realm 自己的版本。

尤其 ScriptCat 現在已經有:

RegExp.$1

相關相容性測試。

如果全部改成 Xray window.RegExp / window.Number / window.Math,會引入另一批跨 realm / Xray 問題。

有意思的是,我也看了目前 ScriptCat main。在你這份 beta.2 之後,上游已經開始嘗試做兩趟:

collectPropertyDescriptors(global);
window !== global && collectPropertyDescriptors(window);

程式註解也已明確認知 Firefox global/window 是兩個 realm。

方向是對的,但我認為現在 main 的實作仍然沒有徹底解決問題,因為它依舊:

getAllPropertyDescriptors(global)

也就是第一趟會沿 global 的 prototype 鑽進 window,然後卻把找到的方法:

value.bind(root)

綁到 global

而且 fallback 仍然:

typeof value === "function"
    ? value.bind(root)

最後 alias 還是:

desc.value === global

所以「兩趟掃描」本身還不夠。


七、方案 A:保留現在架構,改成真正的 dual-source descriptor model

這是我最推薦的近期修法

不要重寫整個 sandbox,保留現在:

sharedInitCopy
→ ownDescs
→ mySandbox
→ with(this.$)

但把 globals 收集重構為兩個來源。

核心應先明確定義:

const realmGlobal = globalThis as typeof globalThis & Record<PropertyKey, any>;
const hostWindow = window as Window & Record<PropertyKey, any>;

const splitGlobal = realmGlobal !== hostWindow;

不要判 Firefox:

isFirefox()

而是 feature detection:

globalThis !== window

realmGlobal 只取 own properties

這是很關鍵的細節。

不要:

getAllPropertyDescriptors(realmGlobal)

應該:

const realmOwnDescs =
  Object.getOwnPropertyDescriptors(realmGlobal);

只取 own。

因為 Firefox 的:

realmGlobal
  prototype -> hostWindow

如果你遍歷 prototype chain,你馬上又把兩個 realm 混回一起。

realm own 用來取得:

Object
Array
Promise
RegExp
Proxy
Error
structuredClone
Firefox USER_SCRIPT own globals

然後另外走 hostWindow prototype chain

collectWindowDescriptors(hostWindow);

這一趟專門取得:

Node
NodeFilter
XMLHttpRequest
HTMLElement
HTMLBodyElement
document/location accessors
setTimeout
addEventListener
dispatchEvent
...

而且 callback 要把 descriptor owner 傳出來。

現在:

callback([key, desc])

不夠。

至少應變成:

callback({
  key,
  desc,
  owner,
  root,
});

因為你必須知道:

這個 function 到底是哪個物件的 method?

而不是只知道「這次是從 global/root 開始掃」。


八、方案 A 的 function 分類我會這樣改

目前最大的錯是:

typeof function => bind

我建議拆成:

function isInterfaceLike(key: string, value: unknown) {
  if (typeof value !== "function") return false;

  // Node / XMLHttpRequest / HTMLBodyElement...
  if ("prototype" in value) return true;

  // NodeFilter 等瀏覽器 interface
  const c = key.charCodeAt(0);
  if (c >= 65 && c <= 90) return true;

  // 有靜態成員的 callable interface 也盡量不要 bind
  const extraKeys = Reflect.ownKeys(value).filter(
    k => k !== "length" && k !== "name" &&
         k !== "caller" && k !== "arguments"
  );

  return extraKeys.length > 0;
}

然後:

if (typeof value === "function") {
  if (isInterfaceLike(key, value)) {
    // Node、XHR、HTMLBodyElement:原封不動
    result[key] = { ...desc };
  } else if (shouldBindWindowMethod(key, value, owner)) {
    result[key] = {
      ...desc,
      value: bindWindowMethod(value, key),
    };
  } else {
    result[key] = { ...desc };
  }
}

最重要的是:

protoBaseDescs[key] = { ...desc };

不能再有:

value.bind(...)

的 unconditional fallback。


九、EventTarget 我甚至更偏好 forwarding,而不是 bind

最低改動當然可以:

value.bind(hostWindow)

但跨 Firefox compartment/Xray,我會更傾向:

const makeWindowMethod = (fn: Function, name: string) => {
  const wrapped = function (this: unknown, ...args: unknown[]) {
    return Reflect.apply(fn, hostWindow, args);
  };

  Object.defineProperty(wrapped, "name", {
    value: name,
    configurable: true,
  });

  return wrapped;
};

於是:

sandbox.addEventListener(...)

實際:

Reflect.apply(
  EventTarget.prototype.addEventListener,
  hostWindow,
  args
)

removeEventListenerdispatchEvent 同理。

這能確保三個 API 永遠作用在:

同一個 hostWindow EventTarget

你的第 10 個 lifecycle test 就自然通過。

.bind(hostWindow) 的優點

修改小、效能好、現有測試可能已依賴 "bound xxx" name。

forwarding wrapper 的優點

receiver 完全可控,跨 realm 行為更清楚,也比較容易日後特殊處理。

所以近期 backport 我可以接受 bind(hostWindow);長期我偏 forwarding。


十、alias 不應再靠 descriptor value 猜

現在:

if (desc.value === global)

我建議整段刪掉,直接明確定義。

window/self/globalThis

for (const key of ["window", "self", "globalThis"] as const) {
  ownDescs[key] = {
    configurable: true,
    enumerable: ownDescs[key]?.enumerable ?? true,
    get() {
      return mySandbox;
    },
  };
}

這樣無論 Firefox host 怎麼設計:

window === globalThis
self === globalThis

userscript observable semantics 中永遠成立。

這不是修改 Firefox global。

只是 ScriptCat 的 userscript global façade。


十一、top / parent / frames 不要無腦等於 sandbox

它們稍微不同。

top-level page:

window.top === window
window.parent === window
window.frames === window

所以 userscript sandbox 應該得到:

sandbox.top === sandbox
sandbox.parent === sandbox
sandbox.frames === sandbox

但是 iframe 中:

window.parent !== window

不能把 parent 永遠改成 sandbox。

我會做:

const wrapWindowReference = (key: "top" | "parent" | "frames") => ({
  configurable: true,
  enumerable: ownDescs[key]?.enumerable ?? true,

  get() {
    const value = Reflect.get(hostWindow, key, hostWindow);

    return value === hostWindow
      ? mySandbox
      : value;
  },
});

這才是真正跨瀏覽器一致。


十二、createEventProp 也要直接換 receiver

beta.2:

global.removeEventListener(...)
global.addEventListener(...)

應改:

hostWindow.removeEventListener(...)
hostWindow.addEventListener(...)

createFuncWrapper()

const ret = f.call(global);
if (ret === global) return mySandbox;

應改成:

const ret = Reflect.apply(f, hostWindow, []);

if (ret === hostWindow) {
  return mySandbox;
}

return ret;

這兩個屬於低風險、高收益修改。


十三、方案 A 對你的 10 個測試

測試 修復機制
window === globalThis 三個 alias 明確回傳 mySandbox
self === globalThis 同上
window.GM_getValue window → mySandbox,GM API 已複製到 mySandbox
self.xglobalThis.x 兩者同一物件
Node.ELEMENT_NODE Node 原 interface,不 bind
NodeFilter.SHOW_TEXT NodeFilter 原 interface,不 bind
XMLHttpRequest.DONE XHR constructor 不 bind
HTMLBodyElement.prototype constructor identity 保留
globalThis.addEventListener sandbox method forwarding 到 hostWindow
add/dispatch/remove 全部使用同一 hostWindow

這套不是為 10 個 case 寫 allowlist,而是修正 underlying object model。


十四、方案 B:直接改成 Violentmonkey 式 lazy Proxy

這是我認為長期最乾淨的方向。

現在 ScriptCat 是:

啟動時把大量 Window descriptors 複製一份
                 ↓
           sharedInitCopy
                 ↓
             mySandbox

可以改成:

local GM/global properties
           ↓
      Proxy wrapper
       ↙       ↘
realmGlobal   hostWindow

簡化概念:

const local = Object.create(null);

Object.assign(local, grantedGMAPIs);

let wrapper: any;

wrapper = new Proxy(local, {
  get(_, key) {
    if (
      key === "window" ||
      key === "self" ||
      key === "globalThis"
    ) {
      return wrapper;
    }

    if (Reflect.has(local, key)) {
      return Reflect.get(local, key);
    }

    const desc = resolveGlobalDescriptor(key);

    if (!desc) return undefined;

    return materializeDescriptorValue(
      key,
      desc,
      realmGlobal,
      hostWindow
    );
  },

  set(_, key, value) {
    local[key] = value;
    return true;
  },

  has(_, key) {
    return key in local ||
      key in realmGlobal ||
      key in hostWindow;
  },

  ownKeys() {
    return unionKeys(local, realmGlobal, hostWindow);
  },
});

這基本就是 VM 的方向。

優勢

不用在頁面初始化時複製幾百個 property。

Firefox global/window 分裂可以自然表示。

未來瀏覽器新增 API,不需要 ScriptCat 更新 snapshot。

可以 lazy cache descriptor。

更容易明確實現:

ES intrinsic -> realmGlobal
DOM interface -> hostWindow
window alias -> wrapper
GM -> local

也能處理頁面 frame index、late-added globals 等。

缺點

這不是小 patch。

Proxy 必須處理:

get
set
has
ownKeys
getOwnPropertyDescriptor
defineProperty
deleteProperty

否則:

Object.keys(window)
Object.getOwnPropertyNames(window)
"x" in window
delete window.x
Object.defineProperty(window, ...)

可能跟 TM/VM 不一致。

還會碰 Proxy invariant。

另外很多老 userscript/library 對 window 很敏感,所以要跑大量 regression。

我的判斷

不要拿這個當 beta.2 緊急修。

但如果 ScriptCat 的目標真的是:

Chrome / Firefox / 未來其他 browser 的 userscript sandbox 長期完全一致

那最終應該往這個方向。


十五、方案 C:只做 compatibility overlay,最快能發布

如果現在目標是很快出一個 Firefox 修復版,可以不動 descriptor engine 主體。

直接在 createProxyContext() 建好 mySandbox 之前/之後覆蓋:

window
self
globalThis

Node
NodeFilter
XMLHttpRequest
HTMLBodyElement

addEventListener
removeEventListener
dispatchEvent

例如:

defineSelfAlias("window");
defineSelfAlias("self");
defineSelfAlias("globalThis");

for (const key of [
  "Node",
  "NodeFilter",
  "XMLHttpRequest",
  "HTMLBodyElement",
]) {
  ownDescs[key] =
    Object.getOwnPropertyDescriptor(hostWindow, key) ??
    { value: hostWindow[key], writable: true, configurable: true };
}

for (const key of [
  "addEventListener",
  "removeEventListener",
  "dispatchEvent",
]) {
  ownDescs[key] = {
    configurable: true,
    value: hostWindow[key].bind(hostWindow),
  };
}

優點

改動非常小。

可以快速驗證你的 10 個 testcase。

風險集中。

適合作為 beta hotfix。

缺點

它只是補表面。

下一個 userscript 可能馬上撞:

requestAnimationFrame
postMessage
matchMedia
getComputedStyle
queueMicrotask
CSS
DOMParser
MutationObserver
FileReader
...

然後繼續加 allowlist。

所以如果你的要求是:

「所有 userscript Chrome / Firefox 一致」

我不會把 C 當最終方案。


十六、方案 D:直接修改 Firefox 的 globalThis sandbox object

另一個看起來很誘人的方案:

if (globalThis !== window) {
  Object.defineProperty(globalThis, "window", {
    value: globalThis,
  });

  Object.defineProperty(globalThis, "self", {
    value: globalThis,
  });
}

甚至把:

addEventListener
removeEventListener
dispatchEvent

也掛到 Firefox sandbox global。

這樣真正的:

window === globalThis

可能就能被 shadow 成 true。

優點

@grant none 都可能一起得到正常 alias。

new Function() 產生的真正 global scope 也比較接近 Chrome。

wrapper 的複雜度降低。

缺點很嚴重

Firefox USER_SCRIPT world 是 ScriptCat runtime 自己也在使用的 global。

你是在修改 browser-created execution realm,不是單一 userscript sandbox。

多個 ScriptCat userscript 可能共享這個 USER_SCRIPT world。

任何 global patch 都可能造成 scripts 間互相污染。

globalThis 本體仍不是 EventTarget/Window,只是偽裝。

getter、brand check、prototype、instanceof 等還是會暴露真相。

未來 Firefox 改 sandbox implementation,風險很大。

而且你們現在本來就有 mySandbox 作 per-script isolation,繞過它反而退步。

我的評價

不建議。

除非只作非常小的 runtime internal shim,而且絕對不讓 userscript直接修改。


十七、方案 E:Firefox content 全部改跑 MAIN world

這個會讓:

window === globalThis

瞬間「正常」。

但我會直接排除。

runtime.ts 現在很清楚:

scriptcat-inject -> world: "MAIN"
scriptcat-content -> world: "USER_SCRIPT"

這個架構是合理的。

Firefox USER_SCRIPT 是 sandbox world;Firefox 本身也是這麼定義的。(Searchfox)

如果 @inject-into content 改 MAIN:

頁面可以污染 prototype
頁面可以窺探 globals
GM API bridge 安全邊界惡化
@inject-into content 名存實亡
unsafeWindow 語義消失

相當於為解 compatibility 問題把 isolation 拆掉。

不值得。


十八、還有一件事:@grant none

這部分需要單獨決策。

現在 exec_script.ts 約 90 行:

this.execContext =
  sandboxContext
    ? createProxyContext(sandboxContext)
    : global;

所以 @grant none 不建立 ScriptCat sandbox。

在 Firefox USER_SCRIPT world 裡,即使 ScriptCat 完全不介入:

globalThis !== window

仍是 Firefox host semantics。MDN 也明確指出這件事。(MDN Web Docs)

因此如果你們的產品目標真的定義成:

任何 @inject-into content userscript,包括 @grant none,都必須看到 Chrome-like window === self === globalThis

那方案 A 只修 granted scripts 還不夠。

你們需要讓 @grant none 也經過一層沒有 GM 權限、但有 global compatibility 的 scope façade

不是完整 sandbox,只處理 browser-global semantics。

例如概念上:

const compatScope = createContentCompatScope({
  realmGlobal,
  hostWindow,
  grants: null,
  isolateWrites: false, // 這個語義需你們決定
});

再:

scriptFunc.call(compatScope, compatScope, scriptName);

這裡最大的產品決策是:

foo = 1;
window.foo = 1;
globalThis.foo = 1;

@grant none + content 下究竟應寫到哪個 realm。

這不能靠 patch 猜。

要先拿 Chrome ScriptCat 現有行為作 reference,再要求 Firefox完全 replicate。


十九、我最推薦的落地順序

方案 改動量 10 tests 長期通用性 風險 建議
C:10-case overlay 很小 緊急 hotfix
A:dual-source descriptor 中低 現在最值得做
B:VM-style Proxy 最高 中高 長期重構
D:patch Firefox global 部分 不建議
E:MAIN world ✅表面上 很低 很高 排除

如果是我負責這個版本,我會:

beta.2 fix
    ↓
實作 A
    ↓
保留現有 sharedInitCopy 架構
    ↓
所有 browser 都走 realmGlobal + hostWindow 雙來源
    ↓
Firefox 用 globalThis !== window 自動啟用 split-global path
    ↓
真 Firefox E2E
    ↓
之後再考慮 B

二十、方案 A 我會具體改哪些地方

這份 beta.2,我會集中改 create_context.ts,而不是到處 patch:

172  initOwnDescs
186  getAllPropertyDescriptors(global)
199  value.bind(global)
205  protoBaseDescs fallback
229  getter.bind(global)
230  setter.bind(global)
250  Symbol.toStringTag
256  constructor
263  __proto__
301  f.call(global)
302  ret === global
317  global.removeEventListener
339  global.removeEventListener
343  global.addEventListener
360  alias loop

另外 exec_script.ts:90 要單獨決定 @grant none 的跨瀏覽器 policy。

unsafeWindow = window 這一行反而是對的,不應改。


二十一、測試層目前也有盲點

現在 create_context.test.ts 已經有:

expect(sandbox.window).toBe(sandbox);
expect(sandbox.self).toBe(sandbox);
expect(sandbox.globalThis).toBe(sandbox);

以及 NodeFilter、Illegal invocation 等測試。

問題是這些測試跑在 happy-dom / Node test global 中。

它沒有真的模擬:

Firefox:
globalThis !== window
Object.getPrototypeOf(globalThis) -> Xray-ish Window

所以測試 green 並不代表 Firefox green。

而且目前 E2E fixtures 從你這份 source 看都是:

import { chromium } from "@playwright/test";

基本沒有真正的 Firefox sandbox E2E。

這種 bug 靠 happy-dom 很難可靠抓到。

我會把你現在這 10 個 case 直接升格成一份 browser conformance userscript,最少建立這個矩陣:

Case Chrome USER_SCRIPT Firefox USER_SCRIPT
window===globalThis pass pass
self===globalThis pass pass
GM on window pass pass
self/globalThis write pass pass
Node constants pass pass
NodeFilter constants pass pass
XHR constants pass pass
DOM prototype identity pass pass
EventTarget method pass pass
Event lifecycle pass pass
RegExp.$1 pass pass
Object.freeze pass pass
Number.isNaN pass pass
top/parent/frames top frame pass pass
top/parent iframe pass pass
onload this === sandbox pass pass
sandbox A/B globals isolation pass pass

最後幾項很重要,因為它們可以阻止「修 Firefox 10 tests,但把 ScriptCat 舊相容性修壞」。


結論

我不建議把問題定義成「Firefox 的 globalThis 不符合 Chrome spec」。

更準確的是:

Firefox USER_SCRIPT world 本來就是 Cu.Sandbox + sandboxPrototype: contentWindow;ScriptCat 現有 sandbox abstraction 把「JS realm global」和「DOM Window」當成一個物件,因此在 Firefox 上崩掉。(Searchfox)

真正值得做的修復是:

realmGlobal
   │
   ├─ ES intrinsics
   │
hostWindow
   │
   ├─ DOM/WebIDL interfaces
   ├─ Window accessors
   └─ EventTarget methods
   │
   ▼
scriptGlobal / mySandbox
   ├─ window ─────┐
   ├─ self ───────┼─ self-reference
   ├─ globalThis ─┘
   └─ GM APIs

其中 「realmGlobal 只取 own properties;hostWindow 另外遍歷 prototype chain」 是我認為最關鍵的一刀。這比目前 main 的「先完整遍歷 global,再完整遍歷 window」更可靠,因為 Firefox 的 global prototype 本身就是 window;不把兩者來源拆開,receiver 永遠有機會綁錯。

如果下一步是直接修 code,我可以基於你這份 1.5.0-beta.2 給你做一版實際 diff,只改 create_context.ts + exec_script.ts + tests,按方案 A 實現,並把你這 10 個 testcase 加成 regression tests。

兼容性 / 迁移 / 测试

No response

ScriptCat 版本

No response

Metadata

Metadata

Assignees

Labels

proposalTechnical proposal or design discussiontechnicalTechnical implementation, architecture, or refactoring topic

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions