feat(frontend): 支持 Gemini OAuth 类型选择 (Code Assist/AI Studio)
- CreateAccountModal.vue: 新增 OAuth 类型选择 UI - ReAuthAccountModal.vue: 重授权支持选择类型 - OAuthAuthorizationFlow.vue: 新增 Project ID 输入框 - AccountTestModal.vue: Gemini 模型默认选择优化 - useGeminiOAuth.ts: OAuth 逻辑参数变更 - gemini.ts: API 调用更新
This commit is contained in:
@@ -370,7 +370,7 @@
|
||||
<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"
|
||||
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"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
@@ -380,6 +380,70 @@
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 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">
|
||||
<button
|
||||
type="button"
|
||||
@click="geminiOAuthType = 'code_assist'"
|
||||
: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>
|
||||
<div>
|
||||
<span class="block text-sm font-medium text-gray-900 dark:text-white">Code Assist</span>
|
||||
<span class="block text-xs font-medium text-blue-600 dark:text-blue-400">{{ t('admin.accounts.oauth.gemini.needsProjectId') }}</span>
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">{{ t('admin.accounts.oauth.gemini.needsProjectIdDesc') }}</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
@click="geminiOAuthType = 'ai_studio'"
|
||||
:class="[
|
||||
'flex items-center gap-3 rounded-lg border-2 p-3 text-left transition-all',
|
||||
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="[
|
||||
'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>
|
||||
<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>
|
||||
|
||||
<!-- Add Method (only for Anthropic OAuth-based type) -->
|
||||
@@ -442,7 +506,13 @@
|
||||
: 'sk-ant-...'
|
||||
"
|
||||
/>
|
||||
<p class="input-hint">{{ t('admin.accounts.apiKeyHint') }}</p>
|
||||
<p class="input-hint">
|
||||
{{
|
||||
form.platform === 'gemini'
|
||||
? t('admin.accounts.gemini.apiKeyHint')
|
||||
: t('admin.accounts.apiKeyHint')
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Model Restriction Section (不适用于 Gemini) -->
|
||||
@@ -900,34 +970,6 @@
|
||||
|
||||
<!-- Step 2: OAuth Authorization -->
|
||||
<div v-else class="space-y-5">
|
||||
<div
|
||||
v-if="form.platform === 'gemini'"
|
||||
class="rounded-lg border border-gray-200 bg-gray-50 p-4 dark:border-dark-600 dark:bg-dark-700"
|
||||
>
|
||||
<label class="input-label">{{ t('admin.accounts.oauth.gemini.redirectUri') }}</label>
|
||||
<input
|
||||
v-model="geminiRedirectUri"
|
||||
type="text"
|
||||
class="input font-mono text-sm"
|
||||
:placeholder="defaultCallbackUrl"
|
||||
/>
|
||||
<p class="input-hint">{{ t('admin.accounts.oauth.gemini.redirectUriHint') }}</p>
|
||||
<div class="mt-3 flex items-start gap-2">
|
||||
<input
|
||||
id="gemini-redirect-uri-confirm"
|
||||
v-model="geminiRedirectUriConfirmed"
|
||||
type="checkbox"
|
||||
class="mt-0.5 h-4 w-4 rounded border-gray-300 text-primary-600 focus:ring-primary-500 dark:border-dark-500"
|
||||
/>
|
||||
<label for="gemini-redirect-uri-confirm" class="text-sm text-gray-700 dark:text-gray-300">
|
||||
{{ t('admin.accounts.oauth.gemini.confirmRedirectUri') }}
|
||||
</label>
|
||||
</div>
|
||||
<p v-if="geminiRedirectUri && !isValidGeminiRedirectUri" class="mt-2 text-xs text-red-600">
|
||||
{{ t('admin.accounts.oauth.gemini.invalidRedirectUri') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<OAuthAuthorizationFlow
|
||||
ref="oauthFlowRef"
|
||||
:add-method="form.platform === 'anthropic' ? addMethod : 'oauth'"
|
||||
@@ -940,6 +982,7 @@
|
||||
: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"
|
||||
/>
|
||||
@@ -1009,6 +1052,7 @@ import OAuthAuthorizationFlow from './OAuthAuthorizationFlow.vue'
|
||||
interface OAuthFlowExposed {
|
||||
authCode: string
|
||||
oauthState: string
|
||||
projectId: string
|
||||
sessionKey: string
|
||||
inputMethod: AuthInputMethod
|
||||
reset: () => void
|
||||
@@ -1089,24 +1133,7 @@ const customErrorCodesEnabled = ref(false)
|
||||
const selectedErrorCodes = ref<number[]>([])
|
||||
const customErrorCodeInput = ref<number | null>(null)
|
||||
const interceptWarmupRequests = ref(false)
|
||||
|
||||
// 优先使用环境变量配置的 Redirect URI,否则使用当前域名
|
||||
const defaultCallbackUrl =
|
||||
import.meta.env.VITE_OAUTH_CALLBACK_URL ||
|
||||
(typeof window !== 'undefined' ? `${window.location.origin}/auth/callback` : '')
|
||||
const geminiRedirectUri = ref(defaultCallbackUrl)
|
||||
const geminiRedirectUriConfirmed = ref(false)
|
||||
|
||||
const isValidGeminiRedirectUri = computed(() => {
|
||||
const raw = geminiRedirectUri.value?.trim()
|
||||
if (!raw) return false
|
||||
try {
|
||||
const parsed = new URL(raw)
|
||||
return parsed.protocol === 'http:' || parsed.protocol === 'https:'
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
})
|
||||
const geminiOAuthType = ref<'code_assist' | 'ai_studio'>('code_assist')
|
||||
|
||||
// Common models for whitelist - Anthropic
|
||||
const anthropicModels = [
|
||||
@@ -1305,13 +1332,7 @@ const canExchangeCode = computed(() => {
|
||||
return authCode.trim() && openaiOAuth.sessionId.value && !openaiOAuth.loading.value
|
||||
}
|
||||
if (form.platform === 'gemini') {
|
||||
return (
|
||||
authCode.trim() &&
|
||||
geminiOAuth.sessionId.value &&
|
||||
!geminiOAuth.loading.value &&
|
||||
geminiRedirectUriConfirmed.value &&
|
||||
isValidGeminiRedirectUri.value
|
||||
)
|
||||
return authCode.trim() && geminiOAuth.sessionId.value && !geminiOAuth.loading.value
|
||||
}
|
||||
return authCode.trim() && oauth.sessionId.value && !oauth.loading.value
|
||||
})
|
||||
@@ -1361,17 +1382,9 @@ watch(
|
||||
oauth.resetState()
|
||||
openaiOAuth.resetState()
|
||||
geminiOAuth.resetState()
|
||||
if (newPlatform === 'gemini' && !geminiRedirectUri.value) {
|
||||
geminiRedirectUri.value = defaultCallbackUrl
|
||||
}
|
||||
geminiRedirectUriConfirmed.value = false
|
||||
}
|
||||
)
|
||||
|
||||
watch(geminiRedirectUri, () => {
|
||||
geminiRedirectUriConfirmed.value = false
|
||||
})
|
||||
|
||||
// Model mapping helpers
|
||||
const addModelMapping = () => {
|
||||
modelMappings.value.push({ from: '', to: '' })
|
||||
@@ -1468,11 +1481,10 @@ const resetForm = () => {
|
||||
selectedErrorCodes.value = []
|
||||
customErrorCodeInput.value = null
|
||||
interceptWarmupRequests.value = false
|
||||
geminiOAuthType.value = 'code_assist'
|
||||
oauth.resetState()
|
||||
openaiOAuth.resetState()
|
||||
geminiOAuth.resetState()
|
||||
geminiRedirectUri.value = defaultCallbackUrl
|
||||
geminiRedirectUriConfirmed.value = false
|
||||
oauthFlowRef.value?.reset()
|
||||
}
|
||||
|
||||
@@ -1551,7 +1563,6 @@ const goBackToBasicInfo = () => {
|
||||
oauth.resetState()
|
||||
openaiOAuth.resetState()
|
||||
geminiOAuth.resetState()
|
||||
geminiRedirectUriConfirmed.value = false
|
||||
oauthFlowRef.value?.reset()
|
||||
}
|
||||
|
||||
@@ -1559,15 +1570,7 @@ const handleGenerateUrl = async () => {
|
||||
if (form.platform === 'openai') {
|
||||
await openaiOAuth.generateAuthUrl(form.proxy_id)
|
||||
} else if (form.platform === 'gemini') {
|
||||
if (!isValidGeminiRedirectUri.value) {
|
||||
appStore.showError(t('admin.accounts.oauth.gemini.invalidRedirectUri'))
|
||||
return
|
||||
}
|
||||
if (!geminiRedirectUriConfirmed.value) {
|
||||
appStore.showError(t('admin.accounts.oauth.gemini.redirectUriNotConfirmed'))
|
||||
return
|
||||
}
|
||||
await geminiOAuth.generateAuthUrl(form.proxy_id, geminiRedirectUri.value)
|
||||
await geminiOAuth.generateAuthUrl(form.proxy_id, oauthFlowRef.value?.projectId, geminiOAuthType.value)
|
||||
} else {
|
||||
await oauth.generateAuthUrl(addMethod.value, form.proxy_id)
|
||||
}
|
||||
@@ -1631,17 +1634,6 @@ const handleExchangeCode = async () => {
|
||||
geminiOAuth.error.value = ''
|
||||
|
||||
try {
|
||||
if (!isValidGeminiRedirectUri.value) {
|
||||
geminiOAuth.error.value = t('admin.accounts.oauth.gemini.invalidRedirectUri')
|
||||
appStore.showError(geminiOAuth.error.value)
|
||||
return
|
||||
}
|
||||
if (!geminiRedirectUriConfirmed.value) {
|
||||
geminiOAuth.error.value = t('admin.accounts.oauth.gemini.redirectUriNotConfirmed')
|
||||
appStore.showError(geminiOAuth.error.value)
|
||||
return
|
||||
}
|
||||
|
||||
const stateFromInput = oauthFlowRef.value?.oauthState || ''
|
||||
const stateToUse = stateFromInput || geminiOAuth.state.value
|
||||
if (!stateToUse) {
|
||||
@@ -1654,8 +1646,8 @@ const handleExchangeCode = async () => {
|
||||
code: authCode.trim(),
|
||||
sessionId: geminiOAuth.sessionId.value,
|
||||
state: stateToUse,
|
||||
redirectUri: geminiRedirectUri.value,
|
||||
proxyId: form.proxy_id
|
||||
proxyId: form.proxy_id,
|
||||
oauthType: geminiOAuthType.value
|
||||
})
|
||||
if (!tokenInfo) return
|
||||
|
||||
|
||||
Reference in New Issue
Block a user