feat: http body type & http input support editor variable (#2603)

* feat: http body type & http input support editor variable

* fix type

* chore: code

* code
This commit is contained in:
heheer
2024-09-03 23:43:21 +08:00
committed by GitHub
parent a7569037fe
commit 85a11d08b2
11 changed files with 407 additions and 258 deletions

View File

@@ -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'
}

View File

@@ -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: [

View File

@@ -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<string, any>;
[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<HttpResponse> => {
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<H
system_httpHeader: httpHeader,
system_httpParams: httpParams = [],
system_httpJsonBody: httpJsonBody,
system_httpFormBody: httpFormBody,
system_httpContentType: httpContentType = ContentTypes.json,
system_httpTimeout: httpTimeout = 60,
[NodeInputKeyEnum.addInputParam]: dynamicInput,
...body
@@ -77,21 +93,41 @@ export const dispatchHttp468Request = async (props: HttpRequestProps): Promise<H
// ...dynamicInput,
...systemVariables
};
const allVariables = {
[NodeInputKeyEnum.addInputParam]: concatVariables,
...concatVariables
};
httpReqUrl = replaceVariable(httpReqUrl, allVariables);
// parse header
const headers = await (() => {
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<string, string>, 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<H
return Promise.reject('Header 为非法 JSON 格式');
}
})();
const params = httpParams.reduce((acc: Record<string, string>, 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<H
// format output value type
const results: Record<string, any> = {};
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,

View File

@@ -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 (
<Flex
position={'relative'}
@@ -133,14 +124,12 @@ export default function Editor({
});
}}
/>
{hasVariablePlugin ? <VariablePickerPlugin variables={variables} /> : ''}
<VariablePlugin variables={variables} />
<VariableLabelPlugin variables={variableLabels} />
<VariableLabelPickerPlugin variables={variableLabels} isFocus={focus} />
<OnBlurPlugin onBlur={onBlur} />
<SingleLinePlugin />
</LexicalComposer>
{focus && hasDropDownPlugin && (
<DropDownMenu variables={dropdownVariables} setDropdownValue={setDropdownValue} />
)}
</Flex>
);
}

View File

@@ -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 (
<>
<Editor
hasVariablePlugin={hasVariablePlugin}
hasDropDownPlugin={hasDropDownPlugin}
variables={variables}
variableLabels={variableLabels}
h={h}
value={value}
currentValue={currentValue}
onChange={onChangeInput}
onBlur={onBlurInput}
placeholder={placeholder}
setDropdownValue={setDropdownValue}
updateTrigger={updateTrigger}
/>
</>

View File

@@ -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",

View File

@@ -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",

View File

@@ -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 中",

View File

@@ -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请求错误信息成功时返回空",