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小时缓存避免频繁调用 ## 测试 - ✅ 后端编译成功 - ✅ 前端构建成功 - ✅ 所有代码符合现有规范
This commit is contained in:
@@ -48,6 +48,12 @@ const isCodeAssist = computed(() => {
|
||||
return creds?.oauth_type === 'code_assist' || (!creds?.oauth_type && !!creds?.project_id)
|
||||
})
|
||||
|
||||
// 是否为 Google One OAuth
|
||||
const isGoogleOne = computed(() => {
|
||||
const creds = props.account.credentials as GeminiCredentials | undefined
|
||||
return creds?.oauth_type === 'google_one'
|
||||
})
|
||||
|
||||
// 是否应该显示配额信息
|
||||
const shouldShowQuota = computed(() => {
|
||||
return props.account.platform === 'gemini'
|
||||
@@ -55,33 +61,73 @@ const shouldShowQuota = computed(() => {
|
||||
|
||||
// Tier 标签文本
|
||||
const tierLabel = computed(() => {
|
||||
const creds = props.account.credentials as GeminiCredentials | undefined
|
||||
|
||||
if (isCodeAssist.value) {
|
||||
const creds = props.account.credentials as GeminiCredentials | undefined
|
||||
// GCP Code Assist: 显示 GCP tier
|
||||
const tierMap: Record<string, string> = {
|
||||
LEGACY: 'Free',
|
||||
PRO: 'Pro',
|
||||
ULTRA: 'Ultra'
|
||||
ULTRA: 'Ultra',
|
||||
'standard-tier': 'Standard',
|
||||
'pro-tier': 'Pro',
|
||||
'ultra-tier': 'Ultra'
|
||||
}
|
||||
return tierMap[creds?.tier_id || ''] || 'Unknown'
|
||||
return tierMap[creds?.tier_id || ''] || (creds?.tier_id ? 'GCP' : 'Unknown')
|
||||
}
|
||||
|
||||
if (isGoogleOne.value) {
|
||||
// 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'
|
||||
}
|
||||
return tierMap[creds?.tier_id || ''] || 'Personal'
|
||||
}
|
||||
|
||||
// AI Studio 或其他
|
||||
return 'Gemini'
|
||||
})
|
||||
|
||||
// Tier Badge 样式
|
||||
const tierBadgeClass = computed(() => {
|
||||
if (!isCodeAssist.value) {
|
||||
return 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400'
|
||||
}
|
||||
const creds = props.account.credentials as GeminiCredentials | undefined
|
||||
const tierColorMap: Record<string, string> = {
|
||||
LEGACY: 'bg-gray-100 text-gray-700 dark:bg-gray-900/30 dark:text-gray-400',
|
||||
PRO: 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400',
|
||||
ULTRA: 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400'
|
||||
|
||||
if (isCodeAssist.value) {
|
||||
// GCP Code Assist 样式
|
||||
const tierColorMap: Record<string, string> = {
|
||||
LEGACY: 'bg-gray-100 text-gray-700 dark:bg-gray-900/30 dark:text-gray-400',
|
||||
PRO: 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400',
|
||||
ULTRA: 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400',
|
||||
'standard-tier': 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400',
|
||||
'pro-tier': 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400',
|
||||
'ultra-tier': 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400'
|
||||
}
|
||||
return (
|
||||
tierColorMap[creds?.tier_id || ''] ||
|
||||
'bg-gray-100 text-gray-700 dark:bg-gray-900/30 dark:text-gray-400'
|
||||
)
|
||||
}
|
||||
return (
|
||||
tierColorMap[creds?.tier_id || ''] ||
|
||||
'bg-gray-100 text-gray-700 dark:bg-gray-900/30 dark:text-gray-400'
|
||||
)
|
||||
|
||||
if (isGoogleOne.value) {
|
||||
// Google One tier 样式
|
||||
const tierColorMap: Record<string, string> = {
|
||||
AI_PREMIUM: 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400',
|
||||
GOOGLE_ONE_STANDARD: 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400',
|
||||
GOOGLE_ONE_BASIC: 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400',
|
||||
FREE: 'bg-gray-100 text-gray-700 dark:bg-gray-900/30 dark:text-gray-400',
|
||||
GOOGLE_ONE_UNKNOWN: 'bg-gray-100 text-gray-700 dark:bg-gray-900/30 dark:text-gray-400',
|
||||
GOOGLE_ONE_UNLIMITED: 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400'
|
||||
}
|
||||
return tierColorMap[creds?.tier_id || ''] || 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400'
|
||||
}
|
||||
|
||||
// AI Studio 默认样式:蓝色
|
||||
return 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400'
|
||||
})
|
||||
|
||||
// 是否限流
|
||||
|
||||
@@ -568,6 +568,24 @@ const isGeminiCodeAssist = computed(() => {
|
||||
// Gemini 账户类型显示标签
|
||||
const geminiTierLabel = computed(() => {
|
||||
if (!geminiTier.value) return null
|
||||
|
||||
const creds = props.account.credentials as GeminiCredentials | undefined
|
||||
const isGoogleOne = creds?.oauth_type === 'google_one'
|
||||
|
||||
if (isGoogleOne) {
|
||||
// Google One tier 标签
|
||||
const tierMap: Record<string, string> = {
|
||||
AI_PREMIUM: t('admin.accounts.tier.aiPremium'),
|
||||
GOOGLE_ONE_STANDARD: t('admin.accounts.tier.standard'),
|
||||
GOOGLE_ONE_BASIC: t('admin.accounts.tier.basic'),
|
||||
FREE: t('admin.accounts.tier.free'),
|
||||
GOOGLE_ONE_UNKNOWN: t('admin.accounts.tier.personal'),
|
||||
GOOGLE_ONE_UNLIMITED: t('admin.accounts.tier.unlimited')
|
||||
}
|
||||
return tierMap[geminiTier.value] || t('admin.accounts.tier.personal')
|
||||
}
|
||||
|
||||
// Code Assist tier 标签
|
||||
const tierMap: Record<string, string> = {
|
||||
LEGACY: t('admin.accounts.tier.free'),
|
||||
PRO: t('admin.accounts.tier.pro'),
|
||||
@@ -578,6 +596,25 @@ const geminiTierLabel = computed(() => {
|
||||
|
||||
// Gemini 账户类型徽章样式
|
||||
const geminiTierClass = computed(() => {
|
||||
if (!geminiTier.value) return ''
|
||||
|
||||
const creds = props.account.credentials as GeminiCredentials | undefined
|
||||
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 颜色
|
||||
switch (geminiTier.value) {
|
||||
case 'LEGACY':
|
||||
return 'bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-300'
|
||||
|
||||
@@ -455,6 +455,52 @@
|
||||
<div v-if="accountCategory === 'oauth-based'" class="mt-4">
|
||||
<label class="input-label">{{ t('admin.accounts.oauth.gemini.oauthTypeLabel') }}</label>
|
||||
<div class="mt-2 grid grid-cols-2 gap-3">
|
||||
<!-- Google One OAuth -->
|
||||
<button
|
||||
type="button"
|
||||
@click="handleSelectGeminiOAuthType('google_one')"
|
||||
:class="[
|
||||
'flex items-center gap-3 rounded-lg border-2 p-3 text-left transition-all',
|
||||
geminiOAuthType === 'google_one'
|
||||
? 'border-purple-500 bg-purple-50 dark:bg-purple-900/20'
|
||||
: 'border-gray-200 hover:border-purple-300 dark:border-dark-600 dark:hover:border-purple-700'
|
||||
]"
|
||||
>
|
||||
<div
|
||||
:class="[
|
||||
'flex h-8 w-8 items-center justify-center rounded-lg',
|
||||
geminiOAuthType === 'google_one'
|
||||
? 'bg-purple-500 text-white'
|
||||
: 'bg-gray-100 text-gray-500 dark:bg-dark-600 dark:text-gray-400'
|
||||
]"
|
||||
>
|
||||
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.501 20.118a7.5 7.5 0 0114.998 0A17.933 17.933 0 0112 21.75c-2.676 0-5.216-.584-7.499-1.632z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<span class="block text-sm font-medium text-gray-900 dark:text-white">
|
||||
Google One
|
||||
</span>
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">
|
||||
个人账号,享受 Google One 订阅配额
|
||||
</span>
|
||||
<div class="mt-2 flex flex-wrap gap-1">
|
||||
<span
|
||||
class="rounded bg-purple-100 px-2 py-0.5 text-[10px] font-semibold text-purple-700 dark:bg-purple-900/40 dark:text-purple-300"
|
||||
>
|
||||
推荐个人用户
|
||||
</span>
|
||||
<span
|
||||
class="rounded bg-emerald-100 px-2 py-0.5 text-[10px] font-semibold text-emerald-700 dark:bg-emerald-900/40 dark:text-emerald-300"
|
||||
>
|
||||
无需 GCP
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<!-- GCP Code Assist OAuth -->
|
||||
<button
|
||||
type="button"
|
||||
@click="handleSelectGeminiOAuthType('code_assist')"
|
||||
@@ -479,13 +525,13 @@
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<span class="block text-sm font-medium text-gray-900 dark:text-white">
|
||||
{{ t('admin.accounts.gemini.oauthType.builtInTitle') }}
|
||||
GCP Code Assist
|
||||
</span>
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('admin.accounts.gemini.oauthType.builtInDesc') }}
|
||||
企业级,需要 GCP 项目
|
||||
</span>
|
||||
<div class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('admin.accounts.gemini.oauthType.builtInRequirement') }}
|
||||
需要激活 GCP 项目并绑定信用卡
|
||||
<a
|
||||
:href="geminiHelpLinks.gcpProject"
|
||||
class="ml-1 text-blue-600 hover:underline dark:text-blue-400"
|
||||
@@ -499,94 +545,110 @@
|
||||
<span
|
||||
class="rounded bg-blue-100 px-2 py-0.5 text-[10px] font-semibold text-blue-700 dark:bg-blue-900/40 dark:text-blue-300"
|
||||
>
|
||||
{{ t('admin.accounts.gemini.oauthType.badges.recommended') }}
|
||||
企业用户
|
||||
</span>
|
||||
<span
|
||||
class="rounded bg-emerald-100 px-2 py-0.5 text-[10px] font-semibold text-emerald-700 dark:bg-emerald-900/40 dark:text-emerald-300"
|
||||
>
|
||||
{{ t('admin.accounts.gemini.oauthType.badges.highConcurrency') }}
|
||||
</span>
|
||||
<span
|
||||
class="rounded bg-gray-100 px-2 py-0.5 text-[10px] font-semibold text-gray-700 dark:bg-gray-800 dark:text-gray-300"
|
||||
>
|
||||
{{ t('admin.accounts.gemini.oauthType.badges.noAdmin') }}
|
||||
高并发
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="group relative">
|
||||
<button
|
||||
type="button"
|
||||
:disabled="!geminiAIStudioOAuthEnabled"
|
||||
@click="handleSelectGeminiOAuthType('ai_studio')"
|
||||
<!-- Advanced Options Toggle -->
|
||||
<div class="mt-3">
|
||||
<button
|
||||
type="button"
|
||||
@click="showAdvancedOAuth = !showAdvancedOAuth"
|
||||
class="flex items-center gap-2 text-sm text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-200"
|
||||
>
|
||||
<svg
|
||||
:class="['h-4 w-4 transition-transform', showAdvancedOAuth ? 'rotate-90' : '']"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
<span>{{ showAdvancedOAuth ? '隐藏' : '显示' }}高级选项(自建 OAuth Client)</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Custom OAuth Client (Advanced) -->
|
||||
<div v-if="showAdvancedOAuth" class="mt-3 group relative">
|
||||
<button
|
||||
type="button"
|
||||
:disabled="!geminiAIStudioOAuthEnabled"
|
||||
@click="handleSelectGeminiOAuthType('ai_studio')"
|
||||
:class="[
|
||||
'flex w-full items-center gap-3 rounded-lg border-2 p-3 text-left transition-all',
|
||||
!geminiAIStudioOAuthEnabled ? 'cursor-not-allowed opacity-60' : '',
|
||||
geminiOAuthType === 'ai_studio'
|
||||
? 'border-amber-500 bg-amber-50 dark:bg-amber-900/20'
|
||||
: 'border-gray-200 hover:border-amber-300 dark:border-dark-600 dark:hover:border-amber-700'
|
||||
]"
|
||||
>
|
||||
<div
|
||||
:class="[
|
||||
'flex w-full items-center gap-3 rounded-lg border-2 p-3 text-left transition-all',
|
||||
!geminiAIStudioOAuthEnabled ? 'cursor-not-allowed opacity-60' : '',
|
||||
'flex h-8 w-8 items-center justify-center rounded-lg',
|
||||
geminiOAuthType === 'ai_studio'
|
||||
? 'border-purple-500 bg-purple-50 dark:bg-purple-900/20'
|
||||
: 'border-gray-200 hover:border-purple-300 dark:border-dark-600 dark:hover:border-purple-700'
|
||||
? 'bg-amber-500 text-white'
|
||||
: 'bg-gray-100 text-gray-500 dark:bg-dark-600 dark:text-gray-400'
|
||||
]"
|
||||
>
|
||||
<div
|
||||
:class="[
|
||||
'flex h-8 w-8 items-center justify-center rounded-lg',
|
||||
geminiOAuthType === 'ai_studio'
|
||||
? 'bg-purple-500 text-white'
|
||||
: 'bg-gray-100 text-gray-500 dark:bg-dark-600 dark:text-gray-400'
|
||||
]"
|
||||
<svg
|
||||
class="h-4 w-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
>
|
||||
<svg
|
||||
class="h-4 w-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M9.813 15.904L9 18.75l-.813-2.846a4.5 4.5 0 00-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 003.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 003.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 00-3.09 3.09z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<span class="block text-sm font-medium text-gray-900 dark:text-white">
|
||||
{{ t('admin.accounts.gemini.oauthType.customTitle') }}
|
||||
</span>
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('admin.accounts.gemini.oauthType.customDesc') }}
|
||||
</span>
|
||||
<div class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('admin.accounts.gemini.oauthType.customRequirement') }}
|
||||
</div>
|
||||
<div class="mt-2 flex flex-wrap gap-1">
|
||||
<span
|
||||
class="rounded bg-purple-100 px-2 py-0.5 text-[10px] font-semibold text-purple-700 dark:bg-purple-900/40 dark:text-purple-300"
|
||||
>
|
||||
{{ t('admin.accounts.gemini.oauthType.badges.orgManaged') }}
|
||||
</span>
|
||||
<span
|
||||
class="rounded bg-amber-100 px-2 py-0.5 text-[10px] font-semibold text-amber-700 dark:bg-amber-900/40 dark:text-amber-300"
|
||||
>
|
||||
{{ t('admin.accounts.gemini.oauthType.badges.adminRequired') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
v-if="!geminiAIStudioOAuthEnabled"
|
||||
class="ml-auto shrink-0 rounded bg-amber-100 px-2 py-0.5 text-xs text-amber-700 dark:bg-amber-900/30 dark:text-amber-300"
|
||||
>
|
||||
{{ t('admin.accounts.oauth.gemini.aiStudioNotConfiguredShort') }}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<div
|
||||
v-if="!geminiAIStudioOAuthEnabled"
|
||||
class="pointer-events-none absolute right-0 top-full z-50 mt-2 w-80 rounded-md border border-amber-200 bg-amber-50 px-3 py-2 text-xs text-amber-800 opacity-0 shadow-lg transition-opacity group-hover:opacity-100 dark:border-amber-700 dark:bg-amber-900/40 dark:text-amber-200"
|
||||
>
|
||||
{{ t('admin.accounts.oauth.gemini.aiStudioNotConfiguredTip') }}
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M9.813 15.904L9 18.75l-.813-2.846a4.5 4.5 0 00-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 003.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 003.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 00-3.09 3.09z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<span class="block text-sm font-medium text-gray-900 dark:text-white">
|
||||
{{ t('admin.accounts.gemini.oauthType.customTitle') }}
|
||||
</span>
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('admin.accounts.gemini.oauthType.customDesc') }}
|
||||
</span>
|
||||
<div class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('admin.accounts.gemini.oauthType.customRequirement') }}
|
||||
</div>
|
||||
<div class="mt-2 flex flex-wrap gap-1">
|
||||
<span
|
||||
class="rounded bg-amber-100 px-2 py-0.5 text-[10px] font-semibold text-amber-700 dark:bg-amber-900/40 dark:text-amber-300"
|
||||
>
|
||||
{{ t('admin.accounts.gemini.oauthType.badges.orgManaged') }}
|
||||
</span>
|
||||
<span
|
||||
class="rounded bg-amber-100 px-2 py-0.5 text-[10px] font-semibold text-amber-700 dark:bg-amber-900/40 dark:text-amber-300"
|
||||
>
|
||||
{{ t('admin.accounts.gemini.oauthType.badges.adminRequired') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
v-if="!geminiAIStudioOAuthEnabled"
|
||||
class="ml-auto shrink-0 rounded bg-amber-100 px-2 py-0.5 text-xs text-amber-700 dark:bg-amber-900/30 dark:text-amber-300"
|
||||
>
|
||||
{{ t('admin.accounts.oauth.gemini.aiStudioNotConfiguredShort') }}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<div
|
||||
v-if="!geminiAIStudioOAuthEnabled"
|
||||
class="pointer-events-none absolute right-0 top-full z-50 mt-2 w-80 rounded-md border border-amber-200 bg-amber-50 px-3 py-2 text-xs text-amber-800 opacity-0 shadow-lg transition-opacity group-hover:opacity-100 dark:border-amber-700 dark:bg-amber-900/40 dark:text-amber-200"
|
||||
>
|
||||
{{ t('admin.accounts.oauth.gemini.aiStudioNotConfiguredTip') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1610,8 +1672,9 @@ const selectedErrorCodes = ref<number[]>([])
|
||||
const customErrorCodeInput = ref<number | null>(null)
|
||||
const interceptWarmupRequests = ref(false)
|
||||
const mixedScheduling = ref(false) // For antigravity accounts: enable mixed scheduling
|
||||
const geminiOAuthType = ref<'code_assist' | 'ai_studio'>('code_assist')
|
||||
const geminiOAuthType = ref<'code_assist' | 'google_one' | 'ai_studio'>('google_one')
|
||||
const geminiAIStudioOAuthEnabled = ref(false)
|
||||
const showAdvancedOAuth = ref(false)
|
||||
|
||||
// Common models for whitelist - Anthropic
|
||||
const anthropicModels = [
|
||||
@@ -1902,7 +1965,7 @@ watch(
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
const handleSelectGeminiOAuthType = (oauthType: 'code_assist' | 'ai_studio') => {
|
||||
const handleSelectGeminiOAuthType = (oauthType: 'code_assist' | 'google_one' | 'ai_studio') => {
|
||||
if (oauthType === 'ai_studio' && !geminiAIStudioOAuthEnabled.value) {
|
||||
appStore.showError(t('admin.accounts.oauth.gemini.aiStudioNotConfigured'))
|
||||
return
|
||||
|
||||
@@ -93,7 +93,13 @@ export function useGeminiOAuth() {
|
||||
const tokenInfo = await adminAPI.gemini.exchangeCode(payload as any)
|
||||
return tokenInfo as GeminiTokenInfo
|
||||
} catch (err: any) {
|
||||
error.value = err.response?.data?.detail || t('admin.accounts.oauth.gemini.failedToExchangeCode')
|
||||
// Check for specific missing project_id error
|
||||
const errorMessage = err.message || err.response?.data?.message || ''
|
||||
if (errorMessage.includes('missing project_id')) {
|
||||
error.value = t('admin.accounts.oauth.gemini.missingProjectId')
|
||||
} else {
|
||||
error.value = errorMessage || t('admin.accounts.oauth.gemini.failedToExchangeCode')
|
||||
}
|
||||
appStore.showError(error.value)
|
||||
return null
|
||||
} finally {
|
||||
|
||||
@@ -1076,6 +1076,7 @@ export default {
|
||||
failedToGenerateUrl: 'Failed to generate Gemini auth URL',
|
||||
missingExchangeParams: 'Missing auth code, session ID, or state',
|
||||
failedToExchangeCode: 'Failed to exchange Gemini auth code',
|
||||
missingProjectId: 'GCP Project ID retrieval failed: Your Google account is not linked to an active GCP project. Please activate GCP and bind a credit card in Google Cloud Console, or manually enter the Project ID during authorization.',
|
||||
modelPassthrough: 'Gemini Model Passthrough',
|
||||
modelPassthroughDesc:
|
||||
'All model requests are forwarded directly to the Gemini API without model restrictions or mappings.',
|
||||
@@ -1290,7 +1291,12 @@ export default {
|
||||
tier: {
|
||||
free: 'Free',
|
||||
pro: 'Pro',
|
||||
ultra: 'Ultra'
|
||||
ultra: 'Ultra',
|
||||
aiPremium: 'AI Premium',
|
||||
standard: 'Standard',
|
||||
basic: 'Basic',
|
||||
personal: 'Personal',
|
||||
unlimited: 'Unlimited'
|
||||
},
|
||||
ineligibleWarning:
|
||||
'This account is not eligible for Antigravity, but API forwarding still works. Use at your own risk.'
|
||||
|
||||
@@ -996,7 +996,12 @@ export default {
|
||||
tier: {
|
||||
free: 'Free',
|
||||
pro: 'Pro',
|
||||
ultra: 'Ultra'
|
||||
ultra: 'Ultra',
|
||||
aiPremium: 'AI Premium',
|
||||
standard: '标准版',
|
||||
basic: '基础版',
|
||||
personal: '个人版',
|
||||
unlimited: '无限制'
|
||||
},
|
||||
ineligibleWarning:
|
||||
'该账号无 Antigravity 使用权限,但仍能进行 API 转发。继续使用请自行承担风险。',
|
||||
@@ -1215,6 +1220,7 @@ export default {
|
||||
failedToGenerateUrl: '生成 Gemini 授权链接失败',
|
||||
missingExchangeParams: '缺少 code / session_id / state',
|
||||
failedToExchangeCode: 'Gemini 授权码兑换失败',
|
||||
missingProjectId: 'GCP Project ID 获取失败:您的 Google 账号未关联有效的 GCP 项目。请前往 Google Cloud Console 激活 GCP 并绑定信用卡,或在授权时手动填写 Project ID。',
|
||||
modelPassthrough: 'Gemini 直接转发模型',
|
||||
modelPassthroughDesc: '所有模型请求将直接转发至 Gemini API,不进行模型限制或映射。',
|
||||
stateWarningTitle: '提示',
|
||||
|
||||
Reference in New Issue
Block a user