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:
2026-08-12 21:28:02 +08:00
parent b843eea9ae
commit e79590a6d8
10 changed files with 220 additions and 13 deletions
+93 -4
View File
@@ -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);
}