|
import express from 'express';
|
|
import githubService from '../services/githubService.js';
|
|
import screenshotService from '../services/screenshotService.js';
|
|
|
|
import { generateSlideHTML, generateExportPage } from '../../../shared/export-utils.js';
|
|
|
|
const router = express.Router();
|
|
|
|
|
|
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>
|
|
`;
|
|
}
|
|
|
|
|
|
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;
|
|
|
|
|
|
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'));
|
|
}
|
|
|
|
|
|
const htmlContent = generateSlideHTML(pptData, slideIdx, { format: 'view' });
|
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
res.send(htmlContent);
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
|
|
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;
|
|
|
|
|
|
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' });
|
|
}
|
|
|
|
|
|
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);
|
|
}
|
|
});
|
|
|
|
|
|
router.get('/ppt/:userId/:pptId', async (req, res, next) => {
|
|
try {
|
|
const { userId, pptId } = req.params;
|
|
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' });
|
|
}
|
|
|
|
|
|
res.json({
|
|
...pptData,
|
|
isPublicView: true,
|
|
readOnly: true
|
|
});
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
|
|
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}`);
|
|
|
|
|
|
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 = {
|
|
|
|
slideUrl: `${protocol}://${baseUrl}/api/public/view/${userId}/${pptId}/${slideIndex}`,
|
|
|
|
pptUrl: `${protocol}://${baseUrl}/api/public/ppt/${userId}/${pptId}`,
|
|
|
|
viewUrl: `${protocol}://${baseUrl}/public/${userId}/${pptId}/${slideIndex}`,
|
|
|
|
screenshotUrl: `${protocol}://${baseUrl}/api/public/screenshot/${userId}/${pptId}/${slideIndex}`,
|
|
|
|
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);
|
|
}
|
|
});
|
|
|
|
|
|
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}`);
|
|
|
|
|
|
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' });
|
|
}
|
|
|
|
|
|
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);
|
|
}
|
|
|
|
|
|
console.log('Using backend screenshot for direct image return...');
|
|
|
|
|
|
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);
|
|
|
|
|
|
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);
|
|
}
|
|
});
|
|
|
|
|
|
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}`);
|
|
|
|
|
|
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
|
|
});
|
|
|
|
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);
|
|
}
|
|
});
|
|
|
|
|
|
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}`);
|
|
|
|
|
|
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' });
|
|
}
|
|
|
|
|
|
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);
|
|
}
|
|
});
|
|
|
|
|
|
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,
|
|
screenshotSize: 0
|
|
}
|
|
};
|
|
|
|
|
|
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; |