Fix: when save dataset parser_config, warn user only new files take effects (#21416)

This commit is contained in:
Wang Qi authored and GitHub committed 2026-10-23 20:09:07 +08:00
1 parent 77b249eb7a
commit 14133cc5f7
4 files changed
+125 -2

No files matched your search

+2
View File
@@ -772,6 +772,8 @@ Example: A 1 KB message with 1024-dim embedding uses ~9 KB. The 5 MB default lim
parsingMode: 'Parsing mode',
manualSetup: 'Custom ingestion pipeline',
builtIn: 'Built-in parsing template',
parserSettingsChangeWarning:
'Updated parser settings apply only to newly uploaded files. To apply these settings to existing files, update their parser settings and re-parse them.',
noConfigChunkerHint:
'The built-in {{name}} parser handles chunking automatically, so no additional configuration is required.',
titleDescription:
+2
View File
@@ -708,6 +708,8 @@ export default {
parsingMode: '解析方式',
manualSetup: '自定义Ingestion pipeline',
builtIn: '内置解析模板',
parserSettingsChangeWarning:
'更新后的解析设置仅对新上传的文件生效。如需对现有文件生效,请修改这些文件的解析设置并重新解析。',
noConfigChunkerHint:
'内置的 {{name}} 解析器会自动处理分块,因此无需额外配置。',
titleDescription: '在这里更新知识库配置,特别是大语言模型和提示词。',
+36 -2
View File
@@ -9,6 +9,7 @@ import {
CardTitle,
} from '@/components/ui/card';
import Divider from '@/components/ui/divider';
import { Modal } from '@/components/ui/modal/modal';
import {
Form,
FormField,
@@ -24,6 +25,7 @@ import {
usePipelineOperatorNodes,
useResetParserConfigOnPipelineChange,
} from '@/hooks/use-pipeline-operator';
import { buildParserConfigFromNodes } from '@/utils/pipeline-operator';
import { zodResolver } from '@hookform/resolvers/zod';
import { isEqual } from 'lodash';
import { useCallback, useEffect, useState } from 'react';
@@ -42,6 +44,7 @@ import {
usePipelineDataList,
useSaveDatasetSetting,
} from './hooks';
import { normalizeParserConfigForComparison } from './parser-config';
import { useRevealSubmitErrors } from './use-reveal-submit-errors';
export default function DatasetSetting() {
@@ -146,9 +149,39 @@ export default function DatasetSetting() {
const handleSubmit = useCallback(
async (data: z.infer<typeof formSchema>) => {
await handleSave(data);
const savedValues = form.formState.defaultValues;
const operatorDefaults = buildParserConfigFromNodes(operatorNodes);
const [savedParserConfig, parserConfig] = [
savedValues?.parser_config,
data.parser_config,
].map((config) =>
normalizeParserConfigForComparison(config, operatorDefaults, t),
);
const parserSettingsChanged =
data.parse_type !== savedValues?.parse_type ||
(data.parser_id || '') !== (savedValues?.parser_id || '') ||
(data.pipeline_id || '') !== (savedValues?.pipeline_id || '') ||
!isEqual(parserConfig, savedParserConfig);
if (knowledgeDetails.chunk_count > 0 && parserSettingsChanged) {
await new Promise<void>((resolve) => {
Modal.warning({
title: t('common.warn'),
content: t('knowledgeConfiguration.parserSettingsChangeWarning'),
okText: t('common.ok'),
closable: false,
showCancel: false,
onOk: resolve,
});
});
}
const result = await handleSave(data);
if (result.code === 0) {
form.reset(data);
}
},
[handleSave],
[form, handleSave, knowledgeDetails.chunk_count, operatorNodes, t],
);
const { errors } = useFormState({
@@ -294,6 +327,7 @@ export default function DatasetSetting() {
loading={
datasetSettingLoading ||
saveLoading ||
form.formState.isSubmitting ||
(operatorNodesLoading && operatorNodes.length === 0)
}
>
@@ -0,0 +1,85 @@
import { Languages } from '@/components/cross-language-form-field';
import { ParseDocumentType } from '@/components/layout-recognize-form-field';
import { Operator } from '@/constants/agent';
import { FileType } from '@/constants/file';
import { normalizeParserFormValues } from '@/pages/agent/form/parser-form/utils';
import { transformApiResponseToForm } from '@/pages/agent/form/title-chunker-form/hook';
import {
getOperatorType,
normalizeOperatorForm,
transformExtractorConfigToForm,
transformFormConfigToApi,
} from '@/utils/pipeline-operator';
import type { TFunction } from 'i18next';
export function normalizeParserConfigForComparison(
config: Record<string, any> | undefined,
operatorDefaults: Record<string, any>,
t: TFunction,
) {
const entries = Object.entries({ ...operatorDefaults, ...config })
.filter(([operatorId]) => operatorId.includes(':'))
.map(([operatorId, values]) => {
const operatorType = getOperatorType(operatorId);
let normalized = normalizeOperatorForm(operatorId, values);
// Mirror initialization performed inside the operator forms, including
// defaults that their mounted controls write back without user edits.
if (operatorType === Operator.Parser) {
normalized = normalizeParserFormValues(normalized);
normalized.setups = normalized.setups.map(
(setup: Record<string, any>) => {
const next = { ...setup };
if (
setup.fileFormat === FileType.PDF &&
setup.parse_method &&
![
ParseDocumentType.DeepDOC,
ParseDocumentType.PlainText,
ParseDocumentType.TCADPParser,
].includes(setup.parse_method) &&
!setup.lang
) {
next.lang = Languages[0];
}
if (
[
FileType.PDF,
FileType.Spreadsheet,
FileType.PowerPoint,
].includes(setup.fileFormat) &&
setup.parse_method === ParseDocumentType.TCADPParser
) {
next.table_result_type ||= '1';
next.markdown_image_response_type ||= '1';
}
return next;
},
);
} else if (operatorType === Operator.Extractor) {
normalized = transformExtractorConfigToForm(normalized);
for (const feature of ['keywords', 'questions', 'summary']) {
normalized[feature] = {
...normalized[feature],
system_prompt:
normalized[feature]?.system_prompt ||
t(`flow.prompts.system.${feature}`),
};
}
} else if (
operatorType === Operator.TitleChunker ||
operatorType === Operator.ManualChunker
) {
normalized = transformApiResponseToForm(normalized);
if (operatorType === Operator.ManualChunker) {
normalized.method = 'group';
}
}
return [operatorId, transformFormConfigToApi(operatorType, normalized)];
});
// Registered optional controls can add undefined properties. Compare the
// serialized settings sent to the API, where those properties are omitted.
return JSON.parse(JSON.stringify(Object.fromEntries(entries)));
}