2026-03-04 22:06:19 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<Dialog
|
|
|
|
|
|
:model-value="modelValue"
|
|
|
|
|
|
title="号池调度"
|
2026-03-05 23:22:25 +08:00
|
|
|
|
description="管理号池内 Key 的分配模式和排序偏好"
|
2026-03-04 22:06:19 +08:00
|
|
|
|
size="lg"
|
|
|
|
|
|
@update:model-value="emit('update:modelValue', $event)"
|
|
|
|
|
|
>
|
2026-04-09 13:51:50 +08:00
|
|
|
|
<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">
|
2026-03-05 23:22:25 +08:00
|
|
|
|
<!-- Section 1: 分配模式 (distribution_mode 互斥组, 四选一) -->
|
2026-04-09 13:51:50 +08:00
|
|
|
|
<div class="space-y-4 rounded-2xl border border-border/60 bg-card/70 p-4">
|
2026-03-04 22:06:19 +08:00
|
|
|
|
<div class="space-y-1">
|
2026-04-09 13:51:50 +08:00
|
|
|
|
<h3 class="text-sm font-medium">
|
2026-03-05 23:22:25 +08:00
|
|
|
|
分配模式
|
2026-03-04 22:06:19 +08:00
|
|
|
|
</h3>
|
|
|
|
|
|
<p class="text-xs text-muted-foreground">
|
2026-03-05 23:22:25 +08:00
|
|
|
|
控制 Key 的基础分配方式,选择一种模式。
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-04-09 13:51:50 +08:00
|
|
|
|
<div class="grid grid-cols-2 gap-2 sm:grid-cols-4">
|
2026-03-05 23:22:25 +08:00
|
|
|
|
<button
|
|
|
|
|
|
v-for="{ index, item } in distributionItems"
|
|
|
|
|
|
:key="item.preset"
|
|
|
|
|
|
type="button"
|
2026-04-09 13:51:50 +08:00
|
|
|
|
class="min-h-11 rounded-xl border px-3 py-2.5 text-sm font-medium leading-tight transition-all duration-200"
|
2026-03-05 23:22:25 +08:00
|
|
|
|
:disabled="!item.applicable"
|
|
|
|
|
|
:class="[
|
|
|
|
|
|
activeDistributionPreset === item.preset
|
2026-04-09 13:51:50 +08:00
|
|
|
|
? 'border-primary bg-primary text-primary-foreground shadow-sm shadow-primary/20'
|
2026-03-05 23:22:25 +08:00
|
|
|
|
: item.applicable
|
2026-04-09 13:51:50 +08:00
|
|
|
|
? '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'
|
2026-03-05 23:22:25 +08:00
|
|
|
|
]"
|
|
|
|
|
|
@click="item.applicable && selectDistribution(index, item.preset)"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ item.label }}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-04-09 13:51:50 +08:00
|
|
|
|
<div
|
|
|
|
|
|
v-if="activeDistributionLabel || activeDistributionDesc"
|
|
|
|
|
|
class="rounded-xl border border-primary/15 bg-primary/5 px-3 py-2.5"
|
2026-03-05 23:22:25 +08:00
|
|
|
|
>
|
2026-04-09 13:51:50 +08:00
|
|
|
|
<p
|
|
|
|
|
|
v-if="activeDistributionDesc"
|
|
|
|
|
|
class="mt-1 text-xs leading-5 text-muted-foreground"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ activeDistributionDesc }}
|
|
|
|
|
|
</p>
|
2026-09-01 08:06:58 +08:00
|
|
|
|
<div
|
|
|
|
|
|
v-if="activeDistributionItem?.modeOptions.length"
|
|
|
|
|
|
data-testid="pool-cache-affinity-secondary-mode"
|
|
|
|
|
|
class="mt-2 flex w-fit flex-wrap gap-1 rounded-lg bg-muted/50 p-1"
|
|
|
|
|
|
>
|
|
|
|
|
|
<button
|
|
|
|
|
|
v-for="modeOpt in activeDistributionItem.modeOptions"
|
|
|
|
|
|
:key="modeOpt.value"
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
:data-mode="modeOpt.value"
|
|
|
|
|
|
class="rounded-md px-2.5 py-1 text-xs font-medium transition-all"
|
|
|
|
|
|
:class="[
|
|
|
|
|
|
activeDistributionItem.mode === modeOpt.value
|
|
|
|
|
|
? 'bg-primary text-primary-foreground shadow-sm'
|
|
|
|
|
|
: 'text-muted-foreground hover:bg-background/70 hover:text-foreground'
|
|
|
|
|
|
]"
|
|
|
|
|
|
@click="setPresetModeByPreset(activeDistributionItem.preset, modeOpt.value)"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ modeOpt.label }}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
2026-04-09 13:51:50 +08:00
|
|
|
|
</div>
|
2026-03-05 23:22:25 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-09-23 14:44:29 +08:00
|
|
|
|
<div
|
|
|
|
|
|
v-if="isCodex"
|
|
|
|
|
|
class="flex items-start justify-between gap-4 rounded-2xl border border-border/60 bg-card/70 p-4"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="space-y-1">
|
|
|
|
|
|
<label
|
|
|
|
|
|
for="pool-reserve-minimum-quota"
|
|
|
|
|
|
class="text-sm font-medium"
|
|
|
|
|
|
>
|
|
|
|
|
|
保留最低额度
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<p
|
|
|
|
|
|
id="pool-reserve-minimum-quota-description"
|
|
|
|
|
|
class="text-xs leading-5 text-muted-foreground"
|
|
|
|
|
|
>
|
|
|
|
|
|
账号剩余额度不高于 1% 时提前标记为额度耗尽并停止调度,待额度恢复后再使用。
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<Switch
|
|
|
|
|
|
id="pool-reserve-minimum-quota"
|
|
|
|
|
|
v-model="reserveMinimumQuota"
|
|
|
|
|
|
aria-describedby="pool-reserve-minimum-quota-description"
|
|
|
|
|
|
:disabled="loading"
|
|
|
|
|
|
class="mt-0.5 shrink-0"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-03-05 23:22:25 +08:00
|
|
|
|
<!-- Section 2: 策略调度 (非互斥, 可叠加组合 + 拖拽排序) -->
|
2026-04-09 13:51:50 +08:00
|
|
|
|
<div class="space-y-4 rounded-2xl border border-border/60 bg-card/70 p-4">
|
2026-03-05 23:22:25 +08:00
|
|
|
|
<div class="space-y-1">
|
2026-04-09 13:51:50 +08:00
|
|
|
|
<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>
|
2026-03-05 23:22:25 +08:00
|
|
|
|
<p class="text-xs text-muted-foreground">
|
2026-04-09 13:51:50 +08:00
|
|
|
|
在分配模式基础上叠加排序因素,可组合启用。
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<p class="text-xs text-muted-foreground">
|
|
|
|
|
|
桌面端支持拖拽排序,移动端可点按上下调整优先级。
|
2026-03-04 22:06:19 +08:00
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-04-09 13:51:50 +08:00
|
|
|
|
<div class="space-y-1.5">
|
2026-03-04 22:06:19 +08:00
|
|
|
|
<div
|
2026-03-05 23:22:25 +08:00
|
|
|
|
v-for="{ index, item } in strategyItems"
|
2026-03-04 22:06:19 +08:00
|
|
|
|
:key="item.preset"
|
2026-04-09 13:51:50 +08:00
|
|
|
|
class="group rounded-xl border px-3 py-2.5 transition-all duration-200"
|
2026-03-04 22:06:19 +08:00
|
|
|
|
:class="[
|
2026-03-05 23:22:25 +08:00
|
|
|
|
!item.applicable
|
2026-04-09 13:51:50 +08:00
|
|
|
|
? 'border-border/40 bg-muted/20 opacity-80'
|
2026-03-04 22:06:19 +08:00
|
|
|
|
: draggedIndex === index
|
2026-04-09 13:51:50 +08:00
|
|
|
|
? 'border-primary/50 bg-primary/5 shadow-md'
|
2026-03-04 22:06:19 +08:00
|
|
|
|
: dragOverIndex === index
|
|
|
|
|
|
? 'border-primary/30 bg-primary/5'
|
2026-04-09 13:51:50 +08:00
|
|
|
|
: item.enabled
|
|
|
|
|
|
? 'border-primary/20 bg-primary/5 hover:border-primary/30'
|
|
|
|
|
|
: 'border-border/60 bg-background hover:border-border hover:bg-muted/30'
|
2026-03-04 22:06:19 +08:00
|
|
|
|
]"
|
2026-03-05 23:22:25 +08:00
|
|
|
|
:draggable="item.applicable"
|
|
|
|
|
|
@dragstart="item.applicable && handleDragStart(index, $event)"
|
2026-03-04 22:06:19 +08:00
|
|
|
|
@dragend="handleDragEnd"
|
2026-03-05 23:22:25 +08:00
|
|
|
|
@dragover.prevent="item.applicable && handleDragOver(index)"
|
2026-03-04 22:06:19 +08:00
|
|
|
|
@dragleave="handleDragLeave"
|
2026-03-05 23:22:25 +08:00
|
|
|
|
@drop="item.applicable && handleDrop(index)"
|
2026-03-04 22:06:19 +08:00
|
|
|
|
>
|
2026-04-09 13:51:50 +08:00
|
|
|
|
<div class="flex items-start gap-2.5">
|
|
|
|
|
|
<!-- Drag handle -->
|
2026-03-04 22:06:19 +08:00
|
|
|
|
<div
|
2026-04-09 13:51:50 +08:00
|
|
|
|
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'"
|
2026-03-04 22:06:19 +08:00
|
|
|
|
>
|
2026-04-09 13:51:50 +08:00
|
|
|
|
<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"
|
2026-03-04 22:06:19 +08:00
|
|
|
|
>
|
2026-04-09 13:51:50 +08:00
|
|
|
|
<!-- 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>
|
2026-03-04 22:06:19 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<template #footer>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
variant="outline"
|
2026-04-09 13:51:50 +08:00
|
|
|
|
class="min-w-[96px] flex-1 sm:flex-none"
|
2026-03-04 22:06:19 +08:00
|
|
|
|
:disabled="loading"
|
|
|
|
|
|
@click="emit('update:modelValue', false)"
|
|
|
|
|
|
>
|
|
|
|
|
|
取消
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Button
|
2026-04-09 13:51:50 +08:00
|
|
|
|
class="min-w-[96px] flex-1 sm:flex-none"
|
2026-03-04 22:06:19 +08:00
|
|
|
|
:disabled="loading"
|
|
|
|
|
|
@click="handleSave"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ loading ? '保存中...' : '保存' }}
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</Dialog>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
|
import { computed, ref, watch } from 'vue'
|
|
|
|
|
|
import { GripVertical } from 'lucide-vue-next'
|
2026-03-05 15:15:26 +08:00
|
|
|
|
import { Dialog, Button, Switch } from '@/components/ui'
|
2026-03-04 22:06:19 +08:00
|
|
|
|
import { useToast } from '@/composables/useToast'
|
|
|
|
|
|
import { parseApiError } from '@/utils/errorParser'
|
2026-07-27 22:06:28 +08:00
|
|
|
|
import { getProvider, updateProvider } from '@/api/endpoints'
|
2026-03-04 22:06:19 +08:00
|
|
|
|
import { getPoolSchedulingPresets } from '@/api/endpoints/pool'
|
2026-07-27 22:06:28 +08:00
|
|
|
|
import {
|
|
|
|
|
|
mergePoolAdvancedPatch,
|
|
|
|
|
|
moveStrategyItem,
|
|
|
|
|
|
normalizeMutexSelection,
|
|
|
|
|
|
} from '@/features/pool/utils/poolSchedulingDialog'
|
2026-03-04 22:06:19 +08:00
|
|
|
|
import type { PoolPresetMeta } from '@/api/endpoints/pool'
|
2026-03-05 09:53:23 +08:00
|
|
|
|
import type {
|
|
|
|
|
|
PoolAdvancedConfig,
|
|
|
|
|
|
SchedulingPresetItem,
|
|
|
|
|
|
ProviderWithEndpointsSummary,
|
|
|
|
|
|
} from '@/api/endpoints/types/provider'
|
2026-03-04 22:06:19 +08:00
|
|
|
|
|
|
|
|
|
|
interface PresetModeOption {
|
|
|
|
|
|
value: string
|
|
|
|
|
|
label: string
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface PresetListItem {
|
|
|
|
|
|
preset: string
|
|
|
|
|
|
label: string
|
|
|
|
|
|
desc: string
|
|
|
|
|
|
enabled: boolean
|
|
|
|
|
|
mode: string | null
|
|
|
|
|
|
modeOptions: PresetModeOption[]
|
|
|
|
|
|
applicable: boolean
|
2026-03-05 09:53:23 +08:00
|
|
|
|
mutexGroup: string | null
|
|
|
|
|
|
evidenceHint: string
|
2026-03-04 22:06:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const props = defineProps<{
|
|
|
|
|
|
modelValue: boolean
|
|
|
|
|
|
providerId: string
|
|
|
|
|
|
providerType?: string
|
|
|
|
|
|
currentConfig: PoolAdvancedConfig | null
|
|
|
|
|
|
}>()
|
|
|
|
|
|
|
|
|
|
|
|
const emit = defineEmits<{
|
|
|
|
|
|
'update:modelValue': [value: boolean]
|
2026-03-05 09:53:23 +08:00
|
|
|
|
saved: [provider: ProviderWithEndpointsSummary]
|
2026-03-04 22:06:19 +08:00
|
|
|
|
}>()
|
|
|
|
|
|
|
2026-03-05 23:22:25 +08:00
|
|
|
|
const DISTRIBUTION_GROUP = 'distribution_mode'
|
|
|
|
|
|
|
2026-03-04 22:06:19 +08:00
|
|
|
|
const FALLBACK_PRESET_DEFS: PoolPresetMeta[] = [
|
2026-03-05 23:22:25 +08:00
|
|
|
|
{
|
|
|
|
|
|
name: 'cache_affinity',
|
|
|
|
|
|
label: '缓存亲和',
|
2026-09-01 08:06:58 +08:00
|
|
|
|
description: '同一用户持续复用 Key,首次分配可集中或轮转',
|
2026-03-05 23:22:25 +08:00
|
|
|
|
mutex_group: DISTRIBUTION_GROUP,
|
2026-09-01 08:06:58 +08:00
|
|
|
|
evidence_hint: '先复用用户粘性 Key,未命中时按所选二级模式分配',
|
2026-03-05 23:22:25 +08:00
|
|
|
|
providers: [],
|
2026-07-27 22:06:28 +08:00
|
|
|
|
default_enabled: true,
|
2026-09-01 08:06:58 +08:00
|
|
|
|
modes: [
|
|
|
|
|
|
{ value: 'single_account', label: '单号优先' },
|
|
|
|
|
|
{ value: 'lru', label: 'LRU 轮号' },
|
|
|
|
|
|
],
|
|
|
|
|
|
default_mode: 'single_account',
|
2026-03-05 23:22:25 +08:00
|
|
|
|
},
|
2026-03-04 22:06:19 +08:00
|
|
|
|
{
|
|
|
|
|
|
name: 'lru',
|
|
|
|
|
|
label: 'LRU 轮转',
|
|
|
|
|
|
description: '最久未使用的 Key 优先',
|
2026-03-05 23:22:25 +08:00
|
|
|
|
mutex_group: DISTRIBUTION_GROUP,
|
2026-03-05 09:53:23 +08:00
|
|
|
|
evidence_hint: '依据 LRU 时间戳(最近未使用优先)',
|
2026-03-04 22:06:19 +08:00
|
|
|
|
providers: [],
|
|
|
|
|
|
modes: null,
|
|
|
|
|
|
default_mode: null,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
2026-03-05 23:22:25 +08:00
|
|
|
|
name: 'single_account',
|
|
|
|
|
|
label: '单号优先',
|
|
|
|
|
|
description: '集中使用同一账号(反向 LRU)',
|
|
|
|
|
|
mutex_group: DISTRIBUTION_GROUP,
|
|
|
|
|
|
evidence_hint: '先按账号优先级(internal_priority),同级再按反向 LRU 集中',
|
|
|
|
|
|
providers: [],
|
|
|
|
|
|
modes: null,
|
|
|
|
|
|
default_mode: null,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: 'load_balance',
|
|
|
|
|
|
label: '负载均衡',
|
|
|
|
|
|
description: '随机分散 Key 使用,均匀分摊负载',
|
|
|
|
|
|
mutex_group: DISTRIBUTION_GROUP,
|
|
|
|
|
|
evidence_hint: '每次随机分值,实现完全均匀分散',
|
|
|
|
|
|
providers: [],
|
|
|
|
|
|
modes: null,
|
|
|
|
|
|
default_mode: null,
|
|
|
|
|
|
},
|
2026-07-27 22:06:28 +08:00
|
|
|
|
{
|
|
|
|
|
|
name: 'free_team_first',
|
|
|
|
|
|
label: 'Free/Team 优先',
|
|
|
|
|
|
description: '兼容旧配置:优先消耗 Free、Team 或两者',
|
|
|
|
|
|
evidence_hint: '依据 plan_type,保留旧 free_only/team_only/both 语义',
|
2026-09-14 21:09:03 +08:00
|
|
|
|
providers: ['codex', 'grok', 'kiro', 'windsurf', 'xai'],
|
2026-07-27 22:06:28 +08:00
|
|
|
|
modes: [
|
|
|
|
|
|
{ value: 'free_only', label: 'Free' },
|
|
|
|
|
|
{ value: 'team_only', label: 'Team' },
|
|
|
|
|
|
{ value: 'both', label: 'Free + Team' },
|
|
|
|
|
|
],
|
|
|
|
|
|
default_mode: 'both',
|
|
|
|
|
|
},
|
2026-03-05 23:22:25 +08:00
|
|
|
|
{
|
|
|
|
|
|
name: 'free_first',
|
|
|
|
|
|
label: 'Free 优先',
|
|
|
|
|
|
description: '优先消耗 Free 账号(依赖 plan_type)',
|
|
|
|
|
|
evidence_hint: '依据 plan_type(Free 账号优先调度)',
|
2026-09-14 21:09:03 +08:00
|
|
|
|
providers: ['codex', 'grok', 'kiro', 'windsurf', 'xai'],
|
2026-03-05 23:22:25 +08:00
|
|
|
|
modes: null,
|
|
|
|
|
|
default_mode: null,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: 'team_first',
|
|
|
|
|
|
label: 'Team 优先',
|
|
|
|
|
|
description: '优先消耗 Team 账号(依赖 plan_type)',
|
|
|
|
|
|
evidence_hint: '依据 plan_type(Team 账号优先调度)',
|
2026-09-14 21:09:03 +08:00
|
|
|
|
providers: ['codex', 'grok', 'kiro', 'windsurf', 'xai'],
|
2026-03-05 23:22:25 +08:00
|
|
|
|
modes: null,
|
|
|
|
|
|
default_mode: null,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: 'plus_first',
|
|
|
|
|
|
label: 'Plus 优先',
|
2026-04-24 13:29:05 +08:00
|
|
|
|
description: '优先消耗 Plus 账号(依赖 plan_type)',
|
|
|
|
|
|
evidence_hint: '依据 plan_type(Plus 账号优先调度)',
|
2026-09-14 21:09:03 +08:00
|
|
|
|
providers: ['codex', 'grok', 'kiro', 'windsurf', 'xai'],
|
2026-04-24 13:29:05 +08:00
|
|
|
|
modes: null,
|
|
|
|
|
|
default_mode: null,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: 'pro_first',
|
|
|
|
|
|
label: 'Pro 优先',
|
|
|
|
|
|
description: '优先消耗 Pro 账号(依赖 plan_type)',
|
|
|
|
|
|
evidence_hint: '依据 plan_type(Pro 账号优先调度)',
|
2026-09-14 21:09:03 +08:00
|
|
|
|
providers: ['codex', 'grok', 'kiro', 'windsurf', 'xai'],
|
2026-03-05 23:22:25 +08:00
|
|
|
|
modes: null,
|
|
|
|
|
|
default_mode: null,
|
2026-03-04 22:06:19 +08:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: 'quota_balanced',
|
|
|
|
|
|
label: '额度平均',
|
|
|
|
|
|
description: '优先选额度消耗最少的账号',
|
2026-03-05 09:53:23 +08:00
|
|
|
|
evidence_hint: '依据账号配额使用率;无配额时回退到窗口成本使用',
|
2026-03-04 22:06:19 +08:00
|
|
|
|
providers: [],
|
|
|
|
|
|
modes: null,
|
|
|
|
|
|
default_mode: null,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: 'recent_refresh',
|
|
|
|
|
|
label: '额度刷新优先',
|
|
|
|
|
|
description: '优先选即将刷新额度的账号',
|
2026-03-05 09:53:23 +08:00
|
|
|
|
evidence_hint: '依据账号额度重置倒计时(next_reset / reset_seconds)',
|
2026-09-14 21:09:03 +08:00
|
|
|
|
providers: ['codex', 'grok', 'kiro', 'windsurf', 'xai'],
|
2026-07-27 22:06:28 +08:00
|
|
|
|
default_enabled_providers: ['codex', 'windsurf'],
|
2026-03-04 22:06:19 +08:00
|
|
|
|
modes: null,
|
|
|
|
|
|
default_mode: null,
|
|
|
|
|
|
},
|
2026-03-05 09:53:23 +08:00
|
|
|
|
{
|
|
|
|
|
|
name: 'priority_first',
|
|
|
|
|
|
label: '优先级优先',
|
|
|
|
|
|
description: '按账号优先级顺序调度(数字越小越优先)',
|
|
|
|
|
|
evidence_hint: '依据 internal_priority(支持拖拽/手工编辑)',
|
|
|
|
|
|
providers: [],
|
|
|
|
|
|
modes: null,
|
|
|
|
|
|
default_mode: null,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: 'health_first',
|
|
|
|
|
|
label: '健康优先',
|
|
|
|
|
|
description: '优先选择健康分更高、失败更少的账号',
|
|
|
|
|
|
evidence_hint: '依据 health_by_format 聚合分(含熔断/失败衰减)',
|
|
|
|
|
|
providers: [],
|
|
|
|
|
|
modes: null,
|
|
|
|
|
|
default_mode: null,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: 'latency_first',
|
|
|
|
|
|
label: '延迟优先',
|
|
|
|
|
|
description: '优先选择最近延迟更低的账号',
|
|
|
|
|
|
evidence_hint: '依据号池延迟窗口均值(latency_window_seconds)',
|
|
|
|
|
|
providers: [],
|
|
|
|
|
|
modes: null,
|
|
|
|
|
|
default_mode: null,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: 'cost_first',
|
|
|
|
|
|
label: '成本优先',
|
|
|
|
|
|
description: '优先选择窗口消耗更低的账号',
|
|
|
|
|
|
evidence_hint: '依据窗口成本/Token 用量,缺失时回退配额使用率',
|
2026-03-04 22:06:19 +08:00
|
|
|
|
providers: [],
|
|
|
|
|
|
modes: null,
|
|
|
|
|
|
default_mode: null,
|
|
|
|
|
|
},
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
const { success, error: showError } = useToast()
|
|
|
|
|
|
const loading = ref(false)
|
2026-09-23 14:44:29 +08:00
|
|
|
|
const reserveMinimumQuota = ref(false)
|
|
|
|
|
|
const isCodex = computed(() => normalizeProviderType(props.providerType) === 'codex')
|
2026-07-27 22:06:28 +08:00
|
|
|
|
let dialogRevision = 0
|
2026-03-04 22:06:19 +08:00
|
|
|
|
const presetDefs = ref<PoolPresetMeta[]>([])
|
|
|
|
|
|
const presetDefsLoaded = ref(false)
|
|
|
|
|
|
const loadingPresetDefs = ref(false)
|
|
|
|
|
|
|
|
|
|
|
|
const draggedIndex = ref<number | null>(null)
|
|
|
|
|
|
const dragOverIndex = ref<number | null>(null)
|
|
|
|
|
|
const presetList = ref<PresetListItem[]>([])
|
|
|
|
|
|
|
|
|
|
|
|
function normalizeProviderType(value: string | undefined): string {
|
|
|
|
|
|
return (value || '').trim().toLowerCase()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function normalizePresetName(value: unknown): string {
|
|
|
|
|
|
return String(value ?? '').trim().toLowerCase()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function normalizeMode(value: unknown): string | null {
|
|
|
|
|
|
const normalized = String(value ?? '').trim().toLowerCase()
|
|
|
|
|
|
return normalized || null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-05 09:53:23 +08:00
|
|
|
|
function normalizeMutexGroup(value: unknown): string | null {
|
|
|
|
|
|
const normalized = String(value ?? '').trim().toLowerCase()
|
|
|
|
|
|
return normalized || null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-05 23:22:25 +08:00
|
|
|
|
const FALLBACK_ORDER = FALLBACK_PRESET_DEFS.map(d => d.name)
|
|
|
|
|
|
|
2026-03-04 22:06:19 +08:00
|
|
|
|
function normalizePresetDefs(defs: PoolPresetMeta[]): PoolPresetMeta[] {
|
|
|
|
|
|
const ordered: PoolPresetMeta[] = []
|
|
|
|
|
|
const seen = new Set<string>()
|
|
|
|
|
|
for (const raw of defs) {
|
|
|
|
|
|
const name = normalizePresetName(raw.name)
|
|
|
|
|
|
if (!name || seen.has(name)) continue
|
|
|
|
|
|
seen.add(name)
|
|
|
|
|
|
const providers = Array.isArray(raw.providers)
|
|
|
|
|
|
? raw.providers.map(p => normalizeProviderType(p)).filter(Boolean)
|
|
|
|
|
|
: []
|
|
|
|
|
|
const modes = Array.isArray(raw.modes)
|
|
|
|
|
|
? raw.modes
|
|
|
|
|
|
.map(mode => ({
|
|
|
|
|
|
value: normalizePresetName(mode.value),
|
|
|
|
|
|
label: String(mode.label ?? '').trim() || String(mode.value ?? '').trim(),
|
|
|
|
|
|
}))
|
|
|
|
|
|
.filter(mode => Boolean(mode.value))
|
|
|
|
|
|
: null
|
|
|
|
|
|
const defaultMode = normalizeMode(raw.default_mode)
|
2026-07-27 22:06:28 +08:00
|
|
|
|
const defaultEnabledProviders = Array.isArray(raw.default_enabled_providers)
|
|
|
|
|
|
? raw.default_enabled_providers.map(p => normalizeProviderType(p)).filter(Boolean)
|
|
|
|
|
|
: []
|
2026-03-04 22:06:19 +08:00
|
|
|
|
ordered.push({
|
|
|
|
|
|
name,
|
|
|
|
|
|
label: String(raw.label ?? '').trim() || name,
|
|
|
|
|
|
description: String(raw.description ?? '').trim(),
|
|
|
|
|
|
providers,
|
2026-07-27 22:06:28 +08:00
|
|
|
|
default_enabled: raw.default_enabled === true,
|
|
|
|
|
|
default_enabled_providers: defaultEnabledProviders,
|
2026-03-04 22:06:19 +08:00
|
|
|
|
modes: modes && modes.length > 0 ? modes : null,
|
|
|
|
|
|
default_mode: defaultMode,
|
2026-03-05 09:53:23 +08:00
|
|
|
|
mutex_group: normalizeMutexGroup(raw.mutex_group),
|
|
|
|
|
|
evidence_hint: String(raw.evidence_hint ?? '').trim() || null,
|
2026-03-04 22:06:19 +08:00
|
|
|
|
})
|
|
|
|
|
|
}
|
2026-03-05 23:22:25 +08:00
|
|
|
|
// Re-order by FALLBACK_PRESET_DEFS order; unknown presets go to the end.
|
|
|
|
|
|
ordered.sort((a, b) => {
|
|
|
|
|
|
const ia = FALLBACK_ORDER.indexOf(a.name)
|
|
|
|
|
|
const ib = FALLBACK_ORDER.indexOf(b.name)
|
|
|
|
|
|
return (ia === -1 ? 9999 : ia) - (ib === -1 ? 9999 : ib)
|
|
|
|
|
|
})
|
2026-03-04 22:06:19 +08:00
|
|
|
|
return ordered
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function getPresetDefs(): PoolPresetMeta[] {
|
|
|
|
|
|
if (presetDefs.value.length > 0) {
|
|
|
|
|
|
return presetDefs.value
|
|
|
|
|
|
}
|
|
|
|
|
|
return FALLBACK_PRESET_DEFS
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function ensurePresetDefsLoaded(): Promise<void> {
|
|
|
|
|
|
if (presetDefsLoaded.value || loadingPresetDefs.value) return
|
|
|
|
|
|
loadingPresetDefs.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
const remoteDefs = await getPoolSchedulingPresets()
|
|
|
|
|
|
const normalized = normalizePresetDefs(Array.isArray(remoteDefs) ? remoteDefs : [])
|
|
|
|
|
|
if (normalized.length > 0) {
|
|
|
|
|
|
presetDefs.value = normalized
|
2026-07-27 22:06:28 +08:00
|
|
|
|
presetDefsLoaded.value = true
|
2026-03-04 22:06:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
showError(parseApiError(err))
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
loadingPresetDefs.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function isApplicablePreset(def: PoolPresetMeta): boolean {
|
|
|
|
|
|
const providerType = normalizeProviderType(props.providerType)
|
|
|
|
|
|
const providers = Array.isArray(def.providers) ? def.providers : []
|
|
|
|
|
|
if (providers.length === 0) return true
|
|
|
|
|
|
if (!providerType) return true
|
|
|
|
|
|
return providers.includes(providerType)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function getModeOptions(def: PoolPresetMeta): PresetModeOption[] {
|
|
|
|
|
|
const modes = Array.isArray(def.modes) ? def.modes : []
|
|
|
|
|
|
return modes
|
|
|
|
|
|
.map(mode => ({
|
|
|
|
|
|
value: normalizePresetName(mode.value),
|
|
|
|
|
|
label: String(mode.label ?? '').trim() || String(mode.value ?? '').trim(),
|
|
|
|
|
|
}))
|
|
|
|
|
|
.filter(mode => Boolean(mode.value))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function defaultModeForPreset(def: PoolPresetMeta): string | null {
|
|
|
|
|
|
const options = getModeOptions(def)
|
|
|
|
|
|
if (options.length === 0) return null
|
|
|
|
|
|
const normalizedDefault = normalizeMode(def.default_mode)
|
|
|
|
|
|
if (normalizedDefault && options.some(option => option.value === normalizedDefault)) {
|
|
|
|
|
|
return normalizedDefault
|
|
|
|
|
|
}
|
|
|
|
|
|
return options[0].value
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-05 23:22:25 +08:00
|
|
|
|
function buildPresetListItem(def: PoolPresetMeta, enabled: boolean, mode?: unknown): PresetListItem {
|
|
|
|
|
|
return {
|
2026-03-04 22:06:19 +08:00
|
|
|
|
preset: def.name,
|
|
|
|
|
|
label: def.label,
|
|
|
|
|
|
desc: def.description,
|
2026-03-05 23:22:25 +08:00
|
|
|
|
enabled,
|
|
|
|
|
|
mode: mode !== undefined ? resolveMode(def, mode) : defaultModeForPreset(def),
|
2026-03-04 22:06:19 +08:00
|
|
|
|
modeOptions: getModeOptions(def),
|
|
|
|
|
|
applicable: isApplicablePreset(def),
|
2026-03-05 09:53:23 +08:00
|
|
|
|
mutexGroup: normalizeMutexGroup(def.mutex_group),
|
|
|
|
|
|
evidenceHint: String(def.evidence_hint ?? '').trim(),
|
2026-03-05 23:22:25 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function buildDefaultPresetList(): PresetListItem[] {
|
2026-07-27 22:06:28 +08:00
|
|
|
|
const providerType = normalizeProviderType(props.providerType)
|
|
|
|
|
|
return getPresetDefs().map((def) => {
|
|
|
|
|
|
const providerDefaults = Array.isArray(def.default_enabled_providers)
|
|
|
|
|
|
? def.default_enabled_providers.map(normalizeProviderType)
|
|
|
|
|
|
: []
|
|
|
|
|
|
const enabled = def.default_enabled === true
|
|
|
|
|
|
|| (Boolean(providerType) && providerDefaults.includes(providerType))
|
|
|
|
|
|
return buildPresetListItem(def, enabled)
|
|
|
|
|
|
})
|
2026-03-04 22:06:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function isNewFormatPresetItem(item: unknown): item is SchedulingPresetItem {
|
|
|
|
|
|
return typeof item === 'object' && item !== null && 'preset' in item
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function resolveMode(def: PoolPresetMeta, mode: unknown): string | null {
|
|
|
|
|
|
const options = getModeOptions(def)
|
|
|
|
|
|
if (options.length === 0) return null
|
|
|
|
|
|
const normalized = normalizeMode(mode)
|
|
|
|
|
|
if (normalized && options.some(option => option.value === normalized)) {
|
|
|
|
|
|
return normalized
|
|
|
|
|
|
}
|
|
|
|
|
|
return defaultModeForPreset(def)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-05 23:22:25 +08:00
|
|
|
|
function insertMissingByPreferredOrder(
|
|
|
|
|
|
ordered: PresetListItem[],
|
|
|
|
|
|
seen: Set<string>,
|
|
|
|
|
|
defs: PoolPresetMeta[],
|
|
|
|
|
|
defsByName: Map<string, PoolPresetMeta>,
|
|
|
|
|
|
) {
|
|
|
|
|
|
// Insert missing presets at their preferred position from FALLBACK_ORDER
|
|
|
|
|
|
// rather than appending to the end.
|
|
|
|
|
|
for (const name of FALLBACK_ORDER) {
|
|
|
|
|
|
if (seen.has(name)) continue
|
|
|
|
|
|
const def = defsByName.get(name)
|
|
|
|
|
|
if (!def) continue
|
|
|
|
|
|
seen.add(name)
|
|
|
|
|
|
const item = buildPresetListItem(def, false)
|
|
|
|
|
|
// Find the best insertion point: right after the last item whose
|
|
|
|
|
|
// FALLBACK_ORDER index is smaller than ours.
|
|
|
|
|
|
const myIdx = FALLBACK_ORDER.indexOf(name)
|
|
|
|
|
|
let insertAt = ordered.length
|
|
|
|
|
|
for (let i = ordered.length - 1; i >= 0; i--) {
|
|
|
|
|
|
const peerIdx = FALLBACK_ORDER.indexOf(ordered[i].preset)
|
|
|
|
|
|
if (peerIdx !== -1 && peerIdx < myIdx) {
|
|
|
|
|
|
insertAt = i + 1
|
|
|
|
|
|
break
|
|
|
|
|
|
}
|
|
|
|
|
|
if (i === 0) insertAt = 0
|
|
|
|
|
|
}
|
|
|
|
|
|
ordered.splice(insertAt, 0, item)
|
|
|
|
|
|
}
|
|
|
|
|
|
// Any remaining defs not in FALLBACK_ORDER go to the end.
|
|
|
|
|
|
for (const def of defs) {
|
|
|
|
|
|
if (seen.has(def.name)) continue
|
|
|
|
|
|
seen.add(def.name)
|
|
|
|
|
|
ordered.push(buildPresetListItem(def, false))
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function reorderDistributionGroup(items: PresetListItem[]): PresetListItem[] {
|
|
|
|
|
|
// Distribution mode items are rendered as a fixed button group,
|
|
|
|
|
|
// so their order should always match FALLBACK_ORDER regardless of saved config.
|
|
|
|
|
|
const distIndexes: number[] = []
|
|
|
|
|
|
const distItems: PresetListItem[] = []
|
|
|
|
|
|
items.forEach((item, i) => {
|
|
|
|
|
|
if (item.mutexGroup === DISTRIBUTION_GROUP) {
|
|
|
|
|
|
distIndexes.push(i)
|
|
|
|
|
|
distItems.push(item)
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
if (distItems.length <= 1) return items
|
|
|
|
|
|
|
|
|
|
|
|
distItems.sort((a, b) => {
|
|
|
|
|
|
const ia = FALLBACK_ORDER.indexOf(a.preset)
|
|
|
|
|
|
const ib = FALLBACK_ORDER.indexOf(b.preset)
|
|
|
|
|
|
return (ia === -1 ? 9999 : ia) - (ib === -1 ? 9999 : ib)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const result = [...items]
|
|
|
|
|
|
distIndexes.forEach((origIdx, i) => {
|
|
|
|
|
|
result[origIdx] = distItems[i]
|
|
|
|
|
|
})
|
|
|
|
|
|
return result
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-04 22:06:19 +08:00
|
|
|
|
function loadFromConfig(cfg: PoolAdvancedConfig | null): PresetListItem[] {
|
|
|
|
|
|
const defs = getPresetDefs()
|
|
|
|
|
|
const defsByName = new Map(defs.map(def => [def.name, def]))
|
|
|
|
|
|
const defaults = buildDefaultPresetList()
|
|
|
|
|
|
if (!cfg) return defaults
|
|
|
|
|
|
|
|
|
|
|
|
const rawPresets = cfg.scheduling_presets
|
|
|
|
|
|
if (!Array.isArray(rawPresets) || rawPresets.length === 0) {
|
2026-04-24 13:29:05 +08:00
|
|
|
|
if (cfg.scheduling_mode === 'lru' || cfg.lru_enabled === true) {
|
2026-03-04 22:06:19 +08:00
|
|
|
|
return defaults.map(item => ({
|
|
|
|
|
|
...item,
|
|
|
|
|
|
enabled: item.preset === 'lru',
|
|
|
|
|
|
}))
|
|
|
|
|
|
}
|
|
|
|
|
|
return defaults
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const first = rawPresets[0]
|
|
|
|
|
|
if (isNewFormatPresetItem(first)) {
|
|
|
|
|
|
const configItems = rawPresets as SchedulingPresetItem[]
|
|
|
|
|
|
const ordered: PresetListItem[] = []
|
|
|
|
|
|
const seen = new Set<string>()
|
|
|
|
|
|
|
|
|
|
|
|
for (const ci of configItems) {
|
|
|
|
|
|
const presetName = normalizePresetName(ci.preset)
|
|
|
|
|
|
const def = defsByName.get(presetName)
|
|
|
|
|
|
if (!def || seen.has(presetName)) continue
|
|
|
|
|
|
seen.add(presetName)
|
2026-03-05 23:22:25 +08:00
|
|
|
|
ordered.push(buildPresetListItem(def, ci.enabled !== false, ci.mode))
|
2026-03-04 22:06:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-05 23:22:25 +08:00
|
|
|
|
insertMissingByPreferredOrder(ordered, seen, defs, defsByName)
|
2026-07-27 22:06:28 +08:00
|
|
|
|
return reorderDistributionGroup(normalizeMutexSelection(ordered))
|
2026-03-04 22:06:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const legacyPresets = rawPresets as string[]
|
|
|
|
|
|
const lruEnabled = cfg.lru_enabled !== false
|
|
|
|
|
|
const ordered: PresetListItem[] = []
|
|
|
|
|
|
const seen = new Set<string>()
|
|
|
|
|
|
|
|
|
|
|
|
const lruDef = defsByName.get('lru')
|
|
|
|
|
|
if (lruDef) {
|
2026-03-05 23:22:25 +08:00
|
|
|
|
ordered.push(buildPresetListItem(lruDef, lruEnabled))
|
2026-03-04 22:06:19 +08:00
|
|
|
|
seen.add('lru')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
for (const name of legacyPresets) {
|
|
|
|
|
|
const presetName = normalizePresetName(name)
|
|
|
|
|
|
const def = defsByName.get(presetName)
|
|
|
|
|
|
if (!def || seen.has(presetName)) continue
|
|
|
|
|
|
seen.add(presetName)
|
2026-03-05 23:22:25 +08:00
|
|
|
|
ordered.push(buildPresetListItem(def, true, undefined))
|
2026-03-04 22:06:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-05 23:22:25 +08:00
|
|
|
|
insertMissingByPreferredOrder(ordered, seen, defs, defsByName)
|
2026-07-27 22:06:28 +08:00
|
|
|
|
return reorderDistributionGroup(normalizeMutexSelection(ordered))
|
2026-03-04 22:06:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-05 09:53:23 +08:00
|
|
|
|
function togglePreset(index: number, enabled: boolean) {
|
|
|
|
|
|
const item = presetList.value[index]
|
|
|
|
|
|
if (!item) return
|
2026-03-05 23:22:25 +08:00
|
|
|
|
item.enabled = enabled
|
|
|
|
|
|
}
|
2026-03-05 09:53:23 +08:00
|
|
|
|
|
2026-03-05 23:22:25 +08:00
|
|
|
|
function selectDistribution(_anchorIndex: number, presetName: string) {
|
|
|
|
|
|
presetList.value.forEach(item => {
|
|
|
|
|
|
if (item.mutexGroup === DISTRIBUTION_GROUP) {
|
|
|
|
|
|
item.enabled = item.preset === presetName && item.applicable
|
2026-03-05 09:53:23 +08:00
|
|
|
|
}
|
2026-03-05 23:22:25 +08:00
|
|
|
|
})
|
2026-03-04 22:06:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-05 09:53:23 +08:00
|
|
|
|
function setPresetModeByPreset(preset: string, mode: string) {
|
|
|
|
|
|
const targetIndex = presetList.value.findIndex(item => item.preset === preset)
|
|
|
|
|
|
if (targetIndex < 0) return
|
|
|
|
|
|
presetList.value[targetIndex].mode = mode
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-05 23:22:25 +08:00
|
|
|
|
const distributionItems = computed(() => {
|
|
|
|
|
|
const items: { index: number; item: PresetListItem }[] = []
|
2026-03-05 09:53:23 +08:00
|
|
|
|
presetList.value.forEach((item, index) => {
|
2026-03-05 23:22:25 +08:00
|
|
|
|
if (item.mutexGroup === DISTRIBUTION_GROUP) {
|
|
|
|
|
|
items.push({ index, item })
|
|
|
|
|
|
}
|
2026-03-05 09:53:23 +08:00
|
|
|
|
})
|
2026-03-05 23:22:25 +08:00
|
|
|
|
return items
|
2026-03-05 09:53:23 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
2026-03-05 23:22:25 +08:00
|
|
|
|
const activeDistributionPreset = computed(() => {
|
|
|
|
|
|
const found = distributionItems.value.find(({ item }) => item.enabled && item.applicable)
|
|
|
|
|
|
return found?.item.preset ?? null
|
|
|
|
|
|
})
|
2026-03-05 09:53:23 +08:00
|
|
|
|
|
2026-09-01 08:06:58 +08:00
|
|
|
|
const activeDistributionItem = computed(() => {
|
2026-03-05 23:22:25 +08:00
|
|
|
|
const found = distributionItems.value.find(({ item }) => item.enabled && item.applicable)
|
2026-09-01 08:06:58 +08:00
|
|
|
|
return found?.item ?? null
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const activeDistributionDesc = computed(() => {
|
|
|
|
|
|
return activeDistributionItem.value?.desc ?? null
|
2026-03-05 23:22:25 +08:00
|
|
|
|
})
|
2026-03-05 09:53:23 +08:00
|
|
|
|
|
2026-04-09 13:51:50 +08:00
|
|
|
|
const activeDistributionLabel = computed(() => {
|
2026-09-01 08:06:58 +08:00
|
|
|
|
return activeDistributionItem.value?.label ?? null
|
2026-04-09 13:51:50 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
2026-03-05 23:22:25 +08:00
|
|
|
|
const strategyItems = computed(() => {
|
|
|
|
|
|
const items: { index: number; item: PresetListItem }[] = []
|
|
|
|
|
|
presetList.value.forEach((item, index) => {
|
|
|
|
|
|
if (!item.mutexGroup) {
|
|
|
|
|
|
items.push({ index, item })
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
return items
|
|
|
|
|
|
})
|
2026-03-05 09:53:23 +08:00
|
|
|
|
|
2026-04-09 13:51:50 +08:00
|
|
|
|
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)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-04 22:06:19 +08:00
|
|
|
|
function handleDragStart(index: number, event: DragEvent) {
|
|
|
|
|
|
draggedIndex.value = index
|
|
|
|
|
|
if (event.dataTransfer) {
|
|
|
|
|
|
event.dataTransfer.effectAllowed = 'move'
|
|
|
|
|
|
event.dataTransfer.setData('text/html', '')
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function handleDragEnd() {
|
|
|
|
|
|
draggedIndex.value = null
|
|
|
|
|
|
dragOverIndex.value = null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function handleDragOver(index: number) {
|
|
|
|
|
|
dragOverIndex.value = index
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function handleDragLeave() {
|
|
|
|
|
|
dragOverIndex.value = null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function handleDrop(dropIndex: number) {
|
|
|
|
|
|
if (draggedIndex.value === null || draggedIndex.value === dropIndex) {
|
|
|
|
|
|
draggedIndex.value = null
|
|
|
|
|
|
dragOverIndex.value = null
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
const items = [...presetList.value]
|
|
|
|
|
|
const [draggedItem] = items.splice(draggedIndex.value, 1)
|
|
|
|
|
|
items.splice(dropIndex, 0, draggedItem)
|
|
|
|
|
|
presetList.value = items
|
|
|
|
|
|
draggedIndex.value = null
|
|
|
|
|
|
dragOverIndex.value = null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-27 22:06:28 +08:00
|
|
|
|
watch([() => props.modelValue, () => props.providerId], async ([open]) => {
|
|
|
|
|
|
const revision = ++dialogRevision
|
|
|
|
|
|
loading.value = false
|
2026-03-04 22:06:19 +08:00
|
|
|
|
if (!open) return
|
2026-09-23 14:44:29 +08:00
|
|
|
|
reserveMinimumQuota.value = props.currentConfig?.reserve_minimum_quota === true
|
2026-03-04 22:06:19 +08:00
|
|
|
|
await ensurePresetDefsLoaded()
|
2026-07-27 22:06:28 +08:00
|
|
|
|
if (!props.modelValue || dialogRevision !== revision) return
|
2026-03-05 09:53:23 +08:00
|
|
|
|
presetList.value = normalizeMutexSelection(loadFromConfig(props.currentConfig))
|
2026-03-04 22:06:19 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
async function handleSave() {
|
2026-07-27 22:06:28 +08:00
|
|
|
|
const providerId = props.providerId
|
|
|
|
|
|
const revision = dialogRevision
|
2026-03-04 22:06:19 +08:00
|
|
|
|
loading.value = true
|
|
|
|
|
|
try {
|
2026-07-27 22:06:28 +08:00
|
|
|
|
await ensurePresetDefsLoaded()
|
|
|
|
|
|
if (!props.modelValue || props.providerId !== providerId || dialogRevision !== revision) return
|
|
|
|
|
|
if (!presetDefsLoaded.value) {
|
|
|
|
|
|
showError('调度策略元数据加载失败,请重试')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-03-05 09:53:23 +08:00
|
|
|
|
presetList.value = normalizeMutexSelection(presetList.value)
|
2026-03-04 22:06:19 +08:00
|
|
|
|
const schedulingPresets: SchedulingPresetItem[] = presetList.value.map(item => {
|
|
|
|
|
|
const result: SchedulingPresetItem = {
|
|
|
|
|
|
preset: item.preset,
|
|
|
|
|
|
enabled: item.enabled && item.applicable,
|
|
|
|
|
|
}
|
|
|
|
|
|
if (item.modeOptions.length > 0 && item.mode) {
|
|
|
|
|
|
result.mode = item.mode
|
|
|
|
|
|
}
|
|
|
|
|
|
return result
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-07-27 22:06:28 +08:00
|
|
|
|
const latestProvider = await getProvider(providerId)
|
|
|
|
|
|
if (!props.modelValue || props.providerId !== providerId || dialogRevision !== revision) return
|
2026-09-07 21:14:27 +08:00
|
|
|
|
const latestAdvanced = latestProvider.pool_advanced
|
2026-07-27 22:06:28 +08:00
|
|
|
|
const mergedAdvanced = mergePoolAdvancedPatch(latestAdvanced, {
|
2026-03-05 15:15:26 +08:00
|
|
|
|
scheduling_presets: schedulingPresets,
|
2026-09-23 14:44:29 +08:00
|
|
|
|
...(isCodex.value ? { reserve_minimum_quota: reserveMinimumQuota.value } : {}),
|
2026-07-27 22:06:28 +08:00
|
|
|
|
})
|
2026-03-05 15:15:26 +08:00
|
|
|
|
const payload: Parameters<typeof updateProvider>[1] = {
|
|
|
|
|
|
pool_advanced: mergedAdvanced as PoolAdvancedConfig,
|
2026-03-04 22:06:19 +08:00
|
|
|
|
}
|
2026-07-27 22:06:28 +08:00
|
|
|
|
const updatedProvider = await updateProvider(providerId, payload)
|
|
|
|
|
|
if (!props.modelValue || props.providerId !== providerId || dialogRevision !== revision) return
|
2026-03-05 23:22:25 +08:00
|
|
|
|
|
2026-03-04 22:06:19 +08:00
|
|
|
|
success('号池调度已保存')
|
2026-03-05 09:53:23 +08:00
|
|
|
|
emit('saved', updatedProvider)
|
2026-03-04 22:06:19 +08:00
|
|
|
|
emit('update:modelValue', false)
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
showError(parseApiError(err))
|
|
|
|
|
|
} finally {
|
2026-07-27 22:06:28 +08:00
|
|
|
|
if (dialogRevision === revision) {
|
|
|
|
|
|
loading.value = false
|
|
|
|
|
|
}
|
2026-03-04 22:06:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</script>
|