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"
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Claude 4.5 -->
|
|
|
|
|
|
<UsageProgressBar
|
2026-01-03 06:32:04 -08:00
|
|
|
|
v-if="antigravityClaude45UsageFromAPI !== null"
|
2025-12-28 22:29:01 +08:00
|
|
|
|
:label="t('admin.accounts.usageWindow.claude45')"
|
2026-01-03 06:32:04 -08:00
|
|
|
|
:utilization="antigravityClaude45UsageFromAPI.utilization"
|
|
|
|
|
|
:resets-at="antigravityClaude45UsageFromAPI.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 10:22:02 +08:00
|
|
|
|
<!-- Code Assist: show model usage bars -->
|
|
|
|
|
|
<div v-else-if="isGeminiCodeAssist && geminiUsageAvailable" class="space-y-1">
|
2026-01-01 04:22:50 +08:00
|
|
|
|
<UsageProgressBar
|
|
|
|
|
|
v-if="usageInfo?.gemini_pro_daily"
|
2026-01-04 10:38:57 +08:00
|
|
|
|
label="Pro"
|
2026-01-01 04:22:50 +08:00
|
|
|
|
:utilization="usageInfo.gemini_pro_daily.utilization"
|
|
|
|
|
|
:resets-at="usageInfo.gemini_pro_daily.resets_at"
|
|
|
|
|
|
:window-stats="usageInfo.gemini_pro_daily.window_stats"
|
|
|
|
|
|
color="indigo"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<UsageProgressBar
|
|
|
|
|
|
v-if="usageInfo?.gemini_flash_daily"
|
2026-01-04 10:38:57 +08:00
|
|
|
|
label="Flash"
|
2026-01-01 04:22:50 +08:00
|
|
|
|
:utilization="usageInfo.gemini_flash_daily.utilization"
|
|
|
|
|
|
:resets-at="usageInfo.gemini_flash_daily.resets_at"
|
|
|
|
|
|
:window-stats="usageInfo.gemini_flash_daily.window_stats"
|
|
|
|
|
|
color="emerald"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<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:22:02 +08:00
|
|
|
|
<!-- AI Studio & Google One: show unlimited flow -->
|
|
|
|
|
|
<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">
|
2025-12-22 22:58:31 +08:00
|
|
|
|
import { ref, computed, onMounted } 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-01 08:29:57 +08:00
|
|
|
|
import type { Account, AccountUsageInfo, GeminiCredentials } from '@/types'
|
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()
|
|
|
|
|
|
|
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
|
2025-12-25 08:40:12 -08:00
|
|
|
|
const showUsageWindows = computed(
|
|
|
|
|
|
() => 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') {
|
|
|
|
|
|
return props.account.type === 'oauth'
|
|
|
|
|
|
}
|
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 (
|
|
|
|
|
|
!!usageInfo.value?.gemini_pro_daily ||
|
|
|
|
|
|
!!usageInfo.value?.gemini_flash_daily
|
|
|
|
|
|
)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2025-12-23 16:26:07 +08:00
|
|
|
|
// OpenAI Codex usage computed properties
|
|
|
|
|
|
const hasCodexUsage = computed(() => {
|
|
|
|
|
|
const extra = props.account.extra
|
2025-12-25 08:40:12 -08:00
|
|
|
|
return (
|
|
|
|
|
|
extra &&
|
2025-12-25 17:00:02 +08:00
|
|
|
|
// Check for new canonical fields first
|
2025-12-25 08:40:12 -08:00
|
|
|
|
(extra.codex_5h_used_percent !== undefined ||
|
|
|
|
|
|
extra.codex_7d_used_percent !== undefined ||
|
|
|
|
|
|
// Fallback to legacy fields
|
|
|
|
|
|
extra.codex_primary_used_percent !== undefined ||
|
|
|
|
|
|
extra.codex_secondary_used_percent !== undefined)
|
2025-12-23 16:26:07 +08:00
|
|
|
|
)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2025-12-25 17:00:02 +08:00
|
|
|
|
// 5h window usage (prefer canonical field)
|
|
|
|
|
|
const codex5hUsedPercent = computed(() => {
|
2025-12-23 16:26:07 +08:00
|
|
|
|
const extra = props.account.extra
|
2025-12-25 17:00:02 +08:00
|
|
|
|
if (!extra) return null
|
|
|
|
|
|
|
|
|
|
|
|
// Prefer canonical field
|
|
|
|
|
|
if (extra.codex_5h_used_percent !== undefined) {
|
|
|
|
|
|
return extra.codex_5h_used_percent
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Fallback: detect from legacy fields using window_minutes
|
2025-12-25 08:40:12 -08:00
|
|
|
|
if (
|
|
|
|
|
|
extra.codex_primary_window_minutes !== undefined &&
|
|
|
|
|
|
extra.codex_primary_window_minutes <= 360
|
|
|
|
|
|
) {
|
2025-12-25 17:00:02 +08:00
|
|
|
|
return extra.codex_primary_used_percent ?? null
|
|
|
|
|
|
}
|
2025-12-25 08:40:12 -08:00
|
|
|
|
if (
|
|
|
|
|
|
extra.codex_secondary_window_minutes !== undefined &&
|
|
|
|
|
|
extra.codex_secondary_window_minutes <= 360
|
|
|
|
|
|
) {
|
2025-12-25 17:00:02 +08:00
|
|
|
|
return extra.codex_secondary_used_percent ?? null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Legacy assumption: secondary = 5h (may be incorrect)
|
|
|
|
|
|
return extra.codex_secondary_used_percent ?? null
|
2025-12-23 16:26:07 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
2025-12-25 17:00:02 +08:00
|
|
|
|
const codex5hResetAt = computed(() => {
|
2025-12-23 16:26:07 +08:00
|
|
|
|
const extra = props.account.extra
|
2025-12-25 17:00:02 +08:00
|
|
|
|
if (!extra) return null
|
|
|
|
|
|
|
|
|
|
|
|
// Prefer canonical field
|
|
|
|
|
|
if (extra.codex_5h_reset_after_seconds !== undefined) {
|
|
|
|
|
|
const resetTime = new Date(Date.now() + extra.codex_5h_reset_after_seconds * 1000)
|
|
|
|
|
|
return resetTime.toISOString()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Fallback: detect from legacy fields using window_minutes
|
2025-12-25 08:40:12 -08:00
|
|
|
|
if (
|
|
|
|
|
|
extra.codex_primary_window_minutes !== undefined &&
|
|
|
|
|
|
extra.codex_primary_window_minutes <= 360
|
|
|
|
|
|
) {
|
2025-12-25 17:00:02 +08:00
|
|
|
|
if (extra.codex_primary_reset_after_seconds !== undefined) {
|
|
|
|
|
|
const resetTime = new Date(Date.now() + extra.codex_primary_reset_after_seconds * 1000)
|
|
|
|
|
|
return resetTime.toISOString()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-12-25 08:40:12 -08:00
|
|
|
|
if (
|
|
|
|
|
|
extra.codex_secondary_window_minutes !== undefined &&
|
|
|
|
|
|
extra.codex_secondary_window_minutes <= 360
|
|
|
|
|
|
) {
|
2025-12-25 17:00:02 +08:00
|
|
|
|
if (extra.codex_secondary_reset_after_seconds !== undefined) {
|
|
|
|
|
|
const resetTime = new Date(Date.now() + extra.codex_secondary_reset_after_seconds * 1000)
|
|
|
|
|
|
return resetTime.toISOString()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Legacy assumption: secondary = 5h
|
|
|
|
|
|
if (extra.codex_secondary_reset_after_seconds !== undefined) {
|
|
|
|
|
|
const resetTime = new Date(Date.now() + extra.codex_secondary_reset_after_seconds * 1000)
|
|
|
|
|
|
return resetTime.toISOString()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return null
|
2025-12-23 16:26:07 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
2025-12-25 17:00:02 +08:00
|
|
|
|
// 7d window usage (prefer canonical field)
|
|
|
|
|
|
const codex7dUsedPercent = computed(() => {
|
2025-12-23 16:26:07 +08:00
|
|
|
|
const extra = props.account.extra
|
2025-12-25 17:00:02 +08:00
|
|
|
|
if (!extra) return null
|
|
|
|
|
|
|
|
|
|
|
|
// Prefer canonical field
|
|
|
|
|
|
if (extra.codex_7d_used_percent !== undefined) {
|
|
|
|
|
|
return extra.codex_7d_used_percent
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Fallback: detect from legacy fields using window_minutes
|
2025-12-25 08:40:12 -08:00
|
|
|
|
if (
|
|
|
|
|
|
extra.codex_primary_window_minutes !== undefined &&
|
|
|
|
|
|
extra.codex_primary_window_minutes >= 10000
|
|
|
|
|
|
) {
|
2025-12-25 17:00:02 +08:00
|
|
|
|
return extra.codex_primary_used_percent ?? null
|
|
|
|
|
|
}
|
2025-12-25 08:40:12 -08:00
|
|
|
|
if (
|
|
|
|
|
|
extra.codex_secondary_window_minutes !== undefined &&
|
|
|
|
|
|
extra.codex_secondary_window_minutes >= 10000
|
|
|
|
|
|
) {
|
2025-12-25 17:00:02 +08:00
|
|
|
|
return extra.codex_secondary_used_percent ?? null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Legacy assumption: primary = 7d (may be incorrect)
|
|
|
|
|
|
return extra.codex_primary_used_percent ?? null
|
2025-12-23 16:26:07 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
2025-12-25 17:00:02 +08:00
|
|
|
|
const codex7dResetAt = computed(() => {
|
2025-12-23 16:26:07 +08:00
|
|
|
|
const extra = props.account.extra
|
2025-12-25 17:00:02 +08:00
|
|
|
|
if (!extra) return null
|
|
|
|
|
|
|
|
|
|
|
|
// Prefer canonical field
|
|
|
|
|
|
if (extra.codex_7d_reset_after_seconds !== undefined) {
|
|
|
|
|
|
const resetTime = new Date(Date.now() + extra.codex_7d_reset_after_seconds * 1000)
|
|
|
|
|
|
return resetTime.toISOString()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Fallback: detect from legacy fields using window_minutes
|
2025-12-25 08:40:12 -08:00
|
|
|
|
if (
|
|
|
|
|
|
extra.codex_primary_window_minutes !== undefined &&
|
|
|
|
|
|
extra.codex_primary_window_minutes >= 10000
|
|
|
|
|
|
) {
|
2025-12-25 17:00:02 +08:00
|
|
|
|
if (extra.codex_primary_reset_after_seconds !== undefined) {
|
|
|
|
|
|
const resetTime = new Date(Date.now() + extra.codex_primary_reset_after_seconds * 1000)
|
|
|
|
|
|
return resetTime.toISOString()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-12-25 08:40:12 -08:00
|
|
|
|
if (
|
|
|
|
|
|
extra.codex_secondary_window_minutes !== undefined &&
|
|
|
|
|
|
extra.codex_secondary_window_minutes >= 10000
|
|
|
|
|
|
) {
|
2025-12-25 17:00:02 +08:00
|
|
|
|
if (extra.codex_secondary_reset_after_seconds !== undefined) {
|
|
|
|
|
|
const resetTime = new Date(Date.now() + extra.codex_secondary_reset_after_seconds * 1000)
|
|
|
|
|
|
return resetTime.toISOString()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Legacy assumption: primary = 7d
|
|
|
|
|
|
if (extra.codex_primary_reset_after_seconds !== undefined) {
|
|
|
|
|
|
const resetTime = new Date(Date.now() + extra.codex_primary_reset_after_seconds * 1000)
|
|
|
|
|
|
return resetTime.toISOString()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return null
|
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-01-03 06:32:04 -08:00
|
|
|
|
// Gemini 3 Image from API
|
|
|
|
|
|
const antigravity3ImageUsageFromAPI = computed(() => getAntigravityUsageFromAPI(['gemini-3-pro-image']))
|
2025-12-28 22:29:01 +08:00
|
|
|
|
|
2026-01-03 06:32:04 -08:00
|
|
|
|
// Claude 4.5 from API
|
|
|
|
|
|
const antigravityClaude45UsageFromAPI = computed(() =>
|
|
|
|
|
|
getAntigravityUsageFromAPI(['claude-sonnet-4-5', 'claude-opus-4-5-thinking'])
|
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
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 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-03 06:32:04 -08:00
|
|
|
|
// Gemini 认证类型 + Tier 组合标签(简洁版)
|
|
|
|
|
|
const geminiAuthTypeLabel = computed(() => {
|
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
|
|
|
|
const creds = props.account.credentials as GeminiCredentials | undefined
|
2026-01-03 06:32:04 -08:00
|
|
|
|
const oauthType = creds?.oauth_type
|
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
|
|
|
|
// For API Key accounts, don't show auth type label
|
|
|
|
|
|
if (props.account.type !== 'oauth') return null
|
|
|
|
|
|
|
|
|
|
|
|
if (oauthType === 'google_one') {
|
2026-01-04 10:38:57 +08:00
|
|
|
|
// Google One: show "Google One" + tier
|
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
|
|
|
|
const tierMap: Record<string, string> = {
|
2026-01-03 06:32:04 -08:00
|
|
|
|
AI_PREMIUM: 'AI Premium',
|
|
|
|
|
|
GOOGLE_ONE_STANDARD: 'Standard',
|
|
|
|
|
|
GOOGLE_ONE_BASIC: 'Basic',
|
|
|
|
|
|
FREE: 'Free',
|
|
|
|
|
|
GOOGLE_ONE_UNKNOWN: 'Personal',
|
|
|
|
|
|
GOOGLE_ONE_UNLIMITED: 'Unlimited'
|
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
|
|
|
|
const tierLabel = geminiTier.value ? tierMap[geminiTier.value] || 'Personal' : 'Personal'
|
2026-01-04 10:38:57 +08:00
|
|
|
|
return `Google One ${tierLabel}`
|
2026-01-03 06:32:04 -08:00
|
|
|
|
} else if (oauthType === 'code_assist' || (!oauthType && isGeminiCodeAssist.value)) {
|
2026-01-04 10:38:57 +08:00
|
|
|
|
// Code Assist: show "GCP" + tier
|
2026-01-03 06:32:04 -08:00
|
|
|
|
const tierMap: Record<string, string> = {
|
|
|
|
|
|
LEGACY: 'Free',
|
|
|
|
|
|
PRO: 'Pro',
|
|
|
|
|
|
ULTRA: 'Ultra'
|
|
|
|
|
|
}
|
|
|
|
|
|
const tierLabel = geminiTier.value ? tierMap[geminiTier.value] || 'Free' : 'Free'
|
2026-01-04 10:38:57 +08:00
|
|
|
|
return `GCP ${tierLabel}`
|
2026-01-03 06:32:04 -08:00
|
|
|
|
} else if (oauthType === 'ai_studio') {
|
2026-01-04 10:39:28 +08:00
|
|
|
|
// 自定义 OAuth Client: show "Client" (no tier)
|
|
|
|
|
|
return 'Client'
|
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
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// Gemini 账户类型徽章样式
|
|
|
|
|
|
const geminiTierClass = computed(() => {
|
2026-01-03 06:32:04 -08:00
|
|
|
|
const creds = props.account.credentials as GeminiCredentials | undefined
|
|
|
|
|
|
const oauthType = creds?.oauth_type
|
|
|
|
|
|
|
|
|
|
|
|
// AI Studio: use neutral gray color (no tier)
|
|
|
|
|
|
if (oauthType === 'ai_studio') {
|
|
|
|
|
|
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
|
|
|
|
if (!geminiTier.value) return ''
|
|
|
|
|
|
|
|
|
|
|
|
const isGoogleOne = creds?.oauth_type === 'google_one'
|
|
|
|
|
|
|
|
|
|
|
|
if (isGoogleOne) {
|
|
|
|
|
|
// Google One tier 颜色
|
|
|
|
|
|
const colorMap: Record<string, string> = {
|
|
|
|
|
|
AI_PREMIUM: 'bg-purple-100 text-purple-600 dark:bg-purple-900/40 dark:text-purple-300',
|
|
|
|
|
|
GOOGLE_ONE_STANDARD: 'bg-blue-100 text-blue-600 dark:bg-blue-900/40 dark:text-blue-300',
|
|
|
|
|
|
GOOGLE_ONE_BASIC: 'bg-green-100 text-green-600 dark:bg-green-900/40 dark:text-green-300',
|
|
|
|
|
|
FREE: 'bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-300',
|
|
|
|
|
|
GOOGLE_ONE_UNKNOWN: 'bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-300',
|
|
|
|
|
|
GOOGLE_ONE_UNLIMITED: 'bg-amber-100 text-amber-600 dark:bg-amber-900/40 dark:text-amber-300'
|
|
|
|
|
|
}
|
|
|
|
|
|
return colorMap[geminiTier.value] || 'bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-300'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Code Assist tier 颜色
|
2026-01-01 08:29:57 +08:00
|
|
|
|
switch (geminiTier.value) {
|
|
|
|
|
|
case 'LEGACY':
|
|
|
|
|
|
return 'bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-300'
|
|
|
|
|
|
case 'PRO':
|
|
|
|
|
|
return 'bg-blue-100 text-blue-600 dark:bg-blue-900/40 dark:text-blue-300'
|
|
|
|
|
|
case 'ULTRA':
|
|
|
|
|
|
return 'bg-purple-100 text-purple-600 dark:bg-purple-900/40 dark:text-purple-300'
|
|
|
|
|
|
default:
|
|
|
|
|
|
return ''
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// Gemini 配额政策信息
|
|
|
|
|
|
const geminiQuotaPolicyChannel = computed(() => {
|
|
|
|
|
|
if (isGeminiCodeAssist.value) {
|
|
|
|
|
|
return t('admin.accounts.gemini.quotaPolicy.rows.cli.channel')
|
|
|
|
|
|
}
|
|
|
|
|
|
return t('admin.accounts.gemini.quotaPolicy.rows.aiStudio.channel')
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const geminiQuotaPolicyLimits = computed(() => {
|
|
|
|
|
|
if (isGeminiCodeAssist.value) {
|
|
|
|
|
|
if (geminiTier.value === 'PRO' || geminiTier.value === 'ULTRA') {
|
|
|
|
|
|
return t('admin.accounts.gemini.quotaPolicy.rows.cli.limitsPremium')
|
|
|
|
|
|
}
|
|
|
|
|
|
return t('admin.accounts.gemini.quotaPolicy.rows.cli.limitsFree')
|
|
|
|
|
|
}
|
|
|
|
|
|
// AI Studio - 默认显示免费层限制
|
|
|
|
|
|
return t('admin.accounts.gemini.quotaPolicy.rows.aiStudio.limitsFree')
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const geminiQuotaPolicyDocsUrl = computed(() => {
|
|
|
|
|
|
if (isGeminiCodeAssist.value) {
|
|
|
|
|
|
return 'https://cloud.google.com/products/gemini/code-assist#pricing'
|
|
|
|
|
|
}
|
|
|
|
|
|
return 'https://ai.google.dev/pricing'
|
|
|
|
|
|
})
|
|
|
|
|
|
|
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 {
|
|
|
|
|
|
usageInfo.value = await adminAPI.accounts.getUsage(props.account.id)
|
|
|
|
|
|
} catch (e: any) {
|
2025-12-27 10:50:25 +08:00
|
|
|
|
error.value = t('common.error')
|
2025-12-18 13:50:39 +08:00
|
|
|
|
console.error('Failed to load usage:', e)
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
loading.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
|
loadUsage()
|
|
|
|
|
|
})
|
|
|
|
|
|
</script>
|