修复和优化agent sdk预检和安装

This commit is contained in:
xggz
2026-03-07 20:26:43 +08:00
parent dacec69481
commit f3986e7efb
2 changed files with 161 additions and 14 deletions

View File

@@ -153,7 +153,10 @@ pub fn from_registry_id(id: &str) -> Option<AgentType> {
}
pub fn get_agent_meta(agent_type: AgentType) -> AcpAgentMeta {
debug_assert_eq!(from_registry_id(registry_id_for(agent_type)), Some(agent_type));
debug_assert_eq!(
from_registry_id(registry_id_for(agent_type)),
Some(agent_type)
);
match agent_type {
AgentType::Auggie => AcpAgentMeta {
agent_type,

View File

@@ -98,20 +98,146 @@ fn package_name_from_spec(package: &str) -> String {
normalized.to_string()
}
async fn detect_npx_cached_version(package: &str) -> Option<String> {
let output = crate::process::tokio_command("npx")
.arg("--yes")
.arg("--no-install")
.arg(package)
.arg("--version")
.output()
.await
.ok()?;
#[derive(Deserialize)]
#[serde(untagged)]
enum NpmPackageBin {
Single(String),
Multiple(BTreeMap<String, String>),
}
if !output.status.success() {
return None;
#[derive(Deserialize)]
struct NpmPackageManifest {
version: Option<String>,
bin: Option<NpmPackageBin>,
}
fn read_npx_cached_package_version(package_dir: &Path) -> Option<String> {
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<NpmPackageManifest> {
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<String> {
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<PathBuf> {
let package_parts = npx_package_parts(package);
if package_parts.is_empty() {
return vec![];
}
parse_version_output(&output).and_then(|value| normalize_version_candidate(&value))
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<String> {
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
}
async fn detect_uvx_cached_version(package: &str) -> Option<String> {
@@ -147,8 +273,12 @@ async fn detect_local_version(agent_type: AgentType) -> Option<String> {
async fn prepare_npx_package(package: &str) -> Result<(), AcpError> {
let output = crate::process::tokio_command("npx")
.arg("--yes")
.arg("--package")
.arg(package)
.arg("--version")
.arg("--")
.arg("node")
.arg("-e")
.arg("process.exit(0)")
.output()
.await
.map_err(|e| AcpError::protocol(format!("failed to run npx: {e}")))?;
@@ -163,6 +293,10 @@ async fn prepare_npx_package(package: &str) -> Result<(), AcpError> {
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(())
}
@@ -1137,6 +1271,8 @@ pub async fn acp_connect(
app_handle: tauri::AppHandle,
window: tauri::WebviewWindow,
) -> Result<String, AcpError> {
let meta = registry::get_agent_meta(agent_type);
let setting = agent_setting_service::get_by_agent_type(&db.conn, agent_type)
.await
.map_err(|e| AcpError::protocol(e.to_string()))?;
@@ -1153,6 +1289,14 @@ pub async fn acp_connect(
let runtime_env =
build_runtime_env_from_setting(agent_type, setting.as_ref(), local_config_json.as_deref());
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?;
}
}
manager
.spawn_agent(
agent_type,