Merge branch 'feat/plugins' of github.com:langgenius/dify into feat/plugins

This commit is contained in:
Yi 2024-11-11 17:21:37 +08:00
commit dbc10425c8
42 changed files with 894 additions and 669 deletions

View File

@ -15,20 +15,20 @@ const Layout = ({ children }: { children: ReactNode }) => {
<>
<GA gaType={GaType.admin} />
<SwrInitor>
<AppContextProvider>
<EventEmitterContextProvider>
<ProviderContextProvider>
<ModalContextProvider>
<HeaderWrapper>
<Header />
</HeaderWrapper>
<TanstackQueryIniter>
<TanstackQueryIniter>
<AppContextProvider>
<EventEmitterContextProvider>
<ProviderContextProvider>
<ModalContextProvider>
<HeaderWrapper>
<Header />
</HeaderWrapper>
{children}
</TanstackQueryIniter>
</ModalContextProvider>
</ProviderContextProvider>
</EventEmitterContextProvider>
</AppContextProvider>
</ModalContextProvider>
</ProviderContextProvider>
</EventEmitterContextProvider>
</AppContextProvider>
</TanstackQueryIniter>
</SwrInitor>
</>
)

View File

@ -222,7 +222,7 @@ const ModelProviderPage = ({ searchText }: Props) => {
{!collapse && !isPluginsLoading && (
<div className='grid grid-cols-2 gap-2'>
{plugins.map(plugin => (
<ProviderCard key={plugin.plugin_id} payload={plugin} />
<ProviderCard key={plugin.plugin_id} payload={plugin} onSuccess={updateModelProviders} />
))}
</div>
)}

View File

@ -50,15 +50,15 @@ const ModelList: FC<ModelListProps> = ({
return (
<div className='px-2 pb-2 rounded-b-xl'>
<div className='py-1 bg-white rounded-lg'>
<div className='py-1 bg-components-panel-bg rounded-lg'>
<div className='flex items-center pl-1 pr-[3px]'>
<span className='group shrink-0 flex items-center mr-2'>
<span className='group-hover:hidden inline-flex pl-1 pr-1.5 h-6 leading-6 text-xs font-medium text-gray-500'>
<span className='group-hover:hidden inline-flex items-center pl-1 pr-1.5 h-6 system-xs-medium text-text-tertiary'>
{t('common.modelProvider.modelsNum', { num: models.length })}
<RiArrowRightSLine className='mr-0.5 w-4 h-4 rotate-90' />
</span>
<span
className='hidden group-hover:inline-flex items-center pl-1 pr-1.5 h-6 text-xs font-medium text-gray-500 bg-gray-50 cursor-pointer rounded-lg'
className='hidden group-hover:inline-flex items-center pl-1 pr-1.5 h-6 system-xs-medium text-text-tertiary bg-state-base-hover cursor-pointer rounded-lg'
onClick={() => onCollapse()}
>
{t('common.modelProvider.modelsNum', { num: models.length })}

View File

@ -8,7 +8,7 @@ import Button from '@/app/components/base/button'
import { Trans, useTranslation } from 'react-i18next'
import { RiLoader2Line } from '@remixicon/react'
import Badge, { BadgeState } from '@/app/components/base/badge/index'
import { installPackageFromLocal } from '@/service/plugins'
import { useInstallPackageFromLocal } from '@/service/use-plugins'
import checkTaskStatus from '../../base/check-task-status'
import { usePluginTasksStore } from '@/app/components/plugins/plugin-page/store'
@ -33,6 +33,8 @@ const Installed: FC<Props> = ({
}) => {
const { t } = useTranslation()
const [isInstalling, setIsInstalling] = React.useState(false)
const { mutateAsync: installPackageFromLocal } = useInstallPackageFromLocal()
const {
check,
stop,

View File

@ -9,7 +9,7 @@ import Button from '@/app/components/base/button'
import { useTranslation } from 'react-i18next'
import { RiLoader2Line } from '@remixicon/react'
import Badge, { BadgeState } from '@/app/components/base/badge/index'
import { installPackageFromMarketPlace } from '@/service/plugins'
import { useInstallPackageFromMarketPlace } from '@/service/use-plugins'
import checkTaskStatus from '../../base/check-task-status'
const i18nPrefix = 'plugin.installModal'
@ -32,6 +32,7 @@ const Installed: FC<Props> = ({
onFailed,
}) => {
const { t } = useTranslation()
const { mutateAsync: installPackageFromMarketPlace } = useInstallPackageFromMarketPlace()
const [isInstalling, setIsInstalling] = React.useState(false)
const {
check,

View File

@ -34,7 +34,7 @@ export type MarketplaceContextValue = {
activePluginType: string
handleActivePluginTypeChange: (type: string) => void
plugins?: Plugin[]
setPlugins: (plugins: Plugin[]) => void
resetPlugins: () => void
sort: PluginsSort
handleSortChange: (sort: PluginsSort) => void
marketplaceCollectionsFromClient?: MarketplaceCollection[]
@ -53,7 +53,7 @@ export const MarketplaceContext = createContext<MarketplaceContextValue>({
activePluginType: PLUGIN_TYPE_SEARCH_MAP.all,
handleActivePluginTypeChange: () => {},
plugins: undefined,
setPlugins: () => {},
resetPlugins: () => {},
sort: DEFAULT_SORT,
handleSortChange: () => {},
marketplaceCollectionsFromClient: [],
@ -91,7 +91,7 @@ export const MarketplaceContextProvider = ({
} = useMarketplaceCollectionsAndPlugins()
const {
plugins,
setPlugins,
resetPlugins,
queryPlugins,
queryPluginsWithDebounced,
} = useMarketplacePlugins()
@ -104,7 +104,7 @@ export const MarketplaceContextProvider = ({
queryMarketplaceCollectionsAndPlugins({
category: activePluginTypeRef.current === PLUGIN_TYPE_SEARCH_MAP.all ? undefined : activePluginTypeRef.current,
})
setPlugins(undefined)
resetPlugins()
return
}
@ -116,7 +116,7 @@ export const MarketplaceContextProvider = ({
sortBy: sortRef.current.sortBy,
sortOrder: sortRef.current.sortOrder,
})
}, [queryPluginsWithDebounced, queryMarketplaceCollectionsAndPlugins, setPlugins])
}, [queryPluginsWithDebounced, queryMarketplaceCollectionsAndPlugins, resetPlugins])
const handleFilterPluginTagsChange = useCallback((tags: string[]) => {
setFilterPluginTags(tags)
@ -126,7 +126,7 @@ export const MarketplaceContextProvider = ({
queryMarketplaceCollectionsAndPlugins({
category: activePluginTypeRef.current === PLUGIN_TYPE_SEARCH_MAP.all ? undefined : activePluginTypeRef.current,
})
setPlugins(undefined)
resetPlugins()
return
}
@ -138,7 +138,7 @@ export const MarketplaceContextProvider = ({
sortBy: sortRef.current.sortBy,
sortOrder: sortRef.current.sortOrder,
})
}, [queryPlugins, setPlugins, queryMarketplaceCollectionsAndPlugins])
}, [queryPlugins, resetPlugins, queryMarketplaceCollectionsAndPlugins])
const handleActivePluginTypeChange = useCallback((type: string) => {
setActivePluginType(type)
@ -148,7 +148,7 @@ export const MarketplaceContextProvider = ({
queryMarketplaceCollectionsAndPlugins({
category: type === PLUGIN_TYPE_SEARCH_MAP.all ? undefined : type,
})
setPlugins(undefined)
resetPlugins()
return
}
@ -160,7 +160,7 @@ export const MarketplaceContextProvider = ({
sortBy: sortRef.current.sortBy,
sortOrder: sortRef.current.sortOrder,
})
}, [queryPlugins, setPlugins, queryMarketplaceCollectionsAndPlugins])
}, [queryPlugins, resetPlugins, queryMarketplaceCollectionsAndPlugins])
const handleSortChange = useCallback((sort: PluginsSort) => {
setSort(sort)
@ -187,7 +187,7 @@ export const MarketplaceContextProvider = ({
activePluginType,
handleActivePluginTypeChange,
plugins,
setPlugins,
resetPlugins,
sort,
handleSortChange,
marketplaceCollectionsFromClient,

View File

@ -15,10 +15,10 @@ const Description = async ({
return (
<>
<h1 className='shrink-0 mb-2 text-center title-4xl-semi-bold text-text-primary'>
Empower your AI development
{t('marketplace.empower')}
</h1>
<h2 className='shrink-0 flex justify-center items-center text-center body-md-regular text-text-tertiary'>
Discover
{t('marketplace.discover')}
<span className="relative ml-1 body-md-medium text-text-secondary after:content-[''] after:absolute after:left-0 after:bottom-[1.5px] after:w-full after:h-2 after:bg-text-text-selected">
{t('category.models')}
</span>
@ -30,11 +30,11 @@ const Description = async ({
<span className="relative ml-1 mr-1 body-md-medium text-text-secondary after:content-[''] after:absolute after:left-0 after:bottom-[1.5px] after:w-full after:h-2 after:bg-text-text-selected">
{t('category.extensions')}
</span>
and
{t('marketplace.and')}
<span className="relative ml-1 mr-1 body-md-medium text-text-secondary after:content-[''] after:absolute after:left-0 after:bottom-[1.5px] after:w-full after:h-2 after:bg-text-text-selected">
{t('category.bundles')}
</span>
in Dify Marketplace
{t('marketplace.inDifyMarketplace')}
</h2>
</>
)

View File

@ -4,7 +4,9 @@ import {
} from 'react'
import { useTranslation } from 'react-i18next'
import { useDebounceFn } from 'ahooks'
import type { Plugin } from '../types'
import type {
Plugin,
} from '../types'
import type {
CollectionsAndPluginsSearchParams,
MarketplaceCollection,
@ -12,9 +14,9 @@ import type {
} from './types'
import {
getMarketplaceCollectionsAndPlugins,
getMarketplacePlugins,
} from './utils'
import i18n from '@/i18n/i18next-config'
import { useMutationPluginsFromMarketplace } from '@/service/use-plugins'
export const useMarketplaceCollectionsAndPlugins = () => {
const [isLoading, setIsLoading] = useState(false)
@ -41,28 +43,29 @@ export const useMarketplaceCollectionsAndPlugins = () => {
}
export const useMarketplacePlugins = () => {
const [isLoading, setIsLoading] = useState(false)
const [plugins, setPlugins] = useState<Plugin[]>()
const {
data,
mutate,
reset,
isPending,
} = useMutationPluginsFromMarketplace()
const queryPlugins = useCallback(async (query: PluginsSearchParams) => {
setIsLoading(true)
const { marketplacePlugins } = await getMarketplacePlugins(query)
setIsLoading(false)
const queryPlugins = useCallback((pluginsSearchParams: PluginsSearchParams) => {
mutate(pluginsSearchParams)
}, [mutate])
setPlugins(marketplacePlugins)
}, [])
const { run: queryPluginsWithDebounced } = useDebounceFn(queryPlugins, {
const { run: queryPluginsWithDebounced } = useDebounceFn((pluginsSearchParams) => {
mutate(pluginsSearchParams)
}, {
wait: 500,
})
return {
plugins,
setPlugins,
plugins: data?.data?.plugins,
resetPlugins: reset,
queryPlugins,
queryPluginsWithDebounced,
isLoading,
setIsLoading,
isLoading: isPending,
}
}

View File

@ -5,6 +5,7 @@ import SearchBoxWrapper from './search-box/search-box-wrapper'
import PluginTypeSwitch from './plugin-type-switch'
import ListWrapper from './list/list-wrapper'
import { getMarketplaceCollectionsAndPlugins } from './utils'
import { TanstackQueryIniter } from '@/context/query-client'
type MarketplaceProps = {
locale?: string
@ -17,18 +18,20 @@ const Marketplace = async ({
const { marketplaceCollections, marketplaceCollectionPluginsMap } = await getMarketplaceCollectionsAndPlugins()
return (
<MarketplaceContextProvider>
<Description locale={locale} />
<IntersectionLine />
<SearchBoxWrapper locale={locale} />
<PluginTypeSwitch locale={locale} />
<ListWrapper
locale={locale}
marketplaceCollections={marketplaceCollections}
marketplaceCollectionPluginsMap={marketplaceCollectionPluginsMap}
showInstallButton={showInstallButton}
/>
</MarketplaceContextProvider>
<TanstackQueryIniter>
<MarketplaceContextProvider>
<Description locale={locale} />
<IntersectionLine />
<SearchBoxWrapper locale={locale} />
<PluginTypeSwitch locale={locale} />
<ListWrapper
locale={locale}
marketplaceCollections={marketplaceCollections}
marketplaceCollectionPluginsMap={marketplaceCollectionPluginsMap}
showInstallButton={showInstallButton}
/>
</MarketplaceContextProvider>
</TanstackQueryIniter>
)
}

View File

@ -30,7 +30,7 @@ const PluginTypeSwitch = ({
const options = [
{
value: PLUGIN_TYPE_SEARCH_MAP.all,
text: 'All',
text: t('plugin.category.all'),
icon: null,
},
{

View File

@ -34,7 +34,7 @@ const PluginSettingModal: FC<Props> = ({
const handleSave = useCallback(async () => {
await onSave(tempPrivilege)
onHide()
}, [tempPrivilege])
}, [onHide, onSave, tempPrivilege])
return (
<Modal

View File

@ -1,5 +1,4 @@
import React, { useState } from 'react'
import useSWR from 'swr'
import { useTranslation } from 'react-i18next'
import { usePluginPageContext } from '@/app/components/plugins/plugin-page/context'
import { useAppContext } from '@/context/app-context'
@ -9,28 +8,39 @@ import Indicator from '@/app/components/header/indicator'
import ToolItem from '@/app/components/tools/provider/tool-item'
import ConfigCredential from '@/app/components/tools/setting/build-in/config-credentials'
import {
fetchBuiltInToolList,
fetchCollectionDetail,
removeBuiltInToolCredential,
updateBuiltInToolCredential,
} from '@/service/tools'
useBuiltinProviderInfo,
useBuiltinTools,
useInvalidateBuiltinProviderInfo,
useRemoveProviderCredentials,
useUpdateProviderCredentials,
} from '@/service/use-tools'
const ActionList = () => {
const { t } = useTranslation()
const { isCurrentWorkspaceManager } = useAppContext()
const currentPluginDetail = usePluginPageContext(v => v.currentPluginDetail)
const { data: provider } = useSWR(
`builtin/${currentPluginDetail.plugin_id}/${currentPluginDetail.name}`,
fetchCollectionDetail,
)
const { data } = useSWR(
`${currentPluginDetail.plugin_id}/${currentPluginDetail.name}`,
fetchBuiltInToolList,
)
const { data: provider } = useBuiltinProviderInfo(`${currentPluginDetail.plugin_id}/${currentPluginDetail.name}`)
const invalidateProviderInfo = useInvalidateBuiltinProviderInfo()
const { data } = useBuiltinTools(`${currentPluginDetail.plugin_id}/${currentPluginDetail.name}`)
const [showSettingAuth, setShowSettingAuth] = useState(false)
const handleCredentialSettingUpdate = () => {}
const handleCredentialSettingUpdate = () => {
invalidateProviderInfo(`${currentPluginDetail.plugin_id}/${currentPluginDetail.name}`)
Toast.notify({
type: 'success',
message: t('common.api.actionSuccess'),
})
setShowSettingAuth(false)
}
const { mutate: updatePermission } = useUpdateProviderCredentials({
onSuccess: handleCredentialSettingUpdate,
})
const { mutate: removePermission } = useRemoveProviderCredentials({
onSuccess: handleCredentialSettingUpdate,
})
if (!data || !provider)
return null
@ -77,24 +87,11 @@ const ActionList = () => {
<ConfigCredential
collection={provider}
onCancel={() => setShowSettingAuth(false)}
onSaved={async (value) => {
await updateBuiltInToolCredential(provider.name, value)
Toast.notify({
type: 'success',
message: t('common.api.actionSuccess'),
})
handleCredentialSettingUpdate()
setShowSettingAuth(false)
}}
onRemove={async () => {
await removeBuiltInToolCredential(provider.name)
Toast.notify({
type: 'success',
message: t('common.api.actionSuccess'),
})
handleCredentialSettingUpdate()
setShowSettingAuth(false)
}}
onSaved={async value => updatePermission({
providerName: provider.name,
credentials: value,
})}
onRemove={async () => removePermission(provider.name)}
/>
)}
</div>

View File

@ -13,6 +13,8 @@ import Description from '../card/base/description'
import Icon from '../card/base/card-icon'
import Title from '../card/base/title'
import OrgInfo from '../card/base/org-info'
import { useGitHubReleases } from '../install-plugin/hooks'
import { compareVersion, getLatestVersion } from '@/utils/semver'
import OperationDropdown from './operation-dropdown'
import PluginInfo from '@/app/components/plugins/plugin-page/plugin-info'
import ActionButton from '@/app/components/base/action-button'
@ -20,10 +22,13 @@ import Button from '@/app/components/base/button'
import Badge from '@/app/components/base/badge'
import Confirm from '@/app/components/base/confirm'
import Tooltip from '@/app/components/base/tooltip'
import Toast from '@/app/components/base/toast'
import { BoxSparkleFill } from '@/app/components/base/icons/src/vender/plugin'
import { Github } from '@/app/components/base/icons/src/public/common'
import { uninstallPlugin } from '@/service/plugins'
import { useGetLanguage } from '@/context/i18n'
import { useModalContext } from '@/context/modal-context'
import { API_PREFIX, MARKETPLACE_URL_PREFIX } from '@/config'
import cn from '@/utils/classnames'
@ -32,16 +37,18 @@ const i18nPrefix = 'plugin.action'
type Props = {
detail: PluginDetail
onHide: () => void
onDelete: () => void
onUpdate: () => void
}
const DetailHeader = ({
detail,
onHide,
onDelete,
onUpdate,
}: Props) => {
const { t } = useTranslation()
const locale = useGetLanguage()
const { fetchReleases } = useGitHubReleases()
const { setShowUpdatePluginModal } = useModalContext()
const {
installation_id,
@ -53,13 +60,51 @@ const DetailHeader = ({
} = detail
const { author, name, label, description, icon, verified } = detail.declaration
const isFromGitHub = source === PluginSource.github
// Only plugin installed from GitHub need to check if it's the new version
const hasNewVersion = useMemo(() => {
return source === PluginSource.github && latest_version !== version
}, [source, latest_version, version])
// #plugin TODO# update plugin
const handleUpdate = () => { }
const handleUpdate = async () => {
try {
const fetchedReleases = await fetchReleases(author, name)
if (fetchedReleases.length === 0)
return
const versions = fetchedReleases.map(release => release.tag_name)
const latestVersion = getLatestVersion(versions)
if (compareVersion(latestVersion, version) === 1) {
setShowUpdatePluginModal({
onSaveCallback: () => {
onUpdate()
},
payload: {
type: PluginSource.github,
github: {
originalPackageInfo: {
id: installation_id,
repo: meta!.repo,
version: meta!.version,
package: meta!.package,
releases: fetchedReleases,
},
},
},
})
}
else {
Toast.notify({
type: 'info',
message: 'No new version available',
})
}
}
catch {
Toast.notify({
type: 'error',
message: 'Failed to compare versions',
})
}
}
const [isShowPluginInfo, {
setTrue: showPluginInfo,
@ -82,9 +127,9 @@ const DetailHeader = ({
hideDeleting()
if (res.success) {
hideDeleteConfirm()
onDelete()
onUpdate()
}
}, [hideDeleteConfirm, hideDeleting, installation_id, showDeleting, onDelete])
}, [hideDeleteConfirm, hideDeleting, installation_id, showDeleting, onUpdate])
// #plugin TODO# used in apps
// const usedInApps = 3
@ -141,6 +186,7 @@ const DetailHeader = ({
</div>
<div className='flex gap-1'>
<OperationDropdown
source={detail.source}
onInfo={showPluginInfo}
onCheckVersion={handleUpdate}
onRemove={showDeleteConfirm}

View File

@ -4,62 +4,62 @@ import { useBoolean } from 'ahooks'
import { RiDeleteBinLine, RiEditLine, RiLoginCircleLine } from '@remixicon/react'
import type { EndpointListItem } from '../types'
import EndpointModal from './endpoint-modal'
import { NAME_FIELD } from './utils'
import { addDefaultValue, toolCredentialToFormSchemas } from '@/app/components/tools/utils/to-form-schema'
import ActionButton from '@/app/components/base/action-button'
import CopyBtn from '@/app/components/base/copy-btn'
import Confirm from '@/app/components/base/confirm'
import Indicator from '@/app/components/header/indicator'
import Switch from '@/app/components/base/switch'
import Toast from '@/app/components/base/toast'
import {
deleteEndpoint,
disableEndpoint,
enableEndpoint,
updateEndpoint,
} from '@/service/plugins'
useDeleteEndpoint,
useDisableEndpoint,
useEnableEndpoint,
useUpdateEndpoint,
} from '@/service/use-endpoints'
type Props = {
data: EndpointListItem
handleChange: () => void
}
const EndpointCard = ({
data,
handleChange,
}: Props) => {
const { t } = useTranslation()
const [active, setActive] = useState(data.enabled)
const endpointID = data.id
// switch
const [isShowDisableConfirm, {
setTrue: showDisableConfirm,
setFalse: hideDisableConfirm,
}] = useBoolean(false)
const activeEndpoint = async () => {
try {
await enableEndpoint({
url: '/workspaces/current/endpoints/enable',
endpointID,
})
}
catch (error) {
console.error(error)
const { mutate: enableEndpoint } = useEnableEndpoint({
onSuccess: async () => {
await handleChange()
},
onError: () => {
Toast.notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') })
setActive(false)
}
}
const inactiveEndpoint = async () => {
try {
await disableEndpoint({
url: '/workspaces/current/endpoints/disable',
endpointID,
})
}
catch (error) {
console.error(error)
setActive(true)
}
}
},
})
const { mutate: disableEndpoint } = useDisableEndpoint({
onSuccess: async () => {
await handleChange()
hideDisableConfirm()
},
onError: () => {
Toast.notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') })
setActive(false)
},
})
const handleSwitch = (state: boolean) => {
if (state) {
setActive(true)
activeEndpoint()
enableEndpoint(endpointID)
}
else {
setActive(false)
@ -67,49 +67,49 @@ const EndpointCard = ({
}
}
// delete
const [isShowDeleteConfirm, {
setTrue: showDeleteConfirm,
setFalse: hideDeleteConfirm,
}] = useBoolean(false)
const handleDelete = async () => {
try {
await deleteEndpoint({
url: '/workspaces/current/endpoints/delete',
endpointID,
})
}
catch (error) {
console.error(error)
}
}
const { mutate: deleteEndpoint } = useDeleteEndpoint({
onSuccess: async () => {
await handleChange()
hideDeleteConfirm()
},
onError: () => {
Toast.notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') })
},
})
// update
const [isShowEndpointModal, {
setTrue: showEndpointModalConfirm,
setFalse: hideEndpointModalConfirm,
}] = useBoolean(false)
const formSchemas = useMemo(() => {
return toolCredentialToFormSchemas(data.declaration.settings)
return toolCredentialToFormSchemas([NAME_FIELD, ...data.declaration.settings])
}, [data.declaration.settings])
const formValue = useMemo(() => {
return addDefaultValue(data.settings, formSchemas)
}, [data.settings, formSchemas])
const handleUpdate = (state: any) => {
try {
updateEndpoint({
url: '/workspaces/current/endpoints',
body: {
endpoint_id: data.id,
settings: state,
name: state.name,
},
})
const formValue = {
name: data.name,
...data.settings,
}
catch (error) {
console.error(error)
}
}
return addDefaultValue(formValue, formSchemas)
}, [data.name, data.settings, formSchemas])
const { mutate: updateEndpoint } = useUpdateEndpoint({
onSuccess: async () => {
await handleChange()
hideEndpointModalConfirm()
},
onError: () => {
Toast.notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') })
},
})
const handleUpdate = (state: any) => updateEndpoint({
endpointID,
state,
})
return (
<div className='p-0.5 bg-background-section-burn rounded-xl'>
@ -171,7 +171,7 @@ const EndpointCard = ({
hideDisableConfirm()
setActive(true)
}}
onConfirm={inactiveEndpoint}
onConfirm={() => disableEndpoint(endpointID)}
/>
)}
{isShowDeleteConfirm && (
@ -180,7 +180,7 @@ const EndpointCard = ({
title={t('plugin.detailPanel.endpointDeleteTip')}
content={<div>{t('plugin.detailPanel.endpointDeleteContent', { name: data.name })}</div>}
onCancel={hideDeleteConfirm}
onConfirm={handleDelete}
onConfirm={() => deleteEndpoint(endpointID)}
/>
)}
{isShowEndpointModal && (

View File

@ -1,65 +1,62 @@
import React, { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import useSWR from 'swr'
import { useBoolean } from 'ahooks'
import { RiAddLine } from '@remixicon/react'
import EndpointModal from './endpoint-modal'
import EndpointCard from './endpoint-card'
import { NAME_FIELD } from './utils'
import { toolCredentialToFormSchemas } from '@/app/components/tools/utils/to-form-schema'
import ActionButton from '@/app/components/base/action-button'
import Tooltip from '@/app/components/base/tooltip'
import Toast from '@/app/components/base/toast'
import { usePluginPageContext } from '@/app/components/plugins/plugin-page/context'
import {
createEndpoint,
fetchEndpointList,
} from '@/service/plugins'
useCreateEndpoint,
useEndpointList,
useInvalidateEndpointList,
} from '@/service/use-endpoints'
import cn from '@/utils/classnames'
const EndpointList = () => {
type Props = {
showTopBorder?: boolean
}
const EndpointList = ({ showTopBorder }: Props) => {
const { t } = useTranslation()
const pluginDetail = usePluginPageContext(v => v.currentPluginDetail)
const pluginUniqueID = pluginDetail.plugin_unique_identifier
const declaration = pluginDetail.declaration.endpoint
const { data } = useSWR(
{
url: '/workspaces/current/endpoints/list/plugin',
params: {
plugin_id: pluginDetail.plugin_id,
page: 1,
page_size: 100,
},
},
fetchEndpointList,
)
const { data } = useEndpointList(pluginDetail.plugin_id)
const invalidateEndpointList = useInvalidateEndpointList()
const [isShowEndpointModal, {
setTrue: showEndpointModal,
setFalse: hideEndpointModal,
}] = useBoolean(false)
const formSchemas = useMemo(() => {
return toolCredentialToFormSchemas(declaration.settings)
return toolCredentialToFormSchemas([NAME_FIELD, ...declaration.settings])
}, [declaration.settings])
const handleCreate = (state: any) => {
try {
createEndpoint({
url: '/workspaces/current/endpoints',
body: {
plugin_unique_identifier: pluginUniqueID,
settings: state,
name: state.name,
},
})
}
catch (error) {
console.error(error)
}
}
const { mutate: createEndpoint } = useCreateEndpoint({
onSuccess: async () => {
await invalidateEndpointList(pluginDetail.plugin_id)
hideEndpointModal()
},
onError: () => {
Toast.notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') })
},
})
const handleCreate = (state: any) => createEndpoint({
pluginUniqueID,
state,
})
if (!data)
return null
return (
<div className='px-4 py-2 border-t border-divider-subtle'>
<div className={cn('px-4 py-2 border-divider-subtle', showTopBorder && 'border-t')}>
<div className='mb-1 h-6 flex items-center justify-between text-text-secondary system-sm-semibold-uppercase'>
<div className='flex items-center gap-0.5'>
{t('plugin.detailPanel.endpoints')}
@ -81,6 +78,7 @@ const EndpointList = () => {
<EndpointCard
key={index}
data={item}
handleChange={() => invalidateEndpointList(pluginDetail.plugin_id)}
/>
))}
</div>

View File

@ -10,11 +10,11 @@ import { usePluginPageContext } from '@/app/components/plugins/plugin-page/conte
import cn from '@/utils/classnames'
type Props = {
onDelete: () => void
onUpdate: () => void
}
const PluginDetailPanel: FC<Props> = ({
onDelete,
onUpdate,
}) => {
const pluginDetail = usePluginPageContext(v => v.currentPluginDetail)
const setCurrentPluginDetail = usePluginPageContext(v => v.setCurrentPluginDetail)
@ -39,11 +39,11 @@ const PluginDetailPanel: FC<Props> = ({
<DetailHeader
detail={pluginDetail}
onHide={handleHide}
onDelete={onDelete}
onUpdate={onUpdate}
/>
<div className='grow overflow-y-auto'>
{!!pluginDetail.declaration.endpoint && <EndpointList />}
{!!pluginDetail.declaration.tool && <ActionList />}
{!!pluginDetail.declaration.endpoint && <EndpointList showTopBorder={!!pluginDetail.declaration.tool} />}
{!!pluginDetail.declaration.model && <ModelList />}
</div>
</>

View File

@ -1,105 +0,0 @@
import { PluginSource, PluginType } from '../types'
export const toolNotion = {
id: 'dlfajkgjdga-dfjalksjfglkds-dfjakld',
created_at: '2024-10-16 16:05:33',
updated_at: '2024-10-16 16:05:33',
name: 'notion page search',
plugin_id: 'Notion/notion-page-search',
plugin_unique_identifier: 'Notion/notion-page-search:1.2.0@fldsjflkdsajfldsakajfkls',
declaration: {
version: '1.2.0',
author: 'Notion',
name: 'notion page search',
category: PluginType.tool,
icon: 'https://via.placeholder.com/150',
label: {
'en-US': 'Notion Page Search',
'zh-Hans': 'Notion 页面搜索',
},
description: {
'en-US': 'Description: Search Notion pages and open visited ones faster. No admin access required.More and more info...More and more info...More and more info...',
'zh-Hans': '搜索 Notion 页面并更快地打开已访问的页面。无需管理员访问权限。More and more info...More and more info...More and more info...',
},
created_at: '2024-10-16 16:05:33',
resource: {},
plugins: {},
endpoint: {
settings: [
{
type: 'secret-input',
name: 'api-key',
required: true,
default: null,
options: null,
label: {
en_US: 'API-key',
zh_Hans: 'API-key',
},
help: null,
url: null,
placeholder: {
en_US: 'Please input your API key',
zh_Hans: '请输入你的 API key',
},
},
],
endpoints: [
{ path: '/duck/<app_id>', method: 'GET' },
{ path: '/neko', method: 'GET' },
],
},
tool: null, // TODO
verified: true,
},
installation_id: 'jflkdsjoewingljlsadjgoijg-dkfjldajglkajglask-dlfkajdg',
tenant_id: 'jflkdsjoewingljlsadjgoijg',
endpoints_setups: 2,
endpoints_active: 1,
version: '1.2.0',
source: PluginSource.marketplace,
meta: null,
}
export const toolNotionEndpoints = [
{
id: 'dlfajkgjdga-dfjalksjfglkds-dfjakld',
created_at: '2024-10-16 16:05:33',
updated_at: '2024-10-16 16:05:33',
settings: {
'api-key': '*******',
},
tenant_id: 'jflkdsjoewingljlsadjgoijg',
plugin_id: 'Notion/notion-page-search',
expired_at: '2024-10-16 16:05:33',
declaration: {
settings: [
{
type: 'secret-input',
name: 'api-key',
required: true,
default: null,
options: null,
label: {
en_US: 'API-key',
zh_Hans: 'API-key',
},
help: null,
url: null,
placeholder: {
en_US: 'Please input your API key',
zh_Hans: '请输入你的 API key',
},
},
],
endpoints: [
{ path: '/duck/<app_id>', method: 'GET' },
{ path: '/neko', method: 'GET' },
],
},
name: 'default',
enabled: true,
url: 'http://localhost:5002/e/45rj9V4TRxAjL0I2wXRZgZdXjdHEKBh8',
hook_id: '45rj9V4TRxAjL0I2wXRZgZdXjdHEKBh8',
},
]

View File

@ -1,19 +1,14 @@
import React from 'react'
import useSWR from 'swr'
import { useTranslation } from 'react-i18next'
import { usePluginPageContext } from '@/app/components/plugins/plugin-page/context'
import ModelIcon from '@/app/components/header/account-setting/model-provider-page/model-icon'
import ModelName from '@/app/components/header/account-setting/model-provider-page/model-name'
import { fetchModelProviderModelList } from '@/service/common'
import { useModelProviderModelList } from '@/service/use-models'
const ModelList = () => {
const { t } = useTranslation()
const currentPluginDetail = usePluginPageContext(v => v.currentPluginDetail)
const { data: res } = useSWR(
`/workspaces/current/model-providers/${currentPluginDetail.plugin_id}/${currentPluginDetail.name}/models`,
fetchModelProviderModelList,
)
const { data: res } = useModelProviderModelList(`${currentPluginDetail.plugin_id}/${currentPluginDetail.name}`)
if (!res)
return null
@ -21,7 +16,7 @@ const ModelList = () => {
return (
<div className='px-4 py-2'>
<div className='mb-1 h-6 flex items-center text-text-secondary system-sm-semibold-uppercase'>{t('plugin.detailPanel.modelNum', { num: res.data.length })}</div>
<div className='h-8 flex items-center'>
<div className='flex flex-col'>
{res.data.map(model => (
<div key={model.model} className='h-6 py-1 flex items-center'>
<ModelIcon

View File

@ -2,6 +2,7 @@
import type { FC } from 'react'
import React, { useCallback, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { PluginSource } from '../types'
import { RiArrowRightUpLine, RiMoreFill } from '@remixicon/react'
import ActionButton from '@/app/components/base/action-button'
// import Button from '@/app/components/base/button'
@ -13,6 +14,7 @@ import {
import cn from '@/utils/classnames'
type Props = {
source: PluginSource
onInfo: () => void
onCheckVersion: () => void
onRemove: () => void
@ -20,10 +22,11 @@ type Props = {
}
const OperationDropdown: FC<Props> = ({
source,
detailUrl,
onInfo,
onCheckVersion,
onRemove,
detailUrl,
}) => {
const { t } = useTranslation()
const [open, doSetOpen] = useState(false)
@ -56,25 +59,33 @@ const OperationDropdown: FC<Props> = ({
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-50'>
<div className='w-[160px] p-1 bg-components-panel-bg-blur rounded-xl border-[0.5px] border-components-panel-border shadow-lg'>
<div
onClick={() => {
onInfo()
handleTrigger()
}}
className='px-3 py-1.5 rounded-lg text-text-secondary system-md-regular cursor-pointer hover:bg-state-base-hover'
>{t('plugin.detailPanel.operation.info')}</div>
<div
onClick={() => {
onCheckVersion()
handleTrigger()
}}
className='px-3 py-1.5 rounded-lg text-text-secondary system-md-regular cursor-pointer hover:bg-state-base-hover'
>{t('plugin.detailPanel.operation.checkUpdate')}</div>
<a href={detailUrl} target='_blank' className='flex items-center px-3 py-1.5 rounded-lg text-text-secondary system-md-regular cursor-pointer hover:bg-state-base-hover'>
<span className='grow'>{t('plugin.detailPanel.operation.viewDetail')}</span>
<RiArrowRightUpLine className='shrink-0 w-3.5 h-3.5 text-text-tertiary' />
</a>
<div className='my-1 h-px bg-divider-subtle'></div>
{source === PluginSource.github && (
<div
onClick={() => {
onInfo()
handleTrigger()
}}
className='px-3 py-1.5 rounded-lg text-text-secondary system-md-regular cursor-pointer hover:bg-state-base-hover'
>{t('plugin.detailPanel.operation.info')}</div>
)}
{source === PluginSource.github && (
<div
onClick={() => {
onCheckVersion()
handleTrigger()
}}
className='px-3 py-1.5 rounded-lg text-text-secondary system-md-regular cursor-pointer hover:bg-state-base-hover'
>{t('plugin.detailPanel.operation.checkUpdate')}</div>
)}
{source === PluginSource.marketplace && (
<a href={detailUrl} target='_blank' className='flex items-center px-3 py-1.5 rounded-lg text-text-secondary system-md-regular cursor-pointer hover:bg-state-base-hover'>
<span className='grow'>{t('plugin.detailPanel.operation.viewDetail')}</span>
<RiArrowRightUpLine className='shrink-0 w-3.5 h-3.5 text-text-tertiary' />
</a>
)}
{(source === PluginSource.marketplace || source === PluginSource.github) && (
<div className='my-1 h-px bg-divider-subtle'></div>
)}
<div
onClick={() => {
onRemove()

View File

@ -0,0 +1,21 @@
import { FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
export const NAME_FIELD = {
type: FormTypeEnum.textInput,
name: 'name',
label: {
en_US: 'Endpoint Name',
zh_Hans: '端点名称',
ja_JP: 'エンドポイント名',
pt_BR: 'Nome do ponto final',
},
placeholder: {
en_US: 'Endpoint Name',
zh_Hans: '端点名称',
ja_JP: 'エンドポイント名',
pt_BR: 'Nome do ponto final',
},
required: true,
default: '',
help: null,
}

View File

@ -11,16 +11,13 @@ import {
useContextSelector,
} from 'use-context-selector'
import { useSelector as useAppContextSelector } from '@/context/app-context'
import type { Permissions, PluginDetail } from '../types'
import type { PluginDetail } from '../types'
import type { FilterState } from './filter-management'
import { PermissionType } from '../types'
import { useTranslation } from 'react-i18next'
import { useTabSearchParams } from '@/hooks/use-tab-searchparams'
export type PluginPageContextValue = {
containerRef: React.RefObject<HTMLDivElement>
permissions: Permissions
setPermissions: (permissions: PluginPageContextValue['permissions']) => void
currentPluginDetail: PluginDetail | undefined
setCurrentPluginDetail: (plugin: PluginDetail) => void
filters: FilterState
@ -32,21 +29,16 @@ export type PluginPageContextValue = {
export const PluginPageContext = createContext<PluginPageContextValue>({
containerRef: { current: null },
permissions: {
install_permission: PermissionType.noOne,
debug_permission: PermissionType.noOne,
},
setPermissions: () => {},
currentPluginDetail: undefined,
setCurrentPluginDetail: () => {},
setCurrentPluginDetail: () => { },
filters: {
categories: [],
tags: [],
searchQuery: '',
},
setFilters: () => {},
setFilters: () => { },
activeTab: '',
setActiveTab: () => {},
setActiveTab: () => { },
options: [],
})
@ -63,10 +55,6 @@ export const PluginPageContextProvider = ({
}: PluginPageContextProviderProps) => {
const { t } = useTranslation()
const containerRef = useRef<HTMLDivElement>(null)
const [permissions, setPermissions] = useState<PluginPageContextValue['permissions']>({
install_permission: PermissionType.noOne,
debug_permission: PermissionType.noOne,
})
const [filters, setFilters] = useState<FilterState>({
categories: [],
tags: [],
@ -93,8 +81,6 @@ export const PluginPageContextProvider = ({
<PluginPageContext.Provider
value={{
containerRef,
permissions,
setPermissions,
currentPluginDetail,
setCurrentPluginDetail,
filters,

View File

@ -206,7 +206,7 @@ const PluginPage = ({
{showPluginSettingModal && (
<PermissionSetModal
payload={permissions}
payload={permissions!}
onHide={setHidePluginSettingModal}
onSave={setPermissions}
/>

View File

@ -48,7 +48,7 @@ const PluginsPanel = () => {
) : (
<Empty />
)}
<PluginDetailPanel onDelete={() => invalidateInstalledPluginList()}/>
<PluginDetailPanel onUpdate={() => invalidateInstalledPluginList()}/>
</>
)
}

View File

@ -1,15 +1,12 @@
import { useEffect } from 'react'
import type { Permissions } from '../types'
import { PermissionType } from '../types'
import {
usePluginPageContext,
} from './context'
import { useAppContext } from '@/context/app-context'
import { updatePermission as doUpdatePermission, fetchPermission } from '@/service/plugins'
import Toast from '../../base/toast'
import { useTranslation } from 'react-i18next'
import { useInvalidatePermissions, useMutationPermissions, usePermissions } from '@/service/use-plugins'
const hasPermission = (permission: PermissionType, isAdmin: boolean) => {
const hasPermission = (permission: PermissionType | undefined, isAdmin: boolean) => {
if (!permission)
return false
if (permission === PermissionType.noOne)
return false
@ -22,29 +19,26 @@ const hasPermission = (permission: PermissionType, isAdmin: boolean) => {
const usePermission = () => {
const { t } = useTranslation()
const { isCurrentWorkspaceManager, isCurrentWorkspaceOwner } = useAppContext()
const [permissions, setPermissions] = usePluginPageContext(v => [v.permissions, v.setPermissions])
const { data: permissions } = usePermissions()
const invalidatePermissions = useInvalidatePermissions()
const { mutate: updatePermission, isPending: isUpdatePending } = useMutationPermissions({
onSuccess: () => {
invalidatePermissions()
Toast.notify({
type: 'success',
message: t('common.api.actionSuccess'),
})
},
})
const isAdmin = isCurrentWorkspaceManager || isCurrentWorkspaceOwner
const updatePermission = async (permission: Permissions) => {
await doUpdatePermission(permission)
setPermissions(permission)
Toast.notify({
type: 'success',
message: t('common.api.actionSuccess'),
})
}
useEffect(() => {
(async () => {
const permission = await fetchPermission()
setPermissions(permission)
})()
}, [])
return {
canManagement: hasPermission(permissions.install_permission, isAdmin),
canDebugger: hasPermission(permissions.debug_permission, isAdmin),
canManagement: hasPermission(permissions?.install_permission, isAdmin),
canDebugger: hasPermission(permissions?.debug_permission, isAdmin),
canSetPermissions: isAdmin,
permissions,
setPermissions: updatePermission,
isUpdatePending,
}
}

View File

@ -19,11 +19,13 @@ import { useBoolean } from 'ahooks'
type Props = {
className?: string
payload: Plugin
onSuccess: () => void
}
const ProviderCard: FC<Props> = ({
className,
payload,
onSuccess,
}) => {
const { t } = useTranslation()
const [isShowInstallFromMarketplace, {
@ -84,7 +86,10 @@ const ProviderCard: FC<Props> = ({
manifest={payload as any}
uniqueIdentifier={payload.latest_package_identifier}
onClose={hideInstallFromMarketplace}
onSuccess={hideInstallFromMarketplace}
onSuccess={() => {
onSuccess()
hideInstallFromMarketplace()
}}
/>
)
}

View File

@ -194,19 +194,10 @@ export type GitHubUrlInfo = {
}
// endpoint
export type CreateEndpointRequest = {
plugin_unique_identifier: string
settings: Record<string, any>
name: string
}
export type EndpointOperationResponse = {
result: 'success' | 'error'
}
export type EndpointsRequest = {
page_size: number
page: number
plugin_id: string
}
export type EndpointsResponse = {
endpoints: EndpointListItem[]
has_more: boolean
@ -301,3 +292,7 @@ export type InstalledPluginListResponse = {
export type UninstallPluginResponse = {
success: boolean
}
export type PluginsFromMarketplaceResponse = {
plugins: Plugin[]
}

View File

@ -15,7 +15,7 @@ export const useMarketplace = (searchPluginText: string, filterPluginTags: strin
} = useMarketplaceCollectionsAndPlugins()
const {
plugins,
setPlugins,
resetPlugins,
queryPlugins,
queryPluginsWithDebounced,
isLoading: isPluginsLoading,
@ -37,9 +37,9 @@ export const useMarketplace = (searchPluginText: string, filterPluginTags: strin
}
else {
queryMarketplaceCollectionsAndPlugins()
setPlugins(undefined)
resetPlugins()
}
}, [searchPluginText, filterPluginTags, queryPlugins, queryMarketplaceCollectionsAndPlugins, queryPluginsWithDebounced, setPlugins])
}, [searchPluginText, filterPluginTags, queryPlugins, queryMarketplaceCollectionsAndPlugins, queryPluginsWithDebounced, resetPlugins])
return {
isLoading: isLoading || isPluginsLoading,

View File

@ -31,9 +31,11 @@ const Marketplace = ({
onClick={() => onMarketplaceScroll()}
/>
<div className='sticky top-0 pt-5 pb-3 bg-background-default-subtle z-10'>
<div className='title-2xl-semi-bold bg-gradient-to-r from-[rgba(11,165,236,0.95)] to-[rgba(21,90,239,0.95)] bg-clip-text text-transparent'>More from Marketplace</div>
<div className='title-2xl-semi-bold bg-gradient-to-r from-[rgba(11,165,236,0.95)] to-[rgba(21,90,239,0.95)] bg-clip-text text-transparent'>
{t('plugin.marketplace.moreFrom')}
</div>
<div className='flex items-center text-center body-md-regular text-text-tertiary'>
Discover
{t('plugin.marketplace.discover')}
<span className="relative ml-1 body-md-medium text-text-secondary after:content-[''] after:absolute after:left-0 after:bottom-[1.5px] after:w-full after:h-2 after:bg-text-text-selected">
{t('plugin.category.models')}
</span>
@ -45,11 +47,11 @@ const Marketplace = ({
<span className="relative ml-1 mr-1 body-md-medium text-text-secondary after:content-[''] after:absolute after:left-0 after:bottom-[1.5px] after:w-full after:h-2 after:bg-text-text-selected">
{t('plugin.category.extensions')}
</span>
and
{t('plugin.marketplace.and')}
<span className="relative ml-1 mr-1 body-md-medium text-text-secondary after:content-[''] after:absolute after:left-0 after:bottom-[1.5px] after:w-full after:h-2 after:bg-text-text-selected">
{t('plugin.category.bundles')}
</span>
in Dify Marketplace
{t('plugin.marketplace.inDifyMarketplace')}
</div>
</div>
{

View File

@ -80,7 +80,7 @@ export type Tool = {
export type ToolCredential = {
name: string
label: TypeWithI18N
help: TypeWithI18N
help: TypeWithI18N | null
placeholder: TypeWithI18N
type: string
required: boolean

View File

@ -1,10 +0,0 @@
const translation = {
plugins: {
title: 'Plugins',
},
discover: {
title: 'Explore Marketplace',
},
}
export default translation

View File

@ -1,5 +1,6 @@
const translation = {
category: {
all: 'All',
models: 'models',
tools: 'tools',
extensions: 'extensions',
@ -116,6 +117,13 @@ const translation = {
error: {
inValidGitHubUrl: 'Invalid GitHub URL. Please enter a valid URL in the format: https://github.com/owner/repo',
},
marketplace: {
empower: 'Empower your AI development',
discover: 'Discover',
and: 'and',
inDifyMarketplace: 'in Dify Marketplace',
moreFrom: 'More from Marketplace',
},
}
export default translation

View File

@ -1,10 +0,0 @@
const translation = {
plugins: {
title: '插件',
},
discover: {
title: '探索市场',
},
}
export default translation

View File

@ -1,5 +1,6 @@
const translation = {
category: {
all: '全部',
models: '模型',
tools: '工具',
extensions: '扩展',
@ -116,6 +117,13 @@ const translation = {
error: {
inValidGitHubUrl: '无效的 GitHub URL。请输入格式为 https://github.com/owner/repo 的有效 URL',
},
marketplace: {
empower: '助力您的 AI 开发',
discover: '探索',
and: '和',
inDifyMarketplace: '在 Dify 市场中',
moreFrom: '更多来自市场',
},
}
export default translation

View File

@ -64,6 +64,7 @@
"js-cookie": "^3.0.5",
"jwt-decode": "^4.0.0",
"katex": "^0.16.11",
"ky": "^1.7.2",
"lamejs": "^1.2.1",
"lexical": "^0.18.0",
"lodash-es": "^4.17.21",
@ -84,9 +85,9 @@
"react-hook-form": "^7.53.1",
"react-i18next": "^15.1.0",
"react-infinite-scroll-component": "^6.1.0",
"react-markdown": "^9.0.1",
"react-multi-email": "^1.0.25",
"react-papaparse": "^4.4.0",
"react-markdown": "^9.0.1",
"react-slider": "^2.0.6",
"react-sortablejs": "^6.1.4",
"react-syntax-highlighter": "^15.6.1",

View File

@ -133,6 +133,9 @@ importers:
katex:
specifier: ^0.16.11
version: 0.16.11
ky:
specifier: ^1.7.2
version: 1.7.2
lamejs:
specifier: ^1.2.1
version: 1.2.1
@ -5612,6 +5615,10 @@ packages:
resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==}
engines: {node: '>=6'}
ky@1.7.2:
resolution: {integrity: sha512-OzIvbHKKDpi60TnF9t7UUVAF1B4mcqc02z5PIvrm08Wyb+yOcz63GRvEuVxNT18a9E1SrNouhB4W2NNLeD7Ykg==}
engines: {node: '>=18'}
lamejs@1.2.1:
resolution: {integrity: sha512-s7bxvjvYthw6oPLCm5pFxvA84wUROODB8jEO2+CE1adhKgrIvVOlmMgY8zyugxGrvRaDHNJanOiS21/emty6dQ==}
@ -14744,6 +14751,8 @@ snapshots:
kleur@4.1.5: {}
ky@1.7.2: {}
lamejs@1.2.1:
dependencies:
use-strict: 1.0.1

View File

@ -1,4 +1,4 @@
import { API_PREFIX, IS_CE_EDITION, MARKETPLACE_API_PREFIX, PUBLIC_API_PREFIX } from '@/config'
import { API_PREFIX, IS_CE_EDITION, PUBLIC_API_PREFIX } from '@/config'
import { refreshAccessTokenOrRelogin } from './refresh-token'
import Toast from '@/app/components/base/toast'
import type { AnnotationReply, MessageEnd, MessageReplace, ThoughtItem } from '@/app/components/base/chat/chat/type'
@ -17,27 +17,10 @@ import type {
WorkflowStartedResponse,
} from '@/types/workflow'
import { removeAccessToken } from '@/app/components/share/utils'
import type { FetchOptionType, ResponseError } from './fetch'
import { ContentType, base, baseOptions, getPublicToken } from './fetch'
const TIME_OUT = 100000
const ContentType = {
json: 'application/json',
stream: 'text/event-stream',
audio: 'audio/mpeg',
form: 'application/x-www-form-urlencoded; charset=UTF-8',
download: 'application/octet-stream', // for download
upload: 'multipart/form-data', // for upload
}
const baseOptions = {
method: 'GET',
mode: 'cors',
credentials: 'include', // always send cookies、HTTP Basic authentication.
headers: new Headers({
'Content-Type': ContentType.json,
}),
redirect: 'follow',
}
export type IOnDataMoreInfo = {
conversationId?: string
taskId?: string
@ -100,17 +83,6 @@ export type IOtherOptions = {
onTextReplace?: IOnTextReplace
}
type ResponseError = {
code: string
message: string
status: number
}
type FetchOptionType = Omit<RequestInit, 'body'> & {
params?: Record<string, any>
body?: BodyInit | Record<string, any> | null
}
function unicodeToChar(text: string) {
if (!text)
return ''
@ -277,153 +249,13 @@ const handleStream = (
read()
}
const baseFetch = <T>(
url: string,
fetchOptions: FetchOptionType,
{
isPublicAPI = false,
isMarketplaceAPI = false,
bodyStringify = true,
needAllResponseContent,
deleteContentType,
getAbortController,
silent,
}: IOtherOptions,
): Promise<T> => {
const options: typeof baseOptions & FetchOptionType = Object.assign({}, baseOptions, fetchOptions)
if (isMarketplaceAPI)
options.credentials = 'omit'
if (getAbortController) {
const abortController = new AbortController()
getAbortController(abortController)
options.signal = abortController.signal
}
if (isPublicAPI) {
const sharedToken = globalThis.location.pathname.split('/').slice(-1)[0]
const accessToken = localStorage.getItem('token') || JSON.stringify({ [sharedToken]: '' })
let accessTokenJson = { [sharedToken]: '' }
try {
accessTokenJson = JSON.parse(accessToken)
}
catch (e) {
}
options.headers.set('Authorization', `Bearer ${accessTokenJson[sharedToken]}`)
}
else if (!isMarketplaceAPI) {
const accessToken = localStorage.getItem('console_token') || ''
options.headers.set('Authorization', `Bearer ${accessToken}`)
}
if (deleteContentType) {
options.headers.delete('Content-Type')
}
else {
const contentType = options.headers.get('Content-Type')
if (!contentType)
options.headers.set('Content-Type', ContentType.json)
}
const urlPrefix = (() => {
if (isMarketplaceAPI)
return MARKETPLACE_API_PREFIX
if (isPublicAPI)
return PUBLIC_API_PREFIX
return API_PREFIX
})()
let urlWithPrefix = `${urlPrefix}${url.startsWith('/') ? url : `/${url}`}`
const { method, params, body } = options
// handle query
if (method === 'GET' && params) {
const paramsArray: string[] = []
Object.keys(params).forEach(key =>
paramsArray.push(`${key}=${encodeURIComponent(params[key])}`),
)
if (urlWithPrefix.search(/\?/) === -1)
urlWithPrefix += `?${paramsArray.join('&')}`
else
urlWithPrefix += `&${paramsArray.join('&')}`
delete options.params
}
if (body && bodyStringify)
options.body = JSON.stringify(body)
// Handle timeout
return Promise.race([
new Promise((resolve, reject) => {
setTimeout(() => {
reject(new Error('request timeout'))
}, TIME_OUT)
}),
new Promise((resolve, reject) => {
globalThis.fetch(urlWithPrefix, options as RequestInit)
.then((res) => {
const resClone = res.clone()
// Error handler
if (!/^(2|3)\d{2}$/.test(String(res.status))) {
const bodyJson = res.json()
switch (res.status) {
case 401:
return Promise.reject(resClone)
case 403:
bodyJson.then((data: ResponseError) => {
if (!silent)
Toast.notify({ type: 'error', message: data.message })
if (data.code === 'already_setup')
globalThis.location.href = `${globalThis.location.origin}/signin`
})
break
// fall through
default:
bodyJson.then((data: ResponseError) => {
if (!silent)
Toast.notify({ type: 'error', message: data.message })
})
}
return Promise.reject(resClone)
}
// handle delete api. Delete api not return content.
if (res.status === 204) {
resolve({ result: 'success' })
return
}
// return data
if (options.headers.get('Content-type') === ContentType.download || options.headers.get('Content-type') === ContentType.audio)
resolve(needAllResponseContent ? resClone : res.blob())
else resolve(needAllResponseContent ? resClone : res.json())
})
.catch((err) => {
if (!silent)
Toast.notify({ type: 'error', message: err })
reject(err)
})
}),
]) as Promise<T>
}
const baseFetch = base
export const upload = (options: any, isPublicAPI?: boolean, url?: string, searchParams?: string): Promise<any> => {
const urlPrefix = isPublicAPI ? PUBLIC_API_PREFIX : API_PREFIX
let token = ''
if (isPublicAPI) {
const sharedToken = globalThis.location.pathname.split('/').slice(-1)[0]
const accessToken = localStorage.getItem('token') || JSON.stringify({ [sharedToken]: '' })
let accessTokenJson = { [sharedToken]: '' }
try {
accessTokenJson = JSON.parse(accessToken)
}
catch (e) {
}
token = accessTokenJson[sharedToken]
token = getPublicToken()
}
else {
const accessToken = localStorage.getItem('console_token') || ''
@ -499,9 +331,9 @@ export const ssePost = (
signal: abortController.signal,
}, fetchOptions)
const contentType = options.headers.get('Content-Type')
const contentType = (options.headers as Headers).get('Content-Type')
if (!contentType)
options.headers.set('Content-Type', ContentType.json)
(options.headers as Headers).set('Content-Type', ContentType.json)
getAbortController?.(abortController)
@ -559,18 +391,17 @@ export const ssePost = (
}
// base request
export const request = <T>(url: string, options = {}, otherOptions?: IOtherOptions) => {
export const request = <T>(url: string, options = {}, otherOptions: IOtherOptions = {}) => {
return new Promise<T>((resolve, reject) => {
const otherOptionsForBaseFetch = otherOptions || {}
baseFetch<T>(url, options, otherOptionsForBaseFetch).then(resolve).catch((errResp) => {
baseFetch<T>(url, options, otherOptions).then(resolve).catch((errResp) => {
if (errResp?.status === 401) {
return refreshAccessTokenOrRelogin(TIME_OUT).then(() => {
baseFetch<T>(url, options, otherOptionsForBaseFetch).then(resolve).catch(reject)
baseFetch<T>(url, options, otherOptions).then(resolve).catch(reject)
}).catch(() => {
const {
isPublicAPI = false,
silent,
} = otherOptionsForBaseFetch
} = otherOptions
const bodyJson = errResp.json()
if (isPublicAPI) {
return bodyJson.then((data: ResponseError) => {
@ -629,6 +460,11 @@ export const post = <T>(url: string, options = {}, otherOptions?: IOtherOptions)
return request<T>(url, Object.assign({}, options, { method: 'POST' }), otherOptions)
}
// For Marketplace API
export const postMarketplace = <T>(url: string, options = {}, otherOptions?: IOtherOptions) => {
return post<T>(url, options, { ...otherOptions, isMarketplaceAPI: true })
}
export const postPublic = <T>(url: string, options = {}, otherOptions?: IOtherOptions) => {
return post<T>(url, options, { ...otherOptions, isPublicAPI: true })
}

187
web/service/fetch.ts Normal file
View File

@ -0,0 +1,187 @@
import type { AfterResponseHook, BeforeErrorHook, BeforeRequestHook, Hooks } from 'ky'
import ky from 'ky'
import type { IOtherOptions } from './base'
import Toast from '@/app/components/base/toast'
import { API_PREFIX, MARKETPLACE_API_PREFIX, PUBLIC_API_PREFIX } from '@/config'
const TIME_OUT = 100000
export const ContentType = {
json: 'application/json',
stream: 'text/event-stream',
audio: 'audio/mpeg',
form: 'application/x-www-form-urlencoded; charset=UTF-8',
download: 'application/octet-stream', // for download
upload: 'multipart/form-data', // for upload
}
export type FetchOptionType = Omit<RequestInit, 'body'> & {
params?: Record<string, any>
body?: BodyInit | Record<string, any> | null
}
const afterResponse204: AfterResponseHook = async (_request, _options, response) => {
if (response.status === 204) return Response.json({ result: 'success' })
}
export type ResponseError = {
code: string
message: string
status: number
}
const afterResponseErrorCode = (otherOptions: IOtherOptions): AfterResponseHook => {
return async (_request, _options, response) => {
if (!/^(2|3)\d{2}$/.test(String(response.status))) {
const bodyJson = response.json() as Promise<ResponseError>
switch (response.status) {
case 401:
return Promise.reject(response)
case 403:
bodyJson.then((data: ResponseError) => {
if (!otherOptions.silent)
Toast.notify({ type: 'error', message: data.message })
if (data.code === 'already_setup')
globalThis.location.href = `${globalThis.location.origin}/signin`
})
break
// fall through
default:
bodyJson.then((data: ResponseError) => {
if (!otherOptions.silent)
Toast.notify({ type: 'error', message: data.message })
})
}
throw response
}
}
}
const beforeErrorToast = (otherOptions: IOtherOptions): BeforeErrorHook => {
return (error) => {
if (!otherOptions.silent)
Toast.notify({ type: 'error', message: error.message })
return error
}
}
export const getPublicToken = () => {
let token = ''
const sharedToken = globalThis.location.pathname.split('/').slice(-1)[0]
const accessToken = localStorage.getItem('token') || JSON.stringify({ [sharedToken]: '' })
let accessTokenJson = { [sharedToken]: '' }
try {
accessTokenJson = JSON.parse(accessToken)
}
catch {}
token = accessTokenJson[sharedToken]
return token || ''
}
const beforeRequestPublicAuthorization: BeforeRequestHook = (request) => {
const token = getPublicToken()
request.headers.set('Authorization', `Bearer ${token}`)
}
const beforeRequestAuthorization: BeforeRequestHook = (request) => {
const accessToken = localStorage.getItem('console_token') || ''
request.headers.set('Authorization', `Bearer ${accessToken}`)
}
const beforeRequestDeleteContentType: BeforeRequestHook = (request) => {
request.headers.delete('Content-Type')
}
const baseHooks: Hooks = {
afterResponse: [
afterResponse204,
],
}
const client = ky.create({
hooks: baseHooks,
timeout: TIME_OUT,
})
export const baseOptions: RequestInit = {
method: 'GET',
mode: 'cors',
credentials: 'include', // always send cookies、HTTP Basic authentication.
headers: new Headers({
'Content-Type': ContentType.json,
}),
redirect: 'follow',
}
async function base<T>(url: string, options: FetchOptionType = {}, otherOptions: IOtherOptions = {}): Promise<T> {
const { params, body, ...init } = Object.assign({}, baseOptions, options)
const {
isPublicAPI = false,
isMarketplaceAPI = false,
bodyStringify = true,
needAllResponseContent,
deleteContentType,
getAbortController,
} = otherOptions
const base
= isMarketplaceAPI
? MARKETPLACE_API_PREFIX
: isPublicAPI
? PUBLIC_API_PREFIX
: API_PREFIX
if (getAbortController) {
const abortController = new AbortController()
getAbortController(abortController)
options.signal = abortController.signal
}
const fetchPathname = `${base}${url.startsWith('/') ? url : `/${url}`}`
const res = await client.extend({
hooks: {
...baseHooks,
beforeError: [
...baseHooks.beforeError || [],
beforeErrorToast(otherOptions),
],
beforeRequest: [
...baseHooks.beforeRequest || [],
isPublicAPI && beforeRequestPublicAuthorization,
!isPublicAPI && !isMarketplaceAPI && beforeRequestAuthorization,
deleteContentType && beforeRequestDeleteContentType,
].filter(i => !!i),
afterResponse: [
...baseHooks.afterResponse || [],
afterResponseErrorCode(otherOptions),
],
},
})(fetchPathname, {
...init,
credentials: isMarketplaceAPI
? 'omit'
: (options.credentials || 'include'),
retry: {
methods: [],
},
...(bodyStringify ? { json: body } : { body: body as BodyInit }),
searchParams: params,
})
if (needAllResponseContent)
return res as T
const contentType = res.headers.get('content-type')
if (
contentType
&& [ContentType.download, ContentType.audio].includes(contentType)
)
return await res.blob() as T
return await res.json() as T
}
export {
client,
base,
}

View File

@ -1,10 +1,6 @@
import type { Fetcher } from 'swr'
import { del, get, getMarketplace, post, upload } from './base'
import { get, getMarketplace, post, upload } from './base'
import type {
CreateEndpointRequest,
EndpointOperationResponse,
EndpointsRequest,
EndpointsResponse,
InstallPackageResponse,
Permissions,
PluginDeclaration,
@ -12,49 +8,13 @@ import type {
PluginTasksResponse,
TaskStatusResponse,
UninstallPluginResponse,
UpdateEndpointRequest,
uploadGitHubResponse,
} from '@/app/components/plugins/types'
import type { DebugInfo as DebugInfoTypes } from '@/app/components/plugins/types'
import type {
MarketplaceCollectionPluginsResponse,
MarketplaceCollectionsResponse,
} from '@/app/components/plugins/marketplace/types'
export const createEndpoint: Fetcher<EndpointOperationResponse, { url: string; body: CreateEndpointRequest }> = ({ url, body }) => {
// url = /workspaces/current/endpoints/create
return post<EndpointOperationResponse>(url, { body })
}
export const fetchEndpointList: Fetcher<EndpointsResponse, { url: string; params?: EndpointsRequest }> = ({ url, params }) => {
// url = /workspaces/current/endpoints/list/plugin?plugin_id=xxx
return get<EndpointsResponse>(url, { params })
}
export const deleteEndpoint: Fetcher<EndpointOperationResponse, { url: string; endpointID: string }> = ({ url, endpointID }) => {
// url = /workspaces/current/endpoints/delete
return del<EndpointOperationResponse>(url, { body: { endpoint_id: endpointID } })
}
export const updateEndpoint: Fetcher<EndpointOperationResponse, { url: string; body: UpdateEndpointRequest }> = ({ url, body }) => {
// url = /workspaces/current/endpoints/update
return post<EndpointOperationResponse>(url, { body })
}
export const enableEndpoint: Fetcher<EndpointOperationResponse, { url: string; endpointID: string }> = ({ url, endpointID }) => {
// url = /workspaces/current/endpoints/enable
return post<EndpointOperationResponse>(url, { body: { endpoint_id: endpointID } })
}
export const disableEndpoint: Fetcher<EndpointOperationResponse, { url: string; endpointID: string }> = ({ url, endpointID }) => {
// url = /workspaces/current/endpoints/disable
return post<EndpointOperationResponse>(url, { body: { endpoint_id: endpointID } })
}
export const fetchDebugKey = async () => {
return get<DebugInfoTypes>('/workspaces/current/plugin/debugging-key')
}
export const uploadPackageFile = async (file: File) => {
const formData = new FormData()
formData.append('pkg', file)
@ -64,12 +24,6 @@ export const uploadPackageFile = async (file: File) => {
}, false, '/workspaces/current/plugin/upload/pkg')
}
export const installPackageFromLocal = async (uniqueIdentifier: string) => {
return post<InstallPackageResponse>('/workspaces/current/plugin/install/pkg', {
body: { plugin_unique_identifiers: [uniqueIdentifier] },
})
}
export const updateFromMarketPlace = async (body: Record<string, string>) => {
return post<InstallPackageResponse>('/workspaces/current/plugin/upgrade/marketplace', {
body,
@ -109,12 +63,6 @@ export const fetchManifestFromMarketPlace = async (uniqueIdentifier: string) =>
return getMarketplace<{ data: { plugin: PluginManifestInMarket } }>(`/plugins/identifier?unique_identifier=${uniqueIdentifier}`)
}
export const installPackageFromMarketPlace = async (uniqueIdentifier: string) => {
return post<InstallPackageResponse>('/workspaces/current/plugin/install/marketplace', {
body: { plugin_unique_identifiers: [uniqueIdentifier] },
})
}
export const fetchMarketplaceCollections: Fetcher<MarketplaceCollectionsResponse, { url: string; }> = ({ url }) => {
return get<MarketplaceCollectionsResponse>(url)
}
@ -131,10 +79,6 @@ export const checkTaskStatus = async (taskId: string) => {
return get<TaskStatusResponse>(`/workspaces/current/plugin/tasks/${taskId}`)
}
export const fetchPermission = async () => {
return get<Permissions>('/workspaces/current/plugin/permission/fetch')
}
export const updatePermission = async (permissions: Permissions) => {
return post('/workspaces/current/plugin/permission/change', { body: permissions })
}

View File

@ -0,0 +1,149 @@
import { get, post } from './base'
import type {
EndpointsResponse,
} from '@/app/components/plugins/types'
import {
useMutation,
useQuery,
useQueryClient,
} from '@tanstack/react-query'
const NAME_SPACE = 'endpoints'
export const useEndpointList = (pluginID: string) => {
return useQuery({
queryKey: [NAME_SPACE, 'list', pluginID],
queryFn: () => get<EndpointsResponse>('/workspaces/current/endpoints/list/plugin', {
params: {
plugin_id: pluginID,
page: 1,
page_size: 100,
},
}),
})
}
export const useInvalidateEndpointList = () => {
const queryClient = useQueryClient()
return (pluginID: string) => {
queryClient.invalidateQueries(
{
queryKey: [NAME_SPACE, 'list', pluginID],
})
}
}
export const useCreateEndpoint = ({
onSuccess,
onError,
}: {
onSuccess?: () => void
onError?: (error: any) => void
}) => {
return useMutation({
mutationKey: [NAME_SPACE, 'create'],
mutationFn: (payload: { pluginUniqueID: string, state: Record<string, any> }) => {
const { pluginUniqueID, state } = payload
const newName = state.name
delete state.name
return post('/workspaces/current/endpoints/create', {
body: {
plugin_unique_identifier: pluginUniqueID,
settings: state,
name: newName,
},
})
},
onSuccess,
onError,
})
}
export const useUpdateEndpoint = ({
onSuccess,
onError,
}: {
onSuccess?: () => void
onError?: (error: any) => void
}) => {
return useMutation({
mutationKey: [NAME_SPACE, 'update'],
mutationFn: (payload: { endpointID: string, state: Record<string, any> }) => {
const { endpointID, state } = payload
const newName = state.name
delete state.name
return post('/workspaces/current/endpoints/update', {
body: {
endpoint_id: endpointID,
settings: state,
name: newName,
},
})
},
onSuccess,
onError,
})
}
export const useDeleteEndpoint = ({
onSuccess,
onError,
}: {
onSuccess?: () => void
onError?: (error: any) => void
}) => {
return useMutation({
mutationKey: [NAME_SPACE, 'delete'],
mutationFn: (endpointID: string) => {
return post('/workspaces/current/endpoints/delete', {
body: {
endpoint_id: endpointID,
},
})
},
onSuccess,
onError,
})
}
export const useEnableEndpoint = ({
onSuccess,
onError,
}: {
onSuccess?: () => void
onError?: (error: any) => void
}) => {
return useMutation({
mutationKey: [NAME_SPACE, 'enable'],
mutationFn: (endpointID: string) => {
return post('/workspaces/current/endpoints/enable', {
body: {
endpoint_id: endpointID,
},
})
},
onSuccess,
onError,
})
}
export const useDisableEndpoint = ({
onSuccess,
onError,
}: {
onSuccess?: () => void
onError?: (error: any) => void
}) => {
return useMutation({
mutationKey: [NAME_SPACE, 'disable'],
mutationFn: (endpointID: string) => {
return post('/workspaces/current/endpoints/disable', {
body: {
endpoint_id: endpointID,
},
})
},
onSuccess,
onError,
})
}

17
web/service/use-models.ts Normal file
View File

@ -0,0 +1,17 @@
import { get } from './base'
import type {
ModelItem,
} from '@/app/components/header/account-setting/model-provider-page/declarations'
import {
useQuery,
// useQueryClient,
} from '@tanstack/react-query'
const NAME_SPACE = 'models'
export const useModelProviderModelList = (provider: string) => {
return useQuery({
queryKey: [NAME_SPACE, 'model-list', provider],
queryFn: () => get<{ data: ModelItem[] }>(`/workspaces/current/model-providers/${provider}/models`),
})
}

View File

@ -1,11 +1,18 @@
import type { DebugInfo as DebugInfoTypes, InstalledPluginListResponse } from '@/app/components/plugins/types'
import { get } from './base'
import {
useQueryClient,
} from '@tanstack/react-query'
import type {
DebugInfo as DebugInfoTypes,
InstallPackageResponse,
InstalledPluginListResponse,
Permissions,
PluginsFromMarketplaceResponse,
} from '@/app/components/plugins/types'
import type {
PluginsSearchParams,
} from '@/app/components/plugins/marketplace/types'
import { get, post, postMarketplace } from './base'
import {
useMutation,
useQuery,
useQueryClient,
} from '@tanstack/react-query'
const NAME_SPACE = 'plugins'
@ -28,9 +35,83 @@ export const useInvalidateInstalledPluginList = () => {
}
}
export const useInstallPackageFromMarketPlace = () => {
return useMutation({
mutationFn: (uniqueIdentifier: string) => {
return post<InstallPackageResponse>('/workspaces/current/plugin/install/marketplace', { body: { plugin_unique_identifiers: [uniqueIdentifier] } })
},
})
}
export const useInstallPackageFromLocal = () => {
return useMutation({
mutationFn: (uniqueIdentifier: string) => {
return post<InstallPackageResponse>('/workspaces/current/plugin/install/pkg', {
body: { plugin_unique_identifiers: [uniqueIdentifier] },
})
},
})
}
export const useDebugKey = () => {
return useQuery({
queryKey: [NAME_SPACE, 'debugKey'],
queryFn: () => get<DebugInfoTypes>('/workspaces/current/plugin/debugging-key'),
})
}
const usePermissionsKey = [NAME_SPACE, 'permissions']
export const usePermissions = () => {
return useQuery({
queryKey: usePermissionsKey,
queryFn: () => get<Permissions>('/workspaces/current/plugin/permission/fetch'),
})
}
export const useInvalidatePermissions = () => {
const queryClient = useQueryClient()
return () => {
queryClient.invalidateQueries(
{
queryKey: usePermissionsKey,
})
}
}
export const useMutationPermissions = ({
onSuccess,
}: {
onSuccess?: () => void
}) => {
return useMutation({
mutationFn: (payload: Permissions) => {
return post('/workspaces/current/plugin/permission/change', { body: payload })
},
onSuccess,
})
}
export const useMutationPluginsFromMarketplace = () => {
return useMutation({
mutationFn: (pluginsSearchParams: PluginsSearchParams) => {
const {
query,
sortBy,
sortOrder,
category,
tags,
} = pluginsSearchParams
return postMarketplace<{ data: PluginsFromMarketplaceResponse }>('/plugins/search/basic', {
body: {
page: 1,
page_size: 10,
query,
sort_by: sortBy,
sort_order: sortOrder,
category: category !== 'all' ? category : '',
tags,
},
})
},
})
}

View File

@ -1,14 +1,13 @@
import { get } from './base'
import { get, post } from './base'
import type {
Collection,
Tool,
} from '@/app/components/tools/types'
import type { ToolWithProvider } from '@/app/components/workflow/types'
import {
useQueryClient,
} from '@tanstack/react-query'
import {
useMutation,
useQuery,
useQueryClient,
} from '@tanstack/react-query'
const NAME_SPACE = 'tools'
@ -45,9 +44,61 @@ export const useAllWorkflowTools = () => {
})
}
export const useBuiltInTools = (collectionName: string) => {
export const useBuiltinProviderInfo = (providerName: string) => {
return useQuery({
queryKey: [NAME_SPACE, 'builtIn', collectionName],
queryFn: () => get<Tool[]>(`/workspaces/current/tool-provider/builtin/${collectionName}/tools`),
queryKey: [NAME_SPACE, 'builtin-provider-info', providerName],
queryFn: () => get<Collection>(`/workspaces/current/tool-provider/builtin/${providerName}/info`),
})
}
export const useInvalidateBuiltinProviderInfo = () => {
const queryClient = useQueryClient()
return (providerName: string) => {
queryClient.invalidateQueries(
{
queryKey: [NAME_SPACE, 'builtin-provider-info', providerName],
})
}
}
export const useBuiltinTools = (providerName: string) => {
return useQuery({
queryKey: [NAME_SPACE, 'builtin-provider-tools', providerName],
queryFn: () => get<Tool[]>(`/workspaces/current/tool-provider/builtin/${providerName}/tools`),
})
}
export const useUpdateProviderCredentials = ({
onSuccess,
}: {
onSuccess?: () => void
}) => {
return useMutation({
mutationKey: [NAME_SPACE, 'update-provider-credentials'],
mutationFn: (payload: { providerName: string, credentials: Record<string, any> }) => {
const { providerName, credentials } = payload
return post(`/workspaces/current/tool-provider/builtin/${providerName}/update`, {
body: {
credentials,
},
})
},
onSuccess,
})
}
export const useRemoveProviderCredentials = ({
onSuccess,
}: {
onSuccess?: () => void
}) => {
return useMutation({
mutationKey: [NAME_SPACE, 'remove-provider-credentials'],
mutationFn: (providerName: string) => {
return post(`/workspaces/current/tool-provider/builtin/${providerName}/delete`, {
body: {},
})
},
onSuccess,
})
}