feat: 增加共享 Agent Browser 调试能力
需求:在 AI 编程会话中让用户与 Agent 共享同一浏览器页面,并查看控制台与网络信息。 实现:新增沙箱浏览器内核、Host API/渲染器面板、OpenCode 工具接入及安全边界测试。
This commit is contained in:
128
electron/agent-browser/event-buffer.ts
Normal file
128
electron/agent-browser/event-buffer.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user