Files
zclaw_openfang/desktop/src/lib/browser-client.ts
iven 1c99e5f3a3
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
fix(browser): stability enhancements + MCP frontend client
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>
2026-04-03 22:16:12 +08:00

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;