2025-12-18 13:50:39 +08:00
|
|
|
|
<template>
|
2025-12-28 01:00:06 +08:00
|
|
|
|
<BaseDialog
|
|
|
|
|
|
:show="show"
|
|
|
|
|
|
:title="t('admin.accounts.createAccount')"
|
2025-12-29 16:13:09 +08:00
|
|
|
|
width="normal"
|
2025-12-28 01:00:06 +08:00
|
|
|
|
@close="handleClose"
|
|
|
|
|
|
>
|
2025-12-18 13:50:39 +08:00
|
|
|
|
<!-- Step Indicator for OAuth accounts -->
|
|
|
|
|
|
<div v-if="isOAuthFlow" class="mb-6 flex items-center justify-center">
|
|
|
|
|
|
<div class="flex items-center space-x-4">
|
|
|
|
|
|
<div class="flex items-center">
|
|
|
|
|
|
<div
|
|
|
|
|
|
:class="[
|
|
|
|
|
|
'flex h-8 w-8 items-center justify-center rounded-full text-sm font-semibold',
|
|
|
|
|
|
step >= 1 ? 'bg-primary-500 text-white' : 'bg-gray-200 text-gray-500 dark:bg-dark-600'
|
|
|
|
|
|
]"
|
|
|
|
|
|
>
|
|
|
|
|
|
1
|
|
|
|
|
|
</div>
|
2025-12-25 08:40:05 -08:00
|
|
|
|
<span class="ml-2 text-sm font-medium text-gray-700 dark:text-gray-300">{{
|
|
|
|
|
|
t('admin.accounts.oauth.authMethod')
|
|
|
|
|
|
}}</span>
|
2025-12-18 13:50:39 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="h-0.5 w-8 bg-gray-300 dark:bg-dark-600" />
|
|
|
|
|
|
<div class="flex items-center">
|
|
|
|
|
|
<div
|
|
|
|
|
|
:class="[
|
|
|
|
|
|
'flex h-8 w-8 items-center justify-center rounded-full text-sm font-semibold',
|
|
|
|
|
|
step >= 2 ? 'bg-primary-500 text-white' : 'bg-gray-200 text-gray-500 dark:bg-dark-600'
|
|
|
|
|
|
]"
|
|
|
|
|
|
>
|
|
|
|
|
|
2
|
|
|
|
|
|
</div>
|
2025-12-25 08:40:05 -08:00
|
|
|
|
<span class="ml-2 text-sm font-medium text-gray-700 dark:text-gray-300">{{
|
|
|
|
|
|
oauthStepTitle
|
|
|
|
|
|
}}</span>
|
2025-12-18 13:50:39 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Step 1: Basic Info -->
|
2025-12-28 01:00:06 +08:00
|
|
|
|
<form
|
|
|
|
|
|
v-if="step === 1"
|
|
|
|
|
|
id="create-account-form"
|
|
|
|
|
|
@submit.prevent="handleSubmit"
|
|
|
|
|
|
class="space-y-5"
|
|
|
|
|
|
>
|
2025-12-18 13:50:39 +08:00
|
|
|
|
<div>
|
|
|
|
|
|
<label class="input-label">{{ t('admin.accounts.accountName') }}</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
v-model="form.name"
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
required
|
|
|
|
|
|
class="input"
|
|
|
|
|
|
:placeholder="t('admin.accounts.enterAccountName')"
|
2025-12-29 15:21:05 +08:00
|
|
|
|
data-tour="account-form-name"
|
2025-12-18 13:50:39 +08:00
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-12-22 22:58:31 +08:00
|
|
|
|
<!-- Platform Selection - Segmented Control Style -->
|
2025-12-18 13:50:39 +08:00
|
|
|
|
<div>
|
2025-12-22 22:58:31 +08:00
|
|
|
|
<label class="input-label">{{ t('admin.accounts.platform') }}</label>
|
2025-12-29 15:21:05 +08:00
|
|
|
|
<div class="mt-2 flex rounded-lg bg-gray-100 p-1 dark:bg-dark-700" data-tour="account-form-platform">
|
2025-12-22 22:58:31 +08:00
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
@click="form.platform = 'anthropic'"
|
|
|
|
|
|
:class="[
|
2025-12-25 08:40:05 -08:00
|
|
|
|
'flex flex-1 items-center justify-center gap-2 rounded-md px-4 py-2.5 text-sm font-medium transition-all',
|
2025-12-22 22:58:31 +08:00
|
|
|
|
form.platform === 'anthropic'
|
2025-12-25 08:40:05 -08:00
|
|
|
|
? 'bg-white text-orange-600 shadow-sm dark:bg-dark-600 dark:text-orange-400'
|
|
|
|
|
|
: 'text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-200'
|
2025-12-22 22:58:31 +08:00
|
|
|
|
]"
|
|
|
|
|
|
>
|
2025-12-25 08:40:05 -08:00
|
|
|
|
<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.09zM18.259 8.715L18 9.75l-.259-1.035a3.375 3.375 0 00-2.455-2.456L14.25 6l1.036-.259a3.375 3.375 0 002.455-2.456L18 2.25l.259 1.035a3.375 3.375 0 002.456 2.456L21.75 6l-1.035.259a3.375 3.375 0 00-2.456 2.456z"
|
|
|
|
|
|
/>
|
2025-12-22 22:58:31 +08:00
|
|
|
|
</svg>
|
|
|
|
|
|
Anthropic
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
@click="form.platform = 'openai'"
|
|
|
|
|
|
:class="[
|
2025-12-25 08:40:05 -08:00
|
|
|
|
'flex flex-1 items-center justify-center gap-2 rounded-md px-4 py-2.5 text-sm font-medium transition-all',
|
2025-12-22 22:58:31 +08:00
|
|
|
|
form.platform === 'openai'
|
2025-12-25 08:40:05 -08:00
|
|
|
|
? 'bg-white text-green-600 shadow-sm dark:bg-dark-600 dark:text-green-400'
|
|
|
|
|
|
: 'text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-200'
|
2025-12-22 22:58:31 +08:00
|
|
|
|
]"
|
|
|
|
|
|
>
|
2025-12-25 08:40:05 -08:00
|
|
|
|
<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="M3.75 13.5l10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75z"
|
|
|
|
|
|
/>
|
2025-12-22 22:58:31 +08:00
|
|
|
|
</svg>
|
|
|
|
|
|
OpenAI
|
|
|
|
|
|
</button>
|
2025-12-25 08:40:05 -08:00
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
@click="form.platform = 'gemini'"
|
|
|
|
|
|
:class="[
|
|
|
|
|
|
'flex flex-1 items-center justify-center gap-2 rounded-md px-4 py-2.5 text-sm font-medium transition-all',
|
|
|
|
|
|
form.platform === 'gemini'
|
|
|
|
|
|
? 'bg-white text-blue-600 shadow-sm dark:bg-dark-600 dark:text-blue-400'
|
|
|
|
|
|
: 'text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-200'
|
|
|
|
|
|
]"
|
|
|
|
|
|
>
|
|
|
|
|
|
<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="M12 2l1.5 6.5L20 10l-6.5 1.5L12 18l-1.5-6.5L4 10l6.5-1.5L12 2z"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
Gemini
|
|
|
|
|
|
</button>
|
2025-12-28 15:54:42 +08:00
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
@click="form.platform = 'antigravity'"
|
|
|
|
|
|
:class="[
|
|
|
|
|
|
'flex flex-1 items-center justify-center gap-2 rounded-md px-4 py-2.5 text-sm font-medium transition-all',
|
|
|
|
|
|
form.platform === 'antigravity'
|
|
|
|
|
|
? 'bg-white text-purple-600 shadow-sm dark:bg-dark-600 dark:text-purple-400'
|
|
|
|
|
|
: 'text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-200'
|
|
|
|
|
|
]"
|
|
|
|
|
|
>
|
|
|
|
|
|
<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="M2.25 15a4.5 4.5 0 004.5 4.5H18a3.75 3.75 0 001.332-7.257 3 3 0 00-3.758-3.848 5.25 5.25 0 00-10.233 2.33A4.502 4.502 0 002.25 15z"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
Antigravity
|
|
|
|
|
|
</button>
|
2025-12-22 22:58:31 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Account Type Selection (Anthropic) -->
|
|
|
|
|
|
<div v-if="form.platform === 'anthropic'">
|
2025-12-18 13:50:39 +08:00
|
|
|
|
<label class="input-label">{{ t('admin.accounts.accountType') }}</label>
|
2025-12-29 15:21:05 +08:00
|
|
|
|
<div class="mt-2 grid grid-cols-2 gap-3" data-tour="account-form-type">
|
2025-12-22 22:58:31 +08:00
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
@click="accountCategory = 'oauth-based'"
|
2025-12-18 13:50:39 +08:00
|
|
|
|
:class="[
|
2025-12-25 08:40:05 -08:00
|
|
|
|
'flex items-center gap-3 rounded-lg border-2 p-3 text-left transition-all',
|
2025-12-18 13:50:39 +08:00
|
|
|
|
accountCategory === 'oauth-based'
|
2025-12-22 22:58:31 +08:00
|
|
|
|
? 'border-orange-500 bg-orange-50 dark:bg-orange-900/20'
|
2025-12-25 08:40:05 -08:00
|
|
|
|
: 'border-gray-200 hover:border-orange-300 dark:border-dark-600 dark:hover:border-orange-700'
|
2025-12-18 13:50:39 +08:00
|
|
|
|
]"
|
|
|
|
|
|
>
|
2025-12-25 08:40:05 -08:00
|
|
|
|
<div
|
|
|
|
|
|
:class="[
|
|
|
|
|
|
'flex h-8 w-8 items-center justify-center rounded-lg',
|
|
|
|
|
|
accountCategory === 'oauth-based'
|
|
|
|
|
|
? 'bg-orange-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="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.09zM18.259 8.715L18 9.75l-.259-1.035a3.375 3.375 0 00-2.455-2.456L14.25 6l1.036-.259a3.375 3.375 0 002.455-2.456L18 2.25l.259 1.035a3.375 3.375 0 002.456 2.456L21.75 6l-1.035.259a3.375 3.375 0 00-2.456 2.456z"
|
|
|
|
|
|
/>
|
2025-12-18 13:50:39 +08:00
|
|
|
|
</svg>
|
|
|
|
|
|
</div>
|
2025-12-22 22:58:31 +08:00
|
|
|
|
<div>
|
2025-12-25 08:40:05 -08:00
|
|
|
|
<span class="block text-sm font-medium text-gray-900 dark:text-white">{{
|
|
|
|
|
|
t('admin.accounts.claudeCode')
|
|
|
|
|
|
}}</span>
|
|
|
|
|
|
<span class="text-xs text-gray-500 dark:text-gray-400">{{
|
|
|
|
|
|
t('admin.accounts.oauthSetupToken')
|
|
|
|
|
|
}}</span>
|
2025-12-22 22:58:31 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</button>
|
2025-12-18 13:50:39 +08:00
|
|
|
|
|
2025-12-22 22:58:31 +08:00
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
@click="accountCategory = 'apikey'"
|
2025-12-18 13:50:39 +08:00
|
|
|
|
:class="[
|
2025-12-25 08:40:05 -08:00
|
|
|
|
'flex items-center gap-3 rounded-lg border-2 p-3 text-left transition-all',
|
2025-12-18 13:50:39 +08:00
|
|
|
|
accountCategory === 'apikey'
|
2025-12-22 22:58:31 +08:00
|
|
|
|
? 'border-purple-500 bg-purple-50 dark:bg-purple-900/20'
|
2025-12-25 08:40:05 -08:00
|
|
|
|
: 'border-gray-200 hover:border-purple-300 dark:border-dark-600 dark:hover:border-purple-700'
|
2025-12-18 13:50:39 +08:00
|
|
|
|
]"
|
|
|
|
|
|
>
|
2025-12-25 08:40:05 -08:00
|
|
|
|
<div
|
|
|
|
|
|
:class="[
|
|
|
|
|
|
'flex h-8 w-8 items-center justify-center rounded-lg',
|
|
|
|
|
|
accountCategory === 'apikey'
|
|
|
|
|
|
? '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 5.25a3 3 0 013 3m3 0a6 6 0 01-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1121.75 8.25z"
|
|
|
|
|
|
/>
|
2025-12-22 22:58:31 +08:00
|
|
|
|
</svg>
|
2025-12-18 13:50:39 +08:00
|
|
|
|
</div>
|
2025-12-22 22:58:31 +08:00
|
|
|
|
<div>
|
2025-12-25 08:40:05 -08:00
|
|
|
|
<span class="block text-sm font-medium text-gray-900 dark:text-white">{{
|
|
|
|
|
|
t('admin.accounts.claudeConsole')
|
|
|
|
|
|
}}</span>
|
|
|
|
|
|
<span class="text-xs text-gray-500 dark:text-gray-400">{{
|
|
|
|
|
|
t('admin.accounts.apiKey')
|
|
|
|
|
|
}}</span>
|
2025-12-22 22:58:31 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Account Type Selection (OpenAI) -->
|
|
|
|
|
|
<div v-if="form.platform === 'openai'">
|
|
|
|
|
|
<label class="input-label">{{ t('admin.accounts.accountType') }}</label>
|
2025-12-29 15:21:05 +08:00
|
|
|
|
<div class="mt-2 grid grid-cols-2 gap-3" data-tour="account-form-type">
|
2025-12-22 22:58:31 +08:00
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
@click="accountCategory = 'oauth-based'"
|
|
|
|
|
|
:class="[
|
2025-12-25 08:40:05 -08:00
|
|
|
|
'flex items-center gap-3 rounded-lg border-2 p-3 text-left transition-all',
|
2025-12-22 22:58:31 +08:00
|
|
|
|
accountCategory === 'oauth-based'
|
|
|
|
|
|
? 'border-green-500 bg-green-50 dark:bg-green-900/20'
|
2025-12-25 08:40:05 -08:00
|
|
|
|
: 'border-gray-200 hover:border-green-300 dark:border-dark-600 dark:hover:border-green-700'
|
2025-12-22 22:58:31 +08:00
|
|
|
|
]"
|
|
|
|
|
|
>
|
2025-12-25 08:40:05 -08:00
|
|
|
|
<div
|
|
|
|
|
|
:class="[
|
|
|
|
|
|
'flex h-8 w-8 items-center justify-center rounded-lg',
|
|
|
|
|
|
accountCategory === 'oauth-based'
|
|
|
|
|
|
? 'bg-green-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 5.25a3 3 0 013 3m3 0a6 6 0 01-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1121.75 8.25z"
|
|
|
|
|
|
/>
|
2025-12-18 13:50:39 +08:00
|
|
|
|
</svg>
|
|
|
|
|
|
</div>
|
2025-12-22 22:58:31 +08:00
|
|
|
|
<div>
|
|
|
|
|
|
<span class="block text-sm font-medium text-gray-900 dark:text-white">OAuth</span>
|
2025-12-27 10:50:25 +08:00
|
|
|
|
<span class="text-xs text-gray-500 dark:text-gray-400">{{ t('admin.accounts.types.chatgptOauth') }}</span>
|
2025-12-22 22:58:31 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
@click="accountCategory = 'apikey'"
|
|
|
|
|
|
:class="[
|
2025-12-25 08:40:05 -08:00
|
|
|
|
'flex items-center gap-3 rounded-lg border-2 p-3 text-left transition-all',
|
2025-12-22 22:58:31 +08:00
|
|
|
|
accountCategory === 'apikey'
|
|
|
|
|
|
? 'border-purple-500 bg-purple-50 dark:bg-purple-900/20'
|
2025-12-25 08:40:05 -08:00
|
|
|
|
: 'border-gray-200 hover:border-purple-300 dark:border-dark-600 dark:hover:border-purple-700'
|
2025-12-22 22:58:31 +08:00
|
|
|
|
]"
|
|
|
|
|
|
>
|
2025-12-25 08:40:05 -08:00
|
|
|
|
<div
|
|
|
|
|
|
:class="[
|
|
|
|
|
|
'flex h-8 w-8 items-center justify-center rounded-lg',
|
|
|
|
|
|
accountCategory === 'apikey'
|
|
|
|
|
|
? '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 5.25a3 3 0 013 3m3 0a6 6 0 01-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1121.75 8.25z"
|
|
|
|
|
|
/>
|
2025-12-22 22:58:31 +08:00
|
|
|
|
</svg>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<span class="block text-sm font-medium text-gray-900 dark:text-white">API Key</span>
|
2025-12-27 10:50:25 +08:00
|
|
|
|
<span class="text-xs text-gray-500 dark:text-gray-400">{{ t('admin.accounts.types.responsesApi') }}</span>
|
2025-12-22 22:58:31 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</button>
|
2025-12-18 13:50:39 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-12-25 08:40:05 -08:00
|
|
|
|
<!-- Account Type Selection (Gemini) -->
|
|
|
|
|
|
<div v-if="form.platform === 'gemini'">
|
2026-01-04 17:02:38 +08:00
|
|
|
|
<div class="flex items-center justify-between">
|
|
|
|
|
|
<label class="input-label">{{ t('admin.accounts.accountType') }}</label>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
@click="showGeminiHelpDialog = true"
|
|
|
|
|
|
class="flex items-center gap-1 rounded px-2 py-1 text-xs text-blue-600 hover:bg-blue-50 dark:text-blue-400 dark:hover:bg-blue-900/20"
|
|
|
|
|
|
>
|
|
|
|
|
|
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M9.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827v.75M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9 5.25h.008v.008H12v-.008z" />
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
{{ t('admin.accounts.gemini.helpButton') }}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
2025-12-29 15:21:05 +08:00
|
|
|
|
<div class="mt-2 grid grid-cols-2 gap-3" data-tour="account-form-type">
|
2025-12-25 08:40:05 -08:00
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
@click="accountCategory = 'oauth-based'"
|
|
|
|
|
|
:class="[
|
|
|
|
|
|
'flex items-center gap-3 rounded-lg border-2 p-3 text-left transition-all',
|
|
|
|
|
|
accountCategory === 'oauth-based'
|
|
|
|
|
|
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20'
|
|
|
|
|
|
: 'border-gray-200 hover:border-blue-300 dark:border-dark-600 dark:hover:border-blue-700'
|
|
|
|
|
|
]"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div
|
|
|
|
|
|
:class="[
|
|
|
|
|
|
'flex h-8 w-8 items-center justify-center rounded-lg',
|
|
|
|
|
|
accountCategory === 'oauth-based'
|
|
|
|
|
|
? 'bg-blue-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 5.25a3 3 0 013 3m3 0a6 6 0 01-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1121.75 8.25z"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
2026-01-01 04:22:50 +08:00
|
|
|
|
<span class="block text-sm font-medium text-gray-900 dark:text-white">
|
|
|
|
|
|
{{ t('admin.accounts.gemini.accountType.oauthTitle') }}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<span class="text-xs text-gray-500 dark:text-gray-400">
|
|
|
|
|
|
{{ t('admin.accounts.gemini.accountType.oauthDesc') }}
|
|
|
|
|
|
</span>
|
2025-12-25 08:40:05 -08:00
|
|
|
|
</div>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
@click="accountCategory = 'apikey'"
|
|
|
|
|
|
:class="[
|
|
|
|
|
|
'flex items-center gap-3 rounded-lg border-2 p-3 text-left transition-all',
|
|
|
|
|
|
accountCategory === 'apikey'
|
|
|
|
|
|
? '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',
|
|
|
|
|
|
accountCategory === 'apikey'
|
|
|
|
|
|
? '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"
|
2025-12-25 21:25:02 -08:00
|
|
|
|
d="M15.75 5.25a3 3 0 013 3m3 0a6 6 0 01-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1721.75 8.25z"
|
2025-12-25 08:40:05 -08:00
|
|
|
|
/>
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
2026-01-01 04:22:50 +08:00
|
|
|
|
<span class="block text-sm font-medium text-gray-900 dark:text-white">
|
|
|
|
|
|
{{ t('admin.accounts.gemini.accountType.apiKeyTitle') }}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<span class="text-xs text-gray-500 dark:text-gray-400">
|
|
|
|
|
|
{{ t('admin.accounts.gemini.accountType.apiKeyDesc') }}
|
|
|
|
|
|
</span>
|
2025-12-25 08:40:05 -08:00
|
|
|
|
</div>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
2025-12-25 21:25:02 -08:00
|
|
|
|
|
2026-01-01 04:22:50 +08:00
|
|
|
|
<div
|
|
|
|
|
|
v-if="accountCategory === 'apikey'"
|
|
|
|
|
|
class="mt-3 rounded-lg border border-purple-200 bg-purple-50 px-3 py-2 text-xs text-purple-800 dark:border-purple-800/40 dark:bg-purple-900/20 dark:text-purple-200"
|
|
|
|
|
|
>
|
|
|
|
|
|
<p>{{ t('admin.accounts.gemini.accountType.apiKeyNote') }}</p>
|
|
|
|
|
|
<div class="mt-2 flex flex-wrap gap-2">
|
|
|
|
|
|
<a
|
|
|
|
|
|
:href="geminiHelpLinks.apiKey"
|
|
|
|
|
|
class="font-medium text-blue-600 hover:underline dark:text-blue-400"
|
|
|
|
|
|
target="_blank"
|
|
|
|
|
|
rel="noreferrer"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ t('admin.accounts.gemini.accountType.apiKeyLink') }}
|
|
|
|
|
|
</a>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-12-25 21:25:02 -08:00
|
|
|
|
<!-- OAuth Type Selection (only show when oauth-based is selected) -->
|
|
|
|
|
|
<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">
|
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
|
|
|
|
<!-- 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 -->
|
2025-12-25 21:25:02 -08:00
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
2025-12-25 23:52:55 -08:00
|
|
|
|
@click="handleSelectGeminiOAuthType('code_assist')"
|
2025-12-25 21:25:02 -08:00
|
|
|
|
:class="[
|
|
|
|
|
|
'flex items-center gap-3 rounded-lg border-2 p-3 text-left transition-all',
|
|
|
|
|
|
geminiOAuthType === 'code_assist'
|
|
|
|
|
|
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20'
|
|
|
|
|
|
: 'border-gray-200 hover:border-blue-300 dark:border-dark-600 dark:hover:border-blue-700'
|
|
|
|
|
|
]"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div
|
|
|
|
|
|
:class="[
|
|
|
|
|
|
'flex h-8 w-8 items-center justify-center rounded-lg',
|
|
|
|
|
|
geminiOAuthType === 'code_assist'
|
|
|
|
|
|
? 'bg-blue-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="M2.25 15a4.5 4.5 0 004.5 4.5H18a3.75 3.75 0 001.332-7.257 3 3 0 00-3.758-3.848 5.25 5.25 0 00-10.233 2.33A4.502 4.502 0 002.25 15z" />
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
</div>
|
2026-01-01 04:22:50 +08:00
|
|
|
|
<div class="min-w-0">
|
|
|
|
|
|
<span class="block text-sm font-medium text-gray-900 dark:text-white">
|
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
|
|
|
|
GCP Code Assist
|
2026-01-01 04:22:50 +08:00
|
|
|
|
</span>
|
|
|
|
|
|
<span class="text-xs text-gray-500 dark:text-gray-400">
|
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
|
|
|
|
企业级,需要 GCP 项目
|
2026-01-01 04:22:50 +08:00
|
|
|
|
</span>
|
|
|
|
|
|
<div class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
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
|
|
|
|
需要激活 GCP 项目并绑定信用卡
|
2026-01-01 04:22:50 +08:00
|
|
|
|
<a
|
|
|
|
|
|
:href="geminiHelpLinks.gcpProject"
|
|
|
|
|
|
class="ml-1 text-blue-600 hover:underline dark:text-blue-400"
|
|
|
|
|
|
target="_blank"
|
|
|
|
|
|
rel="noreferrer"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ t('admin.accounts.gemini.oauthType.gcpProjectLink') }}
|
|
|
|
|
|
</a>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="mt-2 flex flex-wrap gap-1">
|
|
|
|
|
|
<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"
|
|
|
|
|
|
>
|
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-01 04:22:50 +08:00
|
|
|
|
</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"
|
|
|
|
|
|
>
|
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-01 04:22:50 +08:00
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
2025-12-25 21:25:02 -08:00
|
|
|
|
</div>
|
|
|
|
|
|
</button>
|
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
|
|
|
|
</div>
|
2025-12-25 21:25:02 -08:00
|
|
|
|
|
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
|
|
|
|
<!-- 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
|
2025-12-25 21:25:02 -08:00
|
|
|
|
:class="[
|
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
|
|
|
|
'flex h-8 w-8 items-center justify-center rounded-lg',
|
2025-12-25 21:25:02 -08:00
|
|
|
|
geminiOAuthType === 'ai_studio'
|
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
|
|
|
|
? 'bg-amber-500 text-white'
|
|
|
|
|
|
: 'bg-gray-100 text-gray-500 dark:bg-dark-600 dark:text-gray-400'
|
2025-12-25 21:25:02 -08:00
|
|
|
|
]"
|
|
|
|
|
|
>
|
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
|
|
|
|
<svg
|
|
|
|
|
|
class="h-4 w-4"
|
|
|
|
|
|
fill="none"
|
|
|
|
|
|
viewBox="0 0 24 24"
|
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
|
stroke-width="1.5"
|
2025-12-25 23:52:55 -08:00
|
|
|
|
>
|
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
|
|
|
|
<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') }}
|
2025-12-25 23:52:55 -08:00
|
|
|
|
</div>
|
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
|
|
|
|
<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') }}
|
2026-01-01 04:22:50 +08:00
|
|
|
|
</span>
|
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
|
|
|
|
<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') }}
|
2026-01-01 04:22:50 +08:00
|
|
|
|
</span>
|
2025-12-25 23:52:55 -08:00
|
|
|
|
</div>
|
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
|
|
|
|
</div>
|
|
|
|
|
|
<span
|
2025-12-25 23:52:55 -08:00
|
|
|
|
v-if="!geminiAIStudioOAuthEnabled"
|
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
|
|
|
|
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"
|
2025-12-25 23:52:55 -08:00
|
|
|
|
>
|
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
|
|
|
|
{{ 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') }}
|
2025-12-25 23:52:55 -08:00
|
|
|
|
</div>
|
2025-12-25 21:25:02 -08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-01-01 04:22:50 +08:00
|
|
|
|
|
2026-01-04 15:36:00 +08:00
|
|
|
|
<!-- Tier selection (used as fallback when auto-detection is unavailable/fails) -->
|
|
|
|
|
|
<div class="mt-4">
|
|
|
|
|
|
<label class="input-label">{{ t('admin.accounts.gemini.tier.label') }}</label>
|
|
|
|
|
|
<div class="mt-2">
|
|
|
|
|
|
<select
|
|
|
|
|
|
v-if="geminiOAuthType === 'google_one'"
|
|
|
|
|
|
v-model="geminiTierGoogleOne"
|
|
|
|
|
|
class="input"
|
|
|
|
|
|
>
|
|
|
|
|
|
<option value="google_one_free">{{ t('admin.accounts.gemini.tier.googleOne.free') }}</option>
|
|
|
|
|
|
<option value="google_ai_pro">{{ t('admin.accounts.gemini.tier.googleOne.pro') }}</option>
|
|
|
|
|
|
<option value="google_ai_ultra">{{ t('admin.accounts.gemini.tier.googleOne.ultra') }}</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
|
|
|
|
|
|
<select
|
|
|
|
|
|
v-else-if="geminiOAuthType === 'code_assist'"
|
|
|
|
|
|
v-model="geminiTierGcp"
|
|
|
|
|
|
class="input"
|
|
|
|
|
|
>
|
|
|
|
|
|
<option value="gcp_standard">{{ t('admin.accounts.gemini.tier.gcp.standard') }}</option>
|
|
|
|
|
|
<option value="gcp_enterprise">{{ t('admin.accounts.gemini.tier.gcp.enterprise') }}</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
|
|
|
|
|
|
<select
|
|
|
|
|
|
v-else
|
|
|
|
|
|
v-model="geminiTierAIStudio"
|
|
|
|
|
|
class="input"
|
|
|
|
|
|
>
|
|
|
|
|
|
<option value="aistudio_free">{{ t('admin.accounts.gemini.tier.aiStudio.free') }}</option>
|
|
|
|
|
|
<option value="aistudio_paid">{{ t('admin.accounts.gemini.tier.aiStudio.paid') }}</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<p class="input-hint">{{ t('admin.accounts.gemini.tier.hint') }}</p>
|
|
|
|
|
|
</div>
|
2025-12-25 08:40:05 -08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-12-28 15:54:42 +08:00
|
|
|
|
<!-- Account Type Selection (Antigravity - OAuth only) -->
|
|
|
|
|
|
<div v-if="form.platform === 'antigravity'">
|
|
|
|
|
|
<label class="input-label">{{ t('admin.accounts.accountType') }}</label>
|
|
|
|
|
|
<div class="mt-2">
|
|
|
|
|
|
<div
|
|
|
|
|
|
class="flex items-center gap-3 rounded-lg border-2 border-purple-500 bg-purple-50 p-3 dark:bg-purple-900/20"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="flex h-8 w-8 items-center justify-center rounded-lg bg-purple-500 text-white">
|
|
|
|
|
|
<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 5.25a3 3 0 013 3m3 0a6 6 0 01-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1121.75 8.25z"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<span class="block text-sm font-medium text-gray-900 dark:text-white">OAuth</span>
|
|
|
|
|
|
<span class="text-xs text-gray-500 dark:text-gray-400">{{ t('admin.accounts.types.antigravityOauth') }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-12-22 22:58:31 +08:00
|
|
|
|
<!-- Add Method (only for Anthropic OAuth-based type) -->
|
|
|
|
|
|
<div v-if="form.platform === 'anthropic' && isOAuthFlow">
|
2025-12-18 13:50:39 +08:00
|
|
|
|
<label class="input-label">{{ t('admin.accounts.addMethod') }}</label>
|
2025-12-25 08:40:05 -08:00
|
|
|
|
<div class="mt-2 flex gap-4">
|
2025-12-18 13:50:39 +08:00
|
|
|
|
<label class="flex cursor-pointer items-center">
|
|
|
|
|
|
<input
|
|
|
|
|
|
v-model="addMethod"
|
|
|
|
|
|
type="radio"
|
|
|
|
|
|
value="oauth"
|
|
|
|
|
|
class="mr-2 text-primary-600 focus:ring-primary-500"
|
|
|
|
|
|
/>
|
2025-12-27 10:50:25 +08:00
|
|
|
|
<span class="text-sm text-gray-700 dark:text-gray-300">{{ t('admin.accounts.types.oauth') }}</span>
|
2025-12-18 13:50:39 +08:00
|
|
|
|
</label>
|
|
|
|
|
|
<label class="flex cursor-pointer items-center">
|
|
|
|
|
|
<input
|
|
|
|
|
|
v-model="addMethod"
|
|
|
|
|
|
type="radio"
|
|
|
|
|
|
value="setup-token"
|
|
|
|
|
|
class="mr-2 text-primary-600 focus:ring-primary-500"
|
|
|
|
|
|
/>
|
2025-12-25 08:40:05 -08:00
|
|
|
|
<span class="text-sm text-gray-700 dark:text-gray-300">{{
|
|
|
|
|
|
t('admin.accounts.setupTokenLongLived')
|
|
|
|
|
|
}}</span>
|
2025-12-18 13:50:39 +08:00
|
|
|
|
</label>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- API Key input (only for apikey type) -->
|
|
|
|
|
|
<div v-if="form.type === 'apikey'" class="space-y-4">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label class="input-label">{{ t('admin.accounts.baseUrl') }}</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
v-model="apiKeyBaseUrl"
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
class="input"
|
2025-12-25 08:40:05 -08:00
|
|
|
|
:placeholder="
|
|
|
|
|
|
form.platform === 'openai'
|
|
|
|
|
|
? 'https://api.openai.com'
|
|
|
|
|
|
: form.platform === 'gemini'
|
|
|
|
|
|
? 'https://generativelanguage.googleapis.com'
|
|
|
|
|
|
: 'https://api.anthropic.com'
|
|
|
|
|
|
"
|
2025-12-18 13:50:39 +08:00
|
|
|
|
/>
|
2025-12-28 23:24:46 +08:00
|
|
|
|
<p class="input-hint">{{ baseUrlHint }}</p>
|
2025-12-18 13:50:39 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label class="input-label">{{ t('admin.accounts.apiKeyRequired') }}</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
v-model="apiKeyValue"
|
|
|
|
|
|
type="password"
|
|
|
|
|
|
required
|
|
|
|
|
|
class="input font-mono"
|
2025-12-25 08:40:05 -08:00
|
|
|
|
:placeholder="
|
|
|
|
|
|
form.platform === 'openai'
|
|
|
|
|
|
? 'sk-proj-...'
|
|
|
|
|
|
: form.platform === 'gemini'
|
|
|
|
|
|
? 'AIza...'
|
|
|
|
|
|
: 'sk-ant-...'
|
|
|
|
|
|
"
|
2025-12-18 13:50:39 +08:00
|
|
|
|
/>
|
2025-12-28 23:24:46 +08:00
|
|
|
|
<p class="input-hint">{{ apiKeyHint }}</p>
|
2025-12-18 13:50:39 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-01-04 15:36:00 +08:00
|
|
|
|
<!-- Gemini API Key tier selection -->
|
|
|
|
|
|
<div v-if="form.platform === 'gemini'">
|
|
|
|
|
|
<label class="input-label">{{ t('admin.accounts.gemini.tier.label') }}</label>
|
|
|
|
|
|
<select v-model="geminiTierAIStudio" class="input">
|
|
|
|
|
|
<option value="aistudio_free">{{ t('admin.accounts.gemini.tier.aiStudio.free') }}</option>
|
|
|
|
|
|
<option value="aistudio_paid">{{ t('admin.accounts.gemini.tier.aiStudio.paid') }}</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
<p class="input-hint">{{ t('admin.accounts.gemini.tier.aiStudioHint') }}</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-12-25 08:40:05 -08:00
|
|
|
|
<!-- Model Restriction Section (不适用于 Gemini) -->
|
|
|
|
|
|
<div v-if="form.platform !== 'gemini'" class="border-t border-gray-200 pt-4 dark:border-dark-600">
|
2025-12-18 13:50:39 +08:00
|
|
|
|
<label class="input-label">{{ t('admin.accounts.modelRestriction') }}</label>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Mode Toggle -->
|
2025-12-25 08:40:05 -08:00
|
|
|
|
<div class="mb-4 flex gap-2">
|
2025-12-18 13:50:39 +08:00
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
@click="modelRestrictionMode = 'whitelist'"
|
|
|
|
|
|
:class="[
|
|
|
|
|
|
'flex-1 rounded-lg px-4 py-2 text-sm font-medium transition-all',
|
|
|
|
|
|
modelRestrictionMode === 'whitelist'
|
|
|
|
|
|
? 'bg-primary-100 text-primary-700 dark:bg-primary-900/30 dark:text-primary-400'
|
|
|
|
|
|
: 'bg-gray-100 text-gray-600 hover:bg-gray-200 dark:bg-dark-600 dark:text-gray-400 dark:hover:bg-dark-500'
|
|
|
|
|
|
]"
|
|
|
|
|
|
>
|
2025-12-25 08:40:05 -08:00
|
|
|
|
<svg
|
|
|
|
|
|
class="mr-1.5 inline h-4 w-4"
|
|
|
|
|
|
fill="none"
|
|
|
|
|
|
viewBox="0 0 24 24"
|
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
|
>
|
|
|
|
|
|
<path
|
|
|
|
|
|
stroke-linecap="round"
|
|
|
|
|
|
stroke-linejoin="round"
|
|
|
|
|
|
stroke-width="2"
|
|
|
|
|
|
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
|
|
|
|
|
|
/>
|
2025-12-18 13:50:39 +08:00
|
|
|
|
</svg>
|
|
|
|
|
|
{{ t('admin.accounts.modelWhitelist') }}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
@click="modelRestrictionMode = 'mapping'"
|
|
|
|
|
|
:class="[
|
|
|
|
|
|
'flex-1 rounded-lg px-4 py-2 text-sm font-medium transition-all',
|
|
|
|
|
|
modelRestrictionMode === 'mapping'
|
|
|
|
|
|
? 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400'
|
|
|
|
|
|
: 'bg-gray-100 text-gray-600 hover:bg-gray-200 dark:bg-dark-600 dark:text-gray-400 dark:hover:bg-dark-500'
|
|
|
|
|
|
]"
|
|
|
|
|
|
>
|
2025-12-25 08:40:05 -08:00
|
|
|
|
<svg
|
|
|
|
|
|
class="mr-1.5 inline h-4 w-4"
|
|
|
|
|
|
fill="none"
|
|
|
|
|
|
viewBox="0 0 24 24"
|
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
|
>
|
|
|
|
|
|
<path
|
|
|
|
|
|
stroke-linecap="round"
|
|
|
|
|
|
stroke-linejoin="round"
|
|
|
|
|
|
stroke-width="2"
|
|
|
|
|
|
d="M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4"
|
|
|
|
|
|
/>
|
2025-12-18 13:50:39 +08:00
|
|
|
|
</svg>
|
|
|
|
|
|
{{ t('admin.accounts.modelMapping') }}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Whitelist Mode -->
|
|
|
|
|
|
<div v-if="modelRestrictionMode === 'whitelist'">
|
2026-01-01 16:03:48 +08:00
|
|
|
|
<ModelWhitelistSelector v-model="allowedModels" :platform="form.platform" />
|
2025-12-18 13:50:39 +08:00
|
|
|
|
<p class="text-xs text-gray-500 dark:text-gray-400">
|
|
|
|
|
|
{{ t('admin.accounts.selectedModels', { count: allowedModels.length }) }}
|
2025-12-25 08:40:05 -08:00
|
|
|
|
<span v-if="allowedModels.length === 0">{{
|
|
|
|
|
|
t('admin.accounts.supportsAllModels')
|
|
|
|
|
|
}}</span>
|
2025-12-18 13:50:39 +08:00
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Mapping Mode -->
|
|
|
|
|
|
<div v-else>
|
2025-12-25 08:40:05 -08:00
|
|
|
|
<div class="mb-3 rounded-lg bg-purple-50 p-3 dark:bg-purple-900/20">
|
2025-12-18 13:50:39 +08:00
|
|
|
|
<p class="text-xs text-purple-700 dark:text-purple-400">
|
2025-12-25 08:40:05 -08:00
|
|
|
|
<svg
|
|
|
|
|
|
class="mr-1 inline h-4 w-4"
|
|
|
|
|
|
fill="none"
|
|
|
|
|
|
viewBox="0 0 24 24"
|
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
|
>
|
|
|
|
|
|
<path
|
|
|
|
|
|
stroke-linecap="round"
|
|
|
|
|
|
stroke-linejoin="round"
|
|
|
|
|
|
stroke-width="2"
|
|
|
|
|
|
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
|
|
|
|
|
/>
|
2025-12-18 13:50:39 +08:00
|
|
|
|
</svg>
|
|
|
|
|
|
{{ t('admin.accounts.mapRequestModels') }}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Model Mapping List -->
|
2025-12-25 08:40:05 -08:00
|
|
|
|
<div v-if="modelMappings.length > 0" class="mb-3 space-y-2">
|
2025-12-18 13:50:39 +08:00
|
|
|
|
<div
|
|
|
|
|
|
v-for="(mapping, index) in modelMappings"
|
|
|
|
|
|
:key="index"
|
|
|
|
|
|
class="flex items-center gap-2"
|
|
|
|
|
|
>
|
|
|
|
|
|
<input
|
|
|
|
|
|
v-model="mapping.from"
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
class="input flex-1"
|
|
|
|
|
|
:placeholder="t('admin.accounts.requestModel')"
|
|
|
|
|
|
/>
|
2025-12-25 08:40:05 -08:00
|
|
|
|
<svg
|
|
|
|
|
|
class="h-4 w-4 flex-shrink-0 text-gray-400"
|
|
|
|
|
|
fill="none"
|
|
|
|
|
|
viewBox="0 0 24 24"
|
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
|
>
|
|
|
|
|
|
<path
|
|
|
|
|
|
stroke-linecap="round"
|
|
|
|
|
|
stroke-linejoin="round"
|
|
|
|
|
|
stroke-width="2"
|
|
|
|
|
|
d="M14 5l7 7m0 0l-7 7m7-7H3"
|
|
|
|
|
|
/>
|
2025-12-18 13:50:39 +08:00
|
|
|
|
</svg>
|
|
|
|
|
|
<input
|
|
|
|
|
|
v-model="mapping.to"
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
class="input flex-1"
|
|
|
|
|
|
:placeholder="t('admin.accounts.actualModel')"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
@click="removeModelMapping(index)"
|
2025-12-25 08:40:05 -08:00
|
|
|
|
class="rounded-lg p-2 text-red-500 transition-colors hover:bg-red-50 hover:text-red-600 dark:hover:bg-red-900/20"
|
2025-12-18 13:50:39 +08:00
|
|
|
|
>
|
2025-12-25 08:40:05 -08:00
|
|
|
|
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
|
|
|
|
<path
|
|
|
|
|
|
stroke-linecap="round"
|
|
|
|
|
|
stroke-linejoin="round"
|
|
|
|
|
|
stroke-width="2"
|
|
|
|
|
|
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
|
|
|
|
|
/>
|
2025-12-18 13:50:39 +08:00
|
|
|
|
</svg>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
@click="addModelMapping"
|
2025-12-25 08:40:05 -08:00
|
|
|
|
class="mb-3 w-full rounded-lg border-2 border-dashed border-gray-300 px-4 py-2 text-gray-600 transition-colors hover:border-gray-400 hover:text-gray-700 dark:border-dark-500 dark:text-gray-400 dark:hover:border-dark-400 dark:hover:text-gray-300"
|
2025-12-18 13:50:39 +08:00
|
|
|
|
>
|
2025-12-25 08:40:05 -08:00
|
|
|
|
<svg
|
|
|
|
|
|
class="mr-1 inline h-4 w-4"
|
|
|
|
|
|
fill="none"
|
|
|
|
|
|
viewBox="0 0 24 24"
|
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
|
>
|
|
|
|
|
|
<path
|
|
|
|
|
|
stroke-linecap="round"
|
|
|
|
|
|
stroke-linejoin="round"
|
|
|
|
|
|
stroke-width="2"
|
|
|
|
|
|
d="M12 4v16m8-8H4"
|
|
|
|
|
|
/>
|
2025-12-18 13:50:39 +08:00
|
|
|
|
</svg>
|
|
|
|
|
|
{{ t('admin.accounts.addMapping') }}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Quick Add Buttons -->
|
|
|
|
|
|
<div class="flex flex-wrap gap-2">
|
|
|
|
|
|
<button
|
|
|
|
|
|
v-for="preset in presetMappings"
|
|
|
|
|
|
:key="preset.label"
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
@click="addPresetMapping(preset.from, preset.to)"
|
2025-12-25 08:40:05 -08:00
|
|
|
|
:class="['rounded-lg px-3 py-1 text-xs transition-colors', preset.color]"
|
2025-12-18 13:50:39 +08:00
|
|
|
|
>
|
|
|
|
|
|
+ {{ preset.label }}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Custom Error Codes Section -->
|
2025-12-25 08:40:05 -08:00
|
|
|
|
<div
|
|
|
|
|
|
v-if="form.platform !== 'gemini'"
|
|
|
|
|
|
class="border-t border-gray-200 pt-4 dark:border-dark-600"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="mb-3 flex items-center justify-between">
|
2025-12-18 13:50:39 +08:00
|
|
|
|
<div>
|
|
|
|
|
|
<label class="input-label mb-0">{{ t('admin.accounts.customErrorCodes') }}</label>
|
2025-12-25 08:40:05 -08:00
|
|
|
|
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
|
|
|
|
|
{{ t('admin.accounts.customErrorCodesHint') }}
|
|
|
|
|
|
</p>
|
2025-12-18 13:50:39 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
@click="customErrorCodesEnabled = !customErrorCodesEnabled"
|
|
|
|
|
|
:class="[
|
|
|
|
|
|
'relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2',
|
|
|
|
|
|
customErrorCodesEnabled ? 'bg-primary-600' : 'bg-gray-200 dark:bg-dark-600'
|
|
|
|
|
|
]"
|
|
|
|
|
|
>
|
|
|
|
|
|
<span
|
|
|
|
|
|
:class="[
|
|
|
|
|
|
'pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out',
|
|
|
|
|
|
customErrorCodesEnabled ? 'translate-x-5' : 'translate-x-0'
|
|
|
|
|
|
]"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div v-if="customErrorCodesEnabled" class="space-y-3">
|
2025-12-25 08:40:05 -08:00
|
|
|
|
<div class="rounded-lg bg-amber-50 p-3 dark:bg-amber-900/20">
|
2025-12-18 13:50:39 +08:00
|
|
|
|
<p class="text-xs text-amber-700 dark:text-amber-400">
|
2025-12-25 08:40:05 -08:00
|
|
|
|
<svg
|
|
|
|
|
|
class="mr-1 inline h-4 w-4"
|
|
|
|
|
|
fill="none"
|
|
|
|
|
|
viewBox="0 0 24 24"
|
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
|
>
|
|
|
|
|
|
<path
|
|
|
|
|
|
stroke-linecap="round"
|
|
|
|
|
|
stroke-linejoin="round"
|
|
|
|
|
|
stroke-width="2"
|
|
|
|
|
|
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
|
|
|
|
|
|
/>
|
2025-12-18 13:50:39 +08:00
|
|
|
|
</svg>
|
|
|
|
|
|
{{ t('admin.accounts.customErrorCodesWarning') }}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Error Code Buttons -->
|
|
|
|
|
|
<div class="flex flex-wrap gap-2">
|
|
|
|
|
|
<button
|
|
|
|
|
|
v-for="code in commonErrorCodes"
|
|
|
|
|
|
:key="code.value"
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
@click="toggleErrorCode(code.value)"
|
|
|
|
|
|
:class="[
|
|
|
|
|
|
'rounded-lg px-3 py-1.5 text-sm font-medium transition-colors',
|
|
|
|
|
|
selectedErrorCodes.includes(code.value)
|
2025-12-25 08:40:05 -08:00
|
|
|
|
? 'bg-red-100 text-red-700 ring-1 ring-red-500 dark:bg-red-900/30 dark:text-red-400'
|
2025-12-18 13:50:39 +08:00
|
|
|
|
: 'bg-gray-100 text-gray-600 hover:bg-gray-200 dark:bg-dark-600 dark:text-gray-400 dark:hover:bg-dark-500'
|
|
|
|
|
|
]"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ code.value }} {{ code.label }}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Manual input -->
|
|
|
|
|
|
<div class="flex items-center gap-2">
|
|
|
|
|
|
<input
|
2026-01-03 06:34:00 -08:00
|
|
|
|
v-model.number="customErrorCodeInput"
|
2025-12-18 13:50:39 +08:00
|
|
|
|
type="number"
|
|
|
|
|
|
min="100"
|
|
|
|
|
|
max="599"
|
|
|
|
|
|
class="input flex-1"
|
|
|
|
|
|
:placeholder="t('admin.accounts.enterErrorCode')"
|
|
|
|
|
|
@keyup.enter="addCustomErrorCode"
|
|
|
|
|
|
/>
|
2025-12-25 08:40:05 -08:00
|
|
|
|
<button type="button" @click="addCustomErrorCode" class="btn btn-secondary px-3">
|
|
|
|
|
|
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
|
|
|
|
<path
|
|
|
|
|
|
stroke-linecap="round"
|
|
|
|
|
|
stroke-linejoin="round"
|
|
|
|
|
|
stroke-width="2"
|
|
|
|
|
|
d="M12 4v16m8-8H4"
|
|
|
|
|
|
/>
|
2025-12-18 13:50:39 +08:00
|
|
|
|
</svg>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Selected codes summary -->
|
|
|
|
|
|
<div class="flex flex-wrap gap-1.5">
|
|
|
|
|
|
<span
|
|
|
|
|
|
v-for="code in selectedErrorCodes.sort((a, b) => a - b)"
|
|
|
|
|
|
:key="code"
|
2025-12-25 08:40:05 -08:00
|
|
|
|
class="inline-flex items-center gap-1 rounded-full bg-red-100 px-2.5 py-0.5 text-sm font-medium text-red-700 dark:bg-red-900/30 dark:text-red-400"
|
2025-12-18 13:50:39 +08:00
|
|
|
|
>
|
|
|
|
|
|
{{ code }}
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
@click="removeErrorCode(code)"
|
|
|
|
|
|
class="hover:text-red-900 dark:hover:text-red-300"
|
|
|
|
|
|
>
|
2025-12-25 08:40:05 -08:00
|
|
|
|
<svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
|
|
|
|
<path
|
|
|
|
|
|
stroke-linecap="round"
|
|
|
|
|
|
stroke-linejoin="round"
|
|
|
|
|
|
stroke-width="2"
|
|
|
|
|
|
d="M6 18L18 6M6 6l12 12"
|
|
|
|
|
|
/>
|
2025-12-18 13:50:39 +08:00
|
|
|
|
</svg>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<span v-if="selectedErrorCodes.length === 0" class="text-xs text-gray-400">
|
|
|
|
|
|
{{ t('admin.accounts.noneSelectedUsesDefault') }}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2025-12-25 08:40:05 -08:00
|
|
|
|
|
|
|
|
|
|
<!-- Gemini 模型说明 -->
|
|
|
|
|
|
<div v-if="form.platform === 'gemini'" class="border-t border-gray-200 pt-4 dark:border-dark-600">
|
|
|
|
|
|
<div class="rounded-lg bg-blue-50 p-4 dark:bg-blue-900/20">
|
|
|
|
|
|
<div class="flex items-start gap-3">
|
|
|
|
|
|
<svg
|
|
|
|
|
|
class="h-5 w-5 flex-shrink-0 text-blue-600 dark:text-blue-400"
|
|
|
|
|
|
fill="none"
|
|
|
|
|
|
viewBox="0 0 24 24"
|
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
|
>
|
|
|
|
|
|
<path
|
|
|
|
|
|
stroke-linecap="round"
|
|
|
|
|
|
stroke-linejoin="round"
|
|
|
|
|
|
stroke-width="2"
|
|
|
|
|
|
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<p class="text-sm font-medium text-blue-800 dark:text-blue-300">
|
|
|
|
|
|
{{ t('admin.accounts.gemini.modelPassthrough') }}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<p class="mt-1 text-xs text-blue-700 dark:text-blue-400">
|
|
|
|
|
|
{{ t('admin.accounts.gemini.modelPassthroughDesc') }}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2025-12-18 13:50:39 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-01-03 06:34:00 -08:00
|
|
|
|
<!-- Temp Unschedulable Rules -->
|
|
|
|
|
|
<div class="border-t border-gray-200 pt-4 dark:border-dark-600">
|
|
|
|
|
|
<div class="mb-3 flex items-center justify-between">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label class="input-label mb-0">{{ t('admin.accounts.tempUnschedulable.title') }}</label>
|
|
|
|
|
|
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
|
|
|
|
|
{{ t('admin.accounts.tempUnschedulable.hint') }}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
@click="tempUnschedEnabled = !tempUnschedEnabled"
|
|
|
|
|
|
:class="[
|
|
|
|
|
|
'relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2',
|
|
|
|
|
|
tempUnschedEnabled ? 'bg-primary-600' : 'bg-gray-200 dark:bg-dark-600'
|
|
|
|
|
|
]"
|
|
|
|
|
|
>
|
|
|
|
|
|
<span
|
|
|
|
|
|
:class="[
|
|
|
|
|
|
'pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out',
|
|
|
|
|
|
tempUnschedEnabled ? 'translate-x-5' : 'translate-x-0'
|
|
|
|
|
|
]"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div v-if="tempUnschedEnabled" class="space-y-3">
|
|
|
|
|
|
<div class="rounded-lg bg-blue-50 p-3 dark:bg-blue-900/20">
|
|
|
|
|
|
<p class="text-xs text-blue-700 dark:text-blue-400">
|
|
|
|
|
|
<svg
|
|
|
|
|
|
class="mr-1 inline h-4 w-4"
|
|
|
|
|
|
fill="none"
|
|
|
|
|
|
viewBox="0 0 24 24"
|
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
|
>
|
|
|
|
|
|
<path
|
|
|
|
|
|
stroke-linecap="round"
|
|
|
|
|
|
stroke-linejoin="round"
|
|
|
|
|
|
stroke-width="2"
|
|
|
|
|
|
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
{{ t('admin.accounts.tempUnschedulable.notice') }}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="flex flex-wrap gap-2">
|
|
|
|
|
|
<button
|
|
|
|
|
|
v-for="preset in tempUnschedPresets"
|
|
|
|
|
|
:key="preset.label"
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
@click="addTempUnschedRule(preset.rule)"
|
|
|
|
|
|
class="rounded-lg bg-gray-100 px-3 py-1.5 text-xs font-medium text-gray-600 transition-colors hover:bg-gray-200 dark:bg-dark-600 dark:text-gray-300 dark:hover:bg-dark-500"
|
|
|
|
|
|
>
|
|
|
|
|
|
+ {{ preset.label }}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div v-if="tempUnschedRules.length > 0" class="space-y-3">
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-for="(rule, index) in tempUnschedRules"
|
|
|
|
|
|
:key="index"
|
|
|
|
|
|
class="rounded-lg border border-gray-200 p-3 dark:border-dark-600"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="mb-2 flex items-center justify-between">
|
|
|
|
|
|
<span class="text-xs font-medium text-gray-500 dark:text-gray-400">
|
|
|
|
|
|
{{ t('admin.accounts.tempUnschedulable.ruleIndex', { index: index + 1 }) }}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<div class="flex items-center gap-2">
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
:disabled="index === 0"
|
|
|
|
|
|
@click="moveTempUnschedRule(index, -1)"
|
|
|
|
|
|
class="rounded p-1 text-gray-400 transition-colors hover:text-gray-600 disabled:cursor-not-allowed disabled:opacity-40 dark:hover:text-gray-200"
|
|
|
|
|
|
>
|
|
|
|
|
|
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 15l7-7 7 7" />
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
:disabled="index === tempUnschedRules.length - 1"
|
|
|
|
|
|
@click="moveTempUnschedRule(index, 1)"
|
|
|
|
|
|
class="rounded p-1 text-gray-400 transition-colors hover:text-gray-600 disabled:cursor-not-allowed disabled:opacity-40 dark:hover:text-gray-200"
|
|
|
|
|
|
>
|
|
|
|
|
|
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
@click="removeTempUnschedRule(index)"
|
|
|
|
|
|
class="rounded p-1 text-red-500 transition-colors hover:text-red-600"
|
|
|
|
|
|
>
|
|
|
|
|
|
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
|
|
|
|
<path
|
|
|
|
|
|
stroke-linecap="round"
|
|
|
|
|
|
stroke-linejoin="round"
|
|
|
|
|
|
stroke-width="2"
|
|
|
|
|
|
d="M6 18L18 6M6 6l12 12"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label class="input-label">{{ t('admin.accounts.tempUnschedulable.errorCode') }}</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
v-model.number="rule.error_code"
|
|
|
|
|
|
type="number"
|
|
|
|
|
|
min="100"
|
|
|
|
|
|
max="599"
|
|
|
|
|
|
class="input"
|
|
|
|
|
|
:placeholder="t('admin.accounts.tempUnschedulable.errorCodePlaceholder')"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label class="input-label">{{ t('admin.accounts.tempUnschedulable.durationMinutes') }}</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
v-model.number="rule.duration_minutes"
|
|
|
|
|
|
type="number"
|
|
|
|
|
|
min="1"
|
|
|
|
|
|
class="input"
|
|
|
|
|
|
:placeholder="t('admin.accounts.tempUnschedulable.durationPlaceholder')"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="sm:col-span-2">
|
|
|
|
|
|
<label class="input-label">{{ t('admin.accounts.tempUnschedulable.keywords') }}</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
v-model="rule.keywords"
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
class="input"
|
|
|
|
|
|
:placeholder="t('admin.accounts.tempUnschedulable.keywordsPlaceholder')"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<p class="input-hint">{{ t('admin.accounts.tempUnschedulable.keywordsHint') }}</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="sm:col-span-2">
|
|
|
|
|
|
<label class="input-label">{{ t('admin.accounts.tempUnschedulable.description') }}</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
v-model="rule.description"
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
class="input"
|
|
|
|
|
|
:placeholder="t('admin.accounts.tempUnschedulable.descriptionPlaceholder')"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
@click="addTempUnschedRule()"
|
|
|
|
|
|
class="w-full rounded-lg border-2 border-dashed border-gray-300 px-4 py-2 text-sm text-gray-600 transition-colors hover:border-gray-400 hover:text-gray-700 dark:border-dark-500 dark:text-gray-400 dark:hover:border-dark-400 dark:hover:text-gray-300"
|
|
|
|
|
|
>
|
|
|
|
|
|
<svg
|
|
|
|
|
|
class="mr-1 inline h-4 w-4"
|
|
|
|
|
|
fill="none"
|
|
|
|
|
|
viewBox="0 0 24 24"
|
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
|
>
|
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
{{ t('admin.accounts.tempUnschedulable.addRule') }}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-12-22 22:58:31 +08:00
|
|
|
|
<!-- Intercept Warmup Requests (Anthropic only) -->
|
2025-12-25 08:40:05 -08:00
|
|
|
|
<div
|
|
|
|
|
|
v-if="form.platform === 'anthropic'"
|
|
|
|
|
|
class="border-t border-gray-200 pt-4 dark:border-dark-600"
|
|
|
|
|
|
>
|
2025-12-19 16:39:25 +08:00
|
|
|
|
<div class="flex items-center justify-between">
|
|
|
|
|
|
<div>
|
2025-12-25 08:40:05 -08:00
|
|
|
|
<label class="input-label mb-0">{{
|
|
|
|
|
|
t('admin.accounts.interceptWarmupRequests')
|
|
|
|
|
|
}}</label>
|
|
|
|
|
|
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
|
|
|
|
|
{{ t('admin.accounts.interceptWarmupRequestsDesc') }}
|
|
|
|
|
|
</p>
|
2025-12-19 16:39:25 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
@click="interceptWarmupRequests = !interceptWarmupRequests"
|
|
|
|
|
|
:class="[
|
|
|
|
|
|
'relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2',
|
|
|
|
|
|
interceptWarmupRequests ? 'bg-primary-600' : 'bg-gray-200 dark:bg-dark-600'
|
|
|
|
|
|
]"
|
|
|
|
|
|
>
|
|
|
|
|
|
<span
|
|
|
|
|
|
:class="[
|
|
|
|
|
|
'pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out',
|
|
|
|
|
|
interceptWarmupRequests ? 'translate-x-5' : 'translate-x-0'
|
|
|
|
|
|
]"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-12-18 13:50:39 +08:00
|
|
|
|
<div>
|
|
|
|
|
|
<label class="input-label">{{ t('admin.accounts.proxy') }}</label>
|
2025-12-25 08:40:05 -08:00
|
|
|
|
<ProxySelector v-model="form.proxy_id" :proxies="proxies" />
|
2025-12-18 13:50:39 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="grid grid-cols-2 gap-4">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label class="input-label">{{ t('admin.accounts.concurrency') }}</label>
|
2025-12-25 08:40:05 -08:00
|
|
|
|
<input v-model.number="form.concurrency" type="number" min="1" class="input" />
|
2025-12-18 13:50:39 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label class="input-label">{{ t('admin.accounts.priority') }}</label>
|
2025-12-29 15:21:05 +08:00
|
|
|
|
<input
|
|
|
|
|
|
v-model.number="form.priority"
|
|
|
|
|
|
type="number"
|
|
|
|
|
|
min="1"
|
|
|
|
|
|
class="input"
|
|
|
|
|
|
data-tour="account-form-priority"
|
|
|
|
|
|
/>
|
2025-12-18 13:50:39 +08:00
|
|
|
|
<p class="input-hint">{{ t('admin.accounts.priorityHint') }}</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-12-29 09:44:39 +08:00
|
|
|
|
<!-- Mixed Scheduling (only for antigravity accounts) -->
|
|
|
|
|
|
<div v-if="form.platform === 'antigravity'" class="flex items-center gap-2">
|
|
|
|
|
|
<label class="flex cursor-pointer items-center gap-2">
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="checkbox"
|
|
|
|
|
|
v-model="mixedScheduling"
|
|
|
|
|
|
class="h-4 w-4 rounded border-gray-300 text-primary-500 focus:ring-primary-500 dark:border-dark-500"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">
|
|
|
|
|
|
{{ t('admin.accounts.mixedScheduling') }}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<div class="group relative">
|
|
|
|
|
|
<span
|
|
|
|
|
|
class="inline-flex h-4 w-4 cursor-help items-center justify-center rounded-full bg-gray-200 text-xs text-gray-500 hover:bg-gray-300 dark:bg-dark-600 dark:text-gray-400 dark:hover:bg-dark-500"
|
|
|
|
|
|
>
|
|
|
|
|
|
?
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<!-- Tooltip(向下显示避免被弹窗裁剪) -->
|
|
|
|
|
|
<div
|
|
|
|
|
|
class="pointer-events-none absolute left-0 top-full z-[100] mt-1.5 w-72 rounded bg-gray-900 px-3 py-2 text-xs text-white opacity-0 transition-opacity group-hover:opacity-100 dark:bg-gray-700"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ t('admin.accounts.mixedSchedulingTooltip') }}
|
|
|
|
|
|
<div
|
|
|
|
|
|
class="absolute bottom-full left-3 border-4 border-transparent border-b-gray-900 dark:border-b-gray-700"
|
|
|
|
|
|
></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2025-12-29 15:21:05 +08:00
|
|
|
|
</div>
|
2025-12-18 13:50:39 +08:00
|
|
|
|
|
2025-12-29 03:17:25 +08:00
|
|
|
|
<!-- Group Selection - 仅标准模式显示 -->
|
2025-12-29 09:44:39 +08:00
|
|
|
|
<GroupSelector
|
2025-12-29 03:17:25 +08:00
|
|
|
|
v-if="!authStore.isSimpleMode"
|
2025-12-29 09:44:39 +08:00
|
|
|
|
v-model="form.group_ids"
|
|
|
|
|
|
:groups="groups"
|
|
|
|
|
|
:platform="form.platform"
|
|
|
|
|
|
:mixed-scheduling="mixedScheduling"
|
2025-12-29 20:32:20 +08:00
|
|
|
|
data-tour="account-form-groups"
|
2025-12-29 09:44:39 +08:00
|
|
|
|
/>
|
2025-12-18 13:50:39 +08:00
|
|
|
|
|
2025-12-28 01:00:06 +08:00
|
|
|
|
</form>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Step 2: OAuth Authorization -->
|
|
|
|
|
|
<div v-else class="space-y-5">
|
|
|
|
|
|
<OAuthAuthorizationFlow
|
|
|
|
|
|
ref="oauthFlowRef"
|
|
|
|
|
|
:add-method="form.platform === 'anthropic' ? addMethod : 'oauth'"
|
|
|
|
|
|
:auth-url="currentAuthUrl"
|
|
|
|
|
|
:session-id="currentSessionId"
|
|
|
|
|
|
:loading="currentOAuthLoading"
|
|
|
|
|
|
:error="currentOAuthError"
|
|
|
|
|
|
:show-help="form.platform === 'anthropic'"
|
|
|
|
|
|
:show-proxy-warning="form.platform !== 'openai' && !!form.proxy_id"
|
|
|
|
|
|
:allow-multiple="form.platform === 'anthropic'"
|
|
|
|
|
|
:show-cookie-option="form.platform === 'anthropic'"
|
|
|
|
|
|
:platform="form.platform"
|
|
|
|
|
|
:show-project-id="geminiOAuthType === 'code_assist'"
|
|
|
|
|
|
@generate-url="handleGenerateUrl"
|
|
|
|
|
|
@cookie-auth="handleCookieAuth"
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<template #footer>
|
|
|
|
|
|
<div v-if="step === 1" class="flex justify-end gap-3">
|
2025-12-25 08:40:05 -08:00
|
|
|
|
<button @click="handleClose" type="button" class="btn btn-secondary">
|
2025-12-18 13:50:39 +08:00
|
|
|
|
{{ t('common.cancel') }}
|
|
|
|
|
|
</button>
|
2025-12-28 01:00:06 +08:00
|
|
|
|
<button
|
|
|
|
|
|
type="submit"
|
|
|
|
|
|
form="create-account-form"
|
|
|
|
|
|
:disabled="submitting"
|
|
|
|
|
|
class="btn btn-primary"
|
2025-12-29 15:21:05 +08:00
|
|
|
|
data-tour="account-form-submit"
|
2025-12-28 01:00:06 +08:00
|
|
|
|
>
|
2025-12-18 13:50:39 +08:00
|
|
|
|
<svg
|
|
|
|
|
|
v-if="submitting"
|
2025-12-25 08:40:05 -08:00
|
|
|
|
class="-ml-1 mr-2 h-4 w-4 animate-spin"
|
2025-12-18 13:50:39 +08:00
|
|
|
|
fill="none"
|
|
|
|
|
|
viewBox="0 0 24 24"
|
|
|
|
|
|
>
|
2025-12-25 08:40:05 -08:00
|
|
|
|
<circle
|
|
|
|
|
|
class="opacity-25"
|
|
|
|
|
|
cx="12"
|
|
|
|
|
|
cy="12"
|
|
|
|
|
|
r="10"
|
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
|
stroke-width="4"
|
|
|
|
|
|
></circle>
|
|
|
|
|
|
<path
|
|
|
|
|
|
class="opacity-75"
|
|
|
|
|
|
fill="currentColor"
|
|
|
|
|
|
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
|
|
|
|
|
></path>
|
2025-12-18 13:50:39 +08:00
|
|
|
|
</svg>
|
2025-12-25 08:40:05 -08:00
|
|
|
|
{{
|
|
|
|
|
|
isOAuthFlow
|
|
|
|
|
|
? t('common.next')
|
|
|
|
|
|
: submitting
|
|
|
|
|
|
? t('admin.accounts.creating')
|
|
|
|
|
|
: t('common.create')
|
|
|
|
|
|
}}
|
2025-12-18 13:50:39 +08:00
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
2025-12-28 01:00:06 +08:00
|
|
|
|
<div v-else class="flex justify-between gap-3">
|
2025-12-25 08:40:05 -08:00
|
|
|
|
<button type="button" class="btn btn-secondary" @click="goBackToBasicInfo">
|
2025-12-18 13:50:39 +08:00
|
|
|
|
{{ t('common.back') }}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button
|
2025-12-18 14:26:55 +08:00
|
|
|
|
v-if="isManualInputMethod"
|
2025-12-18 13:50:39 +08:00
|
|
|
|
type="button"
|
|
|
|
|
|
:disabled="!canExchangeCode"
|
|
|
|
|
|
class="btn btn-primary"
|
|
|
|
|
|
@click="handleExchangeCode"
|
|
|
|
|
|
>
|
|
|
|
|
|
<svg
|
2025-12-22 22:58:31 +08:00
|
|
|
|
v-if="currentOAuthLoading"
|
2025-12-25 08:40:05 -08:00
|
|
|
|
class="-ml-1 mr-2 h-4 w-4 animate-spin"
|
2025-12-18 13:50:39 +08:00
|
|
|
|
fill="none"
|
|
|
|
|
|
viewBox="0 0 24 24"
|
|
|
|
|
|
>
|
2025-12-25 08:40:05 -08:00
|
|
|
|
<circle
|
|
|
|
|
|
class="opacity-25"
|
|
|
|
|
|
cx="12"
|
|
|
|
|
|
cy="12"
|
|
|
|
|
|
r="10"
|
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
|
stroke-width="4"
|
|
|
|
|
|
></circle>
|
|
|
|
|
|
<path
|
|
|
|
|
|
class="opacity-75"
|
|
|
|
|
|
fill="currentColor"
|
|
|
|
|
|
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
|
|
|
|
|
></path>
|
2025-12-18 13:50:39 +08:00
|
|
|
|
</svg>
|
2025-12-25 08:40:05 -08:00
|
|
|
|
{{
|
|
|
|
|
|
currentOAuthLoading
|
|
|
|
|
|
? t('admin.accounts.oauth.verifying')
|
|
|
|
|
|
: t('admin.accounts.oauth.completeAuth')
|
|
|
|
|
|
}}
|
2025-12-18 13:50:39 +08:00
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
2025-12-28 01:00:06 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
</BaseDialog>
|
2026-01-04 17:02:38 +08:00
|
|
|
|
|
|
|
|
|
|
<!-- Gemini Help Dialog -->
|
|
|
|
|
|
<BaseDialog
|
|
|
|
|
|
:show="showGeminiHelpDialog"
|
|
|
|
|
|
:title="t('admin.accounts.gemini.helpDialog.title')"
|
|
|
|
|
|
@close="showGeminiHelpDialog = false"
|
|
|
|
|
|
max-width="max-w-3xl"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="space-y-6">
|
|
|
|
|
|
<!-- Setup Guide Section -->
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<h3 class="mb-3 text-sm font-semibold text-gray-900 dark:text-white">
|
|
|
|
|
|
{{ t('admin.accounts.gemini.setupGuide.title') }}
|
|
|
|
|
|
</h3>
|
|
|
|
|
|
<div class="space-y-4">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<p class="mb-2 text-sm font-medium text-gray-700 dark:text-gray-300">
|
|
|
|
|
|
{{ t('admin.accounts.gemini.setupGuide.checklistTitle') }}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<ul class="list-inside list-disc space-y-1 text-sm text-gray-600 dark:text-gray-400">
|
|
|
|
|
|
<li>{{ t('admin.accounts.gemini.setupGuide.checklistItems.usIp') }}</li>
|
|
|
|
|
|
<li>{{ t('admin.accounts.gemini.setupGuide.checklistItems.age') }}</li>
|
|
|
|
|
|
</ul>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<p class="mb-2 text-sm font-medium text-gray-700 dark:text-gray-300">
|
|
|
|
|
|
{{ t('admin.accounts.gemini.setupGuide.activationTitle') }}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<ul class="list-inside list-disc space-y-1 text-sm text-gray-600 dark:text-gray-400">
|
|
|
|
|
|
<li>{{ t('admin.accounts.gemini.setupGuide.activationItems.geminiWeb') }}</li>
|
|
|
|
|
|
<li>{{ t('admin.accounts.gemini.setupGuide.activationItems.gcpProject') }}</li>
|
|
|
|
|
|
</ul>
|
|
|
|
|
|
<div class="mt-2 flex flex-wrap gap-2">
|
|
|
|
|
|
<a
|
2026-01-04 18:26:39 -08:00
|
|
|
|
href="https://policies.google.com/terms"
|
2026-01-04 17:02:38 +08:00
|
|
|
|
target="_blank"
|
|
|
|
|
|
rel="noreferrer"
|
|
|
|
|
|
class="text-sm text-blue-600 hover:underline dark:text-blue-400"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ t('admin.accounts.gemini.setupGuide.links.countryCheck') }}
|
|
|
|
|
|
</a>
|
|
|
|
|
|
<span class="text-gray-400">·</span>
|
|
|
|
|
|
<a
|
2026-01-04 18:26:39 -08:00
|
|
|
|
href="https://policies.google.com/country-association-form"
|
|
|
|
|
|
target="_blank"
|
|
|
|
|
|
rel="noreferrer"
|
|
|
|
|
|
class="text-sm text-blue-600 hover:underline dark:text-blue-400"
|
|
|
|
|
|
>
|
|
|
|
|
|
修改归属地
|
|
|
|
|
|
</a>
|
|
|
|
|
|
<span class="text-gray-400">·</span>
|
|
|
|
|
|
<a
|
|
|
|
|
|
href="https://gemini.google.com/gems/create?hl=en-US&pli=1"
|
2026-01-04 17:02:38 +08:00
|
|
|
|
target="_blank"
|
|
|
|
|
|
rel="noreferrer"
|
|
|
|
|
|
class="text-sm text-blue-600 hover:underline dark:text-blue-400"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ t('admin.accounts.gemini.setupGuide.links.geminiWebActivation') }}
|
|
|
|
|
|
</a>
|
|
|
|
|
|
<span class="text-gray-400">·</span>
|
|
|
|
|
|
<a
|
|
|
|
|
|
href="https://console.cloud.google.com"
|
|
|
|
|
|
target="_blank"
|
|
|
|
|
|
rel="noreferrer"
|
|
|
|
|
|
class="text-sm text-blue-600 hover:underline dark:text-blue-400"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ t('admin.accounts.gemini.setupGuide.links.gcpProject') }}
|
|
|
|
|
|
</a>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Quota Policy Section -->
|
|
|
|
|
|
<div class="border-t border-gray-200 pt-6 dark:border-dark-600">
|
|
|
|
|
|
<h3 class="mb-3 text-sm font-semibold text-gray-900 dark:text-white">
|
|
|
|
|
|
{{ t('admin.accounts.gemini.quotaPolicy.title') }}
|
|
|
|
|
|
</h3>
|
|
|
|
|
|
<p class="mb-4 text-xs text-amber-600 dark:text-amber-400">
|
|
|
|
|
|
{{ t('admin.accounts.gemini.quotaPolicy.note') }}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<div class="overflow-x-auto">
|
|
|
|
|
|
<table class="w-full text-xs">
|
|
|
|
|
|
<thead class="bg-gray-50 dark:bg-dark-600">
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<th class="px-3 py-2 text-left font-medium text-gray-700 dark:text-gray-300">
|
|
|
|
|
|
{{ t('admin.accounts.gemini.quotaPolicy.columns.channel') }}
|
|
|
|
|
|
</th>
|
|
|
|
|
|
<th class="px-3 py-2 text-left font-medium text-gray-700 dark:text-gray-300">
|
|
|
|
|
|
{{ t('admin.accounts.gemini.quotaPolicy.columns.account') }}
|
|
|
|
|
|
</th>
|
|
|
|
|
|
<th class="px-3 py-2 text-left font-medium text-gray-700 dark:text-gray-300">
|
|
|
|
|
|
{{ t('admin.accounts.gemini.quotaPolicy.columns.limits') }}
|
|
|
|
|
|
</th>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
</thead>
|
|
|
|
|
|
<tbody class="divide-y divide-gray-200 dark:divide-dark-600">
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<td class="px-3 py-2 text-gray-900 dark:text-white">
|
|
|
|
|
|
{{ t('admin.accounts.gemini.quotaPolicy.rows.googleOne.channel') }}
|
|
|
|
|
|
</td>
|
|
|
|
|
|
<td class="px-3 py-2 text-gray-600 dark:text-gray-400">Free</td>
|
|
|
|
|
|
<td class="px-3 py-2 text-gray-600 dark:text-gray-400">
|
|
|
|
|
|
{{ t('admin.accounts.gemini.quotaPolicy.rows.googleOne.limitsFree') }}
|
|
|
|
|
|
</td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<td class="px-3 py-2 text-gray-900 dark:text-white"></td>
|
|
|
|
|
|
<td class="px-3 py-2 text-gray-600 dark:text-gray-400">Pro</td>
|
|
|
|
|
|
<td class="px-3 py-2 text-gray-600 dark:text-gray-400">
|
|
|
|
|
|
{{ t('admin.accounts.gemini.quotaPolicy.rows.googleOne.limitsPro') }}
|
|
|
|
|
|
</td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<td class="px-3 py-2 text-gray-900 dark:text-white"></td>
|
|
|
|
|
|
<td class="px-3 py-2 text-gray-600 dark:text-gray-400">Ultra</td>
|
|
|
|
|
|
<td class="px-3 py-2 text-gray-600 dark:text-gray-400">
|
|
|
|
|
|
{{ t('admin.accounts.gemini.quotaPolicy.rows.googleOne.limitsUltra') }}
|
|
|
|
|
|
</td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<td class="px-3 py-2 text-gray-900 dark:text-white">
|
|
|
|
|
|
{{ t('admin.accounts.gemini.quotaPolicy.rows.gcp.channel') }}
|
|
|
|
|
|
</td>
|
|
|
|
|
|
<td class="px-3 py-2 text-gray-600 dark:text-gray-400">Standard</td>
|
|
|
|
|
|
<td class="px-3 py-2 text-gray-600 dark:text-gray-400">
|
|
|
|
|
|
{{ t('admin.accounts.gemini.quotaPolicy.rows.gcp.limitsStandard') }}
|
|
|
|
|
|
</td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<td class="px-3 py-2 text-gray-900 dark:text-white"></td>
|
|
|
|
|
|
<td class="px-3 py-2 text-gray-600 dark:text-gray-400">Enterprise</td>
|
|
|
|
|
|
<td class="px-3 py-2 text-gray-600 dark:text-gray-400">
|
|
|
|
|
|
{{ t('admin.accounts.gemini.quotaPolicy.rows.gcp.limitsEnterprise') }}
|
|
|
|
|
|
</td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<td class="px-3 py-2 text-gray-900 dark:text-white">
|
|
|
|
|
|
{{ t('admin.accounts.gemini.quotaPolicy.rows.aiStudio.channel') }}
|
|
|
|
|
|
</td>
|
|
|
|
|
|
<td class="px-3 py-2 text-gray-600 dark:text-gray-400">Free</td>
|
|
|
|
|
|
<td class="px-3 py-2 text-gray-600 dark:text-gray-400">
|
|
|
|
|
|
{{ t('admin.accounts.gemini.quotaPolicy.rows.aiStudio.limitsFree') }}
|
|
|
|
|
|
</td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<td class="px-3 py-2 text-gray-900 dark:text-white"></td>
|
|
|
|
|
|
<td class="px-3 py-2 text-gray-600 dark:text-gray-400">Paid</td>
|
|
|
|
|
|
<td class="px-3 py-2 text-gray-600 dark:text-gray-400">
|
|
|
|
|
|
{{ t('admin.accounts.gemini.quotaPolicy.rows.aiStudio.limitsPaid') }}
|
|
|
|
|
|
</td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
</tbody>
|
|
|
|
|
|
</table>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="mt-4 flex flex-wrap gap-3">
|
|
|
|
|
|
<a
|
|
|
|
|
|
:href="geminiQuotaDocs.codeAssist"
|
|
|
|
|
|
target="_blank"
|
|
|
|
|
|
rel="noreferrer"
|
|
|
|
|
|
class="text-sm text-blue-600 hover:underline dark:text-blue-400"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ t('admin.accounts.gemini.quotaPolicy.docs.codeAssist') }}
|
|
|
|
|
|
</a>
|
|
|
|
|
|
<a
|
|
|
|
|
|
:href="geminiQuotaDocs.aiStudio"
|
|
|
|
|
|
target="_blank"
|
|
|
|
|
|
rel="noreferrer"
|
|
|
|
|
|
class="text-sm text-blue-600 hover:underline dark:text-blue-400"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ t('admin.accounts.gemini.quotaPolicy.docs.aiStudio') }}
|
|
|
|
|
|
</a>
|
|
|
|
|
|
<a
|
|
|
|
|
|
:href="geminiQuotaDocs.vertex"
|
|
|
|
|
|
target="_blank"
|
|
|
|
|
|
rel="noreferrer"
|
|
|
|
|
|
class="text-sm text-blue-600 hover:underline dark:text-blue-400"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ t('admin.accounts.gemini.quotaPolicy.docs.vertex') }}
|
|
|
|
|
|
</a>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- API Key Links Section -->
|
|
|
|
|
|
<div class="border-t border-gray-200 pt-6 dark:border-dark-600">
|
|
|
|
|
|
<h3 class="mb-3 text-sm font-semibold text-gray-900 dark:text-white">
|
|
|
|
|
|
{{ t('admin.accounts.gemini.helpDialog.apiKeySection') }}
|
|
|
|
|
|
</h3>
|
|
|
|
|
|
<div class="flex flex-wrap gap-3">
|
|
|
|
|
|
<a
|
|
|
|
|
|
:href="geminiHelpLinks.apiKey"
|
|
|
|
|
|
target="_blank"
|
|
|
|
|
|
rel="noreferrer"
|
|
|
|
|
|
class="text-sm text-blue-600 hover:underline dark:text-blue-400"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ t('admin.accounts.gemini.accountType.apiKeyLink') }}
|
|
|
|
|
|
</a>
|
|
|
|
|
|
<a
|
|
|
|
|
|
:href="geminiHelpLinks.aiStudioPricing"
|
|
|
|
|
|
target="_blank"
|
|
|
|
|
|
rel="noreferrer"
|
|
|
|
|
|
class="text-sm text-blue-600 hover:underline dark:text-blue-400"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ t('admin.accounts.gemini.accountType.quotaLink') }}
|
|
|
|
|
|
</a>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<template #footer>
|
|
|
|
|
|
<div class="flex justify-end">
|
|
|
|
|
|
<button @click="showGeminiHelpDialog = false" type="button" class="btn btn-primary">
|
|
|
|
|
|
{{ t('common.close') }}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</BaseDialog>
|
2025-12-18 13:50:39 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
|
import { ref, reactive, computed, watch } from 'vue'
|
|
|
|
|
|
import { useI18n } from 'vue-i18n'
|
|
|
|
|
|
import { useAppStore } from '@/stores/app'
|
2026-01-01 16:03:48 +08:00
|
|
|
|
import { claudeModels, getPresetMappingsByPlatform, getModelsByPlatform, commonErrorCodes, buildModelMappingObject } from '@/composables/useModelWhitelist'
|
2025-12-29 03:17:25 +08:00
|
|
|
|
import { useAuthStore } from '@/stores/auth'
|
2025-12-18 13:50:39 +08:00
|
|
|
|
import { adminAPI } from '@/api/admin'
|
2025-12-25 08:40:05 -08:00
|
|
|
|
import {
|
|
|
|
|
|
useAccountOAuth,
|
|
|
|
|
|
type AddMethod,
|
|
|
|
|
|
type AuthInputMethod
|
|
|
|
|
|
} from '@/composables/useAccountOAuth'
|
2025-12-22 22:58:31 +08:00
|
|
|
|
import { useOpenAIOAuth } from '@/composables/useOpenAIOAuth'
|
2025-12-25 08:40:05 -08:00
|
|
|
|
import { useGeminiOAuth } from '@/composables/useGeminiOAuth'
|
2025-12-28 15:54:42 +08:00
|
|
|
|
import { useAntigravityOAuth } from '@/composables/useAntigravityOAuth'
|
2025-12-18 13:50:39 +08:00
|
|
|
|
import type { Proxy, Group, AccountPlatform, AccountType } from '@/types'
|
2025-12-28 01:00:06 +08:00
|
|
|
|
import BaseDialog from '@/components/common/BaseDialog.vue'
|
2025-12-18 13:50:39 +08:00
|
|
|
|
import ProxySelector from '@/components/common/ProxySelector.vue'
|
|
|
|
|
|
import GroupSelector from '@/components/common/GroupSelector.vue'
|
2026-01-01 16:03:48 +08:00
|
|
|
|
import ModelWhitelistSelector from '@/components/account/ModelWhitelistSelector.vue'
|
2025-12-18 13:50:39 +08:00
|
|
|
|
import OAuthAuthorizationFlow from './OAuthAuthorizationFlow.vue'
|
|
|
|
|
|
|
2025-12-18 14:26:55 +08:00
|
|
|
|
// Type for exposed OAuthAuthorizationFlow component
|
|
|
|
|
|
// Note: defineExpose automatically unwraps refs, so we use the unwrapped types
|
|
|
|
|
|
interface OAuthFlowExposed {
|
|
|
|
|
|
authCode: string
|
2025-12-25 08:40:05 -08:00
|
|
|
|
oauthState: string
|
2025-12-25 21:25:02 -08:00
|
|
|
|
projectId: string
|
2025-12-18 14:26:55 +08:00
|
|
|
|
sessionKey: string
|
|
|
|
|
|
inputMethod: AuthInputMethod
|
|
|
|
|
|
reset: () => void
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-18 13:50:39 +08:00
|
|
|
|
const { t } = useI18n()
|
2025-12-29 03:17:25 +08:00
|
|
|
|
const authStore = useAuthStore()
|
2025-12-18 13:50:39 +08:00
|
|
|
|
|
2025-12-25 08:40:05 -08:00
|
|
|
|
const oauthStepTitle = computed(() => {
|
|
|
|
|
|
if (form.platform === 'openai') return t('admin.accounts.oauth.openai.title')
|
|
|
|
|
|
if (form.platform === 'gemini') return t('admin.accounts.oauth.gemini.title')
|
2025-12-28 15:54:42 +08:00
|
|
|
|
if (form.platform === 'antigravity') return t('admin.accounts.oauth.antigravity.title')
|
2025-12-25 08:40:05 -08:00
|
|
|
|
return t('admin.accounts.oauth.title')
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2025-12-28 23:24:46 +08:00
|
|
|
|
// Platform-specific hints for API Key type
|
|
|
|
|
|
const baseUrlHint = computed(() => {
|
|
|
|
|
|
if (form.platform === 'openai') return t('admin.accounts.openai.baseUrlHint')
|
|
|
|
|
|
if (form.platform === 'gemini') return t('admin.accounts.gemini.baseUrlHint')
|
|
|
|
|
|
return t('admin.accounts.baseUrlHint')
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const apiKeyHint = computed(() => {
|
|
|
|
|
|
if (form.platform === 'openai') return t('admin.accounts.openai.apiKeyHint')
|
|
|
|
|
|
if (form.platform === 'gemini') return t('admin.accounts.gemini.apiKeyHint')
|
|
|
|
|
|
return t('admin.accounts.apiKeyHint')
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2025-12-18 13:50:39 +08:00
|
|
|
|
interface Props {
|
|
|
|
|
|
show: boolean
|
|
|
|
|
|
proxies: Proxy[]
|
|
|
|
|
|
groups: Group[]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const props = defineProps<Props>()
|
|
|
|
|
|
const emit = defineEmits<{
|
|
|
|
|
|
close: []
|
|
|
|
|
|
created: []
|
|
|
|
|
|
}>()
|
|
|
|
|
|
|
|
|
|
|
|
const appStore = useAppStore()
|
|
|
|
|
|
|
2025-12-22 22:58:31 +08:00
|
|
|
|
// OAuth composables
|
2025-12-25 08:40:05 -08:00
|
|
|
|
const oauth = useAccountOAuth() // For Anthropic OAuth
|
|
|
|
|
|
const openaiOAuth = useOpenAIOAuth() // For OpenAI OAuth
|
|
|
|
|
|
const geminiOAuth = useGeminiOAuth() // For Gemini OAuth
|
2025-12-28 15:54:42 +08:00
|
|
|
|
const antigravityOAuth = useAntigravityOAuth() // For Antigravity OAuth
|
2025-12-22 22:58:31 +08:00
|
|
|
|
|
|
|
|
|
|
// Computed: current OAuth state for template binding
|
|
|
|
|
|
const currentAuthUrl = computed(() => {
|
2025-12-25 08:40:05 -08:00
|
|
|
|
if (form.platform === 'openai') return openaiOAuth.authUrl.value
|
|
|
|
|
|
if (form.platform === 'gemini') return geminiOAuth.authUrl.value
|
2025-12-28 15:54:42 +08:00
|
|
|
|
if (form.platform === 'antigravity') return antigravityOAuth.authUrl.value
|
2025-12-25 08:40:05 -08:00
|
|
|
|
return oauth.authUrl.value
|
2025-12-22 22:58:31 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const currentSessionId = computed(() => {
|
2025-12-25 08:40:05 -08:00
|
|
|
|
if (form.platform === 'openai') return openaiOAuth.sessionId.value
|
|
|
|
|
|
if (form.platform === 'gemini') return geminiOAuth.sessionId.value
|
2025-12-28 15:54:42 +08:00
|
|
|
|
if (form.platform === 'antigravity') return antigravityOAuth.sessionId.value
|
2025-12-25 08:40:05 -08:00
|
|
|
|
return oauth.sessionId.value
|
2025-12-22 22:58:31 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const currentOAuthLoading = computed(() => {
|
2025-12-25 08:40:05 -08:00
|
|
|
|
if (form.platform === 'openai') return openaiOAuth.loading.value
|
|
|
|
|
|
if (form.platform === 'gemini') return geminiOAuth.loading.value
|
2025-12-28 15:54:42 +08:00
|
|
|
|
if (form.platform === 'antigravity') return antigravityOAuth.loading.value
|
2025-12-25 08:40:05 -08:00
|
|
|
|
return oauth.loading.value
|
2025-12-22 22:58:31 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const currentOAuthError = computed(() => {
|
2025-12-25 08:40:05 -08:00
|
|
|
|
if (form.platform === 'openai') return openaiOAuth.error.value
|
|
|
|
|
|
if (form.platform === 'gemini') return geminiOAuth.error.value
|
2025-12-28 15:54:42 +08:00
|
|
|
|
if (form.platform === 'antigravity') return antigravityOAuth.error.value
|
2025-12-25 08:40:05 -08:00
|
|
|
|
return oauth.error.value
|
2025-12-22 22:58:31 +08:00
|
|
|
|
})
|
2025-12-18 13:50:39 +08:00
|
|
|
|
|
|
|
|
|
|
// Refs
|
2025-12-18 14:26:55 +08:00
|
|
|
|
const oauthFlowRef = ref<OAuthFlowExposed | null>(null)
|
2025-12-18 13:50:39 +08:00
|
|
|
|
|
|
|
|
|
|
// Model mapping type
|
|
|
|
|
|
interface ModelMapping {
|
|
|
|
|
|
from: string
|
|
|
|
|
|
to: string
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-03 06:34:00 -08:00
|
|
|
|
interface TempUnschedRuleForm {
|
|
|
|
|
|
error_code: number | null
|
|
|
|
|
|
keywords: string
|
|
|
|
|
|
duration_minutes: number | null
|
|
|
|
|
|
description: string
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-18 13:50:39 +08:00
|
|
|
|
// State
|
|
|
|
|
|
const step = ref(1)
|
|
|
|
|
|
const submitting = ref(false)
|
|
|
|
|
|
const accountCategory = ref<'oauth-based' | 'apikey'>('oauth-based') // UI selection for account category
|
|
|
|
|
|
const addMethod = ref<AddMethod>('oauth') // For oauth-based: 'oauth' or 'setup-token'
|
|
|
|
|
|
const apiKeyBaseUrl = ref('https://api.anthropic.com')
|
|
|
|
|
|
const apiKeyValue = ref('')
|
|
|
|
|
|
const modelMappings = ref<ModelMapping[]>([])
|
|
|
|
|
|
const modelRestrictionMode = ref<'whitelist' | 'mapping'>('whitelist')
|
|
|
|
|
|
const allowedModels = ref<string[]>([])
|
|
|
|
|
|
const customErrorCodesEnabled = ref(false)
|
|
|
|
|
|
const selectedErrorCodes = ref<number[]>([])
|
|
|
|
|
|
const customErrorCodeInput = ref<number | null>(null)
|
2025-12-19 16:39:25 +08:00
|
|
|
|
const interceptWarmupRequests = ref(false)
|
2025-12-29 09:44:39 +08:00
|
|
|
|
const mixedScheduling = ref(false) // For antigravity accounts: enable mixed scheduling
|
2026-01-03 06:34:00 -08:00
|
|
|
|
const tempUnschedEnabled = ref(false)
|
|
|
|
|
|
const tempUnschedRules = ref<TempUnschedRuleForm[]>([])
|
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 geminiOAuthType = ref<'code_assist' | 'google_one' | 'ai_studio'>('google_one')
|
2025-12-25 23:52:55 -08:00
|
|
|
|
const geminiAIStudioOAuthEnabled = ref(false)
|
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 showAdvancedOAuth = ref(false)
|
2026-01-04 17:02:38 +08:00
|
|
|
|
const showGeminiHelpDialog = ref(false)
|
2025-12-25 08:40:05 -08:00
|
|
|
|
|
2026-01-04 15:36:00 +08:00
|
|
|
|
// Gemini tier selection (used as fallback when auto-detection is unavailable/fails)
|
|
|
|
|
|
const geminiTierGoogleOne = ref<'google_one_free' | 'google_ai_pro' | 'google_ai_ultra'>('google_one_free')
|
|
|
|
|
|
const geminiTierGcp = ref<'gcp_standard' | 'gcp_enterprise'>('gcp_standard')
|
|
|
|
|
|
const geminiTierAIStudio = ref<'aistudio_free' | 'aistudio_paid'>('aistudio_free')
|
|
|
|
|
|
|
|
|
|
|
|
const geminiSelectedTier = computed(() => {
|
|
|
|
|
|
if (form.platform !== 'gemini') return ''
|
|
|
|
|
|
if (accountCategory.value === 'apikey') return geminiTierAIStudio.value
|
|
|
|
|
|
switch (geminiOAuthType.value) {
|
|
|
|
|
|
case 'google_one':
|
|
|
|
|
|
return geminiTierGoogleOne.value
|
|
|
|
|
|
case 'code_assist':
|
|
|
|
|
|
return geminiTierGcp.value
|
|
|
|
|
|
default:
|
|
|
|
|
|
return geminiTierAIStudio.value
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-01-01 04:22:50 +08:00
|
|
|
|
const geminiQuotaDocs = {
|
|
|
|
|
|
codeAssist: 'https://developers.google.com/gemini-code-assist/resources/quotas',
|
|
|
|
|
|
aiStudio: 'https://ai.google.dev/pricing',
|
|
|
|
|
|
vertex: 'https://cloud.google.com/vertex-ai/generative-ai/docs/quotas'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const geminiHelpLinks = {
|
|
|
|
|
|
apiKey: 'https://aistudio.google.com/app/apikey',
|
|
|
|
|
|
aiStudioPricing: 'https://ai.google.dev/pricing',
|
|
|
|
|
|
gcpProject: 'https://console.cloud.google.com/welcome/new',
|
2026-01-04 18:26:39 -08:00
|
|
|
|
geminiWebActivation: 'https://gemini.google.com/gems/create?hl=en-US&pli=1',
|
|
|
|
|
|
countryCheck: 'https://policies.google.com/terms',
|
|
|
|
|
|
countryChange: 'https://policies.google.com/country-association-form'
|
2026-01-01 04:22:50 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-22 22:58:31 +08:00
|
|
|
|
// Computed: current preset mappings based on platform
|
2026-01-01 16:03:48 +08:00
|
|
|
|
const presetMappings = computed(() => getPresetMappingsByPlatform(form.platform))
|
2026-01-03 06:34:00 -08:00
|
|
|
|
const tempUnschedPresets = computed(() => [
|
|
|
|
|
|
{
|
|
|
|
|
|
label: t('admin.accounts.tempUnschedulable.presets.overloadLabel'),
|
|
|
|
|
|
rule: {
|
|
|
|
|
|
error_code: 529,
|
|
|
|
|
|
keywords: 'overloaded, too many',
|
|
|
|
|
|
duration_minutes: 60,
|
|
|
|
|
|
description: t('admin.accounts.tempUnschedulable.presets.overloadDesc')
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
label: t('admin.accounts.tempUnschedulable.presets.rateLimitLabel'),
|
|
|
|
|
|
rule: {
|
|
|
|
|
|
error_code: 429,
|
|
|
|
|
|
keywords: 'rate limit, too many requests',
|
|
|
|
|
|
duration_minutes: 10,
|
|
|
|
|
|
description: t('admin.accounts.tempUnschedulable.presets.rateLimitDesc')
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
label: t('admin.accounts.tempUnschedulable.presets.unavailableLabel'),
|
|
|
|
|
|
rule: {
|
|
|
|
|
|
error_code: 503,
|
|
|
|
|
|
keywords: 'unavailable, maintenance',
|
|
|
|
|
|
duration_minutes: 30,
|
|
|
|
|
|
description: t('admin.accounts.tempUnschedulable.presets.unavailableDesc')
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
])
|
2025-12-18 13:50:39 +08:00
|
|
|
|
|
|
|
|
|
|
const form = reactive({
|
|
|
|
|
|
name: '',
|
|
|
|
|
|
platform: 'anthropic' as AccountPlatform,
|
|
|
|
|
|
type: 'oauth' as AccountType, // Will be 'oauth', 'setup-token', or 'apikey'
|
|
|
|
|
|
credentials: {} as Record<string, unknown>,
|
|
|
|
|
|
proxy_id: null as number | null,
|
|
|
|
|
|
concurrency: 10,
|
|
|
|
|
|
priority: 1,
|
|
|
|
|
|
group_ids: [] as number[]
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// Helper to check if current type needs OAuth flow
|
|
|
|
|
|
const isOAuthFlow = computed(() => accountCategory.value === 'oauth-based')
|
|
|
|
|
|
|
2025-12-18 14:26:55 +08:00
|
|
|
|
const isManualInputMethod = computed(() => {
|
|
|
|
|
|
return oauthFlowRef.value?.inputMethod === 'manual'
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2025-12-18 13:50:39 +08:00
|
|
|
|
const canExchangeCode = computed(() => {
|
2025-12-18 14:26:55 +08:00
|
|
|
|
const authCode = oauthFlowRef.value?.authCode || ''
|
2025-12-22 22:58:31 +08:00
|
|
|
|
if (form.platform === 'openai') {
|
|
|
|
|
|
return authCode.trim() && openaiOAuth.sessionId.value && !openaiOAuth.loading.value
|
|
|
|
|
|
}
|
2025-12-25 08:40:05 -08:00
|
|
|
|
if (form.platform === 'gemini') {
|
2025-12-25 21:25:02 -08:00
|
|
|
|
return authCode.trim() && geminiOAuth.sessionId.value && !geminiOAuth.loading.value
|
2025-12-25 08:40:05 -08:00
|
|
|
|
}
|
2025-12-28 15:54:42 +08:00
|
|
|
|
if (form.platform === 'antigravity') {
|
|
|
|
|
|
return authCode.trim() && antigravityOAuth.sessionId.value && !antigravityOAuth.loading.value
|
|
|
|
|
|
}
|
2025-12-18 13:50:39 +08:00
|
|
|
|
return authCode.trim() && oauth.sessionId.value && !oauth.loading.value
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// Watchers
|
2025-12-25 08:40:05 -08:00
|
|
|
|
watch(
|
|
|
|
|
|
() => props.show,
|
|
|
|
|
|
(newVal) => {
|
2026-01-01 16:03:48 +08:00
|
|
|
|
if (newVal) {
|
|
|
|
|
|
// Modal opened - fill related models
|
|
|
|
|
|
allowedModels.value = [...getModelsByPlatform(form.platform)]
|
|
|
|
|
|
} else {
|
2025-12-25 08:40:05 -08:00
|
|
|
|
resetForm()
|
|
|
|
|
|
}
|
2025-12-18 13:50:39 +08:00
|
|
|
|
}
|
2025-12-25 08:40:05 -08:00
|
|
|
|
)
|
2025-12-18 13:50:39 +08:00
|
|
|
|
|
|
|
|
|
|
// Sync form.type based on accountCategory and addMethod
|
2025-12-25 08:40:05 -08:00
|
|
|
|
watch(
|
|
|
|
|
|
[accountCategory, addMethod],
|
|
|
|
|
|
([category, method]) => {
|
|
|
|
|
|
if (category === 'oauth-based') {
|
|
|
|
|
|
form.type = method as AccountType // 'oauth' or 'setup-token'
|
|
|
|
|
|
} else {
|
|
|
|
|
|
form.type = 'apikey'
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
{ immediate: true }
|
|
|
|
|
|
)
|
2025-12-18 13:50:39 +08:00
|
|
|
|
|
2025-12-22 22:58:31 +08:00
|
|
|
|
// Reset platform-specific settings when platform changes
|
2025-12-25 08:40:05 -08:00
|
|
|
|
watch(
|
|
|
|
|
|
() => form.platform,
|
|
|
|
|
|
(newPlatform) => {
|
|
|
|
|
|
// Reset base URL based on platform
|
|
|
|
|
|
apiKeyBaseUrl.value =
|
|
|
|
|
|
newPlatform === 'openai'
|
|
|
|
|
|
? 'https://api.openai.com'
|
|
|
|
|
|
: newPlatform === 'gemini'
|
|
|
|
|
|
? 'https://generativelanguage.googleapis.com'
|
|
|
|
|
|
: 'https://api.anthropic.com'
|
|
|
|
|
|
// Clear model-related settings
|
|
|
|
|
|
allowedModels.value = []
|
|
|
|
|
|
modelMappings.value = []
|
|
|
|
|
|
// Reset Anthropic-specific settings when switching to other platforms
|
|
|
|
|
|
if (newPlatform !== 'anthropic') {
|
|
|
|
|
|
interceptWarmupRequests.value = false
|
|
|
|
|
|
}
|
2025-12-28 15:54:42 +08:00
|
|
|
|
// Antigravity only supports OAuth
|
|
|
|
|
|
if (newPlatform === 'antigravity') {
|
|
|
|
|
|
accountCategory.value = 'oauth-based'
|
|
|
|
|
|
}
|
2025-12-25 08:40:05 -08:00
|
|
|
|
// Reset OAuth states
|
|
|
|
|
|
oauth.resetState()
|
|
|
|
|
|
openaiOAuth.resetState()
|
|
|
|
|
|
geminiOAuth.resetState()
|
2025-12-28 15:54:42 +08:00
|
|
|
|
antigravityOAuth.resetState()
|
2025-12-25 08:40:05 -08:00
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2025-12-25 23:52:55 -08:00
|
|
|
|
// Gemini AI Studio OAuth availability (requires operator-configured OAuth client)
|
|
|
|
|
|
watch(
|
|
|
|
|
|
[() => props.show, () => form.platform, accountCategory],
|
|
|
|
|
|
async ([show, platform, category]) => {
|
|
|
|
|
|
if (!show || platform !== 'gemini' || category !== 'oauth-based') {
|
|
|
|
|
|
geminiAIStudioOAuthEnabled.value = false
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
const caps = await geminiOAuth.getCapabilities()
|
|
|
|
|
|
geminiAIStudioOAuthEnabled.value = !!caps?.ai_studio_oauth_enabled
|
|
|
|
|
|
if (!geminiAIStudioOAuthEnabled.value && geminiOAuthType.value === 'ai_studio') {
|
|
|
|
|
|
geminiOAuthType.value = 'code_assist'
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
{ immediate: true }
|
|
|
|
|
|
)
|
|
|
|
|
|
|
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 handleSelectGeminiOAuthType = (oauthType: 'code_assist' | 'google_one' | 'ai_studio') => {
|
2025-12-25 23:52:55 -08:00
|
|
|
|
if (oauthType === 'ai_studio' && !geminiAIStudioOAuthEnabled.value) {
|
|
|
|
|
|
appStore.showError(t('admin.accounts.oauth.gemini.aiStudioNotConfigured'))
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
geminiOAuthType.value = oauthType
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-01 16:03:48 +08:00
|
|
|
|
// Auto-fill related models when switching to whitelist mode or changing platform
|
|
|
|
|
|
watch(
|
|
|
|
|
|
[modelRestrictionMode, () => form.platform],
|
|
|
|
|
|
([newMode]) => {
|
|
|
|
|
|
if (newMode === 'whitelist') {
|
|
|
|
|
|
allowedModels.value = [...getModelsByPlatform(form.platform)]
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2025-12-18 13:50:39 +08:00
|
|
|
|
// Model mapping helpers
|
|
|
|
|
|
const addModelMapping = () => {
|
|
|
|
|
|
modelMappings.value.push({ from: '', to: '' })
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const removeModelMapping = (index: number) => {
|
|
|
|
|
|
modelMappings.value.splice(index, 1)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const addPresetMapping = (from: string, to: string) => {
|
2026-01-01 16:03:48 +08:00
|
|
|
|
if (modelMappings.value.some((m) => m.from === from)) {
|
2025-12-18 13:50:39 +08:00
|
|
|
|
appStore.showInfo(t('admin.accounts.mappingExists', { model: from }))
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
modelMappings.value.push({ from, to })
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Error code toggle helper
|
|
|
|
|
|
const toggleErrorCode = (code: number) => {
|
|
|
|
|
|
const index = selectedErrorCodes.value.indexOf(code)
|
|
|
|
|
|
if (index === -1) {
|
|
|
|
|
|
selectedErrorCodes.value.push(code)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
selectedErrorCodes.value.splice(index, 1)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Add custom error code from input
|
|
|
|
|
|
const addCustomErrorCode = () => {
|
|
|
|
|
|
const code = customErrorCodeInput.value
|
|
|
|
|
|
if (code === null || code < 100 || code > 599) {
|
|
|
|
|
|
appStore.showError(t('admin.accounts.invalidErrorCode'))
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
if (selectedErrorCodes.value.includes(code)) {
|
|
|
|
|
|
appStore.showInfo(t('admin.accounts.errorCodeExists'))
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
selectedErrorCodes.value.push(code)
|
|
|
|
|
|
customErrorCodeInput.value = null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Remove error code
|
|
|
|
|
|
const removeErrorCode = (code: number) => {
|
|
|
|
|
|
const index = selectedErrorCodes.value.indexOf(code)
|
|
|
|
|
|
if (index !== -1) {
|
|
|
|
|
|
selectedErrorCodes.value.splice(index, 1)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-03 06:34:00 -08:00
|
|
|
|
const addTempUnschedRule = (preset?: TempUnschedRuleForm) => {
|
|
|
|
|
|
if (preset) {
|
|
|
|
|
|
tempUnschedRules.value.push({ ...preset })
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
tempUnschedRules.value.push({
|
|
|
|
|
|
error_code: null,
|
|
|
|
|
|
keywords: '',
|
|
|
|
|
|
duration_minutes: 30,
|
|
|
|
|
|
description: ''
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const removeTempUnschedRule = (index: number) => {
|
|
|
|
|
|
tempUnschedRules.value.splice(index, 1)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const moveTempUnschedRule = (index: number, direction: number) => {
|
|
|
|
|
|
const target = index + direction
|
|
|
|
|
|
if (target < 0 || target >= tempUnschedRules.value.length) return
|
|
|
|
|
|
const rules = tempUnschedRules.value
|
|
|
|
|
|
const current = rules[index]
|
|
|
|
|
|
rules[index] = rules[target]
|
|
|
|
|
|
rules[target] = current
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const buildTempUnschedRules = (rules: TempUnschedRuleForm[]) => {
|
|
|
|
|
|
const out: Array<{
|
|
|
|
|
|
error_code: number
|
|
|
|
|
|
keywords: string[]
|
|
|
|
|
|
duration_minutes: number
|
|
|
|
|
|
description: string
|
|
|
|
|
|
}> = []
|
|
|
|
|
|
|
|
|
|
|
|
for (const rule of rules) {
|
|
|
|
|
|
const errorCode = Number(rule.error_code)
|
|
|
|
|
|
const duration = Number(rule.duration_minutes)
|
|
|
|
|
|
const keywords = splitTempUnschedKeywords(rule.keywords)
|
|
|
|
|
|
if (!Number.isFinite(errorCode) || errorCode < 100 || errorCode > 599) {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!Number.isFinite(duration) || duration <= 0) {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
if (keywords.length === 0) {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
out.push({
|
|
|
|
|
|
error_code: Math.trunc(errorCode),
|
|
|
|
|
|
keywords,
|
|
|
|
|
|
duration_minutes: Math.trunc(duration),
|
|
|
|
|
|
description: rule.description.trim()
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return out
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const applyTempUnschedConfig = (credentials: Record<string, unknown>) => {
|
|
|
|
|
|
if (!tempUnschedEnabled.value) {
|
|
|
|
|
|
delete credentials.temp_unschedulable_enabled
|
|
|
|
|
|
delete credentials.temp_unschedulable_rules
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const rules = buildTempUnschedRules(tempUnschedRules.value)
|
|
|
|
|
|
if (rules.length === 0) {
|
|
|
|
|
|
appStore.showError(t('admin.accounts.tempUnschedulable.rulesInvalid'))
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
credentials.temp_unschedulable_enabled = true
|
|
|
|
|
|
credentials.temp_unschedulable_rules = rules
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const splitTempUnschedKeywords = (value: string) => {
|
|
|
|
|
|
return value
|
|
|
|
|
|
.split(/[,;]/)
|
|
|
|
|
|
.map((item) => item.trim())
|
|
|
|
|
|
.filter((item) => item.length > 0)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-18 13:50:39 +08:00
|
|
|
|
// Methods
|
|
|
|
|
|
const resetForm = () => {
|
|
|
|
|
|
step.value = 1
|
|
|
|
|
|
form.name = ''
|
|
|
|
|
|
form.platform = 'anthropic'
|
|
|
|
|
|
form.type = 'oauth'
|
|
|
|
|
|
form.credentials = {}
|
|
|
|
|
|
form.proxy_id = null
|
|
|
|
|
|
form.concurrency = 10
|
|
|
|
|
|
form.priority = 1
|
|
|
|
|
|
form.group_ids = []
|
|
|
|
|
|
accountCategory.value = 'oauth-based'
|
|
|
|
|
|
addMethod.value = 'oauth'
|
|
|
|
|
|
apiKeyBaseUrl.value = 'https://api.anthropic.com'
|
|
|
|
|
|
apiKeyValue.value = ''
|
|
|
|
|
|
modelMappings.value = []
|
|
|
|
|
|
modelRestrictionMode.value = 'whitelist'
|
2026-01-01 16:03:48 +08:00
|
|
|
|
allowedModels.value = [...claudeModels] // Default fill related models
|
2025-12-18 13:50:39 +08:00
|
|
|
|
customErrorCodesEnabled.value = false
|
|
|
|
|
|
selectedErrorCodes.value = []
|
|
|
|
|
|
customErrorCodeInput.value = null
|
2025-12-19 16:39:25 +08:00
|
|
|
|
interceptWarmupRequests.value = false
|
2026-01-03 06:34:00 -08:00
|
|
|
|
tempUnschedEnabled.value = false
|
|
|
|
|
|
tempUnschedRules.value = []
|
2025-12-25 21:25:02 -08:00
|
|
|
|
geminiOAuthType.value = 'code_assist'
|
2026-01-04 15:36:00 +08:00
|
|
|
|
geminiTierGoogleOne.value = 'google_one_free'
|
|
|
|
|
|
geminiTierGcp.value = 'gcp_standard'
|
|
|
|
|
|
geminiTierAIStudio.value = 'aistudio_free'
|
2025-12-18 13:50:39 +08:00
|
|
|
|
oauth.resetState()
|
2025-12-22 22:58:31 +08:00
|
|
|
|
openaiOAuth.resetState()
|
2025-12-25 08:40:05 -08:00
|
|
|
|
geminiOAuth.resetState()
|
2025-12-28 15:54:42 +08:00
|
|
|
|
antigravityOAuth.resetState()
|
2025-12-18 13:50:39 +08:00
|
|
|
|
oauthFlowRef.value?.reset()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleClose = () => {
|
|
|
|
|
|
emit('close')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleSubmit = async () => {
|
|
|
|
|
|
// For OAuth-based type, handle OAuth flow (goes to step 2)
|
|
|
|
|
|
if (isOAuthFlow.value) {
|
|
|
|
|
|
if (!form.name.trim()) {
|
|
|
|
|
|
appStore.showError(t('admin.accounts.pleaseEnterAccountName'))
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
step.value = 2
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// For apikey type, create directly
|
|
|
|
|
|
if (!apiKeyValue.value.trim()) {
|
|
|
|
|
|
appStore.showError(t('admin.accounts.pleaseEnterApiKey'))
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-22 22:58:31 +08:00
|
|
|
|
// Determine default base URL based on platform
|
2025-12-25 08:40:05 -08:00
|
|
|
|
const defaultBaseUrl =
|
|
|
|
|
|
form.platform === 'openai'
|
|
|
|
|
|
? 'https://api.openai.com'
|
|
|
|
|
|
: form.platform === 'gemini'
|
|
|
|
|
|
? 'https://generativelanguage.googleapis.com'
|
|
|
|
|
|
: 'https://api.anthropic.com'
|
2025-12-22 22:58:31 +08:00
|
|
|
|
|
2025-12-18 13:50:39 +08:00
|
|
|
|
// Build credentials with optional model mapping
|
|
|
|
|
|
const credentials: Record<string, unknown> = {
|
2025-12-22 22:58:31 +08:00
|
|
|
|
base_url: apiKeyBaseUrl.value.trim() || defaultBaseUrl,
|
2025-12-18 13:50:39 +08:00
|
|
|
|
api_key: apiKeyValue.value.trim()
|
|
|
|
|
|
}
|
2026-01-04 15:36:00 +08:00
|
|
|
|
if (form.platform === 'gemini') {
|
|
|
|
|
|
credentials.tier_id = geminiTierAIStudio.value
|
|
|
|
|
|
}
|
2025-12-18 13:50:39 +08:00
|
|
|
|
|
|
|
|
|
|
// Add model mapping if configured
|
2026-01-01 16:03:48 +08:00
|
|
|
|
const modelMapping = buildModelMappingObject(modelRestrictionMode.value, allowedModels.value, modelMappings.value)
|
2025-12-18 13:50:39 +08:00
|
|
|
|
if (modelMapping) {
|
|
|
|
|
|
credentials.model_mapping = modelMapping
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Add custom error codes if enabled
|
|
|
|
|
|
if (customErrorCodesEnabled.value) {
|
|
|
|
|
|
credentials.custom_error_codes_enabled = true
|
|
|
|
|
|
credentials.custom_error_codes = [...selectedErrorCodes.value]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-19 16:39:25 +08:00
|
|
|
|
// Add intercept warmup requests setting
|
|
|
|
|
|
if (interceptWarmupRequests.value) {
|
|
|
|
|
|
credentials.intercept_warmup_requests = true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-03 06:34:00 -08:00
|
|
|
|
if (!applyTempUnschedConfig(credentials)) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-18 13:50:39 +08:00
|
|
|
|
form.credentials = credentials
|
|
|
|
|
|
|
|
|
|
|
|
submitting.value = true
|
|
|
|
|
|
try {
|
2025-12-22 22:58:31 +08:00
|
|
|
|
await adminAPI.accounts.create({
|
|
|
|
|
|
...form,
|
|
|
|
|
|
group_ids: form.group_ids
|
|
|
|
|
|
})
|
2025-12-18 13:50:39 +08:00
|
|
|
|
appStore.showSuccess(t('admin.accounts.accountCreated'))
|
|
|
|
|
|
emit('created')
|
|
|
|
|
|
handleClose()
|
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
|
appStore.showError(error.response?.data?.detail || t('admin.accounts.failedToCreate'))
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
submitting.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const goBackToBasicInfo = () => {
|
|
|
|
|
|
step.value = 1
|
|
|
|
|
|
oauth.resetState()
|
2025-12-22 22:58:31 +08:00
|
|
|
|
openaiOAuth.resetState()
|
2025-12-25 08:40:05 -08:00
|
|
|
|
geminiOAuth.resetState()
|
2025-12-28 15:54:42 +08:00
|
|
|
|
antigravityOAuth.resetState()
|
2025-12-18 13:50:39 +08:00
|
|
|
|
oauthFlowRef.value?.reset()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleGenerateUrl = async () => {
|
2025-12-22 22:58:31 +08:00
|
|
|
|
if (form.platform === 'openai') {
|
|
|
|
|
|
await openaiOAuth.generateAuthUrl(form.proxy_id)
|
2025-12-25 08:40:05 -08:00
|
|
|
|
} else if (form.platform === 'gemini') {
|
2026-01-04 15:36:00 +08:00
|
|
|
|
await geminiOAuth.generateAuthUrl(
|
|
|
|
|
|
form.proxy_id,
|
|
|
|
|
|
oauthFlowRef.value?.projectId,
|
|
|
|
|
|
geminiOAuthType.value,
|
|
|
|
|
|
geminiSelectedTier.value
|
|
|
|
|
|
)
|
2025-12-28 15:54:42 +08:00
|
|
|
|
} else if (form.platform === 'antigravity') {
|
|
|
|
|
|
await antigravityOAuth.generateAuthUrl(form.proxy_id)
|
2025-12-22 22:58:31 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
await oauth.generateAuthUrl(addMethod.value, form.proxy_id)
|
|
|
|
|
|
}
|
2025-12-18 13:50:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-28 15:54:42 +08:00
|
|
|
|
// Create account and handle success/failure
|
|
|
|
|
|
const createAccountAndFinish = async (
|
|
|
|
|
|
platform: AccountPlatform,
|
|
|
|
|
|
type: AccountType,
|
|
|
|
|
|
credentials: Record<string, unknown>,
|
2025-12-29 09:44:39 +08:00
|
|
|
|
extra?: Record<string, unknown>
|
2025-12-28 15:54:42 +08:00
|
|
|
|
) => {
|
2026-01-03 06:34:00 -08:00
|
|
|
|
if (!applyTempUnschedConfig(credentials)) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2025-12-28 15:54:42 +08:00
|
|
|
|
await adminAPI.accounts.create({
|
|
|
|
|
|
name: form.name,
|
|
|
|
|
|
platform,
|
|
|
|
|
|
type,
|
|
|
|
|
|
credentials,
|
|
|
|
|
|
extra,
|
|
|
|
|
|
proxy_id: form.proxy_id,
|
|
|
|
|
|
concurrency: form.concurrency,
|
|
|
|
|
|
priority: form.priority,
|
|
|
|
|
|
group_ids: form.group_ids
|
|
|
|
|
|
})
|
|
|
|
|
|
appStore.showSuccess(t('admin.accounts.accountCreated'))
|
|
|
|
|
|
emit('created')
|
|
|
|
|
|
handleClose()
|
|
|
|
|
|
}
|
2025-12-22 22:58:31 +08:00
|
|
|
|
|
2025-12-28 15:54:42 +08:00
|
|
|
|
// OpenAI OAuth 授权码兑换
|
|
|
|
|
|
const handleOpenAIExchange = async (authCode: string) => {
|
|
|
|
|
|
if (!authCode.trim() || !openaiOAuth.sessionId.value) return
|
2025-12-22 22:58:31 +08:00
|
|
|
|
|
2025-12-28 15:54:42 +08:00
|
|
|
|
openaiOAuth.loading.value = true
|
|
|
|
|
|
openaiOAuth.error.value = ''
|
2025-12-22 22:58:31 +08:00
|
|
|
|
|
2025-12-28 15:54:42 +08:00
|
|
|
|
try {
|
|
|
|
|
|
const tokenInfo = await openaiOAuth.exchangeAuthCode(
|
|
|
|
|
|
authCode.trim(),
|
|
|
|
|
|
openaiOAuth.sessionId.value,
|
|
|
|
|
|
form.proxy_id
|
|
|
|
|
|
)
|
|
|
|
|
|
if (!tokenInfo) return
|
|
|
|
|
|
|
|
|
|
|
|
const credentials = openaiOAuth.buildCredentials(tokenInfo)
|
|
|
|
|
|
const extra = openaiOAuth.buildExtraInfo(tokenInfo)
|
|
|
|
|
|
await createAccountAndFinish('openai', 'oauth', credentials, extra)
|
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
|
openaiOAuth.error.value = error.response?.data?.detail || t('admin.accounts.oauth.authFailed')
|
|
|
|
|
|
appStore.showError(openaiOAuth.error.value)
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
openaiOAuth.loading.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-12-22 22:58:31 +08:00
|
|
|
|
|
2025-12-28 15:54:42 +08:00
|
|
|
|
// Gemini OAuth 授权码兑换
|
|
|
|
|
|
const handleGeminiExchange = async (authCode: string) => {
|
|
|
|
|
|
if (!authCode.trim() || !geminiOAuth.sessionId.value) return
|
2025-12-22 22:58:31 +08:00
|
|
|
|
|
2025-12-28 15:54:42 +08:00
|
|
|
|
geminiOAuth.loading.value = true
|
|
|
|
|
|
geminiOAuth.error.value = ''
|
2025-12-22 22:58:31 +08:00
|
|
|
|
|
2025-12-28 15:54:42 +08:00
|
|
|
|
try {
|
|
|
|
|
|
const stateFromInput = oauthFlowRef.value?.oauthState || ''
|
|
|
|
|
|
const stateToUse = stateFromInput || geminiOAuth.state.value
|
|
|
|
|
|
if (!stateToUse) {
|
|
|
|
|
|
geminiOAuth.error.value = t('admin.accounts.oauth.authFailed')
|
|
|
|
|
|
appStore.showError(geminiOAuth.error.value)
|
|
|
|
|
|
return
|
2025-12-22 22:58:31 +08:00
|
|
|
|
}
|
2025-12-28 15:54:42 +08:00
|
|
|
|
|
|
|
|
|
|
const tokenInfo = await geminiOAuth.exchangeAuthCode({
|
|
|
|
|
|
code: authCode.trim(),
|
|
|
|
|
|
sessionId: geminiOAuth.sessionId.value,
|
|
|
|
|
|
state: stateToUse,
|
|
|
|
|
|
proxyId: form.proxy_id,
|
2026-01-04 15:36:00 +08:00
|
|
|
|
oauthType: geminiOAuthType.value,
|
|
|
|
|
|
tierId: geminiSelectedTier.value
|
2025-12-28 15:54:42 +08:00
|
|
|
|
})
|
|
|
|
|
|
if (!tokenInfo) return
|
|
|
|
|
|
|
|
|
|
|
|
const credentials = geminiOAuth.buildCredentials(tokenInfo)
|
2026-01-03 06:34:00 -08:00
|
|
|
|
const extra = geminiOAuth.buildExtraInfo(tokenInfo)
|
|
|
|
|
|
await createAccountAndFinish('gemini', 'oauth', credentials, extra)
|
2025-12-28 15:54:42 +08:00
|
|
|
|
} catch (error: any) {
|
|
|
|
|
|
geminiOAuth.error.value = error.response?.data?.detail || t('admin.accounts.oauth.authFailed')
|
|
|
|
|
|
appStore.showError(geminiOAuth.error.value)
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
geminiOAuth.loading.value = false
|
2025-12-22 22:58:31 +08:00
|
|
|
|
}
|
2025-12-28 15:54:42 +08:00
|
|
|
|
}
|
2025-12-22 22:58:31 +08:00
|
|
|
|
|
2025-12-28 15:54:42 +08:00
|
|
|
|
// Antigravity OAuth 授权码兑换
|
|
|
|
|
|
const handleAntigravityExchange = async (authCode: string) => {
|
|
|
|
|
|
if (!authCode.trim() || !antigravityOAuth.sessionId.value) return
|
2025-12-25 08:40:05 -08:00
|
|
|
|
|
2025-12-28 15:54:42 +08:00
|
|
|
|
antigravityOAuth.loading.value = true
|
|
|
|
|
|
antigravityOAuth.error.value = ''
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const stateFromInput = oauthFlowRef.value?.oauthState || ''
|
|
|
|
|
|
const stateToUse = stateFromInput || antigravityOAuth.state.value
|
|
|
|
|
|
if (!stateToUse) {
|
|
|
|
|
|
antigravityOAuth.error.value = t('admin.accounts.oauth.authFailed')
|
|
|
|
|
|
appStore.showError(antigravityOAuth.error.value)
|
|
|
|
|
|
return
|
2025-12-25 08:40:05 -08:00
|
|
|
|
}
|
2025-12-28 15:54:42 +08:00
|
|
|
|
|
|
|
|
|
|
const tokenInfo = await antigravityOAuth.exchangeAuthCode({
|
|
|
|
|
|
code: authCode.trim(),
|
|
|
|
|
|
sessionId: antigravityOAuth.sessionId.value,
|
|
|
|
|
|
state: stateToUse,
|
|
|
|
|
|
proxyId: form.proxy_id
|
|
|
|
|
|
})
|
|
|
|
|
|
if (!tokenInfo) return
|
|
|
|
|
|
|
|
|
|
|
|
const credentials = antigravityOAuth.buildCredentials(tokenInfo)
|
2025-12-29 09:44:39 +08:00
|
|
|
|
const extra = mixedScheduling.value ? { mixed_scheduling: true } : undefined
|
|
|
|
|
|
await createAccountAndFinish('antigravity', 'oauth', credentials, extra)
|
2025-12-28 15:54:42 +08:00
|
|
|
|
} catch (error: any) {
|
|
|
|
|
|
antigravityOAuth.error.value = error.response?.data?.detail || t('admin.accounts.oauth.authFailed')
|
|
|
|
|
|
appStore.showError(antigravityOAuth.error.value)
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
antigravityOAuth.loading.value = false
|
2025-12-25 08:40:05 -08:00
|
|
|
|
}
|
2025-12-28 15:54:42 +08:00
|
|
|
|
}
|
2025-12-25 08:40:05 -08:00
|
|
|
|
|
2025-12-28 15:54:42 +08:00
|
|
|
|
// Anthropic OAuth 授权码兑换
|
|
|
|
|
|
const handleAnthropicExchange = async (authCode: string) => {
|
2025-12-18 13:50:39 +08:00
|
|
|
|
if (!authCode.trim() || !oauth.sessionId.value) return
|
|
|
|
|
|
|
|
|
|
|
|
oauth.loading.value = true
|
|
|
|
|
|
oauth.error.value = ''
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const proxyConfig = form.proxy_id ? { proxy_id: form.proxy_id } : {}
|
2025-12-25 08:40:05 -08:00
|
|
|
|
const endpoint =
|
|
|
|
|
|
addMethod.value === 'oauth'
|
|
|
|
|
|
? '/admin/accounts/exchange-code'
|
|
|
|
|
|
: '/admin/accounts/exchange-setup-token-code'
|
2025-12-18 13:50:39 +08:00
|
|
|
|
|
|
|
|
|
|
const tokenInfo = await adminAPI.accounts.exchangeCode(endpoint, {
|
|
|
|
|
|
session_id: oauth.sessionId.value,
|
|
|
|
|
|
code: authCode.trim(),
|
|
|
|
|
|
...proxyConfig
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const extra = oauth.buildExtraInfo(tokenInfo)
|
2025-12-19 16:39:25 +08:00
|
|
|
|
const credentials = {
|
|
|
|
|
|
...tokenInfo,
|
|
|
|
|
|
...(interceptWarmupRequests.value ? { intercept_warmup_requests: true } : {})
|
|
|
|
|
|
}
|
2025-12-28 15:54:42 +08:00
|
|
|
|
await createAccountAndFinish(form.platform, addMethod.value as AccountType, credentials, extra)
|
2025-12-18 13:50:39 +08:00
|
|
|
|
} catch (error: any) {
|
|
|
|
|
|
oauth.error.value = error.response?.data?.detail || t('admin.accounts.oauth.authFailed')
|
|
|
|
|
|
appStore.showError(oauth.error.value)
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
oauth.loading.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-28 15:54:42 +08:00
|
|
|
|
// 主入口:根据平台路由到对应处理函数
|
|
|
|
|
|
const handleExchangeCode = async () => {
|
|
|
|
|
|
const authCode = oauthFlowRef.value?.authCode || ''
|
|
|
|
|
|
|
|
|
|
|
|
switch (form.platform) {
|
|
|
|
|
|
case 'openai':
|
|
|
|
|
|
return handleOpenAIExchange(authCode)
|
|
|
|
|
|
case 'gemini':
|
|
|
|
|
|
return handleGeminiExchange(authCode)
|
|
|
|
|
|
case 'antigravity':
|
|
|
|
|
|
return handleAntigravityExchange(authCode)
|
|
|
|
|
|
default:
|
|
|
|
|
|
return handleAnthropicExchange(authCode)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-18 13:50:39 +08:00
|
|
|
|
const handleCookieAuth = async (sessionKey: string) => {
|
|
|
|
|
|
oauth.loading.value = true
|
|
|
|
|
|
oauth.error.value = ''
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const proxyConfig = form.proxy_id ? { proxy_id: form.proxy_id } : {}
|
|
|
|
|
|
const keys = oauth.parseSessionKeys(sessionKey)
|
|
|
|
|
|
|
|
|
|
|
|
if (keys.length === 0) {
|
|
|
|
|
|
oauth.error.value = t('admin.accounts.oauth.pleaseEnterSessionKey')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-03 06:34:00 -08:00
|
|
|
|
const tempUnschedPayload = tempUnschedEnabled.value
|
|
|
|
|
|
? buildTempUnschedRules(tempUnschedRules.value)
|
|
|
|
|
|
: []
|
|
|
|
|
|
if (tempUnschedEnabled.value && tempUnschedPayload.length === 0) {
|
|
|
|
|
|
appStore.showError(t('admin.accounts.tempUnschedulable.rulesInvalid'))
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-25 08:40:05 -08:00
|
|
|
|
const endpoint =
|
|
|
|
|
|
addMethod.value === 'oauth'
|
|
|
|
|
|
? '/admin/accounts/cookie-auth'
|
|
|
|
|
|
: '/admin/accounts/setup-token-cookie-auth'
|
2025-12-18 13:50:39 +08:00
|
|
|
|
|
|
|
|
|
|
let successCount = 0
|
|
|
|
|
|
let failedCount = 0
|
|
|
|
|
|
const errors: string[] = []
|
|
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < keys.length; i++) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const tokenInfo = await adminAPI.accounts.exchangeCode(endpoint, {
|
|
|
|
|
|
session_id: '',
|
|
|
|
|
|
code: keys[i],
|
|
|
|
|
|
...proxyConfig
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const extra = oauth.buildExtraInfo(tokenInfo)
|
|
|
|
|
|
const accountName = keys.length > 1 ? `${form.name} #${i + 1}` : form.name
|
|
|
|
|
|
|
2025-12-19 16:39:25 +08:00
|
|
|
|
// Merge interceptWarmupRequests into credentials
|
2026-01-03 17:10:37 -08:00
|
|
|
|
const credentials: Record<string, unknown> = {
|
2025-12-19 16:39:25 +08:00
|
|
|
|
...tokenInfo,
|
|
|
|
|
|
...(interceptWarmupRequests.value ? { intercept_warmup_requests: true } : {})
|
|
|
|
|
|
}
|
2026-01-03 06:34:00 -08:00
|
|
|
|
if (tempUnschedEnabled.value) {
|
|
|
|
|
|
credentials.temp_unschedulable_enabled = true
|
|
|
|
|
|
credentials.temp_unschedulable_rules = tempUnschedPayload
|
|
|
|
|
|
}
|
2025-12-19 16:39:25 +08:00
|
|
|
|
|
2025-12-18 13:50:39 +08:00
|
|
|
|
await adminAPI.accounts.create({
|
|
|
|
|
|
name: accountName,
|
|
|
|
|
|
platform: form.platform,
|
|
|
|
|
|
type: addMethod.value, // Use addMethod as type: 'oauth' or 'setup-token'
|
2025-12-19 16:39:25 +08:00
|
|
|
|
credentials,
|
2025-12-18 13:50:39 +08:00
|
|
|
|
extra,
|
|
|
|
|
|
proxy_id: form.proxy_id,
|
|
|
|
|
|
concurrency: form.concurrency,
|
|
|
|
|
|
priority: form.priority
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
successCount++
|
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
|
failedCount++
|
2025-12-25 08:40:05 -08:00
|
|
|
|
errors.push(
|
|
|
|
|
|
t('admin.accounts.oauth.keyAuthFailed', {
|
|
|
|
|
|
index: i + 1,
|
|
|
|
|
|
error: error.response?.data?.detail || t('admin.accounts.oauth.authFailed')
|
|
|
|
|
|
})
|
|
|
|
|
|
)
|
2025-12-18 13:50:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (successCount > 0) {
|
|
|
|
|
|
appStore.showSuccess(t('admin.accounts.oauth.successCreated', { count: successCount }))
|
|
|
|
|
|
if (failedCount === 0) {
|
|
|
|
|
|
emit('created')
|
|
|
|
|
|
handleClose()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
emit('created')
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (failedCount > 0) {
|
|
|
|
|
|
oauth.error.value = errors.join('\n')
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
|
oauth.error.value = error.response?.data?.detail || t('admin.accounts.oauth.cookieAuthFailed')
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
oauth.loading.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</script>
|