1use crate::errors::{Result, SpiderError};
4use crate::events::SpiderEventEmitter;
5use crate::page::SpiderPage;
6use crate::protocol::protocol_adapter::{ProtocolAdapter, ProtocolAdapterOptions};
7use crate::protocol::transport::{Transport, TransportOptions};
8use arc_swap::ArcSwap;
9use std::sync::Arc;
10use tokio::sync::mpsc;
11use tracing::info;
12
13#[cfg(feature = "ai")]
14use crate::ai::llm_provider::{create_provider, LLMConfig, LLMProvider};
15
16#[derive(Clone, Debug)]
18pub struct SpiderBrowserOptions {
19 pub api_key: String,
21 pub server_url: Option<String>,
23 pub browser: Option<String>,
25 pub url: Option<String>,
27 pub captcha: Option<String>,
29 pub smart_retry: Option<bool>,
31 pub max_retries: Option<u32>,
33 pub stealth: Option<u32>,
35 pub max_stealth_levels: Option<u32>,
37 pub connect_timeout_ms: Option<u64>,
39 pub command_timeout_ms: Option<u64>,
41 pub retry_timeout_ms: Option<u64>,
43 pub hedge: Option<bool>,
45 pub record: Option<bool>,
47 pub mode: Option<String>,
49 pub country: Option<String>,
51 pub proxy_url: Option<String>,
53 #[cfg(feature = "ai")]
55 pub llm: Option<LLMConfig>,
56}
57
58impl SpiderBrowserOptions {
59 pub fn new(api_key: impl Into<String>) -> Self {
60 Self {
61 api_key: api_key.into(),
62 server_url: None,
63 browser: None,
64 url: None,
65 captcha: None,
66 smart_retry: None,
67 max_retries: None,
68 stealth: None,
69 max_stealth_levels: None,
70 connect_timeout_ms: None,
71 command_timeout_ms: None,
72 retry_timeout_ms: None,
73 hedge: None,
74 record: None,
75 mode: None,
76 country: None,
77 proxy_url: None,
78 #[cfg(feature = "ai")]
79 llm: None,
80 }
81 }
82}
83
84struct ResolvedOptions {
85 api_key: String,
86 server_url: String,
87 browser: String,
88 url: Option<String>,
89 captcha: String,
90 smart_retry: bool,
91 max_retries: u32,
92 stealth: u32,
93 max_stealth_levels: u32,
94 connect_timeout_ms: u64,
95 command_timeout_ms: u64,
96 retry_timeout_ms: u64,
97 hedge: Option<bool>,
98 record: Option<bool>,
99 mode: Option<String>,
100 country: Option<String>,
101 proxy_url: Option<String>,
102}
103
104pub struct SpiderBrowser {
109 opts: ResolvedOptions,
110 transport: Option<Arc<Transport>>,
111 adapter: Option<Arc<ProtocolAdapter>>,
112 page: Option<Arc<SpiderPage>>,
113 emitter: SpiderEventEmitter,
114 current_url: ArcSwap<Option<String>>,
115 #[cfg(feature = "ai")]
116 llm_provider: Option<Box<dyn LLMProvider>>,
117 msg_send_tx: Option<mpsc::UnboundedSender<String>>,
118}
119
120impl SpiderBrowser {
121 pub fn new(options: SpiderBrowserOptions) -> Self {
122 let resolved = ResolvedOptions {
123 api_key: options.api_key.clone(),
124 server_url: options.server_url.unwrap_or_else(|| "wss://browser.spider.cloud".into()),
125 browser: options.browser.unwrap_or_else(|| "auto".into()),
126 url: options.url.clone(),
127 captcha: options.captcha.unwrap_or_else(|| "solve".into()),
128 smart_retry: options.smart_retry.unwrap_or(true),
129 max_retries: options.max_retries.unwrap_or(12),
130 stealth: options.stealth.unwrap_or(0),
131 max_stealth_levels: options.max_stealth_levels.unwrap_or(3),
132 connect_timeout_ms: options.connect_timeout_ms.unwrap_or(30_000),
133 command_timeout_ms: options.command_timeout_ms.unwrap_or(30_000),
134 retry_timeout_ms: options.retry_timeout_ms.unwrap_or(15_000),
135 hedge: options.hedge,
136 record: options.record,
137 mode: options.mode.clone(),
138 country: options.country.clone(),
139 proxy_url: options.proxy_url.clone(),
140 };
141
142 #[cfg(feature = "ai")]
143 let llm_provider: Option<Box<dyn LLMProvider>> =
144 options.llm.map(|config| create_provider(config));
145
146 Self {
147 opts: resolved,
148 transport: None,
149 adapter: None,
150 page: None,
151 emitter: SpiderEventEmitter::new(),
152 current_url: ArcSwap::from_pointee(options.url),
153 #[cfg(feature = "ai")]
154 llm_provider,
155 msg_send_tx: None,
156 }
157 }
158
159 pub fn page(&self) -> &SpiderPage {
161 self.page
162 .as_ref()
163 .expect("SpiderBrowser not initialized. Call init() first.")
164 }
165
166 pub fn browser(&self) -> String {
168 self.transport
169 .as_ref()
170 .map(|t| t.browser())
171 .unwrap_or_else(|| self.opts.browser.clone())
172 }
173
174 pub fn connected(&self) -> bool {
176 self.transport
177 .as_ref()
178 .map(|t| t.is_connected())
179 .unwrap_or(false)
180 }
181
182 pub fn stealth_level(&self) -> u32 {
184 self.transport
185 .as_ref()
186 .map(|t| t.get_stealth_level())
187 .unwrap_or(self.opts.stealth)
188 }
189
190 pub fn credits(&self) -> Option<f64> {
192 self.transport.as_ref().and_then(|t| t.upgrade_credits())
193 }
194
195 pub fn session_credits_used(&self) -> Option<f64> {
197 self.transport
198 .as_ref()
199 .and_then(|t| t.session_credits_used())
200 }
201
202 pub fn on(&self, event: &str, handler: crate::events::EventHandler) {
204 self.emitter.on(event, handler);
205 }
206
207 pub async fn init(&mut self) -> Result<()> {
209 let transport_opts = TransportOptions {
210 api_key: self.opts.api_key.clone(),
211 server_url: self.opts.server_url.clone(),
212 browser: self.opts.browser.clone(),
213 url: self.opts.url.clone(),
214 captcha: Some(self.opts.captcha.clone()),
215 stealth_level: self.opts.stealth,
216 connect_timeout_ms: self.opts.connect_timeout_ms,
217 command_timeout_ms: self.opts.command_timeout_ms,
218 hedge: self.opts.hedge.unwrap_or(false),
219 record: self.opts.record.unwrap_or(false),
220 mode: self.opts.mode.clone(),
221 country: self.opts.country.clone(),
222 proxy_url: self.opts.proxy_url.clone(),
223 };
224
225 let transport = Transport::new(transport_opts, self.emitter.clone());
226 transport.connect(3).await?;
227
228 let active_browser = transport.browser();
229
230 let mut msg_rx = transport
232 .take_message_rx()
233 .await
234 .ok_or_else(|| SpiderError::Protocol("Message receiver already taken".into()))?;
235
236 let adapter_opts = if self.opts.command_timeout_ms != 30_000 {
237 Some(ProtocolAdapterOptions {
238 command_timeout_ms: Some(self.opts.command_timeout_ms),
239 })
240 } else {
241 None
242 };
243
244 let (proto_tx, mut proto_rx) = mpsc::unbounded_channel::<String>();
246 let transport_for_relay = Arc::clone(&transport);
247 tokio::spawn(async move {
248 while let Some(data) = proto_rx.recv().await {
249 let _ = transport_for_relay.send(data);
250 }
251 });
252
253 let mut adapter = ProtocolAdapter::new(
254 proto_tx.clone(),
255 self.emitter.clone(),
256 &active_browser,
257 adapter_opts,
258 );
259 adapter.init().await?;
260
261 let adapter = Arc::new(adapter);
263 let page = SpiderPage::from_arc(Arc::clone(&adapter));
264 let page = Arc::new(page);
265
266 let page_for_routing = Arc::clone(&page);
268 tokio::spawn(async move {
269 while let Some(data) = msg_rx.recv().await {
270 page_for_routing.route_message(&data);
271 }
272 });
273
274 self.transport = Some(transport);
275 self.adapter = Some(adapter);
276 self.page = Some(page);
277 self.msg_send_tx = Some(proto_tx);
278
279 info!("SpiderBrowser initialized (browser={})", active_browser);
280 Ok(())
281 }
282
283 pub async fn goto(&self, url: &str) -> Result<()> {
285 self.current_url.store(Arc::new(Some(url.to_string())));
286 self.page().goto(url).await
287 }
288
289 pub fn close(&mut self) {
291 if let Some(ref page) = self.page {
292 page.destroy();
293 }
294 if let Some(ref transport) = self.transport {
295 transport.close();
296 }
297 self.emitter.remove_all_listeners();
298 self.page = None;
299 self.adapter = None;
300 self.transport = None;
301 info!("SpiderBrowser closed");
302 }
303
304 #[cfg(feature = "ai")]
310 pub async fn act(&self, instruction: &str) -> Result<()> {
311 let llm = self.require_llm()?;
312 let adapter = self.require_adapter()?;
313 crate::ai::act::act(adapter, llm.as_ref(), instruction).await
314 }
315
316 #[cfg(feature = "ai")]
318 pub async fn observe(
319 &self,
320 instruction: Option<&str>,
321 ) -> Result<Vec<crate::ai::observe::ObserveResult>> {
322 let adapter = self.require_adapter()?;
323 let llm_ref: Option<&dyn LLMProvider> = self.llm_provider.as_ref().map(|b| b.as_ref());
324 crate::ai::observe::observe(adapter, instruction, llm_ref).await
325 }
326
327 #[cfg(feature = "ai")]
329 pub async fn extract<T: serde::de::DeserializeOwned + Send>(
330 &self,
331 instruction: &str,
332 ) -> Result<T> {
333 let llm = self.require_llm()?;
334 let adapter = self.require_adapter()?;
335 crate::ai::extract::extract(adapter, llm.as_ref(), instruction, None).await
336 }
337
338 #[cfg(feature = "ai")]
340 pub fn agent(
341 &self,
342 options: Option<crate::ai::agent::AgentOptions>,
343 ) -> crate::ai::agent::Agent<'_> {
344 let llm = self
345 .llm_provider
346 .as_ref()
347 .expect("LLM not configured. Pass llm option for AI methods.");
348 let adapter = self
349 .adapter
350 .as_ref()
351 .expect("SpiderBrowser not initialized. Call init() first.");
352 crate::ai::agent::Agent::new(adapter, llm.as_ref(), &self.emitter, options)
353 }
354
355 #[cfg(feature = "ai")]
356 fn require_llm(&self) -> Result<&Box<dyn LLMProvider>> {
357 self.llm_provider.as_ref().ok_or_else(|| {
358 SpiderError::Llm(
359 "LLM not configured. Pass llm option to SpiderBrowser for AI methods.".into(),
360 )
361 })
362 }
363
364 fn require_adapter(&self) -> Result<&ProtocolAdapter> {
365 self.adapter
366 .as_ref()
367 .map(|a| a.as_ref())
368 .ok_or_else(|| {
369 SpiderError::Protocol(
370 "SpiderBrowser not initialized. Call init() first.".into(),
371 )
372 })
373 }
374}