From 1439eb39a9fd07eb27466bd3bbc65afefae966cd Mon Sep 17 00:00:00 2001 From: shaw Date: Sat, 7 Feb 2026 17:12:15 +0800 Subject: [PATCH] fix(gateway): harden digest logging and align antigravity ops - avoid panic by using safe UUID prefix truncation in Gemini digest fallback logs\n- remove unconditional Antigravity 429 full-body debug logs and honor log truncation config\n- align Antigravity quick preset mappings to opus 4.6-thinking targets only\n- restore scope rate-limit aggregation/output in ops availability stats --- .../handler/gemini_cli_session_test.go | 21 +++++++++++++++++++ .../internal/handler/gemini_v1beta_handler.go | 11 +++++++++- .../service/antigravity_gateway_service.go | 11 ++++------ .../service/ops_account_availability.go | 20 ++++++++++++++++++ frontend/src/composables/useModelWhitelist.ts | 4 ++-- 5 files changed, 57 insertions(+), 10 deletions(-) diff --git a/backend/internal/handler/gemini_cli_session_test.go b/backend/internal/handler/gemini_cli_session_test.go index 0b37f5f2..80bc79c8 100644 --- a/backend/internal/handler/gemini_cli_session_test.go +++ b/backend/internal/handler/gemini_cli_session_test.go @@ -120,3 +120,24 @@ func TestGeminiCLITmpDirRegex(t *testing.T) { }) } } + +func TestSafeShortPrefix(t *testing.T) { + tests := []struct { + name string + input string + n int + want string + }{ + {name: "空字符串", input: "", n: 8, want: ""}, + {name: "长度小于截断值", input: "abc", n: 8, want: "abc"}, + {name: "长度等于截断值", input: "12345678", n: 8, want: "12345678"}, + {name: "长度大于截断值", input: "1234567890", n: 8, want: "12345678"}, + {name: "截断值为0", input: "123456", n: 0, want: "123456"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + require.Equal(t, tt.want, safeShortPrefix(tt.input, tt.n)) + }) + } +} diff --git a/backend/internal/handler/gemini_v1beta_handler.go b/backend/internal/handler/gemini_v1beta_handler.go index cc71e8e6..b1477ac6 100644 --- a/backend/internal/handler/gemini_v1beta_handler.go +++ b/backend/internal/handler/gemini_v1beta_handler.go @@ -294,7 +294,7 @@ func (h *GatewayHandler) GeminiV1BetaModels(c *gin.Context) { sessionBoundAccountID = foundAccountID geminiSessionUUID = foundUUID log.Printf("[Gemini] Digest fallback matched: uuid=%s, accountID=%d, chain=%s", - foundUUID[:8], foundAccountID, truncateDigestChain(geminiDigestChain)) + safeShortPrefix(foundUUID, 8), foundAccountID, truncateDigestChain(geminiDigestChain)) // 关键:如果原 sessionKey 为空,使用 prefixHash + uuid 作为 sessionKey // 这样 SelectAccountWithLoadAwareness 的粘性会话逻辑会优先使用匹配到的账号 @@ -650,6 +650,15 @@ func truncateDigestChain(chain string) string { return chain[:50] + "..." } +// safeShortPrefix 返回字符串前 n 个字符;长度不足时返回原字符串。 +// 用于日志展示,避免切片越界。 +func safeShortPrefix(value string, n int) string { + if n <= 0 || len(value) <= n { + return value + } + return value[:n] +} + // derefGroupID 安全解引用 *int64,nil 返回 0 func derefGroupID(groupID *int64) int64 { if groupID == nil { diff --git a/backend/internal/service/antigravity_gateway_service.go b/backend/internal/service/antigravity_gateway_service.go index 7fdb4d19..3d3c9cca 100644 --- a/backend/internal/service/antigravity_gateway_service.go +++ b/backend/internal/service/antigravity_gateway_service.go @@ -1950,11 +1950,6 @@ func parseAntigravitySmartRetryInfo(body []byte) *antigravitySmartRetryInfo { isResourceExhausted := status == googleRPCStatusResourceExhausted isUnavailable := status == googleRPCStatusUnavailable - // 调试日志:打印 RESOURCE_EXHAUSTED 的完整响应 - if isResourceExhausted { - log.Printf("[Antigravity-Debug] 429 RESOURCE_EXHAUSTED full body: %s", string(body)) - } - if !isResourceExhausted && !isUnavailable { return nil } @@ -2208,8 +2203,10 @@ func (s *AntigravityGatewayService) handleUpstreamError( // ========== 原有逻辑,保持不变 ========== // 429 使用 Gemini 格式解析(从 body 解析重置时间) if statusCode == 429 { - // 调试日志:打印 429 响应的完整 body - log.Printf("[Antigravity-Debug] 429 response full body: %s", string(body)) + // 调试日志遵循统一日志开关与长度限制,避免无条件记录完整上游响应体。 + if logBody, maxBytes := s.getLogConfig(); logBody { + log.Printf("[Antigravity-Debug] 429 response body: %s", truncateString(string(body), maxBytes)) + } useScopeLimit := quotaScope != "" resetAt := ParseGeminiRateLimitResetTime(body) diff --git a/backend/internal/service/ops_account_availability.go b/backend/internal/service/ops_account_availability.go index da66ec4d..a649e7b5 100644 --- a/backend/internal/service/ops_account_availability.go +++ b/backend/internal/service/ops_account_availability.go @@ -66,6 +66,7 @@ func (s *OpsService) GetAccountAvailabilityStats(ctx context.Context, platformFi } isAvailable := acc.Status == StatusActive && acc.Schedulable && !isRateLimited && !isOverloaded && !isTempUnsched + scopeRateLimits := acc.GetAntigravityScopeRateLimits() if acc.Platform != "" { if _, ok := platform[acc.Platform]; !ok { @@ -84,6 +85,14 @@ func (s *OpsService) GetAccountAvailabilityStats(ctx context.Context, platformFi if hasError { p.ErrorCount++ } + if len(scopeRateLimits) > 0 { + if p.ScopeRateLimitCount == nil { + p.ScopeRateLimitCount = make(map[string]int64) + } + for scope := range scopeRateLimits { + p.ScopeRateLimitCount[scope]++ + } + } } for _, grp := range acc.Groups { @@ -108,6 +117,14 @@ func (s *OpsService) GetAccountAvailabilityStats(ctx context.Context, platformFi if hasError { g.ErrorCount++ } + if len(scopeRateLimits) > 0 { + if g.ScopeRateLimitCount == nil { + g.ScopeRateLimitCount = make(map[string]int64) + } + for scope := range scopeRateLimits { + g.ScopeRateLimitCount[scope]++ + } + } } displayGroupID := int64(0) @@ -140,6 +157,9 @@ func (s *OpsService) GetAccountAvailabilityStats(ctx context.Context, platformFi item.RateLimitRemainingSec = &remainingSec } } + if len(scopeRateLimits) > 0 { + item.ScopeRateLimits = scopeRateLimits + } if isOverloaded && acc.OverloadUntil != nil { item.OverloadUntil = acc.OverloadUntil remainingSec := int64(time.Until(*acc.OverloadUntil).Seconds()) diff --git a/frontend/src/composables/useModelWhitelist.ts b/frontend/src/composables/useModelWhitelist.ts index d4446e2e..0ef80431 100644 --- a/frontend/src/composables/useModelWhitelist.ts +++ b/frontend/src/composables/useModelWhitelist.ts @@ -263,14 +263,14 @@ const antigravityPresetMappings = [ // Claude 通配符映射 { label: 'Claude→Sonnet', from: 'claude-*', to: 'claude-sonnet-4-5', color: 'bg-blue-100 text-blue-700 hover:bg-blue-200 dark:bg-blue-900/30 dark:text-blue-400' }, { 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-5-thinking', color: 'bg-purple-100 text-purple-700 hover:bg-purple-200 dark:bg-purple-900/30 dark:text-purple-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' }, // Gemini 通配符映射 { label: 'Gemini 3→Flash', from: 'gemini-3*', to: 'gemini-3-flash', color: 'bg-amber-100 text-amber-700 hover:bg-amber-200 dark:bg-amber-900/30 dark:text-amber-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: '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.5', from: 'claude-opus-4-5-thinking', to: 'claude-opus-4-5-thinking', color: 'bg-pink-100 text-pink-700 hover:bg-pink-200 dark:bg-pink-900/30 dark:text-pink-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' } ] // Antigravity 默认映射(从后端 API 获取,与 constants.go 保持一致)