feat(knowledge): support PDF and PPTX uploads for knowledge base and … - #132
feat(knowledge): support PDF and PPTX uploads for knowledge base and …#132myrenyi wants to merge 7 commits into
Conversation
…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 <input accept> with .pdf,.ppt,.pptx
- resources/views/admin/knowledge-bases/form.blade.php: update <input accept> 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
8c6508f to
d111255
Compare
yaojingang
left a comment
There was a problem hiding this comment.
@myrenyi, thank you for adding PDF and PPTX import support. Before this can be merged, please remove the unrelated broad .gitignore rules that hide repository-owned agent configuration, preserve pdf and presentation types in the actual knowledge-base import path, read PPTX order from presentation.xml relationships, reject decks with no extracted text, cap PDF decompression before parsing, stop marking legacy .ppt files as valid in the browser, regenerate composer.lock, and fix the five Pint failures; please also add request-level coverage for a real PDF, a blank or image-only PPTX, and a reordered multi-slide PPTX, complete the CLA fields, then push the updated branch so CI can run and I can re-review it.
Add an opt-in outbound HTTP proxy path so operators can route GEOFlow traffic to specific hosts through a residential/VPN egress when the target site is behind aggressive WAF (jshh.com, high-protection Cloudflare, etc.). Private-network targets remain on the existing direct connect path (GEOFLOW_OUTBOUND_PRIVATE_TARGETS). The policy is fail-closed: with no proxy URL or empty host list it behaves identically to today, keeping AI / search / updater traffic untouched. Pattern matching supports exact hosts (jshh.com), suffix wildcards (*.cdn.test), and global catch-alls (* or .). Changes: - app/Services/Outbound/OutboundProxyPolicy.php: new readonly value object; fromConfig() trims/normalizes inputs, isEnabled() requires both proxy URL and at least one pattern, appliesTo() is case-insensitive and rejects empty hosts - app/Services/Outbound/ResolvedOutboundTarget.php: optional proxyUrl field (defaults to null) so existing callers stay wire-compatible - app/Services/Outbound/SafeOutboundHttpClient.php: inject the policy (default-disabled for backward compatibility) and apply it after target resolution; carries the proxy URL through to the transport - app/Services/Outbound/LaravelPinnedOutboundTransport.php: forward ResolvedOutboundTarget.proxyUrl to the Guzzle 'proxy' option - app/Providers/AppServiceProvider.php: bind OutboundProxyPolicy as a singleton built from config/geoflow.outbound_proxy_url + outbound_proxy_hosts - config/geoflow.php + .env.example: new GEOFLOW_OUTBOUND_PROXY and GEOFLOW_OUTBOUND_PROXY_HOSTS knobs with bilingual inline guidance - tests/Unit/OutboundProxyPolicyTest.php: 10 cases (30 assertions) covering empty policy, missing url/hosts, exact/suffix/global matching, multiple patterns, whitespace tolerance, case insensitivity, and proxy URL trimming Test evidence: php artisan test --filter=OutboundProxyPolicyTest => 10 passed (30 assertions). Pint clean on the touched files.
…ted endpoint
Changes:
* OpenAiRuntimeProvider::resolveEmbeddingDriver now selects the new driver
'volcengine-multimodal' when the API URL points at *.volces.com and the model
id contains 'vision'.
* OpenAiRuntimeProvider gains three pure helpers:
- isVolcengineProviderUrl (host ends with .volces.com)
- isVolcengineMultimodalEmbeddingModel (case-insensitive 'vision' substring)
- volcengineMultimodalEmbeddingPath ('/embeddings/multimodal-embedding-v1').
* KnowledgeChunkSyncService::requestEmbeddingVectors dispatches the new driver
before falling back to the OpenAI-compatible /embeddings path.
* KnowledgeChunkSyncService::requestVolcengineMultimodalEmbeddings wraps the
multimodal endpoint through the safe outbound HTTP gateway and reshapes
inputs into the required [{type:'text',text:'...'}] payload.
* isVolcengineMultimodalEmbeddingMetadata mirrors the existing Gemini helper
so the dispatcher can route by either explicit driver or inferred
host/model combination.
* New VolcengineMultimodalEmbeddingRouteTest covers 7 cases (16 assertions)
including driver priority over volcengine when the URL is Gemini.
Test evidence:
* php -l on all three files: clean.
* php artisan test --filter=VolcengineMultimodalEmbeddingRouteTest
-> 7 passed (16 assertions).
* php artisan test --filter='OpenAiRuntimeProviderTest|OutboundProxyPolicyTest|VolcengineMultimodalEmbeddingRouteTest'
-> 38 passed (78 assertions).
* docker compose exec -T app ./vendor/bin/pint <three files> -> fixed 3 style issues.
…ction test
Changes:
* OpenAiRuntimeProvider::isVolcengineMultimodalEmbedding(url, modelId) is a new
pure helper that composes the existing URL + model checks.
* AdminAiModelTestSnapshot gains a volcengineMultimodal bool alongside the
existing gemini / usesOpenAiResponses flags so the controller can build the
correct request payload without recomputing classification.
* AdminAiModelTestPreparationService::resolveEndpoint now selects the dedicated
/embeddings/multimodal-embedding-v1 path for volcengine multimodal embedding
models, mirroring the KnowledgeChunkSyncService dispatch.
* AiModelController::testConnection derives the flag from the snapshot and
forwards it to buildTestPayload / isValidTestResponse.
* buildTestPayload emits the multimodal-shaped payload
{model, input: [{type:'text', text: ...}]} for volcengine multimodal embedding
and keeps the OpenAI-compatible string input for the regular /embeddings path.
* VolcengineMultimodalEmbeddingRouteTest adds two coverage cases for the new
composite helper and the embedding base URL preservation invariant.
Test evidence:
* php -l on all five files: clean.
* php artisan test --filter='VolcengineMultimodalEmbeddingRouteTest|OpenAiRuntimeProviderTest|OutboundProxyPolicyTest'
-> 40 passed (83 assertions).
* php artisan test --testsuite=Feature --filter='AiModel'
-> 197 passed (1528 assertions), including the dedicated
'admin can test volcengine embedding model connection' case.
* docker compose exec -T app ./vendor/bin/pint <five files> -> fixed 5 style issues.
… embedding Changes: * OpenAiRuntimeProvider::isVolcengineMultimodalEmbeddingModel now also returns true for model ids starting with 'ep-' so Volcano endpoint ids like ep-m-20260911110530-ndb86 are routed through the multimodal endpoint without forcing users to keep the product name (which contains 'vision') in the ai_models.model_id field. * VolcengineMultimodalEmbeddingRouteTest adds two cases: - it_detects_volcengine_endpoint_id_prefix_as_multimodal_embedding - it_routes_volcengine_endpoint_id_to_multimodal_driver Test evidence: * php -l on both files: clean. * php artisan test --filter='VolcengineMultimodalEmbeddingRouteTest|OpenAiRuntimeProviderTest|OutboundProxyPolicyTest' -> 42 passed (89 assertions). * docker compose exec -T app ./vendor/bin/pint <files> -> fixed 2 style issues.
…0 in collect panel
Changes:
* AiVisibilityAnalyticsController::keywordLibraries attaches a
recently_sampled flag to each keyword entry by cross-referencing
ai_visibility_runs for completed sample runs in the last 7 days.
* _ai-visibility-collect.blade.php renders recently_sampled keywords with
an emerald-tinted label, a small '最近已采' / 'Sampled' badge, and a
tooltip describing the 7-day window. Per-library and global counters show
'Selected X/50' so the per-batch quota is always visible.
* 'select all' is now a deterministic action: clicking it checks the first
50 checkboxes in display order and unchecks any others, which matches
the backend 'max:50' validation regardless of library size. When the
library has more than 50 entries the button shows an explanatory tooltip.
* lang/zh_CN/admin.php and lang/en/admin.php gain four new collect.* keys
(recently_sampled, recently_sampled_hint, selection_counter,
select_all_hint_overflow).
Test evidence:
* php -l on all touched files: clean.
* php artisan test --filter=AdminAiVisibilityAnalyticsTest
-> 12 passed (81 assertions) including two new cases:
test_keyword_libraries_mark_recently_sampled_keywords_for_collection_panel
test_collect_panel_rejects_more_than_50_keywords
* docker compose exec -T app ./vendor/bin/pint <five files> -> fixed 4 style issues.
…ently-sampled keywords
Changes:
* _ai-visibility-collect.blade.php disables the <input> checkbox for any
keyword whose library entry has `recently_sampled` set, so:
- users cannot pick a keyword that was sampled in the last 7 days,
- the "select all" button (which caps at 50) only fills the top-N
eligible keywords and ignores already-sampled ones,
- the HTML form submission naturally excludes disabled inputs, which
keeps the controller `keyword_ids.max:50` validation meaningful.
* The select-all button is now a toggle: clicking while any eligible box
is checked clears the entire library selection, and the button label
swaps between `select_all` ("全选本库" / "Select all") and
`select_all_clear` ("取消选择" / "Clear selection"). The label is
sourced from data-attributes so JS does not hardcode translations.
* `eligibleBoxes()` filters out disabled inputs before counting or
checking, so the counter (`Selected X/50`) reflects the real
submission. The overflow tooltip is also rebound whenever the user
toggles selection inside the library.
* lang/zh_CN/admin.php gains `select_all_clear`. lang/en/admin.php gains
`select_all_clear` and clarifies `select_all_hint_overflow` to mention
that recently-sampled keywords are skipped.
Test evidence:
* docker compose exec -T app ./vendor/bin/pint on all four files
-> fixed 1 style issue.
* docker compose exec -T app php artisan test --filter=AdminAiVisibilityAnalyticsTest
-> 13 passed (96 assertions), including the new case
test_collect_panel_disables_recently_sampled_checkboxes_so_they_are_not_resubmitted
that asserts (a) the rendered HTML for a recently-sampled keyword
carries a real `disabled` attribute, and (b) the rendered HTML for a
fresh keyword does NOT.
* Full AdminAi* suite: 499 passed, 1 pre-existing failure in
AdminAiExecutionIdentityTest::test_due_draft_quality_dispatch_uses_the_quality_model_when_the_content_model_is_inactive
unrelated to this change.
…enterprise knowledge
Add PDF and PPTX as supported file types for knowledge base and enterprise knowledge uploads. Legacy PPT binary remains unsupported.
Changes:
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
变更说明 / Summary
验证 / Verification
CLA 声明 / CLA declaration
可构成版权作品的贡献必须接受 GEOFlow Contributor License Agreement v1.0。
Copyrightable contributions require acceptance of the GEOFlow Contributor License Agreement v1.0.
以下内容会公开显示。如需私下签署,请将姓名字段填写为
Private CLA requested,并在合并前与维护者完成单独签署。The following information is public. To sign privately, enter
Private CLA requestedin the name field and complete a separate signature with the maintainer before merge.个人 Individual/企业 EntityYYYY-MM-DD):检查清单 / Checklist
I have identified the source, license, and modifications of third-party materials, or this contribution contains none.
I have not submitted credentials, customer data, or other sensitive information.
I have added tests for behavior changes or explained why no new test is needed.