feat(sync): 同步 upstream 改动并升级到 0.16.3-fork.5
This commit is contained in:
+116
-19
@@ -7,6 +7,7 @@ use super::cdp::chrome::{
|
||||
auto_connect_cdp, discover_cdp_url, launch_chrome, ChromeProcess, LaunchOptions,
|
||||
};
|
||||
use super::cdp::client::CdpClient;
|
||||
use super::cdp::lightpanda::{launch_lightpanda, LightpandaLaunchOptions, LightpandaProcess};
|
||||
use super::cdp::types::*;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -55,6 +56,34 @@ pub fn validate_launch_options(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_lightpanda_options(options: &LaunchOptions) -> Result<(), String> {
|
||||
if options
|
||||
.extensions
|
||||
.as_ref()
|
||||
.is_some_and(|exts| !exts.is_empty())
|
||||
{
|
||||
return Err("Extensions are not supported with Lightpanda".to_string());
|
||||
}
|
||||
if options.profile.is_some() {
|
||||
return Err("Profiles are not supported with Lightpanda".to_string());
|
||||
}
|
||||
if options.storage_state.is_some() {
|
||||
return Err("Storage state is not supported with Lightpanda".to_string());
|
||||
}
|
||||
if options.allow_file_access {
|
||||
return Err("File access is not supported with Lightpanda".to_string());
|
||||
}
|
||||
if !options.headless {
|
||||
return Err("Headed mode is not supported with Lightpanda (headless only)".to_string());
|
||||
}
|
||||
if !options.args.is_empty() {
|
||||
return Err(
|
||||
"Custom Chrome arguments (--args) are not supported with Lightpanda".to_string(),
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Converts common error messages into AI-friendly, actionable descriptions.
|
||||
pub fn to_ai_friendly_error(error: &str) -> String {
|
||||
let lower = error.to_lowercase();
|
||||
@@ -86,6 +115,7 @@ pub struct PageInfo {
|
||||
pub session_id: String,
|
||||
pub url: String,
|
||||
pub title: String,
|
||||
pub target_type: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
@@ -105,37 +135,72 @@ impl WaitUntil {
|
||||
}
|
||||
}
|
||||
|
||||
pub enum BrowserProcess {
|
||||
Chrome(ChromeProcess),
|
||||
Lightpanda(LightpandaProcess),
|
||||
}
|
||||
|
||||
pub struct BrowserManager {
|
||||
pub client: CdpClient,
|
||||
chrome_process: Option<ChromeProcess>,
|
||||
browser_process: Option<BrowserProcess>,
|
||||
pages: Vec<PageInfo>,
|
||||
active_page_index: usize,
|
||||
default_timeout_ms: u64,
|
||||
}
|
||||
|
||||
impl BrowserManager {
|
||||
pub async fn launch(options: LaunchOptions) -> Result<Self, String> {
|
||||
validate_launch_options(
|
||||
options.extensions.as_deref(),
|
||||
false,
|
||||
options.profile.as_deref(),
|
||||
options.storage_state.as_deref(),
|
||||
options.allow_file_access,
|
||||
options.executable_path.as_deref(),
|
||||
)?;
|
||||
pub async fn launch(options: LaunchOptions, engine: Option<&str>) -> Result<Self, String> {
|
||||
let engine = engine.unwrap_or("chrome");
|
||||
|
||||
match engine {
|
||||
"chrome" => validate_launch_options(
|
||||
options.extensions.as_deref(),
|
||||
false,
|
||||
options.profile.as_deref(),
|
||||
options.storage_state.as_deref(),
|
||||
options.allow_file_access,
|
||||
options.executable_path.as_deref(),
|
||||
)?,
|
||||
"lightpanda" => validate_lightpanda_options(&options)?,
|
||||
_ => {
|
||||
return Err(format!(
|
||||
"Unknown engine '{}'. Supported engines: chrome, lightpanda",
|
||||
engine
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
let ignore_https_errors = options.ignore_https_errors;
|
||||
let user_agent = options.user_agent.clone();
|
||||
let color_scheme = options.color_scheme.clone();
|
||||
let download_path = options.download_path.clone();
|
||||
|
||||
let chrome = launch_chrome(&options)?;
|
||||
let ws_url = chrome.ws_url.clone();
|
||||
let (ws_url, process) = match engine {
|
||||
"lightpanda" => {
|
||||
let lp_options = LightpandaLaunchOptions {
|
||||
executable_path: options.executable_path.clone(),
|
||||
proxy: options.proxy.clone(),
|
||||
port: None,
|
||||
};
|
||||
let process = tokio::task::spawn_blocking(move || launch_lightpanda(&lp_options))
|
||||
.await
|
||||
.map_err(|e| format!("Lightpanda launch task failed: {}", e))??;
|
||||
let ws_url = process.ws_url.clone();
|
||||
(ws_url, BrowserProcess::Lightpanda(process))
|
||||
}
|
||||
_ => {
|
||||
let process = tokio::task::spawn_blocking(move || launch_chrome(&options))
|
||||
.await
|
||||
.map_err(|e| format!("Chrome launch task failed: {}", e))??;
|
||||
let ws_url = process.ws_url.clone();
|
||||
(ws_url, BrowserProcess::Chrome(process))
|
||||
}
|
||||
};
|
||||
|
||||
let client = CdpClient::connect(&ws_url).await?;
|
||||
let mut manager = Self {
|
||||
client,
|
||||
chrome_process: Some(chrome),
|
||||
browser_process: Some(process),
|
||||
pages: Vec::new(),
|
||||
active_page_index: 0,
|
||||
default_timeout_ms: 25_000,
|
||||
@@ -197,7 +262,7 @@ impl BrowserManager {
|
||||
let client = CdpClient::connect(&ws_url).await?;
|
||||
let mut manager = Self {
|
||||
client,
|
||||
chrome_process: None,
|
||||
browser_process: None,
|
||||
pages: Vec::new(),
|
||||
active_page_index: 0,
|
||||
default_timeout_ms: 10_000,
|
||||
@@ -229,7 +294,9 @@ impl BrowserManager {
|
||||
let page_targets: Vec<TargetInfo> = result
|
||||
.target_infos
|
||||
.into_iter()
|
||||
.filter(|t| t.target_type == "page" && !t.url.is_empty())
|
||||
.filter(|t| {
|
||||
(t.target_type == "page" || t.target_type == "webview") && !t.url.is_empty()
|
||||
})
|
||||
.collect();
|
||||
|
||||
if page_targets.is_empty() {
|
||||
@@ -262,6 +329,7 @@ impl BrowserManager {
|
||||
session_id: attach_result.session_id.clone(),
|
||||
url: "about:blank".to_string(),
|
||||
title: String::new(),
|
||||
target_type: "page".to_string(),
|
||||
});
|
||||
self.active_page_index = 0;
|
||||
self.enable_domains(&attach_result.session_id).await?;
|
||||
@@ -284,6 +352,7 @@ impl BrowserManager {
|
||||
session_id: attach_result.session_id.clone(),
|
||||
url: target.url.clone(),
|
||||
title: target.title.clone(),
|
||||
target_type: target.target_type.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -507,10 +576,11 @@ impl BrowserManager {
|
||||
.send_command_no_params("Browser.close", None)
|
||||
.await;
|
||||
|
||||
if let Some(mut chrome) = self.chrome_process.take() {
|
||||
if let Some(process) = self.browser_process.take() {
|
||||
let timeout = std::time::Duration::from_secs(5);
|
||||
let _ = tokio::task::spawn_blocking(move || {
|
||||
chrome.wait_or_kill(timeout);
|
||||
let _ = tokio::task::spawn_blocking(move || match process {
|
||||
BrowserProcess::Chrome(mut chrome) => chrome.wait_or_kill(timeout),
|
||||
BrowserProcess::Lightpanda(mut lightpanda) => lightpanda.kill(),
|
||||
})
|
||||
.await;
|
||||
}
|
||||
@@ -541,7 +611,7 @@ impl BrowserManager {
|
||||
|
||||
/// Returns true if this manager was connected via CDP (as opposed to local launch).
|
||||
pub fn is_cdp_connection(&self) -> bool {
|
||||
self.chrome_process.is_none()
|
||||
self.browser_process.is_none()
|
||||
}
|
||||
|
||||
/// Ensures the browser has at least one page. If `pages` is empty, creates a new
|
||||
@@ -579,6 +649,7 @@ impl BrowserManager {
|
||||
session_id: attach_result.session_id.clone(),
|
||||
url: "about:blank".to_string(),
|
||||
title: String::new(),
|
||||
target_type: "page".to_string(),
|
||||
});
|
||||
self.active_page_index = 0;
|
||||
self.enable_domains(&attach_result.session_id).await?;
|
||||
@@ -611,6 +682,7 @@ impl BrowserManager {
|
||||
"index": i,
|
||||
"title": p.title,
|
||||
"url": p.url,
|
||||
"type": p.target_type,
|
||||
"active": i == self.active_page_index,
|
||||
})
|
||||
})
|
||||
@@ -651,6 +723,7 @@ impl BrowserManager {
|
||||
session_id: attach.session_id,
|
||||
url: target_url.to_string(),
|
||||
title: String::new(),
|
||||
target_type: "page".to_string(),
|
||||
});
|
||||
self.active_page_index = index;
|
||||
|
||||
@@ -1068,6 +1141,30 @@ mod tests {
|
||||
assert!(validate_launch_options(None, false, None, None, false, None,).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_lightpanda_options_rejects_extensions() {
|
||||
let opts = LaunchOptions {
|
||||
extensions: Some(vec!["/tmp/ext".to_string()]),
|
||||
..Default::default()
|
||||
};
|
||||
assert!(validate_lightpanda_options(&opts).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_lightpanda_options_rejects_headed() {
|
||||
let opts = LaunchOptions {
|
||||
headless: false,
|
||||
..Default::default()
|
||||
};
|
||||
assert!(validate_lightpanda_options(&opts).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_lightpanda_options_valid() {
|
||||
let opts = LaunchOptions::default();
|
||||
assert!(validate_lightpanda_options(&opts).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_to_ai_friendly_error_strict_mode() {
|
||||
assert_eq!(
|
||||
|
||||
Reference in New Issue
Block a user