feat: the frontend part of mcp (#22131)
Co-authored-by: jZonG <jzongcode@gmail.com> Co-authored-by: Novice <novice12185727@gmail.com> Co-authored-by: nite-knite <nkCoding@gmail.com> Co-authored-by: Hanqing Zhao <sherry9277@gmail.com>
This commit is contained in:
@@ -1,19 +1,48 @@
|
||||
'use client'
|
||||
import { useSearchParams } from 'next/navigation'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
const Empty = () => {
|
||||
import { ToolTypeEnum } from '../../workflow/block-selector/types'
|
||||
import { RiArrowRightUpLine } from '@remixicon/react'
|
||||
import Link from 'next/link'
|
||||
import cn from '@/utils/classnames'
|
||||
import { NoToolPlaceholder } from '../../base/icons/src/vender/other'
|
||||
type Props = {
|
||||
type?: ToolTypeEnum
|
||||
isAgent?: boolean
|
||||
}
|
||||
|
||||
const getLink = (type?: ToolTypeEnum) => {
|
||||
switch (type) {
|
||||
case ToolTypeEnum.Custom:
|
||||
return '/tools?category=api'
|
||||
case ToolTypeEnum.MCP:
|
||||
return '/tools?category=mcp'
|
||||
default:
|
||||
return '/tools?category=api'
|
||||
}
|
||||
}
|
||||
const Empty = ({
|
||||
type,
|
||||
isAgent,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const searchParams = useSearchParams()
|
||||
|
||||
const hasLink = type && [ToolTypeEnum.Custom, ToolTypeEnum.MCP].includes(type)
|
||||
const Comp = (hasLink ? Link : 'div') as any
|
||||
const linkProps = hasLink ? { href: getLink(type), target: '_blank' } : {}
|
||||
const renderType = isAgent ? 'agent' : type
|
||||
const hasTitle = t(`tools.addToolModal.${renderType}.title`) !== `tools.addToolModal.${renderType}.title`
|
||||
|
||||
return (
|
||||
<div className='flex flex-col items-center'>
|
||||
<div className="h-[149px] w-[163px] shrink-0 bg-[url('~@/app/components/tools/add-tool-modal/empty.png')] bg-cover bg-no-repeat"></div>
|
||||
<div className='mb-1 text-[13px] font-medium leading-[18px] text-text-primary'>
|
||||
{t(`tools.addToolModal.${searchParams.get('category') === 'workflow' ? 'emptyTitle' : 'emptyTitleCustom'}`)}
|
||||
</div>
|
||||
<div className='text-[13px] leading-[18px] text-text-tertiary'>
|
||||
{t(`tools.addToolModal.${searchParams.get('category') === 'workflow' ? 'emptyTip' : 'emptyTipCustom'}`)}
|
||||
<div className='flex h-[336px] flex-col items-center justify-center'>
|
||||
<NoToolPlaceholder />
|
||||
<div className='mb-1 mt-2 text-[13px] font-medium leading-[18px] text-text-primary'>
|
||||
{hasTitle ? t(`tools.addToolModal.${renderType}.title`) : 'No tools available'}
|
||||
</div>
|
||||
{(!isAgent && hasTitle) && (
|
||||
<Comp className={cn('flex items-center text-[13px] leading-[18px] text-text-tertiary', hasLink && 'cursor-pointer hover:text-text-accent')} {...linkProps}>
|
||||
{t(`tools.addToolModal.${renderType}.tip`)} {hasLink && <RiArrowRightUpLine className='ml-0.5 h-3 w-3' />}
|
||||
</Comp>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -184,6 +184,7 @@ const EditCustomCollectionModal: FC<Props> = ({
|
||||
onClose={onHide}
|
||||
closable
|
||||
className='!h-[calc(100vh-16px)] !max-w-[630px] !p-0'
|
||||
wrapperClassName='z-[1000]'
|
||||
>
|
||||
<div className='flex h-full flex-col'>
|
||||
<div className='ml-6 mt-6 text-base font-semibold text-text-primary'>
|
||||
|
||||
75
web/app/components/tools/mcp/create-card.tsx
Normal file
75
web/app/components/tools/mcp/create-card.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
'use client'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import {
|
||||
RiAddCircleFill,
|
||||
RiArrowRightUpLine,
|
||||
RiBookOpenLine,
|
||||
} from '@remixicon/react'
|
||||
import MCPModal from './modal'
|
||||
import I18n from '@/context/i18n'
|
||||
import { getLanguage } from '@/i18n/language'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { useCreateMCP } from '@/service/use-tools'
|
||||
import type { ToolWithProvider } from '@/app/components/workflow/types'
|
||||
|
||||
type Props = {
|
||||
handleCreate: (provider: ToolWithProvider) => void
|
||||
}
|
||||
|
||||
const NewMCPCard = ({ handleCreate }: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const { locale } = useContext(I18n)
|
||||
const language = getLanguage(locale)
|
||||
const { isCurrentWorkspaceManager } = useAppContext()
|
||||
|
||||
const { mutateAsync: createMCP } = useCreateMCP()
|
||||
|
||||
const create = async (info: any) => {
|
||||
const provider = await createMCP(info)
|
||||
handleCreate(provider)
|
||||
}
|
||||
|
||||
const linkUrl = useMemo(() => {
|
||||
if (language.startsWith('zh_'))
|
||||
return 'https://docs.dify.ai/zh-hans/guides/tools/mcp'
|
||||
if (language.startsWith('ja_jp'))
|
||||
return 'https://docs.dify.ai/ja_jp/guides/tools/mcp'
|
||||
return 'https://docs.dify.ai/en/guides/tools/mcp'
|
||||
}, [language])
|
||||
|
||||
const [showModal, setShowModal] = useState(false)
|
||||
|
||||
return (
|
||||
<>
|
||||
{isCurrentWorkspaceManager && (
|
||||
<div className='col-span-1 flex min-h-[108px] cursor-pointer flex-col rounded-xl bg-background-default-dimmed transition-all duration-200 ease-in-out'>
|
||||
<div className='group grow rounded-t-xl' onClick={() => setShowModal(true)}>
|
||||
<div className='flex shrink-0 items-center p-4 pb-3'>
|
||||
<div className='flex h-10 w-10 items-center justify-center rounded-lg border border-dashed border-divider-deep group-hover:border-solid group-hover:border-state-accent-hover-alt group-hover:bg-state-accent-hover'>
|
||||
<RiAddCircleFill className='h-4 w-4 text-text-quaternary group-hover:text-text-accent'/>
|
||||
</div>
|
||||
<div className='system-md-semibold ml-3 text-text-secondary group-hover:text-text-accent'>{t('tools.mcp.create.cardTitle')}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='rounded-b-xl border-t-[0.5px] border-divider-subtle px-4 py-3 text-text-tertiary hover:text-text-accent'>
|
||||
<a href={linkUrl} target='_blank' rel='noopener noreferrer' className='flex items-center space-x-1'>
|
||||
<RiBookOpenLine className='h-3 w-3 shrink-0' />
|
||||
<div className='system-xs-regular grow truncate' title={t('tools.mcp.create.cardLink') || ''}>{t('tools.mcp.create.cardLink')}</div>
|
||||
<RiArrowRightUpLine className='h-3 w-3 shrink-0' />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{showModal && (
|
||||
<MCPModal
|
||||
show={showModal}
|
||||
onConfirm={create}
|
||||
onHide={() => setShowModal(false)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default NewMCPCard
|
||||
308
web/app/components/tools/mcp/detail/content.tsx
Normal file
308
web/app/components/tools/mcp/detail/content.tsx
Normal file
@@ -0,0 +1,308 @@
|
||||
'use client'
|
||||
import React, { useCallback, useEffect } from 'react'
|
||||
import type { FC } from 'react'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import copy from 'copy-to-clipboard'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import {
|
||||
RiCloseLine,
|
||||
RiLoader2Line,
|
||||
RiLoopLeftLine,
|
||||
} from '@remixicon/react'
|
||||
import type { ToolWithProvider } from '../../../workflow/types'
|
||||
import Icon from '@/app/components/plugins/card/base/card-icon'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Confirm from '@/app/components/base/confirm'
|
||||
import Indicator from '@/app/components/header/indicator'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import MCPModal from '../modal'
|
||||
import OperationDropdown from './operation-dropdown'
|
||||
import ListLoading from './list-loading'
|
||||
import ToolItem from './tool-item'
|
||||
import {
|
||||
useAuthorizeMCP,
|
||||
useDeleteMCP,
|
||||
useInvalidateMCPTools,
|
||||
useMCPTools,
|
||||
useUpdateMCP,
|
||||
useUpdateMCPTools,
|
||||
} from '@/service/use-tools'
|
||||
import { openOAuthPopup } from '@/hooks/use-oauth'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type Props = {
|
||||
detail: ToolWithProvider
|
||||
onUpdate: (isDelete?: boolean) => void
|
||||
onHide: () => void
|
||||
isTriggerAuthorize: boolean
|
||||
onFirstCreate: () => void
|
||||
}
|
||||
|
||||
const MCPDetailContent: FC<Props> = ({
|
||||
detail,
|
||||
onUpdate,
|
||||
onHide,
|
||||
isTriggerAuthorize,
|
||||
onFirstCreate,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { isCurrentWorkspaceManager } = useAppContext()
|
||||
|
||||
const { data, isFetching: isGettingTools } = useMCPTools(detail.is_team_authorization ? detail.id : '')
|
||||
const invalidateMCPTools = useInvalidateMCPTools()
|
||||
const { mutateAsync: updateTools, isPending: isUpdating } = useUpdateMCPTools()
|
||||
const { mutateAsync: authorizeMcp, isPending: isAuthorizing } = useAuthorizeMCP()
|
||||
const toolList = data?.tools || []
|
||||
|
||||
const [isShowUpdateConfirm, {
|
||||
setTrue: showUpdateConfirm,
|
||||
setFalse: hideUpdateConfirm,
|
||||
}] = useBoolean(false)
|
||||
|
||||
const handleUpdateTools = useCallback(async () => {
|
||||
hideUpdateConfirm()
|
||||
if (!detail)
|
||||
return
|
||||
await updateTools(detail.id)
|
||||
invalidateMCPTools(detail.id)
|
||||
onUpdate()
|
||||
}, [detail, hideUpdateConfirm, invalidateMCPTools, onUpdate, updateTools])
|
||||
|
||||
const { mutateAsync: updateMCP } = useUpdateMCP({})
|
||||
const { mutateAsync: deleteMCP } = useDeleteMCP({})
|
||||
|
||||
const [isShowUpdateModal, {
|
||||
setTrue: showUpdateModal,
|
||||
setFalse: hideUpdateModal,
|
||||
}] = useBoolean(false)
|
||||
|
||||
const [isShowDeleteConfirm, {
|
||||
setTrue: showDeleteConfirm,
|
||||
setFalse: hideDeleteConfirm,
|
||||
}] = useBoolean(false)
|
||||
|
||||
const [deleting, {
|
||||
setTrue: showDeleting,
|
||||
setFalse: hideDeleting,
|
||||
}] = useBoolean(false)
|
||||
|
||||
const handleOAuthCallback = useCallback(() => {
|
||||
if (!isCurrentWorkspaceManager)
|
||||
return
|
||||
if (!detail.id)
|
||||
return
|
||||
handleUpdateTools()
|
||||
}, [detail.id, handleUpdateTools, isCurrentWorkspaceManager])
|
||||
|
||||
const handleAuthorize = useCallback(async () => {
|
||||
onFirstCreate()
|
||||
if (!isCurrentWorkspaceManager)
|
||||
return
|
||||
if (!detail)
|
||||
return
|
||||
const res = await authorizeMcp({
|
||||
provider_id: detail.id,
|
||||
})
|
||||
if (res.result === 'success')
|
||||
handleUpdateTools()
|
||||
|
||||
else if (res.authorization_url)
|
||||
openOAuthPopup(res.authorization_url, handleOAuthCallback)
|
||||
}, [onFirstCreate, isCurrentWorkspaceManager, detail, authorizeMcp, handleUpdateTools, handleOAuthCallback])
|
||||
|
||||
const handleUpdate = useCallback(async (data: any) => {
|
||||
if (!detail)
|
||||
return
|
||||
const res = await updateMCP({
|
||||
...data,
|
||||
provider_id: detail.id,
|
||||
})
|
||||
if ((res as any)?.result === 'success') {
|
||||
hideUpdateModal()
|
||||
onUpdate()
|
||||
handleAuthorize()
|
||||
}
|
||||
}, [detail, updateMCP, hideUpdateModal, onUpdate, handleAuthorize])
|
||||
|
||||
const handleDelete = useCallback(async () => {
|
||||
if (!detail)
|
||||
return
|
||||
showDeleting()
|
||||
const res = await deleteMCP(detail.id)
|
||||
hideDeleting()
|
||||
if ((res as any)?.result === 'success') {
|
||||
hideDeleteConfirm()
|
||||
onUpdate(true)
|
||||
}
|
||||
}, [detail, showDeleting, deleteMCP, hideDeleting, hideDeleteConfirm, onUpdate])
|
||||
|
||||
useEffect(() => {
|
||||
if (isTriggerAuthorize)
|
||||
handleAuthorize()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
if (!detail)
|
||||
return null
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={cn('shrink-0 border-b border-divider-subtle bg-components-panel-bg p-4 pb-3')}>
|
||||
<div className='flex'>
|
||||
<div className='shrink-0 overflow-hidden rounded-xl border border-components-panel-border-subtle'>
|
||||
<Icon src={detail.icon} />
|
||||
</div>
|
||||
<div className='ml-3 w-0 grow'>
|
||||
<div className='flex h-5 items-center'>
|
||||
<div className='system-md-semibold truncate text-text-primary' title={detail.name}>{detail.name}</div>
|
||||
</div>
|
||||
<div className='mt-0.5 flex items-center gap-1'>
|
||||
<Tooltip popupContent={t('tools.mcp.identifier')}>
|
||||
<div className='system-xs-regular shrink-0 cursor-pointer text-text-secondary' onClick={() => copy(detail.server_identifier || '')}>{detail.server_identifier}</div>
|
||||
</Tooltip>
|
||||
<div className='system-xs-regular shrink-0 text-text-quaternary'>·</div>
|
||||
<Tooltip popupContent={t('tools.mcp.modal.serverUrl')}>
|
||||
<div className='system-xs-regular truncate text-text-secondary'>{detail.server_url}</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex gap-1'>
|
||||
<OperationDropdown
|
||||
onEdit={showUpdateModal}
|
||||
onRemove={showDeleteConfirm}
|
||||
/>
|
||||
<ActionButton onClick={onHide}>
|
||||
<RiCloseLine className='h-4 w-4' />
|
||||
</ActionButton>
|
||||
</div>
|
||||
</div>
|
||||
<div className='mt-5'>
|
||||
{!isAuthorizing && detail.is_team_authorization && (
|
||||
<Button
|
||||
variant='secondary'
|
||||
className='w-full'
|
||||
onClick={handleAuthorize}
|
||||
disabled={!isCurrentWorkspaceManager}
|
||||
>
|
||||
<Indicator className='mr-2' color={'green'} />
|
||||
{t('tools.auth.authorized')}
|
||||
</Button>
|
||||
)}
|
||||
{!detail.is_team_authorization && !isAuthorizing && (
|
||||
<Button
|
||||
variant='primary'
|
||||
className='w-full'
|
||||
onClick={handleAuthorize}
|
||||
disabled={!isCurrentWorkspaceManager}
|
||||
>
|
||||
{t('tools.mcp.authorize')}
|
||||
</Button>
|
||||
)}
|
||||
{isAuthorizing && (
|
||||
<Button
|
||||
variant='primary'
|
||||
className='w-full'
|
||||
disabled
|
||||
>
|
||||
<RiLoader2Line className={cn('mr-1 h-4 w-4 animate-spin')} />
|
||||
{t('tools.mcp.authorizing')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex grow flex-col'>
|
||||
{((detail.is_team_authorization && isGettingTools) || isUpdating) && (
|
||||
<>
|
||||
<div className='flex shrink-0 justify-between gap-2 px-4 pb-1 pt-2'>
|
||||
<div className='flex h-6 items-center'>
|
||||
{!isUpdating && <div className='system-sm-semibold-uppercase text-text-secondary'>{t('tools.mcp.gettingTools')}</div>}
|
||||
{isUpdating && <div className='system-sm-semibold-uppercase text-text-secondary'>{t('tools.mcp.updateTools')}</div>}
|
||||
</div>
|
||||
<div></div>
|
||||
</div>
|
||||
<div className='flex h-full w-full grow flex-col overflow-hidden px-4 pb-4'>
|
||||
<ListLoading />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{!isUpdating && detail.is_team_authorization && !isGettingTools && !toolList.length && (
|
||||
<div className='flex h-full w-full flex-col items-center justify-center'>
|
||||
<div className='system-sm-regular mb-3 text-text-tertiary'>{t('tools.mcp.toolsEmpty')}</div>
|
||||
<Button
|
||||
variant='primary'
|
||||
onClick={handleUpdateTools}
|
||||
>{t('tools.mcp.getTools')}</Button>
|
||||
</div>
|
||||
)}
|
||||
{!isUpdating && !isGettingTools && toolList.length > 0 && (
|
||||
<>
|
||||
<div className='flex shrink-0 justify-between gap-2 px-4 pb-1 pt-2'>
|
||||
<div className='flex h-6 items-center'>
|
||||
{toolList.length > 1 && <div className='system-sm-semibold-uppercase text-text-secondary'>{t('tools.mcp.toolsNum', { count: toolList.length })}</div>}
|
||||
{toolList.length === 1 && <div className='system-sm-semibold-uppercase text-text-secondary'>{t('tools.mcp.onlyTool')}</div>}
|
||||
</div>
|
||||
<div>
|
||||
<Button size='small' onClick={showUpdateConfirm}>
|
||||
<RiLoopLeftLine className='mr-1 h-3.5 w-3.5' />
|
||||
{t('tools.mcp.update')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex h-0 w-full grow flex-col gap-2 overflow-y-auto px-4 pb-4'>
|
||||
{toolList.map(tool => (
|
||||
<ToolItem
|
||||
key={`${detail.id}${tool.name}`}
|
||||
tool={tool}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{!isUpdating && !detail.is_team_authorization && (
|
||||
<div className='flex h-full w-full flex-col items-center justify-center'>
|
||||
{!isAuthorizing && <div className='system-md-medium mb-1 text-text-secondary'>{t('tools.mcp.authorizingRequired')}</div>}
|
||||
{isAuthorizing && <div className='system-md-medium mb-1 text-text-secondary'>{t('tools.mcp.authorizing')}</div>}
|
||||
<div className='system-sm-regular text-text-tertiary'>{t('tools.mcp.authorizeTip')}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{isShowUpdateModal && (
|
||||
<MCPModal
|
||||
data={detail}
|
||||
show={isShowUpdateModal}
|
||||
onConfirm={handleUpdate}
|
||||
onHide={hideUpdateModal}
|
||||
/>
|
||||
)}
|
||||
{isShowDeleteConfirm && (
|
||||
<Confirm
|
||||
isShow
|
||||
title={t('tools.mcp.delete')}
|
||||
content={
|
||||
<div>
|
||||
{t('tools.mcp.deleteConfirmTitle', { mcp: detail.name })}
|
||||
</div>
|
||||
}
|
||||
onCancel={hideDeleteConfirm}
|
||||
onConfirm={handleDelete}
|
||||
isLoading={deleting}
|
||||
isDisabled={deleting}
|
||||
/>
|
||||
)}
|
||||
{isShowUpdateConfirm && (
|
||||
<Confirm
|
||||
isShow
|
||||
title={t('tools.mcp.toolUpdateConfirmTitle')}
|
||||
content={t('tools.mcp.toolUpdateConfirmContent')}
|
||||
onCancel={hideUpdateConfirm}
|
||||
onConfirm={handleUpdateTools}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default MCPDetailContent
|
||||
37
web/app/components/tools/mcp/detail/list-loading.tsx
Normal file
37
web/app/components/tools/mcp/detail/list-loading.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
'use client'
|
||||
import React from 'react'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
const ListLoading = () => {
|
||||
return (
|
||||
<div className={cn('space-y-2')}>
|
||||
<div className='space-y-3 rounded-xl bg-components-panel-on-panel-item-bg-hover p-4'>
|
||||
<div className='h-2 w-[180px] rounded-sm bg-text-quaternary opacity-20'></div>
|
||||
<div className='h-2 rounded-sm bg-text-quaternary opacity-10'></div>
|
||||
<div className='mr-10 h-2 rounded-sm bg-text-quaternary opacity-10'></div>
|
||||
</div>
|
||||
<div className='space-y-3 rounded-xl bg-components-panel-on-panel-item-bg-hover p-4'>
|
||||
<div className='h-2 w-[148px] rounded-sm bg-text-quaternary opacity-20'></div>
|
||||
<div className='h-2 rounded-sm bg-text-quaternary opacity-10'></div>
|
||||
<div className='mr-10 h-2 rounded-sm bg-text-quaternary opacity-10'></div>
|
||||
</div>
|
||||
<div className='space-y-3 rounded-xl bg-components-panel-on-panel-item-bg-hover p-4'>
|
||||
<div className='h-2 w-[196px] rounded-sm bg-text-quaternary opacity-20'></div>
|
||||
<div className='h-2 rounded-sm bg-text-quaternary opacity-10'></div>
|
||||
<div className='mr-10 h-2 rounded-sm bg-text-quaternary opacity-10'></div>
|
||||
</div>
|
||||
<div className='space-y-3 rounded-xl bg-components-panel-on-panel-item-bg-hover p-4'>
|
||||
<div className='h-2 w-[148px] rounded-sm bg-text-quaternary opacity-20'></div>
|
||||
<div className='h-2 rounded-sm bg-text-quaternary opacity-10'></div>
|
||||
<div className='mr-10 h-2 rounded-sm bg-text-quaternary opacity-10'></div>
|
||||
</div>
|
||||
<div className='space-y-3 rounded-xl bg-components-panel-on-panel-item-bg-hover p-4'>
|
||||
<div className='h-2 w-[180px] rounded-sm bg-text-quaternary opacity-20'></div>
|
||||
<div className='h-2 rounded-sm bg-text-quaternary opacity-10'></div>
|
||||
<div className='mr-10 h-2 rounded-sm bg-text-quaternary opacity-10'></div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ListLoading
|
||||
88
web/app/components/tools/mcp/detail/operation-dropdown.tsx
Normal file
88
web/app/components/tools/mcp/detail/operation-dropdown.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
RiDeleteBinLine,
|
||||
RiEditLine,
|
||||
RiMoreFill,
|
||||
} from '@remixicon/react'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type Props = {
|
||||
inCard?: boolean
|
||||
onOpenChange?: (open: boolean) => void
|
||||
onEdit: () => void
|
||||
onRemove: () => void
|
||||
}
|
||||
|
||||
const OperationDropdown: FC<Props> = ({
|
||||
inCard,
|
||||
onOpenChange,
|
||||
onEdit,
|
||||
onRemove,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [open, doSetOpen] = useState(false)
|
||||
const openRef = useRef(open)
|
||||
const setOpen = useCallback((v: boolean) => {
|
||||
doSetOpen(v)
|
||||
openRef.current = v
|
||||
onOpenChange?.(v)
|
||||
}, [doSetOpen])
|
||||
|
||||
const handleTrigger = useCallback(() => {
|
||||
setOpen(!openRef.current)
|
||||
}, [setOpen])
|
||||
|
||||
return (
|
||||
<PortalToFollowElem
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
placement='bottom-end'
|
||||
offset={{
|
||||
mainAxis: !inCard ? -12 : 0,
|
||||
crossAxis: !inCard ? 36 : 0,
|
||||
}}
|
||||
>
|
||||
<PortalToFollowElemTrigger onClick={handleTrigger}>
|
||||
<div>
|
||||
<ActionButton size={inCard ? 'l' : 'm'} className={cn(open && 'bg-state-base-hover')}>
|
||||
<RiMoreFill className={cn('h-4 w-4', inCard && 'h-5 w-5')} />
|
||||
</ActionButton>
|
||||
</div>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className='z-50'>
|
||||
<div className='w-[160px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-1 shadow-lg backdrop-blur-sm'>
|
||||
<div
|
||||
className='flex cursor-pointer items-center rounded-lg px-3 py-1.5 hover:bg-state-base-hover'
|
||||
onClick={() => {
|
||||
onEdit()
|
||||
handleTrigger()
|
||||
}}
|
||||
>
|
||||
<RiEditLine className='h-4 w-4 text-text-tertiary' />
|
||||
<div className='system-md-regular ml-2 text-text-secondary'>{t('tools.mcp.operation.edit')}</div>
|
||||
</div>
|
||||
<div
|
||||
className='group flex cursor-pointer items-center rounded-lg px-3 py-1.5 hover:bg-state-destructive-hover'
|
||||
onClick={() => {
|
||||
onRemove()
|
||||
handleTrigger()
|
||||
}}
|
||||
>
|
||||
<RiDeleteBinLine className='h-4 w-4 text-text-tertiary group-hover:text-text-destructive-secondary' />
|
||||
<div className='system-md-regular ml-2 text-text-secondary group-hover:text-text-destructive'>{t('tools.mcp.operation.remove')}</div>
|
||||
</div>
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
)
|
||||
}
|
||||
export default React.memo(OperationDropdown)
|
||||
56
web/app/components/tools/mcp/detail/provider-detail.tsx
Normal file
56
web/app/components/tools/mcp/detail/provider-detail.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
'use client'
|
||||
import React from 'react'
|
||||
import type { FC } from 'react'
|
||||
import Drawer from '@/app/components/base/drawer'
|
||||
import MCPDetailContent from './content'
|
||||
import type { ToolWithProvider } from '../../../workflow/types'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type Props = {
|
||||
detail?: ToolWithProvider
|
||||
onUpdate: () => void
|
||||
onHide: () => void
|
||||
isTriggerAuthorize: boolean
|
||||
onFirstCreate: () => void
|
||||
}
|
||||
|
||||
const MCPDetailPanel: FC<Props> = ({
|
||||
detail,
|
||||
onUpdate,
|
||||
onHide,
|
||||
isTriggerAuthorize,
|
||||
onFirstCreate,
|
||||
}) => {
|
||||
const handleUpdate = (isDelete = false) => {
|
||||
if (isDelete)
|
||||
onHide()
|
||||
onUpdate()
|
||||
}
|
||||
|
||||
if (!detail)
|
||||
return null
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
isOpen={!!detail}
|
||||
clickOutsideNotOpen={false}
|
||||
onClose={onHide}
|
||||
footer={null}
|
||||
mask={false}
|
||||
positionCenter={false}
|
||||
panelClassName={cn('mb-2 mr-2 mt-[64px] !w-[420px] !max-w-[420px] justify-start rounded-2xl border-[0.5px] border-components-panel-border !bg-components-panel-bg !p-0 shadow-xl')}
|
||||
>
|
||||
{detail && (
|
||||
<MCPDetailContent
|
||||
detail={detail}
|
||||
onHide={onHide}
|
||||
onUpdate={handleUpdate}
|
||||
isTriggerAuthorize={isTriggerAuthorize}
|
||||
onFirstCreate={onFirstCreate}
|
||||
/>
|
||||
)}
|
||||
</Drawer>
|
||||
)
|
||||
}
|
||||
|
||||
export default MCPDetailPanel
|
||||
41
web/app/components/tools/mcp/detail/tool-item.tsx
Normal file
41
web/app/components/tools/mcp/detail/tool-item.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
'use client'
|
||||
import React from 'react'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import type { Tool } from '@/app/components/tools/types'
|
||||
import I18n from '@/context/i18n'
|
||||
import { getLanguage } from '@/i18n/language'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type Props = {
|
||||
tool: Tool
|
||||
}
|
||||
|
||||
const MCPToolItem = ({
|
||||
tool,
|
||||
}: Props) => {
|
||||
const { locale } = useContext(I18n)
|
||||
const language = getLanguage(locale)
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
key={tool.name}
|
||||
position='left'
|
||||
popupClassName='!p-0 !px-4 !py-3.5 !w-[360px] !border-[0.5px] !border-components-panel-border !rounded-xl !shadow-lg'
|
||||
popupContent={(
|
||||
<div>
|
||||
<div className='title-xs-semi-bold mb-1 text-text-primary'>{tool.label[language]}</div>
|
||||
<div className='body-xs-regular text-text-secondary'>{tool.description[language]}</div>
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn('bg-components-panel-item-bg cursor-pointer rounded-xl border-[0.5px] border-components-panel-border-subtle px-4 py-3 shadow-xs hover:bg-components-panel-on-panel-item-bg-hover')}
|
||||
>
|
||||
<div className='system-md-semibold pb-0.5 text-text-secondary'>{tool.label[language]}</div>
|
||||
<div className='system-xs-regular line-clamp-2 text-text-tertiary' title={tool.description[language]}>{tool.description[language]}</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
export default MCPToolItem
|
||||
12
web/app/components/tools/mcp/hooks.ts
Normal file
12
web/app/components/tools/mcp/hooks.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import dayjs from 'dayjs'
|
||||
import { useCallback } from 'react'
|
||||
import { useI18N } from '@/context/i18n'
|
||||
|
||||
export const useFormatTimeFromNow = () => {
|
||||
const { locale } = useI18N()
|
||||
const formatTimeFromNow = useCallback((time: number) => {
|
||||
return dayjs(time).locale(locale === 'zh-Hans' ? 'zh-cn' : locale).fromNow()
|
||||
}, [locale])
|
||||
|
||||
return { formatTimeFromNow }
|
||||
}
|
||||
98
web/app/components/tools/mcp/index.tsx
Normal file
98
web/app/components/tools/mcp/index.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
'use client'
|
||||
import { useMemo, useState } from 'react'
|
||||
import NewMCPCard from './create-card'
|
||||
import MCPCard from './provider-card'
|
||||
import MCPDetailPanel from './detail/provider-detail'
|
||||
import {
|
||||
useAllToolProviders,
|
||||
} from '@/service/use-tools'
|
||||
import type { ToolWithProvider } from '@/app/components/workflow/types'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type Props = {
|
||||
searchText: string
|
||||
}
|
||||
|
||||
function renderDefaultCard() {
|
||||
const defaultCards = Array.from({ length: 36 }, (_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={cn(
|
||||
'inline-flex h-[111px] rounded-xl bg-background-default-lighter opacity-10',
|
||||
index < 4 && 'opacity-60',
|
||||
index >= 4 && index < 8 && 'opacity-50',
|
||||
index >= 8 && index < 12 && 'opacity-40',
|
||||
index >= 12 && index < 16 && 'opacity-30',
|
||||
index >= 16 && index < 20 && 'opacity-25',
|
||||
index >= 20 && index < 24 && 'opacity-20',
|
||||
)}
|
||||
></div>
|
||||
))
|
||||
return defaultCards
|
||||
}
|
||||
|
||||
const MCPList = ({
|
||||
searchText,
|
||||
}: Props) => {
|
||||
const { data: list = [] as ToolWithProvider[], refetch } = useAllToolProviders()
|
||||
const [isTriggerAuthorize, setIsTriggerAuthorize] = useState<boolean>(false)
|
||||
|
||||
const filteredList = useMemo(() => {
|
||||
return list.filter((collection) => {
|
||||
if (searchText)
|
||||
return Object.values(collection.name).some(value => (value as string).toLowerCase().includes(searchText.toLowerCase()))
|
||||
return collection.type === 'mcp'
|
||||
}) as ToolWithProvider[]
|
||||
}, [list, searchText])
|
||||
|
||||
const [currentProviderID, setCurrentProviderID] = useState<string>()
|
||||
|
||||
const currentProvider = useMemo(() => {
|
||||
return list.find(provider => provider.id === currentProviderID)
|
||||
}, [list, currentProviderID])
|
||||
|
||||
const handleCreate = async (provider: ToolWithProvider) => {
|
||||
await refetch() // update list
|
||||
setCurrentProviderID(provider.id)
|
||||
setIsTriggerAuthorize(true)
|
||||
}
|
||||
|
||||
const handleUpdate = async (providerID: string) => {
|
||||
await refetch() // update list
|
||||
setCurrentProviderID(providerID)
|
||||
setIsTriggerAuthorize(true)
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={cn(
|
||||
'relative grid shrink-0 grid-cols-1 content-start gap-4 px-12 pb-4 pt-2 sm:grid-cols-1 md:grid-cols-2 xl:grid-cols-4 2xl:grid-cols-5 2k:grid-cols-6',
|
||||
!list.length && 'h-[calc(100vh_-_136px)] overflow-hidden',
|
||||
)}
|
||||
>
|
||||
<NewMCPCard handleCreate={handleCreate} />
|
||||
{filteredList.map(provider => (
|
||||
<MCPCard
|
||||
key={provider.id}
|
||||
data={provider}
|
||||
currentProvider={currentProvider as ToolWithProvider}
|
||||
handleSelect={setCurrentProviderID}
|
||||
onUpdate={handleUpdate}
|
||||
onDeleted={refetch}
|
||||
/>
|
||||
))}
|
||||
{!list.length && renderDefaultCard()}
|
||||
</div>
|
||||
{currentProvider && (
|
||||
<MCPDetailPanel
|
||||
detail={currentProvider as ToolWithProvider}
|
||||
onHide={() => setCurrentProviderID(undefined)}
|
||||
onUpdate={refetch}
|
||||
isTriggerAuthorize={isTriggerAuthorize}
|
||||
onFirstCreate={() => setIsTriggerAuthorize(false)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default MCPList
|
||||
134
web/app/components/tools/mcp/mcp-server-modal.tsx
Normal file
134
web/app/components/tools/mcp/mcp-server-modal.tsx
Normal file
@@ -0,0 +1,134 @@
|
||||
'use client'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RiCloseLine } from '@remixicon/react'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Textarea from '@/app/components/base/textarea'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import MCPServerParamItem from '@/app/components/tools/mcp/mcp-server-param-item'
|
||||
import type {
|
||||
MCPServerDetail,
|
||||
} from '@/app/components/tools/types'
|
||||
import {
|
||||
useCreateMCPServer,
|
||||
useInvalidateMCPServerDetail,
|
||||
useUpdateMCPServer,
|
||||
} from '@/service/use-tools'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
export type ModalProps = {
|
||||
appID: string
|
||||
latestParams?: any[]
|
||||
data?: MCPServerDetail
|
||||
show: boolean
|
||||
onHide: () => void
|
||||
}
|
||||
|
||||
const MCPServerModal = ({
|
||||
appID,
|
||||
latestParams = [],
|
||||
data,
|
||||
show,
|
||||
onHide,
|
||||
}: ModalProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { mutateAsync: createMCPServer, isPending: creating } = useCreateMCPServer()
|
||||
const { mutateAsync: updateMCPServer, isPending: updating } = useUpdateMCPServer()
|
||||
const invalidateMCPServerDetail = useInvalidateMCPServerDetail()
|
||||
|
||||
const [description, setDescription] = React.useState(data?.description || '')
|
||||
const [params, setParams] = React.useState(data?.parameters || {})
|
||||
|
||||
const handleParamChange = (variable: string, value: string) => {
|
||||
setParams(prev => ({
|
||||
...prev,
|
||||
[variable]: value,
|
||||
}))
|
||||
}
|
||||
|
||||
const getParamValue = () => {
|
||||
const res = {} as any
|
||||
latestParams.map((param) => {
|
||||
res[param.variable] = params[param.variable]
|
||||
return param
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
const submit = async () => {
|
||||
if (!data) {
|
||||
await createMCPServer({
|
||||
appID,
|
||||
description,
|
||||
parameters: getParamValue(),
|
||||
})
|
||||
invalidateMCPServerDetail(appID)
|
||||
onHide()
|
||||
}
|
||||
else {
|
||||
await updateMCPServer({
|
||||
appID,
|
||||
id: data.id,
|
||||
description,
|
||||
parameters: getParamValue(),
|
||||
})
|
||||
invalidateMCPServerDetail(appID)
|
||||
onHide()
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isShow={show}
|
||||
onClose={onHide}
|
||||
className={cn('relative !max-w-[520px] !p-0')}
|
||||
>
|
||||
<div className='absolute right-5 top-5 z-10 cursor-pointer p-1.5' onClick={onHide}>
|
||||
<RiCloseLine className='h-5 w-5 text-text-tertiary' />
|
||||
</div>
|
||||
<div className='title-2xl-semi-bold relative p-6 pb-3 text-xl text-text-primary'>
|
||||
{!data ? t('tools.mcp.server.modal.addTitle') : t('tools.mcp.server.modal.editTitle')}
|
||||
</div>
|
||||
<div className='space-y-5 px-6 py-3'>
|
||||
<div className='space-y-0.5'>
|
||||
<div className='flex h-6 items-center gap-1'>
|
||||
<div className='system-sm-medium text-text-secondary'>{t('tools.mcp.server.modal.description')}</div>
|
||||
<div className='system-xs-regular text-text-destructive-secondary'>*</div>
|
||||
</div>
|
||||
<Textarea
|
||||
className='h-[96px] resize-none'
|
||||
value={description}
|
||||
placeholder={t('tools.mcp.server.modal.descriptionPlaceholder')}
|
||||
onChange={e => setDescription(e.target.value)}
|
||||
></Textarea>
|
||||
</div>
|
||||
{latestParams.length > 0 && (
|
||||
<div>
|
||||
<div className='mb-1 flex items-center gap-2'>
|
||||
<div className='system-xs-medium-uppercase shrink-0 text-text-primary'>{t('tools.mcp.server.modal.parameters')}</div>
|
||||
<Divider type='horizontal' className='!m-0 !h-px grow bg-divider-subtle' />
|
||||
</div>
|
||||
<div className='body-xs-regular mb-2 text-text-tertiary'>{t('tools.mcp.server.modal.parametersTip')}</div>
|
||||
<div className='space-y-3'>
|
||||
{latestParams.map(paramItem => (
|
||||
<MCPServerParamItem
|
||||
key={paramItem.variable}
|
||||
data={paramItem}
|
||||
value={params[paramItem.variable] || ''}
|
||||
onChange={value => handleParamChange(paramItem.variable, value)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className='flex flex-row-reverse p-6 pt-5'>
|
||||
<Button disabled={!description || creating || updating} className='ml-2' variant='primary' onClick={submit}>{data ? t('tools.mcp.modal.save') : t('tools.mcp.server.modal.confirm')}</Button>
|
||||
<Button onClick={onHide}>{t('tools.mcp.modal.cancel')}</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default MCPServerModal
|
||||
37
web/app/components/tools/mcp/mcp-server-param-item.tsx
Normal file
37
web/app/components/tools/mcp/mcp-server-param-item.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
'use client'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Textarea from '@/app/components/base/textarea'
|
||||
|
||||
type Props = {
|
||||
data?: any
|
||||
value: string
|
||||
onChange: (value: string) => void
|
||||
}
|
||||
|
||||
const MCPServerParamItem = ({
|
||||
data,
|
||||
value,
|
||||
onChange,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className='space-y-0.5'>
|
||||
<div className='flex h-6 items-center gap-2'>
|
||||
<div className='system-xs-medium text-text-secondary'>{data.label}</div>
|
||||
<div className='system-xs-medium text-text-quaternary'>·</div>
|
||||
<div className='system-xs-medium text-text-secondary'>{data.variable}</div>
|
||||
<div className='system-xs-medium text-text-tertiary'>{data.type}</div>
|
||||
</div>
|
||||
<Textarea
|
||||
className='h-8 resize-none'
|
||||
value={value}
|
||||
placeholder={t('tools.mcp.server.modal.parametersPlaceholder')}
|
||||
onChange={e => onChange(e.target.value)}
|
||||
></Textarea>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default MCPServerParamItem
|
||||
244
web/app/components/tools/mcp/mcp-service-card.tsx
Normal file
244
web/app/components/tools/mcp/mcp-service-card.tsx
Normal file
@@ -0,0 +1,244 @@
|
||||
'use client'
|
||||
import React, { useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
RiLoopLeftLine,
|
||||
} from '@remixicon/react'
|
||||
import {
|
||||
Mcp,
|
||||
} from '@/app/components/base/icons/src/vender/other'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import Switch from '@/app/components/base/switch'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import CopyFeedback from '@/app/components/base/copy-feedback'
|
||||
import Confirm from '@/app/components/base/confirm'
|
||||
import type { AppDetailResponse } from '@/models/app'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import type { AppSSO } from '@/types/app'
|
||||
import Indicator from '@/app/components/header/indicator'
|
||||
import MCPServerModal from '@/app/components/tools/mcp/mcp-server-modal'
|
||||
import { useAppWorkflow } from '@/service/use-workflow'
|
||||
import {
|
||||
useInvalidateMCPServerDetail,
|
||||
useMCPServerDetail,
|
||||
useRefreshMCPServerCode,
|
||||
useUpdateMCPServer,
|
||||
} from '@/service/use-tools'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import cn from '@/utils/classnames'
|
||||
import { fetchAppDetail } from '@/service/apps'
|
||||
|
||||
export type IAppCardProps = {
|
||||
appInfo: AppDetailResponse & Partial<AppSSO>
|
||||
}
|
||||
|
||||
function MCPServiceCard({
|
||||
appInfo,
|
||||
}: IAppCardProps) {
|
||||
const { t } = useTranslation()
|
||||
const appId = appInfo.id
|
||||
const { mutateAsync: updateMCPServer } = useUpdateMCPServer()
|
||||
const { mutateAsync: refreshMCPServerCode, isPending: genLoading } = useRefreshMCPServerCode()
|
||||
const invalidateMCPServerDetail = useInvalidateMCPServerDetail()
|
||||
const { isCurrentWorkspaceManager, isCurrentWorkspaceEditor } = useAppContext()
|
||||
const [showConfirmDelete, setShowConfirmDelete] = useState(false)
|
||||
const [showMCPServerModal, setShowMCPServerModal] = useState(false)
|
||||
|
||||
const isAdvancedApp = appInfo?.mode === 'advanced-chat' || appInfo?.mode === 'workflow'
|
||||
const isBasicApp = !isAdvancedApp
|
||||
const { data: currentWorkflow } = useAppWorkflow(isAdvancedApp ? appId : '')
|
||||
const [basicAppConfig, setBasicAppConfig] = useState<any>({})
|
||||
const basicAppInputForm = useMemo(() => {
|
||||
if(!isBasicApp || !basicAppConfig?.user_input_form)
|
||||
return []
|
||||
return basicAppConfig.user_input_form.map((item: any) => {
|
||||
const type = Object.keys(item)[0]
|
||||
return {
|
||||
...item[type],
|
||||
type: type || 'text-input',
|
||||
}
|
||||
})
|
||||
}, [basicAppConfig.user_input_form, isBasicApp])
|
||||
useEffect(() => {
|
||||
if(isBasicApp && appId) {
|
||||
(async () => {
|
||||
const res = await fetchAppDetail({ url: '/apps', id: appId })
|
||||
setBasicAppConfig(res?.model_config || {})
|
||||
})()
|
||||
}
|
||||
}, [appId, isBasicApp])
|
||||
const { data: detail } = useMCPServerDetail(appId)
|
||||
const { id, status, server_code } = detail ?? {}
|
||||
|
||||
const appUnpublished = isAdvancedApp ? !currentWorkflow?.graph : !basicAppConfig.updated_at
|
||||
const serverPublished = !!id
|
||||
const serverActivated = status === 'active'
|
||||
const serverURL = serverPublished ? `${appInfo.api_base_url.replace('/v1', '')}/mcp/server/${server_code}/mcp` : '***********'
|
||||
const toggleDisabled = !isCurrentWorkspaceEditor || appUnpublished
|
||||
|
||||
const [activated, setActivated] = useState(serverActivated)
|
||||
|
||||
const latestParams = useMemo(() => {
|
||||
if(isAdvancedApp) {
|
||||
if (!currentWorkflow?.graph)
|
||||
return []
|
||||
const startNode = currentWorkflow?.graph.nodes.find(node => node.data.type === BlockEnum.Start) as any
|
||||
return startNode?.data.variables as any[] || []
|
||||
}
|
||||
return basicAppInputForm
|
||||
}, [currentWorkflow, basicAppInputForm, isAdvancedApp])
|
||||
|
||||
const onGenCode = async () => {
|
||||
await refreshMCPServerCode(detail?.id || '')
|
||||
invalidateMCPServerDetail(appId)
|
||||
}
|
||||
|
||||
const onChangeStatus = async (state: boolean) => {
|
||||
setActivated(state)
|
||||
if (state) {
|
||||
if (!serverPublished) {
|
||||
setShowMCPServerModal(true)
|
||||
return
|
||||
}
|
||||
|
||||
await updateMCPServer({
|
||||
appID: appId,
|
||||
id: id || '',
|
||||
description: detail?.description || '',
|
||||
parameters: detail?.parameters || {},
|
||||
status: 'active',
|
||||
})
|
||||
invalidateMCPServerDetail(appId)
|
||||
}
|
||||
else {
|
||||
await updateMCPServer({
|
||||
appID: appId,
|
||||
id: id || '',
|
||||
description: detail?.description || '',
|
||||
parameters: detail?.parameters || {},
|
||||
status: 'inactive',
|
||||
})
|
||||
invalidateMCPServerDetail(appId)
|
||||
}
|
||||
}
|
||||
|
||||
const handleServerModalHide = () => {
|
||||
setShowMCPServerModal(false)
|
||||
if (!serverActivated)
|
||||
setActivated(false)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setActivated(serverActivated)
|
||||
}, [serverActivated])
|
||||
|
||||
if (!currentWorkflow && isAdvancedApp)
|
||||
return null
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={cn('w-full max-w-full rounded-xl border-l-[0.5px] border-t border-effects-highlight')}>
|
||||
<div className='rounded-xl bg-background-default'>
|
||||
<div className='flex w-full flex-col items-start justify-center gap-3 self-stretch border-b-[0.5px] border-divider-subtle p-3'>
|
||||
<div className='flex w-full items-center gap-3 self-stretch'>
|
||||
<div className='flex grow items-center'>
|
||||
<div className='mr-3 shrink-0 rounded-lg border-[0.5px] border-divider-subtle bg-util-colors-indigo-indigo-500 p-1 shadow-md'>
|
||||
<Mcp className='h-4 w-4 text-text-primary-on-surface' />
|
||||
</div>
|
||||
<div className="group w-full">
|
||||
<div className="system-md-semibold min-w-0 overflow-hidden text-ellipsis break-normal text-text-secondary group-hover:text-text-primary">
|
||||
{t('tools.mcp.server.title')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex items-center gap-1'>
|
||||
<Indicator color={serverActivated ? 'green' : 'yellow'} />
|
||||
<div className={`${serverActivated ? 'text-text-success' : 'text-text-warning'} system-xs-semibold-uppercase`}>
|
||||
{serverActivated
|
||||
? t('appOverview.overview.status.running')
|
||||
: t('appOverview.overview.status.disable')}
|
||||
</div>
|
||||
</div>
|
||||
<Tooltip
|
||||
popupContent={appUnpublished ? t('tools.mcp.server.publishTip') : ''}
|
||||
>
|
||||
<div>
|
||||
<Switch defaultValue={activated} onChange={onChangeStatus} disabled={toggleDisabled} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className='flex flex-col items-start justify-center self-stretch'>
|
||||
<div className="system-xs-medium pb-1 text-text-tertiary">
|
||||
{t('tools.mcp.server.url')}
|
||||
</div>
|
||||
<div className="inline-flex h-9 w-full items-center gap-0.5 rounded-lg bg-components-input-bg-normal p-1 pl-2">
|
||||
<div className="flex h-4 min-w-0 flex-1 items-start justify-start gap-2 px-1">
|
||||
<div className="overflow-hidden text-ellipsis whitespace-nowrap text-xs font-medium text-text-secondary">
|
||||
{serverURL}
|
||||
</div>
|
||||
</div>
|
||||
{serverPublished && (
|
||||
<>
|
||||
<CopyFeedback
|
||||
content={serverURL}
|
||||
className={'!size-6'}
|
||||
/>
|
||||
<Divider type="vertical" className="!mx-0.5 !h-3.5 shrink-0" />
|
||||
{isCurrentWorkspaceManager && (
|
||||
<Tooltip
|
||||
popupContent={t('appOverview.overview.appInfo.regenerate') || ''}
|
||||
>
|
||||
<div
|
||||
className="cursor-pointer rounded-md p-1 hover:bg-state-base-hover"
|
||||
onClick={() => setShowConfirmDelete(true)}
|
||||
>
|
||||
<RiLoopLeftLine className={cn('h-4 w-4 text-text-tertiary hover:text-text-secondary', genLoading && 'animate-spin')}/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex items-center gap-1 self-stretch p-3'>
|
||||
<Button
|
||||
disabled={toggleDisabled}
|
||||
size='small'
|
||||
variant='ghost'
|
||||
onClick={() => setShowMCPServerModal(true)}
|
||||
>
|
||||
{serverPublished ? t('tools.mcp.server.edit') : t('tools.mcp.server.addDescription')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{showMCPServerModal && (
|
||||
<MCPServerModal
|
||||
show={showMCPServerModal}
|
||||
appID={appId}
|
||||
data={serverPublished ? detail : undefined}
|
||||
latestParams={latestParams}
|
||||
onHide={handleServerModalHide}
|
||||
/>
|
||||
)}
|
||||
{/* button copy link/ button regenerate */}
|
||||
{showConfirmDelete && (
|
||||
<Confirm
|
||||
type='warning'
|
||||
title={t('appOverview.overview.appInfo.regenerate')}
|
||||
content={t('tools.mcp.server.reGen')}
|
||||
isShow={showConfirmDelete}
|
||||
onConfirm={() => {
|
||||
onGenCode()
|
||||
setShowConfirmDelete(false)
|
||||
}}
|
||||
onCancel={() => setShowConfirmDelete(false)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default MCPServiceCard
|
||||
154
web/app/components/tools/mcp/mock.ts
Normal file
154
web/app/components/tools/mcp/mock.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
const tools = [
|
||||
{
|
||||
author: 'Novice',
|
||||
name: 'NOTION_ADD_PAGE_CONTENT',
|
||||
label: {
|
||||
en_US: 'NOTION_ADD_PAGE_CONTENT',
|
||||
zh_Hans: 'NOTION_ADD_PAGE_CONTENT',
|
||||
pt_BR: 'NOTION_ADD_PAGE_CONTENT',
|
||||
ja_JP: 'NOTION_ADD_PAGE_CONTENT',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Adds a single content block to a notion page. multiple calls needed for multiple blocks. note: only supports adding to notion pages. blocks that can contain children: - page (any block type) - toggle (any nested content) - to-do (nested to-dos/blocks) - bulleted list (nested lists/blocks) - numbered list (nested lists/blocks) - callout (child blocks) - quote (nested blocks)',
|
||||
zh_Hans: 'Adds a single content block to a notion page. multiple calls needed for multiple blocks. note: only supports adding to notion pages. blocks that can contain children: - page (any block type) - toggle (any nested content) - to-do (nested to-dos/blocks) - bulleted list (nested lists/blocks) - numbered list (nested lists/blocks) - callout (child blocks) - quote (nested blocks)',
|
||||
pt_BR: 'Adds a single content block to a notion page. multiple calls needed for multiple blocks. note: only supports adding to notion pages. blocks that can contain children: - page (any block type) - toggle (any nested content) - to-do (nested to-dos/blocks) - bulleted list (nested lists/blocks) - numbered list (nested lists/blocks) - callout (child blocks) - quote (nested blocks)',
|
||||
ja_JP: 'Adds a single content block to a notion page. multiple calls needed for multiple blocks. note: only supports adding to notion pages. blocks that can contain children: - page (any block type) - toggle (any nested content) - to-do (nested to-dos/blocks) - bulleted list (nested lists/blocks) - numbered list (nested lists/blocks) - callout (child blocks) - quote (nested blocks)',
|
||||
},
|
||||
parameters: [
|
||||
{
|
||||
name: 'after',
|
||||
label: {
|
||||
en_US: 'after',
|
||||
zh_Hans: 'after',
|
||||
pt_BR: 'after',
|
||||
ja_JP: 'after',
|
||||
},
|
||||
placeholder: null,
|
||||
scope: null,
|
||||
auto_generate: null,
|
||||
template: null,
|
||||
required: false,
|
||||
default: null,
|
||||
min: null,
|
||||
max: null,
|
||||
precision: null,
|
||||
options: [],
|
||||
type: 'string',
|
||||
human_description: {
|
||||
en_US: 'The ID of the existing block that the new block should be appended after. If not provided, content will be appended at the end of the page.',
|
||||
zh_Hans: 'The ID of the existing block that the new block should be appended after. If not provided, content will be appended at the end of the page.',
|
||||
pt_BR: 'The ID of the existing block that the new block should be appended after. If not provided, content will be appended at the end of the page.',
|
||||
ja_JP: 'The ID of the existing block that the new block should be appended after. If not provided, content will be appended at the end of the page.',
|
||||
},
|
||||
form: 'llm',
|
||||
llm_description: 'The ID of the existing block that the new block should be appended after. If not provided, content will be appended at the end of the page.',
|
||||
},
|
||||
{
|
||||
name: 'content_block',
|
||||
label: {
|
||||
en_US: 'content_block',
|
||||
zh_Hans: 'content_block',
|
||||
pt_BR: 'content_block',
|
||||
ja_JP: 'content_block',
|
||||
},
|
||||
placeholder: null,
|
||||
scope: null,
|
||||
auto_generate: null,
|
||||
template: null,
|
||||
required: false,
|
||||
default: null,
|
||||
min: null,
|
||||
max: null,
|
||||
precision: null,
|
||||
options: [],
|
||||
type: 'string',
|
||||
human_description: {
|
||||
en_US: 'Child content to append to a page.',
|
||||
zh_Hans: 'Child content to append to a page.',
|
||||
pt_BR: 'Child content to append to a page.',
|
||||
ja_JP: 'Child content to append to a page.',
|
||||
},
|
||||
form: 'llm',
|
||||
llm_description: 'Child content to append to a page.',
|
||||
},
|
||||
{
|
||||
name: 'parent_block_id',
|
||||
label: {
|
||||
en_US: 'parent_block_id',
|
||||
zh_Hans: 'parent_block_id',
|
||||
pt_BR: 'parent_block_id',
|
||||
ja_JP: 'parent_block_id',
|
||||
},
|
||||
placeholder: null,
|
||||
scope: null,
|
||||
auto_generate: null,
|
||||
template: null,
|
||||
required: false,
|
||||
default: null,
|
||||
min: null,
|
||||
max: null,
|
||||
precision: null,
|
||||
options: [],
|
||||
type: 'string',
|
||||
human_description: {
|
||||
en_US: 'The ID of the page which the children will be added.',
|
||||
zh_Hans: 'The ID of the page which the children will be added.',
|
||||
pt_BR: 'The ID of the page which the children will be added.',
|
||||
ja_JP: 'The ID of the page which the children will be added.',
|
||||
},
|
||||
form: 'llm',
|
||||
llm_description: 'The ID of the page which the children will be added.',
|
||||
},
|
||||
],
|
||||
labels: [],
|
||||
output_schema: null,
|
||||
},
|
||||
]
|
||||
|
||||
export const listData = [
|
||||
{
|
||||
id: 'fdjklajfkljadslf111',
|
||||
author: 'KVOJJJin',
|
||||
name: 'GOGOGO',
|
||||
icon: 'https://cloud.dify.dev/console/api/workspaces/694cc430-fa36-4458-86a0-4a98c09c4684/model-providers/langgenius/openai/openai/icon_small/en_US',
|
||||
server_url: 'https://mcp.composio.dev/notion/****/abc',
|
||||
type: 'mcp',
|
||||
is_team_authorization: true,
|
||||
tools,
|
||||
update_elapsed_time: 1744793369,
|
||||
label: {
|
||||
en_US: 'GOGOGO',
|
||||
zh_Hans: 'GOGOGO',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'fdjklajfkljadslf222',
|
||||
author: 'KVOJJJin',
|
||||
name: 'GOGOGO2',
|
||||
icon: 'https://cloud.dify.dev/console/api/workspaces/694cc430-fa36-4458-86a0-4a98c09c4684/model-providers/langgenius/openai/openai/icon_small/en_US',
|
||||
server_url: 'https://mcp.composio.dev/notion/****/abc',
|
||||
type: 'mcp',
|
||||
is_team_authorization: false,
|
||||
tools: [],
|
||||
update_elapsed_time: 1744793369,
|
||||
label: {
|
||||
en_US: 'GOGOGO2',
|
||||
zh_Hans: 'GOGOGO2',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'fdjklajfkljadslf333',
|
||||
author: 'KVOJJJin',
|
||||
name: 'GOGOGO3',
|
||||
icon: 'https://cloud.dify.dev/console/api/workspaces/694cc430-fa36-4458-86a0-4a98c09c4684/model-providers/langgenius/openai/openai/icon_small/en_US',
|
||||
server_url: 'https://mcp.composio.dev/notion/****/abc',
|
||||
type: 'mcp',
|
||||
is_team_authorization: true,
|
||||
tools,
|
||||
update_elapsed_time: 1744793369,
|
||||
label: {
|
||||
en_US: 'GOGOGO3',
|
||||
zh_Hans: 'GOGOGO3',
|
||||
},
|
||||
},
|
||||
]
|
||||
221
web/app/components/tools/mcp/modal.tsx
Normal file
221
web/app/components/tools/mcp/modal.tsx
Normal file
@@ -0,0 +1,221 @@
|
||||
'use client'
|
||||
import React, { useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { getDomain } from 'tldts'
|
||||
import { RiCloseLine, RiEditLine } from '@remixicon/react'
|
||||
import AppIconPicker from '@/app/components/base/app-icon-picker'
|
||||
import type { AppIconSelection } from '@/app/components/base/app-icon-picker'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Input from '@/app/components/base/input'
|
||||
import type { AppIconType } from '@/types/app'
|
||||
import type { ToolWithProvider } from '@/app/components/workflow/types'
|
||||
import { noop } from 'lodash-es'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { uploadRemoteFileInfo } from '@/service/common'
|
||||
import cn from '@/utils/classnames'
|
||||
import { useHover } from 'ahooks'
|
||||
|
||||
export type DuplicateAppModalProps = {
|
||||
data?: ToolWithProvider
|
||||
show: boolean
|
||||
onConfirm: (info: {
|
||||
name: string
|
||||
server_url: string
|
||||
icon_type: AppIconType
|
||||
icon: string
|
||||
icon_background?: string | null
|
||||
server_identifier: string
|
||||
}) => void
|
||||
onHide: () => void
|
||||
}
|
||||
|
||||
const DEFAULT_ICON = { type: 'emoji', icon: '🧿', background: '#EFF1F5' }
|
||||
const extractFileId = (url: string) => {
|
||||
const match = url.match(/files\/(.+?)\/file-preview/)
|
||||
return match ? match[1] : null
|
||||
}
|
||||
const getIcon = (data?: ToolWithProvider) => {
|
||||
if (!data)
|
||||
return DEFAULT_ICON as AppIconSelection
|
||||
if (typeof data.icon === 'string')
|
||||
return { type: 'image', url: data.icon, fileId: extractFileId(data.icon) } as AppIconSelection
|
||||
return {
|
||||
...data.icon,
|
||||
icon: data.icon.content,
|
||||
type: 'emoji',
|
||||
} as unknown as AppIconSelection
|
||||
}
|
||||
|
||||
const MCPModal = ({
|
||||
data,
|
||||
show,
|
||||
onConfirm,
|
||||
onHide,
|
||||
}: DuplicateAppModalProps) => {
|
||||
const { t } = useTranslation()
|
||||
const isCreate = !data
|
||||
|
||||
const originalServerUrl = data?.server_url
|
||||
const originalServerID = data?.server_identifier
|
||||
const [url, setUrl] = React.useState(data?.server_url || '')
|
||||
const [name, setName] = React.useState(data?.name || '')
|
||||
const [appIcon, setAppIcon] = useState<AppIconSelection>(getIcon(data))
|
||||
const [showAppIconPicker, setShowAppIconPicker] = useState(false)
|
||||
const [serverIdentifier, setServerIdentifier] = React.useState(data?.server_identifier || '')
|
||||
const [isFetchingIcon, setIsFetchingIcon] = useState(false)
|
||||
const appIconRef = useRef<HTMLDivElement>(null)
|
||||
const isHovering = useHover(appIconRef)
|
||||
|
||||
const isValidUrl = (string: string) => {
|
||||
try {
|
||||
const urlPattern = /^(https?:\/\/)((([a-z\d]([a-z\d-]*[a-z\d])*)\.)+[a-z]{2,}|((\d{1,3}\.){3}\d{1,3}))(\:\d+)?(\/[-a-z\d%_.~+]*)*(\?[;&a-z\d%_.~+=-]*)?/i
|
||||
return urlPattern.test(string)
|
||||
}
|
||||
catch (e) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const isValidServerID = (str: string) => {
|
||||
return /^[a-z0-9_-]{1,24}$/.test(str)
|
||||
}
|
||||
|
||||
const handleBlur = async (url: string) => {
|
||||
if (data)
|
||||
return
|
||||
if (!isValidUrl(url))
|
||||
return
|
||||
const domain = getDomain(url)
|
||||
const remoteIcon = `https://www.google.com/s2/favicons?domain=${domain}&sz=128`
|
||||
setIsFetchingIcon(true)
|
||||
try {
|
||||
const res = await uploadRemoteFileInfo(remoteIcon, undefined, true)
|
||||
setAppIcon({ type: 'image', url: res.url, fileId: extractFileId(res.url) || '' })
|
||||
}
|
||||
catch (e) {
|
||||
console.error('Failed to fetch remote icon:', e)
|
||||
Toast.notify({ type: 'warning', message: 'Failed to fetch remote icon' })
|
||||
}
|
||||
finally {
|
||||
setIsFetchingIcon(false)
|
||||
}
|
||||
}
|
||||
|
||||
const submit = async () => {
|
||||
if (!isValidUrl(url)) {
|
||||
Toast.notify({ type: 'error', message: 'invalid server url' })
|
||||
return
|
||||
}
|
||||
if (!isValidServerID(serverIdentifier.trim())) {
|
||||
Toast.notify({ type: 'error', message: 'invalid server identifier' })
|
||||
return
|
||||
}
|
||||
await onConfirm({
|
||||
server_url: originalServerUrl === url ? '[__HIDDEN__]' : url.trim(),
|
||||
name,
|
||||
icon_type: appIcon.type,
|
||||
icon: appIcon.type === 'emoji' ? appIcon.icon : appIcon.fileId,
|
||||
icon_background: appIcon.type === 'emoji' ? appIcon.background : undefined,
|
||||
server_identifier: serverIdentifier.trim(),
|
||||
})
|
||||
if(isCreate)
|
||||
onHide()
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
isShow={show}
|
||||
onClose={noop}
|
||||
className={cn('relative !max-w-[520px]', 'p-6')}
|
||||
>
|
||||
<div className='absolute right-5 top-5 z-10 cursor-pointer p-1.5' onClick={onHide}>
|
||||
<RiCloseLine className='h-5 w-5 text-text-tertiary' />
|
||||
</div>
|
||||
<div className='title-2xl-semi-bold relative pb-3 text-xl text-text-primary'>{!isCreate ? t('tools.mcp.modal.editTitle') : t('tools.mcp.modal.title')}</div>
|
||||
<div className='space-y-5 py-3'>
|
||||
<div>
|
||||
<div className='mb-1 flex h-6 items-center'>
|
||||
<span className='system-sm-medium text-text-secondary'>{t('tools.mcp.modal.serverUrl')}</span>
|
||||
</div>
|
||||
<Input
|
||||
value={url}
|
||||
onChange={e => setUrl(e.target.value)}
|
||||
onBlur={e => handleBlur(e.target.value.trim())}
|
||||
placeholder={t('tools.mcp.modal.serverUrlPlaceholder')}
|
||||
/>
|
||||
{originalServerUrl && originalServerUrl !== url && (
|
||||
<div className='mt-1 flex h-5 items-center'>
|
||||
<span className='body-xs-regular text-text-warning'>{t('tools.mcp.modal.serverUrlWarning')}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className='flex space-x-3'>
|
||||
<div className='grow pb-1'>
|
||||
<div className='mb-1 flex h-6 items-center'>
|
||||
<span className='system-sm-medium text-text-secondary'>{t('tools.mcp.modal.name')}</span>
|
||||
</div>
|
||||
<Input
|
||||
value={name}
|
||||
onChange={e => setName(e.target.value)}
|
||||
placeholder={t('tools.mcp.modal.namePlaceholder')}
|
||||
/>
|
||||
</div>
|
||||
<div className='pt-2' ref={appIconRef}>
|
||||
<AppIcon
|
||||
iconType={appIcon.type}
|
||||
icon={appIcon.type === 'emoji' ? appIcon.icon : appIcon.fileId}
|
||||
background={appIcon.type === 'emoji' ? appIcon.background : undefined}
|
||||
imageUrl={appIcon.type === 'image' ? appIcon.url : undefined}
|
||||
size='xxl'
|
||||
className='relative cursor-pointer rounded-2xl'
|
||||
coverElement={
|
||||
isHovering
|
||||
? (<div className='absolute inset-0 flex items-center justify-center overflow-hidden rounded-2xl bg-background-overlay-alt'>
|
||||
<RiEditLine className='size-6 text-text-primary-on-surface' />
|
||||
</div>) : null
|
||||
}
|
||||
onClick={() => { setShowAppIconPicker(true) }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className='flex h-6 items-center'>
|
||||
<span className='system-sm-medium text-text-secondary'>{t('tools.mcp.modal.serverIdentifier')}</span>
|
||||
</div>
|
||||
<div className='body-xs-regular mb-1 text-text-tertiary'>{t('tools.mcp.modal.serverIdentifierTip')}</div>
|
||||
<Input
|
||||
value={serverIdentifier}
|
||||
onChange={e => setServerIdentifier(e.target.value)}
|
||||
placeholder={t('tools.mcp.modal.serverIdentifierPlaceholder')}
|
||||
/>
|
||||
{originalServerID && originalServerID !== serverIdentifier && (
|
||||
<div className='mt-1 flex h-5 items-center'>
|
||||
<span className='body-xs-regular text-text-warning'>{t('tools.mcp.modal.serverIdentifierWarning')}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex flex-row-reverse pt-5'>
|
||||
<Button disabled={!name || !url || !serverIdentifier || isFetchingIcon} className='ml-2' variant='primary' onClick={submit}>{data ? t('tools.mcp.modal.save') : t('tools.mcp.modal.confirm')}</Button>
|
||||
<Button onClick={onHide}>{t('tools.mcp.modal.cancel')}</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
{showAppIconPicker && <AppIconPicker
|
||||
onSelect={(payload) => {
|
||||
setAppIcon(payload)
|
||||
setShowAppIconPicker(false)
|
||||
}}
|
||||
onClose={() => {
|
||||
setAppIcon(getIcon(data))
|
||||
setShowAppIconPicker(false)
|
||||
}}
|
||||
/>}
|
||||
</>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
export default MCPModal
|
||||
152
web/app/components/tools/mcp/provider-card.tsx
Normal file
152
web/app/components/tools/mcp/provider-card.tsx
Normal file
@@ -0,0 +1,152 @@
|
||||
'use client'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { RiHammerFill } from '@remixicon/react'
|
||||
import Indicator from '@/app/components/header/indicator'
|
||||
import Icon from '@/app/components/plugins/card/base/card-icon'
|
||||
import { useFormatTimeFromNow } from './hooks'
|
||||
import type { ToolWithProvider } from '../../workflow/types'
|
||||
import Confirm from '@/app/components/base/confirm'
|
||||
import MCPModal from './modal'
|
||||
import OperationDropdown from './detail/operation-dropdown'
|
||||
import { useDeleteMCP, useUpdateMCP } from '@/service/use-tools'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type Props = {
|
||||
currentProvider?: ToolWithProvider
|
||||
data: ToolWithProvider
|
||||
handleSelect: (providerID: string) => void
|
||||
onUpdate: (providerID: string) => void
|
||||
onDeleted: () => void
|
||||
}
|
||||
|
||||
const MCPCard = ({
|
||||
currentProvider,
|
||||
data,
|
||||
onUpdate,
|
||||
handleSelect,
|
||||
onDeleted,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const { formatTimeFromNow } = useFormatTimeFromNow()
|
||||
const { isCurrentWorkspaceManager } = useAppContext()
|
||||
|
||||
const { mutateAsync: updateMCP } = useUpdateMCP({})
|
||||
const { mutateAsync: deleteMCP } = useDeleteMCP({})
|
||||
|
||||
const [isOperationShow, setIsOperationShow] = useState(false)
|
||||
|
||||
const [isShowUpdateModal, {
|
||||
setTrue: showUpdateModal,
|
||||
setFalse: hideUpdateModal,
|
||||
}] = useBoolean(false)
|
||||
|
||||
const [isShowDeleteConfirm, {
|
||||
setTrue: showDeleteConfirm,
|
||||
setFalse: hideDeleteConfirm,
|
||||
}] = useBoolean(false)
|
||||
|
||||
const [deleting, {
|
||||
setTrue: showDeleting,
|
||||
setFalse: hideDeleting,
|
||||
}] = useBoolean(false)
|
||||
|
||||
const handleUpdate = useCallback(async (form: any) => {
|
||||
const res = await updateMCP({
|
||||
...form,
|
||||
provider_id: data.id,
|
||||
})
|
||||
if ((res as any)?.result === 'success') {
|
||||
hideUpdateModal()
|
||||
onUpdate(data.id)
|
||||
}
|
||||
}, [data, updateMCP, hideUpdateModal, onUpdate])
|
||||
|
||||
const handleDelete = useCallback(async () => {
|
||||
showDeleting()
|
||||
const res = await deleteMCP(data.id)
|
||||
hideDeleting()
|
||||
if ((res as any)?.result === 'success') {
|
||||
hideDeleteConfirm()
|
||||
onDeleted()
|
||||
}
|
||||
}, [showDeleting, deleteMCP, data.id, hideDeleting, hideDeleteConfirm, onDeleted])
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={() => handleSelect(data.id)}
|
||||
className={cn(
|
||||
'group relative flex cursor-pointer flex-col rounded-xl border-[1.5px] border-transparent bg-components-card-bg shadow-xs hover:bg-components-card-bg-alt hover:shadow-md',
|
||||
currentProvider?.id === data.id && 'border-components-option-card-option-selected-border bg-components-card-bg-alt',
|
||||
)}
|
||||
>
|
||||
<div className='flex grow items-center gap-3 rounded-t-xl p-4'>
|
||||
<div className='shrink-0 overflow-hidden rounded-xl border border-components-panel-border-subtle'>
|
||||
<Icon src={data.icon} />
|
||||
</div>
|
||||
<div className='grow'>
|
||||
<div className='system-md-semibold mb-1 truncate text-text-secondary' title={data.name}>{data.name}</div>
|
||||
<div className='system-xs-regular text-text-tertiary'>{data.server_identifier}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex items-center gap-1 rounded-b-xl pb-2.5 pl-4 pr-2.5 pt-1.5'>
|
||||
<div className='flex w-0 grow items-center gap-2'>
|
||||
<div className='flex items-center gap-1'>
|
||||
<RiHammerFill className='h-3 w-3 shrink-0 text-text-quaternary' />
|
||||
{data.tools.length > 0 && (
|
||||
<div className='system-xs-regular shrink-0 text-text-tertiary'>{t('tools.mcp.toolsCount', { count: data.tools.length })}</div>
|
||||
)}
|
||||
{!data.tools.length && (
|
||||
<div className='system-xs-regular shrink-0 text-text-tertiary'>{t('tools.mcp.noTools')}</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={cn('system-xs-regular text-divider-deep', (!data.is_team_authorization || !data.tools.length) && 'sm:hidden')}>/</div>
|
||||
<div className={cn('system-xs-regular truncate text-text-tertiary', (!data.is_team_authorization || !data.tools.length) && ' sm:hidden')} title={`${t('tools.mcp.updateTime')} ${formatTimeFromNow(data.updated_at! * 1000)}`}>{`${t('tools.mcp.updateTime')} ${formatTimeFromNow(data.updated_at! * 1000)}`}</div>
|
||||
</div>
|
||||
{data.is_team_authorization && data.tools.length > 0 && <Indicator color='green' className='shrink-0' />}
|
||||
{(!data.is_team_authorization || !data.tools.length) && (
|
||||
<div className='system-xs-medium flex shrink-0 items-center gap-1 rounded-md border border-util-colors-red-red-500 bg-components-badge-bg-red-soft px-1.5 py-0.5 text-util-colors-red-red-500'>
|
||||
{t('tools.mcp.noConfigured')}
|
||||
<Indicator color='red' />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{isCurrentWorkspaceManager && (
|
||||
<div className={cn('absolute right-2.5 top-2.5 hidden group-hover:block', isOperationShow && 'block')} onClick={e => e.stopPropagation()}>
|
||||
<OperationDropdown
|
||||
inCard
|
||||
onOpenChange={setIsOperationShow}
|
||||
onEdit={showUpdateModal}
|
||||
onRemove={showDeleteConfirm}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{isShowUpdateModal && (
|
||||
<MCPModal
|
||||
data={data}
|
||||
show={isShowUpdateModal}
|
||||
onConfirm={handleUpdate}
|
||||
onHide={hideUpdateModal}
|
||||
/>
|
||||
)}
|
||||
{isShowDeleteConfirm && (
|
||||
<Confirm
|
||||
isShow
|
||||
title={t('tools.mcp.delete')}
|
||||
content={
|
||||
<div>
|
||||
{t('tools.mcp.deleteConfirmTitle', { mcp: data.name })}
|
||||
</div>
|
||||
}
|
||||
onCancel={hideDeleteConfirm}
|
||||
onConfirm={handleDelete}
|
||||
isLoading={deleting}
|
||||
isDisabled={deleting}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default MCPCard
|
||||
@@ -15,11 +15,29 @@ import WorkflowToolEmpty from '@/app/components/tools/add-tool-modal/empty'
|
||||
import Card from '@/app/components/plugins/card'
|
||||
import CardMoreInfo from '@/app/components/plugins/card/card-more-info'
|
||||
import PluginDetailPanel from '@/app/components/plugins/plugin-detail-panel'
|
||||
import MCPList from './mcp'
|
||||
import { useAllToolProviders } from '@/service/use-tools'
|
||||
import { useInstalledPluginList, useInvalidateInstalledPluginList } from '@/service/use-plugins'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
import { ToolTypeEnum } from '../workflow/block-selector/types'
|
||||
|
||||
const getToolType = (type: string) => {
|
||||
switch (type) {
|
||||
case 'builtin':
|
||||
return ToolTypeEnum.BuiltIn
|
||||
case 'api':
|
||||
return ToolTypeEnum.Custom
|
||||
case 'workflow':
|
||||
return ToolTypeEnum.Workflow
|
||||
case 'mcp':
|
||||
return ToolTypeEnum.MCP
|
||||
default:
|
||||
return ToolTypeEnum.BuiltIn
|
||||
}
|
||||
}
|
||||
const ProviderList = () => {
|
||||
// const searchParams = useSearchParams()
|
||||
// searchParams.get('category') === 'workflow'
|
||||
const { t } = useTranslation()
|
||||
const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures)
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
@@ -31,6 +49,7 @@ const ProviderList = () => {
|
||||
{ value: 'builtin', text: t('tools.type.builtIn') },
|
||||
{ value: 'api', text: t('tools.type.custom') },
|
||||
{ value: 'workflow', text: t('tools.type.workflow') },
|
||||
{ value: 'mcp', text: 'MCP' },
|
||||
]
|
||||
const [tagFilterValue, setTagFilterValue] = useState<string[]>([])
|
||||
const handleTagsChange = (value: string[]) => {
|
||||
@@ -85,7 +104,9 @@ const ProviderList = () => {
|
||||
options={options}
|
||||
/>
|
||||
<div className='flex items-center gap-2'>
|
||||
<LabelFilter value={tagFilterValue} onChange={handleTagsChange} />
|
||||
{activeTab !== 'mcp' && (
|
||||
<LabelFilter value={tagFilterValue} onChange={handleTagsChange} />
|
||||
)}
|
||||
<Input
|
||||
showLeftIcon
|
||||
showClearIcon
|
||||
@@ -96,7 +117,7 @@ const ProviderList = () => {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{(filteredCollectionList.length > 0 || activeTab !== 'builtin') && (
|
||||
{activeTab !== 'mcp' && (
|
||||
<div className={cn(
|
||||
'relative grid shrink-0 grid-cols-1 content-start gap-4 px-12 pb-4 pt-2 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4',
|
||||
!filteredCollectionList.length && activeTab === 'workflow' && 'grow',
|
||||
@@ -127,25 +148,26 @@ const ProviderList = () => {
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
{!filteredCollectionList.length && activeTab === 'workflow' && <div className='absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2'><WorkflowToolEmpty /></div>}
|
||||
{!filteredCollectionList.length && activeTab === 'workflow' && <div className='absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2'><WorkflowToolEmpty type={getToolType(activeTab)} /></div>}
|
||||
</div>
|
||||
)}
|
||||
{!filteredCollectionList.length && activeTab === 'builtin' && (
|
||||
<Empty lightCard text={t('tools.noTools')} className='h-[224px] px-12' />
|
||||
)}
|
||||
{
|
||||
enable_marketplace && activeTab === 'builtin' && (
|
||||
<Marketplace
|
||||
onMarketplaceScroll={() => {
|
||||
containerRef.current?.scrollTo({ top: containerRef.current.scrollHeight, behavior: 'smooth' })
|
||||
}}
|
||||
searchPluginText={keywords}
|
||||
filterPluginTags={tagFilterValue}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div >
|
||||
</div >
|
||||
{enable_marketplace && activeTab === 'builtin' && (
|
||||
<Marketplace
|
||||
onMarketplaceScroll={() => {
|
||||
containerRef.current?.scrollTo({ top: containerRef.current.scrollHeight, behavior: 'smooth' })
|
||||
}}
|
||||
searchPluginText={keywords}
|
||||
filterPluginTags={tagFilterValue}
|
||||
/>
|
||||
)}
|
||||
{activeTab === 'mcp' && (
|
||||
<MCPList searchText={keywords} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{currentProvider && !currentProvider.plugin_id && (
|
||||
<ProviderDetail
|
||||
collection={currentProvider}
|
||||
|
||||
@@ -3,13 +3,13 @@ import { useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import {
|
||||
RiAddLine,
|
||||
RiAddCircleFill,
|
||||
RiArrowRightUpLine,
|
||||
RiBookOpenLine,
|
||||
} from '@remixicon/react'
|
||||
import type { CustomCollectionBackend } from '../types'
|
||||
import I18n from '@/context/i18n'
|
||||
import { getLanguage } from '@/i18n/language'
|
||||
import { BookOpen01 } from '@/app/components/base/icons/src/vender/line/education'
|
||||
import { ArrowUpRight } from '@/app/components/base/icons/src/vender/line/arrows'
|
||||
import EditCustomToolModal from '@/app/components/tools/edit-custom-collection-modal'
|
||||
import { createCustomCollection } from '@/service/tools'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
@@ -47,20 +47,20 @@ const Contribute = ({ onRefreshData }: Props) => {
|
||||
return (
|
||||
<>
|
||||
{isCurrentWorkspaceManager && (
|
||||
<div className='col-span-1 flex min-h-[135px] cursor-pointer flex-col rounded-xl border-[0.5px] border-divider-subtle bg-components-panel-on-panel-item-bg transition-all duration-200 ease-in-out hover:bg-components-panel-on-panel-item-bg-hover hover:shadow-lg'>
|
||||
<div className='group grow rounded-t-xl hover:bg-background-body' onClick={() => setIsShowEditCustomCollectionModal(true)}>
|
||||
<div className='col-span-1 flex min-h-[135px] cursor-pointer flex-col rounded-xl bg-background-default-dimmed transition-all duration-200 ease-in-out'>
|
||||
<div className='group grow rounded-t-xl' onClick={() => setIsShowEditCustomCollectionModal(true)}>
|
||||
<div className='flex shrink-0 items-center p-4 pb-3'>
|
||||
<div className='flex h-10 w-10 items-center justify-center rounded-lg border border-components-option-card-option-border bg-components-option-card-option-bg group-hover:border-components-option-card-option-border-hover group-hover:bg-components-option-card-option-bg-hover'>
|
||||
<RiAddLine className='h-4 w-4 text-text-tertiary group-hover:text-text-accent'/>
|
||||
<div className='flex h-10 w-10 items-center justify-center rounded-lg border border-dashed border-divider-deep group-hover:border-solid group-hover:border-state-accent-hover-alt group-hover:bg-state-accent-hover'>
|
||||
<RiAddCircleFill className='h-4 w-4 text-text-quaternary group-hover:text-text-accent'/>
|
||||
</div>
|
||||
<div className='ml-3 text-sm font-semibold leading-5 text-text-primary group-hover:text-text-accent'>{t('tools.createCustomTool')}</div>
|
||||
<div className='system-md-semibold ml-3 text-text-secondary group-hover:text-text-accent'>{t('tools.createCustomTool')}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='rounded-b-xl border-t-[0.5px] border-divider-regular px-4 py-3 text-text-tertiary hover:bg-background-body hover:text-text-accent'>
|
||||
<div className='rounded-b-xl border-t-[0.5px] border-divider-subtle px-4 py-3 text-text-tertiary hover:text-text-accent'>
|
||||
<a href={linkUrl} target='_blank' rel='noopener noreferrer' className='flex items-center space-x-1'>
|
||||
<BookOpen01 className='h-3 w-3 shrink-0' />
|
||||
<div className='grow truncate text-xs font-normal leading-[18px]' title={t('tools.customToolTip') || ''}>{t('tools.customToolTip')}</div>
|
||||
<ArrowUpRight className='h-3 w-3 shrink-0' />
|
||||
<RiBookOpenLine className='h-3 w-3 shrink-0' />
|
||||
<div className='system-xs-regular grow truncate' title={t('tools.customToolTip') || ''}>{t('tools.customToolTip')}</div>
|
||||
<RiArrowRightUpLine className='h-3 w-3 shrink-0' />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -29,7 +29,7 @@ const ToolItem = ({
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={cn('bg-components-panel-item-bg mb-2 cursor-pointer rounded-xl border-[0.5px] border-components-panel-border-subtle px-4 py-3 shadow-xs hover:bg-components-panel-on-panel-item-bg-hover', disabled && '!cursor-not-allowed opacity-50')}
|
||||
className={cn('bg-components-panel-item-bg cursor-pointer rounded-xl border-[0.5px] border-components-panel-border-subtle px-4 py-3 shadow-xs hover:bg-components-panel-on-panel-item-bg-hover', disabled && '!cursor-not-allowed opacity-50')}
|
||||
onClick={() => !disabled && setShowDetail(true)}
|
||||
>
|
||||
<div className='system-md-semibold pb-0.5 text-text-secondary'>{tool.label[language]}</div>
|
||||
|
||||
@@ -29,6 +29,7 @@ export enum CollectionType {
|
||||
custom = 'api',
|
||||
model = 'model',
|
||||
workflow = 'workflow',
|
||||
mcp = 'mcp',
|
||||
}
|
||||
|
||||
export type Emoji = {
|
||||
@@ -50,6 +51,10 @@ export type Collection = {
|
||||
labels: string[]
|
||||
plugin_id?: string
|
||||
letter?: string
|
||||
// MCP Server
|
||||
server_url?: string
|
||||
updated_at?: number
|
||||
server_identifier?: string
|
||||
}
|
||||
|
||||
export type ToolParameter = {
|
||||
@@ -168,3 +173,11 @@ export type WorkflowToolProviderResponse = {
|
||||
}
|
||||
privacy_policy: string
|
||||
}
|
||||
|
||||
export type MCPServerDetail = {
|
||||
id: string
|
||||
server_code: string
|
||||
description: string
|
||||
status: string
|
||||
parameters?: Record<string, string>
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import type { ToolCredential, ToolParameter } from '../types'
|
||||
import { FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { VarType as VarKindType } from '@/app/components/workflow/nodes/tool/types'
|
||||
|
||||
export const toType = (type: string) => {
|
||||
switch (type) {
|
||||
case 'string':
|
||||
@@ -54,7 +57,7 @@ export const toolCredentialToFormSchemas = (parameters: ToolCredential[]) => {
|
||||
return formSchemas
|
||||
}
|
||||
|
||||
export const addDefaultValue = (value: Record<string, any>, formSchemas: { variable: string; default?: any }[]) => {
|
||||
export const addDefaultValue = (value: Record<string, any>, formSchemas: { variable: string; type: string; default?: any }[]) => {
|
||||
const newValues = { ...value }
|
||||
formSchemas.forEach((formSchema) => {
|
||||
const itemValue = value[formSchema.variable]
|
||||
@@ -64,14 +67,47 @@ export const addDefaultValue = (value: Record<string, any>, formSchemas: { varia
|
||||
return newValues
|
||||
}
|
||||
|
||||
export const generateFormValue = (value: Record<string, any>, formSchemas: { variable: string; default?: any }[], isReasoning = false) => {
|
||||
const correctInitialData = (type: string, target: any, defaultValue: any) => {
|
||||
if (type === 'text-input' || type === 'secret-input')
|
||||
target.type = 'mixed'
|
||||
|
||||
if (type === 'boolean') {
|
||||
if (typeof defaultValue === 'string')
|
||||
target.value = defaultValue === 'true' || defaultValue === '1'
|
||||
|
||||
if (typeof defaultValue === 'boolean')
|
||||
target.value = defaultValue
|
||||
|
||||
if (typeof defaultValue === 'number')
|
||||
target.value = defaultValue === 1
|
||||
}
|
||||
|
||||
if (type === 'number-input') {
|
||||
if (typeof defaultValue === 'string' && defaultValue !== '')
|
||||
target.value = Number.parseFloat(defaultValue)
|
||||
}
|
||||
|
||||
if (type === 'app-selector' || type === 'model-selector')
|
||||
target.value = defaultValue
|
||||
|
||||
return target
|
||||
}
|
||||
|
||||
export const generateFormValue = (value: Record<string, any>, formSchemas: { variable: string; default?: any; type: string }[], isReasoning = false) => {
|
||||
const newValues = {} as any
|
||||
formSchemas.forEach((formSchema) => {
|
||||
const itemValue = value[formSchema.variable]
|
||||
if ((formSchema.default !== undefined) && (value === undefined || itemValue === null || itemValue === '' || itemValue === undefined)) {
|
||||
const value = formSchema.default
|
||||
newValues[formSchema.variable] = {
|
||||
...(isReasoning ? { value: null, auto: 1 } : { value: formSchema.default }),
|
||||
value: {
|
||||
type: 'constant',
|
||||
value: formSchema.default,
|
||||
},
|
||||
...(isReasoning ? { auto: 1, value: null } : {}),
|
||||
}
|
||||
if (!isReasoning)
|
||||
newValues[formSchema.variable].value = correctInitialData(formSchema.type, newValues[formSchema.variable].value, value)
|
||||
}
|
||||
})
|
||||
return newValues
|
||||
@@ -80,7 +116,9 @@ export const generateFormValue = (value: Record<string, any>, formSchemas: { var
|
||||
export const getPlainValue = (value: Record<string, any>) => {
|
||||
const plainValue = { ...value }
|
||||
Object.keys(plainValue).forEach((key) => {
|
||||
plainValue[key] = value[key].value
|
||||
plainValue[key] = {
|
||||
...value[key].value,
|
||||
}
|
||||
})
|
||||
return plainValue
|
||||
}
|
||||
@@ -94,3 +132,65 @@ export const getStructureValue = (value: Record<string, any>) => {
|
||||
})
|
||||
return newValue
|
||||
}
|
||||
|
||||
export const getConfiguredValue = (value: Record<string, any>, formSchemas: { variable: string; type: string; default?: any }[]) => {
|
||||
const newValues = { ...value }
|
||||
formSchemas.forEach((formSchema) => {
|
||||
const itemValue = value[formSchema.variable]
|
||||
if ((formSchema.default !== undefined) && (value === undefined || itemValue === null || itemValue === '' || itemValue === undefined)) {
|
||||
const value = formSchema.default
|
||||
newValues[formSchema.variable] = {
|
||||
type: 'constant',
|
||||
value: formSchema.default,
|
||||
}
|
||||
newValues[formSchema.variable] = correctInitialData(formSchema.type, newValues[formSchema.variable], value)
|
||||
}
|
||||
})
|
||||
return newValues
|
||||
}
|
||||
|
||||
const getVarKindType = (type: FormTypeEnum) => {
|
||||
if (type === FormTypeEnum.file || type === FormTypeEnum.files)
|
||||
return VarKindType.variable
|
||||
if (type === FormTypeEnum.select || type === FormTypeEnum.boolean || type === FormTypeEnum.textNumber)
|
||||
return VarKindType.constant
|
||||
if (type === FormTypeEnum.textInput || type === FormTypeEnum.secretInput)
|
||||
return VarKindType.mixed
|
||||
}
|
||||
|
||||
export const generateAgentToolValue = (value: Record<string, any>, formSchemas: { variable: string; default?: any; type: string }[], isReasoning = false) => {
|
||||
const newValues = {} as any
|
||||
if (!isReasoning) {
|
||||
formSchemas.forEach((formSchema) => {
|
||||
const itemValue = value[formSchema.variable]
|
||||
newValues[formSchema.variable] = {
|
||||
value: {
|
||||
type: 'constant',
|
||||
value: itemValue.value,
|
||||
},
|
||||
}
|
||||
newValues[formSchema.variable].value = correctInitialData(formSchema.type, newValues[formSchema.variable].value, itemValue.value)
|
||||
})
|
||||
}
|
||||
else {
|
||||
formSchemas.forEach((formSchema) => {
|
||||
const itemValue = value[formSchema.variable]
|
||||
if (itemValue.auto === 1) {
|
||||
newValues[formSchema.variable] = {
|
||||
auto: 1,
|
||||
value: null,
|
||||
}
|
||||
}
|
||||
else {
|
||||
newValues[formSchema.variable] = {
|
||||
auto: 0,
|
||||
value: itemValue.value || {
|
||||
type: getVarKindType(formSchema.type as FormTypeEnum),
|
||||
value: null,
|
||||
},
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
return newValues
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user