Files
sub2api-ht/frontend/src/composables/useOpenAIOAuth.ts
QTom 9f8cffe887 feat(openai): 新增"手动输入 Mobile RT"入口,使用 SoraClientID 刷新
在 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>
2026-03-24 14:50:03 +08:00

246 lines
6.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}
}