Skip to content

gemini: 个人版 Code Assist 已停用该 OAuth 客户端(This client is no longer supported)——「+Gemini」授权成功后仍无法入池;附带 pollLogin 空指针 panic #167

Closed

Description

@zrtzrt

现象

macOS 26 / WildWork 2.9.2,点面板「+Gemini」:

Failed to sign in. Message: This client is no longer supported
for Gemini Code Assist for individuals. To continue using
Gemini, please migrate to the Antigravity suite of products:
https://antigravity.google

即使用户按错误提示把官方 CLI 装上、并让 CMDC_PAK_GOOGLE_CLIENT_ID/_SECRET 生效(WILDWORK_GEMINI_DIAG=1 可见),
仍然走不到账号入池。个人版 Code Assist 的这条 OAuth 路已经不通了。

取证(本机实测,逐条可复现)

  1. 凭据链路是通的:env 注入后 app.log 出现 gemini 登录流程已发起,授权页正常打开 —— 说明缺凭据那关已过。
  2. 授权页先报 403 restricted_client / Unregistered scope(s) in the request: https://www.googleapis.com/auth/cclog。
    internal/gemini/constants.go 的 Scopes 送 6 项,其中 cclog、experimentsandconfigs 是
    Antigravity 的内部 scope;CLI 那套客户端只登记了 cloud-platform + userinfo.email/profile
    (对照 google-gemini/gemini-cli@v0.64.0 packages/core/src/code_assist/oauth2.ts 的 OAUTH_SCOPE)。
  3. 换自建 OAuth 客户端也不行:cclog 是受限 scope,未经验证的应用申请不到。
  4. 最终定论来自 Google 自己:This client is no longer supported for Gemini Code Assist for individuals,
    官方要求迁移到 Antigravity。也就是说「复用 CLI 客户端 + authorization_code 换 refresh_token」
    这条路线对个人版账号已经关闭,不是参数问题。

附带的一个真 bug(与上面独立,建议一起修)

登录发起约 2 秒后,pollLogin 必现空指针崩溃:

2026/10/19 12:28:31.803048 gemini 登录流程已发起
2026/10/19 12:28:31.803060 登录流程已发起,等待浏览器完成…
2026/10/19 12:28:33.805566 login poll panic: runtime error: invalid memory address or nil pointer dereference

internal/app/app.go 里 pollLogin 的 defer 会读 a.loginKind、a.loginStateFP、a.muLogin,
而 finishLogin() 在不持 muLogin 的情况下就能被另一条路径清空这些字段:

// app.go:1834
func (a *App) finishLogin() {
	a.muLogin.Lock()
	a.loginBusy = false
	a.loginCtx, a.loginCancel, a.loginClient = nil, nil, nil
	a.loginKind = ""
	a.muLogin.Unlock()
}

但 StartLoginFor 在 flow.Start 失败分支直接调 a.finishLogin()(app.go:967-972),
POST /api/login/start 的 handler 里同一个 OP 可能并发进来(cancelLogin 也一样),
于是「A 的 pollLogin 还在跑 → B 失败把 loginKind/loginStateFP 清空 → A 的 defer 读到零值」。
前端 startLoginPoll() 每 3s 拉一次 /api/state,看到 login_busy=false 就收尾并提示「登录完成,正在同步账号…」,
假成功(和 #153 / H9 同一类症状)。

建议:pollLogin 里把本次登录的 kind/stateFP 作为参数捕获,defer 只用局部值;
或 finishLogin 只对调用方自己的那一轮生效(带 token/代次校验)。

建议方向

  • 短期:+Gemini 的 scope 收紧到 CLI 的三项 + openid,并在错误文案里说清「个人版 Code Assist 已停止支持该客户端」。
  • 中期:个人号改用 AI Studio API key(GEMINI_API_KEY,渠道凭据走 api_key 而不是 OAuth),
    这跟 dsh-our-free-model 里的 gemini 渠道形态一致;或者改走 Antigravity 的身份头 + 其宿主令牌(需要用户装 Antigravity)。
  • 兜底:文档里别再把「装 CLI 就能用」写成解决办法——用户装完反而得到一条更令人困惑的报错。

环境

  • macOS(arm64),WildWork 2.9.2(com.rockswang.wild-work)
  • 官方 CLI:@google/gemini-cli@0.64.0(已按提示安装,未做任何 patch)
  • 面板:http://127.0.0.1:7863

需要的话我可以把 login poll panic 的最小复现脚本和修复补丁提 PR。

Activity

  1. zrtzrt commented on Oct 19, 2026

    @zrtzrt
    Author

    更正:根因不是 scope,是「旧 Gemini CLI 的 OAuth 客户端对个人用户已停用」

    上一版 issue 里我把根因写成「Scopes 多带了 cclog / experimentsandconfigs,导致 403」。
    补测之后确认:scope 只是先撞上的那堵墙,真正的墙在客户端层面,改 scope 也过不去。

    现在的证据链

    层级 现象 说明
    授权请求 403 restricted_client + Unregistered scope(s): …/auth/cclog 这一层确实是 scope 越界(cclog 是 Antigravity 的内部 scope,CLI 客户端没登记)
    收紧 scope 之后 This client is no longer supported for Gemini Code Assist for individuals 即使只请求 openid + cloud-platform,这个 client_id 对个人号也不再放行
    换 @google/antigravity npm 上不存在该包(npm view @google/antigravity → 404) 原错误文案里的建议安装命令已经失效
    换 Antigravity 官方 CLI(agy) 登录成功,token 可用,loadCodeAssist 返回 free-tier (Antigravity) 新通道活着

    也就是说:Google 认可的是 Antigravity 那一套 client_id + 授权流程,而不是旧 CLI 的 client_id。
    WildWork 的 Gemini 渠道整套复制了旧 CLI 的 OAuth(accounts.google.com/o/oauth2/v2/auth + loopback 回调 + client_secret 换码),
    所以在个人号上必然复现同一个错误,与代码质量无关。

    为什么改 scope 不够

    internal/gemini/constants.go 的 Scopes 只是其中一环。把 cclog / experimentsandconfigs 去掉,
    授权页会前进到「选账号」,然后在登录后返回 no longer supported。
    两关的判据都在 Google 侧,且是按 client_id + 账号类型判的,客户端无法自行绕过。

    顺便验证了一个反直觉的点

    POST https://oauth2.googleapis.com/device/code  (client_id=681255809395-…)
    → {"error":"invalid_client","error_description":"Invalid client type."}
    

    旧 CLI 的客户端是「installed app + 授权码」类型,不支持设备码流程;
    所以「改成设备码登录」也不是替代方案(设备码只对特定 client 类型开放)。

    建议的修复方向(按可行性排序)

    1. 承认个人号走不通,把渠道拆成两条(推荐)

    • 个人号(Gemini Code Assist for individuals / AI Pro):不再走 OAuth,改为
      • API key(Google AI Studio,GEMINI_API_KEY),渠道凭据存 api_key 而不是 refresh_token;
      • 或直接提示用户「本渠道需 Antigravity 账号,请使用 Antigravity CLI」。
    • 企业 / Vertex 账号:保留现有 OAuth 路径。公告原文是
      enterprise customers will maintain existing access,这半边是可用的。

    建议 login_gemini.Start 先做一次 loadCodeAssist 预检(用当前凭据),拿到 tier 后按 tier 决定:
    free-tier / AI Pro 直接返回可读错误,而不是等用户在浏览器里点完才看到英文报错。

    2. 如果要继续复用 Antigravity 的身份(技术上可行,但请先确认合规)

    本次实测:Antigravity CLI 的 token 存在 macOS 钥匙串(svce=gemini, acct=antigravity,
    auth_method=consumer,含 refresh_token),用 antigravity/4.3.0 (cmdc-pak) 身份头调用
    https://daily-cloudcode-pa.googleapis.com/v1internal:loadCodeAssist 返回 200 + currentTier: {id:"free-tier", name:"Antigravity"}。

    也就是说:数据面已经通了,缺的只是「把这份 token 喂进 WildWork」。
    可行做法是照 internal/auth/load_gemini.go 的字段,把 Antigravity 的 token 落成
    auths/gemini-<uid>.json。但这是复刻私有格式,Antigravity 刷新 token 时可能覆盖,
    需要明确的导入 UI + 失效提示,不能当成默认自动流程。

    3. 若考虑走 Antigravity 官方 OAuth 申请新客户端

    Antigravity 的 client(884354919052-…)是 Web 类型:device/code 返回
    invalid_client: Invalid client type,只能走 loopback / 授权码回调 + PKCE。
    这需要在 Google Cloud Console 给自己的项目申请一个 Desktop 客户端,
    并把回调登记为 http://localhost:<port>/oauth-callback。
    风险:新客户端很可能同样拿不到 Code Assist 个人版资格(资格是按 client 白名单发的),
    这条建议先拿测试账号验证再动手。

    附加 bug(独立于上面的问题)

    login poll panic: runtime error: invalid memory address or nil pointer dereference

    pollLogin 的 defer 读 a.loginKind / a.loginStateFP / a.muLogin,
    而 finishLogin()(app.go:1834)在不持锁的情况下就能被另一条路径清空这些字段:

    func (a *App) finishLogin() {
    	a.muLogin.Lock()
    	a.loginBusy = false
    	a.loginCtx, a.loginCancel, a.loginClient = nil, nil, nil
    	a.loginKind = ""
    	a.muLogin.Unlock()
    }

    StartLoginFor 在 flow.Start 失败分支直接调 finishLogin()(app.go:967-972),
    POST /api/login/start 与 /api/login/cancel 并发进来时就会把「别人那一轮」的状态清空。
    建议把 kind/stateFP 作为参数捕获进 pollLogin,defer 只用局部值;或给 finishLogin 带代次校验。

    环境

    • macOS (arm64),WildWork 2.9.2(com.rockswang.wild-work)
    • @google/gemini-cli@0.64.0(按提示安装过,已验证)
    • Antigravity CLI agy 1.4.2(官方 install.sh),Google AI Pro 个人账号
  2. zrtzrt commented on Oct 19, 2026

    @zrtzrt
    Author

    修复方案:照 OmniRoute 的做法,把 Gemini 渠道从「OAuth 换 refresh_token」改成「导入 Antigravity 身份(agy token)+ 走 v1internal 信封」

    你说得对,API key 方案不成立:Gemini Code Assist for individuals 已经没有可用的 AI Studio 直连路径
    (个人号只能走 Code Assist 的 Cloud Code 端点,而那边的准入是按 client 身份 发的)。
    OmniRoute 在同样的政策变化下走的正是这条路,而且我在本机把它的协议逐字节复现成功了。

    一、已经实测通过的最小调用(可直接抄)

    本机 Antigravity CLI(agy 1.4.2)登录后,token 存在 macOS 钥匙串:
    security find-generic-password -a antigravity -w → go-keyring-base64:<json>,
    解出 {token:{access_token, refresh_token, expiry, token_type}, auth_method:"consumer", id_token}。

    用它直接打上游:

    POST https://daily-cloudcode-pa.googleapis.com/v1internal:streamGenerateContent?alt=sse
    Authorization: Bearer <access_token>
    Content-Type: application/json
    Accept: text/event-stream
    User-Agent: antigravity/cli/1.4.2 (aidev_client; os_type=darwin; arch=arm64; auth_method=consumer)
    {
      "project": "default-cli-project",
      "requestId": "agent/<epoch_ms>/<uuid>",
      "request": {
        "contents": [{ "role": "user", "parts": [{ "text": "Reply with exactly: PROXY_OK" }] }],
        "generationConfig": { "maxOutputTokens": 64 }
      },
      "model": "gemini-3.8-flash-high",
      "userAgent": "antigravity",
      "requestType": "agent"
    }

    返回(实测):

    data: {"response": {"candidates": [{"content": {"role": "model","parts": [{"text": "PROXY"}]}}],
          "usageMetadata": {...}, "modelVersion": "gemini-3.8-flash-n", ...}, "traceId": "..."}
    

    要点:响应是被包了一层信封的 —— 真正的 Gemini 载荷在 response 字段里,
    事件同时带 traceId / metadata。WildWork 现在的 internal/gemini/sse.go 如果按原生
    Gemini SSE 解析,会直接丢数据。这是需要改的第一处。

    二、对应到 WildWork 的改动清单

    1. internal/auth/load_gemini.go:新增「从 Antigravity 导入」

    沿用现有 load_codearts.go / load_zcode.go 的模式(扫描本机客户端凭据并导入),增加:

    • macOS:security find-generic-password -a antigravity -w(服务名 gemini),
      值形如 go-keyring-base64:<base64(json)>,解码后取 token.access_token / refresh_token / expiry
    • CLI 数据目录:~/.gemini/antigravity-cli/(存在即说明 agy 已登录)
    • IDE 数据目录:~/.gemini/antigravity/
    • 写入 auths/gemini-<uid>.json 时额外存
      • extra.auth_method = "consumer"(OAuth 刷新与 UA 都要用)
      • extra.project = "default-cli-project"(或 loadCodeAssist 返回的 cloudaicompanionProject)
      • extra.ua_flavor = "cli"

    2. internal/gemini/oauthclient.go:refresh 换 client

    现在的 refresh 用的是旧 Gemini CLI 的 client(681255809395-…),对个人号已停用。
    改用 Antigravity 的 client(agy 二进制内置,共 3 枚,主用):

    884354919052-36trc1jjb3tguiac32ov6cod268c5blh.apps.googleusercontent.com
    

    ⚠️ 这枚是 Web 类型(device/code 会返回 invalid_client: Invalid client type),
    所以只能走 refresh_token 换新 token,不能拿它做 loopback 授权码登录。
    这也解释了为什么「装 CLI 就行」那个提示是错的 —— 旧 CLI 的 client 拿不到了。

    refresh 请求头要带 User-Agent: antigravity/cli/<ver> (aidev_client; os_type=darwin; arch=arm64; auth_method=consumer)。

    3. internal/gemini/envelope.go:请求要包信封

    当前实现是裸的 {"contents":[...]}。改成:

    {"project": "<project>", "requestId": "agent/<ms>/<hex>", "request": { ...原始 body... },
     "model": "<upstreamModel>", "userAgent": "antigravity", "requestType": "agent"}
    • project 先用 loadCodeAssist 拿 cloudaicompanionProject;为空时回落到 default-cli-project
      (免费的 consumer 账号实测就是不返回 project,CLI 自己也是用这个兜底)
    • requestId 每次请求新生成,格式 agent/<epoch_ms>/<hex8>(OmniRoute 的 generateAntigravityRequestId)
    • request.sessionId:从账号 email 做 FNV-1a 64 位哈希,保证同一账号同一会话稳定

    4. internal/gemini/sse.go:解信封

    每个 data: 里的 JSON 形如 {"response": {...}, "traceId": "...", "metadata": {}},
    要取 obj.response 再当 Gemini 原生响应处理;traceId 用于日志串联。

    5. 模型 ID

    上游要带档位后缀,实测可用:

    面板显示 上游 model
    gemini-3.8-flash-high gemini-3.8-flash-high
    gemini-3.8-flash-medium gemini-3.8-flash-medium
    gemini-3.8-flash-low gemini-3.8-flash-low
    gemini-3.1-pro-high gemini-pro-agent(上游 id 不同,需别名)

    agy models 可实时列出当前账号的可用清单(gemini-3.8-flash-{high,medium,low}、
    claude-sonnet-4-6、claude-opus-4-6-thinking、gpt-oss-120b-medium),
    建议渠道模型表改成「从 fetchAvailableModels 探测 + 静态兜底」。

    6. 失败诊断(很重要)

    OmniRoute 的排查文档里记了个关键坑:agy/Cloud Code 读的是账号后端的 Country Association 字段,
    不是当前出口 IP。本机实测也印证了:

    error: FAILED_PRECONDITION (code 400): User location is not supported for the API use.
    

    换 IP 后立刻成功。所以登录/调用失败时要区分:

    • User location is not supported → 账号地区关联问题,引导用户去
      https://policies.google.com/country-association-form 改地区
    • This client is no longer supported → 还在用旧 CLI 的 client,没切到 Antigravity 身份
    • not eligible for Antigravity → 账号没有 Code Assist 资格

    三、最小可行顺序(建议 PR 拆成 3 个提交)

    1. 导入:load_gemini.go 支持从钥匙串 / ~/.gemini/antigravity-cli 导入 token(不动网络层)
    2. 信封:envelope.go + sse.go 改成 project/requestId/request 结构与 response 解包
    3. 身份:oauthclient.go 换成 Antigravity client + UA,refresh 后回写

    第 1 步单独就能让用户「先用 agy 登录 → 面板导入」,第 2、3 步才是完整的产品化。

    四、已验证的本机事实(供你复现)

    • agy --version → 1.4.2;agy -p 'Reply with exactly: AGY_OK' → AGY_OK
    • loadCodeAssist(携带 agy token + Antigravity UA)→ 200,currentTier: {id:"free-tier", name:"Antigravity"}
    • 上面那个 streamGenerateContent 请求 → SSE 正常返回 response.candidates
    • 旧 CLI client 的 device/code → invalid_client: Invalid client type(确认它不支持设备码,也不是可替代路径)
  3. rockswang commented on Oct 19, 2026

    @rockswang
    Owner

    你要是验证成功的话,建议直接PR,我手上没有gemini账号

  4. rockswang commented on Oct 19, 2026

    @rockswang
    Owner

    短期处置已落地(master 4252d48)

    感谢极高质量的下钻取证——两堵墙的分层(scope → client 停用)和 device/code 的 invalid_client 佐证都采纳了。

    本轮落地的部分:

    1. scope 收紧:Scopes 剔除 cclog / experimentsandconfigs(internal/gemini/constants.go),消除第一堵墙 403 restricted_client。已确认 Cloud Code 端点鉴权只认 access_token 不复核 scope 清单,运行时行为不变。回归:TestScopesNoInternalScopes。
    2. 错误文案可行动化:exchange 失败时按信封字段挂 geminiErrorHint 引导——命中 "no longer supported" 时直说「Google 已停用该 OAuth 客户端对个人版 Code Assist 的授权,属上游政策变更,非本工具或账号问题」,避免用户按官方报错装 CLI 后撞同一堵墙。回归:TestGeminiErrorHint。

    暂不落地的部分(同意你的最小可行顺序,但想先解决一个前置):

    • Antigravity 通道(导入 agy token / 信封改造 / 身份切换)涉及整套 client_id + UA + envelope 的替换,是产品级改动。你实测过 streamGenerateContent 全链路可通,这块证据充分;但 gemini-pro-agent 别名映射、fetchAvailableModels 探测、以及地区关联(Country Association)的用户引导都还没有回归护栏,不想在 2.9.x 的 hotfix 线里夹带。已作为独立方向记录,预计在 2.10 线做(到时大概率直接基于你列的三步拆 PR,欢迎认领)。
    • 文档侧「装 CLI 就能用」的表述会在 Antigravity 改造的同一个 PR 里一并修正(现在改了也是半句话)。

    另外你附带的 pollLogin panic 问题正是 #168 —— 根因(pending 态 done 为 nil 仍被调用)已由 PR #169 修复(你的 #171 与它同根因同修法,感谢撤回并留下生产 A/B 数据,对 review 很有帮助)。

  5. rockswang commented on Oct 19, 2026

    @rockswang
    Owner

    短期处置(scope 收紧 + 错误引导)随 v2.9.3 发布(tag 已打,CI 产物已出)。

    Antigravity 通道(agy token 导入 / 信封改造 / 身份切换)作为独立方向规划在 2.10 线,取证链路保留在本 issue 供届时直接引用。先关闭本 issue——个人版用户当前请以错误文案里的引导为准(上游政策变更,非本工具/账号问题);有 Antigravity 集成诉求时欢迎在此 issue 或新 issue 继续跟进。

  6. added 6 commits that reference this issue on Oct 22, 2026
    b82cef7
    aa0f1c1
    9bd1d20
    47b9954
    a377240
    48aed6d
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions