diff --git a/backend/internal/pkg/claude/constants.go b/backend/internal/pkg/claude/constants.go index c9c015bb..092a2641 100644 --- a/backend/internal/pkg/claude/constants.go +++ b/backend/internal/pkg/claude/constants.go @@ -52,7 +52,7 @@ const APIKeyHaikuBetaHeader = BetaInterleavedThinking var DefaultHeaders = map[string]string{ // Keep these in sync with recent Claude CLI traffic to reduce the chance // that Claude Code-scoped OAuth credentials are rejected as "non-CLI" usage. - "User-Agent": "claude-cli/2.1.84 (external, cli)", + "User-Agent": "claude-cli/2.1.87 (external, cli)", "X-Stainless-Lang": "js", "X-Stainless-Package-Version": "0.74.0", "X-Stainless-OS": "MacOS", diff --git a/backend/internal/repository/claude_oauth_service.go b/backend/internal/repository/claude_oauth_service.go index fee5c645..433a4803 100644 --- a/backend/internal/repository/claude_oauth_service.go +++ b/backend/internal/repository/claude_oauth_service.go @@ -12,6 +12,7 @@ import ( "github.com/Wei-Shaw/sub2api/internal/pkg/logger" "github.com/Wei-Shaw/sub2api/internal/pkg/oauth" "github.com/Wei-Shaw/sub2api/internal/pkg/proxyurl" + "github.com/Wei-Shaw/sub2api/internal/pkg/tlsfingerprint" "github.com/Wei-Shaw/sub2api/internal/service" "github.com/Wei-Shaw/sub2api/internal/util/logredact" @@ -267,18 +268,43 @@ func (s *claudeOAuthService) RefreshToken(ctx context.Context, refreshToken, pro } func createReqClient(proxyURL string) (*req.Client, error) { - // 禁用 CookieJar,确保每次授权都是干净的会话 + // Use Node.js 24.x TLS fingerprint (same as API requests) instead of Chrome + // to ensure TLS fingerprint consistency across the entire token lifecycle. + // Previously used ImpersonateChrome() which created a JA3 mismatch between + // OAuth token exchange/refresh and API calls. + profile := &tlsfingerprint.Profile{ + Name: "oauth-nodejs24", + EnableGREASE: true, + } + client := req.C(). - SetTimeout(60 * time.Second). - ImpersonateChrome(). - SetCookieJar(nil) // 禁用 CookieJar + SetTimeout(15 * time.Second). + SetCookieJar(nil) // 禁用 CookieJar,确保每次授权都是干净的会话 trimmed, _, err := proxyurl.Parse(proxyURL) if err != nil { return nil, err } + if trimmed != "" { - client.SetProxyURL(trimmed) + parsedProxy, parseErr := url.Parse(trimmed) + if parseErr != nil { + return nil, fmt.Errorf("parse proxy URL: %w", parseErr) + } + scheme := strings.ToLower(parsedProxy.Scheme) + switch scheme { + case "socks5", "socks5h": + socks5Dialer := tlsfingerprint.NewSOCKS5ProxyDialer(profile, parsedProxy) + client.SetDialTLS(socks5Dialer.DialTLSContext) + case "http", "https": + httpDialer := tlsfingerprint.NewHTTPProxyDialer(profile, parsedProxy) + client.SetDialTLS(httpDialer.DialTLSContext) + default: + client.SetProxyURL(trimmed) + } + } else { + dialer := tlsfingerprint.NewDialer(profile, nil) + client.SetDialTLS(dialer.DialTLSContext) } return client, nil diff --git a/backend/internal/service/identity_service.go b/backend/internal/service/identity_service.go index c6a260a8..2c536de6 100644 --- a/backend/internal/service/identity_service.go +++ b/backend/internal/service/identity_service.go @@ -26,7 +26,7 @@ var ( // 默认指纹值(当客户端未提供时使用) var defaultFingerprint = Fingerprint{ - UserAgent: "claude-cli/2.1.84 (external, cli)", + UserAgent: "claude-cli/2.1.87 (external, cli)", StainlessLang: "js", StainlessPackageVersion: "0.74.0", StainlessOS: "MacOS",