diff --git a/packages/global/core/app/httpTools/utils.ts b/packages/global/core/app/httpTools/utils.ts index 329e5ce7b925..eac7f4cc1b87 100644 --- a/packages/global/core/app/httpTools/utils.ts +++ b/packages/global/core/app/httpTools/utils.ts @@ -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: [], diff --git a/packages/global/core/app/type.d.ts b/packages/global/core/app/type.d.ts index 017dff7d78e6..40ff44c8b433 100644 --- a/packages/global/core/app/type.d.ts +++ b/packages/global/core/app/type.d.ts @@ -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 */ diff --git a/packages/global/core/workflow/constants.ts b/packages/global/core/workflow/constants.ts index a8eccad719a9..166d6dd74457 100644 --- a/packages/global/core/workflow/constants.ts +++ b/packages/global/core/workflow/constants.ts @@ -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.string]: WorkflowIOValueTypeEnum.arrayString, [WorkflowIOValueTypeEnum.number]: WorkflowIOValueTypeEnum.arrayNumber, diff --git a/packages/global/core/workflow/type/node.d.ts b/packages/global/core/workflow/type/node.d.ts index 33656022dc97..efc24aa418fe 100644 --- a/packages/global/core/workflow/type/node.d.ts +++ b/packages/global/core/workflow/type/node.d.ts @@ -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?: { diff --git a/packages/service/core/app/http.ts b/packages/service/core/app/http.ts index 698695441fa6..9fc002b3fbce 100644 --- a/packages/service/core/app/http.ts +++ b/packages/service/core/app/http.ts @@ -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; headerSecret?: StoreSecretValueType; customHeaders?: Record; + 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) => { + 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 + ) || {}) + }; + + const queryParams = (() => { + const staticParamsObj = + staticParams?.reduce( + (acc, { key, value }) => { + acc[key] = value; + return acc; + }, + {} as Record + ) || {}; + + 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 { + customHeaders, + staticParams, + staticHeaders, + staticBody +}: RunHTTPToolParams): Promise => { 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) }; } -} +}; diff --git a/packages/service/core/workflow/dispatch/child/runTool.ts b/packages/service/core/workflow/dispatch/child/runTool.ts index 6b03fe0d42a2..3f6738f23d68 100644 --- a/packages/service/core/workflow/dispatch/child/runTool.ts +++ b/packages/service/core/workflow/dispatch/child/runTool.ts @@ -236,16 +236,19 @@ export const dispatchRunTool = async (props: RunToolProps): Promise => { let { runningAppInfo: { id: appId, teamId, tmbId }, diff --git a/packages/web/components/common/Radio/LeftRadio.tsx b/packages/web/components/common/Radio/LeftRadio.tsx index 6006d959e2c4..3ef981de9231 100644 --- a/packages/web/components/common/Radio/LeftRadio.tsx +++ b/packages/web/components/common/Radio/LeftRadio.tsx @@ -25,6 +25,7 @@ const LeftRadio = ({ align = 'center', px = 3.5, py = 4, + gridGap = [3, 5], defaultBg = 'myGray.50', activeBg = 'primary.50', onChange, @@ -75,7 +76,7 @@ const LeftRadio = ({ ); return ( - + {list.map((item) => { const isActive = value === item.value; return ( @@ -131,7 +132,7 @@ const LeftRadio = ({ lineHeight={1} color={'myGray.900'} > - {t(item.title as any)} + {t(item.title as any)} {!!item.tooltip && } ) : ( diff --git a/packages/web/i18n/en/app.json b/packages/web/i18n/en/app.json index 39bb925a8adb..c88478549fea 100644 --- a/packages/web/i18n/en/app.json +++ b/packages/web/i18n/en/app.json @@ -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", diff --git a/packages/web/i18n/zh-CN/app.json b/packages/web/i18n/zh-CN/app.json index d3e79c34e9ff..20b6c340c21b 100644 --- a/packages/web/i18n/zh-CN/app.json +++ b/packages/web/i18n/zh-CN/app.json @@ -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": "工具", diff --git a/packages/web/i18n/zh-Hant/app.json b/packages/web/i18n/zh-Hant/app.json index 5267f65c87d5..4be50758cb3c 100644 --- a/packages/web/i18n/zh-Hant/app.json +++ b/packages/web/i18n/zh-Hant/app.json @@ -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": "工具", diff --git a/projects/app/src/pageComponents/app/detail/HTTPTools/ChatTest.tsx b/projects/app/src/pageComponents/app/detail/HTTPTools/ChatTest.tsx index 30d866a7c372..4ea89301e72f 100644 --- a/projects/app/src/pageComponents/app/detail/HTTPTools/ChatTest.tsx +++ b/projects/app/src/pageComponents/app/detail/HTTPTools/ChatTest.tsx @@ -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 }); }, { diff --git a/projects/app/src/pageComponents/app/detail/HTTPTools/CurlImportModal.tsx b/projects/app/src/pageComponents/app/detail/HTTPTools/CurlImportModal.tsx new file mode 100644 index 000000000000..fa0e20f30e6a --- /dev/null +++ b/projects/app/src/pageComponents/app/detail/HTTPTools/CurlImportModal.tsx @@ -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 ( + + +