mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-07-25 01:43:27 +08:00
## Summary Adds page-range parsing support to the Go-native pipeline path and introduces strict `parse_type` validation for both dataset and document update endpoints. ## What changed ### Pages range parsing - **`internal/utility/pdf_pages.go`** — `NormalizePDFPages`: normalizes raw page ranges (list of `[from,to]` 1-indexed inclusive ranges) into sorted, merged, deduplicated `[][]int`. Invalid ranges are dropped. - **`internal/ingestion/pipeline/pdf_pages.go`** — `NormalizeParserConfigPages`: walks any parser_config map and normalizes `"pages"` values under every component → filetype setup, so the persisted config always carries clean, merged ranges. - **`internal/deepdoc/parser/pdf/parser.go`** — integrates `resolvePagesToProcess` to filter parsed PDF pages by the configured ranges. - Pipeline integration (parser pages): `internal/parser/parser/pdf_parser_common.go`, `chunk_process.go`, plus associated e2e and unit tests. ### Parse type validation (shared logic) - **`internal/service/parser_mode.go`** (new) — `ValidateParseTypeMode`: shared function that validates `parse_type` (1=BuiltIn/parser_id, 2=Pipeline/pipeline_id) and ensures the corresponding field is present. Used by both dataset and document update endpoints. - **`internal/service/dataset/crud.go`** / `update.go` — replaces inline `isPipelineMode`/`isBuiltinMode` computation with the shared `service.ValidateParseTypeMode`. - **`internal/service/document/document_dataset_update.go`** — adds strict `parse_type` validation in `validateDatasetDocumentUpdate`, simplifies the reparse logic to a two-way switch (isBuiltin/isPipeline) now that parse_type is always valid. - **`internal/service/document/document.go`** — adds `ParseType` field to `UpdateDatasetDocumentRequest`. - **`internal/service/document/document_dataset_update.go`** — `updateDocumentParserConfig` fallback path when DSL loading fails. - **`internal/service/parser_mode_test.go`** (new) — test coverage for nil, invalid, and missing-field scenarios. ### Frontend - **`web/src/interfaces/request/document.ts`** — adds `parseType` to `IChangeParserRequestBody`. - **`web/src/hooks/use-document-request.ts`** — `useSetDocumentPipelineParser` sends `parse_type` in the PATCH payload. - **`web/src/pages/dataset/dataset/use-change-document-parser.ts`** — Go/Python branching for the document parser config dialog. - **`web/src/components/document-pipeline-dialog/use-document-pipeline-form.ts`** — `buildSubmitData` returns `parseType` (bugfix: was dropped from the return value). ### Test changes - **Removed**: 2 tests that verified the old "mutually exclusive" error (replaced by `ValidateParseTypeMode` coverage). - **Modified**: 6 tests across document and dataset packages to include `ParseType` in request structs. - **Added**: new e2e tests for pages parsing (`pages_e2e_test.go`, `pdf_parser_pages_e2e_test.go`) and unit tests for `NormalizePDFPages`, `NormalizeParserConfigPages`, `resolvePagesToProcess`. ## Backward compatibility - The `parse_type` field is **required** when `parser_id` or `pipeline_id` is sent. This changes the contract for both dataset and document PATCH endpoints, but aligns the Go backend with the existing frontend behavior (the frontend already sends `parse_type`). Callers that omit `parse_type` when updating parser/pipeline selections will receive a clear error message. - Existing callers that only update fields like `name`, `enabled`, or `meta_fields` are unaffected. - Test updates ensure all known call sites are compliant.
277 lines
9.5 KiB
Go
277 lines
9.5 KiB
Go
//
|
|
// Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
//
|
|
|
|
package document
|
|
|
|
import (
|
|
"errors"
|
|
"ragflow/internal/service"
|
|
"regexp"
|
|
"time"
|
|
|
|
"ragflow/internal/dao"
|
|
"ragflow/internal/engine"
|
|
)
|
|
|
|
// DocumentService document service
|
|
type DocumentService struct {
|
|
documentDAO *dao.DocumentDAO
|
|
kbDAO *dao.KnowledgebaseDAO
|
|
ingestionTaskDAO *dao.IngestionTaskDAO
|
|
ingestionTaskLogDAO *dao.IngestionTaskLogDAO
|
|
ingestionTaskSvc *service.IngestionTaskService
|
|
docEngine engine.DocEngine
|
|
metadataSvc *service.MetadataService
|
|
taskDAO *dao.TaskDAO
|
|
file2DocumentDAO *dao.File2DocumentDAO
|
|
fileDAO *dao.FileDAO
|
|
canvasDAO *dao.UserCanvasDAO
|
|
api4ConvDAO *dao.API4ConversationDAO
|
|
}
|
|
|
|
// NewDocumentService create document service
|
|
func NewDocumentService() *DocumentService {
|
|
publisher := service.NewMessageQueueTaskPublisher()
|
|
ingestionTaskSvc := service.NewIngestionTaskService()
|
|
ingestionTaskSvc.SetTaskPublisher(publisher)
|
|
return &DocumentService{
|
|
documentDAO: dao.NewDocumentDAO(),
|
|
ingestionTaskDAO: dao.NewIngestionTaskDAO(),
|
|
ingestionTaskLogDAO: dao.NewIngestionTaskLogDAO(),
|
|
ingestionTaskSvc: ingestionTaskSvc,
|
|
kbDAO: dao.NewKnowledgebaseDAO(),
|
|
docEngine: engine.Get(),
|
|
metadataSvc: service.NewMetadataService(),
|
|
taskDAO: dao.NewTaskDAO(),
|
|
file2DocumentDAO: dao.NewFile2DocumentDAO(),
|
|
fileDAO: dao.NewFileDAO(),
|
|
canvasDAO: dao.NewUserCanvasDAO(),
|
|
api4ConvDAO: dao.NewAPI4ConversationDAO(),
|
|
}
|
|
}
|
|
|
|
// UpdateDocumentRequest update document request
|
|
type UpdateDocumentRequest struct {
|
|
Name *string `json:"name"`
|
|
Run *string `json:"run"`
|
|
TokenNum *int64 `json:"token_num"`
|
|
ChunkNum *int64 `json:"chunk_num"`
|
|
Progress *float64 `json:"progress"`
|
|
ProgressMsg *string `json:"progress_msg"`
|
|
}
|
|
|
|
// DocumentResponse document response
|
|
type DocumentResponse struct {
|
|
ID string `json:"id"`
|
|
Name *string `json:"name,omitempty"`
|
|
KbID string `json:"kb_id"`
|
|
ParserID string `json:"parser_id"`
|
|
PipelineID *string `json:"pipeline_id,omitempty"`
|
|
Type string `json:"type"`
|
|
SourceType string `json:"source_type"`
|
|
CreatedBy string `json:"created_by"`
|
|
Location *string `json:"location,omitempty"`
|
|
Size int64 `json:"size"`
|
|
TokenNum int64 `json:"token_num"`
|
|
ChunkNum int64 `json:"chunk_num"`
|
|
Progress float64 `json:"progress"`
|
|
ProgressMsg *string `json:"progress_msg,omitempty"`
|
|
ProcessDuration float64 `json:"process_duration"`
|
|
Suffix string `json:"suffix"`
|
|
Run *string `json:"run,omitempty"`
|
|
Status *string `json:"status,omitempty"`
|
|
CreatedAt string `json:"created_at"`
|
|
UpdatedAt string `json:"updated_at"`
|
|
}
|
|
|
|
type ThumbnailResponse struct {
|
|
ID string `json:"id"`
|
|
Thumbnail *string `json:"thumbnail,omitempty"`
|
|
KbID string `json:"kb_id"`
|
|
}
|
|
|
|
const imgBase64Prefix = "data:image/png;base64,"
|
|
|
|
type ArtifactResponse struct {
|
|
Data []byte
|
|
ContentType string
|
|
SafeFilename string
|
|
ForceAttachment bool
|
|
}
|
|
|
|
type UpdateDatasetDocumentRequest struct {
|
|
Name *string `json:"name"`
|
|
ParserID *string `json:"parser_id"`
|
|
ChunkCount *int64 `json:"chunk_count"`
|
|
TokenCount *int64 `json:"token_count"`
|
|
PipelineID *string `json:"pipeline_id"`
|
|
Enabled *int `json:"enabled"`
|
|
Progress *float64 `json:"progress"`
|
|
ParserConfig map[string]any `json:"parser_config"`
|
|
MetaFields map[string]any `json:"meta_fields"`
|
|
// ParseType indicates pipeline selection mode: 1 = BuiltIn (parser_id),
|
|
// 2 = Pipeline (pipeline_id). nil means unspecified.
|
|
ParseType *int `json:"parse_type,omitempty"`
|
|
}
|
|
|
|
// PATCH /api/v1/datasets/:dataset_id/documents/:document_id.
|
|
type UpdateDatasetDocumentResponse struct {
|
|
ID string `json:"id"`
|
|
Thumbnail *string `json:"thumbnail,omitempty"`
|
|
DatasetID string `json:"dataset_id"`
|
|
ParserID string `json:"parser_id"`
|
|
PipelineID *string `json:"pipeline_id,omitempty"`
|
|
ParserConfig map[string]interface{} `json:"parser_config"`
|
|
SourceType string `json:"source_type"`
|
|
Type string `json:"type"`
|
|
CreatedBy string `json:"created_by"`
|
|
Name *string `json:"name,omitempty"`
|
|
Location *string `json:"location,omitempty"`
|
|
Size int64 `json:"size"`
|
|
TokenCount int64 `json:"token_count"`
|
|
ChunkCount int64 `json:"chunk_count"`
|
|
Progress float64 `json:"progress"`
|
|
ProgressMsg *string `json:"progress_msg,omitempty"`
|
|
ProcessBeginAt *time.Time `json:"process_begin_at,omitempty"`
|
|
ProcessDuration float64 `json:"process_duration"`
|
|
ContentHash *string `json:"content_hash,omitempty"`
|
|
MetaFields map[string]interface{} `json:"meta_fields,omitempty"`
|
|
Suffix string `json:"suffix"`
|
|
Run string `json:"run"`
|
|
Status *string `json:"status,omitempty"`
|
|
CreateTime *int64 `json:"create_time,omitempty"`
|
|
CreateDate *time.Time `json:"create_date,omitempty"`
|
|
UpdateTime *int64 `json:"update_time,omitempty"`
|
|
UpdateDate *time.Time `json:"update_date,omitempty"`
|
|
}
|
|
|
|
var (
|
|
ErrArtifactInvalidFilename = errors.New("Invalid filename.")
|
|
ErrArtifactInvalidFileType = errors.New("Invalid file type.")
|
|
ErrArtifactNotFound = errors.New("Artifact not found.")
|
|
)
|
|
|
|
var artifactContentTypes = map[string]string{
|
|
".png": "image/png",
|
|
".jpg": "image/jpeg",
|
|
".jpeg": "image/jpeg",
|
|
".svg": "image/svg+xml",
|
|
".pdf": "application/pdf",
|
|
".csv": "text/csv",
|
|
".json": "application/json",
|
|
".html": "text/html",
|
|
}
|
|
|
|
var artifactForceAttachmentExtensions = map[string]struct{}{
|
|
".htm": {},
|
|
".html": {},
|
|
".shtml": {},
|
|
".xht": {},
|
|
".xhtml": {},
|
|
".xml": {},
|
|
".mhtml": {},
|
|
".svg": {},
|
|
}
|
|
var artifactForceAttachmentContentTypes = map[string]struct{}{
|
|
"text/html": {},
|
|
"image/svg+xml": {},
|
|
"application/xhtml+xml": {},
|
|
"text/xml": {},
|
|
"application/xml": {},
|
|
"multipart/related": {},
|
|
}
|
|
|
|
var artifactUnsafeFilenameChars = regexp.MustCompile(`[^\pL\pN_.-]`)
|
|
|
|
type DocumentPreview struct {
|
|
Data []byte
|
|
ContentType string
|
|
FileName string
|
|
}
|
|
|
|
type DownloadDocumentResp struct {
|
|
Data []byte
|
|
FileName string
|
|
ContentType string
|
|
}
|
|
|
|
type IngestDocumentRequest struct {
|
|
DocIDs []string `json:"doc_ids" binding:"required"`
|
|
Run interface{} `json:"run" binding:"required"`
|
|
Delete bool `json:"delete"`
|
|
ApplyKB bool `json:"apply_kb"`
|
|
}
|
|
|
|
// StartParseOptions controls StartParseDocuments behavior.
|
|
type StartParseOptions struct {
|
|
// ApplyKB merges the knowledgebase's parser_config (llm_id, metadata)
|
|
// into the document before parsing.
|
|
ApplyKB bool
|
|
// RerunWithDelete clears prior chunks/tasks/counters before re-parsing.
|
|
RerunWithDelete bool
|
|
}
|
|
|
|
// GetMetadataSummaryRequest request for metadata summary
|
|
type GetMetadataSummaryRequest struct {
|
|
KBID string `json:"kb_id" binding:"required"`
|
|
DocIDs []string `json:"doc_ids"`
|
|
}
|
|
|
|
// GetMetadataSummaryResponse response for metadata summary
|
|
type GetMetadataSummaryResponse struct {
|
|
Summary map[string]interface{} `json:"summary"`
|
|
}
|
|
|
|
// valueInfo holds count and order of first appearance
|
|
type valueInfo struct {
|
|
count int
|
|
firstOrder int
|
|
}
|
|
|
|
// knowledgebaseFolderName is the file-manager folder under each tenant's root
|
|
// that holds per-dataset subfolders, mirroring Python KNOWLEDGEBASE_FOLDER_NAME.
|
|
const knowledgebaseFolderName = ".knowledgebase"
|
|
|
|
// maxUploadDocSize bounds a single uploaded file held in memory, mirroring the
|
|
// Python DOC_MAXIMUM_SIZE default (128 MiB; overridable there via MAX_CONTENT_LENGTH).
|
|
const maxUploadDocSize = 128 * 1024 * 1024
|
|
|
|
// MetadataUpdate is one update item: set key to value.
|
|
type DocumentMetadataUpdate struct {
|
|
Key string `json:"key"`
|
|
Value interface{} `json:"value"`
|
|
Match interface{} `json:"match,omitempty"`
|
|
ValueType string `json:"valueType,omitempty"`
|
|
}
|
|
|
|
// MetadataDelete removes a whole key, or a specific value from a list field.
|
|
type DocumentMetadataDelete struct {
|
|
Key string `json:"key"`
|
|
Value interface{} `json:"value,omitempty"`
|
|
}
|
|
|
|
// MetadataSelector selects which documents to target.
|
|
type DocumentMetadataSelector struct {
|
|
DocumentIDs []string `json:"document_ids"`
|
|
MetadataCondition map[string]interface{} `json:"metadata_condition"`
|
|
}
|
|
|
|
// BatchUpdateDocumentMetadatasResponse summarises the operation.
|
|
type BatchUpdateDocumentMetadatasResponse struct {
|
|
Updated int `json:"updated"`
|
|
MatchedDocs int `json:"matched_docs"`
|
|
}
|