import { useCallback, useEffect, useId, 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 { trapFocusInContainer } from '../components/focusTrap.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 OAuthImportResponse, 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 TokenFilter = '' | 'valid' | 'expiring' | 'expired' | 'unknown'; type ColumnMenuFocusEdge = 'first' | 'last'; 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 OAuthProviderWireInfo = Partial & { id?: string | null; key?: string | null; value?: string | null; name?: string | null; displayName?: string | null; display_name?: string | null; type?: string | null; login_type?: string | null; requires_project_id?: boolean | null; supports_direct_account_routing?: boolean | null; supports_cloud_validation?: boolean | null; supports_native_proxy?: boolean | null; }; 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; hasInvalidItems: boolean; 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: '代理 / 项目' }, ]; const TOKEN_FILTER_OPTIONS: Array<{ value: TokenFilter; label: string }> = [ { value: '', label: '全部 Token' }, { value: 'valid', label: 'Token 正常' }, { value: 'expiring', label: 'Token 即将到期' }, { value: 'expired', label: 'Token 已过期' }, { value: 'unknown', label: 'Token 未知' }, ]; function formatCountedLabel(label: string, count: number): string { return count > 0 ? `${label} · ${count}` : 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 { if (typeof value !== 'string') return ''; const text = value.trim(); const lowerText = text.toLowerCase(); if (lowerText === 'undefined' || lowerText === 'null') return ''; return text; } function resolveOAuthProviderFallbackLabel(providerKey: string): string { const normalized = providerKey.trim().toLowerCase(); if (normalized === 'codex') return 'Codex'; if (normalized === 'openai') return 'OpenAI'; if (normalized === 'anthropic') return 'Anthropic'; if (normalized === 'claude') return 'Claude'; if (normalized === 'google') return 'Google AI'; if (normalized === 'gemini-cli' || normalized === 'gemini') return 'Gemini CLI'; if (normalized === 'antigravity') return 'Antigravity'; return providerKey; } function normalizeOAuthProvider(provider: OAuthProviderWireInfo): OAuthProviderInfo | null { const providerKey = asTrimmedString(provider.provider) || asTrimmedString(provider.id) || asTrimmedString(provider.value) || asTrimmedString(provider.key) || asTrimmedString(provider.type); if (!providerKey) return null; const platform = asTrimmedString(provider.platform) || asTrimmedString(provider.type) || providerKey; const label = asTrimmedString(provider.label) || asTrimmedString(provider.name) || asTrimmedString(provider.displayName) || asTrimmedString(provider.display_name) || resolveOAuthProviderFallbackLabel(providerKey); return { provider: providerKey, label, platform, enabled: provider.enabled !== false, loginType: 'oauth', requiresProjectId: provider.requiresProjectId === true || provider.requires_project_id === true, supportsDirectAccountRouting: provider.supportsDirectAccountRouting ?? provider.supports_direct_account_routing ?? true, supportsCloudValidation: provider.supportsCloudValidation ?? provider.supports_cloud_validation ?? true, supportsNativeProxy: provider.supportsNativeProxy ?? provider.supports_native_proxy ?? true, }; } 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 isOAuthDirectFlowUnavailable(error: unknown): boolean { const message = error instanceof Error ? error.message : typeof error === 'string' ? error : ''; const normalized = message.trim().toLowerCase(); return normalized.includes('not implemented') || normalized.includes('not configured') || normalized.includes('未实现') || normalized.includes('未配置'); } function isModelRefreshResultSuccessful(value: unknown): boolean { if (!value || typeof value !== 'object') return true; const refresh = (value as { refresh?: { status?: unknown } }).refresh; return refresh?.status !== 'error'; } function formatOAuthImportMessage(result: OAuthImportResponse): string { const imported = Number(result.imported || 0); const updated = Number(result.updated || 0); const failed = Number(result.failed || 0); const skipped = Number(result.skipped || 0); const changedParts = [ imported > 0 ? `添加 ${imported} 个` : '', updated > 0 ? `更新 ${updated} 个` : '', skipped > 0 ? `跳过 ${skipped} 个` : '', ].filter(Boolean); if (failed > 0) { return [ '批量导入完成', ...changedParts, `失败 ${failed} 个`, ].join(','); } if (changedParts.length > 0) { return `已${changedParts.join('、')} OAuth 连接`; } return '没有可导入的 OAuth 连接'; } 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 rawNumber = 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(rawNumber) || rawNumber <= 0) { throw new Error('expired 时间格式无效'); } const parsed = rawNumber < 10_000_000_000 ? rawNumber * 1000 : rawNumber; 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 : '') || asTrimmedString(typeof payload.provider === 'string' ? payload.provider : '') || asTrimmedString(typeof payload.oauthProvider === 'string' ? payload.oauthProvider : ''); 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 || '未知'}`, }; } const accessToken = asTrimmedString(typeof payload.access_token === 'string' ? payload.access_token : '') || asTrimmedString(typeof payload.accessToken === 'string' ? payload.accessToken : ''); if (!accessToken) { return { sourceName: source.sourceName, valid: false, error: '缺少 access_token', }; } try { const idToken = typeof payload.id_token === 'string' ? payload.id_token : typeof payload.idToken === 'string' ? payload.idToken : undefined; const claims = decodeJwtPayload(idToken); 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 : '') || asTrimmedString(typeof claims?.preferred_username === 'string' ? claims.preferred_username : ''), accountKey: asTrimmedString(typeof payload.account_key === 'string' ? payload.account_key : '') || asTrimmedString(typeof payload.account_id === 'string' ? payload.account_id : '') || asTrimmedString(typeof payload.accountKey === 'string' ? payload.accountKey : '') || asTrimmedString(typeof payload.accountId === 'string' ? payload.accountId : '') || asTrimmedString(typeof authRecord?.chatgpt_account_id === 'string' ? authRecord.chatgpt_account_id : ''), expiresLabel: resolveImportPreviewExpiryLabel( payload.expired ?? payload.expires_at ?? payload.expiresAt ?? payload.tokenExpiresAt ?? payload.token_expires_at, ), 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 resolveTokenExpiryMillis(connection: OAuthConnectionInfo): number | null { const rawMillis = Number(connection.tokenExpiresAt); if (Number.isFinite(rawMillis) && rawMillis > 0) return rawMillis; const parsed = Date.parse(asTrimmedString(connection.expiresAt)); return Number.isFinite(parsed) && parsed > 0 ? parsed : null; } function resolveTokenFilterValue(connection: OAuthConnectionInfo): Exclude { const expiresAt = resolveTokenExpiryMillis(connection); if (!expiresAt) return 'unknown'; const remainingMs = expiresAt - Date.now(); if (remainingMs <= 0) return 'expired'; if (remainingMs <= 5 * 60 * 1000) return 'expiring'; return 'valid'; } function resolveTokenExpiryStatusText(connection: OAuthConnectionInfo): string { const tokenStatus = resolveTokenFilterValue(connection); if (tokenStatus === 'unknown') return 'Token 未知'; if (tokenStatus === 'expired') return 'Token 已过期'; if (tokenStatus === 'expiring') return 'Token 即将到期'; const expiresAt = resolveTokenExpiryMillis(connection); if (!expiresAt) return ''; return `Token 到期:${new Date(expiresAt).toLocaleString()}`; } function resolveTokenStatusClassName(connection: OAuthConnectionInfo): string { return `oauth-token-status oauth-token-status-${resolveTokenFilterValue(connection)}`; } 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); const titleId = useId(); const panelRef = useRef(null); const lastActiveElementRef = useRef(null); 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]); useEffect(() => { if (open) { const activeElement = typeof document !== 'undefined' ? document.activeElement : null; lastActiveElementRef.current = typeof HTMLElement !== 'undefined' && activeElement instanceof HTMLElement ? activeElement : null; const focusPanel = () => { const panel = panelRef.current; if (!panel) return; const currentActiveElement = typeof document !== 'undefined' ? document.activeElement : null; if (typeof Node !== 'undefined' && currentActiveElement instanceof Node && panel.contains(currentActiveElement)) return; panel.focus({ preventScroll: true }); }; if (typeof window !== 'undefined' && typeof window.requestAnimationFrame === 'function') { const animationFrame = window.requestAnimationFrame(focusPanel); return () => { if (typeof window.cancelAnimationFrame === 'function') { window.cancelAnimationFrame(animationFrame); } }; } focusPanel(); return undefined; } if (lastActiveElementRef.current?.isConnected !== false) { lastActiveElementRef.current?.focus({ preventScroll: true }); } lastActiveElementRef.current = null; return undefined; }, [open]); useEffect(() => () => { if (lastActiveElementRef.current?.isConnected !== false) { lastActiveElementRef.current?.focus({ preventScroll: true }); } lastActiveElementRef.current = null; }, []); useEffect(() => { if (!open || typeof document === 'undefined') return; const handleKeydown = (event: globalThis.KeyboardEvent) => { if (event.key === 'Escape') { onClose(); return; } trapFocusInContainer(event, panelRef.current); }; document.addEventListener('keydown', handleKeydown); return () => { document.removeEventListener('keydown', handleKeydown); }; }, [onClose, open]); if (!presence.shouldRender) return null; const interactive = open && presence.isVisible; const panel = (
event.stopPropagation()} role="dialog" aria-modal="true" aria-labelledby={titleId} aria-hidden={!interactive} tabIndex={-1} >
{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 mountedRef = useRef(true); const connectionsLoadSeqRef = useRef(0); 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 [tokenFilter, setTokenFilter] = useState(''); const [visibleColumns, setVisibleColumns] = useState>({ identity: true, site: true, status: true, quota: true, proxy: true, }); const [showColumnMenu, setShowColumnMenu] = useState(false); const columnMenuAnchorRef = useRef(null); const columnMenuButtonRef = useRef(null); const columnMenuRef = useRef(null); const columnMenuFocusEdgeRef = useRef('first'); const columnMenuId = useId(); const columnMenuPresence = useAnimatedVisibility(showColumnMenu, 140); 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 focusColumnMenuItem = useCallback((edge: ColumnMenuFocusEdge = 'first') => { const focusItem = () => { const items = columnMenuRef.current?.querySelectorAll('input[type="checkbox"]'); if (!items?.length) return; const target = edge === 'last' ? items[items.length - 1] : items[0]; target?.focus({ preventScroll: true }); }; if (typeof window !== 'undefined' && typeof window.requestAnimationFrame === 'function') { window.requestAnimationFrame(focusItem); return; } focusItem(); }, []); const openColumnMenu = useCallback((edge: ColumnMenuFocusEdge = 'first') => { columnMenuFocusEdgeRef.current = edge; setShowColumnMenu(true); }, []); const closeColumnMenu = useCallback((options?: { restoreFocus?: boolean }) => { setShowColumnMenu(false); if (options?.restoreFocus) { columnMenuButtonRef.current?.focus({ preventScroll: true }); } }, []); useEffect(() => { if (!showColumnMenu) return undefined; const handlePointerDown = (event: MouseEvent) => { const target = event.target; if ( typeof Node !== 'undefined' && target instanceof Node && columnMenuAnchorRef.current?.contains(target) ) { return; } closeColumnMenu(); }; const handleKeyDown = (event: KeyboardEvent) => { if (event.key !== 'Escape') return; event.preventDefault(); closeColumnMenu({ restoreFocus: true }); }; document.addEventListener('mousedown', handlePointerDown); document.addEventListener('keydown', handleKeyDown); return () => { document.removeEventListener('mousedown', handlePointerDown); document.removeEventListener('keydown', handleKeyDown); }; }, [closeColumnMenu, showColumnMenu]); useEffect(() => { if (!showColumnMenu || !columnMenuPresence.shouldRender || !columnMenuPresence.isVisible) return; focusColumnMenuItem(columnMenuFocusEdgeRef.current); }, [ columnMenuPresence.isVisible, columnMenuPresence.shouldRender, focusColumnMenuItem, showColumnMenu, ]); 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]); useEffect(() => () => { mountedRef.current = false; connectionsLoadSeqRef.current += 1; modelsModalRequestSeqRef.current += 1; }, []); const loadConnections = useCallback(async () => { const requestSeq = ++connectionsLoadSeqRef.current; const response = await api.getOAuthConnections({ limit: CONNECTION_PAGE_LIMIT, offset: 0, }); const nextItems = Array.isArray(response?.items) ? response.items : []; if (!mountedRef.current || connectionsLoadSeqRef.current !== requestSeq) return nextItems; 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.from( new Map( (Array.isArray(providersResponse?.providers) ? providersResponse.providers : []) .map((provider) => normalizeOAuthProvider(provider as OAuthProviderWireInfo)) .filter((provider): provider is OAuthProviderInfo => provider != null) .map((provider) => [provider.provider, provider]), ).values(), ); if (!mountedRef.current) return; setRuntimeSystemProxyConfigured(providersResponse?.defaults?.systemProxyConfigured === true); setProviders(nextProviders); setSelectedProviderKey((current) => current || nextProviders[0]?.provider || ''); } catch (error: any) { if (!mountedRef.current) return; console.error('failed to load oauth management data', error); setSessionError(error?.message || 'OAuth 管理数据加载失败'); } finally { if (!mountedRef.current) return; 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) => { if (!mountedRef.current) return; 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 || !mountedRef.current) return; if (session.status === 'pending') { setSessionInfo('等待授权完成'); timer = setTimeout(poll, POLL_INTERVAL_MS); return; } if (session.status === 'success') { setSessionSuccess('授权成功'); await loadConnections(); if (cancelled || !mountedRef.current) return; setActiveSession(null); return; } setSessionError(`授权失败:${session.error || '未知错误'}`); setActiveSession(null); } catch (error: any) { if (cancelled || !mountedRef.current) 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.flatMap((provider) => { const value = asTrimmedString(provider.provider); if (!value) return []; const label = asTrimmedString(provider.label) || resolveOAuthProviderFallbackLabel(value); const platform = asTrimmedString(provider.platform) || value; return [{ value, label, description: `${platform}${provider.requiresProjectId ? ' · 可选 Project ID' : ''}${!provider.enabled ? ' · 当前环境未启用' : ''}`, }]; }), [providers], ); const selectedProvider = useMemo( () => providers.find((provider) => provider.provider === selectedProviderKey) || null, [providers, selectedProviderKey], ); const hasProviderOptions = providerOptions.length > 0; 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 tokenStatusCounts = useMemo(() => { const counts: Record, number> = { valid: 0, expiring: 0, expired: 0, unknown: 0, }; connections.forEach((connection) => { counts[resolveTokenFilterValue(connection)] += 1; }); return counts; }, [connections]); const tokenFilterOptions = useMemo( () => TOKEN_FILTER_OPTIONS.map((option) => ({ ...option, label: option.value ? formatCountedLabel(option.label, tokenStatusCounts[option.value]) : formatCountedLabel(option.label, connections.length), })), [connections.length, tokenStatusCounts], ); const tokenRiskSummaryItems = useMemo(() => { const items: Array<{ value: Exclude; label: string; count: number; badgeClass: string }> = []; if (tokenStatusCounts.expired > 0) { items.push({ value: 'expired', label: 'Token 已过期', count: tokenStatusCounts.expired, badgeClass: 'badge-danger' }); } if (tokenStatusCounts.expiring > 0) { items.push({ value: 'expiring', label: '即将到期', count: tokenStatusCounts.expiring, badgeClass: 'badge-warning' }); } if (tokenStatusCounts.unknown > 0) { items.push({ value: 'unknown', label: '未知', count: tokenStatusCounts.unknown, badgeClass: 'badge-muted' }); } return items; }, [tokenStatusCounts]); const applyTokenRiskFilter = useCallback((value: Exclude) => { setSearchQuery(''); setTokenFilter((current) => (current === value ? '' : value)); }, []); const hasActiveFilters = Boolean( searchQuery.trim() || providerFilter || statusFilter || siteFilter || tokenFilter, ); const clearFilters = useCallback(() => { setSearchQuery(''); setProviderFilter(''); setStatusFilter(''); setSiteFilter(''); setTokenFilter(''); }, []); 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 (tokenFilter && resolveTokenFilterValue(connection) !== tokenFilter) 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(' '), resolveTokenExpiryStatusText(connection), ].join(' ').toLowerCase(); return haystack.includes(search); }); }, [connections, providerFilter, searchQuery, siteFilter, statusFilter, tokenFilter]); const allVisibleSelected = filteredConnections.length > 0 && filteredConnections.every((connection) => selectedConnectionIds.includes(connection.accountId)); const visibleSelectedCount = useMemo( () => filteredConnections.filter((connection) => selectedConnectionIds.includes(connection.accountId)).length, [filteredConnections, selectedConnectionIds], ); const hiddenSelectedCount = Math.max(0, selectedConnectionIds.length - visibleSelectedCount); 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) { if (isOAuthDirectFlowUnavailable(error)) { if (drawerIntent.mode === 'proxy' && accountId) { await api.updateOAuthConnectionProxy(accountId, proxySettings); await loadConnections(); } setDrawerOpen(false); setActiveSession(null); resetOauthProxySettings(); resetImportState(); resetImportProxySettings(oauthSystemProxyEnabled); setImportCustomProxyEnabled(oauthCustomProxyEnabled); setImportProxyUrl(oauthCustomProxyEnabled ? customProxyUrl : ''); setImportOpen(true); setSessionInfo(drawerIntent.mode === 'proxy' ? '代理设置已保存。当前环境未配置官方浏览器授权,请改用 JSON 导入更新 OAuth 连接。' : '当前环境未配置官方浏览器授权,请改用 JSON 导入添加 OAuth 连接。'); } else { 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 hiddenNote = hiddenSelectedCount > 0 ? ` 其中 ${hiddenSelectedCount} 个当前被筛选隐藏。` : ''; const confirmed = window.confirm(`确定要删除选中的 ${selectedConnectionIds.length} 个 OAuth 连接吗?${hiddenNote}`); 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; if (!mountedRef.current) return; 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 (!mountedRef.current || modelsModalRequestSeqRef.current !== requestId) return; applyLoadedModelsModal(connection, result); if (options.refreshUpstream) { await loadConnections(); if (!mountedRef.current || modelsModalRequestSeqRef.current !== requestId) return; setSessionSuccess('模型列表已刷新'); } } catch (error: any) { if (!mountedRef.current || 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, hasInvalidItems: invalidCount > 0, canImport: validCount > 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, }); const localSkippedItems: OAuthImportResponse['items'] = importPreviewSummary.items .filter((item) => !item.valid) .map((item) => ({ name: item.sourceName, status: 'skipped' as const, message: item.error || 'JSON 结构无效', })); const mergedResult = localSkippedItems.length > 0 ? { ...result, skipped: Number(result.skipped || 0) + localSkippedItems.length, items: [ ...localSkippedItems, ...result.items, ], } : result; const changedAccountIds = mergedResult.items.flatMap((item) => { if (item.status !== 'imported' && item.status !== 'updated') return []; if (!('accountId' in item)) return []; const accountId = Number(item.accountId); return Number.isFinite(accountId) ? [accountId] : []; }); const modelRefreshResults = changedAccountIds.length > 0 ? await Promise.allSettled(changedAccountIds.map((accountId) => api.checkModels(accountId))) : []; const modelRefreshFailed = modelRefreshResults.filter((item) => ( item.status === 'rejected' || !isModelRefreshResultSuccessful(item.value) )).length; await loadConnections(); const updatedCount = Number(mergedResult.updated || 0); const changedCount = mergedResult.imported + updatedCount; const importMessage = formatOAuthImportMessage(mergedResult); if (mergedResult.failed > 0) { toast.info(importMessage); } else { toast.success(importMessage); } if (mergedResult.failed > 0) { setSessionInfo(importMessage); } else if (modelRefreshFailed > 0) { setSessionInfo(`${importMessage},但 ${modelRefreshFailed} 个账号模型刷新失败,可稍后手动刷新模型`); } else { setSessionSuccess(changedCount > 0 ? `${importMessage},模型列表已刷新` : 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 keepOnlyVisibleSelected = () => { setSelectedConnectionIds((current) => current.filter((id) => filteredConnections.some((connection) => connection.accountId === id))); }; const filterBar = (
setSearchQuery(event.target.value)} placeholder="搜索账号 / 邮箱 / 站点 / 项目 / Token" />
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="全部站点" />
setTokenFilter((value || '') as TokenFilter)} options={tokenFilterOptions} placeholder="全部 Token" />
{hasActiveFilters ? ( ) : null}
setAutoRefreshSeconds(Number(value || 0))} options={AUTO_REFRESH_OPTIONS.map((seconds) => ({ value: String(seconds), label: seconds === 0 ? '自动刷新:关闭' : `自动刷新:${seconds}s`, }))} placeholder="自动刷新" />
{autoRefreshSeconds > 0 ? (
下次刷新 {autoRefreshCountdown}s
) : null}
{columnMenuPresence.shouldRender ? ( ) : null}
OAuth 账号以后只在这里维护。连接管理页默认只保留普通 Session / API Key / Token 连接。
); const mobileFilterContent = (
setSearchQuery(event.target.value)} placeholder="搜索账号 / 邮箱 / 站点 / 项目 / Token" /> 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="全部站点" /> setTokenFilter((value || '') as TokenFilter)} options={tokenFilterOptions} placeholder="全部 Token" /> setAutoRefreshSeconds(Number(value || 0))} options={AUTO_REFRESH_OPTIONS.map((seconds) => ({ value: String(seconds), label: seconds === 0 ? '自动刷新:关闭' : `自动刷新:${seconds}s`, }))} placeholder="自动刷新" /> {hasActiveFilters ? ( ) : null}
); 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); const tokenExpiryStatus = resolveTokenExpiryStatusText(connection); 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}
{tokenExpiryStatus ? (
Token
{tokenExpiryStatus}
) : null}
{quota ? (
{resolveQuotaStatusLabel(quota.status)} {resolveQuotaSourceLabel(quota.source)}
) : ( -- )}
{resolveProxyProjectSummary(connection)}
{resolveRouteParticipationSummary(connection)}
{resolveProxyDisplayText(connection)}
); const mobileList = (
{filteredConnections.map((connection) => { const quota = connection.quota; const tokenExpiryStatus = resolveTokenExpiryStatusText(connection); 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}
{tokenExpiryStatus ? (
Token
{tokenExpiryStatus}
) : 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} 个。 {tokenRiskSummaryItems.length > 0 ? ( {tokenRiskSummaryItems.map((item) => ( ))} ) : null}
{selectedConnectionIds.length > 0 ? ( 0 ? `,${hiddenSelectedCount} 项被筛选隐藏` : ''}`} desktopStyle={{ marginBottom: 12 }} > {hiddenSelectedCount > 0 ? ( ) : null} {canMergeSelectedIntoRouteUnit ? ( ) : null} {canSplitSelectedRouteUnit ? ( ) : null} ) : null} {!loaded ? (
加载中...
正在加载 OAuth 连接与额度信息。
) : filteredConnections.length === 0 && connections.length > 0 ? (
没有匹配的 OAuth 连接
当前筛选条件没有命中账号,可清空筛选后重新查看全部 OAuth 连接。
{hasActiveFilters ? ( ) : null}
) : 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" /> {!hasProviderOptions ? (
当前没有可用的 OAuth Provider。请检查后端 OAuth 配置或清理无效 Provider 数据。
) : null}
) : (
当前连接
{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 ? (