Skip to content
Open
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
16 changes: 16 additions & 0 deletions astrbot/core/knowledge_base/kb_db_sqlite.py
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,22 @@ async def delete_document_by_id(self, doc_id: str, vec_db: "FaissVecDB") -> None
# 在 vec db 中删除相关向量
await vec_db.delete_documents(metadata_filters={"kb_doc_id": doc_id})

async def delete_kb_by_id(self, kb_id: str) -> None:
"""删除知识库及其所有文档与多媒体记录。

KBDocument / KBMedia 通过 kb_id 关联,但没有 ORM / 外键级联,只删除
KnowledgeBase 行会把该库的全部文档和多媒体记录留成孤儿(与 #9120 给
单文档删除补的多媒体清理是同一类问题)。向量库由调用方单独清理。
"""
async with self.get_db() as session, session.begin():
await session.execute(delete(KBMedia).where(col(KBMedia.kb_id) == kb_id))
await session.execute(
delete(KBDocument).where(col(KBDocument.kb_id) == kb_id)
)
await session.execute(
delete(KnowledgeBase).where(col(KnowledgeBase.kb_id) == kb_id)
)

# ===== 多媒体查询 =====

async def list_media_by_doc(self, doc_id: str) -> list[KBMedia]:
Expand Down
4 changes: 1 addition & 3 deletions astrbot/core/knowledge_base/kb_mgr.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,9 +159,7 @@ async def delete_kb(self, kb_id: str) -> bool:
return False

await kb_helper.delete_vec_db()
async with self.kb_db.get_db() as session:
await session.delete(kb_helper.kb)
await session.commit()
await self.kb_db.delete_kb_by_id(kb_id)

self.kb_insts.pop(kb_id, None)
return True
Expand Down
61 changes: 61 additions & 0 deletions tests/unit/test_kb_document_cleanup.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,3 +197,64 @@ async def test_update_kb_stats_counts_chunks_for_single_kb(kb_db, seeded_kb):
mock_vec_db.count_documents.assert_awaited_once_with(
metadata_filter={"kb_id": kb_id1},
)


@pytest.mark.asyncio
async def test_delete_kb_cleans_documents_and_media(kb_db, seeded_media):
"""删除知识库时, 其下所有文档和多媒体记录应一并被删除, 不留孤儿。"""
kb_id, doc_id, _ = seeded_media

# 前置: KB / 文档 / media 都存在
assert await kb_db.get_kb_by_id(kb_id) is not None
assert await kb_db.get_document_by_id(doc_id) is not None
assert len(await kb_db.list_media_by_doc(doc_id)) == 2

await kb_db.delete_kb_by_id(kb_id)

# KB / 文档 / media 全部删除, 无孤儿残留
assert await kb_db.get_kb_by_id(kb_id) is None
assert await kb_db.get_document_by_id(doc_id) is None
assert await kb_db.list_media_by_doc(doc_id) == []


@pytest.mark.asyncio
async def test_delete_kb_keeps_other_kb_data(kb_db, seeded_media):
"""删除一个知识库不应影响其他知识库的文档和多媒体记录。"""
kb_id_a, doc_id_a, _ = seeded_media

# 第二个知识库 B, 带文档与 media
kb_b = KnowledgeBase(
kb_name="KB B", description="", embedding_provider_id="test-embedding"
)
async with kb_db.get_db() as session, session.begin():
session.add(kb_b)
await session.flush()
kb_id_b = kb_b.kb_id
doc_b = KBDocument(
kb_id=kb_id_b, doc_name="b.txt", file_type="txt", file_size=1, file_path=""
)
async with kb_db.get_db() as session, session.begin():
session.add(doc_b)
await session.flush()
doc_id_b = doc_b.doc_id
media_b = KBMedia(
doc_id=doc_id_b,
kb_id=kb_id_b,
media_type="image",
file_name="b.png",
file_path="/tmp/fake/b.png",
file_size=1,
mime_type="image/png",
)
async with kb_db.get_db() as session, session.begin():
session.add(media_b)

await kb_db.delete_kb_by_id(kb_id_a)

# A 全删
assert await kb_db.get_kb_by_id(kb_id_a) is None
assert await kb_db.get_document_by_id(doc_id_a) is None
# B 不受影响
assert await kb_db.get_kb_by_id(kb_id_b) is not None
assert await kb_db.get_document_by_id(doc_id_b) is not None
assert len(await kb_db.list_media_by_doc(doc_id_b)) == 1
Loading