import puppeteer from 'puppeteer'; class ScreenshotService { constructor() { this.browser = null; this.isInitialized = false; this.maxRetries = 2; this.browserLaunchRetries = 0; this.maxBrowserLaunchRetries = 2; this.isClosing = false; this.isHuggingFaceSpace = process.env.SPACE_ID || process.env.HF_SPACE_ID; } async initBrowser() { // 在Hugging Face Space环境中,Puppeteer可能不稳定,尝试更保守的配置 if (this.isClosing) { console.log('浏览器正在关闭中,等待重新初始化...'); this.browser = null; this.isClosing = false; } if (!this.browser) { try { console.log('初始化Puppeteer浏览器...'); const launchOptions = { headless: 'new', timeout: 60000, protocolTimeout: 60000, args: [ '--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage', '--disable-accelerated-2d-canvas', '--no-first-run', '--disable-gpu', '--disable-background-timer-throttling', '--disable-backgrounding-occluded-windows', '--disable-renderer-backgrounding', '--disable-features=TranslateUI', '--disable-extensions', '--hide-scrollbars', '--mute-audio', '--no-default-browser-check', '--disable-default-apps', '--disable-background-networking', '--disable-sync', '--metrics-recording-only', '--disable-domain-reliability', '--force-device-scale-factor=1', '--disable-features=VizDisplayCompositor', '--run-all-compositor-stages-before-draw', '--disable-new-content-rendering-timeout', '--disable-ipc-flooding-protection', '--disable-hang-monitor', '--disable-prompt-on-repost', '--memory-pressure-off', '--max_old_space_size=1024' ] }; // 如果是Hugging Face Space环境,使用更保守的配置 if (this.isHuggingFaceSpace) { console.log('检测到Hugging Face Space环境,使用保守配置'); launchOptions.args.push( '--single-process', '--no-zygote', '--disable-web-security', '--disable-features=site-per-process', '--disable-site-isolation-trials' ); } this.browser = await puppeteer.launch(launchOptions); // 监听浏览器断开连接事件 this.browser.on('disconnected', () => { console.log('Puppeteer浏览器连接断开'); this.browser = null; this.isClosing = false; }); console.log('Puppeteer浏览器初始化完成'); this.browserLaunchRetries = 0; } catch (error) { console.error('Puppeteer浏览器初始化失败:', error); this.browserLaunchRetries++; if (this.browserLaunchRetries <= this.maxBrowserLaunchRetries) { console.log(`尝试重新初始化浏览器 (${this.browserLaunchRetries}/${this.maxBrowserLaunchRetries})`); await this.delay(3000); // 等待3秒后重试 return this.initBrowser(); } else { // 如果Puppeteer完全失败,返回null,使用fallback方法 console.warn('Puppeteer初始化完全失败,将使用fallback方法'); return null; } } } // 验证浏览器是否仍然连接 if (this.browser) { try { await this.browser.version(); } catch (error) { console.warn('浏览器连接已断开,重新初始化:', error.message); this.browser = null; return this.initBrowser(); } } return this.browser; } async closeBrowser() { if (this.browser && !this.isClosing) { try { this.isClosing = true; console.log('正在关闭Puppeteer浏览器...'); await this.browser.close(); console.log('Puppeteer浏览器已关闭'); } catch (error) { console.warn('关闭浏览器时出错:', error.message); } finally { this.browser = null; this.isClosing = false; } } } // 延迟函数 delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } // 从HTML中提取PPT尺寸信息 extractPPTDimensions(htmlContent) { try { // 从JavaScript中提取PPT_DIMENSIONS const dimensionMatch = htmlContent.match(/window\.PPT_DIMENSIONS\s*=\s*{\s*width:\s*(\d+),\s*height:\s*(\d+)\s*}/); if (dimensionMatch) { return { width: parseInt(dimensionMatch[1]), height: parseInt(dimensionMatch[2]) }; } // 从viewport meta标签中提取 const viewportMatch = htmlContent.match(/width=(\d+),\s*height=(\d+)/); if (viewportMatch) { return { width: parseInt(viewportMatch[1]), height: parseInt(viewportMatch[2]) }; } // 默认尺寸 return { width: 960, height: 720 }; } catch (error) { console.warn('提取PPT尺寸失败,使用默认尺寸:', error.message); return { width: 960, height: 720 }; } } // 生成简化的PNG图片作为fallback generateFallbackImage(width, height, message = 'Screenshot not available') { // 创建一个简单的SVG作为fallback const svg = ` ${message} Size: ${width}x${height} `; // 将SVG转换为base64编码的PNG const base64Svg = Buffer.from(svg).toString('base64'); const dataUrl = `data:image/svg+xml;base64,${base64Svg}`; // 返回简单的占位图片数据 return Buffer.from(svg, 'utf8'); } // 优化HTML内容以确保精确截图 optimizeHtmlForScreenshot(htmlContent, targetWidth, targetHeight) { console.log(`优化HTML for精确截图, 目标尺寸: ${targetWidth}x${targetHeight}`); // 在标签后立即插入截图优化代码 const optimizedHtml = htmlContent.replace( /(]*>)/i, `$1 ` ); return optimizedHtml; } async generateScreenshotWithPuppeteer(htmlContent, options = {}, retryCount = 0) { try { const browser = await this.initBrowser(); if (!browser) { throw new Error('浏览器初始化失败'); } console.log(`开始生成截图... (Puppeteer尝试 ${retryCount + 1}/${this.maxRetries + 1})`); // 从HTML中提取PPT的精确尺寸 const dimensions = this.extractPPTDimensions(htmlContent); console.log(`检测到PPT尺寸: ${dimensions.width}x${dimensions.height}`); // 优化HTML内容以确保精确截图 const optimizedHtml = this.optimizeHtmlForScreenshot(htmlContent, dimensions.width, dimensions.height); // 创建新页面 let page = null; try { page = await browser.newPage(); // 设置页面超时 page.setDefaultTimeout(45000); page.setDefaultNavigationTimeout(45000); // 设置精确的viewport尺寸 await page.setViewport({ width: dimensions.width, height: dimensions.height, deviceScaleFactor: options.deviceScaleFactor || 1, // 降低设备缩放因子以减少内存使用 }); // 设置页面内容 await page.setContent(optimizedHtml, { waitUntil: ['load', 'domcontentloaded'], timeout: 45000 }); // 等待页面完全渲染 await page.waitForTimeout(1500); // 执行截图,使用精确的剪裁区域 const screenshot = await page.screenshot({ type: options.format || 'jpeg', quality: options.quality || 90, clip: { x: 0, y: 0, width: dimensions.width, height: dimensions.height }, omitBackground: false, captureBeyondViewport: false, }); console.log(`Puppeteer截图成功生成,尺寸: ${dimensions.width}x${dimensions.height}, 数据大小: ${screenshot.length} 字节`); return screenshot; } finally { if (page) { try { await page.close(); } catch (error) { console.warn('关闭页面时出错:', error.message); } } } } catch (error) { console.error(`Puppeteer截图生成失败 (尝试 ${retryCount + 1}):`, error.message); // 如果是目标关闭错误或连接断开,重置浏览器 if (error.message.includes('Target closed') || error.message.includes('Connection closed') || error.message.includes('Protocol error')) { console.log('检测到浏览器连接问题,重置浏览器实例'); this.browser = null; this.isClosing = false; } // 如果还有重试机会,进行重试 if (retryCount < this.maxRetries) { console.log(`等待 ${(retryCount + 1) * 3} 秒后重试...`); await this.delay((retryCount + 1) * 3000); return this.generateScreenshotWithPuppeteer(htmlContent, options, retryCount + 1); } throw error; } } async generateScreenshot(htmlContent, options = {}) { try { // 首先尝试使用Puppeteer return await this.generateScreenshotWithPuppeteer(htmlContent, options); } catch (puppeteerError) { console.warn('Puppeteer截图失败,使用fallback方法:', puppeteerError.message); // 如果Puppeteer失败,生成fallback图片 const dimensions = this.extractPPTDimensions(htmlContent); const fallbackImage = this.generateFallbackImage( dimensions.width, dimensions.height, 'PPT Preview' ); console.log(`生成fallback图片,尺寸: ${dimensions.width}x${dimensions.height}`); return fallbackImage; } } // 兼容旧方法名 async captureScreenshot(htmlContent, width, height, options = {}) { console.log('使用兼容方法captureScreenshot,建议使用generateScreenshot'); return this.generateScreenshot(htmlContent, options); } // 清理资源 async cleanup() { await this.closeBrowser(); } } // 创建单例实例 const screenshotService = new ScreenshotService(); // 进程退出时清理资源 process.on('exit', async () => { await screenshotService.cleanup(); }); process.on('SIGINT', async () => { await screenshotService.cleanup(); process.exit(0); }); process.on('SIGTERM', async () => { await screenshotService.cleanup(); process.exit(0); }); export default screenshotService;