Merge remote-tracking branch 'upstream/aether-rust-pioneer' into rust

This commit is contained in:
AAEE86
2026-04-10 15:08:32 +08:00
255 changed files with 15056 additions and 3205 deletions

View File

@@ -155,7 +155,12 @@ export const cacheApi = {
const response = await api.get('/api/admin/monitoring/cache/affinities', {
params: keyword ? { keyword } : undefined
})
return response.data.data
const data = response.data.data ?? {}
return {
items: data.items ?? [],
total: data.meta?.total ?? data.items?.length ?? 0,
matched_user_id: data.matched_user_id ?? null
}
}
}

View File

@@ -8,6 +8,8 @@ export interface CandidateRecord {
provider_id?: string
provider_name?: string
provider_website?: string // Provider 官网
provider_priority?: number
provider_keep_priority_on_conversion?: boolean
endpoint_id?: string
endpoint_name?: string // 端点显示名称api_format
key_id?: string
@@ -15,6 +17,8 @@ export interface CandidateRecord {
key_account_label?: string // 更适合展示的测试账号标签(优先 OAuth 邮箱)
key_preview?: string // 密钥脱敏预览(如 sk-***abcOAuth 类型不返回
key_auth_type?: string // 密钥认证类型api_key, service_account, oauth 等)
key_internal_priority?: number
key_global_priority_by_format?: Record<string, number> | null
key_oauth_plan_type?: string // OAuth 账号套餐类型free/plus/team/enterprise
key_capabilities?: Record<string, boolean> | null // Key 支持的能力
required_capabilities?: Record<string, boolean> | null // 请求实际需要的能力标签

View File

@@ -33,6 +33,9 @@
<h4 class="text-sm font-semibold">
请求链路追踪
</h4>
<span class="text-xs text-muted-foreground">
按实际调度顺序
</span>
<Badge :variant="getFinalStatusBadgeVariant(computedFinalStatus)">
{{ getFinalStatusLabel(computedFinalStatus) }}
</Badge>
@@ -196,6 +199,22 @@
<code class="format-code">{{ currentAttemptFormatDisplay }}</code>
</span>
</div>
<div
v-if="currentAttemptSchedulerInfo"
class="info-item"
>
<span class="info-label">调度顺位</span>
<span class="info-value info-value-stacked">
<code class="format-code">
全局 {{ currentAttemptSchedulerInfo.globalPriorityLabel }}
/ Provider {{ currentAttemptSchedulerInfo.providerPriorityLabel }}
/ Key {{ currentAttemptSchedulerInfo.keyPriorityLabel }}
</code>
<span class="text-xs text-muted-foreground">
{{ currentAttemptSchedulerInfo.hint }}
</span>
</span>
</div>
<div
v-if="currentAttempt.key_name || currentAttempt.key_id"
class="info-item"
@@ -303,17 +322,31 @@
</span>
</div>
<div
v-if="mergedCapabilities.length > 0"
v-if="activeCapabilities.length > 0"
class="info-item"
>
<span class="info-label">能力</span>
<span class="info-label">请求能力</span>
<span class="info-value">
<span class="capability-tags">
<span
v-for="cap in mergedCapabilities"
:key="cap"
v-for="cap in activeCapabilities"
:key="`required-${cap}`"
class="capability-tag active"
>{{ formatCapabilityLabel(cap) }}</span>
</span>
</span>
</div>
<div
v-if="keyCapabilities.length > 0"
class="info-item"
>
<span class="info-label">Key 能力</span>
<span class="info-value">
<span class="capability-tags">
<span
v-for="cap in keyCapabilities"
:key="`key-${cap}`"
class="capability-tag"
:class="{ active: isCapabilityUsed(cap) }"
>{{ formatCapabilityLabel(cap) }}</span>
</span>
</span>
@@ -415,6 +448,7 @@
<script setup lang="ts">
import { ref, watch, computed } from 'vue'
import { isAxiosError } from 'axios'
import Card from '@/components/ui/card.vue'
import Badge from '@/components/ui/badge.vue'
import Skeleton from '@/components/ui/skeleton.vue'
@@ -1072,6 +1106,47 @@ const normalizeFormatSignature = (value: string): string => {
return value.trim().toLowerCase()
}
const normalizePriorityNumber = (value: unknown): number | null => {
if (typeof value === 'number' && Number.isFinite(value)) {
return Math.trunc(value)
}
if (typeof value === 'string' && value.trim()) {
const parsed = Number(value)
if (Number.isFinite(parsed)) {
return Math.trunc(parsed)
}
}
return null
}
const resolveClientApiFormat = (attempt: CandidateRecord): string => {
const extra = (
attempt.extra_data && typeof attempt.extra_data === 'object' && !Array.isArray(attempt.extra_data)
? attempt.extra_data
: {}
) as Record<string, unknown>
const fromExtra = typeof extra.client_api_format === 'string' ? extra.client_api_format.trim() : ''
if (fromExtra) return fromExtra
if (typeof props.requestApiFormat === 'string' && props.requestApiFormat.trim()) {
return props.requestApiFormat.trim()
}
return ''
}
const resolveProviderApiFormat = (attempt: CandidateRecord): string => {
const extra = (
attempt.extra_data && typeof attempt.extra_data === 'object' && !Array.isArray(attempt.extra_data)
? attempt.extra_data
: {}
) as Record<string, unknown>
const fromExtra = typeof extra.provider_api_format === 'string' ? extra.provider_api_format.trim() : ''
if (fromExtra) return fromExtra
if (typeof attempt.endpoint_name === 'string' && attempt.endpoint_name.trim()) {
return attempt.endpoint_name.trim()
}
return ''
}
const currentAttemptFormatDisplay = computed(() => {
const attempt = currentAttempt.value
if (!attempt) return ''
@@ -1103,6 +1178,54 @@ const currentAttemptFormatDisplay = computed(() => {
return providerText || requestText
})
const currentAttemptSchedulerInfo = computed<{
globalPriorityLabel: string
providerPriorityLabel: string
keyPriorityLabel: string
hint: string
} | null>(() => {
const attempt = currentAttempt.value
if (!attempt) return null
const clientApiFormat = resolveClientApiFormat(attempt)
const providerApiFormat = resolveProviderApiFormat(attempt)
const providerPriority = normalizePriorityNumber(attempt.provider_priority)
const keyInternalPriority = normalizePriorityNumber(attempt.key_internal_priority)
let globalPriority: number | null = null
const globalPriorityMap = attempt.key_global_priority_by_format
if (globalPriorityMap && typeof globalPriorityMap === 'object' && !Array.isArray(globalPriorityMap) && clientApiFormat) {
const match = Object.entries(globalPriorityMap).find(([format]) => (
normalizeFormatSignature(format) === normalizeFormatSignature(clientApiFormat)
))
globalPriority = match ? normalizePriorityNumber(match[1]) : null
}
const isCrossFormat = Boolean(
clientApiFormat &&
providerApiFormat &&
normalizeFormatSignature(clientApiFormat) !== normalizeFormatSignature(providerApiFormat),
)
const keepPriorityOnConversion = attempt.provider_keep_priority_on_conversion === true
let hint = '链路按实际调度顺序展示'
if (globalPriority !== null) {
hint = `当前格式 ${formatApiFormat(clientApiFormat)} 先看全局 Key 优先级`
}
if (isCrossFormat) {
hint = keepPriorityOnConversion
? '跨格式候选已开启保持优先级'
: '跨格式候选默认排在同格式候选之后'
}
return {
globalPriorityLabel: globalPriority !== null ? String(globalPriority) : '-',
providerPriorityLabel: providerPriority !== null ? String(providerPriority) : '-',
keyPriorityLabel: keyInternalPriority !== null ? String(keyInternalPriority) : '-',
hint,
}
})
// 计算当前尝试启用的能力标签(请求需要的能力)
const activeCapabilities = computed(() => {
if (!currentAttempt.value?.required_capabilities) return []
@@ -1123,20 +1246,6 @@ const keyCapabilities = computed(() => {
.map(([key]) => key)
})
// 合并后的能力列表Key 支持的能力 + 请求需要的能力(去重)
const mergedCapabilities = computed(() => {
const keyCaps = new Set(keyCapabilities.value)
const activeCaps = new Set(activeCapabilities.value)
// 合并两个集合
const merged = new Set([...keyCaps, ...activeCaps])
return Array.from(merged)
})
// 检查某个能力是否被请求使用
const isCapabilityUsed = (cap: string): boolean => {
return activeCapabilities.value.includes(cap)
}
// 判断是否为 OAuth 类型provider_type 为具体值时也算 OAuth
const isOAuthType = (authType?: string): boolean => {
if (!authType) return false
@@ -1251,6 +1360,11 @@ const loadTrace = async (silent = false) => {
try {
internalTrace.value = await requestTraceApi.getRequestTrace(props.requestId)
} catch (err: unknown) {
if (isAxiosError(err) && err.response?.status === 404) {
internalTrace.value = null
error.value = null
return
}
if (!silent) {
error.value = parseApiError(err, '加载失败')
}

View File

@@ -114,8 +114,6 @@
<span>{{ formatApiFormat(detail.api_format) }}</span>
<span class="opacity-40">|</span>
<span>用户: {{ detail.user?.username || 'Unknown' }}</span>
<span class="opacity-40">|</span>
<span class="font-mono">{{ detail.api_key?.display || 'N/A' }}</span>
</div>
</div>

View File

@@ -515,7 +515,7 @@ onBeforeUnmount(() => {
活跃亲和性
</div>
<div class="text-2xl font-bold mt-1">
{{ stats?.affinity_stats?.active_affinities || 0 }}
{{ stats?.affinity_stats?.active_affinities || stats?.affinity_stats?.total_affinities || 0 }}
</div>
<div class="text-xs text-muted-foreground mt-1">
TTL {{ config?.cache_ttl_seconds || 300 }}s
@@ -653,7 +653,7 @@ onBeforeUnmount(() => {
<TableBody v-if="!listLoading && affinityList.length">
<TableRow
v-for="item in paginatedAffinityList"
:key="`${item.affinity_key}-${item.endpoint_id}-${item.key_id}`"
:key="`${item.affinity_key}-${item.endpoint_id}-${item.key_id}-${item.global_model_id || item.model_name || 'unknown'}-${item.api_format || 'unknown'}`"
>
<TableCell>
<div class="flex items-center gap-1.5">
@@ -757,7 +757,7 @@ onBeforeUnmount(() => {
>
<div
v-for="item in paginatedAffinityList"
:key="`m-${item.affinity_key}-${item.endpoint_id}-${item.key_id}`"
:key="`m-${item.affinity_key}-${item.endpoint_id}-${item.key_id}-${item.global_model_id || item.model_name || 'unknown'}-${item.api_format || 'unknown'}`"
class="p-4 space-y-2"
>
<div class="flex items-start justify-between gap-3">