use async_trait::async_trait; use serde_json::{json, Map, Value}; use zeroclaw::tools::{Tool, ToolResult}; use crate::pipe::{Action, BrowserPipeTool, Transport}; pub const BROWSER_ACTION_TOOL_NAME: &str = "browser_action"; pub struct ZeroClawBrowserTool { browser_tool: BrowserPipeTool, } impl ZeroClawBrowserTool { pub fn new(browser_tool: BrowserPipeTool) -> Self { Self { browser_tool } } } #[async_trait] impl Tool for ZeroClawBrowserTool { fn name(&self) -> &str { BROWSER_ACTION_TOOL_NAME } fn description(&self) -> &str { "Execute browser actions in SuperRPA through the existing sgClaw pipe protocol." } fn parameters_schema(&self) -> Value { json!({ "type": "object", "required": ["action", "expected_domain"], "properties": { "action": { "type": "string", "enum": ["click", "type", "navigate", "getText"] }, "expected_domain": { "type": "string" }, "selector": { "type": "string" }, "text": { "type": "string" }, "url": { "type": "string" }, "clear_first": { "type": "boolean" } } }) } async fn execute(&self, args: Value) -> anyhow::Result { let request = match parse_browser_action_request(args) { Ok(request) => request, Err(err) => return Ok(failed_tool_result(err.to_string())), }; let result = match self.browser_tool.invoke( request.action, request.params, &request.expected_domain, ) { Ok(result) => result, Err(err) => return Ok(failed_tool_result(err.to_string())), }; let output = serde_json::to_string(&json!({ "seq": result.seq, "success": result.success, "data": result.data, "aom_snapshot": result.aom_snapshot, "timing": result.timing }))?; Ok(ToolResult { success: result.success, output, error: (!result.success) .then(|| format_browser_action_error(&result.data)), }) } } struct BrowserActionRequest { action: Action, expected_domain: String, params: Value, } fn parse_browser_action_request(args: Value) -> Result { let mut args = match args { Value::Object(args) => args, other => { return Err(BrowserActionAdapterError::InvalidArguments(format!( "expected object arguments, got {other}" ))) } }; let action_name = take_required_string(&mut args, "action")?; let expected_domain = take_required_string(&mut args, "expected_domain")?; let action = parse_action(&action_name)?; validate_action_params(&action_name, &args)?; Ok(BrowserActionRequest { action, expected_domain, params: Value::Object(args), }) } fn parse_action(action_name: &str) -> Result { match action_name { "click" => Ok(Action::Click), "type" => Ok(Action::Type), "navigate" => Ok(Action::Navigate), "getText" => Ok(Action::GetText), other => Err(BrowserActionAdapterError::UnsupportedAction( other.to_string(), )), } } fn take_required_string( args: &mut Map, key: &'static str, ) -> Result { match args.remove(key) { Some(Value::String(value)) if !value.trim().is_empty() => Ok(value), Some(other) => Err(BrowserActionAdapterError::InvalidArguments(format!( "{key} must be a non-empty string, got {other}" ))), None => Err(BrowserActionAdapterError::MissingField(key)), } } fn failed_tool_result(error: String) -> ToolResult { ToolResult { success: false, output: String::new(), error: Some(error), } } fn validate_action_params( action_name: &str, args: &Map, ) -> Result<(), BrowserActionAdapterError> { match action_name { "click" | "getText" => require_non_empty_string(args, "selector", action_name), "type" => { require_non_empty_string(args, "selector", action_name)?; require_non_empty_string(args, "text", action_name) } "navigate" => require_non_empty_string(args, "url", action_name), _ => Ok(()), } } fn require_non_empty_string( args: &Map, key: &'static str, action_name: &str, ) -> Result<(), BrowserActionAdapterError> { match args.get(key) { Some(Value::String(value)) if !value.trim().is_empty() => Ok(()), Some(other) => Err(BrowserActionAdapterError::InvalidArguments(format!( "{action_name} requires a non-empty {key}, got {other}" ))), None => Err(BrowserActionAdapterError::InvalidArguments(format!( "{action_name} requires {key}" ))), } } fn format_browser_action_error(data: &Value) -> String { if let Some(error) = data.get("error") { if let Some(message) = error.get("message").and_then(Value::as_str) { return message.to_string(); } return format!("browser action failed: {error}"); } if data.is_null() { return "browser action returned success=false".to_string(); } format!("browser action failed: {data}") } #[derive(Debug, thiserror::Error)] enum BrowserActionAdapterError { #[error("unsupported action: {0}")] UnsupportedAction(String), #[error("missing required field: {0}")] MissingField(&'static str), #[error("invalid tool arguments: {0}")] InvalidArguments(String), }