Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 28 additions & 8 deletions astrbot/core/knowledge_base/kb_db_sqlite.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
30 changes: 28 additions & 2 deletions astrbot/core/knowledge_base/kb_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 2 additions & 0 deletions astrbot/dashboard/api/knowledge_bases.py
Original file line number Diff line number Diff line change
Expand Up @@ -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="获取文档列表失败",
)
Expand Down Expand Up @@ -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="获取文档列表失败",
)
Expand Down
30 changes: 27 additions & 3 deletions astrbot/dashboard/services/knowledge_base_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -266,16 +266,24 @@ 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)

# 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]
Comment thread
lxfight marked this conversation as resolved.

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(
Expand Down Expand Up @@ -437,19 +445,33 @@ 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")
kb_helper = await self.get_kb_manager().get_kb(kb_id)
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)
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,
"page_size": page_size,
"total": total,
}

async def list_documents_from_dashboard_query(
Expand All @@ -458,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(
Expand Down
4 changes: 4 additions & 0 deletions dashboard/src/api/generated/openapi-v1/types.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2640,6 +2640,10 @@ export type ListKnowledgeDocumentsData = {
query?: {
page?: number;
page_size?: number;
/**
* Filter documents by name (case-insensitive partial match).
*/
search?: string;
};
};

Expand Down
2 changes: 1 addition & 1 deletion dashboard/src/api/v1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<any>(
openApiV1.listKnowledgeDocuments({
path: { kb_id: kbId },
Expand Down
30 changes: 22 additions & 8 deletions dashboard/src/views/knowledge-base/KBList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,15 @@
</v-tooltip>
</template>
</OutlinedActionListItem>

<v-pagination
v-if="total > pageSize"
v-model="page"
:length="Math.ceil(total / pageSize)"
:total-visible="7"
class="mt-4"
@update:model-value="loadKnowledgeBases()"
/>
</div>

<!-- 空状态 -->
Expand Down Expand Up @@ -269,6 +278,9 @@ const loading = ref(false)
const saving = ref(false)
const deleting = ref(false)
const kbList = ref<any[]>([])
const page = ref(1)
const pageSize = ref(20)
const total = ref(0)
const embeddingProviders = ref<any[]>([])
const rerankProviders = ref<any[]>([])
const originalEmbeddingProvider = ref<string | null>(null)
Expand Down Expand Up @@ -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')
}
Expand Down Expand Up @@ -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
Expand Down
45 changes: 39 additions & 6 deletions dashboard/src/views/knowledge-base/components/DocumentsTab.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@

<!-- 文档列表 -->
<v-card variant="outlined">
<v-data-table :headers="headers" :items="documents" :loading="loading" :search="searchQuery" :items-per-page="10">
<v-data-table-server :headers="headers" :items="documents" :loading="loading"
:items-per-page="pageSize" :page="page" :items-length="total"
@update:page="onPageChange" @update:items-per-page="onItemsPerPageChange">
<template #item.doc_name="{ item }">
<div class="d-flex align-center gap-2">
<v-icon :color="getFileColor(item.file_type)" class="mr-2">
Expand Down Expand Up @@ -53,7 +55,7 @@
<p class="mt-4 text-medium-emphasis">{{ t('documents.empty') }}</p>
</div>
</template>
</v-data-table>
</v-data-table-server>
</v-card>

<!-- 上传对话框 -->
Expand Down Expand Up @@ -236,7 +238,7 @@

<script setup lang="ts">
import TavilyKeyDialog from './TavilyKeyDialog.vue'
import { ref, onMounted, onUnmounted, computed } from 'vue'
import { ref, watch, onMounted, onUnmounted, computed } from 'vue'
import { useRouter } from 'vue-router'
import { configProfileApi, knowledgeApi, providerApi } from '@/api/v1'
import { useModuleI18n } from '@/i18n/composables'
Expand All @@ -256,6 +258,9 @@ const loading = ref(false)
const uploading = ref(false)
const deleting = ref(false)
const documents = ref<any[]>([])
const page = ref(1)
const pageSize = ref(10)
const total = ref(0)
const searchQuery = ref('')
const showUploadDialog = ref(false)
const showDeleteDialog = ref(false)
Expand Down Expand Up @@ -340,9 +345,15 @@ 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,
search: searchQuery.value.trim() || undefined,
})
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)
Expand All @@ -352,6 +363,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
Expand Down Expand Up @@ -591,7 +614,7 @@ const startProgressPolling = (taskId: string) => {
// 移除上传中的占位文档
documents.value = documents.value.filter(doc => doc.taskId !== taskId)

// 重新加载文档列表
// Reload current page
await loadDocuments()
emit('refresh')

Expand Down Expand Up @@ -684,6 +707,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 {
Expand Down Expand Up @@ -782,6 +809,12 @@ const onTavilyKeySet = () => {
checkTavilyConfig()
}

// Reset to page 1 and reload when search text changes
watch(searchQuery, () => {
page.value = 1
loadDocuments()
})

onMounted(() => {
loadDocuments()
loadLlmProviders()
Expand Down
6 changes: 6 additions & 0 deletions openspec/openapi-v1.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3430,6 +3430,12 @@ paths:
- $ref: "#/components/parameters/KbId"
- $ref: "#/components/parameters/Page"
- $ref: "#/components/parameters/PageSize"
- name: search
in: query
required: false
schema:
type: string
description: Filter documents by name (case-insensitive partial match).
responses:
"200":
$ref: "#/components/responses/Ok"
Expand Down
Loading
Loading