diff --git a/backend/internal/service/maintenance.go b/backend/internal/service/maintenance.go index 0ce70e9..02f418a 100644 --- a/backend/internal/service/maintenance.go +++ b/backend/internal/service/maintenance.go @@ -131,12 +131,17 @@ func (m *MaintenanceService) tick(ctx context.Context) { log.Printf("maintenance: roll_reset: %v", err) } - // 1b. Runway tokens have no refresh — once the JWT expiry (its reset marker) - // passes, mark them dead directly instead of letting them 401 on next use. - if n, err := m.tokens.ExpireByReset(ctx, "runway"); err != nil { - log.Printf("maintenance: expire_runway: %v", err) - } else if n > 0 { - log.Printf("maintenance: expired %d runway token(s)", n) + // 1b. Runway/grok tokens have no refresh — once the reset marker passes, mark + // them dead directly instead of letting them 401 on next use. Runway's + // marker is the JWT expiry; grok's is the credits reset (grok sso can't be + // renewed either — 失效就失效 — so a purchased short-lived account that has + // lapsed by its reset time is treated as dead rather than re-scheduled). + for _, pool := range []string{"runway", "grok"} { + if n, err := m.tokens.ExpireByReset(ctx, pool); err != nil { + log.Printf("maintenance: expire_%s: %v", pool, err) + } else if n > 0 { + log.Printf("maintenance: expired %d %s token(s)", n, pool) + } } // 1c. Proactively renew krea/imagine sessions ~10min before expiry so a diff --git a/backend/internal/service/tokens.go b/backend/internal/service/tokens.go index f52c2f8..4513ada 100644 --- a/backend/internal/service/tokens.go +++ b/backend/internal/service/tokens.go @@ -1451,7 +1451,11 @@ func (s *TokenService) Quota(ctx context.Context, pool, id string) (map[string]a meta["cached_quota_total"] = total } patch["meta"] = meta - if reset := strings.TrimSpace(stringValue(data["reset_after"])); reset != "" { + // Recovery time is the death deadline: the maintenance sweep expires a grok + // account once this marker passes (grok sso can't be renewed). Only stamp it + // when still unset (import couldn't resolve it) — never move it forward on a + // later refresh, so an admin opening 账号管理 can't push the death time out. + if reset := strings.TrimSpace(stringValue(data["reset_after"])); reset != "" && strings.TrimSpace(item.CachedQuotaResetAfter) == "" { patch["cached_quota_reset_after"] = reset item.CachedQuotaResetAfter = reset }