feat(frontend): UI 显示 AI Studio OAuth 配置状态

CreateAccountModal:
- 检测 AI Studio OAuth 是否可用(服务器配置了自定义 OAuth 客户端)
- 未配置时禁用 AI Studio 选项并显示提示
- 添加悬停提示说明配置要求

ReAuthAccountModal:
- 同步 AI Studio OAuth 可用性检测逻辑
- 未配置时自动回退到 Code Assist
This commit is contained in:
ianshaw
2025-12-25 23:52:55 -08:00
parent 1bec35999b
commit f9f33e7b5c
2 changed files with 115 additions and 26 deletions

View File

@@ -387,7 +387,7 @@
<div class="mt-2 grid grid-cols-2 gap-3"> <div class="mt-2 grid grid-cols-2 gap-3">
<button <button
type="button" type="button"
@click="geminiOAuthType = 'code_assist'" @click="handleSelectGeminiOAuthType('code_assist')"
:class="[ :class="[
'flex items-center gap-3 rounded-lg border-2 p-3 text-left transition-all', 'flex items-center gap-3 rounded-lg border-2 p-3 text-left transition-all',
geminiOAuthType === 'code_assist' geminiOAuthType === 'code_assist'
@@ -414,34 +414,65 @@
</div> </div>
</button> </button>
<button <div class="group relative">
type="button" <button
@click="geminiOAuthType = 'ai_studio'" type="button"
:class="[ :disabled="!geminiAIStudioOAuthEnabled"
'flex items-center gap-3 rounded-lg border-2 p-3 text-left transition-all', @click="handleSelectGeminiOAuthType('ai_studio')"
geminiOAuthType === 'ai_studio'
? 'border-purple-500 bg-purple-50 dark:bg-purple-900/20'
: 'border-gray-200 hover:border-purple-300 dark:border-dark-600 dark:hover:border-purple-700'
]"
>
<div
:class="[ :class="[
'flex h-8 w-8 items-center justify-center rounded-lg', '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' geminiOAuthType === 'ai_studio'
? 'bg-purple-500 text-white' ? 'border-purple-500 bg-purple-50 dark:bg-purple-900/20'
: 'bg-gray-100 text-gray-500 dark:bg-dark-600 dark:text-gray-400' : 'border-gray-200 hover:border-purple-300 dark:border-dark-600 dark:hover:border-purple-700'
]" ]"
> >
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"> <div
<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" /> :class="[
</svg> 'flex h-8 w-8 items-center justify-center rounded-lg',
geminiOAuthType === 'ai_studio'
? 'bg-purple-500 text-white'
: 'bg-gray-100 text-gray-500 dark:bg-dark-600 dark:text-gray-400'
]"
>
<svg
class="h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="1.5"
>
<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">AI Studio</span>
<span class="block text-xs font-medium text-purple-600 dark:text-purple-400">{{
t('admin.accounts.oauth.gemini.noProjectIdNeeded')
}}</span>
<span class="text-xs text-gray-500 dark:text-gray-400">{{
t('admin.accounts.oauth.gemini.noProjectIdNeededDesc')
}}</span>
</div>
<span
v-if="!geminiAIStudioOAuthEnabled"
class="ml-auto shrink-0 rounded bg-amber-100 px-2 py-0.5 text-xs text-amber-700 dark:bg-amber-900/30 dark:text-amber-300"
>
{{ t('admin.accounts.oauth.gemini.aiStudioNotConfiguredShort') }}
</span>
</button>
<div
v-if="!geminiAIStudioOAuthEnabled"
class="pointer-events-none absolute left-0 top-full z-10 mt-2 w-[28rem] rounded-md border border-amber-200 bg-amber-50 px-3 py-2 text-xs text-amber-800 opacity-0 shadow-sm transition-opacity group-hover:opacity-100 dark:border-amber-700 dark:bg-amber-900/40 dark:text-amber-200"
>
{{ t('admin.accounts.oauth.gemini.aiStudioNotConfiguredTip') }}
</div> </div>
<div> </div>
<span class="block text-sm font-medium text-gray-900 dark:text-white">AI Studio</span>
<span class="block text-xs font-medium text-purple-600 dark:text-purple-400">{{ t('admin.accounts.oauth.gemini.noProjectIdNeeded') }}</span>
<span class="text-xs text-gray-500 dark:text-gray-400">{{ t('admin.accounts.oauth.gemini.noProjectIdNeededDesc') }}</span>
</div>
</button>
</div> </div>
</div> </div>
</div> </div>
@@ -1134,6 +1165,7 @@ const selectedErrorCodes = ref<number[]>([])
const customErrorCodeInput = ref<number | null>(null) const customErrorCodeInput = ref<number | null>(null)
const interceptWarmupRequests = ref(false) const interceptWarmupRequests = ref(false)
const geminiOAuthType = ref<'code_assist' | 'ai_studio'>('code_assist') const geminiOAuthType = ref<'code_assist' | 'ai_studio'>('code_assist')
const geminiAIStudioOAuthEnabled = ref(false)
// Common models for whitelist - Anthropic // Common models for whitelist - Anthropic
const anthropicModels = [ const anthropicModels = [
@@ -1385,6 +1417,31 @@ watch(
} }
) )
// 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 }
)
const handleSelectGeminiOAuthType = (oauthType: 'code_assist' | 'ai_studio') => {
if (oauthType === 'ai_studio' && !geminiAIStudioOAuthEnabled.value) {
appStore.showError(t('admin.accounts.oauth.gemini.aiStudioNotConfigured'))
return
}
geminiOAuthType.value = oauthType
}
// Model mapping helpers // Model mapping helpers
const addModelMapping = () => { const addModelMapping = () => {
modelMappings.value.push({ from: '', to: '' }) modelMappings.value.push({ from: '', to: '' })

View File

@@ -85,7 +85,7 @@
<div class="mt-2 grid grid-cols-2 gap-3"> <div class="mt-2 grid grid-cols-2 gap-3">
<button <button
type="button" type="button"
@click="geminiOAuthType = 'code_assist'" @click="handleSelectGeminiOAuthType('code_assist')"
:class="[ :class="[
'flex items-center gap-3 rounded-lg border-2 p-3 text-left transition-all', 'flex items-center gap-3 rounded-lg border-2 p-3 text-left transition-all',
geminiOAuthType === 'code_assist' geminiOAuthType === 'code_assist'
@@ -128,9 +128,11 @@
<button <button
type="button" type="button"
@click="geminiOAuthType = 'ai_studio'" :disabled="!geminiAIStudioOAuthEnabled"
@click="handleSelectGeminiOAuthType('ai_studio')"
:class="[ :class="[
'flex items-center gap-3 rounded-lg border-2 p-3 text-left transition-all', 'flex items-center gap-3 rounded-lg border-2 p-3 text-left transition-all',
!geminiAIStudioOAuthEnabled ? 'cursor-not-allowed opacity-60' : '',
geminiOAuthType === 'ai_studio' geminiOAuthType === 'ai_studio'
? 'border-purple-500 bg-purple-50 dark:bg-purple-900/20' ? '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' : 'border-gray-200 hover:border-purple-300 dark:border-dark-600 dark:hover:border-purple-700'
@@ -166,6 +168,18 @@
<span class="text-xs text-gray-500 dark:text-gray-400">{{ <span class="text-xs text-gray-500 dark:text-gray-400">{{
t('admin.accounts.oauth.gemini.noProjectIdNeededDesc') t('admin.accounts.oauth.gemini.noProjectIdNeededDesc')
}}</span> }}</span>
<div v-if="!geminiAIStudioOAuthEnabled" class="group relative mt-1 inline-block">
<span
class="rounded bg-amber-100 px-2 py-0.5 text-xs text-amber-700 dark:bg-amber-900/30 dark:text-amber-300"
>
{{ t('admin.accounts.oauth.gemini.aiStudioNotConfiguredShort') }}
</span>
<div
class="pointer-events-none absolute left-0 top-full z-10 mt-2 w-[28rem] rounded-md border border-amber-200 bg-amber-50 px-3 py-2 text-xs text-amber-800 opacity-0 shadow-sm transition-opacity group-hover:opacity-100 dark:border-amber-700 dark:bg-amber-900/40 dark:text-amber-200"
>
{{ t('admin.accounts.oauth.gemini.aiStudioNotConfiguredTip') }}
</div>
</div>
</div> </div>
</button> </button>
</div> </div>
@@ -283,6 +297,7 @@ const oauthFlowRef = ref<OAuthFlowExposed | null>(null)
// State // State
const addMethod = ref<AddMethod>('oauth') const addMethod = ref<AddMethod>('oauth')
const geminiOAuthType = ref<'code_assist' | 'ai_studio'>('code_assist') const geminiOAuthType = ref<'code_assist' | 'ai_studio'>('code_assist')
const geminiAIStudioOAuthEnabled = ref(false)
// Computed - check if this is an OpenAI account // Computed - check if this is an OpenAI account
const isOpenAI = computed(() => props.account?.platform === 'openai') const isOpenAI = computed(() => props.account?.platform === 'openai')
@@ -348,6 +363,14 @@ watch(
const creds = (props.account.credentials || {}) as Record<string, unknown> const creds = (props.account.credentials || {}) as Record<string, unknown>
geminiOAuthType.value = creds.oauth_type === 'ai_studio' ? 'ai_studio' : 'code_assist' geminiOAuthType.value = creds.oauth_type === 'ai_studio' ? 'ai_studio' : 'code_assist'
} }
if (isGemini.value) {
geminiOAuth.getCapabilities().then((caps) => {
geminiAIStudioOAuthEnabled.value = !!caps?.ai_studio_oauth_enabled
if (!geminiAIStudioOAuthEnabled.value && geminiOAuthType.value === 'ai_studio') {
geminiOAuthType.value = 'code_assist'
}
})
}
} else { } else {
resetState() resetState()
} }
@@ -358,12 +381,21 @@ watch(
const resetState = () => { const resetState = () => {
addMethod.value = 'oauth' addMethod.value = 'oauth'
geminiOAuthType.value = 'code_assist' geminiOAuthType.value = 'code_assist'
geminiAIStudioOAuthEnabled.value = false
claudeOAuth.resetState() claudeOAuth.resetState()
openaiOAuth.resetState() openaiOAuth.resetState()
geminiOAuth.resetState() geminiOAuth.resetState()
oauthFlowRef.value?.reset() oauthFlowRef.value?.reset()
} }
const handleSelectGeminiOAuthType = (oauthType: 'code_assist' | 'ai_studio') => {
if (oauthType === 'ai_studio' && !geminiAIStudioOAuthEnabled.value) {
appStore.showError(t('admin.accounts.oauth.gemini.aiStudioNotConfigured'))
return
}
geminiOAuthType.value = oauthType
}
const handleClose = () => { const handleClose = () => {
emit('close') emit('close')
} }