From 14ddafc4614c3931e9c3a9477adab86247bcc41e Mon Sep 17 00:00:00 2001 From: Archer <545436317@qq.com> Date: Tue, 15 Apr 2025 15:29:40 +0800 Subject: [PATCH 01/12] 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 --- .../content/zh-cn/docs/development/sealos.md | 2 +- .../zh-cn/docs/development/upgrading/496.md | 22 + packages/global/core/app/constants.ts | 5 +- .../web/components/common/Icon/constants.ts | 3 +- .../common/Icon/icons/common/app.svg | 6 + .../common/Input/SearchInput/index.tsx | 2 +- packages/web/hooks/useResizable.tsx | 61 +++ packages/web/i18n/en/common.json | 5 + packages/web/i18n/zh-CN/common.json | 1 + packages/web/i18n/zh-Hant/common.json | 5 + .../src/components/Layout/WorkorderButton.tsx | 2 +- projects/app/src/components/Layout/navbar.tsx | 17 +- .../app/src/components/Layout/navbarPhone.tsx | 17 +- .../app/detail/Plugin/Header.tsx | 2 +- .../app/detail/SimpleApp/Header.tsx | 2 +- .../app/detail/Workflow/Header.tsx | 2 +- .../Flow/NodeTemplatesModal.tsx | 2 +- .../src/pageComponents/app/detail/context.tsx | 4 +- .../app/list/TemplateMarketModal.tsx | 481 ------------------ .../pageComponents/chat/ChatHistorySlider.tsx | 2 +- .../src/pageComponents/chat/SliderApps.tsx | 2 +- .../pageComponents/dashboard/Container.tsx | 327 ++++++++++++ .../SystemPlugin/ToolCard.tsx} | 0 .../list => dashboard/apps}/CreateModal.tsx | 22 +- .../apps}/HttpPluginEditModal.tsx | 0 .../apps}/JsonImportModal.tsx | 0 .../{app/list => dashboard/apps}/List.tsx | 9 +- .../{app/list => dashboard/apps}/TypeTag.tsx | 0 .../{app/list => dashboard/apps}/context.tsx | 8 +- .../login/LoginForm/FormLayout.tsx | 2 +- projects/app/src/pages/404.tsx | 2 +- projects/app/src/pages/_error.tsx | 2 +- projects/app/src/pages/chat/index.tsx | 4 +- .../pages/dashboard/[pluginGroupId]/index.tsx | 113 ++++ .../{app/list => dashboard/apps}/index.tsx | 140 ++--- .../pages/dashboard/templateMarket/index.tsx | 348 +++++++++++++ projects/app/src/pages/index.tsx | 2 +- projects/app/src/pages/login/fastlogin.tsx | 2 +- projects/app/src/pages/login/index.tsx | 6 +- projects/app/src/pages/login/provider.tsx | 6 +- projects/app/src/pages/toolkit/index.tsx | 226 -------- .../src/web/common/system/useSystemStore.ts | 2 +- 42 files changed, 993 insertions(+), 873 deletions(-) create mode 100644 docSite/content/zh-cn/docs/development/upgrading/496.md create mode 100644 packages/web/components/common/Icon/icons/common/app.svg create mode 100644 packages/web/hooks/useResizable.tsx delete mode 100644 projects/app/src/pageComponents/app/list/TemplateMarketModal.tsx create mode 100644 projects/app/src/pageComponents/dashboard/Container.tsx rename projects/app/src/pageComponents/{toolkit/PluginCard.tsx => dashboard/SystemPlugin/ToolCard.tsx} (100%) rename projects/app/src/pageComponents/{app/list => dashboard/apps}/CreateModal.tsx (96%) rename projects/app/src/pageComponents/{app/list => dashboard/apps}/HttpPluginEditModal.tsx (100%) rename projects/app/src/pageComponents/{app/list => dashboard/apps}/JsonImportModal.tsx (100%) rename projects/app/src/pageComponents/{app/list => dashboard/apps}/List.tsx (99%) rename projects/app/src/pageComponents/{app/list => dashboard/apps}/TypeTag.tsx (100%) rename projects/app/src/pageComponents/{app/list => dashboard/apps}/context.tsx (96%) create mode 100644 projects/app/src/pages/dashboard/[pluginGroupId]/index.tsx rename projects/app/src/pages/{app/list => dashboard/apps}/index.tsx (74%) create mode 100644 projects/app/src/pages/dashboard/templateMarket/index.tsx delete mode 100644 projects/app/src/pages/toolkit/index.tsx diff --git a/docSite/content/zh-cn/docs/development/sealos.md b/docSite/content/zh-cn/docs/development/sealos.md index cd8f6980e4ff..8e3028732e44 100644 --- a/docSite/content/zh-cn/docs/development/sealos.md +++ b/docSite/content/zh-cn/docs/development/sealos.md @@ -138,7 +138,7 @@ FastGPT 商业版共包含了2个应用(fastgpt, fastgpt-plus)和2个数据 SYSTEM_NAME=FastGPT SYSTEM_DESCRIPTION= SYSTEM_FAVICON=/favicon.ico -HOME_URL=/app/list +HOME_URL=/dashboard/apps ``` SYSTEM_FAVICON 可以是一个网络地址 diff --git a/docSite/content/zh-cn/docs/development/upgrading/496.md b/docSite/content/zh-cn/docs/development/upgrading/496.md new file mode 100644 index 000000000000..ed5553170cde --- /dev/null +++ b/docSite/content/zh-cn/docs/development/upgrading/496.md @@ -0,0 +1,22 @@ +--- +title: 'V4.9.6(进行中)' +description: 'FastGPT V4.9.6 更新说明' +icon: 'upgrade' +draft: false +toc: true +weight: 794 +--- + + + +## 🚀 新增内容 + +1. 增加工作台二级菜单,合并工具箱。 +2. 增加 grok3、GPT4.1、Gemini2.5 模型系统配置。 + +## ⚙️ 优化 + + + +## 🐛 修复 + diff --git a/packages/global/core/app/constants.ts b/packages/global/core/app/constants.ts index f24a194aef64..ab18297d5286 100644 --- a/packages/global/core/app/constants.ts +++ b/packages/global/core/app/constants.ts @@ -53,7 +53,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; diff --git a/packages/web/components/common/Icon/constants.ts b/packages/web/components/common/Icon/constants.ts index 50734b3e68d3..16e1bf32736a 100644 --- a/packages/web/components/common/Icon/constants.ts +++ b/packages/web/components/common/Icon/constants.ts @@ -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'), @@ -425,8 +426,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'), diff --git a/packages/web/components/common/Icon/icons/common/app.svg b/packages/web/components/common/Icon/icons/common/app.svg new file mode 100644 index 000000000000..168eec2bfa97 --- /dev/null +++ b/packages/web/components/common/Icon/icons/common/app.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/packages/web/components/common/Input/SearchInput/index.tsx b/packages/web/components/common/Input/SearchInput/index.tsx index 10603f091b45..519caeb925d1 100644 --- a/packages/web/components/common/Input/SearchInput/index.tsx +++ b/packages/web/components/common/Input/SearchInput/index.tsx @@ -8,7 +8,7 @@ const SearchInput = (props: InputProps) => { - + ); }; diff --git a/packages/web/hooks/useResizable.tsx b/packages/web/hooks/useResizable.tsx new file mode 100644 index 000000000000..b9c0224dcc32 --- /dev/null +++ b/packages/web/hooks/useResizable.tsx @@ -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; diff --git a/packages/web/i18n/en/common.json b/packages/web/i18n/en/common.json index 3498a85496b8..33947f429ff7 100644 --- a/packages/web/i18n/en/common.json +++ b/packages/web/i18n/en/common.json @@ -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", @@ -371,6 +375,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.", diff --git a/packages/web/i18n/zh-CN/common.json b/packages/web/i18n/zh-CN/common.json index 1f68a9b57e5c..9520845f0302 100644 --- a/packages/web/i18n/zh-CN/common.json +++ b/packages/web/i18n/zh-CN/common.json @@ -374,6 +374,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你是电影《星际穿越》的助手,当用户询问与《星际穿越》相关的内容时,请搜索知识库并结合搜索结果进行回答。", diff --git a/packages/web/i18n/zh-Hant/common.json b/packages/web/i18n/zh-Hant/common.json index 34c2d9fa1e7d..0c5b121c7281 100644 --- a/packages/web/i18n/zh-Hant/common.json +++ b/packages/web/i18n/zh-Hant/common.json @@ -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": "無法使用瀏覽器自動複製,請手動複製下面內容", @@ -370,6 +374,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您是電影《星際效應》的助手,當使用者詢問與《星際效應》相關的內容時,請搜尋知識庫並根據搜尋結果回答。", diff --git a/projects/app/src/components/Layout/WorkorderButton.tsx b/projects/app/src/components/Layout/WorkorderButton.tsx index b0445ea92ccb..582f14981bb6 100644 --- a/projects/app/src/components/Layout/WorkorderButton.tsx +++ b/projects/app/src/components/Layout/WorkorderButton.tsx @@ -12,7 +12,7 @@ import { useRouter } from 'next/router'; import { useMemo } from 'react'; const WorkOrderShowRouter: { [key: string]: boolean } = { - '/app/list': true, + '/dashboard/apps': true, '/dataset/list': true, '/toolkit': true }; diff --git a/projects/app/src/components/Layout/navbar.tsx b/projects/app/src/components/Layout/navbar.tsx index e9b15acbafe0..639cbc8b7a38 100644 --- a/projects/app/src/components/Layout/navbar.tsx +++ b/projects/app/src/components/Layout/navbar.tsx @@ -55,8 +55,13 @@ const Navbar = ({ unread }: { unread: number }) => { label: t('common:navbar.Studio'), icon: 'core/app/aiLight', activeIcon: 'core/app/aiFill', - link: `/app/list`, - activeLink: ['/app/list', '/app/detail'] + link: `/dashboard/apps`, + activeLink: [ + '/dashboard/apps', + '/app/detail', + '/dashboard/templateMarket', + '/dashboard/[pluginGroupId]' + ] }, { label: t('common:navbar.Datasets'), @@ -65,13 +70,6 @@ const Navbar = ({ unread }: { unread: number }) => { link: `/dataset/list`, activeLink: ['/dataset/list', '/dataset/detail'] }, - { - label: t('common:navbar.Toolkit'), - icon: 'phoneTabbar/tool', - activeIcon: 'phoneTabbar/toolFill', - link: `/toolkit`, - activeLink: ['/toolkit'] - }, { label: t('common:navbar.Account'), icon: 'support/user/userLight', @@ -125,6 +123,7 @@ const Navbar = ({ unread }: { unread: number }) => { {navbarList.map((item) => { const isActive = item.activeLink.includes(router.pathname); + return ( { label: t('common:navbar.Studio'), icon: 'core/app/aiLight', activeIcon: 'core/app/aiFill', - link: `/app/list`, - activeLink: ['/app/list', '/app/detail'], + link: `/dashboard/apps`, + activeLink: [ + '/dashboard/apps', + '/app/detail', + '/dashboard/templateMarket', + '/dashboard/[pluginGroupId]' + ], unread: 0 }, { @@ -36,14 +41,6 @@ const NavbarPhone = ({ unread }: { unread: number }) => { activeLink: ['/dataset/list', '/dataset/detail'], unread: 0 }, - { - label: t('common:navbar.Toolkit'), - icon: 'phoneTabbar/tool', - activeIcon: 'phoneTabbar/toolFill', - link: `/toolkit`, - activeLink: ['/toolkit'], - unread: 0 - }, { label: t('common:navbar.Account'), icon: 'support/user/userLight', diff --git a/projects/app/src/pageComponents/app/detail/Plugin/Header.tsx b/projects/app/src/pageComponents/app/detail/Plugin/Header.tsx index b9cc86898e1b..9995281ef3f7 100644 --- a/projects/app/src/pageComponents/app/detail/Plugin/Header.tsx +++ b/projects/app/src/pageComponents/app/detail/Plugin/Header.tsx @@ -103,7 +103,7 @@ const Header = () => { const onBack = useCallback(async () => { leaveSaveSign.current = false; router.push({ - pathname: '/app/list', + pathname: '/dashboard/apps', query: { parentId: appDetail.parentId, type: lastAppListRouteType diff --git a/projects/app/src/pageComponents/app/detail/SimpleApp/Header.tsx b/projects/app/src/pageComponents/app/detail/SimpleApp/Header.tsx index c7121764505f..026677f0edf1 100644 --- a/projects/app/src/pageComponents/app/detail/SimpleApp/Header.tsx +++ b/projects/app/src/pageComponents/app/detail/SimpleApp/Header.tsx @@ -71,7 +71,7 @@ const Header = ({ const onClickRoute = useCallback( (parentId: string) => { router.push({ - pathname: '/app/list', + pathname: '/dashboard/apps', query: { parentId, type: lastAppListRouteType diff --git a/projects/app/src/pageComponents/app/detail/Workflow/Header.tsx b/projects/app/src/pageComponents/app/detail/Workflow/Header.tsx index 0615b085fc2c..d4530f099556 100644 --- a/projects/app/src/pageComponents/app/detail/Workflow/Header.tsx +++ b/projects/app/src/pageComponents/app/detail/Workflow/Header.tsx @@ -107,7 +107,7 @@ const Header = () => { const onBack = useCallback(async () => { leaveSaveSign.current = false; router.push({ - pathname: '/app/list', + pathname: '/dashboard/apps', query: { parentId: appDetail.parentId, type: lastAppListRouteType diff --git a/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/NodeTemplatesModal.tsx b/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/NodeTemplatesModal.tsx index ac1dd1046ea5..4775c3cba452 100644 --- a/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/NodeTemplatesModal.tsx +++ b/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/NodeTemplatesModal.tsx @@ -364,7 +364,7 @@ const RenderHeader = React.memo(function RenderHeader({ color: 'primary.600' }} fontSize={'sm'} - onClick={() => router.push('/app/list')} + onClick={() => router.push('/dashboard/apps')} gap={1} > {t('common:create')} diff --git a/projects/app/src/pageComponents/app/detail/context.tsx b/projects/app/src/pageComponents/app/detail/context.tsx index 797cbd5aeb3e..4e9f9b4ab600 100644 --- a/projects/app/src/pageComponents/app/detail/context.tsx +++ b/projects/app/src/pageComponents/app/detail/context.tsx @@ -137,7 +137,7 @@ const AppContextProvider = ({ children }: { children: ReactNode }) => { refreshDeps: [appId], errorToast: t('common:core.app.error.Get app failed'), onError(err: any) { - router.replace('/app/list'); + router.replace('/dashboard/apps'); }, onSuccess(res) { setAppDetail(res); @@ -189,7 +189,7 @@ const AppContextProvider = ({ children }: { children: ReactNode }) => { }, { onSuccess() { - router.replace(`/app/list`); + router.replace(`/dashboard/apps`); }, successToast: t('common:common.Delete Success'), errorToast: t('common:common.Delete Failed') diff --git a/projects/app/src/pageComponents/app/list/TemplateMarketModal.tsx b/projects/app/src/pageComponents/app/list/TemplateMarketModal.tsx deleted file mode 100644 index 9b2553ad3098..000000000000 --- a/projects/app/src/pageComponents/app/list/TemplateMarketModal.tsx +++ /dev/null @@ -1,481 +0,0 @@ -import { - Box, - Button, - Flex, - Grid, - HStack, - Modal, - ModalBody, - ModalCloseButton, - ModalContent, - ModalHeader, - ModalOverlay -} from '@chakra-ui/react'; -import Avatar from '@fastgpt/web/components/common/Avatar'; -import { useCallback, useMemo, useState } from 'react'; -import MyBox from '@fastgpt/web/components/common/MyBox'; -import AppTypeTag from './TypeTag'; -import { AppTemplateTypeEnum, AppTypeEnum } from '@fastgpt/global/core/app/constants'; -import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; -import { - getTemplateMarketItemDetail, - getTemplateMarketItemList, - getTemplateTagList -} from '@/web/core/app/api/template'; -import { postCreateApp } from '@/web/core/app/api'; -import { useContextSelector } from 'use-context-selector'; -import { AppListContext } from './context'; -import { useRouter } from 'next/router'; -import MySelect from '@fastgpt/web/components/common/MySelect'; -import { useTranslation } from 'next-i18next'; -import { useSystem } from '@fastgpt/web/hooks/useSystem'; -import EmptyTip from '@fastgpt/web/components/common/EmptyTip'; -import SearchInput from '@fastgpt/web/components/common/Input/SearchInput/index'; -import MyIcon from '@fastgpt/web/components/common/Icon'; -import { useSystemStore } from '@/web/common/system/useSystemStore'; -import { webPushTrack } from '@/web/common/middle/tracks/utils'; -import { AppTemplateSchemaType, TemplateTypeSchemaType } from '@fastgpt/global/core/app/type'; -import { i18nT } from '@fastgpt/web/i18n/utils'; -import UseGuideModal from '@/components/common/Modal/UseGuideModal'; -import { form2AppWorkflow } from '@/web/core/app/utils'; - -type TemplateAppType = AppTypeEnum | 'all'; - -const recommendTag: TemplateTypeSchemaType = { - typeId: AppTemplateTypeEnum.recommendation, - typeName: i18nT('app:templateMarket.templateTags.Recommendation'), - typeOrder: 0 -}; - -const TemplateMarketModal = ({ - defaultType = 'all', - onClose -}: { - defaultType?: TemplateAppType; - onClose: () => void; -}) => { - const { t } = useTranslation(); - const { feConfigs } = useSystemStore(); - - const { parentId } = useContextSelector(AppListContext, (v) => v); - const router = useRouter(); - const { isPc } = useSystem(); - - const [currentTag, setCurrentTag] = useState(AppTemplateTypeEnum.recommendation); - const [currentAppType, setCurrentAppType] = useState(defaultType); - const [currentSearch, setCurrentSearch] = useState(''); - - const { data: templateList = [], loading: isLoadingTemplates } = useRequest2( - () => getTemplateMarketItemList({ type: currentAppType }), - { - manual: false, - refreshDeps: [currentAppType] - } - ); - - const { data: templateTags = [], loading: isLoadingTags } = useRequest2( - () => getTemplateTagList().then((res) => [recommendTag, ...res]), - { - manual: false - } - ); - // Batch by tags - const filterTemplateTags = useMemo(() => { - return templateTags - .map((tag) => { - const templates = templateList.filter((template) => template.tags.includes(tag.typeId)); - return { - ...tag, - templates - }; - }) - .filter((item) => item.templates.length > 0); - }, [templateList, templateTags]); - - const { runAsync: onUseTemplate, loading: isCreating } = useRequest2( - async (template: AppTemplateSchemaType) => { - const templateDetail = await getTemplateMarketItemDetail(template.templateId); - - return postCreateApp({ - parentId, - avatar: template.avatar, - name: template.name, - type: template.type as AppTypeEnum, - modules: templateDetail.workflow.nodes || [], - edges: templateDetail.workflow.edges || [], - chatConfig: templateDetail.workflow.chatConfig - }).then((res) => { - webPushTrack.useAppTemplate({ - id: res, - name: template.name - }); - - return res; - }); - }, - { - onSuccess(id: string) { - onClose(); - router.push(`/app/detail?appId=${id}`); - }, - successToast: t('common:common.Create Success'), - errorToast: t('common:common.Create Failed') - } - ); - - const { run: handleScroll } = useRequest2( - async () => { - let firstVisibleTitle: any = null; - - filterTemplateTags - .map((type) => type.typeId) - .forEach((type) => { - const element = document.getElementById(type); - if (!element) return; - - const elementRect = element.getBoundingClientRect(); - if (elementRect.top <= window.innerHeight && elementRect.bottom >= 0) { - if ( - !firstVisibleTitle || - elementRect.top < firstVisibleTitle.getBoundingClientRect().top - ) { - firstVisibleTitle = element; - } - } - }); - - if (firstVisibleTitle) { - setCurrentTag(firstVisibleTitle.id); - } - }, - { - throttleWait: 100, - refreshDeps: [filterTemplateTags.length] - } - ); - - const TemplateCard = useCallback( - ({ item }: { item: AppTemplateSchemaType }) => { - const { t } = useTranslation(); - - return ( - - - - - {item.name} - - - - - - - {item.intro || t('app:templateMarket.no_intro')} - - - - {`by ${item.author || feConfigs.systemTitle}`} - - {((item.userGuide?.type === 'markdown' && item.userGuide?.content) || - (item.userGuide?.type === 'link' && item.userGuide?.link)) && ( - - {({ onClick }) => ( - - )} - - )} - - - - - ); - }, - [feConfigs.systemTitle, onUseTemplate] - ); - - const isLoading = isLoadingTags || isLoadingTemplates || isCreating; - - return ( - - - - - - {t('app:template_market')} - - - - - h={'8'} - value={currentAppType} - onChange={(value) => { - setCurrentAppType(value); - }} - bg={'myGray.100'} - minW={'7rem'} - borderRadius={'sm'} - list={[ - { label: t('app:type.All'), value: 'all' }, - { label: t('app:type.Simple bot'), value: AppTypeEnum.simple }, - { label: t('app:type.Workflow bot'), value: AppTypeEnum.workflow }, - { label: t('app:type.Plugin'), value: AppTypeEnum.plugin } - ]} - /> - - - {isPc && ( - - setCurrentSearch(e.target.value)} - h={8} - bg={'myGray.50'} - maxLength={100} - borderRadius={'sm'} - /> - - )} - - - - {isPc && ( - - {filterTemplateTags.map((item) => { - return ( - { - setCurrentTag(item.typeId); - const anchor = document.getElementById(item.typeId); - if (anchor) { - anchor.scrollIntoView({ behavior: 'auto', block: 'start' }); - } - }} - > - {t(item.typeName as any)} - - ); - })} - - - {feConfigs?.appTemplateCourse && ( - window.open(feConfigs.appTemplateCourse)} - gap={1} - > - - {t('common:contribute_app_template')} - - )} - - )} - - - {currentSearch ? ( - <> - - {t('common:xx_search_result', { key: currentSearch })} - - {(() => { - const templates = templateList.filter((template) => - `${template.name}${template.intro}`.includes(currentSearch) - ); - - if (templates.length > 0) { - return ( - - {templates.map((item) => ( - - ))} - - ); - } - - return ; - })()} - - ) : ( - <> - {filterTemplateTags.map((item) => { - return ( - - - {t(item.typeName as any)} - - - {item.templates.map((item) => ( - - ))} - - - ); - })} - - )} - - - - - - ); -}; - -export default TemplateMarketModal; diff --git a/projects/app/src/pageComponents/chat/ChatHistorySlider.tsx b/projects/app/src/pageComponents/chat/ChatHistorySlider.tsx index b0ba3aba8d6d..c84f5936a3f9 100644 --- a/projects/app/src/pageComponents/chat/ChatHistorySlider.tsx +++ b/projects/app/src/pageComponents/chat/ChatHistorySlider.tsx @@ -299,7 +299,7 @@ const ChatHistorySlider = ({ confirmClearText }: { confirmClearText: string }) = alignItems={'center'} cursor={'pointer'} p={3} - onClick={() => router.push('/app/list')} + onClick={() => router.push('/dashboard/apps')} > router.push('/app/list')} + onClick={() => router.push('/dashboard/apps')} > React.ReactNode; +}) => { + const router = useRouter(); + const { t } = useTranslation(); + const { isPc } = useSystem(); + const { feConfigs } = useSystemStore(); + + const { isOpen: isOpenSidebar, onOpen: onOpenSidebar, onClose: onCloseSidebar } = useDisclosure(); + + // First tab + const currentTab = useMemo(() => { + const path = router.asPath.split('?')[0]; // 移除查询参数 + const segments = path.split('/').filter(Boolean); // 过滤空字符串 + + return (segments.pop() as TabEnumType) || TabEnum.apps; + }, [router.asPath]); + + // Sub tab + const { type: currentType, appType } = router.query as { + type: string; + appType?: AppTypeEnum | 'all'; + }; + + // Template market + const { data: templateTags = [], loading: isLoadingTemplatesTags } = useRequest2( + () => + currentTab === TabEnum.app_templates + ? getTemplateTagList().then((res) => [ + { + typeId: AppTemplateTypeEnum.recommendation, + typeName: t('app:templateMarket.templateTags.Recommendation'), + typeOrder: 0 + }, + ...res + ]) + : Promise.resolve([]), + { + manual: false, + refreshDeps: [currentTab] + } + ); + const { data: templateList = [], loading: isLoadingTemplates } = useRequest2( + () => + currentTab === TabEnum.app_templates + ? getTemplateMarketItemList({ type: appType }) + : Promise.resolve([]), + { + manual: false, + refreshDeps: [currentTab, appType] + } + ); + + // System tools + const { data: pluginGroups = [], loading: isLoadingToolGroups } = useRequest2( + () => + getPluginGroups().then((res) => + res.map((item) => ({ + ...item, + groupTypes: [ + { + typeId: 'all', + typeName: t('app:type.All') + }, + ...item.groupTypes + ] + })) + ), + { + manual: false + } + ); + + const groupList = useMemo< + { + groupId: string; + groupAvatar: string; + groupName: string; + children: { + typeId: string; + typeName: string; + isActive?: boolean; + onClick?: () => void; + }[]; + }[] + >(() => { + return [ + { + groupId: TabEnum.apps, + groupAvatar: 'common/app', + groupName: t('common:core.module.template.Team app'), + children: [ + { + isActive: !currentType, + typeId: 'all', + typeName: t('app:type.All') + }, + { + typeId: AppTypeEnum.simple, + typeName: t('app:type.Simple bot') + }, + { + typeId: AppTypeEnum.workflow, + typeName: t('app:type.Workflow bot') + }, + { + typeId: AppTypeEnum.plugin, + typeName: t('app:type.Plugin') + } + ] + }, + ...pluginGroups.map((group) => ({ + groupId: group.groupId, + groupAvatar: group.groupAvatar, + groupName: t(group.groupName as any), + children: group.groupTypes.map((type, index) => ({ + typeId: type.typeId, + typeName: t(type.typeName as any), + isActive: index === 0 && !currentType + })) + })), + { + groupId: TabEnum.app_templates, + groupAvatar: 'common/templateMarket', + groupName: t('app:template_market'), + children: [ + ...templateTags + .map((tag) => { + const templates = templateList.filter((template) => + template.tags.includes(tag.typeId) + ); + return { + ...tag, + templates + }; + }) + .filter((tag) => tag.templates.length > 0) + .map((tag, index) => ({ + typeId: tag.typeId, + typeName: t(tag.typeName as any), + isActive: index === 0 && !currentType + })), + ...(feConfigs?.appTemplateCourse + ? [ + { + typeId: AppTemplateTypeEnum.contribute, + typeName: t('common:contribute_app_template'), + onClick: () => { + window.open(feConfigs.appTemplateCourse); + } + } + ] + : []) + ] + } + ]; + }, [currentType, feConfigs.appTemplateCourse, pluginGroups, t, templateList, templateTags]); + + const MenuIcon = useMemo( + () => ( + + {isOpenSidebar && ( + + )} + + + ), + [isOpenSidebar, onCloseSidebar, onOpenSidebar] + ); + + const isLoading = isLoadingTemplatesTags || isLoadingTemplates || isLoadingToolGroups; + + return ( + + {/* Side bar */} + {(isPc || isOpenSidebar) && ( + + {groupList.map((group) => { + const selected = currentTab === group.groupId; + + return ( + + { + router.push(`/dashboard/${group.groupId}`); + onCloseSidebar(); + }} + > + + + {group.groupName} + + + + + {selected && ( + + {group.children.map((child) => { + const isActive = child.isActive || child.typeId === currentType; + + return ( + { + if (child.onClick) { + child.onClick(); + } else { + router.push({ + query: { + ...router.query, + type: child.typeId + } + }); + onCloseSidebar(); + } + }} + > + {child.typeName} + + ); + })} + + )} + + ); + })} + + )} + + + {children({ + templateTags, + templateList, + pluginGroups, + MenuIcon + })} + + + ); +}; + +export default DashboardContainer; diff --git a/projects/app/src/pageComponents/toolkit/PluginCard.tsx b/projects/app/src/pageComponents/dashboard/SystemPlugin/ToolCard.tsx similarity index 100% rename from projects/app/src/pageComponents/toolkit/PluginCard.tsx rename to projects/app/src/pageComponents/dashboard/SystemPlugin/ToolCard.tsx diff --git a/projects/app/src/pageComponents/app/list/CreateModal.tsx b/projects/app/src/pageComponents/dashboard/apps/CreateModal.tsx similarity index 96% rename from projects/app/src/pageComponents/app/list/CreateModal.tsx rename to projects/app/src/pageComponents/dashboard/apps/CreateModal.tsx index 3424ca153804..f10111ee9036 100644 --- a/projects/app/src/pageComponents/app/list/CreateModal.tsx +++ b/projects/app/src/pageComponents/dashboard/apps/CreateModal.tsx @@ -42,15 +42,7 @@ type FormType = { export type CreateAppType = AppTypeEnum.simple | AppTypeEnum.workflow | AppTypeEnum.plugin; -const CreateModal = ({ - onClose, - type, - onOpenTemplateModal -}: { - type: CreateAppType; - onClose: () => void; - onOpenTemplateModal: (type: AppTypeEnum) => void; -}) => { +const CreateModal = ({ onClose, type }: { type: CreateAppType; onClose: () => void }) => { const { t } = useTranslation(); const router = useRouter(); const { parentId, loadMyApps } = useContextSelector(AppListContext, (v) => v); @@ -194,14 +186,22 @@ const CreateModal = ({ {isTemplateMode && ( onOpenTemplateModal(type)} + onClick={() => { + router.push({ + pathname: '/dashboard/templateMarket', + query: { + appType: type + } + }); + onClose(); + }} alignItems={'center'} cursor={'pointer'} color={'myGray.600'} fontSize={'xs'} _hover={{ color: 'blue.700' }} > - {t('common:core.app.more')} + {t('common:core.app.switch_to_template_market')} )} diff --git a/projects/app/src/pageComponents/app/list/HttpPluginEditModal.tsx b/projects/app/src/pageComponents/dashboard/apps/HttpPluginEditModal.tsx similarity index 100% rename from projects/app/src/pageComponents/app/list/HttpPluginEditModal.tsx rename to projects/app/src/pageComponents/dashboard/apps/HttpPluginEditModal.tsx diff --git a/projects/app/src/pageComponents/app/list/JsonImportModal.tsx b/projects/app/src/pageComponents/dashboard/apps/JsonImportModal.tsx similarity index 100% rename from projects/app/src/pageComponents/app/list/JsonImportModal.tsx rename to projects/app/src/pageComponents/dashboard/apps/JsonImportModal.tsx diff --git a/projects/app/src/pageComponents/app/list/List.tsx b/projects/app/src/pageComponents/dashboard/apps/List.tsx similarity index 99% rename from projects/app/src/pageComponents/app/list/List.tsx rename to projects/app/src/pageComponents/dashboard/apps/List.tsx index a0f10fd5cbcd..2b35894c652e 100644 --- a/projects/app/src/pageComponents/app/list/List.tsx +++ b/projects/app/src/pageComponents/dashboard/apps/List.tsx @@ -51,8 +51,10 @@ const ListItem = () => { content: t('app:move.hint') }); - const { myApps, loadMyApps, onUpdateApp, setMoveAppId, folderDetail, setSearchKey } = - useContextSelector(AppListContext, (v) => v); + const { myApps, loadMyApps, onUpdateApp, setMoveAppId, folderDetail } = useContextSelector( + AppListContext, + (v) => v + ); const [editedApp, setEditedApp] = useState(); const [editHttpPlugin, setEditHttpPlugin] = useState(); @@ -132,7 +134,7 @@ const ListItem = () => { gridTemplateColumns={ folderDetail ? ['1fr', 'repeat(2,1fr)', 'repeat(2,1fr)', 'repeat(3,1fr)'] - : ['1fr', 'repeat(2,1fr)', 'repeat(3,1fr)', 'repeat(3,1fr)', 'repeat(4,1fr)'] + : ['1fr', 'repeat(2,1fr)', 'repeat(2,1fr)', 'repeat(3,1fr)', 'repeat(4,1fr)'] } gridGap={5} alignItems={'stretch'} @@ -176,7 +178,6 @@ const ListItem = () => { }} onClick={() => { if (AppFolderTypeList.includes(app.type)) { - setSearchKey(''); router.push({ query: { ...router.query, diff --git a/projects/app/src/pageComponents/app/list/TypeTag.tsx b/projects/app/src/pageComponents/dashboard/apps/TypeTag.tsx similarity index 100% rename from projects/app/src/pageComponents/app/list/TypeTag.tsx rename to projects/app/src/pageComponents/dashboard/apps/TypeTag.tsx diff --git a/projects/app/src/pageComponents/app/list/context.tsx b/projects/app/src/pageComponents/dashboard/apps/context.tsx similarity index 96% rename from projects/app/src/pageComponents/app/list/context.tsx rename to projects/app/src/pageComponents/dashboard/apps/context.tsx index 8147a4551195..2a83f15eafc8 100644 --- a/projects/app/src/pageComponents/app/list/context.tsx +++ b/projects/app/src/pageComponents/dashboard/apps/context.tsx @@ -19,7 +19,7 @@ const MoveModal = dynamic(() => import('@/components/common/folder/MoveModal')); type AppListContextType = { parentId?: string | null; - appType: AppTypeEnum | 'ALL'; + appType: AppTypeEnum | 'all'; myApps: AppListItemType[]; loadMyApps: () => Promise; isFetchingApps: boolean; @@ -47,7 +47,7 @@ export const AppListContext = createContext({ setMoveAppId: function (value: React.SetStateAction): void { throw new Error('Function not implemented.'); }, - appType: 'ALL', + appType: 'all', refetchFolderDetail: async function (): Promise { throw new Error('Function not implemented.'); }, @@ -60,7 +60,7 @@ export const AppListContext = createContext({ const AppListContextProvider = ({ children }: { children: ReactNode }) => { const { t } = useTranslation(); const router = useRouter(); - const { parentId = null, type = 'ALL' } = router.query as { + const { parentId = null, type = 'all' } = router.query as { parentId?: string | null; type: AppTypeEnum; }; @@ -73,7 +73,7 @@ const AppListContextProvider = ({ children }: { children: ReactNode }) => { } = useRequest2( () => { const formatType = (() => { - if (!type || type === 'ALL') return undefined; + if (!type || type === 'all') return undefined; if (type === AppTypeEnum.plugin) return [AppTypeEnum.folder, AppTypeEnum.plugin, AppTypeEnum.httpPlugin]; diff --git a/projects/app/src/pageComponents/login/LoginForm/FormLayout.tsx b/projects/app/src/pageComponents/login/LoginForm/FormLayout.tsx index db310bc068dd..27a08057b212 100644 --- a/projects/app/src/pageComponents/login/LoginForm/FormLayout.tsx +++ b/projects/app/src/pageComponents/login/LoginForm/FormLayout.tsx @@ -37,7 +37,7 @@ const FormLayout = ({ children, setPageType, pageType }: Props) => { const { setLoginStore, feConfigs } = useSystemStore(); const { isPc } = useSystem(); - const { lastRoute = '/app/list' } = router.query as { lastRoute: string }; + const { lastRoute = '/dashboard/apps' } = router.query as { lastRoute: string }; const state = useRef(getNanoid(8)); const redirectUri = `${location.origin}/login/provider`; diff --git a/projects/app/src/pages/404.tsx b/projects/app/src/pages/404.tsx index f85507355b65..1ab9826a79ae 100644 --- a/projects/app/src/pages/404.tsx +++ b/projects/app/src/pages/404.tsx @@ -4,7 +4,7 @@ import { useRouter } from 'next/router'; const NonePage = () => { const router = useRouter(); useEffect(() => { - router.push('/app/list'); + router.push('/dashboard/apps'); }, [router]); return
; diff --git a/projects/app/src/pages/_error.tsx b/projects/app/src/pages/_error.tsx index 1b20247d3e60..5920d0123161 100644 --- a/projects/app/src/pages/_error.tsx +++ b/projects/app/src/pages/_error.tsx @@ -48,7 +48,7 @@ function Error() { if (modelError) { router.push('/account/model'); } else { - router.push('/app/list'); + router.push('/dashboard/apps'); } }, 2000); }, []); diff --git a/projects/app/src/pages/chat/index.tsx b/projects/app/src/pages/chat/index.tsx index e9c2a83e4dfc..6f78faa28321 100644 --- a/projects/app/src/pages/chat/index.tsx +++ b/projects/app/src/pages/chat/index.tsx @@ -89,7 +89,7 @@ const Chat = ({ myApps }: { myApps: AppListItemType[] }) => { // reset all chat tore if (e?.code === 501) { setLastChatAppId(''); - router.replace('/app/list'); + router.replace('/dashboard/apps'); } else { router.replace({ query: { @@ -259,7 +259,7 @@ const Render = (props: { appId: string; isStandalone?: string }) => { status: 'error', title: t('common:core.chat.You need to a chat app') }); - router.replace('/app/list'); + router.replace('/dashboard/apps'); } else { router.replace({ query: { diff --git a/projects/app/src/pages/dashboard/[pluginGroupId]/index.tsx b/projects/app/src/pages/dashboard/[pluginGroupId]/index.tsx new file mode 100644 index 000000000000..68f8a3634045 --- /dev/null +++ b/projects/app/src/pages/dashboard/[pluginGroupId]/index.tsx @@ -0,0 +1,113 @@ +import DashboardContainer from '@/pageComponents/dashboard/Container'; + +import PluginCard from '@/pageComponents/dashboard/SystemPlugin/ToolCard'; +import { serviceSideProps } from '@/web/common/i18n/utils'; +import { getSystemPlugTemplates } from '@/web/core/app/api/plugin'; +import { Box, Flex, Grid } from '@chakra-ui/react'; +import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; +import { useRouter } from 'next/router'; +import { useMemo, useState } from 'react'; +import { useTranslation } from 'next-i18next'; +import MyBox from '@fastgpt/web/components/common/MyBox'; +import SearchInput from '@fastgpt/web/components/common/Input/SearchInput'; +import EmptyTip from '@fastgpt/web/components/common/EmptyTip'; +import { useSystem } from '@fastgpt/web/hooks/useSystem'; + +const SystemTools = () => { + const { t } = useTranslation(); + const router = useRouter(); + const { type, pluginGroupId } = router.query as { type?: string; pluginGroupId?: string }; + const { isPc } = useSystem(); + + const [searchKey, setSearchKey] = useState(''); + + const { data: plugins = [], loading: isLoading } = useRequest2(getSystemPlugTemplates, { + manual: false + }); + + const currentPlugins = useMemo(() => { + return plugins + .filter((plugin) => { + if (!type || type === 'all') return true; + return plugin.templateType === type; + }) + .filter((item) => { + if (!searchKey) return true; + const regx = new RegExp(searchKey, 'i'); + return regx.test(`${item.name}${item.intro}${item.instructions}`); + }); + }, [plugins, searchKey, type]); + + return ( + + {({ pluginGroups, MenuIcon }) => { + const currentGroup = pluginGroups.find((group) => group.groupId === pluginGroupId); + const groupTemplateTypeIds = + currentGroup?.groupTypes + ?.map((type) => type.typeId) + .reduce( + (acc, cur) => { + acc[cur] = true; + return acc; + }, + {} as Record + ) || {}; + const filterPluginsByGroup = currentPlugins.filter((plugin) => { + if (!currentGroup) return true; + return groupTemplateTypeIds[plugin.templateType]; + }); + + return ( + + + + {isPc ? ( + + {t('common:core.module.template.System Plugin')} + + ) : ( + MenuIcon + )} + + + setSearchKey(e.target.value)} + placeholder={t('common:plugin.Search plugin')} + /> + + + + {filterPluginsByGroup.map((item) => ( + + ))} + + {filterPluginsByGroup.length === 0 && } + + + ); + }} + + ); +}; + +export default SystemTools; + +export async function getServerSideProps(content: any) { + return { + props: { + ...(await serviceSideProps(content, ['app'])) + } + }; +} diff --git a/projects/app/src/pages/app/list/index.tsx b/projects/app/src/pages/dashboard/apps/index.tsx similarity index 74% rename from projects/app/src/pages/app/list/index.tsx rename to projects/app/src/pages/dashboard/apps/index.tsx index cd21267deb5e..2465704dc239 100644 --- a/projects/app/src/pages/app/list/index.tsx +++ b/projects/app/src/pages/dashboard/apps/index.tsx @@ -11,7 +11,7 @@ import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; import { postCreateAppFolder } from '@/web/core/app/api/app'; import type { EditFolderFormType } from '@fastgpt/web/components/common/MyModal/EditFolderModal'; import { useContextSelector } from 'use-context-selector'; -import AppListContextProvider, { AppListContext } from '@/pageComponents/app/list/context'; +import AppListContextProvider, { AppListContext } from '@/pageComponents/dashboard/apps/context'; import FolderPath from '@/components/common/folder/Path'; import { useRouter } from 'next/router'; import FolderSlideCard from '@/components/common/folder/SlideCard'; @@ -22,25 +22,24 @@ import { getCollaboratorList, postUpdateAppCollaborators } from '@/web/core/app/api/collaborator'; -import type { CreateAppType } from '@/pageComponents/app/list/CreateModal'; +import type { CreateAppType } from '@/pageComponents/dashboard/apps/CreateModal'; import { AppTypeEnum } from '@fastgpt/global/core/app/constants'; import MyBox from '@fastgpt/web/components/common/MyBox'; import LightRowTabs from '@fastgpt/web/components/common/Tabs/LightRowTabs'; import { useSystem } from '@fastgpt/web/hooks/useSystem'; import MyIcon from '@fastgpt/web/components/common/Icon'; -import TemplateMarketModal from '@/pageComponents/app/list/TemplateMarketModal'; -import MyImage from '@fastgpt/web/components/common/Image/MyImage'; -import JsonImportModal from '@/pageComponents/app/list/JsonImportModal'; +import JsonImportModal from '@/pageComponents/dashboard/apps/JsonImportModal'; import { PermissionValueType } from '@fastgpt/global/support/permission/type'; +import DashboardContainer from '@/pageComponents/dashboard/Container'; +import List from '@/pageComponents/dashboard/apps/List'; -const CreateModal = dynamic(() => import('@/pageComponents/app/list/CreateModal')); +const CreateModal = dynamic(() => import('@/pageComponents/dashboard/apps/CreateModal')); const EditFolderModal = dynamic( () => import('@fastgpt/web/components/common/MyModal/EditFolderModal') ); -const HttpEditModal = dynamic(() => import('@/pageComponents/app/list/HttpPluginEditModal')); -const List = dynamic(() => import('@/pageComponents/app/list/List')); +const HttpEditModal = dynamic(() => import('@/pageComponents/dashboard/apps/HttpPluginEditModal')); -const MyApps = () => { +const MyApps = ({ MenuIcon }: { MenuIcon: JSX.Element }) => { const { t } = useTranslation(); const router = useRouter(); const { isPc } = useSystem(); @@ -72,7 +71,6 @@ const MyApps = () => { onClose: onCloseJsonImportModal } = useDisclosure(); const [editFolder, setEditFolder] = useState(); - const [templateModalType, setTemplateModalType] = useState(); const { runAsync: onCreateFolder } = useRequest2(postCreateAppFolder, { onSuccess() { @@ -91,6 +89,17 @@ const MyApps = () => { errorToast: 'Error' }); + const appTypeName = useMemo(() => { + const map: Record = { + all: t('common:core.module.template.Team app'), + [AppTypeEnum.simple]: t('app:type.Simple bot'), + [AppTypeEnum.workflow]: t('app:type.Workflow bot'), + [AppTypeEnum.plugin]: t('app:type.Plugin'), + [AppTypeEnum.httpPlugin]: t('app:type.Http plugin'), + [AppTypeEnum.folder]: t('common:Folder') + }; + return map[appType] || map['all']; + }, [appType, t]); const RenderSearchInput = useMemo( () => ( @@ -120,7 +129,7 @@ const MyApps = () => { return ( {paths.length > 0 && ( - + { flex={'1 0 0'} flexDirection={'column'} h={'100%'} - pr={folderDetail ? [3, 2] : [3, 8]} - pl={3} + pr={folderDetail ? [3, 2] : [3, 6]} + pl={6} overflowY={'auto'} overflowX={'hidden'} > 0 ? 3 : [4, 6]} alignItems={'center'} gap={3}> - { - router.push({ - query: { - ...router.query, - type: e - } - }); - }} - /> + {isPc ? ( + + {appTypeName} + + ) : ( + MenuIcon + )} {isPc && RenderSearchInput} - {isPc && ( - setTemplateModalType('all')} - > - - {t('app:template_market')} - - )} - {(folderDetail ? folderDetail.permission.hasWritePer && folderDetail?.type !== AppTypeEnum.httpPlugin : userInfo?.team.permission.hasAppCreatePer) && ( @@ -261,20 +215,6 @@ const MyApps = () => { } ] }, - ...(isPc - ? [] - : [ - { - children: [ - { - icon: '/imgs/app/templateFill.svg', - label: t('app:template_market'), - description: t('app:template_market_description'), - onClick: () => setTemplateModalType('all') - } - ] - } - ]), { children: [ { @@ -379,19 +319,9 @@ const MyApps = () => { /> )} {!!createAppType && ( - setCreateAppType(undefined)} - onOpenTemplateModal={setTemplateModalType} - /> + setCreateAppType(undefined)} /> )} {isOpenCreateHttpPlugin && } - {!!templateModalType && ( - setTemplateModalType(undefined)} - defaultType={templateModalType} - /> - )} {isOpenJsonImportModal && } ); @@ -399,9 +329,13 @@ const MyApps = () => { function ContextRender() { return ( - - - + + {({ MenuIcon }) => ( + + + + )} + ); } diff --git a/projects/app/src/pages/dashboard/templateMarket/index.tsx b/projects/app/src/pages/dashboard/templateMarket/index.tsx new file mode 100644 index 000000000000..cd48d8bd4e75 --- /dev/null +++ b/projects/app/src/pages/dashboard/templateMarket/index.tsx @@ -0,0 +1,348 @@ +import { serviceSideProps } from '@/web/common/i18n/utils'; +import DashboardContainer from '@/pageComponents/dashboard/Container'; +import { Box, Button, Flex, Grid, HStack } from '@chakra-ui/react'; +import { useRouter } from 'next/router'; +import { useTranslation } from 'next-i18next'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { useSystemStore } from '@/web/common/system/useSystemStore'; +import { ParentIdType } from '@fastgpt/global/common/parentFolder/type'; +import { AppTypeEnum } from '@fastgpt/global/core/app/constants'; +import { AppTemplateSchemaType, TemplateTypeSchemaType } from '@fastgpt/global/core/app/type'; +import MyBox from '@fastgpt/web/components/common/MyBox'; +import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; +import { getTemplateMarketItemDetail } from '@/web/core/app/api/template'; +import { postCreateApp } from '@/web/core/app/api'; +import { webPushTrack } from '@/web/common/middle/tracks/utils'; +import Avatar from '@fastgpt/web/components/common/Avatar'; +import AppTypeTag from '@/pageComponents/dashboard/apps/TypeTag'; + +import dynamic from 'next/dynamic'; +import SearchInput from '@fastgpt/web/components/common/Input/SearchInput'; +import MySelect from '@fastgpt/web/components/common/MySelect'; +import EmptyTip from '@fastgpt/web/components/common/EmptyTip'; +import { useSystem } from '@fastgpt/web/hooks/useSystem'; +const UseGuideModal = dynamic(() => import('@/components/common/Modal/UseGuideModal'), { + ssr: false +}); + +const TemplateMarket = ({ + templateList, + templateTags, + MenuIcon +}: { + templateList: AppTemplateSchemaType[]; + templateTags: TemplateTypeSchemaType[]; + MenuIcon: JSX.Element; +}) => { + const router = useRouter(); + const { t } = useTranslation(); + const { feConfigs } = useSystemStore(); + const { isPc } = useSystem(); + const containerRef = useRef(null); + + const { + parentId, + type, + appType = 'all' + } = router.query as { parentId?: ParentIdType; type?: string; appType?: AppTypeEnum | 'all' }; + const [searchKey, setSearchKey] = useState(''); + + const filterTemplateTags = useMemo(() => { + return templateTags + .map((tag) => { + const templates = templateList.filter((template) => template.tags.includes(tag.typeId)); + return { + ...tag, + templates + }; + }) + .filter((item) => item.templates.length > 0); + }, [templateList, templateTags]); + + const { runAsync: onUseTemplate, loading: isCreating } = useRequest2( + async (template: AppTemplateSchemaType) => { + const templateDetail = await getTemplateMarketItemDetail(template.templateId); + + return postCreateApp({ + parentId, + avatar: template.avatar, + name: template.name, + type: template.type as AppTypeEnum, + modules: templateDetail.workflow.nodes || [], + edges: templateDetail.workflow.edges || [], + chatConfig: templateDetail.workflow.chatConfig + }).then((res) => { + webPushTrack.useAppTemplate({ + id: res, + name: template.name + }); + + return res; + }); + }, + { + onSuccess(id: string) { + router.push(`/app/detail?appId=${id}`); + }, + successToast: t('common:common.Create Success'), + errorToast: t('common:common.Create Failed') + } + ); + + const TemplateCard = useCallback( + ({ item }: { item: AppTemplateSchemaType }) => { + const { t } = useTranslation(); + + return ( + + + + + {item.name} + + + + + + + {item.intro || t('app:templateMarket.no_intro')} + + + + {`by ${item.author || feConfigs.systemTitle}`} + + {((item.userGuide?.type === 'markdown' && item.userGuide?.content) || + (item.userGuide?.type === 'link' && item.userGuide?.link)) && ( + + {({ onClick }) => ( + + )} + + )} + + + + + ); + }, + [onUseTemplate, feConfigs.systemTitle] + ); + + // Scroll to the selected template type + useEffect(() => { + if (type) { + const typeElement = document.getElementById(type as string); + if (typeElement) { + typeElement.scrollIntoView({ behavior: 'smooth', block: 'start' }); + } + } + }, [type]); + + return ( + + + + {isPc ? ( + + {t('app:template_market')} + + ) : ( + MenuIcon + )} + + + + setSearchKey(e.target.value)} + /> + + { + router.push({ + query: { + ...router.query, + type: '', + appType: e + } + }); + }} + /> + + + + {searchKey ? ( + <> + + {t('common:xx_search_result', { key: searchKey })} + + {(() => { + const templates = templateList.filter((template) => + `${template.name}${template.intro}`.includes(searchKey) + ); + + if (templates.length > 0) { + return ( + + {templates.map((item) => ( + + ))} + + ); + } + + return ; + })()} + + ) : ( + <> + {filterTemplateTags.map((item) => { + return ( + + + {t(item.typeName as any)} + + + {item.templates.map((item) => ( + + ))} + + + ); + })} + + )} + + + + ); +}; + +const TemplateMarketContainer = ({ children }: { children: React.ReactNode }) => { + return ( + + {({ templateTags, templateList, MenuIcon }) => ( + + )} + + ); +}; + +export default TemplateMarketContainer; + +export async function getServerSideProps(content: any) { + return { + props: { + ...(await serviceSideProps(content, ['app'])) + } + }; +} diff --git a/projects/app/src/pages/index.tsx b/projects/app/src/pages/index.tsx index c96f598f7ab6..8e03c019e82b 100644 --- a/projects/app/src/pages/index.tsx +++ b/projects/app/src/pages/index.tsx @@ -6,7 +6,7 @@ import { useRouter } from 'next/router'; const index = () => { const router = useRouter(); useEffect(() => { - router.push('/app/list'); + router.push('/dashboard/apps'); }, [router]); return ; }; diff --git a/projects/app/src/pages/login/fastlogin.tsx b/projects/app/src/pages/login/fastlogin.tsx index 7ec6cb3b8323..e353b73d5f8d 100644 --- a/projects/app/src/pages/login/fastlogin.tsx +++ b/projects/app/src/pages/login/fastlogin.tsx @@ -77,7 +77,7 @@ export async function getServerSideProps(content: any) { props: { code: content?.query?.code || '', token: content?.query?.token || '', - callbackUrl: content?.query?.callbackUrl || '/app/list', + callbackUrl: content?.query?.callbackUrl || '/dashboard/apps', ...(await serviceSideProps(content)) } }; diff --git a/projects/app/src/pages/login/index.tsx b/projects/app/src/pages/login/index.tsx index 3977f7def9cb..0917450770ab 100644 --- a/projects/app/src/pages/login/index.tsx +++ b/projects/app/src/pages/login/index.tsx @@ -66,7 +66,9 @@ const Login = ({ ChineseRedirectUrl }: { ChineseRedirectUrl: string }) => { const decodeLastRoute = decodeURIComponent(lastRoute); // 检查是否是当前的 route const navigateTo = - decodeLastRoute && !decodeLastRoute.includes('/login') ? decodeLastRoute : '/app/list'; + decodeLastRoute && !decodeLastRoute.includes('/login') + ? decodeLastRoute + : '/dashboard/apps'; router.push(navigateTo); }, [setUserInfo, lastRoute, router] @@ -129,7 +131,7 @@ const Login = ({ ChineseRedirectUrl }: { ChineseRedirectUrl: string }) => { useMount(() => { clearToken(); - router.prefetch('/app/list'); + router.prefetch('/dashboard/apps'); ChineseRedirectUrl && showRedirect && checkIpInChina(); localCookieVersion !== cookieVersion && onOpenCookiesDrawer(); diff --git a/projects/app/src/pages/login/provider.tsx b/projects/app/src/pages/login/provider.tsx index b6a109c1d3fd..a1327a7412ed 100644 --- a/projects/app/src/pages/login/provider.tsx +++ b/projects/app/src/pages/login/provider.tsx @@ -26,7 +26,9 @@ const provider = () => { (res: ResLogin) => { setUserInfo(res.user); - router.push(loginStore?.lastRoute ? decodeURIComponent(loginStore?.lastRoute) : '/app/list'); + router.push( + loginStore?.lastRoute ? decodeURIComponent(loginStore?.lastRoute) : '/dashboard/apps' + ); }, [setUserInfo, router, loginStore?.lastRoute] ); @@ -95,7 +97,7 @@ const provider = () => { (async () => { await clearToken(); - router.prefetch('/app/list'); + router.prefetch('/dashboard/apps'); if (loginStore && loginStore.provider !== 'sso' && state !== loginStore.state) { toast({ diff --git a/projects/app/src/pages/toolkit/index.tsx b/projects/app/src/pages/toolkit/index.tsx deleted file mode 100644 index 33f0b584ccc1..000000000000 --- a/projects/app/src/pages/toolkit/index.tsx +++ /dev/null @@ -1,226 +0,0 @@ -import { serviceSideProps } from '@/web/common/i18n/utils'; -import { getPluginGroups, getSystemPlugTemplates } from '@/web/core/app/api/plugin'; -import { Box, Flex, Grid, useDisclosure } from '@chakra-ui/react'; -import Avatar from '@fastgpt/web/components/common/Avatar'; -import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; -import { useMemo, useState } from 'react'; -import PluginCard from '@/pageComponents/toolkit/PluginCard'; -import { i18nT } from '@fastgpt/web/i18n/utils'; -import { useTranslation } from 'next-i18next'; -import { useRouter } from 'next/router'; -import MyIcon from '@fastgpt/web/components/common/Icon'; -import { useSystem } from '@fastgpt/web/hooks/useSystem'; -import SearchInput from '@fastgpt/web/components/common/Input/SearchInput'; -import { navbarWidth } from '@/components/Layout'; - -const Toolkit = () => { - const { t } = useTranslation(); - const router = useRouter(); - const { isPc } = useSystem(); - - const { data: plugins = [] } = useRequest2(getSystemPlugTemplates, { - manual: false - }); - const { data: pluginGroups = [] } = useRequest2(getPluginGroups, { - manual: false - }); - const isOneGroup = pluginGroups.length === 1; - - const [search, setSearch] = useState(''); - const { isOpen, onOpen, onClose } = useDisclosure(); - - const { group: selectedGroup = pluginGroups?.[0]?.groupId, type: selectedType = 'all' } = - router.query; - - const pluginGroupTypes = useMemo(() => { - const allTypes = [ - { - typeId: 'all', - typeName: i18nT('common:common.All') - } - ]; - const currentTypes = - pluginGroups?.find((group) => group.groupId === selectedGroup)?.groupTypes ?? []; - - return [ - ...allTypes, - ...currentTypes.filter((type) => - plugins.find((plugin) => plugin.templateType === type.typeId) - ) - ]; - }, [pluginGroups, plugins, selectedGroup]); - - const currentPlugins = useMemo(() => { - const typeArray = pluginGroupTypes?.map((type) => type.typeId); - return plugins - .filter( - (plugin) => - (selectedType === 'all' && typeArray?.includes(plugin.templateType)) || - selectedType === plugin.templateType - ) - .filter((plugin) => { - const str = `${plugin.name}${plugin.intro}${plugin.instructions}`; - const regx = new RegExp(search, 'gi'); - return regx.test(str); - }); - }, [pluginGroupTypes, plugins, selectedType, search]); - - return ( - - {/* Mask */} - {!isPc && isOpen && ( - - )} - {/* Sidebar */} - {(isPc || isOpen) && ( - - {pluginGroups.map((group) => { - const selected = group.groupId === selectedGroup; - return ( - - { - router.push({ - query: { group: group.groupId, type: 'all' } - }); - onClose(); - } - })} - > - - {t(group.groupName as any)} - - {!isOneGroup && ( - - )} - - {/* group types */} - {selected && - pluginGroupTypes.map((type) => { - return ( - { - router.push({ - query: { group: selectedGroup, type: type.typeId } - }); - onClose(); - }} - > - {t(type.typeName as any)} - - ); - })} - - ); - })} - - )} - - - - {isPc ? ( - - {t( - pluginGroups?.find((group) => group.groupId === selectedGroup)?.groupName as any - )} - - ) : ( - - )} - - - setSearch(e.target.value)} - placeholder={t('common:plugin.Search plugin')} - /> - - - - - {currentPlugins.map((item) => ( - - ))} - - - - ); -}; - -export default Toolkit; - -export async function getServerSideProps(context: any) { - return { - props: { - ...(await serviceSideProps(context, ['app', 'user'])) - } - }; -} diff --git a/projects/app/src/web/common/system/useSystemStore.ts b/projects/app/src/web/common/system/useSystemStore.ts index 4a19a06b3a4b..a78339cbef1a 100644 --- a/projects/app/src/web/common/system/useSystemStore.ts +++ b/projects/app/src/web/common/system/useSystemStore.ts @@ -77,7 +77,7 @@ export const useSystemStore = create()( state.initd = true; }); }, - lastRoute: '/app/list', + lastRoute: '/dashboard/apps', setLastRoute(e) { set((state) => { state.lastRoute = e; From 4b37eee9879cef6007c856edac65d56bea2af46c Mon Sep 17 00:00:00 2001 From: archer <545436317@qq.com> Date: Tue, 15 Apr 2025 16:20:34 +0800 Subject: [PATCH 02/12] feat: value format test --- .vscode/nextapi.code-snippets | 72 +---- .../zh-cn/docs/development/upgrading/496.md | 2 +- .../service/core/workflow/dispatch/utils.ts | 132 ++++---- .../core/app/workflow/dispatch/utils.test.ts | 289 ++++++++++++++++++ .../app/workflow/workflowDispatch.test.ts | 1 + 5 files changed, 377 insertions(+), 119 deletions(-) create mode 100644 test/cases/service/core/app/workflow/dispatch/utils.test.ts diff --git a/.vscode/nextapi.code-snippets b/.vscode/nextapi.code-snippets index ca6bd28f3f89..701732b0eeb6 100644 --- a/.vscode/nextapi.code-snippets +++ b/.vscode/nextapi.code-snippets @@ -52,71 +52,17 @@ "description": "FastGPT usecontext template" }, - "Jest test template": { - "scope": "typescriptreact", - "prefix": "jesttest", + "Vitest test case template": { + "scope": "typescript", + "prefix": "template_test", "body": [ - "import '@/pages/api/__mocks__/base';", - "import { root } from '@/pages/api/__mocks__/db/init';", - "import { getTestRequest } from '@fastgpt/service/test/utils'; ;", - "import { AppErrEnum } from '@fastgpt/global/common/error/code/app';", - "import handler from './demo';", - "", - "// Import the schema", - "import { MongoOutLink } from '@fastgpt/service/support/outLink/schema';", - "", - "beforeAll(async () => {", - " // await MongoOutLink.create({", - " // shareId: 'aaa',", - " // appId: root.appId,", - " // tmbId: root.tmbId,", - " // teamId: root.teamId,", - " // type: 'share',", - " // name: 'aaa'", - " // })", - "});", - "", - "test('Should return a list of outLink', async () => {", - " // Mock request", - " const res = (await handler(", - " ...getTestRequest({", - " query: {", - " appId: root.appId,", - " type: 'share'", - " },", - " user: root", - " })", - " )) as any;", - "", - " expect(res.code).toBe(200);", - " expect(res.data.length).toBe(2);", - "});", - "", - "test('appId is required', async () => {", - " const res = (await handler(", - " ...getTestRequest({", - " query: {", - " type: 'share'", - " },", - " user: root", - " })", - " )) as any;", - " expect(res.code).toBe(500);", - " expect(res.error).toBe(AppErrEnum.unExist);", - "});", + "import { describe, it, expect } from 'vitest';", "", - "test('if type is not provided, return nothing', async () => {", - " const res = (await handler(", - " ...getTestRequest({", - " query: {", - " appId: root.appId", - " },", - " user: root", - " })", - " )) as any;", - " expect(res.code).toBe(200);", - " expect(res.data.length).toBe(0);", + "describe('authType2UsageSource', () => {", + " it('Test description', () => {", + " expect().toBe();", + " });", "});" - ] + ] } } \ No newline at end of file diff --git a/docSite/content/zh-cn/docs/development/upgrading/496.md b/docSite/content/zh-cn/docs/development/upgrading/496.md index ed5553170cde..496e361b0fd6 100644 --- a/docSite/content/zh-cn/docs/development/upgrading/496.md +++ b/docSite/content/zh-cn/docs/development/upgrading/496.md @@ -16,7 +16,7 @@ weight: 794 ## ⚙️ 优化 - +1. 工作流数据类型转化鲁棒性和兼容性增强。 ## 🐛 修复 diff --git a/packages/service/core/workflow/dispatch/utils.ts b/packages/service/core/workflow/dispatch/utils.ts index b8fedd1ca74f..5d8b8fda9616 100644 --- a/packages/service/core/workflow/dispatch/utils.ts +++ b/packages/service/core/workflow/dispatch/utils.ts @@ -106,8 +106,18 @@ export const getHistories = (history?: ChatItemType[] | number, histories: ChatI /* value type format */ export const valueTypeFormat = (value: any, type?: WorkflowIOValueTypeEnum) => { - // 1. 基础条件检查 - if (value === undefined || value === null) return; + 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 (!type || type === WorkflowIOValueTypeEnum.any) return value; // 2. 如果值已经符合目标类型,直接返回 @@ -115,62 +125,37 @@ export const valueTypeFormat = (value: any, type?: WorkflowIOValueTypeEnum) => { (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)) + (type.startsWith('array') && Array.isArray(value)) || + (type === WorkflowIOValueTypeEnum.object && typeof value === 'object') || + (type === WorkflowIOValueTypeEnum.chatHistory && Array.isArray(value)) || + (type === WorkflowIOValueTypeEnum.datasetQuote && Array.isArray(value)) || + (type === WorkflowIOValueTypeEnum.selectDataset && Array.isArray(value)) || + (type === WorkflowIOValueTypeEnum.selectApp && typeof value === 'object') ) { 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]; + // 3. 空值处理 + if (value === undefined || value === null) { + if (type === WorkflowIOValueTypeEnum.string) return ''; + if (type === WorkflowIOValueTypeEnum.number) return 0; + if (type === WorkflowIOValueTypeEnum.boolean) return false; + if (type.startsWith('array')) return []; + if (type === WorkflowIOValueTypeEnum.object) return {}; + if (type === WorkflowIOValueTypeEnum.chatHistory) return []; + if (type === WorkflowIOValueTypeEnum.datasetQuote) return []; + if (type === WorkflowIOValueTypeEnum.selectDataset) return []; + if (type === WorkflowIOValueTypeEnum.selectApp) return {}; } - // 4.2 基本类型转换 + // 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'; @@ -178,22 +163,59 @@ export const valueTypeFormat = (value: any, type?: WorkflowIOValueTypeEnum) => { return Boolean(value); } - // 4.3 复杂对象类型处理 + // 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.object, WorkflowIOValueTypeEnum.chatHistory, WorkflowIOValueTypeEnum.datasetQuote, - WorkflowIOValueTypeEnum.selectApp, WorkflowIOValueTypeEnum.selectDataset - ].includes(type) && - typeof value !== 'object' + ].includes(type) ) { - try { - return json5.parse(value); - } catch (error) { - return value; + 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 {}; } // 5. 默认返回原值 diff --git a/test/cases/service/core/app/workflow/dispatch/utils.test.ts b/test/cases/service/core/app/workflow/dispatch/utils.test.ts new file mode 100644 index 000000000000..b2e32ad0a56d --- /dev/null +++ b/test/cases/service/core/app/workflow/dispatch/utils.test.ts @@ -0,0 +1,289 @@ +import { describe, it, expect } from 'vitest'; +import { valueTypeFormat } from '@fastgpt/service/core/workflow/dispatch/utils'; +import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants'; + +describe('valueTypeFormat', () => { + // value 为字符串 + const strTestList = [ + { + value: 'a', + type: WorkflowIOValueTypeEnum.string, + result: 'a' + }, + { + value: 'a', + type: WorkflowIOValueTypeEnum.number, + result: Number('a') + }, + { + value: 'a', + type: WorkflowIOValueTypeEnum.boolean, + result: false + }, + { + value: 'true', + type: WorkflowIOValueTypeEnum.boolean, + result: true + }, + { + value: 'false', + type: WorkflowIOValueTypeEnum.boolean, + result: false + }, + { + value: 'false', + type: WorkflowIOValueTypeEnum.arrayNumber, + result: ['false'] + }, + { + value: 'false', + type: WorkflowIOValueTypeEnum.arrayString, + result: ['false'] + }, + { + value: 'false', + type: WorkflowIOValueTypeEnum.object, + result: {} + }, + { + value: 'false', + type: WorkflowIOValueTypeEnum.selectApp, + result: {} + }, + { + value: 'false', + type: WorkflowIOValueTypeEnum.selectDataset, + result: [] + }, + { + value: 'saf', + type: WorkflowIOValueTypeEnum.selectDataset, + result: [] + }, + { + value: '[]', + type: WorkflowIOValueTypeEnum.selectDataset, + result: [] + }, + { + value: '{"a":1}', + type: WorkflowIOValueTypeEnum.object, + result: { a: 1 } + }, + { + value: '[{"a":1}]', + type: WorkflowIOValueTypeEnum.arrayAny, + result: [{ a: 1 }] + }, + { + value: '["111"]', + type: WorkflowIOValueTypeEnum.arrayString, + result: ['111'] + } + ]; + strTestList.forEach((item, index) => { + it(`String test ${index}`, () => { + expect(valueTypeFormat(item.value, item.type)).toEqual(item.result); + }); + }); + + // value 为 number + const numTestList = [ + { + value: 1, + type: WorkflowIOValueTypeEnum.string, + result: '1' + }, + { + value: 1, + type: WorkflowIOValueTypeEnum.number, + result: 1 + }, + { + value: 1, + type: WorkflowIOValueTypeEnum.boolean, + result: true + }, + { + value: 0, + type: WorkflowIOValueTypeEnum.boolean, + result: false + }, + { + value: 0, + type: WorkflowIOValueTypeEnum.any, + result: 0 + }, + { + value: 0, + type: WorkflowIOValueTypeEnum.arrayAny, + result: [0] + }, + { + value: 0, + type: WorkflowIOValueTypeEnum.arrayNumber, + result: [0] + }, + { + value: 0, + type: WorkflowIOValueTypeEnum.arrayString, + result: [0] + } + ]; + numTestList.forEach((item, index) => { + it(`Number test ${index}`, () => { + expect(valueTypeFormat(item.value, item.type)).toEqual(item.result); + }); + }); + + // value 为 boolean + const boolTestList = [ + { + value: true, + type: WorkflowIOValueTypeEnum.string, + result: 'true' + }, + { + value: true, + type: WorkflowIOValueTypeEnum.number, + result: 1 + }, + { + value: false, + type: WorkflowIOValueTypeEnum.number, + result: 0 + }, + { + value: true, + type: WorkflowIOValueTypeEnum.boolean, + result: true + }, + { + value: true, + type: WorkflowIOValueTypeEnum.any, + result: true + }, + { + value: true, + type: WorkflowIOValueTypeEnum.arrayBoolean, + result: [true] + }, + { + value: true, + type: WorkflowIOValueTypeEnum.object, + result: true + } + ]; + boolTestList.forEach((item, index) => { + it(`Boolean test ${index}`, () => { + expect(valueTypeFormat(item.value, item.type)).toEqual(item.result); + }); + }); + + // value 为 object + const objTestList = [ + { + value: { a: 1 }, + type: WorkflowIOValueTypeEnum.string, + result: JSON.stringify({ a: 1 }) + }, + { + value: { a: 1 }, + type: WorkflowIOValueTypeEnum.number, + result: Number({ a: 1 }) + }, + { + value: { a: 1 }, + type: WorkflowIOValueTypeEnum.boolean, + result: Boolean({ a: 1 }) + }, + { + value: { a: 1 }, + type: WorkflowIOValueTypeEnum.object, + result: { a: 1 } + }, + { + value: { a: 1 }, + type: WorkflowIOValueTypeEnum.arrayAny, + result: [{ a: 1 }] + } + ]; + objTestList.forEach((item, index) => { + it(`Object test ${index}`, () => { + expect(valueTypeFormat(item.value, item.type)).toEqual(item.result); + }); + }); + + // value 为 array + const arrayTestList = [ + { + value: [1, 2, 3], + type: WorkflowIOValueTypeEnum.string, + result: JSON.stringify([1, 2, 3]) + }, + { + value: [1, 2, 3], + type: WorkflowIOValueTypeEnum.number, + result: Number([1, 2, 3]) + }, + { + value: [1, 2, 3], + type: WorkflowIOValueTypeEnum.boolean, + result: Boolean([1, 2, 3]) + }, + { + value: [1, 2, 3], + type: WorkflowIOValueTypeEnum.arrayNumber, + result: [1, 2, 3] + }, + { + value: [1, 2, 3], + type: WorkflowIOValueTypeEnum.arrayAny, + result: [1, 2, 3] + } + ]; + arrayTestList.forEach((item, index) => { + it(`Array test ${index}`, () => { + expect(valueTypeFormat(item.value, item.type)).toEqual(item.result); + }); + }); + + // value 为 null/undefined + const nullTestList = [ + { + value: undefined, + type: WorkflowIOValueTypeEnum.string, + result: '' + }, + { + value: undefined, + type: WorkflowIOValueTypeEnum.number, + result: 0 + }, + { + value: undefined, + type: WorkflowIOValueTypeEnum.boolean, + result: false + }, + { + value: undefined, + type: WorkflowIOValueTypeEnum.arrayAny, + result: [] + }, + { + value: undefined, + type: WorkflowIOValueTypeEnum.object, + result: {} + }, + { + value: undefined, + type: WorkflowIOValueTypeEnum.chatHistory, + result: [] + } + ]; + nullTestList.forEach((item, index) => { + it(`Null test ${index}`, () => { + expect(valueTypeFormat(item.value, item.type)).toEqual(item.result); + }); + }); +}); diff --git a/test/cases/service/core/app/workflow/workflowDispatch.test.ts b/test/cases/service/core/app/workflow/workflowDispatch.test.ts index 7a5926afc966..6a6810acf034 100644 --- a/test/cases/service/core/app/workflow/workflowDispatch.test.ts +++ b/test/cases/service/core/app/workflow/workflowDispatch.test.ts @@ -7,6 +7,7 @@ import { storeNodes2RuntimeNodes } from '@fastgpt/global/core/workflow/runtime/utils'; import { ChatItemValueTypeEnum } from '@fastgpt/global/core/chat/constants'; + vi.mock(import('@fastgpt/service/common/string/tiktoken'), async (importOriginal) => { const mod = await importOriginal(); return { From 4818b71efb04d90ec491eedc9d54f82525749eec Mon Sep 17 00:00:00 2001 From: archer <545436317@qq.com> Date: Tue, 15 Apr 2025 16:26:34 +0800 Subject: [PATCH 03/12] doc --- docSite/content/zh-cn/docs/development/upgrading/496.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docSite/content/zh-cn/docs/development/upgrading/496.md b/docSite/content/zh-cn/docs/development/upgrading/496.md index 496e361b0fd6..7277c413f95a 100644 --- a/docSite/content/zh-cn/docs/development/upgrading/496.md +++ b/docSite/content/zh-cn/docs/development/upgrading/496.md @@ -17,6 +17,7 @@ weight: 794 ## ⚙️ 优化 1. 工作流数据类型转化鲁棒性和兼容性增强。 +2. Python sandbox 代码,支持大数据输入。 ## 🐛 修复 From e9e5b6d2ade36663687f4b0d97d0fc6913c5c18d Mon Sep 17 00:00:00 2001 From: Archer <545436317@qq.com> Date: Wed, 16 Apr 2025 12:57:54 +0800 Subject: [PATCH 04/12] Mcp export (#4555) * feat: mcp server * feat: mcp server * feat: mcp server build * update doc --- .github/workflows/fastgpt-preview-image.yml | 42 +- .github/workflows/mcp_server-build-image.yml | 151 ++++++ Makefile | 2 +- .../zh-cn/docs/development/upgrading/496.md | 5 +- packages/global/common/error/code/common.ts | 5 + packages/global/common/error/code/team.ts | 7 +- .../global/common/system/types/index.d.ts | 1 + packages/global/core/chat/constants.ts | 6 +- packages/global/support/mcp/type.d.ts | 14 + .../global/support/wallet/usage/constants.ts | 6 +- packages/service/core/app/controller.ts | 16 + .../workflow/dispatch/init/workflowStart.tsx | 16 +- .../service/core/workflow/dispatch/utils.ts | 14 +- packages/service/support/mcp/schema.ts | 58 ++ .../service/support/permission/mcp/auth.ts | 45 ++ .../common/MyPopover/PopoverConfirm.tsx | 1 + packages/web/i18n/en/account_usage.json | 1 + packages/web/i18n/en/common.json | 7 +- packages/web/i18n/en/dashboard_mcp.json | 20 + packages/web/i18n/zh-CN/account_usage.json | 1 + packages/web/i18n/zh-CN/common.json | 9 +- packages/web/i18n/zh-CN/dashboard_mcp.json | 20 + packages/web/i18n/zh-Hant/account_usage.json | 1 + packages/web/i18n/zh-Hant/common.json | 7 +- packages/web/i18n/zh-Hant/dashboard_mcp.json | 20 + packages/web/types/i18next.d.ts | 5 +- pnpm-lock.yaml | 499 ++++++++++++++++-- projects/app/data/config.json | 3 +- projects/app/next.config.js | 2 +- projects/app/package.json | 4 +- .../pageComponents/dashboard/Container.tsx | 31 +- .../dashboard/mcp/EditModal.tsx | 414 +++++++++++++++ .../pageComponents/dashboard/mcp/usageWay.tsx | 62 +++ .../src/pages/api/core/app/getBasicInfo.ts | 33 ++ .../app/src/pages/api/support/mcp/create.ts | 77 +++ .../app/src/pages/api/support/mcp/delete.ts | 34 ++ .../app/src/pages/api/support/mcp/list.ts | 34 ++ .../pages/api/support/mcp/server/toolCall.ts | 196 +++++++ .../pages/api/support/mcp/server/toolList.ts | 152 ++++++ .../app/src/pages/api/support/mcp/update.ts | 66 +++ .../src/pages/dashboard/mcpServer/index.tsx | 159 ++++++ projects/app/src/web/core/app/api.ts | 7 + projects/app/src/web/support/mcp/api.ts | 20 + projects/mcp_server/.env.template | 1 + projects/mcp_server/Dockerfile | 43 ++ projects/mcp_server/package.json | 30 ++ projects/mcp_server/src/api/fastgpt.ts | 7 + projects/mcp_server/src/api/request.ts | 111 ++++ projects/mcp_server/src/index.ts | 101 ++++ projects/mcp_server/src/init.ts | 3 + projects/mcp_server/src/type.d.ts | 5 + projects/mcp_server/src/utils/error.ts | 10 + projects/mcp_server/src/utils/log.ts | 68 +++ projects/mcp_server/src/utils/string.ts | 8 + projects/mcp_server/tsconfig.json | 17 + .../core/app/workflow/dispatch/utils.test.ts | 74 +-- 56 files changed, 2621 insertions(+), 130 deletions(-) create mode 100644 .github/workflows/mcp_server-build-image.yml create mode 100644 packages/global/support/mcp/type.d.ts create mode 100644 packages/service/support/mcp/schema.ts create mode 100644 packages/service/support/permission/mcp/auth.ts create mode 100644 packages/web/i18n/en/dashboard_mcp.json create mode 100644 packages/web/i18n/zh-CN/dashboard_mcp.json create mode 100644 packages/web/i18n/zh-Hant/dashboard_mcp.json create mode 100644 projects/app/src/pageComponents/dashboard/mcp/EditModal.tsx create mode 100644 projects/app/src/pageComponents/dashboard/mcp/usageWay.tsx create mode 100644 projects/app/src/pages/api/core/app/getBasicInfo.ts create mode 100644 projects/app/src/pages/api/support/mcp/create.ts create mode 100644 projects/app/src/pages/api/support/mcp/delete.ts create mode 100644 projects/app/src/pages/api/support/mcp/list.ts create mode 100644 projects/app/src/pages/api/support/mcp/server/toolCall.ts create mode 100644 projects/app/src/pages/api/support/mcp/server/toolList.ts create mode 100644 projects/app/src/pages/api/support/mcp/update.ts create mode 100644 projects/app/src/pages/dashboard/mcpServer/index.tsx create mode 100644 projects/app/src/web/support/mcp/api.ts create mode 100644 projects/mcp_server/.env.template create mode 100644 projects/mcp_server/Dockerfile create mode 100644 projects/mcp_server/package.json create mode 100644 projects/mcp_server/src/api/fastgpt.ts create mode 100644 projects/mcp_server/src/api/request.ts create mode 100644 projects/mcp_server/src/index.ts create mode 100644 projects/mcp_server/src/init.ts create mode 100644 projects/mcp_server/src/type.d.ts create mode 100644 projects/mcp_server/src/utils/error.ts create mode 100644 projects/mcp_server/src/utils/log.ts create mode 100644 projects/mcp_server/src/utils/string.ts create mode 100644 projects/mcp_server/tsconfig.json diff --git a/.github/workflows/fastgpt-preview-image.yml b/.github/workflows/fastgpt-preview-image.yml index ec9a37189931..42d275f2366d 100644 --- a/.github/workflows/fastgpt-preview-image.yml +++ b/.github/workflows/fastgpt-preview-image.yml @@ -13,25 +13,32 @@ jobs: pull-requests: write runs-on: ubuntu-24.04 + strategy: + matrix: + image: [fastgpt, sandbox, mcp-server] + fail-fast: false # 即使一个镜像构建失败,也继续构建其他镜像 + steps: - name: Checkout uses: actions/checkout@v3 with: ref: ${{ github.event.pull_request.head.ref }} repository: ${{ github.event.pull_request.head.repo.full_name }} - fetch-depth: 0 # Fetch all history for .GitInfo and .Lastmod + fetch-depth: 0 token: ${{ secrets.GITHUB_TOKEN }} - name: Set up Docker Buildx uses: docker/setup-buildx-action@v2 with: driver-opts: network=host + - name: Cache Docker layers uses: actions/cache@v3 with: path: /tmp/.buildx-cache - key: ${{ runner.os }}-buildx-${{ github.sha }} + key: ${{ runner.os }}-buildx-${{ github.sha }}-${{ matrix.image }} restore-keys: | + ${{ runner.os }}-buildx-${{ github.sha }}- ${{ runner.os }}-buildx- - name: Login to GitHub Container Registry @@ -41,24 +48,35 @@ jobs: username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Set DOCKER_REPO_TAGGED based on branch or tag + - name: Set image config + id: config run: | - echo "DOCKER_REPO_TAGGED=ghcr.io/${{ github.repository_owner }}/fastgpt-pr:${{ github.event.pull_request.head.sha }}" >> $GITHUB_ENV + if [[ "${{ matrix.image }}" == "fastgpt" ]]; then + echo "DOCKERFILE=projects/app/Dockerfile" >> $GITHUB_OUTPUT + echo "DESCRIPTION=fastgpt-pr image" >> $GITHUB_OUTPUT + echo "DOCKER_REPO_TAGGED=ghcr.io/${{ github.repository_owner }}/fastgpt-pr:fatsgpt_${{ github.event.pull_request.head.sha }}" >> $GITHUB_OUTPUT + elif [[ "${{ matrix.image }}" == "sandbox" ]]; then + echo "DOCKERFILE=projects/app/sandbox.Dockerfile" >> $GITHUB_OUTPUT + echo "DESCRIPTION=fastgpt-sandbox-pr image" >> $GITHUB_OUTPUT + echo "DOCKER_REPO_TAGGED=ghcr.io/${{ github.repository_owner }}/fastgpt-pr:fatsgpt_sandbox_${{ github.event.pull_request.head.sha }}" >> $GITHUB_OUTPUT + elif [[ "${{ matrix.image }}" == "mcp-server" ]]; then + echo "DOCKERFILE=projects/app/mcp-server.Dockerfile" >> $GITHUB_OUTPUT + echo "DESCRIPTION=fastgpt-mcp-server-pr image" >> $GITHUB_OUTPUT + echo "DOCKER_REPO_TAGGED=ghcr.io/${{ github.repository_owner }}/fastgpt-pr:fatsgpt_mcp_server_${{ github.event.pull_request.head.sha }}" >> $GITHUB_OUTPUT + fi - - name: Build image for PR - env: - DOCKER_REPO_TAGGED: ${{ env.DOCKER_REPO_TAGGED }} + - name: Build ${{ matrix.image }} image for PR run: | docker buildx build \ - -f projects/app/Dockerfile \ + -f ${{ steps.config.outputs.DOCKERFILE }} \ --label "org.opencontainers.image.source=https://github.com/${{ github.repository_owner }}/FastGPT" \ - --label "org.opencontainers.image.description=fastgpt-pr image" \ - --label "org.opencontainers.image.licenses=Apache" \ + --label "org.opencontainers.image.description=${{ steps.config.outputs.DESCRIPTION }}" \ --push \ --cache-from=type=local,src=/tmp/.buildx-cache \ --cache-to=type=local,dest=/tmp/.buildx-cache \ - -t ${DOCKER_REPO_TAGGED} \ + -t ${{ steps.config.outputs.DOCKER_REPO_TAGGED }} \ . + - uses: actions/github-script@v7 with: github-token: ${{secrets.GITHUB_TOKEN}} @@ -67,5 +85,5 @@ jobs: issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, - body: 'Preview Image: `${{ env.DOCKER_REPO_TAGGED }}`' + body: 'Preview ${{ matrix.image }} Image: `${{ steps.config.outputs.DOCKER_REPO_TAGGED }}`' }) diff --git a/.github/workflows/mcp_server-build-image.yml b/.github/workflows/mcp_server-build-image.yml new file mode 100644 index 000000000000..469d51d5daee --- /dev/null +++ b/.github/workflows/mcp_server-build-image.yml @@ -0,0 +1,151 @@ +name: Build fastgpt-mcp-server images +on: + workflow_dispatch: + push: + paths: + - 'projects/sandbox/**' + tags: + - 'v*' +jobs: + build-fastgpt-mcp_server-images: + permissions: + packages: write + contents: read + attestations: write + id-token: write + strategy: + matrix: + include: + - arch: amd64 + - arch: arm64 + runs-on: ubuntu-24.04-arm + runs-on: ${{ matrix.runs-on || 'ubuntu-24.04' }} + steps: + # install env + - name: Checkout + uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + with: + driver-opts: network=host + - name: Cache Docker layers + uses: actions/cache@v4 + with: + path: /tmp/.buildx-cache + key: ${{ runner.os }}-mcp-server-buildx-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-mcp_server-buildx- + + # login docker + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Login to Ali Hub + uses: docker/login-action@v3 + with: + registry: registry.cn-hangzhou.aliyuncs.com + username: ${{ secrets.ALI_HUB_USERNAME }} + password: ${{ secrets.ALI_HUB_PASSWORD }} + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_HUB_NAME }} + password: ${{ secrets.DOCKER_HUB_PASSWORD }} + + - name: Build for ${{ matrix.arch }} + id: build + uses: docker/build-push-action@v6 + with: + context: . + file: projects/mcp_server/Dockerfile + platforms: linux/${{ matrix.arch }} + labels: | + org.opencontainers.image.source=https://github.com/${{ github.repository }} + org.opencontainers.image.description=fastgpt-mcp_server image + outputs: type=image,"name=ghcr.io/${{ github.repository_owner }}/fastgpt-mcp_server,${{ secrets.ALI_IMAGE_NAME }}/fastgpt-mcp_server,${{ secrets.DOCKER_IMAGE_NAME }}/fastgpt-mcp_server",push-by-digest=true,push=true + cache-from: type=local,src=/tmp/.buildx-cache + cache-to: type=local,dest=/tmp/.buildx-cache + + - name: Export digest + run: | + mkdir -p ${{ runner.temp }}/digests + digest="${{ steps.build.outputs.digest }}" + touch "${{ runner.temp }}/digests/${digest#sha256:}" + + - name: Upload digest + uses: actions/upload-artifact@v4 + with: + name: digests-fastgpt-mcp_server-${{ github.sha }}-${{ matrix.arch }} + path: ${{ runner.temp }}/digests/* + if-no-files-found: error + retention-days: 1 + + release-fastgpt-mcp_server-images: + permissions: + packages: write + contents: read + attestations: write + id-token: write + needs: build-fastgpt-mcp_server-images + runs-on: ubuntu-24.04 + steps: + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Login to Ali Hub + uses: docker/login-action@v3 + with: + registry: registry.cn-hangzhou.aliyuncs.com + username: ${{ secrets.ALI_HUB_USERNAME }} + password: ${{ secrets.ALI_HUB_PASSWORD }} + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_HUB_NAME }} + password: ${{ secrets.DOCKER_HUB_PASSWORD }} + + - name: Download digests + uses: actions/download-artifact@v4 + with: + path: ${{ runner.temp }}/digests + pattern: digests-fastgpt-mcp_server-${{ github.sha }}-* + merge-multiple: true + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Set image name and tag + run: | + if [[ "${{ github.ref_name }}" == "main" ]]; then + echo "Git_Tag=ghcr.io/${{ github.repository_owner }}/fastgpt-mcp_server:latest" >> $GITHUB_ENV + echo "Git_Latest=ghcr.io/${{ github.repository_owner }}/fastgpt-mcp_server:latest" >> $GITHUB_ENV + echo "Ali_Tag=${{ secrets.ALI_IMAGE_NAME }}/fastgpt-mcp_server:latest" >> $GITHUB_ENV + echo "Ali_Latest=${{ secrets.ALI_IMAGE_NAME }}/fastgpt-mcp_server:latest" >> $GITHUB_ENV + echo "Docker_Hub_Tag=${{ secrets.DOCKER_IMAGE_NAME }}/fastgpt-mcp_server:latest" >> $GITHUB_ENV + echo "Docker_Hub_Latest=${{ secrets.DOCKER_IMAGE_NAME }}/fastgpt-mcp_server:latest" >> $GITHUB_ENV + else + echo "Git_Tag=ghcr.io/${{ github.repository_owner }}/fastgpt-mcp_server:${{ github.ref_name }}" >> $GITHUB_ENV + echo "Git_Latest=ghcr.io/${{ github.repository_owner }}/fastgpt-mcp_server:latest" >> $GITHUB_ENV + echo "Ali_Tag=${{ secrets.ALI_IMAGE_NAME }}/fastgpt-mcp_server:${{ github.ref_name }}" >> $GITHUB_ENV + echo "Ali_Latest=${{ secrets.ALI_IMAGE_NAME }}/fastgpt-mcp_server:latest" >> $GITHUB_ENV + echo "Docker_Hub_Tag=${{ secrets.DOCKER_IMAGE_NAME }}/fastgpt-mcp_server:${{ github.ref_name }}" >> $GITHUB_ENV + echo "Docker_Hub_Latest=${{ secrets.DOCKER_IMAGE_NAME }}/fastgpt-mcp_server:latest" >> $GITHUB_ENV + fi + + - name: Create manifest list and push + working-directory: ${{ runner.temp }}/digests + run: | + TAGS="$(echo -e "${Git_Tag}\n${Git_Latest}\n${Ali_Tag}\n${Ali_Latest}\n${Docker_Hub_Tag}\n${Docker_Hub_Latest}")" + for TAG in $TAGS; do + docker buildx imagetools create -t $TAG \ + $(printf 'ghcr.io/${{ github.repository_owner }}/fastgpt-mcp_server@sha256:%s ' *) + sleep 5 + done diff --git a/Makefile b/Makefile index e3f708898bcf..8397fdda0ad6 100644 --- a/Makefile +++ b/Makefile @@ -17,7 +17,7 @@ dev: build: ifeq ($(proxy), taobao) - docker build -f $(filePath) -t $(image) . --build-arg proxy=taobao + docker build -f $(filePath) -t $(image) . --build-arg proxy=taobao else ifeq ($(proxy), clash) docker build -f $(filePath) -t $(image) . --network host --build-arg HTTP_PROXY=http://127.0.0.1:7890 --build-arg HTTPS_PROXY=http://127.0.0.1:7890 else diff --git a/docSite/content/zh-cn/docs/development/upgrading/496.md b/docSite/content/zh-cn/docs/development/upgrading/496.md index 7277c413f95a..d2d977d8e0bc 100644 --- a/docSite/content/zh-cn/docs/development/upgrading/496.md +++ b/docSite/content/zh-cn/docs/development/upgrading/496.md @@ -11,8 +11,9 @@ weight: 794 ## 🚀 新增内容 -1. 增加工作台二级菜单,合并工具箱。 -2. 增加 grok3、GPT4.1、Gemini2.5 模型系统配置。 +1. 以 MCP 方式对外提供应用调用。 +2. 增加工作台二级菜单,合并工具箱。 +3. 增加 grok3、GPT4.1、Gemini2.5 模型系统配置。 ## ⚙️ 优化 diff --git a/packages/global/common/error/code/common.ts b/packages/global/common/error/code/common.ts index 38d76dbec009..f4ca49666dee 100644 --- a/packages/global/common/error/code/common.ts +++ b/packages/global/common/error/code/common.ts @@ -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' diff --git a/packages/global/common/error/code/team.ts b/packages/global/common/error/code/team.ts index f60d08ccad43..0d42ee79ac56 100644 --- a/packages/global/common/error/code/team.ts +++ b/packages/global/common/error/code/team.ts @@ -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') diff --git a/packages/global/common/system/types/index.d.ts b/packages/global/common/system/types/index.d.ts index 6349226eaf7f..861ff74d93f7 100644 --- a/packages/global/common/system/types/index.d.ts +++ b/packages/global/common/system/types/index.d.ts @@ -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; diff --git a/packages/global/core/chat/constants.ts b/packages/global/core/chat/constants.ts index 34c6ff1e0ce8..43b18c27b87d 100644 --- a/packages/global/core/chat/constants.ts +++ b/packages/global/core/chat/constants.ts @@ -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') } }; diff --git a/packages/global/support/mcp/type.d.ts b/packages/global/support/mcp/type.d.ts new file mode 100644 index 000000000000..986d32924aae --- /dev/null +++ b/packages/global/support/mcp/type.d.ts @@ -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; +}; diff --git a/packages/global/support/wallet/usage/constants.ts b/packages/global/support/wallet/usage/constants.ts index e2848c7038c6..26486c550b2e 100644 --- a/packages/global/support/wallet/usage/constants.ts +++ b/packages/global/support/wallet/usage/constants.ts @@ -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') } }; diff --git a/packages/service/core/app/controller.ts b/packages/service/core/app/controller.ts index 02cdd08f4880..4ca1e7e10f9d 100644 --- a/packages/service/core/app/controller.ts +++ b/packages/service/core/app/controller.ts @@ -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 + })); +}; diff --git a/packages/service/core/workflow/dispatch/init/workflowStart.tsx b/packages/service/core/workflow/dispatch/init/workflowStart.tsx index 73c2704a284c..4e7300029c0e 100644 --- a/packages/service/core/workflow/dispatch/init/workflowStart.tsx +++ b/packages/service/core/workflow/dispatch/init/workflowStart.tsx @@ -17,19 +17,25 @@ type Response = DispatchNodeResultType<{ export const dispatchWorkflowStart = (props: Record): 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 }; }; diff --git a/packages/service/core/workflow/dispatch/utils.ts b/packages/service/core/workflow/dispatch/utils.ts index 5d8b8fda9616..e10301d131a3 100644 --- a/packages/service/core/workflow/dispatch/utils.ts +++ b/packages/service/core/workflow/dispatch/utils.ts @@ -118,6 +118,7 @@ export const valueTypeFormat = (value: any, type?: WorkflowIOValueTypeEnum) => { }; // 1. any值,忽略格式化 + if (value === undefined || value === null) return value; if (!type || type === WorkflowIOValueTypeEnum.any) return value; // 2. 如果值已经符合目标类型,直接返回 @@ -135,19 +136,6 @@ export const valueTypeFormat = (value: any, type?: WorkflowIOValueTypeEnum) => { return value; } - // 3. 空值处理 - if (value === undefined || value === null) { - if (type === WorkflowIOValueTypeEnum.string) return ''; - if (type === WorkflowIOValueTypeEnum.number) return 0; - if (type === WorkflowIOValueTypeEnum.boolean) return false; - if (type.startsWith('array')) return []; - if (type === WorkflowIOValueTypeEnum.object) return {}; - if (type === WorkflowIOValueTypeEnum.chatHistory) return []; - if (type === WorkflowIOValueTypeEnum.datasetQuote) return []; - if (type === WorkflowIOValueTypeEnum.selectDataset) return []; - if (type === WorkflowIOValueTypeEnum.selectApp) return {}; - } - // 4. 按目标类型,进行格式转化 // 4.1 基本类型转换 if (type === WorkflowIOValueTypeEnum.string) { diff --git a/packages/service/support/mcp/schema.ts b/packages/service/support/mcp/schema.ts new file mode 100644 index 000000000000..4b8281f5257c --- /dev/null +++ b/packages/service/support/mcp/schema.ts @@ -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(mcpCollectionName, McpKeySchema); diff --git a/packages/service/support/permission/mcp/auth.ts b/packages/service/support/permission/mcp/auth.ts new file mode 100644 index 000000000000..0fbb010e543b --- /dev/null +++ b/packages/service/support/permission/mcp/auth.ts @@ -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 + }; +}; diff --git a/packages/web/components/common/MyPopover/PopoverConfirm.tsx b/packages/web/components/common/MyPopover/PopoverConfirm.tsx index 0be921592665..c68d2130d927 100644 --- a/packages/web/components/common/MyPopover/PopoverConfirm.tsx +++ b/packages/web/components/common/MyPopover/PopoverConfirm.tsx @@ -74,6 +74,7 @@ const PopoverConfirm = ({ isLazy lazyBehavior="keepMounted" arrowSize={10} + strategy={'fixed'} > {Trigger} diff --git a/packages/web/i18n/en/account_usage.json b/packages/web/i18n/en/account_usage.json index 459007293349..01f5e0a7300d 100644 --- a/packages/web/i18n/en/account_usage.json +++ b/packages/web/i18n/en/account_usage.json @@ -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", diff --git a/packages/web/i18n/en/common.json b/packages/web/i18n/en/common.json index 33947f429ff7..8858a0ec57a7 100644 --- a/packages/web/i18n/en/common.json +++ b/packages/web/i18n/en/common.json @@ -101,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", @@ -449,6 +449,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", @@ -901,7 +902,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", @@ -931,6 +934,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", @@ -1213,6 +1217,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", diff --git a/packages/web/i18n/en/dashboard_mcp.json b/packages/web/i18n/en/dashboard_mcp.json new file mode 100644 index 000000000000..9b140d756b9b --- /dev/null +++ b/packages/web/i18n/en/dashboard_mcp.json @@ -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" +} diff --git a/packages/web/i18n/zh-CN/account_usage.json b/packages/web/i18n/zh-CN/account_usage.json index 9255688570fc..13b6a115153e 100644 --- a/packages/web/i18n/zh-CN/account_usage.json +++ b/packages/web/i18n/zh-CN/account_usage.json @@ -22,6 +22,7 @@ "generation_time": "生成时间", "image_parse": "图片标注", "input_token_length": "输入 tokens", + "mcp": "MCP 调用", "member": "成员", "member_name": "成员名", "module_name": "模块名", diff --git a/packages/web/i18n/zh-CN/common.json b/packages/web/i18n/zh-CN/common.json index 9520845f0302..af5051daa471 100644 --- a/packages/web/i18n/zh-CN/common.json +++ b/packages/web/i18n/zh-CN/common.json @@ -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": "您的有效邀请链接数已达上限,请先清理链接", @@ -448,6 +448,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": "外部链接调用", @@ -900,7 +901,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": "字段名", @@ -930,6 +933,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": "模型计费", @@ -1212,6 +1216,7 @@ "system.Help Document": "帮助文档", "tag_list": "标签列表", "team_tag": "团队标签", + "template_market": "模板市场", "textarea_variable_picker_tip": "输入\"/\"可选择变量", "unauth_token": "凭证已过期,请重新登录", "unit.character": "字符", diff --git a/packages/web/i18n/zh-CN/dashboard_mcp.json b/packages/web/i18n/zh-CN/dashboard_mcp.json new file mode 100644 index 000000000000..f2fdf28af934 --- /dev/null +++ b/packages/web/i18n/zh-CN/dashboard_mcp.json @@ -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 服务使用" +} diff --git a/packages/web/i18n/zh-Hant/account_usage.json b/packages/web/i18n/zh-Hant/account_usage.json index 893d8cc2d12b..4f0fa5f28135 100644 --- a/packages/web/i18n/zh-Hant/account_usage.json +++ b/packages/web/i18n/zh-Hant/account_usage.json @@ -20,6 +20,7 @@ "generation_time": "生成時間", "image_parse": "圖片標註", "input_token_length": "輸入 tokens", + "mcp": "MCP 調用", "member": "成員", "member_name": "成員名", "module_name": "模組名", diff --git a/packages/web/i18n/zh-Hant/common.json b/packages/web/i18n/zh-Hant/common.json index 0c5b121c7281..34ddea030610 100644 --- a/packages/web/i18n/zh-Hant/common.json +++ b/packages/web/i18n/zh-Hant/common.json @@ -100,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": "您的有效邀請連結數已達上限,請先清理連結", @@ -448,6 +448,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": "外部連結呼叫", @@ -901,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": "欄位名稱", @@ -931,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": "不同索引模型的相關度有區別,請透過搜尋測試來選擇合適的數值。\n使用 結果重排 時,使用重排結果過濾。", "model.billing": "模型計費", @@ -1212,6 +1216,7 @@ "system.Help Document": "說明文件", "tag_list": "標籤列表", "team_tag": "團隊標籤", + "template_market": "模板市場", "textarea_variable_picker_tip": "輸入「/」以選擇變數", "unauth_token": "憑證已過期,請重新登入", "unit.character": "字元", diff --git a/packages/web/i18n/zh-Hant/dashboard_mcp.json b/packages/web/i18n/zh-Hant/dashboard_mcp.json new file mode 100644 index 000000000000..a5fc386d2119 --- /dev/null +++ b/packages/web/i18n/zh-Hant/dashboard_mcp.json @@ -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 服務使用" +} diff --git a/packages/web/types/i18next.d.ts b/packages/web/types/i18next.d.ts index 629568740a53..cf337f727c6c 100644 --- a/packages/web/types/i18next.d.ts +++ b/packages/web/types/i18next.d.ts @@ -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; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e124521918f7..44e674954da4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -483,6 +483,9 @@ importers: '@fortaine/fetch-event-source': specifier: ^3.0.6 version: 3.0.6 + '@modelcontextprotocol/sdk': + specifier: ^1.9.0 + version: 1.9.0 '@node-rs/jieba': specifier: 2.0.1 version: 2.0.1 @@ -609,6 +612,9 @@ importers: use-context-selector: specifier: ^1.4.4 version: 1.4.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(scheduler@0.23.2) + zod: + specifier: ^3.24.2 + version: 3.24.2 devDependencies: '@svgr/webpack': specifier: ^6.5.1 @@ -659,6 +665,40 @@ importers: specifier: ^3.0.2 version: 3.0.8(@types/debug@4.1.12)(@types/node@20.17.24)(sass@1.85.1)(terser@5.39.0) + projects/mcp_server: + dependencies: + '@modelcontextprotocol/sdk': + specifier: 1.9.0 + version: 1.9.0 + axios: + specifier: ^1.8.2 + version: 1.8.4 + chalk: + specifier: ^5.3.0 + version: 5.4.1 + dayjs: + specifier: ^1.11.7 + version: 1.11.13 + dotenv: + specifier: ^16.5.0 + version: 16.5.0 + express: + specifier: ^4.21.2 + version: 4.21.2 + devDependencies: + '@types/express': + specifier: ^5.0.1 + version: 5.0.1 + nodemon: + specifier: ^3.1.9 + version: 3.1.9 + shx: + specifier: ^0.3.4 + version: 0.3.4 + typescript: + specifier: ^5.6.2 + version: 5.8.2 + projects/sandbox: dependencies: '@fastify/static': @@ -2309,6 +2349,10 @@ packages: '@mixmark-io/domino@2.2.0': resolution: {integrity: sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==} + '@modelcontextprotocol/sdk@1.9.0': + resolution: {integrity: sha512-Jq2EUCQpe0iyO5FGpzVYDNFR6oR53AIrwph9yWl7uSc7IWUMsrmpmSaTGra5hQNunXpM+9oit85p924jWuHzUA==} + engines: {node: '>=18'} + '@monaco-editor/loader@1.5.0': resolution: {integrity: sha512-hKoGSM+7aAc7eRTRjpqAZucPmoNOC4UUbknb/VNoTkEIkCPhqV8LfbsgM1webRM7S/z21eHEx9Fkwx8Z/C/+Xw==} @@ -3316,6 +3360,9 @@ packages: '@types/express@5.0.0': resolution: {integrity: sha512-DvZriSMehGHL1ZNLzi6MidnsDhUZM/x2pRdDIKdwbUNqqwHxMlRdkxtn6/EPKyqKpHqTl/4nRZsRNLpZxZRpPQ==} + '@types/express@5.0.1': + resolution: {integrity: sha512-UZUw8vjpWFXuDnjFTh7/5c2TWDlQqeXHi6hcN7F2XSVT5P+WmUnnbFS3KA6Jnc6IsEqI2qCVu2bK0R0J4A8ZQQ==} + '@types/formidable@2.0.6': resolution: {integrity: sha512-L4HcrA05IgQyNYJj6kItuIkXrInJvsXTPC5B1i64FggWKKqSL+4hgt7asiSNva75AoLQjq29oPxFfU4GAQ6Z2w==} @@ -3759,6 +3806,10 @@ packages: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -3987,6 +4038,9 @@ packages: axios@1.8.3: resolution: {integrity: sha512-iP4DebzoNlP/YN2dpwCgb8zoCmhtkajzS48JvwmkSkXvPI3DHc7m+XYL5tGnSlJtR6nImXZmdCuN5aP8dh1d8A==} + axios@1.8.4: + resolution: {integrity: sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==} + axobject-query@4.1.0: resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} engines: {node: '>= 0.4'} @@ -4076,6 +4130,10 @@ packages: resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + body-parser@2.2.0: + resolution: {integrity: sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==} + engines: {node: '>=18'} + boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} @@ -4475,6 +4533,10 @@ packages: resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} engines: {node: '>= 0.6'} + content-disposition@1.0.0: + resolution: {integrity: sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==} + engines: {node: '>= 0.6'} + content-type@1.0.5: resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} engines: {node: '>= 0.6'} @@ -4488,6 +4550,10 @@ packages: cookie-signature@1.0.6: resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + cookie@0.7.1: resolution: {integrity: sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==} engines: {node: '>= 0.6'} @@ -4995,6 +5061,10 @@ packages: resolution: {integrity: sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==} engines: {node: '>=12'} + dotenv@16.5.0: + resolution: {integrity: sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==} + engines: {node: '>=12'} + duck-duck-scrape@2.2.7: resolution: {integrity: sha512-BEcglwnfx5puJl90KQfX+Q2q5vCguqyMpZcSRPBWk8OY55qWwV93+E+7DbIkrGDW4qkqPfUvtOUdi0lXz6lEMQ==} @@ -5304,6 +5374,14 @@ packages: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} + eventsource-parser@3.0.1: + resolution: {integrity: sha512-VARTJ9CYeuQYb0pZEPbzi740OWFgpHe7AYJ2WFZVnUDUQp5Dk2yJUgF36YsZ81cOyxT0QxmXD2EQpapAouzWVA==} + engines: {node: '>=18.0.0'} + + eventsource@3.0.6: + resolution: {integrity: sha512-l19WpE2m9hSuyP06+FbuUUf1G+R0SFLrtQfbRb9PRr+oimOfxQhgGCbVaXg5IvZyyTThJsxh6L/srkMiCeBPDA==} + engines: {node: '>=18.0.0'} + execa@5.1.1: resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} engines: {node: '>=10'} @@ -5338,10 +5416,20 @@ packages: expr-eval@2.0.2: resolution: {integrity: sha512-4EMSHGOPSwAfBiibw3ndnP0AvjDWLsMvGOvWEZ2F96IGk0bIVdjQisOHxReSkE13mHcfbuCiXw+G4y0zv6N8Eg==} + express-rate-limit@7.5.0: + resolution: {integrity: sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==} + engines: {node: '>= 16'} + peerDependencies: + express: ^4.11 || 5 || ^5.0.0-beta.1 + express@4.21.2: resolution: {integrity: sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==} engines: {node: '>= 0.10.0'} + express@5.1.0: + resolution: {integrity: sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==} + engines: {node: '>= 18'} + extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} @@ -5461,6 +5549,10 @@ packages: resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==} engines: {node: '>= 0.8'} + finalhandler@2.1.0: + resolution: {integrity: sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==} + engines: {node: '>= 0.8'} + find-cache-dir@3.3.2: resolution: {integrity: sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==} engines: {node: '>=8'} @@ -5557,6 +5649,10 @@ packages: resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} engines: {node: '>= 0.6'} + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + fs-constants@1.0.0: resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} @@ -5880,6 +5976,9 @@ packages: ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + ignore-by-default@1.0.1: + resolution: {integrity: sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==} + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -5953,6 +6052,10 @@ packages: resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} engines: {node: '>=12'} + interpret@1.4.0: + resolution: {integrity: sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==} + engines: {node: '>= 0.10'} + intersection-observer@0.12.2: resolution: {integrity: sha512-7m1vEcPCxXYI8HqnL8CKI6siDyD+eIWSwgB3DZA+ZTogxk9I4CDnj4wilt9x/+/QbHI4YG5YZNmC6458/e9Ktg==} @@ -6137,6 +6240,9 @@ packages: resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} engines: {node: '>=12'} + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + is-property@1.0.2: resolution: {integrity: sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==} @@ -6868,6 +6974,10 @@ packages: resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} engines: {node: '>= 0.6'} + media-typer@1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} + memfs@3.5.3: resolution: {integrity: sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==} engines: {node: '>= 4.0.0'} @@ -6881,6 +6991,10 @@ packages: merge-descriptors@1.0.3: resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} @@ -7057,10 +7171,18 @@ packages: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} engines: {node: '>= 0.6'} + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + mime-types@2.1.35: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} + mime-types@3.0.1: + resolution: {integrity: sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==} + engines: {node: '>= 0.6'} + mime@1.6.0: resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} engines: {node: '>=4'} @@ -7293,6 +7415,10 @@ packages: resolution: {integrity: sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==} engines: {node: '>= 0.6'} + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} @@ -7390,6 +7516,11 @@ packages: resolution: {integrity: sha512-SQ3wZCExjeSatLE/HBaXS5vqUOQk6GtBdIIKxiFdmm01mOQZX/POJkO3SUX1wDiYcwUOJwT23scFSC9fY2H8IA==} engines: {node: '>=6.0.0'} + nodemon@3.1.9: + resolution: {integrity: sha512-hdr1oIb2p6ZSxu3PB2JWWYS7ZQ0qvaZsc3hK8DR8f02kRzc8rjYmxAIvdz+aYC+8F2IjNaB7HMcSDg8nQpJxyg==} + engines: {node: '>=10'} + hasBin: true + non-layered-tidy-tree-layout@2.0.2: resolution: {integrity: sha512-gkXMxRzUH+PB0ax9dUN0yYF0S25BqeAYqhgMaLUFmpXLEk7Fcu8f4emJuOAY0V8kjDICxROIKsTAKsV/v355xw==} @@ -7638,6 +7769,10 @@ packages: path-to-regexp@6.3.0: resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} + path-to-regexp@8.2.0: + resolution: {integrity: sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==} + engines: {node: '>=16'} + path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} @@ -7758,6 +7893,10 @@ packages: resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} engines: {node: '>= 6'} + pkce-challenge@5.0.0: + resolution: {integrity: sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==} + engines: {node: '>=16.20.0'} + pkg-dir@4.2.0: resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} engines: {node: '>=8'} @@ -7894,6 +8033,9 @@ packages: proxy-from-env@1.1.0: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + pstree.remy@1.1.8: + resolution: {integrity: sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==} + pump@3.0.2: resolution: {integrity: sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==} @@ -7945,6 +8087,10 @@ packages: resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} engines: {node: '>= 0.8'} + raw-body@3.0.0: + resolution: {integrity: sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==} + engines: {node: '>= 0.8'} + rc@1.2.8: resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} hasBin: true @@ -8138,6 +8284,10 @@ packages: react: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + rechoir@0.6.2: + resolution: {integrity: sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==} + engines: {node: '>= 0.10'} + redis-errors@1.2.0: resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} engines: {node: '>=4'} @@ -8317,6 +8467,10 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + run-applescript@7.0.0: resolution: {integrity: sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==} engines: {node: '>=18'} @@ -8420,6 +8574,10 @@ packages: resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==} engines: {node: '>= 0.8.0'} + send@1.2.0: + resolution: {integrity: sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==} + engines: {node: '>= 18'} + seq-queue@0.0.5: resolution: {integrity: sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==} @@ -8430,6 +8588,10 @@ packages: resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==} engines: {node: '>= 0.8.0'} + serve-static@2.2.0: + resolution: {integrity: sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==} + engines: {node: '>= 18'} + set-blocking@2.0.0: resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} @@ -8462,6 +8624,16 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} + shelljs@0.8.5: + resolution: {integrity: sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==} + engines: {node: '>=4'} + hasBin: true + + shx@0.3.4: + resolution: {integrity: sha512-N6A9MLVqjxZYcVn8hLmtneQWIJtp8IKzMP4eMnx+nqkvXoqinUPCbUFLp2UcWTEIUONhlk0ewxr/jaVGlc+J+g==} + engines: {node: '>=6'} + hasBin: true + side-channel-list@1.0.0: resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} engines: {node: '>= 0.4'} @@ -8500,6 +8672,10 @@ packages: simple-swizzle@0.2.2: resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} + simple-update-notifier@2.0.0: + resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==} + engines: {node: '>=10'} + sisteransi@1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} @@ -8924,6 +9100,10 @@ packages: resolution: {integrity: sha512-lbDrTLVsHhOMljPscd0yitpozq7Ga2M5Cvez5AjGg8GASBjtt6iERCAJ93yommPmz62fb45oFIXHEZ3u9bfJEA==} engines: {node: '>=14.16'} + touch@3.1.1: + resolution: {integrity: sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==} + hasBin: true + tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} @@ -9072,6 +9252,10 @@ packages: resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} engines: {node: '>= 0.6'} + type-is@2.0.1: + resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} + engines: {node: '>= 0.6'} + typed-array-buffer@1.0.3: resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} engines: {node: '>= 0.4'} @@ -9122,6 +9306,9 @@ packages: unbzip2-stream@1.4.3: resolution: {integrity: sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==} + undefsafe@2.0.5: + resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==} + underscore@1.13.7: resolution: {integrity: sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==} @@ -9739,6 +9926,11 @@ packages: resolution: {integrity: sha512-E1rA6TyQJ1cWWfMoM8KE1hMdDDi5B8Gv+8OYPXe733Lf0C3EwJ+jh1cpoK/KTrYeITumRZQ0KSPkBRMNZuC8oA==} hasBin: true + zod-to-json-schema@3.24.5: + resolution: {integrity: sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==} + peerDependencies: + zod: ^3.24.1 + zod-validation-error@3.4.0: resolution: {integrity: sha512-ZOPR9SVY6Pb2qqO5XHt+MkkTRxGXb4EVtnjc9JpXUOtUB1T9Ru7mZOT361AN3MsetVe7R0a1KZshJDZdgp9miQ==} engines: {node: '>=18.0.0'} @@ -9981,7 +10173,7 @@ snapshots: '@babel/traverse': 7.26.10 '@babel/types': 7.26.10 convert-source-map: 2.0.0 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -10033,7 +10225,7 @@ snapshots: '@babel/core': 7.26.10 '@babel/helper-compilation-targets': 7.26.5 '@babel/helper-plugin-utils': 7.26.5 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) lodash.debounce: 4.0.8 resolve: 1.22.10 transitivePeerDependencies: @@ -10737,7 +10929,7 @@ snapshots: '@babel/parser': 7.26.10 '@babel/template': 7.26.9 '@babel/types': 7.26.10 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -11197,7 +11389,7 @@ snapshots: '@eslint/eslintrc@2.1.4': dependencies: ajv: 6.12.6 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) espree: 9.6.1 globals: 13.24.0 ignore: 5.3.2 @@ -11283,7 +11475,7 @@ snapshots: '@humanwhocodes/config-array@0.11.14': dependencies: '@humanwhocodes/object-schema': 2.0.3 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -11668,6 +11860,21 @@ snapshots: '@mixmark-io/domino@2.2.0': {} + '@modelcontextprotocol/sdk@1.9.0': + dependencies: + content-type: 1.0.5 + cors: 2.8.5 + cross-spawn: 7.0.6 + eventsource: 3.0.6 + express: 5.1.0 + express-rate-limit: 7.5.0(express@5.1.0) + pkce-challenge: 5.0.0 + raw-body: 3.0.0 + zod: 3.24.2 + zod-to-json-schema: 3.24.5(zod@3.24.2) + transitivePeerDependencies: + - supports-color + '@monaco-editor/loader@1.5.0': dependencies: state-local: 1.0.7 @@ -12602,6 +12809,12 @@ snapshots: '@types/qs': 6.9.18 '@types/serve-static': 1.15.7 + '@types/express@5.0.1': + dependencies: + '@types/body-parser': 1.19.5 + '@types/express-serve-static-core': 5.0.6 + '@types/serve-static': 1.15.7 + '@types/formidable@2.0.6': dependencies: '@types/node': 20.17.24 @@ -12826,7 +13039,7 @@ snapshots: '@typescript-eslint/type-utils': 6.21.0(eslint@8.56.0)(typescript@5.8.2) '@typescript-eslint/utils': 6.21.0(eslint@8.56.0)(typescript@5.8.2) '@typescript-eslint/visitor-keys': 6.21.0 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) eslint: 8.56.0 graphemer: 1.4.0 ignore: 5.3.2 @@ -12844,7 +13057,7 @@ snapshots: '@typescript-eslint/types': 6.21.0 '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.8.2) '@typescript-eslint/visitor-keys': 6.21.0 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) eslint: 8.56.0 optionalDependencies: typescript: 5.8.2 @@ -12860,7 +13073,7 @@ snapshots: dependencies: '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.8.2) '@typescript-eslint/utils': 6.21.0(eslint@8.56.0)(typescript@5.8.2) - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) eslint: 8.56.0 ts-api-utils: 1.4.3(typescript@5.8.2) optionalDependencies: @@ -12874,7 +13087,7 @@ snapshots: dependencies: '@typescript-eslint/types': 6.21.0 '@typescript-eslint/visitor-keys': 6.21.0 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) globby: 11.1.0 is-glob: 4.0.3 minimatch: 9.0.3 @@ -12910,7 +13123,7 @@ snapshots: dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 1.0.2 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) istanbul-lib-coverage: 3.2.2 istanbul-lib-report: 3.0.1 istanbul-lib-source-maps: 5.0.6 @@ -13201,6 +13414,11 @@ snapshots: mime-types: 2.1.35 negotiator: 0.6.3 + accepts@2.0.0: + dependencies: + mime-types: 3.0.1 + negotiator: 1.0.0 + acorn-jsx@5.3.2(acorn@8.14.1): dependencies: acorn: 8.14.1 @@ -13444,6 +13662,14 @@ snapshots: transitivePeerDependencies: - debug + axios@1.8.4: + dependencies: + follow-redirects: 1.15.9(debug@4.4.0) + form-data: 4.0.2 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + axobject-query@4.1.0: {} b4a@1.6.7: {} @@ -13589,6 +13815,20 @@ snapshots: transitivePeerDependencies: - supports-color + body-parser@2.2.0: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 4.4.0(supports-color@5.5.0) + http-errors: 2.0.0 + iconv-lite: 0.6.3 + on-finished: 2.4.1 + qs: 6.14.0 + raw-body: 3.0.0 + type-is: 2.0.1 + transitivePeerDependencies: + - supports-color + boolbase@1.0.0: {} boxen@7.1.1: @@ -14017,6 +14257,10 @@ snapshots: dependencies: safe-buffer: 5.2.1 + content-disposition@1.0.0: + dependencies: + safe-buffer: 5.2.1 + content-type@1.0.5: {} convert-source-map@1.9.0: {} @@ -14025,6 +14269,8 @@ snapshots: cookie-signature@1.0.6: {} + cookie-signature@1.2.2: {} + cookie@0.7.1: {} cookie@0.7.2: {} @@ -14358,9 +14604,11 @@ snapshots: dependencies: ms: 2.1.2 - debug@4.4.0: + debug@4.4.0(supports-color@5.5.0): dependencies: ms: 2.1.3 + optionalDependencies: + supports-color: 5.5.0 decamelize@1.2.0: {} @@ -14560,6 +14808,8 @@ snapshots: dotenv@16.4.7: {} + dotenv@16.5.0: {} + duck-duck-scrape@2.2.7: dependencies: html-entities: 2.5.2 @@ -14817,8 +15067,8 @@ snapshots: '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.8.2) eslint: 8.56.0 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.9.0(eslint-plugin-import@2.31.0)(eslint@8.56.0) - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.2))(eslint-import-resolver-typescript@3.9.0)(eslint@8.56.0) + eslint-import-resolver-typescript: 3.9.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.2))(eslint@8.56.0))(eslint@8.56.0) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.2))(eslint-import-resolver-typescript@3.9.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.2))(eslint@8.56.0))(eslint@8.56.0))(eslint@8.56.0) eslint-plugin-jsx-a11y: 6.10.2(eslint@8.56.0) eslint-plugin-react: 7.37.4(eslint@8.56.0) eslint-plugin-react-hooks: 5.0.0-canary-7118f5dd7-20230705(eslint@8.56.0) @@ -14837,10 +15087,10 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.9.0(eslint-plugin-import@2.31.0)(eslint@8.56.0): + eslint-import-resolver-typescript@3.9.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.2))(eslint@8.56.0))(eslint@8.56.0): dependencies: '@nolyfill/is-core-module': 1.0.39 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) eslint: 8.56.0 get-tsconfig: 4.10.0 is-bun-module: 1.3.0 @@ -14848,22 +15098,22 @@ snapshots: stable-hash: 0.0.5 tinyglobby: 0.2.12 optionalDependencies: - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.2))(eslint-import-resolver-typescript@3.9.0)(eslint@8.56.0) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.2))(eslint-import-resolver-typescript@3.9.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.2))(eslint@8.56.0))(eslint@8.56.0))(eslint@8.56.0) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.9.0)(eslint@8.56.0): + eslint-module-utils@2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.9.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.2))(eslint@8.56.0))(eslint@8.56.0))(eslint@8.56.0): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.8.2) eslint: 8.56.0 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.9.0(eslint-plugin-import@2.31.0)(eslint@8.56.0) + eslint-import-resolver-typescript: 3.9.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.2))(eslint@8.56.0))(eslint@8.56.0) transitivePeerDependencies: - supports-color - eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.2))(eslint-import-resolver-typescript@3.9.0)(eslint@8.56.0): + eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.2))(eslint-import-resolver-typescript@3.9.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.2))(eslint@8.56.0))(eslint@8.56.0))(eslint@8.56.0): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.8 @@ -14874,7 +15124,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.56.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.9.0)(eslint@8.56.0) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.9.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.2))(eslint@8.56.0))(eslint@8.56.0))(eslint@8.56.0) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -14962,7 +15212,7 @@ snapshots: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.6 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) doctrine: 3.0.0 escape-string-regexp: 4.0.0 eslint-scope: 7.2.2 @@ -15032,6 +15282,12 @@ snapshots: events@3.3.0: {} + eventsource-parser@3.0.1: {} + + eventsource@3.0.6: + dependencies: + eventsource-parser: 3.0.1 + execa@5.1.1: dependencies: cross-spawn: 7.0.6 @@ -15086,6 +15342,10 @@ snapshots: expr-eval@2.0.2: {} + express-rate-limit@7.5.0(express@5.1.0): + dependencies: + express: 5.1.0 + express@4.21.2: dependencies: accepts: 1.3.8 @@ -15122,6 +15382,38 @@ snapshots: transitivePeerDependencies: - supports-color + express@5.1.0: + dependencies: + accepts: 2.0.0 + body-parser: 2.2.0 + content-disposition: 1.0.0 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.0(supports-color@5.5.0) + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.0 + fresh: 2.0.0 + http-errors: 2.0.0 + merge-descriptors: 2.0.0 + mime-types: 3.0.1 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.14.0 + range-parser: 1.2.1 + router: 2.2.0 + send: 1.2.0 + serve-static: 2.2.0 + statuses: 2.0.1 + type-is: 2.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + extend@3.0.2: {} external-editor@3.1.0: @@ -15277,6 +15569,17 @@ snapshots: transitivePeerDependencies: - supports-color + finalhandler@2.1.0: + dependencies: + debug: 4.4.0(supports-color@5.5.0) + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.1 + transitivePeerDependencies: + - supports-color + find-cache-dir@3.3.2: dependencies: commondir: 1.0.1 @@ -15317,7 +15620,7 @@ snapshots: follow-redirects@1.15.9(debug@4.4.0): optionalDependencies: - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) for-each@0.3.5: dependencies: @@ -15386,6 +15689,8 @@ snapshots: fresh@0.5.2: {} + fresh@2.0.0: {} + fs-constants@1.0.0: {} fs-extra@10.1.0: @@ -15722,7 +16027,7 @@ snapshots: http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.3 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) transitivePeerDependencies: - supports-color @@ -15734,7 +16039,7 @@ snapshots: https-proxy-agent@7.0.6: dependencies: agent-base: 7.1.3 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) transitivePeerDependencies: - supports-color @@ -15770,6 +16075,8 @@ snapshots: ieee754@1.2.1: {} + ignore-by-default@1.0.1: {} + ignore@5.3.2: {} immediate@3.0.6: {} @@ -15857,13 +16164,15 @@ snapshots: internmap@2.0.3: {} + interpret@1.4.0: {} + intersection-observer@0.12.2: {} ioredis@5.6.0: dependencies: '@ioredis/commands': 1.2.0 cluster-key-slot: 1.1.2 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) denque: 2.1.0 lodash.defaults: 4.2.0 lodash.isarguments: 3.1.0 @@ -16023,6 +16332,8 @@ snapshots: is-plain-obj@4.1.0: {} + is-promise@4.0.0: {} + is-property@1.0.2: {} is-regex@1.2.1: @@ -16132,7 +16443,7 @@ snapshots: istanbul-lib-source-maps@4.0.1: dependencies: - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) istanbul-lib-coverage: 3.2.2 source-map: 0.6.1 transitivePeerDependencies: @@ -16141,7 +16452,7 @@ snapshots: istanbul-lib-source-maps@5.0.6: dependencies: '@jridgewell/trace-mapping': 0.3.25 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) istanbul-lib-coverage: 3.2.2 transitivePeerDependencies: - supports-color @@ -17096,6 +17407,8 @@ snapshots: media-typer@0.3.0: {} + media-typer@1.1.0: {} + memfs@3.5.3: dependencies: fs-monkey: 1.0.6 @@ -17106,6 +17419,8 @@ snapshots: merge-descriptors@1.0.3: {} + merge-descriptors@2.0.0: {} + merge-stream@2.0.0: {} merge2@1.4.1: {} @@ -17430,7 +17745,7 @@ snapshots: micromark@3.2.0: dependencies: '@types/debug': 4.1.12 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) decode-named-character-reference: 1.1.0 micromark-core-commonmark: 1.1.0 micromark-factory-space: 1.1.0 @@ -17452,7 +17767,7 @@ snapshots: micromark@4.0.2: dependencies: '@types/debug': 4.1.12 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) decode-named-character-reference: 1.1.0 devlop: 1.1.0 micromark-core-commonmark: 2.0.3 @@ -17483,10 +17798,16 @@ snapshots: mime-db@1.52.0: {} + mime-db@1.54.0: {} + mime-types@2.1.35: dependencies: mime-db: 1.52.0 + mime-types@3.0.1: + dependencies: + mime-db: 1.54.0 + mime@1.6.0: {} mime@2.6.0: {} @@ -17586,7 +17907,7 @@ snapshots: dependencies: async-mutex: 0.5.0 camelcase: 6.3.0 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) find-cache-dir: 3.3.2 follow-redirects: 1.15.9(debug@4.4.0) https-proxy-agent: 7.0.6 @@ -17651,7 +17972,7 @@ snapshots: mquery@5.0.0: dependencies: - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) transitivePeerDependencies: - supports-color @@ -17683,7 +18004,7 @@ snapshots: dependencies: '@tediousjs/connection-string': 0.5.0 commander: 11.1.0 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) rfdc: 1.4.1 tarn: 3.0.2 tedious: 18.6.1 @@ -17739,11 +18060,13 @@ snapshots: negotiator@0.6.4: {} + negotiator@1.0.0: {} + neo-async@2.6.2: {} new-find-package-json@2.0.0: dependencies: - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) transitivePeerDependencies: - supports-color @@ -17849,6 +18172,19 @@ snapshots: nodemailer@6.10.0: {} + nodemon@3.1.9: + dependencies: + chokidar: 3.6.0 + debug: 4.4.0(supports-color@5.5.0) + ignore-by-default: 1.0.1 + minimatch: 3.1.2 + pstree.remy: 1.1.8 + semver: 7.7.1 + simple-update-notifier: 2.0.0 + supports-color: 5.5.0 + touch: 3.1.1 + undefsafe: 2.0.5 + non-layered-tidy-tree-layout@2.0.2: {} nopt@7.2.1: @@ -18149,6 +18485,8 @@ snapshots: path-to-regexp@6.3.0: {} + path-to-regexp@8.2.0: {} + path-type@4.0.0: {} pathe@1.1.2: {} @@ -18256,6 +18594,8 @@ snapshots: pirates@4.0.6: {} + pkce-challenge@5.0.0: {} + pkg-dir@4.2.0: dependencies: find-up: 4.1.0 @@ -18391,6 +18731,8 @@ snapshots: proxy-from-env@1.1.0: {} + pstree.remy@1.1.8: {} + pump@3.0.2: dependencies: end-of-stream: 1.4.4 @@ -18439,6 +18781,13 @@ snapshots: iconv-lite: 0.4.24 unpipe: 1.0.0 + raw-body@3.0.0: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.6.3 + unpipe: 1.0.0 + rc@1.2.8: dependencies: deep-extend: 0.6.0 @@ -18679,6 +19028,10 @@ snapshots: tiny-invariant: 1.3.3 victory-vendor: 36.9.2 + rechoir@0.6.2: + dependencies: + resolve: 1.22.10 + redis-errors@1.2.0: {} redis-parser@3.0.0: @@ -18934,6 +19287,16 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.35.0 fsevents: 2.3.3 + router@2.2.0: + dependencies: + debug: 4.4.0(supports-color@5.5.0) + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.2.0 + transitivePeerDependencies: + - supports-color + run-applescript@7.0.0: {} run-async@2.4.1: {} @@ -19050,6 +19413,22 @@ snapshots: transitivePeerDependencies: - supports-color + send@1.2.0: + dependencies: + debug: 4.4.0(supports-color@5.5.0) + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.0 + mime-types: 3.0.1 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.1 + transitivePeerDependencies: + - supports-color + seq-queue@0.0.5: {} serialize-javascript@6.0.2: @@ -19065,6 +19444,15 @@ snapshots: transitivePeerDependencies: - supports-color + serve-static@2.2.0: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.0 + transitivePeerDependencies: + - supports-color + set-blocking@2.0.0: {} set-cookie-parser@2.7.1: {} @@ -19101,6 +19489,17 @@ snapshots: shebang-regex@3.0.0: {} + shelljs@0.8.5: + dependencies: + glob: 7.2.3 + interpret: 1.4.0 + rechoir: 0.6.2 + + shx@0.3.4: + dependencies: + minimist: 1.2.8 + shelljs: 0.8.5 + side-channel-list@1.0.0: dependencies: es-errors: 1.3.0 @@ -19149,6 +19548,10 @@ snapshots: dependencies: is-arrayish: 0.3.2 + simple-update-notifier@2.0.0: + dependencies: + semver: 7.7.1 + sisteransi@1.0.5: {} slash@3.0.0: {} @@ -19163,7 +19566,7 @@ snapshots: socks-proxy-agent@8.0.5: dependencies: agent-base: 7.1.3 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) socks: 2.8.4 transitivePeerDependencies: - supports-color @@ -19396,7 +19799,7 @@ snapshots: dependencies: component-emitter: 1.3.1 cookiejar: 2.1.4 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) fast-safe-stringify: 2.1.1 form-data: 4.0.2 formidable: 2.1.2 @@ -19593,6 +19996,8 @@ snapshots: '@tokenizer/token': 0.3.0 ieee754: 1.2.1 + touch@3.1.1: {} + tr46@0.0.3: {} tr46@5.1.0: @@ -19723,6 +20128,12 @@ snapshots: media-typer: 0.3.0 mime-types: 2.1.35 + type-is@2.0.1: + dependencies: + content-type: 1.0.5 + media-typer: 1.1.0 + mime-types: 3.0.1 + typed-array-buffer@1.0.3: dependencies: call-bound: 1.0.4 @@ -19786,6 +20197,8 @@ snapshots: buffer: 5.7.1 through: 2.3.8 + undefsafe@2.0.5: {} + underscore@1.13.7: {} undici-types@5.26.5: {} @@ -20048,7 +20461,7 @@ snapshots: vite-node@1.6.1(@types/node@20.17.24)(sass@1.85.1)(terser@5.39.0): dependencies: cac: 6.7.14 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) pathe: 1.1.2 picocolors: 1.1.1 vite: 5.4.14(@types/node@20.17.24)(sass@1.85.1)(terser@5.39.0) @@ -20066,7 +20479,7 @@ snapshots: vite-node@3.0.8(@types/node@20.17.24)(sass@1.85.1)(terser@5.39.0): dependencies: cac: 6.7.14 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) es-module-lexer: 1.6.0 pathe: 2.0.3 vite: 6.2.2(@types/node@20.17.24)(sass@1.85.1)(terser@5.39.0) @@ -20087,7 +20500,7 @@ snapshots: vite-node@3.1.1(@types/node@20.17.24)(sass@1.85.1)(terser@5.39.0): dependencies: cac: 6.7.14 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) es-module-lexer: 1.6.0 pathe: 2.0.3 vite: 6.2.2(@types/node@20.17.24)(sass@1.85.1)(terser@5.39.0) @@ -20136,7 +20549,7 @@ snapshots: '@vitest/utils': 1.6.1 acorn-walk: 8.3.4 chai: 4.5.0 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) execa: 8.0.1 local-pkg: 0.5.1 magic-string: 0.30.17 @@ -20171,7 +20584,7 @@ snapshots: '@vitest/spy': 3.0.8 '@vitest/utils': 3.0.8 chai: 5.2.0 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) expect-type: 1.2.0 magic-string: 0.30.17 pathe: 2.0.3 @@ -20210,7 +20623,7 @@ snapshots: '@vitest/spy': 3.1.1 '@vitest/utils': 3.1.1 chai: 5.2.0 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) expect-type: 1.2.0 magic-string: 0.30.17 pathe: 2.0.3 @@ -20539,6 +20952,10 @@ snapshots: - terser - typescript + zod-to-json-schema@3.24.5(zod@3.24.2): + dependencies: + zod: 3.24.2 + zod-validation-error@3.4.0(zod@3.24.2): dependencies: zod: 3.24.2 diff --git a/projects/app/data/config.json b/projects/app/data/config.json index 78c55c8f4a58..612050b165c6 100644 --- a/projects/app/data/config.json +++ b/projects/app/data/config.json @@ -1,7 +1,8 @@ // 已使用 json5 进行解析,会自动去掉注释,无需手动去除 { "feConfigs": { - "lafEnv": "https://laf.dev" // laf环境。 https://laf.run (杭州阿里云) ,或者私有化的laf环境。如果使用 Laf openapi 功能,需要最新版的 laf 。 + "lafEnv": "https://laf.dev", // laf环境。 https://laf.run (杭州阿里云) ,或者私有化的laf环境。如果使用 Laf openapi 功能,需要最新版的 laf 。 + "mcpServerProxyEndpoint": "" // mcp server 代理地址,例如: http://localhost:3005 }, "systemEnv": { "vectorMaxProcess": 10, // 向量处理线程数量 diff --git a/projects/app/next.config.js b/projects/app/next.config.js index 4df4cf741d21..558cfff7ff65 100644 --- a/projects/app/next.config.js +++ b/projects/app/next.config.js @@ -77,7 +77,7 @@ const nextConfig = { return config; }, // 需要转译的包 - transpilePackages: ['@fastgpt/global', '@fastgpt/web', 'ahooks'], + transpilePackages: ['@modelcontextprotocol/sdk', 'ahooks'], experimental: { // 优化 Server Components 的构建和运行,避免不必要的客户端打包。 serverComponentsExternalPackages: [ diff --git a/projects/app/package.json b/projects/app/package.json index 9ec59a459a9c..9cb830d46457 100644 --- a/projects/app/package.json +++ b/projects/app/package.json @@ -23,6 +23,8 @@ "@fastgpt/templates": "workspace:*", "@fastgpt/web": "workspace:*", "@fortaine/fetch-event-source": "^3.0.6", + "@modelcontextprotocol/sdk": "^1.9.0", + "@node-rs/jieba": "2.0.1", "@tanstack/react-query": "^4.24.10", "ahooks": "^3.7.11", "axios": "^1.8.2", @@ -64,7 +66,7 @@ "request-ip": "^3.3.0", "sass": "^1.58.3", "use-context-selector": "^1.4.4", - "@node-rs/jieba": "2.0.1" + "zod": "^3.24.2" }, "devDependencies": { "@svgr/webpack": "^6.5.1", diff --git a/projects/app/src/pageComponents/dashboard/Container.tsx b/projects/app/src/pageComponents/dashboard/Container.tsx index 01d44911c867..14218a7d6aa0 100644 --- a/projects/app/src/pageComponents/dashboard/Container.tsx +++ b/projects/app/src/pageComponents/dashboard/Container.tsx @@ -17,7 +17,8 @@ import { PluginGroupSchemaType } from '@fastgpt/service/core/app/plugin/type'; export enum TabEnum { apps = 'apps', - app_templates = 'templateMarket' + app_templates = 'templateMarket', + mcp_server = 'mcpServer' } type TabEnumType = `${keyof typeof TabEnum}` | string; @@ -152,7 +153,7 @@ const DashboardContainer = ({ { groupId: TabEnum.app_templates, groupAvatar: 'common/templateMarket', - groupName: t('app:template_market'), + groupName: t('common:template_market'), children: [ ...templateTags .map((tag) => { @@ -182,7 +183,17 @@ const DashboardContainer = ({ ] : []) ] - } + }, + ...(feConfigs?.mcpServerProxyEndpoint + ? [ + { + groupId: TabEnum.mcp_server, + groupAvatar: 'key', + groupName: t('common:mcp_server'), + children: [] + } + ] + : []) ]; }, [currentType, feConfigs.appTemplateCourse, pluginGroups, t, templateList, templateTags]); @@ -249,17 +260,21 @@ const DashboardContainer = ({ router.push(`/dashboard/${group.groupId}`); onCloseSidebar(); }} + {...(group.children.length === 0 && + selected && { bg: 'primary.100', color: 'primary.600' })} > {group.groupName} - + {group.children.length > 0 && ( + + )} {selected && ( diff --git a/projects/app/src/pageComponents/dashboard/mcp/EditModal.tsx b/projects/app/src/pageComponents/dashboard/mcp/EditModal.tsx new file mode 100644 index 000000000000..12937e439380 --- /dev/null +++ b/projects/app/src/pageComponents/dashboard/mcp/EditModal.tsx @@ -0,0 +1,414 @@ +import React, { useState } from 'react'; +import { + Box, + Button, + Checkbox, + Flex, + Grid, + HStack, + Input, + ModalBody, + ModalFooter, + Table, + TableContainer, + Tbody, + Td, + Th, + Thead, + Tr, + useDisclosure +} from '@chakra-ui/react'; +import MyModal from '@fastgpt/web/components/common/MyModal'; +import { McpAppType } from '@fastgpt/global/support/mcp/type'; +import { useTranslation } from 'next-i18next'; +import { useFieldArray, useForm } from 'react-hook-form'; +import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel'; +import MyIconButton from '@fastgpt/web/components/common/Icon/button'; +import EmptyTip from '@fastgpt/web/components/common/EmptyTip'; +import SearchInput from '@fastgpt/web/components/common/Input/SearchInput'; +import Path from '@/components/common/folder/Path'; +import Avatar from '@fastgpt/web/components/common/Avatar'; +import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; +import { getAppBasicInfoByIds, getMyApps } from '@/web/core/app/api'; +import { ParentIdType } from '@fastgpt/global/common/parentFolder/type'; +import { getAppFolderPath } from '@/web/core/app/api/app'; +import { AppFolderTypeList } from '@fastgpt/global/core/app/constants'; +import MyIcon from '@fastgpt/web/components/common/Icon'; +import { postCreateMcpServer, putUpdateMcpServer } from '../../../web/support/mcp/api'; + +export type EditMcForm = { + id?: string; + name: string; + apps: McpAppType[]; +}; + +export const defaultForm: EditMcForm = { + name: '', + apps: [] +}; + +const SelectAppModal = ({ + selectedApps, + onClose, + onConfirm +}: { + selectedApps: McpAppType[]; + onClose: () => void; + onConfirm: (e: McpAppType[]) => void; +}) => { + const { t } = useTranslation(); + + const [selectedList, setSelectedList] = useState< + { + appId: string; + toolName: string; + avatar: string; + description: string; + }[] + >([]); + + // Load selected app + useRequest2(() => getAppBasicInfoByIds(selectedApps.map((item) => item.appId)), { + manual: false, + onSuccess: (data) => { + setSelectedList( + data.map((item) => ({ + appId: item.id, + toolName: item.name, + avatar: item.avatar, + description: selectedApps.find((app) => app.appId === item.id)?.description || '' + })) + ); + } + }); + + // Load all apps + const [searchKey, setSearchKey] = useState(''); + const [parentId, setParentId] = useState(''); + + const { data: apps = [], loading: loadingApps } = useRequest2( + () => + getMyApps({ + searchKey, + parentId + }), + { + manual: false, + refreshDeps: [searchKey, parentId], + throttleWait: 200 + } + ); + const { data: paths = [] } = useRequest2( + () => getAppFolderPath({ sourceId: parentId, type: 'current' }), + { + manual: false, + refreshDeps: [parentId] + } + ); + + const isLoading = loadingApps; + + return ( + + + + + setSearchKey(e.target.value)} + /> + + {paths.length > 0 && !searchKey && ( + + + + )} + + + {apps.map((item) => { + const selected = selectedList.some((app) => app.appId === item._id); + const isFolder = AppFolderTypeList.includes(item.type); + + return ( + { + if (isFolder) { + setParentId(item._id); + } else if (selected) { + setSelectedList((state) => state.filter((app) => app.appId !== item._id)); + } else { + setSelectedList((state) => [ + ...state, + { + appId: item._id, + toolName: item.name, + avatar: item.avatar, + description: item.intro + } + ]); + } + }} + > + + {!isFolder && } + + + {item.name} + + ); + })} + + + + + + {`${t('dashboard_mcp:has_chosen')}: `} + {selectedList.length} + + + {selectedList.map((item) => { + return ( + + + + {item.toolName} + + { + setSelectedList((state) => state.filter((app) => app.appId !== item.appId)); + }} + /> + + ); + })} + + + + + + + + + ); +}; + +const EditMcpModal = ({ + editMcp, + onClose, + onSuccess +}: { + editMcp: EditMcForm; + onClose: () => void; + onSuccess: () => void; +}) => { + const { t } = useTranslation(); + const isEdit = !!editMcp.id; + console.log(editMcp); + const { + isOpen: isOpenSelectApp, + onOpen: onOpenSelectApp, + onClose: onCloseSelectApp + } = useDisclosure(); + + const { register, handleSubmit, control } = useForm({ + defaultValues: editMcp + }); + + const { + fields: apps, + replace: replaceSelectedApps, + remove + } = useFieldArray({ + control, + name: 'apps' + }); + + const { runAsync: createMcp, loading: loadingCreate } = useRequest2( + (data: EditMcForm) => + postCreateMcpServer({ + name: data.name, + apps: data.apps.map((item) => ({ + appId: item.appId, + toolName: item.toolName, + description: item.description + })) + }), + { + manual: true, + successToast: t('common:common.Create Success'), + onSuccess + } + ); + const { runAsync: updateMcp, loading: loadingUpdate } = useRequest2( + (data: EditMcForm) => + putUpdateMcpServer({ + id: data.id!, + name: data.name, + apps: data.apps.map((item) => ({ + appId: item.appId, + toolName: item.toolName, + description: item.description + })) + }), + { + manual: true, + successToast: t('common:common.Update Success'), + onSuccess + } + ); + const isConfirming = loadingCreate || loadingUpdate; + + return ( + <> + + + + + {t('common:common.Input name')} + + + + + + {t('dashboard_mcp:apps')} + + + + + + + + + + + + + {apps.map((app, index) => { + return ( + + + + + + ); + })} + +
{t('dashboard_mcp:app_name')}{t('dashboard_mcp:app_description')}
{app.toolName} + + + + remove(index)} + color={'myGray.600'} + /> + +
+ {apps.length === 0 && } +
+
+
+ + + + +
+ + {isOpenSelectApp && ( + { + replaceSelectedApps( + e.map((item) => ({ + appId: item.appId, + toolName: item.toolName, + description: item.description + })) + ); + onCloseSelectApp(); + }} + /> + )} + + ); +}; + +export default EditMcpModal; diff --git a/projects/app/src/pageComponents/dashboard/mcp/usageWay.tsx b/projects/app/src/pageComponents/dashboard/mcp/usageWay.tsx new file mode 100644 index 000000000000..bdda202ee682 --- /dev/null +++ b/projects/app/src/pageComponents/dashboard/mcp/usageWay.tsx @@ -0,0 +1,62 @@ +import { McpKeyType } from '@fastgpt/global/support/mcp/type'; +import MyModal from '@fastgpt/web/components/common/MyModal'; +import React from 'react'; +import { useTranslation } from 'next-i18next'; +import { Box, Flex, HStack, ModalBody } from '@chakra-ui/react'; +import { useSystemStore } from '@/web/common/system/useSystemStore'; +import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel'; +import CopyBox from '@fastgpt/web/components/common/String/CopyBox'; +import MyIconButton from '@fastgpt/web/components/common/Icon/button'; + +const UsageWay = ({ mcp, onClose }: { mcp: McpKeyType; onClose: () => void }) => { + const { t } = useTranslation(); + const { feConfigs } = useSystemStore(); + + const sseUrl = `${feConfigs?.mcpServerProxyEndpoint}/${mcp.key}/sse`; + const jsonConfig = `{ + "mcpServers": { + "${feConfigs?.systemTitle}-mcp-${mcp._id}": { + "url": "${sseUrl}" + } + } +}`; + + return ( + + + + {t('dashboard_mcp:mcp_endpoints')} + + + {sseUrl} + + + + + + + + + + {t('dashboard_mcp:mcp_json_config')} + + + + + + {jsonConfig} + + + + + + ); +}; + +export default React.memo(UsageWay); diff --git a/projects/app/src/pages/api/core/app/getBasicInfo.ts b/projects/app/src/pages/api/core/app/getBasicInfo.ts new file mode 100644 index 000000000000..560bfea97817 --- /dev/null +++ b/projects/app/src/pages/api/core/app/getBasicInfo.ts @@ -0,0 +1,33 @@ +import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next'; +import { NextAPI } from '@/service/middleware/entry'; +import { authCert } from '@fastgpt/service/support/permission/auth/common'; +import { getAppBasicInfoByIds } from '@fastgpt/service/core/app/controller'; + +export type getBasicInfoQuery = {}; + +export type getBasicInfoBody = { + ids: string[]; +}; + +export type getBasicInfoResponse = { + id: string; + name: string; + avatar: string; +}[]; + +async function handler( + req: ApiRequestProps, + res: ApiResponseType +): Promise { + const { ids } = req.body; + const { teamId } = await authCert({ req, authToken: true }); + + const apps = await getAppBasicInfoByIds({ + teamId, + ids + }); + + return apps; +} + +export default NextAPI(handler); diff --git a/projects/app/src/pages/api/support/mcp/create.ts b/projects/app/src/pages/api/support/mcp/create.ts new file mode 100644 index 000000000000..6ae5403f8251 --- /dev/null +++ b/projects/app/src/pages/api/support/mcp/create.ts @@ -0,0 +1,77 @@ +import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next'; +import { NextAPI } from '@/service/middleware/entry'; +import { authUserPer } from '@fastgpt/service/support/permission/user/auth'; +import { TeamErrEnum } from '@fastgpt/global/common/error/code/team'; +import { CommonErrEnum } from '@fastgpt/global/common/error/code/common'; +import { authAppByTmbId } from '@fastgpt/service/support/permission/app/auth'; +import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant'; +import { MongoMcpKey } from '@fastgpt/service/support/mcp/schema'; +import { McpAppType } from '@fastgpt/global/support/mcp/type'; + +export type createQuery = {}; + +export type createBody = { + name: string; + apps: McpAppType[]; +}; + +export type createResponse = {}; + +async function handler( + req: ApiRequestProps, + res: ApiResponseType +): Promise { + const { teamId, tmbId, permission } = await authUserPer({ + req, + authToken: true, + authApiKey: true + }); + + if (!permission.hasApikeyCreatePer) { + return Promise.reject(TeamErrEnum.unPermission); + } + + let { name, apps } = req.body; + + if (!apps.length) { + return Promise.reject(CommonErrEnum.missingParams); + } + + // Count mcp length + const totalMcp = await MongoMcpKey.countDocuments({ teamId }); + if (totalMcp >= 100) { + return Promise.reject('暂时只支持100个MCP服务'); + } + + // 对 apps 中的 id 进行去重,确保每个应用只出现一次 + const uniqueAppIds = new Set(); + apps = apps.filter((app) => { + if (uniqueAppIds.has(app.appId)) { + return false; // 过滤掉重复的 app id + } + uniqueAppIds.add(app.appId); + return true; + }); + + // Check app read permission + await Promise.all( + apps.map((app) => + authAppByTmbId({ + tmbId, + appId: app.appId, + per: ReadPermissionVal + }) + ) + ); + + await MongoMcpKey.create({ + teamId, + tmbId, + name, + apps + }); + + return {}; +} + +export default NextAPI(handler); diff --git a/projects/app/src/pages/api/support/mcp/delete.ts b/projects/app/src/pages/api/support/mcp/delete.ts new file mode 100644 index 000000000000..4aab79f72056 --- /dev/null +++ b/projects/app/src/pages/api/support/mcp/delete.ts @@ -0,0 +1,34 @@ +import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next'; +import { NextAPI } from '@/service/middleware/entry'; +import { authMcp } from '@fastgpt/service/support/permission/mcp/auth'; +import { WritePermissionVal } from '@fastgpt/global/support/permission/constant'; +import { MongoMcpKey } from '@fastgpt/service/support/mcp/schema'; + +export type deleteQuery = { + id: string; +}; + +export type deleteBody = {}; + +export type deleteResponse = {}; + +async function handler( + req: ApiRequestProps, + res: ApiResponseType +): Promise { + const { id } = req.query; + + await authMcp({ + req, + authToken: true, + authApiKey: true, + mcpId: id, + per: WritePermissionVal + }); + + await MongoMcpKey.deleteOne({ _id: id }); + + return {}; +} + +export default NextAPI(handler); diff --git a/projects/app/src/pages/api/support/mcp/list.ts b/projects/app/src/pages/api/support/mcp/list.ts new file mode 100644 index 000000000000..2ec88c11f155 --- /dev/null +++ b/projects/app/src/pages/api/support/mcp/list.ts @@ -0,0 +1,34 @@ +import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next'; +import { NextAPI } from '@/service/middleware/entry'; +import { authCert } from '@fastgpt/service/support/permission/auth/common'; +import { authUserPer } from '@fastgpt/service/support/permission/user/auth'; +import { MongoMcpKey } from '@fastgpt/service/support/mcp/schema'; +import { McpKeyType } from '@fastgpt/global/support/mcp/type'; + +export type listQuery = {}; + +export type listBody = {}; + +export type listResponse = McpKeyType[]; + +async function handler( + req: ApiRequestProps, + res: ApiResponseType +): Promise { + const { teamId, tmbId, permission } = await authUserPer({ + req, + authToken: true, + authApiKey: true + }); + + const list = await (async () => { + if (permission.hasManagePer) { + return await MongoMcpKey.find({ teamId }).lean().sort({ _id: -1 }); + } + return await MongoMcpKey.find({ teamId, tmbId }).lean().sort({ _id: -1 }); + })(); + + return list; +} + +export default NextAPI(handler); diff --git a/projects/app/src/pages/api/support/mcp/server/toolCall.ts b/projects/app/src/pages/api/support/mcp/server/toolCall.ts new file mode 100644 index 000000000000..d76de051b33c --- /dev/null +++ b/projects/app/src/pages/api/support/mcp/server/toolCall.ts @@ -0,0 +1,196 @@ +import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next'; +import { NextAPI } from '@/service/middleware/entry'; +import { MongoMcpKey } from '@fastgpt/service/support/mcp/schema'; +import { CommonErrEnum } from '@fastgpt/global/common/error/code/common'; +import { MongoApp } from '@fastgpt/service/core/app/schema'; +import { AppTypeEnum } from '@fastgpt/global/core/app/constants'; +import { AppSchema } from '@fastgpt/global/core/app/type'; +import { getUserChatInfoAndAuthTeamPoints } from '@fastgpt/service/support/permission/auth/team'; +import { getAppLatestVersion } from '@fastgpt/service/core/app/version/controller'; +import { getNanoid } from '@fastgpt/global/common/string/tools'; +import { AIChatItemType, UserChatItemType } from '@fastgpt/global/core/chat/type'; +import { + getPluginRunUserQuery, + updatePluginInputByVariables +} from '@fastgpt/global/core/workflow/utils'; +import { getPluginInputsFromStoreNodes } from '@fastgpt/global/core/app/plugin/utils'; +import { + ChatItemValueTypeEnum, + ChatRoleEnum, + ChatSourceEnum +} from '@fastgpt/global/core/chat/constants'; +import { + getWorkflowEntryNodeIds, + initWorkflowEdgeStatus, + storeNodes2RuntimeNodes +} from '@fastgpt/global/core/workflow/runtime/utils'; +import { WORKFLOW_MAX_RUN_TIMES } from '@fastgpt/service/core/workflow/constants'; +import { dispatchWorkFlow } from '@fastgpt/service/core/workflow/dispatch'; +import { removeEmptyUserInput } from '@fastgpt/global/core/chat/utils'; +import { saveChat } from '@fastgpt/service/core/chat/saveChat'; +import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runtime/constants'; +import { createChatUsage } from '@fastgpt/service/support/wallet/usage/controller'; +import { UsageSourceEnum } from '@fastgpt/global/support/wallet/usage/constants'; +import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; + +export type toolCallQuery = {}; + +export type toolCallBody = { + key: string; + toolName: string; + inputs: Record; +}; + +export type toolCallResponse = {}; + +const dispatchApp = async (app: AppSchema, variables: Record) => { + const isPlugin = app.type === AppTypeEnum.plugin; + + const { timezone, externalProvider } = await getUserChatInfoAndAuthTeamPoints(app.tmbId); + // Get app latest version + const { nodes, edges, chatConfig } = await getAppLatestVersion(app._id, app); + + const userQuestion: UserChatItemType = (() => { + if (isPlugin) { + return getPluginRunUserQuery({ + pluginInputs: getPluginInputsFromStoreNodes(nodes || app.modules), + variables + }); + } + + return { + obj: ChatRoleEnum.Human, + value: [ + { + type: ChatItemValueTypeEnum.text, + text: { + content: variables.question + } + } + ] + }; + })(); + + let runtimeNodes = storeNodes2RuntimeNodes(nodes, getWorkflowEntryNodeIds(nodes)); + if (isPlugin) { + // Assign values to runtimeNodes using variables + runtimeNodes = updatePluginInputByVariables(runtimeNodes, variables); + // Plugin runtime does not need global variables(It has been injected into the pluginInputNode) + variables = {}; + } else { + delete variables.question; + variables.system_fileUrlList = variables.fileUrlList; + delete variables.fileUrlList; + } + + const chatId = getNanoid(); + + const { flowUsages, assistantResponses, newVariables, flowResponses } = await dispatchWorkFlow({ + chatId, + timezone, + externalProvider, + mode: 'chat', + runningAppInfo: { + id: String(app._id), + teamId: String(app.teamId), + tmbId: String(app.tmbId) + }, + runningUserInfo: { + teamId: String(app.teamId), + tmbId: String(app.tmbId) + }, + uid: String(app.tmbId), + runtimeNodes, + runtimeEdges: initWorkflowEdgeStatus(edges), + variables, + query: removeEmptyUserInput(userQuestion.value), + chatConfig, + histories: [], + stream: false, + maxRunTimes: WORKFLOW_MAX_RUN_TIMES + }); + + // Save chat + const aiResponse: AIChatItemType & { dataId?: string } = { + obj: ChatRoleEnum.AI, + value: assistantResponses, + [DispatchNodeResponseKeyEnum.nodeResponse]: flowResponses + }; + await saveChat({ + chatId, + appId: app._id, + teamId: app.teamId, + tmbId: app.tmbId, + nodes, + appChatConfig: chatConfig, + variables: newVariables, + isUpdateUseTime: false, // owner update use time + newTitle: 'MCP call', + source: ChatSourceEnum.mcp, + content: [userQuestion, aiResponse] + }); + + // Push usage + createChatUsage({ + appName: app.name, + appId: app._id, + teamId: app.teamId, + tmbId: app.tmbId, + source: UsageSourceEnum.mcp, + flowUsages + }); + + // Get MCP response type + const responseContent = (() => { + if (isPlugin) { + const output = flowResponses.find( + (item) => item.moduleType === FlowNodeTypeEnum.pluginOutput + ); + if (output) { + return JSON.stringify(output.pluginOutput); + } else { + return 'Can not get response from plugin'; + } + } + + return assistantResponses + .map((item) => item?.text?.content) + .filter(Boolean) + .join('\n'); + })(); + + return responseContent; +}; + +async function handler( + req: ApiRequestProps, + res: ApiResponseType +): Promise { + const { key, toolName, inputs } = req.body; + + const mcp = await MongoMcpKey.findOne({ key }, { apps: 1 }).lean(); + + if (!mcp) { + return Promise.reject(CommonErrEnum.invalidResource); + } + + // Get app list + const appList = await MongoApp.find({ + _id: { $in: mcp.apps.map((app) => app.appId) }, + type: { $in: [AppTypeEnum.simple, AppTypeEnum.workflow, AppTypeEnum.plugin] } + }).lean(); + + const app = appList.find((app) => { + const mcpApp = mcp.apps.find((mcpApp) => String(mcpApp.appId) === String(app._id))!; + + return toolName === mcpApp.toolName; + }); + + if (!app) { + return Promise.reject(CommonErrEnum.missingParams); + } + + return await dispatchApp(app, inputs); +} + +export default NextAPI(handler); diff --git a/projects/app/src/pages/api/support/mcp/server/toolList.ts b/projects/app/src/pages/api/support/mcp/server/toolList.ts new file mode 100644 index 000000000000..29ef241f4ad0 --- /dev/null +++ b/projects/app/src/pages/api/support/mcp/server/toolList.ts @@ -0,0 +1,152 @@ +import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next'; +import { NextAPI } from '@/service/middleware/entry'; +import { MongoMcpKey } from '@fastgpt/service/support/mcp/schema'; +import { CommonErrEnum } from '@fastgpt/global/common/error/code/common'; +import { MongoApp } from '@fastgpt/service/core/app/schema'; +import { authAppByTmbId } from '@fastgpt/service/support/permission/app/auth'; +import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant'; +import { getAppLatestVersion } from '@fastgpt/service/core/app/version/controller'; +import { Tool } from '@modelcontextprotocol/sdk/types'; +import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; +import { StoreNodeItemType } from '@fastgpt/global/core/workflow/type/node'; +import { toolValueTypeList } from '@fastgpt/global/core/workflow/constants'; +import { AppChatConfigType } from '@fastgpt/global/core/app/type'; +import { AppTypeEnum } from '@fastgpt/global/core/app/constants'; + +export type listToolsQuery = { key: string }; + +export type listToolsBody = {}; + +export type listToolsResponse = {}; + +const pluginNodes2InputSchema = (nodes: StoreNodeItemType[]) => { + const pluginInput = nodes.find((node) => node.flowNodeType === FlowNodeTypeEnum.pluginInput); + + const schema: Tool['inputSchema'] = { + type: 'object', + properties: {}, + required: [] + }; + + pluginInput?.inputs.forEach((input) => { + const jsonSchema = ( + toolValueTypeList.find((type) => type.value === input.valueType) || toolValueTypeList[0] + )?.jsonSchema; + + schema.properties![input.key] = { + ...jsonSchema, + description: input.description, + enum: input.enum?.split('\n').filter(Boolean) || undefined + }; + + if (input.required) { + // @ts-ignore + schema.required.push(input.key); + } + }); + + return schema; +}; +const workflow2InputSchema = (chatConfig?: AppChatConfigType) => { + const schema: Tool['inputSchema'] = { + type: 'object', + properties: { + question: { + type: 'string', + description: 'Question from user' + }, + ...(chatConfig?.fileSelectConfig?.canSelectFile || chatConfig?.fileSelectConfig?.canSelectImg + ? { + fileUrlList: { + type: 'array', + items: { + type: 'string' + }, + description: 'File linkage' + } + } + : {}) + }, + required: ['question'] + }; + + chatConfig?.variables?.forEach((item) => { + const jsonSchema = ( + toolValueTypeList.find((type) => type.value === item.valueType) || toolValueTypeList[0] + )?.jsonSchema; + + schema.properties![item.key] = { + ...jsonSchema, + description: item.description, + enum: item.enums?.map((enumItem) => enumItem.value) || undefined + }; + + if (item.required) { + // @ts-ignore + schema.required!.push(item.key); + } + }); + + return schema; +}; + +async function handler( + req: ApiRequestProps, + res: ApiResponseType +): Promise { + const { key } = req.query; + + const mcp = await MongoMcpKey.findOne({ key }, { apps: 1 }).lean(); + + if (!mcp) { + return Promise.reject(CommonErrEnum.invalidResource); + } + + // Get app list + const appList = await MongoApp.find( + { + _id: { $in: mcp.apps.map((app) => app.appId) }, + type: { $in: [AppTypeEnum.simple, AppTypeEnum.workflow, AppTypeEnum.plugin] } + }, + { name: 1, intro: 1 } + ).lean(); + + // Filter not permission app + const permissionAppList = await Promise.all( + appList.filter(async (app) => { + try { + await authAppByTmbId({ tmbId: mcp.tmbId, appId: app._id, per: ReadPermissionVal }); + return true; + } catch (error) { + return false; + } + }) + ); + + // Get latest version + const versionList = await Promise.all( + permissionAppList.map((app) => getAppLatestVersion(app._id, app)) + ); + + // Compute mcp tools + const tools = versionList.map((version, index) => { + const app = permissionAppList[index]; + const mcpApp = mcp.apps.find((mcpApp) => String(mcpApp.appId) === String(app._id))!; + + const isPlugin = !!version.nodes.find( + (node) => node.flowNodeType === FlowNodeTypeEnum.pluginInput + ); + + return { + name: mcpApp.toolName, + description: mcpApp.description, + inputSchema: isPlugin + ? pluginNodes2InputSchema(version.nodes) + : workflow2InputSchema(version.chatConfig) + }; + }); + + return tools; +} + +export default NextAPI(handler); diff --git a/projects/app/src/pages/api/support/mcp/update.ts b/projects/app/src/pages/api/support/mcp/update.ts new file mode 100644 index 000000000000..103c98660e98 --- /dev/null +++ b/projects/app/src/pages/api/support/mcp/update.ts @@ -0,0 +1,66 @@ +import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next'; +import { NextAPI } from '@/service/middleware/entry'; +import { authMcp } from '../../../../../../../packages/service/support/permission/mcp/auth'; +import { ReadPermissionVal, WritePermissionVal } from '@fastgpt/global/support/permission/constant'; +import { authAppByTmbId } from '@fastgpt/service/support/permission/app/auth'; +import { MongoMcpKey } from '@fastgpt/service/support/mcp/schema'; +import { McpAppType } from '@fastgpt/global/support/mcp/type'; + +export type updateQuery = {}; + +export type updateBody = { + id: string; + name: string; + apps: McpAppType[]; +}; + +export type updateResponse = {}; + +async function handler( + req: ApiRequestProps, + res: ApiResponseType +): Promise { + let { id: mcpId, name, apps } = req.body; + const { tmbId } = await authMcp({ + req, + authToken: true, + authApiKey: true, + mcpId, + per: WritePermissionVal + }); + + // 对 apps 中的 id 进行去重,确保每个应用只出现一次 + const uniqueAppIds = new Set(); + apps = apps.filter((app) => { + if (uniqueAppIds.has(app.appId)) { + return false; // 过滤掉重复的 app id + } + uniqueAppIds.add(app.appId); + return true; + }); + + // Check app read permission + await Promise.all( + apps.map((app) => + authAppByTmbId({ + tmbId, + appId: app.appId, + per: ReadPermissionVal + }) + ) + ); + + await MongoMcpKey.updateOne( + { _id: mcpId }, + { + $set: { + ...(name && { name }), + apps + } + } + ); + + return {}; +} + +export default NextAPI(handler); diff --git a/projects/app/src/pages/dashboard/mcpServer/index.tsx b/projects/app/src/pages/dashboard/mcpServer/index.tsx new file mode 100644 index 000000000000..1b696446107d --- /dev/null +++ b/projects/app/src/pages/dashboard/mcpServer/index.tsx @@ -0,0 +1,159 @@ +import { serviceSideProps } from '@/web/common/i18n/utils'; +import React, { useState } from 'react'; +import DashboardContainer from '@/pageComponents/dashboard/Container'; +import { + Box, + Button, + Flex, + HStack, + Table, + TableContainer, + Tbody, + Td, + Th, + Thead, + Tr +} from '@chakra-ui/react'; +import { useTranslation } from 'next-i18next'; +import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; +import { deleteMcpServer, getMcpServerList } from '@/web/support/mcp/api'; +import MyBox from '@fastgpt/web/components/common/MyBox'; +import EditMcpModal, { defaultForm, EditMcForm } from '@/pageComponents/dashboard/mcp/EditModal'; +import EmptyTip from '@fastgpt/web/components/common/EmptyTip'; +import MyIconButton from '@fastgpt/web/components/common/Icon/button'; +import { useSystemStore } from '@/web/common/system/useSystemStore'; +import { useConfirm } from '@fastgpt/web/hooks/useConfirm'; +import dynamic from 'next/dynamic'; +import { McpKeyType } from '@fastgpt/global/support/mcp/type'; + +const UsageWay = dynamic(() => import('@/pageComponents/dashboard/mcp/usageWay'), { + ssr: false +}); + +const McpServer = () => { + const { t } = useTranslation(); + const { feConfigs } = useSystemStore(); + + const { + data: mcpServerList = [], + loading: loadingList, + refresh: loadMcpList + } = useRequest2(getMcpServerList, { + manual: false + }); + + const [editMcp, setEditMcp] = useState(); + const [usageWay, setUsageWay] = useState(); + + const { openConfirm: openDelConfirm, ConfirmModal: DelConfirmModal } = useConfirm({ + type: 'delete', + content: t('dashboard_mcp:delete_mcp_server_confirm_tip') + }); + const { runAsync: onDeleteMcpServer } = useRequest2(deleteMcpServer, { + manual: true, + onSuccess: () => { + loadMcpList(); + } + }); + + const isLoading = loadingList; + + return ( + <> + + {() => ( + + + + + {t('dashboard_mcp:mcp_server')} + + + {t('dashboard_mcp:mcp_server_description')} + + + + + + {/* table */} + + + + + + + + + + + {mcpServerList.map((mcp) => { + return ( + + + + + + ); + })} + +
{t('dashboard_mcp:mcp_name')}{t('dashboard_mcp:mcp_apps')}
{mcp.name}{mcp.apps.length} + + + + setEditMcp({ + id: mcp._id, + name: mcp.name, + apps: mcp.apps + }) + } + /> + + openDelConfirm(() => onDeleteMcpServer(mcp._id))()} + /> + +
+ {mcpServerList.length === 0 && } +
+
+ )} +
+ + + {!!usageWay && setUsageWay(undefined)} />} + {!!editMcp && ( + setEditMcp(undefined)} + onSuccess={() => { + setEditMcp(undefined); + loadMcpList(); + }} + /> + )} + + ); +}; + +export default McpServer; + +export async function getServerSideProps(content: any) { + return { + props: { + ...(await serviceSideProps(content, ['dashboard_mcp'])) + } + }; +} diff --git a/projects/app/src/web/core/app/api.ts b/projects/app/src/web/core/app/api.ts index 958097924a38..fabf8da4dc8e 100644 --- a/projects/app/src/web/core/app/api.ts +++ b/projects/app/src/web/core/app/api.ts @@ -6,6 +6,7 @@ import type { CreateAppBody } from '@/pages/api/core/app/create'; import type { ListAppBody } from '@/pages/api/core/app/list'; import type { AppLogsListItemType } from '@/types/app'; import type { PaginationResponse } from '@fastgpt/web/common/fetch/type'; +import type { getBasicInfoResponse } from '@/pages/api/core/app/getBasicInfo'; /** * 获取应用列表 @@ -37,6 +38,12 @@ export const getAppDetailById = (id: string) => GET(`/core/app/de export const putAppById = (id: string, data: AppUpdateParams) => PUT(`/core/app/update?appId=${id}`, data); +/** + * Get app basic info by ids + */ +export const getAppBasicInfoByIds = (ids: string[]) => + POST(`/core/app/getBasicInfo`, { ids }); + // =================== chat logs export const getAppChatLogs = (data: GetAppChatLogsParams) => POST>(`/core/app/getChatLogs`, data, { maxQuantity: 1 }); diff --git a/projects/app/src/web/support/mcp/api.ts b/projects/app/src/web/support/mcp/api.ts new file mode 100644 index 000000000000..0912ad539b24 --- /dev/null +++ b/projects/app/src/web/support/mcp/api.ts @@ -0,0 +1,20 @@ +import type { updateBody } from '@/pages/api/support/mcp/update'; +import { GET, POST, DELETE, PUT } from '../../common/api/request'; +import type { createBody } from '@/pages/api/support/mcp/create'; +import type { listResponse } from '@/pages/api/support/mcp/list'; + +export const getMcpServerList = () => { + return GET('/support/mcp/list'); +}; + +export const postCreateMcpServer = (data: createBody) => { + return POST('/support/mcp/create', data); +}; + +export const putUpdateMcpServer = (data: updateBody) => { + return PUT('/support/mcp/update', data); +}; + +export const deleteMcpServer = (id: string) => { + return DELETE(`/support/mcp/delete`, { id }); +}; diff --git a/projects/mcp_server/.env.template b/projects/mcp_server/.env.template new file mode 100644 index 000000000000..207d3ceb6542 --- /dev/null +++ b/projects/mcp_server/.env.template @@ -0,0 +1 @@ +FASTGPT_ENDPOINT=http://localhost:3000 \ No newline at end of file diff --git a/projects/mcp_server/Dockerfile b/projects/mcp_server/Dockerfile new file mode 100644 index 000000000000..5b65f90e55f0 --- /dev/null +++ b/projects/mcp_server/Dockerfile @@ -0,0 +1,43 @@ +# --------- Install ----------- +FROM node:20.14.0-alpine AS install +WORKDIR /app + +RUN npm install -g pnpm@9.4.0 + +# 复制package.json +COPY pnpm-lock.yaml pnpm-workspace.yaml ./ +COPY projects/mcp_server/package.json ./projects/mcp_server/package.json + +RUN apk add --no-cache\ + curl ca-certificates\ + && update-ca-certificates + +# 安装依赖 +RUN [ -f pnpm-lock.yaml ] || (echo "Lockfile not found." && exit 1) +RUN pnpm i + +# --------- builder ----------- +FROM node:20.14.0-alpine AS builder +WORKDIR /app + +COPY package.json pnpm-workspace.yaml /app/ +COPY --from=install /app/node_modules /app/node_modules +COPY ./projects/mcp_server /app/projects/mcp_server +COPY --from=install /app/projects/mcp_server /app/projects/mcp_server + +RUN npm install -g pnpm@9.4.0 +RUN pnpm --filter=mcp_server build + +# runner +FROM node:20.14.0-alpine AS runner +WORKDIR /app + +RUN apk add --no-cache libffi libffi-dev strace bash + +COPY --from=builder /app/node_modules /app/node_modules +COPY --from=builder /app/projects/mcp_server /app/projects/mcp_server + +ENV NODE_ENV=production +ENV PORT=3000 + +ENTRYPOINT ["sh","-c","node projects/mcp_server/dist/index.js"] diff --git a/projects/mcp_server/package.json b/projects/mcp_server/package.json new file mode 100644 index 000000000000..574eef2540e7 --- /dev/null +++ b/projects/mcp_server/package.json @@ -0,0 +1,30 @@ +{ + "name": "fastgpt-mcp-server", + "version": "0.1", + "keywords": [], + "author": "fastgpt", + "files": [ + "dist" + ], + "type": "module", + "scripts": { + "build": "tsc && shx chmod +x dist/*.js", + "dev": "nodemon --watch src --ext ts,json --exec \"npm run dev:run\"", + "dev:run": "tsc && node dist/index.js", + "mcp_test": "npx @modelcontextprotocol/inspector" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "1.9.0", + "axios": "^1.8.2", + "chalk": "^5.3.0", + "dayjs": "^1.11.7", + "dotenv": "^16.5.0", + "express": "^4.21.2" + }, + "devDependencies": { + "@types/express": "^5.0.1", + "nodemon": "^3.1.9", + "shx": "^0.3.4", + "typescript": "^5.6.2" + } +} diff --git a/projects/mcp_server/src/api/fastgpt.ts b/projects/mcp_server/src/api/fastgpt.ts new file mode 100644 index 000000000000..75240ff17e9a --- /dev/null +++ b/projects/mcp_server/src/api/fastgpt.ts @@ -0,0 +1,7 @@ +import { Tool } from '@modelcontextprotocol/sdk/types.js'; +import { GET, POST } from './request.js'; + +export const getTools = (key: string) => GET('/support/mcp/server/toolList', { key }); + +export const callTool = (data: { key: string; toolName: string; inputs: Record }) => + POST('/support/mcp/server/toolCall', data); diff --git a/projects/mcp_server/src/api/request.ts b/projects/mcp_server/src/api/request.ts new file mode 100644 index 000000000000..48fcd25e6a7e --- /dev/null +++ b/projects/mcp_server/src/api/request.ts @@ -0,0 +1,111 @@ +import axios, { Method, InternalAxiosRequestConfig, AxiosResponse } from 'axios'; + +type ConfigType = {}; +type ResponseDataType = { + code: number; + message: string; + data: any; +}; + +/** + * 请求开始 + */ +function startInterceptors(config: InternalAxiosRequestConfig): InternalAxiosRequestConfig { + if (config.headers) { + } + + return config; +} + +/** + * 请求成功,检查请求头 + */ +function responseSuccess(response: AxiosResponse) { + return response; +} +/** + * 响应数据检查 + */ +function checkRes(data: ResponseDataType) { + if (data === undefined) { + console.log('error->', data, 'data is empty'); + return Promise.reject('服务器异常'); + } else if (data.code < 200 || data.code >= 400) { + return Promise.reject(data); + } + return data.data; +} + +/** + * 响应错误 + */ +function responseError(err: any) { + console.log('error->', '请求错误', err); + const data = err?.response?.data || err; + + if (!err) { + return Promise.reject({ message: '未知错误' }); + } + if (typeof err === 'string') { + return Promise.reject({ message: err }); + } + if (typeof data === 'string') { + return Promise.reject(data); + } +} + +/* 创建请求实例 */ +const instance = axios.create({ + baseURL: `${process.env.FASTGPT_ENDPOINT}/api`, + timeout: 600000, // 超时时间 + headers: { + 'content-type': 'application/json' + } +}); + +/* 请求拦截 */ +instance.interceptors.request.use(startInterceptors, (err) => Promise.reject(err)); +/* 响应拦截 */ +instance.interceptors.response.use(responseSuccess, (err) => Promise.reject(err)); + +function request(url: string, data: any, config: ConfigType, method: Method): any { + /* 去空 */ + for (const key in data) { + if (data[key] === undefined) { + delete data[key]; + } + } + + return instance + .request({ + url, + method, + data: ['POST', 'PUT'].includes(method) ? data : undefined, + params: !['POST', 'PUT'].includes(method) ? data : undefined + }) + .then((res) => checkRes(res.data)) + .catch((err) => responseError(err)); +} + +/** + * api请求方式 + * @param {String} url + * @param {Any} params + * @param {Object} config + * @returns + */ +export function GET(url: string, params = {}, config: ConfigType = {}): Promise { + return request(url, params, config, 'GET'); +} + +export function POST(url: string, data = {}, config: ConfigType = {}): Promise { + return request(url, data, config, 'POST'); +} + +export function PUT(url: string, data = {}, config: ConfigType = {}): Promise { + return request(url, data, config, 'PUT'); +} + +export function DELETE(url: string, data = {}, config: ConfigType = {}): Promise { + return request(url, data, config, 'DELETE'); +} diff --git a/projects/mcp_server/src/index.ts b/projects/mcp_server/src/index.ts new file mode 100644 index 000000000000..b67889025448 --- /dev/null +++ b/projects/mcp_server/src/index.ts @@ -0,0 +1,101 @@ +#!/usr/bin/env node +import './init.js'; +import { Server } from '@modelcontextprotocol/sdk/server/index.js'; +import { + CallToolRequestSchema, + ListToolsRequestSchema, + CallToolResult +} from '@modelcontextprotocol/sdk/types.js'; +import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js'; +import express from 'express'; + +import { callTool, getTools } from './api/fastgpt.js'; +import { addLog } from './utils/log.js'; +import { getErrText } from './utils/error.js'; + +const app = express(); + +const transportMap: Record = {}; + +app.get('/:key/sse', async (req, res) => { + const { key } = req.params; + + const transport = new SSEServerTransport(`/${key}/messages`, res); + + transport.onclose = () => { + addLog.info(`Transport ${transport.sessionId} closed`); + delete transportMap[transport.sessionId]; + }; + transport.onerror = (err) => { + addLog.error(`Transport ${transport.sessionId} error`, err); + }; + + transportMap[transport.sessionId] = transport; + + // Create server + const server = new Server( + { + name: 'fastgpt-mcp-server', + version: '1.0.0' + }, + { + capabilities: { + tools: {} + } + } + ); + + server.setRequestHandler(ListToolsRequestSchema, async () => ({ + tools: await getTools(key) + })); + + const handleToolCall = async ( + name: string, + args: Record + ): Promise => { + try { + addLog.info(`Call tool: ${name} with args: ${JSON.stringify(args)}`); + const result = await callTool({ key, toolName: name, inputs: args }); + + return { + content: [ + { + type: 'text', + text: typeof result === 'string' ? result : JSON.stringify(result) + } + ], + isError: false + }; + } catch (error) { + return { + message: getErrText(error), + content: [], + isError: true + }; + } + }; + server.setRequestHandler(CallToolRequestSchema, async (request) => + handleToolCall(request.params.name, request.params.arguments ?? {}) + ); + + await server.connect(transport); + addLog.info(`Server connected: ${transport.sessionId}`); +}); + +app.post('/:key/messages', (req, res) => { + const { sessionId } = req.query as { sessionId: string }; + + const transport = transportMap[sessionId]; + if (transport) { + transport.handlePostMessage(req, res); + } +}); + +const PORT = process.env.PORT || 3000; +app + .listen(PORT, () => { + addLog.info(`Server is running on port ${PORT}`); + }) + .on('error', (err) => { + addLog.error(`Server error`, err); + }); diff --git a/projects/mcp_server/src/init.ts b/projects/mcp_server/src/init.ts new file mode 100644 index 000000000000..e7e085d79f0a --- /dev/null +++ b/projects/mcp_server/src/init.ts @@ -0,0 +1,3 @@ +import * as dotenv from 'dotenv'; +dotenv.config(); +dotenv.config({ path: '.env.local' }); diff --git a/projects/mcp_server/src/type.d.ts b/projects/mcp_server/src/type.d.ts new file mode 100644 index 000000000000..c4ac82e49a93 --- /dev/null +++ b/projects/mcp_server/src/type.d.ts @@ -0,0 +1,5 @@ +declare namespace NodeJS { + interface ProcessEnv { + FASTGPT_ENDPOINT: string; + } +} diff --git a/projects/mcp_server/src/utils/error.ts b/projects/mcp_server/src/utils/error.ts new file mode 100644 index 000000000000..716a6ad5221b --- /dev/null +++ b/projects/mcp_server/src/utils/error.ts @@ -0,0 +1,10 @@ +import { replaceSensitiveText } from './string.js'; + +export const getErrText = (err: any, def = ''): any => { + const msg: string = + typeof err === 'string' + ? err + : err?.response?.data?.message || err?.response?.message || err?.message || def; + // msg && console.log('error =>', msg); + return replaceSensitiveText(msg); +}; diff --git a/projects/mcp_server/src/utils/log.ts b/projects/mcp_server/src/utils/log.ts new file mode 100644 index 000000000000..4af72e1f0e45 --- /dev/null +++ b/projects/mcp_server/src/utils/log.ts @@ -0,0 +1,68 @@ +import chalk from 'chalk'; +import dayjs from 'dayjs'; + +export enum LogLevelEnum { + debug = 0, + info = 1, + warn = 2, + error = 3 +} + +const logMap = { + [LogLevelEnum.debug]: { + levelLog: chalk.green('[Debug]') + }, + [LogLevelEnum.info]: { + levelLog: chalk.blue('[Info]') + }, + [LogLevelEnum.warn]: { + levelLog: chalk.yellow('[Warn]') + }, + [LogLevelEnum.error]: { + levelLog: chalk.red('[Error]') + } +}; + +/* add logger */ +export const addLog = { + log(level: LogLevelEnum, msg: string, obj: Record = {}) { + const stringifyObj = JSON.stringify(obj); + const isEmpty = Object.keys(obj).length === 0; + + console.log( + `${logMap[level].levelLog} ${dayjs().format('YYYY-MM-DD HH:mm:ss')} ${msg} ${ + level !== LogLevelEnum.error && !isEmpty ? stringifyObj : '' + }` + ); + + level === LogLevelEnum.error && console.error(obj); + }, + debug(msg: string, obj?: Record) { + this.log(LogLevelEnum.debug, msg, obj); + }, + info(msg: string, obj?: Record) { + this.log(LogLevelEnum.info, msg, obj); + }, + warn(msg: string, obj?: Record) { + this.log(LogLevelEnum.warn, msg, obj); + }, + error(msg: string, error?: any) { + this.log(LogLevelEnum.error, msg, { + message: error?.message || error, + stack: error?.stack, + ...(error?.config && { + config: { + headers: error.config.headers, + url: error.config.url, + data: error.config.data + } + }), + ...(error?.response && { + response: { + status: error.response.status, + statusText: error.response.statusText + } + }) + }); + } +}; diff --git a/projects/mcp_server/src/utils/string.ts b/projects/mcp_server/src/utils/string.ts new file mode 100644 index 000000000000..d88ee1e4e0ef --- /dev/null +++ b/projects/mcp_server/src/utils/string.ts @@ -0,0 +1,8 @@ +export const replaceSensitiveText = (text: string) => { + // 1. http link + text = text.replace(/(?<=https?:\/\/)[^\s]+/g, 'xxx'); + // 2. nx-xxx 全部替换成xxx + text = text.replace(/ns-[\w-]+/g, 'xxx'); + + return text; +}; diff --git a/projects/mcp_server/tsconfig.json b/projects/mcp_server/tsconfig.json new file mode 100644 index 000000000000..2abd685d3fa2 --- /dev/null +++ b/projects/mcp_server/tsconfig.json @@ -0,0 +1,17 @@ +{ + "include": ["src"], + "exclude": ["node_modules"], + "compilerOptions": { + "target": "ES2022", + "module": "esnext", + "moduleResolution": "node", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "outDir": "./dist", + "baseUrl": "./src", + "lib": ["ES2015", "DOM"] + } +} diff --git a/test/cases/service/core/app/workflow/dispatch/utils.test.ts b/test/cases/service/core/app/workflow/dispatch/utils.test.ts index b2e32ad0a56d..a9bd1f234bc1 100644 --- a/test/cases/service/core/app/workflow/dispatch/utils.test.ts +++ b/test/cases/service/core/app/workflow/dispatch/utils.test.ts @@ -249,41 +249,41 @@ describe('valueTypeFormat', () => { }); // value 为 null/undefined - const nullTestList = [ - { - value: undefined, - type: WorkflowIOValueTypeEnum.string, - result: '' - }, - { - value: undefined, - type: WorkflowIOValueTypeEnum.number, - result: 0 - }, - { - value: undefined, - type: WorkflowIOValueTypeEnum.boolean, - result: false - }, - { - value: undefined, - type: WorkflowIOValueTypeEnum.arrayAny, - result: [] - }, - { - value: undefined, - type: WorkflowIOValueTypeEnum.object, - result: {} - }, - { - value: undefined, - type: WorkflowIOValueTypeEnum.chatHistory, - result: [] - } - ]; - nullTestList.forEach((item, index) => { - it(`Null test ${index}`, () => { - expect(valueTypeFormat(item.value, item.type)).toEqual(item.result); - }); - }); + // const nullTestList = [ + // { + // value: undefined, + // type: WorkflowIOValueTypeEnum.string, + // result: '' + // }, + // { + // value: undefined, + // type: WorkflowIOValueTypeEnum.number, + // result: 0 + // }, + // { + // value: undefined, + // type: WorkflowIOValueTypeEnum.boolean, + // result: false + // }, + // { + // value: undefined, + // type: WorkflowIOValueTypeEnum.arrayAny, + // result: [] + // }, + // { + // value: undefined, + // type: WorkflowIOValueTypeEnum.object, + // result: {} + // }, + // { + // value: undefined, + // type: WorkflowIOValueTypeEnum.chatHistory, + // result: [] + // } + // ]; + // nullTestList.forEach((item, index) => { + // it(`Null test ${index}`, () => { + // expect(valueTypeFormat(item.value, item.type)).toEqual(item.result); + // }); + // }); }); From 2568d399b444b53cb80e4dd519296f9264972c47 Mon Sep 17 00:00:00 2001 From: Archer <545436317@qq.com> Date: Wed, 16 Apr 2025 13:27:42 +0800 Subject: [PATCH 05/12] perf: path selector (#4556) * perf: path selector * fix: docker file path --- .github/workflows/fastgpt-preview-image.yml | 4 +- .../zh-cn/docs/development/upgrading/496.md | 1 + .../app/src/components/common/folder/Path.tsx | 77 ++++++++++--------- .../dashboard/apps/CreateModal.tsx | 3 +- .../detail/Import/diffSource/APIDataset.tsx | 1 + .../app/src/pages/dashboard/apps/index.tsx | 1 + 6 files changed, 49 insertions(+), 38 deletions(-) diff --git a/.github/workflows/fastgpt-preview-image.yml b/.github/workflows/fastgpt-preview-image.yml index 42d275f2366d..ace04ee1871f 100644 --- a/.github/workflows/fastgpt-preview-image.yml +++ b/.github/workflows/fastgpt-preview-image.yml @@ -56,11 +56,11 @@ jobs: echo "DESCRIPTION=fastgpt-pr image" >> $GITHUB_OUTPUT echo "DOCKER_REPO_TAGGED=ghcr.io/${{ github.repository_owner }}/fastgpt-pr:fatsgpt_${{ github.event.pull_request.head.sha }}" >> $GITHUB_OUTPUT elif [[ "${{ matrix.image }}" == "sandbox" ]]; then - echo "DOCKERFILE=projects/app/sandbox.Dockerfile" >> $GITHUB_OUTPUT + echo "DOCKERFILE=projects/sandbox/Dockerfile" >> $GITHUB_OUTPUT echo "DESCRIPTION=fastgpt-sandbox-pr image" >> $GITHUB_OUTPUT echo "DOCKER_REPO_TAGGED=ghcr.io/${{ github.repository_owner }}/fastgpt-pr:fatsgpt_sandbox_${{ github.event.pull_request.head.sha }}" >> $GITHUB_OUTPUT elif [[ "${{ matrix.image }}" == "mcp-server" ]]; then - echo "DOCKERFILE=projects/app/mcp-server.Dockerfile" >> $GITHUB_OUTPUT + echo "DOCKERFILE=projects/mcp-server/Dockerfile" >> $GITHUB_OUTPUT echo "DESCRIPTION=fastgpt-mcp-server-pr image" >> $GITHUB_OUTPUT echo "DOCKER_REPO_TAGGED=ghcr.io/${{ github.repository_owner }}/fastgpt-pr:fatsgpt_mcp_server_${{ github.event.pull_request.head.sha }}" >> $GITHUB_OUTPUT fi diff --git a/docSite/content/zh-cn/docs/development/upgrading/496.md b/docSite/content/zh-cn/docs/development/upgrading/496.md index d2d977d8e0bc..546f27ba65ba 100644 --- a/docSite/content/zh-cn/docs/development/upgrading/496.md +++ b/docSite/content/zh-cn/docs/development/upgrading/496.md @@ -19,6 +19,7 @@ weight: 794 1. 工作流数据类型转化鲁棒性和兼容性增强。 2. Python sandbox 代码,支持大数据输入。 +3. 路径组件支持配置最后一步是否可点击。 ## 🐛 修复 diff --git a/projects/app/src/components/common/folder/Path.tsx b/projects/app/src/components/common/folder/Path.tsx index 61a2d1f7805d..f271f2626c49 100644 --- a/projects/app/src/components/common/folder/Path.tsx +++ b/projects/app/src/components/common/folder/Path.tsx @@ -11,6 +11,7 @@ const FolderPath = (props: { onClick: (parentId: string) => void; fontSize?: string; hoverStyle?: BoxProps; + forbidLastClick?: boolean; }) => { const { t } = useTranslation(); const { @@ -19,7 +20,8 @@ const FolderPath = (props: { FirstPathDom, onClick, fontSize, - hoverStyle + hoverStyle, + forbidLastClick = false } = props; const concatPaths = useMemo( @@ -37,41 +39,46 @@ const FolderPath = (props: { <>{FirstPathDom} ) : ( - {concatPaths.map((item, i) => ( - - 1 - ? { - cursor: 'default', - color: 'myGray.700', - fontWeight: 'bold' - } - : { - cursor: 'pointer', - fontWeight: 'medium', - color: 'myGray.500', - _hover: { - bg: 'myGray.100', - ...hoverStyle - }, - onClick: () => { - onClick(item.parentId); + {concatPaths.map((item, i) => { + const clickStyles = { + cursor: 'pointer', + _hover: { + bg: 'myGray.100', + ...hoverStyle + }, + onClick: () => { + onClick(item.parentId); + } + }; + return ( + + 1 + ? { + color: 'myGray.700', + fontWeight: 'bold' } - })} - > - {item.parentName} - - {i !== concatPaths.length - 1 && ( - - )} - - ))} + : { + fontWeight: 'medium', + color: 'myGray.500', + ...clickStyles + })} + {...(i === concatPaths.length - 1 && !forbidLastClick && clickStyles)} + > + {item.parentName} + + {i !== concatPaths.length - 1 && ( + + )} + + ); + })} ); }; diff --git a/projects/app/src/pageComponents/dashboard/apps/CreateModal.tsx b/projects/app/src/pageComponents/dashboard/apps/CreateModal.tsx index f10111ee9036..bcc571d8a1dd 100644 --- a/projects/app/src/pageComponents/dashboard/apps/CreateModal.tsx +++ b/projects/app/src/pageComponents/dashboard/apps/CreateModal.tsx @@ -190,7 +190,8 @@ const CreateModal = ({ onClose, type }: { type: CreateAppType; onClose: () => vo router.push({ pathname: '/dashboard/templateMarket', query: { - appType: type + appType: type, + parentId } }); onClose(); diff --git a/projects/app/src/pageComponents/dataset/detail/Import/diffSource/APIDataset.tsx b/projects/app/src/pageComponents/dataset/detail/Import/diffSource/APIDataset.tsx index a8facf7eec3f..a95e3a85df35 100644 --- a/projects/app/src/pageComponents/dataset/detail/Import/diffSource/APIDataset.tsx +++ b/projects/app/src/pageComponents/dataset/detail/Import/diffSource/APIDataset.tsx @@ -162,6 +162,7 @@ const CustomAPIFileInput = () => { { const index = paths.findIndex((item) => item.parentId === parentId); diff --git a/projects/app/src/pages/dashboard/apps/index.tsx b/projects/app/src/pages/dashboard/apps/index.tsx index 2465704dc239..2dccae31c553 100644 --- a/projects/app/src/pages/dashboard/apps/index.tsx +++ b/projects/app/src/pages/dashboard/apps/index.tsx @@ -133,6 +133,7 @@ const MyApps = ({ MenuIcon }: { MenuIcon: JSX.Element }) => { { router.push({ query: { From 9bb2085a5acb43e81185a4fbf16259e3d9d56017 Mon Sep 17 00:00:00 2001 From: Archer <545436317@qq.com> Date: Wed, 16 Apr 2025 13:46:53 +0800 Subject: [PATCH 06/12] perf: add image endpoint to dataset search (#4557) * perf: add image endpoint to dataset search * fix: mcp_server url --- .github/workflows/fastgpt-preview-image.yml | 8 ++++---- .../zh-cn/docs/development/upgrading/496.md | 1 + packages/service/common/file/image/utils.ts | 10 ++++++++++ packages/service/core/chat/utils.ts | 15 +++------------ .../core/workflow/dispatch/dataset/search.ts | 3 ++- 5 files changed, 20 insertions(+), 17 deletions(-) diff --git a/.github/workflows/fastgpt-preview-image.yml b/.github/workflows/fastgpt-preview-image.yml index ace04ee1871f..661881d1405d 100644 --- a/.github/workflows/fastgpt-preview-image.yml +++ b/.github/workflows/fastgpt-preview-image.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-24.04 strategy: matrix: - image: [fastgpt, sandbox, mcp-server] + image: [fastgpt, sandbox, mcp_server] fail-fast: false # 即使一个镜像构建失败,也继续构建其他镜像 steps: @@ -59,9 +59,9 @@ jobs: echo "DOCKERFILE=projects/sandbox/Dockerfile" >> $GITHUB_OUTPUT echo "DESCRIPTION=fastgpt-sandbox-pr image" >> $GITHUB_OUTPUT echo "DOCKER_REPO_TAGGED=ghcr.io/${{ github.repository_owner }}/fastgpt-pr:fatsgpt_sandbox_${{ github.event.pull_request.head.sha }}" >> $GITHUB_OUTPUT - elif [[ "${{ matrix.image }}" == "mcp-server" ]]; then - echo "DOCKERFILE=projects/mcp-server/Dockerfile" >> $GITHUB_OUTPUT - echo "DESCRIPTION=fastgpt-mcp-server-pr image" >> $GITHUB_OUTPUT + elif [[ "${{ matrix.image }}" == "mcp_server" ]]; then + echo "DOCKERFILE=projects/mcp_server/Dockerfile" >> $GITHUB_OUTPUT + echo "DESCRIPTION=fastgpt-mcp_server-pr image" >> $GITHUB_OUTPUT echo "DOCKER_REPO_TAGGED=ghcr.io/${{ github.repository_owner }}/fastgpt-pr:fatsgpt_mcp_server_${{ github.event.pull_request.head.sha }}" >> $GITHUB_OUTPUT fi diff --git a/docSite/content/zh-cn/docs/development/upgrading/496.md b/docSite/content/zh-cn/docs/development/upgrading/496.md index 546f27ba65ba..8a0ffb1fb62b 100644 --- a/docSite/content/zh-cn/docs/development/upgrading/496.md +++ b/docSite/content/zh-cn/docs/development/upgrading/496.md @@ -20,6 +20,7 @@ weight: 794 1. 工作流数据类型转化鲁棒性和兼容性增强。 2. Python sandbox 代码,支持大数据输入。 3. 路径组件支持配置最后一步是否可点击。 +4. 知识库工具调用结果,自动补充图片域名。 ## 🐛 修复 diff --git a/packages/service/common/file/image/utils.ts b/packages/service/common/file/image/utils.ts index 57820879df81..492662e70fcd 100644 --- a/packages/service/common/file/image/utils.ts +++ b/packages/service/common/file/image/utils.ts @@ -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( + /(? `${baseURL}${match}` + ); +}; diff --git a/packages/service/core/chat/utils.ts b/packages/service/core/chat/utils.ts index b5a70ace235d..6a36b13c380f 100644 --- a/packages/service/core/chat/utils.ts +++ b/packages/service/core/chat/utils.ts @@ -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( - /(? `${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; }; diff --git a/packages/service/core/workflow/dispatch/dataset/search.ts b/packages/service/core/workflow/dispatch/dataset/search.ts index a682b8358376..3d3996240c82 100644 --- a/packages/service/core/workflow/dispatch/dataset/search.ts +++ b/packages/service/core/workflow/dispatch/dataset/search.ts @@ -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()) })) }; } From f7437c1075e3c7445899e853f849c941e145c657 Mon Sep 17 00:00:00 2001 From: Archer <545436317@qq.com> Date: Wed, 16 Apr 2025 15:51:04 +0800 Subject: [PATCH 07/12] 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> --- .../zh-cn/docs/development/upgrading/496.md | 6 +- packages/global/core/chat/utils.ts | 66 +++++++--- .../global/core/workflow/runtime/utils.ts | 117 +++++++++++++++++- .../template/system/interactive/type.d.ts | 13 +- .../service/core/workflow/dispatch/index.ts | 3 +- .../core/workflow/dispatch/loop/runLoop.ts | 101 +++++++++++---- .../core/workflow/dispatch/plugin/runApp.ts | 14 ++- .../core/workflow/dispatch/tools/http468.ts | 3 +- .../core/workflow/dispatch/tools/runLaf.ts | 2 +- .../workflow/dispatch/tools/runUpdateVar.ts | 3 +- .../service/core/workflow/dispatch/utils.ts | 106 ---------------- .../core/chat/ChatContainer/ChatBox/index.tsx | 4 +- .../core/app/workflow/dispatch/utils.test.ts | 2 +- 13 files changed, 280 insertions(+), 160 deletions(-) diff --git a/docSite/content/zh-cn/docs/development/upgrading/496.md b/docSite/content/zh-cn/docs/development/upgrading/496.md index 8a0ffb1fb62b..cf7d7fa3a063 100644 --- a/docSite/content/zh-cn/docs/development/upgrading/496.md +++ b/docSite/content/zh-cn/docs/development/upgrading/496.md @@ -12,8 +12,9 @@ weight: 794 ## 🚀 新增内容 1. 以 MCP 方式对外提供应用调用。 -2. 增加工作台二级菜单,合并工具箱。 -3. 增加 grok3、GPT4.1、Gemini2.5 模型系统配置。 +2. 批量执行节点支持交互节点,可实现每一轮循环都人工参与。 +3. 增加工作台二级菜单,合并工具箱。 +4. 增加 grok3、GPT4.1、Gemini2.5 模型系统配置。 ## ⚙️ 优化 @@ -24,3 +25,4 @@ weight: 794 ## 🐛 修复 +1. 修复子工作流包含交互节点时,未成功恢复子工作流所有数据。 \ No newline at end of file diff --git a/packages/global/core/chat/utils.ts b/packages/global/core/chat/utils.ts index f33e00007f60..a946c7c10ba5 100644 --- a/packages/global/core/chat/utils.ts +++ b/packages/global/core/chat/utils.ts @@ -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((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( + (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; }; diff --git a/packages/global/core/workflow/runtime/utils.ts b/packages/global/core/workflow/runtime/utils.ts index 403b526e4728..b0d57472af40 100644 --- a/packages/global/core/workflow/runtime/utils.ts +++ b/packages/global/core/workflow/runtime/utils.ts @@ -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,112 @@ 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)) || + (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.chatHistory, + 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 {}; + } + + // 5. 默认返回原值 + return value; +}; + /* Get interaction information (if any) from the last AI message. What can be done: @@ -62,7 +172,10 @@ export const getLastInteractiveValue = ( return; } - if (lastValue.interactive.type === 'childrenInteractive') { + if ( + lastValue.interactive.type === 'childrenInteractive' || + lastValue.interactive.type === 'loopInteractive' + ) { return lastValue.interactive; } diff --git a/packages/global/core/workflow/template/system/interactive/type.d.ts b/packages/global/core/workflow/template/system/interactive/type.d.ts index f44f1ea887b6..e242b1860733 100644 --- a/packages/global/core/workflow/template/system/interactive/type.d.ts +++ b/packages/global/core/workflow/template/system/interactive/type.d.ts @@ -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; diff --git a/packages/service/core/workflow/dispatch/index.ts b/packages/service/core/workflow/dispatch/index.ts index be837281f2f8..4e8690aa07cc 100644 --- a/packages/service/core/workflow/dispatch/index.ts +++ b/packages/service/core/workflow/dispatch/index.ts @@ -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 } from './utils'; +import { valueTypeFormat } from '@fastgpt/global/core/workflow/runtime/utils'; import { filterWorkflowEdges, checkNodeRunStatus, diff --git a/packages/service/core/workflow/dispatch/loop/runLoop.ts b/packages/service/core/workflow/dispatch/loop/runLoop.ts index 9207f3053e25..9c3936adff81 100644 --- a/packages/service/core/workflow/dispatch/loop/runLoop.ts +++ b/packages/service/core/workflow/dispatch/loop/runLoop.ts @@ -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 { initWorkflowEdgeStatus } from '@fastgpt/global/core/workflow/runtime/utils'; type Props = ModuleDispatchProps<{ [NodeInputKeyEnum.loopInputArray]: Array; [NodeInputKeyEnum.childrenNodeIdList]: string[]; }>; type Response = DispatchNodeResultType<{ + [DispatchNodeResponseKeyEnum.interactive]?: LoopInteractive; [NodeOutputKeyEnum.loopArray]: Array; }>; @@ -21,6 +27,7 @@ export const dispatchLoop = async (props: Props): Promise => { const { params, runtimeEdges, + lastInteractive, runtimeNodes, node: { name } } = props; @@ -29,6 +36,8 @@ export const dispatchLoop = async (props: Props): Promise => { 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 => { 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 = 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( + initWorkflowEdgeStatus(runtimeEdges, interactiveData?.childrenResponse) + ) }); const loopOutputValue = response.flowResponses.find( @@ -71,8 +109,10 @@ export const dispatchLoop = async (props: Props): Promise => { )?.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 => { ...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]: [ { diff --git a/packages/service/core/workflow/dispatch/plugin/runApp.ts b/packages/service/core/workflow/dispatch/plugin/runApp.ts index 0412aa6f29f4..709a15febc67 100644 --- a/packages/service/core/workflow/dispatch/plugin/runApp.ts +++ b/packages/service/core/workflow/dispatch/plugin/runApp.ts @@ -6,6 +6,7 @@ import { SseResponseEventEnum } from '@fastgpt/global/core/workflow/runtime/cons import { getWorkflowEntryNodeIds, initWorkflowEdgeStatus, + rewriteNodeOutputByHistories, storeNodes2RuntimeNodes, textAdaptGptResponse } from '@fastgpt/global/core/workflow/runtime/utils'; @@ -107,8 +108,14 @@ export const dispatchRunAppNode = async (props: Props): Promise => { lastInteractive?.type === 'childrenInteractive' ? lastInteractive.params.childrenResponse : undefined; - const entryNodeIds = getWorkflowEntryNodeIds(nodes, childrenInteractive || undefined); - const runtimeNodes = storeNodes2RuntimeNodes(nodes, entryNodeIds); + const runtimeNodes = rewriteNodeOutputByHistories( + storeNodes2RuntimeNodes( + nodes, + getWorkflowEntryNodeIds(nodes, childrenInteractive || undefined) + ), + childrenInteractive + ); + const runtimeEdges = initWorkflowEdgeStatus(edges, childrenInteractive); const theQuery = childrenInteractive ? query @@ -170,7 +177,8 @@ export const dispatchRunAppNode = async (props: Props): Promise => { totalPoints: usagePoints, query: userChatInput, textOutput: text, - pluginDetail: appData.permission.hasWritePer ? flowResponses : undefined + pluginDetail: appData.permission.hasWritePer ? flowResponses : undefined, + mergeSignId: props.node.nodeId }, [DispatchNodeResponseKeyEnum.nodeDispatchUsages]: [ { diff --git a/packages/service/core/workflow/dispatch/tools/http468.ts b/packages/service/core/workflow/dispatch/tools/http468.ts index 6df026eaa449..4fc136c0d88a 100644 --- a/packages/service/core/workflow/dispatch/tools/http468.ts +++ b/packages/service/core/workflow/dispatch/tools/http468.ts @@ -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'; diff --git a/packages/service/core/workflow/dispatch/tools/runLaf.ts b/packages/service/core/workflow/dispatch/tools/runLaf.ts index badfaf47272a..5b39a37db116 100644 --- a/packages/service/core/workflow/dispatch/tools/runLaf.ts +++ b/packages/service/core/workflow/dispatch/tools/runLaf.ts @@ -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'; diff --git a/packages/service/core/workflow/dispatch/tools/runUpdateVar.ts b/packages/service/core/workflow/dispatch/tools/runUpdateVar.ts index 00daeab1f177..f2f651982840 100644 --- a/packages/service/core/workflow/dispatch/tools/runUpdateVar.ts +++ b/packages/service/core/workflow/dispatch/tools/runUpdateVar.ts @@ -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[]; diff --git a/packages/service/core/workflow/dispatch/utils.ts b/packages/service/core/workflow/dispatch/utils.ts index e10301d131a3..4fcc7f41f71b 100644 --- a/packages/service/core/workflow/dispatch/utils.ts +++ b/packages/service/core/workflow/dispatch/utils.ts @@ -104,112 +104,6 @@ export const getHistories = (history?: ChatItemType[] | number, histories: ChatI return [...systemHistories, ...filterHistories]; }; -/* 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)) || - (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.chatHistory, - 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 {}; - } - - // 5. 默认返回原值 - return value; -}; - export const checkQuoteQAValue = (quoteQA?: SearchDataResponseItemType[]) => { if (!quoteQA) return undefined; if (quoteQA.length === 0) { diff --git a/projects/app/src/components/core/chat/ChatContainer/ChatBox/index.tsx b/projects/app/src/components/core/chat/ChatContainer/ChatBox/index.tsx index 22642d610e90..69c9f6dc4c07 100644 --- a/projects/app/src/components/core/chat/ChatContainer/ChatBox/index.tsx +++ b/projects/app/src/components/core/chat/ChatContainer/ChatBox/index.tsx @@ -66,6 +66,7 @@ import { ChatItemContext } from '@/web/core/chat/context/chatItemContext'; import TimeBox from './components/TimeBox'; import MyBox from '@fastgpt/web/components/common/MyBox'; import { VariableInputEnum } from '@fastgpt/global/core/workflow/constants'; +import { valueTypeFormat } from '@fastgpt/global/core/workflow/runtime/utils'; const ResponseTags = dynamic(() => import('./components/ResponseTags')); const FeedbackModal = dynamic(() => import('./components/FeedbackModal')); @@ -440,12 +441,13 @@ const ChatBox = ({ // Only declared variables are kept const requestVariables: Record = {}; allVariableList?.forEach((item) => { - requestVariables[item.key] = + const val = variables[item.key] === '' || variables[item.key] === undefined || variables[item.key] === null ? item.defaultValue : variables[item.key]; + requestVariables[item.key] = valueTypeFormat(val, item.valueType); }); const responseChatId = getNanoid(24); diff --git a/test/cases/service/core/app/workflow/dispatch/utils.test.ts b/test/cases/service/core/app/workflow/dispatch/utils.test.ts index a9bd1f234bc1..3d9c9cc6af84 100644 --- a/test/cases/service/core/app/workflow/dispatch/utils.test.ts +++ b/test/cases/service/core/app/workflow/dispatch/utils.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect } from 'vitest'; -import { valueTypeFormat } from '@fastgpt/service/core/workflow/dispatch/utils'; import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants'; +import { valueTypeFormat } from '@fastgpt/global/core/workflow/runtime/utils'; describe('valueTypeFormat', () => { // value 为字符串 From ed0f016b5863295036af076d2a2a490cd639fd91 Mon Sep 17 00:00:00 2001 From: archer <545436317@qq.com> Date: Wed, 16 Apr 2025 16:30:59 +0800 Subject: [PATCH 08/12] mcp server ui --- .../pageComponents/dashboard/Container.tsx | 15 +++---- .../pageComponents/dashboard/mcp/usageWay.tsx | 2 +- .../src/pages/dashboard/mcpServer/index.tsx | 42 ++++++++++++++----- 3 files changed, 38 insertions(+), 21 deletions(-) diff --git a/projects/app/src/pageComponents/dashboard/Container.tsx b/projects/app/src/pageComponents/dashboard/Container.tsx index 14218a7d6aa0..59f65b918631 100644 --- a/projects/app/src/pageComponents/dashboard/Container.tsx +++ b/projects/app/src/pageComponents/dashboard/Container.tsx @@ -199,7 +199,7 @@ const DashboardContainer = ({ const MenuIcon = useMemo( () => ( - + {isOpenSidebar && ( )} - - + + ), [isOpenSidebar, onCloseSidebar, onOpenSidebar] ); @@ -250,7 +250,7 @@ const DashboardContainer = ({ p={2} fontSize={'sm'} rounded={'md'} - color={'myGray.900'} + color={'myGray.700'} cursor={'pointer'} _hover={{ bg: 'primary.50' @@ -263,14 +263,11 @@ const DashboardContainer = ({ {...(group.children.length === 0 && selected && { bg: 'primary.100', color: 'primary.600' })} > - - - {group.groupName} - + + {group.groupName} {group.children.length > 0 && ( diff --git a/projects/app/src/pageComponents/dashboard/mcp/usageWay.tsx b/projects/app/src/pageComponents/dashboard/mcp/usageWay.tsx index bdda202ee682..66f77387f1cd 100644 --- a/projects/app/src/pageComponents/dashboard/mcp/usageWay.tsx +++ b/projects/app/src/pageComponents/dashboard/mcp/usageWay.tsx @@ -27,7 +27,7 @@ const UsageWay = ({ mcp, onClose }: { mcp: McpKeyType; onClose: () => void }) => {t('dashboard_mcp:mcp_endpoints')} - + {sseUrl} diff --git a/projects/app/src/pages/dashboard/mcpServer/index.tsx b/projects/app/src/pages/dashboard/mcpServer/index.tsx index 1b696446107d..674ef639d929 100644 --- a/projects/app/src/pages/dashboard/mcpServer/index.tsx +++ b/projects/app/src/pages/dashboard/mcpServer/index.tsx @@ -25,6 +25,7 @@ import { useSystemStore } from '@/web/common/system/useSystemStore'; import { useConfirm } from '@fastgpt/web/hooks/useConfirm'; import dynamic from 'next/dynamic'; import { McpKeyType } from '@fastgpt/global/support/mcp/type'; +import { useSystem } from '@fastgpt/web/hooks/useSystem'; const UsageWay = dynamic(() => import('@/pageComponents/dashboard/mcp/usageWay'), { ssr: false @@ -32,7 +33,7 @@ const UsageWay = dynamic(() => import('@/pageComponents/dashboard/mcp/usageWay') const McpServer = () => { const { t } = useTranslation(); - const { feConfigs } = useSystemStore(); + const { isPc } = useSystem(); const { data: mcpServerList = [], @@ -61,21 +62,40 @@ const McpServer = () => { return ( <> - {() => ( + {({ MenuIcon }) => ( - - - - {t('dashboard_mcp:mcp_server')} + {isPc ? ( + + + + {t('dashboard_mcp:mcp_server')} + + + {t('dashboard_mcp:mcp_server_description')} + + + + ) : ( + <> + + {MenuIcon} + + {t('dashboard_mcp:mcp_server')} + + {t('dashboard_mcp:mcp_server_description')} - - - + + + + + )} {/* table */} From 76746178695ebf97f210b194be78bd1ab3c06103 Mon Sep 17 00:00:00 2001 From: heheer Date: Wed, 16 Apr 2025 18:35:33 +0800 Subject: [PATCH 09/12] 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 --- packages/global/core/app/constants.ts | 4 +- packages/global/core/app/mcpTools/utils.ts | 92 +++++++ packages/global/core/app/type.d.ts | 10 + packages/global/core/app/utils.ts | 4 +- .../global/core/workflow/node/constant.ts | 4 +- .../global/core/workflow/runtime/type.d.ts | 2 + .../global/core/workflow/runtime/utils.ts | 7 +- .../core/workflow/template/constants.ts | 6 +- .../core/workflow/template/system/runTool.ts | 19 ++ .../workflow/template/system/runToolSet.ts | 19 ++ packages/global/core/workflow/utils.ts | 32 +++ .../service/core/app/plugin/controller.ts | 51 +++- .../workflow/dispatch/agent/runTool/index.ts | 21 +- .../workflow/dispatch/agent/runTool/utils.ts | 48 ++++ .../service/core/workflow/dispatch/index.ts | 3 + .../core/workflow/dispatch/plugin/runTool.ts | 56 ++++ packages/service/package.json | 1 + .../web/components/common/Icon/button.tsx | 9 +- .../web/components/common/Icon/constants.ts | 5 +- .../common/Icon/icons/common/detail.svg | 3 + .../Icon/icons/core/app/type/mcpTools.svg | 3 + .../Icon/icons/core/app/type/mcpToolsFill.svg | 13 + packages/web/i18n/en/app.json | 7 + packages/web/i18n/en/common.json | 1 + packages/web/i18n/en/workflow.json | 1 + packages/web/i18n/zh-CN/app.json | 16 ++ packages/web/i18n/zh-CN/common.json | 1 + packages/web/i18n/zh-CN/workflow.json | 2 + packages/web/i18n/zh-Hant/app.json | 7 + packages/web/i18n/zh-Hant/common.json | 1 + packages/web/i18n/zh-Hant/workflow.json | 1 + pnpm-lock.yaml | 3 + .../ChatBox/hooks/useChatBox.tsx | 2 +- .../chat/components/WholeResponseModal.tsx | 3 + .../app/detail/MCPTools/AppCard.tsx | 86 ++++++ .../app/detail/MCPTools/ChatTest.tsx | 241 +++++++++++++++++ .../app/detail/MCPTools/Edit.tsx | 71 +++++ .../app/detail/MCPTools/EditForm.tsx | 253 +++++++++++++++++ .../app/detail/MCPTools/Header.tsx | 81 ++++++ .../app/detail/MCPTools/index.tsx | 39 +++ .../app/detail/SimpleApp/ChatTest.tsx | 3 +- .../SimpleApp/components/ToolSelect.tsx | 8 +- .../SimpleApp/components/ToolSelectModal.tsx | 25 ++ .../Flow/NodeTemplatesModal.tsx | 47 +++- .../detail/WorkflowComponents/Flow/index.tsx | 2 + .../Flow/nodes/NodeTool.tsx | 46 ++++ .../Flow/nodes/NodeToolSet.tsx | 63 +++++ .../Flow/nodes/render/NodeCard.tsx | 4 +- .../pageComponents/dashboard/apps/List.tsx | 4 +- .../dashboard/apps/MCPToolsEditModal.tsx | 256 ++++++++++++++++++ .../pageComponents/dashboard/apps/TypeTag.tsx | 7 + .../src/pages/api/core/app/mcpTools/create.ts | 64 +++++ .../api/core/app/mcpTools/getMCPTools.ts | 43 +++ .../pages/api/core/app/mcpTools/runTest.ts | 45 +++ .../src/pages/api/core/app/mcpTools/update.ts | 122 +++++++++ .../app/src/pages/api/core/chat/chatTest.ts | 18 +- projects/app/src/pages/app/detail/index.tsx | 5 + .../app/src/pages/dashboard/apps/index.tsx | 17 +- projects/app/src/web/core/app/api/plugin.ts | 27 +- 59 files changed, 1994 insertions(+), 40 deletions(-) create mode 100644 packages/global/core/app/mcpTools/utils.ts create mode 100644 packages/global/core/workflow/template/system/runTool.ts create mode 100644 packages/global/core/workflow/template/system/runToolSet.ts create mode 100644 packages/service/core/workflow/dispatch/plugin/runTool.ts create mode 100644 packages/web/components/common/Icon/icons/common/detail.svg create mode 100644 packages/web/components/common/Icon/icons/core/app/type/mcpTools.svg create mode 100644 packages/web/components/common/Icon/icons/core/app/type/mcpToolsFill.svg create mode 100644 projects/app/src/pageComponents/app/detail/MCPTools/AppCard.tsx create mode 100644 projects/app/src/pageComponents/app/detail/MCPTools/ChatTest.tsx create mode 100644 projects/app/src/pageComponents/app/detail/MCPTools/Edit.tsx create mode 100644 projects/app/src/pageComponents/app/detail/MCPTools/EditForm.tsx create mode 100644 projects/app/src/pageComponents/app/detail/MCPTools/Header.tsx create mode 100644 projects/app/src/pageComponents/app/detail/MCPTools/index.tsx create mode 100644 projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/NodeTool.tsx create mode 100644 projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/NodeToolSet.tsx create mode 100644 projects/app/src/pageComponents/dashboard/apps/MCPToolsEditModal.tsx create mode 100644 projects/app/src/pages/api/core/app/mcpTools/create.ts create mode 100644 projects/app/src/pages/api/core/app/mcpTools/getMCPTools.ts create mode 100644 projects/app/src/pages/api/core/app/mcpTools/runTest.ts create mode 100644 projects/app/src/pages/api/core/app/mcpTools/update.ts diff --git a/packages/global/core/app/constants.ts b/packages/global/core/app/constants.ts index ab18297d5286..3b79b49ba66d 100644 --- a/packages/global/core/app/constants.ts +++ b/packages/global/core/app/constants.ts @@ -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]; diff --git a/packages/global/core/app/mcpTools/utils.ts b/packages/global/core/app/mcpTools/utils.ts new file mode 100644 index 000000000000..64745692ec6d --- /dev/null +++ b/packages/global/core/app/mcpTools/utils.ts @@ -0,0 +1,92 @@ +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'; + +export const getMCPToolSetNodes = ({ + url, + toolList, + name, + avatar +}: { + url: string; + toolList: ToolType[]; + name?: string; + avatar?: string; +}) => { + 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 getMCPToolNodes = ({ tool, url }: { tool: ToolType; url: string }) => { + return [ + { + nodeId: nanoid(16), + flowNodeType: FlowNodeTypeEnum.tool, + avatar: 'core/app/type/mcpToolsFill', + 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), + 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: '' + } + ]; +}; diff --git a/packages/global/core/app/type.d.ts b/packages/global/core/app/type.d.ts index f852efbc976c..ee9c916cce43 100644 --- a/packages/global/core/app/type.d.ts +++ b/packages/global/core/app/type.d.ts @@ -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; + required?: string[]; + }; +}; + export type AppSchema = { _id: string; parentId?: ParentIdType; diff --git a/packages/global/core/app/utils.ts b/packages/global/core/app/utils.ts index 17794dee921f..92e1d1dc8952 100644 --- a/packages/global/core/app/utils.ts +++ b/packages/global/core/app/utils.ts @@ -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; diff --git a/packages/global/core/workflow/node/constant.ts b/packages/global/core/workflow/node/constant.ts index af4aa98b7b0d..3d92308c220c 100644 --- a/packages/global/core/workflow/node/constant.ts +++ b/packages/global/core/workflow/node/constant.ts @@ -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 diff --git a/packages/global/core/workflow/runtime/type.d.ts b/packages/global/core/workflow/runtime/type.d.ts index 39058da5a67c..be2885f106d8 100644 --- a/packages/global/core/workflow/runtime/type.d.ts +++ b/packages/global/core/workflow/runtime/type.d.ts @@ -217,6 +217,8 @@ export type DispatchNodeResponseType = { // tool params toolParamsResult?: Record; + toolRes?: any; + // abandon extensionModel?: string; extensionResult?: string; diff --git a/packages/global/core/workflow/runtime/utils.ts b/packages/global/core/workflow/runtime/utils.ts index b0d57472af40..fc98804a7484 100644 --- a/packages/global/core/workflow/runtime/utils.ts +++ b/packages/global/core/workflow/runtime/utils.ts @@ -227,7 +227,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); }; diff --git a/packages/global/core/workflow/template/constants.ts b/packages/global/core/workflow/template/constants.ts index bc2d9fada8fe..c8478302550d 100644 --- a/packages/global/core/workflow/template/constants.ts +++ b/packages/global/core/workflow/template/constants.ts @@ -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 ]; diff --git a/packages/global/core/workflow/template/system/runTool.ts b/packages/global/core/workflow/template/system/runTool.ts new file mode 100644 index 000000000000..4e4f6fa6e074 --- /dev/null +++ b/packages/global/core/workflow/template/system/runTool.ts @@ -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: [] +}; diff --git a/packages/global/core/workflow/template/system/runToolSet.ts b/packages/global/core/workflow/template/system/runToolSet.ts new file mode 100644 index 000000000000..f193c7d7491f --- /dev/null +++ b/packages/global/core/workflow/template/system/runToolSet.ts @@ -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: [] +}; diff --git a/packages/global/core/workflow/utils.ts b/packages/global/core/workflow/utils.ts index 06307c403415..b9fef2b332d6 100644 --- a/packages/global/core/workflow/utils.ts +++ b/packages/global/core/workflow/utils.ts @@ -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[] => { diff --git a/packages/service/core/app/plugin/controller.ts b/packages/service/core/app/plugin/controller.ts index dbbb7e273dbf..ec5b0b283418 100644 --- a/packages/service/core/app/plugin/controller.ts +++ b/packages/service/core/app/plugin/controller.ts @@ -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 }; } diff --git a/packages/service/core/workflow/dispatch/agent/runTool/index.ts b/packages/service/core/workflow/dispatch/agent/runTool/index.ts index 7557862676ef..13b0d60b7ce5 100644 --- a/packages/service/core/workflow/dispatch/agent/runTool/index.ts +++ b/packages/service/core/workflow/dispatch/agent/runTool/index.ts @@ -22,9 +22,9 @@ 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 { filterToolResponseToPreview, formatRuntimeWorkFlow } from './utils'; import { InteractiveNodeResponseType } from '@fastgpt/global/core/workflow/template/system/interactive/type'; import { getFileContentFromLinks, getHistoryFileLinks } from '../../tools/readFiles'; import { parseUrlToFileType } from '@fastgpt/global/common/file/tools'; @@ -32,6 +32,8 @@ import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; import { ModelTypeEnum } from '@fastgpt/global/core/ai/model'; import { getDocumentQuotePrompt } from '@fastgpt/global/core/ai/prompt/AIChat'; import { postTextCensor } from '../../../../chat/postTextCensor'; +import { ToolType } from '@fastgpt/global/core/app/type'; +import { getMCPToolNodes } from '@fastgpt/global/core/app/mcpTools/utils'; type Response = DispatchNodeResultType<{ [NodeOutputKeyEnum.answerText]: string; @@ -41,8 +43,8 @@ type Response = DispatchNodeResultType<{ export const dispatchRunTools = async (props: DispatchToolModuleProps): Promise => { const { node: { nodeId, name, isEntry, version }, - runtimeNodes, - runtimeEdges, + runtimeNodes: originRuntimeNodes, + runtimeEdges: originRuntimeEdges, histories, query, requestOrigin, @@ -67,6 +69,11 @@ export const dispatchRunTools = async (props: DispatchToolModuleProps): Promise< props.params.aiChatVision = aiChatVision && toolModel.vision; props.params.aiChatReasoning = aiChatReasoning && toolModel.reasoning; + const { runtimeNodes, runtimeEdges } = formatRuntimeWorkFlow( + originRuntimeNodes, + originRuntimeEdges + ); + const toolNodeIds = filterToolNodeIdByEdges({ nodeId, edges: runtimeEdges }); // Gets the module to which the tool is connected @@ -188,6 +195,8 @@ export const dispatchRunTools = async (props: DispatchToolModuleProps): Promise< if (toolModel.toolChoice) { return runToolWithToolChoice({ ...props, + runtimeNodes, + runtimeEdges, toolNodes, toolModel, maxRunToolTimes: 30, @@ -198,6 +207,8 @@ export const dispatchRunTools = async (props: DispatchToolModuleProps): Promise< if (toolModel.functionCall) { return runToolWithFunctionCall({ ...props, + runtimeNodes, + runtimeEdges, toolNodes, toolModel, messages: adaptMessages, @@ -226,6 +237,8 @@ export const dispatchRunTools = async (props: DispatchToolModuleProps): Promise< return runToolWithPromptCall({ ...props, + runtimeNodes, + runtimeEdges, toolNodes, toolModel, messages: adaptMessages, diff --git a/packages/service/core/workflow/dispatch/agent/runTool/utils.ts b/packages/service/core/workflow/dispatch/agent/runTool/utils.ts index 61ea478af7a8..d363e1673169 100644 --- a/packages/service/core/workflow/dispatch/agent/runTool/utils.ts +++ b/packages/service/core/workflow/dispatch/agent/runTool/utils.ts @@ -4,6 +4,8 @@ import { AIChatItemValueItemType } from '@fastgpt/global/core/chat/type'; import { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io'; import { RuntimeEdgeItemType } from '@fastgpt/global/core/workflow/type/edge'; import { RuntimeNodeItemType } from '@fastgpt/global/core/workflow/runtime/type'; +import { getMCPToolNodes } from '@fastgpt/global/core/app/mcpTools/utils'; +import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; export const updateToolInputValue = ({ params, @@ -68,3 +70,49 @@ export const initToolNodes = ( } }); }; + +export const formatRuntimeWorkFlow = ( + nodes: RuntimeNodeItemType[], + edges: RuntimeEdgeItemType[] +) => { + const newNodes = [...nodes]; + const newEdges = [...edges]; + + const toolSetNodes = nodes.filter((node) => node.flowNodeType === FlowNodeTypeEnum.toolSet); + + if (toolSetNodes.length === 0) { + return { runtimeNodes: nodes, runtimeEdges: edges }; + } + + const nodeIdsToRemove = new Set(); + + 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 = newEdges.filter((edge) => edge.target === toolSetNode.nodeId); + + for (const tool of toolList) { + const newToolNodes = getMCPToolNodes({ tool, url }); + + newNodes.push({ ...newToolNodes[0], name: `${toolSetNode.name} - ${tool.name}` }); + + for (const inEdge of incomingEdges) { + newEdges.push({ + source: inEdge.source, + target: newToolNodes[0].nodeId, + sourceHandle: inEdge.sourceHandle, + targetHandle: 'selectedTools', + status: inEdge.status + }); + } + } + } + + const filteredNodes = newNodes.filter((node) => !nodeIdsToRemove.has(node.nodeId)); + const filteredEdges = newEdges.filter((edge) => !nodeIdsToRemove.has(edge.target)); + + return { runtimeNodes: filteredNodes, runtimeEdges: filteredEdges }; +}; diff --git a/packages/service/core/workflow/dispatch/index.ts b/packages/service/core/workflow/dispatch/index.ts index 4e8690aa07cc..6fd183dc56b3 100644 --- a/packages/service/core/workflow/dispatch/index.ts +++ b/packages/service/core/workflow/dispatch/index.ts @@ -75,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.workflowStart]: dispatchWorkflowStart, @@ -105,6 +106,7 @@ const callbackMap: Record = { [FlowNodeTypeEnum.loopStart]: dispatchLoopStart, [FlowNodeTypeEnum.loopEnd]: dispatchLoopEnd, [FlowNodeTypeEnum.formInput]: dispatchFormInput, + [FlowNodeTypeEnum.tool]: dispatchRunTool, // none [FlowNodeTypeEnum.systemConfig]: dispatchSystemConfig, @@ -112,6 +114,7 @@ const callbackMap: Record = { [FlowNodeTypeEnum.emptyNode]: () => Promise.resolve(), [FlowNodeTypeEnum.globalVariable]: () => Promise.resolve(), [FlowNodeTypeEnum.comment]: () => Promise.resolve(), + [FlowNodeTypeEnum.toolSet]: () => Promise.resolve(), [FlowNodeTypeEnum.runApp]: dispatchAppRequest // abandoned }; diff --git a/packages/service/core/workflow/dispatch/plugin/runTool.ts b/packages/service/core/workflow/dispatch/plugin/runTool.ts new file mode 100644 index 000000000000..20752c03a868 --- /dev/null +++ b/packages/service/core/workflow/dispatch/plugin/runTool.ts @@ -0,0 +1,56 @@ +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 => { + const { params } = 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 + }, + [DispatchNodeResponseKeyEnum.toolResponses]: result, + [NodeOutputKeyEnum.rawResponse]: result + }; +}; diff --git a/packages/service/package.json b/packages/service/package.json index 0865803fe299..894514e2e297 100644 --- a/packages/service/package.json +++ b/packages/service/package.json @@ -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", diff --git a/packages/web/components/common/Icon/button.tsx b/packages/web/components/common/Icon/button.tsx index 4ec1f9656fec..13f9164952cc 100644 --- a/packages/web/components/common/Icon/button.tsx +++ b/packages/web/components/common/Icon/button.tsx @@ -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; diff --git a/packages/web/components/common/Icon/constants.ts b/packages/web/components/common/Icon/constants.ts index 16e1bf32736a..7b783907f8a9 100644 --- a/packages/web/components/common/Icon/constants.ts +++ b/packages/web/components/common/Icon/constants.ts @@ -33,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'), @@ -158,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'), @@ -170,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'), @@ -184,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'), diff --git a/packages/web/components/common/Icon/icons/common/detail.svg b/packages/web/components/common/Icon/icons/common/detail.svg new file mode 100644 index 000000000000..d8b72f46d30f --- /dev/null +++ b/packages/web/components/common/Icon/icons/common/detail.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/packages/web/components/common/Icon/icons/core/app/type/mcpTools.svg b/packages/web/components/common/Icon/icons/core/app/type/mcpTools.svg new file mode 100644 index 000000000000..ff1f125ba13e --- /dev/null +++ b/packages/web/components/common/Icon/icons/core/app/type/mcpTools.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/web/components/common/Icon/icons/core/app/type/mcpToolsFill.svg b/packages/web/components/common/Icon/icons/core/app/type/mcpToolsFill.svg new file mode 100644 index 000000000000..3a6adcba915a --- /dev/null +++ b/packages/web/components/common/Icon/icons/core/app/type/mcpToolsFill.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/packages/web/i18n/en/app.json b/packages/web/i18n/en/app.json index 18149468e306..356d45572927 100644 --- a/packages/web/i18n/en/app.json +++ b/packages/web/i18n/en/app.json @@ -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!", diff --git a/packages/web/i18n/en/common.json b/packages/web/i18n/en/common.json index 8858a0ec57a7..026d866c6b57 100644 --- a/packages/web/i18n/en/common.json +++ b/packages/web/i18n/en/common.json @@ -179,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", diff --git a/packages/web/i18n/en/workflow.json b/packages/web/i18n/en/workflow.json index 9d0d44e1cb82..8354ac10f9e6 100644 --- a/packages/web/i18n/en/workflow.json +++ b/packages/web/i18n/en/workflow.json @@ -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", diff --git a/packages/web/i18n/zh-CN/app.json b/packages/web/i18n/zh-CN/app.json index c84f2ce04649..b828d9f1dee1 100644 --- a/packages/web/i18n/zh-CN/app.json +++ b/packages/web/i18n/zh-CN/app.json @@ -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": "工作流", diff --git a/packages/web/i18n/zh-CN/common.json b/packages/web/i18n/zh-CN/common.json index af5051daa471..0473462c9cbd 100644 --- a/packages/web/i18n/zh-CN/common.json +++ b/packages/web/i18n/zh-CN/common.json @@ -179,6 +179,7 @@ "common.Other": "其他", "common.Output": "输出", "common.Params": "参数", + "common.Parse": "解析", "common.Password inconsistency": "两次密码不一致", "common.Permission": "权限", "common.Permission_tip": "个人权限大于群组权限", diff --git a/packages/web/i18n/zh-CN/workflow.json b/packages/web/i18n/zh-CN/workflow.json index 8a1ce7289f9f..7d2d2229b242 100644 --- a/packages/web/i18n/zh-CN/workflow.json +++ b/packages/web/i18n/zh-CN/workflow.json @@ -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": "更新链接异常", diff --git a/packages/web/i18n/zh-Hant/app.json b/packages/web/i18n/zh-Hant/app.json index 8b53d2a173da..357b783a36fc 100644 --- a/packages/web/i18n/zh-Hant/app.json +++ b/packages/web/i18n/zh-Hant/app.json @@ -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": "轉換成工作流程後,將無法轉換回簡易模式,請確認!", diff --git a/packages/web/i18n/zh-Hant/common.json b/packages/web/i18n/zh-Hant/common.json index 34ddea030610..576a72eca975 100644 --- a/packages/web/i18n/zh-Hant/common.json +++ b/packages/web/i18n/zh-Hant/common.json @@ -178,6 +178,7 @@ "common.Other": "其他", "common.Output": "輸出", "common.Params": "參數", + "common.Parse": "解析", "common.Password inconsistency": "兩次密碼不一致", "common.Permission": "權限", "common.Permission_tip": "個人權限大於群組權限", diff --git a/packages/web/i18n/zh-Hant/workflow.json b/packages/web/i18n/zh-Hant/workflow.json index b4b682537aa4..0681d61cb2aa 100644 --- a/packages/web/i18n/zh-Hant/workflow.json +++ b/packages/web/i18n/zh-Hant/workflow.json @@ -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": "更新連結發生錯誤", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 44e674954da4..8dbc4720789c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -160,6 +160,9 @@ importers: '@fastgpt/global': specifier: workspace:* version: link:../global + '@modelcontextprotocol/sdk': + specifier: ^1.9.0 + version: 1.9.0 '@node-rs/jieba': specifier: 2.0.1 version: 2.0.1 diff --git a/projects/app/src/components/core/chat/ChatContainer/ChatBox/hooks/useChatBox.tsx b/projects/app/src/components/core/chat/ChatContainer/ChatBox/hooks/useChatBox.tsx index 5aeb248ec8a5..76eb9f152f4e 100644 --- a/projects/app/src/components/core/chat/ChatContainer/ChatBox/hooks/useChatBox.tsx +++ b/projects/app/src/components/core/chat/ChatContainer/ChatBox/hooks/useChatBox.tsx @@ -55,7 +55,7 @@ export const useChatBox = () => { `; } else if (item.type === ChatItemValueTypeEnum.tool) { return ` -\`\`\`Toll +\`\`\`Tool ${JSON.stringify(item.tools, null, 2)} \`\`\` `; diff --git a/projects/app/src/components/core/chat/components/WholeResponseModal.tsx b/projects/app/src/components/core/chat/components/WholeResponseModal.tsx index babef946c15f..8b01d7d76845 100644 --- a/projects/app/src/components/core/chat/components/WholeResponseModal.tsx +++ b/projects/app/src/components/core/chat/components/WholeResponseModal.tsx @@ -438,6 +438,9 @@ export const WholeResponseContent = ({ label={t('workflow:tool_params.tool_params_result')} value={activeModule?.toolParamsResult} /> + + {/* tool */} + ) : null; }; diff --git a/projects/app/src/pageComponents/app/detail/MCPTools/AppCard.tsx b/projects/app/src/pageComponents/app/detail/MCPTools/AppCard.tsx new file mode 100644 index 000000000000..f0855b7e91de --- /dev/null +++ b/projects/app/src/pageComponents/app/detail/MCPTools/AppCard.tsx @@ -0,0 +1,86 @@ +import { Box, Button, Flex, HStack, IconButton } from '@chakra-ui/react'; +import React, { useState } from 'react'; +import { AppContext } from '../context'; +import { useContextSelector } from 'use-context-selector'; +import Avatar from '@fastgpt/web/components/common/Avatar'; +import { useTranslation } from 'react-i18next'; +import MyIcon from '@fastgpt/web/components/common/Icon'; +import MyMenu from '@fastgpt/web/components/common/MyMenu'; +import { AppSchema } from '@fastgpt/global/core/app/type'; +import TagsEditModal from '../TagsEditModal'; + +const AppCard = () => { + const { t } = useTranslation(); + + const appDetail = useContextSelector(AppContext, (v) => v.appDetail); + const onOpenInfoEdit = useContextSelector(AppContext, (v) => v.onOpenInfoEdit); + const onDelApp = useContextSelector(AppContext, (v) => v.onDelApp); + + const [TeamTagsSet, setTeamTagsSet] = useState(); + + return ( + <> + + + + + {appDetail.name} + + + + {appDetail.intro || t('common:core.app.tip.Add a intro to app')} + + + {appDetail.permission.hasManagePer && ( + + )} + {appDetail.permission.isOwner && ( + } + aria-label={''} + /> + } + menuList={[ + { + children: [ + { + icon: 'delete', + type: 'danger', + label: t('common:common.Delete'), + onClick: onDelApp + } + ] + } + ]} + /> + )} + + + + {TeamTagsSet && setTeamTagsSet(undefined)} />} + + ); +}; + +export default React.memo(AppCard); diff --git a/projects/app/src/pageComponents/app/detail/MCPTools/ChatTest.tsx b/projects/app/src/pageComponents/app/detail/MCPTools/ChatTest.tsx new file mode 100644 index 000000000000..9679835c3cf7 --- /dev/null +++ b/projects/app/src/pageComponents/app/detail/MCPTools/ChatTest.tsx @@ -0,0 +1,241 @@ +import { useChatStore } from '@/web/core/chat/context/useChatStore'; +import React, { useEffect, useMemo, useState } from 'react'; +import { useContextSelector } from 'use-context-selector'; +import { AppContext } from '../context'; +import ChatItemContextProvider from '@/web/core/chat/context/chatItemContext'; +import ChatRecordContextProvider from '@/web/core/chat/context/chatRecordContext'; +import { Box, Button, Flex, Switch, Textarea } from '@chakra-ui/react'; +import { cardStyles } from '../constants'; +import { useTranslation } from 'react-i18next'; +import { ToolType } from '@fastgpt/global/core/app/type'; +import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel'; +import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip'; +import { Controller, useForm } from 'react-hook-form'; +import MyNumberInput from '@fastgpt/web/components/common/Input/NumberInput'; +import dynamic from 'next/dynamic'; +import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; +import Markdown from '@/components/Markdown'; +import { postRunMCPTools } from '@/web/core/app/api/plugin'; + +const JsonEditor = dynamic(() => import('@fastgpt/web/components/common/Textarea/JsonEditor')); + +const ChatTest = ({ currentTool, url }: { currentTool: ToolType | null; url: string }) => { + const { t } = useTranslation(); + + const [output, setOutput] = useState(''); + + const { + control, + handleSubmit, + reset, + formState: { errors } + } = useForm(); + + useEffect(() => { + reset({}); + setOutput(''); + }, [currentTool, reset]); + + const { runAsync: runTool, loading: isRunning } = useRequest2( + async (data: Record) => { + if (!currentTool) return; + return await postRunMCPTools({ + params: data, + url, + toolName: currentTool.name + }); + }, + { + onSuccess: (res) => { + try { + const resStr = JSON.stringify(res, null, 2); + setOutput(resStr); + } catch (error) { + console.error(error); + } + } + } + ); + + return ( + + + + + {t('app:chat_debug')} + + + + + + {Object.keys(currentTool?.inputSchema.properties || {}).length > 0 && ( + <> + + {t('common:common.Input')} + + + {Object.entries(currentTool?.inputSchema.properties || {}).map( + ([paramName, paramInfo]) => ( + { + if (!currentTool?.inputSchema.required?.includes(paramName)) return true; + return !!value; + } + }} + render={({ field: { onChange, value } }) => { + return ( + + ); + }} + /> + ) + )} + + + )} + + + + {output && ( + <> + + {t('common:common.Output')} + + + + + + )} + + + + ); +}; + +const Render = ({ currentTool, url }: { currentTool: ToolType | null; url: string }) => { + const { chatId } = useChatStore(); + const { appDetail } = useContextSelector(AppContext, (v) => v); + + const chatRecordProviderParams = useMemo( + () => ({ + chatId: chatId, + appId: appDetail._id + }), + [appDetail._id, chatId] + ); + + return ( + + + + + + ); +}; + +export default React.memo(Render); + +const RenderToolInput = ({ + paramName, + paramInfo, + toolData, + value, + onChange, + isInvalid +}: { + paramName: string; + paramInfo: { + type: string; + description?: string; + }; + toolData: ToolType | null; + value: any; + onChange: (value: any) => void; + isInvalid: boolean; +}) => { + const render = (() => { + if (paramInfo.type === 'string') { + return ( +