import puppeteer from 'puppeteer'; // 添加Playwright作为备用截图引擎 import { chromium } from 'playwright'; class ScreenshotService { constructor() { this.browser = null; this.playwrightBrowser = null; // 添加Playwright浏览器实例 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; this.preferredEngine = 'puppeteer'; // 优先使用的截图引擎 } async initBrowser() { 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', '--disable-background-media-suspend', '--disable-backgrounding-occluded-windows', '--disable-renderer-backgrounding', '--disable-field-trial-config', '--disable-component-extensions-with-background-pages', '--disable-permissions-api', '--disable-client-side-phishing-detection', '--no-zygote', '--disable-web-security', '--allow-running-insecure-content', '--disable-features=VizDisplayCompositor,AudioServiceOutOfProcess', '--disable-software-rasterizer', '--disable-canvas-aa', '--disable-2d-canvas-clip-aa', '--disable-gl-drawing-for-tests', '--use-gl=swiftshader' ] }; if (this.isHuggingFaceSpace) { console.log('检测到Hugging Face Space环境,使用单进程模式'); launchOptions.args.push( '--single-process', '--disable-features=site-per-process', '--disable-site-isolation-trials', '--disable-features=BlockInsecurePrivateNetworkRequests' ); } try { const chromePath = process.env.CHROME_BIN || process.env.GOOGLE_CHROME_BIN || '/usr/bin/google-chrome-stable' || '/usr/bin/chromium-browser'; if (chromePath && require('fs').existsSync(chromePath)) { launchOptions.executablePath = chromePath; console.log(`使用Chrome路径: ${chromePath}`); } } catch (pathError) { console.log('未找到自定义Chrome路径,使用默认配置'); } this.browser = await puppeteer.launch(launchOptions); console.log('✅ Puppeteer浏览器初始化成功'); this.browser.on('disconnected', () => { console.log('Puppeteer浏览器连接断开'); this.browser = null; this.isClosing = false; }); this.browserLaunchRetries = 0; } catch (error) { console.error('❌ Puppeteer浏览器初始化失败:', error.message); this.browserLaunchRetries++; if (this.browserLaunchRetries <= this.maxBrowserLaunchRetries) { console.log(`尝试重新初始化浏览器 (${this.browserLaunchRetries}/${this.maxBrowserLaunchRetries})`); await this.delay(3000); return this.initBrowser(); } else { console.warn('⚠️ Puppeteer初始化完全失败,将使用fallback方法'); return null; } } } if (this.browser) { try { await this.browser.version(); console.log('✅ 浏览器连接正常'); } catch (error) { console.warn('浏览器连接已断开,重新初始化:', error.message); this.browser = null; return this.initBrowser(); } } return this.browser; } async initPlaywrightBrowser() { if (this.playwrightBrowser) { try { const context = await this.playwrightBrowser.newContext(); await context.close(); return this.playwrightBrowser; } catch (error) { console.warn('Playwright浏览器连接已断开,重新初始化'); this.playwrightBrowser = null; } } try { console.log('初始化Playwright浏览器...'); const launchOptions = { headless: true, timeout: 30000, args: [ '--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage', '--disable-gpu', '--disable-extensions', '--no-first-run', '--disable-background-timer-throttling', '--disable-renderer-backgrounding', '--disable-backgrounding-occluded-windows' ] }; if (this.isHuggingFaceSpace) { console.log('检测到Hugging Face Space环境,使用Playwright优化配置'); launchOptions.args.push( '--single-process', '--no-zygote', '--disable-web-security' ); } this.playwrightBrowser = await chromium.launch(launchOptions); console.log('✅ Playwright浏览器初始化成功'); return this.playwrightBrowser; } catch (error) { console.error('❌ Playwright浏览器初始化失败:', error.message); return null; } } 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; } } } async closePlaywrightBrowser() { if (this.playwrightBrowser) { try { console.log('正在关闭Playwright浏览器...'); await this.playwrightBrowser.close(); console.log('Playwright浏览器已关闭'); } catch (error) { console.warn('关闭Playwright浏览器时出错:', error.message); } finally { this.playwrightBrowser = null; } } } delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } extractPPTDimensions(htmlContent) { try { 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]) }; } 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 }; } } generateFallbackImage(width, height, message = 'Screenshot not available') { console.log(`🔄 生成fallback图片: ${width}x${height}`); const svg = ` PPT 预览图 ${message} 尺寸: ${width} × ${height} `; return Buffer.from(svg, 'utf8'); } 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 { console.log(`🎯 开始Puppeteer截图生成... (尝试 ${retryCount + 1}/${this.maxRetries + 1})`); const browser = await this.initBrowser(); if (!browser) { throw new Error('浏览器初始化失败'); } const dimensions = this.extractPPTDimensions(htmlContent); console.log(`📐 检测到PPT尺寸: ${dimensions.width}x${dimensions.height}`); const optimizedHtml = this.optimizeHtmlForScreenshot(htmlContent, dimensions.width, dimensions.height); let page = null; try { page = await browser.newPage(); console.log('📄 创建新页面成功'); page.setDefaultTimeout(20000); page.setDefaultNavigationTimeout(20000); await page.setViewport({ width: dimensions.width, height: dimensions.height, deviceScaleFactor: 1, }); console.log(`🖥️ 设置viewport: ${dimensions.width}x${dimensions.height}`); await page.setContent(optimizedHtml, { waitUntil: ['load', 'domcontentloaded'], timeout: 20000 }); console.log('📝 页面内容设置完成'); await page.waitForTimeout(1000); console.log('⏱️ 等待渲染完成'); console.log('📸 开始执行截图...'); const screenshot = await page.screenshot({ type: 'jpeg', quality: 85, 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(); console.log('📄 页面已关闭'); } 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) { const waitTime = (retryCount + 1) * 2; console.log(`⏳ 等待 ${waitTime} 秒后重试...`); await this.delay(waitTime * 1000); return this.generateScreenshotWithPuppeteer(htmlContent, options, retryCount + 1); } throw error; } } async generateScreenshotWithPlaywright(htmlContent, options = {}) { try { console.log('🎭 开始Playwright截图生成...'); const browser = await this.initPlaywrightBrowser(); if (!browser) { throw new Error('Playwright浏览器初始化失败'); } const dimensions = this.extractPPTDimensions(htmlContent); console.log(`📐 Playwright检测到PPT尺寸: ${dimensions.width}x${dimensions.height}`); const context = await browser.newContext({ viewport: { width: dimensions.width, height: dimensions.height }, deviceScaleFactor: 1, hasTouch: false, isMobile: false }); const page = await context.newPage(); await page.setContent(htmlContent, { waitUntil: 'domcontentloaded', timeout: 15000 }); await page.waitForTimeout(800); const screenshot = await page.screenshot({ type: 'jpeg', quality: 90, clip: { x: 0, y: 0, width: dimensions.width, height: dimensions.height }, animations: 'disabled' }); await context.close(); console.log(`✅ Playwright截图成功生成,尺寸: ${dimensions.width}x${dimensions.height}, 数据大小: ${screenshot.length} 字节`); return screenshot; } catch (error) { console.error('❌ Playwright截图生成失败:', error.message); throw error; } } async generateScreenshot(htmlContent, options = {}) { console.log('🎯 开始生成截图...'); if (this.preferredEngine === 'puppeteer') { try { console.log('🚀 尝试使用Puppeteer生成截图'); const screenshot = await this.generateScreenshotWithPuppeteer(htmlContent, options); console.log('✅ Puppeteer截图生成成功'); return screenshot; } catch (puppeteerError) { console.warn('⚠️ Puppeteer截图失败,尝试Playwright:', puppeteerError.message); try { const screenshot = await this.generateScreenshotWithPlaywright(htmlContent, options); console.log('✅ Playwright截图生成成功'); this.preferredEngine = 'playwright'; return screenshot; } catch (playwrightError) { console.warn('⚠️ Playwright截图也失败,使用fallback方法:', playwrightError.message); } } } else { try { console.log('🎭 尝试使用Playwright生成截图'); const screenshot = await this.generateScreenshotWithPlaywright(htmlContent, options); console.log('✅ Playwright截图生成成功'); return screenshot; } catch (playwrightError) { console.warn('⚠️ Playwright截图失败,尝试Puppeteer:', playwrightError.message); try { const screenshot = await this.generateScreenshotWithPuppeteer(htmlContent, options); console.log('✅ Puppeteer截图生成成功'); return screenshot; } catch (puppeteerError) { console.warn('⚠️ Puppeteer截图也失败,使用fallback方法:', puppeteerError.message); } } } const dimensions = this.extractPPTDimensions(htmlContent); const fallbackImage = this.generateFallbackImage( dimensions.width, dimensions.height, '截图引擎不可用,显示占位图' ); console.log(`📋 生成fallback图片,尺寸: ${dimensions.width}x${dimensions.height}`); return fallbackImage; } setPreferredEngine(engine) { if (['puppeteer', 'playwright'].includes(engine)) { this.preferredEngine = engine; console.log(`截图引擎偏好设置为: ${engine}`); } } async captureScreenshot(htmlContent, width, height, options = {}) { console.log('使用兼容方法captureScreenshot,建议使用generateScreenshot'); return this.generateScreenshot(htmlContent, options); } async cleanup() { await Promise.all([ this.closeBrowser(), this.closePlaywrightBrowser() ]); } } 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;