import puppeteer from 'puppeteer'; import playwright from 'playwright'; import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; // 获取当前文件的目录路径 const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); class ScreenshotService { constructor() { this.puppeteerBrowser = null; this.playwrightBrowser = null; this.isInitializing = false; this.puppeteerReady = false; this.playwrightReady = false; // 页面池 - 重用页面以提高性能 this.puppeteerPagePool = []; this.playwrightPagePool = []; this.maxPoolSize = 3; // 最大页面池大小 // 字体配置 this.googleFonts = [ 'Noto+Sans+SC:400,700', 'Noto+Serif+SC:400,700' ]; console.log('📸 Screenshot service initialized (optimized version)'); // 启动时预热浏览器 this.warmupBrowsers(); } // 预热浏览器实例 async warmupBrowsers() { setTimeout(async () => { try { console.log('🔥 Warming up browser instances...'); await this.initPuppeteer(); console.log('✅ Browser warmup complete'); } catch (error) { console.error('❌ Browser warmup failed:', error.message); } }, 1000); // 延迟1秒启动,避免与服务器启动冲突 } // Initialize Puppeteer browser with optimizations async initPuppeteer() { if (this.puppeteerBrowser || this.isInitializing) return; this.isInitializing = true; try { console.log('🚀 Starting Puppeteer browser (optimized)...'); // 优化的启动选项 const launchOptions = { headless: 'new', // 使用新的无头模式 args: [ // 安全性设置 '--no-sandbox', '--disable-setuid-sandbox', // 内存和性能优化 '--disable-dev-shm-usage', '--disable-gpu', '--disable-extensions', '--disable-background-timer-throttling', '--disable-backgrounding-occluded-windows', '--disable-renderer-backgrounding', '--no-first-run', '--no-default-browser-check', '--disable-default-apps', '--disable-features=TranslateUI', '--disable-ipc-flooding-protection', // 内存限制优化 '--js-flags=--max-old-space-size=512', // 降低JS内存使用 '--memory-pressure-off', // 进程优化 '--single-process', // 使用单进程模式减少资源消耗 '--disable-background-networking', '--disable-background-mode', // 字体渲染优化 '--font-render-hinting=none', // 禁用不必要的功能 '--disable-breakpad', '--disable-component-update', '--disable-domain-reliability', '--disable-sync', '--disable-hang-monitor', '--disable-prompt-on-repost', '--disable-client-side-phishing-detection', '--disable-component-extensions-with-background-pages', '--blink-settings=imagesEnabled=true' ], timeout: 20000, // 减少超时时间 ignoreHTTPSErrors: true, defaultViewport: null, // 动态设置视口 handleSIGINT: false, // 由我们自己处理 handleSIGTERM: false, handleSIGHUP: false, dumpio: false, // 禁用浏览器日志输出到控制台 protocolTimeout: 15000 }; // 检测环境并应用特定优化 const isLowMemoryEnv = process.env.LOW_MEMORY === 'true'; if (isLowMemoryEnv) { console.log('🔧 Applying low memory optimizations'); launchOptions.args.push( '--disable-javascript', '--disable-images', '--disable-css-animations' ); } this.puppeteerBrowser = await puppeteer.launch(launchOptions); this.puppeteerReady = true; console.log('✅ Puppeteer browser started successfully'); // 预创建页面池 await this.initPagePool(); // 监控浏览器健康状态 this.startBrowserMonitoring(); // Cleanup when process exits process.on('exit', () => this.cleanup()); process.on('SIGINT', () => this.cleanup()); process.on('SIGTERM', () => this.cleanup()); } catch (error) { console.error('❌ Puppeteer initialization failed:', error.message); this.puppeteerReady = false; } finally { this.isInitializing = false; } } // 初始化页面池 async initPagePool() { if (!this.puppeteerReady || !this.puppeteerBrowser) return; try { console.log(`🔄 Initializing page pool (size: ${this.maxPoolSize})...`); // 清空现有池 for (const page of this.puppeteerPagePool) { await page.close().catch(console.error); } this.puppeteerPagePool = []; // 创建新页面 for (let i = 0; i < this.maxPoolSize; i++) { const page = await this.puppeteerBrowser.newPage(); // 优化页面设置 await page.setRequestInterception(true); page.on('request', (request) => { // 阻止不必要的资源加载 const resourceType = request.resourceType(); if (resourceType === 'media' || resourceType === 'font') { // 允许字体资源加载 request.continue(); } else if (resourceType === 'stylesheet' || resourceType === 'script' || resourceType === 'image') { // 允许基本资源 request.continue(); } else { // 阻止其他资源 request.abort(); } }); // 注入Google字体 await this.injectGoogleFonts(page); // 设置默认视口 await page.setViewport({ width: 1000, height: 562 }); // 添加到池中 this.puppeteerPagePool.push(page); } console.log(`✅ Page pool initialized with ${this.puppeteerPagePool.length} pages`); } catch (error) { console.error('❌ Failed to initialize page pool:', error.message); } } // 注入Google字体 async injectGoogleFonts(page) { try { // 构建Google字体URL const googleFontsUrl = `https://fonts.googleapis.com/css2?family=${this.googleFonts.join('&family=')}&display=swap`; // 注入字体CSS await page.evaluateOnNewDocument((fontsUrl) => { // 创建link元素加载Google字体 const link = document.createElement('link'); link.rel = 'stylesheet'; link.href = fontsUrl; // 当文档创建时添加到head document.addEventListener('DOMContentLoaded', () => { document.head.appendChild(link); }); }, googleFontsUrl); } catch (error) { console.warn('⚠️ Failed to inject Google Fonts:', error.message); } } // 监控浏览器健康状态 startBrowserMonitoring() { const checkInterval = 5 * 60 * 1000; // 5分钟检查一次 setInterval(async () => { if (!this.puppeteerBrowser) return; try { // 检查浏览器是否响应 await this.puppeteerBrowser.version(); // 检查内存使用情况 const pages = await this.puppeteerBrowser.pages(); console.log(`🔍 Browser health check: ${pages.length} pages open`); // 如果页面过多,关闭多余页面 if (pages.length > this.maxPoolSize * 2) { console.log(`⚠️ Too many pages open (${pages.length}), cleaning up...`); // 保留页面池中的页面,关闭其他页面 const poolPageIds = this.puppeteerPagePool.map(p => p.target()._targetId); for (const page of pages) { const pageId = page.target()._targetId; if (!poolPageIds.includes(pageId)) { await page.close().catch(console.error); } } } } catch (error) { console.error('❌ Browser health check failed:', error.message); // 尝试重启浏览器 await this.restartBrowser(); } }, checkInterval); } // 重启浏览器 async restartBrowser() { console.log('🔄 Attempting to restart browser...'); try { // 清理现有资源 await this.cleanup(); // 重新初始化 this.puppeteerReady = false; this.playwrightReady = false; await this.initPuppeteer(); console.log('✅ Browser successfully restarted'); } catch (error) { console.error('❌ Browser restart failed:', error.message); } } // Initialize Playwright browser (fallback) with optimizations async initPlaywright() { if (this.playwrightBrowser) return; try { console.log('🎭 Starting Playwright browser (optimized)...'); // 优化的启动选项 const launchOptions = { headless: true, args: [ // 安全性设置 '--no-sandbox', '--disable-setuid-sandbox', // 内存和性能优化 '--disable-dev-shm-usage', '--disable-gpu', '--disable-extensions', '--js-flags=--max-old-space-size=512', '--single-process', '--disable-background-networking', // 字体渲染优化 '--font-render-hinting=none' ], ignoreDefaultArgs: ['--enable-automation'], chromiumSandbox: false, handleSIGINT: false, handleSIGTERM: false, handleSIGHUP: false, timeout: 15000 }; this.playwrightBrowser = await playwright.chromium.launch(launchOptions); // 初始化页面池 await this.initPlaywrightPagePool(); this.playwrightReady = true; console.log('✅ Playwright browser started successfully'); } catch (error) { console.error('❌ Playwright initialization failed:', error.message); this.playwrightReady = false; } } // 初始化Playwright页面池 async initPlaywrightPagePool() { if (!this.playwrightBrowser) return; try { console.log(`🔄 Initializing Playwright page pool...`); // 清空现有池 for (const page of this.playwrightPagePool) { await page.close().catch(console.error); } this.playwrightPagePool = []; // 创建新页面 for (let i = 0; i < this.maxPoolSize; i++) { const context = await this.playwrightBrowser.newContext({ viewport: { width: 1000, height: 562 }, ignoreHTTPSErrors: true }); const page = await context.newPage(); // 注入Google字体 await this.injectPlaywrightGoogleFonts(page); // 添加到池中 this.playwrightPagePool.push(page); } console.log(`✅ Playwright page pool initialized with ${this.playwrightPagePool.length} pages`); } catch (error) { console.error('❌ Failed to initialize Playwright page pool:', error.message); } } // 注入Google字体到Playwright页面 async injectPlaywrightGoogleFonts(page) { try { // 构建Google字体URL const googleFontsUrl = `https://fonts.googleapis.com/css2?family=${this.googleFonts.join('&family=')}&display=swap`; // 注入字体CSS await page.addInitScript(({ fontsUrl }) => { // 创建link元素加载Google字体 const link = document.createElement('link'); link.rel = 'stylesheet'; link.href = fontsUrl; // 当文档创建时添加到head document.addEventListener('DOMContentLoaded', () => { document.head.appendChild(link); }); }, { fontsUrl: googleFontsUrl }); } catch (error) { console.warn('⚠️ Failed to inject Google Fonts to Playwright:', error.message); } } // Generate screenshot async generateScreenshot(htmlContent, options = {}) { const { format = 'jpeg', quality = 90, width = 1000, height = 562, timeout = 15000 } = options; console.log(`📸 Generating screenshot: ${width}x${height}, ${format}@${quality}%`); // Try Puppeteer first if (!this.puppeteerReady) { await this.initPuppeteer(); } if (this.puppeteerReady) { try { return await this.generateWithPuppeteer(htmlContent, { format, quality, width, height, timeout }); } catch (error) { console.warn('Puppeteer screenshot failed, trying Playwright:', error.message); } } // Fallback to Playwright if (!this.playwrightReady) { await this.initPlaywright(); } if (this.playwrightReady) { try { return await this.generateWithPlaywright(htmlContent, { format, quality, width, height, timeout }); } catch (error) { console.warn('Playwright screenshot failed:', error.message); } } // Final fallback: generate SVG console.warn('All screenshot methods failed, generating fallback SVG'); return this.generateFallbackImage(width, height, 'Screenshot Service', 'Browser unavailable'); } // Generate screenshot using Puppeteer with page pool optimization async generateWithPuppeteer(htmlContent, options) { const { format, quality, width, height, timeout } = options; let page = null; let pageFromPool = false; try { // 尝试从页面池获取页面 if (this.puppeteerPagePool && this.puppeteerPagePool.length > 0) { page = this.puppeteerPagePool.shift(); pageFromPool = true; console.log(`📄 Using page from Puppeteer pool (${this.puppeteerPagePool.length} remaining)`); } else { // 如果池为空,创建新页面 page = await this.puppeteerBrowser.newPage(); console.log(`📄 Created new Puppeteer page (pool empty)`); // 优化新页面设置 await page.setRequestInterception(true); page.on('request', (request) => { // 阻止不必要的资源加载 const resourceType = request.resourceType(); if (resourceType === 'media' || resourceType === 'font') { // 允许字体资源加载 request.continue(); } else if (resourceType === 'stylesheet' || resourceType === 'script' || resourceType === 'image') { // 允许基本资源 request.continue(); } else { // 阻止其他资源 request.abort(); } }); // 为新创建的页面注入Google字体 await this.injectGoogleFonts(page); } // 设置视口大小(即使从池中获取的页面也需要重新设置,因为尺寸可能不同) await page.setViewport({ width, height }); // 设置内容,使用更短的超时时间 await page.setContent(htmlContent, { waitUntil: 'networkidle0', timeout: Math.min(timeout, 10000) // 使用较短的超时时间 }); // 等待字体加载 await page.evaluate(() => { return document.fonts ? document.fonts.ready : Promise.resolve(); }); // 减少等待时间 await page.waitForTimeout(200); // 优化内存使用 if (global.gc && Math.random() < 0.1) { // 随机触发GC,避免每次都执行 global.gc(); } // 截图选项 const screenshotOptions = { type: format, quality: format === 'jpeg' ? quality : undefined, fullPage: false, omitBackground: format === 'png', // PNG格式可以透明背景 clip: { x: 0, y: 0, width, height } }; const screenshot = await page.screenshot(screenshotOptions); console.log(`✅ Puppeteer screenshot generated: ${screenshot.length} bytes`); return screenshot; } catch (error) { console.error(`❌ Puppeteer screenshot failed: ${error.message}`); throw error; } finally { if (page) { if (pageFromPool && this.puppeteerPagePool.length < this.maxPoolSize) { // 重置页面状态 try { await page.goto('about:blank').catch(() => {}); // 将页面放回池中 this.puppeteerPagePool.push(page); console.log(`♻️ Returned Puppeteer page to pool (${this.puppeteerPagePool.length} total)`); } catch (e) { console.warn('⚠️ Failed to reset page, closing it:', e.message); await page.close().catch(console.error); } } else if (!pageFromPool) { // 关闭非池中的页面 await page.close().catch(console.error); } } } } // Generate screenshot using Playwright with page pool optimization async generateWithPlaywright(htmlContent, options) { const { format, quality, width, height, timeout } = options; let page = null; let pageFromPool = false; try { // 尝试从页面池获取页面 if (this.playwrightPagePool && this.playwrightPagePool.length > 0) { page = this.playwrightPagePool.shift(); pageFromPool = true; console.log(`📄 Using page from Playwright pool (${this.playwrightPagePool.length} remaining)`); } else { // 如果池为空,创建新页面 const context = await this.playwrightBrowser.newContext({ viewport: { width, height }, ignoreHTTPSErrors: true }); page = await context.newPage(); console.log(`📄 Created new Playwright page (pool empty)`); // 为新创建的页面注入Google字体 await this.injectPlaywrightGoogleFonts(page); } // 设置视口大小(即使从池中获取的页面也需要重新设置,因为尺寸可能不同) await page.setViewportSize({ width, height }); // 设置内容,使用更短的超时时间 await page.setContent(htmlContent, { waitUntil: 'networkidle', timeout: Math.min(timeout, 10000) // 使用较短的超时时间 }); // 等待字体加载 await page.evaluate(() => { return document.fonts ? document.fonts.ready : Promise.resolve(); }); // 减少等待时间 await page.waitForTimeout(200); // 优化内存使用 if (global.gc && Math.random() < 0.1) { // 随机触发GC,避免每次都执行 global.gc(); } // 截图选项 const screenshotOptions = { type: format, quality: format === 'jpeg' ? quality : undefined, fullPage: false, omitBackground: format === 'png', // PNG格式可以透明背景 clip: { x: 0, y: 0, width, height } }; const screenshot = await page.screenshot(screenshotOptions); console.log(`✅ Playwright screenshot generated: ${screenshot.length} bytes`); return screenshot; } catch (error) { console.error(`❌ Playwright screenshot failed: ${error.message}`); throw error; } finally { if (page) { if (pageFromPool && this.playwrightPagePool.length < this.maxPoolSize) { // 重置页面状态 try { await page.goto('about:blank').catch(() => {}); // 将页面放回池中 this.playwrightPagePool.push(page); console.log(`♻️ Returned Playwright page to pool (${this.playwrightPagePool.length} total)`); } catch (e) { console.warn('⚠️ Failed to reset page, closing it:', e.message); await page.close().catch(console.error); } } else if (!pageFromPool) { // 关闭非池中的页面 await page.close().catch(console.error); } } } } // Generate fallback SVG image generateFallbackImage(width = 1000, height = 562, title = 'PPT Screenshot', subtitle = '', message = '') { const svg = ` ${title} ${subtitle ? ` ${subtitle} ` : ''} ${message ? ` ${message} ` : ''} PPT Screenshot Service • ${new Date().toLocaleString('zh-CN')} Size: ${width} × ${height} `; return Buffer.from(svg, 'utf-8'); } // Get service status getStatus() { return { puppeteerReady: this.puppeteerReady, playwrightReady: this.playwrightReady, environment: process.env.NODE_ENV || 'development', isHuggingFace: !!process.env.SPACE_ID }; } // Cleanup resources async cleanup() { console.log('🧹 Cleaning up screenshot service...'); if (this.puppeteerBrowser) { try { await this.puppeteerBrowser.close(); this.puppeteerBrowser = null; this.puppeteerReady = false; console.log('✅ Puppeteer browser closed'); } catch (error) { console.error('Error closing Puppeteer browser:', error); } } if (this.playwrightBrowser) { try { await this.playwrightBrowser.close(); this.playwrightBrowser = null; this.playwrightReady = false; console.log('✅ Playwright browser closed'); } catch (error) { console.error('Error closing Playwright browser:', error); } } } } export default new ScreenshotService();