| @@ -249,6 +249,31 @@ class PluginInstallFromMarketplaceApi(Resource): | |||
| return jsonable_encoder(response) | |||
| class PluginFetchMarketplacePkgApi(Resource): | |||
| @setup_required | |||
| @login_required | |||
| @account_initialization_required | |||
| @plugin_permission_required(install_required=True) | |||
| def get(self): | |||
| tenant_id = current_user.current_tenant_id | |||
| parser = reqparse.RequestParser() | |||
| parser.add_argument("plugin_unique_identifier", type=str, required=True, location="args") | |||
| args = parser.parse_args() | |||
| try: | |||
| return jsonable_encoder( | |||
| { | |||
| "manifest": PluginService.fetch_marketplace_pkg( | |||
| tenant_id, | |||
| args["plugin_unique_identifier"], | |||
| ) | |||
| } | |||
| ) | |||
| except PluginDaemonClientSideError as e: | |||
| raise ValueError(e) | |||
| class PluginFetchManifestApi(Resource): | |||
| @setup_required | |||
| @login_required | |||
| @@ -488,6 +513,7 @@ api.add_resource(PluginDeleteInstallTaskApi, "/workspaces/current/plugin/tasks/< | |||
| api.add_resource(PluginDeleteAllInstallTaskItemsApi, "/workspaces/current/plugin/tasks/delete_all") | |||
| api.add_resource(PluginDeleteInstallTaskItemApi, "/workspaces/current/plugin/tasks/<task_id>/delete/<path:identifier>") | |||
| api.add_resource(PluginUninstallApi, "/workspaces/current/plugin/uninstall") | |||
| api.add_resource(PluginFetchMarketplacePkgApi, "/workspaces/current/plugin/marketplace/pkg") | |||
| api.add_resource(PluginChangePermissionApi, "/workspaces/current/plugin/permission/change") | |||
| api.add_resource(PluginFetchPermissionApi, "/workspaces/current/plugin/permission/fetch") | |||
| @@ -70,6 +70,9 @@ class PluginDeclaration(BaseModel): | |||
| models: Optional[list[str]] = Field(default_factory=list) | |||
| endpoints: Optional[list[str]] = Field(default_factory=list) | |||
| class Meta(BaseModel): | |||
| minimum_dify_version: Optional[str] = Field(default=None, pattern=r"^\d{1,4}(\.\d{1,4}){1,3}(-\w{1,16})?$") | |||
| version: str = Field(..., pattern=r"^\d{1,4}(\.\d{1,4}){1,3}(-\w{1,16})?$") | |||
| author: Optional[str] = Field(..., pattern=r"^[a-zA-Z0-9_-]{1,64}$") | |||
| name: str = Field(..., pattern=r"^[a-z0-9_-]{1,128}$") | |||
| @@ -86,6 +89,7 @@ class PluginDeclaration(BaseModel): | |||
| model: Optional[ProviderEntity] = None | |||
| endpoint: Optional[EndpointProviderDeclaration] = None | |||
| agent_strategy: Optional[AgentStrategyProviderEntity] = None | |||
| meta: Meta | |||
| @model_validator(mode="before") | |||
| @classmethod | |||
| @@ -309,6 +309,22 @@ class PluginService: | |||
| ], | |||
| ) | |||
| @staticmethod | |||
| def fetch_marketplace_pkg( | |||
| tenant_id: str, plugin_unique_identifier: str, verify_signature: bool = False | |||
| ) -> PluginDeclaration: | |||
| """ | |||
| Fetch marketplace package | |||
| """ | |||
| manager = PluginInstallationManager() | |||
| try: | |||
| declaration = manager.fetch_plugin_manifest(tenant_id, plugin_unique_identifier) | |||
| except Exception: | |||
| pkg = download_plugin_pkg(plugin_unique_identifier) | |||
| declaration = manager.upload_pkg(tenant_id, pkg, verify_signature).manifest | |||
| return declaration | |||
| @staticmethod | |||
| def install_from_marketplace_pkg( | |||
| tenant_id: str, plugin_unique_identifiers: Sequence[str], verify_signature: bool = False | |||
| @@ -1,6 +1,6 @@ | |||
| 'use client' | |||
| import type { FC } from 'react' | |||
| import React, { useEffect } from 'react' | |||
| import React, { useEffect, useMemo } from 'react' | |||
| import { type PluginDeclaration, TaskStatus } from '../../../types' | |||
| import Card from '../../../card' | |||
| import { pluginManifestToCardPluginProps } from '../../utils' | |||
| @@ -12,6 +12,8 @@ import { useInstallPackageFromLocal, usePluginTaskList } from '@/service/use-plu | |||
| import useCheckInstalled from '@/app/components/plugins/install-plugin/hooks/use-check-installed' | |||
| import { uninstallPlugin } from '@/service/plugins' | |||
| import Version from '../../base/version' | |||
| import { useAppContext } from '@/context/app-context' | |||
| import { gte } from 'semver' | |||
| const i18nPrefix = 'plugin.installModal' | |||
| @@ -103,6 +105,13 @@ const Installed: FC<Props> = ({ | |||
| } | |||
| } | |||
| const { langeniusVersionInfo } = useAppContext() | |||
| const isDifyVersionCompatible = useMemo(() => { | |||
| if (!langeniusVersionInfo.current_version) | |||
| return true | |||
| return gte(langeniusVersionInfo.current_version, payload.meta.minimum_dify_version ?? '0.0.0') | |||
| }, [langeniusVersionInfo.current_version, payload.meta.minimum_dify_version]) | |||
| return ( | |||
| <> | |||
| <div className='flex flex-col items-start justify-center gap-4 self-stretch px-6 py-3'> | |||
| @@ -114,6 +123,11 @@ const Installed: FC<Props> = ({ | |||
| components={{ trustSource: <span className='system-md-semibold' /> }} | |||
| /> | |||
| </p> | |||
| {!isDifyVersionCompatible && ( | |||
| <p className='system-md-regular flex items-center gap-1 text-text-secondary text-text-warning'> | |||
| {t('plugin.difyVersionNotCompatible', { minimalDifyVersion: payload.meta.minimum_dify_version })} | |||
| </p> | |||
| )} | |||
| </div> | |||
| <div className='flex flex-wrap content-start items-start gap-1 self-stretch rounded-2xl bg-background-section-burn p-2'> | |||
| <Card | |||
| @@ -1,6 +1,6 @@ | |||
| 'use client' | |||
| import type { FC } from 'react' | |||
| import React, { useEffect } from 'react' | |||
| import React, { useEffect, useMemo } from 'react' | |||
| // import { RiInformation2Line } from '@remixicon/react' | |||
| import { type Plugin, type PluginManifestInMarket, TaskStatus } from '../../../types' | |||
| import Card from '../../../card' | |||
| @@ -8,11 +8,13 @@ import { pluginManifestInMarketToPluginProps } from '../../utils' | |||
| import Button from '@/app/components/base/button' | |||
| import { useTranslation } from 'react-i18next' | |||
| import { RiLoader2Line } from '@remixicon/react' | |||
| import { useInstallPackageFromMarketPlace, useUpdatePackageFromMarketPlace } from '@/service/use-plugins' | |||
| import { useInstallPackageFromMarketPlace, usePluginDeclarationFromMarketPlace, useUpdatePackageFromMarketPlace } from '@/service/use-plugins' | |||
| import checkTaskStatus from '../../base/check-task-status' | |||
| import useCheckInstalled from '@/app/components/plugins/install-plugin/hooks/use-check-installed' | |||
| import Version from '../../base/version' | |||
| import { usePluginTaskList } from '@/service/use-plugins' | |||
| import { gte } from 'semver' | |||
| import { useAppContext } from '@/context/app-context' | |||
| const i18nPrefix = 'plugin.installModal' | |||
| @@ -117,11 +119,23 @@ const Installed: FC<Props> = ({ | |||
| } | |||
| } | |||
| const { langeniusVersionInfo } = useAppContext() | |||
| const { data: pluginDeclaration } = usePluginDeclarationFromMarketPlace(uniqueIdentifier) | |||
| const isDifyVersionCompatible = useMemo(() => { | |||
| if (!pluginDeclaration || !langeniusVersionInfo.current_version) return true | |||
| return gte(langeniusVersionInfo.current_version, pluginDeclaration?.manifest.meta.minimum_dify_version ?? '0.0.0') | |||
| }, [langeniusVersionInfo.current_version, pluginDeclaration?.manifest.meta.minimum_dify_version]) | |||
| return ( | |||
| <> | |||
| <div className='flex flex-col items-start justify-center gap-4 self-stretch px-6 py-3'> | |||
| <div className='system-md-regular text-text-secondary'> | |||
| <p>{t(`${i18nPrefix}.readyToInstall`)}</p> | |||
| {!isDifyVersionCompatible && ( | |||
| <p className='system-md-regular text-text-secondary text-text-warning'> | |||
| {t('plugin.difyVersionNotCompatible', { minimalDifyVersion: pluginDeclaration?.manifest.meta.minimum_dify_version })} | |||
| </p> | |||
| )} | |||
| </div> | |||
| <div className='flex flex-wrap content-start items-start gap-1 self-stretch rounded-2xl bg-background-section-burn p-2'> | |||
| <Card | |||
| @@ -4,6 +4,7 @@ import React, { useMemo } from 'react' | |||
| import { | |||
| RiArrowRightUpLine, | |||
| RiBugLine, | |||
| RiErrorWarningLine, | |||
| RiHardDrive3Line, | |||
| RiLoginCircleLine, | |||
| RiVerifiedBadgeLine, | |||
| @@ -23,6 +24,9 @@ import { API_PREFIX, MARKETPLACE_URL_PREFIX } from '@/config' | |||
| import { useSingleCategories } from '../hooks' | |||
| import { useRenderI18nObject } from '@/hooks/use-i18n' | |||
| import useRefreshPluginList from '@/app/components/plugins/install-plugin/hooks/use-refresh-plugin-list' | |||
| import { useAppContext } from '@/context/app-context' | |||
| import { gte } from 'semver' | |||
| import Tooltip from '@/app/components/base/tooltip' | |||
| type Props = { | |||
| className?: string | |||
| @@ -48,12 +52,20 @@ const PluginItem: FC<Props> = ({ | |||
| meta, | |||
| plugin_id, | |||
| } = plugin | |||
| const { category, author, name, label, description, icon, verified } = plugin.declaration | |||
| const { category, author, name, label, description, icon, verified, meta: declarationMeta } = plugin.declaration | |||
| const orgName = useMemo(() => { | |||
| return [PluginSource.github, PluginSource.marketplace].includes(source) ? author : '' | |||
| }, [source, author]) | |||
| const { langeniusVersionInfo } = useAppContext() | |||
| const isDifyVersionCompatible = useMemo(() => { | |||
| if (!langeniusVersionInfo.current_version) | |||
| return true | |||
| return gte(langeniusVersionInfo.current_version, declarationMeta.minimum_dify_version ?? '0.0.0') | |||
| }, [declarationMeta.minimum_dify_version, langeniusVersionInfo.current_version]) | |||
| const handleDelete = () => { | |||
| refreshPluginList({ category } as any) | |||
| } | |||
| @@ -89,6 +101,9 @@ const PluginItem: FC<Props> = ({ | |||
| <div className="flex h-5 items-center"> | |||
| <Title title={title} /> | |||
| {verified && <RiVerifiedBadgeLine className="ml-0.5 h-4 w-4 shrink-0 text-text-accent" />} | |||
| {!isDifyVersionCompatible && <Tooltip popupContent={ | |||
| t('plugin.difyVersionNotCompatible', { minimalDifyVersion: declarationMeta.minimum_dify_version }) | |||
| }><RiErrorWarningLine color='red' className="ml-0.5 h-4 w-4 shrink-0 text-text-accent" /></Tooltip>} | |||
| <Badge className='ml-1 shrink-0' text={source === PluginSource.github ? plugin.meta!.version : plugin.version} /> | |||
| </div> | |||
| <div className='flex items-center justify-between'> | |||
| @@ -53,6 +53,11 @@ export type EndpointListItem = { | |||
| hook_id: string | |||
| } | |||
| export type PluginDeclarationMeta = { | |||
| version: string | |||
| minimum_dify_version?: string | |||
| } | |||
| // Plugin manifest | |||
| export type PluginDeclaration = { | |||
| plugin_unique_identifier: string | |||
| @@ -72,6 +77,7 @@ export type PluginDeclaration = { | |||
| model: any | |||
| tags: string[] | |||
| agent_strategy: any | |||
| meta: PluginDeclarationMeta | |||
| } | |||
| export type PluginManifestInMarket = { | |||
| @@ -209,6 +209,7 @@ const translation = { | |||
| clearAll: 'Clear all', | |||
| }, | |||
| submitPlugin: 'Submit plugin', | |||
| difyVersionNotCompatible: 'The current Dify version is not compatible with this plugin, please upgrade to the minimum version required: {{minimalDifyVersion}}', | |||
| } | |||
| export default translation | |||
| @@ -206,6 +206,7 @@ const translation = { | |||
| installPlugin: 'プラグインをインストールする', | |||
| searchInMarketplace: 'マーケットプレイスで検索', | |||
| submitPlugin: 'プラグインを提出する', | |||
| difyVersionNotCompatible: '現在のDifyバージョンはこのプラグインと互換性がありません。最小バージョンは{{minimalDifyVersion}}です。', | |||
| } | |||
| export default translation | |||
| @@ -209,6 +209,7 @@ const translation = { | |||
| clearAll: '清除所有', | |||
| }, | |||
| submitPlugin: '上传插件', | |||
| difyVersionNotCompatible: '当前 Dify 版本不兼容该插件,其最低版本要求为 {{minimalDifyVersion}}', | |||
| } | |||
| export default translation | |||
| @@ -14,6 +14,7 @@ import type { | |||
| PackageDependency, | |||
| Permissions, | |||
| Plugin, | |||
| PluginDeclaration, | |||
| PluginDetail, | |||
| PluginInfoFromMarketPlace, | |||
| PluginTask, | |||
| @@ -118,6 +119,14 @@ export const useUpdatePackageFromMarketPlace = (options?: MutateOptions<InstallP | |||
| }) | |||
| } | |||
| export const usePluginDeclarationFromMarketPlace = (pluginUniqueIdentifier: string) => { | |||
| return useQuery({ | |||
| queryKey: [NAME_SPACE, 'pluginDeclaration', pluginUniqueIdentifier], | |||
| queryFn: () => get<{ manifest: PluginDeclaration }>('/workspaces/current/plugin/marketplace/pkg', { params: { plugin_unique_identifier: pluginUniqueIdentifier } }), | |||
| enabled: !!pluginUniqueIdentifier, | |||
| }) | |||
| } | |||
| export const useVersionListOfPlugin = (pluginID: string) => { | |||
| return useQuery<{ data: VersionListResponse }>({ | |||
| enabled: !!pluginID, | |||