Skip to main content
Glama
browser-manager.js13.4 kB
/** * 浏览器管理器 - 反风控核心模块 * 负责浏览器实例管理、指纹生成、代理配置和反风控策略 */ import { chromium, firefox, webkit } from 'playwright'; import stealth from 'puppeteer-extra-plugin-stealth'; import { FingerprintGenerator } from './fingerprint-generator.js'; import { logger } from '../utils/logger.js'; class BrowserManager { constructor(options = {}) { this.browserType = options.browserType || 'chromium'; this.headless = options.headless !== false; this.proxyManager = options.proxyManager; this.fingerprintGenerator = new FingerprintGenerator(); this.instances = new Map(); this.maxInstances = options.maxInstances || 10; this.defaultTimeout = options.defaultTimeout || 30000; } /** * 创建新的浏览器实例 * @param {Object} options - 浏览器配置选项 * @returns {Promise<Object>} 浏览器实例信息 */ async createInstance(options = {}) { try { const instanceId = this.generateInstanceId(); const fingerprint = await this.fingerprintGenerator.generateFingerprint(); const proxy = options.proxy || await this.getRandomProxy(); const browserOptions = await this.buildBrowserOptions(fingerprint, proxy, options); const browser = await this.launchBrowser(browserOptions); const context = await this.createContext(browser, fingerprint, proxy); const instance = { id: instanceId, browser, context, fingerprint, proxy, createdAt: new Date(), lastUsed: new Date(), pages: new Set(), isActive: true }; this.instances.set(instanceId, instance); logger.info(`浏览器实例创建成功`, { instanceId, fingerprint: fingerprint.fingerprint_id, proxy: proxy ? `${proxy.host}:${proxy.port}` : 'none' }); return instance; } catch (error) { logger.error('创建浏览器实例失败', error); throw error; } } /** * 获取页面实例 * @param {string} instanceId - 浏览器实例ID * @param {Object} options - 页面配置选项 * @returns {Promise<Object>} 页面实例 */ async getPage(instanceId, options = {}) { try { const instance = this.instances.get(instanceId); if (!instance || !instance.isActive) { throw new Error('浏览器实例不存在或已关闭'); } const page = await instance.context.newPage(); // 应用反风控策略 await this.applyAntiDetection(page, instance.fingerprint); // 配置页面选项 await this.configurePage(page, options); instance.pages.add(page); instance.lastUsed = new Date(); logger.debug(`获取页面成功`, { instanceId, pageCount: instance.pages.size }); return page; } catch (error) { logger.error('获取页面失败', error); throw error; } } /** * 关闭浏览器实例 * @param {string} instanceId - 浏览器实例ID */ async closeInstance(instanceId) { try { const instance = this.instances.get(instanceId); if (!instance) return; // 关闭所有页面 for (const page of instance.pages) { try { await page.close(); } catch (error) { logger.warn('关闭页面失败', error); } } // 关闭浏览器 await instance.browser.close(); this.instances.delete(instanceId); logger.info(`浏览器实例关闭成功`, { instanceId }); } catch (error) { logger.error('关闭浏览器实例失败', error); } } /** * 获取活跃实例列表 * @returns {Array} 活跃实例信息 */ getActiveInstances() { return Array.from(this.instances.values()).map(instance => ({ id: instance.id, fingerprint: instance.fingerprint.fingerprint_id, proxy: instance.proxy ? `${instance.proxy.host}:${instance.proxy.port}` : 'none', pageCount: instance.pages.size, createdAt: instance.createdAt, lastUsed: instance.lastUsed })); } /** * 清理不活跃的实例 * @param {number} maxInactiveTime - 最大不活跃时间(分钟) */ async cleanupInactiveInstances(maxInactiveTime = 30) { const now = new Date(); const cutoffTime = new Date(now.getTime() - maxInactiveTime * 60 * 1000); const inactiveInstances = Array.from(this.instances.values()) .filter(instance => instance.lastUsed < cutoffTime); for (const instance of inactiveInstances) { await this.closeInstance(instance.id); } logger.info(`清理不活跃实例完成`, { cleanedCount: inactiveInstances.length, remainingCount: this.instances.size }); } /** * 构建浏览器启动选项 * @private */ async buildBrowserOptions(fingerprint, proxy, options) { const browserOptions = { headless: this.headless, args: [ '--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage', '--disable-gpu', '--disable-web-security', '--disable-features=VizDisplayCompositor', '--disable-background-timer-throttling', '--disable-backgrounding-occluded-windows', '--disable-renderer-backgrounding', '--disable-background-networking', '--enable-features=NetworkService,NetworkServiceLogging', '--disable-webgl', '--disable-extensions', '--disable-default-apps', '--disable-sync', '--disable-translate', '--disable-background-timer-throttling', '--disable-renderer-backgrounding', '--disable-backgrounding-occluded-windows', '--disable-features=TranslateUI' ], viewport: { width: fingerprint.viewport_width, height: fingerprint.viewport_height }, userAgent: fingerprint.user_agent, locale: fingerprint.language, timezoneId: fingerprint.timezone, geolocation: { latitude: fingerprint.latitude, longitude: fingerprint.longitude }, permissions: ['geolocation', 'notifications'], ignoreDefaultArgs: ['--enable-automation'], bypassCSP: true }; // 配置代理 if (proxy) { browserOptions.proxy = { server: `${proxy.type}://${proxy.host}:${proxy.port}`, username: proxy.username, password: proxy.password }; } return browserOptions; } /** * 创建浏览器上下文 * @private */ async createContext(browser, fingerprint, proxy) { const context = await browser.newContext({ viewport: { width: fingerprint.viewport_width, height: fingerprint.viewport_height }, userAgent: fingerprint.user_agent, locale: fingerprint.language, timezoneId: fingerprint.timezone, geolocation: { latitude: fingerprint.latitude, longitude: fingerprint.longitude }, permissions: ['geolocation', 'notifications'] }); // 设置Cookie和本地存储 await this.setupContextData(context, fingerprint); return context; } /** * 应用反检测策略 * @private */ async applyAntiDetection(page, fingerprint) { // 注入反检测脚本 await page.addInitScript(() => { // 移除webdriver属性 Object.defineProperty(navigator, 'webdriver', { get: () => undefined, }); // 伪造插件信息 Object.defineProperty(navigator, 'plugins', { get: () => [1, 2, 3, 4, 5], }); // 伪造语言信息 Object.defineProperty(navigator, 'languages', { get: () => ['zh-CN', 'zh', 'en'], }); // 移除自动化标志 delete navigator.__proto__.webdriver; // 伪造屏幕信息 Object.defineProperty(screen, 'width', { get: () => 1920, }); Object.defineProperty(screen, 'height', { get: () => 1080, }); // 伪造WebGL信息 const getParameter = WebGLRenderingContext.prototype.getParameter; WebGLRenderingContext.prototype.getParameter = function(parameter) { if (parameter === 37445) { return 'Intel Inc.'; } if (parameter === 37446) { return 'Intel Iris OpenGL Engine'; } return getParameter(parameter); }; }); // 设置额外的headers await page.setExtraHTTPHeaders({ 'Accept-Language': fingerprint.language, 'Accept-Encoding': 'gzip, deflate, br', 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', 'Cache-Control': 'no-cache', 'Pragma': 'no-cache' }); } /** * 配置页面选项 * @private */ async configurePage(page, options) { // 设置默认超时 page.setDefaultTimeout(this.defaultTimeout); page.setDefaultNavigationTimeout(this.defaultTimeout); // 配置视口 if (options.viewport) { await page.setViewportSize(options.viewport); } // 拦截图片加载(可选) if (options.blockImages) { await page.route('**/*.{png,jpg,jpeg,gif,svg,webp}', route => route.abort()); } // 拦截不必要的资源 await page.route('**/*', (route) => { const resourceType = route.request().resourceType(); const blockedTypes = ['font', 'media', 'websocket']; if (blockedTypes.includes(resourceType)) { route.abort(); } else { route.continue(); } }); // 监听控制台消息 page.on('console', (msg) => { logger.debug('页面控制台消息', { type: msg.type(), text: msg.text(), url: page.url() }); }); // 监听错误 page.on('pageerror', (error) => { logger.warn('页面错误', { error: error.message, url: page.url() }); }); } /** * 设置上下文数据 * @private */ async setupContextData(context, fingerprint) { // 设置本地存储 await context.addInitScript((data) => { localStorage.setItem('fingerprint_id', data.fingerprint_id); localStorage.setItem('user_agent', data.user_agent); }, fingerprint); // 设置Cookie if (fingerprint.cookies) { await context.addCookies(fingerprint.cookies); } } /** * 启动浏览器 * @private */ async launchBrowser(options) { const browserMap = { chromium, firefox, webkit }; const browserEngine = browserMap[this.browserType]; if (!browserEngine) { throw new Error(`不支持的浏览器类型: ${this.browserType}`); } return await browserEngine.launch(options); } /** * 获取随机代理 * @private */ async getRandomProxy() { if (!this.proxyManager) return null; return await this.proxyManager.getRandomProxy(); } /** * 生成实例ID * @private */ generateInstanceId() { return `browser_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } /** * 等待页面加载完成 * @param {Object} page - Playwright页面实例 * @param {Object} options - 等待选项 */ async waitForPageLoad(page, options = {}) { const { timeout = 30000, waitForNetwork = true } = options; try { await page.waitForLoadState('networkidle', { timeout }); if (waitForNetwork) { await page.waitForLoadState('domcontentloaded', { timeout }); } // 等待额外的时间模拟人类行为 await page.waitForTimeout(Math.random() * 2000 + 1000); } catch (error) { logger.warn('页面加载等待超时', { url: page.url(), timeout }); } } /** * 模拟人类行为 * @param {Object} page - Playwright页面实例 */ async simulateHumanBehavior(page) { // 随机滚动 const scrollHeight = await page.evaluate(() => document.body.scrollHeight); const randomScroll = Math.random() * scrollHeight; await page.evaluate((scroll) => { window.scrollTo(0, scroll); }, randomScroll); // 随机等待 await page.waitForTimeout(Math.random() * 3000 + 1000); // 模拟鼠标移动 const viewport = page.viewportSize(); await page.mouse.move( Math.random() * viewport.width, Math.random() * viewport.height ); } /** * 初始化浏览器管理器 */ async initialize() { // 浏览器管理器不需要特殊的初始化逻辑 // 实例会在需要时按需创建 logger.info('浏览器管理器初始化完成'); } /** * 清理浏览器管理器 */ async cleanup() { await this.closeAllInstances(); logger.info('浏览器管理器清理完成'); } /** * 关闭所有实例 */ async closeAllInstances() { const instanceIds = Array.from(this.instances.keys()); for (const instanceId of instanceIds) { await this.closeInstance(instanceId); } logger.info('所有浏览器实例已关闭'); } /** * 获取实例统计信息 */ getStats() { return { totalInstances: this.instances.size, activeInstances: Array.from(this.instances.values()).filter(i => i.isActive).length, totalPages: Array.from(this.instances.values()).reduce((sum, i) => sum + i.pages.size, 0), maxInstances: this.maxInstances }; } } export default BrowserManager;

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/billyangbc/xiaohongshu-mcp-nodejs'

If you have feedback or need assistance with the MCP directory API, please join our Discord server