feat: 添加账号时支持同步上游模型

新增 POST /admin/accounts/models/sync-upstream-preview 端点,
允许在创建账号流程中直接使用凭证同步上游支持的模型,
无需先保存账号获取 ID。

- 后端: 新增 SyncUpstreamModelsPreview handler
- 前端: 新增 syncUpstreamModelsPreview API 函数
- 前端: ModelWhitelistSelector 支持 syncCredentials prop
- 前端: CreateAccountModal 传递凭证给 ModelWhitelistSelector
This commit is contained in:
benjamin
2026-05-30 12:19:38 +08:00
parent f18451e56f
commit 57d9e15e04
5 changed files with 109 additions and 8 deletions
+18
View File
@@ -487,6 +487,23 @@ export async function syncUpstreamModels(id: number): Promise<SyncUpstreamModels
return data
}
export interface SyncUpstreamPreviewParams {
platform: string
type: string
base_url?: string
api_key: string
}
/**
* Preview upstream models without a saved account (create-flow)
* @param params - Connection credentials
* @returns List of model IDs returned by the upstream
*/
export async function syncUpstreamModelsPreview(params: SyncUpstreamPreviewParams): Promise<SyncUpstreamModelsResult> {
const { data } = await apiClient.post<SyncUpstreamModelsResult>('/admin/accounts/models/sync-upstream-preview', params)
return data
}
export interface CRSPreviewAccount {
crs_account_id: string
kind: string
@@ -703,6 +720,7 @@ export const accountsAPI = {
setSchedulable,
getAvailableModels,
syncUpstreamModels,
syncUpstreamModelsPreview,
generateAuthUrl,
exchangeCode,
refreshOpenAIToken,
@@ -1124,7 +1124,7 @@
<!-- Whitelist Mode -->
<div v-if="modelRestrictionMode === 'whitelist'">
<ModelWhitelistSelector v-model="allowedModels" :platform="form.platform" />
<ModelWhitelistSelector v-model="allowedModels" :platform="form.platform" :sync-credentials="syncPreviewCredentials" />
<p class="text-xs text-gray-500 dark:text-gray-400">
{{ t('admin.accounts.selectedModels', { count: allowedModels.length }) }}
<span v-if="allowedModels.length === 0">{{
@@ -1562,7 +1562,7 @@
<!-- Whitelist Mode -->
<div v-if="modelRestrictionMode === 'whitelist'">
<ModelWhitelistSelector v-model="allowedModels" platform="anthropic" />
<ModelWhitelistSelector v-model="allowedModels" platform="anthropic" :sync-credentials="syncPreviewCredentials" />
<p class="text-xs text-gray-500 dark:text-gray-400">
{{ t('admin.accounts.selectedModels', { count: allowedModels.length }) }}
<span v-if="allowedModels.length === 0">{{ t('admin.accounts.supportsAllModels') }}</span>
@@ -1813,7 +1813,7 @@
<!-- Whitelist Mode -->
<div v-if="modelRestrictionMode === 'whitelist'">
<ModelWhitelistSelector v-model="allowedModels" :platform="form.platform" />
<ModelWhitelistSelector v-model="allowedModels" :platform="form.platform" :sync-credentials="syncPreviewCredentials" />
<p class="text-xs text-gray-500 dark:text-gray-400">
{{ t('admin.accounts.selectedModels', { count: allowedModels.length }) }}
<span v-if="allowedModels.length === 0">{{
@@ -3361,6 +3361,17 @@ const accountCategory = ref<'oauth-based' | 'apikey' | 'bedrock' | 'service_acco
const addMethod = ref<AddMethod>('oauth') // For oauth-based: 'oauth' or 'setup-token'
const apiKeyBaseUrl = ref('https://api.anthropic.com')
const apiKeyValue = ref('')
const syncPreviewCredentials = computed(() => {
if (!apiKeyValue.value) return undefined
return {
platform: form.platform,
type: form.type,
base_url: apiKeyBaseUrl.value || undefined,
api_key: apiKeyValue.value
}
})
const editQuotaLimit = ref<number | null>(null)
const editQuotaDailyLimit = ref<number | null>(null)
const editQuotaWeeklyLimit = ref<number | null>(null)
@@ -133,6 +133,7 @@ import { ref, computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { useAppStore } from '@/stores/app'
import { accountsAPI } from '@/api/admin/accounts'
import type { SyncUpstreamPreviewParams } from '@/api/admin/accounts'
import ModelIcon from '@/components/common/ModelIcon.vue'
import Icon from '@/components/icons/Icon.vue'
import { allModels, getModelsByPlatform } from '@/composables/useModelWhitelist'
@@ -144,6 +145,12 @@ const props = defineProps<{
platform?: string
platforms?: string[]
accountId?: number
syncCredentials?: {
platform: string
type: string
base_url?: string
api_key: string
}
}>()
const emit = defineEmits<{
@@ -176,9 +183,14 @@ const normalizedPlatforms = computed(() => {
const upstreamSyncPlatforms = new Set(['anthropic', 'openai', 'gemini', 'antigravity'])
const canSyncUpstream = computed(() => {
if (!props.accountId) return false
if (normalizedPlatforms.value.length === 0) return true
return normalizedPlatforms.value.some(platform => upstreamSyncPlatforms.has(platform.toLowerCase()))
if (props.accountId) {
if (normalizedPlatforms.value.length === 0) return true
return normalizedPlatforms.value.some(platform => upstreamSyncPlatforms.has(platform.toLowerCase()))
}
if (props.syncCredentials) {
return upstreamSyncPlatforms.has(props.syncCredentials.platform.toLowerCase())
}
return false
})
const availableOptions = computed(() => {
@@ -249,11 +261,20 @@ const fillRelated = () => {
}
const syncUpstreamModels = async () => {
if (!props.accountId || isSyncingUpstream.value) return
if (isSyncingUpstream.value) return
if (!props.accountId && !props.syncCredentials) return
isSyncingUpstream.value = true
try {
const result = await accountsAPI.syncUpstreamModels(props.accountId)
let result
if (props.accountId) {
result = await accountsAPI.syncUpstreamModels(props.accountId)
} else if (props.syncCredentials) {
result = await accountsAPI.syncUpstreamModelsPreview(props.syncCredentials as SyncUpstreamPreviewParams)
} else {
return
}
const upstreamModels = result.models.map(model => model.trim()).filter(Boolean)
if (upstreamModels.length === 0) {
appStore.showInfo(t('admin.accounts.syncUpstreamModelsEmpty'))