mirror of
https://github.com/zyycn/codex-proxy-rs.git
synced 2026-11-06 08:06:19 +00:00
feat(accounts): support mixed CPR batch import
按导出文档中的 provider 分流批量导入 OpenAI 与 xAI 账号。\n\nCo-authored-by: Codex <noreply@openai.com>
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
import type { useAccountOnboarding } from '../composables/useAccountOnboarding'
|
||||
import type { AccountRow } from '../constants'
|
||||
import { Openai, Xai } from '@boxicons/vue'
|
||||
import { Copy, KeyRound, Upload } from '@lucide/vue'
|
||||
import { Copy, Grid2X2, KeyRound, Upload } from '@lucide/vue'
|
||||
|
||||
import { useClipboard, useFileDialog } from '@vueuse/core'
|
||||
import { computed, ref } from 'vue'
|
||||
@@ -51,7 +51,7 @@ const { open: openImportFile, onChange: onImportFileChange } = useFileDialog({
|
||||
})
|
||||
|
||||
const modeOptions = computed(() => accountProviderModeOptions(form.value.provider))
|
||||
const isProviderSelected = computed(() => form.value.provider === 'openai' || form.value.provider === 'xai')
|
||||
const isProviderSelected = computed(() => ['openai', 'xai', 'batch'].includes(form.value.provider))
|
||||
const isChoosingProvider = computed(() => !props.reauthorizing && !isProviderSelected.value)
|
||||
|
||||
const provider = computed({
|
||||
@@ -91,10 +91,17 @@ const oauthCallback = computed({
|
||||
|
||||
const oauthAuthUrl = computed(() => form.value.oauthAuthUrl || '')
|
||||
const isXai = computed(() => form.value.provider === 'xai')
|
||||
const importFileLabel = computed(() => mode.value === 'agent_identity' ? 'Agent 身份文件' : '账号文件')
|
||||
const isBatch = computed(() => form.value.provider === 'batch')
|
||||
const importFileLabel = computed(() => {
|
||||
if (isBatch.value)
|
||||
return '批量账号文件'
|
||||
return mode.value === 'agent_identity' ? 'Agent 身份文件' : '账号文件'
|
||||
})
|
||||
const importFilePlaceholder = computed(() => mode.value === 'agent_identity'
|
||||
? '粘贴 Agent 身份文件内容'
|
||||
: '粘贴 CPR、Sub2API 或 CPA 账号文件内容')
|
||||
: isBatch.value
|
||||
? '粘贴 CPR 多平台导出文件内容'
|
||||
: '粘贴 CPR、Sub2API 或 CPA 账号文件内容')
|
||||
|
||||
const accountName = computed(() => {
|
||||
return props.account?.email || props.account?.accountId || props.account?.id || '该账号'
|
||||
@@ -140,6 +147,8 @@ const description = computed<string | undefined>(() => {
|
||||
return undefined
|
||||
if (props.reauthorizing)
|
||||
return '完成授权后粘贴回调地址,系统会更新账号凭据'
|
||||
if (isBatch.value)
|
||||
return '导入 CPR 多平台账号包,系统会按文档中的平台自动分流'
|
||||
if (isXai.value) {
|
||||
return mode.value === 'oauth'
|
||||
? '复制 xAI 授权链接,完成后粘贴回调地址,不使用 xAI API Key'
|
||||
@@ -152,7 +161,7 @@ const description = computed<string | undefined>(() => {
|
||||
return '导入 CPR、Sub2API 或 CPA 账号文件,已存在账号会更新'
|
||||
})
|
||||
|
||||
function selectProvider(value: 'openai' | 'xai') {
|
||||
function selectProvider(value: 'openai' | 'xai' | 'batch') {
|
||||
provider.value = value
|
||||
}
|
||||
|
||||
@@ -198,7 +207,8 @@ async function copyText(value: string, successText: string) {
|
||||
:hide-footer="isChoosingProvider"
|
||||
>
|
||||
<template #icon>
|
||||
<Xai v-if="isXai" class="text-(--cp-text-primary)" aria-hidden="true" :width="20" :height="20" />
|
||||
<Grid2X2 v-if="isBatch" class="text-(--cp-text-primary)" aria-hidden="true" :width="20" :height="20" />
|
||||
<Xai v-else-if="isXai" class="text-(--cp-text-primary)" aria-hidden="true" :width="20" :height="20" />
|
||||
<Openai v-else class="text-(--cp-text-primary)" aria-hidden="true" :width="20" :height="20" />
|
||||
</template>
|
||||
|
||||
@@ -209,7 +219,12 @@ async function copyText(value: string, successText: string) {
|
||||
/>
|
||||
|
||||
<div v-else class="flex flex-col gap-4">
|
||||
<BaseSegmented v-if="!reauthorizing" v-model="mode" :options="modeOptions" class="w-full" />
|
||||
<BaseSegmented
|
||||
v-if="!reauthorizing && !isBatch"
|
||||
v-model="mode"
|
||||
:options="modeOptions"
|
||||
class="w-full"
|
||||
/>
|
||||
|
||||
<div v-if="mode === 'oauth'" class="flex flex-col gap-4">
|
||||
<div class="rounded-(--cp-input-radius-base) bg-(--cp-bg-subtle) px-4 py-3">
|
||||
@@ -316,7 +331,7 @@ async function copyText(value: string, successText: string) {
|
||||
:disabled="!canSubmit"
|
||||
@click="emit('create')"
|
||||
>
|
||||
{{ reauthorizing ? '完成重新授权' : mode === 'oauth' ? '完成授权导入' : '导入' }}
|
||||
{{ reauthorizing ? '完成重新授权' : mode === 'oauth' ? '完成授权导入' : isBatch ? '批量导入' : '导入' }}
|
||||
</BaseButton>
|
||||
</template>
|
||||
</BaseModal>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { Openai, Xai } from '@boxicons/vue'
|
||||
import { Grid2X2 } from '@lucide/vue'
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
@@ -11,7 +12,7 @@ withDefaults(
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
select: [provider: 'openai' | 'xai']
|
||||
select: [provider: 'openai' | 'xai' | 'batch']
|
||||
}>()
|
||||
|
||||
const providers = [
|
||||
@@ -25,6 +26,11 @@ const providers = [
|
||||
label: 'xAI',
|
||||
icon: Xai,
|
||||
},
|
||||
{
|
||||
value: 'batch' as const,
|
||||
label: '批量导入',
|
||||
icon: Grid2X2,
|
||||
},
|
||||
]
|
||||
</script>
|
||||
|
||||
|
||||
@@ -11,6 +11,12 @@ import { toast } from '@/components/base/BaseToast'
|
||||
import { useAsyncAction } from '@/composables/useAsyncAction'
|
||||
|
||||
type AccountRow = Awaited<ReturnType<typeof getAccounts>>['items'][number]
|
||||
type ImportProvider = 'openai' | 'xai'
|
||||
|
||||
interface MixedImportDocument {
|
||||
provider: ImportProvider
|
||||
document: Record<string, unknown>
|
||||
}
|
||||
|
||||
export function useAccountOnboarding(options: {
|
||||
reload: () => Promise<unknown>
|
||||
@@ -46,10 +52,10 @@ export function useAccountOnboarding(options: {
|
||||
|
||||
await creatingAccountAction.run(
|
||||
async () => {
|
||||
await importAccountDocument()
|
||||
await finishCreate(
|
||||
createForm.value.provider === 'xai' ? 'xAI OAuth 账号已导入' : 'OpenAI 账号已导入',
|
||||
)
|
||||
const message = createForm.value.provider === 'batch'
|
||||
? await importMixedAccountDocument()
|
||||
: await importAccountDocument()
|
||||
await finishCreate(message)
|
||||
},
|
||||
{ errorText: '导入失败' },
|
||||
)
|
||||
@@ -170,6 +176,41 @@ export function useAccountOnboarding(options: {
|
||||
data,
|
||||
}))
|
||||
commitConfigRevision(result.configRevision)
|
||||
return createForm.value.provider === 'xai' ? 'xAI 账号已导入' : 'OpenAI 账号已导入'
|
||||
}
|
||||
|
||||
async function importMixedAccountDocument() {
|
||||
const documents = parseMixedImportDocuments(parseImportJson(createForm.value.importText))
|
||||
let importedCount = 0
|
||||
const failures: string[] = []
|
||||
|
||||
for (const entry of documents) {
|
||||
try {
|
||||
const expectedConfigRevision = await requireConfigRevision()
|
||||
const result = await withConflictRefresh(() => importAccounts({
|
||||
provider: entry.provider,
|
||||
expectedConfigRevision,
|
||||
data: entry.document,
|
||||
}))
|
||||
commitConfigRevision(result.configRevision)
|
||||
importedCount += result.importedCount
|
||||
}
|
||||
catch (error) {
|
||||
const message = error instanceof Error && error.message
|
||||
? error.message
|
||||
: '导入失败'
|
||||
failures.push(`${entry.provider === 'xai' ? 'xAI' : 'OpenAI'}:${message}`)
|
||||
}
|
||||
}
|
||||
|
||||
if (importedCount === 0) {
|
||||
throw new Error(failures.length > 0 ? `批量导入失败:${failures.join(';')}` : '批量文件没有可导入的账号')
|
||||
}
|
||||
|
||||
if (failures.length > 0) {
|
||||
return `已导入 ${importedCount} 个账号,${failures.length} 个文档失败:${failures.join(';')}`
|
||||
}
|
||||
return `已导入 ${importedCount} 个账号`
|
||||
}
|
||||
|
||||
async function finishCreate(message: string) {
|
||||
@@ -195,7 +236,7 @@ export function useAccountOnboarding(options: {
|
||||
() => {
|
||||
createForm.value = {
|
||||
...createForm.value,
|
||||
mode: 'oauth',
|
||||
mode: createForm.value.provider === 'batch' ? 'json' : 'oauth',
|
||||
oauthFlowId: '',
|
||||
oauthAuthUrl: '',
|
||||
oauthCallback: '',
|
||||
@@ -255,3 +296,28 @@ function parseImportJson(value: string) {
|
||||
throw new Error('JSON 格式不正确')
|
||||
}
|
||||
}
|
||||
|
||||
function parseMixedImportDocuments(value: unknown): MixedImportDocument[] {
|
||||
if (!isRecord(value) || !Array.isArray(value.documents))
|
||||
throw new Error('批量导入文件必须是 CPR 多平台导出文件')
|
||||
|
||||
const documents: MixedImportDocument[] = []
|
||||
for (const entry of value.documents) {
|
||||
if (!isRecord(entry))
|
||||
throw new Error('批量导入文件包含无效的 Provider 文档')
|
||||
const provider = entry.provider
|
||||
if (provider !== 'openai' && provider !== 'xai')
|
||||
throw new Error('批量导入文件包含无效的 Provider 文档')
|
||||
if (!isRecord(entry.document))
|
||||
throw new Error('批量导入文件包含无效的 Provider 文档')
|
||||
documents.push({ provider, document: entry.document })
|
||||
}
|
||||
|
||||
if (documents.length === 0)
|
||||
throw new Error('批量文件没有可导入的账号文档')
|
||||
return documents
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user