diff --git a/api/controllers/console/workspace/plugin.py b/api/controllers/console/workspace/plugin.py index 302bc30905..e9c1884c60 100644 --- a/api/controllers/console/workspace/plugin.py +++ b/api/controllers/console/workspace/plugin.py @@ -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//delete/") 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") diff --git a/api/core/plugin/entities/plugin.py b/api/core/plugin/entities/plugin.py index 421c16093d..07ed94380a 100644 --- a/api/core/plugin/entities/plugin.py +++ b/api/core/plugin/entities/plugin.py @@ -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 diff --git a/api/services/plugin/plugin_service.py b/api/services/plugin/plugin_service.py index 25d192410f..96a07d36b9 100644 --- a/api/services/plugin/plugin_service.py +++ b/api/services/plugin/plugin_service.py @@ -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 diff --git a/web/app/components/plugins/install-plugin/install-from-local-package/steps/install.tsx b/web/app/components/plugins/install-plugin/install-from-local-package/steps/install.tsx index cdd9541584..412517283c 100644 --- a/web/app/components/plugins/install-plugin/install-from-local-package/steps/install.tsx +++ b/web/app/components/plugins/install-plugin/install-from-local-package/steps/install.tsx @@ -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 = ({ } } + 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 ( <>
@@ -114,6 +123,11 @@ const Installed: FC = ({ components={{ trustSource: }} />

+ {!isDifyVersionCompatible && ( +

+ {t('plugin.difyVersionNotCompatible', { minimalDifyVersion: payload.meta.minimum_dify_version })} +

+ )}
= ({ } } + 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 ( <>

{t(`${i18nPrefix}.readyToInstall`)}

+ {!isDifyVersionCompatible && ( +

+ {t('plugin.difyVersionNotCompatible', { minimalDifyVersion: pluginDeclaration?.manifest.meta.minimum_dify_version })} +

+ )}
= ({ 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 = ({
{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'> diff --git a/web/app/components/plugins/types.ts b/web/app/components/plugins/types.ts index 64f15a08a9..6c42e50123 100644 --- a/web/app/components/plugins/types.ts +++ b/web/app/components/plugins/types.ts @@ -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 = { diff --git a/web/i18n/en-US/plugin.ts b/web/i18n/en-US/plugin.ts index fd1a8c2a05..a0b36fbd65 100644 --- a/web/i18n/en-US/plugin.ts +++ b/web/i18n/en-US/plugin.ts @@ -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 diff --git a/web/i18n/ja-JP/plugin.ts b/web/i18n/ja-JP/plugin.ts index 8dd21dcb92..cdd0e443e4 100644 --- a/web/i18n/ja-JP/plugin.ts +++ b/web/i18n/ja-JP/plugin.ts @@ -206,6 +206,7 @@ const translation = { installPlugin: 'プラグインをインストールする', searchInMarketplace: 'マーケットプレイスで検索', submitPlugin: 'プラグインを提出する', + difyVersionNotCompatible: '現在のDifyバージョンはこのプラグインと互換性がありません。最小バージョンは{{minimalDifyVersion}}です。', } export default translation diff --git a/web/i18n/zh-Hans/plugin.ts b/web/i18n/zh-Hans/plugin.ts index 94b81ef1d5..e088557dfb 100644 --- a/web/i18n/zh-Hans/plugin.ts +++ b/web/i18n/zh-Hans/plugin.ts @@ -209,6 +209,7 @@ const translation = { clearAll: '清除所有', }, submitPlugin: '上传插件', + difyVersionNotCompatible: '当前 Dify 版本不兼容该插件,其最低版本要求为 {{minimalDifyVersion}}', } export default translation diff --git a/web/service/use-plugins.ts b/web/service/use-plugins.ts index 2dc59fd1f1..13a494b50d 100644 --- a/web/service/use-plugins.ts +++ b/web/service/use-plugins.ts @@ -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,