CatPtain's picture
Upload 85 files
28e1dba verified
import express from 'express';
// import screenshotService from '../services/screenshotService.js'; // 已禁用
import githubService from '../services/githubService.js';
import { generateSlideHTML } from '../utils/htmlGenerator.js';
const router = express.Router();
// SVG功能已被完全删除
// 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);
let pptData = null;
try {
// 尝试从Huggingface存储读取
const { default: huggingfaceStorageService } = await import('../services/huggingfaceStorageService.js');
pptData = await huggingfaceStorageService.getPPTData(userId, pptId);
console.log('✅ PPT data loaded from Huggingface storage for view');
} catch (hfError) {
console.warn('⚠️ Failed to load from Huggingface storage, trying GitHub:', hfError.message);
// 回退到GitHub存储
const fileName = `${pptId}.json`;
// Check if GitHub repositories are configured
if (!githubService.repositories || githubService.repositories.length === 0) {
console.warn('⚠️ GitHub repositories not configured, cannot fallback to GitHub storage');
} else {
// 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;
console.log(`✅ PPT data loaded from GitHub repository ${i} for view`);
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) {
console.log(`❌ Invalid slide index: ${slideIndex} (parsed: ${slideIdx}), PPT has ${pptData.slides.length} slides (valid range: 0-${pptData.slides.length - 1})`);
return res.status(404).send(generateErrorPage('Slide Not Found', `Slide ${slideIndex} not found. PPT has ${pptData.slides.length} slides (valid range: 0-${pptData.slides.length - 1}).`));
}
// 使用共享模块生成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;
let pptData = null;
try {
// 尝试从Huggingface存储读取
const { default: huggingfaceStorageService } = await import('../services/huggingfaceStorageService.js');
pptData = await huggingfaceStorageService.getPPTData(userId, pptId);
console.log('✅ PPT data loaded from Huggingface storage for api-view');
} catch (hfError) {
console.warn('⚠️ Failed to load from Huggingface storage, trying GitHub:', hfError.message);
// 回退到GitHub存储
const fileName = `${pptId}.json`;
// Check if GitHub repositories are configured
if (!githubService.repositories || githubService.repositories.length === 0) {
console.warn('⚠️ GitHub repositories not configured, cannot fallback to GitHub storage');
} else {
// 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;
console.log(`✅ PPT data loaded from GitHub repository ${i} for api-view`);
break;
}
} catch (error) {
continue;
}
}
}
}
if (!pptData) {
return res.status(404).json({ error: 'PPT not found' });
}
const slideIdx = parseInt(slideIndex);
// 验证 slideIndex 是否为有效数字
if (isNaN(slideIdx)) {
console.log(`❌ Invalid slide index format: ${slideIndex}`);
const errorPage = generateErrorPage('Invalid Slide Index', `Slide index "${slideIndex}" is not a valid number.`);
res.setHeader('Content-Type', 'text/html; charset=utf-8');
return res.status(400).send(errorPage);
}
// 安全检查:确保 slides 数组存在
if (!pptData.slides || !Array.isArray(pptData.slides)) {
console.log(`❌ Invalid PPT data: slides array not found`);
const errorPage = generateErrorPage('Invalid PPT Data', 'PPT slides data not found or corrupted.');
res.setHeader('Content-Type', 'text/html; charset=utf-8');
return res.status(400).send(errorPage);
}
if (slideIdx >= pptData.slides.length || slideIdx < 0) {
console.log(`❌ Invalid slide index: ${slideIndex} (parsed: ${slideIdx}), PPT has ${pptData.slides.length} slides (valid range: 0-${pptData.slides.length - 1})`);
return res.status(404).json({
error: 'Slide not found',
details: `Slide ${slideIndex} not found. PPT has ${pptData.slides.length} slides (valid range: 0-${pptData.slides.length - 1}).`,
slideIndex: slideIdx,
totalSlides: pptData.slides.length,
validRange: `0-${pptData.slides.length - 1}`
});
}
// 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;
let pptData = null;
try {
// 尝试从Huggingface存储读取
const { default: huggingfaceStorageService } = await import('../services/huggingfaceStorageService.js');
pptData = await huggingfaceStorageService.getPPTData(userId, pptId);
console.log('✅ PPT data loaded from Huggingface storage for ppt endpoint');
} catch (hfError) {
console.warn('⚠️ Failed to load from Huggingface storage, trying GitHub:', hfError.message);
// 回退到GitHub存储
const fileName = `${pptId}.json`;
// Check if GitHub repositories are configured
if (!githubService.repositories || githubService.repositories.length === 0) {
console.warn('⚠️ GitHub repositories not configured, cannot fallback to GitHub storage');
} else {
// 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;
console.log(`✅ PPT data loaded from GitHub repository ${i} for ppt endpoint`);
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 - 优先从Huggingface存储读取
let pptData = null;
try {
// 尝试从Huggingface存储读取
const { default: huggingfaceStorageService } = await import('../services/huggingfaceStorageService.js');
pptData = await huggingfaceStorageService.getPPTData(userId, pptId);
console.log('✅ PPT data loaded from Huggingface storage for share link generation');
} catch (hfError) {
console.warn('⚠️ Failed to load from Huggingface storage, trying GitHub:', hfError.message);
// 回退到GitHub存储
const fileName = `${pptId}.json`;
// Check if GitHub repositories are configured
if (!githubService.repositories || githubService.repositories.length === 0) {
console.warn('⚠️ GitHub repositories not configured, cannot fallback to GitHub storage');
} else {
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) {
pptData = result.content;
console.log(`✅ PPT data loaded from GitHub repository ${i}`);
break;
}
} catch (error) {
console.log(`PPT not found in repository ${i}: ${error.message}`);
continue;
}
}
}
}
if (!pptData) {
return res.status(404).json({
error: 'PPT not found',
details: `PPT ${pptId} not found for user ${userId}`,
searchedLocations: 'Huggingface storage and GitHub repositories'
});
}
const baseUrl = process.env.PUBLIC_URL || req.get('host');
// 修复协议判断:localhost 始终使用 http,生产环境使用 https
const protocol = baseUrl.includes('localhost') ? 'http' : 'https';
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}`,
// 图片链接 - 支持内存图片访问
screenshotUrl: `${protocol}://${baseUrl}/api/public/image/${userId}/${pptId}/${slideIndex}`,
directImageUrl: `${protocol}://${baseUrl}/api/public/direct-image/${userId}/${pptId}/${slideIndex}`,
metadata: {
title: pptData.title || 'Untitled PPT',
slideCount: pptData.slides?.length || 0,
currentSlide: slideIndex,
generatedAt: new Date().toISOString(),
note: 'Image links support memory-based image access with fallback to error SVG'
}
};
console.log('✅ Share links generated successfully');
res.json(shareLinks);
} catch (error) {
console.error('❌ Error generating share link:', error);
next(error);
}
});
// 公开图片访问路由 - 处理 /image/:userId/:linkId/:pageIndex 格式
router.get('/image/:userId/:linkId/:pageIndex', async (req, res, next) => {
try {
const { userId, linkId, pageIndex } = req.params;
const pageIdx = parseInt(pageIndex);
console.log(`📷 Public image request: userId=${userId}, linkId=${linkId}, pageIndex=${pageIndex}`);
// 首先尝试从内存存储中获取图片
try {
const { default: huggingfaceStorageService } = await import('../services/huggingfaceStorageService.js');
// 检查内存中是否有该图片
if (huggingfaceStorageService.hasPublicImage(userId, linkId, pageIdx)) {
console.log(`✅ Found image in memory storage: ${userId}/${linkId}/${pageIdx}`);
const imageResult = await huggingfaceStorageService.getPublicImage(userId, linkId, pageIdx);
// 设置响应头
const format = imageResult.metadata.format || 'png';
res.setHeader('Content-Type', `image/${format}`);
res.setHeader('Cache-Control', 'public, max-age=3600'); // 缓存1小时
res.setHeader('Content-Length', imageResult.data.length);
// 返回图片数据
return res.send(imageResult.data);
}
} catch (memoryError) {
console.log(`⚠️ Memory storage access failed: ${memoryError.message}`);
}
// 检查linkId是否为UUID格式(持久化链接)
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
if (uuidRegex.test(linkId)) {
// 检查这是否真的是一个持久化链接
try {
const { default: persistentImageLinkService } = await import('../services/persistentImageLinkService.js');
// 确保服务已初始化
if (!persistentImageLinkService.initialized) {
await persistentImageLinkService.initialize();
}
// 检查持久化链接是否存在
const linkExists = persistentImageLinkService.persistentLinks.has(linkId);
if (linkExists) {
// 这是真正的持久化图片链接,重定向到正确的持久化图片API
console.log(`🔗 Redirecting persistent image request: ${linkId}`);
return res.redirect(`/api/persistent-images/${linkId}`);
} else {
// UUID格式但不是持久化链接,当作PPT ID处理
console.log(`⚠️ UUID format but not a persistent link, treating as PPT ID: ${linkId}`);
}
} catch (error) {
console.log(`⚠️ Error checking persistent link, treating as PPT ID: ${error.message}`);
}
}
// 内存中没有找到图片,且不是持久化链接,生成错误SVG图片
console.log(`⚠️ Image not found in memory storage, generating error SVG for pptId: ${linkId}`);
// 生成错误提示SVG
const errorSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="400" height="300" viewBox="0 0 400 300">
<rect width="100%" height="100%" fill="#f8f9fa" stroke="#dee2e6" stroke-width="2"/>
<text x="200" y="120" text-anchor="middle" font-family="Arial, sans-serif" font-size="16" fill="#6c757d">图片未找到</text>
<text x="200" y="150" text-anchor="middle" font-family="Arial, sans-serif" font-size="14" fill="#6c757d">PPT ID: ${linkId}</text>
<text x="200" y="180" text-anchor="middle" font-family="Arial, sans-serif" font-size="14" fill="#6c757d">页面: ${pageIndex}</text>
<text x="200" y="210" text-anchor="middle" font-family="Arial, sans-serif" font-size="12" fill="#6c757d">请先生成图片或使用HTML预览</text>
</svg>`;
res.setHeader('Content-Type', 'image/svg+xml');
res.setHeader('Cache-Control', 'no-cache');
return res.send(errorSvg);
} catch (error) {
next(error);
}
});
// 传统图片路由(3个参数)- 已禁用
router.get('/image/:userId/:pptId', async (req, res, next) => {
try {
console.log('❌ Image generation feature has been disabled');
// 对于图片请求,返回适当的错误响应而不是HTML页面
res.status(410).json({
error: 'Feature Disabled',
message: 'Image generation functionality has been disabled. Please use the HTML view instead.',
note: 'This endpoint no longer generates images. Use /api/public/view/:userId/:pptId/:slideIndex for HTML view.'
});
} catch (error) {
next(error);
}
});
// 公开访问:获取用户的PPT列表(仅包含内存中有图片的PPT)
router.get('/ppt-list/:userId', async (req, res, next) => {
try {
const { userId } = req.params;
console.log(`📋 Public PPT list request for user: ${userId}`);
try {
const { default: huggingfaceStorageService } = await import('../services/huggingfaceStorageService.js');
// 获取用户的PPT列表(仅包含内存中有图片的PPT)
const pptList = huggingfaceStorageService.getPublicPPTList(userId);
console.log(`✅ Found ${pptList.length} PPTs with images for user ${userId}`);
res.json({
success: true,
userId,
pptCount: pptList.length,
ppts: pptList,
note: 'Only PPTs with images stored in memory are listed'
});
} catch (error) {
console.error(`❌ Failed to get PPT list for user ${userId}:`, error);
res.status(500).json({
success: false,
error: 'Failed to retrieve PPT list',
message: error.message
});
}
} catch (error) {
next(error);
}
});
// 直接持久化图片链接路由 - 处理 /direct-image/:userId/:linkId/:pageIndex 格式
router.get('/direct-image/:userId/:linkId/:pageIndex', async (req, res, next) => {
try {
const { userId, linkId, pageIndex } = req.params;
const pageIdx = parseInt(pageIndex);
// 首先尝试从内存存储中获取图片
try {
const { default: huggingfaceStorageService } = await import('../services/huggingfaceStorageService.js');
// 检查内存中是否有该图片
if (huggingfaceStorageService.hasPublicImage(userId, linkId, pageIdx)) {
console.log(`✅ Found direct image in memory storage: ${userId}/${linkId}/${pageIdx}`);
const imageResult = await huggingfaceStorageService.getPublicImage(userId, linkId, pageIdx);
// 设置响应头
const format = imageResult.metadata.format || 'png';
res.setHeader('Content-Type', `image/${format}`);
res.setHeader('Cache-Control', 'public, max-age=3600'); // 缓存1小时
res.setHeader('Content-Length', imageResult.data.length);
// 返回图片数据
return res.send(imageResult.data);
}
} catch (memoryError) {
console.log(`⚠️ Memory storage access failed: ${memoryError.message}`);
}
// 检查linkId是否为UUID格式(持久化链接)
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
if (uuidRegex.test(linkId)) {
// 这是持久化图片链接,重定向到正确的持久化图片API
console.log(`🔗 Redirecting direct persistent image request: ${linkId}`);
return res.redirect(`/api/persistent-images/${linkId}`);
} else {
// 内存中没有找到图片,且不是持久化链接
console.log('❌ Direct image not found in memory storage');
// 对于图片请求,返回适当的错误响应而不是HTML页面
res.status(404).json({
error: 'Image Not Found',
message: 'Image not found in memory storage. Please generate the image first or use the HTML view instead.',
note: 'Use /api/public/view/:userId/:pptId/:slideIndex for HTML view.'
});
}
} catch (error) {
next(error);
}
});
// 传统直接图片路由(3个参数)- 已禁用
router.get('/direct-image/:userId/:pptId', async (req, res, next) => {
try {
console.log('❌ Direct image generation feature has been disabled');
// 对于图片请求,返回适当的错误响应而不是HTML页面
res.status(410).json({
error: 'Feature Disabled',
message: 'Direct image generation functionality has been disabled. Please use the HTML view instead.',
note: 'This endpoint no longer generates images. Use /api/public/view/:userId/:pptId/:slideIndex for HTML view.'
});
} catch (error) {
next(error);
}
});
export default router;