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(), }, ] } fn as_any(&self) -> &dyn Any { self } async fn on_startup( &self, ctx: &erp_core::module::ModuleContext, ) -> erp_core::error::AppResult<()> { let (mut rx, _handle) = ctx.event_bus.subscribe_filtered("ai.reanalysis.".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(_) => {} None => { tracing::info!("AI 再分析事件订阅通道已关闭"); break; } } } }); tracing::info!(module = "ai", "AI 模块事件处理器已注册(监听 reanalysis)"); 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/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}/comparison", axum::routing::get(crate::handler::suggestion_handler::get_comparison), ) } }