- 支持Anthropic OAuth/SetupToken账号的5h窗口费用阈值控制 - 支持账号级别的并发会话数量限制 - 使用Redis缓存窗口费用(30秒TTL)减少数据库压力 - 费用计算基于标准费用(不含账号倍率)
200 lines
7.5 KiB
Vue
200 lines
7.5 KiB
Vue
<template>
|
||
<div class="flex flex-col gap-1.5">
|
||
<!-- 并发槽位 -->
|
||
<div class="flex items-center gap-1.5">
|
||
<span
|
||
:class="[
|
||
'inline-flex items-center gap-1 rounded-md px-2 py-0.5 text-xs font-medium',
|
||
concurrencyClass
|
||
]"
|
||
>
|
||
<svg class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6A2.25 2.25 0 016 3.75h2.25A2.25 2.25 0 0110.5 6v2.25a2.25 2.25 0 01-2.25 2.25H6a2.25 2.25 0 01-2.25-2.25V6zM3.75 15.75A2.25 2.25 0 016 13.5h2.25a2.25 2.25 0 012.25 2.25V18a2.25 2.25 0 01-2.25 2.25H6A2.25 2.25 0 013.75 18v-2.25zM13.5 6a2.25 2.25 0 012.25-2.25H18A2.25 2.25 0 0120.25 6v2.25A2.25 2.25 0 0118 10.5h-2.25a2.25 2.25 0 01-2.25-2.25V6zM13.5 15.75a2.25 2.25 0 012.25-2.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-2.25A2.25 2.25 0 0113.5 18v-2.25z" />
|
||
</svg>
|
||
<span class="font-mono">{{ currentConcurrency }}</span>
|
||
<span class="text-gray-400 dark:text-gray-500">/</span>
|
||
<span class="font-mono">{{ account.concurrency }}</span>
|
||
</span>
|
||
</div>
|
||
|
||
<!-- 5h窗口费用限制(仅 Anthropic OAuth/SetupToken 且启用时显示) -->
|
||
<div v-if="showWindowCost" class="flex items-center gap-1">
|
||
<span
|
||
:class="[
|
||
'inline-flex items-center gap-1 rounded-md px-1.5 py-0.5 text-[10px] font-medium',
|
||
windowCostClass
|
||
]"
|
||
:title="windowCostTooltip"
|
||
>
|
||
<svg class="h-2.5 w-2.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6v12m-3-2.818l.879.659c1.171.879 3.07.879 4.242 0 1.172-.879 1.172-2.303 0-3.182C13.536 12.219 12.768 12 12 12c-.725 0-1.45-.22-2.003-.659-1.106-.879-1.106-2.303 0-3.182s2.9-.879 4.006 0l.415.33M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||
</svg>
|
||
<span class="font-mono">${{ formatCost(currentWindowCost) }}</span>
|
||
<span class="text-gray-400 dark:text-gray-500">/</span>
|
||
<span class="font-mono">${{ formatCost(account.window_cost_limit) }}</span>
|
||
</span>
|
||
</div>
|
||
|
||
<!-- 会话数量限制(仅 Anthropic OAuth/SetupToken 且启用时显示) -->
|
||
<div v-if="showSessionLimit" class="flex items-center gap-1">
|
||
<span
|
||
:class="[
|
||
'inline-flex items-center gap-1 rounded-md px-1.5 py-0.5 text-[10px] font-medium',
|
||
sessionLimitClass
|
||
]"
|
||
:title="sessionLimitTooltip"
|
||
>
|
||
<svg class="h-2.5 w-2.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||
<path stroke-linecap="round" stroke-linejoin="round" d="M15 19.128a9.38 9.38 0 002.625.372 9.337 9.337 0 004.121-.952 4.125 4.125 0 00-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 018.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0111.964-3.07M12 6.375a3.375 3.375 0 11-6.75 0 3.375 3.375 0 016.75 0zm8.25 2.25a2.625 2.625 0 11-5.25 0 2.625 2.625 0 015.25 0z" />
|
||
</svg>
|
||
<span class="font-mono">{{ activeSessions }}</span>
|
||
<span class="text-gray-400 dark:text-gray-500">/</span>
|
||
<span class="font-mono">{{ account.max_sessions }}</span>
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { computed } from 'vue'
|
||
import { useI18n } from 'vue-i18n'
|
||
import type { Account } from '@/types'
|
||
|
||
const props = defineProps<{
|
||
account: Account
|
||
}>()
|
||
|
||
const { t } = useI18n()
|
||
|
||
// 当前并发数
|
||
const currentConcurrency = computed(() => props.account.current_concurrency || 0)
|
||
|
||
// 是否为 Anthropic OAuth/SetupToken 账号
|
||
const isAnthropicOAuthOrSetupToken = computed(() => {
|
||
return (
|
||
props.account.platform === 'anthropic' &&
|
||
(props.account.type === 'oauth' || props.account.type === 'setup-token')
|
||
)
|
||
})
|
||
|
||
// 是否显示窗口费用限制
|
||
const showWindowCost = computed(() => {
|
||
return (
|
||
isAnthropicOAuthOrSetupToken.value &&
|
||
props.account.window_cost_limit !== undefined &&
|
||
props.account.window_cost_limit !== null &&
|
||
props.account.window_cost_limit > 0
|
||
)
|
||
})
|
||
|
||
// 当前窗口费用
|
||
const currentWindowCost = computed(() => props.account.current_window_cost ?? 0)
|
||
|
||
// 是否显示会话限制
|
||
const showSessionLimit = computed(() => {
|
||
return (
|
||
isAnthropicOAuthOrSetupToken.value &&
|
||
props.account.max_sessions !== undefined &&
|
||
props.account.max_sessions !== null &&
|
||
props.account.max_sessions > 0
|
||
)
|
||
})
|
||
|
||
// 当前活跃会话数
|
||
const activeSessions = computed(() => props.account.active_sessions ?? 0)
|
||
|
||
// 并发状态样式
|
||
const concurrencyClass = computed(() => {
|
||
const current = currentConcurrency.value
|
||
const max = props.account.concurrency
|
||
|
||
if (current >= max) {
|
||
return 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400'
|
||
}
|
||
if (current > 0) {
|
||
return 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400'
|
||
}
|
||
return 'bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400'
|
||
})
|
||
|
||
// 窗口费用状态样式
|
||
const windowCostClass = computed(() => {
|
||
if (!showWindowCost.value) return ''
|
||
|
||
const current = currentWindowCost.value
|
||
const limit = props.account.window_cost_limit || 0
|
||
const reserve = props.account.window_cost_sticky_reserve || 10
|
||
|
||
// >= 阈值+预留: 完全不可调度 (红色)
|
||
if (current >= limit + reserve) {
|
||
return 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400'
|
||
}
|
||
// >= 阈值: 仅粘性会话 (橙色)
|
||
if (current >= limit) {
|
||
return 'bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400'
|
||
}
|
||
// >= 80% 阈值: 警告 (黄色)
|
||
if (current >= limit * 0.8) {
|
||
return 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400'
|
||
}
|
||
// 正常 (绿色)
|
||
return 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400'
|
||
})
|
||
|
||
// 窗口费用提示文字
|
||
const windowCostTooltip = computed(() => {
|
||
if (!showWindowCost.value) return ''
|
||
|
||
const current = currentWindowCost.value
|
||
const limit = props.account.window_cost_limit || 0
|
||
const reserve = props.account.window_cost_sticky_reserve || 10
|
||
|
||
if (current >= limit + reserve) {
|
||
return t('admin.accounts.capacity.windowCost.blocked')
|
||
}
|
||
if (current >= limit) {
|
||
return t('admin.accounts.capacity.windowCost.stickyOnly')
|
||
}
|
||
return t('admin.accounts.capacity.windowCost.normal')
|
||
})
|
||
|
||
// 会话限制状态样式
|
||
const sessionLimitClass = computed(() => {
|
||
if (!showSessionLimit.value) return ''
|
||
|
||
const current = activeSessions.value
|
||
const max = props.account.max_sessions || 0
|
||
|
||
// >= 最大: 完全占满 (红色)
|
||
if (current >= max) {
|
||
return 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400'
|
||
}
|
||
// >= 80%: 警告 (黄色)
|
||
if (current >= max * 0.8) {
|
||
return 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400'
|
||
}
|
||
// 正常 (绿色)
|
||
return 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400'
|
||
})
|
||
|
||
// 会话限制提示文字
|
||
const sessionLimitTooltip = computed(() => {
|
||
if (!showSessionLimit.value) return ''
|
||
|
||
const current = activeSessions.value
|
||
const max = props.account.max_sessions || 0
|
||
const idle = props.account.session_idle_timeout_minutes || 5
|
||
|
||
if (current >= max) {
|
||
return t('admin.accounts.capacity.sessions.full', { idle })
|
||
}
|
||
return t('admin.accounts.capacity.sessions.normal', { idle })
|
||
})
|
||
|
||
// 格式化费用显示
|
||
const formatCost = (value: number | null | undefined) => {
|
||
if (value === null || value === undefined) return '0'
|
||
return value.toFixed(2)
|
||
}
|
||
</script>
|