Spaces:
Paused
Paused
| import { fileURLToPath } from 'url'; | |
| import { v4 as uuidv4 } from 'uuid'; | |
| import crypto from 'crypto'; | |
| import path from 'path'; | |
| import fs from 'fs/promises'; | |
| const __filename = fileURLToPath(import.meta.url); | |
| const __dirname = path.dirname(__filename); | |
| /** | |
| * Huggingface存储服务 | |
| * 管理PPT图片文件的内存存储、链接生成和版本控制 | |
| * 利用Huggingface Space的16G内存进行临时存储 | |
| */ | |
| class HuggingfaceStorageService { | |
| constructor() { | |
| // 文件系统路径配置 | |
| this.usersDir = path.join(__dirname, '../../data/users'); | |
| // 内存存储配置 | |
| this.memoryStorage = new Map(); // 存储图片Buffer数据 | |
| this.linkMap = new Map(); // 图片链接映射 | |
| this.metadataMap = new Map(); // 图片元数据映射 | |
| this.userDataMap = new Map(); // 用户数据映射 | |
| // 内存管理配置 | |
| this.maxMemoryUsage = 14 * 1024 * 1024 * 1024; // 14GB 最大内存使用量 | |
| this.currentMemoryUsage = 0; | |
| this.cleanupThreshold = 0.8; // 80%时开始清理 | |
| // 缓存策略配置 | |
| this.maxImageAge = 24 * 60 * 60 * 1000; // 24小时过期 | |
| this.maxImagesPerUser = 100; // 每用户最大图片数 | |
| this.initialized = false; | |
| // 定期清理内存 | |
| this.startMemoryCleanup(); | |
| } | |
| /** | |
| * 初始化内存存储服务 | |
| */ | |
| async initialize() { | |
| if (this.initialized) return; | |
| try { | |
| // 初始化内存存储 | |
| this.memoryStorage.clear(); | |
| this.linkMap.clear(); | |
| this.metadataMap.clear(); | |
| this.userDataMap.clear(); | |
| this.currentMemoryUsage = 0; | |
| this.initialized = true; | |
| console.log('✅ HuggingfaceStorageService (Memory Mode) initialized successfully'); | |
| console.log(`💾 Max memory usage: ${(this.maxMemoryUsage / 1024 / 1024 / 1024).toFixed(1)}GB`); | |
| console.log(`🧹 Cleanup threshold: ${(this.cleanupThreshold * 100)}%`); | |
| console.log(`⏰ Image expiry: ${this.maxImageAge / 1000 / 60 / 60}h`); | |
| } catch (error) { | |
| console.error('❌ Failed to initialize HuggingfaceStorageService:', error); | |
| throw error; | |
| } | |
| } | |
| /** | |
| * 启动内存清理定时器 | |
| */ | |
| startMemoryCleanup() { | |
| // 每5分钟检查一次内存使用情况 | |
| setInterval(() => { | |
| this.performMemoryCleanup(); | |
| }, 5 * 60 * 1000); | |
| console.log('🧹 Memory cleanup scheduler started (every 5 minutes)'); | |
| } | |
| /** | |
| * 执行内存清理 | |
| */ | |
| performMemoryCleanup() { | |
| const memoryUsageRatio = this.currentMemoryUsage / this.maxMemoryUsage; | |
| if (memoryUsageRatio > this.cleanupThreshold) { | |
| console.log(`🧹 Memory cleanup triggered (${(memoryUsageRatio * 100).toFixed(1)}% usage)`); | |
| // 清理过期图片 | |
| this.cleanupExpiredImages(); | |
| // 如果内存使用仍然过高,清理最旧的图片 | |
| if (this.currentMemoryUsage / this.maxMemoryUsage > this.cleanupThreshold) { | |
| this.cleanupOldestImages(); | |
| } | |
| } | |
| } | |
| /** | |
| * 清理过期图片 | |
| */ | |
| cleanupExpiredImages() { | |
| const now = Date.now(); | |
| let cleanedCount = 0; | |
| let freedMemory = 0; | |
| for (const [imageId, metadata] of this.metadataMap.entries()) { | |
| const imageAge = now - new Date(metadata.createdAt).getTime(); | |
| if (imageAge > this.maxImageAge) { | |
| const imageData = this.memoryStorage.get(imageId); | |
| if (imageData) { | |
| freedMemory += imageData.length; | |
| this.memoryStorage.delete(imageId); | |
| this.metadataMap.delete(imageId); | |
| this.linkMap.delete(imageId); | |
| cleanedCount++; | |
| } | |
| } | |
| } | |
| this.currentMemoryUsage -= freedMemory; | |
| if (cleanedCount > 0) { | |
| console.log(`🧹 Cleaned ${cleanedCount} expired images, freed ${(freedMemory / 1024 / 1024).toFixed(1)}MB`); | |
| } | |
| } | |
| /** | |
| * 清理最旧的图片 | |
| */ | |
| cleanupOldestImages() { | |
| const images = Array.from(this.metadataMap.entries()) | |
| .sort((a, b) => new Date(a[1].createdAt) - new Date(b[1].createdAt)); | |
| const targetCleanup = Math.floor(images.length * 0.2); // 清理20%最旧的图片 | |
| let cleanedCount = 0; | |
| let freedMemory = 0; | |
| for (let i = 0; i < targetCleanup && i < images.length; i++) { | |
| const [imageId, metadata] = images[i]; | |
| const imageData = this.memoryStorage.get(imageId); | |
| if (imageData) { | |
| freedMemory += imageData.length; | |
| this.memoryStorage.delete(imageId); | |
| this.metadataMap.delete(imageId); | |
| this.linkMap.delete(imageId); | |
| cleanedCount++; | |
| } | |
| } | |
| this.currentMemoryUsage -= freedMemory; | |
| if (cleanedCount > 0) { | |
| console.log(`🧹 Cleaned ${cleanedCount} oldest images, freed ${(freedMemory / 1024 / 1024).toFixed(1)}MB`); | |
| } | |
| } | |
| /** | |
| * 生成图片ID | |
| * @param {string} userId - 用户ID | |
| * @param {string} pptId - PPT ID | |
| * @param {number} pageIndex - 页面索引 | |
| * @param {string} version - 版本号 | |
| * @returns {string} 图片ID | |
| */ | |
| generateImageId(userId, pptId, pageIndex, version = 'latest') { | |
| const data = `${userId}-${pptId}-${pageIndex}-${version}`; | |
| return crypto.createHash('sha256').update(data).digest('hex').substring(0, 16); | |
| } | |
| /** | |
| * 生成版本号 | |
| * @returns {string} 版本号 | |
| */ | |
| generateVersion() { | |
| return Date.now().toString(); | |
| } | |
| /** | |
| * 存储图片到内存 | |
| * @param {string} userId - 用户ID | |
| * @param {string} pptId - PPT ID | |
| * @param {number} pageIndex - 页面索引 | |
| * @param {Buffer} imageBuffer - 图片数据 | |
| * @param {Object} options - 存储选项 | |
| * @returns {Promise<Object>} 存储结果 | |
| */ | |
| async storeImage(userId, pptId, pageIndex, imageBuffer, options = {}) { | |
| if (!this.initialized) { | |
| await this.initialize(); | |
| } | |
| const { | |
| format = 'png', | |
| quality = 0.9, | |
| updateExisting = true | |
| } = options; | |
| try { | |
| // 检查内存使用情况 | |
| if (this.currentMemoryUsage + imageBuffer.length > this.maxMemoryUsage) { | |
| console.log('⚠️ Memory limit approaching, performing cleanup...'); | |
| this.performMemoryCleanup(); | |
| // 如果清理后仍然超出限制,拒绝存储 | |
| if (this.currentMemoryUsage + imageBuffer.length > this.maxMemoryUsage) { | |
| throw new Error('Memory limit exceeded, cannot store image'); | |
| } | |
| } | |
| // 检查用户图片数量限制 | |
| const userImages = Array.from(this.metadataMap.values()) | |
| .filter(meta => meta.userId === userId); | |
| if (userImages.length >= this.maxImagesPerUser) { | |
| // 删除用户最旧的图片 | |
| const oldestImage = userImages | |
| .sort((a, b) => new Date(a.createdAt) - new Date(b.createdAt))[0]; | |
| if (oldestImage) { | |
| this.deleteImageFromMemory(oldestImage.imageId); | |
| } | |
| } | |
| // 生成版本号 | |
| const version = this.generateVersion(); | |
| // 生成图片ID | |
| const imageId = this.generateImageId(userId, pptId, pageIndex, 'latest'); | |
| const versionedImageId = this.generateImageId(userId, pptId, pageIndex, version); | |
| // 如果已存在相同的图片ID,先删除旧的 | |
| if (this.memoryStorage.has(imageId)) { | |
| this.deleteImageFromMemory(imageId); | |
| } | |
| // 存储图片到内存 | |
| this.memoryStorage.set(imageId, imageBuffer); | |
| this.memoryStorage.set(versionedImageId, imageBuffer); | |
| // 更新内存使用量 | |
| this.currentMemoryUsage += imageBuffer.length * 2; // 存储了两份(latest和versioned) | |
| // 创建元数据 | |
| const metadata = { | |
| imageId, | |
| versionedImageId, | |
| userId, | |
| pptId, | |
| pageIndex, | |
| version, | |
| format, | |
| createdAt: new Date().toISOString(), | |
| updatedAt: new Date().toISOString(), | |
| size: imageBuffer.length, | |
| memoryStored: true | |
| }; | |
| // 存储元数据 | |
| this.metadataMap.set(imageId, metadata); | |
| this.metadataMap.set(versionedImageId, { ...metadata, imageId: versionedImageId }); | |
| // 更新链接映射 | |
| this.linkMap.set(imageId, metadata); | |
| this.linkMap.set(versionedImageId, { ...metadata, imageId: versionedImageId }); | |
| // 更新用户数据统计 | |
| if (!this.userDataMap.has(userId)) { | |
| this.userDataMap.set(userId, { imageCount: 0, totalSize: 0 }); | |
| } | |
| const userData = this.userDataMap.get(userId); | |
| userData.imageCount++; | |
| userData.totalSize += imageBuffer.length; | |
| console.log(`✅ Image stored in memory: ${imageId} (${imageBuffer.length} bytes)`); | |
| console.log(`💾 Memory usage: ${(this.currentMemoryUsage / 1024 / 1024).toFixed(1)}MB / ${(this.maxMemoryUsage / 1024 / 1024 / 1024).toFixed(1)}GB`); | |
| return { | |
| success: true, | |
| imageId, | |
| versionedImageId, | |
| version, | |
| url: `/api/images/${imageId}`, | |
| versionedUrl: `/api/images/${versionedImageId}`, | |
| memoryStored: true, | |
| size: imageBuffer.length, | |
| memoryUsage: { | |
| current: this.currentMemoryUsage, | |
| max: this.maxMemoryUsage, | |
| percentage: (this.currentMemoryUsage / this.maxMemoryUsage * 100).toFixed(1) | |
| } | |
| }; | |
| } catch (error) { | |
| console.error('❌ Failed to store image in memory:', error); | |
| throw error; | |
| } | |
| } | |
| /** | |
| * 从内存中删除图片 | |
| * @param {string} imageId - 图片ID | |
| */ | |
| deleteImageFromMemory(imageId) { | |
| const imageData = this.memoryStorage.get(imageId); | |
| if (imageData) { | |
| this.currentMemoryUsage -= imageData.length; | |
| this.memoryStorage.delete(imageId); | |
| this.metadataMap.delete(imageId); | |
| this.linkMap.delete(imageId); | |
| console.log(`🗑️ Deleted image from memory: ${imageId} (freed ${imageData.length} bytes)`); | |
| } | |
| } | |
| /** | |
| * 从内存中获取图片 | |
| * @param {string} imageId - 图片ID | |
| * @returns {Promise<Object>} 图片数据和元信息 | |
| */ | |
| async getImage(imageId) { | |
| if (!this.initialized) { | |
| await this.initialize(); | |
| } | |
| // 从内存中获取图片数据 | |
| const imageBuffer = this.memoryStorage.get(imageId); | |
| if (!imageBuffer) { | |
| throw new Error(`Image not found in memory: ${imageId}`); | |
| } | |
| // 获取元数据 | |
| const metadata = this.metadataMap.get(imageId); | |
| if (!metadata) { | |
| throw new Error(`Image metadata not found: ${imageId}`); | |
| } | |
| try { | |
| return { | |
| success: true, | |
| data: imageBuffer, | |
| metadata: { | |
| imageId, | |
| format: metadata.format, | |
| size: metadata.size, | |
| createdAt: metadata.createdAt, | |
| updatedAt: metadata.updatedAt, | |
| version: metadata.version, | |
| memoryStored: true | |
| } | |
| }; | |
| } catch (error) { | |
| console.error(`❌ Failed to get image ${imageId}:`, error); | |
| throw error; | |
| } | |
| } | |
| /** | |
| * 从内存中删除图片 | |
| * @param {string} imageId - 图片ID | |
| * @returns {Promise<boolean>} 删除结果 | |
| */ | |
| async deleteImage(imageId) { | |
| if (!this.initialized) { | |
| await this.initialize(); | |
| } | |
| const metadata = this.metadataMap.get(imageId); | |
| if (!metadata) { | |
| return false; | |
| } | |
| try { | |
| // 从内存中删除图片数据 | |
| this.deleteImageFromMemory(imageId); | |
| // 如果有版本化的图片ID,也删除它 | |
| if (metadata.versionedImageId && metadata.versionedImageId !== imageId) { | |
| this.deleteImageFromMemory(metadata.versionedImageId); | |
| } | |
| // 更新用户数据统计 | |
| const userData = this.userDataMap.get(metadata.userId); | |
| if (userData) { | |
| userData.imageCount = Math.max(0, userData.imageCount - 1); | |
| userData.totalSize = Math.max(0, userData.totalSize - metadata.size); | |
| } | |
| console.log(`✅ Image deleted from memory: ${imageId}`); | |
| return true; | |
| } catch (error) { | |
| console.error(`❌ Failed to delete image from memory ${imageId}:`, error); | |
| return false; | |
| } | |
| } | |
| /** | |
| * 批量存储PPT所有页面图片 | |
| * @param {string} userId - 用户ID | |
| * @param {string} pptId - PPT ID | |
| * @param {Array} imageBuffers - 图片Buffer数组 | |
| * @param {Object} options - 存储选项 | |
| * @returns {Promise<Array>} 存储结果数组 | |
| */ | |
| async storeAllImages(userId, pptId, imageBuffers, options = {}) { | |
| const results = []; | |
| for (let i = 0; i < imageBuffers.length; i++) { | |
| try { | |
| if (imageBuffers[i].success) { | |
| const result = await this.storeImage( | |
| userId, | |
| pptId, | |
| i, | |
| imageBuffers[i].data, | |
| options | |
| ); | |
| results.push({ | |
| pageIndex: i, | |
| success: true, | |
| ...result | |
| }); | |
| } else { | |
| results.push({ | |
| pageIndex: i, | |
| success: false, | |
| error: imageBuffers[i].error | |
| }); | |
| } | |
| } catch (error) { | |
| results.push({ | |
| pageIndex: i, | |
| success: false, | |
| error: error.message | |
| }); | |
| } | |
| } | |
| return results; | |
| } | |
| /** | |
| * 存储PPT数据到Huggingface硬盘 | |
| * @param {string} userId - 用户ID | |
| * @param {string} pptId - PPT ID | |
| * @param {Object} pptData - PPT数据 | |
| * @returns {Promise<Object>} 存储结果 | |
| */ | |
| async storePPTData(userId, pptId, pptData) { | |
| if (!this.initialized) { | |
| await this.initialize(); | |
| } | |
| try { | |
| const userDir = path.join(this.usersDir, userId); | |
| const pptDir = path.join(userDir, pptId); | |
| // 确保目录存在 | |
| await fs.mkdir(pptDir, { recursive: true }); | |
| // 存储PPT数据 | |
| const pptDataFile = path.join(pptDir, 'data.json'); | |
| await fs.writeFile(pptDataFile, JSON.stringify(pptData, null, 2)); | |
| // 存储元数据 | |
| const metadata = { | |
| pptId, | |
| userId, | |
| title: pptData.title || '未命名演示文稿', | |
| slidesCount: pptData.slides?.length || 0, | |
| createdAt: new Date().toISOString(), | |
| updatedAt: new Date().toISOString(), | |
| storageType: 'huggingface', | |
| size: JSON.stringify(pptData).length | |
| }; | |
| const metaFile = path.join(pptDir, 'meta.json'); | |
| await fs.writeFile(metaFile, JSON.stringify(metadata, null, 2)); | |
| console.log(`✅ PPT data stored to Huggingface: ${pptId} for user ${userId}`); | |
| return { | |
| success: true, | |
| pptId, | |
| metadata | |
| }; | |
| } catch (error) { | |
| console.error(`❌ Failed to store PPT data ${pptId}:`, error); | |
| throw error; | |
| } | |
| } | |
| /** | |
| * 从Huggingface硬盘获取PPT数据 | |
| * @param {string} userId - 用户ID | |
| * @param {string} pptId - PPT ID | |
| * @returns {Promise<Object>} PPT数据 | |
| */ | |
| async getPPTData(userId, pptId) { | |
| if (!this.initialized) { | |
| await this.initialize(); | |
| } | |
| try { | |
| const pptDataFile = path.join(this.usersDir, userId, pptId, 'data.json'); | |
| const data = await fs.readFile(pptDataFile, 'utf-8'); | |
| const pptData = JSON.parse(data); | |
| console.log(`✅ PPT data loaded from Huggingface: ${pptId} for user ${userId}`); | |
| return pptData; | |
| } catch (error) { | |
| console.error(`❌ Failed to get PPT data ${pptId}:`, error); | |
| throw error; | |
| } | |
| } | |
| /** | |
| * 获取用户的PPT列表 | |
| * @param {string} userId - 用户ID | |
| * @returns {Promise<Array>} PPT列表 | |
| */ | |
| async getUserPPTList(userId) { | |
| if (!this.initialized) { | |
| await this.initialize(); | |
| } | |
| try { | |
| const userDir = path.join(this.usersDir, userId); | |
| // 检查用户目录是否存在 | |
| try { | |
| await fs.access(userDir); | |
| } catch { | |
| console.log(`📁 No PPTs found for user ${userId} in Huggingface storage`); | |
| return []; | |
| } | |
| const pptFolders = await fs.readdir(userDir, { withFileTypes: true }); | |
| const pptList = []; | |
| for (const folder of pptFolders) { | |
| if (folder.isDirectory()) { | |
| try { | |
| const metaFile = path.join(userDir, folder.name, 'meta.json'); | |
| const metaData = await fs.readFile(metaFile, 'utf-8'); | |
| const metadata = JSON.parse(metaData); | |
| pptList.push({ | |
| name: folder.name, | |
| title: metadata.title || '未命名演示文稿', | |
| updatedAt: metadata.updatedAt || new Date().toISOString(), | |
| slidesCount: metadata.slidesCount || 0, | |
| storageType: 'huggingface', | |
| size: metadata.size || 0, | |
| repoUrl: 'Huggingface Storage' | |
| }); | |
| } catch (error) { | |
| console.warn(`⚠️ Skipping invalid PPT folder ${folder.name}:`, error.message); | |
| } | |
| } | |
| } | |
| console.log(`📁 Found ${pptList.length} PPTs in Huggingface storage for user ${userId}`); | |
| return pptList.sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt)); | |
| } catch (error) { | |
| console.error(`❌ Failed to get PPT list for user ${userId}:`, error); | |
| throw error; | |
| } | |
| } | |
| /** | |
| * 删除PPT数据 | |
| * @param {string} userId - 用户ID | |
| * @param {string} pptId - PPT ID | |
| * @returns {Promise<boolean>} 删除结果 | |
| */ | |
| async deletePPTData(userId, pptId) { | |
| if (!this.initialized) { | |
| await this.initialize(); | |
| } | |
| try { | |
| const pptDir = path.join(this.usersDir, userId, pptId); | |
| // 递归删除PPT目录 | |
| await fs.rm(pptDir, { recursive: true, force: true }); | |
| console.log(`✅ PPT data deleted from Huggingface: ${pptId} for user ${userId}`); | |
| return true; | |
| } catch (error) { | |
| console.error(`❌ Failed to delete PPT data ${pptId}:`, error); | |
| return false; | |
| } | |
| } | |
| /** | |
| * 获取用户的所有图片 | |
| * @param {string} userId - 用户ID | |
| * @param {string} pptId - PPT ID (可选) | |
| * @returns {Promise<Array>} 图片列表 | |
| */ | |
| async getUserImages(userId, pptId = null) { | |
| if (!this.initialized) { | |
| await this.initialize(); | |
| } | |
| const userImages = []; | |
| for (const [imageId, linkInfo] of this.linkMap.entries()) { | |
| if (linkInfo.userId === userId) { | |
| if (!pptId || linkInfo.pptId === pptId) { | |
| userImages.push({ | |
| imageId, | |
| pptId: linkInfo.pptId, | |
| pageIndex: linkInfo.pageIndex, | |
| version: linkInfo.version, | |
| format: linkInfo.format, | |
| size: linkInfo.size, | |
| url: `/api/images/${imageId}`, | |
| createdAt: linkInfo.createdAt, | |
| updatedAt: linkInfo.updatedAt | |
| }); | |
| } | |
| } | |
| } | |
| return userImages.sort((a, b) => { | |
| if (a.pptId !== b.pptId) { | |
| return a.pptId.localeCompare(b.pptId); | |
| } | |
| return a.pageIndex - b.pageIndex; | |
| }); | |
| } | |
| /** | |
| * 清理过期图片 | |
| * @param {number} maxAge - 最大保留时间(毫秒) | |
| * @returns {Promise<number>} 清理的图片数量 | |
| */ | |
| async cleanupExpiredImages(maxAge = 30 * 24 * 60 * 60 * 1000) { // 默认30天 | |
| if (!this.initialized) { | |
| await this.initialize(); | |
| } | |
| const now = Date.now(); | |
| let cleanedCount = 0; | |
| for (const [imageId, linkInfo] of this.linkMap.entries()) { | |
| const createdAt = new Date(linkInfo.createdAt).getTime(); | |
| if (now - createdAt > maxAge) { | |
| await this.deleteImage(imageId); | |
| cleanedCount++; | |
| } | |
| } | |
| console.log(`🧹 Cleaned up ${cleanedCount} expired images`); | |
| return cleanedCount; | |
| } | |
| /** | |
| * 获取存储统计信息 | |
| * @returns {Promise<Object>} 统计信息 | |
| */ | |
| async getStorageStats() { | |
| if (!this.initialized) { | |
| await this.initialize(); | |
| } | |
| let totalSize = 0; | |
| let totalImages = 0; | |
| const userStats = {}; | |
| // 从内存存储中统计 | |
| for (const [imageId, metadata] of this.metadataMap.entries()) { | |
| totalSize += metadata.size || 0; | |
| totalImages++; | |
| if (!userStats[metadata.userId]) { | |
| userStats[metadata.userId] = { | |
| imageCount: 0, | |
| totalSize: 0, | |
| ppts: new Set() | |
| }; | |
| } | |
| userStats[metadata.userId].imageCount++; | |
| userStats[metadata.userId].totalSize += metadata.size || 0; | |
| userStats[metadata.userId].ppts.add(metadata.pptId); | |
| } | |
| // 转换Set为数组 | |
| for (const userId in userStats) { | |
| userStats[userId].pptCount = userStats[userId].ppts.size; | |
| delete userStats[userId].ppts; | |
| } | |
| return { | |
| totalImages, | |
| totalSize, | |
| totalSizeMB: Math.round(totalSize / 1024 / 1024 * 100) / 100, | |
| userCount: Object.keys(userStats).length, | |
| userStats, | |
| memoryUsage: { | |
| current: this.currentMemoryUsage, | |
| max: this.maxMemoryUsage, | |
| currentMB: Math.round(this.currentMemoryUsage / 1024 / 1024 * 100) / 100, | |
| maxGB: Math.round(this.maxMemoryUsage / 1024 / 1024 / 1024 * 100) / 100, | |
| percentage: Math.round(this.currentMemoryUsage / this.maxMemoryUsage * 100 * 100) / 100, | |
| imagesInMemory: this.memoryStorage.size | |
| } | |
| }; | |
| } | |
| /** | |
| * 获取内存使用情况 | |
| * @returns {Object} 内存使用统计 | |
| */ | |
| getMemoryUsage() { | |
| return { | |
| current: this.currentMemoryUsage, | |
| max: this.maxMemoryUsage, | |
| currentMB: Math.round(this.currentMemoryUsage / 1024 / 1024 * 100) / 100, | |
| maxGB: Math.round(this.maxMemoryUsage / 1024 / 1024 / 1024 * 100) / 100, | |
| percentage: Math.round(this.currentMemoryUsage / this.maxMemoryUsage * 100 * 100) / 100, | |
| imagesInMemory: this.memoryStorage.size, | |
| metadataCount: this.metadataMap.size, | |
| userCount: this.userDataMap.size | |
| }; | |
| } | |
| /** | |
| * 清空所有内存数据 | |
| */ | |
| clearAllMemory() { | |
| this.memoryStorage.clear(); | |
| this.metadataMap.clear(); | |
| this.linkMap.clear(); | |
| this.userDataMap.clear(); | |
| this.currentMemoryUsage = 0; | |
| console.log('🧹 All memory data cleared'); | |
| } | |
| /** | |
| * 健康检查 | |
| */ | |
| async healthCheck() { | |
| try { | |
| if (!this.initialized) { | |
| return { | |
| status: 'unhealthy', | |
| message: 'Service not initialized', | |
| details: { | |
| initialized: false, | |
| memoryUsage: 0, | |
| imagesCount: 0 | |
| } | |
| }; | |
| } | |
| // 检查内存使用情况 | |
| const memoryUsage = this.getMemoryUsage(); | |
| const isMemoryHealthy = memoryUsage.percentage < 90; // 90%以下认为健康 | |
| // 检查数据目录是否可访问 | |
| let dirAccessible = true; | |
| try { | |
| await fs.access(this.usersDir); | |
| } catch (error) { | |
| dirAccessible = false; | |
| } | |
| const isHealthy = isMemoryHealthy && dirAccessible; | |
| return { | |
| status: isHealthy ? 'healthy' : 'unhealthy', | |
| message: isHealthy | |
| ? 'Huggingface Storage Service is running normally' | |
| : 'Service has issues', | |
| details: { | |
| initialized: this.initialized, | |
| memoryUsage: memoryUsage, | |
| dirAccessible: dirAccessible, | |
| usersDir: this.usersDir, | |
| isMemoryHealthy: isMemoryHealthy, | |
| maxMemoryUsageGB: Math.round(this.maxMemoryUsage / 1024 / 1024 / 1024 * 100) / 100, | |
| cleanupThreshold: this.cleanupThreshold | |
| } | |
| }; | |
| } catch (error) { | |
| return { | |
| status: 'unhealthy', | |
| message: 'Health check failed', | |
| details: { | |
| error: error.message, | |
| stack: error.stack | |
| } | |
| }; | |
| } | |
| } | |
| /** | |
| * 获取用户的图片列表 | |
| * @param {string} userId - 用户ID | |
| * @returns {Array} 用户的图片列表 | |
| */ | |
| getUserImageList(userId) { | |
| const userImages = []; | |
| for (const [imageId, metadata] of this.metadataMap.entries()) { | |
| if (metadata.userId === userId) { | |
| userImages.push({ | |
| imageId, | |
| pptId: metadata.pptId, | |
| pageIndex: metadata.pageIndex, | |
| format: metadata.format, | |
| size: metadata.size, | |
| createdAt: metadata.createdAt, | |
| version: metadata.version | |
| }); | |
| } | |
| } | |
| return userImages; | |
| } | |
| /** | |
| * 获取PPT的所有图片 | |
| * @param {string} userId - 用户ID | |
| * @param {string} pptId - PPT ID | |
| * @returns {Array} PPT的图片列表 | |
| */ | |
| getPPTImages(userId, pptId) { | |
| const pptImages = []; | |
| for (const [imageId, metadata] of this.metadataMap.entries()) { | |
| if (metadata.userId === userId && metadata.pptId === pptId) { | |
| pptImages.push({ | |
| imageId, | |
| pageIndex: metadata.pageIndex, | |
| format: metadata.format, | |
| size: metadata.size, | |
| createdAt: metadata.createdAt, | |
| version: metadata.version, | |
| url: `/api/images/${imageId}` | |
| }); | |
| } | |
| } | |
| return pptImages.sort((a, b) => a.pageIndex - b.pageIndex); | |
| } | |
| /** | |
| * 公开访问:根据用户ID、PPT ID和页面索引获取图片 | |
| * @param {string} userId - 用户ID | |
| * @param {string} pptId - PPT ID | |
| * @param {number} pageIndex - 页面索引 | |
| * @returns {Promise<Object>} 图片数据和元信息 | |
| */ | |
| async getPublicImage(userId, pptId, pageIndex) { | |
| if (!this.initialized) { | |
| await this.initialize(); | |
| } | |
| // 生成图片ID | |
| const imageId = this.generateImageId(userId, pptId, pageIndex, 'latest'); | |
| // 从内存中获取图片数据 | |
| const imageBuffer = this.memoryStorage.get(imageId); | |
| if (!imageBuffer) { | |
| throw new Error(`Public image not found: ${userId}/${pptId}/${pageIndex}`); | |
| } | |
| // 获取元数据 | |
| const metadata = this.metadataMap.get(imageId); | |
| if (!metadata) { | |
| throw new Error(`Public image metadata not found: ${userId}/${pptId}/${pageIndex}`); | |
| } | |
| console.log(`✅ Public image accessed: ${userId}/${pptId}/${pageIndex} (${imageBuffer.length} bytes)`); | |
| return { | |
| success: true, | |
| data: imageBuffer, | |
| metadata: { | |
| imageId, | |
| userId, | |
| pptId, | |
| pageIndex, | |
| format: metadata.format, | |
| size: metadata.size, | |
| createdAt: metadata.createdAt, | |
| updatedAt: metadata.updatedAt, | |
| version: metadata.version, | |
| memoryStored: true | |
| } | |
| }; | |
| } | |
| /** | |
| * 公开访问:检查图片是否存在 | |
| * @param {string} userId - 用户ID | |
| * @param {string} pptId - PPT ID | |
| * @param {number} pageIndex - 页面索引 | |
| * @returns {boolean} 图片是否存在 | |
| */ | |
| hasPublicImage(userId, pptId, pageIndex) { | |
| if (!this.initialized) { | |
| return false; | |
| } | |
| const imageId = this.generateImageId(userId, pptId, pageIndex, 'latest'); | |
| return this.memoryStorage.has(imageId) && this.metadataMap.has(imageId); | |
| } | |
| /** | |
| * 公开访问:获取用户的PPT列表(仅包含有图片的PPT) | |
| * @param {string} userId - 用户ID | |
| * @returns {Array} PPT列表 | |
| */ | |
| getPublicPPTList(userId) { | |
| if (!this.initialized) { | |
| return []; | |
| } | |
| const pptMap = new Map(); | |
| for (const [imageId, metadata] of this.metadataMap.entries()) { | |
| if (metadata.userId === userId) { | |
| if (!pptMap.has(metadata.pptId)) { | |
| pptMap.set(metadata.pptId, { | |
| pptId: metadata.pptId, | |
| userId: metadata.userId, | |
| pageCount: 0, | |
| totalSize: 0, | |
| createdAt: metadata.createdAt, | |
| updatedAt: metadata.updatedAt, | |
| pages: [] | |
| }); | |
| } | |
| const pptInfo = pptMap.get(metadata.pptId); | |
| pptInfo.pageCount++; | |
| pptInfo.totalSize += metadata.size; | |
| pptInfo.pages.push({ | |
| pageIndex: metadata.pageIndex, | |
| format: metadata.format, | |
| size: metadata.size, | |
| url: `/api/public/image/${userId}/${metadata.pptId}/${metadata.pageIndex}` | |
| }); | |
| // 更新最新时间 | |
| if (new Date(metadata.updatedAt) > new Date(pptInfo.updatedAt)) { | |
| pptInfo.updatedAt = metadata.updatedAt; | |
| } | |
| } | |
| } | |
| // 排序页面并返回结果 | |
| const result = Array.from(pptMap.values()); | |
| result.forEach(ppt => { | |
| ppt.pages.sort((a, b) => a.pageIndex - b.pageIndex); | |
| }); | |
| return result.sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt)); | |
| } | |
| } | |
| // 创建单例实例 | |
| const huggingfaceStorageService = new HuggingfaceStorageService(); | |
| export default huggingfaceStorageService; |