import express from 'express'; import { v4 as uuidv4 } from 'uuid'; import githubService from '../services/githubService.js'; const router = express.Router(); // 添加路由级别的日志中间件 router.use((req, res, next) => { console.log(`PPT Router - ${req.method} ${req.path} - Body:`, Object.keys(req.body || {})); next(); }); // 获取用户的PPT列表 router.get('/list', async (req, res, next) => { try { const userId = req.user.userId; const pptList = await githubService.getUserPPTList(userId); res.json(pptList); } catch (error) { next(error); } }); // 获取指定PPT数据 router.get('/:pptId', async (req, res, next) => { try { const userId = req.user.userId; const { pptId } = req.params; console.log(`🔍 Fetching PPT: ${pptId} for user: ${userId}`); let pptData = null; let foundInRepo = -1; if (githubService.useMemoryStorage) { // 内存存储模式 const result = await githubService.getPPTFromMemory(userId, pptId); if (result && result.content) { pptData = result.content; foundInRepo = 0; console.log(`✅ PPT found in memory storage`); } } else { // GitHub存储模式 - 尝试所有仓库 console.log(`Available repositories: ${githubService.repositories.length}`); for (let i = 0; i < githubService.repositories.length; i++) { try { console.log(`📂 Checking repository ${i}: ${githubService.repositories[i]}`); const result = await githubService.getPPT(userId, pptId, i); if (result && result.content) { pptData = result.content; foundInRepo = i; console.log(`✅ PPT found in repository ${i}${result.isReassembled ? ' (reassembled from chunks)' : ''}`); break; } } catch (error) { console.log(`❌ PPT not found in repository ${i}: ${error.message}`); continue; } } } if (!pptData) { console.log(`❌ PPT ${pptId} not found for user ${userId}`); return res.status(404).json({ error: 'PPT not found', pptId: pptId, userId: userId, storageMode: githubService.useMemoryStorage ? 'memory' : 'github' }); } // 🔧 修复:标准化PPT数据格式,确保前端兼容性 const standardizedPptData = { // 确保基本字段存在 id: pptData.id || pptData.pptId || pptId, pptId: pptData.pptId || pptData.id || pptId, title: pptData.title || '未命名演示文稿', // 标准化slides数组 slides: Array.isArray(pptData.slides) ? pptData.slides.map((slide, index) => ({ id: slide.id || `slide-${index}`, elements: Array.isArray(slide.elements) ? slide.elements : [], background: slide.background || { type: 'solid', color: '#ffffff' }, ...slide })) : [], // 标准化主题 theme: pptData.theme || { backgroundColor: '#ffffff', themeColor: '#d14424', fontColor: '#333333', fontName: 'Microsoft YaHei' }, // 🔧 关键修复:确保视口信息正确传递 viewportSize: pptData.viewportSize || 1000, viewportRatio: pptData.viewportRatio || 0.5625, // 时间戳 createdAt: pptData.createdAt || new Date().toISOString(), updatedAt: pptData.updatedAt || new Date().toISOString(), // 保留其他可能的属性 ...pptData }; // 🔧 新增:数据验证和修复 if (standardizedPptData.slides.length === 0) { console.log(`⚠️ PPT ${pptId} has no slides, creating default slide`); standardizedPptData.slides = [{ id: 'default-slide', elements: [], background: { type: 'solid', color: '#ffffff' } }]; } // 验证视口比例的合理性 if (standardizedPptData.viewportRatio <= 0 || standardizedPptData.viewportRatio > 2) { console.log(`⚠️ Invalid viewportRatio ${standardizedPptData.viewportRatio}, resetting to 0.5625`); standardizedPptData.viewportRatio = 0.5625; } // 验证视口尺寸的合理性 if (standardizedPptData.viewportSize <= 0 || standardizedPptData.viewportSize > 2000) { console.log(`⚠️ Invalid viewportSize ${standardizedPptData.viewportSize}, resetting to 1000`); standardizedPptData.viewportSize = 1000; } console.log(`✅ Successfully found and standardized PPT ${pptId}:`, { slidesCount: standardizedPptData.slides.length, viewportSize: standardizedPptData.viewportSize, viewportRatio: standardizedPptData.viewportRatio, storageMode: githubService.useMemoryStorage ? 'memory' : `repository ${foundInRepo}` }); res.json(standardizedPptData); } catch (error) { console.error(`❌ Error fetching PPT ${req.params.pptId}:`, error); next(error); } }); // 保存PPT数据 - 新架构版本 router.post('/save', async (req, res, next) => { try { const userId = req.user.userId; const { pptId, title, slides, theme, viewportSize, viewportRatio } = req.body; console.log(`🔄 Starting PPT save for user ${userId}, PPT ID: ${pptId}`); console.log(`📊 Request body analysis:`, { pptId: !!pptId, title: title?.length || 0, slidesCount: Array.isArray(slides) ? slides.length : 'not array', theme: !!theme, requestSize: req.get('content-length'), storageMode: githubService.useMemoryStorage ? 'memory' : 'github' }); if (!pptId || !slides) { console.log(`❌ Missing required fields - pptId: ${!!pptId}, slides: ${!!slides}`); return res.status(400).json({ error: 'PPT ID and slides are required' }); } // 验证slides数组 if (!Array.isArray(slides) || slides.length === 0) { console.log(`❌ Invalid slides array - isArray: ${Array.isArray(slides)}, length: ${slides?.length || 0}`); return res.status(400).json({ error: 'Slides must be a non-empty array' }); } // 构建标准化的PPT数据结构 const pptData = { id: pptId, title: title || '未命名演示文稿', slides: slides.map((slide, index) => ({ id: slide.id || `slide-${index}`, elements: Array.isArray(slide.elements) ? slide.elements : [], background: slide.background || { type: 'solid', color: '#ffffff' }, ...slide })), theme: theme || { backgroundColor: '#ffffff', themeColor: '#d14424', fontColor: '#333333', fontName: 'Microsoft YaHei' }, viewportSize: viewportSize || 1000, viewportRatio: viewportRatio || 0.5625, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() }; // 计算文件大小分析 const jsonData = JSON.stringify(pptData); const totalDataSize = Buffer.byteLength(jsonData, 'utf8'); console.log(`📊 PPT data analysis:`); console.log(` - Total slides: ${slides.length}`); console.log(` - Total data size: ${totalDataSize} bytes (${(totalDataSize / 1024).toFixed(2)} KB)`); console.log(` - Average slide size: ${(totalDataSize / slides.length / 1024).toFixed(2)} KB`); // 分析每个slide的大小 const slideAnalysis = slides.map((slide, index) => { const slideJson = JSON.stringify(slide); const slideSize = Buffer.byteLength(slideJson, 'utf8'); return { index, size: slideSize, sizeKB: (slideSize / 1024).toFixed(2), elementsCount: slide.elements?.length || 0, hasLargeContent: slideSize > 800 * 1024 // 800KB+ }; }); const largeSlides = slideAnalysis.filter(s => s.hasLargeContent); const maxSlideSize = Math.max(...slideAnalysis.map(s => s.size)); console.log(`📋 Slide size analysis:`); console.log(` - Largest slide: ${(maxSlideSize / 1024).toFixed(2)} KB`); console.log(` - Large slides (>800KB): ${largeSlides.length}`); if (largeSlides.length > 0) { console.log(` - Large slide indices: ${largeSlides.map(s => s.index).join(', ')}`); } try { let result; console.log(`💾 Using storage mode: ${githubService.useMemoryStorage ? 'memory' : 'GitHub folder architecture'}`); if (githubService.useMemoryStorage) { // 内存存储模式 result = await githubService.savePPTToMemory(userId, pptId, pptData); console.log(`✅ PPT saved to memory storage:`, result); } else { // GitHub存储模式 - 使用新的文件夹架构 console.log(`🐙 Using GitHub folder storage for ${pptId}`); console.log(`📂 Available repositories: ${githubService.repositories?.length || 0}`); if (!githubService.repositories || githubService.repositories.length === 0) { throw new Error('No GitHub repositories configured'); } console.log(`🔍 Pre-save validation:`); console.log(` - Repository 0: ${githubService.repositories[0]}`); console.log(` - Token configured: ${!!githubService.token}`); console.log(` - API URL: ${githubService.apiUrl}`); result = await githubService.savePPT(userId, pptId, pptData, 0); console.log(`✅ PPT saved to GitHub folder storage:`, result); } console.log(`✅ PPT save completed successfully:`, { pptId, userId, slideCount: slides.length, totalDataSize, storageType: result.storage || 'unknown', compressionApplied: result.compressionSummary?.compressedSlides > 0, storageMode: githubService.useMemoryStorage ? 'memory' : 'github' }); const response = { message: 'PPT saved successfully', pptId, slidesCount: slides.length, savedAt: new Date().toISOString(), totalFileSize: `${(totalDataSize / 1024).toFixed(2)} KB`, storageType: result.storage || 'unknown', storageMode: githubService.useMemoryStorage ? 'memory' : 'github', architecture: 'folder-based', slideAnalysis: { totalSlides: slides.length, largestSlideSize: `${(maxSlideSize / 1024).toFixed(2)} KB`, largeSlides: largeSlides.length, averageSlideSize: `${(totalDataSize / slides.length / 1024).toFixed(2)} KB` } }; // 添加压缩信息 if (result.compressionSummary) { response.compression = { applied: result.compressionSummary.compressedSlides > 0, compressedSlides: result.compressionSummary.compressedSlides, totalSlides: result.compressionSummary.totalSlides, savedSlides: result.compressionSummary.savedSlides, failedSlides: result.compressionSummary.failedSlides, originalSize: `${(result.compressionSummary.totalOriginalSize / 1024).toFixed(2)} KB`, finalSize: `${(result.compressionSummary.totalFinalSize / 1024).toFixed(2)} KB`, savedSpace: `${((result.compressionSummary.totalOriginalSize - result.compressionSummary.totalFinalSize) / 1024).toFixed(2)} KB`, compressionRatio: `${(result.compressionSummary.compressionRatio * 100).toFixed(1)}%` }; if (result.compressionSummary.compressedSlides > 0) { response.message = `PPT saved successfully with ${result.compressionSummary.compressedSlides} slides optimized for storage`; response.optimization = `Automatic compression applied to ${result.compressionSummary.compressedSlides} large slides, saving ${response.compression.savedSpace}`; } // 添加保存警告信息 if (result.compressionSummary.failedSlides > 0) { response.warning = `${result.compressionSummary.failedSlides} slides failed to save`; response.partialSave = true; response.savedSlides = result.compressionSummary.savedSlides; response.failedSlides = result.compressionSummary.failedSlides; if (result.compressionSummary.errors) { response.slideErrors = result.compressionSummary.errors; } } } // 添加存储路径信息 if (result.folderPath) { response.storagePath = result.folderPath; response.architecture = 'folder-based (meta.json + individual slide files)'; } // 添加警告信息 if (result.warnings) { response.warnings = result.warnings; } res.json(response); } catch (saveError) { console.error(`❌ Save operation failed:`, { error: saveError.message, stack: saveError.stack, userId, pptId, slidesCount: slides.length, totalDataSize, errorName: saveError.name, errorCode: saveError.code }); let errorResponse = { error: 'PPT save failed', details: saveError.message, pptId: pptId, slidesCount: slides.length, totalFileSize: `${(totalDataSize / 1024).toFixed(2)} KB`, timestamp: new Date().toISOString(), slideAnalysis: { largestSlideSize: `${(maxSlideSize / 1024).toFixed(2)} KB`, largeSlides: largeSlides.length } }; // 根据错误类型提供具体建议 if (saveError.message.includes('File too large') || saveError.message.includes('exceeds')) { errorResponse.error = 'Slide file too large for storage'; errorResponse.suggestion = 'Some slides are too large even after compression. Try reducing image sizes or slide complexity.'; errorResponse.limits = { maxSlideSize: '999 KB per slide', largestSlideSize: `${(maxSlideSize / 1024).toFixed(2)} KB`, oversizeSlides: largeSlides.length }; return res.status(413).json(errorResponse); } if (saveError.message.includes('Failed to compress slide')) { errorResponse.error = 'Slide compression failed'; errorResponse.suggestion = 'Unable to compress slides to acceptable size. Try manually reducing content complexity.'; errorResponse.compressionError = true; return res.status(500).json(errorResponse); } if (saveError.message.includes('GitHub API') || saveError.message.includes('GitHub')) { errorResponse.error = 'GitHub storage service error'; errorResponse.suggestion = 'Temporary GitHub service issue. Please try again in a few minutes.'; errorResponse.githubError = true; return res.status(502).json(errorResponse); } if (saveError.message.includes('No GitHub repositories')) { errorResponse.error = 'Storage configuration error'; errorResponse.suggestion = 'GitHub repositories not properly configured. Please contact support.'; errorResponse.configError = true; return res.status(500).json(errorResponse); } // 网络相关错误 if (saveError.code === 'ETIMEDOUT' || saveError.message.includes('timeout')) { errorResponse.error = 'Storage operation timeout'; errorResponse.suggestion = 'The save operation took too long. Try reducing file size or try again later.'; errorResponse.timeoutError = true; return res.status(504).json(errorResponse); } // 默认错误 errorResponse.suggestion = 'Unknown save error. Please try again or contact support if the problem persists.'; errorResponse.unknownError = true; return res.status(500).json(errorResponse); } } catch (error) { console.error(`❌ PPT save failed for user ${req.user?.userId}, PPT ID: ${req.body?.pptId}:`, { error: error.message, stack: error.stack, userId: req.user?.userId, pptId: req.body?.pptId, requestSize: req.get('content-length'), slidesCount: req.body?.slides?.length, errorName: error.name, errorCode: error.code }); // 提供更详细的错误处理 let errorResponse = { error: 'PPT save processing failed', details: error.message, timestamp: new Date().toISOString(), userId: req.user?.userId, pptId: req.body?.pptId, architecture: 'folder-based' }; if (error.message.includes('Invalid slide data')) { errorResponse.error = 'Invalid slide data detected'; errorResponse.suggestion = 'One or more slides contain invalid or corrupted data'; return res.status(400).json(errorResponse); } if (error.message.includes('JSON')) { errorResponse.error = 'Invalid data format'; errorResponse.suggestion = 'Check slide data structure and content'; return res.status(400).json(errorResponse); } errorResponse.suggestion = 'Unknown processing error. Please try again or contact support if the problem persists.'; next(error); } }); // 创建新PPT router.post('/create', async (req, res, next) => { try { const userId = req.user.userId; const { title } = req.body; if (!title) { return res.status(400).json({ error: 'Title is required' }); } const pptId = uuidv4(); const now = new Date().toISOString(); // 确保数据格式与前端store一致 const pptData = { id: pptId, title, theme: { backgroundColor: '#ffffff', themeColor: '#d14424', fontColor: '#333333', fontName: 'Microsoft YaHei' }, slides: [ { id: uuidv4(), elements: [ { type: 'text', id: uuidv4(), left: 150, top: 200, width: 600, height: 100, content: title, fontSize: 32, fontName: 'Microsoft YaHei', defaultColor: '#333333', bold: true, align: 'center' } ], background: { type: 'solid', color: '#ffffff' } } ], // 确保视口信息与前端一致 viewportSize: 1000, viewportRatio: 0.5625, createdAt: now, updatedAt: now }; const fileName = `${pptId}.json`; console.log(`Creating PPT for user ${userId}, using ${githubService.useMemoryStorage ? 'memory' : 'GitHub'} storage`); if (githubService.useMemoryStorage) { await githubService.saveToMemory(userId, fileName, pptData); } else { await githubService.saveFile(userId, fileName, pptData); } console.log(`PPT created successfully: ${pptId}`); res.json({ success: true, pptId, pptData }); } catch (error) { console.error('PPT creation error:', error); next(error); } }); // 删除PPT router.delete('/:pptId', async (req, res, next) => { try { const userId = req.user.userId; const { pptId } = req.params; const fileName = `${pptId}.json`; if (githubService.useMemoryStorage) { // 内存存储模式 const deleted = githubService.memoryStorage.delete(`users/${userId}/${fileName}`); if (!deleted) { return res.status(404).json({ error: 'PPT not found' }); } } else { // GitHub存储模式 - 从所有仓库中删除 let deleted = false; for (let i = 0; i < githubService.repositories.length; i++) { try { await githubService.deleteFile(userId, fileName, i); deleted = true; break; // 只需要从一个仓库删除成功即可 } catch (error) { // 继续尝试其他仓库 continue; } } if (!deleted) { return res.status(404).json({ error: 'PPT not found in any repository' }); } } res.json({ message: 'PPT deleted successfully' }); } catch (error) { next(error); } }); // 复制PPT router.post('/:pptId/copy', async (req, res, next) => { try { const userId = req.user.userId; const { pptId } = req.params; const { title } = req.body; const sourceFileName = `${pptId}.json`; // 获取源PPT数据 let sourcePPT = null; if (githubService.useMemoryStorage) { // 内存存储模式 sourcePPT = await githubService.getFromMemory(userId, sourceFileName); } else { // GitHub存储模式 for (let i = 0; i < githubService.repositories.length; i++) { try { const result = await githubService.getFile(userId, sourceFileName, i); if (result) { sourcePPT = result; break; } } catch (error) { continue; } } } if (!sourcePPT) { return res.status(404).json({ error: 'Source PPT not found' }); } // 创建新的PPT ID和数据 const newPptId = uuidv4(); const newFileName = `${newPptId}.json`; const newPPTData = { ...sourcePPT, id: newPptId, title: title || `${sourcePPT.title} - 副本`, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() }; // 保存复制的PPT if (githubService.useMemoryStorage) { await githubService.saveToMemory(userId, newFileName, newPPTData); } else { await githubService.saveFile(userId, newFileName, newPPTData, 0); } res.json({ message: 'PPT copied successfully', pptId: newPptId, ppt: newPPTData }); } catch (error) { next(error); } }); export default router;