Skip to main content

aster/diagnostics/
health.rs

1//! 健康评分系统
2//!
3//! 提供系统健康状态评估和自动修复功能
4
5use super::checker::{CheckStatus, DiagnosticCheck};
6use super::report::DiagnosticReport;
7use serde::{Deserialize, Serialize};
8use std::path::Path;
9
10/// 健康状态
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
12pub enum HealthStatus {
13    /// 健康
14    Healthy,
15    /// 降级
16    Degraded,
17    /// 不健康
18    Unhealthy,
19}
20
21/// 健康摘要
22#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct HealthSummary {
24    /// 状态
25    pub status: HealthStatus,
26    /// 健康评分 (0-100)
27    pub score: u8,
28    /// 关键问题
29    pub critical_issues: Vec<String>,
30}
31
32impl HealthSummary {
33    /// 从诊断报告生成健康摘要
34    pub fn from_report(report: &DiagnosticReport) -> Self {
35        let total = report.checks.len();
36        let failed = report.summary.failed;
37        let warnings = report.summary.warnings;
38
39        // 计算健康评分
40        let score = if total > 0 {
41            let penalty = failed as f64 + warnings as f64 * 0.5;
42            let raw_score = ((total as f64 - penalty) / total as f64) * 100.0;
43            raw_score.clamp(0.0, 100.0) as u8
44        } else {
45            100
46        };
47
48        // 确定状态
49        let status = if score >= 90 {
50            HealthStatus::Healthy
51        } else if score >= 70 {
52            HealthStatus::Degraded
53        } else {
54            HealthStatus::Unhealthy
55        };
56
57        // 收集关键问题
58        let critical_issues: Vec<String> = report
59            .checks
60            .iter()
61            .filter(|c| c.status == CheckStatus::Fail)
62            .map(|c| format!("{}: {}", c.name, c.message))
63            .collect();
64
65        Self {
66            status,
67            score,
68            critical_issues,
69        }
70    }
71}
72
73/// 自动修复结果
74#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct AutoFixResult {
76    /// 已修复的问题
77    pub fixed: Vec<String>,
78    /// 修复失败的问题
79    pub failed: Vec<String>,
80}
81
82/// 自动修复器
83pub struct AutoFixer;
84
85impl AutoFixer {
86    /// 尝试自动修复问题
87    pub fn auto_fix(report: &DiagnosticReport) -> AutoFixResult {
88        let mut fixed = Vec::new();
89        let mut failed = Vec::new();
90
91        for check in &report.checks {
92            if check.status == CheckStatus::Fail || check.status == CheckStatus::Warn {
93                match Self::try_fix(check) {
94                    Ok(msg) => fixed.push(msg),
95                    Err(msg) => failed.push(msg),
96                }
97            }
98        }
99
100        AutoFixResult { fixed, failed }
101    }
102
103    fn try_fix(check: &DiagnosticCheck) -> Result<String, String> {
104        match check.name.as_str() {
105            "文件权限" | "会话目录" | "缓存目录" | "配置目录" => {
106                Self::fix_directory_issue(check)
107            }
108            _ => {
109                // 无法自动修复
110                if let Some(ref fix) = check.fix {
111                    Err(format!("{}: {}", check.name, fix))
112                } else {
113                    Err(format!("{}: 无法自动修复", check.name))
114                }
115            }
116        }
117    }
118
119    fn fix_directory_issue(check: &DiagnosticCheck) -> Result<String, String> {
120        // 从详情中提取路径
121        let path = check
122            .details
123            .as_ref()
124            .and_then(|d| {
125                d.strip_prefix("路径: ")
126                    .or_else(|| d.strip_prefix("Path: "))
127            })
128            .map(|s| s.trim());
129
130        if let Some(path_str) = path {
131            let path = Path::new(path_str);
132            if !path.exists() {
133                match std::fs::create_dir_all(path) {
134                    Ok(_) => Ok(format!("已创建目录: {}", path_str)),
135                    Err(e) => Err(format!("无法创建目录 {}: {}", path_str, e)),
136                }
137            } else {
138                Ok(format!("目录已存在: {}", path_str))
139            }
140        } else {
141            Err(format!("{}: 无法确定目录路径", check.name))
142        }
143    }
144}
145
146/// 快速健康检查(最小检查集)
147pub async fn quick_health_check() -> (bool, Vec<String>) {
148    let mut issues = Vec::new();
149
150    // 检查配置目录
151    let config_dir = dirs::config_dir()
152        .map(|p| p.join("aster"))
153        .unwrap_or_else(|| std::path::PathBuf::from("~/.config/aster"));
154
155    if !config_dir.exists() && std::fs::create_dir_all(&config_dir).is_err() {
156        issues.push("无法创建配置目录".to_string());
157    }
158
159    // 检查环境变量
160    let has_api_key =
161        std::env::var("ANTHROPIC_API_KEY").is_ok() || std::env::var("OPENAI_API_KEY").is_ok();
162
163    if !has_api_key {
164        issues.push("未配置 API 密钥".to_string());
165    }
166
167    (issues.is_empty(), issues)
168}
169
170/// 获取系统健康摘要
171pub async fn get_system_health_summary() -> HealthSummary {
172    use super::report::{DiagnosticOptions, DiagnosticReport};
173
174    let options = DiagnosticOptions::default();
175    let report = DiagnosticReport::generate(&options);
176
177    HealthSummary::from_report(&report)
178}
179
180#[cfg(test)]
181mod tests {
182    use super::*;
183    use crate::diagnostics::report::{DiagnosticReport, ReportSummary};
184
185    fn create_test_report(passed: usize, warnings: usize, failed: usize) -> DiagnosticReport {
186        let mut checks = Vec::new();
187
188        for i in 0..passed {
189            checks.push(DiagnosticCheck::pass(format!("Pass{}", i), "通过"));
190        }
191        for i in 0..warnings {
192            checks.push(DiagnosticCheck::warn(format!("Warn{}", i), "警告"));
193        }
194        for i in 0..failed {
195            checks.push(DiagnosticCheck::fail(format!("Fail{}", i), "失败"));
196        }
197
198        DiagnosticReport {
199            timestamp: chrono::Utc::now().timestamp(),
200            version: "test".to_string(),
201            platform: "test".to_string(),
202            checks,
203            summary: ReportSummary {
204                passed,
205                warnings,
206                failed,
207            },
208            system_info: None,
209        }
210    }
211
212    #[test]
213    fn test_health_summary_healthy() {
214        let report = create_test_report(10, 0, 0);
215        let summary = HealthSummary::from_report(&report);
216
217        assert_eq!(summary.status, HealthStatus::Healthy);
218        assert_eq!(summary.score, 100);
219        assert!(summary.critical_issues.is_empty());
220    }
221
222    #[test]
223    fn test_health_summary_degraded() {
224        let report = create_test_report(7, 3, 0);
225        let summary = HealthSummary::from_report(&report);
226
227        assert_eq!(summary.status, HealthStatus::Degraded);
228        assert!(summary.score >= 70 && summary.score < 90);
229    }
230
231    #[test]
232    fn test_health_summary_unhealthy() {
233        let report = create_test_report(3, 2, 5);
234        let summary = HealthSummary::from_report(&report);
235
236        assert_eq!(summary.status, HealthStatus::Unhealthy);
237        assert!(summary.score < 70);
238        assert_eq!(summary.critical_issues.len(), 5);
239    }
240
241    #[test]
242    fn test_health_summary_empty_report() {
243        let report = create_test_report(0, 0, 0);
244        let summary = HealthSummary::from_report(&report);
245
246        assert_eq!(summary.status, HealthStatus::Healthy);
247        assert_eq!(summary.score, 100);
248    }
249
250    #[test]
251    fn test_auto_fixer_no_issues() {
252        let report = create_test_report(5, 0, 0);
253        let result = AutoFixer::auto_fix(&report);
254
255        assert!(result.fixed.is_empty());
256        assert!(result.failed.is_empty());
257    }
258
259    #[test]
260    fn test_auto_fixer_with_directory_issue() {
261        let temp_path = std::env::temp_dir().join("aster_autofix_test");
262        let _ = std::fs::remove_dir_all(&temp_path);
263
264        let check = DiagnosticCheck::fail("会话目录", "目录不存在")
265            .with_details(format!("路径: {}", temp_path.display()));
266
267        let report = DiagnosticReport {
268            timestamp: chrono::Utc::now().timestamp(),
269            version: "test".to_string(),
270            platform: "test".to_string(),
271            checks: vec![check],
272            summary: ReportSummary {
273                passed: 0,
274                warnings: 0,
275                failed: 1,
276            },
277            system_info: None,
278        };
279
280        let result = AutoFixer::auto_fix(&report);
281
282        // 应该能修复目录问题
283        assert!(!result.fixed.is_empty() || !result.failed.is_empty());
284
285        let _ = std::fs::remove_dir_all(&temp_path);
286    }
287
288    #[tokio::test]
289    async fn test_quick_health_check() {
290        let (healthy, issues) = quick_health_check().await;
291        // 函数应该能运行
292        assert!(healthy || !issues.is_empty());
293    }
294
295    #[tokio::test]
296    async fn test_get_system_health_summary() {
297        let summary = get_system_health_summary().await;
298        // 应该返回有效的健康摘要
299        assert!(summary.score <= 100);
300    }
301}