import express from 'express'; import githubService from '../services/githubService.js'; import screenshotService from '../services/screenshotService.js'; // 修正导入路径:从 backend/src 向上两级到 app,再进入 shared import { generateSlideHTML, generateExportPage } from '../../../shared/export-utils.js'; const router = express.Router(); // Generate error page for frontend display function generateErrorPage(title, message) { return ` ${title} - PPT导出
${title}
${message}
回到首页
`; } // Publicly access PPT page - return HTML page router.get('/view/:userId/:pptId/:slideIndex?', async (req, res, next) => { try { const { userId, pptId, slideIndex = 0 } = req.params; const querySlideIndex = req.query.slide ? parseInt(req.query.slide) : parseInt(slideIndex); const fileName = `${pptId}.json`; let pptData = null; // Try all GitHub repositories for (let i = 0; i < githubService.repositories.length; i++) { try { const result = await githubService.getFile(userId, fileName, i); if (result) { pptData = result.content; break; } } catch (error) { continue; } } if (!pptData) { return res.status(404).send(generateErrorPage('PPT Not Found', 'Please check if the link is correct')); } const slideIdx = querySlideIndex; if (slideIdx >= pptData.slides.length || slideIdx < 0) { return res.status(404).send(generateErrorPage('Slide Not Found', 'Please check if the slide index is correct')); } // 使用共享模块生成HTML const htmlContent = generateSlideHTML(pptData, slideIdx, { format: 'view' }); res.setHeader('Content-Type', 'text/html; charset=utf-8'); res.send(htmlContent); } catch (error) { next(error); } }); // API endpoint: Get PPT data and specified slide (JSON format) router.get('/api-view/:userId/:pptId/:slideIndex?', async (req, res, next) => { try { const { userId, pptId, slideIndex = 0 } = req.params; const fileName = `${pptId}.json`; let pptData = null; // Try all GitHub repositories for (let i = 0; i < githubService.repositories.length; i++) { try { const result = await githubService.getFile(userId, fileName, i); if (result) { pptData = result.content; break; } } catch (error) { continue; } } if (!pptData) { return res.status(404).json({ error: 'PPT not found' }); } const slideIdx = parseInt(slideIndex); if (slideIdx >= pptData.slides.length || slideIdx < 0) { return res.status(404).json({ error: 'Slide not found' }); } // Return PPT data and specified slide res.json({ id: pptData.id, title: pptData.title, theme: pptData.theme, currentSlide: pptData.slides[slideIdx], slideIndex: slideIdx, totalSlides: pptData.slides.length, isPublicView: true }); } catch (error) { next(error); } }); // Get complete PPT data (read-only mode) router.get('/ppt/:userId/:pptId', async (req, res, next) => { try { const { userId, pptId } = req.params; const fileName = `${pptId}.json`; let pptData = null; // Try all GitHub repositories for (let i = 0; i < githubService.repositories.length; i++) { try { const result = await githubService.getFile(userId, fileName, i); if (result) { pptData = result.content; break; } } catch (error) { continue; } } if (!pptData) { return res.status(404).json({ error: 'PPT not found' }); } // Return read-only version of PPT data res.json({ ...pptData, isPublicView: true, readOnly: true }); } catch (error) { next(error); } }); // Generate PPT share link router.post('/generate-share-link', async (req, res, next) => { try { const { userId, pptId, slideIndex = 0 } = req.body; if (!userId || !pptId) { return res.status(400).json({ error: 'User ID and PPT ID are required' }); } console.log(`Generating share link for PPT: ${pptId}, User: ${userId}`); // Verify if PPT exists const fileName = `${pptId}.json`; let pptExists = false; let pptData = null; console.log(`Checking ${githubService.repositories.length} GitHub repositories...`); for (let i = 0; i < githubService.repositories.length; i++) { try { console.log(`Checking repository ${i}: ${githubService.repositories[i]}`); const result = await githubService.getFile(userId, fileName, i); if (result) { pptExists = true; pptData = result.content; console.log(`PPT found in repository ${i}`); break; } } catch (error) { console.log(`PPT not found in repository ${i}: ${error.message}`); continue; } } if (!pptExists) { return res.status(404).json({ error: 'PPT not found', details: `PPT ${pptId} not found for user ${userId}`, searchedLocations: 'GitHub repositories' }); } const baseUrl = process.env.PUBLIC_URL || req.get('host'); const protocol = process.env.NODE_ENV === 'production' ? 'https' : req.protocol; const shareLinks = { // Single page share link slideUrl: `${protocol}://${baseUrl}/api/public/view/${userId}/${pptId}/${slideIndex}`, // Complete PPT share link pptUrl: `${protocol}://${baseUrl}/api/public/ppt/${userId}/${pptId}`, // Frontend view link viewUrl: `${protocol}://${baseUrl}/public/${userId}/${pptId}/${slideIndex}`, // Screenshot link screenshotUrl: `${protocol}://${baseUrl}/api/public/screenshot/${userId}/${pptId}/${slideIndex}`, // Add PPT information pptInfo: { id: pptId, title: pptData?.title || 'Unknown Title', slideCount: pptData?.slides?.length || 0 } }; console.log('Share links generated successfully:', shareLinks); res.json(shareLinks); } catch (error) { next(error); } }); // Screenshot endpoint - Frontend Export Strategy router.get('/screenshot/:userId/:pptId/:slideIndex?', async (req, res, next) => { try { const { userId, pptId, slideIndex = 0 } = req.params; const { format = 'jpeg', quality = 90, strategy = 'frontend-first', returnHtml = 'true' } = req.query; console.log(`Screenshot request: userId=${userId}, pptId=${pptId}, slideIndex=${slideIndex}, strategy=${strategy}, returnHtml=${returnHtml}`); // Get PPT data const fileName = `${pptId}.json`; let pptData = null; // Try all GitHub repositories for (let i = 0; i < githubService.repositories.length; i++) { try { const result = await githubService.getFile(userId, fileName, i); if (result) { pptData = result.content; break; } } catch (error) { continue; } } if (!pptData) { return res.status(404).json({ error: 'PPT not found' }); } const slideIdx = parseInt(slideIndex); if (slideIdx >= pptData.slides.length || slideIdx < 0) { return res.status(404).json({ error: 'Invalid slide index' }); } // Frontend Export Strategy - Return HTML page for client-side screenshot generation if (strategy === 'frontend-first' && returnHtml !== 'false') { console.log('Using frontend export strategy - returning HTML page'); // 使用共享模块生成导出页面 const htmlPage = generateExportPage(pptData, slideIdx, { format, quality: parseInt(quality), autoDownload: req.query.autoDownload === 'true' }); res.setHeader('Content-Type', 'text/html; charset=utf-8'); res.setHeader('X-Screenshot-Strategy', 'frontend-export'); res.setHeader('X-Generation-Time', '< 100ms'); return res.send(htmlPage); } // Fallback: Backend screenshot (if specifically requested) console.log('Using backend screenshot for direct image return...'); // 使用共享模块生成HTML用于后端截图 const htmlContent = generateSlideHTML(pptData, slideIdx, { format: 'screenshot' }); try { const screenshot = await screenshotService.generateScreenshot(htmlContent, { format, quality: parseInt(quality), width: pptData.viewportSize || 1000, height: Math.ceil((pptData.viewportSize || 1000) * (pptData.viewportRatio || 0.5625)) }); res.setHeader('Content-Type', `image/${format}`); res.setHeader('X-Screenshot-Type', 'backend-generated'); res.setHeader('X-Generation-Time', '2-5s'); res.send(screenshot); } catch (error) { console.error('Backend screenshot failed:', error); // Generate fallback image const fallbackImage = screenshotService.generateFallbackImage( pptData.viewportSize || 1000, Math.ceil((pptData.viewportSize || 1000) * (pptData.viewportRatio || 0.5625)) ); res.setHeader('Content-Type', `image/${format}`); res.setHeader('X-Screenshot-Type', 'fallback-generated'); res.setHeader('X-Generation-Time', '< 50ms'); res.send(fallbackImage); } } catch (error) { next(error); } }); // Image endpoint - Direct frontend export page router.get('/image/:userId/:pptId/:slideIndex?', async (req, res, next) => { try { const { userId, pptId, slideIndex = 0 } = req.params; const { format = 'jpeg', quality = 90 } = req.query; console.log(`Image export request: userId=${userId}, pptId=${pptId}, slideIndex=${slideIndex}`); // Get PPT data const fileName = `${pptId}.json`; let pptData = null; for (let i = 0; i < githubService.repositories.length; i++) { try { const result = await githubService.getFile(userId, fileName, i); if (result) { pptData = result.content; break; } } catch (error) { continue; } } if (!pptData) { const errorPage = generateErrorPage('PPT Not Found', `PPT ${pptId} not found for user ${userId}`); res.setHeader('Content-Type', 'text/html; charset=utf-8'); return res.status(404).send(errorPage); } const slideIdx = parseInt(slideIndex); if (slideIdx >= pptData.slides.length || slideIdx < 0) { const errorPage = generateErrorPage('Invalid Slide', `Slide ${slideIndex} not found in PPT`); res.setHeader('Content-Type', 'text/html; charset=utf-8'); return res.status(404).send(errorPage); } // 使用共享模块生成导出页面 const htmlPage = generateExportPage(pptData, slideIdx, { format, quality: parseInt(quality), autoDownload: true // Auto-generate and download }); res.setHeader('Content-Type', 'text/html; charset=utf-8'); res.setHeader('X-Screenshot-Strategy', 'frontend-export-page'); res.setHeader('X-Generation-Time', '< 50ms'); res.send(htmlPage); } catch (error) { next(error); } }); // Screenshot data endpoint - For frontend direct rendering router.get('/screenshot-data/:userId/:pptId/:slideIndex?', async (req, res, next) => { try { const { userId, pptId, slideIndex = 0 } = req.params; const { format = 'jpeg', quality = 90 } = req.query; console.log(`Screenshot data request: userId=${userId}, pptId=${pptId}, slideIndex=${slideIndex}`); // Get PPT data const fileName = `${pptId}.json`; let pptData = null; for (let i = 0; i < githubService.repositories.length; i++) { try { const result = await githubService.getFile(userId, fileName, i); if (result) { pptData = result.content; break; } } catch (error) { continue; } } if (!pptData) { return res.status(404).json({ error: 'PPT not found' }); } const slideIdx = parseInt(slideIndex); if (slideIdx >= pptData.slides.length || slideIdx < 0) { return res.status(404).json({ error: 'Invalid slide index' }); } // Return PPT data for frontend rendering const responseData = { pptData: { id: pptData.id, title: pptData.title, theme: pptData.theme, viewportSize: pptData.viewportSize, viewportRatio: pptData.viewportRatio, slide: pptData.slides[slideIdx] }, slideIndex: slideIdx, totalSlides: pptData.slides.length, exportConfig: { format, quality: parseInt(quality), width: pptData.viewportSize || 1000, height: Math.ceil((pptData.viewportSize || 1000) * (pptData.viewportRatio || 0.5625)) }, strategy: 'frontend-direct-rendering', timestamp: new Date().toISOString() }; res.setHeader('X-Screenshot-Strategy', 'frontend-data-api'); res.json(responseData); } catch (error) { next(error); } }); // Screenshot health check router.get('/screenshot-health', async (req, res, next) => { try { const status = { status: 'healthy', timestamp: new Date().toISOString(), environment: { nodeEnv: process.env.NODE_ENV, isHuggingFace: process.env.SPACE_ID ? true : false, platform: process.platform }, screenshotService: { available: true, strategy: 'frontend-first', backendFallback: false, // Disable backend browsers screenshotSize: 0 } }; // Test fallback image generation try { const testImage = screenshotService.generateFallbackImage(100, 100); status.screenshotService.screenshotSize = testImage.length; status.screenshotService.fallbackAvailable = true; } catch (error) { status.screenshotService.fallbackAvailable = false; status.screenshotService.fallbackError = error.message; } res.json(status); } catch (error) { res.status(500).json({ status: 'error', timestamp: new Date().toISOString(), error: error.message }); } }); export default router;