English | 中文
为 AI Coding Agent 设计的"已登录浏览器接管"框架 —— 让 agent 用真人已登录的 Chrome 干活,配套生产级端口租约 / 看门狗 / 状态机治理。
A logged-in browser takeover framework for AI coding agents (ZCode / Claude Code / Codex …). Drives your real, already-logged-in Chrome via the Chrome DevTools Protocol, with production-grade port lease / watchdog / state-machine governance.
通用浏览器自动化方案(Playwright / Puppeteer / 各 agent 自带的 browser-use)有一个共同前提:起一个全新的、无登录态的浏览器实例,用完即弃。这对"从零打开网页、完成一次性任务"很合适,但对以下场景无能为力:
- agent 需要操作你已经登录好的账号(公司后台、已扫码的微信生态、需要短信验证的金融站点、付费订阅内容……)
- 你不想把账号密码 / 2FA / Cookie 重新喂给一个临时浏览器
- 多个 agent 会话要并发地各自操控一个 Chrome,且互不抢端口、互不串数据、互不漏资源
- Chrome 要能长期存活(durable),跨多个客户端连接复用,而不是每次都冷启动
zcode-cdp 的核心价值一句话:
让 agent 用你已登录好的真人 Chrome 干活,且 N 个 agent 并发时不抢端口、不漏资源。
| # | 痛点 | zcode-cdp 怎么解 |
|---|---|---|
| 1 | 登录态丢失 —— 通用方案每次起全新浏览器,登录态/2FA/Cookie 全得重来 | 接管真人 Chrome:首次 rsync 日常 Chrome profile 继承全部登录态,之后独立演化 |
| 2 | 多 agent 并发抢端口 —— N 个会话同时连 9222 互相踩 | 统一端口租约池(9223–9229 共 7 槽),原子 mkdir 抢锁 + leaseId ownership 校验,杜绝双 owner 竞态 |
| 3 | 僵尸 Chrome 吃内存 —— 开 5 个 agent 窗口其中 3 个不碰浏览器,却挂了 3 份 Chrome | 懒加载:首次 browser_* 调用才起 Chrome,不碰浏览器的窗口 = 0 Chrome 进程 |
| 4 | 孤儿进程烧 CPU —— agent 崩溃后 Chrome/proxy 残留,单核 99% 跑几小时 | 三层看门狗(软 CPU lag / 孤儿超时 / 硬 worker 线程 SIGKILL),扛过两次真实生产事故(见 postmortem) |
| 5 | 重复提交/发布 —— 网络抖动触发自动重放,导致同一篇文章发两次 | Chrome/backend 异常不自动重放,下次 browser_* 再懒启动;exactly-once enqueue |
| 6 | 反爬识别 bot 指纹 —— stealth 插件反而暴露自动化 | 连真 Chrome 指纹本就真,禁 stealth / 禁 new_context / 禁覆盖 UA(详见 platform-notes) |
| 7 | Python 脚本拿不到登录态 —— Playwright 自启的浏览器和 agent 用的不是同一个 | durable 模式:Python 裸 WebSocket / connect_over_cdp 直连固定端口的同一份 Chrome,共享登录态 |
它是怎么做到的:
| 维度 | 通用方案 (Playwright/Puppeteer/browser-use) | zcode-cdp |
|---|---|---|
| 浏览器登录态 | 全新实例,从零登录或注入 Cookie | 接管真人已登录 Chrome(首次 rsync 日常 Chrome profile 继承登录态) |
| 启动模型 | agent 一启动就拉起浏览器 | 状态机懒启动:不碰浏览器的窗口 = 0 Chrome 进程 |
| 并发治理 | 各自为政,易端口冲突 | 统一端口租约池(原子 mkdir + leaseId ownership) |
| 稳定性 | 单层超时 | 三层看门狗(软 / CPU / 硬 worker 线程,扛过 EPIPE 异常风暴) |
| 生命周期 | 用完即弃 | durable 长期存活 + lazy 按需启动双模式 |
- 🔑 Take over a real, logged-in Chrome — takeover profile 通过
rsync从日常 Chrome 继承登录态,之后独立演化;--refresh可随时强制刷新。 - 🧠 Relay lazy proxy — 本地 TCP 中继 + 单常驻 backend,状态机
IDLE → ENSURING → ACTIVE → CLOSING → IDLE;开 N 个不碰浏览器的窗口 = 0 个 Chrome 进程,激活循环不重启 backend。 - 🔒 Port lease & ownership — 统一
mkdir原子抢锁 +leaseId校验,防 PID 复用误删新锁;proxy / cdpcc / cdp-takeover 共享同一锁命名空间,杜绝双 owner 竞态。 - 🐕 Three-layer watchdog — 软看门狗(事件循环延迟)/ CPU 看门狗(主进程 CPU% busy-loop 检测)/ 硬看门狗(独立 worker 线程,60s 无心跳即
SIGKILL,免疫主线程卡死)。 - 🧰 Three client channels — MCP 工具(
mcp__cdp__browser_*)/ 裸 WebSocket / Playwrightconnect_over_cdp,三种客户端共享同一端口池。 - 🎭 Per-port profile isolation — 每个端口独立 profile + 不同卡通头像,多账号天然隔离,一眼分清哪个 Chrome 属于哪个会话。
- 🧹 挂起语义替代缓冲 — 激活期间 backend 连接在中继挂起,Chrome 就绪后自然流动;零应用层缓冲代码。
┌─────────────────────────────────────────────────────────────────────┐
│ Agent Clients (顶层) │
│ │
│ ZCode 会话 Claude Code Python 爬虫 / 自动化脚本 │
│ (MCP stdio) (cdpcc wrapper) (裸 CDP / playwright) │
└─────────┬──────────────────┬──────────────────────┬─────────────────┘
│ │ │
① MCP 工具调用 ① MCP 工具调用 ② 直连 CDP 协议
(JSON-RPC stdio) (JSON-RPC stdio) (WebSocket / connect_over_cdp)
│ │ │
┌─────────▼──────────────────▼──────────────────────│──────────────────┐
│ zcode-cdp-core (中层 / 治理层) │ │
│ │ │
│ ┌──────────────────┐ ┌────────────────────┐ │ │
│ │ zcode-cdp-proxy │ │ cdpcc │ │ │
│ │ (状态机懒加载) │ │ (CC 端口预占) │ │ │
│ │ IDLE→ACTIVE │ │ + cdp-ensure hook │ │ │
│ └────────┬─────────┘ └─────────┬──────────┘ │ │
│ │ │ │ │
│ └───────────┬───────────┘ │ │
│ ▼ ▼ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ zcode-cdp-lease (统一端口租约 / 所有权) │ │
│ │ 原子 mkdir + leaseId ownership 校验 │ │
│ └─────────────────────┬───────────────────────────┘ │
└───────────────────────────────┼──────────────────────────────────────┘
▼
┌───────────────────────────────────────────────────────────────────────┐
│ N 个已登录 Chrome 实例 (底层 / 端口池) │
│ │
│ 会话临时池 (9223-9229, 最多 7 并发) 脚本固定端口 (93xx, durable) │
│ ┌─────┬─────┬─────┬─────┬─────┬─────┬─────┐ ┌─────┬─────┐ │
│ │9223 │9224 │9225 │9226 │9227 │9228 │9229 │ │9324 │9326 │ │
│ │avatar│avatar│... │ │ │ │ │ │ │ │ │
│ │profile│profile│ │ │ │ │ │ │ │ │ │
│ └─────┴─────┴─────┴─────┴─────┴─────┴─────┘ └─────┴─────┘ │
│ 每端口: 独立 profile (rsync 继承登录态) + 不同头像, 互不串数据 │
└───────────────────────────────────────────────────────────────────────┘
两条路径:
① MCP 工具调用 Agent → proxy/cdpcc → lease 领端口 → 起 Chrome → @playwright/mcp → browser_* 工具
② 直连 CDP 协议 Python/Playwright → cdp-takeover <port> 起 durable Chrome → 直接连 WebSocket
- Node.js ≥ 18
- Chrome(macOS 主力测试,Linux 应该可用)
- 全局安装 MCP 后端:
npm install -g @playwright/mcp
-
克隆仓库
git clone https://github.com/Lumos-789/zcode-cdp.git cd zcode-cdp -
把
bin/加入 PATH(或记下绝对路径,下面要用)# 临时(当前 shell) export PATH="$PWD/bin:$PATH" # 永久(写入 ~/.zshrc 或 ~/.bashrc) echo 'export PATH="/path/to/zcode-cdp/bin:$PATH"' >> ~/.zshrc
-
配置 MCP —— 把下面这段拷进你 agent 客户端的 config.json,把路径改成你的绝对路径:
{ "mcpServers": { "cdp": { "type": "stdio", "command": "node", "args": ["/absolute/path/to/zcode-cdp/bin/zcode-cdp-proxy.js"] } } }- ZCode: 写入
~/.zcode/cli/config.json - Claude Code: 写入
~/.claude.json,或项目级.mcp.json - (Claude Code 用户也可直接用下文的
cdpcc命令,免去手改配置)
- ZCode: 写入
-
重启 agent —— 现在你的工具列表里多了一批
mcp__cdp__browser_*(navigate / click / type / snapshot / screenshot ……)。 -
第一次调用 —— 让 agent 调任意
browser_*工具。proxy 状态机自动流转:- 首次会从你日常 Chrome(
~/Library/Application Support/Google/Chrome)rsyncprofile 到该端口目录,继承你已登录的全部账号; - 然后启动该端口的 Agent Chrome,挂上
@playwright/mcp,工具调用直达。 - 之后该端口的 profile 独立演化,不影响你日常 Chrome。
- 首次会从你日常 Chrome(
首次
rsync可能需要几十秒(取决于你日常 profile 大小)。后续启动秒级。
| 模式 | 谁用 | 怎么起 | 生命周期 | 典型场景 |
|---|---|---|---|---|
| ZCode lazy proxy | ZCode 会话 | MCP 自动(写好 config 即生效) | 首次调用懒启动;browser_close 释放,回到 IDLE |
ZCode agent 日常浏览/操作 |
| cdpcc | Claude Code | 命令行 cdpcc [claude args] |
启动时预占端口(不起 Chrome),首次调 mcp__cdp__* 由 hook 起 Chrome;CC 退出自动关 Chrome |
给 Claude Code 一个带浏览器的窗口 |
| durable | 人工 / Python 脚本 | cdp-takeover [port] |
跨客户端长期存在,直到手动关 | Playwright connect_over_cdp、裸 WebSocket 长连接爬取 |
cdpcc # 自动选空闲端口,启带 cdp 工具的 CC(Chrome 懒起)
cdpcc --resume # 透传给 claude
cdpcc /path/to/project # 在指定目录起 CCcdpcc 启动时只预占端口 + 注入 MCP 配置,不起 Chrome;等你(或 CC)第一次调 mcp__cdp__* 工具时,PreToolUse hook(hooks/cdp-ensure.sh)触发 cdp-takeover 把 Chrome 起到该端口。不用浏览器 = 0 Chrome 进程。
安装 hook:把
hooks/cdp-ensure.sh配成 ZCode/CC 的 PreToolUse hook,matcher 填mcp__cdp__.*。
cdp-takeover # 自动找第一个空闲端口(9223-9229)
cdp-takeover 9324 # 显式端口(建议脚本固定端口走 93xx)
cdp-takeover 9324 --refresh # 强制从源 rsync 该端口 profile(须先关实例)
cdp-takeover status # 打印端口占用表然后 Python 侧:
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.connect_over_cdp("http://127.0.0.1:9324")
# 用你已登录的真人 Chrome 干活最常见的用法 —— 让 agent 访问一个需要登录的页面,读取数据:
# 在 ZCode / Claude Code 里,对 agent 说:
"打开 https://your-company-dashboard.example.com,把今天的订单数读出来给我。"
# agent 会自动调用:
# 1. mcp__cdp__browser_navigate({ url: "https://your-company-dashboard.example.com" })
# ↑ 首次调用,proxy 状态机自动:
# IDLE → 领端口 9223 → rsync 你日常 Chrome profile(继承登录态)
# → 启动 Agent Chrome → 挂 @playwright/mcp → 工具直达
# 2. mcp__cdp__browser_snapshot() ← 读取页面 ARIA 树
# 3. mcp__cdp__browser_click({ ... }) ← 点开"今日订单" tab(如需要)
# 4. mcp__cdp__browser_take_screenshot() ← 视觉确认
#
# 关键:因为用的是你已登录的 Chrome,不需要你喂账号密码;
# 用完调 browser_close,proxy 释放端口回 IDLE(Chrome 关闭)。
进阶:同一台机器上 3 个 agent 会话并发?没问题 —— 每个会话独立 spawn 一个 proxy,各自领不同端口(9223 / 9224 / 9225),互不干扰;其中不碰浏览器的会话 = 0 Chrome 进程。
所有变量都有合理默认值,不设也能跑。需要精细调优时按需覆盖。
| 变量 | 默认值 | 作用 |
|---|---|---|
CDP_PLAYWRIGHT_MCP_CLI |
自动发现(require.resolve("@playwright/mcp/cli.js"),兜底常见全局路径) |
指向 @playwright/mcp 的 cli.js 绝对路径 |
CDP_TAKEOVER |
bin/cdp-takeover(与 proxy 同目录) |
指向 cdp-takeover 脚本 |
CDP_LOCK_ROOT |
/tmp/zcode-cdp/ports |
端口租约锁目录(<port>.lock/owner.json) |
| 变量 | 默认值 | 作用 |
|---|---|---|
CDP_PORTS |
9223 9224 9225 9226 9227 9228 9229 |
会话临时端口池(ZCode/cdpcc 从中 pick 空闲,进租约) |
CDP_SCRIPT_PORTS |
9324 9326 |
脚本固定端口(durable,不进 pick 池) |
| 变量 | 默认值 | 作用 |
|---|---|---|
CDP_ORPHAN_TIMEOUT_MS |
1800000(30 min) |
孤儿 Chrome(owner 已死、Chrome 还在)超时回收 |
CDP_HARD_KILL_MS |
60000(60 s) |
硬看门狗:worker 线程 N 秒无心跳即 SIGKILL 主进程(免疫 busy-loop 卡死) |
CDP_HARD_CPU_THRESHOLD |
85 |
硬看门狗 CPU 检测:主进程 CPU% 超此值视为 busy-loop |
CDP_HARD_CPU_TRIES |
5 |
滑动窗口(2*N 次采样)内 ≥ N 次超阈值 → SIGKILL(容忍间歇 busy-loop 的间隙样本) |
CDP_WATCHDOG_LAG_MS |
2000 |
软看门狗:事件循环延迟阈值 |
CDP_WATCHDOG_TRIES |
3 |
软看门狗:连续触发次数 |
CDP_HEALTH_CHECK_MS |
5000 |
健康检查轮询间隔 |
CDP_OUTPUT_RATE_LIMIT |
500 |
backend stdout 限流(行/秒) |
CDP_STDERR_RATE_LIMIT |
200 |
backend stderr 限流(段/秒) |
CDP_BUF_MAX_BYTES |
2097152(2 MB) |
backend stdout 缓冲上限 |
CDP_INBUF_MAX_BYTES |
2097152(2 MB) |
stdin inBuf 上限(防无换行堆积) |
CDP_STARTUP_GRACE_MS |
8000 |
启动宽限期(lease 判定新锁不算 stale) |
CDP_STALE_LOCK_AGE_MS |
30000 |
锁龄超过此值且 owner 已死 → 视为 stale 回收 |
CDP_RELAY_HOLD_MS |
30000 |
TCP 中继无 upstream 时挂起新连接的上限(ms);ensure 失败时挂起连接被销毁,backend 收到连接错误 |
| 文档 | 内容 |
|---|---|
docs/architecture.md |
架构总览 —— 三层组件、状态机流转、端口池与 profile 模型 |
docs/port-lease.md |
端口租约模型 —— mkdir 原子抢锁、leaseId ownership、stale 回收、双 owner 竞态消除 |
docs/watchdog-postmortem.md |
看门狗事故复盘 ★ —— 两次真实生产事故(ppid 误杀 launchd、EPIPE 异常风暴 CPU 100%)的根因与修复 |
docs/profile-management.md |
Profile 管理 —— rsync 继承登录态、--refresh、每端口头像、多账号隔离 |
docs/troubleshooting.md |
排查清单 —— 端口被占 / Chrome 不起 / 工具不出现 / stale 锁 等常见问题 |
docs/platform-notes.md |
反爬与编辑器踩坑 —— CDP 三禁令、拟人滚动、Draft.js 注入、风控信号识别 |
docs/backtest.md |
标准回测流程 —— L0 静态 / L1 租约 / L2 状态机三层回归,改代码后 npm test 一键跑,零真实 Chrome 副作用 |
CHANGELOG 见 CHANGELOG.md,bug 上报模板见 .github/ISSUE_TEMPLATE/bug-report.md。
- macOS:主力测试平台,开箱即用。
- Linux:应该可用,但 Chrome profile 源路径需手动调整(默认硬编码 macOS 路径
~/Library/Application Support/Google/Chrome,Linux 下通常是~/.config/google-chrome),lsof/stat语法差异也已尽量规避。 - Windows:未测试,欢迎反馈。
- Agent 客户端:支持任何能注册 stdio MCP server 的客户端 —— ZCode、Claude Code、Codex 等。
⚠️ 请务必阅读本节后再使用。
- 本工具仅用于自动化你有合法权限访问的账号和站点 —— 例如你自己的账号、你有授权的内容管理、CTF / 安全研究、个人数据备份等。
- 请遵守目标网站的 Terms of Service 和当地法律法规。对滥用本工具进行批量抓取、规避反爬机制、账号滥用等行为,作者不承担任何责任。
- takeover 已登录 profile 意味着你的真实登录态会暴露给 agent。请仅在你信任的 agent 环境中使用,避免在不可信的 agent / 第三方脚本中暴露高价值账号。
English | 中文