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

@@ -1,4 +1,4 @@
import { type FC, useState } from 'react';
import { type FC, useState, useEffect } from 'react';
import { LexicalComposer } from '@lexical/react/LexicalComposer';
import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin';
import { ContentEditable } from '@lexical/react/LexicalContentEditable';
@@ -23,6 +23,7 @@ interface LexicalEditorProps {
options: Suggestion[];
variant?: 'outlined' | 'borderless';
height?: number;
enableJinja2?: boolean;
}
const theme = {
@@ -33,6 +34,15 @@ const theme = {
},
};
const jinja2Theme = {
...theme,
code: 'jinja2-expression',
text: {
...theme.text,
code: 'jinja2-inline',
},
};
const Editor: FC<LexicalEditorProps> =({
placeholder = "请输入内容...",
value = "",
@@ -40,19 +50,62 @@ const Editor: FC<LexicalEditorProps> =({
options,
variant = 'borderless',
height = 60,
enableJinja2 = false,
}) => {
const [_count, setCount] = useState(0);
useEffect(() => {
if (enableJinja2) {
const styleId = 'jinja2-styles';
let existingStyle = document.getElementById(styleId);
if (!existingStyle) {
const style = document.createElement('style');
style.id = styleId;
style.textContent = `
.jinja2-expression {
background-color: #f6f8fa !important;
border: 1px solid #d1d9e0 !important;
border-radius: 3px !important;
padding: 2px 4px !important;
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace !important;
font-size: 13px !important;
color: #0969da !important;
}
.jinja2-inline {
background-color: #f6f8fa !important;
padding: 1px 3px !important;
border-radius: 2px !important;
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace !important;
font-size: 13px !important;
color: #0969da !important;
}
.editor-paragraph {
margin: 0;
}
.editor-paragraph:has-text('{') .editor-text,
.editor-paragraph:has-text('[') .editor-text {
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace !important;
}
`;
document.head.appendChild(style);
}
}
}, [enableJinja2]);
const initialConfig = {
namespace: 'AutocompleteEditor',
theme,
nodes: [
theme: enableJinja2 ? jinja2Theme : theme,
nodes: enableJinja2 ? [
// 当启用jinja2时不使用VariableNode使用普通文本
] : [
// HeadingNode,
// QuoteNode,
// ListItemNode,
// ListNode,
// LinkNode,
// CodeNode,
VariableNode
VariableNode,
],
onError: (error: Error) => {
console.error(error);
@@ -96,9 +149,9 @@ const Editor: FC<LexicalEditorProps> =({
/>
<HistoryPlugin />
<CommandPlugin />
<AutocompletePlugin options={options} />
<AutocompletePlugin options={options} enableJinja2={enableJinja2} />
<CharacterCountPlugin setCount={(count) => { setCount(count) }} onChange={onChange} />
<InitialValuePlugin value={value} options={options} />
<InitialValuePlugin value={value} options={options} enableJinja2={enableJinja2} />
</div>
</LexicalComposer>
);

View File

@@ -17,7 +17,7 @@ export interface Suggestion {
disabled?: boolean; // 标记是否禁用
}
const AutocompletePlugin: FC<{ options: Suggestion[] }> = ({ options }) => {
const AutocompletePlugin: FC<{ options: Suggestion[], enableJinja2?: boolean }> = ({ options, enableJinja2 = false }) => {
const [editor] = useLexicalComposerContext();
const [showSuggestions, setShowSuggestions] = useState(false);
const [selectedIndex, setSelectedIndex] = useState(0);
@@ -82,7 +82,32 @@ const AutocompletePlugin: FC<{ options: Suggestion[] }> = ({ options }) => {
}, [editor]);
const insertMention = (suggestion: Suggestion) => {
editor.dispatchCommand(INSERT_VARIABLE_COMMAND, { data: suggestion });
if (enableJinja2) {
// 在jinja2模式下插入{{variable}}格式的文本
editor.update(() => {
const selection = $getSelection();
if ($isRangeSelection(selection)) {
const anchorNode = selection.anchor.getNode();
const anchorOffset = selection.anchor.offset;
const nodeText = anchorNode.getTextContent();
// 移除触发字符'/'
const textBefore = nodeText.substring(0, anchorOffset - 1);
const textAfter = nodeText.substring(anchorOffset);
const newText = textBefore + `{{${suggestion.value}}}` + textAfter;
anchorNode.setTextContent(newText);
// 设置光标位置到插入文本之后
const newOffset = textBefore.length + `{{${suggestion.value}}}`.length;
selection.anchor.offset = newOffset;
selection.focus.offset = newOffset;
}
});
} else {
// 普通模式下使用VariableNode
editor.dispatchCommand(INSERT_VARIABLE_COMMAND, { data: suggestion });
}
setShowSuggestions(false);
};

View File

@@ -8,14 +8,31 @@ import { type Suggestion } from '../plugin/AutocompletePlugin'
interface InitialValuePluginProps {
value: string;
options?: Suggestion[];
enableJinja2?: boolean;
}
const InitialValuePlugin: React.FC<InitialValuePluginProps> = ({ value, options = [] }) => {
const InitialValuePlugin: React.FC<InitialValuePluginProps> = ({ value, options = [], enableJinja2 = false }) => {
const [editor] = useLexicalComposerContext();
const initializedRef = useRef(false);
const prevValueRef = useRef<string>('');
const isUserInputRef = useRef(false);
useEffect(() => {
if (!initializedRef.current && value) {
// 监听编辑器变化,标记是否为用户输入
const removeListener = editor.registerUpdateListener(({ editorState }) => {
editorState.read(() => {
const root = $getRoot();
const textContent = root.getTextContent();
if (textContent !== prevValueRef.current) {
isUserInputRef.current = true;
}
});
});
return removeListener;
}, [editor]);
useEffect(() => {
if (value !== prevValueRef.current && !isUserInputRef.current) {
editor.update(() => {
const root = $getRoot();
root.clear();
@@ -28,7 +45,11 @@ const InitialValuePlugin: React.FC<InitialValuePluginProps> = ({ value, options
const contextMatch = part.match(/^\{\{context\}\}$/);
const conversationMatch = part.match(/^\{\{conv\.([^}]+)\}\}$/);
// 匹配{{context}}格式
if (enableJinja2) {
paragraph.append($createTextNode(part));
return;
}
if (contextMatch) {
const contextSuggestion = options.find(s => s.isContext && s.label === 'context');
if (contextSuggestion) {
@@ -39,7 +60,6 @@ const InitialValuePlugin: React.FC<InitialValuePluginProps> = ({ value, options
return
}
// 匹配{{conv.xx}}格式
if (conversationMatch) {
const [_, variableName] = conversationMatch;
const conversationSuggestion = options.find(s =>
@@ -53,7 +73,6 @@ const InitialValuePlugin: React.FC<InitialValuePluginProps> = ({ value, options
return
}
// 匹配普通变量{{nodeId.label}}格式
if (match) {
const [_, nodeId, label] = match;
@@ -75,11 +94,12 @@ const InitialValuePlugin: React.FC<InitialValuePluginProps> = ({ value, options
});
root.append(paragraph);
});
initializedRef.current = true;
}, { discrete: true });
}
}, [options]);
prevValueRef.current = value;
isUserInputRef.current = false;
}, [value, options, editor, enableJinja2]);
return null;
};

View File

@@ -0,0 +1,109 @@
import { useEffect } from 'react';
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import { $getRoot, $getSelection, $isRangeSelection, TextNode, $createTextNode } from 'lexical';
const JsonHighlightPlugin = () => {
const [editor] = useLexicalComposerContext();
useEffect(() => {
return editor.registerNodeTransform(TextNode, (textNode: TextNode) => {
const text = textNode.getTextContent();
// Check if text contains JSON-like patterns
if (containsJsonPatterns(text)) {
const parent = textNode.getParent();
if (!parent) return;
// Split text into tokens and create new nodes with appropriate classes
const tokens = tokenizeJson(text);
const newNodes = tokens.map(token => {
const newNode = $createTextNode(token.text);
// Set format based on token type
switch (token.type) {
case 'string':
newNode.setFormat('code');
newNode.setStyle('color: #032f62');
break;
case 'number':
newNode.setFormat('code');
newNode.setStyle('color: #005cc5');
break;
case 'boolean':
newNode.setFormat('code');
newNode.setStyle('color: #d73a49');
break;
case 'null':
newNode.setFormat('code');
newNode.setStyle('color: #6f42c1');
break;
case 'key':
newNode.setFormat('code');
newNode.setStyle('color: #22863a; font-weight: bold');
break;
case 'punctuation':
newNode.setFormat('code');
newNode.setStyle('color: #24292e');
break;
}
return newNode;
});
// Replace the original text node with the new highlighted nodes
if (newNodes.length > 1) {
textNode.replace(newNodes[0]);
for (let i = 1; i < newNodes.length; i++) {
newNodes[i - 1].insertAfter(newNodes[i]);
}
}
}
});
}, [editor]);
return null;
};
function containsJsonPatterns(text: string): boolean {
// Check for JSON-like patterns
return /[{}\[\]:,]/.test(text) ||
/"[^"]*"/.test(text) ||
/\b\d+(\.\d+)?\b/.test(text) ||
/\b(true|false|null)\b/.test(text);
}
function tokenizeJson(text: string): Array<{text: string, type: string}> {
const tokens: Array<{text: string, type: string}> = [];
const regex = /("[^"]*")|([{}\[\]:,])|(\b\d+(?:\.\d+)?\b)|(\b(?:true|false|null)\b)|(\s+)|([^\s{}\[\]:,"]+)/g;
let match;
while ((match = regex.exec(text)) !== null) {
const [fullMatch, string, punctuation, number, boolean, whitespace, other] = match;
if (string) {
// Check if it's a key (followed by colon)
const afterMatch = text.slice(match.index + fullMatch.length).trim();
if (afterMatch.startsWith(':')) {
tokens.push({ text: fullMatch, type: 'key' });
} else {
tokens.push({ text: fullMatch, type: 'string' });
}
} else if (punctuation) {
tokens.push({ text: fullMatch, type: 'punctuation' });
} else if (number) {
tokens.push({ text: fullMatch, type: 'number' });
} else if (boolean) {
if (fullMatch === 'null') {
tokens.push({ text: fullMatch, type: 'null' });
} else {
tokens.push({ text: fullMatch, type: 'boolean' });
}
} else if (whitespace || other) {
tokens.push({ text: fullMatch, type: 'text' });
}
}
return tokens;
}
export default JsonHighlightPlugin;

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')}

View File

@@ -300,6 +300,7 @@ export const nodeLibrary: NodeLibrary[] = [
config: {
cycle_vars: {
type: 'cycleVarsList',
defaultValue: []
},
condition: {
type: 'conditionList',
@@ -395,12 +396,14 @@ export const nodeLibrary: NodeLibrary[] = [
},
retry: {
type: 'switch',
defaultValue: false
defaultValue: {
enable: false
}
},
error_handle: {
type: 'define',
defaultValue: {
method: 'default'
method: 'none'
}
}
}
@@ -420,11 +423,13 @@ export const nodeLibrary: NodeLibrary[] = [
config: {
mapping: {
type: 'mappingList',
defaultValue: []
defaultValue: [{name: 'arg1'}]
},
template: {
type: 'messageEditor',
isArray: false,
enableJinja2: true,
defaultValue: "{{arg1}}"
},
}
}

View File

@@ -193,6 +193,27 @@ export const useWorkflowGraph = ({
nodeConfig.height = newHeight;
}
// 如果是http-request节点检查error_handle.method配置
if (type === 'http-request' && (config as any).error_handle?.method === 'branch') {
const portAttrs = {
circle: {
r: 4, magnet: true, stroke: '#155EEF', strokeWidth: 2, fill: '#155EEF', position: { top: 22 }
},
};
nodeConfig.ports = {
groups: {
right: { position: 'right', attrs: portAttrs },
left: { position: 'left', attrs: portAttrs },
},
items: [
{ group: 'left' },
{ group: 'right', id: 'right' },
{ group: 'right', id: 'ERROR', attrs: { text: { text: t('workflow.config.http-request.errorBranch'), fontSize: 12, fill: '#5B6167' }}}
]
};
}
return nodeConfig
})
@@ -284,6 +305,14 @@ export const useWorkflowGraph = ({
}
}
// 如果是http-request节点且有label根据label匹配对应的端口
if (sourceCell.getData()?.type === 'http-request' && label) {
const matchingPort = sourcePorts.find((port: any) => port.id === label);
if (matchingPort) {
sourcePort = label;
}
}
const edgeConfig = {
source: {
cell: sourceCell.id,
@@ -954,6 +983,23 @@ export const useWorkflowGraph = ({
};
}
// 如果是http-request节点的右侧端口连线添加label
if (sourceCell?.getData()?.type === 'http-request') {
if (sourcePortId === 'ERROR') {
return {
source: sourceCell.getData().id,
target: targetCell?.getData().id,
label: 'ERROR',
};
} else {
return {
source: sourceCell.getData().id,
target: targetCell?.getData().id,
label: 'SUCCESS',
};
}
}
return {
source: sourceCell?.getData().id,
target: targetCell?.getData().id,

View File

@@ -26,6 +26,7 @@ export interface NodeConfig {
group_variables?: Array<{ key: string, value: string[] }>
cycle?: string;
cycle_vars?: Array<{ name: string; type: string; value: string; input_type: string; }>
[key: string]: unknown;
}