pub use crate::browser::profile::{BrowserProfile, ProxyConfig};
use crate::error::BrowsingError;
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
use tracing::warn;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentConfig {
pub max_steps: Option<u32>,
pub use_vision: Option<bool>,
pub system_prompt: Option<String>,
}
impl Default for AgentConfig {
fn default() -> Self {
Self {
max_steps: Some(100),
use_vision: None,
system_prompt: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct Config {
pub browser_profile: BrowserProfile,
pub agent: AgentConfig,
}
impl Config {
pub fn validate(&self) -> Result<(), Vec<String>> {
let mut errors = Vec::new();
if let Some(max_steps) = self.agent.max_steps {
if max_steps == 0 {
errors.push("Agent max_steps cannot be zero".to_string());
} else if max_steps > 1000 {
errors.push(format!(
"Agent max_steps too large ({}), maximum is 1000",
max_steps
));
}
}
if let Some(ref allowed_domains) = self.browser_profile.allowed_domains {
if allowed_domains.is_empty() {
errors.push(
"Browser allowed_domains list is empty, which would block all domains"
.to_string(),
);
} else {
for domain in allowed_domains {
if domain.is_empty() {
errors.push("Browser allowed_domains contains empty domain".to_string());
} else if !domain.contains('.') && !domain.is_empty() {
errors.push(format!(
"Invalid domain format: '{}', domains should contain at least one dot",
domain
));
}
}
}
}
if let Some(ref proxy) = self.browser_profile.proxy {
if proxy.server.is_empty() {
errors.push("Proxy server URL is empty".to_string());
} else if !proxy.server.starts_with("http://")
&& !proxy.server.starts_with("https://")
&& !proxy.server.starts_with("socks://")
{
errors.push(format!(
"Invalid proxy URL scheme: '{}', must be http://, https://, or socks://",
proxy.server
));
}
if let Some(ref bypass) = proxy.bypass {
if bypass.is_empty() {
errors.push("Proxy bypass list is empty".to_string());
}
}
}
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
pub fn validate_or_error(&self) -> Result<(), BrowsingError> {
match self.validate() {
Ok(()) => Ok(()),
Err(errors) => {
let error_msg = if errors.len() == 1 {
format!("Configuration error: {}", errors[0])
} else {
format!(
"Configuration errors ({}):\n {}",
errors.len(),
errors.join("\n ")
)
};
Err(BrowsingError::Config(error_msg))
}
}
}
}
pub fn parse_proxy_config_from_env() -> Option<ProxyConfig> {
let server = std::env::var("HTTP_PROXY")
.or_else(|_| std::env::var("http_proxy"))
.or_else(|_| std::env::var("HTTPS_PROXY"))
.or_else(|_| std::env::var("https_proxy"))
.ok()?;
let bypass = std::env::var("NO_PROXY")
.or_else(|_| std::env::var("no_proxy"))
.ok()
.filter(|s| !s.is_empty());
let username = std::env::var("PROXY_USERNAME")
.or_else(|_| std::env::var("proxy_username"))
.ok()
.filter(|s| !s.is_empty());
let password = std::env::var("PROXY_PASSWORD")
.or_else(|_| std::env::var("proxy_password"))
.ok()
.filter(|s| !s.is_empty());
Some(ProxyConfig {
server,
bypass,
username,
password,
})
}
impl Config {
pub fn from_env() -> Self {
let _ = dotenv::dotenv();
Self {
browser_profile: BrowserProfile {
headless: std::env::var("BROWSER_USE_HEADLESS")
.ok()
.and_then(|v| v.parse().ok()),
user_data_dir: std::env::var("BROWSER_USE_USER_DATA_DIR")
.ok()
.map(PathBuf::from),
allowed_domains: std::env::var("BROWSER_USE_ALLOWED_DOMAINS")
.ok()
.map(|s| s.split(',').map(|s| s.trim().to_string()).collect()),
downloads_path: std::env::var("BROWSER_USE_DOWNLOADS_PATH")
.ok()
.map(PathBuf::from),
proxy: parse_proxy_config_from_env(),
stealth: std::env::var("BROWSER_USE_STEALTH")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(false),
user_agent: std::env::var("BROWSER_USE_USER_AGENT").ok(),
},
agent: AgentConfig {
max_steps: std::env::var("BROWSER_USE_MAX_STEPS")
.ok()
.and_then(|v| v.parse().ok())
.or(Some(100)),
use_vision: std::env::var("BROWSER_USE_VISION")
.ok()
.and_then(|v| v.parse().ok()),
system_prompt: None,
},
}
}
pub fn load_from_file<P: AsRef<Path>>(path: P) -> anyhow::Result<Self> {
if !path.as_ref().exists() {
warn!("Config file not found, using defaults");
return Ok(Self::from_env());
}
let content = std::fs::read_to_string(path)?;
let config: Config = serde_json::from_str(&content)?;
Ok(config)
}
}