fix(web): workflow bugfix

This commit is contained in:
zhaoying
2026-01-07 17:35:23 +08:00
parent 72c27273e4
commit 030a141c64
14 changed files with 833 additions and 186 deletions

View File

@@ -28,12 +28,18 @@ const AuthConfigModal = forwardRef<AuthConfigModalRef, AuthConfigModalProps>(({
const handleOpen = (data?: HttpRequestConfigForm['auth']) => {
if (data) {
form.setFieldsValue({
const initialValues = {
auth: !data.auth_type || data.auth_type === 'none' ? 'none' : 'api_key',
auth_type: !data.auth_type || data.auth_type === 'none' ? undefined : data.auth_type,
header: data.header,
api_key: data.api_key
})
}
form.setFieldValue('auth', initialValues.auth)
if (initialValues.auth !== 'none') {
setTimeout(() => {
form.setFieldsValue(initialValues)
}, 1)
}
}
setVisible(true);
};
@@ -91,6 +97,9 @@ const AuthConfigModal = forwardRef<AuthConfigModalRef, AuthConfigModalProps>(({
<FormItem
name="auth"
label={t('workflow.config.http-request.authType')}
rules={[
{ required: true, message: t('common.pleaseSelect') }
]}
>
<Select
options={[
@@ -103,6 +112,9 @@ const AuthConfigModal = forwardRef<AuthConfigModalRef, AuthConfigModalProps>(({
<FormItem
name="auth_type"
label={t('workflow.config.http-request.authType')}
rules={[
{ required: true, message: t('common.pleaseSelect') }
]}
>
<Select
options={[

View File

@@ -1,4 +1,4 @@
import { useMemo, useCallback } from 'react';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next'
import { Button, Select, Table, Form, type TableProps } from 'antd';
import { PlusOutlined, DeleteOutlined } from '@ant-design/icons';
@@ -6,46 +6,8 @@ import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin';
import Empty from '@/components/Empty';
import VariableSelect from '../VariableSelect';
interface EditableCellProps extends React.HTMLAttributes<HTMLElement> {
name?: string | string[];
inputType?: 'select' | 'variableSelect';
options?: { value: string, label: string }[] | Suggestion[];
}
const EditableCell: React.FC<React.PropsWithChildren<EditableCellProps>> = ({
name,
inputType,
options,
children,
...restProps
}) => {
const { t } = useTranslation();
if (!inputType) return <td {...restProps}>{children}</td>;
return (
<td {...restProps}>
<Form.Item name={name} style={{ margin: 0 }}>
{inputType === 'select' ? (
<Select
placeholder={t('common.pleaseSelect')}
size="small"
options={options as { value: string, label: string }[]}
/>
) : (
<VariableSelect
placeholder={t('common.pleaseSelect')}
size="small"
options={(options as Suggestion[]) || []}
/>
)}
</Form.Item>
</td>
);
};
export interface TableRow {
key: string;
key?: string;
name?: string;
value?: string;
type?: string;
@@ -56,100 +18,158 @@ interface EditableTableProps {
title?: string;
options?: Suggestion[];
typeOptions?: { value: string, label: string }[]
filterBooleanType?: boolean;
}
const EditableTable: React.FC<EditableTableProps> = ({
parentName,
title,
options = [],
typeOptions = []
typeOptions = [],
filterBooleanType = false
}) => {
const { t } = useTranslation();
const form = Form.useFormInstance();
const values = Form.useWatch(typeof parentName === 'string' ? [parentName] : parentName, form);
const createNewRow = (): TableRow => ({
key: Date.now().toString(),
name: undefined,
value: undefined,
...(typeOptions.length > 0 && { type: typeOptions[0].value })
});
const handleAdd = useCallback(() => {
form.setFieldValue(parentName, [...(values ?? []), createNewRow()]);
}, [form, parentName, values, typeOptions]);
const handleDelete = useCallback((index: number) => {
const currentValues = form.getFieldValue(parentName) || [];
form.setFieldValue(parentName, currentValues.filter((_: TableRow, i: number) => i !== index));
}, [form, parentName]);
const createColumn = (dataIndex: string, inputType: 'select' | 'variableSelect', width: string, columnOptions: any[]) => ({
title: t(`workflow.config.${dataIndex}`),
dataIndex,
width,
onCell: (_: TableRow, index?: number) => ({
name: typeof parentName === 'string' ? [parentName, index ?? 0, dataIndex] : [...parentName, index ?? 0, dataIndex],
inputType,
options: columnOptions
} as any)
});
const columns: TableProps<TableRow>['columns'] = useMemo(() => {
const getColumns = (remove: (index: number) => void): TableProps<TableRow>['columns'] => {
const hasType = typeOptions.length > 0;
const baseWidth = hasType ? '35%' : '45%';
return [
createColumn('name', 'variableSelect', baseWidth, options),
...(hasType ? [createColumn('type', 'select', '20%', typeOptions)] : []),
createColumn('value', 'variableSelect', baseWidth, options),
{
title: t('workflow.config.name'),
dataIndex: 'name',
width: baseWidth,
render: (_: any, __: TableRow, index: number) => (
<Form.Item name={[index, 'name']} noStyle>
<VariableSelect
placeholder={t('common.pleaseSelect')}
size="small"
options={options}
filterBooleanType={filterBooleanType}
popupMatchSelectWidth={false}
/>
</Form.Item>
)
},
...(hasType ? [{
title: t('workflow.config.type'),
dataIndex: 'type',
width: '20%',
render: (_: any, __: TableRow, index: number) => (
<Form.Item shouldUpdate noStyle>
{(form) => (
<Form.Item name={[index, 'type']} noStyle>
<Select
placeholder={t('common.pleaseSelect')}
size="small"
options={typeOptions}
popupMatchSelectWidth={false}
onChange={() => {
form.setFieldValue([...Array.isArray(parentName) ? parentName : [parentName], index, 'value'], undefined);
}}
/>
</Form.Item>
)}
</Form.Item>
)
}] : []),
{
title: t('workflow.config.value'),
dataIndex: 'value',
width: baseWidth,
render: (_: any, __: TableRow, index: number) => (
<Form.Item
shouldUpdate={(prevValues, currentValues) => {
const prevType = prevValues?.[Array.isArray(parentName) ? parentName.join('.') : parentName]?.[index]?.type;
const currentType = currentValues?.[Array.isArray(parentName) ? parentName.join('.') : parentName]?.[index]?.type;
return prevType !== currentType;
}}
noStyle
>
{(form) => {
const currentType = form.getFieldValue([...Array.isArray(parentName) ? parentName : [parentName], index, 'type']);
const filteredOptions = currentType === 'file'
? options.filter(option => option.dataType === 'file')
: options;
return (
<Form.Item name={[index, 'value']} noStyle>
<VariableSelect
placeholder={t('common.pleaseSelect')}
size="small"
options={filteredOptions}
filterBooleanType={filterBooleanType}
popupMatchSelectWidth={false}
/>
</Form.Item>
);
}}
</Form.Item>
)
},
{
title: '',
dataIndex: 'actions',
width: '10%',
render: (_: any, __: TableRow, index: number) => (
<Button type="text" icon={<DeleteOutlined />} onClick={() => handleDelete(index)} />
<Button type="text" icon={<DeleteOutlined />} onClick={() => remove(index)} />
)
}
];
}, [typeOptions, options, t, parentName, handleDelete]);
const AddButton = ({ block = false }: { block?: boolean }) => (
<Button
type={block ? "dashed" : "text"}
icon={<PlusOutlined />}
onClick={handleAdd}
size="small"
block={block}
className={block ? "rb:mt-1" : ""}
>
{block && `+${t('common.add')}`}
</Button>
);
};
return (
<div className="rb:mb-4">
{title && (
<div className="rb:flex rb:items-center rb:mb-2 rb:justify-between">
<div className="rb:font-medium">{title}</div>
<AddButton />
</div>
)}
<Form.Item name={parentName}>
<Table<TableRow>
components={{ body: { cell: EditableCell } }}
bordered
dataSource={values}
columns={columns}
pagination={false}
size="small"
locale={{ emptyText: <Empty size={88} /> }}
scroll={{ x: 'max-content' }}
/>
</Form.Item>
{!title && <AddButton block />}
<Form.List name={parentName}>
{(fields, { add, remove }) => {
const AddButton = ({ block = false }: { block?: boolean }) => (
<Button
type={block ? "dashed" : "text"}
icon={<PlusOutlined />}
onClick={() => add(createNewRow())}
size="small"
block={block}
className={block ? "rb:mt-1" : ""}
>
{block && `+${t('common.add')}`}
</Button>
);
return (
<>
{title && (
<div className="rb:flex rb:items-center rb:mb-2 rb:justify-between">
<div className="rb:font-medium">{title}</div>
<AddButton />
</div>
)}
<Table<TableRow>
bordered
dataSource={fields.map((field) => ({
key: String(field.key),
name: undefined,
value: undefined,
type: undefined
}))}
columns={getColumns(remove)}
pagination={false}
size="small"
locale={{ emptyText: <Empty size={88} /> }}
scroll={{ x: 'max-content' }}
/>
{!title && <AddButton block />}
</>
);
}}
</Form.List>
</div>
);
};

View File

@@ -9,8 +9,10 @@ import VariableSelect from "../VariableSelect";
import MessageEditor from '../MessageEditor'
import EditableTable from './EditableTable'
const HttpRequest: FC<{ options: Suggestion[]; }> = ({
const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: any; }> = ({
options,
selectedNode,
graphRef
}) => {
const { t } = useTranslation()
const form = Form.useFormInstance();
@@ -22,18 +24,45 @@ const HttpRequest: FC<{ options: Suggestion[]; }> = ({
}
const handleRefresh = (auth: HttpRequestConfigForm['auth']) => {
console.log('handleRefresh', auth)
form.setFieldsValue({ auth: {...auth} })
form.setFieldsValue({ auth })
}
const handleChangeBodyContentType = (contentType: string) => {
const currentValues = form.getFieldsValue()
const handleChangeBodyContentType = () => {
form.setFieldValue(['body', 'data'], undefined)
}
const handleChangeErrorHandleMethod = (method: string) => {
form.setFieldsValue({
body: {
...currentValues?.body,
content_type: contentType,
data: undefined
error_handle: {
method,
body: undefined,
status_code: undefined,
headers: undefined
}
})
// 更新节点连接桩
console.log('handleChangeErrorHandleMethod', selectedNode, graphRef?.current)
if (selectedNode && graphRef?.current) {
const existingPorts = selectedNode.getPorts();
const errorPort = existingPorts.find((port: any) => port.id === 'ERROR');
if (method === 'branch' && !errorPort) {
// 添加异常节点连接桩
selectedNode.addPort({
id: 'ERROR',
group: 'right',
attrs: { text: { text: t('workflow.config.http-request.errorBranch'), fontSize: 12, fill: '#5B6167' }}
});
} else if (method !== 'branch' && errorPort) {
// 移除异常节点连接桩和相关连线
const edges = graphRef.current.getEdges().filter((edge: any) =>
edge.getSourceCellId() === selectedNode.id && edge.getSourcePortId() === 'ERROR'
);
edges.forEach((edge: any) => graphRef.current.removeCell(edge));
selectedNode.removePort('ERROR');
}
}
}
console.log('HttpRequest', values)
@@ -73,6 +102,7 @@ const HttpRequest: FC<{ options: Suggestion[]; }> = ({
parentName="headers"
title="HEADERS"
options={options}
filterBooleanType={true}
/>
</Form.Item>
@@ -81,6 +111,7 @@ const HttpRequest: FC<{ options: Suggestion[]; }> = ({
parentName="params"
title="PARAMS"
options={options}
filterBooleanType={true}
/>
</Form.Item>
@@ -104,6 +135,7 @@ const HttpRequest: FC<{ options: Suggestion[]; }> = ({
<EditableTable
parentName={['body', 'data']}
options={options}
filterBooleanType={true}
typeOptions={[
{ label: 'text', value: 'text' },
{ label: 'file', value: 'file' }
@@ -116,12 +148,15 @@ const HttpRequest: FC<{ options: Suggestion[]; }> = ({
<EditableTable
parentName={['body', 'data']}
options={options}
filterBooleanType={true}
/>
</Form.Item>
}
{values?.body?.content_type === 'json' &&
<Form.Item name={['body', 'data']}>
<MessageEditor
key="json"
parentName={['body', 'data']}
options={options}
isArray={false}
title="JSON"
@@ -131,6 +166,8 @@ const HttpRequest: FC<{ options: Suggestion[]; }> = ({
{values?.body?.content_type === 'raw' &&
<Form.Item name={['body', 'data']}>
<MessageEditor
key="raw"
parentName={['body', 'data']}
options={options}
isArray={false}
title="RAW TEXT"
@@ -141,6 +178,7 @@ const HttpRequest: FC<{ options: Suggestion[]; }> = ({
<Form.Item name={['body', 'data']}>
<VariableSelect
options={options}
filterBooleanType={true}
/>
</Form.Item>
}
@@ -185,7 +223,7 @@ const HttpRequest: FC<{ options: Suggestion[]; }> = ({
</Form.Item>
<Form.Item
name={['retry', 'retry_interval']}
label={t('workflow.config.http-request.retry_interval')}
label={<>{t('workflow.config.http-request.retry_interval')} <span className="rb:text-[#5B6167]">(ms)</span></>}
>
<InputNumber placeholder={t('common.pleaseEnter')} className="rb:w-full!" />
</Form.Item>
@@ -196,6 +234,7 @@ const HttpRequest: FC<{ options: Suggestion[]; }> = ({
<Form.Item layout="horizontal" name={['error_handle', 'method']} label={t('workflow.config.http-request.error_handle')}>
<Select
placeholder={t('common.pleaseSelect')}
onChange={handleChangeErrorHandleMethod}
options={[
{ value: 'none', label: t('workflow.config.http-request.none') },
{ value: 'default', label: t('workflow.config.http-request.default') },
@@ -207,32 +246,19 @@ const HttpRequest: FC<{ options: Suggestion[]; }> = ({
<>
<Form.Item
name={['error_handle', 'body']}
label="body"
label={<>body <span className="rb:text-[#5B6167] rb:ml-1">string</span></>}
>
<Input placeholder={t('common.pleaseEnter')} />
</Form.Item>
<Form.Item
name={['error_handle', 'status_code']}
label="status_code"
label={<>status_code <span className="rb:text-[#5B6167] rb:ml-1">number</span></>}
>
<InputNumber placeholder={t('common.pleaseEnter')} className="rb:w-full!" />
</Form.Item>
<Form.Item
name={['error_handle', 'headers']}
label="headers"
rules={[
{
validator: (_, value) => {
if (!value) return Promise.resolve();
try {
JSON.parse(value);
return Promise.resolve();
} catch {
return Promise.reject(new Error('Please enter valid JSON format'));
}
}
}
]}
label={<>headers <span className="rb:text-[#5B6167] rb:ml-1">object</span></>}
>
<Input.TextArea placeholder={t('common.pleaseEnter')} />
</Form.Item>

View File

@@ -17,14 +17,14 @@ const MappingList: React.FC<MappingListProps> = ({ name, options }) => {
{(fields, { add, remove }) => (
<>
{fields.map(({ key, name, ...restField }) => (
<Row gutter={12} className="rb:mb-2">
<Row key={key} gutter={12} className="rb:mb-2">
<Col span={10}>
<Form.Item
{...restField}
name={[name, 'name']}
noStyle
>
<Input placeholder={t('common.pleaseEnter')} />
<Input placeholder={t('common.pleaseEnter')} data-field-type="mapping-name" />
</Form.Item>
</Col>
<Col span={12}>

View File

@@ -5,14 +5,15 @@ import { MinusCircleOutlined } from '@ant-design/icons';
import Editor from '../Editor'
import type { Suggestion } from '../Editor/plugin/AutocompletePlugin'
interface TextareaProps {
interface MessageEditor {
options: Suggestion[];
title?: string
isArray?: boolean;
parentName?: string;
parentName?: string | string[];
label?: string;
placeholder?: string;
value?: string;
enableJinja2?: boolean;
onChange?: (value?: string) => void;
}
const roleOptions = [
@@ -20,12 +21,13 @@ const roleOptions = [
{ label: 'USER', value: 'USER' },
{ label: 'ASSISTANT', value: 'ASSISTANT' },
]
const MessageEditor: FC<TextareaProps> = ({
const MessageEditor: FC<MessageEditor> = ({
title,
isArray = true,
parentName = 'messages',
placeholder,
options,
enableJinja2 = false,
}) => {
const { t } = useTranslation()
const form = Form.useFormInstance();
@@ -33,10 +35,17 @@ const MessageEditor: FC<TextareaProps> = ({
// 检查是否已经使用了context变量将已使用的context设置为disabled
const processedOptions = useMemo(() => {
if (!isArray || !values?.[parentName]) return options;
if (!isArray) return options;
// 获取表单中对应字段的值
const fieldValue = Array.isArray(parentName)
? parentName.reduce((obj, key) => obj?.[key], values)
: values?.[parentName];
if (!fieldValue) return options;
// 获取所有消息内容
const allContents = values[parentName]
const allContents = fieldValue
.map((msg: any) => msg?.content || '')
.join(' ');
@@ -50,7 +59,11 @@ const MessageEditor: FC<TextareaProps> = ({
}, [options, values, parentName, isArray]);
const handleAdd = (add: FormListOperation['add']) => {
const list = values?.[parentName] || [];
const fieldValue = Array.isArray(parentName)
? parentName.reduce((obj, key) => obj?.[key], values)
: values?.[parentName];
const list = fieldValue || [];
const lastRole = list.length > 0 ? list[list.length - 1]?.role : 'ASSISTANT';
add({
@@ -61,14 +74,14 @@ const MessageEditor: FC<TextareaProps> = ({
if (!isArray) {
return (
<Space size={12} direction="vertical" className="rb:w-full rb:border rb:border-[#DFE4ED] rb:rounded-md rb:px-2 rb:py-1.5 rb:bg-white">
<Space size={12} direction="vertical" className="rb:w-full rb:border rb:border-[#DFE4ED] rb:rounded-md rb:px-2 rb:py-1.5 rb:bg-white" data-editor-type={parentName === 'template' ? 'template' : undefined}>
<Row>
<Col span={12}>
{title ?? t('workflow.answerDesc')}
</Col>
</Row>
<Form.Item name={parentName} noStyle>
<Editor placeholder={placeholder} options={processedOptions} />
<Editor enableJinja2={enableJinja2} placeholder={placeholder} options={processedOptions} />
</Form.Item>
</Space>
);
@@ -79,7 +92,11 @@ const MessageEditor: FC<TextareaProps> = ({
{(fields, { add, remove }) => (
<Space size={12} direction="vertical" className="rb:w-full">
{fields.map(({ key, name, ...restField }) => {
const currentRole = (values?.[parentName]?.[name]?.role || 'USER').toUpperCase();
const fieldValue = Array.isArray(parentName)
? parentName.reduce((obj, key) => obj?.[key], values)
: values?.[parentName];
const currentRole = (fieldValue?.[name]?.role || 'USER').toUpperCase();
return (
<Space key={key} size={12} direction="vertical" className="rb:w-full rb:border rb:border-[#DFE4ED] rb:rounded-md rb:px-2 rb:py-1.5 rb:bg-white">
@@ -105,7 +122,7 @@ const MessageEditor: FC<TextareaProps> = ({
)}
</Row>
<Form.Item {...restField} name={[name, 'content']} noStyle>
<Editor placeholder={placeholder} options={processedOptions} />
<Editor enableJinja2={enableJinja2} placeholder={placeholder} options={processedOptions} />
</Form.Item>
</Space>
);

View File

@@ -9,6 +9,7 @@ interface VariableSelectProps extends SelectProps {
value?: string;
onChange?: (value: string) => void;
allowClear?: boolean;
filterBooleanType?: boolean;
}
const VariableSelect: FC<VariableSelectProps> = ({
@@ -18,6 +19,7 @@ const VariableSelect: FC<VariableSelectProps> = ({
allowClear = true,
onChange,
size,
filterBooleanType = false,
...resetPorps
}) => {
@@ -26,7 +28,7 @@ const VariableSelect: FC<VariableSelectProps> = ({
}
const labelRender: LabelRender = (props) => {
const { value } = props
const filterOption = options.find(vo => `{{${vo.value}}}` === value)
const filterOption = filteredOptions.find(vo => `{{${vo.value}}}` === value)
if (filterOption) {
return (
@@ -54,7 +56,11 @@ const VariableSelect: FC<VariableSelectProps> = ({
}
return null
}
const groupedSuggestions = options.reduce((groups: Record<string, any[]>, suggestion) => {
const filteredOptions = filterBooleanType
? options.filter(option => option.dataType !== 'boolean')
: options;
const groupedSuggestions = filteredOptions.reduce((groups: Record<string, any[]>, suggestion) => {
const { nodeData } = suggestion
const nodeId = nodeData.id as string;
if (!groups[nodeId]) {
@@ -64,12 +70,10 @@ const VariableSelect: FC<VariableSelectProps> = ({
return groups;
}, {});
const groupedOptions = Object.entries(groupedSuggestions).map(([nodeId, suggestions]) => ({
const groupedOptions = Object.entries(groupedSuggestions).map(([_nodeId, suggestions]) => ({
label: suggestions[0].nodeData.name,
options: suggestions.map(s => ({ label: s.label, value: `{{${s.value}}}` }))
}));
console.log('groupedOptions', groupedOptions)
return (
<Select

View File

@@ -45,13 +45,134 @@ const Properties: FC<PropertiesProps> = ({
const values = Form.useWatch([], form);
const variableModalRef = useRef<VariableEditModalRef>(null)
const [editIndex, setEditIndex] = useState<number | null>(null)
const prevMappingNamesRef = useRef<string[]>([])
const prevTemplateVarsRef = useRef<string[]>([])
const syncTimeoutRef = useRef<NodeJS.Timeout | null>(null)
const isSyncingRef = useRef(false)
const lastSyncSourceRef = useRef<'mapping' | 'template' | null>(null)
useEffect(() => {
if (selectedNode?.getData()?.id) {
form.resetFields()
prevMappingNamesRef.current = []
prevTemplateVarsRef.current = []
lastSyncSourceRef.current = null
}
}, [selectedNode?.getData()?.id])
// Sync template when mapping names change
useEffect(() => {
if (isSyncingRef.current || lastSyncSourceRef.current === 'mapping' || selectedNode?.data?.type !== 'jinja-render' || !values?.mapping || !values?.template) return
const currentMappingNames = values.mapping.map((item: any) => item.name).filter(Boolean)
const prevNames = prevMappingNamesRef.current
if (prevNames.length === 0) {
prevMappingNamesRef.current = currentMappingNames
return
}
if (JSON.stringify(prevNames) === JSON.stringify(currentMappingNames)) return
if (syncTimeoutRef.current) clearTimeout(syncTimeoutRef.current)
const activeElement = document.activeElement as HTMLElement
syncTimeoutRef.current = setTimeout(() => {
let updatedTemplate = String(form.getFieldValue('template') || '')
prevNames.forEach((oldName, index) => {
const newName = currentMappingNames[index]
if (newName && oldName !== newName) {
updatedTemplate = updatedTemplate.replace(new RegExp(`{{\\s*${oldName}\\s*}}`, 'g'), `{{${newName}}}`)
}
})
if (updatedTemplate !== form.getFieldValue('template')) {
isSyncingRef.current = true
lastSyncSourceRef.current = 'mapping'
const newTemplateVars = (updatedTemplate.match(/{{\s*([\w.]+)\s*}}/g) || []).map(m => m.replace(/{{\s*|\s*}}/g, ''))
prevTemplateVarsRef.current = newTemplateVars
prevMappingNamesRef.current = currentMappingNames
form.setFieldValue('template', updatedTemplate)
requestAnimationFrame(() => {
activeElement?.focus?.()
setTimeout(() => {
isSyncingRef.current = false
lastSyncSourceRef.current = null
}, 50)
})
} else {
prevMappingNamesRef.current = currentMappingNames
}
}, 0)
}, [values?.mapping, selectedNode?.data?.type, form])
// Sync mapping when template variables change
useEffect(() => {
if (isSyncingRef.current || lastSyncSourceRef.current === 'template' || selectedNode?.data?.type !== 'jinja-render' || !values?.template || !values?.mapping) return
const templateVars = (String(values.template).match(/{{\s*([\w.]+)\s*}}/g) || []).map(m => m.replace(/{{\s*|\s*}}/g, ''))
if (JSON.stringify(prevTemplateVarsRef.current) === JSON.stringify(templateVars)) return
const isTemplateEditor = document.activeElement?.closest('[data-editor-type="template"]')
if (!isTemplateEditor) {
prevTemplateVarsRef.current = templateVars
return
}
const updatedMapping = [...values.mapping]
const existingNames = updatedMapping.map(item => item.name)
let updatedTemplate = String(values.template)
if (prevTemplateVarsRef.current.length > 0) {
prevTemplateVarsRef.current.forEach((oldVar, index) => {
const newVar = templateVars[index]
if (newVar && oldVar !== newVar && updatedMapping[index]) {
updatedMapping[index] = { ...updatedMapping[index], name: newVar }
}
})
}
templateVars.forEach(varName => {
const existingMapping = updatedMapping.find(item => item.value === `{{${varName}}}`)
const regex = new RegExp(`{{\\s*${varName.replace(/\./g, '\\.')}\\s*}}`, 'g')
if (existingMapping) {
updatedTemplate = updatedTemplate.replace(regex, `{{${existingMapping.name}}}`)
} else if (!existingNames.includes(varName)) {
const mappingName = varName.includes('.') ? varName.split('.').pop() || varName : varName
updatedMapping.push({ name: mappingName, value: `{{${varName}}}` })
updatedTemplate = updatedTemplate.replace(regex, `{{${mappingName}}}`)
}
})
const seenNames = new Set<string>()
const finalMapping = updatedMapping.filter(item => {
const isUsed = templateVars.some(v => item.name === v || item.value === `{{${v}}}`)
if (!isUsed || seenNames.has(item.name)) return false
seenNames.add(item.name)
return true
})
isSyncingRef.current = true
lastSyncSourceRef.current = 'template'
prevMappingNamesRef.current = finalMapping.map((item: any) => item.name).filter(Boolean)
prevTemplateVarsRef.current = templateVars
if (JSON.stringify(finalMapping) !== JSON.stringify(values.mapping)) {
form.setFieldValue('mapping', finalMapping)
}
if (updatedTemplate !== String(values.template)) {
form.setFieldValue('template', updatedTemplate)
}
setTimeout(() => {
isSyncingRef.current = false
lastSyncSourceRef.current = null
}, 50)
}, [values?.template, selectedNode?.data?.type, form])
useEffect(() => {
if (selectedNode && form) {
const { type = 'default', name = '', config } = selectedNode.getData() || {}
@@ -96,6 +217,8 @@ const Properties: FC<PropertiesProps> = ({
}))
}
Object.keys(values).forEach(key => {
if (selectedNode.data?.config?.[key]) {
// Create a deep copy to avoid reference sharing between nodes
@@ -114,7 +237,7 @@ const Properties: FC<PropertiesProps> = ({
...allRest,
})
}
}, [values, selectedNode])
}, [values, selectedNode, form])
const handleAddVariable = () => {
variableModalRef.current?.handleOpen()
@@ -190,11 +313,88 @@ const Properties: FC<PropertiesProps> = ({
.map(node => node.id);
};
// Find parent loop/iteration node if current node is a child
const getParentLoopNode = (nodeId: string): Node | null => {
const node = nodes.find(n => n.id === nodeId);
if (!node) return null;
const nodeData = node.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;
};
const allPreviousNodeIds = getAllPreviousNodes(selectedNode.id);
const childNodeIds = getChildNodes(selectedNode.id);
const parentLoopNode = getParentLoopNode(selectedNode.id);
console.log('childNodeIds', selectedNode, childNodeIds)
const allRelevantNodeIds = [...allPreviousNodeIds, ...childNodeIds];
// Add parent loop/iteration node variables if current node is a child
if (parentLoopNode) {
const parentData = parentLoopNode.getData();
if (parentData.type === 'loop') {
const cycleVars = parentData.cycle_vars || [];
cycleVars.forEach((cycleVar: any) => {
const key = `${parentLoopNode.getData().id}_cycle_${cycleVar.name}`;
if (!addedKeys.has(key)) {
addedKeys.add(key);
variableList.push({
key,
label: cycleVar.name,
type: 'variable',
dataType: cycleVar.type || 'String',
value: `${parentLoopNode.getData().id}.${cycleVar.name}`,
nodeData: parentData,
});
}
});
} else if (parentData.type === 'iteration') {
// Add item and index variables for iteration parent
const itemKey = `${parentLoopNode.getData().id}_item`;
const indexKey = `${parentLoopNode.getData().id}_index`;
if (!addedKeys.has(itemKey)) {
addedKeys.add(itemKey);
variableList.push({
key: itemKey,
label: 'item',
type: 'variable',
dataType: 'Object',
value: `${parentLoopNode.getData().id}.item`,
nodeData: parentData,
});
}
if (!addedKeys.has(indexKey)) {
addedKeys.add(indexKey);
variableList.push({
key: indexKey,
label: 'index',
type: 'variable',
dataType: 'Number',
value: `${parentLoopNode.getData().id}.index`,
nodeData: parentData,
});
}
}
// Add variables from nodes preceding the parent loop/iteration node
const parentPreviousNodeIds = getAllPreviousNodes(parentLoopNode.id);
allRelevantNodeIds.push(...parentPreviousNodeIds);
}
allRelevantNodeIds.forEach(nodeId => {
const node = nodes.find(n => n.id === nodeId);
if (!node) return;
@@ -363,6 +563,87 @@ const Properties: FC<PropertiesProps> = ({
});
}
break
case 'question-classifier':
const classNameKey = `${nodeId}_class_name`;
const outputKey = `${nodeId}_output`;
if (!addedKeys.has(classNameKey)) {
addedKeys.add(classNameKey);
variableList.push({
key: classNameKey,
label: 'class_name',
type: 'variable',
dataType: 'string',
value: `${node.getData().id}.class_name`,
nodeData: nodeData,
});
}
if (!addedKeys.has(outputKey)) {
addedKeys.add(outputKey);
variableList.push({
key: outputKey,
label: 'output',
type: 'variable',
dataType: 'string',
value: `${node.getData().id}.output`,
nodeData: nodeData,
});
}
break
case 'iteration':
const iterationOutputKey = `${nodeId}_output`;
if (!addedKeys.has(iterationOutputKey)) {
addedKeys.add(iterationOutputKey);
// Get the data type from the output configuration, default to string
const outputConfig = nodeData.output;
let outputDataType = 'string';
if (outputConfig) {
// Find the selected variable from variableList to get its type
const selectedVariable = variableList.find(v => v.value === outputConfig);
if (selectedVariable) {
outputDataType = selectedVariable.dataType;
}
}
variableList.push({
key: iterationOutputKey,
label: 'output',
type: 'variable',
dataType: outputDataType,
value: `${node.getData().id}.output`,
nodeData: nodeData,
});
}
break
case 'loop':
const cycleVars = nodeData.cycle_vars || [];
cycleVars.forEach((cycleVar: any) => {
const cycleVarKey = `${nodeId}_cycle_${cycleVar.name}`;
if (!addedKeys.has(cycleVarKey)) {
addedKeys.add(cycleVarKey);
variableList.push({
key: cycleVarKey,
label: cycleVar.name,
type: 'variable',
dataType: cycleVar.type || 'string',
value: `${node.getData().id}.${cycleVar.name}`,
nodeData: nodeData,
});
}
});
break
case 'tool':
const toolDataKey = `${nodeId}_data`;
if (!addedKeys.has(toolDataKey)) {
addedKeys.add(toolDataKey);
variableList.push({
key: toolDataKey,
label: 'data',
type: 'variable',
dataType: 'object',
value: `${node.getData().id}.data`,
nodeData: nodeData,
});
}
break
}
});
@@ -388,6 +669,14 @@ const Properties: FC<PropertiesProps> = ({
return variableList;
}, [selectedNode, graphRef]);
// Filter out boolean type variables for loop and llm nodes
const getFilteredVariableList = (nodeType?: string) => {
if (nodeType === 'loop' || nodeType === 'llm') {
return variableList.filter(variable => variable.dataType !== 'boolean');
}
return variableList;
};
console.log('values', values)
console.log('variableList', variableList, selectedNode?.data)
@@ -412,6 +701,8 @@ const Properties: FC<PropertiesProps> = ({
{selectedNode?.data?.type === 'http-request'
? <HttpRequest
options={variableList}
selectedNode={selectedNode}
graphRef={graphRef}
/>
: selectedNode?.data?.type === 'tool'
? <ToolConfig options={variableList} />
@@ -469,7 +760,7 @@ const Properties: FC<PropertiesProps> = ({
if (selectedNode?.data?.type === 'llm' && key === 'messages' && config.type === 'define') {
// 为llm节点且isArray=true时添加context变量支持
let contextVariableList = [...variableList];
let contextVariableList = [...getFilteredVariableList('llm')];
const isArrayMode = config.isArray !== false; // 默认为true
if (isArrayMode) {
@@ -491,14 +782,14 @@ const Properties: FC<PropertiesProps> = ({
return (
<Form.Item key={key} name={key}>
<MessageEditor options={contextVariableList} parentName={key} />
<MessageEditor key={key} options={contextVariableList} parentName={key} />
</Form.Item>
)
}
if (selectedNode?.data?.type === 'end' && key === 'output') {
return (
<Form.Item key={key} name={key}>
<MessageEditor isArray={false} parentName={key} options={variableList} />
<MessageEditor key={key} isArray={false} parentName={key} options={variableList} />
</Form.Item>
)
}
@@ -525,7 +816,8 @@ const Properties: FC<PropertiesProps> = ({
title={t(`workflow.config.${selectedNode?.data?.type}.${key}`)}
isArray={!!config.isArray}
parentName={key}
options={variableList}
enableJinja2={config.enableJinja2 as boolean}
options={getFilteredVariableList(selectedNode?.data?.type)}
/>
</Form.Item>
)
@@ -546,7 +838,7 @@ const Properties: FC<PropertiesProps> = ({
<Form.Item key={key} name={key}>
<GroupVariableList
name={key}
options={variableList}
options={getFilteredVariableList(selectedNode?.data?.type)}
isCanAdd={!!(values as any)?.group}
/>
</Form.Item>
@@ -558,7 +850,7 @@ const Properties: FC<PropertiesProps> = ({
<Form.Item key={key} name={key}>
<CaseList
name={key}
options={variableList}
options={getFilteredVariableList(selectedNode?.data?.type)}
selectedNode={selectedNode}
graphRef={graphRef}
/>
@@ -571,7 +863,7 @@ const Properties: FC<PropertiesProps> = ({
<Form.Item key={key} name={key}
label={t(`workflow.config.${selectedNode?.data?.type}.${key}`)}
>
<MappingList name={key} options={variableList} />
<MappingList name={key} options={getFilteredVariableList(selectedNode?.data?.type)} />
</Form.Item>
)
@@ -581,7 +873,7 @@ const Properties: FC<PropertiesProps> = ({
<Form.Item key={key} name={key}>
<CycleVarsList
parentName={key}
options={variableList}
options={getFilteredVariableList(selectedNode?.data?.type)}
/>
</Form.Item>
)
@@ -655,9 +947,9 @@ const Properties: FC<PropertiesProps> = ({
findParentLoopIteration(selectedNode.id);
}
return [...variableList, ...loopIterationVars];
return [...getFilteredVariableList(selectedNode?.data?.type), ...loopIterationVars];
}
return variableList;
return getFilteredVariableList(selectedNode?.data?.type);
})()
}
/>
@@ -678,7 +970,7 @@ const Properties: FC<PropertiesProps> = ({
? <Input.TextArea placeholder={t('common.pleaseEnter')} />
: config.type === 'select'
? <Select
options={config.needTranslation ? config.options?.map(vo => ({ ...vo, label: t(vo.label) })) : config.options}
options={config.needTranslation ? (config.options || []).map(vo => ({ ...vo, label: t(vo.label) })) : config.options}
placeholder={t('common.pleaseSelect')}
/>
: config.type === 'inputNumber'
@@ -698,9 +990,10 @@ const Properties: FC<PropertiesProps> = ({
? <VariableSelect
placeholder={t('common.pleaseSelect')}
options={(() => {
const baseVariableList = getFilteredVariableList(selectedNode?.data?.type);
// Apply filtering if specified in config
if (config.filterNodeTypes || config.filterVariableNames) {
return variableList.filter(variable => {
return baseVariableList.filter(variable => {
const nodeTypeMatch = !config.filterNodeTypes ||
(Array.isArray(config.filterNodeTypes) && config.filterNodeTypes.includes(variable.nodeData?.type));
const variableNameMatch = !config.filterVariableNames ||
@@ -721,22 +1014,38 @@ const Properties: FC<PropertiesProps> = ({
return nodeData?.cycle === selectedNode.id;
});
return variableList.filter(variable =>
return baseVariableList.filter(variable =>
childNodes.some(node => node.id === variable.nodeData?.id)
);
}
return variableList;
return baseVariableList;
})()
}
/>
: config.type === 'switch'
? <Switch onChange={key === 'group' ? () => { form.setFieldValue('group_variables', []) } : undefined} />
? <Switch onChange={key === 'group' ? () => { form.setFieldValue('group_variables', []) } : undefined} />
: config.type === 'categoryList'
? <CategoryList parentName={key} selectedNode={selectedNode} graphRef={graphRef} />
: config.type === 'conditionList'
? <ConditionList
parentName={key}
options={variableList}
options={(() => {
// For loop nodes, add cycle_vars to condition options
if (selectedNode?.data?.type === 'loop') {
const cycleVars = values?.cycle_vars || [];
const cycleVarSuggestions: Suggestion[] = cycleVars.map((cycleVar: any) => ({
key: `${selectedNode.id}_cycle_${cycleVar.name}`,
label: cycleVar.name,
type: 'variable',
dataType: cycleVar.type || 'String',
value: `${selectedNode.getData().id}.${cycleVar.name}`,
nodeData: selectedNode.getData(),
}));
return [...getFilteredVariableList(selectedNode?.data?.type), ...cycleVarSuggestions];
}
return getFilteredVariableList(selectedNode?.data?.type);
})()
}
selectedNode={selectedNode}
graphRef={graphRef}
addBtnText={t('workflow.config.addCase')}