中文亮度
本地双语对照页 · 非官方 · 译文:claude-fable-5 · effort xhigh · 260723 · 英文原文 · 三部曲前传:CLI 3.0 思想族谱的第一块化石

Closer to the Metal: Leaving Playwright for CDP

贴近底层:离开 Playwright,拥抱 CDP
标题译注 · metal 是计算机习语 bare metal(裸机)的缩影——指没有任何抽象层包裹的机器本体。"closer to the metal"即"少隔几层抽象,直接和机器对话"。此处 CDP 并非真硬件,但相对 Playwright 的层层包装,它就是那块"金属"。译作"贴近底层"取意,舍弃金属意象(260723 应读者质询修订——原译"贴近金属"直译保象,但无此习语背景的读者接不住)。
Nick Sweeting
August 20, 2025(250820)

Goodbye Playwright, Hello CDP / 再见 Playwright,你好 CDP

Playwright and Puppeteer are great for making QA tests and automation scripts short and readable, but as AI browser companies have been learning the hard way over the last year, sometimes these adapters obscure important details about the underlying browsers.

Playwright 和 Puppeteer 很适合把 QA 测试与自动化脚本写得简短易读;但过去一年,AI 浏览器公司们用血泪学到:这些适配器有时会遮蔽底层浏览器的重要细节。

We decided to peek behind the curtain and figure out what the browser was really doing, and it made us decide to drop playwright entirely and just speak the browser's native tongue: CDP.

我们决定掀开幕布,看看浏览器到底在干什么——看完就决定彻底抛弃 Playwright,直接说浏览器的母语:CDP。

By switcing to raw CDP we've massively increased the speed of element extraction, screenshots, and all our default actions. We've also managed to add new async reaction capabilities to the agent, and proper cross-origin iframe support.

切到裸 CDP 后,元素提取、截图和所有默认动作的速度大幅提升;我们还给 agent 加上了新的异步响应能力,以及真正的跨域 iframe 支持。

Obviously we ignored the (wise) advice in the header of the getting-started-with-cdp docs 👌

显然,我们无视了 CDP 入门文档开头那句(明智的)劝退忠告 👌

The Curse of Abstraction / 抽象的诅咒

Building AI browser automation is like building on top of a jenga tower of complexity. Every layer presents its own leaky abstractions, its own subtle crashes, and its own resource constraints.

做 AI 浏览器自动化,就像在一座复杂性叠成的叠叠乐塔顶上盖房子。每一层都有自己的"漏水"抽象、自己的隐性崩溃、自己的资源限制。

If you've ever heavily depended on an adapter library and build up a large codebase around it, you know the feeling that eventually comes when you realize the adapter library is no longer saving you any time by "hiding the true complexity".

如果你曾重度依赖某个适配器库、围着它盖起大片代码,你一定懂那种终将到来的感觉:你意识到,它靠"藏起真实复杂性"给你省下的时间,已经归零了。

In our case that time has finally come for Browser-Use and playwright-python, the library that we've historically used to drive our browsers with LLM-powered tool calls like click, input_text, go_to_url.

对我们来说,这一刻终于降临在 Browser-Use 与 playwright-python 之间——后者是我们一直用来承接 click、input_text、go_to_url 这些 LLM 工具调用的浏览器驱动库。

At first glance it may seem foolish to throw out such a mature adapter library and reinvent the wheel, but luckily the needs of AI browser agents are much narrower than the entire surface area that playwright provides, and we believe we can implement the calls we need with more specialized logic to better suit AI drivers.

乍看之下,扔掉这么成熟的库去重新造轮子很蠢;但幸运的是,AI 浏览器 agent 的需求比 Playwright 的完整功能面窄得多,我们相信可以用更专门的逻辑实现所需的那些调用,更贴合 AI 驾驶员。

Playwright also introduces a 2nd network hop going through a node.js playwright server websocket, which incurs a meaningful amount of latency when we do thousands of CDP calls to check for element position, opacity, paint order, JS event listeners, aria properties, etc.

Playwright 还引入了第二跳网络:所有指令都要过一遍 node.js 中继服务端的 websocket。当我们为检查元素位置、透明度、绘制顺序、JS 事件监听、aria 属性等发出成千上万次 CDP 调用时,这一跳的延迟就相当可观了。

📜 A Quick History of Browser Automation / 浏览器自动化简史

读法提示 · 这段编年史的看点只有一句话:同一个轮子,每隔几年就被重新发明一次。赶时间可跳到下一节,不影响主线。

The Millenial Era
1992 – Lynx (text-mode browser) could browse and automate keystroke inputs from a script, still useful today!
1995–1997 – Netscape Navigator (Unix) exposed netscape -remote "openURL(...)" to control an already-running GUI browser
1997 – Internet Explorer exposed a COM automation object so VB/VBA/WSH could launch, navigate, read/write the DOM, handle events
1998 – Mercury's Astra QuickTest (→ QTP → HP UFT), also WinRunner/XRunner
1999–2002 – headless & macro tools: HttpUnit (1999), iMacros (2001), HtmlUnit (2002)
2001–2003 – Watir (Ruby) grows out of driving IE via COM/OLE

千禧年代
1992——Lynx(纯文本浏览器)就能用脚本自动敲键浏览,至今能用!
1995–1997——Netscape Navigator(Unix)暴露 netscape -remote 命令,可遥控已在运行的图形界面浏览器
1997——IE 暴露 COM 自动化对象,VB/VBA/WSH 可启动、导航、读写 DOM、处理事件
1998——Mercury 的 Astra QuickTest(后来的 QTP/HP UFT),同期还有 WinRunner/XRunner
1999–2002——无头与宏工具:HttpUnit(1999)、iMacros(2001 录制回放)、HtmlUnit(2002 无头 Java"浏览器")
2001–2003——Watir(Ruby)从 COM/OLE 驱动 IE 起家,渐成通用 API

Selenium Dominance Era
2004 – Selenium comes out
2009 – Selenium + WebDriver join forces
2011–2017 – Before headless Chrome, PhantomJS (headless WebKit) filled the gap, with mixed reliability.

Selenium 霸权年代
2004——Selenium 问世
2009——Selenium 与 WebDriver 合流
2011–2017——无头 Chrome 出现之前,PhantomJS(无头 WebKit)填补空缺,可靠性参差。

The Pre-Headless Era (the Dark Ages)
2011 — Chrome ships remote debugging
2012 — WebKit Remote Debugging Protocol v1.0; the domains/events model that CDP still uses
2013–2014 — Blink forks from WebKit; the protocol becomes known as the Chrome DevTools Protocol (CDP)
2014 — chrome.automation accessibility API appears (separate from CDP)

前无头时代(黑暗年代)
2011——Chrome 上线远程调试
2012——WebKit 远程调试协议 v1.0 发布;CDP 沿用至今的"域/事件"模型在此定型
2013–2014——Blink 从 WebKit 分叉;协议在 Chromium 侧定名为 CDP(Chrome 开发者工具协议)
2014——chrome.automation 无障碍自动化 API 出现(与 CDP 各行其道)

Headless Chrome & CDP Era
Apr 2017 — Headless Chrome announced; Puppeteer introduced by the Chrome team
Jan 2018 — Puppeteer 1.0 ships
Jun 2018 — WebDriver becomes a W3C Recommendation
2019 — Google I/O talk popularizes modern testing with Puppeteer

无头 Chrome 与 CDP 年代
2017.4——无头 Chrome 发布;Chrome 团队推出 Puppeteer
2018.1——Puppeteer 1.0
2018.6——WebDriver 成为 W3C 标准
2019——Google I/O 演讲让 Puppeteer 现代测试流行开来

Multi-Browser Standardization Era
2019–early 2020 — Several core Puppeteer engineers leave Google for Microsoft and start Playwright 🎭 (oooo drama)
Jan 31, 2020 — Playwright 0.x public release; May 6, 2020 — 1.0 ships
Sep–Oct 2020 → multi-language support begins (Python: Sep 30, 2020)
2023 — ChromeDriver adds WebDriver BiDi; 2024 — Puppeteer adds BiDi; Selenium "welcomes Puppeteer to the WebDriver world."

多浏览器标准化年代
2019–2020 初——Puppeteer 数名核心工程师集体离开谷歌加入微软,创立 Playwright 🎭(好一出大戏)
2020.1.31——Playwright 0.x 公开;2020.5.6——1.0 发布
2020.9 起——多语言支持铺开(Python 版 9 月 30 日发布)
2023——ChromeDriver 支持 WebDriver BiDi;2024——Puppeteer 跟进;Selenium"欢迎 Puppeteer 来到 WebDriver 世界"。

This wheel has been reinvented every few years it seems.

这个轮子,看来每隔几年就要被重新发明一次。

Modern Times: A Multitude of Choice / 当代:选择过剩

Now in 2025 we are lucky to have many high quality driver libraries to choose between, our favorites include: pydoll (best python-first playwright replacement); go-rod (best CDP reference implementation), chromedp (great CDP debug tooling); puppeteer (best native chrome behavior), playwright (best cross-browser support); selenium (and Selenium Grid) still a great mature option, cypress; appium (system-level accessibility APIs on Android, iOS, macOS, Windows).

到了 2025 年,高质量驱动库已经多到挑花眼,我们的心头好包括:pydoll(最佳 Python 优先的 Playwright 替代)、go-rod(最佳 CDP 参考实现)与 chromedp(极好的 CDP 调试工具)、puppeteer(最原生的 Chrome 行为)与 playwright(最佳跨浏览器支持)、selenium 及 Selenium Grid(依旧成熟可靠)、cypress,以及 appium(经系统级无障碍 API 通吃四大平台)。

So why did we feel the need to write our own with cdp-use? Well for all the same reasons as everyone else: everlasting desire to be closer to the metal and have more detailed control over every step.

那我们为什么还非要自己写一个 cdp-use?理由和所有前人一模一样:那份永恒的渴望——更贴近金属,对每一步有更细的掌控。

How do Browser Drivers Work? / 浏览器驱动的底细

So what APIs does the browser actually expose anyway? What sits underneath all these "drivers"? All these adapter libraries, drivers, and AI helper extensions really just exist to pass messages and make RPC calls to these underlying browser APIs:

浏览器到底暴露了哪些 API?这些"驱动"的地基是什么?所有适配器库、驱动、AI 辅助扩展,本质上都只是在向下面这几类底层 API 传话、发远程调用:

Chrome Extension APIs — chrome.tabs.captureVisibleTab(), chrome.automation.getTree(), chrome.scripting.executeScript(), chrome.debugger.sendCommand(...). They appear the most powerful at first glance because they encompass CDP with chrome.debugger, but raw CDP lets you access some calls that are not available through chrome.debugger, and allows parallel connections to multiple targets.

Chrome 扩展 API——截图、无障碍树、注入脚本、chrome.debugger 转发 CDP 命令等。乍看最强(chrome.debugger 连 CDP 都包了),但裸 CDP 有些调用是 chrome.debugger 拿不到的,而且裸 CDP 允许对多个目标并行连接。

CDP APIs (via pure CDP Websocket or WebDriver BIDI socket) — Page.navigate({url}), Target.createTarget(), DOMSnapshot.captureSnapshot(), Page.handleJavaScriptDialog({accept: true}), Browser.setDownloadBehavior(), and many more.

CDP API(纯 CDP websocket 或 BiDi socket)——导航、开标签、抓 DOM 快照、处理弹窗、设置下载行为……不胜枚举。你在第 2 课 helpers.py 里看到的每个函数,底下就是这一层。

OS-Level Accessibility & screenreader APIs (NVDA/VoiceOver/AppleScript/Appium) — get a tree of all elements shown to screenreaders; script copy/paste, mouse, keypress, and rotor/tab-based navigation.

操作系统级无障碍/读屏 API(NVDA/VoiceOver/AppleScript/Appium)——拿到读屏软件视角的元素树;脚本化复制粘贴、鼠标、按键和轮盘/Tab 导航。

Internal Chromium C++ APIs — you can call arbitrary helpers in content/browser/devtools/protocol/*_handler.cc, or edit the CDP spec and add commands... anything is possible when the call is coming from inside the house.

Chromium 内部 C++ API——直接调源码里的任意内部帮手,甚至改 CDP 规范自己加命令……"当调用来自屋子内部,一切皆有可能"。

Launch Flags, User Data Dir, and Preferences — we've tested over 300+ chrome launch flags; user data dir state can drastically affect browser behavior: preferences files, state dbs, cookie stores, enterprise/registry options, chrome://flags.

启动旗标、用户数据目录与偏好——我们实测过 300 多个 Chrome 启动旗标;用户数据目录的状态会剧烈影响浏览器行为:偏好文件、状态数据库、cookie 仓、企业/注册表策略、chrome://flags,全是变量。

Ignoring for now: Classic WebDriver W3C REST APIs (not actually exposed by any browser directly — a standardized REST shape drivers provide above raw CDP/BIDI); WebDriver BiDi (merger of WebDriver + CDP in one websocket, official release delayed for years, not feature complete — check back in 2027).

暂且按下不表:经典 WebDriver 的 W3C REST API(并非浏览器直接暴露,而是驱动程序在裸 CDP/BIDI 之上提供的标准化外形);WebDriver BiDi(把 REST 与 CDP 合进一条 websocket 的野心之作,官方发布拖延多年,功能仍不全——2027 年再来看吧)。

🎭 How does Playwright work? / Playwright 是怎么工作的

Playwright achieves multi-language support by using a client-server model between clients in various languages and a single core implementation that runs as a node.js websocket server. The relay server accepts standardized "playwright protocol" RPC calls, then sends out CDP or BIDI calls to the browser to execute them.

Playwright 靠"客户端-服务端"模型实现多语言支持:各语言的客户端连向同一个 node.js websocket 核心。这个中继服务端接收标准化的"playwright 协议"远程调用,再翻译成 CDP 或 BIDI 发给浏览器执行。

This API is elegant in some ways: a nicely typed RPC interface, standardized behavior across languages, and lower-level browser ideas like targets, frames, and sessions abstracted into simple Page and BrowserContext handles that (usually) stay in sync and not deadlocked across node.js, the browser, and python.

这套 API 有它优雅的一面:类型良好的接口、跨语言行为统一;还把 target、frame、session 这些底层概念抽象成简单的 Page 和 BrowserContext 句柄,而且(通常)能让它们在 node.js、浏览器、Python 三方之间不失步、不死锁。

Unfortunately the double RPC through the node.js relay means some state inevitably drifts across the 3 places (and across three different languages and runtimes): live browser, playwright node.js relay process, python client process.

不幸的是,经过 node.js 中继的双重远程调用,意味着状态难免在三个地方漂移(横跨三种语言、三个运行时):活着的浏览器、node.js 中继进程、Python 客户端进程。

When a tab crashes or some operation is performed without focusing a page correctly, there are edge cases where the node.js process can hang indefinitely waiting for a browser reply, meanwhile the python client needs to send the CDP call the browser is expecting in order to proceed. Currently we have no recourse but to kill -9 and attempt to reconnect from scratch.

当标签页崩溃、或某个操作没先聚焦页面,就会出现这样的死局:node.js 进程无限期挂着等浏览器回话,而解铃的那条 CDP 调用只有 Python 客户端能发。此时我们无计可施,只能强杀进程、从零重连。

There are numerous cases like that only crop up in 1% of cases with specific slow network conditions, but edge cases can quickly drag down overall success scores when we run thousands of steps per eval.

这类问题往往只在 1% 的特定慢网络条件下冒头——但当每轮评测要跑几千步时,1% 的边缘案例足以拖垮总分。

🩸 Playwright's Sharp Edges / Playwright 的刀口

The playwright happy paths usually work fine, but the devil is in the details: fullPage=True screenshot on pages longer than 16,000px (reliably crashes playwright); alert()/confirm()/onbeforeunload handling; keyboard/mouse/dialog input without focusing a page; file upload & download on remote browsers; about:*, chrome://*, chrome-error://, PDF tab handling; preferences and enterprise configuration; crashed tab handling.

Playwright 的"幸福路径"通常没毛病,魔鬼全在细节:超过 16000 像素高的整页截图(必崩);alert/confirm/离开确认弹窗;没聚焦页面就敲键盘动鼠标;远程浏览器上的文件上传下载;about:、chrome:// 这类特殊标签页和 PDF 页;偏好与企业注册表配置;崩溃标签的善后。

DEBUG=pw:api helps but it only goes so far, at a certain point it doens't make sense to build workarounds around a relay layer that we're fighting to customize and control anyway.

调试开关能帮的有限。到了某个节点,继续围着一个你本来就在与之搏斗的中继层修修补补,已经没有意义了。

Sometimes when you are forced to thoroughly stretch every nook and cranny of an adapter layer, you start to see the ugly truths of the underlying resource, and you no longer want the "pretty version" as a veil pulled over your eyes, you'd rather see the ugly truth.

当你被迫把适配层的每个犄角旮旯都撑到极限,你会开始看见底层资源的丑陋真相——这时你不再想要那层蒙在眼前的"漂亮版本",你宁可直视丑陋的真相。

🍳 Starting From Scratch / 从零开始:才出油锅,又入火坑

Delivering a reliable experience when so many of the underlying components are inherently unreliable (or actively adversarial) is a monumental engineering challenge. Did you know there are at least 10 different ways a tab can crash in Chrome?

底层组件天生不可靠(有的甚至主动与你为敌),还要交付可靠体验,是纪念碑级的工程挑战。你知道 Chrome 标签页至少有 10 种死法吗?

All targets start briefly semi-"crashed" while initial requests are inflight; the zygote/root process can crash (slow io, oom, cpu lag); the GPU process can crash (there's even a helpful CDP call Browser.crashGpuProcess); renderers can crash on internal exceptions; pages can exceed allowed resources (Page.crash()); JS main threads can spinlock/oom on infinite loops or crypto mining; scrolling/input/screenshot before activateTarget focus can crash targets (5sec delayed!); handling a JS popup before activateTarget or after closing; parent frame navigation during child onbeforeunload; and any of the above inside a nested OOPIF, leading to subtle issues in the parent.

十种死法点名:所有目标在初始请求飞行中都短暂处于半"崩溃"态;zygote 根进程能崩(IO 慢、内存尽、CPU 卡);GPU 进程能崩(CDP 甚至贴心提供 Browser.crashGpuProcess 一键崩它);渲染器因内部异常而崩;页面超资源限额而崩;JS 主线程死循环或挖矿把自己锁死;没 activateTarget 聚焦就滚动/输入/截图会崩目标(还延迟 5 秒才发作!);聚焦前处理弹窗、或关完再处理;子 iframe 弹"确定要离开吗"时父框架恰好导航;以上任何一种发生在嵌套的跨进程 iframe 里,再把毛病悄悄传给父目标。

Playwright handled about half of these well, and presented impassible barrier to solving the other half, so we made the call to switch. But now we're faced with the difficult challenge of solving 100% of these cases ourself.

Playwright 处理好了其中一半,而对另一半构成不可逾越的障碍——所以我们拍板换道。但现在,100% 的死法都得我们自己接。

We take on this challenge with glee, we'd rather lose sleep thinking about these things so you can build reliable apps on top of us 💪.

我们乐呵呵地接下这挑战——宁可我们为这些事失眠,好让你在我们之上盖出可靠的应用 💪。

第 2 课回响 · 这一节就是"十个看门狗"的案发现场。八个月后的续作《Agent Harnesses》里,同一批作者宣布:看门狗全删了,让 agent 直面崩溃、读报错、自己重连。你手里这篇是"我们来替你扛下全部复杂性"的宣言;下一篇是这份宣言的葬礼。

Case Studies: Key Changes in the Migration / 迁移中的关键改动

New CDP-USE Library. A type-safe Python client generator for CDP: automatically generates Python bindings with TypeScript-like type safety from the official protocol specs. It's only shallow type bindings — no complex logic for session management, pages, elements — just 100% direct access.

新库 cdp-use。类型安全的 CDP Python 客户端生成器:从官方协议规范自动生成带类型的 Python 绑定。只做浅层类型,不做会话管理、页面、元素这些复杂逻辑——100% 直通。

New Event-Driven Architecturre. We used to only update our view of the world between actions, right before sending the next state summary to the LLM. This makes sense when your assumption is that the page contents will only change as a result of actions, but this is not always true! Take a slowly loading list that streams in, an animated carousel, or a bit of JS that runs every 3s.

事件驱动新架构。过去我们只在两次动作之间、给 LLM 发状态摘要之前,才更新对世界的认知。这个做法的前提假设是"页面只因动作而变"——但现实不是!流式加载的结果列表、自动轮播、每 3 秒跑一次的 JS,都会在任意时刻改变页面。

We've introduced a new event-driven architecture to fit the underlying event-driven nature of CDP. Our downloads_watchdog watches for file downloads that start spontaneously; crash_watchdog.py can watch for page crashes in a single place by subscribing to a crash event, and we no longer scatter crash detection and retry logic all over the codebase.

于是改成事件驱动,顺应 CDP 本身的事件天性:downloads_watchdog 盯着任何时刻自发开始的下载;crash_watchdog.py 订阅崩溃事件,在一个地方统一看家,崩溃检测和重试逻辑不再撒满代码库。——对,就是后来被全数处决的那批看门狗,此刻正是它们的高光出生时刻。

New Extracted Element Handle that works across OOPIFs. A tab is not a page; it's a constellation of targets (root + cross-origin iframes + workers), each hosting frames, each containing nodes. Abstract that away and you lose the ability to route input, correlate events, and re-find elements after DOM churn.

跨进程 iframe 通用的元素句柄。标签页不是一张页面,而是一个目标星座(根目标 + 跨域 iframe + worker),每个目标里住着 frame,每个 frame 里住着节点。把这个结构抽象掉,你就失去了三样命根子:把输入送到正确位置、把事件对上号、DOM 翻新后重新找回元素。

@dataclass(frozen=True)
class EnhancedDOMTreeNode:
    target_id: str                 # which DevTools target owns the renderer
    frame_id: str                  # which frame inside that target
    backend_node_id: int           # renderer-local node handle
    frame_path: Tuple[str, ...]    # root -> ... -> leaf
    element_index: int             # LLM-friendly stable ordinal
    ...

Outcome: zero guessing about who owns the node or where input should land, even with nested cross-origin iframes and DOM element shifts after actions.

成效:再也不用猜"这个节点归谁管、输入该落在哪"——哪怕面对层层嵌套的跨域 iframe 和动作后的 DOM 挪位。

Time is a Flat Circle / 时间是个平圆

Back in my first startup job in 2014 we were using PhantomJS and some RPC between python and JS, and in a way it's surprising how little has changed since then. Now it's 2025 I'm still dealing with all the same issues: tab crash handling, page load retrying, JS+Python RPC translation issues, python asyncio headaches, mouse movement fuzzing, etc.

2014 年我的第一份创业公司工作,用的是 PhantomJS 加一套 Python 与 JS 之间的远程调用——某种意义上,这十一年变化之少令人吃惊。2025 年的我仍在对付同一张问题清单:标签崩溃、页面重试、跨语言调用翻译、asyncio 头疼、鼠标轨迹模糊化。

Luckily a lot has improved since 2014, and we finally have a big light at end of the tunnel leading out of the manual QA automation mines: AI.

幸运的是,比起 2014 年还是进步了很多——而且隧道尽头终于亮起了一盏大灯,照着我们走出手工 QA 自动化的矿井:AI。

We aim to continue solving all the complexities of browser automation and CDP for our users. Our agents shouldn't have to know the nuances of CDP Targets in order to Get Stuff Done™️, and neither should you.

我们的目标,是继续替用户扛下浏览器自动化和 CDP 的全部复杂性。我们的 agent 不应该为了把事办成™️而去了解 CDP Target 的细枝末节——你也不应该。

Try out our new libraries and beta releases with cdp-use and let us know your feedback!

欢迎试用 cdp-use 和我们的新测试版,告诉我们你的反馈!

历史的反转 · 上面倒数第二句,八个月后被《Agent Harnesses》原文引用,并宣判:"Turns out we were wrong(事实证明我们错了)"。你之前读的是判决书,刚才读完的是案发现场——整条思想弧线到此闭合:替 agent 藏复杂性(本文)→ 发现藏不住也不该藏(Harnesses)→ 600 行开放厨房(CLI 3.0)。