From 9d93c028428174359124794780ca9921e8dc76b5 Mon Sep 17 00:00:00 2001 From: lxfight <1686540385@qq.com> Date: Sat, 27 Jun 2026 20:16:58 +0800 Subject: [PATCH 1/2] fix: paginate knowledge base dashboard lists --- .../services/knowledge_base_service.py | 12 +++++-- dashboard/src/views/knowledge-base/KBList.vue | 30 +++++++++++----- .../components/DocumentsTab.vue | 36 ++++++++++++++++--- 3 files changed, 63 insertions(+), 15 deletions(-) diff --git a/astrbot/dashboard/services/knowledge_base_service.py b/astrbot/dashboard/services/knowledge_base_service.py index c7f9546418..0ac7921213 100644 --- a/astrbot/dashboard/services/knowledge_base_service.py +++ b/astrbot/dashboard/services/knowledge_base_service.py @@ -266,16 +266,22 @@ async def background_import_task( async def list_kbs(self, *, page: int, page_size: int) -> dict[str, Any]: kb_manager = self.get_kb_manager() kbs = await kb_manager.list_kbs() + total = len(kbs) + + # Apply pagination after building the full list (list_kbs returns all) + start = (page - 1) * page_size + end = start + page_size + paged_kbs = kbs[start:end] kb_list = [] - for kb in kbs: + for kb in paged_kbs: kb_dict = kb.model_dump() kb_helper = await kb_manager.get_kb(kb.kb_id) if kb_helper and kb_helper.init_error: kb_dict["init_error"] = kb_helper.init_error kb_list.append(kb_dict) - return {"items": kb_list, "page": page, "page_size": page_size} + return {"items": kb_list, "page": page, "page_size": page_size, "total": total} async def list_kbs_from_dashboard_query(self, *, page, page_size) -> dict[str, Any]: return await self.list_kbs( @@ -446,10 +452,12 @@ async def list_documents( offset = (page - 1) * page_size doc_list = await kb_helper.list_documents(offset=offset, limit=page_size) + total = kb_helper.kb.doc_count or 0 return { "items": [doc.model_dump() for doc in doc_list], "page": page, "page_size": page_size, + "total": total, } async def list_documents_from_dashboard_query( diff --git a/dashboard/src/views/knowledge-base/KBList.vue b/dashboard/src/views/knowledge-base/KBList.vue index d25b8e458a..17989528ed 100644 --- a/dashboard/src/views/knowledge-base/KBList.vue +++ b/dashboard/src/views/knowledge-base/KBList.vue @@ -79,6 +79,15 @@ + + @@ -269,6 +278,9 @@ const loading = ref(false) const saving = ref(false) const deleting = ref(false) const kbList = ref([]) +const page = ref(1) +const pageSize = ref(20) +const total = ref(0) const embeddingProviders = ref([]) const rerankProviders = ref([]) const originalEmbeddingProvider = ref(null) @@ -324,18 +336,18 @@ const emojiCategories = [ const loadKnowledgeBases = async (refreshStats = false) => { loading.value = true try { - const params: any = {} if (refreshStats) { - params.refresh_stats = 'true' + page.value = 1 } - const response = await knowledgeApi.list({ - page: params.page, - page_size: params.page_size, - refresh_stats: params.refresh_stats === 'true' + page: page.value, + page_size: pageSize.value, + refresh_stats: refreshStats }) if (response.data.status === 'ok') { - kbList.value = response.data.data.items || [] + const data = response.data.data + kbList.value = data.items || [] + total.value = data.total || 0 } else { showSnackbar(response.data.message || t('messages.loadError'), 'error') } @@ -407,7 +419,9 @@ const deleteKB = async () => { if (response.data.status === 'ok') { showSnackbar(t('messages.deleteSuccess')) - // 先刷新列表,再关闭对话框 + if (kbList.value.length === 1 && page.value > 1) { + page.value -= 1 + } await loadKnowledgeBases() showDeleteDialog.value = false deleteTarget.value = null diff --git a/dashboard/src/views/knowledge-base/components/DocumentsTab.vue b/dashboard/src/views/knowledge-base/components/DocumentsTab.vue index 29a49d0da7..48d1a7c054 100644 --- a/dashboard/src/views/knowledge-base/components/DocumentsTab.vue +++ b/dashboard/src/views/knowledge-base/components/DocumentsTab.vue @@ -11,7 +11,9 @@ - + - + @@ -256,6 +258,9 @@ const loading = ref(false) const uploading = ref(false) const deleting = ref(false) const documents = ref([]) +const page = ref(1) +const pageSize = ref(10) +const total = ref(0) const searchQuery = ref('') const showUploadDialog = ref(false) const showDeleteDialog = ref(false) @@ -340,9 +345,14 @@ const headers = [ const loadDocuments = async () => { loading.value = true try { - const response = await knowledgeApi.documents(props.kbId) + const response = await knowledgeApi.documents(props.kbId, { + page: page.value, + page_size: pageSize.value + }) if (response.data.status === 'ok') { - documents.value = response.data.data.items || [] + const data = response.data.data + documents.value = data.items || [] + total.value = data.total || 0 } } catch (error) { console.error('Failed to load documents:', error) @@ -352,6 +362,18 @@ const loadDocuments = async () => { } } +// Handle pagination +const onPageChange = (newPage: number) => { + page.value = newPage + loadDocuments() +} + +const onItemsPerPageChange = (newSize: number) => { + pageSize.value = newSize + page.value = 1 + loadDocuments() +} + // 文件选择 const handleFileSelect = (event: Event) => { const target = event.target as HTMLInputElement @@ -591,7 +613,7 @@ const startProgressPolling = (taskId: string) => { // 移除上传中的占位文档 documents.value = documents.value.filter(doc => doc.taskId !== taskId) - // 重新加载文档列表 + // Reload current page await loadDocuments() emit('refresh') @@ -684,6 +706,10 @@ const deleteDocument = async () => { if (response.data.status === 'ok') { showSnackbar(t('documents.deleteSuccess')) showDeleteDialog.value = false + // If current page becomes empty after delete and is not the first page, go back one page + if (documents.value.length === 1 && page.value > 1) { + page.value -= 1 + } await loadDocuments() emit('refresh') } else { From 75a42a7557b5c1bea7b1cf6fb1bd208e773a1fcf Mon Sep 17 00:00:00 2001 From: lxfight <1686540385@qq.com> Date: Sat, 27 Jun 2026 20:45:51 +0800 Subject: [PATCH 2/2] fix: preserve knowledge document search pagination --- astrbot/core/knowledge_base/kb_db_sqlite.py | 36 ++++++++--- astrbot/core/knowledge_base/kb_helper.py | 30 ++++++++- astrbot/dashboard/api/knowledge_bases.py | 2 + .../services/knowledge_base_service.py | 22 ++++++- .../src/api/generated/openapi-v1/types.gen.ts | 4 ++ dashboard/src/api/v1.ts | 2 +- .../components/DocumentsTab.vue | 13 +++- openspec/openapi-v1.yaml | 6 ++ tests/test_kb_import.py | 64 +++++++++++++++++++ 9 files changed, 162 insertions(+), 17 deletions(-) diff --git a/astrbot/core/knowledge_base/kb_db_sqlite.py b/astrbot/core/knowledge_base/kb_db_sqlite.py index 6a2cb5e0a8..ada9793a99 100644 --- a/astrbot/core/knowledge_base/kb_db_sqlite.py +++ b/astrbot/core/knowledge_base/kb_db_sqlite.py @@ -219,25 +219,45 @@ async def list_documents_by_kb( kb_id: str, offset: int = 0, limit: int = 100, + search: str | None = None, ) -> list[KBDocument]: - """列出知识库的所有文档""" + """List documents in a knowledge base. + + Args: + kb_id: Knowledge base ID. + offset: Number of documents to skip. + limit: Maximum number of documents to return. + search: Optional partial match on document name; disabled when None or empty. + + Returns: + List of matching KBDocument rows. + """ async with self.get_db() as session: + stmt = select(KBDocument).where(col(KBDocument.kb_id) == kb_id) + if search: + stmt = stmt.where(col(KBDocument.doc_name).contains(search)) stmt = ( - select(KBDocument) - .where(col(KBDocument.kb_id) == kb_id) - .offset(offset) - .limit(limit) - .order_by(desc(KBDocument.created_at)) + stmt.offset(offset).limit(limit).order_by(desc(KBDocument.created_at)) ) result = await session.execute(stmt) return list(result.scalars().all()) - async def count_documents_by_kb(self, kb_id: str) -> int: - """统计知识库的文档数量""" + async def count_documents_by_kb(self, kb_id: str, search: str | None = None) -> int: + """Count documents in a knowledge base. + + Args: + kb_id: Knowledge base ID. + search: Optional partial match on document name; disabled when None or empty. + + Returns: + Total number of matching documents. + """ async with self.get_db() as session: stmt = select(func.count(col(KBDocument.id))).where( col(KBDocument.kb_id) == kb_id, ) + if search: + stmt = stmt.where(col(KBDocument.doc_name).contains(search)) result = await session.execute(stmt) return result.scalar() or 0 diff --git a/astrbot/core/knowledge_base/kb_helper.py b/astrbot/core/knowledge_base/kb_helper.py index c29e45876d..bcd42d1126 100644 --- a/astrbot/core/knowledge_base/kb_helper.py +++ b/astrbot/core/knowledge_base/kb_helper.py @@ -469,11 +469,37 @@ async def list_documents( self, offset: int = 0, limit: int = 100, + search: str | None = None, ) -> list[KBDocument]: - """列出知识库的所有文档""" - docs = await self.kb_db.list_documents_by_kb(self.kb.kb_id, offset, limit) + """List documents in the knowledge base. + + Args: + offset: Number of documents to skip. + limit: Maximum number of documents to return. + search: Optional partial match on document name; disabled when None or empty. + + Returns: + List of matching KBDocument rows. + """ + docs = await self.kb_db.list_documents_by_kb( + self.kb.kb_id, + offset, + limit, + search=search, + ) return docs + async def count_documents(self, search: str | None = None) -> int: + """Count documents in the knowledge base. + + Args: + search: Optional partial match on document name; disabled when None or empty. + + Returns: + Total number of matching documents. + """ + return await self.kb_db.count_documents_by_kb(self.kb.kb_id, search=search) + async def get_document(self, doc_id: str) -> KBDocument | None: """获取单个文档""" doc = await self.kb_db.get_document_by_id(doc_id) diff --git a/astrbot/dashboard/api/knowledge_bases.py b/astrbot/dashboard/api/knowledge_bases.py index c6f62235dd..595f6ff911 100644 --- a/astrbot/dashboard/api/knowledge_bases.py +++ b/astrbot/dashboard/api/knowledge_bases.py @@ -182,6 +182,7 @@ async def list_knowledge_base_documents( kb_id=kb_id, page=_to_int(request.query_params.get("page"), 1), page_size=_to_int(request.query_params.get("page_size"), 100), + search=request.query_params.get("search"), ), prefix="获取文档列表失败", ) @@ -390,6 +391,7 @@ async def dashboard_list_documents( kb_id=request.query_params.get("kb_id"), page=_to_int(request.query_params.get("page"), 1), page_size=_to_int(request.query_params.get("page_size"), 100), + search=request.query_params.get("search"), ), prefix="获取文档列表失败", ) diff --git a/astrbot/dashboard/services/knowledge_base_service.py b/astrbot/dashboard/services/knowledge_base_service.py index 0ac7921213..ec162aa299 100644 --- a/astrbot/dashboard/services/knowledge_base_service.py +++ b/astrbot/dashboard/services/knowledge_base_service.py @@ -268,7 +268,9 @@ async def list_kbs(self, *, page: int, page_size: int) -> dict[str, Any]: kbs = await kb_manager.list_kbs() total = len(kbs) - # Apply pagination after building the full list (list_kbs returns all) + # Clamp page and page_size to at least 1 before calculating offsets/slices. + page = max(page, 1) + page_size = max(page_size, 1) start = (page - 1) * page_size end = start + page_size paged_kbs = kbs[start:end] @@ -443,6 +445,7 @@ async def list_documents( kb_id: str | None, page: int, page_size: int, + search: str | None = None, ) -> dict[str, Any]: if not kb_id: raise KnowledgeBaseServiceError("缺少参数 kb_id") @@ -450,9 +453,20 @@ async def list_documents( if not kb_helper: raise KnowledgeBaseServiceError("知识库不存在") + if search is not None: + search = search.strip() + if not search: + search = None + + page = max(page, 1) + page_size = max(page_size, 1) offset = (page - 1) * page_size - doc_list = await kb_helper.list_documents(offset=offset, limit=page_size) - total = kb_helper.kb.doc_count or 0 + doc_list = await kb_helper.list_documents( + offset=offset, + limit=page_size, + search=search, + ) + total = await kb_helper.count_documents(search=search) return { "items": [doc.model_dump() for doc in doc_list], "page": page, @@ -466,11 +480,13 @@ async def list_documents_from_dashboard_query( kb_id: str | None, page, page_size, + search: str | None = None, ) -> dict[str, Any]: return await self.list_documents( kb_id=kb_id, page=self._to_int(page, 1), page_size=self._to_int(page_size, 100), + search=search, ) async def upload_document( diff --git a/dashboard/src/api/generated/openapi-v1/types.gen.ts b/dashboard/src/api/generated/openapi-v1/types.gen.ts index b865e34371..abd4474889 100644 --- a/dashboard/src/api/generated/openapi-v1/types.gen.ts +++ b/dashboard/src/api/generated/openapi-v1/types.gen.ts @@ -2640,6 +2640,10 @@ export type ListKnowledgeDocumentsData = { query?: { page?: number; page_size?: number; + /** + * Filter documents by name (case-insensitive partial match). + */ + search?: string; }; }; diff --git a/dashboard/src/api/v1.ts b/dashboard/src/api/v1.ts index cf505abd14..62d181dd74 100644 --- a/dashboard/src/api/v1.ts +++ b/dashboard/src/api/v1.ts @@ -1378,7 +1378,7 @@ export const knowledgeApi = { openApiV1.deleteKnowledgeBase({ path: { kb_id: kbId } }), ); }, - documents(kbId: string, params?: { page?: number; page_size?: number }) { + documents(kbId: string, params?: { page?: number; page_size?: number; search?: string }) { return typed( openApiV1.listKnowledgeDocuments({ path: { kb_id: kbId }, diff --git a/dashboard/src/views/knowledge-base/components/DocumentsTab.vue b/dashboard/src/views/knowledge-base/components/DocumentsTab.vue index 48d1a7c054..7769ada8f9 100644 --- a/dashboard/src/views/knowledge-base/components/DocumentsTab.vue +++ b/dashboard/src/views/knowledge-base/components/DocumentsTab.vue @@ -11,7 +11,7 @@ -