add manual create http toolset (#5743)

* add manual create http toolset

* optimize code

* optimize

* fix

* fix
This commit is contained in:
heheer
2025-10-10 19:53:39 +08:00
committed by archer
parent 12096f6c58
commit 8fffd7b6ce
21 changed files with 1215 additions and 189 deletions

View File

@@ -13,9 +13,9 @@ import { i18nT } from '../../../../web/i18n/utils';
export const getHTTPToolSetRuntimeNode = ({
name,
avatar,
baseUrl = '',
customHeaders = '',
apiSchemaStr = '',
baseUrl,
customHeaders,
apiSchemaStr,
toolList = [],
headerSecret
}: {
@@ -34,12 +34,11 @@ export const getHTTPToolSetRuntimeNode = ({
intro: 'HTTP Tools',
toolConfig: {
httpToolSet: {
baseUrl,
toolList,
headerSecret,
customHeaders,
apiSchemaStr,
toolId: ''
...(baseUrl !== undefined && { baseUrl }),
...(apiSchemaStr !== undefined && { apiSchemaStr }),
...(customHeaders !== undefined && { customHeaders }),
...(headerSecret !== undefined && { headerSecret })
}
},
inputs: [],

View File

@@ -2,6 +2,7 @@ import type { FlowNodeTemplateType, StoreNodeItemType } from '../workflow/type/n
import type { AppTypeEnum } from './constants';
import { PermissionTypeEnum } from '../../support/permission/constant';
import type {
ContentTypes,
NodeInputKeyEnum,
VariableInputEnum,
WorkflowIOValueTypeEnum
@@ -127,6 +128,16 @@ export type HttpToolConfigType = {
outputSchema: JSONSchemaOutputType;
path: string;
method: string;
// manual
staticParams?: Array<{ key: string; value: string }>;
staticHeaders?: Array<{ key: string; value: string }>;
staticBody?: {
type: ContentTypes;
content?: string;
formData?: Array<{ key: string; value: string }>;
};
headerSecret?: StoreSecretValueType;
};
/* app chat config type */

View File

@@ -477,6 +477,19 @@ export enum ContentTypes {
raw = 'raw-text'
}
export 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'
};
// http request methods
export const HTTP_METHODS = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'] as const;
export type HttpMethod = (typeof HTTP_METHODS)[number];
export const ArrayTypeMap: Record<WorkflowIOValueTypeEnum, WorkflowIOValueTypeEnum> = {
[WorkflowIOValueTypeEnum.string]: WorkflowIOValueTypeEnum.arrayString,
[WorkflowIOValueTypeEnum.number]: WorkflowIOValueTypeEnum.arrayNumber,

View File

@@ -52,11 +52,10 @@ export type NodeToolConfigType = {
}[];
};
httpToolSet?: {
toolId: string;
baseUrl: string;
toolList: HttpToolConfigType[];
apiSchemaStr: string;
customHeaders: string;
baseUrl?: string;
apiSchemaStr?: string;
customHeaders?: string;
headerSecret?: StoreSecretValueType;
};
httpTool?: {

View File

@@ -3,6 +3,8 @@ import { getSecretValue } from '../../common/secret/utils';
import axios from 'axios';
import { getErrText } from '@fastgpt/global/common/error/utils';
import type { RequireOnlyOne } from '@fastgpt/global/common/type/utils';
import type { HttpToolConfigType } from '@fastgpt/global/core/app/type';
import { contentTypeMap, ContentTypes } from '@fastgpt/global/core/workflow/constants';
export type RunHTTPToolParams = {
baseUrl: string;
@@ -11,6 +13,9 @@ export type RunHTTPToolParams = {
params: Record<string, any>;
headerSecret?: StoreSecretValueType;
customHeaders?: Record<string, string>;
staticParams?: HttpToolConfigType['staticParams'];
staticHeaders?: HttpToolConfigType['staticHeaders'];
staticBody?: HttpToolConfigType['staticBody'];
};
export type RunHTTPToolResult = RequireOnlyOne<{
@@ -18,41 +23,130 @@ export type RunHTTPToolResult = RequireOnlyOne<{
errorMsg?: string;
}>;
export async function runHTTPTool({
const buildHttpRequest = ({
method,
params,
headerSecret,
customHeaders,
staticParams,
staticHeaders,
staticBody
}: Omit<RunHTTPToolParams, 'baseUrl' | 'toolPath'>) => {
const body = (() => {
if (!staticBody || staticBody.type === ContentTypes.none) {
return ['POST', 'PUT', 'PATCH'].includes(method.toUpperCase()) ? params : undefined;
}
if (staticBody.type === ContentTypes.json) {
const staticContent = staticBody.content ? JSON.parse(staticBody.content) : {};
return { ...staticContent, ...params };
}
if (staticBody.type === ContentTypes.formData) {
const formData = new (require('form-data'))();
staticBody.formData?.forEach(({ key, value }) => {
formData.append(key, value);
});
Object.entries(params).forEach(([key, value]) => {
formData.append(key, value);
});
return formData;
}
if (staticBody.type === ContentTypes.xWwwFormUrlencoded) {
const urlencoded = new URLSearchParams();
staticBody.formData?.forEach(({ key, value }) => {
urlencoded.append(key, value);
});
Object.entries(params).forEach(([key, value]) => {
urlencoded.append(key, String(value));
});
return urlencoded.toString();
}
if (staticBody.type === ContentTypes.xml || staticBody.type === ContentTypes.raw) {
return staticBody.content || '';
}
return undefined;
})();
const contentType = contentTypeMap[staticBody?.type || ContentTypes.none];
const headers = {
...(contentType && { 'Content-Type': contentType }),
...(customHeaders || {}),
...(headerSecret ? getSecretValue({ storeSecret: headerSecret }) : {}),
...(staticHeaders?.reduce(
(acc, { key, value }) => {
acc[key] = value;
return acc;
},
{} as Record<string, string>
) || {})
};
const queryParams = (() => {
const staticParamsObj =
staticParams?.reduce(
(acc, { key, value }) => {
acc[key] = value;
return acc;
},
{} as Record<string, any>
) || {};
const mergedParams =
method.toUpperCase() === 'GET' || staticParams
? { ...staticParamsObj, ...params }
: staticParamsObj;
return Object.keys(mergedParams).length > 0 ? mergedParams : undefined;
})();
return {
headers,
body,
queryParams
};
};
export const runHTTPTool = async ({
baseUrl,
toolPath,
method = 'POST',
params,
headerSecret,
customHeaders
}: RunHTTPToolParams): Promise<RunHTTPToolResult> {
customHeaders,
staticParams,
staticHeaders,
staticBody
}: RunHTTPToolParams): Promise<RunHTTPToolResult> => {
try {
const headers = {
'Content-Type': 'application/json',
...(customHeaders || {}),
...(headerSecret ? getSecretValue({ storeSecret: headerSecret }) : {})
};
const { headers, body, queryParams } = buildHttpRequest({
method,
params,
headerSecret,
customHeaders,
staticParams,
staticHeaders,
staticBody
});
const { data } = await axios({
method: method.toUpperCase(),
baseURL: baseUrl.startsWith('https://') ? baseUrl : `https://${baseUrl}`,
url: toolPath,
headers,
data: params,
params,
data: body,
params: queryParams,
timeout: 300000,
httpsAgent: new (require('https').Agent)({
rejectUnauthorized: false
})
});
return {
data
};
return { data };
} catch (error: any) {
console.log(error);
return {
errorMsg: getErrText(error)
};
return { errorMsg: getErrText(error) };
}
}
};

View File

@@ -236,16 +236,19 @@ export const dispatchRunTool = async (props: RunToolProps): Promise<RunToolRespo
}
const { data, errorMsg } = await runHTTPTool({
baseUrl: baseUrl,
baseUrl: baseUrl || '',
toolPath: httpTool.path,
method: httpTool.method,
params,
headerSecret,
headerSecret: httpTool.headerSecret || headerSecret,
customHeaders: customHeaders
? typeof customHeaders === 'string'
? JSON.parse(customHeaders)
: customHeaders
: undefined
: undefined,
staticParams: httpTool.staticParams,
staticHeaders: httpTool.staticHeaders,
staticBody: httpTool.staticBody
});
if (errorMsg) {

View File

@@ -1,5 +1,6 @@
import { getErrText } from '@fastgpt/global/common/error/utils';
import {
contentTypeMap,
ContentTypes,
NodeInputKeyEnum,
NodeOutputKeyEnum,
@@ -59,15 +60,6 @@ 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, teamId, tmbId },

View File

@@ -25,6 +25,7 @@ const LeftRadio = <T = any,>({
align = 'center',
px = 3.5,
py = 4,
gridGap = [3, 5],
defaultBg = 'myGray.50',
activeBg = 'primary.50',
onChange,
@@ -75,7 +76,7 @@ const LeftRadio = <T = any,>({
);
return (
<Grid gridGap={[3, 5]} fontSize={['sm', 'md']} {...props}>
<Grid gridGap={gridGap} fontSize={['sm', 'md']} {...props}>
{list.map((item) => {
const isActive = value === item.value;
return (
@@ -131,7 +132,7 @@ const LeftRadio = <T = any,>({
lineHeight={1}
color={'myGray.900'}
>
<Box>{t(item.title as any)}</Box>
<Box mb={1}>{t(item.title as any)}</Box>
{!!item.tooltip && <QuestionTip label={item.tooltip} color={'myGray.600'} />}
</HStack>
) : (

View File

@@ -1,7 +1,12 @@
{
"Add_tool": "Add tool",
"AutoOptimize": "Automatic optimization",
"Click_to_delete_this_field": "Click to delete this field",
"Custom_params": "Custom parameters",
"Edit_tool": "Edit tool",
"Filed_is_deprecated": "This field is deprecated",
"HTTPTools_Create_Type": "Create Type",
"HTTPTools_Create_Type_Tip": "Modification is not supported after selection",
"HTTP_tools_list_with_number": "Tool list: {{total}}",
"Index": "Index",
"MCP_tools_debug": "debug",
@@ -30,6 +35,7 @@
"Selected": "Selected",
"Start_config": "Start configuration",
"Team_Tags": "Team tags",
"Tool_name": "Tool name",
"ai_point_price": "Billing",
"ai_settings": "AI Configuration",
"all_apps": "All Applications",
@@ -283,6 +289,7 @@
"tool_detail": "Tool details",
"tool_input_param_tip": "This plugin requires configuration of related information to run properly.",
"tool_not_active": "This tool has not been activated yet",
"tool_params_description_tips": "The description of parameter functions, if used as tool invocation parameters, affects the model tool invocation effect.",
"tool_run_free": "This tool runs without points consumption",
"tool_tip": "When executed as a tool, is this field used as a tool response result?",
"tool_type_tools": "tool",

View File

@@ -1,7 +1,12 @@
{
"Add_tool": "添加工具",
"AutoOptimize": "自动优化",
"Click_to_delete_this_field": "点击删除该字段",
"Custom_params": "自定义参数",
"Edit_tool": "编辑工具",
"Filed_is_deprecated": "该字段已弃用",
"HTTPTools_Create_Type": "创建方式",
"HTTPTools_Create_Type_Tip": "选择后不支持修改",
"HTTP_tools_detail": "查看详情",
"HTTP_tools_list_with_number": "工具列表: {{total}}",
"Index": "索引",
@@ -31,6 +36,8 @@
"Selected": "已选择",
"Start_config": "开始配置",
"Team_Tags": "团队标签",
"Tool_description": "工具描述",
"Tool_name": "工具名称",
"ai_point_price": "AI积分计费",
"ai_settings": "AI 配置",
"all_apps": "全部应用",
@@ -90,6 +97,7 @@
"document_upload": "文档上传",
"edit_app": "应用详情",
"edit_info": "编辑信息",
"edit_param": "编辑参数",
"execute_time": "执行时间",
"export_config_successful": "已复制配置,自动过滤部分敏感信息,请注意检查是否仍有敏感数据",
"export_configs": "导出配置",
@@ -297,6 +305,7 @@
"tool_detail": "工具详情",
"tool_input_param_tip": "该插件正常运行需要配置相关信息",
"tool_not_active": "该工具尚未激活",
"tool_params_description_tips": "参数功能的描述,若作为工具调用参数,影响模型工具调用效果",
"tool_run_free": "该工具运行无积分消耗",
"tool_tip": "作为工具执行时,该字段是否作为工具响应结果",
"tool_type_tools": "工具",

View File

@@ -1,7 +1,11 @@
{
"Add_tool": "添加工具",
"AutoOptimize": "自動優化",
"Click_to_delete_this_field": "點擊刪除該字段",
"Custom_params": "自定義參數",
"Filed_is_deprecated": "該字段已棄用",
"HTTPTools_Create_Type": "創建方式",
"HTTPTools_Create_Type_Tip": "選擇後不支持修改",
"HTTP_tools_list_with_number": "工具列表: {{total}}",
"Index": "索引",
"MCP_tools_debug": "偵錯",
@@ -30,6 +34,8 @@
"Selected": "已選擇",
"Start_config": "開始配置",
"Team_Tags": "團隊標籤",
"Tool_description": "工具描述",
"Tool_name": "工具名稱",
"ai_point_price": "AI 積分計費",
"ai_settings": "AI 設定",
"all_apps": "所有應用程式",
@@ -283,6 +289,7 @@
"tool_detail": "工具詳情",
"tool_input_param_tip": "這個外掛正常執行需要設定相關資訊",
"tool_not_active": "該工具尚未激活",
"tool_params_description_tips": "參數功能的描述,若作為工具調用參數,影響模型工具調用效果",
"tool_run_free": "該工具運行無積分消耗",
"tool_tip": "作為工具執行時,該字段是否作為工具響應結果",
"tool_type_tools": "工具",

View File

@@ -56,10 +56,13 @@ const ChatTest = ({
return await postRunHTTPTool({
baseUrl,
params: data,
headerSecret,
headerSecret: currentTool.headerSecret || headerSecret,
toolPath: currentTool.path,
method: currentTool.method,
customHeaders: customHeaders
customHeaders: customHeaders,
staticParams: currentTool.staticParams,
staticHeaders: currentTool.staticHeaders,
staticBody: currentTool.staticBody
});
},
{

View File

@@ -0,0 +1,107 @@
import MyModal from '@fastgpt/web/components/common/MyModal';
import React from 'react';
import { useTranslation } from 'next-i18next';
import { Button, ModalBody, ModalFooter, Textarea } from '@chakra-ui/react';
import { useForm } from 'react-hook-form';
import { parseCurl } from '@fastgpt/global/common/string/http';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { type HttpMethod, ContentTypes } from '@fastgpt/global/core/workflow/constants';
import type { ParamItemType } from './ManualToolModal';
export type CurlImportResult = {
method: HttpMethod;
path: string;
params?: ParamItemType[];
headers?: ParamItemType[];
bodyType: string;
bodyContent?: string;
bodyFormData?: ParamItemType[];
};
type CurlImportModalProps = {
onClose: () => void;
onImport: (result: CurlImportResult) => void;
};
const CurlImportModal = ({ onClose, onImport }: CurlImportModalProps) => {
const { t } = useTranslation();
const { toast } = useToast();
const { register, handleSubmit } = useForm({
defaultValues: {
curlContent: ''
}
});
const handleCurlImport = (data: { curlContent: string }) => {
try {
const parsed = parseCurl(data.curlContent);
const convertToParamItemType = (
items: Array<{ key: string; value?: string; type?: string }>
): ParamItemType[] => {
return items.map((item) => ({
key: item.key,
value: item.value || ''
}));
};
const bodyType = (() => {
if (!parsed.body || parsed.body === '{}') {
return ContentTypes.none;
}
return ContentTypes.json;
})();
const result: CurlImportResult = {
method: parsed.method as HttpMethod,
path: parsed.url,
params: parsed.params.length > 0 ? convertToParamItemType(parsed.params) : undefined,
headers: parsed.headers.length > 0 ? convertToParamItemType(parsed.headers) : undefined,
bodyType,
bodyContent: bodyType === ContentTypes.json ? parsed.body : undefined
};
onImport(result);
toast({
title: t('common:import_success'),
status: 'success'
});
} catch (error: any) {
toast({
title: t('common:import_failed'),
description: error.message,
status: 'error'
});
console.error('Curl import error:', error);
}
};
return (
<MyModal
isOpen
onClose={onClose}
iconSrc="modal/edit"
title={t('common:core.module.http.curl import')}
w={600}
>
<ModalBody>
<Textarea
rows={20}
mt={2}
autoFocus
{...register('curlContent')}
placeholder={t('common:core.module.http.curl import placeholder')}
/>
</ModalBody>
<ModalFooter>
<Button variant={'whiteBase'} mr={3} onClick={onClose}>
{t('common:Close')}
</Button>
<Button onClick={handleSubmit(handleCurlImport)}>{t('common:Confirm')}</Button>
</ModalFooter>
</MyModal>
);
};
export default React.memo(CurlImportModal);

View File

@@ -27,7 +27,7 @@ const Edit = () => {
);
const baseUrl = toolSetData?.baseUrl ?? '';
const toolList = toolSetData?.toolList ?? [];
const apiSchemaStr = toolSetData?.apiSchemaStr ?? '';
const apiSchemaStr = toolSetData?.apiSchemaStr;
const headerSecret = toolSetData?.headerSecret ?? {};
const customHeaders = useMemo(() => {
try {

View File

@@ -14,6 +14,7 @@ import MyBox from '@fastgpt/web/components/common/MyBox';
import { putUpdateHttpPlugin } from '@/web/core/app/api/plugin';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import ConfigModal from './ConfigModal';
import ManualToolModal from './ManualToolModal';
import type { StoreSecretValueType } from '@fastgpt/global/common/secret/type';
import type { UpdateHttpPluginBody } from '@/pages/api/core/app/httpTools/update';
@@ -36,7 +37,13 @@ const EditForm = ({
}) => {
const { t } = useTranslation();
const appDetail = useContextSelector(AppContext, (v) => v.appDetail);
const reloadApp = useContextSelector(AppContext, (v) => v.reloadApp);
const [toolDetail, setToolDetail] = useState<HttpToolConfigType | null>(null);
const [editingManualTool, setEditingManualTool] = useState<HttpToolConfigType | null>(null);
const isBatchMode = apiSchemaStr !== undefined;
const {
onOpen: onOpenConfigModal,
@@ -44,6 +51,28 @@ const EditForm = ({
onClose: onCloseConfigModal
} = useDisclosure();
const {
onOpen: onOpenAddToolModal,
isOpen: isOpenAddToolModal,
onClose: onCloseAddToolModal
} = useDisclosure();
const { runAsync: runDeleteHttpTool, loading: isDeletingTool } = useRequest2(
async (updatedToolList: HttpToolConfigType[]) =>
await putUpdateHttpPlugin({
appId: appDetail._id,
toolList: updatedToolList
}),
{
manual: true,
onSuccess: () => {
reloadApp();
},
successToast: t('common:delete_success'),
errorToast: t('common:delete_failed')
}
);
return (
<>
<Box p={6}>
@@ -54,21 +83,31 @@ const EditForm = ({
total: toolList?.length || 0
})}
</FormLabel>
<Button
px={'2'}
leftIcon={
<MyIcon
name={toolList?.length && toolList.length > 0 ? 'change' : 'common/setting'}
w={'18px'}
/>
}
onClick={onOpenConfigModal}
>
{toolList?.length && toolList.length > 0 ? t('common:Config') : t('app:Start_config')}
</Button>
{isBatchMode ? (
<Button
px={'2'}
leftIcon={
<MyIcon
name={toolList?.length && toolList.length > 0 ? 'change' : 'common/setting'}
w={'18px'}
/>
}
onClick={onOpenConfigModal}
>
{toolList?.length && toolList.length > 0 ? t('common:Config') : t('app:Start_config')}
</Button>
) : (
<Button
px={'2'}
leftIcon={<MyIcon name={'common/addLight'} w={'18px'} />}
onClick={onOpenAddToolModal}
>
{t('common:Add')}
</Button>
)}
</Flex>
<Box mt={3}>
<MyBox mt={3} isLoading={isDeletingTool}>
{toolList?.map((tool, index) => {
return (
<MyBox
@@ -148,28 +187,67 @@ const EditForm = ({
bg="linear-gradient(90deg, rgba(255,255,255,0) 0%, rgba(255,255,255,1) 15%, rgba(255,255,255,1) 100%)"
paddingLeft="20px"
>
<MyIconButton
size={'16px'}
icon={'common/detail'}
p={2}
border={'1px solid'}
borderColor={'myGray.250'}
hoverBg={'rgba(51, 112, 255, 0.10)'}
hoverBorderColor={'primary.300'}
tip={t('app:HTTP_tools_detail')}
onClick={(e) => {
e.stopPropagation();
setToolDetail(tool);
}}
/>
{isBatchMode ? (
<MyIconButton
size={'16px'}
icon={'common/detail'}
p={2}
border={'1px solid'}
borderColor={'myGray.250'}
hoverBg={'rgba(51, 112, 255, 0.10)'}
hoverBorderColor={'primary.300'}
tip={t('app:HTTP_tools_detail')}
onClick={(e) => {
e.stopPropagation();
setToolDetail(tool);
}}
/>
) : (
<>
<MyIconButton
size={'16px'}
icon={'edit'}
p={2}
border={'1px solid'}
borderColor={'myGray.250'}
hoverBg={'rgba(51, 112, 255, 0.10)'}
hoverBorderColor={'primary.300'}
tip={t('common:Edit')}
onClick={(e) => {
e.stopPropagation();
setEditingManualTool(tool);
}}
/>
<MyIconButton
size={'16px'}
icon={'delete'}
p={2}
border={'1px solid'}
borderColor={'myGray.250'}
_hover={{
color: 'red.500',
bg: 'rgba(255, 0, 0, 0.10)',
borderColor: 'red.300'
}}
tip={t('common:Delete')}
onClick={(e) => {
e.stopPropagation();
const updatedToolList =
toolList?.filter((t) => t.name !== tool.name) || [];
runDeleteHttpTool(updatedToolList);
}}
/>
</>
)}
</Flex>
</MyBox>
);
})}
</Box>
</MyBox>
</Box>
{isOpenConfigModal && <ConfigModal onClose={onCloseConfigModal} />}
{isOpenAddToolModal && <ManualToolModal onClose={onCloseAddToolModal} />}
{toolDetail && (
<ToolDetailModal
tool={toolDetail}
@@ -181,6 +259,12 @@ const EditForm = ({
customHeaders={customHeaders || '{}'}
/>
)}
{editingManualTool && (
<ManualToolModal
onClose={() => setEditingManualTool(null)}
editingTool={editingManualTool}
/>
)}
</>
);
};

View File

@@ -0,0 +1,685 @@
import MyModal from '@fastgpt/web/components/common/MyModal';
import React, { useState } from 'react';
import { useTranslation } from 'next-i18next';
import {
Box,
Button,
Flex,
Input,
ModalBody,
ModalFooter,
Textarea,
useDisclosure,
Table,
Thead,
Tbody,
Tr,
Th,
Td,
TableContainer,
Switch
} from '@chakra-ui/react';
import { useForm } from 'react-hook-form';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useContextSelector } from 'use-context-selector';
import { AppContext } from '../context';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
import MySelect from '@fastgpt/web/components/common/MySelect';
import { useToast } from '@fastgpt/web/hooks/useToast';
import {
HTTP_METHODS,
type HttpMethod,
toolValueTypeList,
ContentTypes
} from '@fastgpt/global/core/workflow/constants';
import {
headerValue2StoreHeader,
storeHeader2HeaderValue
} from '@/components/common/secret/HeaderAuthConfig';
import HeaderAuthForm from '@/components/common/secret/HeaderAuthForm';
import type { StoreSecretValueType } from '@fastgpt/global/common/secret/type';
import MyIcon from '@fastgpt/web/components/common/Icon';
import HttpInput from '@fastgpt/web/components/common/Input/HttpInput';
import { putUpdateHttpPlugin } from '@/web/core/app/api/plugin';
import type { HttpToolConfigType } from '@fastgpt/global/core/app/type';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import CurlImportModal from './CurlImportModal';
type ManualToolFormType = {
name: string;
description: string;
method: HttpMethod;
path: string;
headerSecret: StoreSecretValueType;
customParams: CustomParamItemType[];
params: ParamItemType[];
bodyType: ContentTypes;
bodyContent: string;
bodyFormData: ParamItemType[];
headers: ParamItemType[];
};
type CustomParamItemType = {
key: string;
description: string;
type: string;
required: boolean;
isTool: boolean;
};
export type ParamItemType = {
key: string;
value: string;
};
const ManualToolModal = ({
onClose,
editingTool
}: {
onClose: () => void;
editingTool?: HttpToolConfigType;
}) => {
const { t } = useTranslation();
const { toast } = useToast();
const appDetail = useContextSelector(AppContext, (v) => v.appDetail);
const reloadApp = useContextSelector(AppContext, (v) => v.reloadApp);
const isEditMode = editingTool !== undefined;
const { register, handleSubmit, watch, setValue } = useForm<ManualToolFormType>({
defaultValues: {
name: editingTool?.name || '',
description: editingTool?.description || '',
method: (editingTool?.method.toUpperCase() as any) || 'POST',
path: editingTool?.path || '',
headerSecret: editingTool?.headerSecret || {},
customParams: editingTool
? Object.entries(editingTool.inputSchema.properties || {}).map(
([key, value]: [string, any]) => ({
key,
description: value.description || '',
type: value.type || 'string',
required: editingTool.inputSchema.required?.includes(key) || false,
isTool: !!value['x-tool-description']
})
)
: [],
params: editingTool?.staticParams || [],
bodyType: editingTool?.staticBody?.type || ContentTypes.json,
bodyContent: editingTool?.staticBody?.content || '',
bodyFormData: editingTool?.staticBody?.formData || [],
headers: editingTool?.staticHeaders || []
}
});
const method = watch('method');
const headerSecret = watch('headerSecret');
const customParams = watch('customParams');
const params = watch('params');
const bodyType = watch('bodyType');
const bodyContent = watch('bodyContent');
const bodyFormData = watch('bodyFormData');
const headers = watch('headers');
const hasBody = method !== 'GET' && method !== 'DELETE';
const isFormBody =
bodyType === ContentTypes.formData || bodyType === ContentTypes.xWwwFormUrlencoded;
const isContentBody =
bodyType === ContentTypes.json ||
bodyType === ContentTypes.xml ||
bodyType === ContentTypes.raw;
const [editingParam, setEditingParam] = useState<CustomParamItemType | null>(null);
const {
onOpen: onOpenCurlImport,
isOpen: isOpenCurlImport,
onClose: onCloseCurlImport
} = useDisclosure();
const { runAsync: onSubmit, loading: isSubmitting } = useRequest2(
async (data: ManualToolFormType) => {
if (bodyType === ContentTypes.json && bodyContent) {
try {
JSON.parse(bodyContent);
} catch (error) {
return Promise.reject(t('common:json_parse_error'));
}
}
const inputProperties: Record<string, any> = {};
const inputRequired: string[] = [];
customParams.forEach((param) => {
inputProperties[param.key] = {
type: param.type,
description: param.description || '',
'x-tool-description': param.isTool ? param.description : ''
};
if (param.required) {
inputRequired.push(param.key);
}
});
const newTool: HttpToolConfigType = {
name: data.name,
description: data.description,
path: data.path,
method: data.method.toLowerCase(),
inputSchema: {
type: 'object',
properties: inputProperties,
required: inputRequired
},
outputSchema: {
type: 'object',
properties: {},
required: []
},
...(params.length > 0 && { staticParams: params }),
...(headers.length > 0 && { staticHeaders: headers }),
...(hasBody &&
bodyType !== ContentTypes.none && {
staticBody: {
type: bodyType,
...(isContentBody ? { content: bodyContent } : {}),
...(isFormBody ? { formData: bodyFormData } : {})
}
}),
headerSecret: data.headerSecret
};
const toolSetNode = appDetail.modules.find(
(item) => item.flowNodeType === FlowNodeTypeEnum.toolSet
);
const existingToolList = toolSetNode?.toolConfig?.httpToolSet?.toolList || [];
const updatedToolList = (() => {
if (isEditMode) {
return existingToolList.map((tool) => (tool.name === editingTool?.name ? newTool : tool));
}
return [...existingToolList, newTool];
})();
return putUpdateHttpPlugin({
appId: appDetail._id,
toolList: updatedToolList
});
},
{
onSuccess: () => {
reloadApp();
onClose();
}
}
);
return (
<MyModal
isOpen
onClose={onClose}
iconSrc={isEditMode ? 'modal/edit' : 'common/addLight'}
iconColor={'primary.600'}
title={isEditMode ? t('app:Edit_tool') : t('app:Add_tool')}
maxW={'1167px'}
>
<ModalBody display={'flex'}>
<Flex w={'1167px'}>
<Flex
w={'500px'}
px={9}
py={3}
flexDirection={'column'}
gap={6}
borderRight={'1px solid'}
borderColor={'myGray.200'}
>
<Flex gap={8} alignItems={'center'}>
<FormLabel>{t('app:Tool_name')}</FormLabel>
<Input
h={8}
{...register('name', { required: true })}
placeholder={t('app:Tool_name')}
/>
</Flex>
<Box>
<FormLabel mb={2}>{t('app:Tool_description')}</FormLabel>
<Textarea
{...register('description')}
rows={8}
minH={'150px'}
maxH={'400px'}
placeholder={t('app:Tool_description')}
/>
</Box>
<Box>
<Flex mb={2} alignItems={'center'} justifyContent={'space-between'}>
<FormLabel>{t('common:core.module.Http request settings')}</FormLabel>
<Button size={'sm'} onClick={onOpenCurlImport}>
{t('common:core.module.http.curl import')}
</Button>
</Flex>
<Flex gap={2}>
<MySelect
h={9}
w={'100px'}
value={method}
list={HTTP_METHODS.map((method) => ({ label: method, value: method }))}
onChange={(e) => setValue('method', e)}
/>
<Input
{...register('path', { required: true })}
placeholder={t('common:core.module.input.label.Http Request Url')}
/>
</Flex>
</Box>
<Box alignItems={'center'}>
<FormLabel mb={0}>{t('common:auth_config')}</FormLabel>
<Box>
<HeaderAuthForm
headerSecretValue={storeHeader2HeaderValue(headerSecret)}
onChange={(data) => {
const storeData = headerValue2StoreHeader(data);
setValue('headerSecret', storeData);
}}
fontWeight="normal"
/>
</Box>
</Box>
</Flex>
<Flex flex={1} px={9} py={3} flexDirection={'column'} gap={6}>
<Box>
<Flex alignItems={'center'} mb={2}>
<FormLabel flex={1}>{t('app:Custom_params')}</FormLabel>
<Button
size={'sm'}
variant={'whitePrimary'}
leftIcon={<MyIcon name={'common/addLight'} w={'14px'} />}
onClick={() => {
setEditingParam({
key: '',
description: '',
type: 'string',
required: false,
isTool: true
});
}}
>
{t('common:add_new')}
</Button>
</Flex>
<CustomParamsTable
list={customParams}
onEdit={(param) => {
setEditingParam(param);
}}
onDelete={(index) => {
setValue(
'customParams',
customParams.filter((_, i) => i !== index)
);
}}
/>
</Box>
<Box>
<FormLabel mb={2}>Params</FormLabel>
<ParamsTable list={params} setList={(newParams) => setValue('params', newParams)} />
</Box>
{hasBody && (
<Box>
<FormLabel mb={2}>Body</FormLabel>
<Flex
mb={2}
p={1}
flexWrap={'nowrap'}
bg={'myGray.25'}
border={'1px solid'}
borderColor={'myGray.200'}
borderRadius={'8px'}
justifyContent={'space-between'}
>
{Object.values(ContentTypes).map((type) => (
<Box
key={type}
cursor={'pointer'}
px={3}
py={1.5}
fontSize={'12px'}
fontWeight={'medium'}
color={'myGray.500'}
borderRadius={'6px'}
bg={bodyType === type ? 'white' : 'none'}
boxShadow={
bodyType === type
? '0 1px 2px 0 rgba(19, 51, 107, 0.10), 0 0 1px 0 rgba(19, 51, 107, 0.15)'
: ''
}
onClick={() => setValue('bodyType', type)}
>
{type}
</Box>
))}
</Flex>
{isContentBody && (
<Textarea
value={bodyContent}
onChange={(e) => setValue('bodyContent', e.target.value)}
onBlur={(e) => {
if (bodyType === ContentTypes.json && e.target.value) {
try {
JSON.parse(e.target.value);
} catch (error) {
toast({
status: 'warning',
title: t('common:json_parse_error')
});
}
}
}}
minH={'100px'}
maxH={'200px'}
/>
)}
{isFormBody && (
<ParamsTable
list={bodyFormData}
setList={(newFormData) => setValue('bodyFormData', newFormData)}
/>
)}
</Box>
)}
<Box>
<FormLabel mb={2}>Headers</FormLabel>
<ParamsTable
list={headers}
setList={(newHeaders) => setValue('headers', newHeaders)}
/>
</Box>
</Flex>
</Flex>
</ModalBody>
<ModalFooter>
<Button variant={'whiteBase'} mr={3} onClick={onClose}>
{t('common:Close')}
</Button>
<Button onClick={handleSubmit((data) => onSubmit(data))} isLoading={isSubmitting}>
{t('common:Confirm')}
</Button>
</ModalFooter>
{isOpenCurlImport && (
<CurlImportModal
onClose={onCloseCurlImport}
onImport={(result) => {
setValue('method', result.method);
setValue('path', result.path);
if (result.params) {
setValue('params', result.params);
}
if (result.headers) {
setValue('headers', result.headers);
}
setValue('bodyType', result.bodyType as ContentTypes);
if (result.bodyContent) {
setValue('bodyContent', result.bodyContent);
}
if (result.bodyFormData) {
setValue('bodyFormData', result.bodyFormData);
}
onCloseCurlImport();
}}
/>
)}
{editingParam && (
<CustomParamEditModal
param={editingParam}
onClose={() => setEditingParam(null)}
onConfirm={(newParam) => {
if (editingParam.key) {
setValue(
'customParams',
customParams.map((param) => (param.key === editingParam.key ? newParam : param))
);
} else {
setValue('customParams', [...customParams, newParam]);
}
}}
/>
)}
</MyModal>
);
};
const CustomParamEditModal = ({
param,
onClose,
onConfirm
}: {
param: CustomParamItemType;
onClose: () => void;
onConfirm: (param: CustomParamItemType) => void;
}) => {
const { t } = useTranslation();
const isEdit = !!param.key;
const { register, handleSubmit, watch, setValue } = useForm<CustomParamItemType>({
defaultValues: param
});
const type = watch('type');
const required = watch('required');
const isTool = watch('isTool');
return (
<MyModal
isOpen
onClose={onClose}
iconSrc={isEdit ? 'modal/edit' : 'common/addLight'}
iconColor={'primary.600'}
title={isEdit ? t('app:edit_param') : t('common:add_new_param')}
w={500}
>
<ModalBody px={9}>
<Flex mb={6} alignItems={'center'}>
<FormLabel w={'120px'}>{t('common:core.module.http.Props name')}</FormLabel>
<Input
{...register('key', { required: true })}
placeholder={t('common:core.module.http.Props name')}
bg={'myGray.50'}
/>
</Flex>
<Flex mb={6}>
<FormLabel w={'120px'}>{t('common:plugin.Description')}</FormLabel>
<Textarea
{...register('description', { required: isTool })}
rows={4}
placeholder={t('app:tool_params_description_tips')}
bg={'myGray.50'}
/>
</Flex>
<Flex mb={6} alignItems={'center'}>
<FormLabel w={'120px'}>{t('common:core.module.Data Type')}</FormLabel>
<MySelect
value={type}
list={toolValueTypeList}
onChange={(val) => setValue('type', val)}
flex={1}
/>
</Flex>
<Flex mb={6} alignItems={'center'}>
<FormLabel w={'120px'}>{t('common:Required_input')}</FormLabel>
<Switch isChecked={required} onChange={(e) => setValue('required', e.target.checked)} />
</Flex>
<Flex mb={6} alignItems={'center'}>
<FormLabel w={'120px'}>{t('workflow:field_used_as_tool_input')}</FormLabel>
<Switch isChecked={isTool} onChange={(e) => setValue('isTool', e.target.checked)} />
</Flex>
</ModalBody>
<ModalFooter>
<Button variant={'whiteBase'} mr={3} onClick={onClose}>
{t('common:Close')}
</Button>
<Button
onClick={handleSubmit((data) => {
onConfirm(data);
onClose();
})}
>
{t('common:Confirm')}
</Button>
</ModalFooter>
</MyModal>
);
};
const CustomParamsTable = ({
list,
onEdit,
onDelete
}: {
list: CustomParamItemType[];
onEdit: (param: CustomParamItemType) => void;
onDelete: (index: number) => void;
}) => {
const { t } = useTranslation();
return (
<Box
borderRadius={'md'}
overflow={'hidden'}
borderWidth={'1px'}
borderBottom={'none'}
bg={'white'}
>
<TableContainer overflowY={'visible'} overflowX={'unset'}>
<Table size={'sm'}>
<Thead>
<Tr bg={'myGray.50'} h={8}>
<Th px={2}>{t('common:core.module.http.Props name')}</Th>
<Th px={2}>{t('common:plugin.Description')}</Th>
<Th px={2}>{t('common:support.standard.type')}</Th>
<Th px={2}>{t('app:type.Tool')}</Th>
<Th px={2}>{t('common:Operation')}</Th>
</Tr>
</Thead>
<Tbody>
{list.map((item, index) => (
<Tr key={index} h={8}>
<Td px={2}>{item.key}</Td>
<Td px={2}>{item.description}</Td>
<Td px={2}>{item.type}</Td>
<Td px={2}>{item.isTool ? t('common:yes') : t('common:no')}</Td>
<Td px={2}>
<Flex gap={2}>
<MyIcon
name={'edit'}
cursor={'pointer'}
_hover={{ color: 'primary.600' }}
w={'14px'}
onClick={() => onEdit(item)}
/>
<MyIcon
name={'delete'}
cursor={'pointer'}
_hover={{ color: 'red.600' }}
w={'14px'}
onClick={() => onDelete(index)}
/>
</Flex>
</Td>
</Tr>
))}
</Tbody>
</Table>
</TableContainer>
</Box>
);
};
const ParamsTable = ({
list,
setList
}: {
list: ParamItemType[];
setList: (list: ParamItemType[]) => void;
}) => {
const { t } = useTranslation();
const { toast } = useToast();
const [updateTrigger, setUpdateTrigger] = useState(false);
return (
<Box borderRadius={'md'} overflow={'hidden'} borderWidth={'1px'} borderBottom={'none'}>
<TableContainer overflowY={'visible'} overflowX={'unset'}>
<Table size={'sm'}>
<Thead>
<Tr bg={'myGray.50'} h={8}>
<Th px={2}>{t('common:core.module.http.Props name')}</Th>
<Th px={2}>{t('common:core.module.http.Props value')}</Th>
</Tr>
</Thead>
<Tbody>
{[...list, { key: '', value: '' }].map((item, index) => (
<Tr key={index}>
<Td w={1 / 2} p={0} borderRight={'1px solid'} borderColor={'myGray.150'}>
<HttpInput
placeholder={'key'}
value={item.key}
onBlur={(val) => {
if (!val) return;
if (list.find((item, i) => i !== index && item.key === val)) {
setUpdateTrigger((prev) => !prev);
toast({
status: 'warning',
title: t('common:core.module.http.Key already exists')
});
return;
}
if (index === list.length) {
setList([...list, { key: val, value: '' }]);
setUpdateTrigger((prev) => !prev);
} else {
setList(list.map((p, i) => (i === index ? { ...p, key: val } : p)));
}
}}
updateTrigger={updateTrigger}
/>
</Td>
<Td w={1 / 2} p={0} borderColor={'myGray.150'}>
<Box display={'flex'} alignItems={'center'}>
<HttpInput
placeholder={'value'}
value={item.value}
onBlur={(val) =>
setList(list.map((p, i) => (i === index ? { ...p, value: val } : p)))
}
/>
{index !== list.length && (
<MyIcon
name={'delete'}
cursor={'pointer'}
_hover={{ color: 'red.600' }}
w={'14px'}
mx={'2'}
display={'block'}
onClick={() => setList(list.filter((_, i) => i !== index))}
/>
)}
</Box>
</Td>
</Tr>
))}
</Tbody>
</Table>
</TableContainer>
</Box>
);
};
export default React.memo(ManualToolModal);

View File

@@ -25,6 +25,7 @@ import {
} from '@chakra-ui/react';
import {
ContentTypes,
HTTP_METHODS,
NodeInputKeyEnum,
WorkflowIOValueTypeEnum
} from '@fastgpt/global/core/workflow/constants';
@@ -198,28 +199,7 @@ const RenderHttpMethodAndUrl = React.memo(function RenderHttpMethodAndUrl({
bg={'white'}
width={'100%'}
value={requestMethods?.value}
list={[
{
label: 'GET',
value: 'GET'
},
{
label: 'POST',
value: 'POST'
},
{
label: 'PUT',
value: 'PUT'
},
{
label: 'DELETE',
value: 'DELETE'
},
{
label: 'PATCH',
value: 'PATCH'
}
]}
list={HTTP_METHODS.map((method) => ({ label: method, value: method }))}
onChange={(e) => {
onChangeNode({
nodeId,

View File

@@ -1,4 +1,4 @@
import React from 'react';
import React, { useState } from 'react';
import { Box, Flex, Button, ModalBody, Input, Textarea, ModalFooter } from '@chakra-ui/react';
import { useSelectFile } from '@/web/common/file/hooks/useSelectFile';
import { useForm } from 'react-hook-form';
@@ -13,6 +13,8 @@ import { useContextSelector } from 'use-context-selector';
import { AppListContext } from './context';
import { useRouter } from 'next/router';
import type { StoreSecretValueType } from '@fastgpt/global/common/secret/type';
import LeftRadio from '@fastgpt/web/components/common/Radio/LeftRadio';
import MyIcon from '@fastgpt/web/components/common/Icon';
export type HttpToolsType = {
id?: string;
@@ -35,6 +37,8 @@ const HttpPluginCreateModal = ({ onClose }: { onClose: () => void }) => {
const { t } = useTranslation();
const router = useRouter();
const [createType, setCreateType] = useState<'batch' | 'manual'>('batch');
const { parentId, loadMyApps } = useContextSelector(AppListContext, (v) => v);
const { register, setValue, handleSubmit, watch } = useForm<HttpToolsType>({
@@ -46,6 +50,7 @@ const HttpPluginCreateModal = ({ onClose }: { onClose: () => void }) => {
const { runAsync: onCreate, loading: isCreating } = useRequest2(
async (data: HttpToolsType) => {
return postCreateHttpTools({
createType,
parentId,
name: data.name,
intro: data.intro,
@@ -83,94 +88,91 @@ const HttpPluginCreateModal = ({ onClose }: { onClose: () => void }) => {
position={'relative'}
>
<ModalBody flex={'0 1 auto'} overflow={'auto'} pb={0} px={9}>
<>
<Box color={'myGray.800'} fontWeight={'bold'}>
{t('common:input_name')}
<Box color={'myGray.900'} fontWeight={'medium'} fontSize={'14px'}>
{t('common:input_name')}
</Box>
<Flex mt={3} alignItems={'center'}>
<MyTooltip label={t('common:set_avatar')}>
<Avatar
flexShrink={0}
src={avatar}
w={['28px', '32px']}
h={['28px', '32px']}
cursor={'pointer'}
borderRadius={'md'}
onClick={onOpenSelectFile}
/>
</MyTooltip>
<Input
flex={1}
ml={4}
bg={'myWhite.600'}
{...register('name', {
required: t('common:name_is_empty')
})}
/>
</Flex>
<Box color={'myGray.900'} fontWeight={'medium'} mt={6} fontSize={'14px'}>
{t('common:core.app.App intro')}
</Box>
<Textarea
{...register('intro')}
bg={'myWhite.600'}
h={'122px'}
rows={3}
mt={3}
placeholder={t('common:core.app.Make a brief introduction of your app')}
/>
<Box display={'flex'} alignItems={'center'} py={1} gap={'281px'} mt={6}>
<Box color={'myGray.900'} fontWeight={'medium'} fontSize={'14px'}>
{t('app:HTTPTools_Create_Type')}
</Box>
<Flex mt={3} alignItems={'center'}>
<MyTooltip label={t('common:set_avatar')}>
<Avatar
flexShrink={0}
src={avatar}
w={['28px', '32px']}
h={['28px', '32px']}
cursor={'pointer'}
borderRadius={'md'}
onClick={onOpenSelectFile}
/>
</MyTooltip>
<Input
flex={1}
ml={4}
bg={'myWhite.600'}
{...register('name', {
required: t('common:name_is_empty')
})}
/>
</Flex>
<>
<Box color={'myGray.800'} fontWeight={'bold'} mt={6}>
{t('common:core.app.App intro')}
</Box>
<Textarea
{...register('intro')}
bg={'myWhite.600'}
h={'122px'}
rows={3}
mt={3}
placeholder={t('common:core.app.Make a brief introduction of your app')}
/>
</>
</>
{/* <>
<Box display={'flex'} alignItems={'center'} py={1} gap={'281px'} mt={6}>
<Box color={'myGray.800'} fontWeight={'bold'}>
{t('common:plugin.Create Type')}
</Box>
<Box
display={'flex'}
justifyContent={'center'}
alignItems={'center'}
ml={'auto'}
gap={'4px'}
>
<MyIcon name={'common/info'} w={'16px'} h={'16px'} />
<Box
display={'flex'}
justifyContent={'center'}
alignItems={'center'}
ml={'auto'}
gap={'4px'}
fontSize={'12px'}
fontStyle={'normal'}
fontWeight={'500'}
lineHeight={'16px'}
letterSpacing={'0.5px'}
>
<MyIcon name={'common/info'} w={'16px'} h={'16px'} />
<Box
fontSize={'12px'}
fontStyle={'normal'}
fontWeight={'500'}
lineHeight={'16px'}
letterSpacing={'0.5px'}
>
{t('common:plugin.Create Type Tip')}
</Box>
{t('app:HTTPTools_Create_Type_Tip')}
</Box>
</Box>
<Box mt={2}>
<LeftRadio
list={[
{
title: t('app:type.Http batch'),
value: 'batch',
desc: t('app:type.Http batch tip')
},
{
title: t('app:type.Http manual'),
value: 'manual',
desc: t('app:type.Http manual tip')
}
]}
value={createType}
fontSize={'xs'}
onChange={(e) => setCreateType(e as 'batch' | 'manual')}
defaultBg={'white'}
activeBg={'white'}
/>
</Box>
</> */}
</Box>
<Box my={2}>
<LeftRadio
list={[
{
title: t('app:type.Http batch'),
value: 'batch',
desc: t('app:type.Http batch tip')
},
{
title: t('app:type.Http manual'),
value: 'manual',
desc: t('app:type.Http manual tip')
}
]}
value={createType}
fontSize={'xs'}
onChange={(e) => setCreateType(e as 'batch' | 'manual')}
defaultBg={'white'}
activeBg={'white'}
py={2}
px={3}
gridGap={4}
/>
</Box>
</ModalBody>
<ModalFooter my={6} py={0} px={9}>
<ModalFooter mt={4} mb={6} py={0} px={9}>
<Button variant={'whiteBase'} mr={3} onClick={onClose}>
{t('common:Close')}
</Button>

View File

@@ -4,7 +4,6 @@ import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun';
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
import { NextAPI } from '@/service/middleware/entry';
import { onCreateApp, type CreateAppBody } from '../create';
import { type AppSchema } from '@fastgpt/global/core/app/type';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import { pushTrack } from '@fastgpt/service/common/middle/tracks/utils';
import { authApp } from '@fastgpt/service/support/permission/app/auth';
@@ -14,13 +13,15 @@ import { getHTTPToolSetRuntimeNode } from '@fastgpt/global/core/app/httpTools/ut
export type createHttpToolsQuery = {};
export type createHttpToolsBody = Omit<CreateAppBody, 'type' | 'modules' | 'edges' | 'chatConfig'>;
export type createHttpToolsBody = {
createType: 'batch' | 'manual';
} & Omit<CreateAppBody, 'type' | 'modules' | 'edges' | 'chatConfig'>;
async function handler(
req: ApiRequestProps<createHttpToolsBody, createHttpToolsQuery>,
res: ApiResponseType<string>
): Promise<string> {
const { name, avatar, intro, parentId } = req.body;
const { name, avatar, intro, parentId, createType } = req.body;
const { teamId, tmbId, userId } = parentId
? await authApp({ req, appId: parentId, per: TeamAppCreatePermissionVal, authToken: true })
@@ -40,7 +41,14 @@ async function handler(
modules: [
getHTTPToolSetRuntimeNode({
name,
avatar
avatar,
toolList: [],
...(createType === 'batch' && {
baseUrl: '',
apiSchemaStr: '',
customHeaders: '{}',
headerSecret: {}
})
})
],
session

View File

@@ -3,6 +3,7 @@ import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/nex
import { type StoreSecretValueType } from '@fastgpt/global/common/secret/type';
import type { RunHTTPToolResult } from '@fastgpt/service/core/app/http';
import { runHTTPTool } from '@fastgpt/service/core/app/http';
import type { HttpToolConfigType } from '@fastgpt/global/core/app/type';
export type RunHTTPToolQuery = {};
@@ -13,6 +14,9 @@ export type RunHTTPToolBody = {
method: string;
customHeaders?: Record<string, string>;
headerSecret?: StoreSecretValueType;
staticParams?: HttpToolConfigType['staticParams'];
staticHeaders?: HttpToolConfigType['staticHeaders'];
staticBody?: HttpToolConfigType['staticBody'];
};
export type RunHTTPToolResponse = RunHTTPToolResult;
@@ -21,7 +25,17 @@ async function handler(
req: ApiRequestProps<RunHTTPToolBody, RunHTTPToolQuery>,
res: ApiResponseType<RunHTTPToolResponse>
): Promise<RunHTTPToolResponse> {
const { params, baseUrl, toolPath, method = 'POST', customHeaders, headerSecret } = req.body;
const {
params,
baseUrl,
toolPath,
method = 'POST',
customHeaders,
headerSecret,
staticParams,
staticHeaders,
staticBody
} = req.body;
return runHTTPTool({
baseUrl,
@@ -29,7 +43,10 @@ async function handler(
method,
params,
headerSecret,
customHeaders
customHeaders,
staticParams,
staticHeaders,
staticBody
});
}

View File

@@ -13,10 +13,10 @@ import { MongoAppVersion } from '@fastgpt/service/core/app/version/schema';
export type UpdateHttpPluginBody = {
appId: string;
baseUrl: string;
apiSchemaStr: string;
toolList: HttpToolConfigType[];
headerSecret: StoreSecretValueType;
baseUrl?: string;
apiSchemaStr?: string;
headerSecret?: StoreSecretValueType;
customHeaders?: string;
};
@@ -27,12 +27,17 @@ async function handler(req: ApiRequestProps<UpdateHttpPluginBody>, res: NextApiR
const formatedHeaderAuth = storeSecretValue(headerSecret);
const formattedToolList = toolList.map((tool) => ({
...tool,
headerSecret: tool.headerSecret ? storeSecretValue(tool.headerSecret) : undefined
}));
const toolSetRuntimeNode = getHTTPToolSetRuntimeNode({
name: app.name,
avatar: app.avatar,
baseUrl,
apiSchemaStr,
toolList,
toolList: formattedToolList,
headerSecret: formatedHeaderAuth,
customHeaders
});