Files
FastGPT/packages/global/core/app/httpPlugin/utils.ts
Archer 33d40fd077 feature: System plugin (#5131)
* feat: system Tool (#4959)

* feat: independent system tool

* chore: use ToolNode instead of PluginModule

* chore: tools

* chore: tools templateDir

* refactor: templates

* feat: flush code

* chore: update template

* refactor: migrate delay

* feat: worker pool

* chore: Dockerfile

* docs:  add tools.template.json

* feat: auto flush system tools

* fix: ts error

* chore: create new pool temporarily

* chore: system tool migration

* chore: migration

* fix: fix pnpm-workspace.yaml

* chore: update pnpm-lock.yaml to integrate tool

* chore(systemTool): chore

* chore: add system plugin

* chore(deps): update @fastgpt-sdk/plugin

* fix: type error

* chore: remove plugin package

* chore: move pro plugins code to open source

* feat: support system tool config input

* fix: type error

* perf: i18n

* fix: cr

* chore: update sdk

* feat: system plugin cache

* update mcp server (#5076)

* update mcp server

* fix: action

* fix: dockerfile

* fix: dockerfile

* fix: dockerfile

* fix: dockerfile

* fix: dockerfile

* fix: dockerfile

* feat: system Tool (#4959)

* feat: independent system tool

* chore: use ToolNode instead of PluginModule

* chore: tools

* chore: tools templateDir

* refactor: templates

* feat: flush code

* chore: update template

* refactor: migrate delay

* feat: worker pool

* chore: Dockerfile

* docs:  add tools.template.json

* feat: auto flush system tools

* fix: ts error

* chore: create new pool temporarily

* chore: system tool migration

* chore: migration

* fix: fix pnpm-workspace.yaml

* chore: update pnpm-lock.yaml to integrate tool

* chore(systemTool): chore

* chore: add system plugin

* chore(deps): update @fastgpt-sdk/plugin

* fix: type error

* chore: remove plugin package

* chore: move pro plugins code to open source

* feat: support system tool config input

* fix: type error

* perf: i18n

* fix: cr

* chore: update sdk

* feat: system plugin cache

* perf: run tool

* update package

* perf: config key

* fix: tool ini

* tool config params

* perf: workflow type

* rename tools to  agent

* version list

* perf: tool error

* config secret ux

* perf: config secret ux

* fix: tool config field

* add course to secret input

* feat: support inputConfig switch (#5099)

* feat: support inputConfig switch

* deps: update @fastgpt-sdk/plugin

* chore: update workflows

* fix: inputType

* fix: secret

* add default value to node

* update i18n

* eslint

* add precision to number input

* feat: add number input and select

* perf: number ux

* fix: code

* Proxies image requests to plugin service (#5111)

* Proxies image requests to plugin service

Adds a rewrite rule and API endpoint to proxy image requests
to the plugin service. This allows the app to fetch images from
the plugin's tools directory.

It also adds the plugin base URL to the service's constants, so that
it can use the plugin URL when proxying requests.

* fix: update FastGPTPluginUrl to remove unnecessary API path

* feat: update image proxy destination and add plugin image handler

* Adapt plugin id

* replace avatar

* remove rewrite

* fix: plugin avatar

* update system tool doc

* feat: system tool type

* yml sh

* yml sh

* update doc

* fix: simple app tool select

* fix: switch ui

* update pacakge

* Yamljs (#5129)

* update docker-compose configuration: bump fastgpt and fastgpt-plugin images, change minio host to service name, and adjust service dependencies

* refactor: comment out port exposure in docker-compose configuration

* update: uncomment port exposure in docker-compose configuration

* update: change MINIO_HOST to use specific IP address in docker configuration

* update: modify fastgpt-plugin image version in docker configuration

* update readme

* doc

* remove

---------

Co-authored-by: Finley Ge <32237950+FinleyGe@users.noreply.github.com>
Co-authored-by: Theresa <63280168+sd0ric4@users.noreply.github.com>
2025-07-02 18:15:00 +08:00

391 lines
12 KiB
TypeScript

import { getNanoid } from '../../../common/string/tools';
import { type OpenApiJsonSchema } from './type';
import yaml from 'js-yaml';
import type { OpenAPIV3 } from 'openapi-types';
import { type FlowNodeInputItemType, type FlowNodeOutputItemType } from '../../workflow/type/io';
import { FlowNodeInputTypeEnum, FlowNodeOutputTypeEnum } from '../../workflow/node/constant';
import { WorkflowIOValueTypeEnum } from '../../workflow/constants';
import { PluginInputModule } from '../../workflow/template/system/pluginInput';
import { PluginOutputModule } from '../../workflow/template/system/pluginOutput';
import { HttpNode468 } from '../../workflow/template/system/http468';
import { type HttpParamAndHeaderItemType } from '../../workflow/type/io';
import { type StoreNodeItemType } from '../../workflow/type/node';
import { HttpImgUrl } from '../../../common/file/image/constants';
import SwaggerParser from '@apidevtools/swagger-parser';
import { getHandleId } from '../../workflow/utils';
import { type CreateHttpPluginChildrenPros } from '../controller';
import { AppTypeEnum } from '../constants';
import type { StoreEdgeItemType } from '../../workflow/type/edge';
export const str2OpenApiSchema = async (yamlStr = ''): Promise<OpenApiJsonSchema> => {
try {
const data = (() => {
try {
return JSON.parse(yamlStr);
} catch (jsonError) {
return yaml.load(yamlStr, { schema: yaml.FAILSAFE_SCHEMA });
}
})();
const jsonSchema = (await SwaggerParser.parse(data)) as OpenAPIV3.Document;
const serverPath = jsonSchema.servers?.[0].url || '';
const pathData = Object.keys(jsonSchema.paths)
.map((path) => {
const methodData: any = data.paths[path];
return Object.keys(methodData)
.filter((method) =>
['get', 'post', 'put', 'delete', 'patch'].includes(method.toLocaleLowerCase())
)
.map((method) => {
const methodInfo = methodData[method];
if (methodInfo.deprecated) return;
const result = {
path,
method,
name: methodInfo.operationId || path,
description: methodInfo.description || methodInfo.summary,
params: methodInfo.parameters,
request: methodInfo?.requestBody,
response: methodInfo.responses
};
return result;
});
})
.flat()
.filter(Boolean) as OpenApiJsonSchema['pathData'];
return { pathData, serverPath };
} catch (err) {
throw new Error('Invalid Schema');
}
};
export const getType = (schema: { type: string; items?: { type: string } }) => {
const typeMap: { [key: string]: WorkflowIOValueTypeEnum } = {
string: WorkflowIOValueTypeEnum.arrayString,
number: WorkflowIOValueTypeEnum.arrayNumber,
integer: WorkflowIOValueTypeEnum.arrayNumber,
boolean: WorkflowIOValueTypeEnum.arrayBoolean,
object: WorkflowIOValueTypeEnum.arrayObject
};
if (schema?.type === 'integer') {
return WorkflowIOValueTypeEnum.number;
}
if (schema?.type === 'array' && schema?.items) {
const itemType = typeMap[schema.items.type];
if (itemType) {
return itemType;
}
}
return schema?.type as WorkflowIOValueTypeEnum;
};
export const httpApiSchema2Plugins = async ({
parentId,
apiSchemaStr = '',
customHeader = ''
}: {
parentId: string;
apiSchemaStr?: string;
customHeader?: string;
}): Promise<CreateHttpPluginChildrenPros[]> => {
const jsonSchema = await str2OpenApiSchema(apiSchemaStr);
const baseUrl = jsonSchema.serverPath;
return jsonSchema.pathData.map((item) => {
const pluginOutputId = getNanoid();
const httpId = getNanoid();
const pluginInputId = getNanoid();
const inputIdMap = new Map();
const pluginOutputKey = 'result';
const properties = item.request?.content?.['application/json']?.schema?.properties;
const propsKeys = properties ? Object.keys(properties) : [];
const pluginInputs: FlowNodeInputItemType[] = [
...(item.params?.map((param: any) => {
return {
key: param.name,
valueType: getType(param.schema),
label: param.name,
renderTypeList: [FlowNodeInputTypeEnum.reference],
required: param.required,
description: param.description,
toolDescription: param.description,
canEdit: true
};
}) || []),
...(propsKeys?.map((key) => {
const prop = properties[key];
return {
key,
valueType: getType(prop),
label: key,
renderTypeList: [FlowNodeInputTypeEnum.reference],
required: false,
description: prop.description,
toolDescription: prop.description,
canEdit: true
};
}) || [])
];
const pluginOutputs: FlowNodeOutputItemType[] = [
...(item.params?.map((param: any) => {
const id = getNanoid();
inputIdMap.set(param.name, id);
return {
id,
key: param.name,
valueType: getType(param.schema),
label: param.name,
type: FlowNodeOutputTypeEnum.source
};
}) || []),
...(propsKeys?.map((key) => {
const id = getNanoid();
inputIdMap.set(key, id);
return {
id,
key,
valueType: getType(properties[key]),
label: key,
type: FlowNodeOutputTypeEnum.source,
edit: true
};
}) || [])
];
const httpInputs: FlowNodeInputItemType[] = [
...(item.params?.map((param: any) => {
return {
key: param.name,
valueType: getType(param.schema),
label: param.name,
renderTypeList: [FlowNodeInputTypeEnum.reference],
canEdit: true,
value: [pluginInputId, inputIdMap.get(param.name)]
};
}) || []),
...(propsKeys?.map((key) => {
return {
key,
valueType: getType(properties[key]),
label: key,
renderTypeList: [FlowNodeInputTypeEnum.reference],
canEdit: true,
value: [pluginInputId, inputIdMap.get(key)]
};
}) || [])
];
/* http node setting */
const httpNodeParams: HttpParamAndHeaderItemType[] = [];
const httpNodeHeaders: HttpParamAndHeaderItemType[] = [];
let httpNodeBody = '{}';
const requestUrl = `${baseUrl}${item.path}`;
if (item.params && item.params.length > 0) {
for (const param of item.params) {
if (param.in === 'header') {
httpNodeHeaders.push({
key: param.name,
type: getType(param.schema) || WorkflowIOValueTypeEnum.string,
value: `{{${param.name}}}`
});
} else if (param.in === 'body') {
httpNodeBody = JSON.stringify(
{ ...JSON.parse(httpNodeBody), [param.name]: `{{${param.name}}}` },
null,
2
);
} else if (param.in === 'query') {
httpNodeParams.push({
key: param.name,
type: getType(param.schema) || WorkflowIOValueTypeEnum.string,
value: `{{${param.name}}}`
});
}
}
}
if (item.request) {
const properties = item.request?.content?.['application/json']?.schema?.properties || {};
const keys = Object.keys(properties);
if (keys.length > 0) {
httpNodeBody = JSON.stringify(
keys.reduce((acc: any, key) => {
acc[key] = `{{${key}}}`;
return acc;
}, {}),
null,
2
);
}
}
if (customHeader) {
const headersObj = (() => {
try {
return JSON.parse(customHeader) as Record<string, string>;
} catch (err) {
return {};
}
})();
for (const key in headersObj) {
httpNodeHeaders.push({
key,
type: WorkflowIOValueTypeEnum.string,
// @ts-ignore
value: headersObj[key]
});
}
}
/* Combine complete modules */
const nodes: StoreNodeItemType[] = [
{
nodeId: pluginInputId,
name: PluginInputModule.name,
intro: PluginInputModule.intro,
avatar: PluginInputModule.avatar,
flowNodeType: PluginInputModule.flowNodeType,
showStatus: PluginInputModule.showStatus,
position: {
x: 473.55206291900333,
y: -145.65080850146154
},
version: PluginInputModule.version,
inputs: pluginInputs,
outputs: pluginOutputs
},
{
nodeId: pluginOutputId,
name: PluginOutputModule.name,
intro: PluginOutputModule.intro,
avatar: PluginOutputModule.avatar,
flowNodeType: PluginOutputModule.flowNodeType,
showStatus: PluginOutputModule.showStatus,
position: {
x: 1847.5956872650024,
y: 5.114324648101558
},
version: PluginOutputModule.version,
inputs: [
{
key: pluginOutputKey,
valueType: WorkflowIOValueTypeEnum.string,
label: pluginOutputKey,
renderTypeList: [FlowNodeInputTypeEnum.reference],
required: false,
description: '',
canEdit: true,
value: [httpId, 'httpRawResponse']
}
],
outputs: [
{
id: pluginOutputId,
key: pluginOutputKey,
valueType: WorkflowIOValueTypeEnum.string,
label: pluginOutputKey,
type: FlowNodeOutputTypeEnum.static
}
]
},
{
nodeId: httpId,
name: HttpNode468.name,
intro: HttpNode468.intro,
avatar: HttpNode468.avatar,
flowNodeType: HttpNode468.flowNodeType,
showStatus: true,
position: {
x: 1188.947986995841,
y: -473.52694296182904
},
version: HttpNode468.version,
inputs: [
HttpNode468.inputs[0],
...httpInputs,
{
key: 'system_httpMethod',
renderTypeList: [FlowNodeInputTypeEnum.custom],
valueType: WorkflowIOValueTypeEnum.string,
label: '',
value: item.method.toUpperCase(),
required: true
},
{
key: 'system_httpReqUrl',
renderTypeList: [FlowNodeInputTypeEnum.hidden],
valueType: WorkflowIOValueTypeEnum.string,
label: '',
description: 'core.module.input.description.Http Request Url',
placeholder: 'https://api.ai.com/getInventory',
required: false,
value: requestUrl
},
{
key: 'system_httpHeader',
renderTypeList: [FlowNodeInputTypeEnum.custom],
valueType: WorkflowIOValueTypeEnum.any,
value: httpNodeHeaders,
label: '',
description: 'core.module.input.description.Http Request Header',
placeholder: 'core.module.input.description.Http Request Header',
required: false
},
{
key: 'system_httpParams',
renderTypeList: [FlowNodeInputTypeEnum.hidden],
valueType: WorkflowIOValueTypeEnum.any,
value: httpNodeParams,
label: '',
required: false
},
{
key: 'system_httpJsonBody',
renderTypeList: [FlowNodeInputTypeEnum.hidden],
valueType: WorkflowIOValueTypeEnum.any,
value: httpNodeBody,
label: '',
required: false
}
],
outputs: HttpNode468.outputs
}
];
const edges: StoreEdgeItemType[] = [
{
source: pluginInputId,
target: httpId,
sourceHandle: getHandleId(pluginInputId, 'source', 'right'),
targetHandle: getHandleId(httpId, 'target', 'left')
},
{
source: httpId,
target: pluginOutputId,
sourceHandle: getHandleId(httpId, 'source', 'right'),
targetHandle: getHandleId(pluginOutputId, 'target', 'left')
}
];
return {
name: item.name,
avatar: HttpImgUrl,
intro: item.description,
parentId,
type: AppTypeEnum.plugin,
modules: nodes,
edges,
pluginData: {
pluginUniId: item.name
}
};
});
};