diff --git a/backend/internal/handler/admin/setting_handler.go b/backend/internal/handler/admin/setting_handler.go index 9907d441..f73e4486 100644 --- a/backend/internal/handler/admin/setting_handler.go +++ b/backend/internal/handler/admin/setting_handler.go @@ -247,6 +247,7 @@ func (h *SettingHandler) GetSettings(c *gin.Context) { EnableAnthropicCacheTTL1hInjection: settings.EnableAnthropicCacheTTL1hInjection, RewriteMessageCacheControl: settings.RewriteMessageCacheControl, AntigravityUserAgentVersion: settings.AntigravityUserAgentVersion, + OpenAICodexUserAgent: settings.OpenAICodexUserAgent, WebSearchEmulationEnabled: settings.WebSearchEmulationEnabled, PaymentVisibleMethodAlipaySource: settings.PaymentVisibleMethodAlipaySource, PaymentVisibleMethodWxpaySource: settings.PaymentVisibleMethodWxpaySource, @@ -563,6 +564,7 @@ type UpdateSettingsRequest struct { EnableAnthropicCacheTTL1hInjection *bool `json:"enable_anthropic_cache_ttl_1h_injection"` RewriteMessageCacheControl *bool `json:"rewrite_message_cache_control"` AntigravityUserAgentVersion *string `json:"antigravity_user_agent_version"` + OpenAICodexUserAgent *string `json:"openai_codex_user_agent"` // Payment visible method routing PaymentVisibleMethodAlipaySource *string `json:"payment_visible_method_alipay_source"` @@ -1404,6 +1406,15 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) { return } } + if req.OpenAICodexUserAgent != nil { + normalized := strings.TrimSpace(*req.OpenAICodexUserAgent) + req.OpenAICodexUserAgent = &normalized + // 仅做长度上限保护,不限制具体格式(运维需要可自由调整 codex 版本号) + if len(normalized) > 512 { + response.Error(c, http.StatusBadRequest, "openai_codex_user_agent must be at most 512 characters") + return + } + } // 交叉验证:如果同时设置了最低和最高版本号,最高版本号必须 >= 最低版本号 if req.MinClaudeCodeVersion != "" && req.MaxClaudeCodeVersion != "" { @@ -1597,6 +1608,12 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) { } return previousSettings.AntigravityUserAgentVersion }(), + OpenAICodexUserAgent: func() string { + if req.OpenAICodexUserAgent != nil { + return *req.OpenAICodexUserAgent + } + return previousSettings.OpenAICodexUserAgent + }(), PaymentVisibleMethodAlipaySource: func() string { if req.PaymentVisibleMethodAlipaySource != nil { return strings.TrimSpace(*req.PaymentVisibleMethodAlipaySource) @@ -1956,6 +1973,7 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) { EnableAnthropicCacheTTL1hInjection: updatedSettings.EnableAnthropicCacheTTL1hInjection, RewriteMessageCacheControl: updatedSettings.RewriteMessageCacheControl, AntigravityUserAgentVersion: updatedSettings.AntigravityUserAgentVersion, + OpenAICodexUserAgent: updatedSettings.OpenAICodexUserAgent, PaymentVisibleMethodAlipaySource: updatedSettings.PaymentVisibleMethodAlipaySource, PaymentVisibleMethodWxpaySource: updatedSettings.PaymentVisibleMethodWxpaySource, PaymentVisibleMethodAlipayEnabled: updatedSettings.PaymentVisibleMethodAlipayEnabled, @@ -2411,6 +2429,9 @@ func diffSettings(before *service.SystemSettings, after *service.SystemSettings, if before.AntigravityUserAgentVersion != after.AntigravityUserAgentVersion { changed = append(changed, "antigravity_user_agent_version") } + if before.OpenAICodexUserAgent != after.OpenAICodexUserAgent { + changed = append(changed, "openai_codex_user_agent") + } if before.PaymentVisibleMethodAlipaySource != after.PaymentVisibleMethodAlipaySource { changed = append(changed, "payment_visible_method_alipay_source") } diff --git a/backend/internal/handler/dto/settings.go b/backend/internal/handler/dto/settings.go index 45ad7a70..fd3a90b3 100644 --- a/backend/internal/handler/dto/settings.go +++ b/backend/internal/handler/dto/settings.go @@ -181,6 +181,7 @@ type SystemSettings struct { EnableAnthropicCacheTTL1hInjection bool `json:"enable_anthropic_cache_ttl_1h_injection"` RewriteMessageCacheControl bool `json:"rewrite_message_cache_control"` AntigravityUserAgentVersion string `json:"antigravity_user_agent_version"` + OpenAICodexUserAgent string `json:"openai_codex_user_agent"` // Web Search Emulation WebSearchEmulationEnabled bool `json:"web_search_emulation_enabled"` diff --git a/backend/internal/pkg/openai/request.go b/backend/internal/pkg/openai/request.go index dd8fe566..ae3886d6 100644 --- a/backend/internal/pkg/openai/request.go +++ b/backend/internal/pkg/openai/request.go @@ -30,6 +30,17 @@ var CodexOfficialClientOriginatorPrefixes = []string{ "codex ", } +// IsBrowserUserAgent 判断 User-Agent 是否来自浏览器(Chrome/Firefox/Safari/Edge/Opera 等)。 +// 所有现代浏览器的 UA 均以 "Mozilla/" 作为前缀,CLI 工具(codex/claude/curl/postman/python-requests 等)不会。 +// 该判定用于避免 Cloudflare 对浏览器型 UA 在 OpenAI 上游接口上触发 JS 质询。 +func IsBrowserUserAgent(userAgent string) bool { + ua := strings.TrimSpace(userAgent) + if ua == "" { + return false + } + return strings.HasPrefix(strings.ToLower(ua), "mozilla/") +} + // IsCodexCLIRequest checks if the User-Agent indicates a Codex CLI request func IsCodexCLIRequest(userAgent string) bool { ua := normalizeCodexClientHeader(userAgent) diff --git a/backend/internal/service/domain_constants.go b/backend/internal/service/domain_constants.go index f39c5d7e..e697f459 100644 --- a/backend/internal/service/domain_constants.go +++ b/backend/internal/service/domain_constants.go @@ -400,6 +400,10 @@ const ( SettingKeyRewriteMessageCacheControl = "rewrite_message_cache_control" // SettingKeyAntigravityUserAgentVersion Antigravity 上游 User-Agent 版本号(空值使用环境变量/默认值) SettingKeyAntigravityUserAgentVersion = "antigravity_user_agent_version" + // SettingKeyOpenAICodexUserAgent OpenAI Codex 完整 User-Agent(空值使用内置默认) + // 当客户端 UA 被识别为浏览器(Chrome/Firefox/Safari/Edge 等)时,转发给 OpenAI 上游前会替换为此值, + // 用于避免 Cloudflare 对浏览器型 UA 的质询拦截。 + SettingKeyOpenAICodexUserAgent = "openai_codex_user_agent" // Balance Low Notification SettingKeyBalanceLowNotifyEnabled = "balance_low_notify_enabled" // 全局开关 diff --git a/backend/internal/service/openai_gateway_service.go b/backend/internal/service/openai_gateway_service.go index cfaf5bff..bf3318a3 100644 --- a/backend/internal/service/openai_gateway_service.go +++ b/backend/internal/service/openai_gateway_service.go @@ -3231,6 +3231,10 @@ func (s *OpenAIGatewayService) buildUpstreamRequestOpenAIPassthrough( req.Header.Set("user-agent", codexCLIUserAgent) } + // 浏览器型 UA 兜底:仅 OAuth(ChatGPT 内部接口)账号生效,若最终 user-agent 仍为浏览器 + // (Chrome/Firefox/Safari/Edge 等),替换为后台配置的 Codex UA,避免 Cloudflare 触发 JS 质询。 + s.overrideBrowserUserAgent(ctx, account, req) + if req.Header.Get("content-type") == "" { req.Header.Set("content-type", "application/json") } @@ -3947,6 +3951,10 @@ func (s *OpenAIGatewayService) buildUpstreamRequest(ctx context.Context, c *gin. req.Header.Set("user-agent", codexCLIUserAgent) } + // 浏览器型 UA 兜底:仅 OAuth(ChatGPT 内部接口)账号生效,若最终 user-agent 仍为浏览器 + // (Chrome/Firefox/Safari/Edge 等),替换为后台配置的 Codex UA,避免 Cloudflare 触发 JS 质询。 + s.overrideBrowserUserAgent(ctx, account, req) + // Ensure required headers exist if req.Header.Get("content-type") == "" { req.Header.Set("content-type", "application/json") @@ -3955,6 +3963,30 @@ func (s *OpenAIGatewayService) buildUpstreamRequest(ctx context.Context, c *gin. return req, nil } +// overrideBrowserUserAgent 检查请求的最终 user-agent,若为浏览器 UA 则替换为后台配置的 Codex UA。 +// 用于规避 Cloudflare 对浏览器型 UA 在 ChatGPT 内部接口上的访问质询。 +// 影响范围严格限定:仅 OAuth(Codex/ChatGPT 内部接口)账号生效;API Key 等其他账号原样透传。 +// 仅在识别为浏览器(Mozilla/...)时改写,其他 CLI/工具 UA 不动。 +func (s *OpenAIGatewayService) overrideBrowserUserAgent(ctx context.Context, account *Account, req *http.Request) { + if req == nil || account == nil { + return + } + if account.Type != AccountTypeOAuth { + return + } + currentUA := req.Header.Get("user-agent") + if !openai.IsBrowserUserAgent(currentUA) { + return + } + codexUA := DefaultOpenAICodexUserAgent + if s != nil && s.settingService != nil { + if v := strings.TrimSpace(s.settingService.GetOpenAICodexUserAgent(ctx)); v != "" { + codexUA = v + } + } + req.Header.Set("user-agent", codexUA) +} + func (s *OpenAIGatewayService) handleErrorResponse( ctx context.Context, resp *http.Response, diff --git a/backend/internal/service/setting_service.go b/backend/internal/service/setting_service.go index a5c16b1f..bd99e341 100644 --- a/backend/internal/service/setting_service.go +++ b/backend/internal/service/setting_service.go @@ -128,6 +128,19 @@ const antigravityUserAgentVersionCacheTTL = 60 * time.Second const antigravityUserAgentVersionErrorTTL = 5 * time.Second const antigravityUserAgentVersionDBTimeout = 5 * time.Second +// DefaultOpenAICodexUserAgent OpenAI Codex 默认 User-Agent(用于规避 Cloudflare 对浏览器 UA 的质询) +const DefaultOpenAICodexUserAgent = "codex-tui/0.125.0 (Ubuntu 22.4.0; x86_64) xterm-256color (codex-tui; 0.125.0)" + +// cachedOpenAICodexUserAgent 缓存 OpenAI Codex UA(进程内缓存,60s TTL) +type cachedOpenAICodexUserAgent struct { + value string + expiresAt int64 // unix nano +} + +const openAICodexUserAgentCacheTTL = 60 * time.Second +const openAICodexUserAgentErrorTTL = 5 * time.Second +const openAICodexUserAgentDBTimeout = 5 * time.Second + // DefaultSubscriptionGroupReader validates group references used by default subscriptions. type DefaultSubscriptionGroupReader interface { GetByID(ctx context.Context, id int64) (*Group, error) @@ -148,6 +161,8 @@ type SettingService struct { webSearchManagerBuilder WebSearchManagerBuilder antigravityUAVersionCache atomic.Value // *cachedAntigravityUserAgentVersion antigravityUAVersionSF singleflight.Group + openAICodexUACache atomic.Value // *cachedOpenAICodexUserAgent + openAICodexUASF singleflight.Group } type ProviderDefaultGrantSettings struct { @@ -907,6 +922,55 @@ func (s *SettingService) GetAntigravityUserAgentVersion(ctx context.Context) str return fallback } +// GetOpenAICodexUserAgent 返回 OpenAI Codex 上游请求使用的 User-Agent。 +// 后台设置优先;为空时回退到内置默认值。 +func (s *SettingService) GetOpenAICodexUserAgent(ctx context.Context) string { + fallback := DefaultOpenAICodexUserAgent + if s == nil || s.settingRepo == nil { + return fallback + } + if cached, ok := s.openAICodexUACache.Load().(*cachedOpenAICodexUserAgent); ok && cached != nil { + if time.Now().UnixNano() < cached.expiresAt { + return cached.value + } + } + + result, _, _ := s.openAICodexUASF.Do("openai_codex_user_agent", func() (any, error) { + if cached, ok := s.openAICodexUACache.Load().(*cachedOpenAICodexUserAgent); ok && cached != nil { + if time.Now().UnixNano() < cached.expiresAt { + return cached.value, nil + } + } + if ctx == nil { + ctx = context.Background() + } + dbCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), openAICodexUserAgentDBTimeout) + defer cancel() + value, err := s.settingRepo.GetValue(dbCtx, SettingKeyOpenAICodexUserAgent) + if err != nil && !errors.Is(err, ErrSettingNotFound) { + slog.Warn("failed to get openai codex user agent setting", "error", err) + s.openAICodexUACache.Store(&cachedOpenAICodexUserAgent{ + value: fallback, + expiresAt: time.Now().Add(openAICodexUserAgentErrorTTL).UnixNano(), + }) + return fallback, nil + } + ua := strings.TrimSpace(value) + if ua == "" { + ua = fallback + } + s.openAICodexUACache.Store(&cachedOpenAICodexUserAgent{ + value: ua, + expiresAt: time.Now().Add(openAICodexUserAgentCacheTTL).UnixNano(), + }) + return ua, nil + }) + if ua, ok := result.(string); ok && ua != "" { + return ua + } + return fallback +} + // SetOnUpdateCallback sets a callback function to be called when settings are updated // This is used for cache invalidation (e.g., HTML cache in frontend server) func (s *SettingService) SetOnUpdateCallback(callback func()) { @@ -1706,6 +1770,7 @@ func (s *SettingService) buildSystemSettingsUpdates(ctx context.Context, setting updates[SettingKeyEnableAnthropicCacheTTL1hInjection] = strconv.FormatBool(settings.EnableAnthropicCacheTTL1hInjection) updates[SettingKeyRewriteMessageCacheControl] = strconv.FormatBool(settings.RewriteMessageCacheControl) updates[SettingKeyAntigravityUserAgentVersion] = antigravity.NormalizeUserAgentVersion(settings.AntigravityUserAgentVersion) + updates[SettingKeyOpenAICodexUserAgent] = strings.TrimSpace(settings.OpenAICodexUserAgent) updates[SettingPaymentVisibleMethodAlipaySource] = settings.PaymentVisibleMethodAlipaySource updates[SettingPaymentVisibleMethodWxpaySource] = settings.PaymentVisibleMethodWxpaySource updates[SettingPaymentVisibleMethodAlipayEnabled] = strconv.FormatBool(settings.PaymentVisibleMethodAlipayEnabled) @@ -1788,6 +1853,15 @@ func (s *SettingService) refreshCachedSettings(settings *SystemSettings) { version: antigravityUserAgentVersion, expiresAt: time.Now().Add(antigravityUserAgentVersionCacheTTL).UnixNano(), }) + s.openAICodexUASF.Forget("openai_codex_user_agent") + codexUA := strings.TrimSpace(settings.OpenAICodexUserAgent) + if codexUA == "" { + codexUA = DefaultOpenAICodexUserAgent + } + s.openAICodexUACache.Store(&cachedOpenAICodexUserAgent{ + value: codexUA, + expiresAt: time.Now().Add(openAICodexUserAgentCacheTTL).UnixNano(), + }) openAIAdvancedSchedulerSettingSF.Forget(openAIAdvancedSchedulerSettingKey) openAIAdvancedSchedulerSettingCache.Store(&cachedOpenAIAdvancedSchedulerSetting{ enabled: settings.OpenAIAdvancedSchedulerEnabled, @@ -2529,6 +2603,7 @@ func (s *SettingService) InitializeDefaultSettings(ctx context.Context) error { SettingKeyEnableAnthropicCacheTTL1hInjection: "false", SettingKeyRewriteMessageCacheControl: strconv.FormatBool(s.defaultRewriteMessageCacheControl()), SettingKeyAntigravityUserAgentVersion: "", + SettingKeyOpenAICodexUserAgent: "", SettingPaymentVisibleMethodAlipaySource: "", SettingPaymentVisibleMethodWxpaySource: "", SettingPaymentVisibleMethodAlipayEnabled: "false", @@ -3041,6 +3116,7 @@ func (s *SettingService) parseSettings(settings map[string]string) *SystemSettin result.RewriteMessageCacheControl = s.defaultRewriteMessageCacheControl() } result.AntigravityUserAgentVersion = antigravity.NormalizeUserAgentVersion(settings[SettingKeyAntigravityUserAgentVersion]) + result.OpenAICodexUserAgent = strings.TrimSpace(settings[SettingKeyOpenAICodexUserAgent]) // Web search emulation: quick enabled check from the JSON config if raw := settings[SettingKeyWebSearchEmulationConfig]; raw != "" { diff --git a/backend/internal/service/settings_view.go b/backend/internal/service/settings_view.go index ea5fa57c..1e5e8b1c 100644 --- a/backend/internal/service/settings_view.go +++ b/backend/internal/service/settings_view.go @@ -193,6 +193,7 @@ type SystemSettings struct { EnableAnthropicCacheTTL1hInjection bool // 是否对 Anthropic OAuth/SetupToken 请求体注入 1h cache_control ttl(默认 false) RewriteMessageCacheControl bool // 是否改写 messages[*].content[*].cache_control(默认 false) AntigravityUserAgentVersion string // Antigravity 上游 User-Agent 版本号;空值使用配置/默认值 + OpenAICodexUserAgent string // OpenAI Codex 上游完整 User-Agent;空值使用内置默认 // Web Search Emulation WebSearchEmulationEnabled bool // 是否启用 web search 模拟 diff --git a/frontend/src/api/admin/settings.ts b/frontend/src/api/admin/settings.ts index dfef451d..fd9e2b8d 100644 --- a/frontend/src/api/admin/settings.ts +++ b/frontend/src/api/admin/settings.ts @@ -504,6 +504,7 @@ export interface SystemSettings { enable_anthropic_cache_ttl_1h_injection: boolean; rewrite_message_cache_control: boolean; antigravity_user_agent_version: string; + openai_codex_user_agent: string; web_search_emulation_enabled?: boolean; // Payment configuration @@ -724,6 +725,7 @@ export interface UpdateSettingsRequest { enable_anthropic_cache_ttl_1h_injection?: boolean; rewrite_message_cache_control?: boolean; antigravity_user_agent_version?: string; + openai_codex_user_agent?: string; // Payment configuration payment_enabled?: boolean; risk_control_enabled?: boolean; diff --git a/frontend/src/i18n/locales/en.ts b/frontend/src/i18n/locales/en.ts index 9ac1466e..70f7724c 100644 --- a/frontend/src/i18n/locales/en.ts +++ b/frontend/src/i18n/locales/en.ts @@ -5454,6 +5454,9 @@ export default { antigravityUserAgentVersion: 'Antigravity UA Version', antigravityUserAgentVersionPlaceholder: '1.23.2', antigravityUserAgentVersionHint: 'Leave empty to use ANTIGRAVITY_USER_AGENT_VERSION or the built-in default 1.23.2; when set, the admin setting takes precedence.', + openaiCodexUserAgent: 'OpenAI Codex UA', + openaiCodexUserAgentPlaceholder: 'codex-tui/0.125.0 (Ubuntu 22.4.0; x86_64) xterm-256color (codex-tui; 0.125.0)', + openaiCodexUserAgentHint: 'Used to bypass Cloudflare browser-UA challenges on the OpenAI upstream. Only applies when the client User-Agent is detected as a browser (Mozilla/...). Leave empty to use the built-in default.', }, webSearchEmulation: { title: 'Web Search Emulation', diff --git a/frontend/src/i18n/locales/zh.ts b/frontend/src/i18n/locales/zh.ts index 3e90405a..3a9a64ff 100644 --- a/frontend/src/i18n/locales/zh.ts +++ b/frontend/src/i18n/locales/zh.ts @@ -5612,6 +5612,9 @@ export default { antigravityUserAgentVersion: 'Antigravity UA 版本', antigravityUserAgentVersionPlaceholder: '1.23.2', antigravityUserAgentVersionHint: '留空时使用 ANTIGRAVITY_USER_AGENT_VERSION 或内置默认值 1.23.2;填写后后台设置优先。', + openaiCodexUserAgent: 'OpenAI Codex UA', + openaiCodexUserAgentPlaceholder: 'codex-tui/0.125.0 (Ubuntu 22.4.0; x86_64) xterm-256color (codex-tui; 0.125.0)', + openaiCodexUserAgentHint: '用于规避 OpenAI 上游 Cloudflare 对浏览器 UA 的访问质询。仅在检测到客户端 User-Agent 为浏览器(Mozilla/...)时生效,其他客户端原样透传。留空使用内置默认值。', }, webSearchEmulation: { title: 'Web Search 模拟', diff --git a/frontend/src/views/admin/SettingsView.vue b/frontend/src/views/admin/SettingsView.vue index e0c3e1d4..118dd1a9 100644 --- a/frontend/src/views/admin/SettingsView.vue +++ b/frontend/src/views/admin/SettingsView.vue @@ -3769,6 +3769,36 @@ }}

+ + +
+ + +

+ {{ + t( + "admin.settings.gatewayForwarding.openaiCodexUserAgentHint", + ) + }} +

+
@@ -6942,6 +6972,7 @@ const form = reactive({ enable_anthropic_cache_ttl_1h_injection: false, rewrite_message_cache_control: false, antigravity_user_agent_version: "", + openai_codex_user_agent: "", // Balance & quota notification balance_low_notify_enabled: false, balance_low_notify_threshold: 0, @@ -8042,6 +8073,8 @@ async function saveSettings() { rewrite_message_cache_control: form.rewrite_message_cache_control, antigravity_user_agent_version: form.antigravity_user_agent_version?.trim() || "", + openai_codex_user_agent: + form.openai_codex_user_agent?.trim() || "", // Payment configuration payment_enabled: form.payment_enabled, risk_control_enabled: form.risk_control_enabled, diff --git a/frontend/src/views/admin/__tests__/SettingsView.spec.ts b/frontend/src/views/admin/__tests__/SettingsView.spec.ts index 275e38c5..0d4ab7d2 100644 --- a/frontend/src/views/admin/__tests__/SettingsView.spec.ts +++ b/frontend/src/views/admin/__tests__/SettingsView.spec.ts @@ -371,6 +371,7 @@ const baseSettingsResponse = { enable_anthropic_cache_ttl_1h_injection: false, rewrite_message_cache_control: false, antigravity_user_agent_version: "", + openai_codex_user_agent: "", payment_enabled: true, payment_min_amount: 1, payment_max_amount: 10000,