Spaces:
Running
Running
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; |