Co-authored-by: Yeuoly <admin@srmxy.cn>tags/0.6.6
| @@ -156,5 +156,10 @@ CODE_MAX_NUMBER_ARRAY_LENGTH=1000 | |||
| API_TOOL_DEFAULT_CONNECT_TIMEOUT=10 | |||
| API_TOOL_DEFAULT_READ_TIMEOUT=60 | |||
| # HTTP Node configuration | |||
| HTTP_REQUEST_MAX_CONNECT_TIMEOUT=300 | |||
| HTTP_REQUEST_MAX_READ_TIMEOUT=600 | |||
| HTTP_REQUEST_MAX_WRITE_TIMEOUT=600 | |||
| # Log file path | |||
| LOG_FILE= | |||
| @@ -35,9 +35,15 @@ class HttpRequestNodeData(BaseNodeData): | |||
| type: Literal['none', 'form-data', 'x-www-form-urlencoded', 'raw-text', 'json'] | |||
| data: Union[None, str] | |||
| class Timeout(BaseModel): | |||
| connect: int | |||
| read: int | |||
| write: int | |||
| method: Literal['get', 'post', 'put', 'patch', 'delete', 'head'] | |||
| url: str | |||
| authorization: Authorization | |||
| headers: str | |||
| params: str | |||
| body: Optional[Body] | |||
| body: Optional[Body] | |||
| timeout: Optional[Timeout] | |||
| @@ -13,7 +13,6 @@ from core.workflow.entities.variable_pool import ValueType, VariablePool | |||
| from core.workflow.nodes.http_request.entities import HttpRequestNodeData | |||
| from core.workflow.utils.variable_template_parser import VariableTemplateParser | |||
| HTTP_REQUEST_DEFAULT_TIMEOUT = (10, 60) | |||
| MAX_BINARY_SIZE = 1024 * 1024 * 10 # 10MB | |||
| READABLE_MAX_BINARY_SIZE = '10MB' | |||
| MAX_TEXT_SIZE = 1024 * 1024 // 10 # 0.1MB | |||
| @@ -137,14 +136,16 @@ class HttpExecutor: | |||
| files: Union[None, dict[str, Any]] | |||
| boundary: str | |||
| variable_selectors: list[VariableSelector] | |||
| timeout: HttpRequestNodeData.Timeout | |||
| def __init__(self, node_data: HttpRequestNodeData, variable_pool: Optional[VariablePool] = None): | |||
| def __init__(self, node_data: HttpRequestNodeData, timeout: HttpRequestNodeData.Timeout, variable_pool: Optional[VariablePool] = None): | |||
| """ | |||
| init | |||
| """ | |||
| self.server_url = node_data.url | |||
| self.method = node_data.method | |||
| self.authorization = node_data.authorization | |||
| self.timeout = timeout | |||
| self.params = {} | |||
| self.headers = {} | |||
| self.body = None | |||
| @@ -307,7 +308,7 @@ class HttpExecutor: | |||
| 'url': self.server_url, | |||
| 'headers': headers, | |||
| 'params': self.params, | |||
| 'timeout': HTTP_REQUEST_DEFAULT_TIMEOUT, | |||
| 'timeout': (self.timeout.connect, self.timeout.read, self.timeout.write), | |||
| 'follow_redirects': True | |||
| } | |||
| @@ -1,4 +1,5 @@ | |||
| import logging | |||
| import os | |||
| from mimetypes import guess_extension | |||
| from os import path | |||
| from typing import cast | |||
| @@ -12,18 +13,49 @@ from core.workflow.nodes.http_request.entities import HttpRequestNodeData | |||
| from core.workflow.nodes.http_request.http_executor import HttpExecutor, HttpExecutorResponse | |||
| from models.workflow import WorkflowNodeExecutionStatus | |||
| MAX_CONNECT_TIMEOUT = int(os.environ.get('HTTP_REQUEST_MAX_CONNECT_TIMEOUT', '300')) | |||
| MAX_READ_TIMEOUT = int(os.environ.get('HTTP_REQUEST_MAX_READ_TIMEOUT', '600')) | |||
| MAX_WRITE_TIMEOUT = int(os.environ.get('HTTP_REQUEST_MAX_WRITE_TIMEOUT', '600')) | |||
| HTTP_REQUEST_DEFAULT_TIMEOUT = HttpRequestNodeData.Timeout(connect=min(10, MAX_CONNECT_TIMEOUT), | |||
| read=min(60, MAX_READ_TIMEOUT), | |||
| write=min(20, MAX_WRITE_TIMEOUT)) | |||
| class HttpRequestNode(BaseNode): | |||
| _node_data_cls = HttpRequestNodeData | |||
| node_type = NodeType.HTTP_REQUEST | |||
| @classmethod | |||
| def get_default_config(cls) -> dict: | |||
| return { | |||
| "type": "http-request", | |||
| "config": { | |||
| "method": "get", | |||
| "authorization": { | |||
| "type": "no-auth", | |||
| }, | |||
| "body": { | |||
| "type": "none" | |||
| }, | |||
| "timeout": { | |||
| **HTTP_REQUEST_DEFAULT_TIMEOUT.dict(), | |||
| "max_connect_timeout": MAX_CONNECT_TIMEOUT, | |||
| "max_read_timeout": MAX_READ_TIMEOUT, | |||
| "max_write_timeout": MAX_WRITE_TIMEOUT, | |||
| } | |||
| }, | |||
| } | |||
| def _run(self, variable_pool: VariablePool) -> NodeRunResult: | |||
| node_data: HttpRequestNodeData = cast(self._node_data_cls, self.node_data) | |||
| # init http executor | |||
| http_executor = None | |||
| try: | |||
| http_executor = HttpExecutor(node_data=node_data, variable_pool=variable_pool) | |||
| http_executor = HttpExecutor(node_data=node_data, | |||
| timeout=self._get_request_timeout(node_data), | |||
| variable_pool=variable_pool) | |||
| # invoke http executor | |||
| response = http_executor.invoke() | |||
| @@ -38,7 +70,7 @@ class HttpRequestNode(BaseNode): | |||
| error=str(e), | |||
| process_data=process_data | |||
| ) | |||
| files = self.extract_files(http_executor.server_url, response) | |||
| return NodeRunResult( | |||
| @@ -54,6 +86,16 @@ class HttpRequestNode(BaseNode): | |||
| } | |||
| ) | |||
| def _get_request_timeout(self, node_data: HttpRequestNodeData) -> HttpRequestNodeData.Timeout: | |||
| timeout = node_data.timeout | |||
| if timeout is None: | |||
| return HTTP_REQUEST_DEFAULT_TIMEOUT | |||
| timeout.connect = min(timeout.connect, MAX_CONNECT_TIMEOUT) | |||
| timeout.read = min(timeout.read, MAX_READ_TIMEOUT) | |||
| timeout.write = min(timeout.write, MAX_WRITE_TIMEOUT) | |||
| return timeout | |||
| @classmethod | |||
| def _extract_variable_selector_to_variable_mapping(cls, node_data: HttpRequestNodeData) -> dict[str, list[str]]: | |||
| """ | |||
| @@ -62,7 +104,7 @@ class HttpRequestNode(BaseNode): | |||
| :return: | |||
| """ | |||
| try: | |||
| http_executor = HttpExecutor(node_data=node_data) | |||
| http_executor = HttpExecutor(node_data=node_data, timeout=HTTP_REQUEST_DEFAULT_TIMEOUT) | |||
| variable_selectors = http_executor.variable_selectors | |||
| @@ -84,7 +126,7 @@ class HttpRequestNode(BaseNode): | |||
| # if not image, return directly | |||
| if 'image' not in mimetype: | |||
| return files | |||
| if mimetype: | |||
| # extract filename from url | |||
| filename = path.basename(url) | |||
| @@ -0,0 +1,101 @@ | |||
| 'use client' | |||
| import type { FC } from 'react' | |||
| import React from 'react' | |||
| import cn from 'classnames' | |||
| import { useTranslation } from 'react-i18next' | |||
| import { useBoolean } from 'ahooks' | |||
| import type { Timeout as TimeoutPayloadType } from '../../types' | |||
| import { ChevronRight } from '@/app/components/base/icons/src/vender/line/arrows' | |||
| type Props = { | |||
| readonly: boolean | |||
| nodeId: string | |||
| payload: TimeoutPayloadType | |||
| onChange: (payload: TimeoutPayloadType) => void | |||
| } | |||
| const i18nPrefix = 'workflow.nodes.http' | |||
| const InputField: FC<{ | |||
| title: string | |||
| description: string | |||
| placeholder: string | |||
| value?: number | |||
| onChange: (value: number) => void | |||
| readOnly?: boolean | |||
| min: number | |||
| max: number | |||
| }> = ({ title, description, placeholder, value, onChange, readOnly, min, max }) => { | |||
| return ( | |||
| <div className="space-y-1"> | |||
| <div className="flex items-center h-[18px] space-x-2"> | |||
| <span className="text-[13px] font-medium text-gray-900">{title}</span> | |||
| <span className="text-xs font-normal text-gray-500">{description}</span> | |||
| </div> | |||
| <input className="w-full px-3 text-sm leading-9 text-gray-900 border-0 rounded-lg grow h-9 bg-gray-100 focus:outline-none focus:ring-1 focus:ring-inset focus:ring-gray-200" value={value} onChange={(e) => { | |||
| const value = Math.max(min, Math.min(max, parseInt(e.target.value, 10))) | |||
| onChange(value) | |||
| }} placeholder={placeholder} type='number' readOnly={readOnly} min={min} max={max} /> | |||
| </div> | |||
| ) | |||
| } | |||
| const Timeout: FC<Props> = ({ readonly, payload, onChange }) => { | |||
| const { t } = useTranslation() | |||
| const { connect, read, write, max_connect_timeout, max_read_timeout, max_write_timeout } = payload ?? {} | |||
| const [isFold, { | |||
| toggle: toggleFold, | |||
| }] = useBoolean(true) | |||
| return ( | |||
| <> | |||
| <div> | |||
| <div | |||
| onClick={toggleFold} | |||
| className={cn('flex justify-between leading-[18px] text-[13px] font-semibold text-gray-700 uppercase cursor-pointer')}> | |||
| <div>{t(`${i18nPrefix}.timeout.title`)}</div> | |||
| <ChevronRight className='w-4 h-4 text-gray-500 transform transition-transform' style={{ transform: isFold ? 'rotate(0deg)' : 'rotate(90deg)' }} /> | |||
| </div> | |||
| {!isFold && ( | |||
| <div className='mt-2 space-y-1'> | |||
| <div className="space-y-3"> | |||
| <InputField | |||
| title={t('workflow.nodes.http.timeout.connectLabel')!} | |||
| description={t('workflow.nodes.http.timeout.connectPlaceholder')!} | |||
| placeholder={t('workflow.nodes.http.timeout.connectPlaceholder')!} | |||
| readOnly={readonly} | |||
| value={connect} | |||
| onChange={v => onChange?.({ ...payload, connect: v })} | |||
| min={1} | |||
| max={max_connect_timeout ?? 300} | |||
| /> | |||
| <InputField | |||
| title={t('workflow.nodes.http.timeout.readLabel')!} | |||
| description={t('workflow.nodes.http.timeout.readPlaceholder')!} | |||
| placeholder={t('workflow.nodes.http.timeout.readPlaceholder')!} | |||
| readOnly={readonly} | |||
| value={read} | |||
| onChange={v => onChange?.({ ...payload, read: v })} | |||
| min={1} | |||
| max={max_read_timeout ?? 600} | |||
| /> | |||
| <InputField | |||
| title={t('workflow.nodes.http.timeout.writeLabel')!} | |||
| description={t('workflow.nodes.http.timeout.writePlaceholder')!} | |||
| placeholder={t('workflow.nodes.http.timeout.writePlaceholder')!} | |||
| readOnly={readonly} | |||
| value={write} | |||
| onChange={v => onChange?.({ ...payload, write: v })} | |||
| min={1} | |||
| max={max_write_timeout ?? 600} | |||
| /> | |||
| </div> | |||
| </div> | |||
| )} | |||
| </div> | |||
| </> | |||
| ) | |||
| } | |||
| export default React.memo(Timeout) | |||
| @@ -18,6 +18,11 @@ const nodeDefault: NodeDefault<HttpNodeType> = { | |||
| type: BodyType.none, | |||
| data: '', | |||
| }, | |||
| timeout: { | |||
| max_connect_timeout: 0, | |||
| max_read_timeout: 0, | |||
| max_write_timeout: 0, | |||
| }, | |||
| }, | |||
| getAvailablePrevNodes(isChatMode: boolean) { | |||
| const nodes = isChatMode | |||
| @@ -8,6 +8,7 @@ import KeyValue from './components/key-value' | |||
| import EditBody from './components/edit-body' | |||
| import AuthorizationModal from './components/authorization' | |||
| import type { HttpNodeType } from './types' | |||
| import Timeout from './components/timeout' | |||
| import Field from '@/app/components/workflow/nodes/_base/components/field' | |||
| import Split from '@/app/components/workflow/nodes/_base/components/split' | |||
| import OutputVars, { VarItem } from '@/app/components/workflow/nodes/_base/components/output-vars' | |||
| @@ -40,6 +41,7 @@ const Panel: FC<NodePanelProps<HttpNodeType>> = ({ | |||
| showAuthorization, | |||
| hideAuthorization, | |||
| setAuthorization, | |||
| setTimeout, | |||
| // single run | |||
| isShowSingleRun, | |||
| hideSingleRun, | |||
| @@ -112,6 +114,15 @@ const Panel: FC<NodePanelProps<HttpNodeType>> = ({ | |||
| /> | |||
| </Field> | |||
| </div> | |||
| <Split /> | |||
| <div className='px-4 pt-4 pb-4'> | |||
| <Timeout | |||
| nodeId={id} | |||
| readonly={readOnly} | |||
| payload={inputs.timeout} | |||
| onChange={setTimeout} | |||
| /> | |||
| </div> | |||
| {(isShowAuthorization && !readOnly) && ( | |||
| <AuthorizationModal | |||
| isShow | |||
| @@ -48,6 +48,15 @@ export type Authorization = { | |||
| } | null | |||
| } | |||
| export type Timeout = { | |||
| connect?: number | |||
| read?: number | |||
| write?: number | |||
| max_connect_timeout?: number | |||
| max_read_timeout?: number | |||
| max_write_timeout?: number | |||
| } | |||
| export type HttpNodeType = CommonNodeType & { | |||
| variables: Variable[] | |||
| method: Method | |||
| @@ -56,4 +65,5 @@ export type HttpNodeType = CommonNodeType & { | |||
| params: string | |||
| body: Body | |||
| authorization: Authorization | |||
| timeout: Timeout | |||
| } | |||
| @@ -1,10 +1,11 @@ | |||
| import { useCallback } from 'react' | |||
| import { useCallback, useEffect } from 'react' | |||
| import produce from 'immer' | |||
| import { useBoolean } from 'ahooks' | |||
| import useVarList from '../_base/hooks/use-var-list' | |||
| import { VarType } from '../../types' | |||
| import type { Var } from '../../types' | |||
| import type { Authorization, Body, HttpNodeType, Method } from './types' | |||
| import { useStore } from '../../store' | |||
| import type { Authorization, Body, HttpNodeType, Method, Timeout } from './types' | |||
| import useKeyValueList from './hooks/use-key-value-list' | |||
| import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-crud' | |||
| import useOneStepRun from '@/app/components/workflow/nodes/_base/hooks/use-one-step-run' | |||
| @@ -14,6 +15,9 @@ import { | |||
| const useConfig = (id: string, payload: HttpNodeType) => { | |||
| const { nodesReadOnly: readOnly } = useNodesReadOnly() | |||
| const defaultConfig = useStore(s => s.nodesDefaultConfigs)[payload.type] | |||
| const { inputs, setInputs } = useNodeCrud<HttpNodeType>(id, payload) | |||
| const { handleVarListChange, handleAddVariable } = useVarList<HttpNodeType>({ | |||
| @@ -21,6 +25,17 @@ const useConfig = (id: string, payload: HttpNodeType) => { | |||
| setInputs, | |||
| }) | |||
| useEffect(() => { | |||
| const isReady = defaultConfig && Object.keys(defaultConfig).length > 0 | |||
| if (isReady) { | |||
| setInputs({ | |||
| ...inputs, | |||
| ...defaultConfig, | |||
| }) | |||
| } | |||
| // eslint-disable-next-line react-hooks/exhaustive-deps | |||
| }, [defaultConfig]) | |||
| const handleMethodChange = useCallback((method: Method) => { | |||
| const newInputs = produce(inputs, (draft: HttpNodeType) => { | |||
| draft.method = method | |||
| @@ -80,6 +95,13 @@ const useConfig = (id: string, payload: HttpNodeType) => { | |||
| setInputs(newInputs) | |||
| }, [inputs, setInputs]) | |||
| const setTimeout = useCallback((timeout: Timeout) => { | |||
| const newInputs = produce(inputs, (draft: HttpNodeType) => { | |||
| draft.timeout = timeout | |||
| }) | |||
| setInputs(newInputs) | |||
| }, [inputs, setInputs]) | |||
| const filterVar = useCallback((varPayload: Var) => { | |||
| return [VarType.string, VarType.number].includes(varPayload.type) | |||
| }, []) | |||
| @@ -148,6 +170,7 @@ const useConfig = (id: string, payload: HttpNodeType) => { | |||
| showAuthorization, | |||
| hideAuthorization, | |||
| setAuthorization, | |||
| setTimeout, | |||
| // single run | |||
| isShowSingleRun, | |||
| hideSingleRun, | |||
| @@ -245,6 +245,15 @@ const translation = { | |||
| 'header': 'Kopfzeile', | |||
| }, | |||
| insertVarPlaceholder: 'Tippen Sie ‘/’, um eine Variable einzufügen', | |||
| timeout: { | |||
| title: 'Zeitüberschreitung', | |||
| connectLabel: 'Verbindungszeitüberschreitung', | |||
| connectPlaceholder: 'Geben Sie die Verbindungszeitüberschreitung in Sekunden ein', | |||
| readLabel: 'Lesezeitüberschreitung', | |||
| readPlaceholder: 'Geben Sie die Lesezeitüberschreitung in Sekunden ein', | |||
| writeLabel: 'Schreibzeitüberschreitung', | |||
| writePlaceholder: 'Geben Sie die Schreibzeitüberschreitung in Sekunden ein', | |||
| }, | |||
| }, | |||
| code: { | |||
| inputVars: 'Eingabevariablen', | |||
| @@ -252,6 +252,15 @@ const translation = { | |||
| 'header': 'Header', | |||
| }, | |||
| insertVarPlaceholder: 'type \'/\' to insert variable', | |||
| timeout: { | |||
| title: 'Timeout', | |||
| connectLabel: 'Connection Timeout', | |||
| connectPlaceholder: 'Enter connection timeout in seconds', | |||
| readLabel: 'Read Timeout', | |||
| readPlaceholder: 'Enter read timeout in seconds', | |||
| writeLabel: 'Write Timeout', | |||
| writePlaceholder: 'Enter write timeout in seconds', | |||
| }, | |||
| }, | |||
| code: { | |||
| inputVars: 'Input Variables', | |||
| @@ -248,6 +248,15 @@ const translation = { | |||
| 'header': 'En-tête', | |||
| }, | |||
| insertVarPlaceholder: 'tapez \'/\' pour insérer une variable', | |||
| timeout: { | |||
| title: 'Délai d\'expiration', | |||
| connectLabel: 'Délai de connexion', | |||
| connectPlaceholder: 'Entrez le délai de connexion en secondes', | |||
| readLabel: 'Délai de lecture', | |||
| readPlaceholder: 'Entrez le délai de lecture en secondes', | |||
| writeLabel: 'Délai d\'écriture', | |||
| writePlaceholder: 'Entrez le délai d\'écriture en secondes', | |||
| }, | |||
| }, | |||
| code: { | |||
| inputVars: 'Variables d\'entrée', | |||
| @@ -248,6 +248,15 @@ const translation = { | |||
| 'header': 'ヘッダー', | |||
| }, | |||
| insertVarPlaceholder: '変数を挿入するには\'/\'を入力してください', | |||
| timeout: { | |||
| title: 'タイムアウト', | |||
| connectLabel: '接続タイムアウト', | |||
| connectPlaceholder: '接続タイムアウトを秒で入力', | |||
| readLabel: '読み取りタイムアウト', | |||
| readPlaceholder: '読み取りタイムアウトを秒で入力', | |||
| writeLabel: '書き込みタイムアウト', | |||
| writePlaceholder: '書き込みタイムアウトを秒で入力', | |||
| }, | |||
| }, | |||
| code: { | |||
| inputVars: '入力変数', | |||
| @@ -248,6 +248,15 @@ const translation = { | |||
| 'header': 'Cabeçalho', | |||
| }, | |||
| insertVarPlaceholder: 'digite \'/\' para inserir variável', | |||
| timeout: { | |||
| title: 'Tempo esgotado', | |||
| connectLabel: 'Tempo de conexão', | |||
| connectPlaceholder: 'Insira o tempo de conexão em segundos', | |||
| readLabel: 'Tempo de leitura', | |||
| readPlaceholder: 'Insira o tempo de leitura em segundos', | |||
| writeLabel: 'Tempo de escrita', | |||
| writePlaceholder: 'Insira o tempo de escrita em segundos', | |||
| }, | |||
| }, | |||
| code: { | |||
| inputVars: 'Variáveis de entrada', | |||
| @@ -248,6 +248,15 @@ const translation = { | |||
| 'header': 'Заголовок', | |||
| }, | |||
| insertVarPlaceholder: 'наберіть \'/\' для вставки змінної', | |||
| timeout: { | |||
| title: 'Час вичерпано', | |||
| connectLabel: 'Тайм-аут з’єднання', | |||
| connectPlaceholder: 'Введіть час тайм-ауту з’єднання у секундах', | |||
| readLabel: 'Тайм-аут читання', | |||
| readPlaceholder: 'Введіть час тайм-ауту читання у секундах', | |||
| writeLabel: 'Тайм-аут запису', | |||
| writePlaceholder: 'Введіть час тайм-ауту запису у секундах', | |||
| }, | |||
| }, | |||
| code: { | |||
| inputVars: 'Вхідні змінні', | |||
| @@ -248,6 +248,15 @@ const translation = { | |||
| 'header': 'Tiêu đề', | |||
| }, | |||
| insertVarPlaceholder: 'nhập \'/\' để chèn biến', | |||
| timeout: { | |||
| title: 'Hết thời gian', | |||
| connectLabel: 'Hết thời gian kết nối', | |||
| connectPlaceholder: 'Nhập thời gian kết nối bằng giây', | |||
| readLabel: 'Hết thời gian đọc', | |||
| readPlaceholder: 'Nhập thời gian đọc bằng giây', | |||
| writeLabel: 'Hết thời gian ghi', | |||
| writePlaceholder: 'Nhập thời gian ghi bằng giây', | |||
| }, | |||
| }, | |||
| code: { | |||
| inputVars: 'Biến đầu vào', | |||
| @@ -252,6 +252,15 @@ const translation = { | |||
| 'header': 'Header', | |||
| }, | |||
| insertVarPlaceholder: '键入 \'/\' 键快速插入变量', | |||
| timeout: { | |||
| title: '超时设置', | |||
| connectLabel: '连接超时', | |||
| connectPlaceholder: '输入连接超时(以秒为单位)', | |||
| readLabel: '读取超时', | |||
| readPlaceholder: '输入读取超时(以秒为单位)', | |||
| writeLabel: '写入超时', | |||
| writePlaceholder: '输入写入超时(以秒为单位)', | |||
| }, | |||
| }, | |||
| code: { | |||
| inputVars: '输入变量', | |||