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'; // 添加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 { authenticateToken } from './middleware/auth.js'; import { errorHandler } from './middleware/errorHandler.js'; const app = express(); const PORT = process.env.PORT || 7860; // 修改为7860端口 // 设置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' })); // 提供前端静态文件 - 修复路径配置 const frontendDistPath = path.join(__dirname, '../../frontend/dist'); console.log('Frontend dist path:', frontendDistPath); app.use(express.static(frontendDistPath)); // 提供数据文件 app.use('/data', express.static(path.join(__dirname, '../../frontend/public/mocks'))); // 直接提供测试页面路由 app.get('/test.html', (req, res) => { try { // 检查文件是否存在 const testFilePath = path.join(__dirname, '../../test.html'); console.log('Looking for test.html at:', testFilePath); // 发送测试页面 res.sendFile(testFilePath, (err) => { if (err) { console.error('Error serving test.html:', err); // 如果文件不存在,返回一个简单的测试页面 res.send(` Test Page

PPTist API Test

Test file not found at: ${testFilePath}

`); } }); } catch (error) { console.error('Test page error:', error); res.status(500).send('Test page error: ' + error.message); } }); // 添加内置测试页面端点 app.get('/test', (req, res) => { res.send(` PPTist API 测试页面

🚀 PPTist API 测试控制台

🔗 基础连接测试

🔐 用户认证测试

当前Token: 未登录

📄 PPT管理测试

🌐 公共分享测试

`); }); // API路由 console.log('Registering API routes...'); // 健康检查 - 需要在其他路由之前 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: [] }; // 检查每个仓库中用户的文件 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 }; } // 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: [] }; // 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); // 认证相关路由(不需要认证) 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); console.log('All 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 }); }); // 前端路由处理 - 修复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(`

前端文件未找到

index.html路径: ${indexPath}

请确保前端已正确构建

访问API测试页面 `); } }); // 错误处理中间件 app.use(errorHandler); app.listen(PORT, '0.0.0.0', () => { console.log(`Server is running on port ${PORT}`); console.log(`Environment: ${process.env.NODE_ENV || 'development'}`); });