diff --git a/frontend/src/components/ui/button.vue b/frontend/src/components/ui/button.vue index 0f50bda..64247ab 100644 --- a/frontend/src/components/ui/button.vue +++ b/frontend/src/components/ui/button.vue @@ -34,11 +34,10 @@ const buttonClass = computed(() => { 'inline-flex items-center justify-center rounded-xl text-sm font-semibold transition-all duration-200 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 active:scale-[0.98]' const variantClasses = { - default: - 'bg-primary text-white shadow-[0_20px_35px_rgba(204,120,92,0.35)] hover:bg-primary/90 hover:shadow-[0_25px_45px_rgba(204,120,92,0.45)]', - destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/85 shadow-sm', + default: 'bg-primary text-white hover:bg-primary/90', + destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/85', outline: - 'border border-border/60 bg-card/60 text-foreground hover:border-primary/60 hover:text-primary hover:bg-primary/10 shadow-sm backdrop-blur transition-all', + 'border border-border/60 bg-card/60 text-foreground hover:border-primary/60 hover:text-primary hover:bg-primary/10 backdrop-blur transition-all', secondary: 'bg-secondary text-secondary-foreground shadow-inner hover:bg-secondary/80', ghost: 'hover:bg-accent hover:text-accent-foreground', diff --git a/frontend/src/components/ui/dialog/Dialog.vue b/frontend/src/components/ui/dialog/Dialog.vue index 5bdae7e..cb8fc89 100644 --- a/frontend/src/components/ui/dialog/Dialog.vue +++ b/frontend/src/components/ui/dialog/Dialog.vue @@ -2,7 +2,7 @@
@@ -16,7 +16,7 @@ >
@@ -34,7 +34,7 @@ >
(), { const contentClass = computed(() => cn( - 'z-[100] max-h-96 min-w-[8rem] overflow-hidden rounded-2xl border border-border bg-card text-foreground shadow-2xl backdrop-blur-xl pointer-events-auto', + 'z-[200] max-h-96 min-w-[8rem] overflow-hidden rounded-2xl border border-border bg-card text-foreground shadow-2xl backdrop-blur-xl pointer-events-auto', 'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95', 'data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2', props.class diff --git a/frontend/src/features/providers/components/PriorityManagementDialog.vue b/frontend/src/features/providers/components/PriorityManagementDialog.vue index 9b00f47..5815f7b 100644 --- a/frontend/src/features/providers/components/PriorityManagementDialog.vue +++ b/frontend/src/features/providers/components/PriorityManagementDialog.vue @@ -312,8 +312,41 @@ @@ -794,11 +781,6 @@ import SelectContent from '@/components/ui/select-content.vue' import SelectItem from '@/components/ui/select-item.vue' import { Dialog, - DialogContent, - DialogHeader, - DialogTitle, - DialogDescription, - DialogFooter } from '@/components/ui' import { PageHeader, PageContainer, CardSection } from '@/components/layout' import { useToast } from '@/composables/useToast' @@ -843,6 +825,7 @@ const configFileInput = ref(null) const importPreview = ref(null) const importResult = ref(null) const mergeMode = ref<'skip' | 'overwrite' | 'error'>('skip') +const mergeModeSelectOpen = ref(false) // 用户数据导出/导入相关 const exportUsersLoading = ref(false) @@ -853,6 +836,7 @@ const usersFileInput = ref(null) const importUsersPreview = ref(null) const importUsersResult = ref(null) const usersMergeMode = ref<'skip' | 'overwrite' | 'error'>('skip') +const usersMergeModeSelectOpen = ref(false) const systemConfig = ref({ // 基础配置 @@ -1136,6 +1120,7 @@ async function confirmImport() { }) importResult.value = result importDialogOpen.value = false + mergeModeSelectOpen.value = false importResultDialogOpen.value = true success('配置导入成功') } catch (err: any) { @@ -1224,6 +1209,7 @@ async function confirmImportUsers() { }) importUsersResult.value = result importUsersDialogOpen.value = false + usersMergeModeSelectOpen.value = false importUsersResultDialogOpen.value = true success('用户数据导入成功') } catch (err: any) { diff --git a/frontend/src/views/shared/Dashboard.vue b/frontend/src/views/shared/Dashboard.vue index 94a1a3a..fb9d898 100644 --- a/frontend/src/views/shared/Dashboard.vue +++ b/frontend/src/views/shared/Dashboard.vue @@ -103,7 +103,7 @@
- +

平均响应 @@ -114,7 +114,7 @@

- +

错误率 @@ -128,7 +128,7 @@

- +

转移次数 @@ -142,7 +142,7 @@ v-if="costStats" class="relative p-3 sm:p-4 border-manilla/40" > - +

实际成本 @@ -180,7 +180,7 @@

- +

缓存命中率 @@ -191,7 +191,7 @@

- +

缓存读取 @@ -202,7 +202,7 @@

- +

缓存创建 @@ -216,7 +216,7 @@ v-if="tokenBreakdown" class="relative p-3 sm:p-4 border-manilla/40" > - +

总Token @@ -254,16 +254,16 @@

- +
- +

暂无公告

@@ -793,9 +793,8 @@ const statCardGlows = [ 'bg-kraft/30' ] -const getStatIconColor = (index: number): string => { - const colors = ['text-book-cloth', 'text-kraft', 'text-book-cloth', 'text-kraft'] - return colors[index % colors.length] +const getStatIconColor = (_index: number): string => { + return 'text-muted-foreground' } // 统计数据 diff --git a/src/api/admin/monitoring/cache.py b/src/api/admin/monitoring/cache.py index 2173e44..8f61714 100644 --- a/src/api/admin/monitoring/cache.py +++ b/src/api/admin/monitoring/cache.py @@ -21,7 +21,8 @@ from src.core.logger import logger from src.database import get_db from src.models.database import ApiKey, User from src.services.cache.affinity_manager import get_affinity_manager -from src.services.cache.aware_scheduler import get_cache_aware_scheduler +from src.services.cache.aware_scheduler import CacheAwareScheduler, get_cache_aware_scheduler +from src.services.system.config import SystemConfigService router = APIRouter(prefix="/api/admin/monitoring/cache", tags=["Admin - Monitoring: Cache"]) pipeline = ApiRequestPipeline() @@ -250,7 +251,22 @@ class AdminCacheStatsAdapter(AdminApiAdapter): async def handle(self, context: ApiRequestContext) -> Dict[str, Any]: # type: ignore[override] try: redis_client = get_redis_client_sync() - scheduler = await get_cache_aware_scheduler(redis_client) + # 读取系统配置,确保监控接口与编排器使用一致的模式 + priority_mode = SystemConfigService.get_config( + context.db, + "provider_priority_mode", + CacheAwareScheduler.PRIORITY_MODE_PROVIDER, + ) + scheduling_mode = SystemConfigService.get_config( + context.db, + "scheduling_mode", + CacheAwareScheduler.SCHEDULING_MODE_CACHE_AFFINITY, + ) + scheduler = await get_cache_aware_scheduler( + redis_client, + priority_mode=priority_mode, + scheduling_mode=scheduling_mode, + ) stats = await scheduler.get_stats() logger.info("缓存统计信息查询成功") context.add_audit_metadata( @@ -270,7 +286,22 @@ class AdminCacheMetricsAdapter(AdminApiAdapter): async def handle(self, context: ApiRequestContext) -> PlainTextResponse: try: redis_client = get_redis_client_sync() - scheduler = await get_cache_aware_scheduler(redis_client) + # 读取系统配置,确保监控接口与编排器使用一致的模式 + priority_mode = SystemConfigService.get_config( + context.db, + "provider_priority_mode", + CacheAwareScheduler.PRIORITY_MODE_PROVIDER, + ) + scheduling_mode = SystemConfigService.get_config( + context.db, + "scheduling_mode", + CacheAwareScheduler.SCHEDULING_MODE_CACHE_AFFINITY, + ) + scheduler = await get_cache_aware_scheduler( + redis_client, + priority_mode=priority_mode, + scheduling_mode=scheduling_mode, + ) stats = await scheduler.get_stats() payload = self._format_prometheus(stats) context.add_audit_metadata( diff --git a/src/api/admin/system.py b/src/api/admin/system.py index c2be36a..0ae2ae2 100644 --- a/src/api/admin/system.py +++ b/src/api/admin/system.py @@ -852,7 +852,7 @@ class AdminImportConfigAdapter(AdminApiAdapter): from src.services.cache.invalidation import get_cache_invalidation_service cache_service = get_cache_invalidation_service() - cache_service.invalidate_all() + cache_service.clear_all_caches() return { "message": "配置导入成功", diff --git a/src/services/cache/aware_scheduler.py b/src/services/cache/aware_scheduler.py index f4b3a0b..9f712cd 100644 --- a/src/services/cache/aware_scheduler.py +++ b/src/services/cache/aware_scheduler.py @@ -121,8 +121,17 @@ class CacheAwareScheduler: PRIORITY_MODE_PROVIDER, PRIORITY_MODE_GLOBAL_KEY, } + # 调度模式常量 + SCHEDULING_MODE_FIXED_ORDER = "fixed_order" # 固定顺序模式 + SCHEDULING_MODE_CACHE_AFFINITY = "cache_affinity" # 缓存亲和模式 + ALLOWED_SCHEDULING_MODES = { + SCHEDULING_MODE_FIXED_ORDER, + SCHEDULING_MODE_CACHE_AFFINITY, + } - def __init__(self, redis_client=None, priority_mode: Optional[str] = None): + def __init__( + self, redis_client=None, priority_mode: Optional[str] = None, scheduling_mode: Optional[str] = None + ): """ 初始化调度器 @@ -132,12 +141,16 @@ class CacheAwareScheduler: Args: redis_client: Redis客户端(可选) priority_mode: 候选排序策略(provider | global_key) + scheduling_mode: 调度模式(fixed_order | cache_affinity) """ self.redis = redis_client self.priority_mode = self._normalize_priority_mode( priority_mode or self.PRIORITY_MODE_PROVIDER ) - logger.debug(f"[CacheAwareScheduler] 初始化优先级模式: {self.priority_mode}") + self.scheduling_mode = self._normalize_scheduling_mode( + scheduling_mode or self.SCHEDULING_MODE_CACHE_AFFINITY + ) + logger.debug(f"[CacheAwareScheduler] 初始化优先级模式: {self.priority_mode}, 调度模式: {self.scheduling_mode}") # 初始化子组件(将在第一次使用时异步初始化) self._affinity_manager: Optional[CacheAffinityManager] = None @@ -673,14 +686,19 @@ class CacheAwareScheduler: f"(api_format={target_format.value}, model={model_name})" ) - # 4. 应用缓存亲和性排序(使用 global_model_id 作为模型标识) - if affinity_key and candidates: - candidates = await self._apply_cache_affinity( - candidates=candidates, - affinity_key=affinity_key, - api_format=target_format, - global_model_id=global_model_id, - ) + # 4. 应用缓存亲和性排序(仅在缓存亲和模式下启用) + if self.scheduling_mode == self.SCHEDULING_MODE_CACHE_AFFINITY: + if affinity_key and candidates: + candidates = await self._apply_cache_affinity( + candidates=candidates, + affinity_key=affinity_key, + api_format=target_format, + global_model_id=global_model_id, + ) + else: + # 固定顺序模式:标记所有候选为非缓存 + for candidate in candidates: + candidate.is_cached = False return candidates, global_model_id @@ -1060,6 +1078,22 @@ class CacheAwareScheduler: self.priority_mode = normalized logger.debug(f"[CacheAwareScheduler] 切换优先级模式为: {self.priority_mode}") + def _normalize_scheduling_mode(self, mode: Optional[str]) -> str: + normalized = (mode or "").strip().lower() + if normalized not in self.ALLOWED_SCHEDULING_MODES: + if normalized: + logger.warning(f"[CacheAwareScheduler] 无效的调度模式 '{mode}',回退为 cache_affinity") + return self.SCHEDULING_MODE_CACHE_AFFINITY + return normalized + + def set_scheduling_mode(self, mode: Optional[str]) -> None: + """运行时更新调度模式""" + normalized = self._normalize_scheduling_mode(mode) + if normalized == self.scheduling_mode: + return + self.scheduling_mode = normalized + logger.debug(f"[CacheAwareScheduler] 切换调度模式为: {self.scheduling_mode}") + def _apply_priority_mode_sort( self, candidates: List[ProviderCandidate], affinity_key: Optional[str] = None ) -> List[ProviderCandidate]: @@ -1307,6 +1341,7 @@ _scheduler: Optional[CacheAwareScheduler] = None async def get_cache_aware_scheduler( redis_client=None, priority_mode: Optional[str] = None, + scheduling_mode: Optional[str] = None, ) -> CacheAwareScheduler: """ 获取全局CacheAwareScheduler实例 @@ -1317,6 +1352,7 @@ async def get_cache_aware_scheduler( Args: redis_client: Redis客户端(可选) priority_mode: 外部覆盖的优先级模式(provider | global_key) + scheduling_mode: 外部覆盖的调度模式(fixed_order | cache_affinity) Returns: CacheAwareScheduler实例 @@ -1324,8 +1360,13 @@ async def get_cache_aware_scheduler( global _scheduler if _scheduler is None: - _scheduler = CacheAwareScheduler(redis_client, priority_mode=priority_mode) - elif priority_mode: - _scheduler.set_priority_mode(priority_mode) + _scheduler = CacheAwareScheduler( + redis_client, priority_mode=priority_mode, scheduling_mode=scheduling_mode + ) + else: + if priority_mode: + _scheduler.set_priority_mode(priority_mode) + if scheduling_mode: + _scheduler.set_scheduling_mode(scheduling_mode) return _scheduler diff --git a/src/services/orchestration/fallback_orchestrator.py b/src/services/orchestration/fallback_orchestrator.py index 2577acc..1574a2c 100644 --- a/src/services/orchestration/fallback_orchestrator.py +++ b/src/services/orchestration/fallback_orchestrator.py @@ -102,9 +102,15 @@ class FallbackOrchestrator: "provider_priority_mode", CacheAwareScheduler.PRIORITY_MODE_PROVIDER, ) + scheduling_mode = SystemConfigService.get_config( + self.db, + "scheduling_mode", + CacheAwareScheduler.SCHEDULING_MODE_CACHE_AFFINITY, + ) self.cache_scheduler = await get_cache_aware_scheduler( self.redis, priority_mode=priority_mode, + scheduling_mode=scheduling_mode, ) else: # 确保运行时配置变更能生效 @@ -113,7 +119,13 @@ class FallbackOrchestrator: "provider_priority_mode", CacheAwareScheduler.PRIORITY_MODE_PROVIDER, ) + scheduling_mode = SystemConfigService.get_config( + self.db, + "scheduling_mode", + CacheAwareScheduler.SCHEDULING_MODE_CACHE_AFFINITY, + ) self.cache_scheduler.set_priority_mode(priority_mode) + self.cache_scheduler.set_scheduling_mode(scheduling_mode) # 确保 cache_scheduler 内部组件也已初始化 await self.cache_scheduler._ensure_initialized() diff --git a/src/services/system/config.py b/src/services/system/config.py index 220ba7c..0c26651 100644 --- a/src/services/system/config.py +++ b/src/services/system/config.py @@ -71,6 +71,10 @@ class SystemConfigService: "value": "provider", "description": "优先级策略:provider(提供商优先模式) 或 global_key(全局Key优先模式)", }, + "scheduling_mode": { + "value": "cache_affinity", + "description": "调度模式:fixed_order(固定顺序模式,严格按优先级顺序) 或 cache_affinity(缓存亲和模式,优先使用已缓存的Provider)", + }, "auto_delete_expired_keys": { "value": False, "description": "是否自动删除过期的API Key(True=物理删除,False=仅禁用),仅管理员可配置",