feat(Claude Console): 添加Claude Console账号每日配额

1. 额度检查优先级更高:即使不启用限流机制,超额仍会禁用账户
2. 状态会被覆盖:quota_exceeded 会覆盖 rate_limited
3. 两种恢复时间:
  - 限流恢复:分钟级(如60分钟)
  - 额度恢复:天级(第二天重置)
4. 独立控制:
  - rateLimitDuration = 0:只管理额度,忽略429
  - rateLimitDuration > 0:同时管理限流和额度
This commit is contained in:
sususu
2025-09-05 14:58:59 +08:00
parent bdd17a85e9
commit 4cc937a144
6 changed files with 656 additions and 22 deletions

View File

@@ -658,6 +658,41 @@
</p>
</div>
<!-- 额度管理字段 -->
<div class="grid grid-cols-2 gap-4">
<div>
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300">
每日额度限制 ($)
</label>
<input
v-model.number="form.dailyQuota"
class="form-input w-full dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200"
min="0"
placeholder="0 表示不限制"
step="0.01"
type="number"
/>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
设置每日使用额度0 表示不限制
</p>
</div>
<div>
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300">
额度重置时间
</label>
<input
v-model="form.quotaResetTime"
class="form-input w-full dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200"
placeholder="00:00"
type="time"
/>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
每日自动重置额度的时间
</p>
</div>
</div>
<div>
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300"
>模型映射表 (可选)</label
@@ -1544,6 +1579,75 @@
<p class="mt-1 text-xs text-gray-500">留空表示不更新 API Key</p>
</div>
<!-- 额度管理字段 -->
<div class="grid grid-cols-2 gap-4">
<div>
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300">
每日额度限制 ($)
</label>
<input
v-model.number="form.dailyQuota"
class="form-input w-full dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200"
min="0"
placeholder="0 表示不限制"
step="0.01"
type="number"
/>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
设置每日使用额度0 表示不限制
</p>
</div>
<div>
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300">
额度重置时间
</label>
<input
v-model="form.quotaResetTime"
class="form-input w-full dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200"
placeholder="00:00"
type="time"
/>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">每日自动重置额度的时间</p>
</div>
</div>
<!-- 当前使用情况(仅编辑模式显示) -->
<div
v-if="isEdit && form.dailyQuota > 0"
class="rounded-lg bg-gray-50 p-4 dark:bg-gray-800"
>
<div class="mb-2 flex items-center justify-between">
<span class="text-sm font-semibold text-gray-700 dark:text-gray-300">
今日使用情况
</span>
<span class="text-sm text-gray-500 dark:text-gray-400">
${{ calculateCurrentUsage().toFixed(4) }} / ${{ form.dailyQuota.toFixed(2) }}
</span>
</div>
<div class="relative h-2 w-full rounded-full bg-gray-200 dark:bg-gray-700">
<div
class="absolute left-0 top-0 h-full rounded-full transition-all"
:class="
usagePercentage >= 90
? 'bg-red-500'
: usagePercentage >= 70
? 'bg-yellow-500'
: 'bg-green-500'
"
:style="{ width: `${Math.min(usagePercentage, 100)}%` }"
/>
</div>
<div class="mt-2 flex items-center justify-between text-xs">
<span class="text-gray-500 dark:text-gray-400">
剩余: ${{ Math.max(0, form.dailyQuota - calculateCurrentUsage()).toFixed(2) }}
</span>
<span class="text-gray-500 dark:text-gray-400">
{{ usagePercentage.toFixed(1) }}% 已使用
</span>
</div>
</div>
<div>
<label class="mb-3 block text-sm font-semibold text-gray-700"
>模型映射表 (可选)</label
@@ -2100,6 +2204,10 @@ const form = ref({
userAgent: props.account?.userAgent || '',
enableRateLimit: props.account ? props.account.rateLimitDuration > 0 : true,
rateLimitDuration: props.account?.rateLimitDuration || 60,
// 额度管理字段
dailyQuota: props.account?.dailyQuota || 0,
dailyUsage: props.account?.dailyUsage || 0,
quotaResetTime: props.account?.quotaResetTime || '00:00',
// Bedrock 特定字段
accessKeyId: props.account?.accessKeyId || '',
secretAccessKey: props.account?.secretAccessKey || '',
@@ -2162,6 +2270,45 @@ const canExchangeSetupToken = computed(() => {
return setupTokenAuthUrl.value && setupTokenAuthCode.value.trim()
})
// 获取当前使用量(实时)
const calculateCurrentUsage = () => {
// 如果不是编辑模式或没有账户ID返回0
if (!isEdit.value || !props.account?.id) {
return 0
}
// 如果已经加载了今日使用数据,直接使用
if (typeof form.value.dailyUsage === 'number') {
return form.value.dailyUsage
}
return 0
}
// 计算额度使用百分比
const usagePercentage = computed(() => {
if (!form.value.dailyQuota || form.value.dailyQuota <= 0) {
return 0
}
const currentUsage = calculateCurrentUsage()
return (currentUsage / form.value.dailyQuota) * 100
})
// 加载账户今日使用情况
const loadAccountUsage = async () => {
if (!isEdit.value || !props.account?.id) return
try {
const response = await apiClient.get(`/admin/claude-console-accounts/${props.account.id}/usage`)
if (response) {
// 更新表单中的使用量数据
form.value.dailyUsage = response.dailyUsage || 0
}
} catch (error) {
console.warn('Failed to load account usage:', error)
}
}
// // 计算是否可以创建
// const canCreate = computed(() => {
// if (form.value.addType === 'manual') {
@@ -2601,6 +2748,9 @@ const createAccount = async () => {
data.userAgent = form.value.userAgent || null
// 如果不启用限流,传递 0 表示不限流
data.rateLimitDuration = form.value.enableRateLimit ? form.value.rateLimitDuration || 60 : 0
// 额度管理字段
data.dailyQuota = form.value.dailyQuota || 0
data.quotaResetTime = form.value.quotaResetTime || '00:00'
} else if (form.value.platform === 'bedrock') {
// Bedrock 账户特定数据 - 构造 awsCredentials 对象
data.awsCredentials = {
@@ -2798,6 +2948,9 @@ const updateAccount = async () => {
data.userAgent = form.value.userAgent || null
// 如果不启用限流,传递 0 表示不限流
data.rateLimitDuration = form.value.enableRateLimit ? form.value.rateLimitDuration || 60 : 0
// 额度管理字段
data.dailyQuota = form.value.dailyQuota || 0
data.quotaResetTime = form.value.quotaResetTime || '00:00'
}
// Bedrock 特定更新
@@ -3207,7 +3360,16 @@ watch(
// Azure OpenAI 特定字段
azureEndpoint: newAccount.azureEndpoint || '',
apiVersion: newAccount.apiVersion || '',
deploymentName: newAccount.deploymentName || ''
deploymentName: newAccount.deploymentName || '',
// 额度管理字段
dailyQuota: newAccount.dailyQuota || 0,
dailyUsage: newAccount.dailyUsage || 0,
quotaResetTime: newAccount.quotaResetTime || '00:00'
}
// 如果是Claude Console账户加载实时使用情况
if (newAccount.platform === 'claude-console') {
loadAccountUsage()
}
// 如果是分组类型加载分组ID
@@ -3287,6 +3449,10 @@ const clearUnifiedCache = async () => {
onMounted(() => {
// 获取Claude Code统一User-Agent信息
fetchUnifiedUserAgent()
// 如果是编辑模式且是Claude Console账户加载使用情况
if (isEdit.value && props.account?.platform === 'claude-console') {
loadAccountUsage()
}
})
// 监听平台变化当切换到Claude平台时获取统一User-Agent信息

View File

@@ -584,6 +584,44 @@
</div>
</div>
</div>
<!-- Claude Console: 显示每日额度使用进度 -->
<div v-else-if="account.platform === 'claude-console'" class="space-y-2">
<div v-if="Number(account.dailyQuota) > 0">
<div class="flex items-center justify-between text-xs">
<span class="text-gray-600 dark:text-gray-300">额度进度</span>
<span class="font-medium text-gray-700 dark:text-gray-200">
{{ getQuotaUsagePercent(account).toFixed(1) }}%
</span>
</div>
<div class="flex items-center gap-2">
<div class="h-2 w-24 rounded-full bg-gray-200 dark:bg-gray-700">
<div
:class="[
'h-2 rounded-full transition-all duration-300',
getQuotaBarClass(getQuotaUsagePercent(account))
]"
:style="{ width: Math.min(100, getQuotaUsagePercent(account)) + '%' }"
/>
</div>
<span
class="min-w-[32px] text-xs font-medium text-gray-700 dark:text-gray-200"
>
${{ formatCost(account.usage?.daily?.cost || 0) }} / ${{
Number(account.dailyQuota).toFixed(2)
}}
</span>
</div>
<div class="text-xs text-gray-600 dark:text-gray-400">
剩余 ${{ formatRemainingQuota(account) }}
<span class="ml-2 text-gray-400"
>重置 {{ account.quotaResetTime || '00:00' }}</span
>
</div>
</div>
<div v-else class="text-sm text-gray-400">
<i class="fas fa-minus" />
</div>
</div>
<div v-else-if="account.platform === 'claude'" class="text-sm text-gray-400">
<i class="fas fa-minus" />
</div>
@@ -1788,6 +1826,29 @@ const formatCost = (cost) => {
return cost.toFixed(2)
}
// 额度使用百分比Claude Console
const getQuotaUsagePercent = (account) => {
const used = Number(account?.usage?.daily?.cost || 0)
const quota = Number(account?.dailyQuota || 0)
if (!quota || quota <= 0) return 0
return (used / quota) * 100
}
// 额度进度条颜色Claude Console
const getQuotaBarClass = (percent) => {
if (percent >= 90) return 'bg-red-500'
if (percent >= 70) return 'bg-yellow-500'
return 'bg-green-500'
}
// 剩余额度Claude Console
const formatRemainingQuota = (account) => {
const used = Number(account?.usage?.daily?.cost || 0)
const quota = Number(account?.dailyQuota || 0)
if (!quota || quota <= 0) return '0.00'
return Math.max(0, quota - used).toFixed(2)
}
// 计算每日费用(使用后端返回的精确费用数据)
const calculateDailyCost = (account) => {
if (!account.usage || !account.usage.daily) return '0.0000'