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": "工具",