From 9ab54c93ef08b4af83a8e0f3194e2c35653d79aa Mon Sep 17 00:00:00 2001 From: YoVinchen Date: Mon, 30 Mar 2026 23:05:28 +0800 Subject: [PATCH] fix(terminal): use pushd for UNC paths in Windows batch launcher MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `cmd.exe` cannot set a UNC path (e.g. `\\wsl$\...`) as the current directory via `cd /d`; it errors with "CMD does not support UNC paths as current directories". Switch to `pushd` which temporarily maps the UNC share to a drive letter. Rename `build_windows_cd_command` → `build_windows_cwd_command` to reflect the broader semantics. Extract `build_windows_cwd_command_str` and `is_windows_unc_path` helpers for testability, and add unit tests covering drive paths, UNC paths, and batch metacharacter escaping. Also fix minor style issues: sort mod declarations alphabetically, add missing EOF newline in lightweight.rs, add explicit type annotation in streaming_responses test, and reformat tray menu builder chain. --- src-tauri/src/commands/misc.rs | 63 +++++++++++++++---- src-tauri/src/commands/mod.rs | 4 +- src-tauri/src/lightweight.rs | 2 +- .../proxy/providers/streaming_responses.rs | 4 +- src-tauri/src/tray.rs | 13 ++-- 5 files changed, 62 insertions(+), 24 deletions(-) diff --git a/src-tauri/src/commands/misc.rs b/src-tauri/src/commands/misc.rs index fb6b980f9..4e491b5f2 100644 --- a/src-tauri/src/commands/misc.rs +++ b/src-tauri/src/commands/misc.rs @@ -1172,11 +1172,11 @@ fn launch_windows_terminal( let bat_file = temp_dir.join(format!("cc_switch_claude_{}.bat", std::process::id())); let config_path_for_batch = escape_windows_batch_value(&config_file.to_string_lossy()); - let cd_command = build_windows_cd_command(cwd); + let cwd_command = build_windows_cwd_command(cwd); let content = format!( "@echo off -{cd_command} +{cwd_command} echo Using provider-specific claude config: echo {} claude --settings \"{}\" @@ -1186,7 +1186,7 @@ del \"%~f0\" >nul 2>&1 config_path_for_batch, config_path_for_batch, config_path_for_batch, - cd_command = cd_command, + cwd_command = cwd_command, ); std::fs::write(&bat_file, &content).map_err(|e| format!("写入批处理文件失败: {e}"))?; @@ -1231,18 +1231,30 @@ fn shell_single_quote(value: &str) -> String { format!("'{}'", value.replace('\'', "'\"'\"'")) } -#[cfg(target_os = "windows")] -fn build_windows_cd_command(cwd: Option<&Path>) -> String { - cwd.map(|dir| { - format!( - "cd /d \"{}\" || exit /b 1\r\n", - escape_windows_batch_value(&dir.to_string_lossy()) - ) - }) - .unwrap_or_default() +#[cfg_attr(not(target_os = "windows"), allow(dead_code))] +fn is_windows_unc_path(path: &str) -> bool { + path.starts_with(r"\\") +} + +#[cfg_attr(not(target_os = "windows"), allow(dead_code))] +fn build_windows_cwd_command_str(path: &str) -> String { + let escaped = escape_windows_batch_value(path); + + if is_windows_unc_path(path) { + // `cmd.exe` cannot make a UNC path current via `cd`; `pushd` maps it first. + format!("pushd \"{escaped}\" || exit /b 1\r\n") + } else { + format!("cd /d \"{escaped}\" || exit /b 1\r\n") + } } #[cfg(target_os = "windows")] +fn build_windows_cwd_command(cwd: Option<&Path>) -> String { + cwd.map(|dir| build_windows_cwd_command_str(&dir.to_string_lossy())) + .unwrap_or_default() +} + +#[cfg_attr(not(target_os = "windows"), allow(dead_code))] fn escape_windows_batch_value(value: &str) -> String { value .replace('^', "^^") @@ -1459,4 +1471,31 @@ mod tests { assert_eq!(command, "cd '/tmp/project O'\"'\"'Brien' || exit 1\n"); } + + #[test] + fn build_windows_cwd_command_str_uses_cd_for_drive_paths() { + let command = build_windows_cwd_command_str(r"C:\work\repo"); + + assert_eq!(command, "cd /d \"C:\\work\\repo\" || exit /b 1\r\n"); + } + + #[test] + fn build_windows_cwd_command_str_uses_pushd_for_unc_paths() { + let command = build_windows_cwd_command_str(r"\\wsl$\Ubuntu\home\coder\repo"); + + assert_eq!( + command, + "pushd \"\\\\wsl$\\Ubuntu\\home\\coder\\repo\" || exit /b 1\r\n" + ); + } + + #[test] + fn build_windows_cwd_command_str_escapes_batch_metacharacters() { + let command = build_windows_cwd_command_str(r"\\server\share\100%&(test)"); + + assert_eq!( + command, + "pushd \"\\\\server\\share\\100%%^&^(test^)\" || exit /b 1\r\n" + ); + } } diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs index 09bb312b3..04bb6da3a 100644 --- a/src-tauri/src/commands/mod.rs +++ b/src-tauri/src/commands/mod.rs @@ -22,10 +22,10 @@ pub mod skill; mod stream_check; mod sync_support; +mod lightweight; mod usage; mod webdav_sync; mod workspace; -mod lightweight; pub use auth::*; pub use config::*; @@ -48,7 +48,7 @@ pub use settings::*; pub use skill::*; pub use stream_check::*; +pub use lightweight::*; pub use usage::*; pub use webdav_sync::*; pub use workspace::*; -pub use lightweight::*; diff --git a/src-tauri/src/lightweight.rs b/src-tauri/src/lightweight.rs index cd0875710..c16af79ef 100644 --- a/src-tauri/src/lightweight.rs +++ b/src-tauri/src/lightweight.rs @@ -87,4 +87,4 @@ pub fn exit_lightweight_mode(app: &tauri::AppHandle) -> Result<(), String> { pub fn is_lightweight_mode() -> bool { LIGHTWEIGHT_MODE.load(Ordering::Acquire) -} \ No newline at end of file +} diff --git a/src-tauri/src/proxy/providers/streaming_responses.rs b/src-tauri/src/proxy/providers/streaming_responses.rs index 4ad941f09..ea9274ff8 100644 --- a/src-tauri/src/proxy/providers/streaming_responses.rs +++ b/src-tauri/src/proxy/providers/streaming_responses.rs @@ -974,7 +974,9 @@ mod tests { "data: {\"type\":\"response.completed\",\"response\":{\"status\":\"completed\",\"usage\":{\"input_tokens\":5,\"output_tokens\":2}}}\n\n" ); - let upstream = stream::iter(vec![Ok(Bytes::from(input.as_bytes().to_vec()))]); + let upstream = stream::iter(vec![Ok::<_, std::io::Error>(Bytes::from( + input.as_bytes().to_vec(), + ))]); let converted = create_anthropic_sse_stream_from_responses(upstream); let chunks: Vec<_> = converted.collect().await; let events: Vec = chunks diff --git a/src-tauri/src/tray.rs b/src-tauri/src/tray.rs index f5b068157..6ac3d2046 100644 --- a/src-tauri/src/tray.rs +++ b/src-tauri/src/tray.rs @@ -393,11 +393,10 @@ pub fn create_tray_menu( true, crate::lightweight::is_lightweight_mode(), None::<&str>, - ).map_err(|e| AppError::Message(format!("创建轻量模式菜单失败: {e}")))?; + ) + .map_err(|e| AppError::Message(format!("创建轻量模式菜单失败: {e}")))?; - menu_builder = menu_builder - .item(&lightweight_item) - .separator(); + menu_builder = menu_builder.item(&lightweight_item).separator(); // 退出菜单(分隔符已在上面的 section 循环中添加) let quit_item = MenuItem::with_id(app, "quit", tray_texts.quit, true, None::<&str>) @@ -472,10 +471,8 @@ pub fn handle_tray_menu_event(app: &tauri::AppHandle, event_id: &str) { if let Err(e) = crate::lightweight::exit_lightweight_mode(app) { log::error!("退出轻量模式失败: {e}"); } - } else { - if let Err(e) = crate::lightweight::enter_lightweight_mode(app) { - log::error!("进入轻量模式失败: {e}"); - } + } else if let Err(e) = crate::lightweight::enter_lightweight_mode(app) { + log::error!("进入轻量模式失败: {e}"); } } "quit" => {