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

This commit is contained in:
JzoNg 2024-12-30 15:30:29 +08:00
commit f6c1ae52dd
23 changed files with 364 additions and 141 deletions

View File

@ -0,0 +1,27 @@
import Button from '../button'
import { RiInstallLine, RiLoader2Line } from '@remixicon/react'
type InstallButtonProps = {
loading: boolean
onInstall: () => void
t: any
}
const InstallButton = ({ loading, onInstall, t }: InstallButtonProps) => {
return (
<Button size='small' className='z-[100]' onClick={onInstall}>
<div className={`flex px-[3px] justify-center items-center gap-1
${loading ? 'text-components-button-secondary-text-disabled' : 'text-components-button-secondary-text'}
system-xs-medium`}
>
{loading ? t('workflow.nodes.agent.pluginInstaller.installing') : t('workflow.nodes.agent.pluginInstaller.install')}
</div>
{loading
? <RiLoader2Line className='w-3.5 h-3.5 text-text-quaternary' />
: <RiInstallLine className='w-3.5 h-3.5 text-text-secondary' />
}
</Button>
)
}
export default InstallButton

View File

@ -4,7 +4,7 @@ import type {
ModelProvider, ModelProvider,
} from '../declarations' } from '../declarations'
import { useLanguage } from '../hooks' import { useLanguage } from '../hooks'
import { CubeOutline } from '@/app/components/base/icons/src/vender/line/shapes' import { Group } from '@/app/components/base/icons/src/vender/other'
import { OpenaiBlue, OpenaiViolet } from '@/app/components/base/icons/src/public/llm' import { OpenaiBlue, OpenaiViolet } from '@/app/components/base/icons/src/public/llm'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
@ -41,10 +41,12 @@ const ModelIcon: FC<ModelIconProps> = ({
return ( return (
<div className={cn( <div className={cn(
'flex items-center justify-center w-6 h-6 rounded border-[0.5px] border-black/5 bg-gray-50', 'flex items-center justify-center w-5 h-5 rounded-md border-[0.5px] border-components-panel-border-subtle bg-background-default-subtle',
className, className,
)}> )}>
<CubeOutline className='w-4 h-4 text-text-quaternary' /> <div className='flex w-3 h-3 items-center justify-center opacity-35'>
<Group className='text-text-tertiary' />
</div>
</div> </div>
) )
} }

View File

@ -1,4 +1,5 @@
import type { FC } from 'react' import type { FC } from 'react'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import type { import type {
CustomConfigurationModelFixedFields, CustomConfigurationModelFixedFields,
@ -10,20 +11,24 @@ import {
CustomConfigurationStatusEnum, CustomConfigurationStatusEnum,
} from '../declarations' } from '../declarations'
import { UPDATE_MODEL_PROVIDER_CUSTOM_MODEL_LIST } from '../provider-added-card' import { UPDATE_MODEL_PROVIDER_CUSTOM_MODEL_LIST } from '../provider-added-card'
import { ModelStatusEnum } from '../declarations' import type { PluginInfoFromMarketPlace } from '@/app/components/plugins/types'
import { useInstallPackageFromMarketPlace } from '@/service/use-plugins'
import ConfigurationButton from './configuration-button'
import { PluginType } from '@/app/components/plugins/types'
import { import {
useUpdateModelList, useUpdateModelList,
useUpdateModelProviders, useUpdateModelProviders,
} from '../hooks' } from '../hooks'
import ModelIcon from '../model-icon' import ModelIcon from '../model-icon'
import ModelName from '../model-name' import ModelDisplay from './model-display'
import Button from '@/app/components/base/button' import InstallButton from '@/app/components/base/install-button'
import StatusIndicators from './status-indicators'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import { useProviderContext } from '@/context/provider-context' import { useProviderContext } from '@/context/provider-context'
import { useModalContextSelector } from '@/context/modal-context' import { useModalContextSelector } from '@/context/modal-context'
import { useEventEmitterContextContext } from '@/context/event-emitter' import { useEventEmitterContextContext } from '@/context/event-emitter'
import Tooltip from '@/app/components/base/tooltip' import { RiEqualizer2Line } from '@remixicon/react'
import { RiEqualizer2Line, RiErrorWarningFill } from '@remixicon/react' import { fetchPluginInfoFromMarketPlace } from '@/service/plugins'
export type AgentModelTriggerProps = { export type AgentModelTriggerProps = {
open?: boolean open?: boolean
@ -56,6 +61,36 @@ const AgentModelTrigger: FC<AgentModelTriggerProps> = ({
item => item.quota_type === modelProvider.system_configuration.current_quota_type, item => item.quota_type === modelProvider.system_configuration.current_quota_type,
) )
) )
const [pluginInfo, setPluginInfo] = useState<PluginInfoFromMarketPlace | null>(null)
const [isPluginChecked, setIsPluginChecked] = useState(false)
const [loading, setLoading] = useState(false)
const [installed, setInstalled] = useState(false)
const { mutateAsync: installPackageFromMarketPlace } = useInstallPackageFromMarketPlace()
useEffect(() => {
(async () => {
if (providerName && !modelProvider) {
const parts = providerName.split('/')
const org = parts[0]
const name = parts[1]
try {
const pluginInfo = await fetchPluginInfoFromMarketPlace({ org, name })
if (pluginInfo.data.plugin.category === PluginType.model)
setPluginInfo(pluginInfo.data.plugin)
}
catch (error) {
// pass
}
setIsPluginChecked(true)
}
else {
setIsPluginChecked(true)
}
})()
}, [providerName, modelProvider])
if (modelId && !isPluginChecked)
return null
const handleOpenModal = ( const handleOpenModal = (
provider: ModelProvider, provider: ModelProvider,
@ -97,64 +132,41 @@ const AgentModelTrigger: FC<AgentModelTriggerProps> = ({
> >
{modelId ? ( {modelId ? (
<> <>
{currentProvider && ( <ModelIcon
<ModelIcon className="m-0.5"
className="m-0.5" provider={currentProvider || modelProvider}
provider={currentProvider} modelName={currentModel?.model || modelId}
modelName={currentModel?.model} isDeprecated={hasDeprecated}
isDeprecated={hasDeprecated} />
/> <ModelDisplay
)} currentModel={currentModel}
{!currentProvider && ( modelId={modelId}
<ModelIcon />
className="m-0.5"
provider={modelProvider}
modelName={modelId}
isDeprecated={hasDeprecated}
/>
)}
{currentModel && (
<ModelName
className="flex px-1 py-[3px] items-center gap-1 grow"
modelItem={currentModel}
showMode
showFeatures
/>
)}
{!currentModel && (
<div className="flex py-[3px] px-1 items-center gap-1 grow opacity-50 truncate">
<div className="text-components-input-text-filled text-ellipsis overflow-hidden system-sm-regular">
{modelId}
</div>
</div>
)}
{needsConfiguration && ( {needsConfiguration && (
<Button <ConfigurationButton
size="small" modelProvider={modelProvider}
className="z-[100]" handleOpenModal={handleOpenModal}
onClick={(e) => { />
e.stopPropagation() )}
handleOpenModal(modelProvider, ConfigurationMethodEnum.predefinedModel, undefined) <StatusIndicators
}} needsConfiguration={needsConfiguration}
> modelProvider={!!modelProvider}
<div className="flex px-[3px] justify-center items-center gap-1"> disabled={!!disabled}
{t('workflow.nodes.agent.notAuthorized')} pluginInfo={pluginInfo}
</div> t={t}
<div className="flex w-[14px] h-[14px] justify-center items-center"> />
<div className="w-2 h-2 shrink-0 rounded-[3px] border border-components-badge-status-light-warning-border-inner {!installed && !modelProvider && pluginInfo && (
bg-components-badge-status-light-warning-bg shadow-components-badge-status-light-warning-halo" /> <InstallButton
</div> loading={loading}
</Button> onInstall={async () => {
setLoading(true)
const { all_installed } = await installPackageFromMarketPlace(pluginInfo.latest_package_identifier)
if (all_installed)
setInstalled(true)
}}
t={t}
/>
)} )}
{!needsConfiguration && disabled && (
<Tooltip
popupContent={t('workflow.nodes.agent.modelSelectorTooltips.deprecated')}
asChild={false}
>
<RiErrorWarningFill className='w-4 h-4 text-text-destructive' />
</Tooltip>
)
}
</> </>
) : ( ) : (
<> <>
@ -168,11 +180,6 @@ const AgentModelTrigger: FC<AgentModelTriggerProps> = ({
</div> </div>
</> </>
)} )}
{currentProvider && currentModel && currentModel.status === ModelStatusEnum.active && (
<div className="flex pr-1 items-center">
<RiEqualizer2Line className="w-4 h-4 text-text-tertiary group-hover:text-text-secondary" />
</div>
)}
</div> </div>
) )
} }

View File

@ -0,0 +1,32 @@
import Button from '@/app/components/base/button'
import { ConfigurationMethodEnum } from '../declarations'
import { useTranslation } from 'react-i18next'
type ConfigurationButtonProps = {
modelProvider: any
handleOpenModal: any
}
const ConfigurationButton = ({ modelProvider, handleOpenModal }: ConfigurationButtonProps) => {
const { t } = useTranslation()
return (
<Button
size="small"
className="z-[100]"
onClick={(e) => {
e.stopPropagation()
handleOpenModal(modelProvider, ConfigurationMethodEnum.predefinedModel, undefined)
}}
>
<div className="flex px-[3px] justify-center items-center gap-1">
{t('workflow.nodes.agent.notAuthorized')}
</div>
<div className="flex w-[14px] h-[14px] justify-center items-center">
<div className="w-2 h-2 shrink-0 rounded-[3px] border border-components-badge-status-light-warning-border-inner
bg-components-badge-status-light-warning-bg shadow-components-badge-status-light-warning-halo" />
</div>
</Button>
)
}
export default ConfigurationButton

View File

@ -0,0 +1,25 @@
import ModelName from '../model-name'
type ModelDisplayProps = {
currentModel: any
modelId: string
}
const ModelDisplay = ({ currentModel, modelId }: ModelDisplayProps) => {
return currentModel ? (
<ModelName
className="flex px-1 py-[3px] items-center gap-1 grow"
modelItem={currentModel}
showMode
showFeatures
/>
) : (
<div className="flex py-[3px] px-1 items-center gap-1 grow opacity-50 truncate">
<div className="text-components-input-text-filled text-ellipsis overflow-hidden system-sm-regular">
{modelId}
</div>
</div>
)
}
export default ModelDisplay

View File

@ -0,0 +1,44 @@
import Tooltip from '@/app/components/base/tooltip'
import { RiErrorWarningFill } from '@remixicon/react'
type StatusIndicatorsProps = {
needsConfiguration: boolean
modelProvider: boolean
disabled: boolean
pluginInfo: any
t: any
}
const StatusIndicators = ({ needsConfiguration, modelProvider, disabled, pluginInfo, t }: StatusIndicatorsProps) => {
return (
<>
{!needsConfiguration && modelProvider && disabled && (
<Tooltip
popupContent={t('workflow.nodes.agent.modelSelectorTooltips.deprecated')}
asChild={false}
>
<RiErrorWarningFill className='w-4 h-4 text-text-destructive' />
</Tooltip>
)}
{!modelProvider && !pluginInfo && (
<Tooltip
popupContent={
<div className='flex w-[240px] max-w-[240px] gap-1 flex-col px-1 py-1.5'>
<div className='text-text-primary title-xs-semi-bold'>{t('workflow.nodes.agent.modelNotInMarketplace.title')}</div>
<div className='min-w-[200px] text-text-secondary body-xs-regular'>
{t('workflow.nodes.agent.modelNotInMarketplace.desc')}
</div>
<div className='text-text-accent body-xs-regular'>{t('workflow.nodes.agent.modelNotInMarketplace.manageInPlugins')}</div>
</div>
}
asChild={false}
needsDelay
>
<RiErrorWarningFill className='w-4 h-4 text-text-destructive' />
</Tooltip>
)}
</>
)
}
export default StatusIndicators

View File

@ -7,7 +7,6 @@ import ActionList from './action-list'
import ModelList from './model-list' import ModelList from './model-list'
import AgentStrategyList from './agent-strategy-list' import AgentStrategyList from './agent-strategy-list'
import Drawer from '@/app/components/base/drawer' import Drawer from '@/app/components/base/drawer'
import MultipleToolSelector from '@/app/components/plugins/plugin-detail-panel/multiple-tool-selector'
import type { PluginDetail } from '@/app/components/plugins/types' import type { PluginDetail } from '@/app/components/plugins/types'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
@ -28,12 +27,6 @@ const PluginDetailPanel: FC<Props> = ({
onUpdate() onUpdate()
} }
const [value, setValue] = React.useState<any>(undefined)
const testChange = (val: any) => {
console.log('tool change', val)
setValue(val)
}
if (!detail) if (!detail)
return null return null
@ -59,15 +52,6 @@ const PluginDetailPanel: FC<Props> = ({
{!!detail.declaration.agent_strategy && <AgentStrategyList detail={detail} />} {!!detail.declaration.agent_strategy && <AgentStrategyList detail={detail} />}
{!!detail.declaration.endpoint && <EndpointList detail={detail} />} {!!detail.declaration.endpoint && <EndpointList detail={detail} />}
{!!detail.declaration.model && <ModelList detail={detail} />} {!!detail.declaration.model && <ModelList detail={detail} />}
{false && (
<div className='px-4 py-2'>
<MultipleToolSelector
value={value || []}
label='TOOLS'
onChange={testChange}
/>
</div>
)}
</div> </div>
</> </>
)} )}

View File

@ -0,0 +1,14 @@
import {
usePluginManifestInfo,
} from '@/service/use-plugins'
export const usePluginInstalledCheck = (providerName = '') => {
const pluginID = providerName?.split('/').splice(0, 2).join('/')
const { data: manifest } = usePluginManifestInfo(pluginID)
return {
inMarketPlace: !!manifest,
manifest: manifest?.data.plugin,
}
}

View File

@ -32,12 +32,15 @@ import {
useInvalidateAllBuiltInTools, useInvalidateAllBuiltInTools,
useUpdateProviderCredentials, useUpdateProviderCredentials,
} from '@/service/use-tools' } from '@/service/use-tools'
import { useInstallPackageFromMarketPlace } from '@/service/use-plugins'
import { usePluginInstalledCheck } from '@/app/components/plugins/plugin-detail-panel/tool-selector/hooks'
import { CollectionType } from '@/app/components/tools/types' import { CollectionType } from '@/app/components/tools/types'
import type { ToolDefaultValue } from '@/app/components/workflow/block-selector/types' import type { ToolDefaultValue } from '@/app/components/workflow/block-selector/types'
import type { import type {
OffsetOptions, OffsetOptions,
Placement, Placement,
} from '@floating-ui/react' } from '@floating-ui/react'
import { MARKETPLACE_API_PREFIX } from '@/config'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
export type ToolValue = { export type ToolValue = {
@ -92,6 +95,9 @@ const ToolSelector: FC<Props> = ({
const { data: workflowTools } = useAllWorkflowTools() const { data: workflowTools } = useAllWorkflowTools()
const invalidateAllBuiltinTools = useInvalidateAllBuiltInTools() const invalidateAllBuiltinTools = useInvalidateAllBuiltInTools()
// plugin info check
const { inMarketPlace, manifest } = usePluginInstalledCheck(value?.provider_name)
const currentProvider = useMemo(() => { const currentProvider = useMemo(() => {
const mergedTools = [...(buildInTools || []), ...(customTools || []), ...(workflowTools || [])] const mergedTools = [...(buildInTools || []), ...(customTools || []), ...(workflowTools || [])]
return mergedTools.find((toolWithProvider) => { return mergedTools.find((toolWithProvider) => {
@ -164,6 +170,25 @@ const ToolSelector: FC<Props> = ({
onSuccess: handleCredentialSettingUpdate, onSuccess: handleCredentialSettingUpdate,
}) })
// install from marketplace
const { mutateAsync: installPackageFromMarketPlace, isPending } = useInstallPackageFromMarketPlace()
const manifestIcon = useMemo(() => {
if (!manifest)
return ''
return `${MARKETPLACE_API_PREFIX}/plugins/${(manifest as any).plugin_id}/icon`
}, [manifest])
const handleInstall = async () => {
if (!manifest)
return
try {
await installPackageFromMarketPlace(manifest.latest_package_identifier)
invalidateAllBuiltinTools()
}
catch (e: any) {
Toast.notify({ type: 'error', message: `${e.message || e}` })
}
}
return ( return (
<> <>
<PortalToFollowElem <PortalToFollowElem
@ -188,7 +213,7 @@ const ToolSelector: FC<Props> = ({
{!trigger && value?.provider_name && ( {!trigger && value?.provider_name && (
<ToolItem <ToolItem
open={isShow} open={isShow}
icon={currentProvider?.icon} icon={currentProvider?.icon || manifestIcon}
providerName={value.provider_name} providerName={value.provider_name}
toolName={value.tool_name} toolName={value.tool_name}
showSwitch={supportEnableSwitch} showSwitch={supportEnableSwitch}
@ -197,13 +222,15 @@ const ToolSelector: FC<Props> = ({
onDelete={onDelete} onDelete={onDelete}
noAuth={currentProvider && !currentProvider.is_team_authorization} noAuth={currentProvider && !currentProvider.is_team_authorization}
onAuth={() => setShowSettingAuth(true)} onAuth={() => setShowSettingAuth(true)}
// uninstalled TODO uninstalled={!currentProvider && inMarketPlace}
// isError TODO isInstalling={isPending}
errorTip={<div className='space-y-1 text-xs'> onInstall={() => handleInstall()}
<h3 className='text-text-primary font-semibold'>{t('workflow.nodes.agent.pluginNotInstalled')}</h3> isError={!currentProvider && !inMarketPlace}
<p className='text-text-secondary tracking-tight'>{t('workflow.nodes.agent.pluginNotInstalledDesc')}</p> 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> <p>
<Link href={'/plugins'} className='text-text-accent tracking-tight'>{t('workflow.nodes.agent.linkToPlugin')}</Link> <Link href={'/plugins'} className='text-text-accent tracking-tight'>{t('plugin.detailPanel.toolSelector.uninstalledLink')}</Link>
</p> </p>
</div>} </div>}
/> />

View File

@ -6,6 +6,7 @@ import {
RiEqualizer2Line, RiEqualizer2Line,
RiErrorWarningFill, RiErrorWarningFill,
} from '@remixicon/react' } from '@remixicon/react'
import { Group } from '@/app/components/base/icons/src/vender/other'
import AppIcon from '@/app/components/base/app-icon' import AppIcon from '@/app/components/base/app-icon'
import Switch from '@/app/components/base/switch' import Switch from '@/app/components/base/switch'
import Button from '@/app/components/base/button' import Button from '@/app/components/base/button'
@ -61,10 +62,21 @@ const ToolItem = ({
open && 'bg-components-panel-on-panel-item-bg-hover shadow-sm', open && 'bg-components-panel-on-panel-item-bg-hover shadow-sm',
isDeleting && 'hover:bg-state-destructive-hover border-state-destructive-border shadow-xs', isDeleting && 'hover:bg-state-destructive-hover border-state-destructive-border shadow-xs',
)}> )}>
<div className={cn('shrink-0', isTransparent && 'opacity-50')}> {icon && (
{typeof icon === 'string' && <div className='w-7 h-7 bg-cover bg-center border-[0.5px] border-components-panel-border-subtle bg-background-default-dodge rounded-lg' style={{ backgroundImage: `url(${icon})` }} />} <div className={cn('shrink-0', isTransparent && 'opacity-50')}>
{typeof icon !== 'string' && <AppIcon className='w-7 h-7 border-[0.5px] border-components-panel-border-subtle bg-background-default-dodge rounded-lg' size='xs' icon={icon?.content} background={icon?.background} />} {typeof icon === 'string' && <div className='w-7 h-7 bg-cover bg-center border-[0.5px] border-components-panel-border-subtle bg-background-default-dodge rounded-lg' style={{ backgroundImage: `url(${icon})` }} />}
</div> {typeof icon !== 'string' && <AppIcon className='w-7 h-7 border-[0.5px] border-components-panel-border-subtle bg-background-default-dodge rounded-lg' size='xs' icon={icon?.content} background={icon?.background} />}
</div>
)}
{!icon && (
<div className={cn(
'flex items-center justify-center w-7 h-7 rounded-md border-[0.5px] border-components-panel-border-subtle bg-background-default-subtle',
)}>
<div className='flex w-5 h-5 items-center justify-center opacity-35'>
<Group className='text-text-tertiary' />
</div>
</div>
)}
<div className={cn('pl-0.5 grow truncate', isTransparent && 'opacity-50')}> <div className={cn('pl-0.5 grow truncate', isTransparent && 'opacity-50')}>
<div className='text-text-tertiary system-2xs-medium-uppercase'>{providerNameText}</div> <div className='text-text-tertiary system-2xs-medium-uppercase'>{providerNameText}</div>
<div className='text-text-secondary system-xs-medium'>{toolName}</div> <div className='text-text-secondary system-xs-medium'>{toolName}</div>

View File

@ -81,7 +81,7 @@ export type PluginManifestInMarket = {
icon: string icon: string
label: Record<Locale, string> label: Record<Locale, string>
category: PluginType category: PluginType
version: string // conbine the other place to it version: string // combine the other place to it
latest_version: string latest_version: string
brief: Record<Locale, string> brief: Record<Locale, string>
introduction: string introduction: string
@ -108,6 +108,11 @@ export type PluginDetail = {
meta?: MetaData meta?: MetaData
} }
export type PluginInfoFromMarketPlace = {
category: PluginType
latest_package_identifier: string
}
export type Plugin = { export type Plugin = {
type: 'plugin' | 'bundle' | 'model' | 'extension' | 'tool' | 'agent_strategy' type: 'plugin' | 'bundle' | 'model' | 'extension' | 'tool' | 'agent_strategy'
org: string org: string

View File

@ -83,7 +83,6 @@ export const AgentStrategySelector = (props: AgentStrategySelectorProps) => {
}, [query, list]) }, [query, list])
// TODO: should be replaced by real data // TODO: should be replaced by real data
const isExternalInstalled = true const isExternalInstalled = true
// TODO: 验证这玩意写对了没
const icon = list?.find( const icon = list?.find(
coll => coll.tools?.find(tool => tool.name === value?.agent_strategy_name), coll => coll.tools?.find(tool => tool.name === value?.agent_strategy_name),
)?.icon as string | undefined )?.icon as string | undefined
@ -125,9 +124,10 @@ export const AgentStrategySelector = (props: AgentStrategySelectorProps) => {
onChange({ onChange({
agent_strategy_name: tool!.tool_name, agent_strategy_name: tool!.tool_name,
agent_strategy_provider_name: tool!.provider_name, agent_strategy_provider_name: tool!.provider_name,
agent_parameters: tool!.params,
agent_strategy_label: tool!.tool_label, agent_strategy_label: tool!.tool_label,
agent_output_schema: tool!.output_schema, agent_output_schema: tool!.output_schema,
agent_configurations: {},
agent_parameters: {},
}) })
setOpen(false) setOpen(false)
}} }}

View File

@ -19,7 +19,8 @@ export type Strategy = {
agent_strategy_provider_name: string agent_strategy_provider_name: string
agent_strategy_name: string agent_strategy_name: string
agent_strategy_label: string agent_strategy_label: string
agent_parameters?: ToolVarInputs agent_configurations?: Record<string, any>
agent_parameters?: Record<string, ToolVarInputs>
agent_output_schema: Record<string, any> agent_output_schema: Record<string, any>
} }

View File

@ -1,30 +1,40 @@
import Tooltip from '@/app/components/base/tooltip' import Tooltip from '@/app/components/base/tooltip'
import Indicator from '@/app/components/header/indicator' import Indicator from '@/app/components/header/indicator'
import classNames from '@/utils/classnames' import classNames from '@/utils/classnames'
import { useRef } from 'react' import { useMemo, useRef } from 'react'
import { useAllBuiltInTools, useAllCustomTools, useAllWorkflowTools } from '@/service/use-tools'
export type ToolIconProps = { export type ToolIconProps = {
src: string
alt?: string
status?: 'error' | 'warning' status?: 'error' | 'warning'
tooltip?: string tooltip?: string
providerName: string
} }
export const ToolIcon = ({ src, status, tooltip, alt }: ToolIconProps) => { export const ToolIcon = ({ status, tooltip, providerName }: ToolIconProps) => {
const indicator = status === 'error' ? 'red' : status === 'warning' ? 'yellow' : undefined const indicator = status === 'error' ? 'red' : status === 'warning' ? 'yellow' : undefined
const containerRef = useRef<HTMLDivElement>(null) const containerRef = useRef<HTMLDivElement>(null)
const notSuccess = (['error', 'warning'] as Array<ToolIconProps['status']>).includes(status) const notSuccess = (['error', 'warning'] as Array<ToolIconProps['status']>).includes(status)
const { data: buildInTools } = useAllBuiltInTools()
const { data: customTools } = useAllCustomTools()
const { data: workflowTools } = useAllWorkflowTools()
const currentProvider = useMemo(() => {
const mergedTools = [...(buildInTools || []), ...(customTools || []), ...(workflowTools || [])]
return mergedTools.find((toolWithProvider) => {
return toolWithProvider.name === providerName
})
}, [providerName, buildInTools, customTools, workflowTools])
return <Tooltip triggerMethod='hover' popupContent={tooltip} disabled={!notSuccess}> return <Tooltip triggerMethod='hover' popupContent={tooltip} disabled={!notSuccess}>
<div className={classNames( <div className={classNames(
'size-5 border-[0.5px] border-components-panel-border-subtle bg-background-default-dodge relative', 'size-5 border-[0.5px] border-components-panel-border-subtle bg-background-default-dodge relative flex items-center justify-center rounded-[6px]',
)} )}
ref={containerRef} ref={containerRef}
> >
{/* eslint-disable-next-line @next/next/no-img-element */}
<img <img
src={src} src={currentProvider?.icon as string}
alt={alt} alt='tool icon'
className={classNames( className={classNames(
'w-full h-full max-w-5 max-h-5 object-cover rounded-[6px]', 'w-full h-full size-3.5 object-cover',
notSuccess && 'opacity-50', notSuccess && 'opacity-50',
)} )}
/> />

View File

@ -8,14 +8,11 @@ import type { ToolIconProps } from './components/tool-icon'
import { ToolIcon } from './components/tool-icon' import { ToolIcon } from './components/tool-icon'
import useConfig from './use-config' import useConfig from './use-config'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useInstalledPluginList } from '@/service/use-plugins'
import { FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' import { FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
const AgentNode: FC<NodeProps<AgentNodeType>> = (props) => { const AgentNode: FC<NodeProps<AgentNodeType>> = (props) => {
const { inputs, currentStrategy } = useConfig(props.id, props.data) const { inputs, currentStrategy } = useConfig(props.id, props.data)
const { t } = useTranslation() const { t } = useTranslation()
const pluginList = useInstalledPluginList()
// TODO: Implement models
const models = useMemo(() => { const models = useMemo(() => {
if (!inputs) return [] if (!inputs) return []
// if selected, show in node // if selected, show in node
@ -24,7 +21,7 @@ const AgentNode: FC<NodeProps<AgentNodeType>> = (props) => {
const models = currentStrategy?.parameters const models = currentStrategy?.parameters
.filter(param => param.type === FormTypeEnum.modelSelector) .filter(param => param.type === FormTypeEnum.modelSelector)
.reduce((acc, param) => { .reduce((acc, param) => {
const item = inputs.agent_parameters?.[param.name] const item = inputs.agent_configurations?.[param.name]
if (!item) { if (!item) {
if (param.required) { if (param.required) {
acc.push({ param: param.name }) acc.push({ param: param.name })
@ -41,18 +38,29 @@ const AgentNode: FC<NodeProps<AgentNodeType>> = (props) => {
const tools = useMemo(() => { const tools = useMemo(() => {
const tools: Array<ToolIconProps> = [] const tools: Array<ToolIconProps> = []
currentStrategy?.parameters.forEach((param) => { currentStrategy?.parameters.forEach((param) => {
if (['array[tool]', 'tool'].includes(param.type)) { if (param.type === FormTypeEnum.toolSelector) {
const vari = inputs.agent_parameters?.[param.name] const field = param.name
if (!vari) return const value = inputs.agent_configurations?.[field]
if (Array.isArray(vari.value)) { if (value) {
// TODO: Implement array of tools tools.push({
providerName: value.provider_name as any,
})
} }
else { }
// TODO: Implement single tool if (param.type === FormTypeEnum.multiToolSelector) {
const field = param.name
const value = inputs.agent_configurations?.[field]
if (value) {
(value as unknown as any[]).forEach((item) => {
tools.push({
providerName: item.provider_name,
})
})
} }
} }
}) })
}, [currentStrategy, inputs.agent_parameters]) return tools
}, [currentStrategy?.parameters, inputs.agent_configurations])
return <div className='mb-1 px-3 py-1 space-y-1'> return <div className='mb-1 px-3 py-1 space-y-1'>
{inputs.agent_strategy_name {inputs.agent_strategy_name
? <SettingItem ? <SettingItem
@ -65,7 +73,7 @@ const AgentNode: FC<NodeProps<AgentNodeType>> = (props) => {
{inputs.agent_strategy_label} {inputs.agent_strategy_label}
</SettingItem> </SettingItem>
: <SettingItem label={t('workflow.nodes.agent.strategyNotSet')} />} : <SettingItem label={t('workflow.nodes.agent.strategyNotSet')} />}
{models.length && <Group {models.length > 0 && <Group
label={<GroupLabel className='mt-1'> label={<GroupLabel className='mt-1'>
{t('workflow.nodes.agent.model')} {t('workflow.nodes.agent.model')}
</GroupLabel>} </GroupLabel>}
@ -85,25 +93,13 @@ const AgentNode: FC<NodeProps<AgentNodeType>> = (props) => {
/> />
})} })}
</Group>} </Group>}
<Group label={<GroupLabel className='mt-1'> {tools.length > 0 && <Group label={<GroupLabel className='mt-1'>
{t('workflow.nodes.agent.toolbox')} {t('workflow.nodes.agent.toolbox')}
</GroupLabel>}> </GroupLabel>}>
<div className='grid grid-cols-10 gap-0.5'> <div className='grid grid-cols-10 gap-0.5'>
<ToolIcon src='/logo/logo.png' /> {tools.map(tool => <ToolIcon {...tool} key={Math.random()} />)}
<ToolIcon
src='/logo/logo.png'
status='error'
tooltip={t('workflow.nodes.agent.toolNotInstallTooltip', {
tool: 'Gmail Sender',
})} />
<ToolIcon
src='/logo/logo.png'
status='warning'
tooltip={t('workflow.nodes.agent.toolNotAuthorizedTooltip', {
tool: 'DuckDuckGo AI Search',
})} />
</div> </div>
</Group> </Group>}
</div> </div>
} }

View File

@ -28,25 +28,27 @@ const AgentPanel: FC<NodePanelProps<AgentNodeType>> = (props) => {
strategy={inputs.agent_strategy_name ? { strategy={inputs.agent_strategy_name ? {
agent_strategy_provider_name: inputs.agent_strategy_provider_name!, agent_strategy_provider_name: inputs.agent_strategy_provider_name!,
agent_strategy_name: inputs.agent_strategy_name!, agent_strategy_name: inputs.agent_strategy_name!,
agent_parameters: inputs.agent_parameters, agent_configurations: inputs.agent_configurations,
agent_strategy_label: inputs.agent_strategy_label!, agent_strategy_label: inputs.agent_strategy_label!,
agent_output_schema: inputs.output_schema, agent_output_schema: inputs.output_schema,
agent_parameters: inputs.agent_parameters,
} : undefined} } : undefined}
onStrategyChange={(strategy) => { onStrategyChange={(strategy) => {
setInputs({ setInputs({
...inputs, ...inputs,
agent_strategy_provider_name: strategy?.agent_strategy_provider_name, agent_strategy_provider_name: strategy?.agent_strategy_provider_name,
agent_strategy_name: strategy?.agent_strategy_name, agent_strategy_name: strategy?.agent_strategy_name,
agent_configurations: strategy?.agent_configurations,
agent_parameters: strategy?.agent_parameters, agent_parameters: strategy?.agent_parameters,
agent_strategy_label: strategy?.agent_strategy_label, agent_strategy_label: strategy?.agent_strategy_label,
output_schema: strategy!.agent_output_schema, output_schema: strategy!.agent_output_schema,
}) })
}} }}
formSchema={currentStrategy?.parameters?.map(strategyParamToCredientialForm) || []} formSchema={currentStrategy?.parameters?.map(strategyParamToCredientialForm) || []}
formValue={inputs.agent_parameters || {}} formValue={inputs.agent_configurations || {}}
onFormValueChange={value => setInputs({ onFormValueChange={value => setInputs({
...inputs, ...inputs,
agent_parameters: value, agent_configurations: value,
})} })}
/> />
</Field> </Field>

View File

@ -5,7 +5,7 @@ export type AgentNodeType = CommonNodeType & {
agent_strategy_provider_name?: string agent_strategy_provider_name?: string
agent_strategy_name?: string agent_strategy_name?: string
agent_strategy_label?: string agent_strategy_label?: string
agent_parameters?: Record<string, any> agent_parameters?: Record<string, ToolVarInputs>
agent_configurations?: Record<string, ToolVarInputs> agent_configurations?: Record<string, any>
output_schema: Record<string, any> output_schema: Record<string, any>
} }

View File

@ -74,6 +74,9 @@ const translation = {
auth: 'AUTHORIZATION', auth: 'AUTHORIZATION',
settings: 'TOOL SETTINGS', settings: 'TOOL SETTINGS',
empty: 'Click the \'+\' button to add tools. You can add multiple tools.', empty: 'Click the \'+\' button to add tools. You can add multiple tools.',
uninstalledTitle: 'Tool not installed',
uninstalledContent: 'This plugin is installed from the local/GitHub repository. Please use after installation.',
uninstalledLink: 'Manage in Plugins',
}, },
configureApp: 'Configure App', configureApp: 'Configure App',
configureModel: 'Configure model', configureModel: 'Configure model',

View File

@ -714,6 +714,11 @@ const translation = {
install: 'Install', install: 'Install',
installing: 'Installing', installing: 'Installing',
}, },
modelNotInMarketplace: {
title: 'Model not installed',
desc: 'This model is not installed from the marketplace. Please go to Plugins to reinstall.',
manageInPlugins: 'Manage in Plugins',
},
configureModel: 'Configure Model', configureModel: 'Configure Model',
notAuthorized: 'Not Authorized', notAuthorized: 'Not Authorized',
model: 'model', model: 'model',

View File

@ -74,6 +74,9 @@ const translation = {
auth: '授权', auth: '授权',
settings: '工具设置', settings: '工具设置',
empty: '点击 "+" 按钮添加工具。您可以添加多个工具。', empty: '点击 "+" 按钮添加工具。您可以添加多个工具。',
uninstalledTitle: '工具未安装',
uninstalledContent: '此插件安装自 本地 / GitHub 仓库,请安装后使用。',
uninstalledLink: '在插件中管理',
}, },
configureApp: '应用设置', configureApp: '应用设置',
configureModel: '模型设置', configureModel: '模型设置',

View File

@ -714,6 +714,11 @@ const translation = {
install: '安装', install: '安装',
installing: '安装中', installing: '安装中',
}, },
modelNotInMarketplace: {
title: '模型未安装',
desc: '此模型未从市场安装。请转到插件重新安装。',
manageInPlugins: '在插件中管理',
},
model: '模型', model: '模型',
toolbox: '工具箱', toolbox: '工具箱',
strategyNotSet: '代理策略未设置', strategyNotSet: '代理策略未设置',

View File

@ -5,6 +5,7 @@ import type {
InstallPackageResponse, InstallPackageResponse,
Permissions, Permissions,
PluginDeclaration, PluginDeclaration,
PluginInfoFromMarketPlace,
PluginManifestInMarket, PluginManifestInMarket,
PluginTasksResponse, PluginTasksResponse,
TaskStatusResponse, TaskStatusResponse,
@ -75,6 +76,13 @@ export const fetchBundleInfoFromMarketPlace = async ({
return getMarketplace<{ data: { version: { dependencies: Dependency[] } } }>(`/bundles/${org}/${name}/${version}`) return getMarketplace<{ data: { version: { dependencies: Dependency[] } } }>(`/bundles/${org}/${name}/${version}`)
} }
export const fetchPluginInfoFromMarketPlace = async ({
org,
name,
}: Record<string, string>) => {
return getMarketplace<{ data: { plugin: PluginInfoFromMarketPlace, version: { version: string } } }>(`/plugins/${org}/${name}`)
}
export const fetchMarketplaceCollections: Fetcher<MarketplaceCollectionsResponse, { url: string; }> = ({ url }) => { export const fetchMarketplaceCollections: Fetcher<MarketplaceCollectionsResponse, { url: string; }> = ({ url }) => {
return get<MarketplaceCollectionsResponse>(url) return get<MarketplaceCollectionsResponse>(url)
} }

View File

@ -9,6 +9,7 @@ import type {
Permissions, Permissions,
Plugin, Plugin,
PluginDetail, PluginDetail,
PluginInfoFromMarketPlace,
PluginTask, PluginTask,
PluginsFromMarketplaceByInfoResponse, PluginsFromMarketplaceByInfoResponse,
PluginsFromMarketplaceResponse, PluginsFromMarketplaceResponse,
@ -91,6 +92,7 @@ export const useUpdatePackageFromMarketPlace = () => {
export const useVersionListOfPlugin = (pluginID: string) => { export const useVersionListOfPlugin = (pluginID: string) => {
return useQuery<{ data: VersionListResponse }>({ return useQuery<{ data: VersionListResponse }>({
enabled: !!pluginID,
queryKey: [NAME_SPACE, 'versions', pluginID], queryKey: [NAME_SPACE, 'versions', pluginID],
queryFn: () => getMarketplace<{ data: VersionListResponse }>(`/plugins/${pluginID}/versions`, { params: { page: 1, page_size: 100 } }), queryFn: () => getMarketplace<{ data: VersionListResponse }>(`/plugins/${pluginID}/versions`, { params: { page: 1, page_size: 100 } }),
}) })
@ -399,6 +401,15 @@ export const useMutationClearAllTaskPlugin = () => {
}) })
} }
export const usePluginManifestInfo = (pluginUID: string) => {
return useQuery({
enabled: !!pluginUID,
queryKey: [[NAME_SPACE, 'manifest', pluginUID]],
queryFn: () => getMarketplace<{ data: { plugin: PluginInfoFromMarketPlace, version: { version: string } } }>(`/plugins/${pluginUID}`),
retry: 0,
})
}
export const useDownloadPlugin = (info: { organization: string; pluginName: string; version: string }, needDownload: boolean) => { export const useDownloadPlugin = (info: { organization: string; pluginName: string; version: string }, needDownload: boolean) => {
return useQuery({ return useQuery({
queryKey: [NAME_SPACE, 'downloadPlugin', info], queryKey: [NAME_SPACE, 'downloadPlugin', info],