扩展自动化示例里,通常会见到两个常用参数:
--disable-extensions-except=/path/to/ext --load-extension=/path/to/ext
较早的教程会让 Puppeteer 或 Playwright 指向已安装的 Chrome,并同时传入这两个参数。
在这里测试的原版 Google Chrome 150 版本中,浏览器虽然能正常启动,不会报命令行错误,但扩展服务会直接忽略这两个参数。自动化连接一切正常,扩展却没有加载;随后在这个测试框架里出现的症状,是选择器超时。
Chrome 其实在一行日志里写明了拒绝原因,只是必须先开启 stderr 日志输出才能看到。
本次发现的核心,是不同浏览器构建版本之间的对比。后面两个小节明确属于测试框架说明:一个讲扩展触发下载的处理方式,另一个讲在 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 导出到本地的三个商品样例。测试框架记录了六项编号检查:service worker 是否注册;扩展标记是否带有 ID;content script 标记是否匹配;popup 按钮是否可见;popup 是否返回三行数据;抓取到的 CSV 是否包含预期行。另有一项一致性断言,用来比较独立的 service worker 信号和页面标记信号。
三个浏览器构建都使用了同样明确的扩展参数、扩展目录、HTTP 夹具、可见的持久上下文模式,以及每个分支都重新生成的独立 profile。stock 分支通过 channel: 'chrome' 解析;另外两个通过明确的可执行文件路径启动。版本号则通过 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 警告。
Chrome for Testing 149 是特意加入的:它比拿来对比的原版构建还要更早。如果功能是被版本升级移除掉的,那么更老的版本应该落在可用这一侧,而失败的应该是最新版本。可实际情况是,失败的构建夹在两个可用构建之间,这就排除了“随版本递进逐步消失”的简单解释。
不过,仅凭这张表还不能证明这个门槛一定和构建类型有关,这一点必须说准确。 失败的那一格同时也是唯一的 stock 那一格、以及唯一的 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') 没有返回。测试框架通过打开 CDP 会话、显式设置下载行为、再读取输出目录来捕获文件:
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 正确,两个构建都一样。命令行加载的未打包扩展本来就会获得文件访问权限;大家常说的“授予文件访问”开关,适用的是另一种安装路径。
把夹具通过 HTTP 提供,仍然是更好的默认做法,因为 file:// 页面和真实目标完全不是一回事。但这只是现实性更高的选择,不是技术上必须,而且通常被拿来解释这件事的机制本身就是错的。
这篇文章没有证明什么
- 这个门槛的实现层级,只能从那条警告字符串推到这么深。 Chrome 只说了这组参数在这个构建里不允许,并指出了源文件。到底是构建配置、策略管线,还是别的东西驱动的,本文没有从源码里读出来。
- 这只是一台机器。 macOS / arm64、一个原版补丁版本、两个 Chrome for Testing 构建。Chrome 变化太快,这件事需要反复复核,而不是直接当成引用结论。
- 这里只测了
--load-extension这条路径。 打包好的.crx安装、开发者模式加载、以及企业白名单策略都没有测。这里并不能推出“原版 Chrome 不能运行扩展”。 - 这个扩展是专门做出来的 stub。 它覆盖了抓取类扩展都会用到的机制,但真实扩展通常更复杂,也可能出现 stub 不会遇到的问题。
我在这件事上最开始哪里写错了
这一点值得直说,因为这两处错误都属于那种“看起来没问题,所以很容易逃过审稿”的类型。
第一版写法说这是一个静默失败,说没有日志行,也说这个机制无法知道——还说任何声称知道的人都只是猜。其实机制只隔着一个参数,Chrome 一直都在以 WARNING 级别打印它。“我没找到”被我写成了“它根本找不到”。
第二处就是上面的 file:// 说法:它来自笔记,还没测试就先带着一个很肯定的机制写进了草稿。一次运行就把它推翻了。
两次出错的模式是一样的:一句看起来不会有人质疑的话,被一路带下去,因为检查它显得没必要。修正办法不是在措辞上更谨慎,而是立一个原则:不能追溯到运行结果的说法,不准上线。
本地测试框架使用的命令
这里链接了带版本号的 probe、extension stub、fixture 以及原始摘要,但这还不是一个可以单独公开复现的完整包。文章并没有记录 Chrome for Testing 149 和 151 的确切下载来源与校验和,下面的可执行文件路径也只是本地输入。要把完整对比作为可独立复现的内容发布,先把浏览器来源和稳定的仓库提交一起补齐。
相关评测: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
三个分支都用了同一个脚本和相同的扩展参数,但可执行文件的解析方式不同:原版 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:// 夹具上也能注入;其他安装路径并未测试。
试用 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 再恢复”的 Chrome 150 特定回归,仍然与这三分支设计兼容。
为什么我看不到任何错误?怎么区分“根本没加载”和“加载了但悄悄失败”?
默认日志级别下不会打印这条警告。启动时加上 --enable-logging=stderr,它会立刻出现;不加的话,Chrome 表面上正常启动,但扩展服务会忽略这些参数,而测试框架最先看到的症状就是选择器超时。要区分“从未加载”与“注入失败”,请使用两个独立信号:MV3 后台 service worker,以及目标 DOM 里的 content script 标记。在本次测试的原版 Chrome 分支上,这两个信号都没有出现;在两个 Chrome for Testing 分支上,它们都出现了。
那我到底该用什么来自动化扩展?
通过 executablePath 指向固定版本的 Chrome for Testing 可执行文件,是这次通过的做法。Playwright 管理的 Chromium 也是一种可能的自动化二进制,但它不是本次测试中的一个分支;在没确认实际解析到的可执行文件之前,不应把它说成和这里用的一样。
为什么扩展导出文件时,waitForEvent('download') 一直不返回?
在这个 MV3 / Playwright 1.56.0 配置里,这个事件不会触发,也不会提供 suggested filename。测试框架打开了 CDP 会话,调用 Browser.setDownloadBehavior 指定目录,然后从磁盘读取文件。在测试结果里,CSV 字节都保住了,但 probe-export.csv 最后变成了 download.csv;更广泛的扩展与浏览器组合并未测试。
这篇文章没有说什么——关于 file:// URL,以及关于原版 Chrome 整体?
有两个边界,方向相反。其一,命令行加载的扩展可以在 file:// URL 上运行——这点在两个 Chrome for Testing 构建上都测过了,脚本正常注入,扩展 ID 也正确;所以那种“因为扩展默认没有文件访问权限,所以它们不能在 file:// 上运行”的常见说法,对这种安装路径来说是错的。把夹具通过 HTTP 提供,仍然是更好的实践,因为它更像真实目标,而不是因为 file:// 会阻止注入。其二,这些内容并没有说明原版 Chrome 不能运行扩展。这里只测了 --load-extension 这条命令行路径;打包 .crx 安装、开发者模式加载以及企业白名单策略都没测,也没有对它们作出任何结论。本文的范围,就是自动化教程总让你使用的那一对参数。


