香菜的博客
全部项目

PROJECT · 2026

受控浏览器启动器:让 Agent 操控的浏览器,就是你在登录的那一个

双击启动一个「固定 profile + 固定 CDP 端口」的浏览器窗口,你在这里登录,Agent 通过 9222 接管同一个实例——彻底解决登录态不持久、调试浏览器与使用浏览器不一致的问题。

运行中 CDP浏览器自动化登录态PowerShellNode.jsWindows

起因: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 永远连这里)
        +
双击即用的一键启动器(人只需要做"登录"这件事)

三个不变式:

  1. 唯一实例:同一时刻只有这一个窗口,你登录的就是它。
  2. 唯一身份:登录态写在项目内的 profile\,关窗口、重启电脑都不丢。
  3. 唯一入口: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 操作,用的都是同一个已登录身份,不用再扫码、不用再解释「你连错了浏览器」。