From 3e71e4d15e37e5f3a5832d88357f9eb2ec2eb514 Mon Sep 17 00:00:00 2001 From: zhaoying Date: Mon, 5 Jan 2026 16:18:04 +0800 Subject: [PATCH] =?UTF-8?q?feat(web):=20memory-read=E3=80=81memory-write?= =?UTF-8?q?=E3=80=81iteration=E3=80=81assigner=E3=80=81tool=20node?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/src/api/tools.ts | 7 +- web/src/assets/images/workflow/assigner.png | Bin 0 -> 588 bytes web/src/i18n/en.ts | 31 ++- web/src/i18n/zh.ts | 31 ++- web/src/views/ToolManagement/types.ts | 2 +- .../Workflow/components/PortClickHandler.tsx | 10 +- .../Properties/AssignmentList/index.tsx | 107 +++++++++ .../Properties/CategoryList/index.tsx | 133 ++++++++++- .../Properties/ConditionList/index.tsx | 2 - .../Properties/CycleVarsList/index.tsx | 19 +- .../Properties/ToolConfig/index.tsx | 216 ++++++++++++++++++ .../components/Properties/VariableSelect.tsx | 18 +- .../Workflow/components/Properties/index.tsx | 147 +++++++++++- web/src/views/Workflow/constant.ts | 135 +++++++++-- .../views/Workflow/hooks/useWorkflowGraph.ts | 54 +++++ 15 files changed, 849 insertions(+), 63 deletions(-) create mode 100644 web/src/assets/images/workflow/assigner.png create mode 100644 web/src/views/Workflow/components/Properties/AssignmentList/index.tsx create mode 100644 web/src/views/Workflow/components/Properties/ToolConfig/index.tsx diff --git a/web/src/api/tools.ts b/web/src/api/tools.ts index 142d4c41..b14905f8 100644 --- a/web/src/api/tools.ts +++ b/web/src/api/tools.ts @@ -27,5 +27,10 @@ export const execute = (data: ExecuteData) => { } export const parseSchema = (data: Record) => { 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`) } \ No newline at end of file diff --git a/web/src/assets/images/workflow/assigner.png b/web/src/assets/images/workflow/assigner.png new file mode 100644 index 0000000000000000000000000000000000000000..4370bfdd2c0e5216e29fa7ca7009cb2ded1538aa GIT binary patch literal 588 zcmV-S0<-;zP)xj z|GAX^yqN#Jn*Y9>|G}XD#HIhptN+lq|I)hu+QfitC;Q#LB|M2Pm^6UTf?f>=g|M&9$`S<_&`2YR;|NZ^{{{8>||9kx+_W%F@4RlgY zQvezqPHS$Pw%PLY{QYtMO$h)10VPR9K~y-)rIp!IgFp~PnkwNo+zZ#>&za{wMAo>BP-9?g9x(CavN*d60|E%?t*EEdU#@ = ({ graph }) => { {nodeLibrary.map((category, categoryIndex) => { const sourceNodeData = sourceNode?.getData(); 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; if (isChildOfLoop) { // Use same filtering as AddNode for child nodes of loop - filteredNodes = category.nodes.filter(nodeType => - nodeType.type !== 'start' && nodeType.type !== 'end' && nodeType.type !== 'loop' && nodeType.type !== 'cycle-start' - ); - + filteredNodes = category.nodes.filter(nodeType => !['start', 'end', 'loop', 'cycle-start', 'iteration'].includes(nodeType.type)); + } 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 { // 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 => nodeType.type !== 'start' && nodeType.type !== 'end' && nodeType.type !== 'cycle-start' && nodeType.type !== 'break' ); diff --git a/web/src/views/Workflow/components/Properties/AssignmentList/index.tsx b/web/src/views/Workflow/components/Properties/AssignmentList/index.tsx new file mode 100644 index 00000000..34c133c7 --- /dev/null +++ b/web/src/views/Workflow/components/Properties/AssignmentList/index.tsx @@ -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 = ({ + parentName, + options = [], +}) => { + const { t } = useTranslation(); + const form = Form.useFormInstance(); + + return ( + + {(fields, { add, remove }) => ( + <> +
+ {t(`workflow.config.assigner.${parentName}`)} + add({ operation: 'cover'})} /> +
+ {fields.map(({ key, name, ...restField }) => { + return ( +
+ + + + + + + + + ({ value: key, label: t(`workflow.config.if-else.${key}`) diff --git a/web/src/views/Workflow/components/Properties/CycleVarsList/index.tsx b/web/src/views/Workflow/components/Properties/CycleVarsList/index.tsx index a4186d7b..367e8483 100644 --- a/web/src/views/Workflow/components/Properties/CycleVarsList/index.tsx +++ b/web/src/views/Workflow/components/Properties/CycleVarsList/index.tsx @@ -80,14 +80,14 @@ const CycleVarsList: FC = ({ return (
-
- 循环变量 - -
{(fields, { add, remove }) => ( <> +
+ 循环变量 + add({ name: '', type: 'string', input_type: 'constant', value: '' })} /> +
{fields.map(({ key, name, ...field }, index) => { const currentInputType = value?.[index]?.input_type; @@ -96,7 +96,7 @@ const CycleVarsList: FC = ({ - + @@ -153,15 +153,6 @@ const CycleVarsList: FC = ({
) })} - - )} diff --git a/web/src/views/Workflow/components/Properties/ToolConfig/index.tsx b/web/src/views/Workflow/components/Properties/ToolConfig/index.tsx new file mode 100644 index 00000000..0cba5596 --- /dev/null +++ b/web/src/views/Workflow/components/Properties/ToolConfig/index.tsx @@ -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([ + { 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([]) + + 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