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:
165
crates/gyandoot-browser/src/macros.rs
Normal file
165
crates/gyandoot-browser/src/macros.rs
Normal file
@@ -0,0 +1,165 @@
|
||||
//! Search macros for common websites.
|
||||
//!
|
||||
//! Camofox-browser supports 14+ built-in search macros that map to
|
||||
//! site-specific search URLs. Using macros is more reliable than
|
||||
//! manually constructing URLs because the browser handles all
|
||||
//! redirects, cookies, and anti-bot challenges.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt;
|
||||
|
||||
/// All supported search macros.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum SearchMacro {
|
||||
/// `@google_search` — Search Google
|
||||
GoogleSearch,
|
||||
/// `@youtube_search` — Search YouTube
|
||||
YoutubeSearch,
|
||||
/// `@amazon_search` — Search Amazon
|
||||
AmazonSearch,
|
||||
/// `@reddit_search` — Search Reddit (returns JSON)
|
||||
RedditSearch,
|
||||
/// `@reddit_subreddit` — Browse a subreddit (returns JSON)
|
||||
RedditSubreddit,
|
||||
/// `@wikipedia_search` — Search Wikipedia
|
||||
WikipediaSearch,
|
||||
/// `@twitter_search` — Search Twitter/X
|
||||
TwitterSearch,
|
||||
/// `@yelp_search` — Search Yelp
|
||||
YelpSearch,
|
||||
/// `@spotify_search` — Search Spotify
|
||||
SpotifySearch,
|
||||
/// `@netflix_search` — Search Netflix
|
||||
NetflixSearch,
|
||||
/// `@linkedin_search` — Search LinkedIn
|
||||
LinkedinSearch,
|
||||
/// `@instagram_search` — Search Instagram
|
||||
InstagramSearch,
|
||||
/// `@tiktok_search` — Search TikTok
|
||||
TiktokSearch,
|
||||
/// `@twitch_search` — Search Twitch
|
||||
TwitchSearch,
|
||||
}
|
||||
|
||||
impl SearchMacro {
|
||||
/// All supported macros as a static slice.
|
||||
pub const ALL: &'static [SearchMacro] = &[
|
||||
SearchMacro::GoogleSearch,
|
||||
SearchMacro::YoutubeSearch,
|
||||
SearchMacro::AmazonSearch,
|
||||
SearchMacro::RedditSearch,
|
||||
SearchMacro::RedditSubreddit,
|
||||
SearchMacro::WikipediaSearch,
|
||||
SearchMacro::TwitterSearch,
|
||||
SearchMacro::YelpSearch,
|
||||
SearchMacro::SpotifySearch,
|
||||
SearchMacro::NetflixSearch,
|
||||
SearchMacro::LinkedinSearch,
|
||||
SearchMacro::InstagramSearch,
|
||||
SearchMacro::TiktokSearch,
|
||||
SearchMacro::TwitchSearch,
|
||||
];
|
||||
|
||||
/// The `@name` string sent to the camofox API.
|
||||
pub fn as_macro_str(&self) -> &'static str {
|
||||
match self {
|
||||
SearchMacro::GoogleSearch => "@google_search",
|
||||
SearchMacro::YoutubeSearch => "@youtube_search",
|
||||
SearchMacro::AmazonSearch => "@amazon_search",
|
||||
SearchMacro::RedditSearch => "@reddit_search",
|
||||
SearchMacro::RedditSubreddit => "@reddit_subreddit",
|
||||
SearchMacro::WikipediaSearch => "@wikipedia_search",
|
||||
SearchMacro::TwitterSearch => "@twitter_search",
|
||||
SearchMacro::YelpSearch => "@yelp_search",
|
||||
SearchMacro::SpotifySearch => "@spotify_search",
|
||||
SearchMacro::NetflixSearch => "@netflix_search",
|
||||
SearchMacro::LinkedinSearch => "@linkedin_search",
|
||||
SearchMacro::InstagramSearch => "@instagram_search",
|
||||
SearchMacro::TiktokSearch => "@tiktok_search",
|
||||
SearchMacro::TwitchSearch => "@twitch_search",
|
||||
}
|
||||
}
|
||||
|
||||
/// Human-readable site name.
|
||||
pub fn site_name(&self) -> &'static str {
|
||||
match self {
|
||||
SearchMacro::GoogleSearch => "Google",
|
||||
SearchMacro::YoutubeSearch => "YouTube",
|
||||
SearchMacro::AmazonSearch => "Amazon",
|
||||
SearchMacro::RedditSearch => "Reddit",
|
||||
SearchMacro::RedditSubreddit => "Reddit (subreddit)",
|
||||
SearchMacro::WikipediaSearch => "Wikipedia",
|
||||
SearchMacro::TwitterSearch => "Twitter/X",
|
||||
SearchMacro::YelpSearch => "Yelp",
|
||||
SearchMacro::SpotifySearch => "Spotify",
|
||||
SearchMacro::NetflixSearch => "Netflix",
|
||||
SearchMacro::LinkedinSearch => "LinkedIn",
|
||||
SearchMacro::InstagramSearch => "Instagram",
|
||||
SearchMacro::TiktokSearch => "TikTok",
|
||||
SearchMacro::TwitchSearch => "Twitch",
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse a `@name` string into a `SearchMacro`.
|
||||
pub fn from_str(s: &str) -> Option<Self> {
|
||||
match s {
|
||||
"@google_search" => Some(SearchMacro::GoogleSearch),
|
||||
"@youtube_search" => Some(SearchMacro::YoutubeSearch),
|
||||
"@amazon_search" => Some(SearchMacro::AmazonSearch),
|
||||
"@reddit_search" => Some(SearchMacro::RedditSearch),
|
||||
"@reddit_subreddit" => Some(SearchMacro::RedditSubreddit),
|
||||
"@wikipedia_search" => Some(SearchMacro::WikipediaSearch),
|
||||
"@twitter_search" => Some(SearchMacro::TwitterSearch),
|
||||
"@yelp_search" => Some(SearchMacro::YelpSearch),
|
||||
"@spotify_search" => Some(SearchMacro::SpotifySearch),
|
||||
"@netflix_search" => Some(SearchMacro::NetflixSearch),
|
||||
"@linkedin_search" => Some(SearchMacro::LinkedinSearch),
|
||||
"@instagram_search" => Some(SearchMacro::InstagramSearch),
|
||||
"@tiktok_search" => Some(SearchMacro::TiktokSearch),
|
||||
"@twitch_search" => Some(SearchMacro::TwitchSearch),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for SearchMacro {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.as_macro_str())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_all_macros_have_valid_names() {
|
||||
for m in SearchMacro::ALL {
|
||||
let name = m.as_macro_str();
|
||||
assert!(name.starts_with('@'), "Macro should start with @");
|
||||
assert!(name.len() > 2, "Macro should have a name");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_roundtrip_parse() {
|
||||
for m in SearchMacro::ALL {
|
||||
let name = m.as_macro_str();
|
||||
let parsed = SearchMacro::from_str(name);
|
||||
assert_eq!(parsed, Some(*m), "Roundtrip failed for {name}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_macro() {
|
||||
assert_eq!(SearchMacro::from_str("@invalid_search"), None);
|
||||
assert_eq!(SearchMacro::from_str("google_search"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_site_names() {
|
||||
assert_eq!(SearchMacro::GoogleSearch.site_name(), "Google");
|
||||
assert_eq!(SearchMacro::YoutubeSearch.site_name(), "YouTube");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user