Spaces:
Paused
Paused
| import express from 'express'; | |
| import { authenticateToken } from '../middleware/auth.js'; | |
| import persistentImageLinkService from '../services/persistentImageLinkService.js'; | |
| import githubService from '../services/githubService.js'; | |
| const router = express.Router(); | |
| // 添加路由级别的日志中间件 | |
| router.use((req, res, next) => { | |
| console.log(`Persistent Images Router - ${req.method} ${req.path}`); | |
| next(); | |
| }); | |
| /** | |
| * 获取或创建PPT页面的持久化链接 | |
| * POST /api/persistent-images/create | |
| */ | |
| router.post('/create', authenticateToken, async (req, res, next) => { | |
| try { | |
| const userId = req.user.userId; | |
| const { | |
| pptId, | |
| pageIndex, | |
| slideId, | |
| slideData, | |
| options = {} | |
| } = req.body; | |
| // 验证必需参数 - 需要slideId或pageIndex其中之一 | |
| if (!pptId || (slideId === undefined && pageIndex === undefined)) { | |
| return res.status(400).json({ | |
| error: 'Missing required parameters: pptId and (slideId or pageIndex)' | |
| }); | |
| } | |
| // 优先使用slideId,如果不存在则使用pageIndex | |
| const uniqueId = slideId || pageIndex; | |
| console.log(`🔗 Creating/getting persistent link for PPT ${pptId}, uniqueId ${uniqueId}`); | |
| // 获取或创建持久化链接 | |
| const result = await persistentImageLinkService.getOrCreatePersistentLink( | |
| userId, | |
| pptId, | |
| uniqueId, | |
| slideData, | |
| options | |
| ); | |
| res.json({ | |
| success: true, | |
| message: 'Persistent link created/retrieved successfully', | |
| ...result | |
| }); | |
| } catch (error) { | |
| console.error('❌ Failed to create persistent link:', error); | |
| next(error); | |
| } | |
| }); | |
| /** | |
| * 批量创建PPT所有页面的持久化链接 | |
| * POST /api/persistent-images/create-all | |
| */ | |
| router.post('/create-all', authenticateToken, async (req, res, next) => { | |
| try { | |
| const userId = req.user.userId; | |
| const { | |
| pptId, | |
| slides, | |
| options = {} | |
| } = req.body; | |
| // 验证必需参数 | |
| if (!pptId || !slides || !Array.isArray(slides)) { | |
| return res.status(400).json({ | |
| error: 'Missing required parameters: pptId, slides (array)' | |
| }); | |
| } | |
| console.log(`🔗 Creating persistent links for ${slides.length} pages of PPT ${pptId}`); | |
| // 批量创建持久化链接 | |
| const results = await persistentImageLinkService.updateAllPersistentLinks( | |
| userId, | |
| pptId, | |
| slides, | |
| options | |
| ); | |
| // 统计结果 | |
| const successCount = results.filter(r => r.success).length; | |
| const failureCount = results.length - successCount; | |
| console.log(`✅ Created ${successCount} persistent links, ${failureCount} failed`); | |
| // 转换结果格式以匹配前端期望的数据结构 | |
| const links = results.filter(r => r.success).map(result => ({ | |
| linkId: result.linkId, | |
| url: result.url, | |
| publicUrl: result.publicUrl, | |
| pageIndex: result.pageIndex, | |
| slideId: result.slideId, | |
| lastUpdated: result.lastUpdated | |
| })); | |
| res.json({ | |
| success: true, | |
| message: `Created ${successCount} persistent links successfully`, | |
| totalPages: slides.length, | |
| successCount, | |
| failureCount, | |
| links, | |
| results | |
| }); | |
| } catch (error) { | |
| console.error('❌ Failed to create persistent links:', error); | |
| next(error); | |
| } | |
| }); | |
| /** | |
| * 更新PPT页面图片 | |
| * PUT /api/persistent-images/:linkId | |
| */ | |
| router.put('/:linkId', authenticateToken, async (req, res, next) => { | |
| try { | |
| const { linkId } = req.params; | |
| const { | |
| slideData, | |
| options = {} | |
| } = req.body; | |
| // 验证必需参数 | |
| if (!slideData) { | |
| return res.status(400).json({ | |
| error: 'Missing required parameter: slideData' | |
| }); | |
| } | |
| console.log(`🔄 Updating image for persistent link: ${linkId}`); | |
| // 更新图片 | |
| await persistentImageLinkService.updatePageImage(linkId, slideData, options); | |
| res.json({ | |
| success: true, | |
| message: 'Image updated successfully', | |
| linkId, | |
| url: `/api/persistent-images/${linkId}` | |
| }); | |
| } catch (error) { | |
| console.error(`❌ Failed to update persistent link ${req.params.linkId}:`, error); | |
| next(error); | |
| } | |
| }); | |
| /** | |
| * 更新持久化链接的图片内容(通过图片数据) | |
| * POST /api/persistent-images/update-image | |
| */ | |
| router.post('/update-image', authenticateToken, async (req, res, next) => { | |
| try { | |
| const { | |
| linkId, | |
| imageData, | |
| format = 'jpeg' | |
| } = req.body; | |
| // 验证必需参数 | |
| if (!linkId || !imageData) { | |
| return res.status(400).json({ | |
| error: 'Missing required parameters: linkId, imageData' | |
| }); | |
| } | |
| console.log(`🔄 Updating image content for persistent link: ${linkId}`); | |
| // 将base64图片数据转换为Buffer | |
| let imageBuffer; | |
| try { | |
| // 移除data:image/jpeg;base64,前缀(如果存在) | |
| const base64Data = imageData.replace(/^data:image\/[a-z]+;base64,/, ''); | |
| imageBuffer = Buffer.from(base64Data, 'base64'); | |
| } catch (error) { | |
| return res.status(400).json({ | |
| error: 'Invalid image data format' | |
| }); | |
| } | |
| // 直接更新持久化链接的图片内容 | |
| const result = await persistentImageLinkService.updateImageContent(linkId, imageBuffer, format); | |
| if (!result.success) { | |
| return res.status(404).json({ | |
| error: 'Persistent link not found', | |
| linkId | |
| }); | |
| } | |
| res.json({ | |
| success: true, | |
| message: 'Image content updated successfully', | |
| linkId, | |
| url: `/api/persistent-images/${linkId}`, | |
| size: imageBuffer.length, | |
| format | |
| }); | |
| } catch (error) { | |
| console.error(`❌ Failed to update image content for link ${req.body.linkId}:`, error); | |
| next(error); | |
| } | |
| }); | |
| /** | |
| * 获取用户的所有持久化链接 | |
| * GET /api/persistent-images/user/list | |
| */ | |
| router.get('/user/list', authenticateToken, async (req, res, next) => { | |
| try { | |
| const userId = req.user.userId; | |
| const { pptId } = req.query; | |
| console.log(`📋 Getting persistent links for user ${userId}${pptId ? `, PPT ${pptId}` : ''}`); | |
| const userLinks = persistentImageLinkService.getUserPersistentLinks(userId, pptId); | |
| res.json({ | |
| success: true, | |
| userId, | |
| pptId: pptId || null, | |
| linkCount: userLinks.length, | |
| links: userLinks | |
| }); | |
| } catch (error) { | |
| console.error(`❌ Failed to get user persistent links:`, error); | |
| next(error); | |
| } | |
| }); | |
| /** | |
| * 获取服务统计信息 | |
| * GET /api/persistent-images/stats | |
| */ | |
| router.get('/stats', authenticateToken, async (req, res, next) => { | |
| try { | |
| console.log('📊 Getting persistent image service stats'); | |
| const stats = persistentImageLinkService.getStats(); | |
| res.json({ | |
| success: true, | |
| stats | |
| }); | |
| } catch (error) { | |
| console.error('❌ Failed to get persistent image stats:', error); | |
| next(error); | |
| } | |
| }); | |
| /** | |
| * 获取持久化链接的图片 (通过 pptId 和 slideId) | |
| * GET /api/persistent-images/:pptId/:slideId | |
| */ | |
| router.get('/:pptId/:slideId', async (req, res, next) => { | |
| try { | |
| const { pptId, slideId } = req.params; | |
| const { download } = req.query; | |
| // 验证pptId格式 (应该是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(pptId)) { | |
| return next(); // 不是UUID格式,继续到下一个路由 | |
| } | |
| // 验证slideId格式 (不应该包含文件扩展名) | |
| if (slideId.includes('.')) { | |
| return next(); // 包含文件扩展名,可能是静态文件请求 | |
| } | |
| console.log(`🖼️ Getting persistent image by pptId: ${pptId}, slideId: ${slideId}`); | |
| // 通过 pptId 和 slideId 查找对应的链接 | |
| const linkInfo = await persistentImageLinkService.findLinkByPptAndSlide(pptId, slideId); | |
| if (!linkInfo) { | |
| return res.status(404).json({ | |
| error: 'Persistent image not found for the specified PPT and slide', | |
| pptId, | |
| slideId | |
| }); | |
| } | |
| // 获取图片 | |
| const imageResult = await persistentImageLinkService.getPersistentImage(linkInfo.linkId); | |
| if (!imageResult.success) { | |
| return res.status(404).json({ | |
| error: 'Persistent image not found', | |
| pptId, | |
| slideId, | |
| linkId: linkInfo.linkId | |
| }); | |
| } | |
| const { data: imageBuffer, metadata } = imageResult; | |
| // 设置响应头 | |
| const contentType = metadata.format === 'jpg' ? 'image/jpeg' : `image/${metadata.format}`; | |
| res.set({ | |
| 'Content-Type': contentType, | |
| 'Content-Length': metadata.size, | |
| 'Cache-Control': 'public, max-age=3600', // 缓存1小时 | |
| 'ETag': `"${linkInfo.linkId}"`, | |
| 'Last-Modified': new Date(metadata.lastUpdated).toUTCString() | |
| }); | |
| // 如果是下载请求,设置下载头 | |
| if (download) { | |
| const fileName = `persistent-slide-${pptId}-${slideId}.${metadata.format}`; | |
| res.set('Content-Disposition', `attachment; filename="${fileName}"`); | |
| } | |
| // 检查条件请求 | |
| const ifNoneMatch = req.get('If-None-Match'); | |
| if (ifNoneMatch === `"${linkInfo.linkId}"`) { | |
| return res.status(304).end(); | |
| } | |
| res.send(imageBuffer); | |
| } catch (error) { | |
| console.error(`❌ Failed to get persistent image ${req.params.pptId}/${req.params.slideId}:`, error); | |
| if (error.message.includes('not found')) { | |
| return res.status(404).json({ | |
| error: 'Persistent image not found', | |
| pptId: req.params.pptId, | |
| slideId: req.params.slideId | |
| }); | |
| } | |
| next(error); | |
| } | |
| }); | |
| /** | |
| * 获取持久化链接的图片 (通过 pptId 或 linkId) | |
| * GET /api/persistent-images/:id | |
| */ | |
| router.get('/:id', async (req, res, next) => { | |
| try { | |
| const { id } = req.params; | |
| const { download } = req.query; | |
| console.log(`🖼️ Getting persistent image: ${id}`); | |
| // 检查是否是PPT ID格式(通常比linkId短,且可能包含特殊字符) | |
| // 如果是PPT ID,返回该PPT的第一页图片 | |
| let imageResult; | |
| let isLinkId = false; | |
| try { | |
| // 首先尝试作为linkId获取图片 | |
| imageResult = await persistentImageLinkService.getPersistentImage(id); | |
| isLinkId = true; | |
| } catch (error) { | |
| // 如果作为linkId失败,尝试作为PPT ID获取第一页图片 | |
| console.log(`⚠️ Not found as linkId, trying as PPT ID: ${id}`); | |
| // 查找该PPT的第一页链接 | |
| const links = []; | |
| for (const [linkId, linkInfo] of persistentImageLinkService.persistentLinks) { | |
| if (linkInfo.pptId === id && linkInfo.hasImage) { | |
| links.push({ linkId, pageIndex: linkInfo.pageIndex || 0 }); | |
| } | |
| } | |
| if (links.length === 0) { | |
| return res.status(404).json({ | |
| error: 'No persistent images found for PPT', | |
| pptId: id | |
| }); | |
| } | |
| // 按页面索引排序,取第一页 | |
| links.sort((a, b) => a.pageIndex - b.pageIndex); | |
| const firstPageLinkId = links[0].linkId; | |
| imageResult = await persistentImageLinkService.getPersistentImage(firstPageLinkId); | |
| } | |
| if (!imageResult.success) { | |
| return res.status(404).json({ | |
| error: 'Persistent image not found', | |
| id | |
| }); | |
| } | |
| const { data: imageBuffer, metadata } = imageResult; | |
| // 设置响应头 | |
| const contentType = metadata.format === 'jpg' ? 'image/jpeg' : `image/${metadata.format}`; | |
| res.set({ | |
| 'Content-Type': contentType, | |
| 'Content-Length': metadata.size, | |
| 'Cache-Control': 'public, max-age=3600', // 缓存1小时 | |
| 'ETag': `"${id}"`, | |
| 'Last-Modified': new Date(metadata.lastUpdated).toUTCString() | |
| }); | |
| // 如果是下载请求,设置下载头 | |
| if (download) { | |
| const fileName = isLinkId ? `persistent-slide-${id}.${metadata.format}` : `persistent-ppt-${id}.${metadata.format}`; | |
| res.set('Content-Disposition', `attachment; filename="${fileName}"`); | |
| } | |
| // 检查条件请求 | |
| const ifNoneMatch = req.get('If-None-Match'); | |
| if (ifNoneMatch === `"${id}"`) { | |
| return res.status(304).end(); | |
| } | |
| res.send(imageBuffer); | |
| } catch (error) { | |
| console.error(`❌ Failed to get persistent image ${req.params.id}:`, error); | |
| if (error.message.includes('not found')) { | |
| return res.status(404).json({ | |
| error: 'Persistent image not found', | |
| id: req.params.id | |
| }); | |
| } | |
| next(error); | |
| } | |
| }); | |
| /** | |
| * 删除持久化链接 | |
| * DELETE /api/persistent-images/:linkId | |
| */ | |
| router.delete('/:linkId', authenticateToken, async (req, res, next) => { | |
| try { | |
| const { linkId } = req.params; | |
| console.log(`🗑️ Deleting persistent link: ${linkId}`); | |
| // 删除持久化链接 | |
| const deleteResult = await persistentImageLinkService.deletePersistentLink(linkId); | |
| if (deleteResult) { | |
| res.json({ | |
| success: true, | |
| message: 'Persistent link deleted successfully', | |
| linkId | |
| }); | |
| } else { | |
| res.status(404).json({ | |
| error: 'Persistent link not found', | |
| linkId | |
| }); | |
| } | |
| } catch (error) { | |
| console.error(`❌ Failed to delete persistent link ${req.params.linkId}:`, error); | |
| next(error); | |
| } | |
| }); | |
| /** | |
| * 从PPT数据同步创建持久化链接 | |
| * POST /api/persistent-images/sync-from-ppt | |
| */ | |
| router.post('/sync-from-ppt', authenticateToken, async (req, res, next) => { | |
| try { | |
| const userId = req.user.userId; | |
| const { pptId } = req.body; | |
| // 验证必需参数 | |
| if (!pptId) { | |
| return res.status(400).json({ | |
| error: 'Missing required parameter: pptId' | |
| }); | |
| } | |
| console.log(`🔄 Syncing persistent links from PPT data: ${pptId}`); | |
| // 从GitHub服务获取PPT数据 | |
| const pptData = await githubService.getPPT(userId, pptId); | |
| if (!pptData || !pptData.slides) { | |
| return res.status(404).json({ | |
| error: 'PPT not found or has no slides', | |
| pptId | |
| }); | |
| } | |
| // 创建持久化链接(不生成图片,只创建链接) | |
| const results = []; | |
| for (let pageIndex = 0; pageIndex < pptData.slides.length; pageIndex++) { | |
| try { | |
| const result = await persistentImageLinkService.getOrCreatePersistentLink( | |
| userId, | |
| pptId, | |
| pageIndex, | |
| null, // 不提供slideData,只创建链接 | |
| {} | |
| ); | |
| results.push({ | |
| pageIndex, | |
| success: true, | |
| ...result | |
| }); | |
| } catch (error) { | |
| console.error(`❌ Failed to sync persistent link for page ${pageIndex}:`, error); | |
| results.push({ | |
| pageIndex, | |
| success: false, | |
| error: error.message | |
| }); | |
| } | |
| } | |
| const successCount = results.filter(r => r.success).length; | |
| const failureCount = results.length - successCount; | |
| console.log(`✅ Synced ${successCount} persistent links, ${failureCount} failed`); | |
| res.json({ | |
| success: true, | |
| message: `Synced ${successCount} persistent links from PPT data`, | |
| pptId, | |
| totalPages: pptData.slides.length, | |
| successCount, | |
| failureCount, | |
| results | |
| }); | |
| } catch (error) { | |
| console.error('❌ Failed to sync persistent links from PPT:', error); | |
| next(error); | |
| } | |
| }); | |
| export default router; |