夜雨聆风学习资料网

ARTICLE · 1015854

AI Agent 是如何控制 Chrome 的?CDP 原理与实操

AI Agent 是如何控制 Chrome 的?CDP 原理与实操

你可能用过 Puppeteer、Playwright,也可能让 AI Agent 打开网页、点按钮、读取页面内容。

这些工具看起来各玩各的,但只要它们在控制 Chromium 系浏览器,很多时候都绕不开同一个东西:Chrome DevTools Protocol,简称 CDP。

CDP 是 Chrome 暴露出来的一套高权限「神经系统」。DevTools 能看到什么、改掉什么,外部程序通过这套协议,基本也能做到。

重点放在协议结构和一条可运行链路:用 Node.js 内置 WebSocket 连接 Chrome、执行 JavaScript,并调用浏览器截图。

DevTools 是界面,CDP 是通信协议

Chrome DevTools 是你按 F12 打开的那个界面,CDP 是 DevTools 和 Chrome 底层通信时使用的协议。

你在 Elements 里选中一个节点,在 Network 里查看请求,在 Console 里执行一段 JavaScript,界面背后都会产生 CDP 消息。官方对它的定义很直接:用来对 Chromium、Chrome 和其他 Blink 内核浏览器进行检测、审查、调试和性能分析。

理解 CDP,可以从五个问题入手。

问题
对应的核心概念
怎么连
HTTP 发现端点 + WebSocket 通信
连谁
browser、page、iframe、worker 等 Target
能做什么
Page、Runtime、Network、DOM 等 Domain
消息怎么走
Command、Response 和 Event
哪里会出事
版本兼容、用户数据和调试权限

把这五件事看懂,再遇到一个新的 CDP API,你基本就知道它应该放在哪了。

CDP 的骨架只有三层

CDP 由 Target、Domain 和 Message 三层组成

CDP 三层骨架:先选调试目标,再选功能领域,最后通过 Command、Response 和 Event 传递消息。

第一层叫 Target。打开一个 Chrome,除了网页,内部还运行着其他可调试对象。一个标签页是 Target,跨进程 iframe 可能是 Target,Web Worker、Service Worker 也可能是 Target。命令要发给谁,是 CDP 的第一个问题。

第二层叫 Domain。Chrome 把能力拆成了很多领域。Page 负责导航、页面生命周期和截图;Runtime 负责 JavaScript 执行上下文;Network 负责请求与响应;DOM 负责页面节点;Debugger 负责断点和调试。

还有一个很容易被低估的 Fetch Domain。它可以暂停请求,改 URL、改方法,甚至直接返回一份伪造响应。这份权限早已超出「看看网页」的范围。

第三层是消息。客户端发出 Command,Chrome 返回 Response;页面又发生了什么,Chrome 主动推送 Event。三种消息都是 JSON,核心结构很简单。

// Command{"id": 7, "method": "Runtime.evaluate", "params": {"expression": "document.title"}}// Response:用同一个 id 和请求配对{"id": 7, "result": {"result": {"type": "string", "value": "Example Domain"}}}// Event:没有 id,由 Chrome 主动推送{"method": "Page.loadEventFired", "params": {"timestamp": 12345.67}}

id 负责匹配 Command 和 Response。WebSocket 上可以同时飞着很多条命令,返回顺序不一定和发送顺序相同。客户端要用 id 把每个 Response 送回它对应的 Promise。

Protocol Monitor 展示 CDP 的命令、请求和响应

Chrome DevTools 内置 Protocol Monitor,左侧是消息流,右侧是请求或响应详情。图片来源:Chrome for Developers 官方文档。

用 Protocol Monitor 观察 CDP 消息

CDP 最好的入门工具已经装在 Chrome 里了。

打开 DevTools 设置,在 Experiments 中启用 Protocol Monitor,然后重新打开 DevTools。再从 More tools 里找到 Protocol monitor,你会看到 DevTools 自己发出的全部 CDP 消息。

现在回到 Elements 面板,随便点一个 DOM 节点,Protocol Monitor 里会刷出 DOM、Overlay 等一串命令。切到 Network 面板并刷新页面,又会看到 Network.requestWillBeSent、Network.responseReceived 这类事件。

一种高效的学习方式是:在界面里完成一次操作,观察 DevTools 发出的命令,再把必要的几条放进自己的程序。

Protocol Monitor 可以手动发送 Page.captureScreenshot

除了观察消息,Protocol Monitor 还可以直接发送 Page.captureScreenshot 这类命令。图片来源:Chrome for Developers 官方文档。

实操一:启动 Chrome 远程调试

实测环境为 macOS、Chrome 153.0.8010.37 和 Node.js 25.2.0。文中的脚本已在该环境运行通过。

macOS 可以这样启动:

"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \  --remote-debugging-port=9222 \  --remote-debugging-address=127.0.0.1 \  --user-data-dir=/tmp/chrome-cdp-demo \  https://example.com

Linux 常见写法:

google-chrome \  --remote-debugging-port=9222 \  --remote-debugging-address=127.0.0.1 \  --user-data-dir=/tmp/chrome-cdp-demo \  https://example.com

Windows 可以在命令提示符中使用:

"C:\Program Files\Google\Chrome\Application\chrome.exe" ^  --remote-debugging-port=9222 ^  --remote-debugging-address=127.0.0.1 ^  --user-data-dir="%TEMP%\chrome-cdp-demo" ^  https://example.com

启动时必须注意 --user-data-dir。

Chrome 官方在 2025 年 3 月公布,从 Chrome 136 开始,--remote-debugging-port 和 --remote-debugging-pipe 不再对默认 Chrome 数据目录生效,必须配合一个非默认用户目录。这项看似普通的参数调整,背后是攻击者滥用远程调试端口窃取 Cookie 的现实风险。

所以,不要为了「方便」去连你每天在用的主 Profile。建一个专用、可丢弃的调试目录,这一步不该省。官方对自动化场景也更推荐 Chrome for Testing。

启动后,在浏览器里打开:

http://127.0.0.1:9222/json/version

页面会显示浏览器版本、协议版本和 webSocketDebuggerUrl。本次实测返回 Chrome/153.0.8010.37 和 Protocol-Version: 1.3。

这个 1.3 容易让人误会。看到现代 Chrome 返回 Protocol-Version: 1.3,不能据此断定它只支持官方页面里那份「stable 1.3」小子集。要判断当前实例究竟有哪些命令,直接查 /json/protocol 更靠谱。

Chrome 153 实测的 json/version 返回

这张图来自本文的现场实测:示例脚本连上 Chrome 153 后,通过 Page.captureScreenshot 生成截图。

还有两个端点也很有用:

http://127.0.0.1:9222/json/list      # 列出可调试 targethttp://127.0.0.1:9222/json/protocol  # 当前 Chrome 实际支持的协议定义

兼容指定机器上的 Chrome 时,需要同时查看在线 tip-of-tree 文档和该实例暴露的 /json/protocol。

实操二:通过 WebSocket 发送 CDP 消息

这份代码只使用 Node.js 内置能力。建议使用 Node.js 22 或更新版本,确保全局 fetch 和 WebSocket 可用。程序读取 /json/list,找到第一个 page Target,然后连接该 Target 的 WebSocket。

import { writeFile } from'node:fs/promises';const targets = await fetch('http://127.0.0.1:9222/json/list')  .then(response => response.json());const page = targets.find(target => target.type === 'page');if (!page) thrownewError('没有找到可调试的 page Target');const socket = new WebSocket(page.webSocketDebuggerUrl);const pending = newMap();const listeners = newMap();let nextId = 1;socket.addEventListener('message', ({ data }) => {const message = JSON.parse(data);if (message.id) {const task = pending.get(message.id);    pending.delete(message.id);if (message.error) task.reject(newError(message.error.message));else task.resolve(message.result);return;  }for (const resolve of listeners.get(message.method) ?? []) {    resolve(message.params);  }  listeners.delete(message.method);});awaitnewPromise((resolve, reject) => {  socket.addEventListener('open', resolve, { once: true });  socket.addEventListener('error', reject, { once: true });});functionsend(method, params = {}) {const id = nextId++;  socket.send(JSON.stringify({ id, method, params }));returnnewPromise((resolve, reject) => {    pending.set(id, { resolve, reject });  });}functiononce(method) {returnnewPromise(resolve => {const current = listeners.get(method) ?? [];    current.push(resolve);    listeners.set(method, current);  });}await send('Page.enable');await send('Runtime.enable');await send('Network.enable');const loaded = once('Page.loadEventFired');await send('Page.navigate', {url: 'http://127.0.0.1:9222/json/version',});await loaded;const title = await send('Runtime.evaluate', {expression: 'document.title || document.body.innerText.slice(0, 80)',returnByValue: true,});const screenshot = await send('Page.captureScreenshot', { format: 'png' });await writeFile('cdp-shot.png', Buffer.from(screenshot.data, 'base64'));console.log(title.result.value);socket.close();

把文件保存为 cdp-demo.mjs,再运行 node cdp-demo.mjs。代码不算长,但 CDP 客户端最核心的东西都在里面了。

第一,send() 为每个命令分配一个自增 id,再把 Promise 存进 pending。响应回来时,根据 id 找到正确的 Promise。

第二,once() 等待没有 id 的事件。Page.navigate 返回,只能说明 Chrome 收到了导航命令;确认页面加载完成,还要等 Page.loadEventFired。

第三,很多 Domain 需要先 enable,之后才会持续推送事件。如果你忘了 Network.enable,就等不到那些网络事件。

Protocol Monitor 的 CDP 命令编辑器

Protocol Monitor 的命令编辑器会根据协议定义提示参数,特别适合在写脚本前做小范围试验。图片来源:Chrome for Developers 官方文档。

多 Target 场景:iframe 与 Worker

上面的示例只连了一个 page Target,适合理解原理。复杂网页里通常还同时存在 iframe、Worker 和多个执行上下文。

iframe 和 Target 的关系需要分两种情况:同进程 iframe 可能共享 Target,各自使用不同 execution context;跨进程 iframe 可能拥有独立 Target。

连接 page Target 后,后来出现的子 Target 不会自动纳入当前会话。复杂场景会用到 Target.setAutoAttach、Target.attachedToTarget 和 flat session,每个子 session 再用 sessionId 区分。

如果你只想抓一张主页截图,没必要一开始就把这套都造出来。但如果你在做浏览器插件、调试 Service Worker,或者你的脚本「明明看到 iframe,却永远找不到元素」,那就该回来查 Target 和 execution context 了。

CDP 与 Puppeteer、Playwright 如何选择

做业务自动化时优先使用上层工具;遇到封装层没有暴露的 Chrome 能力,再下沉到 CDP。

Puppeteer 官方提供 CDPSession,可以用 send() 直接发原始命令,也可以监听协议事件。Playwright 也有 newCDPSession(),但官方明确写了,这个能力只支持 Chromium 系浏览器。

场景
更合适的选择
理由
表单、点击、断言、等待元素
Playwright / Puppeteer
已处理等待、选择器和生命周期
精细追踪网络、Tracing、特殊 Chrome 能力
上层工具 + CDPSession
保留便利性,必要处下沉
开发调试器、IDE 接入、协议录制器
原生 CDP
需要自己管理 target、session 和消息
需要跨 Chromium、Firefox、WebKit
Playwright 等跨浏览器封装
CDP 绑定 Chromium / Blink

我不建议因为 CDP 「更底层」就全部手写。你会很快重新造出超时、重连、target 跟踪、上下文切换和错误恢复,而这些正是上层工具已经帮你踩过的坑。

CDP 的价值,是让你看清封装层下面发生了什么;当封装不够用时,你还可以继续下沉。

版本兼容与安全风险

第一个坑是版本。

官方协议页把版本分成 tip-of-tree、V8 inspector 和 stable 1.3。其中 tip-of-tree 能力最全,也变得最快,官方直接说明它可能随时破坏,不提供向后兼容保证。stable 1.3 虽然名字里有「stable」,实际上只是一个较小子集。

用于交付的脚本,需要固定 Chrome 版本,启动时读取 /json/version,对关键命令做能力检测,别默认今天的 tip-of-tree 一年后还一样。

第二个坑是权限。

一个能执行 Runtime.evaluate、读写 Cookie、拦截网络、操作页面和下载文件的端口,权限已经接近这个浏览器 Profile 的管理员接口。只绑本机,使用独立 Profile,用完即关,不要把 9222 直接暴露到公网。

第三个坑是 AI Agent。

现在很多 Agent 会把页面内容读回模型,再根据模型的决策通过 CDP 点击、输入、下载。这时候,网页里的文字既是数据,也可能夹带针对 Agent 的恶意指令。

如果 Agent 还能访问登录态、下载目录、内网页面和密钥,提示注入就会和 CDP 的高权限叠在一起。这类 Agent 应限制在专用 Profile 和最小权限环境里,发布、付款、删除、发送消息等操作应保留人工确认。

从 Protocol Monitor 开始实践

这个示例揭示了 CDP 的两面性。

拆到底层,CDP 的结构很朴素:一条 WebSocket,一边发带 id 的命令,一边接收响应和事件。

复杂度来自 Target 的层级、Domain 的状态和版本的漂移。它还能直接触碰登录态和网络数据,用的时候必须有清晰的边界感。

打开 Protocol Monitor,完成一次常用的 DevTools 操作,找出背后对应的 CDP 消息,再把其中一条放进上面的 send() 里。

Chrome 对这条 JSON 命令做出响应时,协议结构也就落到了可以观察和验证的行为上。

相关学习资料