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