Files
ragflow/internal/ingestion/pipeline/parser_page_cap.go
Jack bb96bb687d refactor(task): sink parser page-cap override into pipeline package (#17905)
## Summary

Moves the canvas-debug parser page-cap injection out of the `task`
orchestrator and into a **debug-agnostic** `pipeline` helper, so
`PipelineExecutor` keeps only the orchestration skeleton (resolving one
of the P1 review findings: the executor was overloaded with
DSL/parser-param assembly).

### Changes
- **`pipeline/parser_page_cap.go`** (new):
- `BuildParserPageCapOverride(parserConfig, dsl, docType, capPages int,
parserComponentName string, familyOf)` — injects the
`ParserConfig[cpnID][family]["pages"]` cap through the same
`override_params` channel production uses. The cap value and family
resolution are injected by the caller, so the function carries no debug
semantics and is reusable for any page-cap scenario.
- `ExtractParserCpnID(dsl, parserComponentName)` — shared Parser cpnID
discovery from (optionally enveloped) DSL.
- `UnwrapCanvasDSL(raw []byte)` — exported single source of truth for
stripping the `{"dsl": {...}}` canvas envelope.
- `pipeline` does **not** import `component` (no reverse dependency);
callers inject `component.ComponentNameParser` /
`component.ParserFileFamily`.
- **`task/pipeline_executor.go`**: removed `injectDebugPageCap` (the
`debugPageCapPages = 2` constant stays in the task package). The debug
branch now calls `pipeline.BuildParserPageCapOverride(...)`.
- **`task/pipeline_executor.go` `warnUnknownComponentParams`**: fixed a
production no-op bug — it passed the enveloped DSL straight to
`ExtractAllComponentParams`, which silently errored and disabled the
unknown-cpnID guard. It now unwraps the envelope first.
- **`task/debug_result_dsl.go`**: reuses `pipeline.UnwrapCanvasDSL`
instead of a third inline envelope-unwrap copy.

### Behavior
No external debug-preview behavior changes. The three original
invariants are preserved exactly:
1. explicit `pages` caps under `cpnID+family` are respected (not
overwritten),
2. an empty family (unknown docType) is a no-op,
3. the injected shape is `[]any{[]any{1, capPages}}` (the
`[]any`-of-`[]any` form `NormalizePDFPages` requires).

## Test plan
- New `pipeline/parser_page_cap_test.go`: `BuildParserPageCapOverride`
(inject / respect-existing / unknown-family no-op / no-Parser no-op),
`ExtractParserCpnID` (enveloped + raw), `UnwrapCanvasDSL`.
- `task/debug_test.go`: `TestInjectDebugPageCap` migrated to the new
helper; new
`TestWarnUnknownComponentParamsDetectsUnknownCPNFromEnvelope` captures
the warning via `zaptest/observer` to prove the envelope no-op bug is
fixed.
- Both `internal/ingestion/pipeline` and `internal/ingestion/task` pass
`build.sh --test` (unit tier).

## Notes
- `TOKEN_CHUNKER_HANDOFF.md` is an unrelated untracked file and was
deliberately **not** included in this PR.
2026-08-06 15:50:39 +08:00

110 lines
3.5 KiB
Go

package pipeline
import (
"encoding/json"
"fmt"
)
// UnwrapCanvasDSL decodes a raw pipeline DSL and strips the optional canvas
// envelope {"dsl": {...}}, returning the inner components-carrying map. A raw
// (non-enveloped) DSL is returned unchanged. It is the []byte entry point used
// by helpers that need the inner DSL (ExtractParserCpnID,
// BuildParserPageCapOverride) so envelope-handling lives in exactly one place.
func UnwrapCanvasDSL(raw []byte) (map[string]any, error) {
var top map[string]any
if err := json.Unmarshal(raw, &top); err != nil {
return nil, fmt.Errorf("UnwrapCanvasDSL: decode: %w", err)
}
if top == nil {
return nil, errNilDSL
}
if env, ok := top["dsl"].(map[string]any); ok && len(env) > 0 {
return env, nil
}
return top, nil
}
// ExtractParserCpnID returns the cpnID of the first component whose
// component_name equals parserComponentName, or "" when no such component
// exists or the DSL cannot be read. dsl may be enveloped; it is unwrapped
// first.
func ExtractParserCpnID(dsl []byte, parserComponentName string) string {
inner, err := UnwrapCanvasDSL(dsl)
if err != nil {
return ""
}
innerJSON, err := json.Marshal(inner)
if err != nil {
return ""
}
schemas, err := ExtractAllComponentParams(innerJSON)
if err != nil {
return ""
}
for _, s := range schemas {
if s.ComponentName == parserComponentName {
return s.CpnID
}
}
return ""
}
// BuildParserPageCapOverride returns parserConfig with a page cap injected for
// the Parser component, keyed by its cpnID and the document's filetype family.
//
// It is debug-agnostic: capPages and familyOf are supplied by the caller, so
// the function carries no debug-specific semantics and is reusable for any
// page-cap scenario. docType is the uploaded file extension (e.g. "pdf");
// familyOf maps it to a parser setup family (callers pass
// component.ParserFileFamily); parserComponentName is the Parser component
// name (callers pass component.ComponentNameParser) — kept as a parameter so
// the pipeline package does not import component.
//
// When no Parser component is found or the family is empty, the call is a
// no-op and parserConfig is returned unchanged. An explicit "pages" cap
// already present under cpnID+family is respected and left untouched — the
// cap is only a fallback. The injected shape is []any{[]any{1, capPages}}: a
// JSON-decoded list of 1-indexed inclusive ranges, the exact form the
// deepdoc/pdf parser's NormalizePDFPages consumes via
// ParserConfig[cpnID][family]["pages"].
func BuildParserPageCapOverride(
parserConfig map[string]any,
dsl []byte,
docType string,
capPages int,
parserComponentName string,
familyOf func(string) string,
) map[string]any {
if parserConfig == nil {
parserConfig = map[string]any{}
}
parserCpnID := ExtractParserCpnID(dsl, parserComponentName)
if parserCpnID == "" {
return parserConfig
}
family := familyOf(docType)
if family == "" {
return parserConfig
}
// Respect an explicit page cap already present under cpnID + family.
if cpnEntry, ok := parserConfig[parserCpnID].(map[string]any); ok {
if famEntry, ok := cpnEntry[family].(map[string]any); ok {
if _, has := famEntry["pages"]; has {
return parserConfig
}
}
}
cpnEntry, ok := parserConfig[parserCpnID].(map[string]any)
if !ok {
cpnEntry = map[string]any{}
parserConfig[parserCpnID] = cpnEntry
}
famEntry, ok := cpnEntry[family].(map[string]any)
if !ok {
famEntry = map[string]any{}
cpnEntry[family] = famEntry
}
famEntry["pages"] = []any{[]any{1, capPages}}
return parserConfig
}