diff --git a/packages/global/core/workflow/constants.ts b/packages/global/core/workflow/constants.ts index 6f5df62e5..eab814023 100644 --- a/packages/global/core/workflow/constants.ts +++ b/packages/global/core/workflow/constants.ts @@ -108,6 +108,8 @@ export enum NodeInputKeyEnum { httpMethod = 'system_httpMethod', httpParams = 'system_httpParams', httpJsonBody = 'system_httpJsonBody', + httpFormBody = 'system_httpFormBody', + httpContentType = 'system_httpContentType', httpTimeout = 'system_httpTimeout', abandon_httpUrl = 'url', @@ -217,3 +219,13 @@ export enum RuntimeEdgeStatusEnum { export const VARIABLE_NODE_ID = 'VARIABLE_NODE_ID'; export const DYNAMIC_INPUT_REFERENCE_KEY = 'DYNAMIC_INPUT_REFERENCE_KEY'; + +// http node body content type +export enum ContentTypes { + none = 'none', + formData = 'form-data', + xWwwFormUrlencoded = 'x-www-form-urlencoded', + json = 'json', + xml = 'xml', + raw = 'raw-text' +} diff --git a/packages/global/core/workflow/template/system/http468.ts b/packages/global/core/workflow/template/system/http468.ts index acb90590d..fab83fe6b 100644 --- a/packages/global/core/workflow/template/system/http468.ts +++ b/packages/global/core/workflow/template/system/http468.ts @@ -8,7 +8,8 @@ import { WorkflowIOValueTypeEnum, NodeInputKeyEnum, NodeOutputKeyEnum, - FlowNodeTemplateTypeEnum + FlowNodeTemplateTypeEnum, + ContentTypes } from '../../constants'; import { Input_Template_DynamicInput } from '../input'; import { Output_Template_AddOutput } from '../output'; @@ -82,6 +83,7 @@ export const HttpNode468: FlowNodeTemplateType = { label: '', required: false }, + // json body data { key: NodeInputKeyEnum.httpJsonBody, renderTypeList: [FlowNodeInputTypeEnum.hidden], @@ -89,6 +91,24 @@ export const HttpNode468: FlowNodeTemplateType = { value: '', label: '', required: false + }, + // form body data + { + key: NodeInputKeyEnum.httpFormBody, + renderTypeList: [FlowNodeInputTypeEnum.hidden], + valueType: WorkflowIOValueTypeEnum.any, + value: [], + label: '', + required: false + }, + // body data type + { + key: NodeInputKeyEnum.httpContentType, + renderTypeList: [FlowNodeInputTypeEnum.hidden], + valueType: WorkflowIOValueTypeEnum.string, + value: ContentTypes.json, + label: '', + required: false } ], outputs: [ diff --git a/packages/service/core/workflow/dispatch/tools/http468.ts b/packages/service/core/workflow/dispatch/tools/http468.ts index c0e9fdc15..22029b3c7 100644 --- a/packages/service/core/workflow/dispatch/tools/http468.ts +++ b/packages/service/core/workflow/dispatch/tools/http468.ts @@ -16,6 +16,8 @@ import { DispatchNodeResultType } from '@fastgpt/global/core/workflow/runtime/ty import { getErrText } from '@fastgpt/global/common/error/utils'; import { textAdaptGptResponse } from '@fastgpt/global/core/workflow/runtime/utils'; import { getSystemPluginCb } from '../../../../../plugins/register'; +import { ContentTypes } from '@fastgpt/global/core/workflow/constants'; +import { replaceEditorVariable } from '@fastgpt/global/core/workflow/utils'; type PropsArrType = { key: string; @@ -29,6 +31,8 @@ type HttpRequestProps = ModuleDispatchProps<{ [NodeInputKeyEnum.httpHeaders]: PropsArrType[]; [NodeInputKeyEnum.httpParams]: PropsArrType[]; [NodeInputKeyEnum.httpJsonBody]: string; + [NodeInputKeyEnum.httpFormBody]: PropsArrType[]; + [NodeInputKeyEnum.httpContentType]: ContentTypes; [NodeInputKeyEnum.addInputParam]: Record; [NodeInputKeyEnum.httpTimeout]?: number; [key: string]: any; @@ -40,13 +44,23 @@ type HttpResponse = DispatchNodeResultType<{ const UNDEFINED_SIGN = 'UNDEFINED_SIGN'; +const contentTypeMap = { + [ContentTypes.none]: '', + [ContentTypes.formData]: '', + [ContentTypes.xWwwFormUrlencoded]: 'application/x-www-form-urlencoded', + [ContentTypes.json]: 'application/json', + [ContentTypes.xml]: 'application/xml', + [ContentTypes.raw]: 'text/plain' +}; + export const dispatchHttp468Request = async (props: HttpRequestProps): Promise => { let { runningAppInfo: { id: appId }, chatId, responseChatItemId, variables, - node: { outputs }, + node, + runtimeNodes, histories, workflowStreamResponse, params: { @@ -55,6 +69,8 @@ export const dispatchHttp468Request = async (props: HttpRequestProps): Promise { try { + const contentType = contentTypeMap[httpContentType]; + if (contentType) { + httpHeader = [{ key: 'Content-Type', value: contentType, type: 'string' }, ...httpHeader]; + } + if (!httpHeader || httpHeader.length === 0) return {}; // array return httpHeader.reduce((acc: Record, item) => { - const key = replaceVariable(item.key, allVariables); - const value = replaceVariable(item.value, allVariables); + const key = replaceVariable( + replaceEditorVariable({ + text: item.key, + nodes: runtimeNodes, + variables, + runningNode: node + }), + allVariables + ); + const value = replaceVariable( + replaceEditorVariable({ + text: item.value, + nodes: runtimeNodes, + variables, + runningNode: node + }), + allVariables + ); acc[key] = valueTypeFormat(value, WorkflowIOValueTypeEnum.string); return acc; }, {}); @@ -99,28 +135,109 @@ export const dispatchHttp468Request = async (props: HttpRequestProps): Promise, item) => { - const key = replaceVariable(item.key, allVariables); - const value = replaceVariable(item.value, allVariables); + const key = replaceVariable( + replaceEditorVariable({ + text: item.key, + nodes: runtimeNodes, + variables, + runningNode: node + }), + allVariables + ); + const value = replaceVariable( + replaceEditorVariable({ + text: item.value, + nodes: runtimeNodes, + variables, + runningNode: node + }), + allVariables + ); acc[key] = valueTypeFormat(value, WorkflowIOValueTypeEnum.string); return acc; }, {}); const requestBody = await (() => { - if (!httpJsonBody) return {}; + if (httpContentType === ContentTypes.none) return {}; try { - // Replace all variables in the string body - httpJsonBody = replaceVariable(httpJsonBody, allVariables); - - // Text body, return directly - if (headers['Content-Type']?.includes('text/plain')) { - return httpJsonBody?.replaceAll(UNDEFINED_SIGN, 'null'); + if (httpContentType === ContentTypes.formData) { + if (!Array.isArray(httpFormBody)) return {}; + httpFormBody = httpFormBody.map((item) => ({ + key: replaceVariable( + replaceEditorVariable({ + text: item.key, + nodes: runtimeNodes, + variables, + runningNode: node + }), + allVariables + ), + type: item.type, + value: replaceVariable( + replaceEditorVariable({ + text: item.value, + nodes: runtimeNodes, + variables, + runningNode: node + }), + allVariables + ) + })); + const formData = new FormData(); + for (const { key, value } of httpFormBody) { + formData.append(key, value); + } + return formData; } - - // Json body, parse and return - const jsonParse = JSON.parse(httpJsonBody); - const removeSignJson = removeUndefinedSign(jsonParse); - return removeSignJson; + if (httpContentType === ContentTypes.xWwwFormUrlencoded) { + if (!Array.isArray(httpFormBody)) return {}; + httpFormBody = httpFormBody.map((item) => ({ + key: replaceVariable( + replaceEditorVariable({ + text: item.key, + nodes: runtimeNodes, + variables, + runningNode: node + }), + allVariables + ), + type: item.type, + value: replaceVariable( + replaceEditorVariable({ + text: item.value, + nodes: runtimeNodes, + variables, + runningNode: node + }), + allVariables + ) + })); + const urlSearchParams = new URLSearchParams(); + for (const { key, value } of httpFormBody) { + urlSearchParams.append(key, value); + } + return urlSearchParams; + } + if (!httpJsonBody) return {}; + if (httpContentType === ContentTypes.json) { + httpJsonBody = replaceVariable(httpJsonBody, allVariables); + // Json body, parse and return + const jsonParse = JSON.parse(httpJsonBody); + const removeSignJson = removeUndefinedSign(jsonParse); + return removeSignJson; + } + httpJsonBody = replaceVariable( + replaceEditorVariable({ + text: httpJsonBody, + nodes: runtimeNodes, + variables, + runningNode: node + }), + allVariables + ); + return httpJsonBody.replaceAll(UNDEFINED_SIGN, 'null'); } catch (error) { console.log(error); return Promise.reject(`Invalid JSON body: ${httpJsonBody}`); @@ -150,7 +267,7 @@ export const dispatchHttp468Request = async (props: HttpRequestProps): Promise = {}; for (const key in formatResponse) { - const output = outputs.find((item) => item.key === key); + const output = node.outputs.find((item) => item.key === key); if (!output) continue; results[key] = valueTypeFormat(formatResponse[key], output.valueType); } @@ -213,7 +330,6 @@ async function fetchData({ baseURL: `http://${SERVICE_LOCAL_HOST}`, url, headers: { - 'Content-Type': 'application/json', ...headers }, timeout: timeout * 1000, diff --git a/packages/web/components/common/Input/HttpInput/Editor.tsx b/packages/web/components/common/Input/HttpInput/Editor.tsx index 82d0e75fb..c83019fd8 100644 --- a/packages/web/components/common/Input/HttpInput/Editor.tsx +++ b/packages/web/components/common/Input/HttpInput/Editor.tsx @@ -17,39 +17,40 @@ import { Box, Flex } from '@chakra-ui/react'; import styles from './index.module.scss'; import { EditorState, LexicalEditor } from 'lexical'; import { getNanoid } from '@fastgpt/global/common/string/tools'; -import { EditorVariablePickerType } from '../../Textarea/PromptEditor/type'; +import { + EditorVariableLabelPickerType, + EditorVariablePickerType +} from '../../Textarea/PromptEditor/type'; import { VariableNode } from '../../Textarea/PromptEditor/plugins/VariablePlugin/node'; import { textToEditorState } from '../../Textarea/PromptEditor/utils'; -import DropDownMenu from '../../Textarea/PromptEditor/modules/DropDownMenu'; import { SingleLinePlugin } from '../../Textarea/PromptEditor/plugins/SingleLinePlugin'; import OnBlurPlugin from '../../Textarea/PromptEditor/plugins/OnBlurPlugin'; import VariablePlugin from '../../Textarea/PromptEditor/plugins/VariablePlugin'; import VariablePickerPlugin from '../../Textarea/PromptEditor/plugins/VariablePickerPlugin'; import FocusPlugin from '../../Textarea/PromptEditor/plugins/FocusPlugin'; +import VariableLabelPlugin from '../../Textarea/PromptEditor/plugins/VariableLabelPlugin'; +import { VariableLabelNode } from '../../Textarea/PromptEditor/plugins/VariableLabelPlugin/node'; +import VariableLabelPickerPlugin from '../../Textarea/PromptEditor/plugins/VariableLabelPickerPlugin'; export default function Editor({ h = 40, - hasVariablePlugin = true, - hasDropDownPlugin = false, variables, + variableLabels, onChange, onBlur, value, currentValue, placeholder = '', - setDropdownValue, updateTrigger }: { h?: number; - hasVariablePlugin?: boolean; - hasDropDownPlugin?: boolean; variables: EditorVariablePickerType[]; + variableLabels: EditorVariableLabelPickerType[]; onChange?: (editorState: EditorState, editor: LexicalEditor) => void; onBlur?: (editor: LexicalEditor) => void; value?: string; currentValue?: string; placeholder?: string; - setDropdownValue?: (value: string) => void; updateTrigger?: boolean; }) { const [key, setKey] = useState(getNanoid(6)); @@ -58,7 +59,7 @@ export default function Editor({ const initialConfig = { namespace: 'HttpInput', - nodes: [VariableNode], + nodes: [VariableNode, VariableLabelNode], editorState: textToEditorState(value), onError: (error: Error) => { throw error; @@ -75,16 +76,6 @@ export default function Editor({ setFocus(false); }, [updateTrigger]); - const dropdownVariables = useMemo( - () => - variables.filter((item) => { - const key = item.key.toLowerCase(); - const current = currentValue?.toLowerCase(); - return key.includes(current || '') && item.key !== currentValue; - }), - [currentValue, variables] - ); - return ( - {hasVariablePlugin ? : ''} + + - {focus && hasDropDownPlugin && ( - - )} ); } diff --git a/packages/web/components/common/Input/HttpInput/index.tsx b/packages/web/components/common/Input/HttpInput/index.tsx index b0838d764..39dc54e71 100644 --- a/packages/web/components/common/Input/HttpInput/index.tsx +++ b/packages/web/components/common/Input/HttpInput/index.tsx @@ -1,58 +1,61 @@ -import React, { useEffect } from 'react'; -import { $getRoot, EditorState, type LexicalEditor } from 'lexical'; +import React from 'react'; +import { EditorState, type LexicalEditor } from 'lexical'; import { useCallback } from 'react'; import { editorStateToText } from '../../Textarea/PromptEditor/utils'; -import { EditorVariablePickerType } from '../../Textarea/PromptEditor/type'; +import { + EditorVariableLabelPickerType, + EditorVariablePickerType +} from '../../Textarea/PromptEditor/type'; import Editor from './Editor'; const HttpInput = ({ - hasVariablePlugin = true, - hasDropDownPlugin = false, variables = [], + variableLabels = [], value, onChange, onBlur, h, placeholder, - setDropdownValue, updateTrigger }: { - hasVariablePlugin?: boolean; - hasDropDownPlugin?: boolean; variables?: EditorVariablePickerType[]; + variableLabels?: EditorVariableLabelPickerType[]; value?: string; onChange?: (text: string) => void; onBlur?: (text: string) => void; h?: number; placeholder?: string; - setDropdownValue?: (value: string) => void; updateTrigger?: boolean; }) => { const [currentValue, setCurrentValue] = React.useState(value); - const onChangeInput = useCallback((editorState: EditorState, editor: LexicalEditor) => { - const text = editorStateToText(editor).replaceAll('}}{{', '}} {{'); - setCurrentValue(text); - onChange?.(text); - }, []); - const onBlurInput = useCallback((editor: LexicalEditor) => { - const text = editorStateToText(editor).replaceAll('}}{{', '}} {{'); - onBlur?.(text); - }, []); + const onChangeInput = useCallback( + (editorState: EditorState, editor: LexicalEditor) => { + const text = editorStateToText(editor).replaceAll('}}{{', '}} {{'); + setCurrentValue(text); + onChange?.(text); + }, + [onChange] + ); + const onBlurInput = useCallback( + (editor: LexicalEditor) => { + const text = editorStateToText(editor).replaceAll('}}{{', '}} {{'); + onBlur?.(text); + }, + [onBlur] + ); return ( <> diff --git a/packages/web/i18n/en/common.json b/packages/web/i18n/en/common.json index 70d82483e..644fab1f5 100644 --- a/packages/web/i18n/en/common.json +++ b/packages/web/i18n/en/common.json @@ -866,7 +866,7 @@ "Key already exists": "Key already exists", "Key cannot be empty": "Parameter name cannot be empty", "Props name": "Parameter name", - "Props tip": "Can set HTTP request related parameters\nCan use {{key}} to call global variables or external parameter input, currently available variables:\n{{variable}}", + "Props tip": "Can set HTTP request related parameters\nCan use / to call variables, currently available variables:\n{{variable}}", "Props value": "Parameter value", "ResponseChatItemId": "AI response ID", "Url and params have been split": "Path parameters have been automatically added to Params", diff --git a/packages/web/i18n/en/workflow.json b/packages/web/i18n/en/workflow.json index a9e3f36b2..6fba464dc 100644 --- a/packages/web/i18n/en/workflow.json +++ b/packages/web/i18n/en/workflow.json @@ -58,6 +58,9 @@ "greater_than": "greater than", "greater_than_or_equal_to": "Greater than or equal to", "greeting": "greet", + "http": { + "body_none": "This request has no body parameters." + }, "http_raw_response_description": "The raw response of the HTTP request. \nOnly string or JSON type response data can be accepted.", "http_request": "HTTP request", "http_request_error_info": "HTTP request error information, returns empty when successful", diff --git a/packages/web/i18n/zh/common.json b/packages/web/i18n/zh/common.json index 3aafcdd3b..8a86bd9dc 100644 --- a/packages/web/i18n/zh/common.json +++ b/packages/web/i18n/zh/common.json @@ -112,7 +112,6 @@ "common": { "Action": "操作", "Add": "添加", - "copy_to_clipboard": "复制到剪贴板", "Add New": "新增", "Add Success": "添加成功", "All": "全部", @@ -123,7 +122,6 @@ "Confirm": "确认", "Confirm Create": "确认创建", "Confirm Import": "确认导入", - "export_to_json": "导出为 JSON", "Confirm Move": "移动到这", "Confirm Update": "确认更新", "Confirm to leave the page": "确认离开该页面?", @@ -195,7 +193,6 @@ "Save Success": "保存成功", "Save_and_exit": "保存并退出", "Search": "搜索", - "json_config": "JSON 配置", "Select File Failed": "选择文件异常", "Select template": "选择模板", "Set Avatar": "点击设置头像", @@ -230,6 +227,7 @@ "confirm": { "Common Tip": "操作确认" }, + "copy_to_clipboard": "复制到剪贴板", "course": { "Read Course": "查看教程" }, @@ -241,6 +239,7 @@ "too_many_request": "请求太频繁了,请稍后重试。", "unKnow": "出现了点意外~" }, + "export_to_json": "导出为 JSON", "failed": "失败", "folder": { "Drag Tip": "点我可拖动", @@ -260,6 +259,7 @@ "jsonEditor": { "Parse error": "JSON 可能有误,请仔细检查" }, + "json_config": "JSON 配置", "link": { "UnValid": "无效的链接" }, @@ -647,7 +647,8 @@ "success": "开始同步" } }, - "training": {} + "training": { + } }, "data": { "Auxiliary Data": "辅助数据", @@ -865,7 +866,7 @@ "Key already exists": "Key 已经存在", "Key cannot be empty": "参数名不能为空", "Props name": "参数名", - "Props tip": "可以设置 HTTP 请求的相关参数\n可通过 {{key}} 来调用全局变量或外部参数输入,当前可使用变量:\n{{variable}}", + "Props tip": "可以设置 HTTP 请求的相关参数\n可通过输入 / 来调用变量,当前可使用变量:\n{{variable}}", "Props value": "参数值", "ResponseChatItemId": "AI 回复的 ID", "Url and params have been split": "路径参数已被自动加入 Params 中", diff --git a/packages/web/i18n/zh/workflow.json b/packages/web/i18n/zh/workflow.json index 476823b2e..a5ee946e3 100644 --- a/packages/web/i18n/zh/workflow.json +++ b/packages/web/i18n/zh/workflow.json @@ -58,6 +58,9 @@ "greater_than": "大于", "greater_than_or_equal_to": "大于等于", "greeting": "打招呼", + "http": { + "body_none": "该请求没有 Body 体" + }, "http_raw_response_description": "HTTP请求的原始响应。只能接受字符串或JSON类型响应数据。", "http_request": "HTTP 请求", "http_request_error_info": "HTTP请求错误信息,成功时返回空", diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeHttp/index.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeHttp/index.tsx index d88c186cc..2db01bfab 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeHttp/index.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeHttp/index.tsx @@ -24,15 +24,18 @@ import { NumberDecrementStepper, NumberInput } from '@chakra-ui/react'; -import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants'; +import { + ContentTypes, + NodeInputKeyEnum, + WorkflowIOValueTypeEnum +} from '@fastgpt/global/core/workflow/constants'; import { useTranslation } from 'next-i18next'; import LightRowTabs from '@fastgpt/web/components/common/Tabs/LightRowTabs'; import MyIcon from '@fastgpt/web/components/common/Icon'; import { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io.d'; import { useToast } from '@fastgpt/web/hooks/useToast'; import JSONEditor from '@fastgpt/web/components/common/Textarea/JsonEditor'; -import { formatEditorVariablePickerIcon } from '@fastgpt/global/core/workflow/utils'; -import { EditorVariablePickerType } from '@fastgpt/web/components/common/Textarea/PromptEditor/type'; +import { EditorVariableLabelPickerType } from '@fastgpt/web/components/common/Textarea/PromptEditor/type'; import HttpInput from '@fastgpt/web/components/common/Input/HttpInput'; import dynamic from 'next/dynamic'; import MySelect from '@fastgpt/web/components/common/MySelect'; @@ -40,52 +43,22 @@ import RenderToolInput from '../render/RenderToolInput'; import IOTitle from '../../components/IOTitle'; import { useContextSelector } from 'use-context-selector'; import { WorkflowContext } from '../../../context'; -import { getWorkflowGlobalVariables } from '@/web/core/workflow/utils'; -import { useMemoizedFn } from 'ahooks'; +import { useCreation, useMemoizedFn } from 'ahooks'; import { AppContext } from '@/pages/app/detail/components/context'; import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip'; +import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; +import { getEditorVariables } from '../../../utils'; +import PromptEditor from '@fastgpt/web/components/common/Textarea/PromptEditor'; const CurlImportModal = dynamic(() => import('./CurlImportModal')); -export const HttpHeaders = [ - { key: 'A-IM', label: 'A-IM' }, - { key: 'Accept', label: 'Accept' }, - { key: 'Accept-Charset', label: 'Accept-Charset' }, - { key: 'Accept-Encoding', label: 'Accept-Encoding' }, - { key: 'Accept-Language', label: 'Accept-Language' }, - { key: 'Accept-Datetime', label: 'Accept-Datetime' }, - { key: 'Access-Control-Request-Method', label: 'Access-Control-Request-Method' }, - { key: 'Access-Control-Request-Headers', label: 'Access-Control-Request-Headers' }, - { key: 'Authorization', label: 'Authorization' }, - { key: 'Cache-Control', label: 'Cache-Control' }, - { key: 'Connection', label: 'Connection' }, - { key: 'Content-Length', label: 'Content-Length' }, - { key: 'Content-Type', label: 'Content-Type' }, - { key: 'Cookie', label: 'Cookie' }, - { key: 'Date', label: 'Date' }, - { key: 'Expect', label: 'Expect' }, - { key: 'Forwarded', label: 'Forwarded' }, - { key: 'From', label: 'From' }, - { key: 'Host', label: 'Host' }, - { key: 'If-Match', label: 'If-Match' }, - { key: 'If-Modified-Since', label: 'If-Modified-Since' }, - { key: 'If-None-Match', label: 'If-None-Match' }, - { key: 'If-Range', label: 'If-Range' }, - { key: 'If-Unmodified-Since', label: 'If-Unmodified-Since' }, - { key: 'Max-Forwards', label: 'Max-Forwards' }, - { key: 'Origin', label: 'Origin' }, - { key: 'Pragma', label: 'Pragma' }, - { key: 'Proxy-Authorization', label: 'Proxy-Authorization' }, - { key: 'Range', label: 'Range' }, - { key: 'Referer', label: 'Referer' }, - { key: 'TE', label: 'TE' }, - { key: 'User-Agent', label: 'User-Agent' }, - { key: 'Upgrade', label: 'Upgrade' }, - { key: 'Via', label: 'Via' }, - { key: 'Warning', label: 'Warning' }, - { key: 'Dnt', label: 'Dnt' }, - { key: 'X-Requested-With', label: 'X-Requested-With' }, - { key: 'X-CSRF-Token', label: 'X-CSRF-Token' } -]; +const defaultFormBody = { + key: NodeInputKeyEnum.httpFormBody, + renderTypeList: [FlowNodeInputTypeEnum.hidden], + valueType: WorkflowIOValueTypeEnum.any, + value: [], + label: '', + required: false +}; enum TabEnum { params = 'params', @@ -260,7 +233,7 @@ export function RenderHttpProps({ const { t } = useTranslation(); const [selectedTab, setSelectedTab] = useState(TabEnum.params); const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList); - const getNodeDynamicInputs = useContextSelector(WorkflowContext, (v) => v.getNodeDynamicInputs); + const edges = useContextSelector(WorkflowContext, (v) => v.edges); const { appDetail } = useContextSelector(AppContext, (v) => v); @@ -268,21 +241,23 @@ export function RenderHttpProps({ const params = inputs.find((item) => item.key === NodeInputKeyEnum.httpParams); const headers = inputs.find((item) => item.key === NodeInputKeyEnum.httpHeaders); const jsonBody = inputs.find((item) => item.key === NodeInputKeyEnum.httpJsonBody); + const formBody = + inputs.find((item) => item.key === NodeInputKeyEnum.httpFormBody) || defaultFormBody; + const contentType = inputs.find((item) => item.key === NodeInputKeyEnum.httpContentType); const paramsLength = params?.value?.length || 0; const headersLength = headers?.value?.length || 0; // get variable - const variables = useMemo(() => { - const globalVariables = getWorkflowGlobalVariables({ - nodes: nodeList, - chatConfig: appDetail.chatConfig + const variables = useCreation(() => { + return getEditorVariables({ + nodeId, + nodeList, + edges, + appDetail, + t }); - - const nodeVariables = formatEditorVariablePickerIcon(getNodeDynamicInputs(nodeId)); - - return [...nodeVariables, ...globalVariables]; - }, [appDetail.chatConfig, getNodeDynamicInputs, nodeId, nodeList]); + }, [nodeList, edges, inputs, t]); const variableText = useMemo(() => { return variables @@ -310,10 +285,11 @@ export function RenderHttpProps({ + /> width={'100%'} + mb={selectedTab === TabEnum.body ? 1 : 2} list={[ { label: , value: TabEnum.params }, ...(!['GET', 'DELETE'].includes(requestMethods) @@ -337,33 +313,31 @@ export function RenderHttpProps({ value={selectedTab} onChange={setSelectedTab} /> - + {params && headers && jsonBody && { - [TabEnum.params]: ( - , + [TabEnum.body]: ( + ), - [TabEnum.body]: , [TabEnum.headers]: ( - + ) }[selectedTab]} ); }, [ + contentType, + formBody, headersLength, nodeId, paramsLength, @@ -433,13 +407,11 @@ const RenderHttpTimeout = ({ const RenderForm = ({ nodeId, input, - variables, - tabType + variables }: { nodeId: string; input: FlowNodeInputItemType; - variables: EditorVariablePickerType[]; - tabType?: TabEnum; + variables: EditorVariableLabelPickerType[]; }) => { const { t } = useTranslation(); const { toast } = useToast(); @@ -449,13 +421,6 @@ const RenderForm = ({ const [updateTrigger, setUpdateTrigger] = useState(false); const [shouldUpdateNode, setShouldUpdateNode] = useState(false); - const leftVariables = useMemo(() => { - return (tabType === TabEnum.headers ? HttpHeaders : variables).filter((variable) => { - const existVariables = list.map((item) => item.key); - return !existVariables.includes(variable.key); - }); - }, [list, tabType, variables]); - useEffect(() => { setList(input.value || []); }, [input.value]); @@ -529,7 +494,7 @@ const RenderForm = ({ const Render = useMemo(() => { return ( - + @@ -545,29 +510,25 @@ const RenderForm = ({ {list.map((item, index) => ( - - ))} - -
+ { - handleKeyChange(index, value); - setUpdateTrigger((prev) => !prev); - }} placeholder={t('common:core.module.http.Props name')} value={item.key} - variables={leftVariables} + variableLabels={variables} + variables={variables} onBlur={(val) => { handleKeyChange(index, val); }} updateTrigger={updateTrigger} /> + { setList((prevList) => prevList.map((item, i) => @@ -592,17 +553,12 @@ const RenderForm = ({
+ { - handleAddNewProps(val); - setUpdateTrigger((prev) => !prev); - }} placeholder={t('common:core.module.http.Add props')} value={''} - variables={leftVariables} + variableLabels={variables} + variables={variables} updateTrigger={updateTrigger} onBlur={(val) => { handleAddNewProps(val); @@ -610,7 +566,7 @@ const RenderForm = ({ }} /> + @@ -621,63 +577,158 @@ const RenderForm = ({ ); - }, [ - handleAddNewProps, - handleKeyChange, - input.key, - leftVariables, - list, - t, - tabType, - updateTrigger, - variables - ]); + }, [handleAddNewProps, handleKeyChange, input.key, list, t, updateTrigger, variables]); return Render; }; -const RenderJson = ({ +const RenderBody = ({ nodeId, - input, + jsonBody, + formBody, + typeInput, variables }: { nodeId: string; - input: FlowNodeInputItemType; - variables: EditorVariablePickerType[]; + jsonBody: FlowNodeInputItemType; + formBody: FlowNodeInputItemType; + typeInput: FlowNodeInputItemType | undefined; + variables: EditorVariableLabelPickerType[]; }) => { const { t } = useTranslation(); const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode); const [_, startSts] = useTransition(); + useEffect(() => { + if (typeInput === undefined) { + onChangeNode({ + nodeId, + type: 'addInput', + value: { + key: NodeInputKeyEnum.httpContentType, + renderTypeList: [FlowNodeInputTypeEnum.hidden], + valueType: WorkflowIOValueTypeEnum.string, + value: ContentTypes.json, + label: '', + required: false + } + }); + } + }, [nodeId, onChangeNode, typeInput]); + const Render = useMemo(() => { return ( - - { - startSts(() => { - onChangeNode({ - nodeId, - type: 'updateInput', - key: input.key, - value: { - ...input, - value: e - } + + + {Object.values(ContentTypes).map((item) => ( + { + onChangeNode({ + nodeId, + type: 'updateInput', + key: NodeInputKeyEnum.httpContentType, + value: { + key: NodeInputKeyEnum.httpContentType, + renderTypeList: [FlowNodeInputTypeEnum.hidden], + valueType: WorkflowIOValueTypeEnum.string, + value: item, + label: '', + required: false + } + }); + }} + cursor={'pointer'} + whiteSpace={'nowrap'} + > + {item} + + ))} + + {typeInput?.value === ContentTypes.none && ( + + {t('workflow:http.body_none')} + + )} + {(typeInput?.value === ContentTypes.formData || + typeInput?.value === ContentTypes.xWwwFormUrlencoded) && ( + + )} + {typeInput?.value === ContentTypes.json && ( + { + startSts(() => { + onChangeNode({ + nodeId, + type: 'updateInput', + key: jsonBody.key, + value: { + ...jsonBody, + value: e + } + }); }); - }); - }} - variables={variables} - /> + }} + variables={variables} + /> + )} + {(typeInput?.value === ContentTypes.xml || typeInput?.value === ContentTypes.raw) && ( + { + startSts(() => { + onChangeNode({ + nodeId, + type: 'updateInput', + key: jsonBody.key, + value: { + ...jsonBody, + value: e + } + }); + }); + }} + showOpenModal={false} + variableLabels={variables} + h={200} + /> + )} ); - }, [input, nodeId, onChangeNode, t, variables]); - + }, [typeInput?.value, t, nodeId, formBody, variables, jsonBody, onChangeNode]); return Render; }; + const RenderPropsItem = ({ text, num }: { text: string; num: number }) => { return ( @@ -709,7 +760,7 @@ const NodeHttp = ({ data, selected }: NodeProps) => { [NodeInputKeyEnum.httpHeaders]: Headers, [NodeInputKeyEnum.httpTimeout]: HttpTimeout }; - }, [Headers, HttpMethodAndUrl]); + }, [Headers, HttpMethodAndUrl, HttpTimeout]); return ( diff --git a/projects/app/src/pages/app/list/components/HttpPluginEditModal.tsx b/projects/app/src/pages/app/list/components/HttpPluginEditModal.tsx index d6d88c2ec..9bf703c00 100644 --- a/projects/app/src/pages/app/list/components/HttpPluginEditModal.tsx +++ b/projects/app/src/pages/app/list/components/HttpPluginEditModal.tsx @@ -34,7 +34,6 @@ import { str2OpenApiSchema } from '@fastgpt/global/core/app/httpPlugin/utils'; import MyIcon from '@fastgpt/web/components/common/Icon'; import MyModal from '@fastgpt/web/components/common/MyModal'; import HttpInput from '@fastgpt/web/components/common/Input/HttpInput'; -import { HttpHeaders } from '../../detail/components/WorkflowComponents/Flow/nodes/NodeHttp'; import { OpenApiJsonSchema } from '@fastgpt/global/core/app/httpPlugin/type'; import { AppSchema } from '@fastgpt/global/core/app/type'; import { useContextSelector } from 'use-context-selector'; @@ -168,15 +167,6 @@ const HttpPluginEditModal = ({ errorToast: t('common:plugin.Invalid Schema') }); - const leftVariables = useMemo( - () => - HttpHeaders.filter((variable) => { - const existVariables = customHeaders.map((item) => item.key); - return !existVariables.includes(variable.key); - }), - [customHeaders] - ); - useEffect(() => { (async () => { if (!apiSchemaStr) { @@ -315,28 +305,8 @@ const HttpPluginEditModal = ({
{ - setCustomHeaders((prev) => { - const newHeaders = prev.map((item, i) => - i === index ? { ...item, key: val } : item - ); - setValue( - 'pluginData.customHeaders', - '{\n' + - newHeaders - .map((item) => `"${item.key}":"${item.value}"`) - .join(',\n') + - '\n}' - ); - return newHeaders; - }); - setUpdateTrigger((prev) => !prev); - }} placeholder={t('common:core.module.http.Props name')} value={item.key} - variables={leftVariables} onBlur={(val) => { setCustomHeaders((prev) => { const newHeaders = prev.map((item, i) => @@ -360,7 +330,6 @@ const HttpPluginEditModal = ({ setCustomHeaders((prev) => { @@ -406,26 +375,8 @@ const HttpPluginEditModal = ({
{ - setCustomHeaders((prev) => { - const newHeaders = [...prev, { key: val, value: '' }]; - setValue( - 'pluginData.customHeaders', - '{\n' + - newHeaders - .map((item) => `"${item.key}":"${item.value}"`) - .join(',\n') + - '\n}' - ); - return newHeaders; - }); - setUpdateTrigger((prev) => !prev); - }} placeholder={t('common:core.module.http.Add props')} value={''} - variables={leftVariables} updateTrigger={updateTrigger} onBlur={(val) => { if (!val) return;