明明已经登录,点按钮却说「需要验证」:Electron + Playwright 双引擎 Session 隔离陷阱
某个内部桌面自动化工具,需要访问一个需要登录的外部平台。用户打开「授权」页面,扫码、滑动验证码、看到「已登录」提示——一切顺利。然后点「提取数据」按钮,系统返回:verification_required。
反复退出重登,结果一模一样。
现象
- 授权页面显示账号已登录,手动测试(在授权页触发的测试按钮)也能正常执行。
- 主界面的功能按钮每次都报「需要登录」或「需要人工验证」。
- 刷新、重启 APP、重新扫码均无效。
- 更诡异的是:隐藏 WebView 捕捉到了验证码的 QR 码截图,却无法完成滑动验证——滑块一动都不动。
根因
这个 APP 在迁移过程中,无意间留下了两套浏览器引擎同时驱动同一个外部平台的局面:
- 引擎 A:Electron 内嵌的
WebContentsView,session 绑定在partition: 'persist:xxx',以 hidden(不可见) 模式运行。 - 引擎 B:通过 IPC 调起的 Playwright sidecar 进程,session 存在独立的
userDataDir/browser-profiles/xxx,以 visible(可见窗口) 模式运行。
这两个引擎的 cookie 桶完全隔离,互不相通。
授权页绑定的是引擎 B(sidecar):用户在这里扫码、登录,session 写入 sidecar 的 profile。
功能按钮绑定的是引擎 A(Electron hidden view):操作在这个从未登录过的 session 里执行,当然返回「未登录」。
代码注释里甚至直白地写着:
// 登录态独立(partition 跟 sidecar 不通)但当时加这条注释的人并没有同步修正调用路径。
迁移期间两套引擎并存,前端 IPC 调用没有对齐:授权页 callSidecar('xxx') → Playwright sidecar(已登录),功能按钮 ipcRenderer.invoke('xxx:extract') → Electron hidden view(从未登录)。用户在 A 登录,操作却在 B 执行,永远报「需要验证」。
隐藏引擎还有一个额外的致命缺陷:它能截取 QR 码图片,但无法完成需要用户交互的滑动验证。隐藏 WebView 没有可见的 DOM,用户根本无法操作滑块,流程彻底死锁。
排查思路
遇到「已登录但被拒」的症状,先做最快的区分实验:
- 在授权页的测试入口触发同一个操作 → 如果成功,说明 session 本身是对的,只是操作按钮走了另一条路。
- 搜索两个入口各自调用的 IPC channel 名,追踪到 main process handler,再看 handler 用了哪个引擎、哪个 partition/profile。
- 如果两条路收敛到不同的 partition/profile,session 就是分裂的。
不要让用户反复重新登录——orphaned 引擎的 session 是由架构决定的,重登只会再次写入错误的 session 桶。
正解
把功能按钮的 IPC 调用改成与授权页相同的引擎。 优先选择 visible(可见)引擎,因为任何可能触发交互式验证码(滑块、拼图)的流程,必须有用户可操作的界面。
// 修改前:走 Electron hidden view,session 隔离
window.appDesktop.platform.extract(params);
// 修改后:走 Playwright sidecar,与授权页共享 session
callSidecar('platform.extract', params);如果 sidecar IPC channel 已经存在(只是按钮没有调它),这是纯前端改动,不需要重新编译原生层,直接走 web-only 发布通道即可。
附带优化:批量操作时,相邻请求之间加约 1 秒的间隔,避免快速连续的自动化导航触发平台的反爬安全验证。
将所有对该平台的操作统一路由到与授权页相同的引擎(Playwright sidecar),确保 session 共享;同时保证该引擎以 visible 模式运行,以便用户能完成交互式验证码挑战。
长期方案:选定一个引擎,把另一个彻底退役。同一个外部平台挂两套引擎是迁移残留,不是高可用,是定时炸弹。
一句话外卖
桌面应用里「已登录但操作被拒」,第一反应不是重新登录,而是确认操作按钮和授权 UI 是否连到了同一个引擎的同一个 session。