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<Arc<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<Arc<dyn LLMProvider>> =
144 options.llm.map(|config| Arc::from(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 #[cfg(feature = "ai")]
264 let page = SpiderPage::from_arc_with(
265 Arc::clone(&adapter),
266 self.emitter.clone(),
267 self.llm_provider.clone(),
268 );
269 #[cfg(not(feature = "ai"))]
270 let page = SpiderPage::from_arc(Arc::clone(&adapter));
271 let page = Arc::new(page);
272
273 let page_for_routing = Arc::clone(&page);
275 tokio::spawn(async move {
276 while let Some(data) = msg_rx.recv().await {
277 page_for_routing.route_message(&data);
278 }
279 });
280
281 self.transport = Some(transport);
282 self.adapter = Some(adapter);
283 self.page = Some(page);
284 self.msg_send_tx = Some(proto_tx);
285
286 info!("SpiderBrowser initialized (browser={})", active_browser);
287 Ok(())
288 }
289
290 pub async fn goto(&self, url: &str) -> Result<()> {
292 self.current_url.store(Arc::new(Some(url.to_string())));
293 self.page().goto(url).await
294 }
295
296 pub fn close(&mut self) {
298 if let Some(ref page) = self.page {
299 page.destroy();
300 }
301 if let Some(ref transport) = self.transport {
302 transport.close();
303 }
304 self.emitter.remove_all_listeners();
305 self.page = None;
306 self.adapter = None;
307 self.transport = None;
308 info!("SpiderBrowser closed");
309 }
310
311 #[cfg(feature = "ai")]
317 pub async fn act(&self, instruction: &str) -> Result<()> {
318 let llm = self.require_llm()?;
319 let adapter = self.require_adapter()?;
320 crate::ai::act::act(adapter, llm.as_ref(), instruction).await
321 }
322
323 #[cfg(feature = "ai")]
325 pub async fn observe(
326 &self,
327 instruction: Option<&str>,
328 ) -> Result<Vec<crate::ai::observe::ObserveResult>> {
329 let adapter = self.require_adapter()?;
330 let llm_ref: Option<&dyn LLMProvider> = self.llm_provider.as_ref().map(|b| b.as_ref());
331 crate::ai::observe::observe(adapter, instruction, llm_ref).await
332 }
333
334 #[cfg(feature = "ai")]
336 pub async fn extract<T: serde::de::DeserializeOwned + Send>(
337 &self,
338 instruction: &str,
339 ) -> Result<T> {
340 let llm = self.require_llm()?;
341 let adapter = self.require_adapter()?;
342 crate::ai::extract::extract(adapter, llm.as_ref(), instruction, None).await
343 }
344
345 #[cfg(feature = "ai")]
347 pub fn agent(
348 &self,
349 options: Option<crate::ai::agent::AgentOptions>,
350 ) -> crate::ai::agent::Agent<'_> {
351 let llm = self
352 .llm_provider
353 .as_ref()
354 .expect("LLM not configured. Pass llm option for AI methods.");
355 let adapter = self
356 .adapter
357 .as_ref()
358 .expect("SpiderBrowser not initialized. Call init() first.");
359 crate::ai::agent::Agent::new(adapter, llm.as_ref(), &self.emitter, options)
360 }
361
362 #[cfg(feature = "ai")]
363 fn require_llm(&self) -> Result<&Arc<dyn LLMProvider>> {
364 self.llm_provider.as_ref().ok_or_else(|| {
365 SpiderError::Llm(
366 "LLM not configured. Pass llm option to SpiderBrowser for AI methods.".into(),
367 )
368 })
369 }
370
371 fn require_adapter(&self) -> Result<&ProtocolAdapter> {
372 self.adapter
373 .as_ref()
374 .map(|a| a.as_ref())
375 .ok_or_else(|| {
376 SpiderError::Protocol(
377 "SpiderBrowser not initialized. Call init() first.".into(),
378 )
379 })
380 }
381}