fix(web): node's variable update

This commit is contained in:
zhaoying
2026-01-13 21:17:18 +08:00
parent 1159da111a
commit 954d754c09
11 changed files with 569 additions and 204 deletions

View File

@@ -107,7 +107,7 @@ const AddNode: ReactShapeConfig['component'] = ({ node, graph }) => {
<div style={{ maxHeight: '300px', overflowY: 'auto', minWidth: '240px' }}> <div style={{ maxHeight: '300px', overflowY: 'auto', minWidth: '240px' }}>
{nodeLibrary.map((category, categoryIndex) => { {nodeLibrary.map((category, categoryIndex) => {
const filteredNodes = category.nodes.filter(nodeType => const filteredNodes = category.nodes.filter(nodeType =>
nodeType.type !== 'start' && nodeType.type !== 'end' && nodeType.type !== 'loop' && nodeType.type !== 'cycle-start' nodeType.type !== 'start' && nodeType.type !== 'end' && nodeType.type !== 'iteration' && nodeType.type !== 'loop' && nodeType.type !== 'cycle-start'
); );
if (filteredNodes.length === 0) return null; if (filteredNodes.length === 0) return null;

View File

@@ -61,7 +61,7 @@ const LoopNode: ReactShapeConfig['component'] = ({ node, graph }) => {
}, },
}, },
}, },
zIndex: 3 zIndex: 10
}); });
} }
} }
@@ -128,7 +128,7 @@ const LoopNode: ReactShapeConfig['component'] = ({ node, graph }) => {
}, },
}, },
}, },
zIndex: 3 zIndex: 10
} }
graph.addEdge(edgeConfig) graph.addEdge(edgeConfig)

View File

@@ -151,11 +151,11 @@ const PortClickHandler: React.FC<PortClickHandlerProps> = ({ graph }) => {
let filteredNodes; let filteredNodes;
if (isChildOfLoop) { if (isChildOfLoop) {
// Use same filtering as AddNode for child nodes of loop // Use same filtering as AddNode for child nodes of loop, but allow break
filteredNodes = category.nodes.filter(nodeType => !['start', 'end', 'loop', 'cycle-start', 'iteration'].includes(nodeType.type)); filteredNodes = category.nodes.filter(nodeType => !['start', 'end', 'loop', 'cycle-start', 'iteration'].includes(nodeType.type));
} else if (isChildOfIteration) { } else if (isChildOfIteration) {
// Filter out loop and iteration nodes for children of iteration nodes // Filter out loop and iteration nodes for children of iteration nodes, but allow break
filteredNodes = category.nodes.filter(nodeType => !['start', 'end', 'loop', 'break', 'cycle-start', 'iteration'].includes(nodeType.type)); filteredNodes = category.nodes.filter(nodeType => !['start', 'end', 'loop', 'cycle-start', 'iteration'].includes(nodeType.type));
} else { } else {
// Original filtering for non-loop child nodes // Original filtering for non-loop child nodes
filteredNodes = category.nodes.filter(nodeType => !['start', 'end', 'break', 'cycle-start'].includes(nodeType.type)); filteredNodes = category.nodes.filter(nodeType => !['start', 'end', 'break', 'cycle-start'].includes(nodeType.type));

View File

@@ -60,7 +60,7 @@ const AssignmentList: FC<AssignmentListProps> = ({
> >
<VariableSelect <VariableSelect
placeholder={t('common.pleaseSelect')} placeholder={t('common.pleaseSelect')}
options={options} options={options.filter(vo => vo.nodeData.type === 'loop' || vo.value.includes('conv.'))}
popupMatchSelectWidth={false} popupMatchSelectWidth={false}
onChange={() => { onChange={() => {
form.setFieldValue([parentName, name, 'operation'], undefined); form.setFieldValue([parentName, name, 'operation'], undefined);

View File

@@ -1,17 +1,19 @@
import { type FC } from 'react'; import { type FC } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Input, Button, Form, Space } from 'antd'; import { Button, Form, Space } from 'antd';
import { PlusOutlined, CopyOutlined, DeleteOutlined, ExpandOutlined } from '@ant-design/icons'; import { DeleteOutlined } from '@ant-design/icons';
import { Graph, Node } from '@antv/x6'; import { Graph, Node } from '@antv/x6';
import type { PortMetadata } from '@antv/x6/lib/model/port'; import Editor from '../../Editor';
import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin'
interface CategoryListProps { interface CategoryListProps {
parentName: string; parentName: string;
options: Suggestion[];
selectedNode?: Node | null; selectedNode?: Node | null;
graphRef?: React.MutableRefObject<Graph | undefined>; graphRef?: React.MutableRefObject<Graph | undefined>;
} }
const CategoryList: FC<CategoryListProps> = ({ parentName, selectedNode, graphRef }) => { const CategoryList: FC<CategoryListProps> = ({ parentName, selectedNode, graphRef, options }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const form = Form.useFormInstance(); const form = Form.useFormInstance();
const formValues = Form.useWatch([parentName], form); const formValues = Form.useWatch([parentName], form);
@@ -167,9 +169,9 @@ const CategoryList: FC<CategoryListProps> = ({ parentName, selectedNode, graphRe
name={[name, 'class_name']} name={[name, 'class_name']}
noStyle noStyle
> >
<Input.TextArea <Editor
placeholder={t('common.pleaseEnter')} placeholder={t('common.pleaseEnter')}
rows={2} options={options}
/> />
</Form.Item> </Form.Item>
</div> </div>

View File

@@ -1,6 +1,6 @@
import { type FC } from 'react' import { type FC } from 'react'
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Form, Button, Select, Row, Col, InputNumber, Radio, type SelectProps } from 'antd' import { Form, Button, Select, Row, Col, InputNumber, Radio, Input, type SelectProps } from 'antd'
import { DeleteOutlined } from '@ant-design/icons'; import { DeleteOutlined } from '@ant-design/icons';
import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin' import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin'
@@ -114,7 +114,7 @@ const ConditionList: FC<CaseListProps> = ({
<Col span={14}> <Col span={14}>
<Form.Item name={[field.name, 'left']} noStyle> <Form.Item name={[field.name, 'left']} noStyle>
<VariableSelect <VariableSelect
options={options} options={options.filter(vo => vo.value.includes('sys.') || vo.value.includes('conv.') || vo.nodeData.type === 'loop')}
size="small" size="small"
allowClear={false} allowClear={false}
popupMatchSelectWidth={false} popupMatchSelectWidth={false}
@@ -186,7 +186,7 @@ const ConditionList: FC<CaseListProps> = ({
<Radio.Button value={true}>True</Radio.Button> <Radio.Button value={true}>True</Radio.Button>
<Radio.Button value={false}>False</Radio.Button> <Radio.Button value={false}>False</Radio.Button>
</Radio.Group> </Radio.Group>
: <Editor options={options} /> : <Input placeholder={t('common.pleaseEnter')} />
} }
</Form.Item> </Form.Item>
</Col> </Col>

View File

@@ -1,6 +1,6 @@
import { type FC } from 'react' import { type FC } from 'react'
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Form, Button, Select, Row, Col, Input } from 'antd' import { Form, Select, Row, Col, Input } from 'antd'
import { DeleteOutlined, PlusOutlined } from '@ant-design/icons'; import { DeleteOutlined, PlusOutlined } from '@ant-design/icons';
import VariableSelect from '../VariableSelect' import VariableSelect from '../VariableSelect'
@@ -36,7 +36,6 @@ const CycleVarsList: FC<CycleVarsListProps> = ({
value = [], value = [],
options, options,
parentName, parentName,
onChange,
selectedNode, selectedNode,
graphRef graphRef
}) => { }) => {
@@ -139,12 +138,17 @@ const CycleVarsList: FC<CycleVarsListProps> = ({
<Form.Item name={[name, 'value']} noStyle> <Form.Item name={[name, 'value']} noStyle>
{currentInputType === 'variable' ? ( {currentInputType === 'variable' ? (
<VariableSelect <VariableSelect
placeholder="选择变量" placeholder={t('common.pleaseSelect')}
options={availableOptions} options={availableOptions.filter(option => {
const currentType = value?.[index]?.type;
if (!currentType) return true;
return option.dataType === currentType
})}
/> />
) : ( ) : (
<Input.TextArea <Input.TextArea
placeholder="输入值" placeholder={t('common.pleaseEnter')}
rows={3} rows={3}
className="rb:w-full" className="rb:w-full"
/> />

View File

@@ -18,8 +18,22 @@ const GroupVariableList: FC<GroupVariableListProps> = ({
isCanAdd = false isCanAdd = false
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const form = Form.useFormInstance();
const value = form.getFieldValue(name) || [];
console.log('GroupVariableList', value)
if (!isCanAdd) { if (!isCanAdd) {
// Filter options based on first variable's dataType if value exists
let filteredOptions = options;
if (value.length > 0) {
const firstVariableValue = value[0];
const firstVariable = options.find(opt => `{{${opt.value}}}` === firstVariableValue);
if (firstVariable) {
filteredOptions = options.filter(opt => opt.dataType === firstVariable.dataType);
}
}
return ( return (
<div className="rb:mb-4"> <div className="rb:mb-4">
<Row gutter={12} className="rb:mb-2!"> <Row gutter={12} className="rb:mb-2!">
@@ -38,7 +52,7 @@ const GroupVariableList: FC<GroupVariableListProps> = ({
> >
<VariableSelect <VariableSelect
placeholder={t('common.pleaseSelect')} placeholder={t('common.pleaseSelect')}
options={options} options={filteredOptions}
mode="multiple" mode="multiple"
/> />
</Form.Item> </Form.Item>
@@ -77,7 +91,18 @@ const GroupVariableList: FC<GroupVariableListProps> = ({
> >
<VariableSelect <VariableSelect
placeholder={t('common.pleaseSelect')} placeholder={t('common.pleaseSelect')}
options={options} options={(() => {
const currentGroupValue = value[name]?.value || [];
if (currentGroupValue.length > 0) {
const firstVariableValue = currentGroupValue[0];
const firstVariable = options.find(opt => `{{${opt.value}}}` === firstVariableValue);
if (firstVariable) {
return options.filter(opt => opt.dataType === firstVariable.dataType);
}
}
return options;
})()
}
mode="multiple" mode="multiple"
/> />
</Form.Item> </Form.Item>

View File

@@ -90,7 +90,7 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an
</Col> </Col>
<Col span={16}> <Col span={16}>
<Form.Item name="url"> <Form.Item name="url">
<Editor options={options} variant="outlined" /> <Editor options={options.filter(vo => vo.dataType === 'string' || vo.dataType === 'number')} variant="outlined" />
</Form.Item> </Form.Item>
</Col> </Col>
</Row> </Row>
@@ -144,7 +144,7 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an
<Form.Item name={['body', 'data']} noStyle> <Form.Item name={['body', 'data']} noStyle>
<EditableTable <EditableTable
parentName={['body', 'data']} parentName={['body', 'data']}
options={options} options={options.filter(vo => vo.dataType === 'string' || vo.dataType === 'number')}
filterBooleanType={true} filterBooleanType={true}
/> />
</Form.Item> </Form.Item>
@@ -154,7 +154,7 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an
<MessageEditor <MessageEditor
key="json" key="json"
parentName={['body', 'data']} parentName={['body', 'data']}
options={options} options={options.filter(vo => vo.dataType === 'string' || vo.dataType === 'number')}
isArray={false} isArray={false}
title="JSON" title="JSON"
/> />

View File

@@ -22,6 +22,7 @@ import ConditionList from './ConditionList'
import CycleVarsList from './CycleVarsList' import CycleVarsList from './CycleVarsList'
import AssignmentList from './AssignmentList' import AssignmentList from './AssignmentList'
import ToolConfig from './ToolConfig' import ToolConfig from './ToolConfig'
import { calculateVariableList } from './utils/variableListCalculator'
interface PropertiesProps { interface PropertiesProps {
selectedNode?: Node | null; selectedNode?: Node | null;
@@ -338,112 +339,35 @@ const Properties: FC<PropertiesProps> = ({
const parentLoopNode = getParentLoopNode(selectedNode.id); const parentLoopNode = getParentLoopNode(selectedNode.id);
console.log('childNodeIds', selectedNode, childNodeIds) console.log('childNodeIds', selectedNode, childNodeIds)
const allRelevantNodeIds = [...allPreviousNodeIds, ...childNodeIds]; let allRelevantNodeIds = [...allPreviousNodeIds, ...childNodeIds];
// Add parent loop/iteration node variables if current node is a child // Add variables from nodes preceding the parent loop/iteration node if current node is a child
if (parentLoopNode) { if (parentLoopNode) {
const parentData = parentLoopNode.getData();
const parentNodeId = parentLoopNode.getData().id;
if (parentData.type === 'loop') {
const cycleVars = parentData.cycle_vars || [];
cycleVars.forEach((cycleVar: any) => {
const key = `${parentNodeId}_cycle_${cycleVar.name}`;
if (!addedKeys.has(key)) {
addedKeys.add(key);
variableList.push({
key,
label: cycleVar.name,
type: 'variable',
dataType: cycleVar.type || 'String',
value: `${parentNodeId}.${cycleVar.name}`,
nodeData: parentData,
});
}
});
} else if (parentData.type === 'iteration') {
// Add item and index variables for iteration parent
const itemKey = `${parentNodeId}_item`;
const indexKey = `${parentNodeId}_index`;
if (!addedKeys.has(itemKey)) {
addedKeys.add(itemKey);
variableList.push({
key: itemKey,
label: 'item',
type: 'variable',
dataType: 'Object',
value: `${parentNodeId}.item`,
nodeData: parentData,
});
}
if (!addedKeys.has(indexKey)) {
addedKeys.add(indexKey);
variableList.push({
key: indexKey,
label: 'index',
type: 'variable',
dataType: 'Number',
value: `${parentNodeId}.index`,
nodeData: parentData,
});
}
}
// Check if parent loop/iteration is connected to http-request via ERROR connection
if (parentData.type === 'loop' || parentData.type === 'iteration') {
const parentPreviousNodeIds = getAllPreviousNodes(parentLoopNode.id);
parentPreviousNodeIds.forEach(prevNodeId => {
const prevNode = nodes.find(n => n.id === prevNodeId);
if (!prevNode) return;
const prevNodeData = prevNode.getData();
if (prevNodeData.type === 'http-request') {
// Check if connected via ERROR connection point
const errorEdges = edges.filter(edge => {
return edge.getTargetCellId() === parentLoopNode.id &&
edge.getSourceCellId() === prevNodeId &&
edge.getSourcePortId() === 'ERROR'
});
if (errorEdges.length > 0) {
const errorMessageKey = `${prevNodeData.id}_error_message`;
const errorTypeKey = `${prevNodeData.id}_error_type`;
if (!addedKeys.has(errorMessageKey)) {
addedKeys.add(errorMessageKey);
variableList.push({
key: errorMessageKey,
label: 'error_message',
type: 'variable',
dataType: 'string',
value: `${prevNodeData.id}.error_message`,
nodeData: prevNodeData,
});
}
if (!addedKeys.has(errorTypeKey)) {
addedKeys.add(errorTypeKey);
variableList.push({
key: errorTypeKey,
label: 'error_type',
type: 'variable',
dataType: 'string',
value: `${prevNodeData.id}.error_type`,
nodeData: prevNodeData,
});
}
}
}
});
}
// Add variables from nodes preceding the parent loop/iteration node
const parentPreviousNodeIds = getAllPreviousNodes(parentLoopNode.id); const parentPreviousNodeIds = getAllPreviousNodes(parentLoopNode.id);
allRelevantNodeIds.push(...parentPreviousNodeIds); allRelevantNodeIds.push(...parentPreviousNodeIds);
} }
// Add conversation variables from global config
const conversationVariables = workflowConfig?.variables || [];
conversationVariables.forEach((variable: any) => {
const key = `CONVERSATION_${variable.name}`;
if (!addedKeys.has(key)) {
addedKeys.add(key);
variableList.push({
key,
label: variable.name,
type: 'variable',
dataType: variable.type,
value: `conv.${variable.name}`,
nodeData: { type: 'CONVERSATION', name: 'CONVERSATION', icon: '' },
group: 'CONVERSATION'
});
}
});
allRelevantNodeIds.forEach(nodeId => { allRelevantNodeIds.forEach(nodeId => {
const node = nodes.find(n => n.id === nodeId); const node = nodes.find(n => n.id === nodeId);
if (!node) return; if (!node) return;
@@ -496,7 +420,7 @@ const Properties: FC<PropertiesProps> = ({
key: llmKey, key: llmKey,
label: 'output', label: 'output',
type: 'variable', type: 'variable',
dataType: 'String', dataType: 'string',
value: `${dataNodeId}.output`, value: `${dataNodeId}.output`,
nodeData: nodeData, nodeData: nodeData,
}); });
@@ -565,6 +489,17 @@ const Properties: FC<PropertiesProps> = ({
const groupVariables = nodeData.config.group_variables.defaultValue || []; const groupVariables = nodeData.config.group_variables.defaultValue || [];
groupVariables?.forEach((groupVar: any) => { groupVariables?.forEach((groupVar: any) => {
if (!groupVar || !groupVar.key) return; if (!groupVar || !groupVar.key) return;
// Determine dataType from first variable in the group
let groupDataType = 'string';
if (groupVar.value && Array.isArray(groupVar.value) && groupVar.value.length > 0) {
const firstVariableValue = groupVar.value[0];
const firstVariable = variableList.find(v => `{{${v.value}}}` === firstVariableValue);
if (firstVariable) {
groupDataType = firstVariable.dataType;
}
}
const groupVarKey = `${dataNodeId}_${groupVar.key}`; const groupVarKey = `${dataNodeId}_${groupVar.key}`;
if (!addedKeys.has(groupVarKey)) { if (!addedKeys.has(groupVarKey)) {
addedKeys.add(groupVarKey); addedKeys.add(groupVarKey);
@@ -572,14 +507,26 @@ const Properties: FC<PropertiesProps> = ({
key: groupVarKey, key: groupVarKey,
label: groupVar.key, label: groupVar.key,
type: 'variable', type: 'variable',
dataType: 'string', dataType: groupDataType,
value: `${dataNodeId}.${groupVar.key}`, value: `${dataNodeId}.${groupVar.key}`,
nodeData: nodeData, nodeData: nodeData,
}); });
} }
}); });
} else { } else {
// If group=false, add output variable // If group=false, add output variable with type from first group_variable
const groupVariables = nodeData.config.group_variables.defaultValue || [];
const firstVariable = groupVariables[0];
let outputDataType: string = 'any';
if (firstVariable) {
const filterVo = [...variableList].find(v => {
return `{{${v.value}}}` === firstVariable
})
if (filterVo) {
outputDataType = filterVo?.dataType
}
}
const varAggregatorKey = `${dataNodeId}_output`; const varAggregatorKey = `${dataNodeId}_output`;
if (!addedKeys.has(varAggregatorKey)) { if (!addedKeys.has(varAggregatorKey)) {
addedKeys.add(varAggregatorKey); addedKeys.add(varAggregatorKey);
@@ -587,7 +534,7 @@ const Properties: FC<PropertiesProps> = ({
key: varAggregatorKey, key: varAggregatorKey,
label: 'output', label: 'output',
type: 'variable', type: 'variable',
dataType: 'string', dataType: outputDataType,
value: `${dataNodeId}.output`, value: `${dataNodeId}.output`,
nodeData: nodeData, nodeData: nodeData,
}); });
@@ -684,21 +631,20 @@ const Properties: FC<PropertiesProps> = ({
nodeData: nodeData, nodeData: nodeData,
}); });
} }
if (!addedKeys.has(outputKey)) { // if (!addedKeys.has(outputKey)) {
addedKeys.add(outputKey); // addedKeys.add(outputKey);
variableList.push({ // variableList.push({
key: outputKey, // key: outputKey,
label: 'output', // label: 'output',
type: 'variable', // type: 'variable',
dataType: 'string', // dataType: 'string',
value: `${dataNodeId}.output`, // value: `${dataNodeId}.output`,
nodeData: nodeData, // nodeData: nodeData,
}); // });
} // }
break break
case 'iteration': case 'iteration':
const iterationOutputKey = `${dataNodeId}_output`; const iterationOutputKey = `${dataNodeId}_output`;
const iterationItemKey = `${dataNodeId}_item`;
if (!addedKeys.has(iterationOutputKey)) { if (!addedKeys.has(iterationOutputKey)) {
addedKeys.add(iterationOutputKey); addedKeys.add(iterationOutputKey);
// Get the data type from the output configuration, default to string // Get the data type from the output configuration, default to string
@@ -715,22 +661,11 @@ const Properties: FC<PropertiesProps> = ({
key: iterationOutputKey, key: iterationOutputKey,
label: 'output', label: 'output',
type: 'variable', type: 'variable',
dataType: outputDataType, dataType: `array[${outputDataType}]`,
value: `${dataNodeId}.output`, value: `${dataNodeId}.output`,
nodeData: nodeData, nodeData: nodeData,
}); });
} }
if (!addedKeys.has(iterationItemKey)) {
addedKeys.add(iterationItemKey);
variableList.push({
key: iterationItemKey,
label: 'item',
type: 'variable',
dataType: 'string',
value: `${dataNodeId}.item`,
nodeData: nodeData,
});
}
break break
case 'loop': case 'loop':
const cycleVars = nodeData.config.cycle_vars.defaultValue || []; const cycleVars = nodeData.config.cycle_vars.defaultValue || [];
@@ -760,47 +695,337 @@ const Properties: FC<PropertiesProps> = ({
key: toolDataKey, key: toolDataKey,
label: 'data', label: 'data',
type: 'variable', type: 'variable',
dataType: 'object', dataType: 'string',
value: `${dataNodeId}.data`, value: `${dataNodeId}.data`,
nodeData: nodeData, nodeData: nodeData,
}); });
} }
break break
case 'memory-read':
const memoryReadAnswerKey = `${dataNodeId}_answer`;
const memoryReadIntermediateOutputs = `${dataNodeId}_intermediate_outputs`;
if (!addedKeys.has(memoryReadAnswerKey)) {
addedKeys.add(memoryReadAnswerKey);
variableList.push({
key: memoryReadAnswerKey,
label: 'answer',
type: 'variable',
dataType: 'string',
value: `${dataNodeId}.answer`,
nodeData: nodeData,
});
}
if (!addedKeys.has(memoryReadIntermediateOutputs)) {
addedKeys.add(memoryReadIntermediateOutputs);
variableList.push({
key: memoryReadIntermediateOutputs,
label: 'intermediate_outputs',
type: 'variable',
dataType: 'array[object]',
value: `${dataNodeId}.intermediate_outputs`,
nodeData: nodeData,
});
}
break
} }
}); });
// Add conversation variables from global config
const conversationVariables = workflowConfig?.variables || [];
conversationVariables.forEach((variable: any) => { // Add parent loop/iteration node variables if current node is a child
const key = `CONVERSATION_${variable.name}`; if (parentLoopNode) {
if (!addedKeys.has(key)) { const parentData = parentLoopNode.getData();
addedKeys.add(key); const parentNodeId = parentLoopNode.getData().id;
variableList.push({
key, if (parentData.type === 'loop') {
label: variable.name, const cycleVars = parentData.cycle_vars || [];
type: 'variable', cycleVars.forEach((cycleVar: any) => {
dataType: variable.type, const key = `${parentNodeId}_cycle_${cycleVar.name}`;
value: `conv.${variable.name}`, if (!addedKeys.has(key)) {
nodeData: { type: 'CONVERSATION', name: 'CONVERSATION', icon: '' }, addedKeys.add(key);
group: 'CONVERSATION' variableList.push({
key,
label: cycleVar.name,
type: 'variable',
dataType: cycleVar.type || 'String',
value: `${parentNodeId}.${cycleVar.name}`,
nodeData: parentData,
});
}
});
} else if (parentData.type === 'iteration') {
// Add item and index variables for iteration parent only if input has value
if (parentData.config.input.defaultValue) {
const itemKey = `${parentNodeId}_item`;
const indexKey = `${parentNodeId}_index`;
// Determine item dataType from input variable
let itemDataType = 'object';
const inputVariable = variableList.find(v => `{{${v.value}}}` === parentData.config.input.defaultValue);
console.log('itemDataType defaultValue', parentData.config.input.defaultValue, variableList, inputVariable)
if (inputVariable && inputVariable.dataType.startsWith('array[')) {
itemDataType = inputVariable.dataType.replace(/^array\[(.+)\]$/, '$1');
console.log('itemDataType', itemDataType)
}
if (!addedKeys.has(itemKey)) {
addedKeys.add(itemKey);
variableList.push({
key: itemKey,
label: 'item',
type: 'variable',
dataType: itemDataType,
value: `${parentNodeId}.item`,
nodeData: parentData,
});
}
if (!addedKeys.has(indexKey)) {
addedKeys.add(indexKey);
variableList.push({
key: indexKey,
label: 'index',
type: 'variable',
dataType: 'number',
value: `${parentNodeId}.index`,
nodeData: parentData,
});
}
}
}
// Check if parent loop/iteration is connected to http-request via ERROR connection
if (parentData.type === 'loop' || parentData.type === 'iteration') {
const parentPreviousNodeIds = getAllPreviousNodes(parentLoopNode.id);
parentPreviousNodeIds.forEach(prevNodeId => {
const prevNode = nodes.find(n => n.id === prevNodeId);
if (!prevNode) return;
const prevNodeData = prevNode.getData();
if (prevNodeData.type === 'http-request') {
// Check if connected via ERROR connection point
const errorEdges = edges.filter(edge => {
return edge.getTargetCellId() === parentLoopNode.id &&
edge.getSourceCellId() === prevNodeId &&
edge.getSourcePortId() === 'ERROR'
});
if (errorEdges.length > 0) {
const errorMessageKey = `${prevNodeData.id}_error_message`;
const errorTypeKey = `${prevNodeData.id}_error_type`;
if (!addedKeys.has(errorMessageKey)) {
addedKeys.add(errorMessageKey);
variableList.push({
key: errorMessageKey,
label: 'error_message',
type: 'variable',
dataType: 'string',
value: `${prevNodeData.id}.error_message`,
nodeData: prevNodeData,
});
}
if (!addedKeys.has(errorTypeKey)) {
addedKeys.add(errorTypeKey);
variableList.push({
key: errorTypeKey,
label: 'error_type',
type: 'variable',
dataType: 'string',
value: `${prevNodeData.id}.error_type`,
nodeData: prevNodeData,
});
}
}
}
}); });
} }
}); }
return variableList; return variableList;
}, [selectedNode, graphRef, workflowConfig?.variables]); }, [selectedNode, graphRef, workflowConfig?.variables]);
// Filter out boolean type variables for loop and llm nodes // Filter out boolean type variables for loop and llm nodes
const getFilteredVariableList = (nodeType?: string) => { const getFilteredVariableList = (nodeType?: string, key?: string) => {
if (nodeType === 'loop' || nodeType === 'llm') { // Check if current node is a child of iteration node
return variableList.filter(variable => variable.dataType !== 'boolean'); const parentIterationNode = selectedNode ? (() => {
const nodes = graphRef.current?.getNodes() || [];
const nodeData = selectedNode.getData();
const cycle = nodeData?.cycle;
if (cycle) {
const parentNode = nodes.find(n => n.getData().id === cycle);
if (parentNode) {
const parentData = parentNode.getData();
if (parentData?.type === 'iteration') {
return parentNode;
}
}
}
return null;
})() : null;
// Helper function to add parent iteration variables
const addParentIterationVars = (filteredList: any[]) => {
if (parentIterationNode) {
const parentData = parentIterationNode.getData();
const parentNodeId = parentData.id;
if (parentData.config?.input?.defaultValue) {
const itemKey = `${parentNodeId}_item`;
const indexKey = `${parentNodeId}_index`;
const existingItemVar = filteredList.find(v => v.key === itemKey);
const existingIndexVar = filteredList.find(v => v.key === indexKey);
if (!existingItemVar) {
// Determine item dataType from input variable
let itemDataType = 'object';
const inputVariable = variableList.find(v => `{{${v.value}}}` === parentData.config.input.defaultValue);
if (inputVariable && inputVariable.dataType.startsWith('array[')) {
itemDataType = inputVariable.dataType.replace(/^array\[(.+)\]$/, '$1');
}
filteredList.push({
key: itemKey,
label: 'item',
type: 'variable',
dataType: itemDataType,
value: `${parentNodeId}.item`,
nodeData: parentData,
});
}
if (!existingIndexVar) {
filteredList.push({
key: indexKey,
label: 'index',
type: 'variable',
dataType: 'number',
value: `${parentNodeId}.index`,
nodeData: parentData,
});
}
}
}
return filteredList;
};
if (nodeType === 'llm') {
// For LLM nodes that are children of iteration or loop nodes, include parent variables
const parentLoopNode = selectedNode ? (() => {
const nodes = graphRef.current?.getNodes() || [];
const nodeData = selectedNode.getData();
const cycle = nodeData?.cycle;
if (cycle) {
const parentNode = nodes.find(n => n.getData().id === cycle);
if (parentNode) {
const parentData = parentNode.getData();
if (parentData?.type === 'loop' || parentData?.type === 'iteration') {
return parentNode;
}
}
}
return null;
})() : null;
let filteredList = variableList.filter(variable => variable.dataType !== 'boolean');
// If this LLM node is a child of iteration/loop, ensure parent variables are included
if (parentLoopNode) {
const parentData = parentLoopNode.getData();
const parentNodeId = parentData.id;
// Ensure parent loop/iteration variables are included
if (parentData.type === 'loop') {
const cycleVars = parentData.cycle_vars || [];
cycleVars.forEach((cycleVar: any) => {
const key = `${parentNodeId}_cycle_${cycleVar.name}`;
const existingVar = filteredList.find(v => v.key === key);
if (!existingVar && cycleVar.name && cycleVar.type !== 'boolean') {
filteredList.push({
key,
label: cycleVar.name,
type: 'variable',
dataType: cycleVar.type || 'String',
value: `${parentNodeId}.${cycleVar.name}`,
nodeData: parentData,
});
}
});
} else if (parentData.type === 'iteration') {
// Add item and index variables for iteration parent
if (parentData.config?.input?.defaultValue) {
const itemKey = `${parentNodeId}_item`;
const indexKey = `${parentNodeId}_index`;
const existingItemVar = filteredList.find(v => v.key === itemKey);
const existingIndexVar = filteredList.find(v => v.key === indexKey);
if (!existingItemVar) {
// Determine item dataType from input variable
let itemDataType = 'object';
const inputVariable = variableList.find(v => `{{${v.value}}}` === parentData.config.input.defaultValue);
if (inputVariable && inputVariable.dataType.startsWith('array[')) {
itemDataType = inputVariable.dataType.replace(/^array\[(.+)\]$/, '$1');
}
filteredList.push({
key: itemKey,
label: 'item',
type: 'variable',
dataType: itemDataType,
value: `${parentNodeId}.item`,
nodeData: parentData,
});
}
if (!existingIndexVar) {
filteredList.push({
key: indexKey,
label: 'index',
type: 'variable',
dataType: 'Number',
value: `${parentNodeId}.index`,
nodeData: parentData,
});
}
}
}
}
return filteredList;
} }
return variableList; if (nodeType === 'knowledge-retrieval' || nodeType === 'parameter-extractor' && key !== 'prompt' || nodeType === 'memory-read' || nodeType === 'memory-write' || nodeType === 'question-classifier') {
let filteredList = variableList.filter(variable => variable.dataType === 'string');
return addParentIterationVars(filteredList);
}
if (nodeType === 'parameter-extractor' && key === 'prompt') {
let filteredList = variableList.filter(variable => variable.dataType === 'string' || variable.dataType === 'number');
return addParentIterationVars(filteredList);
}
if (nodeType === 'iteration' && key === 'output') {
return variableList.filter(variable => variable.value.includes('sys.'));
}
if (nodeType === 'iteration') {
return variableList.filter(variable => variable.dataType.includes('array'));
}
if (nodeType === 'loop' && key === 'condition') {
let filteredList = variableList.filter(variable => variable.nodeData.type !== 'loop');
return addParentIterationVars(filteredList);
}
// For all other node types, add parent iteration variables if applicable
let baseList = variableList;
return addParentIterationVars(baseList);
}; };
const defaultVariableList = calculateVariableList(selectedNode as Node, graphRef, workflowConfig )
console.log('values', values) console.log('values', values)
console.log('variableList', variableList, selectedNode?.data) console.log('variableList', variableList, defaultVariableList)
return ( return (
<div className="rb:w-75 rb:fixed rb:right-0 rb:top-16 rb:bottom-0 rb:p-3"> <div className="rb:w-75 rb:fixed rb:right-0 rb:top-16 rb:bottom-0 rb:p-3">
@@ -901,7 +1126,6 @@ const Properties: FC<PropertiesProps> = ({
}); });
} }
} }
return ( return (
<Form.Item key={key} name={key}> <Form.Item key={key} name={key}>
<MessageEditor <MessageEditor
@@ -915,7 +1139,12 @@ const Properties: FC<PropertiesProps> = ({
if (selectedNode?.data?.type === 'end' && key === 'output') { if (selectedNode?.data?.type === 'end' && key === 'output') {
return ( return (
<Form.Item key={key} name={key}> <Form.Item key={key} name={key}>
<MessageEditor key={key} isArray={false} parentName={key} options={variableList} /> <MessageEditor
key={key}
isArray={false}
parentName={key}
options={variableList.filter(variable => variable.nodeData?.type !== 'knowledge-retrieval')}
/>
</Form.Item> </Form.Item>
) )
} }
@@ -943,7 +1172,7 @@ const Properties: FC<PropertiesProps> = ({
isArray={!!config.isArray} isArray={!!config.isArray}
parentName={key} parentName={key}
enableJinja2={config.enableJinja2 as boolean} enableJinja2={config.enableJinja2 as boolean}
options={getFilteredVariableList(selectedNode?.data?.type)} options={getFilteredVariableList(selectedNode?.data?.type, key)}
/> />
</Form.Item> </Form.Item>
) )
@@ -964,7 +1193,7 @@ const Properties: FC<PropertiesProps> = ({
<Form.Item key={key} name={key}> <Form.Item key={key} name={key}>
<GroupVariableList <GroupVariableList
name={key} name={key}
options={getFilteredVariableList(selectedNode?.data?.type)} options={getFilteredVariableList(selectedNode?.data?.type, key)}
isCanAdd={!!(values as any)?.group} isCanAdd={!!(values as any)?.group}
/> />
</Form.Item> </Form.Item>
@@ -976,7 +1205,7 @@ const Properties: FC<PropertiesProps> = ({
<Form.Item key={key} name={key}> <Form.Item key={key} name={key}>
<CaseList <CaseList
name={key} name={key}
options={getFilteredVariableList(selectedNode?.data?.type)} options={getFilteredVariableList(selectedNode?.data?.type, key)}
selectedNode={selectedNode} selectedNode={selectedNode}
graphRef={graphRef} graphRef={graphRef}
/> />
@@ -989,7 +1218,7 @@ const Properties: FC<PropertiesProps> = ({
<Form.Item key={key} name={key} <Form.Item key={key} name={key}
label={t(`workflow.config.${selectedNode?.data?.type}.${key}`)} label={t(`workflow.config.${selectedNode?.data?.type}.${key}`)}
> >
<MappingList name={key} options={getFilteredVariableList(selectedNode?.data?.type)} /> <MappingList name={key} options={getFilteredVariableList(selectedNode?.data?.type, key)} />
</Form.Item> </Form.Item>
) )
@@ -999,7 +1228,7 @@ const Properties: FC<PropertiesProps> = ({
<Form.Item key={key} name={key}> <Form.Item key={key} name={key}>
<CycleVarsList <CycleVarsList
parentName={key} parentName={key}
options={getFilteredVariableList(selectedNode?.data?.type)} options={getFilteredVariableList(selectedNode?.data?.type, key)}
/> />
</Form.Item> </Form.Item>
) )
@@ -1013,9 +1242,9 @@ const Properties: FC<PropertiesProps> = ({
if (config.filterLoopIterationVars) { if (config.filterLoopIterationVars) {
const loopIterationVars: Suggestion[] = []; const loopIterationVars: Suggestion[] = [];
return [...getFilteredVariableList(selectedNode?.data?.type), ...loopIterationVars]; return [...getFilteredVariableList(selectedNode?.data?.type, key), ...loopIterationVars];
} }
return getFilteredVariableList(selectedNode?.data?.type); return getFilteredVariableList(selectedNode?.data?.type, key);
})() })()
} }
/> />
@@ -1060,7 +1289,7 @@ const Properties: FC<PropertiesProps> = ({
? <VariableSelect ? <VariableSelect
placeholder={t('common.pleaseSelect')} placeholder={t('common.pleaseSelect')}
options={(() => { options={(() => {
const baseVariableList = getFilteredVariableList(selectedNode?.data?.type); const baseVariableList = getFilteredVariableList(selectedNode?.data?.type, key);
// Apply filtering if specified in config // Apply filtering if specified in config
if (config.filterNodeTypes || config.filterVariableNames) { if (config.filterNodeTypes || config.filterVariableNames) {
return baseVariableList.filter(variable => { return baseVariableList.filter(variable => {
@@ -1085,7 +1314,7 @@ const Properties: FC<PropertiesProps> = ({
}); });
return baseVariableList.filter(variable => return baseVariableList.filter(variable =>
childNodes.some(node => node.id === variable.nodeData?.id) childNodes.some(node => node.id === variable.nodeData?.id) || selectedNode?.data?.type === 'iteration' && key === 'output' && variable.value.includes('sys.')
); );
} }
return baseVariableList; return baseVariableList;
@@ -1095,7 +1324,12 @@ const Properties: FC<PropertiesProps> = ({
: config.type === 'switch' : config.type === 'switch'
? <Switch onChange={key === 'group' ? () => { form.setFieldValue('group_variables', []) } : undefined} /> ? <Switch onChange={key === 'group' ? () => { form.setFieldValue('group_variables', []) } : undefined} />
: config.type === 'categoryList' : config.type === 'categoryList'
? <CategoryList parentName={key} selectedNode={selectedNode} graphRef={graphRef} /> ? <CategoryList
parentName={key}
selectedNode={selectedNode}
graphRef={graphRef}
options={getFilteredVariableList(selectedNode?.data?.type, key)}
/>
: config.type === 'conditionList' : config.type === 'conditionList'
? <ConditionList ? <ConditionList
parentName={key} parentName={key}
@@ -1109,18 +1343,9 @@ const Properties: FC<PropertiesProps> = ({
value: `${selectedNode.getData().id}.${cycleVar.name}`, value: `${selectedNode.getData().id}.${cycleVar.name}`,
nodeData: selectedNode.getData(), nodeData: selectedNode.getData(),
})); }));
return [...variableList.filter(variable => {
// Keep conversation variables return [...getFilteredVariableList(selectedNode?.data?.type, key), ...cycleVarSuggestions];
if (variable.group === 'CONVERSATION') return true; })()}
// Keep sys variables from start nodes
if (variable.nodeData?.type === 'start' && variable.value?.startsWith('sys.')) return true;
// Keep variables from non-start nodes
if (variable.nodeData?.type !== 'start' && variable.nodeData?.type !== 'http-request' && variable.dataType !== 'boolean') return true;
// Filter out custom variables from start nodes
return false;
}), ...cycleVarSuggestions];
})()
}
selectedNode={selectedNode} selectedNode={selectedNode}
graphRef={graphRef} graphRef={graphRef}
addBtnText={t('workflow.config.addCase')} addBtnText={t('workflow.config.addCase')}

View File

@@ -270,7 +270,7 @@ export const nodeLibrary: NodeLibrary[] = [
config: { config: {
input: { input: {
type: 'variableList', type: 'variableList',
filterNodeTypes: ['knowledge-retrieval'], filterNodeTypes: ['knowledge-retrieval', 'iteration', 'loop'],
filterVariableNames: ['message'] filterVariableNames: ['message']
}, },
parallel: { parallel: {
@@ -334,8 +334,7 @@ export const nodeLibrary: NodeLibrary[] = [
} }
} }
}, },
{ { type: "assigner", icon: assignerIcon,
type: "assigner", icon: assignerIcon,
config: { config: {
assignments: { assignments: {
type: 'assignmentList', type: 'assignmentList',
@@ -629,3 +628,113 @@ export const graphNodeLibrary: Record<string, NodeConfig> = {
}, },
} }
} }
export interface OutputVariable {
default?: Array<{
name: string;
type: string;
}>;
define?: string[];
sys?: Array<{
name: string;
type: string;
}>;
error?: Array<{
name: string;
type: string;
}>;
}
export const outputVariable: { [key: string]: OutputVariable } = {
start: {
sys: [
{ name: "message", type: "string" },
{ name: "conversation_id", type: "string" },
{ name: "execution_id", type: "string", },
{ name: "workspace_id", type: "string" },
{ name: "user_id", type: "string" },
],
define: ['variables']
},
end: {
},
llm: {
default: [
{ name: "output", type: "string" },
]
},
'knowledge-retrieval': {
default: [
{ name: "output", type: "array[object]" },
]
},
'parameter-extractor': {
default: [
{ name: "__is_success", type: "number" },
{ name: "__reason", type: "string" },
],
define: ['params']
},
'memory-read': {
default: [
{ name: "answer", type: "string" },
{ name: "intermediate_outputs", type: "array[object]" },
],
},
'memory-write': {
},
'if-else': {
},
'question-classifier': {
default: [
{ name: "class_name", type: "string" },
// { name: "output", type: "string" },
],
},
'iteration': {
default: [
// { name: "item", type: "string" }, // 仅内部使用
{ name: "output", type: "array[string]" },
],
},
'loop': {
define: ['cycle_vars']
},
'cycle-start': {
},
'break': {
},
'var-aggregator': {
// default: [
// { name: "output", type: "string" },
// ],
define: ['group_variables']
},
'assigner': {
},
'http-request': {
default: [
{ name: "body", type: "string" },
{ name: "status_code", type: "number" },
],
error: [
{ name: "error_message", type: "string" },
{ name: "error_type", type: "string" },
]
},
'tool': {
default: [
{ name: "data", type: "string" },
],
},
'jinja-render': {
default: [
{ name: "output", type: "string" },
],
},
}