diff --git a/packages/global/core/workflow/constants.ts b/packages/global/core/workflow/constants.ts index b02fc83b0..c51d2ff10 100644 --- a/packages/global/core/workflow/constants.ts +++ b/packages/global/core/workflow/constants.ts @@ -201,6 +201,7 @@ export enum NodeInputKeyEnum { nodeHeight = 'nodeHeight', // loop start loopStartInput = 'loopStartInput', + loopStartIndex = 'loopStartIndex', // loop end loopEndInput = 'loopEndInput', @@ -258,7 +259,7 @@ export enum NodeOutputKeyEnum { loopArray = 'loopArray', // loop start loopStartInput = 'loopStartInput', - loopArrayIndex = 'loopArrayIndex', + loopStartIndex = 'loopStartIndex', // form input formInputResult = 'formInputResult' diff --git a/packages/global/core/workflow/runtime/utils.ts b/packages/global/core/workflow/runtime/utils.ts index b48f7dd93..43a43ce1c 100644 --- a/packages/global/core/workflow/runtime/utils.ts +++ b/packages/global/core/workflow/runtime/utils.ts @@ -239,7 +239,7 @@ export const getReferenceVariableValue = ({ nodes: RuntimeNodeItemType[]; variables: Record; }) => { - if (!value) return undefined; + if (!value) return value; // handle single reference value if (isValidReferenceValueFormat(value)) { @@ -253,7 +253,7 @@ export const getReferenceVariableValue = ({ const node = nodes.find((node) => node.nodeId === sourceNodeId); if (!node) { - return undefined; + return value; } return node.outputs.find((output) => output.id === outputId)?.value; diff --git a/packages/global/core/workflow/template/system/loop/loopStart.ts b/packages/global/core/workflow/template/system/loop/loopStart.ts index 3a41a8b78..2c0d97855 100644 --- a/packages/global/core/workflow/template/system/loop/loopStart.ts +++ b/packages/global/core/workflow/template/system/loop/loopStart.ts @@ -33,12 +33,18 @@ export const LoopStartNode: FlowNodeTemplateType = { label: '', required: true, value: '' + }, + { + key: NodeInputKeyEnum.loopStartIndex, + renderTypeList: [FlowNodeInputTypeEnum.hidden], + valueType: WorkflowIOValueTypeEnum.number, + label: i18nT('workflow:Array_element_index') } ], outputs: [ { - id: NodeOutputKeyEnum.loopArrayIndex, - key: NodeOutputKeyEnum.loopArrayIndex, + id: NodeOutputKeyEnum.loopStartIndex, + key: NodeOutputKeyEnum.loopStartIndex, label: i18nT('workflow:Array_element_index'), type: FlowNodeOutputTypeEnum.static, valueType: WorkflowIOValueTypeEnum.number diff --git a/packages/service/core/workflow/dispatch/index.ts b/packages/service/core/workflow/dispatch/index.ts index 88ac17115..acbf726cc 100644 --- a/packages/service/core/workflow/dispatch/index.ts +++ b/packages/service/core/workflow/dispatch/index.ts @@ -387,6 +387,7 @@ export async function dispatchWorkFlow(data: Props): Promise { if (status === 'run') { nodeRunBeforeHook(node); @@ -482,8 +483,16 @@ export async function dispatchWorkFlow(data: Props): Promise { + // Special input, not format if (input.key === dynamicInput?.key) return; + // Skip some special key + if (input.key === NodeInputKeyEnum.childrenNodeIdList) { + params[input.key] = input.value; + + return; + } + // replace {{xx}} variables let value = replaceVariable(input.value, variables); @@ -506,7 +515,6 @@ export async function dispatchWorkFlow(data: Props): Promise => { node.flowNodeType === FlowNodeTypeEnum.loopStart ) { node.isEntry = true; - node.inputs = node.inputs.map((input) => { + node.inputs.forEach((input) => { if (input.key === NodeInputKeyEnum.loopStartInput) { - return { - ...input, - value: item - }; - } else if (input.key === NodeInputKeyEnum.loopStartInput) { - return { - ...input, - value: index++ - }; - } else { - return input; + input.value = item; + } else if (input.key === NodeInputKeyEnum.loopStartIndex) { + input.value = index++; } }); } }); + const response = await dispatchWorkFlow({ ...props, runtimeEdges: cloneDeep(runtimeEdges) @@ -77,11 +70,13 @@ export const dispatchLoop = async (props: Props): Promise => { (res) => res.moduleType === FlowNodeTypeEnum.loopEnd )?.loopOutputValue; + // Concat runtime response outputValueArr.push(loopOutputValue); loopDetail.push(...response.flowResponses); assistantResponses.push(...response.assistantResponses); + totalPoints += response.flowUsages.reduce((acc, usage) => acc + usage.totalPoints, 0); - totalPoints = response.flowUsages.reduce((acc, usage) => acc + usage.totalPoints, 0); + // Concat new variables newVariables = { ...newVariables, ...response.newVariables diff --git a/packages/service/core/workflow/dispatch/loop/runLoopStart.ts b/packages/service/core/workflow/dispatch/loop/runLoopStart.ts index c7c4cfb24..a6364a88e 100644 --- a/packages/service/core/workflow/dispatch/loop/runLoopStart.ts +++ b/packages/service/core/workflow/dispatch/loop/runLoopStart.ts @@ -7,9 +7,11 @@ import { type Props = ModuleDispatchProps<{ [NodeInputKeyEnum.loopStartInput]: any; + [NodeInputKeyEnum.loopStartIndex]: number; }>; type Response = DispatchNodeResultType<{ [NodeOutputKeyEnum.loopStartInput]: any; + [NodeOutputKeyEnum.loopStartIndex]: number; }>; export const dispatchLoopStart = async (props: Props): Promise => { @@ -18,6 +20,7 @@ export const dispatchLoopStart = async (props: Props): Promise => { [DispatchNodeResponseKeyEnum.nodeResponse]: { loopInputValue: params.loopStartInput }, - [NodeOutputKeyEnum.loopStartInput]: params.loopStartInput + [NodeOutputKeyEnum.loopStartInput]: params.loopStartInput, + [NodeOutputKeyEnum.loopStartIndex]: params.loopStartIndex }; }; diff --git a/packages/service/core/workflow/dispatch/plugin/runInput.ts b/packages/service/core/workflow/dispatch/plugin/runInput.ts index d82727ea2..dde187eba 100644 --- a/packages/service/core/workflow/dispatch/plugin/runInput.ts +++ b/packages/service/core/workflow/dispatch/plugin/runInput.ts @@ -17,6 +17,8 @@ export const dispatchPluginInput = (props: PluginInputProps) => { * 插件单独运行时,这里会是一个特殊的数组 * 插件调用的话,这个参数是一个 string[] 不会进行处理 * 硬性要求:API 单独调用插件时,要避免这种特殊类型冲突 + + TODO: 需要 filter max files */ for (const key in params) { const val = params[key]; diff --git a/packages/web/i18n/en/app.json b/packages/web/i18n/en/app.json index f138befb3..80f7e6024 100644 --- a/packages/web/i18n/en/app.json +++ b/packages/web/i18n/en/app.json @@ -130,7 +130,7 @@ "type.Simple bot": "Simple App", "type.Workflow bot": "Workflow", "upload_file_max_amount": "Maximum File Quantity", - "upload_file_max_amount_tip": "1. The maximum number of files that can be uploaded at one time.\n2. The maximum number of files remembered by the chat window: each round of dialogue will automatically retrieve files from history, files beyond the range will be forgotten.", + "upload_file_max_amount_tip": "Maximum number of files uploaded in a single round of conversation", "variable.select type_desc": "You can define a global variable that does not need to be filled in by the user.\n\nThe value of this variable can come from the API interface, the Query of the shared link, or assigned through the [Variable Update] module.", "variable.textarea_type_desc": "Allows users to input up to 4000 characters in the dialogue box.", "version.Revert success": "Revert Successful", @@ -154,7 +154,7 @@ "workflow.read_files": "Document Parsing", "workflow.read_files_result": "Document Parsing Result", "workflow.read_files_result_desc": "Original document text, consisting of file names and document content, separated by hyphens between multiple files.", - "workflow.read_files_tip": "Parse all uploaded documents in the dialogue and return the corresponding document content.", + "workflow.read_files_tip": "Parse the documents uploaded in this round of dialogue and return the corresponding document content", "workflow.select_description": "Description Text", "workflow.select_description_placeholder": "For example: \nAre there tomatoes in the fridge?", "workflow.select_description_tip": "You can add a description text to explain the meaning of each option to the user.", diff --git a/packages/web/i18n/zh/app.json b/packages/web/i18n/zh/app.json index 59d062c62..4b61a986e 100644 --- a/packages/web/i18n/zh/app.json +++ b/packages/web/i18n/zh/app.json @@ -131,7 +131,7 @@ "type.Simple bot": "简易应用", "type.Workflow bot": "工作流", "upload_file_max_amount": "最大文件数量", - "upload_file_max_amount_tip": "1.单次上传文件的最大数量。\n2.对话窗口记忆的最大文件数量:每轮对话会自动获取历史中的文件,超出范围的文件会被遗忘。", + "upload_file_max_amount_tip": "单轮对话中最大上传文件数量", "variable.select type_desc": "可以为工作流定义全局变量,常用临时缓存。赋值的方式包括:\n1. 从对话页面的 query 参数获取。\n2. 通过 API 的 variables 对象传递。\n3. 通过【变量更新】节点进行赋值。", "variable.textarea_type_desc": "允许用户最多输入4000字的对话框。", "version.Revert success": "回滚成功", @@ -155,7 +155,7 @@ "workflow.read_files": "文档解析", "workflow.read_files_result": "文档解析结果", "workflow.read_files_result_desc": "文档原文,由文件名和文档内容组成,多个文件之间通过横线隔开。", - "workflow.read_files_tip": "解析对话中所有上传的文档,并返回对应文档内容", + "workflow.read_files_tip": "解析本轮对话上传的文档,并返回对应文档内容", "workflow.select_description": "说明文字", "workflow.select_description_placeholder": "例如: \n冰箱里是否有西红柿?", "workflow.select_description_tip": "你可以添加一段说明文字,用以向用户说明每个选项代表的含义。", diff --git a/projects/app/src/components/core/chat/ChatContainer/PluginRunBox/context.tsx b/projects/app/src/components/core/chat/ChatContainer/PluginRunBox/context.tsx index 934347c47..008281b1e 100644 --- a/projects/app/src/components/core/chat/ChatContainer/PluginRunBox/context.tsx +++ b/projects/app/src/components/core/chat/ChatContainer/PluginRunBox/context.tsx @@ -18,6 +18,7 @@ import { OutLinkChatAuthProps } from '@fastgpt/global/support/permission/chat'; import { ChatBoxInputFormType } from '../ChatBox/type'; import { chats2GPTMessages } from '@fastgpt/global/core/chat/adapt'; import { getPluginRunUserQuery } from '@fastgpt/global/core/workflow/utils'; +import { cloneDeep } from 'lodash'; type PluginRunContextType = OutLinkChatAuthProps & PluginRunBoxProps & { @@ -226,13 +227,25 @@ const PluginRunContextProvider = ({ }); try { + // Remove files icon + const formatVariables = cloneDeep(variables); + for (const key in formatVariables) { + if (Array.isArray(formatVariables[key])) { + formatVariables[key].forEach((item) => { + if (item.url && item.icon) { + delete item.icon; + } + }); + } + } + const { responseData } = await onStartChat({ messages, controller: chatController.current, generatingMessage, variables: { - files: files, - ...variables + files, + ...formatVariables } }); if (responseData?.[responseData.length - 1]?.error) { diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/components/ButtonEdge.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/components/ButtonEdge.tsx index 7248ea8f1..faaefe645 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/components/ButtonEdge.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/components/ButtonEdge.tsx @@ -34,9 +34,12 @@ const ButtonEdge = (props: EdgeProps) => { // If parentNode is folded, the edge will not be displayed const parentNode = useMemo(() => { - return nodeList.find( - (node) => (node.nodeId === source || node.nodeId === target) && node.parentNodeId - ); + for (const node of nodeList) { + if ((node.nodeId === source || node.nodeId === target) && node.parentNodeId) { + return nodeList.find((parent) => parent.nodeId === node.parentNodeId); + } + } + return undefined; }, [nodeList, source, target]); const defaultZIndex = useMemo( diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/hooks/useWorkflow.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/hooks/useWorkflow.tsx index 78fdd7718..293dfc1ee 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/hooks/useWorkflow.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/hooks/useWorkflow.tsx @@ -294,7 +294,20 @@ export const useWorkflow = () => { // Loop node size and position const resetParentNodeSizeAndPosition = useMemoizedFn((parentId: string) => { - const childNodes = nodes.filter((node) => node.data.parentNodeId === parentId); + const { childNodes, loopNode } = nodes.reduce( + (acc, node) => { + if (node.data.parentNodeId === parentId) { + acc.childNodes.push(node); + } + if (node.id === parentId) { + acc.loopNode = node; + } + return acc; + }, + { childNodes: [] as Node[], loopNode: undefined as Node | undefined } + ); + + if (!loopNode) return; const rect = getNodesBounds(childNodes); // Calculate parent node size with minimum width/height constraints @@ -320,7 +333,6 @@ export const useWorkflow = () => { value: height } }); - // Update parentNode position onNodesChange([ { diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/Loop/NodeLoop.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/Loop/NodeLoop.tsx index 2d7060e89..0fe5f0dae 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/Loop/NodeLoop.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/Loop/NodeLoop.tsx @@ -103,6 +103,7 @@ const NodeLoop = ({ data, selected }: NodeProps) => { nodeList.filter((node) => node.parentNodeId === nodeId).map((node) => node.nodeId) ); }, [nodeId, nodeList]); + useEffect(() => { onChangeNode({ nodeId, @@ -143,7 +144,6 @@ const NodeLoop = ({ data, selected }: NodeProps) => { } }; }); - console.log(childNodesChange); onNodesChange(childNodesChange); }, [size?.height]); diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/Loop/NodeLoopStart.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/Loop/NodeLoopStart.tsx index b418425ee..9294ba05a 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/Loop/NodeLoopStart.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/Loop/NodeLoopStart.tsx @@ -121,7 +121,7 @@ const NodeLoopStart = ({ data, selected }: NodeProps) => { mr={1} color={'primary.600'} /> - {t(output.label)} + {t(output.label as any)} {output.valueType && {FlowValueTypeMap[output.valueType]?.label}} diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/RenderInput/templates/Reference.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/RenderInput/templates/Reference.tsx index 0e3c7629a..c37d8dfcc 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/RenderInput/templates/Reference.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/RenderInput/templates/Reference.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useMemo } from 'react'; +import React, { useCallback, useEffect, useMemo } from 'react'; import type { RenderInputProps } from '../type'; import { Flex, Box, ButtonProps, Grid } from '@chakra-ui/react'; import MyIcon from '@fastgpt/web/components/common/Icon'; @@ -240,25 +240,42 @@ const MultipleReferenceSelector = ({ [list] ); - const ArraySelector = useMemo(() => { - const selectorVal = value as ReferenceItemValueType[]; - const validSelectValue = selectorVal && selectorVal.length > 0; + // Get valid item and remove invalid item + const formatList = useMemo(() => { + if (!value) return []; + return value?.map((item) => { + const [nodeName, outputName] = getSelectValue(item); + return { + rawValue: item, + nodeName, + outputName + }; + }); + }, [getSelectValue, value]); + + useEffect(() => { + const validList = formatList.filter((item) => item.nodeName && item.outputName); + if (validList.length !== value?.length) { + onSelect(validList.map((item) => item.rawValue)); + } + }, [formatList, onSelect, value]); + + const ArraySelector = useMemo(() => { return ( 0 ? ( - {selectorVal?.map((item, index) => { - const [nodeName, outputName] = getSelectValue(item); - const isInvalidItem = !nodeName || !outputName; + {formatList.map(({ nodeName, outputName }, index) => { + if (!nodeName || !outputName) return null; return ( - {isInvalidItem ? ( - <>{t('common:invalid_variable')} - ) : ( - <> - {nodeName} - - {outputName} - - )} + {nodeName} + + {outputName} ) } - value={selectorVal as any} + value={value as any} list={list} onSelect={onSelect as any} popDirection={popDirection} /> ); - }, [getSelectValue, list, onSelect, placeholder, popDirection, t, value]); + }, [formatList, list, onSelect, placeholder, popDirection, value]); return ArraySelector; }; diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/context/index.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/context/index.tsx index fa98014a1..ec8d0a503 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/context/index.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/context/index.tsx @@ -364,7 +364,6 @@ const WorkflowContextProvider = ({ WorkflowNodeEdgeContext, (state) => state.nodeListString ); - const nodeList = useMemo( () => JSON.parse(nodeListString) as FlowNodeItemType[], [nodeListString] diff --git a/projects/app/src/web/core/workflow/utils.ts b/projects/app/src/web/core/workflow/utils.ts index 5161fd0a8..5f2a21c94 100644 --- a/projects/app/src/web/core/workflow/utils.ts +++ b/projects/app/src/web/core/workflow/utils.ts @@ -413,8 +413,10 @@ export const checkWorkflowNodeAndConnection = ({ return true; } - return input.required && !isValidArrayReferenceValue(input.value, nodeIds); + return !isValidArrayReferenceValue(input.value, nodeIds); } + + // Single reference return input.required && !isValidReferenceValue(input.value, nodeIds); } return false;