From d1112558c6ccbe4ce37314bccd2acfe24c3358f9 Mon Sep 17 00:00:00 2001 From: GEOFlow Local Dev Date: Thu, 10 Sep 2026 16:05:16 +0800 Subject: [PATCH 1/7] feat(knowledge): support PDF and PPTX uploads for knowledge base and enterprise knowledge Add PDF and PPTX as supported file types for knowledge base and enterprise knowledge uploads. Legacy PPT binary remains unsupported. Changes: - composer.json / composer.lock: add smalot/pdfparser ^2.12 (pure-PHP PDF text extraction) - app/Http/Controllers/Admin/EnterpriseKnowledgeController.php: extend File::types() with pdf, ppt, pptx - app/Http/Controllers/Admin/KnowledgeBaseController.php: same validation + inline parser dispatch for PDF/PPTX, legacy PPT error - app/Services/GeoFlow/KnowledgeSourceParser.php: add extractPdfContent() and extractPptxContent() with safe XML expansion guards; extend parseUploadedKnowledgeFile() dispatch; resolveKnowledgeFileType() accepts 'pdf' and 'presentation' types - resources/views/admin/enterprise-knowledge/create.blade.php: update with .pdf,.ppt,.pptx - resources/views/admin/knowledge-bases/form.blade.php: update and JS allowedExtensions - lang/{zh_CN,en,pt_BR}/admin.php: update dropzone / upload_desc / import_rule / file_type_invalid strings + format_pdf / format_pptx + pptx_expansion_too_large + ppt_legacy_not_supported error keys - tests/Feature/KnowledgeSourceParserSafetyTest.php: add 4 new tests (legacy PPT rejection, empty PPTX rejection, high-compression PPTX guard, single-slide PPTX parsing) - .gitignore: exclude AI tool caches (.agents/, .boost/, .claude/, .codex/, .cursor/, .gemini/), per-machine debug artifacts (_route_refs.txt, .bak/, .untracked-stash/) Test evidence: php artisan test --filter=KnowledgeSourceParserSafetyTest => 6 passed (17 assertions); broader KnowledgeBase / EnterpriseKnowledge suite => 72 passed (479 assertions) with no regressions. Smoke verification (PPTX with one slide): PPTX with content: presentation -> '# Slide 1\n\nHello Slide Content' Empty PPTX -> file_type_invalid error Legacy PPT -> ppt_legacy_not_supported error --- .gitignore | 15 ++ .../Admin/EnterpriseKnowledgeController.php | 2 +- .../Admin/KnowledgeBaseController.php | 32 +++- .../GeoFlow/KnowledgeSourceParser.php | 169 +++++++++++++++++- composer.json | 3 +- composer.lock | 57 +++++- lang/en/admin.php | 12 +- lang/pt_BR/admin.php | 10 +- lang/zh_CN/admin.php | 14 +- .../enterprise-knowledge/create.blade.php | 2 +- .../admin/knowledge-bases/form.blade.php | 4 +- .../KnowledgeSourceParserSafetyTest.php | 111 ++++++++++++ 12 files changed, 410 insertions(+), 21 deletions(-) diff --git a/.gitignore b/.gitignore index e28e95c54..66657caf9 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,18 @@ __pycache__/ Homestead.json Homestead.yaml Thumbs.db + +# AI tool caches (per-machine) +.agents/ +.boost/ +.claude/ +.codex/ +.cursor/ +.gemini/ +.bak/ +.dockerignore.local + +_route_refs.txt + +.untracked-stash/ + diff --git a/app/Http/Controllers/Admin/EnterpriseKnowledgeController.php b/app/Http/Controllers/Admin/EnterpriseKnowledgeController.php index 73008a247..2b0f15c8b 100644 --- a/app/Http/Controllers/Admin/EnterpriseKnowledgeController.php +++ b/app/Http/Controllers/Admin/EnterpriseKnowledgeController.php @@ -67,7 +67,7 @@ public function store(Request $request): RedirectResponse 'description' => ['nullable', 'string', 'max:1000'], 'content' => ['nullable', 'string'], 'enterprise_files' => ['nullable', 'array', 'max:10'], - 'enterprise_files.*' => ['file', File::types(['txt', 'md', 'markdown', 'docx'])->max(8 * 1024)], + 'enterprise_files.*' => ['file', File::types(['txt', 'md', 'markdown', 'docx', 'pdf', 'ppt', 'pptx'])->max(8 * 1024)], ], [ 'enterprise_files.max' => __('admin.enterprise_knowledge.error.files_limit'), ]); diff --git a/app/Http/Controllers/Admin/KnowledgeBaseController.php b/app/Http/Controllers/Admin/KnowledgeBaseController.php index 2dd9fa8dd..1c18d5e3d 100644 --- a/app/Http/Controllers/Admin/KnowledgeBaseController.php +++ b/app/Http/Controllers/Admin/KnowledgeBaseController.php @@ -566,9 +566,9 @@ private function validateKnowledgeImportForm(Request $request): array 'risk_level' => ['nullable', 'in:low,medium,high'], 'review_status' => ['nullable', 'in:unreviewed,reviewed'], 'import_action' => ['nullable', 'in:save,save_and_chunk'], - 'knowledge_file' => ['nullable', File::types(['txt', 'md', 'docx'])->max(8 * 1024)], + 'knowledge_file' => ['nullable', File::types(['txt', 'md', 'docx', 'pdf', 'ppt', 'pptx'])->max(8 * 1024)], 'knowledge_files' => ['nullable', 'array', 'max:10'], - 'knowledge_files.*' => ['file', File::types(['txt', 'md', 'docx'])->max(8 * 1024)], + 'knowledge_files.*' => ['file', File::types(['txt', 'md', 'docx', 'pdf', 'ppt', 'pptx'])->max(8 * 1024)], ], [ 'knowledge_file.mimes' => __('admin.knowledge_bases.error.file_type_invalid'), 'knowledge_file.max' => __('admin.knowledge_bases.error.file_too_large'), @@ -1106,6 +1106,34 @@ private function parseUploadedKnowledgeFile(string $absolutePath, string $origin ]; } + if ($extension === 'pdf') { + $content = app(\App\Services\GeoFlow\KnowledgeSourceParser::class)->extractPdfContent($absolutePath); + if ($content === '') { + throw new \RuntimeException(__('admin.knowledge_bases.error.file_type_invalid')); + } + + return [ + 'content' => $content, + 'file_type' => 'pdf', + ]; + } + + if ($extension === 'pptx') { + $content = app(\App\Services\GeoFlow\KnowledgeSourceParser::class)->extractPptxContent($absolutePath); + if ($content === '') { + throw new \RuntimeException(__('admin.knowledge_bases.error.file_type_invalid')); + } + + return [ + 'content' => $content, + 'file_type' => 'presentation', + ]; + } + + if ($extension === 'ppt') { + throw new \RuntimeException(__('admin.knowledge_bases.error.ppt_legacy_not_supported')); + } + throw new \RuntimeException(__('admin.knowledge_bases.error.file_type_invalid')); } diff --git a/app/Services/GeoFlow/KnowledgeSourceParser.php b/app/Services/GeoFlow/KnowledgeSourceParser.php index 6ae791b0b..ce06decf2 100644 --- a/app/Services/GeoFlow/KnowledgeSourceParser.php +++ b/app/Services/GeoFlow/KnowledgeSourceParser.php @@ -5,6 +5,7 @@ use Illuminate\Http\Request; use Illuminate\Http\UploadedFile; use Illuminate\Support\Facades\Storage; +use Smalot\PdfParser\Parser as SmalotPdfParser; final class KnowledgeSourceParser { @@ -14,6 +15,12 @@ final class KnowledgeSourceParser private const MAX_DOCX_COMPRESSION_RATIO = 100; + private const MAX_PPTX_XML_BYTES = 16 * 1024 * 1024; + + private const MAX_PPTX_COMPRESSION_RATIO = 100; + + private const MAX_PDF_BYTES = 32 * 1024 * 1024; + public function storeUploadedKnowledgeFile(UploadedFile $file, string $relativeDirectory = 'uploads/knowledge'): string { $extension = strtolower($file->getClientOriginalExtension() ?: 'txt'); @@ -191,7 +198,7 @@ public function resolveKnowledgeFileType(string $requestedType, string $manualCo $fileType = (string) ($parsedFiles[0]['file_type'] ?? 'markdown'); - return in_array($fileType, ['markdown', 'word', 'text'], true) ? $fileType : 'markdown'; + return in_array($fileType, ['markdown', 'word', 'text', 'pdf', 'presentation'], true) ? $fileType : 'markdown'; } /** @@ -249,6 +256,34 @@ public function parseUploadedKnowledgeFile(string $absolutePath, string $origina ]; } + if ($extension === 'pdf') { + $content = $this->extractPdfContent($absolutePath); + if ($content === '') { + throw new \RuntimeException(__('admin.knowledge_bases.error.file_type_invalid')); + } + + return [ + 'content' => $content, + 'file_type' => 'pdf', + ]; + } + + if ($extension === 'pptx') { + $content = $this->extractPptxContent($absolutePath); + if ($content === '') { + throw new \RuntimeException(__('admin.knowledge_bases.error.file_type_invalid')); + } + + return [ + 'content' => $content, + 'file_type' => 'presentation', + ]; + } + + if ($extension === 'ppt') { + throw new \RuntimeException(__('admin.knowledge_bases.error.ppt_legacy_not_supported')); + } + throw new \RuntimeException(__('admin.knowledge_bases.error.file_type_invalid')); } @@ -434,6 +469,138 @@ public function extractDocxContent(string $absolutePath): string return is_string($content) ? $this->normalizeKnowledgeText($content) : ''; } + + public function extractPdfContent(string $absolutePath): string + { + if (@filesize($absolutePath) > self::MAX_PDF_BYTES) { + throw new \RuntimeException(__('admin.knowledge_bases.error.file_too_large')); + } + if (! class_exists(SmalotPdfParser::class)) { + return ''; + } + + try { + $parser = new SmalotPdfParser; + $document = $parser->parseFile($absolutePath); + $text = $document->getText(); + if (! is_string($text) || $text === '') { + return ''; + } + + if (strlen($text) > self::MAX_KNOWLEDGE_BYTES) { + throw new \RuntimeException(__('admin.knowledge_bases.error.content_too_large')); + } + + return $this->normalizeKnowledgeText($this->convertUploadedTextToUtf8($text)); + } catch (\Throwable $exception) { + return ''; + } + } + + public function extractPptxContent(string $absolutePath): string + { + if (! class_exists('ZipArchive') || ! class_exists('XMLReader')) { + return ''; + } + + $zip = new \ZipArchive; + if ($zip->open($absolutePath) !== true) { + return ''; + } + + $slideNames = []; + for ($i = 0; $i < $zip->numFiles; $i++) { + $name = $zip->getNameIndex($i); + if (is_string($name) && preg_match('#^ppt/slides/slide\d+\.xml$#', $name)) { + $slideNames[] = $name; + } + } + sort($slideNames, SORT_NATURAL); + + if ($slideNames === []) { + $zip->close(); + return ''; + } + + $presentationNamespace = 'http://schemas.openxmlformats.org/drawingml/2006/main'; + $textOutput = tmpfile(); + if (! is_resource($textOutput)) { + $zip->close(); + return ''; + } + + $slideNumber = 0; + $contentBytes = 0; + foreach ($slideNames as $slideName) { + $stat = $zip->statName($slideName); + $uncompressedSize = is_array($stat) ? max(0, (int) ($stat['size'] ?? 0)) : 0; + $compressedSize = is_array($stat) ? max(1, (int) ($stat['comp_size'] ?? 0)) : 1; + if ($uncompressedSize > self::MAX_PPTX_XML_BYTES || ($uncompressedSize / $compressedSize) > self::MAX_PPTX_COMPRESSION_RATIO) { + fclose($textOutput); + $zip->close(); + throw new \RuntimeException(__('admin.knowledge_bases.error.pptx_expansion_too_large')); + } + + $source = $zip->getStream($slideName); + if (! is_resource($source)) { + continue; + } + + $temporary = tmpfile(); + if (! is_resource($temporary)) { + fclose($source); + fclose($textOutput); + $zip->close(); + return ''; + } + + $copiedBytes = stream_copy_to_stream($source, $temporary, self::MAX_PPTX_XML_BYTES + 1); + fclose($source); + if (! is_int($copiedBytes) || $copiedBytes > self::MAX_PPTX_XML_BYTES) { + fclose($temporary); + fclose($textOutput); + $zip->close(); + throw new \RuntimeException(__('admin.knowledge_bases.error.pptx_expansion_too_large')); + } + + $metadata = stream_get_meta_data($temporary); + $temporaryPath = (string) ($metadata['uri'] ?? ''); + $reader = new \XMLReader; + if ($temporaryPath !== '' && @$reader->open($temporaryPath, null, LIBXML_NONET | LIBXML_NOERROR | LIBXML_NOWARNING)) { + $slideNumber++; + fwrite($textOutput, "\n\n# Slide " . $slideNumber . "\n\n"); + while ($reader->read()) { + if ( + $reader->nodeType === \XMLReader::ELEMENT + && $reader->localName === 't' + && $reader->namespaceURI === $presentationNamespace + ) { + $value = trim($reader->readString()); + if ($value !== '') { + $contentBytes += strlen($value) + 1; + if ($contentBytes > self::MAX_KNOWLEDGE_BYTES) { + $reader->close(); + fclose($temporary); + fclose($textOutput); + $zip->close(); + throw new \RuntimeException(__('admin.knowledge_bases.error.content_too_large')); + } + fwrite($textOutput, $value . "\n"); + } + } + } + $reader->close(); + } + fclose($temporary); + } + $zip->close(); + rewind($textOutput); + $content = stream_get_contents($textOutput, self::MAX_KNOWLEDGE_BYTES + 1); + fclose($textOutput); + + return is_string($content) ? $this->normalizeKnowledgeText($content) : ''; + } + private function assertContentSize(string $content): void { if (strlen($content) > self::MAX_KNOWLEDGE_BYTES) { diff --git a/composer.json b/composer.json index f190edbbc..742677cc3 100644 --- a/composer.json +++ b/composer.json @@ -13,7 +13,8 @@ "laravel/horizon": "^5.45", "laravel/reverb": "^1.0", "laravel/sanctum": "^4.3", - "laravel/tinker": "^2.10.1" + "laravel/tinker": "^2.10.1", + "smalot/pdfparser": "^2.12" }, "require-dev": { "fakerphp/faker": "^1.23", diff --git a/composer.lock b/composer.lock index 21bec22aa..154e7031f 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,11 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], + "content-hash": "fd791f4005dc2d8a7061adee81d530ef", + + "content-hash": "8427f7b21ba65e63d42577f0328c27c6", + "packages": [ { "name": "aws/aws-crt-php", @@ -4871,6 +4875,57 @@ ], "time": "2024-06-11T12:45:25+00:00" }, + { + "name": "smalot/pdfparser", + "version": "v2.12.5", + "source": { + "type": "git", + "url": "https://github.com/smalot/pdfparser.git", + "reference": "2cfa0d92bd557875c9f52a75fde0e8392302a354" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/smalot/pdfparser/zipball/2cfa0d92bd557875c9f52a75fde0e8392302a354", + "reference": "2cfa0d92bd557875c9f52a75fde0e8392302a354", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "ext-zlib": "*", + "php": ">=7.1", + "symfony/polyfill-mbstring": "^1.18" + }, + "type": "library", + "autoload": { + "psr-0": { + "Smalot\\PdfParser\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0" + ], + "authors": [ + { + "name": "Sebastien MALOT", + "email": "sebastien@malot.fr" + } + ], + "description": "Pdf parser library. Can read and extract information from pdf file.", + "homepage": "https://www.pdfparser.org", + "keywords": [ + "extract", + "parse", + "parser", + "pdf", + "text" + ], + "support": { + "issues": "https://github.com/smalot/pdfparser/issues", + "source": "https://github.com/smalot/pdfparser/tree/v2.12.5" + }, + "time": "2026-04-17T11:37:58+00:00" + }, { "name": "symfony/clock", "version": "v7.4.8", @@ -10252,5 +10307,5 @@ "platform-overrides": { "php": "8.3.0" }, - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.9.0" } diff --git a/lang/en/admin.php b/lang/en/admin.php index b7d64e2a0..84209dfbc 100644 --- a/lang/en/admin.php +++ b/lang/en/admin.php @@ -3996,7 +3996,7 @@ 'source_files_title' => 'Upload Documents', 'source_files_desc' => 'Batch upload files. They will be cleaned and merged in file order.', 'dropzone_title' => 'Click to choose files or drag them here', - 'dropzone_desc' => 'TXT, MD, and DOCX are supported. Save legacy DOC files as DOCX first.', + 'dropzone_desc' => 'TXT, MD, DOCX, PDF, and PPTX are supported. Save legacy DOC/PPT files as DOCX/PPTX first.', 'upload_limits' => 'Up to 10 files, with an 8MB per-file and combined limit', 'content_counter' => ':count chars', 'file_status_ready' => 'Ready', @@ -4013,7 +4013,7 @@ 'pipeline_vector' => 'Chunk and Vectorize', 'pipeline_vector_desc' => 'Save the source and build chunks for later vector writing.', 'import_rules_title' => 'Import Rules', - 'import_rule_formats' => 'TXT, MD, and DOCX are supported. Legacy DOC is not parsed directly.', + 'import_rule_formats' => 'TXT, MD, DOCX, PDF, and PPTX are supported. Legacy DOC/PPT is not parsed directly.', 'import_rule_merge' => 'Multiple sources receive source headings for review and traceability.', 'import_rule_server' => 'If uploads are blocked by deployment limits, adjust PHP/Nginx upload settings.', 'import_submit_only' => 'Submit', @@ -4034,6 +4034,8 @@ 'imported_multi_file_name_with_first' => ':name and :count source files', 'format_help' => 'Supported file formats:', 'format_txt' => 'TXT - plain text file', + 'format_pdf' => 'PDF - PDF document', + 'format_pptx' => 'PPTX - PowerPoint presentation', 'format_md' => 'MD - Markdown file', 'format_docx' => 'DOCX - Word document with automatic body extraction', 'format_doc' => 'DOC - legacy Word document; please save as DOCX before uploading', @@ -4082,8 +4084,10 @@ 'total_files_too_large' => 'The combined upload size cannot exceed 8MB.', 'content_too_large' => 'Knowledge base content cannot exceed 8MB.', 'docx_expansion_too_large' => 'The expanded DOCX content is too large or has an unsafe compression ratio.', + 'pptx_expansion_too_large' => 'The expanded PPTX content is too large or has an unsafe compression ratio.', + 'ppt_legacy_not_supported' => 'Legacy PPT binary format is not supported. Save the file as PPTX before uploading.', 'files_limit' => 'Upload up to 10 knowledge documents at a time.', - 'file_type_invalid' => 'Unsupported file type. Upload TXT, MD, or DOCX files.', + 'file_type_invalid' => 'Unsupported file type. Upload TXT, MD, DOCX, PDF, or PPTX files.', 'in_use' => 'This knowledge base is referenced by :count tasks. Remove the references before deleting it.', 'system_delete_forbidden' => 'This knowledge base powers the AI Workspace and cannot be deleted. You may edit it or restore a revision from its detail page.', 'not_found' => 'Knowledge base not found', @@ -4155,7 +4159,7 @@ 'upload_title' => 'Upload Company Materials', 'upload_desc' => 'Supports TXT, Markdown, and DOCX. Up to 10 files, with an 8MB per-file and combined limit.', 'dropzone_title' => 'Click to select files or drag them here', - 'dropzone_desc' => 'Supports TXT, MD, and DOCX. Save legacy DOC files as DOCX first.', + 'dropzone_desc' => 'Supports TXT, MD, DOCX, PDF, and PPTX. Save legacy DOC/PPT files as DOCX/PPTX first.', 'drop_fallback' => 'This browser cannot assign dropped files directly. Please click the upload area to select files.', 'content_title' => 'Supplemental Text', 'content_desc' => 'Paste website copy, product docs, sales materials, FAQ, or banned phrases.', diff --git a/lang/pt_BR/admin.php b/lang/pt_BR/admin.php index 1dfc3a96a..75da00eb4 100644 --- a/lang/pt_BR/admin.php +++ b/lang/pt_BR/admin.php @@ -3799,7 +3799,7 @@ 'source_files_title' => 'Enviar Documentos', 'source_files_desc' => 'Envie arquivos em lote. Eles serão limpos e mesclados na ordem enviada.', 'dropzone_title' => 'Clique para escolher ou arraste arquivos aqui', - 'dropzone_desc' => 'TXT, MD e DOCX são suportados. Salve DOC antigo como DOCX antes.', + 'dropzone_desc' => 'TXT, MD, DOCX, PDF e PPTX são suportados. Salve DOC/PPT antigos como DOCX/PPTX antes.', 'upload_limits' => 'Até 10 arquivos, com limite de 8MB por arquivo e no total', 'content_counter' => ':count caracteres', 'file_status_ready' => 'Pronto', @@ -3816,7 +3816,7 @@ 'pipeline_vector' => 'Fragmentar e Vetorizar', 'pipeline_vector_desc' => 'Salva o conteúdo e cria fragmentos para vetores futuros.', 'import_rules_title' => 'Regras de Importação', - 'import_rule_formats' => 'TXT, MD e DOCX são suportados. DOC antigo não é analisado diretamente.', + 'import_rule_formats' => 'TXT, MD, DOCX, PDF e PPTX são suportados. DOC/PPT antigos não são analisados diretamente.', 'import_rule_merge' => 'Várias fontes recebem títulos para revisão e rastreabilidade.', 'import_rule_server' => 'Se o servidor bloquear uploads, ajuste os limites de PHP/Nginx.', 'import_submit_only' => 'Enviar', @@ -3837,6 +3837,8 @@ 'imported_multi_file_name_with_first' => ':name e :count fontes', 'format_help' => 'Formatos suportados:', 'format_txt' => 'TXT', + 'format_pdf' => 'PDF', + 'format_pptx' => 'PPTX', 'format_md' => 'MD - Markdown', 'format_docx' => 'DOCX - Word', 'confirm_delete' => 'Excluir base ":name"?', @@ -3848,6 +3850,8 @@ 'total_files_too_large' => 'O tamanho total dos arquivos não pode exceder 8MB.', 'content_too_large' => 'O conteúdo da base não pode exceder 8MB.', 'docx_expansion_too_large' => 'O conteúdo expandido do DOCX é muito grande ou tem uma taxa de compressão insegura.', + 'pptx_expansion_too_large' => 'O conteúdo expandido do PPTX é muito grande ou tem uma taxa de compressão insegura.', + 'ppt_legacy_not_supported' => 'O formato PPT legado não é suportado. Salve o arquivo como PPTX antes de enviar.', 'files_limit' => 'Envie no máximo 10 documentos por vez', 'file_type_invalid' => 'Tipo não suportado', 'in_use' => 'Base referenciada por :count tarefas', @@ -3931,7 +3935,7 @@ 'upload_title' => 'Enviar Materiais da Empresa', 'upload_desc' => 'Suporta TXT, Markdown e DOCX. Até 10 arquivos, com limite de 8MB por arquivo e no total.', 'dropzone_title' => 'Clique para selecionar ou arraste arquivos aqui', - 'dropzone_desc' => 'Suporta TXT, MD e DOCX. Salve arquivos DOC antigos como DOCX primeiro.', + 'dropzone_desc' => 'Suporta TXT, MD, DOCX, PDF e PPTX. Salve arquivos DOC/PPT antigos como DOCX/PPTX primeiro.', 'drop_fallback' => 'Este navegador não permite atribuir arquivos arrastados diretamente. Clique na área de upload para selecionar.', 'content_title' => 'Texto Complementar', 'content_desc' => 'Cole textos do site, documentos de produto, materiais comerciais, FAQ ou expressões proibidas.', diff --git a/lang/zh_CN/admin.php b/lang/zh_CN/admin.php index ad64b81eb..d99dfe902 100644 --- a/lang/zh_CN/admin.php +++ b/lang/zh_CN/admin.php @@ -4056,7 +4056,7 @@ 'source_files_title' => '上传文档', 'source_files_desc' => '支持批量上传,系统会按文件顺序清洗并合并。', 'dropzone_title' => '点击选择或拖拽文件到这里', - 'dropzone_desc' => '支持 TXT、MD、DOCX,旧版 DOC 请先另存为 DOCX。', + 'dropzone_desc' => '支持 TXT、MD、DOCX、PDF、PPT、PPTX;旧版 PPT/DOC 请先另存为新版格式。', 'upload_limits' => '最多 10 个文件,单文件及合计均不超过 8MB', 'content_counter' => ':count 字', 'file_status_ready' => '待合并', @@ -4073,7 +4073,7 @@ 'pipeline_vector' => '切片向量化', 'pipeline_vector_desc' => '保存后同步生成知识片段,后续可写入向量。', 'import_rules_title' => '导入规则', - 'import_rule_formats' => '支持 TXT、MD、DOCX;暂不直接解析旧版 DOC。', + 'import_rule_formats' => '支持 TXT、MD、DOCX、PDF、PPTX;暂不直接解析旧版 DOC/PPT。', 'import_rule_merge' => '多来源会自动添加来源标题,方便后续查看和追溯。', 'import_rule_server' => '若部署环境限制上传大小,请同步调整 PHP/Nginx 的上传限制。', 'import_submit_only' => '提交', @@ -4094,6 +4094,8 @@ 'imported_multi_file_name_with_first' => ':name 等 :count 份资料', 'format_help' => '支持的文件格式:', 'format_txt' => 'TXT - 纯文本文件', + 'format_pdf' => 'PDF - PDF 文档', + 'format_pptx' => 'PPTX - PowerPoint 演示文稿', 'format_md' => 'MD - Markdown文件', 'format_docx' => 'DOCX - Word文档,支持自动提取正文', 'format_doc' => 'DOC - 旧版 Word 文档,请先另存为 DOCX 后上传', @@ -4142,8 +4144,10 @@ 'total_files_too_large' => '本次上传文件合计不能超过 8MB', 'content_too_large' => '知识库正文不能超过 8MB', 'docx_expansion_too_large' => 'DOCX 解压后的正文过大或压缩比异常,已停止解析', + 'pptx_expansion_too_large' => 'PPTX 解压后的正文过大或压缩比异常,已停止解析', + 'ppt_legacy_not_supported' => '旧版 PPT 二进制格式暂不支持解析,请先另存为 PPTX 再上传', 'files_limit' => '一次最多上传 10 个知识文档', - 'file_type_invalid' => '不支持的文件格式,请上传 TXT、MD 或 DOCX 文件', + 'file_type_invalid' => '不支持的文件格式,请上传 TXT、MD、DOCX、PDF、PPTX 文件', 'in_use' => '该知识库正在被 :count 个任务引用,请先解除引用后再删除', 'system_delete_forbidden' => '该知识库是 AI 工作台的系统知识,不能删除;可以在详情页编辑或恢复修订。', 'not_found' => '知识库不存在', @@ -4213,9 +4217,9 @@ 'description' => '说明', 'description_placeholder' => '记录资料范围、业务线或适用任务', 'upload_title' => '上传企业资料', - 'upload_desc' => '支持 TXT、Markdown、DOCX,最多 10 个文件,单文件及合计均不超过 8MB。', + 'upload_desc' => '支持 TXT、Markdown、DOCX、PDF、PPTX,最多 10 个文件,单文件及合计均不超过 8MB。', 'dropzone_title' => '点击选择或拖拽文件到这里', - 'dropzone_desc' => '支持 TXT、MD、DOCX;旧版 DOC 请先另存为 DOCX。', + 'dropzone_desc' => '支持 TXT、MD、DOCX、PDF、PPTX;旧版 PPT/DOC 请先另存为新版格式。', 'drop_fallback' => '当前浏览器不支持直接拖拽写入文件,请点击上传区域选择文件。', 'content_title' => '补充文本', 'content_desc' => '可以粘贴官网、产品手册、销售资料、FAQ 或禁用表述。', diff --git a/resources/views/admin/enterprise-knowledge/create.blade.php b/resources/views/admin/enterprise-knowledge/create.blade.php index ce2be31eb..95e7b2e1a 100644 --- a/resources/views/admin/enterprise-knowledge/create.blade.php +++ b/resources/views/admin/enterprise-knowledge/create.blade.php @@ -83,7 +83,7 @@ {{ __('admin.enterprise_knowledge.dropzone_title') }} {{ __('admin.enterprise_knowledge.dropzone_desc') }} - + @error('enterprise_files') diff --git a/resources/views/admin/knowledge-bases/form.blade.php b/resources/views/admin/knowledge-bases/form.blade.php index 9de40830f..d36042f35 100644 --- a/resources/views/admin/knowledge-bases/form.blade.php +++ b/resources/views/admin/knowledge-bases/form.blade.php @@ -295,7 +295,7 @@ class="space-y-6"

{{ __('admin.knowledge_bases.source_files_desc') }}

- +
@@ -37,7 +39,7 @@