CatPtain's picture
Upload 85 files
28e1dba verified
import express from 'express';
import { v4 as uuidv4 } from 'uuid';
import githubService from '../services/githubService.js';
const router = express.Router();
// 添加路由级别的日志中间件
router.use((req, res, next) => {
console.log(`PPT Router - ${req.method} ${req.path} - Body:`, Object.keys(req.body || {}));
next();
});
// 获取用户的PPT列表 - 优先从Huggingface硬盘读取
router.get('/list', async (req, res, next) => {
try {
const userId = req.user.userId;
// 优先从Huggingface存储服务获取PPT列表
const { default: huggingfaceStorageService } = await import('../services/huggingfaceStorageService.js');
let pptList = [];
try {
// 尝试从Huggingface硬盘读取PPT列表
pptList = await huggingfaceStorageService.getUserPPTList(userId);
console.log(`📁 Found ${pptList.length} PPTs from Huggingface storage for user ${userId}`);
} catch (hfError) {
console.warn('⚠️ Huggingface storage unavailable, falling back to GitHub:', hfError.message);
// 回退到GitHub存储
pptList = await githubService.getUserPPTList(userId);
}
res.json(pptList);
} catch (error) {
next(error);
}
});
// 获取指定PPT数据 - 优先从Huggingface硬盘读取
router.get('/:pptId', async (req, res, next) => {
try {
const userId = req.user.userId;
const { pptId } = req.params;
console.log(`🔍 Fetching PPT: ${pptId} for user: ${userId}`);
let pptData = null;
let foundInRepo = -1;
// 优先从Huggingface存储服务获取PPT数据
const { default: huggingfaceStorageService } = await import('../services/huggingfaceStorageService.js');
try {
// 尝试从Huggingface硬盘读取PPT数据
pptData = await huggingfaceStorageService.getPPTData(userId, pptId);
foundInRepo = 0;
console.log(`✅ PPT found in Huggingface storage`);
} catch (hfError) {
console.warn('⚠️ PPT not found in Huggingface storage, trying other sources:', hfError.message);
if (githubService.useMemoryStorage) {
// 内存存储模式
const result = await githubService.getPPTFromMemory(userId, pptId);
if (result && result.content) {
pptData = result.content;
foundInRepo = 0;
console.log(`✅ PPT found in memory storage`);
}
} else {
// GitHub存储模式 - 尝试所有仓库
console.log(`Available repositories: ${githubService.repositories.length}`);
for (let i = 0; i < githubService.repositories.length; i++) {
try {
console.log(`📂 Checking repository ${i}: ${githubService.repositories[i]}`);
const result = await githubService.getPPT(userId, pptId, i);
if (result && result.content) {
pptData = result.content;
foundInRepo = i;
console.log(`✅ PPT found in repository ${i}${result.isReassembled ? ' (reassembled from chunks)' : ''}`);
break;
}
} catch (error) {
console.log(`❌ PPT not found in repository ${i}: ${error.message}`);
continue;
}
}
}
}
if (!pptData) {
console.log(`❌ PPT ${pptId} not found for user ${userId}`);
return res.status(404).json({
error: 'PPT not found',
pptId: pptId,
userId: userId,
storageMode: githubService.useMemoryStorage ? 'memory' : 'github'
});
}
// 🔧 修复:标准化PPT数据格式,确保前端兼容性
const standardizedPptData = {
// 确保基本字段存在
id: pptData.id || pptData.pptId || pptId,
pptId: pptData.pptId || pptData.id || pptId,
title: pptData.title || '未命名演示文稿',
// 标准化slides数组
slides: Array.isArray(pptData.slides) ? pptData.slides.map((slide, index) => ({
id: slide.id || `slide-${index}`,
elements: Array.isArray(slide.elements) ? slide.elements : [],
background: slide.background || { type: 'solid', color: '#ffffff' },
...slide
})) : [],
// 标准化主题
theme: pptData.theme || {
backgroundColor: '#ffffff',
themeColor: '#d14424',
fontColor: '#333333',
fontName: 'Microsoft YaHei'
},
// 🔧 关键修复:确保视口信息正确传递
viewportSize: pptData.viewportSize || 1000,
viewportRatio: pptData.viewportRatio || 0.5625,
// 时间戳
createdAt: pptData.createdAt || new Date().toISOString(),
updatedAt: pptData.updatedAt || new Date().toISOString(),
// 保留其他可能的属性
...pptData
};
// 🔧 新增:数据验证和修复
if (standardizedPptData.slides.length === 0) {
console.log(`⚠️ PPT ${pptId} has no slides, creating default slide`);
standardizedPptData.slides = [{
id: 'default-slide',
elements: [],
background: { type: 'solid', color: '#ffffff' }
}];
}
// 验证视口比例的合理性
if (standardizedPptData.viewportRatio <= 0 || standardizedPptData.viewportRatio > 2) {
console.log(`⚠️ Invalid viewportRatio ${standardizedPptData.viewportRatio}, resetting to 0.5625`);
standardizedPptData.viewportRatio = 0.5625;
}
// 验证视口尺寸的合理性
if (standardizedPptData.viewportSize <= 0 || standardizedPptData.viewportSize > 2000) {
console.log(`⚠️ Invalid viewportSize ${standardizedPptData.viewportSize}, resetting to 1000`);
standardizedPptData.viewportSize = 1000;
}
console.log(`✅ Successfully found and standardized PPT ${pptId}:`, {
slidesCount: standardizedPptData.slides.length,
viewportSize: standardizedPptData.viewportSize,
viewportRatio: standardizedPptData.viewportRatio,
storageMode: githubService.useMemoryStorage ? 'memory' : `repository ${foundInRepo}`
});
res.json(standardizedPptData);
} catch (error) {
console.error(`❌ Error fetching PPT ${req.params.pptId}:`, error);
next(error);
}
});
// 保存PPT数据 - 新架构版本
router.post('/save', async (req, res, next) => {
try {
const userId = req.user.userId;
const { pptId, title, slides, theme, viewportSize, viewportRatio } = req.body;
console.log(`🔄 Starting PPT save for user ${userId}, PPT ID: ${pptId}`);
console.log(`📊 Request body analysis:`, {
pptId: !!pptId,
title: title?.length || 0,
slidesCount: Array.isArray(slides) ? slides.length : 'not array',
theme: !!theme,
requestSize: req.get('content-length'),
storageMode: githubService.useMemoryStorage ? 'memory' : 'github'
});
if (!pptId || !slides) {
console.log(`❌ Missing required fields - pptId: ${!!pptId}, slides: ${!!slides}`);
return res.status(400).json({ error: 'PPT ID and slides are required' });
}
// 验证slides数组
if (!Array.isArray(slides) || slides.length === 0) {
console.log(`❌ Invalid slides array - isArray: ${Array.isArray(slides)}, length: ${slides?.length || 0}`);
return res.status(400).json({ error: 'Slides must be a non-empty array' });
}
// 构建标准化的PPT数据结构
const pptData = {
id: pptId,
title: title || '未命名演示文稿',
slides: slides.map((slide, index) => ({
id: slide.id || `slide-${index}`,
elements: Array.isArray(slide.elements) ? slide.elements : [],
background: slide.background || { type: 'solid', color: '#ffffff' },
...slide
})),
theme: theme || {
backgroundColor: '#ffffff',
themeColor: '#d14424',
fontColor: '#333333',
fontName: 'Microsoft YaHei'
},
viewportSize: viewportSize || 1000,
viewportRatio: viewportRatio || 0.5625,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
};
// 计算文件大小分析
const jsonData = JSON.stringify(pptData);
const totalDataSize = Buffer.byteLength(jsonData, 'utf8');
console.log(`📊 PPT data analysis:`);
console.log(` - Total slides: ${slides.length}`);
console.log(` - Total data size: ${totalDataSize} bytes (${(totalDataSize / 1024).toFixed(2)} KB)`);
console.log(` - Average slide size: ${(totalDataSize / slides.length / 1024).toFixed(2)} KB`);
// 分析每个slide的大小
const slideAnalysis = slides.map((slide, index) => {
const slideJson = JSON.stringify(slide);
const slideSize = Buffer.byteLength(slideJson, 'utf8');
return {
index,
size: slideSize,
sizeKB: (slideSize / 1024).toFixed(2),
elementsCount: slide.elements?.length || 0,
hasLargeContent: slideSize > 800 * 1024 // 800KB+
};
});
const largeSlides = slideAnalysis.filter(s => s.hasLargeContent);
const maxSlideSize = Math.max(...slideAnalysis.map(s => s.size));
console.log(`📋 Slide size analysis:`);
console.log(` - Largest slide: ${(maxSlideSize / 1024).toFixed(2)} KB`);
console.log(` - Large slides (>800KB): ${largeSlides.length}`);
if (largeSlides.length > 0) {
console.log(` - Large slide indices: ${largeSlides.map(s => s.index).join(', ')}`);
}
try {
let result;
// 优先保存到Huggingface存储服务
const { default: huggingfaceStorageService } = await import('../services/huggingfaceStorageService.js');
try {
// 尝试保存到Huggingface硬盘
result = await huggingfaceStorageService.storePPTData(userId, pptId, pptData);
console.log(`✅ PPT saved to Huggingface storage:`, result);
} catch (hfError) {
console.warn('⚠️ Failed to save to Huggingface storage, falling back to other storage:', hfError.message);
console.log(`💾 Using fallback storage mode: ${githubService.useMemoryStorage ? 'memory' : 'GitHub folder architecture'}`);
if (githubService.useMemoryStorage) {
// 内存存储模式
result = await githubService.savePPTToMemory(userId, pptId, pptData);
console.log(`✅ PPT saved to memory storage:`, result);
} else {
// GitHub存储模式 - 使用新的文件夹架构
console.log(`🐙 Using GitHub folder storage for ${pptId}`);
console.log(`📂 Available repositories: ${githubService.repositories?.length || 0}`);
if (!githubService.repositories || githubService.repositories.length === 0) {
throw new Error('No GitHub repositories configured');
}
console.log(`🔍 Pre-save validation:`);
console.log(` - Repository 0: ${githubService.repositories[0]}`);
console.log(` - Token configured: ${!!githubService.token}`);
console.log(` - API URL: ${githubService.apiUrl}`);
result = await githubService.savePPT(userId, pptId, pptData, 0);
console.log(`✅ PPT saved to GitHub folder storage:`, result);
}
}
console.log(`✅ PPT save completed successfully:`, {
pptId,
userId,
slideCount: slides.length,
totalDataSize,
storageType: result.storage || 'unknown',
compressionApplied: result.compressionSummary?.compressedSlides > 0,
storageMode: githubService.useMemoryStorage ? 'memory' : 'github'
});
const response = {
message: 'PPT saved successfully',
pptId,
slidesCount: slides.length,
savedAt: new Date().toISOString(),
totalFileSize: `${(totalDataSize / 1024).toFixed(2)} KB`,
storageType: result.storage || 'unknown',
storageMode: githubService.useMemoryStorage ? 'memory' : 'github',
architecture: 'folder-based',
slideAnalysis: {
totalSlides: slides.length,
largestSlideSize: `${(maxSlideSize / 1024).toFixed(2)} KB`,
largeSlides: largeSlides.length,
averageSlideSize: `${(totalDataSize / slides.length / 1024).toFixed(2)} KB`
}
};
// 添加压缩信息
if (result.compressionSummary) {
response.compression = {
applied: result.compressionSummary.compressedSlides > 0,
compressedSlides: result.compressionSummary.compressedSlides,
totalSlides: result.compressionSummary.totalSlides,
savedSlides: result.compressionSummary.savedSlides,
failedSlides: result.compressionSummary.failedSlides,
originalSize: `${(result.compressionSummary.totalOriginalSize / 1024).toFixed(2)} KB`,
finalSize: `${(result.compressionSummary.totalFinalSize / 1024).toFixed(2)} KB`,
savedSpace: `${((result.compressionSummary.totalOriginalSize - result.compressionSummary.totalFinalSize) / 1024).toFixed(2)} KB`,
compressionRatio: `${(result.compressionSummary.compressionRatio * 100).toFixed(1)}%`
};
if (result.compressionSummary.compressedSlides > 0) {
response.message = `PPT saved successfully with ${result.compressionSummary.compressedSlides} slides optimized for storage`;
response.optimization = `Automatic compression applied to ${result.compressionSummary.compressedSlides} large slides, saving ${response.compression.savedSpace}`;
}
// 添加保存警告信息
if (result.compressionSummary.failedSlides > 0) {
response.warning = `${result.compressionSummary.failedSlides} slides failed to save`;
response.partialSave = true;
response.savedSlides = result.compressionSummary.savedSlides;
response.failedSlides = result.compressionSummary.failedSlides;
if (result.compressionSummary.errors) {
response.slideErrors = result.compressionSummary.errors;
}
}
}
// 添加存储路径信息
if (result.folderPath) {
response.storagePath = result.folderPath;
response.architecture = 'folder-based (meta.json + individual slide files)';
}
// 添加警告信息
if (result.warnings) {
response.warnings = result.warnings;
}
// 记录PPT修改,用于自动备份
try {
const { default: autoBackupService } = await import('../services/autoBackupService.js');
autoBackupService.recordPPTModification(userId, pptId);
} catch (backupError) {
console.warn('⚠️ Failed to record PPT modification for auto backup:', backupError.message);
}
// 🔄 新增:自动更新持久化图片链接
try {
const { default: persistentImageLinkService } = await import('../services/persistentImageLinkService.js');
// 异步更新所有页面的持久化链接(不阻塞响应)
setImmediate(async () => {
try {
console.log(`🔄 Starting persistent image links update for PPT ${pptId}`);
const updateResults = await persistentImageLinkService.updateAllPersistentLinksWithFrontendExport(
userId,
pptId,
slides,
{
format: 'jpg',
quality: 0.9,
viewportSize: viewportSize || 1000,
viewportRatio: viewportRatio || 0.5625
}
);
const successCount = updateResults.filter(r => r.success).length;
const failCount = updateResults.filter(r => !r.success).length;
console.log(`✅ Persistent image links update completed: ${successCount} success, ${failCount} failed`);
if (failCount > 0) {
console.warn(`⚠️ Some persistent image links failed to update:`,
updateResults.filter(r => !r.success).map(r => `Page ${r.pageIndex}: ${r.error}`)
);
}
} catch (updateError) {
console.error('❌ Failed to update persistent image links:', updateError.message);
}
});
// 添加持久化链接信息到响应
response.persistentLinks = {
enabled: true,
updateTriggered: true,
message: 'Persistent image links update started in background'
};
} catch (persistentError) {
console.warn('⚠️ Failed to trigger persistent image links update:', persistentError.message);
response.persistentLinks = {
enabled: false,
error: persistentError.message
};
}
res.json(response);
} catch (saveError) {
console.error(`❌ Save operation failed:`, {
error: saveError.message,
stack: saveError.stack,
userId,
pptId,
slidesCount: slides.length,
totalDataSize,
errorName: saveError.name,
errorCode: saveError.code
});
let errorResponse = {
error: 'PPT save failed',
details: saveError.message,
pptId: pptId,
slidesCount: slides.length,
totalFileSize: `${(totalDataSize / 1024).toFixed(2)} KB`,
timestamp: new Date().toISOString(),
slideAnalysis: {
largestSlideSize: `${(maxSlideSize / 1024).toFixed(2)} KB`,
largeSlides: largeSlides.length
}
};
// 根据错误类型提供具体建议
if (saveError.message.includes('File too large') || saveError.message.includes('exceeds')) {
errorResponse.error = 'Slide file too large for storage';
errorResponse.suggestion = 'Some slides are too large even after compression. Try reducing image sizes or slide complexity.';
errorResponse.limits = {
maxSlideSize: '999 KB per slide',
largestSlideSize: `${(maxSlideSize / 1024).toFixed(2)} KB`,
oversizeSlides: largeSlides.length
};
return res.status(413).json(errorResponse);
}
if (saveError.message.includes('Failed to compress slide')) {
errorResponse.error = 'Slide compression failed';
errorResponse.suggestion = 'Unable to compress slides to acceptable size. Try manually reducing content complexity.';
errorResponse.compressionError = true;
return res.status(500).json(errorResponse);
}
if (saveError.message.includes('GitHub API') || saveError.message.includes('GitHub')) {
errorResponse.error = 'GitHub storage service error';
errorResponse.suggestion = 'Temporary GitHub service issue. Please try again in a few minutes.';
errorResponse.githubError = true;
return res.status(502).json(errorResponse);
}
if (saveError.message.includes('No GitHub repositories')) {
errorResponse.error = 'Storage configuration error';
errorResponse.suggestion = 'GitHub repositories not properly configured. Please contact support.';
errorResponse.configError = true;
return res.status(500).json(errorResponse);
}
// 网络相关错误
if (saveError.code === 'ETIMEDOUT' || saveError.message.includes('timeout')) {
errorResponse.error = 'Storage operation timeout';
errorResponse.suggestion = 'The save operation took too long. Try reducing file size or try again later.';
errorResponse.timeoutError = true;
return res.status(504).json(errorResponse);
}
// 默认错误
errorResponse.suggestion = 'Unknown save error. Please try again or contact support if the problem persists.';
errorResponse.unknownError = true;
return res.status(500).json(errorResponse);
}
} catch (error) {
console.error(`❌ PPT save failed for user ${req.user?.userId}, PPT ID: ${req.body?.pptId}:`, {
error: error.message,
stack: error.stack,
userId: req.user?.userId,
pptId: req.body?.pptId,
requestSize: req.get('content-length'),
slidesCount: req.body?.slides?.length,
errorName: error.name,
errorCode: error.code
});
// 提供更详细的错误处理
let errorResponse = {
error: 'PPT save processing failed',
details: error.message,
timestamp: new Date().toISOString(),
userId: req.user?.userId,
pptId: req.body?.pptId,
architecture: 'folder-based'
};
if (error.message.includes('Invalid slide data')) {
errorResponse.error = 'Invalid slide data detected';
errorResponse.suggestion = 'One or more slides contain invalid or corrupted data';
return res.status(400).json(errorResponse);
}
if (error.message.includes('JSON')) {
errorResponse.error = 'Invalid data format';
errorResponse.suggestion = 'Check slide data structure and content';
return res.status(400).json(errorResponse);
}
errorResponse.suggestion = 'Unknown processing error. Please try again or contact support if the problem persists.';
next(error);
}
});
// 创建新PPT
router.post('/create', async (req, res, next) => {
try {
const userId = req.user.userId;
const { title } = req.body;
if (!title) {
return res.status(400).json({ error: 'Title is required' });
}
const pptId = uuidv4();
const now = new Date().toISOString();
// 确保数据格式与前端store一致
const pptData = {
id: pptId,
title,
theme: {
backgroundColor: '#ffffff',
themeColor: '#d14424',
fontColor: '#333333',
fontName: 'Microsoft YaHei'
},
slides: [
{
id: uuidv4(),
elements: [
{
type: 'text',
id: uuidv4(),
left: 150,
top: 200,
width: 600,
height: 100,
content: title,
fontSize: 32,
fontName: 'Microsoft YaHei',
defaultColor: '#333333',
bold: true,
align: 'center'
}
],
background: {
type: 'solid',
color: '#ffffff'
}
}
],
// 确保视口信息与前端一致
viewportSize: 1000,
viewportRatio: 0.5625,
createdAt: now,
updatedAt: now
};
const fileName = `${pptId}.json`;
// 优先使用Huggingface存储服务保存PPT数据
const { default: huggingfaceStorageService } = await import('../services/huggingfaceStorageService.js');
try {
// 尝试保存到Huggingface存储
await huggingfaceStorageService.storePPTData(userId, pptId, pptData);
console.log(`✅ PPT created and saved to Huggingface storage: ${pptId}`);
} catch (hfError) {
console.warn('⚠️ Huggingface storage unavailable, falling back to GitHub:', hfError.message);
// 回退到GitHub存储
console.log(`Creating PPT for user ${userId}, using ${githubService.useMemoryStorage ? 'memory' : 'GitHub'} storage`);
if (githubService.useMemoryStorage) {
await githubService.saveToMemory(userId, fileName, pptData);
} else {
await githubService.saveFile(userId, fileName, pptData);
}
}
console.log(`PPT created successfully: ${pptId}`);
res.json({ success: true, pptId, pptData });
} catch (error) {
console.error('PPT creation error:', error);
next(error);
}
});
// 删除PPT
router.delete('/:pptId', async (req, res, next) => {
try {
const userId = req.user.userId;
const { pptId } = req.params;
const fileName = `${pptId}.json`;
let deleted = false;
// 优先从Huggingface存储删除
const { default: huggingfaceStorageService } = await import('../services/huggingfaceStorageService.js');
try {
await huggingfaceStorageService.deletePPTData(userId, pptId);
deleted = true;
console.log(`✅ PPT deleted from Huggingface storage: ${pptId}`);
} catch (hfError) {
console.warn('⚠️ Failed to delete from Huggingface storage, trying other storage:', hfError.message);
if (githubService.useMemoryStorage) {
// 内存存储模式
deleted = githubService.memoryStorage.delete(`users/${userId}/${fileName}`);
if (deleted) {
console.log(`✅ PPT deleted from memory storage: ${pptId}`);
}
} else {
// GitHub存储模式 - 从所有仓库中删除
if (!githubService.repositories || githubService.repositories.length === 0) {
console.warn('⚠️ GitHub repositories not configured, cannot delete from GitHub storage');
} else {
for (let i = 0; i < githubService.repositories.length; i++) {
try {
await githubService.deleteFile(userId, fileName, i);
deleted = true;
console.log(`✅ PPT deleted from GitHub storage: ${pptId}`);
break; // 只需要从一个仓库删除成功即可
} catch (error) {
// 继续尝试其他仓库
continue;
}
}
}
}
}
if (!deleted) {
return res.status(404).json({ error: 'PPT not found in any storage' });
}
res.json({ message: 'PPT deleted successfully' });
} catch (error) {
next(error);
}
});
// 复制PPT
router.post('/:pptId/copy', async (req, res, next) => {
try {
const userId = req.user.userId;
const { pptId } = req.params;
const { title } = req.body;
const sourceFileName = `${pptId}.json`;
// 获取源PPT数据 - 优先从Huggingface存储读取
let sourcePPT = null;
const { default: huggingfaceStorageService } = await import('../services/huggingfaceStorageService.js');
try {
// 尝试从Huggingface存储读取
sourcePPT = await huggingfaceStorageService.getPPTData(userId, pptId);
console.log('✅ Source PPT loaded from Huggingface storage for copying');
} catch (hfError) {
console.warn('⚠️ Failed to load source PPT from Huggingface storage, trying other storage:', hfError.message);
if (githubService.useMemoryStorage) {
// 内存存储模式
sourcePPT = await githubService.getFromMemory(userId, sourceFileName);
if (sourcePPT) {
console.log('✅ Source PPT loaded from memory storage for copying');
}
} else {
// GitHub存储模式
if (!githubService.repositories || githubService.repositories.length === 0) {
console.warn('⚠️ GitHub repositories not configured, cannot load from GitHub storage');
} else {
for (let i = 0; i < githubService.repositories.length; i++) {
try {
const result = await githubService.getFile(userId, sourceFileName, i);
if (result) {
sourcePPT = result;
console.log('✅ Source PPT loaded from GitHub storage for copying');
break;
}
} catch (error) {
continue;
}
}
}
}
}
if (!sourcePPT) {
return res.status(404).json({ error: 'Source PPT not found' });
}
// 创建新的PPT ID和数据
const newPptId = uuidv4();
const newFileName = `${newPptId}.json`;
const newPPTData = {
...sourcePPT,
id: newPptId,
title: title || `${sourcePPT.title} - 副本`,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
};
// 保存复制的PPT - 优先保存到Huggingface存储
try {
await huggingfaceStorageService.storePPTData(userId, newPptId, newPPTData);
console.log(`✅ Copied PPT saved to Huggingface storage: ${newPptId}`);
} catch (hfError) {
console.warn('⚠️ Failed to save copied PPT to Huggingface storage, using fallback:', hfError.message);
if (githubService.useMemoryStorage) {
await githubService.saveToMemory(userId, newFileName, newPPTData);
console.log(`✅ Copied PPT saved to memory storage: ${newPptId}`);
} else {
await githubService.saveFile(userId, newFileName, newPPTData, 0);
console.log(`✅ Copied PPT saved to GitHub storage: ${newPptId}`);
}
}
res.json({
message: 'PPT copied successfully',
pptId: newPptId,
ppt: newPPTData
});
} catch (error) {
next(error);
}
});
export default router;