refactor: extract browser action parsing and IRC config struct
browser.rs: - Extract parse_browser_action() from Tool::execute, removing one #[allow(clippy::too_many_lines)] suppression irc.rs: - Replace 10-parameter IrcChannel::new() with IrcChannelConfig struct, removing #[allow(clippy::too_many_arguments)] suppression - Update all call sites (mod.rs and tests) Closes #366 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
69a9adde33
commit
46b199c50f
3 changed files with 292 additions and 288 deletions
|
|
@ -220,32 +220,34 @@ fn split_message(message: &str, max_bytes: usize) -> Vec<String> {
|
||||||
chunks
|
chunks
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Configuration for constructing an `IrcChannel`.
|
||||||
|
pub struct IrcChannelConfig {
|
||||||
|
pub server: String,
|
||||||
|
pub port: u16,
|
||||||
|
pub nickname: String,
|
||||||
|
pub username: Option<String>,
|
||||||
|
pub channels: Vec<String>,
|
||||||
|
pub allowed_users: Vec<String>,
|
||||||
|
pub server_password: Option<String>,
|
||||||
|
pub nickserv_password: Option<String>,
|
||||||
|
pub sasl_password: Option<String>,
|
||||||
|
pub verify_tls: bool,
|
||||||
|
}
|
||||||
|
|
||||||
impl IrcChannel {
|
impl IrcChannel {
|
||||||
#[allow(clippy::too_many_arguments)]
|
pub fn new(cfg: IrcChannelConfig) -> Self {
|
||||||
pub fn new(
|
let username = cfg.username.unwrap_or_else(|| cfg.nickname.clone());
|
||||||
server: String,
|
|
||||||
port: u16,
|
|
||||||
nickname: String,
|
|
||||||
username: Option<String>,
|
|
||||||
channels: Vec<String>,
|
|
||||||
allowed_users: Vec<String>,
|
|
||||||
server_password: Option<String>,
|
|
||||||
nickserv_password: Option<String>,
|
|
||||||
sasl_password: Option<String>,
|
|
||||||
verify_tls: bool,
|
|
||||||
) -> Self {
|
|
||||||
let username = username.unwrap_or_else(|| nickname.clone());
|
|
||||||
Self {
|
Self {
|
||||||
server,
|
server: cfg.server,
|
||||||
port,
|
port: cfg.port,
|
||||||
nickname,
|
nickname: cfg.nickname,
|
||||||
username,
|
username,
|
||||||
channels,
|
channels: cfg.channels,
|
||||||
allowed_users,
|
allowed_users: cfg.allowed_users,
|
||||||
server_password,
|
server_password: cfg.server_password,
|
||||||
nickserv_password,
|
nickserv_password: cfg.nickserv_password,
|
||||||
sasl_password,
|
sasl_password: cfg.sasl_password,
|
||||||
verify_tls,
|
verify_tls: cfg.verify_tls,
|
||||||
writer: Arc::new(Mutex::new(None)),
|
writer: Arc::new(Mutex::new(None)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -807,18 +809,18 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn specific_user_allowed() {
|
fn specific_user_allowed() {
|
||||||
let ch = IrcChannel::new(
|
let ch = IrcChannel::new(IrcChannelConfig {
|
||||||
"irc.test".into(),
|
server: "irc.test".into(),
|
||||||
6697,
|
port: 6697,
|
||||||
"bot".into(),
|
nickname: "bot".into(),
|
||||||
None,
|
username: None,
|
||||||
vec![],
|
channels: vec![],
|
||||||
vec!["alice".into(), "bob".into()],
|
allowed_users: vec!["alice".into(), "bob".into()],
|
||||||
None,
|
server_password: None,
|
||||||
None,
|
nickserv_password: None,
|
||||||
None,
|
sasl_password: None,
|
||||||
true,
|
verify_tls: true,
|
||||||
);
|
});
|
||||||
assert!(ch.is_user_allowed("alice"));
|
assert!(ch.is_user_allowed("alice"));
|
||||||
assert!(ch.is_user_allowed("bob"));
|
assert!(ch.is_user_allowed("bob"));
|
||||||
assert!(!ch.is_user_allowed("eve"));
|
assert!(!ch.is_user_allowed("eve"));
|
||||||
|
|
@ -826,18 +828,18 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn allowlist_case_insensitive() {
|
fn allowlist_case_insensitive() {
|
||||||
let ch = IrcChannel::new(
|
let ch = IrcChannel::new(IrcChannelConfig {
|
||||||
"irc.test".into(),
|
server: "irc.test".into(),
|
||||||
6697,
|
port: 6697,
|
||||||
"bot".into(),
|
nickname: "bot".into(),
|
||||||
None,
|
username: None,
|
||||||
vec![],
|
channels: vec![],
|
||||||
vec!["Alice".into()],
|
allowed_users: vec!["Alice".into()],
|
||||||
None,
|
server_password: None,
|
||||||
None,
|
nickserv_password: None,
|
||||||
None,
|
sasl_password: None,
|
||||||
true,
|
verify_tls: true,
|
||||||
);
|
});
|
||||||
assert!(ch.is_user_allowed("alice"));
|
assert!(ch.is_user_allowed("alice"));
|
||||||
assert!(ch.is_user_allowed("ALICE"));
|
assert!(ch.is_user_allowed("ALICE"));
|
||||||
assert!(ch.is_user_allowed("Alice"));
|
assert!(ch.is_user_allowed("Alice"));
|
||||||
|
|
@ -845,18 +847,18 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn empty_allowlist_denies_all() {
|
fn empty_allowlist_denies_all() {
|
||||||
let ch = IrcChannel::new(
|
let ch = IrcChannel::new(IrcChannelConfig {
|
||||||
"irc.test".into(),
|
server: "irc.test".into(),
|
||||||
6697,
|
port: 6697,
|
||||||
"bot".into(),
|
nickname: "bot".into(),
|
||||||
None,
|
username: None,
|
||||||
vec![],
|
channels: vec![],
|
||||||
vec![],
|
allowed_users: vec![],
|
||||||
None,
|
server_password: None,
|
||||||
None,
|
nickserv_password: None,
|
||||||
None,
|
sasl_password: None,
|
||||||
true,
|
verify_tls: true,
|
||||||
);
|
});
|
||||||
assert!(!ch.is_user_allowed("anyone"));
|
assert!(!ch.is_user_allowed("anyone"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -864,35 +866,35 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn new_defaults_username_to_nickname() {
|
fn new_defaults_username_to_nickname() {
|
||||||
let ch = IrcChannel::new(
|
let ch = IrcChannel::new(IrcChannelConfig {
|
||||||
"irc.test".into(),
|
server: "irc.test".into(),
|
||||||
6697,
|
port: 6697,
|
||||||
"mybot".into(),
|
nickname: "mybot".into(),
|
||||||
None,
|
username: None,
|
||||||
vec![],
|
channels: vec![],
|
||||||
vec![],
|
allowed_users: vec![],
|
||||||
None,
|
server_password: None,
|
||||||
None,
|
nickserv_password: None,
|
||||||
None,
|
sasl_password: None,
|
||||||
true,
|
verify_tls: true,
|
||||||
);
|
});
|
||||||
assert_eq!(ch.username, "mybot");
|
assert_eq!(ch.username, "mybot");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn new_uses_explicit_username() {
|
fn new_uses_explicit_username() {
|
||||||
let ch = IrcChannel::new(
|
let ch = IrcChannel::new(IrcChannelConfig {
|
||||||
"irc.test".into(),
|
server: "irc.test".into(),
|
||||||
6697,
|
port: 6697,
|
||||||
"mybot".into(),
|
nickname: "mybot".into(),
|
||||||
Some("customuser".into()),
|
username: Some("customuser".into()),
|
||||||
vec![],
|
channels: vec![],
|
||||||
vec![],
|
allowed_users: vec![],
|
||||||
None,
|
server_password: None,
|
||||||
None,
|
nickserv_password: None,
|
||||||
None,
|
sasl_password: None,
|
||||||
true,
|
verify_tls: true,
|
||||||
);
|
});
|
||||||
assert_eq!(ch.username, "customuser");
|
assert_eq!(ch.username, "customuser");
|
||||||
assert_eq!(ch.nickname, "mybot");
|
assert_eq!(ch.nickname, "mybot");
|
||||||
}
|
}
|
||||||
|
|
@ -905,18 +907,18 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn new_stores_all_fields() {
|
fn new_stores_all_fields() {
|
||||||
let ch = IrcChannel::new(
|
let ch = IrcChannel::new(IrcChannelConfig {
|
||||||
"irc.example.com".into(),
|
server: "irc.example.com".into(),
|
||||||
6697,
|
port: 6697,
|
||||||
"zcbot".into(),
|
nickname: "zcbot".into(),
|
||||||
Some("zeroclaw".into()),
|
username: Some("zeroclaw".into()),
|
||||||
vec!["#test".into()],
|
channels: vec!["#test".into()],
|
||||||
vec!["alice".into()],
|
allowed_users: vec!["alice".into()],
|
||||||
Some("serverpass".into()),
|
server_password: Some("serverpass".into()),
|
||||||
Some("nspass".into()),
|
nickserv_password: Some("nspass".into()),
|
||||||
Some("saslpass".into()),
|
sasl_password: Some("saslpass".into()),
|
||||||
false,
|
verify_tls: false,
|
||||||
);
|
});
|
||||||
assert_eq!(ch.server, "irc.example.com");
|
assert_eq!(ch.server, "irc.example.com");
|
||||||
assert_eq!(ch.port, 6697);
|
assert_eq!(ch.port, 6697);
|
||||||
assert_eq!(ch.nickname, "zcbot");
|
assert_eq!(ch.nickname, "zcbot");
|
||||||
|
|
@ -995,17 +997,17 @@ nickname = "bot"
|
||||||
// ── Helpers ─────────────────────────────────────────────
|
// ── Helpers ─────────────────────────────────────────────
|
||||||
|
|
||||||
fn make_channel() -> IrcChannel {
|
fn make_channel() -> IrcChannel {
|
||||||
IrcChannel::new(
|
IrcChannel::new(IrcChannelConfig {
|
||||||
"irc.example.com".into(),
|
server: "irc.example.com".into(),
|
||||||
6697,
|
port: 6697,
|
||||||
"zcbot".into(),
|
nickname: "zcbot".into(),
|
||||||
None,
|
username: None,
|
||||||
vec!["#zeroclaw".into()],
|
channels: vec!["#zeroclaw".into()],
|
||||||
vec!["*".into()],
|
allowed_users: vec!["*".into()],
|
||||||
None,
|
server_password: None,
|
||||||
None,
|
nickserv_password: None,
|
||||||
None,
|
sasl_password: None,
|
||||||
true,
|
verify_tls: true,
|
||||||
)
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -672,18 +672,18 @@ pub async fn doctor_channels(config: Config) -> Result<()> {
|
||||||
if let Some(ref irc) = config.channels_config.irc {
|
if let Some(ref irc) = config.channels_config.irc {
|
||||||
channels.push((
|
channels.push((
|
||||||
"IRC",
|
"IRC",
|
||||||
Arc::new(IrcChannel::new(
|
Arc::new(IrcChannel::new(irc::IrcChannelConfig {
|
||||||
irc.server.clone(),
|
server: irc.server.clone(),
|
||||||
irc.port,
|
port: irc.port,
|
||||||
irc.nickname.clone(),
|
nickname: irc.nickname.clone(),
|
||||||
irc.username.clone(),
|
username: irc.username.clone(),
|
||||||
irc.channels.clone(),
|
channels: irc.channels.clone(),
|
||||||
irc.allowed_users.clone(),
|
allowed_users: irc.allowed_users.clone(),
|
||||||
irc.server_password.clone(),
|
server_password: irc.server_password.clone(),
|
||||||
irc.nickserv_password.clone(),
|
nickserv_password: irc.nickserv_password.clone(),
|
||||||
irc.sasl_password.clone(),
|
sasl_password: irc.sasl_password.clone(),
|
||||||
irc.verify_tls.unwrap_or(true),
|
verify_tls: irc.verify_tls.unwrap_or(true),
|
||||||
)),
|
})),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -947,18 +947,18 @@ pub async fn start_channels(config: Config) -> Result<()> {
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(ref irc) = config.channels_config.irc {
|
if let Some(ref irc) = config.channels_config.irc {
|
||||||
channels.push(Arc::new(IrcChannel::new(
|
channels.push(Arc::new(IrcChannel::new(irc::IrcChannelConfig {
|
||||||
irc.server.clone(),
|
server: irc.server.clone(),
|
||||||
irc.port,
|
port: irc.port,
|
||||||
irc.nickname.clone(),
|
nickname: irc.nickname.clone(),
|
||||||
irc.username.clone(),
|
username: irc.username.clone(),
|
||||||
irc.channels.clone(),
|
channels: irc.channels.clone(),
|
||||||
irc.allowed_users.clone(),
|
allowed_users: irc.allowed_users.clone(),
|
||||||
irc.server_password.clone(),
|
server_password: irc.server_password.clone(),
|
||||||
irc.nickserv_password.clone(),
|
nickserv_password: irc.nickserv_password.clone(),
|
||||||
irc.sasl_password.clone(),
|
sasl_password: irc.sasl_password.clone(),
|
||||||
irc.verify_tls.unwrap_or(true),
|
verify_tls: irc.verify_tls.unwrap_or(true),
|
||||||
)));
|
})));
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(ref lk) = config.channels_config.lark {
|
if let Some(ref lk) = config.channels_config.lark {
|
||||||
|
|
|
||||||
|
|
@ -854,7 +854,6 @@ impl BrowserTool {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_lines)]
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl Tool for BrowserTool {
|
impl Tool for BrowserTool {
|
||||||
fn name(&self) -> &str {
|
fn name(&self) -> &str {
|
||||||
|
|
@ -1031,165 +1030,13 @@ impl Tool for BrowserTool {
|
||||||
return self.execute_computer_use_action(action_str, &args).await;
|
return self.execute_computer_use_action(action_str, &args).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
let action = match action_str {
|
let action = match parse_browser_action(action_str, &args) {
|
||||||
"open" => {
|
Ok(a) => a,
|
||||||
let url = args
|
Err(e) => {
|
||||||
.get("url")
|
|
||||||
.and_then(|v| v.as_str())
|
|
||||||
.ok_or_else(|| anyhow::anyhow!("Missing 'url' for open action"))?;
|
|
||||||
BrowserAction::Open { url: url.into() }
|
|
||||||
}
|
|
||||||
"snapshot" => BrowserAction::Snapshot {
|
|
||||||
interactive_only: args
|
|
||||||
.get("interactive_only")
|
|
||||||
.and_then(serde_json::Value::as_bool)
|
|
||||||
.unwrap_or(true), // Default to interactive for AI
|
|
||||||
compact: args
|
|
||||||
.get("compact")
|
|
||||||
.and_then(serde_json::Value::as_bool)
|
|
||||||
.unwrap_or(true),
|
|
||||||
depth: args
|
|
||||||
.get("depth")
|
|
||||||
.and_then(serde_json::Value::as_u64)
|
|
||||||
.map(|d| u32::try_from(d).unwrap_or(u32::MAX)),
|
|
||||||
},
|
|
||||||
"click" => {
|
|
||||||
let selector = args
|
|
||||||
.get("selector")
|
|
||||||
.and_then(|v| v.as_str())
|
|
||||||
.ok_or_else(|| anyhow::anyhow!("Missing 'selector' for click"))?;
|
|
||||||
BrowserAction::Click {
|
|
||||||
selector: selector.into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"fill" => {
|
|
||||||
let selector = args
|
|
||||||
.get("selector")
|
|
||||||
.and_then(|v| v.as_str())
|
|
||||||
.ok_or_else(|| anyhow::anyhow!("Missing 'selector' for fill"))?;
|
|
||||||
let value = args
|
|
||||||
.get("value")
|
|
||||||
.and_then(|v| v.as_str())
|
|
||||||
.ok_or_else(|| anyhow::anyhow!("Missing 'value' for fill"))?;
|
|
||||||
BrowserAction::Fill {
|
|
||||||
selector: selector.into(),
|
|
||||||
value: value.into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"type" => {
|
|
||||||
let selector = args
|
|
||||||
.get("selector")
|
|
||||||
.and_then(|v| v.as_str())
|
|
||||||
.ok_or_else(|| anyhow::anyhow!("Missing 'selector' for type"))?;
|
|
||||||
let text = args
|
|
||||||
.get("text")
|
|
||||||
.and_then(|v| v.as_str())
|
|
||||||
.ok_or_else(|| anyhow::anyhow!("Missing 'text' for type"))?;
|
|
||||||
BrowserAction::Type {
|
|
||||||
selector: selector.into(),
|
|
||||||
text: text.into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"get_text" => {
|
|
||||||
let selector = args
|
|
||||||
.get("selector")
|
|
||||||
.and_then(|v| v.as_str())
|
|
||||||
.ok_or_else(|| anyhow::anyhow!("Missing 'selector' for get_text"))?;
|
|
||||||
BrowserAction::GetText {
|
|
||||||
selector: selector.into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"get_title" => BrowserAction::GetTitle,
|
|
||||||
"get_url" => BrowserAction::GetUrl,
|
|
||||||
"screenshot" => BrowserAction::Screenshot {
|
|
||||||
path: args.get("path").and_then(|v| v.as_str()).map(String::from),
|
|
||||||
full_page: args
|
|
||||||
.get("full_page")
|
|
||||||
.and_then(serde_json::Value::as_bool)
|
|
||||||
.unwrap_or(false),
|
|
||||||
},
|
|
||||||
"wait" => BrowserAction::Wait {
|
|
||||||
selector: args
|
|
||||||
.get("selector")
|
|
||||||
.and_then(|v| v.as_str())
|
|
||||||
.map(String::from),
|
|
||||||
ms: args.get("ms").and_then(serde_json::Value::as_u64),
|
|
||||||
text: args.get("text").and_then(|v| v.as_str()).map(String::from),
|
|
||||||
},
|
|
||||||
"press" => {
|
|
||||||
let key = args
|
|
||||||
.get("key")
|
|
||||||
.and_then(|v| v.as_str())
|
|
||||||
.ok_or_else(|| anyhow::anyhow!("Missing 'key' for press"))?;
|
|
||||||
BrowserAction::Press { key: key.into() }
|
|
||||||
}
|
|
||||||
"hover" => {
|
|
||||||
let selector = args
|
|
||||||
.get("selector")
|
|
||||||
.and_then(|v| v.as_str())
|
|
||||||
.ok_or_else(|| anyhow::anyhow!("Missing 'selector' for hover"))?;
|
|
||||||
BrowserAction::Hover {
|
|
||||||
selector: selector.into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"scroll" => {
|
|
||||||
let direction = args
|
|
||||||
.get("direction")
|
|
||||||
.and_then(|v| v.as_str())
|
|
||||||
.ok_or_else(|| anyhow::anyhow!("Missing 'direction' for scroll"))?;
|
|
||||||
BrowserAction::Scroll {
|
|
||||||
direction: direction.into(),
|
|
||||||
pixels: args
|
|
||||||
.get("pixels")
|
|
||||||
.and_then(serde_json::Value::as_u64)
|
|
||||||
.map(|p| u32::try_from(p).unwrap_or(u32::MAX)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"is_visible" => {
|
|
||||||
let selector = args
|
|
||||||
.get("selector")
|
|
||||||
.and_then(|v| v.as_str())
|
|
||||||
.ok_or_else(|| anyhow::anyhow!("Missing 'selector' for is_visible"))?;
|
|
||||||
BrowserAction::IsVisible {
|
|
||||||
selector: selector.into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"close" => BrowserAction::Close,
|
|
||||||
"find" => {
|
|
||||||
let by = args
|
|
||||||
.get("by")
|
|
||||||
.and_then(|v| v.as_str())
|
|
||||||
.ok_or_else(|| anyhow::anyhow!("Missing 'by' for find"))?;
|
|
||||||
let value = args
|
|
||||||
.get("value")
|
|
||||||
.and_then(|v| v.as_str())
|
|
||||||
.ok_or_else(|| anyhow::anyhow!("Missing 'value' for find"))?;
|
|
||||||
let action = args
|
|
||||||
.get("find_action")
|
|
||||||
.and_then(|v| v.as_str())
|
|
||||||
.ok_or_else(|| anyhow::anyhow!("Missing 'find_action' for find"))?;
|
|
||||||
BrowserAction::Find {
|
|
||||||
by: by.into(),
|
|
||||||
value: value.into(),
|
|
||||||
action: action.into(),
|
|
||||||
fill_value: args
|
|
||||||
.get("fill_value")
|
|
||||||
.and_then(|v| v.as_str())
|
|
||||||
.map(String::from),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
return Ok(ToolResult {
|
return Ok(ToolResult {
|
||||||
success: false,
|
success: false,
|
||||||
output: String::new(),
|
output: String::new(),
|
||||||
error: Some(format!(
|
error: Some(e.to_string()),
|
||||||
"Action '{action_str}' is unavailable for backend '{}'",
|
|
||||||
match backend {
|
|
||||||
ResolvedBackend::AgentBrowser => "agent_browser",
|
|
||||||
ResolvedBackend::RustNative => "rust_native",
|
|
||||||
ResolvedBackend::ComputerUse => "computer_use",
|
|
||||||
}
|
|
||||||
)),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -1871,6 +1718,161 @@ mod native_backend {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Action parsing ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Parse a JSON `args` object into a typed `BrowserAction`.
|
||||||
|
fn parse_browser_action(action_str: &str, args: &Value) -> anyhow::Result<BrowserAction> {
|
||||||
|
match action_str {
|
||||||
|
"open" => {
|
||||||
|
let url = args
|
||||||
|
.get("url")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("Missing 'url' for open action"))?;
|
||||||
|
Ok(BrowserAction::Open { url: url.into() })
|
||||||
|
}
|
||||||
|
"snapshot" => Ok(BrowserAction::Snapshot {
|
||||||
|
interactive_only: args
|
||||||
|
.get("interactive_only")
|
||||||
|
.and_then(serde_json::Value::as_bool)
|
||||||
|
.unwrap_or(true),
|
||||||
|
compact: args
|
||||||
|
.get("compact")
|
||||||
|
.and_then(serde_json::Value::as_bool)
|
||||||
|
.unwrap_or(true),
|
||||||
|
depth: args
|
||||||
|
.get("depth")
|
||||||
|
.and_then(serde_json::Value::as_u64)
|
||||||
|
.map(|d| u32::try_from(d).unwrap_or(u32::MAX)),
|
||||||
|
}),
|
||||||
|
"click" => {
|
||||||
|
let selector = args
|
||||||
|
.get("selector")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("Missing 'selector' for click"))?;
|
||||||
|
Ok(BrowserAction::Click {
|
||||||
|
selector: selector.into(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
"fill" => {
|
||||||
|
let selector = args
|
||||||
|
.get("selector")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("Missing 'selector' for fill"))?;
|
||||||
|
let value = args
|
||||||
|
.get("value")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("Missing 'value' for fill"))?;
|
||||||
|
Ok(BrowserAction::Fill {
|
||||||
|
selector: selector.into(),
|
||||||
|
value: value.into(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
"type" => {
|
||||||
|
let selector = args
|
||||||
|
.get("selector")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("Missing 'selector' for type"))?;
|
||||||
|
let text = args
|
||||||
|
.get("text")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("Missing 'text' for type"))?;
|
||||||
|
Ok(BrowserAction::Type {
|
||||||
|
selector: selector.into(),
|
||||||
|
text: text.into(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
"get_text" => {
|
||||||
|
let selector = args
|
||||||
|
.get("selector")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("Missing 'selector' for get_text"))?;
|
||||||
|
Ok(BrowserAction::GetText {
|
||||||
|
selector: selector.into(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
"get_title" => Ok(BrowserAction::GetTitle),
|
||||||
|
"get_url" => Ok(BrowserAction::GetUrl),
|
||||||
|
"screenshot" => Ok(BrowserAction::Screenshot {
|
||||||
|
path: args.get("path").and_then(|v| v.as_str()).map(String::from),
|
||||||
|
full_page: args
|
||||||
|
.get("full_page")
|
||||||
|
.and_then(serde_json::Value::as_bool)
|
||||||
|
.unwrap_or(false),
|
||||||
|
}),
|
||||||
|
"wait" => Ok(BrowserAction::Wait {
|
||||||
|
selector: args
|
||||||
|
.get("selector")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.map(String::from),
|
||||||
|
ms: args.get("ms").and_then(serde_json::Value::as_u64),
|
||||||
|
text: args.get("text").and_then(|v| v.as_str()).map(String::from),
|
||||||
|
}),
|
||||||
|
"press" => {
|
||||||
|
let key = args
|
||||||
|
.get("key")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("Missing 'key' for press"))?;
|
||||||
|
Ok(BrowserAction::Press { key: key.into() })
|
||||||
|
}
|
||||||
|
"hover" => {
|
||||||
|
let selector = args
|
||||||
|
.get("selector")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("Missing 'selector' for hover"))?;
|
||||||
|
Ok(BrowserAction::Hover {
|
||||||
|
selector: selector.into(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
"scroll" => {
|
||||||
|
let direction = args
|
||||||
|
.get("direction")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("Missing 'direction' for scroll"))?;
|
||||||
|
Ok(BrowserAction::Scroll {
|
||||||
|
direction: direction.into(),
|
||||||
|
pixels: args
|
||||||
|
.get("pixels")
|
||||||
|
.and_then(serde_json::Value::as_u64)
|
||||||
|
.map(|p| u32::try_from(p).unwrap_or(u32::MAX)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
"is_visible" => {
|
||||||
|
let selector = args
|
||||||
|
.get("selector")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("Missing 'selector' for is_visible"))?;
|
||||||
|
Ok(BrowserAction::IsVisible {
|
||||||
|
selector: selector.into(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
"close" => Ok(BrowserAction::Close),
|
||||||
|
"find" => {
|
||||||
|
let by = args
|
||||||
|
.get("by")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("Missing 'by' for find"))?;
|
||||||
|
let value = args
|
||||||
|
.get("value")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("Missing 'value' for find"))?;
|
||||||
|
let action = args
|
||||||
|
.get("find_action")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("Missing 'find_action' for find"))?;
|
||||||
|
Ok(BrowserAction::Find {
|
||||||
|
by: by.into(),
|
||||||
|
value: value.into(),
|
||||||
|
action: action.into(),
|
||||||
|
fill_value: args
|
||||||
|
.get("fill_value")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.map(String::from),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
other => anyhow::bail!("Unsupported browser action: {other}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Helper functions ─────────────────────────────────────────────
|
// ── Helper functions ─────────────────────────────────────────────
|
||||||
|
|
||||||
fn is_supported_browser_action(action: &str) -> bool {
|
fn is_supported_browser_action(action: &str) -> bool {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue