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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user