feat: View will move when workflow check error;fix: ui refresh error when continuous file upload (#3077)

* fix: plugin output check

* fix: ui refresh error when continuous file upload

* feat: View will move when workflow check error
This commit is contained in:
Archer 2024-11-05 23:54:10 +08:00 committed by archer
parent 0f1932aadc
commit a9ee6e6a5e
No known key found for this signature in database
GPG Key ID: 4446499B846D4A9E
17 changed files with 129 additions and 125 deletions

View File

@ -12,9 +12,10 @@ weight: 811
1. 1.
2. 新增 - 文件上传方案调整,节点直接支持接收文件链接,插件自定义变量支持文件上传。 2. 新增 - 文件上传方案调整,节点直接支持接收文件链接,插件自定义变量支持文件上传。
3. 新增 - 对话记录增加时间显示。 3. 新增 - 对话记录增加时间显示。
4. 优化 - 知识库上传文件,优化报错提示。 4. 新增 - 工作流校验错误时,跳转至错误节点。
5. 优化 - 全文检索语句,减少一轮查询。 5. 优化 - 知识库上传文件,优化报错提示。
6. 优化 - 修改 findLast 为 [...array].reverse().find适配旧版浏览器。 6. 优化 - 全文检索语句,减少一轮查询。
7. 优化 - Markdown 组件自动空格,避免分割 url 中的中文。 7. 优化 - 修改 findLast 为 [...array].reverse().find适配旧版浏览器。
8. 修复 - Dockerfile pnpm install 支持代理。 8. 优化 - Markdown 组件自动空格,避免分割 url 中的中文。
9. 修复 - BI 图表生成无法写入文件。 9. 修复 - Dockerfile pnpm install 支持代理。
10. 修复 - BI 图表生成无法写入文件。

View File

@ -500,7 +500,7 @@ export async function dispatchWorkFlow(data: Props): Promise<DispatchFlowRespons
nodes: runtimeNodes, nodes: runtimeNodes,
variables variables
}); });
console.log(value, '=-=-');
// Dynamic input is stored in the dynamic key // Dynamic input is stored in the dynamic key
if (input.canEdit && dynamicInput && params[dynamicInput.key]) { if (input.canEdit && dynamicInput && params[dynamicInput.key]) {
params[dynamicInput.key][input.key] = valueTypeFormat(value, input.valueType); params[dynamicInput.key][input.key] = valueTypeFormat(value, input.valueType);

View File

@ -43,7 +43,7 @@ export const dispatchLoop = async (props: Props): Promise<Response> => {
let totalPoints = 0; let totalPoints = 0;
let newVariables: Record<string, any> = props.variables; let newVariables: Record<string, any> = props.variables;
for await (const item of loopInputArray) { for await (const item of loopInputArray.filter(Boolean)) {
runtimeNodes.forEach((node) => { runtimeNodes.forEach((node) => {
if ( if (
childrenNodeIdList.includes(node.nodeId) && childrenNodeIdList.includes(node.nodeId) &&

View File

@ -196,16 +196,13 @@ const VariableEdit = ({
<Thead h={8}> <Thead h={8}>
<Tr> <Tr>
<Th <Th
fontSize={'mini'}
borderRadius={'none !important'} borderRadius={'none !important'}
fontSize={'mini'}
bg={'myGray.50'} bg={'myGray.50'}
p={0} p={0}
px={4} px={4}
fontWeight={'medium'} fontWeight={'medium'}
> >
{t('common:core.module.variable.key')}
</Th>
<Th fontSize={'mini'} bg={'myGray.50'} p={0} px={4} fontWeight={'medium'}>
{t('workflow:Variable_name')} {t('workflow:Variable_name')}
</Th> </Th>
<Th fontSize={'mini'} bg={'myGray.50'} p={0} px={4} fontWeight={'medium'}> <Th fontSize={'mini'} bg={'myGray.50'} p={0} px={4} fontWeight={'medium'}>
@ -236,19 +233,9 @@ const VariableEdit = ({
> >
<Flex alignItems={'center'}> <Flex alignItems={'center'}>
<MyIcon name={item.icon as any} w={'16px'} color={'myGray.400'} mr={2} /> <MyIcon name={item.icon as any} w={'16px'} color={'myGray.400'} mr={2} />
{item.label} {item.key}
</Flex> </Flex>
</Td> </Td>
<Td
p={0}
px={4}
h={8}
color={'myGray.900'}
fontSize={'mini'}
fontWeight={'medium'}
>
<Flex alignItems={'center'}>{item.key}</Flex>
</Td>
<Td p={0} px={4} h={8} color={'myGray.900'} fontSize={'mini'}> <Td p={0} px={4} h={8} color={'myGray.900'} fontSize={'mini'}>
<Flex alignItems={'center'}> <Flex alignItems={'center'}>
{item.required ? ( {item.required ? (

View File

@ -73,7 +73,8 @@ const ChatInput = ({
showSelectFile, showSelectFile,
showSelectImg, showSelectImg,
removeFiles, removeFiles,
replaceFiles replaceFiles,
hasFileUploading
} = useFileUpload({ } = useFileUpload({
outLinkAuthData, outLinkAuthData,
chatId: chatId || '', chatId: chatId || '',
@ -81,7 +82,6 @@ const ChatInput = ({
fileCtrl fileCtrl
}); });
const havInput = !!inputValue || fileList.length > 0; const havInput = !!inputValue || fileList.length > 0;
const hasFileUploading = fileList.some((item) => !item.url);
const canSendMessage = havInput && !hasFileUploading; const canSendMessage = havInput && !hasFileUploading;
// Upload files // Upload files
@ -206,7 +206,7 @@ const ChatInput = ({
<MyTooltip label={selectFileLabel}> <MyTooltip label={selectFileLabel}>
<MyIcon name={selectFileIcon as any} w={'18px'} color={'myGray.600'} /> <MyIcon name={selectFileIcon as any} w={'18px'} color={'myGray.600'} />
</MyTooltip> </MyTooltip>
<File onSelect={(files) => onSelectFile({ files, fileList })} /> <File onSelect={(files) => onSelectFile({ files })} />
</Flex> </Flex>
)} )}
@ -282,7 +282,7 @@ const ChatInput = ({
.filter((file) => { .filter((file) => {
return file && fileTypeFilter(file); return file && fileTypeFilter(file);
}) as File[]; }) as File[];
onSelectFile({ files, fileList }); onSelectFile({ files });
if (files.length > 0) { if (files.length > 0) {
e.stopPropagation(); e.stopPropagation();

View File

@ -33,8 +33,10 @@ export const useFileUpload = (props: UseFileUploadOptions) => {
update: updateFiles, update: updateFiles,
remove: removeFiles, remove: removeFiles,
fields: fileList, fields: fileList,
replace: replaceFiles replace: replaceFiles,
append: appendFiles
} = fileCtrl; } = fileCtrl;
const hasFileUploading = fileList.some((item) => !item.url);
const showSelectFile = fileSelectConfig?.canSelectFile; const showSelectFile = fileSelectConfig?.canSelectFile;
const showSelectImg = fileSelectConfig?.canSelectImg; const showSelectImg = fileSelectConfig?.canSelectImg;
@ -69,7 +71,7 @@ export const useFileUpload = (props: UseFileUploadOptions) => {
}); });
const onSelectFile = useCallback( const onSelectFile = useCallback(
async ({ files, fileList }: { files: File[]; fileList: UserInputFileItemType[] }) => { async ({ files }: { files: File[] }) => {
if (!files || files.length === 0) { if (!files || files.length === 0) {
return []; return [];
} }
@ -128,22 +130,11 @@ export const useFileUpload = (props: UseFileUploadOptions) => {
) )
); );
// Document, image appendFiles(loadFiles);
const concatFileList = clone(
fileList.concat(loadFiles).sort((a, b) => {
if (a.type === ChatFileTypeEnum.image && b.type === ChatFileTypeEnum.file) {
return 1;
} else if (a.type === ChatFileTypeEnum.file && b.type === ChatFileTypeEnum.image) {
return -1;
}
return 0;
})
);
replaceFiles(concatFileList);
return loadFiles; return loadFiles;
}, },
[maxSelectFiles, replaceFiles, toast, t, maxSize] [maxSelectFiles, appendFiles, toast, t, maxSize]
); );
const uploadFiles = async () => { const uploadFiles = async () => {
@ -197,10 +188,23 @@ export const useFileUpload = (props: UseFileUploadOptions) => {
removeFiles(errorFileIndex); removeFiles(errorFileIndex);
}; };
const sortFileList = useMemo(() => {
// Sort: Document, image
const sortResult = clone(fileList).sort((a, b) => {
if (a.type === ChatFileTypeEnum.image && b.type === ChatFileTypeEnum.file) {
return 1;
} else if (a.type === ChatFileTypeEnum.file && b.type === ChatFileTypeEnum.image) {
return -1;
}
return 0;
});
return sortResult;
}, [fileList]);
return { return {
File, File,
onOpenSelectFile, onOpenSelectFile,
fileList, fileList: sortFileList,
onSelectFile, onSelectFile,
uploadFiles, uploadFiles,
selectFileIcon, selectFileIcon,
@ -208,6 +212,7 @@ export const useFileUpload = (props: UseFileUploadOptions) => {
showSelectFile, showSelectFile,
showSelectImg, showSelectImg,
removeFiles, removeFiles,
replaceFiles replaceFiles,
hasFileUploading
}; };
}; };

View File

@ -56,7 +56,7 @@ const RenderInput = () => {
showSelectFile, showSelectFile,
showSelectImg, showSelectImg,
removeFiles, removeFiles,
replaceFiles hasFileUploading
} = useFileUpload({ } = useFileUpload({
outLinkAuthData, outLinkAuthData,
chatId: chatId || '', chatId: chatId || '',
@ -64,9 +64,7 @@ const RenderInput = () => {
fileCtrl fileCtrl
}); });
const isDisabledInput = histories.length > 0; const isDisabledInput = histories.length > 0;
const hasFileUploading = useMemo(() => {
return fileList.some((item) => !item.url);
}, [fileList]);
useRequest2(uploadFiles, { useRequest2(uploadFiles, {
manual: false, manual: false,
errorToast: t('common:upload_file_error'), errorToast: t('common:upload_file_error'),
@ -83,6 +81,7 @@ const RenderInput = () => {
[onNewChat, setRestartData] [onNewChat, setRestartData]
); );
// Get plugin input components
const formatPluginInputs = useMemo(() => { const formatPluginInputs = useMemo(() => {
if (histories.length === 0) return pluginInputs; if (histories.length === 0) return pluginInputs;
try { try {
@ -203,7 +202,7 @@ const RenderInput = () => {
{t('chat:select')} {t('chat:select')}
</Button> </Button>
)} )}
<File onSelect={(files) => onSelectFile({ files, fileList })} /> <File onSelect={(files) => onSelectFile({ files })} />
</Flex> </Flex>
<FilePreview <FilePreview
fileList={fileList} fileList={fileList}

View File

@ -50,7 +50,8 @@ const FileSelector = ({
onOpenSelectFile, onOpenSelectFile,
onSelectFile, onSelectFile,
removeFiles, removeFiles,
replaceFiles replaceFiles,
hasFileUploading
} = useFileUpload({ } = useFileUpload({
outLinkAuthData, outLinkAuthData,
chatId: chatId || '', chatId: chatId || '',
@ -84,9 +85,6 @@ const FileSelector = ({
errorToast: t('common:upload_file_error'), errorToast: t('common:upload_file_error'),
refreshDeps: [fileList, outLinkAuthData, chatId] refreshDeps: [fileList, outLinkAuthData, chatId]
}); });
const hasFileUploading = useMemo(() => {
return fileList.some((item) => !item.url);
}, [fileList]);
useEffect(() => { useEffect(() => {
setUploading(hasFileUploading); setUploading(hasFileUploading);
@ -128,7 +126,7 @@ const FileSelector = ({
<FilePreview fileList={fileList} removeFiles={isDisabledInput ? undefined : removeFiles} /> <FilePreview fileList={fileList} removeFiles={isDisabledInput ? undefined : removeFiles} />
{fileList.length === 0 && <EmptyTip py={0} mt={3} text={t('chat:not_select_file')} />} {fileList.length === 0 && <EmptyTip py={0} mt={3} text={t('chat:not_select_file')} />}
<File onSelect={(files) => onSelectFile({ files, fileList })} /> <File onSelect={(files) => onSelectFile({ files })} />
</> </>
); );
}; };

View File

@ -13,13 +13,16 @@ import dynamic from 'next/dynamic';
import { cloneDeep } from 'lodash'; import { cloneDeep } from 'lodash';
import Flow from '../WorkflowComponents/Flow'; import Flow from '../WorkflowComponents/Flow';
import { t } from 'i18next'; import { ReactFlowProvider } from 'reactflow';
import { useTranslation } from 'next-i18next';
const Logs = dynamic(() => import('../Logs/index')); const Logs = dynamic(() => import('../Logs/index'));
const PublishChannel = dynamic(() => import('../Publish')); const PublishChannel = dynamic(() => import('../Publish'));
const WorkflowEdit = () => { const WorkflowEdit = () => {
const { appDetail, currentTab } = useContextSelector(AppContext, (e) => e); const { appDetail, currentTab } = useContextSelector(AppContext, (e) => e);
const isV2Workflow = appDetail?.version === 'v2'; const isV2Workflow = appDetail?.version === 'v2';
const { t } = useTranslation();
const { openConfirm, ConfirmModal } = useConfirm({ const { openConfirm, ConfirmModal } = useConfirm({
showCancel: false, showCancel: false,
@ -64,9 +67,11 @@ const WorkflowEdit = () => {
const Render = () => { const Render = () => {
return ( return (
<WorkflowContextProvider basicNodeTemplates={pluginSystemModuleTemplates}> <ReactFlowProvider>
<WorkflowEdit /> <WorkflowContextProvider basicNodeTemplates={pluginSystemModuleTemplates}>
</WorkflowContextProvider> <WorkflowEdit />
</WorkflowContextProvider>
</ReactFlowProvider>
); );
}; };

View File

@ -11,15 +11,18 @@ import { Flex } from '@chakra-ui/react';
import { workflowBoxStyles } from '../constants'; import { workflowBoxStyles } from '../constants';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import { cloneDeep } from 'lodash'; import { cloneDeep } from 'lodash';
import { useTranslation } from 'next-i18next';
import Flow from '../WorkflowComponents/Flow'; import Flow from '../WorkflowComponents/Flow';
import { t } from 'i18next'; import { ReactFlowProvider } from 'reactflow';
const Logs = dynamic(() => import('../Logs/index')); const Logs = dynamic(() => import('../Logs/index'));
const PublishChannel = dynamic(() => import('../Publish')); const PublishChannel = dynamic(() => import('../Publish'));
const WorkflowEdit = () => { const WorkflowEdit = () => {
const { appDetail, currentTab } = useContextSelector(AppContext, (e) => e); const { appDetail, currentTab } = useContextSelector(AppContext, (e) => e);
const isV2Workflow = appDetail?.version === 'v2'; const isV2Workflow = appDetail?.version === 'v2';
const { t } = useTranslation();
const { openConfirm, ConfirmModal } = useConfirm({ const { openConfirm, ConfirmModal } = useConfirm({
showCancel: false, showCancel: false,
@ -64,9 +67,11 @@ const WorkflowEdit = () => {
const Render = () => { const Render = () => {
return ( return (
<WorkflowContextProvider basicNodeTemplates={appSystemModuleTemplates}> <ReactFlowProvider>
<WorkflowEdit /> <WorkflowContextProvider basicNodeTemplates={appSystemModuleTemplates}>
</WorkflowContextProvider> <WorkflowEdit />
</WorkflowContextProvider>
</ReactFlowProvider>
); );
}; };

View File

@ -475,8 +475,7 @@ const RenderList = React.memo(function RenderList({
NodeOutputKeyEnum.userChatInput NodeOutputKeyEnum.userChatInput
]; ];
defaultValueMap[NodeInputKeyEnum.fileUrlList] = [ defaultValueMap[NodeInputKeyEnum.fileUrlList] = [
node.nodeId, [node.nodeId, NodeOutputKeyEnum.userFiles]
NodeOutputKeyEnum.userFiles
]; ];
} }
}); });

View File

@ -169,12 +169,4 @@ const Workflow = () => {
); );
}; };
const Render = () => { export default React.memo(Workflow);
return (
<ReactFlowProvider>
<Workflow />
</ReactFlowProvider>
);
};
export default React.memo(Render);

View File

@ -172,6 +172,7 @@ const InputTypeConfig = ({
</FormLabel> </FormLabel>
<Input <Input
bg={'myGray.50'} bg={'myGray.50'}
maxLength={30}
placeholder="appointment/sql" placeholder="appointment/sql"
{...register('label', { {...register('label', {
required: true required: true

View File

@ -247,11 +247,18 @@ const MultipleReferenceSelector = ({
const ArraySelector = useMemo(() => { const ArraySelector = useMemo(() => {
const selectorVal = value as ReferenceItemValueType[]; const selectorVal = value as ReferenceItemValueType[];
const notValidItem =
!selectorVal ||
selectorVal.length === 0 ||
selectorVal.every((item) => {
const [nodeName, outputName] = getSelectValue(item);
return !nodeName || !outputName;
});
return ( return (
<MultipleRowArraySelect <MultipleRowArraySelect
label={ label={
selectorVal && selectorVal.length > 0 ? ( !notValidItem ? (
<Grid py={3} gridTemplateColumns={'1fr 1fr'} gap={2} fontSize={'sm'}> <Grid py={3} gridTemplateColumns={'1fr 1fr'} gap={2} fontSize={'sm'}>
{selectorVal.map((item, index) => { {selectorVal.map((item, index) => {
const [nodeName, outputName] = getSelectValue(item); const [nodeName, outputName] = getSelectValue(item);
@ -261,36 +268,35 @@ const MultipleReferenceSelector = ({
<Flex <Flex
alignItems={'center'} alignItems={'center'}
key={index} key={index}
bg={isInvalidItem ? 'red.50' : 'primary.50'} bg={'primary.50'}
color={isInvalidItem ? 'red.600' : 'myGray.900'} color={'myGray.900'}
py={1} py={1}
px={1.5} px={1.5}
rounded={'sm'} rounded={'sm'}
> >
<Flex alignItems={'center'} flex={1}> <Flex
{isInvalidItem ? ( alignItems={'center'}
t('common:invalid_variable') flex={'1 0 0'}
) : ( maxW={'200px'}
<> className="textEllipsis"
{nodeName} >
<MyIcon {nodeName}
name={'common/rightArrowLight'} <MyIcon
mx={1} name={'common/rightArrowLight'}
w={'12px'} mx={1}
color={'myGray.500'} w={'12px'}
/> color={'myGray.500'}
{outputName} />
</> {outputName}
)}
</Flex> </Flex>
<MyIcon <MyIcon
name={'common/closeLight'} name={'common/closeLight'}
w={'16px'} w={'1rem'}
ml={1} ml={1}
cursor={'pointer'} cursor={'pointer'}
color={'myGray.500'} color={'myGray.500'}
_hover={{ _hover={{
color: 'primary.600' color: 'red.600'
}} }}
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();

View File

@ -32,7 +32,8 @@ import {
NodeChange, NodeChange,
OnConnectStartParams, OnConnectStartParams,
useEdgesState, useEdgesState,
useNodesState useNodesState,
useReactFlow
} from 'reactflow'; } from 'reactflow';
import { createContext, useContextSelector } from 'use-context-selector'; import { createContext, useContextSelector } from 'use-context-selector';
import { defaultRunningStatus } from './constants'; import { defaultRunningStatus } from './constants';
@ -568,6 +569,7 @@ const WorkflowContextProvider = ({
); );
/* ui flow to store data */ /* ui flow to store data */
const { fitView } = useReactFlow();
const flowData2StoreDataAndCheck = useMemoizedFn((hideTip = false) => { const flowData2StoreDataAndCheck = useMemoizedFn((hideTip = false) => {
const checkResults = checkWorkflowNodeAndConnection({ nodes, edges }); const checkResults = checkWorkflowNodeAndConnection({ nodes, edges });
@ -577,6 +579,12 @@ const WorkflowContextProvider = ({
return storeWorkflow; return storeWorkflow;
} else if (!hideTip) { } else if (!hideTip) {
checkResults.forEach((nodeId) => onUpdateNodeError(nodeId, true)); checkResults.forEach((nodeId) => onUpdateNodeError(nodeId, true));
// View move to the node that failed
fitView({
nodes: nodes.filter((node) => checkResults.includes(node.data.nodeId))
});
toast({ toast({
status: 'warning', status: 'warning',
title: t('common:core.workflow.Check Failed') title: t('common:core.workflow.Check Failed')

View File

@ -1,51 +1,47 @@
import React, { useRef, useCallback } from 'react'; import React, { useRef, useCallback } from 'react';
import { Box } from '@chakra-ui/react'; import { Box } from '@chakra-ui/react';
import { useToast } from '@fastgpt/web/hooks/useToast'; import { useToast } from '@fastgpt/web/hooks/useToast';
import { useTranslation } from 'next-i18next';
import { useI18n } from '@/web/context/I18n'; import { useI18n } from '@/web/context/I18n';
import { useMemoizedFn } from 'ahooks';
export const useSelectFile = (props?: { export const useSelectFile = (props?: {
fileType?: string; fileType?: string;
multiple?: boolean; multiple?: boolean;
maxCount?: number; maxCount?: number;
}) => { }) => {
const { t } = useTranslation();
const { fileT } = useI18n(); const { fileT } = useI18n();
const { fileType = '*', multiple = false, maxCount = 10 } = props || {}; const { fileType = '*', multiple = false, maxCount = 10 } = props || {};
const { toast } = useToast(); const { toast } = useToast();
const SelectFileDom = useRef<HTMLInputElement>(null); const SelectFileDom = useRef<HTMLInputElement>(null);
const openSign = useRef<any>(); const openSign = useRef<any>();
const File = useCallback( const File = useMemoizedFn(({ onSelect }: { onSelect: (e: File[], sign?: any) => void }) => (
({ onSelect }: { onSelect: (e: File[], sign?: any) => void }) => ( <Box position={'absolute'} w={0} h={0} overflow={'hidden'}>
<Box position={'absolute'} w={0} h={0} overflow={'hidden'}> <input
<input ref={SelectFileDom}
ref={SelectFileDom} type="file"
type="file" accept={fileType}
accept={fileType} multiple={multiple}
multiple={multiple} onChange={(e) => {
onChange={(e) => { const files = e.target.files;
const files = e.target.files;
if (!files || files?.length === 0) return; if (!files || files?.length === 0) return;
let fileList = Array.from(files); let fileList = Array.from(files);
if (fileList.length > maxCount) { if (fileList.length > maxCount) {
toast({ toast({
status: 'warning', status: 'warning',
title: fileT('select_file_amount_limit', { max: maxCount }) title: fileT('select_file_amount_limit', { max: maxCount })
}); });
fileList = fileList.slice(0, maxCount); fileList = fileList.slice(0, maxCount);
} }
onSelect(fileList, openSign.current); onSelect(fileList, openSign.current);
e.target.value = ''; e.target.value = '';
}} }}
/> />
</Box> </Box>
), ));
[fileT, fileType, maxCount, multiple, toast]
);
const onOpen = useCallback((sign?: any) => { const onOpen = useCallback((sign?: any) => {
openSign.current = sign; openSign.current = sign;

View File

@ -23,7 +23,8 @@ import {
formatEditorVariablePickerIcon, formatEditorVariablePickerIcon,
getAppChatConfig, getAppChatConfig,
getGuideModule, getGuideModule,
isReferenceValue isReferenceValue,
isReferenceValueArray
} from '@fastgpt/global/core/workflow/utils'; } from '@fastgpt/global/core/workflow/utils';
import { TFunction } from 'next-i18next'; import { TFunction } from 'next-i18next';
import { import {
@ -328,7 +329,8 @@ export const checkWorkflowNodeAndConnection = ({
if ( if (
node.data.flowNodeType === FlowNodeTypeEnum.pluginOutput && node.data.flowNodeType === FlowNodeTypeEnum.pluginOutput &&
!(isReferenceValue(input.value, nodeIds) && input.value[1]) (input.value?.length === 0 ||
(isReferenceValue(input.value, nodeIds) && !input.value?.[1]))
) { ) {
return true; return true;
} }