CatPtain's picture
Upload 85 files
28e1dba verified
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;