From 26060e702f210d4c19115ea4559229927c67923f Mon Sep 17 00:00:00 2001 From: huangenjun <1021217094@qq.com> Date: Wed, 25 Feb 2026 11:33:07 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20Sora=20=E5=B9=B3=E5=8F=B0=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E6=89=8B=E5=8A=A8=E5=AF=BC=E5=85=A5=20Access=20Token?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 Access Token 输入方式,支持批量粘贴(每行一个)直接创建账号, 无需经过 OAuth 授权流程。 Co-Authored-By: Claude Opus 4.6 --- .../components/account/CreateAccountModal.vue | 79 +++++++++++++++++ .../account/OAuthAuthorizationFlow.vue | 87 ++++++++++++++++++- frontend/src/composables/useAccountOAuth.ts | 2 +- 3 files changed, 166 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/account/CreateAccountModal.vue b/frontend/src/components/account/CreateAccountModal.vue index 25100c82..72d74318 100644 --- a/frontend/src/components/account/CreateAccountModal.vue +++ b/frontend/src/components/account/CreateAccountModal.vue @@ -1816,12 +1816,14 @@ :show-cookie-option="form.platform === 'anthropic'" :show-refresh-token-option="form.platform === 'openai' || form.platform === 'sora' || form.platform === 'antigravity'" :show-session-token-option="form.platform === 'sora'" + :show-access-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" + @import-access-token="handleImportAccessToken" /> @@ -3188,6 +3190,83 @@ const handleValidateSessionToken = (sessionToken: string) => { } } +// Sora 手动 AT 批量导入 +const handleImportAccessToken = async (accessTokenInput: string) => { + const oauthClient = activeOpenAIOAuth.value + if (!accessTokenInput.trim()) return + + const accessTokens = accessTokenInput + .split('\n') + .map((at) => at.trim()) + .filter((at) => at) + + if (accessTokens.length === 0) { + oauthClient.error.value = 'Please enter at least one Access Token' + return + } + + oauthClient.loading.value = true + oauthClient.error.value = '' + + let successCount = 0 + let failedCount = 0 + const errors: string[] = [] + + try { + for (let i = 0; i < accessTokens.length; i++) { + try { + const credentials: Record = { + access_token: accessTokens[i], + } + const soraExtra = buildSoraExtra() + + const accountName = accessTokens.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( + accessTokens.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 + } +} + const formatDateTimeLocal = formatDateTimeLocalInput const parseDateTimeLocal = parseDateTimeLocalInput diff --git a/frontend/src/components/account/OAuthAuthorizationFlow.vue b/frontend/src/components/account/OAuthAuthorizationFlow.vue index 8e00d25b..94d417dc 100644 --- a/frontend/src/components/account/OAuthAuthorizationFlow.vue +++ b/frontend/src/components/account/OAuthAuthorizationFlow.vue @@ -59,6 +59,17 @@ t(getOAuthKey('sessionTokenAuth')) }} + @@ -227,6 +238,63 @@ + +
+
+

+ {{ t('admin.accounts.oauth.openai.accessTokenDesc', '直接粘贴 Access Token 创建账号,无需 OAuth 授权流程。支持批量导入(每行一个)。') }} +

+ +
+ + +

+ {{ t('admin.accounts.oauth.batchCreateAccounts', { count: parsedAccessTokenCount }) }} +

+
+ +
+

+ {{ error }} +

+
+ + +
+
+
(), { showCookieOption: true, showRefreshTokenOption: false, showSessionTokenOption: false, + showAccessTokenOption: false, platform: 'anthropic', showProjectId: true }) @@ -644,6 +714,7 @@ const emit = defineEmits<{ 'cookie-auth': [sessionKey: string] 'validate-refresh-token': [refreshToken: string] 'validate-session-token': [sessionToken: string] + 'import-access-token': [accessToken: string] 'update:inputMethod': [method: AuthInputMethod] }>() @@ -683,12 +754,13 @@ const authCodeInput = ref('') const sessionKeyInput = ref('') const refreshTokenInput = ref('') const sessionTokenInput = ref('') +const accessTokenInput = ref('') const showHelpDialog = ref(false) const oauthState = ref('') const projectId = ref('') // Computed: show method selection when either cookie or refresh token option is enabled -const showMethodSelection = computed(() => props.showCookieOption || props.showRefreshTokenOption || props.showSessionTokenOption) +const showMethodSelection = computed(() => props.showCookieOption || props.showRefreshTokenOption || props.showSessionTokenOption || props.showAccessTokenOption) // Clipboard const { copied, copyToClipboard } = useClipboard() @@ -716,6 +788,13 @@ const parsedSessionTokenCount = computed(() => { .filter((st) => st).length }) +const parsedAccessTokenCount = computed(() => { + return accessTokenInput.value + .split('\n') + .map((at) => at.trim()) + .filter((at) => at).length +}) + // Watchers watch(inputMethod, (newVal) => { emit('update:inputMethod', newVal) @@ -789,6 +868,12 @@ const handleValidateSessionToken = () => { } } +const handleImportAccessToken = () => { + if (accessTokenInput.value.trim()) { + emit('import-access-token', accessTokenInput.value.trim()) + } +} + // Expose methods and state defineExpose({ authCode: authCodeInput, diff --git a/frontend/src/composables/useAccountOAuth.ts b/frontend/src/composables/useAccountOAuth.ts index 6f53404c..b6f33186 100644 --- a/frontend/src/composables/useAccountOAuth.ts +++ b/frontend/src/composables/useAccountOAuth.ts @@ -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' | 'session_token' +export type AuthInputMethod = 'manual' | 'cookie' | 'refresh_token' | 'session_token' | 'access_token' export interface OAuthState { authUrl: string