From bcba0543094974a746a785745eb2ab287a68886f Mon Sep 17 00:00:00 2001 From: xintaofei Date: Tue, 17 Mar 2026 20:30:51 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=9E=84Agent=E7=AE=A1=E7=90=86/?= =?UTF-8?q?=E9=93=BE=E6=8E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src-tauri/src/acp/connection.rs | 6 +- src-tauri/src/acp/preflight.rs | 36 ++--- src-tauri/src/commands/acp.rs | 262 +++++--------------------------- 3 files changed, 58 insertions(+), 246 deletions(-) diff --git a/src-tauri/src/acp/connection.rs b/src-tauri/src/acp/connection.rs index ef63c20..b8c779c 100644 --- a/src-tauri/src/acp/connection.rs +++ b/src-tauri/src/acp/connection.rs @@ -123,7 +123,7 @@ async fn build_agent( match meta.distribution { AgentDistribution::Npx { - package, args, env, .. + cmd, args, env, .. } => { let merged_env = merge_agent_env(env, runtime_env); let mut parts: Vec = Vec::new(); @@ -131,12 +131,10 @@ async fn build_agent( parts.push(format!("{k}={v}")); } parts.push( - crate::process::normalized_program("npx") + crate::process::normalized_program(cmd) .to_string_lossy() .to_string(), ); - parts.push("-y".into()); - parts.push(package.into()); for a in args { parts.push((*a).into()); } diff --git a/src-tauri/src/acp/preflight.rs b/src-tauri/src/acp/preflight.rs index e67052f..cf34df7 100644 --- a/src-tauri/src/acp/preflight.rs +++ b/src-tauri/src/acp/preflight.rs @@ -5,10 +5,10 @@ use crate::acp::binary_cache; use crate::acp::registry::{self, AgentDistribution}; use crate::models::agent::AgentType; -/// Cache for NPX environment check results. +/// Cache for npm environment check results. /// Stores `Some(checks)` after a successful (all-pass) run; /// stays `None` if checks failed so they are retried next time. -static NPX_ENV_CACHE: Mutex>> = Mutex::new(None); +static NPM_ENV_CACHE: Mutex>> = Mutex::new(None); #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "snake_case")] @@ -52,7 +52,7 @@ pub async fn run_preflight(agent_type: AgentType) -> PreflightResult { let meta = registry::get_agent_meta(agent_type); debug_assert_eq!(meta.agent_type, agent_type); let checks = match &meta.distribution { - AgentDistribution::Npx { node_required, .. } => check_npx_environment(*node_required).await, + AgentDistribution::Npx { node_required, .. } => check_npm_environment(*node_required).await, AgentDistribution::Binary { version, cmd, @@ -73,11 +73,11 @@ pub async fn run_preflight(agent_type: AgentType) -> PreflightResult { } } -async fn check_npx_environment(node_required: Option<&str>) -> Vec { +async fn check_npm_environment(node_required: Option<&str>) -> Vec { // Return cached result if a previous check passed. - // The cache stores only the base checks (node_available + npx_available); + // The cache stores only the base checks (node_available + npm_available); // the per-agent node_version check is appended separately. - let cached = NPX_ENV_CACHE.lock().unwrap().clone(); + let cached = NPM_ENV_CACHE.lock().unwrap().clone(); if let Some(cached) = cached { let mut checks = cached; if let Some(required) = node_required { @@ -89,12 +89,12 @@ async fn check_npx_environment(node_required: Option<&str>) -> Vec { return checks; } - // Run node and npx checks in parallel - let (node_result, npx_result) = tokio::join!( + // Run node and npm checks in parallel + let (node_result, npm_result) = tokio::join!( crate::process::tokio_command("node") .arg("--version") .output(), - crate::process::tokio_command("npx") + crate::process::tokio_command("npm") .arg("--version") .output(), ); @@ -127,22 +127,22 @@ async fn check_npx_environment(node_required: Option<&str>) -> Vec { }, }; - let npx_check = match npx_result { + let npm_check = match npm_result { Ok(output) if output.status.success() => { let version = String::from_utf8_lossy(&output.stdout).trim().to_string(); CheckItem { - check_id: "npx_available".into(), - label: "npx".into(), + check_id: "npm_available".into(), + label: "npm".into(), status: CheckStatus::Pass, - message: format!("npx {version} available"), + message: format!("npm {version} available"), fixes: vec![], } } _ => CheckItem { - check_id: "npx_available".into(), - label: "npx".into(), + check_id: "npm_available".into(), + label: "npm".into(), status: CheckStatus::Fail, - message: "npx is not installed or not in PATH".into(), + message: "npm is not installed or not in PATH".into(), fixes: vec![FixAction { label: "Install Node.js".into(), kind: FixActionKind::OpenUrl, @@ -151,7 +151,7 @@ async fn check_npx_environment(node_required: Option<&str>) -> Vec { }, }; - let mut checks = vec![node_check, npx_check]; + let mut checks = vec![node_check, npm_check]; // Cache only if all checks passed — failed results are not cached so // the user can retry after installing the missing tools. @@ -159,7 +159,7 @@ async fn check_npx_environment(node_required: Option<&str>) -> Vec { .iter() .all(|c| !matches!(c.status, CheckStatus::Fail)); if all_passed { - *NPX_ENV_CACHE.lock().unwrap() = Some(checks.clone()); + *NPM_ENV_CACHE.lock().unwrap() = Some(checks.clone()); } // After caching the base checks, append the per-agent Node.js version diff --git a/src-tauri/src/commands/acp.rs b/src-tauri/src/commands/acp.rs index a515c57..17baaf8 100644 --- a/src-tauri/src/commands/acp.rs +++ b/src-tauri/src/commands/acp.rs @@ -78,153 +78,24 @@ fn package_name_from_spec(package: &str) -> String { normalized.to_string() } -#[derive(Deserialize)] -#[serde(untagged)] -enum NpmPackageBin { - Single(String), - Multiple(BTreeMap), -} - -#[derive(Deserialize)] -struct NpmPackageManifest { - version: Option, - bin: Option, -} - -fn read_npx_cached_package_version(package_dir: &Path) -> Option { - let manifest_path = package_dir.join("package.json"); - let content = std::fs::read_to_string(manifest_path).ok()?; - let manifest: NpmPackageManifest = serde_json::from_str(&content).ok()?; - manifest - .version - .as_deref() - .and_then(normalize_version_candidate) -} - -fn read_npx_cached_package_manifest(package_dir: &Path) -> Option { - let manifest_path = package_dir.join("package.json"); - let content = std::fs::read_to_string(manifest_path).ok()?; - serde_json::from_str(&content).ok() -} - -fn npx_package_parts(package: &str) -> Vec { - package_name_from_spec(package) - .split('/') - .filter(|part| !part.is_empty()) - .map(ToString::to_string) - .collect() -} - -fn npx_cached_package_dirs(cache_dir: &Path, package: &str) -> Vec { - let package_parts = npx_package_parts(package); - if package_parts.is_empty() { - return vec![]; +async fn detect_global_cmd_version(cmd: &str) -> Option { + let output = crate::process::tokio_command(cmd) + .arg("--version") + .output() + .await + .ok()?; + if !output.status.success() { + return None; } - - let npx_root = cache_dir.join("_npx"); - let Ok(entries) = std::fs::read_dir(&npx_root) else { - return vec![]; - }; - - let mut dirs = Vec::new(); - for entry in entries.flatten() { - let root = entry.path(); - if !root.is_dir() { - continue; - } - - let mut package_dir = root.join("node_modules"); - for part in &package_parts { - package_dir = package_dir.join(part); - } - if package_dir.is_dir() { - dirs.push(package_dir); - } - } - - dirs -} - -#[cfg(unix)] -fn ensure_executable(path: &Path) -> std::io::Result<()> { - use std::os::unix::fs::PermissionsExt; - - let metadata = std::fs::metadata(path)?; - let mut permissions = metadata.permissions(); - let current = permissions.mode(); - let next = current | 0o111; - if next != current { - permissions.set_mode(next); - std::fs::set_permissions(path, permissions)?; - } - Ok(()) -} - -#[cfg(not(unix))] -fn ensure_executable(_path: &Path) -> std::io::Result<()> { - Ok(()) -} - -async fn ensure_npx_cached_bins_executable(package: &str) -> Result<(), AcpError> { - let Some(cache_dir) = npm_cache_dir().await else { - return Ok(()); - }; - - for package_dir in npx_cached_package_dirs(&cache_dir, package) { - let Some(manifest) = read_npx_cached_package_manifest(&package_dir) else { - continue; - }; - - let mut bin_rel_paths = Vec::new(); - match manifest.bin { - Some(NpmPackageBin::Single(path)) => bin_rel_paths.push(path), - Some(NpmPackageBin::Multiple(map)) => { - bin_rel_paths.extend(map.into_values()); - } - None => {} - } - - for rel_path in bin_rel_paths { - let script_path = package_dir.join(rel_path); - if !script_path.is_file() { - continue; - } - if let Err(e) = ensure_executable(&script_path) { - return Err(AcpError::protocol(format!( - "failed to set executable permission for npx package script: {e}" - ))); - } - } - } - - Ok(()) -} - -async fn detect_npx_cached_version(package: &str) -> Option { - let cache_dir = npm_cache_dir().await?; - let expected = version_from_package_spec(package); - let mut detected = None; - - for package_dir in npx_cached_package_dirs(&cache_dir, package) { - let version = read_npx_cached_package_version(&package_dir).or_else(|| expected.clone()); - if let Some(found) = version { - if expected.as_deref() == Some(found.as_str()) { - return Some(found); - } - if detected.is_none() { - detected = Some(found); - } - } - } - - detected + let raw = String::from_utf8_lossy(&output.stdout).trim().to_string(); + normalize_version_candidate(&raw) } async fn detect_local_version(agent_type: AgentType) -> Option { let meta = registry::get_agent_meta(agent_type); match meta.distribution { - registry::AgentDistribution::Npx { package, .. } => { - detect_npx_cached_version(package).await + registry::AgentDistribution::Npx { cmd, .. } => { + detect_global_cmd_version(cmd).await } registry::AgentDistribution::Binary { cmd, .. } => { binary_cache::detect_installed_version(agent_type, cmd) @@ -234,104 +105,49 @@ async fn detect_local_version(agent_type: AgentType) -> Option { } } -async fn prepare_npx_package(package: &str) -> Result<(), AcpError> { - let output = crate::process::tokio_command("npx") - .arg("--yes") - .arg("--package") +async fn install_npm_global_package(package: &str) -> Result<(), AcpError> { + let output = crate::process::tokio_command("npm") + .arg("install") + .arg("-g") .arg(package) - .arg("--") - .arg("node") - .arg("-e") - .arg("process.exit(0)") .output() .await - .map_err(|e| AcpError::protocol(format!("failed to run npx: {e}")))?; + .map_err(|e| AcpError::protocol(format!("failed to run npm install -g: {e}")))?; if !output.status.success() { let err = String::from_utf8_lossy(&output.stderr).trim().to_string(); let msg = if err.is_empty() { - "failed to prepare npx package".to_string() + "failed to install npm package globally".to_string() } else { - format!("failed to prepare npx package: {err}") + format!("failed to install npm package globally: {err}") }; return Err(AcpError::protocol(msg)); } - // Some npm packages ship bin scripts without executable bit. - // Normalize permissions in local npx cache to avoid runtime spawn failures. - ensure_npx_cached_bins_executable(package).await?; - Ok(()) } -async fn npm_cache_dir() -> Option { - let output = crate::process::tokio_command("npm") - .arg("config") - .arg("get") - .arg("cache") - .output() - .await - .ok()?; - if !output.status.success() { - return None; - } - let raw = String::from_utf8_lossy(&output.stdout).trim().to_string(); - if raw.is_empty() || raw.eq_ignore_ascii_case("undefined") { - return None; - } - Some(PathBuf::from(raw)) -} - -fn remove_npx_package_cache(cache_dir: &Path, package_name: &str) -> Result<(), AcpError> { - let npx_root = cache_dir.join("_npx"); - if !npx_root.exists() { - return Ok(()); - } - - let package_parts = package_name - .split('/') - .filter(|part| !part.is_empty()) - .collect::>(); - if package_parts.is_empty() { - return Ok(()); - } - - let entries = std::fs::read_dir(&npx_root) - .map_err(|e| AcpError::protocol(format!("failed to read npx cache directory: {e}")))?; - for entry in entries.flatten() { - let root = entry.path(); - if !root.is_dir() { - continue; - } - let mut package_dir = root.join("node_modules"); - for part in &package_parts { - package_dir = package_dir.join(part); - } - if package_dir.exists() { - std::fs::remove_dir_all(&package_dir).map_err(|e| { - AcpError::protocol(format!("failed to remove npx package cache: {e}")) - })?; - } - } - - Ok(()) -} - -async fn uninstall_npx_package(package: &str) -> Result<(), AcpError> { +async fn uninstall_npm_global_package(package: &str) -> Result<(), AcpError> { let package_name = package_name_from_spec(package); if !package_name.is_empty() { - // Best effort: if package was installed globally, remove it as well. - let _ = crate::process::tokio_command("npm") + let output = crate::process::tokio_command("npm") .arg("uninstall") .arg("-g") .arg(&package_name) .output() - .await; - } + .await + .map_err(|e| AcpError::protocol(format!("failed to run npm uninstall -g: {e}")))?; - if let Some(cache_dir) = npm_cache_dir().await { - remove_npx_package_cache(&cache_dir, &package_name)?; + if !output.status.success() { + let err = String::from_utf8_lossy(&output.stderr).trim().to_string(); + let msg = if err.is_empty() { + "failed to uninstall npm package globally".to_string() + } else { + format!("failed to uninstall npm package globally: {err}") + }; + return Err(AcpError::protocol(msg)); + } } Ok(()) @@ -1214,11 +1030,9 @@ pub async fn acp_connect( runtime_env.insert("OPENCLAW_RESET_SESSION".into(), "1".into()); } - if let registry::AgentDistribution::Npx { package, .. } = meta.distribution { - if detect_npx_cached_version(package).await.is_none() { - prepare_npx_package(package).await?; - } else { - ensure_npx_cached_bins_executable(package).await?; + if let registry::AgentDistribution::Npx { cmd, package, .. } = meta.distribution { + if detect_global_cmd_version(cmd).await.is_none() { + install_npm_global_package(package).await?; } } @@ -1635,7 +1449,7 @@ pub async fn acp_prepare_npx_agent( .flatten() .and_then(|m| m.installed_version); - prepare_npx_package(package).await?; + install_npm_global_package(package).await?; let resolved = detect_local_version(agent_type) .await .or_else(|| version_from_package_spec(package)) @@ -1647,7 +1461,7 @@ pub async fn acp_prepare_npx_agent( .or(existing) .ok_or_else(|| { AcpError::protocol( - "npx install succeeded but failed to determine local version", + "npm global install succeeded but failed to determine local version", ) })?; @@ -1679,7 +1493,7 @@ pub async fn acp_uninstall_agent( binary_cache::clear_agent_cache(agent_type)?; } registry::AgentDistribution::Npx { package, .. } => { - uninstall_npx_package(package).await?; + uninstall_npm_global_package(package).await?; } }