先说结论,能省掉你三个晚上:端口不通、登录失效、cookie 迁移失败,是三个互相独立的事实。只要把它们串成一条因果链——「端口起不来,所以登录掉了」——你就会在错误的地方修三轮,而且每一轮都有一半是真修对了,这才是最坑的地方。
这篇文章复盘一次真实的浏览器工位迁移:一个跑在内容平台创作后台的自动化发布工位,从搜狗浏览器迁到 Edge,中间踩了四个坑,每个坑都曾经让我得出一个错误结论。
我有一套多平台工位:每个平台配一个独立的浏览器 profile,配一个固定的 CDP 调试端口,自动化脚本通过 CDP 连上去操作页面。头条、闲鱼、快手都跑得挺稳,唯独内容平台 A 那个工位,连续两晚端口起不来。
现象很朴素:脚本连接 http://127.0.0.1:9335/json/version 超时,浏览器进程明明在,端口就是不监听。日志里 curl 直接 Connection refused。
第一晚我的判断是:浏览器启动太慢,加点等待。加到了 20 秒,还是不行。
第二晚的判断是:profile 被上次异常退出写坏了,删了重建。能起来一次,第二天又不行。
两个判断都不算错,但都没解决。真正的根因藏在浏览器的进程模型里。
搜狗浏览器用的是 Chromium 的单实例模型:同一个用户数据目录,只能有一个浏览器主进程。第二次带 --remote-debugging-port=9335 启动时,它不会开一个新的调试端口,而是把启动参数转交给已经在跑的那个主进程,然后自己退出。
结果就是:命令返回 0,进程列表里也有进程,但调试端口从来没有被真正打开过。更麻烦的是它的执行顺序——它在转交参数之后才退出,所以你会看到端口短暂地「闪现」一下,脚本的第一次探测可能还真连上了,第二次探测就 refused。用 while 循环等端口的人,会被这个闪现骗得很彻底。
顺带还有一个自己给自己挖的坑:我写了一个 keeper 脚本,用来「踢掉卡死的工位进程再重启」。它踢的时候是按进程名匹配的,而多平台用的是同一个浏览器二进制。于是——A 平台的 keeper 把 B 平台的工位杀了。两边互相重启,端口每隔几十秒死一次。这个 bug 在我加了一条日志「记录被杀的 pid 和它的命令行」之后才现形。
# 按命令行区分工位,别再按进程名杀
# 错的:taskkill /F /IM SogouExplorer.exe
# 对的:只杀命令行里带本平台 profile 路径的那个 pid
wmic process where "name='SogouExplorer.exe'" get processid,commandline /format:list \
| grep -B1 "edge-auto-xhs" | grep ProcessId教训一:判断「端口不通」之前,先确认你探测的是不是那个真的在监听的进程。 单实例模型下,「启动成功」和「端口就绪」是两件完全无关的事。
搜狗这条路走不通,我决定迁到 Edge——毕竟头条、闲鱼那几个工位在 Edge 上跑得很好。
然后我遇到了第二个坑,而且这个坑是无解的:Chrome 136 起(Edge 同代,152 也一样),不允许在默认的用户数据目录上开远程调试端口。
这是安全策略的硬性收紧。浏览器检测到「你在用默认 profile 目录 + 你开了 --remote-debugging-port」,就直接忽略调试参数,不给任何报错。你只会看到端口没监听,日志一片干净。
更要命的是配套的那一半:我原本打算「把登录态从默认目录拷出来,塞进独立工位」,结果默认目录下的 Cookies 数据库被浏览器独占锁着。SQLite 的 WAL 模式下,读到一半会被锁,而且这个锁不是普通的文件锁:
ctypes 调 CreateFileW 带 FILE_SHARE_DELETE → 拒绝访问esentutl /y 备份 → 拒绝访问robocopy /B 备份模式 → 拒绝访问(备份模式还需要管理员 SeBackupPrivilege)shutil.copy2 → 报 Windows 错误 32(文件被占用)四种方法全是错误 32 或 16。这不是权限问题,是浏览器主动持有的独占打开。
教训二:从 136 开始,「调试口」和「默认用户目录」永久互斥。 想自动化,就必须从第一天起用独立 profile 目录。这个结论没有绕行方案,别浪费时间试第五种拷贝方法。
迁到独立 profile 之后要重新登录,但手头有一个更快的想法:旧工位的 profile 库里躺着 15 条 cookie,其中 web_session 的名义有效期写到 2027 年。能不能直接搬过来,省一次扫码?
过程其实很顺利,顺利得反常:
Cookies 表 schema 居然兼容,直接文件级拷贝,Edge 正常启动、正常解密。web_session 在,值也对。到这里我几乎要收工了。然后访问创作后台——401,直接跳登录页。
cookie 文件里存在 ≠ 这个会话在服务端还有效。 服务端早就在某次风控或异地登录检测里把这个 session 作废了,本地文件只是没被清理掉的墓碑。你在本地怎么折腾都是自欺欺人。
教训三:验证登录态的唯一判据是「打一个需要鉴权的接口,看它返不返 200」,不是「cookie 罐里有这几条」。 文件级证据不等于会话级证据。
顺便记一条经验:如果一定要移植,能整文件拷贝就别用 DPAPI 裸解。很多沙箱环境会拦 CryptUnprotectData 这类行为(我这边直接报 127),而文件级拷贝走的是普通 IO,成功率反而更高。
扫码登录之后,为了以后能自动判定登录态,我写了个检查脚本:读页面上的 window.__INITIAL_STATE__,取 user.loggedIn,等于 true 就算登录。
它报 false。而页面右上角明明白白是我的头像。
根因:__INITIAL_STATE__ 里那个字段不是布尔值,是 Vue 的 ref 对象——{ _value: true, ... },还带个 dep 引用。{...} === true 当然永远是 false。
// 你以为的
{ "user": { "loggedIn": true } }
// 实际的
{ "user": { "loggedIn": { "_value": true, "dep": {} } } }修法有两种,我选了笨但稳的那种:解包 + 交叉验证。
# 判据 1:解包 ref
node -e "const s=window.__INITIAL_STATE__.user.loggedIn; console.log(s && (s._value !== undefined ? s._value : s))"
# 判据 2:页面有没有「登录 / 扫码」按钮(有这个按钮 = 没登录)
# 判据 3:打一个必须鉴权的接口,看 HTTP 状态码三条里至少两条一致才算数。单条判据在这种前端框架满天飞的环境里,误报率太高了。
教训四:读页面状态之前,先 typeof 一下。 前端状态树里的字段被 ref / proxy / getter 包过是常态,直接 === true 是最常见的假红来源。
这次事故最值钱的不是四个坑本身,而是下面这张拆解表。之前我脑子里是一条链:
端口不通 → 浏览器有问题 → 登录掉了 → 得重新扫码
真实情况是三条独立的线,每条有自己的判据:
你以为的一件事 | 实际的三件事 | 唯一可信的判据 |
|---|---|---|
端口起不来 | 浏览器进程模型 / 调试口安全策略 | 探测 |
登录掉了 | 本地 cookie 存在性 / 服务端会话有效性 | 打一个需要鉴权的接口,看状态码 |
页面说没登录 | 前端状态读取 / 真实登录态 | 解包 ref + 页面元素 + 接口,三选二 |
把这三件事混成一条链,最典型的后果就是:端口修好了,你以为登录也会跟着好;登录修好了,你以为 cookie 不用管。 每一步都有一半是真的,所以每一次「感觉快好了」都是真的快好了,然后第二天继续不通。
现在我的巡检脚本里,这三条判据是分开打印的:
[cdp ] port=9448 version=152.0.4191.66 ok
[auth] /api/user/me -> 200 ok
[ui ] loggedIn_ref=true avatar=yes login_btn=no三行全绿才叫「工位正常」。任何一行红,只修那一行,不再跨行推断。
那次迁移真正的收尾动作,不是把脚本跑通,而是把搜狗这条线整体退役,连同它的端口号、profile 路径、启动分支全部删掉。
原因很简单:一个「有时候能起来」的通道比一个「明确起不来」的通道危险得多。它会持续消耗你排查问题的时间,还会在关键时刻给你一个看起来正常的绿色——而那才是最贵的故障。
宁可少一个平台,不要多一个半死不活的工位。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。