diff --git a/framework/elsa/fit-elsa-react/src/common/Consts.js b/framework/elsa/fit-elsa-react/src/common/Consts.js index 29321c9af..a9a84df5b 100644 --- a/framework/elsa/fit-elsa-react/src/common/Consts.js +++ b/framework/elsa/fit-elsa-react/src/common/Consts.js @@ -191,7 +191,7 @@ export const RENDER_TYPE = { LABEL: 'Label', }; -export const DEFAULT_LOOP_NODE_CONTEXT = { +export const DEFAULT_ADD_TOOL_NODE_CONTEXT = { id: uuidv4(), name: 'context', type: DATA_TYPES.OBJECT, diff --git a/framework/elsa/fit-elsa-react/src/components/asserts/icon-parallel.svg b/framework/elsa/fit-elsa-react/src/components/asserts/icon-parallel.svg new file mode 100644 index 000000000..964de8285 --- /dev/null +++ b/framework/elsa/fit-elsa-react/src/components/asserts/icon-parallel.svg @@ -0,0 +1,22 @@ + + + Created with Pixso. + + + + + + + + + + + + + + + + + + + diff --git a/framework/elsa/fit-elsa-react/src/components/common/InvokeInput.jsx b/framework/elsa/fit-elsa-react/src/components/common/InvokeInput.jsx index 367aacd89..887b61dca 100644 --- a/framework/elsa/fit-elsa-react/src/components/common/InvokeInput.jsx +++ b/framework/elsa/fit-elsa-react/src/components/common/InvokeInput.jsx @@ -13,7 +13,8 @@ import {JadeInputTree} from '@/components/common/JadeInputTree.jsx'; _InvokeInput.propTypes = { inputData: PropTypes.array, - shapeStatus: PropTypes.object + shapeStatus: PropTypes.object, + parentId: PropTypes.string, } /** @@ -25,9 +26,10 @@ _InvokeInput.propTypes = { * @param radioValue Radio对应的值. * @param radioTitle Radio对应的展示信息. * @param radioRuleMessage Radio没填时的报错信息. + * @param parentId 输入入参上层所属id,非必填. * @returns {JSX.Element} */ -function _InvokeInput({inputData, shapeStatus, showRadio = false, radioValue, radioTitle, radioRuleMessage}) { +function _InvokeInput({inputData, shapeStatus, showRadio = false, radioValue, radioTitle, radioRuleMessage, parentId}) { const dispatch = useDispatch(); /** @@ -37,7 +39,7 @@ function _InvokeInput({inputData, shapeStatus, showRadio = false, radioValue, ra * @param changes 需要改变的属性. */ const updateItem = (id, changes) => { - dispatch({type: 'update', id, changes}); + dispatch({type: 'update', id, changes, parentId}); }; /** diff --git a/framework/elsa/fit-elsa-react/src/components/common/JadeInputTreeCollapse.jsx b/framework/elsa/fit-elsa-react/src/components/common/JadeInputTreeCollapse.jsx index db08568f6..8fc99e46b 100644 --- a/framework/elsa/fit-elsa-react/src/components/common/JadeInputTreeCollapse.jsx +++ b/framework/elsa/fit-elsa-react/src/components/common/JadeInputTreeCollapse.jsx @@ -22,10 +22,11 @@ JadeInputTreeCollapse.propTypes = { * * @param data 数据. * @param children 子组件列表. + * @param props 参数。 * @return {JSX.Element} * @constructor */ -export default function JadeInputTreeCollapse({data, children}) { +export default function JadeInputTreeCollapse({data, children, ...props}) { const { t } = useTranslation(); const getContent = () => { @@ -50,7 +51,7 @@ export default function JadeInputTreeCollapse({data, children}) { const content = getContent(); return (<> - + { const filterArgs = isWaterFlow ? args.find(arg => arg.name === 'inputParams')?.value ?? args : args; const filterRadioValue = isWaterFlow && radioValue ? radioValue.replace(/^inputParams\./, '') : radioValue; - - const handlePluginChange = (entity, returnSchema, uniqueName, name, tags) => { + const handlePluginChange = (entity, uniqueName, name, tags) => { dispatch({ type: 'changePluginByMetaData', entity: entity, - returnSchema: returnSchema, uniqueName: uniqueName, pluginName: name, tags: tags, @@ -60,4 +59,8 @@ const LoopWrapper = ({shapeStatus}) => { ); }; +LoopWrapper.propTypes = { + shapeStatus: PropTypes.object, +}; + export default LoopWrapper; \ No newline at end of file diff --git a/framework/elsa/fit-elsa-react/src/components/loopNode/SkillForm.jsx b/framework/elsa/fit-elsa-react/src/components/loopNode/SkillForm.jsx index 4dd92c308..1e6cbef43 100644 --- a/framework/elsa/fit-elsa-react/src/components/loopNode/SkillForm.jsx +++ b/framework/elsa/fit-elsa-react/src/components/loopNode/SkillForm.jsx @@ -12,6 +12,7 @@ import {convertParameter, convertReturnFormat} from '@/components/util/MethodMet import {useTranslation} from 'react-i18next'; import {MinusCircleOutlined} from '@ant-design/icons'; import PropTypes from 'prop-types'; +import {recursive} from '@/components/util/ReferenceUtil.js'; /** * 循环节点插件折叠区域组件 @@ -51,7 +52,7 @@ const _SkillForm = ({plugin, data = undefined, handlePluginChange, handlePluginD const outputParams = convertReturnFormat(selectedData.schema.return); outputParams.type = 'Array'; entity.outputParams = [outputParams]; - handlePluginChange(entity, selectedData.schema.return, selectedData.uniqueName, selectedData.name, selectedData.tags); + handlePluginChange(entity, selectedData.uniqueName, selectedData.name, selectedData.tags); }; const pluginSelectEvent = { @@ -63,17 +64,6 @@ const _SkillForm = ({plugin, data = undefined, handlePluginChange, handlePluginD }, }; - const recursive = (params, parent, action) => { - params.forEach(p => { - if (p.type === 'Object') { - recursive(p.value, p, action); - action(p, parent); - } else { - action(p, parent); - } - }); - }; - const deregisterObservables = () => { if (data) { recursive(data, null, (p) => { @@ -150,9 +140,9 @@ const _SkillForm = ({plugin, data = undefined, handlePluginChange, handlePluginD > {plugin && plugin.id &&
- - {plugin?.name ?? ''} - + + {plugin?.name ?? ''} + {renderDeleteIcon(plugin.id)}
} diff --git a/framework/elsa/fit-elsa-react/src/components/loopNode/loopComponent.jsx b/framework/elsa/fit-elsa-react/src/components/loopNode/loopComponent.jsx index 1dc8c58a2..6ac14423d 100644 --- a/framework/elsa/fit-elsa-react/src/components/loopNode/loopComponent.jsx +++ b/framework/elsa/fit-elsa-react/src/components/loopNode/loopComponent.jsx @@ -9,7 +9,7 @@ import {ChangeFlowMetaReducer} from '@/components/common/reducers/commonReducers import {ChangePluginByMetaDataReducer, DeletePluginReducer, UpdateInputReducer, UpdateRadioInfoReducer} from '@/components/loopNode/reducers/reducers.js'; import {defaultComponent} from '@/components/defaultComponent.js'; import {v4 as uuidv4} from 'uuid'; -import {DATA_TYPES, DEFAULT_LOOP_NODE_CONTEXT, FROM_TYPE} from '@/common/Consts.js'; +import {DATA_TYPES, DEFAULT_ADD_TOOL_NODE_CONTEXT, FROM_TYPE} from '@/common/Consts.js'; export const loopComponent = (jadeConfig, shape) => { const self = defaultComponent(jadeConfig); @@ -50,7 +50,7 @@ export const loopComponent = (jadeConfig, shape) => { from: FROM_TYPE.INPUT, value: {}, }, - DEFAULT_LOOP_NODE_CONTEXT, + DEFAULT_ADD_TOOL_NODE_CONTEXT, ], outputParams: [], }; diff --git a/framework/elsa/fit-elsa-react/src/components/loopNode/reducers/reducers.js b/framework/elsa/fit-elsa-react/src/components/loopNode/reducers/reducers.js index 621a4a376..ae2648620 100644 --- a/framework/elsa/fit-elsa-react/src/components/loopNode/reducers/reducers.js +++ b/framework/elsa/fit-elsa-react/src/components/loopNode/reducers/reducers.js @@ -8,7 +8,7 @@ import {updateInput} from '@/components/util/JadeConfigUtils.js'; import {DEFAULT_INPUT_PARAMS} from '@/components/loopNode/LoopConsts.js'; import {TOOL_TYPE} from '@/common/Consts.js'; -export const ChangePluginByMetaDataReducer = (shape) => { +export const ChangePluginByMetaDataReducer = () => { const self = {}; self.type = 'changePluginByMetaData'; @@ -39,19 +39,18 @@ export const ChangePluginByMetaDataReducer = (shape) => { } }); + const updateToolInfo = (toolInfo = {}) => { + return { + ...toolInfo, + params: newConfig.inputParams?.find(param => param.name === 'args')?.value?.map(({name}) => ({name})) || [], + uniqueName: action.uniqueName, + return: { type: 'array' }, + pluginName: action.pluginName, + tags: action.tags + }; + }; - function updateToolInfo() { - newToolInfo.params = newConfig.inputParams.find(param => param.name === 'args').value.map(property => { - return {name: property.name}; - }); - newToolInfo.uniqueName = action.uniqueName; - newToolInfo.return = {}; - newToolInfo.return.type = 'array'; - newToolInfo.pluginName = action.pluginName; - newToolInfo.tags = action.tags; - } - let newToolInfo = {}; - updateToolInfo(); + const newToolInfo = updateToolInfo(); Object.entries(newConfig).forEach(([key, value]) => { if (key === 'inputParams') { @@ -69,12 +68,9 @@ export const ChangePluginByMetaDataReducer = (shape) => { newConfig[key] = value; } }); - return newConfig; }; - - return self; }; @@ -134,7 +130,6 @@ export const UpdateInputReducer = () => { newConfig[key] = value; } }); - return newConfig; }; diff --git a/framework/elsa/fit-elsa-react/src/components/manualCheck/ManualCheckForm.jsx b/framework/elsa/fit-elsa-react/src/components/manualCheck/ManualCheckForm.jsx index a3448553b..e5445f379 100644 --- a/framework/elsa/fit-elsa-react/src/components/manualCheck/ManualCheckForm.jsx +++ b/framework/elsa/fit-elsa-react/src/components/manualCheck/ManualCheckForm.jsx @@ -13,6 +13,7 @@ import {convertParameter, convertReturnFormat} from '@/components/util/MethodMet import {useTranslation} from 'react-i18next'; import {EyeOutlined, MinusCircleOutlined} from '@ant-design/icons'; import PropTypes from 'prop-types'; +import {recursive} from '@/components/util/ReferenceUtil.js'; /** * 人工检查节点折叠区域组件 @@ -87,17 +88,6 @@ const _ManualCheckForm = ({form, data = undefined, handleFormChange, handleFormD }, }; - const recursive = (params, parent, action) => { - params.forEach(p => { - if (p.type === 'Object') { - recursive(p.value, p, action); - action(p, parent); - } else { - action(p, parent); - } - }); - }; - const deregisterObservables = () => { if (data) { recursive(data, null, (p) => { diff --git a/framework/elsa/fit-elsa-react/src/components/parallelNode/ParallelPluginItem.jsx b/framework/elsa/fit-elsa-react/src/components/parallelNode/ParallelPluginItem.jsx new file mode 100644 index 000000000..1d29e525d --- /dev/null +++ b/framework/elsa/fit-elsa-react/src/components/parallelNode/ParallelPluginItem.jsx @@ -0,0 +1,145 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) 2025 Huawei Technologies Co., Ltd. All rights reserved. + * This file is a part of the ModelEngine Project. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import {Button, Collapse, Form} from 'antd'; +import {useDataContext, useShapeContext} from '@/components/DefaultRoot.jsx'; +import React, {useEffect, useState} from 'react'; +import {useTranslation} from 'react-i18next'; +import {MinusCircleOutlined} from '@ant-design/icons'; +import PropTypes from 'prop-types'; +import {InvokeOutput} from '@/components/common/InvokeOutput.jsx'; +import {InvokeInput} from '@/components/common/InvokeInput.jsx'; +import {JadeCollapse} from '@/components/common/JadeCollapse.jsx'; +import {TOOL_TYPE} from '@/common/Consts.js'; +import {v4 as uuidv4} from 'uuid'; +import {recursive} from '@/components/util/ReferenceUtil.js'; + +const {Panel} = Collapse; + +/** + * 并行节点插件配置组件 + * + * @param plugin 插件信息. + * @param handlePluginDelete 选项删除后的回调. + * @param shapeStatus 图形状态. + * @return {JSX.Element} + * @constructor + */ +const _ParallelPluginItem = ({plugin, handlePluginDelete, shapeStatus}) => { + const shape = useShapeContext(); + const data = useDataContext(); + const [pluginInValid, setPluginInValid] = useState(false); + const {t} = useTranslation(); + const args = plugin?.value?.find(arg => arg.name === 'args')?.value ?? []; + const isWaterFlow = plugin?.value?.find(arg => arg.name === 'tags')?.value ?? [].some(tag => tag === TOOL_TYPE.WATER_FLOW) + const filterArgs = isWaterFlow ? args.find(arg => arg.name === 'inputParams')?.value ?? args : args; + const outputName = plugin?.value?.find(item => item.name === 'outputName')?.value ?? ''; + const output = data?.outputParams?.find(arg => arg.name === 'output') ?? {}; + const registryOutputObject = output?.value?.find(arg => arg.name === outputName) ?? {}; + const virtualPluginOutputData = [{ + id: "output_" + uuidv4(), + name: "output", + type: registryOutputObject?.type, + value: registryOutputObject?.value ?? [] + }]; + + const registryNode = (nodeData, parent, shape) => { + shape.page.registerObservable({ + nodeId: shape.id, + observableId: nodeData.id, + value: nodeData.name, + type: nodeData.type, + parentId: parent ? parent.id : null + }); + if (nodeData.type === "Object") { + nodeData?.value?.map(v => registryNode(v, nodeData, shape)); + } + }; + + useEffect(() => { + if (!registryOutputObject) { + return; + } + registryNode(registryOutputObject, output, shape); + }, [registryOutputObject]); + + const deregisterObservables = () => { + if (registryOutputObject) { + recursive([registryOutputObject], output, (p) => { + shape.page.removeObservable(shape.id, p.id); + }); + } + }; + + const renderDeleteIcon = (id, outputName) => { + return (<> + + ); + }; + + return (<> + { + const validateInfo = shape.graph.validateInfo?.find(node => node?.nodeId === shape.id); + if (!(validateInfo?.isValid ?? true)) { + const modelConfigCheck = validateInfo.configChecks?.find(configCheck => configCheck.configName === 'pluginId'); + if (modelConfigCheck && modelConfigCheck.pluginId === plugin?.id) { + setPluginInValid(true); + return Promise.reject(new Error(`${plugin?.name} ${t('selectedValueNotExist')}`)); + } + } + setPluginInValid(false); + return Promise.resolve(); + }, + }, + ]} + validateTrigger="onBlur" // 或者使用 "onChange" 进行触发校验 + > +
+ + + {plugin?.value?.find(item => item.name === 'outputName')?.value ?? ''} + {renderDeleteIcon(plugin.id, outputName)} +
} + key="parallelPanel"> +
+ {filterArgs.length > 0 && } + {virtualPluginOutputData.length > 0 && } +
+
+
+ + + ); +}; + +_ParallelPluginItem.propTypes = { + plugin: PropTypes.object.isRequired, + handlePluginDelete: PropTypes.func.isRequired, + shapeStatus: PropTypes.object.isRequired, +}; + +const areEqual = (prevProps, nextProps) => { + return prevProps.plugin === nextProps.plugin && + prevProps.handlePluginDelete === nextProps.handlePluginDelete && + prevProps.shapeStatus === nextProps.shapeStatus; +}; + +export const ParallelPluginItem = React.memo(_ParallelPluginItem, areEqual); \ No newline at end of file diff --git a/framework/elsa/fit-elsa-react/src/components/parallelNode/ParallelTopBar.jsx b/framework/elsa/fit-elsa-react/src/components/parallelNode/ParallelTopBar.jsx new file mode 100644 index 000000000..9eb4b4f33 --- /dev/null +++ b/framework/elsa/fit-elsa-react/src/components/parallelNode/ParallelTopBar.jsx @@ -0,0 +1,84 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) 2025 Huawei Technologies Co., Ltd. All rights reserved. + * This file is a part of the ModelEngine Project. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import {Button} from 'antd'; +import {useShapeContext} from '@/components/DefaultRoot.jsx'; +import React from 'react'; +import {convertParameter, convertReturnFormat} from '@/components/util/MethodMetaDataParser.js'; +import {PlusOutlined} from '@ant-design/icons'; +import PropTypes from 'prop-types'; +import {useTranslation} from 'react-i18next'; + +/** + * 并行节点头部工具栏组件 + * + * @param handlePluginAdd 选项选择后的回调. + * @param disabled 禁用状态. + * @return {JSX.Element} + * @constructor + */ +const _ParallelTopBar = ({handlePluginAdd, disabled}) => { + const shape = useShapeContext(); + const {t} = useTranslation(); + + const onSelect = (selectedData) => { + const inputProperties = selectedData.schema?.parameters?.properties?.inputParams?.properties; + if (inputProperties) { + delete inputProperties.traceId; + delete inputProperties.callbackId; + delete inputProperties.userId; + } + const entity = {}; + const orderProperties = selectedData.schema.parameters.order ? + selectedData.schema.parameters.order : Object.keys(selectedData.schema.parameters.properties); + entity.inputParams = orderProperties.map(key => { + return convertParameter({ + propertyName: key, + property: selectedData.schema.parameters.properties[key], + isRequired: selectedData.schema.parameters.required.some(item => item === key), + }); + }); + entity.outputParams = [convertReturnFormat(selectedData.schema.return)]; + handlePluginAdd(entity, selectedData.uniqueName, selectedData.name, selectedData.tags); + }; + + const triggerSelect = (e) => { + e.preventDefault(); + shape.page.triggerEvent({ + type: 'SELECT_PARALLEL_PLUGINS', + value: { + shapeId: shape.id, + onSelect: onSelect, + }, + }); + e.stopPropagation(); // 阻止事件冒泡 + }; + + return (<> +
+ +
+ ); +}; + +_ParallelTopBar.propTypes = { + handlePluginChange: PropTypes.func.isRequired, + disabled: PropTypes.bool.isRequired, +}; + +const areEqual = (prevProps, nextProps) => { + return prevProps.handlePluginChange === nextProps.handlePluginChange && prevProps.disabled === nextProps.disabled; +}; + +export const ParallelTopBar = React.memo(_ParallelTopBar, areEqual); \ No newline at end of file diff --git a/framework/elsa/fit-elsa-react/src/components/parallelNode/ParallelWrapper.jsx b/framework/elsa/fit-elsa-react/src/components/parallelNode/ParallelWrapper.jsx new file mode 100644 index 000000000..002b5307b --- /dev/null +++ b/framework/elsa/fit-elsa-react/src/components/parallelNode/ParallelWrapper.jsx @@ -0,0 +1,89 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) 2025 Huawei Technologies Co., Ltd. All rights reserved. + * This file is a part of the ModelEngine Project. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import {useDataContext, useDispatch, useShapeContext} from '@/components/DefaultRoot.jsx'; +import {ParallelTopBar} from '@/components/parallelNode/ParallelTopBar.jsx'; +import {ParallelPluginItem} from '@/components/parallelNode/ParallelPluginItem.jsx'; +import {Form} from 'antd'; +import {useTranslation} from 'react-i18next'; +import PropTypes from 'prop-types'; +import {useEffect, useMemo} from 'react'; +import {OUTPUT, OUTPUT_NAME, TOOL_CALLS} from '@/components/parallelNode/consts.js'; + +/** + * 并行节点Wrapper + * + * @param shapeStatus 图形状态 + * @returns {JSX.Element} 循环节点Wrapper的DOM + */ +const ParallelWrapper = ({shapeStatus}) => { + const shape = useShapeContext(); + const data = useDataContext(); + const dispatch = useDispatch(); + const {t} = useTranslation(); + + const tools = useMemo( + () => data?.inputParams?.find(value => value.name === TOOL_CALLS)?.value ?? [], + [data?.inputParams] + ); + + useEffect(() => { + const output = data?.outputParams?.find(item => item.name === OUTPUT) ?? {}; + shape.page.registerObservable({ + nodeId: shape.id, + observableId: output.id, + value: output.name, + type: output.type, + parentId: null, + }); + }, [data?.outputParams]); + + const handlePluginAdd = (entity, uniqueName, name, tags) => { + dispatch({ + type: 'addPluginByMetaData', + entity: entity, + uniqueName: uniqueName, + pluginName: name, + tags: tags, + }); + }; + + const handlePluginDelete = (deletePluginId, outputName) => { + dispatch({ + type: 'deletePlugin', id: deletePluginId, outputName: outputName, + }); + }; + + return (<> +
+ + { + if (tools.length < 1) { + return Promise.reject(new Error(t('pluginCannotBeEmpty'))); + } + return Promise.resolve(); + }, + }, + ]} + validateTrigger="onBlur" + > + {tools.map((tool) => ( + item.name === OUTPUT_NAME)?.value ?? ''} plugin={tool} handlePluginDelete={handlePluginDelete} shapeStatus={shapeStatus}/> + ))} + +
+ ); +}; + +ParallelWrapper.propTypes = { + shapeStatus: PropTypes.object, +}; + +export default ParallelWrapper; \ No newline at end of file diff --git a/framework/elsa/fit-elsa-react/src/components/parallelNode/consts.js b/framework/elsa/fit-elsa-react/src/components/parallelNode/consts.js new file mode 100644 index 000000000..278b3249a --- /dev/null +++ b/framework/elsa/fit-elsa-react/src/components/parallelNode/consts.js @@ -0,0 +1,13 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) 2025 Huawei Technologies Co., Ltd. All rights reserved. + * This file is a part of the ModelEngine Project. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +export const TOOL_CALLS = 'toolCalls'; + +export const OUTPUT_NAME = 'outputName'; + +export const ARGS = 'args'; + +export const OUTPUT = 'output'; \ No newline at end of file diff --git a/framework/elsa/fit-elsa-react/src/components/parallelNode/parallelComponent.jsx b/framework/elsa/fit-elsa-react/src/components/parallelNode/parallelComponent.jsx new file mode 100644 index 000000000..851220000 --- /dev/null +++ b/framework/elsa/fit-elsa-react/src/components/parallelNode/parallelComponent.jsx @@ -0,0 +1,70 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) 2025 Huawei Technologies Co., Ltd. All rights reserved. + * This file is a part of the ModelEngine Project. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import ParallelWrapper from '@/components/parallelNode/ParallelWrapper.jsx'; +import {ChangeFlowMetaReducer} from '@/components/common/reducers/commonReducers.js'; + +import {defaultComponent} from '@/components/defaultComponent.js'; +import {v4 as uuidv4} from 'uuid'; +import {DATA_TYPES, DEFAULT_ADD_TOOL_NODE_CONTEXT, FROM_TYPE} from '@/common/Consts.js'; +import {AddPluginByMetaDataReducer, DeletePluginReducer, UpdateInputReducer} from '@/components/parallelNode/reducers/reducers.js'; +import {OUTPUT, TOOL_CALLS} from '@/components/parallelNode/consts.js'; + +export const parallelComponent = (jadeConfig, shape) => { + const self = defaultComponent(jadeConfig); + const addReducer = (map, reducer) => map.set(reducer.type, reducer); + const builtInReducers = new Map(); + addReducer(builtInReducers, AddPluginByMetaDataReducer(shape, self)); + addReducer(builtInReducers, DeletePluginReducer(shape, self)); + addReducer(builtInReducers, UpdateInputReducer(shape, self)); + addReducer(builtInReducers, ChangeFlowMetaReducer(shape, self)); + + /** + * 必填 + * + * @return 组件信息 + */ + self.getJadeConfig = () => { + return jadeConfig ? jadeConfig : { + inputParams: [ + { + id: uuidv4(), + name: TOOL_CALLS, + type: DATA_TYPES.ARRAY, + from: FROM_TYPE.EXPAND, + value: [], + }, + DEFAULT_ADD_TOOL_NODE_CONTEXT], + outputParams: [{ + id: uuidv4(), + name: OUTPUT, + type: DATA_TYPES.OBJECT, + from: FROM_TYPE.EXPAND, + value: [], + }], + }; + }; + + /** + * 必须. + */ + self.getReactComponents = (shapeStatus) => { + return (<> + + ); + }; + + /** + * @override + */ + const reducers = self.reducers; + self.reducers = (config, action) => { + const reducer = builtInReducers.get(action.type); + return reducer ? reducer.reduce(config, action) : reducers.apply(self, [config, action]); + }; + + return self; +}; \ No newline at end of file diff --git a/framework/elsa/fit-elsa-react/src/components/parallelNode/parallelNodeDrawer.jsx b/framework/elsa/fit-elsa-react/src/components/parallelNode/parallelNodeDrawer.jsx new file mode 100644 index 000000000..cbaba3aee --- /dev/null +++ b/framework/elsa/fit-elsa-react/src/components/parallelNode/parallelNodeDrawer.jsx @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) 2025 Huawei Technologies Co., Ltd. All rights reserved. + * This file is a part of the ModelEngine Project. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import {jadeNodeDrawer} from '@/components/base/jadeNodeDrawer.jsx'; +import ParallelIcon from "../asserts/icon-parallel.svg?react"; // 导入背景图片 + +/** + * 并行节点绘制器 + * + * @override + */ +export const parallelNodeDrawer = (shape, div, x, y) => { + const self = jadeNodeDrawer(shape, div, x, y); + self.type = "parallelNodeDrawer"; + + /** + * @override + */ + self.getHeaderIcon = () => { + return (<> + + ); + }; + + return self; +}; \ No newline at end of file diff --git a/framework/elsa/fit-elsa-react/src/components/parallelNode/parallelNodeState.jsx b/framework/elsa/fit-elsa-react/src/components/parallelNode/parallelNodeState.jsx new file mode 100644 index 000000000..b31f4756f --- /dev/null +++ b/framework/elsa/fit-elsa-react/src/components/parallelNode/parallelNodeState.jsx @@ -0,0 +1,58 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) 2025 Huawei Technologies Co., Ltd. All rights reserved. + * This file is a part of the ModelEngine Project. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import {parallelNodeDrawer} from '@/components/parallelNode/parallelNodeDrawer.jsx'; +import {jadeNode} from '@/components/base/jadeNode.jsx'; +import {SECTION_TYPE, TOOL_TYPE} from '@/common/Consts.js'; +import {TOOL_CALLS} from '@/components/parallelNode/consts.js'; + +/** + * jadeStream中的并行节点. + * + * @override + */ +export const parallelNodeState = (id, x, y, width, height, parent, drawer) => { + const self = jadeNode(id, x, y, width, height, parent, drawer ? drawer : parallelNodeDrawer); + self.type = 'parallelNodeState'; + self.text = self.graph.i18n?.t('parallelNode') ?? 'parallelNode'; + self.componentName = 'parallelComponent'; + self.width = 380; + self.flowMeta.jober.type = 'STORE_JOBER'; + const parallelNodeEntity = { + uniqueName: "", + params: [{"name": TOOL_CALLS}, {"name": "context"}], + return: {type: "object"} + }; + + /** + * @override + */ + const processMetaData = self.processMetaData; + self.processMetaData = (metaData) => { + if (!metaData) { + return; + } + processMetaData.apply(self, [metaData]); + self.flowMeta.jober.entity = parallelNodeEntity; + self.flowMeta.jober.entity.uniqueName = metaData.uniqueName; + }; + + /** + * 应用工具流节点的测试报告章节 + */ + self.getRunReportSections = () => { + const inputData = {}; + self.input?.toolCalls?.forEach((toolCall) => { + const isWaterFlow = toolCall?.tags?.includes(TOOL_TYPE.WATER_FLOW) ?? false; + inputData[toolCall?.outputName ?? 'output'] = isWaterFlow ? toolCall?.args?.inputParams : toolCall?.args; + }); + return [{no: '1', name: 'input', type: SECTION_TYPE.DEFAULT, data: inputData ?? {}}, { + no: '2', name: 'output', type: SECTION_TYPE.DEFAULT, data: self.getOutputData(self.output), + }]; + }; + + return self; +}; diff --git a/framework/elsa/fit-elsa-react/src/components/parallelNode/reducers/reducers.js b/framework/elsa/fit-elsa-react/src/components/parallelNode/reducers/reducers.js new file mode 100644 index 000000000..9f0fc1367 --- /dev/null +++ b/framework/elsa/fit-elsa-react/src/components/parallelNode/reducers/reducers.js @@ -0,0 +1,227 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) 2025 Huawei Technologies Co., Ltd. All rights reserved. + * This file is a part of the ModelEngine Project. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import {updateInput} from '@/components/util/JadeConfigUtils.js'; +import {v4 as uuidv4} from 'uuid'; +import {DATA_TYPES, FROM_TYPE} from '@/common/Consts.js'; +import {ARGS, OUTPUT, OUTPUT_NAME, TOOL_CALLS} from '@/components/parallelNode/consts.js'; + +export const AddPluginByMetaDataReducer = () => { + const self = {}; + self.type = 'addPluginByMetaData'; + + /** + * 处理方法. + * + * @param config 配置数据. + * @param action 行为参数. + * @return {*} 处理之后的数据. + */ + self.reduce = (config, action) => { + const newConfig = {...config}; + + const generatePluginName = (text) => { + const toolList = newConfig?.inputParams?.find(value => value.name === TOOL_CALLS)?.value ?? []; + let textVal = text; + const textArray = toolList.map(tool => + tool.value.find(item => item.name === OUTPUT_NAME)?.value + ); + if (textArray.filter(t => t === textVal).length < 1) { + return textVal; + } + const separator = '_'; + let index = 1; + while (true) { + // 不带下划线,直接拼接_1 + const lastSeparatorIndex = textVal.lastIndexOf(separator); + const last = textVal.substring(lastSeparatorIndex + 1, textVal.length); + // 如果是数字,把数字+1 如果不是数字,拼接_1 + if (lastSeparatorIndex !== -1 && !isNaN(parseInt(last))) { + textVal = textVal.substring(0, lastSeparatorIndex) + separator + index; + } else { + textVal = textVal + separator + index; + } + if (!textArray.includes(textVal)) { + return textVal; + } + index++; + } + }; + + const uniquePluginName = generatePluginName(action.pluginName); + + const PLUGIN_INPUT = { + id: uuidv4(), + type: DATA_TYPES.OBJECT, + from: FROM_TYPE.EXPAND, + value: [{ + id: uuidv4(), + name: 'uniqueName', + type: DATA_TYPES.STRING, + from: FROM_TYPE.INPUT, + value: action.uniqueName, + },{ + id: uuidv4(), + name: ARGS, + type: DATA_TYPES.OBJECT, + from: FROM_TYPE.EXPAND, + value: action.entity.inputParams, + },{ + id: uuidv4(), + name: 'order', + type: DATA_TYPES.ARRAY, + from: FROM_TYPE.INPUT, + value: action.entity.inputParams.map(({name}) => ({name})) || [], + }, { + id: uuidv4(), + name: OUTPUT_NAME, + type: DATA_TYPES.STRING, + from: FROM_TYPE.INPUT, + value: uniquePluginName, + }, { + id: uuidv4(), + name: 'tags', + type: DATA_TYPES.ARRAY, + from: FROM_TYPE.INPUT, + value: action.tags, + }], + }; + + const convertedOutput = action.entity?.outputParams?.find(item => item.name === OUTPUT) ?? {}; + + const PLUGIN_OUTPUT = { + id: uuidv4(), + type: convertedOutput?.type ?? DATA_TYPES.OBJECT, + name: uniquePluginName, + value: convertedOutput?.value ?? {}, + }; + + return Object.entries(newConfig).reduce((acc, [key, value]) => { + switch (key) { + case 'inputParams': + acc[key] = value.map(item => + item.name === TOOL_CALLS + ? {...item, value: [...item.value, PLUGIN_INPUT]} + : item + ); + break; + case 'outputParams': + acc[key] = value.map(item => + item.name === OUTPUT + ? {...item, value: [...item.value, PLUGIN_OUTPUT]} + : item + ); + break; + default: + acc[key] = value; + } + return acc; + }, {}); + }; + + return self; +}; + +export const DeletePluginReducer = () => { + const self = {}; + self.type = 'deletePlugin'; + + /** + * 处理方法. + * + * @param config 配置数据. + * @param action 行为参数. + * @return {*} 处理之后的数据. + */ + self.reduce = (config, action) => { + const newConfig = {...config}; + Object.entries(config).forEach(([key, value]) => { + if (key === 'inputParams') { + newConfig[key] = value.map(item => { + if (item.name === TOOL_CALLS) { + return { + ...item, + value: item.value.filter(v => v.value.find(arg => arg.name === OUTPUT_NAME).value !== action.outputName), + }; + } else { + return item; + } + }); + } else if (key === 'outputParams') { + newConfig[key] = value.map(item => { + if (item.name === OUTPUT) { + return { + ...item, + value: item.value.filter(v => v.name !== action.outputName), + }; + } else { + return item; + } + }); + } else { + newConfig[key] = value; + } + }); + + return newConfig; + }; + + return self; +}; + +/** + * update 事件处理器. + * + * @return {{}} 处理器对象. + * @constructor + */ +export const UpdateInputReducer = () => { + const self = {}; + self.type = 'update'; + + /** + * 处理方法. + * + * @param config 配置数据. + * @param action 行为参数. + * @return {*} 处理之后的数据. + */ + self.reduce = (config, action) => { + const newConfig = { ...config }; + if (!config.inputParams) { + return newConfig; + } + newConfig.inputParams = config.inputParams.map(item => { + if (item.name !== TOOL_CALLS) { + return item; + } + return { + ...item, + value: item.value.map(plugin => { + if (plugin.id !== action.parentId) { + return plugin; + } + return { + ...plugin, + value: plugin.value.map(pluginValue => { + if (pluginValue.name !== 'args') { + return pluginValue; + } + return { + ...pluginValue, + value: updateInput(pluginValue.value, action.id, action.changes), + }; + }), + }; + }), + }; + }); + + return newConfig; + }; + + return self; +}; \ No newline at end of file diff --git a/framework/elsa/fit-elsa-react/src/components/util/ReferenceUtil.js b/framework/elsa/fit-elsa-react/src/components/util/ReferenceUtil.js index fd175bcd5..d9bb716a7 100644 --- a/framework/elsa/fit-elsa-react/src/components/util/ReferenceUtil.js +++ b/framework/elsa/fit-elsa-react/src/components/util/ReferenceUtil.js @@ -23,4 +23,22 @@ export const getDefaultReference = (id) => { value: [], editable: true, }; +}; + +/** + * 对Reference中Object结构进行递归调用。 + * + * @param params 传入的数组。 + * @param parent 父元素。 + * @param action 具体递归操作方法。 + */ +export const recursive = (params, parent, action) => { + params.forEach(p => { + if (p.type === 'Object') { + recursive(p.value, p, action); + action(p, parent); + } else { + action(p, parent); + } + }); }; \ No newline at end of file diff --git a/framework/elsa/fit-elsa-react/src/flow/compatibility/compatibilityProcessors.js b/framework/elsa/fit-elsa-react/src/flow/compatibility/compatibilityProcessors.js index 85b1e1196..5e71e956b 100644 --- a/framework/elsa/fit-elsa-react/src/flow/compatibility/compatibilityProcessors.js +++ b/framework/elsa/fit-elsa-react/src/flow/compatibility/compatibilityProcessors.js @@ -12,7 +12,7 @@ import { DEFAULT_KNOWLEDGE_RETRIEVAL_NODE_USER_ID, DEFAULT_LLM_KNOWLEDGE_BASES, DEFAULT_LLM_REFERENCE_OUTPUT, - DEFAULT_LOOP_NODE_CONTEXT, + DEFAULT_ADD_TOOL_NODE_CONTEXT, DEFAULT_MAX_MEMORY_ROUNDS, END_NODE_TYPE, FLOW_TYPE, @@ -206,7 +206,7 @@ export const loopNodeCompatibilityProcessor = (shapeData, graph, pageHandler) => const jober = self.shapeData.flowMeta.jober; if (!jober.entity.params.exist(param => param.name === 'context')) { jober.entity.params.push({name: 'context'}); - jober.converter.entity.inputParams.push(DEFAULT_LOOP_NODE_CONTEXT); + jober.converter.entity.inputParams.push(DEFAULT_ADD_TOOL_NODE_CONTEXT); } }; diff --git a/framework/elsa/fit-elsa-react/src/flow/jadeFlowEntry.jsx b/framework/elsa/fit-elsa-react/src/flow/jadeFlowEntry.jsx index 90a29986c..ca053ea5a 100644 --- a/framework/elsa/fit-elsa-react/src/flow/jadeFlowEntry.jsx +++ b/framework/elsa/fit-elsa-react/src/flow/jadeFlowEntry.jsx @@ -287,6 +287,15 @@ const jadeFlowAgent = (graph) => { addSelectEventListener('SELECT_LOOP_PLUGIN', callback); }; + /** + * 并行节点选择框中选择了某些工具、工具流. + * + * @param callback 回调函数. + */ + self.onParallelSelect = (callback) => { + addSelectEventListener('SELECT_PARALLEL_PLUGINS', callback); + }; + /** * 当需要触发搜索参数配置时的回调. * diff --git a/framework/elsa/fit-elsa-react/src/flow/jadeFlowGraph.js b/framework/elsa/fit-elsa-react/src/flow/jadeFlowGraph.js index 4c9c7562d..4ac308d19 100644 --- a/framework/elsa/fit-elsa-react/src/flow/jadeFlowGraph.js +++ b/framework/elsa/fit-elsa-react/src/flow/jadeFlowGraph.js @@ -57,6 +57,8 @@ import {loopNodeState} from '@/components/loopNode/loopNodeState.jsx'; import {loopComponent} from '@/components/loopNode/loopComponent.jsx'; import {intelligentFormNodeState} from '@/components/intelligentForm/intelligentFormNodeState.jsx'; import {intelligentFormComponent} from '@/components/intelligentForm/intelligentFormComponent.jsx'; +import {parallelNodeState} from '@/components/parallelNode/parallelNodeState.jsx'; +import {parallelComponent} from '@/components/parallelNode/parallelComponent.jsx'; /** * jadeFlow的专用画布. @@ -161,6 +163,8 @@ export const jadeFlowGraph = (div, title) => { self.registerPlugin('loopComponent', loopComponent); self.registerPlugin('intelligentFormNodeState', intelligentFormNodeState); self.registerPlugin('intelligentFormComponent', intelligentFormComponent); + self.registerPlugin('parallelNodeState', parallelNodeState); + self.registerPlugin('parallelComponent', parallelComponent); return initialize.apply(self); }; diff --git a/framework/elsa/fit-elsa-react/src/i18n/en_US.json b/framework/elsa/fit-elsa-react/src/i18n/en_US.json index 9cc2d10a3..56985ae24 100644 --- a/framework/elsa/fit-elsa-react/src/i18n/en_US.json +++ b/framework/elsa/fit-elsa-react/src/i18n/en_US.json @@ -411,5 +411,8 @@ "appConfig": "App Configuration", "appChatStyle": "App Interface Configuration", "appChatStyleCannotBeEmpty": "App interface configuration cannot be empty", - "formItemFieldTypeCannotBeEmpty": "Field type is required" + "formItemFieldTypeCannotBeEmpty": "Field type is required", + "addParallelTask": "Add Parallel Tasks", + "parameterDescription": "Parameter Description: ", + "parallelNode": "Parallel Node" } diff --git a/framework/elsa/fit-elsa-react/src/i18n/zh_CN.json b/framework/elsa/fit-elsa-react/src/i18n/zh_CN.json index 83a3ae190..8b255540e 100644 --- a/framework/elsa/fit-elsa-react/src/i18n/zh_CN.json +++ b/framework/elsa/fit-elsa-react/src/i18n/zh_CN.json @@ -704,5 +704,8 @@ "appConfig": "应用配置", "appChatStyle": "应用界面配置", "appChatStyleCannotBeEmpty": "应用界面配置不能为空", - "formItemFieldTypeCannotBeEmpty": "表单项类型不能为空" + "formItemFieldTypeCannotBeEmpty": "表单项类型不能为空", + "addParallelTask": "添加并行任务", + "parameterDescription": "参数介绍:", + "parallelNode": "并行节点" }