//! Claude Code ACP Agent 启动器 (SACP 版本) //! //! 使用 symposium-acp (sacp) 库的新实现,支持: //! - 标准 tokio::spawn(无需 LocalSet) //! - Builder 模式的连接构建 //! - 回调函数式的消息处理 //! //! 此文件是 `claude_code.rs` 的 SACP 版本替代品。 use std::collections::HashMap; use std::path::PathBuf; use std::process::Stdio; use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; #[cfg(windows)] use crate::path_env::TAURI_APP_DATA_DIR; use agent_config::{AgentServersConfig, ContextServerConfig}; use anyhow::{Context, Result}; use process_wrap::tokio::CommandWrap; #[cfg(unix)] use process_wrap::tokio::ProcessGroup; #[cfg(windows)] use process_wrap::tokio::{CreationFlags, JobObject}; use shared_types::{ModelProviderConfig, ProjectAndAgentInfo, error_codes}; use tokio::sync::mpsc; use tokio_util::compat::{TokioAsyncReadCompatExt, TokioAsyncWriteCompatExt}; use tokio_util::sync::CancellationToken; use tracing::{debug, error, info, warn}; use agent_client_protocol::schema::{ CancelNotification, InitializeRequest, LoadSessionRequest, McpServer, McpServerStdio, NewSessionRequest, PermissionOptionKind, PromptRequest, ProtocolVersion, RequestPermissionOutcome, RequestPermissionRequest, RequestPermissionResponse, SelectedPermissionOutcome, SessionId, SessionNotification, }; use agent_client_protocol::{ Agent, Client, ConnectionTo, Dispatch, Handled, JsonRpcMessage, Responder, }; use crate::acp::CancelNotificationRequestWrapper; use crate::traits::AgentStartConfig; use crate::traits::session_notifier::SessionNotifier; use crate::traits::session_registry::SessionRegistry; // 导入生命周期管理 use super::lifecycle::AgentLifecycleGuard; #[cfg(windows)] use super::windows_launch::{ CREATE_NO_WINDOW_FLAG, normalize_windows_command_for_no_window, resolve_windows_node_cli_command, }; #[cfg(windows)] use windows::Win32::System::Threading::PROCESS_CREATION_FLAGS; /// 使用最新协议版本 const VERSION: ProtocolVersion = ProtocolVersion::LATEST; /// API 密钥占位符(实际密钥由 Pingora 代理注入) const API_KEY_PLACEHOLDER: &str = "PROXY_MANAGED_KEY"; /// 环境变量键名常量 const ENV_ANTHROPIC_API_KEY: &str = "ANTHROPIC_API_KEY"; const ENV_ANTHROPIC_BASE_URL: &str = "ANTHROPIC_BASE_URL"; const ENV_ANTHROPIC_MODEL: &str = "ANTHROPIC_MODEL"; const ENV_DISABLE_NONESSENTIAL: &str = "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC"; const ENV_AGENT_SDK_SKIP_VERSION_CHECK: &str = "CLAUDE_AGENT_SDK_SKIP_VERSION_CHECK"; const ENV_RUST_LOG: &str = "RUST_LOG"; const ENV_AGENT_WORKING_DIR: &str = "AGENT_WORKING_DIR"; const ENV_AGENT_PROJECT_ID: &str = "AGENT_PROJECT_ID"; /// OpenAI 环境变量常量 const ENV_OPENAI_API_KEY: &str = "OPENAI_API_KEY"; const ENV_OPENAI_BASE_URL: &str = "OPENAI_BASE_URL"; /// nuwaxcode 使用 OPENCODE_MODEL 而不是 OPENAI_MODEL const ENV_OPENCODE_MODEL: &str = "OPENCODE_MODEL"; /// 默认代理 Base URL(包含 UUID 占位符) const DEFAULT_PROXY_BASE_URL: &str = "http://localhost:8088/api/{SERVICE_UUID}"; /// 获取各平台下常用的用户二进制目录 /// /// Linux/Mac: /// - ~/.cargo/bin (Rust cargo) /// - ~/.npm-global/bin (npm global) /// - ~/.local/bin (uv, pipx) /// - /opt/homebrew/bin (Homebrew on Apple Silicon) /// - /usr/local/bin (Homebrew on Intel Mac / 通用) /// - /home/linuxbrew/.linuxbrew/bin (Linuxbrew) /// /// Windows: /// - 返回空,Windows 环境变量已包含这些路径 /// /// 注意:使用 $HOME 变量而非硬编码 /root,HOME 不存在时自动回退到 /root fn get_common_user_bins() -> Vec<&'static str> { #[cfg(windows)] { vec![] // Windows 用户路径已在系统 PATH 中 } #[cfg(not(windows))] { vec![ // Cargo (Rust) "$HOME/.cargo/bin", // NPM global "$HOME/.npm-global/bin", // UV / PIPX global "$HOME/.local/bin", // Homebrew (Apple Silicon) "/opt/homebrew/bin", // Homebrew (Intel Mac) / 通用本地安装 "/usr/local/bin", // Linuxbrew "/home/linuxbrew/.linuxbrew/bin", // 系统级 cargo(某些容器环境) "/opt/cargo/bin", ] } } /// 确保子进程环境变量中包含 PATH / PATHEXT,便于解析可执行路径与 Windows .cmd 脚本 /// /// PATH 组成(优先级从高到低): /// 1. NUWAX_APP_RUNTIME_PATH — 应用自有运行时目录(node/uv/mcp-proxy 等) /// 2. 当前系统 PATH — 保留所有现有路径(关键改进!) /// 3. 常用用户目录 — cargo, npm, uv, homebrew 等 /// 4. 系统基础目录 — `/bin`, `/usr/bin` 等(仅在 PATH 为空时) fn ensure_subprocess_path_env(merged_envs: &mut std::collections::HashMap) { if !merged_envs.contains_key("PATH") { let path = build_mcp_server_path_env(); if !path.is_empty() { merged_envs.insert("PATH".to_string(), path); debug!("[SACP] 📋 already built PATH (using existing PATH directory)"); } } #[cfg(windows)] ensure_windows_subprocess_env(merged_envs); } /// 构建 MCP 服务器子进程所需的 PATH 环境变量 /// /// Claude Code SDK 在启动 MCP 服务器子进程时会用提供的 env 替换整个环境。 /// 如果 env 中缺少 PATH,mcp-proxy convert --config 模式下的孙进程 /// (如 uvx、npx)将因为找不到命令而静默失败。 /// /// 此函数与 ensure_subprocess_path_env 使用相同的逻辑构建 PATH: /// 1. NUWAX_APP_RUNTIME_PATH — 应用自有运行时目录(优先) /// 2. 当前系统 PATH — 保留所有现有路径(关键改进!) /// 3. 平台特定目录: /// - Windows: APPDATA 推导的 Tauri 应用 node/npm 路径 /// - Unix: 常用用户目录(cargo, npm, uv, homebrew 等) /// 4. 系统基础目录 — 仅在 PATH 为空时兜底 fn build_mcp_server_path_env() -> String { let sep = if cfg!(windows) { ";" } else { ":" }; let mut paths: Vec = Vec::new(); // 1. 优先:NUWAX_APP_RUNTIME_PATH if let Ok(runtime_path) = std::env::var("NUWAX_APP_RUNTIME_PATH") { for p in runtime_path.split(sep) { let p = p.trim(); if !p.is_empty() && !paths.contains(&p.to_string()) { paths.push(p.to_string()); } } } // 2. 追加:当前系统 PATH(关键改动!) if let Ok(current_path) = std::env::var("PATH") { for p in current_path.split(sep) { let p = p.trim(); if !p.is_empty() && !paths.contains(&p.to_string()) { paths.push(p.to_string()); } } } // 3. 追加:平台特定目录 #[cfg(windows)] { // Windows: 从 APPDATA 推导 Tauri 应用的 node/npm 路径 if let Ok(appdata) = std::env::var("APPDATA") { use std::path::PathBuf; let app_base = PathBuf::from(&appdata).join(TAURI_APP_DATA_DIR); let node_bin = app_base.join("runtime").join("node").join("bin"); if node_bin.is_dir() { let node_bin_str = node_bin.to_string_lossy().to_string(); if !paths.contains(&node_bin_str) { paths.push(node_bin_str); } } let npm_bin = app_base.join("node_modules").join(".bin"); if npm_bin.is_dir() { let npm_bin_str = npm_bin.to_string_lossy().to_string(); if !paths.contains(&npm_bin_str) { paths.push(npm_bin_str); } } } } #[cfg(not(windows))] { // Unix: 追加常用用户目录(自动扩展 $HOME) let home = std::env::var("HOME").unwrap_or_else(|_| "/root".to_string()); for bin in get_common_user_bins() { let expanded = bin.replace("$HOME", &home); let path = std::path::Path::new(&expanded); if path.is_dir() && !paths.contains(&expanded) { paths.push(expanded); } } } // 4. 兜底:系统基础目录(仅在 PATH 为空时) // 正常情况下不会走到这里,因为步骤 2 已经追加了当前系统 PATH // 这是为了防御性编程,确保即使在极端情况下也有可用的基础命令 if paths.is_empty() { #[cfg(not(windows))] for sys_dir in &["/bin", "/usr/bin", "/usr/local/bin", "/sbin", "/usr/sbin"] { let s = sys_dir.to_string(); if std::path::Path::new(sys_dir).is_dir() { paths.push(s); } } #[cfg(windows)] { // Windows: 直接返回系统 PATH 作为最后手段 if let Ok(current_path) = std::env::var("PATH") { return current_path; } } } paths.join(sep) } /// Windows 专属:确保 PATHEXT 存在,以便解析 .cmd/.bat 等脚本 #[cfg(windows)] fn ensure_windows_subprocess_env(merged_envs: &mut std::collections::HashMap) { if !merged_envs.contains_key("PATHEXT") { if let Ok(pathext) = std::env::var("PATHEXT") { merged_envs.insert("PATHEXT".to_string(), pathext); debug!("[SACP] 📋 PATHEXT already set"); } } } /// 根据 proxy feature 决定使用占位符还是真实 API Key fn resolve_api_key(provider: &ModelProviderConfig) -> String { if cfg!(feature = "proxy") { API_KEY_PLACEHOLDER.to_string() } else { provider.api_key.clone() } } /// 根据 proxy feature 决定使用代理 URL 还是真实 Base URL fn resolve_base_url(provider: &ModelProviderConfig) -> String { if cfg!(feature = "proxy") { DEFAULT_PROXY_BASE_URL.to_string() } else { provider.base_url.clone() } } /// Agent 配置参数 (与旧版兼容) #[derive(Debug, Clone)] pub struct SacpAgentLaunchConfig { /// 命令路径 pub command: String, /// 命令参数 pub args: Vec, /// 环境变量 pub env: HashMap, /// Context 服务器配置 (MCP servers) pub context_servers: HashMap, } /// Agent 连接信息(SACP 版本) pub struct SacpLauncherConnectionInfo { /// 会话 ID pub session_id: SessionId, /// 发送 Prompt 消息的通道(有界通道,提供背压保护) pub prompt_tx: mpsc::Sender, /// 发送取消请求的通道(有界通道,提供背压保护) pub cancel_tx: mpsc::Sender, /// 生命周期守卫(自动清理资源) pub lifecycle_guard: Arc, } /// 从配置文件加载 Agent 配置 /// /// 优先加载嵌入的JSON配置文件,如果加载失败则使用默认配置 /// 同时检查并自动安装 agent(如果需要) pub async fn load_sacp_agent_config( model_provider: Option<&ModelProviderConfig>, service_type: &shared_types::ServiceType, ) -> Result { // 复用旧版配置加载逻辑 let config = AgentServersConfig::load_or_default_for_service(service_type).await; if let Some(agent_config) = config.get_agent("claude-code-acp-ts") { debug!( "📋 [SACP] using default Agent config: {}", agent_config.agent_id ); // 检查并安装 agent - 临时禁用以测试本地 claude-code-acp-ts // if agent_config.installation.package_name.is_some() { // let installation_manager = AgentInstallationManager::new(); // match installation_manager // .ensure_installed(&agent_config.installation, &agent_config.command) // .await // { // Ok(result) => { // if result.already_installed { // debug!("[SACP] Agent already installed: {}", agent_config.command); // } else { // info!("[SACP] Agent message succeeded: {}", result.message); // } // } // Err(e) => { // warn!( // "[SACP] Agent message Installation failed: {}, message started", // e // ); // } // } // } // 解析环境变量 let mut resolved_env = agent_config.env.clone(); if let Some(provider) = model_provider { // 统一替换所有环境变量中的模板 // proxy 模式下:API_KEY 和 BASE_URL 使用占位符/代理URL,由 Pingora 代理注入真实值 // 非 proxy 模式下:直接使用真实的 API Key 和 Base URL let resolved_key = resolve_api_key(provider); let resolved_url = resolve_base_url(provider); for (_key, value) in resolved_env.iter_mut() { *value = value .replace("{MODEL_PROVIDER_API_KEY}", &resolved_key) .replace("{MODEL_PROVIDER_BASE_URL}", &resolved_url) .replace("{MODEL_PROVIDER_DEFAULT_MODEL}", &provider.default_model) .replace("{MODEL_PROVIDER_NAME}", &provider.name); } } // 禁用 Claude Code 非必要网络请求 resolved_env.insert(ENV_DISABLE_NONESSENTIAL.to_string(), "1".to_string()); // 跳过 Agent SDK 版本检查 resolved_env.insert( ENV_AGENT_SDK_SKIP_VERSION_CHECK.to_string(), "1".to_string(), ); // debug: 打印最终环境变量(API Key 已脱敏) let mask_key = |v: &String| -> String { if v.len() > 8 { format!("{}***{}", &v[..4], &v[v.len() - 4..]) } else { "***".to_string() } }; debug!( "[SACP] Final env config: command={}, ANTHROPIC_API_KEY={}, ANTHROPIC_BASE_URL={}, ANTHROPIC_MODEL={}, \ OPENAI_API_KEY={}, OPENAI_BASE_URL={}, OPENCODE_MODEL={}, \ RUST_LOG={}, CLAUDE_CODE_MAX_TOKENS={}, CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC={}", agent_config.command, resolved_env .get("ANTHROPIC_API_KEY") .map(|v| mask_key(v)) .unwrap_or_default(), resolved_env .get("ANTHROPIC_BASE_URL") .unwrap_or(&"".to_string()), resolved_env .get("ANTHROPIC_MODEL") .unwrap_or(&"".to_string()), resolved_env .get("OPENAI_API_KEY") .map(|v| mask_key(v)) .unwrap_or_default(), resolved_env .get("OPENAI_BASE_URL") .unwrap_or(&"".to_string()), resolved_env .get("OPENCODE_MODEL") .unwrap_or(&"".to_string()), resolved_env .get("RUST_LOG") .unwrap_or(&"".to_string()), resolved_env .get("CLAUDE_CODE_MAX_TOKENS") .unwrap_or(&"".to_string()), resolved_env .get("CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC") .unwrap_or(&"".to_string()), ); Ok(SacpAgentLaunchConfig { command: agent_config.command.clone(), args: agent_config.args.clone(), env: resolved_env, context_servers: config.context_servers.clone(), }) } else { warn!("[SACP] config not found for claude-code-acp-ts, using default config"); get_default_sacp_agent_config(model_provider, service_type) } } /// 获取默认的 Agent 配置(后备方案) pub fn get_default_sacp_agent_config( model_provider: Option<&ModelProviderConfig>, _service_type: &shared_types::ServiceType, ) -> Result { let mut env = HashMap::new(); if let Some(provider) = model_provider { let resolved_key = resolve_api_key(provider); let resolved_url = resolve_base_url(provider); // Anthropic 环境变量 if !provider.api_key.is_empty() { env.insert(ENV_ANTHROPIC_API_KEY.to_string(), resolved_key.clone()); } if !provider.base_url.is_empty() { env.insert(ENV_ANTHROPIC_BASE_URL.to_string(), resolved_url.clone()); } if !provider.default_model.is_empty() { env.insert( ENV_ANTHROPIC_MODEL.to_string(), provider.default_model.clone(), ); } // OpenAI 环境变量 (支持 OpenAI 兼容的 Agent) if !provider.api_key.is_empty() { env.insert(ENV_OPENAI_API_KEY.to_string(), resolved_key); } if !provider.base_url.is_empty() { env.insert(ENV_OPENAI_BASE_URL.to_string(), resolved_url); } if !provider.default_model.is_empty() { // nuwaxcode 使用 OPENCODE_MODEL,model_name 中已包含 openai-compatible/ 前缀 env.insert( ENV_OPENCODE_MODEL.to_string(), provider.default_model.clone(), ); } } env.insert(ENV_RUST_LOG.to_string(), "info".to_string()); env.insert(ENV_DISABLE_NONESSENTIAL.to_string(), "1".to_string()); env.insert( ENV_AGENT_SDK_SKIP_VERSION_CHECK.to_string(), "1".to_string(), ); // Resolve the claude-code-acp-ts command path. // Priority: CLAUDE_CODE_ACP_PATH env var > `which` crate lookup > bare command name. // Tauri apps may not inherit the user's shell PATH, so we try `which` crate to get // an absolute path at build/launch time. let command = if let Ok(path) = std::env::var("CLAUDE_CODE_ACP_PATH") { path } else { match which::which("claude-code-acp-ts") { Ok(resolved_path) => { tracing::info!( "Resolved claude-code-acp-ts path via `which` crate: {}", resolved_path.display() ); resolved_path.to_string_lossy().to_string() } Err(_) => "claude-code-acp-ts".to_string(), } }; Ok(SacpAgentLaunchConfig { command, args: Vec::new(), env, context_servers: HashMap::new(), }) } /// 从文件内容中解析 D-Bus 会话地址 fn parse_dbus_address_from_content(content: &str) -> Option { content .lines() .find(|line| line.starts_with("DBUS_SESSION_BUS_ADDRESS=")) .and_then(|line| line.split_once('=')) .map(|(_, val)| { val.trim() .trim_end_matches(';') .trim_matches('\'') .trim_matches('"') .to_string() }) } /// 获取 D-Bus 会话地址 /// 优先从环境变量读取,否则从文件读取 fn get_dbus_session_address() -> Option { std::env::var("DBUS_SESSION_BUS_ADDRESS").ok().or_else(|| { std::fs::read_to_string("/tmp/dbus-session-env") .ok() .and_then(|content| parse_dbus_address_from_content(&content)) }) } /// mcp-proxy 日志目录环境变量名 const ENV_MCP_PROXY_LOG_DIR: &str = "MCP_PROXY_LOG_DIR"; /// 检测命令是否为 mcp-proxy(简化版,只检测命令名) fn is_mcp_proxy_command(command: &str) -> bool { command == "mcp-proxy" } /// 检测参数中是否有 convert 子命令 fn has_convert_subcommand(args: &[String]) -> bool { args.iter().any(|arg| arg == "convert") } /// 检测当前日志级别是否为 debug fn is_debug_log_level() -> bool { // 优先检查 RUST_LOG 环境变量 if let Ok(rust_log) = std::env::var("RUST_LOG") { let log_lower = rust_log.to_lowercase(); return log_lower.contains("debug") || log_lower.contains("trace"); } // 使用 tracing 的 enabled! 宏检测 tracing::enabled!(tracing::Level::DEBUG) } /// 获取 mcp-proxy 日志目录(如果配置了的话) fn get_mcp_proxy_log_dir() -> Option { std::env::var(ENV_MCP_PROXY_LOG_DIR).ok() } /// 检查参数中是否已有 --log-dir 或 --log-file 参数 fn has_log_dir_arg(args: &[String]) -> bool { args.iter().any(|arg| { arg == "--log-dir" || arg.starts_with("--log-dir=") || arg == "--log-file" || arg.starts_with("--log-file=") }) } /// 为 mcp-proxy convert 命令追加诊断参数 /// /// 当检测到以下条件时,自动追加 `--diagnostic` 参数: /// 1. 命令是 `mcp-proxy` /// 2. 参数包含 `convert` 子命令 /// 3. 当前日志级别是 debug /// /// 只有配置了 `MCP_PROXY_LOG_DIR` 环境变量时才追加 `--log-dir` 参数 /// /// 重复检查逻辑: /// 1. 如果参数中已有 --diagnostic,跳过注入 /// 2. 如果参数中已有 --log-dir 或 --log-file,不追加 --log-dir(避免覆盖用户配置) fn enhance_mcp_proxy_args(command: &str, args: Vec) -> Vec { // 检查是否为 mcp-proxy convert 命令 if !is_mcp_proxy_command(command) || !has_convert_subcommand(&args) { return args; } // 检查日志级别是否为 debug if !is_debug_log_level() { debug!("[MCP] mcp-proxy convert detected, but not debug, skip diagnostic params"); return args; } // 检查是否已有 --diagnostic 参数 let has_diagnostic = args.iter().any(|arg| arg == "--diagnostic"); if has_diagnostic { debug!("[MCP] mcp-proxy already has --diagnostic params, skipping"); return args; } let mut enhanced_args = args; // 追加 --diagnostic 参数 enhanced_args.push("--diagnostic".to_string()); info!("[MCP] added --diagnostic params to mcp-proxy"); // 🔒 关键检查:如果用户已配置 --log-dir 或 --log-file,不覆盖 if has_log_dir_arg(&enhanced_args) { debug!("[MCP] already has config params, skip --log-dir"); return enhanced_args; } // 只有配置了 MCP_PROXY_LOG_DIR 环境变量时才追加 --log-dir 参数 if let Some(log_dir) = get_mcp_proxy_log_dir() { enhanced_args.push("--log-dir".to_string()); enhanced_args.push(log_dir.clone()); info!( "[MCP] adding mcp-proxy convert --log-dir {} params", log_dir ); } enhanced_args } /// 将配置中的 Context 服务器转换为 SACP 协议的 McpServer pub fn convert_context_servers_sacp( configs: &HashMap, ) -> Vec { let dbus_address = get_dbus_session_address(); configs .iter() .filter(|(_, c)| c.enabled) .filter_map(|(name, c)| { let command = c.command.as_ref()?; let mut server = McpServerStdio::new(name, PathBuf::from(command)); // 处理参数,可能需要为 mcp-proxy convert 追加诊断参数 let final_args = if let Some(args) = &c.args { enhance_mcp_proxy_args(command, args.clone()) } else { Vec::new() }; if !final_args.is_empty() { server = server.args(final_args); } let mut env_vars: Vec = if let Some(env) = &c.env { env.iter() .map(|(k, v)| { agent_client_protocol::schema::EnvVariable::new(k.clone(), v.clone()) }) .collect() } else { Vec::new() }; // 注入 D-Bus 会话地址 if let Some(ref addr) = dbus_address { if !env_vars .iter() .any(|e| e.name == "DBUS_SESSION_BUS_ADDRESS") { env_vars.push(agent_client_protocol::schema::EnvVariable::new( "DBUS_SESSION_BUS_ADDRESS".to_string(), addr.clone(), )); } } // 注入镜像源环境变量(npx/bunx/uvx 子进程使用) for (key, val) in crate::mirror_env::collect_mirror_env_vars() { if !env_vars.iter().any(|e| e.name == key) { env_vars.push(agent_client_protocol::schema::EnvVariable::new(key, val)); } } // 注入 PATH 环境变量(关键!) // Claude Code SDK 用 MCP server 的 env 替换整个子进程环境。 // 如果 env 中没有 PATH,mcp-proxy convert --config 模式下 // 无法找到 uvx/npx 等命令来启动 MCP 子服务。 if !env_vars.iter().any(|e| e.name == "PATH") { let path_value = build_mcp_server_path_env(); if !path_value.is_empty() { env_vars.push(agent_client_protocol::schema::EnvVariable::new( "PATH".to_string(), path_value, )); } } // 注入 HOME 环境变量(uvx/npx 等工具需要 HOME 来定位缓存目录) #[cfg(not(windows))] if !env_vars.iter().any(|e| e.name == "HOME") { if let Ok(home) = std::env::var("HOME") { env_vars.push(agent_client_protocol::schema::EnvVariable::new( "HOME".to_string(), home, )); } } // Windows: 注入 USERPROFILE 和 PATHEXT #[cfg(windows)] { if !env_vars.iter().any(|e| e.name == "USERPROFILE") { if let Ok(profile) = std::env::var("USERPROFILE") { env_vars.push(agent_client_protocol::schema::EnvVariable::new( "USERPROFILE".to_string(), profile, )); } } if !env_vars.iter().any(|e| e.name == "PATHEXT") { if let Ok(pathext) = std::env::var("PATHEXT") { env_vars.push(agent_client_protocol::schema::EnvVariable::new( "PATHEXT".to_string(), pathext, )); } } } if !env_vars.is_empty() { server = server.env(env_vars); } Some(McpServer::Stdio(server)) }) .collect() } /// Claude Code ACP Agent 启动器 (SACP 版本) /// /// 使用 SACP 库的 Builder 模式和回调函数,无需 LocalSet。 pub struct SacpClaudeCodeLauncher { /// 会话通知器 notifier: Arc, } impl SacpClaudeCodeLauncher { /// 创建新的启动器 pub fn new(notifier: Arc) -> Self { Self { notifier } } /// 启动 Claude Code ACP Agent 服务 /// /// 使用 SACP 库的 Builder 模式,支持标准 tokio::spawn pub async fn launch( &self, project_id: String, project_path: PathBuf, model_provider: Option, start_config: AgentStartConfig, _registry: Arc, service_uuid: Option, ) -> Result where R::Entry: Into + From, { // 从配置加载默认 Agent 参数 let default_agent_config = load_sacp_agent_config(model_provider.as_ref(), &start_config.service_type).await?; // 🎯 关键:检查是否有自定义 agent_server 配置覆盖 let (command_path, command_args, base_env) = if let Some(ref agent_server_override) = start_config.agent_server_override { // 使用自定义 command(如果提供),否则用默认 let cmd = agent_server_override .command .clone() .unwrap_or_else(|| default_agent_config.command.clone()); // 使用自定义 args(如果提供),否则用默认 let args = agent_server_override .args .clone() .unwrap_or_else(|| default_agent_config.args.clone()); // 合并环境变量:默认配置 + 自定义配置(自定义覆盖默认) let mut env = default_agent_config.env.clone(); if let Some(custom_env) = &agent_server_override.env { // 使用 extend 替代循环,更高效 env.extend(custom_env.iter().map(|(k, v)| (k.clone(), v.clone()))); } // 🔧 关键修复:替换自定义环境变量中的模板变量 // 用户可能传入 {MODEL_PROVIDER_API_KEY} 等模板,需要替换为实际值 // proxy 模式下:API_KEY 和 BASE_URL 使用占位符/代理URL,由 Pingora 代理注入真实值 // 非 proxy 模式下:直接使用真实的 API Key 和 Base URL if let Some(ref provider) = model_provider { let resolved_key = resolve_api_key(provider); let resolved_url = resolve_base_url(provider); for (_key, value) in env.iter_mut() { *value = value .replace("{MODEL_PROVIDER_API_KEY}", &resolved_key) .replace("{MODEL_PROVIDER_BASE_URL}", &resolved_url) .replace("{MODEL_PROVIDER_DEFAULT_MODEL}", &provider.default_model) .replace("{MODEL_PROVIDER_NAME}", &provider.name); } debug!( "🔧 [SACP] Replaced custom env var template, model={}", provider.default_model ); } info!( "🎯 [SACP] Using custom Agent: agent_id={}, command={} {:?}", agent_server_override.get_agent_id(), cmd, args ); (cmd, args, env) } else { // 使用默认配置 info!( "📋 [SACP] Using default Agent: {} {:?}", default_agent_config.command, default_agent_config.args ); ( default_agent_config.command.clone(), default_agent_config.args.clone(), default_agent_config.env.clone(), ) }; // 创建通道(使用有界通道防止 OOM) // 容量由常量定义,足够处理突发请求,同时提供背压保护 let (cancel_tx, cancel_rx) = mpsc::channel::( shared_types::AGENT_CANCEL_CHANNEL_CAPACITY, ); let (prompt_tx, prompt_rx) = mpsc::channel::(shared_types::AGENT_PROMPT_CHANNEL_CAPACITY); let (session_id_tx, session_id_rx) = tokio::sync::oneshot::channel::(); // 创建 CancellationToken let cancel_token = CancellationToken::new(); info!( "[SACP] projectworkdirectory: {}", &project_path.to_string_lossy() ); // 准备 MCP 服务器 let mcp_servers = if start_config.has_mcp_servers() { info!("[SACP] using AgentStartConfig MCP servers"); start_config.mcp_servers.clone() } else if !default_agent_config.context_servers.is_empty() { info!("[SACP] using config file MCP servers"); convert_context_servers_sacp(&default_agent_config.context_servers) } else { info!("📝 [SACP] no config MCP servers"); Vec::new() }; let mut command_path = command_path; let mut command_args = command_args; #[cfg(windows)] if let Some((resolved_program, resolved_args)) = resolve_windows_node_cli_command(&command_path, &command_args) { let entry = resolved_args.first().cloned().unwrap_or_default(); info!( "[SACP] Windows direct node startup: {} -> {} {}", command_path, resolved_program, entry ); command_path = resolved_program; command_args = resolved_args; } // 准备环境变量(在 base_env 基础上添加项目相关变量) let mut merged_envs = base_env; merged_envs.insert( ENV_AGENT_WORKING_DIR.to_string(), project_path.to_string_lossy().to_string(), ); merged_envs.insert(ENV_AGENT_PROJECT_ID.to_string(), project_id.clone()); ensure_subprocess_path_env(&mut merged_envs); // 替换 UUID 占位符 if let Some(ref uuid) = service_uuid { for (_key, value) in merged_envs.iter_mut() { *value = value.replace("{SERVICE_UUID}", uuid); } } // 🔒 安全防护:proxy 模式下强制将敏感环境变量替换为占位符/代理 URL,防止密钥泄露 // 即使用户在配置中直接写了真实的 API_KEY 或 BASE_URL,也会被替换 if cfg!(feature = "proxy") { if model_provider.is_some() { // 强制替换 Anthropic 敏感变量 if merged_envs.contains_key(ENV_ANTHROPIC_API_KEY) { merged_envs.insert( ENV_ANTHROPIC_API_KEY.to_string(), API_KEY_PLACEHOLDER.to_string(), ); } // ANTHROPIC_AUTH_TOKEN 也需要替换(某些场景下可能存在) if merged_envs.contains_key("ANTHROPIC_AUTH_TOKEN") { merged_envs.insert( "ANTHROPIC_AUTH_TOKEN".to_string(), API_KEY_PLACEHOLDER.to_string(), ); } if merged_envs.contains_key(ENV_ANTHROPIC_BASE_URL) { merged_envs.insert( ENV_ANTHROPIC_BASE_URL.to_string(), service_uuid .as_ref() .map(|uuid| DEFAULT_PROXY_BASE_URL.replace("{SERVICE_UUID}", uuid)) .unwrap_or_else(|| DEFAULT_PROXY_BASE_URL.to_string()), ); } // 强制替换 OpenAI 敏感变量 if merged_envs.contains_key(ENV_OPENAI_API_KEY) { merged_envs.insert( ENV_OPENAI_API_KEY.to_string(), API_KEY_PLACEHOLDER.to_string(), ); } if merged_envs.contains_key(ENV_OPENAI_BASE_URL) { merged_envs.insert( ENV_OPENAI_BASE_URL.to_string(), service_uuid .as_ref() .map(|uuid| DEFAULT_PROXY_BASE_URL.replace("{SERVICE_UUID}", uuid)) .unwrap_or_else(|| DEFAULT_PROXY_BASE_URL.to_string()), ); } debug!("[SACP] 🔒 force replaced with proxy URL"); } } else { debug!("[SACP] 🔓 not proxy mode, keeping API Key and Base URL"); } // 🔍 打印传递给 Agent 的完整环境变量(用于调试) // 注意:敏感字段(API Key)需要脱敏处理,防止日志泄露 debug!( "[SACP] 📋 Start Agent command: {} {:?}", command_path, command_args ); debug!("[SACP] 📋 work directory: {:?}", project_path); debug!( "[SACP] 📋 Environment variables passed to Agent ({} items):", merged_envs.len() ); // 需要脱敏的环境变量 key 列表(即使在 debug 日志中也不暴露完整值) const SENSITIVE_ENV_KEYS: &[&str] = &[ ENV_ANTHROPIC_API_KEY, ENV_OPENAI_API_KEY, "ANTHROPIC_AUTH_TOKEN", ]; // 按字母顺序排序并打印所有环境变量(仅在 debug 级别) let mut env_keys: Vec<_> = merged_envs.keys().collect(); env_keys.sort(); for key in env_keys.iter() { let value = merged_envs.get(*key).unwrap(); if SENSITIVE_ENV_KEYS.contains(&key.as_str()) { // 脱敏:只显示前4个字符 + *** let masked = if value.len() > 4 { format!("{}***", &value[..4]) } else { "***".to_string() }; debug!("[SACP] 📋 {} = {}", key, masked); } else { debug!("[SACP] 📋 {} = {}", key, value); } } // 🔧 Windows:将 .cmd/.bat 等规范化为不弹窗的 node.exe + JS 形式(逻辑在 windows_launch 中) #[cfg(windows)] let (command_path, command_args) = normalize_windows_command_for_no_window(command_path, command_args); // 启动子进程(使用进程组/Job Object 来管理整个进程树) // Unix: ProcessGroup::leader() 创建进程组,确保能够清理所有孙进程 // Windows: JobObject 管理进程树 let mut cmd_wrap = CommandWrap::with_new(&command_path, |cmd| { cmd.args(&command_args) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .current_dir(&project_path); cmd.envs(&merged_envs); }); #[cfg(unix)] let mut child = cmd_wrap .wrap(ProcessGroup::leader()) .spawn() .context("[SACP] Failed to start claude-code-acp-ts subprocess")?; #[cfg(windows)] let mut child = cmd_wrap .wrap(CreationFlags(PROCESS_CREATION_FLAGS(CREATE_NO_WINDOW_FLAG))) .wrap(JobObject) .spawn() .context("[SACP] Failed to start claude-code-acp-ts subprocess")?; #[cfg(not(any(unix, windows)))] compile_error!("neither unix nor windows"); let child_pid = child.id().unwrap_or(0); info!( "[SACP] Claude Code ACP child process already started, PID: {}", child_pid ); // 获取 stdio 句柄(process_wrap 使用方法访问 stdio) let stdin = take_stdio(&mut child.stdin(), "stdin")?; let stdout = take_stdio(&mut child.stdout(), "stdout")?; let stderr = take_stdio(&mut child.stderr(), "stderr")?; // 🔥 立即启动 stderr 读取任务(在 session_id 等待之前) // 这样即使子进程在初始化阶段就退出,也能捕获 stderr 输出 let cancel_token_for_stderr = cancel_token.clone(); let stderr_output_shared = Arc::new(std::sync::Mutex::new(Vec::::new())); let stderr_output_clone = stderr_output_shared.clone(); let stderr_task_handle: tokio::task::JoinHandle<()> = tokio::spawn(async move { use tokio::io::{AsyncBufReadExt, BufReader}; let mut lines = BufReader::new(stderr).lines(); loop { tokio::select! { biased; // 优先检查取消信号 _ = cancel_token_for_stderr.cancelled() => { debug!("[SACP] stderr cancel received"); break; } result = lines.next_line() => { match result { Ok(Some(line)) if !line.trim().is_empty() => { warn!("[SACP] Claude Code Agent stderr: {}", line.trim()); // 存储 stderr 输出,用于错误传播 if let Ok(mut buf) = stderr_output_clone.lock() { buf.push(line.trim().to_string()); // 限制最多存储 20 行,避免内存膨胀 if buf.len() > 20 { buf.remove(0); } } } Ok(Some(_)) => {} // 空行,忽略 Ok(None) => break, // EOF Err(e) => { error!("[SACP] read stderr failed: {}", e); break; } } } } } }); // 创建 SACP transport let transport = agent_client_protocol::ByteStreams::new(stdin.compat_write(), stdout.compat()); // 🔥 新增:创建共享的异常退出标志 // 此标志在 reaper_task 检测到子进程异常退出时设置为 true // SACP 连接层可以检测此标志并发送相应的错误通知 let abnormal_exit_flag = Arc::new(AtomicBool::new(false)); // 共享的 session_id,用于连接失败时发送错误通知 let session_id_shared = Arc::new(std::sync::Mutex::new(None::)); // 共享的连接错误信息,用于 "channel dropped" 时传播真实错误原因 let connection_error_shared = Arc::new(std::sync::Mutex::new(None::)); // 克隆用于闭包 let project_path_clone = project_path.clone(); let project_id_clone = project_id.clone(); let cancel_token_clone = cancel_token.clone(); let notifier_clone = self.notifier.clone(); let abnormal_exit_flag_clone = abnormal_exit_flag.clone(); let session_id_shared_clone = session_id_shared.clone(); let connection_error_clone = connection_error_shared.clone(); let error_notifier = self.notifier.clone(); // 🔥 连接失败通知通道:连接任务失败时立即通知,不等 60 秒超时 let (connection_failed_tx, connection_failed_rx) = tokio::sync::oneshot::channel::(); let mut connection_failed_tx = Some(connection_failed_tx); // 保存 command_path 用于超时日志 let command_path_for_log = command_path.clone(); // 🔥 使用标准 tokio::spawn(无需 LocalSet!) // 保存 JoinHandle 用于超时时取消子任务 let spawn_project_id = project_id.clone(); let connection_task_handle = tokio::spawn(async move { info!( "[SACP] 🚀 Spawned ACP connection task, project_id={}", spawn_project_id ); let params = SacpConnectionParams { project_path: project_path_clone, project_id: project_id_clone.clone(), mcp_servers, start_config, session_id_tx, prompt_rx, cancel_rx, cancel_token: cancel_token_clone, notifier: notifier_clone, abnormal_exit_flag: abnormal_exit_flag_clone, session_id_shared: session_id_shared_clone, connection_failed_tx: connection_failed_tx.take(), child_pid, }; let result = run_sacp_connection(transport, params).await; match &result { Ok(_) => info!( "[SACP] ✅ ACP connection task completed successfully, project_id={}", spawn_project_id ), Err(e) => error!( "[SACP] ❌ ACP connection task failed: {}, project_id={}", e, spawn_project_id ), } if let Err(e) = result { error!("[SACP] Claude Code ACP Agent connection failed: {}", e); // 存储连接错误到共享状态,供 "channel dropped" 时使用 if let Ok(mut guard) = connection_error_clone.lock() { *guard = Some(format!("{}", e)); } // 🔥 立即通知外层连接失败,避免等待 60 秒超时 if let Some(tx) = connection_failed_tx.take() { let _ = tx.send(format!("{}", e)); } // 🔥 关键修复:连接失败时发送错误通知到 SSE 流 // 只有在 session_id 已经初始化的情况下才能发送(连接建立后才会有 session_id) let session_id = session_id_shared .lock() .ok() .and_then(|guard| guard.clone()); if let Some(session_id) = session_id { warn!( "[SACP] Sending error notification to SSE stream: project_id={}, session_id={}", project_id_clone, session_id ); let error = agent_client_protocol::schema::Error::new( 1001, format!("ACP connection failed: {}", e), ); if let Err(notify_err) = error_notifier .notify_prompt_error(&project_id_clone, &session_id, error, None) .await { error!("[SACP] Failed to send error notification: {:?}", notify_err); } } else { debug!( "[SACP] session_id not yet available, skipping error notification: project_id={}", project_id_clone ); } } }); // 等待会话 ID(60 秒超时),同时监听连接失败 info!( "[SACP] Waiting for session_id from ACP agent, project_id={}, timeout=60s", project_id ); let session_id = match tokio::time::timeout( std::time::Duration::from_secs(60), async { tokio::select! { result = session_id_rx => { match result { Ok(sid) => Ok(Ok(sid)), Err(e) => Ok(Err(anyhow::anyhow!("channel dropped: {}", e))), } } failed = connection_failed_rx => { match failed { Ok(err_msg) => Err(anyhow::anyhow!("{}", err_msg)), Err(_) => Ok(Err(anyhow::anyhow!("connection ended without session_id or error"))), } } } }, ) .await { Ok(Ok(Ok(session_id))) => { info!( "[SACP] Received session_id from ACP agent: {}, project_id={}", session_id, project_id ); session_id } Err(_timeout_elapsed) => { // 60 秒超时,连接任务仍在运行 let stderr_info = stderr_output_shared.lock().ok() .map(|buf| buf.join("\n")) .filter(|s| !s.is_empty()) .map(|s| format!("; stderr: {}", s)) .unwrap_or_default(); error!( "[SACP] ⏰ Agent initialization timeout (60s), project_id={}, command={}, child_pid={}, stderr={}", project_id, command_path_for_log, child_pid, stderr_info ); // 超时后取消 spawned 任务,避免子进程泄漏 connection_task_handle.abort(); // kill 子进程(使用进程组 kill 清理所有孙进程) #[cfg(unix)] { use nix::sys::signal::{Signal, killpg}; use nix::unistd::Pid; if child_pid > 0 { let pgid = Pid::from_raw(-(child_pid as i32)); match killpg(pgid, Signal::SIGKILL) { Ok(_) => warn!( "[SACP] Killed process group (SIGKILL) for child_pid={}, project_id={}", child_pid, project_id ), Err(e) => error!( "[SACP] Failed to kill process group for child_pid={}: {}, project_id={}", child_pid, e, project_id ), } } } return Err(anyhow::anyhow!( "{}: agent initialization timeout (60s){}", error_codes::get_i18n_message_default("error.agent_init_timeout"), stderr_info )); } Ok(Err(e)) => { // 连接任务主动报告了失败,立即返回 let err_str = e.to_string(); let stderr_info = stderr_output_shared.lock().ok() .map(|buf| buf.join("\n")) .filter(|s| !s.is_empty()) .map(|s| format!("; stderr: {}", s)) .unwrap_or_default(); let clean_msg = err_str .strip_prefix("connection failed: ") .unwrap_or(&err_str); error!( "[SACP] Agent connection failed early: project_id={}, error={}, stderr={}", project_id, err_str, stderr_info ); return Err(anyhow::anyhow!( "Agent process failed: {}{}", clean_msg, stderr_info )); } Ok(Ok(Err(e))) => { // channel dropped — 读取连接任务的实际错误原因 let connection_error = connection_error_shared.lock().ok() .and_then(|guard| guard.clone()) .unwrap_or_else(|| "unknown error".to_string()); // 读取 stderr 输出 let stderr_info = stderr_output_shared.lock().ok() .map(|buf| buf.join("\n")) .filter(|s| !s.is_empty()) .map(|s| format!("; stderr: {}", s)) .unwrap_or_default(); error!( "[SACP] session_id channel dropped (connection task failed): recv_error={}, actual_error={}, project_id={}", e, connection_error, project_id ); // 连接任务已自行结束,无需 abort return Err(anyhow::anyhow!( "{}: {}{}", error_codes::get_i18n_message_default("error.agent_init_timeout"), connection_error, stderr_info )); } }; info!( "[SACP] Claude Code ACP Agent service started successfully, session ID: {}", session_id ); // stderr 任务已在子进程启动后立即创建(stderr_task_handle),无需重复创建 // 创建生命周期守卫(带异常退出标志) let lifecycle_guard = AgentLifecycleGuard::new_claude_with_abnormal_flag( project_id.clone(), session_id.clone(), child, stderr_task_handle, cancel_token.clone(), abnormal_exit_flag, ); Ok(SacpLauncherConnectionInfo { session_id, prompt_tx, cancel_tx, lifecycle_guard: Arc::new(lifecycle_guard), }) } } /// 从 Option 中取出 stdio 句柄,失败时返回错误 fn take_stdio(opt: &mut Option, name: &str) -> Result { opt.take() .ok_or_else(|| anyhow::anyhow!("[SACP] Failed to get subprocess {}", name)) } /// SACP 连接参数(封装 run_sacp_connection 的参数) struct SacpConnectionParams { project_path: PathBuf, project_id: String, mcp_servers: Vec, start_config: AgentStartConfig, session_id_tx: tokio::sync::oneshot::Sender, prompt_rx: mpsc::Receiver, cancel_rx: mpsc::Receiver, cancel_token: CancellationToken, notifier: Arc, /// 🔥 新增:共享的异常退出标志(子进程异常退出时设置为 true) abnormal_exit_flag: Arc, /// 共享的 session_id,用于连接失败时发送错误通知 /// 在 connect_with 内部初始化完成后设置,供外部错误处理使用 session_id_shared: Arc>>, /// 🔥 连接失败通知通道:内部失败时立即通知外层,避免等待超时 connection_failed_tx: Option>, /// 子进程 PID,用于 waitpid 检测子进程退出 child_pid: u32, } /// 运行 SACP 连接 /// /// 使用 SACP 的 Builder 模式建立连接并处理消息 async fn run_sacp_connection( transport: agent_client_protocol::ByteStreams< tokio_util::compat::Compat, tokio_util::compat::Compat, >, params: SacpConnectionParams, ) -> Result<()> { // 解构参数 let SacpConnectionParams { project_path, project_id, mcp_servers, start_config, session_id_tx, mut prompt_rx, mut cancel_rx, cancel_token, notifier, abnormal_exit_flag, session_id_shared, mut connection_failed_tx, child_pid, } = params; // 克隆变量供 handlers 使用 let notifier_for_handlers = notifier.clone(); let project_id_for_handlers = project_id.clone(); // 克隆 notifier 和 project_id 供 prompt 结束通知使用 let notifier_for_prompt_end = notifier.clone(); let project_id_for_prompt_end = project_id.clone(); // 使用 SACP Builder 模式 Client.builder() .name("rcoder-agent-runner-sacp") // 处理 SessionNotification 通知(使用 dispatch 方式,优雅处理未知消息类型) .on_receive_dispatch( { let notifier = notifier_for_handlers.clone(); let project_id = project_id_for_handlers.clone(); async move |dispatch: Dispatch, _cx: ConnectionTo| { match dispatch { Dispatch::Notification(message) => { if SessionNotification::matches_method(&message.method) { match SessionNotification::parse_message(&message.method, &message.params) { Ok(notification) => { handle_session_notification(notification, notifier.clone(), project_id.clone()).await; Ok(Handled::Yes) } Err(err) => { // 🔥 关键:未知消息类型只打 warn,不中断连接 warn!( "[SACP] Failed to parse SessionNotification, ignoring: method={}, error={:?}, json={:?}", message.method, err, message.params ); Ok(Handled::Yes) } } } else { Ok(Handled::No { message: Dispatch::Notification(message), retry: false, }) } } other => Ok(Handled::No { message: other, retry: false, }), } } }, agent_client_protocol::on_receive_dispatch!(), ) // 处理 RequestPermission .on_receive_request( move |request: RequestPermissionRequest, responder: Responder, _cx: ConnectionTo| { async move { handle_permission_request(request, responder).await } }, agent_client_protocol::on_receive_request!(), ) // 主连接逻辑 .connect_with(transport, move |cx: ConnectionTo| { let project_path = project_path.clone(); let mcp_servers = mcp_servers.clone(); let start_config = start_config.clone(); let notifier_for_prompt = notifier_for_prompt_end.clone(); let project_id_for_prompt = project_id_for_prompt_end.clone(); let abnormal_exit_flag = abnormal_exit_flag.clone(); let session_id_shared = session_id_shared.clone(); async move { // 1. 初始化连接(30 秒超时) info!( "[SACP] Step 1/4: Initializing ACP connection, project_id={}", project_id ); let init_request = InitializeRequest::new(VERSION) .client_info(agent_client_protocol::schema::Implementation::new( "rcoder-agent-runner", env!("CARGO_PKG_VERSION"), )); debug!("[SACP] Sending InitializeRequest: {:?}", init_request); // 🔥 同时等待 InitializeRequest 和子进程退出 // 子进程崩溃时立即返回,不等 50 秒超时 let init_result = tokio::time::timeout( std::time::Duration::from_secs(50), async { tokio::select! { result = cx.send_request(init_request).block_task() => { Ok(result) } exit_info = tokio::task::spawn_blocking(move || { use nix::sys::wait::{WaitPidFlag, WaitStatus, waitpid}; use nix::unistd::Pid; use nix::errno::Errno; let target_pid = Pid::from_raw(child_pid as i32); // 使用 WNOHANG 轮询特定子进程 PID // 避免与 tokio 内部进程 reaper 竞争 loop { match waitpid(target_pid, Some(WaitPidFlag::WNOHANG)) { Ok(WaitStatus::Exited(pid, code)) => { return Some((pid.as_raw(), code)); } Ok(WaitStatus::Signaled(pid, signal, _)) => { return Some((pid.as_raw(), signal.as_str().parse::().unwrap_or(-1))); } Ok(WaitStatus::StillAlive) => { // 子进程仍在运行,等待 50ms 后重试 std::thread::sleep(std::time::Duration::from_millis(50)); continue; } Err(Errno::ECHILD) => { // 子进程已被回收(可能被 tokio reaper 或已退出) // 视为进程已退出 return Some((child_pid as i32, -1)); } _ => { // 其他错误 return None; } } } }) => { match exit_info { Ok(Some((pid, code))) => { // 子进程退出,立即返回错误 if code == -1 { // ECHILD: 进程已被回收,退出码未知 Err(anyhow::anyhow!("subprocess exited prematurely: pid={}, exit_code unknown (process already reaped)", pid)) } else { Err(anyhow::anyhow!("subprocess exited prematurely: pid={}, exit_code={}", pid, code)) } } _ => { // waitpid 未返回有效信息 Err(anyhow::anyhow!("subprocess exit detection failed for pid={}", child_pid)) } } } } } ).await; let _init_response = match init_result { Ok(Ok(Ok(result))) => { // InitializeRequest 成功 result } Ok(Ok(Err(e))) => { // send_request 返回错误 error!("[SACP] InitializeRequest error: {}, project_id={}", e, project_id); if let Some(tx) = connection_failed_tx.take() { let _ = tx.send(format!("InitializeRequest error: {}", e)); } return Err(e); } Ok(Err(process_err)) => { // 子进程异常退出 let err_msg = process_err.to_string(); error!("[SACP] Process exited during init: {}, project_id={}", err_msg, project_id); if let Some(tx) = connection_failed_tx.take() { let _ = tx.send(err_msg.clone()); } return Err(agent_client_protocol::Error::new(1003, err_msg)); } Err(_elapsed) => { error!( "[SACP] ⏰ InitializeRequest timeout (30s), project_id={}", project_id ); if let Some(tx) = connection_failed_tx.take() { let _ = tx.send(format!("ACP InitializeRequest timeout (30s), project_id={}", project_id)); } return Err(agent_client_protocol::Error::new( 1002, format!("ACP InitializeRequest timeout (30s), project_id={}", project_id), )); } }; info!( "[SACP] Step 1/4: ACP connection initialized successfully, project_id={}", project_id ); // 2. 构建 meta(包含系统提示词和可能的 resume) let system_prompt_meta = start_config.build_meta(); // 构建不含 resume 的 clean meta,用于 LoadSession 失败后回退到 NewSession // NewSession 应该是全新会话,不应携带旧的 resume session_id let new_session_meta = { let mut meta = system_prompt_meta.clone(); if let Some(claude_code) = meta.get_mut("claudeCode").and_then(|v| v.as_object_mut()) { if let Some(options) = claude_code.get_mut("options").and_then(|v| v.as_object_mut()) { options.remove("resume"); } } meta }; // 3. 创建或加载会话 // 从配置获取超时值,默认 60 秒 let timeout_secs = start_config .acp_session_create_timeout_secs .unwrap_or(60); info!( "[SACP] Step 3/4: Creating/loading session, project_id={}, timeout={}s, has_resume={}", project_id, timeout_secs, start_config.resume_session_id.is_some() ); // 🔥 修复:使用 Result 累积错误,避免 ? 操作符提前返回 // 无论成功失败,都确保能执行到 session_id_tx.send() let session_result: Result = if let Some(ref resume_id) = start_config.resume_session_id { // 有 resume_session_id,尝试加载历史会话 info!( "[SACP] Attempting to load existing session: {}", resume_id ); let load_request = LoadSessionRequest::new( resume_id.clone(), project_path.clone(), ) .mcp_servers(mcp_servers.clone()) .meta(system_prompt_meta.clone()); debug!("load_session_request: {:?}", load_request); match tokio::time::timeout( tokio::time::Duration::from_secs(timeout_secs), cx.send_request(load_request).block_task(), ) .await { Ok(Ok(_response)) => { // LoadSession 成功,使用请求中的 session_id info!( "[SACP] Session loaded successfully: {}, resuming session", resume_id ); Ok(SessionId::from(resume_id.clone())) } Ok(Err(load_err)) => { // LoadSession 返回错误,降级到 NewSessionRequest warn!( "[SACP] LoadSession failed, falling back to NewSession: {}", load_err ); let cancel_notification = CancelNotification::new(SessionId::from(resume_id.clone())); if let Err(e) = cx.send_notification(cancel_notification) { debug!( "[SACP] Failed to send cancel notification for LoadSession: {}", e ); } // 等待一小段时间让 agent 有机会清理 tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; let new_request = NewSessionRequest::new(project_path.clone()) .mcp_servers(mcp_servers.clone()) .meta(new_session_meta.clone()); debug!("new_session_request: {:?}", new_request); // 🔥 尝试 NewSession,不要用 ? 操作符 match tokio::time::timeout( tokio::time::Duration::from_secs(timeout_secs), cx.send_request(new_request).block_task(), ) .await { Ok(Ok(response)) => Ok(response.session_id), Ok(Err(new_err)) => Err(format!( "[SACP] LoadSession failed ({}), NewSession also failed ({})", load_err, new_err )), Err(_) => Err(format!( "[SACP] LoadSession failed (timeout), NewSession timeout" )), } } Err(_) => { // LoadSession 超时,降级到 NewSessionRequest warn!( "[SACP] LoadSession timeout ({}s), falling back to NewSession", timeout_secs ); let cancel_notification = CancelNotification::new(SessionId::from(resume_id.clone())); if let Err(e) = cx.send_notification(cancel_notification) { debug!( "[SACP] Failed to send cancel notification for LoadSession: {}", e ); } // 等待一小段时间让 agent 有机会清理 tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; let new_request = NewSessionRequest::new(project_path.clone()) .mcp_servers(mcp_servers.clone()) .meta(new_session_meta.clone()); debug!("new_session_request: {:?}", new_request); // 🔥 尝试 NewSession,不要用 ? 操作符 match tokio::time::timeout( tokio::time::Duration::from_secs(timeout_secs), cx.send_request(new_request).block_task(), ) .await { Ok(Ok(response)) => Ok(response.session_id), Ok(Err(new_err)) => Err(format!( "[SACP] LoadSession timeout, NewSession failed ({})", new_err )), Err(_) => { Err("[SACP] LoadSession timeout, NewSession timeout".to_string()) } } } } } else { // 没有 resume_session_id,创建新会话 info!("[SACP] Creating new ACP session (no resume_session_id)..."); let new_request = NewSessionRequest::new(project_path.clone()) .mcp_servers(mcp_servers.clone()) .meta(system_prompt_meta); debug!("new_session_request: {:?}", new_request); // 🔥 尝试 NewSession,不要用 ? 操作符 match tokio::time::timeout( tokio::time::Duration::from_secs(timeout_secs), cx.send_request(new_request).block_task(), ) .await { Ok(Ok(response)) => Ok(response.session_id), Ok(Err(e)) => Err(format!("[SACP] NewSession failed: {}", e)), Err(_) => Err(format!( "[SACP] NewSession timeout ({}s)", timeout_secs )), } }; // 🔥 关键修复:在闭包最后统一处理 session 创建结果 // 确保无论成功失败都能执行到发送逻辑 let session_id = match session_result { Ok(sid) => sid, Err(err_msg) => { error!("[SACP] Session creation failed: {}", err_msg); return Err(agent_client_protocol::Error::new(1000, err_msg)); } }; info!( "[SACP] ACP session ready, session_id={}", session_id ); // 发送会话 ID 到主任务 if session_id_tx.send(session_id.clone()).is_err() { error!("[SACP] unable to send session ID"); return Err(agent_client_protocol::Error::new( 1001, error_codes::get_i18n_message_default("error.sacp_session_id_send_failed"), )); } // 同步设置共享 session_id,供连接失败时的错误通知使用 if let Ok(mut guard) = session_id_shared.lock() { *guard = Some(session_id.to_string()); } // 4. 处理 Prompt 和 Cancel 请求 info!( "[SACP] Step 4/4: Entering prompt processing loop, project_id={}, session_id={}", project_id, session_id ); let mut session_cancelled = false; loop { tokio::select! { _ = cancel_token.cancelled() => { // 🔥 检测取消原因,区分"正常取消"和"Agent 进程退出" // 注意:如果在 prompt 处理中检测到取消,会在内层 loop 发送通知 // 这里只处理"没有正在处理的 prompt"时的情况 let is_abnormal = abnormal_exit_flag.load(Ordering::SeqCst); if is_abnormal { // Agent 进程异常退出,发送 SSE 错误通知 warn!( "[SACP] Agent process exited abnormally, sending SSE error notification and disconnecting: project_id={}, session_id={}", project_id_for_prompt, session_id ); if let Err(e) = notifier_for_prompt .notify_prompt_error( &project_id_for_prompt, &session_id.to_string(), agent_client_protocol::Error::new( 1001, error_codes::get_i18n_message_default("error.agent_process_abnormal_exit"), ), None, // request_id 可能已经不可用 ) .await { error!("[SACP] send Agent error notification failed: {:?}", e); } else { info!("[SACP] already sent Agent error notification: project_id={}", project_id_for_prompt); } } else { // 🔥 修复:正常取消时也要发送 PromptEnd,确保状态回退 Idle // 避免 Agent 一直卡在 Active 状态无法回收 if let Err(e) = notifier_for_prompt .notify_prompt_end( &project_id_for_prompt, &session_id.to_string(), agent_client_protocol::schema::StopReason::Cancelled, Some(error_codes::get_i18n_message_default("error.session_cancelled")), None, ) .await { error!("[SACP] send PromptEnd (Cancelled) notification failed: {:?}", e); } else { info!( "[SACP] Sent PromptEnd (Cancelled) notification, state will revert to Idle: project_id={}, session_id={}", project_id_for_prompt, session_id ); } } break; } Some(cancel_request) = cancel_rx.recv() => { let session_id_str = cancel_request.cancel_notification.session_id.0.to_string(); info!("[SACP] received cancel request: session_id={}", session_id_str); // 构建 SACP 版本的 CancelNotification 并发送到 Agent let sacp_session_id = SessionId::new(Arc::from(session_id_str.as_str())); let cancel_notification = CancelNotification::new(sacp_session_id); if let Err(e) = cx.send_notification(cancel_notification) { error!("[SACP] send cancel notification failed: {:?}", e); // 通知调用方取消失败 let _ = cancel_request.result_tx.send(shared_types::CancelResult::Failed( format!("Failed to send cancel notification: {:?}", e) )); } else { info!("[SACP] cancel notification sent"); // 通知调用方取消成功 let _ = cancel_request.result_tx.send(shared_types::CancelResult::Success); // 标记会话已取消,当前 prompt 完成后退出循环 session_cancelled = true; } } Some(prompt_request) = prompt_rx.recv() => { // 收到新 prompt,重置取消标志 // 场景:用户快速发送 prompt A → cancel → prompt B // - cancel 设置 session_cancelled=true // - prompt B 到达时重置为 false,处理完后不 break // - 保持 outer loop 存活以接收后续 prompt session_cancelled = false; debug!("[SACP] received Prompt request"); // 从 meta 中提取 request_id let request_id = prompt_request .meta .as_ref() .and_then(|meta| meta.get("request_id")) .and_then(|v| v.as_str()) .map(|s| s.to_string()); // 🎯 关键修复:通知状态管理器 Agent 开始处理 prompt // 此时状态从 Pending -> Active,确保状态与 agent 实际执行同步 let session_id_str = session_id.to_string(); if let Err(e) = notifier_for_prompt .notify_prompt_start( &project_id_for_prompt, &session_id_str, request_id.clone(), ) .await { error!("[SACP] send PromptStart notification failed: {:?}", e); } else { info!( "[SACP] PromptStart notification sent: session_id={}, request_id={:?}", session_id_str, request_id ); } // 创建 Prompt 响应的 Future,使用 pin! 来固定它 let prompt_future = cx.send_request(prompt_request).block_task(); tokio::pin!(prompt_future); // 取消后的超时保护:收到取消请求后最多等待 10 秒 let cancel_timeout = tokio::time::sleep(std::time::Duration::from_secs(3600)); // 初始设置一个很长的超时 tokio::pin!(cancel_timeout); let mut is_cancelled = false; // 在等待 Prompt 响应时也监听取消请求 let prompt_result = loop { tokio::select! { biased; // 🔥 监听 cancel_token(Agent 进程退出时会触发) _ = cancel_token.cancelled() => { let is_abnormal = abnormal_exit_flag.load(Ordering::SeqCst); if is_abnormal { warn!( "[SACP] Detected Agent process abnormal exit during prompt processing: project_id={}, session_id={}", project_id_for_prompt, session_id ); break Err(agent_client_protocol::Error::new( 1001, error_codes::get_i18n_message_default("error.agent_process_abnormal_exit"), )); } else { // 正常取消(用户主动取消或 Agent 正常退出) info!( "[SACP] Received cancel signal during prompt processing: project_id={}, session_id={}", project_id_for_prompt, session_id ); break Err(agent_client_protocol::Error::new( 1002, error_codes::get_i18n_message_default("error.session_cancelled"), )); } } // 取消后的超时保护(只有 is_cancelled 为 true 时才有意义) _ = &mut cancel_timeout, if is_cancelled => { // 取消后超时,强制返回错误 warn!("[SACP] cancel message Prompt response timeout (10s), force exit"); break Err(agent_client_protocol::Error::new( 1001, error_codes::get_i18n_message_default("error.cancel_response_timeout"), )); } // 检查取消请求(无论是否已取消都要接收,避免调用方超时) Some(cancel_request) = cancel_rx.recv() => { if is_cancelled { // 🎯 已经在取消中,直接返回成功(通知已发送) info!("[SACP] already sent cancel request, notification succeeded"); let _ = cancel_request.result_tx.send(shared_types::CancelResult::Success); } else { let session_id_str = cancel_request.cancel_notification.session_id.0.to_string(); info!("[SACP] received Prompt cancel request: session_id={}", session_id_str); // 发送取消通知给 Agent let sacp_session_id = SessionId::new(Arc::from(session_id_str.as_str())); let cancel_notification = CancelNotification::new(sacp_session_id); if let Err(e) = cx.send_notification(cancel_notification) { error!("[SACP] send cancel notification failed: {:?}", e); // 发送失败立即返回错误 let _ = cancel_request.result_tx.send(shared_types::CancelResult::Failed( format!("Failed to send cancel notification: {:?}", e) )); } else { info!("[SACP] cancel notification sent"); // 🎯 立即返回成功,不阻塞调用方 let _ = cancel_request.result_tx.send(shared_types::CancelResult::Success); is_cancelled = true; // 设置超时保护:取消后最多等待 10 秒让 prompt 完成 cancel_timeout.as_mut().reset(tokio::time::Instant::now() + std::time::Duration::from_secs(10)); } } // 继续等待 Prompt 响应(Agent 应该会因为取消而提前返回) } result = &mut prompt_future => { // Prompt 响应完成 break result; } } }; // 处理 Prompt 响应结果 match prompt_result { Ok(response) => { debug!("[SACP] Prompt response: stop_reason={:?}", response.stop_reason); // 发送 PromptEnd 通知 if let Err(e) = notifier_for_prompt .notify_prompt_end( &project_id_for_prompt, &session_id.to_string(), response.stop_reason, None, request_id.clone(), ) .await { error!("[SACP] send PromptEnd notification failed: {:?}", e); } else { info!( "[SACP] PromptEnd notification sent: session_id={}, request_id={:?}", session_id, request_id ); } } Err(e) => { // 🎯 区分"取消超时"和"真正的错误" if is_cancelled { // 取消超时:发送 PromptEnd (Cancelled) 而非 PromptError info!("[SACP] cancel timeout, send PromptEnd (Cancelled): session_id={}", session_id); if let Err(notify_err) = notifier_for_prompt .notify_prompt_end( &project_id_for_prompt, &session_id.to_string(), agent_client_protocol::schema::StopReason::Cancelled, Some(error_codes::get_i18n_message_default("error.session_cancelled_timeout")), request_id.clone(), ) .await { error!("[SACP] send PromptEnd (Cancelled) notification failed: {:?}", notify_err); } } else { // 真正的错误:发送 PromptError error!("[SACP] Prompt request failed: {:?}", e); if let Err(notify_err) = notifier_for_prompt .notify_prompt_error( &project_id_for_prompt, &session_id.to_string(), e, request_id.clone(), ) .await { error!("[SACP] send PromptError notification failed: {:?}", notify_err); } } // 🔥 关键:如果 cancel_token 已取消,直接退出外层 loop // 避免回到外层 loop 时再次触发 cancel_token.cancelled() 导致重复发送通知 if cancel_token.is_cancelled() { info!("[SACP] Prompt completed but cancel_token already cancelled, exiting"); break; } } } // 🎯 关键设计:cancel 后不退出 outer loop,保持 Agent 子进程存活 // // 为什么不能 break outer loop: // - outer loop break → spawned task 退出 → lifecycle_guard drop // - LifecycleGuard::drop() → SIGKILL → Agent 子进程被杀 // - 子进程被杀 → 内存中的对话上下文丢失 // - 下次请求 get_or_create_session → is_channel_closed()=true → 创建新 session → 上下文断裂 // // 正确行为: // - inner loop 处理了 cancel → is_cancelled=true → inner loop 退出 // - notify_prompt_end(Cancelled) → 状态恢复 Idle // - outer loop 继续等待 prompt_rx.recv() → 收到新 prompt → 复用同一 Agent 进程 // - 上下文连续:同一子进程、同一 SACP 连接、同一对话历史 // // is_cancelled 不传播到 session_cancelled,保持 outer loop 存活。 // session_cancelled 仅在 outer select 中收到 cancel(无活跃 prompt)时设置。 info!( "[SACP] Prompt cancelled, session ready for next prompt: project_id={}, session_id={}", project_id_for_prompt, session_id ); } else => { // 所有通道已关闭 info!("[SACP] channels already closed, exiting"); break; } } } Ok(()) } }) .await?; Ok(()) } /// 处理 SessionNotification 回调 async fn handle_session_notification( notification: SessionNotification, notifier: Arc, project_id: String, ) { let session_id = notification.session_id.to_string(); debug!( "[SACP] SessionNotification: project_id={}, session_id={}, update={:?}", project_id, session_id, notification.update ); // 提取 request_id(如果有) let request_id = notification .meta .as_ref() .and_then(|meta| meta.get("request_id")) .and_then(|v| v.as_str()) .map(|s| s.to_string()); // 通过 notifier 推送会话更新 // SessionUpdate 通过 agent_client_protocol::schema 导入 if let Err(e) = notifier .notify_session_update(&project_id, &session_id, notification.update, request_id) .await { error!( "[SACP] Push session update failed: project_id={}, session_id={}, error={:?}", project_id, session_id, e ); } } /// 处理 RequestPermission 回调 async fn handle_permission_request( request: RequestPermissionRequest, request_cx: Responder, ) -> Result<(), agent_client_protocol::Error> { debug!("[SACP] permission request: {:?}", request); // 自动允许:优先选择 AllowAlways,其次 AllowOnce let selected = request .options .iter() .find(|o| o.kind == PermissionOptionKind::AllowAlways) .or_else(|| { request .options .iter() .find(|o| o.kind == PermissionOptionKind::AllowOnce) }) .or_else(|| request.options.first()); if let Some(option) = selected { request_cx.respond(RequestPermissionResponse::new( RequestPermissionOutcome::Selected(SelectedPermissionOutcome::new( option.option_id.clone(), )), )) } else { // 无可选项则取消 request_cx.respond(RequestPermissionResponse::new( RequestPermissionOutcome::Cancelled, )) } } #[cfg(test)] mod tests { use super::*; use agent_config::ContextServerConfig; #[test] fn test_default_config() { let config = get_default_sacp_agent_config(None, &shared_types::ServiceType::RCoder); assert!(config.is_ok()); let config = config.unwrap(); // 命令应该是 "claude-code-acp-ts" 或其绝对路径(如果 which crate 能找到) // 两种情况都是正确的 let cmd = &config.command; assert!( cmd == "claude-code-acp-ts" || cmd.ends_with("claude-code-acp-ts"), "Expected command to be 'claude-code-acp-ts' or an absolute path ending with 'claude-code-acp-ts', got: {}", cmd ); } #[test] fn test_default_config_with_model_provider() { let provider = ModelProviderConfig { id: "test-id".to_string(), name: "test-provider".to_string(), api_key: "sk-test-key".to_string(), base_url: "https://api.test.com".to_string(), default_model: "test-model".to_string(), requires_openai_auth: false, api_protocol: None, }; let config = get_default_sacp_agent_config(Some(&provider), &shared_types::ServiceType::RCoder); assert!(config.is_ok()); let config = config.unwrap(); // 验证 API Key:proxy 模式下为占位符,非 proxy 模式下为真实值 assert!(config.env.contains_key("ANTHROPIC_API_KEY")); if cfg!(feature = "proxy") { assert_eq!( config.env.get("ANTHROPIC_API_KEY"), Some(&API_KEY_PLACEHOLDER.to_string()) ); } else { assert_eq!( config.env.get("ANTHROPIC_API_KEY"), Some(&"sk-test-key".to_string()) ); } // 应该包含模型设置 assert!(config.env.contains_key("ANTHROPIC_MODEL")); assert_eq!( config.env.get("ANTHROPIC_MODEL"), Some(&"test-model".to_string()) ); } #[test] fn test_default_config_disables_nonessential_traffic() { let config = get_default_sacp_agent_config(None, &shared_types::ServiceType::RCoder); assert!(config.is_ok()); let config = config.unwrap(); assert_eq!( config.env.get("CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC"), Some(&"1".to_string()) ); } #[test] fn test_default_config_with_openai_provider() { let provider = ModelProviderConfig { id: "test-openai".to_string(), name: "openai".to_string(), api_key: "sk-test-openai-key".to_string(), base_url: "https://api.openai.com/v1".to_string(), default_model: "openai-compatible/gpt-4".to_string(), // model_name 已包含前缀 requires_openai_auth: true, api_protocol: Some("openai".to_string()), }; let config = get_default_sacp_agent_config(Some(&provider), &shared_types::ServiceType::RCoder); assert!(config.is_ok()); let config = config.unwrap(); // 验证 OpenAI 环境变量 assert!(config.env.contains_key("OPENAI_API_KEY")); assert!(config.env.contains_key("OPENAI_BASE_URL")); if cfg!(feature = "proxy") { assert_eq!( config.env.get("OPENAI_API_KEY"), Some(&API_KEY_PLACEHOLDER.to_string()) ); assert_eq!( config.env.get("OPENAI_BASE_URL"), Some(&DEFAULT_PROXY_BASE_URL.to_string()) ); } else { assert_eq!( config.env.get("OPENAI_API_KEY"), Some(&"sk-test-openai-key".to_string()) ); assert_eq!( config.env.get("OPENAI_BASE_URL"), Some(&"https://api.openai.com/v1".to_string()) ); } // nuwaxcode 使用 OPENCODE_MODEL,直接使用 model_name(已包含 openai-compatible/ 前缀) assert!(config.env.contains_key("OPENCODE_MODEL")); assert_eq!( config.env.get("OPENCODE_MODEL"), Some(&"openai-compatible/gpt-4".to_string()) ); // 同时验证 Anthropic 环境变量也存在 (兼容性) assert!(config.env.contains_key("ANTHROPIC_API_KEY")); assert!(config.env.contains_key("ANTHROPIC_BASE_URL")); } #[test] fn test_sensitive_env_vars_protection() { // 测试默认配置中的环境变量值 // proxy 模式下:API_KEY 和 BASE_URL 为占位符/代理 URL // 非 proxy 模式下:API_KEY 和 BASE_URL 为真实值 let provider = ModelProviderConfig { id: "test".to_string(), name: "test".to_string(), api_key: "sk-real-key-should-be-replaced".to_string(), base_url: "https://real-url-should-be-replaced.com".to_string(), default_model: "openai-compatible/gpt-4".to_string(), requires_openai_auth: true, api_protocol: Some("openai".to_string()), }; let config = get_default_sacp_agent_config(Some(&provider), &shared_types::ServiceType::RCoder); assert!(config.is_ok()); let config = config.unwrap(); if cfg!(feature = "proxy") { // proxy 模式下:敏感变量应该是占位符 assert_eq!( config.env.get("ANTHROPIC_API_KEY"), Some(&API_KEY_PLACEHOLDER.to_string()) ); assert_eq!( config.env.get("OPENAI_API_KEY"), Some(&API_KEY_PLACEHOLDER.to_string()) ); assert_eq!( config.env.get("ANTHROPIC_BASE_URL"), Some(&DEFAULT_PROXY_BASE_URL.to_string()) ); assert_eq!( config.env.get("OPENAI_BASE_URL"), Some(&DEFAULT_PROXY_BASE_URL.to_string()) ); } else { // 非 proxy 模式下:使用真实值 assert_eq!( config.env.get("ANTHROPIC_API_KEY"), Some(&"sk-real-key-should-be-replaced".to_string()) ); assert_eq!( config.env.get("OPENAI_API_KEY"), Some(&"sk-real-key-should-be-replaced".to_string()) ); assert_eq!( config.env.get("ANTHROPIC_BASE_URL"), Some(&"https://real-url-should-be-replaced.com".to_string()) ); assert_eq!( config.env.get("OPENAI_BASE_URL"), Some(&"https://real-url-should-be-replaced.com".to_string()) ); } } #[test] fn test_convert_context_servers_empty() { let configs: HashMap = HashMap::new(); let servers = convert_context_servers_sacp(&configs); assert!(servers.is_empty()); } #[test] fn test_convert_context_servers_disabled() { let mut configs = HashMap::new(); configs.insert( "disabled-server".to_string(), ContextServerConfig { source: "local".to_string(), enabled: false, command: Some("node".to_string()), args: None, env: None, }, ); let servers = convert_context_servers_sacp(&configs); assert!(servers.is_empty()); // disabled 的服务器应该被过滤 } #[test] fn test_convert_context_servers_no_command() { let mut configs = HashMap::new(); configs.insert( "no-command-server".to_string(), ContextServerConfig { source: "local".to_string(), enabled: true, command: None, // 没有命令 args: None, env: None, }, ); let servers = convert_context_servers_sacp(&configs); assert!(servers.is_empty()); // 没有命令的服务器应该被过滤 } #[test] fn test_convert_context_servers_stdio() { let mut configs = HashMap::new(); configs.insert( "test-mcp".to_string(), ContextServerConfig { source: "local".to_string(), enabled: true, command: Some("node".to_string()), args: Some(vec![ "server.js".to_string(), "--port".to_string(), "3000".to_string(), ]), env: Some({ let mut env = HashMap::new(); env.insert("NODE_ENV".to_string(), "production".to_string()); env }), }, ); let servers = convert_context_servers_sacp(&configs); assert_eq!(servers.len(), 1); // 验证是 Stdio 类型 match &servers[0] { McpServer::Stdio(stdio) => { assert_eq!(stdio.name, "test-mcp"); } _ => panic!("Expected Stdio variant"), } } #[test] fn test_convert_context_servers_multiple() { let mut configs = HashMap::new(); configs.insert( "server1".to_string(), ContextServerConfig { source: "local".to_string(), enabled: true, command: Some("node".to_string()), args: Some(vec!["server1.js".to_string()]), env: None, }, ); configs.insert( "server2".to_string(), ContextServerConfig { source: "local".to_string(), enabled: true, command: Some("python".to_string()), args: Some(vec!["server2.py".to_string()]), env: None, }, ); configs.insert( "disabled".to_string(), ContextServerConfig { source: "local".to_string(), enabled: false, command: Some("ruby".to_string()), args: None, env: None, }, ); let servers = convert_context_servers_sacp(&configs); // 应该只有 2 个 enabled 的服务器 assert_eq!(servers.len(), 2); } #[test] fn test_sacp_agent_launch_config_fields() { let config = SacpAgentLaunchConfig { command: "test-cmd".to_string(), args: vec!["arg1".to_string(), "arg2".to_string()], env: { let mut env = HashMap::new(); env.insert("KEY".to_string(), "VALUE".to_string()); env }, context_servers: HashMap::new(), }; assert_eq!(config.command, "test-cmd"); assert_eq!(config.args.len(), 2); assert_eq!(config.env.get("KEY"), Some(&"VALUE".to_string())); assert!(config.context_servers.is_empty()); } #[test] fn test_sacp_agent_launch_config_debug() { let config = SacpAgentLaunchConfig { command: "test".to_string(), args: vec![], env: HashMap::new(), context_servers: HashMap::new(), }; let debug_str = format!("{:?}", config); assert!(debug_str.contains("SacpAgentLaunchConfig")); assert!(debug_str.contains("test")); } // === mcp-proxy convert 诊断参数测试 === #[test] fn test_is_mcp_proxy_command_simple() { // 简化版只检测精确的命令名 assert!(is_mcp_proxy_command("mcp-proxy")); // 不再检测大小写变体和路径 assert!(!is_mcp_proxy_command("MCP-PROXY")); assert!(!is_mcp_proxy_command("Mcp-Proxy")); } #[test] fn test_is_mcp_proxy_command_not_mcp_proxy() { assert!(!is_mcp_proxy_command("node")); assert!(!is_mcp_proxy_command("bunx")); assert!(!is_mcp_proxy_command("/usr/bin/uvx")); assert!(!is_mcp_proxy_command("mcp-proxy-other")); // 路径形式不再匹配(简化版) assert!(!is_mcp_proxy_command("/usr/local/bin/mcp-proxy")); assert!(!is_mcp_proxy_command("C:\\Users\\test\\mcp-proxy.exe")); } #[test] fn test_has_convert_subcommand() { assert!(has_convert_subcommand(&["convert".to_string()])); assert!(has_convert_subcommand(&[ "convert".to_string(), "http://example.com".to_string() ])); assert!(has_convert_subcommand(&[ "--config".to_string(), "config.json".to_string(), "convert".to_string() ])); } #[test] fn test_has_convert_subcommand_no_convert() { assert!(!has_convert_subcommand(&[])); assert!(!has_convert_subcommand(&["serve".to_string()])); assert!(!has_convert_subcommand(&[ "--config".to_string(), "config.json".to_string() ])); } #[test] fn test_enhance_mcp_proxy_args_non_mcp_proxy() { // 非 mcp-proxy 命令,应该原样返回 let args = vec!["arg1".to_string(), "arg2".to_string()]; let result = enhance_mcp_proxy_args("node", args.clone()); assert_eq!(result, args); } #[test] fn test_enhance_mcp_proxy_args_no_convert() { // mcp-proxy 但没有 convert 子命令,应该原样返回 let args = vec!["serve".to_string()]; let result = enhance_mcp_proxy_args("mcp-proxy", args.clone()); assert_eq!(result, args); } #[test] fn test_enhance_mcp_proxy_args_already_has_diagnostic() { // 已有 --diagnostic 参数 let args = vec![ "convert".to_string(), "--diagnostic".to_string(), "--log-dir".to_string(), "/tmp/logs".to_string(), ]; let result = enhance_mcp_proxy_args("mcp-proxy", args.clone()); // 应该原样返回,不重复添加 assert_eq!(result, args); } #[test] fn test_get_mcp_proxy_log_dir_none_when_unset() { // 清除环境变量以测试返回 None // SAFETY: 测试环境中修改环境变量是安全的 unsafe { std::env::remove_var(ENV_MCP_PROXY_LOG_DIR); } let log_dir = get_mcp_proxy_log_dir(); assert_eq!(log_dir, None); } #[test] fn test_get_mcp_proxy_log_dir_from_env() { let custom_dir = "/custom/mcp-proxy-logs"; // SAFETY: 测试环境中修改环境变量是安全的 unsafe { std::env::set_var(ENV_MCP_PROXY_LOG_DIR, custom_dir); } let log_dir = get_mcp_proxy_log_dir(); assert_eq!(log_dir, Some(custom_dir.to_string())); // 清理环境变量 // SAFETY: 测试环境中修改环境变量是安全的 unsafe { std::env::remove_var(ENV_MCP_PROXY_LOG_DIR); } } #[test] fn test_has_log_dir_arg() { // 检测 --log-dir 参数 assert!(has_log_dir_arg(&[ "--log-dir".to_string(), "/tmp".to_string() ])); assert!(has_log_dir_arg(&["--log-dir=/tmp".to_string()])); assert!(has_log_dir_arg(&[ "convert".to_string(), "--log-dir".to_string() ])); // 检测 --log-file 参数 assert!(has_log_dir_arg(&[ "--log-file".to_string(), "/tmp/log.txt".to_string() ])); assert!(has_log_dir_arg(&["--log-file=/tmp/log.txt".to_string()])); } #[test] fn test_has_log_dir_arg_no_log_args() { assert!(!has_log_dir_arg(&[])); assert!(!has_log_dir_arg(&["convert".to_string()])); assert!(!has_log_dir_arg(&["--diagnostic".to_string()])); assert!(!has_log_dir_arg(&[ "--config".to_string(), "config.json".to_string() ])); } #[test] fn test_enhance_args_respects_existing_log_dir() { // 模拟 debug 日志级别 // SAFETY: 测试环境中修改环境变量是安全的 unsafe { std::env::set_var("RUST_LOG", "debug"); std::env::set_var(ENV_MCP_PROXY_LOG_DIR, "/env/path"); } // 用户已配置 --log-dir,不应覆盖 let args = vec![ "convert".to_string(), "--log-dir".to_string(), "/custom/path".to_string(), ]; let result = enhance_mcp_proxy_args("mcp-proxy", args); // 应该只追加 --diagnostic,不重复追加 --log-dir assert!(result.contains(&"--diagnostic".to_string())); // 只应有一个 --log-dir assert_eq!(result.iter().filter(|a| *a == "--log-dir").count(), 1); // --log-dir 的值应该是用户配置的 /custom/path let log_dir_idx = result.iter().position(|a| a == "--log-dir").unwrap(); assert_eq!( result.get(log_dir_idx + 1), Some(&"/custom/path".to_string()) ); // 清理环境变量 // SAFETY: 测试环境中修改环境变量是安全的 unsafe { std::env::remove_var("RUST_LOG"); std::env::remove_var(ENV_MCP_PROXY_LOG_DIR); } } #[test] fn test_enhance_args_respects_existing_log_file() { // 模拟 debug 日志级别 // SAFETY: 测试环境中修改环境变量是安全的 unsafe { std::env::set_var("RUST_LOG", "debug"); std::env::set_var(ENV_MCP_PROXY_LOG_DIR, "/env/path"); } // 用户已配置 --log-file,不应追加 --log-dir let args = vec![ "convert".to_string(), "--log-file=/custom/file.log".to_string(), ]; let result = enhance_mcp_proxy_args("mcp-proxy", args); // 应该只追加 --diagnostic assert!(result.contains(&"--diagnostic".to_string())); // 不应有 --log-dir assert!(!result.iter().any(|a| a == "--log-dir")); // 清理环境变量 // SAFETY: 测试环境中修改环境变量是安全的 unsafe { std::env::remove_var("RUST_LOG"); std::env::remove_var(ENV_MCP_PROXY_LOG_DIR); } } #[test] fn test_enhance_args_adds_log_dir_when_env_set() { // 模拟 debug 日志级别和配置了 MCP_PROXY_LOG_DIR // SAFETY: 测试环境中修改环境变量是安全的 unsafe { std::env::set_var("RUST_LOG", "debug"); std::env::set_var(ENV_MCP_PROXY_LOG_DIR, "/var/log/mcp"); } let args = vec!["convert".to_string()]; let result = enhance_mcp_proxy_args("mcp-proxy", args); // 应该追加 --diagnostic 和 --log-dir assert!(result.contains(&"--diagnostic".to_string())); assert!(result.contains(&"--log-dir".to_string())); assert!(result.contains(&"/var/log/mcp".to_string())); // 清理环境变量 // SAFETY: 测试环境中修改环境变量是安全的 unsafe { std::env::remove_var("RUST_LOG"); std::env::remove_var(ENV_MCP_PROXY_LOG_DIR); } } #[test] fn test_enhance_args_no_log_dir_when_env_unset() { // 模拟 debug 日志级别但没有配置 MCP_PROXY_LOG_DIR // SAFETY: 测试环境中修改环境变量是安全的 unsafe { std::env::set_var("RUST_LOG", "debug"); std::env::remove_var(ENV_MCP_PROXY_LOG_DIR); } let args = vec!["convert".to_string()]; let result = enhance_mcp_proxy_args("mcp-proxy", args); // 应该只追加 --diagnostic,不应有 --log-dir assert!(result.contains(&"--diagnostic".to_string())); assert!(!result.iter().any(|a| a == "--log-dir")); // 清理环境变量 // SAFETY: 测试环境中修改环境变量是安全的 unsafe { std::env::remove_var("RUST_LOG"); } } }