From 1c68f9996df8e0bd7933d94d4ca826bc3dc570c7 Mon Sep 17 00:00:00 2001 From: feeeei Date: Sat, 10 Oct 2026 22:22:29 +0800 Subject: [PATCH] =?UTF-8?q?feat(gemini):=20=E9=80=82=E9=85=8D=20AI=20Studi?= =?UTF-8?q?o=20AQ.=20=E6=A0=BC=E5=BC=8F=20Key?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AI Studio 新建 Key 为 AQ.Ab 前缀的 auth key:logredact.RedactText 与 渠道监控 sanitizeErrorMessage 增加 AQ.Ab 规则,长度按 40 位以上放宽以覆盖格式变体 - 创建 / 编辑账号的 Key 占位符改为 AQ.Ab...,提示文案说明新旧两种前缀 与第三方上游 Key --- .../service/channel_monitor_checker.go | 4 +++- .../channel_monitor_checker_body_test.go | 11 ++++++++++ backend/internal/util/logredact/redact.go | 3 +++ .../internal/util/logredact/redact_test.go | 22 +++++++++++++++++++ .../components/account/CreateAccountModal.vue | 2 +- .../components/account/EditAccountModal.vue | 2 +- .../src/i18n/locales/en/admin/accounts.ts | 2 +- .../src/i18n/locales/zh/admin/accounts.ts | 2 +- 8 files changed, 43 insertions(+), 5 deletions(-) diff --git a/backend/internal/service/channel_monitor_checker.go b/backend/internal/service/channel_monitor_checker.go index ddb5e78317..c8b4fdf069 100644 --- a/backend/internal/service/channel_monitor_checker.go +++ b/backend/internal/service/channel_monitor_checker.go @@ -636,6 +636,8 @@ var monitorAPIKeyPatterns = []struct { {regexp.MustCompile(`xai-[A-Za-z0-9_-]{6,}`), "xai-***REDACTED***"}, // Gemini / Google API Key:固定前缀 + 35 位 {regexp.MustCompile(`AIza[A-Za-z0-9_-]{35}`), "AIza***REDACTED***"}, + // Gemini auth key:AQ.Ab 前缀 + 48 位,长度放宽以覆盖格式变体 + {regexp.MustCompile(`AQ\.Ab[A-Za-z0-9_-]{40,}`), "AQ.***REDACTED***"}, // JWT 三段式(Bearer 后常出现):eyJxxx.eyJxxx.signature {regexp.MustCompile(`eyJ[A-Za-z0-9_-]{8,}\.eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}`), "eyJ***REDACTED.JWT***"}, } @@ -643,7 +645,7 @@ var monitorAPIKeyPatterns = []struct { // sanitizeErrorMessage 擦除错误/响应文本中可能泄露的 API key。 // 处理两类来源: // 1. URL query 中的 ?key= / ?api_key= 等(Go *url.Error 会回填完整 URL) -// 2. 上游 HTTP body 文本里直接出现的 sk-* / xai-* / AIza* / JWT 等密钥碎片 +// 2. 上游 HTTP body 文本里直接出现的 sk-* / xai-* / AIza* / AQ.* / JWT 等密钥碎片 // // 注意:与 gemini_messages_compat_service.go 的 sanitizeUpstreamErrorMessage 关注点类似但参数集更广, // 监控模块独立维护,避免互相耦合。 diff --git a/backend/internal/service/channel_monitor_checker_body_test.go b/backend/internal/service/channel_monitor_checker_body_test.go index 922eb86732..5c347cae89 100644 --- a/backend/internal/service/channel_monitor_checker_body_test.go +++ b/backend/internal/service/channel_monitor_checker_body_test.go @@ -279,6 +279,17 @@ func TestRunCheckForModel_Grok_RedactsXAIKeyFromUpstreamBody(t *testing.T) { } } +func TestSanitizeErrorMessage_RedactsGeminiAuthKey(t *testing.T) { + authKey := "AQ." + "Ab" + strings.Repeat("Xy9_-z", 8) + got := sanitizeErrorMessage(`{"error":{"message":"API key not valid: ` + authKey + `"}}`) + if strings.Contains(got, authKey) { + t.Fatalf("Gemini auth key leaked: %q", got) + } + if !strings.Contains(got, "AQ.***REDACTED***") { + t.Fatalf("expected redaction marker, got %q", got) + } +} + func TestRunCheckForModel_OpenAIResponses_DefaultRequest(t *testing.T) { h := &openAICaptureHandler{} endpoint := setupFakeOpenAI(t, h) diff --git a/backend/internal/util/logredact/redact.go b/backend/internal/util/logredact/redact.go index 9249b761c7..8be6448a18 100644 --- a/backend/internal/util/logredact/redact.go +++ b/backend/internal/util/logredact/redact.go @@ -42,6 +42,8 @@ type textRedactPatterns struct { var ( reGOCSPX = regexp.MustCompile(`GOCSPX-[0-9A-Za-z_-]{24,}`) reAIza = regexp.MustCompile(`AIza[0-9A-Za-z_-]{35}`) + // Gemini auth key:AQ.Ab 前缀 + 48 位,长度放宽以覆盖格式变体。 + reGeminiAuthKey = regexp.MustCompile(`AQ\.Ab[0-9A-Za-z_-]{40,}`) defaultTextRedactPatterns = compileTextRedactPatterns(nil) extraTextPatternCache sync.Map // map[string]*textRedactPatterns @@ -99,6 +101,7 @@ func RedactText(input string, extraKeys ...string) string { out := input out = reGOCSPX.ReplaceAllString(out, "GOCSPX-***") out = reAIza.ReplaceAllString(out, "AIza***") + out = reGeminiAuthKey.ReplaceAllString(out, "AQ.***") out = patterns.reJSONLike.ReplaceAllString(out, `$1***$3`) out = patterns.reQueryLike.ReplaceAllString(out, `$1=***`) out = patterns.rePlain.ReplaceAllString(out, `$1$2***`) diff --git a/backend/internal/util/logredact/redact_test.go b/backend/internal/util/logredact/redact_test.go index 266db69dbd..e615f33e0e 100644 --- a/backend/internal/util/logredact/redact_test.go +++ b/backend/internal/util/logredact/redact_test.go @@ -38,6 +38,28 @@ func TestRedactText_GOCSPX(t *testing.T) { } } +func TestRedactText_GeminiAPIKeys(t *testing.T) { + legacyKey := "AI" + "za" + strings.Repeat("Ab1_-", 7) + authKey := "AQ." + "Ab" + strings.Repeat("Xy9_-z", 8) + in := "upstream rejected " + legacyKey + " and " + authKey + " for model gemini-3.1-pro" + out := RedactText(in) + if strings.Contains(out, legacyKey) || strings.Contains(out, authKey) { + t.Fatalf("expected Gemini keys redacted, got %q", out) + } + for _, want := range []string{"AIza***", "AQ.***", "for model gemini-3.1-pro"} { + if !strings.Contains(out, want) { + t.Fatalf("expected %q in %q", want, out) + } + } +} + +func TestRedactText_GeminiAuthKeyPrefixAloneUnchanged(t *testing.T) { + in := "key format AQ.Ab... is documented" + if out := RedactText(in); out != in { + t.Fatalf("expected short AQ. prefix unchanged, got %q", out) + } +} + func TestRedactText_ExtraKeyCacheUsesNormalizedSortedKey(t *testing.T) { clearExtraTextPatternCache() diff --git a/frontend/src/components/account/CreateAccountModal.vue b/frontend/src/components/account/CreateAccountModal.vue index 2dccc59363..315feae303 100644 --- a/frontend/src/components/account/CreateAccountModal.vue +++ b/frontend/src/components/account/CreateAccountModal.vue @@ -4253,7 +4253,7 @@ const apiKeyValuePlaceholder = computed(() => { case 'openai': return 'sk-proj-...' case 'gemini': - return 'AIza...' + return 'AQ.Ab...' case 'grok': return 'xai-...' case 'kimi': diff --git a/frontend/src/components/account/EditAccountModal.vue b/frontend/src/components/account/EditAccountModal.vue index 1fdd3727f2..9bc4f9167a 100644 --- a/frontend/src/components/account/EditAccountModal.vue +++ b/frontend/src/components/account/EditAccountModal.vue @@ -239,7 +239,7 @@ account.platform === 'openai' ? 'sk-proj-...' : account.platform === 'gemini' - ? 'AIza...' + ? 'AQ.Ab...' : account.platform === 'antigravity' ? 'sk-...' : account.platform === 'grok' diff --git a/frontend/src/i18n/locales/en/admin/accounts.ts b/frontend/src/i18n/locales/en/admin/accounts.ts index 317e8ec933..bb6fa68552 100644 --- a/frontend/src/i18n/locales/en/admin/accounts.ts +++ b/frontend/src/i18n/locales/en/admin/accounts.ts @@ -1422,7 +1422,7 @@ export default { modelPassthroughDesc: 'All model requests are forwarded directly to the Gemini API without model restrictions or mappings.', baseUrlHint: 'Leave default for official Gemini API', - apiKeyHint: 'Your Gemini API Key (starts with AIza)', + apiKeyHint: 'New AI Studio keys start with AQ. (legacy keys start with AIza); for a third-party upstream, use the key it provides', tier: { label: 'Account Tier', hint: 'Tip: The system will try to auto-detect the tier first; if auto-detection is unavailable or fails, your selected tier is used as a fallback (simulated quota).', diff --git a/frontend/src/i18n/locales/zh/admin/accounts.ts b/frontend/src/i18n/locales/zh/admin/accounts.ts index 3e9c06fefa..73a2f3f21b 100644 --- a/frontend/src/i18n/locales/zh/admin/accounts.ts +++ b/frontend/src/i18n/locales/zh/admin/accounts.ts @@ -1513,7 +1513,7 @@ export default { modelPassthrough: 'Gemini 直接转发模型', modelPassthroughDesc: '所有模型请求将直接转发至 Gemini API,不进行模型限制或映射。', baseUrlHint: '留空使用官方 Gemini API', - apiKeyHint: '您的 Gemini API Key(以 AIza 开头)', + apiKeyHint: 'AI Studio 新建的 Key 以 AQ. 开头(旧版以 AIza 开头);第三方上游填写其提供的 Key', tier: { label: '账号等级', hint: '提示:系统会优先尝试自动识别账号等级;若自动识别不可用或失败,则使用你选择的等级作为回退(本地模拟配额)。',