Files
hms/crates/erp-ai/src/module.rs
iven 0a5290aee4
Some checks failed
CI / rust-check (push) Has been cancelled
CI / rust-test (push) Has been cancelled
CI / frontend-build (push) Has been cancelled
CI / security-audit (push) Has been cancelled
feat(ai): KDIGO 透析专用风险评分器 — Phase 1 关怀引擎 MVP 第二步
新增 DialysisRiskScorer:12 条 KDIGO 规则覆盖 Kt/V、血磷、血钾、血红蛋白、
体重增长、eGFR、白蛋白,含 KDIGO CKD G1-G5 分期。同步暴露
POST /ai/dialysis/risk-assessment 端点。76 个测试全部通过。
2026-05-04 18:44:22 +08:00

236 lines
8.7 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use async_trait::async_trait;
use axum::Router;
use erp_core::module::{ErpModule, ModuleType, PermissionDescriptor};
use std::any::Any;
pub struct AiModule;
#[async_trait]
impl ErpModule for AiModule {
fn name(&self) -> &str {
"ai"
}
fn module_type(&self) -> ModuleType {
ModuleType::Builtin
}
fn dependencies(&self) -> Vec<&str> {
vec!["health"]
}
fn permissions(&self) -> Vec<PermissionDescriptor> {
vec![
PermissionDescriptor {
code: "ai.analysis.list".into(),
name: "查看分析历史".into(),
description: "查看 AI 分析结果历史记录".into(),
module: "ai".into(),
},
PermissionDescriptor {
code: "ai.analysis.manage".into(),
name: "请求分析".into(),
description: "发起 AI 分析请求".into(),
module: "ai".into(),
},
PermissionDescriptor {
code: "ai.prompt.list".into(),
name: "查看 Prompt".into(),
description: "查看 AI Prompt 模板列表".into(),
module: "ai".into(),
},
PermissionDescriptor {
code: "ai.prompt.manage".into(),
name: "管理 Prompt".into(),
description: "创建/编辑/激活/回滚 Prompt 模板".into(),
module: "ai".into(),
},
PermissionDescriptor {
code: "ai.usage.list".into(),
name: "查看用量".into(),
description: "查看 AI 用量统计".into(),
module: "ai".into(),
},
PermissionDescriptor {
code: "ai.provider.manage".into(),
name: "管理提供商".into(),
description: "管理 AI 提供商配置".into(),
module: "ai".into(),
},
PermissionDescriptor {
code: "ai.suggestion.list".into(),
name: "查看 AI 建议".into(),
description: "查看 AI 分析生成的建议列表".into(),
module: "ai".into(),
},
PermissionDescriptor {
code: "ai.suggestion.manage".into(),
name: "审批 AI 建议".into(),
description: "批准或拒绝 AI 建议".into(),
module: "ai".into(),
},
]
}
fn as_any(&self) -> &dyn Any {
self
}
async fn on_startup(
&self,
ctx: &erp_core::module::ModuleContext,
) -> erp_core::error::AppResult<()> {
// 订阅 ai.* 前缀的所有事件reanalysis + analysis.requested
let (mut rx, _handle) = ctx.event_bus.subscribe_filtered("ai.".to_string());
let db = ctx.db.clone();
tokio::spawn(async move {
loop {
match rx.recv().await {
Some(event) if event.event_type == "ai.reanalysis.requested" => {
let suggestion_id = event.payload.get("original_suggestion_id")
.and_then(|v| v.as_str())
.and_then(|s| uuid::Uuid::parse_str(s).ok());
let patient_id = event.payload.get("patient_id")
.and_then(|v| v.as_str())
.and_then(|s| uuid::Uuid::parse_str(s).ok());
match (suggestion_id, patient_id) {
(Some(sid), Some(pid)) => {
if let Err(e) = crate::service::reanalysis::handle_reanalysis_requested(
&db, event.tenant_id, sid, pid,
).await {
tracing::warn!(
suggestion_id = %sid,
error = %e,
"AI 再分析处理失败"
);
}
}
_ => {
tracing::warn!("ai.reanalysis.requested 事件缺少必要字段");
}
}
}
Some(event) if event.event_type == "ai.analysis.requested" => {
let source_type = event.payload.get("source_type").and_then(|v| v.as_str());
let source_id = event.payload.get("source_id")
.and_then(|v| v.as_str())
.and_then(|s| uuid::Uuid::parse_str(s).ok());
let patient_id = event.payload.get("patient_id")
.and_then(|v| v.as_str())
.and_then(|s| uuid::Uuid::parse_str(s).ok());
tracing::info!(
source_type = ?source_type,
source_id = ?source_id,
patient_id = ?patient_id,
tenant_id = %event.tenant_id,
"收到 AI 分析请求事件(化验单上传触发,待 Prompt 模板就绪后实现自动分析)"
);
}
Some(event) => {
tracing::debug!(
event_type = %event.event_type,
"忽略非目标 AI 事件"
);
}
None => {
tracing::info!("AI 事件订阅通道已关闭");
break;
}
}
}
});
tracing::info!(module = "ai", "AI 模块事件处理器已注册(监听 ai.* 事件)");
Ok(())
}
}
impl AiModule {
pub fn public_routes<S>() -> Router<S>
where
crate::state::AiState: axum::extract::FromRef<S>,
S: Clone + Send + Sync + 'static,
{
Router::new()
}
pub fn protected_routes<S>() -> Router<S>
where
crate::state::AiState: axum::extract::FromRef<S>,
S: Clone + Send + Sync + 'static,
{
Router::new()
.route(
"/ai/analyze/lab-report",
axum::routing::post(crate::handler::stream_lab_report),
)
.route(
"/ai/analyze/trends",
axum::routing::post(crate::handler::stream_trends),
)
.route(
"/ai/analyze/checkup-plan",
axum::routing::post(crate::handler::stream_checkup_plan),
)
.route(
"/ai/analyze/report-summary",
axum::routing::post(crate::handler::stream_report_summary),
)
.route(
"/ai/analysis/history",
axum::routing::get(crate::handler::list_analysis),
)
.route(
"/ai/analysis/{id}",
axum::routing::get(crate::handler::get_analysis),
)
.route(
"/ai/prompts",
axum::routing::get(crate::handler::list_prompts),
)
.route(
"/ai/prompts",
axum::routing::post(crate::handler::create_prompt),
)
.route(
"/ai/prompts/{id}/activate",
axum::routing::post(crate::handler::activate_prompt),
)
.route(
"/ai/prompts/{id}/rollback",
axum::routing::post(crate::handler::rollback_prompt),
)
.route(
"/ai/usage/overview",
axum::routing::get(crate::handler::usage_overview),
)
.route(
"/ai/usage/by-type",
axum::routing::get(crate::handler::usage_by_type),
)
.route(
"/ai/suggestions",
axum::routing::get(crate::handler::suggestion_handler::list_suggestions),
)
.route(
"/ai/suggestions/{id}/approve",
axum::routing::post(crate::handler::suggestion_handler::approve_suggestion),
)
.route(
"/ai/suggestions/{id}/execute",
axum::routing::post(crate::handler::suggestion_handler::execute_suggestion),
)
.route(
"/ai/suggestions/{id}/comparison",
axum::routing::get(crate::handler::suggestion_handler::get_comparison),
)
.route(
"/ai/dialysis/risk-assessment",
axum::routing::post(crate::handler::assess_dialysis_risk),
)
}
}