feat(openai): 增加 OAuth 账号 Codex 官方客户端限制开关

新增 codex_cli_only 开关并默认关闭,关闭时完全绕过限制逻辑。
在 OpenAI 网关引入统一检测入口,集中判定账号类型、开关与客户端族。
开启后仅放行 codex_cli_rs、codex_vscode、codex_app 客户端家族。
补充后端判定与网关分支测试,并在前端创建/编辑页增加开关配置与回显。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
yangjianbo
2026-02-12 22:32:59 +08:00
parent 2f190d812a
commit a9518cc5be
13 changed files with 671 additions and 4 deletions

View File

@@ -1603,6 +1603,36 @@
</div>
</div>
<!-- OpenAI OAuth Codex 官方客户端限制开关 -->
<div
v-if="form.platform === 'openai' && accountCategory === 'oauth-based'"
class="border-t border-gray-200 pt-4 dark:border-dark-600"
>
<div class="flex items-center justify-between">
<div>
<label class="input-label mb-0">{{ t('admin.accounts.openai.codexCLIOnly') }}</label>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
{{ t('admin.accounts.openai.codexCLIOnlyDesc') }}
</p>
</div>
<button
type="button"
@click="codexCLIOnlyEnabled = !codexCLIOnlyEnabled"
:class="[
'relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2',
codexCLIOnlyEnabled ? 'bg-primary-600' : 'bg-gray-200 dark:bg-dark-600'
]"
>
<span
:class="[
'pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out',
codexCLIOnlyEnabled ? 'translate-x-5' : 'translate-x-0'
]"
/>
</button>
</div>
</div>
<div>
<div class="flex items-center justify-between">
<div>
@@ -2185,6 +2215,7 @@ const customErrorCodeInput = ref<number | null>(null)
const interceptWarmupRequests = ref(false)
const autoPauseOnExpired = ref(true)
const openaiPassthroughEnabled = ref(false)
const codexCLIOnlyEnabled = ref(false)
const enableSoraOnOpenAIOAuth = ref(false) // OpenAI OAuth 时同时启用 Sora
const mixedScheduling = ref(false) // For antigravity accounts: enable mixed scheduling
const antigravityAccountType = ref<'oauth' | 'upstream'>('oauth') // For antigravity: oauth or upstream
@@ -2410,6 +2441,7 @@ watch(
}
if (newPlatform !== 'openai') {
openaiPassthroughEnabled.value = false
codexCLIOnlyEnabled.value = false
}
// Reset OAuth states
oauth.resetState()
@@ -2420,6 +2452,15 @@ watch(
)
// Gemini AI Studio OAuth availability (requires operator-configured OAuth client)
watch(
[accountCategory, () => form.platform],
([category, platform]) => {
if (platform === 'openai' && category !== 'oauth-based') {
codexCLIOnlyEnabled.value = false
}
}
)
watch(
[() => props.show, () => form.platform, accountCategory],
async ([show, platform, category]) => {
@@ -2665,6 +2706,7 @@ const resetForm = () => {
interceptWarmupRequests.value = false
autoPauseOnExpired.value = true
openaiPassthroughEnabled.value = false
codexCLIOnlyEnabled.value = false
enableSoraOnOpenAIOAuth.value = false
// Reset quota control state
windowCostEnabled.value = false
@@ -2695,7 +2737,7 @@ const handleClose = () => {
emit('close')
}
const buildOpenAIPassthroughExtra = (base?: Record<string, unknown>): Record<string, unknown> | undefined => {
const buildOpenAIExtra = (base?: Record<string, unknown>): Record<string, unknown> | undefined => {
if (form.platform !== 'openai') {
return base
}
@@ -2707,6 +2749,13 @@ const buildOpenAIPassthroughExtra = (base?: Record<string, unknown>): Record<str
delete extra.openai_passthrough
delete extra.openai_oauth_passthrough
}
if (accountCategory.value === 'oauth-based' && codexCLIOnlyEnabled.value) {
extra.codex_cli_only = true
} else {
delete extra.codex_cli_only
}
return Object.keys(extra).length > 0 ? extra : undefined
}
@@ -2863,7 +2912,7 @@ const handleSubmit = async () => {
}
form.credentials = credentials
const extra = buildOpenAIPassthroughExtra()
const extra = buildOpenAIExtra()
await doCreateAccount({
...form,
@@ -2949,7 +2998,7 @@ const handleOpenAIExchange = async (authCode: string) => {
const credentials = openaiOAuth.buildCredentials(tokenInfo)
const oauthExtra = openaiOAuth.buildExtraInfo(tokenInfo) as Record<string, unknown> | undefined
const extra = buildOpenAIPassthroughExtra(oauthExtra)
const extra = buildOpenAIExtra(oauthExtra)
// 应用临时不可调度配置
if (!applyTempUnschedConfig(credentials)) {
@@ -3064,7 +3113,7 @@ const handleOpenAIValidateRT = async (refreshTokenInput: string) => {
const credentials = openaiOAuth.buildCredentials(tokenInfo)
const oauthExtra = openaiOAuth.buildExtraInfo(tokenInfo) as Record<string, unknown> | undefined
const extra = buildOpenAIPassthroughExtra(oauthExtra)
const extra = buildOpenAIExtra(oauthExtra)
// Generate account name with index for batch
const accountName = refreshTokens.length > 1 ? `${form.name} #${i + 1}` : form.name

View File

@@ -735,6 +735,36 @@
</div>
</div>
<!-- OpenAI OAuth Codex 官方客户端限制开关 -->
<div
v-if="account?.platform === 'openai' && account?.type === 'oauth'"
class="border-t border-gray-200 pt-4 dark:border-dark-600"
>
<div class="flex items-center justify-between">
<div>
<label class="input-label mb-0">{{ t('admin.accounts.openai.codexCLIOnly') }}</label>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
{{ t('admin.accounts.openai.codexCLIOnlyDesc') }}
</p>
</div>
<button
type="button"
@click="codexCLIOnlyEnabled = !codexCLIOnlyEnabled"
:class="[
'relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2',
codexCLIOnlyEnabled ? 'bg-primary-600' : 'bg-gray-200 dark:bg-dark-600'
]"
>
<span
:class="[
'pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out',
codexCLIOnlyEnabled ? 'translate-x-5' : 'translate-x-0'
]"
/>
</button>
</div>
</div>
<div>
<div class="flex items-center justify-between">
<div>
@@ -1146,6 +1176,7 @@ const sessionIdMaskingEnabled = ref(false)
// OpenAI 自动透传开关OAuth/API Key
const openaiPassthroughEnabled = ref(false)
const codexCLIOnlyEnabled = ref(false)
const isOpenAIModelRestrictionDisabled = computed(() =>
props.account?.platform === 'openai' && openaiPassthroughEnabled.value
)
@@ -1239,8 +1270,12 @@ watch(
// Load OpenAI passthrough toggle (OpenAI OAuth/API Key)
openaiPassthroughEnabled.value = false
codexCLIOnlyEnabled.value = false
if (newAccount.platform === 'openai' && (newAccount.type === 'oauth' || newAccount.type === 'apikey')) {
openaiPassthroughEnabled.value = extra?.openai_passthrough === true || extra?.openai_oauth_passthrough === true
if (newAccount.type === 'oauth') {
codexCLIOnlyEnabled.value = extra?.codex_cli_only === true
}
}
// Load antigravity model mapping (Antigravity 只支持映射模式)
@@ -1794,6 +1829,13 @@ const handleSubmit = async () => {
delete newExtra.openai_passthrough
delete newExtra.openai_oauth_passthrough
}
if (props.account.type === 'oauth' && codexCLIOnlyEnabled.value) {
newExtra.codex_cli_only = true
} else {
delete newExtra.codex_cli_only
}
updatePayload.extra = newExtra
}

View File

@@ -1534,6 +1534,9 @@ export default {
oauthPassthrough: 'Auto passthrough (auth only)',
oauthPassthroughDesc:
'When enabled, this OpenAI account uses automatic passthrough: the gateway forwards request/response as-is and only swaps auth, while keeping billing/concurrency/audit and necessary safety filtering.',
codexCLIOnly: 'Codex official clients only',
codexCLIOnlyDesc:
'Only applies to OpenAI OAuth. When enabled, only Codex official client families are allowed; when disabled, the gateway bypasses this restriction and keeps existing behavior.',
modelRestrictionDisabledByPassthrough: 'Automatic passthrough is enabled: model whitelist/mapping will not take effect.',
enableSora: 'Enable Sora simultaneously',
enableSoraHint: 'Sora uses the same OpenAI account. Enable to create Sora account simultaneously.'

View File

@@ -1683,6 +1683,8 @@ export default {
oauthPassthrough: '自动透传(仅替换认证)',
oauthPassthroughDesc:
'开启后,该 OpenAI 账号将自动透传请求与响应,仅替换认证并保留计费/并发/审计及必要安全过滤;如遇兼容性问题可随时关闭回滚。',
codexCLIOnly: '仅允许 Codex 官方客户端',
codexCLIOnlyDesc: '仅对 OpenAI OAuth 生效。开启后仅允许 Codex 官方客户端家族访问;关闭后完全绕过并保持原逻辑。',
modelRestrictionDisabledByPassthrough: '已开启自动透传:模型白名单/映射不会生效。',
enableSora: '同时启用 Sora',
enableSoraHint: 'Sora 使用相同的 OpenAI 账号,开启后将同时创建 Sora 平台账号'