feat(openai): 增加 Agent Identity 独立导入入口

This commit is contained in:
cat
2026-07-15 17:52:10 +08:00
parent 529744c7c7
commit 3b5072187a
5 changed files with 173 additions and 25 deletions
@@ -296,7 +296,7 @@
<!-- Account Type Selection (OpenAI) -->
<div v-if="form.platform === 'openai'">
<label class="input-label">{{ t('admin.accounts.accountType') }}</label>
<div class="mt-2 grid grid-cols-2 gap-3" data-tour="account-form-type">
<div class="mt-2 grid grid-cols-1 gap-3 sm:grid-cols-2" data-tour="account-form-type">
<button
type="button"
@click="accountCategory = 'oauth-based'"
@@ -323,6 +323,60 @@
</div>
</button>
<button
type="button"
data-testid="openai-account-type-agent-identity"
@click="accountCategory = 'agent-identity'"
:class="[
'flex items-center gap-3 rounded-lg border-2 p-3 text-left transition-all',
accountCategory === 'agent-identity'
? 'border-emerald-500 bg-emerald-50 dark:bg-emerald-900/20'
: 'border-gray-200 hover:border-emerald-300 dark:border-dark-600 dark:hover:border-emerald-700'
]"
>
<div
:class="[
'flex h-8 w-8 shrink-0 items-center justify-center rounded-lg',
accountCategory === 'agent-identity'
? 'bg-emerald-500 text-white'
: 'bg-gray-100 text-gray-500 dark:bg-dark-600 dark:text-gray-400'
]"
>
<Icon name="shield" size="sm" />
</div>
<div>
<span class="block text-sm font-medium text-gray-900 dark:text-white">Agent Identity</span>
<span class="text-xs text-gray-500 dark:text-gray-400">{{ t('admin.accounts.types.agentIdentity') }}</span>
</div>
</button>
<button
type="button"
data-testid="openai-account-type-codex-pat"
@click="accountCategory = 'codex-pat'"
:class="[
'flex items-center gap-3 rounded-lg border-2 p-3 text-left transition-all',
accountCategory === 'codex-pat'
? 'border-cyan-500 bg-cyan-50 dark:bg-cyan-900/20'
: 'border-gray-200 hover:border-cyan-300 dark:border-dark-600 dark:hover:border-cyan-700'
]"
>
<div
:class="[
'flex h-8 w-8 shrink-0 items-center justify-center rounded-lg',
accountCategory === 'codex-pat'
? 'bg-cyan-500 text-white'
: 'bg-gray-100 text-gray-500 dark:bg-dark-600 dark:text-gray-400'
]"
>
<Icon name="key" size="sm" />
</div>
<div>
<span class="block text-sm font-medium text-gray-900 dark:text-white">Codex PAT</span>
<span class="text-xs text-gray-500 dark:text-gray-400">{{ t('admin.accounts.types.codexPat') }}</span>
</div>
</button>
<button
type="button"
@click="accountCategory = 'apikey'"
@@ -3112,15 +3166,17 @@
:show-proxy-warning="form.platform !== 'openai' && form.platform !== 'grok' && !!form.proxy_id"
:allow-multiple="form.platform === 'anthropic'"
:show-cookie-option="form.platform === 'anthropic'"
:show-refresh-token-option="form.platform === 'openai' || form.platform === 'antigravity' || form.platform === 'grok'"
:show-mobile-refresh-token-option="form.platform === 'openai'"
:show-refresh-token-option="(form.platform === 'openai' && accountCategory === 'oauth-based') || form.platform === 'antigravity' || form.platform === 'grok'"
:show-mobile-refresh-token-option="form.platform === 'openai' && accountCategory === 'oauth-based'"
:show-session-token-option="false"
:show-access-token-option="false"
:show-codex-session-import-option="form.platform === 'openai'"
:show-codex-pat-option="form.platform === 'openai'"
:show-codex-session-import-option="form.platform === 'openai' && (accountCategory === 'oauth-based' || accountCategory === 'agent-identity')"
:show-codex-pat-option="form.platform === 'openai' && accountCategory === 'codex-pat'"
:show-sso-option="form.platform === 'grok'"
:show-manual-option="true"
:initial-input-method="'manual'"
:show-manual-option="form.platform !== 'openai' || accountCategory === 'oauth-based'"
:initial-input-method="openAIInitialInputMethod"
:title-override="oauthStepTitle"
:agent-identity-only="form.platform === 'openai' && accountCategory === 'agent-identity'"
:platform="form.platform"
:show-project-id="geminiOAuthType === 'code_assist'"
@generate-url="handleGenerateUrl"
@@ -3537,6 +3593,8 @@ const { t } = useI18n()
const authStore = useAuthStore()
const oauthStepTitle = computed(() => {
if (form.platform === 'openai' && accountCategory.value === 'agent-identity') return 'Agent Identity'
if (form.platform === 'openai' && accountCategory.value === 'codex-pat') return 'Codex PAT'
if (form.platform === 'openai') return t('admin.accounts.oauth.openai.title')
if (form.platform === 'gemini') return t('admin.accounts.oauth.gemini.title')
if (form.platform === 'antigravity') return t('admin.accounts.oauth.antigravity.title')
@@ -3632,7 +3690,7 @@ interface TempUnschedRuleForm {
// State
const step = ref(1)
const submitting = ref(false)
const accountCategory = ref<'oauth-based' | 'apikey' | 'bedrock' | 'service_account'>('oauth-based') // UI selection for account category
const accountCategory = ref<'oauth-based' | 'agent-identity' | 'codex-pat' | 'apikey' | 'bedrock' | 'service_account'>('oauth-based') // UI selection for account category
const addMethod = ref<AddMethod>('oauth') // For oauth-based: 'oauth' or 'setup-token'
const apiKeyBaseUrl = ref('https://api.anthropic.com')
const apiKeyValue = ref('')
@@ -4017,7 +4075,14 @@ const isOAuthFlow = computed(() => {
if (form.platform === 'anthropic' && accountCategory.value === 'bedrock') {
return false
}
return accountCategory.value === 'oauth-based'
return accountCategory.value === 'oauth-based' || accountCategory.value === 'agent-identity' || accountCategory.value === 'codex-pat'
})
const openAIInitialInputMethod = computed<AuthInputMethod>(() => {
if (form.platform !== 'openai') return 'manual'
if (accountCategory.value === 'agent-identity') return 'codex_session'
if (accountCategory.value === 'codex-pat') return 'codex_pat'
return 'manual'
})
const isGrokSSOInputMethod = computed(() => form.platform === 'grok' && oauthFlowRef.value?.inputMethod === 'sso_cookie')
@@ -4095,7 +4160,7 @@ watch(
}
if ((form.platform === 'gemini' || form.platform === 'anthropic') && category === 'service_account') {
form.type = 'service_account' as AccountType
} else if (category === 'oauth-based') {
} else if (category === 'oauth-based' || category === 'agent-identity' || category === 'codex-pat') {
form.type = form.platform === 'anthropic' ? method as AccountType : 'oauth'
} else {
form.type = 'apikey'
@@ -4196,7 +4261,7 @@ watch(
watch(
[accountCategory, () => form.platform],
([category, platform]) => {
if (platform === 'openai' && category !== 'oauth-based') {
if (platform === 'openai' && category !== 'oauth-based' && category !== 'agent-identity' && category !== 'codex-pat') {
codexCLIOnlyEnabled.value = false
codexCLIOnlyAppServerEnabled.value = false
}
@@ -5445,6 +5510,27 @@ const formatCodexImportMessages = (messages?: CodexSessionImportMessage[]) => {
.join('\n')
}
const isAgentIdentityImportContent = (content: string) => {
const isAgentIdentityValue = (value: unknown): boolean => {
if (Array.isArray(value)) return value.length > 0 && value.every(isAgentIdentityValue)
if (!value || typeof value !== 'object') return false
const record = value as Record<string, unknown>
return record.auth_mode === 'agentIdentity' && !!record.agent_identity && typeof record.agent_identity === 'object'
}
try {
return isAgentIdentityValue(JSON.parse(content))
} catch {
const lines = content.split('\n').map((line) => line.trim()).filter(Boolean)
if (lines.length === 0) return false
try {
return lines.every((line) => isAgentIdentityValue(JSON.parse(line)))
} catch {
return false
}
}
}
const handleOpenAIImportCodexSession = async (content: string) => {
const oauthClient = openaiOAuth
const trimmed = content.trim()
@@ -5452,6 +5538,10 @@ const handleOpenAIImportCodexSession = async (content: string) => {
oauthClient.error.value = t('admin.accounts.oauth.openai.codexSessionEmpty')
return
}
if (accountCategory.value === 'agent-identity' && !isAgentIdentityImportContent(trimmed)) {
oauthClient.error.value = t('admin.accounts.oauth.openai.agentIdentityInvalid')
return
}
const credentialExtras = buildOpenAICodexImportCredentialExtras()
if (credentialExtras === null) {
@@ -282,7 +282,7 @@
class="rounded-lg border border-blue-300 bg-white/80 p-4 dark:border-blue-600 dark:bg-gray-800/80"
>
<p class="mb-3 text-sm text-blue-700 dark:text-blue-300">
{{ t('admin.accounts.oauth.openai.codexSessionDesc') }}
{{ t(agentIdentityOnly ? 'admin.accounts.oauth.openai.agentIdentityDesc' : 'admin.accounts.oauth.openai.codexSessionDesc') }}
</p>
<div class="mb-4">
@@ -290,7 +290,7 @@
class="mb-2 flex items-center gap-2 text-sm font-semibold text-gray-700 dark:text-gray-300"
>
<Icon name="key" size="sm" class="text-blue-500" />
{{ t('admin.accounts.oauth.openai.codexSessionInputLabel') }}
{{ t(agentIdentityOnly ? 'admin.accounts.oauth.openai.agentIdentityInputLabel' : 'admin.accounts.oauth.openai.codexSessionInputLabel') }}
<span
v-if="parsedCodexSessionCount > 1"
class="rounded-full bg-blue-500 px-2 py-0.5 text-xs text-white"
@@ -302,11 +302,11 @@
v-model="codexSessionInput"
rows="8"
class="input w-full resize-y font-mono text-sm"
:placeholder="t('admin.accounts.oauth.openai.codexSessionPlaceholder')"
:placeholder="t(agentIdentityOnly ? 'admin.accounts.oauth.openai.agentIdentityPlaceholder' : 'admin.accounts.oauth.openai.codexSessionPlaceholder')"
spellcheck="false"
></textarea>
<p class="mt-1 text-xs text-blue-600 dark:text-blue-400">
{{ t('admin.accounts.oauth.openai.codexSessionHint') }}
{{ t(agentIdentityOnly ? 'admin.accounts.oauth.openai.agentIdentityHint' : 'admin.accounts.oauth.openai.codexSessionHint') }}
</p>
</div>
@@ -828,6 +828,8 @@ interface Props {
initialInputMethod?: AuthInputMethod
platform?: AccountPlatform // Platform type for different UI/text
showProjectId?: boolean // New prop to control project ID visibility
titleOverride?: string
agentIdentityOnly?: boolean
}
const props = withDefaults(defineProps<Props>(), {
@@ -850,7 +852,9 @@ const props = withDefaults(defineProps<Props>(), {
showManualOption: true,
initialInputMethod: 'manual',
platform: 'anthropic',
showProjectId: true
showProjectId: true,
titleOverride: '',
agentIdentityOnly: false
})
const emit = defineEmits<{
@@ -881,7 +885,7 @@ const getOAuthKey = (key: string) => {
}
// Computed translations for current platform
const oauthTitle = computed(() => t(getOAuthKey('title')))
const oauthTitle = computed(() => props.titleOverride || t(getOAuthKey('title')))
const oauthFollowSteps = computed(() => t(getOAuthKey('followSteps')))
const oauthStep1GenerateUrl = computed(() => t(getOAuthKey('step1GenerateUrl')))
const oauthGenerateAuthUrl = computed(() => t(getOAuthKey('generateAuthUrl')))
@@ -64,6 +64,14 @@ const BaseDialogStub = defineComponent({
const OAuthAuthorizationFlowStub = defineComponent({
name: 'OAuthAuthorizationFlow',
props: {
showManualOption: Boolean,
showCodexSessionImportOption: Boolean,
showCodexPatOption: Boolean,
initialInputMethod: String,
agentIdentityOnly: Boolean,
titleOverride: String,
},
emits: ['import-codex-session', 'import-codex-pat'],
template: `
<div>
@@ -147,6 +155,38 @@ describe('CreateAccountModal OpenAI long-context billing', () => {
expect(createAccountMock.mock.calls[0]?.[0]?.extra?.openai_long_context_billing_enabled).toBe(false)
})
it('shows Agent Identity as a separate OpenAI account type', async () => {
const wrapper = mountModal()
await selectButtonByText(wrapper, 'OpenAI')
await wrapper.get('[data-testid="openai-account-type-agent-identity"]').trigger('click')
await wrapper.get('form#create-account-form input[type="text"]').setValue('Agent Identity')
await wrapper.get('form#create-account-form').trigger('submit.prevent')
const flow = wrapper.getComponent(OAuthAuthorizationFlowStub)
expect(flow.props('showManualOption')).toBe(false)
expect(flow.props('showCodexSessionImportOption')).toBe(true)
expect(flow.props('showCodexPatOption')).toBe(false)
expect(flow.props('initialInputMethod')).toBe('codex_session')
expect(flow.props('agentIdentityOnly')).toBe(true)
expect(flow.props('titleOverride')).toBe('Agent Identity')
})
it('shows Codex PAT as a separate OpenAI account type', async () => {
const wrapper = mountModal()
await selectButtonByText(wrapper, 'OpenAI')
await wrapper.get('[data-testid="openai-account-type-codex-pat"]').trigger('click')
await wrapper.get('form#create-account-form input[type="text"]').setValue('Codex PAT')
await wrapper.get('form#create-account-form').trigger('submit.prevent')
const flow = wrapper.getComponent(OAuthAuthorizationFlowStub)
expect(flow.props('showManualOption')).toBe(false)
expect(flow.props('showCodexSessionImportOption')).toBe(false)
expect(flow.props('showCodexPatOption')).toBe(true)
expect(flow.props('initialInputMethod')).toBe('codex_pat')
expect(flow.props('agentIdentityOnly')).toBe(false)
expect(flow.props('titleOverride')).toBe('Codex PAT')
})
it('sends true explicitly when OpenAI long-context billing is enabled', async () => {
await submitApiKeyAccount('openai', true)
+11 -4
View File
@@ -108,6 +108,8 @@ export default {
types: {
oauth: 'OAuth',
chatgptOauth: 'ChatGPT OAuth',
agentIdentity: 'Import Codex auth.json',
codexPat: 'Personal access token',
responsesApi: 'Responses API',
googleOauth: 'Google OAuth',
codeAssist: 'Code Assist',
@@ -825,16 +827,21 @@ export default {
refreshTokenAuth: 'Manual RT Input',
refreshTokenDesc: 'Enter your existing OpenAI Refresh Token(s). Supports batch input (one per line). The system will automatically validate and create accounts.',
refreshTokenPlaceholder: 'Paste your OpenAI Refresh Token...\nSupports multiple, one per line',
codexSessionAuth: 'Codex auth.json / AT Import',
codexSessionDesc: 'Paste a Codex auth.json (OAuth or Agent Identity) or an accessToken. Accounts use the step 1 settings.',
codexSessionInputLabel: 'Codex auth.json or accessToken',
codexSessionAuth: 'Codex OAuth auth.json / AT Import',
codexSessionDesc: 'Paste a Codex OAuth auth.json or an accessToken. Accounts use the step 1 settings.',
codexSessionInputLabel: 'Codex OAuth auth.json or accessToken',
codexSessionPlaceholder: 'Multiple lines supported, one token or auth.json object per line',
codexSessionHint: 'Agent Identity keeps no OAuth token and signs each upstream request dynamically. Session/access-token imports retain their existing expiration behavior.',
codexSessionHint: 'OAuth session/access-token imports retain their existing expiration behavior.',
codexSessionImportAndCreate: 'Import & Create Account',
codexSessionEmpty: 'Please enter a Codex auth.json or accessToken',
codexSessionImportFailed: 'Failed to import Codex account',
codexSessionImportSuccess: 'Import completed: created {created}, updated {updated}, skipped {skipped}',
codexSessionImportPartial: 'Partial success: created {created}, updated {updated}, skipped {skipped}, failed {failed}',
agentIdentityDesc: 'Import a Codex Agent Identity auth.json. No OAuth access or refresh token is stored.',
agentIdentityInputLabel: 'Agent Identity auth.json',
agentIdentityPlaceholder: 'Paste one Agent Identity auth.json object',
agentIdentityHint: 'The file must use auth_mode=agentIdentity. Upstream requests are signed dynamically.',
agentIdentityInvalid: 'Use a Codex auth.json with auth_mode=agentIdentity.',
codexPatAuth: 'Codex Personal Access Token',
codexPatDesc: 'Enter a Codex at- personal access token. The system validates it with OpenAI whoami before creating the account.',
codexPatInputLabel: 'Codex PAT',
+11 -4
View File
@@ -227,6 +227,8 @@ export default {
types: {
oauth: 'OAuth',
chatgptOauth: 'ChatGPT OAuth',
agentIdentity: '导入 Codex auth.json',
codexPat: '个人访问令牌',
responsesApi: 'Responses API',
googleOauth: 'Google OAuth',
codeAssist: 'Code Assist',
@@ -912,16 +914,21 @@ export default {
refreshTokenAuth: '手动输入 RT',
refreshTokenDesc: '输入您已有的 OpenAI Refresh Token,支持批量输入(每行一个),系统将自动验证并创建账号。',
refreshTokenPlaceholder: '粘贴您的 OpenAI Refresh Token...\n支持多个,每行一个',
codexSessionAuth: 'Codex auth.json / AT 导入',
codexSessionDesc: '粘贴 Codex auth.json(OAuth 或 Agent Identity)或 accessToken,按第一步配置创建账号。',
codexSessionInputLabel: 'Codex auth.json 或 accessToken',
codexSessionAuth: 'Codex OAuth auth.json / AT 导入',
codexSessionDesc: '粘贴 Codex OAuth auth.json 或 accessToken,按第一步配置创建账号。',
codexSessionInputLabel: 'Codex OAuth auth.json 或 accessToken',
codexSessionPlaceholder: '支持多行,每行一个 token 或 auth.json 对象',
codexSessionHint: 'Agent Identity 不保存 OAuth token,并为每次上游请求动态签名;session/accessToken 导入继续沿用原有过期规则。',
codexSessionHint: 'OAuth session/accessToken 导入继续沿用原有过期规则。',
codexSessionImportAndCreate: '导入并创建账号',
codexSessionEmpty: '请输入 Codex auth.json 或 accessToken',
codexSessionImportFailed: 'Codex 账号导入失败',
codexSessionImportSuccess: '导入完成:新增 {created},更新 {updated},跳过 {skipped}',
codexSessionImportPartial: '部分成功:新增 {created},更新 {updated},跳过 {skipped},失败 {failed}',
agentIdentityDesc: '导入 Codex Agent Identity auth.json,不保存 OAuth access token 或 refresh token。',
agentIdentityInputLabel: 'Agent Identity auth.json',
agentIdentityPlaceholder: '粘贴一个 Agent Identity auth.json 对象',
agentIdentityHint: '文件必须使用 auth_mode=agentIdentity;每次上游请求都会动态签名。',
agentIdentityInvalid: '请选择 auth_mode=agentIdentity 的 Codex auth.json。',
codexPatAuth: 'Codex Personal Access Token',
codexPatDesc: '输入 Codex at- Personal Access Token,系统会先调用 OpenAI whoami 校验后再创建账号。',
codexPatInputLabel: 'Codex PAT',