diff --git a/backend/internal/pkg/geminicli/constants.go b/backend/internal/pkg/geminicli/constants.go index 6d7e5a5d..d4d52116 100644 --- a/backend/internal/pkg/geminicli/constants.go +++ b/backend/internal/pkg/geminicli/constants.go @@ -27,10 +27,9 @@ const ( // 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" - // DefaultScopes for Google One (personal Google accounts with Gemini access) - // Only used when a custom OAuth client is configured. When using the built-in Gemini CLI client, - // Google One uses DefaultCodeAssistScopes (same as code_assist) because the built-in client - // cannot request restricted scopes like generative-language.retriever or drive.readonly. + // DefaultGoogleOneScopes (DEPRECATED, no longer used) + // Google One now always uses the built-in Gemini CLI client with DefaultCodeAssistScopes. + // This constant is kept for backward compatibility but is not actively used. DefaultGoogleOneScopes = "https://www.googleapis.com/auth/cloud-platform https://www.googleapis.com/auth/generative-language.retriever https://www.googleapis.com/auth/drive.readonly https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile" // GeminiCLIRedirectURI is the redirect URI used by Gemini CLI for Code Assist OAuth. diff --git a/backend/internal/pkg/geminicli/oauth.go b/backend/internal/pkg/geminicli/oauth.go index 473017a2..c71e8aad 100644 --- a/backend/internal/pkg/geminicli/oauth.go +++ b/backend/internal/pkg/geminicli/oauth.go @@ -185,13 +185,9 @@ func EffectiveOAuthConfig(cfg OAuthConfig, oauthType string) (OAuthConfig, error effective.Scopes = DefaultAIStudioScopes } case "google_one": - // Google One uses built-in Gemini CLI client (same as code_assist) - // Built-in client can't request restricted scopes like generative-language.retriever - if isBuiltinClient { - effective.Scopes = DefaultCodeAssistScopes - } else { - effective.Scopes = DefaultGoogleOneScopes - } + // Google One always uses built-in Gemini CLI client (same as code_assist) + // Built-in client can't request restricted scopes like generative-language.retriever or drive.readonly + effective.Scopes = DefaultCodeAssistScopes default: // Default to Code Assist scopes effective.Scopes = DefaultCodeAssistScopes diff --git a/backend/internal/pkg/geminicli/oauth_test.go b/backend/internal/pkg/geminicli/oauth_test.go index 0520f0f2..0770730a 100644 --- a/backend/internal/pkg/geminicli/oauth_test.go +++ b/backend/internal/pkg/geminicli/oauth_test.go @@ -23,14 +23,14 @@ func TestEffectiveOAuthConfig_GoogleOne(t *testing.T) { wantErr: false, }, { - name: "Google One with custom client", + name: "Google One always uses built-in client (even if custom credentials passed)", input: OAuthConfig{ ClientID: "custom-client-id", ClientSecret: "custom-client-secret", }, oauthType: "google_one", wantClientID: "custom-client-id", - wantScopes: DefaultGoogleOneScopes, + wantScopes: DefaultCodeAssistScopes, // Uses code assist scopes even with custom client wantErr: false, }, { diff --git a/backend/internal/repository/gemini_oauth_client.go b/backend/internal/repository/gemini_oauth_client.go index 14ecfc89..8b7fe625 100644 --- a/backend/internal/repository/gemini_oauth_client.go +++ b/backend/internal/repository/gemini_oauth_client.go @@ -30,14 +30,15 @@ func (c *geminiOAuthClient) ExchangeCode(ctx context.Context, oauthType, code, c // Use different OAuth clients based on oauthType: // - code_assist: always use built-in Gemini CLI OAuth client (public) - // - google_one: uses configured OAuth client when provided; otherwise falls back to built-in client + // - google_one: always use built-in Gemini CLI OAuth client (public) // - ai_studio: requires a user-provided OAuth client oauthCfgInput := geminicli.OAuthConfig{ ClientID: c.cfg.Gemini.OAuth.ClientID, ClientSecret: c.cfg.Gemini.OAuth.ClientSecret, Scopes: c.cfg.Gemini.OAuth.Scopes, } - if oauthType == "code_assist" { + if oauthType == "code_assist" || oauthType == "google_one" { + // Force use of built-in Gemini CLI OAuth client oauthCfgInput.ClientID = "" oauthCfgInput.ClientSecret = "" } @@ -78,7 +79,8 @@ func (c *geminiOAuthClient) RefreshToken(ctx context.Context, oauthType, refresh ClientSecret: c.cfg.Gemini.OAuth.ClientSecret, Scopes: c.cfg.Gemini.OAuth.Scopes, } - if oauthType == "code_assist" { + if oauthType == "code_assist" || oauthType == "google_one" { + // Force use of built-in Gemini CLI OAuth client oauthCfgInput.ClientID = "" oauthCfgInput.ClientSecret = "" } diff --git a/backend/internal/service/account_test_service.go b/backend/internal/service/account_test_service.go index 7121a13d..8419c2b4 100644 --- a/backend/internal/service/account_test_service.go +++ b/backend/internal/service/account_test_service.go @@ -661,13 +661,7 @@ func (s *AccountTestService) processGeminiStream(c *gin.Context, body io.Reader) } if candidates, ok := data["candidates"].([]any); ok && len(candidates) > 0 { if candidate, ok := candidates[0].(map[string]any); ok { - // Check for completion - if finishReason, ok := candidate["finishReason"].(string); ok && finishReason != "" { - s.sendEvent(c, TestEvent{Type: "test_complete", Success: true}) - return nil - } - - // Extract content + // Extract content first (before checking completion) if content, ok := candidate["content"].(map[string]any); ok { if parts, ok := content["parts"].([]any); ok { for _, part := range parts { @@ -679,6 +673,12 @@ func (s *AccountTestService) processGeminiStream(c *gin.Context, body io.Reader) } } } + + // Check for completion after extracting content + if finishReason, ok := candidate["finishReason"].(string); ok && finishReason != "" { + s.sendEvent(c, TestEvent{Type: "test_complete", Success: true}) + return nil + } } } diff --git a/backend/internal/service/gemini_oauth_service.go b/backend/internal/service/gemini_oauth_service.go index 48d31da9..bc84baeb 100644 --- a/backend/internal/service/gemini_oauth_service.go +++ b/backend/internal/service/gemini_oauth_service.go @@ -120,15 +120,16 @@ func (s *GeminiOAuthService) GenerateAuthURL(ctx context.Context, proxyID *int64 } // OAuth client selection: - // - code_assist: always use built-in Gemini CLI OAuth client (public), regardless of configured client_id/secret. - // - google_one: uses configured OAuth client when provided; otherwise falls back to built-in client. - // - ai_studio: requires a user-provided OAuth client. + // - code_assist: always use built-in Gemini CLI OAuth client (public) + // - google_one: always use built-in Gemini CLI OAuth client (public) + // - ai_studio: requires a user-provided OAuth client oauthCfg := geminicli.OAuthConfig{ ClientID: s.cfg.Gemini.OAuth.ClientID, ClientSecret: s.cfg.Gemini.OAuth.ClientSecret, Scopes: s.cfg.Gemini.OAuth.Scopes, } - if oauthType == "code_assist" { + if oauthType == "code_assist" || oauthType == "google_one" { + // Force use of built-in Gemini CLI OAuth client oauthCfg.ClientID = "" oauthCfg.ClientSecret = "" } @@ -576,6 +577,20 @@ func (s *GeminiOAuthService) ExchangeCode(ctx context.Context, input *GeminiExch case "google_one": log.Printf("[GeminiOAuth] Processing google_one OAuth type") + + // Google One accounts use cloudaicompanion API, which requires a project_id. + // For personal accounts, Google auto-assigns a project_id via the LoadCodeAssist API. + if projectID == "" { + log.Printf("[GeminiOAuth] No project_id provided, attempting to fetch from LoadCodeAssist API...") + var err error + projectID, _, err = s.fetchProjectID(ctx, tokenResp.AccessToken, proxyURL) + if err != nil { + log.Printf("[GeminiOAuth] ERROR: Failed to fetch project_id: %v", err) + return nil, fmt.Errorf("google One accounts require a project_id, failed to auto-detect: %w", err) + } + log.Printf("[GeminiOAuth] Successfully fetched project_id: %s", projectID) + } + log.Printf("[GeminiOAuth] Attempting to fetch Google One tier from Drive API...") // Attempt to fetch Drive storage tier var storageInfo *geminicli.DriveStorageInfo diff --git a/backend/internal/service/gemini_oauth_service_test.go b/backend/internal/service/gemini_oauth_service_test.go index eb3d86e6..5591eb39 100644 --- a/backend/internal/service/gemini_oauth_service_test.go +++ b/backend/internal/service/gemini_oauth_service_test.go @@ -40,7 +40,7 @@ func TestGeminiOAuthService_GenerateAuthURL_RedirectURIStrategy(t *testing.T) { wantProjectID: "", }, { - name: "google_one uses custom client when configured and redirects to localhost", + name: "google_one always forces built-in client even when custom client configured", cfg: &config.Config{ Gemini: config.GeminiConfig{ OAuth: config.GeminiOAuthConfig{ @@ -50,9 +50,9 @@ func TestGeminiOAuthService_GenerateAuthURL_RedirectURIStrategy(t *testing.T) { }, }, oauthType: "google_one", - wantClientID: "custom-client-id", - wantRedirect: geminicli.AIStudioOAuthRedirectURI, - wantScope: geminicli.DefaultGoogleOneScopes, + wantClientID: geminicli.GeminiCLIOAuthClientID, + wantRedirect: geminicli.GeminiCLIRedirectURI, + wantScope: geminicli.DefaultCodeAssistScopes, wantProjectID: "", }, { diff --git a/frontend/src/components/account/CreateAccountModal.vue b/frontend/src/components/account/CreateAccountModal.vue index e90bec6c..5833632b 100644 --- a/frontend/src/components/account/CreateAccountModal.vue +++ b/frontend/src/components/account/CreateAccountModal.vue @@ -166,7 +166,7 @@ >
-
+
diff --git a/frontend/src/components/account/ReAuthAccountModal.vue b/frontend/src/components/account/ReAuthAccountModal.vue index 43d1198f..b2734b4f 100644 --- a/frontend/src/components/account/ReAuthAccountModal.vue +++ b/frontend/src/components/account/ReAuthAccountModal.vue @@ -73,113 +73,48 @@
- -
- {{ t('admin.accounts.oauth.gemini.oauthTypeLabel') }} -
- - - - - + +
+
+ {{ t('admin.accounts.oauth.gemini.oauthTypeLabel') }}
-
+
+
+ + + +
+
+ + {{ + geminiOAuthType === 'google_one' + ? 'Google One' + : geminiOAuthType === 'code_assist' + ? t('admin.accounts.gemini.oauthType.builtInTitle') + : t('admin.accounts.gemini.oauthType.customTitle') + }} + + + {{ + geminiOAuthType === 'google_one' + ? '个人账号' + : geminiOAuthType === 'code_assist' + ? t('admin.accounts.gemini.oauthType.builtInDesc') + : t('admin.accounts.gemini.oauthType.customDesc') + }} + +
+
+
(null) // State const addMethod = ref('oauth') const geminiOAuthType = ref<'code_assist' | 'google_one' | 'ai_studio'>('code_assist') -const geminiAIStudioOAuthEnabled = ref(false) // Computed - check platform const isOpenAI = computed(() => props.account?.platform === 'openai') @@ -367,14 +301,6 @@ watch( ? 'ai_studio' : 'code_assist' } - if (isGemini.value) { - geminiOAuth.getCapabilities().then((caps) => { - geminiAIStudioOAuthEnabled.value = !!caps?.ai_studio_oauth_enabled - if (!geminiAIStudioOAuthEnabled.value && geminiOAuthType.value === 'ai_studio') { - geminiOAuthType.value = 'code_assist' - } - }) - } } else { resetState() } @@ -385,7 +311,6 @@ watch( const resetState = () => { addMethod.value = 'oauth' geminiOAuthType.value = 'code_assist' - geminiAIStudioOAuthEnabled.value = false claudeOAuth.resetState() openaiOAuth.resetState() geminiOAuth.resetState() @@ -393,14 +318,6 @@ const resetState = () => { oauthFlowRef.value?.reset() } -const handleSelectGeminiOAuthType = (oauthType: 'code_assist' | 'google_one' | 'ai_studio') => { - if (oauthType === 'ai_studio' && !geminiAIStudioOAuthEnabled.value) { - appStore.showError(t('admin.accounts.oauth.gemini.aiStudioNotConfigured')) - return - } - geminiOAuthType.value = oauthType -} - const handleClose = () => { emit('close') } diff --git a/frontend/src/components/admin/account/ReAuthAccountModal.vue b/frontend/src/components/admin/account/ReAuthAccountModal.vue index d9838a2e..8133e029 100644 --- a/frontend/src/components/admin/account/ReAuthAccountModal.vue +++ b/frontend/src/components/admin/account/ReAuthAccountModal.vue @@ -73,111 +73,48 @@
- -
- {{ t('admin.accounts.oauth.gemini.oauthTypeLabel') }} -
- - - - - + +
+
+ {{ t('admin.accounts.oauth.gemini.oauthTypeLabel') }}
-
+
+
+ + + +
+
+ + {{ + geminiOAuthType === 'google_one' + ? 'Google One' + : geminiOAuthType === 'code_assist' + ? t('admin.accounts.gemini.oauthType.builtInTitle') + : t('admin.accounts.gemini.oauthType.customTitle') + }} + + + {{ + geminiOAuthType === 'google_one' + ? '个人账号' + : geminiOAuthType === 'code_assist' + ? t('admin.accounts.gemini.oauthType.builtInDesc') + : t('admin.accounts.gemini.oauthType.customDesc') + }} + +
+
+
(null) // State const addMethod = ref('oauth') const geminiOAuthType = ref<'code_assist' | 'google_one' | 'ai_studio'>('code_assist') -const geminiAIStudioOAuthEnabled = ref(false) // Computed - check platform const isOpenAI = computed(() => props.account?.platform === 'openai') @@ -365,14 +301,6 @@ watch( ? 'ai_studio' : 'code_assist' } - if (isGemini.value) { - geminiOAuth.getCapabilities().then((caps) => { - geminiAIStudioOAuthEnabled.value = !!caps?.ai_studio_oauth_enabled - if (!geminiAIStudioOAuthEnabled.value && geminiOAuthType.value === 'ai_studio') { - geminiOAuthType.value = 'code_assist' - } - }) - } } else { resetState() } @@ -383,7 +311,6 @@ watch( const resetState = () => { addMethod.value = 'oauth' geminiOAuthType.value = 'code_assist' - geminiAIStudioOAuthEnabled.value = false claudeOAuth.resetState() openaiOAuth.resetState() geminiOAuth.resetState() @@ -391,14 +318,6 @@ const resetState = () => { oauthFlowRef.value?.reset() } -const handleSelectGeminiOAuthType = (oauthType: 'code_assist' | 'google_one' | 'ai_studio') => { - if (oauthType === 'ai_studio' && !geminiAIStudioOAuthEnabled.value) { - appStore.showError(t('admin.accounts.oauth.gemini.aiStudioNotConfigured')) - return - } - geminiOAuthType.value = oauthType -} - const handleClose = () => { emit('close') }