From eea949853a751e78b563c4623a7c0e61b7322726 Mon Sep 17 00:00:00 2001 From: ianshaw Date: Thu, 25 Dec 2025 23:51:11 -0800 Subject: [PATCH] =?UTF-8?q?feat(geminicli):=20=E6=B7=BB=E5=8A=A0=E5=86=85?= =?UTF-8?q?=E7=BD=AE=20Gemini=20CLI=20OAuth=20=E5=AE=A2=E6=88=B7=E7=AB=AF?= =?UTF-8?q?=E5=B8=B8=E9=87=8F=E5=92=8C=E6=94=B9=E8=BF=9B=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加 GeminiCLIOAuthClientID/Secret 常量(Gemini CLI 公开 OAuth 客户端) - 更新 DefaultAIStudioScopes 使用 generative-language.retriever(符合 Google 文档) - EffectiveOAuthConfig 支持自动回退到内置客户端 - 内置客户端自动过滤受限 scope(如 generative-language) - 添加 scope 向后兼容性处理 --- backend/internal/pkg/geminicli/constants.go | 10 +++- backend/internal/pkg/geminicli/oauth.go | 60 ++++++++++++++++++--- 2 files changed, 62 insertions(+), 8 deletions(-) diff --git a/backend/internal/pkg/geminicli/constants.go b/backend/internal/pkg/geminicli/constants.go index 8dd8c645..63f48727 100644 --- a/backend/internal/pkg/geminicli/constants.go +++ b/backend/internal/pkg/geminicli/constants.go @@ -22,11 +22,19 @@ const ( // DefaultScopes for AI Studio (uses generativelanguage API with OAuth) // Reference: https://ai.google.dev/gemini-api/docs/oauth // For regular Google accounts, supports API calls to generativelanguage.googleapis.com - DefaultAIStudioScopes = "https://www.googleapis.com/auth/cloud-platform https://www.googleapis.com/auth/generative-language" + // Note: Google Auth platform currently documents the OAuth scope as + // https://www.googleapis.com/auth/generative-language.retriever (often with cloud-platform). + DefaultAIStudioScopes = "https://www.googleapis.com/auth/cloud-platform https://www.googleapis.com/auth/generative-language.retriever" // GeminiCLIRedirectURI is the redirect URI used by Gemini CLI for Code Assist OAuth. GeminiCLIRedirectURI = "https://codeassist.google.com/authcode" + // GeminiCLIOAuthClientID/Secret are the public OAuth client credentials used by Google Gemini CLI. + // They enable the "login without creating your own OAuth client" experience, but Google may + // restrict which scopes are allowed for this client. + GeminiCLIOAuthClientID = "681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com" + GeminiCLIOAuthClientSecret = "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" + SessionTTL = 30 * time.Minute // GeminiCLIUserAgent mimics Gemini CLI to maximize compatibility with internal endpoints. diff --git a/backend/internal/pkg/geminicli/oauth.go b/backend/internal/pkg/geminicli/oauth.go index e502de65..f93d99b9 100644 --- a/backend/internal/pkg/geminicli/oauth.go +++ b/backend/internal/pkg/geminicli/oauth.go @@ -140,9 +140,13 @@ func base64URLEncode(data []byte) string { } // EffectiveOAuthConfig returns the effective OAuth configuration. -// oauthType: "code_assist" or "ai_studio" (defaults to "code_assist" if empty) -// Returns error if ClientID or ClientSecret is not configured. -// Configure via GEMINI_OAUTH_CLIENT_ID and GEMINI_OAUTH_CLIENT_SECRET environment variables. +// oauthType: "code_assist" or "ai_studio" (defaults to "code_assist" if empty). +// +// If ClientID/ClientSecret is not provided, this falls back to the built-in Gemini CLI OAuth client. +// +// Note: The built-in Gemini CLI OAuth client is restricted and may reject some scopes (e.g. +// https://www.googleapis.com/auth/generative-language), which will surface as +// "restricted_client" / "Unregistered scope(s)" errors during browser authorization. func EffectiveOAuthConfig(cfg OAuthConfig, oauthType string) (OAuthConfig, error) { effective := OAuthConfig{ ClientID: strings.TrimSpace(cfg.ClientID), @@ -150,19 +154,61 @@ func EffectiveOAuthConfig(cfg OAuthConfig, oauthType string) (OAuthConfig, error Scopes: strings.TrimSpace(cfg.Scopes), } - // Require OAuth credentials to be configured - if effective.ClientID == "" || effective.ClientSecret == "" { - return OAuthConfig{}, fmt.Errorf("gemini OAuth credentials not configured, set GEMINI_OAUTH_CLIENT_ID and GEMINI_OAUTH_CLIENT_SECRET environment variables") + // Normalize scopes: allow comma-separated input but send space-delimited scopes to Google. + if effective.Scopes != "" { + effective.Scopes = strings.Join(strings.Fields(strings.ReplaceAll(effective.Scopes, ",", " ")), " ") } + // Fall back to built-in Gemini CLI OAuth client when not configured. + if effective.ClientID == "" && effective.ClientSecret == "" { + effective.ClientID = GeminiCLIOAuthClientID + effective.ClientSecret = GeminiCLIOAuthClientSecret + } else if effective.ClientID == "" || effective.ClientSecret == "" { + return OAuthConfig{}, fmt.Errorf("OAuth client not configured: please set both client_id and client_secret (or leave both empty to use the built-in Gemini CLI client)") + } + + isBuiltinClient := effective.ClientID == GeminiCLIOAuthClientID && + effective.ClientSecret == GeminiCLIOAuthClientSecret + if effective.Scopes == "" { // Use different default scopes based on OAuth type if oauthType == "ai_studio" { - effective.Scopes = DefaultAIStudioScopes + // Built-in client can't request some AI Studio scopes (notably generative-language). + if isBuiltinClient { + effective.Scopes = DefaultCodeAssistScopes + } else { + effective.Scopes = DefaultAIStudioScopes + } } else { // Default to Code Assist scopes effective.Scopes = DefaultCodeAssistScopes } + } else if oauthType == "ai_studio" && isBuiltinClient { + // If user overrides scopes while still using the built-in client, strip restricted scopes. + parts := strings.Fields(effective.Scopes) + filtered := make([]string, 0, len(parts)) + for _, s := range parts { + if strings.Contains(s, "generative-language") { + continue + } + filtered = append(filtered, s) + } + if len(filtered) == 0 { + effective.Scopes = DefaultCodeAssistScopes + } else { + effective.Scopes = strings.Join(filtered, " ") + } + } + + // Backward compatibility: normalize older AI Studio scope to the currently documented one. + if oauthType == "ai_studio" && effective.Scopes != "" { + parts := strings.Fields(effective.Scopes) + for i := range parts { + if parts[i] == "https://www.googleapis.com/auth/generative-language" { + parts[i] = "https://www.googleapis.com/auth/generative-language.retriever" + } + } + effective.Scopes = strings.Join(parts, " ") } return effective, nil