项目: OpenSynapse
日期: 2026-03-28
适用范围: CLI 登录、服务端复用登录态、Code Assist 对话调用、历史踩坑说明
本教程回答 4 件事:
- 我们现在的登录认证功能到底是怎么实现的
- 为什么没有继续走普通
GEMINI_API_KEY或自建 Google OAuth Desktop Client - 这条链路在实现过程中踩过哪些坑,为什么会失败
- 现在项目里“正确”的最新调用方式是什么
如果你只想快速用,先看:
如果你想知道我们为什么这么实现、代码在哪、怎么调试,继续看这份教程。
我们最终没有采用“自建 Gemini API OAuth 客户端 + 直接调 Developer API”的路线,而是采用了:
- Gemini CLI / Google Code Assist 风格登录
- 复用本机已安装
geminiCLI 内置的 OAuth client - 拿到 Google 登录后的 access token / refresh token
- 调用
cloudcode-pa.googleapis.com的 Code Assist 接口
一句话概括:
登录像 Gemini CLI,调用也尽量像 Gemini CLI。
最初我们尝试过两种更“直觉”的做法,但都不够稳。
优点:
- 简单
- 官方标准路径
- 适合稳定服务端
缺点:
- 用户需要自己管理 API Key
- 不像 Gemini CLI 那样“浏览器登录一次就能用”
我们尝试过:
- 自己在 Google Cloud Console 创建 OAuth Client
- 浏览器打开授权页
- 本地回调拿 code
- 用 code 换 token
- 再把 access token 交给模型 SDK
这条路表面上能登录,但后面会撞坑:
client_secret is missingredirect_uri_mismatchinvalid_scopeACCESS_TOKEN_SCOPE_INSUFFICIENT- access token 不能直接当 Gemini API key 用
更关键的是:
OpenClaw 和 Gemini CLI 真正能跑通的并不是这条路。
它们本质上走的是:
- Google Code Assist OAuth
cloudcode-pa.googleapis.com- Gemini CLI 内置 client
所以我们最后决定直接复刻这条更接近真实 Gemini CLI 的方案。
当前认证与调用相关代码主要在这几个文件:
各自职责如下:
负责:
- 解析 Gemini CLI 内置 OAuth client
- 生成 PKCE
- 启动本地回调服务
- 构造 Google 授权 URL
- 用 code 换 token
- 自动刷新 token
- 调用 Code Assist 探测用户 project
- 保存本地凭证
负责:
- 把 OpenSynapse 的
generateContent请求转成 Code Assist 请求体 - 调用
cloudcode-pa.googleapis.com/v1internal:generateContent - 处理 fallback 模型
- 把响应里的文本抽出来
负责:
auth loginauth statusauth logout
负责:
- CLI 启动时优先检查本地 OAuth 凭证
- 如果本地凭证兼容,就走 Code Assist
- 否则再退到 API Key 或 ADC
负责:
- Web 端
/api/ai/generateContent - 优先用 API Key
- 没 API Key 时复用本地保存的 Gemini CLI / Code Assist 登录态
也就是说:
- CLI 和 Web 后端现在都可以复用同一份登录态
- 凭证保存一次即可
当前本地回调固定为:
http://localhost:3088/oauth2callback代码位置:
当前请求的 scope 是:
https://www.googleapis.com/auth/cloud-platform
https://www.googleapis.com/auth/userinfo.email
https://www.googleapis.com/auth/userinfo.profile注意:
这里没有再使用 generative-language scope。
这是我们踩坑后得出的结论,后面会详细讲。
本地凭证保存到:
~/.opensynapse/credentials.json结构大致是:
{
"access_token": "ya29....",
"refresh_token": "1//0....",
"expires_at": 1774670000000,
"token_type": "Bearer",
"scope": "https://www.googleapis.com/auth/cloud-platform ...",
"client_id": "681255809395-....apps.googleusercontent.com",
"email": "you@example.com",
"project_id": "your-code-assist-project"
}这是整条链路最关键的一步。
src/lib/oauth.ts 会优先尝试:
- 从环境变量读取显式覆盖的 client id / secret
- 从已安装的
gemini命令所在目录里,解析 Gemini CLI 自带的oauth2.js - 如果都没有,再退回旧环境变量
也就是说,默认情况下:
用户不需要自己去 Google Cloud Console 新建 Desktop Client。
这点和 OpenClaw 的实现思路一致。
登录前会生成:
verifierchallenge
用于:
- 防止授权码被截获
- 符合现代 OAuth 公共客户端安全要求
认证命令启动后,会在本地监听:
localhost:3088用户在浏览器完成授权后,Google 会把 code 回调到:
/oauth2callback拿到 authorization_code 后,会请求:
https://oauth2.googleapis.com/token并带上:
client_idclient_secret(如果当前 client 需要)codecode_verifiergrant_type=authorization_code
仅拿到 token 还不够。
后续我们还会调用:
v1internal:loadCodeAssist- 必要时
v1internal:onboardUser
作用是:
- 探测当前账号有没有可用 project
- 没有的话尝试创建 / onboard
- 最终把
project_id写入本地凭证
这一步是后面调用 generateContent 的前提。
在项目根目录执行:
npx tsx cli.ts auth login常见流程:
- 终端显示 OAuth client 来源
- 终端打印授权 URL
- 浏览器打开 Google 登录页
- 用户同意授权
- 浏览器显示“认证成功”
- 终端保存凭证并显示:
- 账号邮箱
- Code Assist Project
- Token 过期时间
查看状态:
npx tsx cli.ts auth status退出登录:
npx tsx cli.ts auth logout这部分是本教程最重要的内容。
当前 CLI 启动时,认证优先级是:
- 本地 Gemini CLI / Code Assist OAuth 凭证
GEMINI_API_KEY- ADC / GoogleAuth
代码位置:
只要本地已经登录成功,CLI 就会走:
generateContentWithCodeAssist()
而不是继续把 access token 当作 API key。
当前 Web 服务端 /api/ai/generateContent 的逻辑是:
- 如果配置了
GEMINI_API_KEY,走官方 API key client - 否则读取
~/.opensynapse/credentials.json - 检查凭证是否与当前 Gemini CLI client 兼容
- 调用
generateContentWithCodeAssist()
代码位置:
当前最终调用的后端接口是:
POST https://cloudcode-pa.googleapis.com/v1internal:generateContent请求头核心字段:
Authorization: Bearer <access_token>
Content-Type: application/json
User-Agent: google-api-nodejs-client/9.15.1
X-Goog-Api-Client: gl-node/opensynapse-cli请求体核心结构:
{
"model": "gemini-2.5-flash",
"project": "your-code-assist-project",
"user_prompt_id": "uuid",
"request": {
"contents": [...],
"systemInstruction": {...},
"generationConfig": {...},
"session_id": ""
}
}组装逻辑在:
当前调用不是只打一种模型,而是会按配置 fallback。
模型配置在:
当前 fallback 思路大致是:
gemini-3-flash-preview不可用时,退到gemini-2.5-flash/gemini-2.5-flash-litegemini-3.1-pro-preview不可用时,退到gemini-2.5-pro/gemini-2.5-flash-lite
触发条件通常是:
429 RATE_LIMIT_EXCEEDEDMODEL_CAPACITY_EXHAUSTED404 NOT_FOUND
这部分是整个迁移里最有价值的经验。
我们一开始出现过这种情况:
- 本地监听一个端口
REDIRECT_URI却写成另一个端口
结果就是:
- 浏览器授权看起来正常
- 回调时直接
ERR_CONNECTION_REFUSED
最终修正:
- 本地监听和
REDIRECT_URI统一到同一地址 - 当前固定为
http://localhost:3088/oauth2callback
如果 3088 已被别的进程占用,登录命令一启动就会失败:
EADDRINUSE解决办法:
lsof -nP -iTCP:3088 -sTCP:LISTEN
kill <PID>我们一度尝试使用自己在 Google Cloud Console 下载的 Desktop Client JSON。
表面上浏览器能授权成功,但换 token 时失败:
client_secret is missing根因:
- 当前 client 类型在这个流程里实际上仍要求
client_secret - 只带
client_id不够
最终结论:
- 不再以“自己新建 Desktop Client”作为主路径
- 默认优先复用 Gemini CLI 内置 client
我们尝试过把 scope 改成:
https://www.googleapis.com/auth/generative-language结果授权阶段直接报:
invalid_scope这说明:
- 这条自建浏览器 OAuth 流程并不适合这么接 Gemini Developer API
- 至少它不是 OpenClaw / Gemini CLI 的真实路径
最终修正:
- 不再走
generative-languagescope - 改走
cloud-platform + userinfo.* - 调用 Code Assist 后端
这是一个很容易犯的错。
错误做法是:
- 浏览器登录拿到 OAuth token
- 然后把它塞进
new GoogleGenAI({ apiKey: access_token })
这样并不成立。
原因:
apiKey是 Gemini API keyaccess_token是 OAuth Bearer token- 它们不是同一种认证材料
最终修正:
- OAuth 登录后走
cloudcode-pa.googleapis.com - API Key 路径保留给真正的
GEMINI_API_KEY
我们切换实现后,之前保存过的旧凭证还在本地。
结果会出现:
- 明明有凭证
- 但调用仍然失败
最终修正:
- 在状态检查和调用前比对
client_id - 不兼容就提示重新登录
代码位置:
虽然官方模型页会列出一批模型,但在当前 Code Assist 路径下:
- 某些 Preview 模型可能直接
404 NOT_FOUND - 某些模型会频繁
MODEL_CAPACITY_EXHAUSTED
这不是认证坏了,而是:
- 当前后端链路不支持该模型 或
- 当前窗口容量不够
所以后来我们加了:
- fallback 模型
- 更明确的错误提示
这也是很关键的经验。
CLI 能正常回复,只能说明:
- 认证链路通了
- 账号没问题
- Code Assist 请求至少能成功
但 Web 页面仍可能更容易 429,因为它当前的聊天链路更重:
- 长系统提示
- 全量历史
- RAG
所以“CLI 正常,页面容易限流”并不矛盾。
npx tsx cli.ts auth status你应该看到:
- 已登录
- OAuth Client
- 账号邮箱
- Code Assist Project
- Access Token 剩余有效时间
最直接方式是让 CLI 处理一个文件:
npx tsx cli.ts ./path/to/file.txt如果想做更细粒度的 smoke test,可以在代码里直接调用:
generateContentWithCodeAssist()
启动开发服务后,可直接打:
curl -X POST http://127.0.0.1:3000/api/ai/generateContent \
-H 'Content-Type: application/json' \
-d '{
"model": "gemini-2.5-flash",
"contents": "你好"
}'如果返回:
{"text":"..."}就说明:
- 服务端已经成功复用了本地登录态
- Web 路由与 CLI 一样可以工作
说明:
- 本机没有安装
gemini或 - 没配置显式覆盖的 client id / secret
解决:
- 安装 Gemini CLI 或
- 在
.env.local中配置:
OPENSYNAPSE_GEMINI_OAUTH_CLIENT_ID=...
OPENSYNAPSE_GEMINI_OAUTH_CLIENT_SECRET=...优先检查:
- 当前登录是不是旧 URL
- 本地回调服务是否仍在运行
- 端口是否被占用
- client id / secret 是否匹配
说明当前账号的 Code Assist tier 需要显式 project。
可以这样设置:
GOOGLE_CLOUD_PROJECT=your-project-id或:
GOOGLE_CLOUD_PROJECT_ID=your-project-id这不是登录失败,而是:
- 当前模型短时限流 或
- 当前模型没有容量
处理方式:
- 等几十秒再试
- 让 fallback 自动切到别的模型
- 切到更稳的
gemini-2.5-flash
通常说明:
- 当前模型不被这条 Code Assist 链路支持
这时不要先怀疑认证,要先怀疑模型可用性。
如果你要在 OpenSynapse 里继续沿用这套登录能力,建议坚持下面这几个原则:
-
默认优先 Gemini CLI / Code Assist 登录 这样用户体验最好,不用自己管 API key。
-
不要再把 OAuth token 当 API key 这两个认证体系不是一回事。
-
不要再把
generative-languagescope 当主路径 我们已经验证过,这条路在当前实现里不稳,且与 OpenClaw / Gemini CLI 实际做法不一致。 -
Web 与 CLI 共用同一份凭证 这样登录一次即可全项目复用。
-
把模型可用性问题和认证问题分开看 认证成功不代表每个模型都能用。
建议后续维护时按这个顺序读代码:
如果要看我们参考的外部思路,再看:
这套登录认证功能的本质不是“给 OpenSynapse 接了一个普通 Google OAuth”,而是:
把 OpenSynapse 接到了 Gemini CLI / Google Code Assist 那套更接近真实终端体验的认证与调用链路上。
而我们真正踩出来的经验是:
- 登录能成功,不代表模型就都能用
- CLI 能用,不代表 Web 请求就一定轻
- 认证、模型、限流、上下文重量,这 4 件事必须分开看
把这几点分清楚,后面的实现和排障会简单很多。