Skip to main content

aster/diagnostics/
checker.rs

1//! 诊断检查器
2//!
3//! 提供各种系统检查功能
4
5use serde::{Deserialize, Serialize};
6use std::process::Command;
7
8/// 检查状态
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
10pub enum CheckStatus {
11    /// 通过
12    Pass,
13    /// 警告
14    Warn,
15    /// 失败
16    Fail,
17}
18
19/// 诊断检查结果
20#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct DiagnosticCheck {
22    /// 检查名称
23    pub name: String,
24    /// 检查状态
25    pub status: CheckStatus,
26    /// 消息
27    pub message: String,
28    /// 详细信息
29    pub details: Option<String>,
30    /// 修复建议
31    pub fix: Option<String>,
32}
33
34impl DiagnosticCheck {
35    /// 创建通过的检查结果
36    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    /// 创建警告的检查结果
47    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    /// 创建失败的检查结果
58    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    /// 添加详细信息
69    pub fn with_details(mut self, details: impl Into<String>) -> Self {
70        self.details = Some(details.into());
71        self
72    }
73
74    /// 添加修复建议
75    pub fn with_fix(mut self, fix: impl Into<String>) -> Self {
76        self.fix = Some(fix.into());
77        self
78    }
79}
80
81/// 诊断检查器
82pub struct DiagnosticChecker;
83
84impl DiagnosticChecker {
85    /// 检查 Git 可用性
86    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    /// 检查 Ripgrep 可用性
99    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    /// 检查磁盘空间
116    pub fn check_disk_space(path: &std::path::Path) -> DiagnosticCheck {
117        #[cfg(unix)]
118        {
119            if std::fs::metadata(path).is_ok() {
120                // 简化检查,实际应使用 statvfs
121                DiagnosticCheck::pass("磁盘空间", "磁盘空间检查通过")
122            } else {
123                DiagnosticCheck::warn("磁盘空间", "无法检查磁盘空间")
124            }
125        }
126        #[cfg(not(unix))]
127        {
128            let _ = path;
129            DiagnosticCheck::pass("磁盘空间", "磁盘空间检查跳过")
130        }
131    }
132
133    /// 检查文件权限
134    pub fn check_file_permissions(path: &std::path::Path) -> DiagnosticCheck {
135        if !path.exists() {
136            // 尝试创建目录
137            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        // 尝试写入测试文件
145        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    /// 检查内存使用
157    pub fn check_memory_usage() -> DiagnosticCheck {
158        #[cfg(target_os = "macos")]
159        {
160            // macOS 使用 sysctl
161            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    /// 检查网络连接
220    pub async fn check_network() -> DiagnosticCheck {
221        // 简单的网络检查
222        DiagnosticCheck::pass("网络", "网络检查需要异步运行时")
223    }
224
225    /// 检查环境变量
226    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    /// 检查配置目录
254    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
263/// 运行所有诊断检查
264pub fn run_diagnostics() -> Vec<DiagnosticCheck> {
265    use super::network::NetworkChecker;
266    use super::system::SystemChecker;
267
268    vec![
269        // 环境检查
270        DiagnosticChecker::check_git(),
271        DiagnosticChecker::check_ripgrep(),
272        // 系统检查
273        DiagnosticChecker::check_memory_usage(),
274        SystemChecker::check_cpu_load(),
275        // 配置检查
276        DiagnosticChecker::check_environment_variables(),
277        DiagnosticChecker::check_config_directory(),
278        SystemChecker::check_mcp_servers(),
279        // 目录检查
280        SystemChecker::check_session_directory(),
281        SystemChecker::check_cache_directory(),
282        // 网络检查
283        NetworkChecker::check_proxy_configuration(),
284        NetworkChecker::check_ssl_certificates(),
285    ]
286}
287
288/// 运行所有诊断检查(包括异步检查)
289#[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        // 环境检查
296        DiagnosticChecker::check_git(),
297        DiagnosticChecker::check_ripgrep(),
298        // 系统检查
299        DiagnosticChecker::check_memory_usage(),
300        SystemChecker::check_cpu_load(),
301        // 配置检查
302        DiagnosticChecker::check_environment_variables(),
303        DiagnosticChecker::check_config_directory(),
304        SystemChecker::check_mcp_servers(),
305        // 目录检查
306        SystemChecker::check_session_directory(),
307        SystemChecker::check_cache_directory(),
308        // 网络检查(同步)
309        NetworkChecker::check_proxy_configuration(),
310        NetworkChecker::check_ssl_certificates(),
311    ];
312
313    // 异步网络检查
314    checks.push(NetworkChecker::check_api_connectivity().await);
315    checks.push(NetworkChecker::check_network_connectivity().await);
316
317    checks
318}
319
320// quick_health_check 已移至 health.rs
321
322#[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        // 应该能创建或已存在
354        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        // 至少应该有环境检查
385        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        // 临时目录应该可写
395        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        // 只验证函数能运行,不关心结果
403        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        // 异步版本应该包含网络检查
411        assert!(checks.len() >= run_diagnostics().len());
412    }
413}