From d571f300e55744d082f2c764ad902a5d54dec8cb Mon Sep 17 00:00:00 2001 From: shaw Date: Thu, 26 Mar 2026 16:43:38 +0800 Subject: [PATCH] =?UTF-8?q?feat(rectifier):=20=E8=AF=B7=E6=B1=82=E6=95=B4?= =?UTF-8?q?=E6=B5=81=E5=99=A8=E5=A2=9E=E5=8A=A0=20API=20Key=20=E8=B4=A6?= =?UTF-8?q?=E5=8F=B7=E7=AD=BE=E5=90=8D=E6=95=B4=E6=B5=81=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增独立开关控制 API Key 账号的签名整流功能,支持配置自定义 匹配关键词以捕获不同格式的上游错误响应。 - 新增 apikey_signature_enabled 开关(默认关闭) - 新增 apikey_signature_patterns 自定义关键词配置 - 内置签名检测规则对 API Key 账号同样生效 - 自定义关键词对完整响应体做不区分大小写匹配 - 重试二阶段检测仅做模式匹配,不重复校验开关 - Handler 层校验关键词数量(≤50)和长度(≤500) - API 响应 nil patterns 统一序列化为空数组 - OAuth/SetupToken/Upstream/Bedrock 账号行为不变 --- .../internal/handler/admin/setting_handler.go | 42 +++++++++- backend/internal/handler/dto/settings.go | 8 +- backend/internal/service/gateway_service.go | 59 ++++++++++++- backend/internal/service/settings_view.go | 8 +- frontend/src/api/admin/settings.ts | 2 + frontend/src/i18n/locales/en.ts | 8 ++ frontend/src/i18n/locales/zh.ts | 8 ++ frontend/src/views/admin/SettingsView.vue | 83 ++++++++++++++++++- 8 files changed, 204 insertions(+), 14 deletions(-) diff --git a/backend/internal/handler/admin/setting_handler.go b/backend/internal/handler/admin/setting_handler.go index f57244fb..397526a7 100644 --- a/backend/internal/handler/admin/setting_handler.go +++ b/backend/internal/handler/admin/setting_handler.go @@ -1594,18 +1594,26 @@ func (h *SettingHandler) GetRectifierSettings(c *gin.Context) { return } + patterns := settings.APIKeySignaturePatterns + if patterns == nil { + patterns = []string{} + } response.Success(c, dto.RectifierSettings{ Enabled: settings.Enabled, ThinkingSignatureEnabled: settings.ThinkingSignatureEnabled, ThinkingBudgetEnabled: settings.ThinkingBudgetEnabled, + APIKeySignatureEnabled: settings.APIKeySignatureEnabled, + APIKeySignaturePatterns: patterns, }) } // UpdateRectifierSettingsRequest 更新整流器配置请求 type UpdateRectifierSettingsRequest struct { - Enabled bool `json:"enabled"` - ThinkingSignatureEnabled bool `json:"thinking_signature_enabled"` - ThinkingBudgetEnabled bool `json:"thinking_budget_enabled"` + Enabled bool `json:"enabled"` + ThinkingSignatureEnabled bool `json:"thinking_signature_enabled"` + ThinkingBudgetEnabled bool `json:"thinking_budget_enabled"` + APIKeySignatureEnabled bool `json:"apikey_signature_enabled"` + APIKeySignaturePatterns []string `json:"apikey_signature_patterns"` } // UpdateRectifierSettings 更新请求整流器配置 @@ -1617,10 +1625,32 @@ func (h *SettingHandler) UpdateRectifierSettings(c *gin.Context) { return } + // 校验并清理自定义匹配关键词 + const maxPatterns = 50 + const maxPatternLen = 500 + if len(req.APIKeySignaturePatterns) > maxPatterns { + response.BadRequest(c, "Too many signature patterns (max 50)") + return + } + var cleanedPatterns []string + for _, p := range req.APIKeySignaturePatterns { + p = strings.TrimSpace(p) + if p == "" { + continue + } + if len(p) > maxPatternLen { + response.BadRequest(c, "Signature pattern too long (max 500 characters)") + return + } + cleanedPatterns = append(cleanedPatterns, p) + } + settings := &service.RectifierSettings{ Enabled: req.Enabled, ThinkingSignatureEnabled: req.ThinkingSignatureEnabled, ThinkingBudgetEnabled: req.ThinkingBudgetEnabled, + APIKeySignatureEnabled: req.APIKeySignatureEnabled, + APIKeySignaturePatterns: cleanedPatterns, } if err := h.settingService.SetRectifierSettings(c.Request.Context(), settings); err != nil { @@ -1635,10 +1665,16 @@ func (h *SettingHandler) UpdateRectifierSettings(c *gin.Context) { return } + updatedPatterns := updatedSettings.APIKeySignaturePatterns + if updatedPatterns == nil { + updatedPatterns = []string{} + } response.Success(c, dto.RectifierSettings{ Enabled: updatedSettings.Enabled, ThinkingSignatureEnabled: updatedSettings.ThinkingSignatureEnabled, ThinkingBudgetEnabled: updatedSettings.ThinkingBudgetEnabled, + APIKeySignatureEnabled: updatedSettings.APIKeySignatureEnabled, + APIKeySignaturePatterns: updatedPatterns, }) } diff --git a/backend/internal/handler/dto/settings.go b/backend/internal/handler/dto/settings.go index 59d7f688..47bab091 100644 --- a/backend/internal/handler/dto/settings.go +++ b/backend/internal/handler/dto/settings.go @@ -188,9 +188,11 @@ type StreamTimeoutSettings struct { // RectifierSettings 请求整流器配置 DTO type RectifierSettings struct { - Enabled bool `json:"enabled"` - ThinkingSignatureEnabled bool `json:"thinking_signature_enabled"` - ThinkingBudgetEnabled bool `json:"thinking_budget_enabled"` + Enabled bool `json:"enabled"` + ThinkingSignatureEnabled bool `json:"thinking_signature_enabled"` + ThinkingBudgetEnabled bool `json:"thinking_budget_enabled"` + APIKeySignatureEnabled bool `json:"apikey_signature_enabled"` + APIKeySignaturePatterns []string `json:"apikey_signature_patterns"` } // BetaPolicyRule Beta 策略规则 DTO diff --git a/backend/internal/service/gateway_service.go b/backend/internal/service/gateway_service.go index ae66ae4a..5de6dcae 100644 --- a/backend/internal/service/gateway_service.go +++ b/backend/internal/service/gateway_service.go @@ -4188,7 +4188,7 @@ func (s *GatewayService) Forward(ctx context.Context, c *gin.Context, account *A if readErr == nil { _ = resp.Body.Close() - if s.isThinkingBlockSignatureError(respBody) && s.settingService.IsSignatureRectifierEnabled(ctx) { + if s.shouldRectifySignatureError(ctx, account, respBody) { appendOpsUpstreamError(c, OpsUpstreamErrorEvent{ Platform: account.Platform, AccountID: account.ID, @@ -4243,7 +4243,7 @@ func (s *GatewayService) Forward(ctx context.Context, c *gin.Context, account *A retryRespBody, retryReadErr := io.ReadAll(io.LimitReader(retryResp.Body, 2<<20)) _ = retryResp.Body.Close() - if retryReadErr == nil && retryResp.StatusCode == 400 && s.isThinkingBlockSignatureError(retryRespBody) { + if retryReadErr == nil && retryResp.StatusCode == 400 && s.isSignatureErrorPattern(ctx, account, retryRespBody) { appendOpsUpstreamError(c, OpsUpstreamErrorEvent{ Platform: account.Platform, AccountID: account.ID, @@ -6145,6 +6145,59 @@ func truncateForLog(b []byte, maxBytes int) string { return s } +// shouldRectifySignatureError 统一判断是否应触发签名整流(strip thinking blocks 并重试)。 +// 根据账号类型检查对应的开关和匹配模式。 +func (s *GatewayService) shouldRectifySignatureError(ctx context.Context, account *Account, respBody []byte) bool { + if account.Type == AccountTypeAPIKey { + // API Key 账号:独立开关,一次读取配置 + settings, err := s.settingService.GetRectifierSettings(ctx) + if err != nil || !settings.Enabled || !settings.APIKeySignatureEnabled { + return false + } + // 先检查内置模式(同 OAuth),再检查自定义关键词 + if s.isThinkingBlockSignatureError(respBody) { + return true + } + return matchSignaturePatterns(respBody, settings.APIKeySignaturePatterns) + } + // OAuth/SetupToken/Upstream/Bedrock 等:保持原有行为(内置模式 + 原开关) + return s.isThinkingBlockSignatureError(respBody) && s.settingService.IsSignatureRectifierEnabled(ctx) +} + +// isSignatureErrorPattern 仅做模式匹配,不检查开关。 +// 用于已进入重试流程后的二阶段检测(此时开关已在首次调用时验证过)。 +func (s *GatewayService) isSignatureErrorPattern(ctx context.Context, account *Account, respBody []byte) bool { + if s.isThinkingBlockSignatureError(respBody) { + return true + } + if account.Type == AccountTypeAPIKey { + settings, err := s.settingService.GetRectifierSettings(ctx) + if err != nil { + return false + } + return matchSignaturePatterns(respBody, settings.APIKeySignaturePatterns) + } + return false +} + +// matchSignaturePatterns 检查响应体是否匹配自定义关键词列表(不区分大小写)。 +func matchSignaturePatterns(respBody []byte, patterns []string) bool { + if len(patterns) == 0 { + return false + } + bodyLower := strings.ToLower(string(respBody)) + for _, p := range patterns { + p = strings.TrimSpace(p) + if p == "" { + continue + } + if strings.Contains(bodyLower, strings.ToLower(p)) { + return true + } + } + return false +} + // isThinkingBlockSignatureError 检测是否是thinking block相关错误 // 这类错误可以通过过滤thinking blocks并重试来解决 func (s *GatewayService) isThinkingBlockSignatureError(respBody []byte) bool { @@ -8013,7 +8066,7 @@ func (s *GatewayService) ForwardCountTokens(ctx context.Context, c *gin.Context, } // 检测 thinking block 签名错误(400)并重试一次(过滤 thinking blocks) - if resp.StatusCode == 400 && s.isThinkingBlockSignatureError(respBody) && s.settingService.IsSignatureRectifierEnabled(ctx) { + if resp.StatusCode == 400 && s.shouldRectifySignatureError(ctx, account, respBody) { logger.LegacyPrintf("service.gateway", "Account %d: detected thinking block signature error on count_tokens, retrying with filtered thinking blocks", account.ID) filteredBody := FilterThinkingBlocksForRetry(body) diff --git a/backend/internal/service/settings_view.go b/backend/internal/service/settings_view.go index 4e29dba5..411939bb 100644 --- a/backend/internal/service/settings_view.go +++ b/backend/internal/service/settings_view.go @@ -190,9 +190,11 @@ func DefaultStreamTimeoutSettings() *StreamTimeoutSettings { // RectifierSettings 请求整流器配置 type RectifierSettings struct { - Enabled bool `json:"enabled"` // 总开关 - ThinkingSignatureEnabled bool `json:"thinking_signature_enabled"` // Thinking 签名整流 - ThinkingBudgetEnabled bool `json:"thinking_budget_enabled"` // Thinking Budget 整流 + Enabled bool `json:"enabled"` // 总开关 + ThinkingSignatureEnabled bool `json:"thinking_signature_enabled"` // Thinking 签名整流 + ThinkingBudgetEnabled bool `json:"thinking_budget_enabled"` // Thinking Budget 整流 + APIKeySignatureEnabled bool `json:"apikey_signature_enabled"` // API Key 签名整流开关 + APIKeySignaturePatterns []string `json:"apikey_signature_patterns"` // API Key 自定义匹配关键词 } // DefaultRectifierSettings 返回默认的整流器配置(全部启用) diff --git a/frontend/src/api/admin/settings.ts b/frontend/src/api/admin/settings.ts index 196e3788..cabdd5aa 100644 --- a/frontend/src/api/admin/settings.ts +++ b/frontend/src/api/admin/settings.ts @@ -323,6 +323,8 @@ export interface RectifierSettings { enabled: boolean thinking_signature_enabled: boolean thinking_budget_enabled: boolean + apikey_signature_enabled: boolean + apikey_signature_patterns: string[] } /** diff --git a/frontend/src/i18n/locales/en.ts b/frontend/src/i18n/locales/en.ts index 42f58a77..54d757bb 100644 --- a/frontend/src/i18n/locales/en.ts +++ b/frontend/src/i18n/locales/en.ts @@ -4473,6 +4473,14 @@ export default { thinkingSignatureHint: 'Automatically strip signatures and retry when upstream returns thinking block signature validation errors', thinkingBudget: 'Thinking Budget Rectifier', thinkingBudgetHint: 'Automatically set budget to 32000 and retry when upstream returns budget_tokens constraint error (≥1024)', + apikeySignature: 'API Key Signature Rectifier', + apikeySignatureHint: + 'Automatically strip signatures and retry when API Key accounts receive signature-related errors (built-in patterns always apply)', + apikeyPatterns: 'Custom Match Patterns', + apikeyPatternsHint: + 'Additional keywords matched against the response body (case-insensitive). Built-in patterns always apply; use these for supplementary matching.', + apikeyPatternPlaceholder: 'e.g., thinking_error', + addPattern: 'Add Pattern', saved: 'Rectifier settings saved', saveFailed: 'Failed to save rectifier settings' }, diff --git a/frontend/src/i18n/locales/zh.ts b/frontend/src/i18n/locales/zh.ts index 7ca78373..ac75188d 100644 --- a/frontend/src/i18n/locales/zh.ts +++ b/frontend/src/i18n/locales/zh.ts @@ -4637,6 +4637,14 @@ export default { thinkingSignatureHint: '当上游返回 thinking block 签名校验错误时,自动去除签名并重试', thinkingBudget: 'Thinking Budget 整流', thinkingBudgetHint: '当上游返回 budget_tokens 约束错误(≥1024)时,自动将 budget 设为 32000 并重试', + apikeySignature: 'API Key 签名整流', + apikeySignatureHint: + '当 API Key 账号的上游返回签名相关错误时,自动去除签名并重试(内置规则始终生效)', + apikeyPatterns: '自定义匹配关键词', + apikeyPatternsHint: + '额外的关键词,匹配响应体中的内容(不区分大小写)。内置规则始终生效,此处用于补充额外匹配。', + apikeyPatternPlaceholder: '例如:thinking_error 或 签名无效', + addPattern: '添加关键词', saved: '整流器设置保存成功', saveFailed: '保存整流器设置失败' }, diff --git a/frontend/src/views/admin/SettingsView.vue b/frontend/src/views/admin/SettingsView.vue index 0e510aa9..198d484b 100644 --- a/frontend/src/views/admin/SettingsView.vue +++ b/frontend/src/views/admin/SettingsView.vue @@ -454,6 +454,72 @@ + + +
+
+ +

+ {{ t('admin.settings.rectifier.apikeySignatureHint') }} +

+
+ +
+ + +
+
+ +

+ {{ t('admin.settings.rectifier.apikeyPatternsHint') }} +

+
+
+ + +
+ +
@@ -2010,7 +2076,9 @@ const rectifierSaving = ref(false) const rectifierForm = reactive({ enabled: true, thinking_signature_enabled: true, - thinking_budget_enabled: true + thinking_budget_enabled: true, + apikey_signature_enabled: false, + apikey_signature_patterns: [] as string[] }) // Beta Policy 状态 @@ -2626,6 +2694,10 @@ async function loadRectifierSettings() { try { const settings = await adminAPI.settings.getRectifierSettings() Object.assign(rectifierForm, settings) + // 确保 patterns 是数组(旧数据可能为 null) + if (!Array.isArray(rectifierForm.apikey_signature_patterns)) { + rectifierForm.apikey_signature_patterns = [] + } } catch (error: any) { console.error('Failed to load rectifier settings:', error) } finally { @@ -2639,9 +2711,16 @@ async function saveRectifierSettings() { const updated = await adminAPI.settings.updateRectifierSettings({ enabled: rectifierForm.enabled, thinking_signature_enabled: rectifierForm.thinking_signature_enabled, - thinking_budget_enabled: rectifierForm.thinking_budget_enabled + thinking_budget_enabled: rectifierForm.thinking_budget_enabled, + apikey_signature_enabled: rectifierForm.apikey_signature_enabled, + apikey_signature_patterns: rectifierForm.apikey_signature_patterns.filter( + (p) => p.trim() !== '' + ) }) Object.assign(rectifierForm, updated) + if (!Array.isArray(rectifierForm.apikey_signature_patterns)) { + rectifierForm.apikey_signature_patterns = [] + } appStore.showSuccess(t('admin.settings.rectifier.saved')) } catch (error: any) { appStore.showError(