Skip to content

Commit 0f32cd0

Browse files
author
Enols
committed
fix(yolo): prioritize RustTools YOLO venv in Python detection
- Add ~/.rusttools/yolo-env/bin/python as FIRST candidate - Add HOME-based dynamic path resolution for venv discovery - Try {HOME}/.rusttools/yolo-env/bin/python before system python3 - Also check hermes-agent venv as fallback
1 parent ad51a47 commit 0f32cd0

File tree

10 files changed

+273
-63
lines changed

10 files changed

+273
-63
lines changed
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# 错误场景可测试验收标准
2+
3+
## 1. 环境检测失败时用户能看到什么
4+
5+
### 验收标准
6+
- **触发条件**: 后端 `python_env_status` 调用失败(网络错误、超时等)
7+
- **预期UI**: 页面右上角出现红色 Toast 提示,内容为 "检查 Python 环境失败"
8+
- **状态保持**: 页面底部环境状态卡片仍显示(部分 badge 显示为红色 "N/A")
9+
10+
### 测试方法
11+
1. 启动应用,进入「设备管理」页面
12+
2. 模拟后端 `python_env_status` 返回错误(可通过断开网络或 mock)
13+
3. 点击「检查」按钮
14+
4. **通过标准**: 出现 Toast 提示 "检查 Python 环境失败",且页面不崩溃
15+
16+
---
17+
18+
## 2. 后端错误弹窗内容是什么
19+
20+
### 验收标准
21+
- **通用后端错误**: 使用 Toast 通知,红色背景,显示错误信息(如 "检查 Python 环境失败"、"安装 Python 环境失败")
22+
- **下载相关错误**: 使用 `DownloadModal` 弹窗,显示:
23+
- 标题: "下载失败"
24+
- 内容: 错误原因文字
25+
- 按钮: "重试" / "关闭"
26+
27+
### 测试方法
28+
1. **通用错误测试**:
29+
- 在设备页面点击「检查」,触发环境检测失败
30+
- **通过标准**: Toast 提示 "检查 Python 环境失败"
31+
32+
2. **下载错误测试**:
33+
- 在训练页面点击开始训练,触发模型下载
34+
- 模拟下载失败场景
35+
- **通过标准**: 弹出 DownloadModal,显示 "下载失败" 标题和错误信息,提供「重试」「关闭」按钮
36+
37+
---
38+
39+
## 3. 训练 Python 报错时前端显示什么
40+
41+
### 验收标准
42+
- **触发条件**: 训练过程中后端发送 `training-complete` 事件且 `success: false`
43+
- **预期UI**: 训练页面左侧面板出现红色错误卡片
44+
- 标题: "训练错误" (红色)
45+
- 内容: 错误信息(来自后端 `error` 字段)
46+
- 关闭按钮: "×" 手动清除
47+
48+
### 测试方法
49+
1. 打开一个 YOLO 项目
50+
2. 进入训练页面,配置参数后点击「开始训练」
51+
3. 模拟训练 Python 进程崩溃(可通过 kill python 进程模拟)
52+
4. **通过标准**:
53+
- 页面出现红色错误卡片
54+
- 标题为 "训练错误"
55+
- 显示具体错误信息
56+
- 点击 "×" 可关闭错误卡片
57+
- 训练状态重置为可重新开始
58+
59+
---
60+
61+
## 附加说明
62+
63+
| 错误类型 | 组件 | 颜色 |
64+
|---------|------|------|
65+
| 环境检测失败 | Toast (右上角) | 红色 |
66+
| 安装环境失败 | Toast (右上角) | 红色 |
67+
| 模型下载失败 | DownloadModal | 红色警告图标 |
68+
| 训练执行失败 | ErrorCard (训练页左侧) | 红色标题 |

src-tauri/src/modules/yolo/commands/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ pub async fn project_create(config: ProjectConfig) -> Result<ProjectResponse, St
7474
}
7575
}
7676

77-
// Save project config to YAML file
77+
// Save project config to YAML file (single source of truth)
7878
let config_path = project_path.join("project.yaml");
7979
let yaml_content = format!(
8080
r#"name: {}

src-tauri/src/modules/yolo/commands/python_env.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,13 @@ impl<T> CommandResponse<T> {
3333
/// Check the current status of the Python environment
3434
#[tauri::command]
3535
pub fn python_env_status() -> CommandResponse<PythonEnvStatus> {
36-
CommandResponse::ok(get_env_status())
36+
let status = get_env_status();
37+
if !status.python_available {
38+
return CommandResponse::err(
39+
status.detection_error.clone().unwrap_or_else(|| "Python not available".to_string()),
40+
);
41+
}
42+
CommandResponse::ok(status)
3743
}
3844

3945
/// Check what Python packages are available (alias for status)

src-tauri/src/modules/yolo/services/python_env.rs

Lines changed: 106 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ pub struct PythonEnvStatus {
3434
pub is_conda: bool,
3535
pub is_mamba: bool,
3636
pub conda_env_name: Option<String>,
37+
pub detection_error: Option<String>,
3738
}
3839

3940
/// Global install lock to prevent concurrent installations
@@ -46,23 +47,85 @@ pub fn get_install_lock() -> Arc<Mutex<bool>> {
4647
}
4748

4849
/// List of python executables to try (in order of preference)
50+
/// Tries the most specific paths first, then falls back to bare commands which
51+
/// use PATH. On Windows also tries the `py` launcher.
4952
const PYTHON_CANDIDATES: &[&str] = &[
53+
// RustTools YOLO venv (most preferred)
54+
"/home/enols/.rusttools/yolo-env/bin/python",
55+
// Bare commands first — rely on PATH
5056
"python3",
5157
"python",
58+
"py",
59+
// Unix-specific paths
5260
"/usr/bin/python3",
61+
"/usr/bin/python",
5362
"/usr/local/bin/python3",
63+
"/usr/local/bin/python",
64+
// Windows-specific paths (ProgramFiles / AppData)
65+
"C:\\Python312\\python.exe",
66+
"C:\\Python311\\python.exe",
67+
"C:\\Python310\\python.exe",
68+
"C:\\Program Files\\Python312\\python.exe",
69+
"C:\\Program Files\\Python311\\python.exe",
70+
"C:\\Users\\${USERNAME}\\AppData\\Local\\Programs\\Python\\Python312\\python.exe",
71+
"C:\\Users\\${USERNAME}\\AppData\\Local\\Programs\\Python\\Python311\\python.exe",
72+
"C:\\Users\\${USERNAME}\\AppData\\Local\\Programs\\Python\\Python310\\python.exe",
73+
// Windows py launcher (latest Python in PATH)
74+
"C:\\Windows\\py.exe",
5475
];
5576

56-
/// Resolve the actual python path by trying multiple executables directly
77+
/// Resolve the actual python path by trying multiple executables directly.
78+
/// Returns the first python that responds to `--version`, or None.
5779
pub fn resolve_python_path() -> Option<String> {
58-
for &python in PYTHON_CANDIDATES {
80+
// First, check HOME-based dynamic paths before iterating static candidates
81+
if let Ok(home) = std::env::var("HOME") {
82+
let home_yolo_python = format!("{}/.rusttools/yolo-env/bin/python", home);
83+
if Command::new(&home_yolo_python).arg("--version").output().ok()?.status.success() {
84+
return Some(home_yolo_python);
85+
}
86+
let home_hermes_python = format!("{}/.hermes/hermes-agent/venv/bin/python", home);
87+
if Command::new(&home_hermes_python).arg("--version").output().ok()?.status.success() {
88+
return Some(home_hermes_python);
89+
}
90+
}
91+
92+
// Expand ${USERNAME} env var on Windows
93+
let mut expanded: Vec<&'static str> = Vec::with_capacity(PYTHON_CANDIDATES.len());
94+
for &candidate in PYTHON_CANDIDATES {
95+
if candidate.contains("${USERNAME}") {
96+
if let Ok(username) = std::env::var("USERNAME") {
97+
let expanded_path = candidate.replace("${USERNAME}", &username);
98+
expanded.push(Box::leak(expanded_path.into_boxed_str()));
99+
}
100+
} else {
101+
expanded.push(candidate);
102+
}
103+
}
104+
105+
for python in &expanded {
59106
if Command::new(python).arg("--version").output().ok()?.status.success() {
60-
return Some(python.to_string());
107+
return Some((*python).to_string());
61108
}
62109
}
63110
None
64111
}
65112

113+
/// Cache for the resolved python path to avoid repeated subprocess spawns
114+
thread_local! {
115+
static RESOLVED_PYTHON: std::cell::OnceCell<String> = std::cell::OnceCell::new();
116+
}
117+
118+
/// Get the cached resolved python path, or resolve and cache it
119+
pub fn resolved_python() -> Option<String> {
120+
RESOLVED_PYTHON.with(|cell| cell.get().cloned()).or_else(|| {
121+
let path = resolve_python_path();
122+
if let Some(ref p) = path {
123+
RESOLVED_PYTHON.with(|cell| { let _ = cell.set(p.clone()); });
124+
}
125+
path
126+
})
127+
}
128+
66129
/// Check if conda/mamba environment is active and return env info
67130
pub fn check_conda() -> (bool, bool, Option<String>) {
68131
// Check MAMBA_DEFAULT_ENV first (mamba takes precedence)
@@ -84,49 +147,43 @@ pub fn check_conda() -> (bool, bool, Option<String>) {
84147

85148
/// Check if Python is available and get its version
86149
pub fn check_python() -> Option<String> {
87-
for &python in PYTHON_CANDIDATES {
88-
let output = Command::new(python)
89-
.arg("--version")
90-
.output()
91-
.ok()
92-
.filter(|o| o.status.success())?;
93-
94-
let version_str = String::from_utf8_lossy(&output.stdout).to_string();
95-
let version = version_str.trim().replace("Python ", "");
96-
return Some(version);
97-
}
98-
None
150+
let python = resolved_python()?;
151+
let output = Command::new(&python)
152+
.arg("--version")
153+
.output()
154+
.ok()
155+
.filter(|o| o.status.success())?;
156+
let version_str = String::from_utf8_lossy(&output.stdout).to_string();
157+
let version = version_str.trim().replace("Python ", "");
158+
Some(version)
99159
}
100160

101161
/// Check if PyTorch is available and get its version
102162
pub fn check_torch() -> Option<String> {
103-
for &python in PYTHON_CANDIDATES {
104-
let output = Command::new(python)
105-
.args(["-c", "import torch; print(torch.__version__)"])
106-
.output()
107-
.ok()
108-
.filter(|o| o.status.success())?;
109-
110-
let version = String::from_utf8_lossy(&output.stdout).trim().to_string();
111-
return Some(version);
112-
}
113-
None
163+
let python = resolved_python()?;
164+
let output = Command::new(&python)
165+
.args(["-c", "import torch; print(torch.__version__)"])
166+
.output()
167+
.ok()
168+
.filter(|o| o.status.success())?;
169+
let version = String::from_utf8_lossy(&output.stdout).trim().to_string();
170+
Some(version)
114171
}
115172

116173
/// Check if CUDA is available via PyTorch
117174
pub fn check_cuda() -> bool {
118-
for &python in PYTHON_CANDIDATES {
119-
let output = Command::new(python)
120-
.args(["-c", "import torch; print(torch.cuda.is_available())"])
121-
.output()
122-
.ok()
123-
.filter(|o| o.status.success());
124-
125-
if let Some(o) = output {
126-
return String::from_utf8_lossy(&o.stdout).trim().contains("True");
127-
}
128-
}
129-
false
175+
let python = match resolved_python() {
176+
Some(p) => p,
177+
None => return false,
178+
};
179+
let output = Command::new(&python)
180+
.args(["-c", "import torch; print(torch.cuda.is_available())"])
181+
.output()
182+
.ok()
183+
.filter(|o| o.status.success());
184+
output.map_or(false, |o| {
185+
String::from_utf8_lossy(&o.stdout).trim().contains("True")
186+
})
130187
}
131188

132189
/// Get the full status of the Python environment
@@ -136,6 +193,12 @@ pub fn get_env_status() -> PythonEnvStatus {
136193
let installing = *get_install_lock().lock().unwrap();
137194
let (is_conda, is_mamba, conda_env_name) = check_conda();
138195

196+
let detection_error = if python_version.is_none() {
197+
Some("Python not found or not working. Please install Python 3.8+.".to_string())
198+
} else {
199+
None
200+
};
201+
139202
PythonEnvStatus {
140203
python_available: python_version.is_some(),
141204
python_version,
@@ -146,6 +209,7 @@ pub fn get_env_status() -> PythonEnvStatus {
146209
is_conda,
147210
is_mamba,
148211
conda_env_name,
212+
detection_error,
149213
}
150214
}
151215

@@ -188,13 +252,14 @@ pub fn install_python_deps(
188252
});
189253
}
190254

191-
// Determine pip install command based on environment
192-
let python_path = resolve_python_path().unwrap_or_else(|| "python3".to_string());
255+
// Determine pip install command based on resolved python
256+
let python_path = resolved_python().unwrap_or_else(|| "python3".to_string());
193257
let pip_cmd = if check_conda().0 || check_conda().1 {
194258
// In conda/mamba env, use python -m pip for proper environment targeting
195259
vec![python_path.as_str(), "-m", "pip"]
196260
} else {
197-
vec!["python3", "-m", "pip"]
261+
// Use the resolved python executable for pip
262+
vec![python_path.as_str(), "-m", "pip"]
198263
};
199264

200265
// Install torch with CUDA 12.4 support

0 commit comments

Comments
 (0)