4.8.13 test (#3102)

* fix: loop index;edge parent check

* perf: reference invalid check

* fix: ts
This commit is contained in:
Archer
2024-11-08 20:53:58 +08:00
committed by archer
parent 49aaf9b77e
commit 8bd0749afe
17 changed files with 117 additions and 62 deletions

View File

@@ -201,6 +201,7 @@ export enum NodeInputKeyEnum {
nodeHeight = 'nodeHeight', nodeHeight = 'nodeHeight',
// loop start // loop start
loopStartInput = 'loopStartInput', loopStartInput = 'loopStartInput',
loopStartIndex = 'loopStartIndex',
// loop end // loop end
loopEndInput = 'loopEndInput', loopEndInput = 'loopEndInput',
@@ -258,7 +259,7 @@ export enum NodeOutputKeyEnum {
loopArray = 'loopArray', loopArray = 'loopArray',
// loop start // loop start
loopStartInput = 'loopStartInput', loopStartInput = 'loopStartInput',
loopArrayIndex = 'loopArrayIndex', loopStartIndex = 'loopStartIndex',
// form input // form input
formInputResult = 'formInputResult' formInputResult = 'formInputResult'

View File

@@ -239,7 +239,7 @@ export const getReferenceVariableValue = ({
nodes: RuntimeNodeItemType[]; nodes: RuntimeNodeItemType[];
variables: Record<string, any>; variables: Record<string, any>;
}) => { }) => {
if (!value) return undefined; if (!value) return value;
// handle single reference value // handle single reference value
if (isValidReferenceValueFormat(value)) { if (isValidReferenceValueFormat(value)) {
@@ -253,7 +253,7 @@ export const getReferenceVariableValue = ({
const node = nodes.find((node) => node.nodeId === sourceNodeId); const node = nodes.find((node) => node.nodeId === sourceNodeId);
if (!node) { if (!node) {
return undefined; return value;
} }
return node.outputs.find((output) => output.id === outputId)?.value; return node.outputs.find((output) => output.id === outputId)?.value;

View File

@@ -33,12 +33,18 @@ export const LoopStartNode: FlowNodeTemplateType = {
label: '', label: '',
required: true, required: true,
value: '' value: ''
},
{
key: NodeInputKeyEnum.loopStartIndex,
renderTypeList: [FlowNodeInputTypeEnum.hidden],
valueType: WorkflowIOValueTypeEnum.number,
label: i18nT('workflow:Array_element_index')
} }
], ],
outputs: [ outputs: [
{ {
id: NodeOutputKeyEnum.loopArrayIndex, id: NodeOutputKeyEnum.loopStartIndex,
key: NodeOutputKeyEnum.loopArrayIndex, key: NodeOutputKeyEnum.loopStartIndex,
label: i18nT('workflow:Array_element_index'), label: i18nT('workflow:Array_element_index'),
type: FlowNodeOutputTypeEnum.static, type: FlowNodeOutputTypeEnum.static,
valueType: WorkflowIOValueTypeEnum.number valueType: WorkflowIOValueTypeEnum.number

View File

@@ -387,6 +387,7 @@ export async function dispatchWorkFlow(data: Props): Promise<DispatchFlowRespons
node, node,
runtimeEdges runtimeEdges
}); });
const nodeRunResult = await (() => { const nodeRunResult = await (() => {
if (status === 'run') { if (status === 'run') {
nodeRunBeforeHook(node); nodeRunBeforeHook(node);
@@ -482,8 +483,16 @@ export async function dispatchWorkFlow(data: Props): Promise<DispatchFlowRespons
: {}; : {};
node.inputs.forEach((input) => { node.inputs.forEach((input) => {
// Special input, not format
if (input.key === dynamicInput?.key) return; if (input.key === dynamicInput?.key) return;
// Skip some special key
if (input.key === NodeInputKeyEnum.childrenNodeIdList) {
params[input.key] = input.value;
return;
}
// replace {{xx}} variables // replace {{xx}} variables
let value = replaceVariable(input.value, variables); let value = replaceVariable(input.value, variables);
@@ -506,7 +515,6 @@ export async function dispatchWorkFlow(data: Props): Promise<DispatchFlowRespons
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);
} }
params[input.key] = valueTypeFormat(value, input.valueType); params[input.key] = valueTypeFormat(value, input.valueType);
}); });

View File

@@ -51,23 +51,16 @@ export const dispatchLoop = async (props: Props): Promise<Response> => {
node.flowNodeType === FlowNodeTypeEnum.loopStart node.flowNodeType === FlowNodeTypeEnum.loopStart
) { ) {
node.isEntry = true; node.isEntry = true;
node.inputs = node.inputs.map((input) => { node.inputs.forEach((input) => {
if (input.key === NodeInputKeyEnum.loopStartInput) { if (input.key === NodeInputKeyEnum.loopStartInput) {
return { input.value = item;
...input, } else if (input.key === NodeInputKeyEnum.loopStartIndex) {
value: item input.value = index++;
};
} else if (input.key === NodeInputKeyEnum.loopStartInput) {
return {
...input,
value: index++
};
} else {
return input;
} }
}); });
} }
}); });
const response = await dispatchWorkFlow({ const response = await dispatchWorkFlow({
...props, ...props,
runtimeEdges: cloneDeep(runtimeEdges) runtimeEdges: cloneDeep(runtimeEdges)
@@ -77,11 +70,13 @@ export const dispatchLoop = async (props: Props): Promise<Response> => {
(res) => res.moduleType === FlowNodeTypeEnum.loopEnd (res) => res.moduleType === FlowNodeTypeEnum.loopEnd
)?.loopOutputValue; )?.loopOutputValue;
// Concat runtime response
outputValueArr.push(loopOutputValue); outputValueArr.push(loopOutputValue);
loopDetail.push(...response.flowResponses); loopDetail.push(...response.flowResponses);
assistantResponses.push(...response.assistantResponses); 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 = {
...newVariables, ...newVariables,
...response.newVariables ...response.newVariables

View File

@@ -7,9 +7,11 @@ import {
type Props = ModuleDispatchProps<{ type Props = ModuleDispatchProps<{
[NodeInputKeyEnum.loopStartInput]: any; [NodeInputKeyEnum.loopStartInput]: any;
[NodeInputKeyEnum.loopStartIndex]: number;
}>; }>;
type Response = DispatchNodeResultType<{ type Response = DispatchNodeResultType<{
[NodeOutputKeyEnum.loopStartInput]: any; [NodeOutputKeyEnum.loopStartInput]: any;
[NodeOutputKeyEnum.loopStartIndex]: number;
}>; }>;
export const dispatchLoopStart = async (props: Props): Promise<Response> => { export const dispatchLoopStart = async (props: Props): Promise<Response> => {
@@ -18,6 +20,7 @@ export const dispatchLoopStart = async (props: Props): Promise<Response> => {
[DispatchNodeResponseKeyEnum.nodeResponse]: { [DispatchNodeResponseKeyEnum.nodeResponse]: {
loopInputValue: params.loopStartInput loopInputValue: params.loopStartInput
}, },
[NodeOutputKeyEnum.loopStartInput]: params.loopStartInput [NodeOutputKeyEnum.loopStartInput]: params.loopStartInput,
[NodeOutputKeyEnum.loopStartIndex]: params.loopStartIndex
}; };
}; };

View File

@@ -17,6 +17,8 @@ export const dispatchPluginInput = (props: PluginInputProps) => {
* 插件单独运行时,这里会是一个特殊的数组 * 插件单独运行时,这里会是一个特殊的数组
* 插件调用的话,这个参数是一个 string[] 不会进行处理 * 插件调用的话,这个参数是一个 string[] 不会进行处理
* 硬性要求API 单独调用插件时,要避免这种特殊类型冲突 * 硬性要求API 单独调用插件时,要避免这种特殊类型冲突
TODO: 需要 filter max files
*/ */
for (const key in params) { for (const key in params) {
const val = params[key]; const val = params[key];

View File

@@ -130,7 +130,7 @@
"type.Simple bot": "Simple App", "type.Simple bot": "Simple App",
"type.Workflow bot": "Workflow", "type.Workflow bot": "Workflow",
"upload_file_max_amount": "Maximum File Quantity", "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.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.", "variable.textarea_type_desc": "Allows users to input up to 4000 characters in the dialogue box.",
"version.Revert success": "Revert Successful", "version.Revert success": "Revert Successful",
@@ -154,7 +154,7 @@
"workflow.read_files": "Document Parsing", "workflow.read_files": "Document Parsing",
"workflow.read_files_result": "Document Parsing Result", "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_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": "Description Text",
"workflow.select_description_placeholder": "For example: \nAre there tomatoes in the fridge?", "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.", "workflow.select_description_tip": "You can add a description text to explain the meaning of each option to the user.",

View File

@@ -131,7 +131,7 @@
"type.Simple bot": "简易应用", "type.Simple bot": "简易应用",
"type.Workflow bot": "工作流", "type.Workflow bot": "工作流",
"upload_file_max_amount": "最大文件数量", "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.select type_desc": "可以为工作流定义全局变量,常用临时缓存。赋值的方式包括:\n1. 从对话页面的 query 参数获取。\n2. 通过 API 的 variables 对象传递。\n3. 通过【变量更新】节点进行赋值。",
"variable.textarea_type_desc": "允许用户最多输入4000字的对话框。", "variable.textarea_type_desc": "允许用户最多输入4000字的对话框。",
"version.Revert success": "回滚成功", "version.Revert success": "回滚成功",
@@ -155,7 +155,7 @@
"workflow.read_files": "文档解析", "workflow.read_files": "文档解析",
"workflow.read_files_result": "文档解析结果", "workflow.read_files_result": "文档解析结果",
"workflow.read_files_result_desc": "文档原文,由文件名和文档内容组成,多个文件之间通过横线隔开。", "workflow.read_files_result_desc": "文档原文,由文件名和文档内容组成,多个文件之间通过横线隔开。",
"workflow.read_files_tip": "解析对话中所有上传的文档,并返回对应文档内容", "workflow.read_files_tip": "解析本轮对话上传的文档,并返回对应文档内容",
"workflow.select_description": "说明文字", "workflow.select_description": "说明文字",
"workflow.select_description_placeholder": "例如: \n冰箱里是否有西红柿", "workflow.select_description_placeholder": "例如: \n冰箱里是否有西红柿",
"workflow.select_description_tip": "你可以添加一段说明文字,用以向用户说明每个选项代表的含义。", "workflow.select_description_tip": "你可以添加一段说明文字,用以向用户说明每个选项代表的含义。",

View File

@@ -18,6 +18,7 @@ import { OutLinkChatAuthProps } from '@fastgpt/global/support/permission/chat';
import { ChatBoxInputFormType } from '../ChatBox/type'; import { ChatBoxInputFormType } from '../ChatBox/type';
import { chats2GPTMessages } from '@fastgpt/global/core/chat/adapt'; import { chats2GPTMessages } from '@fastgpt/global/core/chat/adapt';
import { getPluginRunUserQuery } from '@fastgpt/global/core/workflow/utils'; import { getPluginRunUserQuery } from '@fastgpt/global/core/workflow/utils';
import { cloneDeep } from 'lodash';
type PluginRunContextType = OutLinkChatAuthProps & type PluginRunContextType = OutLinkChatAuthProps &
PluginRunBoxProps & { PluginRunBoxProps & {
@@ -226,13 +227,25 @@ const PluginRunContextProvider = ({
}); });
try { 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({ const { responseData } = await onStartChat({
messages, messages,
controller: chatController.current, controller: chatController.current,
generatingMessage, generatingMessage,
variables: { variables: {
files: files, files,
...variables ...formatVariables
} }
}); });
if (responseData?.[responseData.length - 1]?.error) { if (responseData?.[responseData.length - 1]?.error) {

View File

@@ -34,9 +34,12 @@ const ButtonEdge = (props: EdgeProps) => {
// If parentNode is folded, the edge will not be displayed // If parentNode is folded, the edge will not be displayed
const parentNode = useMemo(() => { const parentNode = useMemo(() => {
return nodeList.find( for (const node of nodeList) {
(node) => (node.nodeId === source || node.nodeId === target) && node.parentNodeId if ((node.nodeId === source || node.nodeId === target) && node.parentNodeId) {
); return nodeList.find((parent) => parent.nodeId === node.parentNodeId);
}
}
return undefined;
}, [nodeList, source, target]); }, [nodeList, source, target]);
const defaultZIndex = useMemo( const defaultZIndex = useMemo(

View File

@@ -294,7 +294,20 @@ export const useWorkflow = () => {
// Loop node size and position // Loop node size and position
const resetParentNodeSizeAndPosition = useMemoizedFn((parentId: string) => { 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); const rect = getNodesBounds(childNodes);
// Calculate parent node size with minimum width/height constraints // Calculate parent node size with minimum width/height constraints
@@ -320,7 +333,6 @@ export const useWorkflow = () => {
value: height value: height
} }
}); });
// Update parentNode position // Update parentNode position
onNodesChange([ onNodesChange([
{ {

View File

@@ -103,6 +103,7 @@ const NodeLoop = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
nodeList.filter((node) => node.parentNodeId === nodeId).map((node) => node.nodeId) nodeList.filter((node) => node.parentNodeId === nodeId).map((node) => node.nodeId)
); );
}, [nodeId, nodeList]); }, [nodeId, nodeList]);
useEffect(() => { useEffect(() => {
onChangeNode({ onChangeNode({
nodeId, nodeId,
@@ -143,7 +144,6 @@ const NodeLoop = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
} }
}; };
}); });
console.log(childNodesChange);
onNodesChange(childNodesChange); onNodesChange(childNodesChange);
}, [size?.height]); }, [size?.height]);

View File

@@ -121,7 +121,7 @@ const NodeLoopStart = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
mr={1} mr={1}
color={'primary.600'} color={'primary.600'}
/> />
{t(output.label)} {t(output.label as any)}
</Flex> </Flex>
</Td> </Td>
{output.valueType && <Td>{FlowValueTypeMap[output.valueType]?.label}</Td>} {output.valueType && <Td>{FlowValueTypeMap[output.valueType]?.label}</Td>}

View File

@@ -1,4 +1,4 @@
import React, { useCallback, useMemo } from 'react'; import React, { useCallback, useEffect, useMemo } from 'react';
import type { RenderInputProps } from '../type'; import type { RenderInputProps } from '../type';
import { Flex, Box, ButtonProps, Grid } from '@chakra-ui/react'; import { Flex, Box, ButtonProps, Grid } from '@chakra-ui/react';
import MyIcon from '@fastgpt/web/components/common/Icon'; import MyIcon from '@fastgpt/web/components/common/Icon';
@@ -240,25 +240,42 @@ const MultipleReferenceSelector = ({
[list] [list]
); );
const ArraySelector = useMemo(() => { // Get valid item and remove invalid item
const selectorVal = value as ReferenceItemValueType[]; const formatList = useMemo(() => {
const validSelectValue = selectorVal && selectorVal.length > 0; 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 ( return (
<MultipleRowArraySelect <MultipleRowArraySelect
label={ label={
validSelectValue ? ( formatList.length > 0 ? (
<Grid py={3} gridTemplateColumns={'1fr 1fr'} gap={2} fontSize={'sm'}> <Grid py={3} gridTemplateColumns={'1fr 1fr'} gap={2} fontSize={'sm'}>
{selectorVal?.map((item, index) => { {formatList.map(({ nodeName, outputName }, index) => {
const [nodeName, outputName] = getSelectValue(item); if (!nodeName || !outputName) return null;
const isInvalidItem = !nodeName || !outputName;
return ( return (
<Flex <Flex
alignItems={'center'} alignItems={'center'}
key={index} key={index}
bg={isInvalidItem ? 'red.50' : 'primary.50'} bg={'primary.50'}
color={isInvalidItem ? 'red.500' : 'myGray.900'} color={'myGray.900'}
py={1} py={1}
px={1.5} px={1.5}
rounded={'sm'} rounded={'sm'}
@@ -269,27 +286,21 @@ const MultipleReferenceSelector = ({
maxW={'200px'} maxW={'200px'}
className="textEllipsis" className="textEllipsis"
> >
{isInvalidItem ? ( {nodeName}
<>{t('common:invalid_variable')}</> <MyIcon
) : ( name={'common/rightArrowLight'}
<> mx={1}
{nodeName} w={'12px'}
<MyIcon color={'myGray.500'}
name={'common/rightArrowLight'} />
mx={1} {outputName}
w={'12px'}
color={'myGray.500'}
/>
{outputName}
</>
)}
</Flex> </Flex>
<MyIcon <MyIcon
name={'common/closeLight'} name={'common/closeLight'}
w={'1rem'} w={'1rem'}
ml={1} ml={1}
cursor={'pointer'} cursor={'pointer'}
color={isInvalidItem ? 'red.500' : 'myGray.500'} color={'myGray.500'}
_hover={{ _hover={{
color: 'red.600' color: 'red.600'
}} }}
@@ -308,13 +319,13 @@ const MultipleReferenceSelector = ({
</Box> </Box>
) )
} }
value={selectorVal as any} value={value as any}
list={list} list={list}
onSelect={onSelect as any} onSelect={onSelect as any}
popDirection={popDirection} popDirection={popDirection}
/> />
); );
}, [getSelectValue, list, onSelect, placeholder, popDirection, t, value]); }, [formatList, list, onSelect, placeholder, popDirection, value]);
return ArraySelector; return ArraySelector;
}; };

View File

@@ -364,7 +364,6 @@ const WorkflowContextProvider = ({
WorkflowNodeEdgeContext, WorkflowNodeEdgeContext,
(state) => state.nodeListString (state) => state.nodeListString
); );
const nodeList = useMemo( const nodeList = useMemo(
() => JSON.parse(nodeListString) as FlowNodeItemType[], () => JSON.parse(nodeListString) as FlowNodeItemType[],
[nodeListString] [nodeListString]

View File

@@ -413,8 +413,10 @@ export const checkWorkflowNodeAndConnection = ({
return true; return true;
} }
return input.required && !isValidArrayReferenceValue(input.value, nodeIds); return !isValidArrayReferenceValue(input.value, nodeIds);
} }
// Single reference
return input.required && !isValidReferenceValue(input.value, nodeIds); return input.required && !isValidReferenceValue(input.value, nodeIds);
} }
return false; return false;