feat(file): Add file ancestor directory lookup feature by go (#14037)

### What problem does this PR solve?

feat(file): Add file ancestor directory lookup feature by go

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
chanx
2026-04-14 15:22:03 +08:00
committed by GitHub
parent 6aec8058bb
commit 1031aebc8f
7 changed files with 224 additions and 133 deletions

View File

@@ -93,9 +93,10 @@ export function TwoColumnCheckFormField({ prefix }: CommonProps) {
return (
<RAGFlowFormItem
name={buildFieldNameWithPrefix(`enable_multi_column`, prefix)}
label={t('flow.enableMultiColumn', 'Enable multi column')}
label={t('flow.enableMultiColumn')}
horizontal={true}
labelClassName="w-[200px]"
labelClassName="w-full"
tooltip={t('flow.enableMultiColumnTip')}
>
{(field) => (
<Checkbox
@@ -114,9 +115,10 @@ export function RmdirFormField({ prefix }: CommonProps) {
return (
<RAGFlowFormItem
name={buildFieldNameWithPrefix(`remove_toc`, prefix)}
label={t('flow.remove_toc', 'Remove TOC')}
label={t('flow.removeToc')}
horizontal={true}
labelClassName="w-[200px]"
tooltip={t('flow.removeTocTip')}
labelClassName="w-full"
>
{(field) => (
<Checkbox

View File

@@ -5,14 +5,13 @@ import {
import { RAGFlowFormItem } from '@/components/ragflow-form';
import { BlockButton, Button } from '@/components/ui/button';
import { Form } from '@/components/ui/form';
import { MultiSelect } from '@/components/ui/multi-select';
import { Separator } from '@/components/ui/separator';
import { cn } from '@/lib/utils';
import { buildOptions } from '@/utils/form';
import { zodResolver } from '@hookform/resolvers/zod';
import { useHover } from 'ahooks';
import { Trash2 } from 'lucide-react';
import { memo, useCallback, useEffect, useMemo, useRef } from 'react';
import { memo, useCallback, useMemo, useRef } from 'react';
import {
useFieldArray,
UseFieldArrayRemove,
@@ -25,8 +24,6 @@ import {
FileType,
InitialOutputFormatMap,
initialParserValues,
MAIN_CONTENT_PREPROCESS_VALUE,
PreprocessValue,
} from '../../constant/pipeline';
import { useFormValues } from '../../hooks/use-form-values';
import { useWatchFormChange } from '../../hooks/use-watch-form-change';
@@ -49,82 +46,82 @@ import { WordFormFields } from './word-form-fields';
const outputList = buildOutputList(initialParserValues.outputs);
type PreprocessOptionConfig = {
value: PreprocessValue;
required?: boolean;
};
// type PreprocessOptionConfig = {
// value: PreprocessValue;
// required?: boolean;
// };
const DefaultPreprocessOptionConfigs: PreprocessOptionConfig[] = [
{ value: MAIN_CONTENT_PREPROCESS_VALUE, required: true },
];
// const DefaultPreprocessOptionConfigs: PreprocessOptionConfig[] = [
// { value: MAIN_CONTENT_PREPROCESS_VALUE, required: true },
// ];
const PreprocessOptionConfigsMap: Partial<
Record<FileType, PreprocessOptionConfig[]>
> = {
[FileType.PDF]: [
{ value: MAIN_CONTENT_PREPROCESS_VALUE, required: true },
{ value: PreprocessValue.abstract },
{ value: PreprocessValue.author },
{ value: PreprocessValue.section_title },
],
[FileType.PowerPoint]: [
{ value: MAIN_CONTENT_PREPROCESS_VALUE, required: true },
],
[FileType.Spreadsheet]: [
{ value: MAIN_CONTENT_PREPROCESS_VALUE, required: true },
],
[FileType.TextMarkdown]: [
{ value: MAIN_CONTENT_PREPROCESS_VALUE, required: true },
{ value: PreprocessValue.section_title },
],
[FileType.Code]: [{ value: MAIN_CONTENT_PREPROCESS_VALUE, required: true }],
[FileType.Html]: [
{ value: MAIN_CONTENT_PREPROCESS_VALUE, required: true },
{ value: PreprocessValue.section_title },
],
[FileType.Doc]: [
{ value: MAIN_CONTENT_PREPROCESS_VALUE, required: true },
{ value: PreprocessValue.section_title },
],
[FileType.Docx]: [
{ value: MAIN_CONTENT_PREPROCESS_VALUE, required: true },
{ value: PreprocessValue.section_title },
],
};
// const PreprocessOptionConfigsMap: Partial<
// Record<FileType, PreprocessOptionConfig[]>
// > = {
// [FileType.PDF]: [
// { value: MAIN_CONTENT_PREPROCESS_VALUE, required: true },
// { value: PreprocessValue.abstract },
// { value: PreprocessValue.author },
// { value: PreprocessValue.section_title },
// ],
// [FileType.PowerPoint]: [
// { value: MAIN_CONTENT_PREPROCESS_VALUE, required: true },
// ],
// [FileType.Spreadsheet]: [
// { value: MAIN_CONTENT_PREPROCESS_VALUE, required: true },
// ],
// [FileType.TextMarkdown]: [
// { value: MAIN_CONTENT_PREPROCESS_VALUE, required: true },
// { value: PreprocessValue.section_title },
// ],
// [FileType.Code]: [{ value: MAIN_CONTENT_PREPROCESS_VALUE, required: true }],
// [FileType.Html]: [
// { value: MAIN_CONTENT_PREPROCESS_VALUE, required: true },
// { value: PreprocessValue.section_title },
// ],
// [FileType.Doc]: [
// { value: MAIN_CONTENT_PREPROCESS_VALUE, required: true },
// { value: PreprocessValue.section_title },
// ],
// [FileType.Docx]: [
// { value: MAIN_CONTENT_PREPROCESS_VALUE, required: true },
// { value: PreprocessValue.section_title },
// ],
// };
function getPreprocessOptionConfigs(fileType?: FileType) {
if (!fileType) {
return DefaultPreprocessOptionConfigs;
}
// function getPreprocessOptionConfigs(fileType?: FileType) {
// if (!fileType) {
// return DefaultPreprocessOptionConfigs;
// }
return PreprocessOptionConfigsMap[fileType] ?? DefaultPreprocessOptionConfigs;
}
// return PreprocessOptionConfigsMap[fileType] ?? DefaultPreprocessOptionConfigs;
// }
function normalizePreprocessValuesByFileType(
fileType: FileType | undefined,
values: string[] | undefined,
) {
const optionConfigs = getPreprocessOptionConfigs(fileType);
const allowedValueSet = new Set(optionConfigs.map((x) => x.value));
const requiredValues = optionConfigs
.filter((x) => x.required)
.map((x) => x.value);
const normalizedOptionalValues = (Array.isArray(values) ? values : []).filter(
(value) => allowedValueSet.has(value as PreprocessValue),
) as PreprocessValue[];
// function normalizePreprocessValuesByFileType(
// fileType: FileType | undefined,
// values: string[] | undefined,
// ) {
// const optionConfigs = getPreprocessOptionConfigs(fileType);
// const allowedValueSet = new Set(optionConfigs.map((x) => x.value));
// const requiredValues = optionConfigs
// .filter((x) => x.required)
// .map((x) => x.value);
// const normalizedOptionalValues = (Array.isArray(values) ? values : []).filter(
// (value) => allowedValueSet.has(value as PreprocessValue),
// ) as PreprocessValue[];
return Array.from(
new Set<PreprocessValue>([...requiredValues, ...normalizedOptionalValues]),
);
}
// return Array.from(
// new Set<PreprocessValue>([...requiredValues, ...normalizedOptionalValues]),
// );
// }
function isSameStringArray(a: string[] | undefined, b: string[]) {
if (!a || a.length !== b.length) {
return false;
}
// function isSameStringArray(a: string[] | undefined, b: string[]) {
// if (!a || a.length !== b.length) {
// return false;
// }
return a.every((item, idx) => item === b[idx]);
}
// return a.every((item, idx) => item === b[idx]);
// }
const FileFormatWidgetMap = {
[FileType.PDF]: PdfFormFields,
@@ -151,7 +148,7 @@ export const FormSchema = z.object({
setups: z.array(
z.object({
fileFormat: z.string().nullish(),
preprocess: z.array(z.string()).optional(),
// preprocess: z.array(z.string()).optional(),
output_format: z.string().optional(),
parse_method: z.string().optional(),
lang: z.string().optional(),
@@ -212,56 +209,56 @@ function ParserItem({
[form, index],
);
const handlePreprocessChange = useCallback(
(value: PreprocessValue[]) => {
form.setValue(`setups.${index}.preprocess`, value, {
shouldDirty: true,
shouldValidate: true,
shouldTouch: true,
});
},
[form, index],
);
// const handlePreprocessChange = useCallback(
// (value: PreprocessValue[]) => {
// form.setValue(`setups.${index}.preprocess`, value, {
// shouldDirty: true,
// shouldValidate: true,
// shouldTouch: true,
// });
// },
// [form, index],
// );
const preprocessOptions = useMemo(() => {
const optionConfigs = getPreprocessOptionConfigs(fileFormat as FileType);
// const preprocessOptions = useMemo(() => {
// const optionConfigs = getPreprocessOptionConfigs(fileFormat as FileType);
return optionConfigs.map((optionConfig) => {
const labelMap: Record<string, string> = {
[MAIN_CONTENT_PREPROCESS_VALUE]: t('flow.preprocess.mainContent'),
[PreprocessValue.section_title]: t('flow.preprocess.sectionTitle'),
[PreprocessValue.abstract]: t('flow.preprocess.abstract'),
[PreprocessValue.author]: t('flow.preprocess.author'),
};
// return optionConfigs.map((optionConfig) => {
// const labelMap: Record<string, string> = {
// [MAIN_CONTENT_PREPROCESS_VALUE]: t('flow.preprocess.mainContent'),
// [PreprocessValue.section_title]: t('flow.preprocess.sectionTitle'),
// [PreprocessValue.abstract]: t('flow.preprocess.abstract'),
// [PreprocessValue.author]: t('flow.preprocess.author'),
// };
const label = labelMap[optionConfig.value] || optionConfig.value;
// const label = labelMap[optionConfig.value] || optionConfig.value;
return {
value: optionConfig.value,
disabled: optionConfig.required,
label: label,
};
});
}, [fileFormat, t]);
// return {
// value: optionConfig.value,
// disabled: optionConfig.required,
// label: label,
// };
// });
// }, [fileFormat, t]);
useEffect(() => {
const currentPreprocessValues = form.getValues(
`setups.${index}.preprocess`,
) as string[] | undefined;
const normalizedPreprocessValues = normalizePreprocessValuesByFileType(
fileFormat as FileType,
currentPreprocessValues,
);
// useEffect(() => {
// const currentPreprocessValues = form.getValues(
// `setups.${index}.preprocess`,
// ) as string[] | undefined;
// const normalizedPreprocessValues = normalizePreprocessValuesByFileType(
// fileFormat as FileType,
// currentPreprocessValues,
// );
if (
!isSameStringArray(currentPreprocessValues, normalizedPreprocessValues)
) {
form.setValue(`setups.${index}.preprocess`, normalizedPreprocessValues, {
shouldDirty: false,
shouldValidate: true,
});
}
}, [fileFormat, form, index]);
// if (
// !isSameStringArray(currentPreprocessValues, normalizedPreprocessValues)
// ) {
// form.setValue(`setups.${index}.preprocess`, normalizedPreprocessValues, {
// shouldDirty: false,
// shouldValidate: true,
// });
// }
// }, [fileFormat, form, index]);
return (
<section
@@ -301,7 +298,7 @@ function ParserItem({
fileType={fileFormat as FileType}
/>
</div>
<RAGFlowFormItem
{/* <RAGFlowFormItem
name={buildFieldNameWithPrefix(`preprocess`, prefix)}
label={t('flow.preprocess.preprocess')}
>
@@ -320,7 +317,7 @@ function ParserItem({
options={preprocessOptions}
></MultiSelect>
)}
</RAGFlowFormItem>
</RAGFlowFormItem> */}
{index < fieldLength - 1 && <Separator />}
</section>
);
@@ -351,10 +348,10 @@ const ParserForm = ({ node }: INextOperatorForm) => {
parse_method: '',
lang: '',
fields: [],
llm_id: '',
vlm: { llm_id: '' },
table_result_type: '',
markdown_image_response_type: '',
preprocess: [],
// preprocess: [],
});
}, [append]);

View File

@@ -8,7 +8,7 @@ import { Form } from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { zodResolver } from '@hookform/resolvers/zod';
import { Trash2 } from 'lucide-react';
import { memo, useEffect, useRef } from 'react';
import { memo, useEffect, useRef, useState } from 'react';
import { useFieldArray, useForm, useFormContext } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { z } from 'zod';
@@ -197,6 +197,7 @@ const TitleChunkerForm = ({ node }: INextOperatorForm) => {
});
const isInitialized = useRef(false);
const initialMode = useRef<string | undefined>(undefined);
const [showAllTip, setShowAllTip] = useState(true);
const method = form.watch('method');
const name = 'rules';
@@ -210,6 +211,7 @@ const TitleChunkerForm = ({ node }: INextOperatorForm) => {
}
if (method !== initialMode.current) {
setShowAllTip(true);
const currentMode = initialMode.current;
const hierarchyValue = form.getValues('hierarchy');
const rulesValue = form.getValues('rules');
@@ -290,6 +292,34 @@ const TitleChunkerForm = ({ node }: INextOperatorForm) => {
],
}}
/>
{/* <div className={cn("text-xs text-text-secondary w-full border p-1", showAllTip ? "block" : "")}>
{method === 'hierarchy' && t('flow.hierarchyTip')}
{method === 'group' && t('flow.groupTip')}
</div> */}
<div
className={`text-xs text-text-secondary w-full border rounded-sm p-2 cursor-pointer ${showAllTip ? 'block' : 'truncate'}`}
onClick={() => setShowAllTip(!showAllTip)}
>
<div className="flex flex-col justify-start items-center">
<span
className="flex self-start"
dangerouslySetInnerHTML={{
__html:
method === 'hierarchy'
? t('flow.hierarchyTip')
: method === 'group'
? t('flow.groupTip')
: '',
}}
>
{/* {method === 'hierarchy' && t('flow.hierarchyTip')}
{method === 'group' && t('flow.groupTip')} */}
</span>
{/* <span className="flex ml-2 text-xs self-center">
{showAllTip ? '▲' : ''}
</span> */}
</div>
</div>
<RAGFlowFormItem name={'hierarchy'} label={''}>
<SelectWithSearch options={hierarchyOptions}></SelectWithSearch>
</RAGFlowFormItem>
@@ -297,12 +327,9 @@ const TitleChunkerForm = ({ node }: INextOperatorForm) => {
<RAGFlowFormItem
name="include_heading_content"
label={t('flow.includeHeadingContent', 'Include heading content')}
tooltip={t(
'flow.includeHeadingContentTip',
'When enabled, content directly under a heading is kept as its own chunk. Child chunks keep only the heading path.',
)}
tooltip={t('flow.includeHeadingContentTip')}
horizontal={true}
labelClassName="w-[200px]"
labelClassName="w-full"
>
{(field) => (
<Checkbox