mirror of
https://github.com/fawney19/Aether.git
synced 2026-01-02 15:52:26 +08:00
refactor: add scheduling mode support and optimize system settings UI
- Add fixed_order and cache_affinity scheduling modes to CacheAwareScheduler - Only apply cache affinity in cache_affinity mode; use fixed order otherwise - Simplify Dialog components with title/description props - Remove unnecessary button shadows in SystemSettings - Optimize import dialog UI structure - Update ModelAliasesTab shadow styling - Fix fallback orchestrator type hints - Add scheduling_mode configuration in system config
This commit is contained in:
@@ -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]'
|
'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 = {
|
const variantClasses = {
|
||||||
default:
|
default: 'bg-primary text-white hover:bg-primary/90',
|
||||||
'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',
|
||||||
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/85 shadow-sm',
|
|
||||||
outline:
|
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:
|
secondary:
|
||||||
'bg-secondary text-secondary-foreground shadow-inner hover:bg-secondary/80',
|
'bg-secondary text-secondary-foreground shadow-inner hover:bg-secondary/80',
|
||||||
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<Teleport to="body">
|
<Teleport to="body">
|
||||||
<div
|
<div
|
||||||
v-if="isOpen"
|
v-if="isOpen"
|
||||||
class="fixed inset-0 overflow-y-auto"
|
class="fixed inset-0 overflow-y-auto pointer-events-none"
|
||||||
:style="{ zIndex: containerZIndex }"
|
:style="{ zIndex: containerZIndex }"
|
||||||
>
|
>
|
||||||
<!-- 背景遮罩 -->
|
<!-- 背景遮罩 -->
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="isOpen"
|
v-if="isOpen"
|
||||||
class="fixed inset-0 bg-black/40 backdrop-blur-sm transition-opacity"
|
class="fixed inset-0 bg-black/40 backdrop-blur-sm transition-opacity pointer-events-auto"
|
||||||
:style="{ zIndex: backdropZIndex }"
|
:style="{ zIndex: backdropZIndex }"
|
||||||
@click="handleClose"
|
@click="handleClose"
|
||||||
/>
|
/>
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="isOpen"
|
v-if="isOpen"
|
||||||
class="relative transform rounded-lg bg-background text-left shadow-2xl transition-all sm:my-8 sm:w-full border border-border"
|
class="relative transform rounded-lg bg-background text-left shadow-2xl transition-all sm:my-8 sm:w-full border border-border pointer-events-auto"
|
||||||
:style="{ zIndex: contentZIndex }"
|
:style="{ zIndex: contentZIndex }"
|
||||||
:class="maxWidthClass"
|
:class="maxWidthClass"
|
||||||
@click.stop
|
@click.stop
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|||||||
|
|
||||||
const contentClass = computed(() =>
|
const contentClass = computed(() =>
|
||||||
cn(
|
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-[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',
|
'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
|
props.class
|
||||||
|
|||||||
@@ -312,8 +312,41 @@
|
|||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<div class="flex items-center justify-between w-full">
|
<div class="flex items-center justify-between w-full">
|
||||||
<div class="text-xs text-muted-foreground">
|
<div class="flex items-center gap-4">
|
||||||
当前模式: <span class="font-medium">{{ activeMainTab === 'provider' ? '提供商优先' : 'Key 优先' }}</span>
|
<div class="text-xs text-muted-foreground">
|
||||||
|
当前模式: <span class="font-medium">{{ activeMainTab === 'provider' ? '提供商优先' : 'Key 优先' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2 pl-4 border-l border-border">
|
||||||
|
<span class="text-xs text-muted-foreground">调度:</span>
|
||||||
|
<div class="flex gap-0.5 p-0.5 bg-muted/40 rounded-md">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="px-2 py-1 text-xs font-medium rounded transition-all"
|
||||||
|
:class="[
|
||||||
|
schedulingMode === 'fixed_order'
|
||||||
|
? 'bg-primary text-primary-foreground shadow-sm'
|
||||||
|
: 'text-muted-foreground hover:text-foreground hover:bg-muted/50'
|
||||||
|
]"
|
||||||
|
title="严格按优先级顺序,不考虑缓存"
|
||||||
|
@click="schedulingMode = 'fixed_order'"
|
||||||
|
>
|
||||||
|
固定顺序
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="px-2 py-1 text-xs font-medium rounded transition-all"
|
||||||
|
:class="[
|
||||||
|
schedulingMode === 'cache_affinity'
|
||||||
|
? 'bg-primary text-primary-foreground shadow-sm'
|
||||||
|
: 'text-muted-foreground hover:text-foreground hover:bg-muted/50'
|
||||||
|
]"
|
||||||
|
title="优先使用已缓存的Provider,利用Prompt Cache"
|
||||||
|
@click="schedulingMode = 'cache_affinity'"
|
||||||
|
>
|
||||||
|
缓存亲和
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<Button
|
<Button
|
||||||
@@ -410,6 +443,9 @@ const saving = ref(false)
|
|||||||
// Key 优先级编辑状态
|
// Key 优先级编辑状态
|
||||||
const editingKeyPriority = ref<Record<string, string | null>>({}) // format -> keyId
|
const editingKeyPriority = ref<Record<string, string | null>>({}) // format -> keyId
|
||||||
|
|
||||||
|
// 调度模式状态
|
||||||
|
const schedulingMode = ref<'fixed_order' | 'cache_affinity'>('cache_affinity')
|
||||||
|
|
||||||
// 可用的 API 格式
|
// 可用的 API 格式
|
||||||
const availableFormats = computed(() => {
|
const availableFormats = computed(() => {
|
||||||
return Object.keys(keysByFormat.value).sort()
|
return Object.keys(keysByFormat.value).sort()
|
||||||
@@ -433,11 +469,18 @@ watch(internalOpen, async (open) => {
|
|||||||
// 加载当前的优先级模式配置
|
// 加载当前的优先级模式配置
|
||||||
async function loadCurrentPriorityMode() {
|
async function loadCurrentPriorityMode() {
|
||||||
try {
|
try {
|
||||||
const response = await adminApi.getSystemConfig('provider_priority_mode')
|
const [priorityResponse, schedulingResponse] = await Promise.all([
|
||||||
const currentMode = response.value || 'provider'
|
adminApi.getSystemConfig('provider_priority_mode'),
|
||||||
|
adminApi.getSystemConfig('scheduling_mode')
|
||||||
|
])
|
||||||
|
const currentMode = priorityResponse.value || 'provider'
|
||||||
activeMainTab.value = currentMode === 'global_key' ? 'key' : 'provider'
|
activeMainTab.value = currentMode === 'global_key' ? 'key' : 'provider'
|
||||||
|
|
||||||
|
const currentSchedulingMode = schedulingResponse.value || 'cache_affinity'
|
||||||
|
schedulingMode.value = currentSchedulingMode === 'fixed_order' ? 'fixed_order' : 'cache_affinity'
|
||||||
} catch {
|
} catch {
|
||||||
activeMainTab.value = 'provider'
|
activeMainTab.value = 'provider'
|
||||||
|
schedulingMode.value = 'cache_affinity'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -611,11 +654,19 @@ async function save() {
|
|||||||
|
|
||||||
const newMode = activeMainTab.value === 'key' ? 'global_key' : 'provider'
|
const newMode = activeMainTab.value === 'key' ? 'global_key' : 'provider'
|
||||||
|
|
||||||
await adminApi.updateSystemConfig(
|
// 保存优先级模式和调度模式
|
||||||
'provider_priority_mode',
|
await Promise.all([
|
||||||
newMode,
|
adminApi.updateSystemConfig(
|
||||||
'Provider/Key 优先级策略:provider(提供商优先模式) 或 global_key(全局Key优先模式)'
|
'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, index) =>
|
const providerUpdates = sortedProviders.value.map((provider, index) =>
|
||||||
updateProvider(provider.id, { provider_priority: index + 1 })
|
updateProvider(provider.id, { provider_priority: index + 1 })
|
||||||
|
|||||||
@@ -286,7 +286,9 @@
|
|||||||
@click="addUpstreamModel(model.id)"
|
@click="addUpstreamModel(model.id)"
|
||||||
>
|
>
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
<div class="font-mono text-xs truncate">{{ model.id }}</div>
|
<div class="font-mono text-xs truncate">
|
||||||
|
{{ model.id }}
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="model.owned_by"
|
v-if="model.owned_by"
|
||||||
class="text-xs text-muted-foreground truncate"
|
class="text-xs text-muted-foreground truncate"
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
<template #actions>
|
<template #actions>
|
||||||
<Button
|
<Button
|
||||||
:disabled="loading"
|
:disabled="loading"
|
||||||
|
class="shadow-none hover:shadow-none"
|
||||||
@click="saveSystemConfig"
|
@click="saveSystemConfig"
|
||||||
>
|
>
|
||||||
{{ loading ? '保存中...' : '保存所有配置' }}
|
{{ loading ? '保存中...' : '保存所有配置' }}
|
||||||
@@ -465,317 +466,303 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 导入配置对话框 -->
|
<!-- 导入配置对话框 -->
|
||||||
<Dialog v-model:open="importDialogOpen">
|
<Dialog
|
||||||
<DialogContent class="max-w-lg">
|
v-model:open="importDialogOpen"
|
||||||
<DialogHeader>
|
title="导入配置"
|
||||||
<DialogTitle>导入配置</DialogTitle>
|
description="选择冲突处理模式并确认导入"
|
||||||
<DialogDescription>
|
>
|
||||||
选择冲突处理模式并确认导入
|
<div class="space-y-4">
|
||||||
</DialogDescription>
|
<div
|
||||||
</DialogHeader>
|
v-if="importPreview"
|
||||||
|
class="p-3 bg-muted rounded-lg text-sm"
|
||||||
<div class="space-y-4 py-4">
|
>
|
||||||
<div
|
<p class="font-medium mb-2">
|
||||||
v-if="importPreview"
|
配置预览
|
||||||
class="p-3 bg-muted rounded-lg text-sm"
|
</p>
|
||||||
>
|
<ul class="space-y-1 text-muted-foreground">
|
||||||
<p class="font-medium mb-2">
|
<li>全局模型: {{ importPreview.global_models?.length || 0 }} 个</li>
|
||||||
配置预览
|
<li>提供商: {{ importPreview.providers?.length || 0 }} 个</li>
|
||||||
</p>
|
<li>
|
||||||
<ul class="space-y-1 text-muted-foreground">
|
端点: {{ importPreview.providers?.reduce((sum: number, p: any) => sum + (p.endpoints?.length || 0), 0) }} 个
|
||||||
<li>全局模型: {{ importPreview.global_models?.length || 0 }} 个</li>
|
</li>
|
||||||
<li>提供商: {{ importPreview.providers?.length || 0 }} 个</li>
|
<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) }} 个
|
||||||
端点: {{ importPreview.providers?.reduce((sum: number, p: any) => sum + (p.endpoints?.length || 0), 0) }} 个
|
</li>
|
||||||
</li>
|
</ul>
|
||||||
<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) }} 个
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<Label class="block text-sm font-medium mb-2">冲突处理模式</Label>
|
|
||||||
<Select v-model="mergeMode">
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="skip">
|
|
||||||
跳过 - 保留现有配置
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value="overwrite">
|
|
||||||
覆盖 - 用导入配置替换
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value="error">
|
|
||||||
报错 - 遇到冲突时中止
|
|
||||||
</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<p class="mt-1 text-xs text-muted-foreground">
|
|
||||||
<template v-if="mergeMode === 'skip'">
|
|
||||||
已存在的配置将被保留,仅导入新配置
|
|
||||||
</template>
|
|
||||||
<template v-else-if="mergeMode === 'overwrite'">
|
|
||||||
已存在的配置将被导入的配置覆盖
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
如果发现任何冲突,导入将中止并回滚
|
|
||||||
</template>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="p-3 bg-yellow-500/10 border border-yellow-500/20 rounded-lg">
|
|
||||||
<p class="text-sm text-yellow-600 dark:text-yellow-400">
|
|
||||||
注意:相同的 API Keys 会自动跳过,不会创建重复记录。
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter>
|
<div>
|
||||||
<Button
|
<Label class="block text-sm font-medium mb-2">冲突处理模式</Label>
|
||||||
variant="outline"
|
<Select
|
||||||
@click="importDialogOpen = false"
|
v-model="mergeMode"
|
||||||
|
v-model:open="mergeModeSelectOpen"
|
||||||
>
|
>
|
||||||
取消
|
<SelectTrigger>
|
||||||
</Button>
|
<SelectValue />
|
||||||
<Button
|
</SelectTrigger>
|
||||||
:disabled="importLoading"
|
<SelectContent>
|
||||||
@click="confirmImport"
|
<SelectItem value="skip">
|
||||||
>
|
跳过 - 保留现有配置
|
||||||
{{ importLoading ? '导入中...' : '确认导入' }}
|
</SelectItem>
|
||||||
</Button>
|
<SelectItem value="overwrite">
|
||||||
</DialogFooter>
|
覆盖 - 用导入配置替换
|
||||||
</DialogContent>
|
</SelectItem>
|
||||||
|
<SelectItem value="error">
|
||||||
|
报错 - 遇到冲突时中止
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<p class="mt-1 text-xs text-muted-foreground">
|
||||||
|
<template v-if="mergeMode === 'skip'">
|
||||||
|
已存在的配置将被保留,仅导入新配置
|
||||||
|
</template>
|
||||||
|
<template v-else-if="mergeMode === 'overwrite'">
|
||||||
|
已存在的配置将被导入的配置覆盖
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
如果发现任何冲突,导入将中止并回滚
|
||||||
|
</template>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="text-xs text-muted-foreground">
|
||||||
|
注意:相同的 API Keys 会自动跳过,不会创建重复记录。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
@click="importDialogOpen = false; mergeModeSelectOpen = false"
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
:disabled="importLoading"
|
||||||
|
@click="confirmImport"
|
||||||
|
>
|
||||||
|
{{ importLoading ? '导入中...' : '确认导入' }}
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<!-- 导入结果对话框 -->
|
<!-- 导入结果对话框 -->
|
||||||
<Dialog v-model:open="importResultDialogOpen">
|
<Dialog
|
||||||
<DialogContent class="max-w-lg">
|
v-model:open="importResultDialogOpen"
|
||||||
<DialogHeader>
|
title="导入完成"
|
||||||
<DialogTitle>导入完成</DialogTitle>
|
>
|
||||||
</DialogHeader>
|
<div
|
||||||
|
v-if="importResult"
|
||||||
<div
|
class="space-y-4"
|
||||||
v-if="importResult"
|
>
|
||||||
class="space-y-4 py-4"
|
<div class="grid grid-cols-2 gap-4 text-sm">
|
||||||
>
|
<div class="p-3 bg-muted rounded-lg">
|
||||||
<div class="grid grid-cols-2 gap-4 text-sm">
|
<p class="font-medium">
|
||||||
<div class="p-3 bg-muted rounded-lg">
|
全局模型
|
||||||
<p class="font-medium">
|
</p>
|
||||||
全局模型
|
<p class="text-muted-foreground">
|
||||||
</p>
|
创建: {{ importResult.stats.global_models.created }},
|
||||||
<p class="text-muted-foreground">
|
更新: {{ importResult.stats.global_models.updated }},
|
||||||
创建: {{ importResult.stats.global_models.created }},
|
跳过: {{ importResult.stats.global_models.skipped }}
|
||||||
更新: {{ importResult.stats.global_models.updated }},
|
</p>
|
||||||
跳过: {{ importResult.stats.global_models.skipped }}
|
</div>
|
||||||
</p>
|
<div class="p-3 bg-muted rounded-lg">
|
||||||
</div>
|
<p class="font-medium">
|
||||||
<div class="p-3 bg-muted rounded-lg">
|
提供商
|
||||||
<p class="font-medium">
|
</p>
|
||||||
提供商
|
<p class="text-muted-foreground">
|
||||||
</p>
|
创建: {{ importResult.stats.providers.created }},
|
||||||
<p class="text-muted-foreground">
|
更新: {{ importResult.stats.providers.updated }},
|
||||||
创建: {{ importResult.stats.providers.created }},
|
跳过: {{ importResult.stats.providers.skipped }}
|
||||||
更新: {{ importResult.stats.providers.updated }},
|
</p>
|
||||||
跳过: {{ importResult.stats.providers.skipped }}
|
</div>
|
||||||
</p>
|
<div class="p-3 bg-muted rounded-lg">
|
||||||
</div>
|
<p class="font-medium">
|
||||||
<div class="p-3 bg-muted rounded-lg">
|
端点
|
||||||
<p class="font-medium">
|
</p>
|
||||||
端点
|
<p class="text-muted-foreground">
|
||||||
</p>
|
创建: {{ importResult.stats.endpoints.created }},
|
||||||
<p class="text-muted-foreground">
|
更新: {{ importResult.stats.endpoints.updated }},
|
||||||
创建: {{ importResult.stats.endpoints.created }},
|
跳过: {{ importResult.stats.endpoints.skipped }}
|
||||||
更新: {{ importResult.stats.endpoints.updated }},
|
</p>
|
||||||
跳过: {{ importResult.stats.endpoints.skipped }}
|
</div>
|
||||||
</p>
|
<div class="p-3 bg-muted rounded-lg">
|
||||||
</div>
|
<p class="font-medium">
|
||||||
<div class="p-3 bg-muted rounded-lg">
|
API Keys
|
||||||
<p class="font-medium">
|
</p>
|
||||||
API Keys
|
<p class="text-muted-foreground">
|
||||||
</p>
|
创建: {{ importResult.stats.keys.created }},
|
||||||
<p class="text-muted-foreground">
|
跳过: {{ importResult.stats.keys.skipped }}
|
||||||
创建: {{ importResult.stats.keys.created }},
|
</p>
|
||||||
跳过: {{ importResult.stats.keys.skipped }}
|
</div>
|
||||||
</p>
|
<div class="p-3 bg-muted rounded-lg col-span-2">
|
||||||
</div>
|
<p class="font-medium">
|
||||||
<div class="p-3 bg-muted rounded-lg col-span-2">
|
模型配置
|
||||||
<p class="font-medium">
|
</p>
|
||||||
模型配置
|
<p class="text-muted-foreground">
|
||||||
</p>
|
创建: {{ importResult.stats.models.created }},
|
||||||
<p class="text-muted-foreground">
|
更新: {{ importResult.stats.models.updated }},
|
||||||
创建: {{ importResult.stats.models.created }},
|
跳过: {{ importResult.stats.models.skipped }}
|
||||||
更新: {{ importResult.stats.models.updated }},
|
|
||||||
跳过: {{ importResult.stats.models.skipped }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
v-if="importResult.stats.errors.length > 0"
|
|
||||||
class="p-3 bg-red-500/10 border border-red-500/20 rounded-lg"
|
|
||||||
>
|
|
||||||
<p class="font-medium text-red-600 dark:text-red-400 mb-2">
|
|
||||||
警告信息
|
|
||||||
</p>
|
</p>
|
||||||
<ul class="text-sm text-red-600 dark:text-red-400 space-y-1">
|
|
||||||
<li
|
|
||||||
v-for="(err, index) in importResult.stats.errors"
|
|
||||||
:key="index"
|
|
||||||
>
|
|
||||||
{{ err }}
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter>
|
<div
|
||||||
<Button @click="importResultDialogOpen = false">
|
v-if="importResult.stats.errors.length > 0"
|
||||||
确定
|
class="p-3 bg-destructive/10 rounded-lg"
|
||||||
</Button>
|
>
|
||||||
</DialogFooter>
|
<p class="font-medium text-destructive mb-2">
|
||||||
</DialogContent>
|
警告信息
|
||||||
|
</p>
|
||||||
|
<ul class="text-sm text-destructive space-y-1">
|
||||||
|
<li
|
||||||
|
v-for="(err, index) in importResult.stats.errors"
|
||||||
|
:key="index"
|
||||||
|
>
|
||||||
|
{{ err }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<Button @click="importResultDialogOpen = false">
|
||||||
|
确定
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<!-- 用户数据导入对话框 -->
|
<!-- 用户数据导入对话框 -->
|
||||||
<Dialog v-model:open="importUsersDialogOpen">
|
<Dialog
|
||||||
<DialogContent class="max-w-lg">
|
v-model:open="importUsersDialogOpen"
|
||||||
<DialogHeader>
|
title="导入用户数据"
|
||||||
<DialogTitle>导入用户数据</DialogTitle>
|
description="选择冲突处理模式并确认导入"
|
||||||
<DialogDescription>
|
>
|
||||||
选择冲突处理模式并确认导入
|
<div class="space-y-4">
|
||||||
</DialogDescription>
|
<div
|
||||||
</DialogHeader>
|
v-if="importUsersPreview"
|
||||||
|
class="p-3 bg-muted rounded-lg text-sm"
|
||||||
<div class="space-y-4 py-4">
|
>
|
||||||
<div
|
<p class="font-medium mb-2">
|
||||||
v-if="importUsersPreview"
|
数据预览
|
||||||
class="p-3 bg-muted rounded-lg text-sm"
|
</p>
|
||||||
>
|
<ul class="space-y-1 text-muted-foreground">
|
||||||
<p class="font-medium mb-2">
|
<li>用户: {{ importUsersPreview.users?.length || 0 }} 个</li>
|
||||||
数据预览
|
<li>
|
||||||
</p>
|
API Keys: {{ importUsersPreview.users?.reduce((sum: number, u: any) => sum + (u.api_keys?.length || 0), 0) }} 个
|
||||||
<ul class="space-y-1 text-muted-foreground">
|
</li>
|
||||||
<li>用户: {{ importUsersPreview.users?.length || 0 }} 个</li>
|
</ul>
|
||||||
<li>
|
|
||||||
API Keys: {{ importUsersPreview.users?.reduce((sum: number, u: any) => sum + (u.api_keys?.length || 0), 0) }} 个
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<Label class="block text-sm font-medium mb-2">冲突处理模式</Label>
|
|
||||||
<Select v-model="usersMergeMode">
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="skip">
|
|
||||||
跳过 - 保留现有用户
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value="overwrite">
|
|
||||||
覆盖 - 用导入数据替换
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value="error">
|
|
||||||
报错 - 遇到冲突时中止
|
|
||||||
</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<p class="mt-1 text-xs text-muted-foreground">
|
|
||||||
<template v-if="usersMergeMode === 'skip'">
|
|
||||||
已存在的用户将被保留,仅导入新用户
|
|
||||||
</template>
|
|
||||||
<template v-else-if="usersMergeMode === 'overwrite'">
|
|
||||||
已存在的用户将被导入的数据覆盖
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
如果发现任何冲突,导入将中止并回滚
|
|
||||||
</template>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="p-3 bg-yellow-500/10 border border-yellow-500/20 rounded-lg">
|
|
||||||
<p class="text-sm text-yellow-600 dark:text-yellow-400">
|
|
||||||
注意:用户 API Keys 需要目标系统使用相同的 ENCRYPTION_KEY 环境变量才能正常工作。
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter>
|
<div>
|
||||||
<Button
|
<Label class="block text-sm font-medium mb-2">冲突处理模式</Label>
|
||||||
variant="outline"
|
<Select
|
||||||
@click="importUsersDialogOpen = false"
|
v-model="usersMergeMode"
|
||||||
|
v-model:open="usersMergeModeSelectOpen"
|
||||||
>
|
>
|
||||||
取消
|
<SelectTrigger>
|
||||||
</Button>
|
<SelectValue />
|
||||||
<Button
|
</SelectTrigger>
|
||||||
:disabled="importUsersLoading"
|
<SelectContent>
|
||||||
@click="confirmImportUsers"
|
<SelectItem value="skip">
|
||||||
>
|
跳过 - 保留现有用户
|
||||||
{{ importUsersLoading ? '导入中...' : '确认导入' }}
|
</SelectItem>
|
||||||
</Button>
|
<SelectItem value="overwrite">
|
||||||
</DialogFooter>
|
覆盖 - 用导入数据替换
|
||||||
</DialogContent>
|
</SelectItem>
|
||||||
|
<SelectItem value="error">
|
||||||
|
报错 - 遇到冲突时中止
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<p class="mt-1 text-xs text-muted-foreground">
|
||||||
|
<template v-if="usersMergeMode === 'skip'">
|
||||||
|
已存在的用户将被保留,仅导入新用户
|
||||||
|
</template>
|
||||||
|
<template v-else-if="usersMergeMode === 'overwrite'">
|
||||||
|
已存在的用户将被导入的数据覆盖
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
如果发现任何冲突,导入将中止并回滚
|
||||||
|
</template>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="text-xs text-muted-foreground">
|
||||||
|
注意:用户 API Keys 需要目标系统使用相同的 ENCRYPTION_KEY 环境变量才能正常工作。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
@click="importUsersDialogOpen = false; usersMergeModeSelectOpen = false"
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
:disabled="importUsersLoading"
|
||||||
|
@click="confirmImportUsers"
|
||||||
|
>
|
||||||
|
{{ importUsersLoading ? '导入中...' : '确认导入' }}
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<!-- 用户数据导入结果对话框 -->
|
<!-- 用户数据导入结果对话框 -->
|
||||||
<Dialog v-model:open="importUsersResultDialogOpen">
|
<Dialog
|
||||||
<DialogContent class="max-w-lg">
|
v-model:open="importUsersResultDialogOpen"
|
||||||
<DialogHeader>
|
title="用户数据导入完成"
|
||||||
<DialogTitle>用户数据导入完成</DialogTitle>
|
>
|
||||||
</DialogHeader>
|
<div
|
||||||
|
v-if="importUsersResult"
|
||||||
<div
|
class="space-y-4"
|
||||||
v-if="importUsersResult"
|
>
|
||||||
class="space-y-4 py-4"
|
<div class="grid grid-cols-2 gap-4 text-sm">
|
||||||
>
|
<div class="p-3 bg-muted rounded-lg">
|
||||||
<div class="grid grid-cols-2 gap-4 text-sm">
|
<p class="font-medium">
|
||||||
<div class="p-3 bg-muted rounded-lg">
|
用户
|
||||||
<p class="font-medium">
|
</p>
|
||||||
用户
|
<p class="text-muted-foreground">
|
||||||
</p>
|
创建: {{ importUsersResult.stats.users.created }},
|
||||||
<p class="text-muted-foreground">
|
更新: {{ importUsersResult.stats.users.updated }},
|
||||||
创建: {{ importUsersResult.stats.users.created }},
|
跳过: {{ importUsersResult.stats.users.skipped }}
|
||||||
更新: {{ importUsersResult.stats.users.updated }},
|
</p>
|
||||||
跳过: {{ importUsersResult.stats.users.skipped }}
|
</div>
|
||||||
</p>
|
<div class="p-3 bg-muted rounded-lg">
|
||||||
</div>
|
<p class="font-medium">
|
||||||
<div class="p-3 bg-muted rounded-lg">
|
API Keys
|
||||||
<p class="font-medium">
|
</p>
|
||||||
API Keys
|
<p class="text-muted-foreground">
|
||||||
</p>
|
创建: {{ importUsersResult.stats.api_keys.created }},
|
||||||
<p class="text-muted-foreground">
|
跳过: {{ importUsersResult.stats.api_keys.skipped }}
|
||||||
创建: {{ importUsersResult.stats.api_keys.created }},
|
|
||||||
跳过: {{ importUsersResult.stats.api_keys.skipped }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
v-if="importUsersResult.stats.errors.length > 0"
|
|
||||||
class="p-3 bg-red-500/10 border border-red-500/20 rounded-lg"
|
|
||||||
>
|
|
||||||
<p class="font-medium text-red-600 dark:text-red-400 mb-2">
|
|
||||||
警告信息
|
|
||||||
</p>
|
</p>
|
||||||
<ul class="text-sm text-red-600 dark:text-red-400 space-y-1">
|
|
||||||
<li
|
|
||||||
v-for="(err, index) in importUsersResult.stats.errors"
|
|
||||||
:key="index"
|
|
||||||
>
|
|
||||||
{{ err }}
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter>
|
<div
|
||||||
<Button @click="importUsersResultDialogOpen = false">
|
v-if="importUsersResult.stats.errors.length > 0"
|
||||||
确定
|
class="p-3 bg-destructive/10 rounded-lg"
|
||||||
</Button>
|
>
|
||||||
</DialogFooter>
|
<p class="font-medium text-destructive mb-2">
|
||||||
</DialogContent>
|
警告信息
|
||||||
|
</p>
|
||||||
|
<ul class="text-sm text-destructive space-y-1">
|
||||||
|
<li
|
||||||
|
v-for="(err, index) in importUsersResult.stats.errors"
|
||||||
|
:key="index"
|
||||||
|
>
|
||||||
|
{{ err }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<Button @click="importUsersResultDialogOpen = false">
|
||||||
|
确定
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</PageContainer>
|
</PageContainer>
|
||||||
</template>
|
</template>
|
||||||
@@ -794,11 +781,6 @@ import SelectContent from '@/components/ui/select-content.vue'
|
|||||||
import SelectItem from '@/components/ui/select-item.vue'
|
import SelectItem from '@/components/ui/select-item.vue'
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter
|
|
||||||
} from '@/components/ui'
|
} from '@/components/ui'
|
||||||
import { PageHeader, PageContainer, CardSection } from '@/components/layout'
|
import { PageHeader, PageContainer, CardSection } from '@/components/layout'
|
||||||
import { useToast } from '@/composables/useToast'
|
import { useToast } from '@/composables/useToast'
|
||||||
@@ -843,6 +825,7 @@ const configFileInput = ref<HTMLInputElement | null>(null)
|
|||||||
const importPreview = ref<ConfigExportData | null>(null)
|
const importPreview = ref<ConfigExportData | null>(null)
|
||||||
const importResult = ref<ConfigImportResponse | null>(null)
|
const importResult = ref<ConfigImportResponse | null>(null)
|
||||||
const mergeMode = ref<'skip' | 'overwrite' | 'error'>('skip')
|
const mergeMode = ref<'skip' | 'overwrite' | 'error'>('skip')
|
||||||
|
const mergeModeSelectOpen = ref(false)
|
||||||
|
|
||||||
// 用户数据导出/导入相关
|
// 用户数据导出/导入相关
|
||||||
const exportUsersLoading = ref(false)
|
const exportUsersLoading = ref(false)
|
||||||
@@ -853,6 +836,7 @@ const usersFileInput = ref<HTMLInputElement | null>(null)
|
|||||||
const importUsersPreview = ref<UsersExportData | null>(null)
|
const importUsersPreview = ref<UsersExportData | null>(null)
|
||||||
const importUsersResult = ref<UsersImportResponse | null>(null)
|
const importUsersResult = ref<UsersImportResponse | null>(null)
|
||||||
const usersMergeMode = ref<'skip' | 'overwrite' | 'error'>('skip')
|
const usersMergeMode = ref<'skip' | 'overwrite' | 'error'>('skip')
|
||||||
|
const usersMergeModeSelectOpen = ref(false)
|
||||||
|
|
||||||
const systemConfig = ref<SystemConfig>({
|
const systemConfig = ref<SystemConfig>({
|
||||||
// 基础配置
|
// 基础配置
|
||||||
@@ -1136,6 +1120,7 @@ async function confirmImport() {
|
|||||||
})
|
})
|
||||||
importResult.value = result
|
importResult.value = result
|
||||||
importDialogOpen.value = false
|
importDialogOpen.value = false
|
||||||
|
mergeModeSelectOpen.value = false
|
||||||
importResultDialogOpen.value = true
|
importResultDialogOpen.value = true
|
||||||
success('配置导入成功')
|
success('配置导入成功')
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
@@ -1224,6 +1209,7 @@ async function confirmImportUsers() {
|
|||||||
})
|
})
|
||||||
importUsersResult.value = result
|
importUsersResult.value = result
|
||||||
importUsersDialogOpen.value = false
|
importUsersDialogOpen.value = false
|
||||||
|
usersMergeModeSelectOpen.value = false
|
||||||
importUsersResultDialogOpen.value = true
|
importUsersResultDialogOpen.value = true
|
||||||
success('用户数据导入成功')
|
success('用户数据导入成功')
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
|
|||||||
@@ -103,7 +103,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-2 gap-2 sm:gap-3 xl:grid-cols-4">
|
<div class="grid grid-cols-2 gap-2 sm:gap-3 xl:grid-cols-4">
|
||||||
<Card class="relative p-3 sm:p-4 border-book-cloth/30">
|
<Card class="relative p-3 sm:p-4 border-book-cloth/30">
|
||||||
<Clock class="absolute top-3 right-3 h-3.5 w-3.5 sm:h-4 sm:w-4 text-book-cloth" />
|
<Clock class="absolute top-3 right-3 h-3.5 w-3.5 sm:h-4 sm:w-4 text-muted-foreground" />
|
||||||
<div class="pr-6">
|
<div class="pr-6">
|
||||||
<p class="text-[9px] sm:text-[10px] font-semibold uppercase tracking-[0.2em] sm:tracking-[0.3em] text-muted-foreground">
|
<p class="text-[9px] sm:text-[10px] font-semibold uppercase tracking-[0.2em] sm:tracking-[0.3em] text-muted-foreground">
|
||||||
平均响应
|
平均响应
|
||||||
@@ -114,7 +114,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
<Card class="relative p-3 sm:p-4 border-kraft/30">
|
<Card class="relative p-3 sm:p-4 border-kraft/30">
|
||||||
<AlertTriangle class="absolute top-3 right-3 h-3.5 w-3.5 sm:h-4 sm:w-4 text-kraft" />
|
<AlertTriangle class="absolute top-3 right-3 h-3.5 w-3.5 sm:h-4 sm:w-4 text-muted-foreground" />
|
||||||
<div class="pr-6">
|
<div class="pr-6">
|
||||||
<p class="text-[9px] sm:text-[10px] font-semibold uppercase tracking-[0.2em] sm:tracking-[0.3em] text-muted-foreground">
|
<p class="text-[9px] sm:text-[10px] font-semibold uppercase tracking-[0.2em] sm:tracking-[0.3em] text-muted-foreground">
|
||||||
错误率
|
错误率
|
||||||
@@ -128,7 +128,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
<Card class="relative p-3 sm:p-4 border-book-cloth/25">
|
<Card class="relative p-3 sm:p-4 border-book-cloth/25">
|
||||||
<Shuffle class="absolute top-3 right-3 h-3.5 w-3.5 sm:h-4 sm:w-4 text-kraft" />
|
<Shuffle class="absolute top-3 right-3 h-3.5 w-3.5 sm:h-4 sm:w-4 text-muted-foreground" />
|
||||||
<div class="pr-6">
|
<div class="pr-6">
|
||||||
<p class="text-[9px] sm:text-[10px] font-semibold uppercase tracking-[0.2em] sm:tracking-[0.3em] text-muted-foreground">
|
<p class="text-[9px] sm:text-[10px] font-semibold uppercase tracking-[0.2em] sm:tracking-[0.3em] text-muted-foreground">
|
||||||
转移次数
|
转移次数
|
||||||
@@ -142,7 +142,7 @@
|
|||||||
v-if="costStats"
|
v-if="costStats"
|
||||||
class="relative p-3 sm:p-4 border-manilla/40"
|
class="relative p-3 sm:p-4 border-manilla/40"
|
||||||
>
|
>
|
||||||
<DollarSign class="absolute top-3 right-3 h-3.5 w-3.5 sm:h-4 sm:w-4 text-book-cloth" />
|
<DollarSign class="absolute top-3 right-3 h-3.5 w-3.5 sm:h-4 sm:w-4 text-muted-foreground" />
|
||||||
<div class="pr-6">
|
<div class="pr-6">
|
||||||
<p class="text-[9px] sm:text-[10px] font-semibold uppercase tracking-[0.2em] sm:tracking-[0.3em] text-muted-foreground">
|
<p class="text-[9px] sm:text-[10px] font-semibold uppercase tracking-[0.2em] sm:tracking-[0.3em] text-muted-foreground">
|
||||||
实际成本
|
实际成本
|
||||||
@@ -180,7 +180,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-2 gap-2 sm:gap-3 xl:grid-cols-4">
|
<div class="grid grid-cols-2 gap-2 sm:gap-3 xl:grid-cols-4">
|
||||||
<Card class="relative p-3 sm:p-4 border-book-cloth/30">
|
<Card class="relative p-3 sm:p-4 border-book-cloth/30">
|
||||||
<Database class="absolute top-3 right-3 h-3.5 w-3.5 sm:h-4 sm:w-4 text-book-cloth" />
|
<Database class="absolute top-3 right-3 h-3.5 w-3.5 sm:h-4 sm:w-4 text-muted-foreground" />
|
||||||
<div class="pr-6">
|
<div class="pr-6">
|
||||||
<p class="text-[9px] sm:text-[10px] font-semibold uppercase tracking-[0.2em] sm:tracking-[0.3em] text-muted-foreground">
|
<p class="text-[9px] sm:text-[10px] font-semibold uppercase tracking-[0.2em] sm:tracking-[0.3em] text-muted-foreground">
|
||||||
缓存命中率
|
缓存命中率
|
||||||
@@ -191,7 +191,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
<Card class="relative p-3 sm:p-4 border-kraft/30">
|
<Card class="relative p-3 sm:p-4 border-kraft/30">
|
||||||
<Hash class="absolute top-3 right-3 h-3.5 w-3.5 sm:h-4 sm:w-4 text-kraft" />
|
<Hash class="absolute top-3 right-3 h-3.5 w-3.5 sm:h-4 sm:w-4 text-muted-foreground" />
|
||||||
<div class="pr-6">
|
<div class="pr-6">
|
||||||
<p class="text-[9px] sm:text-[10px] font-semibold uppercase tracking-[0.2em] sm:tracking-[0.3em] text-muted-foreground">
|
<p class="text-[9px] sm:text-[10px] font-semibold uppercase tracking-[0.2em] sm:tracking-[0.3em] text-muted-foreground">
|
||||||
缓存读取
|
缓存读取
|
||||||
@@ -202,7 +202,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
<Card class="relative p-3 sm:p-4 border-book-cloth/25">
|
<Card class="relative p-3 sm:p-4 border-book-cloth/25">
|
||||||
<Database class="absolute top-3 right-3 h-3.5 w-3.5 sm:h-4 sm:w-4 text-kraft" />
|
<Database class="absolute top-3 right-3 h-3.5 w-3.5 sm:h-4 sm:w-4 text-muted-foreground" />
|
||||||
<div class="pr-6">
|
<div class="pr-6">
|
||||||
<p class="text-[9px] sm:text-[10px] font-semibold uppercase tracking-[0.2em] sm:tracking-[0.3em] text-muted-foreground">
|
<p class="text-[9px] sm:text-[10px] font-semibold uppercase tracking-[0.2em] sm:tracking-[0.3em] text-muted-foreground">
|
||||||
缓存创建
|
缓存创建
|
||||||
@@ -216,7 +216,7 @@
|
|||||||
v-if="tokenBreakdown"
|
v-if="tokenBreakdown"
|
||||||
class="relative p-3 sm:p-4 border-manilla/40"
|
class="relative p-3 sm:p-4 border-manilla/40"
|
||||||
>
|
>
|
||||||
<Hash class="absolute top-3 right-3 h-3.5 w-3.5 sm:h-4 sm:w-4 text-book-cloth" />
|
<Hash class="absolute top-3 right-3 h-3.5 w-3.5 sm:h-4 sm:w-4 text-muted-foreground" />
|
||||||
<div class="pr-6">
|
<div class="pr-6">
|
||||||
<p class="text-[9px] sm:text-[10px] font-semibold uppercase tracking-[0.2em] sm:tracking-[0.3em] text-muted-foreground">
|
<p class="text-[9px] sm:text-[10px] font-semibold uppercase tracking-[0.2em] sm:tracking-[0.3em] text-muted-foreground">
|
||||||
总Token
|
总Token
|
||||||
@@ -254,16 +254,16 @@
|
|||||||
<Card class="overflow-hidden p-4 flex flex-col flex-1 min-h-0 h-full max-h-[280px] sm:max-h-none">
|
<Card class="overflow-hidden p-4 flex flex-col flex-1 min-h-0 h-full max-h-[280px] sm:max-h-none">
|
||||||
<div
|
<div
|
||||||
v-if="loadingAnnouncements"
|
v-if="loadingAnnouncements"
|
||||||
class="py-8 text-center"
|
class="flex-1 flex items-center justify-center"
|
||||||
>
|
>
|
||||||
<Loader2 class="h-5 w-5 animate-spin mx-auto text-muted-foreground" />
|
<Loader2 class="h-5 w-5 animate-spin text-muted-foreground" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-else-if="announcements.length === 0"
|
v-else-if="announcements.length === 0"
|
||||||
class="py-8 text-center"
|
class="flex-1 flex flex-col items-center justify-center"
|
||||||
>
|
>
|
||||||
<Bell class="h-8 w-8 mx-auto text-muted-foreground/40" />
|
<Bell class="h-8 w-8 text-muted-foreground/40" />
|
||||||
<p class="mt-2 text-xs text-muted-foreground">
|
<p class="mt-2 text-xs text-muted-foreground">
|
||||||
暂无公告
|
暂无公告
|
||||||
</p>
|
</p>
|
||||||
@@ -793,9 +793,8 @@ const statCardGlows = [
|
|||||||
'bg-kraft/30'
|
'bg-kraft/30'
|
||||||
]
|
]
|
||||||
|
|
||||||
const getStatIconColor = (index: number): string => {
|
const getStatIconColor = (_index: number): string => {
|
||||||
const colors = ['text-book-cloth', 'text-kraft', 'text-book-cloth', 'text-kraft']
|
return 'text-muted-foreground'
|
||||||
return colors[index % colors.length]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 统计数据
|
// 统计数据
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ from src.core.logger import logger
|
|||||||
from src.database import get_db
|
from src.database import get_db
|
||||||
from src.models.database import ApiKey, User
|
from src.models.database import ApiKey, User
|
||||||
from src.services.cache.affinity_manager import get_affinity_manager
|
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"])
|
router = APIRouter(prefix="/api/admin/monitoring/cache", tags=["Admin - Monitoring: Cache"])
|
||||||
pipeline = ApiRequestPipeline()
|
pipeline = ApiRequestPipeline()
|
||||||
@@ -250,7 +251,22 @@ class AdminCacheStatsAdapter(AdminApiAdapter):
|
|||||||
async def handle(self, context: ApiRequestContext) -> Dict[str, Any]: # type: ignore[override]
|
async def handle(self, context: ApiRequestContext) -> Dict[str, Any]: # type: ignore[override]
|
||||||
try:
|
try:
|
||||||
redis_client = get_redis_client_sync()
|
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()
|
stats = await scheduler.get_stats()
|
||||||
logger.info("缓存统计信息查询成功")
|
logger.info("缓存统计信息查询成功")
|
||||||
context.add_audit_metadata(
|
context.add_audit_metadata(
|
||||||
@@ -270,7 +286,22 @@ class AdminCacheMetricsAdapter(AdminApiAdapter):
|
|||||||
async def handle(self, context: ApiRequestContext) -> PlainTextResponse:
|
async def handle(self, context: ApiRequestContext) -> PlainTextResponse:
|
||||||
try:
|
try:
|
||||||
redis_client = get_redis_client_sync()
|
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()
|
stats = await scheduler.get_stats()
|
||||||
payload = self._format_prometheus(stats)
|
payload = self._format_prometheus(stats)
|
||||||
context.add_audit_metadata(
|
context.add_audit_metadata(
|
||||||
|
|||||||
@@ -852,7 +852,7 @@ class AdminImportConfigAdapter(AdminApiAdapter):
|
|||||||
from src.services.cache.invalidation import get_cache_invalidation_service
|
from src.services.cache.invalidation import get_cache_invalidation_service
|
||||||
|
|
||||||
cache_service = get_cache_invalidation_service()
|
cache_service = get_cache_invalidation_service()
|
||||||
cache_service.invalidate_all()
|
cache_service.clear_all_caches()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"message": "配置导入成功",
|
"message": "配置导入成功",
|
||||||
|
|||||||
67
src/services/cache/aware_scheduler.py
vendored
67
src/services/cache/aware_scheduler.py
vendored
@@ -121,8 +121,17 @@ class CacheAwareScheduler:
|
|||||||
PRIORITY_MODE_PROVIDER,
|
PRIORITY_MODE_PROVIDER,
|
||||||
PRIORITY_MODE_GLOBAL_KEY,
|
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:
|
Args:
|
||||||
redis_client: Redis客户端(可选)
|
redis_client: Redis客户端(可选)
|
||||||
priority_mode: 候选排序策略(provider | global_key)
|
priority_mode: 候选排序策略(provider | global_key)
|
||||||
|
scheduling_mode: 调度模式(fixed_order | cache_affinity)
|
||||||
"""
|
"""
|
||||||
self.redis = redis_client
|
self.redis = redis_client
|
||||||
self.priority_mode = self._normalize_priority_mode(
|
self.priority_mode = self._normalize_priority_mode(
|
||||||
priority_mode or self.PRIORITY_MODE_PROVIDER
|
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
|
self._affinity_manager: Optional[CacheAffinityManager] = None
|
||||||
@@ -673,14 +686,19 @@ class CacheAwareScheduler:
|
|||||||
f"(api_format={target_format.value}, model={model_name})"
|
f"(api_format={target_format.value}, model={model_name})"
|
||||||
)
|
)
|
||||||
|
|
||||||
# 4. 应用缓存亲和性排序(使用 global_model_id 作为模型标识)
|
# 4. 应用缓存亲和性排序(仅在缓存亲和模式下启用)
|
||||||
if affinity_key and candidates:
|
if self.scheduling_mode == self.SCHEDULING_MODE_CACHE_AFFINITY:
|
||||||
candidates = await self._apply_cache_affinity(
|
if affinity_key and candidates:
|
||||||
candidates=candidates,
|
candidates = await self._apply_cache_affinity(
|
||||||
affinity_key=affinity_key,
|
candidates=candidates,
|
||||||
api_format=target_format,
|
affinity_key=affinity_key,
|
||||||
global_model_id=global_model_id,
|
api_format=target_format,
|
||||||
)
|
global_model_id=global_model_id,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# 固定顺序模式:标记所有候选为非缓存
|
||||||
|
for candidate in candidates:
|
||||||
|
candidate.is_cached = False
|
||||||
|
|
||||||
return candidates, global_model_id
|
return candidates, global_model_id
|
||||||
|
|
||||||
@@ -1060,6 +1078,22 @@ class CacheAwareScheduler:
|
|||||||
self.priority_mode = normalized
|
self.priority_mode = normalized
|
||||||
logger.debug(f"[CacheAwareScheduler] 切换优先级模式为: {self.priority_mode}")
|
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(
|
def _apply_priority_mode_sort(
|
||||||
self, candidates: List[ProviderCandidate], affinity_key: Optional[str] = None
|
self, candidates: List[ProviderCandidate], affinity_key: Optional[str] = None
|
||||||
) -> List[ProviderCandidate]:
|
) -> List[ProviderCandidate]:
|
||||||
@@ -1307,6 +1341,7 @@ _scheduler: Optional[CacheAwareScheduler] = None
|
|||||||
async def get_cache_aware_scheduler(
|
async def get_cache_aware_scheduler(
|
||||||
redis_client=None,
|
redis_client=None,
|
||||||
priority_mode: Optional[str] = None,
|
priority_mode: Optional[str] = None,
|
||||||
|
scheduling_mode: Optional[str] = None,
|
||||||
) -> CacheAwareScheduler:
|
) -> CacheAwareScheduler:
|
||||||
"""
|
"""
|
||||||
获取全局CacheAwareScheduler实例
|
获取全局CacheAwareScheduler实例
|
||||||
@@ -1317,6 +1352,7 @@ async def get_cache_aware_scheduler(
|
|||||||
Args:
|
Args:
|
||||||
redis_client: Redis客户端(可选)
|
redis_client: Redis客户端(可选)
|
||||||
priority_mode: 外部覆盖的优先级模式(provider | global_key)
|
priority_mode: 外部覆盖的优先级模式(provider | global_key)
|
||||||
|
scheduling_mode: 外部覆盖的调度模式(fixed_order | cache_affinity)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
CacheAwareScheduler实例
|
CacheAwareScheduler实例
|
||||||
@@ -1324,8 +1360,13 @@ async def get_cache_aware_scheduler(
|
|||||||
global _scheduler
|
global _scheduler
|
||||||
|
|
||||||
if _scheduler is None:
|
if _scheduler is None:
|
||||||
_scheduler = CacheAwareScheduler(redis_client, priority_mode=priority_mode)
|
_scheduler = CacheAwareScheduler(
|
||||||
elif priority_mode:
|
redis_client, priority_mode=priority_mode, scheduling_mode=scheduling_mode
|
||||||
_scheduler.set_priority_mode(priority_mode)
|
)
|
||||||
|
else:
|
||||||
|
if priority_mode:
|
||||||
|
_scheduler.set_priority_mode(priority_mode)
|
||||||
|
if scheduling_mode:
|
||||||
|
_scheduler.set_scheduling_mode(scheduling_mode)
|
||||||
|
|
||||||
return _scheduler
|
return _scheduler
|
||||||
|
|||||||
@@ -102,9 +102,15 @@ class FallbackOrchestrator:
|
|||||||
"provider_priority_mode",
|
"provider_priority_mode",
|
||||||
CacheAwareScheduler.PRIORITY_MODE_PROVIDER,
|
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.cache_scheduler = await get_cache_aware_scheduler(
|
||||||
self.redis,
|
self.redis,
|
||||||
priority_mode=priority_mode,
|
priority_mode=priority_mode,
|
||||||
|
scheduling_mode=scheduling_mode,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# 确保运行时配置变更能生效
|
# 确保运行时配置变更能生效
|
||||||
@@ -113,7 +119,13 @@ class FallbackOrchestrator:
|
|||||||
"provider_priority_mode",
|
"provider_priority_mode",
|
||||||
CacheAwareScheduler.PRIORITY_MODE_PROVIDER,
|
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_priority_mode(priority_mode)
|
||||||
|
self.cache_scheduler.set_scheduling_mode(scheduling_mode)
|
||||||
|
|
||||||
# 确保 cache_scheduler 内部组件也已初始化
|
# 确保 cache_scheduler 内部组件也已初始化
|
||||||
await self.cache_scheduler._ensure_initialized()
|
await self.cache_scheduler._ensure_initialized()
|
||||||
|
|||||||
@@ -71,6 +71,10 @@ class SystemConfigService:
|
|||||||
"value": "provider",
|
"value": "provider",
|
||||||
"description": "优先级策略:provider(提供商优先模式) 或 global_key(全局Key优先模式)",
|
"description": "优先级策略:provider(提供商优先模式) 或 global_key(全局Key优先模式)",
|
||||||
},
|
},
|
||||||
|
"scheduling_mode": {
|
||||||
|
"value": "cache_affinity",
|
||||||
|
"description": "调度模式:fixed_order(固定顺序模式,严格按优先级顺序) 或 cache_affinity(缓存亲和模式,优先使用已缓存的Provider)",
|
||||||
|
},
|
||||||
"auto_delete_expired_keys": {
|
"auto_delete_expired_keys": {
|
||||||
"value": False,
|
"value": False,
|
||||||
"description": "是否自动删除过期的API Key(True=物理删除,False=仅禁用),仅管理员可配置",
|
"description": "是否自动删除过期的API Key(True=物理删除,False=仅禁用),仅管理员可配置",
|
||||||
|
|||||||
Reference in New Issue
Block a user