- list_documents: 分页列表(按知识库过滤)
- get_document: 文档详情
- create_manual_document: 手动输入文档
- upload_document: Multipart 文件上传(20MB 限制 + 自动解析)
- delete_document: 软删除(级联减计数)
- 5 条路由注册到 /ai/knowledge-bases/{kb_id}/documents + /ai/documents/*
Phase 2 Task 10
709 lines
30 KiB
Rust
709 lines
30 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(),
|
||
},
|
||
// 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<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/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/prompts/{id}/deactivate",
|
||
axum::routing::post(crate::handler::deactivate_prompt),
|
||
)
|
||
.route(
|
||
"/ai/prompts/{id}",
|
||
axum::routing::delete(crate::handler::delete_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),
|
||
)
|
||
// 知识库 V2 路由
|
||
.route(
|
||
"/ai/knowledge-bases",
|
||
axum::routing::get(crate::handler::knowledge_v2_handler::list_knowledge_bases),
|
||
)
|
||
.route(
|
||
"/ai/knowledge-bases",
|
||
axum::routing::post(crate::handler::knowledge_v2_handler::create_knowledge_base),
|
||
)
|
||
.route(
|
||
"/ai/knowledge-bases/{id}",
|
||
axum::routing::get(crate::handler::knowledge_v2_handler::get_knowledge_base),
|
||
)
|
||
.route(
|
||
"/ai/knowledge-bases/{id}",
|
||
axum::routing::put(crate::handler::knowledge_v2_handler::update_knowledge_base),
|
||
)
|
||
.route(
|
||
"/ai/knowledge-bases/{id}",
|
||
axum::routing::delete(crate::handler::knowledge_v2_handler::delete_knowledge_base),
|
||
)
|
||
// 文档管理路由
|
||
.route(
|
||
"/ai/knowledge-bases/{kb_id}/documents",
|
||
axum::routing::get(crate::handler::document_handler::list_documents),
|
||
)
|
||
.route(
|
||
"/ai/documents/manual",
|
||
axum::routing::post(crate::handler::document_handler::create_manual_document),
|
||
)
|
||
.route(
|
||
"/ai/documents/upload",
|
||
axum::routing::post(crate::handler::document_handler::upload_document),
|
||
)
|
||
.route(
|
||
"/ai/documents/{id}",
|
||
axum::routing::get(crate::handler::document_handler::get_document),
|
||
)
|
||
.route(
|
||
"/ai/knowledge-bases/{kb_id}/documents/{id}",
|
||
axum::routing::delete(crate::handler::document_handler::delete_document),
|
||
)
|
||
.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),
|
||
)
|
||
}
|
||
}
|