From 50b9e8d683889405382d31135ad39c9ec8d12900 Mon Sep 17 00:00:00 2001 From: iven Date: Tue, 5 May 2026 15:19:49 +0800 Subject: [PATCH] =?UTF-8?q?feat(ai):=20=E6=B7=BB=E5=8A=A0=20Provider=20?= =?UTF-8?q?=E7=AE=A1=E7=90=86=20API=20=E7=AB=AF=E7=82=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GET /ai/providers — 列出已注册提供商 GET /ai/providers/health — 各提供商健康状态 GET /ai/quota/summary — 租户配额使用摘要 --- crates/erp-ai/src/handler/mod.rs | 55 ++++++++++++++++++++++++++++++++ crates/erp-ai/src/module.rs | 12 +++++++ 2 files changed, 67 insertions(+) diff --git a/crates/erp-ai/src/handler/mod.rs b/crates/erp-ai/src/handler/mod.rs index c07aa5d..604d3cf 100644 --- a/crates/erp-ai/src/handler/mod.rs +++ b/crates/erp-ai/src/handler/mod.rs @@ -487,6 +487,61 @@ where Ok(Json(ApiResponse::ok(result))) } +// === Provider 管理 === + +pub async fn provider_health( + State(state): State, + Extension(ctx): Extension, +) -> Result>, erp_core::error::AppError> +where + AiState: FromRef, + S: Clone + Send + Sync + 'static, +{ + require_permission(&ctx, "ai.analysis.list")?; + let statuses = state.provider_registry.health_check_all().await; + let result: serde_json::Value = statuses.iter().map(|entry| { + let (name, health) = entry.pair(); + serde_json::json!({ + "provider": name, + "healthy": health.is_healthy(), + "status": match health { + crate::provider::registry::ProviderHealth::Healthy { last_check } => + serde_json::json!({"status": "healthy", "last_check": last_check.to_rfc3339()}), + crate::provider::registry::ProviderHealth::Degraded { last_check, error } => + serde_json::json!({"status": "degraded", "last_check": last_check.to_rfc3339(), "error": error}), + crate::provider::registry::ProviderHealth::Unavailable { since, error } => + serde_json::json!({"status": "unavailable", "since": since.to_rfc3339(), "error": error}), + }, + }) + }).collect(); + Ok(Json(ApiResponse::ok(result))) +} + +pub async fn provider_names( + State(state): State, + Extension(ctx): Extension, +) -> Result>>, erp_core::error::AppError> +where + AiState: FromRef, + S: Clone + Send + Sync + 'static, +{ + require_permission(&ctx, "ai.analysis.list")?; + Ok(Json(ApiResponse::ok(state.provider_registry.provider_names()))) +} + +pub async fn quota_summary( + State(state): State, + Extension(ctx): Extension, +) -> Result>, erp_core::error::AppError> +where + AiState: FromRef, + S: Clone + Send + Sync + 'static, +{ + require_permission(&ctx, "ai.usage.list")?; + let summary = state.quota.get_usage_summary(ctx.tenant_id).await?; + Ok(Json(ApiResponse::ok(summary))) +} + // === 透析风险评估(KDIGO 规则) === pub async fn assess_dialysis_risk( diff --git a/crates/erp-ai/src/module.rs b/crates/erp-ai/src/module.rs index c598aab..4c57e14 100644 --- a/crates/erp-ai/src/module.rs +++ b/crates/erp-ai/src/module.rs @@ -246,5 +246,17 @@ impl AiModule { "/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), + ) } }