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 {
|
export interface GeminiAuthUrlRequest {
|
||||||
redirect_uri: string
|
|
||||||
proxy_id?: number
|
proxy_id?: number
|
||||||
|
project_id?: string
|
||||||
|
oauth_type?: 'code_assist' | 'ai_studio'
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GeminiExchangeCodeRequest {
|
export interface GeminiExchangeCodeRequest {
|
||||||
session_id: string
|
session_id: string
|
||||||
state: string
|
state: string
|
||||||
code: string
|
code: string
|
||||||
redirect_uri: string
|
|
||||||
proxy_id?: number
|
proxy_id?: number
|
||||||
|
oauth_type?: 'code_assist' | 'ai_studio'
|
||||||
}
|
}
|
||||||
|
|
||||||
export type GeminiTokenInfo = Record<string, unknown>
|
export type GeminiTokenInfo = Record<string, unknown>
|
||||||
|
|||||||
@@ -329,11 +329,18 @@ const loadAvailableModels = async () => {
|
|||||||
selectedModelId.value = '' // Reset selection before loading
|
selectedModelId.value = '' // Reset selection before loading
|
||||||
try {
|
try {
|
||||||
availableModels.value = await adminAPI.accounts.getAvailableModels(props.account.id)
|
availableModels.value = await adminAPI.accounts.getAvailableModels(props.account.id)
|
||||||
// Default to first model (usually Sonnet)
|
// Default selection by platform
|
||||||
if (availableModels.value.length > 0) {
|
if (availableModels.value.length > 0) {
|
||||||
// Try to select Sonnet as default, otherwise use first model
|
if (props.account.platform === 'gemini') {
|
||||||
const sonnetModel = availableModels.value.find((m) => m.id.includes('sonnet'))
|
const preferred =
|
||||||
selectedModelId.value = sonnetModel?.id || availableModels.value[0].id
|
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) {
|
} catch (error) {
|
||||||
console.error('Failed to load available models:', error)
|
console.error('Failed to load available models:', error)
|
||||||
|
|||||||
@@ -370,7 +370,7 @@
|
|||||||
<path
|
<path
|
||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
stroke-linejoin="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>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
@@ -380,6 +380,70 @@
|
|||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
<!-- Add Method (only for Anthropic OAuth-based type) -->
|
<!-- Add Method (only for Anthropic OAuth-based type) -->
|
||||||
@@ -442,7 +506,13 @@
|
|||||||
: 'sk-ant-...'
|
: '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>
|
</div>
|
||||||
|
|
||||||
<!-- Model Restriction Section (不适用于 Gemini) -->
|
<!-- Model Restriction Section (不适用于 Gemini) -->
|
||||||
@@ -900,34 +970,6 @@
|
|||||||
|
|
||||||
<!-- Step 2: OAuth Authorization -->
|
<!-- Step 2: OAuth Authorization -->
|
||||||
<div v-else class="space-y-5">
|
<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
|
<OAuthAuthorizationFlow
|
||||||
ref="oauthFlowRef"
|
ref="oauthFlowRef"
|
||||||
:add-method="form.platform === 'anthropic' ? addMethod : 'oauth'"
|
:add-method="form.platform === 'anthropic' ? addMethod : 'oauth'"
|
||||||
@@ -940,6 +982,7 @@
|
|||||||
:allow-multiple="form.platform === 'anthropic'"
|
:allow-multiple="form.platform === 'anthropic'"
|
||||||
:show-cookie-option="form.platform === 'anthropic'"
|
:show-cookie-option="form.platform === 'anthropic'"
|
||||||
:platform="form.platform"
|
:platform="form.platform"
|
||||||
|
:show-project-id="geminiOAuthType === 'code_assist'"
|
||||||
@generate-url="handleGenerateUrl"
|
@generate-url="handleGenerateUrl"
|
||||||
@cookie-auth="handleCookieAuth"
|
@cookie-auth="handleCookieAuth"
|
||||||
/>
|
/>
|
||||||
@@ -1009,6 +1052,7 @@ import OAuthAuthorizationFlow from './OAuthAuthorizationFlow.vue'
|
|||||||
interface OAuthFlowExposed {
|
interface OAuthFlowExposed {
|
||||||
authCode: string
|
authCode: string
|
||||||
oauthState: string
|
oauthState: string
|
||||||
|
projectId: string
|
||||||
sessionKey: string
|
sessionKey: string
|
||||||
inputMethod: AuthInputMethod
|
inputMethod: AuthInputMethod
|
||||||
reset: () => void
|
reset: () => void
|
||||||
@@ -1089,24 +1133,7 @@ const customErrorCodesEnabled = ref(false)
|
|||||||
const selectedErrorCodes = ref<number[]>([])
|
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')
|
||||||
// 优先使用环境变量配置的 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
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Common models for whitelist - Anthropic
|
// Common models for whitelist - Anthropic
|
||||||
const anthropicModels = [
|
const anthropicModels = [
|
||||||
@@ -1305,13 +1332,7 @@ const canExchangeCode = computed(() => {
|
|||||||
return authCode.trim() && openaiOAuth.sessionId.value && !openaiOAuth.loading.value
|
return authCode.trim() && openaiOAuth.sessionId.value && !openaiOAuth.loading.value
|
||||||
}
|
}
|
||||||
if (form.platform === 'gemini') {
|
if (form.platform === 'gemini') {
|
||||||
return (
|
return authCode.trim() && geminiOAuth.sessionId.value && !geminiOAuth.loading.value
|
||||||
authCode.trim() &&
|
|
||||||
geminiOAuth.sessionId.value &&
|
|
||||||
!geminiOAuth.loading.value &&
|
|
||||||
geminiRedirectUriConfirmed.value &&
|
|
||||||
isValidGeminiRedirectUri.value
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
return authCode.trim() && oauth.sessionId.value && !oauth.loading.value
|
return authCode.trim() && oauth.sessionId.value && !oauth.loading.value
|
||||||
})
|
})
|
||||||
@@ -1361,17 +1382,9 @@ watch(
|
|||||||
oauth.resetState()
|
oauth.resetState()
|
||||||
openaiOAuth.resetState()
|
openaiOAuth.resetState()
|
||||||
geminiOAuth.resetState()
|
geminiOAuth.resetState()
|
||||||
if (newPlatform === 'gemini' && !geminiRedirectUri.value) {
|
|
||||||
geminiRedirectUri.value = defaultCallbackUrl
|
|
||||||
}
|
|
||||||
geminiRedirectUriConfirmed.value = false
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
watch(geminiRedirectUri, () => {
|
|
||||||
geminiRedirectUriConfirmed.value = false
|
|
||||||
})
|
|
||||||
|
|
||||||
// Model mapping helpers
|
// Model mapping helpers
|
||||||
const addModelMapping = () => {
|
const addModelMapping = () => {
|
||||||
modelMappings.value.push({ from: '', to: '' })
|
modelMappings.value.push({ from: '', to: '' })
|
||||||
@@ -1468,11 +1481,10 @@ const resetForm = () => {
|
|||||||
selectedErrorCodes.value = []
|
selectedErrorCodes.value = []
|
||||||
customErrorCodeInput.value = null
|
customErrorCodeInput.value = null
|
||||||
interceptWarmupRequests.value = false
|
interceptWarmupRequests.value = false
|
||||||
|
geminiOAuthType.value = 'code_assist'
|
||||||
oauth.resetState()
|
oauth.resetState()
|
||||||
openaiOAuth.resetState()
|
openaiOAuth.resetState()
|
||||||
geminiOAuth.resetState()
|
geminiOAuth.resetState()
|
||||||
geminiRedirectUri.value = defaultCallbackUrl
|
|
||||||
geminiRedirectUriConfirmed.value = false
|
|
||||||
oauthFlowRef.value?.reset()
|
oauthFlowRef.value?.reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1551,7 +1563,6 @@ const goBackToBasicInfo = () => {
|
|||||||
oauth.resetState()
|
oauth.resetState()
|
||||||
openaiOAuth.resetState()
|
openaiOAuth.resetState()
|
||||||
geminiOAuth.resetState()
|
geminiOAuth.resetState()
|
||||||
geminiRedirectUriConfirmed.value = false
|
|
||||||
oauthFlowRef.value?.reset()
|
oauthFlowRef.value?.reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1559,15 +1570,7 @@ const handleGenerateUrl = async () => {
|
|||||||
if (form.platform === 'openai') {
|
if (form.platform === 'openai') {
|
||||||
await openaiOAuth.generateAuthUrl(form.proxy_id)
|
await openaiOAuth.generateAuthUrl(form.proxy_id)
|
||||||
} else if (form.platform === 'gemini') {
|
} else if (form.platform === 'gemini') {
|
||||||
if (!isValidGeminiRedirectUri.value) {
|
await geminiOAuth.generateAuthUrl(form.proxy_id, oauthFlowRef.value?.projectId, geminiOAuthType.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)
|
|
||||||
} else {
|
} else {
|
||||||
await oauth.generateAuthUrl(addMethod.value, form.proxy_id)
|
await oauth.generateAuthUrl(addMethod.value, form.proxy_id)
|
||||||
}
|
}
|
||||||
@@ -1631,17 +1634,6 @@ const handleExchangeCode = async () => {
|
|||||||
geminiOAuth.error.value = ''
|
geminiOAuth.error.value = ''
|
||||||
|
|
||||||
try {
|
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 stateFromInput = oauthFlowRef.value?.oauthState || ''
|
||||||
const stateToUse = stateFromInput || geminiOAuth.state.value
|
const stateToUse = stateFromInput || geminiOAuth.state.value
|
||||||
if (!stateToUse) {
|
if (!stateToUse) {
|
||||||
@@ -1654,8 +1646,8 @@ const handleExchangeCode = async () => {
|
|||||||
code: authCode.trim(),
|
code: authCode.trim(),
|
||||||
sessionId: geminiOAuth.sessionId.value,
|
sessionId: geminiOAuth.sessionId.value,
|
||||||
state: stateToUse,
|
state: stateToUse,
|
||||||
redirectUri: geminiRedirectUri.value,
|
proxyId: form.proxy_id,
|
||||||
proxyId: form.proxy_id
|
oauthType: geminiOAuthType.value
|
||||||
})
|
})
|
||||||
if (!tokenInfo) return
|
if (!tokenInfo) return
|
||||||
|
|
||||||
|
|||||||
@@ -229,6 +229,31 @@
|
|||||||
<p class="mb-2 font-medium text-blue-900 dark:text-blue-200">
|
<p class="mb-2 font-medium text-blue-900 dark:text-blue-200">
|
||||||
{{ oauthStep1GenerateUrl }}
|
{{ oauthStep1GenerateUrl }}
|
||||||
</p>
|
</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
|
<button
|
||||||
v-if="!authUrl"
|
v-if="!authUrl"
|
||||||
type="button"
|
type="button"
|
||||||
@@ -503,6 +528,7 @@ interface Props {
|
|||||||
methodLabel?: string
|
methodLabel?: string
|
||||||
showCookieOption?: boolean // Whether to show cookie auto-auth option
|
showCookieOption?: boolean // Whether to show cookie auto-auth option
|
||||||
platform?: 'anthropic' | 'openai' | 'gemini' // Platform type for different UI/text
|
platform?: 'anthropic' | 'openai' | 'gemini' // Platform type for different UI/text
|
||||||
|
showProjectId?: boolean // New prop to control project ID visibility
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
@@ -515,7 +541,8 @@ const props = withDefaults(defineProps<Props>(), {
|
|||||||
allowMultiple: false,
|
allowMultiple: false,
|
||||||
methodLabel: 'Authorization Method',
|
methodLabel: 'Authorization Method',
|
||||||
showCookieOption: true,
|
showCookieOption: true,
|
||||||
platform: 'anthropic'
|
platform: 'anthropic',
|
||||||
|
showProjectId: true
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@@ -558,6 +585,7 @@ const authCodeInput = ref('')
|
|||||||
const sessionKeyInput = ref('')
|
const sessionKeyInput = ref('')
|
||||||
const showHelpDialog = ref(false)
|
const showHelpDialog = ref(false)
|
||||||
const oauthState = ref('')
|
const oauthState = ref('')
|
||||||
|
const projectId = ref('')
|
||||||
|
|
||||||
// Clipboard
|
// Clipboard
|
||||||
const { copied, copyToClipboard } = useClipboard()
|
const { copied, copyToClipboard } = useClipboard()
|
||||||
@@ -635,11 +663,13 @@ const handleCookieAuth = () => {
|
|||||||
defineExpose({
|
defineExpose({
|
||||||
authCode: authCodeInput,
|
authCode: authCodeInput,
|
||||||
oauthState,
|
oauthState,
|
||||||
|
projectId,
|
||||||
sessionKey: sessionKeyInput,
|
sessionKey: sessionKeyInput,
|
||||||
inputMethod,
|
inputMethod,
|
||||||
reset: () => {
|
reset: () => {
|
||||||
authCodeInput.value = ''
|
authCodeInput.value = ''
|
||||||
oauthState.value = ''
|
oauthState.value = ''
|
||||||
|
projectId.value = ''
|
||||||
sessionKeyInput.value = ''
|
sessionKeyInput.value = ''
|
||||||
inputMethod.value = 'manual'
|
inputMethod.value = 'manual'
|
||||||
showHelpDialog.value = false
|
showHelpDialog.value = false
|
||||||
|
|||||||
@@ -79,33 +79,96 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- OAuth Authorization Section -->
|
<!-- Gemini OAuth Type Selection -->
|
||||||
<div
|
<div v-if="isGemini">
|
||||||
v-if="isGemini"
|
<label class="input-label">{{ t('admin.accounts.oauth.gemini.oauthTypeLabel') }}</label>
|
||||||
class="rounded-lg border border-gray-200 bg-gray-50 p-4 dark:border-dark-600 dark:bg-dark-700"
|
<div class="mt-2 grid grid-cols-2 gap-3">
|
||||||
>
|
<button
|
||||||
<label class="input-label">{{ t('admin.accounts.oauth.gemini.redirectUri') }}</label>
|
type="button"
|
||||||
<input
|
@click="geminiOAuthType = 'code_assist'"
|
||||||
v-model="geminiRedirectUri"
|
:class="[
|
||||||
type="text"
|
'flex items-center gap-3 rounded-lg border-2 p-3 text-left transition-all',
|
||||||
class="input font-mono text-sm"
|
geminiOAuthType === 'code_assist'
|
||||||
:placeholder="defaultCallbackUrl"
|
? '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'
|
||||||
<p class="input-hint">{{ t('admin.accounts.oauth.gemini.redirectUriHint') }}</p>
|
]"
|
||||||
<div class="mt-3 flex items-start gap-2">
|
>
|
||||||
<input
|
<div
|
||||||
id="gemini-redirect-uri-confirm"
|
:class="[
|
||||||
v-model="geminiRedirectUriConfirmed"
|
'flex h-8 w-8 items-center justify-center rounded-lg',
|
||||||
type="checkbox"
|
geminiOAuthType === 'code_assist'
|
||||||
class="mt-0.5 h-4 w-4 rounded border-gray-300 text-primary-600 focus:ring-primary-500 dark:border-dark-500"
|
? 'bg-blue-500 text-white'
|
||||||
/>
|
: 'bg-gray-100 text-gray-500 dark:bg-dark-600 dark:text-gray-400'
|
||||||
<label for="gemini-redirect-uri-confirm" class="text-sm text-gray-700 dark:text-gray-300">
|
]"
|
||||||
{{ t('admin.accounts.oauth.gemini.confirmRedirectUri') }}
|
>
|
||||||
</label>
|
<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>
|
||||||
<p v-if="geminiRedirectUri && !isValidGeminiRedirectUri" class="mt-2 text-xs text-red-600">
|
|
||||||
{{ t('admin.accounts.oauth.gemini.invalidRedirectUri') }}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<OAuthAuthorizationFlow
|
<OAuthAuthorizationFlow
|
||||||
@@ -121,6 +184,7 @@
|
|||||||
:allow-multiple="false"
|
:allow-multiple="false"
|
||||||
:method-label="t('admin.accounts.inputMethod')"
|
:method-label="t('admin.accounts.inputMethod')"
|
||||||
:platform="isOpenAI ? 'openai' : isGemini ? 'gemini' : 'anthropic'"
|
:platform="isOpenAI ? 'openai' : isGemini ? 'gemini' : 'anthropic'"
|
||||||
|
:show-project-id="isGemini && geminiOAuthType === 'code_assist'"
|
||||||
@generate-url="handleGenerateUrl"
|
@generate-url="handleGenerateUrl"
|
||||||
@cookie-auth="handleCookieAuth"
|
@cookie-auth="handleCookieAuth"
|
||||||
/>
|
/>
|
||||||
@@ -188,6 +252,7 @@ import OAuthAuthorizationFlow from './OAuthAuthorizationFlow.vue'
|
|||||||
interface OAuthFlowExposed {
|
interface OAuthFlowExposed {
|
||||||
authCode: string
|
authCode: string
|
||||||
oauthState: string
|
oauthState: string
|
||||||
|
projectId: string
|
||||||
sessionKey: string
|
sessionKey: string
|
||||||
inputMethod: AuthInputMethod
|
inputMethod: AuthInputMethod
|
||||||
reset: () => void
|
reset: () => void
|
||||||
@@ -217,6 +282,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')
|
||||||
|
|
||||||
// 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')
|
||||||
@@ -263,15 +329,6 @@ const canExchangeCode = computed(() => {
|
|||||||
: isGemini.value
|
: isGemini.value
|
||||||
? geminiOAuth.loading.value
|
? geminiOAuth.loading.value
|
||||||
: claudeOAuth.loading.value
|
: claudeOAuth.loading.value
|
||||||
if (isGemini.value) {
|
|
||||||
return (
|
|
||||||
authCode.trim() &&
|
|
||||||
sessionId &&
|
|
||||||
!loading &&
|
|
||||||
geminiRedirectUriConfirmed.value &&
|
|
||||||
isValidGeminiRedirectUri.value
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return authCode.trim() && sessionId && !loading
|
return authCode.trim() && sessionId && !loading
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -287,6 +344,10 @@ watch(
|
|||||||
) {
|
) {
|
||||||
addMethod.value = props.account.type as AddMethod
|
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 {
|
} else {
|
||||||
resetState()
|
resetState()
|
||||||
}
|
}
|
||||||
@@ -296,11 +357,10 @@ watch(
|
|||||||
// Methods
|
// Methods
|
||||||
const resetState = () => {
|
const resetState = () => {
|
||||||
addMethod.value = 'oauth'
|
addMethod.value = 'oauth'
|
||||||
|
geminiOAuthType.value = 'code_assist'
|
||||||
claudeOAuth.resetState()
|
claudeOAuth.resetState()
|
||||||
openaiOAuth.resetState()
|
openaiOAuth.resetState()
|
||||||
geminiOAuth.resetState()
|
geminiOAuth.resetState()
|
||||||
geminiRedirectUri.value = defaultCallbackUrl
|
|
||||||
geminiRedirectUriConfirmed.value = false
|
|
||||||
oauthFlowRef.value?.reset()
|
oauthFlowRef.value?.reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -314,15 +374,8 @@ const handleGenerateUrl = async () => {
|
|||||||
if (isOpenAI.value) {
|
if (isOpenAI.value) {
|
||||||
await openaiOAuth.generateAuthUrl(props.account.proxy_id)
|
await openaiOAuth.generateAuthUrl(props.account.proxy_id)
|
||||||
} else if (isGemini.value) {
|
} else if (isGemini.value) {
|
||||||
if (!isValidGeminiRedirectUri.value) {
|
const projectId = geminiOAuthType.value === 'code_assist' ? oauthFlowRef.value?.projectId : undefined
|
||||||
appStore.showError(t('admin.accounts.oauth.gemini.invalidRedirectUri'))
|
await geminiOAuth.generateAuthUrl(props.account.proxy_id, projectId, geminiOAuthType.value)
|
||||||
return
|
|
||||||
}
|
|
||||||
if (!geminiRedirectUriConfirmed.value) {
|
|
||||||
appStore.showError(t('admin.accounts.oauth.gemini.redirectUriNotConfirmed'))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
await geminiOAuth.generateAuthUrl(props.account.proxy_id, geminiRedirectUri.value)
|
|
||||||
} else {
|
} else {
|
||||||
await claudeOAuth.generateAuthUrl(addMethod.value, props.account.proxy_id)
|
await claudeOAuth.generateAuthUrl(addMethod.value, props.account.proxy_id)
|
||||||
}
|
}
|
||||||
@@ -372,17 +425,6 @@ const handleExchangeCode = async () => {
|
|||||||
const sessionId = geminiOAuth.sessionId.value
|
const sessionId = geminiOAuth.sessionId.value
|
||||||
if (!sessionId) return
|
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 stateFromInput = oauthFlowRef.value?.oauthState || ''
|
||||||
const stateToUse = stateFromInput || geminiOAuth.state.value
|
const stateToUse = stateFromInput || geminiOAuth.state.value
|
||||||
if (!stateToUse) return
|
if (!stateToUse) return
|
||||||
@@ -391,8 +433,8 @@ const handleExchangeCode = async () => {
|
|||||||
code: authCode.trim(),
|
code: authCode.trim(),
|
||||||
sessionId,
|
sessionId,
|
||||||
state: stateToUse,
|
state: stateToUse,
|
||||||
redirectUri: geminiRedirectUri.value,
|
proxyId: props.account.proxy_id,
|
||||||
proxyId: props.account.proxy_id
|
oauthType: geminiOAuthType.value
|
||||||
})
|
})
|
||||||
if (!tokenInfo) return
|
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) => {
|
const handleCookieAuth = async (sessionKey: string) => {
|
||||||
if (!props.account || isOpenAI.value) return
|
if (!props.account || isOpenAI.value) return
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export interface GeminiTokenInfo {
|
|||||||
scope?: string
|
scope?: string
|
||||||
expires_at?: number | string
|
expires_at?: number | string
|
||||||
project_id?: string
|
project_id?: string
|
||||||
|
oauth_type?: string
|
||||||
[key: string]: unknown
|
[key: string]: unknown
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,7 +34,8 @@ export function useGeminiOAuth() {
|
|||||||
|
|
||||||
const generateAuthUrl = async (
|
const generateAuthUrl = async (
|
||||||
proxyId: number | null | undefined,
|
proxyId: number | null | undefined,
|
||||||
redirectUri: string
|
projectId?: string | null,
|
||||||
|
oauthType?: string
|
||||||
): Promise<boolean> => {
|
): Promise<boolean> => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
authUrl.value = ''
|
authUrl.value = ''
|
||||||
@@ -42,14 +44,11 @@ export function useGeminiOAuth() {
|
|||||||
error.value = ''
|
error.value = ''
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!redirectUri?.trim()) {
|
const payload: Record<string, unknown> = {}
|
||||||
error.value = t('admin.accounts.oauth.gemini.missingRedirectUri')
|
|
||||||
appStore.showError(error.value)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
const payload: Record<string, unknown> = { redirect_uri: redirectUri.trim() }
|
|
||||||
if (proxyId) payload.proxy_id = proxyId
|
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)
|
const response = await adminAPI.gemini.generateAuthUrl(payload as any)
|
||||||
authUrl.value = response.auth_url
|
authUrl.value = response.auth_url
|
||||||
@@ -69,11 +68,11 @@ export function useGeminiOAuth() {
|
|||||||
code: string
|
code: string
|
||||||
sessionId: string
|
sessionId: string
|
||||||
state: string
|
state: string
|
||||||
redirectUri: string
|
|
||||||
proxyId?: number | null
|
proxyId?: number | null
|
||||||
|
oauthType?: string
|
||||||
}): Promise<GeminiTokenInfo | null> => {
|
}): Promise<GeminiTokenInfo | null> => {
|
||||||
const code = params.code?.trim()
|
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')
|
error.value = t('admin.accounts.oauth.gemini.missingExchangeParams')
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -85,10 +84,10 @@ export function useGeminiOAuth() {
|
|||||||
const payload: Record<string, unknown> = {
|
const payload: Record<string, unknown> = {
|
||||||
session_id: params.sessionId,
|
session_id: params.sessionId,
|
||||||
state: params.state,
|
state: params.state,
|
||||||
code,
|
code
|
||||||
redirect_uri: params.redirectUri.trim()
|
|
||||||
}
|
}
|
||||||
if (params.proxyId) payload.proxy_id = params.proxyId
|
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)
|
const tokenInfo = await adminAPI.gemini.exchangeCode(payload as any)
|
||||||
return tokenInfo as GeminiTokenInfo
|
return tokenInfo as GeminiTokenInfo
|
||||||
@@ -115,7 +114,8 @@ export function useGeminiOAuth() {
|
|||||||
token_type: tokenInfo.token_type,
|
token_type: tokenInfo.token_type,
|
||||||
expires_at: expiresAt,
|
expires_at: expiresAt,
|
||||||
scope: tokenInfo.scope,
|
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