From 8376bcbfd26cde349fe12a9ad4aa1185c75622a5 Mon Sep 17 00:00:00 2001 From: lanrtop Date: Tue, 22 Sep 2026 01:23:34 +0900 Subject: [PATCH] =?UTF-8?q?fix(toolchain):=20=E4=BF=AE=E5=A4=8D=20Windows?= =?UTF-8?q?=20wrapper=20=E7=89=88=E6=9C=AC=E6=8E=A2=E6=B5=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 优先选择 .exe/.cmd/.bat 路径并直接执行版本命令,补充含空格路径测试;运行态 MCP 已确认 Enterprise 的 Node 与 pnpm 版本可正确识别。 Rules-Applied: R07,R08,R10,R13 --- .../modules/toolchain-capability-pack.md | 8 +- src-tauri/src/commands/toolchain.rs | 74 +++++++++++++++---- 2 files changed, 64 insertions(+), 18 deletions(-) diff --git a/.blueprint/modules/toolchain-capability-pack.md b/.blueprint/modules/toolchain-capability-pack.md index 78cb009..aa1923c 100644 --- a/.blueprint/modules/toolchain-capability-pack.md +++ b/.blueprint/modules/toolchain-capability-pack.md @@ -90,7 +90,7 @@ Enterprise System 已精确声明 Node `24.19.0` 与 pnpm `9.15.0`,而 Codex b - depends: T1. Node/pnpm 工具链契约解析器 - files: src-tauri/src/commands/toolchain.rs, src-tauri/src/lib.rs, src/lib/commands.ts - acceptance: "[Tauri command] get_toolchain_status(project_id) → 返回 declared/actual/executable/source/resolver/status/recommendation;项目不存在或路径越界时显式报错;实际版本匹配但无 resolver 时为 compliant_unmanaged,版本不符时为 mismatch。" -- done_note: "2026-09-22:新增依赖注入业务函数与薄 Tauri command,只接受已登记项目的 Windows 路径,同时解析 executable path 和 `.cmd` 版本输出;WSL 按用户决策明确不支持。复盘待炼境 MCP 在本会话可用后回填。" +- done_note: "2026-09-22:新增依赖注入业务函数与薄 Tauri command,只接受已登记项目的 Windows 路径,同时解析 executable path 和版本输出;运行态 MCP 自检发现 `where pnpm` 优先返回无扩展 shim 且 cmd 引号不稳,已改为优先 `.exe/.cmd/.bat` 并分别使用直接执行或 `cmd /D /S /C`。WSL 按用户决策明确不支持。复盘待炼境 MCP 在本会话可用后回填。" ### ✅ T3. MCP 只读诊断投影与契约文档 - status: done @@ -106,7 +106,7 @@ Enterprise System 已精确声明 Node `24.19.0` 与 pnpm `9.15.0`,而 Codex b - depends: T2. 只读工具链状态 Tauri command - files: src-tauri/src/commands/toolchain.rs, src-tauri/src/mcp_tools.rs, src-tauri/src/lib.rs, src/lib/commands.ts - acceptance: "[Tauri command] prepare_toolchain(project_id, confirmed) → confirmed=false、未知项目、根路径越界或 contract=conflict 时零执行;confirmed=true 时只执行参数化 `mise install`,不运行项目 task/hook,不修改 package.json/pnpm-lock.yaml,并返回 before/after 状态与命令证据。" -- done_note: "2026-09-22:新增 Tauri/MCP 受控 prepare_toolchain;resolver 就绪后通过 mise exec 探测项目版本,拒绝未确认、冲突、缺版本、缺 mise 和含自定义 task/hook/plugin 的配置;返回 verified 区分命令成功与二次诊断 ready_managed,13 个定向测试全绿,未实际下载安装。复盘待炼境 MCP 在本会话可用后回填。" +- done_note: "2026-09-22:新增 Tauri/MCP 受控 prepare_toolchain;resolver 就绪后通过 mise exec 探测项目版本,拒绝未确认、冲突、缺版本、缺 mise 和含自定义 task/hook/plugin 的配置;返回 verified 区分命令成功与二次诊断 ready_managed,定向测试全绿,未实际下载安装。复盘待炼境 MCP 在本会话可用后回填。" ### 🔴 T5. 项目环境 UI 工具链契约卡 - status: blocked @@ -114,7 +114,7 @@ Enterprise System 已精确声明 Node `24.19.0` 与 pnpm `9.15.0`,而 Codex b - depends: T2. 只读工具链状态 Tauri command, T4. 受控 mise 工具链准备能力 - files: src/components/dashboard/ProjectEnvModal.tsx, src/lib/commands.ts - acceptance: "agent_verify: [pnpm typecheck] ProjectEnvModal 消费 ToolchainStatus 类型且无类型错误;[Tauri command mock/fixture] ready_managed、compliant_unmanaged、mismatch、conflict 分别渲染准确状态与可用动作,prepare 必须经过确认;human_verify: 环境工具弹窗能看懂期望/实际版本、来源路径和准备结果。" -- blocked_reason: "代码接线与 pnpm typecheck 已通过;本项目按规则不为 React 组件临时引入测试框架,且隔离 worktree 未启动 Tauri UI,尚缺 human_verify。需合并/启动该分支后在环境工具弹窗终验四种状态与确认按钮。" +- blocked_reason: "代码接线与 pnpm typecheck 已通过;隔离分支已启动,运行态 MCP 对 Enterprise 返回 Node 24.19.0、pnpm 9.15.0 且均为 compliant_unmanaged,并由此发现并修复 Windows `.cmd` wrapper 版本探测;当前 Computer Use 未暴露 Tauri 原生窗口,本项目也不为 React 组件临时引入测试框架,仍缺 human_verify。需在环境工具弹窗终验状态卡与确认按钮。" ### ✅ T6. Onboarding 与 Codex Skill 条件式工具链指引 - status: done @@ -130,7 +130,7 @@ Enterprise System 已精确声明 Node `24.19.0` 与 pnpm `9.15.0`,而 Codex b - depends: T3. MCP 只读诊断投影与契约文档, T4. 受控 mise 工具链准备能力, T6. Onboarding 与 Codex Skill 条件式工具链指引 - files: E:/code-project/enterprise-system/mise.toml, E:/code-project/enterprise-system/AGENTS.md, E:/code-project/enterprise-system/.gitea/workflows/ci.yml - acceptance: "agent_verify: [CLI] `mise exec -- node --version` → v24.19.0,`mise exec -- pnpm --version` → 9.15.0,`mise exec -- pnpm check:toolchain` → exit 0;[Gitea CI] 从 `.nvmrc` 与 packageManager 安装精确版本并在业务检查前通过 check:toolchain;package.json 与 pnpm-lock.yaml 无非预期改动;human_verify: 新 Codex 会话进入 Enterprise 后不再因 bundled pnpm 11 阻塞。" -- blocked_reason: "消费者试点依赖本分支先完成审查、合并并重建炼境,使新 onboarding/MCP tool 真正可调用;Enterprise 当前另一个会话还有未提交 T3 migration/蓝图改动,现阶段不跨项目写入或混合验证。" +- blocked_reason: "运行态只读试点已确认 Enterprise 的 Node 24.19.0、pnpm 9.15.0 声明与本机版本匹配,但 resolver=unavailable,因此仍不是跨 Agent 可复现的 ready_managed。写入试点依赖本分支先完成审查、合并并重建炼境;Enterprise 当前另一个会话还有未提交 T3 migration/蓝图改动,现阶段不跨项目写入或混合验证。" ## 执行顺序 diff --git a/src-tauri/src/commands/toolchain.rs b/src-tauri/src/commands/toolchain.rs index c8787cb..4039f2a 100644 --- a/src-tauri/src/commands/toolchain.rs +++ b/src-tauri/src/commands/toolchain.rs @@ -320,6 +320,31 @@ pub fn parse_toolchain_contract(root: &Path) -> Result Option { + let paths = String::from_utf8_lossy(output) + .lines() + .map(str::trim) + .filter(|line| !line.is_empty()) + .map(str::to_string) + .collect::>(); + + #[cfg(target_os = "windows")] + if let Some(native_wrapper) = paths.iter().find(|path| { + matches!( + Path::new(path) + .extension() + .and_then(|extension| extension.to_str()) + .map(|extension| extension.to_ascii_lowercase()) + .as_deref(), + Some("exe" | "cmd" | "bat") + ) + }) { + return Some(native_wrapper.clone()); + } + + paths.into_iter().next() +} + fn locate_executable(tool: &str) -> Option { #[cfg(target_os = "windows")] let output = crate::silent_cmd("where.exe").arg(tool).output().ok()?; @@ -329,25 +354,22 @@ fn locate_executable(tool: &str) -> Option { output .status .success() - .then(|| { - String::from_utf8_lossy(&output.stdout) - .lines() - .map(str::trim) - .find(|line| !line.is_empty()) - .unwrap_or_default() - .to_string() - }) - .filter(|path| !path.is_empty()) + .then(|| select_executable_path(&output.stdout)) + .flatten() } fn executable_version(path: &str) -> Option { #[cfg(target_os = "windows")] let output = { - let escaped = path.replace('"', "\"\""); - crate::silent_cmd("cmd") - .args(["/D", "/C", &format!("\"{escaped}\" --version")]) - .output() - .ok()? + let extension = Path::new(path) + .extension() + .and_then(|value| value.to_str()) + .unwrap_or_default(); + if extension.eq_ignore_ascii_case("exe") { + crate::silent_cmd(path).arg("--version").output().ok()? + } else { + crate::silent_cmd(path).arg("--version").output().ok()? + } }; #[cfg(not(target_os = "windows"))] let output = crate::silent_cmd(path).arg("--version").output().ok()?; @@ -789,6 +811,30 @@ mod tests { std::fs::remove_dir_all(root).unwrap(); } + #[test] + fn windows_executable_selection_prefers_native_wrapper() { + let output = b"C:\\nvm4w\\nodejs\\pnpm\r\nC:\\nvm4w\\nodejs\\pnpm.cmd\r\n"; + assert_eq!( + select_executable_path(output).as_deref(), + Some("C:\\nvm4w\\nodejs\\pnpm.cmd") + ); + } + + #[cfg(target_os = "windows")] + #[test] + fn probes_windows_cmd_wrapper_with_spaces_in_path() { + let root = fixture().join("path with spaces"); + std::fs::create_dir_all(&root).unwrap(); + let wrapper = root.join("tool.cmd"); + std::fs::write(&wrapper, "@echo 9.15.0\r\n").unwrap(); + + assert_eq!( + executable_version(wrapper.to_str().unwrap()).as_deref(), + Some("9.15.0") + ); + std::fs::remove_dir_all(root.parent().unwrap()).unwrap(); + } + fn probe(version: Option<&str>, executable: Option<&str>) -> RuntimeProbe { RuntimeProbe { version: version.map(str::to_string),