const logger = require('../utils/logger'); const fs = require('fs').promises; const path = require('path'); const crypto = require('crypto'); /** * Универсальный процессор медиа-контента для всех каналов связи * Обрабатывает текст, аудио, видео, файлы и их комбинации */ class UniversalMediaProcessor { constructor() { // Реальные поддерживаемые форматы из frontend/src/components/ChatInterface.vue this.supportedAudioFormats = ['.mp3', '.wav']; this.supportedVideoFormats = ['.mp4', '.avi']; this.supportedImageFormats = ['.jpg', '.jpeg', '.png', '.gif']; this.supportedDocumentFormats = ['.txt', '.pdf', '.docx', '.xlsx', '.pptx', '.odt', '.ods', '.odp']; this.supportedArchiveFormats = ['.zip', '.rar', '.7z']; // Реальные ограничения размеров из кода: // - uploads.js: 5MB для изображений // - emailBot.js: 10MB для вложений // - frontend: без ограничений (но браузер обычно ограничивает) this.maxFileSize = 10 * 1024 * 1024; // 10MB (как в emailBot) this.maxImageSize = 5 * 1024 * 1024; // 5MB (как в uploads.js) this.uploadPath = path.join(__dirname, '../uploads'); this.ensureUploadDir(); } async ensureUploadDir() { try { await fs.mkdir(this.uploadPath, { recursive: true }); await fs.mkdir(path.join(this.uploadPath, 'audio'), { recursive: true }); await fs.mkdir(path.join(this.uploadPath, 'video'), { recursive: true }); await fs.mkdir(path.join(this.uploadPath, 'images'), { recursive: true }); await fs.mkdir(path.join(this.uploadPath, 'documents'), { recursive: true }); await fs.mkdir(path.join(this.uploadPath, 'archives'), { recursive: true }); } catch (error) { logger.error('[UniversalMediaProcessor] Ошибка создания директорий:', error); } } /** * Определяет тип медиа по расширению файла и MIME-типу */ getMediaType(filename, mimeType = null) { const ext = path.extname(filename).toLowerCase(); // Сначала проверяем по расширению if (this.supportedAudioFormats.includes(ext)) return 'audio'; if (this.supportedVideoFormats.includes(ext)) return 'video'; if (this.supportedImageFormats.includes(ext)) return 'image'; if (this.supportedDocumentFormats.includes(ext)) return 'document'; if (this.supportedArchiveFormats.includes(ext)) return 'archive'; // Если есть MIME-тип, проверяем по нему if (mimeType) { const mime = mimeType.toLowerCase(); if (mime.startsWith('audio/')) return 'audio'; if (mime.startsWith('video/')) return 'video'; if (mime.startsWith('image/')) return 'image'; if (mime.startsWith('application/')) { // Документы и архивы if (mime.includes('pdf') || mime.includes('document') || mime.includes('sheet') || mime.includes('presentation')) { return 'document'; } if (mime.includes('zip') || mime.includes('rar') || mime.includes('7z')) { return 'archive'; } } } return 'unknown'; } /** * Генерирует уникальное имя файла */ generateUniqueFilename(originalName, mediaType) { const ext = path.extname(originalName); const timestamp = Date.now(); const random = crypto.randomBytes(4).toString('hex'); return `${mediaType}_${timestamp}_${random}${ext}`; } /** * Обрабатывает текстовое сообщение */ async processText(text, metadata = {}) { return { type: 'text', content: text, processed: true, metadata: { language: metadata.language || 'ru', length: text.length, ...metadata } }; } /** * Обрабатывает аудио файл */ async processAudio(audioData, filename, metadata = {}) { try { const mediaType = 'audio'; const uniqueFilename = this.generateUniqueFilename(filename, mediaType); const filePath = path.join(this.uploadPath, mediaType, uniqueFilename); // Сохраняем файл await fs.writeFile(filePath, audioData); // Получаем информацию о файле const stats = await fs.stat(filePath); return { type: 'audio', content: `[Аудио сообщение: ${filename}]`, processed: true, file: { originalName: filename, savedName: uniqueFilename, path: filePath, size: stats.size, url: `/uploads/audio/${uniqueFilename}` }, metadata: { duration: metadata.duration || null, format: path.extname(filename).toLowerCase(), ...metadata } }; } catch (error) { logger.error('[UniversalMediaProcessor] Ошибка обработки аудио:', error); return { type: 'audio', content: `[Ошибка обработки аудио: ${filename}]`, processed: false, error: error.message }; } } /** * Обрабатывает видео файл */ async processVideo(videoData, filename, metadata = {}) { try { const mediaType = 'video'; const uniqueFilename = this.generateUniqueFilename(filename, mediaType); const filePath = path.join(this.uploadPath, mediaType, uniqueFilename); // Проверяем размер файла (видео до 10MB) if (videoData.length > this.maxFileSize) { throw new Error(`Видео файл слишком большой: ${(videoData.length / 1024 / 1024).toFixed(2)}MB. Максимум: ${this.maxFileSize / 1024 / 1024}MB`); } await fs.writeFile(filePath, videoData); const stats = await fs.stat(filePath); return { type: 'video', content: `[Видео сообщение: ${filename}]`, processed: true, file: { originalName: filename, savedName: uniqueFilename, path: filePath, size: stats.size, url: `/uploads/video/${uniqueFilename}` }, metadata: { duration: metadata.duration || null, format: path.extname(filename).toLowerCase(), ...metadata } }; } catch (error) { logger.error('[UniversalMediaProcessor] Ошибка обработки видео:', error); return { type: 'video', content: `[Ошибка обработки видео: ${filename}]`, processed: false, error: error.message }; } } /** * Обрабатывает изображение */ async processImage(imageData, filename, metadata = {}) { try { const mediaType = 'image'; const uniqueFilename = this.generateUniqueFilename(filename, mediaType); const filePath = path.join(this.uploadPath, mediaType, uniqueFilename); // Проверяем размер изображения (до 5MB) if (imageData.length > this.maxImageSize) { throw new Error(`Изображение слишком большое: ${(imageData.length / 1024 / 1024).toFixed(2)}MB. Максимум: ${this.maxImageSize / 1024 / 1024}MB`); } await fs.writeFile(filePath, imageData); const stats = await fs.stat(filePath); return { type: 'image', content: `[Изображение: ${filename}]`, processed: true, file: { originalName: filename, savedName: uniqueFilename, path: filePath, size: stats.size, url: `/uploads/images/${uniqueFilename}` }, metadata: { width: metadata.width || null, height: metadata.height || null, format: path.extname(filename).toLowerCase(), ...metadata } }; } catch (error) { logger.error('[UniversalMediaProcessor] Ошибка обработки изображения:', error); return { type: 'image', content: `[Ошибка обработки изображения: ${filename}]`, processed: false, error: error.message }; } } /** * Обрабатывает документ */ async processDocument(docData, filename, metadata = {}) { try { const mediaType = 'document'; const uniqueFilename = this.generateUniqueFilename(filename, mediaType); const filePath = path.join(this.uploadPath, mediaType, uniqueFilename); await fs.writeFile(filePath, docData); const stats = await fs.stat(filePath); return { type: 'document', content: `[Документ: ${filename}]`, processed: true, file: { originalName: filename, savedName: uniqueFilename, path: filePath, size: stats.size, url: `/uploads/documents/${uniqueFilename}` }, metadata: { format: path.extname(filename).toLowerCase(), ...metadata } }; } catch (error) { logger.error('[UniversalMediaProcessor] Ошибка обработки документа:', error); return { type: 'document', content: `[Ошибка обработки документа: ${filename}]`, processed: false, error: error.message }; } } /** * Обрабатывает файл (автоопределение типа) */ async processFile(fileData, filename, metadata = {}) { const mediaType = this.getMediaType(filename, metadata.mimeType); switch (mediaType) { case 'audio': return await this.processAudio(fileData, filename, metadata); case 'video': return await this.processVideo(fileData, filename, metadata); case 'image': return await this.processImage(fileData, filename, metadata); case 'document': return await this.processDocument(fileData, filename, metadata); case 'archive': return await this.processArchive(fileData, filename, metadata); default: return { type: 'file', content: `[Неизвестный файл: ${filename}]`, processed: false, error: 'Неподдерживаемый формат файла' }; } } /** * Обрабатывает архив */ async processArchive(archiveData, filename, metadata = {}) { try { const mediaType = 'archive'; const uniqueFilename = this.generateUniqueFilename(filename, mediaType); const filePath = path.join(this.uploadPath, mediaType, uniqueFilename); // Проверяем размер архива (до 10MB) if (archiveData.length > this.maxFileSize) { throw new Error(`Архив слишком большой: ${(archiveData.length / 1024 / 1024).toFixed(2)}MB. Максимум: ${this.maxFileSize / 1024 / 1024}MB`); } await fs.writeFile(filePath, archiveData); const stats = await fs.stat(filePath); return { type: 'archive', content: `[Архив: ${filename}]`, processed: true, file: { originalName: filename, savedName: uniqueFilename, path: filePath, size: stats.size, url: `/uploads/archives/${uniqueFilename}` }, metadata: { format: path.extname(filename).toLowerCase(), ...metadata } }; } catch (error) { logger.error('[UniversalMediaProcessor] Ошибка обработки архива:', error); return { type: 'archive', content: `[Ошибка обработки архива: ${filename}]`, processed: false, error: error.message }; } } /** * Обрабатывает комбинированный контент (текст + медиа) */ async processCombinedContent(contentData) { const results = []; // Обрабатываем текст если есть if (contentData.text && contentData.text.trim()) { const textResult = await this.processText(contentData.text, contentData.textMetadata); results.push(textResult); } // Обрабатываем файлы если есть if (contentData.files && contentData.files.length > 0) { for (const file of contentData.files) { const fileResult = await this.processFile(file.data, file.filename, file.metadata); results.push(fileResult); } } // Обрабатываем аудио если есть if (contentData.audio) { const audioResult = await this.processAudio( contentData.audio.data, contentData.audio.filename, contentData.audio.metadata ); results.push(audioResult); } // Обрабатываем видео если есть if (contentData.video) { const videoResult = await this.processVideo( contentData.video.data, contentData.video.filename, contentData.video.metadata ); results.push(videoResult); } return { type: 'combined', parts: results, processed: results.every(r => r.processed), summary: this.generateContentSummary(results) }; } /** * Генерирует краткое описание комбинированного контента */ generateContentSummary(parts) { const summary = []; parts.forEach(part => { switch (part.type) { case 'text': summary.push(`Текст (${part.metadata.length} символов)`); break; case 'audio': summary.push(`Аудио: ${part.file.originalName}`); break; case 'video': summary.push(`Видео: ${part.file.originalName}`); break; case 'image': summary.push(`Изображение: ${part.file.originalName}`); break; case 'document': summary.push(`Документ: ${part.file.originalName}`); break; case 'archive': summary.push(`Архив: ${part.file.originalName}`); break; } }); return summary.join(', '); } /** * Создает структуру для сохранения в БД */ createDatabaseRecord(processedContent, identifier, channel) { const baseRecord = { identifier, channel, timestamp: new Date(), processed: processedContent.processed }; if (processedContent.type === 'text') { return { ...baseRecord, content: processedContent.content, content_type: 'text', attachments: null, metadata: processedContent.metadata }; } if (processedContent.type === 'combined') { // Для комбинированного контента сохраняем как JSON return { ...baseRecord, content: processedContent.summary, content_type: 'combined', attachments: JSON.stringify(processedContent.parts), metadata: { partsCount: processedContent.parts.length, hasText: processedContent.parts.some(p => p.type === 'text'), hasMedia: processedContent.parts.some(p => p.type !== 'text') } }; } // Для отдельных медиа файлов return { ...baseRecord, content: processedContent.content, content_type: processedContent.type, attachments: JSON.stringify(processedContent.file), metadata: processedContent.metadata }; } /** * Восстанавливает структуру из БД */ restoreFromDatabase(dbRecord) { if (dbRecord.content_type === 'text') { return { type: 'text', content: dbRecord.content, metadata: dbRecord.metadata }; } if (dbRecord.content_type === 'combined') { return { type: 'combined', parts: JSON.parse(dbRecord.attachments || '[]'), summary: dbRecord.content, metadata: dbRecord.metadata }; } // Для медиа файлов return { type: dbRecord.content_type, content: dbRecord.content, file: JSON.parse(dbRecord.attachments || '{}'), metadata: dbRecord.metadata }; } } module.exports = new UniversalMediaProcessor();