Add camofox-browser integration: gyandoot-browser crate + agent tools
New crate: crates/gyandoot-browser/
- client.rs: Full REST client for camofox-browser API (health, tabs,
navigation, snapshots, click/type/scroll, search macros)
- macros.rs: 14 SearchMacro enum (Google, YouTube, Amazon, Reddit,
Wikipedia, Twitter, Yelp, Spotify, Netflix, LinkedIn, Instagram,
TikTok, Twitch) with parse/display
- snapshot.rs: PageSnapshot, PageLinks, NavigationResult, HealthResponse
- session.rs: BrowserSession, SessionManager with idle cleanup
New module: crates/gyandoot-agent/src/browser_tools.rs
- WebSearchTool: Search the web using anti-detection browser
- WebBrowseTool: Browse URLs, click, type, scroll with element refs
- register_browser_tools(): Wire both tools into ToolExecutor
CLI: Added
o
|
- - - - - - .---. - - - - - -
- - - - - - - - - [ ^ ^ ] - - - - - - - - -
- - - - - - - | ___ | - - - - - - -
`-----'
____ __ __ ___ _ _ ____ ___ ___ _____
/ ___|\ \ / / / _ \ | \ | || _ \ / _ \ / _ \|_ _|
| | _ \ V / / /_\ \| \| || | | | | | | | | | | |
| |_| | | | | _ || |\ || |_| | |_| | |_| | | |
\____/ |_| |_| |_||_| \_||____/ \___/ \___/ |_|
o - - - - - - - knowledge. delivered. - - - - - - - o
Unknown command: browser
Run 'gyandoot help' for usage. subcommand with health/search/browse actions
Installer: Updated install.sh with camofox-browser cloning, Node.js
detection, npm dependency install, and systemd service setup
Camofox-browser running at http://localhost:9377 (Docker, verified working)
Workspace: 12 crates + CLI binary — cargo check passes with 0 errors.
This commit is contained in:
180
crates/gyandoot-browser/src/snapshot.rs
Normal file
180
crates/gyandoot-browser/src/snapshot.rs
Normal file
@@ -0,0 +1,180 @@
|
||||
//! Types representing accessibility tree snapshots from camofox-browser.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// A page snapshot returned by the camofox-browser API.
|
||||
///
|
||||
/// The snapshot is an accessibility tree with stable element refs
|
||||
/// (e.g., `e1`, `e2`) that can be used for clicking and typing.
|
||||
/// This format is ~90% more token-efficient than raw HTML.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PageSnapshot {
|
||||
/// Accessibility tree text with element refs.
|
||||
pub snapshot: String,
|
||||
/// Optional URL of the page.
|
||||
#[serde(default)]
|
||||
pub url: String,
|
||||
/// Optional page title.
|
||||
#[serde(default)]
|
||||
pub title: String,
|
||||
/// Optional screenshot as base64 PNG.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub screenshot: Option<String>,
|
||||
/// Element reference map: ref -> element info.
|
||||
#[serde(default)]
|
||||
pub elements: HashMap<String, ElementInfo>,
|
||||
}
|
||||
|
||||
/// Information about a single element in the snapshot.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ElementInfo {
|
||||
/// The element's tag name (e.g., "a", "button", "input").
|
||||
#[serde(default)]
|
||||
pub tag: String,
|
||||
/// Visible text content.
|
||||
#[serde(default)]
|
||||
pub text: String,
|
||||
/// URL for links.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub href: Option<String>,
|
||||
/// Whether the element is interactive.
|
||||
#[serde(default)]
|
||||
pub interactive: bool,
|
||||
}
|
||||
|
||||
impl PageSnapshot {
|
||||
/// Create a new snapshot from raw text.
|
||||
pub fn new(snapshot: String) -> Self {
|
||||
Self {
|
||||
snapshot,
|
||||
url: String::new(),
|
||||
title: String::new(),
|
||||
screenshot: None,
|
||||
elements: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the snapshot is empty (no content).
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.snapshot.is_empty()
|
||||
}
|
||||
|
||||
/// Number of characters in the snapshot text.
|
||||
pub fn len(&self) -> usize {
|
||||
self.snapshot.len()
|
||||
}
|
||||
|
||||
/// Extract all element refs from the snapshot text.
|
||||
///
|
||||
/// Refs look like `[e1]`, `[e2]` in the accessibility tree.
|
||||
pub fn extract_refs(&self) -> Vec<String> {
|
||||
let mut refs = Vec::new();
|
||||
for line in self.snapshot.lines() {
|
||||
// Pattern: [eN] or eN
|
||||
if let Some(start) = line.find("[e") {
|
||||
let end = line[start + 2..].find(']').map(|i| start + 2 + i);
|
||||
if let Some(end_pos) = end {
|
||||
let ref_str = &line[start + 1..start + 1 + end_pos + 1];
|
||||
refs.push(ref_str.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
refs.sort();
|
||||
refs.dedup();
|
||||
refs
|
||||
}
|
||||
}
|
||||
|
||||
/// Links extracted from a page.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PageLinks {
|
||||
/// List of links on the page.
|
||||
pub links: Vec<LinkItem>,
|
||||
/// Total count of links.
|
||||
pub total: usize,
|
||||
}
|
||||
|
||||
/// A single link item.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct LinkItem {
|
||||
/// Link text.
|
||||
pub text: String,
|
||||
/// Target URL.
|
||||
pub href: String,
|
||||
/// Optional title attribute.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub title: Option<String>,
|
||||
}
|
||||
|
||||
/// Response from page navigation.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct NavigationResult {
|
||||
/// Final URL after navigation.
|
||||
pub url: String,
|
||||
/// Page title.
|
||||
pub title: String,
|
||||
/// Whether navigation succeeded.
|
||||
pub ok: bool,
|
||||
}
|
||||
|
||||
/// Response from element interaction (click/type).
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct InteractionResult {
|
||||
/// Whether the interaction succeeded.
|
||||
pub ok: bool,
|
||||
/// New snapshot after interaction (if available).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub snapshot: Option<PageSnapshot>,
|
||||
}
|
||||
|
||||
/// Health check response from camofox-browser.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct HealthResponse {
|
||||
/// Server is OK.
|
||||
pub ok: bool,
|
||||
/// Engine type (e.g., "camoufox").
|
||||
pub engine: String,
|
||||
/// Whether browser is connected.
|
||||
#[serde(default)]
|
||||
pub browser_connected: bool,
|
||||
/// Whether browser is running.
|
||||
#[serde(default)]
|
||||
pub browser_running: bool,
|
||||
/// Number of active tabs.
|
||||
#[serde(default)]
|
||||
pub active_tabs: u32,
|
||||
/// Number of active sessions.
|
||||
#[serde(default)]
|
||||
pub active_sessions: u32,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_extract_refs_from_snapshot() {
|
||||
let snap = PageSnapshot::new(
|
||||
"- heading \"Example\" [level=1]\n- link \"Click\" [e1]:\n - /url: https://example.com\n- paragraph: text [e2]\n".into(),
|
||||
);
|
||||
let refs = snap.extract_refs();
|
||||
assert!(refs.contains(&"[e1]".to_string()));
|
||||
assert!(refs.contains(&"[e2]".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_snapshot() {
|
||||
let snap = PageSnapshot::new(String::new());
|
||||
assert!(snap.is_empty());
|
||||
assert_eq!(snap.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_health_response_deserialize() {
|
||||
let json = r#"{"ok":true,"engine":"camoufox","browserConnected":true,"browserRunning":true,"activeTabs":3,"activeSessions":2}"#;
|
||||
let health: HealthResponse = serde_json::from_str(json).unwrap();
|
||||
assert!(health.ok);
|
||||
assert_eq!(health.active_tabs, 3);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user