/** * BrowserFactory:standard / real 双轨启动 + 温池 + 独立并发限流 */ const { DEFAULT_UA, MAX_CONCURRENT_BROWSERS, MAX_CONCURRENT_BROWSERS_REAL, QUEUE_TIMEOUT_MS, NAVIGATION_TIMEOUT_MS, PAGE_DEFAULT_TIMEOUT_MS, } = require('./constants'); const { BrowserConcurrencyLimiter } = require('./concurrency'); const { launchStandardBrowser } = require('./launch-standard'); const { launchRealBrowser, isRealBrowserAvailable } = require('./launch-real-browser'); const { browserPool } = require('./browser-pool'); const standardLimiter = new BrowserConcurrencyLimiter(MAX_CONCURRENT_BROWSERS); const realLimiter = new BrowserConcurrencyLimiter(MAX_CONCURRENT_BROWSERS_REAL); 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, }; } function getLimiter(profile) { return profile === 'real' ? realLimiter : standardLimiter; } /** * 冷启动 Browser(供温池 launcher 使用) * @param {{ profile?: 'standard'|'real', extraArgs?: string[], sessionKey?: string }} options */ async function createBrowserSession(options = {}) { const profile = options.profile === 'real' ? 'real' : 'standard'; const extraArgs = options.extraArgs || []; const sessionKey = options.sessionKey || ''; if (profile === 'real') { const { browser, page } = await launchRealBrowser(extraArgs, { sessionKey, profile: 'real' }); return { browser, page, profile: 'real', cleanup: async (destroy = false) => { if (destroy || !sessionKey) { try { await browser.close(); } catch (_) { /* ignore */ } } }, }; } const browser = await launchStandardBrowser(extraArgs, { sessionKey, profile: 'standard' }); return { browser, page: null, profile: 'standard', cleanup: async (destroy = false) => { if (destroy || !sessionKey) { try { await browser.close(); } catch (_) { /* ignore */ } } }, }; } /** * 从温池或冷启动获取 Browser * @param {{ profile?: 'standard'|'real', extraArgs?: string[], sessionKey?: string }} options */ async function acquireBrowserSession(options = {}) { const profile = options.profile === 'real' ? 'real' : 'standard'; const sessionKey = options.sessionKey || ''; const extraArgs = options.extraArgs || []; return browserPool.acquire(sessionKey, profile, async () => createBrowserSession({ profile, extraArgs, sessionKey, })); } 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; } } async function createManagedPage(browser, existingPage, options = {}) { const page = existingPage || await browser.newPage(); page.setDefaultNavigationTimeout(NAVIGATION_TIMEOUT_MS); page.setDefaultTimeout(PAGE_DEFAULT_TIMEOUT_MS); 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; } function getFactoryStats() { return { standard: standardLimiter.getStats(), real: realLimiter.getStats(), realBrowserReady: isRealBrowserAvailable(), pool: browserPool.getStats(), }; } module.exports = { normalizeAntiBot, getLimiter, createBrowserSession, acquireBrowserSession, runWithProfileLimit, createManagedPage, getFactoryStats, standardLimiter, realLimiter, browserPool, QUEUE_TIMEOUT_MS, };