Skip to main content
Glama
stealth-browser.ts8.74 kB
import { chromium, firefox, webkit, Browser, Page, BrowserContext } from 'playwright'; // 簡化的日誌接口 interface Logger { info(message: string, ...args: any[]): void; warn(message: string, ...args: any[]): void; error(message: string, ...args: any[]): void; } // 創建簡單的日誌器 const createSimpleLogger = (name: string): Logger => ({ info: (message: string, ...args: any[]) => console.log(`[${name}] INFO:`, message, ...args), warn: (message: string, ...args: any[]) => console.warn(`[${name}] WARN:`, message, ...args), error: (message: string, ...args: any[]) => console.error(`[${name}] ERROR:`, message, ...args) }); const logger = createSimpleLogger('stealth-browser'); // 簡單的重試函數 async function simpleRetry<T>( fn: () => Promise<T>, retries: number = 3, delay: number = 1000 ): Promise<T> { let lastError: Error; for (let i = 0; i <= retries; i++) { try { return await fn(); } catch (error) { lastError = error as Error; if (i < retries) { logger.warn(`重試 ${i + 1}/${retries}: ${lastError.message}`); await new Promise(resolve => setTimeout(resolve, delay * Math.pow(2, i))); } } } throw lastError!; } // 簡單的速率限制 class SimpleRateLimit { private lastCall: number = 0; constructor(private intervalMs: number) {} async wait(): Promise<void> { const now = Date.now(); const timeSinceLastCall = now - this.lastCall; if (timeSinceLastCall < this.intervalMs) { const waitTime = this.intervalMs - timeSinceLastCall; await new Promise(resolve => setTimeout(resolve, waitTime)); } this.lastCall = Date.now(); } } export interface StealthBrowserOptions { headless?: boolean; timeout?: number; userAgent?: string; proxy?: string; viewport?: { width: number; height: number }; blockedResourceTypes?: string[]; enableStealth?: boolean; browserType?: 'chromium' | 'firefox' | 'webkit'; } export class StealthBrowser { private browser: Browser | null = null; private context: BrowserContext | null = null; private page: Page | null = null; private options: StealthBrowserOptions; private rateLimit: SimpleRateLimit; constructor(options: StealthBrowserOptions = {}) { this.options = { headless: true, timeout: 30000, userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', viewport: { width: 1920, height: 1080 }, blockedResourceTypes: ['image', 'font', 'media'], enableStealth: true, browserType: 'chromium', ...options }; this.rateLimit = new SimpleRateLimit(1000); // 1秒間隔 } async initialize(): Promise<void> { try { logger.info('正在初始化隱身瀏覽器...'); const launchOptions: any = { headless: this.options.headless, args: [ '--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage', '--disable-accelerated-2d-canvas', '--no-first-run', '--no-zygote', '--single-process', '--disable-gpu', '--disable-blink-features=AutomationControlled', '--disable-features=VizDisplayCompositor' ] }; // 添加代理設定 if (this.options.proxy) { launchOptions.proxy = { server: this.options.proxy }; } // 選擇瀏覽器類型 switch (this.options.browserType) { case 'firefox': this.browser = await firefox.launch(launchOptions); break; case 'webkit': this.browser = await webkit.launch(launchOptions); break; default: this.browser = await chromium.launch(launchOptions); } // 創建上下文 this.context = await this.browser.newContext({ userAgent: this.options.userAgent, viewport: this.options.viewport, extraHTTPHeaders: { 'Accept-Language': 'zh-TW,zh;q=0.9,en;q=0.8', 'Accept-Encoding': 'gzip, deflate, br', 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8', 'Cache-Control': 'no-cache', 'Pragma': 'no-cache' } }); // 設定隱身配置 if (this.options.enableStealth) { await this.applyStealth(); } this.page = await this.context.newPage(); // 設定資源攔截 if (this.options.blockedResourceTypes && this.options.blockedResourceTypes.length > 0) { await this.page.route('**/*', (route) => { if (this.options.blockedResourceTypes!.includes(route.request().resourceType())) { route.abort(); } else { route.continue(); } }); } // 設定超時 this.page.setDefaultTimeout(this.options.timeout!); logger.info('隱身瀏覽器初始化完成'); } catch (error) { logger.error('隱身瀏覽器初始化失敗:', error); throw error; } } private async applyStealth(): Promise<void> { if (!this.context) return; // 添加反偵測腳本 await this.context.addInitScript(() => { // 移除 webdriver 標識 Object.defineProperty(navigator, 'webdriver', { get: () => undefined, }); // 隱藏自動化控制特徵 delete (window as any).chrome?.runtime?.onConnect; delete (window as any).chrome?.runtime?.onMessage; // 偽造插件 Object.defineProperty(navigator, 'plugins', { get: () => [ { name: 'Chrome PDF Plugin', description: 'Portable Document Format' }, { name: 'Chrome PDF Viewer', description: 'Portable Document Format' }, { name: 'Native Client', description: 'Native Client' } ], }); // 偽造語言 Object.defineProperty(navigator, 'languages', { get: () => ['zh-TW', 'zh', 'en'], }); // 偽造硬體併發 Object.defineProperty(navigator, 'hardwareConcurrency', { get: () => 4, }); // 偽造記憶體 Object.defineProperty(navigator, 'deviceMemory', { get: () => 8, }); }); } async navigate(url: string): Promise<void> { if (!this.page) { throw new Error('瀏覽器未初始化'); } // 應用速率限制 await this.rateLimit.wait(); await simpleRetry( async () => { logger.info(`正在導航到: ${url}`); await this.page!.goto(url, { waitUntil: 'domcontentloaded', timeout: this.options.timeout }); // 等待頁面完全載入 await this.page!.waitForTimeout(2000); // 模擬人類行為 await this.simulateHumanBehavior(); logger.info(`成功導航到: ${url}`); }, 3, 1000 ); } private async simulateHumanBehavior(): Promise<void> { if (!this.page) return; // 隨機滑鼠移動 await this.page.mouse.move( Math.random() * this.options.viewport!.width, Math.random() * this.options.viewport!.height, { steps: 10 } ); // 隨機滾動 await this.page.evaluate(() => { window.scrollTo(0, Math.random() * document.body.scrollHeight * 0.3); }); // 隨機等待 await this.page.waitForTimeout(500 + Math.random() * 2000); } async getContent(): Promise<{ title: string; content: string; url: string }> { if (!this.page) { throw new Error('瀏覽器未初始化'); } const title = await this.page.title(); const content = await this.page.content(); const url = this.page.url(); return { title, content, url }; } async executeScript(script: string): Promise<any> { if (!this.page) { throw new Error('瀏覽器未初始化'); } return await this.page.evaluate(script); } async screenshot(options?: { path?: string; fullPage?: boolean }): Promise<Buffer> { if (!this.page) { throw new Error('瀏覽器未初始化'); } return await this.page.screenshot({ fullPage: options?.fullPage ?? true, path: options?.path }); } async close(): Promise<void> { try { if (this.page) { await this.page.close(); this.page = null; } if (this.context) { await this.context.close(); this.context = null; } if (this.browser) { await this.browser.close(); this.browser = null; } logger.info('隱身瀏覽器已關閉'); } catch (error) { logger.error('關閉隱身瀏覽器時發生錯誤:', error); } } }

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/SunZhi-Will/website-to-markdown-mcp'

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