CatPtain's picture
Upload 11 files
0afb612 verified
raw
history blame
16.7 kB
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 `
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${title} - PPT导出</title>
<style>
body {
font-family: 'Microsoft YaHei', sans-serif;
margin: 0;
padding: 0;
background: linear-gradient(135deg, #ff6b6b 0%, #feca57 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.error-container {
background: white;
padding: 40px;
border-radius: 15px;
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
text-align: center;
max-width: 500px;
margin: 20px;
}
.error-icon { font-size: 64px; margin-bottom: 20px; }
.error-title { font-size: 24px; font-weight: bold; color: #333; margin-bottom: 15px; }
.error-message { font-size: 16px; color: #666; line-height: 1.6; margin-bottom: 30px; }
.error-actions { display: flex; gap: 15px; justify-content: center; }
.btn {
padding: 12px 24px;
border: none;
border-radius: 5px;
font-size: 16px;
cursor: pointer;
text-decoration: none;
display: inline-block;
transition: all 0.3s;
}
.btn-primary { background: #4CAF50; color: white; }
.btn-primary:hover { background: #45a049; }
.btn-secondary { background: #f8f9fa; color: #333; border: 1px solid #ddd; }
.btn-secondary:hover { background: #e9ecef; }
</style>
</head>
<body>
<div class="error-container">
<div class="error-icon">❌</div>
<div class="error-title">${title}</div>
<div class="error-message">${message}</div>
<div class="error-actions">
<button class="btn btn-secondary" onclick="history.back()">返回上页</button>
<a href="/" class="btn btn-primary">回到首页</a>
</div>
</div>
</body>
</html>
`;
}
// 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;