- 新增 /admin/gemini API 接口封装(generateAuthUrl, exchangeCode) - 新增 useGeminiOAuth composable 处理 Gemini OAuth 流程 - 新增 OAuthCallbackView 视图用于接收 OAuth 回调 - 支持 code/state 参数提取和 credentials 构建
134 lines
3.6 KiB
TypeScript
134 lines
3.6 KiB
TypeScript
import { ref } from 'vue'
|
|
import { useI18n } from 'vue-i18n'
|
|
import { useAppStore } from '@/stores/app'
|
|
import { adminAPI } from '@/api/admin'
|
|
|
|
export interface GeminiTokenInfo {
|
|
access_token?: string
|
|
refresh_token?: string
|
|
token_type?: string
|
|
scope?: string
|
|
expires_at?: number | string
|
|
project_id?: string
|
|
[key: string]: unknown
|
|
}
|
|
|
|
export function useGeminiOAuth() {
|
|
const appStore = useAppStore()
|
|
const { t } = useI18n()
|
|
|
|
const authUrl = ref('')
|
|
const sessionId = ref('')
|
|
const state = ref('')
|
|
const loading = ref(false)
|
|
const error = ref('')
|
|
|
|
const resetState = () => {
|
|
authUrl.value = ''
|
|
sessionId.value = ''
|
|
state.value = ''
|
|
loading.value = false
|
|
error.value = ''
|
|
}
|
|
|
|
const generateAuthUrl = async (
|
|
proxyId: number | null | undefined,
|
|
redirectUri: string
|
|
): Promise<boolean> => {
|
|
loading.value = true
|
|
authUrl.value = ''
|
|
sessionId.value = ''
|
|
state.value = ''
|
|
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() }
|
|
if (proxyId) payload.proxy_id = proxyId
|
|
|
|
const response = await adminAPI.gemini.generateAuthUrl(payload as any)
|
|
authUrl.value = response.auth_url
|
|
sessionId.value = response.session_id
|
|
state.value = response.state
|
|
return true
|
|
} catch (err: any) {
|
|
error.value = err.response?.data?.detail || t('admin.accounts.oauth.gemini.failedToGenerateUrl')
|
|
appStore.showError(error.value)
|
|
return false
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
const exchangeAuthCode = async (params: {
|
|
code: string
|
|
sessionId: string
|
|
state: string
|
|
redirectUri: string
|
|
proxyId?: number | null
|
|
}): Promise<GeminiTokenInfo | null> => {
|
|
const code = params.code?.trim()
|
|
if (!code || !params.sessionId || !params.state || !params.redirectUri?.trim()) {
|
|
error.value = t('admin.accounts.oauth.gemini.missingExchangeParams')
|
|
return null
|
|
}
|
|
|
|
loading.value = true
|
|
error.value = ''
|
|
|
|
try {
|
|
const payload: Record<string, unknown> = {
|
|
session_id: params.sessionId,
|
|
state: params.state,
|
|
code,
|
|
redirect_uri: params.redirectUri.trim()
|
|
}
|
|
if (params.proxyId) payload.proxy_id = params.proxyId
|
|
|
|
const tokenInfo = await adminAPI.gemini.exchangeCode(payload as any)
|
|
return tokenInfo as GeminiTokenInfo
|
|
} catch (err: any) {
|
|
error.value = err.response?.data?.detail || t('admin.accounts.oauth.gemini.failedToExchangeCode')
|
|
appStore.showError(error.value)
|
|
return null
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
const buildCredentials = (tokenInfo: GeminiTokenInfo): Record<string, unknown> => {
|
|
let expiresAt: string | undefined
|
|
if (typeof tokenInfo.expires_at === 'number' && Number.isFinite(tokenInfo.expires_at)) {
|
|
expiresAt = Math.floor(tokenInfo.expires_at).toString()
|
|
} else if (typeof tokenInfo.expires_at === 'string' && tokenInfo.expires_at.trim()) {
|
|
expiresAt = tokenInfo.expires_at.trim()
|
|
}
|
|
|
|
return {
|
|
access_token: tokenInfo.access_token,
|
|
refresh_token: tokenInfo.refresh_token,
|
|
token_type: tokenInfo.token_type,
|
|
expires_at: expiresAt,
|
|
scope: tokenInfo.scope,
|
|
project_id: tokenInfo.project_id
|
|
}
|
|
}
|
|
|
|
return {
|
|
authUrl,
|
|
sessionId,
|
|
state,
|
|
loading,
|
|
error,
|
|
resetState,
|
|
generateAuthUrl,
|
|
exchangeAuthCode,
|
|
buildCredentials
|
|
}
|
|
}
|