/** * BrowserFactory:standard / real 双轨启动 + 独立并发限流 */ const { DEFAULT_UA, MAX_CONCURRENT_BROWSERS, MAX_CONCURRENT_BROWSERS_REAL, QUEUE_TIMEOUT_MS, } = require('./constants'); const { BrowserConcurrencyLimiter } = require('./concurrency'); const { launchStandardBrowser } = require('./launch-standard'); const { launchRealBrowser, isRealBrowserAvailable } = require('./launch-real-browser'); const standardLimiter = new BrowserConcurrencyLimiter(MAX_CONCURRENT_BROWSERS); const realLimiter = new BrowserConcurrencyLimiter(MAX_CONCURRENT_BROWSERS_REAL); /** * @typedef {Object} AntiBotConfig * @property {boolean} [enabled] * @property {'standard'|'real'} [profile] * @property {boolean} [turnstile] * @property {boolean} [solverFallback] * @property {string} [sessionKey] * @property {number} [challengeTimeoutMs] */ /** * 规范化 antiBot 配置(未传则走 standard) * @param {AntiBotConfig|null|undefined} antiBot * @returns {AntiBotConfig} */ function normalizeAntiBot(antiBot) { if (!antiBot || !antiBot.enabled) { return { enabled: false, profile: 'standard' }; } return { enabled: true, profile: antiBot.profile === 'real' ? 'real' : 'standard', turnstile: antiBot.turnstile !== false, solverFallback: antiBot.solverFallback !== false, sessionKey: antiBot.sessionKey || '', challengeTimeoutMs: antiBot.challengeTimeoutMs || 60000, }; } /** * 按 profile 获取限流器 * @param {'standard'|'real'} profile */ function getLimiter(profile) { return profile === 'real' ? realLimiter : standardLimiter; } /** * 创建 Browser 会话 * @param {{ profile?: 'standard'|'real', extraArgs?: string[] }} options * @returns {Promise<{ browser: import('puppeteer').Browser, page: import('puppeteer').Page|null, profile: string, cleanup: () => Promise }>} */ async function createBrowserSession(options = {}) { const profile = options.profile === 'real' ? 'real' : 'standard'; const extraArgs = options.extraArgs || []; if (profile === 'real') { const { browser, page } = await launchRealBrowser(extraArgs); return { browser, page, profile: 'real', cleanup: async () => { try { await browser.close(); } catch (_) { /* ignore */ } }, }; } const browser = await launchStandardBrowser(extraArgs); return { browser, page: null, profile: 'standard', cleanup: async () => { try { await browser.close(); } catch (_) { /* ignore */ } }, }; } /** * 在并发槽位内执行 Browser 任务 * @template T * @param {string} taskName * @param {'standard'|'real'} profile * @param {() => Promise} fn * @returns {Promise} */ async function runWithProfileLimit(taskName, profile, fn) { const limiter = getLimiter(profile); try { return await limiter.run(taskName, fn); } catch (err) { if (err.message === 'QUEUE_TIMEOUT') err.code = 'QUEUE_TIMEOUT'; throw err; } } /** * 创建 Page 并设置 UA / viewport / cookies * @param {import('puppeteer').Browser} browser * @param {import('puppeteer').Page|null} existingPage * @param {{ userAgent?: string, viewport?: object, cookies?: Array }} [options] */ async function createManagedPage(browser, existingPage, options = {}) { const page = existingPage || await browser.newPage(); page.setDefaultNavigationTimeout(30000); page.setDefaultTimeout(15000); const userAgent = options.userAgent || DEFAULT_UA; await page.setUserAgent(userAgent); if (options.viewport) { await page.setViewport(options.viewport); } if (options.cookies && options.cookies.length > 0) { await page.setCookie(...options.cookies); } page.on('error', (err) => console.error('[Page Error]', err.message)); page.on('pageerror', (err) => console.error('[Page JS Error]', err.message)); return page; } /** * @returns {{ standard: object, real: object, realBrowserReady: boolean }} */ function getFactoryStats() { return { standard: standardLimiter.getStats(), real: realLimiter.getStats(), realBrowserReady: isRealBrowserAvailable(), }; } module.exports = { normalizeAntiBot, getLimiter, createBrowserSession, runWithProfileLimit, createManagedPage, getFactoryStats, standardLimiter, realLimiter, QUEUE_TIMEOUT_MS, };