From 4e2844ff51b2c41c6cb9450b91eb745ccd0166a2 Mon Sep 17 00:00:00 2001 From: chiyi Date: Fri, 3 Jul 2026 02:10:30 +0800 Subject: [PATCH] =?UTF-8?q?grok=E4=BF=AE=E5=A4=8D=E6=81=A2=E5=A4=8D?= =?UTF-8?q?=E6=97=B6=E9=97=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/provider/grok/client.go | 17 ++++++++++++----- backend/internal/service/tokens.go | 14 ++++++++++++++ 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/backend/internal/provider/grok/client.go b/backend/internal/provider/grok/client.go index 70bd0c4..f625ad8 100644 --- a/backend/internal/provider/grok/client.go +++ b/backend/internal/provider/grok/client.go @@ -142,7 +142,7 @@ func (c *Client) FetchCreditsBalance(ctx context.Context, token string) (map[str // GetGrokCreditsConfig field #1 is the credits USED this period (not remaining): // an exhausted account reads 100, a fresh one reads ~0. Remaining = 100 - used. - used, reset, ok := parseCreditsConfig(raw) + used, _, ok := parseCreditsConfig(raw) if !ok { return unknownBalance("unparsable credits config"), nil } @@ -154,11 +154,13 @@ func (c *Client) FetchCreditsBalance(ctx context.Context, token string) (map[str } remaining := fullCredits - used - // 恢复时间: prefer the subscription's billing-period end (when the plan renews - // and credits reset) over the credits-config timestamp. Free accounts have no - // subscription, so this falls back to the credits-config reset above. + // 恢复时间: taken solely from the subscription's billing-period end (when the + // plan renews and credits reset). The credits-config weekly reset timestamp is + // intentionally NOT used as a fallback — an account with no active subscription + // has no recovery time. + reset := "" sub, _ := c.FetchSubscription(ctx, token) - if sub != nil && strings.TrimSpace(sub.BillingPeriodEnd) != "" { + if sub != nil { reset = strings.TrimSpace(sub.BillingPeriodEnd) } return map[string]any{ @@ -273,6 +275,7 @@ func (c *Client) FetchSession(ctx context.Context, token string) (email, userID return "", "", fmt.Errorf("%w: session http %d", ErrTemporaryUpstream, resp.StatusCode) } var body struct { + Status string `json:"status"` Session struct { Email string `json:"email"` UserID string `json:"userId"` @@ -281,6 +284,10 @@ func (c *Client) FetchSession(ctx context.Context, token string) (email, userID if err := json.Unmarshal(raw, &body); err != nil { return "", "", fmt.Errorf("%w: session non-json", ErrTemporaryUpstream) } + // A dead sso cookie answers 200 {"status":"unauthenticated"} rather than 401. + if !strings.EqualFold(body.Status, "authenticated") { + return "", "", ErrAuth + } return strings.TrimSpace(body.Session.Email), strings.TrimSpace(body.Session.UserID), nil } diff --git a/backend/internal/service/tokens.go b/backend/internal/service/tokens.go index 4513ada..e396b1e 100644 --- a/backend/internal/service/tokens.go +++ b/backend/internal/service/tokens.go @@ -867,6 +867,9 @@ func (s *TokenService) ImportGrokToken(ctx context.Context, ssoToken, tokenID st email := "" if s.grok != nil { s.applyProxy(ctx) + // Best-effort email resolution. A dead/blocked probe must NOT block import: + // land the account and let the async checkPendingGrok disable it if the + // session is truly dead. Empty email just falls back to the session id. if e, _, ferr := s.grok.FetchSession(ctx, ssoToken); ferr == nil { email = e } @@ -923,6 +926,17 @@ func (s *TokenService) checkPendingGrok(tokenID, ssoToken string) { return } s.applyProxy(ctx) + // Validate the session first: a dead sso answers 200 {"status":"unauthenticated"}, + // which FetchSession now maps to ErrAuth → disable. Also backfill the email if + // import couldn't resolve it (empty account_email currently shows the session id). + if email, _, serr := s.grok.FetchSession(ctx, ssoToken); serr != nil { + if errors.Is(serr, grok.ErrAuth) { + s.finishPending(ctx, "grok", tokenID, "disabled", true, nil) + return + } + } else if strings.TrimSpace(email) != "" { + _, _ = s.tokens.Update(ctx, "grok", tokenID, map[string]any{"account_email": strings.TrimSpace(email)}) + } data, err := s.grok.FetchCreditsBalance(ctx, ssoToken) if err != nil { if errors.Is(err, grok.ErrAuth) {