feat(openai-gateway): Codex OAuth 账号浏览器 UA 自动改写规避 Cloudflare

质询
This commit is contained in:
shaw 2026-05-20 14:33:51 +08:00
parent 91da815993
commit 878ad3b569
12 changed files with 188 additions and 0 deletions

View File

@ -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")
}

View File

@ -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"`

View File

@ -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)

View File

@ -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" // 全局开关

View File

@ -3231,6 +3231,10 @@ func (s *OpenAIGatewayService) buildUpstreamRequestOpenAIPassthrough(
req.Header.Set("user-agent", codexCLIUserAgent)
}
// 浏览器型 UA 兜底:仅 OAuthChatGPT 内部接口)账号生效,若最终 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 兜底:仅 OAuthChatGPT 内部接口)账号生效,若最终 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 内部接口上的访问质询。
// 影响范围严格限定:仅 OAuthCodex/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,

View File

@ -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 != "" {

View File

@ -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 模拟

View File

@ -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;

View File

@ -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',

View File

@ -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 模拟',

View File

@ -3769,6 +3769,36 @@
}}
</p>
</div>
<!-- OpenAI Codex UA -->
<div>
<label
class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
{{
t(
"admin.settings.gatewayForwarding.openaiCodexUserAgent",
)
}}
</label>
<input
v-model="form.openai_codex_user_agent"
type="text"
class="input w-full font-mono text-sm"
:placeholder="
t(
'admin.settings.gatewayForwarding.openaiCodexUserAgentPlaceholder',
)
"
/>
<p class="mt-1.5 text-xs text-gray-500 dark:text-gray-400">
{{
t(
"admin.settings.gatewayForwarding.openaiCodexUserAgentHint",
)
}}
</p>
</div>
</div>
</div>
<!-- Web Search Emulation -->
@ -6942,6 +6972,7 @@ const form = reactive<SettingsForm>({
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,

View File

@ -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,