feat: add AI Credits balance handling and update model status indicators

This commit is contained in:
SilentFlower
2026-03-15 23:50:28 +08:00
committed by erio
parent 17e4033340
commit ced90e1d84
12 changed files with 465 additions and 77 deletions

View File

@@ -76,19 +76,28 @@
</div>
</div>
<!-- Model Rate Limit Indicators (Antigravity OAuth Smart Retry) -->
<!-- Model Status Indicators (普通限流 / 超量请求中) -->
<div
v-if="activeModelRateLimits.length > 0"
v-if="activeModelStatuses.length > 0"
:class="[
activeModelRateLimits.length <= 4
activeModelStatuses.length <= 4
? 'flex flex-col gap-1'
: activeModelRateLimits.length <= 8
: activeModelStatuses.length <= 8
? 'columns-2 gap-x-2'
: 'columns-3 gap-x-2'
]"
>
<div v-for="item in activeModelRateLimits" :key="item.model" class="group relative mb-1 break-inside-avoid">
<div v-for="item in activeModelStatuses" :key="`${item.kind}-${item.model}`" class="group relative mb-1 break-inside-avoid">
<span
v-if="item.kind === 'overages'"
class="inline-flex items-center gap-1 rounded bg-amber-100 px-1.5 py-0.5 text-xs font-medium text-amber-700 dark:bg-amber-900/30 dark:text-amber-400"
>
<span></span>
{{ formatScopeName(item.model) }}
<span class="text-[10px] opacity-70">{{ formatModelResetTime(item.reset_at) }}</span>
</span>
<span
v-else
class="inline-flex items-center gap-1 rounded bg-purple-100 px-1.5 py-0.5 text-xs font-medium text-purple-700 dark:bg-purple-900/30 dark:text-purple-400"
>
<Icon name="exclamationTriangle" size="xs" :stroke-width="2" />
@@ -99,7 +108,11 @@
<div
class="pointer-events-none absolute bottom-full left-1/2 z-50 mb-2 w-56 -translate-x-1/2 whitespace-normal rounded bg-gray-900 px-3 py-2 text-center text-xs leading-relaxed text-white opacity-0 transition-opacity group-hover:opacity-100 dark:bg-gray-700"
>
{{ t('admin.accounts.status.modelRateLimitedUntil', { model: formatScopeName(item.model), time: formatTime(item.reset_at) }) }}
{{
item.kind === 'overages'
? t('admin.accounts.status.modelCreditOveragesUntil', { model: formatScopeName(item.model), time: formatTime(item.reset_at) })
: t('admin.accounts.status.modelRateLimitedUntil', { model: formatScopeName(item.model), time: formatTime(item.reset_at) })
}}
<div
class="absolute left-1/2 top-full -translate-x-1/2 border-4 border-transparent border-t-gray-900 dark:border-t-gray-700"
></div>
@@ -131,6 +144,7 @@
<script setup lang="ts">
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import Icon from '@/components/icons/Icon.vue'
import type { Account } from '@/types'
import { formatCountdown, formatDateTime, formatCountdownWithSuffix, formatTime } from '@/utils/format'
@@ -150,17 +164,37 @@ const isRateLimited = computed(() => {
return new Date(props.account.rate_limit_reset_at) > new Date()
})
type AccountModelStatusItem = {
kind: 'rate_limit' | 'overages'
model: string
reset_at: string
}
// Computed: active model rate limits (Antigravity OAuth Smart Retry)
const activeModelRateLimits = computed(() => {
const modelLimits = (props.account.extra as Record<string, unknown> | undefined)?.model_rate_limits as
// Computed: active model statuses (普通模型限流 + 超量请求运行态)
const activeModelStatuses = computed<AccountModelStatusItem[]>(() => {
const extra = props.account.extra as Record<string, unknown> | undefined
const modelLimits = extra?.model_rate_limits as
| Record<string, { rate_limited_at: string; rate_limit_reset_at: string }>
| undefined
if (!modelLimits) return []
const now = new Date()
return Object.entries(modelLimits)
.filter(([, info]) => new Date(info.rate_limit_reset_at) > now)
.map(([model, info]) => ({ model, reset_at: info.rate_limit_reset_at }))
const items: AccountModelStatusItem[] = []
if (modelLimits) {
items.push(...Object.entries(modelLimits)
.filter(([, info]) => new Date(info.rate_limit_reset_at) > now)
.map(([model, info]) => ({ kind: 'rate_limit' as const, model, reset_at: info.rate_limit_reset_at })))
}
const overagesStates = extra?.antigravity_credits_overages as
| Record<string, { activated_at?: string; active_until: string }>
| undefined
if (overagesStates) {
items.push(...Object.entries(overagesStates)
.filter(([, info]) => new Date(info.active_until) > now)
.map(([model, info]) => ({ kind: 'overages' as const, model, reset_at: info.active_until })))
}
return items
})
const formatScopeName = (scope: string): string => {