diff --git a/backend/internal/service/account.go b/backend/internal/service/account.go index bfe3822c..5d461b9c 100644 --- a/backend/internal/service/account.go +++ b/backend/internal/service/account.go @@ -110,6 +110,28 @@ func (a *Account) GetCredential(key string) string { } } +// GetCredentialAsTime 解析凭证中的时间戳字段,支持多种格式 +// 兼容以下格式: +// - RFC3339 字符串: "2025-01-01T00:00:00Z" +// - Unix 时间戳字符串: "1735689600" +// - Unix 时间戳数字: 1735689600 (float64/int64/json.Number) +func (a *Account) GetCredentialAsTime(key string) *time.Time { + s := a.GetCredential(key) + if s == "" { + return nil + } + // 尝试 RFC3339 格式 + if t, err := time.Parse(time.RFC3339, s); err == nil { + return &t + } + // 尝试 Unix 时间戳(纯数字字符串) + if ts, err := strconv.ParseInt(s, 10, 64); err == nil { + t := time.Unix(ts, 0) + return &t + } + return nil +} + func (a *Account) GetModelMapping() map[string]string { if a.Credentials == nil { return nil @@ -324,19 +346,7 @@ func (a *Account) GetOpenAITokenExpiresAt() *time.Time { if !a.IsOpenAIOAuth() { return nil } - expiresAtStr := a.GetCredential("expires_at") - if expiresAtStr == "" { - return nil - } - t, err := time.Parse(time.RFC3339, expiresAtStr) - if err != nil { - if v, ok := a.Credentials["expires_at"].(float64); ok { - tt := time.Unix(int64(v), 0) - return &tt - } - return nil - } - return &t + return a.GetCredentialAsTime("expires_at") } func (a *Account) IsOpenAITokenExpired() bool { diff --git a/backend/internal/service/account_test_service.go b/backend/internal/service/account_test_service.go index 318be8b8..7dd451cd 100644 --- a/backend/internal/service/account_test_service.go +++ b/backend/internal/service/account_test_service.go @@ -12,7 +12,6 @@ import ( "log" "net/http" "regexp" - "strconv" "strings" "time" @@ -187,9 +186,8 @@ func (s *AccountTestService) testClaudeAccountConnection(c *gin.Context, account // Check if token needs refresh needRefresh := false - if expiresAtStr := account.GetCredential("expires_at"); expiresAtStr != "" { - expiresAt, err := strconv.ParseInt(expiresAtStr, 10, 64) - if err == nil && time.Now().Unix()+300 > expiresAt { + if expiresAt := account.GetCredentialAsTime("expires_at"); expiresAt != nil { + if time.Now().Add(5 * time.Minute).After(*expiresAt) { needRefresh = true } } diff --git a/backend/internal/service/antigravity_quota_refresher.go b/backend/internal/service/antigravity_quota_refresher.go index dd579ef1..c4b11d73 100644 --- a/backend/internal/service/antigravity_quota_refresher.go +++ b/backend/internal/service/antigravity_quota_refresher.go @@ -191,7 +191,7 @@ func (r *AntigravityQuotaRefresher) refreshAccountQuota(ctx context.Context, acc // isTokenExpired 检查 token 是否过期 func (r *AntigravityQuotaRefresher) isTokenExpired(account *Account) bool { - expiresAt := parseAntigravityExpiresAt(account) + expiresAt := account.GetCredentialAsTime("expires_at") if expiresAt == nil { return false } diff --git a/backend/internal/service/antigravity_token_provider.go b/backend/internal/service/antigravity_token_provider.go index efd3e15f..cbd1bef4 100644 --- a/backend/internal/service/antigravity_token_provider.go +++ b/backend/internal/service/antigravity_token_provider.go @@ -55,7 +55,7 @@ func (p *AntigravityTokenProvider) GetAccessToken(ctx context.Context, account * } // 2. 如果即将过期则刷新 - expiresAt := parseAntigravityExpiresAt(account) + expiresAt := account.GetCredentialAsTime("expires_at") needsRefresh := expiresAt == nil || time.Until(*expiresAt) <= antigravityTokenRefreshSkew if needsRefresh && p.tokenCache != nil { locked, err := p.tokenCache.AcquireRefreshLock(ctx, cacheKey, 30*time.Second) @@ -72,7 +72,7 @@ func (p *AntigravityTokenProvider) GetAccessToken(ctx context.Context, account * if err == nil && fresh != nil { account = fresh } - expiresAt = parseAntigravityExpiresAt(account) + expiresAt = account.GetCredentialAsTime("expires_at") if expiresAt == nil || time.Until(*expiresAt) <= antigravityTokenRefreshSkew { if p.antigravityOAuthService == nil { return "", errors.New("antigravity oauth service not configured") @@ -91,7 +91,7 @@ func (p *AntigravityTokenProvider) GetAccessToken(ctx context.Context, account * if updateErr := p.accountRepo.Update(ctx, account); updateErr != nil { log.Printf("[AntigravityTokenProvider] Failed to update account credentials: %v", updateErr) } - expiresAt = parseAntigravityExpiresAt(account) + expiresAt = account.GetCredentialAsTime("expires_at") } } } @@ -128,18 +128,3 @@ func antigravityTokenCacheKey(account *Account) string { } return "ag:account:" + strconv.FormatInt(account.ID, 10) } - -func parseAntigravityExpiresAt(account *Account) *time.Time { - raw := strings.TrimSpace(account.GetCredential("expires_at")) - if raw == "" { - return nil - } - if unixSec, err := strconv.ParseInt(raw, 10, 64); err == nil && unixSec > 0 { - t := time.Unix(unixSec, 0) - return &t - } - if t, err := time.Parse(time.RFC3339, raw); err == nil { - return &t - } - return nil -} diff --git a/backend/internal/service/antigravity_token_refresher.go b/backend/internal/service/antigravity_token_refresher.go index 8ee2d25c..b4739025 100644 --- a/backend/internal/service/antigravity_token_refresher.go +++ b/backend/internal/service/antigravity_token_refresher.go @@ -2,7 +2,6 @@ package service import ( "context" - "strconv" "time" ) @@ -34,16 +33,11 @@ func (r *AntigravityTokenRefresher) NeedsRefresh(account *Account, _ time.Durati if !r.CanRefresh(account) { return false } - expiresAtStr := account.GetCredential("expires_at") - if expiresAtStr == "" { + expiresAt := account.GetCredentialAsTime("expires_at") + if expiresAt == nil { return false } - expiresAt, err := strconv.ParseInt(expiresAtStr, 10, 64) - if err != nil { - return false - } - expiryTime := time.Unix(expiresAt, 0) - return time.Until(expiryTime) < antigravityRefreshWindow + return time.Until(*expiresAt) < antigravityRefreshWindow } // Refresh 执行 token 刷新 diff --git a/backend/internal/service/gemini_token_provider.go b/backend/internal/service/gemini_token_provider.go index f587b500..2195ec55 100644 --- a/backend/internal/service/gemini_token_provider.go +++ b/backend/internal/service/gemini_token_provider.go @@ -50,7 +50,7 @@ func (p *GeminiTokenProvider) GetAccessToken(ctx context.Context, account *Accou } // 2) Refresh if needed (pre-expiry skew). - expiresAt := parseExpiresAt(account) + expiresAt := account.GetCredentialAsTime("expires_at") needsRefresh := expiresAt == nil || time.Until(*expiresAt) <= geminiTokenRefreshSkew if needsRefresh && p.tokenCache != nil { locked, err := p.tokenCache.AcquireRefreshLock(ctx, cacheKey, 30*time.Second) @@ -66,7 +66,7 @@ func (p *GeminiTokenProvider) GetAccessToken(ctx context.Context, account *Accou if err == nil && fresh != nil { account = fresh } - expiresAt = parseExpiresAt(account) + expiresAt = account.GetCredentialAsTime("expires_at") if expiresAt == nil || time.Until(*expiresAt) <= geminiTokenRefreshSkew { if p.geminiOAuthService == nil { return "", errors.New("gemini oauth service not configured") @@ -83,7 +83,7 @@ func (p *GeminiTokenProvider) GetAccessToken(ctx context.Context, account *Accou } account.Credentials = newCredentials _ = p.accountRepo.Update(ctx, account) - expiresAt = parseExpiresAt(account) + expiresAt = account.GetCredentialAsTime("expires_at") } } } @@ -154,18 +154,3 @@ func geminiTokenCacheKey(account *Account) string { } return "account:" + strconv.FormatInt(account.ID, 10) } - -func parseExpiresAt(account *Account) *time.Time { - raw := strings.TrimSpace(account.GetCredential("expires_at")) - if raw == "" { - return nil - } - if unixSec, err := strconv.ParseInt(raw, 10, 64); err == nil && unixSec > 0 { - t := time.Unix(unixSec, 0) - return &t - } - if t, err := time.Parse(time.RFC3339, raw); err == nil { - return &t - } - return nil -} diff --git a/backend/internal/service/gemini_token_refresher.go b/backend/internal/service/gemini_token_refresher.go index 19ba9424..7dfc5521 100644 --- a/backend/internal/service/gemini_token_refresher.go +++ b/backend/internal/service/gemini_token_refresher.go @@ -2,7 +2,6 @@ package service import ( "context" - "strconv" "time" ) @@ -22,16 +21,11 @@ func (r *GeminiTokenRefresher) NeedsRefresh(account *Account, refreshWindow time if !r.CanRefresh(account) { return false } - expiresAtStr := account.GetCredential("expires_at") - if expiresAtStr == "" { + expiresAt := account.GetCredentialAsTime("expires_at") + if expiresAt == nil { return false } - expiresAt, err := strconv.ParseInt(expiresAtStr, 10, 64) - if err != nil { - return false - } - expiryTime := time.Unix(expiresAt, 0) - return time.Until(expiryTime) < refreshWindow + return time.Until(*expiresAt) < refreshWindow } func (r *GeminiTokenRefresher) Refresh(ctx context.Context, account *Account) (map[string]any, error) { diff --git a/backend/internal/service/token_refresher.go b/backend/internal/service/token_refresher.go index 2ae3c822..214a290a 100644 --- a/backend/internal/service/token_refresher.go +++ b/backend/internal/service/token_refresher.go @@ -43,17 +43,11 @@ func (r *ClaudeTokenRefresher) CanRefresh(account *Account) bool { // NeedsRefresh 检查token是否需要刷新 // 基于 expires_at 字段判断是否在刷新窗口内 func (r *ClaudeTokenRefresher) NeedsRefresh(account *Account, refreshWindow time.Duration) bool { - s := account.GetCredential("expires_at") - if s == "" { + expiresAt := account.GetCredentialAsTime("expires_at") + if expiresAt == nil { return false } - - expiresAt, err := strconv.ParseInt(s, 10, 64) - if err != nil { - return false - } - - return time.Until(time.Unix(expiresAt, 0)) < refreshWindow + return time.Until(*expiresAt) < refreshWindow } // Refresh 执行token刷新 diff --git a/backend/internal/service/token_refresher_test.go b/backend/internal/service/token_refresher_test.go index c00fcfa3..0a5135ac 100644 --- a/backend/internal/service/token_refresher_test.go +++ b/backend/internal/service/token_refresher_test.go @@ -33,6 +33,13 @@ func TestClaudeTokenRefresher_NeedsRefresh(t *testing.T) { }, wantRefresh: true, }, + { + name: "expires_at as RFC3339 - expired", + credentials: map[string]any{ + "expires_at": "1970-01-01T00:00:00Z", // RFC3339 格式,已过期 + }, + wantRefresh: true, + }, { name: "expires_at as string - far future", credentials: map[string]any{ @@ -47,6 +54,13 @@ func TestClaudeTokenRefresher_NeedsRefresh(t *testing.T) { }, wantRefresh: false, }, + { + name: "expires_at as RFC3339 - far future", + credentials: map[string]any{ + "expires_at": "2099-12-31T23:59:59Z", // RFC3339 格式,远未来 + }, + wantRefresh: false, + }, { name: "expires_at missing", credentials: map[string]any{},