| return jsonable_encoder(response) | 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): | class PluginFetchManifestApi(Resource): | ||||
| @setup_required | @setup_required | ||||
| @login_required | @login_required | ||||
| api.add_resource(PluginDeleteAllInstallTaskItemsApi, "/workspaces/current/plugin/tasks/delete_all") | 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(PluginDeleteInstallTaskItemApi, "/workspaces/current/plugin/tasks/<task_id>/delete/<path:identifier>") | ||||
| api.add_resource(PluginUninstallApi, "/workspaces/current/plugin/uninstall") | 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(PluginChangePermissionApi, "/workspaces/current/plugin/permission/change") | ||||
| api.add_resource(PluginFetchPermissionApi, "/workspaces/current/plugin/permission/fetch") | api.add_resource(PluginFetchPermissionApi, "/workspaces/current/plugin/permission/fetch") |
| models: Optional[list[str]] = Field(default_factory=list) | models: Optional[list[str]] = Field(default_factory=list) | ||||
| endpoints: 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})?$") | 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}$") | author: Optional[str] = Field(..., pattern=r"^[a-zA-Z0-9_-]{1,64}$") | ||||
| name: str = Field(..., pattern=r"^[a-z0-9_-]{1,128}$") | name: str = Field(..., pattern=r"^[a-z0-9_-]{1,128}$") | ||||
| model: Optional[ProviderEntity] = None | model: Optional[ProviderEntity] = None | ||||
| endpoint: Optional[EndpointProviderDeclaration] = None | endpoint: Optional[EndpointProviderDeclaration] = None | ||||
| agent_strategy: Optional[AgentStrategyProviderEntity] = None | agent_strategy: Optional[AgentStrategyProviderEntity] = None | ||||
| meta: Meta | |||||
| @model_validator(mode="before") | @model_validator(mode="before") | ||||
| @classmethod | @classmethod |
| ], | ], | ||||
| ) | ) | ||||
| @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 | @staticmethod | ||||
| def install_from_marketplace_pkg( | def install_from_marketplace_pkg( | ||||
| tenant_id: str, plugin_unique_identifiers: Sequence[str], verify_signature: bool = False | tenant_id: str, plugin_unique_identifiers: Sequence[str], verify_signature: bool = False |
| 'use client' | 'use client' | ||||
| import type { FC } from 'react' | import type { FC } from 'react' | ||||
| import React, { useEffect } from 'react' | |||||
| import React, { useEffect, useMemo } from 'react' | |||||
| import { type PluginDeclaration, TaskStatus } from '../../../types' | import { type PluginDeclaration, TaskStatus } from '../../../types' | ||||
| import Card from '../../../card' | import Card from '../../../card' | ||||
| import { pluginManifestToCardPluginProps } from '../../utils' | import { pluginManifestToCardPluginProps } from '../../utils' | ||||
| import useCheckInstalled from '@/app/components/plugins/install-plugin/hooks/use-check-installed' | import useCheckInstalled from '@/app/components/plugins/install-plugin/hooks/use-check-installed' | ||||
| import { uninstallPlugin } from '@/service/plugins' | import { uninstallPlugin } from '@/service/plugins' | ||||
| import Version from '../../base/version' | import Version from '../../base/version' | ||||
| import { useAppContext } from '@/context/app-context' | |||||
| import { gte } from 'semver' | |||||
| const i18nPrefix = 'plugin.installModal' | const i18nPrefix = 'plugin.installModal' | ||||
| } | } | ||||
| } | } | ||||
| 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 ( | return ( | ||||
| <> | <> | ||||
| <div className='flex flex-col items-start justify-center gap-4 self-stretch px-6 py-3'> | <div className='flex flex-col items-start justify-center gap-4 self-stretch px-6 py-3'> | ||||
| components={{ trustSource: <span className='system-md-semibold' /> }} | components={{ trustSource: <span className='system-md-semibold' /> }} | ||||
| /> | /> | ||||
| </p> | </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> | ||||
| <div className='flex flex-wrap content-start items-start gap-1 self-stretch rounded-2xl bg-background-section-burn p-2'> | <div className='flex flex-wrap content-start items-start gap-1 self-stretch rounded-2xl bg-background-section-burn p-2'> | ||||
| <Card | <Card |
| 'use client' | 'use client' | ||||
| import type { FC } from 'react' | import type { FC } from 'react' | ||||
| import React, { useEffect } from 'react' | |||||
| import React, { useEffect, useMemo } from 'react' | |||||
| // import { RiInformation2Line } from '@remixicon/react' | // import { RiInformation2Line } from '@remixicon/react' | ||||
| import { type Plugin, type PluginManifestInMarket, TaskStatus } from '../../../types' | import { type Plugin, type PluginManifestInMarket, TaskStatus } from '../../../types' | ||||
| import Card from '../../../card' | import Card from '../../../card' | ||||
| import Button from '@/app/components/base/button' | import Button from '@/app/components/base/button' | ||||
| import { useTranslation } from 'react-i18next' | import { useTranslation } from 'react-i18next' | ||||
| import { RiLoader2Line } from '@remixicon/react' | 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 checkTaskStatus from '../../base/check-task-status' | ||||
| import useCheckInstalled from '@/app/components/plugins/install-plugin/hooks/use-check-installed' | import useCheckInstalled from '@/app/components/plugins/install-plugin/hooks/use-check-installed' | ||||
| import Version from '../../base/version' | import Version from '../../base/version' | ||||
| import { usePluginTaskList } from '@/service/use-plugins' | import { usePluginTaskList } from '@/service/use-plugins' | ||||
| import { gte } from 'semver' | |||||
| import { useAppContext } from '@/context/app-context' | |||||
| const i18nPrefix = 'plugin.installModal' | const i18nPrefix = 'plugin.installModal' | ||||
| } | } | ||||
| } | } | ||||
| 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 ( | return ( | ||||
| <> | <> | ||||
| <div className='flex flex-col items-start justify-center gap-4 self-stretch px-6 py-3'> | <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'> | <div className='system-md-regular text-text-secondary'> | ||||
| <p>{t(`${i18nPrefix}.readyToInstall`)}</p> | <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> | ||||
| <div className='flex flex-wrap content-start items-start gap-1 self-stretch rounded-2xl bg-background-section-burn p-2'> | <div className='flex flex-wrap content-start items-start gap-1 self-stretch rounded-2xl bg-background-section-burn p-2'> | ||||
| <Card | <Card |
| import { | import { | ||||
| RiArrowRightUpLine, | RiArrowRightUpLine, | ||||
| RiBugLine, | RiBugLine, | ||||
| RiErrorWarningLine, | |||||
| RiHardDrive3Line, | RiHardDrive3Line, | ||||
| RiLoginCircleLine, | RiLoginCircleLine, | ||||
| RiVerifiedBadgeLine, | RiVerifiedBadgeLine, | ||||
| import { useSingleCategories } from '../hooks' | import { useSingleCategories } from '../hooks' | ||||
| import { useRenderI18nObject } from '@/hooks/use-i18n' | import { useRenderI18nObject } from '@/hooks/use-i18n' | ||||
| import useRefreshPluginList from '@/app/components/plugins/install-plugin/hooks/use-refresh-plugin-list' | 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 = { | type Props = { | ||||
| className?: string | className?: string | ||||
| meta, | meta, | ||||
| plugin_id, | plugin_id, | ||||
| } = plugin | } = 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(() => { | const orgName = useMemo(() => { | ||||
| return [PluginSource.github, PluginSource.marketplace].includes(source) ? author : '' | return [PluginSource.github, PluginSource.marketplace].includes(source) ? author : '' | ||||
| }, [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 = () => { | const handleDelete = () => { | ||||
| refreshPluginList({ category } as any) | refreshPluginList({ category } as any) | ||||
| } | } | ||||
| <div className="flex h-5 items-center"> | <div className="flex h-5 items-center"> | ||||
| <Title title={title} /> | <Title title={title} /> | ||||
| {verified && <RiVerifiedBadgeLine className="ml-0.5 h-4 w-4 shrink-0 text-text-accent" />} | {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} /> | <Badge className='ml-1 shrink-0' text={source === PluginSource.github ? plugin.meta!.version : plugin.version} /> | ||||
| </div> | </div> | ||||
| <div className='flex items-center justify-between'> | <div className='flex items-center justify-between'> |
| hook_id: string | hook_id: string | ||||
| } | } | ||||
| export type PluginDeclarationMeta = { | |||||
| version: string | |||||
| minimum_dify_version?: string | |||||
| } | |||||
| // Plugin manifest | // Plugin manifest | ||||
| export type PluginDeclaration = { | export type PluginDeclaration = { | ||||
| plugin_unique_identifier: string | plugin_unique_identifier: string | ||||
| model: any | model: any | ||||
| tags: string[] | tags: string[] | ||||
| agent_strategy: any | agent_strategy: any | ||||
| meta: PluginDeclarationMeta | |||||
| } | } | ||||
| export type PluginManifestInMarket = { | export type PluginManifestInMarket = { |
| clearAll: 'Clear all', | clearAll: 'Clear all', | ||||
| }, | }, | ||||
| submitPlugin: 'Submit plugin', | 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 | export default translation |
| installPlugin: 'プラグインをインストールする', | installPlugin: 'プラグインをインストールする', | ||||
| searchInMarketplace: 'マーケットプレイスで検索', | searchInMarketplace: 'マーケットプレイスで検索', | ||||
| submitPlugin: 'プラグインを提出する', | submitPlugin: 'プラグインを提出する', | ||||
| difyVersionNotCompatible: '現在のDifyバージョンはこのプラグインと互換性がありません。最小バージョンは{{minimalDifyVersion}}です。', | |||||
| } | } | ||||
| export default translation | export default translation |
| clearAll: '清除所有', | clearAll: '清除所有', | ||||
| }, | }, | ||||
| submitPlugin: '上传插件', | submitPlugin: '上传插件', | ||||
| difyVersionNotCompatible: '当前 Dify 版本不兼容该插件,其最低版本要求为 {{minimalDifyVersion}}', | |||||
| } | } | ||||
| export default translation | export default translation |
| PackageDependency, | PackageDependency, | ||||
| Permissions, | Permissions, | ||||
| Plugin, | Plugin, | ||||
| PluginDeclaration, | |||||
| PluginDetail, | PluginDetail, | ||||
| PluginInfoFromMarketPlace, | PluginInfoFromMarketPlace, | ||||
| PluginTask, | PluginTask, | ||||
| }) | }) | ||||
| } | } | ||||
| 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) => { | export const useVersionListOfPlugin = (pluginID: string) => { | ||||
| return useQuery<{ data: VersionListResponse }>({ | return useQuery<{ data: VersionListResponse }>({ | ||||
| enabled: !!pluginID, | enabled: !!pluginID, |