V4.9.6 feature (#4565)

* Dashboard submenu (#4545)

* add app submenu (#4452)

* add app submenu

* fix

* width & i18n

* optimize submenu code (#4515)

* optimize submenu code

* fix

* fix

* fix

* fix ts

* perf: dashboard sub menu

* doc

---------

Co-authored-by: heheer <heheer@sealos.io>

* feat: value format test

* doc

* Mcp export (#4555)

* feat: mcp server

* feat: mcp server

* feat: mcp server build

* update doc

* perf: path selector (#4556)

* perf: path selector

* fix: docker file path

* perf: add image endpoint to dataset search (#4557)

* perf: add image endpoint to dataset search

* fix: mcp_server url

* human in loop (#4558)

* Support interactive nodes for loops, and enhance the function of merging nested and loop node history messages. (#4552)

* feat: add LoopInteractive definition

* feat: Support LoopInteractive type and update related logic

* fix: Refactor loop handling logic and improve output value initialization

* feat: Add mergeSignId to dispatchLoop and dispatchRunAppNode responses

* feat: Enhance mergeChatResponseData to recursively merge plugin details and improve response handling

* refactor: Remove redundant comments in mergeChatResponseData for clarity

* perf: loop interactive

* perf: human in loop

---------

Co-authored-by: Theresa <63280168+sd0ric4@users.noreply.github.com>

* mcp server ui

* integrate mcp (#4549)

* integrate mcp

* delete unused code

* fix ts

* bug fix

* fix

* support whole mcp tools

* add try catch

* fix

* fix

* fix ts

* fix test

* fix ts

* fix: interactive in v1 completions

* doc

* fix: router path

* fix mcp integrate (#4563)

* fix mcp integrate

* fix ui

* fix: mcp ux

* feat: mcp call title

* remove repeat loading

* fix mcp tools avatar (#4564)

* fix

* fix avatar

* fix update version

* update doc

* fix: value format

* close server and remove cache

* perf: avatar

---------

Co-authored-by: heheer <heheer@sealos.io>
Co-authored-by: Theresa <63280168+sd0ric4@users.noreply.github.com>
This commit is contained in:
Archer
2025-04-16 22:18:51 +08:00
committed by GitHub
parent ab799e13cd
commit 952412f648
166 changed files with 6318 additions and 1263 deletions

View File

@@ -5,6 +5,7 @@ import { ErrType } from '../errorCode';
const startCode = 507000;
export enum CommonErrEnum {
invalidParams = 'invalidParams',
invalidResource = 'invalidResource',
fileNotFound = 'fileNotFound',
unAuthFile = 'unAuthFile',
missingParams = 'missingParams',
@@ -15,6 +16,10 @@ const datasetErr = [
statusText: CommonErrEnum.fileNotFound,
message: i18nT('common:error.invalid_params')
},
{
statusText: CommonErrEnum.invalidResource,
message: i18nT('common:error_invalid_resource')
},
{
statusText: CommonErrEnum.fileNotFound,
message: 'error.fileNotFound'

View File

@@ -27,7 +27,8 @@ export enum TeamErrEnum {
userNotActive = 'userNotActive',
invitationLinkInvalid = 'invitationLinkInvalid',
youHaveBeenInTheTeam = 'youHaveBeenInTheTeam',
tooManyInvitations = 'tooManyInvitations'
tooManyInvitations = 'tooManyInvitations',
unPermission = 'unPermission'
}
const teamErr = [
@@ -35,6 +36,10 @@ const teamErr = [
statusText: TeamErrEnum.notUser,
message: i18nT('common:code_error.team_error.not_user')
},
{
statusText: TeamErrEnum.unPermission,
message: i18nT('common:error_un_permission')
},
{
statusText: TeamErrEnum.teamOverSize,
message: i18nT('common:code_error.team_error.over_size')

View File

@@ -49,6 +49,7 @@ export type FastGPTFeConfigsType = {
find_password_method?: ['email' | 'phone'];
bind_notification_method?: ['email' | 'phone'];
googleClientVerKey?: string;
mcpServerProxyEndpoint?: string;
show_emptyChat?: boolean;
show_appStore?: boolean;

View File

@@ -11,7 +11,9 @@ export enum AppTypeEnum {
simple = 'simple',
workflow = 'advanced',
plugin = 'plugin',
httpPlugin = 'httpPlugin'
httpPlugin = 'httpPlugin',
toolSet = 'toolSet',
tool = 'tool'
}
export const AppFolderTypeList = [AppTypeEnum.folder, AppTypeEnum.httpPlugin];
@@ -53,7 +55,10 @@ export enum AppTemplateTypeEnum {
imageGeneration = 'image-generation',
webSearch = 'web-search',
roleplay = 'roleplay',
officeServices = 'office-services'
officeServices = 'office-services',
// special type
contribute = 'contribute'
}
export const defaultDatasetMaxTokens = 16000;

View File

@@ -0,0 +1,97 @@
import { NodeOutputKeyEnum, WorkflowIOValueTypeEnum } from '../../workflow/constants';
import {
FlowNodeInputTypeEnum,
FlowNodeOutputTypeEnum,
FlowNodeTypeEnum
} from '../../workflow/node/constant';
import { nanoid } from 'nanoid';
import { ToolType } from '../type';
import { i18nT } from '../../../../web/i18n/utils';
import { RuntimeNodeItemType } from '../../workflow/runtime/type';
export const getMCPToolSetRuntimeNode = ({
url,
toolList,
name,
avatar
}: {
url: string;
toolList: ToolType[];
name?: string;
avatar?: string;
}): RuntimeNodeItemType => {
return {
nodeId: nanoid(16),
flowNodeType: FlowNodeTypeEnum.toolSet,
avatar,
intro: 'MCP Tools',
inputs: [
{
key: 'toolSetData',
label: 'Tool Set Data',
valueType: WorkflowIOValueTypeEnum.object,
renderTypeList: [FlowNodeInputTypeEnum.hidden],
value: { url, toolList }
}
],
outputs: [],
name: name || '',
version: ''
};
};
export const getMCPToolRuntimeNode = ({
tool,
url,
avatar = 'core/app/type/mcpToolsFill'
}: {
tool: ToolType;
url: string;
avatar?: string;
}): RuntimeNodeItemType => {
return {
nodeId: nanoid(16),
flowNodeType: FlowNodeTypeEnum.tool,
avatar,
intro: tool.description,
inputs: [
{
key: 'toolData',
label: 'Tool Data',
valueType: WorkflowIOValueTypeEnum.object,
renderTypeList: [FlowNodeInputTypeEnum.hidden],
value: { ...tool, url }
},
...Object.entries(tool.inputSchema?.properties || {}).map(([key, value]) => ({
key,
label: key,
valueType: value.type as WorkflowIOValueTypeEnum,
description: value.description,
toolDescription: value.description || key,
required: tool.inputSchema?.required?.includes(key) || false,
renderTypeList: [
value.type === 'string'
? FlowNodeInputTypeEnum.input
: value.type === 'number'
? FlowNodeInputTypeEnum.numberInput
: value.type === 'boolean'
? FlowNodeInputTypeEnum.switch
: FlowNodeInputTypeEnum.JSONEditor
]
}))
],
outputs: [
{
id: NodeOutputKeyEnum.rawResponse,
key: NodeOutputKeyEnum.rawResponse,
required: true,
label: i18nT('workflow:raw_response'),
description: i18nT('workflow:tool_raw_response_description'),
valueType: WorkflowIOValueTypeEnum.any,
type: FlowNodeOutputTypeEnum.static
}
],
name: tool.name,
version: ''
};
};

View File

@@ -16,6 +16,16 @@ import { FlowNodeInputTypeEnum } from '../../core/workflow/node/constant';
import { WorkflowTemplateBasicType } from '@fastgpt/global/core/workflow/type';
import { SourceMemberType } from '../../support/user/type';
export type ToolType = {
name: string;
description: string;
inputSchema: {
type: string;
properties?: Record<string, { type: string; description?: string }>;
required?: string[];
};
};
export type AppSchema = {
_id: string;
parentId?: ParentIdType;

View File

@@ -140,7 +140,9 @@ export const appWorkflow2Form = ({
);
} else if (
node.flowNodeType === FlowNodeTypeEnum.pluginModule ||
node.flowNodeType === FlowNodeTypeEnum.appModule
node.flowNodeType === FlowNodeTypeEnum.appModule ||
node.flowNodeType === FlowNodeTypeEnum.tool ||
node.flowNodeType === FlowNodeTypeEnum.toolSet
) {
if (!node.pluginId) return;

View File

@@ -38,7 +38,8 @@ export enum ChatSourceEnum {
team = 'team',
feishu = 'feishu',
official_account = 'official_account',
wecom = 'wecom'
wecom = 'wecom',
mcp = 'mcp'
}
export const ChatSourceMap = {
@@ -68,6 +69,9 @@ export const ChatSourceMap = {
},
[ChatSourceEnum.wecom]: {
name: i18nT('common:core.chat.logs.wecom')
},
[ChatSourceEnum.mcp]: {
name: i18nT('common:core.chat.logs.mcp')
}
};

View File

@@ -154,25 +154,55 @@ export const getChatSourceByPublishChannel = (publishChannel: PublishChannelEnum
/*
Merge chat responseData
1. Same tool mergeSignId (Interactive tool node)
2. Recursively merge plugin details with same mergeSignId
*/
export const mergeChatResponseData = (responseDataList: ChatHistoryItemResType[]) => {
let lastResponse: ChatHistoryItemResType | undefined = undefined;
return responseDataList.reduce<ChatHistoryItemResType[]>((acc, curr) => {
if (lastResponse && lastResponse.mergeSignId && curr.mergeSignId === lastResponse.mergeSignId) {
// 替换 lastResponse
const concatResponse: ChatHistoryItemResType = {
...curr,
runningTime: +((lastResponse.runningTime || 0) + (curr.runningTime || 0)).toFixed(2),
totalPoints: (lastResponse.totalPoints || 0) + (curr.totalPoints || 0),
childTotalPoints: (lastResponse.childTotalPoints || 0) + (curr.childTotalPoints || 0),
toolCallTokens: (lastResponse.toolCallTokens || 0) + (curr.toolCallTokens || 0),
toolDetail: [...(lastResponse.toolDetail || []), ...(curr.toolDetail || [])]
export const mergeChatResponseData = (
responseDataList: ChatHistoryItemResType[]
): ChatHistoryItemResType[] => {
// Merge children reponse data(Children has interactive response)
const responseWithMergedPlugins = responseDataList.map((item) => {
if (item.pluginDetail && item.pluginDetail.length > 1) {
return {
...item,
pluginDetail: mergeChatResponseData(item.pluginDetail)
};
return [...acc.slice(0, -1), concatResponse];
} else {
lastResponse = curr;
return [...acc, curr];
}
}, []);
return item;
});
let lastResponse: ChatHistoryItemResType | undefined = undefined;
let hasMerged = false;
const firstPassResult = responseWithMergedPlugins.reduce<ChatHistoryItemResType[]>(
(acc, curr) => {
if (
lastResponse &&
lastResponse.mergeSignId &&
curr.mergeSignId === lastResponse.mergeSignId
) {
const concatResponse: ChatHistoryItemResType = {
...curr,
runningTime: +((lastResponse.runningTime || 0) + (curr.runningTime || 0)).toFixed(2),
totalPoints: (lastResponse.totalPoints || 0) + (curr.totalPoints || 0),
childTotalPoints: (lastResponse.childTotalPoints || 0) + (curr.childTotalPoints || 0),
toolCallTokens: (lastResponse.toolCallTokens || 0) + (curr.toolCallTokens || 0),
toolDetail: [...(lastResponse.toolDetail || []), ...(curr.toolDetail || [])],
loopDetail: [...(lastResponse.loopDetail || []), ...(curr.loopDetail || [])],
pluginDetail: [...(lastResponse.pluginDetail || []), ...(curr.pluginDetail || [])]
};
hasMerged = true;
return [...acc.slice(0, -1), concatResponse];
} else {
lastResponse = curr;
return [...acc, curr];
}
},
[]
);
if (hasMerged && firstPassResult.length > 1) {
return mergeChatResponseData(firstPassResult);
}
return firstPassResult;
};

View File

@@ -140,7 +140,9 @@ export enum FlowNodeTypeEnum {
loopStart = 'loopStart',
loopEnd = 'loopEnd',
formInput = 'formInput',
comment = 'comment'
comment = 'comment',
tool = 'tool',
toolSet = 'toolSet'
}
// node IO value type

View File

@@ -217,6 +217,8 @@ export type DispatchNodeResponseType = {
// tool params
toolParamsResult?: Record<string, any>;
toolRes?: any;
// abandon
extensionModel?: string;
extensionResult?: string;

View File

@@ -10,6 +10,7 @@ import { FlowNodeOutputItemType, ReferenceValueType } from '../type/io';
import { ChatItemType, NodeOutputItemType } from '../../../core/chat/type';
import { ChatItemValueTypeEnum, ChatRoleEnum } from '../../../core/chat/constants';
import { replaceVariable, valToStr } from '../../../common/string/tools';
import json5 from 'json5';
import {
InteractiveNodeResponseType,
WorkflowInteractiveResponseType
@@ -18,7 +19,10 @@ import {
export const extractDeepestInteractive = (
interactive: WorkflowInteractiveResponseType
): WorkflowInteractiveResponseType => {
if (interactive?.type === 'childrenInteractive' && interactive.params?.childrenResponse) {
if (
(interactive?.type === 'childrenInteractive' || interactive?.type === 'loopInteractive') &&
interactive.params?.childrenResponse
) {
return extractDeepestInteractive(interactive.params.childrenResponse);
}
return interactive;
@@ -40,6 +44,113 @@ export const getMaxHistoryLimitFromNodes = (nodes: StoreNodeItemType[]): number
return limit * 2;
};
/* value type format */
export const valueTypeFormat = (value: any, type?: WorkflowIOValueTypeEnum) => {
const isObjectString = (value: any) => {
if (typeof value === 'string' && value !== 'false' && value !== 'true') {
const trimmedValue = value.trim();
const isJsonString =
(trimmedValue.startsWith('{') && trimmedValue.endsWith('}')) ||
(trimmedValue.startsWith('[') && trimmedValue.endsWith(']'));
return isJsonString;
}
return false;
};
// 1. any值忽略格式化
if (value === undefined || value === null) return value;
if (!type || type === WorkflowIOValueTypeEnum.any) return value;
// 2. 如果值已经符合目标类型,直接返回
if (
(type === WorkflowIOValueTypeEnum.string && typeof value === 'string') ||
(type === WorkflowIOValueTypeEnum.number && typeof value === 'number') ||
(type === WorkflowIOValueTypeEnum.boolean && typeof value === 'boolean') ||
(type.startsWith('array') && Array.isArray(value)) ||
(type === WorkflowIOValueTypeEnum.object && typeof value === 'object') ||
(type === WorkflowIOValueTypeEnum.chatHistory &&
(Array.isArray(value) || typeof value === 'number')) ||
(type === WorkflowIOValueTypeEnum.datasetQuote && Array.isArray(value)) ||
(type === WorkflowIOValueTypeEnum.selectDataset && Array.isArray(value)) ||
(type === WorkflowIOValueTypeEnum.selectApp && typeof value === 'object')
) {
return value;
}
// 4. 按目标类型,进行格式转化
// 4.1 基本类型转换
if (type === WorkflowIOValueTypeEnum.string) {
return typeof value === 'object' ? JSON.stringify(value) : String(value);
}
if (type === WorkflowIOValueTypeEnum.number) {
return Number(value);
}
if (type === WorkflowIOValueTypeEnum.boolean) {
if (typeof value === 'string') {
return value.toLowerCase() === 'true';
}
return Boolean(value);
}
// 4.3 字符串转对象
if (
(type === WorkflowIOValueTypeEnum.object || type.startsWith('array')) &&
typeof value === 'string' &&
value.trim()
) {
const trimmedValue = value.trim();
const isJsonString = isObjectString(trimmedValue);
if (isJsonString) {
try {
const parsed = json5.parse(trimmedValue);
// 检测解析结果与目标类型是否一致
if (type.startsWith('array') && Array.isArray(parsed)) return parsed;
if (type === WorkflowIOValueTypeEnum.object && typeof parsed === 'object') return parsed;
} catch (error) {}
}
}
// 4.4 数组类型(这里 value 不是数组类型)TODO: 嵌套数据类型转化)
if (type.startsWith('array')) {
return [value];
}
// 4.5 特殊类型处理
if (
[WorkflowIOValueTypeEnum.datasetQuote, WorkflowIOValueTypeEnum.selectDataset].includes(type)
) {
if (isObjectString(value)) {
try {
return json5.parse(value);
} catch (error) {
return [];
}
}
return [];
}
if (
[WorkflowIOValueTypeEnum.selectApp, WorkflowIOValueTypeEnum.object].includes(type) &&
typeof value === 'string'
) {
if (isObjectString(value)) {
try {
return json5.parse(value);
} catch (error) {
return {};
}
}
return {};
}
// Invalid history type
if (type === WorkflowIOValueTypeEnum.chatHistory) {
return 0;
}
// 5. 默认返回原值
return value;
};
/*
Get interaction information (if any) from the last AI message.
What can be done:
@@ -62,7 +173,10 @@ export const getLastInteractiveValue = (
return;
}
if (lastValue.interactive.type === 'childrenInteractive') {
if (
lastValue.interactive.type === 'childrenInteractive' ||
lastValue.interactive.type === 'loopInteractive'
) {
return lastValue.interactive;
}
@@ -83,7 +197,7 @@ export const getLastInteractiveValue = (
return;
};
export const initWorkflowEdgeStatus = (
export const storeEdges2RuntimeEdges = (
edges: StoreEdgeItemType[],
lastInteractive?: WorkflowInteractiveResponseType
): RuntimeEdgeItemType[] => {
@@ -114,7 +228,12 @@ export const getWorkflowEntryNodeIds = (
FlowNodeTypeEnum.pluginInput
];
return nodes
.filter((node) => entryList.includes(node.flowNodeType as any))
.filter(
(node) =>
entryList.includes(node.flowNodeType as any) ||
(!nodes.some((item) => entryList.includes(item.flowNodeType as any)) &&
node.flowNodeType === FlowNodeTypeEnum.tool)
)
.map((item) => item.nodeId);
};
@@ -312,7 +431,6 @@ export const formatVariableValByType = (val: any, valueType?: WorkflowIOValueTyp
if (
[
WorkflowIOValueTypeEnum.object,
WorkflowIOValueTypeEnum.chatHistory,
WorkflowIOValueTypeEnum.datasetQuote,
WorkflowIOValueTypeEnum.selectApp,
WorkflowIOValueTypeEnum.selectDataset

View File

@@ -34,6 +34,8 @@ import { LoopStartNode } from './system/loop/loopStart';
import { LoopEndNode } from './system/loop/loopEnd';
import { FormInputNode } from './system/interactive/formInput';
import { ToolParamsNode } from './system/toolParams';
import { RunToolNode } from './system/runTool';
import { RunToolSetNode } from './system/runToolSet';
const systemNodes: FlowNodeTemplateType[] = [
AiChatModule,
@@ -84,5 +86,7 @@ export const moduleTemplatesFlat: FlowNodeTemplateType[] = [
RunAppNode,
RunAppModule,
LoopStartNode,
LoopEndNode
LoopEndNode,
RunToolNode,
RunToolSetNode
];

View File

@@ -8,7 +8,7 @@ import { i18nT } from '../../../../web/i18n/utils';
export const Input_Template_History: FlowNodeInputItemType = {
key: NodeInputKeyEnum.history,
renderTypeList: [FlowNodeInputTypeEnum.numberInput, FlowNodeInputTypeEnum.reference],
valueType: WorkflowIOValueTypeEnum.chatHistory,
valueType: WorkflowIOValueTypeEnum.chatHistory, // Array / Number
label: i18nT('common:core.module.input.label.chat history'),
description: i18nT('workflow:max_dialog_rounds'),

View File

@@ -28,6 +28,15 @@ type ChildrenInteractive = InteractiveNodeType & {
};
};
type LoopInteractive = InteractiveNodeType & {
type: 'loopInteractive';
params: {
loopResult: any[];
childrenResponse: WorkflowInteractiveResponseType;
currentIndex: number;
};
};
export type UserSelectOptionItemType = {
key: string;
value: string;
@@ -71,5 +80,7 @@ type UserInputInteractive = InteractiveNodeType & {
export type InteractiveNodeResponseType =
| UserSelectInteractive
| UserInputInteractive
| ChildrenInteractive;
| ChildrenInteractive
| LoopInteractive;
export type WorkflowInteractiveResponseType = InteractiveBasicType & InteractiveNodeResponseType;

View File

@@ -0,0 +1,19 @@
import { FlowNodeTemplateTypeEnum } from '../../constants';
import { FlowNodeTypeEnum } from '../../node/constant';
import { FlowNodeTemplateType } from '../../type/node';
import { getHandleConfig } from '../utils';
export const RunToolNode: FlowNodeTemplateType = {
id: FlowNodeTypeEnum.tool,
templateType: FlowNodeTemplateTypeEnum.other,
flowNodeType: FlowNodeTypeEnum.tool,
sourceHandle: getHandleConfig(true, true, true, true),
targetHandle: getHandleConfig(true, true, true, true),
intro: '',
name: '',
showStatus: false,
isTool: true,
version: '4.9.6',
inputs: [],
outputs: []
};

View File

@@ -0,0 +1,19 @@
import { FlowNodeTemplateTypeEnum } from '../../constants';
import { FlowNodeTypeEnum } from '../../node/constant';
import { FlowNodeTemplateType } from '../../type/node';
import { getHandleConfig } from '../utils';
export const RunToolSetNode: FlowNodeTemplateType = {
id: FlowNodeTypeEnum.toolSet,
templateType: FlowNodeTemplateTypeEnum.other,
flowNodeType: FlowNodeTypeEnum.toolSet,
sourceHandle: getHandleConfig(false, false, false, false),
targetHandle: getHandleConfig(false, false, false, false),
intro: '',
name: '',
showStatus: false,
isTool: true,
version: '4.9.6',
inputs: [],
outputs: []
};

View File

@@ -311,6 +311,38 @@ export const appData2FlowNodeIO = ({
};
};
export const toolData2FlowNodeIO = ({
nodes
}: {
nodes: StoreNodeItemType[];
}): {
inputs: FlowNodeInputItemType[];
outputs: FlowNodeOutputItemType[];
} => {
const toolNode = nodes.find((node) => node.flowNodeType === FlowNodeTypeEnum.tool);
return {
inputs: toolNode?.inputs || [],
outputs: toolNode?.outputs || []
};
};
export const toolSetData2FlowNodeIO = ({
nodes
}: {
nodes: StoreNodeItemType[];
}): {
inputs: FlowNodeInputItemType[];
outputs: FlowNodeOutputItemType[];
} => {
const toolSetNode = nodes.find((node) => node.flowNodeType === FlowNodeTypeEnum.toolSet);
return {
inputs: toolSetNode?.inputs || [],
outputs: toolSetNode?.outputs || []
};
};
export const formatEditorVariablePickerIcon = (
variables: { key: string; label: string; type?: `${VariableInputEnum}`; required?: boolean }[]
): EditorVariablePickerType[] => {

14
packages/global/support/mcp/type.d.ts vendored Normal file
View File

@@ -0,0 +1,14 @@
export type McpKeyType = {
_id: string;
key: string;
teamId: string;
tmbId: string;
apps: McpAppType[];
name: string;
};
export type McpAppType = {
appId: string;
toolName: string;
description: string;
};

View File

@@ -11,7 +11,8 @@ export enum UsageSourceEnum {
feishu = 'feishu',
dingtalk = 'dingtalk',
official_account = 'official_account',
pdfParse = 'pdfParse'
pdfParse = 'pdfParse',
mcp = 'mcp'
}
export const UsageSourceMap = {
@@ -47,5 +48,8 @@ export const UsageSourceMap = {
},
[UsageSourceEnum.pdfParse]: {
label: i18nT('account_usage:pdf_parse')
},
[UsageSourceEnum.mcp]: {
label: i18nT('account_usage:mcp')
}
};