mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-07 09:57:47 +08:00
feat: improve routing policy diagnostics
This commit is contained in:
@@ -14,9 +14,6 @@
|
||||
{{ group.description || '未填写描述' }}
|
||||
</p>
|
||||
</div>
|
||||
<span class="shrink-0 rounded-md border px-2 py-1 text-xs text-muted-foreground">
|
||||
v{{ group.version }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -28,7 +25,6 @@ export interface RoutingGroupListItem {
|
||||
name: string
|
||||
description?: string | null
|
||||
enabled: boolean
|
||||
version: number
|
||||
}
|
||||
|
||||
defineProps<{
|
||||
|
||||
@@ -69,37 +69,35 @@
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<select
|
||||
<button
|
||||
v-if="effectivePriorityMode === 'provider'"
|
||||
type="button"
|
||||
class="inline-flex h-8 items-center gap-2 rounded-md px-3 text-xs font-medium transition-colors"
|
||||
:class="providerMultiSelectEnabled
|
||||
? 'bg-primary/10 text-primary hover:bg-primary/10 hover:text-primary'
|
||||
: 'text-muted-foreground hover:bg-muted hover:text-foreground'"
|
||||
@click="toggleProviderMultiSelect"
|
||||
>
|
||||
<ListChecks class="h-3.5 w-3.5" />
|
||||
{{ providerMultiSelectEnabled ? '退出多选' : '多选' }}
|
||||
</button>
|
||||
<Select
|
||||
v-if="effectivePriorityMode === 'global_key'"
|
||||
v-model="selectedApiFormat"
|
||||
class="h-9 min-w-[180px] rounded-md border border-border bg-background px-3 text-sm"
|
||||
>
|
||||
<option
|
||||
v-for="format in apiFormats"
|
||||
:key="format"
|
||||
:value="format"
|
||||
>
|
||||
{{ formatLabel(format) }}
|
||||
</option>
|
||||
</select>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex h-9 items-center gap-2 rounded-md border border-border px-3 text-xs"
|
||||
@click="refresh"
|
||||
>
|
||||
<RefreshCw
|
||||
class="h-3.5 w-3.5"
|
||||
:class="{ 'animate-spin': loading }"
|
||||
/>
|
||||
刷新
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="h-9 rounded-md border border-border px-3 text-xs text-muted-foreground"
|
||||
@click="clearActiveOverrides"
|
||||
>
|
||||
清空排序
|
||||
</button>
|
||||
<SelectTrigger class="h-8 w-[180px] rounded-lg border-border/60 bg-background/80 px-3 text-xs">
|
||||
<SelectValue placeholder="选择端点" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem
|
||||
v-for="format in apiFormats"
|
||||
:key="format"
|
||||
:value="format"
|
||||
>
|
||||
{{ formatLabel(format) }}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -131,12 +129,8 @@
|
||||
v-for="(row, index) in providerRows"
|
||||
v-else
|
||||
:key="row.id"
|
||||
class="group grid items-center gap-3 rounded-lg border px-3 py-2 transition-colors sm:grid-cols-[auto_auto_76px_minmax(0,1fr)_auto]"
|
||||
:class="draggedProviderId === row.id
|
||||
? 'border-primary/50 bg-primary/5 shadow-sm'
|
||||
: dragOverProviderId === row.id
|
||||
? 'border-primary/30 bg-primary/5'
|
||||
: 'border-border/50 bg-background hover:bg-muted/30'"
|
||||
class="group grid min-h-[56px] items-center gap-3 rounded-lg border px-3 py-2 transition-colors"
|
||||
:class="[providerGridClass, providerRowClass(row.id)]"
|
||||
draggable="true"
|
||||
@dragstart="handleProviderDragStart(row.id, $event)"
|
||||
@dragend="handleProviderDragEnd"
|
||||
@@ -144,6 +138,15 @@
|
||||
@dragleave="handleProviderDragLeave"
|
||||
@drop="handleProviderDrop(row.id)"
|
||||
>
|
||||
<Checkbox
|
||||
v-if="providerMultiSelectEnabled"
|
||||
class="shrink-0"
|
||||
:checked="isProviderSelected(row.id)"
|
||||
:aria-label="`选择 ${row.name}`"
|
||||
@click.stop
|
||||
@change.stop
|
||||
@update:checked="checked => setProviderSelected(row.id, checked)"
|
||||
/>
|
||||
<div class="cursor-grab rounded p-1 text-muted-foreground/40 transition-colors group-hover:text-muted-foreground active:cursor-grabbing">
|
||||
<GripVertical class="h-4 w-4" />
|
||||
</div>
|
||||
@@ -151,7 +154,7 @@
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-md p-1 text-muted-foreground hover:bg-muted hover:text-foreground disabled:opacity-30"
|
||||
:disabled="index === 0"
|
||||
:disabled="providerMoveDisabled(row.id, index, -1)"
|
||||
@click="moveProvider(row.id, -1)"
|
||||
>
|
||||
<ArrowUp class="h-4 w-4" />
|
||||
@@ -159,7 +162,7 @@
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-md p-1 text-muted-foreground hover:bg-muted hover:text-foreground disabled:opacity-30"
|
||||
:disabled="index === providerRows.length - 1"
|
||||
:disabled="providerMoveDisabled(row.id, index, 1)"
|
||||
@click="moveProvider(row.id, 1)"
|
||||
>
|
||||
<ArrowDown class="h-4 w-4" />
|
||||
@@ -169,7 +172,7 @@
|
||||
:value="row.priority"
|
||||
type="number"
|
||||
min="0"
|
||||
class="h-8 w-full rounded-md border border-border bg-background px-2 text-sm"
|
||||
class="priority-input h-8 w-14 rounded-md border border-border bg-background px-2 text-center text-sm"
|
||||
@change="event => setProviderPriority(row.id, event)"
|
||||
>
|
||||
<div class="min-w-0">
|
||||
@@ -188,17 +191,15 @@
|
||||
停用
|
||||
</span>
|
||||
</div>
|
||||
<div class="mt-0.5 truncate text-xs text-muted-foreground">
|
||||
{{ row.id }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="hidden max-w-[240px] flex-wrap justify-end gap-1 sm:flex">
|
||||
<span
|
||||
v-for="format in row.api_formats.slice(0, 3)"
|
||||
:key="format"
|
||||
:title="formatLabel(format)"
|
||||
class="rounded bg-muted px-1.5 py-0.5 text-[10px] text-muted-foreground"
|
||||
>
|
||||
{{ format }}
|
||||
{{ formatShortLabel(format) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -218,7 +219,7 @@
|
||||
v-for="(row, index) in keyRows"
|
||||
v-else
|
||||
:key="row.id"
|
||||
class="group grid items-center gap-3 rounded-lg border px-3 py-2 transition-colors sm:grid-cols-[auto_auto_76px_minmax(0,1fr)_auto]"
|
||||
class="group grid min-h-[56px] items-center gap-3 rounded-lg border px-3 py-2 transition-colors sm:grid-cols-[auto_auto_56px_minmax(0,1fr)_auto]"
|
||||
:class="draggedKeyId === row.id
|
||||
? 'border-primary/50 bg-primary/5 shadow-sm'
|
||||
: dragOverKeyId === row.id
|
||||
@@ -256,7 +257,7 @@
|
||||
:value="row.priority"
|
||||
type="number"
|
||||
min="0"
|
||||
class="h-8 w-full rounded-md border border-border bg-background px-2 text-sm"
|
||||
class="priority-input h-8 w-14 rounded-md border border-border bg-background px-2 text-center text-sm"
|
||||
@change="event => setKeyPriority(row.id, event)"
|
||||
>
|
||||
<div class="min-w-0">
|
||||
@@ -277,9 +278,10 @@
|
||||
<span
|
||||
v-for="format in row.api_formats.slice(0, 3)"
|
||||
:key="format"
|
||||
:title="formatLabel(format)"
|
||||
class="rounded bg-muted px-1.5 py-0.5 text-[10px] text-muted-foreground"
|
||||
>
|
||||
{{ format }}
|
||||
{{ formatShortLabel(format) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -291,14 +293,22 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { ArrowDown, ArrowUp, GripVertical, Key, Layers, RefreshCw } from 'lucide-vue-next'
|
||||
import { ArrowDown, ArrowUp, GripVertical, Key, Layers, ListChecks } from 'lucide-vue-next'
|
||||
|
||||
import client from '@/api/client'
|
||||
import {
|
||||
Checkbox,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui'
|
||||
import {
|
||||
getProvidersSummary,
|
||||
type ProviderWithEndpointsSummary,
|
||||
} from '@/api/endpoints'
|
||||
import { formatApiFormat, normalizeApiFormatAlias, sortApiFormats } from '@/api/endpoints/types/api-format'
|
||||
import { formatApiFormat, formatApiFormatShort, normalizeApiFormatAlias, sortApiFormats } from '@/api/endpoints/types/api-format'
|
||||
import { parseApiError } from '@/utils/errorParser'
|
||||
import {
|
||||
DEFAULT_ROUTING_POLICY_MODEL,
|
||||
@@ -385,6 +395,8 @@ const draggedProviderId = ref<string | null>(null)
|
||||
const dragOverProviderId = ref<string | null>(null)
|
||||
const draggedKeyId = ref<string | null>(null)
|
||||
const dragOverKeyId = ref<string | null>(null)
|
||||
const providerMultiSelectEnabled = ref(false)
|
||||
const selectedProviderIds = ref<Set<string>>(new Set())
|
||||
|
||||
const config = computed(() => normalizeRoutingGroupConfig(props.config))
|
||||
const targetModel = computed(() => props.model?.trim() || DEFAULT_ROUTING_POLICY_MODEL)
|
||||
@@ -398,6 +410,9 @@ const effectiveSchedulingMode = computed(() => props.schedulingMode ?? config.va
|
||||
const subtitle = computed(() => props.subtitle ?? '默认作用于全部模型')
|
||||
const loading = computed(() => loadingProviders.value || loadingKeys.value)
|
||||
const apiFormats = computed(() => sortApiFormats(Object.keys(keysByFormat.value)))
|
||||
const providerGridClass = computed(() => providerMultiSelectEnabled.value
|
||||
? 'sm:grid-cols-[auto_auto_auto_56px_minmax(0,1fr)_auto]'
|
||||
: 'sm:grid-cols-[auto_auto_56px_minmax(0,1fr)_auto]')
|
||||
const providerById = computed(() => {
|
||||
const map = new Map<string, ProviderWithEndpointsSummary>()
|
||||
for (const provider of providers.value) {
|
||||
@@ -477,6 +492,16 @@ const keyRows = computed<KeyPriorityRow[]>(() => {
|
||||
watch(effectivePriorityMode, mode => {
|
||||
if (mode === 'global_key') {
|
||||
void loadGlobalKeys()
|
||||
providerMultiSelectEnabled.value = false
|
||||
selectedProviderIds.value = new Set()
|
||||
}
|
||||
})
|
||||
|
||||
watch(providerRows, rows => {
|
||||
const visibleIds = new Set(rows.map(row => row.id))
|
||||
const next = new Set([...selectedProviderIds.value].filter(id => visibleIds.has(id)))
|
||||
if (next.size !== selectedProviderIds.value.size) {
|
||||
selectedProviderIds.value = next
|
||||
}
|
||||
})
|
||||
|
||||
@@ -525,15 +550,6 @@ function updateSchedulingMode(mode: RoutingSchedulingMode): void {
|
||||
updateDefaultPolicy({ scheduling_mode: mode })
|
||||
}
|
||||
|
||||
async function refresh(): Promise<void> {
|
||||
if (effectivePriorityMode.value === 'provider') {
|
||||
await loadProviders()
|
||||
} else {
|
||||
await loadProviders()
|
||||
await loadGlobalKeys(true)
|
||||
}
|
||||
}
|
||||
|
||||
async function loadProviders(): Promise<void> {
|
||||
loadingProviders.value = true
|
||||
loadError.value = null
|
||||
@@ -583,7 +599,10 @@ function setProviderPriority(providerId: string, event: Event): void {
|
||||
}
|
||||
|
||||
function moveProvider(providerId: string, direction: -1 | 1): void {
|
||||
const rows = moveRow(providerRows.value, providerId, direction)
|
||||
const movingIds = providerMoveIds(providerId)
|
||||
const rows = movingIds.length > 1
|
||||
? moveRowsByGroup(providerRows.value, movingIds, direction)
|
||||
: moveRow(providerRows.value, providerId, direction)
|
||||
updateProviderOverrides(Object.fromEntries(rows.map((row, index) => [row.id, index])))
|
||||
}
|
||||
|
||||
@@ -591,6 +610,70 @@ function updateProviderOverrides(overrides: Record<string, number>): void {
|
||||
updateConfig(setModelProviderPriorityOverrides(config.value, targetModel.value, overrides))
|
||||
}
|
||||
|
||||
function isProviderSelected(providerId: string): boolean {
|
||||
return providerMultiSelectEnabled.value && selectedProviderIds.value.has(providerId)
|
||||
}
|
||||
|
||||
function setProviderSelected(providerId: string, selected: boolean): void {
|
||||
if (!providerMultiSelectEnabled.value) return
|
||||
const next = new Set(selectedProviderIds.value)
|
||||
if (selected) {
|
||||
next.add(providerId)
|
||||
} else {
|
||||
next.delete(providerId)
|
||||
}
|
||||
selectedProviderIds.value = next
|
||||
}
|
||||
|
||||
function toggleProviderMultiSelect(): void {
|
||||
providerMultiSelectEnabled.value = !providerMultiSelectEnabled.value
|
||||
if (!providerMultiSelectEnabled.value) {
|
||||
selectedProviderIds.value = new Set()
|
||||
}
|
||||
}
|
||||
|
||||
function providerMoveIds(providerId: string): string[] {
|
||||
if (!providerMultiSelectEnabled.value || !selectedProviderIds.value.has(providerId)) {
|
||||
return [providerId]
|
||||
}
|
||||
return providerRows.value
|
||||
.map(row => row.id)
|
||||
.filter(id => selectedProviderIds.value.has(id))
|
||||
}
|
||||
|
||||
function providerMoveDisabled(providerId: string, index: number, direction: -1 | 1): boolean {
|
||||
const movingIds = providerMoveIds(providerId)
|
||||
if (movingIds.length <= 1) {
|
||||
return direction === -1 ? index === 0 : index === providerRows.value.length - 1
|
||||
}
|
||||
const movingSet = new Set(movingIds)
|
||||
const movingIndexes = providerRows.value
|
||||
.map((row, rowIndex) => movingSet.has(row.id) ? rowIndex : -1)
|
||||
.filter(rowIndex => rowIndex >= 0)
|
||||
if (movingIndexes.length === 0) return true
|
||||
return direction === -1
|
||||
? Math.min(...movingIndexes) === 0
|
||||
: Math.max(...movingIndexes) === providerRows.value.length - 1
|
||||
}
|
||||
|
||||
function providerRowClass(providerId: string): string {
|
||||
if (isProviderDragged(providerId)) {
|
||||
return 'border-primary/50 bg-primary/5 shadow-sm'
|
||||
}
|
||||
if (dragOverProviderId.value === providerId) {
|
||||
return 'border-primary/30 bg-primary/5'
|
||||
}
|
||||
if (isProviderSelected(providerId)) {
|
||||
return 'border-primary/40 bg-primary/5'
|
||||
}
|
||||
return 'border-border/50 bg-background hover:bg-muted/30'
|
||||
}
|
||||
|
||||
function isProviderDragged(providerId: string): boolean {
|
||||
const draggedId = draggedProviderId.value
|
||||
return Boolean(draggedId && providerMoveIds(draggedId).includes(providerId))
|
||||
}
|
||||
|
||||
function setKeyPriority(keyId: string, event: Event): void {
|
||||
const priority = readPriorityInput(event)
|
||||
if (priority == null) return
|
||||
@@ -684,7 +767,14 @@ function handleProviderDrop(providerId: string): void {
|
||||
handleProviderDragEnd()
|
||||
return
|
||||
}
|
||||
const rows = reorderRows(providerRows.value, draggedId, providerId)
|
||||
const movingIds = providerMoveIds(draggedId)
|
||||
if (movingIds.includes(providerId)) {
|
||||
handleProviderDragEnd()
|
||||
return
|
||||
}
|
||||
const rows = movingIds.length > 1
|
||||
? reorderRowsByGroup(providerRows.value, movingIds, providerId)
|
||||
: reorderRows(providerRows.value, draggedId, providerId)
|
||||
updateProviderOverrides(Object.fromEntries(rows.map((row, index) => [row.id, index])))
|
||||
handleProviderDragEnd()
|
||||
}
|
||||
@@ -721,14 +811,6 @@ function handleKeyDrop(keyId: string): void {
|
||||
handleKeyDragEnd()
|
||||
}
|
||||
|
||||
function clearActiveOverrides(): void {
|
||||
if (effectivePriorityMode.value === 'provider') {
|
||||
updateProviderOverrides({})
|
||||
} else {
|
||||
updateVisibleKeyAndPoolOverrides([])
|
||||
}
|
||||
}
|
||||
|
||||
function moveRow<T extends { id: string }>(rows: T[], id: string, direction: -1 | 1): T[] {
|
||||
const next = [...rows]
|
||||
const index = next.findIndex(row => row.id === id)
|
||||
@@ -741,6 +823,26 @@ function moveRow<T extends { id: string }>(rows: T[], id: string, direction: -1
|
||||
return next
|
||||
}
|
||||
|
||||
function moveRowsByGroup<T extends { id: string }>(rows: T[], movingIds: string[], direction: -1 | 1): T[] {
|
||||
const movingSet = new Set(movingIds)
|
||||
const movingRows = rows.filter(row => movingSet.has(row.id))
|
||||
if (movingRows.length === 0) return [...rows]
|
||||
|
||||
const firstMovingIndex = rows.findIndex(row => movingSet.has(row.id))
|
||||
const remainingRows = rows.filter(row => !movingSet.has(row.id))
|
||||
const baseInsertIndex = rows
|
||||
.slice(0, firstMovingIndex)
|
||||
.filter(row => !movingSet.has(row.id))
|
||||
.length
|
||||
const insertIndex = direction === -1
|
||||
? Math.max(0, baseInsertIndex - 1)
|
||||
: Math.min(remainingRows.length, baseInsertIndex + 1)
|
||||
|
||||
const next = [...remainingRows]
|
||||
next.splice(insertIndex, 0, ...movingRows)
|
||||
return next
|
||||
}
|
||||
|
||||
function reorderRows<T extends { id: string }>(rows: T[], draggedId: string, targetId: string): T[] {
|
||||
const next = [...rows]
|
||||
const fromIndex = next.findIndex(row => row.id === draggedId)
|
||||
@@ -751,6 +853,29 @@ function reorderRows<T extends { id: string }>(rows: T[], draggedId: string, tar
|
||||
return next
|
||||
}
|
||||
|
||||
function reorderRowsByGroup<T extends { id: string }>(rows: T[], movingIds: string[], targetId: string): T[] {
|
||||
const movingSet = new Set(movingIds)
|
||||
if (movingSet.has(targetId)) return [...rows]
|
||||
|
||||
const movingRows = rows.filter(row => movingSet.has(row.id))
|
||||
if (movingRows.length === 0) return [...rows]
|
||||
|
||||
const targetIndex = rows.findIndex(row => row.id === targetId)
|
||||
const firstMovingIndex = rows.findIndex(row => movingSet.has(row.id))
|
||||
if (targetIndex < 0 || firstMovingIndex < 0) return [...rows]
|
||||
|
||||
const remainingRows = rows.filter(row => !movingSet.has(row.id))
|
||||
const remainingTargetIndex = remainingRows.findIndex(row => row.id === targetId)
|
||||
if (remainingTargetIndex < 0) return [...rows]
|
||||
|
||||
const insertIndex = firstMovingIndex < targetIndex
|
||||
? remainingTargetIndex + 1
|
||||
: remainingTargetIndex
|
||||
const next = [...remainingRows]
|
||||
next.splice(insertIndex, 0, ...movingRows)
|
||||
return next
|
||||
}
|
||||
|
||||
function readPriorityInput(event: Event): number | null {
|
||||
const value = Number((event.target as HTMLInputElement).value)
|
||||
if (!Number.isFinite(value) || value < 0) {
|
||||
@@ -858,6 +983,10 @@ function formatLabel(format: string): string {
|
||||
return formatApiFormat(format)
|
||||
}
|
||||
|
||||
function formatShortLabel(format: string): string {
|
||||
return formatApiFormatShort(format)
|
||||
}
|
||||
|
||||
function normalizePriorityMap(value: Record<string, unknown> | null | undefined): Record<string, number> {
|
||||
if (!value) return {}
|
||||
const normalized: Record<string, number> = {}
|
||||
@@ -882,3 +1011,15 @@ function comparePriorityRows(left: ProviderPriorityRow | KeyPriorityRow, right:
|
||||
|| left.id.localeCompare(right.id)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.priority-input::-webkit-outer-spin-button,
|
||||
.priority-input::-webkit-inner-spin-button {
|
||||
margin: 0;
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
.priority-input[type='number'] {
|
||||
appearance: textfield;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user