- follow_up.completed 消费者:通过 action_result 反查 AI 建议,触发再分析
- ai.reanalysis.requested 消费者:加载原始建议 baseline
- comparison.rs:对比报告生成引擎(指标变化百分比+趋势判断)
- GET /ai/suggestions/{id}/comparison:前后对比报告 API
- find_by_followup_task:通过随访任务反查关联建议ID
205 lines
7.1 KiB
Rust
205 lines
7.1 KiB
Rust
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),
|
||
)
|
||
}
|
||
}
|