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
13 changes: 13 additions & 0 deletions astrbot/core/knowledge_base/kb_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,18 @@ async def upload_document(
parse_result = await parser.parse(file_content, file_name)
except KnowledgeBaseUploadError:
raise
except ValueError as exc:
message = str(exc)
if not message.startswith("暂时不支持的文件格式"):
message = (
"文档解析失败:无法读取或解析上传文件。"
"请确认文件格式受支持且文件内容未损坏。"
)
raise KnowledgeBaseUploadError(
stage="parsing",
user_message=message,
details={"file_name": file_name},
) from exc
except Exception as exc:
raise KnowledgeBaseUploadError(
stage="parsing",
Expand Down Expand Up @@ -334,6 +346,7 @@ async def upload_document(
".md",
".mdx",
".mkd",
".pptx",
".rst",
".xls",
".xlsx",
Expand Down
2 changes: 1 addition & 1 deletion astrbot/core/knowledge_base/parsers/markitdown_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@


class MarkitdownParser(BaseParser):
"""解析 docx, xls, xlsx 格式"""
"""解析 docx, xls, xlsx, pptx 格式"""

async def parse(self, file_content: bytes, file_name: str) -> ParseResult:
md = MarkItDown(enable_plugins=False)
Expand Down
12 changes: 11 additions & 1 deletion astrbot/core/knowledge_base/parsers/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,17 @@


async def select_parser(ext: str) -> BaseParser:
if ext in {".md", ".txt", ".markdown", ".rst", ".adoc", ".xlsx", ".docx", ".xls"}:
if ext in {
".md",
".txt",
".markdown",
".rst",
".adoc",
".xlsx",
".docx",
".xls",
".pptx",
}:
from .markitdown_parser import MarkitdownParser

return MarkitdownParser()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
"title": "Upload Document",
"selectFile": "Select File",
"dropzone": "Drop files here or click to select",
"supportedFormats": "Supported formats: .txt, .md, .markdown, .rst, .adoc, .pdf, .docx, .epub, .xls, .xlsx",
"supportedFormats": "Supported formats: .txt, .md, .markdown, .rst, .adoc, .pdf, .docx, .pptx, .epub, .xls, .xlsx",
"maxSize": "Max file size: 128MB",
"chunkSettings": "Chunk Settings",
"batchSettings": "Batch Settings",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
"title": "ドキュメントをアップロード",
"selectFile": "ファイルを選択",
"dropzone": "ここにファイルをドラッグ&ドロップするか、クリックして選択",
"supportedFormats": "対応形式:.txt, .md, .markdown, .rst, .adoc, .pdf, .docx, .epub, .xls, .xlsx",
"supportedFormats": "対応形式:.txt, .md, .markdown, .rst, .adoc, .pdf, .docx, .pptx, .epub, .xls, .xlsx",
"maxSize": "最大ファイルサイズ:128 MB",
"chunkSettings": "チャンク設定",
"batchSettings": "バッチ処理設定",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
"title": "Добавление контента",
"selectFile": "Файл",
"dropzone": "Нажмите или перетащите файл сюда",
"supportedFormats": "Форматы: .txt, .md, .markdown, .rst, .adoc, .pdf, .docx, .epub, .xls, .xlsx",
"supportedFormats": "Форматы: .txt, .md, .markdown, .rst, .adoc, .pdf, .docx, .pptx, .epub, .xls, .xlsx",
"maxSize": "Максимум: 128MB",
"chunkSettings": "Фрагментация",
"batchSettings": "Пакетная обработка",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
"title": "上传文档",
"selectFile": "选择文件",
"dropzone": "拖放文件到这里或点击选择",
"supportedFormats": "支持的格式: .txt, .md, .markdown, .rst, .adoc, .pdf, .docx, .epub, .xls, .xlsx",
"supportedFormats": "支持的格式: .txt, .md, .markdown, .rst, .adoc, .pdf, .docx, .pptx, .epub, .xls, .xlsx",
"maxSize": "最大文件大小: 128MB",
"chunkSettings": "分块设置",
"batchSettings": "批处理设置",
Expand Down
1 change: 1 addition & 0 deletions dashboard/src/views/knowledge-base/DocumentDetail.vue
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,7 @@ const deleteChunk = async (chunk: any) => {
const getFileIcon = (fileType: string) => {
const type = fileType?.toLowerCase() || ''
if (type.includes('pdf')) return 'mdi-file-pdf-box'
if (type.includes('ppt')) return 'mdi-file-powerpoint-box'
if (type.includes('epub')) return 'mdi-book-open-page-variant'
if (type.includes('md')) return 'mdi-language-markdown'
if (type.includes('txt')) return 'mdi-file-document-outline'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@
<p class="mt-4 text-h6">{{ t('upload.dropzone') }}</p>
<p class="text-caption text-medium-emphasis mt-2">{{ t('upload.supportedFormats') }}</p>
<p class="text-caption text-medium-emphasis">{{ t('upload.maxSize') }}</p>
<input ref="fileInput" type="file" multiple hidden accept=".txt,.md,.markdown,.rst,.adoc,.pdf,.docx,.epub,.xls,.xlsx"
<input ref="fileInput" type="file" multiple hidden accept=".txt,.md,.markdown,.rst,.adoc,.pdf,.docx,.pptx,.epub,.xls,.xlsx"
@change="handleFileSelect" />
</div>

Expand Down Expand Up @@ -730,6 +730,7 @@ const deleteDocument = async () => {
const getFileIcon = (fileType: string) => {
const type = fileType?.toLowerCase() || ''
if (type.includes('pdf')) return 'mdi-file-pdf-box'
if (type.includes('ppt')) return 'mdi-file-powerpoint-box'
if (type.includes('epub')) return 'mdi-book-open-page-variant'
if (type.includes('rst') || type.includes('adoc')) return 'mdi-file-document-outline'
if (type.includes('md') || type.includes('markdown')) return 'mdi-language-markdown'
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ dependencies = [
"aiofiles>=25.1.0",
"rank-bm25>=0.2.2",
"jieba>=0.42.1",
"markitdown-no-magika[docx,xls,xlsx]>=0.1.2",
"markitdown-no-magika[docx,xls,xlsx,pptx]>=0.1.2",
"xinference-client",
"tenacity>=9.1.2",
"shipyard-python-sdk>=0.2.4",
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ pysocks>=1.7.1
aiofiles>=25.1.0
rank-bm25>=0.2.2
jieba>=0.42.1
markitdown-no-magika[docx,xls,xlsx]>=0.1.2
markitdown-no-magika[docx,xls,xlsx,pptx]>=0.1.2
xinference-client
tenacity>=9.1.2
shipyard-python-sdk>=0.2.4
Expand Down
42 changes: 42 additions & 0 deletions tests/test_kb_pptx_parser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
from __future__ import annotations

import io

import pytest

from astrbot.core.knowledge_base.parsers.markitdown_parser import MarkitdownParser
from astrbot.core.knowledge_base.parsers.util import select_parser


def _make_pptx_bytes() -> bytes:
from pptx import Presentation

presentation = Presentation()
slide = presentation.slides.add_slide(presentation.slide_layouts[0])
slide.shapes.title.text = "KB PPTX Slide Title"
buffer = io.BytesIO()
presentation.save(buffer)
return buffer.getvalue()


@pytest.mark.asyncio
async def test_select_parser_supports_pptx():
parser = await select_parser(".pptx")

assert isinstance(parser, MarkitdownParser)


@pytest.mark.asyncio
async def test_select_parser_rejects_legacy_ppt():
with pytest.raises(ValueError, match="暂时不支持的文件格式"):
await select_parser(".ppt")


@pytest.mark.asyncio
async def test_pptx_parser_extracts_slide_text():
pytest.importorskip("pptx")

result = await MarkitdownParser().parse(_make_pptx_bytes(), "slides.pptx")

assert result.media == []
assert "KB PPTX Slide Title" in result.text
38 changes: 38 additions & 0 deletions tests/unit/test_kb_upload_atomicity.py
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,7 @@ async def fake_save_media(**kwargs):
("file_name", "file_type"),
[
("guide.docx", "docx"),
("guide.pptx", "pptx"),
("guide.xlsx", "xlsx"),
("guide.xls", "xls"),
("guide.rst", "rst"),
Expand Down Expand Up @@ -419,6 +420,43 @@ async def test_upload_document_preserves_markdown_heading_paths(
assert embedding_contents[2] == f"guide\n\n{contents[2]}"


@pytest.mark.asyncio
async def test_upload_document_rejects_legacy_ppt(
tmp_path: Path,
stub_provider_manager_module,
) -> None:
"""Legacy .ppt must fail as unsupported format, not as a corrupt file."""
KBHelper = _import_kb_helper()

helper = KBHelper.__new__(KBHelper)
helper.kb = KnowledgeBase(
kb_name="Test KB",
description="",
embedding_provider_id="emb",
)
helper.kb_db = MagicMock()
helper.kb_db.get_db = _failing_get_db()
helper.vec_db = AsyncMock()
helper.kb_medias_dir = tmp_path / "medias"
helper.chunker = AsyncMock()

with (
patch.object(helper, "_ensure_vec_db", new=AsyncMock()),
pytest.raises(KnowledgeBaseUploadError) as exc_info,
):
await helper.upload_document(
file_name="slides.ppt",
file_content=b"not a pptx",
file_type="ppt",
)

assert exc_info.value.stage == "parsing"
assert "暂时不支持的文件格式" in exc_info.value.user_message
assert ".ppt" in exc_info.value.user_message
helper.chunker.chunk.assert_not_awaited()
helper.vec_db.insert_batch.assert_not_awaited()


@pytest.mark.asyncio
async def test_upload_document_cleans_up_on_metadata_failure(
stub_provider_manager_module,
Expand Down