Skip to content

feat(knowledge): support PDF and PPTX uploads for knowledge base and … - #132

Open
myrenyi wants to merge 7 commits into
yaojingang:mainfrom
myrenyi:feature/pdf-pptx-upload
Open

feat(knowledge): support PDF and PPTX uploads for knowledge base and …#132
myrenyi wants to merge 7 commits into
yaojingang:mainfrom
myrenyi:feature/pdf-pptx-upload

Conversation

@myrenyi

@myrenyi myrenyi commented Sep 10, 2026

Copy link
Copy Markdown

…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

变更说明 / 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 requested in the name field and complete a separate signature with the maintainer before merge.

  • 贡献者类型 / Contributor type: 个人 Individual / 企业 Entity
  • 法定姓名或企业法定名称 / Legal name or entity name:
  • 企业授权代表姓名及职务(如适用)/ Authorized representative and title (if applicable):
  • GitHub 用户名 / GitHub username:
  • 接受日期 / Date accepted (YYYY-MM-DD):
  • 我已阅读并接受 GEOFlow CLA v1.0,并确认有权授予其中约定的权利。I have read and accept the GEOFlow CLA v1.0 and confirm that I have authority to grant the rights described in it.

检查清单 / 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.

…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
@myrenyi
myrenyi force-pushed the feature/pdf-pptx-upload branch from 8c6508f to d111255 Compare September 10, 2026 08:51

@yaojingang yaojingang left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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.

GEOFlow Local Dev added 6 commits September 11, 2026 09:53
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants