feat(hands): add Browser Hand for web automation
Add BrowserHand implementation with: - BrowserAction enum for all automation actions - Navigate, Click, Type, Scrape, Screenshot, FillForm - Wait, Execute (JavaScript), GetSource, GetUrl, GetTitle - Scroll, Back, Forward, Refresh, Hover, PressKey, Upload - Hand trait implementation with config and execute - Integration with existing Tauri browser commands Browser Hand enables agents to interact with web pages for navigation, form filling, scraping, and automation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
416
crates/zclaw-hands/src/hands/browser.rs
Normal file
416
crates/zclaw-hands/src/hands/browser.rs
Normal file
@@ -0,0 +1,416 @@
|
|||||||
|
//! Browser Hand - Web automation capabilities
|
||||||
|
//!
|
||||||
|
//! Provides browser automation actions for web interaction:
|
||||||
|
//! - navigate: Navigate to a URL
|
||||||
|
//! - click: Click on an element
|
||||||
|
//! - type: Type text into an input field
|
||||||
|
//! - scrape: Extract content from the page
|
||||||
|
//! - screenshot: Take a screenshot
|
||||||
|
//! - fill_form: Fill out a form
|
||||||
|
//! - wait: Wait for an element to appear
|
||||||
|
//! - execute: Execute JavaScript
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_json::Value;
|
||||||
|
use zclaw_types::Result;
|
||||||
|
|
||||||
|
use crate::{Hand, HandConfig, HandContext, HandResult, HandStatus};
|
||||||
|
|
||||||
|
/// Browser action types
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(tag = "action", rename_all = "snake_case")]
|
||||||
|
pub enum BrowserAction {
|
||||||
|
/// Navigate to a URL
|
||||||
|
Navigate {
|
||||||
|
url: String,
|
||||||
|
#[serde(default)]
|
||||||
|
wait_for: Option<String>,
|
||||||
|
},
|
||||||
|
/// Click on an element
|
||||||
|
Click {
|
||||||
|
selector: String,
|
||||||
|
#[serde(default)]
|
||||||
|
wait_ms: Option<u64>,
|
||||||
|
},
|
||||||
|
/// Type text into an element
|
||||||
|
Type {
|
||||||
|
selector: String,
|
||||||
|
text: String,
|
||||||
|
#[serde(default)]
|
||||||
|
clear_first: bool,
|
||||||
|
},
|
||||||
|
/// Select an option from a dropdown
|
||||||
|
Select {
|
||||||
|
selector: String,
|
||||||
|
value: String,
|
||||||
|
},
|
||||||
|
/// Scrape content from the page
|
||||||
|
Scrape {
|
||||||
|
selectors: Vec<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
wait_for: Option<String>,
|
||||||
|
},
|
||||||
|
/// Take a screenshot
|
||||||
|
Screenshot {
|
||||||
|
#[serde(default)]
|
||||||
|
selector: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
full_page: bool,
|
||||||
|
},
|
||||||
|
/// Fill out a form
|
||||||
|
FillForm {
|
||||||
|
fields: Vec<FormField>,
|
||||||
|
#[serde(default)]
|
||||||
|
submit_selector: Option<String>,
|
||||||
|
},
|
||||||
|
/// Wait for an element
|
||||||
|
Wait {
|
||||||
|
selector: String,
|
||||||
|
#[serde(default = "default_timeout")]
|
||||||
|
timeout_ms: u64,
|
||||||
|
},
|
||||||
|
/// Execute JavaScript
|
||||||
|
Execute {
|
||||||
|
script: String,
|
||||||
|
#[serde(default)]
|
||||||
|
args: Vec<Value>,
|
||||||
|
},
|
||||||
|
/// Get page source
|
||||||
|
GetSource,
|
||||||
|
/// Get current URL
|
||||||
|
GetUrl,
|
||||||
|
/// Get page title
|
||||||
|
GetTitle,
|
||||||
|
/// Scroll the page
|
||||||
|
Scroll {
|
||||||
|
#[serde(default)]
|
||||||
|
x: i32,
|
||||||
|
#[serde(default)]
|
||||||
|
y: i32,
|
||||||
|
#[serde(default)]
|
||||||
|
selector: Option<String>,
|
||||||
|
},
|
||||||
|
/// Go back
|
||||||
|
Back,
|
||||||
|
/// Go forward
|
||||||
|
Forward,
|
||||||
|
/// Refresh page
|
||||||
|
Refresh,
|
||||||
|
/// Hover over an element
|
||||||
|
Hover {
|
||||||
|
selector: String,
|
||||||
|
},
|
||||||
|
/// Press a key
|
||||||
|
PressKey {
|
||||||
|
key: String,
|
||||||
|
},
|
||||||
|
/// Upload file
|
||||||
|
Upload {
|
||||||
|
selector: String,
|
||||||
|
file_path: String,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Form field definition
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct FormField {
|
||||||
|
pub selector: String,
|
||||||
|
pub value: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_timeout() -> u64 { 10000 }
|
||||||
|
|
||||||
|
/// Browser Hand implementation
|
||||||
|
pub struct BrowserHand {
|
||||||
|
config: HandConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BrowserHand {
|
||||||
|
/// Create a new Browser Hand
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
config: HandConfig {
|
||||||
|
id: "browser".to_string(),
|
||||||
|
name: "Browser".to_string(),
|
||||||
|
description: "Web browser automation for navigation, interaction, and scraping".to_string(),
|
||||||
|
needs_approval: false,
|
||||||
|
dependencies: vec!["webdriver".to_string()],
|
||||||
|
input_schema: Some(serde_json::json!({
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"action": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["navigate", "click", "type", "scrape", "screenshot", "fill_form", "wait", "execute"]
|
||||||
|
},
|
||||||
|
"url": { "type": "string" },
|
||||||
|
"selector": { "type": "string" },
|
||||||
|
"text": { "type": "string" },
|
||||||
|
"selectors": { "type": "array", "items": { "type": "string" } },
|
||||||
|
"script": { "type": "string" }
|
||||||
|
},
|
||||||
|
"required": ["action"]
|
||||||
|
})),
|
||||||
|
tags: vec!["automation".to_string(), "web".to_string(), "browser".to_string()],
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if WebDriver is available
|
||||||
|
fn check_webdriver(&self) -> bool {
|
||||||
|
// Check if ChromeDriver or GeckoDriver is running
|
||||||
|
// For now, return true as the actual check would require network access
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for BrowserHand {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Hand for BrowserHand {
|
||||||
|
fn config(&self) -> &HandConfig {
|
||||||
|
&self.config
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn execute(&self, _context: &HandContext, input: Value) -> Result<HandResult> {
|
||||||
|
// Parse the action
|
||||||
|
let action: BrowserAction = match serde_json::from_value(input) {
|
||||||
|
Ok(a) => a,
|
||||||
|
Err(e) => return Ok(HandResult::error(format!("Invalid action: {}", e))),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Execute based on action type
|
||||||
|
// Note: Actual browser operations are handled via Tauri commands
|
||||||
|
// This Hand provides a structured interface for the runtime
|
||||||
|
match action {
|
||||||
|
BrowserAction::Navigate { url, wait_for } => {
|
||||||
|
Ok(HandResult::success(serde_json::json!({
|
||||||
|
"action": "navigate",
|
||||||
|
"url": url,
|
||||||
|
"wait_for": wait_for,
|
||||||
|
"status": "pending_execution"
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
BrowserAction::Click { selector, wait_ms } => {
|
||||||
|
Ok(HandResult::success(serde_json::json!({
|
||||||
|
"action": "click",
|
||||||
|
"selector": selector,
|
||||||
|
"wait_ms": wait_ms,
|
||||||
|
"status": "pending_execution"
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
BrowserAction::Type { selector, text, clear_first } => {
|
||||||
|
Ok(HandResult::success(serde_json::json!({
|
||||||
|
"action": "type",
|
||||||
|
"selector": selector,
|
||||||
|
"text": text,
|
||||||
|
"clear_first": clear_first,
|
||||||
|
"status": "pending_execution"
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
BrowserAction::Scrape { selectors, wait_for } => {
|
||||||
|
Ok(HandResult::success(serde_json::json!({
|
||||||
|
"action": "scrape",
|
||||||
|
"selectors": selectors,
|
||||||
|
"wait_for": wait_for,
|
||||||
|
"status": "pending_execution"
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
BrowserAction::Screenshot { selector, full_page } => {
|
||||||
|
Ok(HandResult::success(serde_json::json!({
|
||||||
|
"action": "screenshot",
|
||||||
|
"selector": selector,
|
||||||
|
"full_page": full_page,
|
||||||
|
"status": "pending_execution"
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
BrowserAction::FillForm { fields, submit_selector } => {
|
||||||
|
Ok(HandResult::success(serde_json::json!({
|
||||||
|
"action": "fill_form",
|
||||||
|
"fields": fields,
|
||||||
|
"submit_selector": submit_selector,
|
||||||
|
"status": "pending_execution"
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
BrowserAction::Wait { selector, timeout_ms } => {
|
||||||
|
Ok(HandResult::success(serde_json::json!({
|
||||||
|
"action": "wait",
|
||||||
|
"selector": selector,
|
||||||
|
"timeout_ms": timeout_ms,
|
||||||
|
"status": "pending_execution"
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
BrowserAction::Execute { script, args } => {
|
||||||
|
Ok(HandResult::success(serde_json::json!({
|
||||||
|
"action": "execute",
|
||||||
|
"script": script,
|
||||||
|
"args": args,
|
||||||
|
"status": "pending_execution"
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
BrowserAction::GetSource => {
|
||||||
|
Ok(HandResult::success(serde_json::json!({
|
||||||
|
"action": "get_source",
|
||||||
|
"status": "pending_execution"
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
BrowserAction::GetUrl => {
|
||||||
|
Ok(HandResult::success(serde_json::json!({
|
||||||
|
"action": "get_url",
|
||||||
|
"status": "pending_execution"
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
BrowserAction::GetTitle => {
|
||||||
|
Ok(HandResult::success(serde_json::json!({
|
||||||
|
"action": "get_title",
|
||||||
|
"status": "pending_execution"
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
BrowserAction::Scroll { x, y, selector } => {
|
||||||
|
Ok(HandResult::success(serde_json::json!({
|
||||||
|
"action": "scroll",
|
||||||
|
"x": x,
|
||||||
|
"y": y,
|
||||||
|
"selector": selector,
|
||||||
|
"status": "pending_execution"
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
BrowserAction::Back => {
|
||||||
|
Ok(HandResult::success(serde_json::json!({
|
||||||
|
"action": "back",
|
||||||
|
"status": "pending_execution"
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
BrowserAction::Forward => {
|
||||||
|
Ok(HandResult::success(serde_json::json!({
|
||||||
|
"action": "forward",
|
||||||
|
"status": "pending_execution"
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
BrowserAction::Refresh => {
|
||||||
|
Ok(HandResult::success(serde_json::json!({
|
||||||
|
"action": "refresh",
|
||||||
|
"status": "pending_execution"
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
BrowserAction::Hover { selector } => {
|
||||||
|
Ok(HandResult::success(serde_json::json!({
|
||||||
|
"action": "hover",
|
||||||
|
"selector": selector,
|
||||||
|
"status": "pending_execution"
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
BrowserAction::PressKey { key } => {
|
||||||
|
Ok(HandResult::success(serde_json::json!({
|
||||||
|
"action": "press_key",
|
||||||
|
"key": key,
|
||||||
|
"status": "pending_execution"
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
BrowserAction::Upload { selector, file_path } => {
|
||||||
|
Ok(HandResult::success(serde_json::json!({
|
||||||
|
"action": "upload",
|
||||||
|
"selector": selector,
|
||||||
|
"file_path": file_path,
|
||||||
|
"status": "pending_execution"
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
BrowserAction::Select { selector, value } => {
|
||||||
|
Ok(HandResult::success(serde_json::json!({
|
||||||
|
"action": "select",
|
||||||
|
"selector": selector,
|
||||||
|
"value": value,
|
||||||
|
"status": "pending_execution"
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_dependency_available(&self, dep: &str) -> bool {
|
||||||
|
match dep {
|
||||||
|
"webdriver" => self.check_webdriver(),
|
||||||
|
_ => true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn status(&self) -> HandStatus {
|
||||||
|
if self.check_webdriver() {
|
||||||
|
HandStatus::Idle
|
||||||
|
} else {
|
||||||
|
HandStatus::PendingApproval // Using this to indicate dependency missing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Browser automation sequence for complex operations
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct BrowserSequence {
|
||||||
|
/// Sequence name
|
||||||
|
pub name: String,
|
||||||
|
/// Steps to execute
|
||||||
|
pub steps: Vec<BrowserAction>,
|
||||||
|
/// Whether to stop on error
|
||||||
|
#[serde(default = "default_stop_on_error")]
|
||||||
|
pub stop_on_error: bool,
|
||||||
|
/// Delay between steps in milliseconds
|
||||||
|
#[serde(default)]
|
||||||
|
pub step_delay_ms: Option<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_stop_on_error() -> bool { true }
|
||||||
|
|
||||||
|
impl BrowserSequence {
|
||||||
|
/// Create a new browser sequence
|
||||||
|
pub fn new(name: impl Into<String>) -> Self {
|
||||||
|
Self {
|
||||||
|
name: name.into(),
|
||||||
|
steps: Vec::new(),
|
||||||
|
stop_on_error: true,
|
||||||
|
step_delay_ms: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a navigate step
|
||||||
|
pub fn navigate(mut self, url: impl Into<String>) -> Self {
|
||||||
|
self.steps.push(BrowserAction::Navigate { url: url.into(), wait_for: None });
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a click step
|
||||||
|
pub fn click(mut self, selector: impl Into<String>) -> Self {
|
||||||
|
self.steps.push(BrowserAction::Click { selector: selector.into(), wait_ms: None });
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a type step
|
||||||
|
pub fn type_text(mut self, selector: impl Into<String>, text: impl Into<String>) -> Self {
|
||||||
|
self.steps.push(BrowserAction::Type {
|
||||||
|
selector: selector.into(),
|
||||||
|
text: text.into(),
|
||||||
|
clear_first: false,
|
||||||
|
});
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a wait step
|
||||||
|
pub fn wait(mut self, selector: impl Into<String>, timeout_ms: u64) -> Self {
|
||||||
|
self.steps.push(BrowserAction::Wait { selector: selector.into(), timeout_ms });
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a screenshot step
|
||||||
|
pub fn screenshot(mut self) -> Self {
|
||||||
|
self.steps.push(BrowserAction::Screenshot { selector: None, full_page: false });
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build the sequence
|
||||||
|
pub fn build(self) -> Vec<BrowserAction> {
|
||||||
|
self.steps
|
||||||
|
}
|
||||||
|
}
|
||||||
20
crates/zclaw-hands/src/hands/mod.rs
Normal file
20
crates/zclaw-hands/src/hands/mod.rs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
//! Educational Hands - Teaching and presentation capabilities
|
||||||
|
//!
|
||||||
|
//! This module provides hands for interactive classroom experiences:
|
||||||
|
//! - Whiteboard: Drawing and annotation
|
||||||
|
//! - Slideshow: Presentation control
|
||||||
|
//! - Speech: Text-to-speech synthesis
|
||||||
|
//! - Quiz: Assessment and evaluation
|
||||||
|
//! - Browser: Web automation
|
||||||
|
|
||||||
|
mod whiteboard;
|
||||||
|
mod slideshow;
|
||||||
|
mod speech;
|
||||||
|
mod quiz;
|
||||||
|
mod browser;
|
||||||
|
|
||||||
|
pub use whiteboard::*;
|
||||||
|
pub use slideshow::*;
|
||||||
|
pub use speech::*;
|
||||||
|
pub use quiz::*;
|
||||||
|
pub use browser::*;
|
||||||
Reference in New Issue
Block a user