import type { Page } from 'playwright'; // Gaussian random using Box-Muller transform function gaussian(mean: number, sigma: number): number { const u1 = Math.random(); const u2 = Math.random(); const z = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2); return mean + z * sigma; } export async function humanDelay(min = 80, max = 450): Promise { const mean = (min + max) / 2; const sigma = (max - min) / 6; const delay = Math.max(min, Math.min(max, gaussian(mean, sigma))); await new Promise(r => setTimeout(r, Math.round(delay))); } export async function simulateMouseMovement(page: Page, targetX: number, targetY: number): Promise { try { const mouse = page.mouse; const steps = 8 + Math.floor(Math.random() * 8); // Generate Bezier control points with slight perpendicular offset const startX = targetX - 100 + Math.random() * 200; const startY = targetY - 100 + Math.random() * 200; const cpX = (startX + targetX) / 2 + (Math.random() - 0.5) * 80; const cpY = (startY + targetY) / 2 + (Math.random() - 0.5) * 80; for (let i = 0; i <= steps; i++) { const t = i / steps; // Quadratic Bezier: B(t) = (1-t)^2 * P0 + 2(1-t)t * P1 + t^2 * P2 const x = (1 - t) * (1 - t) * startX + 2 * (1 - t) * t * cpX + t * t * targetX; const y = (1 - t) * (1 - t) * startY + 2 * (1 - t) * t * cpY + t * t * targetY; await mouse.move(x + (Math.random() - 0.5) * 2, y + (Math.random() - 0.5) * 2); if (i < steps) await humanDelay(8, 25); } // Small overshoot correction await mouse.move(targetX + (Math.random() - 0.5) * 4, targetY + (Math.random() - 0.5) * 4); await humanDelay(30, 80); await mouse.move(targetX, targetY); } catch { /* non-fatal */ } } export async function simulateMouseHover(page: Page, selector: string): Promise { try { const el = await page.$(selector); if (!el) return; const box = await el.boundingBox(); if (!box) return; const cx = box.x + box.width / 2; const cy = box.y + box.height / 2; await simulateMouseMovement(page, cx, cy); await humanDelay(100, 300); } catch { /* non-fatal */ } } export async function simulateClick(page: Page, selector: string): Promise { try { await simulateMouseHover(page, selector); await humanDelay(50, 150); await page.click(selector, { delay: Math.round(gaussian(80, 30)) }); } catch { /* non-fatal */ } } export async function injectFingerprintOverrides(page: Page): Promise { await page.addInitScript(() => { // Canvas noise — subtle per-pixel perturbation const origToDataURL = HTMLCanvasElement.prototype.toDataURL; HTMLCanvasElement.prototype.toDataURL = function(this: HTMLCanvasElement, ...args: any[]) { const ctx = this.getContext('2d'); if (ctx) { const imageData = ctx.getImageData(0, 0, this.width, this.height); for (let i = 0; i < imageData.data.length; i += 4) { imageData.data[i] = Math.max(0, Math.min(255, imageData.data[i] + (Math.random() < 0.01 ? (Math.random() > 0.5 ? 1 : -1) : 0))); } ctx.putImageData(imageData, 0, 0); } return origToDataURL.apply(this, args as [type?: string, quality?: number]); }; // WebGL parameter spoofing const origGetParam = WebGLRenderingContext.prototype.getParameter; WebGLRenderingContext.prototype.getParameter = function(this: WebGLRenderingContext, parameter: number) { const UNMASKED_VENDOR_WEBGL = 0x9245; const UNMASKED_RENDERER_WEBGL = 0x9246; if (parameter === UNMASKED_VENDOR_WEBGL) return 'Intel Inc.'; if (parameter === UNMASKED_RENDERER_WEBGL) return 'Intel Iris OpenGL Engine'; return origGetParam.call(this, parameter); }; // Hardware concurrency — pick from common values const cpuCounts = [4, 6, 8, 8]; Object.defineProperty(navigator, 'hardwareConcurrency', { get: () => cpuCounts[Math.floor(Math.random() * cpuCounts.length)], configurable: true, }); // Device memory Object.defineProperty(navigator, 'deviceMemory', { get: () => [4, 8][Math.floor(Math.random() * 2)], configurable: true, }); }).catch(() => {}); } export async function infiniteScrollToBottom( page: Page, opts: { maxScrolls?: number; scrollDelay?: number; stabilizeMs?: number } = {}, rawPage?: any, ): Promise<{ scrollCount: number; heightReached: number }> { const { maxScrolls = 50, scrollDelay = 400, stabilizeMs = 600 } = opts; let scrollCount = 0; let stableScrollCount = 0; let previousHeight = 0; // One CDP session for wheel events (triggers IntersectionObserver-based virtual scrollers). // React-window / react-virtualized / TanStack Virtual ignore window.scrollBy but respond to // wheel events because they attach their own scroll listeners to the list container. let cdpForWheel: any = null; let viewW = 1280; let viewH = 800; if (rawPage && typeof rawPage.context === 'function') { try { const ctx = rawPage.context(); if (typeof ctx?.newCDPSession === 'function') { cdpForWheel = await ctx.newCDPSession(rawPage); const { result } = await cdpForWheel.send('Runtime.evaluate', { expression: `({"w":window.innerWidth,"h":window.innerHeight})`, returnByValue: true, }).catch(() => ({ result: { value: null } })); if (result?.value) { viewW = result.value.w ?? 1280; viewH = result.value.h ?? 800; } } } catch { cdpForWheel = null; } } try { while (scrollCount < maxScrolls) { previousHeight = await page.evaluate(() => document.documentElement.scrollHeight).catch(() => 0); const scrollBy = 500 + Math.floor(Math.random() * 300); // window.scrollBy handles standard scroll containers await page.evaluate((by: number) => window.scrollBy(0, by), scrollBy).catch(() => {}); // CDP wheel event triggers IntersectionObserver + virtual list scroll listeners if (cdpForWheel) { await cdpForWheel.send('Input.dispatchMouseEvent', { type: 'mouseWheel', x: Math.floor(viewW / 2), y: Math.floor(viewH / 2), deltaX: 0, deltaY: scrollBy, modifiers: 0, }).catch(() => {}); } await humanDelay(scrollDelay - 100, scrollDelay + 200); // Use Node.js setTimeout — page.waitForTimeout may not be available on Stagehand proxy await new Promise(r => setTimeout(r, stabilizeMs)); scrollCount++; const newHeight: number = await page.evaluate(() => document.documentElement.scrollHeight).catch(() => 0); if (newHeight - previousHeight < 10) { stableScrollCount++; if (stableScrollCount >= 3) break; // no new content after 3 consecutive non-growing scrolls } else { stableScrollCount = 0; } previousHeight = newHeight; } } catch { /* non-fatal */ } if (cdpForWheel) cdpForWheel.detach().catch(() => {}); return { scrollCount, heightReached: previousHeight }; } export async function extractIframeContent(page: Page): Promise> { const results: Array<{ src: string; content: string; markdown: string }> = []; try { const iframeSrcs: string[] = await page.evaluate(() => Array.from(document.querySelectorAll('iframe')).map(f => f.getAttribute('src') ?? f.getAttribute('data-src') ?? '') ).catch(() => []); for (const frame of page.frames()) { const src = frame.url(); if (!src || src === 'about:blank' || src === page.url()) continue; try { const content = await frame.content().catch(() => ''); if (!content || content.length < 50) continue; // Simple text extraction from iframe HTML const text = content.replace(/]*>[\s\S]*?<\/script>/gi, '') .replace(/]*>[\s\S]*?<\/style>/gi, '') .replace(/<[^>]+>/g, ' ') .replace(/\s+/g, ' ') .trim(); results.push({ src, content: content.slice(0, 10_000), markdown: text.slice(0, 5_000) }); } catch { /* skip this frame */ } } } catch { /* non-fatal */ } return results; } export async function pierceShadowDom(page: Page): Promise { try { return await page.evaluate(() => { const parts: string[] = []; function walkShadow(root: Element | ShadowRoot): void { const shadow = (root as any).shadowRoot; if (shadow) { parts.push(shadow.innerHTML ?? ''); for (const child of [...shadow.querySelectorAll('*')]) walkShadow(child); } for (const child of Array.from(root.querySelectorAll(':not(script):not(style)'))) { if ((child as any).shadowRoot) walkShadow(child); } } walkShadow(document.body); return parts .join('\n') .replace(/]*>[\s\S]*?<\/script>/gi, '') .replace(/]*>[\s\S]*?<\/style>/gi, '') .replace(/<[^>]+>/g, ' ') .replace(/\s+/g, ' ') .trim() .slice(0, 10_000); }).catch(() => ''); } catch { return ''; } }