feat(sync): 同步 upstream 改动并升级到 0.16.3-fork.5

This commit is contained in:
leeguooooo
2026-03-09 12:00:13 +09:00
parent 3cbc284076
commit 356e2f5f39
46 changed files with 3542 additions and 218 deletions
+116 -19
View File
@@ -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!(