Tags: longbridge/longbridge-terminal
Tags
cli: Recover from a wrong access point and a rejected login (#305) CLI 有三处「拿着一份服务端不认的凭证 / 连着一个连不通的接入点,自己回不来」的问题,都要用户先受苦、再手动补救。这个 PR 把三处都改成自愈。 --- ## 1. 接入点选错后不会自愈 地理定位只是「哪个接入点服务得更好」的近似值。在分流代理下、或者用户只是换了个地方,它就猜错了,客户端会一直钉在一个更慢、甚至根本连不通的接入点上。`check` 里那段分支的注释直接点了名:*"the case that stranded overseas clients."* `check` 一直会实测两个接入点并纠正,但前提是用户想得起来去跑它 —— 而没人会在还没受苦之前主动跑。 **改动**:实测改为**每次启动时在后台跑**。不打印、不等待,重新钉好的结果落盘,下一条命令直接用。会话中途换接入点意味着重建两个 context 和它们的 WebSocket,与「无感」正好相反,所以不做。 对着线上 API 验证过,一个故意写错的 `global` 判定自己纠正了回来,用户侧没有任何输出: ``` Access point repinned to CN by latency (global 140ms ok=true, cn 79ms ok=true) ``` 探测和重钉规则从 `cli::check` 挪进 `region` —— 缓存和判定本来就归它管。`check` 只保留报告,现在与静默路径共用同一条决策链,诊断结果和实际行为不会再各说各话。`check` 的输出和 JSON 结构未变(与已发布的二进制逐字比对过)。 **节流**:region 缓存新增第三行,记录上次实测的时间戳,与 geotest 的时间戳分开 —— 实测的代价高一个数量级,而且它才是能推翻 geotest 判定的那个。旧缓存没有第三行,读作「从未实测」;旧版二进制会忽略不认识的第三行,所以格式双向兼容。时间戳在探测**开始前**就写入,用的是 `update::spawn_version_check` 同样的手法:短命令通常在探测结束前就退出了,不先写就会导致之后每一次运行都重新发起一个永远落不了地的探测。 **启动开销**:探测在发出第一个请求前先等三秒。进程还在启动时就去建两个 TLS 客户端,会拖慢用户真正要做的事 —— 足以把 ACP 的 `initialize` 响应拖过编辑器的超时,表现为 `tests/acp_stdio.rs` 稳定失败。等待还有个好处:短命令在探测产生开销之前就退出了,真正为它买单的是那些本来就活得比它久的长进程。 改动后实测首次 ACP 响应耗时: | | 冷启动 | 热启动 | |---|---|---| | 改动前 | 1.71s | 0.14s | | 改动后 | 1.89s | 0.15s | --- ## 2. 环境变量会悄悄接管已登录的账号 `init_contexts` 之前先试 `Config::from_apikey_env()`,失败才回落到 OAuth。于是 shell 里残留的一份 `LONGBRIDGE_APP_KEY` / `LONGBRIDGE_APP_SECRET` / `LONGBRIDGE_ACCESS_TOKEN` 会静默顶掉已登录的账号:每条命令都 401004,而 `auth status` 还报告 token 一切正常 —— 因为它看的是本地那份 OAuth token,跟真正发出去的凭证不是同一个东西。 **改动**:认证只走 OAuth,这三个变量再也不读。`using_api_key`、整条 API-key 分支、以及那个只为拒绝 API-key 模式而存在的 agent 守卫一并删除。 实测(带假凭证跑同一条命令): | | 结果 | |---|---| | 已发布版本 | `Error: API error (code 401004): token invalid` | | 本 PR | 正常返回 700.HK 行情 | --- ## 3. token 被服务端拒绝后彻底卡死 这是用户报上来的现象:`longbridge check` 显示 ``` token OK valid api error: openapi error: code=401003: token expired ``` `auth status` 同时显示 token 完全正常 —— 状态 valid,access token 还有 13 天到期,refresh token 还有 29 天,当天刚登录过。但每一个 API 都是 401003,`longbridge ai` 里会话列表打不开、行情卡在 "Fetching the quote…"。用户没登出过,也没换过设备。 根因是 **`refresh_if_expired` 只拿本地记录的 `expires_at` 跟本机时钟比**,而那个时间戳只是本机对服务端状态的猜测。两者会脱节: - **刷新会让凭证在服务端轮换**。两个进程同时刷新就会赛跑,谁后写盘谁说了算 —— 而后写的那份很可能是**更早**那次交换的结果,服务端早已作废。盘上于是留下一份「本地看还有两周、服务端已经不认」的 token,谁也没登出,却谁也用不了。用户那份 token 的登录时间 15:03、过期时间却是 15:31 起算,正是中间发生过一次刷新的痕迹。 - **没有提前量**。只剩几秒的 token 也能通过检查,然后在请求途中被服务端拒绝。 - **服务端说了不算**。就算服务端明确回 401003,CLI 也不会去刷新 —— 它只信本地时间戳。全链路(包括 SDK 的 httpclient)没有任何一处 401 处理。 **改动**: - 刷新加**跨进程锁**;等锁期间发现盘上 token 已被别的进程换过,就直接用那一份,不再重复交换(重复交换正是互相作废的来源)。锁是 advisory 的:拿不到就照常刷新,一个卡住的锁文件永远不该把用户锁在自己的 CLI 外面。 - 刷新提前量对齐 SDK 自己的 5 分钟。 - 服务端回 401003/401004 时,**以服务端的回答为准**:无视本地时间戳强制刷新,并重建所有 context。重建才是长进程需要的那一半 —— SDK 的 access token 是从进程内存里发的,盘上刷新过的 token 没有别的路径能送达它。 - `ai` 会话列表、行情、对话握手都走这条自愈路径重试一次(都是只读或未被服务端执行的请求)。 - 单条 CLI 命令**不自动重跑**:命令可能在下过单之后的某个请求上才失败。它只刷新凭证,并告诉用户「登录已自动刷新,请重新运行」。 ## 4. `check` 不再把被拒的 token 报成 valid 上面那行 `token OK valid` 挨着 `401003 token expired` 的自相矛盾,来自 `check` 把任何 API 失败都记成 `token_ok = true`。现在由服务端的回答决定;其他 API 错误说明不了 token 的问题,仍然算 valid。 --- ## 验证 `cargo fmt`、`cargo clippy` 干净。760 个测试全过,含 `acp_stdio` 5/5。 新增测试覆盖:region 缓存第三行的读写、缺失与过期;`is_token_rejected` 只认 401003/401004(429002 等一律不算,否则每次限流都会去刷新);刷新锁在 drop 时释放 —— 不释放的话,下一个进程要空等满整个 stale 超时去等一个早就没了的持有者。 行情输出与已发布二进制逐字比对,结构一致(仅实时价格与成交量随行情跳动)。 Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
PreviousNext