Spaces:
Running
Running
import express from 'express'; | |
import persistentImageLinkService from '../services/persistentImageLinkService.js'; | |
import githubService from '../services/githubService.js'; | |
import huggingfaceStorageService from '../services/huggingfaceStorageService.js'; | |
import { authenticateToken } from '../middleware/auth.js'; | |
const router = express.Router(); | |
/** | |
* 接收前端生成的图片数据并创建持久化链接 | |
* POST /api/export/ppt-to-image | |
*/ | |
router.post('/ppt-to-image', authenticateToken, async (req, res, next) => { | |
try { | |
const { pptId, slideIndex = 0, imageData, format = 'jpeg', metadata = {} } = req.body; | |
const userId = req.user.userId; | |
console.log(`🖼️ Export PPT to image request: userId=${userId}, pptId=${pptId}, slideIndex=${slideIndex}`); | |
// 验证参数 | |
if (!pptId) { | |
return res.status(400).json({ error: 'PPT ID is required' }); | |
} | |
if (!imageData) { | |
return res.status(400).json({ error: 'Image data is required' }); | |
} | |
// 验证图片数据格式 | |
if (!imageData.startsWith('data:image/')) { | |
return res.status(400).json({ error: 'Invalid image data format' }); | |
} | |
// 获取PPT数据以验证权限 | |
let pptData; | |
try { | |
pptData = await huggingfaceStorageService.getPPTData(userId, pptId); | |
} catch (error) { | |
console.log('Huggingface storage failed, trying GitHub...'); | |
try { | |
pptData = await githubService.getPPTData(userId, pptId); | |
} catch (githubError) { | |
console.error('Both storage methods failed:', { huggingface: error.message, github: githubError.message }); | |
return res.status(404).json({ error: 'PPT not found' }); | |
} | |
} | |
// 验证幻灯片索引 | |
if (!pptData.slides || slideIndex >= pptData.slides.length) { | |
return res.status(400).json({ error: 'Invalid slide index' }); | |
} | |
// 将图片数据转换为Buffer并存储到内存 | |
const base64Data = imageData.split(',')[1]; | |
const imageBuffer = Buffer.from(base64Data, 'base64'); | |
// 存储图片到内存中供公开访问 | |
try { | |
await huggingfaceStorageService.storeImage(userId, pptId, slideIndex, imageBuffer, { | |
format: format === 'jpeg' ? 'jpg' : format, | |
updateExisting: true | |
}); | |
console.log(`✅ Image stored in memory: ${userId}/${pptId}/${slideIndex}`); | |
} catch (storeError) { | |
console.warn(`⚠️ Failed to store image in memory: ${storeError.message}`); | |
} | |
// 创建持久化链接 | |
const linkData = { | |
pptId, | |
slideIndex, | |
imageData, | |
format, | |
metadata: { | |
...metadata, | |
exportedAt: new Date().toISOString(), | |
slideTitle: pptData.slides[slideIndex]?.title || `第 ${slideIndex + 1} 页`, | |
exportMethod: 'frontend' | |
} | |
}; | |
const result = await persistentImageLinkService.createLink(userId, linkData); | |
console.log(`✅ PPT exported successfully: ${result.linkId}`); | |
res.json({ | |
success: true, | |
linkId: result.linkId, | |
imageUrl: result.imageUrl, | |
downloadUrl: result.downloadUrl, | |
metadata: result.metadata | |
}); | |
} catch (error) { | |
console.error('Export PPT to image failed:', error); | |
next(error); | |
} | |
}); | |
/** | |
* 接收前端批量生成的图片数据并创建持久化链接 | |
* POST /api/export/ppt-to-images-batch | |
*/ | |
router.post('/ppt-to-images-batch', authenticateToken, async (req, res, next) => { | |
try { | |
const { pptId, imagesData, format = 'jpeg', metadata = {} } = req.body; | |
const userId = req.user.userId; | |
console.log(`🖼️ Batch export PPT to images: userId=${userId}, pptId=${pptId}`); | |
// 验证参数 | |
if (!pptId) { | |
return res.status(400).json({ error: 'PPT ID is required' }); | |
} | |
if (!imagesData || !Array.isArray(imagesData) || imagesData.length === 0) { | |
return res.status(400).json({ error: 'Images data array is required' }); | |
} | |
// 获取PPT数据以验证权限 | |
let pptData; | |
try { | |
pptData = await huggingfaceStorageService.getPPTData(userId, pptId); | |
} catch (error) { | |
console.log('Huggingface storage failed, trying GitHub...'); | |
try { | |
pptData = await githubService.getPPTData(userId, pptId); | |
} catch (githubError) { | |
console.error('Both storage methods failed:', { huggingface: error.message, github: githubError.message }); | |
return res.status(404).json({ error: 'PPT not found' }); | |
} | |
} | |
if (!pptData.slides || pptData.slides.length === 0) { | |
return res.status(400).json({ error: 'No slides found in PPT' }); | |
} | |
// 批量创建持久化链接 | |
const results = []; | |
const errors = []; | |
const exportTime = new Date().toISOString(); | |
for (const imageItem of imagesData) { | |
try { | |
const { slideIndex, imageData } = imageItem; | |
// 验证图片数据 | |
if (!imageData || !imageData.startsWith('data:image/')) { | |
errors.push({ slideIndex, error: 'Invalid image data format' }); | |
continue; | |
} | |
// 验证幻灯片索引 | |
if (slideIndex >= pptData.slides.length) { | |
errors.push({ slideIndex, error: 'Invalid slide index' }); | |
continue; | |
} | |
// 将图片数据转换为Buffer并存储到内存 | |
const base64Data = imageData.split(',')[1]; | |
const imageBuffer = Buffer.from(base64Data, 'base64'); | |
// 存储图片到内存中供公开访问 | |
try { | |
await huggingfaceStorageService.storeImage(userId, pptId, slideIndex, imageBuffer, { | |
format: format === 'jpeg' ? 'jpg' : format, | |
updateExisting: true | |
}); | |
console.log(`✅ Batch image stored in memory: ${userId}/${pptId}/${slideIndex}`); | |
} catch (storeError) { | |
console.warn(`⚠️ Failed to store batch image in memory: ${storeError.message}`); | |
} | |
const linkData = { | |
pptId, | |
slideIndex, | |
imageData, | |
format, | |
metadata: { | |
...metadata, | |
exportedAt: exportTime, | |
slideTitle: pptData.slides[slideIndex]?.title || `第 ${slideIndex + 1} 页`, | |
exportMethod: 'frontend', | |
batchExport: true | |
} | |
}; | |
const result = await persistentImageLinkService.createLink(userId, linkData); | |
results.push({ | |
slideIndex, | |
linkId: result.linkId, | |
imageUrl: result.imageUrl, | |
downloadUrl: result.downloadUrl, | |
metadata: result.metadata | |
}); | |
} catch (error) { | |
console.error(`Failed to create link for slide ${imageItem.slideIndex}:`, error); | |
errors.push({ slideIndex: imageItem.slideIndex, error: error.message }); | |
} | |
} | |
console.log(`✅ Batch export completed: ${results.length}/${imagesData.length} successful`); | |
res.json({ | |
success: true, | |
totalSlides: imagesData.length, | |
successCount: results.length, | |
errorCount: errors.length, | |
results, | |
errors | |
}); | |
} catch (error) { | |
console.error('Batch export PPT to images failed:', error); | |
next(error); | |
} | |
}); | |
/** | |
* 接收前端生成的当前幻灯片图片数据并创建持久化链接 | |
* POST /api/export/current-slide | |
*/ | |
router.post('/current-slide', authenticateToken, async (req, res, next) => { | |
try { | |
const { pptId, slideIndex, imageData, format = 'jpeg', metadata = {} } = req.body; | |
const userId = req.user.userId; | |
console.log(`🖼️ Export current slide: userId=${userId}, pptId=${pptId}, slideIndex=${slideIndex}`); | |
// 验证参数 | |
if (!pptId || slideIndex === undefined || !imageData) { | |
return res.status(400).json({ error: 'PPT ID, slide index and image data are required' }); | |
} | |
// 验证图片数据格式 | |
if (!imageData.startsWith('data:image/')) { | |
return res.status(400).json({ error: 'Invalid image data format' }); | |
} | |
// 获取PPT数据以验证权限 | |
let pptData; | |
try { | |
pptData = await huggingfaceStorageService.getPPTData(userId, pptId); | |
} catch (error) { | |
console.log('Huggingface storage failed, trying GitHub...'); | |
try { | |
pptData = await githubService.getPPTData(userId, pptId); | |
} catch (githubError) { | |
console.error('Both storage methods failed:', { huggingface: error.message, github: githubError.message }); | |
return res.status(404).json({ error: 'PPT not found' }); | |
} | |
} | |
// 验证幻灯片索引 | |
if (!pptData.slides || slideIndex >= pptData.slides.length) { | |
return res.status(400).json({ error: 'Invalid slide index' }); | |
} | |
// 将图片数据转换为Buffer并存储到内存 | |
const base64Data = imageData.split(',')[1]; | |
const imageBuffer = Buffer.from(base64Data, 'base64'); | |
// 存储图片到内存中供公开访问 | |
try { | |
await huggingfaceStorageService.storeImage(userId, pptId, slideIndex, imageBuffer, { | |
format: format === 'jpeg' ? 'jpg' : format, | |
updateExisting: true | |
}); | |
console.log(`✅ Current slide image stored in memory: ${userId}/${pptId}/${slideIndex}`); | |
} catch (storeError) { | |
console.warn(`⚠️ Failed to store current slide image in memory: ${storeError.message}`); | |
} | |
// 创建持久化链接 | |
const linkData = { | |
pptId, | |
slideIndex, | |
imageData, | |
format, | |
metadata: { | |
...metadata, | |
exportedAt: new Date().toISOString(), | |
slideTitle: pptData.slides[slideIndex]?.title || `第 ${slideIndex + 1} 页`, | |
exportMethod: 'frontend', | |
isCurrentSlide: true | |
} | |
}; | |
const result = await persistentImageLinkService.createLink(userId, linkData); | |
console.log(`✅ Current slide exported successfully: ${result.linkId}`); | |
res.json({ | |
success: true, | |
linkId: result.linkId, | |
imageUrl: result.imageUrl, | |
downloadUrl: result.downloadUrl, | |
metadata: result.metadata | |
}); | |
} catch (error) { | |
console.error('Export current slide failed:', error); | |
next(error); | |
} | |
}); | |
/** | |
* 获取导出历史记录 | |
* GET /api/export/history | |
*/ | |
router.get('/history', authenticateToken, async (req, res, next) => { | |
try { | |
const userId = req.user.id; | |
const { page = 1, limit = 20, pptId } = req.query; | |
console.log(`📋 Get export history: userId=${userId}, page=${page}, limit=${limit}`); | |
// 获取用户的持久化链接列表 | |
const links = await persistentImageLinkService.getUserLinks(userId, { | |
page: parseInt(page), | |
limit: parseInt(limit), | |
pptId | |
}); | |
// 过滤出导出相关的链接(包含导出元数据) | |
const exportHistory = links.links.filter(link => | |
link.metadata && (link.metadata.exportedAt || link.metadata.isCurrentSlide || link.metadata.batchExport) | |
); | |
res.json({ | |
success: true, | |
total: exportHistory.length, | |
page: parseInt(page), | |
limit: parseInt(limit), | |
history: exportHistory.map(link => ({ | |
linkId: link.linkId, | |
pptId: link.pptId, | |
slideIndex: link.slideIndex, | |
imageUrl: link.imageUrl, | |
downloadUrl: link.downloadUrl, | |
createdAt: link.createdAt, | |
expiresAt: link.expiresAt, | |
metadata: link.metadata | |
})) | |
}); | |
} catch (error) { | |
console.error('Get export history failed:', error); | |
next(error); | |
} | |
}); | |
/** | |
* 删除导出记录 | |
* DELETE /api/export/:linkId | |
*/ | |
router.delete('/:linkId', authenticateToken, async (req, res, next) => { | |
try { | |
const { linkId } = req.params; | |
const userId = req.user.id; | |
console.log(`🗑️ Delete export record: userId=${userId}, linkId=${linkId}`); | |
// 删除持久化链接 | |
const result = await persistentImageLinkService.deleteLink(linkId, userId); | |
if (!result.success) { | |
return res.status(404).json({ error: 'Export record not found or access denied' }); | |
} | |
console.log(`✅ Export record deleted: ${linkId}`); | |
res.json({ | |
success: true, | |
message: 'Export record deleted successfully' | |
}); | |
} catch (error) { | |
console.error('Delete export record failed:', error); | |
next(error); | |
} | |
}); | |
/** | |
* 前端导出功能复刻 - 单个幻灯片导出 | |
* POST /api/export/frontend-replicate-single | |
*/ | |
router.post('/frontend-replicate-single', authenticateToken, async (req, res, next) => { | |
try { | |
const { pptId, slideIndex = 0, format = 'jpeg', quality = 1, width = 1600, height = 900, ignoreWebfont = true, backgroundColor = '#ffffff', pixelRatio = 1 } = req.body; | |
const userId = req.user.userId; | |
console.log(`🖼️ Frontend replicate single slide: userId=${userId}, pptId=${pptId}, slideIndex=${slideIndex}`); | |
// 验证参数 | |
if (!pptId) { | |
return res.status(400).json({ error: 'PPT ID is required' }); | |
} | |
// 获取PPT数据以验证权限 | |
let pptData; | |
try { | |
pptData = await huggingfaceStorageService.getPPTData(userId, pptId); | |
} catch (error) { | |
console.log('Huggingface storage failed, trying GitHub...'); | |
try { | |
pptData = await githubService.getPPTData(userId, pptId); | |
} catch (githubError) { | |
console.error('Both storage methods failed:', { huggingface: error.message, github: githubError.message }); | |
return res.status(404).json({ error: 'PPT not found' }); | |
} | |
} | |
// 验证幻灯片索引 | |
if (!pptData.slides || slideIndex >= pptData.slides.length) { | |
return res.status(400).json({ error: 'Invalid slide index' }); | |
} | |
// 返回指导信息,因为实际的图片生成需要在前端完成 | |
const response = { | |
success: false, | |
message: 'Frontend rendering required', | |
instruction: { | |
method: 'frontend-rendering', | |
description: 'Please use frontend html-to-image library to generate the image data, then call /api/export/ppt-to-image to create persistent link', | |
steps: [ | |
'1. Use html-to-image library in frontend to capture slide DOM', | |
'2. Convert captured image to base64 data URL', | |
'3. Call /api/export/ppt-to-image with the image data', | |
'4. Receive persistent link for the generated image' | |
], | |
alternativeApi: '/api/export/ppt-to-image', | |
requiredParams: { | |
pptId, | |
slideIndex, | |
imageData: 'data:image/jpeg;base64,...', | |
format, | |
metadata: { | |
width, | |
height, | |
quality, | |
backgroundColor, | |
pixelRatio, | |
ignoreWebfont, | |
exportMethod: 'frontend-replication' | |
} | |
} | |
}, | |
slideInfo: { | |
title: pptData.slides[slideIndex]?.title || `第 ${slideIndex + 1} 页`, | |
slideCount: pptData.slides.length | |
} | |
}; | |
res.json(response); | |
} catch (error) { | |
console.error('Frontend replicate single slide failed:', error); | |
next(error); | |
} | |
}); | |
/** | |
* 前端导出功能复刻 - 批量导出 | |
* POST /api/export/frontend-replicate-batch | |
*/ | |
router.post('/frontend-replicate-batch', authenticateToken, async (req, res, next) => { | |
try { | |
const { pptId, rangeType = 'all', range, currentSlideIndex = 0, format = 'jpeg', quality = 1, width = 1600, height = 900 } = req.body; | |
const userId = req.user.userId; | |
console.log(`🖼️ Frontend replicate batch export: userId=${userId}, pptId=${pptId}, rangeType=${rangeType}`); | |
// 验证参数 | |
if (!pptId) { | |
return res.status(400).json({ error: 'PPT ID is required' }); | |
} | |
// 获取PPT数据以验证权限 | |
let pptData; | |
try { | |
pptData = await huggingfaceStorageService.getPPTData(userId, pptId); | |
} catch (error) { | |
console.log('Huggingface storage failed, trying GitHub...'); | |
try { | |
pptData = await githubService.getPPTData(userId, pptId); | |
} catch (githubError) { | |
console.error('Both storage methods failed:', { huggingface: error.message, github: githubError.message }); | |
return res.status(404).json({ error: 'PPT not found' }); | |
} | |
} | |
if (!pptData.slides || pptData.slides.length === 0) { | |
return res.status(400).json({ error: 'No slides found in PPT' }); | |
} | |
// 确定要导出的幻灯片范围 | |
let slideIndices = []; | |
switch (rangeType) { | |
case 'all': | |
slideIndices = Array.from({ length: pptData.slides.length }, (_, i) => i); | |
break; | |
case 'current': | |
if (currentSlideIndex >= 0 && currentSlideIndex < pptData.slides.length) { | |
slideIndices = [currentSlideIndex]; | |
} | |
break; | |
case 'custom': | |
if (Array.isArray(range) && range.length === 2) { | |
const [start, end] = range; | |
for (let i = Math.max(0, start); i <= Math.min(end, pptData.slides.length - 1); i++) { | |
slideIndices.push(i); | |
} | |
} | |
break; | |
} | |
if (slideIndices.length === 0) { | |
return res.status(400).json({ error: 'No valid slides to export' }); | |
} | |
// 返回批量导出指导信息 | |
const response = { | |
success: false, | |
message: 'Frontend rendering required for batch export', | |
instruction: { | |
method: 'frontend-batch-rendering', | |
description: 'Please use frontend to generate all slide images, then call /api/export/ppt-to-images-batch', | |
slideIndices, | |
totalSlides: slideIndices.length, | |
steps: [ | |
'1. Loop through each slide index in slideIndices array', | |
'2. For each slide, use html-to-image library to capture DOM', | |
'3. Collect all image data in the required format', | |
'4. Call /api/export/ppt-to-images-batch with all image data', | |
'5. Receive persistent links for all generated images' | |
], | |
alternativeApi: '/api/export/ppt-to-images-batch', | |
requiredFormat: { | |
pptId, | |
imagesData: [ | |
{ | |
slideIndex: 'number', | |
imageData: 'data:image/jpeg;base64,...' | |
} | |
], | |
format, | |
metadata: { | |
width, | |
height, | |
quality, | |
exportMethod: 'frontend-batch-replication', | |
rangeType, | |
range: rangeType === 'custom' ? range : undefined | |
} | |
} | |
}, | |
pptInfo: { | |
title: pptData.title || '未命名演示文稿', | |
slideCount: pptData.slides.length, | |
exportRange: { | |
type: rangeType, | |
indices: slideIndices, | |
total: slideIndices.length | |
} | |
} | |
}; | |
res.json(response); | |
} catch (error) { | |
console.error('Frontend replicate batch export failed:', error); | |
next(error); | |
} | |
}); | |
/** | |
* 前端导出服务健康检查 | |
* GET /api/export/health | |
*/ | |
router.get('/health', async (req, res, next) => { | |
try { | |
console.log('🔍 Checking frontend export service health...'); | |
// 检查持久化链接服务状态 | |
const linkServiceStatus = await persistentImageLinkService.healthCheck(); | |
// 检查存储服务状态 | |
const storageStatus = await huggingfaceStorageService.healthCheck(); | |
const isHealthy = linkServiceStatus.status === 'healthy' && storageStatus.status === 'healthy'; | |
const healthStatus = { | |
status: isHealthy ? 'healthy' : 'degraded', | |
timestamp: new Date().toISOString(), | |
services: { | |
persistentImageLink: linkServiceStatus, | |
storage: storageStatus | |
}, | |
exportMethod: 'frontend-based', | |
browserDependencies: 'removed' | |
}; | |
if (isHealthy) { | |
res.json({ | |
success: true, | |
service: 'Frontend Export Service', | |
...healthStatus | |
}); | |
} else { | |
res.status(503).json({ | |
success: false, | |
service: 'Frontend Export Service', | |
...healthStatus | |
}); | |
} | |
} catch (error) { | |
console.error('Health check failed:', error); | |
res.status(503).json({ | |
success: false, | |
status: 'error', | |
service: 'Frontend Export Service', | |
error: error.message, | |
timestamp: new Date().toISOString() | |
}); | |
} | |
}); | |
export default router; |