mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
Improve affinity list display
This commit is contained in:
@@ -69,10 +69,13 @@ export interface UserAffinity {
|
|||||||
global_model_id: string | null // 原始的 global_model_id(用于删除)
|
global_model_id: string | null // 原始的 global_model_id(用于删除)
|
||||||
model_name: string | null // 模型名称(如 claude-haiku-4-5-20250514)
|
model_name: string | null // 模型名称(如 claude-haiku-4-5-20250514)
|
||||||
model_display_name: string | null // 模型显示名称(如 Claude Haiku 4.5)
|
model_display_name: string | null // 模型显示名称(如 Claude Haiku 4.5)
|
||||||
|
client_family: string | null // 客户端类型(如 codex/opencode)
|
||||||
|
session_hash: string | null // 会话维度 hash,用于区分同一客户端的不同会话
|
||||||
api_format: string | null // API 格式 (claude/openai)
|
api_format: string | null // API 格式 (claude/openai)
|
||||||
created_at: number
|
created_at: number
|
||||||
expire_at: number
|
expire_at: number
|
||||||
request_count: number
|
request_count: number
|
||||||
|
request_count_known?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AffinityListResponse {
|
export interface AffinityListResponse {
|
||||||
@@ -128,8 +131,20 @@ export const cacheApi = {
|
|||||||
* @param modelId GlobalModel ID
|
* @param modelId GlobalModel ID
|
||||||
* @param apiFormat API 格式 (claude/openai)
|
* @param apiFormat API 格式 (claude/openai)
|
||||||
*/
|
*/
|
||||||
async clearSingleAffinity(affinityKey: string, endpointId: string, modelId: string, apiFormat: string): Promise<void> {
|
async clearSingleAffinity(
|
||||||
await api.delete(`/api/admin/monitoring/cache/affinity/${affinityKey}/${endpointId}/${modelId}/${apiFormat}`)
|
affinityKey: string,
|
||||||
|
endpointId: string,
|
||||||
|
modelId: string,
|
||||||
|
apiFormat: string,
|
||||||
|
clientFamily?: string | null,
|
||||||
|
sessionHash?: string | null
|
||||||
|
): Promise<void> {
|
||||||
|
await api.delete(`/api/admin/monitoring/cache/affinity/${affinityKey}/${endpointId}/${modelId}/${apiFormat}`, {
|
||||||
|
params: {
|
||||||
|
...(clientFamily ? { client_family: clientFamily } : {}),
|
||||||
|
...(sessionHash ? { session_hash: sessionHash } : {})
|
||||||
|
}
|
||||||
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -178,7 +178,14 @@ async function clearSingleAffinity(item: UserAffinity) {
|
|||||||
|
|
||||||
clearingRowAffinityKey.value = affinityKey
|
clearingRowAffinityKey.value = affinityKey
|
||||||
try {
|
try {
|
||||||
await cacheApi.clearSingleAffinity(affinityKey, endpointId, modelId, apiFormat)
|
await cacheApi.clearSingleAffinity(
|
||||||
|
affinityKey,
|
||||||
|
endpointId,
|
||||||
|
modelId,
|
||||||
|
apiFormat,
|
||||||
|
item.client_family,
|
||||||
|
item.session_hash
|
||||||
|
)
|
||||||
showSuccess('清除成功')
|
showSuccess('清除成功')
|
||||||
await fetchCacheStats()
|
await fetchCacheStats()
|
||||||
await fetchAffinityList(tableKeyword.value.trim() || undefined)
|
await fetchAffinityList(tableKeyword.value.trim() || undefined)
|
||||||
@@ -227,6 +234,76 @@ function getRemainingTime(expireAt?: number): string {
|
|||||||
return formatRemainingTime(expireAt, currentTime.value)
|
return formatRemainingTime(expireAt, currentTime.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function truncateMiddle(value: string, head = 8, tail = 6): string {
|
||||||
|
const normalized = value.trim()
|
||||||
|
if (!normalized) return '---'
|
||||||
|
if (normalized.length <= head + tail + 3) return normalized
|
||||||
|
return `${normalized.slice(0, head)}...${normalized.slice(-tail)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function affinityUserLabel(item: UserAffinity): string {
|
||||||
|
return item.username || item.email || item.user_id || '未知'
|
||||||
|
}
|
||||||
|
|
||||||
|
function affinityUserTitle(item: UserAffinity): string | undefined {
|
||||||
|
return item.email || item.username || item.user_id || undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
function affinityUserApiKeyLabel(item: UserAffinity): string {
|
||||||
|
return item.user_api_key_name || item.user_api_key_prefix || truncateMiddle(item.affinity_key)
|
||||||
|
}
|
||||||
|
|
||||||
|
function affinityModelLabel(item: UserAffinity): string {
|
||||||
|
return item.model_display_name || item.model_name || item.global_model_id || '---'
|
||||||
|
}
|
||||||
|
|
||||||
|
function affinityModelSubtitle(item: UserAffinity): string {
|
||||||
|
if (!item.model_display_name || !item.model_name || item.model_display_name === item.model_name) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
return item.model_name
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatClientFamily(family?: string | null): string {
|
||||||
|
switch (family) {
|
||||||
|
case 'codex':
|
||||||
|
return 'Codex'
|
||||||
|
case 'opencode':
|
||||||
|
return 'OpenCode'
|
||||||
|
case 'claude_code':
|
||||||
|
return 'Claude Code'
|
||||||
|
case 'generic':
|
||||||
|
return '通用'
|
||||||
|
case undefined:
|
||||||
|
case null:
|
||||||
|
case '':
|
||||||
|
return '未知'
|
||||||
|
default:
|
||||||
|
return family
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function providerKeyLabel(item: UserAffinity): string {
|
||||||
|
if (item.key_name) return item.key_name
|
||||||
|
if (item.key_prefix === '[OAuth Token]') return 'OAuth 认证'
|
||||||
|
return item.key_prefix || '---'
|
||||||
|
}
|
||||||
|
|
||||||
|
function providerKeyTitle(item: UserAffinity): string | undefined {
|
||||||
|
if (item.key_name && item.key_prefix) return `${item.key_name} · ${item.key_prefix}`
|
||||||
|
return item.key_name || item.key_prefix || undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatAffinityRequestCount(item: UserAffinity): string {
|
||||||
|
if (item.request_count_known === false) return '—'
|
||||||
|
return formatNumber(item.request_count || 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatAffinityRequestCountUnit(item: UserAffinity): string {
|
||||||
|
const count = formatAffinityRequestCount(item)
|
||||||
|
return count === '—' ? count : `${count}次`
|
||||||
|
}
|
||||||
|
|
||||||
function formatIntervalDescription(user: TTLAnalysisUser): string {
|
function formatIntervalDescription(user: TTLAnalysisUser): string {
|
||||||
const p90 = user.percentiles.p90
|
const p90 = user.percentiles.p90
|
||||||
if (p90 === null || p90 === undefined) return '-'
|
if (p90 === null || p90 === undefined) return '-'
|
||||||
@@ -636,6 +713,9 @@ onBeforeUnmount(() => {
|
|||||||
<TableHead class="w-40">
|
<TableHead class="w-40">
|
||||||
模型
|
模型
|
||||||
</TableHead>
|
</TableHead>
|
||||||
|
<TableHead class="w-24">
|
||||||
|
客户端
|
||||||
|
</TableHead>
|
||||||
<TableHead class="w-36">
|
<TableHead class="w-36">
|
||||||
API 格式 / Key
|
API 格式 / Key
|
||||||
</TableHead>
|
</TableHead>
|
||||||
@@ -653,7 +733,7 @@ onBeforeUnmount(() => {
|
|||||||
<TableBody v-if="!listLoading && affinityList.length">
|
<TableBody v-if="!listLoading && affinityList.length">
|
||||||
<TableRow
|
<TableRow
|
||||||
v-for="item in paginatedAffinityList"
|
v-for="item in paginatedAffinityList"
|
||||||
:key="`${item.affinity_key}-${item.endpoint_id}-${item.key_id}-${item.global_model_id || item.model_name || 'unknown'}-${item.api_format || 'unknown'}`"
|
:key="`${item.affinity_key}-${item.endpoint_id}-${item.key_id}-${item.global_model_id || item.model_name || 'unknown'}-${item.api_format || 'unknown'}-${item.client_family || 'unknown'}-${item.session_hash || 'legacy'}`"
|
||||||
>
|
>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div class="flex items-center gap-1.5">
|
<div class="flex items-center gap-1.5">
|
||||||
@@ -666,16 +746,16 @@ onBeforeUnmount(() => {
|
|||||||
</Badge>
|
</Badge>
|
||||||
<span
|
<span
|
||||||
class="text-sm font-medium truncate max-w-[120px]"
|
class="text-sm font-medium truncate max-w-[120px]"
|
||||||
:title="item.username ?? undefined"
|
:title="affinityUserTitle(item)"
|
||||||
>{{ item.username || '未知' }}</span>
|
>{{ affinityUserLabel(item) }}</span>
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div class="flex items-center gap-1.5">
|
<div class="flex items-center gap-1.5">
|
||||||
<span
|
<span
|
||||||
class="text-sm truncate max-w-[80px]"
|
class="text-sm truncate max-w-[80px]"
|
||||||
:title="item.user_api_key_name || undefined"
|
:title="item.user_api_key_name || item.affinity_key"
|
||||||
>{{ item.user_api_key_name || '未命名' }}</span>
|
>{{ affinityUserApiKeyLabel(item) }}</span>
|
||||||
<Badge
|
<Badge
|
||||||
v-if="item.api_format && item.rate_multipliers?.[item.api_format] && item.rate_multipliers[item.api_format] !== 1.0"
|
v-if="item.api_format && item.rate_multipliers?.[item.api_format] && item.rate_multipliers[item.api_format] !== 1.0"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@@ -699,30 +779,45 @@ onBeforeUnmount(() => {
|
|||||||
<TableCell>
|
<TableCell>
|
||||||
<div
|
<div
|
||||||
class="text-sm truncate max-w-[150px]"
|
class="text-sm truncate max-w-[150px]"
|
||||||
:title="item.model_display_name || undefined"
|
:title="affinityModelLabel(item)"
|
||||||
>
|
>
|
||||||
{{ item.model_display_name || '---' }}
|
{{ affinityModelLabel(item) }}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
v-if="affinityModelSubtitle(item)"
|
||||||
class="text-xs text-muted-foreground"
|
class="text-xs text-muted-foreground"
|
||||||
:title="item.model_name || undefined"
|
:title="item.model_name || undefined"
|
||||||
>
|
>
|
||||||
{{ item.model_name || '---' }}
|
{{ affinityModelSubtitle(item) }}
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
class="text-[10px] px-2 font-normal"
|
||||||
|
>
|
||||||
|
{{ formatClientFamily(item.client_family) }}
|
||||||
|
</Badge>
|
||||||
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div class="text-sm">
|
<div class="text-sm">
|
||||||
{{ formatApiFormat(item.api_format) }}
|
{{ formatApiFormat(item.api_format) }}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-xs text-muted-foreground font-mono">
|
<div
|
||||||
{{ item.key_prefix || '---' }}
|
class="text-xs text-muted-foreground truncate max-w-[130px]"
|
||||||
|
:title="providerKeyTitle(item)"
|
||||||
|
>
|
||||||
|
{{ providerKeyLabel(item) }}
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell class="text-center">
|
<TableCell class="text-center">
|
||||||
<span class="text-xs">{{ getRemainingTime(item.expire_at) }}</span>
|
<span class="text-xs">{{ getRemainingTime(item.expire_at) }}</span>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell class="text-center">
|
<TableCell class="text-center">
|
||||||
<span class="text-sm">{{ item.request_count }}</span>
|
<span
|
||||||
|
class="text-sm"
|
||||||
|
:title="item.request_count_known === false ? '此缓存源没有精确次数统计' : undefined"
|
||||||
|
>{{ formatAffinityRequestCount(item) }}</span>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell class="text-right">
|
<TableCell class="text-right">
|
||||||
<Button
|
<Button
|
||||||
@@ -741,7 +836,7 @@ onBeforeUnmount(() => {
|
|||||||
<TableBody v-else>
|
<TableBody v-else>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell
|
<TableCell
|
||||||
colspan="8"
|
colspan="9"
|
||||||
class="text-center py-6 text-sm text-muted-foreground"
|
class="text-center py-6 text-sm text-muted-foreground"
|
||||||
>
|
>
|
||||||
{{ listLoading ? '加载中...' : '暂无缓存记录' }}
|
{{ listLoading ? '加载中...' : '暂无缓存记录' }}
|
||||||
@@ -757,7 +852,7 @@ onBeforeUnmount(() => {
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-for="item in paginatedAffinityList"
|
v-for="item in paginatedAffinityList"
|
||||||
:key="`m-${item.affinity_key}-${item.endpoint_id}-${item.key_id}-${item.global_model_id || item.model_name || 'unknown'}-${item.api_format || 'unknown'}`"
|
:key="`m-${item.affinity_key}-${item.endpoint_id}-${item.key_id}-${item.global_model_id || item.model_name || 'unknown'}-${item.api_format || 'unknown'}-${item.client_family || 'unknown'}-${item.session_hash || 'legacy'}`"
|
||||||
class="p-4 space-y-2"
|
class="p-4 space-y-2"
|
||||||
>
|
>
|
||||||
<div class="flex items-start justify-between gap-3">
|
<div class="flex items-start justify-between gap-3">
|
||||||
@@ -770,10 +865,10 @@ onBeforeUnmount(() => {
|
|||||||
>
|
>
|
||||||
独立
|
独立
|
||||||
</Badge>
|
</Badge>
|
||||||
<span class="text-sm font-medium truncate">{{ item.username || '未知' }}</span>
|
<span class="text-sm font-medium truncate">{{ affinityUserLabel(item) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-xs text-muted-foreground mt-0.5">
|
<div class="text-xs text-muted-foreground mt-0.5">
|
||||||
{{ item.user_api_key_name || '未命名' }} · {{ item.user_api_key_prefix || '---' }}
|
{{ affinityUserApiKeyLabel(item) }} · {{ item.user_api_key_prefix || truncateMiddle(item.affinity_key) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
@@ -789,11 +884,18 @@ onBeforeUnmount(() => {
|
|||||||
<div class="flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
|
<div class="flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
|
||||||
<span>{{ item.provider_name || '未知' }}</span>
|
<span>{{ item.provider_name || '未知' }}</span>
|
||||||
<span>·</span>
|
<span>·</span>
|
||||||
<span class="truncate max-w-[100px]">{{ item.model_display_name || '---' }}</span>
|
<span class="truncate max-w-[100px]">{{ affinityModelLabel(item) }}</span>
|
||||||
|
<span>·</span>
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
class="text-[10px] px-1.5 font-normal"
|
||||||
|
>
|
||||||
|
{{ formatClientFamily(item.client_family) }}
|
||||||
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-between text-xs">
|
<div class="flex items-center justify-between text-xs">
|
||||||
<span class="text-muted-foreground">{{ formatApiFormat(item.api_format) }}</span>
|
<span class="text-muted-foreground">{{ formatApiFormat(item.api_format) }} · {{ providerKeyLabel(item) }}</span>
|
||||||
<span>{{ getRemainingTime(item.expire_at) }} · {{ item.request_count }}次</span>
|
<span>{{ getRemainingTime(item.expire_at) }} · {{ formatAffinityRequestCountUnit(item) }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user