feat(daemon): add WebSocket relay with console/network monitoring
- Add ws-relay.ts: WebSocket server at /ws with broadcast function - Add network event types (request/response/failed) to core ws.ts types - Add ConsoleEntry and NetworkEntry types - Add consoleLog and networkLog buffers to PageRegistry (100/200 max) - Add console listener in createPage: broadcasts page:console events - Add network listeners (request/response/requestfailed) in createPage - Add page crash handler broadcasting page:crashed events - Add history endpoints: GET /pages/:id/console and GET /pages/:id/network - Integrate createWsRelay in server.ts startup - Bump ws from devDependency to full dependency
This commit is contained in:
@@ -2,10 +2,14 @@
|
||||
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';
|
||||
import type { FingerprintProfile, PageInfo, ConsoleEntry, NetworkEntry } from '@visionl/core';
|
||||
import { PageRegistry, type RegisteredPage } from './page-registry.js';
|
||||
import { getProfile } from './stealth/profiles/index.js';
|
||||
import { applyStealth } from './stealth/index.js';
|
||||
import { broadcast } from './ws-relay.js';
|
||||
|
||||
const MAX_CONSOLE_BUFFER = 100;
|
||||
const MAX_NETWORK_BUFFER = 200;
|
||||
|
||||
// Apply stealth plugin once at module level / 模块级别一次性注入隐身插件
|
||||
chromium.use(StealthPlugin());
|
||||
@@ -80,19 +84,104 @@ export class BrowserManager {
|
||||
const info: PageInfo = { id, url, alias, title, status: 'active', profile: this.profile.id };
|
||||
this.registry.add(info, page, context);
|
||||
|
||||
this.setupPageListeners(page, id);
|
||||
|
||||
// Broadcast page created event / 广播页面创建事件
|
||||
broadcast({ type: 'page:created', data: info });
|
||||
console.log(`[browser-manager] Page created: ${id} → ${url}`);
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
private setupPageListeners(page: Page, id: string): void {
|
||||
const entry = this.registry.get(id);
|
||||
if (!entry) return;
|
||||
|
||||
// Console monitoring / 控制台监控
|
||||
page.on('console', (msg) => {
|
||||
const level = msg.type();
|
||||
const text = msg.text();
|
||||
const consoleEntry: ConsoleEntry = { level, text, timestamp: Date.now() };
|
||||
this.pushToBuffer(entry.consoleLog, consoleEntry, MAX_CONSOLE_BUFFER);
|
||||
broadcast({ type: 'page:console', data: { id, level, text } });
|
||||
});
|
||||
|
||||
// Network request monitoring / 网络请求监控
|
||||
page.on('request', (req) => {
|
||||
const netEntry: NetworkEntry = {
|
||||
type: 'request',
|
||||
url: req.url(),
|
||||
method: req.method(),
|
||||
headers: req.headers(),
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
this.pushToBuffer(entry.networkLog, netEntry, MAX_NETWORK_BUFFER);
|
||||
broadcast({
|
||||
type: 'page:network:request',
|
||||
data: { id, url: req.url(), method: req.method(), headers: req.headers() },
|
||||
});
|
||||
});
|
||||
|
||||
// Network response monitoring / 网络响应监控
|
||||
page.on('response', (res) => {
|
||||
const netEntry: NetworkEntry = {
|
||||
type: 'response',
|
||||
url: res.url(),
|
||||
status: res.status(),
|
||||
headers: res.headers(),
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
this.pushToBuffer(entry.networkLog, netEntry, MAX_NETWORK_BUFFER);
|
||||
broadcast({
|
||||
type: 'page:network:response',
|
||||
data: { id, url: res.url(), status: res.status(), headers: res.headers() },
|
||||
});
|
||||
});
|
||||
|
||||
// Network failure monitoring / 网络失败监控
|
||||
page.on('requestfailed', (req) => {
|
||||
const failureText = req.failure()?.errorText || 'Unknown error';
|
||||
const netEntry: NetworkEntry = {
|
||||
type: 'failed',
|
||||
url: req.url(),
|
||||
failure: failureText,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
this.pushToBuffer(entry.networkLog, netEntry, MAX_NETWORK_BUFFER);
|
||||
broadcast({
|
||||
type: 'page:network:failed',
|
||||
data: { id, url: req.url(), failure: failureText },
|
||||
});
|
||||
});
|
||||
|
||||
// Page crash handling / 页面崩溃处理
|
||||
page.on('crash', () => {
|
||||
console.error(`[browser-manager] Page crashed: ${id}`);
|
||||
entry.info.status = 'crashed';
|
||||
broadcast({ type: 'page:crashed', data: { id, error: 'Page crashed' } });
|
||||
});
|
||||
}
|
||||
|
||||
private pushToBuffer<T>(buffer: T[], item: T, maxSize: number): void {
|
||||
buffer.push(item);
|
||||
if (buffer.length > maxSize) {
|
||||
buffer.shift();
|
||||
}
|
||||
}
|
||||
|
||||
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' });
|
||||
}
|
||||
const pageId = entry.info.id;
|
||||
await entry.context.close();
|
||||
this.registry.remove(entry.info.id);
|
||||
this.registry.remove(pageId);
|
||||
broadcast({ type: 'page:closed', data: { id: pageId } });
|
||||
console.log(`[browser-manager] Page closed: ${pageId}`);
|
||||
}
|
||||
|
||||
getPage(idOrAlias: string) {
|
||||
getPage(idOrAlias: string): RegisteredPage | undefined {
|
||||
return this.registry.findByIdOrAlias(idOrAlias);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user