Files
sub2api/backend/internal/service/openai_client_restriction_detector.go
yangjianbo f323174d07 fix(openai): 修复 codex_cli_only 误拦截并补充 codex 家族识别
- 为 codex_cli_only 增加 originator 判定通道,避免仅依赖 User-Agent 误拦截
- 扩展官方客户端家族标识,补充 codex_chatgpt_desktop 等常见前缀
- 新增并更新单元测试与网关透传回归测试,覆盖 UA 与 originator 组合场景

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 12:06:24 +08:00

87 lines
2.9 KiB
Go

package service
import (
"github.com/Wei-Shaw/sub2api/internal/config"
"github.com/Wei-Shaw/sub2api/internal/pkg/openai"
"github.com/gin-gonic/gin"
)
const (
// CodexClientRestrictionReasonDisabled 表示账号未开启 codex_cli_only。
CodexClientRestrictionReasonDisabled = "codex_cli_only_disabled"
// CodexClientRestrictionReasonMatchedUA 表示请求命中官方客户端 UA 白名单。
CodexClientRestrictionReasonMatchedUA = "official_client_user_agent_matched"
// CodexClientRestrictionReasonMatchedOriginator 表示请求命中官方客户端 originator 白名单。
CodexClientRestrictionReasonMatchedOriginator = "official_client_originator_matched"
// CodexClientRestrictionReasonNotMatchedUA 表示请求未命中官方客户端 UA 白名单。
CodexClientRestrictionReasonNotMatchedUA = "official_client_user_agent_not_matched"
// CodexClientRestrictionReasonForceCodexCLI 表示通过 ForceCodexCLI 配置兜底放行。
CodexClientRestrictionReasonForceCodexCLI = "force_codex_cli_enabled"
)
// CodexClientRestrictionDetectionResult 是 codex_cli_only 统一检测入口结果。
type CodexClientRestrictionDetectionResult struct {
Enabled bool
Matched bool
Reason string
}
// CodexClientRestrictionDetector 定义 codex_cli_only 统一检测入口。
type CodexClientRestrictionDetector interface {
Detect(c *gin.Context, account *Account) CodexClientRestrictionDetectionResult
}
// OpenAICodexClientRestrictionDetector 为 OpenAI OAuth codex_cli_only 的默认实现。
type OpenAICodexClientRestrictionDetector struct {
cfg *config.Config
}
func NewOpenAICodexClientRestrictionDetector(cfg *config.Config) *OpenAICodexClientRestrictionDetector {
return &OpenAICodexClientRestrictionDetector{cfg: cfg}
}
func (d *OpenAICodexClientRestrictionDetector) Detect(c *gin.Context, account *Account) CodexClientRestrictionDetectionResult {
if account == nil || !account.IsCodexCLIOnlyEnabled() {
return CodexClientRestrictionDetectionResult{
Enabled: false,
Matched: false,
Reason: CodexClientRestrictionReasonDisabled,
}
}
if d != nil && d.cfg != nil && d.cfg.Gateway.ForceCodexCLI {
return CodexClientRestrictionDetectionResult{
Enabled: true,
Matched: true,
Reason: CodexClientRestrictionReasonForceCodexCLI,
}
}
userAgent := ""
originator := ""
if c != nil {
userAgent = c.GetHeader("User-Agent")
originator = c.GetHeader("originator")
}
if openai.IsCodexOfficialClientRequest(userAgent) {
return CodexClientRestrictionDetectionResult{
Enabled: true,
Matched: true,
Reason: CodexClientRestrictionReasonMatchedUA,
}
}
if openai.IsCodexOfficialClientOriginator(originator) {
return CodexClientRestrictionDetectionResult{
Enabled: true,
Matched: true,
Reason: CodexClientRestrictionReasonMatchedOriginator,
}
}
return CodexClientRestrictionDetectionResult{
Enabled: true,
Matched: false,
Reason: CodexClientRestrictionReasonNotMatchedUA,
}
}