import { hostApiFetch } from '@/lib/host-api'; import type { AgentBrowserBounds, AgentBrowserCdpEvent, AgentBrowserCdpEventPage, AgentBrowserCdpResult, AgentBrowserPayloadChunk, AgentBrowserSnapshot, } from '../../shared/agent-browser'; type BrowserEnvelope = { success: boolean; browser: AgentBrowserSnapshot; error?: string; }; type EventPageEnvelope = { success: boolean; page: AgentBrowserCdpEventPage; error?: string; }; type CdpResultEnvelope = { success: boolean; result: AgentBrowserCdpResult; error?: string; }; type PayloadEnvelope = { success: boolean; chunk: AgentBrowserPayloadChunk; error?: string; }; export type AgentBrowserNavigateAction = 'url' | 'back' | 'forward' | 'reload'; export interface AgentBrowserConsoleEntry { id: number; level: string; text: string; timestamp: number; source?: string; url?: string; } export interface AgentBrowserNetworkEntry { requestId: string; sequence: number; method: string; url: string; resourceType?: string; status?: number; statusText?: string; mimeType?: string; durationMs?: number; encodedDataLength?: number; errorText?: string; } export interface AgentBrowserDiagnostics { console: AgentBrowserConsoleEntry[]; network: AgentBrowserNetworkEntry[]; } function jsonBody(body: Record): RequestInit { return { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }; } function requireSuccess( envelope: T, ): T { if (!envelope.success) { throw new Error(envelope.error || '开发浏览器请求失败'); } return envelope; } export async function getAgentBrowserState( projectPath: string, ): Promise { const query = new URLSearchParams({ project_path: projectPath }); const response = await hostApiFetch( `/api/agent-browser/state?${query.toString()}`, ); return requireSuccess(response).browser; } export async function openAgentBrowser(input: { projectPath: string; url: string; bounds?: AgentBrowserBounds; }): Promise { const response = await hostApiFetch( '/api/agent-browser/open', jsonBody({ project_path: input.projectPath, url: input.url, visible: true, ...(input.bounds ? { bounds: input.bounds } : {}), }), ); return requireSuccess(response).browser; } export async function presentAgentBrowser(input: { projectPath: string; visible: boolean; bounds?: AgentBrowserBounds; }): Promise { const response = await hostApiFetch( '/api/agent-browser/present', jsonBody({ project_path: input.projectPath, visible: input.visible, ...(input.bounds ? { bounds: input.bounds } : {}), }), ); return requireSuccess(response).browser; } export async function setAgentBrowserDiagnostics(input: { projectPath: string; enabled: boolean; }): Promise { const response = await hostApiFetch( '/api/agent-browser/diagnostics', jsonBody({ project_path: input.projectPath, enabled: input.enabled, }), ); return requireSuccess(response).browser; } export async function navigateAgentBrowser(input: { projectPath: string; action: AgentBrowserNavigateAction; url?: string; }): Promise { const response = await hostApiFetch( '/api/agent-browser/navigate', jsonBody({ project_path: input.projectPath, action: input.action, ...(input.url ? { url: input.url } : {}), }), ); return requireSuccess(response).browser; } export async function sendAgentBrowserCdp(input: { projectPath: string; method: string; params?: Record; sessionRef?: string; timeoutMs?: number; }): Promise { const response = await hostApiFetch( '/api/agent-browser/cdp/send', jsonBody({ project_path: input.projectPath, method: input.method, ...(input.params ? { params: input.params } : {}), ...(input.sessionRef ? { session_ref: input.sessionRef } : {}), ...(input.timeoutMs !== undefined ? { timeout_ms: input.timeoutMs } : {}), }), ); return requireSuccess(response).result; } export async function readAgentBrowserEvents(input: { projectPath: string; after?: number; methods?: string[]; limit?: number; waitMs?: number; }): Promise { const response = await hostApiFetch( '/api/agent-browser/cdp/events', jsonBody({ project_path: input.projectPath, ...(input.after !== undefined ? { after: input.after } : {}), ...(input.methods ? { methods: input.methods } : {}), ...(input.limit !== undefined ? { limit: input.limit } : {}), ...(input.waitMs !== undefined ? { wait_ms: input.waitMs } : {}), }), ); return requireSuccess(response).page; } export async function readAgentBrowserPayload(input: { projectPath: string; handle: string; offset?: number; maxBytes?: number; }): Promise { const response = await hostApiFetch( '/api/agent-browser/payload/read', jsonBody({ project_path: input.projectPath, handle: input.handle, ...(input.offset !== undefined ? { offset: input.offset } : {}), ...(input.maxBytes !== undefined ? { max_bytes: input.maxBytes } : {}), }), ); return requireSuccess(response).chunk; } export async function closeAgentBrowser( projectPath: string, ): Promise { const response = await hostApiFetch( '/api/agent-browser/close', jsonBody({ project_path: projectPath }), ); return requireSuccess(response).browser; } export async function resetAgentBrowserProfile( projectPath: string, ): Promise { const response = await hostApiFetch( '/api/agent-browser/reset-profile', jsonBody({ project_path: projectPath }), ); return requireSuccess(response).browser; } function asRecord(value: unknown): Record | null { return value && typeof value === 'object' && !Array.isArray(value) ? value as Record : null; } function asString(value: unknown): string | undefined { return typeof value === 'string' ? value : undefined; } function asNumber(value: unknown): number | undefined { return typeof value === 'number' && Number.isFinite(value) ? value : undefined; } function stringifyConsoleArgument(value: unknown): string { const remoteObject = asRecord(value); if (!remoteObject) return String(value ?? ''); if ('value' in remoteObject) { const primitive = remoteObject.value; if (typeof primitive === 'string') return primitive; try { return JSON.stringify(primitive); } catch { return String(primitive); } } return asString(remoteObject.description) ?? asString(remoteObject.className) ?? asString(remoteObject.type) ?? ''; } function consoleEntryFromEvent( event: AgentBrowserCdpEvent, ): AgentBrowserConsoleEntry | null { const params = asRecord(event.params); if (!params) { if ( event.payload && ( event.method === 'Runtime.consoleAPICalled' || event.method === 'Runtime.exceptionThrown' || event.method === 'Log.entryAdded' ) ) { return { id: event.sequence, level: 'info', text: `大型 Console 事件(${event.payload.byteLength} 字节),智能体可按需读取完整内容`, timestamp: event.timestamp, }; } return null; } if (event.method === 'Runtime.consoleAPICalled') { const args = Array.isArray(params.args) ? params.args : []; return { id: event.sequence, level: asString(params.type) ?? 'log', text: args.map(stringifyConsoleArgument).join(' '), timestamp: event.timestamp, }; } if (event.method === 'Runtime.exceptionThrown') { const details = asRecord(params.exceptionDetails); const exception = asRecord(details?.exception); const exceptionText = exception ? stringifyConsoleArgument(exception) : ''; return { id: event.sequence, level: 'error', text: asString(exception?.description) ?? (exceptionText || undefined) ?? asString(details?.text) ?? '未捕获异常', timestamp: event.timestamp, url: asString(details?.url), }; } if (event.method === 'Log.entryAdded') { const entry = asRecord(params.entry); if (!entry) return null; return { id: event.sequence, level: asString(entry.level) ?? 'log', text: asString(entry.text) ?? '', timestamp: event.timestamp, source: asString(entry.source), url: asString(entry.url), }; } return null; } export function deriveAgentBrowserDiagnostics( events: AgentBrowserCdpEvent[], ): AgentBrowserDiagnostics { const consoleEntries: AgentBrowserConsoleEntry[] = []; const requests = new Map(); const requestOrder: string[] = []; for (const event of events) { const consoleEntry = consoleEntryFromEvent(event); if (consoleEntry) consoleEntries.push(consoleEntry); const params = asRecord(event.params); const requestId = asString(params?.requestId); if (!params || !requestId) { if (event.payload && event.method.startsWith('Network.')) { const payloadKey = `${event.sessionRef ?? 'root'}:payload:${event.sequence}`; requestOrder.push(payloadKey); requests.set(payloadKey, { requestId: `payload:${event.sequence}`, sequence: event.sequence, method: event.method.slice('Network.'.length), url: `大型 Network 事件(${event.payload.byteLength} 字节),智能体可按需读取完整内容`, resourceType: 'payload', startedAt: event.timestamp, }); } continue; } const requestKey = `${event.sessionRef ?? 'root'}:${requestId}`; if (event.method === 'Network.requestWillBeSent') { const request = asRecord(params.request); if (!requests.has(requestKey)) requestOrder.push(requestKey); requests.set(requestKey, { requestId, sequence: event.sequence, method: asString(request?.method) ?? 'GET', url: asString(request?.url) ?? '', resourceType: asString(params.type), startedAt: event.timestamp, }); continue; } const current = requests.get(requestKey); if (!current) continue; if (event.method === 'Network.responseReceived') { const response = asRecord(params.response); requests.set(requestKey, { ...current, url: asString(response?.url) ?? current.url, resourceType: asString(params.type) ?? current.resourceType, status: asNumber(response?.status), statusText: asString(response?.statusText), mimeType: asString(response?.mimeType), }); } else if (event.method === 'Network.loadingFinished') { requests.set(requestKey, { ...current, durationMs: Math.max(0, event.timestamp - current.startedAt), encodedDataLength: asNumber(params.encodedDataLength), }); } else if (event.method === 'Network.loadingFailed') { requests.set(requestKey, { ...current, durationMs: Math.max(0, event.timestamp - current.startedAt), errorText: asString(params.errorText) ?? '请求失败', }); } } return { console: consoleEntries, network: requestOrder .map((requestId) => requests.get(requestId)) .filter((entry): entry is AgentBrowserNetworkEntry & { startedAt: number } => Boolean(entry)) .map((entry) => ({ requestId: entry.requestId, sequence: entry.sequence, method: entry.method, url: entry.url, ...(entry.resourceType ? { resourceType: entry.resourceType } : {}), ...(entry.status !== undefined ? { status: entry.status } : {}), ...(entry.statusText ? { statusText: entry.statusText } : {}), ...(entry.mimeType ? { mimeType: entry.mimeType } : {}), ...(entry.durationMs !== undefined ? { durationMs: entry.durationMs } : {}), ...(entry.encodedDataLength !== undefined ? { encodedDataLength: entry.encodedDataLength } : {}), ...(entry.errorText ? { errorText: entry.errorText } : {}), })), }; }