Files
claw/src/compat/browser_tool_adapter.rs

205 lines
6.0 KiB
Rust

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<T: Transport> {
browser_tool: BrowserPipeTool<T>,
}
impl<T: Transport> ZeroClawBrowserTool<T> {
pub fn new(browser_tool: BrowserPipeTool<T>) -> Self {
Self { browser_tool }
}
}
#[async_trait]
impl<T: Transport + 'static> Tool for ZeroClawBrowserTool<T> {
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<ToolResult> {
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<BrowserActionRequest, BrowserActionAdapterError> {
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<Action, BrowserActionAdapterError> {
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<String, Value>,
key: &'static str,
) -> Result<String, BrowserActionAdapterError> {
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<String, Value>,
) -> 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<String, Value>,
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),
}