Selenium 中的 CDP 协议资产库:Chromium DevTools Protocol PDL 的多版本维护与 PDL 到 JSON 的转换机制
本文围绕 Selenium 仓库中 common/devtools/README.md 文档展开:它说明了为什么要在仓库中保留多个版本的 Chrome 调试协议(CDP)定义,以及如何为新的 Chromium 版本引入 browser_protocol.pdl 与 js_protocol.pdl 两个协议源文件。读完本文,你将掌握 Selenium 的 CDP 协议资产目录结构、新协议版本的引入流程(含仓库中自动化的更新脚本),以及 PDL 到 JSON 的完整转换调用链——这套机制正是 Java、Python、.NET、Ruby、JavaScript 各语言绑定中 DevTools 支持的共同底层数据源。
一、为什么在仓库内保留多份协议版本
common/devtools/README.md 开宗明义:
"We keep multiple versions of the protocol in the tree in order to allow us to generate bindings as needed."(我们在代码树中保留多份协议版本,以便按需生成各语言的绑定。)
也就是说,各语言绑定(//java、//py、//dotnet、//rb、//javascript/selenium-webdriver)中 DevTools API 的可用方法集合,取决于绑定所支持的 Chromium 里程碑;不同版本的 Selenium 客户端需要面向不同稳定版的 Chrome 生成协议绑定,因此不能只留一份"最新"协议文件。README 还指出这些 PDL 文件通常下载自 Chromium 的 devtools source,而 Chrome 与 Edge 都基于开源的 Chromium 发行版,这使得按版本精确追溯协议定义成为可能。
当前仓库中实际保留了三个 Chromium 版本的协议目录,每个目录包含三个文件:
- common/devtools/chromium/v150/、v151/、v152/
browser_protocol.pdl:浏览器侧协议(Accessibility、Browser、DOM、Network、Page、Security、ServiceWorker 等域)js_protocol.pdl:V8 引擎侧协议(Console、Debugger、HeapProfiler、Profiler、Runtime、Schema 等域)BUILD.bazel:Bazel 构建规则,把两份 PDL 编译为 JSON
以 common/devtools/chromium/v152/browser_protocol.pdl 为例,文件头部即为协议元信息:
version
major 1
minor 3
随后是形如 experimental domain Accessibility、type AXNodeId extends string、deprecated domain Console 的 PDL 声明。v152 版本的 browser_protocol.pdl 中声明了 39 个 domain(从 Accessibility 到 ServiceWorker 等),而 js_protocol.pdl 则包含 V8 引擎的 6 个域,其中 Console 域被标记为 deprecated(README 对应的源文件中注明"建议改用 Runtime 或 Log")。
二、引入一个新协议版本的手工流程
README 的核心内容,是一份为特定 Chrome 发行版引入 vXX 协议目录的操作清单。其步骤与仓库中自动化脚本的实现一一对应,这里先完整给出文档原步骤:
- 确定 Chrome 稳定版号。查询 Chrome 桌面端稳定版频道更新信息,得到形如
96.0.4664.45的版本号。 - 创建版本目录。在 common/devtools/chromium/ 下新建
vXX目录(例如96.0.4664.45对应v96)。 - 复制上一版本的 BUILD 文件。从
vXX-1目录复制BUILD.bazel到新目录——这正是当前 v150/v151/v152 三个 BUILD.bazel 内容完全相同的原因。 - 下载浏览器侧 PDL。到 Chromium 源码树,打开与发行版本号对应的 tag,找到
//third_party/blink/public/devtools_protocol/browser_protocol.pdl并下载到//common/devtools/chromium/vXX。 - 确定该 Chromium 版本使用的 V8 revision。在 Chromium 源码的
//:DEPS文件中查找v8_revision字段。 - 下载引擎侧 PDL。切到 V8 源码对应的 revision,找到
//include/js_protocol.pdl并下载到同一vXX目录。
README 末尾还补充:同样的版本映射信息也可以在 OmahaProxy 的 CSV 查看工具中查到(这是历史上 Chrome 发布渠道数据的常用来源)。
三、自动化更新脚本:scripts/update_cdp.py
上述手工流程在仓库中已有完整的自动化实现——scripts/update_cdp.py。脚本的主流程(L213-L222)依次执行:获取 Chrome 里程碑 → 新增 PDL 目录 → 更新 Java、.NET、Ruby、Python、JavaScript 五个语言绑定的版本引用。它逐条落地的就是 README 的手工步骤:
1. 自动解析稳定版里程碑(get_chrome_milestone,L17-L41)。脚本查询 Chrome-for-Testing 的 last-known-good-versions.json,取 Stable 频道的版本号首位(如 152.0.7823.x 得到里程碑 152),再从 known-good-versions-with-downloads.json 中筛选出该里程碑下的最高版本。这与 README 第 1 步"找到最新 Stable Channel Update"等价,只是数据源换成了 Chrome-for-Testing 的版本接口。
2. 滚动保留三个版本(add_pdls,L86-L124)。脚本定义三个版本位:new_chrome(里程碑本身)、previous_chrome(里程碑减一)、old_chrome(里程碑减三)。执行时:
- 删除
v{old}目录(滚动淘汰,保持目录数恒定——这正是树中只有 v150/v151/v152 三个目录的由来); - 从
v{previous}复制出v{new}目录(对应 README 第 3 步"从vXX-1复制 BUILD.bazel"); - 用
fetch_and_save(L44-L49)下载browser_protocol.pdl与js_protocol.pdl; - 从该版本的
DEPS文件中用"v8_revision" in line定位 V8 revision(L106-L113),再到 V8 源码对应 revision 下取include/js_protocol.pdl——精确实现了 README 第 5、6 步。
3. PDL 的"扁平化"处理(flatten_browser_pdl,L64-L83)。新版 Chromium 的 browser_protocol.pdl 通过 include domains/*.pdl 语句引用拆散的域文件,而 Selenium 树中的 PDL 解析器(见下文第四节)只认单一文件。该函数会提取 version major/minor 版本块,逐一抓取所有被 include 的 domains/*.pdl 文件并拼接成单文件回写。脚本中还有一处细节修复:把 PDL 描述文本中的 `<script>` 替换为 `script`(L119-L124),注释注明是因为 Javadoc 生成器不喜 script 标签——这说明 PDL 里的 # 注释最终会进入各语言绑定的 API 文档。
4. 同步更新语言绑定侧的版本引用(L161-L210)。以 Java 为例(update_java):把 java/src/org/openqa/selenium/devtools 下 v{previous} 目录复制到 v{new} 目录、替换文件名与内容中的版本号,并同步修改 java/src/org/openqa/selenium/devtools/versions.bzl 与 rake_tasks/java.rake 中的版本引用;.NET、Ruby、Python(py/BUILD.bazel)、JavaScript(javascript/selenium-webdriver/BUILD.bazel)各有对应的更新逻辑。也就是说,README 描述的是"如何手工拿协议文件",而该脚本把"拿文件 + 刷新所有语言绑定引用"串成了一次性操作。
四、PDL 到 JSON 的转换链:pdl_to_json 与 pdl.py
拿到两份 PDL 后,各语言绑定消费的实际是 JSON 形态的协议描述。这一转换由三层完成:
1. 各版本目录的 genrule。以 common/devtools/chromium/v152/BUILD.bazel 为例(v150、v151 与之相同),声明了两个 genrule(L11-L37):
genrule(
name = "browser_protocol",
srcs = ["browser_protocol.pdl"],
outs = ["browser_protocol.json"],
cmd = "$(location //common/devtools:pdl_to_json) $(location :browser_protocol.pdl) --map_binary_to_string=true $@",
tools = ["//common/devtools:pdl_to_json"],
)
js_protocol 的 genrule 结构相同。default_visibility 则限定了消费方://dotnet/src/webdriver、//java/src/org/openqa/selenium/devtools、//javascript/selenium-webdriver、//py、//rb/lib/selenium/devtools——与 README 所述"按需生成各语言绑定"的定位一致。
2. 转换工具 pdl_to_json。common/devtools/BUILD.bazel 中定义(L15-L25):
py_binary(
name = "pdl_to_json",
srcs = ["convert_protocol_to_json.py", "pdl.py"],
main = "convert_protocol_to_json.py",
)
入口脚本 common/devtools/convert_protocol_to_json.py 是 README 中提到的"对 Chromium 官方脚本(third_party/inspector_protocol/convert_protocol_to_json.py)的修改版"。其命令行接口(main,L14-L35):
| 参数 | 说明 |
|---|---|
pdl_file |
待解析的 .pdl 输入文件 |
json_file |
输出 .json 文件路径 |
--map_binary_to_string |
设为 true 时,PDL 中的 binary 类型在 JSON 中映射为 string,客户端需要自行做 base64 解码 |
脚本读取 PDL 文本后交给 pdl.loads() 解析,再以 indent=4 写为 JSON。genrule 中统一传入 --map_binary_to_string=true,意味着 Selenium 生成的协议 JSON 里二进制字段都是 base64 字符串——这对纯语言客户端(C#、Java、Python 等直接发 JSON-RPC 的实现)是必需的形态。
3. PDL 解析器 pdl.py。common/devtools/pdl.py 是一个逐行正则驱动的解析器(parse,L59-L181),它定义了 PDL 到 JSON 的完整语法规则:
| PDL 语法 | 解析行为 |
|---|---|
# 开头的行 |
累积为 description 文本,附到下一个条目上(L76-L80) |
(experimental )(deprecated )?domain Name |
新建一个 domain 对象,追加到 protocol["domains"](L87-L91) |
depends on X |
记入该 domain 的 dependencies 列表(L93-L98) |
(experimental )(deprecated )?type Id extends (array of )?Base |
新增类型定义;基础类型命中 primitiveTypes(integer/number/boolean/string/object/any/array/binary)时写入 type,否则写入 $ref 指向其他定义(L26-L40、L100-L110) |
(experimental )(deprecated )?(command|event) Name |
新建命令/事件条目(L112-L128) |
parameters / returns / properties |
建立参数子项列表(L144-L147) |
(optional )(array of )?Type param |
参数定义;optional 标记可选,array of 生成嵌套 items,参数类型若为 enum 则就地建立枚举字面量列表(L130-L142) |
enum 块与缩进字面量行 |
收集枚举取值(L149-L152、L173-L177) |
version + major N / minor N |
写入 protocol["version"](L154-L166) |
redirect Name |
命令重定向(L168-L171) |
两个值得注意的实现细节:其一,assignType(L26-L40)对 enum 类型统一降级为 string、对 binary 按 map_binary_to_string 开关转换为 string,这正是 --map_binary_to_string=true 的落点;其二,任何不匹配上述模式的非空行都会触发 Error in {file}:{i}, illegal token 并直接 sys.exit(1)(L179-L180)——即解析器对 PDL 文本采取严格模式,格式稍有偏差构建即失败,这也是 update_cdp.py 必须先做扁平化等预处理的原因。
五、协议产物与语言侧的消费方式
转换后的 JSON 就是各语言 DevTools 模块的数据源。仓库根目录下的 common/devtools/browser_protocol.json(约 1.7 万行)与 common/devtools/js_protocol.json(约 3400 行)是被显式 exports_files 导出的顶层副本(common/devtools/BUILD.bazel L3-L13),供 //javascript/selenium-webdriver、//py、//rb/lib/selenium/devtools 直接引用;而 Bazel 构建路径上,各语言则通过版本目录的 genrule 产物生成代码,例如:
- Python:py/BUILD.bazel 中
browser_protocol = "//common/devtools/chromium/{version}:browser_protocol",生成代码输出到selenium/webdriver/common/devtools/{version}; - Ruby:rb/lib/selenium/devtools/BUILD.bazel 以同样的 genrule 目标作为输入;
- Java:java/src/org/openqa/selenium/devtools/ 目录提供
CdpClientGenerator、DevToolsProvider、HasDevTools等基础设施,其可用域集合受所用协议 JSON 约束; - .NET:
dotnet/src/webdriver/DevTools目录中的生成代码与DevToolsDomains.cs(见 scripts/update_cdp.py 的更新逻辑)随协议版本滚动刷新。
从源码结构看,整条链路是:Chromium/V8 源码中的 PDL → common/devtools/chromium/vXX(按版本归档)→ genrule 调 pdl_to_json 转 JSON → 各语言绑定消费 JSON 生成类型化 DevTools API。
六、小结与适用前提
回顾 common/devtools/README.md 的主旨并在仓库中得到印证:
- 多版本并存是设计而非冗余:滚动保留最近三个 Chromium 里程碑(当前为 v150/v151/v152),由 scripts/update_cdp.py 自动增删;
- 手工流程与脚本流程一一对应:查稳定版号 → 建
vXX目录 → 复制上一版 BUILD → 取browser_protocol.pdl→ 从DEPS查v8_revision→ 取js_protocol.pdl,脚本额外完成 include 扁平化、<script>标签清洗与各语言绑定引用刷新; - 转换工具链独立可验证:convert_protocol_to_json.py 是 Chromium 官方转换脚本的修改版,配合 pdl.py 的严格逐行解析,任何 PDL 语法偏差都会使 Bazel 构建直接失败,保证了入库协议文件与生成 JSON 的一致性;
- 适用前提:该机制面向 Chromium 系浏览器(Chrome/Edge 等)的 CDP;协议 JSON 中的
binary字段已按--map_binary_to_string=true映射为 base64 字符串,消费方需自行解码;各语言绑定支持的里程碑以对应目录(如java/src/org/openqa/selenium/devtools、py/BUILD.bazel)中的版本引用为准。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00