mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-13 04:29:50 +08:00
refactor(pool): 移除调度多维评分详情,简化为 reasons 摘要
移除 scheduling_score、candidate_eligible、scheduling_dimensions 等字段, 前后端统一使用 scheduling_reasons 展示调度状态,精简号池列表信息密度。
This commit is contained in:
@@ -142,11 +142,6 @@ export interface PoolKeyDetail {
|
|||||||
| string
|
| string
|
||||||
scheduling_label?: string
|
scheduling_label?: string
|
||||||
scheduling_reasons?: PoolSchedulingReason[]
|
scheduling_reasons?: PoolSchedulingReason[]
|
||||||
scheduling_score?: number
|
|
||||||
candidate_eligible?: boolean
|
|
||||||
scheduling_blocked_count?: number
|
|
||||||
scheduling_degraded_count?: number
|
|
||||||
scheduling_dimensions?: PoolSchedulingDimension[]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PoolSchedulingReason {
|
export interface PoolSchedulingReason {
|
||||||
@@ -158,18 +153,6 @@ export interface PoolSchedulingReason {
|
|||||||
detail?: string | null
|
detail?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PoolSchedulingDimension {
|
|
||||||
code: string
|
|
||||||
label: string
|
|
||||||
status: 'ok' | 'degraded' | 'blocked' | string
|
|
||||||
blocking: boolean
|
|
||||||
source: 'manual' | 'pool' | 'health' | 'policy' | string
|
|
||||||
weight: number
|
|
||||||
score: number
|
|
||||||
ttl_seconds?: number | null
|
|
||||||
detail?: string | null
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PoolKeysPageResponse {
|
export interface PoolKeysPageResponse {
|
||||||
total: number
|
total: number
|
||||||
page: number
|
page: number
|
||||||
|
|||||||
@@ -291,9 +291,6 @@
|
|||||||
>
|
>
|
||||||
配额
|
配额
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead class="w-[180px] font-semibold whitespace-nowrap">
|
|
||||||
调度
|
|
||||||
</TableHead>
|
|
||||||
<TableHead class="w-24 font-semibold whitespace-nowrap">
|
<TableHead class="w-24 font-semibold whitespace-nowrap">
|
||||||
状态
|
状态
|
||||||
</TableHead>
|
</TableHead>
|
||||||
@@ -439,95 +436,6 @@
|
|||||||
class="text-xs text-muted-foreground"
|
class="text-xs text-muted-foreground"
|
||||||
>-</span>
|
>-</span>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell class="py-3">
|
|
||||||
<div
|
|
||||||
class="grid grid-rows-[16px_16px] gap-1 w-20"
|
|
||||||
:title="getSchedulingDimensionTitle(key)"
|
|
||||||
>
|
|
||||||
<div class="h-4 flex items-center justify-between gap-1">
|
|
||||||
<span
|
|
||||||
class="text-[11px] font-normal leading-none tabular-nums"
|
|
||||||
:class="isCandidateEligible(key) ? 'text-foreground' : 'text-destructive'"
|
|
||||||
>
|
|
||||||
{{ getSchedulingScore(key).toFixed(1) }}
|
|
||||||
</span>
|
|
||||||
<Popover
|
|
||||||
:open="schedulingDetailDesktopPopoverOpenKeyId === key.key_id"
|
|
||||||
@update:open="(v: boolean) => handleSchedulingDetailDesktopPopoverToggle(key.key_id, v)"
|
|
||||||
>
|
|
||||||
<PopoverTrigger as-child>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="inline-flex h-4 w-4 items-center justify-center text-muted-foreground hover:text-foreground"
|
|
||||||
title="查看计算详情"
|
|
||||||
@click.stop
|
|
||||||
>
|
|
||||||
<CircleHelp class="w-3 h-3" />
|
|
||||||
</button>
|
|
||||||
</PopoverTrigger>
|
|
||||||
<PopoverContent
|
|
||||||
class="w-72 p-3"
|
|
||||||
side="bottom"
|
|
||||||
align="start"
|
|
||||||
>
|
|
||||||
<div class="space-y-2">
|
|
||||||
<div class="text-xs font-medium">
|
|
||||||
调度分计算详情
|
|
||||||
</div>
|
|
||||||
<div class="max-h-56 overflow-y-auto space-y-2 pr-1">
|
|
||||||
<div
|
|
||||||
v-for="item in getSchedulingRuleEntries(key)"
|
|
||||||
:key="`${key.key_id}-score-detail-${item.code}`"
|
|
||||||
class="rounded-md border border-border/60 p-2"
|
|
||||||
>
|
|
||||||
<div class="flex items-center justify-between gap-2">
|
|
||||||
<span class="text-[11px] font-medium">
|
|
||||||
{{ item.label }}
|
|
||||||
</span>
|
|
||||||
<Badge
|
|
||||||
:variant="getSchedulingDimensionStatusVariant(item.status)"
|
|
||||||
class="text-[10px]"
|
|
||||||
>
|
|
||||||
{{ getSchedulingDimensionStatusLabel(item.status) }}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="item.weight != null || item.score != null"
|
|
||||||
class="text-[10px] text-muted-foreground mt-1"
|
|
||||||
>
|
|
||||||
<span v-if="item.weight != null">权重 {{ item.weight }}</span>
|
|
||||||
<span v-if="item.weight != null && item.score != null"> · </span>
|
|
||||||
<span v-if="item.score != null">分值 {{ Math.round(item.score * 100) }}</span>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="item.detail"
|
|
||||||
class="text-[10px] text-muted-foreground mt-1 break-all"
|
|
||||||
>
|
|
||||||
{{ item.detail }}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="item.ttl_seconds && item.ttl_seconds > 0"
|
|
||||||
class="text-[10px] text-muted-foreground mt-1"
|
|
||||||
>
|
|
||||||
剩余 {{ formatTTL(item.ttl_seconds) }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
</div>
|
|
||||||
<div class="h-4 flex items-center">
|
|
||||||
<div class="w-full h-1.5 bg-border rounded-full overflow-hidden">
|
|
||||||
<div
|
|
||||||
class="h-full transition-all duration-300"
|
|
||||||
:class="getSchedulingScoreBarColor(getSchedulingScore(key))"
|
|
||||||
:style="{ width: `${Math.max(Math.min(getSchedulingScore(key), 100), 0)}%` }"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell class="py-3">
|
<TableCell class="py-3">
|
||||||
<Badge
|
<Badge
|
||||||
:variant="getSchedulingBadgeVariant(key)"
|
:variant="getSchedulingBadgeVariant(key)"
|
||||||
@@ -896,94 +804,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="mt-2.5 grid gap-2"
|
class="mt-2.5 grid gap-2"
|
||||||
:class="showAccountQuotaColumn ? 'grid-cols-2 sm:grid-cols-3 lg:grid-cols-5' : 'grid-cols-2 sm:grid-cols-4'"
|
:class="showAccountQuotaColumn ? 'grid-cols-2 sm:grid-cols-3 lg:grid-cols-4' : 'grid-cols-2 sm:grid-cols-3'"
|
||||||
>
|
>
|
||||||
<div class="p-2 bg-muted/50 rounded-lg text-xs">
|
|
||||||
<div class="text-muted-foreground mb-0.5">
|
|
||||||
调度
|
|
||||||
</div>
|
|
||||||
<div class="w-24 flex items-center justify-between gap-1">
|
|
||||||
<div
|
|
||||||
class="font-normal tabular-nums text-[10px] leading-none"
|
|
||||||
:class="isCandidateEligible(key) ? 'text-foreground' : 'text-destructive'"
|
|
||||||
:title="getSchedulingDimensionTitle(key)"
|
|
||||||
>
|
|
||||||
{{ getSchedulingScore(key).toFixed(1) }}
|
|
||||||
</div>
|
|
||||||
<Popover
|
|
||||||
:open="schedulingDetailMobilePopoverOpenKeyId === key.key_id"
|
|
||||||
@update:open="(v: boolean) => handleSchedulingDetailMobilePopoverToggle(key.key_id, v)"
|
|
||||||
>
|
|
||||||
<PopoverTrigger as-child>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="inline-flex h-4 w-4 items-center justify-center text-muted-foreground hover:text-foreground"
|
|
||||||
title="查看计算详情"
|
|
||||||
@click.stop
|
|
||||||
>
|
|
||||||
<CircleHelp class="w-3 h-3" />
|
|
||||||
</button>
|
|
||||||
</PopoverTrigger>
|
|
||||||
<PopoverContent
|
|
||||||
class="w-72 p-3"
|
|
||||||
side="bottom"
|
|
||||||
align="start"
|
|
||||||
>
|
|
||||||
<div class="space-y-2">
|
|
||||||
<div class="text-xs font-medium">
|
|
||||||
调度分计算详情
|
|
||||||
</div>
|
|
||||||
<div class="max-h-56 overflow-y-auto space-y-2 pr-1">
|
|
||||||
<div
|
|
||||||
v-for="item in getSchedulingRuleEntries(key)"
|
|
||||||
:key="`${key.key_id}-mobile-score-detail-${item.code}`"
|
|
||||||
class="rounded-md border border-border/60 p-2"
|
|
||||||
>
|
|
||||||
<div class="flex items-center justify-between gap-2">
|
|
||||||
<span class="text-[11px] font-medium">
|
|
||||||
{{ item.label }}
|
|
||||||
</span>
|
|
||||||
<Badge
|
|
||||||
:variant="getSchedulingDimensionStatusVariant(item.status)"
|
|
||||||
class="text-[10px]"
|
|
||||||
>
|
|
||||||
{{ getSchedulingDimensionStatusLabel(item.status) }}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="item.weight != null || item.score != null"
|
|
||||||
class="text-[10px] text-muted-foreground mt-1"
|
|
||||||
>
|
|
||||||
<span v-if="item.weight != null">权重 {{ item.weight }}</span>
|
|
||||||
<span v-if="item.weight != null && item.score != null"> · </span>
|
|
||||||
<span v-if="item.score != null">分值 {{ Math.round(item.score * 100) }}</span>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="item.detail"
|
|
||||||
class="text-[10px] text-muted-foreground mt-1 break-all"
|
|
||||||
>
|
|
||||||
{{ item.detail }}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="item.ttl_seconds && item.ttl_seconds > 0"
|
|
||||||
class="text-[10px] text-muted-foreground mt-1"
|
|
||||||
>
|
|
||||||
剩余 {{ formatTTL(item.ttl_seconds) }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
</div>
|
|
||||||
<div class="w-24 h-1.5 bg-border rounded-full overflow-hidden mt-1">
|
|
||||||
<div
|
|
||||||
class="h-full transition-all duration-300"
|
|
||||||
:class="getSchedulingScoreBarColor(getSchedulingScore(key))"
|
|
||||||
:style="{ width: `${Math.max(Math.min(getSchedulingScore(key), 100), 0)}%` }"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="p-2 bg-muted/50 rounded-lg text-xs">
|
<div class="p-2 bg-muted/50 rounded-lg text-xs">
|
||||||
<div class="text-muted-foreground mb-0.5">
|
<div class="text-muted-foreground mb-0.5">
|
||||||
最后使用
|
最后使用
|
||||||
@@ -1162,7 +984,6 @@ import {
|
|||||||
Globe,
|
Globe,
|
||||||
SquarePen,
|
SquarePen,
|
||||||
Trash2,
|
Trash2,
|
||||||
CircleHelp,
|
|
||||||
Ban,
|
Ban,
|
||||||
} from 'lucide-vue-next'
|
} from 'lucide-vue-next'
|
||||||
|
|
||||||
@@ -1392,8 +1213,6 @@ async function selectProvider(id: string) {
|
|||||||
oauthKeyEditDialogOpen.value = false
|
oauthKeyEditDialogOpen.value = false
|
||||||
proxyDesktopPopoverOpenKeyId.value = null
|
proxyDesktopPopoverOpenKeyId.value = null
|
||||||
proxyMobilePopoverOpenKeyId.value = null
|
proxyMobilePopoverOpenKeyId.value = null
|
||||||
schedulingDetailDesktopPopoverOpenKeyId.value = null
|
|
||||||
schedulingDetailMobilePopoverOpenKeyId.value = null
|
|
||||||
suppressFiltersWatch = true
|
suppressFiltersWatch = true
|
||||||
currentPage.value = 1
|
currentPage.value = 1
|
||||||
searchQuery.value = ''
|
searchQuery.value = ''
|
||||||
@@ -1438,8 +1257,6 @@ const recoveringHealthKeyId = ref<string | null>(null)
|
|||||||
const savingProxyKeyId = ref<string | null>(null)
|
const savingProxyKeyId = ref<string | null>(null)
|
||||||
const proxyDesktopPopoverOpenKeyId = ref<string | null>(null)
|
const proxyDesktopPopoverOpenKeyId = ref<string | null>(null)
|
||||||
const proxyMobilePopoverOpenKeyId = ref<string | null>(null)
|
const proxyMobilePopoverOpenKeyId = ref<string | null>(null)
|
||||||
const schedulingDetailDesktopPopoverOpenKeyId = ref<string | null>(null)
|
|
||||||
const schedulingDetailMobilePopoverOpenKeyId = ref<string | null>(null)
|
|
||||||
const deletingKeyId = ref<string | null>(null)
|
const deletingKeyId = ref<string | null>(null)
|
||||||
const togglingKeyId = ref<string | null>(null)
|
const togglingKeyId = ref<string | null>(null)
|
||||||
|
|
||||||
@@ -1753,20 +1570,6 @@ function handleProxyMobilePopoverToggle(keyId: string, open: boolean) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSchedulingDetailDesktopPopoverToggle(keyId: string, open: boolean) {
|
|
||||||
schedulingDetailDesktopPopoverOpenKeyId.value = open ? keyId : null
|
|
||||||
if (open) {
|
|
||||||
schedulingDetailMobilePopoverOpenKeyId.value = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleSchedulingDetailMobilePopoverToggle(keyId: string, open: boolean) {
|
|
||||||
schedulingDetailMobilePopoverOpenKeyId.value = open ? keyId : null
|
|
||||||
if (open) {
|
|
||||||
schedulingDetailDesktopPopoverOpenKeyId.value = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function setKeyProxy(key: PoolKeyDetail, nodeId: string) {
|
async function setKeyProxy(key: PoolKeyDetail, nodeId: string) {
|
||||||
savingProxyKeyId.value = key.key_id
|
savingProxyKeyId.value = key.key_id
|
||||||
try {
|
try {
|
||||||
@@ -2050,14 +1853,6 @@ function getSchedulingTitle(key: PoolKeyDetail): string {
|
|||||||
const accountAlertTitle = getAccountAlertTitle(key)
|
const accountAlertTitle = getAccountAlertTitle(key)
|
||||||
if (accountAlertTitle) return accountAlertTitle
|
if (accountAlertTitle) return accountAlertTitle
|
||||||
|
|
||||||
if (key.scheduling_dimensions && key.scheduling_dimensions.length > 0) {
|
|
||||||
return key.scheduling_dimensions.map((item) => {
|
|
||||||
const ttl = item.ttl_seconds && item.ttl_seconds > 0 ? ` (${formatTTL(item.ttl_seconds)})` : ''
|
|
||||||
const detail = item.detail ? ` - ${item.detail}` : ''
|
|
||||||
return `${item.label}: ${item.status}${ttl}${detail}`
|
|
||||||
}).join('\n')
|
|
||||||
}
|
|
||||||
|
|
||||||
const reasons = key.scheduling_reasons ?? []
|
const reasons = key.scheduling_reasons ?? []
|
||||||
if (reasons.length > 0) {
|
if (reasons.length > 0) {
|
||||||
return reasons.map((item) => {
|
return reasons.map((item) => {
|
||||||
@@ -2074,89 +1869,6 @@ function getSchedulingTitle(key: PoolKeyDetail): string {
|
|||||||
return getSchedulingBadgeLabel(key)
|
return getSchedulingBadgeLabel(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSchedulingScore(key: PoolKeyDetail): number {
|
|
||||||
const raw = key.scheduling_score
|
|
||||||
if (typeof raw === 'number' && Number.isFinite(raw)) {
|
|
||||||
return Math.max(Math.min(raw, 100), 0)
|
|
||||||
}
|
|
||||||
const status = getSchedulingStatus(key)
|
|
||||||
if (status === 'blocked') return 35
|
|
||||||
if (status === 'degraded') return 68
|
|
||||||
return 100
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSchedulingScoreBarColor(score: number): string {
|
|
||||||
if (score >= 80) return 'bg-green-500 dark:bg-green-400'
|
|
||||||
if (score >= 50) return 'bg-yellow-500 dark:bg-yellow-400'
|
|
||||||
return 'bg-red-500 dark:bg-red-400'
|
|
||||||
}
|
|
||||||
|
|
||||||
function isCandidateEligible(key: PoolKeyDetail): boolean {
|
|
||||||
if (typeof key.candidate_eligible === 'boolean') return key.candidate_eligible
|
|
||||||
return getSchedulingStatus(key) !== 'blocked'
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSchedulingDimensionTitle(key: PoolKeyDetail): string {
|
|
||||||
const dimensions = key.scheduling_dimensions ?? []
|
|
||||||
if (!dimensions.length) return ''
|
|
||||||
return dimensions.map((item) => {
|
|
||||||
const ttl = item.ttl_seconds && item.ttl_seconds > 0 ? ` (${formatTTL(item.ttl_seconds)})` : ''
|
|
||||||
const detail = item.detail ? ` - ${item.detail}` : ''
|
|
||||||
return `${item.label}: ${item.status}${ttl}${detail}`
|
|
||||||
}).join('\n')
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSchedulingRuleEntries(
|
|
||||||
key: PoolKeyDetail,
|
|
||||||
): NonNullable<PoolKeyDetail['scheduling_dimensions']> {
|
|
||||||
const dimensions = key.scheduling_dimensions ?? []
|
|
||||||
if (dimensions.length > 0) {
|
|
||||||
return dimensions
|
|
||||||
}
|
|
||||||
|
|
||||||
const reasons = key.scheduling_reasons ?? []
|
|
||||||
if (reasons.length > 0) {
|
|
||||||
return reasons.map((reason) => ({
|
|
||||||
code: reason.code,
|
|
||||||
label: reason.label,
|
|
||||||
status: reason.blocking ? 'blocked' : 'degraded',
|
|
||||||
blocking: reason.blocking,
|
|
||||||
source: reason.source,
|
|
||||||
weight: 0,
|
|
||||||
score: 0,
|
|
||||||
ttl_seconds: reason.ttl_seconds ?? null,
|
|
||||||
detail: reason.detail ?? null,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
const fallbackStatus = getSchedulingStatus(key)
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
code: key.scheduling_reason ?? 'available',
|
|
||||||
label: getSchedulingBadgeLabel(key),
|
|
||||||
status: fallbackStatus === 'available' ? 'ok' : fallbackStatus,
|
|
||||||
blocking: fallbackStatus === 'blocked',
|
|
||||||
source: 'pool',
|
|
||||||
weight: 0,
|
|
||||||
score: 0,
|
|
||||||
ttl_seconds: null,
|
|
||||||
detail: null,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSchedulingDimensionStatusLabel(status: string): string {
|
|
||||||
if (status === 'blocked') return '阻塞'
|
|
||||||
if (status === 'degraded') return '降级'
|
|
||||||
return '可用'
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSchedulingDimensionStatusVariant(status: string): PoolStatusVariant {
|
|
||||||
if (status === 'blocked') return 'destructive'
|
|
||||||
if (status === 'degraded') return 'warning'
|
|
||||||
return 'default'
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatTTL(seconds: number): string {
|
function formatTTL(seconds: number): string {
|
||||||
if (seconds <= 0) return ''
|
if (seconds <= 0) return ''
|
||||||
const m = Math.floor(seconds / 60)
|
const m = Math.floor(seconds / 60)
|
||||||
|
|||||||
@@ -47,7 +47,6 @@ from .schemas import (
|
|||||||
PoolKeysPageResponse,
|
PoolKeysPageResponse,
|
||||||
PoolOverviewItem,
|
PoolOverviewItem,
|
||||||
PoolOverviewResponse,
|
PoolOverviewResponse,
|
||||||
PoolSchedulingDimension,
|
|
||||||
PoolSchedulingReason,
|
PoolSchedulingReason,
|
||||||
PresetDimensionMetaResponse,
|
PresetDimensionMetaResponse,
|
||||||
PresetModeMetaResponse,
|
PresetModeMetaResponse,
|
||||||
@@ -508,17 +507,7 @@ def _build_pool_scheduling_state(
|
|||||||
cost_limit: int | None,
|
cost_limit: int | None,
|
||||||
cost_soft_threshold_percent: int,
|
cost_soft_threshold_percent: int,
|
||||||
health_score: float,
|
health_score: float,
|
||||||
) -> tuple[
|
) -> tuple[str, str, str, list[PoolSchedulingReason]]:
|
||||||
str,
|
|
||||||
str,
|
|
||||||
str,
|
|
||||||
list[PoolSchedulingReason],
|
|
||||||
float,
|
|
||||||
bool,
|
|
||||||
int,
|
|
||||||
int,
|
|
||||||
list[PoolSchedulingDimension],
|
|
||||||
]:
|
|
||||||
"""Build unified scheduling state for frontend display."""
|
"""Build unified scheduling state for frontend display."""
|
||||||
snapshot = PoolSchedulingSnapshot(
|
snapshot = PoolSchedulingSnapshot(
|
||||||
is_active=is_active,
|
is_active=is_active,
|
||||||
@@ -537,28 +526,12 @@ def _build_pool_scheduling_state(
|
|||||||
dimensions_raw = evaluate_pool_scheduling_dimensions(snapshot)
|
dimensions_raw = evaluate_pool_scheduling_dimensions(snapshot)
|
||||||
summary = summarize_pool_scheduling_dimensions(dimensions_raw)
|
summary = summarize_pool_scheduling_dimensions(dimensions_raw)
|
||||||
|
|
||||||
scheduling_dimensions: list[PoolSchedulingDimension] = []
|
|
||||||
scheduling_reasons: list[PoolSchedulingReason] = []
|
scheduling_reasons: list[PoolSchedulingReason] = []
|
||||||
|
|
||||||
for item in dimensions_raw:
|
for item in dimensions_raw:
|
||||||
detail = item.detail
|
|
||||||
if item.code == "cooldown":
|
|
||||||
detail = _format_cooldown_detail(detail)
|
|
||||||
|
|
||||||
model = PoolSchedulingDimension(
|
|
||||||
code=item.code,
|
|
||||||
label=item.label,
|
|
||||||
status=item.status,
|
|
||||||
blocking=bool(item.blocking or item.status == "blocked"),
|
|
||||||
source=item.source,
|
|
||||||
weight=item.weight,
|
|
||||||
score=item.score,
|
|
||||||
ttl_seconds=item.ttl_seconds,
|
|
||||||
detail=detail,
|
|
||||||
)
|
|
||||||
scheduling_dimensions.append(model)
|
|
||||||
|
|
||||||
if item.status != "ok":
|
if item.status != "ok":
|
||||||
|
detail = item.detail
|
||||||
|
if item.code == "cooldown":
|
||||||
|
detail = _format_cooldown_detail(detail)
|
||||||
scheduling_reasons.append(
|
scheduling_reasons.append(
|
||||||
PoolSchedulingReason(
|
PoolSchedulingReason(
|
||||||
code=item.code,
|
code=item.code,
|
||||||
@@ -575,11 +548,6 @@ def _build_pool_scheduling_state(
|
|||||||
summary.reason,
|
summary.reason,
|
||||||
summary.label,
|
summary.label,
|
||||||
scheduling_reasons,
|
scheduling_reasons,
|
||||||
summary.score,
|
|
||||||
summary.candidate_eligible,
|
|
||||||
summary.blocked_count,
|
|
||||||
summary.degraded_count,
|
|
||||||
scheduling_dimensions,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -902,11 +870,6 @@ class AdminListPoolKeysAdapter(AdminApiAdapter):
|
|||||||
scheduling_reason,
|
scheduling_reason,
|
||||||
scheduling_label,
|
scheduling_label,
|
||||||
scheduling_reasons,
|
scheduling_reasons,
|
||||||
scheduling_score,
|
|
||||||
candidate_eligible,
|
|
||||||
scheduling_blocked_count,
|
|
||||||
scheduling_degraded_count,
|
|
||||||
scheduling_dimensions,
|
|
||||||
) = _build_pool_scheduling_state(
|
) = _build_pool_scheduling_state(
|
||||||
is_active=bool(k.is_active),
|
is_active=bool(k.is_active),
|
||||||
account_blocked=account_state.blocked,
|
account_blocked=account_state.blocked,
|
||||||
@@ -1035,11 +998,6 @@ class AdminListPoolKeysAdapter(AdminApiAdapter):
|
|||||||
scheduling_reason=scheduling_reason,
|
scheduling_reason=scheduling_reason,
|
||||||
scheduling_label=scheduling_label,
|
scheduling_label=scheduling_label,
|
||||||
scheduling_reasons=scheduling_reasons,
|
scheduling_reasons=scheduling_reasons,
|
||||||
scheduling_score=scheduling_score,
|
|
||||||
candidate_eligible=candidate_eligible,
|
|
||||||
scheduling_blocked_count=scheduling_blocked_count,
|
|
||||||
scheduling_degraded_count=scheduling_degraded_count,
|
|
||||||
scheduling_dimensions=scheduling_dimensions,
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -64,19 +64,6 @@ class PoolSchedulingReason(BaseModel):
|
|||||||
detail: str | None = None
|
detail: str | None = None
|
||||||
|
|
||||||
|
|
||||||
class PoolSchedulingDimension(BaseModel):
|
|
||||||
"""Detailed scheduling dimension status."""
|
|
||||||
|
|
||||||
code: str
|
|
||||||
label: str
|
|
||||||
status: str = "ok" # ok / degraded / blocked
|
|
||||||
blocking: bool = False
|
|
||||||
source: str = "pool"
|
|
||||||
weight: int = 1
|
|
||||||
score: float = 1.0 # normalized 0~1
|
|
||||||
ttl_seconds: int | None = None
|
|
||||||
detail: str | None = None
|
|
||||||
|
|
||||||
|
|
||||||
class PoolKeyDetail(BaseModel):
|
class PoolKeyDetail(BaseModel):
|
||||||
"""Detailed status of a single pool key."""
|
"""Detailed status of a single pool key."""
|
||||||
@@ -124,11 +111,6 @@ class PoolKeyDetail(BaseModel):
|
|||||||
scheduling_reason: str = "available"
|
scheduling_reason: str = "available"
|
||||||
scheduling_label: str = "可用"
|
scheduling_label: str = "可用"
|
||||||
scheduling_reasons: list[PoolSchedulingReason] = Field(default_factory=list)
|
scheduling_reasons: list[PoolSchedulingReason] = Field(default_factory=list)
|
||||||
scheduling_score: float = 100.0
|
|
||||||
candidate_eligible: bool = True
|
|
||||||
scheduling_blocked_count: int = 0
|
|
||||||
scheduling_degraded_count: int = 0
|
|
||||||
scheduling_dimensions: list[PoolSchedulingDimension] = Field(default_factory=list)
|
|
||||||
|
|
||||||
model_config = ConfigDict(from_attributes=True)
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
|||||||
@@ -53,7 +53,6 @@ class PoolSchedulingSummary:
|
|||||||
status: str # available / degraded / blocked
|
status: str # available / degraded / blocked
|
||||||
reason: str
|
reason: str
|
||||||
label: str
|
label: str
|
||||||
score: float
|
|
||||||
candidate_eligible: bool
|
candidate_eligible: bool
|
||||||
blocked_count: int
|
blocked_count: int
|
||||||
degraded_count: int
|
degraded_count: int
|
||||||
@@ -408,7 +407,6 @@ def summarize_pool_scheduling_dimensions(
|
|||||||
status="available",
|
status="available",
|
||||||
reason="available",
|
reason="available",
|
||||||
label="可用",
|
label="可用",
|
||||||
score=100.0,
|
|
||||||
candidate_eligible=True,
|
candidate_eligible=True,
|
||||||
blocked_count=0,
|
blocked_count=0,
|
||||||
degraded_count=0,
|
degraded_count=0,
|
||||||
@@ -417,18 +415,12 @@ def summarize_pool_scheduling_dimensions(
|
|||||||
blocked = [item for item in dimensions if item.status == "blocked" or item.blocking]
|
blocked = [item for item in dimensions if item.status == "blocked" or item.blocking]
|
||||||
degraded = [item for item in dimensions if item.status == "degraded"]
|
degraded = [item for item in dimensions if item.status == "degraded"]
|
||||||
|
|
||||||
total_weight = sum(max(item.weight, 1) for item in dimensions)
|
|
||||||
weighted_score = sum(
|
|
||||||
max(item.weight, 1) * max(min(item.score, 1.0), 0.0) for item in dimensions
|
|
||||||
) / max(total_weight, 1)
|
|
||||||
|
|
||||||
if blocked:
|
if blocked:
|
||||||
primary = blocked[0]
|
primary = blocked[0]
|
||||||
return PoolSchedulingSummary(
|
return PoolSchedulingSummary(
|
||||||
status="blocked",
|
status="blocked",
|
||||||
reason=primary.code,
|
reason=primary.code,
|
||||||
label=primary.label,
|
label=primary.label,
|
||||||
score=round(weighted_score * 100, 1),
|
|
||||||
candidate_eligible=False,
|
candidate_eligible=False,
|
||||||
blocked_count=len(blocked),
|
blocked_count=len(blocked),
|
||||||
degraded_count=len(degraded),
|
degraded_count=len(degraded),
|
||||||
@@ -440,7 +432,6 @@ def summarize_pool_scheduling_dimensions(
|
|||||||
status="degraded",
|
status="degraded",
|
||||||
reason=primary.code,
|
reason=primary.code,
|
||||||
label=primary.label,
|
label=primary.label,
|
||||||
score=round(weighted_score * 100, 1),
|
|
||||||
candidate_eligible=True,
|
candidate_eligible=True,
|
||||||
blocked_count=0,
|
blocked_count=0,
|
||||||
degraded_count=len(degraded),
|
degraded_count=len(degraded),
|
||||||
@@ -450,7 +441,6 @@ def summarize_pool_scheduling_dimensions(
|
|||||||
status="available",
|
status="available",
|
||||||
reason="available",
|
reason="available",
|
||||||
label="可用",
|
label="可用",
|
||||||
score=round(weighted_score * 100, 1),
|
|
||||||
candidate_eligible=True,
|
candidate_eligible=True,
|
||||||
blocked_count=0,
|
blocked_count=0,
|
||||||
degraded_count=0,
|
degraded_count=0,
|
||||||
|
|||||||
@@ -40,7 +40,6 @@ def test_summary_available_when_all_dimensions_ok() -> None:
|
|||||||
assert summary.candidate_eligible is True
|
assert summary.candidate_eligible is True
|
||||||
assert summary.blocked_count == 0
|
assert summary.blocked_count == 0
|
||||||
assert summary.degraded_count == 0
|
assert summary.degraded_count == 0
|
||||||
assert summary.score == 100.0
|
|
||||||
|
|
||||||
|
|
||||||
def test_summary_blocked_when_manual_disabled() -> None:
|
def test_summary_blocked_when_manual_disabled() -> None:
|
||||||
@@ -51,7 +50,6 @@ def test_summary_blocked_when_manual_disabled() -> None:
|
|||||||
assert summary.reason == "manual_disabled"
|
assert summary.reason == "manual_disabled"
|
||||||
assert summary.candidate_eligible is False
|
assert summary.candidate_eligible is False
|
||||||
assert summary.blocked_count >= 1
|
assert summary.blocked_count >= 1
|
||||||
assert summary.score < 100.0
|
|
||||||
|
|
||||||
|
|
||||||
def test_summary_blocked_when_account_state_blocked() -> None:
|
def test_summary_blocked_when_account_state_blocked() -> None:
|
||||||
@@ -114,7 +112,6 @@ def test_empty_summary_defaults_to_available() -> None:
|
|||||||
summary = summarize_pool_scheduling_dimensions([])
|
summary = summarize_pool_scheduling_dimensions([])
|
||||||
assert summary.status == "available"
|
assert summary.status == "available"
|
||||||
assert summary.reason == "available"
|
assert summary.reason == "available"
|
||||||
assert summary.score == 100.0
|
|
||||||
|
|
||||||
|
|
||||||
def test_dimension_result_keeps_degraded_health_details() -> None:
|
def test_dimension_result_keeps_degraded_health_details() -> None:
|
||||||
|
|||||||
@@ -12,11 +12,6 @@ def test_pool_scheduling_state_manual_disabled_is_blocked() -> None:
|
|||||||
reason,
|
reason,
|
||||||
_label,
|
_label,
|
||||||
reasons,
|
reasons,
|
||||||
score,
|
|
||||||
candidate_eligible,
|
|
||||||
blocked_count,
|
|
||||||
_degraded_count,
|
|
||||||
dimensions,
|
|
||||||
) = _build_pool_scheduling_state(
|
) = _build_pool_scheduling_state(
|
||||||
is_active=False,
|
is_active=False,
|
||||||
account_blocked=False,
|
account_blocked=False,
|
||||||
@@ -34,11 +29,7 @@ def test_pool_scheduling_state_manual_disabled_is_blocked() -> None:
|
|||||||
|
|
||||||
assert status == "blocked"
|
assert status == "blocked"
|
||||||
assert reason == "manual_disabled"
|
assert reason == "manual_disabled"
|
||||||
assert candidate_eligible is False
|
|
||||||
assert blocked_count >= 1
|
|
||||||
assert score < 100
|
|
||||||
assert any(item.code == "manual_disabled" for item in reasons)
|
assert any(item.code == "manual_disabled" for item in reasons)
|
||||||
assert any(item.code == "manual_disabled" for item in dimensions)
|
|
||||||
|
|
||||||
|
|
||||||
def test_pool_scheduling_state_cooldown_detail_is_mapped() -> None:
|
def test_pool_scheduling_state_cooldown_detail_is_mapped() -> None:
|
||||||
@@ -47,11 +38,6 @@ def test_pool_scheduling_state_cooldown_detail_is_mapped() -> None:
|
|||||||
reason,
|
reason,
|
||||||
_label,
|
_label,
|
||||||
reasons,
|
reasons,
|
||||||
_score,
|
|
||||||
_candidate_eligible,
|
|
||||||
_blocked_count,
|
|
||||||
_degraded_count,
|
|
||||||
dimensions,
|
|
||||||
) = _build_pool_scheduling_state(
|
) = _build_pool_scheduling_state(
|
||||||
is_active=True,
|
is_active=True,
|
||||||
account_blocked=False,
|
account_blocked=False,
|
||||||
@@ -69,9 +55,7 @@ def test_pool_scheduling_state_cooldown_detail_is_mapped() -> None:
|
|||||||
|
|
||||||
assert reason == "cooldown"
|
assert reason == "cooldown"
|
||||||
cooldown_reason = next(item for item in reasons if item.code == "cooldown")
|
cooldown_reason = next(item for item in reasons if item.code == "cooldown")
|
||||||
cooldown_dimension = next(item for item in dimensions if item.code == "cooldown")
|
|
||||||
assert cooldown_reason.detail == "429 限流"
|
assert cooldown_reason.detail == "429 限流"
|
||||||
assert cooldown_dimension.detail == "429 限流"
|
|
||||||
|
|
||||||
|
|
||||||
def test_pool_scheduling_state_cost_soft_is_degraded() -> None:
|
def test_pool_scheduling_state_cost_soft_is_degraded() -> None:
|
||||||
@@ -80,11 +64,6 @@ def test_pool_scheduling_state_cost_soft_is_degraded() -> None:
|
|||||||
reason,
|
reason,
|
||||||
_label,
|
_label,
|
||||||
_reasons,
|
_reasons,
|
||||||
_score,
|
|
||||||
candidate_eligible,
|
|
||||||
blocked_count,
|
|
||||||
degraded_count,
|
|
||||||
_dimensions,
|
|
||||||
) = _build_pool_scheduling_state(
|
) = _build_pool_scheduling_state(
|
||||||
is_active=True,
|
is_active=True,
|
||||||
account_blocked=False,
|
account_blocked=False,
|
||||||
@@ -102,9 +81,6 @@ def test_pool_scheduling_state_cost_soft_is_degraded() -> None:
|
|||||||
|
|
||||||
assert status == "degraded"
|
assert status == "degraded"
|
||||||
assert reason == "cost_soft"
|
assert reason == "cost_soft"
|
||||||
assert candidate_eligible is True
|
|
||||||
assert blocked_count == 0
|
|
||||||
assert degraded_count >= 1
|
|
||||||
|
|
||||||
|
|
||||||
def test_known_banned_reason_account_block_prefix() -> None:
|
def test_known_banned_reason_account_block_prefix() -> None:
|
||||||
|
|||||||
Reference in New Issue
Block a user