import { NextRequest, NextResponse } from 'next/server' import { prisma } from '@/lib/prisma' import { storeFile } from '@/lib/fileStorage' import { push } from '@/lib/push' import { withCors } from '@/lib/middleware' import ffmpeg from 'fluent-ffmpeg' import { writeFileSync, unlinkSync, mkdtempSync, readFileSync, rmSync } from 'fs' import { tmpdir } from 'os' import { join } from 'path' interface WindowInfo { title: string path: string memory: number } const timeoutMap = new Map() async function handleScreenshotUpload(req: NextRequest) { try { const pathSegments = req.nextUrl.pathname.split('/') const hostnameIndex = pathSegments.indexOf('hosts') + 1 const hostname = pathSegments[hostnameIndex] if (!hostname) { return NextResponse.json({ error: '缺少主机名' }, { status: 400 }) } const formData = await req.formData() const files: File[] = [] const windowsInfo: WindowInfo[] = JSON.parse(formData.get('windows_info') as string || '[]') // Extract files from formData for (const [key, value] of formData.entries()) { if (key.startsWith('screenshot_') && value instanceof File) { files.push(value) } } if (files.length === 0) { return NextResponse.json({ error: '没有收到文件' }, { status: 400 }) } // Process screenshots const processingStartTime = Date.now() console.log(`[${hostname}] 开始处理 ${files.length} 张截图`) const screenshots = await Promise.all(files.map(async (file, index) => { const imageStartTime = Date.now() // Convert image to AV1 format using FFmpeg const buffer = Buffer.from(await file.arrayBuffer()) const originalSize = buffer.length // Create temporary files const tempDir = mkdtempSync(join(tmpdir(), 'ffmpeg-')) const inputPath = join(tempDir, `input_${index}.png`) const outputPath = join(tempDir, `output_${index}.avif`) try { // Write input file writeFileSync(inputPath, buffer) const conversionStartTime = Date.now() // Convert using FFmpeg with libsvtav1 await new Promise((resolve, reject) => { ffmpeg(inputPath) .videoCodec('libsvtav1') .outputOptions([ '-crf 30', // 质量控制,值越小质量越好 '-preset 6', // 编码速度预设,0-12,值越大速度越快但质量可能下降 '-pix_fmt yuv420p' // 像素格式 ]) .output(outputPath) .on('end', () => resolve()) .on('error', (err) => reject(err)) .run() }) const conversionTime = Date.now() - conversionStartTime // Read the converted file const avifBuffer = readFileSync(outputPath) const filename = `${hostname}-${Date.now()}-${Math.round(Math.random() * 1E9)}.avif` const storageStartTime = Date.now() const storedFile = await storeFile(avifBuffer, filename, 'image/avif', 'screenshot', hostname) const storageTime = Date.now() - storageStartTime const totalImageTime = Date.now() - imageStartTime const compressionRatio = ((originalSize - avifBuffer.length) / originalSize * 100).toFixed(2) console.log(`[${hostname}] 图片 ${index + 1}: FFmpeg转换 ${conversionTime}ms, 存储 ${storageTime}ms, 总计 ${totalImageTime}ms, 压缩率 ${compressionRatio}% (${originalSize} -> ${avifBuffer.length} bytes)`) return { fileId: storedFile.id, objectName: storedFile.objectName, filename: storedFile.filename, contentType: storedFile.contentType, fileSize: storedFile.size, monitorName: formData.get(`monitor_name_${index}`) as string || `Monitor ${index + 1}` } } finally { // Clean up temporary files try { rmSync(tempDir, { recursive: true, force: true }) } catch (err) { console.warn(`清理临时文件失败: ${err}`) } } })) const totalProcessingTime = Date.now() - processingStartTime console.log(`[${hostname}] 完成处理 ${files.length} 张截图,总耗时 ${totalProcessingTime}ms,平均每张 ${(totalProcessingTime / files.length).toFixed(2)}ms`) // Ensure host exists first await prisma.host.upsert({ where: { hostname }, update: {}, create: { hostname } }) // Create new record const newRecord = await prisma.record.create({ data: { hostname, timestamp: new Date(), windows: { create: windowsInfo }, screenshots: { create: screenshots } } }) // Handle host status and notifications const host = await prisma.host.findUnique({ where: { hostname }, select: { lastUpdate: true } }) const lastUpdate = host?.lastUpdate || new Date(0) if (lastUpdate.getTime() === new Date(0).getTime()) { push(`新设备 ${decodeURI(hostname)} 上线`) } else if (lastUpdate.getTime() < new Date().getTime() - 1000 * 60) { push(`设备 ${decodeURI(hostname)} 上线`) } // Clear existing timeout and set new one if (timeoutMap.has(hostname)) { clearTimeout(timeoutMap.get(hostname)!) } timeoutMap.set(hostname, setTimeout(() => { push(`设备 ${decodeURI(hostname)} 离线`) timeoutMap.delete(hostname) }, 1000 * 60)) // Update host last update time await prisma.host.upsert({ where: { hostname }, update: { lastUpdate: new Date() }, create: { hostname, lastUpdate: new Date() } }) return NextResponse.json({ message: '上传成功', hostname, filesCount: files.length, windowsCount: windowsInfo.length }) } catch (error) { console.error('上传失败:', error) return NextResponse.json({ error: '上传失败' }, { status: 500 }) } } async function handleGetScreenshots(req: NextRequest) { try { const pathSegments = req.nextUrl.pathname.split('/') const hostnameIndex = pathSegments.indexOf('hosts') + 1 const hostname = pathSegments[hostnameIndex] if (!hostname) { return NextResponse.json({ error: '缺少主机名' }, { status: 400 }) } const { searchParams } = req.nextUrl const startTimeParam = searchParams.get('startTime') const endTimeParam = searchParams.get('endTime') let startTime: Date | undefined let endTime: Date | undefined if (startTimeParam) { const timestamp = isNaN(Number(startTimeParam)) ? new Date(startTimeParam) : new Date(Number(startTimeParam) * 1000) startTime = timestamp } if (endTimeParam) { const timestamp = isNaN(Number(endTimeParam)) ? new Date(endTimeParam) : new Date(Number(endTimeParam) * 1000) endTime = timestamp } // Build query conditions const whereClause: any = { hostname } if (startTime || endTime) { whereClause.timestamp = {} if (startTime) whereClause.timestamp.gte = startTime if (endTime) whereClause.timestamp.lte = endTime } // Get records const records = await prisma.record.findMany({ where: whereClause, include: { windows: true, screenshots: true }, orderBy: { timestamp: 'desc' } }) // Get host info const host = await prisma.host.findUnique({ where: { hostname }, select: { hostname: true, lastUpdate: true } }) if (!host) { return NextResponse.json({ error: '未找到主机记录' }, { status: 404 }) } // Convert BigInt to string in windows.memory field const serializedRecords = records.map(record => ({ ...record, windows: record.windows.map(window => ({ ...window, memory: window.memory.toString() })) })) return NextResponse.json({ hostname, lastUpdate: host.lastUpdate, records: serializedRecords, total: records.length }) } catch (error) { console.error('获取记录失败:', error) return NextResponse.json({ error: '获取记录失败' }, { status: 500 }) } } export const POST = withCors(handleScreenshotUpload) export const GET = withCors(handleGetScreenshots)