PROJECT · 2026
受控浏览器启动器:让 Agent 操控的浏览器,就是你在登录的那一个
双击启动一个「固定 profile + 固定 CDP 端口」的浏览器窗口,你在这里登录,Agent 通过 9222 接管同一个实例——彻底解决登录态不持久、调试浏览器与使用浏览器不一致的问题。
起因:Agent 连上的浏览器,根本不是我正在用的那个
用 CDP 驱动浏览器做自动化,前几次都是这个体验:
- 我在 agent 里让它「打开某站、点某个按钮」,它跑得挺顺,但弹出的登录页永远是未登录状态;
- 我手动在自己浏览器里已经登录好了,agent 那边却看不见,还得再扫码/再收一次验证码;
- 有时候 agent 报「连不上 9222」,可我明明开着浏览器窗口,看起来一切正常;
- 最迷惑的一次:它说打开了目标页,结果标签页停在
about:blank。
结论很直白:CDP 连的那个浏览器,和我登录的那个浏览器,是两个不同的浏览器实例。所谓「登录态不持久」,不是 cookie 过期,而是它压根就在另一份 profile 里。
症状逐条拆解(都是实测踩出来的)
1. 临时 profile = 一次性的身份
Chrome / Edge 用 --user-data-dir 指向哪个目录,Cookie、localStorage、IndexedDB 就全存在哪。如果启动时没指定,或者指向了临时目录 / 全新的空 profile,那这个浏览器就是一张白纸:
- 每次进程退出,登录态跟着一起消失;
- 就算进程不退出,它和我日常用的那套账号也毫无关系。
2. 默认浏览器没有开调试端口,所以「连不上」
我平时双击图标打开的那个浏览器,是没有 --remote-debugging-port 的。也就是说:
- 它存在,但 CDP 没有任何入口可达;
- 想去连它,只能另起一个带端口的实例——于是又是新 profile,又是没登录。
3. Chromium 的单实例机制,会让「重复启动」静默失败
这是最容易骗人的一条。同一个 profile 只允许一个浏览器进程持有:
第 1 次启动:msedge --user-data-dir=profile → 窗口正常,没有调试端口
第 2 次启动:msedge --user-data-dir=profile --remote-debugging-port=9222
→ 新进程把命令行"转发"给已有实例,然后立刻退出
→ 窗口一切正常,但 9222 永远连不上
表现为:浏览器明明开着,端口就是不响应。而且这种失败没有任何报错,看起来像「网络问题」。
4. /json/new 把 URL 整段编码,会开出空白页
用 HTTP 接口新开标签页时,如果对整条 URL 调用 EscapeDataString,file:/// 里的 : 和 / 也会被编码。实测结果:
| 请求 | 实际打开的页面 |
|---|---|
/json/new?file:///C:/.../start.html(原样) | ✅ 目标页 |
/json/new?url=file%3A%2F%2F%2FC%3A...(全编码) | ❌ about:blank |
5. 「当前页」可能选成浏览器内部页面
站点申请通知/剪贴板权限时,Edge 会开一个 edge://permission-request-dialog/ 的 target。如果按「列表第一个」选目标,后面的点击、取文、截图全打在这个弹窗上。
解决思路:把「浏览器身份」固定成一个项目资产
不去追求「连上用户正在用的那个浏览器」(做不到,普通窗口没有调试端口),而是反过来——让用户和 Agent 共用同一个我启动的浏览器,并把它的身份钉死:
固定 profile 目录(登录态永久落地)
+
固定调试端口 127.0.0.1:9222(Agent 永远连这里)
+
双击即用的一键启动器(人只需要做"登录"这件事)
三个不变式:
- 唯一实例:同一时刻只有这一个窗口,你登录的就是它。
- 唯一身份:登录态写在项目内的
profile\,关窗口、重启电脑都不丢。 - 唯一入口:Agent 只认
127.0.0.1:9222,永远能连上。
实现细节
启动参数
--user-data-dir="<项目目录>\profile" # 身份固化
--remote-debugging-port=9222 # Agent 的入口
--remote-allow-origins=* # 允许非浏览器来源的 WebSocket
--no-first-run --no-default-browser-check
--hide-crash-restore-bubble --disable-session-crashed-bubble
--disable-blink-features=AutomationControlled # 少被站点判定为自动化
--disable-features=msEdgeWelcomePage,msImplicitSignin
--start-maximized
调试端口只监听 127.0.0.1,不对外暴露。
幂等 + 兜底
启动器每次运行先探测 http://127.0.0.1:9222/json/version:
| 情况 | 行为 |
|---|---|
| 端口已响应 | 复用已在运行的实例,不新开窗口(需要时只补开一个标签页) |
| 端口无响应 + profile 被旧进程占着 | 明确报错并退出,提示改用 -Force;不会静默失败 |
加 -Force | 先杀掉占用该 profile 的残留进程、清 SingletonLock / lockfile,再干净重启 |
| 首次启动 | 建 profile 目录,拉起进程,轮询等端口就绪(最多 40 秒) |
那次「没开端口的实例占着 profile」的场景,实测提示长这样:
[!!] 该 profile 正被另一个未开调试端口的浏览器占用,新进程会把命令转交给它,导致 Agent 连不上。
[!!] 请改用:.\start-browser.ps1 -Force (会关闭旧窗口后重开)
状态落盘
启动成功后写 browser-state.json:端口、profile 路径、进程号、webSocketDebuggerUrl、启动时间。后续所有工具(包括我写的 cdp.mjs)都从这里取端口,不用在命令里硬写。
零依赖 CDP 驱动 cdp.mjs
只有一个 Node 文件(Node 22+ 自带 WebSocket),命令覆盖日常所需:
node cdp.mjs list # 列标签页
node cdp.mjs goto <url> # 跳转并等 load
node cdp.mjs eval "<js>" # 执行 JS
node cdp.mjs text [selector] # 取文本
node cdp.mjs click <selector> # 点击
node cdp.mjs fill <selector> <value> # 填表单(触发 input/change)
node cdp.mjs press Enter # 按键
node cdp.mjs shot out.png # 截图
node cdp.mjs cookies [domain] # 查登录 Cookie
node cdp.mjs quit # 优雅关闭
选页逻辑做了两处修正,对应上面第 4、5 条症状:默认目标优先挑 http/https/file 的真实页面,跳过 about:blank 与 edge:// 内部弹窗;/json/new 一律先发原始 URL,编码版只作为兜底。
验证记录(都是真跑出来的)
① 登录态跨重启存活
在受控浏览器里写入测试凭据,然后关掉浏览器、重新启动,再读回:
重启前: { cookie: "dsh_login_test=abc123", local: "persist-ok-1792243507616" }
重启后: { cookie: "dsh_login_test=abc123", local: "persist-ok-1792243507616" }
② -Force 能从「静默失败」里救回来
[!!] profile 被 9 个旧进程占用且没有调试端口,强制关闭后重启
[OK] 浏览器已就绪:Edg/155.0.4283.45
[OK] 登录态仍在:cookie=abc123
③ 重复启动不开新窗口
[OK] 检测到已在运行的受控实例(Edg/155.0.4283.45),直接复用,不新开窗口
④ 状态自查
浏览器 : Microsoft Edge
profile : ...\ai浏览器\profile
调试端口 : 9222
[OK] 正在运行,CDP 可用 -> http://127.0.0.1:9222
Cookies 数据库 : 44 KB,最后写入 ...
跑了一个多月之后,这份 profile\ 从 122 MB 长到了 1.4 GB(缓存 + 站点数据),登录态一直有效——这反过来也证明了「身份真的固定住了」。
使用方法
双击 启动浏览器.cmd → 在窗口里登录各站点 → 回到对话里说一声
- 关窗口不丢登录态;下次双击自动恢复。
- 想整体退出所有账号:
关闭浏览器.cmd→ 删除profile\→ 再启动。 - 想无头跑纯自动化:
start-browser.ps1 -Headless。 - 装了 Chrome 会自动优先用 Chrome;也可用
browser-exe.txt指定任意 Chromium 内核 exe。
文件清单
| 文件 | 作用 |
|---|---|
启动浏览器.cmd | 双击即用,启动/复用受控浏览器 |
关闭浏览器.cmd | 优雅关闭(登录态保留) |
start-browser.ps1 | 启动器本体,-Status / -Stop / -Force / -Headless / -Url |
start.html | 浏览器首页,常用站点登录入口,支持关键字筛选 |
cdp.mjs | 零依赖 CDP 驱动 |
browser-state.json | 运行时状态(端口 / profile / WS 端点) |
顺手记下的 Windows / PowerShell 坑
- PowerShell 5.1 读无 BOM 的 UTF-8 脚本会乱码:带中文的
.ps1必须存成 UTF-8 with BOM,否则中文全变问号。改成 pwsh 7 就没这问题。 Write-Host ' ' + ('-'*60)会多打一个+:字符串拼接要整体加括号,否则+被当成额外的-Object参数。$ErrorActionPreference='Stop'下,原生程序往 stderr 写一行就会抛NativeCommandError:-Stop分支里调用node就是这样被打断的,得临时把ErrorActionPreference调回Continue。Start-Process -ArgumentList是拼字符串:带空格的路径参数要自己预加引号。- Cookie 库换了位置:新版 Chromium 是
Default\Network\Cookies,不是老的Default\Cookies。
结论
「登录态不持久」大多不是 cookie 的问题,而是实例身份的问题。把三件事钉死——profile 目录、调试端口、唯一入口——人机共用同一个浏览器就不再是巧合,而是设计。
这套东西最大的价值不是自动化本身,而是把「我登录一次」变成可复用的长期资产:之后每一次 agent 操作,用的都是同一个已登录身份,不用再扫码、不用再解释「你连错了浏览器」。