205 lines
6.0 KiB
Rust
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),
|
|
}
|