Spaces:
Paused
Paused
| import axios from 'axios'; | |
| class GitHubService { | |
| constructor() { | |
| // 延迟初始化,动态读取环境变量 | |
| this.initialized = false; | |
| this.useMemoryStorage = false; | |
| this.memoryStorage = new Map(); | |
| this.apiUrl = 'https://api.github.com'; | |
| // 首次使用时再初始化 | |
| this.initPromise = null; | |
| // 添加错误重试配置 | |
| this.retryConfig = { | |
| maxRetries: 3, | |
| retryDelay: 1000, | |
| backoffMultiplier: 2 | |
| }; | |
| // API限制处理 | |
| this.apiRateLimit = { | |
| requestQueue: [], | |
| processing: false, | |
| minDelay: 100 // 最小请求间隔 | |
| }; | |
| } | |
| // 动态初始化方法 | |
| async initialize() { | |
| if (this.initialized) { | |
| return; | |
| } | |
| console.log('=== GitHub Service Configuration ==='); | |
| try { | |
| // 动态读取环境变量 | |
| this.token = process.env.GITHUB_TOKEN; | |
| this.repositories = process.env.GITHUB_REPOS | |
| ? process.env.GITHUB_REPOS.split(',').map(repo => repo.trim()) | |
| : []; | |
| 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('Repositories length:', this.repositories?.length || 0); | |
| // Check if token is a placeholder | |
| if (!this.token || this.token === 'your_github_token_here') { | |
| console.warn('❌ GitHub token is missing or using placeholder value!'); | |
| console.warn('⚠️ Switching to memory storage mode for development...'); | |
| this.useMemoryStorage = true; | |
| console.log('✅ Memory storage mode activated'); | |
| this.initialized = true; | |
| return; | |
| } | |
| if (!this.repositories || this.repositories.length === 0) { | |
| console.warn('❌ No GitHub repositories configured!'); | |
| console.warn('⚠️ Switching to memory storage mode...'); | |
| this.useMemoryStorage = true; | |
| console.log('✅ Memory storage mode activated'); | |
| this.initialized = true; | |
| return; | |
| } | |
| // Check for placeholder repository URLs | |
| const validRepos = this.repositories.filter(repo => { | |
| const isPlaceholder = repo.includes('your-username') || repo.includes('placeholder'); | |
| if (isPlaceholder) { | |
| console.warn(`⚠️ Skipping placeholder repository: ${repo}`); | |
| return false; | |
| } | |
| return this.isValidRepoUrl(repo); | |
| }); | |
| if (validRepos.length === 0) { | |
| console.warn('❌ No valid GitHub repositories found (all are placeholders)!'); | |
| console.warn('⚠️ Switching to memory storage mode...'); | |
| this.useMemoryStorage = true; | |
| console.log('✅ Memory storage mode activated'); | |
| this.initialized = true; | |
| return; | |
| } | |
| this.repositories = validRepos; | |
| // 验证每个仓库URL的格式 | |
| this.repositories.forEach((repo, index) => { | |
| if (!this.isValidRepoUrl(repo)) { | |
| console.error(`❌ Invalid repository URL at index ${index}: ${repo}`); | |
| throw new Error(`Invalid repository URL: ${repo}. Expected format: https://github.com/owner/repo`); | |
| } | |
| }); | |
| console.log('✅ GitHub Service initialized successfully'); | |
| this.initialized = true; | |
| } catch (error) { | |
| console.error('❌ GitHub Service initialization failed:', error); | |
| console.warn('⚠️ Falling back to memory storage mode...'); | |
| this.useMemoryStorage = true; | |
| this.initialized = true; | |
| } | |
| } | |
| // 确保在所有方法中先初始化 | |
| async ensureInitialized() { | |
| if (!this.initPromise) { | |
| this.initPromise = this.initialize(); | |
| } | |
| await this.initPromise; | |
| } | |
| // 验证仓库URL格式 | |
| isValidRepoUrl(repoUrl) { | |
| const githubUrlPattern = /^https:\/\/github\.com\/[^\/]+\/[^\/]+\/?$/; | |
| return githubUrlPattern.test(repoUrl.trim()); | |
| } | |
| // 带重试的API请求方法 | |
| async makeGitHubRequest(requestFn, operation = 'GitHub API request', maxRetries = null) { | |
| const retries = maxRetries || this.retryConfig.maxRetries; | |
| let lastError = null; | |
| for (let attempt = 0; attempt <= retries; attempt++) { | |
| try { | |
| if (attempt > 0) { | |
| const delay = this.retryConfig.retryDelay * Math.pow(this.retryConfig.backoffMultiplier, attempt - 1); | |
| console.log(`🔄 Retrying ${operation} (attempt ${attempt + 1}/${retries + 1}) after ${delay}ms...`); | |
| await new Promise(resolve => setTimeout(resolve, delay)); | |
| } | |
| const result = await requestFn(); | |
| if (attempt > 0) { | |
| console.log(`✅ ${operation} succeeded on retry attempt ${attempt + 1}`); | |
| } | |
| return result; | |
| } catch (error) { | |
| lastError = error; | |
| // 判断是否应该重试 | |
| if (!this.shouldRetry(error) || attempt === retries) { | |
| break; | |
| } | |
| console.warn(`⚠️ ${operation} failed (attempt ${attempt + 1}):`, error.message); | |
| } | |
| } | |
| console.error(`❌ ${operation} failed after ${retries + 1} attempts:`, lastError.message); | |
| throw lastError; | |
| } | |
| // 判断错误是否应该重试 | |
| shouldRetry(error) { | |
| if (!error.response) { | |
| return true; // 网络错误,重试 | |
| } | |
| const status = error.response.status; | |
| // 这些状态码不应该重试 | |
| if ([400, 401, 403, 404, 422].includes(status)) { | |
| return false; | |
| } | |
| // 5xx错误和429限制错误应该重试 | |
| if (status >= 500 || status === 429) { | |
| return true; | |
| } | |
| return false; | |
| } | |
| // 验证GitHub连接 | |
| async validateConnection() { | |
| await this.ensureInitialized(); | |
| if (this.useMemoryStorage) { | |
| console.log('📝 Memory storage mode active'); | |
| return { | |
| valid: true, | |
| useMemoryStorage: true, | |
| repositories: [], | |
| message: 'Using memory storage mode for development' | |
| }; | |
| } | |
| try { | |
| // 测试GitHub API连接 | |
| const response = await this.makeGitHubRequest(async () => { | |
| return await axios.get(`${this.apiUrl}/user`, { | |
| headers: { | |
| 'Authorization': `token ${this.token}`, | |
| 'Accept': 'application/vnd.github.v3+json' | |
| }, | |
| timeout: 30000 | |
| }); | |
| }, 'GitHub user authentication'); | |
| 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 this.makeGitHubRequest(async () => { | |
| return await axios.get(`${this.apiUrl}/repos/${owner}/${repo}`, { | |
| headers: { | |
| 'Authorization': `token ${this.token}`, | |
| 'Accept': 'application/vnd.github.v3+json' | |
| }, | |
| timeout: 30000 | |
| }); | |
| }, `Repository access check for ${owner}/${repo}`); | |
| // 检查权限 | |
| 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' | |
| }, | |
| timeout: 15000 | |
| }); | |
| 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) { | |
| await this.ensureInitialized(); | |
| if (this.useMemoryStorage) { | |
| console.log('📝 Memory storage mode - no repository initialization needed'); | |
| return { success: true, message: 'Memory storage initialized' }; | |
| } | |
| const repoUrl = this.repositories[repoIndex]; | |
| const { owner, repo } = this.parseRepoUrl(repoUrl); | |
| try { | |
| console.log(`🚀 Initializing repository: ${owner}/${repo}`); | |
| // 创建初始README文件 | |
| const readmeContent = `# PPT Storage Repository\n\nThis repository is used to store PPT data files.\n\nCreated: ${new Date().toISOString()}`; | |
| const content = Buffer.from(readmeContent).toString('base64'); | |
| await this.makeGitHubRequest(async () => { | |
| return await axios.put( | |
| `${this.apiUrl}/repos/${owner}/${repo}/contents/README.md`, | |
| { | |
| message: 'Initialize PPT storage repository', | |
| content: content | |
| }, | |
| { | |
| headers: { | |
| 'Authorization': `token ${this.token}`, | |
| 'Accept': 'application/vnd.github.v3+json' | |
| }, | |
| timeout: 30000 | |
| } | |
| ); | |
| }, `Repository initialization for ${owner}/${repo}`); | |
| console.log(`✅ Repository ${owner}/${repo} initialized successfully`); | |
| return { success: true, message: 'Repository initialized' }; | |
| } catch (error) { | |
| console.error(`❌ Repository initialization failed:`, error); | |
| throw new Error(`Failed to initialize repository: ${error.message}`); | |
| } | |
| } | |
| // 兼容性方法:旧的getFile方法重定向到新的getPPT | |
| async getFile(userId, fileName, repoIndex = 0) { | |
| await this.ensureInitialized(); | |
| const pptId = fileName.replace('.json', ''); | |
| return await this.getPPT(userId, pptId, repoIndex); | |
| } | |
| // 兼容性方法:旧的saveFile方法重定向到新的savePPT | |
| async saveFile(userId, fileName, data, repoIndex = 0) { | |
| await this.ensureInitialized(); | |
| const pptId = fileName.replace('.json', ''); | |
| return await this.savePPT(userId, pptId, data, repoIndex); | |
| } | |
| // 数据格式标准化 | |
| normalizeDataFormat(data) { | |
| if (!data || typeof data !== 'object') { | |
| throw new Error('Invalid data format provided'); | |
| } | |
| const normalized = { | |
| id: data.id || data.pptId || `ppt-${Date.now()}`, | |
| title: data.title || '未命名演示文稿', | |
| slides: Array.isArray(data.slides) ? data.slides : [], | |
| theme: data.theme || { | |
| backgroundColor: '#ffffff', | |
| themeColor: '#d14424', | |
| fontColor: '#333333', | |
| fontName: 'Microsoft YaHei' | |
| }, | |
| viewportSize: data.viewportSize || 1000, | |
| viewportRatio: data.viewportRatio || 0.5625, | |
| createdAt: data.createdAt || new Date().toISOString(), | |
| updatedAt: new Date().toISOString() | |
| }; | |
| // 标准化slides数据 | |
| normalized.slides = normalized.slides.map((slide, index) => { | |
| if (!slide || typeof slide !== 'object') { | |
| return { | |
| id: `slide-${index}`, | |
| elements: [], | |
| background: { type: 'solid', color: '#ffffff' } | |
| }; | |
| } | |
| return { | |
| id: slide.id || `slide-${index}`, | |
| elements: Array.isArray(slide.elements) ? slide.elements : [], | |
| background: slide.background || { type: 'solid', color: '#ffffff' }, | |
| ...slide | |
| }; | |
| }); | |
| // 保留原始数据的其他字段 | |
| Object.keys(data).forEach(key => { | |
| if (!normalized.hasOwnProperty(key) && key !== 'slides') { | |
| normalized[key] = data[key]; | |
| } | |
| }); | |
| return normalized; | |
| } | |
| // 兼容性:旧的deleteFile方法 | |
| async deleteFile(userId, fileName, repoIndex = 0) { | |
| await this.ensureInitialized(); | |
| if (this.useMemoryStorage) { | |
| const key = `users/${userId}/${fileName}`; | |
| return this.memoryStorage.delete(key); | |
| } | |
| const repoUrl = this.repositories[repoIndex]; | |
| const { owner, repo } = this.parseRepoUrl(repoUrl); | |
| const path = `users/${userId}/${fileName}`; | |
| try { | |
| // 获取文件信息 | |
| const response = await this.makeGitHubRequest(async () => { | |
| return await axios.get( | |
| `${this.apiUrl}/repos/${owner}/${repo}/contents/${path}`, | |
| { | |
| headers: { | |
| 'Authorization': `token ${this.token}`, | |
| 'Accept': 'application/vnd.github.v3+json' | |
| }, | |
| timeout: 30000 | |
| } | |
| ); | |
| }, `Get file info for deletion: ${fileName}`); | |
| // 删除主文件 | |
| await this.makeGitHubRequest(async () => { | |
| return await axios.delete( | |
| `${this.apiUrl}/repos/${owner}/${repo}/contents/${path}`, | |
| { | |
| data: { | |
| message: `Delete legacy PPT: ${fileName}`, | |
| sha: response.data.sha | |
| }, | |
| headers: { | |
| 'Authorization': `token ${this.token}`, | |
| 'Accept': 'application/vnd.github.v3+json' | |
| }, | |
| timeout: 30000 | |
| } | |
| ); | |
| }, `Delete legacy file: ${fileName}`); | |
| console.log(`✅ Legacy file deleted: ${fileName}`); | |
| return true; | |
| } catch (error) { | |
| if (error.response?.status === 404) { | |
| console.log(`📄 File not found: ${fileName}`); | |
| return false; | |
| } | |
| console.error(`❌ Delete failed for ${fileName}:`, error); | |
| throw new Error(`Failed to delete file: ${error.message}`); | |
| } | |
| } | |
| // Memory storage methods - 兼容性方法 | |
| async saveToMemory(userId, fileName, data) { | |
| const pptId = fileName.replace('.json', ''); | |
| return await this.savePPTToMemory(userId, pptId, data); | |
| } | |
| async getFromMemory(userId, fileName) { | |
| const pptId = fileName.replace('.json', ''); | |
| const result = await this.getPPTFromMemory(userId, pptId); | |
| return result ? result.content : null; | |
| } | |
| // 新架构:获取PPT(文件夹模式) - 简化版本 | |
| async getPPT(userId, pptId, repoIndex = 0) { | |
| await this.ensureInitialized(); | |
| if (this.useMemoryStorage) { | |
| return await this.getPPTFromMemory(userId, pptId); | |
| } | |
| try { | |
| console.log(`📂 Getting PPT folder: ${pptId} for user: ${userId}`); | |
| const repoUrl = this.repositories[repoIndex]; | |
| const { owner, repo } = this.parseRepoUrl(repoUrl); | |
| const pptFolderPath = `users/${userId}/${pptId}`; | |
| // 1. 获取PPT文件夹内容 - 带重试 | |
| const folderResponse = await this.makeGitHubRequest(async () => { | |
| return await axios.get( | |
| `${this.apiUrl}/repos/${owner}/${repo}/contents/${pptFolderPath}`, | |
| { | |
| headers: { | |
| 'Authorization': `token ${this.token}`, | |
| 'Accept': 'application/vnd.github.v3+json' | |
| }, | |
| timeout: 30000 | |
| } | |
| ); | |
| }, `Get PPT folder contents for ${pptId}`); | |
| // 2. 读取元数据文件 | |
| const metaFile = folderResponse.data.find(file => file.name === 'meta.json'); | |
| if (!metaFile) { | |
| throw new Error('PPT metadata file not found'); | |
| } | |
| const metaResponse = await this.makeGitHubRequest(async () => { | |
| return await axios.get( | |
| `${this.apiUrl}/repos/${owner}/${repo}/contents/${metaFile.path}`, | |
| { | |
| headers: { | |
| 'Authorization': `token ${this.token}`, | |
| 'Accept': 'application/vnd.github.v3+json' | |
| }, | |
| timeout: 15000 | |
| } | |
| ); | |
| }, `Get PPT metadata for ${pptId}`); | |
| let metadata; | |
| try { | |
| const metaContent = Buffer.from(metaResponse.data.content, 'base64').toString('utf8'); | |
| metadata = JSON.parse(metaContent); | |
| } catch (parseError) { | |
| console.error('❌ Failed to parse metadata:', parseError); | |
| throw new Error('Invalid PPT metadata format'); | |
| } | |
| // 3. 加载所有slide文件 | |
| const allFiles = folderResponse.data; | |
| const slides = []; | |
| const failedSlides = []; | |
| // 查找slide文件(只处理普通文件,忽略拆分文件) | |
| const slideFiles = allFiles.filter(file => | |
| file.name.startsWith('slide_') && | |
| file.name.endsWith('.json') && | |
| !file.name.includes('_main') && | |
| !file.name.includes('_part_') | |
| ); | |
| // 按序号排序 | |
| const sortedSlideFiles = slideFiles.sort((a, b) => { | |
| const aIndex = parseInt(a.name.match(/slide_(\d+)\.json/)?.[1] || '0'); | |
| const bIndex = parseInt(b.name.match(/slide_(\d+)\.json/)?.[1] || '0'); | |
| return aIndex - bIndex; | |
| }); | |
| for (const slideFile of sortedSlideFiles) { | |
| try { | |
| const slide = await this.loadSlideFile(slideFile, repoIndex); | |
| const slideIndex = parseInt(slideFile.name.match(/slide_(\d+)\.json/)?.[1] || '0'); | |
| slides[slideIndex] = slide; | |
| } catch (slideError) { | |
| console.warn(`⚠️ Failed to load slide ${slideFile.name}:`, slideError.message); | |
| failedSlides.push(slideFile.name); | |
| } | |
| } | |
| // 4. 组装完整PPT数据 | |
| const finalSlides = slides.filter(slide => slide !== undefined); | |
| const pptData = { | |
| ...metadata, | |
| slides: finalSlides, | |
| storage: { | |
| type: 'folder', | |
| slidesCount: finalSlides.length, | |
| folderPath: pptFolderPath, | |
| loadedAt: new Date().toISOString(), | |
| failedSlides: failedSlides.length > 0 ? failedSlides : undefined | |
| } | |
| }; | |
| if (failedSlides.length > 0) { | |
| console.warn(`⚠️ PPT loaded with ${failedSlides.length} failed slides`); | |
| } else { | |
| console.log(`✅ PPT loaded successfully: ${finalSlides.length} slides`); | |
| } | |
| return { content: pptData }; | |
| } catch (error) { | |
| if (error.response?.status === 404) { | |
| console.log(`📄 PPT folder not found: ${pptId}`); | |
| return null; | |
| } | |
| console.error(`❌ Get PPT failed:`, error); | |
| throw new Error(`Failed to get PPT: ${error.message}`); | |
| } | |
| } | |
| // 新增:检查文件大小 | |
| async checkFileSize(filePath, repoIndex) { | |
| const repoUrl = this.repositories[repoIndex]; | |
| const { owner, repo } = this.parseRepoUrl(repoUrl); | |
| try { | |
| const response = await this.makeGitHubRequest(async () => { | |
| return await axios.get( | |
| `${this.apiUrl}/repos/${owner}/${repo}/contents/${filePath}`, | |
| { | |
| headers: { | |
| 'Authorization': `token ${this.token}`, | |
| 'Accept': 'application/vnd.github.v3+json' | |
| }, | |
| timeout: 15000 | |
| } | |
| ); | |
| }, `Check file size for ${filePath}`); | |
| return { | |
| size: response.data.size, | |
| sha: response.data.sha | |
| }; | |
| } catch (error) { | |
| if (error.response?.status === 404) { | |
| return null; | |
| } | |
| throw error; | |
| } | |
| } | |
| // 新增:智能读取文件内容(根据大小选择策略) | |
| async readFileContent(filePath, repoIndex, maxDirectSize = 1024 * 1024) { // 1MB限制 | |
| const fileInfo = await this.checkFileSize(filePath, repoIndex); | |
| if (!fileInfo) { | |
| return null; | |
| } | |
| // 如果文件小于限制,直接读取 | |
| if (fileInfo.size <= maxDirectSize) { | |
| return await this.readFileContentDirect(filePath, repoIndex); | |
| } | |
| // 大文件:使用Git Blob API读取 | |
| console.log(`📊 Large file detected (${(fileInfo.size / 1024 / 1024).toFixed(2)} MB), using blob API`); | |
| return await this.readFileContentViaBlob(filePath, fileInfo.sha, repoIndex); | |
| } | |
| // 直接读取文件(小文件) | |
| async readFileContentDirect(filePath, repoIndex) { | |
| const repoUrl = this.repositories[repoIndex]; | |
| const { owner, repo } = this.parseRepoUrl(repoUrl); | |
| const response = await this.makeGitHubRequest(async () => { | |
| return await axios.get( | |
| `${this.apiUrl}/repos/${owner}/${repo}/contents/${filePath}`, | |
| { | |
| headers: { | |
| 'Authorization': `token ${this.token}`, | |
| 'Accept': 'application/vnd.github.v3+json' | |
| }, | |
| timeout: 30000 | |
| } | |
| ); | |
| }, `Read file content directly: ${filePath}`); | |
| const content = Buffer.from(response.data.content, 'base64').toString('utf8'); | |
| return JSON.parse(content); | |
| } | |
| // 通过Git Blob API读取大文件 | |
| async readFileContentViaBlob(filePath, sha, repoIndex) { | |
| const repoUrl = this.repositories[repoIndex]; | |
| const { owner, repo } = this.parseRepoUrl(repoUrl); | |
| try { | |
| console.log(`🔍 Reading large file via blob API: ${filePath}`); | |
| const response = await this.makeGitHubRequest(async () => { | |
| return await axios.get( | |
| `${this.apiUrl}/repos/${owner}/${repo}/git/blobs/${sha}`, | |
| { | |
| headers: { | |
| 'Authorization': `token ${this.token}`, | |
| 'Accept': 'application/vnd.github.v3+json' | |
| }, | |
| timeout: 120000 // 2分钟超时 | |
| } | |
| ); | |
| }, `Read blob for large file: ${filePath}`); | |
| if (response.data.encoding === 'base64') { | |
| const content = Buffer.from(response.data.content, 'base64').toString('utf8'); | |
| return JSON.parse(content); | |
| } else { | |
| // 如果不是base64编码,直接解析 | |
| return JSON.parse(response.data.content); | |
| } | |
| } catch (error) { | |
| console.error(`❌ Blob API read failed for ${filePath}:`, error.message); | |
| // 回退到分批读取策略 | |
| console.log(`🔄 Falling back to chunked reading...`); | |
| return await this.readLargeFileInChunks(filePath, repoIndex); | |
| } | |
| } | |
| // 分批读取大文件(最后的回退策略) | |
| async readLargeFileInChunks(filePath, repoIndex) { | |
| console.log(`📦 Attempting chunked read for: ${filePath}`); | |
| // 检查是否存在分块文件(PPT文件夹结构) | |
| const folderPath = filePath.replace(/\/[^\/]+$/, ''); | |
| try { | |
| const repoUrl = this.repositories[repoIndex]; | |
| const { owner, repo } = this.parseRepoUrl(repoUrl); | |
| // 尝试读取文件夹内容 | |
| const folderResponse = await this.makeGitHubRequest(async () => { | |
| return await axios.get( | |
| `${this.apiUrl}/repos/${owner}/${repo}/contents/${folderPath}`, | |
| { | |
| headers: { | |
| 'Authorization': `token ${this.token}`, | |
| 'Accept': 'application/vnd.github.v3+json' | |
| }, | |
| timeout: 30000 | |
| } | |
| ); | |
| }, `Read folder for chunked file: ${folderPath}`); | |
| // 查找相关的分块文件 | |
| const chunkFiles = folderResponse.data.filter(file => | |
| file.name.includes('_part_') || file.name.includes('_chunk_') | |
| ); | |
| if (chunkFiles.length > 0) { | |
| console.log(`📊 Found ${chunkFiles.length} chunk files, reassembling...`); | |
| return await this.reassembleChunkedFiles(chunkFiles, repoIndex); | |
| } | |
| throw new Error('No chunked files found, cannot read large file'); | |
| } catch (error) { | |
| console.error(`❌ Chunked read failed:`, error.message); | |
| throw new Error(`Unable to read large file: ${filePath}. File may be too large for current GitHub API limits.`); | |
| } | |
| } | |
| // 重组分块文件 | |
| async reassembleChunkedFiles(chunkFiles, repoIndex) { | |
| const chunks = []; | |
| // 按顺序读取所有分块 | |
| const sortedChunks = chunkFiles.sort((a, b) => { | |
| const aIndex = parseInt(a.name.match(/_(\d+)(?:_|\.)/)?.[1] || '0'); | |
| const bIndex = parseInt(b.name.match(/_(\d+)(?:_|\.)/)?.[1] || '0'); | |
| return aIndex - bIndex; | |
| }); | |
| for (const chunkFile of sortedChunks) { | |
| try { | |
| const chunkContent = await this.readFileContentDirect(chunkFile.path, repoIndex); | |
| chunks.push(chunkContent); | |
| } catch (error) { | |
| console.error(`⚠️ Failed to read chunk ${chunkFile.name}:`, error.message); | |
| } | |
| } | |
| // 重组数据 | |
| if (chunks.length === 0) { | |
| throw new Error('No valid chunks found'); | |
| } | |
| // 假设第一个chunk包含基础结构 | |
| const baseData = chunks[0]; | |
| const allSlides = []; | |
| chunks.forEach(chunk => { | |
| if (chunk.slides && Array.isArray(chunk.slides)) { | |
| allSlides.push(...chunk.slides); | |
| } | |
| }); | |
| return { | |
| ...baseData, | |
| slides: allSlides, | |
| storage: { | |
| type: 'reassembled', | |
| chunksCount: chunks.length, | |
| reassembledAt: new Date().toISOString() | |
| } | |
| }; | |
| } | |
| // 修改现有的loadSlideFile方法,使用新的智能读取 | |
| async loadSlideFile(slideFile, repoIndex) { | |
| try { | |
| // 使用智能读取策略 | |
| const slideContent = await this.readFileContent(slideFile.path, repoIndex); | |
| return slideContent; | |
| } catch (error) { | |
| console.error(`❌ Failed to load slide file ${slideFile.name}:`, error.message); | |
| throw error; | |
| } | |
| } | |
| // slide压缩算法 - 保留轻量压缩 | |
| async compressSlide(slide) { | |
| let compressedSlide = JSON.parse(JSON.stringify(slide)); // 深拷贝 | |
| // 压缩策略1:移除不必要的属性 | |
| if (compressedSlide.elements) { | |
| compressedSlide.elements = compressedSlide.elements.map(element => { | |
| // 移除编辑状态属性 | |
| element = this.removeUnnecessaryProps(element); | |
| // 压缩文本内容(移除多余空白) | |
| if (element.type === 'text' && element.content && typeof element.content === 'string') { | |
| element.content = element.content.replace(/\s+/g, ' ').trim(); | |
| } | |
| return element; | |
| }); | |
| } | |
| // 压缩策略2:精简数值精度 | |
| compressedSlide = this.roundNumericValues(compressedSlide); | |
| const compressedSize = Buffer.byteLength(JSON.stringify(compressedSlide), 'utf8'); | |
| console.log(`🗜️ Light compression applied: ${(compressedSize / 1024).toFixed(2)} KB`); | |
| return compressedSlide; | |
| } | |
| // 移除不必要的属性 | |
| removeUnnecessaryProps(element) { | |
| const unnecessaryProps = [ | |
| 'selected', 'editing', 'dragData', 'resizeData', | |
| 'tempData', 'cache', 'debug' | |
| ]; | |
| unnecessaryProps.forEach(prop => { | |
| if (element[prop] !== undefined) { | |
| delete element[prop]; | |
| } | |
| }); | |
| return element; | |
| } | |
| // 精简数值精度 - 保持高精度的关键属性 | |
| roundNumericValues(obj, precision = 2) { | |
| if (typeof obj !== 'object' || obj === null) { | |
| return obj; | |
| } | |
| if (Array.isArray(obj)) { | |
| return obj.map(item => this.roundNumericValues(item, precision)); | |
| } | |
| const rounded = {}; | |
| for (const [key, value] of Object.entries(obj)) { | |
| if (typeof value === 'number') { | |
| // 🎯 关键布局属性保持高精度 | |
| if (['left', 'top', 'width', 'height', 'x', 'y', 'viewportRatio', 'viewportSize'].includes(key)) { | |
| rounded[key] = Math.round(value * 1000000000) / 1000000000; // 9位精度 | |
| } | |
| // 🎯 变换和旋转属性保持高精度 | |
| else if (key.includes('transform') || key.includes('rotate') || key === 'rotate') { | |
| rounded[key] = Math.round(value * 1000000000) / 1000000000; | |
| } | |
| // 📐 其他数值属性适度精简 | |
| else { | |
| rounded[key] = typeof value === 'number' && !isNaN(value) && isFinite(value) | |
| ? Math.round(value * 1000000) / 1000000 // 6位精度 | |
| : value; | |
| } | |
| } else if (typeof value === 'object') { | |
| rounded[key] = this.roundNumericValues(value, precision); | |
| } else { | |
| rounded[key] = value; | |
| } | |
| } | |
| return rounded; | |
| } | |
| // 通用文件保存到仓库 - 简化版本 | |
| async saveFileToRepo(filePath, data, commitMessage, repoIndex) { | |
| const repoUrl = this.repositories[repoIndex]; | |
| const { owner, repo } = this.parseRepoUrl(repoUrl); | |
| try { | |
| // 验证数据 | |
| if (!data || typeof data !== 'object') { | |
| throw new Error('Invalid data provided for file save'); | |
| } | |
| const content = Buffer.from(JSON.stringify(data)).toString('base64'); | |
| const fileSize = Buffer.byteLength(JSON.stringify(data), 'utf8'); | |
| // 检查文件大小(GitHub限制100MB) | |
| if (fileSize > 100 * 1024 * 1024) { | |
| throw new Error(`File too large: ${(fileSize / 1024 / 1024).toFixed(2)} MB exceeds GitHub's 100MB limit`); | |
| } | |
| console.log(`💾 Saving file: ${filePath} (${(fileSize / 1024).toFixed(2)} KB)`); | |
| // 检查文件是否已存在 | |
| let sha = null; | |
| try { | |
| const existingResponse = await axios.get( | |
| `${this.apiUrl}/repos/${owner}/${repo}/contents/${filePath}`, | |
| { | |
| headers: { | |
| 'Authorization': `token ${this.token}`, | |
| 'Accept': 'application/vnd.github.v3+json' | |
| }, | |
| timeout: 30000 | |
| } | |
| ); | |
| sha = existingResponse.data.sha; | |
| } catch (error) { | |
| // 文件不存在,将创建新文件 | |
| } | |
| // 保存文件 | |
| const response = await axios.put( | |
| `${this.apiUrl}/repos/${owner}/${repo}/contents/${filePath}`, | |
| { | |
| message: commitMessage, | |
| content: content, | |
| ...(sha && { sha }) | |
| }, | |
| { | |
| headers: { | |
| 'Authorization': `token ${this.token}`, | |
| 'Accept': 'application/vnd.github.v3+json' | |
| }, | |
| timeout: 60000 | |
| } | |
| ); | |
| console.log(`✅ File saved successfully: ${filePath}`); | |
| return response.data; | |
| } catch (error) { | |
| console.error(`❌ File save failed for ${filePath}:`, error.message); | |
| if (error.response?.status === 422) { | |
| if (error.response?.data?.message?.includes('file is too large')) { | |
| throw new Error(`File too large: ${filePath} exceeds GitHub size limits`); | |
| } | |
| if (error.response?.data?.message?.includes('Invalid request')) { | |
| throw new Error(`Invalid file format for: ${filePath}`); | |
| } | |
| } | |
| if (error.response?.status === 409) { | |
| throw new Error(`File conflict for: ${filePath}. File may have been modified by another process.`); | |
| } | |
| throw new Error(`Failed to save file ${filePath}: ${error.message}`); | |
| } | |
| } | |
| // 单个slide保存 - 简化版本 | |
| async saveSlideWithCompression(filePath, slide, slideIndex, repoIndex) { | |
| try { | |
| // 应用轻量压缩 | |
| const compressedSlide = await this.compressSlide(slide); | |
| await this.makeGitHubRequest(async () => { | |
| return await this.saveFileToRepo( | |
| filePath, | |
| compressedSlide, | |
| `Save slide ${slideIndex}`, | |
| repoIndex | |
| ); | |
| }, `Save slide ${slideIndex}`); | |
| const finalSize = Buffer.byteLength(JSON.stringify(compressedSlide), 'utf8'); | |
| return { | |
| slideIndex, | |
| finalSize, | |
| compressed: true | |
| }; | |
| } catch (error) { | |
| console.error(`❌ Failed to save slide ${slideIndex}:`, error); | |
| throw error; | |
| } | |
| } | |
| // 新架构:保存PPT(文件夹模式) - 简化版本 | |
| async savePPT(userId, pptId, pptData, repoIndex = 0) { | |
| await this.ensureInitialized(); | |
| if (this.useMemoryStorage) { | |
| return await this.savePPTToMemory(userId, pptId, pptData); | |
| } | |
| try { | |
| console.log(`📂 Saving PPT to folder: ${pptId} for user: ${userId}`); | |
| const repoUrl = this.repositories[repoIndex]; | |
| const { owner, repo } = this.parseRepoUrl(repoUrl); | |
| const pptFolderPath = `users/${userId}/${pptId}`; | |
| // 验证输入数据 | |
| if (!pptData || typeof pptData !== 'object') { | |
| throw new Error('Invalid PPT data provided'); | |
| } | |
| // 1. 准备元数据(不包含slides) | |
| const metadata = { | |
| id: pptData.id || pptId, | |
| title: pptData.title || '未命名演示文稿', | |
| theme: pptData.theme || { | |
| backgroundColor: '#ffffff', | |
| themeColor: '#d14424', | |
| fontColor: '#333333', | |
| fontName: 'Microsoft YaHei' | |
| }, | |
| viewportSize: pptData.viewportSize || 1000, | |
| viewportRatio: pptData.viewportRatio || 0.5625, | |
| createdAt: pptData.createdAt || new Date().toISOString(), | |
| updatedAt: new Date().toISOString(), | |
| storage: { | |
| type: 'folder', | |
| version: '2.0', | |
| slidesCount: pptData.slides?.length || 0 | |
| } | |
| }; | |
| // 2. 保存元数据文件 | |
| await this.makeGitHubRequest(async () => { | |
| return await this.saveFileToRepo( | |
| `${pptFolderPath}/meta.json`, | |
| metadata, | |
| `Update PPT metadata: ${metadata.title}`, | |
| repoIndex | |
| ); | |
| }, `Save metadata for PPT ${pptId}`); | |
| console.log(`✅ Metadata saved for PPT: ${pptId}`); | |
| // 3. 串行化保存每个slide文件 | |
| const slides = Array.isArray(pptData.slides) ? pptData.slides : []; | |
| const saveResults = []; | |
| console.log(`📊 Starting to save ${slides.length} slides...`); | |
| for (let i = 0; i < slides.length; i++) { | |
| const slide = slides[i]; | |
| const slideFileName = `slide_${String(i).padStart(3, '0')}.json`; | |
| console.log(`💾 Saving slide ${i + 1}/${slides.length}: ${slideFileName}`); | |
| try { | |
| // 验证slide数据 | |
| if (!slide || typeof slide !== 'object') { | |
| throw new Error(`Invalid slide data at index ${i}`); | |
| } | |
| const result = await this.saveSlideWithCompression( | |
| `${pptFolderPath}/${slideFileName}`, | |
| slide, | |
| i, | |
| repoIndex | |
| ); | |
| saveResults.push(result); | |
| console.log(`✅ Slide ${i} saved: ${(result.finalSize / 1024).toFixed(2)} KB`); | |
| // 添加延迟避免GitHub API速率限制 | |
| if (i < slides.length - 1) { | |
| await new Promise(resolve => setTimeout(resolve, this.apiRateLimit.minDelay)); | |
| } | |
| } catch (slideError) { | |
| console.error(`❌ Failed to save slide ${i}: ${slideError.message}`); | |
| throw slideError; // 任何slide保存失败都应该停止整个过程 | |
| } | |
| } | |
| // 4. 计算保存统计 | |
| const totalSize = saveResults.reduce((sum, r) => sum + r.finalSize, 0); | |
| console.log(`🎉 PPT saved successfully: ${slides.length} slides, total size: ${(totalSize / 1024).toFixed(2)} KB`); | |
| return { | |
| success: true, | |
| pptId: pptId, | |
| storage: 'folder', | |
| slidesCount: slides.length, | |
| folderPath: pptFolderPath, | |
| size: totalSize | |
| }; | |
| } catch (error) { | |
| console.error(`❌ PPT save failed for ${pptId}: ${error.message}`); | |
| throw new Error(`Failed to save PPT: ${error.message}`); | |
| } | |
| } | |
| // Memory storage methods - 简化版本 | |
| async getPPTFromMemory(userId, pptId) { | |
| const metaKey = `users/${userId}/${pptId}/meta`; | |
| const metadata = this.memoryStorage.get(metaKey); | |
| if (!metadata) { | |
| console.log(`📄 PPT not found in memory: ${pptId}`); | |
| return null; | |
| } | |
| // 重组slides | |
| const slides = []; | |
| const slidesCount = metadata.storage?.slidesCount || 0; | |
| for (let i = 0; i < slidesCount; i++) { | |
| const slideKey = `users/${userId}/${pptId}/slide_${String(i).padStart(3, '0')}`; | |
| const slide = this.memoryStorage.get(slideKey); | |
| if (slide) { | |
| slides.push(slide); | |
| } | |
| } | |
| const pptData = { | |
| ...metadata, | |
| slides: slides, | |
| storage: { | |
| ...metadata.storage, | |
| loadedAt: new Date().toISOString() | |
| } | |
| }; | |
| console.log(`📖 Read PPT from memory: ${pptId} (${slides.length} slides)`); | |
| return { content: pptData }; | |
| } | |
| // 简化内存存储 | |
| async savePPTToMemory(userId, pptId, pptData) { | |
| // 保存元数据 | |
| const metadata = { | |
| id: pptData.id || pptId, | |
| title: pptData.title || '未命名演示文稿', | |
| theme: pptData.theme, | |
| viewportSize: pptData.viewportSize || 1000, | |
| viewportRatio: pptData.viewportRatio || 0.5625, | |
| createdAt: pptData.createdAt || new Date().toISOString(), | |
| updatedAt: new Date().toISOString(), | |
| storage: { | |
| type: 'folder', | |
| version: '2.0', | |
| slidesCount: pptData.slides?.length || 0 | |
| } | |
| }; | |
| const metaKey = `users/${userId}/${pptId}/meta`; | |
| this.memoryStorage.set(metaKey, metadata); | |
| // 保存slides | |
| const slides = pptData.slides || []; | |
| let totalSize = 0; | |
| for (let i = 0; i < slides.length; i++) { | |
| const slide = slides[i]; | |
| const compressedSlide = await this.compressSlide(slide); | |
| const slideKey = `users/${userId}/${pptId}/slide_${String(i).padStart(3, '0')}`; | |
| this.memoryStorage.set(slideKey, compressedSlide); | |
| totalSize += Buffer.byteLength(JSON.stringify(compressedSlide), 'utf8'); | |
| } | |
| console.log(`💾 Saved PPT to memory: ${pptId} (${slides.length} slides, ${(totalSize / 1024).toFixed(2)} KB)`); | |
| return { | |
| success: true, | |
| storage: 'folder', | |
| slidesCount: slides.length, | |
| size: totalSize | |
| }; | |
| } | |
| // 删除PPT时清理所有文件 | |
| async deletePPTFromMemory(userId, pptId) { | |
| let deleted = 0; | |
| const prefix = `users/${userId}/${pptId}/`; | |
| // 删除所有相关键 | |
| for (const key of this.memoryStorage.keys()) { | |
| if (key.startsWith(prefix)) { | |
| this.memoryStorage.delete(key); | |
| deleted++; | |
| } | |
| } | |
| console.log(`📝 Memory storage: Deleted ${deleted} files for PPT ${pptId}`); | |
| return deleted > 0; | |
| } | |
| // 更新用户PPT列表 | |
| async getUserPPTListFromMemory(userId) { | |
| const results = []; | |
| const userPrefix = `users/${userId}/`; | |
| const pptIds = new Set(); | |
| // 收集所有PPT ID | |
| for (const key of this.memoryStorage.keys()) { | |
| if (key.startsWith(userPrefix) && key.includes('/meta')) { | |
| const pptId = key.replace(userPrefix, '').replace('/meta', ''); | |
| pptIds.add(pptId); | |
| } | |
| } | |
| // 获取每个PPT的元数据 | |
| for (const pptId of pptIds) { | |
| const metaKey = `${userPrefix}${pptId}/meta`; | |
| const metadata = this.memoryStorage.get(metaKey); | |
| if (metadata) { | |
| results.push({ | |
| name: pptId, | |
| title: metadata.title || '未命名演示文稿', | |
| updatedAt: metadata.updatedAt || new Date().toISOString(), | |
| slidesCount: metadata.storage?.slidesCount || 0, | |
| isChunked: false, | |
| storageType: 'folder', | |
| size: JSON.stringify(metadata).length, | |
| repoIndex: 0 | |
| }); | |
| } | |
| } | |
| console.log(`📋 Found ${results.length} PPTs in memory for user ${userId}`); | |
| return results.sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt)); | |
| } | |
| // 获取用户PPT列表(新架构) - 简化版本 | |
| async getUserPPTList(userId) { | |
| await this.ensureInitialized(); | |
| if (this.useMemoryStorage) { | |
| return await this.getUserPPTListFromMemory(userId); | |
| } | |
| const pptList = []; | |
| // 检查所有仓库 | |
| for (let repoIndex = 0; repoIndex < this.repositories.length; repoIndex++) { | |
| try { | |
| const repoUrl = this.repositories[repoIndex]; | |
| const { owner, repo } = this.parseRepoUrl(repoUrl); | |
| const userDirPath = `users/${userId}`; | |
| const response = await this.makeGitHubRequest(async () => { | |
| return await axios.get( | |
| `${this.apiUrl}/repos/${owner}/${repo}/contents/${userDirPath}`, | |
| { | |
| headers: { | |
| 'Authorization': `token ${this.token}`, | |
| 'Accept': 'application/vnd.github.v3+json' | |
| }, | |
| timeout: 30000 | |
| } | |
| ); | |
| }, `Get user directory for ${userId} in repo ${repoIndex}`); | |
| // 查找PPT文件夹 | |
| const pptFolders = response.data.filter(item => | |
| item.type === 'dir' // PPT存储为文件夹 | |
| ); | |
| for (const folder of pptFolders) { | |
| try { | |
| const pptId = folder.name; | |
| // 获取PPT元数据 | |
| const metaResponse = await this.makeGitHubRequest(async () => { | |
| return await axios.get( | |
| `${this.apiUrl}/repos/${owner}/${repo}/contents/${userDirPath}/${pptId}/meta.json`, | |
| { | |
| headers: { | |
| 'Authorization': `token ${this.token}`, | |
| 'Accept': 'application/vnd.github.v3+json' | |
| }, | |
| timeout: 15000 | |
| } | |
| ); | |
| }, `Get metadata for PPT ${pptId}`); | |
| const metaContent = Buffer.from(metaResponse.data.content, 'base64').toString('utf8'); | |
| const metadata = JSON.parse(metaContent); | |
| pptList.push({ | |
| name: pptId, | |
| title: metadata.title || '未命名演示文稿', | |
| updatedAt: metadata.updatedAt || new Date().toISOString(), | |
| slidesCount: metadata.storage?.slidesCount || 0, | |
| isChunked: false, | |
| storageType: 'folder', | |
| size: metaResponse.data.size || 0, | |
| repoIndex: repoIndex | |
| }); | |
| } catch (error) { | |
| console.warn(`跳过无效PPT文件夹 ${folder.name}:`, error.message); | |
| } | |
| } | |
| // 兼容性:同时检查旧的单文件格式 | |
| const jsonFiles = response.data.filter(file => | |
| file.type === 'file' && | |
| file.name.endsWith('.json') && | |
| !file.name.includes('_chunk_') | |
| ); | |
| for (const file of jsonFiles) { | |
| try { | |
| const pptId = file.name.replace('.json', ''); | |
| // 避免重复添加(如果已经有文件夹版本) | |
| if (pptList.some(p => p.name === pptId)) { | |
| continue; | |
| } | |
| const fileResponse = await this.makeGitHubRequest(async () => { | |
| return await axios.get( | |
| `${this.apiUrl}/repos/${owner}/${repo}/contents/${userDirPath}/${file.name}`, | |
| { | |
| headers: { | |
| 'Authorization': `token ${this.token}`, | |
| 'Accept': 'application/vnd.github.v3+json' | |
| }, | |
| timeout: 15000 | |
| } | |
| ); | |
| }, `Get legacy PPT file ${file.name}`); | |
| const content = Buffer.from(fileResponse.data.content, 'base64').toString('utf8'); | |
| const pptData = JSON.parse(content); | |
| pptList.push({ | |
| name: pptId, | |
| title: pptData.title || '未命名演示文稿', | |
| updatedAt: pptData.updatedAt || new Date().toISOString(), | |
| slidesCount: pptData.isChunked ? pptData.totalSlides : (pptData.slides?.length || 0), | |
| isChunked: pptData.isChunked || false, | |
| storageType: 'legacy', | |
| size: file.size, | |
| repoIndex: repoIndex | |
| }); | |
| } catch (error) { | |
| console.warn(`跳过无效文件 ${file.name}:`, error.message); | |
| } | |
| } | |
| } catch (error) { | |
| console.warn(`仓库 ${repoIndex} 中没有找到用户目录或访问失败:`, error.message); | |
| continue; | |
| } | |
| } | |
| // 按更新时间排序 | |
| return pptList.sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt)); | |
| } | |
| // 删除PPT(新架构) - 简化版本 | |
| async deletePPT(userId, pptId, repoIndex = 0) { | |
| await this.ensureInitialized(); | |
| if (this.useMemoryStorage) { | |
| return await this.deletePPTFromMemory(userId, pptId); | |
| } | |
| const repoUrl = this.repositories[repoIndex]; | |
| const { owner, repo } = this.parseRepoUrl(repoUrl); | |
| const pptFolderPath = `users/${userId}/${pptId}`; | |
| try { | |
| console.log(`🗑️ Deleting PPT folder: ${pptFolderPath}`); | |
| // 1. 获取文件夹内容 | |
| const folderResponse = await this.makeGitHubRequest(async () => { | |
| return await axios.get( | |
| `${this.apiUrl}/repos/${owner}/${repo}/contents/${pptFolderPath}`, | |
| { | |
| headers: { | |
| 'Authorization': `token ${this.token}`, | |
| 'Accept': 'application/vnd.github.v3+json' | |
| }, | |
| timeout: 30000 | |
| } | |
| ); | |
| }, `Get PPT folder contents for deletion: ${pptId}`); | |
| // 2. 串行删除所有文件 | |
| for (const file of folderResponse.data) { | |
| try { | |
| await this.makeGitHubRequest(async () => { | |
| return await axios.delete( | |
| `${this.apiUrl}/repos/${owner}/${repo}/contents/${file.path}`, | |
| { | |
| data: { | |
| message: `Delete PPT file: ${file.name}`, | |
| sha: file.sha | |
| }, | |
| headers: { | |
| 'Authorization': `token ${this.token}`, | |
| 'Accept': 'application/vnd.github.v3+json' | |
| }, | |
| timeout: 30000 | |
| } | |
| ); | |
| }, `Delete file ${file.name}`); | |
| console.log(`✅ Deleted file: ${file.name}`); | |
| // 添加延迟避免API限制 | |
| await new Promise(resolve => setTimeout(resolve, this.apiRateLimit.minDelay)); | |
| } catch (error) { | |
| console.warn(`⚠️ Failed to delete file ${file.name}:`, error.message); | |
| } | |
| } | |
| console.log(`✅ PPT folder deleted successfully: ${pptId}`); | |
| return true; | |
| } catch (error) { | |
| if (error.response?.status === 404) { | |
| console.log(`📄 PPT folder not found, trying legacy format: ${pptId}`); | |
| return await this.deleteFile(userId, `${pptId}.json`, repoIndex); | |
| } | |
| console.error(`❌ Delete PPT failed for ${pptId}:`, error); | |
| throw new Error(`Failed to delete PPT: ${error.message}`); | |
| } | |
| } | |
| } | |
| export default new GitHubService(); |