feat(sora): 新增 Sora 平台支持并修复高危安全和性能问题
新增功能: - 新增 Sora 账号管理和 OAuth 认证 - 新增 Sora 视频/图片生成 API 网关 - 新增 Sora 任务调度和缓存机制 - 新增 Sora 使用统计和计费支持 - 前端增加 Sora 平台配置界面 安全修复(代码审核): - [SEC-001] 限制媒体下载响应体大小(图片 20MB、视频 200MB),防止 DoS 攻击 - [SEC-002] 限制 SDK API 响应大小(1MB),防止内存耗尽 - [SEC-003] 修复 SSRF 风险,添加 URL 验证并强制使用代理配置 BUG 修复(代码审核): - [BUG-001] 修复 for 循环内 defer 累积导致的资源泄漏 - [BUG-002] 修复图片并发槽位获取失败时已持有锁未释放的永久泄漏 性能优化(代码审核): - [PERF-001] 添加 Sentinel Token 缓存(3 分钟有效期),减少 PoW 计算开销 技术细节: - 使用 io.LimitReader 限制所有外部输入的大小 - 添加 urlvalidator 验证防止 SSRF 攻击 - 使用 sync.Map 实现线程安全的包级缓存 - 优化并发槽位管理,添加 releaseAll 模式防止泄漏 影响范围: - 后端:新增 Sora 相关数据模型、服务、网关和管理接口 - 前端:新增 Sora 平台配置、账号管理和监控界面 - 配置:新增 Sora 相关配置项和环境变量 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -55,6 +55,25 @@ export interface SystemSettings {
|
||||
enable_identity_patch: boolean
|
||||
identity_patch_prompt: string
|
||||
|
||||
// Sora configuration
|
||||
sora_base_url: string
|
||||
sora_timeout: number
|
||||
sora_max_retries: number
|
||||
sora_poll_interval: number
|
||||
sora_call_logic_mode: string
|
||||
sora_cache_enabled: boolean
|
||||
sora_cache_base_dir: string
|
||||
sora_cache_video_dir: string
|
||||
sora_cache_max_bytes: number
|
||||
sora_cache_allowed_hosts: string[]
|
||||
sora_cache_user_dir_enabled: boolean
|
||||
sora_watermark_free_enabled: boolean
|
||||
sora_watermark_free_parse_method: string
|
||||
sora_watermark_free_custom_parse_url: string
|
||||
sora_watermark_free_custom_parse_token: string
|
||||
sora_watermark_free_fallback_on_failure: boolean
|
||||
sora_token_refresh_enabled: boolean
|
||||
|
||||
// Ops Monitoring (vNext)
|
||||
ops_monitoring_enabled: boolean
|
||||
ops_realtime_monitoring_enabled: boolean
|
||||
@@ -97,6 +116,23 @@ export interface UpdateSettingsRequest {
|
||||
fallback_model_antigravity?: string
|
||||
enable_identity_patch?: boolean
|
||||
identity_patch_prompt?: string
|
||||
sora_base_url?: string
|
||||
sora_timeout?: number
|
||||
sora_max_retries?: number
|
||||
sora_poll_interval?: number
|
||||
sora_call_logic_mode?: string
|
||||
sora_cache_enabled?: boolean
|
||||
sora_cache_base_dir?: string
|
||||
sora_cache_video_dir?: string
|
||||
sora_cache_max_bytes?: number
|
||||
sora_cache_allowed_hosts?: string[]
|
||||
sora_cache_user_dir_enabled?: boolean
|
||||
sora_watermark_free_enabled?: boolean
|
||||
sora_watermark_free_parse_method?: string
|
||||
sora_watermark_free_custom_parse_url?: string
|
||||
sora_watermark_free_custom_parse_token?: string
|
||||
sora_watermark_free_fallback_on_failure?: boolean
|
||||
sora_token_refresh_enabled?: boolean
|
||||
ops_monitoring_enabled?: boolean
|
||||
ops_realtime_monitoring_enabled?: boolean
|
||||
ops_query_mode_default?: 'auto' | 'raw' | 'preagg' | string
|
||||
|
||||
@@ -147,6 +147,19 @@
|
||||
<Icon name="cloud" size="sm" />
|
||||
Antigravity
|
||||
</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'
|
||||
]"
|
||||
>
|
||||
<Icon name="play" size="sm" />
|
||||
Sora
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -672,6 +685,8 @@
|
||||
? 'https://api.openai.com'
|
||||
: form.platform === 'gemini'
|
||||
? 'https://generativelanguage.googleapis.com'
|
||||
: form.platform === 'sora'
|
||||
? 'https://sora.chatgpt.com/backend'
|
||||
: 'https://api.anthropic.com'
|
||||
"
|
||||
/>
|
||||
@@ -689,6 +704,8 @@
|
||||
? 'sk-proj-...'
|
||||
: form.platform === 'gemini'
|
||||
? 'AIza...'
|
||||
: form.platform === 'sora'
|
||||
? 'access-token...'
|
||||
: 'sk-ant-...'
|
||||
"
|
||||
/>
|
||||
@@ -1850,12 +1867,14 @@ const oauthStepTitle = computed(() => {
|
||||
const baseUrlHint = computed(() => {
|
||||
if (form.platform === 'openai') return t('admin.accounts.openai.baseUrlHint')
|
||||
if (form.platform === 'gemini') return t('admin.accounts.gemini.baseUrlHint')
|
||||
if (form.platform === 'sora') return t('admin.accounts.sora.baseUrlHint')
|
||||
return t('admin.accounts.baseUrlHint')
|
||||
})
|
||||
|
||||
const apiKeyHint = computed(() => {
|
||||
if (form.platform === 'openai') return t('admin.accounts.openai.apiKeyHint')
|
||||
if (form.platform === 'gemini') return t('admin.accounts.gemini.apiKeyHint')
|
||||
if (form.platform === 'sora') return t('admin.accounts.sora.apiKeyHint')
|
||||
return t('admin.accounts.apiKeyHint')
|
||||
})
|
||||
|
||||
@@ -2100,7 +2119,9 @@ watch(
|
||||
? 'https://api.openai.com'
|
||||
: newPlatform === 'gemini'
|
||||
? 'https://generativelanguage.googleapis.com'
|
||||
: 'https://api.anthropic.com'
|
||||
: newPlatform === 'sora'
|
||||
? 'https://sora.chatgpt.com/backend'
|
||||
: 'https://api.anthropic.com'
|
||||
// Clear model-related settings
|
||||
allowedModels.value = []
|
||||
modelMappings.value = []
|
||||
@@ -2112,6 +2133,9 @@ watch(
|
||||
if (newPlatform === 'antigravity') {
|
||||
accountCategory.value = 'oauth-based'
|
||||
}
|
||||
if (newPlatform === 'sora') {
|
||||
accountCategory.value = 'apikey'
|
||||
}
|
||||
// Reset OAuth states
|
||||
oauth.resetState()
|
||||
openaiOAuth.resetState()
|
||||
@@ -2383,12 +2407,17 @@ const handleSubmit = async () => {
|
||||
? 'https://api.openai.com'
|
||||
: form.platform === 'gemini'
|
||||
? 'https://generativelanguage.googleapis.com'
|
||||
: 'https://api.anthropic.com'
|
||||
: form.platform === 'sora'
|
||||
? 'https://sora.chatgpt.com/backend'
|
||||
: 'https://api.anthropic.com'
|
||||
|
||||
// Build credentials with optional model mapping
|
||||
const credentials: Record<string, unknown> = {
|
||||
base_url: apiKeyBaseUrl.value.trim() || defaultBaseUrl,
|
||||
api_key: apiKeyValue.value.trim()
|
||||
const credentials: Record<string, unknown> = {}
|
||||
if (form.platform === 'sora') {
|
||||
credentials.access_token = apiKeyValue.value.trim()
|
||||
} else {
|
||||
credentials.base_url = apiKeyBaseUrl.value.trim() || defaultBaseUrl
|
||||
credentials.api_key = apiKeyValue.value.trim()
|
||||
}
|
||||
if (form.platform === 'gemini') {
|
||||
credentials.tier_id = geminiTierAIStudio.value
|
||||
|
||||
@@ -39,6 +39,8 @@
|
||||
? 'https://api.openai.com'
|
||||
: account.platform === 'gemini'
|
||||
? 'https://generativelanguage.googleapis.com'
|
||||
: account.platform === 'sora'
|
||||
? 'https://sora.chatgpt.com/backend'
|
||||
: 'https://api.anthropic.com'
|
||||
"
|
||||
/>
|
||||
@@ -55,6 +57,8 @@
|
||||
? 'sk-proj-...'
|
||||
: account.platform === 'gemini'
|
||||
? 'AIza...'
|
||||
: account.platform === 'sora'
|
||||
? 'access-token...'
|
||||
: 'sk-ant-...'
|
||||
"
|
||||
/>
|
||||
@@ -919,6 +923,7 @@ const baseUrlHint = computed(() => {
|
||||
if (!props.account) return t('admin.accounts.baseUrlHint')
|
||||
if (props.account.platform === 'openai') return t('admin.accounts.openai.baseUrlHint')
|
||||
if (props.account.platform === 'gemini') return t('admin.accounts.gemini.baseUrlHint')
|
||||
if (props.account.platform === 'sora') return t('admin.accounts.sora.baseUrlHint')
|
||||
return t('admin.accounts.baseUrlHint')
|
||||
})
|
||||
|
||||
@@ -997,6 +1002,7 @@ const tempUnschedPresets = computed(() => [
|
||||
const defaultBaseUrl = computed(() => {
|
||||
if (props.account?.platform === 'openai') return 'https://api.openai.com'
|
||||
if (props.account?.platform === 'gemini') return 'https://generativelanguage.googleapis.com'
|
||||
if (props.account?.platform === 'sora') return 'https://sora.chatgpt.com/backend'
|
||||
return 'https://api.anthropic.com'
|
||||
})
|
||||
|
||||
@@ -1061,7 +1067,9 @@ watch(
|
||||
? 'https://api.openai.com'
|
||||
: newAccount.platform === 'gemini'
|
||||
? 'https://generativelanguage.googleapis.com'
|
||||
: 'https://api.anthropic.com'
|
||||
: newAccount.platform === 'sora'
|
||||
? 'https://sora.chatgpt.com/backend'
|
||||
: 'https://api.anthropic.com'
|
||||
editBaseUrl.value = (credentials.base_url as string) || platformDefaultUrl
|
||||
|
||||
// Load model mappings and detect mode
|
||||
@@ -1104,7 +1112,9 @@ watch(
|
||||
? 'https://api.openai.com'
|
||||
: newAccount.platform === 'gemini'
|
||||
? 'https://generativelanguage.googleapis.com'
|
||||
: 'https://api.anthropic.com'
|
||||
: newAccount.platform === 'sora'
|
||||
? 'https://sora.chatgpt.com/backend'
|
||||
: 'https://api.anthropic.com'
|
||||
editBaseUrl.value = platformDefaultUrl
|
||||
modelRestrictionMode.value = 'whitelist'
|
||||
modelMappings.value = []
|
||||
@@ -1381,17 +1391,32 @@ const handleSubmit = async () => {
|
||||
if (props.account.type === 'apikey') {
|
||||
const currentCredentials = (props.account.credentials as Record<string, unknown>) || {}
|
||||
const newBaseUrl = editBaseUrl.value.trim() || defaultBaseUrl.value
|
||||
const isSora = props.account.platform === 'sora'
|
||||
const modelMapping = buildModelMappingObject(modelRestrictionMode.value, allowedModels.value, modelMappings.value)
|
||||
|
||||
// Always update credentials for apikey type to handle model mapping changes
|
||||
const newCredentials: Record<string, unknown> = {
|
||||
base_url: newBaseUrl
|
||||
const newCredentials: Record<string, unknown> = {}
|
||||
if (!isSora) {
|
||||
newCredentials.base_url = newBaseUrl
|
||||
}
|
||||
|
||||
// Handle API key
|
||||
if (editApiKey.value.trim()) {
|
||||
// User provided a new API key
|
||||
newCredentials.api_key = editApiKey.value.trim()
|
||||
if (isSora) {
|
||||
newCredentials.access_token = editApiKey.value.trim()
|
||||
} else {
|
||||
newCredentials.api_key = editApiKey.value.trim()
|
||||
}
|
||||
} else if (isSora) {
|
||||
const existingToken = (currentCredentials.access_token || currentCredentials.token) as string | undefined
|
||||
if (existingToken) {
|
||||
newCredentials.access_token = existingToken
|
||||
} else {
|
||||
appStore.showError(t('admin.accounts.apiKeyIsRequired'))
|
||||
submitting.value = false
|
||||
return
|
||||
}
|
||||
} else if (currentCredentials.api_key) {
|
||||
// Preserve existing api_key
|
||||
newCredentials.api_key = currentCredentials.api_key
|
||||
|
||||
@@ -428,7 +428,7 @@ interface Props {
|
||||
allowMultiple?: boolean
|
||||
methodLabel?: string
|
||||
showCookieOption?: boolean // Whether to show cookie auto-auth option
|
||||
platform?: 'anthropic' | 'openai' | 'gemini' | 'antigravity' // Platform type for different UI/text
|
||||
platform?: 'anthropic' | 'openai' | 'gemini' | 'antigravity' | 'sora' // Platform type for different UI/text
|
||||
showProjectId?: boolean // New prop to control project ID visibility
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ const props = defineProps(['searchQuery', 'filters']); const emit = defineEmits(
|
||||
const updatePlatform = (value: string | number | boolean | null) => { emit('update:filters', { ...props.filters, platform: value }) }
|
||||
const updateType = (value: string | number | boolean | null) => { emit('update:filters', { ...props.filters, type: value }) }
|
||||
const updateStatus = (value: string | number | boolean | null) => { emit('update:filters', { ...props.filters, status: 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' }])
|
||||
const pOpts = computed(() => [{ value: '', label: t('admin.accounts.allPlatforms') }, { value: 'anthropic', label: 'Anthropic' }, { value: 'openai', label: 'OpenAI' }, { value: 'gemini', label: 'Gemini' }, { value: 'sora', label: 'Sora' }, { 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') }])
|
||||
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') }])
|
||||
</script>
|
||||
|
||||
@@ -97,6 +97,9 @@ const labelClass = computed(() => {
|
||||
if (props.platform === 'gemini') {
|
||||
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`
|
||||
})
|
||||
|
||||
@@ -118,6 +121,11 @@ const badgeClass = computed(() => {
|
||||
? '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'
|
||||
}
|
||||
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
|
||||
return isSubscription.value
|
||||
? 'bg-violet-100 text-violet-700 dark:bg-violet-900/30 dark:text-violet-400'
|
||||
|
||||
@@ -19,6 +19,10 @@
|
||||
<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" />
|
||||
</svg>
|
||||
<!-- Sora logo (play icon) -->
|
||||
<svg v-else-if="platform === 'sora'" :class="sizeClass" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M12 2C6.486 2 2 6.486 2 12s4.486 10 10 10 10-4.486 10-10S17.514 2 12 2zm-1 6 6 4-6 4V8z" />
|
||||
</svg>
|
||||
<!-- Fallback: generic platform icon -->
|
||||
<svg v-else :class="sizeClass" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
|
||||
@@ -48,6 +48,7 @@ const platformLabel = computed(() => {
|
||||
if (props.platform === 'anthropic') return 'Anthropic'
|
||||
if (props.platform === 'openai') return 'OpenAI'
|
||||
if (props.platform === 'antigravity') return 'Antigravity'
|
||||
if (props.platform === 'sora') return 'Sora'
|
||||
return 'Gemini'
|
||||
})
|
||||
|
||||
@@ -74,6 +75,9 @@ const platformClass = computed(() => {
|
||||
if (props.platform === 'antigravity') {
|
||||
return 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400'
|
||||
}
|
||||
if (props.platform === 'sora') {
|
||||
return 'bg-rose-100 text-rose-700 dark:bg-rose-900/30 dark:text-rose-400'
|
||||
}
|
||||
return 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400'
|
||||
})
|
||||
|
||||
@@ -87,6 +91,9 @@ const typeClass = computed(() => {
|
||||
if (props.platform === 'antigravity') {
|
||||
return 'bg-purple-100 text-purple-600 dark:bg-purple-900/30 dark:text-purple-400'
|
||||
}
|
||||
if (props.platform === 'sora') {
|
||||
return 'bg-rose-100 text-rose-600 dark:bg-rose-900/30 dark:text-rose-400'
|
||||
}
|
||||
return 'bg-blue-100 text-blue-600 dark:bg-blue-900/30 dark:text-blue-400'
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -180,6 +180,8 @@ const defaultClientTab = computed(() => {
|
||||
switch (props.platform) {
|
||||
case 'openai':
|
||||
return 'codex'
|
||||
case 'sora':
|
||||
return 'codex'
|
||||
case 'gemini':
|
||||
return 'gemini'
|
||||
case 'antigravity':
|
||||
@@ -266,6 +268,7 @@ const clientTabs = computed((): TabConfig[] => {
|
||||
if (!props.platform) return []
|
||||
switch (props.platform) {
|
||||
case 'openai':
|
||||
case 'sora':
|
||||
return [
|
||||
{ id: 'codex', label: t('keys.useKeyModal.cliTabs.codexCli'), icon: TerminalIcon },
|
||||
{ id: 'opencode', label: t('keys.useKeyModal.cliTabs.opencode'), icon: TerminalIcon }
|
||||
@@ -306,7 +309,7 @@ const showShellTabs = computed(() => activeClientTab.value !== 'opencode')
|
||||
|
||||
const currentTabs = computed(() => {
|
||||
if (!showShellTabs.value) return []
|
||||
if (props.platform === 'openai') {
|
||||
if (props.platform === 'openai' || props.platform === 'sora') {
|
||||
return openaiTabs
|
||||
}
|
||||
return shellTabs
|
||||
@@ -315,6 +318,7 @@ const currentTabs = computed(() => {
|
||||
const platformDescription = computed(() => {
|
||||
switch (props.platform) {
|
||||
case 'openai':
|
||||
case 'sora':
|
||||
return t('keys.useKeyModal.openai.description')
|
||||
case 'gemini':
|
||||
return t('keys.useKeyModal.gemini.description')
|
||||
@@ -328,6 +332,7 @@ const platformDescription = computed(() => {
|
||||
const platformNote = computed(() => {
|
||||
switch (props.platform) {
|
||||
case 'openai':
|
||||
case 'sora':
|
||||
return activeTab.value === 'windows'
|
||||
? t('keys.useKeyModal.openai.noteWindows')
|
||||
: t('keys.useKeyModal.openai.note')
|
||||
@@ -386,6 +391,7 @@ const currentFiles = computed((): FileConfig[] => {
|
||||
case 'anthropic':
|
||||
return [generateOpenCodeConfig('anthropic', apiBase, apiKey)]
|
||||
case 'openai':
|
||||
case 'sora':
|
||||
return [generateOpenCodeConfig('openai', apiBase, apiKey)]
|
||||
case 'gemini':
|
||||
return [generateOpenCodeConfig('gemini', geminiBase, apiKey)]
|
||||
@@ -401,6 +407,7 @@ const currentFiles = computed((): FileConfig[] => {
|
||||
|
||||
switch (props.platform) {
|
||||
case 'openai':
|
||||
case 'sora':
|
||||
return generateOpenAIFiles(baseUrl, apiKey)
|
||||
case 'gemini':
|
||||
return [generateGeminiCliContent(baseUrl, apiKey)]
|
||||
|
||||
@@ -52,6 +52,38 @@ const geminiModels = [
|
||||
'gemini-3-pro-preview'
|
||||
]
|
||||
|
||||
// OpenAI Sora
|
||||
const soraModels = [
|
||||
'gpt-image',
|
||||
'gpt-image-landscape',
|
||||
'gpt-image-portrait',
|
||||
'sora2-landscape-10s',
|
||||
'sora2-portrait-10s',
|
||||
'sora2-landscape-15s',
|
||||
'sora2-portrait-15s',
|
||||
'sora2-landscape-25s',
|
||||
'sora2-portrait-25s',
|
||||
'sora2pro-landscape-10s',
|
||||
'sora2pro-portrait-10s',
|
||||
'sora2pro-landscape-15s',
|
||||
'sora2pro-portrait-15s',
|
||||
'sora2pro-landscape-25s',
|
||||
'sora2pro-portrait-25s',
|
||||
'sora2pro-hd-landscape-10s',
|
||||
'sora2pro-hd-portrait-10s',
|
||||
'sora2pro-hd-landscape-15s',
|
||||
'sora2pro-hd-portrait-15s',
|
||||
'prompt-enhance-short-10s',
|
||||
'prompt-enhance-short-15s',
|
||||
'prompt-enhance-short-20s',
|
||||
'prompt-enhance-medium-10s',
|
||||
'prompt-enhance-medium-15s',
|
||||
'prompt-enhance-medium-20s',
|
||||
'prompt-enhance-long-10s',
|
||||
'prompt-enhance-long-15s',
|
||||
'prompt-enhance-long-20s'
|
||||
]
|
||||
|
||||
// 智谱 GLM
|
||||
const zhipuModels = [
|
||||
'glm-4', 'glm-4v', 'glm-4-plus', 'glm-4-0520',
|
||||
@@ -182,6 +214,7 @@ const allModelsList: string[] = [
|
||||
...openaiModels,
|
||||
...claudeModels,
|
||||
...geminiModels,
|
||||
...soraModels,
|
||||
...zhipuModels,
|
||||
...qwenModels,
|
||||
...deepseekModels,
|
||||
@@ -258,6 +291,7 @@ export function getModelsByPlatform(platform: string): string[] {
|
||||
case 'anthropic':
|
||||
case 'claude': return claudeModels
|
||||
case 'gemini': return geminiModels
|
||||
case 'sora': return soraModels
|
||||
case 'zhipu': return zhipuModels
|
||||
case 'qwen': return qwenModels
|
||||
case 'deepseek': return deepseekModels
|
||||
@@ -281,6 +315,7 @@ export function getModelsByPlatform(platform: string): string[] {
|
||||
export function getPresetMappingsByPlatform(platform: string) {
|
||||
if (platform === 'openai') return openaiPresetMappings
|
||||
if (platform === 'gemini') return geminiPresetMappings
|
||||
if (platform === 'sora') return []
|
||||
return anthropicPresetMappings
|
||||
}
|
||||
|
||||
|
||||
@@ -895,6 +895,7 @@ export default {
|
||||
anthropic: 'Anthropic',
|
||||
openai: 'OpenAI',
|
||||
gemini: 'Gemini',
|
||||
sora: 'Sora',
|
||||
antigravity: 'Antigravity'
|
||||
},
|
||||
deleteConfirm:
|
||||
@@ -1079,6 +1080,7 @@ export default {
|
||||
claude: 'Claude',
|
||||
openai: 'OpenAI',
|
||||
gemini: 'Gemini',
|
||||
sora: 'Sora',
|
||||
antigravity: 'Antigravity'
|
||||
},
|
||||
types: {
|
||||
@@ -1247,6 +1249,11 @@ export default {
|
||||
baseUrlHint: 'Leave default for official OpenAI API',
|
||||
apiKeyHint: 'Your OpenAI API Key'
|
||||
},
|
||||
// Sora specific hints
|
||||
sora: {
|
||||
baseUrlHint: 'Leave empty to use global Sora Base URL',
|
||||
apiKeyHint: 'Your Sora access token'
|
||||
},
|
||||
modelRestriction: 'Model Restriction (Optional)',
|
||||
modelWhitelist: 'Model Whitelist',
|
||||
modelMapping: 'Model Mapping',
|
||||
@@ -2784,6 +2791,47 @@ export default {
|
||||
defaultConcurrency: 'Default Concurrency',
|
||||
defaultConcurrencyHint: 'Maximum concurrent requests for new users'
|
||||
},
|
||||
sora: {
|
||||
title: 'Sora Settings',
|
||||
description: 'Configure Sora upstream requests, cache, and watermark-free flow',
|
||||
baseUrl: 'Sora Base URL',
|
||||
baseUrlPlaceholder: 'https://sora.chatgpt.com/backend',
|
||||
baseUrlHint: 'Base URL for the Sora backend API',
|
||||
callLogicMode: 'Call Mode',
|
||||
callLogicModeDefault: 'Default',
|
||||
callLogicModeNative: 'Native',
|
||||
callLogicModeProxy: 'Proxy',
|
||||
callLogicModeHint: 'Default keeps the existing behavior',
|
||||
timeout: 'Timeout (seconds)',
|
||||
timeoutHint: 'Timeout for single request',
|
||||
maxRetries: 'Max Retries',
|
||||
maxRetriesHint: 'Retry count for upstream failures',
|
||||
pollInterval: 'Poll Interval (seconds)',
|
||||
pollIntervalHint: 'Polling interval for task status',
|
||||
cacheEnabled: 'Enable Cache',
|
||||
cacheEnabledHint: 'Cache generated media for local downloads',
|
||||
cacheBaseDir: 'Cache Base Dir',
|
||||
cacheVideoDir: 'Video Cache Dir',
|
||||
cacheMaxBytes: 'Cache Size (bytes)',
|
||||
cacheMaxBytesHint: '0 means unlimited',
|
||||
cacheUserDirEnabled: 'User Directory Isolation',
|
||||
cacheUserDirEnabledHint: 'Create per-user subdirectories',
|
||||
cacheAllowedHosts: 'Cache Allowlist',
|
||||
cacheAllowedHostsPlaceholder: 'One host per line, e.g. oscdn2.dyysy.com',
|
||||
cacheAllowedHostsHint: 'Empty falls back to the global URL allowlist',
|
||||
watermarkFreeEnabled: 'Enable Watermark-Free',
|
||||
watermarkFreeEnabledHint: 'Try to resolve watermark-free videos',
|
||||
watermarkFreeParseMethod: 'Parse Method',
|
||||
watermarkFreeParseMethodThirdParty: 'Third-party',
|
||||
watermarkFreeParseMethodCustom: 'Custom',
|
||||
watermarkFreeParseMethodHint: 'Select the watermark-free parse method',
|
||||
watermarkFreeCustomParseUrl: 'Custom Parse URL',
|
||||
watermarkFreeCustomParseToken: 'Custom Parse Token',
|
||||
watermarkFreeFallback: 'Fallback on Failure',
|
||||
watermarkFreeFallbackHint: 'Return the original video on failure',
|
||||
tokenRefreshEnabled: 'Enable Token Refresh',
|
||||
tokenRefreshEnabledHint: 'Periodic token refresh (requires scheduler)'
|
||||
},
|
||||
site: {
|
||||
title: 'Site Settings',
|
||||
description: 'Customize site branding',
|
||||
|
||||
@@ -941,6 +941,7 @@ export default {
|
||||
anthropic: 'Anthropic',
|
||||
openai: 'OpenAI',
|
||||
gemini: 'Gemini',
|
||||
sora: 'Sora',
|
||||
antigravity: 'Antigravity'
|
||||
},
|
||||
saving: '保存中...',
|
||||
@@ -1199,6 +1200,7 @@ export default {
|
||||
openai: 'OpenAI',
|
||||
anthropic: 'Anthropic',
|
||||
gemini: 'Gemini',
|
||||
sora: 'Sora',
|
||||
antigravity: 'Antigravity'
|
||||
},
|
||||
types: {
|
||||
@@ -1382,6 +1384,11 @@ export default {
|
||||
baseUrlHint: '留空使用官方 OpenAI API',
|
||||
apiKeyHint: '您的 OpenAI API Key'
|
||||
},
|
||||
// Sora specific hints
|
||||
sora: {
|
||||
baseUrlHint: '留空使用全局 Sora Base URL',
|
||||
apiKeyHint: '您的 Sora access token'
|
||||
},
|
||||
modelRestriction: '模型限制(可选)',
|
||||
modelWhitelist: '模型白名单',
|
||||
modelMapping: '模型映射',
|
||||
@@ -2936,6 +2943,47 @@ export default {
|
||||
defaultConcurrency: '默认并发数',
|
||||
defaultConcurrencyHint: '新用户的最大并发请求数'
|
||||
},
|
||||
sora: {
|
||||
title: 'Sora 设置',
|
||||
description: '配置 Sora 上游请求、缓存与去水印策略',
|
||||
baseUrl: 'Sora Base URL',
|
||||
baseUrlPlaceholder: 'https://sora.chatgpt.com/backend',
|
||||
baseUrlHint: 'Sora 后端 API 基础地址',
|
||||
callLogicMode: '调用模式',
|
||||
callLogicModeDefault: '默认',
|
||||
callLogicModeNative: '原生',
|
||||
callLogicModeProxy: '代理',
|
||||
callLogicModeHint: '默认保持当前策略',
|
||||
timeout: '请求超时(秒)',
|
||||
timeoutHint: '单次任务超时控制',
|
||||
maxRetries: '最大重试次数',
|
||||
maxRetriesHint: '上游请求失败时的重试次数',
|
||||
pollInterval: '轮询间隔(秒)',
|
||||
pollIntervalHint: '任务状态轮询间隔',
|
||||
cacheEnabled: '启用缓存',
|
||||
cacheEnabledHint: '启用生成结果缓存并提供本地下载',
|
||||
cacheBaseDir: '缓存根目录',
|
||||
cacheVideoDir: '视频缓存目录',
|
||||
cacheMaxBytes: '缓存容量(字节)',
|
||||
cacheMaxBytesHint: '0 表示不限制',
|
||||
cacheUserDirEnabled: '按用户隔离缓存目录',
|
||||
cacheUserDirEnabledHint: '开启后按用户创建子目录',
|
||||
cacheAllowedHosts: '缓存下载白名单',
|
||||
cacheAllowedHostsPlaceholder: '每行一个域名,例如: oscdn2.dyysy.com',
|
||||
cacheAllowedHostsHint: '为空时回退全局 URL 白名单',
|
||||
watermarkFreeEnabled: '启用去水印',
|
||||
watermarkFreeEnabledHint: '尝试通过解析服务获取无水印视频',
|
||||
watermarkFreeParseMethod: '解析方式',
|
||||
watermarkFreeParseMethodThirdParty: '第三方解析',
|
||||
watermarkFreeParseMethodCustom: '自定义解析',
|
||||
watermarkFreeParseMethodHint: '选择去水印解析方式',
|
||||
watermarkFreeCustomParseUrl: '自定义解析地址',
|
||||
watermarkFreeCustomParseToken: '自定义解析 Token',
|
||||
watermarkFreeFallback: '解析失败降级',
|
||||
watermarkFreeFallbackHint: '失败时返回原视频',
|
||||
tokenRefreshEnabled: '启用 Token 刷新',
|
||||
tokenRefreshEnabledHint: '定时刷新 Sora Token(需配置调度)'
|
||||
},
|
||||
site: {
|
||||
title: '站点设置',
|
||||
description: '自定义站点品牌',
|
||||
|
||||
@@ -252,7 +252,7 @@ export interface PaginationConfig {
|
||||
|
||||
// ==================== API Key & Group Types ====================
|
||||
|
||||
export type GroupPlatform = 'anthropic' | 'openai' | 'gemini' | 'antigravity'
|
||||
export type GroupPlatform = 'anthropic' | 'openai' | 'gemini' | 'antigravity' | 'sora'
|
||||
|
||||
export type SubscriptionType = 'standard' | 'subscription'
|
||||
|
||||
@@ -355,7 +355,7 @@ export interface UpdateGroupRequest {
|
||||
|
||||
// ==================== Account & Proxy Types ====================
|
||||
|
||||
export type AccountPlatform = 'anthropic' | 'openai' | 'gemini' | 'antigravity'
|
||||
export type AccountPlatform = 'anthropic' | 'openai' | 'gemini' | 'antigravity' | 'sora'
|
||||
export type AccountType = 'oauth' | 'setup-token' | 'apikey'
|
||||
export type OAuthAddMethod = 'oauth' | 'setup-token'
|
||||
export type ProxyProtocol = 'http' | 'https' | 'socks5' | 'socks5h'
|
||||
|
||||
@@ -1152,6 +1152,7 @@ const platformOptions = computed(() => [
|
||||
{ value: 'anthropic', label: 'Anthropic' },
|
||||
{ value: 'openai', label: 'OpenAI' },
|
||||
{ value: 'gemini', label: 'Gemini' },
|
||||
{ value: 'sora', label: 'Sora' },
|
||||
{ value: 'antigravity', label: 'Antigravity' }
|
||||
])
|
||||
|
||||
@@ -1160,6 +1161,7 @@ const platformFilterOptions = computed(() => [
|
||||
{ value: 'anthropic', label: 'Anthropic' },
|
||||
{ value: 'openai', label: 'OpenAI' },
|
||||
{ value: 'gemini', label: 'Gemini' },
|
||||
{ value: 'sora', label: 'Sora' },
|
||||
{ value: 'antigravity', label: 'Antigravity' }
|
||||
])
|
||||
|
||||
|
||||
@@ -561,6 +561,221 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sora Settings -->
|
||||
<div class="card">
|
||||
<div class="border-b border-gray-100 px-6 py-4 dark:border-dark-700">
|
||||
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
{{ t('admin.settings.sora.title') }}
|
||||
</h2>
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ t('admin.settings.sora.description') }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="space-y-6 p-6">
|
||||
<div class="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{{ t('admin.settings.sora.baseUrl') }}
|
||||
</label>
|
||||
<input
|
||||
v-model="form.sora_base_url"
|
||||
type="text"
|
||||
class="input font-mono text-sm"
|
||||
:placeholder="t('admin.settings.sora.baseUrlPlaceholder')"
|
||||
/>
|
||||
<p class="mt-1.5 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('admin.settings.sora.baseUrlHint') }}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{{ t('admin.settings.sora.callLogicMode') }}
|
||||
</label>
|
||||
<select v-model="form.sora_call_logic_mode" class="input">
|
||||
<option value="default">{{ t('admin.settings.sora.callLogicModeDefault') }}</option>
|
||||
<option value="native">{{ t('admin.settings.sora.callLogicModeNative') }}</option>
|
||||
<option value="proxy">{{ t('admin.settings.sora.callLogicModeProxy') }}</option>
|
||||
</select>
|
||||
<p class="mt-1.5 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('admin.settings.sora.callLogicModeHint') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-6 md:grid-cols-3">
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{{ t('admin.settings.sora.timeout') }}
|
||||
</label>
|
||||
<input v-model.number="form.sora_timeout" type="number" min="1" class="input" />
|
||||
<p class="mt-1.5 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('admin.settings.sora.timeoutHint') }}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{{ t('admin.settings.sora.maxRetries') }}
|
||||
</label>
|
||||
<input v-model.number="form.sora_max_retries" type="number" min="0" class="input" />
|
||||
<p class="mt-1.5 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('admin.settings.sora.maxRetriesHint') }}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{{ t('admin.settings.sora.pollInterval') }}
|
||||
</label>
|
||||
<input
|
||||
v-model.number="form.sora_poll_interval"
|
||||
type="number"
|
||||
min="0.5"
|
||||
step="0.1"
|
||||
class="input"
|
||||
/>
|
||||
<p class="mt-1.5 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('admin.settings.sora.pollIntervalHint') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4 border-t border-gray-100 pt-4 dark:border-dark-700">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<label class="font-medium text-gray-900 dark:text-white">{{
|
||||
t('admin.settings.sora.cacheEnabled')
|
||||
}}</label>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ t('admin.settings.sora.cacheEnabledHint') }}
|
||||
</p>
|
||||
</div>
|
||||
<Toggle v-model="form.sora_cache_enabled" />
|
||||
</div>
|
||||
|
||||
<div v-if="form.sora_cache_enabled" class="space-y-4">
|
||||
<div class="grid grid-cols-1 gap-6 md:grid-cols-3">
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{{ t('admin.settings.sora.cacheBaseDir') }}
|
||||
</label>
|
||||
<input v-model="form.sora_cache_base_dir" type="text" class="input font-mono text-sm" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{{ t('admin.settings.sora.cacheVideoDir') }}
|
||||
</label>
|
||||
<input v-model="form.sora_cache_video_dir" type="text" class="input font-mono text-sm" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{{ t('admin.settings.sora.cacheMaxBytes') }}
|
||||
</label>
|
||||
<input v-model.number="form.sora_cache_max_bytes" type="number" min="0" class="input" />
|
||||
<p class="mt-1.5 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('admin.settings.sora.cacheMaxBytesHint') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<label class="font-medium text-gray-900 dark:text-white">{{
|
||||
t('admin.settings.sora.cacheUserDirEnabled')
|
||||
}}</label>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ t('admin.settings.sora.cacheUserDirEnabledHint') }}
|
||||
</p>
|
||||
</div>
|
||||
<Toggle v-model="form.sora_cache_user_dir_enabled" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{{ t('admin.settings.sora.cacheAllowedHosts') }}
|
||||
</label>
|
||||
<textarea
|
||||
v-model="form.sora_cache_allowed_hosts_text"
|
||||
rows="3"
|
||||
class="input font-mono text-sm"
|
||||
:placeholder="t('admin.settings.sora.cacheAllowedHostsPlaceholder')"
|
||||
></textarea>
|
||||
<p class="mt-1.5 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('admin.settings.sora.cacheAllowedHostsHint') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4 border-t border-gray-100 pt-4 dark:border-dark-700">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<label class="font-medium text-gray-900 dark:text-white">{{
|
||||
t('admin.settings.sora.watermarkFreeEnabled')
|
||||
}}</label>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ t('admin.settings.sora.watermarkFreeEnabledHint') }}
|
||||
</p>
|
||||
</div>
|
||||
<Toggle v-model="form.sora_watermark_free_enabled" />
|
||||
</div>
|
||||
|
||||
<div v-if="form.sora_watermark_free_enabled" class="space-y-4">
|
||||
<div class="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{{ t('admin.settings.sora.watermarkFreeParseMethod') }}
|
||||
</label>
|
||||
<select v-model="form.sora_watermark_free_parse_method" class="input">
|
||||
<option value="third_party">{{ t('admin.settings.sora.watermarkFreeParseMethodThirdParty') }}</option>
|
||||
<option value="custom">{{ t('admin.settings.sora.watermarkFreeParseMethodCustom') }}</option>
|
||||
</select>
|
||||
<p class="mt-1.5 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('admin.settings.sora.watermarkFreeParseMethodHint') }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<label class="font-medium text-gray-900 dark:text-white">{{
|
||||
t('admin.settings.sora.watermarkFreeFallback')
|
||||
}}</label>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ t('admin.settings.sora.watermarkFreeFallbackHint') }}
|
||||
</p>
|
||||
</div>
|
||||
<Toggle v-model="form.sora_watermark_free_fallback_on_failure" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="form.sora_watermark_free_parse_method === 'custom'" class="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{{ t('admin.settings.sora.watermarkFreeCustomParseUrl') }}
|
||||
</label>
|
||||
<input v-model="form.sora_watermark_free_custom_parse_url" type="text" class="input font-mono text-sm" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{{ t('admin.settings.sora.watermarkFreeCustomParseToken') }}
|
||||
</label>
|
||||
<input v-model="form.sora_watermark_free_custom_parse_token" type="password" class="input font-mono text-sm" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between border-t border-gray-100 pt-4 dark:border-dark-700">
|
||||
<div>
|
||||
<label class="font-medium text-gray-900 dark:text-white">{{
|
||||
t('admin.settings.sora.tokenRefreshEnabled')
|
||||
}}</label>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ t('admin.settings.sora.tokenRefreshEnabledHint') }}
|
||||
</p>
|
||||
</div>
|
||||
<Toggle v-model="form.sora_token_refresh_enabled" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Site Settings -->
|
||||
<div class="card">
|
||||
<div class="border-b border-gray-100 px-6 py-4 dark:border-dark-700">
|
||||
@@ -1023,6 +1238,7 @@ type SettingsForm = SystemSettings & {
|
||||
smtp_password: string
|
||||
turnstile_secret_key: string
|
||||
linuxdo_connect_client_secret: string
|
||||
sora_cache_allowed_hosts_text: string
|
||||
}
|
||||
|
||||
const form = reactive<SettingsForm>({
|
||||
@@ -1067,6 +1283,25 @@ const form = reactive<SettingsForm>({
|
||||
// Identity patch (Claude -> Gemini)
|
||||
enable_identity_patch: true,
|
||||
identity_patch_prompt: '',
|
||||
// Sora
|
||||
sora_base_url: 'https://sora.chatgpt.com/backend',
|
||||
sora_timeout: 120,
|
||||
sora_max_retries: 3,
|
||||
sora_poll_interval: 2.5,
|
||||
sora_call_logic_mode: 'default',
|
||||
sora_cache_enabled: false,
|
||||
sora_cache_base_dir: 'tmp/sora',
|
||||
sora_cache_video_dir: 'data/video',
|
||||
sora_cache_max_bytes: 0,
|
||||
sora_cache_allowed_hosts: [],
|
||||
sora_cache_user_dir_enabled: true,
|
||||
sora_watermark_free_enabled: false,
|
||||
sora_watermark_free_parse_method: 'third_party',
|
||||
sora_watermark_free_custom_parse_url: '',
|
||||
sora_watermark_free_custom_parse_token: '',
|
||||
sora_watermark_free_fallback_on_failure: true,
|
||||
sora_token_refresh_enabled: false,
|
||||
sora_cache_allowed_hosts_text: '',
|
||||
// Ops monitoring (vNext)
|
||||
ops_monitoring_enabled: true,
|
||||
ops_realtime_monitoring_enabled: true,
|
||||
@@ -1136,6 +1371,7 @@ async function loadSettings() {
|
||||
form.smtp_password = ''
|
||||
form.turnstile_secret_key = ''
|
||||
form.linuxdo_connect_client_secret = ''
|
||||
form.sora_cache_allowed_hosts_text = (settings.sora_cache_allowed_hosts || []).join('\n')
|
||||
} catch (error: any) {
|
||||
appStore.showError(
|
||||
t('admin.settings.failedToLoad') + ': ' + (error.message || t('common.unknownError'))
|
||||
@@ -1148,6 +1384,11 @@ async function loadSettings() {
|
||||
async function saveSettings() {
|
||||
saving.value = true
|
||||
try {
|
||||
const soraAllowedHosts = form.sora_cache_allowed_hosts_text
|
||||
.split(/\r?\n/)
|
||||
.map((value) => value.trim())
|
||||
.filter((value) => value.length > 0)
|
||||
|
||||
const payload: UpdateSettingsRequest = {
|
||||
registration_enabled: form.registration_enabled,
|
||||
email_verify_enabled: form.email_verify_enabled,
|
||||
@@ -1182,13 +1423,31 @@ async function saveSettings() {
|
||||
fallback_model_gemini: form.fallback_model_gemini,
|
||||
fallback_model_antigravity: form.fallback_model_antigravity,
|
||||
enable_identity_patch: form.enable_identity_patch,
|
||||
identity_patch_prompt: form.identity_patch_prompt
|
||||
identity_patch_prompt: form.identity_patch_prompt,
|
||||
sora_base_url: form.sora_base_url,
|
||||
sora_timeout: form.sora_timeout,
|
||||
sora_max_retries: form.sora_max_retries,
|
||||
sora_poll_interval: form.sora_poll_interval,
|
||||
sora_call_logic_mode: form.sora_call_logic_mode,
|
||||
sora_cache_enabled: form.sora_cache_enabled,
|
||||
sora_cache_base_dir: form.sora_cache_base_dir,
|
||||
sora_cache_video_dir: form.sora_cache_video_dir,
|
||||
sora_cache_max_bytes: form.sora_cache_max_bytes,
|
||||
sora_cache_allowed_hosts: soraAllowedHosts,
|
||||
sora_cache_user_dir_enabled: form.sora_cache_user_dir_enabled,
|
||||
sora_watermark_free_enabled: form.sora_watermark_free_enabled,
|
||||
sora_watermark_free_parse_method: form.sora_watermark_free_parse_method,
|
||||
sora_watermark_free_custom_parse_url: form.sora_watermark_free_custom_parse_url,
|
||||
sora_watermark_free_custom_parse_token: form.sora_watermark_free_custom_parse_token,
|
||||
sora_watermark_free_fallback_on_failure: form.sora_watermark_free_fallback_on_failure,
|
||||
sora_token_refresh_enabled: form.sora_token_refresh_enabled
|
||||
}
|
||||
const updated = await adminAPI.settings.updateSettings(payload)
|
||||
Object.assign(form, updated)
|
||||
form.smtp_password = ''
|
||||
form.turnstile_secret_key = ''
|
||||
form.linuxdo_connect_client_secret = ''
|
||||
form.sora_cache_allowed_hosts_text = (updated.sora_cache_allowed_hosts || []).join('\n')
|
||||
// Refresh cached public settings so sidebar/header update immediately
|
||||
await appStore.fetchPublicSettings(true)
|
||||
appStore.showSuccess(t('admin.settings.settingsSaved'))
|
||||
|
||||
@@ -111,6 +111,7 @@ const platformOptions = computed(() => [
|
||||
{ value: 'openai', label: 'OpenAI' },
|
||||
{ value: 'anthropic', label: 'Anthropic' },
|
||||
{ value: 'gemini', label: 'Gemini' },
|
||||
{ value: 'sora', label: 'Sora' },
|
||||
{ value: 'antigravity', label: 'Antigravity' }
|
||||
])
|
||||
|
||||
|
||||
@@ -916,6 +916,7 @@ const executeCcsImport = (row: ApiKey, clientType: 'claude' | 'gemini') => {
|
||||
} else {
|
||||
switch (platform) {
|
||||
case 'openai':
|
||||
case 'sora':
|
||||
app = 'codex'
|
||||
endpoint = baseUrl
|
||||
break
|
||||
|
||||
Reference in New Issue
Block a user