适用场景:在 Claude Code 中使用 https://github.com/openai/codex-plugin-cc 插件时,插件经官方 codex app-server 以 clientInfo.name="Claude Code" 完成 initialize 握手,请求头被设为 originator=Claude Code、User-Agent 含 "Claude Code/",不在官方客户端白名单内,原本会被 codex_cli_only 拦截 403。 在官方客户端白名单未命中时评估两层独立放行(OR 语义): - 按账号:account.Extra.codex_cli_only_allowed_clients 引用命名预设 (目前仅 claude_code),detector reason=allowed_client_matched - 全局开关:/admin/settings 网关服务 OpenAI 区块新增 openai_allow_claude_code_codex_plugin(默认 false),开启后对所有 codex_cli_only 账号统一放行,detector reason=global_allowed_client_matched 签名仍要求 originator=Claude Code 精确等值 + UA 含 "Claude Code/"。 上游转发保持透传不变。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
111 lines
4.1 KiB
Go
111 lines
4.1 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"
|
|
// CodexClientRestrictionReasonMatchedAllowedClient 表示请求命中账号级额外放行的命名客户端预设。
|
|
CodexClientRestrictionReasonMatchedAllowedClient = "allowed_client_matched"
|
|
// CodexClientRestrictionReasonMatchedGlobalAllowedClient 表示请求命中全局额外放行的命名客户端预设。
|
|
CodexClientRestrictionReasonMatchedGlobalAllowedClient = "global_allowed_client_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, globalAllowedClients []string) 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, globalAllowedClients []string) 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,
|
|
}
|
|
}
|
|
|
|
// 官方客户端白名单未命中时,先尝试账号级额外放行的命名客户端预设(如 Claude Code codex 插件)。
|
|
if allowed := account.GetCodexCLIOnlyAllowedClients(); len(allowed) > 0 &&
|
|
openai.MatchAllowedClients(userAgent, originator, allowed) {
|
|
return CodexClientRestrictionDetectionResult{
|
|
Enabled: true,
|
|
Matched: true,
|
|
Reason: CodexClientRestrictionReasonMatchedAllowedClient,
|
|
}
|
|
}
|
|
|
|
// 再尝试由更高作用域(全局设置)注入的额外放行客户端列表。
|
|
if len(globalAllowedClients) > 0 &&
|
|
openai.MatchAllowedClients(userAgent, originator, globalAllowedClients) {
|
|
return CodexClientRestrictionDetectionResult{
|
|
Enabled: true,
|
|
Matched: true,
|
|
Reason: CodexClientRestrictionReasonMatchedGlobalAllowedClient,
|
|
}
|
|
}
|
|
|
|
return CodexClientRestrictionDetectionResult{
|
|
Enabled: true,
|
|
Matched: false,
|
|
Reason: CodexClientRestrictionReasonNotMatchedUA,
|
|
}
|
|
}
|