import puppeteer from "puppeteer"; let browser; const pagePool = []; const MAX_PAGES = Number.parseInt(process.env.CHART_MAX_PAGES || "1", 10); const DEFAULT_TIMEOUT_MS = Number.parseInt( process.env.CHART_RENDER_TIMEOUT_MS || "", 10, ); const EFFECTIVE_TIMEOUT_MS = Number.isFinite(DEFAULT_TIMEOUT_MS) ? DEFAULT_TIMEOUT_MS : 500000; const isBrowserAlive = () => browser && browser.isConnected(); const clearPagePool = async () => { const pages = pagePool.splice(0, pagePool.length); await Promise.allSettled( pages.map(async (page) => { if (page && !page.isClosed()) await page.close(); }), ); }; const createPage = async () => { if (!isBrowserAlive()) { await initBrowserPool(); } const page = await browser.newPage(); page.setDefaultTimeout(EFFECTIVE_TIMEOUT_MS); page.setDefaultNavigationTimeout(EFFECTIVE_TIMEOUT_MS); return page; }; const isUsablePage = (page) => page && !page.isClosed() && page.browser()?.isConnected(); /** * 初始化浏览器 + 预创建 page */ export const initBrowserPool = async () => { if (!isBrowserAlive()) { await clearPagePool(); browser = null; browser = await puppeteer.launch({ headless: "new", executablePath: process.env.CHROME_PATH || undefined, args: [ "--no-sandbox", "--disable-setuid-sandbox", "--disable-dev-shm-usage", "--disable-background-timer-throttling", "--disable-backgrounding-occluded-windows", "--disable-renderer-backgrounding", ], // 避免渲染压力下 CDP 调用超时(对应报错:Runtime.callFunctionOn timed out) protocolTimeout: EFFECTIVE_TIMEOUT_MS, }); browser.on("disconnected", () => { browser = null; pagePool.splice(0, pagePool.length); }); console.log("✅ Browser initialized"); // 预创建 page for (let i = 0; i < MAX_PAGES; i++) { pagePool.push(await createPage()); } console.log(`✅ Page pool initialized: ${MAX_PAGES}`); } }; /** * 获取 page(核心) */ export const getPage = async () => { if (!isBrowserAlive()) { await initBrowserPool(); } if (pagePool.length > 0) { while (pagePool.length > 0) { const page = pagePool.pop(); if (isUsablePage(page)) return page; await discardPage(page); } } // pool 空了就新建(兜底) return createPage(); }; /** * 释放 page */ export const releasePage = async (page) => { if (!page) return; try { if (!isUsablePage(page)) { await discardPage(page); return; } if (pagePool.length < MAX_PAGES) { pagePool.push(page); } else { await page.close(); } } catch (err) { console.error("❌ releasePage error:", err); } }; export const discardPage = async (page) => { if (!page) return; try { if (!page.isClosed()) await page.close(); } catch (err) { console.warn("⚠️ discardPage failed:", err.message); } }; /** * 关闭 */ export const closeBrowserPool = async () => { try { await clearPagePool(); if (browser) { await browser.close(); browser = null; } console.log("🛑 Browser pool closed"); } catch (err) { console.error("❌ closeBrowserPool error:", err); } };