diff --git a/frontend/src/api/admin/gemini.ts b/frontend/src/api/admin/gemini.ts index 0a2c4a66..366e22d3 100644 --- a/frontend/src/api/admin/gemini.ts +++ b/frontend/src/api/admin/gemini.ts @@ -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 diff --git a/frontend/src/components/account/AccountTestModal.vue b/frontend/src/components/account/AccountTestModal.vue index d8cb1304..adf3cba6 100644 --- a/frontend/src/components/account/AccountTestModal.vue +++ b/frontend/src/components/account/AccountTestModal.vue @@ -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) diff --git a/frontend/src/components/account/CreateAccountModal.vue b/frontend/src/components/account/CreateAccountModal.vue index e22af4c7..de0f45af 100644 --- a/frontend/src/components/account/CreateAccountModal.vue +++ b/frontend/src/components/account/CreateAccountModal.vue @@ -370,7 +370,7 @@ @@ -380,6 +380,70 @@ + + +
+ +
+ + + +
+
@@ -442,7 +506,13 @@ : 'sk-ant-...' " /> -

{{ t('admin.accounts.apiKeyHint') }}

+

+ {{ + form.platform === 'gemini' + ? t('admin.accounts.gemini.apiKeyHint') + : t('admin.accounts.apiKeyHint') + }} +

@@ -900,34 +970,6 @@
-
- - -

{{ t('admin.accounts.oauth.gemini.redirectUriHint') }}

-
- - -
-

- {{ t('admin.accounts.oauth.gemini.invalidRedirectUri') }} -

-
- @@ -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([]) const customErrorCodeInput = ref(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 diff --git a/frontend/src/components/account/OAuthAuthorizationFlow.vue b/frontend/src/components/account/OAuthAuthorizationFlow.vue index 9cdc9a0f..84760a27 100644 --- a/frontend/src/components/account/OAuthAuthorizationFlow.vue +++ b/frontend/src/components/account/OAuthAuthorizationFlow.vue @@ -229,6 +229,31 @@

{{ oauthStep1GenerateUrl }}

+
+ + +

+ {{ t('admin.accounts.oauth.gemini.projectIdHint') }} +

+
- -
- - -

{{ t('admin.accounts.oauth.gemini.redirectUriHint') }}

-
- - + +
+ +
+ + +
-

- {{ t('admin.accounts.oauth.gemini.invalidRedirectUri') }} -

@@ -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(null) // State const addMethod = ref('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 + 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 diff --git a/frontend/src/composables/useGeminiOAuth.ts b/frontend/src/composables/useGeminiOAuth.ts index 63e5c2b9..ac74b57f 100644 --- a/frontend/src/composables/useGeminiOAuth.ts +++ b/frontend/src/composables/useGeminiOAuth.ts @@ -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 => { 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 = { redirect_uri: redirectUri.trim() } + const payload: Record = {} 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 => { 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 = { 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 } }