在 OpenAI 平台添加独立的"手动输入 Mobile RT"选项,使用 client_id=app_LlGpXReQgckcGGUo2JrYvtJK 刷新 token,与现有 "手动输入 RT"(Codex CLI client_id)互不影响。 共享同一 UI 和批量创建逻辑,通过 clientId 参数区分。 同时修复空名称触发 ent NotEmpty() 校验导致 500 的问题。 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
246 lines
6.5 KiB
TypeScript
246 lines
6.5 KiB
TypeScript
import { ref } from 'vue'
|
||
import { useAppStore } from '@/stores/app'
|
||
import { adminAPI } from '@/api/admin'
|
||
|
||
export interface OpenAITokenInfo {
|
||
access_token?: string
|
||
refresh_token?: string
|
||
client_id?: string
|
||
id_token?: string
|
||
token_type?: string
|
||
expires_in?: number
|
||
expires_at?: number
|
||
scope?: string
|
||
email?: string
|
||
name?: string
|
||
// OpenAI specific IDs (extracted from ID Token)
|
||
chatgpt_account_id?: string
|
||
chatgpt_user_id?: string
|
||
organization_id?: string
|
||
[key: string]: unknown
|
||
}
|
||
|
||
export type OpenAIOAuthPlatform = 'openai' | 'sora'
|
||
|
||
interface UseOpenAIOAuthOptions {
|
||
platform?: OpenAIOAuthPlatform
|
||
}
|
||
|
||
export function useOpenAIOAuth(options?: UseOpenAIOAuthOptions) {
|
||
const appStore = useAppStore()
|
||
const oauthPlatform = options?.platform ?? 'openai'
|
||
const endpointPrefix = oauthPlatform === 'sora' ? '/admin/sora' : '/admin/openai'
|
||
|
||
// State
|
||
const authUrl = ref('')
|
||
const sessionId = ref('')
|
||
const oauthState = ref('')
|
||
const loading = ref(false)
|
||
const error = ref('')
|
||
|
||
// Reset state
|
||
const resetState = () => {
|
||
authUrl.value = ''
|
||
sessionId.value = ''
|
||
oauthState.value = ''
|
||
loading.value = false
|
||
error.value = ''
|
||
}
|
||
|
||
// Generate auth URL for OpenAI OAuth
|
||
const generateAuthUrl = async (
|
||
proxyId?: number | null,
|
||
redirectUri?: string
|
||
): Promise<boolean> => {
|
||
loading.value = true
|
||
authUrl.value = ''
|
||
sessionId.value = ''
|
||
oauthState.value = ''
|
||
error.value = ''
|
||
|
||
try {
|
||
const payload: Record<string, unknown> = {}
|
||
if (proxyId) {
|
||
payload.proxy_id = proxyId
|
||
}
|
||
if (redirectUri) {
|
||
payload.redirect_uri = redirectUri
|
||
}
|
||
|
||
const response = await adminAPI.accounts.generateAuthUrl(
|
||
`${endpointPrefix}/generate-auth-url`,
|
||
payload
|
||
)
|
||
authUrl.value = response.auth_url
|
||
sessionId.value = response.session_id
|
||
try {
|
||
const parsed = new URL(response.auth_url)
|
||
oauthState.value = parsed.searchParams.get('state') || ''
|
||
} catch {
|
||
oauthState.value = ''
|
||
}
|
||
return true
|
||
} catch (err: any) {
|
||
error.value = err.response?.data?.detail || 'Failed to generate OpenAI auth URL'
|
||
appStore.showError(error.value)
|
||
return false
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
// Exchange auth code for tokens
|
||
const exchangeAuthCode = async (
|
||
code: string,
|
||
currentSessionId: string,
|
||
state: string,
|
||
proxyId?: number | null
|
||
): Promise<OpenAITokenInfo | null> => {
|
||
if (!code.trim() || !currentSessionId || !state.trim()) {
|
||
error.value = 'Missing auth code, session ID, or state'
|
||
return null
|
||
}
|
||
|
||
loading.value = true
|
||
error.value = ''
|
||
|
||
try {
|
||
const payload: { session_id: string; code: string; state: string; proxy_id?: number } = {
|
||
session_id: currentSessionId,
|
||
code: code.trim(),
|
||
state: state.trim()
|
||
}
|
||
if (proxyId) {
|
||
payload.proxy_id = proxyId
|
||
}
|
||
|
||
const tokenInfo = await adminAPI.accounts.exchangeCode(`${endpointPrefix}/exchange-code`, payload)
|
||
return tokenInfo as OpenAITokenInfo
|
||
} catch (err: any) {
|
||
error.value = err.response?.data?.detail || 'Failed to exchange OpenAI auth code'
|
||
appStore.showError(error.value)
|
||
return null
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
// Validate refresh token and get full token info
|
||
// clientId: 指定 OAuth client_id(用于第三方渠道获取的 RT,如 app_LlGpXReQgckcGGUo2JrYvtJK)
|
||
const validateRefreshToken = async (
|
||
refreshToken: string,
|
||
proxyId?: number | null,
|
||
clientId?: string
|
||
): Promise<OpenAITokenInfo | null> => {
|
||
if (!refreshToken.trim()) {
|
||
error.value = 'Missing refresh token'
|
||
return null
|
||
}
|
||
|
||
loading.value = true
|
||
error.value = ''
|
||
|
||
try {
|
||
// Use dedicated refresh-token endpoint
|
||
const tokenInfo = await adminAPI.accounts.refreshOpenAIToken(
|
||
refreshToken.trim(),
|
||
proxyId,
|
||
`${endpointPrefix}/refresh-token`,
|
||
clientId
|
||
)
|
||
return tokenInfo as OpenAITokenInfo
|
||
} catch (err: any) {
|
||
error.value = err.response?.data?.detail || err.message || 'Failed to validate refresh token'
|
||
appStore.showError(error.value)
|
||
return null
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
// Validate Sora session token and get access token
|
||
const validateSessionToken = async (
|
||
sessionToken: string,
|
||
proxyId?: number | null
|
||
): Promise<OpenAITokenInfo | null> => {
|
||
if (!sessionToken.trim()) {
|
||
error.value = 'Missing session token'
|
||
return null
|
||
}
|
||
loading.value = true
|
||
error.value = ''
|
||
try {
|
||
const tokenInfo = await adminAPI.accounts.validateSoraSessionToken(
|
||
sessionToken.trim(),
|
||
proxyId,
|
||
`${endpointPrefix}/st2at`
|
||
)
|
||
return tokenInfo as OpenAITokenInfo
|
||
} catch (err: any) {
|
||
error.value = err.response?.data?.detail || 'Failed to validate session token'
|
||
appStore.showError(error.value)
|
||
return null
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
// Build credentials for OpenAI OAuth account
|
||
const buildCredentials = (tokenInfo: OpenAITokenInfo): Record<string, unknown> => {
|
||
const creds: Record<string, unknown> = {
|
||
access_token: tokenInfo.access_token,
|
||
refresh_token: tokenInfo.refresh_token,
|
||
token_type: tokenInfo.token_type,
|
||
expires_in: tokenInfo.expires_in,
|
||
expires_at: tokenInfo.expires_at,
|
||
scope: tokenInfo.scope
|
||
}
|
||
|
||
if (tokenInfo.client_id) {
|
||
creds.client_id = tokenInfo.client_id
|
||
}
|
||
|
||
// Include OpenAI specific IDs (required for forwarding)
|
||
if (tokenInfo.chatgpt_account_id) {
|
||
creds.chatgpt_account_id = tokenInfo.chatgpt_account_id
|
||
}
|
||
if (tokenInfo.chatgpt_user_id) {
|
||
creds.chatgpt_user_id = tokenInfo.chatgpt_user_id
|
||
}
|
||
if (tokenInfo.organization_id) {
|
||
creds.organization_id = tokenInfo.organization_id
|
||
}
|
||
|
||
return creds
|
||
}
|
||
|
||
// Build extra info from token response
|
||
const buildExtraInfo = (tokenInfo: OpenAITokenInfo): Record<string, string> | undefined => {
|
||
const extra: Record<string, string> = {}
|
||
if (tokenInfo.email) {
|
||
extra.email = tokenInfo.email
|
||
}
|
||
if (tokenInfo.name) {
|
||
extra.name = tokenInfo.name
|
||
}
|
||
return Object.keys(extra).length > 0 ? extra : undefined
|
||
}
|
||
|
||
return {
|
||
// State
|
||
authUrl,
|
||
sessionId,
|
||
oauthState,
|
||
loading,
|
||
error,
|
||
// Methods
|
||
resetState,
|
||
generateAuthUrl,
|
||
exchangeAuthCode,
|
||
validateRefreshToken,
|
||
validateSessionToken,
|
||
buildCredentials,
|
||
buildExtraInfo
|
||
}
|
||
}
|