mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-08 18:37:46 +08:00
fix(provider): harden Agent Identity OAuth lifecycle
This commit is contained in:
@@ -725,19 +725,11 @@
|
||||
class="flex flex-col gap-3 justify-center transition-opacity duration-150"
|
||||
:class="mode === 'agent_identity' ? 'opacity-100' : 'opacity-0 pointer-events-none'"
|
||||
>
|
||||
<div class="space-y-1">
|
||||
<label class="text-xs font-medium">
|
||||
{{ legacyT('ChatGPT Session Token') }}
|
||||
</label>
|
||||
<p class="text-[11px] text-muted-foreground">
|
||||
{{ legacyT('仅用于一次性注册,成功后不会保存 Token。') }}
|
||||
</p>
|
||||
</div>
|
||||
<Textarea
|
||||
v-model="agentIdentitySessionToken"
|
||||
v-model="agentIdentityInput"
|
||||
:disabled="creatingAgentIdentity"
|
||||
:placeholder="legacyT('粘贴 ChatGPT Session Token(JWT)')"
|
||||
class="min-h-[230px] text-xs font-mono break-all !rounded-xl"
|
||||
:placeholder="legacyT('粘贴 AT 或 ChatGPT auth/session JSON')"
|
||||
class="min-h-[200px] text-xs font-mono break-all !rounded-xl"
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
/>
|
||||
@@ -778,7 +770,7 @@
|
||||
:disabled="!canCreateAgentIdentity"
|
||||
@click="handleCreateAgentIdentity"
|
||||
>
|
||||
{{ creatingAgentIdentity ? legacyT('创建中...') : legacyT('创建并导入 Agent Identity') }}
|
||||
{{ creatingAgentIdentity ? legacyT('创建中...') : legacyT('创建') }}
|
||||
</Button>
|
||||
</template>
|
||||
</Dialog>
|
||||
@@ -833,7 +825,7 @@ const emit = defineEmits<{
|
||||
saved: []
|
||||
}>()
|
||||
|
||||
const { success, error: showError } = useToast()
|
||||
const { success, warning, error: showError } = useToast()
|
||||
const { copyToClipboard } = useClipboard()
|
||||
const { legacyT, locale } = useI18n()
|
||||
const proxyNodesStore = useProxyNodesStore()
|
||||
@@ -987,7 +979,7 @@ const windsurfImportMethod = ref<WindsurfImportMethod>('email_password')
|
||||
const windsurfEmail = ref('')
|
||||
const windsurfPassword = ref('')
|
||||
const windsurfAccountName = ref('')
|
||||
const agentIdentitySessionToken = ref('')
|
||||
const agentIdentityInput = ref('')
|
||||
const creatingAgentIdentity = ref(false)
|
||||
let agentIdentityRequestId = 0
|
||||
|
||||
@@ -1077,7 +1069,7 @@ const canImport = computed(() => {
|
||||
|
||||
const canCreateAgentIdentity = computed(() =>
|
||||
isCodexProvider.value
|
||||
&& agentIdentitySessionToken.value.trim().length > 0
|
||||
&& agentIdentityInput.value.trim().length > 0
|
||||
&& !creatingAgentIdentity.value
|
||||
)
|
||||
|
||||
@@ -1383,7 +1375,7 @@ function resetForm() {
|
||||
windsurfEmail.value = ''
|
||||
windsurfPassword.value = ''
|
||||
windsurfAccountName.value = ''
|
||||
agentIdentitySessionToken.value = ''
|
||||
agentIdentityInput.value = ''
|
||||
creatingAgentIdentity.value = false
|
||||
proxyPopoverOpen.value = false
|
||||
selectedProxyNodeId.value = ''
|
||||
@@ -1760,6 +1752,33 @@ function isObjectRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
||||
}
|
||||
|
||||
type AgentIdentityAccessTokenResolution =
|
||||
| { ok: true, accessToken: string }
|
||||
| { ok: false, message: string }
|
||||
|
||||
function resolveAgentIdentityAccessToken(input: string): AgentIdentityAccessTokenResolution {
|
||||
const normalized = input.trim()
|
||||
if (!normalized.startsWith('{') && !normalized.startsWith('[')) {
|
||||
return { ok: true, accessToken: normalized }
|
||||
}
|
||||
|
||||
let parsed: unknown
|
||||
try {
|
||||
parsed = JSON.parse(normalized)
|
||||
} catch {
|
||||
return { ok: false, message: 'ChatGPT auth/session JSON 格式无效' }
|
||||
}
|
||||
if (!isObjectRecord(parsed)) {
|
||||
return { ok: false, message: 'ChatGPT auth/session JSON 缺少 accessToken' }
|
||||
}
|
||||
|
||||
const accessToken = normalizeStringField(parsed.accessToken)
|
||||
?? normalizeStringField(parsed.access_token)
|
||||
return accessToken
|
||||
? { ok: true, accessToken }
|
||||
: { ok: false, message: 'ChatGPT auth/session JSON 缺少 accessToken' }
|
||||
}
|
||||
|
||||
function isCodexAgentIdentityObject(root: Record<string, unknown>): boolean {
|
||||
const nestedValue = root.agent_identity ?? root.agentIdentity
|
||||
const nested = isObjectRecord(nestedValue) ? nestedValue : null
|
||||
@@ -1908,18 +1927,26 @@ async function handleImport() {
|
||||
async function handleCreateAgentIdentity() {
|
||||
if (!canCreateAgentIdentity.value || !props.providerId) return
|
||||
|
||||
const sessionToken = agentIdentitySessionToken.value.trim()
|
||||
const resolvedInput = resolveAgentIdentityAccessToken(agentIdentityInput.value)
|
||||
if (!resolvedInput.ok) {
|
||||
showError(legacyT(resolvedInput.message), legacyT('格式错误'))
|
||||
return
|
||||
}
|
||||
const requestId = ++agentIdentityRequestId
|
||||
creatingAgentIdentity.value = true
|
||||
try {
|
||||
const result = await importProviderRefreshToken(props.providerId, {
|
||||
session_token: sessionToken,
|
||||
create_agent_identity_from_session_token: true,
|
||||
access_token: resolvedInput.accessToken,
|
||||
create_agent_identity: true,
|
||||
proxy_node_id: selectedProxyNodeId.value || undefined,
|
||||
})
|
||||
if (requestId !== agentIdentityRequestId) return
|
||||
|
||||
success(getOAuthSuccessMessage('创建', result))
|
||||
if (result.task_ready === false) {
|
||||
warning(legacyT('Agent Identity 已保存,任务将在后台初始化'), legacyT('已保存'))
|
||||
} else {
|
||||
success(getOAuthSuccessMessage('创建', result))
|
||||
}
|
||||
emit('saved')
|
||||
handleClose()
|
||||
} catch (err: unknown) {
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
<Download class="w-2.5 h-2.5" />
|
||||
</Button>
|
||||
<Button
|
||||
v-else
|
||||
v-else-if="apiKey.agent_identity !== true"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-4 w-4 shrink-0"
|
||||
|
||||
+105
-11
@@ -13,6 +13,12 @@ const endpointMocks = vi.hoisted(() => ({
|
||||
getAwsRegions: vi.fn(),
|
||||
}))
|
||||
|
||||
const toastMocks = vi.hoisted(() => ({
|
||||
success: vi.fn(),
|
||||
warning: vi.fn(),
|
||||
error: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/api/endpoints', async () => {
|
||||
const actual = await vi.importActual<typeof import('@/api/endpoints/provider_oauth')>(
|
||||
'@/api/endpoints/provider_oauth',
|
||||
@@ -212,10 +218,7 @@ vi.mock('@/stores/proxy-nodes', () => ({
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/useToast', () => ({
|
||||
useToast: () => ({
|
||||
success: vi.fn(),
|
||||
error: vi.fn(),
|
||||
}),
|
||||
useToast: () => toastMocks,
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/useClipboard', () => ({
|
||||
@@ -302,6 +305,9 @@ describe('OAuthAccountDialog Grok import', () => {
|
||||
endpointMocks.startDeviceAuthorize.mockReset()
|
||||
endpointMocks.pollDeviceAuthorize.mockReset()
|
||||
endpointMocks.getAwsRegions.mockReset()
|
||||
toastMocks.success.mockReset()
|
||||
toastMocks.warning.mockReset()
|
||||
toastMocks.error.mockReset()
|
||||
|
||||
endpointMocks.importProviderRefreshToken.mockResolvedValue({
|
||||
provider_type: 'grok',
|
||||
@@ -438,7 +444,7 @@ describe('OAuthAccountDialog Grok import', () => {
|
||||
expect(endpointMocks.importProviderRefreshToken).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('shows a dedicated Codex Agent Identity mode and creates from a Session Token', async () => {
|
||||
it('shows a dedicated Codex Agent Identity mode and creates from an access token', async () => {
|
||||
const root = mountDialog('codex')
|
||||
await settle()
|
||||
|
||||
@@ -450,25 +456,113 @@ describe('OAuthAccountDialog Grok import', () => {
|
||||
await settle()
|
||||
|
||||
const textarea = root.querySelector<HTMLTextAreaElement>(
|
||||
'textarea[placeholder="粘贴 ChatGPT Session Token(JWT)"]',
|
||||
'textarea[placeholder="粘贴 AT 或 ChatGPT auth/session JSON"]',
|
||||
)
|
||||
expect(textarea).toBeTruthy()
|
||||
if (!textarea) throw new Error('Expected Agent Identity Session Token textarea to exist')
|
||||
textarea.value = 'session-token-for-test-only'
|
||||
if (!textarea) throw new Error('Expected Agent Identity access token textarea to exist')
|
||||
expect(textarea.classList.contains('min-h-[200px]')).toBe(true)
|
||||
expect(root.textContent).not.toContain('ChatGPT Session Token')
|
||||
expect(root.textContent).not.toContain('仅用于一次性注册')
|
||||
expect(root.textContent).not.toContain('创建并导入 Agent Identity')
|
||||
textarea.value = 'access-token-for-test-only'
|
||||
textarea.dispatchEvent(new Event('input'))
|
||||
await settle()
|
||||
|
||||
getExactButton(root, '创建并导入 Agent Identity')?.click()
|
||||
getExactButton(root, '创建')?.click()
|
||||
await settle()
|
||||
|
||||
expect(endpointMocks.importProviderRefreshToken).toHaveBeenCalledWith('provider-1', {
|
||||
session_token: 'session-token-for-test-only',
|
||||
create_agent_identity_from_session_token: true,
|
||||
access_token: 'access-token-for-test-only',
|
||||
create_agent_identity: true,
|
||||
proxy_node_id: undefined,
|
||||
})
|
||||
expect(endpointMocks.startBatchImportOAuthTask).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('extracts only accessToken from ChatGPT auth/session JSON for Agent Identity', async () => {
|
||||
const root = mountDialog('codex')
|
||||
await settle()
|
||||
|
||||
getExactButton(root, 'Agent Identity')?.click()
|
||||
await settle()
|
||||
|
||||
const textarea = root.querySelector<HTMLTextAreaElement>(
|
||||
'textarea[placeholder="粘贴 AT 或 ChatGPT auth/session JSON"]',
|
||||
)
|
||||
if (!textarea) throw new Error('Expected Agent Identity access token textarea to exist')
|
||||
textarea.value = JSON.stringify({
|
||||
WARNING_BANNER: 'sensitive session data',
|
||||
accessToken: 'access-token-from-json',
|
||||
sessionToken: 'session-token-must-not-be-used',
|
||||
user: { email: '[email protected]' },
|
||||
})
|
||||
textarea.dispatchEvent(new Event('input'))
|
||||
await settle()
|
||||
|
||||
getExactButton(root, '创建')?.click()
|
||||
await settle()
|
||||
|
||||
expect(endpointMocks.importProviderRefreshToken).toHaveBeenCalledWith('provider-1', {
|
||||
access_token: 'access-token-from-json',
|
||||
create_agent_identity: true,
|
||||
proxy_node_id: undefined,
|
||||
})
|
||||
expect(endpointMocks.importProviderRefreshToken).not.toHaveBeenCalledWith(
|
||||
'provider-1',
|
||||
expect.objectContaining({ session_token: 'session-token-must-not-be-used' }),
|
||||
)
|
||||
})
|
||||
|
||||
it('does not use sessionToken when ChatGPT auth/session JSON has no accessToken', async () => {
|
||||
const root = mountDialog('codex')
|
||||
await settle()
|
||||
|
||||
getExactButton(root, 'Agent Identity')?.click()
|
||||
await settle()
|
||||
|
||||
const textarea = root.querySelector<HTMLTextAreaElement>(
|
||||
'textarea[placeholder="粘贴 AT 或 ChatGPT auth/session JSON"]',
|
||||
)
|
||||
if (!textarea) throw new Error('Expected Agent Identity access token textarea to exist')
|
||||
textarea.value = JSON.stringify({ sessionToken: 'session-token-must-not-be-used' })
|
||||
textarea.dispatchEvent(new Event('input'))
|
||||
await settle()
|
||||
|
||||
getExactButton(root, '创建')?.click()
|
||||
await settle()
|
||||
|
||||
expect(endpointMocks.importProviderRefreshToken).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('treats a saved Agent Identity with pending task initialization as accepted', async () => {
|
||||
endpointMocks.importProviderRefreshToken.mockResolvedValueOnce({
|
||||
key_id: 'key-agent',
|
||||
provider_type: 'codex',
|
||||
has_refresh_token: false,
|
||||
task_ready: false,
|
||||
recoverable: true,
|
||||
detail: 'pending task',
|
||||
})
|
||||
const root = mountDialog('codex')
|
||||
await settle()
|
||||
|
||||
getExactButton(root, 'Agent Identity')?.click()
|
||||
await settle()
|
||||
const textarea = root.querySelector<HTMLTextAreaElement>(
|
||||
'textarea[placeholder="粘贴 AT 或 ChatGPT auth/session JSON"]',
|
||||
)
|
||||
if (!textarea) throw new Error('Expected Agent Identity access token textarea to exist')
|
||||
textarea.value = 'access-token-for-pending-task'
|
||||
textarea.dispatchEvent(new Event('input'))
|
||||
await settle()
|
||||
|
||||
getExactButton(root, '创建')?.click()
|
||||
await settle()
|
||||
|
||||
expect(toastMocks.warning).toHaveBeenCalled()
|
||||
expect(toastMocks.error).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('keeps Agent Identity creation unavailable for non-Codex providers', async () => {
|
||||
const root = mountDialog('openai')
|
||||
await settle()
|
||||
|
||||
@@ -140,4 +140,18 @@ describe('ProviderKeyIdentityBlock', () => {
|
||||
|
||||
unmount()
|
||||
})
|
||||
|
||||
it('does not offer generic copy or export actions for Agent Identity', () => {
|
||||
const { root, unmount } = mount({
|
||||
apiKey: createProviderKey({ agent_identity: true }),
|
||||
maskedSecretLabel: '[Agent Identity]',
|
||||
canExportCredential: false,
|
||||
})
|
||||
|
||||
expect(root.textContent).toContain('[Agent Identity]')
|
||||
expect(root.querySelector('button[title="下载 OAuth 授权文件"]')).toBeNull()
|
||||
expect(root.querySelector('button[title="复制密钥"]')).toBeNull()
|
||||
|
||||
unmount()
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user