1#[cfg(windows)]
36use tracing::{info, warn};
37
38#[cfg(windows)]
56pub fn check_windows_command(command: &str) {
57 use std::path::Path;
58
59 let cmd_ext = Path::new(command)
60 .extension()
61 .and_then(|e| e.to_str())
62 .map(|s| s.to_ascii_lowercase());
63
64 match cmd_ext.as_deref() {
65 Some("cmd" | "bat") => {
66 warn!(
67 "[MCP] Windows 检测到 .cmd/.bat 命令: {} - 可能会弹 CMD 窗口!",
68 command
69 );
70 warn!("[MCP] 建议改用 node.exe 直接运行 JS 文件,或在配置中使用完整路径");
71 }
72 None => {
73 if command.contains("npx") {
75 warn!(
76 "[MCP] Windows 检测到 npx 命令: {} - 可能会弹 CMD 窗口!",
77 command
78 );
79 warn!("[MCP] 建议改用 node.exe 直接运行 JS 文件");
80 }
81 }
82 _ => {
83 info!("[MCP] Windows 检测到命令格式: {}", command);
84 }
85 }
86}
87
88#[cfg(not(windows))]
90pub fn check_windows_command(_command: &str) {
91 }
93
94#[cfg(target_os = "windows")]
117pub fn resolve_windows_command(command: &str) -> String {
118 use std::path::Path;
119
120 if Path::new(command).extension().is_some() {
122 return command.to_string();
123 }
124
125 if Path::new(command).is_absolute() {
127 return command.to_string();
128 }
129
130 let path_env = match std::env::var("PATH") {
132 Ok(p) => p,
133 Err(_) => return command.to_string(),
134 };
135
136 let extensions = [".cmd", ".exe", ".bat", ".ps1"];
138
139 for dir in path_env.split(';') {
141 let dir = dir.trim();
142 if dir.is_empty() {
143 continue;
144 }
145
146 for ext in &extensions {
148 let full_path = Path::new(dir).join(format!("{}{}", command, ext));
149 if full_path.exists() {
150 tracing::debug!(
151 "[MCP] Windows 命令解析: {} -> {}",
152 command,
153 full_path.display()
154 );
155 return format!("{}{}", command, ext);
157 }
158 }
159 }
160
161 command.to_string()
163}
164
165#[cfg(not(target_os = "windows"))]
167pub fn resolve_windows_command(command: &str) -> String {
168 command.to_string()
169}
170
171pub fn ensure_runtime_path(path: &str) -> String {
182 if let Ok(runtime_path) = std::env::var("NUWAX_APP_RUNTIME_PATH") {
183 let runtime_path = runtime_path.trim();
184 if !runtime_path.is_empty() {
185 let sep = if cfg!(windows) { ";" } else { ":" };
186
187 let runtime_segments: Vec<&str> =
189 runtime_path.split(sep).filter(|s| !s.is_empty()).collect();
190
191 let existing_segments: Vec<&str> = path
193 .split(sep)
194 .filter(|s| !s.is_empty() && !runtime_segments.contains(s))
195 .collect();
196
197 let merged: Vec<&str> = runtime_segments
198 .iter()
199 .copied()
200 .chain(existing_segments)
201 .collect();
202
203 let result = merged.join(sep);
204 if result != path {
205 tracing::info!(
206 "[ProcessCompat] 前置应用内置运行时到 PATH: {}",
207 runtime_path
208 );
209 }
210 return result;
211 }
212 }
213 path.to_string()
214}
215
216pub fn prepare_stdio_env(
226 env: &Option<std::collections::HashMap<String, String>>,
227) -> (Option<String>, Option<Vec<(String, String)>>) {
228 let base_path = if env.as_ref().is_none_or(|e| !e.contains_key("PATH")) {
230 std::env::var("PATH").ok()
231 } else {
232 env.as_ref().and_then(|e| e.get("PATH").cloned())
233 };
234
235 let final_path = base_path.map(|path| {
237 #[cfg(target_os = "windows")]
238 let path = {
239 if let Ok(appdata) = std::env::var("APPDATA") {
240 let npm_path = format!(r"{}\npm", appdata);
241 if !path.contains(&npm_path) {
242 format!("{};{}", path, npm_path)
243 } else {
244 path
245 }
246 } else {
247 tracing::warn!("Windows: APPDATA not found, skipping npm global bin");
248 path
249 }
250 };
251 ensure_runtime_path(&path)
252 });
253
254 let filtered_env = env.as_ref().map(|vars| {
256 vars.iter()
257 .filter(|(k, _)| k.as_str() != "PATH")
258 .map(|(k, v)| (k.clone(), v.clone()))
259 .collect()
260 });
261
262 (final_path, filtered_env)
263}
264
265#[cfg(unix)]
288#[macro_export]
289macro_rules! wrap_process_v8 {
290 ($cmd:expr) => {{
291 use process_wrap::tokio::ProcessGroup;
292 $cmd.wrap(ProcessGroup::leader());
293 }};
294}
295
296#[cfg(windows)]
297#[macro_export]
298macro_rules! wrap_process_v8 {
299 ($cmd:expr) => {{
300 use process_wrap::tokio::{CreationFlags, JobObject};
301 use windows::Win32::System::Threading::{CREATE_NEW_PROCESS_GROUP, CREATE_NO_WINDOW};
302 $cmd.wrap(CreationFlags(CREATE_NO_WINDOW | CREATE_NEW_PROCESS_GROUP));
303 $cmd.wrap(JobObject);
304 }};
305}
306
307#[cfg(unix)]
330#[macro_export]
331macro_rules! wrap_process_v9 {
332 ($cmd:expr) => {{
333 use process_wrap::tokio::ProcessGroup;
334 $cmd.wrap(ProcessGroup::leader());
335 }};
336}
337
338#[cfg(windows)]
339#[macro_export]
340macro_rules! wrap_process_v9 {
341 ($cmd:expr) => {{
342 use process_wrap::tokio::{CreationFlags, JobObject};
343 use windows::Win32::System::Threading::{CREATE_NEW_PROCESS_GROUP, CREATE_NO_WINDOW};
344 $cmd.wrap(CreationFlags(CREATE_NO_WINDOW | CREATE_NEW_PROCESS_GROUP));
345 $cmd.wrap(JobObject);
346 }};
347}
348
349pub fn spawn_stderr_reader<T>(stderr: T, service_name: String) -> tokio::task::JoinHandle<()>
377where
378 T: tokio::io::AsyncRead + Unpin + Send + 'static,
379{
380 tokio::spawn(async move {
381 use tokio::io::{AsyncBufReadExt, BufReader};
382
383 let mut reader = BufReader::new(stderr);
384 let mut line = String::new();
385 loop {
386 line.clear();
387 match reader.read_line(&mut line).await {
388 Ok(0) => {
389 tracing::debug!("[子进程 stderr][{}] 读取结束 (EOF)", service_name);
391 break;
392 }
393 Ok(_) => {
394 let trimmed = line.trim();
395 if !trimmed.is_empty() {
396 tracing::warn!("[子进程 stderr][{}] {}", service_name, trimmed);
397 }
398 }
399 Err(e) => {
400 tracing::debug!("[子进程 stderr][{}] 读取错误: {}", service_name, e);
401 break;
402 }
403 }
404 }
405 })
406}
407
408#[cfg(target_os = "windows")]
428pub fn convert_unix_path_to_windows(path: &str) -> String {
429 let path = path.trim();
430
431 if path.len() >= 2 && path.chars().nth(1) == Some(':') {
433 return path.to_string();
434 }
435
436 if path.starts_with('/') && path.len() > 2 {
438 let chars: Vec<char> = path.chars().collect();
439 if chars[2] == '/' {
440 let drive = chars[1].to_ascii_uppercase();
441 let rest = &path[3..];
442 return format!("{}:\\{}", drive, rest.replace('/', "\\"));
443 }
444 }
445
446 if path.starts_with("/cygdrive/") && path.len() > 11 {
448 let rest = &path[10..];
449 let chars: Vec<char> = rest.chars().collect();
450 if chars.len() >= 2 && chars[1] == '/' {
451 let drive = chars[0].to_ascii_uppercase();
452 let rest_path = &rest[2..];
453 return format!("{}:\\{}", drive, rest_path.replace('/', "\\"));
454 }
455 }
456
457 path.to_string()
458}
459
460#[cfg(not(target_os = "windows"))]
462pub fn convert_unix_path_to_windows(path: &str) -> String {
463 path.to_string()
464}
465
466#[cfg(target_os = "windows")]
485pub fn convert_path_to_windows_format(path_env: &str) -> String {
486 path_env
487 .split(';')
488 .map(convert_unix_path_to_windows)
489 .collect::<Vec<_>>()
490 .join(";")
491}
492
493#[cfg(not(target_os = "windows"))]
495pub fn convert_path_to_windows_format(path_env: &str) -> String {
496 path_env.to_string()
497}
498
499#[cfg(target_os = "windows")]
524pub fn preprocess_npx_command_windows(
525 command: &str,
526 args: Option<&[String]>,
527) -> (String, Option<Vec<String>>) {
528 use tracing::info;
529
530 let is_npx = command == "npx"
532 || command == "npx.cmd"
533 || command.ends_with("/npx")
534 || command.ends_with("\\npx")
535 || command.ends_with("/npx.cmd")
536 || command.ends_with("\\npx.cmd");
537
538 if !is_npx {
539 return (command.to_string(), args.map(|a| a.to_vec()));
540 }
541
542 let args = match args {
543 Some(a) => a,
544 None => return (command.to_string(), None),
545 };
546
547 let package_spec = args
550 .iter()
551 .find(|s| !s.starts_with('-') && (s.contains('@') || s.starts_with('@')));
552
553 let Some(pkg) = package_spec else {
554 return (command.to_string(), Some(args.to_vec()));
555 };
556
557 let package_name = if pkg.starts_with('@') {
559 let parts: Vec<&str> = pkg.splitn(3, '@').collect();
561 if parts.len() >= 3 {
562 format!("@{}", parts[1])
564 } else if parts.len() == 2 && parts[1].contains('/') {
565 pkg.to_string()
567 } else {
568 pkg.to_string()
569 }
570 } else {
571 pkg.split('@').next().unwrap_or(pkg).to_string()
573 };
574
575 if let Some((node_exe, js_entry)) = find_npx_package_entry_windows(&package_name) {
577 info!(
578 "[MCP] Windows npx 转换: npx {} -> node {}",
579 pkg,
580 js_entry.display()
581 );
582
583 let mut new_args = vec![js_entry.to_string_lossy().to_string()];
585 for arg in args {
586 if arg != "-y" && arg != pkg {
588 new_args.push(arg.clone());
589 }
590 }
591
592 return (node_exe.to_string_lossy().to_string(), Some(new_args));
593 }
594
595 info!("[MCP] Windows npx 未找到已安装的包: {},保持原命令", pkg);
597 (command.to_string(), Some(args.to_vec()))
598}
599
600#[cfg(not(target_os = "windows"))]
602pub fn preprocess_npx_command_windows(
603 command: &str,
604 args: Option<&[String]>,
605) -> (String, Option<Vec<String>>) {
606 (command.to_string(), args.map(|a| a.to_vec()))
607}
608
609#[cfg(target_os = "windows")]
611fn find_npx_package_entry_windows(
612 package_name: &str,
613) -> Option<(std::path::PathBuf, std::path::PathBuf)> {
614 use tracing::info;
615
616 let node_exe = find_node_exe_windows()?;
618
619 let base_search_paths = get_npx_cache_paths_windows();
621
622 for base_path in base_search_paths {
623 let mut node_modules_dirs = Vec::new();
625
626 if base_path.ends_with("_npx") {
627 if let Ok(entries) = std::fs::read_dir(&base_path) {
630 for entry in entries.flatten() {
631 let hash_dir = entry.path();
632 let node_modules = hash_dir.join("node_modules");
633 if node_modules.exists() {
634 node_modules_dirs.push(node_modules);
635 }
636 }
637 }
638 } else {
639 if base_path.ends_with("node_modules") {
641 node_modules_dirs.push(base_path.clone());
642 } else if base_path.join("node_modules").exists() {
643 node_modules_dirs.push(base_path.join("node_modules"));
644 }
645 }
646
647 for node_modules_dir in node_modules_dirs {
649 let package_dir = node_modules_dir.join(package_name);
650 if !package_dir.exists() {
651 continue;
652 }
653
654 let package_json_path = package_dir.join("package.json");
656 if let Ok(content) = std::fs::read_to_string(&package_json_path) {
657 if let Ok(json) = serde_json::from_str::<serde_json::Value>(&content) {
658 let bin_entry = json.get("bin").and_then(|b| {
660 if let Some(s) = b.as_str() {
661 Some(s.to_string())
662 } else if let Some(obj) = b.as_object() {
663 obj.get(package_name)
666 .or_else(|| obj.values().next())
667 .and_then(|v| v.as_str())
668 .map(str::to_string)
669 } else {
670 None
671 }
672 });
673
674 if let Some(bin_entry) = bin_entry {
675 let js_entry = package_dir.join(bin_entry);
676 if js_entry.exists() {
677 info!(
678 "[MCP] Windows 找到包入口: {} -> {}",
679 package_name,
680 js_entry.display()
681 );
682 return Some((node_exe.clone(), js_entry));
683 }
684 }
685 }
686 }
687 }
688 }
689
690 None
691}
692
693#[cfg(target_os = "windows")]
695fn find_node_exe_windows() -> Option<std::path::PathBuf> {
696 use std::path::PathBuf;
697
698 if let Ok(node_from_env) = std::env::var("NUWAX_NODE_EXE") {
700 let path = PathBuf::from(node_from_env);
701 if path.exists() {
702 return Some(path);
703 }
704 }
705
706 if let Ok(exe_path) = std::env::current_exe() {
708 if let Some(exe_dir) = exe_path.parent() {
709 let resource_paths = [
710 exe_dir
711 .join("resources")
712 .join("node")
713 .join("bin")
714 .join("node.exe"),
715 exe_dir
716 .parent()
717 .unwrap_or(exe_dir)
718 .join("resources")
719 .join("node")
720 .join("bin")
721 .join("node.exe"),
722 ];
723
724 for path in resource_paths {
725 if path.exists() {
726 return Some(path);
727 }
728 }
729 }
730 }
731
732 which::which("node.exe").ok()
734}
735
736#[cfg(target_os = "windows")]
738fn get_npx_cache_paths_windows() -> Vec<std::path::PathBuf> {
739 use std::path::PathBuf;
740
741 let mut paths = Vec::new();
742
743 if let Ok(local_appdata) = std::env::var("LOCALAPPDATA") {
746 let local_appdata_path = PathBuf::from(&local_appdata);
747 paths.push(local_appdata_path.join("npm-cache").join("_npx"));
749 }
750
751 if let Ok(appdata) = std::env::var("APPDATA") {
753 let appdata_path = PathBuf::from(&appdata);
754
755 paths.push(appdata_path.join("npm").join("node_modules"));
757
758 paths.push(
760 appdata_path
761 .join("com.nuwax.agent-tauri-client")
762 .join("node_modules"),
763 );
764
765 paths.push(appdata_path.join("npm-cache").join("_npx"));
767 }
768
769 if let Ok(exe_path) = std::env::current_exe() {
771 if let Some(exe_dir) = exe_path.parent() {
772 let resource_paths = [
773 exe_dir.join("resources").join("node").join("node_modules"),
774 exe_dir
775 .parent()
776 .unwrap_or(exe_dir)
777 .join("resources")
778 .join("node")
779 .join("node_modules"),
780 ];
781
782 for path in resource_paths {
783 if path.exists() {
784 paths.push(path);
785 }
786 }
787 }
788 }
789
790 paths
791}
792
793#[cfg(test)]
794mod tests {
795 use super::*;
796
797 #[test]
798 fn test_check_windows_command_non_windows() {
799 check_windows_command("npx some-server");
801 check_windows_command("test.cmd");
802 }
803
804 #[test]
805 fn test_ensure_runtime_path_no_env() {
806 unsafe { std::env::remove_var("NUWAX_APP_RUNTIME_PATH") };
808 let result = ensure_runtime_path("/usr/bin:/usr/local/bin");
809 assert_eq!(result, "/usr/bin:/usr/local/bin");
810 }
811
812 #[test]
813 fn test_ensure_runtime_path_prepend() {
814 unsafe {
815 std::env::set_var("NUWAX_APP_RUNTIME_PATH", "/app/node/bin:/app/uv/bin");
816 }
817 let result = ensure_runtime_path("/usr/bin:/usr/local/bin");
818 assert_eq!(result, "/app/node/bin:/app/uv/bin:/usr/bin:/usr/local/bin");
819 unsafe { std::env::remove_var("NUWAX_APP_RUNTIME_PATH") };
820 }
821
822 #[test]
823 fn test_ensure_runtime_path_dedup() {
824 unsafe {
826 std::env::set_var("NUWAX_APP_RUNTIME_PATH", "/app/node/bin:/app/uv/bin");
827 }
828 let result = ensure_runtime_path("/app/node/bin:/opt/homebrew/bin:/usr/bin");
829 assert_eq!(
830 result,
831 "/app/node/bin:/app/uv/bin:/opt/homebrew/bin:/usr/bin"
832 );
833 unsafe { std::env::remove_var("NUWAX_APP_RUNTIME_PATH") };
834 }
835
836 #[test]
837 fn test_ensure_runtime_path_all_present() {
838 unsafe {
840 std::env::set_var("NUWAX_APP_RUNTIME_PATH", "/app/node/bin:/app/uv/bin");
841 }
842 let result = ensure_runtime_path("/app/uv/bin:/usr/bin:/app/node/bin");
843 assert_eq!(result, "/app/node/bin:/app/uv/bin:/usr/bin");
844 unsafe { std::env::remove_var("NUWAX_APP_RUNTIME_PATH") };
845 }
846
847 #[test]
848 fn test_ensure_runtime_path_double_node() {
849 unsafe {
851 std::env::set_var(
852 "NUWAX_APP_RUNTIME_PATH",
853 "/app/node/bin:/app/uv/bin:/app/debug",
854 );
855 }
856 let result = ensure_runtime_path(
857 "/app/node/bin:/app/node/bin:/app/uv/bin:/app/debug:/opt/homebrew/bin",
858 );
859 assert_eq!(
860 result,
861 "/app/node/bin:/app/uv/bin:/app/debug:/opt/homebrew/bin"
862 );
863 unsafe { std::env::remove_var("NUWAX_APP_RUNTIME_PATH") };
864 }
865
866 #[test]
867 #[cfg(target_os = "windows")]
868 fn test_convert_unix_path_to_windows() {
869 assert_eq!(
871 convert_unix_path_to_windows("/c/Program Files/nodejs"),
872 "C:\\Program Files\\nodejs"
873 );
874 assert_eq!(
876 convert_unix_path_to_windows("/cygdrive/c/Windows"),
877 "C:\\Windows"
878 );
879 assert_eq!(
881 convert_unix_path_to_windows("C:\\Windows\\System32"),
882 "C:\\Windows\\System32"
883 );
884 assert_eq!(
886 convert_unix_path_to_windows("/d/tools/bin"),
887 "D:\\tools\\bin"
888 );
889 assert_eq!(convert_unix_path_to_windows("/c/"), "C:\\");
891 assert_eq!(convert_unix_path_to_windows(""), "");
893 assert_eq!(convert_unix_path_to_windows(" "), "");
895 }
896
897 #[test]
898 #[cfg(target_os = "windows")]
899 fn test_convert_path_to_windows_format() {
900 let path = "/c/Program Files/nodejs;C:\\Windows\\System32;/d/tools";
902 let result = convert_path_to_windows_format(path);
903 assert_eq!(
904 result,
905 "C:\\Program Files\\nodejs;C:\\Windows\\System32;D:\\tools"
906 );
907
908 let unix_path = "/c/Program Files/nodejs;/d/tools/bin;/e/dev";
910 let result = convert_path_to_windows_format(unix_path);
911 assert_eq!(result, "C:\\Program Files\\nodejs;D:\\tools\\bin;E:\\dev");
912
913 let win_path = "C:\\Windows\\System32;D:\\tools\\bin";
915 let result = convert_path_to_windows_format(win_path);
916 assert_eq!(result, win_path);
917
918 assert_eq!(convert_path_to_windows_format(""), "");
920
921 assert_eq!(
923 convert_path_to_windows_format("/c/Program Files/nodejs"),
924 "C:\\Program Files\\nodejs"
925 );
926 }
927}