From ca8d5f5cc39441038cd24212249aa99e16b7dfeb Mon Sep 17 00:00:00 2001 From: zhaoying Date: Tue, 30 Dec 2025 18:54:14 +0800 Subject: [PATCH] =?UTF-8?q?feat(web):=20add=20http-request=E3=80=81jinja-r?= =?UTF-8?q?ender=20node?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Workflow/components/Editor/index.tsx | 15 +- .../components/Editor/nodes/VariableNode.tsx | 4 +- .../views/Workflow/components/NodeLibrary.tsx | 2 +- .../Workflow/components/Nodes/LoopNode.tsx | 63 ++-- .../HttpRequest/AuthConfigModal.tsx | 141 +++++++ .../Properties/HttpRequest/EditableTable.tsx | 232 ++++++++++++ .../Properties/HttpRequest/index.tsx | 272 ++++++++++++++ .../Properties/HttpRequest/types.ts | 44 +++ .../Properties/MappingList/index.tsx | 47 +++ .../Workflow/components/Properties/index.tsx | 349 ++++++++++-------- web/src/views/Workflow/constant.ts | 85 ++++- .../views/Workflow/hooks/useWorkflowGraph.ts | 8 +- 12 files changed, 1041 insertions(+), 221 deletions(-) create mode 100644 web/src/views/Workflow/components/Properties/HttpRequest/AuthConfigModal.tsx create mode 100644 web/src/views/Workflow/components/Properties/HttpRequest/EditableTable.tsx create mode 100644 web/src/views/Workflow/components/Properties/HttpRequest/index.tsx create mode 100644 web/src/views/Workflow/components/Properties/HttpRequest/types.ts create mode 100644 web/src/views/Workflow/components/Properties/MappingList/index.tsx diff --git a/web/src/views/Workflow/components/Editor/index.tsx b/web/src/views/Workflow/components/Editor/index.tsx index a882207a..2d12f3f0 100644 --- a/web/src/views/Workflow/components/Editor/index.tsx +++ b/web/src/views/Workflow/components/Editor/index.tsx @@ -21,6 +21,8 @@ interface LexicalEditorProps { value?: string; onChange?: (value: string) => void; options: Suggestion[]; + variant?: 'outlined' | 'borderless'; + height?: number; } const theme = { @@ -36,6 +38,8 @@ const Editor: FC =({ value = "", onChange, options, + variant = 'borderless', + height = 60, }) => { const [_count, setCount] = useState(0); const initialConfig = { @@ -62,9 +66,10 @@ const Editor: FC =({ contentEditable={ =({
= ({ style={{ width: '12px', height: '12px', marginRight: '4px' }} alt="" /> - {data.nodeData?.name} + {data.nodeData?.name} / - {data.label} + {data.label} ); }; diff --git a/web/src/views/Workflow/components/NodeLibrary.tsx b/web/src/views/Workflow/components/NodeLibrary.tsx index 20ef8937..a43c65eb 100644 --- a/web/src/views/Workflow/components/NodeLibrary.tsx +++ b/web/src/views/Workflow/components/NodeLibrary.tsx @@ -10,7 +10,7 @@ const NodeLibrary: FC = () => { console.log('nodeLibrary', nodeLibrary) return ( -
+
{nodeLibrary.map(category => ( { - const data = node.getData() as NodeData; + const data = node.getData() || {}; + const { t } = useTranslation() useEffect(() => { initNodes() @@ -53,46 +55,29 @@ const LoopNode: ReactShapeConfig['component'] = ({ node, graph }) => { node.addChild(childNode1) node.addChild(childNode2) } - - return ( -
- {/* 标题区域 */} -
-
- ♻️ + + return ( +
+
+
+ +
{data.name ?? t(`workflow.${data.type}`)}
+
+ +
{ + e.stopPropagation() + node.remove() + }} + >
- 循环 +
- - - - - {/* 画布内容区域 */} -
-
- ); + ); }; export default LoopNode; diff --git a/web/src/views/Workflow/components/Properties/HttpRequest/AuthConfigModal.tsx b/web/src/views/Workflow/components/Properties/HttpRequest/AuthConfigModal.tsx new file mode 100644 index 00000000..85df4d87 --- /dev/null +++ b/web/src/views/Workflow/components/Properties/HttpRequest/AuthConfigModal.tsx @@ -0,0 +1,141 @@ +import { forwardRef, useEffect, useImperativeHandle, useState } from 'react'; +import { Form, Select, Input } from 'antd'; +import { useTranslation } from 'react-i18next'; + +import type { AuthConfigModalRef, HttpRequestConfigForm } from './types' +import RbModal from '@/components/RbModal' + +const FormItem = Form.Item; + +interface AuthConfigModalProps { + refresh: (values: HttpRequestConfigForm['auth']) => void; +} + +const AuthConfigModal = forwardRef(({ + refresh, +}, ref) => { + const { t } = useTranslation(); + const [visible, setVisible] = useState(false); + const [form] = Form.useForm(); + + const values = Form.useWatch([], form); + + // 封装取消方法,添加关闭弹窗逻辑 + const handleClose = () => { + setVisible(false); + form.resetFields(); + }; + + const handleOpen = (data?: HttpRequestConfigForm['auth']) => { + if (data) { + form.setFieldsValue({ + 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 + }) + } + setVisible(true); + }; + // 封装保存方法,添加提交逻辑 + const handleSave = () => { + form + .validateFields() + .then(() => { + const { auth, auth_type, ...rest } = values ?? {} + refresh({ + auth_type: auth === 'none' ? 'none' : auth_type, + ...rest + }) + handleClose() + }) + .catch((err) => { + console.log('err', err) + }); + } + + // 暴露给父组件的方法 + useImperativeHandle(ref, () => ({ + handleOpen, + handleClose + })); + + useEffect(() => { + if (values?.auth === 'api_key') { + form.setFieldValue('auth_type', 'basic') + } else { + form.setFieldsValue({ + auth_type: undefined, + header: undefined, + api_key: undefined + }) + } + }, [values?.auth]) + + + return ( + +
+ + + + {values?.auth_type === 'custom' && + + + + } + + + + } +
+
+ ); +}); + +export default AuthConfigModal; \ No newline at end of file diff --git a/web/src/views/Workflow/components/Properties/HttpRequest/EditableTable.tsx b/web/src/views/Workflow/components/Properties/HttpRequest/EditableTable.tsx new file mode 100644 index 00000000..1dc1d8b4 --- /dev/null +++ b/web/src/views/Workflow/components/Properties/HttpRequest/EditableTable.tsx @@ -0,0 +1,232 @@ +import { useState, useEffect } from 'react'; +import { useTranslation } from 'react-i18next' +import { Button, Select, Table } from 'antd'; +import { PlusOutlined, DeleteOutlined } from '@ant-design/icons'; +import Editor from '../../Editor'; +import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin'; +import Empty from '@/components/Empty'; +import VariableSelect from '../VariableSelect'; + +export interface TableRow { + key: string; + name: string; + value: string; + type?: string; +} + +interface EditableTableProps { + title?: string; + value?: Record | TableRow[]; + onChange?: (value: TableRow[]) => void; + options?: Suggestion[]; + typeOptions?: {value: string, label: string}[] +} + +const EditableTable: React.FC = ({ + title, + value, + onChange, + options = [], + typeOptions = [] +}) => { + const { t } = useTranslation() + const [rows, setRows] = useState([]); + + useEffect(() => { + console.log('EditableTable value', value) + if (Array.isArray(value)) { + setRows([...value]) + } else if (value && Object.keys(value).length > 0) { + // Only update if rows are empty or significantly different + const valueEntries = Object.entries(value) + if (rows.length === 0 || rows.length !== valueEntries.length) { + setRows(valueEntries.map(([key, val], index) => { + console.log('val', val) + return { + key: index.toString(), + name: key || '', + value: val || '', + type: typeOptions.length > 0 ? typeOptions[0].value : undefined + } + })) + } + } else { + setRows([]) + } + }, [JSON.stringify(value), typeOptions.length]) + + const handleChange = (key: string, field: 'name' | 'value' | 'type', val: string) => { + const newRows = [...rows.map(row => + row.key === key ? { ...row, [field]: val } : row + )]; + + setRows(newRows); + onChange?.(newRows); + }; + + const handleAdd = () => { + const newKey = Date.now().toString(); + if (typeOptions.length) { + setRows([...rows, { key: newKey, name: '', value: '', type: typeOptions[0].value }]); + } else { + setRows([...rows, { key: newKey, name: '', value: '' }]); + } + }; + + const handleDelete = (key: string, index: number) => { + console.log('index', index) + + if (rows.length === 1) { + setRows([]); + onChange?.([]); + } else { + const newRows = rows.filter(row => row.key !== key); + setRows(newRows); + onChange?.(newRows); + } + }; + + const columns = typeOptions?.length > 0 ? [ + { + title: t('workflow.config.name'), + dataIndex: 'name', + width: '45%', + render: (text: string, record: TableRow) => ( + handleChange(record.key, 'name', value)} + /> + ), + }, + { + title: t('workflow.config.type'), + dataIndex: 'type', + width: '20%', + render: (text: string, record: TableRow) => ( + + + + + + + + + + + + + updateObjectList(headers, 'headers')} + /> + + + + updateObjectList(params, 'params')} + /> + + + + + + + {values?.error_handle?.method === 'default' && + <> + + + + + + + { + if (!value) return Promise.resolve(); + try { + JSON.parse(value); + return Promise.resolve(); + } catch { + return Promise.reject(new Error('Please enter valid JSON format')); + } + } + } + ]} + > + + + + } + + + + ); +}; +export default HttpRequest; \ No newline at end of file diff --git a/web/src/views/Workflow/components/Properties/HttpRequest/types.ts b/web/src/views/Workflow/components/Properties/HttpRequest/types.ts new file mode 100644 index 00000000..e208ee3c --- /dev/null +++ b/web/src/views/Workflow/components/Properties/HttpRequest/types.ts @@ -0,0 +1,44 @@ +export interface HttpRequestConfigForm { + method?: string; + url?: string; + auth?: { + auth?: string; + auth_type?: string; + header?: string; + api_key?: string; + }; + headers?: { + [key: string]: string; + }; + params?: { + [key: string]: string; + }; + body?: { + content_type?: string; + data: string | Record; + }; + verify_ssl?: boolean; + timeouts?: { + connect_timeout: number; + read_timeout: number; + write_timeout: number; + }; + retry?: { + max_attempts: number; + retry_interval: number; + }; + error_handle?: { + method: string; + default: { + body: string; + status_code: number; + headers: { + [key: string]: string; + }; + }; + }; +} + +export interface AuthConfigModalRef { + handleOpen: (vo?: HttpRequestConfigForm['auth']) => void; +} \ No newline at end of file diff --git a/web/src/views/Workflow/components/Properties/MappingList/index.tsx b/web/src/views/Workflow/components/Properties/MappingList/index.tsx new file mode 100644 index 00000000..9c8ed265 --- /dev/null +++ b/web/src/views/Workflow/components/Properties/MappingList/index.tsx @@ -0,0 +1,47 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next' +import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons'; +import { Button, Form, Input, Space } from 'antd'; + +interface MappingListProps { + name: string; +} +const MappingList: React.FC = ({ name }) => { + const { t } = useTranslation() + return ( + <> + + {(fields, { add, remove }) => ( + <> + {fields.map(({ key, name, ...restField }) => ( + + + + + + + + remove(name)} /> + + ))} + + + + + )} + + + ) +}; + +export default MappingList; \ No newline at end of file diff --git a/web/src/views/Workflow/components/Properties/index.tsx b/web/src/views/Workflow/components/Properties/index.tsx index f97f4532..c0f5ae7e 100644 --- a/web/src/views/Workflow/components/Properties/index.tsx +++ b/web/src/views/Workflow/components/Properties/index.tsx @@ -15,6 +15,8 @@ import VariableSelect from './VariableSelect'; import ParamsList from './ParamsList'; import GroupVariableList from './GroupVariableList' import CaseList from './CaseList' +import HttpRequest from './HttpRequest'; +import MappingList from './MappingList' interface PropertiesProps { selectedNode?: Node | null; @@ -38,10 +40,10 @@ const Properties: FC = ({ const [editIndex, setEditIndex] = useState(null) useEffect(() => { - if (selectedNode?.getData().id) { + if (selectedNode?.getData()?.id) { form.resetFields() } - }, [selectedNode?.getData().id]) + }, [selectedNode?.getData()?.id]) useEffect(() => { if (selectedNode && form) { @@ -97,7 +99,7 @@ const Properties: FC = ({ } Object.keys(values).forEach(key => { - if (selectedNode.data?.config[key]) { + if (selectedNode.data?.config?.[key]) { selectedNode.data.config[key].defaultValue = values[key] } }) @@ -190,6 +192,7 @@ const Properties: FC = ({ ...(nodeData.config?.variables?.value ?? []) ] list.forEach((variable: any) => { + if (!variable || !variable?.name) return; const key = `${nodeId}_${variable.name}`; if (!addedKeys.has(key)) { addedKeys.add(key); @@ -203,7 +206,8 @@ const Properties: FC = ({ }); } }); - nodeData.config?.variables?.sys.forEach((variable: any) => { + nodeData.config?.variables?.sys?.forEach((variable: any) => { + if (!variable || !variable?.name) return; const key = `${nodeId}_sys_${variable.name}`; if (!addedKeys.has(key)) { addedKeys.add(key); @@ -238,6 +242,8 @@ const Properties: FC = ({ return variableList; }, [selectedNode, graphRef]); + console.log('values', values) + return (
{t('workflow.nodeProperties')}
@@ -251,178 +257,195 @@ const Properties: FC = ({ updateNodeLabel(e.target.value); }} /> - - - - - {configs && Object.keys(configs).length > 0 && Object.keys(configs).map((key) => { - const config = configs[key] || {} + + + + + + {selectedNode?.data?.type === 'http-request' + ? + : configs && Object.keys(configs).length > 0 && Object.keys(configs).map((key) => { + const config = configs[key] || {} - if (selectedNode.data?.type === 'start' && key === 'variables' && config.type === 'define') { - return ( -
-
-
- {t(`workflow.config.${selectedNode.data.type}.${key}`)} + if (selectedNode?.data?.type === 'start' && key === 'variables' && config.type === 'define') { + return ( +
+
+
+ {t(`workflow.config.${selectedNode?.data?.type}.${key}`)} +
+
- -
- - {Array.isArray(config.defaultValue) && config.defaultValue?.map((vo, index) => -
- {vo.name}·{vo.description} + + {Array.isArray(config.defaultValue) && config.defaultValue?.map((vo, index) => +
+ {vo.name}·{vo.description} -
- {vo.required && {t('workflow.config.start.required')}} +
+ {vo.required && {t('workflow.config.start.required')}} + {vo.type} +
+ +
handleEditVariable(index, vo)} + >
+
handleDeleteVariable(index, vo)} + >
+
+
+ )} + + {config.sys?.map((vo, index) => +
+
+ sys.{vo.name} +
{vo.type}
- -
handleEditVariable(index, vo)} - >
-
handleDeleteVariable(index, vo)} - >
-
-
- )} - - {config.sys?.map((vo, index) => -
-
- sys.{vo.name} -
- {vo.type} -
- )} -
-
- ) - } + )} +
+
+ ) + } - if (selectedNode.data?.type === 'llm' && key === 'messages' && config.type === 'define') { - return ( - - - - ) - } - if (selectedNode.data?.type === 'end' && key === 'output') { - return ( - - - - ) - } + if (selectedNode?.data?.type === 'llm' && key === 'messages' && config.type === 'define') { + return ( + + + + ) + } + if (selectedNode?.data?.type === 'end' && key === 'output') { + return ( + + + + ) + } - if (config.type === 'define') { - return null - } + if (config.type === 'define') { + return null + } - if (config.type === 'knowledge') { - return ( - - - - ) - } - - if (config.type === 'messageEditor') { - return ( - - - - ) - } - - if (config.type === 'paramList') { - return ( - - - - - ) - } - if (config.type === 'groupVariableList') { - return ( - - - - - ) - } - if (config.type === 'caseList') { - console.log('key', key) - return ( - - - - ) - } + > + + + ) + } - return ( - - {config.type === 'input' - ? - : config.type === 'textarea' - ? - : config.type === 'select' - ? + : config.type === 'textarea' + ? + : config.type === 'select' + ?