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:
fawney19
2026-04-09 13:51:50 +08:00
parent 4fc95adfb9
commit b0b40c16ff
97 changed files with 5816 additions and 1881 deletions
@@ -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) {