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