From 55db4aeb4552ffad241d4c3d6dbad2dc829a191e Mon Sep 17 00:00:00 2001 From: wutongyuonce <147830929+wutongyuonce@users.noreply.github.com> Date: Fri, 11 Sep 2026 00:04:32 +0800 Subject: [PATCH] feat: support PPTX uploads in knowledge base Route .pptx through MarkItDown, chunk as Markdown, and expose the format in the dashboard picker. Keep legacy .ppt rejected as an unsupported format. --- astrbot/core/knowledge_base/kb_helper.py | 13 ++++++ .../parsers/markitdown_parser.py | 2 +- astrbot/core/knowledge_base/parsers/util.py | 12 +++++- .../en-US/features/knowledge-base/detail.json | 2 +- .../ja-JP/features/knowledge-base/detail.json | 2 +- .../ru-RU/features/knowledge-base/detail.json | 2 +- .../zh-CN/features/knowledge-base/detail.json | 2 +- .../views/knowledge-base/DocumentDetail.vue | 1 + .../components/DocumentsTab.vue | 3 +- pyproject.toml | 2 +- requirements.txt | 2 +- tests/test_kb_pptx_parser.py | 42 +++++++++++++++++++ tests/unit/test_kb_upload_atomicity.py | 38 +++++++++++++++++ 13 files changed, 114 insertions(+), 9 deletions(-) create mode 100644 tests/test_kb_pptx_parser.py diff --git a/astrbot/core/knowledge_base/kb_helper.py b/astrbot/core/knowledge_base/kb_helper.py index 75234840cb..320a3967b6 100644 --- a/astrbot/core/knowledge_base/kb_helper.py +++ b/astrbot/core/knowledge_base/kb_helper.py @@ -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", @@ -334,6 +346,7 @@ async def upload_document( ".md", ".mdx", ".mkd", + ".pptx", ".rst", ".xls", ".xlsx", diff --git a/astrbot/core/knowledge_base/parsers/markitdown_parser.py b/astrbot/core/knowledge_base/parsers/markitdown_parser.py index 9ef347933a..30ff7c1ac1 100644 --- a/astrbot/core/knowledge_base/parsers/markitdown_parser.py +++ b/astrbot/core/knowledge_base/parsers/markitdown_parser.py @@ -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) diff --git a/astrbot/core/knowledge_base/parsers/util.py b/astrbot/core/knowledge_base/parsers/util.py index a98ed60ed2..ee48b47b20 100644 --- a/astrbot/core/knowledge_base/parsers/util.py +++ b/astrbot/core/knowledge_base/parsers/util.py @@ -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() diff --git a/dashboard/src/i18n/locales/en-US/features/knowledge-base/detail.json b/dashboard/src/i18n/locales/en-US/features/knowledge-base/detail.json index ffb79a70e8..60ca913c08 100644 --- a/dashboard/src/i18n/locales/en-US/features/knowledge-base/detail.json +++ b/dashboard/src/i18n/locales/en-US/features/knowledge-base/detail.json @@ -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", diff --git a/dashboard/src/i18n/locales/ja-JP/features/knowledge-base/detail.json b/dashboard/src/i18n/locales/ja-JP/features/knowledge-base/detail.json index babb6789ed..52cf168596 100644 --- a/dashboard/src/i18n/locales/ja-JP/features/knowledge-base/detail.json +++ b/dashboard/src/i18n/locales/ja-JP/features/knowledge-base/detail.json @@ -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": "バッチ処理設定", diff --git a/dashboard/src/i18n/locales/ru-RU/features/knowledge-base/detail.json b/dashboard/src/i18n/locales/ru-RU/features/knowledge-base/detail.json index 4fd446e563..663acfdab7 100644 --- a/dashboard/src/i18n/locales/ru-RU/features/knowledge-base/detail.json +++ b/dashboard/src/i18n/locales/ru-RU/features/knowledge-base/detail.json @@ -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": "Пакетная обработка", diff --git a/dashboard/src/i18n/locales/zh-CN/features/knowledge-base/detail.json b/dashboard/src/i18n/locales/zh-CN/features/knowledge-base/detail.json index 4bbcc69903..6741dfdf0f 100644 --- a/dashboard/src/i18n/locales/zh-CN/features/knowledge-base/detail.json +++ b/dashboard/src/i18n/locales/zh-CN/features/knowledge-base/detail.json @@ -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": "批处理设置", diff --git a/dashboard/src/views/knowledge-base/DocumentDetail.vue b/dashboard/src/views/knowledge-base/DocumentDetail.vue index 0b9b98f880..5b8a2f26d4 100644 --- a/dashboard/src/views/knowledge-base/DocumentDetail.vue +++ b/dashboard/src/views/knowledge-base/DocumentDetail.vue @@ -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' diff --git a/dashboard/src/views/knowledge-base/components/DocumentsTab.vue b/dashboard/src/views/knowledge-base/components/DocumentsTab.vue index e65ebbde06..5270bd8807 100644 --- a/dashboard/src/views/knowledge-base/components/DocumentsTab.vue +++ b/dashboard/src/views/knowledge-base/components/DocumentsTab.vue @@ -87,7 +87,7 @@
{{ t('upload.dropzone') }}
{{ t('upload.supportedFormats') }}
{{ t('upload.maxSize') }}
- @@ -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' diff --git a/pyproject.toml b/pyproject.toml index f2c6a16c97..d27446067d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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", diff --git a/requirements.txt b/requirements.txt index 9bcaa93a61..790f474225 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 diff --git a/tests/test_kb_pptx_parser.py b/tests/test_kb_pptx_parser.py new file mode 100644 index 0000000000..c61a7e5e72 --- /dev/null +++ b/tests/test_kb_pptx_parser.py @@ -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 diff --git a/tests/unit/test_kb_upload_atomicity.py b/tests/unit/test_kb_upload_atomicity.py index c7ad2d750b..dfdb88bb92 100644 --- a/tests/unit/test_kb_upload_atomicity.py +++ b/tests/unit/test_kb_upload_atomicity.py @@ -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"), @@ -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,