mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-10 19:29:50 +08:00
refactor: 优化优先级管理和路由展示功能
- 支持按 API 格式设置不同的全局优先级,替换单一 global_priority 字段为 global_priority_by_format - 路由标签根据调度模式动态显示(缓存亲和/负载均衡/固定顺序) - 优先级管理对话框 UI 优化:紧凑布局、活跃/停用状态分组排序、快捷启用/禁用开关 - 优先级管理对话框包含所有 Key(含停用的),支持快速切换状态 - 优化配置更新流程:先保存优先级数据,再切换调度模式,避免瞬态不一致 - 调度相关配置缓存 TTL 从 60 秒降至 5 秒,确保多 Worker 快速收敛 - 配置更新时立即同步当前 Worker 的 Scheduler 状态
This commit is contained in:
@@ -876,7 +876,7 @@ export interface RoutingKeyInfo {
|
|||||||
name: string
|
name: string
|
||||||
masked_key: string
|
masked_key: string
|
||||||
internal_priority: number
|
internal_priority: number
|
||||||
global_priority?: number | null
|
global_priority_by_format?: Record<string, number> | null // 按 API 格式的全局优先级
|
||||||
rpm_limit?: number | null
|
rpm_limit?: number | null
|
||||||
is_adaptive: boolean
|
is_adaptive: boolean
|
||||||
effective_rpm?: number | null
|
effective_rpm?: number | null
|
||||||
|
|||||||
@@ -110,12 +110,12 @@
|
|||||||
v-for="(keyGroup, groupIndex) in formatGroup.keyGroups"
|
v-for="(keyGroup, groupIndex) in formatGroup.keyGroups"
|
||||||
:key="groupIndex"
|
:key="groupIndex"
|
||||||
>
|
>
|
||||||
<!-- 第一组且有多个 key 时显示负载均衡标签 -->
|
<!-- 第一组且有多个 key 时显示调度行为标签 -->
|
||||||
<div
|
<div
|
||||||
v-if="groupIndex === 0 && keyGroup.keys.length > 1"
|
v-if="groupIndex === 0 && keyGroup.keys.length > 1"
|
||||||
class="ml-6 mr-3 mb-1 flex items-center gap-1 text-[10px] text-muted-foreground/60"
|
class="ml-6 mr-3 mb-1 flex items-center gap-1 text-[10px] text-muted-foreground/60"
|
||||||
>
|
>
|
||||||
<span>负载均衡</span>
|
<span>{{ samePriorityLabel }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 该优先级组内的 Keys -->
|
<!-- 该优先级组内的 Keys -->
|
||||||
@@ -248,7 +248,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 降级标记(如果下一组有多个 key,显示"降级 · 负载均衡") -->
|
<!-- 降级标记 -->
|
||||||
<div
|
<div
|
||||||
v-if="groupIndex < formatGroup.keyGroups.length - 1"
|
v-if="groupIndex < formatGroup.keyGroups.length - 1"
|
||||||
class="flex py-0.5"
|
class="flex py-0.5"
|
||||||
@@ -259,7 +259,7 @@
|
|||||||
<div class="flex items-center gap-1 text-[10px] text-muted-foreground/50">
|
<div class="flex items-center gap-1 text-[10px] text-muted-foreground/50">
|
||||||
<ArrowDown class="w-3 h-3" />
|
<ArrowDown class="w-3 h-3" />
|
||||||
<span>
|
<span>
|
||||||
{{ formatGroup.keyGroups[groupIndex + 1].keys.length > 1 ? '降级 · 负载均衡' : '降级' }}
|
{{ getDemoteLabel(formatGroup.keyGroups[groupIndex + 1].keys.length) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -404,12 +404,12 @@
|
|||||||
v-for="(group, groupIndex) in getKeyPriorityGroups(providerEntry.keys)"
|
v-for="(group, groupIndex) in getKeyPriorityGroups(providerEntry.keys)"
|
||||||
:key="groupIndex"
|
:key="groupIndex"
|
||||||
>
|
>
|
||||||
<!-- 第一组且有多个 key 时显示负载均衡标签 -->
|
<!-- 第一组且有多个 key 时显示调度行为标签 -->
|
||||||
<div
|
<div
|
||||||
v-if="groupIndex === 0 && group.keys.length > 1"
|
v-if="groupIndex === 0 && group.keys.length > 1"
|
||||||
class="flex items-center gap-1 text-[10px] text-muted-foreground/60 mb-0.5 ml-4"
|
class="flex items-center gap-1 text-[10px] text-muted-foreground/60 mb-0.5 ml-4"
|
||||||
>
|
>
|
||||||
<span>负载均衡</span>
|
<span>{{ samePriorityLabel }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 该优先级组内的 Keys -->
|
<!-- 该优先级组内的 Keys -->
|
||||||
@@ -510,7 +510,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 优先级组之间的降级标记(如果下一组有多个 key,显示"降级 · 负载均衡") -->
|
<!-- 优先级组之间的降级标记 -->
|
||||||
<div
|
<div
|
||||||
v-if="groupIndex < getKeyPriorityGroups(providerEntry.keys).length - 1"
|
v-if="groupIndex < getKeyPriorityGroups(providerEntry.keys).length - 1"
|
||||||
class="flex items-center my-0.5 text-[10px] text-muted-foreground/50"
|
class="flex items-center my-0.5 text-[10px] text-muted-foreground/50"
|
||||||
@@ -520,7 +520,7 @@
|
|||||||
<ArrowDown class="w-3 h-3" />
|
<ArrowDown class="w-3 h-3" />
|
||||||
</div>
|
</div>
|
||||||
<span>
|
<span>
|
||||||
{{ getKeyPriorityGroups(providerEntry.keys)[groupIndex + 1].keys.length > 1 ? '降级 · 负载均衡' : '降级' }}
|
{{ getDemoteLabel(getKeyPriorityGroups(providerEntry.keys)[groupIndex + 1].keys.length) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -718,9 +718,13 @@ const apiFormatGroups = computed<ApiFormatGroup[]>(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Key 按全局优先级分组排序(全局 Key 优先模式)
|
// Key 按全局优先级分组排序(全局 Key 优先模式)
|
||||||
|
// 从 global_priority_by_format 中提取当前 API 格式的优先级
|
||||||
const keyGroupMap = new Map<number, GlobalKeyEntry[]>()
|
const keyGroupMap = new Map<number, GlobalKeyEntry[]>()
|
||||||
for (const keyEntry of data.allKeys) {
|
for (const keyEntry of data.allKeys) {
|
||||||
const priority = keyEntry.key.global_priority ?? 999
|
const priorityByFormat = keyEntry.key.global_priority_by_format
|
||||||
|
const priority = (priorityByFormat && format in priorityByFormat)
|
||||||
|
? priorityByFormat[format]
|
||||||
|
: 999
|
||||||
if (!keyGroupMap.has(priority)) {
|
if (!keyGroupMap.has(priority)) {
|
||||||
keyGroupMap.set(priority, [])
|
keyGroupMap.set(priority, [])
|
||||||
}
|
}
|
||||||
@@ -843,6 +847,19 @@ function getSchedulingModeLabel(mode: string): string {
|
|||||||
return labels[mode] || mode
|
return labels[mode] || mode
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 当前调度模式的显示标签(复用 getSchedulingModeLabel,computed 缓存避免重复计算)
|
||||||
|
const samePriorityLabel = computed(() =>
|
||||||
|
getSchedulingModeLabel(routingData.value?.scheduling_mode || 'cache_affinity')
|
||||||
|
)
|
||||||
|
|
||||||
|
// 获取降级标签(含同优先级调度行为)
|
||||||
|
function getDemoteLabel(nextGroupKeyCount: number): string {
|
||||||
|
if (nextGroupKeyCount > 1) {
|
||||||
|
return `降级 · ${samePriorityLabel.value}`
|
||||||
|
}
|
||||||
|
return '降级'
|
||||||
|
}
|
||||||
|
|
||||||
// 获取优先级模式标签
|
// 获取优先级模式标签
|
||||||
function getPriorityModeLabel(mode: string): string {
|
function getPriorityModeLabel(mode: string): string {
|
||||||
const labels: Record<string, string> = {
|
const labels: Record<string, string> = {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<Dialog
|
<Dialog
|
||||||
:model-value="internalOpen"
|
:model-value="internalOpen"
|
||||||
title="优先级管理"
|
title="优先级管理"
|
||||||
description="调整提供商和 API Key 的优先级顺序,保存后自动切换对应的调度策略"
|
description="拖拽调整顺序,点击序号可编辑(相同数字为同级),保存后自动切换对应的调度策略"
|
||||||
:icon="ListOrdered"
|
:icon="ListOrdered"
|
||||||
size="2xl"
|
size="2xl"
|
||||||
@update:model-value="handleDialogUpdate"
|
@update:model-value="handleDialogUpdate"
|
||||||
@@ -38,19 +38,13 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 内容区域 -->
|
<!-- 内容区域:固定高度,切换 tab 时不跳动 -->
|
||||||
<div class="max-h-[70vh]">
|
<div class="h-[min(65vh,520px)]">
|
||||||
<!-- 提供商优先级 -->
|
<!-- 提供商优先级 -->
|
||||||
<div
|
<div
|
||||||
v-show="activeMainTab === 'provider'"
|
v-show="activeMainTab === 'provider'"
|
||||||
class="space-y-4"
|
class="h-full"
|
||||||
>
|
>
|
||||||
<!-- 提示信息 -->
|
|
||||||
<div class="flex items-center gap-2 px-3 py-2 text-xs text-muted-foreground bg-muted/30 rounded-md">
|
|
||||||
<Info class="w-3.5 h-3.5 shrink-0" />
|
|
||||||
<span>拖拽调整顺序,点击序号可编辑(相同数字为同级,负载均衡)</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 空状态 -->
|
<!-- 空状态 -->
|
||||||
<div
|
<div
|
||||||
v-if="sortedProviders.length === 0"
|
v-if="sortedProviders.length === 0"
|
||||||
@@ -63,7 +57,7 @@
|
|||||||
<!-- 提供商列表 -->
|
<!-- 提供商列表 -->
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
class="space-y-1 max-h-[calc(70vh-80px)] overflow-y-auto pr-1"
|
class="space-y-1 h-full overflow-y-auto pr-1"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-for="(provider, index) in sortedProviders"
|
v-for="(provider, index) in sortedProviders"
|
||||||
@@ -104,7 +98,7 @@
|
|||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
class="w-6 h-6 rounded-md bg-muted/50 flex items-center justify-center text-xs font-medium text-muted-foreground cursor-pointer hover:bg-primary/10 hover:text-primary transition-colors"
|
class="w-6 h-6 rounded-md bg-muted/50 flex items-center justify-center text-xs font-medium text-muted-foreground cursor-pointer hover:bg-primary/10 hover:text-primary transition-colors"
|
||||||
title="点击编辑优先级,相同数字为同级(负载均衡)"
|
title="点击编辑优先级,相同数字为同级"
|
||||||
@click.stop="startEditProviderPriority(provider)"
|
@click.stop="startEditProviderPriority(provider)"
|
||||||
>
|
>
|
||||||
{{ provider.provider_priority }}
|
{{ provider.provider_priority }}
|
||||||
@@ -153,14 +147,8 @@
|
|||||||
<!-- Key 优先级 -->
|
<!-- Key 优先级 -->
|
||||||
<div
|
<div
|
||||||
v-show="activeMainTab === 'key'"
|
v-show="activeMainTab === 'key'"
|
||||||
class="space-y-3"
|
class="h-full"
|
||||||
>
|
>
|
||||||
<!-- 提示信息 -->
|
|
||||||
<div class="flex items-center gap-2 px-3 py-2 text-xs text-muted-foreground bg-muted/30 rounded-md">
|
|
||||||
<Info class="w-3.5 h-3.5 shrink-0" />
|
|
||||||
<span>拖拽调整顺序,点击序号可编辑(相同数字为同级,负载均衡)</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 加载状态 -->
|
<!-- 加载状态 -->
|
||||||
<div
|
<div
|
||||||
v-if="loadingKeys"
|
v-if="loadingKeys"
|
||||||
@@ -184,10 +172,10 @@
|
|||||||
<!-- 左右布局:格式列表 + Key 列表 -->
|
<!-- 左右布局:格式列表 + Key 列表 -->
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
class="flex gap-4"
|
class="flex gap-4 h-full"
|
||||||
>
|
>
|
||||||
<!-- 左侧:API 格式列表 -->
|
<!-- 左侧:API 格式列表 -->
|
||||||
<div class="w-32 shrink-0 space-y-1">
|
<div class="w-32 shrink-0 space-y-1 overflow-y-auto">
|
||||||
<button
|
<button
|
||||||
v-for="format in availableFormats"
|
v-for="format in availableFormats"
|
||||||
:key="format"
|
:key="format"
|
||||||
@@ -200,12 +188,12 @@
|
|||||||
]"
|
]"
|
||||||
@click="activeFormatTab = format"
|
@click="activeFormatTab = format"
|
||||||
>
|
>
|
||||||
{{ format }}
|
{{ API_FORMAT_LABELS[format] || format }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 右侧:Key 列表 -->
|
<!-- 右侧:Key 列表 -->
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0 h-full overflow-hidden">
|
||||||
<div
|
<div
|
||||||
v-for="format in availableFormats"
|
v-for="format in availableFormats"
|
||||||
v-show="activeFormatTab === format"
|
v-show="activeFormatTab === format"
|
||||||
@@ -213,29 +201,36 @@
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="keysByFormat[format]?.length > 0"
|
v-if="keysByFormat[format]?.length > 0"
|
||||||
class="space-y-2 max-h-[calc(70vh-80px)] overflow-y-auto pr-1"
|
class="space-y-1 h-full overflow-y-auto pr-1"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-for="(key, index) in keysByFormat[format]"
|
v-for="(key, index) in keysByFormat[format]"
|
||||||
:key="key.id"
|
:key="key.id"
|
||||||
class="group flex items-center gap-3 px-3 py-2.5 rounded-lg border transition-all duration-200"
|
class="group flex items-center gap-2 px-2.5 py-2 rounded-lg border transition-all duration-200"
|
||||||
:class="[
|
:class="[
|
||||||
draggedKey[format] === index
|
!(key.is_active && key.provider_active)
|
||||||
? 'border-primary/50 bg-primary/5 shadow-md scale-[1.01]'
|
? 'border-border/30 bg-muted/20 opacity-50'
|
||||||
: dragOverKey[format] === index
|
: draggedKey[format] === index
|
||||||
? 'border-primary/30 bg-primary/5'
|
? 'border-primary/50 bg-primary/5 shadow-md scale-[1.01]'
|
||||||
: 'border-border/50 bg-background hover:border-border hover:bg-muted/30'
|
: dragOverKey[format] === index
|
||||||
|
? 'border-primary/30 bg-primary/5'
|
||||||
|
: 'border-border/50 bg-background hover:border-border hover:bg-muted/30'
|
||||||
]"
|
]"
|
||||||
draggable="true"
|
:draggable="key.is_active && key.provider_active"
|
||||||
@dragstart="handleKeyDragStart(format, index, $event)"
|
@dragstart="(key.is_active && key.provider_active) && handleKeyDragStart(format, index, $event)"
|
||||||
@dragend="handleKeyDragEnd(format)"
|
@dragend="handleKeyDragEnd(format)"
|
||||||
@dragover.prevent="handleKeyDragOver(format, index)"
|
@dragover.prevent="handleKeyDragOver(format, index)"
|
||||||
@dragleave="handleKeyDragLeave(format)"
|
@dragleave="handleKeyDragLeave(format)"
|
||||||
@drop="handleKeyDrop(format, index)"
|
@drop="handleKeyDrop(format, index)"
|
||||||
>
|
>
|
||||||
<!-- 拖拽手柄 -->
|
<!-- 拖拽手柄 -->
|
||||||
<div class="cursor-grab active:cursor-grabbing p-1 rounded hover:bg-muted text-muted-foreground/40 group-hover:text-muted-foreground transition-colors shrink-0">
|
<div
|
||||||
<GripVertical class="w-4 h-4" />
|
class="p-0.5 rounded transition-colors shrink-0"
|
||||||
|
:class="(key.is_active && key.provider_active)
|
||||||
|
? 'cursor-grab active:cursor-grabbing text-muted-foreground/30 group-hover:text-muted-foreground'
|
||||||
|
: 'text-muted-foreground/15 cursor-default'"
|
||||||
|
>
|
||||||
|
<GripVertical class="w-3.5 h-3.5" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 可编辑序号 -->
|
<!-- 可编辑序号 -->
|
||||||
@@ -245,7 +240,7 @@
|
|||||||
type="number"
|
type="number"
|
||||||
min="1"
|
min="1"
|
||||||
:value="key.priority"
|
:value="key.priority"
|
||||||
class="w-8 h-6 rounded-md bg-background border border-primary text-xs font-medium text-center focus:outline-none [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
|
class="w-7 h-5 rounded bg-background border border-primary text-[11px] font-medium text-center focus:outline-none [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
|
||||||
autofocus
|
autofocus
|
||||||
@blur="finishEditKeyPriority(format, key, $event)"
|
@blur="finishEditKeyPriority(format, key, $event)"
|
||||||
@keydown.enter="($event.target as HTMLInputElement).blur()"
|
@keydown.enter="($event.target as HTMLInputElement).blur()"
|
||||||
@@ -253,87 +248,86 @@
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
class="w-6 h-6 rounded-md bg-muted/50 flex items-center justify-center text-xs font-medium text-muted-foreground cursor-pointer hover:bg-primary/10 hover:text-primary transition-colors"
|
class="w-5 h-5 rounded bg-muted/50 flex items-center justify-center text-[11px] font-medium text-muted-foreground cursor-pointer hover:bg-primary/10 hover:text-primary transition-colors"
|
||||||
title="点击编辑优先级,相同数字为同级(负载均衡)"
|
title="点击编辑优先级"
|
||||||
@click.stop="startEditKeyPriority(format, key)"
|
@click.stop="startEditKeyPriority(format, key)"
|
||||||
>
|
>
|
||||||
{{ key.priority }}
|
{{ key.priority }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Key 信息 -->
|
<!-- Key 信息(两行) -->
|
||||||
<div class="flex-1 min-w-0 flex items-center gap-3">
|
<div class="flex-1 min-w-0">
|
||||||
<!-- 左侧:名称和来源 -->
|
<!-- 第一行:Key 名称 + Key 级别状态 -->
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex items-center gap-1.5">
|
||||||
<div class="flex items-center gap-2">
|
<span
|
||||||
<span class="font-medium text-sm">{{ key.name }}</span>
|
class="text-sm font-medium truncate"
|
||||||
<Badge
|
:class="!(key.is_active && key.provider_active) ? 'text-muted-foreground' : ''"
|
||||||
v-if="key.circuit_breaker_open"
|
>{{ key.name }}</span>
|
||||||
variant="destructive"
|
<Badge
|
||||||
class="text-[10px] h-5 px-1.5 shrink-0"
|
v-if="key.circuit_breaker_open"
|
||||||
>
|
variant="destructive"
|
||||||
熔断
|
class="text-[9px] h-4 px-1 shrink-0"
|
||||||
</Badge>
|
>
|
||||||
<Badge
|
熔断
|
||||||
v-else-if="!key.is_active"
|
</Badge>
|
||||||
variant="secondary"
|
<Badge
|
||||||
class="text-[10px] h-5 px-1.5 shrink-0"
|
v-else-if="!key.is_active && key.provider_active"
|
||||||
>
|
variant="secondary"
|
||||||
停用
|
class="text-[9px] h-4 px-1 shrink-0"
|
||||||
</Badge>
|
>
|
||||||
<!-- 能力标签紧跟名称 -->
|
停用
|
||||||
<template v-if="key.capabilities?.length">
|
</Badge>
|
||||||
<span
|
|
||||||
v-for="cap in key.capabilities.slice(0, 2)"
|
|
||||||
:key="cap"
|
|
||||||
class="px-1 py-0.5 bg-muted text-muted-foreground rounded text-[10px]"
|
|
||||||
>{{ cap }}</span>
|
|
||||||
<span
|
|
||||||
v-if="key.capabilities.length > 2"
|
|
||||||
class="text-[10px] text-muted-foreground"
|
|
||||||
>+{{ key.capabilities.length - 2 }}</span>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-1.5 text-xs text-muted-foreground mt-1">
|
|
||||||
<span class="text-[10px] font-medium shrink-0">{{ key.provider_name }}</span>
|
|
||||||
<span class="font-mono text-[10px] opacity-60 truncate">{{ key.api_key_masked }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<!-- 第二行:密钥脱敏 · Provider 名称 + Provider 级别状态 -->
|
||||||
|
<div class="flex items-center gap-0 mt-0.5">
|
||||||
|
<span class="font-mono text-[10px] text-muted-foreground/50 truncate">{{ key.api_key_masked }}</span>
|
||||||
|
<span class="text-[10px] text-muted-foreground/40 mx-1">·</span>
|
||||||
|
<span class="text-[10px] text-muted-foreground shrink-0">{{ key.provider_name }}</span>
|
||||||
|
<Badge
|
||||||
|
v-if="!key.provider_active"
|
||||||
|
variant="secondary"
|
||||||
|
class="text-[9px] h-4 px-1 shrink-0 ml-1"
|
||||||
|
>
|
||||||
|
停用
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 右侧:健康度 + 速率 -->
|
<!-- 右侧:健康度/倍率 + 开关 -->
|
||||||
<div class="shrink-0 flex items-center gap-3">
|
<div class="shrink-0 flex items-center gap-2">
|
||||||
<!-- 健康度 -->
|
<!-- 健康度 + 倍率(两行) -->
|
||||||
|
<div class="text-right min-w-[36px]">
|
||||||
<div
|
<div
|
||||||
v-if="key.health_score != null"
|
v-if="key.health_score != null"
|
||||||
class="text-xs text-right"
|
class="text-[11px] font-medium tabular-nums text-foreground"
|
||||||
>
|
>
|
||||||
<div
|
{{ ((key.health_score || 0) * 100).toFixed(0) }}%
|
||||||
class="font-medium tabular-nums"
|
|
||||||
:class="[
|
|
||||||
key.health_score >= 0.95 ? 'text-green-600' :
|
|
||||||
key.health_score >= 0.5 ? 'text-yellow-600' : 'text-red-500'
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
{{ ((key.health_score || 0) * 100).toFixed(0) }}%
|
|
||||||
</div>
|
|
||||||
<div class="text-[10px] text-muted-foreground opacity-70">
|
|
||||||
{{ key.request_count }} reqs
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
class="text-xs text-muted-foreground/50 text-right"
|
class="text-[11px] text-muted-foreground/40"
|
||||||
>
|
>
|
||||||
<div>--</div>
|
--
|
||||||
<div class="text-[10px]">
|
|
||||||
无数据
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<!-- 速率倍数 -->
|
<div class="text-[10px] text-muted-foreground tabular-nums">
|
||||||
<div class="text-sm font-medium tabular-nums text-primary min-w-[40px] text-right">
|
|
||||||
{{ key.rate_multipliers?.[format] ?? 1 }}x
|
{{ key.rate_multipliers?.[format] ?? 1 }}x
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- 快捷启用/禁用开关 -->
|
||||||
|
<button
|
||||||
|
class="p-0.5 rounded transition-colors shrink-0"
|
||||||
|
:class="!key.provider_active
|
||||||
|
? 'text-muted-foreground/20 cursor-not-allowed'
|
||||||
|
: key.is_active
|
||||||
|
? 'text-foreground/70 hover:bg-muted hover:text-foreground'
|
||||||
|
: 'text-muted-foreground hover:bg-muted hover:text-foreground'"
|
||||||
|
:title="!key.provider_active ? 'Provider 已停用' : key.is_active ? '点击停用' : '点击启用'"
|
||||||
|
:disabled="!key.provider_active"
|
||||||
|
@click.stop="toggleKeyActive(format, key)"
|
||||||
|
>
|
||||||
|
<Power class="w-3.5 h-3.5" />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -432,7 +426,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, watch } from 'vue'
|
import { ref, computed, watch } from 'vue'
|
||||||
import { GripVertical, Layers, Key, Info, Loader2, ListOrdered } from 'lucide-vue-next'
|
import { GripVertical, Layers, Key, Loader2, ListOrdered, Power } from 'lucide-vue-next'
|
||||||
import { Dialog } from '@/components/ui'
|
import { Dialog } from '@/components/ui'
|
||||||
import Button from '@/components/ui/button.vue'
|
import Button from '@/components/ui/button.vue'
|
||||||
import Badge from '@/components/ui/badge.vue'
|
import Badge from '@/components/ui/badge.vue'
|
||||||
@@ -441,7 +435,7 @@ import { updateProvider, updateProviderKey } from '@/api/endpoints'
|
|||||||
import type { ProviderWithEndpointsSummary } from '@/api/endpoints'
|
import 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, API_FORMAT_LABELS } from '@/api/endpoints/types'
|
||||||
|
|
||||||
interface KeyWithMeta {
|
interface KeyWithMeta {
|
||||||
id: string
|
id: string
|
||||||
@@ -453,6 +447,7 @@ interface KeyWithMeta {
|
|||||||
priority: number // 用于编辑的优先级
|
priority: number // 用于编辑的优先级
|
||||||
rate_multipliers: Record<string, number> | null
|
rate_multipliers: Record<string, number> | null
|
||||||
is_active: boolean
|
is_active: boolean
|
||||||
|
provider_active: boolean
|
||||||
circuit_breaker_open: boolean
|
circuit_breaker_open: boolean
|
||||||
provider_name: string
|
provider_name: string
|
||||||
endpoint_base_url: string
|
endpoint_base_url: string
|
||||||
@@ -567,6 +562,16 @@ const availableFormats = computed(() => {
|
|||||||
return Object.keys(keysByFormat.value).sort()
|
return Object.keys(keysByFormat.value).sort()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 排序 Key:活跃的在前,停用的(Key或Provider)在后,各自按优先级排序
|
||||||
|
function sortKeysByActiveAndPriority(keys: KeyWithMeta[]): KeyWithMeta[] {
|
||||||
|
return [...keys].sort((a, b) => {
|
||||||
|
const aActive = a.is_active && a.provider_active
|
||||||
|
const bActive = b.is_active && b.provider_active
|
||||||
|
if (aActive !== bActive) return aActive ? -1 : 1
|
||||||
|
return a.priority - b.priority
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// 排序 providers:启用的在前,停用的在后,各自按优先级排序
|
// 排序 providers:启用的在前,停用的在后,各自按优先级排序
|
||||||
function sortProvidersByActiveAndPriority(providers: ProviderWithEndpointsSummary[]) {
|
function sortProvidersByActiveAndPriority(providers: ProviderWithEndpointsSummary[]) {
|
||||||
return [...providers].sort((a, b) => {
|
return [...providers].sort((a, b) => {
|
||||||
@@ -640,8 +645,8 @@ async function loadKeysByFormat() {
|
|||||||
// 使用格式特定优先级,如果没有则分配默认值
|
// 使用格式特定优先级,如果没有则分配默认值
|
||||||
priority: key.format_priority ?? nextPriority++
|
priority: key.format_priority ?? nextPriority++
|
||||||
}))
|
}))
|
||||||
// 按优先级排序
|
// 按优先级排序:活跃的在前,停用的(Key或Provider)在后,各自按优先级排序
|
||||||
data[format].sort((a, b) => a.priority - b.priority)
|
data[format] = sortKeysByActiveAndPriority(data[format])
|
||||||
}
|
}
|
||||||
keysByFormat.value = data
|
keysByFormat.value = data
|
||||||
|
|
||||||
@@ -656,6 +661,29 @@ async function loadKeysByFormat() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 快捷切换 Key 启用/禁用状态
|
||||||
|
async function toggleKeyActive(format: string, key: KeyWithMeta) {
|
||||||
|
const newStatus = !key.is_active
|
||||||
|
try {
|
||||||
|
await updateProviderKey(key.id, { is_active: newStatus })
|
||||||
|
// 更新本地状态(该 key 可能出现在多个格式中)
|
||||||
|
for (const fmt of Object.keys(keysByFormat.value)) {
|
||||||
|
const keys = keysByFormat.value[fmt]
|
||||||
|
const found = keys.find(k => k.id === key.id)
|
||||||
|
if (found) {
|
||||||
|
found.is_active = newStatus
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 重新排序所有格式(该 key 可能出现在多个格式中,状态变更后都需要重排)
|
||||||
|
for (const fmt of Object.keys(keysByFormat.value)) {
|
||||||
|
keysByFormat.value[fmt] = sortKeysByActiveAndPriority(keysByFormat.value[fmt])
|
||||||
|
}
|
||||||
|
success(newStatus ? 'Key 已启用' : 'Key 已停用')
|
||||||
|
} catch (err: any) {
|
||||||
|
showError(err.response?.data?.detail || '操作失败', '错误')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Key 优先级编辑
|
// Key 优先级编辑
|
||||||
function startEditKeyPriority(format: string, key: KeyWithMeta) {
|
function startEditKeyPriority(format: string, key: KeyWithMeta) {
|
||||||
editingKeyPriority.value[format] = key.id
|
editingKeyPriority.value[format] = key.id
|
||||||
@@ -673,7 +701,7 @@ function finishEditKeyPriority(format: string, key: KeyWithMeta, event: FocusEve
|
|||||||
// 每个格式独立管理优先级,只更新当前格式
|
// 每个格式独立管理优先级,只更新当前格式
|
||||||
key.priority = newPriority
|
key.priority = newPriority
|
||||||
// 重新排序当前格式
|
// 重新排序当前格式
|
||||||
keysByFormat.value[format] = [...keysByFormat.value[format]].sort((a, b) => a.priority - b.priority)
|
keysByFormat.value[format] = sortKeysByActiveAndPriority(keysByFormat.value[format])
|
||||||
}
|
}
|
||||||
|
|
||||||
editingKeyPriority.value[format] = null
|
editingKeyPriority.value[format] = null
|
||||||
@@ -871,7 +899,7 @@ function handleKeyDrop(format: string, dropIndex: number) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// 按优先级重新排序
|
// 按优先级重新排序
|
||||||
keysByFormat.value[format] = [...items].sort((a, b) => a.priority - b.priority)
|
keysByFormat.value[format] = sortKeysByActiveAndPriority(items)
|
||||||
draggedKey.value[format] = null
|
draggedKey.value[format] = null
|
||||||
dragOverKey.value[format] = null
|
dragOverKey.value[format] = null
|
||||||
}
|
}
|
||||||
@@ -883,20 +911,8 @@ async function save() {
|
|||||||
|
|
||||||
const newMode = activeMainTab.value === 'key' ? 'global_key' : 'provider'
|
const newMode = activeMainTab.value === 'key' ? 'global_key' : 'provider'
|
||||||
|
|
||||||
// 保存优先级模式和调度模式
|
// 第一步:先保存所有 Provider 和 Key 的优先级数据
|
||||||
await Promise.all([
|
// 确保优先级数据全部到位后,再切换调度模式,避免瞬态不一致
|
||||||
adminApi.updateSystemConfig(
|
|
||||||
'provider_priority_mode',
|
|
||||||
newMode,
|
|
||||||
'Provider/Key 优先级策略:provider(提供商优先模式) 或 global_key(全局Key优先模式)'
|
|
||||||
),
|
|
||||||
adminApi.updateSystemConfig(
|
|
||||||
'scheduling_mode',
|
|
||||||
schedulingMode.value,
|
|
||||||
'调度模式:fixed_order(固定顺序模式) 或 cache_affinity(缓存亲和模式)'
|
|
||||||
)
|
|
||||||
])
|
|
||||||
|
|
||||||
const providerUpdates = sortedProviders.value.map((provider) =>
|
const providerUpdates = sortedProviders.value.map((provider) =>
|
||||||
updateProvider(provider.id, { provider_priority: provider.provider_priority })
|
updateProvider(provider.id, { provider_priority: provider.provider_priority })
|
||||||
)
|
)
|
||||||
@@ -919,6 +935,19 @@ async function save() {
|
|||||||
|
|
||||||
await Promise.all([...providerUpdates, ...keyUpdates])
|
await Promise.all([...providerUpdates, ...keyUpdates])
|
||||||
|
|
||||||
|
// 第二步:优先级数据全部就绪后,顺序保存调度配置
|
||||||
|
// 先保存优先级模式,再保存调度模式,确保 Scheduler 状态完整切换
|
||||||
|
await adminApi.updateSystemConfig(
|
||||||
|
'provider_priority_mode',
|
||||||
|
newMode,
|
||||||
|
'Provider/Key 优先级策略:provider(提供商优先模式) 或 global_key(全局Key优先模式)'
|
||||||
|
)
|
||||||
|
await adminApi.updateSystemConfig(
|
||||||
|
'scheduling_mode',
|
||||||
|
schedulingMode.value,
|
||||||
|
'调度模式:cache_affinity(缓存亲和模式) 或 load_balance(负载均衡模式) 或 fixed_order(固定顺序模式)'
|
||||||
|
)
|
||||||
|
|
||||||
await loadKeysByFormat()
|
await loadKeysByFormat()
|
||||||
|
|
||||||
success('优先级已保存')
|
success('优先级已保存')
|
||||||
|
|||||||
@@ -712,13 +712,10 @@ class AdminGetKeysGroupedByFormatAdapter(AdminApiAdapter):
|
|||||||
db = context.db
|
db = context.db
|
||||||
|
|
||||||
# Key 属于 Provider:按 key.api_formats 分组展示
|
# Key 属于 Provider:按 key.api_formats 分组展示
|
||||||
|
# 包含所有 Key(含停用的 Key 和停用的 Provider),前端可显示停用标签和快捷开关
|
||||||
keys = (
|
keys = (
|
||||||
db.query(ProviderAPIKey, Provider)
|
db.query(ProviderAPIKey, Provider)
|
||||||
.join(Provider, ProviderAPIKey.provider_id == Provider.id)
|
.join(Provider, ProviderAPIKey.provider_id == Provider.id)
|
||||||
.filter(
|
|
||||||
ProviderAPIKey.is_active.is_(True),
|
|
||||||
Provider.is_active.is_(True),
|
|
||||||
)
|
|
||||||
.order_by(
|
.order_by(
|
||||||
ProviderAPIKey.internal_priority.asc(),
|
ProviderAPIKey.internal_priority.asc(),
|
||||||
)
|
)
|
||||||
@@ -789,6 +786,7 @@ class AdminGetKeysGroupedByFormatAdapter(AdminApiAdapter):
|
|||||||
"global_priority_by_format": key.global_priority_by_format,
|
"global_priority_by_format": key.global_priority_by_format,
|
||||||
"rate_multipliers": key.rate_multipliers,
|
"rate_multipliers": key.rate_multipliers,
|
||||||
"is_active": key.is_active,
|
"is_active": key.is_active,
|
||||||
|
"provider_active": provider.is_active,
|
||||||
"provider_name": provider.name,
|
"provider_name": provider.name,
|
||||||
"api_formats": api_formats,
|
"api_formats": api_formats,
|
||||||
"capabilities": caps_list,
|
"capabilities": caps_list,
|
||||||
|
|||||||
@@ -634,6 +634,38 @@ class AdminSetSystemConfigAdapter(AdminApiAdapter):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"更新用户配额重置任务时间失败: {e}")
|
logger.warning(f"更新用户配额重置任务时间失败: {e}")
|
||||||
|
|
||||||
|
# 如果更新的是调度模式或优先级模式,立即更新当前 Worker 的 Scheduler 单例
|
||||||
|
if self.key in ("scheduling_mode", "provider_priority_mode"):
|
||||||
|
try:
|
||||||
|
from src.clients.redis_client import get_redis_client_sync
|
||||||
|
from src.services.cache.aware_scheduler import get_cache_aware_scheduler
|
||||||
|
|
||||||
|
redis_client = get_redis_client_sync()
|
||||||
|
# 从数据库读取两个调度配置的最新值,确保一致性
|
||||||
|
priority_mode = SystemConfigService.get_config(
|
||||||
|
context.db,
|
||||||
|
"provider_priority_mode",
|
||||||
|
"provider",
|
||||||
|
)
|
||||||
|
scheduling_mode = SystemConfigService.get_config(
|
||||||
|
context.db,
|
||||||
|
"scheduling_mode",
|
||||||
|
"cache_affinity",
|
||||||
|
)
|
||||||
|
await get_cache_aware_scheduler(
|
||||||
|
redis_client,
|
||||||
|
priority_mode=priority_mode,
|
||||||
|
scheduling_mode=scheduling_mode,
|
||||||
|
)
|
||||||
|
logger.info(
|
||||||
|
"[AdminSetSystemConfig] 已同步更新 Scheduler: "
|
||||||
|
"priority_mode={}, scheduling_mode={}",
|
||||||
|
priority_mode,
|
||||||
|
scheduling_mode,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning("同步更新 Scheduler 失败: {}", e)
|
||||||
|
|
||||||
# 返回时不暴露加密后的值
|
# 返回时不暴露加密后的值
|
||||||
display_value = "********" if self.key in self.ENCRYPTED_KEYS else config.value
|
display_value = "********" if self.key in self.ENCRYPTED_KEYS else config.value
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,13 @@ class RequestRecordLevel(str, Enum):
|
|||||||
# 进程内缓存 TTL(秒)- 系统配置变化不频繁,使用较长的 TTL
|
# 进程内缓存 TTL(秒)- 系统配置变化不频繁,使用较长的 TTL
|
||||||
_CONFIG_CACHE_TTL = 60 # 1 分钟
|
_CONFIG_CACHE_TTL = 60 # 1 分钟
|
||||||
|
|
||||||
|
# 调度相关配置使用更短的缓存 TTL,确保多 Worker 部署时快速收敛
|
||||||
|
# 当管理员切换调度模式时,其他 Worker 最多延迟 5 秒即可感知变更
|
||||||
|
_SCHEDULING_CONFIG_CACHE_TTL = 5 # 5 秒
|
||||||
|
|
||||||
|
# 需要跨 Worker 快速同步的调度相关配置 key
|
||||||
|
SCHEDULING_CONFIG_KEYS = frozenset({"scheduling_mode", "provider_priority_mode"})
|
||||||
|
|
||||||
# 进程内缓存存储: {key: (value, expire_time)}
|
# 进程内缓存存储: {key: (value, expire_time)}
|
||||||
_config_cache: dict[str, tuple[Any, float]] = {}
|
_config_cache: dict[str, tuple[Any, float]] = {}
|
||||||
|
|
||||||
@@ -49,8 +56,12 @@ def _get_cached_config(key: str) -> tuple[bool, Any]:
|
|||||||
|
|
||||||
|
|
||||||
def _set_cached_config(key: str, value: Any) -> None:
|
def _set_cached_config(key: str, value: Any) -> None:
|
||||||
"""设置进程内缓存"""
|
"""设置进程内缓存
|
||||||
_config_cache[key] = (value, time.time() + _CONFIG_CACHE_TTL)
|
|
||||||
|
调度相关配置使用更短的 TTL(5秒),确保多 Worker 部署时快速收敛。
|
||||||
|
"""
|
||||||
|
ttl = _SCHEDULING_CONFIG_CACHE_TTL if key in SCHEDULING_CONFIG_KEYS else _CONFIG_CACHE_TTL
|
||||||
|
_config_cache[key] = (value, time.time() + ttl)
|
||||||
|
|
||||||
|
|
||||||
def invalidate_config_cache(key: str | None = None) -> None:
|
def invalidate_config_cache(key: str | None = None) -> None:
|
||||||
|
|||||||
Reference in New Issue
Block a user