From c989dbf1b6ff4bbd7ad8e00e9f3cc56e151f7133 Mon Sep 17 00:00:00 2001 From: Supra4E8C Date: Sun, 28 Dec 2025 00:48:36 +0800 Subject: [PATCH] feat(auth-files): add oauth excluded provider tag --- src/pages/AuthFilesPage.module.scss | 54 + src/pages/AuthFilesPage.tsx | 2068 ++++++++++++++------------- 2 files changed, 1131 insertions(+), 991 deletions(-) diff --git a/src/pages/AuthFilesPage.module.scss b/src/pages/AuthFilesPage.module.scss index f7d9ab7..b9b598c 100644 --- a/src/pages/AuthFilesPage.module.scss +++ b/src/pages/AuthFilesPage.module.scss @@ -422,6 +422,60 @@ flex-shrink: 0; } +// OAuth 排除列表表单:提供商快捷标签 +.providerField { + display: flex; + flex-direction: column; + gap: $spacing-xs; + + :global(.form-group) { + margin-bottom: 0; + } +} + +.providerTagList { + display: flex; + flex-wrap: wrap; + gap: $spacing-xs; +} + +.providerTag { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 4px 10px; + border-radius: $radius-full; + border: 1px solid var(--border-color); + background-color: var(--bg-secondary); + color: var(--text-secondary); + font-size: 12px; + cursor: pointer; + transition: all $transition-fast; + + &:hover { + border-color: var(--primary-color); + color: var(--text-primary); + background-color: var(--bg-hover); + } + + &:disabled { + opacity: 0.6; + cursor: not-allowed; + } +} + +.providerTagActive { + background-color: var(--primary-color); + border-color: var(--primary-color); + color: #fff; + + &:hover { + background-color: var(--primary-color); + border-color: var(--primary-color); + color: #fff; + } +} + // 详情弹窗 .detailContent { max-height: 400px; diff --git a/src/pages/AuthFilesPage.tsx b/src/pages/AuthFilesPage.tsx index 67ad8e5..d0f5864 100644 --- a/src/pages/AuthFilesPage.tsx +++ b/src/pages/AuthFilesPage.tsx @@ -1,1034 +1,1120 @@ -import { useEffect, useMemo, useRef, useState, useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; -import { useInterval } from '@/hooks/useInterval'; -import { Card } from '@/components/ui/Card'; -import { Button } from '@/components/ui/Button'; -import { LoadingSpinner } from '@/components/ui/LoadingSpinner'; -import { Input } from '@/components/ui/Input'; -import { Modal } from '@/components/ui/Modal'; -import { EmptyState } from '@/components/ui/EmptyState'; -import { IconBot, IconDownload, IconInfo, IconTrash2 } from '@/components/ui/icons'; -import { useAuthStore, useNotificationStore, useThemeStore } from '@/stores'; -import { authFilesApi, usageApi } from '@/services/api'; -import { apiClient } from '@/services/api/client'; -import type { AuthFileItem } from '@/types'; -import type { KeyStats, KeyStatBucket, UsageDetail } from '@/utils/usage'; -import { collectUsageDetails, calculateStatusBarData } from '@/utils/usage'; -import { formatFileSize } from '@/utils/format'; -import styles from './AuthFilesPage.module.scss'; - -type ThemeColors = { bg: string; text: string; border?: string }; -type TypeColorSet = { light: ThemeColors; dark?: ThemeColors }; -type ResolvedTheme = 'light' | 'dark'; - -// 标签类型颜色配置(对齐重构前 styles.css 的 file-type-badge 颜色) +import { useEffect, useMemo, useRef, useState, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useInterval } from '@/hooks/useInterval'; +import { Card } from '@/components/ui/Card'; +import { Button } from '@/components/ui/Button'; +import { LoadingSpinner } from '@/components/ui/LoadingSpinner'; +import { Input } from '@/components/ui/Input'; +import { Modal } from '@/components/ui/Modal'; +import { EmptyState } from '@/components/ui/EmptyState'; +import { IconBot, IconDownload, IconInfo, IconTrash2 } from '@/components/ui/icons'; +import { useAuthStore, useNotificationStore, useThemeStore } from '@/stores'; +import { authFilesApi, usageApi } from '@/services/api'; +import { apiClient } from '@/services/api/client'; +import type { AuthFileItem } from '@/types'; +import type { KeyStats, KeyStatBucket, UsageDetail } from '@/utils/usage'; +import { collectUsageDetails, calculateStatusBarData } from '@/utils/usage'; +import { formatFileSize } from '@/utils/format'; +import styles from './AuthFilesPage.module.scss'; + +type ThemeColors = { bg: string; text: string; border?: string }; +type TypeColorSet = { light: ThemeColors; dark?: ThemeColors }; +type ResolvedTheme = 'light' | 'dark'; + +// 标签类型颜色配置(对齐重构前 styles.css 的 file-type-badge 颜色) const TYPE_COLORS: Record = { - qwen: { - light: { bg: '#e8f5e9', text: '#2e7d32' }, - dark: { bg: '#1b5e20', text: '#81c784' } - }, - gemini: { - light: { bg: '#e3f2fd', text: '#1565c0' }, - dark: { bg: '#0d47a1', text: '#64b5f6' } - }, - 'gemini-cli': { - light: { bg: '#e7efff', text: '#1e4fa3' }, - dark: { bg: '#1c3f73', text: '#a8c7ff' } - }, - aistudio: { - light: { bg: '#f0f2f5', text: '#2f343c' }, - dark: { bg: '#373c42', text: '#cfd3db' } - }, - claude: { - light: { bg: '#fce4ec', text: '#c2185b' }, - dark: { bg: '#880e4f', text: '#f48fb1' } - }, - codex: { - light: { bg: '#fff3e0', text: '#ef6c00' }, - dark: { bg: '#e65100', text: '#ffb74d' } - }, - antigravity: { - light: { bg: '#e0f7fa', text: '#006064' }, - dark: { bg: '#004d40', text: '#80deea' } - }, - iflow: { - light: { bg: '#f3e5f5', text: '#7b1fa2' }, - dark: { bg: '#4a148c', text: '#ce93d8' } - }, - empty: { - light: { bg: '#f5f5f5', text: '#616161' }, - dark: { bg: '#424242', text: '#bdbdbd' } - }, + qwen: { + light: { bg: '#e8f5e9', text: '#2e7d32' }, + dark: { bg: '#1b5e20', text: '#81c784' } + }, + gemini: { + light: { bg: '#e3f2fd', text: '#1565c0' }, + dark: { bg: '#0d47a1', text: '#64b5f6' } + }, + 'gemini-cli': { + light: { bg: '#e7efff', text: '#1e4fa3' }, + dark: { bg: '#1c3f73', text: '#a8c7ff' } + }, + aistudio: { + light: { bg: '#f0f2f5', text: '#2f343c' }, + dark: { bg: '#373c42', text: '#cfd3db' } + }, + claude: { + light: { bg: '#fce4ec', text: '#c2185b' }, + dark: { bg: '#880e4f', text: '#f48fb1' } + }, + codex: { + light: { bg: '#fff3e0', text: '#ef6c00' }, + dark: { bg: '#e65100', text: '#ffb74d' } + }, + antigravity: { + light: { bg: '#e0f7fa', text: '#006064' }, + dark: { bg: '#004d40', text: '#80deea' } + }, + iflow: { + light: { bg: '#f3e5f5', text: '#7b1fa2' }, + dark: { bg: '#4a148c', text: '#ce93d8' } + }, + empty: { + light: { bg: '#f5f5f5', text: '#616161' }, + dark: { bg: '#424242', text: '#bdbdbd' } + }, unknown: { light: { bg: '#f0f0f0', text: '#666666', border: '1px dashed #999999' }, dark: { bg: '#3a3a3a', text: '#aaaaaa', border: '1px dashed #666666' } } }; +const OAUTH_PROVIDER_PRESETS = [ + 'gemini', + 'gemini-cli', + 'vertex', + 'aistudio', + 'antigravity', + 'claude', + 'codex', + 'qwen', + 'iflow' +]; + +const OAUTH_PROVIDER_EXCLUDES = new Set(['all', 'unknown', 'empty']); + interface ExcludedFormState { provider: string; modelsText: string; } - -// 标准化 auth_index 值(与 usage.ts 中的 normalizeAuthIndex 保持一致) -function normalizeAuthIndexValue(value: unknown): string | null { - if (typeof value === 'number' && Number.isFinite(value)) { - return value.toString(); - } - if (typeof value === 'string') { - const trimmed = value.trim(); - return trimmed ? trimmed : null; - } - return null; -} - -function isRuntimeOnlyAuthFile(file: AuthFileItem): boolean { - const raw = file['runtime_only'] ?? file.runtimeOnly; - if (typeof raw === 'boolean') return raw; - if (typeof raw === 'string') return raw.trim().toLowerCase() === 'true'; - return false; -} - -// 解析认证文件的统计数据 -function resolveAuthFileStats( - file: AuthFileItem, - stats: KeyStats -): KeyStatBucket { - const defaultStats: KeyStatBucket = { success: 0, failure: 0 }; - const rawFileName = file?.name || ''; - - // 兼容 auth_index 和 authIndex 两种字段名(API 返回的是 auth_index) - const rawAuthIndex = file['auth_index'] ?? file.authIndex; - const authIndexKey = normalizeAuthIndexValue(rawAuthIndex); - - // 尝试根据 authIndex 匹配 - if (authIndexKey && stats.byAuthIndex?.[authIndexKey]) { - return stats.byAuthIndex[authIndexKey]; - } - - // 尝试根据 source (文件名) 匹配 - if (rawFileName && stats.bySource?.[rawFileName]) { - const fromName = stats.bySource[rawFileName]; - if (fromName.success > 0 || fromName.failure > 0) { - return fromName; - } - } - - // 尝试去掉扩展名后匹配 - if (rawFileName) { - const nameWithoutExt = rawFileName.replace(/\.[^/.]+$/, ''); - if (nameWithoutExt && nameWithoutExt !== rawFileName) { - const fromNameWithoutExt = stats.bySource?.[nameWithoutExt]; - if (fromNameWithoutExt && (fromNameWithoutExt.success > 0 || fromNameWithoutExt.failure > 0)) { - return fromNameWithoutExt; - } - } - } - - return defaultStats; -} - -export function AuthFilesPage() { - const { t } = useTranslation(); - const { showNotification } = useNotificationStore(); - const connectionStatus = useAuthStore((state) => state.connectionStatus); - const resolvedTheme: ResolvedTheme = useThemeStore((state) => state.resolvedTheme); - - const [files, setFiles] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(''); - const [filter, setFilter] = useState<'all' | string>('all'); - const [search, setSearch] = useState(''); - const [page, setPage] = useState(1); - const [pageSize, setPageSize] = useState(9); - const [uploading, setUploading] = useState(false); - const [deleting, setDeleting] = useState(null); - const [deletingAll, setDeletingAll] = useState(false); - const [keyStats, setKeyStats] = useState({ bySource: {}, byAuthIndex: {} }); - const [usageDetails, setUsageDetails] = useState([]); - - // 详情弹窗相关 - const [detailModalOpen, setDetailModalOpen] = useState(false); - const [selectedFile, setSelectedFile] = useState(null); - - // 模型列表弹窗相关 - const [modelsModalOpen, setModelsModalOpen] = useState(false); - const [modelsLoading, setModelsLoading] = useState(false); - const [modelsList, setModelsList] = useState<{ id: string; display_name?: string; type?: string }[]>([]); - const [modelsFileName, setModelsFileName] = useState(''); - const [modelsFileType, setModelsFileType] = useState(''); - const [modelsError, setModelsError] = useState<'unsupported' | null>(null); - - // OAuth 排除模型相关 - const [excluded, setExcluded] = useState>({}); - const [excludedError, setExcludedError] = useState<'unsupported' | null>(null); - const [excludedModalOpen, setExcludedModalOpen] = useState(false); - const [excludedForm, setExcludedForm] = useState({ provider: '', modelsText: '' }); - const [savingExcluded, setSavingExcluded] = useState(false); - - const fileInputRef = useRef(null); - const loadingKeyStatsRef = useRef(false); + +// 标准化 auth_index 值(与 usage.ts 中的 normalizeAuthIndex 保持一致) +function normalizeAuthIndexValue(value: unknown): string | null { + if (typeof value === 'number' && Number.isFinite(value)) { + return value.toString(); + } + if (typeof value === 'string') { + const trimmed = value.trim(); + return trimmed ? trimmed : null; + } + return null; +} + +function isRuntimeOnlyAuthFile(file: AuthFileItem): boolean { + const raw = file['runtime_only'] ?? file.runtimeOnly; + if (typeof raw === 'boolean') return raw; + if (typeof raw === 'string') return raw.trim().toLowerCase() === 'true'; + return false; +} + +// 解析认证文件的统计数据 +function resolveAuthFileStats( + file: AuthFileItem, + stats: KeyStats +): KeyStatBucket { + const defaultStats: KeyStatBucket = { success: 0, failure: 0 }; + const rawFileName = file?.name || ''; + + // 兼容 auth_index 和 authIndex 两种字段名(API 返回的是 auth_index) + const rawAuthIndex = file['auth_index'] ?? file.authIndex; + const authIndexKey = normalizeAuthIndexValue(rawAuthIndex); + + // 尝试根据 authIndex 匹配 + if (authIndexKey && stats.byAuthIndex?.[authIndexKey]) { + return stats.byAuthIndex[authIndexKey]; + } + + // 尝试根据 source (文件名) 匹配 + if (rawFileName && stats.bySource?.[rawFileName]) { + const fromName = stats.bySource[rawFileName]; + if (fromName.success > 0 || fromName.failure > 0) { + return fromName; + } + } + + // 尝试去掉扩展名后匹配 + if (rawFileName) { + const nameWithoutExt = rawFileName.replace(/\.[^/.]+$/, ''); + if (nameWithoutExt && nameWithoutExt !== rawFileName) { + const fromNameWithoutExt = stats.bySource?.[nameWithoutExt]; + if (fromNameWithoutExt && (fromNameWithoutExt.success > 0 || fromNameWithoutExt.failure > 0)) { + return fromNameWithoutExt; + } + } + } + + return defaultStats; +} + +export function AuthFilesPage() { + const { t } = useTranslation(); + const { showNotification } = useNotificationStore(); + const connectionStatus = useAuthStore((state) => state.connectionStatus); + const resolvedTheme: ResolvedTheme = useThemeStore((state) => state.resolvedTheme); + + const [files, setFiles] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(''); + const [filter, setFilter] = useState<'all' | string>('all'); + const [search, setSearch] = useState(''); + const [page, setPage] = useState(1); + const [pageSize, setPageSize] = useState(9); + const [uploading, setUploading] = useState(false); + const [deleting, setDeleting] = useState(null); + const [deletingAll, setDeletingAll] = useState(false); + const [keyStats, setKeyStats] = useState({ bySource: {}, byAuthIndex: {} }); + const [usageDetails, setUsageDetails] = useState([]); + + // 详情弹窗相关 + const [detailModalOpen, setDetailModalOpen] = useState(false); + const [selectedFile, setSelectedFile] = useState(null); + + // 模型列表弹窗相关 + const [modelsModalOpen, setModelsModalOpen] = useState(false); + const [modelsLoading, setModelsLoading] = useState(false); + const [modelsList, setModelsList] = useState<{ id: string; display_name?: string; type?: string }[]>([]); + const [modelsFileName, setModelsFileName] = useState(''); + const [modelsFileType, setModelsFileType] = useState(''); + const [modelsError, setModelsError] = useState<'unsupported' | null>(null); + + // OAuth 排除模型相关 + const [excluded, setExcluded] = useState>({}); + const [excludedError, setExcludedError] = useState<'unsupported' | null>(null); + const [excludedModalOpen, setExcludedModalOpen] = useState(false); + const [excludedForm, setExcludedForm] = useState({ provider: '', modelsText: '' }); + const [savingExcluded, setSavingExcluded] = useState(false); + + const fileInputRef = useRef(null); + const loadingKeyStatsRef = useRef(false); const excludedUnsupportedRef = useRef(false); const disableControls = connectionStatus !== 'connected'; - - // 格式化修改时间 - const formatModified = (item: AuthFileItem): string => { - const raw = item['modtime'] ?? item.modified; - if (!raw) return '-'; - const asNumber = Number(raw); - const date = - Number.isFinite(asNumber) && !Number.isNaN(asNumber) - ? new Date(asNumber < 1e12 ? asNumber * 1000 : asNumber) - : new Date(String(raw)); - return Number.isNaN(date.getTime()) ? '-' : date.toLocaleString(); - }; - - // 加载文件列表 - const loadFiles = useCallback(async () => { - setLoading(true); - setError(''); - try { - const data = await authFilesApi.list(); - setFiles(data?.files || []); - } catch (err: unknown) { - const errorMessage = err instanceof Error ? err.message : t('notification.refresh_failed'); - setError(errorMessage); - } finally { - setLoading(false); - } - }, [t]); - - // 加载 key 统计和 usage 明细(API 层已有60秒超时) - const loadKeyStats = useCallback(async () => { - // 防止重复请求 - if (loadingKeyStatsRef.current) return; - loadingKeyStatsRef.current = true; - try { - const usageResponse = await usageApi.getUsage(); - const usageData = usageResponse?.usage ?? usageResponse; - const stats = await usageApi.getKeyStats(usageData); - setKeyStats(stats); - // 收集 usage 明细用于状态栏 - const details = collectUsageDetails(usageData); - setUsageDetails(details); - } catch { - // 静默失败 - } finally { - loadingKeyStatsRef.current = false; - } - }, []); - - // 加载 OAuth 排除列表 - const loadExcluded = useCallback(async () => { - try { - const res = await authFilesApi.getOauthExcludedModels(); - excludedUnsupportedRef.current = false; - setExcluded(res || {}); - setExcludedError(null); - } catch (err: unknown) { - const status = - typeof err === 'object' && err !== null && 'status' in err - ? (err as { status?: unknown }).status - : undefined; - - if (status === 404) { - setExcluded({}); - setExcludedError('unsupported'); - if (!excludedUnsupportedRef.current) { - excludedUnsupportedRef.current = true; - showNotification(t('oauth_excluded.upgrade_required'), 'warning'); - } - return; - } - // 静默失败 - } - }, [showNotification, t]); - - useEffect(() => { - loadFiles(); - loadKeyStats(); - loadExcluded(); - }, [loadFiles, loadKeyStats, loadExcluded]); - - // 定时刷新状态数据(每240秒) - useInterval(loadKeyStats, 240_000); - + + // 格式化修改时间 + const formatModified = (item: AuthFileItem): string => { + const raw = item['modtime'] ?? item.modified; + if (!raw) return '-'; + const asNumber = Number(raw); + const date = + Number.isFinite(asNumber) && !Number.isNaN(asNumber) + ? new Date(asNumber < 1e12 ? asNumber * 1000 : asNumber) + : new Date(String(raw)); + return Number.isNaN(date.getTime()) ? '-' : date.toLocaleString(); + }; + + // 加载文件列表 + const loadFiles = useCallback(async () => { + setLoading(true); + setError(''); + try { + const data = await authFilesApi.list(); + setFiles(data?.files || []); + } catch (err: unknown) { + const errorMessage = err instanceof Error ? err.message : t('notification.refresh_failed'); + setError(errorMessage); + } finally { + setLoading(false); + } + }, [t]); + + // 加载 key 统计和 usage 明细(API 层已有60秒超时) + const loadKeyStats = useCallback(async () => { + // 防止重复请求 + if (loadingKeyStatsRef.current) return; + loadingKeyStatsRef.current = true; + try { + const usageResponse = await usageApi.getUsage(); + const usageData = usageResponse?.usage ?? usageResponse; + const stats = await usageApi.getKeyStats(usageData); + setKeyStats(stats); + // 收集 usage 明细用于状态栏 + const details = collectUsageDetails(usageData); + setUsageDetails(details); + } catch { + // 静默失败 + } finally { + loadingKeyStatsRef.current = false; + } + }, []); + + // 加载 OAuth 排除列表 + const loadExcluded = useCallback(async () => { + try { + const res = await authFilesApi.getOauthExcludedModels(); + excludedUnsupportedRef.current = false; + setExcluded(res || {}); + setExcludedError(null); + } catch (err: unknown) { + const status = + typeof err === 'object' && err !== null && 'status' in err + ? (err as { status?: unknown }).status + : undefined; + + if (status === 404) { + setExcluded({}); + setExcludedError('unsupported'); + if (!excludedUnsupportedRef.current) { + excludedUnsupportedRef.current = true; + showNotification(t('oauth_excluded.upgrade_required'), 'warning'); + } + return; + } + // 静默失败 + } + }, [showNotification, t]); + + useEffect(() => { + loadFiles(); + loadKeyStats(); + loadExcluded(); + }, [loadFiles, loadKeyStats, loadExcluded]); + + // 定时刷新状态数据(每240秒) + useInterval(loadKeyStats, 240_000); + // 提取所有存在的类型 const existingTypes = useMemo(() => { const types = new Set(['all']); files.forEach((file) => { - if (file.type) { - types.add(file.type); - } - }); + if (file.type) { + types.add(file.type); + } + }); return Array.from(types); }, [files]); + const excludedProviderLookup = useMemo(() => { + const lookup = new Map(); + Object.keys(excluded).forEach((provider) => { + const key = provider.trim().toLowerCase(); + if (key && !lookup.has(key)) { + lookup.set(key, provider); + } + }); + return lookup; + }, [excluded]); + + const providerOptions = useMemo(() => { + const extraProviders = new Set(); + + Object.keys(excluded).forEach((provider) => { + extraProviders.add(provider); + }); + files.forEach((file) => { + if (typeof file.type === 'string') { + extraProviders.add(file.type); + } + if (typeof file.provider === 'string') { + extraProviders.add(file.provider); + } + }); + + const normalizedExtras = Array.from(extraProviders) + .map((value) => value.trim()) + .filter((value) => value && !OAUTH_PROVIDER_EXCLUDES.has(value.toLowerCase())); + + const baseSet = new Set(OAUTH_PROVIDER_PRESETS.map((value) => value.toLowerCase())); + const extraList = normalizedExtras + .filter((value) => !baseSet.has(value.toLowerCase())) + .sort((a, b) => a.localeCompare(b)); + + return [...OAUTH_PROVIDER_PRESETS, ...extraList]; + }, [excluded, files]); + // 过滤和搜索 const filtered = useMemo(() => { return files.filter((item) => { - const matchType = filter === 'all' || item.type === filter; - const term = search.trim().toLowerCase(); - const matchSearch = - !term || - item.name.toLowerCase().includes(term) || - (item.type || '').toString().toLowerCase().includes(term) || - (item.provider || '').toString().toLowerCase().includes(term); - return matchType && matchSearch; - }); - }, [files, filter, search]); - - // 分页计算 - const totalPages = Math.max(1, Math.ceil(filtered.length / pageSize)); - const currentPage = Math.min(page, totalPages); - const start = (currentPage - 1) * pageSize; - const pageItems = filtered.slice(start, start + pageSize); - - // 统计信息 - const totalSize = useMemo(() => files.reduce((sum, item) => sum + (item.size || 0), 0), [files]); - - // 点击上传 - const handleUploadClick = () => { - fileInputRef.current?.click(); - }; - - // 处理文件上传(支持多选) - const handleFileChange = async (event: React.ChangeEvent) => { - const fileList = event.target.files; - if (!fileList || fileList.length === 0) return; - - const filesToUpload = Array.from(fileList); - const validFiles: File[] = []; - const invalidFiles: string[] = []; - - filesToUpload.forEach((file) => { - if (file.name.endsWith('.json')) { - validFiles.push(file); - } else { - invalidFiles.push(file.name); - } - }); - - if (invalidFiles.length > 0) { - showNotification(t('auth_files.upload_error_json'), 'error'); - } - - if (validFiles.length === 0) { - event.target.value = ''; - return; - } - - setUploading(true); - let successCount = 0; - const failed: { name: string; message: string }[] = []; - - for (const file of validFiles) { - try { - await authFilesApi.upload(file); - successCount++; - } catch (err: unknown) { - const errorMessage = err instanceof Error ? err.message : 'Unknown error'; - failed.push({ name: file.name, message: errorMessage }); - } - } - - if (successCount > 0) { - const suffix = validFiles.length > 1 ? ` (${successCount}/${validFiles.length})` : ''; - showNotification(`${t('auth_files.upload_success')}${suffix}`, failed.length ? 'warning' : 'success'); - await loadFiles(); - await loadKeyStats(); - } - - if (failed.length > 0) { - const details = failed.map((item) => `${item.name}: ${item.message}`).join('; '); - showNotification(`${t('notification.upload_failed')}: ${details}`, 'error'); - } - - setUploading(false); - event.target.value = ''; - }; - - // 删除单个文件 - const handleDelete = async (name: string) => { - if (!window.confirm(`${t('auth_files.delete_confirm')} "${name}" ?`)) return; - setDeleting(name); - try { - await authFilesApi.deleteFile(name); - showNotification(t('auth_files.delete_success'), 'success'); - setFiles((prev) => prev.filter((item) => item.name !== name)); - } catch (err: unknown) { - const errorMessage = err instanceof Error ? err.message : ''; - showNotification(`${t('notification.delete_failed')}: ${errorMessage}`, 'error'); - } finally { - setDeleting(null); - } - }; - - // 删除全部(根据筛选类型) - const handleDeleteAll = async () => { - const isFiltered = filter !== 'all'; - const typeLabel = isFiltered ? getTypeLabel(filter) : t('auth_files.filter_all'); - const confirmMessage = isFiltered - ? t('auth_files.delete_filtered_confirm', { type: typeLabel }) - : t('auth_files.delete_all_confirm'); - - if (!window.confirm(confirmMessage)) return; - - setDeletingAll(true); - try { - if (!isFiltered) { - // 删除全部 - await authFilesApi.deleteAll(); - showNotification(t('auth_files.delete_all_success'), 'success'); - setFiles((prev) => prev.filter((file) => isRuntimeOnlyAuthFile(file))); - } else { - // 删除筛选类型的文件 - const filesToDelete = files.filter( - (f) => f.type === filter && !isRuntimeOnlyAuthFile(f) - ); - - if (filesToDelete.length === 0) { - showNotification(t('auth_files.delete_filtered_none', { type: typeLabel }), 'info'); - setDeletingAll(false); - return; - } - - let success = 0; - let failed = 0; - const deletedNames: string[] = []; - - for (const file of filesToDelete) { - try { - await authFilesApi.deleteFile(file.name); - success++; - deletedNames.push(file.name); - } catch { - failed++; - } - } - - setFiles((prev) => prev.filter((f) => !deletedNames.includes(f.name))); - - if (failed === 0) { - showNotification( - t('auth_files.delete_filtered_success', { count: success, type: typeLabel }), - 'success' - ); - } else { - showNotification( - t('auth_files.delete_filtered_partial', { success, failed, type: typeLabel }), - 'warning' - ); - } - setFilter('all'); - } - } catch (err: unknown) { - const errorMessage = err instanceof Error ? err.message : ''; - showNotification(`${t('notification.delete_failed')}: ${errorMessage}`, 'error'); - } finally { - setDeletingAll(false); - } - }; - - // 下载文件 - const handleDownload = async (name: string) => { - try { - const response = await apiClient.getRaw(`/auth-files/download?name=${encodeURIComponent(name)}`, { - responseType: 'blob' - }); - const blob = new Blob([response.data]); - const url = window.URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = name; - a.click(); - window.URL.revokeObjectURL(url); - showNotification(t('auth_files.download_success'), 'success'); - } catch (err: unknown) { - const errorMessage = err instanceof Error ? err.message : ''; - showNotification(`${t('notification.download_failed')}: ${errorMessage}`, 'error'); - } - }; - - // 显示详情弹窗 - const showDetails = (file: AuthFileItem) => { - setSelectedFile(file); - setDetailModalOpen(true); - }; - - // 显示模型列表 - const showModels = async (item: AuthFileItem) => { - setModelsFileName(item.name); - setModelsFileType(item.type || ''); - setModelsList([]); - setModelsError(null); - setModelsModalOpen(true); - setModelsLoading(true); - try { - const models = await authFilesApi.getModelsForAuthFile(item.name); - setModelsList(models); - } catch (err) { - // 检测是否是 API 不支持的错误 (404 或特定错误消息) - const errorMessage = err instanceof Error ? err.message : ''; - if (errorMessage.includes('404') || errorMessage.includes('not found') || errorMessage.includes('Not Found')) { - setModelsError('unsupported'); - } else { - showNotification(`${t('notification.load_failed')}: ${errorMessage}`, 'error'); - } - } finally { - setModelsLoading(false); - } - }; - - // 检查模型是否被 OAuth 排除 - const isModelExcluded = (modelId: string, providerType: string): boolean => { - const excludedModels = excluded[providerType] || []; - return excludedModels.some(pattern => { - if (pattern.includes('*')) { - // 支持通配符匹配 - const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$', 'i'); - return regex.test(modelId); - } - return pattern.toLowerCase() === modelId.toLowerCase(); - }); - }; - - // 获取类型标签显示文本 - const getTypeLabel = (type: string): string => { - const key = `auth_files.filter_${type}`; - const translated = t(key); - if (translated !== key) return translated; - if (type.toLowerCase() === 'iflow') return 'iFlow'; - return type.charAt(0).toUpperCase() + type.slice(1); - }; - - // 获取类型颜色 - const getTypeColor = (type: string): ThemeColors => { - const set = TYPE_COLORS[type] || TYPE_COLORS.unknown; - return resolvedTheme === 'dark' && set.dark ? set.dark : set.light; - }; - + const matchType = filter === 'all' || item.type === filter; + const term = search.trim().toLowerCase(); + const matchSearch = + !term || + item.name.toLowerCase().includes(term) || + (item.type || '').toString().toLowerCase().includes(term) || + (item.provider || '').toString().toLowerCase().includes(term); + return matchType && matchSearch; + }); + }, [files, filter, search]); + + // 分页计算 + const totalPages = Math.max(1, Math.ceil(filtered.length / pageSize)); + const currentPage = Math.min(page, totalPages); + const start = (currentPage - 1) * pageSize; + const pageItems = filtered.slice(start, start + pageSize); + + // 统计信息 + const totalSize = useMemo(() => files.reduce((sum, item) => sum + (item.size || 0), 0), [files]); + + // 点击上传 + const handleUploadClick = () => { + fileInputRef.current?.click(); + }; + + // 处理文件上传(支持多选) + const handleFileChange = async (event: React.ChangeEvent) => { + const fileList = event.target.files; + if (!fileList || fileList.length === 0) return; + + const filesToUpload = Array.from(fileList); + const validFiles: File[] = []; + const invalidFiles: string[] = []; + + filesToUpload.forEach((file) => { + if (file.name.endsWith('.json')) { + validFiles.push(file); + } else { + invalidFiles.push(file.name); + } + }); + + if (invalidFiles.length > 0) { + showNotification(t('auth_files.upload_error_json'), 'error'); + } + + if (validFiles.length === 0) { + event.target.value = ''; + return; + } + + setUploading(true); + let successCount = 0; + const failed: { name: string; message: string }[] = []; + + for (const file of validFiles) { + try { + await authFilesApi.upload(file); + successCount++; + } catch (err: unknown) { + const errorMessage = err instanceof Error ? err.message : 'Unknown error'; + failed.push({ name: file.name, message: errorMessage }); + } + } + + if (successCount > 0) { + const suffix = validFiles.length > 1 ? ` (${successCount}/${validFiles.length})` : ''; + showNotification(`${t('auth_files.upload_success')}${suffix}`, failed.length ? 'warning' : 'success'); + await loadFiles(); + await loadKeyStats(); + } + + if (failed.length > 0) { + const details = failed.map((item) => `${item.name}: ${item.message}`).join('; '); + showNotification(`${t('notification.upload_failed')}: ${details}`, 'error'); + } + + setUploading(false); + event.target.value = ''; + }; + + // 删除单个文件 + const handleDelete = async (name: string) => { + if (!window.confirm(`${t('auth_files.delete_confirm')} "${name}" ?`)) return; + setDeleting(name); + try { + await authFilesApi.deleteFile(name); + showNotification(t('auth_files.delete_success'), 'success'); + setFiles((prev) => prev.filter((item) => item.name !== name)); + } catch (err: unknown) { + const errorMessage = err instanceof Error ? err.message : ''; + showNotification(`${t('notification.delete_failed')}: ${errorMessage}`, 'error'); + } finally { + setDeleting(null); + } + }; + + // 删除全部(根据筛选类型) + const handleDeleteAll = async () => { + const isFiltered = filter !== 'all'; + const typeLabel = isFiltered ? getTypeLabel(filter) : t('auth_files.filter_all'); + const confirmMessage = isFiltered + ? t('auth_files.delete_filtered_confirm', { type: typeLabel }) + : t('auth_files.delete_all_confirm'); + + if (!window.confirm(confirmMessage)) return; + + setDeletingAll(true); + try { + if (!isFiltered) { + // 删除全部 + await authFilesApi.deleteAll(); + showNotification(t('auth_files.delete_all_success'), 'success'); + setFiles((prev) => prev.filter((file) => isRuntimeOnlyAuthFile(file))); + } else { + // 删除筛选类型的文件 + const filesToDelete = files.filter( + (f) => f.type === filter && !isRuntimeOnlyAuthFile(f) + ); + + if (filesToDelete.length === 0) { + showNotification(t('auth_files.delete_filtered_none', { type: typeLabel }), 'info'); + setDeletingAll(false); + return; + } + + let success = 0; + let failed = 0; + const deletedNames: string[] = []; + + for (const file of filesToDelete) { + try { + await authFilesApi.deleteFile(file.name); + success++; + deletedNames.push(file.name); + } catch { + failed++; + } + } + + setFiles((prev) => prev.filter((f) => !deletedNames.includes(f.name))); + + if (failed === 0) { + showNotification( + t('auth_files.delete_filtered_success', { count: success, type: typeLabel }), + 'success' + ); + } else { + showNotification( + t('auth_files.delete_filtered_partial', { success, failed, type: typeLabel }), + 'warning' + ); + } + setFilter('all'); + } + } catch (err: unknown) { + const errorMessage = err instanceof Error ? err.message : ''; + showNotification(`${t('notification.delete_failed')}: ${errorMessage}`, 'error'); + } finally { + setDeletingAll(false); + } + }; + + // 下载文件 + const handleDownload = async (name: string) => { + try { + const response = await apiClient.getRaw(`/auth-files/download?name=${encodeURIComponent(name)}`, { + responseType: 'blob' + }); + const blob = new Blob([response.data]); + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = name; + a.click(); + window.URL.revokeObjectURL(url); + showNotification(t('auth_files.download_success'), 'success'); + } catch (err: unknown) { + const errorMessage = err instanceof Error ? err.message : ''; + showNotification(`${t('notification.download_failed')}: ${errorMessage}`, 'error'); + } + }; + + // 显示详情弹窗 + const showDetails = (file: AuthFileItem) => { + setSelectedFile(file); + setDetailModalOpen(true); + }; + + // 显示模型列表 + const showModels = async (item: AuthFileItem) => { + setModelsFileName(item.name); + setModelsFileType(item.type || ''); + setModelsList([]); + setModelsError(null); + setModelsModalOpen(true); + setModelsLoading(true); + try { + const models = await authFilesApi.getModelsForAuthFile(item.name); + setModelsList(models); + } catch (err) { + // 检测是否是 API 不支持的错误 (404 或特定错误消息) + const errorMessage = err instanceof Error ? err.message : ''; + if (errorMessage.includes('404') || errorMessage.includes('not found') || errorMessage.includes('Not Found')) { + setModelsError('unsupported'); + } else { + showNotification(`${t('notification.load_failed')}: ${errorMessage}`, 'error'); + } + } finally { + setModelsLoading(false); + } + }; + + // 检查模型是否被 OAuth 排除 + const isModelExcluded = (modelId: string, providerType: string): boolean => { + const excludedModels = excluded[providerType] || []; + return excludedModels.some(pattern => { + if (pattern.includes('*')) { + // 支持通配符匹配 + const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$', 'i'); + return regex.test(modelId); + } + return pattern.toLowerCase() === modelId.toLowerCase(); + }); + }; + + // 获取类型标签显示文本 + const getTypeLabel = (type: string): string => { + const key = `auth_files.filter_${type}`; + const translated = t(key); + if (translated !== key) return translated; + if (type.toLowerCase() === 'iflow') return 'iFlow'; + return type.charAt(0).toUpperCase() + type.slice(1); + }; + + // 获取类型颜色 + const getTypeColor = (type: string): ThemeColors => { + const set = TYPE_COLORS[type] || TYPE_COLORS.unknown; + return resolvedTheme === 'dark' && set.dark ? set.dark : set.light; + }; + // OAuth 排除相关方法 const openExcludedModal = (provider?: string) => { - const models = provider ? excluded[provider] : []; + const normalizedProvider = (provider || '').trim(); + const fallbackProvider = normalizedProvider || (filter !== 'all' ? String(filter) : ''); + const lookupKey = fallbackProvider + ? excludedProviderLookup.get(fallbackProvider.toLowerCase()) + : undefined; + const models = lookupKey ? excluded[lookupKey] : []; setExcludedForm({ - provider: provider || '', + provider: lookupKey || fallbackProvider, modelsText: Array.isArray(models) ? models.join('\n') : '' }); setExcludedModalOpen(true); }; - - const saveExcludedModels = async () => { - const provider = excludedForm.provider.trim(); - if (!provider) { - showNotification(t('oauth_excluded.provider_required'), 'error'); - return; - } - const models = excludedForm.modelsText - .split(/[\n,]+/) - .map((item) => item.trim()) - .filter(Boolean); - setSavingExcluded(true); - try { - if (models.length) { - await authFilesApi.saveOauthExcludedModels(provider, models); - } else { - await authFilesApi.deleteOauthExcludedEntry(provider); - } - await loadExcluded(); - showNotification(t('oauth_excluded.save_success'), 'success'); - setExcludedModalOpen(false); - } catch (err: unknown) { - const errorMessage = err instanceof Error ? err.message : ''; - showNotification(`${t('oauth_excluded.save_failed')}: ${errorMessage}`, 'error'); - } finally { - setSavingExcluded(false); - } - }; - - const deleteExcluded = async (provider: string) => { - if (!window.confirm(t('oauth_excluded.delete_confirm', { provider }))) return; - try { - await authFilesApi.deleteOauthExcludedEntry(provider); - await loadExcluded(); - showNotification(t('oauth_excluded.delete_success'), 'success'); - } catch (err: unknown) { - const errorMessage = err instanceof Error ? err.message : ''; - showNotification(`${t('oauth_excluded.delete_failed')}: ${errorMessage}`, 'error'); - } - }; - - // 渲染标签筛选器 - const renderFilterTags = () => ( -
- {existingTypes.map((type) => { - const isActive = filter === type; - const color = type === 'all' ? { bg: 'var(--bg-tertiary)', text: 'var(--text-primary)' } : getTypeColor(type); - const activeTextColor = resolvedTheme === 'dark' ? '#111827' : '#fff'; - return ( - - ); - })} -
- ); - - // 预计算所有认证文件的状态栏数据(避免每次渲染重复计算) - const statusBarCache = useMemo(() => { - const cache = new Map>(); - - files.forEach((file) => { - const rawAuthIndex = file['auth_index'] ?? file.authIndex; - const authIndexKey = normalizeAuthIndexValue(rawAuthIndex); - - if (authIndexKey) { - // 过滤出属于该认证文件的 usage 明细 - const filteredDetails = usageDetails.filter((detail) => { - const detailAuthIndex = normalizeAuthIndexValue(detail.auth_index); - return detailAuthIndex !== null && detailAuthIndex === authIndexKey; - }); - cache.set(authIndexKey, calculateStatusBarData(filteredDetails)); - } - }); - - return cache; - }, [usageDetails, files]); - - // 渲染状态监测栏 - const renderStatusBar = (item: AuthFileItem) => { - // 认证文件使用 authIndex 来匹配 usage 数据 - const rawAuthIndex = item['auth_index'] ?? item.authIndex; - const authIndexKey = normalizeAuthIndexValue(rawAuthIndex); - - const statusData = (authIndexKey && statusBarCache.get(authIndexKey)) || calculateStatusBarData([]); - const hasData = statusData.totalSuccess + statusData.totalFailure > 0; - const rateClass = !hasData - ? '' - : statusData.successRate >= 90 - ? styles.statusRateHigh - : statusData.successRate >= 50 - ? styles.statusRateMedium - : styles.statusRateLow; - - return ( -
-
- {statusData.blocks.map((state, idx) => { - const blockClass = - state === 'success' - ? styles.statusBlockSuccess - : state === 'failure' - ? styles.statusBlockFailure - : state === 'mixed' - ? styles.statusBlockMixed - : styles.statusBlockIdle; - return
; - })} -
- - {hasData ? `${statusData.successRate.toFixed(1)}%` : '--'} - -
- ); - }; - - // 渲染单个认证文件卡片 - const renderFileCard = (item: AuthFileItem) => { - const fileStats = resolveAuthFileStats(item, keyStats); - const isRuntimeOnly = isRuntimeOnlyAuthFile(item); - const typeColor = getTypeColor(item.type || 'unknown'); - - return ( -
-
- - {getTypeLabel(item.type || 'unknown')} - - {item.name} -
- -
- {t('auth_files.file_size')}: {item.size ? formatFileSize(item.size) : '-'} - {t('auth_files.file_modified')}: {formatModified(item)} -
- -
- - {t('stats.success')}: {fileStats.success} - - - {t('stats.failure')}: {fileStats.failure} - -
- - {/* 状态监测栏 */} - {renderStatusBar(item)} - -
- {isRuntimeOnly ? ( -
{t('auth_files.type_virtual') || '虚拟认证文件'}
- ) : ( - <> - - - - - - )} -
-
- ); - }; - - return ( -
-
-

{t('auth_files.title')}

-

{t('auth_files.description')}

-
- - - - - - -
- } - > - {error &&
{error}
} - - {/* 筛选区域 */} -
- {renderFilterTags()} - -
-
- - { - setSearch(e.target.value); - setPage(1); - }} - placeholder={t('auth_files.search_placeholder')} - /> -
-
- - -
-
- -
- {files.length} {t('auth_files.files_count')} · {formatFileSize(totalSize)} -
-
-
-
- - {/* 卡片网格 */} - {loading ? ( -
{t('common.loading')}
- ) : pageItems.length === 0 ? ( - - ) : ( -
- {pageItems.map(renderFileCard)} -
- )} - - {/* 分页 */} - {!loading && filtered.length > pageSize && ( -
- -
- {t('auth_files.pagination_info', { - current: currentPage, - total: totalPages, - count: filtered.length - })} -
- -
- )} - - - {/* OAuth 排除列表卡片 */} - openExcludedModal()} - disabled={disableControls || excludedError === 'unsupported'} - > - {t('oauth_excluded.add')} - - } - > - {excludedError === 'unsupported' ? ( - - ) : Object.keys(excluded).length === 0 ? ( - - ) : ( -
- {Object.entries(excluded).map(([provider, models]) => ( -
-
-
{provider}
-
- {models?.length - ? t('oauth_excluded.model_count', { count: models.length }) - : t('oauth_excluded.no_models')} -
-
-
- - -
-
- ))} -
- )} -
- - {/* 详情弹窗 */} - setDetailModalOpen(false)} - title={selectedFile?.name || t('auth_files.title_section')} - footer={ - <> - - - - } - > - {selectedFile && ( -
-
{JSON.stringify(selectedFile, null, 2)}
-
- )} -
- - {/* 模型列表弹窗 */} - setModelsModalOpen(false)} - title={t('auth_files.models_title', { defaultValue: '支持的模型' }) + ` - ${modelsFileName}`} - footer={ - - } - > - {modelsLoading ? ( -
{t('auth_files.models_loading', { defaultValue: '正在加载模型列表...' })}
- ) : modelsError === 'unsupported' ? ( - - ) : modelsList.length === 0 ? ( - - ) : ( -
- {modelsList.map((model) => { - const isExcluded = isModelExcluded(model.id, modelsFileType); - return ( -
{ - navigator.clipboard.writeText(model.id); - showNotification(t('notification.link_copied', { defaultValue: '已复制到剪贴板' }), 'success'); - }} - title={isExcluded ? t('auth_files.models_excluded_hint', { defaultValue: '此模型已被 OAuth 排除' }) : t('common.copy', { defaultValue: '点击复制' })} - > - {model.id} - {model.display_name && model.display_name !== model.id && ( - {model.display_name} - )} - {model.type && ( - {model.type} - )} - {isExcluded && ( - {t('auth_files.models_excluded_badge', { defaultValue: '已排除' })} - )} -
- ); - })} -
- )} -
- + + const saveExcludedModels = async () => { + const provider = excludedForm.provider.trim(); + if (!provider) { + showNotification(t('oauth_excluded.provider_required'), 'error'); + return; + } + const models = excludedForm.modelsText + .split(/[\n,]+/) + .map((item) => item.trim()) + .filter(Boolean); + setSavingExcluded(true); + try { + if (models.length) { + await authFilesApi.saveOauthExcludedModels(provider, models); + } else { + await authFilesApi.deleteOauthExcludedEntry(provider); + } + await loadExcluded(); + showNotification(t('oauth_excluded.save_success'), 'success'); + setExcludedModalOpen(false); + } catch (err: unknown) { + const errorMessage = err instanceof Error ? err.message : ''; + showNotification(`${t('oauth_excluded.save_failed')}: ${errorMessage}`, 'error'); + } finally { + setSavingExcluded(false); + } + }; + + const deleteExcluded = async (provider: string) => { + if (!window.confirm(t('oauth_excluded.delete_confirm', { provider }))) return; + try { + await authFilesApi.deleteOauthExcludedEntry(provider); + await loadExcluded(); + showNotification(t('oauth_excluded.delete_success'), 'success'); + } catch (err: unknown) { + const errorMessage = err instanceof Error ? err.message : ''; + showNotification(`${t('oauth_excluded.delete_failed')}: ${errorMessage}`, 'error'); + } + }; + + // 渲染标签筛选器 + const renderFilterTags = () => ( +
+ {existingTypes.map((type) => { + const isActive = filter === type; + const color = type === 'all' ? { bg: 'var(--bg-tertiary)', text: 'var(--text-primary)' } : getTypeColor(type); + const activeTextColor = resolvedTheme === 'dark' ? '#111827' : '#fff'; + return ( + + ); + })} +
+ ); + + // 预计算所有认证文件的状态栏数据(避免每次渲染重复计算) + const statusBarCache = useMemo(() => { + const cache = new Map>(); + + files.forEach((file) => { + const rawAuthIndex = file['auth_index'] ?? file.authIndex; + const authIndexKey = normalizeAuthIndexValue(rawAuthIndex); + + if (authIndexKey) { + // 过滤出属于该认证文件的 usage 明细 + const filteredDetails = usageDetails.filter((detail) => { + const detailAuthIndex = normalizeAuthIndexValue(detail.auth_index); + return detailAuthIndex !== null && detailAuthIndex === authIndexKey; + }); + cache.set(authIndexKey, calculateStatusBarData(filteredDetails)); + } + }); + + return cache; + }, [usageDetails, files]); + + // 渲染状态监测栏 + const renderStatusBar = (item: AuthFileItem) => { + // 认证文件使用 authIndex 来匹配 usage 数据 + const rawAuthIndex = item['auth_index'] ?? item.authIndex; + const authIndexKey = normalizeAuthIndexValue(rawAuthIndex); + + const statusData = (authIndexKey && statusBarCache.get(authIndexKey)) || calculateStatusBarData([]); + const hasData = statusData.totalSuccess + statusData.totalFailure > 0; + const rateClass = !hasData + ? '' + : statusData.successRate >= 90 + ? styles.statusRateHigh + : statusData.successRate >= 50 + ? styles.statusRateMedium + : styles.statusRateLow; + + return ( +
+
+ {statusData.blocks.map((state, idx) => { + const blockClass = + state === 'success' + ? styles.statusBlockSuccess + : state === 'failure' + ? styles.statusBlockFailure + : state === 'mixed' + ? styles.statusBlockMixed + : styles.statusBlockIdle; + return
; + })} +
+ + {hasData ? `${statusData.successRate.toFixed(1)}%` : '--'} + +
+ ); + }; + + // 渲染单个认证文件卡片 + const renderFileCard = (item: AuthFileItem) => { + const fileStats = resolveAuthFileStats(item, keyStats); + const isRuntimeOnly = isRuntimeOnlyAuthFile(item); + const typeColor = getTypeColor(item.type || 'unknown'); + + return ( +
+
+ + {getTypeLabel(item.type || 'unknown')} + + {item.name} +
+ +
+ {t('auth_files.file_size')}: {item.size ? formatFileSize(item.size) : '-'} + {t('auth_files.file_modified')}: {formatModified(item)} +
+ +
+ + {t('stats.success')}: {fileStats.success} + + + {t('stats.failure')}: {fileStats.failure} + +
+ + {/* 状态监测栏 */} + {renderStatusBar(item)} + +
+ {isRuntimeOnly ? ( +
{t('auth_files.type_virtual') || '虚拟认证文件'}
+ ) : ( + <> + + + + + + )} +
+
+ ); + }; + + return ( +
+
+

{t('auth_files.title')}

+

{t('auth_files.description')}

+
+ + + + + + +
+ } + > + {error &&
{error}
} + + {/* 筛选区域 */} +
+ {renderFilterTags()} + +
+
+ + { + setSearch(e.target.value); + setPage(1); + }} + placeholder={t('auth_files.search_placeholder')} + /> +
+
+ + +
+
+ +
+ {files.length} {t('auth_files.files_count')} · {formatFileSize(totalSize)} +
+
+
+
+ + {/* 卡片网格 */} + {loading ? ( +
{t('common.loading')}
+ ) : pageItems.length === 0 ? ( + + ) : ( +
+ {pageItems.map(renderFileCard)} +
+ )} + + {/* 分页 */} + {!loading && filtered.length > pageSize && ( +
+ +
+ {t('auth_files.pagination_info', { + current: currentPage, + total: totalPages, + count: filtered.length + })} +
+ +
+ )} + + + {/* OAuth 排除列表卡片 */} + openExcludedModal()} + disabled={disableControls || excludedError === 'unsupported'} + > + {t('oauth_excluded.add')} + + } + > + {excludedError === 'unsupported' ? ( + + ) : Object.keys(excluded).length === 0 ? ( + + ) : ( +
+ {Object.entries(excluded).map(([provider, models]) => ( +
+
+
{provider}
+
+ {models?.length + ? t('oauth_excluded.model_count', { count: models.length }) + : t('oauth_excluded.no_models')} +
+
+
+ + +
+
+ ))} +
+ )} +
+ + {/* 详情弹窗 */} + setDetailModalOpen(false)} + title={selectedFile?.name || t('auth_files.title_section')} + footer={ + <> + + + + } + > + {selectedFile && ( +
+
{JSON.stringify(selectedFile, null, 2)}
+
+ )} +
+ + {/* 模型列表弹窗 */} + setModelsModalOpen(false)} + title={t('auth_files.models_title', { defaultValue: '支持的模型' }) + ` - ${modelsFileName}`} + footer={ + + } + > + {modelsLoading ? ( +
{t('auth_files.models_loading', { defaultValue: '正在加载模型列表...' })}
+ ) : modelsError === 'unsupported' ? ( + + ) : modelsList.length === 0 ? ( + + ) : ( +
+ {modelsList.map((model) => { + const isExcluded = isModelExcluded(model.id, modelsFileType); + return ( +
{ + navigator.clipboard.writeText(model.id); + showNotification(t('notification.link_copied', { defaultValue: '已复制到剪贴板' }), 'success'); + }} + title={isExcluded ? t('auth_files.models_excluded_hint', { defaultValue: '此模型已被 OAuth 排除' }) : t('common.copy', { defaultValue: '点击复制' })} + > + {model.id} + {model.display_name && model.display_name !== model.id && ( + {model.display_name} + )} + {model.type && ( + {model.type} + )} + {isExcluded && ( + {t('auth_files.models_excluded_badge', { defaultValue: '已排除' })} + )} +
+ ); + })} +
+ )} +
+ {/* OAuth 排除弹窗 */} setExcludedModalOpen(false)} title={t('oauth_excluded.add_title')} - footer={ - <> - - + footer={ + <> + + } > - setExcludedForm((prev) => ({ ...prev, provider: e.target.value }))} - /> +
+ setExcludedForm((prev) => ({ ...prev, provider: e.target.value }))} + /> + + {providerOptions.map((provider) => ( + + {providerOptions.length > 0 && ( +
+ {providerOptions.map((provider) => { + const isActive = + excludedForm.provider.trim().toLowerCase() === provider.toLowerCase(); + return ( + + ); + })} +
+ )} +