diff --git a/Cargo.lock b/Cargo.lock index 787709f..17fbe26 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1329,6 +1329,7 @@ dependencies = [ "zclaw-kernel", "zclaw-memory", "zclaw-pipeline", + "zclaw-protocols", "zclaw-runtime", "zclaw-skills", "zclaw-types", diff --git a/desktop/src-tauri/src/browser/actions.rs b/desktop/src-tauri/src/browser/actions.rs index 8929906..940b8cf 100644 --- a/desktop/src-tauri/src/browser/actions.rs +++ b/desktop/src-tauri/src/browser/actions.rs @@ -1,313 +1,5 @@ -// Browser action definitions for Hands system -// Note: These types are reserved for future Browser Hand automation features - -#![allow(dead_code)] - -use serde::{Deserialize, Serialize}; - -/// Browser action types -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(tag = "type", rename_all = "snake_case")] -pub enum BrowserAction { - /// Create a new browser session - CreateSession { - webdriver_url: Option, - headless: Option, - browser_type: Option, - window_size: Option<(u32, u32)>, - }, - - /// Close browser session - CloseSession { - session_id: String, - }, - - /// Navigate to URL - Navigate { - session_id: String, - url: String, - }, - - /// Go back - Back { - session_id: String, - }, - - /// Go forward - Forward { - session_id: String, - }, - - /// Refresh page - Refresh { - session_id: String, - }, - - /// Click element - Click { - session_id: String, - selector: String, - }, - - /// Type text - Type { - session_id: String, - selector: String, - text: String, - clear_first: Option, - }, - - /// Get element text - GetText { - session_id: String, - selector: String, - }, - - /// Get element attribute - GetAttribute { - session_id: String, - selector: String, - attribute: String, - }, - - /// Find element - FindElement { - session_id: String, - selector: String, - }, - - /// Find multiple elements - FindElements { - session_id: String, - selector: String, - }, - - /// Execute JavaScript - ExecuteScript { - session_id: String, - script: String, - args: Option>, - }, - - /// Take screenshot - Screenshot { - session_id: String, - }, - - /// Take element screenshot - ElementScreenshot { - session_id: String, - selector: String, - }, - - /// Wait for element - WaitForElement { - session_id: String, - selector: String, - timeout_ms: Option, - }, - - /// Get page source - GetSource { - session_id: String, - }, - - /// Get current URL - GetCurrentUrl { - session_id: String, - }, - - /// Get page title - GetTitle { - session_id: String, - }, - - /// List all sessions - ListSessions, - - /// Get session info - GetSession { - session_id: String, - }, -} - -/// Action execution result -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(tag = "type", rename_all = "snake_case")] -pub enum ActionResult { - /// Session created - SessionCreated { - session_id: String, - }, - - /// Session closed - SessionClosed { - session_id: String, - }, - - /// Navigation result - Navigated { - url: Option, - title: Option, - }, - - /// Element clicked - Clicked { - selector: String, - }, - - /// Text typed - Typed { - selector: String, - text: String, - }, - - /// Text retrieved - TextRetrieved { - selector: String, - text: String, - }, - - /// Attribute retrieved - AttributeRetrieved { - selector: String, - attribute: String, - value: Option, - }, - - /// Element found - ElementFound { - element: ElementInfo, - }, - - /// Elements found - ElementsFound { - elements: Vec, - }, - - /// Script executed - ScriptExecuted { - result: serde_json::Value, - }, - - /// Screenshot taken - ScreenshotTaken { - base64: String, - format: String, - }, - - /// Page source retrieved - SourceRetrieved { - source: String, - }, - - /// URL retrieved - UrlRetrieved { - url: String, - }, - - /// Title retrieved - TitleRetrieved { - title: String, - }, - - /// Sessions listed - SessionsListed { - sessions: Vec, - }, - - /// Session info retrieved - SessionInfo { - session: SessionInfo, - }, - - /// Operation completed (no specific data) - Completed, - - /// Error occurred - Error { - message: String, - code: String, - }, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ElementInfo { - pub selector: String, - pub tag_name: Option, - pub text: Option, - pub is_displayed: bool, - pub is_enabled: bool, - pub is_selected: bool, - pub location: Option, - pub size: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ElementLocation { - pub x: i32, - pub y: i32, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ElementSize { - pub width: u64, - pub height: u64, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct SessionInfo { - pub id: String, - pub name: String, - pub current_url: Option, - pub title: Option, - pub status: String, - pub created_at: String, - pub last_activity: String, -} - -/// High-level browser task (for Hand integration) -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(tag = "task", rename_all = "snake_case")] -pub enum BrowserTask { - /// Scrape page content - ScrapePage { - url: String, - selectors: Vec, - wait_for: Option, - }, - - /// Fill form - FillForm { - url: String, - fields: Vec, - submit_selector: Option, - }, - - /// Take page snapshot - PageSnapshot { - url: String, - include_screenshot: bool, - }, - - /// Navigate and extract - NavigateAndExtract { - url: String, - extraction_script: String, - }, - - /// Multi-page scraping - MultiPageScrape { - start_url: String, - next_page_selector: String, - item_selector: String, - max_pages: Option, - }, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct FormField { - pub selector: String, - pub value: String, - pub field_type: Option, -} +// Removed dead code (was 314 lines of unused BrowserAction/ActionResult/ElementInfo +// types guarded by #![allow(dead_code)]). The types defined here duplicated structs +// already present in client.rs and were never consumed by any other module. +// If browser action types are needed in the future, use the types from client.rs +// and session.rs directly. diff --git a/desktop/src-tauri/src/browser/client.rs b/desktop/src-tauri/src/browser/client.rs index beb2df3..68dd2bf 100644 --- a/desktop/src-tauri/src/browser/client.rs +++ b/desktop/src-tauri/src/browser/client.rs @@ -407,8 +407,15 @@ impl BrowserClient { let is_displayed = element.is_displayed().await.unwrap_or(false); let is_enabled = element.is_enabled().await.unwrap_or(false); let is_selected = element.is_selected().await.unwrap_or(false); - // Note: location() and size() may not be available in all fantoccini versions - // Using placeholder values if not available + // KNOWN LIMITATION: location and size are always None. + // The fantoccini Element type does not expose a synchronous bounding-box + // helper; retrieving geometry requires a separate execute_script call + // (e.g. element.getBoundingClientRect()). Since no current caller relies + // on these fields, they are intentionally left as None rather than adding + // an extra round-trip. If bounding-box data is needed in the future, + // add a dedicated browser_element_rect command that calls + // execute_script("return arguments[0].getBoundingClientRect()") and + // deprecate the location/size fields on ElementInfo entirely. let location = None; let size = None; diff --git a/desktop/src-tauri/src/browser/commands.rs b/desktop/src-tauri/src/browser/commands.rs index 991da74..0ac0d27 100644 --- a/desktop/src-tauri/src/browser/commands.rs +++ b/desktop/src-tauri/src/browser/commands.rs @@ -1,9 +1,5 @@ // Tauri commands for browser automation -// Note: Some imports are reserved for future Browser Hand features -#![allow(unused_imports)] - -use crate::browser::actions::BrowserAction; use crate::browser::client::BrowserClient; use crate::browser::session::{BrowserType, SessionConfig}; use serde::{Deserialize, Serialize}; @@ -454,9 +450,18 @@ pub async fn browser_scrape_page( let mut results = serde_json::Map::new(); for selector in selectors { - if let Ok(elements) = client.find_elements(&session_id, &selector).await { - let texts: Vec = elements.iter().filter_map(|e| e.text.clone()).collect(); - results.insert(selector, serde_json::json!(texts)); + match client.find_elements(&session_id, &selector).await { + Ok(elements) => { + let texts: Vec = elements.iter().filter_map(|e| e.text.clone()).collect(); + results.insert(selector, serde_json::json!(texts)); + } + Err(e) => { + tracing::warn!( + selector = %selector, + error = %e, + "browser_scrape_page: find_elements failed, skipping selector" + ); + } } } diff --git a/desktop/src-tauri/src/browser/mod.rs b/desktop/src-tauri/src/browser/mod.rs index a0c7b78..2a9495b 100644 --- a/desktop/src-tauri/src/browser/mod.rs +++ b/desktop/src-tauri/src/browser/mod.rs @@ -7,4 +7,4 @@ pub mod client; pub mod commands; pub mod error; pub mod session; -pub mod actions; +// pub mod actions; // Removed: dead code — see actions.rs for details diff --git a/desktop/src/lib/browser-client.ts b/desktop/src/lib/browser-client.ts index fd95765..b176595 100644 --- a/desktop/src/lib/browser-client.ts +++ b/desktop/src/lib/browser-client.ts @@ -324,6 +324,14 @@ export class Browser { 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 */ diff --git a/desktop/src/lib/mcp-client.ts b/desktop/src/lib/mcp-client.ts new file mode 100644 index 0000000..5dac081 --- /dev/null +++ b/desktop/src/lib/mcp-client.ts @@ -0,0 +1,77 @@ +/** + * MCP (Model Context Protocol) Client for ZCLAW + * + * Thin typed wrapper around the 4 Tauri MCP commands. + * All communication goes through invoke() — no direct HTTP or WebSocket. + */ + +import { invoke } from '@tauri-apps/api/core'; +import { createLogger } from './logger'; + +const log = createLogger('mcp-client'); + +// ============================================================================ +// Types +// ============================================================================ + +export interface McpServiceConfig { + name: string; + command: string; + args?: string[]; + env?: Record; + cwd?: string; +} + +export interface McpToolInfo { + service_name: string; + tool_name: string; + description: string; + input_schema: Record; +} + +export interface McpServiceStatus { + name: string; + tool_count: number; + tools: McpToolInfo[]; +} + +// ============================================================================ +// Functions +// ============================================================================ + +/** + * Start an MCP service process and discover its tools. + */ +export async function startMcpService( + config: McpServiceConfig +): Promise { + log.info('startMcpService', { name: config.name }); + return invoke('mcp_start_service', { config }); +} + +/** + * Stop a running MCP service by name. + */ +export async function stopMcpService(name: string): Promise { + log.info('stopMcpService', { name }); + return invoke('mcp_stop_service', { name }); +} + +/** + * List all running MCP services with their discovered tools. + */ +export async function listMcpServices(): Promise { + return invoke('mcp_list_services'); +} + +/** + * Call a tool exposed by a running MCP service. + */ +export async function callMcpTool( + serviceName: string, + toolName: string, + args: Record +): Promise { + log.info('callMcpTool', { serviceName, toolName }); + return invoke('mcp_call_tool', { serviceName, toolName, args }); +} diff --git a/desktop/src/store/browserHandStore.ts b/desktop/src/store/browserHandStore.ts index 9feb61a..9fa0d5b 100644 --- a/desktop/src/store/browserHandStore.ts +++ b/desktop/src/store/browserHandStore.ts @@ -10,6 +10,8 @@ import Browser, { createSession, closeSession, listSessions, + screenshot as screenshotFn, + executeScript as executeScriptFn, } from '../lib/browser-client'; import { BUILTIN_TEMPLATES, @@ -247,14 +249,21 @@ export const useBrowserHandStore = create }, }); - // Create browser instance + // Create browser instance — reuse active session if available const browser = new Browser(); + let createdOwnSession = false; try { store.addLog({ level: 'info', message: `开始执行模板: ${template.name}` }); - // Start browser session - await browser.start({ headless: true }); + // Attach to existing session or start a new one + if (store.activeSessionId) { + browser.connect(store.activeSessionId); + createdOwnSession = false; + } else { + await browser.start({ headless: true }); + createdOwnSession = true; + } // Create execution context const context = { @@ -322,7 +331,10 @@ export const useBrowserHandStore = create throw error; } finally { - await browser.close(); + // Only close the session if we created it (no pre-existing active session) + if (createdOwnSession) { + await browser.close(); + } } }, @@ -340,10 +352,8 @@ export const useBrowserHandStore = create }); try { - const browser = new Browser(); - await browser.start(); - - const result = await browser.eval(script, args); + // Use the standalone function with the existing session — no new session created + const result = await executeScriptFn(store.activeSessionId, script, args); store.updateExecutionState({ isRunning: false, @@ -399,10 +409,8 @@ export const useBrowserHandStore = create } try { - const browser = new Browser(); - await browser.start(); - - const result = await browser.screenshot(); + // Use the standalone function with the existing session — no new session created + const result = await screenshotFn(store.activeSessionId); set((state) => ({ execution: {