mirror of
https://github.com/langgenius/dify.git
synced 2026-02-09 23:20:12 -05:00
feat: add batch upgrade API and UI for marketplace plugins
This commit is contained in:
@@ -578,6 +578,25 @@ class PluginUpgradeFromGithubApi(Resource):
|
||||
raise ValueError(e)
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/current/plugin/upgrade/batch")
|
||||
class PluginBatchUpgradeApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@plugin_permission_required(install_required=True)
|
||||
def post(self):
|
||||
"""
|
||||
Batch upgrade all marketplace plugins that have updates available
|
||||
"""
|
||||
_, tenant_id = current_account_with_tenant()
|
||||
|
||||
try:
|
||||
result = PluginService.batch_upgrade_plugins_from_marketplace(tenant_id)
|
||||
return jsonable_encoder(result)
|
||||
except PluginDaemonClientSideError as e:
|
||||
raise ValueError(e)
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/current/plugin/uninstall")
|
||||
class PluginUninstallApi(Resource):
|
||||
@console_ns.expect(console_ns.models[ParserUninstall.__name__])
|
||||
|
||||
@@ -337,6 +337,91 @@ class PluginService:
|
||||
},
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def batch_upgrade_plugins_from_marketplace(tenant_id: str) -> dict[str, dict]:
|
||||
"""
|
||||
Batch upgrade all marketplace plugins that have updates available
|
||||
|
||||
Returns a dict with:
|
||||
- success: list of successfully upgraded plugins
|
||||
- failed: list of failed upgrades with error messages
|
||||
- skipped: list of plugins skipped (no updates or errors)
|
||||
"""
|
||||
if not dify_config.MARKETPLACE_ENABLED:
|
||||
raise ValueError("marketplace is not enabled")
|
||||
|
||||
manager = PluginInstaller()
|
||||
result = {
|
||||
"success": [],
|
||||
"failed": [],
|
||||
"skipped": [],
|
||||
}
|
||||
|
||||
# Get all installed plugins
|
||||
plugins = manager.list_plugins(tenant_id)
|
||||
|
||||
# Filter marketplace plugins only
|
||||
marketplace_plugins = [plugin for plugin in plugins if plugin.source == PluginInstallationSource.Marketplace]
|
||||
|
||||
if not marketplace_plugins:
|
||||
return result
|
||||
|
||||
# Get latest versions for all marketplace plugins
|
||||
plugin_ids = [plugin.plugin_id for plugin in marketplace_plugins]
|
||||
latest_versions = PluginService.fetch_latest_plugin_version(plugin_ids)
|
||||
|
||||
# Upgrade each plugin if newer version is available
|
||||
for plugin in marketplace_plugins:
|
||||
try:
|
||||
latest_info = latest_versions.get(plugin.plugin_id)
|
||||
if not latest_info:
|
||||
result["skipped"].append(
|
||||
{
|
||||
"plugin_id": plugin.plugin_id,
|
||||
"reason": "no_update_info",
|
||||
"current_version": plugin.version,
|
||||
}
|
||||
)
|
||||
continue
|
||||
|
||||
# Check if update is needed
|
||||
if latest_info.version == plugin.version:
|
||||
result["skipped"].append(
|
||||
{
|
||||
"plugin_id": plugin.plugin_id,
|
||||
"reason": "already_latest",
|
||||
"current_version": plugin.version,
|
||||
}
|
||||
)
|
||||
continue
|
||||
|
||||
# Perform upgrade
|
||||
PluginService.upgrade_plugin_with_marketplace(
|
||||
tenant_id, plugin.plugin_unique_identifier, latest_info.unique_identifier
|
||||
)
|
||||
|
||||
result["success"].append(
|
||||
{
|
||||
"plugin_id": plugin.plugin_id,
|
||||
"from_version": plugin.version,
|
||||
"to_version": latest_info.version,
|
||||
"from_identifier": plugin.plugin_unique_identifier,
|
||||
"to_identifier": latest_info.unique_identifier,
|
||||
}
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.exception("Failed to upgrade plugin %s", plugin.plugin_id)
|
||||
result["failed"].append(
|
||||
{
|
||||
"plugin_id": plugin.plugin_id,
|
||||
"current_version": plugin.version,
|
||||
"error": str(e),
|
||||
}
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def upload_pkg(tenant_id: str, pkg: bytes, verify_signature: bool = False) -> PluginDecodeResponse:
|
||||
"""
|
||||
|
||||
@@ -5,14 +5,16 @@ import {
|
||||
RiBookOpenLine,
|
||||
RiDragDropLine,
|
||||
RiEqualizer2Line,
|
||||
RiRefreshLine,
|
||||
} from '@remixicon/react'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import Link from 'next/link'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Button from '@/app/components/base/button'
|
||||
import TabSlider from '@/app/components/base/tab-slider'
|
||||
import { useToastContext } from '@/app/components/base/toast'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import ReferenceSettingModal from '@/app/components/plugins/reference-setting-modal'
|
||||
import { MARKETPLACE_API_PREFIX, SUPPORT_INSTALL_LOCAL_FILE_EXTENSIONS } from '@/config'
|
||||
@@ -20,7 +22,8 @@ import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
import { useDocLink } from '@/context/i18n'
|
||||
import useDocumentTitle from '@/hooks/use-document-title'
|
||||
import { usePluginInstallation } from '@/hooks/use-query-params'
|
||||
import { fetchBundleInfoFromMarketPlace, fetchManifestFromMarketPlace } from '@/service/plugins'
|
||||
import { batchUpgradePlugins, fetchBundleInfoFromMarketPlace, fetchManifestFromMarketPlace } from '@/service/plugins'
|
||||
import { useInvalidateInstalledPluginList } from '@/service/use-plugins'
|
||||
import { sleep } from '@/utils'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { PLUGIN_PAGE_TABS_MAP } from '../hooks'
|
||||
@@ -48,6 +51,8 @@ const PluginPage = ({
|
||||
const { t } = useTranslation()
|
||||
const docLink = useDocLink()
|
||||
useDocumentTitle(t('metadata.title', { ns: 'plugin' }))
|
||||
const { notify } = useToastContext()
|
||||
const invalidateInstalledPluginList = useInvalidateInstalledPluginList()
|
||||
|
||||
// Use nuqs hook for installation state
|
||||
const [{ packageId, bundleInfo }, setInstallState] = usePluginInstallation()
|
||||
@@ -60,6 +65,9 @@ const PluginPage = ({
|
||||
setFalse: doHideInstallFromMarketplace,
|
||||
}] = useBoolean(false)
|
||||
|
||||
const [isBatchUpgrading, setIsBatchUpgrading] = useState(false)
|
||||
const [showBatchUpgradeTooltip, setShowBatchUpgradeTooltip] = useState(true)
|
||||
|
||||
const hideInstallFromMarketplace = () => {
|
||||
doHideInstallFromMarketplace()
|
||||
setInstallState(null)
|
||||
@@ -134,6 +142,45 @@ const PluginPage = ({
|
||||
enabled: isPluginsTab && canManagement,
|
||||
})
|
||||
|
||||
const handleBatchUpgrade = useCallback(async () => {
|
||||
// Hide tooltip immediately when clicked
|
||||
setShowBatchUpgradeTooltip(false)
|
||||
setIsBatchUpgrading(true)
|
||||
try {
|
||||
const result = await batchUpgradePlugins()
|
||||
const { success, failed, skipped } = result
|
||||
|
||||
// If there are updates (success or failed), show submitted message
|
||||
if (success.length > 0 || failed.length > 0) {
|
||||
notify({
|
||||
type: 'success',
|
||||
message: t('batchUpgrade.submittedMessage', { ns: 'plugin' }),
|
||||
})
|
||||
}
|
||||
// If all plugins are already up to date (only skipped)
|
||||
else if (skipped.length > 0) {
|
||||
notify({
|
||||
type: 'info',
|
||||
message: t('batchUpgrade.noUpdatesMessage', { ns: 'plugin' }),
|
||||
})
|
||||
}
|
||||
|
||||
invalidateInstalledPluginList()
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Failed to batch upgrade plugins:', error)
|
||||
notify({
|
||||
type: 'error',
|
||||
message: t('batchUpgrade.errorMessage', { ns: 'plugin' }),
|
||||
})
|
||||
}
|
||||
finally {
|
||||
setIsBatchUpgrading(false)
|
||||
// Re-enable tooltip after a short delay
|
||||
setTimeout(() => setShowBatchUpgradeTooltip(true), 500)
|
||||
}
|
||||
}, [t, notify, invalidateInstalledPluginList])
|
||||
|
||||
const { dragging, fileUploader, fileChangeHandle, removeFile } = uploaderProps
|
||||
return (
|
||||
<div
|
||||
@@ -189,6 +236,27 @@ const PluginPage = ({
|
||||
</>
|
||||
)
|
||||
}
|
||||
{
|
||||
isPluginsTab && canManagement && (
|
||||
<>
|
||||
<Tooltip
|
||||
popupContent={t('batchUpgrade.tooltip', { ns: 'plugin' })}
|
||||
disabled={!showBatchUpgradeTooltip}
|
||||
>
|
||||
<Button
|
||||
variant="secondary-accent"
|
||||
className="px-3"
|
||||
onClick={handleBatchUpgrade}
|
||||
disabled={isBatchUpgrading}
|
||||
>
|
||||
<RiRefreshLine className={cn('mr-1 h-4 w-4', isBatchUpgrading && 'animate-spin')} />
|
||||
{t('batchUpgrade.button', { ns: 'plugin' })}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<div className="mx-1 h-3.5 w-[1px] shrink-0 bg-divider-regular"></div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
<PluginTasks />
|
||||
{canManagement && (
|
||||
<InstallPluginDropdown
|
||||
|
||||
@@ -63,6 +63,11 @@
|
||||
"autoUpdate.upgradeMode.partial": "Only selected",
|
||||
"autoUpdate.upgradeModePlaceholder.exclude": "Selected plugins will not auto-update",
|
||||
"autoUpdate.upgradeModePlaceholder.partial": "Only selected plugins will auto-update. No plugins are currently selected, so no plugins will auto-update.",
|
||||
"batchUpgrade.button": "Update All",
|
||||
"batchUpgrade.errorMessage": "Failed to submit plugin update task. Please try again.",
|
||||
"batchUpgrade.noUpdatesMessage": "All plugins are up to date",
|
||||
"batchUpgrade.submittedMessage": "Plugin update task submitted",
|
||||
"batchUpgrade.tooltip": "Update all plugins installed from Marketplace to the latest version",
|
||||
"category.agents": "Agent Strategies",
|
||||
"category.all": "All",
|
||||
"category.bundles": "Bundles",
|
||||
|
||||
@@ -63,6 +63,11 @@
|
||||
"autoUpdate.upgradeMode.partial": "仅选定",
|
||||
"autoUpdate.upgradeModePlaceholder.exclude": "选定的插件将不会自动更新",
|
||||
"autoUpdate.upgradeModePlaceholder.partial": "仅选定的插件将自动更新。目前未选择任何插件,因此不会自动更新任何插件。",
|
||||
"batchUpgrade.button": "全部更新",
|
||||
"batchUpgrade.errorMessage": "提交插件更新任务失败,请重试",
|
||||
"batchUpgrade.noUpdatesMessage": "所有插件已是最新版本",
|
||||
"batchUpgrade.submittedMessage": "已提交插件更新任务",
|
||||
"batchUpgrade.tooltip": "将所有从 Marketplace 安装的插件更新到最新版本",
|
||||
"category.agents": "Agent 策略",
|
||||
"category.all": "全部",
|
||||
"category.bundles": "插件集",
|
||||
|
||||
@@ -104,3 +104,27 @@ export const updatePermission = async (permissions: Permissions) => {
|
||||
export const uninstallPlugin = async (pluginId: string) => {
|
||||
return post<UninstallPluginResponse>('/workspaces/current/plugin/uninstall', { body: { plugin_installation_id: pluginId } })
|
||||
}
|
||||
|
||||
export const batchUpgradePlugins = async () => {
|
||||
return post<{
|
||||
success: Array<{
|
||||
plugin_id: string
|
||||
from_version: string
|
||||
to_version: string
|
||||
from_identifier: string
|
||||
to_identifier: string
|
||||
}>
|
||||
failed: Array<{
|
||||
plugin_id: string
|
||||
current_version: string
|
||||
error: string
|
||||
}>
|
||||
skipped: Array<{
|
||||
plugin_id: string
|
||||
reason: string
|
||||
current_version: string
|
||||
}>
|
||||
}>('/workspaces/current/plugin/upgrade/batch', {
|
||||
body: {},
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user