Skip to content

Commit

Permalink
feat: http body type & http input support editor variable (#2603)
Browse files Browse the repository at this point in the history
* feat: http body type & http input support editor variable

* fix type

* chore: code

* code
  • Loading branch information
newfish-cmyk authored Sep 3, 2024
1 parent a756903 commit 85a11d0
Show file tree
Hide file tree
Showing 11 changed files with 407 additions and 258 deletions.
12 changes: 12 additions & 0 deletions packages/global/core/workflow/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',

Expand Down Expand Up @@ -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'
}
22 changes: 21 additions & 1 deletion packages/global/core/workflow/template/system/http468.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -82,13 +83,32 @@ export const HttpNode468: FlowNodeTemplateType = {
label: '',
required: false
},
// json body data
{
key: NodeInputKeyEnum.httpJsonBody,
renderTypeList: [FlowNodeInputTypeEnum.hidden],
valueType: WorkflowIOValueTypeEnum.any,
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: [
Expand Down
158 changes: 137 additions & 21 deletions packages/service/core/workflow/dispatch/tools/http468.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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: {
Expand All @@ -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
Expand All @@ -77,50 +93,151 @@ 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;
}, {});
} catch (error) {
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}`);
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -213,7 +330,6 @@ async function fetchData({
baseURL: `http://${SERVICE_LOCAL_HOST}`,
url,
headers: {
'Content-Type': 'application/json',
...headers
},
timeout: timeout * 1000,
Expand Down
35 changes: 12 additions & 23 deletions packages/web/components/common/Input/HttpInput/Editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand All @@ -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;
Expand All @@ -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'}
Expand Down Expand Up @@ -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>
);
}
Loading

0 comments on commit 85a11d0

Please sign in to comment.