diff --git a/src/lib/cdp.ts b/src/lib/cdp.ts index 874469e..fc9bc5e 100644 --- a/src/lib/cdp.ts +++ b/src/lib/cdp.ts @@ -1,9 +1,7 @@ /** - * Minimal Chrome DevTools Protocol client backed by a raw WebSocket. + * Minimal Chrome DevTools Protocol client backed by native WebSocket. */ -import WebSocket from "ws"; - type CDPResponse = { id: number; result?: Record; @@ -26,33 +24,56 @@ export class CDPClient { constructor(public readonly endpoint: string) {} - async connect(): Promise { - if (this.ws?.readyState === WebSocket.OPEN) return; - return await new Promise((resolve, reject) => { - this.ws = new WebSocket(this.endpoint); - this.ws.once("open", () => resolve()); - this.ws.once("error", (err) => reject(err)); - this.ws.on("message", (data: Buffer) => { - const msg = JSON.parse(data.toString()); - if (msg.id !== undefined && this.pending.has(msg.id)) { - const p = this.pending.get(msg.id)!; - this.pending.delete(msg.id); - p.resolve(msg); - } - if (msg.method && this.eventHandlers.has(msg.method)) { - for (const handler of this.eventHandlers.get(msg.method)!) { - handler(msg.params ?? {}); - } + async connect(retries = 3): Promise { + if (this.ws?.readyState === 1) return; // 1 = OPEN + let lastErr: Error | null = null; + for (let attempt = 0; attempt < retries; attempt++) { + try { + await new Promise((resolve, reject) => { + const ws = new WebSocket(this.endpoint); + const timeout = setTimeout(() => { + ws.close(); + reject(new Error("WebSocket connect timeout")); + }, 5000); + ws.addEventListener("open", () => { + clearTimeout(timeout); + this.ws = ws; + resolve(); + }); + ws.addEventListener("error", (ev) => { + clearTimeout(timeout); + reject(new Error(String(ev.message || "WebSocket error"))); + }); + ws.addEventListener("message", (ev) => { + const msg = JSON.parse(String(ev.data)); + if (msg.id !== undefined && this.pending.has(msg.id)) { + const p = this.pending.get(msg.id)!; + this.pending.delete(msg.id); + p.resolve(msg); + } + if (msg.method && this.eventHandlers.has(msg.method)) { + for (const handler of this.eventHandlers.get(msg.method)!) { + handler(msg.params ?? {}); + } + } + }); + ws.addEventListener("close", () => { + this.ws = null; + }); + }); + return; + } catch (err) { + lastErr = err as Error; + if (attempt < retries - 1) { + await new Promise((r) => setTimeout(r, 300 * (attempt + 1))); } - }); - this.ws.on("close", () => { - this.ws = null; - }); - }); + } + } + throw lastErr ?? new Error("Failed to connect after retries"); } async send(method: string, params: Record = {}): Promise> { - if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { + if (!this.ws || this.ws.readyState !== 1) { throw new Error("CDP not connected"); } const id = ++this.id; @@ -110,7 +131,16 @@ export async function listTargets(browserUrl: string): Promise export async function connectTarget(wsUrl: string): Promise { const client = new CDPClient(wsUrl); - await client.connect(); + await client.connect(3); + return client; +} + +export async function connectTargetById(browserUrl: string, targetId: string): Promise { + const parsed = new URL(browserUrl); + const wsProtocol = parsed.protocol === "https:" ? "wss:" : "ws:"; + const wsUrl = `${wsProtocol}//${parsed.host}/devtools/page/${targetId}`; + const client = new CDPClient(wsUrl); + await client.connect(3); return client; } diff --git a/src/plugin.ts b/src/plugin.ts index 3e7d5ca..3ca9c13 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -11,7 +11,7 @@ import { tool } from "@opencode-ai/plugin"; import { writeFileSync } from "fs"; import { tmpdir } from "os"; import { join } from "path"; -import { connectFirstPage, connectTarget, listTargets } from "./lib/cdp.js"; +import { connectFirstPage, connectTargetById, listTargets } from "./lib/cdp.js"; import { resolveUid, takeSnapshot, type Snapshot } from "./lib/snapshot.js"; const snapshotCache = new Map(); @@ -25,7 +25,8 @@ async function getClient(browserUrl: string, targetId?: string) { const targets = await listTargets(browserUrl); const target = targets.find((t) => t.id === targetId); if (!target) throw new Error(`Target ${targetId} not found`); - return { client: await connectTarget(target.webSocketDebuggerUrl), target }; + const client = await connectTargetById(browserUrl, targetId); + return { client, target }; } return connectFirstPage(browserUrl); }