feat: add codex personal access token auth

This commit is contained in:
syx0310
2026-06-22 16:07:41 +00:00
parent 85a3b12254
commit 32df33a1c3
26 changed files with 848 additions and 65 deletions
+7
View File
@@ -18,6 +18,7 @@ import type {
AdminDataImportResult,
CodexSessionImportRequest,
CodexSessionImportResult,
OpenAICodexPATCreateRequest,
CheckMixedChannelRequest,
CheckMixedChannelResponse
} from '@/types'
@@ -612,6 +613,11 @@ export async function importCodexSession(payload: CodexSessionImportRequest): Pr
return data
}
export async function createOpenAICodexPAT(payload: OpenAICodexPATCreateRequest): Promise<Account> {
const { data } = await apiClient.post<Account>('/admin/openai/create-from-codex-pat', payload)
return data
}
/**
* Get Antigravity default model mapping from backend
* @returns Default model mapping (from -> to)
@@ -812,6 +818,7 @@ export const accountsAPI = {
exportData,
importData,
importCodexSession,
createOpenAICodexPAT,
getAntigravityDefaultModelMapping,
batchClearError,
batchRefresh,
@@ -2869,6 +2869,7 @@
: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'"
:platform="form.platform"
:show-project-id="geminiOAuthType === 'code_assist'"
@generate-url="handleGenerateUrl"
@@ -2877,6 +2878,7 @@
@validate-mobile-refresh-token="handleOpenAIValidateMobileRT"
@validate-session-token="handleValidateSessionToken"
@import-codex-session="handleOpenAIImportCodexSession"
@import-codex-pat="handleOpenAIImportCodexPAT"
/>
</div>
@@ -3262,6 +3264,7 @@ interface OAuthFlowExposed {
refreshToken: string
sessionToken: string
codexSession: string
codexPAT: string
inputMethod: AuthInputMethod
reset: () => void
}
@@ -4976,6 +4979,55 @@ const handleOpenAIImportCodexSession = async (content: string) => {
}
}
const handleOpenAIImportCodexPAT = async (accessToken: string) => {
const oauthClient = openaiOAuth
const trimmed = accessToken.trim()
if (!trimmed) {
oauthClient.error.value = t('admin.accounts.oauth.openai.codexPatEmpty')
return
}
const credentialExtras = buildOpenAICodexImportCredentialExtras()
if (credentialExtras === null) {
return
}
oauthClient.loading.value = true
oauthClient.error.value = ''
try {
const extra = buildOpenAIExtra()
await adminAPI.accounts.createOpenAICodexPAT({
access_token: trimmed,
name: form.name,
notes: form.notes || null,
proxy_id: form.proxy_id,
concurrency: form.concurrency,
load_factor: form.load_factor ?? undefined,
priority: form.priority,
rate_multiplier: form.rate_multiplier,
group_ids: form.group_ids,
expires_at: form.expires_at,
auto_pause_on_expired: autoPauseOnExpired.value,
credential_extras: Object.keys(credentialExtras).length > 0 ? credentialExtras : undefined,
extra
})
appStore.showSuccess(t('admin.accounts.messages.accountCreated'))
emit('created')
handleClose()
} catch (error: any) {
oauthClient.error.value =
error.response?.data?.detail ||
error.response?.data?.message ||
error.message ||
t('admin.accounts.oauth.openai.codexPatImportFailed')
appStore.showError(oauthClient.error.value)
} finally {
oauthClient.loading.value = false
}
}
// OpenAI RT 批量验证和创建(共享逻辑)
const handleOpenAIBatchRT = async (refreshTokenInput: string, clientId?: string) => {
const oauthClient = openaiOAuth
@@ -92,6 +92,17 @@
t('admin.accounts.oauth.openai.codexSessionAuth')
}}</span>
</label>
<label v-if="showCodexPatOption" class="flex cursor-pointer items-center gap-2">
<input
v-model="inputMethod"
type="radio"
value="codex_pat"
class="text-blue-600 focus:ring-blue-500"
/>
<span class="text-sm text-blue-900 dark:text-blue-200">{{
t('admin.accounts.oauth.openai.codexPatAuth')
}}</span>
</label>
</div>
</div>
@@ -179,7 +190,7 @@
</div>
</div>
<!-- Codex JSON / AT 批量输入 -->
<!-- Codex OAuth/session JSON batch import -->
<div v-if="inputMethod === 'codex_session'" class="space-y-4">
<div
class="rounded-lg border border-blue-300 bg-white/80 p-4 dark:border-blue-600 dark:bg-gray-800/80"
@@ -258,6 +269,79 @@
</div>
</div>
<!-- Codex Personal Access Token -->
<div v-if="inputMethod === 'codex_pat'" class="space-y-4">
<div
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.codexPatDesc') }}
</p>
<div class="mb-4">
<label
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.codexPatInputLabel') }}
</label>
<textarea
v-model="codexPATInput"
rows="3"
class="input w-full resize-y font-mono text-sm"
:placeholder="t('admin.accounts.oauth.openai.codexPatPlaceholder')"
spellcheck="false"
></textarea>
<p class="mt-1 text-xs text-blue-600 dark:text-blue-400">
{{ t('admin.accounts.oauth.openai.codexPatHint') }}
</p>
</div>
<div
v-if="error"
class="mb-4 rounded-lg border border-red-200 bg-red-50 p-3 dark:border-red-700 dark:bg-red-900/30"
>
<p class="whitespace-pre-line text-sm text-red-600 dark:text-red-400">
{{ error }}
</p>
</div>
<button
type="button"
class="btn btn-primary w-full"
:disabled="loading || !codexPATInput.trim()"
@click="handleImportCodexPAT"
>
<svg
v-if="loading"
class="-ml-1 mr-2 h-4 w-4 animate-spin"
fill="none"
viewBox="0 0 24 24"
>
<circle
class="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
stroke-width="4"
></circle>
<path
class="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
<Icon v-else name="sparkles" size="sm" class="mr-2" />
{{
loading
? t('admin.accounts.oauth.openai.validating')
: t('admin.accounts.oauth.openai.codexPatImportAndCreate')
}}
</button>
</div>
</div>
<!-- Cookie Auto-Auth Form -->
<div v-if="inputMethod === 'cookie'" class="space-y-4">
<div
@@ -652,6 +736,7 @@ interface Props {
showSessionTokenOption?: boolean
showAccessTokenOption?: boolean
showCodexSessionImportOption?: boolean
showCodexPatOption?: boolean
platform?: AccountPlatform // Platform type for different UI/text
showProjectId?: boolean // New prop to control project ID visibility
}
@@ -671,6 +756,7 @@ const props = withDefaults(defineProps<Props>(), {
showSessionTokenOption: false,
showAccessTokenOption: false,
showCodexSessionImportOption: false,
showCodexPatOption: false,
platform: 'anthropic',
showProjectId: true
})
@@ -684,6 +770,7 @@ const emit = defineEmits<{
'validate-session-token': [sessionToken: string]
'import-access-token': [accessToken: string]
'import-codex-session': [content: string]
'import-codex-pat': [accessToken: string]
'update:inputMethod': [method: AuthInputMethod]
}>()
@@ -724,12 +811,13 @@ const sessionKeyInput = ref('')
const refreshTokenInput = ref('')
const sessionTokenInput = ref('')
const codexSessionInput = ref('')
const codexPATInput = ref('')
const showHelpDialog = ref(false)
const oauthState = ref('')
const projectId = ref('')
// Computed: show method selection when either cookie or refresh token option is enabled
const showMethodSelection = computed(() => props.showCookieOption || props.showRefreshTokenOption || props.showMobileRefreshTokenOption || props.showSessionTokenOption || props.showAccessTokenOption || props.showCodexSessionImportOption)
const showMethodSelection = computed(() => props.showCookieOption || props.showRefreshTokenOption || props.showMobileRefreshTokenOption || props.showSessionTokenOption || props.showAccessTokenOption || props.showCodexSessionImportOption || props.showCodexPatOption)
// Clipboard
const { copied, copyToClipboard } = useClipboard()
@@ -837,6 +925,12 @@ const handleImportCodexSession = () => {
}
}
const handleImportCodexPAT = () => {
if (codexPATInput.value.trim()) {
emit('import-codex-pat', codexPATInput.value.trim())
}
}
// Expose methods and state
defineExpose({
authCode: authCodeInput,
@@ -846,6 +940,7 @@ defineExpose({
refreshToken: refreshTokenInput,
sessionToken: sessionTokenInput,
codexSession: codexSessionInput,
codexPAT: codexPATInput,
inputMethod,
reset: () => {
authCodeInput.value = ''
@@ -855,6 +950,7 @@ defineExpose({
refreshTokenInput.value = ''
sessionTokenInput.value = ''
codexSessionInput.value = ''
codexPATInput.value = ''
inputMethod.value = 'manual'
showHelpDialog.value = false
}
+1 -1
View File
@@ -3,7 +3,7 @@ import { useAppStore } from '@/stores/app'
import { adminAPI } from '@/api/admin'
export type AddMethod = 'oauth' | 'setup-token'
export type AuthInputMethod = 'manual' | 'cookie' | 'refresh_token' | 'mobile_refresh_token' | 'session_token' | 'access_token' | 'codex_session'
export type AuthInputMethod = 'manual' | 'cookie' | 'refresh_token' | 'mobile_refresh_token' | 'session_token' | 'access_token' | 'codex_session' | 'codex_pat'
export interface OAuthState {
authUrl: string
+8
View File
@@ -3802,6 +3802,14 @@ export default {
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}',
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',
codexPatPlaceholder: 'at-...',
codexPatHint: 'This is a separate auth mode. It does not save refresh_token or write an OAuth access_token expiration.',
codexPatImportAndCreate: 'Validate & Create Codex PAT Account',
codexPatEmpty: 'Please enter a Codex personal access token',
codexPatImportFailed: 'Failed to create Codex PAT account',
sessionTokenAuth: 'Manual ST Input',
sessionTokenDesc: 'Enter your existing Session Token(s). Supports batch input (one per line). The system will automatically validate and create accounts.',
sessionTokenPlaceholder: 'Paste your Session Token...\nSupports multiple, one per line',
+8
View File
@@ -3945,6 +3945,14 @@ export default {
codexSessionImportFailed: 'Codex 账号导入失败',
codexSessionImportSuccess: '导入完成:新增 {created},更新 {updated},跳过 {skipped}',
codexSessionImportPartial: '部分成功:新增 {created},更新 {updated},跳过 {skipped},失败 {failed}',
codexPatAuth: 'Codex Personal Access Token',
codexPatDesc: '输入 Codex at- Personal Access Token,系统会先调用 OpenAI whoami 校验后再创建账号。',
codexPatInputLabel: 'Codex PAT',
codexPatPlaceholder: 'at-...',
codexPatHint: '这是独立认证模式,不保存 refresh_token,也不会写入 OAuth access_token 过期时间。',
codexPatImportAndCreate: '校验并创建 Codex PAT 账号',
codexPatEmpty: '请输入 Codex Personal Access Token',
codexPatImportFailed: 'Codex PAT 账号创建失败',
sessionTokenAuth: '手动输入 ST',
sessionTokenDesc: '输入您已有的 Session Token,支持批量输入(每行一个),系统将自动验证并创建账号。',
sessionTokenPlaceholder: '粘贴您的 Session Token...\n支持多个,每行一个',
+18
View File
@@ -1175,6 +1175,24 @@ export interface CodexSessionImportRequest {
confirm_mixed_channel_risk?: boolean
}
export interface OpenAICodexPATCreateRequest {
access_token: string
name?: string
notes?: string | null
group_ids?: number[]
proxy_id?: number | null
concurrency?: number
priority?: number
rate_multiplier?: number
load_factor?: number | null
expires_at?: number | null
auto_pause_on_expired?: boolean
credential_extras?: Record<string, unknown>
extra?: Record<string, unknown>
skip_default_group_bind?: boolean
confirm_mixed_channel_risk?: boolean
}
export interface CodexSessionImportMessage {
index: number
name?: string