mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-31 16:26:30 +00:00
feat(codex-detect): codex_cli_only 检测加固 + 引擎指纹统一信号列表 + 账号级 app-server
将 codex_cli_only 客户端识别从「单一 strict 开关 + 固定 OR 头集合」重构为 可逐项管理的引擎指纹信号列表,加固整条判定链,并补齐账号级 app-server 控制、 对齐前端设置文案。 判定链(每步可短路): - 账号未开 codex_cli_only → 不限制;gateway.force_codex_cli → 旁路放行 - 全局黑名单命中(OR 宽 deny)→ 立即拒 - 身份候选:官方 UA(strict,仅前缀)/ 官方 originator(OR)/ 全局白名单(双因子 AND) / 全局 app-server 开关 OR 账号 app-server 开关;均不命中 → 拒 - 版本门(仅官方候选):UA 须可解析引擎版本,再校验 [min,max] 区间 - 引擎指纹 AND 硬门:按信号列表逐条勾选 AND、每条行内变体 OR;无 Required 信号 → 放行 引擎指纹信号列表(唯一真源) - 新增 openai.EngineFingerprintSignal 类型 + EvaluateEngineFingerprint 求值器 (勾选 AND / 行内变体 OR / 无勾选 → 放行) - CodexRestrictionPolicy 增 EngineFingerprintSignals;信号列表单一决定是否启用指纹门, 不再保留独立「要求引擎指纹」总开关(与「信号全不选」语义重复) - 新设置键 codex_cli_only_engine_fingerprint_signals(默认只勾 x-codex- 前缀); 旧 body 指纹开关幂等迁移并入信号列表;wire 接线 - 黑/白名单自由条目、命名预设、版本区间 全局设置管线 - gateway 缺 settingService(仅测试/误配可达)时指纹门回退默认种子信号、失败关闭, 不再因零值 policy(nil 信号)失败开放 账号级 Codex app-server(替换已失效的 ClaudeCode 放行机制) - account.IsCodexCLIOnlyAppServerAllowed() 读 extra.codex_cli_only_allow_app_server, 仅在 codex_cli_only 开启时生效;候选身份门「全局 OR 账号」,与旧系统双层控制对齐 - 移除已无入口的 claude_code 预设机制(allowedClientRegistry / MatchAllowedClients / 账号 GetCodexCLIOnlyAllowedClients / reason);白名单 AllowedClientEntry / IsAllowedClientMatch 保留 门加固(反伪 + 写入校验) - 官方 UA 访问门改 strict:IsCodexOfficialClientRequestStrict 仅前缀匹配,收窄「浏览器前缀 + 中段 codex token」伪造面(strict 仍保留 Codex 家族前缀与 UA 尾部兜底,故对「任意前缀 + 官方尾部 (name;ver)」仍放行——与 UA 可伪造、真正反伪靠引擎指纹门的设计一致) - 官方客户端识别扩展:新增 codex-tui/、codex_vscode_copilot/ 前缀 + UA 尾部 (name;ver) 兜底 (恢复 CODEX_INTERNAL_ORIGINATOR_OVERRIDE 的真实 client,如 cccc→codex-tui),originator 改 精确集。该识别经 IsCodexOfficialClientByHeaders 被 passthrough 复用,故透传的官方判定一并 修正(codex-tui 等不再被误改写 UA)——非「行为不变」,属有意修正 - 白名单写入校验 ValidateCodexWhitelistEntriesJSON + AllowedClientEntry.IsWhitelistable: 双因子 AND 条目须可命中(非空 originator + 非空 ua_contains),拒绝写入会静默失效的死规则; 黑名单(OR 宽 deny,允许 originator-only)不受约束 管理端 / 前端 - handler / DTO / settings_view / 契约测试;gateway 接入判定链 - 信号列表编辑器(替换 body 开关)、api 类型、SettingsView;无勾选给常驻警告 - Create/Edit/Bulk 三弹窗「Codex Only」下新增 app-server 开关(OR 合并全局) - 文案:UA/Originator → User-Agent/Originator;黑/白名单重命名为 User-Agent/Originator 黑/白名单; 「允许 App Server 第三方客户端」→「Codex app-server」+ 简介示例;i18n zh/en 同步 - 移除死代码 HasCodex*Fingerprint helper 测试:引擎指纹求值器 / 账号 app-server(OR 语义)/ detector(含 N1 strict、失败关闭)/ 白名单写入校验 / BulkEdit spec 等;后端 build + service/openai/admin 单测全绿,前端 vue-tsc + vitest 全绿。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
f93a6c50ce
commit
819fda34d9
@@ -742,44 +742,44 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- OpenAI OAuth: 额外放行 Claude Code 的 Codex 插件 -->
|
||||
<!-- OpenAI OAuth: Codex app-server -->
|
||||
<div v-if="allOpenAIOAuth" class="border-t border-gray-200 pt-4 dark:border-dark-600">
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<label
|
||||
id="bulk-edit-openai-codex-allow-claude-code-label"
|
||||
id="bulk-edit-openai-codex-app-server-label"
|
||||
class="input-label mb-0"
|
||||
for="bulk-edit-openai-codex-allow-claude-code-enabled"
|
||||
for="bulk-edit-openai-codex-app-server-enabled"
|
||||
>
|
||||
{{ t('admin.accounts.openai.codexCLIOnlyAllowClaudeCode') }}
|
||||
{{ t('admin.accounts.openai.codexCLIOnlyAppServer') }}
|
||||
</label>
|
||||
<input
|
||||
v-model="enableCodexCLIOnlyAllowClaudeCode"
|
||||
id="bulk-edit-openai-codex-allow-claude-code-enabled"
|
||||
v-model="enableCodexCLIOnlyAppServer"
|
||||
id="bulk-edit-openai-codex-app-server-enabled"
|
||||
type="checkbox"
|
||||
aria-controls="bulk-edit-openai-codex-allow-claude-code"
|
||||
aria-controls="bulk-edit-openai-codex-app-server"
|
||||
class="rounded border-gray-300 text-primary-600 focus:ring-primary-500"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
id="bulk-edit-openai-codex-allow-claude-code"
|
||||
:class="!enableCodexCLIOnlyAllowClaudeCode && 'pointer-events-none opacity-50'"
|
||||
id="bulk-edit-openai-codex-app-server"
|
||||
:class="!enableCodexCLIOnlyAppServer && 'pointer-events-none opacity-50'"
|
||||
>
|
||||
<p class="mb-3 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('admin.accounts.openai.codexCLIOnlyAllowClaudeCodeDesc') }}
|
||||
{{ t('admin.accounts.openai.codexCLIOnlyAppServerDesc') }}
|
||||
</p>
|
||||
<button
|
||||
id="bulk-edit-openai-codex-allow-claude-code-toggle"
|
||||
id="bulk-edit-openai-codex-app-server-toggle"
|
||||
type="button"
|
||||
: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',
|
||||
codexCLIOnlyAllowClaudeCodeEnabled ? 'bg-primary-600' : 'bg-gray-200 dark:bg-dark-600'
|
||||
codexCLIOnlyAppServerEnabled ? 'bg-primary-600' : 'bg-gray-200 dark:bg-dark-600'
|
||||
]"
|
||||
@click="codexCLIOnlyAllowClaudeCodeEnabled = !codexCLIOnlyAllowClaudeCodeEnabled"
|
||||
@click="codexCLIOnlyAppServerEnabled = !codexCLIOnlyAppServerEnabled"
|
||||
>
|
||||
<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',
|
||||
codexCLIOnlyAllowClaudeCodeEnabled ? 'translate-x-5' : 'translate-x-0'
|
||||
codexCLIOnlyAppServerEnabled ? 'translate-x-5' : 'translate-x-0'
|
||||
]"
|
||||
/>
|
||||
</button>
|
||||
@@ -1263,7 +1263,7 @@ const enableOpenAIPassthrough = ref(false)
|
||||
const enableOpenAIWSMode = ref(false)
|
||||
const enableOpenAIAPIKeyWSMode = ref(false)
|
||||
const enableCodexCLIOnly = ref(false)
|
||||
const enableCodexCLIOnlyAllowClaudeCode = ref(false)
|
||||
const enableCodexCLIOnlyAppServer = ref(false)
|
||||
const enableOpenAICompactMode = ref(false)
|
||||
const enableOpenAICompactModelMapping = ref(false)
|
||||
const enableRpmLimit = ref(false)
|
||||
@@ -1291,7 +1291,7 @@ const openaiPassthroughEnabled = ref(false)
|
||||
const openaiOAuthResponsesWebSocketV2Mode = ref<OpenAIWSMode>(OPENAI_WS_MODE_OFF)
|
||||
const openaiAPIKeyResponsesWebSocketV2Mode = ref<OpenAIWSMode>(OPENAI_WS_MODE_OFF)
|
||||
const codexCLIOnlyEnabled = ref(false)
|
||||
const codexCLIOnlyAllowClaudeCodeEnabled = ref(false)
|
||||
const codexCLIOnlyAppServerEnabled = ref(false)
|
||||
const openAICompactMode = ref<OpenAICompactMode>('auto')
|
||||
const openAICompactModelMappings = ref<ModelMapping[]>([])
|
||||
const rpmLimitEnabled = ref(false)
|
||||
@@ -1542,9 +1542,15 @@ const buildUpdatePayload = (): Record<string, unknown> | null => {
|
||||
extra.codex_cli_only = codexCLIOnlyEnabled.value
|
||||
}
|
||||
|
||||
if (enableCodexCLIOnlyAllowClaudeCode.value) {
|
||||
// 子开关从属于 codex_cli_only:仅当同一次批量编辑也把父开关设为开启时才写入,
|
||||
// 与 Create/Edit 语义对齐,避免在父开关关闭的账号上写入无意义的孤立字段。
|
||||
if (
|
||||
enableCodexCLIOnlyAppServer.value &&
|
||||
enableCodexCLIOnly.value &&
|
||||
codexCLIOnlyEnabled.value
|
||||
) {
|
||||
const extra = ensureExtra()
|
||||
extra.codex_cli_only_allowed_clients = codexCLIOnlyAllowClaudeCodeEnabled.value ? ['claude_code'] : []
|
||||
extra.codex_cli_only_allow_app_server = codexCLIOnlyAppServerEnabled.value
|
||||
}
|
||||
|
||||
if (enableOpenAICompactMode.value) {
|
||||
@@ -1653,7 +1659,7 @@ const handleSubmit = async () => {
|
||||
enableOpenAIWSMode.value ||
|
||||
enableOpenAIAPIKeyWSMode.value ||
|
||||
enableCodexCLIOnly.value ||
|
||||
enableCodexCLIOnlyAllowClaudeCode.value ||
|
||||
enableCodexCLIOnlyAppServer.value ||
|
||||
enableOpenAICompactMode.value ||
|
||||
enableOpenAICompactModelMapping.value ||
|
||||
enableRpmLimit.value ||
|
||||
@@ -1756,7 +1762,7 @@ watch(
|
||||
enableOpenAIWSMode.value = false
|
||||
enableOpenAIAPIKeyWSMode.value = false
|
||||
enableCodexCLIOnly.value = false
|
||||
enableCodexCLIOnlyAllowClaudeCode.value = false
|
||||
enableCodexCLIOnlyAppServer.value = false
|
||||
enableOpenAICompactMode.value = false
|
||||
enableOpenAICompactModelMapping.value = false
|
||||
enableRpmLimit.value = false
|
||||
@@ -1780,7 +1786,7 @@ watch(
|
||||
openaiOAuthResponsesWebSocketV2Mode.value = OPENAI_WS_MODE_OFF
|
||||
openaiAPIKeyResponsesWebSocketV2Mode.value = OPENAI_WS_MODE_OFF
|
||||
codexCLIOnlyEnabled.value = false
|
||||
codexCLIOnlyAllowClaudeCodeEnabled.value = false
|
||||
codexCLIOnlyAppServerEnabled.value = false
|
||||
openAICompactMode.value = 'auto'
|
||||
openAICompactModelMappings.value = []
|
||||
rpmLimitEnabled.value = false
|
||||
|
||||
@@ -2700,23 +2700,23 @@
|
||||
class="mt-4 flex items-center justify-between border-l-2 border-gray-200 pl-4 dark:border-dark-600"
|
||||
>
|
||||
<div>
|
||||
<label class="input-label mb-0">{{ t('admin.accounts.openai.codexCLIOnlyAllowClaudeCode') }}</label>
|
||||
<label class="input-label mb-0">{{ t('admin.accounts.openai.codexCLIOnlyAppServer') }}</label>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('admin.accounts.openai.codexCLIOnlyAllowClaudeCodeDesc') }}
|
||||
{{ t('admin.accounts.openai.codexCLIOnlyAppServerDesc') }}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
@click="codexCLIOnlyAllowClaudeCodeEnabled = !codexCLIOnlyAllowClaudeCodeEnabled"
|
||||
@click="codexCLIOnlyAppServerEnabled = !codexCLIOnlyAppServerEnabled"
|
||||
: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',
|
||||
codexCLIOnlyAllowClaudeCodeEnabled ? 'bg-primary-600' : 'bg-gray-200 dark:bg-dark-600'
|
||||
codexCLIOnlyAppServerEnabled ? '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',
|
||||
codexCLIOnlyAllowClaudeCodeEnabled ? 'translate-x-5' : 'translate-x-0'
|
||||
codexCLIOnlyAppServerEnabled ? 'translate-x-5' : 'translate-x-0'
|
||||
]"
|
||||
/>
|
||||
</button>
|
||||
@@ -3496,7 +3496,7 @@ const openAIEndpointCapabilities = ref<OpenAIEndpointCapability[]>(['chat_comple
|
||||
const openaiOAuthResponsesWebSocketV2Mode = ref<OpenAIWSMode>(OPENAI_WS_MODE_OFF)
|
||||
const openaiAPIKeyResponsesWebSocketV2Mode = ref<OpenAIWSMode>(OPENAI_WS_MODE_OFF)
|
||||
const codexCLIOnlyEnabled = ref(false)
|
||||
const codexCLIOnlyAllowClaudeCodeEnabled = ref(false)
|
||||
const codexCLIOnlyAppServerEnabled = ref(false)
|
||||
const anthropicPassthroughEnabled = ref(false)
|
||||
const webSearchEmulationMode = ref('default')
|
||||
const webSearchGlobalEnabled = ref(false)
|
||||
@@ -3935,7 +3935,7 @@ watch(
|
||||
openaiOAuthResponsesWebSocketV2Mode.value = OPENAI_WS_MODE_OFF
|
||||
openaiAPIKeyResponsesWebSocketV2Mode.value = OPENAI_WS_MODE_OFF
|
||||
codexCLIOnlyEnabled.value = false
|
||||
codexCLIOnlyAllowClaudeCodeEnabled.value = false
|
||||
codexCLIOnlyAppServerEnabled.value = false
|
||||
}
|
||||
if (newPlatform !== 'anthropic') {
|
||||
anthropicPassthroughEnabled.value = false
|
||||
@@ -3957,7 +3957,7 @@ watch(
|
||||
([category, platform]) => {
|
||||
if (platform === 'openai' && category !== 'oauth-based') {
|
||||
codexCLIOnlyEnabled.value = false
|
||||
codexCLIOnlyAllowClaudeCodeEnabled.value = false
|
||||
codexCLIOnlyAppServerEnabled.value = false
|
||||
}
|
||||
if (platform !== 'anthropic' || category !== 'apikey') {
|
||||
anthropicPassthroughEnabled.value = false
|
||||
@@ -4338,7 +4338,7 @@ const resetForm = () => {
|
||||
openaiOAuthResponsesWebSocketV2Mode.value = OPENAI_WS_MODE_OFF
|
||||
openaiAPIKeyResponsesWebSocketV2Mode.value = OPENAI_WS_MODE_OFF
|
||||
codexCLIOnlyEnabled.value = false
|
||||
codexCLIOnlyAllowClaudeCodeEnabled.value = false
|
||||
codexCLIOnlyAppServerEnabled.value = false
|
||||
anthropicPassthroughEnabled.value = false
|
||||
webSearchEmulationMode.value = 'default'
|
||||
// Reset quota control state
|
||||
@@ -4419,14 +4419,15 @@ const buildOpenAIExtra = (base?: Record<string, unknown>): Record<string, unknow
|
||||
} else {
|
||||
delete extra.codex_cli_only
|
||||
}
|
||||
delete extra.codex_cli_only_allowed_clients
|
||||
if (
|
||||
accountCategory.value === 'oauth-based' &&
|
||||
codexCLIOnlyEnabled.value &&
|
||||
codexCLIOnlyAllowClaudeCodeEnabled.value
|
||||
codexCLIOnlyAppServerEnabled.value
|
||||
) {
|
||||
extra.codex_cli_only_allowed_clients = ['claude_code']
|
||||
extra.codex_cli_only_allow_app_server = true
|
||||
} else {
|
||||
delete extra.codex_cli_only_allowed_clients
|
||||
delete extra.codex_cli_only_allow_app_server
|
||||
}
|
||||
if (openAICompactMode.value !== 'auto') {
|
||||
extra.openai_compact_mode = openAICompactMode.value
|
||||
|
||||
@@ -1693,23 +1693,23 @@
|
||||
class="mt-4 flex items-center justify-between border-l-2 border-gray-200 pl-4 dark:border-dark-600"
|
||||
>
|
||||
<div>
|
||||
<label class="input-label mb-0">{{ t('admin.accounts.openai.codexCLIOnlyAllowClaudeCode') }}</label>
|
||||
<label class="input-label mb-0">{{ t('admin.accounts.openai.codexCLIOnlyAppServer') }}</label>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('admin.accounts.openai.codexCLIOnlyAllowClaudeCodeDesc') }}
|
||||
{{ t('admin.accounts.openai.codexCLIOnlyAppServerDesc') }}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
@click="codexCLIOnlyAllowClaudeCodeEnabled = !codexCLIOnlyAllowClaudeCodeEnabled"
|
||||
@click="codexCLIOnlyAppServerEnabled = !codexCLIOnlyAppServerEnabled"
|
||||
: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',
|
||||
codexCLIOnlyAllowClaudeCodeEnabled ? 'bg-primary-600' : 'bg-gray-200 dark:bg-dark-600'
|
||||
codexCLIOnlyAppServerEnabled ? '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',
|
||||
codexCLIOnlyAllowClaudeCodeEnabled ? 'translate-x-5' : 'translate-x-0'
|
||||
codexCLIOnlyAppServerEnabled ? 'translate-x-5' : 'translate-x-0'
|
||||
]"
|
||||
/>
|
||||
</button>
|
||||
@@ -2603,7 +2603,7 @@ const openAIEndpointCapabilities = ref<OpenAIEndpointCapability[]>(['chat_comple
|
||||
const openaiOAuthResponsesWebSocketV2Mode = ref<OpenAIWSMode>(OPENAI_WS_MODE_OFF)
|
||||
const openaiAPIKeyResponsesWebSocketV2Mode = ref<OpenAIWSMode>(OPENAI_WS_MODE_OFF)
|
||||
const codexCLIOnlyEnabled = ref(false)
|
||||
const codexCLIOnlyAllowClaudeCodeEnabled = ref(false)
|
||||
const codexCLIOnlyAppServerEnabled = ref(false)
|
||||
type CodexImageGenerationBridgeMode = 'inherit' | 'enabled' | 'disabled'
|
||||
const codexImageGenerationBridgeMode = ref<CodexImageGenerationBridgeMode>('inherit')
|
||||
const anthropicPassthroughEnabled = ref(false)
|
||||
@@ -2986,7 +2986,7 @@ const syncFormFromAccount = (newAccount: Account | null) => {
|
||||
openaiOAuthResponsesWebSocketV2Mode.value = OPENAI_WS_MODE_OFF
|
||||
openaiAPIKeyResponsesWebSocketV2Mode.value = OPENAI_WS_MODE_OFF
|
||||
codexCLIOnlyEnabled.value = false
|
||||
codexCLIOnlyAllowClaudeCodeEnabled.value = false
|
||||
codexCLIOnlyAppServerEnabled.value = false
|
||||
codexImageGenerationBridgeMode.value = 'inherit'
|
||||
anthropicPassthroughEnabled.value = false
|
||||
webSearchEmulationMode.value = 'default'
|
||||
@@ -3024,9 +3024,8 @@ const syncFormFromAccount = (newAccount: Account | null) => {
|
||||
})
|
||||
if (newAccount.type === 'oauth') {
|
||||
codexCLIOnlyEnabled.value = extra?.codex_cli_only === true
|
||||
codexCLIOnlyAllowClaudeCodeEnabled.value =
|
||||
Array.isArray(extra?.codex_cli_only_allowed_clients) &&
|
||||
(extra.codex_cli_only_allowed_clients as unknown[]).includes('claude_code')
|
||||
codexCLIOnlyAppServerEnabled.value =
|
||||
extra?.codex_cli_only_allow_app_server === true
|
||||
}
|
||||
const credentials = newAccount.credentials as Record<string, unknown> | undefined
|
||||
const compactMappings = credentials?.compact_model_mapping as Record<string, string> | undefined
|
||||
@@ -4169,11 +4168,12 @@ const handleSubmit = async () => {
|
||||
} else {
|
||||
delete newExtra.codex_cli_only
|
||||
}
|
||||
// 仅当 codex_cli_only 开启且子开关开启时写入 Claude Code 插件白名单,否则清除避免孤立字段
|
||||
if (codexCLIOnlyEnabled.value && codexCLIOnlyAllowClaudeCodeEnabled.value) {
|
||||
newExtra.codex_cli_only_allowed_clients = ['claude_code']
|
||||
// Claude Code 插件放行已迁移到全局 codex_cli_only_whitelist,编辑时清理废弃账号级快捷字段。
|
||||
delete newExtra.codex_cli_only_allowed_clients
|
||||
if (codexCLIOnlyEnabled.value && codexCLIOnlyAppServerEnabled.value) {
|
||||
newExtra.codex_cli_only_allow_app_server = true
|
||||
} else {
|
||||
delete newExtra.codex_cli_only_allowed_clients
|
||||
delete newExtra.codex_cli_only_allow_app_server
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -197,25 +197,44 @@ describe('BulkEditAccountModal', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('OpenAI OAuth 批量编辑应提交 codex_cli_only_allowed_clients 字段', async () => {
|
||||
it('OpenAI OAuth 批量编辑应提交 codex_cli_only_allow_app_server 字段(需同时开启父开关)', async () => {
|
||||
const wrapper = mountModal({
|
||||
selectedPlatforms: ['openai'],
|
||||
selectedTypes: ['oauth']
|
||||
})
|
||||
|
||||
await wrapper.get('#bulk-edit-openai-codex-allow-claude-code-enabled').setValue(true)
|
||||
await wrapper.get('#bulk-edit-openai-codex-allow-claude-code-toggle').trigger('click')
|
||||
// 子开关从属于 codex_cli_only:必须同时批量开启父开关才写入
|
||||
await wrapper.get('#bulk-edit-openai-codex-cli-only-enabled').setValue(true)
|
||||
await wrapper.get('#bulk-edit-openai-codex-cli-only-toggle').trigger('click')
|
||||
await wrapper.get('#bulk-edit-openai-codex-app-server-enabled').setValue(true)
|
||||
await wrapper.get('#bulk-edit-openai-codex-app-server-toggle').trigger('click')
|
||||
await wrapper.get('#bulk-edit-account-form').trigger('submit.prevent')
|
||||
await flushPromises()
|
||||
|
||||
expect(adminAPI.accounts.bulkUpdate).toHaveBeenCalledTimes(1)
|
||||
expect(adminAPI.accounts.bulkUpdate).toHaveBeenCalledWith([1, 2], {
|
||||
extra: {
|
||||
codex_cli_only_allowed_clients: ['claude_code']
|
||||
codex_cli_only: true,
|
||||
codex_cli_only_allow_app_server: true
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('未同时开启父开关时不应写入 codex_cli_only_allow_app_server', async () => {
|
||||
const wrapper = mountModal({
|
||||
selectedPlatforms: ['openai'],
|
||||
selectedTypes: ['oauth']
|
||||
})
|
||||
|
||||
// 仅开启子开关、不批量设置父开关 codex_cli_only:不应写入孤立字段,也不应调用接口
|
||||
await wrapper.get('#bulk-edit-openai-codex-app-server-enabled').setValue(true)
|
||||
await wrapper.get('#bulk-edit-openai-codex-app-server-toggle').trigger('click')
|
||||
await wrapper.get('#bulk-edit-account-form').trigger('submit.prevent')
|
||||
await flushPromises()
|
||||
|
||||
expect(adminAPI.accounts.bulkUpdate).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('OpenAI API Key 批量编辑应提交 API Key 专属 WS mode 字段', async () => {
|
||||
const wrapper = mountModal({
|
||||
selectedPlatforms: ['openai'],
|
||||
|
||||
Reference in New Issue
Block a user