feat(sora): 对齐 Sora OAuth 流程并隔离网关请求路径

- 新增并接通 Sora 专用 OAuth 接口与 ST/RT 换取能力
- 完成前端 Sora 授权、RT/ST 手动导入与账号创建流程
- 强化 Sora token 恢复、转发日志与网关路由隔离行为
- 补充后端服务层与路由层相关测试覆盖

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
yangjianbo
2026-02-19 08:02:56 +08:00
parent 36bb327024
commit 900cce20a1
39 changed files with 2561 additions and 283 deletions

View File

@@ -109,6 +109,28 @@
</svg>
OpenAI
</button>
<button
type="button"
@click="form.platform = 'sora'"
:class="[
'flex flex-1 items-center justify-center gap-2 rounded-md px-4 py-2.5 text-sm font-medium transition-all',
form.platform === 'sora'
? 'bg-white text-rose-600 shadow-sm dark:bg-dark-600 dark:text-rose-400'
: 'text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-200'
]"
>
<svg
class="h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" />
<path stroke-linecap="round" stroke-linejoin="round" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
Sora
</button>
<button
type="button"
@click="form.platform = 'gemini'"
@@ -150,6 +172,38 @@
</div>
</div>
<!-- Account Type Selection (Sora) -->
<div v-if="form.platform === 'sora'">
<label class="input-label">{{ t('admin.accounts.accountType') }}</label>
<div class="mt-2 grid grid-cols-1 gap-3" data-tour="account-form-type">
<button
type="button"
@click="accountCategory = 'oauth-based'"
:class="[
'flex items-center gap-3 rounded-lg border-2 p-3 text-left transition-all',
accountCategory === 'oauth-based'
? 'border-rose-500 bg-rose-50 dark:bg-rose-900/20'
: 'border-gray-200 hover:border-rose-300 dark:border-dark-600 dark:hover:border-rose-700'
]"
>
<div
:class="[
'flex h-8 w-8 shrink-0 items-center justify-center rounded-lg',
accountCategory === 'oauth-based'
? 'bg-rose-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">OAuth</span>
<span class="text-xs text-gray-500 dark:text-gray-400">{{ t('admin.accounts.types.chatgptOauth') }}</span>
</div>
</button>
</div>
</div>
<!-- Account Type Selection (Anthropic) -->
<div v-if="form.platform === 'anthropic'">
<label class="input-label">{{ t('admin.accounts.accountType') }}</label>
@@ -1747,32 +1801,6 @@
<!-- Step 2: OAuth Authorization -->
<div v-else class="space-y-5">
<!-- 同时启用 Sora 开关 ( OpenAI OAuth) -->
<div v-if="form.platform === 'openai' && accountCategory === 'oauth-based'" class="mb-4">
<label class="flex items-center justify-between rounded-lg border border-gray-200 p-3 dark:border-dark-600">
<div class="flex items-center gap-3">
<div class="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-rose-100 text-rose-600 dark:bg-rose-900/30 dark:text-rose-400">
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" />
<path stroke-linecap="round" stroke-linejoin="round" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<div>
<span class="block text-sm font-medium text-gray-900 dark:text-white">
{{ t('admin.accounts.openai.enableSora') }}
</span>
<span class="text-xs text-gray-500 dark:text-gray-400">
{{ t('admin.accounts.openai.enableSoraHint') }}
</span>
</div>
</div>
<label :class="['switch', { 'switch-active': enableSoraOnOpenAIOAuth }]">
<input type="checkbox" v-model="enableSoraOnOpenAIOAuth" class="sr-only" />
<span class="switch-thumb"></span>
</label>
</label>
</div>
<OAuthAuthorizationFlow
ref="oauthFlowRef"
:add-method="form.platform === 'anthropic' ? addMethod : 'oauth'"
@@ -1781,15 +1809,17 @@
:loading="currentOAuthLoading"
:error="currentOAuthError"
:show-help="form.platform === 'anthropic'"
:show-proxy-warning="form.platform !== 'openai' && !!form.proxy_id"
:show-proxy-warning="form.platform !== 'openai' && form.platform !== 'sora' && !!form.proxy_id"
:allow-multiple="form.platform === 'anthropic'"
:show-cookie-option="form.platform === 'anthropic'"
:show-refresh-token-option="form.platform === 'openai' || form.platform === 'antigravity'"
:show-refresh-token-option="form.platform === 'openai' || form.platform === 'sora' || form.platform === 'antigravity'"
:show-session-token-option="form.platform === 'sora'"
:platform="form.platform"
:show-project-id="geminiOAuthType === 'code_assist'"
@generate-url="handleGenerateUrl"
@cookie-auth="handleCookieAuth"
@validate-refresh-token="handleValidateRefreshToken"
@validate-session-token="handleValidateSessionToken"
/>
</div>
@@ -2148,6 +2178,7 @@ interface OAuthFlowExposed {
projectId: string
sessionKey: string
refreshToken: string
sessionToken: string
inputMethod: AuthInputMethod
reset: () => void
}
@@ -2156,7 +2187,7 @@ const { t } = useI18n()
const authStore = useAuthStore()
const oauthStepTitle = computed(() => {
if (form.platform === 'openai') return t('admin.accounts.oauth.openai.title')
if (form.platform === 'openai' || form.platform === 'sora') 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')
return t('admin.accounts.oauth.title')
@@ -2164,13 +2195,13 @@ const oauthStepTitle = computed(() => {
// Platform-specific hints for API Key type
const baseUrlHint = computed(() => {
if (form.platform === 'openai') return t('admin.accounts.openai.baseUrlHint')
if (form.platform === 'openai' || form.platform === 'sora') return t('admin.accounts.openai.baseUrlHint')
if (form.platform === 'gemini') return t('admin.accounts.gemini.baseUrlHint')
return t('admin.accounts.baseUrlHint')
})
const apiKeyHint = computed(() => {
if (form.platform === 'openai') return t('admin.accounts.openai.apiKeyHint')
if (form.platform === 'openai' || form.platform === 'sora') return t('admin.accounts.openai.apiKeyHint')
if (form.platform === 'gemini') return t('admin.accounts.gemini.apiKeyHint')
return t('admin.accounts.apiKeyHint')
})
@@ -2191,34 +2222,36 @@ const appStore = useAppStore()
// OAuth composables
const oauth = useAccountOAuth() // For Anthropic OAuth
const openaiOAuth = useOpenAIOAuth() // For OpenAI OAuth
const openaiOAuth = useOpenAIOAuth({ platform: 'openai' }) // For OpenAI OAuth
const soraOAuth = useOpenAIOAuth({ platform: 'sora' }) // For Sora OAuth
const geminiOAuth = useGeminiOAuth() // For Gemini OAuth
const antigravityOAuth = useAntigravityOAuth() // For Antigravity OAuth
const activeOpenAIOAuth = computed(() => (form.platform === 'sora' ? soraOAuth : openaiOAuth))
// Computed: current OAuth state for template binding
const currentAuthUrl = computed(() => {
if (form.platform === 'openai') return openaiOAuth.authUrl.value
if (form.platform === 'openai' || form.platform === 'sora') return activeOpenAIOAuth.value.authUrl.value
if (form.platform === 'gemini') return geminiOAuth.authUrl.value
if (form.platform === 'antigravity') return antigravityOAuth.authUrl.value
return oauth.authUrl.value
})
const currentSessionId = computed(() => {
if (form.platform === 'openai') return openaiOAuth.sessionId.value
if (form.platform === 'openai' || form.platform === 'sora') return activeOpenAIOAuth.value.sessionId.value
if (form.platform === 'gemini') return geminiOAuth.sessionId.value
if (form.platform === 'antigravity') return antigravityOAuth.sessionId.value
return oauth.sessionId.value
})
const currentOAuthLoading = computed(() => {
if (form.platform === 'openai') return openaiOAuth.loading.value
if (form.platform === 'openai' || form.platform === 'sora') return activeOpenAIOAuth.value.loading.value
if (form.platform === 'gemini') return geminiOAuth.loading.value
if (form.platform === 'antigravity') return antigravityOAuth.loading.value
return oauth.loading.value
})
const currentOAuthError = computed(() => {
if (form.platform === 'openai') return openaiOAuth.error.value
if (form.platform === 'openai' || form.platform === 'sora') return activeOpenAIOAuth.value.error.value
if (form.platform === 'gemini') return geminiOAuth.error.value
if (form.platform === 'antigravity') return antigravityOAuth.error.value
return oauth.error.value
@@ -2257,7 +2290,6 @@ const interceptWarmupRequests = ref(false)
const autoPauseOnExpired = ref(true)
const openaiPassthroughEnabled = ref(false)
const codexCLIOnlyEnabled = ref(false)
const enableSoraOnOpenAIOAuth = ref(false) // OpenAI OAuth 时同时启用 Sora
const mixedScheduling = ref(false) // For antigravity accounts: enable mixed scheduling
const antigravityAccountType = ref<'oauth' | 'upstream'>('oauth') // For antigravity: oauth or upstream
const upstreamBaseUrl = ref('') // For upstream type: base URL
@@ -2398,8 +2430,8 @@ const expiresAtInput = computed({
const canExchangeCode = computed(() => {
const authCode = oauthFlowRef.value?.authCode || ''
if (form.platform === 'openai') {
return authCode.trim() && openaiOAuth.sessionId.value && !openaiOAuth.loading.value
if (form.platform === 'openai' || form.platform === 'sora') {
return authCode.trim() && activeOpenAIOAuth.value.sessionId.value && !activeOpenAIOAuth.value.loading.value
}
if (form.platform === 'gemini') {
return authCode.trim() && geminiOAuth.sessionId.value && !geminiOAuth.loading.value
@@ -2459,7 +2491,7 @@ watch(
(newPlatform) => {
// Reset base URL based on platform
apiKeyBaseUrl.value =
newPlatform === 'openai'
(newPlatform === 'openai' || newPlatform === 'sora')
? 'https://api.openai.com'
: newPlatform === 'gemini'
? 'https://generativelanguage.googleapis.com'
@@ -2485,6 +2517,11 @@ watch(
if (newPlatform !== 'anthropic') {
interceptWarmupRequests.value = false
}
if (newPlatform === 'sora') {
accountCategory.value = 'oauth-based'
addMethod.value = 'oauth'
form.type = 'oauth'
}
if (newPlatform !== 'openai') {
openaiPassthroughEnabled.value = false
codexCLIOnlyEnabled.value = false
@@ -2492,6 +2529,7 @@ watch(
// Reset OAuth states
oauth.resetState()
openaiOAuth.resetState()
soraOAuth.resetState()
geminiOAuth.resetState()
antigravityOAuth.resetState()
}
@@ -2753,7 +2791,6 @@ const resetForm = () => {
autoPauseOnExpired.value = true
openaiPassthroughEnabled.value = false
codexCLIOnlyEnabled.value = false
enableSoraOnOpenAIOAuth.value = false
// Reset quota control state
windowCostEnabled.value = false
windowCostLimit.value = null
@@ -2776,6 +2813,7 @@ const resetForm = () => {
geminiTierAIStudio.value = 'aistudio_free'
oauth.resetState()
openaiOAuth.resetState()
soraOAuth.resetState()
geminiOAuth.resetState()
antigravityOAuth.resetState()
oauthFlowRef.value?.reset()
@@ -2807,6 +2845,23 @@ const buildOpenAIExtra = (base?: Record<string, unknown>): Record<string, unknow
return Object.keys(extra).length > 0 ? extra : undefined
}
const buildSoraExtra = (
base?: Record<string, unknown>,
linkedOpenAIAccountId?: string | number
): Record<string, unknown> | undefined => {
const extra: Record<string, unknown> = { ...(base || {}) }
if (linkedOpenAIAccountId !== undefined && linkedOpenAIAccountId !== null) {
const id = String(linkedOpenAIAccountId).trim()
if (id) {
extra.linked_openai_account_id = id
}
}
delete extra.openai_passthrough
delete extra.openai_oauth_passthrough
delete extra.codex_cli_only
return Object.keys(extra).length > 0 ? extra : undefined
}
// Helper function to create account with mixed channel warning handling
const doCreateAccount = async (payload: any) => {
submitting.value = true
@@ -2922,7 +2977,7 @@ const handleSubmit = async () => {
// Determine default base URL based on platform
const defaultBaseUrl =
form.platform === 'openai'
(form.platform === 'openai' || form.platform === 'sora')
? 'https://api.openai.com'
: form.platform === 'gemini'
? 'https://generativelanguage.googleapis.com'
@@ -2974,14 +3029,15 @@ const goBackToBasicInfo = () => {
step.value = 1
oauth.resetState()
openaiOAuth.resetState()
soraOAuth.resetState()
geminiOAuth.resetState()
antigravityOAuth.resetState()
oauthFlowRef.value?.reset()
}
const handleGenerateUrl = async () => {
if (form.platform === 'openai') {
await openaiOAuth.generateAuthUrl(form.proxy_id)
if (form.platform === 'openai' || form.platform === 'sora') {
await activeOpenAIOAuth.value.generateAuthUrl(form.proxy_id)
} else if (form.platform === 'gemini') {
await geminiOAuth.generateAuthUrl(
form.proxy_id,
@@ -2997,13 +3053,19 @@ const handleGenerateUrl = async () => {
}
const handleValidateRefreshToken = (rt: string) => {
if (form.platform === 'openai') {
if (form.platform === 'openai' || form.platform === 'sora') {
handleOpenAIValidateRT(rt)
} else if (form.platform === 'antigravity') {
handleAntigravityValidateRT(rt)
}
}
const handleValidateSessionToken = (sessionToken: string) => {
if (form.platform === 'sora') {
handleSoraValidateST(sessionToken)
}
}
const formatDateTimeLocal = formatDateTimeLocalInput
const parseDateTimeLocal = parseDateTimeLocalInput
@@ -3039,100 +3101,101 @@ const createAccountAndFinish = async (
// OpenAI OAuth 授权码兑换
const handleOpenAIExchange = async (authCode: string) => {
if (!authCode.trim() || !openaiOAuth.sessionId.value) return
const oauthClient = activeOpenAIOAuth.value
if (!authCode.trim() || !oauthClient.sessionId.value) return
openaiOAuth.loading.value = true
openaiOAuth.error.value = ''
oauthClient.loading.value = true
oauthClient.error.value = ''
try {
const tokenInfo = await openaiOAuth.exchangeAuthCode(
const stateToUse = (oauthFlowRef.value?.oauthState || oauthClient.oauthState.value || '').trim()
if (!stateToUse) {
oauthClient.error.value = t('admin.accounts.oauth.authFailed')
appStore.showError(oauthClient.error.value)
return
}
const tokenInfo = await oauthClient.exchangeAuthCode(
authCode.trim(),
openaiOAuth.sessionId.value,
oauthClient.sessionId.value,
stateToUse,
form.proxy_id
)
if (!tokenInfo) return
const credentials = openaiOAuth.buildCredentials(tokenInfo)
const oauthExtra = openaiOAuth.buildExtraInfo(tokenInfo) as Record<string, unknown> | undefined
const credentials = oauthClient.buildCredentials(tokenInfo)
const oauthExtra = oauthClient.buildExtraInfo(tokenInfo) as Record<string, unknown> | undefined
const extra = buildOpenAIExtra(oauthExtra)
const shouldCreateOpenAI = form.platform === 'openai'
const shouldCreateSora = form.platform === 'sora'
// 应用临时不可调度配置
if (!applyTempUnschedConfig(credentials)) {
return
}
// 1. 创建 OpenAI 账号
const openaiAccount = await adminAPI.accounts.create({
name: form.name,
notes: form.notes,
platform: 'openai',
type: 'oauth',
credentials,
extra,
proxy_id: form.proxy_id,
concurrency: form.concurrency,
priority: form.priority,
rate_multiplier: form.rate_multiplier,
group_ids: form.group_ids,
expires_at: form.expires_at,
auto_pause_on_expired: autoPauseOnExpired.value
})
let openaiAccountId: string | number | undefined
appStore.showSuccess(t('admin.accounts.accountCreated'))
if (shouldCreateOpenAI) {
const openaiAccount = await adminAPI.accounts.create({
name: form.name,
notes: form.notes,
platform: 'openai',
type: 'oauth',
credentials,
extra,
proxy_id: form.proxy_id,
concurrency: form.concurrency,
priority: form.priority,
rate_multiplier: form.rate_multiplier,
group_ids: form.group_ids,
expires_at: form.expires_at,
auto_pause_on_expired: autoPauseOnExpired.value
})
openaiAccountId = openaiAccount.id
appStore.showSuccess(t('admin.accounts.accountCreated'))
}
// 2. 如果启用了 Sora同时创建 Sora 账号
if (enableSoraOnOpenAIOAuth.value) {
try {
// Sora 使用相同的 OAuth credentials
const soraCredentials = {
access_token: credentials.access_token,
refresh_token: credentials.refresh_token,
expires_at: credentials.expires_at
}
// 建立关联关系
const soraExtra: Record<string, unknown> = {
...(extra || {}),
linked_openai_account_id: String(openaiAccount.id)
}
delete soraExtra.openai_passthrough
delete soraExtra.openai_oauth_passthrough
await adminAPI.accounts.create({
name: `${form.name} (Sora)`,
notes: form.notes,
platform: 'sora',
type: 'oauth',
credentials: soraCredentials,
extra: soraExtra,
proxy_id: form.proxy_id,
concurrency: form.concurrency,
priority: form.priority,
rate_multiplier: form.rate_multiplier,
group_ids: form.group_ids,
expires_at: form.expires_at,
auto_pause_on_expired: autoPauseOnExpired.value
})
appStore.showSuccess(t('admin.accounts.soraAccountCreated'))
} catch (error: any) {
console.error('创建 Sora 账号失败:', error)
appStore.showWarning(t('admin.accounts.soraAccountFailed'))
if (shouldCreateSora) {
const soraCredentials = {
access_token: credentials.access_token,
refresh_token: credentials.refresh_token,
expires_at: credentials.expires_at
}
const soraName = shouldCreateOpenAI ? `${form.name} (Sora)` : form.name
const soraExtra = buildSoraExtra(shouldCreateOpenAI ? extra : oauthExtra, openaiAccountId)
await adminAPI.accounts.create({
name: soraName,
notes: form.notes,
platform: 'sora',
type: 'oauth',
credentials: soraCredentials,
extra: soraExtra,
proxy_id: form.proxy_id,
concurrency: form.concurrency,
priority: form.priority,
rate_multiplier: form.rate_multiplier,
group_ids: form.group_ids,
expires_at: form.expires_at,
auto_pause_on_expired: autoPauseOnExpired.value
})
appStore.showSuccess(t('admin.accounts.accountCreated'))
}
emit('created')
handleClose()
} catch (error: any) {
openaiOAuth.error.value = error.response?.data?.detail || t('admin.accounts.oauth.authFailed')
appStore.showError(openaiOAuth.error.value)
oauthClient.error.value = error.response?.data?.detail || t('admin.accounts.oauth.authFailed')
appStore.showError(oauthClient.error.value)
} finally {
openaiOAuth.loading.value = false
oauthClient.loading.value = false
}
}
// OpenAI 手动 RT 批量验证和创建
const handleOpenAIValidateRT = async (refreshTokenInput: string) => {
const oauthClient = activeOpenAIOAuth.value
if (!refreshTokenInput.trim()) return
// Parse multiple refresh tokens (one per line)
@@ -3142,53 +3205,86 @@ const handleOpenAIValidateRT = async (refreshTokenInput: string) => {
.filter((rt) => rt)
if (refreshTokens.length === 0) {
openaiOAuth.error.value = t('admin.accounts.oauth.openai.pleaseEnterRefreshToken')
oauthClient.error.value = t('admin.accounts.oauth.openai.pleaseEnterRefreshToken')
return
}
openaiOAuth.loading.value = true
openaiOAuth.error.value = ''
oauthClient.loading.value = true
oauthClient.error.value = ''
let successCount = 0
let failedCount = 0
const errors: string[] = []
const shouldCreateOpenAI = form.platform === 'openai'
const shouldCreateSora = form.platform === 'sora'
try {
for (let i = 0; i < refreshTokens.length; i++) {
try {
const tokenInfo = await openaiOAuth.validateRefreshToken(
const tokenInfo = await oauthClient.validateRefreshToken(
refreshTokens[i],
form.proxy_id
)
if (!tokenInfo) {
failedCount++
errors.push(`#${i + 1}: ${openaiOAuth.error.value || 'Validation failed'}`)
openaiOAuth.error.value = ''
errors.push(`#${i + 1}: ${oauthClient.error.value || 'Validation failed'}`)
oauthClient.error.value = ''
continue
}
const credentials = openaiOAuth.buildCredentials(tokenInfo)
const oauthExtra = openaiOAuth.buildExtraInfo(tokenInfo) as Record<string, unknown> | undefined
const credentials = oauthClient.buildCredentials(tokenInfo)
const oauthExtra = oauthClient.buildExtraInfo(tokenInfo) as Record<string, unknown> | undefined
const extra = buildOpenAIExtra(oauthExtra)
// Generate account name with index for batch
const accountName = refreshTokens.length > 1 ? `${form.name} #${i + 1}` : form.name
await adminAPI.accounts.create({
name: accountName,
notes: form.notes,
platform: 'openai',
type: 'oauth',
credentials,
extra,
proxy_id: form.proxy_id,
concurrency: form.concurrency,
priority: form.priority,
rate_multiplier: form.rate_multiplier,
group_ids: form.group_ids,
expires_at: form.expires_at,
auto_pause_on_expired: autoPauseOnExpired.value
})
let openaiAccountId: string | number | undefined
if (shouldCreateOpenAI) {
const openaiAccount = await adminAPI.accounts.create({
name: accountName,
notes: form.notes,
platform: 'openai',
type: 'oauth',
credentials,
extra,
proxy_id: form.proxy_id,
concurrency: form.concurrency,
priority: form.priority,
rate_multiplier: form.rate_multiplier,
group_ids: form.group_ids,
expires_at: form.expires_at,
auto_pause_on_expired: autoPauseOnExpired.value
})
openaiAccountId = openaiAccount.id
}
if (shouldCreateSora) {
const soraCredentials = {
access_token: credentials.access_token,
refresh_token: credentials.refresh_token,
expires_at: credentials.expires_at
}
const soraName = shouldCreateOpenAI ? `${accountName} (Sora)` : accountName
const soraExtra = buildSoraExtra(shouldCreateOpenAI ? extra : oauthExtra, openaiAccountId)
await adminAPI.accounts.create({
name: soraName,
notes: form.notes,
platform: 'sora',
type: 'oauth',
credentials: soraCredentials,
extra: soraExtra,
proxy_id: form.proxy_id,
concurrency: form.concurrency,
priority: form.priority,
rate_multiplier: form.rate_multiplier,
group_ids: form.group_ids,
expires_at: form.expires_at,
auto_pause_on_expired: autoPauseOnExpired.value
})
}
successCount++
} catch (error: any) {
failedCount++
@@ -3210,14 +3306,99 @@ const handleOpenAIValidateRT = async (refreshTokenInput: string) => {
appStore.showWarning(
t('admin.accounts.oauth.batchPartialSuccess', { success: successCount, failed: failedCount })
)
openaiOAuth.error.value = errors.join('\n')
oauthClient.error.value = errors.join('\n')
emit('created')
} else {
openaiOAuth.error.value = errors.join('\n')
oauthClient.error.value = errors.join('\n')
appStore.showError(t('admin.accounts.oauth.batchFailed'))
}
} finally {
openaiOAuth.loading.value = false
oauthClient.loading.value = false
}
}
// Sora 手动 ST 批量验证和创建
const handleSoraValidateST = async (sessionTokenInput: string) => {
const oauthClient = activeOpenAIOAuth.value
if (!sessionTokenInput.trim()) return
const sessionTokens = sessionTokenInput
.split('\n')
.map((st) => st.trim())
.filter((st) => st)
if (sessionTokens.length === 0) {
oauthClient.error.value = t('admin.accounts.oauth.openai.pleaseEnterSessionToken')
return
}
oauthClient.loading.value = true
oauthClient.error.value = ''
let successCount = 0
let failedCount = 0
const errors: string[] = []
try {
for (let i = 0; i < sessionTokens.length; i++) {
try {
const tokenInfo = await oauthClient.validateSessionToken(sessionTokens[i], form.proxy_id)
if (!tokenInfo) {
failedCount++
errors.push(`#${i + 1}: ${oauthClient.error.value || 'Validation failed'}`)
oauthClient.error.value = ''
continue
}
const credentials = oauthClient.buildCredentials(tokenInfo)
credentials.session_token = sessionTokens[i]
const oauthExtra = oauthClient.buildExtraInfo(tokenInfo) as Record<string, unknown> | undefined
const soraExtra = buildSoraExtra(oauthExtra)
const accountName = sessionTokens.length > 1 ? `${form.name} #${i + 1}` : form.name
await adminAPI.accounts.create({
name: accountName,
notes: form.notes,
platform: 'sora',
type: 'oauth',
credentials,
extra: soraExtra,
proxy_id: form.proxy_id,
concurrency: form.concurrency,
priority: form.priority,
rate_multiplier: form.rate_multiplier,
group_ids: form.group_ids,
expires_at: form.expires_at,
auto_pause_on_expired: autoPauseOnExpired.value
})
successCount++
} catch (error: any) {
failedCount++
const errMsg = error.response?.data?.detail || error.message || 'Unknown error'
errors.push(`#${i + 1}: ${errMsg}`)
}
}
if (successCount > 0 && failedCount === 0) {
appStore.showSuccess(
sessionTokens.length > 1
? t('admin.accounts.oauth.batchSuccess', { count: successCount })
: t('admin.accounts.accountCreated')
)
emit('created')
handleClose()
} else if (successCount > 0 && failedCount > 0) {
appStore.showWarning(
t('admin.accounts.oauth.batchPartialSuccess', { success: successCount, failed: failedCount })
)
oauthClient.error.value = errors.join('\n')
emit('created')
} else {
oauthClient.error.value = errors.join('\n')
appStore.showError(t('admin.accounts.oauth.batchFailed'))
}
} finally {
oauthClient.loading.value = false
}
}
@@ -3462,6 +3643,7 @@ const handleExchangeCode = async () => {
switch (form.platform) {
case 'openai':
case 'sora':
return handleOpenAIExchange(authCode)
case 'gemini':
return handleGeminiExchange(authCode)