import { useCallback, useEffect, useMemo, useRef, useState, type ChangeEvent, type DragEvent, type ReactNode, } from 'react'; import { createPortal } from 'react-dom'; import { useLocation } from 'react-router-dom'; import CenteredModal from '../components/CenteredModal.js'; import ResponsiveBatchActionBar from '../components/ResponsiveBatchActionBar.js'; import ResponsiveFilterPanel from '../components/ResponsiveFilterPanel.js'; import { MobileCard, MobileField } from '../components/MobileCard.js'; import ModernSelect from '../components/ModernSelect.js'; import { useToast } from '../components/Toast.js'; import { useAnimatedVisibility } from '../components/useAnimatedVisibility.js'; import { useIsMobile } from '../components/useIsMobile.js'; import OAuthModelsModal, { type OAuthModelItem } from './oauth/OAuthModelsModal.js'; import { api, type OAuthConnectionInfo, type OAuthProviderInfo, type OAuthRouteParticipation, type OAuthRouteUnitStrategy, type OAuthQuotaInfo, type OAuthQuotaWindowInfo, type OAuthStartInstructions, } from '../api.js'; const POLL_INTERVAL_MS = 1500; const CONNECTION_PAGE_LIMIT = 200; const AUTO_REFRESH_OPTIONS = [0, 5, 10, 15, 30] as const; type ActiveSession = { provider: string; state: string; authorizationUrl: string; instructions: OAuthStartInstructions; }; type DrawerIntent = | { mode: 'create'; provider?: string } | { mode: 'rebind'; account: OAuthConnectionInfo } | { mode: 'proxy'; account: OAuthConnectionInfo }; type ColumnKey = 'identity' | 'site' | 'status' | 'quota' | 'proxy'; type OAuthImportFileLike = { name?: string; text?: () => Promise; }; type OAuthImportDraft = { sourceName: string; rawText: string; error?: string; }; type OAuthImportSource = { sourceName: string; rawText: string; error?: string; }; type OAuthImportPreview = { sourceName: string; valid: boolean; providerLabel?: string; email?: string; accountKey?: string; expiresLabel?: string; disabled?: boolean; error?: string; parsedData?: Record; }; type OAuthImportPreviewSummary = { totalCount: number; validCount: number; invalidCount: number; canImport: boolean; items: OAuthImportPreview[]; }; type OAuthModelsModalState = { open: boolean; loading: boolean; refreshing: boolean; connection: OAuthConnectionInfo | null; models: OAuthModelItem[]; totalCount: number; disabledCount: number; siteName: string; }; type OAuthRouteUnitModalState = { open: boolean; name: string; strategy: OAuthRouteUnitStrategy; }; type SessionRouteUnitFeedback = { action: 'created' | 'deleted'; name: string; memberCount: number; strategy: OAuthRouteUnitStrategy; }; type SessionFeedback = { message: string; tone: 'info' | 'success' | 'error'; routeUnit?: SessionRouteUnitFeedback | null; }; const COLUMN_OPTIONS: Array<{ key: ColumnKey; label: string }> = [ { key: 'identity', label: '账号 / Provider' }, { key: 'site', label: '站点' }, { key: 'status', label: '运行状态' }, { key: 'quota', label: 'Usage / Quota' }, { key: 'proxy', label: '代理 / 项目' }, ]; function openOAuthPopup(provider: string, authorizationUrl: string) { if (typeof window === 'undefined' || typeof window.open !== 'function') return; const popup = window.open( authorizationUrl, `oauth-${provider}`, 'popup=yes,width=540,height=760,resizable=yes,scrollbars=yes,noopener,noreferrer', ); if (popup) { try { popup.opener = null; } catch { // Ignore cross-window opener hardening failures. } } if (popup && typeof popup.focus === 'function') { popup.focus(); } } function asTrimmedString(value: string | null | undefined): string { return typeof value === 'string' ? value.trim() : ''; } function normalizeOauthMessage(value: string | null | undefined): string { const text = asTrimmedString(value); if (!text) return ''; return text .replace(/codex usage windows inferred from rate limit response headers/ig, '额度窗口已从响应头推断') .replace(/official 5h quota window is not exposed by current codex oauth artifacts/ig, '当前 Codex OAuth 未暴露官方 5h 窗口') .replace(/official 7d quota window is not exposed by current codex oauth artifacts/ig, '当前 Codex OAuth 未暴露官方 7d 窗口') .replace(/official 5h quota window is unavailable for this provider/ig, '当前 Provider 不提供官方 5h 窗口') .replace(/official 7d quota window is unavailable for this provider/ig, '当前 Provider 不提供官方 7d 窗口') .replace(/\bfetch failed\b/ig, '网络请求失败'); } function listImportFiles(files: ArrayLike | null | undefined): OAuthImportFileLike[] { return files ? Array.from(files) : []; } async function readOauthImportDrafts( files: ArrayLike | null | undefined, ): Promise { const nextFiles = listImportFiles(files); return Promise.all(nextFiles.map(async (file, index) => { const sourceName = asTrimmedString(file.name) || `oauth-import-${index + 1}.json`; if (typeof file.text !== 'function') { return { sourceName, rawText: '', error: '当前浏览器不支持读取该文件', }; } try { return { sourceName, rawText: await file.text(), }; } catch (error: any) { return { sourceName, rawText: '', error: error?.message || '读取文件失败', }; } })); } function decodeJwtPayload(token?: string): Record | null { if (typeof token !== 'string' || !token.trim()) return null; const parts = token.split('.'); if (parts.length !== 3) return null; const raw = (parts[1] || '').replace(/-/g, '+').replace(/_/g, '/'); const padded = raw + '='.repeat((4 - (raw.length % 4 || 4)) % 4); try { const decoded = typeof window !== 'undefined' && typeof window.atob === 'function' ? window.atob(padded) : ''; const parsed = JSON.parse(decoded) as unknown; return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed as Record : null; } catch { return null; } } function resolveImportProviderLabel(value: string): string | null { const normalized = value.trim().toLowerCase(); if (normalized === 'codex' || normalized === 'openai') return 'Codex'; if (normalized === 'claude' || normalized === 'anthropic') return 'Claude'; if (normalized === 'gemini-cli' || normalized === 'gemini') return 'Gemini CLI'; if (normalized === 'antigravity') return 'Antigravity'; return null; } function resolveImportPreviewExpiryLabel(value: unknown): string | undefined { if (value === undefined || value === null || value === '') return undefined; const parsed = typeof value === 'number' ? value : (typeof value === 'string' && /^\d+$/.test(value.trim())) ? Number.parseInt(value.trim(), 10) : Date.parse(typeof value === 'string' ? value : ''); if (!Number.isFinite(parsed) || parsed <= 0) { throw new Error('expired 时间格式无效'); } return new Date(parsed).toLocaleString(); } function parseOauthImportPreview(source: OAuthImportSource): OAuthImportPreview { if (source.error) { return { sourceName: source.sourceName, valid: false, error: source.error, }; } const raw = source.rawText.trim(); if (!raw) { return { sourceName: source.sourceName, valid: false, error: 'JSON 内容为空', }; } let parsed: unknown; try { parsed = JSON.parse(raw); } catch { return { sourceName: source.sourceName, valid: false, error: 'JSON 解析失败', }; } if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) { return { sourceName: source.sourceName, valid: false, error: '需要单个 OAuth JSON 对象', }; } const payload = parsed as Record; const type = asTrimmedString(typeof payload.type === 'string' ? payload.type : ''); if ( type === 'sub2api-data' || type === 'sub2api-bundle' || 'accounts' in payload || 'proxies' in payload || 'version' in payload || 'exported_at' in payload ) { return { sourceName: source.sourceName, valid: false, error: '这是旧的 sub2api 导出格式', }; } const providerLabel = type ? resolveImportProviderLabel(type) : null; if (!providerLabel) { return { sourceName: source.sourceName, valid: false, error: `不支持的 OAuth 类型:${type || '未知'}`, }; } if (!asTrimmedString(typeof payload.access_token === 'string' ? payload.access_token : '')) { return { sourceName: source.sourceName, valid: false, error: '缺少 access_token', }; } try { const claims = decodeJwtPayload(typeof payload.id_token === 'string' ? payload.id_token : undefined); const authClaims = claims?.['https://api.openai.com/auth']; const authRecord = authClaims && typeof authClaims === 'object' && !Array.isArray(authClaims) ? authClaims as Record : null; return { sourceName: source.sourceName, valid: true, providerLabel, email: asTrimmedString(typeof payload.email === 'string' ? payload.email : '') || asTrimmedString(typeof claims?.email === 'string' ? claims.email : ''), accountKey: asTrimmedString(typeof payload.account_key === 'string' ? payload.account_key : '') || asTrimmedString(typeof payload.account_id === 'string' ? payload.account_id : '') || asTrimmedString(typeof authRecord?.chatgpt_account_id === 'string' ? authRecord.chatgpt_account_id : ''), expiresLabel: resolveImportPreviewExpiryLabel(payload.expired), disabled: payload.disabled === true, parsedData: payload, }; } catch (error: any) { return { sourceName: source.sourceName, valid: false, error: error?.message || 'JSON 结构无效', }; } } function resolveConnectionPrimaryTitle(connection: OAuthConnectionInfo): string { return asTrimmedString(connection.username) || asTrimmedString(connection.email) || asTrimmedString(connection.accountKey) || asTrimmedString(connection.provider) || 'OAuth 连接'; } function resolveConnectionEmailLabel(connection: OAuthConnectionInfo): string { return asTrimmedString(connection.email); } function resolveConnectionStatusLabel(status?: string): string { return status === 'abnormal' ? '异常' : '正常'; } function resolveQuotaStatusLabel(status?: OAuthQuotaInfo['status']): string { if (status === 'unsupported') return '不支持'; if (status === 'error') return '获取失败'; return '支持'; } function resolveQuotaSourceLabel(source?: OAuthQuotaInfo['source']): string { return source === 'official' ? '官方' : '响应头推断'; } function resolveModelSyncStatusText(connection: OAuthConnectionInfo): string { const failureText = normalizeOauthMessage(connection.lastModelSyncError || ''); if (failureText) return '获取失败'; return connection.lastModelSyncAt ? '同步正常' : '未同步'; } function resolveQuotaSyncStatusText(quota?: OAuthQuotaInfo | null): string { if (!quota) return '未刷新额度'; if (quota.status === 'error') { return '获取失败'; } if (quota.status === 'unsupported') { return '不支持'; } return quota.lastSyncAt ? '同步正常' : '未刷新'; } function resolveModelSyncDetail(connection: OAuthConnectionInfo): string { return normalizeOauthMessage(connection.lastModelSyncError || ''); } function resolveQuotaSyncDetail(quota?: OAuthQuotaInfo | null): string { if (!quota) return ''; if (quota.status === 'error') { return normalizeOauthMessage(quota.lastError || quota.providerMessage || '额度刷新失败'); } if (quota.status === 'unsupported') { return normalizeOauthMessage(quota.providerMessage || '当前连接暂不支持额度窗口'); } return ''; } function redactProxyUrl(value: string | null | undefined): string { const text = asTrimmedString(value); if (!text) return ''; try { const parsed = new URL(text); if (parsed.username || parsed.password) { parsed.username = '***'; parsed.password = ''; } const serialized = parsed.toString(); return parsed.pathname === '/' && !parsed.search && !parsed.hash ? serialized.replace(/\/$/, '') : serialized; } catch { return text.replace(/\/\/[^/@:\s]+(?::[^/@\s]*)?@/, '//***@'); } } function compactAccountKey(value?: string | null): string { const text = asTrimmedString(value || ''); if (!text || text.length <= 24) return text; return `${text.slice(0, 8)}...${text.slice(-6)}`; } function formatResetLabel(value?: string | null): string { const text = asTrimmedString(value || ''); if (!text) return ''; const date = new Date(text); if (Number.isNaN(date.getTime())) return text; const diffMs = date.getTime() - Date.now(); if (diffMs <= 0) return '现在'; const diffHours = Math.floor(diffMs / (1000 * 60 * 60)); const diffMinutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60)); if (diffHours >= 24) { const days = Math.floor(diffHours / 24); return `${days}d ${diffHours % 24}h`; } if (diffHours > 0) return `${diffHours}h ${diffMinutes}m`; return `${Math.max(1, diffMinutes)}m`; } function resolveQuotaWindowPercent(window?: OAuthQuotaWindowInfo | null): number | null { if (!window?.supported) return null; if (typeof window.used === 'number' && typeof window.limit === 'number' && window.limit > 0) { return Math.max(0, Math.min(100, Math.round((window.used / window.limit) * 100))); } if (typeof window.remaining === 'number' && typeof window.limit === 'number' && window.limit > 0) { return Math.max(0, Math.min(100, Math.round(((window.limit - window.remaining) / window.limit) * 100))); } return null; } function resolveQuotaWindowSummary(window?: OAuthQuotaWindowInfo | null): string { if (!window || !window.supported) return ''; if (typeof window.used === 'number' && typeof window.limit === 'number') { return `${window.used} / ${window.limit}`; } if (typeof window.remaining === 'number' && typeof window.limit === 'number') { return `剩余 ${window.remaining} / ${window.limit}`; } if (typeof window.limit === 'number') return `总量 ${window.limit}`; return window.message || '官方未提供'; } function resolveProxyProjectSummary(connection: OAuthConnectionInfo): string { const parts = [ asTrimmedString(connection.planType || ''), connection.projectId ? `Project ${connection.projectId}` : '', ].filter(Boolean); return parts.join(' · ') || '--'; } function resolveProxyDisplayText(connection: OAuthConnectionInfo): string { if (connection.useSystemProxy) return '系统级代理'; if (connection.proxyUrl) return redactProxyUrl(connection.proxyUrl); return '未设置代理'; } function hasOauthProxySelection(connection: OAuthConnectionInfo): boolean { return !!connection.useSystemProxy || !!asTrimmedString(connection.proxyUrl); } function resolveRouteUnitStrategyLabel(strategy?: OAuthRouteUnitStrategy | null): string { return strategy === 'stick_until_unavailable' ? '单个用到不可用再切' : '轮询'; } function resolveConnectionRouteParticipation( connection: OAuthConnectionInfo, ): OAuthRouteParticipation { if (connection.routeParticipation?.kind === 'route_unit') { return { ...connection.routeParticipation, id: connection.routeParticipation.id ?? connection.routeUnit?.id, routeUnitId: connection.routeParticipation.routeUnitId ?? connection.routeParticipation.id ?? connection.routeUnit?.routeUnitId ?? connection.routeUnit?.id, }; } if (connection.routeParticipation?.kind === 'single') { return connection.routeParticipation; } if (connection.routeUnit) { return { kind: 'route_unit', routeUnitId: connection.routeUnit.routeUnitId ?? connection.routeUnit.id, id: connection.routeUnit.id, name: connection.routeUnit.name, strategy: connection.routeUnit.strategy, memberCount: connection.routeUnit.memberCount, }; } return { kind: 'single' }; } function resolveRouteParticipationSummary(connection: OAuthConnectionInfo): string { const participation = resolveConnectionRouteParticipation(connection); if (participation.kind !== 'route_unit') { return '单体'; } return `路由池:${participation.name} · ${participation.memberCount} 个成员 · ${resolveRouteUnitStrategyLabel(participation.strategy)}`; } function renderCodeBlock(value: string) { return ( {value} ); } function renderGuideCard(title: string, description: string, children?: ReactNode) { return (
{title}
{description}
{children}
); } function QuotaWindowRow({ label, window, }: { label: string; window?: OAuthQuotaWindowInfo | null; }) { const percent = resolveQuotaWindowPercent(window); const summary = resolveQuotaWindowSummary(window); const tone = percent != null && percent >= 90 ? 'var(--color-danger)' : percent != null && percent >= 70 ? 'var(--color-warning)' : 'var(--color-primary)'; return (
{label}
{percent == null ? 'N/A' : `${percent}%`} {summary && percent == null ? {summary} : null} {window?.resetAt ? ( 重置 {formatResetLabel(window.resetAt)} ) : null}
); } function SideDrawer({ open, onClose, title, children, }: { open: boolean; onClose: () => void; title: ReactNode; children: ReactNode; }) { const presence = useAnimatedVisibility(open, 220); useEffect(() => { if (!open || typeof document === 'undefined') return; const previousOverflow = document.body.style.overflow; document.body.style.overflow = 'hidden'; return () => { document.body.style.overflow = previousOverflow; }; }, [open]); if (!presence.shouldRender) return null; const panel = (
event.stopPropagation()} >
{title}
{children}
); const portalTarget = typeof document !== 'undefined' ? document.body : null; return portalTarget ? createPortal(panel, portalTarget) : panel; } export default function OAuthManagement() { const location = useLocation(); const isMobile = useIsMobile(); const toast = useToast(); const createIntentHandledRef = useRef(false); const modelsModalRequestSeqRef = useRef(0); const [providers, setProviders] = useState([]); const [connections, setConnections] = useState([]); const [loaded, setLoaded] = useState(false); const [sessionFeedback, setSessionFeedback] = useState(null); const [actionLoadingKey, setActionLoadingKey] = useState(''); const [selectedConnectionIds, setSelectedConnectionIds] = useState([]); const [showMobileFilters, setShowMobileFilters] = useState(false); const [searchQuery, setSearchQuery] = useState(''); const [providerFilter, setProviderFilter] = useState(''); const [statusFilter, setStatusFilter] = useState(''); const [siteFilter, setSiteFilter] = useState(''); const [visibleColumns, setVisibleColumns] = useState>({ identity: true, site: true, status: true, quota: true, proxy: true, }); const [showColumnMenu, setShowColumnMenu] = useState(false); const [autoRefreshSeconds, setAutoRefreshSeconds] = useState(0); const [autoRefreshCountdown, setAutoRefreshCountdown] = useState(0); const [runtimeSystemProxyConfigured, setRuntimeSystemProxyConfigured] = useState(false); const [importOpen, setImportOpen] = useState(false); const importFileInputRef = useRef(null); const [importJsonText, setImportJsonText] = useState(''); const [importDrafts, setImportDrafts] = useState([]); const [importDragOver, setImportDragOver] = useState(false); const [importing, setImporting] = useState(false); const [importCustomProxyEnabled, setImportCustomProxyEnabled] = useState(false); const [importSystemProxyEnabled, setImportSystemProxyEnabled] = useState(false); const [importProxyUrl, setImportProxyUrl] = useState(''); const [drawerOpen, setDrawerOpen] = useState(false); const [drawerIntent, setDrawerIntent] = useState({ mode: 'create' }); const [selectedProviderKey, setSelectedProviderKey] = useState(''); const [drawerProjectId, setDrawerProjectId] = useState(''); const [activeSession, setActiveSession] = useState(null); const [manualCallbackVisible, setManualCallbackVisible] = useState(false); const [manualCallbackUrl, setManualCallbackUrl] = useState(''); const [manualCallbackSubmitting, setManualCallbackSubmitting] = useState(false); const [oauthCustomProxyEnabled, setOauthCustomProxyEnabled] = useState(false); const [oauthSystemProxyEnabled, setOauthSystemProxyEnabled] = useState(false); const [oauthProxyUrl, setOauthProxyUrl] = useState(''); const [modelsModal, setModelsModal] = useState({ open: false, loading: false, refreshing: false, connection: null, models: [], totalCount: 0, disabledCount: 0, siteName: '', }); const [routeUnitModal, setRouteUnitModal] = useState({ open: false, name: '', strategy: 'round_robin', }); const setSessionMessage = useCallback(( message: string, tone: SessionFeedback['tone'], options?: { routeUnit?: SessionRouteUnitFeedback | null; }, ) => { setSessionFeedback({ message, tone, routeUnit: options?.routeUnit ?? null, }); }, []); const setSessionInfo = useCallback(( message: string, options?: { routeUnit?: SessionRouteUnitFeedback | null; }, ) => { setSessionMessage(message, 'info', options); }, [setSessionMessage]); const setSessionSuccess = useCallback(( message: string, options?: { routeUnit?: SessionRouteUnitFeedback | null; }, ) => { setSessionMessage(message, 'success', options); }, [setSessionMessage]); const setSessionError = useCallback(( message: string, options?: { routeUnit?: SessionRouteUnitFeedback | null; }, ) => { setSessionMessage(message, 'error', options); }, [setSessionMessage]); const resetOauthProxySettings = useCallback(() => { setOauthCustomProxyEnabled(false); setOauthSystemProxyEnabled(false); setOauthProxyUrl(''); }, []); const resetImportProxySettings = useCallback((defaultToSystem = false) => { setImportCustomProxyEnabled(false); setImportSystemProxyEnabled(defaultToSystem); setImportProxyUrl(''); }, []); const resetImportState = useCallback(() => { setImportJsonText(''); setImportDrafts([]); setImportDragOver(false); }, []); const closeImportModal = useCallback(() => { setImportOpen(false); resetImportState(); resetImportProxySettings(false); }, [resetImportProxySettings, resetImportState]); const openImportModal = useCallback(() => { resetImportState(); resetImportProxySettings(runtimeSystemProxyConfigured); setImportOpen(true); }, [resetImportProxySettings, resetImportState, runtimeSystemProxyConfigured]); const loadConnections = useCallback(async () => { const response = await api.getOAuthConnections({ limit: CONNECTION_PAGE_LIMIT, offset: 0, }); const nextItems = Array.isArray(response?.items) ? response.items : []; setConnections(nextItems); setSelectedConnectionIds((current) => current.filter((id) => nextItems.some((item) => item.accountId === id))); return nextItems; }, []); const load = useCallback(async () => { try { const [providersResponse] = await Promise.all([ api.getOAuthProviders(), loadConnections(), ]); const nextProviders = Array.isArray(providersResponse?.providers) ? providersResponse.providers : []; setRuntimeSystemProxyConfigured(providersResponse?.defaults?.systemProxyConfigured === true); setProviders(nextProviders); setSelectedProviderKey((current) => current || nextProviders[0]?.provider || ''); } catch (error: any) { console.error('failed to load oauth management data', error); setSessionError(error?.message || 'OAuth 管理数据加载失败'); } finally { setLoaded(true); } }, [loadConnections]); useEffect(() => { void load(); }, [load]); useEffect(() => { if (autoRefreshSeconds <= 0) { setAutoRefreshCountdown(0); return undefined; } setAutoRefreshCountdown(autoRefreshSeconds); const timer = setInterval(() => { setAutoRefreshCountdown((current) => { if (current <= 1) { void loadConnections().catch((error: any) => { setSessionError(error?.message || 'OAuth 连接列表刷新失败'); }); return autoRefreshSeconds; } return current - 1; }); }, 1000); return () => clearInterval(timer); }, [autoRefreshSeconds, loadConnections]); useEffect(() => { if (!loaded || providers.length === 0 || createIntentHandledRef.current) return; const params = new URLSearchParams(location.search); if (params.get('create') !== '1') return; createIntentHandledRef.current = true; const provider = asTrimmedString(params.get('provider')) || providers[0]?.provider || ''; setDrawerIntent({ mode: 'create', provider }); setSelectedProviderKey(provider); setDrawerProjectId(''); setDrawerOpen(true); setSessionInfo('从建站流程跳转到 OAuth 管理,请在这里完成授权。'); }, [loaded, location.search, providers]); useEffect(() => { if (!activeSession) return undefined; let cancelled = false; let timer: ReturnType | null = null; const poll = async () => { try { const session = await api.getOAuthSession(activeSession.state); if (cancelled) return; if (session.status === 'pending') { setSessionInfo('等待授权完成'); timer = setTimeout(poll, POLL_INTERVAL_MS); return; } if (session.status === 'success') { setSessionSuccess('授权成功'); await loadConnections(); setActiveSession(null); return; } setSessionError(`授权失败:${session.error || '未知错误'}`); setActiveSession(null); } catch (error: any) { if (cancelled) return; setSessionError(error?.message || 'OAuth 会话状态查询失败'); setActiveSession(null); } }; void poll(); return () => { cancelled = true; if (timer) clearTimeout(timer); }; }, [activeSession, loadConnections]); useEffect(() => { if (!activeSession) { setManualCallbackVisible(false); setManualCallbackUrl(''); setManualCallbackSubmitting(false); return undefined; } setManualCallbackVisible(false); setManualCallbackUrl(''); setManualCallbackSubmitting(false); const timer = setTimeout(() => { setManualCallbackVisible(true); }, Math.max(0, activeSession.instructions.manualCallbackDelayMs || 0)); return () => clearTimeout(timer); }, [activeSession]); const providerOptions = useMemo( () => providers.map((provider) => ({ value: provider.provider, label: provider.label, description: `${provider.platform}${provider.requiresProjectId ? ' · 可选 Project ID' : ''}${!provider.enabled ? ' · 当前环境未启用' : ''}`, })), [providers], ); const selectedProvider = useMemo( () => providers.find((provider) => provider.provider === selectedProviderKey) || null, [providers, selectedProviderKey], ); const siteOptions = useMemo(() => { const seen = new Map(); connections.forEach((connection) => { const id = String(connection.site?.id || ''); const label = asTrimmedString(connection.site?.name) || asTrimmedString(connection.site?.url); if (id && label && !seen.has(id)) { seen.set(id, label); } }); return Array.from(seen.entries()).map(([value, label]) => ({ value, label })); }, [connections]); const filteredConnections = useMemo(() => { const search = searchQuery.trim().toLowerCase(); return connections.filter((connection) => { if (providerFilter && connection.provider !== providerFilter) return false; if (statusFilter && connection.status !== statusFilter) return false; if (siteFilter && String(connection.site?.id || '') !== siteFilter) return false; if (!search) return true; const haystack = [ resolveConnectionPrimaryTitle(connection), resolveConnectionEmailLabel(connection), connection.provider, connection.site?.name, connection.site?.url, connection.accountKey, connection.projectId, connection.modelsPreview.join(' '), ].join(' ').toLowerCase(); return haystack.includes(search); }); }, [connections, providerFilter, searchQuery, siteFilter, statusFilter]); const allVisibleSelected = filteredConnections.length > 0 && filteredConnections.every((connection) => selectedConnectionIds.includes(connection.accountId)); const selectedConnections = useMemo( () => connections.filter((connection) => selectedConnectionIds.includes(connection.accountId)), [connections, selectedConnectionIds], ); const selectedRouteUnitParticipation = useMemo(() => { if (selectedConnections.length <= 0) return null; const participations = selectedConnections.map(resolveConnectionRouteParticipation); if (participations.some((item) => item.kind !== 'route_unit')) return null; const first = participations[0]; if (!first || first.kind !== 'route_unit') return null; const routeUnitId = first.routeUnitId ?? first.id; if (!routeUnitId) return null; const allSameRouteUnit = participations.every((item) => ( item.kind === 'route_unit' && (item.routeUnitId ?? item.id) === routeUnitId )); if (!allSameRouteUnit) return null; const totalRouteUnitMembers = connections.filter((connection) => { const participation = resolveConnectionRouteParticipation(connection); return participation.kind === 'route_unit' && (participation.routeUnitId ?? participation.id) === routeUnitId; }).length; if (totalRouteUnitMembers !== selectedConnections.length) return null; return { ...first, memberCount: Math.max(first.memberCount, totalRouteUnitMembers), }; }, [connections, selectedConnections]); const canMergeSelectedIntoRouteUnit = useMemo(() => { if (selectedConnections.length < 2) return false; const first = selectedConnections[0]; if (!first) return false; const firstSiteId = first.siteId; const firstProvider = first.provider; return selectedConnections.every((connection) => ( connection.siteId === firstSiteId && connection.provider === firstProvider && resolveConnectionRouteParticipation(connection).kind === 'single' )); }, [selectedConnections]); const canSplitSelectedRouteUnit = selectedRouteUnitParticipation != null; const openCreateDrawer = (provider?: string) => { setDrawerIntent({ mode: 'create', provider }); setSelectedProviderKey(provider || providers[0]?.provider || ''); setDrawerProjectId(''); resetOauthProxySettings(); setDrawerOpen(true); setShowColumnMenu(false); }; const openRebindDrawer = (connection: OAuthConnectionInfo) => { setDrawerIntent({ mode: 'rebind', account: connection }); setSelectedProviderKey(connection.provider); setDrawerProjectId(connection.projectId || ''); resetOauthProxySettings(); setDrawerOpen(true); setShowColumnMenu(false); }; const openProxySettingsDrawer = (connection: OAuthConnectionInfo) => { setDrawerIntent({ mode: 'proxy', account: connection }); setSelectedProviderKey(connection.provider); setDrawerProjectId(connection.projectId || ''); setOauthSystemProxyEnabled(connection.useSystemProxy === true); setOauthCustomProxyEnabled(connection.useSystemProxy !== true && !!asTrimmedString(connection.proxyUrl)); setOauthProxyUrl(connection.useSystemProxy ? '' : asTrimmedString(connection.proxyUrl)); setDrawerOpen(true); setShowColumnMenu(false); setSessionInfo('已打开 OAuth 代理设置,修改后可直接保存代理,或保存后重新授权。'); }; const openRouteUnitModal = () => { setRouteUnitModal({ open: true, name: '', strategy: 'round_robin', }); }; const closeRouteUnitModal = () => { setRouteUnitModal((current) => ({ ...current, open: false, })); }; const resolveProxySettingsPayload = ({ customEnabled, systemEnabled, proxyValue, fallbackAccount, clearToSiteFallback = false, }: { customEnabled: boolean; systemEnabled: boolean; proxyValue: string; fallbackAccount?: OAuthConnectionInfo | null; clearToSiteFallback?: boolean; }): { proxyUrl?: string | null; useSystemProxy?: boolean } => { const customProxyUrl = asTrimmedString(proxyValue); if (customEnabled) { return { proxyUrl: customProxyUrl, useSystemProxy: false, }; } if (systemEnabled) { return { proxyUrl: null, useSystemProxy: true, }; } if (clearToSiteFallback) { return { proxyUrl: null, useSystemProxy: false, }; } if (fallbackAccount?.useSystemProxy) { return { proxyUrl: null, useSystemProxy: true, }; } if (fallbackAccount?.proxyUrl !== undefined) { return { proxyUrl: asTrimmedString(fallbackAccount.proxyUrl) || null, useSystemProxy: false, }; } return {}; }; const handleSaveProxy = async () => { if (drawerIntent.mode !== 'proxy') return; const customProxyUrl = asTrimmedString(oauthProxyUrl); if (oauthCustomProxyEnabled && !customProxyUrl) { setSessionError('已开启代理,请先输入完整代理地址'); return; } const actionKey = `save-proxy:${drawerIntent.account.accountId}`; setActionLoadingKey(actionKey); try { await api.updateOAuthConnectionProxy( drawerIntent.account.accountId, resolveProxySettingsPayload({ customEnabled: oauthCustomProxyEnabled, systemEnabled: oauthSystemProxyEnabled, proxyValue: oauthProxyUrl, clearToSiteFallback: true, }), ); await loadConnections(); setDrawerOpen(false); resetOauthProxySettings(); setSessionSuccess('代理已保存'); } catch (error: any) { setSessionError(error?.message || '保存代理失败'); } finally { setActionLoadingKey(''); } }; const handleStart = async () => { const provider = selectedProvider || (drawerIntent.mode === 'create' ? providers.find((item) => item.provider === drawerIntent.provider) : null) || providers[0] || null; if (!provider) { setSessionError('当前没有可用的 OAuth Provider。'); return; } if (!provider.enabled) { setSessionError(`${provider.label} 当前环境未启用`); return; } const rebindAccount = drawerIntent.mode === 'create' ? null : drawerIntent.account; const accountId = rebindAccount?.accountId; const customProxyUrl = asTrimmedString(oauthProxyUrl); if (oauthCustomProxyEnabled && !customProxyUrl) { setSessionError('已开启代理,请先输入完整代理地址'); return; } const proxySettings = resolveProxySettingsPayload({ customEnabled: oauthCustomProxyEnabled, systemEnabled: oauthSystemProxyEnabled, proxyValue: oauthProxyUrl, fallbackAccount: rebindAccount, clearToSiteFallback: drawerIntent.mode === 'proxy', }); const actionKey = `start:${provider.provider}:${accountId || 0}`; setActionLoadingKey(actionKey); try { const projectId = drawerIntent.mode === 'create' && provider.requiresProjectId ? (asTrimmedString(drawerProjectId) || undefined) : undefined; const started = accountId ? await api.rebindOAuthConnection( accountId, { ...proxySettings, }, ) : await api.startOAuthProvider(provider.provider, { projectId, ...proxySettings, }); setSessionInfo('等待授权完成'); setActiveSession({ provider: started.provider, state: started.state, authorizationUrl: started.authorizationUrl, instructions: started.instructions, }); resetOauthProxySettings(); openOAuthPopup(provider.provider, started.authorizationUrl); } catch (error: any) { setSessionError(error?.message || '无法启动 OAuth 授权'); } finally { setActionLoadingKey(''); } }; const handleSubmitManualCallback = async () => { if (!activeSession) return; const callbackUrl = manualCallbackUrl.trim(); if (!callbackUrl) { setSessionError('请输入完整的回调 URL'); return; } setManualCallbackSubmitting(true); try { await api.submitOAuthManualCallback(activeSession.state, callbackUrl); setSessionInfo('回调已提交,等待授权完成'); } catch (error: any) { setSessionError(error?.message || '提交回调 URL 失败'); } finally { setManualCallbackSubmitting(false); } }; const handleDelete = async (accountId: number) => { if (typeof window !== 'undefined' && typeof window.confirm === 'function') { const confirmed = window.confirm('确定要删除这个 OAuth 连接吗?'); if (!confirmed) return; } const actionKey = `delete:${accountId}`; setActionLoadingKey(actionKey); try { await api.deleteOAuthConnection(accountId); setSessionSuccess('连接已删除'); await loadConnections(); setSelectedConnectionIds((current) => current.filter((id) => id !== accountId)); } catch (error: any) { setSessionError(error?.message || '删除 OAuth 连接失败'); } finally { setActionLoadingKey(''); } }; const handleDeleteSelected = async () => { if (selectedConnectionIds.length === 0) return; if (typeof window !== 'undefined' && typeof window.confirm === 'function') { const confirmed = window.confirm(`确定要删除选中的 ${selectedConnectionIds.length} 个 OAuth 连接吗?`); if (!confirmed) return; } setActionLoadingKey('delete:selected'); try { const results = await Promise.allSettled(selectedConnectionIds.map((accountId) => api.deleteOAuthConnection(accountId))); const failed = results.filter((item) => item.status === 'rejected').length; await loadConnections(); setSelectedConnectionIds([]); if (failed > 0) { setSessionInfo(`批量删除完成,${failed} 个连接删除失败`); } else { setSessionSuccess(`已删除 ${results.length} 个 OAuth 连接`); } } finally { setActionLoadingKey(''); } }; const handleRefreshQuota = async (accountId: number) => { const actionKey = `quota:${accountId}`; setActionLoadingKey(actionKey); try { await api.refreshOAuthConnectionQuota(accountId); setSessionSuccess('额度信息已刷新'); await loadConnections(); } catch (error: any) { setSessionError(error?.message || '刷新额度失败'); } finally { setActionLoadingKey(''); } }; const handleRefreshSelected = async () => { if (selectedConnectionIds.length === 0) return; setActionLoadingKey('quota:selected'); try { const result = await api.refreshOAuthConnectionQuotaBatch(selectedConnectionIds); await loadConnections(); if (result.failed > 0) { setSessionInfo(`批量刷新完成,成功 ${result.refreshed} 个,失败 ${result.failed} 个`); } else { setSessionSuccess(`已批量刷新 ${result.refreshed} 个 OAuth 连接`); } } catch (error: any) { setSessionError(error?.message || '批量刷新额度失败'); } finally { setActionLoadingKey(''); } }; const applyLoadedModelsModal = useCallback((connection: OAuthConnectionInfo, result: any) => { const models = Array.isArray(result?.models) ? result.models as OAuthModelItem[] : []; setModelsModal((current) => ({ ...current, open: true, loading: false, refreshing: false, connection, models, totalCount: Number.isFinite(result?.totalCount) ? Number(result.totalCount) : models.length, disabledCount: Number.isFinite(result?.disabledCount) ? Number(result.disabledCount) : models.filter((item) => item.disabled).length, siteName: asTrimmedString(result?.siteName) || connection.site?.name || '', })); }, []); const loadModelsModal = useCallback(async ( connection: OAuthConnectionInfo, options: { refreshUpstream?: boolean; resetBeforeLoad?: boolean; closeOnError?: boolean; } = {}, ) => { const requestId = ++modelsModalRequestSeqRef.current; setModelsModal((current) => ({ ...current, open: true, connection, loading: options.resetBeforeLoad ? true : current.loading, refreshing: options.refreshUpstream ? true : current.refreshing, ...(options.resetBeforeLoad ? { models: [], totalCount: 0, disabledCount: 0, siteName: connection.site?.name || '' } : {}), })); try { if (options.refreshUpstream) { await api.checkModels(connection.accountId); } const result = await api.getAccountModels(connection.accountId); if (modelsModalRequestSeqRef.current !== requestId) return; applyLoadedModelsModal(connection, result); if (options.refreshUpstream) { await loadConnections(); setSessionSuccess('模型列表已刷新'); } } catch (error: any) { if (modelsModalRequestSeqRef.current !== requestId) return; setSessionError(error?.message || '加载模型列表失败'); setModelsModal((current) => ( options.closeOnError ? { ...current, open: false, loading: false, refreshing: false, connection: null, } : { ...current, loading: false, refreshing: false, } )); } }, [applyLoadedModelsModal, loadConnections]); const openModelsModal = useCallback(async (connection: OAuthConnectionInfo) => { await loadModelsModal(connection, { resetBeforeLoad: true, closeOnError: true, }); }, [loadModelsModal]); const closeModelsModal = useCallback(() => { modelsModalRequestSeqRef.current += 1; setModelsModal((current) => ({ ...current, open: false, loading: false, refreshing: false, connection: null, })); }, []); const handleImportFilesSelected = useCallback(async (files: ArrayLike | null | undefined) => { const drafts = await readOauthImportDrafts(files); setImportDrafts(drafts); }, []); const handleImportFileChange = useCallback(async (event: ChangeEvent) => { await handleImportFilesSelected(event.target.files); event.target.value = ''; }, [handleImportFilesSelected]); const handleImportDragOver = useCallback((event: DragEvent) => { event.preventDefault(); setImportDragOver(true); }, []); const handleImportDragLeave = useCallback((event: DragEvent) => { event.preventDefault(); setImportDragOver(false); }, []); const handleImportDrop = useCallback(async (event: DragEvent) => { event.preventDefault(); setImportDragOver(false); await handleImportFilesSelected(event.dataTransfer?.files); }, [handleImportFilesSelected]); const importSources = useMemo(() => { const manualRaw = importJsonText.trim(); return [ ...importDrafts, ...(manualRaw ? [{ sourceName: '手动粘贴 JSON', rawText: manualRaw }] : []), ]; }, [importDrafts, importJsonText]); const importPreviewSummary = useMemo(() => { if (importSources.length <= 0) return null; const items = importSources.map((source) => parseOauthImportPreview(source)); const validCount = items.filter((item) => item.valid).length; const invalidCount = items.length - validCount; return { totalCount: items.length, validCount, invalidCount, canImport: validCount > 0 && invalidCount === 0, items, }; }, [importSources]); const handleImport = async () => { if (importSources.length <= 0) { setSessionError('请先选择 JSON 文件或粘贴 OAuth 连接 JSON 内容'); return; } if (!importPreviewSummary?.canImport) { setSessionError('请先修正无效的 OAuth JSON'); return; } if (importCustomProxyEnabled && !asTrimmedString(importProxyUrl)) { setSessionError('已开启代理,请先输入完整代理地址'); return; } setImporting(true); try { const parsedItems = importPreviewSummary.items .filter((item) => item.valid && item.parsedData) .map((item) => item.parsedData as Record); const importProxySettings = importSystemProxyEnabled && !importCustomProxyEnabled ? { useSystemProxy: true as const } : resolveProxySettingsPayload({ customEnabled: importCustomProxyEnabled, systemEnabled: importSystemProxyEnabled, proxyValue: importProxyUrl, }); const result = parsedItems.length === 1 && !('proxyUrl' in importProxySettings) && !('useSystemProxy' in importProxySettings) ? await api.importOAuthConnections(parsedItems[0]!) : await api.importOAuthConnections({ items: parsedItems, ...importProxySettings, }); await loadConnections(); const importMessage = result.failed > 0 ? `批量导入完成,成功 ${result.imported} 个,失败 ${result.failed} 个` : `已添加 ${result.imported} 个 OAuth 连接`; if (result.failed > 0) { toast.info(importMessage); } else { toast.success(importMessage); } if (result.failed > 0) { setSessionInfo(importMessage); } else { setSessionSuccess(importMessage); } closeImportModal(); } catch (error: any) { const message = error?.message || '导入 OAuth JSON 失败'; toast.error(message); setSessionError(message); } finally { setImporting(false); } }; const handleCreateRouteUnit = async () => { const name = asTrimmedString(routeUnitModal.name); if (!canMergeSelectedIntoRouteUnit || selectedConnections.length < 2) return; if (!name) { setSessionError('请先输入路由池名称'); return; } setActionLoadingKey('route-unit:create'); try { const result = await api.createOAuthRouteUnit({ accountIds: selectedConnections.map((connection) => connection.accountId), name, strategy: routeUnitModal.strategy, }); const routeUnitDefaults: SessionRouteUnitFeedback = { action: 'created' as const, name: asTrimmedString(name) || name, memberCount: selectedConnections.length, strategy: routeUnitModal.strategy, }; const routeUnit: SessionRouteUnitFeedback = result.routeUnit ? { ...routeUnitDefaults, name: asTrimmedString(result.routeUnit.name) || routeUnitDefaults.name, memberCount: result.routeUnit.memberCount || routeUnitDefaults.memberCount, strategy: result.routeUnit.strategy || routeUnitDefaults.strategy, } : routeUnitDefaults; toast.success(`已创建路由池:${routeUnit.name}`); setSessionSuccess('已创建路由池', { routeUnit, }); setSelectedConnectionIds([]); closeRouteUnitModal(); try { await loadConnections(); } catch { toast.error('OAuth 连接列表刷新失败'); setSessionError('已创建路由池,但连接列表刷新失败', { routeUnit, }); } } catch (error: any) { const message = error?.message || '创建路由池失败'; toast.error(message); setSessionError(message); } finally { setActionLoadingKey(''); } }; const handleDeleteSelectedRouteUnit = async () => { if (!selectedRouteUnitParticipation) return; const routeUnitId = selectedRouteUnitParticipation.routeUnitId ?? selectedRouteUnitParticipation.id; if (!routeUnitId) return; setActionLoadingKey('route-unit:delete'); try { const routeUnitFeedback = { action: 'deleted' as const, name: selectedRouteUnitParticipation.name, memberCount: selectedRouteUnitParticipation.memberCount, strategy: selectedRouteUnitParticipation.strategy, }; await api.deleteOAuthRouteUnit(routeUnitId); toast.success(`已拆回单体:${routeUnitFeedback.name}`); setSessionSuccess('已拆回单体', { routeUnit: routeUnitFeedback, }); setSelectedConnectionIds([]); try { await loadConnections(); } catch { toast.error('OAuth 连接列表刷新失败'); setSessionError('已拆回单体,但连接列表刷新失败', { routeUnit: routeUnitFeedback, }); } } catch (error: any) { const message = error?.message || '拆回单体失败'; toast.error(message); setSessionError(message); } finally { setActionLoadingKey(''); } }; const toggleColumn = (key: ColumnKey) => { setVisibleColumns((current) => ({ ...current, [key]: !current[key], })); }; const toggleSelectAllVisible = (checked: boolean) => { if (!checked) { setSelectedConnectionIds((current) => current.filter((id) => !filteredConnections.some((connection) => connection.accountId === id))); return; } setSelectedConnectionIds((current) => Array.from(new Set([ ...current, ...filteredConnections.map((connection) => connection.accountId), ]))); }; const filterBar = (
setSearchQuery(event.target.value)} placeholder="搜索账号 / 邮箱 / 站点 / 项目" />
setProviderFilter(String(value || ''))} options={[ { value: '', label: '全部 Provider' }, ...providerOptions, ]} placeholder="全部 Provider" />
setStatusFilter(String(value || ''))} options={[ { value: '', label: '全部状态' }, { value: 'healthy', label: '正常' }, { value: 'abnormal', label: '异常' }, ]} placeholder="全部状态" />
setSiteFilter(String(value || ''))} options={[ { value: '', label: '全部站点' }, ...siteOptions, ]} placeholder="全部站点" />
setAutoRefreshSeconds(Number(value || 0))} options={AUTO_REFRESH_OPTIONS.map((seconds) => ({ value: String(seconds), label: seconds === 0 ? '自动刷新:关闭' : `自动刷新:${seconds}s`, }))} placeholder="自动刷新" />
{autoRefreshSeconds > 0 ? (
下次刷新 {autoRefreshCountdown}s
) : null}
{showColumnMenu ? (
{COLUMN_OPTIONS.map((column) => ( ))}
) : null}
OAuth 账号以后只在这里维护。连接管理页默认只保留普通 Session / API Key / Token 连接。
); const mobileFilterContent = (
setSearchQuery(event.target.value)} placeholder="搜索账号 / 邮箱 / 站点 / 项目" /> setProviderFilter(String(value || ''))} options={[ { value: '', label: '全部 Provider' }, ...providerOptions, ]} placeholder="全部 Provider" /> setStatusFilter(String(value || ''))} options={[ { value: '', label: '全部状态' }, { value: 'healthy', label: '正常' }, { value: 'abnormal', label: '异常' }, ]} placeholder="全部状态" /> setSiteFilter(String(value || ''))} options={[ { value: '', label: '全部站点' }, ...siteOptions, ]} placeholder="全部站点" /> setAutoRefreshSeconds(Number(value || 0))} options={AUTO_REFRESH_OPTIONS.map((seconds) => ({ value: String(seconds), label: seconds === 0 ? '自动刷新:关闭' : `自动刷新:${seconds}s`, }))} placeholder="自动刷新" />
); const desktopTable = ( {visibleColumns.identity ? : null} {visibleColumns.site ? : null} {visibleColumns.status ? : null} {visibleColumns.quota ? : null} {visibleColumns.proxy ? : null} {filteredConnections.map((connection) => { const quota = connection.quota; const emailLabel = resolveConnectionEmailLabel(connection); const primaryTitle = resolveConnectionPrimaryTitle(connection); const sitePlatform = asTrimmedString(connection.site?.platform); const modelSyncDetail = resolveModelSyncDetail(connection); const quotaSyncDetail = resolveQuotaSyncDetail(quota); return ( {visibleColumns.identity ? ( ) : null} {visibleColumns.site ? ( ) : null} {visibleColumns.status ? ( ) : null} {visibleColumns.quota ? ( ) : null} {visibleColumns.proxy ? ( ) : null} ); })}
toggleSelectAllVisible(event.target.checked)} /> 账号站点状态额度计划 / 代理操作
{ const checked = event.target.checked; setSelectedConnectionIds((current) => checked ? Array.from(new Set([...current, connection.accountId])) : current.filter((id) => id !== connection.accountId)); }} />
{connection.provider} {resolveConnectionStatusLabel(connection.status)}
{emailLabel && emailLabel !== primaryTitle ? (
{emailLabel}
) : null} {connection.accountKey ? (
连接: {compactAccountKey(connection.accountKey)}
) : null}
{connection.site?.name || '--'}
{sitePlatform && sitePlatform !== connection.provider ? (
{sitePlatform}
) : null}
模型
{resolveModelSyncStatusText(connection)}
{modelSyncDetail ? (
{modelSyncDetail}
) : null}
额度
{resolveQuotaSyncStatusText(quota)}
{quotaSyncDetail ? (
{quotaSyncDetail}
) : null}
{quota ? (
{resolveQuotaStatusLabel(quota.status)} {resolveQuotaSourceLabel(quota.source)}
) : ( -- )}
{resolveProxyProjectSummary(connection)}
{resolveRouteParticipationSummary(connection)}
{resolveProxyDisplayText(connection)}
); const mobileList = (
{filteredConnections.map((connection) => { const quota = connection.quota; return ( { const checked = event.target.checked; setSelectedConnectionIds((current) => checked ? Array.from(new Set([...current, connection.accountId])) : current.filter((id) => id !== connection.accountId)); }} /> )} >
{resolveProxyDisplayText(connection)}
)} stacked />
模型
{resolveModelSyncStatusText(connection)}
{resolveModelSyncDetail(connection) ? (
{resolveModelSyncDetail(connection)}
) : null}
额度
{resolveQuotaSyncStatusText(quota)}
{resolveQuotaSyncDetail(quota) ? (
{resolveQuotaSyncDetail(quota)}
) : null}
)} stacked />
Usage / Quota
{quota ? ( <>
{resolveQuotaStatusLabel(quota.status)} {resolveQuotaSourceLabel(quota.source)}
) : (
--
)}
); })} ); return (

OAuth 管理

统一管理需要浏览器授权的官方上游连接。OAuth 账号以后只在这里维护,不再和普通连接管理页重复显示。
{!isMobile ? (
) : null}
{sessionFeedback ? (
{sessionFeedback.message}
{sessionFeedback.tone === 'success' ? '成功' : sessionFeedback.tone === 'error' ? '失败' : '提示'}
{sessionFeedback.routeUnit ? (
{sessionFeedback.routeUnit.name} {sessionFeedback.routeUnit.memberCount} 个成员 {resolveRouteUnitStrategyLabel(sessionFeedback.routeUnit.strategy)}
{sessionFeedback.routeUnit.action === 'created' ? '已将选中的 OAuth 账号合并为一个路由池,后续会以单个路由单元参与路由。' : '该路由池已拆分回单体账号,后续会分别参与路由。'}
) : null}
) : null} setShowMobileFilters(true)} onMobileClose={() => setShowMobileFilters(false)} mobileTitle="OAuth 筛选与操作" mobileContent={mobileFilterContent} desktopContent={filterBar} mobileTrigger={
} />
OAuth 连接列表
已连接 {connections.length} 个 OAuth 账号,当前筛选后显示 {filteredConnections.length} 个。
{selectedConnectionIds.length > 0 ? ( {canMergeSelectedIntoRouteUnit ? ( ) : null} {canSplitSelectedRouteUnit ? ( ) : null} ) : null} {!loaded ? (
加载中...
正在加载 OAuth 连接与额度信息。
) : filteredConnections.length === 0 ? (
暂无 OAuth 连接
使用右上角“新建 OAuth 连接”接入 Codex、Claude、Gemini CLI 或 Antigravity。
) : ( isMobile ? mobileList : desktopTable )}
setDrawerOpen(false)} title={drawerIntent.mode === 'create' ? '新建 OAuth 连接' : drawerIntent.mode === 'proxy' ? `代理设置 · ${resolveConnectionPrimaryTitle(drawerIntent.account)}` : `重新授权 · ${resolveConnectionPrimaryTitle(drawerIntent.account)}`} >
{drawerIntent.mode === 'create' ? (
Provider
setSelectedProviderKey(String(value || ''))} options={providerOptions} placeholder="选择 OAuth Provider" />
) : (
当前连接
{resolveConnectionPrimaryTitle(drawerIntent.account)}
{drawerIntent.account.provider} {drawerIntent.account.projectId ? ` · Project ${drawerIntent.account.projectId}` : ''} {drawerIntent.mode === 'proxy' ? ` · ${resolveProxyDisplayText(drawerIntent.account)}` : ''}
)} {selectedProvider?.requiresProjectId && drawerIntent.mode === 'create' ? (
Google Cloud Project ID(可选)
setDrawerProjectId(event.target.value)} placeholder="留空则交给上游自动解析" />
) : null}
{drawerIntent.mode === 'proxy' ? '这里修改的是账号级 OAuth 代理。点击“保存代理”会立即落库并刷新列表;只有“保存并重新授权”才会重新走授权流程。若两项都不勾选,则回退到站点代理配置。' : '这里的设置会作用于下一次“连接”或“重新授权”。填写代理地址后,本次 OAuth 换 token 和后续生成的账号都会直接带上这份账号级代理配置;若不勾选,则回退到站点代理配置。'}
代理地址
setOauthProxyUrl(event.target.value)} placeholder="如 http://127.0.0.1:7890 或 socks5://127.0.0.1:1080" disabled={!oauthCustomProxyEnabled} />
{drawerIntent.mode === 'proxy' ? (
) : ( )}
{activeSession ? (
授权指引
固定回调地址
{renderCodeBlock(activeSession.instructions.redirectUri)}
{renderGuideCard( '本地部署', 'BoosAPI 和浏览器在同一台机器时,不需要 SSH 隧道。直接点击“连接”,在弹窗里完成授权即可。',
如果浏览器能直接访问上面的 localhost 回调地址,授权完成后会自动回到 BoosAPI。
, )} {activeSession.instructions.sshTunnelCommand ? renderGuideCard( '云端部署', 'BoosAPI 部署在 VPS、容器或远程主机时,浏览器访问到的是你自己电脑的 localhost。先在本地开 SSH 隧道,再继续登录。',
常规 SSH 隧道
{renderCodeBlock(activeSession.instructions.sshTunnelCommand)} {activeSession.instructions.sshTunnelKeyCommand ? ( <>
SSH Key 隧道
{renderCodeBlock(activeSession.instructions.sshTunnelKeyCommand)} ) : null}
, ) : renderGuideCard( '云端部署', '当前没有检测到远程主机地址。如果你实际是云端部署,请用能访问服务器 127.0.0.1 回调端口的 SSH 隧道方式完成授权。', )} {renderGuideCard( '手动回调', `如果浏览器停在 localhost 错误页,复制浏览器地址栏里的完整 URL,等待 ${Math.max(1, Math.round(activeSession.instructions.manualCallbackDelayMs / 1000))} 秒后粘贴回来。`, manualCallbackVisible ? (