Agent 究竟如何使用你的浏览器:CDP(Chrome DevTools Protocol)指南
Kyle Jeong (@kylejeong)
2026-07-18
D
原文
---
title: "Agent 究竟如何使用你的浏览器:CDP(Chrome DevTools Protocol)指南"
author: "Kyle Jeong (@kylejeong)"
source_url: "https://x.com/kylejeong/status/2078196340216185127"
published_at: "2026-07-17T19:13:13.000Z"
fetched_at: "2026-07-18T13:42:40Z"
updated_at: "2026-07-18T13:42:40Z"
language: "zh"
review_status: "draft"
---

# Agent 究竟如何使用你的浏览器:CDP(Chrome DevTools Protocol)指南
**一句话说:CDP 是一个控制面,让外部程序可以访问 Chromium 的页面、网络栈、JavaScript 运行时、输入系统、调试器和性能分析工具。这是一篇面向初学者的 CDP 指南。**
如果你打开 Chrome,按下 F12 或 opt + cmd + i,就会出现一个面板。它可以检查页面、查看每一个网络请求、执行 JavaScript、模拟手机,还能记录性能 trace。

这个面板其实是一个独立进程,正在和浏览器通信。它们之间使用的语言,就是 **Chrome DevTools Protocol**,也就是 CDP。
在 @browserbase,我们花了相当于数年工程时间来研究这个协议,试图驯服它的不完美之处,也因此学到了很多。这就是我们当年希望手边就有的一篇 CDP 讲解。
\* 这篇文章包含交互式组件,[可以在我们的博客上查看!](https://www.browserbase.com/blog/what-is-cdp)
## DevTools 背后的协议
CDP 内置在基于 Chromium 的浏览器里,包括 Chrome、Edge、Brave、Arc 和 Opera。DevTools、Lighthouse、Puppeteer、Playwright,以及很多[浏览器 agent](https://www.browserbase.com/agents),都用它和浏览器通信。
只要你的代码在控制 Chromium,底层某处大概率就有 CDP。
### 一点历史
CDP 一开始是 [Chrome DevTools](https://blog.chromium.org/2018/09/10-years-of-chrome-devtools.html) 背后的管道,并不是一个通用自动化 API。
Chrome 在 2008 年发布时,它的检查器来自 WebKit,也就是当时 Chrome 和 Safari 共同使用的渲染引擎。DevTools 前端需要和被检查的页面分开运行,所以团队定义了一套连接前端和浏览器的线协议(wire protocol)。
```plaintext
┌─────────────────────┐ CDP messages ┌─────────────────────┐
│ DevTools front end │ ← commands / events → │ Chromium browser │
│ HTML, CSS, JS │ │ renderer, network, │
│ │ │ runtime, input │
└─────────────────────┘ └─────────────────────┘
```
这套 [WebKit 远程调试协议](https://code.google.com/archive/p/chromedevtools/wikis/ChromeDevToolsProtocol.wiki),后来成了 CDP 的前身。
随后,Google 在 2013 年把 Blink 从 WebKit 中拆分出来。Chrome 的协议也随之继续演进,并成为有文档、有版本的 [Chrome DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/)。Safari 则继续使用 WebKit Inspector Protocol。
[Headless Chrome](https://www.browserbase.com/blog/what-is-a-headless-browser) 和 Puppeteer 在 2017 年出现。再后来,Playwright 把浏览器自动化推广到了 Chromium、Firefox 和 WebKit。

### 自己和 Chrome 通信
用启用远程调试的方式启动 Chrome。这里使用临时 profile,因为现代 Chrome 会限制对默认 profile 做远程调试。
```bash
/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome \
--remote-debugging-port=9222 \
--user-data-dir=/tmp/cdp-demo \
about:blank
```
Chrome 现在会暴露本地发现端点(discovery endpoint):
```bash
# Run these in a different terminal
curl -s <http://localhost:9222/json/version> | jq .
curl -s <http://localhost:9222/json/list> | jq .
```
第一个端点描述浏览器本身,第二个列出可以 attach 的 target。一个 page target 会包含 `webSocketDebuggerUrl`,也就是建立 CDP 连接的端点。
使用 WebSocket,你可以写一个最小版 DevTools 客户端:
```typescript
import WebSocket from "ws";
const targets = await fetch("<http://localhost:9222/json/list>")
.then(response => response.json());
const page = targets.find(target => target.type === "page");
const socket = new WebSocket(page.webSocketDebuggerUrl);
let id = 0;
const send = (method, params = {}) => socket.send(JSON.stringify({
id: ++id,
method,
params,
}));
socket.on("open", () => {
send("Page.enable");
send("Page.navigate", { url: "<https://example.com>" });
});
socket.on("message", data => {
const message = JSON.parse(data);
if (message.method === "Page.loadEventFired") {
console.log("Page loaded");
socket.close();
}
});
```
它会连接到一个页面,启用生命周期事件,发起导航,然后等待 Chrome 通知页面已经加载完成。
CDP 的其他部分也类似,只是规模要大得多。
### CDP 的语义
CDP 使用 JSON 消息,通常通过 WebSocket 或 pipe 传输。协议按域(domain)划分,比如 Page、Network、Runtime、Input、Target、Accessibility 和 Tracing。
每个域负责浏览器的一部分。
- Page 处理导航。
- Network 观察请求和响应。
- Runtime 执行 JavaScript。
- Target 发现页面、frame 和 worker。
- Tracing 记录性能数据。

一条命令(command)会要求 Chrome 做某件事:
```bash
{"id": 1, "method": "Page.navigate", "params": {"url": "<https://example.com>"}}
```
Chrome 会返回带有同一个 ID 的响应(response)和结果:
```bash
{"id": 1, "result": {"frameId": "...", "loaderId": "..."}}
```
ID 让客户端可以同时发出多条命令,并把每个响应匹配回对应的请求(request)。
事件(event)则反向流动。只要浏览器状态发生变化,Chrome 就会发出事件:
```bash
{"method": "Network.requestWillBeSent", "params": {}}
{"method": "Page.frameNavigated", "params": {}}
{"method": "Runtime.executionContextCreated", "params": {}}
```
事件没有 request ID。要读取它们,需要先启用某个域,然后消费它的事件流(event stream)。
**CDP 命令是问题或指令,事件是浏览器给出的回应。**

### Session(会话)和 target(目标)
可以把 CDP 连接想成一棵树。

这棵树的根是浏览器。从根开始,你会发现各个 target,并 attach 到它们。
target 是 Chrome 可以检查或控制的东西:一个页面、一个 [out-of-process iframe](https://www.chromium.org/developers/design-documents/oop-iframes/)、一个 service worker、一个 shared worker,或者一个扩展页面。
attach 到 target 会创建一个 session。通过这个 session 发出的命令会作用在对应 target 上,它的事件也会通过同一个 session 返回。
Chrome 使用 [Site Isolation](https://www.chromium.org/Home/chromium-security/site-isolation/) 把不同站点的页面隔离到不同的 renderer process 里。当一个页面嵌入跨站 iframe 时,Chrome 可能会把这个 frame 提升成 out-of-process iframe。它看起来仍然嵌在页面里,但 CDP 可能会把它暴露成另一个 target,并拥有自己的 session。
**Worker** 会给这棵树增加更多分支。dedicated worker 在页面主线程之外运行 JavaScript,而 shared worker 和 service worker 可以比单个页面活得更久。CDP 会把这些环境表示成 target 或 execution context,这样客户端在执行代码或监听事件时,就能定位到正确的地方。
**Navigation** 会再次改变这棵树。加载新 document 会销毁 frame 旧的 JavaScript execution context,并创建一个新的。即使 tab 和 frame ID 看起来没变,之前 context 里的任何 object ID 或引用都会失效。
一个 CDP 客户端必须同时追踪结构和生命周期:有哪些 target,attach 了哪些 session,哪些 execution context 属于哪个 frame,以及它们什么时候消失。
一个现代页面看起来可能只是一个 tab,但底下其实是一组不断变化的进程和 JavaScript 环境。这就是为什么在它之上构建系统这么难。
你可以为每个 target 打开一条独立的 CDP 连接,但这很快就会变得痛苦。flat mode 会把它们复用到同一条连接上,并用 `sessionId` 标识每个 session。要让它正常工作,客户端必须注意到 target,及时 attach,把消息路由到正确的 session,并在 target 消失时恢复。
## CDP 暴露了浏览器的哪些部分?
CDP 是从页面外部和浏览器通信的。这个位置让它能看到更多东西:renderer、网络栈、输入系统、worker、storage 和 debugger。

### 网络流量
Network domain 可以观察 document request、script、image、API call、redirect、cache hit 和 service worker 活动。
一个 request 通常会经过 `Network.requestWillBeSent`、`Network.responseReceived` 和 `Network.loadingFinished`。这些事件共享 ID,所以客户端可以重建完整生命周期,然后调用 `Network.getResponseBody`。
一次可见的页面加载,背后可能包含多个 frame、redirect、preflight request 和数百个资源。
### JavaScript 执行
Runtime domain 会执行代码,并报告 console call、exception 和 execution context。execution context 很重要,因为代码并不是运行在一个永久不变的环境里。frame 和 worker 都有自己的 context,但 navigation 会销毁它们。
### 浏览器输入
Input domain 会通过浏览器的输入系统发送鼠标、键盘、触摸和拖拽事件。
这比在某个元素上调用 JavaScript 方法更底层。但它仍然不会决定该点哪个元素、等待布局稳定,或者检查有没有 overlay 挡住目标。
浏览器级输入也不会让自动化和真人变得不可区分。它只是让客户端控制 Chrome 的输入路径,仅此而已。
### Trace 和诊断
`Performance.getMetrics`、`Tracing.start`、console event、exception event、截图和 screencast,暴露的都是 DevTools 里也能看到的东西。
一次失败的浏览器运行,会自带解释:可能是某个 request 卡住了,某个 frame 导航了,某个 exception 触发了,也可能是 trace 显示时间花在了哪里。
CDP 把浏览器变成了一个完全可观测的系统。
## 为什么直接执行原始 CDP 很痛苦
线协议格式很简单,难的是管理状态。
一个原始 CDP 客户端必须先启用 domain,才能收到有用的事件;必须在 navigation 创建和销毁 target、execution context 时持续追踪它们;还要协调多个 domain 中互相竞速的响应和事件。
参考文档解释了 message 的形状,但跳过了真实客户端需要的生命周期细节。一个 session 能活多久?什么会杀掉它?navigation 会保留它吗?另一个 WebSocket 能复用它的 `sessionId` 吗?
一个好的客户端必须在 target、session 和 context 出现、消失时持续追踪它们,而且通常此时命令仍然在飞。我们只能自己发现这些规则:强制触发 navigation 和 crash,记录每一个事件,然后看哪些东西还能活下来。

Chrome 本身也一直在变,而且变得很多。stable、experimental 和 deprecated method 会同时存在于协议里。
还有策略问题:CDP 可以把一个鼠标事件发送到某个坐标,但它不能判断哪个元素值得点击,也不能判断结果算不算成功。
这就是 Playwright 和 Puppeteer 这类库存在的原因。它们负责等待、目标选择、生命周期状态和交互策略;当你需要更底层能力时,它们也会暴露 CDP session。
### 协议的边界
CDP 属于 Chromium,而且只属于 Chromium。Firefox 和 WebKit 暴露的是不同的调试接口([Firefox 已在 2024 年弃用 CDP](https://fxdx.dev/deprecating-cdp-support-in-firefox-embracing-the-future-with-webdriver-bidi/))。[WebDriver](https://www.w3.org/TR/webdriver/) 提供的是跨浏览器自动化标准。[WebDriver BiDi](https://w3c.github.io/webdriver-bidi/) 则在这个模型上增加了双向 event。CDP 能更深入 Chromium,是因为 Chromium 掌握协议的两端。
调试连接也强大到足以变得危险。它可以检查页面、执行任意 JS、读取网络流量,并控制已登录的 session。
[tip-of-tree protocol](https://chromedevtools.github.io/devtools-protocol/tot/) 会随着 Chromium 一起变化。生产客户端需要选择自己支持哪些版本,并处理 method 缺失的情况。
### 浏览器的控制面
人用英语和人交流,程序用协议和其他程序交流,agent 用 CDP 和浏览器交流。
你可以通过同一个协议导航页面、观察请求、执行 JS、检查 worker、发送输入、收集 trace,并跟踪每一个 target。我建议使用更高层的[工具](https://github.com/browserbase/stagehand),让这些能力更容易使用。
你大概不应该把每一次交互都建立在原始 CDP 之上。但理解它如何工作、哪些抽象才合适,能帮助你决定该如何让 agent 访问 Web。
→ Kyle
\--------------------------------
如果你想阅读带交互式组件的版本,可以看看[这篇博客](https://www.browserbase.com/blog/what-is-cdp)!