Skip to content

Commit 758e432

Browse files
authored
fix: paginate knowledge base dashboard lists (#9055)
* fix: paginate knowledge base dashboard lists * fix: preserve knowledge document search pagination
1 parent 3d4c4ed commit 758e432

10 files changed

Lines changed: 221 additions & 28 deletions

File tree

astrbot/core/knowledge_base/kb_db_sqlite.py

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -219,25 +219,45 @@ async def list_documents_by_kb(
219219
kb_id: str,
220220
offset: int = 0,
221221
limit: int = 100,
222+
search: str | None = None,
222223
) -> list[KBDocument]:
223-
"""列出知识库的所有文档"""
224+
"""List documents in a knowledge base.
225+
226+
Args:
227+
kb_id: Knowledge base ID.
228+
offset: Number of documents to skip.
229+
limit: Maximum number of documents to return.
230+
search: Optional partial match on document name; disabled when None or empty.
231+
232+
Returns:
233+
List of matching KBDocument rows.
234+
"""
224235
async with self.get_db() as session:
236+
stmt = select(KBDocument).where(col(KBDocument.kb_id) == kb_id)
237+
if search:
238+
stmt = stmt.where(col(KBDocument.doc_name).contains(search))
225239
stmt = (
226-
select(KBDocument)
227-
.where(col(KBDocument.kb_id) == kb_id)
228-
.offset(offset)
229-
.limit(limit)
230-
.order_by(desc(KBDocument.created_at))
240+
stmt.offset(offset).limit(limit).order_by(desc(KBDocument.created_at))
231241
)
232242
result = await session.execute(stmt)
233243
return list(result.scalars().all())
234244

235-
async def count_documents_by_kb(self, kb_id: str) -> int:
236-
"""统计知识库的文档数量"""
245+
async def count_documents_by_kb(self, kb_id: str, search: str | None = None) -> int:
246+
"""Count documents in a knowledge base.
247+
248+
Args:
249+
kb_id: Knowledge base ID.
250+
search: Optional partial match on document name; disabled when None or empty.
251+
252+
Returns:
253+
Total number of matching documents.
254+
"""
237255
async with self.get_db() as session:
238256
stmt = select(func.count(col(KBDocument.id))).where(
239257
col(KBDocument.kb_id) == kb_id,
240258
)
259+
if search:
260+
stmt = stmt.where(col(KBDocument.doc_name).contains(search))
241261
result = await session.execute(stmt)
242262
return result.scalar() or 0
243263

astrbot/core/knowledge_base/kb_helper.py

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -469,11 +469,37 @@ async def list_documents(
469469
self,
470470
offset: int = 0,
471471
limit: int = 100,
472+
search: str | None = None,
472473
) -> list[KBDocument]:
473-
"""列出知识库的所有文档"""
474-
docs = await self.kb_db.list_documents_by_kb(self.kb.kb_id, offset, limit)
474+
"""List documents in the knowledge base.
475+
476+
Args:
477+
offset: Number of documents to skip.
478+
limit: Maximum number of documents to return.
479+
search: Optional partial match on document name; disabled when None or empty.
480+
481+
Returns:
482+
List of matching KBDocument rows.
483+
"""
484+
docs = await self.kb_db.list_documents_by_kb(
485+
self.kb.kb_id,
486+
offset,
487+
limit,
488+
search=search,
489+
)
475490
return docs
476491

492+
async def count_documents(self, search: str | None = None) -> int:
493+
"""Count documents in the knowledge base.
494+
495+
Args:
496+
search: Optional partial match on document name; disabled when None or empty.
497+
498+
Returns:
499+
Total number of matching documents.
500+
"""
501+
return await self.kb_db.count_documents_by_kb(self.kb.kb_id, search=search)
502+
477503
async def get_document(self, doc_id: str) -> KBDocument | None:
478504
"""获取单个文档"""
479505
doc = await self.kb_db.get_document_by_id(doc_id)

astrbot/dashboard/api/knowledge_bases.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ async def list_knowledge_base_documents(
182182
kb_id=kb_id,
183183
page=_to_int(request.query_params.get("page"), 1),
184184
page_size=_to_int(request.query_params.get("page_size"), 100),
185+
search=request.query_params.get("search"),
185186
),
186187
prefix="获取文档列表失败",
187188
)
@@ -390,6 +391,7 @@ async def dashboard_list_documents(
390391
kb_id=request.query_params.get("kb_id"),
391392
page=_to_int(request.query_params.get("page"), 1),
392393
page_size=_to_int(request.query_params.get("page_size"), 100),
394+
search=request.query_params.get("search"),
393395
),
394396
prefix="获取文档列表失败",
395397
)

astrbot/dashboard/services/knowledge_base_service.py

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -266,16 +266,24 @@ async def background_import_task(
266266
async def list_kbs(self, *, page: int, page_size: int) -> dict[str, Any]:
267267
kb_manager = self.get_kb_manager()
268268
kbs = await kb_manager.list_kbs()
269+
total = len(kbs)
270+
271+
# Clamp page and page_size to at least 1 before calculating offsets/slices.
272+
page = max(page, 1)
273+
page_size = max(page_size, 1)
274+
start = (page - 1) * page_size
275+
end = start + page_size
276+
paged_kbs = kbs[start:end]
269277

270278
kb_list = []
271-
for kb in kbs:
279+
for kb in paged_kbs:
272280
kb_dict = kb.model_dump()
273281
kb_helper = await kb_manager.get_kb(kb.kb_id)
274282
if kb_helper and kb_helper.init_error:
275283
kb_dict["init_error"] = kb_helper.init_error
276284
kb_list.append(kb_dict)
277285

278-
return {"items": kb_list, "page": page, "page_size": page_size}
286+
return {"items": kb_list, "page": page, "page_size": page_size, "total": total}
279287

280288
async def list_kbs_from_dashboard_query(self, *, page, page_size) -> dict[str, Any]:
281289
return await self.list_kbs(
@@ -437,19 +445,33 @@ async def list_documents(
437445
kb_id: str | None,
438446
page: int,
439447
page_size: int,
448+
search: str | None = None,
440449
) -> dict[str, Any]:
441450
if not kb_id:
442451
raise KnowledgeBaseServiceError("缺少参数 kb_id")
443452
kb_helper = await self.get_kb_manager().get_kb(kb_id)
444453
if not kb_helper:
445454
raise KnowledgeBaseServiceError("知识库不存在")
446455

456+
if search is not None:
457+
search = search.strip()
458+
if not search:
459+
search = None
460+
461+
page = max(page, 1)
462+
page_size = max(page_size, 1)
447463
offset = (page - 1) * page_size
448-
doc_list = await kb_helper.list_documents(offset=offset, limit=page_size)
464+
doc_list = await kb_helper.list_documents(
465+
offset=offset,
466+
limit=page_size,
467+
search=search,
468+
)
469+
total = await kb_helper.count_documents(search=search)
449470
return {
450471
"items": [doc.model_dump() for doc in doc_list],
451472
"page": page,
452473
"page_size": page_size,
474+
"total": total,
453475
}
454476

455477
async def list_documents_from_dashboard_query(
@@ -458,11 +480,13 @@ async def list_documents_from_dashboard_query(
458480
kb_id: str | None,
459481
page,
460482
page_size,
483+
search: str | None = None,
461484
) -> dict[str, Any]:
462485
return await self.list_documents(
463486
kb_id=kb_id,
464487
page=self._to_int(page, 1),
465488
page_size=self._to_int(page_size, 100),
489+
search=search,
466490
)
467491

468492
async def upload_document(

dashboard/src/api/generated/openapi-v1/types.gen.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2661,6 +2661,10 @@ export type ListKnowledgeDocumentsData = {
26612661
query?: {
26622662
page?: number;
26632663
page_size?: number;
2664+
/**
2665+
* Filter documents by name (case-insensitive partial match).
2666+
*/
2667+
search?: string;
26642668
};
26652669
};
26662670

dashboard/src/api/v1.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1384,7 +1384,7 @@ export const knowledgeApi = {
13841384
openApiV1.deleteKnowledgeBase({ path: { kb_id: kbId } }),
13851385
);
13861386
},
1387-
documents(kbId: string, params?: { page?: number; page_size?: number }) {
1387+
documents(kbId: string, params?: { page?: number; page_size?: number; search?: string }) {
13881388
return typed<any>(
13891389
openApiV1.listKnowledgeDocuments({
13901390
path: { kb_id: kbId },

dashboard/src/views/knowledge-base/KBList.vue

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,15 @@
7979
</v-tooltip>
8080
</template>
8181
</OutlinedActionListItem>
82+
83+
<v-pagination
84+
v-if="total > pageSize"
85+
v-model="page"
86+
:length="Math.ceil(total / pageSize)"
87+
:total-visible="7"
88+
class="mt-4"
89+
@update:model-value="loadKnowledgeBases()"
90+
/>
8291
</div>
8392

8493
<!-- 空状态 -->
@@ -269,6 +278,9 @@ const loading = ref(false)
269278
const saving = ref(false)
270279
const deleting = ref(false)
271280
const kbList = ref<any[]>([])
281+
const page = ref(1)
282+
const pageSize = ref(20)
283+
const total = ref(0)
272284
const embeddingProviders = ref<any[]>([])
273285
const rerankProviders = ref<any[]>([])
274286
const originalEmbeddingProvider = ref<string | null>(null)
@@ -324,18 +336,18 @@ const emojiCategories = [
324336
const loadKnowledgeBases = async (refreshStats = false) => {
325337
loading.value = true
326338
try {
327-
const params: any = {}
328339
if (refreshStats) {
329-
params.refresh_stats = 'true'
340+
page.value = 1
330341
}
331-
332342
const response = await knowledgeApi.list({
333-
page: params.page,
334-
page_size: params.page_size,
335-
refresh_stats: params.refresh_stats === 'true'
343+
page: page.value,
344+
page_size: pageSize.value,
345+
refresh_stats: refreshStats
336346
})
337347
if (response.data.status === 'ok') {
338-
kbList.value = response.data.data.items || []
348+
const data = response.data.data
349+
kbList.value = data.items || []
350+
total.value = data.total || 0
339351
} else {
340352
showSnackbar(response.data.message || t('messages.loadError'), 'error')
341353
}
@@ -407,7 +419,9 @@ const deleteKB = async () => {
407419
408420
if (response.data.status === 'ok') {
409421
showSnackbar(t('messages.deleteSuccess'))
410-
// 先刷新列表,再关闭对话框
422+
if (kbList.value.length === 1 && page.value > 1) {
423+
page.value -= 1
424+
}
411425
await loadKnowledgeBases()
412426
showDeleteDialog.value = false
413427
deleteTarget.value = null

dashboard/src/views/knowledge-base/components/DocumentsTab.vue

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@
1111

1212
<!-- 文档列表 -->
1313
<v-card variant="outlined">
14-
<v-data-table :headers="headers" :items="documents" :loading="loading" :search="searchQuery" :items-per-page="10">
14+
<v-data-table-server :headers="headers" :items="documents" :loading="loading"
15+
:items-per-page="pageSize" :page="page" :items-length="total"
16+
@update:page="onPageChange" @update:items-per-page="onItemsPerPageChange">
1517
<template #item.doc_name="{ item }">
1618
<div class="d-flex align-center gap-2">
1719
<v-icon :color="getFileColor(item.file_type)" class="mr-2">
@@ -53,7 +55,7 @@
5355
<p class="mt-4 text-medium-emphasis">{{ t('documents.empty') }}</p>
5456
</div>
5557
</template>
56-
</v-data-table>
58+
</v-data-table-server>
5759
</v-card>
5860

5961
<!-- 上传对话框 -->
@@ -236,7 +238,7 @@
236238

237239
<script setup lang="ts">
238240
import TavilyKeyDialog from './TavilyKeyDialog.vue'
239-
import { ref, onMounted, onUnmounted, computed } from 'vue'
241+
import { ref, watch, onMounted, onUnmounted, computed } from 'vue'
240242
import { useRouter } from 'vue-router'
241243
import { configProfileApi, knowledgeApi, providerApi } from '@/api/v1'
242244
import { useModuleI18n } from '@/i18n/composables'
@@ -256,6 +258,9 @@ const loading = ref(false)
256258
const uploading = ref(false)
257259
const deleting = ref(false)
258260
const documents = ref<any[]>([])
261+
const page = ref(1)
262+
const pageSize = ref(10)
263+
const total = ref(0)
259264
const searchQuery = ref('')
260265
const showUploadDialog = ref(false)
261266
const showDeleteDialog = ref(false)
@@ -340,9 +345,15 @@ const headers = [
340345
const loadDocuments = async () => {
341346
loading.value = true
342347
try {
343-
const response = await knowledgeApi.documents(props.kbId)
348+
const response = await knowledgeApi.documents(props.kbId, {
349+
page: page.value,
350+
page_size: pageSize.value,
351+
search: searchQuery.value.trim() || undefined,
352+
})
344353
if (response.data.status === 'ok') {
345-
documents.value = response.data.data.items || []
354+
const data = response.data.data
355+
documents.value = data.items || []
356+
total.value = data.total || 0
346357
}
347358
} catch (error) {
348359
console.error('Failed to load documents:', error)
@@ -352,6 +363,18 @@ const loadDocuments = async () => {
352363
}
353364
}
354365
366+
// Handle pagination
367+
const onPageChange = (newPage: number) => {
368+
page.value = newPage
369+
loadDocuments()
370+
}
371+
372+
const onItemsPerPageChange = (newSize: number) => {
373+
pageSize.value = newSize
374+
page.value = 1
375+
loadDocuments()
376+
}
377+
355378
// 文件选择
356379
const handleFileSelect = (event: Event) => {
357380
const target = event.target as HTMLInputElement
@@ -591,7 +614,7 @@ const startProgressPolling = (taskId: string) => {
591614
// 移除上传中的占位文档
592615
documents.value = documents.value.filter(doc => doc.taskId !== taskId)
593616
594-
// 重新加载文档列表
617+
// Reload current page
595618
await loadDocuments()
596619
emit('refresh')
597620
@@ -684,6 +707,10 @@ const deleteDocument = async () => {
684707
if (response.data.status === 'ok') {
685708
showSnackbar(t('documents.deleteSuccess'))
686709
showDeleteDialog.value = false
710+
// If current page becomes empty after delete and is not the first page, go back one page
711+
if (documents.value.length === 1 && page.value > 1) {
712+
page.value -= 1
713+
}
687714
await loadDocuments()
688715
emit('refresh')
689716
} else {
@@ -782,6 +809,12 @@ const onTavilyKeySet = () => {
782809
checkTavilyConfig()
783810
}
784811
812+
// Reset to page 1 and reload when search text changes
813+
watch(searchQuery, () => {
814+
page.value = 1
815+
loadDocuments()
816+
})
817+
785818
onMounted(() => {
786819
loadDocuments()
787820
loadLlmProviders()

openspec/openapi-v1.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3446,6 +3446,12 @@ paths:
34463446
- $ref: "#/components/parameters/KbId"
34473447
- $ref: "#/components/parameters/Page"
34483448
- $ref: "#/components/parameters/PageSize"
3449+
- name: search
3450+
in: query
3451+
required: false
3452+
schema:
3453+
type: string
3454+
description: Filter documents by name (case-insensitive partial match).
34493455
responses:
34503456
"200":
34513457
$ref: "#/components/responses/Ok"

0 commit comments

Comments
 (0)