Files
zclaw_openfang/desktop/src-tauri/src/browser/client.rs
2026-03-17 23:26:16 +08:00

494 lines
15 KiB
Rust

// Browser client using Fantoccini WebDriver
use crate::browser::error::{BrowserError, Result};
use crate::browser::session::{BrowserSession, BrowserType, SessionConfig, SessionManager};
use base64::{engine::general_purpose::STANDARD, Engine};
use fantoccini::elements::Element;
use fantoccini::{Client, ClientBuilder, Locator};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::RwLock;
use uuid::Uuid;
/// Main browser automation client
pub struct BrowserClient {
/// Active WebDriver connections
connections: Arc<RwLock<HashMap<String, Client>>>,
/// Session manager
session_manager: SessionManager,
}
impl BrowserClient {
pub fn new() -> Self {
Self {
connections: Arc::new(RwLock::new(HashMap::new())),
session_manager: SessionManager::new(),
}
}
/// Create a new browser session
pub async fn create_session(&self, config: SessionConfig) -> Result<String> {
let session_id = Uuid::new_v4().to_string();
// Build WebDriver capabilities as Map
let capabilities = self.build_capabilities(&config)?;
// Connect to WebDriver
let client = ClientBuilder::native()
.capabilities(capabilities)
.connect(&config.webdriver_url)
.await
.map_err(|e| BrowserError::ConnectionFailed(e.to_string()))?;
// Store connection
{
let mut connections = self.connections.write().await;
connections.insert(session_id.clone(), client);
}
// Create session record
let session = BrowserSession::new(session_id.clone(), config);
self.session_manager.add_session(session).await;
Ok(session_id)
}
/// Close a browser session
pub async fn close_session(&self, session_id: &str) -> Result<()> {
let client = self.get_client(session_id).await?;
// Close the browser
client
.close()
.await
.map_err(|e| BrowserError::ConnectionFailed(e.to_string()))?;
// Remove from connections
{
let mut connections = self.connections.write().await;
connections.remove(session_id);
}
// Remove session record
self.session_manager.remove_session(session_id).await;
Ok(())
}
/// Get session information
pub async fn get_session(&self, session_id: &str) -> Result<BrowserSession> {
self.session_manager
.get_session(session_id)
.await
.ok_or_else(|| BrowserError::SessionNotFound(session_id.to_string()))
}
/// List all sessions
pub async fn list_sessions(&self) -> Vec<BrowserSession> {
self.session_manager.list_sessions().await
}
/// Navigate to URL
pub async fn navigate(&self, session_id: &str, url: &str) -> Result<NavigationResult> {
let client = self.get_client(session_id).await?;
client
.goto(url)
.await
.map_err(|_| BrowserError::NavigationFailed {
url: url.to_string(),
})?;
// Get current URL and title
let current_url = client.current_url().await.ok().map(|u| u.to_string());
let title = client.title().await.ok();
// Update session
self.session_manager
.update_session(session_id, |s| {
s.update_location(current_url.clone(), title.clone());
})
.await;
Ok(NavigationResult {
url: current_url,
title,
})
}
/// Go back
pub async fn back(&self, session_id: &str) -> Result<()> {
let client = self.get_client(session_id).await?;
client.back().await.map_err(|e| BrowserError::CommandFailed(e.to_string()))?;
self.update_session_location(session_id).await;
Ok(())
}
/// Go forward
pub async fn forward(&self, session_id: &str) -> Result<()> {
let client = self.get_client(session_id).await?;
client.forward().await.map_err(|e| BrowserError::CommandFailed(e.to_string()))?;
self.update_session_location(session_id).await;
Ok(())
}
/// Refresh page
pub async fn refresh(&self, session_id: &str) -> Result<()> {
let client = self.get_client(session_id).await?;
client.refresh().await.map_err(|e| BrowserError::CommandFailed(e.to_string()))?;
Ok(())
}
/// Find element by CSS selector
pub async fn find_element(&self, session_id: &str, selector: &str) -> Result<ElementInfo> {
let client = self.get_client(session_id).await?;
let element = client
.find(Locator::Css(selector))
.await
.map_err(|_| BrowserError::ElementNotFound {
selector: selector.to_string(),
})?;
self.element_to_info(&element, selector).await
}
/// Find multiple elements
pub async fn find_elements(&self, session_id: &str, selector: &str) -> Result<Vec<ElementInfo>> {
let client = self.get_client(session_id).await?;
let elements = client
.find_all(Locator::Css(selector))
.await
.map_err(|_| BrowserError::ElementNotFound {
selector: selector.to_string(),
})?;
let mut infos = Vec::new();
for element in elements {
if let Ok(info) = self.element_to_info(&element, selector).await {
infos.push(info);
}
}
Ok(infos)
}
/// Click element
pub async fn click(&self, session_id: &str, selector: &str) -> Result<()> {
let client = self.get_client(session_id).await?;
let element = client
.find(Locator::Css(selector))
.await
.map_err(|_| BrowserError::ElementNotFound {
selector: selector.to_string(),
})?;
element.click().await.map_err(|e| BrowserError::CommandFailed(e.to_string()))?;
self.update_session_location(session_id).await;
Ok(())
}
/// Type text into element
pub async fn type_text(&self, session_id: &str, selector: &str, text: &str) -> Result<()> {
let client = self.get_client(session_id).await?;
let element = client
.find(Locator::Css(selector))
.await
.map_err(|_| BrowserError::ElementNotFound {
selector: selector.to_string(),
})?;
element.send_keys(text).await.map_err(|e| BrowserError::CommandFailed(e.to_string()))?;
Ok(())
}
/// Clear and type text
pub async fn clear_and_type(&self, session_id: &str, selector: &str, text: &str) -> Result<()> {
let client = self.get_client(session_id).await?;
let element = client
.find(Locator::Css(selector))
.await
.map_err(|_| BrowserError::ElementNotFound {
selector: selector.to_string(),
})?;
element.clear().await.map_err(|e| BrowserError::CommandFailed(e.to_string()))?;
element.send_keys(text).await.map_err(|e| BrowserError::CommandFailed(e.to_string()))?;
Ok(())
}
/// Get element text
pub async fn get_text(&self, session_id: &str, selector: &str) -> Result<String> {
let client = self.get_client(session_id).await?;
let element = client
.find(Locator::Css(selector))
.await
.map_err(|_| BrowserError::ElementNotFound {
selector: selector.to_string(),
})?;
element.text().await.map_err(|e| BrowserError::CommandFailed(e.to_string()))
}
/// Get element attribute
pub async fn get_attribute(
&self,
session_id: &str,
selector: &str,
attribute: &str,
) -> Result<Option<String>> {
let client = self.get_client(session_id).await?;
let element = client
.find(Locator::Css(selector))
.await
.map_err(|_| BrowserError::ElementNotFound {
selector: selector.to_string(),
})?;
element.attr(attribute).await.map_err(|e| BrowserError::CommandFailed(e.to_string()))
}
/// Execute JavaScript
pub async fn execute_script(
&self,
session_id: &str,
script: &str,
args: Vec<serde_json::Value>,
) -> Result<serde_json::Value> {
let client = self.get_client(session_id).await?;
client.execute(script, args).await.map_err(|e| BrowserError::ScriptError {
message: e.to_string(),
})
}
/// Take screenshot
pub async fn screenshot(&self, session_id: &str) -> Result<ScreenshotResult> {
let client = self.get_client(session_id).await?;
let screenshot = client.screenshot().await.map_err(|e| BrowserError::ScreenshotFailed {
reason: e.to_string(),
})?;
let base64_data = STANDARD.encode(&screenshot);
Ok(ScreenshotResult {
data: screenshot,
base64: base64_data,
format: "png".to_string(),
})
}
/// Take element screenshot
pub async fn element_screenshot(
&self,
session_id: &str,
selector: &str,
) -> Result<ScreenshotResult> {
let client = self.get_client(session_id).await?;
let element = client
.find(Locator::Css(selector))
.await
.map_err(|_| BrowserError::ElementNotFound {
selector: selector.to_string(),
})?;
let screenshot = element.screenshot().await.map_err(|e| BrowserError::ScreenshotFailed {
reason: e.to_string(),
})?;
let base64_data = STANDARD.encode(&screenshot);
Ok(ScreenshotResult {
data: screenshot,
base64: base64_data,
format: "png".to_string(),
})
}
/// Wait for element with custom timeout
pub async fn wait_for_element(
&self,
session_id: &str,
selector: &str,
timeout_ms: u64,
) -> Result<ElementInfo> {
let client = self.get_client(session_id).await?;
let locator = Locator::Css(selector);
// Use the new wait().for_element() API instead of deprecated wait_for_find
let element = tokio::time::timeout(
Duration::from_millis(timeout_ms),
client.wait().for_element(locator)
)
.await
.map_err(|_| BrowserError::Timeout {
selector: selector.to_string(),
})?
.map_err(|_| BrowserError::ElementNotFound {
selector: selector.to_string(),
})?;
self.element_to_info(&element, selector).await
}
/// Get page source
pub async fn get_source(&self, session_id: &str) -> Result<String> {
let client = self.get_client(session_id).await?;
client.source().await.map_err(|e| BrowserError::CommandFailed(e.to_string()))
}
/// Get current URL
pub async fn get_current_url(&self, session_id: &str) -> Result<String> {
let client = self.get_client(session_id).await?;
let url = client.current_url().await.map_err(|e| BrowserError::CommandFailed(e.to_string()))?;
Ok(url.to_string())
}
/// Get page title
pub async fn get_title(&self, session_id: &str) -> Result<String> {
let client = self.get_client(session_id).await?;
client.title().await.map_err(|e| BrowserError::CommandFailed(e.to_string()))
}
// Private helper methods
async fn get_client(&self, session_id: &str) -> Result<Client> {
let connections = self.connections.read().await;
connections
.get(session_id)
.cloned()
.ok_or_else(|| BrowserError::SessionNotFound(session_id.to_string()))
}
fn build_capabilities(&self, config: &SessionConfig) -> Result<serde_json::Map<String, serde_json::Value>> {
let browser_name = match config.browser_type {
BrowserType::Chrome => "chrome",
BrowserType::Firefox => "firefox",
BrowserType::Edge => "MicrosoftEdge",
BrowserType::Safari => "safari",
};
let mut args = vec![];
if config.headless {
args.push("--headless".to_string());
}
if let Some((width, height)) = config.window_size {
args.push(format!("--window-size={},{}", width, height));
}
args.extend(config.browser_args.clone());
let mut caps = serde_json::Map::new();
caps.insert("browserName".to_string(), serde_json::json!(browser_name));
let mut chrome_options = serde_json::Map::new();
chrome_options.insert("args".to_string(), serde_json::json!(args));
chrome_options.insert("w3c".to_string(), serde_json::json!(true));
caps.insert("goog:chromeOptions".to_string(), serde_json::Value::Object(chrome_options));
Ok(caps)
}
async fn element_to_info(&self, element: &Element, selector: &str) -> Result<ElementInfo> {
let tag_name = element.tag_name().await.ok();
let text = element.text().await.ok();
let is_displayed = element.is_displayed().await.unwrap_or(false);
let is_enabled = element.is_enabled().await.unwrap_or(false);
let is_selected = element.is_selected().await.unwrap_or(false);
// Note: location() and size() may not be available in all fantoccini versions
// Using placeholder values if not available
let location = None;
let size = None;
Ok(ElementInfo {
selector: selector.to_string(),
tag_name,
text,
is_displayed,
is_enabled,
is_selected,
location,
size,
})
}
async fn update_session_location(&self, session_id: &str) {
if let Ok(client) = self.get_client(session_id).await {
let current_url = client.current_url().await.ok().map(|u| u.to_string());
let title = client.title().await.ok();
self.session_manager
.update_session(session_id, |s| {
s.update_location(current_url, title);
})
.await;
}
}
}
impl Default for BrowserClient {
fn default() -> Self {
Self::new()
}
}
impl Clone for BrowserClient {
fn clone(&self) -> Self {
Self {
connections: Arc::clone(&self.connections),
session_manager: self.session_manager.clone(),
}
}
}
// Result types
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NavigationResult {
pub url: Option<String>,
pub title: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ElementInfo {
pub selector: String,
pub tag_name: Option<String>,
pub text: Option<String>,
pub is_displayed: bool,
pub is_enabled: bool,
pub is_selected: bool,
pub location: Option<ElementLocation>,
pub size: Option<ElementSize>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ElementLocation {
pub x: i32,
pub y: i32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ElementSize {
pub width: u64,
pub height: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScreenshotResult {
pub data: Vec<u8>,
pub base64: String,
pub format: String,
}