Files
makelore/electron/agent-browser/event-buffer.ts
brother7 e97a7ce9df feat: 增加共享 Agent Browser 调试能力
需求:在 AI 编程会话中让用户与 Agent 共享同一浏览器页面,并查看控制台与网络信息。

实现:新增沙箱浏览器内核、Host API/渲染器面板、OpenCode 工具接入及安全边界测试。
2026-07-31 14:53:36 +08:00

129 lines
3.6 KiB
TypeScript

import type {
AgentBrowserCdpEvent,
AgentBrowserCdpEventPage,
} from '../../shared/agent-browser';
type GapReason = NonNullable<AgentBrowserCdpEventPage['gap']>['reason'];
interface BufferedEvent {
event: AgentBrowserCdpEvent;
byteLength: number;
}
interface GapMarker {
sequence: number;
reason: GapReason;
}
export interface AgentBrowserEventBufferOptions {
maxEvents?: number;
maxBytes?: number;
}
const DEFAULT_MAX_EVENTS = 10_000;
const DEFAULT_MAX_BYTES = 16 * 1024 * 1024;
export class AgentBrowserEventBuffer {
private readonly events: BufferedEvent[] = [];
private readonly gaps: GapMarker[] = [];
private readonly maxEvents: number;
private readonly maxBytes: number;
private bytes = 0;
private sequence = 0;
private evictedThrough = 0;
constructor(options: AgentBrowserEventBufferOptions = {}) {
this.maxEvents = options.maxEvents ?? DEFAULT_MAX_EVENTS;
this.maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
}
get cursor(): number {
return this.sequence;
}
push(event: Omit<AgentBrowserCdpEvent, 'sequence'>): AgentBrowserCdpEvent {
const complete = { ...event, sequence: ++this.sequence };
const byteLength = Buffer.byteLength(JSON.stringify(complete));
this.events.push({ event: complete, byteLength });
this.bytes += byteLength;
this.evict();
return complete;
}
markGap(reason: GapReason): number {
const sequence = ++this.sequence;
this.gaps.push({ sequence, reason });
this.pruneGaps();
return sequence;
}
read(after = 0, methods?: string[], limit = 100): AgentBrowserCdpEventPage {
const safeAfter = Math.max(0, Math.trunc(after));
const safeLimit = Math.min(Math.max(Math.trunc(limit), 1), 200);
const methodSet = methods?.length ? new Set(methods) : null;
const start = Math.max(safeAfter, this.evictedThrough);
const selected: AgentBrowserCdpEvent[] = [];
let scannedThrough = start;
for (const buffered of this.events) {
if (buffered.event.sequence <= start) continue;
scannedThrough = buffered.event.sequence;
if (!methodSet || methodSet.has(buffered.event.method)) {
selected.push(buffered.event);
if (selected.length >= safeLimit) break;
}
}
if (selected.length < safeLimit) {
scannedThrough = Math.max(scannedThrough, this.sequence);
}
const nextCursor = Math.max(safeAfter, scannedThrough);
const hasMore = this.events.some(({ event }) =>
event.sequence > nextCursor && (!methodSet || methodSet.has(event.method)));
const oldestAvailable = this.events[0]?.event.sequence ?? this.sequence + 1;
const gap = safeAfter < this.evictedThrough
? { reason: 'evicted' as const, oldestAvailable }
: this.gaps.find((marker) =>
marker.sequence > safeAfter && marker.sequence <= nextCursor);
return {
events: selected,
nextCursor,
hasMore,
...(gap
? {
gap: {
reason: gap.reason,
oldestAvailable,
},
}
: {}),
};
}
clear(): void {
this.events.length = 0;
this.gaps.length = 0;
this.bytes = 0;
this.sequence = 0;
this.evictedThrough = 0;
}
private evict(): void {
while (this.events.length > this.maxEvents || this.bytes > this.maxBytes) {
const removed = this.events.shift();
if (!removed) break;
this.bytes -= removed.byteLength;
this.evictedThrough = Math.max(this.evictedThrough, removed.event.sequence);
}
this.pruneGaps();
}
private pruneGaps(): void {
while (this.gaps[0] && this.gaps[0].sequence <= this.evictedThrough) {
this.gaps.shift();
}
}
}