import axios from 'axios'; import { GITHUB_CONFIG } from '../config/users.js'; import memoryStorageService from './memoryStorageService.js'; class GitHubService { constructor() { this.apiUrl = GITHUB_CONFIG.apiUrl; this.token = GITHUB_CONFIG.token; this.repositories = GITHUB_CONFIG.repositories; this.useMemoryStorage = !this.token; // 如果没有token,使用内存存储 console.log('=== GitHub Service Configuration ==='); console.log('Token configured:', !!this.token); console.log('Token preview:', this.token ? `${this.token.substring(0, 8)}...` : 'Not set'); console.log('Repositories:', this.repositories); console.log('Using memory storage:', this.useMemoryStorage); if (!this.token) { console.warn('GitHub token not configured, using memory storage for development'); } } // 验证GitHub连接 async validateConnection() { if (this.useMemoryStorage) { return { valid: false, reason: 'No GitHub token configured' }; } try { // 测试GitHub API连接 const response = await axios.get(`${this.apiUrl}/user`, { headers: { 'Authorization': `token ${this.token}`, 'Accept': 'application/vnd.github.v3+json' } }); console.log('GitHub API connection successful:', response.data.login); // 测试仓库访问和权限 const repoResults = []; for (const repoUrl of this.repositories) { try { const { owner, repo } = this.parseRepoUrl(repoUrl); // 检查仓库基本信息 const repoResponse = await axios.get(`${this.apiUrl}/repos/${owner}/${repo}`, { headers: { 'Authorization': `token ${this.token}`, 'Accept': 'application/vnd.github.v3+json' } }); // 检查权限 const permissions = repoResponse.data.permissions; console.log(`Repository permissions for ${owner}/${repo}:`, permissions); // 测试是否可以访问仓库内容 let canAccessContents = false; let repoEmpty = false; try { await axios.get(`${this.apiUrl}/repos/${owner}/${repo}/contents`, { headers: { 'Authorization': `token ${this.token}`, 'Accept': 'application/vnd.github.v3+json' } }); canAccessContents = true; } catch (contentsError) { console.log(`Cannot access repository contents: ${contentsError.message}`); // 如果是409错误,说明仓库为空 if (contentsError.response?.status === 409) { repoEmpty = true; console.log('Repository appears to be empty, will initialize when needed'); } } repoResults.push({ url: repoUrl, accessible: true, name: repoResponse.data.full_name, permissions, canAccessContents, repoEmpty, defaultBranch: repoResponse.data.default_branch, size: repoResponse.data.size }); } catch (error) { console.error(`Repository access error for ${repoUrl}:`, error.response?.data || error.message); repoResults.push({ url: repoUrl, accessible: false, error: error.message }); } } return { valid: true, user: response.data.login, repositories: repoResults }; } catch (error) { console.error('GitHub connection validation failed:', error.message); return { valid: false, reason: error.message }; } } // 获取仓库信息 parseRepoUrl(repoUrl) { const match = repoUrl.match(/github\.com\/([^\/]+)\/([^\/]+)/); if (!match) throw new Error('Invalid GitHub repository URL'); return { owner: match[1], repo: match[2] }; } // 初始化空仓库 async initializeRepository(repoIndex = 0) { if (this.useMemoryStorage) { return { success: false, reason: 'Using memory storage' }; } try { const repoUrl = this.repositories[repoIndex]; const { owner, repo } = this.parseRepoUrl(repoUrl); console.log(`Initializing empty repository: ${owner}/${repo}`); // 创建初始README文件 const readmeContent = Buffer.from(`# PPTist Data Repository This repository stores PPT data for the PPTist application. ## Structure \`\`\` users/ ├── PS01/ │ ├── README.md │ └── *.json (PPT files) ├── PS02/ │ ├── README.md │ └── *.json (PPT files) └── ... \`\`\` ## Auto-generated This repository is automatically managed by PPTist Huggingface Space. Do not manually edit files unless you know what you're doing. `).toString('base64'); const response = await axios.put( `${this.apiUrl}/repos/${owner}/${repo}/contents/README.md`, { message: 'Initialize repository for PPTist data storage', content: readmeContent, branch: 'main' }, { headers: { 'Authorization': `token ${this.token}`, 'Accept': 'application/vnd.github.v3+json' } } ); console.log(`Repository initialized successfully: ${response.data.commit.sha}`); return { success: true, commit: response.data.commit.sha }; } catch (error) { console.error('Repository initialization failed:', error.response?.data || error.message); return { success: false, error: error.message }; } } // 获取文件内容 async getFile(userId, fileName, repoIndex = 0) { // 如果使用内存存储 if (this.useMemoryStorage) { return await memoryStorageService.getFile(userId, fileName); } // 原有的GitHub逻辑 try { const repoUrl = this.repositories[repoIndex]; const { owner, repo } = this.parseRepoUrl(repoUrl); const path = `users/${userId}/${fileName}`; const response = await axios.get( `${this.apiUrl}/repos/${owner}/${repo}/contents/${path}`, { headers: { 'Authorization': `token ${this.token}`, 'Accept': 'application/vnd.github.v3+json' } } ); const content = Buffer.from(response.data.content, 'base64').toString('utf8'); return { content: JSON.parse(content), sha: response.data.sha }; } catch (error) { if (error.response?.status === 404) { return null; } throw error; } } // 保存文件 async saveFile(userId, fileName, data, repoIndex = 0) { // 如果使用内存存储 if (this.useMemoryStorage) { return await memoryStorageService.saveFile(userId, fileName, data); } // 原有的GitHub逻辑 const repoUrl = this.repositories[repoIndex]; const { owner, repo } = this.parseRepoUrl(repoUrl); const path = `users/${userId}/${fileName}`; console.log(`Attempting to save file: ${path} to repo: ${owner}/${repo}`); // 先尝试获取现有文件的SHA let sha = null; try { const existing = await this.getFile(userId, fileName, repoIndex); if (existing) { sha = existing.sha; console.log(`Found existing file with SHA: ${sha}`); } } catch (error) { console.log(`No existing file found: ${error.message}`); } const content = Buffer.from(JSON.stringify(data, null, 2)).toString('base64'); const payload = { message: `${sha ? 'Update' : 'Create'} ${fileName} for user ${userId}`, content: content, branch: 'main' }; if (sha) { payload.sha = sha; } try { console.log(`Saving to GitHub: ${this.apiUrl}/repos/${owner}/${repo}/contents/${path}`); const response = await axios.put( `${this.apiUrl}/repos/${owner}/${repo}/contents/${path}`, payload, { headers: { 'Authorization': `token ${this.token}`, 'Accept': 'application/vnd.github.v3+json' } } ); console.log(`Successfully saved to GitHub: ${response.data.commit.sha}`); return response.data; } catch (error) { console.error(`GitHub save failed:`, error.response?.data || error.message); // 特殊处理空仓库的情况 if (error.response?.status === 409 && error.response?.data?.message?.includes('empty')) { console.log('Repository is empty, initializing...'); const initResult = await this.initializeRepository(repoIndex); if (initResult.success) { console.log('Repository initialized, retrying save...'); // 等待一会儿让GitHub同步 await new Promise(resolve => setTimeout(resolve, 2000)); // 继续执行原来的目录创建逻辑 return this.saveFileWithDirectoryCreation(userId, fileName, data, repoIndex, payload); } } // 详细的错误处理 if (error.response?.status === 404) { return this.saveFileWithDirectoryCreation(userId, fileName, data, repoIndex, payload); } else if (error.response?.status === 403) { throw new Error(`GitHub permission denied. Check if the token has 'repo' permissions: ${error.response.data.message}`); } else { throw new Error(`GitHub API error (${error.response?.status}): ${error.response?.data?.message || error.message}`); } } } // 带目录创建的保存文件方法 async saveFileWithDirectoryCreation(userId, fileName, data, repoIndex, payload) { const repoUrl = this.repositories[repoIndex]; const { owner, repo } = this.parseRepoUrl(repoUrl); const path = `users/${userId}/${fileName}`; console.log('404 error - attempting to create directory structure...'); try { // 方法1: 先检查仓库是否存在和可访问 const repoCheckResponse = await axios.get(`${this.apiUrl}/repos/${owner}/${repo}`, { headers: { 'Authorization': `token ${this.token}`, 'Accept': 'application/vnd.github.v3+json' } }); console.log(`Repository exists: ${repoCheckResponse.data.full_name}`); // 方法2: 检查users目录是否存在 try { await axios.get(`${this.apiUrl}/repos/${owner}/${repo}/contents/users`, { headers: { 'Authorization': `token ${this.token}`, 'Accept': 'application/vnd.github.v3+json' } }); console.log('Users directory exists'); } catch (usersDirError) { console.log('Users directory does not exist, creating...'); // 创建users目录的README const usersReadmePath = 'users/README.md'; const usersReadmeContent = Buffer.from('# Users Directory\n\nThis directory contains user-specific PPT files.\n').toString('base64'); await axios.put( `${this.apiUrl}/repos/${owner}/${repo}/contents/${usersReadmePath}`, { message: 'Create users directory', content: usersReadmeContent, branch: 'main' }, { headers: { 'Authorization': `token ${this.token}`, 'Accept': 'application/vnd.github.v3+json' } } ); console.log('Users directory created'); } // 方法3: 检查用户目录是否存在 try { await axios.get(`${this.apiUrl}/repos/${owner}/${repo}/contents/users/${userId}`, { headers: { 'Authorization': `token ${this.token}`, 'Accept': 'application/vnd.github.v3+json' } }); console.log(`User directory exists: users/${userId}`); } catch (userDirError) { console.log(`User directory does not exist, creating: users/${userId}`); // 创建用户目录的README const userReadmePath = `users/${userId}/README.md`; const userReadmeContent = Buffer.from(`# PPT Files for User ${userId}\n\nThis directory contains PPT files for user ${userId}.\n`).toString('base64'); await axios.put( `${this.apiUrl}/repos/${owner}/${repo}/contents/${userReadmePath}`, { message: `Create user directory for ${userId}`, content: userReadmeContent, branch: 'main' }, { headers: { 'Authorization': `token ${this.token}`, 'Accept': 'application/vnd.github.v3+json' } } ); console.log(`User directory created: users/${userId}`); } // 等待一小会儿让GitHub同步 await new Promise(resolve => setTimeout(resolve, 1000)); // 重试保存PPT文件 console.log('Retrying PPT file save...'); const retryResponse = await axios.put( `${this.apiUrl}/repos/${owner}/${repo}/contents/${path}`, payload, { headers: { 'Authorization': `token ${this.token}`, 'Accept': 'application/vnd.github.v3+json' } } ); console.log(`Successfully saved to GitHub after retry: ${retryResponse.data.commit.sha}`); return retryResponse.data; } catch (retryError) { console.error(`Comprehensive retry also failed:`, retryError.response?.data || retryError.message); // 如果GitHub彻底失败,fallback到内存存储 console.log('GitHub save completely failed, falling back to memory storage...'); try { const memoryResult = await memoryStorageService.saveFile(userId, fileName, data); console.log('Successfully saved to memory storage as fallback'); return { ...memoryResult, warning: 'Saved to temporary memory storage due to GitHub issues' }; } catch (memoryError) { console.error('Memory storage fallback also failed:', memoryError.message); throw new Error(`All storage methods failed. GitHub: ${retryError.message}, Memory: ${memoryError.message}`); } } } // 获取用户的所有PPT列表 async getUserPPTList(userId) { // 如果使用内存存储 if (this.useMemoryStorage) { return await memoryStorageService.getUserPPTList(userId); } // 原有的GitHub逻辑 const results = []; for (let i = 0; i < this.repositories.length; i++) { try { const repoUrl = this.repositories[i]; const { owner, repo } = this.parseRepoUrl(repoUrl); const path = `users/${userId}`; const response = await axios.get( `${this.apiUrl}/repos/${owner}/${repo}/contents/${path}`, { headers: { 'Authorization': `token ${this.token}`, 'Accept': 'application/vnd.github.v3+json' } } ); const files = response.data .filter(item => item.type === 'file' && item.name.endsWith('.json')); // 获取每个PPT文件的实际内容以读取标题 for (const file of files) { try { const pptId = file.name.replace('.json', ''); const fileContent = await this.getFile(userId, file.name, i); if (fileContent && fileContent.content) { results.push({ name: pptId, title: fileContent.content.title || '未命名演示文稿', lastModified: fileContent.content.updatedAt || fileContent.content.createdAt, repoIndex: i, repoUrl: repoUrl }); } } catch (error) { // 如果读取单个文件失败,使用文件名作为标题 console.warn(`Failed to read PPT content for ${file.name}:`, error.message); results.push({ name: file.name.replace('.json', ''), title: file.name.replace('.json', ''), lastModified: new Date().toISOString(), repoIndex: i, repoUrl: repoUrl }); } } } catch (error) { if (error.response?.status !== 404) { console.error(`Error fetching files from repo ${i}:`, error.message); } } } return results; } // 删除文件 async deleteFile(userId, fileName, repoIndex = 0) { // 如果使用内存存储 if (this.useMemoryStorage) { return await memoryStorageService.deleteFile(userId, fileName); } // 原有的GitHub逻辑 const existing = await this.getFile(userId, fileName, repoIndex); if (!existing) { throw new Error('File not found'); } const repoUrl = this.repositories[repoIndex]; const { owner, repo } = this.parseRepoUrl(repoUrl); const path = `users/${userId}/${fileName}`; const response = await axios.delete( `${this.apiUrl}/repos/${owner}/${repo}/contents/${path}`, { data: { message: `Delete ${fileName} for user ${userId}`, sha: existing.sha, branch: 'main' }, headers: { 'Authorization': `token ${this.token}`, 'Accept': 'application/vnd.github.v3+json' } } ); return response.data; } } export default new GitHubService();