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')
}
};

View File

@@ -32,3 +32,13 @@ export const getImageBase64 = async (url: string) => {
return Promise.reject(error);
}
};
export const addEndpointToImageUrl = (text: string) => {
const baseURL = process.env.FE_DOMAIN;
if (!baseURL) return text;
// 匹配 /api/system/img/xxx.xx 的图片链接,并追加 baseURL
return text.replace(
/(?<!https?:\/\/[^\s]*)(?:\/api\/system\/img\/[^\s.]*\.[^\s]*)/g,
(match) => `${baseURL}${match}`
);
};

View File

@@ -86,3 +86,19 @@ export async function findAppAndAllChildren({
return [app, ...childDatasets];
}
export const getAppBasicInfoByIds = async ({ teamId, ids }: { teamId: string; ids: string[] }) => {
const apps = await MongoApp.find(
{
teamId,
_id: { $in: ids }
},
'_id name avatar'
).lean();
return apps.map((item) => ({
id: item._id,
name: item.name,
avatar: item.avatar
}));
};

View File

@@ -1,6 +1,11 @@
import { FlowNodeTemplateType } from '@fastgpt/global/core/workflow/type/node.d';
import { FlowNodeTypeEnum, defaultNodeVersion } from '@fastgpt/global/core/workflow/node/constant';
import { appData2FlowNodeIO, pluginData2FlowNodeIO } from '@fastgpt/global/core/workflow/utils';
import {
appData2FlowNodeIO,
pluginData2FlowNodeIO,
toolData2FlowNodeIO,
toolSetData2FlowNodeIO
} from '@fastgpt/global/core/workflow/utils';
import { PluginSourceEnum } from '@fastgpt/global/core/plugin/constants';
import { FlowNodeTemplateTypeEnum } from '@fastgpt/global/core/workflow/constants';
import { getHandleConfig } from '@fastgpt/global/core/workflow/template/utils';
@@ -128,11 +133,41 @@ export async function getChildAppPreviewNode({
(node) => node.flowNodeType === FlowNodeTypeEnum.pluginInput
);
const isTool =
!!app.workflow.nodes.find((node) => node.flowNodeType === FlowNodeTypeEnum.tool) &&
app.workflow.nodes.length === 1;
const isToolSet =
!!app.workflow.nodes.find((node) => node.flowNodeType === FlowNodeTypeEnum.toolSet) &&
app.workflow.nodes.length === 1;
const { flowNodeType, nodeIOConfig } = (() => {
if (isToolSet)
return {
flowNodeType: FlowNodeTypeEnum.toolSet,
nodeIOConfig: toolSetData2FlowNodeIO({ nodes: app.workflow.nodes })
};
if (isTool)
return {
flowNodeType: FlowNodeTypeEnum.tool,
nodeIOConfig: toolData2FlowNodeIO({ nodes: app.workflow.nodes })
};
if (isPlugin)
return {
flowNodeType: FlowNodeTypeEnum.pluginModule,
nodeIOConfig: pluginData2FlowNodeIO({ nodes: app.workflow.nodes })
};
return {
flowNodeType: FlowNodeTypeEnum.appModule,
nodeIOConfig: appData2FlowNodeIO({ chatConfig: app.workflow.chatConfig })
};
})();
return {
id: getNanoid(),
pluginId: app.id,
templateType: app.templateType,
flowNodeType: isPlugin ? FlowNodeTypeEnum.pluginModule : FlowNodeTypeEnum.appModule,
flowNodeType,
avatar: app.avatar,
name: app.name,
intro: app.intro,
@@ -141,11 +176,13 @@ export async function getChildAppPreviewNode({
showStatus: app.showStatus,
isTool: true,
version: app.version,
sourceHandle: getHandleConfig(true, true, true, true),
targetHandle: getHandleConfig(true, true, true, true),
...(isPlugin
? pluginData2FlowNodeIO({ nodes: app.workflow.nodes })
: appData2FlowNodeIO({ chatConfig: app.workflow.chatConfig }))
sourceHandle: isToolSet
? getHandleConfig(false, false, false, false)
: getHandleConfig(true, true, true, true),
targetHandle: isToolSet
? getHandleConfig(false, false, false, false)
: getHandleConfig(true, true, true, true),
...nodeIOConfig
};
}

View File

@@ -11,7 +11,7 @@ import axios from 'axios';
import { ChatCompletionRequestMessageRoleEnum } from '@fastgpt/global/core/ai/constants';
import { i18nT } from '../../../web/i18n/utils';
import { addLog } from '../../common/system/log';
import { getImageBase64 } from '../../common/file/image/utils';
import { addEndpointToImageUrl, getImageBase64 } from '../../common/file/image/utils';
export const filterGPTMessageByMaxContext = async ({
messages = [],
@@ -87,26 +87,17 @@ export const loadRequestMessages = async ({
useVision?: boolean;
origin?: string;
}) => {
const replaceLinkUrl = (text: string) => {
const baseURL = process.env.FE_DOMAIN;
if (!baseURL) return text;
// 匹配 /api/system/img/xxx.xx 的图片链接,并追加 baseURL
return text.replace(
/(?<!https?:\/\/[^\s]*)(?:\/api\/system\/img\/[^\s.]*\.[^\s]*)/g,
(match) => `${baseURL}${match}`
);
};
const parseSystemMessage = (
content: string | ChatCompletionContentPartText[]
): string | ChatCompletionContentPartText[] | undefined => {
if (typeof content === 'string') {
if (!content) return;
return replaceLinkUrl(content);
return addEndpointToImageUrl(content);
}
const arrayContent = content
.filter((item) => item.text)
.map((item) => ({ ...item, text: replaceLinkUrl(item.text) }));
.map((item) => ({ ...item, text: addEndpointToImageUrl(item.text) }));
if (arrayContent.length === 0) return;
return arrayContent;
};

View File

@@ -7,7 +7,7 @@ import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants';
import { SseResponseEventEnum } from '@fastgpt/global/core/workflow/runtime/constants';
import {
getWorkflowEntryNodeIds,
initWorkflowEdgeStatus,
storeEdges2RuntimeEdges,
storeNodes2RuntimeNodes,
textAdaptGptResponse
} from '@fastgpt/global/core/workflow/runtime/utils';
@@ -70,7 +70,7 @@ export const dispatchAppRequest = async (props: Props): Promise<Response> => {
appData.modules,
getWorkflowEntryNodeIds(appData.modules)
),
runtimeEdges: initWorkflowEdgeStatus(appData.edges),
runtimeEdges: storeEdges2RuntimeEdges(appData.edges),
histories: chatHistories,
query: runtimePrompt2ChatsValue({
files,

View File

@@ -22,7 +22,7 @@ import { formatModelChars2Points } from '../../../../../support/wallet/usage/uti
import { getHistoryPreview } from '@fastgpt/global/core/chat/utils';
import { runToolWithFunctionCall } from './functionCall';
import { runToolWithPromptCall } from './promptCall';
import { replaceVariable } from '@fastgpt/global/common/string/tools';
import { getNanoid, replaceVariable } from '@fastgpt/global/common/string/tools';
import { getMultiplePrompt, Prompt_Tool_Call } from './constants';
import { filterToolResponseToPreview } from './utils';
import { InteractiveNodeResponseType } from '@fastgpt/global/core/workflow/template/system/interactive/type';
@@ -188,6 +188,8 @@ export const dispatchRunTools = async (props: DispatchToolModuleProps): Promise<
if (toolModel.toolChoice) {
return runToolWithToolChoice({
...props,
runtimeNodes,
runtimeEdges,
toolNodes,
toolModel,
maxRunToolTimes: 30,
@@ -198,6 +200,8 @@ export const dispatchRunTools = async (props: DispatchToolModuleProps): Promise<
if (toolModel.functionCall) {
return runToolWithFunctionCall({
...props,
runtimeNodes,
runtimeEdges,
toolNodes,
toolModel,
messages: adaptMessages,
@@ -226,6 +230,8 @@ export const dispatchRunTools = async (props: DispatchToolModuleProps): Promise<
return runToolWithPromptCall({
...props,
runtimeNodes,
runtimeEdges,
toolNodes,
toolModel,
messages: adaptMessages,

View File

@@ -17,6 +17,7 @@ import { MongoDataset } from '../../../dataset/schema';
import { i18nT } from '../../../../../web/i18n/utils';
import { filterDatasetsByTmbId } from '../../../dataset/utils';
import { ModelTypeEnum } from '@fastgpt/global/core/ai/model';
import { addEndpointToImageUrl } from '../../../../common/file/image/utils';
type DatasetSearchProps = ModuleDispatchProps<{
[NodeInputKeyEnum.datasetSelectList]: SelectedDatasetType;
@@ -246,7 +247,7 @@ export async function dispatchDatasetSearch(
[DispatchNodeResponseKeyEnum.toolResponses]: searchRes.map((item) => ({
sourceName: item.sourceName,
updateTime: item.updateTime,
content: `${item.q}\n${item.a}`.trim()
content: addEndpointToImageUrl(`${item.q}\n${item.a}`.trim())
}))
};
}

View File

@@ -37,7 +37,8 @@ import { dispatchQueryExtension } from './tools/queryExternsion';
import { dispatchRunPlugin } from './plugin/run';
import { dispatchPluginInput } from './plugin/runInput';
import { dispatchPluginOutput } from './plugin/runOutput';
import { formatHttpError, removeSystemVariable, valueTypeFormat } from './utils';
import { formatHttpError, removeSystemVariable, rewriteRuntimeWorkFlow } from './utils';
import { valueTypeFormat } from '@fastgpt/global/core/workflow/runtime/utils';
import {
filterWorkflowEdges,
checkNodeRunStatus,
@@ -74,6 +75,7 @@ import { dispatchFormInput } from './interactive/formInput';
import { dispatchToolParams } from './agent/runTool/toolParams';
import { getErrText } from '@fastgpt/global/common/error/utils';
import { filterModuleTypeList } from '@fastgpt/global/core/chat/utils';
import { dispatchRunTool } from './plugin/runTool';
const callbackMap: Record<FlowNodeTypeEnum, Function> = {
[FlowNodeTypeEnum.workflowStart]: dispatchWorkflowStart,
@@ -104,6 +106,7 @@ const callbackMap: Record<FlowNodeTypeEnum, Function> = {
[FlowNodeTypeEnum.loopStart]: dispatchLoopStart,
[FlowNodeTypeEnum.loopEnd]: dispatchLoopEnd,
[FlowNodeTypeEnum.formInput]: dispatchFormInput,
[FlowNodeTypeEnum.tool]: dispatchRunTool,
// none
[FlowNodeTypeEnum.systemConfig]: dispatchSystemConfig,
@@ -111,6 +114,7 @@ const callbackMap: Record<FlowNodeTypeEnum, Function> = {
[FlowNodeTypeEnum.emptyNode]: () => Promise.resolve(),
[FlowNodeTypeEnum.globalVariable]: () => Promise.resolve(),
[FlowNodeTypeEnum.comment]: () => Promise.resolve(),
[FlowNodeTypeEnum.toolSet]: () => Promise.resolve(),
[FlowNodeTypeEnum.runApp]: dispatchAppRequest // abandoned
};
@@ -136,6 +140,8 @@ export async function dispatchWorkFlow(data: Props): Promise<DispatchFlowRespons
...props
} = data;
rewriteRuntimeWorkFlow(runtimeNodes, runtimeEdges);
// 初始化深度和自动增加深度,避免无限嵌套
if (!props.workflowDispatchDeep) {
props.workflowDispatchDeep = 1;
@@ -643,9 +649,7 @@ export async function dispatchWorkFlow(data: Props): Promise<DispatchFlowRespons
) {
props.workflowStreamResponse?.({
event: SseResponseEventEnum.flowNodeResponse,
data: {
...formatResponseData
}
data: formatResponseData
});
}

View File

@@ -17,19 +17,25 @@ type Response = DispatchNodeResultType<{
export const dispatchWorkflowStart = (props: Record<string, any>): Response => {
const {
query,
variables,
params: { userChatInput }
} = props as UserChatInputProps;
const { text, files } = chatValue2RuntimePrompt(query);
const queryFiles = files
.map((item) => {
return item?.url ?? '';
})
.filter(Boolean);
const variablesFiles: string[] = Array.isArray(variables?.fileUrlList)
? variables.fileUrlList
: [];
return {
[DispatchNodeResponseKeyEnum.nodeResponse]: {},
[NodeInputKeyEnum.userChatInput]: text || userChatInput,
[NodeOutputKeyEnum.userFiles]: files
.map((item) => {
return item?.url ?? '';
})
.filter(Boolean)
[NodeOutputKeyEnum.userFiles]: [...queryFiles, ...variablesFiles]
// [NodeInputKeyEnum.inputFiles]: files
};
};

View File

@@ -8,12 +8,18 @@ import { dispatchWorkFlow } from '..';
import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runtime/constants';
import { AIChatItemValueItemType, ChatHistoryItemResType } from '@fastgpt/global/core/chat/type';
import { cloneDeep } from 'lodash';
import {
LoopInteractive,
WorkflowInteractiveResponseType
} from '@fastgpt/global/core/workflow/template/system/interactive/type';
import { storeEdges2RuntimeEdges } from '@fastgpt/global/core/workflow/runtime/utils';
type Props = ModuleDispatchProps<{
[NodeInputKeyEnum.loopInputArray]: Array<any>;
[NodeInputKeyEnum.childrenNodeIdList]: string[];
}>;
type Response = DispatchNodeResultType<{
[DispatchNodeResponseKeyEnum.interactive]?: LoopInteractive;
[NodeOutputKeyEnum.loopArray]: Array<any>;
}>;
@@ -21,6 +27,7 @@ export const dispatchLoop = async (props: Props): Promise<Response> => {
const {
params,
runtimeEdges,
lastInteractive,
runtimeNodes,
node: { name }
} = props;
@@ -29,6 +36,8 @@ export const dispatchLoop = async (props: Props): Promise<Response> => {
if (!Array.isArray(loopInputArray)) {
return Promise.reject('Input value is not an array');
}
// Max loop times
const maxLength = process.env.WORKFLOW_MAX_LOOP_TIMES
? Number(process.env.WORKFLOW_MAX_LOOP_TIMES)
: 50;
@@ -36,34 +45,63 @@ export const dispatchLoop = async (props: Props): Promise<Response> => {
return Promise.reject(`Input array length cannot be greater than ${maxLength}`);
}
const outputValueArr = [];
const loopDetail: ChatHistoryItemResType[] = [];
const interactiveData =
lastInteractive?.type === 'loopInteractive' ? lastInteractive?.params : undefined;
const lastIndex = interactiveData?.currentIndex;
const outputValueArr = interactiveData ? interactiveData.loopResult : [];
const loopResponseDetail: ChatHistoryItemResType[] = [];
let assistantResponses: AIChatItemValueItemType[] = [];
let totalPoints = 0;
let newVariables: Record<string, any> = props.variables;
let interactiveResponse: WorkflowInteractiveResponseType | undefined = undefined;
let index = 0;
for await (const item of loopInputArray.filter(Boolean)) {
runtimeNodes.forEach((node) => {
if (
childrenNodeIdList.includes(node.nodeId) &&
node.flowNodeType === FlowNodeTypeEnum.loopStart
) {
node.isEntry = true;
node.inputs.forEach((input) => {
if (input.key === NodeInputKeyEnum.loopStartInput) {
input.value = item;
} else if (input.key === NodeInputKeyEnum.loopStartIndex) {
input.value = index++;
}
});
}
});
// Skip already looped
if (lastIndex && index < lastIndex) {
index++;
continue;
}
// It takes effect only once in current loop
const isInteractiveResponseIndex = !!interactiveData && index === interactiveData?.currentIndex;
// Init entry
if (isInteractiveResponseIndex) {
runtimeNodes.forEach((node) => {
if (interactiveData?.childrenResponse?.entryNodeIds.includes(node.nodeId)) {
node.isEntry = true;
}
});
} else {
runtimeNodes.forEach((node) => {
if (!childrenNodeIdList.includes(node.nodeId)) return;
// Init interactive response
if (node.flowNodeType === FlowNodeTypeEnum.loopStart) {
node.isEntry = true;
node.inputs.forEach((input) => {
if (input.key === NodeInputKeyEnum.loopStartInput) {
input.value = item;
} else if (input.key === NodeInputKeyEnum.loopStartIndex) {
input.value = index + 1;
}
});
}
});
}
index++;
const response = await dispatchWorkFlow({
...props,
lastInteractive: interactiveData?.childrenResponse,
variables: newVariables,
runtimeEdges: cloneDeep(runtimeEdges)
runtimeNodes,
runtimeEdges: cloneDeep(
storeEdges2RuntimeEdges(runtimeEdges, interactiveData?.childrenResponse)
)
});
const loopOutputValue = response.flowResponses.find(
@@ -71,8 +109,10 @@ export const dispatchLoop = async (props: Props): Promise<Response> => {
)?.loopOutputValue;
// Concat runtime response
outputValueArr.push(loopOutputValue);
loopDetail.push(...response.flowResponses);
if (!response.workflowInteractiveResponse) {
outputValueArr.push(loopOutputValue);
}
loopResponseDetail.push(...response.flowResponses);
assistantResponses.push(...response.assistantResponses);
totalPoints += response.flowUsages.reduce((acc, usage) => acc + usage.totalPoints, 0);
@@ -81,15 +121,32 @@ export const dispatchLoop = async (props: Props): Promise<Response> => {
...newVariables,
...response.newVariables
};
// handle interactive response
if (response.workflowInteractiveResponse) {
interactiveResponse = response.workflowInteractiveResponse;
break;
}
}
return {
[DispatchNodeResponseKeyEnum.interactive]: interactiveResponse
? {
type: 'loopInteractive',
params: {
currentIndex: index - 1,
childrenResponse: interactiveResponse,
loopResult: outputValueArr
}
}
: undefined,
[DispatchNodeResponseKeyEnum.assistantResponses]: assistantResponses,
[DispatchNodeResponseKeyEnum.nodeResponse]: {
totalPoints,
loopInput: loopInputArray,
loopResult: outputValueArr,
loopDetail: loopDetail
loopDetail: loopResponseDetail,
mergeSignId: props.node.nodeId
},
[DispatchNodeResponseKeyEnum.nodeDispatchUsages]: [
{

View File

@@ -5,7 +5,7 @@ import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runti
import { getChildAppRuntimeById } from '../../../app/plugin/controller';
import {
getWorkflowEntryNodeIds,
initWorkflowEdgeStatus,
storeEdges2RuntimeEdges,
storeNodes2RuntimeNodes
} from '@fastgpt/global/core/workflow/runtime/utils';
import { DispatchNodeResultType } from '@fastgpt/global/core/workflow/runtime/type';
@@ -101,7 +101,7 @@ export const dispatchRunPlugin = async (props: RunPluginProps): Promise<RunPlugi
}).value,
chatConfig: {},
runtimeNodes,
runtimeEdges: initWorkflowEdgeStatus(plugin.edges)
runtimeEdges: storeEdges2RuntimeEdges(plugin.edges)
});
const output = flowResponses.find((item) => item.moduleType === FlowNodeTypeEnum.pluginOutput);
if (output) {

View File

@@ -5,7 +5,8 @@ import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants';
import { SseResponseEventEnum } from '@fastgpt/global/core/workflow/runtime/constants';
import {
getWorkflowEntryNodeIds,
initWorkflowEdgeStatus,
storeEdges2RuntimeEdges,
rewriteNodeOutputByHistories,
storeNodes2RuntimeNodes,
textAdaptGptResponse
} from '@fastgpt/global/core/workflow/runtime/utils';
@@ -107,9 +108,15 @@ export const dispatchRunAppNode = async (props: Props): Promise<Response> => {
lastInteractive?.type === 'childrenInteractive'
? lastInteractive.params.childrenResponse
: undefined;
const entryNodeIds = getWorkflowEntryNodeIds(nodes, childrenInteractive || undefined);
const runtimeNodes = storeNodes2RuntimeNodes(nodes, entryNodeIds);
const runtimeEdges = initWorkflowEdgeStatus(edges, childrenInteractive);
const runtimeNodes = rewriteNodeOutputByHistories(
storeNodes2RuntimeNodes(
nodes,
getWorkflowEntryNodeIds(nodes, childrenInteractive || undefined)
),
childrenInteractive
);
const runtimeEdges = storeEdges2RuntimeEdges(edges, childrenInteractive);
const theQuery = childrenInteractive
? query
: runtimePrompt2ChatsValue({ files: userInputFiles, text: userChatInput });
@@ -170,7 +177,8 @@ export const dispatchRunAppNode = async (props: Props): Promise<Response> => {
totalPoints: usagePoints,
query: userChatInput,
textOutput: text,
pluginDetail: appData.permission.hasWritePer ? flowResponses : undefined
pluginDetail: appData.permission.hasWritePer ? flowResponses : undefined,
mergeSignId: props.node.nodeId
},
[DispatchNodeResponseKeyEnum.nodeDispatchUsages]: [
{

View File

@@ -0,0 +1,60 @@
import {
DispatchNodeResultType,
ModuleDispatchProps
} from '@fastgpt/global/core/workflow/runtime/type';
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runtime/constants';
import { NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants';
type RunToolProps = ModuleDispatchProps<{
toolData: {
name: string;
url: string;
};
}>;
type RunToolResponse = DispatchNodeResultType<{
[NodeOutputKeyEnum.rawResponse]: any;
}>;
export const dispatchRunTool = async (props: RunToolProps): Promise<RunToolResponse> => {
const {
params,
node: { avatar }
} = props;
const { toolData, ...restParams } = params;
const { name: toolName, url } = toolData;
const client = new Client({
name: 'FastGPT-MCP-client',
version: '1.0.0'
});
const result = await (async () => {
try {
const transport = new SSEClientTransport(new URL(url));
await client.connect(transport);
return await client.callTool({
name: toolName,
arguments: restParams
});
} catch (error) {
console.error('Error running MCP tool:', error);
return Promise.reject(error);
} finally {
await client.close();
}
})();
return {
[DispatchNodeResponseKeyEnum.nodeResponse]: {
toolRes: result,
moduleLogo: avatar
},
[DispatchNodeResponseKeyEnum.toolResponses]: result,
[NodeOutputKeyEnum.rawResponse]: result
};
};

View File

@@ -10,7 +10,8 @@ import {
SseResponseEventEnum
} from '@fastgpt/global/core/workflow/runtime/constants';
import axios from 'axios';
import { formatHttpError, valueTypeFormat } from '../utils';
import { formatHttpError } from '../utils';
import { valueTypeFormat } from '@fastgpt/global/core/workflow/runtime/utils';
import { SERVICE_LOCAL_HOST } from '../../../../common/system/tools';
import { addLog } from '../../../../common/system/log';
import { DispatchNodeResultType } from '@fastgpt/global/core/workflow/runtime/type';

View File

@@ -2,7 +2,7 @@ import type { ModuleDispatchProps } from '@fastgpt/global/core/workflow/runtime/
import { NodeInputKeyEnum, NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runtime/constants';
import axios from 'axios';
import { valueTypeFormat } from '../utils';
import { valueTypeFormat } from '@fastgpt/global/core/workflow/runtime/utils';
import { SERVICE_LOCAL_HOST } from '../../../../common/system/tools';
import { addLog } from '../../../../common/system/log';
import { DispatchNodeResultType } from '@fastgpt/global/core/workflow/runtime/type';

View File

@@ -10,8 +10,9 @@ import {
} from '@fastgpt/global/core/workflow/runtime/utils';
import { TUpdateListItem } from '@fastgpt/global/core/workflow/template/system/variableUpdate/type';
import { ModuleDispatchProps } from '@fastgpt/global/core/workflow/runtime/type';
import { removeSystemVariable, valueTypeFormat } from '../utils';
import { removeSystemVariable } from '../utils';
import { isValidReferenceValue } from '@fastgpt/global/core/workflow/utils';
import { valueTypeFormat } from '@fastgpt/global/core/workflow/runtime/utils';
type Props = ModuleDispatchProps<{
[NodeInputKeyEnum.updateList]: TUpdateListItem[];

View File

@@ -7,6 +7,7 @@ import {
} from '@fastgpt/global/core/workflow/constants';
import {
RuntimeEdgeItemType,
RuntimeNodeItemType,
SystemVariablesType
} from '@fastgpt/global/core/workflow/runtime/type';
import { responseWrite } from '../../../common/response';
@@ -14,7 +15,8 @@ import { NextApiResponse } from 'next';
import { SseResponseEventEnum } from '@fastgpt/global/core/workflow/runtime/constants';
import { getNanoid } from '@fastgpt/global/common/string/tools';
import { SearchDataResponseItemType } from '@fastgpt/global/core/dataset/type';
import json5 from 'json5';
import { getMCPToolRuntimeNode } from '@fastgpt/global/core/app/mcpTools/utils';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
export const getWorkflowResponseWrite = ({
res,
@@ -104,102 +106,6 @@ export const getHistories = (history?: ChatItemType[] | number, histories: ChatI
return [...systemHistories, ...filterHistories];
};
/* value type format */
export const valueTypeFormat = (value: any, type?: WorkflowIOValueTypeEnum) => {
// 1. 基础条件检查
if (value === undefined || value === null) return;
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 === WorkflowIOValueTypeEnum.object &&
typeof value === 'object' &&
!Array.isArray(value)) ||
(type.startsWith('array') && Array.isArray(value))
) {
return value;
}
// 3. 处理JSON字符串
if (type === WorkflowIOValueTypeEnum.object || type.startsWith('array')) {
if (typeof value === 'string' && value.trim()) {
const trimmedValue = value.trim();
const isJsonLike =
(trimmedValue.startsWith('{') && trimmedValue.endsWith('}')) ||
(trimmedValue.startsWith('[') && trimmedValue.endsWith(']'));
if (isJsonLike) {
try {
const parsed = json5.parse(trimmedValue);
// 解析结果与目标类型匹配时使用解析后的值
if (
(Array.isArray(parsed) && type.startsWith('array')) ||
(type === WorkflowIOValueTypeEnum.object &&
typeof parsed === 'object' &&
!Array.isArray(parsed))
) {
return parsed;
}
} catch (error) {
// 解析失败时继续使用原始值
}
}
}
}
// 4. 按类型处理
// 4.1 数组类型
if (type.startsWith('array')) {
// 数组类型的特殊处理:字符串转为单元素数组
if (type === WorkflowIOValueTypeEnum.arrayString && typeof value === 'string') {
return [value];
}
// 其他值包装为数组
return [value];
}
// 4.2 基本类型转换
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 (
[
WorkflowIOValueTypeEnum.object,
WorkflowIOValueTypeEnum.chatHistory,
WorkflowIOValueTypeEnum.datasetQuote,
WorkflowIOValueTypeEnum.selectApp,
WorkflowIOValueTypeEnum.selectDataset
].includes(type) &&
typeof value !== 'object'
) {
try {
return json5.parse(value);
} catch (error) {
return value;
}
}
// 5. 默认返回原值
return value;
};
export const checkQuoteQAValue = (quoteQA?: SearchDataResponseItemType[]) => {
if (!quoteQA) return undefined;
if (quoteQA.length === 0) {
@@ -252,3 +158,53 @@ export const formatHttpError = (error: any) => {
status: error?.status
};
};
export const rewriteRuntimeWorkFlow = (
nodes: RuntimeNodeItemType[],
edges: RuntimeEdgeItemType[]
) => {
const toolSetNodes = nodes.filter((node) => node.flowNodeType === FlowNodeTypeEnum.toolSet);
if (toolSetNodes.length === 0) {
return;
}
const nodeIdsToRemove = new Set<string>();
for (const toolSetNode of toolSetNodes) {
nodeIdsToRemove.add(toolSetNode.nodeId);
const toolList =
toolSetNode.inputs.find((input) => input.key === 'toolSetData')?.value?.toolList || [];
const url = toolSetNode.inputs.find((input) => input.key === 'toolSetData')?.value?.url;
const incomingEdges = edges.filter((edge) => edge.target === toolSetNode.nodeId);
for (const tool of toolList) {
const newToolNode = getMCPToolRuntimeNode({ avatar: toolSetNode.avatar, tool, url });
nodes.push({ ...newToolNode, name: `${toolSetNode.name} / ${tool.name}` });
for (const inEdge of incomingEdges) {
edges.push({
source: inEdge.source,
target: newToolNode.nodeId,
sourceHandle: inEdge.sourceHandle,
targetHandle: 'selectedTools',
status: inEdge.status
});
}
}
}
for (let i = nodes.length - 1; i >= 0; i--) {
if (nodeIdsToRemove.has(nodes[i].nodeId)) {
nodes.splice(i, 1);
}
}
for (let i = edges.length - 1; i >= 0; i--) {
if (nodeIdsToRemove.has(edges[i].target)) {
edges.splice(i, 1);
}
}
};

View File

@@ -3,6 +3,7 @@
"version": "1.0.0",
"dependencies": {
"@fastgpt/global": "workspace:*",
"@modelcontextprotocol/sdk": "^1.9.0",
"@node-rs/jieba": "2.0.1",
"@xmldom/xmldom": "^0.8.10",
"@zilliz/milvus2-sdk-node": "2.4.2",

View File

@@ -0,0 +1,58 @@
import {
TeamCollectionName,
TeamMemberCollectionName
} from '@fastgpt/global/support/user/team/constant';
import { Schema, getMongoModel } from '../../common/mongo';
import { McpKeyType } from '@fastgpt/global/support/mcp/type';
import { getNanoid } from '@fastgpt/global/common/string/tools';
import { AppCollectionName } from '../../core/app/schema';
export const mcpCollectionName = 'mcp_keys';
const McpKeySchema = new Schema({
name: {
type: String,
required: true
},
key: {
type: String,
required: true,
unique: true,
default: () => getNanoid(24)
},
teamId: {
type: Schema.Types.ObjectId,
ref: TeamCollectionName,
required: true
},
tmbId: {
type: Schema.Types.ObjectId,
ref: TeamMemberCollectionName,
required: true
},
apps: {
type: [
{
appId: {
type: Schema.Types.ObjectId,
ref: AppCollectionName,
required: true
},
toolName: {
type: String
},
description: {
type: String
}
}
],
default: []
}
});
try {
} catch (error) {
console.log(error);
}
export const MongoMcpKey = getMongoModel<McpKeyType>(mcpCollectionName, McpKeySchema);

View File

@@ -0,0 +1,45 @@
import { PermissionValueType } from '@fastgpt/global/support/permission/type';
import { AuthModeType, AuthResponseType } from '../type';
import { McpKeyType } from '@fastgpt/global/support/mcp/type';
import { authUserPer } from '../user/auth';
import { MongoMcpKey } from '../../mcp/schema';
import { CommonErrEnum } from '@fastgpt/global/common/error/code/common';
import { TeamErrEnum } from '@fastgpt/global/common/error/code/team';
export const authMcp = async ({
mcpId,
per,
...props
}: AuthModeType & {
mcpId: string;
per: PermissionValueType;
}): Promise<
AuthResponseType & {
mcp: McpKeyType;
}
> => {
const { userId, teamId, tmbId, permission, isRoot } = await authUserPer(props);
const mcp = await MongoMcpKey.findOne({ _id: mcpId }).lean();
if (!mcp) {
return Promise.reject(CommonErrEnum.invalidResource);
}
if (teamId !== String(mcp.teamId)) {
return Promise.reject(TeamErrEnum.unPermission);
}
if (!permission.hasManagePer && !isRoot && tmbId !== String(mcp.tmbId)) {
return Promise.reject(TeamErrEnum.unPermission);
}
return {
mcp,
userId,
teamId,
tmbId,
isRoot,
permission
};
};

View File

@@ -8,6 +8,8 @@ type Props = FlexProps & {
size?: string;
onClick?: () => void;
hoverColor?: string;
hoverBg?: string;
hoverBorderColor?: string;
tip?: string;
isLoading?: boolean;
};
@@ -16,6 +18,8 @@ const MyIconButton = ({
icon,
onClick,
hoverColor = 'primary.600',
hoverBg = 'myGray.05',
hoverBorderColor = '',
size = '1rem',
tip,
isLoading = false,
@@ -33,8 +37,9 @@ const MyIconButton = ({
transition={'background 0.1s'}
cursor={'pointer'}
_hover={{
bg: 'myGray.05',
color: hoverColor
bg: hoverBg,
color: hoverColor,
borderColor: hoverBorderColor
}}
onClick={() => {
if (isLoading) return;

View File

@@ -17,6 +17,7 @@ export const iconPaths = {
'common/addLight': () => import('./icons/common/addLight.svg'),
'common/addUser': () => import('./icons/common/addUser.svg'),
'common/administrator': () => import('./icons/common/administrator.svg'),
'common/app': () => import('./icons/common/app.svg'),
'common/arrowLeft': () => import('./icons/common/arrowLeft.svg'),
'common/arrowRight': () => import('./icons/common/arrowRight.svg'),
'common/backFill': () => import('./icons/common/backFill.svg'),
@@ -32,6 +33,7 @@ export const iconPaths = {
'common/courseLight': () => import('./icons/common/courseLight.svg'),
'common/customTitleLight': () => import('./icons/common/customTitleLight.svg'),
'common/data': () => import('./icons/common/data.svg'),
'common/detail': () => import('./icons/common/detail.svg'),
'common/dingtalkFill': () => import('./icons/common/dingtalkFill.svg'),
'common/disable': () => import('./icons/common/disable.svg'),
'common/downArrowFill': () => import('./icons/common/downArrowFill.svg'),
@@ -157,6 +159,8 @@ export const iconPaths = {
'core/app/type/httpPlugin': () => import('./icons/core/app/type/httpPlugin.svg'),
'core/app/type/httpPluginFill': () => import('./icons/core/app/type/httpPluginFill.svg'),
'core/app/type/jsonImport': () => import('./icons/core/app/type/jsonImport.svg'),
'core/app/type/mcpTools': () => import('./icons/core/app/type/mcpTools.svg'),
'core/app/type/mcpToolsFill': () => import('./icons/core/app/type/mcpToolsFill.svg'),
'core/app/type/plugin': () => import('./icons/core/app/type/plugin.svg'),
'core/app/type/pluginFill': () => import('./icons/core/app/type/pluginFill.svg'),
'core/app/type/pluginLight': () => import('./icons/core/app/type/pluginLight.svg'),
@@ -169,6 +173,7 @@ export const iconPaths = {
'core/app/variable/select': () => import('./icons/core/app/variable/select.svg'),
'core/app/variable/textarea': () => import('./icons/core/app/variable/textarea.svg'),
'core/chat/QGFill': () => import('./icons/core/chat/QGFill.svg'),
'core/chat/backText': () => import('./icons/core/chat/backText.svg'),
'core/chat/cancelSpeak': () => import('./icons/core/chat/cancelSpeak.svg'),
'core/chat/chatFill': () => import('./icons/core/chat/chatFill.svg'),
'core/chat/chatLight': () => import('./icons/core/chat/chatLight.svg'),
@@ -183,7 +188,6 @@ export const iconPaths = {
'core/chat/feedback/goodLight': () => import('./icons/core/chat/feedback/goodLight.svg'),
'core/chat/fileSelect': () => import('./icons/core/chat/fileSelect.svg'),
'core/chat/finishSpeak': () => import('./icons/core/chat/finishSpeak.svg'),
'core/chat/backText':() => import('./icons/core/chat/backText.svg'),
'core/chat/imgSelect': () => import('./icons/core/chat/imgSelect.svg'),
'core/chat/quoteFill': () => import('./icons/core/chat/quoteFill.svg'),
'core/chat/quoteSign': () => import('./icons/core/chat/quoteSign.svg'),
@@ -425,8 +429,8 @@ export const iconPaths = {
'phoneTabbar/toolFill': () => import('./icons/phoneTabbar/toolFill.svg'),
'plugins/dingding': () => import('./icons/plugins/dingding.svg'),
'plugins/doc2x': () => import('./icons/plugins/doc2x.svg'),
'plugins/qiwei': () => import('./icons/plugins/qiwei.svg'),
'plugins/email': () => import('./icons/plugins/email.svg'),
'plugins/qiwei': () => import('./icons/plugins/qiwei.svg'),
'plugins/textEditor': () => import('./icons/plugins/textEditor.svg'),
point: () => import('./icons/point.svg'),
preview: () => import('./icons/preview.svg'),

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" >
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.06677 2.8C2.06677 2.35817 2.42494 2 2.86677 2H6.60011C7.04193 2 7.40011 2.35817 7.40011 2.8V6.53333C7.40011 6.97516 7.04193 7.33333 6.60011 7.33333H2.86677C2.42494 7.33333 2.06677 6.97516 2.06677 6.53333V2.8ZM3.40011 6V3.33333H6.06677V6H3.40011Z" />
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.73344 2.8C8.73344 2.35817 9.09161 2 9.53344 2H13.2668C13.7086 2 14.0668 2.35817 14.0668 2.8V6.53333C14.0668 6.97516 13.7086 7.33333 13.2668 7.33333H9.53344C9.09161 7.33333 8.73344 6.97516 8.73344 6.53333V2.8ZM10.0668 6V3.33333H12.7334V6H10.0668Z" />
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.53344 8.66667C9.09161 8.66667 8.73344 9.02484 8.73344 9.46667V13.2C8.73344 13.6418 9.09161 14 9.53344 14H13.2668C13.7086 14 14.0668 13.6418 14.0668 13.2V9.46667C14.0668 9.02484 13.7086 8.66667 13.2668 8.66667H9.53344ZM10.0668 10V12.6667H12.7334V10H10.0668Z" />
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.06677 9.46667C2.06677 9.02484 2.42494 8.66667 2.86677 8.66667H6.60011C7.04193 8.66667 7.40011 9.02484 7.40011 9.46667V13.2C7.40011 13.6418 7.04193 14 6.60011 14H2.86677C2.42494 14 2.06677 13.6418 2.06677 13.2V9.46667ZM3.40011 12.6667V10H6.06677V12.6667H3.40011Z" />
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="none">
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.19888 4.05802C1.19888 3.68983 1.51616 3.39136 1.90755 3.39136H14.0925C14.4838 3.39136 14.8011 3.68983 14.8011 4.05802C14.8011 4.42621 14.4838 4.72469 14.0925 4.72469H1.90755C1.51616 4.72469 1.19888 4.42621 1.19888 4.05802ZM1.19888 7.95852C1.19888 7.59033 1.51616 7.29185 1.90755 7.29185H14.0925C14.4838 7.29185 14.8011 7.59033 14.8011 7.95852C14.8011 8.32671 14.4838 8.62518 14.0925 8.62518H1.90755C1.51616 8.62518 1.19888 8.32671 1.19888 7.95852ZM1.19888 11.942C1.19888 11.5738 1.51616 11.2753 1.90755 11.2753H9.7136C10.105 11.2753 10.4223 11.5738 10.4223 11.942C10.4223 12.3102 10.105 12.6087 9.7136 12.6087H1.90755C1.51616 12.6087 1.19888 12.3102 1.19888 11.942Z" />
</svg>

After

Width:  |  Height:  |  Size: 803 B

View File

@@ -0,0 +1,3 @@
<svg viewBox="0 0 12 13" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.01149 1.30106C7.46783 0.771565 6.58633 0.771547 6.04264 1.30107L0.792224 6.41449C0.610991 6.59101 0.317157 6.59101 0.135923 6.41449C-0.0453077 6.23798 -0.0453077 5.95182 0.135923 5.77531L5.38633 0.661891C6.29247 -0.220617 7.76166 -0.220641 8.6678 0.661882C9.20454 1.1846 9.42336 1.8997 9.32429 2.57927C10.0221 2.48276 10.7563 2.69587 11.293 3.21858L11.2933 3.21882L11.3203 3.24523C12.2265 4.12776 12.2265 5.55862 11.3203 6.44112L6.57187 11.0658C6.51148 11.1246 6.51145 11.2199 6.57184 11.2788L7.54692 12.2284C7.72813 12.4049 7.72813 12.6911 7.54689 12.8676C7.36565 13.0441 7.07182 13.0441 6.89058 12.8676L5.91557 11.918C5.4927 11.5061 5.4927 10.8384 5.91557 10.4266L10.6641 5.80193C11.2078 5.27243 11.2078 4.41392 10.6641 3.88442L10.6638 3.88417L10.6368 3.85779C10.0931 3.32835 9.21152 3.32829 8.6678 3.85779L4.75737 7.66624L4.75495 7.66859L4.70266 7.71948C4.52145 7.89599 4.2276 7.89599 4.04639 7.71948C3.86515 7.54297 3.86515 7.25681 4.04636 7.0803L8.01149 3.21861C8.55521 2.68911 8.55521 1.83057 8.01149 1.30106ZM7.35525 2.57943C7.53646 2.40292 7.53646 2.11676 7.35525 1.94024C7.17401 1.76374 6.88018 1.76374 6.69894 1.94024L2.81582 5.72203C1.90965 6.60456 1.90968 8.03539 2.81579 8.91795C3.72196 9.80042 5.19114 9.80042 6.09731 8.91795L9.98043 5.13613C10.1616 4.95962 10.1616 4.67346 9.98043 4.49695C9.79919 4.32043 9.50537 4.32046 9.32413 4.49695L5.44104 8.27876C4.89732 8.80824 4.01582 8.80824 3.47213 8.27879C2.92841 7.74923 2.92844 6.89072 3.47213 6.36122L7.35525 2.57943Z" />
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -0,0 +1,13 @@
<svg viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
<rect width="32" height="32" rx="4" fill="url(#paint0_linear_19272_45870)"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M19.2184 8.48172C18.3486 7.63453 16.9382 7.6345 16.0682 8.48174L7.66758 16.6632C7.37761 16.9456 6.90748 16.9456 6.6175 16.6632C6.32753 16.3808 6.32753 15.9229 6.6175 15.6405L15.0182 7.45905C16.468 6.04704 18.8187 6.047 20.2685 7.45904C21.1273 8.29538 21.4774 9.43954 21.3189 10.5269C22.4353 10.3724 23.6101 10.7134 24.4689 11.5498L24.4693 11.5501L24.5126 11.5924C25.9624 13.0044 25.9625 15.2938 24.5126 16.7058L16.915 24.1053C16.8184 24.1994 16.8183 24.3519 16.915 24.446L18.4751 25.9655C18.765 26.2479 18.765 26.7058 18.475 26.9882C18.1851 27.2706 17.7149 27.2706 17.425 26.9882L15.8649 25.4688C15.1883 24.8098 15.1883 23.7415 15.8649 23.0826L23.4625 15.6831C24.3324 14.8359 24.3325 13.4623 23.4625 12.6151L23.4621 12.6147L23.4189 12.5725C22.5489 11.7254 21.1384 11.7253 20.2685 12.5725L14.0118 18.666L14.0079 18.6698L13.9243 18.7512C13.6343 19.0336 13.1642 19.0336 12.8742 18.7512C12.5843 18.4688 12.5843 18.0109 12.8742 17.7285L19.2184 11.5498C20.0884 10.7026 20.0884 9.32893 19.2184 8.48172ZM18.1684 10.5271C18.4584 10.2447 18.4584 9.78683 18.1684 9.50442C17.8784 9.22201 17.4083 9.22201 17.1183 9.50442L10.9053 15.5553C9.45547 16.9673 9.45552 19.2566 10.9053 20.6687C12.3552 22.0807 14.7059 22.0807 16.1557 20.6687L22.3687 14.6178C22.6587 14.3354 22.6587 13.8776 22.3687 13.5951C22.0787 13.3127 21.6086 13.3128 21.3186 13.5951L15.1057 19.646C14.2357 20.4932 12.8253 20.4932 11.9554 19.6461C11.0855 18.7988 11.0855 17.4252 11.9554 16.578L18.1684 10.5271Z" fill="white"/>
<defs>
<linearGradient id="paint0_linear_19272_45870" x1="0" y1="32" x2="36.8" y2="8.8" gradientUnits="userSpaceOnUse">
<stop offset="0.201923" stop-color="#454D5D"/>
<stop offset="1" stop-color="#BAC2CE"/>
</linearGradient>
<clipPath id="clip0_19272_45870">
<rect width="22.4" height="22.4" fill="white" transform="translate(4.80005 4.80005)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -8,7 +8,7 @@ const SearchInput = (props: InputProps) => {
<InputLeftElement>
<MyIcon name="common/searchLight" w="16px" color={'myGray.500'} />
</InputLeftElement>
<Input fontSize="sm" bg={'myGray.50'} {...props} />
<Input fontSize="sm" bg={'myGray.25'} {...props} />
</InputGroup>
);
};

View File

@@ -74,6 +74,7 @@ const PopoverConfirm = ({
isLazy
lazyBehavior="keepMounted"
arrowSize={10}
strategy={'fixed'}
>
<PopoverTrigger>{Trigger}</PopoverTrigger>
<PopoverContent p={4}>

View File

@@ -0,0 +1,61 @@
import { useState, useRef, useCallback, useEffect } from 'react';
interface UseResizableOptions {
initialWidth?: number;
minWidth?: number;
maxWidth?: number;
}
export const useResizable = (options: UseResizableOptions = {}) => {
const { initialWidth = 300, minWidth = 200, maxWidth = 400 } = options;
const [width, setWidth] = useState(initialWidth);
const [isDragging, setIsDragging] = useState(false);
const startX = useRef(0);
const startWidth = useRef(0);
const handleMouseDown = useCallback(
(e: React.MouseEvent) => {
setIsDragging(true);
startX.current = e.clientX;
startWidth.current = width;
e.preventDefault();
},
[width]
);
const handleMouseMove = useCallback(
(e: MouseEvent) => {
if (!isDragging) return;
const diff = e.clientX - startX.current;
const newWidth = Math.min(Math.max(startWidth.current + diff, minWidth), maxWidth);
setWidth(newWidth);
},
[isDragging, minWidth, maxWidth]
);
const handleMouseUp = useCallback(() => {
setIsDragging(false);
}, []);
useEffect(() => {
if (isDragging) {
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
}
return () => {
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
};
}, [isDragging, handleMouseMove, handleMouseUp]);
return {
width,
isDragging,
handleMouseDown
};
};
export default useResizable;

View File

@@ -20,6 +20,7 @@
"generation_time": "Generation time",
"image_parse": "Image tagging",
"input_token_length": "input tokens",
"mcp": "MCP call",
"member": "member",
"member_name": "Member name",
"module_name": "module name",

View File

@@ -1,4 +1,9 @@
{
"MCP_tools_list_is_empty": "MCP tool not resolved",
"MCP_tools_parse_failed": "Failed to parse MCP address",
"MCP_tools_url": "MCP Address",
"MCP_tools_url_is_empty": "The MCP address cannot be empty",
"MCP_tools_url_placeholder": "After filling in the MCP address, click Analysis",
"Role_setting": "Permission",
"Run": "Execute",
"Team Tags Set": "Team tags",
@@ -98,6 +103,7 @@
"month.unit": "Day",
"move.hint": "After moving, the selected application/folder will inherit the permission settings of the new folder, and the original permission settings will become invalid.",
"move_app": "Move Application",
"no_mcp_tools_list": "No data yet, the MCP address needs to be parsed first",
"node_not_intro": "This node is not introduced",
"not_json_file": "Please select a JSON file",
"oaste_curl_string": "Enter CURL code",
@@ -158,6 +164,7 @@
"template_market_empty_data": "No suitable templates found",
"time_zone": "Time Zone",
"tool_input_param_tip": "This plugin requires configuration of related information to run properly.",
"tools_no_description": "This tool has not been introduced ~",
"transition_to_workflow": "Convert to Workflow",
"transition_to_workflow_create_new_placeholder": "Create a new app instead of modifying the current app",
"transition_to_workflow_create_new_tip": "Once converted to a workflow, it cannot be reverted to simple mode. Please confirm!",

View File

@@ -36,6 +36,10 @@
"Warning": "Warning",
"add_new": "Add New",
"add_new_param": "Add new param",
"app.templateMarket.templateTags.Image_generation": "Image generation",
"app.templateMarket.templateTags.Office_services": "Office Services",
"app.templateMarket.templateTags.Roleplay": "role play",
"app.templateMarket.templateTags.Web_search": "Search online",
"app.templateMarket.templateTags.Writing": "Writing",
"back": "Back",
"can_copy_content_tip": "It is not possible to copy automatically using the browser, please manually copy the following content",
@@ -97,7 +101,7 @@
"code_error.team_error.org_member_not_exist": "Organization member does not exist",
"code_error.team_error.org_not_exist": "Organization does not exist",
"code_error.team_error.org_parent_not_exist": "Parent organization does not exist",
"code_error.team_error.over_size": "error.team.overSize",
"code_error.team_error.over_size": "Team members exceed limit",
"code_error.team_error.plugin_amount_not_enough": "Plugin Limit Reached",
"code_error.team_error.re_rank_not_enough": "Search rearrangement cannot be used in the free version~",
"code_error.team_error.too_many_invitations": "You have reached the maximum number of active invitation links, please clean up some links first",
@@ -175,6 +179,7 @@
"common.Other": "Other",
"common.Output": "Output",
"common.Params": "Parameters",
"common.Parse": "Analysis",
"common.Password inconsistency": "Passwords Do Not Match",
"common.Permission": "Permission",
"common.Permission_tip": "Individual permissions are greater than group permissions",
@@ -371,6 +376,7 @@
"core.app.share.Is response quote": "Return Quote",
"core.app.share.Not share link": "No Share Link Created",
"core.app.share.Role check": "Identity Verification",
"core.app.switch_to_template_market": "Jump template market",
"core.app.tip.Add a intro to app": "Give the app an introduction",
"core.app.tip.chatNodeSystemPromptTip": "Enter a prompt here",
"core.app.tip.systemPromptTip": "Fixed guide words for the model. By adjusting this content, you can guide the model's chat direction. This content will be fixed at the beginning of the context. You can use / to insert variables.\nIf a Dataset is associated, you can also guide the model when to call the Dataset search by appropriate description. For example:\nYou are an assistant for the movie 'Interstellar'. When users ask about content related to 'Interstellar', please search the Dataset and answer based on the search results.",
@@ -444,6 +450,7 @@
"core.chat.logs.api": "API Call",
"core.chat.logs.feishu": "Feishu",
"core.chat.logs.free_login": "No login link",
"core.chat.logs.mcp": "MCP call",
"core.chat.logs.official_account": "Official Account",
"core.chat.logs.online": "Online Use",
"core.chat.logs.share": "External Link Call",
@@ -896,7 +903,9 @@
"error.username_empty": "Account cannot be empty",
"error_collection_not_exist": "The collection does not exist",
"error_embedding_not_config": "Unconfigured index model",
"error_invalid_resource": "Invalid resources",
"error_llm_not_config": "Unconfigured file understanding model",
"error_un_permission": "No permission to operate",
"error_vlm_not_config": "Image comprehension model not configured",
"extraction_results": "Extraction Results",
"field_name": "Field Name",
@@ -926,6 +935,7 @@
"llm_model_not_config": "No language model was detected",
"max_quote_tokens": "Quote cap",
"max_quote_tokens_tips": "The maximum number of tokens in a single search, about 1 character in Chinese = 1.7 tokens, and about 1 character in English = 1 token",
"mcp_server": "MCP Services",
"min_similarity": "lowest correlation",
"min_similarity_tip": "The relevance of different index models is different. Please select the appropriate value through search testing. \nWhen using Result Rearrange , use the rearranged results for filtering.",
"model.billing": "Billing",
@@ -1208,6 +1218,7 @@
"system.Help Document": "Help Document",
"tag_list": "Tag List",
"team_tag": "Team Tag",
"template_market": "Template Market",
"textarea_variable_picker_tip": "Enter \"/\" to select a variable",
"unauth_token": "The certificate has expired, please log in again",
"unit.character": "Character",

View File

@@ -0,0 +1,20 @@
{
"app_alias_name": "Tool name",
"app_description": "Application Description",
"app_name": "Application name",
"apps": "Exposed applications",
"create_mcp_server": "Create a new service",
"delete_mcp_server_confirm_tip": "Confirm to delete the service?",
"has_chosen": "Selected",
"manage_app": "manage",
"mcp_apps": "Number of associated applications",
"mcp_endpoints": "Access address",
"mcp_json_config": "Access script",
"mcp_name": "MCP service name",
"mcp_server": "MCP Services",
"mcp_server_description": "Allows you to select some applications to provide external use with the MCP protocol. \nDue to the immaturity of the MCP protocol, this feature is still in the beta stage.",
"search_app": "Search for apps",
"select_app": "Application selection",
"start_use": "Get started",
"usage_way": "MCP service usage"
}

View File

@@ -186,6 +186,7 @@
"tool_params.params_name": "Name",
"tool_params.params_name_placeholder": "name/age/sql",
"tool_params.tool_params_result": "Parameter configuration results",
"tool_raw_response_description": "The original response of the tool",
"trigger_after_application_completion": "Will be triggered after the application is fully completed",
"unFoldAll": "Expand all",
"update_link_error": "Error updating link",

View File

@@ -22,6 +22,7 @@
"generation_time": "生成时间",
"image_parse": "图片标注",
"input_token_length": "输入 tokens",
"mcp": "MCP 调用",
"member": "成员",
"member_name": "成员名",
"module_name": "模块名",

View File

@@ -1,4 +1,13 @@
{
"MCP_tools_debug": "调试",
"MCP_tools_detail": "查看详情",
"MCP_tools_list": "工具列表",
"MCP_tools_list_is_empty": "未解析到 MCP 工具",
"MCP_tools_list_with_number": "工具列表: {{total}}",
"MCP_tools_parse_failed": "解析 MCP 地址失败",
"MCP_tools_url": "MCP 地址",
"MCP_tools_url_is_empty": "MCP 地址不能为空",
"MCP_tools_url_placeholder": "填入 MCP 地址后,点击解析",
"Role_setting": "权限设置",
"Run": "运行",
"Team Tags Set": "团队标签",
@@ -98,6 +107,7 @@
"month.unit": "号",
"move.hint": "移动后,所选应用/文件夹将继承新文件夹的权限设置,原先的权限设置失效。",
"move_app": "移动应用",
"no_mcp_tools_list": "暂无数据,需先解析 MCP 地址",
"node_not_intro": "这个节点没有介绍",
"not_json_file": "请选择JSON文件",
"oaste_curl_string": "输入 CURL 代码",
@@ -123,6 +133,7 @@
"response_format": "回复格式",
"saved_success": "保存成功!如需在外部使用该版本,请点击“保存并发布”",
"search_app": "搜索应用",
"search_tool": "搜索工具",
"setting_app": "应用配置",
"setting_plugin": "插件配置",
"show_top_p_tip": "用温度采样的替代方法称为Nucleus采样该模型考虑了具有TOP_P概率质量质量的令牌的结果。因此0.1表示仅考虑包含最高概率质量的令牌。默认为 1。",
@@ -157,7 +168,9 @@
"template_market_description": "在模板市场探索更多玩法,配置教程与使用引导,带你理解并上手各种应用",
"template_market_empty_data": "找不到合适的模板",
"time_zone": "时区",
"tool_detail": "工具详情",
"tool_input_param_tip": "该插件正常运行需要配置相关信息",
"tools_no_description": "这个工具没有介绍~",
"transition_to_workflow": "转成工作流",
"transition_to_workflow_create_new_placeholder": "创建一个新的应用,而不是修改当前应用",
"transition_to_workflow_create_new_tip": "转化成工作流后,将无法转化回简易模式,请确认!",
@@ -166,6 +179,7 @@
"tts_close": "关闭",
"type.All": "全部",
"type.Create http plugin tip": "通过 OpenAPI Schema 批量创建插件,兼容 GPTs 格式",
"type.Create mcp tools tip": "通过输入 MCP 地址,自动解析并批量创建可调用的 MCP 工具",
"type.Create one plugin tip": "可以自定义输入和输出的工作流,通常用于封装重复使用的工作流",
"type.Create plugin bot": "创建插件",
"type.Create simple bot": "创建简易应用",
@@ -175,6 +189,8 @@
"type.Http plugin": "HTTP 插件",
"type.Import from json": "导入 JSON 配置",
"type.Import from json tip": "通过 JSON 配置文件,直接创建应用",
"type.MCP tools": "MCP 工具集",
"type.MCP_tools_url": "MCP 地址",
"type.Plugin": "插件",
"type.Simple bot": "简易应用",
"type.Workflow bot": "工作流",

View File

@@ -84,7 +84,7 @@
"code_error.plugin_error.not_exist": "插件不存在",
"code_error.plugin_error.un_auth": "无权操作该插件",
"code_error.system_error.community_version_num_limit": "超出开源版数量限制,请升级商业版: https://fastgpt.in",
"code_error.team_error.ai_points_not_enough": "",
"code_error.team_error.ai_points_not_enough": "AI 积分不足",
"code_error.team_error.app_amount_not_enough": "应用数量已达上限~",
"code_error.team_error.cannot_delete_default_group": "不能删除默认群组",
"code_error.team_error.cannot_delete_non_empty_org": "不能删除非空部门",
@@ -101,7 +101,7 @@
"code_error.team_error.org_member_not_exist": "部门成员不存在",
"code_error.team_error.org_not_exist": "部门不存在",
"code_error.team_error.org_parent_not_exist": "父部门不存在",
"code_error.team_error.over_size": "error.team.overSize",
"code_error.team_error.over_size": "团队成员超出限制",
"code_error.team_error.plugin_amount_not_enough": "插件数量已达上限~",
"code_error.team_error.re_rank_not_enough": "免费版无法使用检索重排~",
"code_error.team_error.too_many_invitations": "您的有效邀请链接数已达上限,请先清理链接",
@@ -179,6 +179,7 @@
"common.Other": "其他",
"common.Output": "输出",
"common.Params": "参数",
"common.Parse": "解析",
"common.Password inconsistency": "两次密码不一致",
"common.Permission": "权限",
"common.Permission_tip": "个人权限大于群组权限",
@@ -374,6 +375,7 @@
"core.app.share.Is response quote": "返回引用",
"core.app.share.Not share link": "没有创建分享链接",
"core.app.share.Role check": "身份校验",
"core.app.switch_to_template_market": "跳转模板市场",
"core.app.tip.Add a intro to app": "快来给应用一个介绍~",
"core.app.tip.chatNodeSystemPromptTip": "在此输入提示词",
"core.app.tip.systemPromptTip": "模型固定的引导词,通过调整该内容,可以引导模型聊天方向。该内容会被固定在上下文的开头。可通过输入 / 插入选择变量\n如果关联了知识库你还可以通过适当的描述来引导模型何时去调用知识库搜索。例如\n你是电影《星际穿越》的助手当用户询问与《星际穿越》相关的内容时请搜索知识库并结合搜索结果进行回答。",
@@ -447,6 +449,7 @@
"core.chat.logs.api": "API 调用",
"core.chat.logs.feishu": "飞书",
"core.chat.logs.free_login": "免登录链接",
"core.chat.logs.mcp": "MCP 调用",
"core.chat.logs.official_account": "公众号",
"core.chat.logs.online": "在线使用",
"core.chat.logs.share": "外部链接调用",
@@ -899,7 +902,9 @@
"error.username_empty": "账号不能为空",
"error_collection_not_exist": "集合不存在",
"error_embedding_not_config": "未配置索引模型",
"error_invalid_resource": "无效的资源",
"error_llm_not_config": "未配置文件理解模型",
"error_un_permission": "无权操作",
"error_vlm_not_config": "未配置图片理解模型",
"extraction_results": "提取结果",
"field_name": "字段名",
@@ -929,6 +934,7 @@
"llm_model_not_config": "检测到没有可用的语言模型",
"max_quote_tokens": "引用上限",
"max_quote_tokens_tips": "单次搜索最大的 token 数量,中文约 1 字=1.7 tokens英文约 1 字=1 token",
"mcp_server": "MCP 服务",
"min_similarity": "最低相关度",
"min_similarity_tip": "不同索引模型的相关度有区别,请通过搜索测试来选择合适的数值。使用 结果重排 时,使用重排结果进行过滤。",
"model.billing": "模型计费",
@@ -1211,6 +1217,7 @@
"system.Help Document": "帮助文档",
"tag_list": "标签列表",
"team_tag": "团队标签",
"template_market": "模板市场",
"textarea_variable_picker_tip": "输入\"/\"可选择变量",
"unauth_token": "凭证已过期,请重新登录",
"unit.character": "字符",

View File

@@ -0,0 +1,20 @@
{
"app_alias_name": "工具名",
"app_description": "应用描述",
"app_name": "应用名",
"apps": "暴露的应用",
"create_mcp_server": "新建服务",
"delete_mcp_server_confirm_tip": "确认删除该服务?",
"has_chosen": "已选择",
"manage_app": "管理",
"mcp_apps": "关联应用数量",
"mcp_endpoints": "接入地址",
"mcp_json_config": "接入脚本",
"mcp_name": "MCP 服务名",
"mcp_server": "MCP 服务",
"mcp_server_description": "允许你选择部分应用,以 MCP 的协议对外提供使用。由于 MCP 协议的不成熟,该功能仍处于测试阶段。",
"search_app": "搜索应用",
"select_app": "应用选择",
"start_use": "开始使用",
"usage_way": "MCP 服务使用"
}

View File

@@ -174,6 +174,7 @@
"text_content_extraction": "文本内容提取",
"text_to_extract": "需要提取的文本",
"these_variables_will_be_input_parameters_for_code_execution": "这些变量会作为代码的运行的输入参数",
"tool.tool_result": "工具运行结果",
"tool_call_termination": "工具调用终止",
"tool_custom_field": "自定义工具变量",
"tool_field": "工具参数配置",
@@ -186,6 +187,7 @@
"tool_params.params_name": "参数名",
"tool_params.params_name_placeholder": "name/age/sql",
"tool_params.tool_params_result": "参数配置结果",
"tool_raw_response_description": "工具的原始响应",
"trigger_after_application_completion": "将在应用完全结束后触发",
"unFoldAll": "全部展开",
"update_link_error": "更新链接异常",

View File

@@ -20,6 +20,7 @@
"generation_time": "生成時間",
"image_parse": "圖片標註",
"input_token_length": "輸入 tokens",
"mcp": "MCP 調用",
"member": "成員",
"member_name": "成員名",
"module_name": "模組名",

View File

@@ -1,4 +1,9 @@
{
"MCP_tools_list_is_empty": "未解析到 MCP 工具",
"MCP_tools_parse_failed": "解析 MCP 地址失敗",
"MCP_tools_url": "MCP 地址",
"MCP_tools_url_is_empty": "MCP 地址不能為空",
"MCP_tools_url_placeholder": "填入 MCP 地址後,點擊解析",
"Role_setting": "權限設定",
"Run": "執行",
"Team Tags Set": "團隊標籤",
@@ -98,6 +103,7 @@
"month.unit": "號",
"move.hint": "移動後,所選應用程式/資料夾將會繼承新資料夾的權限設定,原先的權限設定將會失效。",
"move_app": "移動應用程式",
"no_mcp_tools_list": "暫無數據,需先解析 MCP 地址",
"node_not_intro": "這個節點沒有介紹",
"not_json_file": "請選擇 JSON 檔案",
"oaste_curl_string": "輸入 CURL 代碼",
@@ -158,6 +164,7 @@
"template_market_empty_data": "找不到合適的範本",
"time_zone": "時區",
"tool_input_param_tip": "這個外掛正常執行需要設定相關資訊",
"tools_no_description": "這個工具沒有介紹~",
"transition_to_workflow": "轉換成工作流程",
"transition_to_workflow_create_new_placeholder": "建立新的應用程式,而不是修改目前應用程式",
"transition_to_workflow_create_new_tip": "轉換成工作流程後,將無法轉換回簡易模式,請確認!",

View File

@@ -36,6 +36,10 @@
"Warning": "警告",
"add_new": "新增",
"add_new_param": "新增參數",
"app.templateMarket.templateTags.Image_generation": "圖片生成",
"app.templateMarket.templateTags.Office_services": "辦公服務",
"app.templateMarket.templateTags.Roleplay": "角色扮演",
"app.templateMarket.templateTags.Web_search": "聯網搜索",
"app.templateMarket.templateTags.Writing": "文字創作",
"back": "返回",
"can_copy_content_tip": "無法使用瀏覽器自動複製,請手動複製下面內容",
@@ -96,7 +100,7 @@
"code_error.team_error.org_member_not_exist": "組織成員不存在",
"code_error.team_error.org_not_exist": "組織不存在",
"code_error.team_error.org_parent_not_exist": "父組織不存在",
"code_error.team_error.over_size": "error.team.overSize",
"code_error.team_error.over_size": "團隊成員超出限制",
"code_error.team_error.plugin_amount_not_enough": "已達外掛程式數量上限",
"code_error.team_error.re_rank_not_enough": "免費版無法使用檢索重排~",
"code_error.team_error.too_many_invitations": "您的有效邀請連結數已達上限,請先清理連結",
@@ -174,6 +178,7 @@
"common.Other": "其他",
"common.Output": "輸出",
"common.Params": "參數",
"common.Parse": "解析",
"common.Password inconsistency": "兩次密碼不一致",
"common.Permission": "權限",
"common.Permission_tip": "個人權限大於群組權限",
@@ -370,6 +375,7 @@
"core.app.share.Is response quote": "返回引用",
"core.app.share.Not share link": "尚未建立分享連結",
"core.app.share.Role check": "身份驗證",
"core.app.switch_to_template_market": "跳轉模板市場",
"core.app.tip.Add a intro to app": "快來為應用程式寫一個介紹",
"core.app.tip.chatNodeSystemPromptTip": "在此輸入提示詞",
"core.app.tip.systemPromptTip": "模型固定的引導詞,透過調整此內容,可以引導模型對話方向。此內容會固定在上下文的開頭。可透過輸入 / 插入變數。\n如果關聯了知識庫您還可以透過適當的描述引導模型何時去呼叫知識庫搜尋。例如\n您是電影《星際效應》的助手當使用者詢問與《星際效應》相關的內容時請搜尋知識庫並根據搜尋結果回答。",
@@ -443,6 +449,7 @@
"core.chat.logs.api": "API 呼叫",
"core.chat.logs.feishu": "飛書",
"core.chat.logs.free_login": "免登入連結",
"core.chat.logs.mcp": "MCP 調用",
"core.chat.logs.official_account": "官方帳號",
"core.chat.logs.online": "線上使用",
"core.chat.logs.share": "外部連結呼叫",
@@ -896,7 +903,9 @@
"error.username_empty": "帳號不能為空",
"error_collection_not_exist": "集合不存在",
"error_embedding_not_config": "未設定索引模型",
"error_invalid_resource": "無效的資源",
"error_llm_not_config": "未設定文件理解模型",
"error_un_permission": "無權操作",
"error_vlm_not_config": "未設定圖片理解模型",
"extraction_results": "提取結果",
"field_name": "欄位名稱",
@@ -926,6 +935,7 @@
"llm_model_not_config": "偵測到沒有可用的語言模型",
"max_quote_tokens": "引用上限",
"max_quote_tokens_tips": "單次搜尋最大的 token 數量,中文約 1 字=1.7 tokens英文約 1 字=1 token",
"mcp_server": "MCP 服務",
"min_similarity": "最低相關度",
"min_similarity_tip": "不同索引模型的相關度有區別,請透過搜尋測試來選擇合適的數值。\n使用 結果重排 時,使用重排結果過濾。",
"model.billing": "模型計費",
@@ -1207,6 +1217,7 @@
"system.Help Document": "說明文件",
"tag_list": "標籤列表",
"team_tag": "團隊標籤",
"template_market": "模板市場",
"textarea_variable_picker_tip": "輸入「/」以選擇變數",
"unauth_token": "憑證已過期,請重新登入",
"unit.character": "字元",

View File

@@ -0,0 +1,20 @@
{
"app_alias_name": "工具名",
"app_description": "應用描述",
"app_name": "應用名",
"apps": "暴露的應用",
"create_mcp_server": "新建服務",
"delete_mcp_server_confirm_tip": "確認刪除該服務?",
"has_chosen": "已選擇",
"manage_app": "管理",
"mcp_apps": "關聯應用數量",
"mcp_endpoints": "接入地址",
"mcp_json_config": "接入腳本",
"mcp_name": "MCP 服務名",
"mcp_server": "MCP 服務",
"mcp_server_description": "允許你選擇部分應用,以 MCP 的協議對外提供使用。\n由於 MCP 協議的不成熟,該功能仍處於測試階段。",
"search_app": "搜索應用",
"select_app": "應用選擇",
"start_use": "開始使用",
"usage_way": "MCP 服務使用"
}

View File

@@ -186,6 +186,7 @@
"tool_params.params_name": "參數名稱",
"tool_params.params_name_placeholder": "name/age/sql",
"tool_params.tool_params_result": "參數設定結果",
"tool_raw_response_description": "工具的原始響應",
"trigger_after_application_completion": "將會在應用程式完全結束後觸發",
"unFoldAll": "全部展開",
"update_link_error": "更新連結發生錯誤",

View File

@@ -19,6 +19,7 @@ import user from '../i18n/zh-CN/user.json';
import chat from '../i18n/zh-CN/chat.json';
import login from '../i18n/zh-CN/login.json';
import account_model from '../i18n/zh-CN/account_model.json';
import dashboard_mcp from '../i18n/zh-CN/dashboard_mcp.json';
export interface I18nNamespaces {
common: typeof common;
@@ -41,6 +42,7 @@ export interface I18nNamespaces {
account_team: typeof account_team;
account_thirdParty: typeof account_thirdParty;
account_model: typeof account_model;
dashboard_mcp: typeof dashboard_mcp;
}
export type I18nNsType = (keyof I18nNamespaces)[];
@@ -76,7 +78,8 @@ declare module 'i18next' {
'account_thirdParty',
'account',
'account_team',
'account_model'
'account_model',
'dashboard_mcp'
];
resources: I18nNamespaces;
}