Spaces:
Running
Running
import express from 'express'; | |
import cors from 'cors'; | |
import helmet from 'helmet'; | |
import rateLimit from 'express-rate-limit'; | |
import dotenv from 'dotenv'; | |
import path from 'path'; | |
import { fileURLToPath } from 'url'; | |
import axios from 'axios'; | |
import fs from 'fs/promises'; // 添加ES模块fs导入 | |
const __filename = fileURLToPath(import.meta.url); | |
const __dirname = path.dirname(__filename); | |
// 首先加载环境变量 - 必须在导入服务之前 | |
dotenv.config({ path: path.join(__dirname, '../../.env') }); | |
// 验证环境变量是否正确加载 | |
console.log('=== Environment Variables Check ==='); | |
console.log('GITHUB_TOKEN configured:', !!process.env.GITHUB_TOKEN); | |
console.log('GITHUB_TOKEN length:', process.env.GITHUB_TOKEN ? process.env.GITHUB_TOKEN.length : 0); | |
console.log('GITHUB_REPOS configured:', !!process.env.GITHUB_REPOS); | |
console.log('GITHUB_REPOS value:', process.env.GITHUB_REPOS); | |
// 现在导入需要环境变量的模块 | |
import authRoutes from './routes/auth.js'; | |
import pptRoutes from './routes/ppt.js'; | |
import publicRoutes from './routes/public.js'; | |
import imagesRoutes from './routes/images.js'; | |
import persistentImagesRoutes from './routes/persistentImages.js'; | |
import persistentLinksRoutes from './routes/persistentLinks.js'; | |
import exportRoutes from './routes/export.js'; | |
import { authenticateToken } from './middleware/auth.js'; | |
import { errorHandler } from './middleware/errorHandler.js'; | |
// 导入新的服务 | |
import huggingfaceStorageService from './services/huggingfaceStorageService.js'; | |
import backupSchedulerService from './services/backupSchedulerService.js'; | |
import persistentImageLinkService from './services/persistentImageLinkService.js'; | |
import autoBackupService from './services/autoBackupService.js'; | |
import githubService from './services/githubService.js'; | |
const app = express(); | |
const PORT = process.env.PORT || 7861; // 修改为7861端口 | |
// 设置trust proxy用于Huggingface Space | |
app.set('trust proxy', true); | |
// 安全中间件 | |
app.use(helmet({ | |
contentSecurityPolicy: false, // 为了兼容前端静态文件 | |
})); | |
// 修复限流配置 - 针对Huggingface Space生产环境 | |
const limiter = rateLimit({ | |
windowMs: 15 * 60 * 1000, // 15分钟 | |
max: 100, // 每个IP每15分钟最多100个请求 | |
message: 'Too many requests from this IP, please try again later.', | |
trustProxy: true, // 与Express trust proxy设置保持一致 | |
validate: { | |
trustProxy: false // 禁用trust proxy验证以避免生产环境警告 | |
}, | |
standardHeaders: true, | |
legacyHeaders: false | |
}); | |
// 应用限流中间件 | |
app.use('/api', limiter); | |
// CORS配置 | |
app.use(cors({ | |
origin: process.env.FRONTEND_URL || '*', | |
credentials: true | |
})); | |
app.use(express.json({ limit: '50mb' })); | |
app.use(express.urlencoded({ extended: true, limit: '50mb' })); | |
// 提供前端静态文件 - 修复路径配置,避免与API路由冲突 | |
const frontendDistPath = path.join(__dirname, '../../frontend/dist'); | |
console.log('Frontend dist path:', frontendDistPath); | |
// API路由 - 必须在静态文件服务之前注册 | |
console.log('Registering API routes...'); | |
// 认证相关路由(不需要认证) | |
app.use('/api/auth', authRoutes); | |
// 公共访问路由(不需要认证) | |
app.use('/api/public', publicRoutes); | |
// 添加测试路由来验证 PPT 路由是否工作(不需要认证) | |
app.get('/api/ppt/test', (req, res) => { | |
res.json({ message: 'PPT routes are working', timestamp: new Date().toISOString() }); | |
}); | |
// PPT管理路由(需要认证) | |
app.use('/api/ppt', (req, res, next) => { | |
console.log(`PPT route accessed: ${req.method} ${req.path}`); | |
next(); | |
}, authenticateToken, pptRoutes); | |
// 图片管理路由(部分需要认证) | |
app.use('/api/images', imagesRoutes); | |
// 持久化图片链接路由 | |
app.use('/api/persistent-images', persistentImagesRoutes); | |
// 持久化链接管理路由(需要认证) | |
app.use('/api/persistent-links', persistentLinksRoutes); | |
// 添加 /api/persistent/ 路由来处理前端生成的永久链接格式 | |
app.get('/api/persistent/:pptId/:slideId', async (req, res) => { | |
try { | |
const { pptId, slideId } = req.params; | |
console.log(`🔗 Accessing persistent link: pptId=${pptId}, slideId=${slideId}`); | |
// 通过 pptId 和 slideId 查找对应的 linkId | |
const linkInfo = await persistentImageLinkService.findLinkByPptAndSlide(pptId, slideId); | |
if (!linkInfo) { | |
console.log(`❌ No persistent link found for pptId=${pptId}, slideId=${slideId}`); | |
return res.status(404).json({ error: 'Persistent link not found' }); | |
} | |
// 检查图片是否存在,如果不存在则生成占位图片 | |
let imageResult; | |
try { | |
imageResult = await persistentImageLinkService.getPersistentImage(linkInfo.linkId); | |
} catch (error) { | |
if (error.message.includes('Image not available')) { | |
console.log(`⚠️ Image not available for linkId=${linkInfo.linkId}, generating placeholder`); | |
// 生成占位图片 | |
const placeholderBuffer = await persistentImageLinkService.generatePlaceholderImage({ | |
width: 1920, | |
height: 1080, | |
format: 'jpeg' | |
}); | |
// 保存占位图片到文件系统 | |
const imagePath = path.join(persistentImageLinkService.linksDir, `${linkInfo.linkId}.jpeg`); | |
await fs.writeFile(imagePath, placeholderBuffer); | |
// 更新链接信息 | |
linkInfo.hasImage = true; | |
linkInfo.imagePath = imagePath; | |
linkInfo.format = 'jpeg'; | |
linkInfo.imageSize = placeholderBuffer.length; | |
linkInfo.lastUpdated = new Date().toISOString(); | |
// 保存更新后的链接映射 | |
await persistentImageLinkService.saveLinkMap(); | |
imageResult = { | |
success: true, | |
data: placeholderBuffer, | |
metadata: { | |
linkId: linkInfo.linkId, | |
format: 'jpeg', | |
size: placeholderBuffer.length, | |
createdAt: linkInfo.createdAt, | |
lastUpdated: linkInfo.lastUpdated | |
} | |
}; | |
} else { | |
throw error; | |
} | |
} | |
if (!imageResult || !imageResult.success) { | |
console.log(`❌ Failed to get image for linkId=${linkInfo.linkId}`); | |
return res.status(404).json({ error: 'Image not found' }); | |
} | |
// 设置正确的 Content-Type | |
const format = imageResult.metadata?.format || linkInfo.format || 'jpeg'; | |
res.setHeader('Content-Type', `image/${format}`); | |
res.setHeader('Cache-Control', 'public, max-age=31536000'); // 缓存1年 | |
console.log(`✅ Serving persistent image: linkId=${linkInfo.linkId}, format=${format}`); | |
res.send(imageResult.data); | |
} catch (error) { | |
console.error('❌ Error serving persistent image:', error); | |
res.status(500).json({ error: 'Internal server error', message: error.message }); | |
} | |
}); | |
// 导出功能路由 | |
app.use('/api/export', exportRoutes); | |
console.log('All API routes registered successfully'); | |
// 添加调试中间件 - 处理未匹配的API路由 | |
app.use('/api/*', (req, res) => { | |
console.log(`Unmatched API route: ${req.method} ${req.path}`); | |
res.status(404).json({ error: 'API route not found', path: req.path }); | |
}); | |
// 静态文件服务 - 在API路由之后 | |
app.use((req, res, next) => { | |
// 如果请求路径以 /api/ 开头,跳过静态文件服务 | |
if (req.path.startsWith('/api/')) { | |
return next(); | |
} | |
// 否则使用静态文件服务 | |
express.static(frontendDistPath)(req, res, next); | |
}); | |
// 提供数据文件 | |
app.use('/data', express.static(path.join(__dirname, '../../frontend/public/mocks'))); | |
// 健康检查 - 需要在其他路由之前 | |
app.get('/api/health', (req, res) => { | |
res.json({ status: 'ok', timestamp: new Date().toISOString() }); | |
}); | |
// GitHub连接状态检查 | |
app.get('/api/github/status', async (req, res) => { | |
try { | |
const { default: githubService } = await import('./services/githubService.js'); | |
const validation = await githubService.validateConnection(); | |
res.json({ | |
github: validation, | |
environment: { | |
tokenConfigured: !!process.env.GITHUB_TOKEN, | |
tokenPreview: process.env.GITHUB_TOKEN ? `${process.env.GITHUB_TOKEN.substring(0, 8)}...` : 'Not set', | |
reposConfigured: !!process.env.GITHUB_REPOS, | |
reposList: process.env.GITHUB_REPOS ? process.env.GITHUB_REPOS.split(',') : [], | |
nodeEnv: process.env.NODE_ENV | |
} | |
}); | |
} catch (error) { | |
res.status(500).json({ | |
error: error.message, | |
stack: error.stack | |
}); | |
} | |
}); | |
// 添加GitHub测试路由 | |
app.get('/api/github/test', async (req, res) => { | |
try { | |
console.log('=== GitHub Connection Test ==='); | |
console.log('GITHUB_TOKEN exists:', !!process.env.GITHUB_TOKEN); | |
console.log('GITHUB_REPOS:', process.env.GITHUB_REPOS); | |
const { default: githubService } = await import('./services/githubService.js'); | |
// 测试基本配置 | |
const config = { | |
hasToken: !!githubService.token, | |
useMemoryStorage: githubService.useMemoryStorage, | |
repositories: githubService.repositories, | |
apiUrl: githubService.apiUrl | |
}; | |
console.log('GitHub Service Config:', config); | |
// 如果有token,测试连接 | |
let connectionTest = null; | |
if (githubService.token) { | |
console.log('Testing GitHub API connection...'); | |
connectionTest = await githubService.validateConnection(); | |
console.log('Connection test result:', connectionTest); | |
} | |
res.json({ | |
timestamp: new Date().toISOString(), | |
config, | |
connectionTest, | |
environment: { | |
tokenLength: process.env.GITHUB_TOKEN ? process.env.GITHUB_TOKEN.length : 0, | |
nodeEnv: process.env.NODE_ENV | |
} | |
}); | |
} catch (error) { | |
console.error('GitHub test error:', error); | |
res.status(500).json({ | |
error: error.message, | |
stack: process.env.NODE_ENV === 'development' ? error.stack : undefined | |
}); | |
} | |
}); | |
// 添加GitHub调试路由 | |
app.get('/api/debug/github', async (req, res) => { | |
try { | |
console.log('=== GitHub Debug Information ==='); | |
const { default: githubService } = await import('./services/githubService.js'); | |
const debugInfo = { | |
timestamp: new Date().toISOString(), | |
environment: { | |
tokenConfigured: !!process.env.GITHUB_TOKEN, | |
tokenLength: process.env.GITHUB_TOKEN ? process.env.GITHUB_TOKEN.length : 0, | |
reposConfigured: !!process.env.GITHUB_REPOS, | |
reposList: process.env.GITHUB_REPOS ? process.env.GITHUB_REPOS.split(',') : [], | |
nodeEnv: process.env.NODE_ENV | |
}, | |
service: { | |
hasToken: !!githubService.token, | |
repositoriesCount: githubService.repositories?.length || 0, | |
repositories: githubService.repositories || [], | |
apiUrl: githubService.apiUrl | |
} | |
}; | |
// 测试连接 | |
try { | |
const connectionTest = await githubService.validateConnection(); | |
debugInfo.connectionTest = connectionTest; | |
} catch (connError) { | |
debugInfo.connectionError = connError.message; | |
} | |
console.log('Debug info:', debugInfo); | |
res.json(debugInfo); | |
} catch (error) { | |
console.error('Debug route error:', error); | |
res.status(500).json({ | |
error: error.message, | |
stack: process.env.NODE_ENV === 'development' ? error.stack : undefined | |
}); | |
} | |
}); | |
// 添加PPT调试路由 | |
app.get('/api/debug/ppt/:userId', async (req, res) => { | |
try { | |
const { userId } = req.params; | |
console.log(`=== PPT Debug for User: ${userId} ===`); | |
const { default: githubService } = await import('./services/githubService.js'); | |
const debugInfo = { | |
timestamp: new Date().toISOString(), | |
userId: userId, | |
repositories: [] | |
}; | |
// Check if GitHub repositories are configured | |
if (!githubService.repositories || githubService.repositories.length === 0) { | |
return res.json({ | |
...debugInfo, | |
error: 'GitHub repositories not configured', | |
details: 'No GitHub repositories available for PPT storage' | |
}); | |
} | |
// 检查每个仓库中用户的文件 | |
for (let i = 0; i < githubService.repositories.length; i++) { | |
const repoInfo = { | |
index: i, | |
url: githubService.repositories[i], | |
accessible: false, | |
userDirectoryExists: false, | |
files: [] | |
}; | |
try { | |
const { owner, repo } = githubService.parseRepoUrl(githubService.repositories[i]); | |
// 检查仓库是否可访问 | |
await axios.get(`https://api.github.com/repos/${owner}/${repo}`, { | |
headers: { | |
'Authorization': `token ${githubService.token}`, | |
'Accept': 'application/vnd.github.v3+json' | |
} | |
}); | |
repoInfo.accessible = true; | |
// 检查用户目录 | |
try { | |
const userDirResponse = await axios.get( | |
`https://api.github.com/repos/${owner}/${repo}/contents/users/${userId}`, | |
{ | |
headers: { | |
'Authorization': `token ${githubService.token}`, | |
'Accept': 'application/vnd.github.v3+json' | |
} | |
} | |
); | |
repoInfo.userDirectoryExists = true; | |
repoInfo.files = userDirResponse.data | |
.filter(item => item.type === 'file' && item.name.endsWith('.json')) | |
.map(file => ({ | |
name: file.name, | |
size: file.size, | |
sha: file.sha | |
})); | |
} catch (userDirError) { | |
repoInfo.userDirectoryError = userDirError.response?.status === 404 ? 'Directory not found' : userDirError.message; | |
} | |
} catch (repoError) { | |
repoInfo.error = repoError.message; | |
} | |
debugInfo.repositories.push(repoInfo); | |
} | |
console.log('PPT Debug info:', debugInfo); | |
res.json(debugInfo); | |
} catch (error) { | |
console.error('PPT Debug route error:', error); | |
res.status(500).json({ | |
error: error.message, | |
stack: process.env.NODE_ENV === 'development' ? error.stack : undefined | |
}); | |
} | |
}); | |
// 添加仓库初始化端点 | |
app.post('/api/github/initialize', async (req, res) => { | |
try { | |
console.log('=== Manual Repository Initialization ==='); | |
const { default: githubService } = await import('./services/githubService.js'); | |
if (githubService.useMemoryStorage) { | |
return res.status(400).json({ | |
error: 'Cannot initialize repository: using memory storage mode', | |
reason: 'GitHub token not configured' | |
}); | |
} | |
const { repoIndex = 0 } = req.body; | |
console.log(`Initializing repository at index: ${repoIndex}`); | |
const result = await githubService.initializeRepository(repoIndex); | |
if (result.success) { | |
res.json({ | |
success: true, | |
message: 'Repository initialized successfully', | |
commit: result.commit, | |
timestamp: new Date().toISOString() | |
}); | |
} else { | |
res.status(500).json({ | |
success: false, | |
error: result.error, | |
reason: result.reason | |
}); | |
} | |
} catch (error) { | |
console.error('Repository initialization error:', error); | |
res.status(500).json({ | |
error: error.message, | |
stack: process.env.NODE_ENV === 'development' ? error.stack : undefined | |
}); | |
} | |
}); | |
// 添加GitHub权限详细检查路由 | |
app.get('/api/debug/github-permissions', async (req, res) => { | |
try { | |
console.log('=== GitHub Token Permissions Check ==='); | |
const { default: githubService } = await import('./services/githubService.js'); | |
if (!githubService.token) { | |
return res.status(400).json({ error: 'No GitHub token configured' }); | |
} | |
const debugInfo = { | |
timestamp: new Date().toISOString(), | |
tokenInfo: {}, | |
repositoryTests: [] | |
}; | |
// 1. 检查token基本信息和权限 | |
try { | |
const userResponse = await axios.get('https://api.github.com/user', { | |
headers: { | |
'Authorization': `token ${githubService.token}`, | |
'Accept': 'application/vnd.github.v3+json' | |
} | |
}); | |
debugInfo.tokenInfo = { | |
login: userResponse.data.login, | |
id: userResponse.data.id, | |
type: userResponse.data.type, | |
company: userResponse.data.company, | |
publicRepos: userResponse.data.public_repos, | |
privateRepos: userResponse.data.total_private_repos, | |
tokenScopes: userResponse.headers['x-oauth-scopes'] || 'Unknown' | |
}; | |
console.log('Token info:', debugInfo.tokenInfo); | |
} catch (tokenError) { | |
debugInfo.tokenError = { | |
status: tokenError.response?.status, | |
message: tokenError.message | |
}; | |
} | |
// Check if GitHub repositories are configured | |
if (!githubService.repositories || githubService.repositories.length === 0) { | |
debugInfo.repositoryError = 'GitHub repositories not configured'; | |
return res.json(debugInfo); | |
} | |
// 2. 检查每个仓库的详细状态 | |
for (let i = 0; i < githubService.repositories.length; i++) { | |
const repoUrl = githubService.repositories[i]; | |
const repoTest = { | |
index: i, | |
url: repoUrl, | |
tests: {} | |
}; | |
try { | |
const { owner, repo } = githubService.parseRepoUrl(repoUrl); | |
repoTest.owner = owner; | |
repoTest.repo = repo; | |
// 测试1: 基本仓库访问 | |
try { | |
const repoResponse = await axios.get(`https://api.github.com/repos/${owner}/${repo}`, { | |
headers: { | |
'Authorization': `token ${githubService.token}`, | |
'Accept': 'application/vnd.github.v3+json' | |
} | |
}); | |
repoTest.tests.basicAccess = { | |
success: true, | |
repoExists: true, | |
private: repoResponse.data.private, | |
permissions: repoResponse.data.permissions, | |
defaultBranch: repoResponse.data.default_branch, | |
size: repoResponse.data.size | |
}; | |
} catch (repoError) { | |
repoTest.tests.basicAccess = { | |
success: false, | |
status: repoError.response?.status, | |
message: repoError.message, | |
details: repoError.response?.data | |
}; | |
// 如果是404,检查是否是权限问题还是仓库不存在 | |
if (repoError.response?.status === 404) { | |
// 尝试不使用认证访问(如果是公开仓库应该能访问) | |
try { | |
await axios.get(`https://api.github.com/repos/${owner}/${repo}`); | |
repoTest.tests.basicAccess.possibleCause = 'Repository exists but token lacks permission'; | |
} catch (publicError) { | |
if (publicError.response?.status === 404) { | |
repoTest.tests.basicAccess.possibleCause = 'Repository does not exist'; | |
} | |
} | |
} | |
} | |
// 测试2: 检查是否能列出用户的仓库 | |
try { | |
const userReposResponse = await axios.get(`https://api.github.com/users/${owner}/repos?per_page=100`, { | |
headers: { | |
'Authorization': `token ${githubService.token}`, | |
'Accept': 'application/vnd.github.v3+json' | |
} | |
}); | |
const hasRepo = userReposResponse.data.some(r => r.name === repo); | |
repoTest.tests.userReposList = { | |
success: true, | |
totalRepos: userReposResponse.data.length, | |
targetRepoFound: hasRepo, | |
repoNames: userReposResponse.data.slice(0, 10).map(r => ({ name: r.name, private: r.private })) | |
}; | |
} catch (userReposError) { | |
repoTest.tests.userReposList = { | |
success: false, | |
status: userReposError.response?.status, | |
message: userReposError.message | |
}; | |
} | |
} catch (parseError) { | |
repoTest.parseError = parseError.message; | |
} | |
debugInfo.repositoryTests.push(repoTest); | |
} | |
// 3. 提供修复建议 | |
const suggestions = []; | |
if (debugInfo.tokenError) { | |
suggestions.push('Token authentication failed - check if GITHUB_TOKEN is valid'); | |
} | |
debugInfo.repositoryTests.forEach((repoTest, index) => { | |
if (!repoTest.tests.basicAccess?.success) { | |
if (repoTest.tests.basicAccess?.status === 404) { | |
if (repoTest.tests.basicAccess?.possibleCause === 'Repository does not exist') { | |
suggestions.push(`Repository ${repoTest.url} does not exist - please create it on GitHub`); | |
} else { | |
suggestions.push(`Repository ${repoTest.url} exists but token lacks permission - check token scopes`); | |
} | |
} else if (repoTest.tests.basicAccess?.status === 403) { | |
suggestions.push(`Permission denied for ${repoTest.url} - check if token has 'repo' scope`); | |
} | |
} | |
}); | |
debugInfo.suggestions = suggestions; | |
console.log('GitHub permissions debug completed'); | |
res.json(debugInfo); | |
} catch (error) { | |
console.error('GitHub permissions check error:', error); | |
res.status(500).json({ | |
error: error.message, | |
stack: process.env.NODE_ENV === 'development' ? error.stack : undefined | |
}); | |
} | |
}); | |
// 添加大文件处理调试端点 | |
app.get('/api/debug/large-files/:userId', async (req, res) => { | |
try { | |
const { userId } = req.params; | |
console.log(`=== Large Files Debug for User: ${userId} ===`); | |
const { default: githubService } = await import('./services/githubService.js'); | |
const debugInfo = { | |
timestamp: new Date().toISOString(), | |
userId: userId, | |
fileAnalysis: [] | |
}; | |
// 检查用户的所有PPT文件 | |
const pptList = await githubService.getUserPPTList(userId); | |
for (const ppt of pptList) { | |
const fileInfo = { | |
pptId: ppt.name, | |
title: ppt.title, | |
fileName: `${ppt.name}.json`, | |
analysis: {} | |
}; | |
try { | |
// 获取文件内容 | |
const result = await githubService.getFile(userId, `${ppt.name}.json`, ppt.repoIndex || 0); | |
if (result && result.content) { | |
const content = result.content; | |
const jsonString = JSON.stringify(content); | |
const fileSize = Buffer.byteLength(jsonString, 'utf8'); | |
fileInfo.analysis = { | |
fileSize: fileSize, | |
fileSizeKB: (fileSize / 1024).toFixed(2), | |
slidesCount: content.slides?.length || 0, | |
isChunked: !!content.isChunked, | |
chunkedInfo: content.isChunked ? { | |
totalChunks: content.totalChunks, | |
totalSlides: content.totalSlides | |
} : null, | |
wasReassembled: !!result.isReassembled, | |
metadata: content.metadata || 'No metadata', | |
status: fileSize > 1024 * 1024 ? 'LARGE' : fileSize > 800 * 1024 ? 'MEDIUM' : 'NORMAL' | |
}; | |
// 分析每个slide的大小 | |
if (content.slides && content.slides.length > 0) { | |
const slideSizes = content.slides.map((slide, index) => { | |
const slideJson = JSON.stringify(slide); | |
const slideSize = Buffer.byteLength(slideJson, 'utf8'); | |
return { | |
index: index, | |
size: slideSize, | |
sizeKB: (slideSize / 1024).toFixed(2), | |
elementsCount: slide.elements?.length || 0 | |
}; | |
}); | |
// 找出最大的slides | |
const largestSlides = slideSizes | |
.sort((a, b) => b.size - a.size) | |
.slice(0, 3); | |
fileInfo.analysis.slideSummary = { | |
averageSlideSize: (fileSize / content.slides.length).toFixed(0), | |
largestSlides: largestSlides | |
}; | |
} | |
} else { | |
fileInfo.analysis = { error: 'Could not read file content' }; | |
} | |
} catch (error) { | |
fileInfo.analysis = { | |
error: error.message, | |
errorType: error.name | |
}; | |
} | |
debugInfo.fileAnalysis.push(fileInfo); | |
} | |
// 添加统计摘要 | |
debugInfo.summary = { | |
totalFiles: debugInfo.fileAnalysis.length, | |
largeFiles: debugInfo.fileAnalysis.filter(f => f.analysis.status === 'LARGE').length, | |
chunkedFiles: debugInfo.fileAnalysis.filter(f => f.analysis.isChunked).length, | |
errors: debugInfo.fileAnalysis.filter(f => f.analysis.error).length | |
}; | |
console.log('Large files debug completed'); | |
res.json(debugInfo); | |
} catch (error) { | |
console.error('Large files debug error:', error); | |
res.status(500).json({ | |
error: error.message, | |
stack: process.env.NODE_ENV === 'development' ? error.stack : undefined | |
}); | |
} | |
}); | |
// 添加分块文件修复端点 | |
app.post('/api/debug/fix-chunked-file/:userId/:pptId', async (req, res) => { | |
try { | |
const { userId, pptId } = req.params; | |
console.log(`=== Fixing Chunked File: ${userId}/${pptId} ===`); | |
const { default: githubService } = await import('./services/githubService.js'); | |
const fileName = `${pptId}.json`; | |
const result = { | |
timestamp: new Date().toISOString(), | |
userId, | |
pptId, | |
fileName, | |
status: 'unknown', | |
details: {}, | |
actions: [] | |
}; | |
// Check if GitHub repositories are configured | |
if (!githubService.repositories || githubService.repositories.length === 0) { | |
return res.status(500).json({ | |
...result, | |
status: 'error', | |
error: 'GitHub repositories not configured' | |
}); | |
} | |
// 1. 检查主文件是否存在 | |
let mainFile = null; | |
let mainFileRepo = -1; | |
for (let i = 0; i < githubService.repositories.length; i++) { | |
try { | |
const fileResult = await githubService.getFile(userId, fileName, i); | |
if (fileResult) { | |
mainFile = fileResult; | |
mainFileRepo = i; | |
result.details.mainFileFound = true; | |
result.details.mainFileRepo = i; | |
result.actions.push(`Main file found in repository ${i}`); | |
break; | |
} | |
} catch (error) { | |
continue; | |
} | |
} | |
if (!mainFile) { | |
result.status = 'error'; | |
result.details.error = 'Main file not found in any repository'; | |
return res.json(result); | |
} | |
const content = mainFile.content; | |
// 2. 检查是否是分块文件 | |
if (!content.isChunked) { | |
result.status = 'normal'; | |
result.details.isChunked = false; | |
result.details.slideCount = content.slides?.length || 0; | |
result.actions.push('File is not chunked, no action needed'); | |
return res.json(result); | |
} | |
// 3. 分析分块文件状态 | |
result.details.isChunked = true; | |
result.details.totalChunks = content.totalChunks; | |
result.details.totalSlides = content.totalSlides; | |
result.details.mainFileSlides = content.slides?.length || 0; | |
// 4. 检查所有chunk文件 | |
const chunkStatus = []; | |
let totalFoundSlides = content.slides?.length || 0; | |
for (let i = 1; i < content.totalChunks; i++) { | |
const chunkFileName = fileName.replace('.json', `_chunk_${i}.json`); | |
const chunkInfo = { | |
index: i, | |
fileName: chunkFileName, | |
found: false, | |
slides: 0, | |
error: null | |
}; | |
try { | |
const repoUrl = githubService.repositories[mainFileRepo]; | |
const { owner, repo } = githubService.parseRepoUrl(repoUrl); | |
const path = `users/${userId}/${chunkFileName}`; | |
const response = await axios.get( | |
`${githubService.apiUrl}/repos/${owner}/${repo}/contents/${path}`, | |
{ | |
headers: { | |
'Authorization': `token ${githubService.token}`, | |
'Accept': 'application/vnd.github.v3+json' | |
}, | |
timeout: 30000 | |
} | |
); | |
const chunkContent = Buffer.from(response.data.content, 'base64').toString('utf8'); | |
const chunkData = JSON.parse(chunkContent); | |
chunkInfo.found = true; | |
chunkInfo.slides = chunkData.slides?.length || 0; | |
totalFoundSlides += chunkInfo.slides; | |
result.actions.push(`Chunk ${i} found: ${chunkInfo.slides} slides`); | |
} catch (error) { | |
chunkInfo.error = error.message; | |
result.actions.push(`Chunk ${i} missing or error: ${error.message}`); | |
} | |
chunkStatus.push(chunkInfo); | |
} | |
result.details.chunks = chunkStatus; | |
result.details.totalFoundSlides = totalFoundSlides; | |
result.details.missingSlides = content.totalSlides - totalFoundSlides; | |
// 5. 判断状态和建议修复方案 | |
const missingChunks = chunkStatus.filter(chunk => !chunk.found); | |
if (missingChunks.length === 0 && totalFoundSlides === content.totalSlides) { | |
result.status = 'healthy'; | |
result.actions.push('All chunks found, file should load correctly'); | |
} else if (missingChunks.length > 0) { | |
result.status = 'incomplete'; | |
result.details.missingChunks = missingChunks.map(c => c.index); | |
result.actions.push(`Missing chunks: ${missingChunks.map(c => c.index).join(', ')}`); | |
// 提供修复建议 | |
if (totalFoundSlides >= content.totalSlides * 0.8) { | |
result.actions.push('Recommendation: Reassemble available slides into single file'); | |
result.details.recommendation = 'reassemble'; | |
} else { | |
result.actions.push('Recommendation: File may be corrupted, consider restoration from backup'); | |
result.details.recommendation = 'restore'; | |
} | |
} else { | |
result.status = 'mismatch'; | |
result.actions.push('Slide count mismatch detected'); | |
} | |
console.log('Chunked file analysis completed:', result); | |
res.json(result); | |
} catch (error) { | |
console.error('Chunked file fix error:', error); | |
res.status(500).json({ | |
error: error.message, | |
stack: process.env.NODE_ENV === 'development' ? error.stack : undefined | |
}); | |
} | |
}); | |
// 添加分块文件重组端点 | |
app.post('/api/debug/reassemble-chunked-file/:userId/:pptId', async (req, res) => { | |
try { | |
const { userId, pptId } = req.params; | |
console.log(`=== Reassembling Chunked File: ${userId}/${pptId} ===`); | |
const { default: githubService } = await import('./services/githubService.js'); | |
const fileName = `${pptId}.json`; | |
// 强制重新组装文件 | |
const result = await githubService.getFile(userId, fileName, 0); | |
if (!result) { | |
return res.status(404).json({ error: 'File not found' }); | |
} | |
if (result.isReassembled) { | |
res.json({ | |
success: true, | |
message: 'File reassembled successfully', | |
slideCount: result.content.slides?.length || 0, | |
wasChunked: !!result.content.reassembledInfo, | |
reassembledInfo: result.content.reassembledInfo | |
}); | |
} else { | |
res.json({ | |
success: true, | |
message: 'File was not chunked', | |
slideCount: result.content.slides?.length || 0, | |
wasChunked: false | |
}); | |
} | |
} catch (error) { | |
console.error('File reassembly error:', error); | |
res.status(500).json({ | |
error: error.message, | |
details: error.stack | |
}); | |
} | |
}); | |
// 添加路由注册日志 | |
console.log('Importing route modules...'); | |
console.log('Auth routes imported:', !!authRoutes); | |
console.log('PPT routes imported:', !!pptRoutes); | |
console.log('Public routes imported:', !!publicRoutes); | |
// 这些路由已经在前面注册过了,删除重复的注册 | |
// 前端路由处理 - 修复ES模块兼容性 | |
app.get('*', (req, res) => { | |
const indexPath = path.join(frontendDistPath, 'index.html'); | |
console.log(`Serving frontend route: ${req.path}, index.html path: ${indexPath}`); | |
// 使用ES模块的fs检查文件是否存在 | |
if (fs.existsSync(indexPath)) { | |
res.sendFile(indexPath); | |
} else { | |
console.error('index.html not found at:', indexPath); | |
res.status(404).send(` | |
<h1>前端文件未找到</h1> | |
<p>index.html路径: ${indexPath}</p> | |
<p>请确保前端已正确构建</p> | |
<a href="/test">访问API测试页面</a> | |
`); | |
} | |
}); | |
// 错误处理中间件 | |
app.use(errorHandler); | |
// 初始化服务 | |
async function initializeServices() { | |
console.log('🚀 Initializing services...'); | |
let servicesInitialized = 0; | |
let totalServices = 5; | |
// 初始化GitHub服务 | |
try { | |
await githubService.initialize(); | |
servicesInitialized++; | |
} catch (error) { | |
console.error('❌ Failed to initialize GitHubService:', error); | |
console.log('⚠️ Continuing without GitHubService...'); | |
} | |
// 初始化Huggingface存储服务 | |
try { | |
await huggingfaceStorageService.initialize(); | |
servicesInitialized++; | |
} catch (error) { | |
console.error('❌ Failed to initialize HuggingfaceStorageService:', error); | |
console.log('⚠️ Continuing without HuggingfaceStorageService...'); | |
} | |
// 初始化备份调度服务 | |
try { | |
await backupSchedulerService.initialize(); | |
servicesInitialized++; | |
} catch (error) { | |
console.error('❌ Failed to initialize BackupSchedulerService:', error); | |
console.log('⚠️ Continuing without BackupSchedulerService...'); | |
} | |
// 初始化持久化图片链接服务 | |
try { | |
await persistentImageLinkService.initialize(); | |
servicesInitialized++; | |
} catch (error) { | |
console.error('❌ Failed to initialize PersistentImageLinkService:', error); | |
console.log('⚠️ Continuing without PersistentImageLinkService...'); | |
} | |
// 初始化自动备份服务 | |
try { | |
await autoBackupService.initialize(); | |
servicesInitialized++; | |
} catch (error) { | |
console.error('❌ Failed to initialize AutoBackupService:', error); | |
console.log('⚠️ Continuing without AutoBackupService...'); | |
} | |
console.log(`✅ ${servicesInitialized}/${totalServices} services initialized successfully`); | |
if (servicesInitialized === 0) { | |
console.error('❌ No services could be initialized. Exiting...'); | |
process.exit(1); | |
} | |
} | |
// 启动服务器 | |
async function startServer() { | |
await initializeServices(); | |
app.listen(PORT, '0.0.0.0', () => { | |
console.log(`Server is running on port ${PORT}`); | |
console.log(`Environment: ${process.env.NODE_ENV || 'development'}`); | |
console.log(`💾 Huggingface storage service ready`); | |
console.log(`⏰ Backup scheduler service ready`); | |
console.log(`🔗 Persistent image link service ready`); | |
console.log(`🔄 Auto backup service ready (every 8 hours)`); | |
}); | |
} | |
// 启动服务器 | |
startServer().catch(error => { | |
console.error('❌ Failed to start server:', error); | |
process.exit(1); | |
}); | |
// 优雅关闭处理 | |
process.on('SIGTERM', async () => { | |
console.log('🛑 Received SIGTERM, shutting down gracefully...'); | |
await backupSchedulerService.cleanup(); | |
await autoBackupService.cleanup(); | |
process.exit(0); | |
}); | |
process.on('SIGINT', async () => { | |
console.log('🛑 Received SIGINT, shutting down gracefully...'); | |
await backupSchedulerService.cleanup(); | |
await autoBackupService.cleanup(); | |
process.exit(0); | |
}); |