我在 Mac 上用一个菜单栏/刘海小工具(Agent HUD,我 fork 了一份)盯着各个 AI 的额度。Grok 那一栏总是显示一句黄色的 “Sign in to Grok CLI, then refresh quota” 加一个几天前的旧百分比,哪怕我账号明明一直登着。

这篇记这件事怎么一层层查到根因(不是它说的"没登录"),为什么升级上游没能彻底修好,以及我最后写的那个修复——顺手给上游开了 PR。

所有 token、第三方账号邮箱均未出现;仓库名、PR 号是公开的 GitHub 信息。


一、TL;DR

  • 现象:HUD 里 Grok 永远显示"去登录 Grok CLI"加一个几十小时前的旧额度,重新登录也不消失。
  • 根因不是没登录:Grok CLI 的 access token 存在 ~/.grok/auth.json 里,有效期只有 6 小时,而且只有 CLI 自己启动时才会用 refresh_token 去续期。HUD 直接读磁盘上的 token 去查额度接口,token 一过期就拿到 401,于是一直显示上一次成功读到的旧值。
  • 几个小时不开 grok,token 必过期,这个提示就会回来。
  • 上游(jazzenchen/agent-hud-open)到 v0.5.3 都没处理"token 过期"这种情况,只当成"未登录"。
  • 我写的修复:当所有 Grok 登录都过期时,HUD 先跑一次 grok models 让 CLI 自己续期,再重新读 auth.json。它自己绝不碰 refresh_token(碰了万一 token 轮换,CLI 可能被登出)。最多 15 分钟试一次,30 秒超时。
  • 修复合进了我的 fork,也给上游开了 PR(#63,4 个文件,+147/−3,919 个测试通过)。
  • 诚实一笔:“app 在 token 过期后自己续期"这条端到端路径,到写这篇时只有单元测试覆盖,没在真实过期后亲眼看它恢复过。

二、现象

Grok 那一栏长期停在这样一个状态:

  • 一句黄色提示 “Sign in to Grok CLI, then refresh quota”
  • 一个灰色的旧百分比,旁边写着 “Last read 36h ago” / 更夸张时 “Last read 94h 57m ago”

直觉上这是"掉登录了”。但我在手机和 CLI 里都确认账号是登着的——grok 能正常用。所以提示文案在误导人。


三、排查

3.1 先看磁盘上的 token

读 ~/.grok/auth.json(不打印 token 本身,只看时间戳):

  • access token 是最后一次登录时签发的,有效期 6 小时;
  • 我上一次开 grok 已经是几天前,token 早就过期了;
  • 文件里有 refresh_token,但 access token 没被续过。

3.2 确认过期 token 会被拒

用这个过期 token,带上 HUD 实际发的那组请求头,手动 curl 一下 Grok 的计费接口:

  • 过期 token → HTTP 401;
  • 跑一次 grok models(CLI 启动会用 refresh_token 换新 token)之后再查 → 200,返回已用 2%。

到这里根因就清楚了:

Grok CLI 的 access token 只活 6 小时,而且只有 CLI 自己启动时才续期。任何像 HUD 这样直接读 auth.json 里的 token 去查额度的程序,只要你几小时没开 grok,拿到的就是一个过期 token,接口回 401,于是只能继续显示上一次成功的旧读数。

3.3 一个没钉死的岔路:重新登录后面板不刷新

10-09 那次还撞上另一个现象:我手动开了一次 grok,CLI 在 12:25 自动续了 token,HUD 后台也在 12:25:33 成功读到了新额度(98% left,记录进了它的额度历史库),但面板上显示的还是旧的 “Last read 36h ago”。重启 HUD 后面板就正常了。

我顺着 UsageCollector → CombinedUsageProvider → RetainedUsageProvider 看了一圈,数据层是对的——新读数确实存下来了——问题更像是面板一直开着时某个视图状态没跟着更新。这个具体是哪段代码出的,我当时没钉死。

因为我的 fork 落后上游很多,我先把上游 v0.4.38 合了进来(领先我 110 个 commit,11 个文件、13 处冲突)。合并时保留了 fork 自己有意为之的行为(按屏幕的 Off 模式、显示"剩余"而非"已用"、Grok 用量的 protobuf 兜底请求),命名和结构用上游的。合完之后"重新登录后面板不刷新"这个现象没再复现——上游那批关于后台刷新的改动大概顺带修掉了,但我没法指认是哪一个。

3.4 上游也没修"过期"这件事

到 10-13,Grok 又卡在旧读数上(token 过期四天,这台 Mac 一直没开过 grok)。这次我去翻上游:它到 v0.5.3 有大约 10 个 Grok 相关改动,但没有一个处理 token 过期——GrokClient 还是只读 auth.json,不续期,token 过期就当"未登录"。所以这个得自己写。


四、修复

思路很直接:HUD 自己不要去碰 refresh_token,而是让真正该续期的人——Grok CLI——去续。

新增 Sources/AgentHUDCore/Providers/Grok/GrokCLIRenewal.swift:当检测到每一个 Grok 登录都过期时,HUD 跑一次 grok models。这会让 CLI 用自己的 refresh_token 换一个新 access token 写回 auth.json,HUD 再重新读一遍。

几个有意的约束:

  • 绝不自己用 refresh_token。 如果 HUD 自己拿 refresh_token 去换,而服务端恰好轮换了 refresh_token,CLI 那边就可能被登出。把续期交给 CLI 就没这个风险。
  • 最多 15 分钟试一次,30 秒超时。 一台已经彻底登出的机器不会被反复拉起 grok。
  • 跟着仓库里已有的模式走:代码里本来就有"spawn 官方 CLI 读登录状态"的先例(claude auth status),照着做。

配套写了 4 个回归测试,docs/providers.md 补了一段说明。全量 930 个测试通过(其中一个 AgentSeparationTests 在整套跑时偶发失败、单跑通过,在未改动的上游上也这样,看着是时序敏感,不是这次改动引入的)。

打了 release 包替换掉 /Applications 里的 app,重启后 HUD 显示 Grok 8% used(92% left),和服务端对得上。

开 PR 给上游

这个修复对任何用这个工具的人都有用,所以从干净的上游 main 拉了一个分支 grok-cli-token-renewal,只 cherry-pick 那一个修复 commit(不带我 fork 自己的合并和私有特性),重写成纯英文的提交信息和描述,在未改动的上游上跑了全量 919 个测试、0 失败,然后开了 PR:

https://github.com/jazzenchen/agent-hud-open/pull/63 —— 4 个文件,+147/−3。


五、还没验证的部分

写这篇时有一条路径只有单元测试覆盖、没在真实环境里亲眼看它跑过:

我是手动跑 grok models 把 token 续上的。“HUD 在 token 真的过期之后、不靠我手动开 grok,自己把它续回来"这件事,到目前为止只有单测证明。真正的端到端场景要等下一次 token 自然过期、且期间我没碰 grok 时才会发生。

按房规,这里不写成"修好了”,写成"逻辑对、单测过,真实场景待验证"。


六、可复用的结论

  1. 任何读 ~/.grok/auth.json 里 token 的第三方程序,都要考虑 token 已过期。 Grok 的 access token 只活 6 小时,续期只发生在 CLI 启动时。几小时不开 grok,直接读 token 的工具必然拿到过期值。
  2. “去登录"不等于真没登录。 账号登着、refresh_token 有效,但磁盘上的 access token 过期,一样会让一个只读 token 的工具显示"未登录”。区分这两者,看 auth.json 里的签发/过期时间,再用 token 手动打一次接口看是 401 还是 200。
  3. 续期这种会改共享状态(token 轮换)的事,交给拥有它的那个程序去做,别让旁观者(监控工具)自己动手,否则可能把对方弄登出。
  4. 合并一个落后很多的 fork 时,先把"哪些是自己有意改的行为"列清楚,冲突才好逐处决定是保留自己的还是采上游的。