feat(service): 实现 Gemini OAuth 和 Token 管理服务
- 实现 OAuth 授权流程服务 - 添加 Token 提供者和自动刷新机制 - 实现 Gemini Messages API 兼容层 - 更新服务容器注册
This commit is contained in:
1298
backend/internal/service/gemini_messages_compat_service.go
Normal file
1298
backend/internal/service/gemini_messages_compat_service.go
Normal file
File diff suppressed because it is too large
Load Diff
305
backend/internal/service/gemini_oauth_service.go
Normal file
305
backend/internal/service/gemini_oauth_service.go
Normal file
@@ -0,0 +1,305 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/Wei-Shaw/sub2api/internal/config"
|
||||||
|
"github.com/Wei-Shaw/sub2api/internal/model"
|
||||||
|
"github.com/Wei-Shaw/sub2api/internal/pkg/geminicli"
|
||||||
|
"github.com/Wei-Shaw/sub2api/internal/service/ports"
|
||||||
|
)
|
||||||
|
|
||||||
|
type GeminiOAuthService struct {
|
||||||
|
sessionStore *geminicli.SessionStore
|
||||||
|
proxyRepo ports.ProxyRepository
|
||||||
|
oauthClient ports.GeminiOAuthClient
|
||||||
|
codeAssist ports.GeminiCliCodeAssistClient
|
||||||
|
cfg *config.Config
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewGeminiOAuthService(
|
||||||
|
proxyRepo ports.ProxyRepository,
|
||||||
|
oauthClient ports.GeminiOAuthClient,
|
||||||
|
codeAssist ports.GeminiCliCodeAssistClient,
|
||||||
|
cfg *config.Config,
|
||||||
|
) *GeminiOAuthService {
|
||||||
|
return &GeminiOAuthService{
|
||||||
|
sessionStore: geminicli.NewSessionStore(),
|
||||||
|
proxyRepo: proxyRepo,
|
||||||
|
oauthClient: oauthClient,
|
||||||
|
codeAssist: codeAssist,
|
||||||
|
cfg: cfg,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type GeminiAuthURLResult struct {
|
||||||
|
AuthURL string `json:"auth_url"`
|
||||||
|
SessionID string `json:"session_id"`
|
||||||
|
State string `json:"state"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *GeminiOAuthService) GenerateAuthURL(ctx context.Context, proxyID *int64, redirectURI string) (*GeminiAuthURLResult, error) {
|
||||||
|
state, err := geminicli.GenerateState()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to generate state: %w", err)
|
||||||
|
}
|
||||||
|
codeVerifier, err := geminicli.GenerateCodeVerifier()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to generate code verifier: %w", err)
|
||||||
|
}
|
||||||
|
codeChallenge := geminicli.GenerateCodeChallenge(codeVerifier)
|
||||||
|
sessionID, err := geminicli.GenerateSessionID()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to generate session ID: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var proxyURL string
|
||||||
|
if proxyID != nil {
|
||||||
|
proxy, err := s.proxyRepo.GetByID(ctx, *proxyID)
|
||||||
|
if err == nil && proxy != nil {
|
||||||
|
proxyURL = proxy.URL()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
session := &geminicli.OAuthSession{
|
||||||
|
State: state,
|
||||||
|
CodeVerifier: codeVerifier,
|
||||||
|
ProxyURL: proxyURL,
|
||||||
|
RedirectURI: redirectURI,
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
}
|
||||||
|
s.sessionStore.Set(sessionID, session)
|
||||||
|
|
||||||
|
oauthCfg := geminicli.OAuthConfig{
|
||||||
|
ClientID: s.cfg.Gemini.OAuth.ClientID,
|
||||||
|
ClientSecret: s.cfg.Gemini.OAuth.ClientSecret,
|
||||||
|
Scopes: s.cfg.Gemini.OAuth.Scopes,
|
||||||
|
}
|
||||||
|
|
||||||
|
authURL, err := geminicli.BuildAuthorizationURL(oauthCfg, state, codeChallenge, redirectURI)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &GeminiAuthURLResult{
|
||||||
|
AuthURL: authURL,
|
||||||
|
SessionID: sessionID,
|
||||||
|
State: state,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type GeminiExchangeCodeInput struct {
|
||||||
|
SessionID string
|
||||||
|
State string
|
||||||
|
Code string
|
||||||
|
RedirectURI string
|
||||||
|
ProxyID *int64
|
||||||
|
}
|
||||||
|
|
||||||
|
type GeminiTokenInfo struct {
|
||||||
|
AccessToken string `json:"access_token"`
|
||||||
|
RefreshToken string `json:"refresh_token"`
|
||||||
|
ExpiresIn int64 `json:"expires_in"`
|
||||||
|
ExpiresAt int64 `json:"expires_at"`
|
||||||
|
TokenType string `json:"token_type"`
|
||||||
|
Scope string `json:"scope,omitempty"`
|
||||||
|
ProjectID string `json:"project_id,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *GeminiOAuthService) ExchangeCode(ctx context.Context, input *GeminiExchangeCodeInput) (*GeminiTokenInfo, error) {
|
||||||
|
session, ok := s.sessionStore.Get(input.SessionID)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("session not found or expired")
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(input.State) == "" || input.State != session.State {
|
||||||
|
return nil, fmt.Errorf("invalid state")
|
||||||
|
}
|
||||||
|
|
||||||
|
proxyURL := session.ProxyURL
|
||||||
|
if input.ProxyID != nil {
|
||||||
|
proxy, err := s.proxyRepo.GetByID(ctx, *input.ProxyID)
|
||||||
|
if err == nil && proxy != nil {
|
||||||
|
proxyURL = proxy.URL()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
redirectURI := session.RedirectURI
|
||||||
|
if strings.TrimSpace(input.RedirectURI) != "" {
|
||||||
|
redirectURI = input.RedirectURI
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenResp, err := s.oauthClient.ExchangeCode(ctx, input.Code, session.CodeVerifier, redirectURI, proxyURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to exchange code: %w", err)
|
||||||
|
}
|
||||||
|
s.sessionStore.Delete(input.SessionID)
|
||||||
|
|
||||||
|
expiresAt := time.Now().Unix() + tokenResp.ExpiresIn
|
||||||
|
projectID, _ := s.fetchProjectID(ctx, tokenResp.AccessToken, proxyURL)
|
||||||
|
|
||||||
|
return &GeminiTokenInfo{
|
||||||
|
AccessToken: tokenResp.AccessToken,
|
||||||
|
RefreshToken: tokenResp.RefreshToken,
|
||||||
|
TokenType: tokenResp.TokenType,
|
||||||
|
ExpiresIn: tokenResp.ExpiresIn,
|
||||||
|
ExpiresAt: expiresAt,
|
||||||
|
Scope: tokenResp.Scope,
|
||||||
|
ProjectID: projectID,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *GeminiOAuthService) RefreshToken(ctx context.Context, refreshToken, proxyURL string) (*GeminiTokenInfo, error) {
|
||||||
|
var lastErr error
|
||||||
|
|
||||||
|
for attempt := 0; attempt <= 3; attempt++ {
|
||||||
|
if attempt > 0 {
|
||||||
|
backoff := time.Duration(1<<uint(attempt-1)) * time.Second
|
||||||
|
if backoff > 30*time.Second {
|
||||||
|
backoff = 30 * time.Second
|
||||||
|
}
|
||||||
|
time.Sleep(backoff)
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenResp, err := s.oauthClient.RefreshToken(ctx, refreshToken, proxyURL)
|
||||||
|
if err == nil {
|
||||||
|
expiresAt := time.Now().Unix() + tokenResp.ExpiresIn
|
||||||
|
return &GeminiTokenInfo{
|
||||||
|
AccessToken: tokenResp.AccessToken,
|
||||||
|
RefreshToken: tokenResp.RefreshToken,
|
||||||
|
TokenType: tokenResp.TokenType,
|
||||||
|
ExpiresIn: tokenResp.ExpiresIn,
|
||||||
|
ExpiresAt: expiresAt,
|
||||||
|
Scope: tokenResp.Scope,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if isNonRetryableGeminiOAuthError(err) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
lastErr = err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("token refresh failed after retries: %w", lastErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func isNonRetryableGeminiOAuthError(err error) bool {
|
||||||
|
msg := err.Error()
|
||||||
|
nonRetryable := []string{
|
||||||
|
"invalid_grant",
|
||||||
|
"invalid_client",
|
||||||
|
"unauthorized_client",
|
||||||
|
"access_denied",
|
||||||
|
}
|
||||||
|
for _, needle := range nonRetryable {
|
||||||
|
if strings.Contains(msg, needle) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *GeminiOAuthService) RefreshAccountToken(ctx context.Context, account *model.Account) (*GeminiTokenInfo, error) {
|
||||||
|
if account.Platform != model.PlatformGemini || account.Type != model.AccountTypeOAuth {
|
||||||
|
return nil, fmt.Errorf("account is not a Gemini OAuth account")
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshToken := account.GetCredential("refresh_token")
|
||||||
|
if strings.TrimSpace(refreshToken) == "" {
|
||||||
|
return nil, fmt.Errorf("no refresh token available")
|
||||||
|
}
|
||||||
|
|
||||||
|
var proxyURL string
|
||||||
|
if account.ProxyID != nil {
|
||||||
|
proxy, err := s.proxyRepo.GetByID(ctx, *account.ProxyID)
|
||||||
|
if err == nil && proxy != nil {
|
||||||
|
proxyURL = proxy.URL()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.RefreshToken(ctx, refreshToken, proxyURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *GeminiOAuthService) BuildAccountCredentials(tokenInfo *GeminiTokenInfo) map[string]any {
|
||||||
|
creds := map[string]any{
|
||||||
|
"access_token": tokenInfo.AccessToken,
|
||||||
|
"expires_at": strconv.FormatInt(tokenInfo.ExpiresAt, 10),
|
||||||
|
}
|
||||||
|
if tokenInfo.RefreshToken != "" {
|
||||||
|
creds["refresh_token"] = tokenInfo.RefreshToken
|
||||||
|
}
|
||||||
|
if tokenInfo.TokenType != "" {
|
||||||
|
creds["token_type"] = tokenInfo.TokenType
|
||||||
|
}
|
||||||
|
if tokenInfo.Scope != "" {
|
||||||
|
creds["scope"] = tokenInfo.Scope
|
||||||
|
}
|
||||||
|
if tokenInfo.ProjectID != "" {
|
||||||
|
creds["project_id"] = tokenInfo.ProjectID
|
||||||
|
}
|
||||||
|
return creds
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *GeminiOAuthService) Stop() {
|
||||||
|
s.sessionStore.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *GeminiOAuthService) fetchProjectID(ctx context.Context, accessToken, proxyURL string) (string, error) {
|
||||||
|
if s.codeAssist == nil {
|
||||||
|
return "", errors.New("code assist client not configured")
|
||||||
|
}
|
||||||
|
|
||||||
|
loadResp, err := s.codeAssist.LoadCodeAssist(ctx, accessToken, proxyURL, nil)
|
||||||
|
if err == nil && strings.TrimSpace(loadResp.CurrentTier) != "" && strings.TrimSpace(loadResp.CloudAICompanionProject) != "" {
|
||||||
|
return strings.TrimSpace(loadResp.CloudAICompanionProject), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// pick default tier from allowedTiers, fallback to LEGACY.
|
||||||
|
tierID := "LEGACY"
|
||||||
|
if loadResp != nil {
|
||||||
|
for _, tier := range loadResp.AllowedTiers {
|
||||||
|
if tier.IsDefault && strings.TrimSpace(tier.ID) != "" {
|
||||||
|
tierID = tier.ID
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
req := &geminicli.OnboardUserRequest{
|
||||||
|
TierID: tierID,
|
||||||
|
Metadata: geminicli.LoadCodeAssistMetadata{
|
||||||
|
IDEType: "ANTIGRAVITY",
|
||||||
|
Platform: "PLATFORM_UNSPECIFIED",
|
||||||
|
PluginType: "GEMINI",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
maxAttempts := 5
|
||||||
|
for attempt := 1; attempt <= maxAttempts; attempt++ {
|
||||||
|
resp, err := s.codeAssist.OnboardUser(ctx, accessToken, proxyURL, req)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if resp.Done {
|
||||||
|
if resp.Response == nil || resp.Response.CloudAICompanionProject == nil {
|
||||||
|
return "", errors.New("onboardUser completed but no project_id returned")
|
||||||
|
}
|
||||||
|
switch v := resp.Response.CloudAICompanionProject.(type) {
|
||||||
|
case string:
|
||||||
|
return strings.TrimSpace(v), nil
|
||||||
|
case map[string]any:
|
||||||
|
if id, ok := v["id"].(string); ok {
|
||||||
|
return strings.TrimSpace(id), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", errors.New("onboardUser returned unsupported project_id format")
|
||||||
|
}
|
||||||
|
time.Sleep(2 * time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", fmt.Errorf("onboardUser timeout after %d attempts", maxAttempts)
|
||||||
|
}
|
||||||
139
backend/internal/service/gemini_token_provider.go
Normal file
139
backend/internal/service/gemini_token_provider.go
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/Wei-Shaw/sub2api/internal/model"
|
||||||
|
"github.com/Wei-Shaw/sub2api/internal/service/ports"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
geminiTokenRefreshSkew = 3 * time.Minute
|
||||||
|
geminiTokenCacheSkew = 5 * time.Minute
|
||||||
|
)
|
||||||
|
|
||||||
|
type GeminiTokenProvider struct {
|
||||||
|
accountRepo ports.AccountRepository
|
||||||
|
tokenCache ports.GeminiTokenCache
|
||||||
|
geminiOAuthService *GeminiOAuthService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewGeminiTokenProvider(
|
||||||
|
accountRepo ports.AccountRepository,
|
||||||
|
tokenCache ports.GeminiTokenCache,
|
||||||
|
geminiOAuthService *GeminiOAuthService,
|
||||||
|
) *GeminiTokenProvider {
|
||||||
|
return &GeminiTokenProvider{
|
||||||
|
accountRepo: accountRepo,
|
||||||
|
tokenCache: tokenCache,
|
||||||
|
geminiOAuthService: geminiOAuthService,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *GeminiTokenProvider) GetAccessToken(ctx context.Context, account *model.Account) (string, error) {
|
||||||
|
if account == nil {
|
||||||
|
return "", errors.New("account is nil")
|
||||||
|
}
|
||||||
|
if account.Platform != model.PlatformGemini || account.Type != model.AccountTypeOAuth {
|
||||||
|
return "", errors.New("not a gemini oauth account")
|
||||||
|
}
|
||||||
|
|
||||||
|
cacheKey := geminiTokenCacheKey(account)
|
||||||
|
|
||||||
|
// 1) Try cache first.
|
||||||
|
if p.tokenCache != nil {
|
||||||
|
if token, err := p.tokenCache.GetAccessToken(ctx, cacheKey); err == nil && strings.TrimSpace(token) != "" {
|
||||||
|
return token, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) Refresh if needed (pre-expiry skew).
|
||||||
|
expiresAt := parseExpiresAt(account)
|
||||||
|
needsRefresh := expiresAt == nil || time.Until(*expiresAt) <= geminiTokenRefreshSkew
|
||||||
|
if needsRefresh && p.tokenCache != nil {
|
||||||
|
locked, err := p.tokenCache.AcquireRefreshLock(ctx, cacheKey, 30*time.Second)
|
||||||
|
if err == nil && locked {
|
||||||
|
defer func() { _ = p.tokenCache.ReleaseRefreshLock(ctx, cacheKey) }()
|
||||||
|
|
||||||
|
// Re-check after lock (another worker may have refreshed).
|
||||||
|
if token, err := p.tokenCache.GetAccessToken(ctx, cacheKey); err == nil && strings.TrimSpace(token) != "" {
|
||||||
|
return token, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
fresh, err := p.accountRepo.GetByID(ctx, account.ID)
|
||||||
|
if err == nil && fresh != nil {
|
||||||
|
account = fresh
|
||||||
|
}
|
||||||
|
expiresAt = parseExpiresAt(account)
|
||||||
|
if expiresAt == nil || time.Until(*expiresAt) <= geminiTokenRefreshSkew {
|
||||||
|
if p.geminiOAuthService == nil {
|
||||||
|
return "", errors.New("gemini oauth service not configured")
|
||||||
|
}
|
||||||
|
tokenInfo, err := p.geminiOAuthService.RefreshAccountToken(ctx, account)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
newCredentials := p.geminiOAuthService.BuildAccountCredentials(tokenInfo)
|
||||||
|
for k, v := range account.Credentials {
|
||||||
|
if _, exists := newCredentials[k]; !exists {
|
||||||
|
newCredentials[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
account.Credentials = model.JSONB(newCredentials)
|
||||||
|
_ = p.accountRepo.Update(ctx, account)
|
||||||
|
expiresAt = parseExpiresAt(account)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
accessToken := account.GetCredential("access_token")
|
||||||
|
if strings.TrimSpace(accessToken) == "" {
|
||||||
|
return "", errors.New("access_token not found in credentials")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3) Populate cache with TTL.
|
||||||
|
if p.tokenCache != nil {
|
||||||
|
ttl := 30 * time.Minute
|
||||||
|
if expiresAt != nil {
|
||||||
|
until := time.Until(*expiresAt)
|
||||||
|
switch {
|
||||||
|
case until > geminiTokenCacheSkew:
|
||||||
|
ttl = until - geminiTokenCacheSkew
|
||||||
|
case until > 0:
|
||||||
|
ttl = until
|
||||||
|
default:
|
||||||
|
ttl = time.Minute
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ = p.tokenCache.SetAccessToken(ctx, cacheKey, accessToken, ttl)
|
||||||
|
}
|
||||||
|
|
||||||
|
return accessToken, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func geminiTokenCacheKey(account *model.Account) string {
|
||||||
|
projectID := strings.TrimSpace(account.GetCredential("project_id"))
|
||||||
|
if projectID != "" {
|
||||||
|
return projectID
|
||||||
|
}
|
||||||
|
return "account:" + strconv.FormatInt(account.ID, 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseExpiresAt(account *model.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
|
||||||
|
}
|
||||||
53
backend/internal/service/gemini_token_refresher.go
Normal file
53
backend/internal/service/gemini_token_refresher.go
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/Wei-Shaw/sub2api/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
type GeminiTokenRefresher struct {
|
||||||
|
geminiOAuthService *GeminiOAuthService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewGeminiTokenRefresher(geminiOAuthService *GeminiOAuthService) *GeminiTokenRefresher {
|
||||||
|
return &GeminiTokenRefresher{geminiOAuthService: geminiOAuthService}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *GeminiTokenRefresher) CanRefresh(account *model.Account) bool {
|
||||||
|
return account.Platform == model.PlatformGemini && account.Type == model.AccountTypeOAuth
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *GeminiTokenRefresher) NeedsRefresh(account *model.Account, refreshWindow time.Duration) bool {
|
||||||
|
if !r.CanRefresh(account) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
expiresAtStr := account.GetCredential("expires_at")
|
||||||
|
if expiresAtStr == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
expiresAt, err := strconv.ParseInt(expiresAtStr, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
expiryTime := time.Unix(expiresAt, 0)
|
||||||
|
return time.Until(expiryTime) < refreshWindow
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *GeminiTokenRefresher) Refresh(ctx context.Context, account *model.Account) (map[string]any, error) {
|
||||||
|
tokenInfo, err := r.geminiOAuthService.RefreshAccountToken(ctx, account)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
newCredentials := r.geminiOAuthService.BuildAccountCredentials(tokenInfo)
|
||||||
|
for k, v := range account.Credentials {
|
||||||
|
if _, exists := newCredentials[k]; !exists {
|
||||||
|
newCredentials[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return newCredentials, nil
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user