fix: Gemini CLI 指纹全面修复
Some checks failed
CI / test (push) Failing after 1m33s
CI / golangci-lint (push) Failing after 6s
Security Scan / backend-security (push) Failing after 5s
Security Scan / frontend-security (push) Failing after 6s

- 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 覆盖
This commit is contained in:
win 2026-03-27 13:07:18 +08:00
parent 2279bde564
commit 088a508e60
6 changed files with 66 additions and 20 deletions

View File

@ -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
}

View File

@ -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 {

View File

@ -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",
},

View File

@ -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
}

View File

@ -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
}
}

View File

@ -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),