mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-03 01:40:21 +08:00
feat: Provider 摘要 API 改为服务端分页,支持搜索和筛选
- 后端 /summary 接口新增 page/page_size/search/status/api_format/model_id 参数 - 新增 ProviderSummaryPageResponse 分页响应模型 - 前端 useProviderFilters 从客户端筛选改为构建服务端查询参数 - ProviderManagement 通过 watch queryParams 实现分页/筛选联动,搜索 debounce 300ms - PriorityManagementDialog 改为对话框打开时自行加载全量 providers - 其他使用方(StandaloneKeyFormDialog/UserFormDialog/ReplayDialog/ModelManagement)适配新接口
This commit is contained in:
@@ -9,13 +9,32 @@ import type {
|
|||||||
} from './types'
|
} from './types'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取 Providers 摘要(包含 Endpoints 统计)
|
* 获取 Providers 摘要(分页)
|
||||||
*/
|
*/
|
||||||
export async function getProvidersSummary(): Promise<ProviderWithEndpointsSummary[]> {
|
export interface ProviderSummaryQuery {
|
||||||
return dedupedRequest('providers:summary', async () => {
|
page?: number
|
||||||
const response = await client.get<ProviderWithEndpointsSummary[]>('/api/admin/providers/summary')
|
page_size?: number
|
||||||
return response.data
|
search?: string
|
||||||
})
|
status?: string
|
||||||
|
api_format?: string
|
||||||
|
model_id?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProviderSummaryPageResponse {
|
||||||
|
total: number
|
||||||
|
page: number
|
||||||
|
page_size: number
|
||||||
|
items: ProviderWithEndpointsSummary[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getProvidersSummary(
|
||||||
|
params: ProviderSummaryQuery = {},
|
||||||
|
): Promise<ProviderSummaryPageResponse> {
|
||||||
|
const response = await client.get<ProviderSummaryPageResponse>(
|
||||||
|
'/api/admin/providers/summary',
|
||||||
|
{ params },
|
||||||
|
)
|
||||||
|
return response.data
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -401,12 +401,12 @@ const { isEditMode, handleDialogUpdate, handleCancel } = useFormDialog({
|
|||||||
// 加载选项数据
|
// 加载选项数据
|
||||||
async function loadAccessRestrictionOptions() {
|
async function loadAccessRestrictionOptions() {
|
||||||
try {
|
try {
|
||||||
const [providersData, modelsData, formatsData] = await Promise.all([
|
const [providersResponse, modelsData, formatsData] = await Promise.all([
|
||||||
getProvidersSummary(),
|
getProvidersSummary({ page_size: 9999 }),
|
||||||
getGlobalModels({ limit: 1000, is_active: true }),
|
getGlobalModels({ limit: 1000, is_active: true }),
|
||||||
adminApi.getApiFormats()
|
adminApi.getApiFormats()
|
||||||
])
|
])
|
||||||
providers.value = providersData
|
providers.value = providersResponse.items
|
||||||
globalModels.value = modelsData.models || []
|
globalModels.value = modelsData.models || []
|
||||||
allApiFormats.value = formatsData.formats?.map((f: { value: string }) => f.value) || []
|
allApiFormats.value = formatsData.formats?.map((f: { value: string }) => f.value) || []
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -472,7 +472,7 @@ import Badge from '@/components/ui/badge.vue'
|
|||||||
import { useToast } from '@/composables/useToast'
|
import { useToast } from '@/composables/useToast'
|
||||||
import { parseApiError } from '@/utils/errorParser'
|
import { parseApiError } from '@/utils/errorParser'
|
||||||
import { updateProvider, updateProviderKey } from '@/api/endpoints'
|
import { updateProvider, updateProviderKey } from '@/api/endpoints'
|
||||||
import type { ProviderWithEndpointsSummary } from '@/api/endpoints'
|
import { getProvidersSummary, type ProviderWithEndpointsSummary } from '@/api/endpoints'
|
||||||
import { adminApi } from '@/api/admin'
|
import { adminApi } from '@/api/admin'
|
||||||
import { batchQueryBalance, type ActionResultResponse, type BalanceInfo } from '@/api/providerOps'
|
import { batchQueryBalance, type ActionResultResponse, type BalanceInfo } from '@/api/providerOps'
|
||||||
import { API_FORMAT_SHORT } from '@/api/endpoints/types'
|
import { API_FORMAT_SHORT } from '@/api/endpoints/types'
|
||||||
@@ -508,7 +508,6 @@ interface KeyWithMeta {
|
|||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
modelValue: boolean
|
modelValue: boolean
|
||||||
providers: ProviderWithEndpointsSummary[]
|
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@@ -663,7 +662,7 @@ function normalizeRateMultipliers(
|
|||||||
|
|
||||||
const providerById = computed(() => {
|
const providerById = computed(() => {
|
||||||
const map = new Map<string, ProviderWithEndpointsSummary>()
|
const map = new Map<string, ProviderWithEndpointsSummary>()
|
||||||
props.providers.forEach((provider) => {
|
sortedProviders.value.forEach((provider) => {
|
||||||
map.set(provider.id, provider)
|
map.set(provider.id, provider)
|
||||||
})
|
})
|
||||||
return map
|
return map
|
||||||
@@ -671,7 +670,7 @@ const providerById = computed(() => {
|
|||||||
|
|
||||||
const providerIdByName = computed(() => {
|
const providerIdByName = computed(() => {
|
||||||
const map = new Map<string, string>()
|
const map = new Map<string, string>()
|
||||||
props.providers.forEach((provider) => {
|
sortedProviders.value.forEach((provider) => {
|
||||||
if (!map.has(provider.name)) {
|
if (!map.has(provider.name)) {
|
||||||
map.set(provider.name, provider.id)
|
map.set(provider.name, provider.id)
|
||||||
}
|
}
|
||||||
@@ -686,7 +685,7 @@ function resolveProviderId(key: Pick<KeyWithMeta, 'provider_id' | 'provider_name
|
|||||||
|
|
||||||
const poolProviderIds = computed(() => {
|
const poolProviderIds = computed(() => {
|
||||||
const set = new Set<string>()
|
const set = new Set<string>()
|
||||||
props.providers.forEach((provider) => {
|
sortedProviders.value.forEach((provider) => {
|
||||||
if (provider.pool_advanced) {
|
if (provider.pool_advanced) {
|
||||||
set.add(provider.id)
|
set.add(provider.id)
|
||||||
}
|
}
|
||||||
@@ -835,16 +834,10 @@ function sortProvidersByActiveAndPriority(providers: ProviderWithEndpointsSummar
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 监听 props.providers 变化
|
|
||||||
watch(() => props.providers, (newProviders) => {
|
|
||||||
if (newProviders) {
|
|
||||||
sortedProviders.value = sortProvidersByActiveAndPriority(newProviders)
|
|
||||||
}
|
|
||||||
}, { immediate: true })
|
|
||||||
|
|
||||||
// 监听对话框打开
|
// 监听对话框打开
|
||||||
watch(internalOpen, async (open) => {
|
watch(internalOpen, async (open) => {
|
||||||
if (open) {
|
if (open) {
|
||||||
|
await loadAllProviders()
|
||||||
await loadCurrentPriorityMode()
|
await loadCurrentPriorityMode()
|
||||||
await loadKeysByFormat()
|
await loadKeysByFormat()
|
||||||
// 异步加载余额数据
|
// 异步加载余额数据
|
||||||
@@ -852,6 +845,16 @@ watch(internalOpen, async (open) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 加载全量 providers(优先级管理需要完整列表)
|
||||||
|
async function loadAllProviders() {
|
||||||
|
try {
|
||||||
|
const response = await getProvidersSummary({ page: 1, page_size: 9999 })
|
||||||
|
sortedProviders.value = sortProvidersByActiveAndPriority(response.items)
|
||||||
|
} catch {
|
||||||
|
sortedProviders.value = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 加载当前的优先级模式配置
|
// 加载当前的优先级模式配置
|
||||||
async function loadCurrentPriorityMode() {
|
async function loadCurrentPriorityMode() {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ref, computed, watch } from 'vue'
|
import { ref, computed, watch } from 'vue'
|
||||||
import type { ProviderWithEndpointsSummary } from '@/api/endpoints'
|
import type { ProviderSummaryQuery } from '@/api/endpoints'
|
||||||
|
|
||||||
export interface FilterOption {
|
export interface FilterOption {
|
||||||
value: string
|
value: string
|
||||||
@@ -7,7 +7,6 @@ export interface FilterOption {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useProviderFilters(
|
export function useProviderFilters(
|
||||||
providers: () => ProviderWithEndpointsSummary[],
|
|
||||||
globalModels: () => { id: string; name: string }[],
|
globalModels: () => { id: string; name: string }[],
|
||||||
) {
|
) {
|
||||||
// 搜索与筛选
|
// 搜索与筛选
|
||||||
@@ -33,11 +32,8 @@ export function useProviderFilters(
|
|||||||
{ value: 'gemini:cli', label: 'Gemini CLI' },
|
{ value: 'gemini:cli', label: 'Gemini CLI' },
|
||||||
]
|
]
|
||||||
|
|
||||||
// 动态计算模型筛选选项:只展示当前提供商列表中实际关联的全局模型
|
|
||||||
const modelFilters = computed<FilterOption[]>(() => {
|
const modelFilters = computed<FilterOption[]>(() => {
|
||||||
const usedIds = new Set(providers().flatMap(p => p.global_model_ids || []))
|
|
||||||
const items = globalModels()
|
const items = globalModels()
|
||||||
.filter(m => usedIds.has(m.id))
|
|
||||||
.map(m => ({ value: m.id, label: m.name }))
|
.map(m => ({ value: m.id, label: m.name }))
|
||||||
.sort((a, b) => a.label.localeCompare(b.label))
|
.sort((a, b) => a.label.localeCompare(b.label))
|
||||||
return [{ value: 'all', label: '全部模型' }, ...items]
|
return [{ value: 'all', label: '全部模型' }, ...items]
|
||||||
@@ -52,68 +48,22 @@ export function useProviderFilters(
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
// 筛选后的提供商列表
|
|
||||||
const filteredProviders = computed(() => {
|
|
||||||
let result = [...providers()]
|
|
||||||
|
|
||||||
// 搜索筛选(支持空格分隔的多关键词 AND 搜索)
|
|
||||||
if (searchQuery.value.trim()) {
|
|
||||||
const keywords = searchQuery.value
|
|
||||||
.toLowerCase()
|
|
||||||
.split(/\s+/)
|
|
||||||
.filter(k => k.length > 0)
|
|
||||||
result = result.filter(p => {
|
|
||||||
const searchableText = `${p.name}`.toLowerCase()
|
|
||||||
return keywords.every(keyword => searchableText.includes(keyword))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 状态筛选
|
|
||||||
if (filterStatus.value !== 'all') {
|
|
||||||
const isActive = filterStatus.value === 'active'
|
|
||||||
result = result.filter(p => p.is_active === isActive)
|
|
||||||
}
|
|
||||||
|
|
||||||
// API 格式筛选
|
|
||||||
if (filterApiFormat.value !== 'all') {
|
|
||||||
result = result.filter(
|
|
||||||
p => p.api_formats && p.api_formats.includes(filterApiFormat.value),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 模型筛选
|
|
||||||
if (filterModel.value !== 'all') {
|
|
||||||
result = result.filter(
|
|
||||||
p => p.global_model_ids && p.global_model_ids.includes(filterModel.value),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 排序
|
|
||||||
return result.sort((a, b) => {
|
|
||||||
// 1. 优先显示活跃的提供商
|
|
||||||
if (a.is_active !== b.is_active) {
|
|
||||||
return a.is_active ? -1 : 1
|
|
||||||
}
|
|
||||||
// 2. 按优先级排序
|
|
||||||
if (a.provider_priority !== b.provider_priority) {
|
|
||||||
return a.provider_priority - b.provider_priority
|
|
||||||
}
|
|
||||||
// 3. 按名称排序
|
|
||||||
return a.name.localeCompare(b.name)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// 分页
|
// 分页
|
||||||
const currentPage = ref(1)
|
const currentPage = ref(1)
|
||||||
const pageSize = ref(20)
|
const pageSize = ref(20)
|
||||||
|
const total = ref(0)
|
||||||
|
|
||||||
const paginatedProviders = computed(() => {
|
// 服务端分页查询参数
|
||||||
const start = (currentPage.value - 1) * pageSize.value
|
const queryParams = computed<ProviderSummaryQuery>(() => ({
|
||||||
const end = start + pageSize.value
|
page: currentPage.value,
|
||||||
return filteredProviders.value.slice(start, end)
|
page_size: pageSize.value,
|
||||||
})
|
search: searchQuery.value.trim() || undefined,
|
||||||
|
status: filterStatus.value !== 'all' ? filterStatus.value : undefined,
|
||||||
|
api_format: filterApiFormat.value !== 'all' ? filterApiFormat.value : undefined,
|
||||||
|
model_id: filterModel.value !== 'all' ? filterModel.value : undefined,
|
||||||
|
}))
|
||||||
|
|
||||||
// 搜索/筛选时重置分页
|
// 搜索/筛选变化时重置分页到第1页
|
||||||
watch([searchQuery, filterStatus, filterApiFormat, filterModel], () => {
|
watch([searchQuery, filterStatus, filterApiFormat, filterModel], () => {
|
||||||
currentPage.value = 1
|
currentPage.value = 1
|
||||||
})
|
})
|
||||||
@@ -134,10 +84,10 @@ export function useProviderFilters(
|
|||||||
apiFormatFilters,
|
apiFormatFilters,
|
||||||
modelFilters,
|
modelFilters,
|
||||||
hasActiveFilters,
|
hasActiveFilters,
|
||||||
filteredProviders,
|
|
||||||
currentPage,
|
currentPage,
|
||||||
pageSize,
|
pageSize,
|
||||||
paginatedProviders,
|
total,
|
||||||
|
queryParams,
|
||||||
resetFilters,
|
resetFilters,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -388,8 +388,8 @@ watch(() => props.isOpen, async (isOpen) => {
|
|||||||
showRequestHeaders.value = false
|
showRequestHeaders.value = false
|
||||||
showResponseHeaders.value = false
|
showResponseHeaders.value = false
|
||||||
try {
|
try {
|
||||||
const summary = await getProvidersSummary()
|
const response = await getProvidersSummary({ page_size: 9999 })
|
||||||
providers.value = summary
|
providers.value = response.items
|
||||||
.filter(p => p.is_active && p.active_endpoints > 0)
|
.filter(p => p.is_active && p.active_endpoints > 0)
|
||||||
.map(p => ({ id: p.id, name: p.name }))
|
.map(p => ({ id: p.id, name: p.name }))
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -536,12 +536,12 @@ const isFormValid = computed(() => {
|
|||||||
// 加载访问控制选项
|
// 加载访问控制选项
|
||||||
async function loadAccessControlOptions(): Promise<void> {
|
async function loadAccessControlOptions(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const [providersData, modelsData, formatsData] = await Promise.all([
|
const [providersResponse, modelsData, formatsData] = await Promise.all([
|
||||||
getProvidersSummary(),
|
getProvidersSummary({ page_size: 9999 }),
|
||||||
getGlobalModels({ limit: 1000, is_active: true }),
|
getGlobalModels({ limit: 1000, is_active: true }),
|
||||||
adminApi.getApiFormats()
|
adminApi.getApiFormats()
|
||||||
])
|
])
|
||||||
providers.value = providersData
|
providers.value = providersResponse.items
|
||||||
globalModels.value = modelsData.models || []
|
globalModels.value = modelsData.models || []
|
||||||
apiFormats.value = formatsData.formats || []
|
apiFormats.value = formatsData.formats || []
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -1168,7 +1168,7 @@ async function ensureProviderOptions() {
|
|||||||
providerOptionsRequest = (async () => {
|
providerOptionsRequest = (async () => {
|
||||||
try {
|
try {
|
||||||
loadingProviderOptions.value = true
|
loadingProviderOptions.value = true
|
||||||
providerOptions.value = await getProvidersSummary()
|
providerOptions.value = (await getProvidersSummary({ page_size: 9999 })).items
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
const message = parseApiError(err, '加载 Provider 列表失败')
|
const message = parseApiError(err, '加载 Provider 列表失败')
|
||||||
showError(message, '错误')
|
showError(message, '错误')
|
||||||
@@ -1494,7 +1494,7 @@ async function refreshData() {
|
|||||||
async function loadProviders() {
|
async function loadProviders() {
|
||||||
const requestId = ++providersRequestId
|
const requestId = ++providersRequestId
|
||||||
try {
|
try {
|
||||||
const nextProviders = await getProvidersSummary()
|
const nextProviders = (await getProvidersSummary({ page_size: 9999 })).items
|
||||||
if (requestId !== providersRequestId) return
|
if (requestId !== providersRequestId) return
|
||||||
providers.value = nextProviders
|
providers.value = nextProviders
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
|
|||||||
@@ -36,7 +36,7 @@
|
|||||||
|
|
||||||
<!-- 空状态 -->
|
<!-- 空状态 -->
|
||||||
<div
|
<div
|
||||||
v-else-if="filteredProviders.length === 0"
|
v-else-if="providers.length === 0"
|
||||||
class="flex flex-col items-center justify-center py-16 text-center"
|
class="flex flex-col items-center justify-center py-16 text-center"
|
||||||
>
|
>
|
||||||
<div class="text-muted-foreground mb-2">
|
<div class="text-muted-foreground mb-2">
|
||||||
@@ -87,7 +87,7 @@
|
|||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
<ProviderTableRow
|
<ProviderTableRow
|
||||||
v-for="provider in paginatedProviders"
|
v-for="provider in providers"
|
||||||
:key="provider.id"
|
:key="provider.id"
|
||||||
:provider="provider"
|
:provider="provider"
|
||||||
:editing-description-id="editingDescriptionId"
|
:editing-description-id="editingDescriptionId"
|
||||||
@@ -118,11 +118,11 @@
|
|||||||
|
|
||||||
<!-- 移动端卡片列表 -->
|
<!-- 移动端卡片列表 -->
|
||||||
<div
|
<div
|
||||||
v-if="!loading && filteredProviders.length > 0"
|
v-if="!loading && providers.length > 0"
|
||||||
class="xl:hidden divide-y divide-border/40"
|
class="xl:hidden divide-y divide-border/40"
|
||||||
>
|
>
|
||||||
<ProviderMobileCard
|
<ProviderMobileCard
|
||||||
v-for="provider in paginatedProviders"
|
v-for="provider in providers"
|
||||||
:key="provider.id"
|
:key="provider.id"
|
||||||
:provider="provider"
|
:provider="provider"
|
||||||
:editing-description-id="editingDescriptionId"
|
:editing-description-id="editingDescriptionId"
|
||||||
@@ -146,9 +146,9 @@
|
|||||||
|
|
||||||
<!-- 分页 -->
|
<!-- 分页 -->
|
||||||
<Pagination
|
<Pagination
|
||||||
v-if="!loading && filteredProviders.length > 0"
|
v-if="!loading && total > 0"
|
||||||
:current="currentPage"
|
:current="currentPage"
|
||||||
:total="filteredProviders.length"
|
:total="total"
|
||||||
:page-size="pageSize"
|
:page-size="pageSize"
|
||||||
cache-key="provider-management-page-size"
|
cache-key="provider-management-page-size"
|
||||||
@update:current="currentPage = $event"
|
@update:current="currentPage = $event"
|
||||||
@@ -168,7 +168,6 @@
|
|||||||
|
|
||||||
<PriorityManagementDialog
|
<PriorityManagementDialog
|
||||||
v-model="priorityDialogOpen"
|
v-model="priorityDialogOpen"
|
||||||
:providers="providers"
|
|
||||||
@saved="handlePrioritySaved"
|
@saved="handlePrioritySaved"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -190,7 +189,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
import { ref, computed, watch, onMounted, onUnmounted } from 'vue'
|
||||||
import Button from '@/components/ui/button.vue'
|
import Button from '@/components/ui/button.vue'
|
||||||
import Card from '@/components/ui/card.vue'
|
import Card from '@/components/ui/card.vue'
|
||||||
import Table from '@/components/ui/table.vue'
|
import Table from '@/components/ui/table.vue'
|
||||||
@@ -247,13 +246,12 @@ const {
|
|||||||
apiFormatFilters,
|
apiFormatFilters,
|
||||||
modelFilters,
|
modelFilters,
|
||||||
hasActiveFilters,
|
hasActiveFilters,
|
||||||
filteredProviders,
|
|
||||||
currentPage,
|
currentPage,
|
||||||
pageSize,
|
pageSize,
|
||||||
paginatedProviders,
|
total,
|
||||||
|
queryParams,
|
||||||
resetFilters,
|
resetFilters,
|
||||||
} = useProviderFilters(
|
} = useProviderFilters(
|
||||||
() => providers.value,
|
|
||||||
() => globalModels.value,
|
() => globalModels.value,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -349,14 +347,15 @@ async function loadGlobalModelList() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加载提供商列表
|
// 加载提供商列表(服务端分页)
|
||||||
async function loadProviders() {
|
async function loadProviders() {
|
||||||
const requestId = ++providersRequestId
|
const requestId = ++providersRequestId
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const nextProviders = await getProvidersSummary()
|
const response = await getProvidersSummary(queryParams.value)
|
||||||
if (requestId !== providersRequestId) return
|
if (requestId !== providersRequestId) return
|
||||||
providers.value = nextProviders
|
providers.value = response.items
|
||||||
|
total.value = response.total
|
||||||
// 异步加载配置了 ops 的 provider 的余额数据
|
// 异步加载配置了 ops 的 provider 的余额数据
|
||||||
loadBalances(providers.value)
|
loadBalances(providers.value)
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
@@ -369,6 +368,24 @@ async function loadProviders() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 分页/筛选/搜索变化时重新加载
|
||||||
|
let debounceTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
|
watch(queryParams, (newParams, oldParams) => {
|
||||||
|
if (debounceTimer) clearTimeout(debounceTimer)
|
||||||
|
// 搜索输入 debounce 300ms,其他变化立即执行
|
||||||
|
const isSearchOnly = newParams.search !== oldParams?.search &&
|
||||||
|
newParams.page === oldParams?.page &&
|
||||||
|
newParams.page_size === oldParams?.page_size &&
|
||||||
|
newParams.status === oldParams?.status &&
|
||||||
|
newParams.api_format === oldParams?.api_format &&
|
||||||
|
newParams.model_id === oldParams?.model_id
|
||||||
|
if (isSearchOnly) {
|
||||||
|
debounceTimer = setTimeout(loadProviders, 300)
|
||||||
|
} else {
|
||||||
|
loadProviders()
|
||||||
|
}
|
||||||
|
}, { deep: true })
|
||||||
|
|
||||||
// 使用复用的行点击逻辑
|
// 使用复用的行点击逻辑
|
||||||
const { handleMouseDown, shouldTriggerRowClick } = useRowClick()
|
const { handleMouseDown, shouldTriggerRowClick } = useRowClick()
|
||||||
|
|
||||||
@@ -508,6 +525,7 @@ onMounted(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
|
if (debounceTimer) clearTimeout(debounceTimer)
|
||||||
document.removeEventListener('click', handleGlobalClick, true)
|
document.removeEventListener('click', handleGlobalClick, true)
|
||||||
stopTick()
|
stopTick()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ from src.models.endpoint_models import (
|
|||||||
EndpointHealthEvent,
|
EndpointHealthEvent,
|
||||||
EndpointHealthMonitor,
|
EndpointHealthMonitor,
|
||||||
ProviderEndpointHealthMonitorResponse,
|
ProviderEndpointHealthMonitorResponse,
|
||||||
|
ProviderSummaryPageResponse,
|
||||||
ProviderUpdateRequest,
|
ProviderUpdateRequest,
|
||||||
ProviderWithEndpointsSummary,
|
ProviderWithEndpointsSummary,
|
||||||
)
|
)
|
||||||
@@ -46,46 +47,26 @@ router = APIRouter(tags=["Provider Summary"])
|
|||||||
pipeline = ApiRequestPipeline()
|
pipeline = ApiRequestPipeline()
|
||||||
|
|
||||||
|
|
||||||
@router.get("/summary", response_model=list[ProviderWithEndpointsSummary])
|
@router.get("/summary", response_model=ProviderSummaryPageResponse)
|
||||||
async def get_providers_summary(
|
async def get_providers_summary(
|
||||||
request: Request,
|
request: Request,
|
||||||
|
page: int = Query(1, ge=1),
|
||||||
|
page_size: int = Query(20, ge=1, le=10000),
|
||||||
|
search: str = Query("", description="按名称搜索"),
|
||||||
|
status: str = Query("all", description="all/active/inactive"),
|
||||||
|
api_format: str = Query("all", description="API 格式筛选"),
|
||||||
|
model_id: str = Query("all", description="全局模型 ID 筛选"),
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
) -> list[ProviderWithEndpointsSummary]:
|
) -> ProviderSummaryPageResponse:
|
||||||
"""
|
"""获取提供商摘要信息(分页)"""
|
||||||
获取所有提供商摘要信息
|
adapter = AdminProviderSummaryAdapter(
|
||||||
|
page=page,
|
||||||
获取所有提供商的详细摘要信息,包含端点、密钥、模型统计和健康状态。
|
page_size=page_size,
|
||||||
|
search=search,
|
||||||
**返回字段**(数组,每项包含):
|
status=status,
|
||||||
- `id`: 提供商 ID
|
api_format=api_format,
|
||||||
- `name`: 提供商名称
|
model_id=model_id,
|
||||||
- `description`: 描述信息
|
)
|
||||||
- `website`: 官网地址
|
|
||||||
- `provider_priority`: 优先级
|
|
||||||
- `is_active`: 是否启用
|
|
||||||
- `billing_type`: 计费类型
|
|
||||||
- `monthly_quota_usd`: 月度配额(美元)
|
|
||||||
- `monthly_used_usd`: 本月已使用金额(美元)
|
|
||||||
- `quota_reset_day`: 配额重置日期
|
|
||||||
- `quota_last_reset_at`: 上次配额重置时间
|
|
||||||
- `quota_expires_at`: 配额过期时间
|
|
||||||
- `timeout`: 默认请求超时(秒)
|
|
||||||
- `max_retries`: 默认最大重试次数
|
|
||||||
- `proxy`: 默认代理配置
|
|
||||||
- `total_endpoints`: 端点总数
|
|
||||||
- `active_endpoints`: 活跃端点数
|
|
||||||
- `total_keys`: 密钥总数
|
|
||||||
- `active_keys`: 活跃密钥数
|
|
||||||
- `total_models`: 模型总数
|
|
||||||
- `active_models`: 活跃模型数
|
|
||||||
- `avg_health_score`: 平均健康分数(0-1)
|
|
||||||
- `unhealthy_endpoints`: 不健康端点数(健康分数 < 0.5)
|
|
||||||
- `api_formats`: 支持的 API 格式列表
|
|
||||||
- `endpoint_health_details`: 端点健康详情(包含 api_format, health_score, is_active, active_keys)
|
|
||||||
- `created_at`: 创建时间
|
|
||||||
- `updated_at`: 更新时间
|
|
||||||
"""
|
|
||||||
adapter = AdminProviderSummaryAdapter()
|
|
||||||
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
|
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
|
||||||
|
|
||||||
|
|
||||||
@@ -788,20 +769,71 @@ class AdminProviderHealthMonitorAdapter(AdminApiAdapter):
|
|||||||
return response.model_dump()
|
return response.model_dump()
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
class AdminProviderSummaryAdapter(AdminApiAdapter):
|
class AdminProviderSummaryAdapter(AdminApiAdapter):
|
||||||
@cache_result(
|
page: int = 1
|
||||||
key_prefix="admin:providers:summary",
|
page_size: int = 20
|
||||||
ttl=CacheTTL.ADMIN_USAGE_RECORDS,
|
search: str = ""
|
||||||
user_specific=False,
|
status: str = "all"
|
||||||
)
|
api_format: str = "all"
|
||||||
|
model_id: str = "all"
|
||||||
|
|
||||||
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
|
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
|
||||||
db = context.db
|
db = context.db
|
||||||
|
|
||||||
|
query = db.query(Provider)
|
||||||
|
|
||||||
|
# 搜索筛选
|
||||||
|
if self.search.strip():
|
||||||
|
keywords = self.search.strip().lower().split()
|
||||||
|
for kw in keywords:
|
||||||
|
query = query.filter(func.lower(Provider.name).contains(kw))
|
||||||
|
|
||||||
|
# 状态筛选
|
||||||
|
if self.status == "active":
|
||||||
|
query = query.filter(Provider.is_active == True)
|
||||||
|
elif self.status == "inactive":
|
||||||
|
query = query.filter(Provider.is_active == False)
|
||||||
|
|
||||||
|
# API 格式筛选
|
||||||
|
if self.api_format != "all":
|
||||||
|
query = query.filter(
|
||||||
|
Provider.id.in_(
|
||||||
|
db.query(ProviderEndpoint.provider_id)
|
||||||
|
.filter(ProviderEndpoint.api_format == self.api_format)
|
||||||
|
.distinct()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# 全局模型 ID 筛选
|
||||||
|
if self.model_id != "all":
|
||||||
|
query = query.filter(
|
||||||
|
Provider.id.in_(
|
||||||
|
db.query(Model.provider_id)
|
||||||
|
.filter(
|
||||||
|
Model.global_model_id == self.model_id,
|
||||||
|
Model.is_active == True,
|
||||||
|
)
|
||||||
|
.distinct()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
total = query.count()
|
||||||
|
|
||||||
providers = (
|
providers = (
|
||||||
db.query(Provider)
|
query.order_by(Provider.provider_priority.asc(), Provider.created_at.asc())
|
||||||
.order_by(Provider.provider_priority.asc(), Provider.created_at.asc())
|
.offset((self.page - 1) * self.page_size)
|
||||||
|
.limit(self.page_size)
|
||||||
.all()
|
.all()
|
||||||
)
|
)
|
||||||
return [item.model_dump() for item in _build_provider_summaries_batch(db, providers)]
|
|
||||||
|
items = _build_provider_summaries_batch(db, providers)
|
||||||
|
return ProviderSummaryPageResponse(
|
||||||
|
total=total,
|
||||||
|
page=self.page,
|
||||||
|
page_size=self.page_size,
|
||||||
|
items=items,
|
||||||
|
).model_dump()
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
|||||||
@@ -1041,6 +1041,15 @@ class ProviderWithEndpointsSummary(BaseModel):
|
|||||||
model_config = ConfigDict(from_attributes=True)
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
|
||||||
|
class ProviderSummaryPageResponse(BaseModel):
|
||||||
|
"""Provider 摘要分页响应"""
|
||||||
|
|
||||||
|
total: int
|
||||||
|
page: int
|
||||||
|
page_size: int
|
||||||
|
items: list[ProviderWithEndpointsSummary]
|
||||||
|
|
||||||
|
|
||||||
# ========== 健康监控可视化模型 ==========
|
# ========== 健康监控可视化模型 ==========
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user