feat(ai): Copilot 评分引擎 + Handler + 路由 + 权限码

- scoring.rs: 混合评分 (calculate_risk) + RiskScore/MatchedRule 结构
- engine.rs: CopilotEngine 协调规则评估和评分
- risk_service.rs: 风险计算 + UPSERT 快照 + 规则加载
- insight_service.rs: 洞察 CRUD + 过期清理
- 3 个 Handler: insight/risk/rule,7 个 API 端点
- 5 个权限码: copilot.insights.list/manage, copilot.risk.view, copilot.rules.list/manage
- AiState 扩展 risk_service + insight_service
This commit is contained in:
iven
2026-05-12 12:14:16 +08:00
parent fe983ba4ae
commit 57f33dd726
15 changed files with 698 additions and 0 deletions

View File

@@ -0,0 +1,14 @@
use crate::copilot::rules::{RuleData, evaluate_rules};
use crate::copilot::scoring::{RiskScore, calculate_risk};
use serde_json::Value;
/// Copilot 引擎:协调规则评估和评分
pub struct CopilotEngine;
impl CopilotEngine {
/// 对患者数据运行所有规则并生成风险评分
pub fn assess_patient(rules: &[RuleData], patient_data: &Value) -> RiskScore {
let matched = evaluate_rules(rules, patient_data);
calculate_risk(matched)
}
}

View File

@@ -1 +1,3 @@
pub mod engine;
pub mod rules;
pub mod scoring;

View File

@@ -0,0 +1,45 @@
use crate::copilot::rules::MatchedRuleData;
/// 风险评分结果
#[derive(Debug, Clone, serde::Serialize)]
pub struct RiskScore {
pub score: i16,
pub level: String,
pub matched_rules: Vec<MatchedRule>,
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct MatchedRule {
pub rule_id: uuid::Uuid,
pub name: String,
pub score: i16,
pub severity: String,
pub suggestion: Option<String>,
}
/// 根据匹配规则计算风险评分
pub fn calculate_risk(matched: Vec<MatchedRuleData>) -> RiskScore {
let total: i16 = matched.iter().map(|(_, _, s, _, _)| *s).sum();
let clamped = total.clamp(0, 10);
let level = match clamped {
0..=2 => "low".to_string(),
3..=5 => "medium".to_string(),
6..=8 => "high".to_string(),
_ => "critical".to_string(),
};
let matched_rules = matched
.into_iter()
.map(|(id, name, score, severity, suggestion)| MatchedRule {
rule_id: id,
name,
score,
severity,
suggestion,
})
.collect();
RiskScore {
score: clamped,
level,
matched_rules,
}
}