feat: add browser manager with page registry and Playwright stealth integration
- PageRegistry: thread-safe in-memory registry for tracking active pages by ID and alias - BrowserManager: Playwright browser lifecycle with stealth plugin, context creation, and page navigation - 8 unit tests for PageRegistry (add, get, findByIdOrAlias, hasAlias, remove, list) - 8 integration tests for BrowserManager (skipped by default, guard: VISIONL_INTEGRATION=1) - Fix: PageInfo import in core/client.ts (was incorrectly imported from api.js instead of page.js)
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
// BrowserManager — Playwright browser lifecycle and page management with stealth plugin / 浏览器管理器,使用隐身插件管理 Playwright 浏览器生命周期和页面
|
||||
import { chromium } from 'playwright-extra';
|
||||
import StealthPlugin from 'puppeteer-extra-plugin-stealth';
|
||||
import type { Browser, BrowserContext, Page } from 'playwright';
|
||||
import type { FingerprintProfile, PageInfo } from '@visionl/core';
|
||||
import { PageRegistry } from './page-registry.js';
|
||||
|
||||
// Apply stealth plugin once at module level / 模块级别一次性注入隐身插件
|
||||
chromium.use(StealthPlugin());
|
||||
|
||||
function generatePageId(): string {
|
||||
const hex = Math.random().toString(16).slice(2, 10);
|
||||
return `p_${hex}`;
|
||||
}
|
||||
|
||||
export class BrowserManager {
|
||||
private browser: Browser | null = null;
|
||||
private registry = new PageRegistry();
|
||||
private profile: FingerprintProfile;
|
||||
|
||||
constructor(profile: FingerprintProfile) {
|
||||
this.profile = profile;
|
||||
}
|
||||
|
||||
async init(): Promise<void> {
|
||||
this.browser = await chromium.launch({
|
||||
headless: true,
|
||||
args: [
|
||||
'--no-sandbox',
|
||||
'--disable-setuid-sandbox',
|
||||
'--disable-blink-features=AutomationControlled',
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
async createPage(url: string, alias?: string): Promise<PageInfo> {
|
||||
if (!this.browser) throw new Error('Browser not initialized');
|
||||
|
||||
if (alias && this.registry.hasAlias(alias)) {
|
||||
throw Object.assign(new Error(`Alias "${alias}" already exists`), { code: 'ALIAS_EXISTS' });
|
||||
}
|
||||
|
||||
// Validate URL format / 验证 URL 格式
|
||||
try {
|
||||
new URL(url);
|
||||
} catch {
|
||||
throw Object.assign(new Error(`Invalid URL: ${url}`), { code: 'INVALID_URL' });
|
||||
}
|
||||
|
||||
const context = await this.browser.newContext({
|
||||
viewport: this.profile.viewport,
|
||||
userAgent: this.profile.userAgent,
|
||||
locale: this.profile.languages[0],
|
||||
timezoneId: this.profile.timezone,
|
||||
permissions: Object.entries(this.profile.permissions)
|
||||
.filter(([, v]) => v === 'granted')
|
||||
.map(([k]) => k as any),
|
||||
geolocation: this.profile.geolocation,
|
||||
colorScheme: 'light',
|
||||
deviceScaleFactor: this.profile.screen.pixelRatio,
|
||||
});
|
||||
|
||||
const page = await context.newPage();
|
||||
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 });
|
||||
const title = await page.title();
|
||||
|
||||
const id = generatePageId();
|
||||
const info: PageInfo = { id, url, alias, title, status: 'active', profile: this.profile.id };
|
||||
this.registry.add(info, page, context);
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
async closePage(idOrAlias: string): Promise<void> {
|
||||
const entry = this.registry.findByIdOrAlias(idOrAlias);
|
||||
if (!entry) {
|
||||
throw Object.assign(new Error(`Page "${idOrAlias}" not found`), { code: 'PAGE_NOT_FOUND' });
|
||||
}
|
||||
await entry.context.close();
|
||||
this.registry.remove(entry.info.id);
|
||||
}
|
||||
|
||||
getPage(idOrAlias: string) {
|
||||
return this.registry.findByIdOrAlias(idOrAlias);
|
||||
}
|
||||
|
||||
listPages(): PageInfo[] {
|
||||
return this.registry.list();
|
||||
}
|
||||
|
||||
async cleanup(): Promise<void> {
|
||||
// Close all contexts / 关闭所有上下文
|
||||
const entries = this.registry['pages']?.values() ?? [];
|
||||
for (const entry of entries) {
|
||||
try { await entry.context.close(); } catch { /* ignore / 忽略 */ }
|
||||
}
|
||||
if (this.browser) {
|
||||
await this.browser.close();
|
||||
this.browser = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user