import { NextResponse } from 'next/server' import os from 'os' import { exec } from 'child_process' import { promisify } from 'util' import { getAllSessions } from '@/lib/upload-sessions-redis' import { transcodeQueue } from '@/lib/video-transcode-queue' import { prisma } from '@/lib/prisma' import { getNetworkSpeed } from '@/lib/network-bandwidth-monitor' const execAsync = promisify(exec) // 性能监控 - 快速统计接口(用于悬停卡片) // GET /api/performance/quick export const dynamic = 'force-dynamic' export const revalidate = 0 // 获取CPU使用率(正确的方法:需要两次采样) async function getCpuUsage(): Promise { try { // 方法1:使用 /proc/stat 两次采样计算CPU使用率(最准确) const getCpuStats = async () => { const { stdout } = await execAsync("cat /proc/stat | grep '^cpu '") const parts = stdout.trim().split(/\s+/) return { user: parseInt(parts[1]) || 0, nice: parseInt(parts[2]) || 0, system: parseInt(parts[3]) || 0, idle: parseInt(parts[4]) || 0, iowait: parseInt(parts[5]) || 0, irq: parseInt(parts[6]) || 0, softirq: parseInt(parts[7]) || 0, } } const stats1 = await getCpuStats() await new Promise(resolve => setTimeout(resolve, 1000)) // 等待1秒 const stats2 = await getCpuStats() const total1 = stats1.user + stats1.nice + stats1.system + stats1.idle + stats1.iowait + stats1.irq + stats1.softirq const total2 = stats2.user + stats2.nice + stats2.system + stats2.idle + stats2.iowait + stats2.irq + stats2.softirq const totalDiff = total2 - total1 if (totalDiff > 0) { const idleDiff = stats2.idle - stats1.idle const usedDiff = totalDiff - idleDiff const cpuUsage = (usedDiff / totalDiff) * 100 return Math.min(Math.max(Math.round(cpuUsage), 0), 100) } } catch (error) { console.warn('[PerformanceQuick] Failed to get CPU usage from /proc/stat:', error) } try { // 方法2:尝试使用 top 命令(批处理模式,更准确) const { stdout } = await execAsync("top -bn2 -d1 | grep 'Cpu(s)' | tail -1 | awk '{print $2}' | sed 's/%us,//'", { timeout: 3000 }) const cpuUsage = parseFloat(stdout.trim()) if (!isNaN(cpuUsage) && cpuUsage >= 0 && cpuUsage <= 100) { return Math.round(cpuUsage) } } catch (error) { console.warn('[PerformanceQuick] Failed to get CPU usage from top:', error) } try { // 方法3:使用 vmstat(如果可用) const { stdout } = await execAsync("vmstat 1 2 | tail -1 | awk '{print 100-$15}'", { timeout: 3000 }) const cpuUsage = parseFloat(stdout.trim()) if (!isNaN(cpuUsage) && cpuUsage >= 0 && cpuUsage <= 100) { return Math.round(cpuUsage) } } catch (error) { console.warn('[PerformanceQuick] Failed to get CPU usage from vmstat:', error) } // 降级方案:使用负载平均值估算(不准确,但总比没有好) const loadAvg = os.loadavg()[0] const cpuCount = os.cpus().length // 负载平均值反映系统负载,但不等同于CPU使用率 // 负载平均值 = CPU核心数时,表示CPU满载 // 这里做一个粗略估算:负载平均值 / CPU核心数 * 100,但限制在100%以内 const estimatedUsage = Math.min(Math.round((loadAvg / cpuCount) * 100), 100) console.warn(`[PerformanceQuick] Using loadavg estimation: load=${loadAvg}, cores=${cpuCount}, estimated=${estimatedUsage}%`) return estimatedUsage } export async function GET() { try { // 1. 系统性能(先用 Node.js os 模块,后续可接 TrueNAS) const loadAvg = os.loadavg() // [1min, 5min, 15min] const totalMem = os.totalmem() const freeMem = os.freemem() const usedMem = totalMem - freeMem const memPercentage = totalMem > 0 ? Math.round((usedMem / totalMem) * 100) : 0 // 获取更准确的CPU使用率 const cpuUsage = await getCpuUsage() const cpu = { usage: cpuUsage, loadAverage: loadAvg, } const memory = { used: Math.round(usedMem / (1024 * 1024)), // MB total: Math.round(totalMem / (1024 * 1024)), // MB percentage: memPercentage, } // 获取网络带宽使用情况 const networkSpeed = await getNetworkSpeed() const network = { uploadSpeed: networkSpeed?.uploadSpeed || 0, // 字节/秒 downloadSpeed: networkSpeed?.downloadSpeed || 0, // 字节/秒 interface: networkSpeed?.interface || 'unknown', // 使用的网卡 } // 2. 上传队列(从 Redis 获取,只显示真正活跃的上传) const uploadProcessing: any[] = [] const uploadWaiting: any[] = [] const uploadUserIds = new Set() const uploadIds = new Set() // 从 Redis 获取所有上传会话 const uploadSessions = await getAllSessions() // 只显示真正进行中的上传(进度 < 100% 且最近有活动) const now = Date.now() // 说明: // - lastActivityAt:每个 chunk 上传完成后更新(更能代表“是否真的在上传”) // - createdAt:只能代表会话创建时间,不能代表持续活跃 const ACTIVE_THRESHOLD = 5 * 60 * 1000 // 5分钟内有活动 → 认为“活跃” const MAX_SHOW_THRESHOLD = 60 * 60 * 1000 // 超过1小时无活动 → 不再展示(避免长期误导) uploadSessions.forEach((session, uploadId) => { const completedChunks = session.chunks.size const totalChunks = session.totalChunks || 1 const progress = Math.min( 100, Math.round((completedChunks / totalChunks) * 100) ) const lastActivityAt = (session as any).lastActivityAt || session.createdAt || 0 const inactiveMs = lastActivityAt > 0 ? now - lastActivityAt : Number.POSITIVE_INFINITY const isActive = inactiveMs >= 0 && inactiveMs < ACTIVE_THRESHOLD const isTooOld = inactiveMs >= MAX_SHOW_THRESHOLD // 只展示:未完成 + 未超过“最大无活动时间” // 注意:即使不是“活跃”,也会展示(并返回 inactiveMs),用于解释“队列里有任务但网速很低”的情况 if (progress < 100) { if (!isTooOld) { uploadUserIds.add(session.userId) uploadIds.add(uploadId) uploadProcessing.push({ uploadId, fileName: session.fileName, progress, userId: session.userId, currentSpeed: (session as any).currentSpeed || 0, // 上传速度(字节/秒,估算) uploadedBytes: (session as any).uploadedBytes || 0, lastActivityAt, inactiveMs, isActive, }) } } }) // 批量查询用户信息 const userInfoMap = new Map() if (uploadUserIds.size > 0) { try { const users = await prisma.user.findMany({ where: { id: { in: Array.from(uploadUserIds) }, } as any, select: { id: true, username: true, realName: true, } as any, }) users.forEach((user: any) => { userInfoMap.set(user.id, { username: user.username || '未知', realName: user.realName || undefined, }) }) } catch (error) { console.error('[PerformanceQuick] Failed to fetch user info:', error) } } // 尝试通过uploadId查询已创建的material记录(获取materialType) const uploadMaterialInfoMap = new Map() if (uploadIds.size > 0) { try { const materials = await prisma.material.findMany({ where: { uploadId: { in: Array.from(uploadIds) }, deletedAt: null, } as any, select: { uploadId: true, materialType: true, } as any, }) materials.forEach((m: any) => { if (m.uploadId) { uploadMaterialInfoMap.set(m.uploadId, { materialType: m.materialType || '未知类型', }) } }) } catch (error) { console.error('[PerformanceQuick] Failed to fetch upload material info:', error) } } // 添加用户名和素材类型到上传队列 const uploadProcessingWithUsers = uploadProcessing.map((task) => { const userInfo = userInfoMap.get(task.userId) || { username: '未知用户' } const materialInfo = uploadMaterialInfoMap.get(task.uploadId) || { materialType: '未知类型' } return { ...task, username: userInfo.realName || userInfo.username || '未知用户', materialType: materialInfo.materialType, } }) const uploadQueue = { processing: uploadProcessingWithUsers, waiting: uploadWaiting, total: uploadProcessingWithUsers.length + uploadWaiting.length, } // 3. 转码队列(基于 transcodeQueue) const status = transcodeQueue.getStatus() const progressObj: Record = {} const stageObj: Record = {} // Map 无法直接序列化,转成普通对象 status.progress.forEach((value, key) => { progressObj[key] = value }) status.stages.forEach((value, key) => { stageObj[key] = value }) // 查询转码队列中的素材信息(文件名、类型、上传人等) const processingMaterialIds = status.processingIds const waitingMaterialIds = status.waiting const allMaterialIds = [...processingMaterialIds, ...waitingMaterialIds] const materialInfoMap = new Map() if (allMaterialIds.length > 0) { try { const materials = await prisma.material.findMany({ where: { id: { in: allMaterialIds }, deletedAt: null, } as any, select: { id: true, zipFileName: true, materialType: true, authorName: true, user: { select: { username: true, }, }, } as any, }) materials.forEach((m: any) => { materialInfoMap.set(m.id, { fileName: m.zipFileName || '未知文件', materialType: m.materialType || '未知类型', authorName: m.authorName || m.user?.username || '未知用户', }) }) } catch (error) { console.error('[PerformanceQuick] Failed to fetch material info:', error) } } const transcodeQueueData = { processing: processingMaterialIds.map((id) => { const info = materialInfoMap.get(id) || { fileName: '未知文件', materialType: '未知类型', authorName: '未知用户', } return { materialId: id, fileName: info.fileName, materialType: info.materialType, authorName: info.authorName, progress: progressObj[id] ?? null, stage: stageObj[id] ?? null, } }), waiting: waitingMaterialIds.map((id, index) => { const info = materialInfoMap.get(id) || { fileName: '未知文件', materialType: '未知类型', authorName: '未知用户', } return { materialId: id, fileName: info.fileName, materialType: info.materialType, authorName: info.authorName, queuePosition: index + 1, } }), total: status.queueLength, maxConcurrent: status.maxConcurrent, } return NextResponse.json({ cpu, memory, network, uploadQueue, transcodeQueue: transcodeQueueData, }) } catch (error) { console.error('[PerformanceQuick] 获取性能数据失败:', error) return NextResponse.json( { error: '获取性能数据失败' }, { status: 500 } ) } }