Merge branch 'feat/plugins' into dev/plugin-deploy

This commit is contained in:
zxhlyh
2025-01-08 14:20:36 +08:00
42 changed files with 316 additions and 146 deletions

View File

@ -11,7 +11,7 @@ import Placeholder from './base/placeholder'
import cn from '@/utils/classnames'
import { useGetLanguage } from '@/context/i18n'
import { getLanguage } from '@/i18n/language'
import { useCategories } from '../hooks'
import { useSingleCategories } from '../hooks'
import { renderI18nObject } from '@/hooks/use-i18n'
export type Props = {
@ -43,7 +43,7 @@ const Card = ({
}: Props) => {
const defaultLocale = useGetLanguage()
const locale = localeFromProps ? getLanguage(localeFromProps) : defaultLocale
const { categoriesMap } = useCategories()
const { categoriesMap } = useSingleCategories()
const { category, type, name, org, label, brief, icon, verified } = payload
const isBundle = !['plugin', 'model', 'tool', 'extension', 'agent_strategy'].includes(type)
const cornerMark = isBundle ? categoriesMap.bundle?.label : categoriesMap[category]?.label

View File

@ -64,3 +64,31 @@ export const useCategories = (translateFromOut?: TFunction) => {
categoriesMap,
}
}
export const useSingleCategories = (translateFromOut?: TFunction) => {
const { t: translation } = useTranslation()
const t = translateFromOut || translation
const categories = categoryKeys.map((category) => {
if (category === 'agent') {
return {
name: 'agent_strategy',
label: t(`plugin.categorySingle.${category}`),
}
}
return {
name: category,
label: t(`plugin.categorySingle.${category}`),
}
})
const categoriesMap = categories.reduce((acc, category) => {
acc[category.name] = category
return acc
}, {} as Record<string, Category>)
return {
categories,
categoriesMap,
}
}

View File

@ -3,7 +3,7 @@ import { useProviderContext } from '@/context/provider-context'
import { useInvalidateInstalledPluginList } from '@/service/use-plugins'
import { useInvalidateAllBuiltInTools, useInvalidateAllToolProviders } from '@/service/use-tools'
import { useInvalidateStrategyProviders } from '@/service/use-strategy'
import type { Plugin, PluginManifestInMarket } from '../../types'
import type { Plugin, PluginDeclaration, PluginManifestInMarket } from '../../types'
import { PluginType } from '../../types'
const useRefreshPluginList = () => {
@ -16,25 +16,27 @@ const useRefreshPluginList = () => {
const invalidateStrategyProviders = useInvalidateStrategyProviders()
return {
refreshPluginList: (manifest: PluginManifestInMarket | Plugin) => {
refreshPluginList: (manifest?: PluginManifestInMarket | Plugin | PluginDeclaration | null, refreshAllType?: boolean) => {
// installed list
invalidateInstalledPluginList()
if (!manifest) return
// tool page, tool select
if (PluginType.tool.includes(manifest.category)) {
if (PluginType.tool.includes(manifest.category) || refreshAllType) {
invalidateAllToolProviders()
invalidateAllBuiltInTools()
// TODO: update suggested tools. It's a function in hook useMarketplacePlugins,handleUpdatePlugins
}
// model select
if (PluginType.model.includes(manifest.category)) {
if (PluginType.model.includes(manifest.category) || refreshAllType) {
updateModelProviders()
refreshModelProviders()
}
// agent select
if (PluginType.agent.includes(manifest.category))
if (PluginType.agent.includes(manifest.category) || refreshAllType)
invalidateStrategyProviders()
},
}

View File

@ -7,7 +7,7 @@ import { RiLoader2Line } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import InstallMulti from './install-multi'
import { useInstallOrUpdate } from '@/service/use-plugins'
import { useInvalidateInstalledPluginList } from '@/service/use-plugins'
import useRefreshPluginList from '../../hooks/use-refresh-plugin-list'
const i18nPrefix = 'plugin.installModal'
type Props = {
@ -29,7 +29,7 @@ const Install: FC<Props> = ({
const [selectedPlugins, setSelectedPlugins] = React.useState<Plugin[]>([])
const [selectedIndexes, setSelectedIndexes] = React.useState<number[]>([])
const selectedPluginsNum = selectedPlugins.length
const invalidateInstalledPluginList = useInvalidateInstalledPluginList()
const { refreshPluginList } = useRefreshPluginList()
const handleSelect = (plugin: Plugin, selectedIndex: number) => {
const isSelected = !!selectedPlugins.find(p => p.plugin_id === plugin.plugin_id)
let nextSelectedPlugins
@ -61,7 +61,7 @@ const Install: FC<Props> = ({
}))
const hasInstallSuccess = res.some(r => r.success)
if (hasInstallSuccess)
invalidateInstalledPluginList()
refreshPluginList(undefined, true)
},
})
const handleInstall = () => {

View File

@ -7,7 +7,7 @@ import type { InstallState } from '@/app/components/plugins/types'
import { useGitHubReleases } from '../hooks'
import { convertRepoToUrl, parseGitHubUrl } from '../utils'
import type { PluginDeclaration, UpdateFromGitHubPayload } from '../../types'
import { InstallStepFromGitHub, PluginType } from '../../types'
import { InstallStepFromGitHub } from '../../types'
import Toast from '@/app/components/base/toast'
import SetURL from './steps/setURL'
import SelectPackage from './steps/selectPackage'
@ -15,8 +15,7 @@ import Installed from '../base/installed'
import Loaded from './steps/loaded'
import useGetIcon from '@/app/components/plugins/install-plugin/base/use-get-icon'
import { useTranslation } from 'react-i18next'
import { useUpdateModelProviders } from '@/app/components/header/account-setting/model-provider-page/hooks'
import { useInvalidateAllToolProviders } from '@/service/use-tools'
import useRefreshPluginList from '../hooks/use-refresh-plugin-list'
const i18nPrefix = 'plugin.installFromGitHub'
@ -30,8 +29,8 @@ const InstallFromGitHub: React.FC<InstallFromGitHubProps> = ({ updatePayload, on
const { t } = useTranslation()
const { getIconUrl } = useGetIcon()
const { fetchReleases } = useGitHubReleases()
const updateModelProviders = useUpdateModelProviders()
const invalidateAllToolProviders = useInvalidateAllToolProviders()
const { refreshPluginList } = useRefreshPluginList()
const [state, setState] = useState<InstallState>({
step: updatePayload ? InstallStepFromGitHub.selectPackage : InstallStepFromGitHub.setUrl,
repoUrl: updatePayload?.originalPackageInfo?.repo
@ -115,14 +114,9 @@ const InstallFromGitHub: React.FC<InstallFromGitHubProps> = ({ updatePayload, on
const handleInstalled = useCallback(() => {
setState(prevState => ({ ...prevState, step: InstallStepFromGitHub.installed }))
if (!manifest)
return
if (PluginType.model.includes(manifest.category))
updateModelProviders()
if (PluginType.tool.includes(manifest.category))
invalidateAllToolProviders()
refreshPluginList(manifest)
onSuccess()
}, [invalidateAllToolProviders, manifest, onSuccess, updateModelProviders])
}, [manifest, onSuccess, refreshPluginList])
const handleFailed = useCallback((errorMsg?: string) => {
setState(prevState => ({ ...prevState, step: InstallStepFromGitHub.installFailed }))

View File

@ -2,12 +2,11 @@
import type { FC } from 'react'
import React, { useCallback } from 'react'
import type { PluginDeclaration } from '../../types'
import { InstallStep, PluginType } from '../../types'
import { InstallStep } from '../../types'
import Install from './steps/install'
import Installed from '../base/installed'
import { useInvalidateInstalledPluginList } from '@/service/use-plugins'
import { useUpdateModelProviders } from '@/app/components/header/account-setting/model-provider-page/hooks'
import { useInvalidateAllToolProviders } from '@/service/use-tools'
import useRefreshPluginList from '../hooks/use-refresh-plugin-list'
type Props = {
step: InstallStep
onStepChange: (step: InstallStep) => void,
@ -27,20 +26,12 @@ const ReadyToInstall: FC<Props> = ({
errorMsg,
onError,
}) => {
const invalidateInstalledPluginList = useInvalidateInstalledPluginList()
const updateModelProviders = useUpdateModelProviders()
const invalidateAllToolProviders = useInvalidateAllToolProviders()
const { refreshPluginList } = useRefreshPluginList()
const handleInstalled = useCallback(() => {
onStepChange(InstallStep.installed)
invalidateInstalledPluginList()
if (!manifest)
return
if (PluginType.model.includes(manifest.category))
updateModelProviders()
if (PluginType.tool.includes(manifest.category))
invalidateAllToolProviders()
}, [invalidateAllToolProviders, invalidateInstalledPluginList, manifest, onStepChange, updateModelProviders])
refreshPluginList(manifest)
}, [manifest, onStepChange, refreshPluginList])
const handleFailed = useCallback((errorMsg?: string) => {
onStepChange(InstallStep.installFailed)

View File

@ -9,7 +9,6 @@ import AgentStrategyList from './agent-strategy-list'
import Drawer from '@/app/components/base/drawer'
import type { PluginDetail } from '@/app/components/plugins/types'
import cn from '@/utils/classnames'
import ToolSelector from '@/app/components/plugins/plugin-detail-panel/tool-selector'
type Props = {
detail?: PluginDetail
@ -28,16 +27,6 @@ const PluginDetailPanel: FC<Props> = ({
onUpdate()
}
const [value, setValue] = React.useState({
provider_name: 'langgenius/google/google',
tool_name: 'google_search',
})
const testHandle = (item: any) => {
console.log(item)
setValue(item)
}
if (!detail)
return null
@ -63,17 +52,6 @@ const PluginDetailPanel: FC<Props> = ({
{!!detail.declaration.agent_strategy && <AgentStrategyList detail={detail} />}
{!!detail.declaration.endpoint && <EndpointList detail={detail} />}
{!!detail.declaration.model && <ModelList detail={detail} />}
{false && (
<div className='px-4'>
<ToolSelector
scope={'all'}
value={value}
onSelect={item => testHandle(item)}
onDelete={() => testHandle(null)}
supportEnableSwitch
/>
</div>
)}
</div>
</>
)}

View File

@ -102,7 +102,7 @@ const ToolSelector: FC<Props> = ({
const currentProvider = useMemo(() => {
const mergedTools = [...(buildInTools || []), ...(customTools || []), ...(workflowTools || [])]
return mergedTools.find((toolWithProvider) => {
return toolWithProvider.id === value?.provider_name && toolWithProvider.tools.some(tool => tool.name === value?.tool_name)
return toolWithProvider.id === value?.provider_name
})
}, [value, buildInTools, customTools, workflowTools])
@ -172,7 +172,9 @@ const ToolSelector: FC<Props> = ({
})
// install from marketplace
const currentTool = useMemo(() => {
return currentProvider?.tools.find(tool => tool.name === value?.tool_name)
}, [currentProvider?.tools, value?.tool_name])
const manifestIcon = useMemo(() => {
if (!manifest)
return ''
@ -193,7 +195,10 @@ const ToolSelector: FC<Props> = ({
>
<PortalToFollowElemTrigger
className='w-full'
onClick={handleTriggerClick}
onClick={() => {
if (!currentProvider || !currentTool) return
handleTriggerClick()
}}
>
{trigger}
{!trigger && !value?.provider_name && (
@ -214,19 +219,22 @@ const ToolSelector: FC<Props> = ({
switchValue={value.enabled}
onSwitchChange={handleEnabledChange}
onDelete={onDelete}
noAuth={currentProvider && !currentProvider.is_team_authorization}
noAuth={currentProvider && currentTool && !currentProvider.is_team_authorization}
onAuth={() => setShowSettingAuth(true)}
uninstalled={!currentProvider && inMarketPlace}
versionMismatch={currentProvider && inMarketPlace && !currentTool}
installInfo={manifest?.latest_package_identifier}
onInstall={() => handleInstall()}
isError={!currentProvider && !inMarketPlace}
errorTip={<div className='space-y-1 max-w-[240px] text-xs'>
<h3 className='text-text-primary font-semibold'>{t('plugin.detailPanel.toolSelector.uninstalledTitle')}</h3>
<p className='text-text-secondary tracking-tight'>{t('plugin.detailPanel.toolSelector.uninstalledContent')}</p>
<p>
<Link href={'/plugins'} className='text-text-accent tracking-tight'>{t('plugin.detailPanel.toolSelector.uninstalledLink')}</Link>
</p>
</div>}
isError={(!currentProvider || !currentTool) && !inMarketPlace}
errorTip={
<div className='space-y-1 max-w-[240px] text-xs'>
<h3 className='text-text-primary font-semibold'>{currentTool ? t('plugin.detailPanel.toolSelector.uninstalledTitle') : t('plugin.detailPanel.toolSelector.unsupportedTitle')}</h3>
<p className='text-text-secondary tracking-tight'>{currentTool ? t('plugin.detailPanel.toolSelector.uninstalledContent') : t('plugin.detailPanel.toolSelector.unsupportedContent')}</p>
<p>
<Link href={'/plugins'} className='text-text-accent tracking-tight'>{t('plugin.detailPanel.toolSelector.uninstalledLink')}</Link>
</p>
</div>
}
/>
)}
</PortalToFollowElemTrigger>

View File

@ -13,7 +13,9 @@ import Button from '@/app/components/base/button'
import Indicator from '@/app/components/header/indicator'
import ActionButton from '@/app/components/base/action-button'
import Tooltip from '@/app/components/base/tooltip'
import { ToolTipContent } from '@/app/components/base/tooltip/content'
import { InstallPluginButton } from '@/app/components/workflow/nodes/_base/components/install-plugin-button'
import { SwitchPluginVersion } from '@/app/components/workflow/nodes/_base/components/switch-plugin-version'
import cn from '@/utils/classnames'
type Props = {
@ -31,6 +33,7 @@ type Props = {
uninstalled?: boolean
installInfo?: string
onInstall?: () => void
versionMismatch?: boolean
open: boolean
}
@ -50,10 +53,11 @@ const ToolItem = ({
onInstall,
isError,
errorTip,
versionMismatch,
}: Props) => {
const { t } = useTranslation()
const providerNameText = providerName?.split('/').pop()
const isTransparent = uninstalled || isError
const isTransparent = uninstalled || versionMismatch || isError
const [isDeleting, setIsDeleting] = useState(false)
return (
@ -82,7 +86,7 @@ const ToolItem = ({
<div className='text-text-secondary system-xs-medium'>{toolName}</div>
</div>
<div className='hidden group-hover:flex items-center gap-1'>
{!noAuth && !isError && !uninstalled && (
{!noAuth && !isError && !uninstalled && !versionMismatch && (
<ActionButton>
<RiEqualizer2Line className='w-4 h-4' />
</ActionButton>
@ -99,7 +103,7 @@ const ToolItem = ({
<RiDeleteBinLine className='w-4 h-4' />
</div>
</div>
{!isError && !uninstalled && !noAuth && showSwitch && (
{!isError && !uninstalled && !noAuth && !versionMismatch && showSwitch && (
<div className='mr-1' onClick={e => e.stopPropagation()}>
<Switch
size='md'
@ -108,12 +112,30 @@ const ToolItem = ({
/>
</div>
)}
{!isError && !uninstalled && noAuth && (
{!isError && !uninstalled && !versionMismatch && noAuth && (
<Button variant='secondary' size='small' onClick={onAuth}>
{t('tools.notAuthorized')}
<Indicator className='ml-2' color='orange' />
</Button>
)}
{!isError && !uninstalled && versionMismatch && installInfo && (
<div onClick={e => e.stopPropagation()}>
<SwitchPluginVersion
className='-mt-1'
uniqueIdentifier={installInfo}
tooltip={
<ToolTipContent
title={t('plugin.detailPanel.toolSelector.unsupportedTitle')}
>
{`${t('plugin.detailPanel.toolSelector.unsupportedContent')} ${t('plugin.detailPanel.toolSelector.unsupportedContent2')}`}
</ToolTipContent>
}
onChange={() => {
onInstall?.()
}}
/>
</div>
)}
{!isError && uninstalled && installInfo && (
<InstallPluginButton
onClick={e => e.stopPropagation()}

View File

@ -22,7 +22,7 @@ import cn from '@/utils/classnames'
import { API_PREFIX, MARKETPLACE_URL_PREFIX } from '@/config'
import { useInvalidateInstalledPluginList } from '@/service/use-plugins'
import { useInvalidateAllBuiltInTools, useInvalidateAllToolProviders } from '@/service/use-tools'
import { useCategories } from '../hooks'
import { useSingleCategories } from '../hooks'
import { useProviderContext } from '@/context/provider-context'
import { useRenderI18nObject } from '@/hooks/use-i18n'
@ -36,7 +36,7 @@ const PluginItem: FC<Props> = ({
plugin,
}) => {
const { t } = useTranslation()
const { categoriesMap } = useCategories()
const { categoriesMap } = useSingleCategories()
const currentPluginID = usePluginPageContext(v => v.currentPluginID)
const setCurrentPluginID = usePluginPageContext(v => v.setCurrentPluginID)
const invalidateInstalledPluginList = useInvalidateInstalledPluginList()

View File

@ -0,0 +1,71 @@
import type { FC, ReactNode } from 'react'
import React, { memo } from 'react'
import Card from '@/app/components/plugins/card'
import Modal from '@/app/components/base/modal'
import Button from '@/app/components/base/button'
import type { Plugin } from '../types'
import type { UseMutationResult } from '@tanstack/react-query'
type Props = {
plugin: Plugin
onSave: () => void
onCancel: () => void
mutation: UseMutationResult
confirmButtonText: ReactNode
cancelButtonText: ReactNode
modelTitle: ReactNode
description: ReactNode
cardTitleLeft: ReactNode
}
const PluginMutationModal: FC<Props> = ({
plugin,
onCancel,
mutation,
confirmButtonText,
cancelButtonText,
modelTitle,
description,
cardTitleLeft,
}: Props) => {
return (
<Modal
isShow={true}
onClose={onCancel}
className='min-w-[560px]'
closable
title={modelTitle}
>
<div className='mt-3 mb-2 text-text-secondary system-md-regular'>
{description}
</div>
<div className='flex p-2 items-start content-start gap-1 self-stretch flex-wrap rounded-2xl bg-background-section-burn'>
<Card
installed={mutation.isSuccess}
payload={plugin}
className='w-full'
titleLeft={cardTitleLeft}
/>
</div>
<div className='flex pt-5 justify-end items-center gap-2 self-stretch'>
{mutation.isPending && (
<Button onClick={onCancel}>
{cancelButtonText}
</Button>
)}
<Button
variant='primary'
loading={mutation.isPending}
onClick={mutation.mutate}
disabled={mutation.isPending}
>
{confirmButtonText}
</Button>
</div>
</Modal>
)
}
PluginMutationModal.displayName = 'PluginMutationModal'
export default memo(PluginMutationModal)

View File

@ -8,7 +8,7 @@ import { usePluginPageContext } from '../context'
import { Group } from '@/app/components/base/icons/src/vender/other'
import { useSelector as useAppContextSelector } from '@/context/app-context'
import Line from '../../marketplace/empty/line'
import { useInstalledPluginList, useInvalidateInstalledPluginList } from '@/service/use-plugins'
import { useInstalledPluginList } from '@/service/use-plugins'
import { useTranslation } from 'react-i18next'
import { SUPPORT_INSTALL_LOCAL_FILE_EXTENSIONS } from '@/config'
@ -29,14 +29,13 @@ const Empty = () => {
}
const filters = usePluginPageContext(v => v.filters)
const { data: pluginList } = useInstalledPluginList()
const invalidateInstalledPluginList = useInvalidateInstalledPluginList()
const text = useMemo(() => {
if (pluginList?.plugins.length === 0)
return t('plugin.list.noInstalled')
if (filters.categories.length > 0 || filters.tags.length > 0 || filters.searchQuery)
return t('plugin.list.notFound')
}, [pluginList, filters])
}, [pluginList?.plugins.length, t, filters.categories.length, filters.tags.length, filters.searchQuery])
return (
<div className='grow w-full relative z-0'>
@ -100,7 +99,7 @@ const Empty = () => {
</div>
</div>
{selectedAction === 'github' && <InstallFromGitHub
onSuccess={() => { invalidateInstalledPluginList() }}
onSuccess={() => { }}
onClose={() => setSelectedAction(null)}
/>}
{selectedAction === 'local' && selectedFile

View File

@ -15,7 +15,6 @@ import {
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import { useSelector as useAppContextSelector } from '@/context/app-context'
import { useInvalidateInstalledPluginList } from '@/service/use-plugins'
import { useTranslation } from 'react-i18next'
import { SUPPORT_INSTALL_LOCAL_FILE_EXTENSIONS } from '@/config'
@ -31,7 +30,6 @@ const InstallPluginDropdown = ({
const [selectedAction, setSelectedAction] = useState<string | null>(null)
const [selectedFile, setSelectedFile] = useState<File | null>(null)
const { enable_marketplace } = useAppContextSelector(s => s.systemFeatures)
const invalidateInstalledPluginList = useInvalidateInstalledPluginList()
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0]
@ -120,7 +118,7 @@ const InstallPluginDropdown = ({
</PortalToFollowElemContent>
</div>
{selectedAction === 'github' && <InstallFromGitHub
onSuccess={() => { invalidateInstalledPluginList() }}
onSuccess={() => { }}
onClose={() => setSelectedAction(null)}
/>}
{selectedAction === 'local' && selectedFile

View File

@ -21,6 +21,7 @@ import CardIcon from '@/app/components/plugins/card/base/card-icon'
import cn from '@/utils/classnames'
import { useGetLanguage } from '@/context/i18n'
import useGetIcon from '@/app/components/plugins/install-plugin/base/use-get-icon'
import DownloadingIcon from '@/app/components/header/plugins-nav/downloading-icon'
const PluginTasks = () => {
const { t } = useTranslation()
@ -87,12 +88,21 @@ const PluginTasks = () => {
(isInstallingWithError || isFailed) && 'border-components-button-destructive-secondary-border-hover bg-state-destructive-hover hover:bg-state-destructive-hover-alt cursor-pointer',
)}
>
<RiInstallLine
className={cn(
'w-4 h-4 text-components-button-secondary-text',
(isInstallingWithError || isFailed) && 'text-components-button-destructive-secondary-text',
)}
/>
{
(isInstalling || isInstallingWithError) && (
<DownloadingIcon />
)
}
{
!(isInstalling || isInstallingWithError) && (
<RiInstallLine
className={cn(
'w-4 h-4 text-components-button-secondary-text',
(isInstallingWithError || isFailed) && 'text-components-button-destructive-secondary-text',
)}
/>
)
}
<div className='absolute -right-1 -top-1'>
{
(isInstalling || isInstallingWithSuccess) && (

View File

@ -111,6 +111,7 @@ export type PluginDetail = {
export type PluginInfoFromMarketPlace = {
category: PluginType
latest_package_identifier: string
latest_version: string
}
export type Plugin = {