# 让 agent 自己点完「Sign in with Google」——chrome-use 啃下 OAuth 弹窗登录

![chrome-use 把 id_token 从 Google popup 经 gsi/transform 回传给 opener，并保住 opener 不被销毁](https://img.leeguoo.com/media/14c3cb41-7748-42f4-b97a-0851cb30f466/chrome-use-oauth-hero.png)

我想让 hermes（我那个跑在另一台机器上的 agent）自己登录账号。不是我先登好、把 cookie 喂给它，是它从头自己登。

登录无非两种。一种是账号密码：agent 从我的 Bitwarden 金库取出用户名、密码、2FA 验证码，填进去就完事——这条我用 [bitwarden-use](https://github.com/leeguooooo/bitwarden-use) 打通了，零人工把 github 登了进去，连 passkey 都能（私钥从金库取出来本地签 WebAuthn，不用掏手机碰指纹）。另一种是「Sign in with Google」这类第三方 OAuth。这条卡了我大半天。

下面讲清楚它为什么难，以及 [chrome-use](https://github.com/leeguooooo/chrome-use) 最后怎么过的。

## 一次点击，底下三个窗口在传话

你在 x 的登录页点「使用 Google 账号登录」，看着就一下，其实底下是这样：

1. 那个 Google 按钮不是 x 自己画的，是 `accounts.google.com` 的一个**跨域 iframe** 塞进来的。
2. 点下去，弹出一个 **popup 窗口**，在里面选账号。
3. 选完，Google 不把 token 写进 URL，而是把这个 popup 重定向到一个中转页 `accounts.google.com/gsi/transform`。
4. `gsi/transform` 只干一件事：通过 `window.opener` 把 id_token **postMessage 回**最初那个登录页（opener），然后自己关掉。
5. opener 收到 token，x 才算登录成功，跳到 `/home`。

关键在第 4 步：token 是 popup **隔着窗口喊回** opener 的，靠的是 `window.opener` 这层关系。一旦这层关系断了，token 就喊给了空气，popup 傻站在 gsi/transform 上不关，x 那边永远等不到，几秒后弹回登录选择页。

## 自动化为什么一碰就断

我用 chrome-use 驱动我**真实的 Chrome**（它通过一个浏览器扩展接管标签，叫 relay 模式）。问题出在三处。

**第一，opener 被自己人误杀。** relay 给每个标签的调试会话用 `tabId` 当钥匙。x 登录页跳一次（它是跨进程导航），Chrome 给这个标签换了新 tabId，旧钥匙就失效了。更要命的是当时的"恢复"逻辑：一遇到失效会话，它直接把这个标签**销毁、另开一个空白标签**顶上——而被销毁的，正是那个等着收 token 的 opener。我盯着日志看了半天，每次都卡在 token 回传那一刻：opener 被自己的恢复逻辑干掉了。

**第二，relay 看不见 Google 按钮。** 跨域 iframe 在 Chrome 里是个独立的渲染进程（OOPIF）。chrome-use 直连 CDP 时会自动钻进去，看得到 iframe 里真实的「Continue as 立」按钮；relay 模式下，扩展只 attach 到标签顶层，iframe 是个黑盒。我只能拿坐标瞎点，点出来的还是"换个账号"的通用流程，不是那个个性化的一键登录。

**第三，relay attach 不上 popup。** 那个 Google 账号选择 popup 一开出来，我切过去想点账号，扩展报"标签没响应"——它要等 popup 加载完才慢悠悠去 attach，对一个一闪而过的 OAuth 窗口，太晚了。

还有个隐藏的坑，我排查很久才发现：扩展走的 native-messaging host，指向的是**已安装的旧二进制**，不是我本地新编的。所以前面那些"打了补丁的测试"，跑的全是旧逻辑。把那个 wrapper 脚本指向新编二进制，补丁才真正进了生效路径。

## 三刀

对着上面三处，各砍一刀。

**保住 opener。** 会话失效时不再销毁标签，改成非破坏式重连：同一个 `targetId` 还在（只是 `sessionId` 轮换了），重新 attach 拿到新 sessionId 就行，不开新标签。读、点这类命令遇到失效会话，也容忍重试几次，等扩展那边把会话恢复过来，而不是第一下就硬报错。

**穿透 iframe。** 让扩展在 attach 时主动开 `Target.setAutoAttach({flatten:true})`，把 Google iframe 这个子会话的事件转发给守护进程；命令再按子 sessionId 路由进那个 iframe。这样快照就能走进去，「Continue as 立」拿到一个可点的 ref——和直连 CDP 同等了。

**跟住 popup。** 扩展用 `chrome.tabs.onCreated` 在 popup **一创建就** attach（只认 opener 是自己管的标签那种，避免误伤别的窗口），守护进程把它当次级标签收着，但不抢主标签——opener 仍然钉在那，继续等它的 token。

## 跑通

补丁上去、扩展重载之后，再跑一遍：

- 快照，GSI iframe 里的「Continue as 立, leeguooooo@gmail.com」露出来了，点它；
- 弹出账号选择 popup，切过去（这次不报"没响应"了），选账号；
- 选完**什么都不碰**，让 gsi/transform 自己把 token postMessage 回 opener；
- opener 没被销毁，收到 token——x 跳到了 `/home`，登录态。

全程没有一次人工点击。这块硬骨头，算是啃下来了。

## 收尾

agent 自己登录账号，到这里两条路都通了：OAuth 这种第三方跳转，chrome-use 自己点完；账号密码、2FA、passkey 这种，bitwarden-use 从你的金库取了填。

- chrome-use：[github.com/leeguooooo/chrome-use](https://github.com/leeguooooo/chrome-use)
- bitwarden-use：[github.com/leeguooooo/bitwarden-use](https://github.com/leeguooooo/bitwarden-use)

相关文档：[chrome-use · 登录与凭证](https://chrome-use.leeguoo.com/login-auth.html)、[安装](https://chrome-use.leeguoo.com/install.html)。
