""" 知识库服务 知识库 CRUD 操作 数据权限: - 使用 get_list_with_data_scope() 自动应用数据权限 - 支持本人、本部门、本部门及下级、全部等数据范围 """ import logging from typing import Optional, List, Tuple from sqlalchemy import select, func, or_, and_ from sqlalchemy.ext.asyncio import AsyncSession from ai_platform.knowledge.models import KnowledgeBase, KnowledgeDocument, KnowledgeSegment from ai_platform.knowledge.schemas.knowledge_base_schema import KnowledgeBaseCreate, KnowledgeBaseUpdate from app.data_scope_utils import get_data_scope_filter, apply_data_scope_to_conditions logger = logging.getLogger(__name__) # 资源类型(用于数据权限配置) RESOURCE_TYPE = "knowledge_base" RESOURCE_DISPLAY_NAME = "知识库管理" class KnowledgeService: """知识库服务""" def __init__(self, db: AsyncSession): self._db = db async def get_list( self, page: int = 1, page_size: int = 20, name: Optional[str] = None, status: Optional[str] = None, application_id: Optional[str] = None, ) -> Tuple[List[KnowledgeBase], int]: """获取知识库列表""" query = select(KnowledgeBase).where(KnowledgeBase.is_deleted == False) if application_id: query = query.where(or_( KnowledgeBase.application_id == application_id, and_(KnowledgeBase.application_id.is_(None), KnowledgeBase.is_global == True) )) if name: query = query.where(KnowledgeBase.name.ilike(f"%{name}%")) if status: query = query.where(KnowledgeBase.status == status) # 总数 count_result = await self._db.execute(select(func.count()).select_from(query.subquery())) total = count_result.scalar() or 0 # 分页 offset = (page - 1) * page_size query = query.order_by(KnowledgeBase.sort.desc(), KnowledgeBase.sys_create_datetime.desc()) query = query.offset(offset).limit(page_size) result = await self._db.execute(query) items = result.scalars().all() return items, total async def get_list_with_data_scope( self, page: int = 1, page_size: int = 20, name: Optional[str] = None, status: Optional[str] = None, application_id: Optional[str] = None, ) -> Tuple[List[KnowledgeBase], int]: """ 获取知识库列表(带数据权限过滤) 自动从上下文获取当前用户信息,应用数据权限过滤 """ conditions = [KnowledgeBase.is_deleted == False] if application_id: conditions.append(or_( KnowledgeBase.application_id == application_id, and_(KnowledgeBase.application_id.is_(None), KnowledgeBase.is_global == True) )) if name: conditions.append(KnowledgeBase.name.ilike(f"%{name}%")) if status: conditions.append(KnowledgeBase.status == status) # 获取数据权限过滤条件并应用 data_scope_filter = await get_data_scope_filter(self._db, RESOURCE_TYPE) scope_conditions = apply_data_scope_to_conditions(KnowledgeBase, data_scope_filter) conditions.extend(scope_conditions) # 总数 query = select(KnowledgeBase).where(and_(*conditions)) count_result = await self._db.execute(select(func.count()).select_from(query.subquery())) total = count_result.scalar() or 0 # 分页 offset = (page - 1) * page_size query = query.order_by(KnowledgeBase.sort.desc(), KnowledgeBase.sys_create_datetime.desc()) query = query.offset(offset).limit(page_size) result = await self._db.execute(query) items = result.scalars().all() return items, total async def get_by_id(self, kb_id: str) -> Optional[KnowledgeBase]: """获取知识库详情""" result = await self._db.execute( select(KnowledgeBase).where( KnowledgeBase.id == kb_id, KnowledgeBase.is_deleted == False ) ) return result.scalar_one_or_none() async def get_by_code(self, code: str) -> Optional[KnowledgeBase]: """根据编码获取知识库""" result = await self._db.execute( select(KnowledgeBase).where( KnowledgeBase.code == code, KnowledgeBase.is_deleted == False ) ) return result.scalar_one_or_none() async def create(self, data: KnowledgeBaseCreate) -> KnowledgeBase: """创建知识库""" # 检查编码唯一性 existing = await self.get_by_code(data.code) if existing: raise ValueError(f'知识库编码 {data.code} 已存在') kb_data = data.model_dump() # 自动检测 embedding 模型的真实维度 if data.embedding_model_id: try: from ai_platform.knowledge.services.embedding_service import EmbeddingService embedding_service = EmbeddingService(self._db) real_dim = await embedding_service.get_embedding_dimensions(data.embedding_model_id) kb_data['embedding_dimensions'] = real_dim logger.info(f'自动检测 embedding 维度: {real_dim}') except Exception as e: logger.warning(f'自动检测 embedding 维度失败,使用默认值: {e}') kb = KnowledgeBase(**kb_data) # 自动填充创建人和部门 from utils.context import get_current_user_info_from_context user_info = get_current_user_info_from_context() if user_info: if not kb.sys_creator_id: kb.sys_creator_id = user_info.get('user_id') if not kb.sys_dept_id and user_info.get('dept_id'): kb.sys_dept_id = user_info.get('dept_id') self._db.add(kb) await self._db.commit() await self._db.refresh(kb) return kb async def update(self, kb_id: str, data: KnowledgeBaseUpdate) -> Optional[KnowledgeBase]: """更新知识库""" kb = await self.get_by_id(kb_id) if not kb: return None update_data = data.model_dump(exclude_unset=True) # 如果更换了 embedding 模型,自动重新检测维度 new_model_id = update_data.get('embedding_model_id') if new_model_id and new_model_id != kb.embedding_model_id: try: from ai_platform.knowledge.services.embedding_service import EmbeddingService embedding_service = EmbeddingService(self._db) real_dim = await embedding_service.get_embedding_dimensions(new_model_id) update_data['embedding_dimensions'] = real_dim logger.info(f'更换模型后自动检测 embedding 维度: {real_dim}') except Exception as e: logger.warning(f'自动检测 embedding 维度失败: {e}') for key, value in update_data.items(): setattr(kb, key, value) await self._db.commit() await self._db.refresh(kb) return kb async def delete(self, kb_id: str) -> bool: """删除知识库(软删除 + 清理 Qdrant collection)""" kb = await self.get_by_id(kb_id) if not kb: return False kb.is_deleted = True # 同时软删除所有文档和分段 doc_result = await self._db.execute( select(KnowledgeDocument).where( KnowledgeDocument.knowledge_base_id == kb_id, KnowledgeDocument.is_deleted == False ) ) docs = doc_result.scalars().all() for doc in docs: doc.is_deleted = True # 软删除分段 from sqlalchemy import update await self._db.execute( update(KnowledgeSegment).where( KnowledgeSegment.knowledge_base_id == kb_id ).values(is_deleted=True) ) await self._db.commit() # 删除 Qdrant 中对应的 collection try: from ai_platform.knowledge.vector_store import get_vector_store vector_store = get_vector_store() await vector_store.delete_collection(kb_id) except Exception as e: logger.warning(f'删除 Qdrant collection 失败: {e}') return True async def get_simple_list(self, application_id: Optional[str] = None) -> List[dict]: """获取知识库简单列表(用于下拉选择)""" query = select( KnowledgeBase.id, KnowledgeBase.name, KnowledgeBase.code, KnowledgeBase.document_count, KnowledgeBase.segment_count, ).where( KnowledgeBase.is_deleted == False, KnowledgeBase.status == 'active', ) if application_id: query = query.where(or_( KnowledgeBase.application_id == application_id, and_(KnowledgeBase.application_id.is_(None), KnowledgeBase.is_global == True) )) query = query.order_by(KnowledgeBase.sort.desc(), KnowledgeBase.sys_create_datetime.desc()) result = await self._db.execute(query) rows = result.all() return [ { 'id': row.id, 'name': row.name, 'code': row.code, 'document_count': row.document_count or 0, 'segment_count': row.segment_count or 0, } for row in rows ]