2025-12-18 13:50:39 +08:00
|
|
|
|
<template>
|
2025-12-22 22:58:31 +08:00
|
|
|
|
<div v-if="showUsageWindows">
|
2025-12-24 10:57:40 +08:00
|
|
|
|
<!-- Anthropic OAuth and Setup Token accounts: fetch real usage data -->
|
2025-12-25 08:40:12 -08:00
|
|
|
|
<template
|
|
|
|
|
|
v-if="
|
|
|
|
|
|
account.platform === 'anthropic' &&
|
|
|
|
|
|
(account.type === 'oauth' || account.type === 'setup-token')
|
|
|
|
|
|
"
|
|
|
|
|
|
>
|
2025-12-18 13:50:39 +08:00
|
|
|
|
<!-- Loading state -->
|
|
|
|
|
|
<div v-if="loading" class="space-y-1.5">
|
2025-12-24 10:57:40 +08:00
|
|
|
|
<!-- OAuth: 3 rows, Setup Token: 1 row -->
|
2025-12-18 13:50:39 +08:00
|
|
|
|
<div class="flex items-center gap-1">
|
2025-12-25 08:40:12 -08:00
|
|
|
|
<div class="h-3 w-[32px] animate-pulse rounded bg-gray-200 dark:bg-gray-700"></div>
|
|
|
|
|
|
<div class="h-1.5 w-8 animate-pulse rounded-full bg-gray-200 dark:bg-gray-700"></div>
|
|
|
|
|
|
<div class="h-3 w-[32px] animate-pulse rounded bg-gray-200 dark:bg-gray-700"></div>
|
2025-12-18 13:50:39 +08:00
|
|
|
|
</div>
|
2025-12-24 10:57:40 +08:00
|
|
|
|
<template v-if="account.type === 'oauth'">
|
|
|
|
|
|
<div class="flex items-center gap-1">
|
2025-12-25 08:40:12 -08:00
|
|
|
|
<div class="h-3 w-[32px] animate-pulse rounded bg-gray-200 dark:bg-gray-700"></div>
|
|
|
|
|
|
<div class="h-1.5 w-8 animate-pulse rounded-full bg-gray-200 dark:bg-gray-700"></div>
|
|
|
|
|
|
<div class="h-3 w-[32px] animate-pulse rounded bg-gray-200 dark:bg-gray-700"></div>
|
2025-12-24 10:57:40 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="flex items-center gap-1">
|
2025-12-25 08:40:12 -08:00
|
|
|
|
<div class="h-3 w-[32px] animate-pulse rounded bg-gray-200 dark:bg-gray-700"></div>
|
|
|
|
|
|
<div class="h-1.5 w-8 animate-pulse rounded-full bg-gray-200 dark:bg-gray-700"></div>
|
|
|
|
|
|
<div class="h-3 w-[32px] animate-pulse rounded bg-gray-200 dark:bg-gray-700"></div>
|
2025-12-24 10:57:40 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
2025-12-18 13:50:39 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Error state -->
|
|
|
|
|
|
<div v-else-if="error" class="text-xs text-red-500">
|
|
|
|
|
|
{{ error }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Usage data -->
|
|
|
|
|
|
<div v-else-if="usageInfo" class="space-y-1">
|
|
|
|
|
|
<!-- 5h Window -->
|
|
|
|
|
|
<UsageProgressBar
|
|
|
|
|
|
v-if="usageInfo.five_hour"
|
|
|
|
|
|
label="5h"
|
|
|
|
|
|
:utilization="usageInfo.five_hour.utilization"
|
|
|
|
|
|
:resets-at="usageInfo.five_hour.resets_at"
|
|
|
|
|
|
:window-stats="usageInfo.five_hour.window_stats"
|
|
|
|
|
|
color="indigo"
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
2025-12-24 10:57:40 +08:00
|
|
|
|
<!-- 7d Window (OAuth only) -->
|
2025-12-18 13:50:39 +08:00
|
|
|
|
<UsageProgressBar
|
|
|
|
|
|
v-if="usageInfo.seven_day"
|
|
|
|
|
|
label="7d"
|
|
|
|
|
|
:utilization="usageInfo.seven_day.utilization"
|
|
|
|
|
|
:resets-at="usageInfo.seven_day.resets_at"
|
|
|
|
|
|
color="emerald"
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
2025-12-24 10:57:40 +08:00
|
|
|
|
<!-- 7d Sonnet Window (OAuth only) -->
|
2025-12-18 13:50:39 +08:00
|
|
|
|
<UsageProgressBar
|
|
|
|
|
|
v-if="usageInfo.seven_day_sonnet"
|
|
|
|
|
|
label="7d S"
|
|
|
|
|
|
:utilization="usageInfo.seven_day_sonnet.utilization"
|
|
|
|
|
|
:resets-at="usageInfo.seven_day_sonnet.resets_at"
|
|
|
|
|
|
color="purple"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- No data yet -->
|
2025-12-25 08:40:12 -08:00
|
|
|
|
<div v-else class="text-xs text-gray-400">-</div>
|
2025-12-18 13:50:39 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
|
2025-12-23 16:26:07 +08:00
|
|
|
|
<!-- OpenAI OAuth accounts: show Codex usage from extra field -->
|
|
|
|
|
|
<template v-else-if="account.platform === 'openai' && account.type === 'oauth'">
|
|
|
|
|
|
<div v-if="hasCodexUsage" class="space-y-1">
|
2025-12-25 17:00:02 +08:00
|
|
|
|
<!-- 5h Window -->
|
2025-12-23 16:26:07 +08:00
|
|
|
|
<UsageProgressBar
|
2025-12-25 17:00:02 +08:00
|
|
|
|
v-if="codex5hUsedPercent !== null"
|
2025-12-23 16:26:07 +08:00
|
|
|
|
label="5h"
|
2025-12-25 17:00:02 +08:00
|
|
|
|
:utilization="codex5hUsedPercent"
|
|
|
|
|
|
:resets-at="codex5hResetAt"
|
2025-12-23 16:26:07 +08:00
|
|
|
|
color="indigo"
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
2025-12-25 17:00:02 +08:00
|
|
|
|
<!-- 7d Window -->
|
2025-12-23 16:26:07 +08:00
|
|
|
|
<UsageProgressBar
|
2025-12-25 17:00:02 +08:00
|
|
|
|
v-if="codex7dUsedPercent !== null"
|
2025-12-23 16:26:07 +08:00
|
|
|
|
label="7d"
|
2025-12-25 17:00:02 +08:00
|
|
|
|
:utilization="codex7dUsedPercent"
|
|
|
|
|
|
:resets-at="codex7dResetAt"
|
2025-12-23 16:26:07 +08:00
|
|
|
|
color="emerald"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div v-else class="text-xs text-gray-400">-</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
2026-01-03 06:32:04 -08:00
|
|
|
|
<!-- Antigravity OAuth accounts: fetch usage from API -->
|
2025-12-28 22:29:01 +08:00
|
|
|
|
<template v-else-if="account.platform === 'antigravity' && account.type === 'oauth'">
|
2025-12-29 01:25:09 +08:00
|
|
|
|
<!-- 账户类型徽章 -->
|
2025-12-31 00:34:24 +08:00
|
|
|
|
<div v-if="antigravityTierLabel" class="mb-1 flex items-center gap-1">
|
2025-12-29 01:25:09 +08:00
|
|
|
|
<span
|
|
|
|
|
|
:class="[
|
|
|
|
|
|
'inline-block rounded px-1.5 py-0.5 text-[10px] font-medium',
|
|
|
|
|
|
antigravityTierClass
|
|
|
|
|
|
]"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ antigravityTierLabel }}
|
|
|
|
|
|
</span>
|
2025-12-31 00:34:24 +08:00
|
|
|
|
<!-- 不合格账户警告图标 -->
|
|
|
|
|
|
<span
|
|
|
|
|
|
v-if="hasIneligibleTiers"
|
|
|
|
|
|
class="group relative cursor-help"
|
|
|
|
|
|
>
|
|
|
|
|
|
<svg
|
|
|
|
|
|
class="h-3.5 w-3.5 text-red-500"
|
|
|
|
|
|
fill="currentColor"
|
|
|
|
|
|
viewBox="0 0 20 20"
|
|
|
|
|
|
>
|
|
|
|
|
|
<path
|
|
|
|
|
|
fill-rule="evenodd"
|
|
|
|
|
|
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z"
|
|
|
|
|
|
clip-rule="evenodd"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
<span
|
|
|
|
|
|
class="pointer-events-none absolute left-0 top-full z-50 mt-1 w-80 whitespace-normal break-words rounded bg-gray-900 px-3 py-2 text-xs leading-relaxed text-white opacity-0 shadow-lg transition-opacity group-hover:opacity-100 dark:bg-gray-700"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ t('admin.accounts.ineligibleWarning') }}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</span>
|
2025-12-29 01:25:09 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-01-03 06:32:04 -08:00
|
|
|
|
<!-- Loading state -->
|
|
|
|
|
|
<div v-if="loading" class="space-y-1.5">
|
|
|
|
|
|
<div class="flex items-center gap-1">
|
|
|
|
|
|
<div class="h-3 w-[32px] animate-pulse rounded bg-gray-200 dark:bg-gray-700"></div>
|
|
|
|
|
|
<div class="h-1.5 w-8 animate-pulse rounded-full bg-gray-200 dark:bg-gray-700"></div>
|
|
|
|
|
|
<div class="h-3 w-[32px] animate-pulse rounded bg-gray-200 dark:bg-gray-700"></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Error state -->
|
|
|
|
|
|
<div v-else-if="error" class="text-xs text-red-500">
|
|
|
|
|
|
{{ error }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Usage data from API -->
|
|
|
|
|
|
<div v-else-if="hasAntigravityQuotaFromAPI" class="space-y-1">
|
2025-12-28 22:29:01 +08:00
|
|
|
|
<!-- Gemini 3 Pro -->
|
|
|
|
|
|
<UsageProgressBar
|
2026-01-03 06:32:04 -08:00
|
|
|
|
v-if="antigravity3ProUsageFromAPI !== null"
|
2025-12-28 22:29:01 +08:00
|
|
|
|
:label="t('admin.accounts.usageWindow.gemini3Pro')"
|
2026-01-03 06:32:04 -08:00
|
|
|
|
:utilization="antigravity3ProUsageFromAPI.utilization"
|
|
|
|
|
|
:resets-at="antigravity3ProUsageFromAPI.resetTime"
|
2025-12-28 22:29:01 +08:00
|
|
|
|
color="indigo"
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Gemini 3 Flash -->
|
|
|
|
|
|
<UsageProgressBar
|
2026-01-03 06:32:04 -08:00
|
|
|
|
v-if="antigravity3FlashUsageFromAPI !== null"
|
2025-12-28 22:29:01 +08:00
|
|
|
|
:label="t('admin.accounts.usageWindow.gemini3Flash')"
|
2026-01-03 06:32:04 -08:00
|
|
|
|
:utilization="antigravity3FlashUsageFromAPI.utilization"
|
|
|
|
|
|
:resets-at="antigravity3FlashUsageFromAPI.resetTime"
|
2025-12-28 22:29:01 +08:00
|
|
|
|
color="emerald"
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Gemini 3 Image -->
|
|
|
|
|
|
<UsageProgressBar
|
2026-01-03 06:32:04 -08:00
|
|
|
|
v-if="antigravity3ImageUsageFromAPI !== null"
|
2025-12-28 22:29:01 +08:00
|
|
|
|
:label="t('admin.accounts.usageWindow.gemini3Image')"
|
2026-01-03 06:32:04 -08:00
|
|
|
|
:utilization="antigravity3ImageUsageFromAPI.utilization"
|
|
|
|
|
|
:resets-at="antigravity3ImageUsageFromAPI.resetTime"
|
2025-12-28 22:29:01 +08:00
|
|
|
|
color="purple"
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
2026-02-24 16:44:18 +08:00
|
|
|
|
<!-- Claude -->
|
2025-12-28 22:29:01 +08:00
|
|
|
|
<UsageProgressBar
|
2026-02-24 16:44:18 +08:00
|
|
|
|
v-if="antigravityClaudeUsageFromAPI !== null"
|
|
|
|
|
|
:label="t('admin.accounts.usageWindow.claude')"
|
|
|
|
|
|
:utilization="antigravityClaudeUsageFromAPI.utilization"
|
|
|
|
|
|
:resets-at="antigravityClaudeUsageFromAPI.resetTime"
|
2025-12-28 22:29:01 +08:00
|
|
|
|
color="amber"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div v-else class="text-xs text-gray-400">-</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
2026-01-01 04:22:50 +08:00
|
|
|
|
<!-- Gemini platform: show quota + local usage window -->
|
2025-12-31 23:57:01 +08:00
|
|
|
|
<template v-else-if="account.platform === 'gemini'">
|
2026-01-03 06:32:04 -08:00
|
|
|
|
<!-- Auth Type + Tier Badge (first line) -->
|
|
|
|
|
|
<div v-if="geminiAuthTypeLabel" class="mb-1 flex items-center gap-1">
|
2026-01-01 08:29:57 +08:00
|
|
|
|
<span
|
|
|
|
|
|
:class="[
|
|
|
|
|
|
'inline-block rounded px-1.5 py-0.5 text-[10px] font-medium',
|
|
|
|
|
|
geminiTierClass
|
|
|
|
|
|
]"
|
|
|
|
|
|
>
|
2026-01-03 06:32:04 -08:00
|
|
|
|
{{ geminiAuthTypeLabel }}
|
2026-01-01 08:29:57 +08:00
|
|
|
|
</span>
|
2026-01-03 06:32:04 -08:00
|
|
|
|
<!-- Help icon -->
|
2026-01-01 08:29:57 +08:00
|
|
|
|
<span
|
|
|
|
|
|
class="group relative cursor-help"
|
|
|
|
|
|
>
|
|
|
|
|
|
<svg
|
|
|
|
|
|
class="h-3.5 w-3.5 text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300"
|
|
|
|
|
|
fill="currentColor"
|
|
|
|
|
|
viewBox="0 0 20 20"
|
|
|
|
|
|
>
|
|
|
|
|
|
<path
|
|
|
|
|
|
fill-rule="evenodd"
|
|
|
|
|
|
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-8-3a1 1 0 00-.867.5 1 1 0 11-1.731-1A3 3 0 0113 8a3.001 3.001 0 01-2 2.83V11a1 1 0 11-2 0v-1a1 1 0 011-1 1 1 0 100-2zm0 8a1 1 0 100-2 1 1 0 000 2z"
|
|
|
|
|
|
clip-rule="evenodd"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
<span
|
|
|
|
|
|
class="pointer-events-none absolute left-0 top-full z-50 mt-1 w-80 whitespace-normal break-words rounded bg-gray-900 px-3 py-2 text-xs leading-relaxed text-white opacity-0 shadow-lg transition-opacity group-hover:opacity-100 dark:bg-gray-700"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="font-semibold mb-1">{{ t('admin.accounts.gemini.quotaPolicy.title') }}</div>
|
|
|
|
|
|
<div class="mb-2 text-gray-300">{{ t('admin.accounts.gemini.quotaPolicy.note') }}</div>
|
|
|
|
|
|
<div class="space-y-1">
|
|
|
|
|
|
<div><strong>{{ geminiQuotaPolicyChannel }}:</strong></div>
|
|
|
|
|
|
<div class="pl-2">• {{ geminiQuotaPolicyLimits }}</div>
|
|
|
|
|
|
<div class="mt-2">
|
2026-01-03 06:32:04 -08:00
|
|
|
|
<a :href="geminiQuotaPolicyDocsUrl" target="_blank" rel="noopener noreferrer" class="text-blue-400 hover:text-blue-300 underline">
|
2026-01-01 08:29:57 +08:00
|
|
|
|
{{ t('admin.accounts.gemini.quotaPolicy.columns.docs') }} →
|
|
|
|
|
|
</a>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-01-04 10:22:02 +08:00
|
|
|
|
<!-- Usage data or unlimited flow -->
|
2026-01-01 04:22:50 +08:00
|
|
|
|
<div class="space-y-1">
|
|
|
|
|
|
<div v-if="loading" class="space-y-1">
|
|
|
|
|
|
<div class="flex items-center gap-1">
|
|
|
|
|
|
<div class="h-3 w-[32px] animate-pulse rounded bg-gray-200 dark:bg-gray-700"></div>
|
|
|
|
|
|
<div class="h-1.5 w-8 animate-pulse rounded-full bg-gray-200 dark:bg-gray-700"></div>
|
|
|
|
|
|
<div class="h-3 w-[32px] animate-pulse rounded bg-gray-200 dark:bg-gray-700"></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div v-else-if="error" class="text-xs text-red-500">
|
|
|
|
|
|
{{ error }}
|
|
|
|
|
|
</div>
|
2026-01-04 15:36:00 +08:00
|
|
|
|
<!-- Gemini: show daily usage bars when available -->
|
2026-01-04 10:42:37 +08:00
|
|
|
|
<div v-else-if="geminiUsageAvailable" class="space-y-1">
|
2026-01-01 04:22:50 +08:00
|
|
|
|
<UsageProgressBar
|
2026-01-04 15:36:00 +08:00
|
|
|
|
v-for="bar in geminiUsageBars"
|
|
|
|
|
|
:key="bar.key"
|
|
|
|
|
|
:label="bar.label"
|
|
|
|
|
|
:utilization="bar.utilization"
|
|
|
|
|
|
:resets-at="bar.resetsAt"
|
|
|
|
|
|
:window-stats="bar.windowStats"
|
|
|
|
|
|
:color="bar.color"
|
2026-01-01 04:22:50 +08:00
|
|
|
|
/>
|
|
|
|
|
|
<p class="mt-1 text-[9px] leading-tight text-gray-400 dark:text-gray-500 italic">
|
|
|
|
|
|
* {{ t('admin.accounts.gemini.quotaPolicy.simulatedNote') || 'Simulated quota' }}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
2026-01-04 10:42:37 +08:00
|
|
|
|
<!-- AI Studio Client OAuth: show unlimited flow (no usage tracking) -->
|
2026-01-04 10:22:02 +08:00
|
|
|
|
<div v-else class="text-xs text-gray-400">
|
|
|
|
|
|
{{ t('admin.accounts.gemini.rateLimit.unlimited') }}
|
|
|
|
|
|
</div>
|
2026-01-01 04:22:50 +08:00
|
|
|
|
</div>
|
2025-12-31 23:57:01 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
|
2025-12-23 16:26:07 +08:00
|
|
|
|
<!-- Other accounts: no usage window -->
|
2025-12-22 22:58:31 +08:00
|
|
|
|
<template v-else>
|
|
|
|
|
|
<div class="text-xs text-gray-400">-</div>
|
|
|
|
|
|
</template>
|
2025-12-18 13:50:39 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-12-22 22:58:31 +08:00
|
|
|
|
<!-- Non-OAuth/Setup-Token accounts -->
|
2025-12-31 23:57:01 +08:00
|
|
|
|
<div v-else>
|
|
|
|
|
|
<!-- Gemini API Key accounts: show quota info -->
|
|
|
|
|
|
<AccountQuotaInfo v-if="account.platform === 'gemini'" :account="account" />
|
|
|
|
|
|
<div v-else class="text-xs text-gray-400">-</div>
|
|
|
|
|
|
</div>
|
2025-12-18 13:50:39 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
2026-03-05 19:12:49 +08:00
|
|
|
|
import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
|
2025-12-27 10:50:25 +08:00
|
|
|
|
import { useI18n } from 'vue-i18n'
|
2025-12-18 13:50:39 +08:00
|
|
|
|
import { adminAPI } from '@/api/admin'
|
2026-01-04 15:36:00 +08:00
|
|
|
|
import type { Account, AccountUsageInfo, GeminiCredentials, WindowStats } from '@/types'
|
2026-02-22 21:04:52 +08:00
|
|
|
|
import { resolveCodexUsageWindow } from '@/utils/codexUsage'
|
2026-03-05 19:12:49 +08:00
|
|
|
|
import { enqueueUsageRequest } from '@/utils/usageLoadQueue'
|
2025-12-18 13:50:39 +08:00
|
|
|
|
import UsageProgressBar from './UsageProgressBar.vue'
|
2025-12-31 23:57:01 +08:00
|
|
|
|
import AccountQuotaInfo from './AccountQuotaInfo.vue'
|
2025-12-18 13:50:39 +08:00
|
|
|
|
|
|
|
|
|
|
const props = defineProps<{
|
|
|
|
|
|
account: Account
|
|
|
|
|
|
}>()
|
|
|
|
|
|
|
2025-12-27 10:50:25 +08:00
|
|
|
|
const { t } = useI18n()
|
|
|
|
|
|
|
2026-03-05 19:12:49 +08:00
|
|
|
|
const unmounted = ref(false)
|
|
|
|
|
|
onBeforeUnmount(() => { unmounted.value = true })
|
|
|
|
|
|
|
2025-12-18 13:50:39 +08:00
|
|
|
|
const loading = ref(false)
|
|
|
|
|
|
const error = ref<string | null>(null)
|
|
|
|
|
|
const usageInfo = ref<AccountUsageInfo | null>(null)
|
|
|
|
|
|
|
2025-12-22 22:58:31 +08:00
|
|
|
|
// Show usage windows for OAuth and Setup Token accounts
|
2026-01-04 15:36:00 +08:00
|
|
|
|
const showUsageWindows = computed(() => {
|
|
|
|
|
|
// Gemini: we can always compute local usage windows from DB logs (simulated quotas).
|
|
|
|
|
|
if (props.account.platform === 'gemini') return true
|
|
|
|
|
|
return props.account.type === 'oauth' || props.account.type === 'setup-token'
|
|
|
|
|
|
})
|
2025-12-22 22:58:31 +08:00
|
|
|
|
|
2026-01-01 04:22:50 +08:00
|
|
|
|
const shouldFetchUsage = computed(() => {
|
|
|
|
|
|
if (props.account.platform === 'anthropic') {
|
|
|
|
|
|
return props.account.type === 'oauth' || props.account.type === 'setup-token'
|
|
|
|
|
|
}
|
|
|
|
|
|
if (props.account.platform === 'gemini') {
|
2026-01-04 15:36:00 +08:00
|
|
|
|
return true
|
2026-01-01 04:22:50 +08:00
|
|
|
|
}
|
2026-01-03 06:32:04 -08:00
|
|
|
|
if (props.account.platform === 'antigravity') {
|
|
|
|
|
|
return props.account.type === 'oauth'
|
|
|
|
|
|
}
|
2026-01-01 04:22:50 +08:00
|
|
|
|
return false
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const geminiUsageAvailable = computed(() => {
|
|
|
|
|
|
return (
|
2026-01-04 15:36:00 +08:00
|
|
|
|
!!usageInfo.value?.gemini_shared_daily ||
|
2026-01-01 04:22:50 +08:00
|
|
|
|
!!usageInfo.value?.gemini_pro_daily ||
|
2026-01-04 15:36:00 +08:00
|
|
|
|
!!usageInfo.value?.gemini_flash_daily ||
|
|
|
|
|
|
!!usageInfo.value?.gemini_shared_minute ||
|
|
|
|
|
|
!!usageInfo.value?.gemini_pro_minute ||
|
|
|
|
|
|
!!usageInfo.value?.gemini_flash_minute
|
2026-01-01 04:22:50 +08:00
|
|
|
|
)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-02-22 21:04:52 +08:00
|
|
|
|
const codex5hWindow = computed(() => resolveCodexUsageWindow(props.account.extra, '5h'))
|
|
|
|
|
|
const codex7dWindow = computed(() => resolveCodexUsageWindow(props.account.extra, '7d'))
|
|
|
|
|
|
|
2025-12-23 16:26:07 +08:00
|
|
|
|
// OpenAI Codex usage computed properties
|
|
|
|
|
|
const hasCodexUsage = computed(() => {
|
2026-02-22 21:04:52 +08:00
|
|
|
|
return codex5hWindow.value.usedPercent !== null || codex7dWindow.value.usedPercent !== null
|
2025-12-23 16:26:07 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
2026-02-22 21:04:52 +08:00
|
|
|
|
const codex5hUsedPercent = computed(() => codex5hWindow.value.usedPercent)
|
|
|
|
|
|
const codex5hResetAt = computed(() => codex5hWindow.value.resetAt)
|
|
|
|
|
|
const codex7dUsedPercent = computed(() => codex7dWindow.value.usedPercent)
|
|
|
|
|
|
const codex7dResetAt = computed(() => codex7dWindow.value.resetAt)
|
2025-12-23 16:26:07 +08:00
|
|
|
|
|
2026-01-03 06:32:04 -08:00
|
|
|
|
// Antigravity quota types (用于 API 返回的数据)
|
2025-12-28 22:29:01 +08:00
|
|
|
|
interface AntigravityUsageResult {
|
|
|
|
|
|
utilization: number
|
|
|
|
|
|
resetTime: string | null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-03 06:32:04 -08:00
|
|
|
|
// ===== Antigravity quota from API (usageInfo.antigravity_quota) =====
|
|
|
|
|
|
|
|
|
|
|
|
// 检查是否有从 API 获取的配额数据
|
|
|
|
|
|
const hasAntigravityQuotaFromAPI = computed(() => {
|
|
|
|
|
|
return usageInfo.value?.antigravity_quota && Object.keys(usageInfo.value.antigravity_quota).length > 0
|
2025-12-28 22:29:01 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
2026-01-03 06:32:04 -08:00
|
|
|
|
// 从 API 配额数据中获取使用率(多模型取最高使用率)
|
|
|
|
|
|
const getAntigravityUsageFromAPI = (
|
2025-12-28 22:29:01 +08:00
|
|
|
|
modelNames: string[]
|
|
|
|
|
|
): AntigravityUsageResult | null => {
|
2026-01-03 06:32:04 -08:00
|
|
|
|
const quota = usageInfo.value?.antigravity_quota
|
|
|
|
|
|
if (!quota) return null
|
2025-12-28 22:29:01 +08:00
|
|
|
|
|
2026-01-03 06:32:04 -08:00
|
|
|
|
let maxUtilization = 0
|
2025-12-28 22:29:01 +08:00
|
|
|
|
let earliestReset: string | null = null
|
|
|
|
|
|
|
|
|
|
|
|
for (const model of modelNames) {
|
|
|
|
|
|
const modelQuota = quota[model]
|
|
|
|
|
|
if (!modelQuota) continue
|
|
|
|
|
|
|
2026-01-03 06:32:04 -08:00
|
|
|
|
if (modelQuota.utilization > maxUtilization) {
|
|
|
|
|
|
maxUtilization = modelQuota.utilization
|
2025-12-28 22:29:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
if (modelQuota.reset_time) {
|
|
|
|
|
|
if (!earliestReset || modelQuota.reset_time < earliestReset) {
|
|
|
|
|
|
earliestReset = modelQuota.reset_time
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 如果没有找到任何匹配的模型
|
2026-01-03 06:32:04 -08:00
|
|
|
|
if (maxUtilization === 0 && earliestReset === null) {
|
2025-12-28 22:29:01 +08:00
|
|
|
|
const hasAnyData = modelNames.some((m) => quota[m])
|
|
|
|
|
|
if (!hasAnyData) return null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
2026-01-03 06:32:04 -08:00
|
|
|
|
utilization: maxUtilization,
|
2025-12-28 22:29:01 +08:00
|
|
|
|
resetTime: earliestReset
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-03 06:32:04 -08:00
|
|
|
|
// Gemini 3 Pro from API
|
|
|
|
|
|
const antigravity3ProUsageFromAPI = computed(() =>
|
|
|
|
|
|
getAntigravityUsageFromAPI(['gemini-3-pro-low', 'gemini-3-pro-high', 'gemini-3-pro-preview'])
|
2025-12-28 22:29:01 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
2026-01-03 06:32:04 -08:00
|
|
|
|
// Gemini 3 Flash from API
|
|
|
|
|
|
const antigravity3FlashUsageFromAPI = computed(() => getAntigravityUsageFromAPI(['gemini-3-flash']))
|
2025-12-28 22:29:01 +08:00
|
|
|
|
|
2026-02-27 09:30:44 +08:00
|
|
|
|
// Gemini Image from API
|
2026-02-28 15:01:20 +08:00
|
|
|
|
const antigravity3ImageUsageFromAPI = computed(() =>
|
|
|
|
|
|
getAntigravityUsageFromAPI(['gemini-3.1-flash-image', 'gemini-3-pro-image'])
|
|
|
|
|
|
)
|
2025-12-28 22:29:01 +08:00
|
|
|
|
|
2026-02-24 16:44:18 +08:00
|
|
|
|
// Claude from API (all Claude model variants)
|
|
|
|
|
|
const antigravityClaudeUsageFromAPI = computed(() =>
|
|
|
|
|
|
getAntigravityUsageFromAPI([
|
|
|
|
|
|
'claude-sonnet-4-5', 'claude-opus-4-5-thinking',
|
2026-02-26 14:27:51 +08:00
|
|
|
|
'claude-sonnet-4-6', 'claude-opus-4-6', 'claude-opus-4-6-thinking',
|
2026-02-24 16:44:18 +08:00
|
|
|
|
])
|
2025-12-28 22:29:01 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
2025-12-31 00:15:25 +08:00
|
|
|
|
// Antigravity 账户类型(从 load_code_assist 响应中提取)
|
2025-12-29 01:25:09 +08:00
|
|
|
|
const antigravityTier = computed(() => {
|
|
|
|
|
|
const extra = props.account.extra as Record<string, unknown> | undefined
|
2025-12-31 00:15:25 +08:00
|
|
|
|
if (!extra) return null
|
|
|
|
|
|
|
|
|
|
|
|
const loadCodeAssist = extra.load_code_assist as Record<string, unknown> | undefined
|
|
|
|
|
|
if (!loadCodeAssist) return null
|
|
|
|
|
|
|
|
|
|
|
|
// 优先取 paidTier,否则取 currentTier
|
|
|
|
|
|
const paidTier = loadCodeAssist.paidTier as Record<string, unknown> | undefined
|
|
|
|
|
|
if (paidTier && typeof paidTier.id === 'string') {
|
|
|
|
|
|
return paidTier.id
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const currentTier = loadCodeAssist.currentTier as Record<string, unknown> | undefined
|
|
|
|
|
|
if (currentTier && typeof currentTier.id === 'string') {
|
|
|
|
|
|
return currentTier.id
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return null
|
2025-12-29 01:25:09 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
2026-01-01 08:29:57 +08:00
|
|
|
|
// Gemini 账户类型(从 credentials 中提取)
|
|
|
|
|
|
const geminiTier = computed(() => {
|
|
|
|
|
|
if (props.account.platform !== 'gemini') return null
|
|
|
|
|
|
const creds = props.account.credentials as GeminiCredentials | undefined
|
|
|
|
|
|
return creds?.tier_id || null
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-01-04 15:36:00 +08:00
|
|
|
|
const geminiOAuthType = computed(() => {
|
|
|
|
|
|
if (props.account.platform !== 'gemini') return null
|
|
|
|
|
|
const creds = props.account.credentials as GeminiCredentials | undefined
|
|
|
|
|
|
return (creds?.oauth_type || '').trim() || null
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-01-01 08:29:57 +08:00
|
|
|
|
// Gemini 是否为 Code Assist OAuth
|
|
|
|
|
|
const isGeminiCodeAssist = computed(() => {
|
|
|
|
|
|
if (props.account.platform !== 'gemini') return false
|
|
|
|
|
|
const creds = props.account.credentials as GeminiCredentials | undefined
|
|
|
|
|
|
return creds?.oauth_type === 'code_assist' || (!creds?.oauth_type && !!creds?.project_id)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-01-04 15:36:00 +08:00
|
|
|
|
const geminiChannelShort = computed((): 'ai studio' | 'gcp' | 'google one' | 'client' | null => {
|
|
|
|
|
|
if (props.account.platform !== 'gemini') return null
|
|
|
|
|
|
|
|
|
|
|
|
// API Key accounts are AI Studio.
|
|
|
|
|
|
if (props.account.type === 'apikey') return 'ai studio'
|
|
|
|
|
|
|
|
|
|
|
|
if (geminiOAuthType.value === 'google_one') return 'google one'
|
|
|
|
|
|
if (isGeminiCodeAssist.value) return 'gcp'
|
|
|
|
|
|
if (geminiOAuthType.value === 'ai_studio') return 'client'
|
|
|
|
|
|
|
|
|
|
|
|
// Fallback (unknown legacy data): treat as AI Studio.
|
|
|
|
|
|
return 'ai studio'
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const geminiUserLevel = computed((): string | null => {
|
|
|
|
|
|
if (props.account.platform !== 'gemini') return null
|
|
|
|
|
|
|
|
|
|
|
|
const tier = (geminiTier.value || '').toString().trim()
|
|
|
|
|
|
const tierLower = tier.toLowerCase()
|
|
|
|
|
|
const tierUpper = tier.toUpperCase()
|
|
|
|
|
|
|
|
|
|
|
|
// Google One: free / pro / ultra
|
|
|
|
|
|
if (geminiOAuthType.value === 'google_one') {
|
|
|
|
|
|
if (tierLower === 'google_one_free') return 'free'
|
|
|
|
|
|
if (tierLower === 'google_ai_pro') return 'pro'
|
|
|
|
|
|
if (tierLower === 'google_ai_ultra') return 'ultra'
|
|
|
|
|
|
|
|
|
|
|
|
// Backward compatibility (legacy tier markers)
|
|
|
|
|
|
if (tierUpper === 'AI_PREMIUM' || tierUpper === 'GOOGLE_ONE_STANDARD') return 'pro'
|
|
|
|
|
|
if (tierUpper === 'GOOGLE_ONE_UNLIMITED') return 'ultra'
|
|
|
|
|
|
if (tierUpper === 'FREE' || tierUpper === 'GOOGLE_ONE_BASIC' || tierUpper === 'GOOGLE_ONE_UNKNOWN' || tierUpper === '') return 'free'
|
|
|
|
|
|
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// GCP Code Assist: standard / enterprise
|
|
|
|
|
|
if (isGeminiCodeAssist.value) {
|
|
|
|
|
|
if (tierLower === 'gcp_enterprise') return 'enterprise'
|
|
|
|
|
|
if (tierLower === 'gcp_standard') return 'standard'
|
|
|
|
|
|
|
|
|
|
|
|
// Backward compatibility
|
|
|
|
|
|
if (tierUpper.includes('ULTRA') || tierUpper.includes('ENTERPRISE')) return 'enterprise'
|
|
|
|
|
|
return 'standard'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// AI Studio (API Key) and Client OAuth: free / paid
|
|
|
|
|
|
if (props.account.type === 'apikey' || geminiOAuthType.value === 'ai_studio') {
|
|
|
|
|
|
if (tierLower === 'aistudio_paid') return 'paid'
|
|
|
|
|
|
if (tierLower === 'aistudio_free') return 'free'
|
|
|
|
|
|
|
|
|
|
|
|
// Backward compatibility
|
|
|
|
|
|
if (tierUpper.includes('PAID') || tierUpper.includes('PAYG') || tierUpper.includes('PAY')) return 'paid'
|
|
|
|
|
|
if (tierUpper.includes('FREE')) return 'free'
|
|
|
|
|
|
if (props.account.type === 'apikey') return 'free'
|
|
|
|
|
|
return null
|
feat(gemini): 添加 Google One 存储空间推断 Tier 功能
## 功能概述
通过 Google Drive API 获取存储空间配额来推断 Google One 订阅等级,并优化统一的配额显示系统。
## 后端改动
- 新增 Drive API 客户端 (drive_client.go)
- 支持代理和指数退避重试
- 处理 403/429 错误
- 添加 Tier 推断逻辑 (inferGoogleOneTier)
- 支持 6 种 tier 类型:AI_PREMIUM, GOOGLE_ONE_STANDARD, GOOGLE_ONE_BASIC, FREE, GOOGLE_ONE_UNKNOWN, GOOGLE_ONE_UNLIMITED
- 集成到 OAuth 流程
- ExchangeCode: 授权时自动获取 tier
- RefreshAccountToken: Token 刷新时更新 tier (24小时缓存)
- 新增管理 API 端点
- POST /api/v1/admin/accounts/:id/refresh-tier (单个账号刷新)
- POST /api/v1/admin/accounts/batch-refresh-tier (批量刷新)
## 前端改动
- 更新 AccountQuotaInfo.vue
- 添加 Google One tier 标签映射
- 添加 tier 颜色样式 (紫色/蓝色/绿色/灰色/琥珀色)
- 更新 AccountUsageCell.vue
- 添加 Google One tier 显示逻辑
- 根据 oauth_type 区分显示方式
- 添加国际化翻译 (en.ts, zh.ts)
- aiPremium, standard, basic, free, personal, unlimited
## Tier 推断规则
- >= 2TB: AI Premium
- >= 200GB: Google One Standard
- >= 100GB: Google One Basic
- >= 15GB: Free
- > 100TB: Unlimited (G Suite legacy)
- 其他/失败: Unknown (显示为 Personal)
## 优雅降级
- Drive API 失败时使用 GOOGLE_ONE_UNKNOWN
- 不阻断 OAuth 流程
- 24小时缓存避免频繁调用
## 测试
- ✅ 后端编译成功
- ✅ 前端构建成功
- ✅ 所有代码符合现有规范
2025-12-31 21:45:24 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-03 06:32:04 -08:00
|
|
|
|
return null
|
2026-01-01 08:29:57 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
2026-01-04 15:36:00 +08:00
|
|
|
|
// Gemini 认证类型(按要求:授权方式简称 + 用户等级)
|
|
|
|
|
|
const geminiAuthTypeLabel = computed(() => {
|
|
|
|
|
|
if (props.account.platform !== 'gemini') return null
|
|
|
|
|
|
if (!geminiChannelShort.value) return null
|
|
|
|
|
|
return geminiUserLevel.value ? `${geminiChannelShort.value} ${geminiUserLevel.value}` : geminiChannelShort.value
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-01-04 10:42:37 +08:00
|
|
|
|
// Gemini 账户类型徽章样式(统一样式)
|
2026-01-01 08:29:57 +08:00
|
|
|
|
const geminiTierClass = computed(() => {
|
2026-01-04 15:36:00 +08:00
|
|
|
|
// Use channel+level to choose a stable color without depending on raw tier_id variants.
|
|
|
|
|
|
const channel = geminiChannelShort.value
|
|
|
|
|
|
const level = geminiUserLevel.value
|
2026-01-03 06:32:04 -08:00
|
|
|
|
|
2026-01-04 15:36:00 +08:00
|
|
|
|
if (channel === 'client' || channel === 'ai studio') {
|
2026-01-04 10:42:37 +08:00
|
|
|
|
return 'bg-blue-100 text-blue-600 dark:bg-blue-900/40 dark:text-blue-300'
|
2026-01-03 06:32:04 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-04 15:36:00 +08:00
|
|
|
|
if (channel === 'google one') {
|
|
|
|
|
|
if (level === 'ultra') return 'bg-purple-100 text-purple-600 dark:bg-purple-900/40 dark:text-purple-300'
|
|
|
|
|
|
if (level === 'pro') return 'bg-blue-100 text-blue-600 dark:bg-blue-900/40 dark:text-blue-300'
|
|
|
|
|
|
return 'bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-300'
|
feat(gemini): 添加 Google One 存储空间推断 Tier 功能
## 功能概述
通过 Google Drive API 获取存储空间配额来推断 Google One 订阅等级,并优化统一的配额显示系统。
## 后端改动
- 新增 Drive API 客户端 (drive_client.go)
- 支持代理和指数退避重试
- 处理 403/429 错误
- 添加 Tier 推断逻辑 (inferGoogleOneTier)
- 支持 6 种 tier 类型:AI_PREMIUM, GOOGLE_ONE_STANDARD, GOOGLE_ONE_BASIC, FREE, GOOGLE_ONE_UNKNOWN, GOOGLE_ONE_UNLIMITED
- 集成到 OAuth 流程
- ExchangeCode: 授权时自动获取 tier
- RefreshAccountToken: Token 刷新时更新 tier (24小时缓存)
- 新增管理 API 端点
- POST /api/v1/admin/accounts/:id/refresh-tier (单个账号刷新)
- POST /api/v1/admin/accounts/batch-refresh-tier (批量刷新)
## 前端改动
- 更新 AccountQuotaInfo.vue
- 添加 Google One tier 标签映射
- 添加 tier 颜色样式 (紫色/蓝色/绿色/灰色/琥珀色)
- 更新 AccountUsageCell.vue
- 添加 Google One tier 显示逻辑
- 根据 oauth_type 区分显示方式
- 添加国际化翻译 (en.ts, zh.ts)
- aiPremium, standard, basic, free, personal, unlimited
## Tier 推断规则
- >= 2TB: AI Premium
- >= 200GB: Google One Standard
- >= 100GB: Google One Basic
- >= 15GB: Free
- > 100TB: Unlimited (G Suite legacy)
- 其他/失败: Unknown (显示为 Personal)
## 优雅降级
- Drive API 失败时使用 GOOGLE_ONE_UNKNOWN
- 不阻断 OAuth 流程
- 24小时缓存避免频繁调用
## 测试
- ✅ 后端编译成功
- ✅ 前端构建成功
- ✅ 所有代码符合现有规范
2025-12-31 21:45:24 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-04 15:36:00 +08:00
|
|
|
|
if (channel === 'gcp') {
|
|
|
|
|
|
if (level === 'enterprise') return 'bg-purple-100 text-purple-600 dark:bg-purple-900/40 dark:text-purple-300'
|
|
|
|
|
|
return 'bg-blue-100 text-blue-600 dark:bg-blue-900/40 dark:text-blue-300'
|
2026-01-01 08:29:57 +08:00
|
|
|
|
}
|
2026-01-04 15:36:00 +08:00
|
|
|
|
|
|
|
|
|
|
return ''
|
2026-01-01 08:29:57 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// Gemini 配额政策信息
|
|
|
|
|
|
const geminiQuotaPolicyChannel = computed(() => {
|
2026-01-04 15:36:00 +08:00
|
|
|
|
if (geminiOAuthType.value === 'google_one') {
|
|
|
|
|
|
return t('admin.accounts.gemini.quotaPolicy.rows.googleOne.channel')
|
|
|
|
|
|
}
|
2026-01-01 08:29:57 +08:00
|
|
|
|
if (isGeminiCodeAssist.value) {
|
2026-01-04 15:36:00 +08:00
|
|
|
|
return t('admin.accounts.gemini.quotaPolicy.rows.gcp.channel')
|
2026-01-01 08:29:57 +08:00
|
|
|
|
}
|
|
|
|
|
|
return t('admin.accounts.gemini.quotaPolicy.rows.aiStudio.channel')
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const geminiQuotaPolicyLimits = computed(() => {
|
2026-01-04 15:36:00 +08:00
|
|
|
|
const tierLower = (geminiTier.value || '').toString().trim().toLowerCase()
|
|
|
|
|
|
|
|
|
|
|
|
if (geminiOAuthType.value === 'google_one') {
|
|
|
|
|
|
if (tierLower === 'google_ai_ultra' || geminiUserLevel.value === 'ultra') {
|
|
|
|
|
|
return t('admin.accounts.gemini.quotaPolicy.rows.googleOne.limitsUltra')
|
|
|
|
|
|
}
|
|
|
|
|
|
if (tierLower === 'google_ai_pro' || geminiUserLevel.value === 'pro') {
|
|
|
|
|
|
return t('admin.accounts.gemini.quotaPolicy.rows.googleOne.limitsPro')
|
|
|
|
|
|
}
|
|
|
|
|
|
return t('admin.accounts.gemini.quotaPolicy.rows.googleOne.limitsFree')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-01 08:29:57 +08:00
|
|
|
|
if (isGeminiCodeAssist.value) {
|
2026-01-04 15:36:00 +08:00
|
|
|
|
if (tierLower === 'gcp_enterprise' || geminiUserLevel.value === 'enterprise') {
|
|
|
|
|
|
return t('admin.accounts.gemini.quotaPolicy.rows.gcp.limitsEnterprise')
|
2026-01-01 08:29:57 +08:00
|
|
|
|
}
|
2026-01-04 15:36:00 +08:00
|
|
|
|
return t('admin.accounts.gemini.quotaPolicy.rows.gcp.limitsStandard')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// AI Studio (API Key / custom OAuth)
|
|
|
|
|
|
if (tierLower === 'aistudio_paid' || geminiUserLevel.value === 'paid') {
|
|
|
|
|
|
return t('admin.accounts.gemini.quotaPolicy.rows.aiStudio.limitsPaid')
|
2026-01-01 08:29:57 +08:00
|
|
|
|
}
|
|
|
|
|
|
return t('admin.accounts.gemini.quotaPolicy.rows.aiStudio.limitsFree')
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const geminiQuotaPolicyDocsUrl = computed(() => {
|
2026-01-04 15:36:00 +08:00
|
|
|
|
if (geminiOAuthType.value === 'google_one' || isGeminiCodeAssist.value) {
|
|
|
|
|
|
return 'https://developers.google.com/gemini-code-assist/resources/quotas'
|
2026-01-01 08:29:57 +08:00
|
|
|
|
}
|
|
|
|
|
|
return 'https://ai.google.dev/pricing'
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-01-04 15:36:00 +08:00
|
|
|
|
const geminiUsesSharedDaily = computed(() => {
|
|
|
|
|
|
if (props.account.platform !== 'gemini') return false
|
|
|
|
|
|
// Per requirement: Google One & GCP are shared RPD pools (no per-model breakdown).
|
|
|
|
|
|
return (
|
|
|
|
|
|
!!usageInfo.value?.gemini_shared_daily ||
|
|
|
|
|
|
!!usageInfo.value?.gemini_shared_minute ||
|
|
|
|
|
|
geminiOAuthType.value === 'google_one' ||
|
|
|
|
|
|
isGeminiCodeAssist.value
|
|
|
|
|
|
)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const geminiUsageBars = computed(() => {
|
|
|
|
|
|
if (props.account.platform !== 'gemini') return []
|
|
|
|
|
|
if (!usageInfo.value) return []
|
|
|
|
|
|
|
|
|
|
|
|
const bars: Array<{
|
|
|
|
|
|
key: string
|
|
|
|
|
|
label: string
|
|
|
|
|
|
utilization: number
|
|
|
|
|
|
resetsAt: string | null
|
|
|
|
|
|
windowStats?: WindowStats | null
|
|
|
|
|
|
color: 'indigo' | 'emerald'
|
|
|
|
|
|
}> = []
|
|
|
|
|
|
|
|
|
|
|
|
if (geminiUsesSharedDaily.value) {
|
|
|
|
|
|
const sharedDaily = usageInfo.value.gemini_shared_daily
|
|
|
|
|
|
if (sharedDaily) {
|
|
|
|
|
|
bars.push({
|
|
|
|
|
|
key: 'shared_daily',
|
|
|
|
|
|
label: '1d',
|
|
|
|
|
|
utilization: sharedDaily.utilization,
|
|
|
|
|
|
resetsAt: sharedDaily.resets_at,
|
|
|
|
|
|
windowStats: sharedDaily.window_stats,
|
|
|
|
|
|
color: 'indigo'
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
return bars
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const pro = usageInfo.value.gemini_pro_daily
|
|
|
|
|
|
if (pro) {
|
|
|
|
|
|
bars.push({
|
|
|
|
|
|
key: 'pro_daily',
|
|
|
|
|
|
label: 'pro',
|
|
|
|
|
|
utilization: pro.utilization,
|
|
|
|
|
|
resetsAt: pro.resets_at,
|
|
|
|
|
|
windowStats: pro.window_stats,
|
|
|
|
|
|
color: 'indigo'
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const flash = usageInfo.value.gemini_flash_daily
|
|
|
|
|
|
if (flash) {
|
|
|
|
|
|
bars.push({
|
|
|
|
|
|
key: 'flash_daily',
|
|
|
|
|
|
label: 'flash',
|
|
|
|
|
|
utilization: flash.utilization,
|
|
|
|
|
|
resetsAt: flash.resets_at,
|
|
|
|
|
|
windowStats: flash.window_stats,
|
|
|
|
|
|
color: 'emerald'
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return bars
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2025-12-29 01:25:09 +08:00
|
|
|
|
// 账户类型显示标签
|
|
|
|
|
|
const antigravityTierLabel = computed(() => {
|
|
|
|
|
|
switch (antigravityTier.value) {
|
|
|
|
|
|
case 'free-tier':
|
|
|
|
|
|
return t('admin.accounts.tier.free')
|
|
|
|
|
|
case 'g1-pro-tier':
|
|
|
|
|
|
return t('admin.accounts.tier.pro')
|
|
|
|
|
|
case 'g1-ultra-tier':
|
|
|
|
|
|
return t('admin.accounts.tier.ultra')
|
|
|
|
|
|
default:
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 账户类型徽章样式
|
|
|
|
|
|
const antigravityTierClass = computed(() => {
|
|
|
|
|
|
switch (antigravityTier.value) {
|
|
|
|
|
|
case 'free-tier':
|
|
|
|
|
|
return 'bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-300'
|
|
|
|
|
|
case 'g1-pro-tier':
|
|
|
|
|
|
return 'bg-blue-100 text-blue-600 dark:bg-blue-900/40 dark:text-blue-300'
|
|
|
|
|
|
case 'g1-ultra-tier':
|
|
|
|
|
|
return 'bg-purple-100 text-purple-600 dark:bg-purple-900/40 dark:text-purple-300'
|
|
|
|
|
|
default:
|
|
|
|
|
|
return ''
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2025-12-31 00:34:24 +08:00
|
|
|
|
// 检测账户是否有不合格状态(ineligibleTiers)
|
|
|
|
|
|
const hasIneligibleTiers = computed(() => {
|
|
|
|
|
|
const extra = props.account.extra as Record<string, unknown> | undefined
|
|
|
|
|
|
if (!extra) return false
|
|
|
|
|
|
|
|
|
|
|
|
const loadCodeAssist = extra.load_code_assist as Record<string, unknown> | undefined
|
|
|
|
|
|
if (!loadCodeAssist) return false
|
|
|
|
|
|
|
|
|
|
|
|
const ineligibleTiers = loadCodeAssist.ineligibleTiers as unknown[] | undefined
|
|
|
|
|
|
return Array.isArray(ineligibleTiers) && ineligibleTiers.length > 0
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2025-12-18 13:50:39 +08:00
|
|
|
|
const loadUsage = async () => {
|
2026-01-01 04:22:50 +08:00
|
|
|
|
if (!shouldFetchUsage.value) return
|
2025-12-18 13:50:39 +08:00
|
|
|
|
|
|
|
|
|
|
loading.value = true
|
|
|
|
|
|
error.value = null
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
2026-03-05 19:12:49 +08:00
|
|
|
|
const fetchFn = () => adminAPI.accounts.getUsage(props.account.id)
|
|
|
|
|
|
let result: AccountUsageInfo
|
|
|
|
|
|
// Only throttle Anthropic OAuth/setup-token accounts to avoid upstream 429
|
|
|
|
|
|
if (
|
|
|
|
|
|
props.account.platform === 'anthropic' &&
|
|
|
|
|
|
(props.account.type === 'oauth' || props.account.type === 'setup-token')
|
|
|
|
|
|
) {
|
|
|
|
|
|
result = await enqueueUsageRequest(
|
|
|
|
|
|
props.account.platform,
|
|
|
|
|
|
'claude_code',
|
|
|
|
|
|
props.account.proxy_id,
|
|
|
|
|
|
fetchFn
|
|
|
|
|
|
)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
result = await fetchFn()
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!unmounted.value) usageInfo.value = result
|
2025-12-18 13:50:39 +08:00
|
|
|
|
} catch (e: any) {
|
2026-03-05 19:12:49 +08:00
|
|
|
|
if (!unmounted.value) {
|
|
|
|
|
|
error.value = t('common.error')
|
|
|
|
|
|
console.error('Failed to load usage:', e)
|
|
|
|
|
|
}
|
2025-12-18 13:50:39 +08:00
|
|
|
|
} finally {
|
2026-03-05 19:12:49 +08:00
|
|
|
|
if (!unmounted.value) loading.value = false
|
2025-12-18 13:50:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
|
loadUsage()
|
|
|
|
|
|
})
|
|
|
|
|
|
</script>
|