mirror of
https://github.com/stablyai/orca.git
synced 2026-10-25 00:03:59 +00:00
The five oversized src/main/browser modules and src/main/ipc/browser.ts each carried a file-level `eslint-disable max-lines` and ran 377-654 counted lines against a 300-line budget. AGENTS.md calls for splitting rather than suppressing, and config/max-lines-baseline.txt is a shrink-only ratchet, so this removes all six suppressions and prunes their entries (341 -> 335). Pure move, no behavior change. cdp-ws-proxy is decomposed into collaborating objects rather than free functions because its state is genuinely per-connection: every collaborator is a private readonly instance field built in the constructor with live closures over `this`, so per-connection state stays per-connection. Likewise the screencast pacer's isClosed/isStopping and snapshot capture's getSeq are live thunks, not values captured at wiring time, so guards inside already-armed timers still observe a later stop(). browser-guest-ui.ts is renamed to browser-guest-shortcut-forwarding.ts: after the split it exports exactly one function, setupGuestShortcutForwarding, so the old name no longer described its contents. Also restores a single `webContents.debugger` read in the screencast path. The extraction had left three reads where the original had one; the accessor is stable today, so this is not a behavior fix but it removes a latent divergence. Verified: oxlint clean, ratchet passes, typecheck clean, full unit suite green (remaining failures are pre-existing load flakes in untouched files, each green when re-run serially), no new runtime import cycles, and the IPC channel set diffed identical before/after with all 23 handlers still trust-gated.
127 lines
4.5 KiB
TypeScript
127 lines
4.5 KiB
TypeScript
import { WebSocket } from 'ws'
|
|
import type { WebContents } from 'electron'
|
|
import { ANTI_DETECTION_SCRIPT } from './anti-detection'
|
|
import { acquireElectronDebugger, type ElectronDebuggerLease } from './electron-debugger-lease'
|
|
import type { CdpClientResponseWriter } from './cdp-client-response-writer'
|
|
import type { CdpSyntheticSessionRegistry } from './cdp-synthetic-session-registry'
|
|
|
|
/**
|
|
* The IO boundary with webContents.debugger: lease-based attach, event fan-out to
|
|
* the websocket client, detach teardown, and generic command forwarding.
|
|
*/
|
|
export class CdpDebuggerChannel {
|
|
private debuggerMessageHandler: ((...args: unknown[]) => void) | null = null
|
|
private debuggerDetachHandler: ((...args: unknown[]) => void) | null = null
|
|
private debuggerLease: ElectronDebuggerLease | null = null
|
|
private attached = false
|
|
|
|
constructor(
|
|
private readonly webContents: WebContents,
|
|
private readonly responder: CdpClientResponseWriter,
|
|
private readonly sessions: CdpSyntheticSessionRegistry,
|
|
private readonly getClient: () => WebSocket | null,
|
|
private readonly onDetached: () => void
|
|
) {}
|
|
|
|
async attachDebugger(): Promise<void> {
|
|
if (this.attached) {
|
|
return
|
|
}
|
|
try {
|
|
this.debuggerLease = acquireElectronDebugger(this.webContents)
|
|
} catch {
|
|
throw new Error('Could not attach debugger. DevTools may already be open for this tab.')
|
|
}
|
|
this.attached = true
|
|
|
|
// Why: attaching the CDP debugger sets navigator.webdriver = true and
|
|
// exposes other automation signals that Cloudflare Turnstile checks.
|
|
// Inject before any page loads so challenges succeed.
|
|
try {
|
|
await this.webContents.debugger.sendCommand('Page.enable', {})
|
|
await this.webContents.debugger.sendCommand('Page.addScriptToEvaluateOnNewDocument', {
|
|
source: ANTI_DETECTION_SCRIPT
|
|
})
|
|
} catch {
|
|
/* best-effort — page domain may not be ready yet */
|
|
}
|
|
|
|
this.debuggerMessageHandler = (_event: unknown, ...rest: unknown[]) => {
|
|
const [method, params, sessionId] = rest as [
|
|
string,
|
|
Record<string, unknown>,
|
|
string | undefined
|
|
]
|
|
const client = this.getClient()
|
|
if (!client || client.readyState !== WebSocket.OPEN) {
|
|
return
|
|
}
|
|
// Why: Electron passes empty string (not undefined) for root-session events, but
|
|
// agent-browser filters events by the sessionId from Target.attachToTarget.
|
|
const msg: Record<string, unknown> = { method, params }
|
|
msg.sessionId = sessionId || this.sessions.primarySessionId
|
|
client.send(JSON.stringify(msg))
|
|
}
|
|
this.debuggerDetachHandler = () => {
|
|
this.attached = false
|
|
const lease = this.debuggerLease
|
|
this.debuggerLease = null
|
|
lease?.release()
|
|
this.onDetached()
|
|
}
|
|
this.webContents.debugger.on('message', this.debuggerMessageHandler as never)
|
|
this.webContents.debugger.on('detach', this.debuggerDetachHandler as never)
|
|
}
|
|
|
|
detachDebugger(): void {
|
|
if (this.debuggerMessageHandler) {
|
|
this.webContents.debugger.removeListener('message', this.debuggerMessageHandler as never)
|
|
this.debuggerMessageHandler = null
|
|
}
|
|
if (this.debuggerDetachHandler) {
|
|
this.webContents.debugger.removeListener('detach', this.debuggerDetachHandler as never)
|
|
this.debuggerDetachHandler = null
|
|
}
|
|
const lease = this.debuggerLease
|
|
this.debuggerLease = null
|
|
lease?.release()
|
|
this.attached = false
|
|
}
|
|
|
|
sendDebuggerCommand(
|
|
method: string,
|
|
params: Record<string, unknown>,
|
|
sessionId?: string
|
|
): Promise<unknown> {
|
|
const command = sessionId
|
|
? this.webContents.debugger.sendCommand(method, params, sessionId)
|
|
: this.webContents.debugger.sendCommand(method, params)
|
|
return Promise.resolve(command)
|
|
}
|
|
|
|
forwardCommand(
|
|
client: WebSocket,
|
|
clientId: number,
|
|
method: string,
|
|
params: Record<string, unknown>,
|
|
msgSessionId?: string
|
|
): void {
|
|
if (this.webContents.isDestroyed()) {
|
|
this.responder.sendError(clientId, 'Browser tab is no longer available', client)
|
|
return
|
|
}
|
|
const sessionId = this.sessions.resolveDebuggerSessionId(msgSessionId)
|
|
try {
|
|
this.sendDebuggerCommand(method, params, sessionId)
|
|
.then((result) => {
|
|
this.responder.sendResult(clientId, result, client)
|
|
})
|
|
.catch((err: Error) => {
|
|
this.responder.sendError(clientId, err.message, client)
|
|
})
|
|
} catch (err) {
|
|
this.responder.sendError(clientId, err instanceof Error ? err.message : String(err), client)
|
|
}
|
|
}
|
|
}
|