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:
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user