refactor: 重构限流系统和健康监控,支持按 API 格式区分

- 将 adaptive_concurrency 重命名为 adaptive_rpm,从并发控制改为 RPM 控制
- 健康监控器支持按 API 格式独立管理健康度和熔断器状态
- 新增 model_permissions 模块,支持按格式配置允许的模型
- 重构前端提供商相关表单组件,新增 Collapsible UI 组件
- 新增数据库迁移脚本支持新的数据结构
This commit is contained in:
fawney19
2026-01-10 18:43:53 +08:00
parent dd2fbf4424
commit 09e0f594ff
97 changed files with 6642 additions and 4169 deletions

View File

@@ -530,9 +530,6 @@
/>
<div class="flex-1 min-w-0">
<p class="font-medium text-sm truncate">
{{ provider.display_name || provider.name }}
</p>
<p class="text-xs text-muted-foreground truncate">
{{ provider.name }}
</p>
</div>
@@ -645,10 +642,7 @@
/>
<div class="flex-1 min-w-0">
<p class="font-medium text-sm truncate">
{{ provider.display_name }}
</p>
<p class="text-xs text-muted-foreground truncate">
{{ provider.identifier }}
{{ provider.name }}
</p>
</div>
<Badge
@@ -679,7 +673,7 @@
<ProviderModelFormDialog
:open="editProviderDialogOpen"
:provider-id="editingProvider?.id || ''"
:provider-name="editingProvider?.display_name || ''"
:provider-name="editingProvider?.name || ''"
:editing-model="editingProviderModel"
@update:open="handleEditProviderDialogUpdate"
@saved="handleEditProviderSaved"
@@ -939,7 +933,7 @@ async function batchAddSelectedProviders() {
const errorMessages = result.errors
.map(e => {
const provider = providerOptions.value.find(p => p.id === e.provider_id)
const providerName = provider?.display_name || provider?.name || e.provider_id
const providerName = provider?.name || e.provider_id
return `${providerName}: ${e.error}`
})
.join('\n')
@@ -977,7 +971,7 @@ async function batchRemoveSelectedProviders() {
await deleteModel(providerId, provider.model_id)
successCount++
} catch (err: any) {
errors.push(`${provider.display_name}: ${parseApiError(err, '删除失败')}`)
errors.push(`${provider.name}: ${parseApiError(err, '删除失败')}`)
}
}
@@ -1088,8 +1082,7 @@ async function loadModelProviders(_globalModelId: string) {
selectedModelProviders.value = response.providers.map(p => ({
id: p.provider_id,
model_id: p.model_id,
display_name: p.provider_display_name || p.provider_name,
identifier: p.provider_name,
name: p.provider_name,
provider_type: 'API',
target_model: p.target_model,
is_active: p.is_active,
@@ -1219,7 +1212,7 @@ async function confirmDeleteProviderImplementation(provider: any) {
}
const confirmed = await confirmDanger(
`确定要删除 ${provider.display_name} 的模型关联吗?\n\n模型: ${provider.target_model}\n\n此操作不可恢复`,
`确定要删除 ${provider.name} 的模型关联吗?\n\n模型: ${provider.target_model}\n\n此操作不可恢复`,
'删除关联提供商'
)
if (!confirmed) return
@@ -1227,7 +1220,7 @@ async function confirmDeleteProviderImplementation(provider: any) {
try {
const { deleteModel } = await import('@/api/endpoints')
await deleteModel(provider.id, provider.model_id)
success(`已删除 ${provider.display_name} 的模型实现`)
success(`已删除 ${provider.name} 的模型实现`)
// 重新加载 Provider 列表
if (selectedModel.value) {
await loadModelProviders(selectedModel.value.id)

View File

@@ -134,10 +134,7 @@
@click="handleRowClick($event, provider.id)"
>
<TableCell class="py-3.5">
<div class="flex flex-col gap-0.5">
<span class="text-sm font-medium text-foreground">{{ provider.display_name }}</span>
<span class="text-xs text-muted-foreground/70 font-mono">{{ provider.name }}</span>
</div>
<span class="text-sm font-medium text-foreground">{{ provider.name }}</span>
</TableCell>
<TableCell class="py-3.5">
<Badge
@@ -219,17 +216,10 @@
>${{ (provider.monthly_used_usd ?? 0).toFixed(2) }}</span> / <span class="font-medium">${{ (provider.monthly_quota_usd ?? 0).toFixed(2) }}</span>
</div>
<div
v-if="rpmUsage(provider)"
class="flex items-center gap-1"
>
<span class="text-muted-foreground/70">RPM:</span>
<span class="font-medium text-foreground/80">{{ rpmUsage(provider) }}</span>
</div>
<div
v-if="provider.billing_type !== 'monthly_quota' && !rpmUsage(provider)"
v-else
class="text-muted-foreground/50"
>
无限制
按量付费
</div>
</div>
</TableCell>
@@ -304,7 +294,7 @@
<div class="flex items-start justify-between gap-3">
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2">
<span class="font-medium text-foreground truncate">{{ provider.display_name }}</span>
<span class="font-medium text-foreground truncate">{{ provider.name }}</span>
<Badge
:variant="provider.is_active ? 'success' : 'secondary'"
class="text-xs shrink-0"
@@ -312,7 +302,6 @@
{{ provider.is_active ? '活跃' : '停用' }}
</Badge>
</div>
<span class="text-xs text-muted-foreground/70 font-mono">{{ provider.name }}</span>
</div>
<div
class="flex items-center gap-0.5 shrink-0"
@@ -383,20 +372,17 @@
</span>
</div>
<!-- 第四行配额/限流 -->
<!-- 第四行配额 -->
<div
v-if="provider.billing_type === 'monthly_quota' || rpmUsage(provider)"
v-if="provider.billing_type === 'monthly_quota'"
class="flex items-center gap-3 text-xs text-muted-foreground"
>
<span v-if="provider.billing_type === 'monthly_quota'">
<span>
配额: <span
class="font-semibold"
:class="getQuotaUsedColorClass(provider)"
>${{ (provider.monthly_used_usd ?? 0).toFixed(2) }}</span> / ${{ (provider.monthly_quota_usd ?? 0).toFixed(2) }}
</span>
<span v-if="rpmUsage(provider)">
RPM: {{ rpmUsage(provider) }}
</span>
</div>
</div>
</div>
@@ -509,7 +495,7 @@ const filteredProviders = computed(() => {
if (searchQuery.value.trim()) {
const keywords = searchQuery.value.toLowerCase().split(/\s+/).filter(k => k.length > 0)
result = result.filter(p => {
const searchableText = `${p.display_name} ${p.name}`.toLowerCase()
const searchableText = `${p.name}`.toLowerCase()
return keywords.every(keyword => searchableText.includes(keyword))
})
}
@@ -525,7 +511,7 @@ const filteredProviders = computed(() => {
return a.provider_priority - b.provider_priority
}
// 3. 按名称排序
return a.display_name.localeCompare(b.display_name)
return a.name.localeCompare(b.name)
})
})
@@ -586,7 +572,10 @@ function sortEndpoints(endpoints: any[]) {
// 判断端点是否可用(有 key
function isEndpointAvailable(endpoint: any, _provider: ProviderWithEndpointsSummary): boolean {
// 检查端点是否有活跃的密钥
// 检查端点是否启用,以及是否有活跃的密钥
if (endpoint.is_active === false) {
return false
}
return (endpoint.active_keys ?? 0) > 0
}
@@ -639,21 +628,6 @@ function getQuotaUsedColorClass(provider: ProviderWithEndpointsSummary): string
return 'text-foreground'
}
function rpmUsage(provider: ProviderWithEndpointsSummary): string | null {
const rpmLimit = provider.rpm_limit
const rpmUsed = provider.rpm_used ?? 0
if (rpmLimit === null || rpmLimit === undefined) {
return rpmUsed > 0 ? `${rpmUsed}` : null
}
if (rpmLimit === 0) {
return '已完全禁止'
}
return `${rpmUsed} / ${rpmLimit}`
}
// 使用复用的行点击逻辑
const { handleMouseDown, shouldTriggerRowClick } = useRowClick()
@@ -706,7 +680,7 @@ function handleProviderAdded() {
async function handleDeleteProvider(provider: ProviderWithEndpointsSummary) {
const confirmed = await confirmDanger(
'删除提供商',
`确定要删除提供商 "${provider.display_name}" 吗?\n\n这将同时删除其所有端点、密钥和配置。此操作不可恢复`
`确定要删除提供商 "${provider.name}" 吗?\n\n这将同时删除其所有端点、密钥和配置。此操作不可恢复`
)
if (!confirmed) return

View File

@@ -511,7 +511,7 @@
端点: {{ importPreview.providers?.reduce((sum: number, p: any) => sum + (p.endpoints?.length || 0), 0) }}
</li>
<li>
API Keys: {{ importPreview.providers?.reduce((sum: number, p: any) => sum + p.endpoints?.reduce((s: number, e: any) => s + (e.keys?.length || 0), 0), 0) }}
API Keys: {{ importPreview.providers?.reduce((sum: number, p: any) => sum + (p.api_keys?.length || 0), 0) }}
</li>
</ul>
</div>
@@ -1144,7 +1144,7 @@ function handleConfigFileSelect(event: Event) {
const data = JSON.parse(content) as ConfigExportData
// 验证版本
if (data.version !== '1.0') {
if (data.version !== '2.0') {
error(`不支持的配置版本: ${data.version}`)
return
}