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:
ianshaw
2025-12-25 21:25:02 -08:00
parent 46cb82bac0
commit 09431cfc0b
6 changed files with 239 additions and 189 deletions

View File

@@ -12,16 +12,17 @@ export interface GeminiAuthUrlResponse {
}
export interface GeminiAuthUrlRequest {
redirect_uri: string
proxy_id?: number
project_id?: string
oauth_type?: 'code_assist' | 'ai_studio'
}
export interface GeminiExchangeCodeRequest {
session_id: string
state: string
code: string
redirect_uri: string
proxy_id?: number
oauth_type?: 'code_assist' | 'ai_studio'
}
export type GeminiTokenInfo = Record<string, unknown>

View File

@@ -329,11 +329,18 @@ const loadAvailableModels = async () => {
selectedModelId.value = '' // Reset selection before loading
try {
availableModels.value = await adminAPI.accounts.getAvailableModels(props.account.id)
// Default to first model (usually Sonnet)
// Default selection by platform
if (availableModels.value.length > 0) {
// Try to select Sonnet as default, otherwise use first model
const sonnetModel = availableModels.value.find((m) => m.id.includes('sonnet'))
selectedModelId.value = sonnetModel?.id || availableModels.value[0].id
if (props.account.platform === 'gemini') {
const preferred =
availableModels.value.find((m) => m.id === 'gemini-2.5-pro') ||
availableModels.value.find((m) => m.id === 'gemini-3-pro')
selectedModelId.value = preferred?.id || availableModels.value[0].id
} else {
// Try to select Sonnet as default, otherwise use first model
const sonnetModel = availableModels.value.find((m) => m.id.includes('sonnet'))
selectedModelId.value = sonnetModel?.id || availableModels.value[0].id
}
}
} catch (error) {
console.error('Failed to load available models:', error)

View File

@@ -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

View File

@@ -229,6 +229,31 @@
<p class="mb-2 font-medium text-blue-900 dark:text-blue-200">
{{ oauthStep1GenerateUrl }}
</p>
<div v-if="showProjectId && platform === 'gemini'" class="mb-3">
<label class="input-label flex items-center gap-2">
{{ t('admin.accounts.oauth.gemini.projectIdLabel') }}
<a
href="https://console.cloud.google.com/"
target="_blank"
rel="noopener noreferrer"
class="inline-flex items-center gap-1 text-xs font-normal text-blue-500 hover:text-blue-600 dark:text-blue-400"
>
<svg class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
<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.oauth.gemini.howToGetProjectId') }}
</a>
</label>
<input
v-model="projectId"
type="text"
class="input w-full font-mono text-sm"
:placeholder="t('admin.accounts.oauth.gemini.projectIdPlaceholder')"
/>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
{{ t('admin.accounts.oauth.gemini.projectIdHint') }}
</p>
</div>
<button
v-if="!authUrl"
type="button"
@@ -503,6 +528,7 @@ interface Props {
methodLabel?: string
showCookieOption?: boolean // Whether to show cookie auto-auth option
platform?: 'anthropic' | 'openai' | 'gemini' // Platform type for different UI/text
showProjectId?: boolean // New prop to control project ID visibility
}
const props = withDefaults(defineProps<Props>(), {
@@ -515,7 +541,8 @@ const props = withDefaults(defineProps<Props>(), {
allowMultiple: false,
methodLabel: 'Authorization Method',
showCookieOption: true,
platform: 'anthropic'
platform: 'anthropic',
showProjectId: true
})
const emit = defineEmits<{
@@ -558,6 +585,7 @@ const authCodeInput = ref('')
const sessionKeyInput = ref('')
const showHelpDialog = ref(false)
const oauthState = ref('')
const projectId = ref('')
// Clipboard
const { copied, copyToClipboard } = useClipboard()
@@ -635,11 +663,13 @@ const handleCookieAuth = () => {
defineExpose({
authCode: authCodeInput,
oauthState,
projectId,
sessionKey: sessionKeyInput,
inputMethod,
reset: () => {
authCodeInput.value = ''
oauthState.value = ''
projectId.value = ''
sessionKeyInput.value = ''
inputMethod.value = 'manual'
showHelpDialog.value = false

View File

@@ -79,33 +79,96 @@
</div>
</div>
<!-- OAuth Authorization Section -->
<div
v-if="isGemini"
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>
<!-- Gemini OAuth Type Selection -->
<div v-if="isGemini">
<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>
<p v-if="geminiRedirectUri && !isValidGeminiRedirectUri" class="mt-2 text-xs text-red-600">
{{ t('admin.accounts.oauth.gemini.invalidRedirectUri') }}
</p>
</div>
<OAuthAuthorizationFlow
@@ -121,6 +184,7 @@
:allow-multiple="false"
:method-label="t('admin.accounts.inputMethod')"
:platform="isOpenAI ? 'openai' : isGemini ? 'gemini' : 'anthropic'"
:show-project-id="isGemini && geminiOAuthType === 'code_assist'"
@generate-url="handleGenerateUrl"
@cookie-auth="handleCookieAuth"
/>
@@ -188,6 +252,7 @@ import OAuthAuthorizationFlow from './OAuthAuthorizationFlow.vue'
interface OAuthFlowExposed {
authCode: string
oauthState: string
projectId: string
sessionKey: string
inputMethod: AuthInputMethod
reset: () => void
@@ -217,6 +282,7 @@ const oauthFlowRef = ref<OAuthFlowExposed | null>(null)
// State
const addMethod = ref<AddMethod>('oauth')
const geminiOAuthType = ref<'code_assist' | 'ai_studio'>('code_assist')
// Computed - check if this is an OpenAI account
const isOpenAI = computed(() => props.account?.platform === 'openai')
@@ -263,15 +329,6 @@ const canExchangeCode = computed(() => {
: isGemini.value
? geminiOAuth.loading.value
: claudeOAuth.loading.value
if (isGemini.value) {
return (
authCode.trim() &&
sessionId &&
!loading &&
geminiRedirectUriConfirmed.value &&
isValidGeminiRedirectUri.value
)
}
return authCode.trim() && sessionId && !loading
})
@@ -287,6 +344,10 @@ watch(
) {
addMethod.value = props.account.type as AddMethod
}
if (isGemini.value) {
const creds = (props.account.credentials || {}) as Record<string, unknown>
geminiOAuthType.value = creds.oauth_type === 'ai_studio' ? 'ai_studio' : 'code_assist'
}
} else {
resetState()
}
@@ -296,11 +357,10 @@ watch(
// Methods
const resetState = () => {
addMethod.value = 'oauth'
geminiOAuthType.value = 'code_assist'
claudeOAuth.resetState()
openaiOAuth.resetState()
geminiOAuth.resetState()
geminiRedirectUri.value = defaultCallbackUrl
geminiRedirectUriConfirmed.value = false
oauthFlowRef.value?.reset()
}
@@ -314,15 +374,8 @@ const handleGenerateUrl = async () => {
if (isOpenAI.value) {
await openaiOAuth.generateAuthUrl(props.account.proxy_id)
} else if (isGemini.value) {
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(props.account.proxy_id, geminiRedirectUri.value)
const projectId = geminiOAuthType.value === 'code_assist' ? oauthFlowRef.value?.projectId : undefined
await geminiOAuth.generateAuthUrl(props.account.proxy_id, projectId, geminiOAuthType.value)
} else {
await claudeOAuth.generateAuthUrl(addMethod.value, props.account.proxy_id)
}
@@ -372,17 +425,6 @@ const handleExchangeCode = async () => {
const sessionId = geminiOAuth.sessionId.value
if (!sessionId) return
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) return
@@ -391,8 +433,8 @@ const handleExchangeCode = async () => {
code: authCode.trim(),
sessionId,
state: stateToUse,
redirectUri: geminiRedirectUri.value,
proxyId: props.account.proxy_id
proxyId: props.account.proxy_id,
oauthType: geminiOAuthType.value
})
if (!tokenInfo) return
@@ -456,28 +498,6 @@ const handleExchangeCode = async () => {
}
}
// 优先使用环境变量配置的 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
}
})
watch(geminiRedirectUri, () => {
geminiRedirectUriConfirmed.value = false
})
const handleCookieAuth = async (sessionKey: string) => {
if (!props.account || isOpenAI.value) return

View File

@@ -10,6 +10,7 @@ export interface GeminiTokenInfo {
scope?: string
expires_at?: number | string
project_id?: string
oauth_type?: string
[key: string]: unknown
}
@@ -33,7 +34,8 @@ export function useGeminiOAuth() {
const generateAuthUrl = async (
proxyId: number | null | undefined,
redirectUri: string
projectId?: string | null,
oauthType?: string
): Promise<boolean> => {
loading.value = true
authUrl.value = ''
@@ -42,14 +44,11 @@ export function useGeminiOAuth() {
error.value = ''
try {
if (!redirectUri?.trim()) {
error.value = t('admin.accounts.oauth.gemini.missingRedirectUri')
appStore.showError(error.value)
return false
}
const payload: Record<string, unknown> = { redirect_uri: redirectUri.trim() }
const payload: Record<string, unknown> = {}
if (proxyId) payload.proxy_id = proxyId
const trimmedProjectID = projectId?.trim()
if (trimmedProjectID) payload.project_id = trimmedProjectID
if (oauthType) payload.oauth_type = oauthType
const response = await adminAPI.gemini.generateAuthUrl(payload as any)
authUrl.value = response.auth_url
@@ -69,11 +68,11 @@ export function useGeminiOAuth() {
code: string
sessionId: string
state: string
redirectUri: string
proxyId?: number | null
oauthType?: string
}): Promise<GeminiTokenInfo | null> => {
const code = params.code?.trim()
if (!code || !params.sessionId || !params.state || !params.redirectUri?.trim()) {
if (!code || !params.sessionId || !params.state) {
error.value = t('admin.accounts.oauth.gemini.missingExchangeParams')
return null
}
@@ -85,10 +84,10 @@ export function useGeminiOAuth() {
const payload: Record<string, unknown> = {
session_id: params.sessionId,
state: params.state,
code,
redirect_uri: params.redirectUri.trim()
code
}
if (params.proxyId) payload.proxy_id = params.proxyId
if (params.oauthType) payload.oauth_type = params.oauthType
const tokenInfo = await adminAPI.gemini.exchangeCode(payload as any)
return tokenInfo as GeminiTokenInfo
@@ -115,7 +114,8 @@ export function useGeminiOAuth() {
token_type: tokenInfo.token_type,
expires_at: expiresAt,
scope: tokenInfo.scope,
project_id: tokenInfo.project_id
project_id: tokenInfo.project_id,
oauth_type: tokenInfo.oauth_type
}
}