diff --git a/crates/erp-ai/src/handler/knowledge_v2_handler.rs b/crates/erp-ai/src/handler/knowledge_v2_handler.rs new file mode 100644 index 0000000..90ca81d --- /dev/null +++ b/crates/erp-ai/src/handler/knowledge_v2_handler.rs @@ -0,0 +1,172 @@ +use axum::Json; +use axum::extract::{Extension, FromRef, Path, State}; +use erp_core::rbac::require_permission; +use erp_core::types::{ApiResponse, TenantContext}; +use serde::Deserialize; + +use crate::service::knowledge_v2::{ + CreateKnowledgeBaseReq, ListKnowledgeBasesQuery, UpdateKnowledgeBaseReq, +}; +use crate::state::AiState; + +#[derive(Debug, Deserialize)] +pub struct ListKnowledgeBasesParams { + pub kb_type: Option, + pub is_enabled: Option, + pub page: Option, + pub page_size: Option, +} + +#[utoipa::path( + get, + path = "/ai/knowledge-bases", + responses((status = 200, description = "知识库列表")), + tag = "知识库V2", + security(("bearer_auth" = [])), +)] +pub async fn list_knowledge_bases( + State(state): State, + Extension(ctx): Extension, + axum::extract::Query(params): axum::extract::Query, +) -> Result>, erp_core::error::AppError> +where + AiState: FromRef, + S: Clone + Send + Sync + 'static, +{ + require_permission(&ctx, "ai.knowledge.list")?; + + let query = ListKnowledgeBasesQuery { + kb_type: params.kb_type, + is_enabled: params.is_enabled, + page: params.page, + page_size: params.page_size, + }; + let (items, total) = state.knowledge_v2.list(ctx.tenant_id, &query).await?; + + Ok(Json(ApiResponse::ok(serde_json::json!({ + "data": items, + "total": total, + "page": query.page.unwrap_or(1), + "page_size": query.page_size.unwrap_or(20), + })))) +} + +#[utoipa::path( + get, + path = "/ai/knowledge-bases/{id}", + responses((status = 200, description = "知识库详情")), + tag = "知识库V2", + security(("bearer_auth" = [])), +)] +pub async fn get_knowledge_base( + State(state): State, + Extension(ctx): Extension, + Path(id): Path, +) -> Result>, erp_core::error::AppError> +where + AiState: FromRef, + S: Clone + Send + Sync + 'static, +{ + require_permission(&ctx, "ai.knowledge.list")?; + let kb = state.knowledge_v2.get_by_id(ctx.tenant_id, id).await?; + Ok(Json(ApiResponse::ok( + serde_json::to_value(&kb).unwrap_or_default(), + ))) +} + +#[utoipa::path( + post, + path = "/ai/knowledge-bases", + request_body = CreateKnowledgeBaseReq, + responses((status = 200, description = "创建知识库")), + tag = "知识库V2", + security(("bearer_auth" = [])), +)] +pub async fn create_knowledge_base( + State(state): State, + Extension(ctx): Extension, + Json(body): Json, +) -> Result>, erp_core::error::AppError> +where + AiState: FromRef, + S: Clone + Send + Sync + 'static, +{ + require_permission(&ctx, "ai.knowledge.manage")?; + + if body.name.trim().is_empty() { + return Err(erp_core::error::AppError::Validation( + "知识库名称不能为空".into(), + )); + } + if body.kb_type.trim().is_empty() { + return Err(erp_core::error::AppError::Validation( + "知识库类型不能为空".into(), + )); + } + + let id = state + .knowledge_v2 + .create(ctx.tenant_id, ctx.user_id, body) + .await?; + + Ok(Json(ApiResponse::ok(serde_json::json!({ "id": id })))) +} + +#[utoipa::path( + put, + path = "/ai/knowledge-bases/{id}", + request_body = UpdateKnowledgeBaseReq, + responses((status = 200, description = "更新知识库")), + tag = "知识库V2", + security(("bearer_auth" = [])), +)] +pub async fn update_knowledge_base( + State(state): State, + Extension(ctx): Extension, + Path(id): Path, + Json(body): Json, +) -> Result>, erp_core::error::AppError> +where + AiState: FromRef, + S: Clone + Send + Sync + 'static, +{ + require_permission(&ctx, "ai.knowledge.manage")?; + + if let Some(ref name) = body.name + && name.trim().is_empty() + { + return Err(erp_core::error::AppError::Validation( + "知识库名称不能为空".into(), + )); + } + + state + .knowledge_v2 + .update(ctx.tenant_id, ctx.user_id, id, body) + .await?; + + Ok(Json(ApiResponse::ok(serde_json::json!({ "id": id })))) +} + +#[utoipa::path( + delete, + path = "/ai/knowledge-bases/{id}", + responses((status = 200, description = "删除知识库")), + tag = "知识库V2", + security(("bearer_auth" = [])), +)] +pub async fn delete_knowledge_base( + State(state): State, + Extension(ctx): Extension, + Path(id): Path, +) -> Result>, erp_core::error::AppError> +where + AiState: FromRef, + S: Clone + Send + Sync + 'static, +{ + require_permission(&ctx, "ai.knowledge.manage")?; + + state.knowledge_v2.delete(ctx.tenant_id, id).await?; + + Ok(Json(ApiResponse::ok(serde_json::json!({ "id": id })))) +} diff --git a/crates/erp-ai/src/handler/mod.rs b/crates/erp-ai/src/handler/mod.rs index 8bcec19..38312f2 100644 --- a/crates/erp-ai/src/handler/mod.rs +++ b/crates/erp-ai/src/handler/mod.rs @@ -16,6 +16,7 @@ pub mod chat_handler; pub mod config_handler; pub mod insight_handler; pub mod knowledge_handler; +pub mod knowledge_v2_handler; pub mod risk_handler; pub mod rule_handler; pub mod suggestion_handler; diff --git a/crates/erp-ai/src/module.rs b/crates/erp-ai/src/module.rs index fdd0d6e..502f739 100644 --- a/crates/erp-ai/src/module.rs +++ b/crates/erp-ai/src/module.rs @@ -588,6 +588,27 @@ impl AiModule { "/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/dialysis/risk-assessment", axum::routing::post(crate::handler::assess_dialysis_risk), diff --git a/crates/erp-ai/src/state.rs b/crates/erp-ai/src/state.rs index e06dc4a..fb97114 100644 --- a/crates/erp-ai/src/state.rs +++ b/crates/erp-ai/src/state.rs @@ -12,6 +12,7 @@ use crate::service::chat_session::ChatSessionService; use crate::service::feature_flag_service::FeatureFlagService; use crate::service::insight_service::InsightService; use crate::service::knowledge::KnowledgeService; +use crate::service::knowledge_v2::KnowledgeV2Service; use crate::service::prompt::PromptService; use crate::service::quota::QuotaService; use crate::service::risk_service::RiskService; @@ -34,6 +35,7 @@ pub struct AiState { pub insight_service: Arc, pub feature_flags: Arc, pub knowledge: Arc, + pub knowledge_v2: Arc, pub chat_session: Arc, pub chat_message: Arc, } diff --git a/crates/erp-server/src/main.rs b/crates/erp-server/src/main.rs index ba8d97d..3123f2d 100644 --- a/crates/erp-server/src/main.rs +++ b/crates/erp-server/src/main.rs @@ -616,6 +616,9 @@ async fn main() -> anyhow::Result<()> { erp_ai::service::embedding::EmbeddingService::from_settings(&db).await, ), )), + knowledge_v2: std::sync::Arc::new( + erp_ai::service::knowledge_v2::KnowledgeV2Service::new(db.clone()), + ), chat_session: std::sync::Arc::new( erp_ai::service::chat_session::ChatSessionService::new(db.clone()), ),