//! 知识库 HTTP 处理器 use axum::{ extract::{Extension, Path, Query, State}, Json, }; use crate::auth::types::AuthContext; use crate::error::{SaasError, SaasResult}; use crate::state::AppState; use super::service; use super::types::*; // === 分类管理 === /// GET /api/v1/knowledge/categories — 树形分类列表 pub async fn list_categories( State(state): State, ) -> SaasResult>> { let tree = service::list_categories_tree(&state.db).await?; Ok(Json(tree)) } /// POST /api/v1/knowledge/categories — 创建分类 pub async fn create_category( State(state): State, Extension(ctx): Extension, Json(req): Json, ) -> SaasResult> { check_permission(&ctx, "knowledge:write")?; if req.name.trim().is_empty() { return Err(SaasError::InvalidInput("分类名称不能为空".into())); } let cat = service::create_category( &state.db, req.name.trim(), req.description.as_deref(), req.parent_id.as_deref(), req.icon.as_deref(), ).await?; Ok(Json(serde_json::json!({ "id": cat.id, "name": cat.name, }))) } /// PUT /api/v1/knowledge/categories/:id — 更新分类 pub async fn update_category( State(_state): State, Extension(ctx): Extension, Path(_id): Path, Json(_req): Json, ) -> SaasResult> { check_permission(&ctx, "knowledge:write")?; // TODO: implement update Ok(Json(serde_json::json!({"updated": true}))) } /// DELETE /api/v1/knowledge/categories/:id — 删除分类 pub async fn delete_category( State(state): State, Extension(ctx): Extension, Path(id): Path, ) -> SaasResult> { check_permission(&ctx, "knowledge:admin")?; service::delete_category(&state.db, &id).await?; Ok(Json(serde_json::json!({"deleted": true}))) } /// GET /api/v1/knowledge/categories/:id/items — 分类下条目列表 pub async fn list_category_items( State(_state): State, Path(_id): Path, ) -> SaasResult> { // TODO: implement with pagination Ok(Json(serde_json::json!({"items": [], "total": 0}))) } // === 知识条目 CRUD === /// GET /api/v1/knowledge/items — 分页列表 pub async fn list_items( State(state): State, Query(query): Query, ) -> SaasResult> { let page = query.page.unwrap_or(1).max(1); let page_size = query.page_size.unwrap_or(20).min(100); let offset = (page - 1) * page_size; let items: Vec = sqlx::query_as( "SELECT ki.* FROM knowledge_items ki \ JOIN knowledge_categories kc ON ki.category_id = kc.id \ WHERE ($1::text IS NULL OR ki.category_id = $1) \ AND ($2::text IS NULL OR ki.status = $2) \ AND ($3::text IS NULL OR ki.title ILIKE '%' || $3 || '%') \ ORDER BY ki.priority DESC, ki.updated_at DESC \ LIMIT $4 OFFSET $5" ) .bind(&query.category_id) .bind(&query.status) .bind(&query.keyword) .bind(page_size) .bind(offset) .fetch_all(&state.db) .await?; let total: (i64,) = sqlx::query_as( "SELECT COUNT(*) FROM knowledge_items ki \ WHERE ($1::text IS NULL OR ki.category_id = $1) \ AND ($2::text IS NULL OR ki.status = $2) \ AND ($3::text IS NULL OR ki.title ILIKE '%' || $3 || '%')" ) .bind(&query.category_id) .bind(&query.status) .bind(&query.keyword) .fetch_one(&state.db) .await?; Ok(Json(serde_json::json!({ "items": items, "total": total.0, "page": page, "page_size": page_size, }))) } /// POST /api/v1/knowledge/items — 创建条目 pub async fn create_item( State(state): State, Extension(ctx): Extension, Json(req): Json, ) -> SaasResult> { check_permission(&ctx, "knowledge:write")?; if req.title.trim().is_empty() || req.content.trim().is_empty() { return Err(SaasError::InvalidInput("标题和内容不能为空".into())); } if req.content.len() > 100_000 { return Err(SaasError::InvalidInput("内容不能超过 100KB".into())); } let item = service::create_item(&state.db, &ctx.account_id, &req).await?; // 异步触发 embedding 生成 if let Err(e) = state.worker_dispatcher.dispatch( "generate_embedding", serde_json::json!({ "item_id": item.id }), ).await { tracing::warn!("Failed to dispatch embedding generation: {}", e); } Ok(Json(serde_json::json!({ "id": item.id, "title": item.title, "version": item.version, }))) } /// POST /api/v1/knowledge/items/batch — 批量创建 pub async fn batch_create_items( State(state): State, Extension(ctx): Extension, Json(items): Json>, ) -> SaasResult> { check_permission(&ctx, "knowledge:write")?; if items.len() > 50 { return Err(SaasError::InvalidInput("单次批量创建不能超过 50 条".into())); } let mut created = Vec::new(); for req in items { match service::create_item(&state.db, &ctx.account_id, &req).await { Ok(item) => { let _ = state.worker_dispatcher.dispatch( "generate_embedding", serde_json::json!({ "item_id": item.id }), ).await; created.push(item.id); } Err(e) => { tracing::warn!("Batch create item failed: {}", e); } } } Ok(Json(serde_json::json!({ "created_count": created.len(), "ids": created, }))) } /// GET /api/v1/knowledge/items/:id — 条目详情 pub async fn get_item( State(state): State, Path(id): Path, ) -> SaasResult> { let item = service::get_item(&state.db, &id).await? .ok_or_else(|| SaasError::NotFound("知识条目不存在".into()))?; Ok(Json(serde_json::to_value(item).unwrap_or_default())) } /// PUT /api/v1/knowledge/items/:id — 更新条目 pub async fn update_item( State(state): State, Extension(ctx): Extension, Path(id): Path, Json(req): Json, ) -> SaasResult> { check_permission(&ctx, "knowledge:write")?; let updated = service::update_item(&state.db, &id, &ctx.account_id, &req).await?; // 触发 re-embedding let _ = state.worker_dispatcher.dispatch( "generate_embedding", serde_json::json!({ "item_id": id }), ).await; Ok(Json(serde_json::json!({ "id": updated.id, "version": updated.version, }))) } /// DELETE /api/v1/knowledge/items/:id — 删除条目 pub async fn delete_item( State(state): State, Extension(ctx): Extension, Path(id): Path, ) -> SaasResult> { check_permission(&ctx, "knowledge:admin")?; service::delete_item(&state.db, &id).await?; Ok(Json(serde_json::json!({"deleted": true}))) } // === 版本控制 === /// GET /api/v1/knowledge/items/:id/versions pub async fn list_versions( State(state): State, Path(id): Path, ) -> SaasResult> { let versions: Vec = sqlx::query_as( "SELECT * FROM knowledge_versions WHERE item_id = $1 ORDER BY version DESC" ) .bind(&id) .fetch_all(&state.db) .await?; Ok(Json(serde_json::json!({"versions": versions}))) } /// GET /api/v1/knowledge/items/:id/versions/:v pub async fn get_version( State(state): State, Path((id, v)): Path<(String, i32)>, ) -> SaasResult> { let version: KnowledgeVersion = sqlx::query_as( "SELECT * FROM knowledge_versions WHERE item_id = $1 AND version = $2" ) .bind(&id) .bind(v) .fetch_optional(&state.db) .await? .ok_or_else(|| SaasError::NotFound("版本不存在".into()))?; Ok(Json(serde_json::to_value(version).unwrap_or_default())) } /// POST /api/v1/knowledge/items/:id/rollback/:v pub async fn rollback_version( State(_state): State, Extension(ctx): Extension, Path((_id, v)): Path<(String, i32)>, ) -> SaasResult> { check_permission(&ctx, "knowledge:admin")?; // TODO: implement rollback Ok(Json(serde_json::json!({"rolled_back_to": v}))) } // === 检索 === /// POST /api/v1/knowledge/search — 语义搜索 pub async fn search( State(state): State, Json(req): Json, ) -> SaasResult>> { let limit = req.limit.unwrap_or(5).min(10); let min_score = req.min_score.unwrap_or(0.5); let results = service::search( &state.db, &req.query, req.category_id.as_deref(), limit, min_score, ).await?; Ok(Json(results)) } /// POST /api/v1/knowledge/recommend — 关联推荐 pub async fn recommend( State(_state): State, Json(_req): Json, ) -> SaasResult>> { // TODO: implement recommendation based on keyword overlap Ok(Json(vec![])) } // === 分析看板 === /// GET /api/v1/knowledge/analytics/overview pub async fn analytics_overview( State(state): State, ) -> SaasResult> { let overview = service::analytics_overview(&state.db).await?; Ok(Json(overview)) } /// GET /api/v1/knowledge/analytics/trends pub async fn analytics_trends( State(state): State, ) -> SaasResult> { // 使用 serde_json::Value 行来避免 PgRow 序列化 let trends: Vec<(serde_json::Value,)> = sqlx::query_as( "SELECT json_build_object( 'date', DATE(created_at), 'count', COUNT(*), 'injected_count', SUM(CASE WHEN was_injected THEN 1 ELSE 0 END) ) as row \ FROM knowledge_usage \ WHERE created_at >= NOW() - interval '30 days' \ GROUP BY DATE(created_at) ORDER BY DATE(created_at)" ) .fetch_all(&state.db) .await .unwrap_or_default(); let trends: Vec = trends.into_iter().map(|(v,)| v).collect(); Ok(Json(serde_json::json!({"trends": trends}))) } /// GET /api/v1/knowledge/analytics/top-items pub async fn analytics_top_items( State(state): State, ) -> SaasResult> { let items: Vec<(serde_json::Value,)> = sqlx::query_as( "SELECT json_build_object( 'id', ki.id, 'title', ki.title, 'category', kc.name, 'ref_count', COUNT(ku.id) ) as row \ FROM knowledge_items ki \ JOIN knowledge_categories kc ON ki.category_id = kc.id \ LEFT JOIN knowledge_usage ku ON ku.item_id = ki.id \ WHERE ki.status = 'active' \ GROUP BY ki.id, ki.title, kc.name \ ORDER BY COUNT(ku.id) DESC LIMIT 20" ) .fetch_all(&state.db) .await .unwrap_or_default(); let items: Vec = items.into_iter().map(|(v,)| v).collect(); Ok(Json(serde_json::json!({"items": items}))) } /// GET /api/v1/knowledge/analytics/quality pub async fn analytics_quality( State(_state): State, ) -> SaasResult> { Ok(Json(serde_json::json!({"quality": {}}))) } /// GET /api/v1/knowledge/analytics/gaps pub async fn analytics_gaps( State(_state): State, ) -> SaasResult> { Ok(Json(serde_json::json!({"gaps": []}))) } // === 辅助函数 === fn check_permission(ctx: &AuthContext, permission: &str) -> SaasResult<()> { crate::auth::handlers::check_permission(ctx, permission) }