feat(web): memory-read、memory-write、iteration、assigner、tool node
This commit is contained in:
@@ -27,5 +27,10 @@ export const execute = (data: ExecuteData) => {
|
|||||||
}
|
}
|
||||||
export const parseSchema = (data: Record<string, any>) => {
|
export const parseSchema = (data: Record<string, any>) => {
|
||||||
return request.post(`/tools/parse_schema`, data)
|
return request.post(`/tools/parse_schema`, data)
|
||||||
|
}
|
||||||
|
export const getToolDetail = (tool_id: string) => {
|
||||||
|
return request.get(`/tools/${tool_id}`)
|
||||||
|
}
|
||||||
|
export const getToolMethods = (tool_id: string) => {
|
||||||
|
return request.get(`/tools/${tool_id}/methods`)
|
||||||
}
|
}
|
||||||
BIN
web/src/assets/images/workflow/assigner.png
Normal file
BIN
web/src/assets/images/workflow/assigner.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 588 B |
@@ -1562,14 +1562,17 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
|
|||||||
loop: 'Loop',
|
loop: 'Loop',
|
||||||
'cycle-start': '',
|
'cycle-start': '',
|
||||||
break: 'Break Loop',
|
break: 'Break Loop',
|
||||||
|
assigner: 'Variable Assignment',
|
||||||
parallel: 'Parallel Execution',
|
parallel: 'Parallel Execution',
|
||||||
'var-aggregator': 'Variable Aggregator',
|
'var-aggregator': 'Variable Aggregator',
|
||||||
externalInteraction: 'External Interaction',
|
externalInteraction: 'External Interaction',
|
||||||
"http-request": 'HTTP Request',
|
"http-request": 'HTTP Request',
|
||||||
tools: 'Tools',
|
tool: 'Tools',
|
||||||
code_execution: 'Code Execution',
|
code_execution: 'Code Execution',
|
||||||
"jinja-render": 'Template Rendering',
|
"jinja-render": 'Template Rendering',
|
||||||
cognitiveUpgrading: 'Cognitive Upgrading (Innovation)',
|
cognitiveUpgrading: 'Cognitive Upgrading (Innovation)',
|
||||||
|
'memory-read': 'Memory Retrieval',
|
||||||
|
'memory-write': 'Memory Storage',
|
||||||
task_planning: 'Task Planning',
|
task_planning: 'Task Planning',
|
||||||
reasoning_control: 'Reasoning Control',
|
reasoning_control: 'Reasoning Control',
|
||||||
self_reflection: 'Self Reflection',
|
self_reflection: 'Self Reflection',
|
||||||
@@ -1714,6 +1717,32 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
|
|||||||
cycle_vars: 'Loop Variables',
|
cycle_vars: 'Loop Variables',
|
||||||
condition: 'Loop Termination Condition',
|
condition: 'Loop Termination Condition',
|
||||||
},
|
},
|
||||||
|
assigner: {
|
||||||
|
assignments: 'Variables',
|
||||||
|
cover: 'Overwrite',
|
||||||
|
assign: 'Set',
|
||||||
|
clear: 'Clear'
|
||||||
|
},
|
||||||
|
iteration: {
|
||||||
|
input: 'Input Variable',
|
||||||
|
output: 'Output Variable',
|
||||||
|
parallel: 'Parallel Mode',
|
||||||
|
parallel_count: 'Max Parallelism',
|
||||||
|
flatten: 'Flatten Output',
|
||||||
|
},
|
||||||
|
tool: {
|
||||||
|
tool_id: 'Tool',
|
||||||
|
},
|
||||||
|
'memory-read': {
|
||||||
|
message: 'Message',
|
||||||
|
config_id: 'Memory Configuration',
|
||||||
|
search_switch: 'Search Mode',
|
||||||
|
},
|
||||||
|
'memory-write': {
|
||||||
|
message: 'Message',
|
||||||
|
config_id: 'Memory Configuration',
|
||||||
|
search_switch: 'Search Mode',
|
||||||
|
},
|
||||||
name: 'Key',
|
name: 'Key',
|
||||||
type: 'Type',
|
type: 'Type',
|
||||||
value: 'Value',
|
value: 'Value',
|
||||||
|
|||||||
@@ -1663,14 +1663,17 @@ export const zh = {
|
|||||||
loop: '循环 (Loop)',
|
loop: '循环 (Loop)',
|
||||||
'cycle-start': '',
|
'cycle-start': '',
|
||||||
break: '退出循环',
|
break: '退出循环',
|
||||||
|
assigner: '变量赋值',
|
||||||
parallel: '并行执行',
|
parallel: '并行执行',
|
||||||
'var-aggregator': '变量聚合器',
|
'var-aggregator': '变量聚合器',
|
||||||
externalInteraction: '外部交互',
|
externalInteraction: '外部交互',
|
||||||
"http-request": 'HTTP请求',
|
"http-request": 'HTTP请求',
|
||||||
tools: '工具 (Tools)',
|
tool: '工具 (Tool)',
|
||||||
code_execution: '代码执行',
|
code_execution: '代码执行',
|
||||||
"jinja-render": '模板渲染',
|
"jinja-render": '模板渲染',
|
||||||
cognitiveUpgrading: '认知升级(创新)',
|
cognitiveUpgrading: '认知升级(创新)',
|
||||||
|
'memory-read': '记忆提取',
|
||||||
|
'memory-write': '记忆储存',
|
||||||
task_planning: '任务规划',
|
task_planning: '任务规划',
|
||||||
reasoning_control: '推理控制',
|
reasoning_control: '推理控制',
|
||||||
self_reflection: '自我反思',
|
self_reflection: '自我反思',
|
||||||
@@ -1815,6 +1818,32 @@ export const zh = {
|
|||||||
cycle_vars: '循环变量',
|
cycle_vars: '循环变量',
|
||||||
condition: '循环终止条件',
|
condition: '循环终止条件',
|
||||||
},
|
},
|
||||||
|
assigner: {
|
||||||
|
assignments: '变量',
|
||||||
|
cover: '覆盖',
|
||||||
|
assign: '设置',
|
||||||
|
clear: '清空'
|
||||||
|
},
|
||||||
|
iteration: {
|
||||||
|
input: '输入变量',
|
||||||
|
output: '输出变量',
|
||||||
|
parallel: '并行模式',
|
||||||
|
parallel_count: '最大并行度',
|
||||||
|
flatten: '扁平化输出',
|
||||||
|
},
|
||||||
|
tool: {
|
||||||
|
tool_id: '工具',
|
||||||
|
},
|
||||||
|
'memory-read': {
|
||||||
|
message: '消息',
|
||||||
|
config_id: '记忆配置',
|
||||||
|
search_switch: '检索模式',
|
||||||
|
},
|
||||||
|
'memory-write': {
|
||||||
|
message: '消息',
|
||||||
|
config_id: '记忆配置',
|
||||||
|
search_switch: '检索模式',
|
||||||
|
},
|
||||||
name: '键',
|
name: '键',
|
||||||
type: '类型',
|
type: '类型',
|
||||||
value: '值',
|
value: '值',
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
type ToolType = 'mcp' | 'builtin' | 'custom'
|
export type ToolType = 'mcp' | 'builtin' | 'custom'
|
||||||
export interface Query {
|
export interface Query {
|
||||||
name?: string;
|
name?: string;
|
||||||
tool_type: ToolType
|
tool_type: ToolType
|
||||||
|
|||||||
@@ -145,16 +145,18 @@ const PortClickHandler: React.FC<PortClickHandlerProps> = ({ graph }) => {
|
|||||||
{nodeLibrary.map((category, categoryIndex) => {
|
{nodeLibrary.map((category, categoryIndex) => {
|
||||||
const sourceNodeData = sourceNode?.getData();
|
const sourceNodeData = sourceNode?.getData();
|
||||||
const isChildOfLoop = sourceNodeData?.cycle && graph?.getNodes().find((n: any) => n.getData()?.id === sourceNodeData.cycle && n.getData()?.type === 'loop');
|
const isChildOfLoop = sourceNodeData?.cycle && graph?.getNodes().find((n: any) => n.getData()?.id === sourceNodeData.cycle && n.getData()?.type === 'loop');
|
||||||
|
const isChildOfIteration = sourceNodeData?.cycle && graph?.getNodes().find((n: any) => n.getData()?.id === sourceNodeData.cycle && n.getData()?.type === 'iteration');
|
||||||
|
|
||||||
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
|
||||||
filteredNodes = category.nodes.filter(nodeType =>
|
filteredNodes = category.nodes.filter(nodeType => !['start', 'end', 'loop', 'cycle-start', 'iteration'].includes(nodeType.type));
|
||||||
nodeType.type !== 'start' && nodeType.type !== 'end' && nodeType.type !== 'loop' && nodeType.type !== 'cycle-start'
|
} else if (isChildOfIteration) {
|
||||||
);
|
// Filter out loop and iteration nodes for children of iteration nodes
|
||||||
|
filteredNodes = category.nodes.filter(nodeType => !['start', 'end', 'loop', 'break', '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 =>
|
filteredNodes = category.nodes.filter(nodeType =>
|
||||||
nodeType.type !== 'start' && nodeType.type !== 'end' && nodeType.type !== 'cycle-start' && nodeType.type !== 'break'
|
nodeType.type !== 'start' && nodeType.type !== 'end' && nodeType.type !== 'cycle-start' && nodeType.type !== 'break'
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
import { type FC } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Form, Input, Button, Row, Col, Select } from 'antd'
|
||||||
|
import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons';
|
||||||
|
import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin'
|
||||||
|
import VariableSelect from '../VariableSelect'
|
||||||
|
|
||||||
|
interface AssignmentListProps {
|
||||||
|
value?: Array<{ variable_selector: string; operation: string[]; value: string;}>;
|
||||||
|
parentName: string;
|
||||||
|
options: Suggestion[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const AssignmentList: FC<AssignmentListProps> = ({
|
||||||
|
parentName,
|
||||||
|
options = [],
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const form = Form.useFormInstance();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form.List name={parentName}>
|
||||||
|
{(fields, { add, remove }) => (
|
||||||
|
<>
|
||||||
|
<div className="rb:flex rb:justify-between">
|
||||||
|
{t(`workflow.config.assigner.${parentName}`)}
|
||||||
|
<PlusOutlined onClick={() => add({ operation: 'cover'})} />
|
||||||
|
</div>
|
||||||
|
{fields.map(({ key, name, ...restField }) => {
|
||||||
|
return (
|
||||||
|
<div key={key} className="rb:mb-4">
|
||||||
|
<Row gutter={12} className="rb:mb-2!">
|
||||||
|
<Col span={14}>
|
||||||
|
<Form.Item
|
||||||
|
{...restField}
|
||||||
|
name={[name, 'variable_selector']}
|
||||||
|
noStyle
|
||||||
|
>
|
||||||
|
<VariableSelect
|
||||||
|
placeholder={t('common.pleaseSelect')}
|
||||||
|
options={options}
|
||||||
|
popupMatchSelectWidth={false}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
<Col span={8}>
|
||||||
|
<Form.Item
|
||||||
|
{...restField}
|
||||||
|
name={[name, 'operation']}
|
||||||
|
noStyle
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
options={[
|
||||||
|
{ value: 'cover', label: t('workflow.config.assigner.cover') },
|
||||||
|
{ value: 'clear', label: t('workflow.config.assigner.clear') },
|
||||||
|
{ value: 'assign', label: t('workflow.config.assigner.assign') },
|
||||||
|
]}
|
||||||
|
popupMatchSelectWidth={false}
|
||||||
|
onChange={() => {
|
||||||
|
form.setFieldValue([parentName, name, 'value'], undefined);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
<Col span={2} className="rb:flex! rb:items-center rb:justify-end">
|
||||||
|
<MinusCircleOutlined onClick={() => remove(name)} />
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<Form.Item shouldUpdate noStyle>
|
||||||
|
{(form) => {
|
||||||
|
const operation = form.getFieldValue([parentName, name, 'operation']);
|
||||||
|
if (operation === 'clear') return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form.Item
|
||||||
|
{...restField}
|
||||||
|
name={[name, 'value']}
|
||||||
|
noStyle
|
||||||
|
rules={[{ required: true, message: 'Missing last name' }]}
|
||||||
|
>
|
||||||
|
{operation === 'assign' ? (
|
||||||
|
<Input.TextArea
|
||||||
|
placeholder={t('common.pleaseEnter')}
|
||||||
|
rows={3}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<VariableSelect
|
||||||
|
placeholder={t('common.pleaseSelect')}
|
||||||
|
options={options}
|
||||||
|
popupMatchSelectWidth={false}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Form.Item>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Form.List>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AssignmentList
|
||||||
@@ -2,16 +2,143 @@ import { type FC } from 'react';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Input, Button, Form, Space } from 'antd';
|
import { Input, Button, Form, Space } from 'antd';
|
||||||
import { PlusOutlined, CopyOutlined, DeleteOutlined, ExpandOutlined } from '@ant-design/icons';
|
import { PlusOutlined, CopyOutlined, DeleteOutlined, ExpandOutlined } from '@ant-design/icons';
|
||||||
|
import { Graph, Node } from '@antv/x6';
|
||||||
|
import type { PortMetadata } from '@antv/x6/lib/model/port';
|
||||||
|
|
||||||
interface CategoryListProps {
|
interface CategoryListProps {
|
||||||
parentName: string;
|
parentName: string;
|
||||||
|
selectedNode?: Node | null;
|
||||||
|
graphRef?: React.MutableRefObject<Graph | undefined>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CategoryList: FC<CategoryListProps> = ({ parentName }) => {
|
const CategoryList: FC<CategoryListProps> = ({ parentName, selectedNode, graphRef }) => {
|
||||||
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);
|
||||||
|
|
||||||
|
const updateNodePorts = (caseCount: number, removedCaseIndex?: number) => {
|
||||||
|
if (!selectedNode || !graphRef?.current) return;
|
||||||
|
|
||||||
|
// 保存现有连线信息(包括左侧端口连线)
|
||||||
|
const existingEdges = graphRef.current.getEdges().filter((edge: any) =>
|
||||||
|
edge.getSourceCellId() === selectedNode.id || edge.getTargetCellId() === selectedNode.id
|
||||||
|
);
|
||||||
|
const edgeConnections = existingEdges.map((edge: any) => ({
|
||||||
|
edge,
|
||||||
|
sourcePortId: edge.getSourcePortId(),
|
||||||
|
targetCellId: edge.getTargetCellId(),
|
||||||
|
targetPortId: edge.getTargetPortId(),
|
||||||
|
sourceCellId: edge.getSourceCellId(),
|
||||||
|
isIncoming: edge.getTargetCellId() === selectedNode.id
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 移除所有现有的右侧端口
|
||||||
|
const existingPorts = selectedNode.getPorts();
|
||||||
|
existingPorts.forEach((port: any) => {
|
||||||
|
if (port.group === 'right') {
|
||||||
|
selectedNode.removePort(port.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 计算新的节点高度:基础高度88px + 每个额外port增加30px
|
||||||
|
const baseHeight = 88;
|
||||||
|
const totalPorts = caseCount + 1; // IF/ELIF + ELSE
|
||||||
|
const newHeight = baseHeight + (totalPorts - 2) * 30;
|
||||||
|
|
||||||
|
selectedNode.prop('size', { width: 240, height: newHeight < baseHeight ? baseHeight : newHeight })
|
||||||
|
|
||||||
|
// 添加 分类 端口
|
||||||
|
for (let i = 0; i < caseCount; i++) {
|
||||||
|
selectedNode.addPort({
|
||||||
|
id: `CASE${i + 1}`,
|
||||||
|
group: 'right',
|
||||||
|
args: i === 0 ? { dy: 24 } : undefined,
|
||||||
|
attrs: { text: { text: `分类${i + 1}`, fontSize: 12, fill: '#5B6167' } }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// 恢复连线
|
||||||
|
setTimeout(() => {
|
||||||
|
edgeConnections.forEach(({ edge, sourcePortId, targetCellId, targetPortId, sourceCellId, isIncoming }: any) => {
|
||||||
|
graphRef.current?.removeCell(edge);
|
||||||
|
|
||||||
|
// 如果是进入连线(左侧端口),直接恢复
|
||||||
|
if (isIncoming) {
|
||||||
|
const sourceCell = graphRef.current?.getCellById(sourceCellId);
|
||||||
|
if (sourceCell) {
|
||||||
|
graphRef.current?.addEdge({
|
||||||
|
source: { cell: sourceCellId, port: sourcePortId },
|
||||||
|
target: { cell: selectedNode.id, port: targetPortId },
|
||||||
|
attrs: {
|
||||||
|
line: {
|
||||||
|
stroke: '#155EEF',
|
||||||
|
strokeWidth: 1,
|
||||||
|
targetMarker: {
|
||||||
|
name: 'block',
|
||||||
|
size: 8,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理右侧端口连线
|
||||||
|
const originalCaseNumber = parseInt(sourcePortId.match(/CASE(\d+)/)?.[1] || '0');
|
||||||
|
|
||||||
|
// 如果是被删除的端口,不重新创建连线
|
||||||
|
if (removedCaseIndex !== undefined && originalCaseNumber === removedCaseIndex + 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let newPortId = sourcePortId;
|
||||||
|
|
||||||
|
// 如果删除了某个端口,需要重新映射后续端口的ID
|
||||||
|
if (removedCaseIndex !== undefined && originalCaseNumber > removedCaseIndex + 1) {
|
||||||
|
newPortId = `CASE${originalCaseNumber - 1}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查新端口是否存在
|
||||||
|
const newPorts = selectedNode.getPorts();
|
||||||
|
const matchingPort = newPorts.find((port: any) => port.id === newPortId);
|
||||||
|
|
||||||
|
if (matchingPort) {
|
||||||
|
const targetCell = graphRef.current?.getCellById(targetCellId);
|
||||||
|
if (targetCell) {
|
||||||
|
graphRef.current?.addEdge({
|
||||||
|
source: { cell: selectedNode.id, port: newPortId },
|
||||||
|
target: { cell: targetCellId, port: targetPortId },
|
||||||
|
attrs: {
|
||||||
|
line: {
|
||||||
|
stroke: '#155EEF',
|
||||||
|
strokeWidth: 1,
|
||||||
|
targetMarker: {
|
||||||
|
name: 'block',
|
||||||
|
size: 8,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, 50);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddCategory = (addFunc: Function) => {
|
||||||
|
addFunc({});
|
||||||
|
setTimeout(() => {
|
||||||
|
updateNodePorts((formValues?.length || 0) + 1);
|
||||||
|
}, 100);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveCategory = (removeFunc: Function, fieldName: number, categoryIndex: number) => {
|
||||||
|
removeFunc(fieldName);
|
||||||
|
setTimeout(() => {
|
||||||
|
updateNodePorts((formValues?.length || 1) - 1, categoryIndex);
|
||||||
|
}, 100);
|
||||||
|
};
|
||||||
|
|
||||||
console.log('formValues', formValues)
|
console.log('formValues', formValues)
|
||||||
return (
|
return (
|
||||||
<Form.List name={parentName}>
|
<Form.List name={parentName}>
|
||||||
@@ -31,7 +158,7 @@ const CategoryList: FC<CategoryListProps> = ({ parentName }) => {
|
|||||||
type="text"
|
type="text"
|
||||||
size="small"
|
size="small"
|
||||||
icon={<DeleteOutlined />}
|
icon={<DeleteOutlined />}
|
||||||
onClick={() => remove(name)}
|
onClick={() => handleRemoveCategory(remove, name, index)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -50,7 +177,7 @@ const CategoryList: FC<CategoryListProps> = ({ parentName }) => {
|
|||||||
|
|
||||||
<Button
|
<Button
|
||||||
type="dashed"
|
type="dashed"
|
||||||
onClick={() => add({})}
|
onClick={() => handleAddCategory(add)}
|
||||||
className="rb:w-full"
|
className="rb:w-full"
|
||||||
>
|
>
|
||||||
+ {t('workflow.config.question-classifier.addClassName')}
|
+ {t('workflow.config.question-classifier.addClassName')}
|
||||||
|
|||||||
@@ -78,7 +78,6 @@ 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
|
||||||
placeholder="输入值"
|
|
||||||
options={options}
|
options={options}
|
||||||
size="small"
|
size="small"
|
||||||
allowClear={false}
|
allowClear={false}
|
||||||
@@ -90,7 +89,6 @@ const ConditionList: FC<CaseListProps> = ({
|
|||||||
<Col span={8}>
|
<Col span={8}>
|
||||||
<Form.Item name={[field.name, 'comparison_operator']} noStyle>
|
<Form.Item name={[field.name, 'comparison_operator']} noStyle>
|
||||||
<Select
|
<Select
|
||||||
placeholder="包含"
|
|
||||||
options={operatorList.map(key => ({
|
options={operatorList.map(key => ({
|
||||||
value: key,
|
value: key,
|
||||||
label: t(`workflow.config.if-else.${key}`)
|
label: t(`workflow.config.if-else.${key}`)
|
||||||
|
|||||||
@@ -80,14 +80,14 @@ const CycleVarsList: FC<CycleVarsListProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="rb:flex rb:items-center rb:justify-between rb:mb-3">
|
|
||||||
<span className="rb:text-sm rb:font-medium">循环变量</span>
|
|
||||||
<PlusOutlined className="rb:text-gray-400 rb:cursor-pointer rb:hover:text-blue-500" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Form.List name={parentName}>
|
<Form.List name={parentName}>
|
||||||
{(fields, { add, remove }) => (
|
{(fields, { add, remove }) => (
|
||||||
<>
|
<>
|
||||||
|
<div className="rb:flex rb:items-center rb:justify-between rb:mb-3">
|
||||||
|
<span className="rb:text-sm rb:font-medium">循环变量</span>
|
||||||
|
<PlusOutlined className="rb:text-gray-400 rb:cursor-pointer rb:hover:text-blue-500" onClick={() => add({ name: '', type: 'string', input_type: 'constant', value: '' })} />
|
||||||
|
</div>
|
||||||
{fields.map(({ key, name, ...field }, index) => {
|
{fields.map(({ key, name, ...field }, index) => {
|
||||||
const currentInputType = value?.[index]?.input_type;
|
const currentInputType = value?.[index]?.input_type;
|
||||||
|
|
||||||
@@ -96,7 +96,7 @@ const CycleVarsList: FC<CycleVarsListProps> = ({
|
|||||||
<Row gutter={8} align="middle" className="rb:mb-2">
|
<Row gutter={8} align="middle" className="rb:mb-2">
|
||||||
<Col span={8}>
|
<Col span={8}>
|
||||||
<Form.Item name={[name, 'name']} noStyle>
|
<Form.Item name={[name, 'name']} noStyle>
|
||||||
<Input placeholder="变量名" size="small" />
|
<Input size="small" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={6}>
|
<Col span={6}>
|
||||||
@@ -153,15 +153,6 @@ const CycleVarsList: FC<CycleVarsListProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
|
||||||
<Button
|
|
||||||
type="dashed"
|
|
||||||
onClick={() => add({ name: '', type: 'string', input_type: 'constant', value: '' })}
|
|
||||||
className="rb:w-full"
|
|
||||||
icon={<PlusOutlined />}
|
|
||||||
>
|
|
||||||
添加变量
|
|
||||||
</Button>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Form.List>
|
</Form.List>
|
||||||
|
|||||||
@@ -0,0 +1,216 @@
|
|||||||
|
import { type FC, useEffect, useState } from "react";
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { Form, Select, InputNumber, Switch, Cascader, type CascaderProps } from 'antd'
|
||||||
|
import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin'
|
||||||
|
import { getToolMethods, getToolDetail, getTools } from '@/api/tools'
|
||||||
|
import type { ToolType, ToolItem } from '@/views/ToolManagement/types'
|
||||||
|
import Editor from "../../Editor";
|
||||||
|
|
||||||
|
interface Option {
|
||||||
|
value?: string | number | null;
|
||||||
|
label?: React.ReactNode;
|
||||||
|
children?: Option[];
|
||||||
|
isLeaf?: boolean;
|
||||||
|
method_id?: string;
|
||||||
|
parameters?: Parameter[];
|
||||||
|
}
|
||||||
|
interface Parameter {
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
description: string;
|
||||||
|
required: boolean;
|
||||||
|
default: any;
|
||||||
|
enum: null | string[];
|
||||||
|
minimum: number;
|
||||||
|
maximum: number;
|
||||||
|
pattern: null | string;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const ToolConfig: FC<{ options: Suggestion[]; }> = ({
|
||||||
|
options,
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const form = Form.useFormInstance();
|
||||||
|
const values = Form.useWatch([], form) || {}
|
||||||
|
const [optionList, setOptionList] = useState<Option[]>([
|
||||||
|
{ value: 'mcp', label: t('tool.mcp'), isLeaf: false },
|
||||||
|
{ value: 'builtin', label: t('tool.inner'), isLeaf: false },
|
||||||
|
{ value: 'custom', label: t('tool.custom'), isLeaf: false },
|
||||||
|
])
|
||||||
|
const [parameters, setParameters] = useState<Parameter[]>([])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (values.tool_id) {
|
||||||
|
getToolDetail(values.tool_id)
|
||||||
|
.then(res => {
|
||||||
|
const detail = res as { tool_type: ToolType; }
|
||||||
|
|
||||||
|
getTools({ tool_type: detail.tool_type })
|
||||||
|
.then(toolsRes => {
|
||||||
|
const tools = toolsRes as ToolItem[]
|
||||||
|
|
||||||
|
getToolMethods(values.tool_id)
|
||||||
|
.then(methodsRes => {
|
||||||
|
const response = methodsRes as Array<{ method_id: string; name: string; parameters: Parameter[] }>
|
||||||
|
|
||||||
|
setOptionList(prevList => {
|
||||||
|
return prevList.map(item => {
|
||||||
|
if (item.value === detail.tool_type) {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
children: tools.map((vo: ToolItem) => ({
|
||||||
|
value: vo.id,
|
||||||
|
label: vo.name,
|
||||||
|
isLeaf: false,
|
||||||
|
children: vo.id === values.tool_id ? response.map(method => ({
|
||||||
|
value: method.name,
|
||||||
|
label: method.name,
|
||||||
|
isLeaf: true,
|
||||||
|
method_id: method.method_id,
|
||||||
|
parameters: method.parameters
|
||||||
|
})) : undefined
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return item
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.length > 1) {
|
||||||
|
const filterTarget = response.find(vo => vo.name === values.tool_parameters?.operation)
|
||||||
|
if (filterTarget) {
|
||||||
|
setParameters([...filterTarget.parameters])
|
||||||
|
} else {
|
||||||
|
setParameters([])
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setParameters([...response[0].parameters])
|
||||||
|
}
|
||||||
|
|
||||||
|
form.setFieldValue('tools', [detail.tool_type, values.tool_id, values.tool_parameters?.operation ?? response[0].name])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [values.tool_id, values.tool_parameters?.operation]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (values.tools && values.tools.length === 3) {
|
||||||
|
const [toolType, toolId, operation] = values.tools
|
||||||
|
|
||||||
|
// 从 optionList 中查找对应的参数
|
||||||
|
const typeOption = optionList.find(opt => opt.value === toolType)
|
||||||
|
if (typeOption?.children) {
|
||||||
|
const toolOption = typeOption.children.find(opt => opt.value === toolId)
|
||||||
|
if (toolOption?.children) {
|
||||||
|
const methodOption = toolOption.children.find(opt => opt.value === operation)
|
||||||
|
if (methodOption?.parameters) {
|
||||||
|
setParameters([...methodOption.parameters])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [values.tools])
|
||||||
|
|
||||||
|
const loadData = (selectedOptions: Option[]) => {
|
||||||
|
const targetOption = selectedOptions[selectedOptions.length - 1];
|
||||||
|
if (selectedOptions.length === 1) {
|
||||||
|
getTools({ tool_type: targetOption.value as ToolType })
|
||||||
|
.then(res => {
|
||||||
|
const response = res as ToolItem[]
|
||||||
|
targetOption.children = response.map((vo: any) => {
|
||||||
|
return {
|
||||||
|
value: vo.id,
|
||||||
|
label: vo.name,
|
||||||
|
isLeaf: response.length === 0,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
setOptionList([...optionList])
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
getToolMethods(targetOption.value as string)
|
||||||
|
.then(res => {
|
||||||
|
const response = res as Array<{ method_id: string; name: string }>
|
||||||
|
targetOption.children = response.map((vo: any) => {
|
||||||
|
return {
|
||||||
|
value: vo.name,
|
||||||
|
label: vo.name,
|
||||||
|
isLeaf: true,
|
||||||
|
method_id: vo.method_id,
|
||||||
|
parameters: vo.parameters
|
||||||
|
}
|
||||||
|
})
|
||||||
|
setOptionList([...optionList])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChange: CascaderProps<Option>['onChange'] = (value, selectedOptions) => {
|
||||||
|
const targetOption = selectedOptions[selectedOptions.length - 1];
|
||||||
|
const curParameters = [...(targetOption.parameters ?? [])]
|
||||||
|
setParameters([...curParameters])
|
||||||
|
const inititalValue: any = { tool_id: selectedOptions[1].value, tool_parameters: {} }
|
||||||
|
|
||||||
|
if (value[0] === 'mcp' || (value[0] === 'builtin' && selectedOptions[1]?.children && selectedOptions[1].children.length > 1)) {
|
||||||
|
inititalValue.tool_parameters.operation = value?.[2]
|
||||||
|
} else if (value[0] === 'custom') {
|
||||||
|
inititalValue.tool_parameters.operation = selectedOptions?.[2].method_id
|
||||||
|
}
|
||||||
|
curParameters.forEach(vo => {
|
||||||
|
inititalValue.tool_parameters[vo.name] = vo.default
|
||||||
|
})
|
||||||
|
|
||||||
|
form.setFieldsValue(inititalValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Form.Item
|
||||||
|
name="tools"
|
||||||
|
label={t('workflow.config.tool.tool_id')}
|
||||||
|
>
|
||||||
|
<Cascader
|
||||||
|
options={optionList}
|
||||||
|
loadData={loadData}
|
||||||
|
onChange={handleChange}
|
||||||
|
changeOnSelect={false}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name="tool_id" hidden />
|
||||||
|
<Form.Item name={['tool_parameters', 'operation']} hidden />
|
||||||
|
{parameters.map((parameter) => {
|
||||||
|
return (
|
||||||
|
<div key={parameter.name}>
|
||||||
|
<Form.Item
|
||||||
|
name={['tool_parameters', parameter.name]}
|
||||||
|
label={parameter.name}
|
||||||
|
extra={parameter.type === 'boolean' ? undefined : parameter.description}
|
||||||
|
rules={[
|
||||||
|
{ required: parameter.required, message: t('workflow.config.tool.required') }
|
||||||
|
]}
|
||||||
|
layout={parameter.type === 'boolean' ? 'horizontal' : 'vertical'}
|
||||||
|
className={parameter.type === 'boolean' ? 'rb:mb-0!' : ''}
|
||||||
|
>
|
||||||
|
{parameter.type === 'string' && parameter.enum && parameter.enum.length > 0
|
||||||
|
? <Select options={parameter.enum.map(vo => ({ value: vo, label: vo }))} placeholder={t('common.pleaseSelect')} />
|
||||||
|
: parameter.type === 'boolean'
|
||||||
|
? <Switch />
|
||||||
|
: parameter.type === 'integer' || parameter.type === 'number'
|
||||||
|
? <InputNumber min={parameter.minimum} max={parameter.maximum} step={parameter.type === 'integer' ? 1 : 0.01} placeholder={t('common.pleaseEnter')} className="rb:w-full!" />
|
||||||
|
: <Editor
|
||||||
|
height={32}
|
||||||
|
variant="outlined"
|
||||||
|
options={options}
|
||||||
|
placeholder={t('common.pleaseEnter')}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</Form.Item>
|
||||||
|
{parameter.type === 'boolean' && <div className="rb:mt-1 rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:leading-4 rb:mb-6">{parameter.description}</div>}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default ToolConfig;
|
||||||
@@ -37,13 +37,17 @@ const VariableSelect: FC<VariableSelectProps> = ({
|
|||||||
})}
|
})}
|
||||||
contentEditable={false}
|
contentEditable={false}
|
||||||
>
|
>
|
||||||
<img
|
{filterOption.nodeData?.icon && filterOption.nodeData?.name && (
|
||||||
src={filterOption.nodeData?.icon}
|
<>
|
||||||
style={{ width: '12px', height: '12px', marginRight: '4px' }}
|
<img
|
||||||
alt=""
|
src={filterOption.nodeData.icon}
|
||||||
/>
|
style={{ width: '12px', height: '12px', marginRight: '4px' }}
|
||||||
{filterOption.nodeData?.name}
|
alt=""
|
||||||
<span className="rb:text-[#DFE4ED] rb:mx-0.5">/</span>
|
/>
|
||||||
|
{filterOption.nodeData.name}
|
||||||
|
<span className="rb:text-[#DFE4ED] rb:mx-0.5">/</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<span className="rb:text-[#155EEF]">{filterOption.label}</span>
|
<span className="rb:text-[#155EEF]">{filterOption.label}</span>
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ import MappingList from './MappingList'
|
|||||||
import CategoryList from './CategoryList'
|
import CategoryList from './CategoryList'
|
||||||
import ConditionList from './ConditionList'
|
import ConditionList from './ConditionList'
|
||||||
import CycleVarsList from './CycleVarsList'
|
import CycleVarsList from './CycleVarsList'
|
||||||
|
import AssignmentList from './AssignmentList'
|
||||||
|
import ToolConfig from './ToolConfig'
|
||||||
|
|
||||||
interface PropertiesProps {
|
interface PropertiesProps {
|
||||||
selectedNode?: Node | null;
|
selectedNode?: Node | null;
|
||||||
@@ -183,9 +185,19 @@ const Properties: FC<PropertiesProps> = ({
|
|||||||
return allPrevious;
|
return allPrevious;
|
||||||
};
|
};
|
||||||
|
|
||||||
const allPreviousNodeIds = getAllPreviousNodes(selectedNode.id);
|
// Find child nodes (nodes whose cycle field equals current node's ID)
|
||||||
|
const getChildNodes = (nodeId: string): string[] => {
|
||||||
|
return nodes
|
||||||
|
.filter(node => node.getData()?.cycle === nodeId)
|
||||||
|
.map(node => node.id);
|
||||||
|
};
|
||||||
|
|
||||||
allPreviousNodeIds.forEach(nodeId => {
|
const allPreviousNodeIds = getAllPreviousNodes(selectedNode.id);
|
||||||
|
const childNodeIds = getChildNodes(selectedNode.id);
|
||||||
|
console.log('childNodeIds', childNodeIds)
|
||||||
|
const allRelevantNodeIds = [...allPreviousNodeIds, ...childNodeIds];
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
@@ -250,7 +262,7 @@ const Properties: FC<PropertiesProps> = ({
|
|||||||
key: knowledgeKey,
|
key: knowledgeKey,
|
||||||
label: 'message',
|
label: 'message',
|
||||||
type: 'variable',
|
type: 'variable',
|
||||||
dataType: 'String',
|
dataType: 'array[object]',
|
||||||
value: `${nodeId}.message`,
|
value: `${nodeId}.message`,
|
||||||
nodeData: nodeData,
|
nodeData: nodeData,
|
||||||
});
|
});
|
||||||
@@ -282,6 +294,7 @@ const Properties: FC<PropertiesProps> = ({
|
|||||||
}, [selectedNode, graphRef]);
|
}, [selectedNode, graphRef]);
|
||||||
|
|
||||||
console.log('values', values)
|
console.log('values', values)
|
||||||
|
console.log('variableList', variableList, selectedNode?.data)
|
||||||
|
|
||||||
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">
|
||||||
@@ -302,12 +315,18 @@ const Properties: FC<PropertiesProps> = ({
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
{selectedNode?.data?.type === 'http-request'
|
{selectedNode?.data?.type === 'http-request'
|
||||||
? <HttpRequest
|
? <HttpRequest
|
||||||
options={variableList}
|
options={variableList}
|
||||||
/>
|
/>
|
||||||
|
: selectedNode?.data?.type === 'tool'
|
||||||
|
? <ToolConfig options={variableList} />
|
||||||
: configs && Object.keys(configs).length > 0 && Object.keys(configs).map((key) => {
|
: configs && Object.keys(configs).length > 0 && Object.keys(configs).map((key) => {
|
||||||
const config = configs[key] || {}
|
const config = configs[key] || {}
|
||||||
|
|
||||||
|
if (config.dependsOn && (values as any)?.[config.dependsOn as string] !== config.dependsOnValue) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
if (selectedNode?.data?.type === 'start' && key === 'variables' && config.type === 'define') {
|
if (selectedNode?.data?.type === 'start' && key === 'variables' && config.type === 'define') {
|
||||||
return (
|
return (
|
||||||
<div key={key}>
|
<div key={key}>
|
||||||
@@ -433,7 +452,7 @@ const Properties: FC<PropertiesProps> = ({
|
|||||||
<GroupVariableList
|
<GroupVariableList
|
||||||
name={key}
|
name={key}
|
||||||
options={variableList}
|
options={variableList}
|
||||||
isCanAdd={!!values?.group}
|
isCanAdd={!!(values as any)?.group}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
@@ -472,6 +491,84 @@ const Properties: FC<PropertiesProps> = ({
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if (config.type === 'assignmentList') {
|
||||||
|
return (
|
||||||
|
<Form.Item key={key} name={key}>
|
||||||
|
<AssignmentList
|
||||||
|
parentName={key}
|
||||||
|
options={(() => {
|
||||||
|
if (config.filterLoopIterationVars) {
|
||||||
|
// Add loop cycle variables and iteration item/index variables
|
||||||
|
const loopIterationVars: Suggestion[] = [];
|
||||||
|
const graph = graphRef.current;
|
||||||
|
if (graph && selectedNode) {
|
||||||
|
const nodes = graph.getNodes();
|
||||||
|
|
||||||
|
// Find parent loop/iteration nodes
|
||||||
|
const findParentLoopIteration = (nodeId: string): string[] => {
|
||||||
|
const node = nodes.find(n => n.id === nodeId);
|
||||||
|
if (!node) return [];
|
||||||
|
|
||||||
|
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') {
|
||||||
|
console.log('parentData', parentData)
|
||||||
|
// Add cycle variables from loop node
|
||||||
|
const cycleVars = parentData.cycle_vars || [];
|
||||||
|
cycleVars.forEach((cycleVar: any) => {
|
||||||
|
loopIterationVars.push({
|
||||||
|
key: `${cycle}_cycle_${cycleVar.name}`,
|
||||||
|
label: cycleVar.name,
|
||||||
|
type: 'variable',
|
||||||
|
dataType: 'String',
|
||||||
|
value: `${cycle}.${cycleVar.name}`,
|
||||||
|
nodeData: parentData,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else if (parentData?.type === 'iteration') {
|
||||||
|
// Add item and index variables from iteration node
|
||||||
|
loopIterationVars.push(
|
||||||
|
{
|
||||||
|
key: `${cycle}_item`,
|
||||||
|
label: 'item',
|
||||||
|
type: 'variable',
|
||||||
|
dataType: 'Object',
|
||||||
|
value: `${cycle}.item`,
|
||||||
|
nodeData: parentData,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: `${cycle}_index`,
|
||||||
|
label: 'index',
|
||||||
|
type: 'variable',
|
||||||
|
dataType: 'Number',
|
||||||
|
value: `${cycle}.index`,
|
||||||
|
nodeData: parentData,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return [cycle, ...findParentLoopIteration(cycle)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
|
||||||
|
findParentLoopIteration(selectedNode.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...variableList, ...loopIterationVars];
|
||||||
|
}
|
||||||
|
return variableList;
|
||||||
|
})()
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form.Item
|
<Form.Item
|
||||||
@@ -486,7 +583,7 @@ const Properties: FC<PropertiesProps> = ({
|
|||||||
? <Input.TextArea placeholder={t('common.pleaseEnter')} />
|
? <Input.TextArea placeholder={t('common.pleaseEnter')} />
|
||||||
: config.type === 'select'
|
: config.type === 'select'
|
||||||
? <Select
|
? <Select
|
||||||
options={config.options}
|
options={config.needTranslation ? config.options?.map(vo => ({ ...vo, label: t(vo.label) })) : config.options}
|
||||||
placeholder={t('common.pleaseSelect')}
|
placeholder={t('common.pleaseSelect')}
|
||||||
/>
|
/>
|
||||||
: config.type === 'inputNumber'
|
: config.type === 'inputNumber'
|
||||||
@@ -505,12 +602,42 @@ const Properties: FC<PropertiesProps> = ({
|
|||||||
: config.type === 'variableList'
|
: config.type === 'variableList'
|
||||||
? <VariableSelect
|
? <VariableSelect
|
||||||
placeholder={t('common.pleaseSelect')}
|
placeholder={t('common.pleaseSelect')}
|
||||||
options={variableList}
|
options={(() => {
|
||||||
|
// Apply filtering if specified in config
|
||||||
|
if (config.filterNodeTypes || config.filterVariableNames) {
|
||||||
|
return variableList.filter(variable => {
|
||||||
|
const nodeTypeMatch = !config.filterNodeTypes ||
|
||||||
|
(Array.isArray(config.filterNodeTypes) && config.filterNodeTypes.includes(variable.nodeData?.type));
|
||||||
|
const variableNameMatch = !config.filterVariableNames ||
|
||||||
|
(Array.isArray(config.filterVariableNames) && config.filterVariableNames.includes(variable.label));
|
||||||
|
return nodeTypeMatch && variableNameMatch;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Filter child nodes for iteration output
|
||||||
|
if (config.filterChildNodes && selectedNode) {
|
||||||
|
const graph = graphRef.current;
|
||||||
|
if (!graph) return [];
|
||||||
|
|
||||||
|
const nodes = graph.getNodes();
|
||||||
|
|
||||||
|
// Find child nodes whose cycle field equals parent node's ID
|
||||||
|
const childNodes = nodes.filter(node => {
|
||||||
|
const nodeData = node.getData();
|
||||||
|
return nodeData?.cycle === selectedNode.id;
|
||||||
|
});
|
||||||
|
|
||||||
|
return variableList.filter(variable =>
|
||||||
|
childNodes.some(node => node.id === variable.nodeData?.id)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return variableList;
|
||||||
|
})()
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
: config.type === 'switch'
|
: config.type === 'switch'
|
||||||
? <Switch />
|
? <Switch />
|
||||||
: config.type === 'categoryList'
|
: config.type === 'categoryList'
|
||||||
? <CategoryList parentName={key} />
|
? <CategoryList parentName={key} selectedNode={selectedNode} graphRef={graphRef} />
|
||||||
: config.type === 'conditionList'
|
: config.type === 'conditionList'
|
||||||
? <ConditionList
|
? <ConditionList
|
||||||
parentName={key}
|
parentName={key}
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ import selfOptimizationIcon from '@/assets/images/workflow/self_optimization.png
|
|||||||
import processEvolutionIcon from '@/assets/images/workflow/process_evolution.png';
|
import processEvolutionIcon from '@/assets/images/workflow/process_evolution.png';
|
||||||
import questionClassifierIcon from '@/assets/images/workflow/question-classifier.png'
|
import questionClassifierIcon from '@/assets/images/workflow/question-classifier.png'
|
||||||
import breakIcon from '@/assets/images/workflow/break.png'
|
import breakIcon from '@/assets/images/workflow/break.png'
|
||||||
|
import assignerIcon from '@/assets/images/workflow/assigner.png'
|
||||||
|
import { memoryConfigListUrl } from '@/api/memory'
|
||||||
|
|
||||||
import { getModelListUrl } from '@/api/models'
|
import { getModelListUrl } from '@/api/models'
|
||||||
import type { NodeLibrary } from './types'
|
import type { NodeLibrary } from './types'
|
||||||
@@ -169,15 +171,49 @@ export const nodeLibrary: NodeLibrary[] = [
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
// {
|
{
|
||||||
// category: "cognitiveUpgrading",
|
category: "cognitiveUpgrading",
|
||||||
// nodes: [
|
nodes: [
|
||||||
// { type: "task_planning", icon: taskPlanningIcon },
|
{
|
||||||
// { type: "reasoning_control", icon: reasoningControlIcon },
|
type: "memory-read", icon: memoryEnhancementIcon,
|
||||||
// { type: "self_reflection", icon: selfReflectionIcon },
|
config: {
|
||||||
// { type: "memory_enhancement", icon: memoryEnhancementIcon }
|
message: {
|
||||||
// ]
|
type: 'messageEditor',
|
||||||
// },
|
isArray: false
|
||||||
|
},
|
||||||
|
config_id: {
|
||||||
|
type: 'customSelect',
|
||||||
|
url: memoryConfigListUrl,
|
||||||
|
valueKey: 'config_id',
|
||||||
|
labelKey: 'config_name'
|
||||||
|
},
|
||||||
|
search_switch: {
|
||||||
|
type: 'select',
|
||||||
|
options: [
|
||||||
|
{ value: '0', label: 'memoryConversation.deepThinking' },
|
||||||
|
{ value: '1', label: 'memoryConversation.normalReply' },
|
||||||
|
{ value: '2', label: 'memoryConversation.quickReply' },
|
||||||
|
],
|
||||||
|
needTranslation: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ type: "memory-write", icon: memoryEnhancementIcon,
|
||||||
|
config: {
|
||||||
|
message: {
|
||||||
|
type: 'messageEditor',
|
||||||
|
isArray: false
|
||||||
|
},
|
||||||
|
config_id: {
|
||||||
|
type: 'customSelect',
|
||||||
|
url: memoryConfigListUrl,
|
||||||
|
valueKey: 'config_id',
|
||||||
|
labelKey: 'config_name'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
// {
|
// {
|
||||||
// category: "agentCollaborationNode",
|
// category: "agentCollaborationNode",
|
||||||
// nodes: [
|
// nodes: [
|
||||||
@@ -202,8 +238,7 @@ export const nodeLibrary: NodeLibrary[] = [
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{ type: "question-classifier", icon: questionClassifierIcon,
|
||||||
type: "question-classifier", icon: questionClassifierIcon,
|
|
||||||
config: {
|
config: {
|
||||||
model_id: {
|
model_id: {
|
||||||
type: 'customSelect',
|
type: 'customSelect',
|
||||||
@@ -216,7 +251,11 @@ export const nodeLibrary: NodeLibrary[] = [
|
|||||||
type: 'variableList',
|
type: 'variableList',
|
||||||
},
|
},
|
||||||
categories: {
|
categories: {
|
||||||
type: 'categoryList'
|
type: 'categoryList',
|
||||||
|
defaultValue: [
|
||||||
|
{},
|
||||||
|
{}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
user_supplement_prompt: {
|
user_supplement_prompt: {
|
||||||
type: 'messageEditor',
|
type: 'messageEditor',
|
||||||
@@ -224,7 +263,34 @@ export const nodeLibrary: NodeLibrary[] = [
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// { type: "iteration", icon: iterationIcon },
|
{ type: "iteration", icon: iterationIcon,
|
||||||
|
config: {
|
||||||
|
input: {
|
||||||
|
type: 'variableList',
|
||||||
|
filterNodeTypes: ['knowledge-retrieval'],
|
||||||
|
filterVariableNames: ['message']
|
||||||
|
},
|
||||||
|
parallel: {
|
||||||
|
type: 'switch',
|
||||||
|
},
|
||||||
|
parallel_count: {
|
||||||
|
type: 'slider',
|
||||||
|
min: 1,
|
||||||
|
max: 10,
|
||||||
|
step: 1,
|
||||||
|
defaultValue: 10,
|
||||||
|
dependsOn: 'parallel',
|
||||||
|
dependsOnValue: true
|
||||||
|
},
|
||||||
|
flatten: { // 扁平化输出
|
||||||
|
type: 'switch',
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
type: 'variableList',
|
||||||
|
filterChildNodes: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
{ type: "loop", icon: loopIcon,
|
{ type: "loop", icon: loopIcon,
|
||||||
config: {
|
config: {
|
||||||
cycle_vars: {
|
cycle_vars: {
|
||||||
@@ -254,7 +320,16 @@ export const nodeLibrary: NodeLibrary[] = [
|
|||||||
defaultValue: [{ key: 'Group1', value: []}]
|
defaultValue: [{ key: 'Group1', value: []}]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
{
|
||||||
|
type: "assigner", icon: assignerIcon,
|
||||||
|
config: {
|
||||||
|
assignments: {
|
||||||
|
type: 'assignmentList',
|
||||||
|
filterLoopIterationVars: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -317,7 +392,16 @@ export const nodeLibrary: NodeLibrary[] = [
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// { type: "tools", icon: toolsIcon },
|
{ type: "tool", icon: toolsIcon,
|
||||||
|
config: {
|
||||||
|
tool_id: {
|
||||||
|
type: 'cascader'
|
||||||
|
},
|
||||||
|
tool_parameters: {
|
||||||
|
type: 'define'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
// { type: "code_execution", icon: codeExecutionIcon },
|
// { type: "code_execution", icon: codeExecutionIcon },
|
||||||
{ type: "jinja-render", icon: templateRenderingIcon,
|
{ type: "jinja-render", icon: templateRenderingIcon,
|
||||||
config: {
|
config: {
|
||||||
@@ -354,13 +438,13 @@ export const nodeRegisterLibrary: ReactShapeConfig[] = [
|
|||||||
{
|
{
|
||||||
shape: 'loop-node',
|
shape: 'loop-node',
|
||||||
width: 240,
|
width: 240,
|
||||||
height: 80,
|
height: 120,
|
||||||
component: LoopNode,
|
component: LoopNode,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
shape: 'iteration-node',
|
shape: 'iteration-node',
|
||||||
width: 200,
|
width: 240,
|
||||||
height: 200,
|
height: 120,
|
||||||
component: LoopNode,
|
component: LoopNode,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -421,7 +505,7 @@ const defaultPortItems = [
|
|||||||
export const graphNodeLibrary: Record<string, NodeConfig> = {
|
export const graphNodeLibrary: Record<string, NodeConfig> = {
|
||||||
iteration: {
|
iteration: {
|
||||||
width: 240,
|
width: 240,
|
||||||
height: 200,
|
height: 120,
|
||||||
shape: 'iteration-node',
|
shape: 'iteration-node',
|
||||||
ports: {
|
ports: {
|
||||||
groups: defaultPortGroups,
|
groups: defaultPortGroups,
|
||||||
@@ -450,6 +534,19 @@ export const graphNodeLibrary: Record<string, NodeConfig> = {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
'question-classifier': {
|
||||||
|
width: 240,
|
||||||
|
height: 88,
|
||||||
|
shape: 'condition-node',
|
||||||
|
ports: {
|
||||||
|
groups: defaultPortGroups,
|
||||||
|
items: [
|
||||||
|
{ group: 'left' },
|
||||||
|
{ group: 'right', id: 'CASE1', args: { dy: 24 }, attrs: { text: { text: '分类1', fontSize: 12, color: '#5B6167' } } },
|
||||||
|
{ group: 'right', id: 'CASE2', attrs: { text: { text: '分类2', fontSize: 12, color: '#5B6167' } } }
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
start: {
|
start: {
|
||||||
width: 240,
|
width: 240,
|
||||||
height: 64,
|
height: 64,
|
||||||
|
|||||||
@@ -156,6 +156,43 @@ export const useWorkflowGraph = ({
|
|||||||
nodeConfig.height = newHeight;
|
nodeConfig.height = newHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 如果是question-classifier节点,根据categories动态生成端口
|
||||||
|
if (type === 'question-classifier' && config.categories && Array.isArray(config.categories)) {
|
||||||
|
const categoryCount = config.categories.length;
|
||||||
|
const baseHeight = 88;
|
||||||
|
const newHeight = baseHeight + (categoryCount - 1) * 30;
|
||||||
|
|
||||||
|
const portAttrs = {
|
||||||
|
circle: {
|
||||||
|
r: 4, magnet: true, stroke: '#155EEF', strokeWidth: 2, fill: '#155EEF', position: { top: 22 }
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const portItems: PortMetadata[] = [
|
||||||
|
{ group: 'left' }
|
||||||
|
];
|
||||||
|
|
||||||
|
// 添加分类端口
|
||||||
|
config.categories.forEach((category: any, index: number) => {
|
||||||
|
portItems.push({
|
||||||
|
group: 'right',
|
||||||
|
id: `CASE${index + 1}`,
|
||||||
|
args: index === 0 ? { dy: 24 } : undefined,
|
||||||
|
attrs: { text: { text: category.class_name || `分类${index + 1}`, fontSize: 12, fill: '#5B6167' }}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
nodeConfig.ports = {
|
||||||
|
groups: {
|
||||||
|
right: { position: 'right', attrs: portAttrs },
|
||||||
|
left: { position: 'left', attrs: portAttrs },
|
||||||
|
},
|
||||||
|
items: portItems
|
||||||
|
};
|
||||||
|
|
||||||
|
nodeConfig.height = newHeight;
|
||||||
|
}
|
||||||
|
|
||||||
return nodeConfig
|
return nodeConfig
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -239,6 +276,14 @@ export const useWorkflowGraph = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 如果是question-classifier节点且有label,根据label匹配对应的端口
|
||||||
|
if (sourceCell.getData()?.type === 'question-classifier' && label) {
|
||||||
|
const matchingPort = sourcePorts.find((port: any) => port.id === label);
|
||||||
|
if (matchingPort) {
|
||||||
|
sourcePort = label;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const edgeConfig = {
|
const edgeConfig = {
|
||||||
source: {
|
source: {
|
||||||
cell: sourceCell.id,
|
cell: sourceCell.id,
|
||||||
@@ -881,6 +926,15 @@ export const useWorkflowGraph = ({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 如果是question-classifier节点的右侧端口连线,添加label
|
||||||
|
if (sourceCell?.getData()?.type === 'question-classifier' && sourcePortId?.startsWith('CASE')) {
|
||||||
|
return {
|
||||||
|
source: sourceCell.getData().id,
|
||||||
|
target: targetCell?.getData().id,
|
||||||
|
label: sourcePortId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
source: sourceCell?.getData().id,
|
source: sourceCell?.getData().id,
|
||||||
target: targetCell?.getData().id,
|
target: targetCell?.getData().id,
|
||||||
|
|||||||
Reference in New Issue
Block a user