import puppeteer from 'puppeteer'; import playwright from 'playwright'; import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; import crypto from 'crypto'; // 获取当前文件的目录路径 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.healthCheckFailures = 0; this.maxHealthCheckFailures = 3; // 连续失败3次才重启 this.lastRestartTime = 0; this.minRestartInterval = 5 * 60 * 1000; // 最小重启间隔5分钟 // 主浏览器错误计数和故障状态 this.puppeteerErrorCount = 0; this.maxPuppeteerErrors = 5; // 主浏览器连续错误5次后标记为故障 this.puppeteerFailed = false; // 主浏览器是否彻底故障 this.lastPuppeteerError = 0; // 截图缓存 - 避免重复生成 this.screenshotCache = new Map(); this.maxCacheSize = 50; // 最大缓存数量 this.cacheExpireTime = 10 * 60 * 1000; // 缓存10分钟过期 // 字体配置 this.googleFonts = [ 'Noto+Sans+SC:400,700', 'Noto+Serif+SC:400,700' ]; console.log('📸 Screenshot service initialized (optimized version)'); // 启动时预热浏览器 this.warmupBrowsers(); // 启动主浏览器恢复检查 this.startPrimaryBrowserRecovery(); } // 预热浏览器实例 - 优先启动主浏览器 async warmupBrowsers() { setTimeout(async () => { try { console.log('🔥 Warming up primary browser (Puppeteer)...'); await this.initPuppeteer(); if (this.puppeteerReady) { console.log('✅ Primary browser warmup completed'); } else { console.warn('⚠️ Primary browser failed to start, will use fallback when needed'); } } catch (error) { console.error('❌ Primary browser warmup failed:', error.message); this.puppeteerFailed = true; } }, 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-extensions', '--no-first-run', '--no-default-browser-check', '--disable-default-apps', '--disable-features=TranslateUI', '--disable-ipc-flooding-protection', '--disable-features=VizDisplayCompositor', '--disable-gpu-sandbox', // 容器环境优化 - 禁用D-Bus和系统服务 '--disable-dbus', '--disable-field-trial-config', '--disable-translate', '--disable-web-security', '--allow-running-insecure-content', // SVG和字体渲染优化 '--font-render-hinting=none', // 修复字体渲染不一致问题 '--enable-font-antialiasing', // 启用字体抗锯齿 '--force-color-profile=srgb', // 强制使用sRGB颜色配置 '--disable-software-rasterizer', // 禁用软件光栅化,使用硬件加速 '--enable-gpu-rasterization', // 启用GPU光栅化以改善SVG渲染 '--enable-oop-rasterization', // 启用进程外光栅化 '--disable-crash-reporter', // 禁用崩溃报告 '--disable-logging', // 禁用日志记录 // 内存限制优化 '--js-flags=--max-old-space-size=512', // 降低JS内存使用 '--memory-pressure-off', // 进程优化 '--disable-background-networking', '--disable-background-mode', '--disable-renderer-backgrounding', '--disable-backgrounding-occluded-windows', '--disable-background-timer-throttling', // 禁用不必要的功能 '--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', '--disable-gpu-process-crash-limit', // 禁用GPU进程崩溃限制 '--disable-features=VizDisplayCompositor,AudioServiceOutOfProcess', // 禁用可能导致崩溃的功能 '--no-zygote', // 禁用zygote进程 '--disable-accelerated-2d-canvas', // 禁用2D画布硬件加速 '--disable-accelerated-jpeg-decoding', // 禁用JPEG硬件解码 '--disable-accelerated-mjpeg-decode', // 禁用MJPEG硬件解码 '--disable-accelerated-video-decode' // 禁用视频硬件解码 ], timeout: 60000, // 增加超时时间 ignoreHTTPSErrors: true, defaultViewport: null, // 动态设置视口 handleSIGINT: false, // 由我们自己处理 handleSIGTERM: false, handleSIGHUP: false, dumpio: false, // 禁用浏览器日志输出到控制台 protocolTimeout: 60000 // 增加协议超时时间,解决Network.enable超时问题 }; // 检测环境并应用特定优化 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) => { // 优化的资源拦截逻辑,确保SVG相关资源能够正确加载 const resourceType = request.resourceType(); const url = request.url(); // 允许SVG相关资源和基本资源 if (resourceType === 'document' || resourceType === 'stylesheet' || resourceType === 'script' || resourceType === 'font' || url.includes('svg') || url.includes('data:image/svg') || resourceType === 'image') { request.continue(); } else if (resourceType === 'media' && !url.includes('ads')) { // 允许非广告媒体资源 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; link.crossOrigin = 'anonymous'; // 添加跨域支持 // 立即添加到head,不等待DOMContentLoaded const addFontLink = () => { if (document.head) { document.head.appendChild(link); console.log('Google Fonts injected successfully'); } else { // 如果head还不存在,等待一下再试 setTimeout(addFontLink, 10); } }; // 立即尝试添加 addFontLink(); // 同时添加备用字体CSS const style = document.createElement('style'); style.textContent = ` * { font-family: 'Noto Sans SC', 'Microsoft YaHei', 'SimHei', sans-serif !important; } svg text { font-family: 'Noto Sans SC', 'Microsoft YaHei', 'SimHei', sans-serif !important; } `; const addBackupStyle = () => { if (document.head) { document.head.appendChild(style); } else { setTimeout(addBackupStyle, 10); } }; addBackupStyle(); }, googleFontsUrl); } catch (error) { console.warn('⚠️ Failed to inject Google Fonts:', error.message); } } // 监控浏览器健康状态 startBrowserMonitoring() { const checkInterval = 15 * 60 * 1000; // 15分钟检查一次,减少检查频率 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`); // 重置失败计数器 this.healthCheckFailures = 0; // 如果页面过多,关闭多余页面(提高阈值) if (pages.length > this.maxPoolSize * 4) { // 从2倍提高到4倍 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) { this.healthCheckFailures++; console.error(`❌ Browser health check failed (${this.healthCheckFailures}/${this.maxHealthCheckFailures}):`, error.message); // 只有连续失败多次且距离上次重启足够久才重启浏览器 if (this.healthCheckFailures >= this.maxHealthCheckFailures) { const now = Date.now(); if (now - this.lastRestartTime > this.minRestartInterval) { console.log('🔄 Multiple health check failures detected, attempting browser restart...'); await this.restartBrowser(); } else { console.log('⏳ Browser restart skipped - too soon since last restart'); } } } }, checkInterval); } // 重启浏览器 - 优先恢复主浏览器 async restartBrowser() { console.log('🔄 Attempting to restart primary browser...'); try { // 记录重启时间 this.lastRestartTime = Date.now(); // 清理现有资源 await this.cleanup(); // 重新初始化主浏览器 this.puppeteerReady = false; this.puppeteerFailed = false; this.puppeteerErrorCount = 0; // 重置失败计数器 this.healthCheckFailures = 0; await this.initPuppeteer(); if (this.puppeteerReady) { console.log('✅ Primary browser successfully restarted'); // 如果主浏览器恢复,可以关闭副浏览器以节省资源 if (this.playwrightBrowser) { console.log('🔄 Closing secondary browser to save resources'); await this.playwrightBrowser.close().catch(console.error); this.playwrightBrowser = null; this.playwrightReady = false; this.playwrightPagePool = []; } } else { console.warn('⚠️ Primary browser restart failed, marking as failed'); this.puppeteerFailed = true; } } catch (error) { console.error('❌ Browser restart failed:', error.message); this.puppeteerFailed = true; // 重启失败时也要重置计数器,避免无限重试 this.healthCheckFailures = 0; } } // Initialize Playwright browser (fallback) with optimizations async initPlaywright() { if (this.playwrightBrowser) return; try { console.log('🎭 Starting Playwright browser (optimized)...'); // 优化的启动选项 const launchOptions = { headless: true, executablePath: process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH || '/usr/bin/chromium-browser', 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', // 容器环境优化 - 禁用D-Bus和系统服务 '--disable-dbus', '--disable-features=VizDisplayCompositor', '--disable-software-rasterizer', '--disable-background-timer-throttling', '--disable-backgrounding-occluded-windows', '--disable-renderer-backgrounding', '--disable-field-trial-config', '--disable-ipc-flooding-protection', '--no-first-run', '--no-default-browser-check', '--disable-default-apps', '--disable-component-update', '--disable-sync', '--disable-translate', '--disable-web-security', '--allow-running-insecure-content', // 字体渲染优化 '--font-render-hinting=none' ], ignoreDefaultArgs: ['--enable-automation'], chromiumSandbox: false, handleSIGINT: false, handleSIGTERM: false, handleSIGHUP: false, timeout: 15000 }; console.log('🔧 Using Chromium executable:', launchOptions.executablePath); 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); } } // 生成缓存键 generateCacheKey(htmlContent, options) { const { format, quality, width, height } = options; // 使用内容和选项的哈希作为缓存键 const contentHash = crypto.createHash('md5').update(htmlContent).digest('hex').substring(0, 8); return `${contentHash}_${width}x${height}_${format}_${quality}`; } // 清理过期缓存 cleanExpiredCache() { const now = Date.now(); for (const [key, value] of this.screenshotCache.entries()) { if (now - value.timestamp > this.cacheExpireTime) { this.screenshotCache.delete(key); } } // 如果缓存过多,删除最旧的 if (this.screenshotCache.size > this.maxCacheSize) { const entries = Array.from(this.screenshotCache.entries()); entries.sort((a, b) => a[1].timestamp - b[1].timestamp); const toDelete = entries.slice(0, entries.length - this.maxCacheSize); toDelete.forEach(([key]) => this.screenshotCache.delete(key)); } } // 主浏览器恢复检查 - 定期尝试恢复故障的主浏览器 startPrimaryBrowserRecovery() { const recoveryInterval = 30 * 60 * 1000; // 30分钟检查一次恢复 setInterval(async () => { // 只有在主浏览器故障且副浏览器正在运行时才尝试恢复 if (this.puppeteerFailed && this.playwrightReady) { const now = Date.now(); const timeSinceLastError = now - this.lastPuppeteerError; // 距离上次错误至少10分钟后才尝试恢复 if (timeSinceLastError > 10 * 60 * 1000) { console.log('🔄 Attempting to recover primary browser...'); try { // 重置状态 this.puppeteerFailed = false; this.puppeteerErrorCount = 0; this.puppeteerReady = false; // 尝试重新初始化主浏览器 await this.initPuppeteer(); if (this.puppeteerReady) { console.log('✅ Primary browser successfully recovered!'); // 关闭副浏览器以节省资源 if (this.playwrightBrowser) { console.log('🔄 Closing secondary browser after primary recovery'); await this.playwrightBrowser.close().catch(console.error); this.playwrightBrowser = null; this.playwrightReady = false; this.playwrightPagePool = []; } } else { console.warn('⚠️ Primary browser recovery failed, will retry later'); this.puppeteerFailed = true; } } catch (error) { console.error('❌ Primary browser recovery error:', error.message); this.puppeteerFailed = true; } } } }, recoveryInterval); } // Generate screenshot async generateScreenshot(htmlContent, options = {}) { const { format = 'jpeg', quality = 90, width = 1000, height = 562, timeout = 30000 // 增加默认超时时间,与protocolTimeout保持一致 } = options; // 检查缓存 const cacheKey = this.generateCacheKey(htmlContent, { format, quality, width, height }); const cached = this.screenshotCache.get(cacheKey); if (cached && (Date.now() - cached.timestamp < this.cacheExpireTime)) { console.log(`📋 Using cached screenshot: ${cacheKey}`); return cached.data; } console.log(`📸 Generating screenshot: ${width}x${height}, ${format}@${quality}%`); let screenshot = null; // 优先使用主浏览器 (Puppeteer) if (!this.puppeteerFailed) { // 如果主浏览器未就绪,尝试重启(但不是每次都重启) if (!this.puppeteerReady) { const now = Date.now(); if (now - this.lastRestartTime > this.minRestartInterval) { console.log('🔄 Primary browser not ready, attempting restart...'); await this.initPuppeteer(); } } if (this.puppeteerReady) { try { screenshot = await this.generateWithPuppeteer(htmlContent, { format, quality, width, height, timeout }); // 成功时重置错误计数 this.puppeteerErrorCount = 0; } catch (error) { this.puppeteerErrorCount++; this.lastPuppeteerError = Date.now(); console.warn(`Puppeteer screenshot failed (${this.puppeteerErrorCount}/${this.maxPuppeteerErrors}):`, error.message); // 如果错误次数过多,标记主浏览器为故障 if (this.puppeteerErrorCount >= this.maxPuppeteerErrors) { console.error('❌ Primary browser marked as failed after multiple errors'); this.puppeteerFailed = true; await this.cleanup(); // 清理故障的主浏览器 } } } } // 只有在主浏览器故障或截图失败时才启动副浏览器 if (!screenshot && (this.puppeteerFailed || this.puppeteerErrorCount > 0)) { console.log('🔄 Falling back to secondary browser (Playwright)...'); if (!this.playwrightReady) { await this.initPlaywright(); } if (this.playwrightReady) { try { screenshot = await this.generateWithPlaywright(htmlContent, { format, quality, width, height, timeout }); } catch (error) { console.warn('Playwright screenshot failed:', error.message); } } } // Final fallback: generate SVG if (!screenshot) { console.warn('All screenshot methods failed, generating fallback SVG'); screenshot = this.generateFallbackImage(width, height, 'Screenshot Service', 'Browser unavailable'); } // 缓存结果(只缓存成功的截图,不缓存fallback) if (screenshot && format !== 'svg') { this.screenshotCache.set(cacheKey, { data: screenshot, timestamp: Date.now() }); // 清理过期缓存 this.cleanExpiredCache(); console.log(`💾 Cached screenshot: ${cacheKey} (cache size: ${this.screenshotCache.size})`); } return screenshot; } // 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 }); // 设置内容,增加超时时间以解决Network.enable超时问题 await page.setContent(htmlContent, { waitUntil: 'networkidle0', timeout: Math.min(timeout, 20000) // 增加超时时间,解决网络连接超时问题 }); // 等待字体加载 await page.evaluate(() => { return document.fonts ? document.fonts.ready : Promise.resolve(); }); // 等待SVG元素完全加载和渲染(改进版) await page.evaluate(() => { return new Promise(resolve => { const svgs = document.querySelectorAll('svg'); if (svgs.length === 0) { resolve(); return; } console.log(`Found ${svgs.length} SVG elements, waiting for rendering...`); // 检查SVG是否已渲染的函数 const checkSVGRendered = (svg) => { try { // 检查SVG的bounding box是否有效 const bbox = svg.getBBox(); return bbox && bbox.width > 0 && bbox.height > 0; } catch (e) { // 如果getBBox失败,检查SVG是否有子元素 return svg.children.length > 0; } }; // 等待所有SVG渲染完成 const waitForSVGs = () => { let allRendered = true; svgs.forEach(svg => { if (!checkSVGRendered(svg)) { allRendered = false; } }); if (allRendered) { console.log('All SVG elements rendered successfully'); resolve(); } else { // 继续等待 setTimeout(waitForSVGs, 100); } }; // 开始检查 waitForSVGs(); // 超时保护,最多等待5秒 setTimeout(() => { console.log('SVG rendering timeout, proceeding anyway'); resolve(); }, 5000); }); }); // 额外等待时间确保渲染完成,特别是SVG元素 await page.waitForTimeout(1500); // 优化内存使用 if (global.gc && Math.random() < 0.1) { // 随机触发GC,避免每次都执行 global.gc(); } // 优化的截图选项,提高矢量图形质量 const screenshotOptions = { type: format, quality: format === 'jpeg' ? Math.max(quality, 90) : undefined, // 提高JPEG质量 fullPage: false, omitBackground: format === 'png', // PNG格式可以透明背景 clip: { x: 0, y: 0, width, height }, // 添加设备像素比以提高清晰度,特别是对矢量图形 deviceScaleFactor: 2 }; 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 { // 清理页面状态,但不导航到about:blank(减少操作) await page.evaluate(() => { // 清理页面内容但保持基本结构 if (document.body) document.body.innerHTML = ''; if (document.head) { // 保留基本的meta标签和字体链接 const links = document.head.querySelectorAll('link[rel="stylesheet"]'); const metas = document.head.querySelectorAll('meta'); document.head.innerHTML = ''; metas.forEach(meta => document.head.appendChild(meta)); links.forEach(link => document.head.appendChild(link)); } }); // 将页面放回池中 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) { // 如果池已满,尝试将新页面也加入池中(替换最旧的) if (this.puppeteerPagePool.length >= this.maxPoolSize) { const oldPage = this.puppeteerPagePool.shift(); await oldPage.close().catch(console.error); } try { // 重置新页面状态 await page.evaluate(() => { if (document.body) document.body.innerHTML = ''; }); this.puppeteerPagePool.push(page); console.log(`♻️ Added new page to pool (${this.puppeteerPagePool.length} total)`); } catch (e) { console.warn('⚠️ Failed to add page to pool, closing it:', e.message); 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();