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 { 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(), }, // Copilot 权限 PermissionDescriptor { code: "copilot.insights.list".into(), name: "查看 Copilot 洞察".into(), description: "查看 Copilot 生成的患者洞察列表".into(), module: "ai".into(), }, PermissionDescriptor { code: "copilot.insights.manage".into(), name: "管理 Copilot 洞察".into(), description: "处理/忽略 Copilot 洞察".into(), module: "ai".into(), }, PermissionDescriptor { code: "copilot.risk.view".into(), name: "查看风险评分".into(), description: "查看 Copilot 计算的患者风险评分".into(), module: "ai".into(), }, PermissionDescriptor { code: "copilot.rules.list".into(), name: "查看 Copilot 规则".into(), description: "查看 Copilot 规则引擎配置".into(), module: "ai".into(), }, PermissionDescriptor { code: "copilot.rules.manage".into(), name: "管理 Copilot 规则".into(), description: "创建/编辑/删除 Copilot 规则".into(), module: "ai".into(), }, // AI 客服会话权限 PermissionDescriptor { code: "ai.chat.send".into(), name: "AI 客服对话".into(), description: "向 AI 客服发送消息".into(), module: "ai".into(), }, // AI 配置管理权限 PermissionDescriptor { code: "ai.config.read".into(), name: "查看 AI 配置".into(), description: "查看 AI 模型和参数配置".into(), module: "ai".into(), }, PermissionDescriptor { code: "ai.config.manage".into(), name: "管理 AI 配置".into(), description: "修改 AI 模型、温度、Token 等参数配置".into(), module: "ai".into(), }, PermissionDescriptor { code: "ai.chat.session.list".into(), name: "查看 AI 会话列表".into(), description: "查看用户的 AI 客服会话列表".into(), module: "ai".into(), }, PermissionDescriptor { code: "ai.chat.session.manage".into(), name: "管理 AI 会话".into(), description: "创建/关闭 AI 客服会话".into(), module: "ai".into(), }, PermissionDescriptor { code: "ai.chat.session.history".into(), name: "查看 AI 会话历史".into(), description: "查看 AI 客服会话消息历史".into(), module: "ai".into(), }, // 知识库权限 PermissionDescriptor { code: "ai.knowledge.list".into(), name: "查看知识库".into(), description: "查看 AI 知识库(参考资料和临床指南)".into(), module: "ai".into(), }, PermissionDescriptor { code: "ai.knowledge.manage".into(), name: "管理知识库".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 分析请求事件" ); } // H4: 透析记录→KDIGO 自动风险评估 Some(event) if event.event_type == "ai.dialysis.kdigo_requested" => { let patient_id = event .payload .get("patient_id") .and_then(|v| v.as_str()) .and_then(|s| uuid::Uuid::parse_str(s).ok()); let record_id = event .payload .get("dialysis_record_id") .and_then(|v| v.as_str()); tracing::info!( patient_id = ?patient_id, record_id = ?record_id, tenant_id = %event.tenant_id, "透析→KDIGO 自动评估触发" ); } Some(event) => { tracing::debug!( event_type = %event.event_type, "忽略非目标 AI 事件" ); } None => { tracing::info!("AI 事件订阅通道已关闭"); break; } } } }); // 订阅 erp-health 事件 → 自动入队分析 let (mut health_rx, _) = ctx.event_bus.subscribe_filtered("health_data.".to_string()); let (mut lab_rx, _) = ctx.event_bus.subscribe_filtered("lab_report.".to_string()); let (mut dialysis_rx, _) = ctx.event_bus.subscribe_filtered("dialysis.".to_string()); let queue_db = ctx.db.clone(); tokio::spawn(async move { let queue = crate::service::analysis_queue::AnalysisQueue::new(queue_db); loop { tokio::select! { event = health_rx.recv() => { match event { Some(e) if e.event_type == "health_data.critical_alert" => { if let Some(pid) = e.payload.get("patient_id") .and_then(|v| v.as_str()) .and_then(|s| uuid::Uuid::parse_str(s).ok()) { let job = crate::service::analysis_queue::AnalysisJob { tenant_id: e.tenant_id, patient_id: pid, analysis_type: "trend".into(), priority: 2, source_event: Some(e.event_type.clone()), source_ref: "event".into(), created_by: None, }; if let Err(err) = queue.enqueue(job).await { tracing::warn!(error = %err, "健康告警→分析入队失败"); } else { tracing::info!(tenant = %e.tenant_id, patient = %pid, "健康告警→趋势分析已入队"); } } } Some(e) => { tracing::debug!(event_type = %e.event_type, "忽略非目标 health_data 事件"); } None => return, } } event = lab_rx.recv() => { match event { Some(e) if e.event_type == "lab_report.uploaded" => { if let Some(pid) = e.payload.get("patient_id") .and_then(|v| v.as_str()) .and_then(|s| uuid::Uuid::parse_str(s).ok()) { let job = crate::service::analysis_queue::AnalysisJob { tenant_id: e.tenant_id, patient_id: pid, analysis_type: "lab_report".into(), priority: 1, source_event: Some(e.event_type.clone()), source_ref: "event".into(), created_by: None, }; if let Err(err) = queue.enqueue(job).await { tracing::warn!(error = %err, "化验单上传→分析入队失败"); } else { tracing::info!(tenant = %e.tenant_id, patient = %pid, "化验单上传→解读分析已入队"); } } } Some(e) => { tracing::debug!(event_type = %e.event_type, "忽略非目标 lab_report 事件"); } None => return, } } event = dialysis_rx.recv() => { match event { Some(e) if e.event_type == "dialysis.record.created" => { if let Some(pid) = e.payload.get("patient_id") .and_then(|v| v.as_str()) .and_then(|s| uuid::Uuid::parse_str(s).ok()) { let job = crate::service::analysis_queue::AnalysisJob { tenant_id: e.tenant_id, patient_id: pid, analysis_type: "dialysis_risk".into(), priority: 2, source_event: Some(e.event_type.clone()), source_ref: "event".into(), created_by: None, }; if let Err(err) = queue.enqueue(job).await { tracing::warn!(error = %err, "透析记录→KDIGO入队失败"); } else { tracing::info!(tenant = %e.tenant_id, patient = %pid, "透析记录→KDIGO风险评估已入队"); } } } Some(e) => { tracing::debug!(event_type = %e.event_type, "忽略非目标 dialysis 事件"); } None => return, } } } } }); // Copilot 事件消费者 — 订阅 health 事件触发风险评分刷新 let copilot_handles = crate::event::copilot_consumer::spawn(&ctx.db, &ctx.event_bus); std::mem::forget(copilot_handles); // 巡护事件消费者 — 订阅 ai.patrol.requested,为未处理告警患者入队趋势分析 let patrol_handle = crate::event::patrol_consumer::spawn(&ctx.db, &ctx.event_bus); std::mem::forget(patrol_handle); // 每日凌晨 2:00 批量刷新所有在管患者风险快照 let refresh_db = ctx.db.clone(); let refresh_event_bus = ctx.event_bus.clone(); tokio::spawn(async move { // 首次执行延迟到下一个凌晨 2:00(简单实现:延迟 6 小时后开始 24h 周期) tokio::time::sleep(std::time::Duration::from_secs(6 * 3600)).await; let mut interval = tokio::time::interval(std::time::Duration::from_secs(86400)); loop { interval.tick().await; match crate::service::risk_service::RiskService::refresh_all_patients( &refresh_db, Some(&refresh_event_bus), ) .await { Ok(count) => { tracing::info!(patient_count = count, "每日风险快照刷新完成"); } Err(e) => { tracing::warn!(error = %e, "每日风险快照刷新失败"); } } // 清理过期洞察 + 过期建议 match crate::service::insight_service::InsightService::cleanup_expired(&refresh_db) .await { Ok(n) => tracing::info!(expired_count = n, "过期洞察清理完成"), Err(e) => tracing::warn!(error = %e, "过期洞察清理失败"), } match crate::service::suggestion::SuggestionService::expire_stale_all_tenants( &refresh_db, 30, ) .await { Ok(n) => tracing::info!(expired_count = n, "过期建议清理完成"), Err(e) => tracing::warn!(error = %e, "过期建议清理失败"), } } }); tracing::info!( module = "ai", "AI 模块事件处理器已注册(监听 ai.* 事件 + Copilot 事件 + 巡护事件)" ); Ok(()) } } impl AiModule { pub fn public_routes() -> Router where crate::state::AiState: axum::extract::FromRef, S: Clone + Send + Sync + 'static, { Router::new() } pub fn protected_routes() -> Router where crate::state::AiState: axum::extract::FromRef, S: Clone + Send + Sync + 'static, { Router::new() .route( "/ai/chat", axum::routing::post(crate::handler::chat_handler::chat), ) .route( "/ai/chat/sessions", axum::routing::post(crate::handler::chat_handler::create_session), ) .route( "/ai/chat/sessions", axum::routing::get(crate::handler::chat_handler::list_sessions), ) .route( "/ai/chat/sessions/{session_id}/rename", axum::routing::put(crate::handler::chat_handler::rename_session), ) .route( "/ai/chat/sessions/{session_id}/close", axum::routing::post(crate::handler::chat_handler::close_session), ) .route( "/ai/chat/sessions/{session_id}/messages", axum::routing::get(crate::handler::chat_handler::list_messages), ) .route( "/ai/config", axum::routing::get(crate::handler::config_handler::get_config), ) .route( "/ai/config", axum::routing::put(crate::handler::config_handler::update_config), ) .route( "/ai/config/defaults", axum::routing::get(crate::handler::config_handler::get_config_defaults), ) .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/analyze/follow-up-summary", axum::routing::post(crate::handler::stream_follow_up_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/suggestions/{id}/feedback", axum::routing::post(crate::handler::suggestion_handler::submit_feedback), ) // 知识库路由 .route( "/ai/knowledge/references", axum::routing::get(crate::handler::knowledge_handler::list_references), ) .route( "/ai/knowledge/references", axum::routing::post(crate::handler::knowledge_handler::create_reference), ) .route( "/ai/knowledge/references/{id}", axum::routing::put(crate::handler::knowledge_handler::update_reference), ) .route( "/ai/knowledge/references/{id}", axum::routing::delete(crate::handler::knowledge_handler::delete_reference), ) .route( "/ai/knowledge/references/{id}/re-embed", axum::routing::post(crate::handler::knowledge_handler::re_embed_reference), ) .route( "/ai/knowledge/guides", axum::routing::get(crate::handler::knowledge_handler::list_guides), ) .route( "/ai/knowledge/guides", axum::routing::post(crate::handler::knowledge_handler::create_guide), ) .route( "/ai/knowledge/guides/{id}", axum::routing::put(crate::handler::knowledge_handler::update_guide), ) .route( "/ai/knowledge/guides/{id}", axum::routing::delete(crate::handler::knowledge_handler::delete_guide), ) .route( "/ai/knowledge/guides/{id}/re-embed", axum::routing::post(crate::handler::knowledge_handler::re_embed_guide), ) .route( "/ai/dialysis/risk-assessment", axum::routing::post(crate::handler::assess_dialysis_risk), ) .route( "/ai/providers/health", axum::routing::get(crate::handler::provider_health), ) .route( "/ai/providers", axum::routing::get(crate::handler::provider_names), ) .route( "/ai/quota/summary", axum::routing::get(crate::handler::quota_summary), ) .route( "/ai/health-summary", axum::routing::get(crate::handler::insight_handler::health_summary), ) // AI 管理看板 .route( "/ai/admin/daily-usage", axum::routing::get(crate::handler::admin_daily_usage), ) .route( "/ai/admin/flags", axum::routing::get(crate::handler::admin_list_flags), ) .route( "/ai/admin/flags", axum::routing::post(crate::handler::admin_update_flag), ) .route( "/ai/budget/status", axum::routing::get(crate::handler::budget_status), ) .route( "/ai/cost/estimate", axum::routing::get(crate::handler::cost_estimate), ) // Copilot 路由 .route( "/copilot/insights", axum::routing::get(crate::handler::insight_handler::list_insights), ) .route( "/copilot/insights/{id}", axum::routing::get(crate::handler::insight_handler::get_insight), ) .route( "/copilot/insights/{id}/dismiss", axum::routing::post(crate::handler::insight_handler::dismiss_insight), ) .route( "/copilot/patients/{id}/risk", axum::routing::get(crate::handler::risk_handler::get_patient_risk), ) .route( "/copilot/rules", axum::routing::get(crate::handler::rule_handler::list_rules), ) .route( "/copilot/rules", axum::routing::post(crate::handler::rule_handler::create_rule), ) .route( "/copilot/rules/{id}", axum::routing::put(crate::handler::rule_handler::update_rule), ) .route( "/copilot/rules/{id}", axum::routing::delete(crate::handler::rule_handler::delete_rule), ) } }