Unverified Commit d757df8a authored by Wesley Liddick's avatar Wesley Liddick Committed by GitHub
Browse files

Merge pull request #1463 from touwaeriol/feat/remove-sora

revert: completely remove Sora platform
parents f585a15e 19655a15
package soraerror
import (
"net/http"
"testing"
"github.com/stretchr/testify/require"
)
func TestIsCloudflareChallengeResponse(t *testing.T) {
headers := make(http.Header)
headers.Set("cf-mitigated", "challenge")
require.True(t, IsCloudflareChallengeResponse(http.StatusForbidden, headers, []byte(`{"ok":false}`)))
require.True(t, IsCloudflareChallengeResponse(http.StatusTooManyRequests, nil, []byte(`<!DOCTYPE html><title>Just a moment...</title><script>window._cf_chl_opt={};</script>`)))
require.False(t, IsCloudflareChallengeResponse(http.StatusBadGateway, nil, []byte(`<!DOCTYPE html><title>Just a moment...</title>`)))
}
func TestExtractCloudflareRayID(t *testing.T) {
headers := make(http.Header)
headers.Set("cf-ray", "9d01b0e9ecc35829-SEA")
require.Equal(t, "9d01b0e9ecc35829-SEA", ExtractCloudflareRayID(headers, nil))
body := []byte(`<script>window._cf_chl_opt={cRay: '9cff2d62d83bb98d'};</script>`)
require.Equal(t, "9cff2d62d83bb98d", ExtractCloudflareRayID(nil, body))
}
func TestExtractUpstreamErrorCodeAndMessage(t *testing.T) {
code, msg := ExtractUpstreamErrorCodeAndMessage([]byte(`{"error":{"code":"cf_shield_429","message":"rate limited"}}`))
require.Equal(t, "cf_shield_429", code)
require.Equal(t, "rate limited", msg)
code, msg = ExtractUpstreamErrorCodeAndMessage([]byte(`{"code":"unsupported_country_code","message":"not available"}`))
require.Equal(t, "unsupported_country_code", code)
require.Equal(t, "not available", msg)
code, msg = ExtractUpstreamErrorCodeAndMessage([]byte(`plain text`))
require.Equal(t, "", code)
require.Equal(t, "plain text", msg)
}
func TestFormatCloudflareChallengeMessage(t *testing.T) {
headers := make(http.Header)
headers.Set("cf-ray", "9d03b68c086027a1-SEA")
msg := FormatCloudflareChallengeMessage("blocked", headers, nil)
require.Equal(t, "blocked (cf-ray: 9d03b68c086027a1-SEA)", msg)
}
...@@ -256,7 +256,6 @@ func shouldBypassEmbeddedFrontend(path string) bool { ...@@ -256,7 +256,6 @@ func shouldBypassEmbeddedFrontend(path string) bool {
return strings.HasPrefix(trimmed, "/api/") || return strings.HasPrefix(trimmed, "/api/") ||
strings.HasPrefix(trimmed, "/v1/") || strings.HasPrefix(trimmed, "/v1/") ||
strings.HasPrefix(trimmed, "/v1beta/") || strings.HasPrefix(trimmed, "/v1beta/") ||
strings.HasPrefix(trimmed, "/sora/") ||
strings.HasPrefix(trimmed, "/antigravity/") || strings.HasPrefix(trimmed, "/antigravity/") ||
strings.HasPrefix(trimmed, "/setup/") || strings.HasPrefix(trimmed, "/setup/") ||
trimmed == "/health" || trimmed == "/health" ||
......
...@@ -434,7 +434,6 @@ func TestFrontendServer_Middleware(t *testing.T) { ...@@ -434,7 +434,6 @@ func TestFrontendServer_Middleware(t *testing.T) {
"/api/v1/users", "/api/v1/users",
"/v1/models", "/v1/models",
"/v1beta/chat", "/v1beta/chat",
"/sora/v1/models",
"/antigravity/test", "/antigravity/test",
"/setup/init", "/setup/init",
"/health", "/health",
...@@ -637,7 +636,6 @@ func TestServeEmbeddedFrontend(t *testing.T) { ...@@ -637,7 +636,6 @@ func TestServeEmbeddedFrontend(t *testing.T) {
"/api/users", "/api/users",
"/v1/models", "/v1/models",
"/v1beta/chat", "/v1beta/chat",
"/sora/v1/models",
"/antigravity/test", "/antigravity/test",
"/setup/init", "/setup/init",
"/health", "/health",
......
-- Migration: 090_drop_sora
-- Remove all Sora-related database objects.
-- Drops tables: sora_tasks, sora_generations, sora_accounts
-- Drops columns from: groups, users, usage_logs
-- ============================================================
-- 1. Drop Sora tables
-- ============================================================
DROP TABLE IF EXISTS sora_tasks;
DROP TABLE IF EXISTS sora_generations;
DROP TABLE IF EXISTS sora_accounts;
-- ============================================================
-- 2. Drop Sora columns from groups table
-- ============================================================
ALTER TABLE groups
DROP COLUMN IF EXISTS sora_image_price_360,
DROP COLUMN IF EXISTS sora_image_price_540,
DROP COLUMN IF EXISTS sora_video_price_per_request,
DROP COLUMN IF EXISTS sora_video_price_per_request_hd,
DROP COLUMN IF EXISTS sora_storage_quota_bytes;
-- ============================================================
-- 3. Drop Sora columns from users table
-- ============================================================
ALTER TABLE users
DROP COLUMN IF EXISTS sora_storage_quota_bytes,
DROP COLUMN IF EXISTS sora_storage_used_bytes;
-- ============================================================
-- 4. Drop Sora column from usage_logs table
-- ============================================================
ALTER TABLE usage_logs
DROP COLUMN IF EXISTS media_type;
import { describe, expect, it } from 'vitest'
import {
normalizeGenerationListResponse,
normalizeModelFamiliesResponse
} from '../sora'
describe('sora api normalizers', () => {
it('normalizes generation list from data shape', () => {
const result = normalizeGenerationListResponse({
data: [{ id: 1, status: 'pending' }],
total: 9,
page: 2
})
expect(result.data).toHaveLength(1)
expect(result.total).toBe(9)
expect(result.page).toBe(2)
})
it('normalizes generation list from items shape', () => {
const result = normalizeGenerationListResponse({
items: [{ id: 1, status: 'completed' }],
total: 1
})
expect(result.data).toHaveLength(1)
expect(result.total).toBe(1)
expect(result.page).toBe(1)
})
it('falls back to empty generation list on invalid payload', () => {
const result = normalizeGenerationListResponse(null)
expect(result).toEqual({ data: [], total: 0, page: 1 })
})
it('normalizes family model payload', () => {
const result = normalizeModelFamiliesResponse({
data: [
{
id: 'sora2',
name: 'Sora 2',
type: 'video',
orientations: ['landscape', 'portrait'],
durations: [10, 15]
}
]
})
expect(result).toHaveLength(1)
expect(result[0].id).toBe('sora2')
expect(result[0].orientations).toEqual(['landscape', 'portrait'])
expect(result[0].durations).toEqual([10, 15])
})
it('normalizes legacy flat model list into families', () => {
const result = normalizeModelFamiliesResponse({
items: [
{ id: 'sora2-landscape-10s', type: 'video' },
{ id: 'sora2-portrait-15s', type: 'video' },
{ id: 'gpt-image-square', type: 'image' }
]
})
const sora2 = result.find((m) => m.id === 'sora2')
expect(sora2).toBeTruthy()
expect(sora2?.orientations).toEqual(['landscape', 'portrait'])
expect(sora2?.durations).toEqual([10, 15])
const image = result.find((m) => m.id === 'gpt-image')
expect(image).toBeTruthy()
expect(image?.type).toBe('image')
expect(image?.orientations).toEqual(['square'])
})
it('falls back to empty families on invalid payload', () => {
expect(normalizeModelFamiliesResponse(undefined)).toEqual([])
expect(normalizeModelFamiliesResponse({})).toEqual([])
})
})
...@@ -40,7 +40,6 @@ export interface SystemSettings { ...@@ -40,7 +40,6 @@ export interface SystemSettings {
hide_ccs_import_button: boolean hide_ccs_import_button: boolean
purchase_subscription_enabled: boolean purchase_subscription_enabled: boolean
purchase_subscription_url: string purchase_subscription_url: string
sora_client_enabled: boolean
backend_mode_enabled: boolean backend_mode_enabled: boolean
custom_menu_items: CustomMenuItem[] custom_menu_items: CustomMenuItem[]
custom_endpoints: CustomEndpoint[] custom_endpoints: CustomEndpoint[]
...@@ -114,7 +113,6 @@ export interface UpdateSettingsRequest { ...@@ -114,7 +113,6 @@ export interface UpdateSettingsRequest {
hide_ccs_import_button?: boolean hide_ccs_import_button?: boolean
purchase_subscription_enabled?: boolean purchase_subscription_enabled?: boolean
purchase_subscription_url?: string purchase_subscription_url?: string
sora_client_enabled?: boolean
backend_mode_enabled?: boolean backend_mode_enabled?: boolean
custom_menu_items?: CustomMenuItem[] custom_menu_items?: CustomMenuItem[]
custom_endpoints?: CustomEndpoint[] custom_endpoints?: CustomEndpoint[]
...@@ -394,142 +392,6 @@ export async function updateBetaPolicySettings( ...@@ -394,142 +392,6 @@ export async function updateBetaPolicySettings(
return data return data
} }
// ==================== Sora S3 Settings ====================
export interface SoraS3Settings {
enabled: boolean
endpoint: string
region: string
bucket: string
access_key_id: string
secret_access_key_configured: boolean
prefix: string
force_path_style: boolean
cdn_url: string
default_storage_quota_bytes: number
}
export interface SoraS3Profile {
profile_id: string
name: string
is_active: boolean
enabled: boolean
endpoint: string
region: string
bucket: string
access_key_id: string
secret_access_key_configured: boolean
prefix: string
force_path_style: boolean
cdn_url: string
default_storage_quota_bytes: number
updated_at: string
}
export interface ListSoraS3ProfilesResponse {
active_profile_id: string
items: SoraS3Profile[]
}
export interface UpdateSoraS3SettingsRequest {
profile_id?: string
enabled: boolean
endpoint: string
region: string
bucket: string
access_key_id: string
secret_access_key?: string
prefix: string
force_path_style: boolean
cdn_url: string
default_storage_quota_bytes: number
}
export interface CreateSoraS3ProfileRequest {
profile_id: string
name: string
set_active?: boolean
enabled: boolean
endpoint: string
region: string
bucket: string
access_key_id: string
secret_access_key?: string
prefix: string
force_path_style: boolean
cdn_url: string
default_storage_quota_bytes: number
}
export interface UpdateSoraS3ProfileRequest {
name: string
enabled: boolean
endpoint: string
region: string
bucket: string
access_key_id: string
secret_access_key?: string
prefix: string
force_path_style: boolean
cdn_url: string
default_storage_quota_bytes: number
}
export interface TestSoraS3ConnectionRequest {
profile_id?: string
enabled: boolean
endpoint: string
region: string
bucket: string
access_key_id: string
secret_access_key?: string
prefix: string
force_path_style: boolean
cdn_url: string
default_storage_quota_bytes?: number
}
export async function getSoraS3Settings(): Promise<SoraS3Settings> {
const { data } = await apiClient.get<SoraS3Settings>('/admin/settings/sora-s3')
return data
}
export async function updateSoraS3Settings(settings: UpdateSoraS3SettingsRequest): Promise<SoraS3Settings> {
const { data } = await apiClient.put<SoraS3Settings>('/admin/settings/sora-s3', settings)
return data
}
export async function testSoraS3Connection(
settings: TestSoraS3ConnectionRequest
): Promise<{ message: string }> {
const { data } = await apiClient.post<{ message: string }>('/admin/settings/sora-s3/test', settings)
return data
}
export async function listSoraS3Profiles(): Promise<ListSoraS3ProfilesResponse> {
const { data } = await apiClient.get<ListSoraS3ProfilesResponse>('/admin/settings/sora-s3/profiles')
return data
}
export async function createSoraS3Profile(request: CreateSoraS3ProfileRequest): Promise<SoraS3Profile> {
const { data } = await apiClient.post<SoraS3Profile>('/admin/settings/sora-s3/profiles', request)
return data
}
export async function updateSoraS3Profile(profileID: string, request: UpdateSoraS3ProfileRequest): Promise<SoraS3Profile> {
const { data } = await apiClient.put<SoraS3Profile>(`/admin/settings/sora-s3/profiles/${profileID}`, request)
return data
}
export async function deleteSoraS3Profile(profileID: string): Promise<void> {
await apiClient.delete(`/admin/settings/sora-s3/profiles/${profileID}`)
}
export async function setActiveSoraS3Profile(profileID: string): Promise<SoraS3Profile> {
const { data } = await apiClient.post<SoraS3Profile>(`/admin/settings/sora-s3/profiles/${profileID}/activate`)
return data
}
export const settingsAPI = { export const settingsAPI = {
getSettings, getSettings,
updateSettings, updateSettings,
...@@ -545,15 +407,7 @@ export const settingsAPI = { ...@@ -545,15 +407,7 @@ export const settingsAPI = {
getRectifierSettings, getRectifierSettings,
updateRectifierSettings, updateRectifierSettings,
getBetaPolicySettings, getBetaPolicySettings,
updateBetaPolicySettings, updateBetaPolicySettings
getSoraS3Settings,
updateSoraS3Settings,
testSoraS3Connection,
listSoraS3Profiles,
createSoraS3Profile,
updateSoraS3Profile,
deleteSoraS3Profile,
setActiveSoraS3Profile
} }
export default settingsAPI export default settingsAPI
/**
* Sora 客户端 API
* 封装所有 Sora 生成、作品库、配额等接口调用
*/
import { apiClient } from './client'
// ==================== 类型定义 ====================
export interface SoraGeneration {
id: number
user_id: number
model: string
prompt: string
media_type: string
status: string // pending | generating | completed | failed | cancelled
storage_type: string // upstream | s3 | local
media_url: string
media_urls: string[]
s3_object_keys: string[]
file_size_bytes: number
error_message: string
created_at: string
completed_at?: string
}
export interface GenerateRequest {
model: string
prompt: string
video_count?: number
media_type?: string
image_input?: string
api_key_id?: number
}
export interface GenerateResponse {
generation_id: number
status: string
}
export interface GenerationListResponse {
data: SoraGeneration[]
total: number
page: number
}
export interface QuotaInfo {
quota_bytes: number
used_bytes: number
available_bytes: number
quota_source: string // user | group | system | unlimited
source?: string // 兼容旧字段
}
export interface StorageStatus {
s3_enabled: boolean
s3_healthy: boolean
local_enabled: boolean
}
/** 单个扁平模型(旧接口,保留兼容) */
export interface SoraModel {
id: string
name: string
type: string // video | image
orientation?: string
duration?: number
}
/** 模型家族(新接口 — 后端从 soraModelConfigs 自动聚合) */
export interface SoraModelFamily {
id: string // 家族 ID,如 "sora2"
name: string // 显示名,如 "Sora 2"
type: string // "video" | "image"
orientations: string[] // ["landscape", "portrait"] 或 ["landscape", "portrait", "square"]
durations?: number[] // [10, 15, 25](仅视频模型)
}
type LooseRecord = Record<string, unknown>
function asRecord(value: unknown): LooseRecord | null {
return value !== null && typeof value === 'object' ? value as LooseRecord : null
}
function asArray<T = unknown>(value: unknown): T[] {
return Array.isArray(value) ? value as T[] : []
}
function asPositiveInt(value: unknown): number | null {
const n = Number(value)
if (!Number.isFinite(n) || n <= 0) return null
return Math.round(n)
}
function dedupeStrings(values: string[]): string[] {
return Array.from(new Set(values))
}
function extractOrientationFromModelID(modelID: string): string | null {
const m = modelID.match(/-(landscape|portrait|square)(?:-\d+s)?$/i)
return m ? m[1].toLowerCase() : null
}
function extractDurationFromModelID(modelID: string): number | null {
const m = modelID.match(/-(\d+)s$/i)
return m ? asPositiveInt(m[1]) : null
}
function normalizeLegacyFamilies(candidates: unknown[]): SoraModelFamily[] {
const familyMap = new Map<string, SoraModelFamily>()
for (const item of candidates) {
const model = asRecord(item)
if (!model || typeof model.id !== 'string' || model.id.trim() === '') continue
const rawID = model.id.trim()
const type = model.type === 'image' ? 'image' : 'video'
const name = typeof model.name === 'string' && model.name.trim() ? model.name.trim() : rawID
const baseID = rawID.replace(/-(landscape|portrait|square)(?:-\d+s)?$/i, '')
const orientation =
typeof model.orientation === 'string' && model.orientation
? model.orientation.toLowerCase()
: extractOrientationFromModelID(rawID)
const duration = asPositiveInt(model.duration) ?? extractDurationFromModelID(rawID)
const familyKey = baseID || rawID
const family = familyMap.get(familyKey) ?? {
id: familyKey,
name,
type,
orientations: [],
durations: []
}
if (orientation) {
family.orientations.push(orientation)
}
if (type === 'video' && duration) {
family.durations = family.durations || []
family.durations.push(duration)
}
familyMap.set(familyKey, family)
}
return Array.from(familyMap.values())
.map((family) => ({
...family,
orientations:
family.orientations.length > 0
? dedupeStrings(family.orientations)
: (family.type === 'image' ? ['square'] : ['landscape']),
durations:
family.type === 'video'
? Array.from(new Set((family.durations || []).filter((d): d is number => Number.isFinite(d)))).sort((a, b) => a - b)
: []
}))
.filter((family) => family.id !== '')
}
function normalizeModelFamilyRecord(item: unknown): SoraModelFamily | null {
const model = asRecord(item)
if (!model || typeof model.id !== 'string' || model.id.trim() === '') return null
// 仅把明确的“家族结构”识别为 family;老结构(单模型)走 legacy 聚合逻辑。
if (!Array.isArray(model.orientations) && !Array.isArray(model.durations)) return null
const orientations = asArray<string>(model.orientations).filter((o): o is string => typeof o === 'string' && o.length > 0)
const durations = asArray<unknown>(model.durations)
.map(asPositiveInt)
.filter((d): d is number => d !== null)
return {
id: model.id.trim(),
name: typeof model.name === 'string' && model.name.trim() ? model.name.trim() : model.id.trim(),
type: model.type === 'image' ? 'image' : 'video',
orientations: dedupeStrings(orientations),
durations: Array.from(new Set(durations)).sort((a, b) => a - b)
}
}
function extractCandidateArray(payload: unknown): unknown[] {
if (Array.isArray(payload)) return payload
const record = asRecord(payload)
if (!record) return []
const keys: Array<keyof LooseRecord> = ['data', 'items', 'models', 'families']
for (const key of keys) {
if (Array.isArray(record[key])) {
return record[key] as unknown[]
}
}
return []
}
export function normalizeModelFamiliesResponse(payload: unknown): SoraModelFamily[] {
const candidates = extractCandidateArray(payload)
if (candidates.length === 0) return []
const normalized = candidates
.map(normalizeModelFamilyRecord)
.filter((item): item is SoraModelFamily => item !== null)
if (normalized.length > 0) return normalized
return normalizeLegacyFamilies(candidates)
}
export function normalizeGenerationListResponse(payload: unknown): GenerationListResponse {
const record = asRecord(payload)
if (!record) {
return { data: [], total: 0, page: 1 }
}
const data = Array.isArray(record.data)
? (record.data as SoraGeneration[])
: Array.isArray(record.items)
? (record.items as SoraGeneration[])
: []
const total = Number(record.total)
const page = Number(record.page)
return {
data,
total: Number.isFinite(total) ? total : data.length,
page: Number.isFinite(page) && page > 0 ? page : 1
}
}
// ==================== API 方法 ====================
/** 异步生成 — 创建 pending 记录后立即返回 */
export async function generate(req: GenerateRequest): Promise<GenerateResponse> {
const { data } = await apiClient.post<GenerateResponse>('/sora/generate', req)
return data
}
/** 查询生成记录列表 */
export async function listGenerations(params?: {
page?: number
page_size?: number
status?: string
storage_type?: string
media_type?: string
}): Promise<GenerationListResponse> {
const { data } = await apiClient.get<unknown>('/sora/generations', { params })
return normalizeGenerationListResponse(data)
}
/** 查询生成记录详情 */
export async function getGeneration(id: number): Promise<SoraGeneration> {
const { data } = await apiClient.get<SoraGeneration>(`/sora/generations/${id}`)
return data
}
/** 删除生成记录 */
export async function deleteGeneration(id: number): Promise<{ message: string }> {
const { data } = await apiClient.delete<{ message: string }>(`/sora/generations/${id}`)
return data
}
/** 取消生成任务 */
export async function cancelGeneration(id: number): Promise<{ message: string }> {
const { data } = await apiClient.post<{ message: string }>(`/sora/generations/${id}/cancel`)
return data
}
/** 手动保存到 S3 */
export async function saveToStorage(
id: number
): Promise<{ message: string; object_key: string; object_keys?: string[] }> {
const { data } = await apiClient.post<{ message: string; object_key: string; object_keys?: string[] }>(
`/sora/generations/${id}/save`
)
return data
}
/** 查询配额信息 */
export async function getQuota(): Promise<QuotaInfo> {
const { data } = await apiClient.get<QuotaInfo>('/sora/quota')
return data
}
/** 获取可用模型家族列表 */
export async function getModels(): Promise<SoraModelFamily[]> {
const { data } = await apiClient.get<unknown>('/sora/models')
return normalizeModelFamiliesResponse(data)
}
/** 获取存储状态 */
export async function getStorageStatus(): Promise<StorageStatus> {
const { data } = await apiClient.get<StorageStatus>('/sora/storage-status')
return data
}
const soraAPI = {
generate,
listGenerations,
getGeneration,
deleteGeneration,
cancelGeneration,
saveToStorage,
getQuota,
getModels,
getStorageStatus
}
export default soraAPI
...@@ -41,7 +41,7 @@ ...@@ -41,7 +41,7 @@
</span> </span>
</div> </div>
<div v-if="!isSoraAccount" class="space-y-1.5"> <div class="space-y-1.5">
<label class="text-sm font-medium text-gray-700 dark:text-gray-300"> <label class="text-sm font-medium text-gray-700 dark:text-gray-300">
{{ t('admin.accounts.selectTestModel') }} {{ t('admin.accounts.selectTestModel') }}
</label> </label>
...@@ -54,12 +54,6 @@ ...@@ -54,12 +54,6 @@
:placeholder="loadingModels ? t('common.loading') + '...' : t('admin.accounts.selectTestModel')" :placeholder="loadingModels ? t('common.loading') + '...' : t('admin.accounts.selectTestModel')"
/> />
</div> </div>
<div
v-else
class="rounded-lg border border-blue-200 bg-blue-50 px-3 py-2 text-xs text-blue-700 dark:border-blue-700 dark:bg-blue-900/20 dark:text-blue-300"
>
{{ t('admin.accounts.soraTestHint') }}
</div>
<div v-if="supportsGeminiImageTest" class="space-y-1.5"> <div v-if="supportsGeminiImageTest" class="space-y-1.5">
<TextArea <TextArea
...@@ -152,17 +146,15 @@ ...@@ -152,17 +146,15 @@
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<span class="flex items-center gap-1"> <span class="flex items-center gap-1">
<Icon name="grid" size="sm" :stroke-width="2" /> <Icon name="grid" size="sm" :stroke-width="2" />
{{ isSoraAccount ? t('admin.accounts.soraTestTarget') : t('admin.accounts.testModel') }} {{ t('admin.accounts.testModel') }}
</span> </span>
</div> </div>
<span class="flex items-center gap-1"> <span class="flex items-center gap-1">
<Icon name="chat" size="sm" :stroke-width="2" /> <Icon name="chat" size="sm" :stroke-width="2" />
{{ {{
isSoraAccount supportsGeminiImageTest
? t('admin.accounts.soraTestMode') ? t('admin.accounts.geminiImageTestMode')
: supportsGeminiImageTest : t('admin.accounts.testPrompt')
? t('admin.accounts.geminiImageTestMode')
: t('admin.accounts.testPrompt')
}} }}
</span> </span>
</div> </div>
...@@ -179,10 +171,10 @@ ...@@ -179,10 +171,10 @@
</button> </button>
<button <button
@click="startTest" @click="startTest"
:disabled="status === 'connecting' || (!isSoraAccount && !selectedModelId)" :disabled="status === 'connecting' || !selectedModelId"
:class="[ :class="[
'flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium transition-all', 'flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium transition-all',
status === 'connecting' || (!isSoraAccount && !selectedModelId) status === 'connecting' || !selectedModelId
? 'cursor-not-allowed bg-primary-400 text-white' ? 'cursor-not-allowed bg-primary-400 text-white'
: status === 'success' : status === 'success'
? 'bg-green-500 text-white hover:bg-green-600' ? 'bg-green-500 text-white hover:bg-green-600'
...@@ -258,11 +250,9 @@ const selectedModelId = ref('') ...@@ -258,11 +250,9 @@ const selectedModelId = ref('')
const testPrompt = ref('') const testPrompt = ref('')
const loadingModels = ref(false) const loadingModels = ref(false)
let eventSource: EventSource | null = null let eventSource: EventSource | null = null
const isSoraAccount = computed(() => props.account?.platform === 'sora')
const generatedImages = ref<PreviewImage[]>([]) const generatedImages = ref<PreviewImage[]>([])
const prioritizedGeminiModels = ['gemini-3.1-flash-image', 'gemini-2.5-flash-image', 'gemini-2.5-flash', 'gemini-2.5-pro', 'gemini-3-flash-preview', 'gemini-3-pro-preview', 'gemini-2.0-flash'] const prioritizedGeminiModels = ['gemini-3.1-flash-image', 'gemini-2.5-flash-image', 'gemini-2.5-flash', 'gemini-2.5-pro', 'gemini-3-flash-preview', 'gemini-3-pro-preview', 'gemini-2.0-flash']
const supportsGeminiImageTest = computed(() => { const supportsGeminiImageTest = computed(() => {
if (isSoraAccount.value) return false
const modelID = selectedModelId.value.toLowerCase() const modelID = selectedModelId.value.toLowerCase()
if (!modelID.startsWith('gemini-') || !modelID.includes('-image')) return false if (!modelID.startsWith('gemini-') || !modelID.includes('-image')) return false
...@@ -302,12 +292,6 @@ watch(selectedModelId, () => { ...@@ -302,12 +292,6 @@ watch(selectedModelId, () => {
const loadAvailableModels = async () => { const loadAvailableModels = async () => {
if (!props.account) return if (!props.account) return
if (props.account.platform === 'sora') {
availableModels.value = []
selectedModelId.value = ''
loadingModels.value = false
return
}
loadingModels.value = true loadingModels.value = true
selectedModelId.value = '' // Reset selection before loading selectedModelId.value = '' // Reset selection before loading
...@@ -373,7 +357,7 @@ const scrollToBottom = async () => { ...@@ -373,7 +357,7 @@ const scrollToBottom = async () => {
} }
const startTest = async () => { const startTest = async () => {
if (!props.account || (!isSoraAccount.value && !selectedModelId.value)) return if (!props.account || !selectedModelId.value) return
resetState() resetState()
status.value = 'connecting' status.value = 'connecting'
...@@ -394,14 +378,10 @@ const startTest = async () => { ...@@ -394,14 +378,10 @@ const startTest = async () => {
Authorization: `Bearer ${localStorage.getItem('auth_token')}`, Authorization: `Bearer ${localStorage.getItem('auth_token')}`,
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}, },
body: JSON.stringify( body: JSON.stringify({
isSoraAccount.value
? {}
: {
model_id: selectedModelId.value, model_id: selectedModelId.value,
prompt: supportsGeminiImageTest.value ? testPrompt.value.trim() : '' prompt: supportsGeminiImageTest.value ? testPrompt.value.trim() : ''
} })
)
}) })
if (!response.ok) { if (!response.ok) {
...@@ -461,9 +441,7 @@ const handleEvent = (event: { ...@@ -461,9 +441,7 @@ const handleEvent = (event: {
addLine(t('admin.accounts.usingModel', { model: event.model }), 'text-cyan-400') addLine(t('admin.accounts.usingModel', { model: event.model }), 'text-cyan-400')
} }
addLine( addLine(
isSoraAccount.value supportsGeminiImageTest.value
? t('admin.accounts.soraTestingFlow')
: supportsGeminiImageTest.value
? t('admin.accounts.sendingGeminiImageRequest') ? t('admin.accounts.sendingGeminiImageRequest')
: t('admin.accounts.sendingTestMessage'), : t('admin.accounts.sendingTestMessage'),
'text-gray-400' 'text-gray-400'
......
...@@ -109,28 +109,6 @@ ...@@ -109,28 +109,6 @@
</svg> </svg>
OpenAI OpenAI
</button> </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 <button
type="button" type="button"
@click="form.platform = 'gemini'" @click="form.platform = 'gemini'"
...@@ -172,63 +150,6 @@ ...@@ -172,63 +150,6 @@
</div> </div>
</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-2 gap-3" data-tour="account-form-type">
<button
type="button"
@click="soraAccountType = 'oauth'; accountCategory = 'oauth-based'; addMethod = 'oauth'"
:class="[
'flex items-center gap-3 rounded-lg border-2 p-3 text-left transition-all',
soraAccountType === 'oauth'
? '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',
soraAccountType === 'oauth'
? '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>
<button
type="button"
@click="soraAccountType = 'apikey'; accountCategory = 'apikey'"
:class="[
'flex items-center gap-3 rounded-lg border-2 p-3 text-left transition-all',
soraAccountType === 'apikey'
? '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',
soraAccountType === 'apikey'
? 'bg-rose-500 text-white'
: 'bg-gray-100 text-gray-500 dark:bg-dark-600 dark:text-gray-400'
]"
>
<Icon name="link" size="sm" />
</div>
<div>
<span class="block text-sm font-medium text-gray-900 dark:text-white">{{ t('admin.accounts.types.soraApiKey') }}</span>
<span class="text-xs text-gray-500 dark:text-gray-400">{{ t('admin.accounts.types.soraApiKeyHint') }}</span>
</div>
</button>
</div>
</div>
<!-- Account Type Selection (Anthropic) --> <!-- Account Type Selection (Anthropic) -->
<div v-if="form.platform === 'anthropic'"> <div v-if="form.platform === 'anthropic'">
<label class="input-label">{{ t('admin.accounts.accountType') }}</label> <label class="input-label">{{ t('admin.accounts.accountType') }}</label>
...@@ -935,14 +856,14 @@ ...@@ -935,14 +856,14 @@
type="text" type="text"
class="input" class="input"
:placeholder=" :placeholder="
form.platform === 'openai' || form.platform === 'sora' form.platform === 'openai'
? 'https://api.openai.com' ? 'https://api.openai.com'
: form.platform === 'gemini' : form.platform === 'gemini'
? 'https://generativelanguage.googleapis.com' ? 'https://generativelanguage.googleapis.com'
: 'https://api.anthropic.com' : 'https://api.anthropic.com'
" "
/> />
<p class="input-hint">{{ form.platform === 'sora' ? t('admin.accounts.soraUpstreamBaseUrlHint') : baseUrlHint }}</p> <p class="input-hint">{{ baseUrlHint }}</p>
</div> </div>
<div> <div>
<label class="input-label">{{ t('admin.accounts.apiKeyRequired') }}</label> <label class="input-label">{{ t('admin.accounts.apiKeyRequired') }}</label>
...@@ -2543,13 +2464,13 @@ ...@@ -2543,13 +2464,13 @@
:loading="currentOAuthLoading" :loading="currentOAuthLoading"
:error="currentOAuthError" :error="currentOAuthError"
:show-help="form.platform === 'anthropic'" :show-help="form.platform === 'anthropic'"
:show-proxy-warning="form.platform !== 'openai' && form.platform !== 'sora' && !!form.proxy_id" :show-proxy-warning="form.platform !== 'openai' && !!form.proxy_id"
:allow-multiple="form.platform === 'anthropic'" :allow-multiple="form.platform === 'anthropic'"
:show-cookie-option="form.platform === 'anthropic'" :show-cookie-option="form.platform === 'anthropic'"
:show-refresh-token-option="form.platform === 'openai' || form.platform === 'sora' || form.platform === 'antigravity'" :show-refresh-token-option="form.platform === 'openai' || form.platform === 'antigravity'"
:show-mobile-refresh-token-option="form.platform === 'openai'" :show-mobile-refresh-token-option="form.platform === 'openai'"
:show-session-token-option="form.platform === 'sora'" :show-session-token-option="false"
:show-access-token-option="form.platform === 'sora'" :show-access-token-option="false"
:platform="form.platform" :platform="form.platform"
:show-project-id="geminiOAuthType === 'code_assist'" :show-project-id="geminiOAuthType === 'code_assist'"
@generate-url="handleGenerateUrl" @generate-url="handleGenerateUrl"
...@@ -2557,7 +2478,6 @@ ...@@ -2557,7 +2478,6 @@
@validate-refresh-token="handleValidateRefreshToken" @validate-refresh-token="handleValidateRefreshToken"
@validate-mobile-refresh-token="handleOpenAIValidateMobileRT" @validate-mobile-refresh-token="handleOpenAIValidateMobileRT"
@validate-session-token="handleValidateSessionToken" @validate-session-token="handleValidateSessionToken"
@import-access-token="handleImportAccessToken"
/> />
</div> </div>
...@@ -2943,7 +2863,7 @@ const { t } = useI18n() ...@@ -2943,7 +2863,7 @@ const { t } = useI18n()
const authStore = useAuthStore() const authStore = useAuthStore()
const oauthStepTitle = computed(() => { const oauthStepTitle = computed(() => {
if (form.platform === 'openai' || form.platform === 'sora') return t('admin.accounts.oauth.openai.title') if (form.platform === 'openai') return t('admin.accounts.oauth.openai.title')
if (form.platform === 'gemini') return t('admin.accounts.oauth.gemini.title') if (form.platform === 'gemini') return t('admin.accounts.oauth.gemini.title')
if (form.platform === 'antigravity') return t('admin.accounts.oauth.antigravity.title') if (form.platform === 'antigravity') return t('admin.accounts.oauth.antigravity.title')
return t('admin.accounts.oauth.title') return t('admin.accounts.oauth.title')
...@@ -2951,13 +2871,13 @@ const oauthStepTitle = computed(() => { ...@@ -2951,13 +2871,13 @@ const oauthStepTitle = computed(() => {
// Platform-specific hints for API Key type // Platform-specific hints for API Key type
const baseUrlHint = computed(() => { const baseUrlHint = computed(() => {
if (form.platform === 'openai' || form.platform === 'sora') return t('admin.accounts.openai.baseUrlHint') if (form.platform === 'openai') return t('admin.accounts.openai.baseUrlHint')
if (form.platform === 'gemini') return t('admin.accounts.gemini.baseUrlHint') if (form.platform === 'gemini') return t('admin.accounts.gemini.baseUrlHint')
return t('admin.accounts.baseUrlHint') return t('admin.accounts.baseUrlHint')
}) })
const apiKeyHint = computed(() => { const apiKeyHint = computed(() => {
if (form.platform === 'openai' || form.platform === 'sora') return t('admin.accounts.openai.apiKeyHint') if (form.platform === 'openai') return t('admin.accounts.openai.apiKeyHint')
if (form.platform === 'gemini') return t('admin.accounts.gemini.apiKeyHint') if (form.platform === 'gemini') return t('admin.accounts.gemini.apiKeyHint')
return t('admin.accounts.apiKeyHint') return t('admin.accounts.apiKeyHint')
}) })
...@@ -2978,36 +2898,34 @@ const appStore = useAppStore() ...@@ -2978,36 +2898,34 @@ const appStore = useAppStore()
// OAuth composables // OAuth composables
const oauth = useAccountOAuth() // For Anthropic OAuth const oauth = useAccountOAuth() // For Anthropic OAuth
const openaiOAuth = useOpenAIOAuth({ platform: 'openai' }) // For OpenAI OAuth const openaiOAuth = useOpenAIOAuth() // For OpenAI OAuth
const soraOAuth = useOpenAIOAuth({ platform: 'sora' }) // For Sora OAuth
const geminiOAuth = useGeminiOAuth() // For Gemini OAuth const geminiOAuth = useGeminiOAuth() // For Gemini OAuth
const antigravityOAuth = useAntigravityOAuth() // For Antigravity OAuth const antigravityOAuth = useAntigravityOAuth() // For Antigravity OAuth
const activeOpenAIOAuth = computed(() => (form.platform === 'sora' ? soraOAuth : openaiOAuth))
// Computed: current OAuth state for template binding // Computed: current OAuth state for template binding
const currentAuthUrl = computed(() => { const currentAuthUrl = computed(() => {
if (form.platform === 'openai' || form.platform === 'sora') return activeOpenAIOAuth.value.authUrl.value if (form.platform === 'openai') return openaiOAuth.authUrl.value
if (form.platform === 'gemini') return geminiOAuth.authUrl.value if (form.platform === 'gemini') return geminiOAuth.authUrl.value
if (form.platform === 'antigravity') return antigravityOAuth.authUrl.value if (form.platform === 'antigravity') return antigravityOAuth.authUrl.value
return oauth.authUrl.value return oauth.authUrl.value
}) })
const currentSessionId = computed(() => { const currentSessionId = computed(() => {
if (form.platform === 'openai' || form.platform === 'sora') return activeOpenAIOAuth.value.sessionId.value if (form.platform === 'openai') return openaiOAuth.sessionId.value
if (form.platform === 'gemini') return geminiOAuth.sessionId.value if (form.platform === 'gemini') return geminiOAuth.sessionId.value
if (form.platform === 'antigravity') return antigravityOAuth.sessionId.value if (form.platform === 'antigravity') return antigravityOAuth.sessionId.value
return oauth.sessionId.value return oauth.sessionId.value
}) })
const currentOAuthLoading = computed(() => { const currentOAuthLoading = computed(() => {
if (form.platform === 'openai' || form.platform === 'sora') return activeOpenAIOAuth.value.loading.value if (form.platform === 'openai') return openaiOAuth.loading.value
if (form.platform === 'gemini') return geminiOAuth.loading.value if (form.platform === 'gemini') return geminiOAuth.loading.value
if (form.platform === 'antigravity') return antigravityOAuth.loading.value if (form.platform === 'antigravity') return antigravityOAuth.loading.value
return oauth.loading.value return oauth.loading.value
}) })
const currentOAuthError = computed(() => { const currentOAuthError = computed(() => {
if (form.platform === 'openai' || form.platform === 'sora') return activeOpenAIOAuth.value.error.value if (form.platform === 'openai') return openaiOAuth.error.value
if (form.platform === 'gemini') return geminiOAuth.error.value if (form.platform === 'gemini') return geminiOAuth.error.value
if (form.platform === 'antigravity') return antigravityOAuth.error.value if (form.platform === 'antigravity') return antigravityOAuth.error.value
return oauth.error.value return oauth.error.value
...@@ -3065,7 +2983,6 @@ const anthropicPassthroughEnabled = ref(false) ...@@ -3065,7 +2983,6 @@ const anthropicPassthroughEnabled = ref(false)
const mixedScheduling = ref(false) // For antigravity accounts: enable mixed scheduling const mixedScheduling = ref(false) // For antigravity accounts: enable mixed scheduling
const allowOverages = ref(false) // For antigravity accounts: enable AI Credits overages const allowOverages = ref(false) // For antigravity accounts: enable AI Credits overages
const antigravityAccountType = ref<'oauth' | 'upstream'>('oauth') // For antigravity: oauth or upstream const antigravityAccountType = ref<'oauth' | 'upstream'>('oauth') // For antigravity: oauth or upstream
const soraAccountType = ref<'oauth' | 'apikey'>('oauth') // For sora: oauth or apikey (upstream)
const upstreamBaseUrl = ref('') // For upstream type: base URL const upstreamBaseUrl = ref('') // For upstream type: base URL
const upstreamApiKey = ref('') // For upstream type: API key const upstreamApiKey = ref('') // For upstream type: API key
const antigravityModelRestrictionMode = ref<'whitelist' | 'mapping'>('whitelist') const antigravityModelRestrictionMode = ref<'whitelist' | 'mapping'>('whitelist')
...@@ -3277,8 +3194,8 @@ const expiresAtInput = computed({ ...@@ -3277,8 +3194,8 @@ const expiresAtInput = computed({
const canExchangeCode = computed(() => { const canExchangeCode = computed(() => {
const authCode = oauthFlowRef.value?.authCode || '' const authCode = oauthFlowRef.value?.authCode || ''
if (form.platform === 'openai' || form.platform === 'sora') { if (form.platform === 'openai') {
return authCode.trim() && activeOpenAIOAuth.value.sessionId.value && !activeOpenAIOAuth.value.loading.value return authCode.trim() && openaiOAuth.sessionId.value && !openaiOAuth.loading.value
} }
if (form.platform === 'gemini') { if (form.platform === 'gemini') {
return authCode.trim() && geminiOAuth.sessionId.value && !geminiOAuth.loading.value return authCode.trim() && geminiOAuth.sessionId.value && !geminiOAuth.loading.value
...@@ -3320,18 +3237,13 @@ watch( ...@@ -3320,18 +3237,13 @@ watch(
// Sync form.type based on accountCategory, addMethod, and platform-specific type // Sync form.type based on accountCategory, addMethod, and platform-specific type
watch( watch(
[accountCategory, addMethod, antigravityAccountType, soraAccountType], [accountCategory, addMethod, antigravityAccountType],
([category, method, agType, soraType]) => { ([category, method, agType]) => {
// Antigravity upstream 类型(实际创建为 apikey) // Antigravity upstream 类型(实际创建为 apikey)
if (form.platform === 'antigravity' && agType === 'upstream') { if (form.platform === 'antigravity' && agType === 'upstream') {
form.type = 'apikey' form.type = 'apikey'
return return
} }
// Sora apikey 类型(上游透传)
if (form.platform === 'sora' && soraType === 'apikey') {
form.type = 'apikey'
return
}
// Bedrock 类型 // Bedrock 类型
if (form.platform === 'anthropic' && category === 'bedrock') { if (form.platform === 'anthropic' && category === 'bedrock') {
form.type = 'bedrock' as AccountType form.type = 'bedrock' as AccountType
...@@ -3352,7 +3264,7 @@ watch( ...@@ -3352,7 +3264,7 @@ watch(
(newPlatform) => { (newPlatform) => {
// Reset base URL based on platform // Reset base URL based on platform
apiKeyBaseUrl.value = apiKeyBaseUrl.value =
(newPlatform === 'openai' || newPlatform === 'sora') (newPlatform === 'openai')
? 'https://api.openai.com' ? 'https://api.openai.com'
: newPlatform === 'gemini' : newPlatform === 'gemini'
? 'https://generativelanguage.googleapis.com' ? 'https://generativelanguage.googleapis.com'
...@@ -3387,13 +3299,6 @@ watch( ...@@ -3387,13 +3299,6 @@ watch(
if (newPlatform !== 'anthropic' && newPlatform !== 'antigravity') { if (newPlatform !== 'anthropic' && newPlatform !== 'antigravity') {
interceptWarmupRequests.value = false interceptWarmupRequests.value = false
} }
if (newPlatform === 'sora') {
// 默认 OAuth,但允许用户选择 API Key
accountCategory.value = 'oauth-based'
addMethod.value = 'oauth'
form.type = 'oauth'
soraAccountType.value = 'oauth'
}
if (newPlatform !== 'openai') { if (newPlatform !== 'openai') {
openaiPassthroughEnabled.value = false openaiPassthroughEnabled.value = false
openaiOAuthResponsesWebSocketV2Mode.value = OPENAI_WS_MODE_OFF openaiOAuthResponsesWebSocketV2Mode.value = OPENAI_WS_MODE_OFF
...@@ -3406,7 +3311,7 @@ watch( ...@@ -3406,7 +3311,7 @@ watch(
// Reset OAuth states // Reset OAuth states
oauth.resetState() oauth.resetState()
openaiOAuth.resetState() openaiOAuth.resetState()
soraOAuth.resetState()
geminiOAuth.resetState() geminiOAuth.resetState()
antigravityOAuth.resetState() antigravityOAuth.resetState()
} }
...@@ -3816,7 +3721,6 @@ const resetForm = () => { ...@@ -3816,7 +3721,6 @@ const resetForm = () => {
geminiTierAIStudio.value = 'aistudio_free' geminiTierAIStudio.value = 'aistudio_free'
oauth.resetState() oauth.resetState()
openaiOAuth.resetState() openaiOAuth.resetState()
soraOAuth.resetState()
geminiOAuth.resetState() geminiOAuth.resetState()
antigravityOAuth.resetState() antigravityOAuth.resetState()
oauthFlowRef.value?.reset() oauthFlowRef.value?.reset()
...@@ -3877,29 +3781,6 @@ const buildAnthropicExtra = (base?: Record<string, unknown>): Record<string, unk ...@@ -3877,29 +3781,6 @@ const buildAnthropicExtra = (base?: Record<string, unknown>): Record<string, unk
return Object.keys(extra).length > 0 ? extra : undefined 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
delete extra.openai_oauth_responses_websockets_v2_mode
delete extra.openai_apikey_responses_websockets_v2_mode
delete extra.openai_oauth_responses_websockets_v2_enabled
delete extra.openai_apikey_responses_websockets_v2_enabled
delete extra.responses_websockets_v2_enabled
delete extra.openai_ws_enabled
return Object.keys(extra).length > 0 ? extra : undefined
}
// Helper function to create account with mixed channel warning handling // Helper function to create account with mixed channel warning handling
const doCreateAccount = async (payload: CreateAccountRequest) => { const doCreateAccount = async (payload: CreateAccountRequest) => {
const canContinue = await ensureAntigravityMixedChannelConfirmed(async () => { const canContinue = await ensureAntigravityMixedChannelConfirmed(async () => {
...@@ -4064,19 +3945,6 @@ const handleSubmit = async () => { ...@@ -4064,19 +3945,6 @@ const handleSubmit = async () => {
return return
} }
// Sora apikey 账号 base_url 必填 + scheme 校验
if (form.platform === 'sora') {
const soraBaseUrl = apiKeyBaseUrl.value.trim()
if (!soraBaseUrl) {
appStore.showError(t('admin.accounts.soraBaseUrlRequired'))
return
}
if (!soraBaseUrl.startsWith('http://') && !soraBaseUrl.startsWith('https://')) {
appStore.showError(t('admin.accounts.soraBaseUrlInvalidScheme'))
return
}
}
// Determine default base URL based on platform // Determine default base URL based on platform
const defaultBaseUrl = const defaultBaseUrl =
form.platform === 'openai' form.platform === 'openai'
...@@ -4134,15 +4002,14 @@ const goBackToBasicInfo = () => { ...@@ -4134,15 +4002,14 @@ const goBackToBasicInfo = () => {
step.value = 1 step.value = 1
oauth.resetState() oauth.resetState()
openaiOAuth.resetState() openaiOAuth.resetState()
soraOAuth.resetState()
geminiOAuth.resetState() geminiOAuth.resetState()
antigravityOAuth.resetState() antigravityOAuth.resetState()
oauthFlowRef.value?.reset() oauthFlowRef.value?.reset()
} }
const handleGenerateUrl = async () => { const handleGenerateUrl = async () => {
if (form.platform === 'openai' || form.platform === 'sora') { if (form.platform === 'openai') {
await activeOpenAIOAuth.value.generateAuthUrl(form.proxy_id) await openaiOAuth.generateAuthUrl(form.proxy_id)
} else if (form.platform === 'gemini') { } else if (form.platform === 'gemini') {
await geminiOAuth.generateAuthUrl( await geminiOAuth.generateAuthUrl(
form.proxy_id, form.proxy_id,
...@@ -4158,95 +4025,15 @@ const handleGenerateUrl = async () => { ...@@ -4158,95 +4025,15 @@ const handleGenerateUrl = async () => {
} }
const handleValidateRefreshToken = (rt: string) => { const handleValidateRefreshToken = (rt: string) => {
if (form.platform === 'openai' || form.platform === 'sora') { if (form.platform === 'openai') {
handleOpenAIValidateRT(rt) handleOpenAIValidateRT(rt)
} else if (form.platform === 'antigravity') { } else if (form.platform === 'antigravity') {
handleAntigravityValidateRT(rt) handleAntigravityValidateRT(rt)
} }
} }
const handleValidateSessionToken = (sessionToken: string) => { const handleValidateSessionToken = (_sessionToken: string) => {
if (form.platform === 'sora') { // Session token validation removed
handleSoraValidateST(sessionToken)
}
}
// 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<string, unknown> = {
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,
load_factor: form.load_factor ?? undefined,
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 formatDateTimeLocal = formatDateTimeLocalInput
...@@ -4312,7 +4099,7 @@ const createAccountAndFinish = async ( ...@@ -4312,7 +4099,7 @@ const createAccountAndFinish = async (
// OpenAI OAuth 授权码兑换 // OpenAI OAuth 授权码兑换
const handleOpenAIExchange = async (authCode: string) => { const handleOpenAIExchange = async (authCode: string) => {
const oauthClient = activeOpenAIOAuth.value const oauthClient = openaiOAuth
if (!authCode.trim() || !oauthClient.sessionId.value) return if (!authCode.trim() || !oauthClient.sessionId.value) return
oauthClient.loading.value = true oauthClient.loading.value = true
...@@ -4338,7 +4125,6 @@ const handleOpenAIExchange = async (authCode: string) => { ...@@ -4338,7 +4125,6 @@ const handleOpenAIExchange = async (authCode: string) => {
const oauthExtra = oauthClient.buildExtraInfo(tokenInfo) as Record<string, unknown> | undefined const oauthExtra = oauthClient.buildExtraInfo(tokenInfo) as Record<string, unknown> | undefined
const extra = buildOpenAIExtra(oauthExtra) const extra = buildOpenAIExtra(oauthExtra)
const shouldCreateOpenAI = form.platform === 'openai' const shouldCreateOpenAI = form.platform === 'openai'
const shouldCreateSora = form.platform === 'sora'
// Add model mapping for OpenAI OAuth accounts(透传模式下不应用) // Add model mapping for OpenAI OAuth accounts(透传模式下不应用)
if (shouldCreateOpenAI && !isOpenAIModelRestrictionDisabled.value) { if (shouldCreateOpenAI && !isOpenAIModelRestrictionDisabled.value) {
...@@ -4353,10 +4139,8 @@ const handleOpenAIExchange = async (authCode: string) => { ...@@ -4353,10 +4139,8 @@ const handleOpenAIExchange = async (authCode: string) => {
return return
} }
let openaiAccountId: string | number | undefined
if (shouldCreateOpenAI) { if (shouldCreateOpenAI) {
const openaiAccount = await adminAPI.accounts.create({ await adminAPI.accounts.create({
name: form.name, name: form.name,
notes: form.notes, notes: form.notes,
platform: 'openai', platform: 'openai',
...@@ -4372,36 +4156,6 @@ const handleOpenAIExchange = async (authCode: string) => { ...@@ -4372,36 +4156,6 @@ const handleOpenAIExchange = async (authCode: string) => {
expires_at: form.expires_at, expires_at: form.expires_at,
auto_pause_on_expired: autoPauseOnExpired.value auto_pause_on_expired: autoPauseOnExpired.value
}) })
openaiAccountId = openaiAccount.id
appStore.showSuccess(t('admin.accounts.accountCreated'))
}
if (shouldCreateSora) {
const soraCredentials = {
access_token: credentials.access_token,
refresh_token: credentials.refresh_token,
client_id: credentials.client_id,
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,
load_factor: form.load_factor ?? undefined,
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')) appStore.showSuccess(t('admin.accounts.accountCreated'))
} }
...@@ -4416,12 +4170,12 @@ const handleOpenAIExchange = async (authCode: string) => { ...@@ -4416,12 +4170,12 @@ const handleOpenAIExchange = async (authCode: string) => {
} }
// OpenAI 手动 RT 批量验证和创建 // OpenAI 手动 RT 批量验证和创建
// OpenAI Mobile RT 使用的 client_id(与后端 openai.SoraClientID 一致) // OpenAI Mobile RT client_id
const OPENAI_MOBILE_RT_CLIENT_ID = 'app_LlGpXReQgckcGGUo2JrYvtJK' const OPENAI_MOBILE_RT_CLIENT_ID = 'app_LlGpXReQgckcGGUo2JrYvtJK'
// OpenAI/Sora RT 批量验证和创建(共享逻辑) // OpenAI RT 批量验证和创建(共享逻辑)
const handleOpenAIBatchRT = async (refreshTokenInput: string, clientId?: string) => { const handleOpenAIBatchRT = async (refreshTokenInput: string, clientId?: string) => {
const oauthClient = activeOpenAIOAuth.value const oauthClient = openaiOAuth
if (!refreshTokenInput.trim()) return if (!refreshTokenInput.trim()) return
const refreshTokens = refreshTokenInput const refreshTokens = refreshTokenInput
...@@ -4441,7 +4195,6 @@ const handleOpenAIBatchRT = async (refreshTokenInput: string, clientId?: string) ...@@ -4441,7 +4195,6 @@ const handleOpenAIBatchRT = async (refreshTokenInput: string, clientId?: string)
let failedCount = 0 let failedCount = 0
const errors: string[] = [] const errors: string[] = []
const shouldCreateOpenAI = form.platform === 'openai' const shouldCreateOpenAI = form.platform === 'openai'
const shouldCreateSora = form.platform === 'sora'
try { try {
for (let i = 0; i < refreshTokens.length; i++) { for (let i = 0; i < refreshTokens.length; i++) {
...@@ -4477,10 +4230,8 @@ const handleOpenAIBatchRT = async (refreshTokenInput: string, clientId?: string) ...@@ -4477,10 +4230,8 @@ const handleOpenAIBatchRT = async (refreshTokenInput: string, clientId?: string)
const baseName = form.name || tokenInfo.email || 'OpenAI OAuth Account' const baseName = form.name || tokenInfo.email || 'OpenAI OAuth Account'
const accountName = refreshTokens.length > 1 ? `${baseName} #${i + 1}` : baseName const accountName = refreshTokens.length > 1 ? `${baseName} #${i + 1}` : baseName
let openaiAccountId: string | number | undefined
if (shouldCreateOpenAI) { if (shouldCreateOpenAI) {
const openaiAccount = await adminAPI.accounts.create({ await adminAPI.accounts.create({
name: accountName, name: accountName,
notes: form.notes, notes: form.notes,
platform: 'openai', platform: 'openai',
...@@ -4496,34 +4247,6 @@ const handleOpenAIBatchRT = async (refreshTokenInput: string, clientId?: string) ...@@ -4496,34 +4247,6 @@ const handleOpenAIBatchRT = async (refreshTokenInput: string, clientId?: string)
expires_at: form.expires_at, expires_at: form.expires_at,
auto_pause_on_expired: autoPauseOnExpired.value auto_pause_on_expired: autoPauseOnExpired.value
}) })
openaiAccountId = openaiAccount.id
}
if (shouldCreateSora) {
const soraCredentials = {
access_token: credentials.access_token,
refresh_token: credentials.refresh_token,
client_id: credentials.client_id,
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,
load_factor: form.load_factor ?? undefined,
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++ successCount++
...@@ -4561,95 +4284,9 @@ const handleOpenAIBatchRT = async (refreshTokenInput: string, clientId?: string) ...@@ -4561,95 +4284,9 @@ const handleOpenAIBatchRT = async (refreshTokenInput: string, clientId?: string)
// 手动输入 RT(Codex CLI client_id,默认) // 手动输入 RT(Codex CLI client_id,默认)
const handleOpenAIValidateRT = (rt: string) => handleOpenAIBatchRT(rt) const handleOpenAIValidateRT = (rt: string) => handleOpenAIBatchRT(rt)
// 手动输入 Mobile RT(SoraClientID) // 手动输入 Mobile RT
const handleOpenAIValidateMobileRT = (rt: string) => handleOpenAIBatchRT(rt, OPENAI_MOBILE_RT_CLIENT_ID) const handleOpenAIValidateMobileRT = (rt: string) => handleOpenAIBatchRT(rt, OPENAI_MOBILE_RT_CLIENT_ID)
// 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,
load_factor: form.load_factor ?? undefined,
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
}
}
// Antigravity 手动 RT 批量验证和创建 // Antigravity 手动 RT 批量验证和创建
const handleAntigravityValidateRT = async (refreshTokenInput: string) => { const handleAntigravityValidateRT = async (refreshTokenInput: string) => {
if (!refreshTokenInput.trim()) return if (!refreshTokenInput.trim()) return
...@@ -4918,7 +4555,6 @@ const handleExchangeCode = async () => { ...@@ -4918,7 +4555,6 @@ const handleExchangeCode = async () => {
switch (form.platform) { switch (form.platform) {
case 'openai': case 'openai':
case 'sora':
return handleOpenAIExchange(authCode) return handleOpenAIExchange(authCode)
case 'gemini': case 'gemini':
return handleGeminiExchange(authCode) return handleGeminiExchange(authCode)
......
...@@ -35,7 +35,7 @@ ...@@ -35,7 +35,7 @@
type="text" type="text"
class="input" class="input"
:placeholder=" :placeholder="
account.platform === 'openai' || account.platform === 'sora' account.platform === 'openai'
? 'https://api.openai.com' ? 'https://api.openai.com'
: account.platform === 'gemini' : account.platform === 'gemini'
? 'https://generativelanguage.googleapis.com' ? 'https://generativelanguage.googleapis.com'
...@@ -53,7 +53,7 @@ ...@@ -53,7 +53,7 @@
type="password" type="password"
class="input font-mono" class="input font-mono"
:placeholder=" :placeholder="
account.platform === 'openai' || account.platform === 'sora' account.platform === 'openai'
? 'sk-proj-...' ? 'sk-proj-...'
: account.platform === 'gemini' : account.platform === 'gemini'
? 'AIza...' ? 'AIza...'
...@@ -1969,7 +1969,7 @@ const tempUnschedPresets = computed(() => [ ...@@ -1969,7 +1969,7 @@ const tempUnschedPresets = computed(() => [
// Computed: default base URL based on platform // Computed: default base URL based on platform
const defaultBaseUrl = computed(() => { const defaultBaseUrl = computed(() => {
if (props.account?.platform === 'openai' || props.account?.platform === 'sora') return 'https://api.openai.com' if (props.account?.platform === 'openai') return 'https://api.openai.com'
if (props.account?.platform === 'gemini') return 'https://generativelanguage.googleapis.com' if (props.account?.platform === 'gemini') return 'https://generativelanguage.googleapis.com'
return 'https://api.anthropic.com' return 'https://api.anthropic.com'
}) })
...@@ -2157,7 +2157,7 @@ const syncFormFromAccount = (newAccount: Account | null) => { ...@@ -2157,7 +2157,7 @@ const syncFormFromAccount = (newAccount: Account | null) => {
if (newAccount.type === 'apikey' && newAccount.credentials) { if (newAccount.type === 'apikey' && newAccount.credentials) {
const credentials = newAccount.credentials as Record<string, unknown> const credentials = newAccount.credentials as Record<string, unknown>
const platformDefaultUrl = const platformDefaultUrl =
newAccount.platform === 'openai' || newAccount.platform === 'sora' newAccount.platform === 'openai'
? 'https://api.openai.com' ? 'https://api.openai.com'
: newAccount.platform === 'gemini' : newAccount.platform === 'gemini'
? 'https://generativelanguage.googleapis.com' ? 'https://generativelanguage.googleapis.com'
...@@ -2253,7 +2253,7 @@ const syncFormFromAccount = (newAccount: Account | null) => { ...@@ -2253,7 +2253,7 @@ const syncFormFromAccount = (newAccount: Account | null) => {
editBaseUrl.value = (credentials.base_url as string) || '' editBaseUrl.value = (credentials.base_url as string) || ''
} else { } else {
const platformDefaultUrl = const platformDefaultUrl =
newAccount.platform === 'openai' || newAccount.platform === 'sora' newAccount.platform === 'openai'
? 'https://api.openai.com' ? 'https://api.openai.com'
: newAccount.platform === 'gemini' : newAccount.platform === 'gemini'
? 'https://generativelanguage.googleapis.com' ? 'https://generativelanguage.googleapis.com'
......
...@@ -168,217 +168,6 @@ ...@@ -168,217 +168,6 @@
</div> </div>
</div> </div>
<!-- Session Token Input (Sora) -->
<div v-if="inputMethod === 'session_token'" class="space-y-4">
<div
class="rounded-lg border border-blue-300 bg-white/80 p-4 dark:border-blue-600 dark:bg-gray-800/80"
>
<p class="mb-3 text-sm text-blue-700 dark:text-blue-300">
{{ t(getOAuthKey('sessionTokenDesc')) }}
</p>
<div class="mb-4">
<label
class="mb-2 flex items-center gap-2 text-sm font-semibold text-gray-700 dark:text-gray-300"
>
<Icon name="key" size="sm" class="text-blue-500" />
{{ t(getOAuthKey('sessionTokenRawLabel')) }}
<span
v-if="parsedSessionTokenCount > 1"
class="rounded-full bg-blue-500 px-2 py-0.5 text-xs text-white"
>
{{ t('admin.accounts.oauth.keysCount', { count: parsedSessionTokenCount }) }}
</span>
</label>
<textarea
v-model="sessionTokenInput"
rows="3"
class="input w-full resize-y font-mono text-sm"
:placeholder="t(getOAuthKey('sessionTokenRawPlaceholder'))"
></textarea>
<p class="mt-1 text-xs text-blue-600 dark:text-blue-400">
{{ t(getOAuthKey('sessionTokenRawHint')) }}
</p>
<div class="mt-2 flex flex-wrap items-center gap-2">
<button
type="button"
class="btn btn-secondary px-2 py-1 text-xs"
@click="handleOpenSoraSessionUrl"
>
{{ t(getOAuthKey('openSessionUrl')) }}
</button>
<button
type="button"
class="btn btn-secondary px-2 py-1 text-xs"
@click="handleCopySoraSessionUrl"
>
{{ t(getOAuthKey('copySessionUrl')) }}
</button>
</div>
<p class="mt-1 break-all text-xs text-blue-600 dark:text-blue-400">
{{ soraSessionUrl }}
</p>
<p class="mt-1 text-xs text-amber-600 dark:text-amber-400">
{{ t(getOAuthKey('sessionUrlHint')) }}
</p>
<p
v-if="parsedSessionTokenCount > 1"
class="mt-1 text-xs text-blue-600 dark:text-blue-400"
>
{{ t('admin.accounts.oauth.batchCreateAccounts', { count: parsedSessionTokenCount }) }}
</p>
</div>
<div v-if="sessionTokenInput.trim()" class="mb-4 space-y-3">
<div>
<label
class="mb-2 flex items-center gap-2 text-xs font-semibold text-gray-700 dark:text-gray-300"
>
{{ t(getOAuthKey('parsedSessionTokensLabel')) }}
<span
v-if="parsedSessionTokenCount > 0"
class="rounded-full bg-emerald-500 px-2 py-0.5 text-[10px] text-white"
>
{{ parsedSessionTokenCount }}
</span>
</label>
<textarea
:value="parsedSessionTokensText"
rows="2"
readonly
class="input w-full resize-y bg-gray-50 font-mono text-xs dark:bg-gray-700"
></textarea>
<p
v-if="parsedSessionTokenCount === 0"
class="mt-1 text-xs text-amber-600 dark:text-amber-400"
>
{{ t(getOAuthKey('parsedSessionTokensEmpty')) }}
</p>
</div>
<div>
<label
class="mb-2 flex items-center gap-2 text-xs font-semibold text-gray-700 dark:text-gray-300"
>
{{ t(getOAuthKey('parsedAccessTokensLabel')) }}
<span
v-if="parsedAccessTokenFromSessionInputCount > 0"
class="rounded-full bg-emerald-500 px-2 py-0.5 text-[10px] text-white"
>
{{ parsedAccessTokenFromSessionInputCount }}
</span>
</label>
<textarea
:value="parsedAccessTokensText"
rows="2"
readonly
class="input w-full resize-y bg-gray-50 font-mono text-xs dark:bg-gray-700"
></textarea>
</div>
</div>
<div
v-if="error"
class="mb-4 rounded-lg border border-red-200 bg-red-50 p-3 dark:border-red-700 dark:bg-red-900/30"
>
<p class="whitespace-pre-line text-sm text-red-600 dark:text-red-400">
{{ error }}
</p>
</div>
<button
type="button"
class="btn btn-primary w-full"
:disabled="loading || parsedSessionTokenCount === 0"
@click="handleValidateSessionToken"
>
<svg
v-if="loading"
class="-ml-1 mr-2 h-4 w-4 animate-spin"
fill="none"
viewBox="0 0 24 24"
>
<circle
class="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
stroke-width="4"
></circle>
<path
class="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
<Icon v-else name="sparkles" size="sm" class="mr-2" />
{{
loading
? t(getOAuthKey('validating'))
: t(getOAuthKey('validateAndCreate'))
}}
</button>
</div>
</div>
<!-- Access Token Input (Sora) -->
<div v-if="inputMethod === 'access_token'" class="space-y-4">
<div
class="rounded-lg border border-blue-300 bg-white/80 p-4 dark:border-blue-600 dark:bg-gray-800/80"
>
<p class="mb-3 text-sm text-blue-700 dark:text-blue-300">
{{ t('admin.accounts.oauth.openai.accessTokenDesc', '直接粘贴 Access Token 创建账号,无需 OAuth 授权流程。支持批量导入(每行一个)。') }}
</p>
<div class="mb-4">
<label
class="mb-2 flex items-center gap-2 text-sm font-semibold text-gray-700 dark:text-gray-300"
>
<Icon name="key" size="sm" class="text-blue-500" />
Access Token
<span
v-if="parsedAccessTokenCount > 1"
class="rounded-full bg-blue-500 px-2 py-0.5 text-xs text-white"
>
{{ t('admin.accounts.oauth.keysCount', { count: parsedAccessTokenCount }) }}
</span>
</label>
<textarea
v-model="accessTokenInput"
rows="3"
class="input w-full resize-y font-mono text-sm"
:placeholder="t('admin.accounts.oauth.openai.accessTokenPlaceholder', '粘贴 Access Token,每行一个')"
></textarea>
<p
v-if="parsedAccessTokenCount > 1"
class="mt-1 text-xs text-blue-600 dark:text-blue-400"
>
{{ t('admin.accounts.oauth.batchCreateAccounts', { count: parsedAccessTokenCount }) }}
</p>
</div>
<div
v-if="error"
class="mb-4 rounded-lg border border-red-200 bg-red-50 p-3 dark:border-red-700 dark:bg-red-900/30"
>
<p class="whitespace-pre-line text-sm text-red-600 dark:text-red-400">
{{ error }}
</p>
</div>
<button
type="button"
class="btn btn-primary w-full"
:disabled="loading || !accessTokenInput.trim()"
@click="handleImportAccessToken"
>
<Icon name="sparkles" size="sm" class="mr-2" />
{{ t('admin.accounts.oauth.openai.importAccessToken', '导入 Access Token') }}
</button>
</div>
</div>
<!-- Cookie Auto-Auth Form --> <!-- Cookie Auto-Auth Form -->
<div v-if="inputMethod === 'cookie'" class="space-y-4"> <div v-if="inputMethod === 'cookie'" class="space-y-4">
<div <div
...@@ -753,7 +542,6 @@ ...@@ -753,7 +542,6 @@
import { ref, computed, watch } from 'vue' import { ref, computed, watch } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useClipboard } from '@/composables/useClipboard' import { useClipboard } from '@/composables/useClipboard'
import { parseSoraRawTokens } from '@/utils/soraTokenParser'
import Icon from '@/components/icons/Icon.vue' import Icon from '@/components/icons/Icon.vue'
import type { AddMethod, AuthInputMethod } from '@/composables/useAccountOAuth' import type { AddMethod, AuthInputMethod } from '@/composables/useAccountOAuth'
import type { AccountPlatform } from '@/types' import type { AccountPlatform } from '@/types'
...@@ -771,8 +559,8 @@ interface Props { ...@@ -771,8 +559,8 @@ interface Props {
showCookieOption?: boolean // Whether to show cookie auto-auth option showCookieOption?: boolean // Whether to show cookie auto-auth option
showRefreshTokenOption?: boolean // Whether to show refresh token input option (OpenAI only) showRefreshTokenOption?: boolean // Whether to show refresh token input option (OpenAI only)
showMobileRefreshTokenOption?: boolean // Whether to show mobile refresh token option (OpenAI only) showMobileRefreshTokenOption?: boolean // Whether to show mobile refresh token option (OpenAI only)
showSessionTokenOption?: boolean // Whether to show session token input option (Sora only) showSessionTokenOption?: boolean
showAccessTokenOption?: boolean // Whether to show access token input option (Sora only) showAccessTokenOption?: boolean
platform?: AccountPlatform // Platform type for different UI/text platform?: AccountPlatform // Platform type for different UI/text
showProjectId?: boolean // New prop to control project ID visibility showProjectId?: boolean // New prop to control project ID visibility
} }
...@@ -808,11 +596,11 @@ const emit = defineEmits<{ ...@@ -808,11 +596,11 @@ const emit = defineEmits<{
const { t } = useI18n() const { t } = useI18n()
const isOpenAI = computed(() => props.platform === 'openai' || props.platform === 'sora') const isOpenAI = computed(() => props.platform === 'openai')
// Get translation key based on platform // Get translation key based on platform
const getOAuthKey = (key: string) => { const getOAuthKey = (key: string) => {
if (props.platform === 'openai' || props.platform === 'sora') return `admin.accounts.oauth.openai.${key}` if (props.platform === 'openai') return `admin.accounts.oauth.openai.${key}`
if (props.platform === 'gemini') return `admin.accounts.oauth.gemini.${key}` if (props.platform === 'gemini') return `admin.accounts.oauth.gemini.${key}`
if (props.platform === 'antigravity') return `admin.accounts.oauth.antigravity.${key}` if (props.platform === 'antigravity') return `admin.accounts.oauth.antigravity.${key}`
return `admin.accounts.oauth.${key}` return `admin.accounts.oauth.${key}`
...@@ -831,7 +619,7 @@ const oauthAuthCode = computed(() => t(getOAuthKey('authCode'))) ...@@ -831,7 +619,7 @@ const oauthAuthCode = computed(() => t(getOAuthKey('authCode')))
const oauthAuthCodePlaceholder = computed(() => t(getOAuthKey('authCodePlaceholder'))) const oauthAuthCodePlaceholder = computed(() => t(getOAuthKey('authCodePlaceholder')))
const oauthAuthCodeHint = computed(() => t(getOAuthKey('authCodeHint'))) const oauthAuthCodeHint = computed(() => t(getOAuthKey('authCodeHint')))
const oauthImportantNotice = computed(() => { const oauthImportantNotice = computed(() => {
if (props.platform === 'openai' || props.platform === 'sora') return t('admin.accounts.oauth.openai.importantNotice') if (props.platform === 'openai') return t('admin.accounts.oauth.openai.importantNotice')
if (props.platform === 'antigravity') return t('admin.accounts.oauth.antigravity.importantNotice') if (props.platform === 'antigravity') return t('admin.accounts.oauth.antigravity.importantNotice')
return '' return ''
}) })
...@@ -842,7 +630,6 @@ const authCodeInput = ref('') ...@@ -842,7 +630,6 @@ const authCodeInput = ref('')
const sessionKeyInput = ref('') const sessionKeyInput = ref('')
const refreshTokenInput = ref('') const refreshTokenInput = ref('')
const sessionTokenInput = ref('') const sessionTokenInput = ref('')
const accessTokenInput = ref('')
const showHelpDialog = ref(false) const showHelpDialog = ref(false)
const oauthState = ref('') const oauthState = ref('')
const projectId = ref('') const projectId = ref('')
...@@ -869,33 +656,6 @@ const parsedRefreshTokenCount = computed(() => { ...@@ -869,33 +656,6 @@ const parsedRefreshTokenCount = computed(() => {
.filter((rt) => rt).length .filter((rt) => rt).length
}) })
const parsedSoraRawTokens = computed(() => parseSoraRawTokens(sessionTokenInput.value))
const parsedSessionTokenCount = computed(() => {
return parsedSoraRawTokens.value.sessionTokens.length
})
const parsedSessionTokensText = computed(() => {
return parsedSoraRawTokens.value.sessionTokens.join('\n')
})
const parsedAccessTokenFromSessionInputCount = computed(() => {
return parsedSoraRawTokens.value.accessTokens.length
})
const parsedAccessTokensText = computed(() => {
return parsedSoraRawTokens.value.accessTokens.join('\n')
})
const soraSessionUrl = 'https://sora.chatgpt.com/api/auth/session'
const parsedAccessTokenCount = computed(() => {
return accessTokenInput.value
.split('\n')
.map((at) => at.trim())
.filter((at) => at).length
})
// Watchers // Watchers
watch(inputMethod, (newVal) => { watch(inputMethod, (newVal) => {
emit('update:inputMethod', newVal) emit('update:inputMethod', newVal)
...@@ -904,7 +664,7 @@ watch(inputMethod, (newVal) => { ...@@ -904,7 +664,7 @@ watch(inputMethod, (newVal) => {
// Auto-extract code from callback URL (OpenAI/Gemini/Antigravity) // Auto-extract code from callback URL (OpenAI/Gemini/Antigravity)
// e.g., http://localhost:8085/callback?code=xxx...&state=... // e.g., http://localhost:8085/callback?code=xxx...&state=...
watch(authCodeInput, (newVal) => { watch(authCodeInput, (newVal) => {
if (props.platform !== 'openai' && props.platform !== 'gemini' && props.platform !== 'antigravity' && props.platform !== 'sora') return if (props.platform !== 'openai' && props.platform !== 'gemini' && props.platform !== 'antigravity') return
const trimmed = newVal.trim() const trimmed = newVal.trim()
// Check if it looks like a URL with code parameter // Check if it looks like a URL with code parameter
...@@ -914,7 +674,7 @@ watch(authCodeInput, (newVal) => { ...@@ -914,7 +674,7 @@ watch(authCodeInput, (newVal) => {
const url = new URL(trimmed) const url = new URL(trimmed)
const code = url.searchParams.get('code') const code = url.searchParams.get('code')
const stateParam = url.searchParams.get('state') const stateParam = url.searchParams.get('state')
if ((props.platform === 'openai' || props.platform === 'sora' || props.platform === 'gemini' || props.platform === 'antigravity') && stateParam) { if ((props.platform === 'openai' || props.platform === 'gemini' || props.platform === 'antigravity') && stateParam) {
oauthState.value = stateParam oauthState.value = stateParam
} }
if (code && code !== trimmed) { if (code && code !== trimmed) {
...@@ -925,7 +685,7 @@ watch(authCodeInput, (newVal) => { ...@@ -925,7 +685,7 @@ watch(authCodeInput, (newVal) => {
// If URL parsing fails, try regex extraction // If URL parsing fails, try regex extraction
const match = trimmed.match(/[?&]code=([^&]+)/) const match = trimmed.match(/[?&]code=([^&]+)/)
const stateMatch = trimmed.match(/[?&]state=([^&]+)/) const stateMatch = trimmed.match(/[?&]state=([^&]+)/)
if ((props.platform === 'openai' || props.platform === 'sora' || props.platform === 'gemini' || props.platform === 'antigravity') && stateMatch && stateMatch[1]) { if ((props.platform === 'openai' || props.platform === 'gemini' || props.platform === 'antigravity') && stateMatch && stateMatch[1]) {
oauthState.value = stateMatch[1] oauthState.value = stateMatch[1]
} }
if (match && match[1] && match[1] !== trimmed) { if (match && match[1] && match[1] !== trimmed) {
...@@ -967,26 +727,6 @@ const handleValidateRefreshToken = () => { ...@@ -967,26 +727,6 @@ const handleValidateRefreshToken = () => {
} }
} }
const handleValidateSessionToken = () => {
if (parsedSessionTokenCount.value > 0) {
emit('validate-session-token', parsedSessionTokensText.value)
}
}
const handleOpenSoraSessionUrl = () => {
window.open(soraSessionUrl, '_blank', 'noopener,noreferrer')
}
const handleCopySoraSessionUrl = () => {
copyToClipboard(soraSessionUrl, 'URL copied to clipboard')
}
const handleImportAccessToken = () => {
if (accessTokenInput.value.trim()) {
emit('import-access-token', accessTokenInput.value.trim())
}
}
// Expose methods and state // Expose methods and state
defineExpose({ defineExpose({
authCode: authCodeInput, authCode: authCodeInput,
......
...@@ -33,8 +33,6 @@ ...@@ -33,8 +33,6 @@
{{ {{
isOpenAI isOpenAI
? t('admin.accounts.openaiAccount') ? t('admin.accounts.openaiAccount')
: isSora
? t('admin.accounts.soraAccount')
: isGemini : isGemini
? t('admin.accounts.geminiAccount') ? t('admin.accounts.geminiAccount')
: isAntigravity : isAntigravity
...@@ -130,7 +128,7 @@ ...@@ -130,7 +128,7 @@
:show-cookie-option="isAnthropic" :show-cookie-option="isAnthropic"
:allow-multiple="false" :allow-multiple="false"
:method-label="t('admin.accounts.inputMethod')" :method-label="t('admin.accounts.inputMethod')"
:platform="isOpenAI ? 'openai' : isSora ? 'sora' : isGemini ? 'gemini' : isAntigravity ? 'antigravity' : 'anthropic'" :platform="isOpenAI ? 'openai' : isGemini ? 'gemini' : isAntigravity ? 'antigravity' : 'anthropic'"
:show-project-id="isGemini && geminiOAuthType === 'code_assist'" :show-project-id="isGemini && geminiOAuthType === 'code_assist'"
@generate-url="handleGenerateUrl" @generate-url="handleGenerateUrl"
@cookie-auth="handleCookieAuth" @cookie-auth="handleCookieAuth"
...@@ -226,8 +224,7 @@ const { t } = useI18n() ...@@ -226,8 +224,7 @@ const { t } = useI18n()
// OAuth composables // OAuth composables
const claudeOAuth = useAccountOAuth() const claudeOAuth = useAccountOAuth()
const openaiOAuth = useOpenAIOAuth({ platform: 'openai' }) const openaiOAuth = useOpenAIOAuth()
const soraOAuth = useOpenAIOAuth({ platform: 'sora' })
const geminiOAuth = useGeminiOAuth() const geminiOAuth = useGeminiOAuth()
const antigravityOAuth = useAntigravityOAuth() const antigravityOAuth = useAntigravityOAuth()
...@@ -240,34 +237,32 @@ const geminiOAuthType = ref<'code_assist' | 'google_one' | 'ai_studio'>('code_as ...@@ -240,34 +237,32 @@ const geminiOAuthType = ref<'code_assist' | 'google_one' | 'ai_studio'>('code_as
// Computed - check platform // Computed - check platform
const isOpenAI = computed(() => props.account?.platform === 'openai') const isOpenAI = computed(() => props.account?.platform === 'openai')
const isSora = computed(() => props.account?.platform === 'sora') const isOpenAILike = computed(() => isOpenAI.value)
const isOpenAILike = computed(() => isOpenAI.value || isSora.value)
const isGemini = computed(() => props.account?.platform === 'gemini') const isGemini = computed(() => props.account?.platform === 'gemini')
const isAnthropic = computed(() => props.account?.platform === 'anthropic') const isAnthropic = computed(() => props.account?.platform === 'anthropic')
const isAntigravity = computed(() => props.account?.platform === 'antigravity') const isAntigravity = computed(() => props.account?.platform === 'antigravity')
const activeOpenAIOAuth = computed(() => (isSora.value ? soraOAuth : openaiOAuth))
// Computed - current OAuth state based on platform // Computed - current OAuth state based on platform
const currentAuthUrl = computed(() => { const currentAuthUrl = computed(() => {
if (isOpenAILike.value) return activeOpenAIOAuth.value.authUrl.value if (isOpenAILike.value) return openaiOAuth.authUrl.value
if (isGemini.value) return geminiOAuth.authUrl.value if (isGemini.value) return geminiOAuth.authUrl.value
if (isAntigravity.value) return antigravityOAuth.authUrl.value if (isAntigravity.value) return antigravityOAuth.authUrl.value
return claudeOAuth.authUrl.value return claudeOAuth.authUrl.value
}) })
const currentSessionId = computed(() => { const currentSessionId = computed(() => {
if (isOpenAILike.value) return activeOpenAIOAuth.value.sessionId.value if (isOpenAILike.value) return openaiOAuth.sessionId.value
if (isGemini.value) return geminiOAuth.sessionId.value if (isGemini.value) return geminiOAuth.sessionId.value
if (isAntigravity.value) return antigravityOAuth.sessionId.value if (isAntigravity.value) return antigravityOAuth.sessionId.value
return claudeOAuth.sessionId.value return claudeOAuth.sessionId.value
}) })
const currentLoading = computed(() => { const currentLoading = computed(() => {
if (isOpenAILike.value) return activeOpenAIOAuth.value.loading.value if (isOpenAILike.value) return openaiOAuth.loading.value
if (isGemini.value) return geminiOAuth.loading.value if (isGemini.value) return geminiOAuth.loading.value
if (isAntigravity.value) return antigravityOAuth.loading.value if (isAntigravity.value) return antigravityOAuth.loading.value
return claudeOAuth.loading.value return claudeOAuth.loading.value
}) })
const currentError = computed(() => { const currentError = computed(() => {
if (isOpenAILike.value) return activeOpenAIOAuth.value.error.value if (isOpenAILike.value) return openaiOAuth.error.value
if (isGemini.value) return geminiOAuth.error.value if (isGemini.value) return geminiOAuth.error.value
if (isAntigravity.value) return antigravityOAuth.error.value if (isAntigravity.value) return antigravityOAuth.error.value
return claudeOAuth.error.value return claudeOAuth.error.value
...@@ -275,7 +270,7 @@ const currentError = computed(() => { ...@@ -275,7 +270,7 @@ const currentError = computed(() => {
// Computed // Computed
const isManualInputMethod = computed(() => { const isManualInputMethod = computed(() => {
// OpenAI/Sora/Gemini/Antigravity always use manual input (no cookie auth option) // OpenAI/Gemini/Antigravity always use manual input (no cookie auth option)
return isOpenAILike.value || isGemini.value || isAntigravity.value || oauthFlowRef.value?.inputMethod === 'manual' return isOpenAILike.value || isGemini.value || isAntigravity.value || oauthFlowRef.value?.inputMethod === 'manual'
}) })
...@@ -319,7 +314,6 @@ const resetState = () => { ...@@ -319,7 +314,6 @@ const resetState = () => {
geminiOAuthType.value = 'code_assist' geminiOAuthType.value = 'code_assist'
claudeOAuth.resetState() claudeOAuth.resetState()
openaiOAuth.resetState() openaiOAuth.resetState()
soraOAuth.resetState()
geminiOAuth.resetState() geminiOAuth.resetState()
antigravityOAuth.resetState() antigravityOAuth.resetState()
oauthFlowRef.value?.reset() oauthFlowRef.value?.reset()
...@@ -333,7 +327,7 @@ const handleGenerateUrl = async () => { ...@@ -333,7 +327,7 @@ const handleGenerateUrl = async () => {
if (!props.account) return if (!props.account) return
if (isOpenAILike.value) { if (isOpenAILike.value) {
await activeOpenAIOAuth.value.generateAuthUrl(props.account.proxy_id) await openaiOAuth.generateAuthUrl(props.account.proxy_id)
} else if (isGemini.value) { } else if (isGemini.value) {
const creds = (props.account.credentials || {}) as Record<string, unknown> const creds = (props.account.credentials || {}) as Record<string, unknown>
const tierId = typeof creds.tier_id === 'string' ? creds.tier_id : undefined const tierId = typeof creds.tier_id === 'string' ? creds.tier_id : undefined
...@@ -354,7 +348,7 @@ const handleExchangeCode = async () => { ...@@ -354,7 +348,7 @@ const handleExchangeCode = async () => {
if (isOpenAILike.value) { if (isOpenAILike.value) {
// OpenAI OAuth flow // OpenAI OAuth flow
const oauthClient = activeOpenAIOAuth.value const oauthClient = openaiOAuth
const sessionId = oauthClient.sessionId.value const sessionId = oauthClient.sessionId.value
if (!sessionId) return if (!sessionId) return
const stateToUse = (oauthFlowRef.value?.oauthState || oauthClient.oauthState.value || '').trim() const stateToUse = (oauthFlowRef.value?.oauthState || oauthClient.oauthState.value || '').trim()
......
...@@ -25,7 +25,7 @@ const updateType = (value: string | number | boolean | null) => { emit('update:f ...@@ -25,7 +25,7 @@ const updateType = (value: string | number | boolean | null) => { emit('update:f
const updateStatus = (value: string | number | boolean | null) => { emit('update:filters', { ...props.filters, status: value }) } const updateStatus = (value: string | number | boolean | null) => { emit('update:filters', { ...props.filters, status: value }) }
const updatePrivacyMode = (value: string | number | boolean | null) => { emit('update:filters', { ...props.filters, privacy_mode: value }) } const updatePrivacyMode = (value: string | number | boolean | null) => { emit('update:filters', { ...props.filters, privacy_mode: value }) }
const updateGroup = (value: string | number | boolean | null) => { emit('update:filters', { ...props.filters, group: value }) } const updateGroup = (value: string | number | boolean | null) => { emit('update:filters', { ...props.filters, group: value }) }
const pOpts = computed(() => [{ value: '', label: t('admin.accounts.allPlatforms') }, { value: 'anthropic', label: 'Anthropic' }, { value: 'openai', label: 'OpenAI' }, { value: 'gemini', label: 'Gemini' }, { value: 'antigravity', label: 'Antigravity' }, { value: 'sora', label: 'Sora' }]) const pOpts = computed(() => [{ value: '', label: t('admin.accounts.allPlatforms') }, { value: 'anthropic', label: 'Anthropic' }, { value: 'openai', label: 'OpenAI' }, { value: 'gemini', label: 'Gemini' }, { value: 'antigravity', label: 'Antigravity' }])
const tOpts = computed(() => [{ value: '', label: t('admin.accounts.allTypes') }, { value: 'oauth', label: t('admin.accounts.oauthType') }, { value: 'setup-token', label: t('admin.accounts.setupToken') }, { value: 'apikey', label: t('admin.accounts.apiKey') }, { value: 'bedrock', label: 'AWS Bedrock' }]) const tOpts = computed(() => [{ value: '', label: t('admin.accounts.allTypes') }, { value: 'oauth', label: t('admin.accounts.oauthType') }, { value: 'setup-token', label: t('admin.accounts.setupToken') }, { value: 'apikey', label: t('admin.accounts.apiKey') }, { value: 'bedrock', label: 'AWS Bedrock' }])
const sOpts = computed(() => [{ value: '', label: t('admin.accounts.allStatus') }, { value: 'active', label: t('admin.accounts.status.active') }, { value: 'inactive', label: t('admin.accounts.status.inactive') }, { value: 'error', label: t('admin.accounts.status.error') }, { value: 'rate_limited', label: t('admin.accounts.status.rateLimited') }, { value: 'temp_unschedulable', label: t('admin.accounts.status.tempUnschedulable') }]) const sOpts = computed(() => [{ value: '', label: t('admin.accounts.allStatus') }, { value: 'active', label: t('admin.accounts.status.active') }, { value: 'inactive', label: t('admin.accounts.status.inactive') }, { value: 'error', label: t('admin.accounts.status.error') }, { value: 'rate_limited', label: t('admin.accounts.status.rateLimited') }, { value: 'temp_unschedulable', label: t('admin.accounts.status.tempUnschedulable') }])
const privacyOpts = computed(() => [ const privacyOpts = computed(() => [
......
...@@ -41,7 +41,7 @@ ...@@ -41,7 +41,7 @@
</span> </span>
</div> </div>
<div v-if="!isSoraAccount" class="space-y-1.5"> <div class="space-y-1.5">
<label class="text-sm font-medium text-gray-700 dark:text-gray-300"> <label class="text-sm font-medium text-gray-700 dark:text-gray-300">
{{ t('admin.accounts.selectTestModel') }} {{ t('admin.accounts.selectTestModel') }}
</label> </label>
...@@ -54,12 +54,6 @@ ...@@ -54,12 +54,6 @@
:placeholder="loadingModels ? t('common.loading') + '...' : t('admin.accounts.selectTestModel')" :placeholder="loadingModels ? t('common.loading') + '...' : t('admin.accounts.selectTestModel')"
/> />
</div> </div>
<div
v-else
class="rounded-lg border border-blue-200 bg-blue-50 px-3 py-2 text-xs text-blue-700 dark:border-blue-700 dark:bg-blue-900/20 dark:text-blue-300"
>
{{ t('admin.accounts.soraTestHint') }}
</div>
<div v-if="supportsGeminiImageTest" class="space-y-1.5"> <div v-if="supportsGeminiImageTest" class="space-y-1.5">
<TextArea <TextArea
...@@ -152,17 +146,15 @@ ...@@ -152,17 +146,15 @@
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<span class="flex items-center gap-1"> <span class="flex items-center gap-1">
<Icon name="grid" size="sm" :stroke-width="2" /> <Icon name="grid" size="sm" :stroke-width="2" />
{{ isSoraAccount ? t('admin.accounts.soraTestTarget') : t('admin.accounts.testModel') }} {{ t('admin.accounts.testModel') }}
</span> </span>
</div> </div>
<span class="flex items-center gap-1"> <span class="flex items-center gap-1">
<Icon name="chat" size="sm" :stroke-width="2" /> <Icon name="chat" size="sm" :stroke-width="2" />
{{ {{
isSoraAccount supportsGeminiImageTest
? t('admin.accounts.soraTestMode') ? t('admin.accounts.geminiImageTestMode')
: supportsGeminiImageTest : t('admin.accounts.testPrompt')
? t('admin.accounts.geminiImageTestMode')
: t('admin.accounts.testPrompt')
}} }}
</span> </span>
</div> </div>
...@@ -179,10 +171,10 @@ ...@@ -179,10 +171,10 @@
</button> </button>
<button <button
@click="startTest" @click="startTest"
:disabled="status === 'connecting' || (!isSoraAccount && !selectedModelId)" :disabled="status === 'connecting' || !selectedModelId"
:class="[ :class="[
'flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium transition-all', 'flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium transition-all',
status === 'connecting' || (!isSoraAccount && !selectedModelId) status === 'connecting' || !selectedModelId
? 'cursor-not-allowed bg-primary-400 text-white' ? 'cursor-not-allowed bg-primary-400 text-white'
: status === 'success' : status === 'success'
? 'bg-green-500 text-white hover:bg-green-600' ? 'bg-green-500 text-white hover:bg-green-600'
...@@ -258,11 +250,9 @@ const selectedModelId = ref('') ...@@ -258,11 +250,9 @@ const selectedModelId = ref('')
const testPrompt = ref('') const testPrompt = ref('')
const loadingModels = ref(false) const loadingModels = ref(false)
let eventSource: EventSource | null = null let eventSource: EventSource | null = null
const isSoraAccount = computed(() => props.account?.platform === 'sora')
const generatedImages = ref<PreviewImage[]>([]) const generatedImages = ref<PreviewImage[]>([])
const prioritizedGeminiModels = ['gemini-3.1-flash-image', 'gemini-2.5-flash-image', 'gemini-2.5-flash', 'gemini-2.5-pro', 'gemini-3-flash-preview', 'gemini-3-pro-preview', 'gemini-2.0-flash'] const prioritizedGeminiModels = ['gemini-3.1-flash-image', 'gemini-2.5-flash-image', 'gemini-2.5-flash', 'gemini-2.5-pro', 'gemini-3-flash-preview', 'gemini-3-pro-preview', 'gemini-2.0-flash']
const supportsGeminiImageTest = computed(() => { const supportsGeminiImageTest = computed(() => {
if (isSoraAccount.value) return false
const modelID = selectedModelId.value.toLowerCase() const modelID = selectedModelId.value.toLowerCase()
if (!modelID.startsWith('gemini-') || !modelID.includes('-image')) return false if (!modelID.startsWith('gemini-') || !modelID.includes('-image')) return false
...@@ -302,12 +292,6 @@ watch(selectedModelId, () => { ...@@ -302,12 +292,6 @@ watch(selectedModelId, () => {
const loadAvailableModels = async () => { const loadAvailableModels = async () => {
if (!props.account) return if (!props.account) return
if (props.account.platform === 'sora') {
availableModels.value = []
selectedModelId.value = ''
loadingModels.value = false
return
}
loadingModels.value = true loadingModels.value = true
selectedModelId.value = '' // Reset selection before loading selectedModelId.value = '' // Reset selection before loading
...@@ -373,7 +357,7 @@ const scrollToBottom = async () => { ...@@ -373,7 +357,7 @@ const scrollToBottom = async () => {
} }
const startTest = async () => { const startTest = async () => {
if (!props.account || (!isSoraAccount.value && !selectedModelId.value)) return if (!props.account || !selectedModelId.value) return
resetState() resetState()
status.value = 'connecting' status.value = 'connecting'
...@@ -394,14 +378,10 @@ const startTest = async () => { ...@@ -394,14 +378,10 @@ const startTest = async () => {
Authorization: `Bearer ${localStorage.getItem('auth_token')}`, Authorization: `Bearer ${localStorage.getItem('auth_token')}`,
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}, },
body: JSON.stringify( body: JSON.stringify({
isSoraAccount.value
? {}
: {
model_id: selectedModelId.value, model_id: selectedModelId.value,
prompt: supportsGeminiImageTest.value ? testPrompt.value.trim() : '' prompt: supportsGeminiImageTest.value ? testPrompt.value.trim() : ''
} })
)
}) })
if (!response.ok) { if (!response.ok) {
...@@ -461,9 +441,7 @@ const handleEvent = (event: { ...@@ -461,9 +441,7 @@ const handleEvent = (event: {
addLine(t('admin.accounts.usingModel', { model: event.model }), 'text-cyan-400') addLine(t('admin.accounts.usingModel', { model: event.model }), 'text-cyan-400')
} }
addLine( addLine(
isSoraAccount.value supportsGeminiImageTest.value
? t('admin.accounts.soraTestingFlow')
: supportsGeminiImageTest.value
? t('admin.accounts.sendingGeminiImageRequest') ? t('admin.accounts.sendingGeminiImageRequest')
: t('admin.accounts.sendingTestMessage'), : t('admin.accounts.sendingTestMessage'),
'text-gray-400' 'text-gray-400'
......
...@@ -33,8 +33,6 @@ ...@@ -33,8 +33,6 @@
{{ {{
isOpenAI isOpenAI
? t('admin.accounts.openaiAccount') ? t('admin.accounts.openaiAccount')
: isSora
? t('admin.accounts.soraAccount')
: isGemini : isGemini
? t('admin.accounts.geminiAccount') ? t('admin.accounts.geminiAccount')
: isAntigravity : isAntigravity
...@@ -130,7 +128,7 @@ ...@@ -130,7 +128,7 @@
:show-cookie-option="isAnthropic" :show-cookie-option="isAnthropic"
:allow-multiple="false" :allow-multiple="false"
:method-label="t('admin.accounts.inputMethod')" :method-label="t('admin.accounts.inputMethod')"
:platform="isOpenAI ? 'openai' : isSora ? 'sora' : isGemini ? 'gemini' : isAntigravity ? 'antigravity' : 'anthropic'" :platform="isOpenAI ? 'openai' : isGemini ? 'gemini' : isAntigravity ? 'antigravity' : 'anthropic'"
:show-project-id="isGemini && geminiOAuthType === 'code_assist'" :show-project-id="isGemini && geminiOAuthType === 'code_assist'"
@generate-url="handleGenerateUrl" @generate-url="handleGenerateUrl"
@cookie-auth="handleCookieAuth" @cookie-auth="handleCookieAuth"
...@@ -226,8 +224,7 @@ const { t } = useI18n() ...@@ -226,8 +224,7 @@ const { t } = useI18n()
// OAuth composables // OAuth composables
const claudeOAuth = useAccountOAuth() const claudeOAuth = useAccountOAuth()
const openaiOAuth = useOpenAIOAuth({ platform: 'openai' }) const openaiOAuth = useOpenAIOAuth()
const soraOAuth = useOpenAIOAuth({ platform: 'sora' })
const geminiOAuth = useGeminiOAuth() const geminiOAuth = useGeminiOAuth()
const antigravityOAuth = useAntigravityOAuth() const antigravityOAuth = useAntigravityOAuth()
...@@ -240,34 +237,32 @@ const geminiOAuthType = ref<'code_assist' | 'google_one' | 'ai_studio'>('code_as ...@@ -240,34 +237,32 @@ const geminiOAuthType = ref<'code_assist' | 'google_one' | 'ai_studio'>('code_as
// Computed - check platform // Computed - check platform
const isOpenAI = computed(() => props.account?.platform === 'openai') const isOpenAI = computed(() => props.account?.platform === 'openai')
const isSora = computed(() => props.account?.platform === 'sora') const isOpenAILike = computed(() => isOpenAI.value)
const isOpenAILike = computed(() => isOpenAI.value || isSora.value)
const isGemini = computed(() => props.account?.platform === 'gemini') const isGemini = computed(() => props.account?.platform === 'gemini')
const isAnthropic = computed(() => props.account?.platform === 'anthropic') const isAnthropic = computed(() => props.account?.platform === 'anthropic')
const isAntigravity = computed(() => props.account?.platform === 'antigravity') const isAntigravity = computed(() => props.account?.platform === 'antigravity')
const activeOpenAIOAuth = computed(() => (isSora.value ? soraOAuth : openaiOAuth))
// Computed - current OAuth state based on platform // Computed - current OAuth state based on platform
const currentAuthUrl = computed(() => { const currentAuthUrl = computed(() => {
if (isOpenAILike.value) return activeOpenAIOAuth.value.authUrl.value if (isOpenAILike.value) return openaiOAuth.authUrl.value
if (isGemini.value) return geminiOAuth.authUrl.value if (isGemini.value) return geminiOAuth.authUrl.value
if (isAntigravity.value) return antigravityOAuth.authUrl.value if (isAntigravity.value) return antigravityOAuth.authUrl.value
return claudeOAuth.authUrl.value return claudeOAuth.authUrl.value
}) })
const currentSessionId = computed(() => { const currentSessionId = computed(() => {
if (isOpenAILike.value) return activeOpenAIOAuth.value.sessionId.value if (isOpenAILike.value) return openaiOAuth.sessionId.value
if (isGemini.value) return geminiOAuth.sessionId.value if (isGemini.value) return geminiOAuth.sessionId.value
if (isAntigravity.value) return antigravityOAuth.sessionId.value if (isAntigravity.value) return antigravityOAuth.sessionId.value
return claudeOAuth.sessionId.value return claudeOAuth.sessionId.value
}) })
const currentLoading = computed(() => { const currentLoading = computed(() => {
if (isOpenAILike.value) return activeOpenAIOAuth.value.loading.value if (isOpenAILike.value) return openaiOAuth.loading.value
if (isGemini.value) return geminiOAuth.loading.value if (isGemini.value) return geminiOAuth.loading.value
if (isAntigravity.value) return antigravityOAuth.loading.value if (isAntigravity.value) return antigravityOAuth.loading.value
return claudeOAuth.loading.value return claudeOAuth.loading.value
}) })
const currentError = computed(() => { const currentError = computed(() => {
if (isOpenAILike.value) return activeOpenAIOAuth.value.error.value if (isOpenAILike.value) return openaiOAuth.error.value
if (isGemini.value) return geminiOAuth.error.value if (isGemini.value) return geminiOAuth.error.value
if (isAntigravity.value) return antigravityOAuth.error.value if (isAntigravity.value) return antigravityOAuth.error.value
return claudeOAuth.error.value return claudeOAuth.error.value
...@@ -275,7 +270,7 @@ const currentError = computed(() => { ...@@ -275,7 +270,7 @@ const currentError = computed(() => {
// Computed // Computed
const isManualInputMethod = computed(() => { const isManualInputMethod = computed(() => {
// OpenAI/Sora/Gemini/Antigravity always use manual input (no cookie auth option) // OpenAI/Gemini/Antigravity always use manual input (no cookie auth option)
return isOpenAILike.value || isGemini.value || isAntigravity.value || oauthFlowRef.value?.inputMethod === 'manual' return isOpenAILike.value || isGemini.value || isAntigravity.value || oauthFlowRef.value?.inputMethod === 'manual'
}) })
...@@ -319,7 +314,6 @@ const resetState = () => { ...@@ -319,7 +314,6 @@ const resetState = () => {
geminiOAuthType.value = 'code_assist' geminiOAuthType.value = 'code_assist'
claudeOAuth.resetState() claudeOAuth.resetState()
openaiOAuth.resetState() openaiOAuth.resetState()
soraOAuth.resetState()
geminiOAuth.resetState() geminiOAuth.resetState()
antigravityOAuth.resetState() antigravityOAuth.resetState()
oauthFlowRef.value?.reset() oauthFlowRef.value?.reset()
...@@ -333,7 +327,7 @@ const handleGenerateUrl = async () => { ...@@ -333,7 +327,7 @@ const handleGenerateUrl = async () => {
if (!props.account) return if (!props.account) return
if (isOpenAILike.value) { if (isOpenAILike.value) {
await activeOpenAIOAuth.value.generateAuthUrl(props.account.proxy_id) await openaiOAuth.generateAuthUrl(props.account.proxy_id)
} else if (isGemini.value) { } else if (isGemini.value) {
const creds = (props.account.credentials || {}) as Record<string, unknown> const creds = (props.account.credentials || {}) as Record<string, unknown>
const tierId = typeof creds.tier_id === 'string' ? creds.tier_id : undefined const tierId = typeof creds.tier_id === 'string' ? creds.tier_id : undefined
...@@ -354,7 +348,7 @@ const handleExchangeCode = async () => { ...@@ -354,7 +348,7 @@ const handleExchangeCode = async () => {
if (isOpenAILike.value) { if (isOpenAILike.value) {
// OpenAI OAuth flow // OpenAI OAuth flow
const oauthClient = activeOpenAIOAuth.value const oauthClient = openaiOAuth
const sessionId = oauthClient.sessionId.value const sessionId = oauthClient.sessionId.value
if (!sessionId) return if (!sessionId) return
const stateToUse = (oauthFlowRef.value?.oauthState || oauthClient.oauthState.value || '').trim() const stateToUse = (oauthFlowRef.value?.oauthState || oauthClient.oauthState.value || '').trim()
......
...@@ -184,7 +184,6 @@ export function getPlatformTagClass(platform: string): string { ...@@ -184,7 +184,6 @@ export function getPlatformTagClass(platform: string): string {
case 'openai': return 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400' case 'openai': return 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400'
case 'gemini': return 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400' case 'gemini': return 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400'
case 'antigravity': return 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400' case 'antigravity': return 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400'
case 'sora': return 'bg-rose-100 text-rose-700 dark:bg-rose-900/30 dark:text-rose-400'
default: return 'bg-gray-100 text-gray-700 dark:bg-gray-900/30 dark:text-gray-400' default: return 'bg-gray-100 text-gray-700 dark:bg-gray-900/30 dark:text-gray-400'
} }
} }
...@@ -37,14 +37,6 @@ ...@@ -37,14 +37,6 @@
<label class="input-label">{{ t('admin.users.columns.concurrency') }}</label> <label class="input-label">{{ t('admin.users.columns.concurrency') }}</label>
<input v-model.number="form.concurrency" type="number" class="input" /> <input v-model.number="form.concurrency" type="number" class="input" />
</div> </div>
<div>
<label class="input-label">{{ t('admin.users.soraStorageQuota') }}</label>
<div class="flex items-center gap-2">
<input v-model.number="form.sora_storage_quota_gb" type="number" min="0" step="0.1" class="input" placeholder="0" />
<span class="shrink-0 text-sm text-gray-500">GB</span>
</div>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">{{ t('admin.users.soraStorageQuotaHint') }}</p>
</div>
<UserAttributeForm v-model="form.customAttributes" :user-id="user?.id" /> <UserAttributeForm v-model="form.customAttributes" :user-id="user?.id" />
</form> </form>
<template #footer> <template #footer>
...@@ -74,11 +66,11 @@ const emit = defineEmits(['close', 'success']) ...@@ -74,11 +66,11 @@ const emit = defineEmits(['close', 'success'])
const { t } = useI18n(); const appStore = useAppStore(); const { copyToClipboard } = useClipboard() const { t } = useI18n(); const appStore = useAppStore(); const { copyToClipboard } = useClipboard()
const submitting = ref(false); const passwordCopied = ref(false) const submitting = ref(false); const passwordCopied = ref(false)
const form = reactive({ email: '', password: '', username: '', notes: '', concurrency: 1, sora_storage_quota_gb: 0, customAttributes: {} as UserAttributeValuesMap }) const form = reactive({ email: '', password: '', username: '', notes: '', concurrency: 1, customAttributes: {} as UserAttributeValuesMap })
watch(() => props.user, (u) => { watch(() => props.user, (u) => {
if (u) { if (u) {
Object.assign(form, { email: u.email, password: '', username: u.username || '', notes: u.notes || '', concurrency: u.concurrency, sora_storage_quota_gb: Number(((u.sora_storage_quota_bytes || 0) / (1024 * 1024 * 1024)).toFixed(2)), customAttributes: {} }) Object.assign(form, { email: u.email, password: '', username: u.username || '', notes: u.notes || '', concurrency: u.concurrency, customAttributes: {} })
passwordCopied.value = false passwordCopied.value = false
} }
}, { immediate: true }) }, { immediate: true })
...@@ -105,7 +97,7 @@ const handleUpdateUser = async () => { ...@@ -105,7 +97,7 @@ const handleUpdateUser = async () => {
} }
submitting.value = true submitting.value = true
try { try {
const data: any = { email: form.email, username: form.username, notes: form.notes, concurrency: form.concurrency, sora_storage_quota_bytes: Math.round((form.sora_storage_quota_gb || 0) * 1024 * 1024 * 1024) } const data: any = { email: form.email, username: form.username, notes: form.notes, concurrency: form.concurrency }
if (form.password.trim()) data.password = form.password.trim() if (form.password.trim()) data.password = form.password.trim()
await adminAPI.users.update(props.user.id, data) await adminAPI.users.update(props.user.id, data)
if (Object.keys(form.customAttributes).length > 0) await adminAPI.userAttributes.updateUserAttributeValues(props.user.id, form.customAttributes) if (Object.keys(form.customAttributes).length > 0) await adminAPI.userAttributes.updateUserAttributeValues(props.user.id, form.customAttributes)
......
...@@ -116,9 +116,6 @@ const labelClass = computed(() => { ...@@ -116,9 +116,6 @@ const labelClass = computed(() => {
if (props.platform === 'gemini') { if (props.platform === 'gemini') {
return `${base} bg-blue-200/60 text-blue-800 dark:bg-blue-800/40 dark:text-blue-300` return `${base} bg-blue-200/60 text-blue-800 dark:bg-blue-800/40 dark:text-blue-300`
} }
if (props.platform === 'sora') {
return `${base} bg-rose-200/60 text-rose-800 dark:bg-rose-800/40 dark:text-rose-300`
}
return `${base} bg-violet-200/60 text-violet-800 dark:bg-violet-800/40 dark:text-violet-300` return `${base} bg-violet-200/60 text-violet-800 dark:bg-violet-800/40 dark:text-violet-300`
}) })
...@@ -140,11 +137,6 @@ const badgeClass = computed(() => { ...@@ -140,11 +137,6 @@ const badgeClass = computed(() => {
? 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400' ? 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400'
: 'bg-sky-50 text-sky-700 dark:bg-sky-900/20 dark:text-sky-400' : 'bg-sky-50 text-sky-700 dark:bg-sky-900/20 dark:text-sky-400'
} }
if (props.platform === 'sora') {
return isSubscription.value
? 'bg-rose-100 text-rose-700 dark:bg-rose-900/30 dark:text-rose-400'
: 'bg-rose-50 text-rose-700 dark:bg-rose-900/20 dark:text-rose-400'
}
// Fallback: original colors // Fallback: original colors
return isSubscription.value return isSubscription.value
? 'bg-violet-100 text-violet-700 dark:bg-violet-900/30 dark:text-violet-400' ? 'bg-violet-100 text-violet-700 dark:bg-violet-900/30 dark:text-violet-400'
......
...@@ -91,8 +91,6 @@ const ratePillClass = computed(() => { ...@@ -91,8 +91,6 @@ const ratePillClass = computed(() => {
return 'bg-green-50 text-green-700 dark:bg-green-900/20 dark:text-green-400' return 'bg-green-50 text-green-700 dark:bg-green-900/20 dark:text-green-400'
case 'gemini': case 'gemini':
return 'bg-sky-50 text-sky-700 dark:bg-sky-900/20 dark:text-sky-400' return 'bg-sky-50 text-sky-700 dark:bg-sky-900/20 dark:text-sky-400'
case 'sora':
return 'bg-rose-50 text-rose-700 dark:bg-rose-900/20 dark:text-rose-400'
default: // antigravity and others default: // antigravity and others
return 'bg-violet-50 text-violet-700 dark:bg-violet-900/20 dark:text-violet-400' return 'bg-violet-50 text-violet-700 dark:bg-violet-900/20 dark:text-violet-400'
} }
......
...@@ -19,12 +19,6 @@ ...@@ -19,12 +19,6 @@
<svg v-else-if="platform === 'antigravity'" :class="sizeClass" viewBox="0 0 24 24" fill="currentColor"> <svg v-else-if="platform === 'antigravity'" :class="sizeClass" viewBox="0 0 24 24" fill="currentColor">
<path d="M19.35 10.04C18.67 6.59 15.64 4 12 4 9.11 4 6.6 5.64 5.35 8.04 2.34 8.36 0 10.91 0 14c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5 0-2.64-2.05-4.78-4.65-4.96z" /> <path d="M19.35 10.04C18.67 6.59 15.64 4 12 4 9.11 4 6.6 5.64 5.35 8.04 2.34 8.36 0 10.91 0 14c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5 0-2.64-2.05-4.78-4.65-4.96z" />
</svg> </svg>
<!-- Sora logo (sparkle) -->
<svg v-else-if="platform === 'sora'" :class="sizeClass" viewBox="0 0 24 24" fill="currentColor">
<path
d="M12 2.5l2.1 4.7 5.1.5-3.9 3.4 1.2 5-4.5-2.6-4.5 2.6 1.2-5-3.9-3.4 5.1-.5L12 2.5z"
/>
</svg>
<!-- Fallback: generic platform icon --> <!-- Fallback: generic platform icon -->
<svg v-else :class="sizeClass" fill="currentColor" viewBox="0 0 24 24"> <svg v-else :class="sizeClass" fill="currentColor" viewBox="0 0 24 24">
<path <path
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment