const express = require('express'); const puppeteer = require('puppeteer-core'); const app = express(); const port = process.env.PORT || 4000; const wsEndpoint = process.env.BROWSER_WS_ENDPOINT; const poolSize = parseInt(process.env.MAX_CONCURRENCY, 10) || 5; const renderTimeout = parseInt(process.env.RENDER_TIMEOUT_MS, 10) || 10000; let browser, context; let isReady = false; const pagePool = []; const pageQueue = []; // 初始化单个页面拦截和加载空白页 async function initializePage(page) { await page.setRequestInterception(true); page.on('request', req => { const url = req.url(); if (/\.(png|jpg|jpeg|gif|svg|woff2?|ttf)$/.test(url) || /analytics|gtag|ads|doubleclick/i.test(url)) { req.abort(); } else { req.continue(); } }); await page.goto('about:blank', { timeout: 5000, waitUntil: 'networkidle2' }); } // 创建新的浏览器上下文和页面池 async function createContextAndPages() { if (context) { try { await context.close(); } catch {} } context = await browser.createIncognitoBrowserContext(); pagePool.length = 0; for (let i = 0; i < poolSize; i++) { const page = await context.newPage(); await initializePage(page); pagePool.push(page); } console.log(`Created new context and initialized ${pagePool.length} pages`); } // 定时重启浏览器上下文,避免 browserless 自动关闭 async function periodicContextRestart() { try { console.log('Periodic context restart started...'); isReady = false; await createContextAndPages(); isReady = true; console.log('Periodic context restart done.'); } catch (err) { console.error('Error during periodic context restart:', err); } } // 连接并初始化浏览器和上下文 async function initializeBrowser() { browser = await puppeteer.connect({ browserWSEndpoint: wsEndpoint }); browser.on('disconnected', () => { console.warn('Browser disconnected, marking not ready and reconnecting...'); isReady = false; reconnectBrowser(); }); await createContextAndPages(); isReady = true; console.log('Browser connected and ready'); } // 重连逻辑,失败最多重试5次,失败后退出进程 let reconnectCount = 0; async function reconnectBrowser() { if (browser) { try { await browser.close(); } catch {} } reconnectCount++; if (reconnectCount > 5) { console.error('Reconnect failed 5 times, exiting process'); process.exit(1); } console.log(`Reconnect attempt #${reconnectCount}...`); try { await initializeBrowser(); reconnectCount = 0; // 成功重连后计数归零 } catch (err) { console.error('Reconnect failed:', err.message); setTimeout(reconnectBrowser, 3000); } } // 页面池获取 function acquirePage() { return new Promise(resolve => { if (pagePool.length > 0) { resolve(pagePool.pop()); } else { pageQueue.push(resolve); } }); } // 页面释放 function releasePage(page) { if (page.isClosed()) return; if (pageQueue.length > 0) { const next = pageQueue.shift(); next(page); } else { pagePool.push(page); } } // 启动服务主流程 (async () => { try { await initializeBrowser(); // 每7分钟重启一次上下文,避开 browserless 10分钟超时关闭 setInterval(periodicContextRestart, 7 * 60 * 1000); } catch (err) { console.error('Startup failed:', err.message); reconnectBrowser(); } })(); app.get('/healthz', (req, res) => { res.json({ ready: isReady, poolSize, queueLength: pageQueue.length }); }); app.get('/', (req, res) => { res.send(isReady ? 'Renderer running' : 'Renderer not ready'); }); app.get('/render', async (req, res) => { if (!isReady) return res.status(503).send('Renderer not ready'); const url = req.query.url; if (!url) return res.status(400).send('Missing url parameter'); let page; try { page = await acquirePage(); await page.goto('about:blank', { waitUntil: 'networkidle2', timeout: 5000 }); await page.goto(url, { waitUntil: 'networkidle2', timeout: renderTimeout }); await page.waitForFunction('document.readyState === "complete"', { timeout: 5000 }); const html = await page.content(); res.set('Content-Type', 'text/html; charset=utf-8'); res.send(html); } catch (err) { console.error('Render error:', err.message); res.status(504).send('Render failed: ' + err.message); } finally { if (page && !page.isClosed()) { try { releasePage(page); } catch (e) { console.warn('Release page error:', e.message); await page.close().catch(() => {}); } } } }); app.listen(port, () => { console.log(`Renderer listening on port ${port}`); });