'use client' import { useState, useRef, useEffect } from 'react' import { useToast } from '@/hooks/useToast' import ToastBubble from './ToastBubble' import { formatHumanSpeed } from '@/lib/human-readable-transfer' interface NetworkDiagnoseModalProps { isOpen: boolean onClose: () => void } interface TestResult { type: | 'network-env' // 网络环境检测 | 'connection' // DNS和连接测试 | 'download' // 下载测速 | 'upload' // 上传测速 | 'storage' // 存储连通性 | 'preview-image' // 预览图片加载 | 'preview-video' // 预览视频加载 | 'preview-audio' // 音频文件加载 | 'proxy-performance' // 代理性能测试 | 'video-range' // 视频Range请求测试 | 'intranet-minio' // 内网MinIO直连测试 | 'public-s3' // 公网S3直连测试 | 'concurrent' // 并发测试 | 'cache' // 缓存测试 | 's3-performance' // S3存储性能分析 | 'api-materials-list' // API: 素材列表 | 'api-material-detail' // API: 素材详情 | 'api-material-stats' // API: 素材统计 | 'api-upload-init' // API: 上传初始化 | 'api-download' // API: 下载接口 | 'api-user-profile' // API: 用户信息 | 'api-system-settings' // API: 系统设置 | 'api-health' // API: 健康检查 | 'database-query' // 数据库查询性能 | 'redis-connection' // Redis连接测试 | 'page-home-load' // 页面: 首页加载 | 'page-detail-load' // 页面: 详情页加载 | 'upload-flow' // 上传流程测试 | 'download-flow' // 下载流程测试 | 'transcode-service' // 转码服务测试 | 'auth-session' // 认证Session测试 | 'intranet-url-rewrite' // 内网用户URL重写测试 | 'public-url-rewrite' // 外网用户URL重写测试 status: 'testing' | 'success' | 'error' | 'warning' speed?: number message?: string details?: { dnsTime?: number connectTime?: number ttfb?: number downloadTime?: number totalTime?: number fileSize?: number url?: string [key: string]: any // 允许其他详细信息 } } const PUBLIC_S3_DOMAINS = ['s3.jiansun.vip', 's3.1080p.hk'] function isPublicS3Url(url: string): boolean { return PUBLIC_S3_DOMAINS.some((domain) => url.includes(domain)) } export default function NetworkDiagnoseModal({ isOpen, onClose }: NetworkDiagnoseModalProps) { const [testing, setTesting] = useState(false) const [results, setResults] = useState([]) const [copied, setCopied] = useState(false) const [networkInfo, setNetworkInfo] = useState(null) const { toast, showToast } = useToast() // 跟踪需要清理的资源 const createdBlobs = useRef([]) const createdUrls = useRef([]) const createdUploadIds = useRef([]) // ✅ 组件卸载时自动清理资源 useEffect(() => { return () => { cleanupDiagnosticResources() } }, []) // 格式化文件大小 const formatFileSize = (bytes: number) => { if (bytes < 1024) return `${bytes} B` if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(2)} KB` return `${(bytes / 1024 / 1024).toFixed(2)} MB` } // 清理诊断过程中创建的资源 const cleanupDiagnosticResources = async () => { try { // 1. 清理Blob对象(释放内存) createdBlobs.current.forEach(blob => { if (blob && typeof blob === 'object' && 'close' in blob) { // 如果是可关闭的Blob(如File),尝试关闭 try { (blob as any).close?.() } catch (e) { // 忽略错误 } } }) createdBlobs.current = [] // 2. 清理URL对象(revokeObjectURL) createdUrls.current.forEach(url => { try { URL.revokeObjectURL(url) } catch (e) { // 忽略错误 } }) createdUrls.current = [] // 3. 清理服务器端的uploadId(删除临时文件) if (createdUploadIds.current.length > 0) { // 批量清理服务器端的临时文件 const cleanupPromises = createdUploadIds.current.map(uploadId => fetch('/api/materials/upload/cleanup', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ uploadId }) }).catch(err => { console.warn(`清理uploadId ${uploadId}失败:`, err) return null }) ) await Promise.all(cleanupPromises) console.log(`已清理 ${createdUploadIds.current.length} 个上传会话的临时文件`) createdUploadIds.current = [] } } catch (error) { console.warn('清理诊断资源时出错:', error) } } // 1. 网络环境检测 const testNetworkEnvironment = async (): Promise => { try { const response = await fetch('/api/network-status', { cache: 'no-store' }) if (!response.ok) throw new Error('网络环境检测失败') const data = await response.json() setNetworkInfo(data) // 获取浏览器网络信息 const connection = (navigator as any).connection || (navigator as any).mozConnection || (navigator as any).webkitConnection const networkType = connection?.effectiveType || 'unknown' const downlink = connection?.downlink || 'unknown' const rtt = connection?.rtt || 'unknown' const saveData = connection?.saveData || false const browserDownlinkSpeed = typeof downlink === 'number' ? formatHumanSpeed((downlink * 1000 * 1000) / 8) : '未知' const message = [ `网络类型: ${data.isIntranet ? '内网' : '外网'}`, `访问域名: ${data.hostname}`, `客户端IP: ${data.clientIp}`, `浏览器网络: ${networkType}`, `浏览器估算下行: ${browserDownlinkSpeed}`, `RTT延迟: ${rtt !== 'unknown' ? `${rtt} ms` : '未知'}`, saveData ? '省流模式: 开启' : '' ].filter(Boolean).join(', ') return { type: 'network-env', status: 'success', message, details: { isIntranet: data.isIntranet, isPublicDomain: data.isPublicDomain, clientIp: data.clientIp, hostname: data.hostname, networkType, downlink, rtt, saveData } } } catch (error) { return { type: 'network-env', status: 'error', message: `网络环境检测失败: ${error instanceof Error ? error.message : '未知错误'}` } } } // 2. DNS和连接测试(增强版) const testDNSAndConnection = async (): Promise => { try { const testUrl = `${window.location.origin}/api/network-status` const startTime = performance.now() const response = await fetch(testUrl, { cache: 'no-store' }) const endTime = performance.now() if (!response.ok) throw new Error('连接测试失败') // 使用Resource Timing API获取详细的网络时序 const entries = performance.getEntriesByType('resource') as PerformanceResourceTiming[] const entry = entries.find(e => e.name.includes('/api/network-status')) let dnsTime = 0 let connectTime = 0 let ttfb = 0 let sslTime = 0 let requestTime = 0 let responseTime = 0 if (entry) { dnsTime = entry.domainLookupEnd - entry.domainLookupStart connectTime = entry.connectEnd - entry.connectStart sslTime = entry.secureConnectionStart > 0 ? entry.connectEnd - entry.secureConnectionStart : 0 ttfb = entry.responseStart - entry.requestStart requestTime = entry.responseStart - entry.requestStart responseTime = entry.responseEnd - entry.responseStart } const totalTime = endTime - startTime const details: any = { dnsTime: dnsTime > 0 ? dnsTime : undefined, connectTime: connectTime > 0 ? connectTime : undefined, sslTime: sslTime > 0 ? sslTime : undefined, ttfb: ttfb > 0 ? ttfb : undefined, requestTime: requestTime > 0 ? requestTime : undefined, responseTime: responseTime > 0 ? responseTime : undefined, totalTime } const issues: string[] = [] if (dnsTime > 100) issues.push(`DNS解析较慢: ${dnsTime.toFixed(0)}ms`) if (connectTime > 200) issues.push(`TCP连接较慢: ${connectTime.toFixed(0)}ms`) if (ttfb > 500) issues.push(`首字节时间较长: ${ttfb.toFixed(0)}ms`) const message = issues.length > 0 ? `总耗时: ${totalTime.toFixed(0)}ms (${issues.join(', ')})` : `总耗时: ${totalTime.toFixed(0)}ms` return { type: 'connection', status: totalTime > 1000 ? 'warning' : 'success', message, details } } catch (error) { return { type: 'connection', status: 'error', message: `连接测试失败: ${error instanceof Error ? error.message : '未知错误'}` } } } // 3. 下载测速(增强版:测试不同大小) const testDownloadSpeed = async (): Promise => { try { const testFileSize = 1024 * 1024 // 1MB const startTime = Date.now() const response = await fetch('/api/network/test-download', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ size: testFileSize }) }) if (!response.ok) throw new Error('下载测试失败') const blob = await response.blob() createdBlobs.current.push(blob) // 跟踪Blob以便清理 const endTime = Date.now() const duration = (endTime - startTime) / 1000 const speed = blob.size / duration // 获取Resource Timing信息 const entries = performance.getEntriesByType('resource') as PerformanceResourceTiming[] const entry = entries.find(e => e.name.includes('/api/network/test-download')) let ttfb = 0 if (entry) { ttfb = entry.responseStart - entry.requestStart } return { type: 'download', status: speed < 1024 * 1024 ? 'warning' : 'success', // < 1MB/s 警告 speed, message: `下载速度: ${formatHumanSpeed(speed)}${ttfb > 0 ? `, TTFB: ${ttfb.toFixed(0)}ms` : ''}`, details: { ttfb: ttfb > 0 ? ttfb : undefined, totalTime: duration * 1000, fileSize: blob.size } } } catch (error) { return { type: 'download', status: 'error', message: '下载测试失败' } } } // 4. 上传测速 const testUploadSpeed = async (): Promise => { try { const testData = new Blob([new ArrayBuffer(1024 * 1024)]) // 1MB createdBlobs.current.push(testData) // 跟踪Blob以便清理 const formData = new FormData() formData.append('file', testData, 'test.bin') const startTime = Date.now() const response = await fetch('/api/network/test-upload', { method: 'POST', body: formData }) if (!response.ok) throw new Error('上传测试失败') const endTime = Date.now() const duration = (endTime - startTime) / 1000 const speed = testData.size / duration return { type: 'upload', status: speed < 512 * 1024 ? 'warning' : 'success', // < 512KB/s 警告 speed, message: `上传速度: ${formatHumanSpeed(speed)}`, details: { totalTime: duration * 1000, fileSize: testData.size } } } catch (error) { return { type: 'upload', status: 'error', message: '上传测试失败' } } } // 5. 存储连通性测试 const testStorageConnectivity = async (): Promise => { try { const response = await fetch('/api/network/test-storage') if (!response.ok) throw new Error('存储连通性测试失败') const data = await response.json() return { type: 'storage', status: 'success', message: data.message || '存储服务连接正常' } } catch (error) { return { type: 'storage', status: 'error', message: '存储服务连接失败' } } } // 6. 预览图片加载测试(增强版) const testPreviewImageLoad = async (): Promise => { try { const response = await fetch('/api/materials?pageSize=1&materialType=3D场景') if (!response.ok) throw new Error('获取素材失败') const data = await response.json() const materials = data.materials || data if (!materials || materials.length === 0) { return { type: 'preview-image', status: 'warning', message: '没有可测试的预览图片' } } const imageUrl = materials[0].previewUrl if (!imageUrl) { return { type: 'preview-image', status: 'warning', message: '素材没有预览图' } } // 使用Performance API测量加载时间 const startTime = performance.now() const img = new Image() await new Promise((resolve, reject) => { const timeout = setTimeout(() => reject(new Error('图片加载超时')), 10000) img.onload = () => { clearTimeout(timeout) resolve(undefined) } img.onerror = () => { clearTimeout(timeout) reject(new Error('图片加载失败')) } img.src = imageUrl }) const endTime = performance.now() const loadTime = endTime - startTime // 获取文件大小和详细时序 let fileSize = 0 let ttfb = 0 try { const sizeResponse = await fetch(imageUrl, { method: 'HEAD' }) fileSize = parseInt(sizeResponse.headers.get('content-length') || '0') // 获取Resource Timing const entries = performance.getEntriesByType('resource') as PerformanceResourceTiming[] const imageFileName = imageUrl.split('/').pop() || '' const entry = entries.find((e: PerformanceResourceTiming) => e.name.includes(imageFileName)) if (entry) { ttfb = entry.responseStart - entry.requestStart } } catch (e) { // 忽略错误 } const speed = fileSize > 0 ? (fileSize / (loadTime / 1000)) : 0 return { type: 'preview-image', status: loadTime > 3000 ? 'warning' : 'success', speed, message: `加载时间: ${(loadTime / 1000).toFixed(2)}秒 ${fileSize > 0 ? `(${formatHumanSpeed(speed)})` : ''}`, details: { totalTime: loadTime, ttfb: ttfb > 0 ? ttfb : undefined, fileSize, url: imageUrl } } } catch (error) { return { type: 'preview-image', status: 'error', message: `预览图片加载失败: ${error instanceof Error ? error.message : '未知错误'}` } } } // 7. 预览视频加载测试(增强版) const testPreviewVideoLoad = async (): Promise => { try { const response = await fetch('/api/materials?pageSize=1&materialType=淘系-主图视频') if (!response.ok) throw new Error('获取视频素材失败') const data = await response.json() const materials = data.materials || data if (!materials || materials.length === 0) { return { type: 'preview-video', status: 'warning', message: '没有可测试的视频素材' } } const videoUrl = materials[0].videoPreviewClipUrl || materials[0].videoPreview540Url || materials[0].videoSourceUrl if (!videoUrl) { return { type: 'preview-video', status: 'warning', message: '视频素材没有预览URL' } } const startTime = performance.now() // 测试视频元数据加载(首字节时间) const video = document.createElement('video') video.preload = 'metadata' await new Promise((resolve, reject) => { const timeout = setTimeout(() => reject(new Error('视频加载超时')), 15000) video.onloadedmetadata = () => { clearTimeout(timeout) resolve(undefined) } video.onerror = () => { clearTimeout(timeout) reject(new Error('视频加载失败')) } video.src = videoUrl }) const metadataTime = performance.now() - startTime // 获取文件大小 let fileSize = 0 try { const sizeResponse = await fetch(videoUrl, { method: 'HEAD' }) fileSize = parseInt(sizeResponse.headers.get('content-length') || '0') } catch (e) { // 忽略错误 } return { type: 'preview-video', status: metadataTime > 5000 ? 'warning' : 'success', message: `元数据加载: ${(metadataTime / 1000).toFixed(2)}秒 ${fileSize > 0 ? `(文件: ${formatFileSize(fileSize)})` : ''}`, details: { ttfb: metadataTime, totalTime: metadataTime, fileSize, url: videoUrl } } } catch (error) { return { type: 'preview-video', status: 'error', message: `视频加载失败: ${error instanceof Error ? error.message : '未知错误'}` } } } // 8. 音频文件加载测试 const testPreviewAudioLoad = async (): Promise => { try { const response = await fetch('/api/materials?pageSize=1&materialType=音乐') if (!response.ok) throw new Error('获取音频素材失败') const data = await response.json() const materials = data.materials || data if (!materials || materials.length === 0) { return { type: 'preview-audio', status: 'warning', message: '没有可测试的音频素材' } } const material = materials[0] let audioUrl = material.zipFileName || '' if (!audioUrl && material.zipFilePath) { const zipFilePath = material.zipFilePath if (zipFilePath.startsWith('http://') || zipFilePath.startsWith('https://')) { audioUrl = zipFilePath } else if (zipFilePath.startsWith('/uploads/') || zipFilePath.startsWith('uploads/')) { audioUrl = zipFilePath.startsWith('/') ? zipFilePath : `/${zipFilePath}` } else { audioUrl = `/uploads/${zipFilePath}` } } if (!audioUrl && material.zipFileName && !material.zipFileName.startsWith('http')) { audioUrl = `/uploads/${material.zipFileName}` } if (!audioUrl) { return { type: 'preview-audio', status: 'warning', message: '音频素材没有可用的URL' } } const startTime = performance.now() const audio = new Audio() audio.preload = 'metadata' await new Promise((resolve, reject) => { const timeout = setTimeout(() => reject(new Error('音频加载超时')), 10000) audio.onloadedmetadata = () => { clearTimeout(timeout) resolve(undefined) } audio.onerror = () => { clearTimeout(timeout) reject(new Error('音频加载失败')) } audio.src = audioUrl }) const metadataTime = performance.now() - startTime // 获取文件大小 let fileSize = 0 try { const sizeResponse = await fetch(audioUrl, { method: 'HEAD' }) fileSize = parseInt(sizeResponse.headers.get('content-length') || '0') } catch (e) { // 忽略错误 } return { type: 'preview-audio', status: metadataTime > 3000 ? 'warning' : 'success', message: `元数据加载: ${(metadataTime / 1000).toFixed(2)}秒 ${fileSize > 0 ? `(文件: ${formatFileSize(fileSize)})` : ''}`, details: { ttfb: metadataTime, totalTime: metadataTime, fileSize, url: audioUrl } } } catch (error) { return { type: 'preview-audio', status: 'error', message: `音频加载失败: ${error instanceof Error ? error.message : '未知错误'}` } } } // 9. 代理性能测试 const testProxyPerformance = async (): Promise => { try { // 获取真实的预览图片URL(通常会走代理),增加pageSize提高找到有效素材的概率 const response = await fetch('/api/materials?pageSize=10') if (!response.ok) throw new Error('获取测试素材失败') const data = await response.json() const materials = data.materials || data if (!materials || materials.length === 0) { return { type: 'proxy-performance', status: 'warning', message: '没有可测试的素材' } } // 查找第一个有previewUrl的素材 const materialWithPreview = materials.find((m: any) => m.previewUrl) if (!materialWithPreview || !materialWithPreview.previewUrl) { return { type: 'proxy-performance', status: 'warning', message: '没有找到有预览URL的素材' } } const testUrl = materialWithPreview.previewUrl // 检查URL是否为内网MinIO地址(内网用户不应该通过代理访问内网地址) const isIntranetMinIO = testUrl.includes('192.168.') || testUrl.includes('localhost') || testUrl.includes('127.0.0.1') if (isIntranetMinIO) { return { type: 'proxy-performance', status: 'success', message: '内网用户直接访问内网MinIO,无需代理', details: { url: testUrl } } } // 如果URL已经是代理URL,直接测试;否则构造代理URL(仅对公网URL) let proxyUrl = testUrl if (!testUrl.includes('/api/proxy-media')) { // 确保URL是公网S3地址,才构造代理URL const isPublicS3 = isPublicS3Url(testUrl) if (isPublicS3) { proxyUrl = `/api/proxy-media?url=${encodeURIComponent(testUrl)}` } else { // 如果不是公网S3地址,直接测试原始URL proxyUrl = testUrl } } const startTime = performance.now() const testResponse = await fetch(proxyUrl, { cache: 'no-store' }) const endTime = performance.now() if (!testResponse.ok) throw new Error('代理测试失败') const blob = await testResponse.blob() createdBlobs.current.push(blob) // 跟踪Blob以便清理 const totalTime = endTime - startTime // 获取Resource Timing const entries = performance.getEntriesByType('resource') as PerformanceResourceTiming[] const entry = entries.find(e => e.name.includes('/api/proxy-media')) let dnsTime = 0 let connectTime = 0 let ttfb = 0 if (entry) { dnsTime = entry.domainLookupEnd - entry.domainLookupStart connectTime = entry.connectEnd - entry.connectStart ttfb = entry.responseStart - entry.requestStart } const speed = blob.size / (totalTime / 1000) return { type: 'proxy-performance', status: ttfb > 1000 ? 'warning' : 'success', speed, message: `代理响应: TTFB ${ttfb > 0 ? ttfb.toFixed(0) : totalTime.toFixed(0)}ms, 速度 ${formatHumanSpeed(speed)}`, details: { dnsTime: dnsTime > 0 ? dnsTime : undefined, connectTime: connectTime > 0 ? connectTime : undefined, ttfb: ttfb > 0 ? ttfb : totalTime, totalTime, fileSize: blob.size, url: proxyUrl } } } catch (error) { return { type: 'proxy-performance', status: 'error', message: `代理性能测试失败: ${error instanceof Error ? error.message : '未知错误'}` } } } // 10. 视频Range请求测试 const testVideoRangeRequest = async (): Promise => { try { const response = await fetch('/api/materials?pageSize=1&materialType=淘系-主图视频') if (!response.ok) throw new Error('获取视频素材失败') const data = await response.json() const materials = data.materials || data if (!materials || materials.length === 0) { return { type: 'video-range', status: 'warning', message: '没有可测试的视频素材' } } const videoUrl = materials[0].videoPreviewClipUrl || materials[0].videoPreview540Url || materials[0].videoSourceUrl if (!videoUrl) { return { type: 'video-range', status: 'warning', message: '视频素材没有预览URL' } } // 测试Range请求(模拟视频拖拽) const rangeSize = 1024 * 1024 // 1MB const startTime = performance.now() const rangeResponse = await fetch(videoUrl, { headers: { 'Range': `bytes=0-${rangeSize - 1}` } }) const endTime = performance.now() const rangeTime = endTime - startTime if (!rangeResponse.ok && rangeResponse.status !== 206) { throw new Error('Range请求不支持') } const blob = await rangeResponse.blob() createdBlobs.current.push(blob) // 跟踪Blob以便清理 const speed = blob.size / (rangeTime / 1000) return { type: 'video-range', status: rangeTime > 2000 ? 'warning' : 'success', speed, message: `Range请求: ${rangeTime.toFixed(0)}ms, 速度 ${formatHumanSpeed(speed)}`, details: { totalTime: rangeTime, fileSize: blob.size, rangeSize, url: videoUrl, supportsRange: rangeResponse.status === 206 } } } catch (error) { return { type: 'video-range', status: 'error', message: `Range请求测试失败: ${error instanceof Error ? error.message : '未知错误'}` } } } // 11. 内网MinIO直连测试 const testIntranetMinIO = async (): Promise => { try { // 获取一个素材,检查URL是否会被重写为内网MinIO地址 const response = await fetch('/api/materials?pageSize=1') if (!response.ok) throw new Error('获取测试素材失败') const data = await response.json() const materials = data.materials || data if (!materials || materials.length === 0) { return { type: 'intranet-minio', status: 'warning', message: '没有可测试的素材' } } const testUrl = materials[0].previewUrl if (!testUrl) { return { type: 'intranet-minio', status: 'warning', message: '素材没有预览URL' } } // 检查URL是否为内网MinIO地址 const isIntranetMinIO = testUrl.includes('192.168.') || testUrl.includes('localhost') || testUrl.includes('127.0.0.1') if (!isIntranetMinIO) { return { type: 'intranet-minio', status: 'warning', message: '当前URL不是内网MinIO地址(可能被重写为公网地址)', details: { url: testUrl } } } const startTime = performance.now() const testResponse = await fetch(testUrl, { cache: 'no-store' }) const endTime = performance.now() if (!testResponse.ok) throw new Error('内网MinIO连接失败') const blob = await testResponse.blob() createdBlobs.current.push(blob) // 跟踪Blob以便清理 const totalTime = endTime - startTime const speed = blob.size / (totalTime / 1000) // 获取Resource Timing const entries = performance.getEntriesByType('resource') as PerformanceResourceTiming[] const entry = entries.find(e => e.name.includes(testUrl.split('/').pop() || '')) let ttfb = 0 if (entry) { ttfb = entry.responseStart - entry.requestStart } return { type: 'intranet-minio', status: ttfb > 500 ? 'warning' : 'success', speed, message: `内网MinIO: TTFB ${ttfb > 0 ? ttfb.toFixed(0) : totalTime.toFixed(0)}ms, 速度 ${formatHumanSpeed(speed)}`, details: { ttfb: ttfb > 0 ? ttfb : totalTime, totalTime, fileSize: blob.size, url: testUrl } } } catch (error) { return { type: 'intranet-minio', status: 'error', message: `内网MinIO测试失败: ${error instanceof Error ? error.message : '未知错误'}` } } } // 12. 公网S3直连测试 const testPublicS3 = async (): Promise => { try { // 增加pageSize提高找到有效素材的概率 const response = await fetch('/api/materials?pageSize=10') if (!response.ok) throw new Error('获取测试素材失败') const data = await response.json() const materials = data.materials || data if (!materials || materials.length === 0) { return { type: 'public-s3', status: 'warning', message: '没有可测试的素材' } } // 查找第一个有previewUrl的素材 const materialWithPreview = materials.find((m: any) => m.previewUrl) if (!materialWithPreview || !materialWithPreview.previewUrl) { return { type: 'public-s3', status: 'warning', message: '没有找到有预览URL的素材' } } const testUrl = materialWithPreview.previewUrl // 检查URL是否为公网S3地址 const isPublicS3 = isPublicS3Url(testUrl) const isIntranetMinIO = testUrl.includes('192.168.') || testUrl.includes('localhost') || testUrl.includes('127.0.0.1') // 如果是内网MinIO地址,说明是内网用户,这是正常的(内网用户应该使用内网地址) if (isIntranetMinIO) { return { type: 'public-s3', status: 'success', message: '内网用户使用内网MinIO地址(正常,无需测试公网S3)', details: { url: testUrl } } } if (!isPublicS3 && !testUrl.includes('/api/proxy-media')) { return { type: 'public-s3', status: 'warning', message: '当前URL不是公网S3地址', details: { url: testUrl } } } // 如果是代理URL,提取原始URL let directUrl = testUrl if (testUrl.includes('/api/proxy-media')) { const urlParams = new URLSearchParams(testUrl.split('?')[1]) directUrl = urlParams.get('url') || testUrl } const startTime = performance.now() const testResponse = await fetch(directUrl, { cache: 'no-store', signal: AbortSignal.timeout(10000) }) const endTime = performance.now() if (!testResponse.ok) throw new Error('公网S3连接失败') const blob = await testResponse.blob() createdBlobs.current.push(blob) // 跟踪Blob以便清理 const totalTime = endTime - startTime const speed = blob.size / (totalTime / 1000) // 获取Resource Timing const entries = performance.getEntriesByType('resource') as PerformanceResourceTiming[] const entry = entries.find(e => e.name.includes(directUrl.split('/').pop() || '')) let dnsTime = 0 let connectTime = 0 let ttfb = 0 if (entry) { dnsTime = entry.domainLookupEnd - entry.domainLookupStart connectTime = entry.connectEnd - entry.connectStart ttfb = entry.responseStart - entry.requestStart } return { type: 'public-s3', status: ttfb > 2000 ? 'warning' : 'success', speed, message: `公网S3: TTFB ${ttfb > 0 ? ttfb.toFixed(0) : totalTime.toFixed(0)}ms, 速度 ${formatHumanSpeed(speed)}`, details: { dnsTime: dnsTime > 0 ? dnsTime : undefined, connectTime: connectTime > 0 ? connectTime : undefined, ttfb: ttfb > 0 ? ttfb : totalTime, totalTime, fileSize: blob.size, url: directUrl } } } catch (error) { return { type: 'public-s3', status: 'error', message: `公网S3测试失败: ${error instanceof Error ? error.message : '未知错误'}` } } } // 13. 并发测试 const testConcurrent = async (): Promise => { try { const response = await fetch('/api/materials?pageSize=5') if (!response.ok) throw new Error('获取测试素材失败') const data = await response.json() const materials = data.materials || data if (!materials || materials.length === 0) { return { type: 'concurrent', status: 'warning', message: '没有可测试的素材' } } // 获取5个图片URL const imageUrls: string[] = materials .map((m: any) => m.previewUrl) .filter((url: string | null | undefined): url is string => !!url) .slice(0, 5) if (imageUrls.length === 0) { return { type: 'concurrent', status: 'warning', message: '没有可测试的图片URL' } } const startTime = performance.now() // 并发加载5个图片 const promises = imageUrls.map((url: string) => fetch(url, { cache: 'no-store' }).then(r => r.blob()) ) const results = await Promise.all(promises) // 跟踪所有Blob以便清理 results.forEach(blob => createdBlobs.current.push(blob)) const endTime = performance.now() const totalTime = endTime - startTime const totalSize = results.reduce((sum, blob) => sum + blob.size, 0) const avgSpeed = totalSize / (totalTime / 1000) const avgTimePerFile = totalTime / imageUrls.length return { type: 'concurrent', status: avgTimePerFile > 2000 ? 'warning' : 'success', speed: avgSpeed, message: `并发${imageUrls.length}个文件: 平均${avgTimePerFile.toFixed(0)}ms/文件, 总速度 ${formatHumanSpeed(avgSpeed)}`, details: { totalTime, totalSize, fileCount: imageUrls.length, avgTimePerFile, urls: imageUrls } } } catch (error) { return { type: 'concurrent', status: 'error', message: `并发测试失败: ${error instanceof Error ? error.message : '未知错误'}` } } } // 14. 缓存测试 const testCache = async (): Promise => { try { // 增加pageSize提高找到有效素材的概率 const response = await fetch('/api/materials?pageSize=10') if (!response.ok) throw new Error('获取测试素材失败') const data = await response.json() const materials = data.materials || data if (!materials || materials.length === 0) { return { type: 'cache', status: 'warning', message: '没有可测试的素材' } } // 查找第一个有previewUrl的素材 const materialWithPreview = materials.find((m: any) => m.previewUrl) if (!materialWithPreview || !materialWithPreview.previewUrl) { return { type: 'cache', status: 'warning', message: '没有找到有预览URL的素材' } } const testUrl = materialWithPreview.previewUrl // 第一次加载(无缓存,跳过浏览器缓存) const firstStart = performance.now() const firstResponse = await fetch(testUrl, { cache: 'no-store' }) const firstBlob = await firstResponse.blob() createdBlobs.current.push(firstBlob) // 跟踪Blob以便清理 const firstTime = performance.now() - firstStart // 检查响应头的缓存控制 const cacheControl = firstResponse.headers.get('cache-control') || '' const etag = firstResponse.headers.get('etag') || '' const canCache = !cacheControl.includes('no-store') && !cacheControl.includes('no-cache') const fileSize = firstBlob.size // 等待更长时间,确保浏览器有足够时间缓存(特别是小文件) await new Promise(resolve => setTimeout(resolve, 300)) // 第二次加载(尝试使用缓存) const secondStart = performance.now() const secondResponse = await fetch(testUrl, { cache: 'default' // 使用默认缓存策略 }) const secondBlob = await secondResponse.blob() createdBlobs.current.push(secondBlob) // 跟踪Blob以便清理 const secondTime = performance.now() - secondStart const cacheSpeedup = firstTime > 0 ? ((firstTime - secondTime) / firstTime) * 100 : 0 // 改进的判断逻辑: // 1. 对于小文件(<100KB),缓存效果可能不明显(网络延迟占比大),降低判断标准 // 2. 对于大文件,缓存应该明显更快(至少快30%) // 3. 如果服务器不允许缓存(no-cache/no-store),则认为缓存无效是正常的 const isSmallFile = fileSize < 100 * 1024 // 100KB const threshold = isSmallFile ? 0.7 : 0.5 // 小文件降低标准到70% const isCached = canCache && secondTime < firstTime * threshold // 生成更详细的提示信息 let message = `缓存效果: ${isCached ? '有效' : '无效'}` if (!canCache) { message += ' (服务器不允许缓存)' } else if (isSmallFile) { message += ` (小文件,缓存效果可能不明显)` } message += `, 加速 ${cacheSpeedup.toFixed(1)}% (首次${firstTime.toFixed(0)}ms, 二次${secondTime.toFixed(0)}ms)` return { type: 'cache', status: isCached ? 'success' : (canCache ? 'warning' : 'success'), // 如果服务器不允许缓存,标记为success(这是正常的) message, details: { firstTime, secondTime, cacheSpeedup, isCached, url: testUrl, cacheControl, canCache, fileSize } } } catch (error) { return { type: 'cache', status: 'error', message: `缓存测试失败: ${error instanceof Error ? error.message : '未知错误'}` } } } // 15. S3存储性能分析(增强版) const testS3Performance = async (): Promise => { try { // 增加pageSize提高找到有效素材的概率 const response = await fetch('/api/materials?pageSize=10') if (!response.ok) throw new Error('获取测试素材失败') const data = await response.json() const materials = data.materials || data if (!materials || materials.length === 0) { return { type: 's3-performance', status: 'warning', message: '没有可测试的素材' } } // 查找第一个有previewUrl的素材 const materialWithPreview = materials.find((m: any) => m.previewUrl) if (!materialWithPreview || !materialWithPreview.previewUrl) { return { type: 's3-performance', status: 'warning', message: '没有找到有预览URL的素材' } } const testUrl = materialWithPreview.previewUrl let fullTestUrl = testUrl if (!testUrl.startsWith('http://') && !testUrl.startsWith('https://')) { fullTestUrl = new URL(testUrl, window.location.origin).toString() } let urlObj: URL let s3Domain: string try { urlObj = new URL(fullTestUrl) s3Domain = urlObj.hostname } catch (e) { return { type: 's3-performance', status: 'error', message: `S3性能测试失败: 无效的URL格式 - ${testUrl}` } } // 测试1: 检测是否使用frp(仅对公网域名,跳过内网IP以避免CORS错误) let isFrp = false const isPublicDomain = isPublicS3Url(s3Domain) const isInternalIP = /^192\.168\.|^10\.|^172\.(1[6-9]|2[0-9]|3[01])\./.test(s3Domain) // 仅对公网域名进行FRP检测,跳过内网IP和localhost(避免CORS错误) if (isPublicDomain && !isInternalIP && !['localhost', '127.0.0.1'].includes(s3Domain)) { try { const frpTestUrl = `${urlObj.protocol}//${s3Domain}/` const frpResponse = await fetch(frpTestUrl, { method: 'GET', signal: AbortSignal.timeout(5000) }) const text = await frpResponse.text() if (text.includes('frp') || text.includes('内网穿透')) { isFrp = true } } catch (e) { // 忽略错误(CORS等) } } // 测试2: 下载速度和TTFB测试 const startTime = performance.now() let ttfb = 0 const testResponse = await fetch(fullTestUrl, { signal: AbortSignal.timeout(30000) }) ttfb = performance.now() - startTime const blob = await testResponse.blob() createdBlobs.current.push(blob) // 跟踪Blob以便清理 const totalTime = performance.now() - startTime const downloadTime = (totalTime - ttfb) / 1000 const downloadSpeed = downloadTime > 0 ? blob.size / downloadTime : 0 // 获取Resource Timing const entries = performance.getEntriesByType('resource') as PerformanceResourceTiming[] const entry = entries.find(e => e.name.includes(s3Domain)) let dnsTime = 0 let connectTime = 0 if (entry) { dnsTime = entry.domainLookupEnd - entry.domainLookupStart connectTime = entry.connectEnd - entry.connectStart if (ttfb === 0) { ttfb = entry.responseStart - entry.requestStart } } // 分析结果 const issues: string[] = [] const suggestions: string[] = [] if (isFrp) { issues.push('⚠ 当前资源仍需经入口层回源,峰值时吞吐会波动') suggestions.push('• 优先确认展示图是否统一落到 s3.jiansun.vip 的 public-display 前缀') suggestions.push('• 大源文件、压缩包、视频源文件不要纳入展示缓存') suggestions.push('• 如问题集中在公网入口,优先排查 180.97.215.57 缓存层与 192.168.58.20:9000 回源链路') } if (ttfb > 1000) { issues.push(`🔴 首字节时间过长: ${Math.round(ttfb)}ms(正常<200ms)`) if (!isFrp) { suggestions.push('• TTFB过高可能是服务器响应慢或网络延迟大') } } if (downloadSpeed < 1024 * 1024 && downloadSpeed > 0) { issues.push(`🔴 下载速度偏慢: ${formatHumanSpeed(downloadSpeed)}(低于 ${formatHumanSpeed(1024 * 1024)})`) if (!isFrp) { suggestions.push('• 带宽不足,建议升级服务器带宽或使用CDN') } } if (dnsTime > 100) { issues.push(`⚠ DNS解析较慢: ${dnsTime.toFixed(0)}ms`) } if (connectTime > 200) { issues.push(`⚠ TCP连接较慢: ${connectTime.toFixed(0)}ms`) } const isSuccess = !isFrp && ttfb < 1000 && downloadSpeed > 1024 * 1024 const isWarning = !isSuccess && (ttfb < 3000 || downloadSpeed > 100 * 1024) let message = '' if (isSuccess) { message = `性能良好\n下载速度: ${formatHumanSpeed(downloadSpeed)}, TTFB: ${Math.round(ttfb)}ms` } else if (isWarning) { message = `性能一般\n${issues.slice(0, 2).join('\n')}` } else { message = `性能较差\n${issues.slice(0, 2).join('\n')}` } return { type: 's3-performance', status: isSuccess ? 'success' : isWarning ? 'warning' : 'error', message, details: { dnsTime: dnsTime > 0 ? dnsTime : undefined, connectTime: connectTime > 0 ? connectTime : undefined, ttfb, totalTime, fileSize: blob.size, downloadSpeed, url: fullTestUrl, isFrp, issues, suggestions } } } catch (error) { return { type: 's3-performance', status: 'error', message: `S3性能测试失败: ${error instanceof Error ? error.message : '未知错误'}` } } } // 16. API: 素材列表测试 const testApiMaterialsList = async (): Promise => { try { const startTime = performance.now() const response = await fetch('/api/materials?pageSize=10', { cache: 'no-store' }) const endTime = performance.now() if (!response.ok) throw new Error('素材列表API失败') const data = await response.json() const totalTime = endTime - startTime const materialCount = data.materials?.length || data.length || 0 return { type: 'api-materials-list', status: totalTime > 1000 ? 'warning' : 'success', message: `响应时间: ${totalTime.toFixed(0)}ms, 返回${materialCount}条数据`, details: { totalTime, materialCount, url: '/api/materials' } } } catch (error) { return { type: 'api-materials-list', status: 'error', message: `素材列表API测试失败: ${error instanceof Error ? error.message : '未知错误'}` } } } // 17. API: 素材详情测试 const testApiMaterialDetail = async (): Promise => { try { // 先获取一个素材ID const listResponse = await fetch('/api/materials?pageSize=1', { cache: 'no-store' }) if (!listResponse.ok) throw new Error('获取素材列表失败') const listData = await listResponse.json() const materials = listData.materials || listData if (!materials || materials.length === 0) { return { type: 'api-material-detail', status: 'warning', message: '没有可测试的素材' } } const materialId = materials[0].id const startTime = performance.now() const response = await fetch(`/api/materials/${materialId}`, { cache: 'no-store' }) const endTime = performance.now() if (!response.ok) throw new Error('素材详情API失败') const data = await response.json() const totalTime = endTime - startTime return { type: 'api-material-detail', status: totalTime > 500 ? 'warning' : 'success', message: `响应时间: ${totalTime.toFixed(0)}ms`, details: { totalTime, materialId, url: `/api/materials/${materialId}` } } } catch (error) { return { type: 'api-material-detail', status: 'error', message: `素材详情API测试失败: ${error instanceof Error ? error.message : '未知错误'}` } } } // 18. API: 素材统计测试 const testApiMaterialStats = async (): Promise => { try { const startTime = performance.now() const response = await fetch('/api/materials/stats', { cache: 'no-store' }) const endTime = performance.now() if (!response.ok) throw new Error('素材统计API失败') const data = await response.json() const totalTime = endTime - startTime return { type: 'api-material-stats', status: totalTime > 1000 ? 'warning' : 'success', message: `响应时间: ${totalTime.toFixed(0)}ms`, details: { totalTime, stats: data, url: '/api/materials/stats' } } } catch (error) { return { type: 'api-material-stats', status: 'error', message: `素材统计API测试失败: ${error instanceof Error ? error.message : '未知错误'}` } } } // 19. API: 上传初始化测试 const testApiUploadInit = async (): Promise => { try { // 上传初始化API需要JSON格式,不是FormData const testData = { fileName: 'test.txt', fileSize: 4, totalChunks: 1 } const startTime = performance.now() const response = await fetch('/api/materials/upload/init', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(testData), cache: 'no-store' }) const endTime = performance.now() const totalTime = endTime - startTime // 如果失败,检查错误原因 if (!response.ok) { const errorData = await response.json().catch(() => ({})) const isUnauthorized = response.status === 401 || response.status === 403 const isBadRequest = response.status === 400 // 根据错误类型返回不同的状态 let status: 'success' | 'warning' | 'error' = 'warning' let message = `响应时间: ${totalTime.toFixed(0)}ms, 状态: ${response.status}` if (isUnauthorized) { status = 'success' message += ' (需要登录,这是正常的)' } else if (isBadRequest) { status = 'warning' message += ` (${errorData.error || '参数错误'})` } else { status = 'error' message += ` (${errorData.error || '服务器错误'})` } return { type: 'api-upload-init', status, message, details: { totalTime, statusCode: response.status, error: errorData.error, url: '/api/materials/upload/init' } } } const data = await response.json() // 跟踪uploadId以便后续清理 if (data.uploadId) { createdUploadIds.current.push(data.uploadId) } return { type: 'api-upload-init', status: totalTime > 1000 ? 'warning' : 'success', message: `响应时间: ${totalTime.toFixed(0)}ms`, details: { totalTime, uploadId: data.uploadId, url: '/api/materials/upload/init' } } } catch (error) { return { type: 'api-upload-init', status: 'error', message: `上传初始化API测试失败: ${error instanceof Error ? error.message : '未知错误'}` } } } // 20. API: 下载接口测试 const testApiDownload = async (): Promise => { try { // 先获取一个素材ID const listResponse = await fetch('/api/materials?pageSize=1', { cache: 'no-store' }) if (!listResponse.ok) throw new Error('获取素材列表失败') const listData = await listResponse.json() const materials = listData.materials || listData if (!materials || materials.length === 0) { return { type: 'api-download', status: 'warning', message: '没有可测试的素材' } } const materialId = materials[0].id const startTime = performance.now() const response = await fetch(`/api/materials/download?id=${materialId}`, { method: 'GET', cache: 'no-store' }) const endTime = performance.now() const totalTime = endTime - startTime // 即使失败也记录(可能是权限问题) if (!response.ok) { return { type: 'api-download', status: 'warning', message: `响应时间: ${totalTime.toFixed(0)}ms, 状态: ${response.status} (可能未登录或无权下载)`, details: { totalTime, statusCode: response.status, materialId, url: `/api/materials/download?id=${materialId}` } } } // 检查是否是重定向或文件流 const contentType = response.headers.get('content-type') || '' const isFile = contentType.includes('application/zip') || contentType.includes('application/octet-stream') return { type: 'api-download', status: totalTime > 2000 ? 'warning' : 'success', message: `响应时间: ${totalTime.toFixed(0)}ms${isFile ? ', 返回文件流' : ', 返回数据'}`, details: { totalTime, materialId, contentType, url: `/api/materials/download?id=${materialId}` } } } catch (error) { return { type: 'api-download', status: 'error', message: `下载API测试失败: ${error instanceof Error ? error.message : '未知错误'}` } } } // 21. API: 用户信息测试 const testApiUserProfile = async (): Promise => { try { const startTime = performance.now() const response = await fetch('/api/user/profile', { cache: 'no-store' }) const endTime = performance.now() const totalTime = endTime - startTime if (!response.ok) { return { type: 'api-user-profile', status: 'warning', message: `响应时间: ${totalTime.toFixed(0)}ms, 状态: ${response.status} (可能未登录)`, details: { totalTime, statusCode: response.status, url: '/api/user/profile' } } } const data = await response.json() return { type: 'api-user-profile', status: totalTime > 500 ? 'warning' : 'success', message: `响应时间: ${totalTime.toFixed(0)}ms${data.username ? `, 用户: ${data.username}` : ''}`, details: { totalTime, userId: data.id, url: '/api/user/profile' } } } catch (error) { return { type: 'api-user-profile', status: 'error', message: `用户信息API测试失败: ${error instanceof Error ? error.message : '未知错误'}` } } } // 22. API: 系统设置测试 const testApiSystemSettings = async (): Promise => { try { const startTime = performance.now() const response = await fetch('/api/system-settings', { cache: 'no-store' }) const endTime = performance.now() if (!response.ok) throw new Error('系统设置API失败') const data = await response.json() const totalTime = endTime - startTime return { type: 'api-system-settings', status: totalTime > 500 ? 'warning' : 'success', message: `响应时间: ${totalTime.toFixed(0)}ms`, details: { totalTime, url: '/api/system-settings' } } } catch (error) { return { type: 'api-system-settings', status: 'error', message: `系统设置API测试失败: ${error instanceof Error ? error.message : '未知错误'}` } } } // 23. API: 健康检查测试 const testApiHealth = async (): Promise => { try { const startTime = performance.now() const response = await fetch('/api/health', { cache: 'no-store' }) const endTime = performance.now() if (!response.ok) throw new Error('健康检查API失败') const data = await response.json() const totalTime = endTime - startTime return { type: 'api-health', status: totalTime > 200 ? 'warning' : 'success', message: `响应时间: ${totalTime.toFixed(0)}ms, 状态: ${data.status || 'ok'}`, details: { totalTime, health: data, url: '/api/health' } } } catch (error) { return { type: 'api-health', status: 'error', message: `健康检查API测试失败: ${error instanceof Error ? error.message : '未知错误'}` } } } // 24. 数据库查询性能测试 const testDatabaseQuery = async (): Promise => { try { // 通过素材列表API间接测试数据库查询性能 const startTime = performance.now() const response = await fetch('/api/materials?pageSize=1', { cache: 'no-store' }) const endTime = performance.now() if (!response.ok) throw new Error('数据库查询测试失败') const totalTime = endTime - startTime // 获取Resource Timing const entries = performance.getEntriesByType('resource') as PerformanceResourceTiming[] const entry = entries.find(e => e.name.includes('/api/materials')) let ttfb = 0 if (entry) { ttfb = entry.responseStart - entry.requestStart } return { type: 'database-query', status: ttfb > 500 ? 'warning' : 'success', message: `查询时间: ${ttfb > 0 ? ttfb.toFixed(0) : totalTime.toFixed(0)}ms (总耗时: ${totalTime.toFixed(0)}ms)`, details: { totalTime, ttfb: ttfb > 0 ? ttfb : totalTime, url: '/api/materials' } } } catch (error) { return { type: 'database-query', status: 'error', message: `数据库查询测试失败: ${error instanceof Error ? error.message : '未知错误'}` } } } // 25. Redis连接测试(通过系统状态API) const testRedisConnection = async (): Promise => { try { const startTime = performance.now() const response = await fetch('/api/admin/system-status', { cache: 'no-store' }) const endTime = performance.now() const totalTime = endTime - startTime if (!response.ok) { return { type: 'redis-connection', status: 'warning', message: `响应时间: ${totalTime.toFixed(0)}ms, 状态: ${response.status} (可能未登录或无权访问)`, details: { totalTime, statusCode: response.status, url: '/api/admin/system-status' } } } const data = await response.json() const redisStatus = data.redis || data.redisStatus || 'unknown' return { type: 'redis-connection', status: totalTime > 500 ? 'warning' : 'success', message: `响应时间: ${totalTime.toFixed(0)}ms, Redis: ${redisStatus === 'connected' || redisStatus === 'ok' ? '已连接' : '状态未知'}`, details: { totalTime, redisStatus, url: '/api/admin/system-status' } } } catch (error) { return { type: 'redis-connection', status: 'error', message: `Redis连接测试失败: ${error instanceof Error ? error.message : '未知错误'}` } } } // 26. 页面: 首页加载测试 const testPageHomeLoad = async (): Promise => { try { const startTime = performance.now() const response = await fetch('/', { cache: 'no-store' }) const endTime = performance.now() if (!response.ok) throw new Error('首页加载失败') const html = await response.text() const totalTime = endTime - startTime const htmlSize = new Blob([html]).size // 获取Resource Timing const entries = performance.getEntriesByType('navigation') as PerformanceNavigationTiming[] const entry = entries[0] let domContentLoaded = 0 let loadComplete = 0 if (entry) { domContentLoaded = entry.domContentLoadedEventEnd - entry.domContentLoadedEventStart loadComplete = entry.loadEventEnd - entry.loadEventStart } return { type: 'page-home-load', status: totalTime > 2000 ? 'warning' : 'success', message: `加载时间: ${totalTime.toFixed(0)}ms, HTML大小: ${formatFileSize(htmlSize)}`, details: { totalTime, htmlSize, domContentLoaded: domContentLoaded > 0 ? domContentLoaded : undefined, loadComplete: loadComplete > 0 ? loadComplete : undefined, url: '/' } } } catch (error) { return { type: 'page-home-load', status: 'error', message: `首页加载测试失败: ${error instanceof Error ? error.message : '未知错误'}` } } } // 27. 页面: 详情页加载测试 const testPageDetailLoad = async (): Promise => { try { // 先获取一个素材ID const listResponse = await fetch('/api/materials?pageSize=1', { cache: 'no-store' }) if (!listResponse.ok) throw new Error('获取素材列表失败') const listData = await listResponse.json() const materials = listData.materials || listData if (!materials || materials.length === 0) { return { type: 'page-detail-load', status: 'warning', message: '没有可测试的素材' } } const materialId = materials[0].id const startTime = performance.now() const response = await fetch(`/share/${materialId}`, { cache: 'no-store' }) const endTime = performance.now() if (!response.ok) throw new Error('详情页加载失败') const html = await response.text() const totalTime = endTime - startTime const htmlSize = new Blob([html]).size return { type: 'page-detail-load', status: totalTime > 2000 ? 'warning' : 'success', message: `加载时间: ${totalTime.toFixed(0)}ms, HTML大小: ${formatFileSize(htmlSize)}`, details: { totalTime, htmlSize, materialId, url: `/share/${materialId}` } } } catch (error) { return { type: 'page-detail-load', status: 'error', message: `详情页加载测试失败: ${error instanceof Error ? error.message : '未知错误'}` } } } // 28. 上传流程测试(完整流程) const testUploadFlow = async (): Promise => { try { // 上传初始化API需要JSON格式 const testData = { fileName: 'test-upload.txt', fileSize: 13, // 'test content'的长度 totalChunks: 1 } const startTime = performance.now() // 步骤1: 初始化上传 const initResponse = await fetch('/api/materials/upload/init', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(testData), cache: 'no-store' }) const initTime = performance.now() - startTime if (!initResponse.ok) { const errorData = await initResponse.json().catch(() => ({})) const isUnauthorized = initResponse.status === 401 || initResponse.status === 403 const isBadRequest = initResponse.status === 400 let status: 'success' | 'warning' | 'error' = 'warning' let message = `初始化失败: ${initTime.toFixed(0)}ms, 状态: ${initResponse.status}` if (isUnauthorized) { status = 'success' message += ' (需要登录,这是正常的)' } else if (isBadRequest) { status = 'warning' message += ` (${errorData.error || '参数错误'})` } else { status = 'error' message += ` (${errorData.error || '服务器错误'})` } return { type: 'upload-flow', status, message, details: { totalTime: initTime, step: 'init', statusCode: initResponse.status, error: errorData.error, url: '/api/materials/upload/init' } } } const initData = await initResponse.json() const totalTime = performance.now() - startTime // 跟踪uploadId以便后续清理 if (initData.uploadId) { createdUploadIds.current.push(initData.uploadId) } return { type: 'upload-flow', status: totalTime > 2000 ? 'warning' : 'success', message: `初始化完成: ${initTime.toFixed(0)}ms (总耗时: ${totalTime.toFixed(0)}ms)`, details: { totalTime, initTime, uploadId: initData.uploadId, step: 'init', url: '/api/materials/upload/init' } } } catch (error) { return { type: 'upload-flow', status: 'error', message: `上传流程测试失败: ${error instanceof Error ? error.message : '未知错误'}` } } } // 29. 下载流程测试 const testDownloadFlow = async (): Promise => { try { // 先获取一个素材ID const listResponse = await fetch('/api/materials?pageSize=1', { cache: 'no-store' }) if (!listResponse.ok) throw new Error('获取素材列表失败') const listData = await listResponse.json() const materials = listData.materials || listData if (!materials || materials.length === 0) { return { type: 'download-flow', status: 'warning', message: '没有可测试的素材' } } const materialId = materials[0].id const startTime = performance.now() // 测试下载接口响应 const response = await fetch(`/api/materials/download?id=${materialId}`, { method: 'GET', cache: 'no-store' }) const totalTime = performance.now() - startTime if (!response.ok) { return { type: 'download-flow', status: 'warning', message: `下载接口响应: ${totalTime.toFixed(0)}ms, 状态: ${response.status} (可能未登录或无权下载)`, details: { totalTime, statusCode: response.status, materialId, url: `/api/materials/download?id=${materialId}` } } } const contentType = response.headers.get('content-type') || '' const isFile = contentType.includes('application/zip') || contentType.includes('application/octet-stream') return { type: 'download-flow', status: totalTime > 2000 ? 'warning' : 'success', message: `下载接口响应: ${totalTime.toFixed(0)}ms${isFile ? ', 返回文件流' : ', 返回数据'}`, details: { totalTime, contentType, materialId, url: `/api/materials/download?id=${materialId}` } } } catch (error) { return { type: 'download-flow', status: 'error', message: `下载流程测试失败: ${error instanceof Error ? error.message : '未知错误'}` } } } // 30. 转码服务测试 const testTranscodeService = async (): Promise => { try { const startTime = performance.now() const response = await fetch('/api/admin/video-transcode-stats', { cache: 'no-store' }) const endTime = performance.now() const totalTime = endTime - startTime if (!response.ok) { return { type: 'transcode-service', status: 'warning', message: `响应时间: ${totalTime.toFixed(0)}ms, 状态: ${response.status} (可能未登录或无权访问)`, details: { totalTime, statusCode: response.status, url: '/api/admin/video-transcode-stats' } } } const data = await response.json() const queueSize = data.queueSize || data.pending || 0 const processing = data.processing || 0 return { type: 'transcode-service', status: totalTime > 1000 ? 'warning' : 'success', message: `响应时间: ${totalTime.toFixed(0)}ms, 队列: ${queueSize}, 处理中: ${processing}`, details: { totalTime, queueSize, processing, stats: data, url: '/api/admin/video-transcode-stats' } } } catch (error) { return { type: 'transcode-service', status: 'error', message: `转码服务测试失败: ${error instanceof Error ? error.message : '未知错误'}` } } } // 31. 认证Session测试 const testAuthSession = async (): Promise => { try { const startTime = performance.now() const response = await fetch('/api/user/profile', { cache: 'no-store' }) const endTime = performance.now() const totalTime = endTime - startTime if (response.status === 401 || response.status === 403) { return { type: 'auth-session', status: 'warning', message: `响应时间: ${totalTime.toFixed(0)}ms, 状态: ${response.status} (未登录或Session过期)`, details: { totalTime, statusCode: response.status, isAuthenticated: false, url: '/api/user/profile' } } } if (!response.ok) throw new Error('认证测试失败') const data = await response.json() const isAuthenticated = !!data.id return { type: 'auth-session', status: totalTime > 500 ? 'warning' : 'success', message: `响应时间: ${totalTime.toFixed(0)}ms, 认证状态: ${isAuthenticated ? '已登录' : '未登录'}`, details: { totalTime, isAuthenticated, userId: data.id, url: '/api/user/profile' } } } catch (error) { return { type: 'auth-session', status: 'error', message: `认证Session测试失败: ${error instanceof Error ? error.message : '未知错误'}` } } } // 开始诊断(完整版) const startDiagnose = async () => { setTesting(true) setResults([]) setNetworkInfo(null) // 清空资源跟踪数组 createdBlobs.current = [] createdUrls.current = [] createdUploadIds.current = [] try { // 1. 网络环境检测 setResults([{ type: 'network-env', status: 'testing' }]) const networkEnvResult = await testNetworkEnvironment() setResults([networkEnvResult]) // 从网络环境检测结果中获取网络信息(用于后续测试判断) const isIntranet = networkEnvResult.details?.isIntranet ?? false // 2. DNS和连接测试 setResults(prev => [...prev, { type: 'connection', status: 'testing' }]) const connectionResult = await testDNSAndConnection() setResults(prev => [...prev.slice(0, -1), connectionResult]) // 3. 下载测速 setResults(prev => [...prev, { type: 'download', status: 'testing' }]) const downloadResult = await testDownloadSpeed() setResults(prev => [...prev.slice(0, -1), downloadResult]) // 4. 上传测速 setResults(prev => [...prev, { type: 'upload', status: 'testing' }]) const uploadResult = await testUploadSpeed() setResults(prev => [...prev.slice(0, -1), uploadResult]) // 5. 存储连通性 setResults(prev => [...prev, { type: 'storage', status: 'testing' }]) const storageResult = await testStorageConnectivity() setResults(prev => [...prev.slice(0, -1), storageResult]) // 6. 预览图片加载测试 setResults(prev => [...prev, { type: 'preview-image', status: 'testing' }]) const imageResult = await testPreviewImageLoad() setResults(prev => [...prev.slice(0, -1), imageResult]) // 7. 预览视频加载测试 setResults(prev => [...prev, { type: 'preview-video', status: 'testing' }]) const videoResult = await testPreviewVideoLoad() setResults(prev => [...prev.slice(0, -1), videoResult]) // 8. 音频文件加载测试 setResults(prev => [...prev, { type: 'preview-audio', status: 'testing' }]) const audioResult = await testPreviewAudioLoad() setResults(prev => [...prev.slice(0, -1), audioResult]) // 9. 代理性能测试 setResults(prev => [...prev, { type: 'proxy-performance', status: 'testing' }]) const proxyResult = await testProxyPerformance() setResults(prev => [...prev.slice(0, -1), proxyResult]) // 10. 视频Range请求测试 setResults(prev => [...prev, { type: 'video-range', status: 'testing' }]) const rangeResult = await testVideoRangeRequest() setResults(prev => [...prev.slice(0, -1), rangeResult]) // 11. 内网MinIO直连测试(仅内网用户) if (isIntranet) { setResults(prev => [...prev, { type: 'intranet-minio', status: 'testing' }]) const intranetResult = await testIntranetMinIO() setResults(prev => [...prev.slice(0, -1), intranetResult]) } // 12. 公网S3直连测试 setResults(prev => [...prev, { type: 'public-s3', status: 'testing' }]) const publicS3Result = await testPublicS3() setResults(prev => [...prev.slice(0, -1), publicS3Result]) // 13. 并发测试 setResults(prev => [...prev, { type: 'concurrent', status: 'testing' }]) const concurrentResult = await testConcurrent() setResults(prev => [...prev.slice(0, -1), concurrentResult]) // 14. 缓存测试 setResults(prev => [...prev, { type: 'cache', status: 'testing' }]) const cacheResult = await testCache() setResults(prev => [...prev.slice(0, -1), cacheResult]) // 15. S3存储性能分析 setResults(prev => [...prev, { type: 's3-performance', status: 'testing' }]) const s3Result = await testS3Performance() setResults(prev => [...prev.slice(0, -1), s3Result]) // 16. API: 素材列表测试 setResults(prev => [...prev, { type: 'api-materials-list', status: 'testing' }]) const apiMaterialsListResult = await testApiMaterialsList() setResults(prev => [...prev.slice(0, -1), apiMaterialsListResult]) // 17. API: 素材详情测试 setResults(prev => [...prev, { type: 'api-material-detail', status: 'testing' }]) const apiMaterialDetailResult = await testApiMaterialDetail() setResults(prev => [...prev.slice(0, -1), apiMaterialDetailResult]) // 18. API: 素材统计测试 setResults(prev => [...prev, { type: 'api-material-stats', status: 'testing' }]) const apiMaterialStatsResult = await testApiMaterialStats() setResults(prev => [...prev.slice(0, -1), apiMaterialStatsResult]) // 19. API: 上传初始化测试 setResults(prev => [...prev, { type: 'api-upload-init', status: 'testing' }]) const apiUploadInitResult = await testApiUploadInit() setResults(prev => [...prev.slice(0, -1), apiUploadInitResult]) // 20. API: 下载接口测试 setResults(prev => [...prev, { type: 'api-download', status: 'testing' }]) const apiDownloadResult = await testApiDownload() setResults(prev => [...prev.slice(0, -1), apiDownloadResult]) // 21. API: 用户信息测试 setResults(prev => [...prev, { type: 'api-user-profile', status: 'testing' }]) const apiUserProfileResult = await testApiUserProfile() setResults(prev => [...prev.slice(0, -1), apiUserProfileResult]) // 22. API: 系统设置测试 setResults(prev => [...prev, { type: 'api-system-settings', status: 'testing' }]) const apiSystemSettingsResult = await testApiSystemSettings() setResults(prev => [...prev.slice(0, -1), apiSystemSettingsResult]) // 23. API: 健康检查测试 setResults(prev => [...prev, { type: 'api-health', status: 'testing' }]) const apiHealthResult = await testApiHealth() setResults(prev => [...prev.slice(0, -1), apiHealthResult]) // 24. 数据库查询性能测试 setResults(prev => [...prev, { type: 'database-query', status: 'testing' }]) const databaseQueryResult = await testDatabaseQuery() setResults(prev => [...prev.slice(0, -1), databaseQueryResult]) // 25. Redis连接测试 setResults(prev => [...prev, { type: 'redis-connection', status: 'testing' }]) const redisConnectionResult = await testRedisConnection() setResults(prev => [...prev.slice(0, -1), redisConnectionResult]) // 26. 页面: 首页加载测试 setResults(prev => [...prev, { type: 'page-home-load', status: 'testing' }]) const pageHomeLoadResult = await testPageHomeLoad() setResults(prev => [...prev.slice(0, -1), pageHomeLoadResult]) // 27. 页面: 详情页加载测试 setResults(prev => [...prev, { type: 'page-detail-load', status: 'testing' }]) const pageDetailLoadResult = await testPageDetailLoad() setResults(prev => [...prev.slice(0, -1), pageDetailLoadResult]) // 28. 上传流程测试 setResults(prev => [...prev, { type: 'upload-flow', status: 'testing' }]) const uploadFlowResult = await testUploadFlow() setResults(prev => [...prev.slice(0, -1), uploadFlowResult]) // 29. 下载流程测试 setResults(prev => [...prev, { type: 'download-flow', status: 'testing' }]) const downloadFlowResult = await testDownloadFlow() setResults(prev => [...prev.slice(0, -1), downloadFlowResult]) // 30. 转码服务测试 setResults(prev => [...prev, { type: 'transcode-service', status: 'testing' }]) const transcodeServiceResult = await testTranscodeService() setResults(prev => [...prev.slice(0, -1), transcodeServiceResult]) // 31. 认证Session测试 setResults(prev => [...prev, { type: 'auth-session', status: 'testing' }]) const authSessionResult = await testAuthSession() setResults(prev => [...prev.slice(0, -1), authSessionResult]) // 32. 内外网URL重写测试 if (isIntranet) { // 内网用户的URL重写测试 setResults(prev => [...prev, { type: 'intranet-url-rewrite', status: 'testing' }]) const intranetUrlRewriteResult = await testIntranetUrlRewrite() setResults(prev => [...prev.slice(0, -1), intranetUrlRewriteResult]) } else { // 外网用户的URL重写测试 setResults(prev => [...prev, { type: 'public-url-rewrite', status: 'testing' }]) const publicUrlRewriteResult = await testPublicUrlRewrite() setResults(prev => [...prev.slice(0, -1), publicUrlRewriteResult]) } } catch (error) { console.error('诊断过程出错:', error) showToast('诊断过程出错', { variant: 'error' }) } finally { // 清理诊断过程中创建的所有资源 await cleanupDiagnosticResources() setTesting(false) } } // 32. 内网用户URL重写测试 const testIntranetUrlRewrite = async (): Promise => { try { // 增加pageSize提高找到有效素材的概率(内网用户专用测试) const response = await fetch('/api/materials?pageSize=10', { cache: 'no-store' }) if (!response.ok) throw new Error('获取测试素材失败') const data = await response.json() const materials = data.materials || data if (!materials || materials.length === 0) { return { type: 'intranet-url-rewrite', status: 'warning', message: '没有可测试的素材' } } // 查找第一个有previewUrl的素材 const materialWithPreview = materials.find((m: any) => m.previewUrl) if (!materialWithPreview || !materialWithPreview.previewUrl) { return { type: 'intranet-url-rewrite', status: 'warning', message: '没有找到有预览URL的素材' } } const testUrl = materialWithPreview.previewUrl // 检查URL是否被正确重写为内网MinIO地址 const isIntranetMinIO = testUrl.includes('192.168.') || testUrl.includes('localhost') || testUrl.includes('127.0.0.1') const isProxyUrl = testUrl.includes('/api/proxy-media') const isPublicS3 = isPublicS3Url(testUrl) let status: 'success' | 'warning' | 'error' = 'success' let message = '' const issues: string[] = [] if (isIntranetMinIO && !isProxyUrl) { message = 'URL已正确重写为内网MinIO地址(直连,不走代理)' } else if (isProxyUrl && isPublicS3) { status = 'warning' issues.push('内网用户仍通过代理访问公网S3,应直接访问内网MinIO') message = `URL通过代理访问: ${issues.join(', ')}` } else if (isPublicS3 && !isProxyUrl) { status = 'warning' issues.push('内网用户直接访问公网S3,应重写为内网MinIO') message = `URL未重写: ${issues.join(', ')}` } else { message = `URL格式: ${isProxyUrl ? '代理URL' : isIntranetMinIO ? '内网MinIO' : '其他'}` } return { type: 'intranet-url-rewrite', status, message, details: { url: testUrl, isIntranetMinIO, isProxyUrl, isPublicS3, issues: issues.length > 0 ? issues : undefined } } } catch (error) { return { type: 'intranet-url-rewrite', status: 'error', message: `内网URL重写测试失败: ${error instanceof Error ? error.message : '未知错误'}` } } } // 33. 外网用户URL重写测试 const testPublicUrlRewrite = async (): Promise => { try { // 增加pageSize提高找到有效素材的概率(外网用户专用测试) const response = await fetch('/api/materials?pageSize=10', { cache: 'no-store' }) if (!response.ok) throw new Error('获取测试素材失败') const data = await response.json() const materials = data.materials || data if (!materials || materials.length === 0) { return { type: 'public-url-rewrite', status: 'warning', message: '没有可测试的素材' } } // 查找第一个有previewUrl的素材 const materialWithPreview = materials.find((m: any) => m.previewUrl) if (!materialWithPreview || !materialWithPreview.previewUrl) { return { type: 'public-url-rewrite', status: 'warning', message: '没有找到有预览URL的素材' } } const testUrl = materialWithPreview.previewUrl // 检查URL是否正确(外网用户应使用公网S3或代理) const isIntranetMinIO = testUrl.includes('192.168.') || testUrl.includes('localhost') || testUrl.includes('127.0.0.1') const isProxyUrl = testUrl.includes('/api/proxy-media') const isPublicS3 = isPublicS3Url(testUrl) let status: 'success' | 'warning' | 'error' = 'success' let message = '' const issues: string[] = [] if (isIntranetMinIO) { status = 'error' issues.push('外网用户不应访问内网MinIO地址') message = `URL错误: ${issues.join(', ')}` } else if (isProxyUrl || isPublicS3) { message = `URL格式正确: ${isProxyUrl ? '通过代理' : '直接访问公网S3'}` } else { status = 'warning' message = 'URL格式未知' } return { type: 'public-url-rewrite', status, message, details: { url: testUrl, isIntranetMinIO, isProxyUrl, isPublicS3, issues: issues.length > 0 ? issues : undefined } } } catch (error) { return { type: 'public-url-rewrite', status: 'error', message: `外网URL重写测试失败: ${error instanceof Error ? error.message : '未知错误'}` } } } // 复制结果(增强版:包含所有详细信息) const copyResults = async () => { if (results.length === 0) return const timestamp = new Date().toLocaleString('zh-CN') const browserInfo = navigator.userAgent const currentUrl = window.location.href // 获取浏览器详细信息 const connection = (navigator as any).connection || (navigator as any).mozConnection || (navigator as any).webkitConnection const networkType = connection?.effectiveType || 'unknown' const downlink = connection?.downlink || 'unknown' const rtt = connection?.rtt || 'unknown' const saveData = connection?.saveData || false const browserDownlinkSpeed = typeof downlink === 'number' ? formatHumanSpeed((downlink * 1000 * 1000) / 8) : '未知' const report = [ '=== 网络诊断完整报告 ===', `时间: ${timestamp}`, `浏览器: ${browserInfo}`, `当前URL: ${currentUrl}`, '', '=== 网络环境信息 ===', `在线状态: ${navigator.onLine ? '在线' : '离线'}`, `网络类型: ${networkType}`, `浏览器估算下行: ${browserDownlinkSpeed}`, `RTT延迟: ${rtt !== 'unknown' ? `${rtt} ms` : '未知'}`, `省流模式: ${saveData ? '开启' : '关闭'}`, networkInfo ? [ `内网/外网: ${networkInfo.isIntranet ? '内网' : '外网'}`, `访问域名: ${networkInfo.hostname}`, `客户端IP: ${networkInfo.clientIp}`, `公网域名: ${networkInfo.isPublicDomain ? '是' : '否'}` ].join('\n') : '', '', '=== 测试结果 ===', ...results.map(r => { const typeMap: Record = { 'network-env': '网络环境检测', 'connection': 'DNS和连接测试', 'download': '下载测速', 'upload': '上传测速', 'storage': '存储连通性', 'preview-image': '预览图片加载', 'preview-video': '预览视频加载', 'preview-audio': '音频文件加载', 'proxy-performance': '代理性能测试', 'video-range': '视频Range请求测试', 'intranet-minio': '内网MinIO直连测试', 'public-s3': '公网S3直连测试', 'concurrent': '并发测试', 'cache': '缓存测试', 's3-performance': 'S3存储性能分析', 'api-materials-list': 'API: 素材列表', 'api-material-detail': 'API: 素材详情', 'api-material-stats': 'API: 素材统计', 'api-upload-init': 'API: 上传初始化', 'api-download': 'API: 下载接口', 'api-user-profile': 'API: 用户信息', 'api-system-settings': 'API: 系统设置', 'api-health': 'API: 健康检查', 'database-query': '数据库查询性能', 'redis-connection': 'Redis连接测试', 'page-home-load': '页面: 首页加载', 'page-detail-load': '页面: 详情页加载', 'upload-flow': '上传流程测试', 'download-flow': '下载流程测试', 'transcode-service': '转码服务测试', 'auth-session': '认证Session测试', 'intranet-url-rewrite': '内网URL重写测试', 'public-url-rewrite': '外网URL重写测试' } const typeText = typeMap[r.type] || r.type const statusText = r.status === 'success' ? '✓ 成功' : r.status === 'error' ? '✗ 失败' : r.status === 'warning' ? '⚠ 警告' : '⋯ 测试中' let detailsText = '' if (r.details) { const details: string[] = [] if (r.details.dnsTime !== undefined) details.push(`DNS解析: ${r.details.dnsTime.toFixed(0)}ms`) if (r.details.connectTime !== undefined) details.push(`TCP连接: ${r.details.connectTime.toFixed(0)}ms`) if (r.details.sslTime !== undefined) details.push(`SSL握手: ${r.details.sslTime.toFixed(0)}ms`) if (r.details.ttfb !== undefined) details.push(`首字节时间: ${r.details.ttfb.toFixed(0)}ms`) if (r.details.requestTime !== undefined) details.push(`请求时间: ${r.details.requestTime.toFixed(0)}ms`) if (r.details.responseTime !== undefined) details.push(`响应时间: ${r.details.responseTime.toFixed(0)}ms`) if (r.details.totalTime !== undefined) details.push(`总耗时: ${r.details.totalTime.toFixed(0)}ms`) if (r.details.fileSize !== undefined) details.push(`文件大小: ${formatFileSize(r.details.fileSize)}`) if (r.details.downloadSpeed !== undefined) details.push(`下载速度: ${formatHumanSpeed(r.details.downloadSpeed)}`) if (r.details.url) details.push(`URL: ${r.details.url}`) if (r.details.isFrp !== undefined) details.push(`FRP穿透: ${r.details.isFrp ? '是' : '否'}`) if (r.details.supportsRange !== undefined) details.push(`Range支持: ${r.details.supportsRange ? '是' : '否'}`) if (r.details.cacheSpeedup !== undefined) details.push(`缓存加速: ${r.details.cacheSpeedup.toFixed(1)}%`) if (r.details.fileCount !== undefined) details.push(`并发文件数: ${r.details.fileCount}`) if (r.details.avgTimePerFile !== undefined) details.push(`平均时间/文件: ${r.details.avgTimePerFile.toFixed(0)}ms`) if (r.details.materialCount !== undefined) details.push(`素材数量: ${r.details.materialCount}`) if (r.details.materialId !== undefined) details.push(`素材ID: ${r.details.materialId}`) if (r.details.uploadId !== undefined) details.push(`上传ID: ${r.details.uploadId}`) if (r.details.userId !== undefined) details.push(`用户ID: ${r.details.userId}`) if (r.details.statusCode !== undefined) details.push(`HTTP状态: ${r.details.statusCode}`) if (r.details.contentType !== undefined) details.push(`内容类型: ${r.details.contentType}`) if (r.details.htmlSize !== undefined) details.push(`HTML大小: ${formatFileSize(r.details.htmlSize)}`) if (r.details.queueSize !== undefined) details.push(`队列大小: ${r.details.queueSize}`) if (r.details.processing !== undefined) details.push(`处理中: ${r.details.processing}`) if (r.details.isAuthenticated !== undefined) details.push(`认证状态: ${r.details.isAuthenticated ? '已登录' : '未登录'}`) if (r.details.redisStatus !== undefined) details.push(`Redis状态: ${r.details.redisStatus}`) if (r.details.step !== undefined) details.push(`步骤: ${r.details.step}`) if (r.details.stats !== undefined) details.push(`统计信息: ${JSON.stringify(r.details.stats).substring(0, 100)}`) if (r.details.health !== undefined) details.push(`健康状态: ${JSON.stringify(r.details.health).substring(0, 100)}`) if (r.details.error !== undefined) details.push(`错误信息: ${r.details.error}`) if (r.details.issues && Array.isArray(r.details.issues) && r.details.issues.length > 0) { details.push(`问题: ${r.details.issues.join(', ')}`) } if (details.length > 0) { detailsText = '\n 详细信息: ' + details.join(', ') } } return `${typeText}: ${statusText}\n ${r.message || '测试中...'}${detailsText}` }), '', '=== 性能分析 ===', ...results.filter(r => r.status === 'warning' || r.status === 'error').map(r => { const typeMap: Record = { 'preview-image': '预览图片', 'preview-video': '预览视频', 'preview-audio': '音频文件', 'connection': '网络连接', 'proxy-performance': '代理性能', 'video-range': '视频Range请求', 'intranet-minio': '内网MinIO', 'public-s3': '公网S3', 'concurrent': '并发性能', 'cache': '缓存效果', 's3-performance': 'S3存储性能', 'api-materials-list': '素材列表API', 'api-material-detail': '素材详情API', 'api-material-stats': '素材统计API', 'api-upload-init': '上传初始化API', 'api-download': '下载API', 'api-user-profile': '用户信息API', 'api-system-settings': '系统设置API', 'api-health': '健康检查API', 'database-query': '数据库查询', 'redis-connection': 'Redis连接', 'page-home-load': '首页加载', 'page-detail-load': '详情页加载', 'upload-flow': '上传流程', 'download-flow': '下载流程', 'transcode-service': '转码服务', 'auth-session': '认证Session', 'intranet-url-rewrite': '内网URL重写', 'public-url-rewrite': '外网URL重写' } const typeText = typeMap[r.type] || r.type return `⚠ ${typeText}${r.status === 'error' ? '失败' : '性能较差'},可能影响用户体验` }), '', '=== 优化建议 ===', ...results .filter(r => r.details?.suggestions && Array.isArray(r.details.suggestions)) .flatMap(r => r.details!.suggestions as string[]), '', '提示: 请将此报告提供给技术支持以便快速定位问题' ].filter(Boolean).join('\n') try { if (navigator.clipboard && navigator.clipboard.writeText) { await navigator.clipboard.writeText(report) setCopied(true) setTimeout(() => setCopied(false), 2000) return } throw new Error('Clipboard API not available') } catch (error) { console.error('复制失败,尝试降级方案:', error) try { const textarea = document.createElement('textarea') textarea.value = report textarea.style.position = 'fixed' textarea.style.left = '-999999px' textarea.style.top = '-999999px' textarea.style.opacity = '0' textarea.setAttribute('readonly', '') document.body.appendChild(textarea) if (navigator.userAgent.match(/ipad|iphone/i)) { const range = document.createRange() range.selectNodeContents(textarea) const selection = window.getSelection() selection?.removeAllRanges() selection?.addRange(range) textarea.setSelectionRange(0, 999999) } else { textarea.select() } const successful = document.execCommand('copy') document.body.removeChild(textarea) if (successful) { setCopied(true) setTimeout(() => setCopied(false), 2000) } else { throw new Error('execCommand failed') } } catch (err) { console.error('降级复制也失败:', err) showToast('复制失败,请手动复制结果', { variant: 'error' }) } } } const getStatusIcon = (status: string) => { switch (status) { case 'testing': return ( ) case 'success': return ( ) case 'warning': return ( ) case 'error': return ( ) default: return null } } const getTypeText = (type: string) => { switch (type) { case 'network-env': return '网络环境检测' case 'connection': return 'DNS和连接' case 'download': return '下载测速' case 'upload': return '上传测速' case 'storage': return '存储连通性' case 'preview-image': return '预览图片加载' case 'preview-video': return '预览视频加载' case 'preview-audio': return '音频文件加载' case 'proxy-performance': return '代理性能测试' case 'video-range': return '视频Range请求' case 'intranet-minio': return '内网MinIO直连' case 'public-s3': return '公网S3直连' case 'concurrent': return '并发测试' case 'cache': return '缓存测试' case 's3-performance': return 'S3存储性能' case 'api-materials-list': return 'API: 素材列表' case 'api-material-detail': return 'API: 素材详情' case 'api-material-stats': return 'API: 素材统计' case 'api-upload-init': return 'API: 上传初始化' case 'api-download': return 'API: 下载接口' case 'api-user-profile': return 'API: 用户信息' case 'api-system-settings': return 'API: 系统设置' case 'api-health': return 'API: 健康检查' case 'database-query': return '数据库查询' case 'redis-connection': return 'Redis连接' case 'page-home-load': return '页面: 首页加载' case 'page-detail-load': return '页面: 详情页加载' case 'upload-flow': return '上传流程' case 'download-flow': return '下载流程' case 'transcode-service': return '转码服务' case 'auth-session': return '认证Session' case 'intranet-url-rewrite': return '内网URL重写' case 'public-url-rewrite': return '外网URL重写' default: return '' } } if (!isOpen) return null return (
{/* Header */}

网络诊断(完整版)

全面测试网络连接、存储服务和文件加载性能

{/* Content */}
{results.length === 0 ? (

点击下方按钮开始完整诊断

将测试33项网络、API、数据库和业务流程指标,预计耗时60-120秒

) : (
{results.map((result, index) => (
{getStatusIcon(result.status)}

{getTypeText(result.type)}

{result.message || '测试中...'}

{result.details && (
{result.details.dnsTime !== undefined && (
DNS解析: {result.details.dnsTime.toFixed(0)}ms
)} {result.details.connectTime !== undefined && (
TCP连接: {result.details.connectTime.toFixed(0)}ms
)} {result.details.sslTime !== undefined && (
SSL握手: {result.details.sslTime.toFixed(0)}ms
)} {result.details.ttfb !== undefined && (
首字节时间: {result.details.ttfb.toFixed(0)}ms
)}
)}
))}
)}
{/* Footer */}
{results.length > 0 && !testing && ( )}
{/* 统一的Toast提示 */} {toast && toast.show && ( )}
) }