1use serde::{Deserialize, Serialize};
6use std::process::Command;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
10pub enum CheckStatus {
11 Pass,
13 Warn,
15 Fail,
17}
18
19#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct DiagnosticCheck {
22 pub name: String,
24 pub status: CheckStatus,
26 pub message: String,
28 pub details: Option<String>,
30 pub fix: Option<String>,
32}
33
34impl DiagnosticCheck {
35 pub fn pass(name: impl Into<String>, message: impl Into<String>) -> Self {
37 Self {
38 name: name.into(),
39 status: CheckStatus::Pass,
40 message: message.into(),
41 details: None,
42 fix: None,
43 }
44 }
45
46 pub fn warn(name: impl Into<String>, message: impl Into<String>) -> Self {
48 Self {
49 name: name.into(),
50 status: CheckStatus::Warn,
51 message: message.into(),
52 details: None,
53 fix: None,
54 }
55 }
56
57 pub fn fail(name: impl Into<String>, message: impl Into<String>) -> Self {
59 Self {
60 name: name.into(),
61 status: CheckStatus::Fail,
62 message: message.into(),
63 details: None,
64 fix: None,
65 }
66 }
67
68 pub fn with_details(mut self, details: impl Into<String>) -> Self {
70 self.details = Some(details.into());
71 self
72 }
73
74 pub fn with_fix(mut self, fix: impl Into<String>) -> Self {
76 self.fix = Some(fix.into());
77 self
78 }
79}
80
81pub struct DiagnosticChecker;
83
84impl DiagnosticChecker {
85 pub fn check_git() -> DiagnosticCheck {
87 match Command::new("git").arg("--version").output() {
88 Ok(output) if output.status.success() => {
89 let version = String::from_utf8_lossy(&output.stdout).trim().to_string();
90 DiagnosticCheck::pass("Git", version)
91 }
92 _ => DiagnosticCheck::warn("Git", "Git 未找到")
93 .with_details("部分功能可能无法使用")
94 .with_fix("请安装 Git: https://git-scm.com/"),
95 }
96 }
97
98 pub fn check_ripgrep() -> DiagnosticCheck {
100 match Command::new("rg").arg("--version").output() {
101 Ok(output) if output.status.success() => {
102 let version = String::from_utf8_lossy(&output.stdout)
103 .lines()
104 .next()
105 .unwrap_or("unknown")
106 .to_string();
107 DiagnosticCheck::pass("Ripgrep", version)
108 }
109 _ => DiagnosticCheck::warn("Ripgrep", "Ripgrep 未找到")
110 .with_details("文件搜索将使用备用方案")
111 .with_fix("安装 ripgrep: https://github.com/BurntSushi/ripgrep"),
112 }
113 }
114
115 pub fn check_disk_space(path: &std::path::Path) -> DiagnosticCheck {
117 #[cfg(unix)]
118 {
119 if std::fs::metadata(path).is_ok() {
120 DiagnosticCheck::pass("磁盘空间", "磁盘空间检查通过")
122 } else {
123 DiagnosticCheck::warn("磁盘空间", "无法检查磁盘空间")
124 }
125 }
126 #[cfg(not(unix))]
127 {
128 let _ = path;
129 DiagnosticCheck::pass("磁盘空间", "磁盘空间检查跳过")
130 }
131 }
132
133 pub fn check_file_permissions(path: &std::path::Path) -> DiagnosticCheck {
135 if !path.exists() {
136 if std::fs::create_dir_all(path).is_ok() {
138 return DiagnosticCheck::pass("文件权限", "目录已创建");
139 }
140 return DiagnosticCheck::fail("文件权限", "无法创建目录")
141 .with_details(format!("路径: {}", path.display()));
142 }
143
144 let test_file = path.join(".write-test");
146 match std::fs::write(&test_file, "test") {
147 Ok(_) => {
148 let _ = std::fs::remove_file(&test_file);
149 DiagnosticCheck::pass("文件权限", "文件权限正常")
150 }
151 Err(e) => DiagnosticCheck::fail("文件权限", "无法写入目录")
152 .with_details(format!("错误: {}", e)),
153 }
154 }
155
156 pub fn check_memory_usage() -> DiagnosticCheck {
158 #[cfg(target_os = "macos")]
159 {
160 match Command::new("sysctl").args(["-n", "hw.memsize"]).output() {
162 Ok(output) if output.status.success() => {
163 let total_str = String::from_utf8_lossy(&output.stdout).trim().to_string();
164 if let Ok(total) = total_str.parse::<u64>() {
165 let total_gb = total as f64 / (1024.0 * 1024.0 * 1024.0);
166 DiagnosticCheck::pass("内存", format!("总内存: {:.1} GB", total_gb))
167 } else {
168 DiagnosticCheck::pass("内存", "内存检查通过")
169 }
170 }
171 _ => DiagnosticCheck::warn("内存", "无法检查内存"),
172 }
173 }
174 #[cfg(target_os = "linux")]
175 {
176 if let Ok(content) = std::fs::read_to_string("/proc/meminfo") {
177 let mut total_kb = 0u64;
178 let mut available_kb = 0u64;
179 for line in content.lines() {
180 if line.starts_with("MemTotal:") {
181 total_kb = line
182 .split_whitespace()
183 .nth(1)
184 .and_then(|s| s.parse().ok())
185 .unwrap_or(0);
186 } else if line.starts_with("MemAvailable:") {
187 available_kb = line
188 .split_whitespace()
189 .nth(1)
190 .and_then(|s| s.parse().ok())
191 .unwrap_or(0);
192 }
193 }
194 let total_gb = total_kb as f64 / (1024.0 * 1024.0);
195 let used_percent = if total_kb > 0 {
196 ((total_kb - available_kb) as f64 / total_kb as f64) * 100.0
197 } else {
198 0.0
199 };
200
201 if used_percent >= 90.0 {
202 DiagnosticCheck::warn("内存", format!("内存使用率高: {:.1}%", used_percent))
203 } else {
204 DiagnosticCheck::pass(
205 "内存",
206 format!("{:.1}% ({:.1} GB)", used_percent, total_gb),
207 )
208 }
209 } else {
210 DiagnosticCheck::warn("内存", "无法检查内存")
211 }
212 }
213 #[cfg(not(any(target_os = "macos", target_os = "linux")))]
214 {
215 DiagnosticCheck::pass("内存", "内存检查跳过")
216 }
217 }
218
219 pub async fn check_network() -> DiagnosticCheck {
221 DiagnosticCheck::pass("网络", "网络检查需要异步运行时")
223 }
224
225 pub fn check_environment_variables() -> DiagnosticCheck {
227 let relevant_vars = [
228 "ANTHROPIC_API_KEY",
229 "OPENAI_API_KEY",
230 "ASTER_CONFIG_DIR",
231 "ASTER_LOG_LEVEL",
232 ];
233
234 let set_vars: Vec<_> = relevant_vars
235 .iter()
236 .filter(|v| std::env::var(v).is_ok())
237 .collect();
238
239 if set_vars.is_empty() {
240 DiagnosticCheck::pass("环境变量", "使用默认配置")
241 } else {
242 DiagnosticCheck::pass("环境变量", format!("已设置 {} 个变量", set_vars.len()))
243 .with_details(
244 set_vars
245 .iter()
246 .map(|v| v.to_string())
247 .collect::<Vec<_>>()
248 .join(", "),
249 )
250 }
251 }
252
253 pub fn check_config_directory() -> DiagnosticCheck {
255 let config_dir = dirs::config_dir()
256 .map(|p| p.join("aster"))
257 .unwrap_or_else(|| std::path::PathBuf::from("~/.config/aster"));
258
259 Self::check_file_permissions(&config_dir)
260 }
261}
262
263pub fn run_diagnostics() -> Vec<DiagnosticCheck> {
265 use super::network::NetworkChecker;
266 use super::system::SystemChecker;
267
268 vec![
269 DiagnosticChecker::check_git(),
271 DiagnosticChecker::check_ripgrep(),
272 DiagnosticChecker::check_memory_usage(),
274 SystemChecker::check_cpu_load(),
275 DiagnosticChecker::check_environment_variables(),
277 DiagnosticChecker::check_config_directory(),
278 SystemChecker::check_mcp_servers(),
279 SystemChecker::check_session_directory(),
281 SystemChecker::check_cache_directory(),
282 NetworkChecker::check_proxy_configuration(),
284 NetworkChecker::check_ssl_certificates(),
285 ]
286}
287
288#[allow(dead_code)]
290pub async fn run_diagnostics_async() -> Vec<DiagnosticCheck> {
291 use super::network::NetworkChecker;
292 use super::system::SystemChecker;
293
294 let mut checks = vec![
295 DiagnosticChecker::check_git(),
297 DiagnosticChecker::check_ripgrep(),
298 DiagnosticChecker::check_memory_usage(),
300 SystemChecker::check_cpu_load(),
301 DiagnosticChecker::check_environment_variables(),
303 DiagnosticChecker::check_config_directory(),
304 SystemChecker::check_mcp_servers(),
305 SystemChecker::check_session_directory(),
307 SystemChecker::check_cache_directory(),
308 NetworkChecker::check_proxy_configuration(),
310 NetworkChecker::check_ssl_certificates(),
311 ];
312
313 checks.push(NetworkChecker::check_api_connectivity().await);
315 checks.push(NetworkChecker::check_network_connectivity().await);
316
317 checks
318}
319
320#[cfg(test)]
323mod tests {
324 use super::*;
325
326 #[test]
327 fn test_check_git() {
328 let result = DiagnosticChecker::check_git();
329 assert!(result.status == CheckStatus::Pass || result.status == CheckStatus::Warn);
330 }
331
332 #[test]
333 fn test_check_ripgrep() {
334 let result = DiagnosticChecker::check_ripgrep();
335 assert!(result.status == CheckStatus::Pass || result.status == CheckStatus::Warn);
336 }
337
338 #[test]
339 fn test_check_environment_variables() {
340 let result = DiagnosticChecker::check_environment_variables();
341 assert_eq!(result.status, CheckStatus::Pass);
342 }
343
344 #[test]
345 fn test_check_memory_usage() {
346 let result = DiagnosticChecker::check_memory_usage();
347 assert!(result.status == CheckStatus::Pass || result.status == CheckStatus::Warn);
348 }
349
350 #[test]
351 fn test_check_config_directory() {
352 let result = DiagnosticChecker::check_config_directory();
353 assert!(result.status == CheckStatus::Pass || result.status == CheckStatus::Fail);
355 }
356
357 #[test]
358 fn test_diagnostic_check_pass() {
359 let check = DiagnosticCheck::pass("Test", "通过");
360 assert_eq!(check.status, CheckStatus::Pass);
361 assert_eq!(check.name, "Test");
362 }
363
364 #[test]
365 fn test_diagnostic_check_warn() {
366 let check = DiagnosticCheck::warn("Test", "警告")
367 .with_details("详情")
368 .with_fix("修复建议");
369 assert_eq!(check.status, CheckStatus::Warn);
370 assert!(check.details.is_some());
371 assert!(check.fix.is_some());
372 }
373
374 #[test]
375 fn test_diagnostic_check_fail() {
376 let check = DiagnosticCheck::fail("Test", "失败");
377 assert_eq!(check.status, CheckStatus::Fail);
378 }
379
380 #[test]
381 fn test_run_diagnostics() {
382 let checks = run_diagnostics();
383 assert!(!checks.is_empty());
384 assert!(checks
386 .iter()
387 .any(|c| c.name == "Git" || c.name == "Ripgrep"));
388 }
389
390 #[test]
391 fn test_check_file_permissions() {
392 let temp_dir = std::env::temp_dir().join("aster_test_perms");
393 let result = DiagnosticChecker::check_file_permissions(&temp_dir);
394 assert!(result.status == CheckStatus::Pass || result.status == CheckStatus::Fail);
396 let _ = std::fs::remove_dir_all(&temp_dir);
397 }
398
399 #[tokio::test]
400 async fn test_quick_health_check() {
401 let (healthy, _issues) = crate::diagnostics::quick_health_check().await;
402 let _ = healthy;
404 }
405
406 #[tokio::test]
407 async fn test_run_diagnostics_async() {
408 let checks = run_diagnostics_async().await;
409 assert!(!checks.is_empty());
410 assert!(checks.len() >= run_diagnostics().len());
412 }
413}