在擴充功能自動化的範例中,常常會看到兩個參數:
--disable-extensions-except=/path/to/ext --load-extension=/path/to/ext
比較舊的做法,通常是把 Puppeteer 或 Playwright 指向已安裝的 Chrome,然後一起帶上這兩個參數。
這次測試的原生 Google Chrome 150 版本裡,瀏覽器雖然可以正常啟動,命令列也沒有報錯,但擴充功能服務卻直接把這兩個參數忽略掉了。自動化連線表面上看起來一切正常,實際上擴充功能根本沒有載入;延後才出現的症狀,就是這個測試架構裡的 selector timeout。
Chrome 其實有把拒絕原因記成一行,只是你要先開啟 stderr logging 才看得到。
這篇文章最核心的發現,是不同瀏覽器版本與建置之間的對照。後面兩段很明確是測試架構註記:一段是在講擴充功能觸發下載,另一段是在講 file:// 測試檔上的命令列安裝路徑。Thunderbit 本身就是 Chrome 擴充功能,所以我們對這個問題有直接利害關係;但 Thunderbit 並不是這次測試的對象。
那行提示訊息
加入 --enable-logging=stderr,再用這些參數啟動原生 Chrome:
官方參考:Chromium extension service source.
WARNING:chrome/browser/extensions/extension_service.cc:442]
--disable-extensions-except is not allowed in Google Chrome, ignoring.
如果只帶 --load-extension,同一個檔案裡的另一行也會印出對應警告:
WARNING:chrome/browser/extensions/extension_service.cc:420]
--load-extension is not allowed in Google Chrome, ignoring.
而 Chrome for Testing 在完全相同的參數下,則不會印出任何這類訊息。
「不允許在 Google Chrome 中使用。」 這句警告很容易讓人把這次拒絕理解成「品牌版建置限制」。它確實證明這次原生 Google Chrome 150 版本忽略了這個參數,也點出了來源位置;但它單獨還不能證明跟版本無關,也沒辦法直接揭露真正的實作判斷條件。
以下內容,都是對這個結果的驗證與延伸影響。
用行為驗證它
這次測試用的是一個最小化的 MV3 擴充功能,不是現成下載來的,而是專門為這個實驗建立的。它包含 content script、popup、訊息往返、DOM 擷取,以及透過 chrome.downloads 匯出到本機的三欄 fixture。測試架構記錄六項編號檢查:service worker 是否註冊;擴充功能標記是否帶有 ID;content script 標記是否一致;popup 按鈕是否可見;popup 是否回傳三列資料;擷取到的 CSV 是否包含預期資料列。另外還有一個一致性判斷,用來比對 service worker 與頁面標記這兩個彼此獨立的訊號。
三個建置都使用相同的顯式擴充功能參數、相同的擴充功能目錄、HTTP fixture、headed persistent-context 模式,以及每個分支都重新建立的 profile。原生分支是透過 channel: 'chrome' 解析;另外兩個通過的分支則使用明確的 executable path。版本字串則是透過 CDP 讀回。
| 建置 | 瀏覽器回報版本 | 擴充功能 service worker | content script 是否注入 | 六項完整檢查 |
|---|---|---|---|---|
| Chrome for Testing | Chrome/149.0.7827.55 | ✅ | ✅ | 通過 6/6 |
| 原生 Google Chrome | Chrome/150.0.7871.187 | ❌ 從未註冊 | ❌ | 在步驟 0 失敗 |
| Chrome for Testing | Chrome/151.0.7922.10 | ✅ | ✅ | 通過 6/6 |
原始摘要:Chrome for Testing 149、原生 Chrome 150、Chrome for Testing 151,以及 stderr warnings。
刻意把 Chrome for Testing 149 放進來:它比拿來對照的原生版本還要舊。如果功能真的是因為版本升級才被移除,那麼較舊的版本應該還在可用那一邊,而失敗的應該會是更新的版本。結果剛好相反:失敗的版本夾在兩個可用版本中間,這就排除了單純沿著版本往前走就會被移除的說法。
不過,光看這張表還不能直接證明限制一定是建置層級,這點要講精準。 那個唯一失敗的欄位,同時也是唯一的原生 Chrome 欄位與唯一的 150 欄位——在這個設計裡,建置和版本還是完全糾纏在一起。三個分支最多只能排除「隨版本單調移除」;它們沒辦法排除「150 版退化、151 又修回來」這種情況。若只靠行為,要真正證明這點,就需要一個這台機器無法產生的欄位:Chrome for Testing 150,或是其他版本的品牌版建置。
這則警告之所以更支持「品牌版限制」的解讀,是因為它直接點名 Google Chrome;而三分支行為最多只能排除單純的單調移除。若要證明它是跟版本無關的機制,還是需要同版本跨建置比較,或是源碼/設定層級的佐證。
連結裡的各建置原始檔案展示了每個分支的摘要結果;本文並沒有公開額外啟動次數的逐次矩陣,因此也沒有把那個次數當成獨立證據。
光看一個訊號,不能說「它沒載入」
這個測試最早的版本,是靠 content script 寫進頁面的標記來判斷擴充功能有沒有載入;同時也用它來判斷 content script 是否執行。同一個讀數,被拿來當成兩個測量。 如果標記不見了,你根本無法分辨是「擴充功能根本沒載入」,還是「有載入,但 content script 沒有注入」;這兩種情況的修法完全不同。
MV3 擴充功能會跑背景 service worker,而 Playwright 可以直接存取 service worker。這就是一個獨立訊號:它完全不碰頁面,因此不會和注入結果混在一起。現在的測試會同時讀取這兩者,並檢查它們是否一致。
三次執行的結果都一致。原生 Chrome 上從未註冊任何 service worker——這是最強的說法。而在兩個 Chrome for Testing 版本上,worker 都在頁面開啟前就已經出現在 chrome-extension://<id>/background.js。
改用 Chrome for Testing,其實你可能早就有了
兩個通過的分支,使用的是明確指定的 Chrome for Testing 執行檔,版本分別是 149.0.7827.55 與 151.0.7922.10。與其透過 channel: 'chrome' 去解析原生 Chrome,不如把 Playwright 的 executablePath 指向你鎖定好的執行檔。npx playwright install chromium 會安裝由 Playwright 管理的 Chromium;這對自動化也可能有用,但它和這裡的兩個 Chrome for Testing 分支不是同一種發行標籤,也不該被當成這次比較裡的第四個分支。
官方參考:Chrome for Testing announcement.
相關檢視:Chrome extension permissions audit.
還有一個好處,會比這次故障更長久。原生 Chrome 會在背景自動更新,所以今天還能過的測試,下週二可能就突然失敗,卻沒有任何 commit 能解釋原因。Chrome for Testing 是鎖版的。對任何希望三個月後結果仍然有意義的流程來說,這一點比方便更重要。
測試架構註記:擷取擴充功能匯出檔
對爬蟲型擴充功能來說,匯出本身就是重點——欄位會在這裡遺失、編碼會在這裡被弄亂、巢狀資料也常在這裡被錯誤扁平化。關於這件事,有兩個文件沒有明說的地方,而且兩個都很容易踩雷。
| 匯出流程紀錄到的內容 | 值 |
|---|---|
playwright_download_event_fired | false |
suggestedFilename | null |
| 擴充功能要求的檔名 | probe-export.csv |
| 實際落到目錄中的檔名 | download.csv |
| 檔案內容 | 標頭與三列資料完整保留 |
在這個 MV3 / Playwright 1.56.0 測試架構中,chrome.downloads 並沒有觸發 Playwright 的 download 事件。 當擴充功能透過 API 匯出 CSV 時,waitForEvent('download') 不會 resolve。測試架構是透過開啟 CDP session、明確設定下載行為,再讀取輸出目錄來擷取檔案:
const cdp = await ctx.newCDPSession(page);
await cdp.send('Browser.setDownloadBehavior', {
behavior: 'allow', downloadPath: DIR, eventsEnabled: true,
});
在同一個測試架構裡,這條 CDP 擷取路徑不會保留原本要求的檔名。 標頭與三列資料都完整保留,但 probe-export.csv 最後會以 download.csv 落地。這是本次測試瀏覽器版本與設定下觀察到的行為,不是所有擴充功能下載都必然遵守的文件化不變量。請把內容與檔名分開驗證。
測試架構註記:這條安裝路徑下的 file:// 行為
在驗證這部分時,有一個被很多人反覆講的說法被證明是錯的,而且它還一度出現在這篇文章較早的草稿裡:因為擴充功能預設沒有檔案存取權,所以 content script 不會在 file:// 頁面上執行。
在兩個 Chrome for Testing 版本上實測,從命令列載入擴充功能,然後直接導向 file:///…/fixture/index.html:content script 會正常注入。 標記存在、擴充功能 ID 正確,兩個版本都一樣。透過命令列載入的 unpacked extension 會取得檔案存取權;大家常記得的那個「授予檔案存取權」切換,適用的是另一種安裝路徑。
把 fixture 透過 HTTP 提供,依然是比較好的預設做法,因為 file:// 頁面本來就不像真實目標。但這是逼真度的理由,不是技術上一定做不到的理由;而人們通常拿來解釋它的那個機制,其實是錯的。
這篇文章沒有證明什麼
- 這個限制的實作深度,只能從警告字串看出一部分。 Chrome 只說這些參數在這個建置中不允許,並指出了來源檔案。但它到底是由 build config、policy plumbing,還是其他機制控制,這篇文章沒有從原始碼讀出來。
- 這只是一台機器的結果。 macOS、arm64、單一原生 patch version,再加上兩個 Chrome for Testing 版本。Chrome 變動很快,這件事需要重新驗證,而不是只拿來引用。
- 這只測了
--load-extension這條路徑。 打包好的.crx安裝、開發者模式載入、以及企業白名單政策都沒有測。這裡的結果,不能拿來支持「原生 Chrome 根本不能跑擴充功能」這種說法。 - 這個擴充功能只是專門做來的 stub。 它測到的是每個 scraper 擴充功能都會用到的機制,但真正的擴充功能更大,也可能以這個 stub 不會遇到的方式失敗。
我一路走來曾經寫錯的地方
這點值得直接說清楚,因為這兩個錯誤都屬於那種「看起來結果沒問題,就很容易在審稿中滑過去」的類型。
這篇文章的第一版說,失敗是無聲的,沒有任何 log line,而且機制不可知——誰如果說自己知道機制,一定只是在猜。其實只差一個參數,Chrome 一直都有在 WARNING 等級印出那條訊息。原本寫成「我找不到」,卻被我寫成了「它不可能被找到」。
第二個錯誤就是上面提到的 file:// 說法:它是從筆記裡重複出來的,並且在測試前就先套上了一個自信的機制說明。一次執行就把它推翻了。
兩次錯誤背後的模式其實一樣。那是一個看起來合理、沒人會立刻挑戰的說法,因為檢查它感覺沒必要。修正方式不是把文字寫得更保守,而是訂出一條規則:凡是無法追溯到一次實測的主張,都不能上線。
本地測試架構使用的命令
這裡有版本化的 probe、extension stub、fixture,以及原始摘要的連結;但這還不是一個可獨立公開重現的完整套件。文章裡沒有記錄 Chrome for Testing 149 與 151 的精確下載來源與 checksum,而下面的 executable path 也只是本機輸入。若要把整個比較說成可獨立重現,請先公開那些瀏覽器來源與穩定的 repository commit。
相關檢視:Playwright review.
cd harness
npm install playwright@1.56.0
npx playwright install chromium # Playwright Chromium;不是下面兩個 CfT 分支
(cd fixture && python3 -m http.server 8731 &)
SP=$(pwd) OUT=cft-149.json LABEL=cft-149 EXE="<Chrome for Testing 149>" node probe_v2.mjs
SP=$(pwd) OUT=stock-150.json LABEL=stock-150 CHANNEL=chrome node probe_v2.mjs
SP=$(pwd) OUT=cft-151.json LABEL=cft-151 EXE="<Chrome for Testing 151>" node probe_v2.mjs
三個分支都使用同一個腳本與相同的擴充功能參數,但 executable 解析方式不同:原生 Chrome 用 CHANNEL=chrome,而兩個 Chrome for Testing 二進位檔則用 EXE。每個輸出檔中的版本,都是從瀏覽器讀回來的,不是直接相信標籤。
至於那條警告訊息,不需要測試架構也能抓:
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \
--user-data-dir=/tmp/p --enable-logging=stderr \
--load-extension=/path/to/ext about:blank 2>&1 | grep "not allowed"
截至 2026-07-28。
結論與注意事項
在這組測試矩陣中,原生 Google Chrome 150 拒絕了 --disable-extensions-except 加上 --load-extension,而 Chrome for Testing 149 與 151 都成功載入了擴充功能。Chrome 以 WARNING 等級記錄了這次拒絕:「--load-extension is not allowed in Google Chrome, ignoring.」 這句話很支持「品牌版限制」的解讀,而版本夾心的結果也排除了單純的單調移除;但如果沒有同版本跨建置分支或原始碼/設定證據,建置與版本還是會互相糾纏。
如果要建立一個可固定版本的擴充功能測試架構,應該明確指定瀏覽器執行檔版本,並同時驗證 service worker 與 content-script 標記。在這個 Playwright 1.56.0 設定下,擴充功能匯出需要透過 CDP 目錄擷取,而且最後會以不同的檔名落地。這個命令列載入的 stub 在測試過的 file:// fixture 上也能正常注入;其他安裝路徑則沒有測試。
試用 Thunderbit 進行網頁資料擷取 Get Started Free
常見問題
--load-extension 是不是已經從 Chrome 完全移除了?
沒有。Chrome for Testing 149.0.7827.55 與 151.0.7922.10 都能透過這個參數載入未打包的 MV3 stub,並完成六項檢查。原生 Google Chrome 150.0.7871.187 則拒絕了它,並記錄 「--load-extension is not allowed in Google Chrome, ignoring」。這支持了品牌版限制的推測,但不能算是最終證明:這個結果可以排除單純的單調移除,但也仍然相容於「150 版特有退化,151 又修復」的情況。
為什麼我看不到任何錯誤?要怎麼分辨「根本沒載入」和「有載入但安靜失敗」?
預設的 verbosity 不會印出那條警告。用 --enable-logging=stderr 啟動就會立刻看到;沒加的話,Chrome 雖然正常啟動,但它的 extension service 會忽略那些參數,而測試架構第一個看到的症狀通常就是 selector timeout。要分辨「根本沒載入」和「注入失敗」,請用兩個彼此獨立的訊號:MV3 背景 service worker,以及目標 DOM 裡的 content-script 標記。在這次測過的原生 Chrome 分支中,兩者都沒有出現;而在兩個 Chrome for Testing 分支中,兩者都有。
那我應該用什麼來自動化擴充功能?
通過的分支使用的是透過 executablePath 指定的 Chrome for Testing 鎖版執行檔。Playwright 管理的 Chromium 也是另一個可能的自動化二進位檔,但它不屬於這次測試的分支之一;在沒有確認實際解析出的執行檔前,不應把它說成同一種發行版。
為什麼擴充功能匯出檔時,waitForEvent('download') 永遠不會 resolve?
在這個 MV3 / Playwright 1.56.0 設定中,事件沒有 resolve,而且也沒有提供 suggested filename。測試架構是透過開啟 CDP session,呼叫 Browser.setDownloadBehavior 並指定目錄,然後直接從磁碟讀檔。在這次測試裡,CSV 的 bytes 保住了,但 probe-export.csv 會變成 download.csv;更廣泛的擴充功能與瀏覽器組合並沒有測過。
這篇文章沒有說什麼——關於 file:// URL,以及關於原生 Chrome 一般情況?
有兩個界線,方向相反。content script 確實可以在 file:// URL 上運作,只要擴充功能是從命令列載入的——這一點已在兩個 Chrome for Testing 版本上實測,script 正常注入,而且擴充功能 ID 也正確回報,所以那個常見說法「因為預設沒有檔案存取權,所以不能用」對這種安裝路徑來說是錯的。把 fixture 用 HTTP 提供仍然是比較好的做法,因為那比較像真實目標,不是因為 file:// 會擋注入。另一方面,這些結果也沒有說原生 Chrome 不能跑擴充功能。這裡只測了 --load-extension 的命令列路徑。打包好的 .crx 安裝、開發者模式載入與企業白名單政策都沒有測,本文也沒有對它們下結論。本文的範圍,就是自動化教學最常叫你使用的那一組參數。


