From 088a508e60b1c0c10ba97fcb4d858fa72b465835 Mon Sep 17 00:00:00 2001 From: win Date: Fri, 27 Mar 2026 13:07:18 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20Gemini=20CLI=20=E6=8C=87=E7=BA=B9?= =?UTF-8?q?=E5=85=A8=E9=9D=A2=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - User-Agent: GeminiCLI/0.1.5 → GeminiCLI/0.33.1/{model} ({platform}; {arch}) 格式、版本、大小写全部对齐真实 Gemini CLI 0.33.1 - 新增 x-goog-api-client: gl-node/24.13.1 (匹配 google-auth-library DefaultTransporter) - ideType: ANTIGRAVITY → IDE_UNSPECIFIED (修复身份泄露,真实 Gemini CLI 用 IDE_UNSPECIFIED) - Token 交换/刷新: 添加 google-api-nodejs-client UA + x-goog-api-client - 版本号可通过环境变量 GEMINI_CLI_VERSION 覆盖 --- backend/internal/pkg/geminicli/constants.go | 54 +++++++++++++++---- .../repository/gemini_oauth_client.go | 4 ++ .../repository/geminicli_codeassist_client.go | 10 ++-- .../internal/service/account_test_service.go | 3 +- .../service/gemini_messages_compat_service.go | 12 +++-- .../internal/service/gemini_oauth_service.go | 3 +- 6 files changed, 66 insertions(+), 20 deletions(-) diff --git a/backend/internal/pkg/geminicli/constants.go b/backend/internal/pkg/geminicli/constants.go index 9b204640..19a3ddd9 100644 --- a/backend/internal/pkg/geminicli/constants.go +++ b/backend/internal/pkg/geminicli/constants.go @@ -3,8 +3,8 @@ package geminicli import ( "fmt" + "os" "runtime" - "strings" "time" ) @@ -51,15 +51,49 @@ const ( SessionTTL = 30 * time.Minute - // GeminiCLIUserAgent mimics Gemini CLI to maximize compatibility with internal endpoints. - // Note: The real Gemini CLI uses OS-appropriate platform strings. - // Use GetGeminiCLIUserAgent() for runtime-aware User-Agent. - GeminiCLIUserAgent = "GeminiCLI/0.1.5" + // GeminiCLIUserAgent 静态回退值(不含 model) + // 优先使用 GetGeminiCLIUserAgent(model) 获取完整格式 + GeminiCLIUserAgent = "GeminiCLI/0.33.1" + + // FakeNodeVersion 模拟真实 Gemini CLI 的 Node.js 版本 + // 用于 x-goog-api-client 和 token exchange User-Agent + FakeNodeVersion = "24.13.1" + + // GoogleAuthLibraryUA 模拟 google-auth-library 的 User-Agent + // 真实 Gemini CLI token exchange 由 google-auth-library 发起 + GoogleAuthLibraryUA = "google-api-nodejs-client" ) -// GetGeminiCLIUserAgent 返回带有正确平台信息的 Gemini CLI User-Agent -func GetGeminiCLIUserAgent() string { - osName := strings.Title(runtime.GOOS) // Darwin, Linux, Windows - arch := strings.ToUpper(runtime.GOARCH) - return fmt.Sprintf("GeminiCLI/0.1.5 (%s; %s)", osName, arch) +// defaultGeminiCLIVersion 可通过环境变量 GEMINI_CLI_VERSION 覆盖 +var defaultGeminiCLIVersion = "0.33.1" + +func init() { + if v := os.Getenv("GEMINI_CLI_VERSION"); v != "" { + defaultGeminiCLIVersion = v + } +} + +// GetGeminiCLIUserAgent 返回匹配真实 Gemini CLI 格式的 User-Agent +// 真实格式: GeminiCLI/{version}/{model} ({platform}; {arch}) +// 示例: GeminiCLI/0.33.1/gemini-2.5-pro (darwin; arm64) +func GetGeminiCLIUserAgent(model ...string) string { + m := "unknown" + if len(model) > 0 && model[0] != "" { + m = model[0] + } + return fmt.Sprintf("GeminiCLI/%s/%s (%s; %s)", + defaultGeminiCLIVersion, m, runtime.GOOS, runtime.GOARCH) +} + +// GetGeminiCLIGoogAPIClient 返回 x-goog-api-client 头的值 +// 真实 Gemini CLI 通过 google-auth-library DefaultTransporter 自动注入: +// gl-node/{nodeVersion} +func GetGeminiCLIGoogAPIClient() string { + return fmt.Sprintf("gl-node/%s", FakeNodeVersion) +} + +// GetGeminiCLITokenExchangeUA 返回 token exchange/refresh 时的 User-Agent +// 真实 Gemini CLI 使用 google-auth-library 发起 token 交换 +func GetGeminiCLITokenExchangeUA() string { + return GoogleAuthLibraryUA } diff --git a/backend/internal/repository/gemini_oauth_client.go b/backend/internal/repository/gemini_oauth_client.go index eb14f313..3841dfe0 100644 --- a/backend/internal/repository/gemini_oauth_client.go +++ b/backend/internal/repository/gemini_oauth_client.go @@ -63,6 +63,8 @@ func (c *geminiOAuthClient) ExchangeCode(ctx context.Context, oauthType, code, c resp, err := client.R(). SetContext(ctx). SetFormDataFromValues(formData). + SetHeader("User-Agent", geminicli.GetGeminiCLITokenExchangeUA()). + SetHeader("X-Goog-Api-Client", geminicli.GetGeminiCLIGoogAPIClient()). SetSuccessResult(&tokenResp). Post(c.tokenURL) if err != nil { @@ -106,6 +108,8 @@ func (c *geminiOAuthClient) RefreshToken(ctx context.Context, oauthType, refresh resp, err := client.R(). SetContext(ctx). SetFormDataFromValues(formData). + SetHeader("User-Agent", geminicli.GetGeminiCLITokenExchangeUA()). + SetHeader("X-Goog-Api-Client", geminicli.GetGeminiCLIGoogAPIClient()). SetSuccessResult(&tokenResp). Post(c.tokenURL) if err != nil { diff --git a/backend/internal/repository/geminicli_codeassist_client.go b/backend/internal/repository/geminicli_codeassist_client.go index b5bc6497..7e0d85b3 100644 --- a/backend/internal/repository/geminicli_codeassist_client.go +++ b/backend/internal/repository/geminicli_codeassist_client.go @@ -34,7 +34,8 @@ func (c *geminiCliCodeAssistClient) LoadCodeAssist(ctx context.Context, accessTo SetContext(ctx). SetHeader("Authorization", "Bearer "+accessToken). SetHeader("Content-Type", "application/json"). - SetHeader("User-Agent", geminicli.GeminiCLIUserAgent). + SetHeader("User-Agent", geminicli.GetGeminiCLIUserAgent()). + SetHeader("X-Goog-Api-Client", geminicli.GetGeminiCLIGoogAPIClient()). SetBody(reqBody). SetSuccessResult(&out). Post(c.baseURL + "/v1internal:loadCodeAssist") @@ -78,7 +79,8 @@ func (c *geminiCliCodeAssistClient) OnboardUser(ctx context.Context, accessToken SetContext(ctx). SetHeader("Authorization", "Bearer "+accessToken). SetHeader("Content-Type", "application/json"). - SetHeader("User-Agent", geminicli.GeminiCLIUserAgent). + SetHeader("User-Agent", geminicli.GetGeminiCLIUserAgent()). + SetHeader("X-Goog-Api-Client", geminicli.GetGeminiCLIGoogAPIClient()). SetBody(reqBody). SetSuccessResult(&out). Post(c.baseURL + "/v1internal:onboardUser") @@ -116,7 +118,7 @@ func createGeminiCliReqClient(proxyURL string) (*req.Client, error) { func defaultLoadCodeAssistRequest() *geminicli.LoadCodeAssistRequest { return &geminicli.LoadCodeAssistRequest{ Metadata: geminicli.LoadCodeAssistMetadata{ - IDEType: "ANTIGRAVITY", + IDEType: "IDE_UNSPECIFIED", Platform: "PLATFORM_UNSPECIFIED", PluginType: "GEMINI", }, @@ -127,7 +129,7 @@ func defaultOnboardUserRequest() *geminicli.OnboardUserRequest { return &geminicli.OnboardUserRequest{ TierID: "LEGACY", Metadata: geminicli.LoadCodeAssistMetadata{ - IDEType: "ANTIGRAVITY", + IDEType: "IDE_UNSPECIFIED", Platform: "PLATFORM_UNSPECIFIED", PluginType: "GEMINI", }, diff --git a/backend/internal/service/account_test_service.go b/backend/internal/service/account_test_service.go index 12617336..0ba0e9a3 100644 --- a/backend/internal/service/account_test_service.go +++ b/backend/internal/service/account_test_service.go @@ -1464,7 +1464,8 @@ func (s *AccountTestService) buildCodeAssistRequest(ctx context.Context, accessT req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+accessToken) - req.Header.Set("User-Agent", geminicli.GeminiCLIUserAgent) + req.Header.Set("User-Agent", geminicli.GetGeminiCLIUserAgent()) + req.Header.Set("X-Goog-Api-Client", geminicli.GetGeminiCLIGoogAPIClient()) return req, nil } diff --git a/backend/internal/service/gemini_messages_compat_service.go b/backend/internal/service/gemini_messages_compat_service.go index 0e0898b6..490722ce 100644 --- a/backend/internal/service/gemini_messages_compat_service.go +++ b/backend/internal/service/gemini_messages_compat_service.go @@ -669,7 +669,8 @@ func (s *GeminiMessagesCompatService) Forward(ctx context.Context, c *gin.Contex } upstreamReq.Header.Set("Content-Type", "application/json") upstreamReq.Header.Set("Authorization", "Bearer "+accessToken) - upstreamReq.Header.Set("User-Agent", geminicli.GetGeminiCLIUserAgent()) + upstreamReq.Header.Set("User-Agent", geminicli.GetGeminiCLIUserAgent(mappedModel)) + upstreamReq.Header.Set("X-Goog-Api-Client", geminicli.GetGeminiCLIGoogAPIClient()) return upstreamReq, "x-request-id", nil } else { // Mode 2: AI Studio API with OAuth (like API key mode, but using Bearer token) @@ -690,7 +691,8 @@ func (s *GeminiMessagesCompatService) Forward(ctx context.Context, c *gin.Contex } upstreamReq.Header.Set("Content-Type", "application/json") upstreamReq.Header.Set("Authorization", "Bearer "+accessToken) - upstreamReq.Header.Set("User-Agent", geminicli.GetGeminiCLIUserAgent()) + upstreamReq.Header.Set("User-Agent", geminicli.GetGeminiCLIUserAgent(mappedModel)) + upstreamReq.Header.Set("X-Goog-Api-Client", geminicli.GetGeminiCLIGoogAPIClient()) return upstreamReq, "x-request-id", nil } } @@ -1171,7 +1173,8 @@ func (s *GeminiMessagesCompatService) ForwardNative(ctx context.Context, c *gin. } upstreamReq.Header.Set("Content-Type", "application/json") upstreamReq.Header.Set("Authorization", "Bearer "+accessToken) - upstreamReq.Header.Set("User-Agent", geminicli.GetGeminiCLIUserAgent()) + upstreamReq.Header.Set("User-Agent", geminicli.GetGeminiCLIUserAgent(mappedModel)) + upstreamReq.Header.Set("X-Goog-Api-Client", geminicli.GetGeminiCLIGoogAPIClient()) return upstreamReq, "x-request-id", nil } else { // Mode 2: AI Studio API with OAuth (like API key mode, but using Bearer token) @@ -1192,7 +1195,8 @@ func (s *GeminiMessagesCompatService) ForwardNative(ctx context.Context, c *gin. } upstreamReq.Header.Set("Content-Type", "application/json") upstreamReq.Header.Set("Authorization", "Bearer "+accessToken) - upstreamReq.Header.Set("User-Agent", geminicli.GetGeminiCLIUserAgent()) + upstreamReq.Header.Set("User-Agent", geminicli.GetGeminiCLIUserAgent(mappedModel)) + upstreamReq.Header.Set("X-Goog-Api-Client", geminicli.GetGeminiCLIGoogAPIClient()) return upstreamReq, "x-request-id", nil } } diff --git a/backend/internal/service/gemini_oauth_service.go b/backend/internal/service/gemini_oauth_service.go index 08a74a37..091f9aa3 100644 --- a/backend/internal/service/gemini_oauth_service.go +++ b/backend/internal/service/gemini_oauth_service.go @@ -1037,7 +1037,8 @@ func fetchProjectIDFromResourceManager(ctx context.Context, accessToken, proxyUR } req.Header.Set("Authorization", "Bearer "+accessToken) - req.Header.Set("User-Agent", geminicli.GeminiCLIUserAgent) + req.Header.Set("User-Agent", geminicli.GetGeminiCLIUserAgent()) + req.Header.Set("X-Goog-Api-Client", geminicli.GetGeminiCLIGoogAPIClient()) client, err := httpclient.GetClient(httpclient.Options{ ProxyURL: strings.TrimSpace(proxyURL),