Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
S7 Browser Hand: - Remove dead code: browser/actions.rs (314 lines of unused BrowserAction/ActionResult types) - Fix browser_scrape_page: log failed selector matches instead of silently swallowing errors - Fix element_to_info: document known limitation for always-None location/size fields - Fix browserHandStore: reuse activeSessionId in executeScript/takeScreenshot/executeTemplate instead of creating orphan Browser sessions - Add Browser.connect(sessionId) method for session reuse MCP Frontend: - Add desktop/src/lib/mcp-client.ts (77 lines) — typed client for MCP Tauri commands (startMcpService, stopMcpService, listMcpServices, callMcpTool) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
469 lines
10 KiB
TypeScript
469 lines
10 KiB
TypeScript
/**
|
|
* Browser Automation Client for ZCLAW
|
|
* Provides TypeScript API for Fantoccini-based browser automation
|
|
*/
|
|
|
|
import { invoke } from '@tauri-apps/api/core';
|
|
|
|
// ============================================================================
|
|
// Types
|
|
// ============================================================================
|
|
|
|
export interface BrowserSessionResult {
|
|
session_id: string;
|
|
}
|
|
|
|
export interface BrowserSessionInfo {
|
|
id: string;
|
|
name: string;
|
|
current_url: string | null;
|
|
title: string | null;
|
|
status: string;
|
|
created_at: string;
|
|
last_activity: string;
|
|
}
|
|
|
|
export interface BrowserNavigationResult {
|
|
url: string | null;
|
|
title: string | null;
|
|
}
|
|
|
|
export interface BrowserElementInfo {
|
|
selector: string;
|
|
tag_name: string | null;
|
|
text: string | null;
|
|
is_displayed: boolean;
|
|
is_enabled: boolean;
|
|
is_selected: boolean;
|
|
location: BrowserElementLocation | null;
|
|
size: BrowserElementSize | null;
|
|
}
|
|
|
|
export interface BrowserElementLocation {
|
|
x: number;
|
|
y: number;
|
|
}
|
|
|
|
export interface BrowserElementSize {
|
|
width: number;
|
|
height: number;
|
|
}
|
|
|
|
export interface BrowserScreenshotResult {
|
|
base64: string;
|
|
format: string;
|
|
}
|
|
|
|
export interface FormFieldData {
|
|
selector: string;
|
|
value: string;
|
|
}
|
|
|
|
// ============================================================================
|
|
// Session Management
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Create a new browser session
|
|
*/
|
|
export async function createSession(options?: {
|
|
webdriverUrl?: string;
|
|
headless?: boolean;
|
|
browserType?: 'chrome' | 'firefox' | 'edge' | 'safari';
|
|
windowWidth?: number;
|
|
windowHeight?: number;
|
|
}): Promise<BrowserSessionResult> {
|
|
return invoke('browser_create_session', {
|
|
webdriverUrl: options?.webdriverUrl,
|
|
headless: options?.headless,
|
|
browserType: options?.browserType,
|
|
windowWidth: options?.windowWidth,
|
|
windowHeight: options?.windowHeight,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Close a browser session
|
|
*/
|
|
export async function closeSession(sessionId: string): Promise<void> {
|
|
return invoke('browser_close_session', { sessionId });
|
|
}
|
|
|
|
/**
|
|
* List all browser sessions
|
|
*/
|
|
export async function listSessions(): Promise<BrowserSessionInfo[]> {
|
|
return invoke('browser_list_sessions');
|
|
}
|
|
|
|
/**
|
|
* Get session info
|
|
*/
|
|
export async function getSession(sessionId: string): Promise<BrowserSessionInfo> {
|
|
return invoke('browser_get_session', { sessionId });
|
|
}
|
|
|
|
// ============================================================================
|
|
// Navigation
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Navigate to URL
|
|
*/
|
|
export async function navigate(
|
|
sessionId: string,
|
|
url: string
|
|
): Promise<BrowserNavigationResult> {
|
|
return invoke('browser_navigate', { sessionId, url });
|
|
}
|
|
|
|
/**
|
|
* Go back
|
|
*/
|
|
export async function back(sessionId: string): Promise<void> {
|
|
return invoke('browser_back', { sessionId });
|
|
}
|
|
|
|
/**
|
|
* Go forward
|
|
*/
|
|
export async function forward(sessionId: string): Promise<void> {
|
|
return invoke('browser_forward', { sessionId });
|
|
}
|
|
|
|
/**
|
|
* Refresh page
|
|
*/
|
|
export async function refresh(sessionId: string): Promise<void> {
|
|
return invoke('browser_refresh', { sessionId });
|
|
}
|
|
|
|
/**
|
|
* Get current URL
|
|
*/
|
|
export async function getCurrentUrl(sessionId: string): Promise<string> {
|
|
return invoke('browser_get_url', { sessionId });
|
|
}
|
|
|
|
/**
|
|
* Get page title
|
|
*/
|
|
export async function getTitle(sessionId: string): Promise<string> {
|
|
return invoke('browser_get_title', { sessionId });
|
|
}
|
|
|
|
// ============================================================================
|
|
// Element Interaction
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Find element by CSS selector
|
|
*/
|
|
export async function findElement(
|
|
sessionId: string,
|
|
selector: string
|
|
): Promise<BrowserElementInfo> {
|
|
return invoke('browser_find_element', { sessionId, selector });
|
|
}
|
|
|
|
/**
|
|
* Find multiple elements
|
|
*/
|
|
export async function findElements(
|
|
sessionId: string,
|
|
selector: string
|
|
): Promise<BrowserElementInfo[]> {
|
|
return invoke('browser_find_elements', { sessionId, selector });
|
|
}
|
|
|
|
/**
|
|
* Click element
|
|
*/
|
|
export async function click(sessionId: string, selector: string): Promise<void> {
|
|
return invoke('browser_click', { sessionId, selector });
|
|
}
|
|
|
|
/**
|
|
* Type text into element
|
|
*/
|
|
export async function typeText(
|
|
sessionId: string,
|
|
selector: string,
|
|
text: string,
|
|
clearFirst?: boolean
|
|
): Promise<void> {
|
|
return invoke('browser_type', { sessionId, selector, text, clearFirst });
|
|
}
|
|
|
|
/**
|
|
* Get element text
|
|
*/
|
|
export async function getText(sessionId: string, selector: string): Promise<string> {
|
|
return invoke('browser_get_text', { sessionId, selector });
|
|
}
|
|
|
|
/**
|
|
* Get element attribute
|
|
*/
|
|
export async function getAttribute(
|
|
sessionId: string,
|
|
selector: string,
|
|
attribute: string
|
|
): Promise<string | null> {
|
|
return invoke('browser_get_attribute', { sessionId, selector, attribute });
|
|
}
|
|
|
|
/**
|
|
* Wait for element
|
|
*/
|
|
export async function waitForElement(
|
|
sessionId: string,
|
|
selector: string,
|
|
timeoutMs?: number
|
|
): Promise<BrowserElementInfo> {
|
|
return invoke('browser_wait_for_element', {
|
|
sessionId,
|
|
selector,
|
|
timeoutMs: timeoutMs ?? 10000,
|
|
});
|
|
}
|
|
|
|
// ============================================================================
|
|
// Advanced Operations
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Execute JavaScript
|
|
*/
|
|
export async function executeScript(
|
|
sessionId: string,
|
|
script: string,
|
|
args?: unknown[]
|
|
): Promise<unknown> {
|
|
return invoke('browser_execute_script', { sessionId, script, args });
|
|
}
|
|
|
|
/**
|
|
* Take screenshot
|
|
*/
|
|
export async function screenshot(sessionId: string): Promise<BrowserScreenshotResult> {
|
|
return invoke('browser_screenshot', { sessionId });
|
|
}
|
|
|
|
/**
|
|
* Take element screenshot
|
|
*/
|
|
export async function elementScreenshot(
|
|
sessionId: string,
|
|
selector: string
|
|
): Promise<BrowserScreenshotResult> {
|
|
return invoke('browser_element_screenshot', { sessionId, selector });
|
|
}
|
|
|
|
/**
|
|
* Get page source
|
|
*/
|
|
export async function getSource(sessionId: string): Promise<string> {
|
|
return invoke('browser_get_source', { sessionId });
|
|
}
|
|
|
|
// ============================================================================
|
|
// High-Level Tasks
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Scrape page content
|
|
*/
|
|
export async function scrapePage(
|
|
sessionId: string,
|
|
selectors: string[],
|
|
waitFor?: string,
|
|
timeoutMs?: number
|
|
): Promise<Record<string, string[]>> {
|
|
return invoke('browser_scrape_page', {
|
|
sessionId,
|
|
selectors,
|
|
waitFor,
|
|
timeoutMs,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Fill form
|
|
*/
|
|
export async function fillForm(
|
|
sessionId: string,
|
|
fields: FormFieldData[],
|
|
submitSelector?: string
|
|
): Promise<void> {
|
|
return invoke('browser_fill_form', { sessionId, fields, submitSelector });
|
|
}
|
|
|
|
// ============================================================================
|
|
// Browser Client Class (Convenience Wrapper)
|
|
// ============================================================================
|
|
|
|
/**
|
|
* High-level browser client for easier usage
|
|
*/
|
|
export class Browser {
|
|
private sessionId: string | null = null;
|
|
|
|
/**
|
|
* Start a new browser session
|
|
*/
|
|
async start(options?: {
|
|
webdriverUrl?: string;
|
|
headless?: boolean;
|
|
browserType?: 'chrome' | 'firefox' | 'edge' | 'safari';
|
|
windowWidth?: number;
|
|
windowHeight?: number;
|
|
}): Promise<string> {
|
|
const result = await createSession(options);
|
|
this.sessionId = result.session_id;
|
|
return this.sessionId;
|
|
}
|
|
|
|
/**
|
|
* Attach to an existing session by ID.
|
|
* Use this to reuse a session created elsewhere without spawning a new one.
|
|
*/
|
|
connect(sessionId: string): void {
|
|
this.sessionId = sessionId;
|
|
}
|
|
|
|
/**
|
|
* Close browser session
|
|
*/
|
|
async close(): Promise<void> {
|
|
if (this.sessionId) {
|
|
await closeSession(this.sessionId);
|
|
this.sessionId = null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get current session ID
|
|
*/
|
|
getSessionId(): string | null {
|
|
return this.sessionId;
|
|
}
|
|
|
|
/**
|
|
* Navigate to URL
|
|
*/
|
|
async goto(url: string): Promise<BrowserNavigationResult> {
|
|
this.ensureSession();
|
|
return navigate(this.sessionId!, url);
|
|
}
|
|
|
|
/**
|
|
* Find element
|
|
*/
|
|
async $(selector: string): Promise<BrowserElementInfo> {
|
|
this.ensureSession();
|
|
return findElement(this.sessionId!, selector);
|
|
}
|
|
|
|
/**
|
|
* Find multiple elements
|
|
*/
|
|
async $$(selector: string): Promise<BrowserElementInfo[]> {
|
|
this.ensureSession();
|
|
return findElements(this.sessionId!, selector);
|
|
}
|
|
|
|
/**
|
|
* Click element
|
|
*/
|
|
async click(selector: string): Promise<void> {
|
|
this.ensureSession();
|
|
return click(this.sessionId!, selector);
|
|
}
|
|
|
|
/**
|
|
* Type text
|
|
*/
|
|
async type(selector: string, text: string, clearFirst = false): Promise<void> {
|
|
this.ensureSession();
|
|
return typeText(this.sessionId!, selector, text, clearFirst);
|
|
}
|
|
|
|
/**
|
|
* Wait for element
|
|
*/
|
|
async wait(selector: string, timeoutMs = 10000): Promise<BrowserElementInfo> {
|
|
this.ensureSession();
|
|
return waitForElement(this.sessionId!, selector, timeoutMs);
|
|
}
|
|
|
|
/**
|
|
* Take screenshot
|
|
*/
|
|
async screenshot(): Promise<BrowserScreenshotResult> {
|
|
this.ensureSession();
|
|
return screenshot(this.sessionId!);
|
|
}
|
|
|
|
/**
|
|
* Execute JavaScript
|
|
*/
|
|
async eval(script: string, args?: unknown[]): Promise<unknown> {
|
|
this.ensureSession();
|
|
return executeScript(this.sessionId!, script, args);
|
|
}
|
|
|
|
/**
|
|
* Get page source
|
|
*/
|
|
async source(): Promise<string> {
|
|
this.ensureSession();
|
|
return getSource(this.sessionId!);
|
|
}
|
|
|
|
/**
|
|
* Get current URL
|
|
*/
|
|
async url(): Promise<string> {
|
|
this.ensureSession();
|
|
return getCurrentUrl(this.sessionId!);
|
|
}
|
|
|
|
/**
|
|
* Get page title
|
|
*/
|
|
async title(): Promise<string> {
|
|
this.ensureSession();
|
|
return getTitle(this.sessionId!);
|
|
}
|
|
|
|
/**
|
|
* Scrape page content
|
|
*/
|
|
async scrape(
|
|
selectors: string[],
|
|
waitFor?: string,
|
|
timeoutMs?: number
|
|
): Promise<Record<string, string[]>> {
|
|
this.ensureSession();
|
|
return scrapePage(this.sessionId!, selectors, waitFor, timeoutMs);
|
|
}
|
|
|
|
/**
|
|
* Fill form
|
|
*/
|
|
async fillForm(fields: FormFieldData[], submitSelector?: string): Promise<void> {
|
|
this.ensureSession();
|
|
return fillForm(this.sessionId!, fields, submitSelector);
|
|
}
|
|
|
|
private ensureSession(): void {
|
|
if (!this.sessionId) {
|
|
throw new Error('Browser session not started. Call start() first.');
|
|
}
|
|
}
|
|
}
|
|
|
|
// Default export
|
|
export default Browser;
|