fix(garmin): 重新绑定走的是同一个被封的登录接口,也得拦住

用户问:限流了,重新输账号密码验证码换个新令牌行不行。

不行,而且是最糟的一种试法。`garth.login()` 和 `refresh_oauth2()` 打的是
同一个 SSO 端点,流程还更重;限流按**账号**计(不是按 IP、按 UA),换设备
换网络都绕不开;而窗口内每次尝试都会把窗口往后推。

而这正是被卡住时第一个会去试的操作,代码里却只有 `_connect` 的刷新有闸门,
重新绑定那条路照发不误。

- start_login 在 sso 冷却窗口内直接拒绝,不建会话行、不碰网络
- 错误信息说清三件事:为什么现在不试、什么时候恢复、换设备没用
- 路由返 429(请求本身没毛病,是该晚点再来)并带 retryAfterSeconds
- 数据端点的 429 不参与拦截,force 可以推翻

前端补上 UI:报错文案早先承诺了「同步页选择强制重试」,但那个按钮不存在。
现在只在被冷却拒绝之后才出现,样式刻意做得不像第二个「开始同步」——它是给
估算失准时的出口,不是随手可点的第二选择。

顺带修一个正要被我引入的 bug:`onClick={syncHistory}` 会把 MouseEvent 当成
force 传进去,等于每次点开始同步都跳过冷却。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
ericwyuan
2026-09-04 06:20:05 +08:00
parent fd5e3363fd
commit 1c80b16325
6 changed files with 163 additions and 10 deletions

View File

@@ -613,13 +613,21 @@ class ApiClient {
* plaintext password must be supplied because only a hash is kept — and an
* MFA-protected account cannot log in this way at all (see garmin_login.py).
*/
/** Starts a sync in the background; poll getGarminSyncStatus for progress. */
async syncGarminData(days?: number, garminPassword?: string) {
/**
* Starts a sync in the background; poll getGarminSyncStatus for progress.
*
* `force` discards the recorded rate-limit cooldown before trying. That
* deadline is the app's own 24h guess, not something Garmin stated, so the
* user must be able to overrule it — but only deliberately: attempting a
* login inside Garmin's real window extends the window.
*/
async syncGarminData(days?: number, garminPassword?: string, force?: boolean) {
const { data } = await this.client.post<{
status: string; days?: number; message?: string; retryAfterSeconds?: number;
}>(
'/garmin/sync',
{
...(force ? { force: true } : {}),
// `days` must survive being 0 — that is "全部历史", not "unset".
// `days ? …` dropped it, so the backend fell back to its 7-day
// default and a full backfill silently pulled a week.