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导出
`;
}
// 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;