Files
hms/crates/erp-ai/src/module.rs
iven 5d2402a1e7 feat(ai+health): 闭环核心 — 随访完成→再分析触发 + 前后对比报告
- follow_up.completed 消费者:通过 action_result 反查 AI 建议,触发再分析
- ai.reanalysis.requested 消费者:加载原始建议 baseline
- comparison.rs:对比报告生成引擎(指标变化百分比+趋势判断)
- GET /ai/suggestions/{id}/comparison:前后对比报告 API
- find_by_followup_task:通过随访任务反查关联建议ID
2026-05-01 09:14:13 +08:00

205 lines
7.1 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<()> {
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<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}/comparison",
axum::routing::get(crate::handler::suggestion_handler::get_comparison),
)
}
}