diff --git a/backend/cmd/server/wire_gen.go b/backend/cmd/server/wire_gen.go index 9240e51e..9a79731b 100644 --- a/backend/cmd/server/wire_gen.go +++ b/backend/cmd/server/wire_gen.go @@ -105,14 +105,15 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) { geminiTokenCache := repository.NewGeminiTokenCache(redisClient) compositeTokenCacheInvalidator := service.NewCompositeTokenCacheInvalidator(geminiTokenCache) rateLimitService := service.ProvideRateLimitService(accountRepository, usageLogRepository, configConfig, geminiQuotaService, tempUnschedCache, timeoutCounterCache, settingService, compositeTokenCacheInvalidator) - claudeUsageFetcher := repository.NewClaudeUsageFetcher() + httpUpstream := repository.NewHTTPUpstream(configConfig) + claudeUsageFetcher := repository.NewClaudeUsageFetcher(httpUpstream) antigravityQuotaFetcher := service.NewAntigravityQuotaFetcher(proxyRepository) usageCache := service.NewUsageCache() - accountUsageService := service.NewAccountUsageService(accountRepository, usageLogRepository, claudeUsageFetcher, geminiQuotaService, antigravityQuotaFetcher, usageCache) + identityCache := repository.NewIdentityCache(redisClient) + accountUsageService := service.NewAccountUsageService(accountRepository, usageLogRepository, claudeUsageFetcher, geminiQuotaService, antigravityQuotaFetcher, usageCache, identityCache) geminiTokenProvider := service.NewGeminiTokenProvider(accountRepository, geminiTokenCache, geminiOAuthService) gatewayCache := repository.NewGatewayCache(redisClient) antigravityTokenProvider := service.NewAntigravityTokenProvider(accountRepository, geminiTokenCache, antigravityOAuthService) - httpUpstream := repository.NewHTTPUpstream(configConfig) antigravityGatewayService := service.NewAntigravityGatewayService(accountRepository, gatewayCache, antigravityTokenProvider, rateLimitService, httpUpstream, settingService) accountTestService := service.NewAccountTestService(accountRepository, geminiTokenProvider, antigravityGatewayService, httpUpstream, configConfig) concurrencyCache := repository.ProvideConcurrencyCache(redisClient, configConfig) @@ -137,7 +138,6 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) { return nil, err } billingService := service.NewBillingService(configConfig, pricingService) - identityCache := repository.NewIdentityCache(redisClient) identityService := service.NewIdentityService(identityCache) deferredService := service.ProvideDeferredService(accountRepository, timingWheelService) claudeTokenProvider := service.NewClaudeTokenProvider(accountRepository, geminiTokenCache, oAuthService) diff --git a/backend/internal/repository/claude_usage_service.go b/backend/internal/repository/claude_usage_service.go index 4c87b2de..1198f472 100644 --- a/backend/internal/repository/claude_usage_service.go +++ b/backend/internal/repository/claude_usage_service.go @@ -14,37 +14,82 @@ import ( const defaultClaudeUsageURL = "https://api.anthropic.com/api/oauth/usage" +// 默认 User-Agent,与用户抓包的请求一致 +const defaultUsageUserAgent = "claude-code/2.1.7" + type claudeUsageService struct { usageURL string allowPrivateHosts bool + httpUpstream service.HTTPUpstream } -func NewClaudeUsageFetcher() service.ClaudeUsageFetcher { - return &claudeUsageService{usageURL: defaultClaudeUsageURL} +// NewClaudeUsageFetcher 创建 Claude 用量获取服务 +// httpUpstream: 可选,如果提供则支持 TLS 指纹伪装 +func NewClaudeUsageFetcher(httpUpstream service.HTTPUpstream) service.ClaudeUsageFetcher { + return &claudeUsageService{ + usageURL: defaultClaudeUsageURL, + httpUpstream: httpUpstream, + } } +// FetchUsage 简单版本,不支持 TLS 指纹(向后兼容) func (s *claudeUsageService) FetchUsage(ctx context.Context, accessToken, proxyURL string) (*service.ClaudeUsageResponse, error) { - client, err := httpclient.GetClient(httpclient.Options{ - ProxyURL: proxyURL, - Timeout: 30 * time.Second, - ValidateResolvedIP: true, - AllowPrivateHosts: s.allowPrivateHosts, + return s.FetchUsageWithOptions(ctx, &service.ClaudeUsageFetchOptions{ + AccessToken: accessToken, + ProxyURL: proxyURL, }) - if err != nil { - client = &http.Client{Timeout: 30 * time.Second} +} + +// FetchUsageWithOptions 完整版本,支持 TLS 指纹和自定义 User-Agent +func (s *claudeUsageService) FetchUsageWithOptions(ctx context.Context, opts *service.ClaudeUsageFetchOptions) (*service.ClaudeUsageResponse, error) { + if opts == nil { + return nil, fmt.Errorf("options is nil") } + // 创建请求 req, err := http.NewRequestWithContext(ctx, "GET", s.usageURL, nil) if err != nil { return nil, fmt.Errorf("create request failed: %w", err) } - req.Header.Set("Authorization", "Bearer "+accessToken) + // 设置请求头(与抓包一致,但不设置 Accept-Encoding,让 Go 自动处理压缩) + req.Header.Set("Accept", "application/json, text/plain, */*") + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+opts.AccessToken) req.Header.Set("anthropic-beta", "oauth-2025-04-20") - resp, err := client.Do(req) - if err != nil { - return nil, fmt.Errorf("request failed: %w", err) + // 设置 User-Agent(优先使用缓存的 Fingerprint,否则使用默认值) + userAgent := defaultUsageUserAgent + if opts.Fingerprint != nil && opts.Fingerprint.UserAgent != "" { + userAgent = opts.Fingerprint.UserAgent + } + req.Header.Set("User-Agent", userAgent) + + var resp *http.Response + + // 如果启用 TLS 指纹且有 HTTPUpstream,使用 DoWithTLS + if opts.EnableTLSFingerprint && s.httpUpstream != nil { + // accountConcurrency 传 0 使用默认连接池配置,usage 请求不需要特殊的并发设置 + resp, err = s.httpUpstream.DoWithTLS(req, opts.ProxyURL, opts.AccountID, 0, true) + if err != nil { + return nil, fmt.Errorf("request with TLS fingerprint failed: %w", err) + } + } else { + // 不启用 TLS 指纹,使用普通 HTTP 客户端 + client, err := httpclient.GetClient(httpclient.Options{ + ProxyURL: opts.ProxyURL, + Timeout: 30 * time.Second, + ValidateResolvedIP: true, + AllowPrivateHosts: s.allowPrivateHosts, + }) + if err != nil { + client = &http.Client{Timeout: 30 * time.Second} + } + + resp, err = client.Do(req) + if err != nil { + return nil, fmt.Errorf("request failed: %w", err) + } } defer func() { _ = resp.Body.Close() }() diff --git a/backend/internal/service/account_usage_service.go b/backend/internal/service/account_usage_service.go index 6c617e27..f3b3e20d 100644 --- a/backend/internal/service/account_usage_service.go +++ b/backend/internal/service/account_usage_service.go @@ -157,9 +157,20 @@ type ClaudeUsageResponse struct { } `json:"seven_day_sonnet"` } +// ClaudeUsageFetchOptions 包含获取 Claude 用量数据所需的所有选项 +type ClaudeUsageFetchOptions struct { + AccessToken string // OAuth access token + ProxyURL string // 代理 URL(可选) + AccountID int64 // 账号 ID(用于 TLS 指纹选择) + EnableTLSFingerprint bool // 是否启用 TLS 指纹伪装 + Fingerprint *Fingerprint // 缓存的指纹信息(User-Agent 等) +} + // ClaudeUsageFetcher fetches usage data from Anthropic OAuth API type ClaudeUsageFetcher interface { FetchUsage(ctx context.Context, accessToken, proxyURL string) (*ClaudeUsageResponse, error) + // FetchUsageWithOptions 使用完整选项获取用量数据,支持 TLS 指纹和自定义 User-Agent + FetchUsageWithOptions(ctx context.Context, opts *ClaudeUsageFetchOptions) (*ClaudeUsageResponse, error) } // AccountUsageService 账号使用量查询服务 @@ -170,6 +181,7 @@ type AccountUsageService struct { geminiQuotaService *GeminiQuotaService antigravityQuotaFetcher *AntigravityQuotaFetcher cache *UsageCache + identityCache IdentityCache } // NewAccountUsageService 创建AccountUsageService实例 @@ -180,6 +192,7 @@ func NewAccountUsageService( geminiQuotaService *GeminiQuotaService, antigravityQuotaFetcher *AntigravityQuotaFetcher, cache *UsageCache, + identityCache IdentityCache, ) *AccountUsageService { return &AccountUsageService{ accountRepo: accountRepo, @@ -188,6 +201,7 @@ func NewAccountUsageService( geminiQuotaService: geminiQuotaService, antigravityQuotaFetcher: antigravityQuotaFetcher, cache: cache, + identityCache: identityCache, } } @@ -424,6 +438,8 @@ func (s *AccountUsageService) GetAccountUsageStats(ctx context.Context, accountI } // fetchOAuthUsageRaw 从 Anthropic API 获取原始响应(不构建 UsageInfo) +// 如果账号开启了 TLS 指纹,则使用 TLS 指纹伪装 +// 如果有缓存的 Fingerprint,则使用缓存的 User-Agent 等信息 func (s *AccountUsageService) fetchOAuthUsageRaw(ctx context.Context, account *Account) (*ClaudeUsageResponse, error) { accessToken := account.GetCredential("access_token") if accessToken == "" { @@ -435,7 +451,22 @@ func (s *AccountUsageService) fetchOAuthUsageRaw(ctx context.Context, account *A proxyURL = account.Proxy.URL() } - return s.usageFetcher.FetchUsage(ctx, accessToken, proxyURL) + // 构建完整的选项 + opts := &ClaudeUsageFetchOptions{ + AccessToken: accessToken, + ProxyURL: proxyURL, + AccountID: account.ID, + EnableTLSFingerprint: account.IsTLSFingerprintEnabled(), + } + + // 尝试获取缓存的 Fingerprint(包含 User-Agent 等信息) + if s.identityCache != nil { + if fp, err := s.identityCache.GetFingerprint(ctx, account.ID); err == nil && fp != nil { + opts.Fingerprint = fp + } + } + + return s.usageFetcher.FetchUsageWithOptions(ctx, opts) } // parseTime 尝试多种格式解析时间