From afcfbb458d2e2ce9585289ecb36b9790cd882afb Mon Sep 17 00:00:00 2001
From: IanShaw <131567472+IanShaw027@users.noreply.github.com>
Date: Thu, 8 Jan 2026 23:47:29 +0800
Subject: [PATCH 01/17] =?UTF-8?q?fix(gemini):=20Google=20One=20=E5=BC=BA?=
=?UTF-8?q?=E5=88=B6=E4=BD=BF=E7=94=A8=E5=86=85=E7=BD=AE=20OAuth=20client?=
=?UTF-8?q?=20+=20=E8=87=AA=E5=8A=A8=E8=8E=B7=E5=8F=96=20project=5Fid=20+?=
=?UTF-8?q?=20UI=20=E4=BC=98=E5=8C=96=20(#212)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* fix(gemini): Google One 强制使用内置 OAuth client + 自动获取 project_id + UI 优化
## 后端改动
### 1. Google One 强制使用内置 Gemini CLI OAuth Client
**问题**:
- Google One 之前允许使用自定义 OAuth client,导致认证流程不稳定
- 与 Code Assist 的行为不一致
**解决方案**:
- 修改 `gemini_oauth_service.go`: Google One 现在与 Code Assist 一样强制使用内置 client (L122-135)
- 更新 `gemini_oauth_client.go`: ExchangeCode 和 RefreshToken 方法支持强制内置 client (L31-44, L77-86)
- 简化 `geminicli/oauth.go`: Google One scope 选择逻辑 (L187-190)
- 标记 `geminicli/constants.go`: DefaultGoogleOneScopes 为 DEPRECATED (L30-33)
- 更新测试用例以反映新行为
**OAuth 类型对比**:
| OAuth类型 | Client来源 | Scopes | Redirect URI |
|-----------|-----------|--------|-----------------|
| code_assist | 内置 Gemini CLI | DefaultCodeAssistScopes | https://codeassist.google.com/authcode |
| google_one | 内置 Gemini CLI (新) | DefaultCodeAssistScopes | https://codeassist.google.com/authcode |
| ai_studio | 必须自定义 | DefaultAIStudioScopes | http://localhost:1455/auth/callback |
### 2. Google One 自动获取 project_id
**问题**:
- Google One 个人账号测试模型时返回 403/404 错误
- 原因:cloudaicompanion API 需要 project_id,但个人账号无需手动创建 GCP 项目
**解决方案**:
- 修改 `gemini_oauth_service.go`: OAuth 流程中自动调用 fetchProjectID
- Google 通过 LoadCodeAssist API 自动分配 project_id
- 与 Gemini CLI 行为保持一致
- 后端根据 project_id 自动选择正确的 API 端点
**影响**:
- Google One 账号现在可以正常使用(需要重新授权)
- Code Assist 和 AI Studio 账号不受影响
### 3. 修复 Gemini 测试账号无内容输出问题
**问题**:
- 测试 Gemini 账号时只显示"测试成功",没有显示 AI 响应内容
- 原因:processGeminiStream 在检查到 finishReason 时立即返回,跳过了内容提取
**解决方案**:
- 修改 `account_test_service.go`: 调整逻辑顺序,先提取内容再检查是否完成
- 确保最后一个 chunk 的内容也能被正确显示
**影响**:
- 所有 Gemini 账号类型(API Key、OAuth)的测试现在都会显示完整响应内容
- 用户可以看到流式输出效果
## 前端改动
### 1. 修复图标宽度压缩问题
**问题**:
- 账户类型选择按钮中的图标在某些情况下会被压缩变形
**解决方案**:
- 修改 `CreateAccountModal.vue`: 为所有平台图标容器添加 `shrink-0` 类
- 确保 Anthropic、OpenAI、Gemini、Antigravity 图标保持固定 8×8 尺寸 (32px × 32px)
### 2. 优化重新授权界面
**问题**:
- 重新授权时显示三个可点击的授权类型选择按钮,可能导致用户误切换到不兼容的授权方式
**解决方案**:
- 修改 `ReAuthAccountModal.vue` (admin 和普通用户版本):
- 将可点击的授权类型选择按钮改为只读信息展示框
- 根据账号的 `credentials.oauth_type` 动态显示对应图标和文本
- 删除 `geminiAIStudioOAuthEnabled` 状态和 `handleSelectGeminiOAuthType` 方法
- 防止用户误操作
## 测试验证
- ✅ 所有后端单元测试通过
- ✅ OAuth client 选择逻辑正确
- ✅ Google One 和 Code Assist 行为一致
- ✅ 测试账号显示完整响应内容
- ✅ UI 图标显示正常
- ✅ 重新授权界面只读展示正确
* fix(lint): 修复 golangci-lint 错误信息格式问题
- 将错误信息改为小写开头以符合 Go 代码规范
- 修复 ST1005: error strings should not be capitalized
---
backend/internal/pkg/geminicli/constants.go | 7 +-
backend/internal/pkg/geminicli/oauth.go | 10 +-
backend/internal/pkg/geminicli/oauth_test.go | 4 +-
.../repository/gemini_oauth_client.go | 8 +-
.../internal/service/account_test_service.go | 14 +-
.../internal/service/gemini_oauth_service.go | 23 ++-
.../service/gemini_oauth_service_test.go | 8 +-
.../components/account/CreateAccountModal.vue | 20 +--
.../components/account/ReAuthAccountModal.vue | 165 +++++-------------
.../admin/account/ReAuthAccountModal.vue | 163 +++++------------
10 files changed, 135 insertions(+), 287 deletions(-)
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 @@
-
-
-
-