Improve affinity list display

This commit is contained in:
RWDai
2026-05-07 15:08:30 +08:00
parent 2cfeb14d88
commit cb9d9944e3
2 changed files with 138 additions and 21 deletions

View File

@@ -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 } : {})
}
})
}, },
/** /**

View File

@@ -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>