From 0dbea6ca5890e10a0389f9ea3758fd7a0c2910be Mon Sep 17 00:00:00 2001 From: cagedbird043 Date: Tue, 24 Feb 2026 20:01:48 +0800 Subject: [PATCH 1/5] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20Gemini=20?= =?UTF-8?q?=E6=8E=88=E6=9D=83=E9=93=BE=E6=8E=A5=E7=94=9F=E6=88=90=E5=A4=B1?= =?UTF-8?q?=E8=B4=A5=E5=B9=B6=E6=94=B9=E8=BF=9B=E9=94=99=E8=AF=AF=E6=8F=90?= =?UTF-8?q?=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../internal/handler/admin/gemini_oauth_handler.go | 6 +++++- backend/internal/pkg/geminicli/constants.go | 6 ++---- backend/internal/pkg/geminicli/oauth_test.go | 14 ++++++++------ 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/backend/internal/handler/admin/gemini_oauth_handler.go b/backend/internal/handler/admin/gemini_oauth_handler.go index 50caaa26..8c398a1e 100644 --- a/backend/internal/handler/admin/gemini_oauth_handler.go +++ b/backend/internal/handler/admin/gemini_oauth_handler.go @@ -61,7 +61,11 @@ func (h *GeminiOAuthHandler) GenerateAuthURL(c *gin.Context) { if err != nil { msg := err.Error() // Treat missing/invalid OAuth client configuration as a user/config error. - if strings.Contains(msg, "OAuth client not configured") || strings.Contains(msg, "requires your own OAuth Client") { + if strings.Contains(msg, "OAuth client not configured") || + strings.Contains(msg, "requires your own OAuth Client") || + strings.Contains(msg, "requires a custom OAuth Client") || + strings.Contains(msg, "GEMINI_CLI_OAUTH_CLIENT_SECRET_MISSING") || + strings.Contains(msg, "built-in Gemini CLI OAuth client_secret is not configured") { response.BadRequest(c, "Failed to generate auth URL: "+msg) return } diff --git a/backend/internal/pkg/geminicli/constants.go b/backend/internal/pkg/geminicli/constants.go index f85e3b97..97234ffd 100644 --- a/backend/internal/pkg/geminicli/constants.go +++ b/backend/internal/pkg/geminicli/constants.go @@ -38,10 +38,8 @@ const ( // 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 is intentionally not embedded in this repository. - // If you rely on the built-in Gemini CLI OAuth client, you MUST provide its client_secret via config/env. - GeminiCLIOAuthClientSecret = "" + GeminiCLIOAuthClientID = "681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com" + GeminiCLIOAuthClientSecret = "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" // GeminiCLIOAuthClientSecretEnv is the environment variable name for the built-in client secret. GeminiCLIOAuthClientSecretEnv = "GEMINI_CLI_OAUTH_CLIENT_SECRET" diff --git a/backend/internal/pkg/geminicli/oauth_test.go b/backend/internal/pkg/geminicli/oauth_test.go index 14bc3c6b..d6f1090b 100644 --- a/backend/internal/pkg/geminicli/oauth_test.go +++ b/backend/internal/pkg/geminicli/oauth_test.go @@ -685,15 +685,17 @@ func TestEffectiveOAuthConfig_WhitespaceTriming(t *testing.T) { } func TestEffectiveOAuthConfig_NoEnvSecret(t *testing.T) { - // 不设置环境变量且不提供凭据,应该报错 t.Setenv(GeminiCLIOAuthClientSecretEnv, "") - _, err := EffectiveOAuthConfig(OAuthConfig{}, "code_assist") - if err == nil { - t.Error("没有内置 secret 且未提供凭据时应该报错") + cfg, err := EffectiveOAuthConfig(OAuthConfig{}, "code_assist") + if err != nil { + t.Fatalf("不设置环境变量时应回退到内置 secret,实际报错: %v", err) } - if !strings.Contains(err.Error(), GeminiCLIOAuthClientSecretEnv) { - t.Errorf("错误消息应提及环境变量 %s,实际: %v", GeminiCLIOAuthClientSecretEnv, err) + if strings.TrimSpace(cfg.ClientSecret) == "" { + t.Error("ClientSecret 不应为空") + } + if cfg.ClientID != GeminiCLIOAuthClientID { + t.Errorf("ClientID 应回退为内置客户端 ID,实际: %q", cfg.ClientID) } } From 9bd6a62ab3f894e90703811f101e639a8b45cfea Mon Sep 17 00:00:00 2001 From: cagedbird043 Date: Tue, 24 Feb 2026 20:03:39 +0800 Subject: [PATCH 2/5] =?UTF-8?q?test:=20=E6=9B=B4=E6=96=B0=20Gemini=20OAuth?= =?UTF-8?q?=20=E5=86=85=E7=BD=AE=E5=9B=9E=E9=80=80=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=E7=94=A8=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/pkg/geminicli/oauth_test.go | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/backend/internal/pkg/geminicli/oauth_test.go b/backend/internal/pkg/geminicli/oauth_test.go index d6f1090b..2a430f9e 100644 --- a/backend/internal/pkg/geminicli/oauth_test.go +++ b/backend/internal/pkg/geminicli/oauth_test.go @@ -408,11 +408,10 @@ func TestBuildAuthorizationURL_WithProjectID(t *testing.T) { } } -func TestBuildAuthorizationURL_OAuthConfigError(t *testing.T) { - // 不设置环境变量,也不提供 client 凭据,EffectiveOAuthConfig 应该报错 +func TestBuildAuthorizationURL_UsesBuiltinSecretFallback(t *testing.T) { t.Setenv(GeminiCLIOAuthClientSecretEnv, "") - _, err := BuildAuthorizationURL( + authURL, err := BuildAuthorizationURL( OAuthConfig{}, "test-state", "test-challenge", @@ -420,8 +419,11 @@ func TestBuildAuthorizationURL_OAuthConfigError(t *testing.T) { "", "code_assist", ) - if err == nil { - t.Error("当 EffectiveOAuthConfig 失败时,BuildAuthorizationURL 应该返回错误") + if err != nil { + t.Fatalf("BuildAuthorizationURL() 不应报错: %v", err) + } + if !strings.Contains(authURL, "client_id="+GeminiCLIOAuthClientID) { + t.Errorf("应使用内置 Gemini CLI client_id,实际 URL: %s", authURL) } } From c10267ce2be8ad0668daabcae8a6ab8dc585cf2e Mon Sep 17 00:00:00 2001 From: cagedbird043 Date: Tue, 24 Feb 2026 20:01:58 +0800 Subject: [PATCH 3/5] =?UTF-8?q?fix:=20=E5=88=B7=E6=96=B0=E7=94=A8=E9=87=8F?= =?UTF-8?q?=E6=88=90=E5=8A=9F=E5=90=8E=E8=87=AA=E5=8A=A8=E6=B8=85=E7=90=86?= =?UTF-8?q?=E8=B4=A6=E5=8F=B7=E5=8F=AF=E6=81=A2=E5=A4=8D=E9=94=99=E8=AF=AF?= =?UTF-8?q?=E7=8A=B6=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../internal/service/account_usage_service.go | 40 ++++++++++++++++++- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/backend/internal/service/account_usage_service.go b/backend/internal/service/account_usage_service.go index 7698223e..a363a790 100644 --- a/backend/internal/service/account_usage_service.go +++ b/backend/internal/service/account_usage_service.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "log" + "strings" "sync" "time" @@ -217,12 +218,20 @@ func (s *AccountUsageService) GetUsage(ctx context.Context, accountID int64) (*U } if account.Platform == PlatformGemini { - return s.getGeminiUsage(ctx, account) + usage, err := s.getGeminiUsage(ctx, account) + if err == nil { + s.tryClearRecoverableAccountError(ctx, account) + } + return usage, err } // Antigravity 平台:使用 AntigravityQuotaFetcher 获取额度 if account.Platform == PlatformAntigravity { - return s.getAntigravityUsage(ctx, account) + usage, err := s.getAntigravityUsage(ctx, account) + if err == nil { + s.tryClearRecoverableAccountError(ctx, account) + } + return usage, err } // 只有oauth类型账号可以通过API获取usage(有profile scope) @@ -256,6 +265,7 @@ func (s *AccountUsageService) GetUsage(ctx context.Context, accountID int64) (*U // 4. 添加窗口统计(有独立缓存,1 分钟) s.addWindowStats(ctx, account, usage) + s.tryClearRecoverableAccountError(ctx, account) return usage, nil } @@ -486,6 +496,32 @@ func parseTime(s string) (time.Time, error) { return time.Time{}, fmt.Errorf("unable to parse time: %s", s) } +func (s *AccountUsageService) tryClearRecoverableAccountError(ctx context.Context, account *Account) { + if account == nil || account.Status != StatusError { + return + } + + msg := strings.ToLower(strings.TrimSpace(account.ErrorMessage)) + if msg == "" { + return + } + + if !strings.Contains(msg, "token refresh failed") && + !strings.Contains(msg, "invalid_client") && + !strings.Contains(msg, "missing_project_id") && + !strings.Contains(msg, "unauthenticated") { + return + } + + if err := s.accountRepo.ClearError(ctx, account.ID); err != nil { + log.Printf("[usage] failed to clear recoverable account error for account %d: %v", account.ID, err) + return + } + + account.Status = StatusActive + account.ErrorMessage = "" +} + // buildUsageInfo 构建UsageInfo func (s *AccountUsageService) buildUsageInfo(resp *ClaudeUsageResponse, updatedAt *time.Time) *UsageInfo { info := &UsageInfo{ From ea8104c6a25e69ebabd2112cb36e57b12189ec4a Mon Sep 17 00:00:00 2001 From: cagedbird043 Date: Tue, 24 Feb 2026 20:30:15 +0800 Subject: [PATCH 4/5] =?UTF-8?q?fix:=20antigravity=20=E9=BB=98=E8=AE=A4?= =?UTF-8?q?=E8=A1=A5=E5=85=A8=20gemini-3-flash=20=E9=80=8F=E4=BC=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/service/account.go | 18 ++++++++++ .../internal/service/account_wildcard_test.go | 35 +++++++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/backend/internal/service/account.go b/backend/internal/service/account.go index 51ab84dd..6ac185dc 100644 --- a/backend/internal/service/account.go +++ b/backend/internal/service/account.go @@ -372,6 +372,9 @@ func (a *Account) GetModelMapping() map[string]string { } } if len(result) > 0 { + if a.Platform == domain.PlatformAntigravity { + ensureAntigravityDefaultPassthrough(result, "gemini-3-flash") + } return result } } @@ -382,6 +385,21 @@ func (a *Account) GetModelMapping() map[string]string { return nil } +func ensureAntigravityDefaultPassthrough(mapping map[string]string, model string) { + if mapping == nil || model == "" { + return + } + if _, exists := mapping[model]; exists { + return + } + for pattern := range mapping { + if matchWildcard(pattern, model) { + return + } + } + mapping[model] = model +} + // IsModelSupported 检查模型是否在 model_mapping 中(支持通配符) // 如果未配置 mapping,返回 true(允许所有模型) func (a *Account) IsModelSupported(requestedModel string) bool { diff --git a/backend/internal/service/account_wildcard_test.go b/backend/internal/service/account_wildcard_test.go index 90e5b573..c78ce554 100644 --- a/backend/internal/service/account_wildcard_test.go +++ b/backend/internal/service/account_wildcard_test.go @@ -267,3 +267,38 @@ func TestAccountGetMappedModel(t *testing.T) { }) } } + +func TestAccountGetModelMapping_AntigravityEnsuresGemini3FlashPassthrough(t *testing.T) { + account := &Account{ + Platform: PlatformAntigravity, + Credentials: map[string]any{ + "model_mapping": map[string]any{ + "gemini-3-pro-high": "gemini-3.1-pro-high", + }, + }, + } + + mapping := account.GetModelMapping() + if mapping["gemini-3-flash"] != "gemini-3-flash" { + t.Fatalf("expected gemini-3-flash passthrough to be auto-filled, got: %q", mapping["gemini-3-flash"]) + } +} + +func TestAccountGetModelMapping_AntigravityRespectsWildcardOverride(t *testing.T) { + account := &Account{ + Platform: PlatformAntigravity, + Credentials: map[string]any{ + "model_mapping": map[string]any{ + "gemini-3*": "gemini-3.1-pro-high", + }, + }, + } + + mapping := account.GetModelMapping() + if _, exists := mapping["gemini-3-flash"]; exists { + t.Fatalf("did not expect explicit gemini-3-flash passthrough when wildcard already exists") + } + if mapped := account.GetMappedModel("gemini-3-flash"); mapped != "gemini-3.1-pro-high" { + t.Fatalf("expected wildcard mapping to stay effective, got: %q", mapped) + } +} From 076c00063d0d20e12b311c3d54fb0b789fcc3162 Mon Sep 17 00:00:00 2001 From: cagedbird043 Date: Tue, 24 Feb 2026 20:30:27 +0800 Subject: [PATCH 5/5] =?UTF-8?q?feat:=20=E8=A1=A5=E5=85=A8=20antigravity=20?= =?UTF-8?q?=E6=A8=A1=E5=9E=8B=E6=98=A0=E5=B0=84=E5=BF=AB=E6=8D=B7=E6=8C=89?= =?UTF-8?q?=E9=92=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../account/BulkEditAccountModal.vue | 55 +++++++++++++++++++ frontend/src/composables/useModelWhitelist.ts | 6 ++ 2 files changed, 61 insertions(+) diff --git a/frontend/src/components/account/BulkEditAccountModal.vue b/frontend/src/components/account/BulkEditAccountModal.vue index 75fffc19..18d4510f 100644 --- a/frontend/src/components/account/BulkEditAccountModal.vue +++ b/frontend/src/components/account/BulkEditAccountModal.vue @@ -765,6 +765,31 @@ const presetMappings = [ color: 'bg-purple-100 text-purple-700 hover:bg-purple-200 dark:bg-purple-900/30 dark:text-purple-400' }, + { + label: 'Sonnet4→4.6', + from: 'claude-sonnet-4-20250514', + to: 'claude-sonnet-4-6', + color: 'bg-sky-100 text-sky-700 hover:bg-sky-200 dark:bg-sky-900/30 dark:text-sky-400' + }, + { + label: 'Sonnet4.5→4.6', + from: 'claude-sonnet-4-5-20250929', + to: 'claude-sonnet-4-6', + color: 'bg-cyan-100 text-cyan-700 hover:bg-cyan-200 dark:bg-cyan-900/30 dark:text-cyan-400' + }, + { + label: 'Sonnet3.5→4.6', + from: 'claude-3-5-sonnet-20241022', + to: 'claude-sonnet-4-6', + color: 'bg-teal-100 text-teal-700 hover:bg-teal-200 dark:bg-teal-900/30 dark:text-teal-400' + }, + { + label: 'Opus4.5→4.6', + from: 'claude-opus-4-5-20251101', + to: 'claude-opus-4-6-thinking', + color: + 'bg-violet-100 text-violet-700 hover:bg-violet-200 dark:bg-violet-900/30 dark:text-violet-400' + }, { label: 'Opus->Sonnet', from: 'claude-opus-4-5-20251101', @@ -794,6 +819,36 @@ const presetMappings = [ from: 'gpt-5.1-codex-max', to: 'gpt-5.1-codex', color: 'bg-pink-100 text-pink-700 hover:bg-pink-200 dark:bg-pink-900/30 dark:text-pink-400' + }, + { + label: '3-Pro-Preview→3.1-Pro-High', + from: 'gemini-3-pro-preview', + to: 'gemini-3.1-pro-high', + color: 'bg-amber-100 text-amber-700 hover:bg-amber-200 dark:bg-amber-900/30 dark:text-amber-400' + }, + { + label: '3-Pro-High→3.1-Pro-High', + from: 'gemini-3-pro-high', + to: 'gemini-3.1-pro-high', + color: 'bg-orange-100 text-orange-700 hover:bg-orange-200 dark:bg-orange-900/30 dark:text-orange-400' + }, + { + label: '3-Pro-Low→3.1-Pro-Low', + from: 'gemini-3-pro-low', + to: 'gemini-3.1-pro-low', + color: 'bg-yellow-100 text-yellow-700 hover:bg-yellow-200 dark:bg-yellow-900/30 dark:text-yellow-400' + }, + { + label: '3-Flash透传', + from: 'gemini-3-flash', + to: 'gemini-3-flash', + color: 'bg-lime-100 text-lime-700 hover:bg-lime-200 dark:bg-lime-900/30 dark:text-lime-400' + }, + { + label: '2.5-Flash-Lite透传', + from: 'gemini-2.5-flash-lite', + to: 'gemini-2.5-flash-lite', + color: 'bg-green-100 text-green-700 hover:bg-green-200 dark:bg-green-900/30 dark:text-green-400' } ] diff --git a/frontend/src/composables/useModelWhitelist.ts b/frontend/src/composables/useModelWhitelist.ts index 80416f40..5de7efb2 100644 --- a/frontend/src/composables/useModelWhitelist.ts +++ b/frontend/src/composables/useModelWhitelist.ts @@ -287,6 +287,10 @@ const antigravityPresetMappings = [ { label: 'Sonnet→Sonnet', from: 'claude-sonnet-*', to: 'claude-sonnet-4-5', color: 'bg-indigo-100 text-indigo-700 hover:bg-indigo-200 dark:bg-indigo-900/30 dark:text-indigo-400' }, { label: 'Opus→Opus', from: 'claude-opus-*', to: 'claude-opus-4-6-thinking', color: 'bg-purple-100 text-purple-700 hover:bg-purple-200 dark:bg-purple-900/30 dark:text-purple-400' }, { label: 'Haiku→Sonnet', from: 'claude-haiku-*', to: 'claude-sonnet-4-5', color: 'bg-emerald-100 text-emerald-700 hover:bg-emerald-200 dark:bg-emerald-900/30 dark:text-emerald-400' }, + { label: 'Sonnet4→4.6', from: 'claude-sonnet-4-20250514', to: 'claude-sonnet-4-6', color: 'bg-sky-100 text-sky-700 hover:bg-sky-200 dark:bg-sky-900/30 dark:text-sky-400' }, + { label: 'Sonnet4.5→4.6', from: 'claude-sonnet-4-5-20250929', to: 'claude-sonnet-4-6', color: 'bg-cyan-100 text-cyan-700 hover:bg-cyan-200 dark:bg-cyan-900/30 dark:text-cyan-400' }, + { label: 'Sonnet3.5→4.6', from: 'claude-3-5-sonnet-20241022', to: 'claude-sonnet-4-6', color: 'bg-teal-100 text-teal-700 hover:bg-teal-200 dark:bg-teal-900/30 dark:text-teal-400' }, + { label: 'Opus4.5→4.6', from: 'claude-opus-4-5-20251101', to: 'claude-opus-4-6-thinking', color: 'bg-violet-100 text-violet-700 hover:bg-violet-200 dark:bg-violet-900/30 dark:text-violet-400' }, // Gemini 3→3.1 映射 { label: '3-Pro-Preview→3.1-Pro-High', from: 'gemini-3-pro-preview', to: 'gemini-3.1-pro-high', color: 'bg-amber-100 text-amber-700 hover:bg-amber-200 dark:bg-amber-900/30 dark:text-amber-400' }, { label: '3-Pro-High→3.1-Pro-High', from: 'gemini-3-pro-high', to: 'gemini-3.1-pro-high', color: 'bg-orange-100 text-orange-700 hover:bg-orange-200 dark:bg-orange-900/30 dark:text-orange-400' }, @@ -294,6 +298,8 @@ const antigravityPresetMappings = [ // Gemini 通配符映射 { label: 'Gemini 3→Flash', from: 'gemini-3*', to: 'gemini-3-flash', color: 'bg-yellow-100 text-yellow-700 hover:bg-yellow-200 dark:bg-yellow-900/30 dark:text-yellow-400' }, { label: 'Gemini 2.5→Flash', from: 'gemini-2.5*', to: 'gemini-2.5-flash', color: 'bg-orange-100 text-orange-700 hover:bg-orange-200 dark:bg-orange-900/30 dark:text-orange-400' }, + { label: '3-Flash透传', from: 'gemini-3-flash', to: 'gemini-3-flash', color: 'bg-lime-100 text-lime-700 hover:bg-lime-200 dark:bg-lime-900/30 dark:text-lime-400' }, + { label: '2.5-Flash-Lite透传', from: 'gemini-2.5-flash-lite', to: 'gemini-2.5-flash-lite', color: 'bg-green-100 text-green-700 hover:bg-green-200 dark:bg-green-900/30 dark:text-green-400' }, // 精确映射 { label: 'Sonnet 4.5', from: 'claude-sonnet-4-5', to: 'claude-sonnet-4-5', color: 'bg-cyan-100 text-cyan-700 hover:bg-cyan-200 dark:bg-cyan-900/30 dark:text-cyan-400' }, { label: 'Opus 4.6-thinking', from: 'claude-opus-4-6-thinking', to: 'claude-opus-4-6-thinking', color: 'bg-pink-100 text-pink-700 hover:bg-pink-200 dark:bg-pink-900/30 dark:text-pink-400' }