feat(antigravity): 添加配额窗口显示功能

后端:
- 新增 AntigravityQuotaRefresher 定时刷新配额
- Client 添加 FetchAvailableModels 方法获取模型配额
- 配额数据存入 account.extra.quota 字段

前端:
- AccountUsageCell 支持显示 Antigravity 账户配额
- UsageProgressBar 新增 amber 颜色
- 显示 G3P/G3F/G3I/C4.5 四个配额进度条
This commit is contained in:
song
2025-12-28 22:29:01 +08:00
parent b6b739431c
commit 08ce6de4db
9 changed files with 420 additions and 5 deletions

View File

@@ -93,6 +93,48 @@
<div v-else class="text-xs text-gray-400">-</div>
</template>
<!-- Antigravity OAuth accounts: show quota from extra field -->
<template v-else-if="account.platform === 'antigravity' && account.type === 'oauth'">
<div v-if="hasAntigravityQuota" class="space-y-1">
<!-- Gemini 3 Pro -->
<UsageProgressBar
v-if="antigravity3ProUsage !== null"
:label="t('admin.accounts.usageWindow.gemini3Pro')"
:utilization="antigravity3ProUsage.utilization"
:resets-at="antigravity3ProUsage.resetTime"
color="indigo"
/>
<!-- Gemini 3 Flash -->
<UsageProgressBar
v-if="antigravity3FlashUsage !== null"
:label="t('admin.accounts.usageWindow.gemini3Flash')"
:utilization="antigravity3FlashUsage.utilization"
:resets-at="antigravity3FlashUsage.resetTime"
color="emerald"
/>
<!-- Gemini 3 Image -->
<UsageProgressBar
v-if="antigravity3ImageUsage !== null"
:label="t('admin.accounts.usageWindow.gemini3Image')"
:utilization="antigravity3ImageUsage.utilization"
:resets-at="antigravity3ImageUsage.resetTime"
color="purple"
/>
<!-- Claude 4.5 -->
<UsageProgressBar
v-if="antigravityClaude45Usage !== null"
:label="t('admin.accounts.usageWindow.claude45')"
:utilization="antigravityClaude45Usage.utilization"
:resets-at="antigravityClaude45Usage.resetTime"
color="amber"
/>
</div>
<div v-else class="text-xs text-gray-400">-</div>
</template>
<!-- Other accounts: no usage window -->
<template v-else>
<div class="text-xs text-gray-400">-</div>
@@ -273,6 +315,82 @@ const codex7dResetAt = computed(() => {
return null
})
// Antigravity quota types
interface AntigravityModelQuota {
remaining: number // 剩余百分比 0-100
reset_time: string // ISO 8601 重置时间
}
interface AntigravityQuotaData {
[model: string]: AntigravityModelQuota
}
interface AntigravityUsageResult {
utilization: number
resetTime: string | null
}
// Antigravity quota computed properties
const hasAntigravityQuota = computed(() => {
const extra = props.account.extra as Record<string, unknown> | undefined
return extra && typeof extra.quota === 'object' && extra.quota !== null
})
// 从配额数据中获取使用率(多模型取最低剩余 = 最高使用)
const getAntigravityUsage = (
modelNames: string[]
): AntigravityUsageResult | null => {
const extra = props.account.extra as Record<string, unknown> | undefined
if (!extra || typeof extra.quota !== 'object' || extra.quota === null) return null
const quota = extra.quota as AntigravityQuotaData
let minRemaining = 100
let earliestReset: string | null = null
for (const model of modelNames) {
const modelQuota = quota[model]
if (!modelQuota) continue
if (modelQuota.remaining < minRemaining) {
minRemaining = modelQuota.remaining
}
if (modelQuota.reset_time) {
if (!earliestReset || modelQuota.reset_time < earliestReset) {
earliestReset = modelQuota.reset_time
}
}
}
// 如果没有找到任何匹配的模型
if (minRemaining === 100 && earliestReset === null) {
// 检查是否至少有一个模型有数据
const hasAnyData = modelNames.some((m) => quota[m])
if (!hasAnyData) return null
}
return {
utilization: 100 - minRemaining,
resetTime: earliestReset
}
}
// Gemini 3 Pro: gemini-3-pro-low, gemini-3-pro-high, gemini-3-pro-preview
const antigravity3ProUsage = computed(() =>
getAntigravityUsage(['gemini-3-pro-low', 'gemini-3-pro-high', 'gemini-3-pro-preview'])
)
// Gemini 3 Flash: gemini-3-flash
const antigravity3FlashUsage = computed(() => getAntigravityUsage(['gemini-3-flash']))
// Gemini 3 Image: gemini-3-pro-image
const antigravity3ImageUsage = computed(() => getAntigravityUsage(['gemini-3-pro-image']))
// Claude 4.5: claude-sonnet-4-5, claude-opus-4-5-thinking
const antigravityClaude45Usage = computed(() =>
getAntigravityUsage(['claude-sonnet-4-5', 'claude-opus-4-5-thinking'])
)
const loadUsage = async () => {
// Fetch usage for Anthropic OAuth and Setup Token accounts
// OpenAI usage comes from account.extra field (updated during forwarding)

View File

@@ -58,7 +58,7 @@ const props = defineProps<{
label: string
utilization: number // Percentage (0-100+)
resetsAt?: string | null
color: 'indigo' | 'emerald' | 'purple'
color: 'indigo' | 'emerald' | 'purple' | 'amber'
windowStats?: WindowStats | null
}>()
@@ -69,7 +69,8 @@ const labelClass = computed(() => {
const colors = {
indigo: 'bg-indigo-100 text-indigo-700 dark:bg-indigo-900/40 dark:text-indigo-300',
emerald: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/40 dark:text-emerald-300',
purple: 'bg-purple-100 text-purple-700 dark:bg-purple-900/40 dark:text-purple-300'
purple: 'bg-purple-100 text-purple-700 dark:bg-purple-900/40 dark:text-purple-300',
amber: 'bg-amber-100 text-amber-700 dark:bg-amber-900/40 dark:text-amber-300'
}
return colors[props.color]
})

View File

@@ -1157,7 +1157,11 @@ export default {
noData: 'No usage data available for this account'
},
usageWindow: {
statsTitle: '5-Hour Window Usage Statistics'
statsTitle: '5-Hour Window Usage Statistics',
gemini3Pro: 'G3P',
gemini3Flash: 'G3F',
gemini3Image: 'G3I',
claude45: 'C4.5'
}
},

View File

@@ -966,7 +966,11 @@ export default {
cooldown: '冷却中'
},
usageWindow: {
statsTitle: '5小时窗口用量统计'
statsTitle: '5小时窗口用量统计',
gemini3Pro: 'G3P',
gemini3Flash: 'G3F',
gemini3Image: 'G3I',
claude45: 'C4.5'
},
form: {
nameLabel: '账号名称',