mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-30 00:21:34 +00:00
Merge pull request #4359 from catoncat/agent/fix-agent-identity-import-expiry
fix(openai): make Agent Identity usable end to end
This commit is contained in:
@@ -3117,6 +3117,7 @@
|
||||
:show-session-token-option="false"
|
||||
:show-access-token-option="false"
|
||||
:show-codex-session-import-option="form.platform === 'openai'"
|
||||
:show-agent-identity-option="form.platform === 'openai'"
|
||||
:show-codex-pat-option="form.platform === 'openai'"
|
||||
:show-sso-option="form.platform === 'grok'"
|
||||
:show-manual-option="true"
|
||||
@@ -5445,6 +5446,30 @@ 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>
|
||||
const authMode = record.auth_mode ?? record.authMode
|
||||
const agentIdentity = record.agent_identity ?? record.agentIdentity
|
||||
return (typeof authMode === 'string' && authMode.toLowerCase() === 'agentidentity')
|
||||
|| (!!agentIdentity && typeof agentIdentity === '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 +5477,10 @@ const handleOpenAIImportCodexSession = async (content: string) => {
|
||||
oauthClient.error.value = t('admin.accounts.oauth.openai.codexSessionEmpty')
|
||||
return
|
||||
}
|
||||
if (oauthFlowRef.value?.inputMethod === 'agent_identity' && !isAgentIdentityImportContent(trimmed)) {
|
||||
oauthClient.error.value = t('admin.accounts.oauth.openai.agentIdentityInvalid')
|
||||
return
|
||||
}
|
||||
|
||||
const credentialExtras = buildOpenAICodexImportCredentialExtras()
|
||||
if (credentialExtras === null) {
|
||||
|
||||
@@ -103,6 +103,17 @@
|
||||
t('admin.accounts.oauth.openai.codexSessionAuth')
|
||||
}}</span>
|
||||
</label>
|
||||
<label v-if="showAgentIdentityOption" class="flex cursor-pointer items-center gap-2">
|
||||
<input
|
||||
v-model="inputMethod"
|
||||
type="radio"
|
||||
value="agent_identity"
|
||||
class="text-blue-600 focus:ring-blue-500"
|
||||
/>
|
||||
<span class="text-sm text-blue-900 dark:text-blue-200">{{
|
||||
t('admin.accounts.oauth.openai.agentIdentityAuth')
|
||||
}}</span>
|
||||
</label>
|
||||
<label v-if="showCodexPatOption" class="flex cursor-pointer items-center gap-2">
|
||||
<input
|
||||
v-model="inputMethod"
|
||||
@@ -277,12 +288,12 @@
|
||||
</div>
|
||||
|
||||
<!-- Codex auth.json / session credential batch import -->
|
||||
<div v-if="inputMethod === 'codex_session'" class="space-y-4">
|
||||
<div v-if="inputMethod === 'codex_session' || inputMethod === 'agent_identity'" 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.codexSessionDesc') }}
|
||||
{{ t(isAgentIdentityInput ? 'admin.accounts.oauth.openai.agentIdentityDesc' : 'admin.accounts.oauth.openai.codexSessionDesc') }}
|
||||
</p>
|
||||
|
||||
<div class="mb-4">
|
||||
@@ -290,7 +301,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(isAgentIdentityInput ? '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 +313,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(isAgentIdentityInput ? '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(isAgentIdentityInput ? 'admin.accounts.oauth.openai.agentIdentityHint' : 'admin.accounts.oauth.openai.codexSessionHint') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -822,6 +833,7 @@ interface Props {
|
||||
showSessionTokenOption?: boolean
|
||||
showAccessTokenOption?: boolean
|
||||
showCodexSessionImportOption?: boolean
|
||||
showAgentIdentityOption?: boolean
|
||||
showCodexPatOption?: boolean
|
||||
showSsoOption?: boolean
|
||||
showManualOption?: boolean
|
||||
@@ -845,6 +857,7 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
showSessionTokenOption: false,
|
||||
showAccessTokenOption: false,
|
||||
showCodexSessionImportOption: false,
|
||||
showAgentIdentityOption: false,
|
||||
showCodexPatOption: false,
|
||||
showSsoOption: false,
|
||||
showManualOption: true,
|
||||
@@ -901,6 +914,7 @@ const oauthImportantNotice = computed(() => {
|
||||
|
||||
// Local state
|
||||
const inputMethod = ref<AuthInputMethod>(props.initialInputMethod)
|
||||
const isAgentIdentityInput = computed(() => inputMethod.value === 'agent_identity')
|
||||
const authCodeInput = ref('')
|
||||
const sessionKeyInput = ref('')
|
||||
const refreshTokenInput = ref('')
|
||||
@@ -921,6 +935,7 @@ const methodOptionCount = computed(() => [
|
||||
props.showSessionTokenOption,
|
||||
props.showAccessTokenOption,
|
||||
props.showCodexSessionImportOption,
|
||||
props.showAgentIdentityOption,
|
||||
props.showCodexPatOption,
|
||||
props.showSsoOption
|
||||
].filter(Boolean).length)
|
||||
|
||||
@@ -64,6 +64,14 @@ const BaseDialogStub = defineComponent({
|
||||
|
||||
const OAuthAuthorizationFlowStub = defineComponent({
|
||||
name: 'OAuthAuthorizationFlow',
|
||||
props: {
|
||||
showManualOption: Boolean,
|
||||
showCodexSessionImportOption: Boolean,
|
||||
showAgentIdentityOption: Boolean,
|
||||
showCodexPatOption: Boolean,
|
||||
initialInputMethod: String,
|
||||
},
|
||||
data: () => ({ inputMethod: 'manual' }),
|
||||
emits: ['import-codex-session', 'import-codex-pat'],
|
||||
template: `
|
||||
<div>
|
||||
@@ -147,6 +155,34 @@ describe('CreateAccountModal OpenAI long-context billing', () => {
|
||||
expect(createAccountMock.mock.calls[0]?.[0]?.extra?.openai_long_context_billing_enabled).toBe(false)
|
||||
})
|
||||
|
||||
it('exposes Agent Identity in the OpenAI authorization methods', async () => {
|
||||
const wrapper = mountModal()
|
||||
await selectButtonByText(wrapper, 'OpenAI')
|
||||
await wrapper.get('form#create-account-form input[type="text"]').setValue('OpenAI account')
|
||||
await wrapper.get('form#create-account-form').trigger('submit.prevent')
|
||||
|
||||
const flow = wrapper.getComponent(OAuthAuthorizationFlowStub)
|
||||
expect(flow.props('showManualOption')).toBe(true)
|
||||
expect(flow.props('showCodexSessionImportOption')).toBe(true)
|
||||
expect(flow.props('showAgentIdentityOption')).toBe(true)
|
||||
expect(flow.props('showCodexPatOption')).toBe(true)
|
||||
expect(flow.props('initialInputMethod')).toBe('manual')
|
||||
})
|
||||
|
||||
it.each([
|
||||
['camelCase', { authMode: 'agentIdentity', agentIdentity: { agentRuntimeId: 'runtime' } }],
|
||||
['nested identity without auth_mode', { agent_identity: { agent_runtime_id: 'runtime' } }],
|
||||
])('accepts backend-compatible %s Agent Identity imports', async (_name, content) => {
|
||||
const wrapper = await openCodexImportStep()
|
||||
const flow = wrapper.getComponent(OAuthAuthorizationFlowStub)
|
||||
flow.vm.inputMethod = 'agent_identity'
|
||||
|
||||
flow.vm.$emit('import-codex-session', JSON.stringify(content))
|
||||
await flushPromises()
|
||||
|
||||
expect(importCodexSessionMock).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('sends true explicitly when OpenAI long-context billing is enabled', async () => {
|
||||
await submitApiKeyAccount('openai', true)
|
||||
|
||||
|
||||
@@ -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' | 'codex_pat' | 'sso_cookie'
|
||||
export type AuthInputMethod = 'manual' | 'cookie' | 'refresh_token' | 'mobile_refresh_token' | 'session_token' | 'access_token' | 'codex_session' | 'agent_identity' | 'codex_pat' | 'sso_cookie'
|
||||
|
||||
export interface OAuthState {
|
||||
authUrl: string
|
||||
|
||||
@@ -825,16 +825,22 @@ 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}',
|
||||
agentIdentityAuth: 'Agent Identity auth.json',
|
||||
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',
|
||||
|
||||
@@ -912,16 +912,22 @@ 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}',
|
||||
agentIdentityAuth: 'Agent Identity auth.json',
|
||||
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',
|
||||
|
||||
Reference in New Issue
Block a user