mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-08 02:17:46 +08:00
feat: 全栈功能增强 - 扩展 provider/pool 管理、完善调度与数据层、重构前端 Pool 页面
后端: - 扩展 pool_admin payloads 和 provider query models,增强 endpoint key 管理 - 完善 scheduler-core 候选排序与请求候选逻辑 - 增强 usage-runtime 写入、provider-transport 网络层与 OAuth 刷新 - 改进 AI pipeline 响应转换与流式处理 - 扩展 global_models/provider_catalog 数据层查询能力 - 增强 video-tasks-core 多 provider 支持 - 新增大量集成测试覆盖 pool/keys/provider_query/frontdoor 前端: - 重构 PoolManagement 页面,拆分状态管理/对话框逻辑到独立模块 - 新增 poolAdvancedDialog/poolSchedulingDialog/poolManagementState/poolMobilePresentation 工具函数及测试 - 改进 Dialog 组件与 provider tabs 显示 部署: - 更新 Rust CI workflow 和 Dockerfile 构建配置 Closes #275 Co-authored-by: AAEE86 <[email protected]>
This commit is contained in:
@@ -6,11 +6,11 @@
|
||||
size="lg"
|
||||
@update:model-value="emit('update:modelValue', $event)"
|
||||
>
|
||||
<div class="space-y-6">
|
||||
<div class="max-h-[calc(100dvh-13rem)] space-y-5 overflow-y-auto overscroll-contain pr-1 sm:max-h-[min(72vh,42rem)] sm:space-y-6 sm:pr-2">
|
||||
<!-- Section 1: 分配模式 (distribution_mode 互斥组, 四选一) -->
|
||||
<div class="space-y-3">
|
||||
<div class="space-y-4 rounded-2xl border border-border/60 bg-card/70 p-4">
|
||||
<div class="space-y-1">
|
||||
<h3 class="text-sm font-medium border-b pb-2">
|
||||
<h3 class="text-sm font-medium">
|
||||
分配模式
|
||||
</h3>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
@@ -18,19 +18,19 @@
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-0.5 p-1 bg-muted/40 rounded-lg">
|
||||
<div class="grid grid-cols-2 gap-2 sm:grid-cols-4">
|
||||
<button
|
||||
v-for="{ index, item } in distributionItems"
|
||||
:key="item.preset"
|
||||
type="button"
|
||||
class="flex-1 px-3 py-2 text-sm font-medium rounded-md transition-all duration-200"
|
||||
class="min-h-11 rounded-xl border px-3 py-2.5 text-sm font-medium leading-tight transition-all duration-200"
|
||||
:disabled="!item.applicable"
|
||||
:class="[
|
||||
activeDistributionPreset === item.preset
|
||||
? 'bg-primary text-primary-foreground shadow-sm'
|
||||
? 'border-primary bg-primary text-primary-foreground shadow-sm shadow-primary/20'
|
||||
: item.applicable
|
||||
? 'text-muted-foreground hover:text-foreground hover:bg-background/60'
|
||||
: 'text-muted-foreground/40 cursor-not-allowed'
|
||||
? 'border-border/60 bg-background text-foreground hover:border-border hover:bg-muted/40'
|
||||
: 'border-border/30 bg-muted/20 text-muted-foreground/50 cursor-not-allowed'
|
||||
]"
|
||||
@click="item.applicable && selectDistribution(index, item.preset)"
|
||||
>
|
||||
@@ -38,38 +38,53 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p
|
||||
v-if="activeDistributionDesc"
|
||||
class="text-xs text-muted-foreground px-1"
|
||||
<div
|
||||
v-if="activeDistributionLabel || activeDistributionDesc"
|
||||
class="rounded-xl border border-primary/15 bg-primary/5 px-3 py-2.5"
|
||||
>
|
||||
{{ activeDistributionDesc }}
|
||||
</p>
|
||||
<p
|
||||
v-if="activeDistributionDesc"
|
||||
class="mt-1 text-xs leading-5 text-muted-foreground"
|
||||
>
|
||||
{{ activeDistributionDesc }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Section 2: 策略调度 (非互斥, 可叠加组合 + 拖拽排序) -->
|
||||
<div class="space-y-3">
|
||||
<div class="space-y-4 rounded-2xl border border-border/60 bg-card/70 p-4">
|
||||
<div class="space-y-1">
|
||||
<h3 class="text-sm font-medium border-b pb-2">
|
||||
策略调度
|
||||
</h3>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<h3 class="text-sm font-medium">
|
||||
策略调度
|
||||
</h3>
|
||||
<span class="rounded-full bg-muted px-2 py-0.5 text-[11px] text-muted-foreground">
|
||||
已启用 {{ enabledStrategyCount }} 项
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
在分配模式基础上叠加排序因素,可组合启用,拖拽调整优先级。
|
||||
在分配模式基础上叠加排序因素,可组合启用。
|
||||
</p>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
桌面端支持拖拽排序,移动端可点按上下调整优先级。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-0.5">
|
||||
<div class="space-y-1.5">
|
||||
<div
|
||||
v-for="{ index, item } in strategyItems"
|
||||
:key="item.preset"
|
||||
class="group flex items-center gap-3 px-3 py-2.5 rounded-lg border transition-all duration-200"
|
||||
class="group rounded-xl border px-3 py-2.5 transition-all duration-200"
|
||||
:class="[
|
||||
!item.applicable
|
||||
? 'border-border/30 bg-muted/20 opacity-50'
|
||||
? 'border-border/40 bg-muted/20 opacity-80'
|
||||
: draggedIndex === index
|
||||
? 'border-primary/50 bg-primary/5 shadow-md scale-[1.01]'
|
||||
? 'border-primary/50 bg-primary/5 shadow-md'
|
||||
: dragOverIndex === index
|
||||
? 'border-primary/30 bg-primary/5'
|
||||
: 'border-border/50 bg-background hover:border-border hover:bg-muted/30'
|
||||
: item.enabled
|
||||
? 'border-primary/20 bg-primary/5 hover:border-primary/30'
|
||||
: 'border-border/60 bg-background hover:border-border hover:bg-muted/30'
|
||||
]"
|
||||
:draggable="item.applicable"
|
||||
@dragstart="item.applicable && handleDragStart(index, $event)"
|
||||
@@ -78,57 +93,98 @@
|
||||
@dragleave="handleDragLeave"
|
||||
@drop="item.applicable && handleDrop(index)"
|
||||
>
|
||||
<!-- Drag handle -->
|
||||
<div
|
||||
class="p-1 rounded transition-colors shrink-0"
|
||||
:class="item.applicable
|
||||
? 'cursor-grab active:cursor-grabbing text-muted-foreground/40 group-hover:text-muted-foreground'
|
||||
: 'text-muted-foreground/15 cursor-default'"
|
||||
>
|
||||
<GripVertical class="w-4 h-4" />
|
||||
</div>
|
||||
|
||||
<Switch
|
||||
:model-value="item.enabled"
|
||||
:disabled="!item.applicable"
|
||||
@update:model-value="(v: boolean) => togglePreset(index, v)"
|
||||
/>
|
||||
|
||||
<!-- Info -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-2">
|
||||
<span
|
||||
class="text-sm font-medium"
|
||||
:class="!item.applicable ? 'text-muted-foreground' : ''"
|
||||
>{{ item.label }}</span>
|
||||
<span
|
||||
v-if="!item.applicable"
|
||||
class="text-[10px] text-muted-foreground/60"
|
||||
>(不适用)</span>
|
||||
</div>
|
||||
<p class="text-xs text-muted-foreground mt-0.5">
|
||||
{{ item.desc }}
|
||||
</p>
|
||||
|
||||
<!-- Mode sub-config -->
|
||||
<div class="flex items-start gap-2.5">
|
||||
<!-- Drag handle -->
|
||||
<div
|
||||
v-if="item.modeOptions.length > 0 && item.enabled && item.applicable"
|
||||
class="flex gap-0.5 mt-2 p-0.5 bg-muted/40 rounded-md w-fit"
|
||||
class="hidden rounded-lg p-1 transition-colors sm:flex sm:shrink-0"
|
||||
:class="item.applicable
|
||||
? 'cursor-grab active:cursor-grabbing text-muted-foreground/40 group-hover:text-muted-foreground'
|
||||
: 'text-muted-foreground/20 cursor-default'"
|
||||
>
|
||||
<button
|
||||
v-for="modeOpt in item.modeOptions"
|
||||
:key="modeOpt.value"
|
||||
type="button"
|
||||
class="px-2.5 py-1 text-xs font-medium rounded transition-all"
|
||||
:class="[
|
||||
item.mode === modeOpt.value
|
||||
? 'bg-primary text-primary-foreground shadow-sm'
|
||||
: 'text-muted-foreground hover:text-foreground hover:bg-background/50'
|
||||
]"
|
||||
@click="setPresetModeByPreset(item.preset, modeOpt.value)"
|
||||
<GripVertical class="h-4 w-4" />
|
||||
</div>
|
||||
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex items-start gap-2.5">
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<span
|
||||
class="text-sm font-medium"
|
||||
:class="!item.applicable ? 'text-muted-foreground' : 'text-foreground'"
|
||||
>{{ item.label }}</span>
|
||||
<span
|
||||
v-if="getStrategyPriority(index)"
|
||||
class="rounded-full bg-primary/10 px-2 py-0.5 text-[11px] font-medium text-primary"
|
||||
>
|
||||
#{{ getStrategyPriority(index) }}
|
||||
</span>
|
||||
<span
|
||||
v-if="!item.applicable"
|
||||
class="rounded-full border border-border/60 bg-muted px-2 py-0.5 text-[11px] text-muted-foreground"
|
||||
>
|
||||
当前不可用
|
||||
</span>
|
||||
</div>
|
||||
<p class="mt-0.5 text-xs leading-5 text-muted-foreground">
|
||||
{{ item.desc }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="item.applicable"
|
||||
class="flex shrink-0 items-center gap-1.5 sm:hidden"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex h-7 items-center justify-center rounded-lg border border-border/60 px-2.5 text-[11px] font-medium text-foreground transition-colors hover:bg-muted disabled:cursor-not-allowed disabled:opacity-40"
|
||||
:disabled="!canMoveStrategy(index, -1)"
|
||||
@click="moveStrategy(index, -1)"
|
||||
>
|
||||
上移
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex h-7 items-center justify-center rounded-lg border border-border/60 px-2.5 text-[11px] font-medium text-foreground transition-colors hover:bg-muted disabled:cursor-not-allowed disabled:opacity-40"
|
||||
:disabled="!canMoveStrategy(index, 1)"
|
||||
@click="moveStrategy(index, 1)"
|
||||
>
|
||||
下移
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Switch
|
||||
:model-value="item.enabled && item.applicable"
|
||||
:disabled="!item.applicable"
|
||||
class="mt-0.5 shrink-0"
|
||||
@update:model-value="(v: boolean) => togglePreset(index, v)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="(item.modeOptions.length > 0 && item.enabled && item.applicable) || item.applicable"
|
||||
class="mt-2 flex flex-col gap-1.5 sm:flex-row sm:items-center sm:justify-between"
|
||||
>
|
||||
{{ modeOpt.label }}
|
||||
</button>
|
||||
<!-- Mode sub-config -->
|
||||
<div
|
||||
v-if="item.modeOptions.length > 0 && item.enabled && item.applicable"
|
||||
class="flex flex-wrap gap-1 rounded-lg bg-muted/40 p-1"
|
||||
>
|
||||
<button
|
||||
v-for="modeOpt in item.modeOptions"
|
||||
:key="modeOpt.value"
|
||||
type="button"
|
||||
class="rounded-md px-2.5 py-1 text-xs font-medium transition-all"
|
||||
:class="[
|
||||
item.mode === modeOpt.value
|
||||
? 'bg-primary text-primary-foreground shadow-sm'
|
||||
: 'text-muted-foreground hover:bg-background/70 hover:text-foreground'
|
||||
]"
|
||||
@click="setPresetModeByPreset(item.preset, modeOpt.value)"
|
||||
>
|
||||
{{ modeOpt.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -139,12 +195,14 @@
|
||||
<template #footer>
|
||||
<Button
|
||||
variant="outline"
|
||||
class="min-w-[96px] flex-1 sm:flex-none"
|
||||
:disabled="loading"
|
||||
@click="emit('update:modelValue', false)"
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
class="min-w-[96px] flex-1 sm:flex-none"
|
||||
:disabled="loading"
|
||||
@click="handleSave"
|
||||
>
|
||||
@@ -162,6 +220,7 @@ import { useToast } from '@/composables/useToast'
|
||||
import { parseApiError } from '@/utils/errorParser'
|
||||
import { updateProvider } from '@/api/endpoints'
|
||||
import { getPoolSchedulingPresets } from '@/api/endpoints/pool'
|
||||
import { moveStrategyItem } from '@/features/pool/utils/poolSchedulingDialog'
|
||||
import type { PoolPresetMeta } from '@/api/endpoints/pool'
|
||||
import type {
|
||||
PoolAdvancedConfig,
|
||||
@@ -665,6 +724,11 @@ const activeDistributionDesc = computed(() => {
|
||||
return found?.item.desc ?? null
|
||||
})
|
||||
|
||||
const activeDistributionLabel = computed(() => {
|
||||
const found = distributionItems.value.find(({ item }) => item.enabled && item.applicable)
|
||||
return found?.item.label ?? null
|
||||
})
|
||||
|
||||
const strategyItems = computed(() => {
|
||||
const items: { index: number; item: PresetListItem }[] = []
|
||||
presetList.value.forEach((item, index) => {
|
||||
@@ -675,6 +739,37 @@ const strategyItems = computed(() => {
|
||||
return items
|
||||
})
|
||||
|
||||
const enabledStrategyPriorityMap = computed(() => {
|
||||
const priorities = new Map<number, number>()
|
||||
let priority = 0
|
||||
|
||||
strategyItems.value.forEach(({ index, item }) => {
|
||||
if (!item.enabled || !item.applicable) return
|
||||
priority += 1
|
||||
priorities.set(index, priority)
|
||||
})
|
||||
|
||||
return priorities
|
||||
})
|
||||
|
||||
const enabledStrategyCount = computed(() => enabledStrategyPriorityMap.value.size)
|
||||
|
||||
function getStrategyPriority(index: number): number | null {
|
||||
return enabledStrategyPriorityMap.value.get(index) ?? null
|
||||
}
|
||||
|
||||
function canMoveStrategy(index: number, direction: -1 | 1): boolean {
|
||||
const strategyIndexes = strategyItems.value.map(({ index: currentIndex }) => currentIndex)
|
||||
const currentPosition = strategyIndexes.indexOf(index)
|
||||
if (currentPosition === -1) return false
|
||||
const targetPosition = currentPosition + direction
|
||||
return targetPosition >= 0 && targetPosition < strategyIndexes.length
|
||||
}
|
||||
|
||||
function moveStrategy(index: number, direction: -1 | 1) {
|
||||
presetList.value = moveStrategyItem(presetList.value, index, direction)
|
||||
}
|
||||
|
||||
function handleDragStart(index: number, event: DragEvent) {
|
||||
draggedIndex.value = index
|
||||
if (event.dataTransfer) {
|
||||
|
||||
Reference in New Issue
Block a user