feat(gemini): 完善 Gemini OAuth 配额系统和用量显示
主要改动: - 后端:重构 Gemini 配额服务,支持多层级配额策略(GCP Standard/Free, Google One, AI Studio, Code Assist) - 后端:优化 OAuth 服务,增强 tier_id 识别和存储逻辑 - 后端:改进用量统计服务,支持不同平台的配额查询 - 后端:优化限流服务,增加临时解除调度状态管理 - 前端:统一四种授权方式的用量显示格式和徽标样式 - 前端:增强账户配额信息展示,支持多种配额类型 - 前端:改进创建和重新授权模态框的用户体验 - 国际化:完善中英文配额相关文案 - 移除 CHANGELOG.md 文件 测试:所有单元测试通过
This commit is contained in:
@@ -241,23 +241,16 @@
|
||||
<div v-else-if="error" class="text-xs text-red-500">
|
||||
{{ error }}
|
||||
</div>
|
||||
<!-- GCP & Google One: show model usage bars when available -->
|
||||
<!-- Gemini: show daily usage bars when available -->
|
||||
<div v-else-if="geminiUsageAvailable" class="space-y-1">
|
||||
<UsageProgressBar
|
||||
v-if="usageInfo?.gemini_pro_daily"
|
||||
label="Pro"
|
||||
: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"
|
||||
label="Flash"
|
||||
:utilization="usageInfo.gemini_flash_daily.utilization"
|
||||
:resets-at="usageInfo.gemini_flash_daily.resets_at"
|
||||
:window-stats="usageInfo.gemini_flash_daily.window_stats"
|
||||
color="emerald"
|
||||
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"
|
||||
/>
|
||||
<p class="mt-1 text-[9px] leading-tight text-gray-400 dark:text-gray-500 italic">
|
||||
* {{ t('admin.accounts.gemini.quotaPolicy.simulatedNote') || 'Simulated quota' }}
|
||||
@@ -288,7 +281,7 @@
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { adminAPI } from '@/api/admin'
|
||||
import type { Account, AccountUsageInfo, GeminiCredentials } from '@/types'
|
||||
import type { Account, AccountUsageInfo, GeminiCredentials, WindowStats } from '@/types'
|
||||
import UsageProgressBar from './UsageProgressBar.vue'
|
||||
import AccountQuotaInfo from './AccountQuotaInfo.vue'
|
||||
|
||||
@@ -303,16 +296,18 @@ const error = ref<string | null>(null)
|
||||
const usageInfo = ref<AccountUsageInfo | null>(null)
|
||||
|
||||
// Show usage windows for OAuth and Setup Token accounts
|
||||
const showUsageWindows = computed(
|
||||
() => props.account.type === 'oauth' || props.account.type === 'setup-token'
|
||||
)
|
||||
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'
|
||||
})
|
||||
|
||||
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'
|
||||
return true
|
||||
}
|
||||
if (props.account.platform === 'antigravity') {
|
||||
return props.account.type === 'oauth'
|
||||
@@ -322,8 +317,12 @@ const shouldFetchUsage = computed(() => {
|
||||
|
||||
const geminiUsageAvailable = computed(() => {
|
||||
return (
|
||||
!!usageInfo.value?.gemini_shared_daily ||
|
||||
!!usageInfo.value?.gemini_pro_daily ||
|
||||
!!usageInfo.value?.gemini_flash_daily
|
||||
!!usageInfo.value?.gemini_flash_daily ||
|
||||
!!usageInfo.value?.gemini_shared_minute ||
|
||||
!!usageInfo.value?.gemini_pro_minute ||
|
||||
!!usageInfo.value?.gemini_flash_minute
|
||||
)
|
||||
})
|
||||
|
||||
@@ -569,6 +568,12 @@ const geminiTier = computed(() => {
|
||||
return creds?.tier_id || null
|
||||
})
|
||||
|
||||
const geminiOAuthType = computed(() => {
|
||||
if (props.account.platform !== 'gemini') return null
|
||||
const creds = props.account.credentials as GeminiCredentials | undefined
|
||||
return (creds?.oauth_type || '').trim() || null
|
||||
})
|
||||
|
||||
// Gemini 是否为 Code Assist OAuth
|
||||
const isGeminiCodeAssist = computed(() => {
|
||||
if (props.account.platform !== 'gemini') return false
|
||||
@@ -576,109 +581,208 @@ const isGeminiCodeAssist = computed(() => {
|
||||
return creds?.oauth_type === 'code_assist' || (!creds?.oauth_type && !!creds?.project_id)
|
||||
})
|
||||
|
||||
// Gemini 认证类型 + Tier 组合标签(简洁版)
|
||||
const geminiAuthTypeLabel = computed(() => {
|
||||
const creds = props.account.credentials as GeminiCredentials | undefined
|
||||
const oauthType = creds?.oauth_type
|
||||
const geminiChannelShort = computed((): 'ai studio' | 'gcp' | 'google one' | 'client' | null => {
|
||||
if (props.account.platform !== 'gemini') return null
|
||||
|
||||
// For API Key accounts, don't show auth type label
|
||||
if (props.account.type !== 'oauth') return null
|
||||
// API Key accounts are AI Studio.
|
||||
if (props.account.type === 'apikey') return 'ai studio'
|
||||
|
||||
if (oauthType === 'google_one') {
|
||||
// Google One: show "Google One" + tier
|
||||
const tierMap: Record<string, string> = {
|
||||
AI_PREMIUM: 'AI Premium',
|
||||
GOOGLE_ONE_STANDARD: 'Standard',
|
||||
GOOGLE_ONE_BASIC: 'Basic',
|
||||
FREE: 'Free',
|
||||
GOOGLE_ONE_UNKNOWN: 'Personal',
|
||||
GOOGLE_ONE_UNLIMITED: 'Unlimited'
|
||||
}
|
||||
const tierLabel = geminiTier.value ? tierMap[geminiTier.value] || 'Personal' : 'Personal'
|
||||
return `Google One ${tierLabel}`
|
||||
} else if (oauthType === 'code_assist' || (!oauthType && isGeminiCodeAssist.value)) {
|
||||
// Code Assist: show "GCP" + tier
|
||||
const tierMap: Record<string, string> = {
|
||||
LEGACY: 'Free',
|
||||
PRO: 'Pro',
|
||||
ULTRA: 'Ultra'
|
||||
}
|
||||
const tierLabel = geminiTier.value ? tierMap[geminiTier.value] || 'Free' : 'Free'
|
||||
return `GCP ${tierLabel}`
|
||||
} else if (oauthType === 'ai_studio') {
|
||||
// 自定义 OAuth Client: show "Client" (no tier)
|
||||
return 'Client'
|
||||
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
|
||||
}
|
||||
|
||||
return null
|
||||
})
|
||||
|
||||
// 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
|
||||
})
|
||||
|
||||
// Gemini 账户类型徽章样式(统一样式)
|
||||
const geminiTierClass = computed(() => {
|
||||
const creds = props.account.credentials as GeminiCredentials | undefined
|
||||
const oauthType = creds?.oauth_type
|
||||
// Use channel+level to choose a stable color without depending on raw tier_id variants.
|
||||
const channel = geminiChannelShort.value
|
||||
const level = geminiUserLevel.value
|
||||
|
||||
// Client (自定义 OAuth): 使用蓝色(与 AI Studio 一致)
|
||||
if (oauthType === 'ai_studio') {
|
||||
if (channel === 'client' || channel === 'ai studio') {
|
||||
return 'bg-blue-100 text-blue-600 dark:bg-blue-900/40 dark:text-blue-300'
|
||||
}
|
||||
|
||||
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'
|
||||
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'
|
||||
}
|
||||
|
||||
// Code Assist tier 颜色
|
||||
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 ''
|
||||
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'
|
||||
}
|
||||
|
||||
return ''
|
||||
})
|
||||
|
||||
// Gemini 配额政策信息
|
||||
const geminiQuotaPolicyChannel = computed(() => {
|
||||
if (geminiOAuthType.value === 'google_one') {
|
||||
return t('admin.accounts.gemini.quotaPolicy.rows.googleOne.channel')
|
||||
}
|
||||
if (isGeminiCodeAssist.value) {
|
||||
return t('admin.accounts.gemini.quotaPolicy.rows.cli.channel')
|
||||
return t('admin.accounts.gemini.quotaPolicy.rows.gcp.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')
|
||||
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')
|
||||
}
|
||||
return t('admin.accounts.gemini.quotaPolicy.rows.cli.limitsFree')
|
||||
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')
|
||||
}
|
||||
|
||||
if (isGeminiCodeAssist.value) {
|
||||
if (tierLower === 'gcp_enterprise' || geminiUserLevel.value === 'enterprise') {
|
||||
return t('admin.accounts.gemini.quotaPolicy.rows.gcp.limitsEnterprise')
|
||||
}
|
||||
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')
|
||||
}
|
||||
// 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'
|
||||
if (geminiOAuthType.value === 'google_one' || isGeminiCodeAssist.value) {
|
||||
return 'https://developers.google.com/gemini-code-assist/resources/quotas'
|
||||
}
|
||||
return 'https://ai.google.dev/pricing'
|
||||
})
|
||||
|
||||
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
|
||||
})
|
||||
|
||||
// 账户类型显示标签
|
||||
const antigravityTierLabel = computed(() => {
|
||||
switch (antigravityTier.value) {
|
||||
|
||||
Reference in New Issue
Block a user