refactor: 优化优先级管理和路由展示功能

- 支持按 API 格式设置不同的全局优先级,替换单一 global_priority 字段为 global_priority_by_format
- 路由标签根据调度模式动态显示(缓存亲和/负载均衡/固定顺序)
- 优先级管理对话框 UI 优化:紧凑布局、活跃/停用状态分组排序、快捷启用/禁用开关
- 优先级管理对话框包含所有 Key(含停用的),支持快速切换状态
- 优化配置更新流程:先保存优先级数据,再切换调度模式,避免瞬态不一致
- 调度相关配置缓存 TTL 从 60 秒降至 5 秒,确保多 Worker 快速收敛
- 配置更新时立即同步当前 Worker 的 Scheduler 状态
This commit is contained in:
fawney19
2026-02-09 18:21:34 +08:00
parent b14fb31933
commit 7f7f569148
6 changed files with 220 additions and 133 deletions

View File

@@ -876,7 +876,7 @@ export interface RoutingKeyInfo {
name: string
masked_key: string
internal_priority: number
global_priority?: number | null
global_priority_by_format?: Record<string, number> | null // 按 API 格式的全局优先级
rpm_limit?: number | null
is_adaptive: boolean
effective_rpm?: number | null

View File

@@ -110,12 +110,12 @@
v-for="(keyGroup, groupIndex) in formatGroup.keyGroups"
:key="groupIndex"
>
<!-- 第一组且有多个 key 时显示负载均衡标签 -->
<!-- 第一组且有多个 key 时显示调度行为标签 -->
<div
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"
>
<span>负载均衡</span>
<span>{{ samePriorityLabel }}</span>
</div>
<!-- 该优先级组内的 Keys -->
@@ -248,7 +248,7 @@
</div>
</div>
<!-- 降级标记如果下一组有多个 key显示"降级 · 负载均衡" -->
<!-- 降级标记 -->
<div
v-if="groupIndex < formatGroup.keyGroups.length - 1"
class="flex py-0.5"
@@ -259,7 +259,7 @@
<div class="flex items-center gap-1 text-[10px] text-muted-foreground/50">
<ArrowDown class="w-3 h-3" />
<span>
{{ formatGroup.keyGroups[groupIndex + 1].keys.length > 1 ? '降级 · 负载均衡' : '降级' }}
{{ getDemoteLabel(formatGroup.keyGroups[groupIndex + 1].keys.length) }}
</span>
</div>
</div>
@@ -404,12 +404,12 @@
v-for="(group, groupIndex) in getKeyPriorityGroups(providerEntry.keys)"
:key="groupIndex"
>
<!-- 第一组且有多个 key 时显示负载均衡标签 -->
<!-- 第一组且有多个 key 时显示调度行为标签 -->
<div
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"
>
<span>负载均衡</span>
<span>{{ samePriorityLabel }}</span>
</div>
<!-- 该优先级组内的 Keys -->
@@ -510,7 +510,7 @@
</div>
</div>
<!-- 优先级组之间的降级标记如果下一组有多个 key显示"降级 · 负载均衡" -->
<!-- 优先级组之间的降级标记 -->
<div
v-if="groupIndex < getKeyPriorityGroups(providerEntry.keys).length - 1"
class="flex items-center my-0.5 text-[10px] text-muted-foreground/50"
@@ -520,7 +520,7 @@
<ArrowDown class="w-3 h-3" />
</div>
<span>
{{ getKeyPriorityGroups(providerEntry.keys)[groupIndex + 1].keys.length > 1 ? '降级 · 负载均衡' : '降级' }}
{{ getDemoteLabel(getKeyPriorityGroups(providerEntry.keys)[groupIndex + 1].keys.length) }}
</span>
</div>
</div>
@@ -718,9 +718,13 @@ const apiFormatGroups = computed<ApiFormatGroup[]>(() => {
})
// Key 按全局优先级分组排序(全局 Key 优先模式)
// 从 global_priority_by_format 中提取当前 API 格式的优先级
const keyGroupMap = new Map<number, GlobalKeyEntry[]>()
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)) {
keyGroupMap.set(priority, [])
}
@@ -843,6 +847,19 @@ function getSchedulingModeLabel(mode: string): string {
return labels[mode] || mode
}
// 当前调度模式的显示标签(复用 getSchedulingModeLabelcomputed 缓存避免重复计算)
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 {
const labels: Record<string, string> = {

View File

@@ -2,7 +2,7 @@
<Dialog
:model-value="internalOpen"
title="优先级管理"
description="调整提供商和 API Key 的优先级顺序,保存后自动切换对应的调度策略"
description="拖拽调整顺序,点击序号可编辑(相同数字为同级),保存后自动切换对应的调度策略"
:icon="ListOrdered"
size="2xl"
@update:model-value="handleDialogUpdate"
@@ -38,19 +38,13 @@
</button>
</div>
<!-- 内容区域 -->
<div class="max-h-[70vh]">
<!-- 内容区域固定高度切换 tab 时不跳动 -->
<div class="h-[min(65vh,520px)]">
<!-- 提供商优先级 -->
<div
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
v-if="sortedProviders.length === 0"
@@ -63,7 +57,7 @@
<!-- 提供商列表 -->
<div
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
v-for="(provider, index) in sortedProviders"
@@ -104,7 +98,7 @@
<div
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"
title="点击编辑优先级,相同数字为同级(负载均衡)"
title="点击编辑优先级,相同数字为同级"
@click.stop="startEditProviderPriority(provider)"
>
{{ provider.provider_priority }}
@@ -153,14 +147,8 @@
<!-- Key 优先级 -->
<div
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
v-if="loadingKeys"
@@ -184,10 +172,10 @@
<!-- 左右布局格式列表 + Key 列表 -->
<div
v-else
class="flex gap-4"
class="flex gap-4 h-full"
>
<!-- 左侧API 格式列表 -->
<div class="w-32 shrink-0 space-y-1">
<div class="w-32 shrink-0 space-y-1 overflow-y-auto">
<button
v-for="format in availableFormats"
:key="format"
@@ -200,12 +188,12 @@
]"
@click="activeFormatTab = format"
>
{{ format }}
{{ API_FORMAT_LABELS[format] || format }}
</button>
</div>
<!-- 右侧Key 列表 -->
<div class="flex-1 min-w-0">
<div class="flex-1 min-w-0 h-full overflow-hidden">
<div
v-for="format in availableFormats"
v-show="activeFormatTab === format"
@@ -213,29 +201,36 @@
>
<div
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
v-for="(key, index) in keysByFormat[format]"
: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="[
draggedKey[format] === index
? 'border-primary/50 bg-primary/5 shadow-md scale-[1.01]'
: dragOverKey[format] === index
? 'border-primary/30 bg-primary/5'
: 'border-border/50 bg-background hover:border-border hover:bg-muted/30'
!(key.is_active && key.provider_active)
? 'border-border/30 bg-muted/20 opacity-50'
: draggedKey[format] === index
? 'border-primary/50 bg-primary/5 shadow-md scale-[1.01]'
: dragOverKey[format] === index
? 'border-primary/30 bg-primary/5'
: 'border-border/50 bg-background hover:border-border hover:bg-muted/30'
]"
draggable="true"
@dragstart="handleKeyDragStart(format, index, $event)"
:draggable="key.is_active && key.provider_active"
@dragstart="(key.is_active && key.provider_active) && handleKeyDragStart(format, index, $event)"
@dragend="handleKeyDragEnd(format)"
@dragover.prevent="handleKeyDragOver(format, index)"
@dragleave="handleKeyDragLeave(format)"
@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">
<GripVertical class="w-4 h-4" />
<div
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>
<!-- 可编辑序号 -->
@@ -245,7 +240,7 @@
type="number"
min="1"
: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
@blur="finishEditKeyPriority(format, key, $event)"
@keydown.enter="($event.target as HTMLInputElement).blur()"
@@ -253,87 +248,86 @@
>
<div
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"
title="点击编辑优先级,相同数字为同级(负载均衡)"
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="点击编辑优先级"
@click.stop="startEditKeyPriority(format, key)"
>
{{ key.priority }}
</div>
</div>
<!-- Key 信息 -->
<div class="flex-1 min-w-0 flex items-center gap-3">
<!-- 左侧名称和来源 -->
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2">
<span class="font-medium text-sm">{{ key.name }}</span>
<Badge
v-if="key.circuit_breaker_open"
variant="destructive"
class="text-[10px] h-5 px-1.5 shrink-0"
>
熔断
</Badge>
<Badge
v-else-if="!key.is_active"
variant="secondary"
class="text-[10px] h-5 px-1.5 shrink-0"
>
停用
</Badge>
<!-- 能力标签紧跟名称 -->
<template v-if="key.capabilities?.length">
<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>
<!-- Key 信息两行 -->
<div class="flex-1 min-w-0">
<!-- 第一行Key 名称 + Key 级别状态 -->
<div class="flex items-center gap-1.5">
<span
class="text-sm font-medium truncate"
:class="!(key.is_active && key.provider_active) ? 'text-muted-foreground' : ''"
>{{ key.name }}</span>
<Badge
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 && key.provider_active"
variant="secondary"
class="text-[9px] h-4 px-1 shrink-0"
>
停用
</Badge>
</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
v-if="key.health_score != null"
class="text-xs text-right"
class="text-[11px] font-medium tabular-nums text-foreground"
>
<div
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>
{{ ((key.health_score || 0) * 100).toFixed(0) }}%
</div>
<div
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 class="text-sm font-medium tabular-nums text-primary min-w-[40px] text-right">
<div class="text-[10px] text-muted-foreground tabular-nums">
{{ key.rate_multipliers?.[format] ?? 1 }}x
</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>
@@ -432,7 +426,7 @@
<script setup lang="ts">
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 Button from '@/components/ui/button.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 { adminApi } from '@/api/admin'
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 {
id: string
@@ -453,6 +447,7 @@ interface KeyWithMeta {
priority: number // 用于编辑的优先级
rate_multipliers: Record<string, number> | null
is_active: boolean
provider_active: boolean
circuit_breaker_open: boolean
provider_name: string
endpoint_base_url: string
@@ -567,6 +562,16 @@ const availableFormats = computed(() => {
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启用的在前停用的在后各自按优先级排序
function sortProvidersByActiveAndPriority(providers: ProviderWithEndpointsSummary[]) {
return [...providers].sort((a, b) => {
@@ -640,8 +645,8 @@ async function loadKeysByFormat() {
// 使用格式特定优先级,如果没有则分配默认值
priority: key.format_priority ?? nextPriority++
}))
// 按优先级排序
data[format].sort((a, b) => a.priority - b.priority)
// 按优先级排序:活跃的在前,停用的(Key或Provider)在后,各自按优先级排序
data[format] = sortKeysByActiveAndPriority(data[format])
}
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 优先级编辑
function startEditKeyPriority(format: string, key: KeyWithMeta) {
editingKeyPriority.value[format] = key.id
@@ -673,7 +701,7 @@ function finishEditKeyPriority(format: string, key: KeyWithMeta, event: FocusEve
// 每个格式独立管理优先级,只更新当前格式
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
@@ -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
dragOverKey.value[format] = null
}
@@ -883,20 +911,8 @@ async function save() {
const newMode = activeMainTab.value === 'key' ? 'global_key' : 'provider'
// 保存优先级模式和调度模式
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(缓存亲和模式)'
)
])
// 第一步:先保存所有 Provider 和 Key 的优先级数据
// 确保优先级数据全部到位后,再切换调度模式,避免瞬态不一致
const providerUpdates = sortedProviders.value.map((provider) =>
updateProvider(provider.id, { provider_priority: provider.provider_priority })
)
@@ -919,6 +935,19 @@ async function save() {
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()
success('优先级已保存')

View File

@@ -712,13 +712,10 @@ class AdminGetKeysGroupedByFormatAdapter(AdminApiAdapter):
db = context.db
# Key 属于 Provider按 key.api_formats 分组展示
# 包含所有 Key含停用的 Key 和停用的 Provider前端可显示停用标签和快捷开关
keys = (
db.query(ProviderAPIKey, Provider)
.join(Provider, ProviderAPIKey.provider_id == Provider.id)
.filter(
ProviderAPIKey.is_active.is_(True),
Provider.is_active.is_(True),
)
.order_by(
ProviderAPIKey.internal_priority.asc(),
)
@@ -789,6 +786,7 @@ class AdminGetKeysGroupedByFormatAdapter(AdminApiAdapter):
"global_priority_by_format": key.global_priority_by_format,
"rate_multipliers": key.rate_multipliers,
"is_active": key.is_active,
"provider_active": provider.is_active,
"provider_name": provider.name,
"api_formats": api_formats,
"capabilities": caps_list,

View File

@@ -634,6 +634,38 @@ class AdminSetSystemConfigAdapter(AdminApiAdapter):
except Exception as 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

View File

@@ -29,6 +29,13 @@ class RequestRecordLevel(str, Enum):
# 进程内缓存 TTL- 系统配置变化不频繁,使用较长的 TTL
_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)}
_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:
"""设置进程内缓存"""
_config_cache[key] = (value, time.time() + _CONFIG_CACHE_TTL)
"""设置进程内缓存
调度相关配置使用更短的 TTL5秒确保多 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: