
最近折腾了一下给本地 AI Gateway 加 Google 风格的 OAuth,本以为一个晚上能搞定——打开浏览器、拿 OAuth code、换 token、保存账号、收工。理论上就这么简单。
结果呢?把 Antigravity 接入 CliGate 变成了一串连锁问题,每个看起来都解决了,下一个请求又挂了。
这种集成 bug 从远处看特别不起眼:
如果你做过本地开发者工具的浏览器 OAuth,这种模式应该不陌生。
CliGate 是一个本地 AI 编程工具的网关。
它让 Claude Code、Codex CLI、Gemini CLI、OpenClaw 这些工具都连到本地同一个控制面板上,CliGate 负责账号池管理、API key 路由、协议转换、仪表盘聊天,还有针对每个工具的特定配置。
Antigravity 支持的目标听起来很简单:
但这不是一个问题。
至少是四个。
第一个 bug 长这样:
Google token exchange failed: 400
error_description: "client_secret is missing."
浏览器登录本身没问题。失败发生在重定向之后,CliGate 尝试把授权码换成 token 的时候。
这个区别很重要。
打开 Google 同意屏幕并不等于 OAuth 流程有效。它只证明你的授权 URL 足够有效,能让用户登录。
真正的门槛是 token 交换。
我先试了干净的做法:
code_verifier这解决了明显的本地错误处理问题,但 Google 还是返回:
client_secret is missing
这说明当前的 OAuth 客户端仍然被当作机密客户端(confidential client)处理。
PKCE 很好,但它不会把 Google 期望使用 client_secret 的客户端魔法般转换成公共客户端(public client)。
所以本地集成必须停止假装浏览器重定向是最难的部分。真正的约束是 OAuth 客户端注册本身。
token 交换路径搞定之后,下一个问题更隐蔽。
一个通过浏览器添加的账号可以工作一次,然后后续刷新时失败,因为原始的 OAuth 客户端上下文实际上已经被遗忘了。
这是持久化 bug,不是认证 bug。
修复方案是把 OAuth 客户端信息和账号一起保存,而不是假设一个全局的运行时默认值以后还够用。
所以每个 Antigravity 账号现在都保留自己的 OAuth 客户端上下文用于刷新:
{ "email": "user@example.com", "oauthClientKey": "antigravity-enterprise", "oauthClientConfig": { "key": "antigravity-enterprise", "clientId": "...", "clientSecret": "..." }
}
这样,浏览器添加的账号重启后仍然能刷新,而不是默默依赖当时环境变量里碰巧有什么。
然后来了个最让我意外的问题。
账号可以成功添加,但下一步报错:
ANTIGRAVITY_PROJECT_ERROR: cloudaicompanionProject missing
这个跟 OAuth 没关系了。
问题出在登录后的平台元数据,上游在服务实际请求之前想要这些。
我对比了 CliGate 和另一个专注于 Antigravity 的开源项目,发现了一个有用的设计差异:
cloudaicompanionProject 当成致命的登录失败这才是正确的做法。
所以流程从:
变成了:
loadCodeAssist这很重要,因为"无法立即解析所有东西"在实际集成中很正常。产品代码需要退化路径。
这个又完全不一样了。
账号在账号标签页里显示。
但在仪表盘聊天源选择器里不显示。
这说明 bug 根本不是 OAuth、token 存储或上游认证的问题。只是 UI(用户界面)数据源没接上。
CliGate 的聊天源选择器还在只返回:
Antigravity 账号接入了账号管理,但没接入聊天源目录。
所以用户体验是:
原因搞清楚之后修复很简单:
/api/chat/sources这种 bug 会浪费很多时间,因为用户看到的表现听起来像是"登录坏了",但登录实际上已经成功了。
做本地开发者工具的时候,我总是遇到同一个模式:
大家谈"加 OAuth"就像它是一个勾选框。
但它不是。
这次真正的工作是:
把这些都做完之后,用户体验才是:
"我点了登录,在聊天里选了账号,发了条消息。"
用户应该看到的就是这部分。
剩下的都是让"简单"集成不再在下一步挂掉的隐形工程工作。
之前:
之后:
这才是"支持已添加"的更好定义,而不是 OAuth 之后弹出一个绿色成功弹窗。
最大的错误是把认证当成整个集成。
通常不是。
如果你的 app 新增了一个账号类型,在宣告完成之前问自己这几个问题:
如果任何一个答案是"是",那 OAuth 成功不等于完成。
真正的完成是:用户能在他们预期的地方真正用上这个账号。
原文链接:https://dev.to/...