diff --git a/api/configs/feature/__init__.py b/api/configs/feature/__init__.py index bd58132839..f5262a2f8b 100644 --- a/api/configs/feature/__init__.py +++ b/api/configs/feature/__init__.py @@ -196,7 +196,7 @@ class AsyncWorkflowConfig(BaseSettings): "to avoid this, workflow can be suspended if needed, to achieve" "this, a time-based checker is required, every granularity seconds, " "the checker will check the workflow queue and suspend the workflow", - default=1, + default=60, ge=1, ) diff --git a/api/core/app/engine_layers/suspend_layer.py b/api/core/app/engine_layers/suspend_layer.py index f981523149..0a107de012 100644 --- a/api/core/app/engine_layers/suspend_layer.py +++ b/api/core/app/engine_layers/suspend_layer.py @@ -1,5 +1,6 @@ from core.workflow.graph_engine.layers.base import GraphEngineLayer from core.workflow.graph_events.base import GraphEngineEvent +from core.workflow.graph_events.graph import GraphRunPausedEvent class SuspendLayer(GraphEngineLayer): @@ -9,7 +10,11 @@ class SuspendLayer(GraphEngineLayer): pass def on_event(self, event: GraphEngineEvent): - pass + """ + Handle the paused event, stash runtime state into storage and wait for resume. + """ + if isinstance(event, GraphRunPausedEvent): + pass def on_graph_end(self, error: Exception | None): """ """ diff --git a/api/core/app/engine_layers/timeslice_layer.py b/api/core/app/engine_layers/timeslice_layer.py index de5129de55..8d4491b93c 100644 --- a/api/core/app/engine_layers/timeslice_layer.py +++ b/api/core/app/engine_layers/timeslice_layer.py @@ -4,7 +4,6 @@ from typing import ClassVar from apscheduler.schedulers.background import BackgroundScheduler # type: ignore -from configs import dify_config from core.workflow.graph_engine.entities.commands import CommandType, GraphEngineCommand from core.workflow.graph_engine.layers.base import GraphEngineLayer from core.workflow.graph_events.base import GraphEngineEvent @@ -13,7 +12,7 @@ from services.workflow.scheduler import CFSPlanScheduler, SchedulerCommand logger = logging.getLogger(__name__) -class TimesliceLayer(GraphEngineLayer): +class TimeSliceLayer(GraphEngineLayer): """ CFS plan scheduler to control the timeslice of the workflow. """ @@ -25,13 +24,43 @@ class TimesliceLayer(GraphEngineLayer): CFS plan scheduler allows to control the timeslice of the workflow. """ - if not TimesliceLayer.scheduler.running: - TimesliceLayer.scheduler.start() + if not TimeSliceLayer.scheduler.running: + TimeSliceLayer.scheduler.start() super().__init__() self.cfs_plan_scheduler = cfs_plan_scheduler self.stopped = False + def _checker_job(self, schedule_id: str): + """ + Check if the workflow need to be suspended. + """ + try: + if self.stopped: + self.scheduler.remove_job(schedule_id) + return + + if self.cfs_plan_scheduler.can_schedule() == SchedulerCommand.RESOURCE_LIMIT_REACHED: + # remove the job + self.scheduler.remove_job(schedule_id) + + if not self.command_channel: + logger.exception("No command channel to stop the workflow") + return + + # send command to pause the workflow + self.command_channel.send_command( + GraphEngineCommand( + command_type=CommandType.PAUSE, + payload={ + "reason": SchedulerCommand.RESOURCE_LIMIT_REACHED, + }, + ) + ) + + except Exception: + logger.exception("scheduler error during check if the workflow need to be suspended") + def on_graph_start(self): """ Start timer to check if the workflow need to be suspended. @@ -39,39 +68,11 @@ class TimesliceLayer(GraphEngineLayer): schedule_id = uuid.uuid4().hex - def runner(): - """ - Whenever the workflow is running, keep checking if we need to suspend it. - Otherwise, return directly. - """ - try: - if self.stopped: - self.scheduler.remove_job(schedule_id) - return - - if self.cfs_plan_scheduler.can_schedule() == SchedulerCommand.RESOURCE_LIMIT_REACHED: - # remove the job - self.scheduler.remove_job(schedule_id) - - if not self.command_channel: - logger.exception("No command channel to stop the workflow") - return - - # send command to pause the workflow - self.command_channel.send_command( - GraphEngineCommand( - command_type=CommandType.PAUSE, - payload={ - "reason": SchedulerCommand.RESOURCE_LIMIT_REACHED, - }, - ) - ) - - except Exception: - logger.exception("scheduler error during check if the workflow need to be suspended") - self.scheduler.add_job( - runner, "interval", seconds=dify_config.ASYNC_WORKFLOW_SCHEDULER_GRANULARITY, id=schedule_id + lambda: self._checker_job(schedule_id), + "interval", + seconds=self.cfs_plan_scheduler.plan.granularity, + id=schedule_id, ) def on_event(self, event: GraphEngineEvent): diff --git a/api/core/app/engine_layers/trigger_post_layer.py b/api/core/app/engine_layers/trigger_post_layer.py index 5b31ec2ae1..1309295b1a 100644 --- a/api/core/app/engine_layers/trigger_post_layer.py +++ b/api/core/app/engine_layers/trigger_post_layer.py @@ -11,7 +11,7 @@ from core.workflow.graph_events.graph import GraphRunFailedEvent, GraphRunPaused from models.engine import db from models.enums import WorkflowTriggerStatus from repositories.sqlalchemy_workflow_trigger_log_repository import SQLAlchemyWorkflowTriggerLogRepository -from tasks.workflow_cfs_scheduler.cfs_scheduler import TriggerWorkflowCFSPlanEntity +from tasks.workflow_cfs_scheduler.cfs_scheduler import AsyncWorkflowCFSPlanEntity logger = logging.getLogger(__name__) @@ -29,7 +29,7 @@ class TriggerPostLayer(GraphEngineLayer): def __init__( self, - cfs_plan_scheduler_entity: TriggerWorkflowCFSPlanEntity, + cfs_plan_scheduler_entity: AsyncWorkflowCFSPlanEntity, start_time: datetime, trigger_log_id: str, ): diff --git a/api/tasks/async_workflow_tasks.py b/api/tasks/async_workflow_tasks.py index 14b289a6a7..cce0256a42 100644 --- a/api/tasks/async_workflow_tasks.py +++ b/api/tasks/async_workflow_tasks.py @@ -12,8 +12,9 @@ from celery import shared_task from sqlalchemy import select from sqlalchemy.orm import Session, sessionmaker +from configs import dify_config from core.app.apps.workflow.app_generator import WorkflowAppGenerator -from core.app.engine_layers.timeslice_layer import TimesliceLayer +from core.app.engine_layers.timeslice_layer import TimeSliceLayer from core.app.engine_layers.trigger_post_layer import TriggerPostLayer from core.app.entities.app_invoke_entities import InvokeFrom from extensions.ext_database import db @@ -29,7 +30,7 @@ from services.workflow.entities import ( WorkflowScheduleCFSPlanEntity, WorkflowTaskData, ) -from tasks.workflow_cfs_scheduler.cfs_scheduler import TriggerCFSPlanScheduler, TriggerWorkflowCFSPlanEntity +from tasks.workflow_cfs_scheduler.cfs_scheduler import AsyncWorkflowCFSPlanEntity, AsyncWorkflowCFSPlanScheduler from tasks.workflow_cfs_scheduler.entities import AsyncWorkflowQueue @@ -37,12 +38,14 @@ from tasks.workflow_cfs_scheduler.entities import AsyncWorkflowQueue def execute_workflow_professional(task_data_dict: dict[str, Any]): """Execute workflow for professional tier with highest priority""" task_data = WorkflowTaskData.model_validate(task_data_dict) - cfs_plan_scheduler_entity = TriggerWorkflowCFSPlanEntity( - queue=AsyncWorkflowQueue.PROFESSIONAL_QUEUE, schedule_strategy=WorkflowScheduleCFSPlanEntity.Strategy.TimeSlice + cfs_plan_scheduler_entity = AsyncWorkflowCFSPlanEntity( + queue=AsyncWorkflowQueue.PROFESSIONAL_QUEUE, + schedule_strategy=WorkflowScheduleCFSPlanEntity.Strategy.TimeSlice, + granularity=dify_config.ASYNC_WORKFLOW_SCHEDULER_GRANULARITY, ) _execute_workflow_common( task_data, - TriggerCFSPlanScheduler(plan=cfs_plan_scheduler_entity), + AsyncWorkflowCFSPlanScheduler(plan=cfs_plan_scheduler_entity), cfs_plan_scheduler_entity, ) @@ -51,12 +54,14 @@ def execute_workflow_professional(task_data_dict: dict[str, Any]): def execute_workflow_team(task_data_dict: dict[str, Any]): """Execute workflow for team tier""" task_data = WorkflowTaskData.model_validate(task_data_dict) - cfs_plan_scheduler_entity = TriggerWorkflowCFSPlanEntity( - queue=AsyncWorkflowQueue.TEAM_QUEUE, schedule_strategy=WorkflowScheduleCFSPlanEntity.Strategy.TimeSlice + cfs_plan_scheduler_entity = AsyncWorkflowCFSPlanEntity( + queue=AsyncWorkflowQueue.TEAM_QUEUE, + schedule_strategy=WorkflowScheduleCFSPlanEntity.Strategy.TimeSlice, + granularity=dify_config.ASYNC_WORKFLOW_SCHEDULER_GRANULARITY, ) _execute_workflow_common( task_data, - TriggerCFSPlanScheduler(plan=cfs_plan_scheduler_entity), + AsyncWorkflowCFSPlanScheduler(plan=cfs_plan_scheduler_entity), cfs_plan_scheduler_entity, ) @@ -65,20 +70,22 @@ def execute_workflow_team(task_data_dict: dict[str, Any]): def execute_workflow_sandbox(task_data_dict: dict[str, Any]): """Execute workflow for free tier with lower retry limit""" task_data = WorkflowTaskData.model_validate(task_data_dict) - cfs_plan_scheduler_entity = TriggerWorkflowCFSPlanEntity( - queue=AsyncWorkflowQueue.SANDBOX_QUEUE, schedule_strategy=WorkflowScheduleCFSPlanEntity.Strategy.TimeSlice + cfs_plan_scheduler_entity = AsyncWorkflowCFSPlanEntity( + queue=AsyncWorkflowQueue.SANDBOX_QUEUE, + schedule_strategy=WorkflowScheduleCFSPlanEntity.Strategy.TimeSlice, + granularity=dify_config.ASYNC_WORKFLOW_SCHEDULER_GRANULARITY, ) _execute_workflow_common( task_data, - TriggerCFSPlanScheduler(plan=cfs_plan_scheduler_entity), + AsyncWorkflowCFSPlanScheduler(plan=cfs_plan_scheduler_entity), cfs_plan_scheduler_entity, ) def _execute_workflow_common( task_data: WorkflowTaskData, - cfs_plan_scheduler: TriggerCFSPlanScheduler, - cfs_plan_scheduler_entity: TriggerWorkflowCFSPlanEntity, + cfs_plan_scheduler: AsyncWorkflowCFSPlanScheduler, + cfs_plan_scheduler_entity: AsyncWorkflowCFSPlanEntity, ): """Execute workflow with common logic and trigger log updates.""" @@ -140,7 +147,7 @@ def _execute_workflow_common( triggered_from=trigger_data.trigger_type, root_node_id=trigger_data.root_node_id, layers=[ - TimesliceLayer(cfs_plan_scheduler), + TimeSliceLayer(cfs_plan_scheduler), TriggerPostLayer(cfs_plan_scheduler_entity, start_time, trigger_log.id), ], ) diff --git a/api/tasks/workflow_cfs_scheduler/cfs_scheduler.py b/api/tasks/workflow_cfs_scheduler/cfs_scheduler.py index 2a4153dcd2..218e61f6d9 100644 --- a/api/tasks/workflow_cfs_scheduler/cfs_scheduler.py +++ b/api/tasks/workflow_cfs_scheduler/cfs_scheduler.py @@ -3,7 +3,7 @@ from services.workflow.scheduler import CFSPlanScheduler, SchedulerCommand from tasks.workflow_cfs_scheduler.entities import AsyncWorkflowQueue -class TriggerWorkflowCFSPlanEntity(WorkflowScheduleCFSPlanEntity): +class AsyncWorkflowCFSPlanEntity(WorkflowScheduleCFSPlanEntity): """ Trigger workflow CFS plan entity. """ @@ -11,23 +11,22 @@ class TriggerWorkflowCFSPlanEntity(WorkflowScheduleCFSPlanEntity): queue: AsyncWorkflowQueue -class TriggerCFSPlanScheduler(CFSPlanScheduler): +class AsyncWorkflowCFSPlanScheduler(CFSPlanScheduler): """ Trigger workflow CFS plan scheduler. """ + plan: AsyncWorkflowCFSPlanEntity + def can_schedule(self) -> SchedulerCommand: """ Check if the workflow can be scheduled. """ - assert isinstance(self.plan, TriggerWorkflowCFSPlanEntity) - if self.plan.queue in [AsyncWorkflowQueue.PROFESSIONAL_QUEUE, AsyncWorkflowQueue.TEAM_QUEUE]: """ - permitted all paid users to schedule the workflow + permitted all paid users to schedule the workflow any time """ return SchedulerCommand.NONE # FIXME: avoid the sandbox user's workflow at a running state for ever - - return SchedulerCommand.NONE + return SchedulerCommand.RESOURCE_LIMIT_REACHED diff --git a/web/app/components/workflow/block-selector/all-tools.tsx b/web/app/components/workflow/block-selector/all-tools.tsx index dabb8f4a9a..d473ddb541 100644 --- a/web/app/components/workflow/block-selector/all-tools.tsx +++ b/web/app/components/workflow/block-selector/all-tools.tsx @@ -25,10 +25,12 @@ import { SearchMenu } from '@/app/components/base/icons/src/vender/line/general' import { useGetLanguage } from '@/context/i18n' import type { ListRef } from '@/app/components/workflow/block-selector/market-place-plugin/list' import PluginList, { type ListProps } from '@/app/components/workflow/block-selector/market-place-plugin/list' +import type { Plugin } from '../../plugins/types' import { PluginCategoryEnum } from '../../plugins/types' import { useMarketplacePlugins } from '../../plugins/marketplace/hooks' import { useGlobalPublicStore } from '@/context/global-public-context' import RAGToolSuggestions from './rag-tool-suggestions' +import FeaturedTools from './featured-tools' import Link from 'next/link' type AllToolsProps = { @@ -47,6 +49,10 @@ type AllToolsProps = { canChooseMCPTool?: boolean onTagsChange?: Dispatch> isInRAGPipeline?: boolean + featuredPlugins?: Plugin[] + featuredLoading?: boolean + showFeatured?: boolean + onFeaturedInstallSuccess?: () => Promise | void } const DEFAULT_TAGS: AllToolsProps['tags'] = [] @@ -67,6 +73,10 @@ const AllTools = ({ canChooseMCPTool, onTagsChange, isInRAGPipeline = false, + featuredPlugins = [], + featuredLoading = false, + showFeatured = false, + onFeaturedInstallSuccess, }: AllToolsProps) => { const { t } = useTranslation() const language = useGetLanguage() @@ -80,6 +90,16 @@ const AllTools = ({ const isMatchingKeywords = (text: string, keywords: string) => { return text.toLowerCase().includes(keywords.toLowerCase()) } + const allProviders = useMemo(() => [...buildInTools, ...customTools, ...workflowTools, ...mcpTools], [buildInTools, customTools, workflowTools, mcpTools]) + const providerMap = useMemo(() => { + const map = new Map() + allProviders.forEach((provider) => { + const key = provider.plugin_id || provider.id + if (key) + map.set(key, provider) + }) + return map + }, [allProviders]) const tools = useMemo(() => { let mergedTools: ToolWithProvider[] = [] if (activeTab === ToolTypeEnum.All) @@ -136,6 +156,7 @@ const AllTools = ({ } = useMarketplacePlugins() const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures) + useEffect(() => { if (!enable_marketplace) return if (hasFilter) { @@ -155,6 +176,12 @@ const AllTools = ({ const hasToolsContent = tools.length > 0 const hasPluginContent = enable_marketplace && notInstalledPlugins.length > 0 const shouldShowEmptyState = hasFilter && !hasToolsContent && !hasPluginContent + const shouldShowFeatured = showFeatured + && enable_marketplace + && !isInRAGPipeline + && activeTab === ToolTypeEnum.All + && !hasFilter + && (featuredLoading || featuredPlugins.length > 0) return (
@@ -193,6 +220,19 @@ const AllTools = ({ onTagsChange={onTagsChange} /> )} + {shouldShowFeatured && ( + { + await onFeaturedInstallSuccess?.() + }} + /> + )} +
+ onSelect: (type: BlockEnum, tool: ToolDefaultValue) => void + selectedTools?: ToolValue[] + canChooseMCPTool?: boolean + isLoading?: boolean + onInstallSuccess?: () => void +} + +const STORAGE_KEY = 'workflow_tools_featured_collapsed' + +const FeaturedTools = ({ + plugins, + providerMap, + onSelect, + selectedTools, + canChooseMCPTool, + isLoading = false, + onInstallSuccess, +}: FeaturedToolsProps) => { + const { t } = useTranslation() + const language = useGetLanguage() + const [visibleCount, setVisibleCount] = useState(INITIAL_VISIBLE_COUNT) + const [installingIdentifier, setInstallingIdentifier] = useState(null) + const [isCollapsed, setIsCollapsed] = useState(false) + + const installMutation = useInstallPackageFromMarketPlace({ + onSuccess: () => { + onInstallSuccess?.() + }, + onSettled: () => { + setInstallingIdentifier(null) + }, + }) + + useEffect(() => { + if (typeof window === 'undefined') + return + const stored = window.localStorage.getItem(STORAGE_KEY) + if (stored !== null) + setIsCollapsed(stored === 'true') + }, []) + + useEffect(() => { + if (typeof window === 'undefined') + return + window.localStorage.setItem(STORAGE_KEY, String(isCollapsed)) + }, [isCollapsed]) + + useEffect(() => { + setVisibleCount(INITIAL_VISIBLE_COUNT) + }, [plugins]) + + const visiblePlugins = useMemo( + () => plugins.slice(0, Math.min(MAX_RECOMMENDED_COUNT, visibleCount)), + [plugins, visibleCount], + ) + + const installedProviders = useMemo( + () => + visiblePlugins + .map(plugin => providerMap.get(plugin.plugin_id)) + .filter((provider): provider is ToolWithProvider => Boolean(provider)), + [visiblePlugins, providerMap], + ) + + const uninstalledPlugins = useMemo( + () => visiblePlugins.filter(plugin => !providerMap.has(plugin.plugin_id)), + [visiblePlugins, providerMap], + ) + + const showMore = visibleCount < Math.min(MAX_RECOMMENDED_COUNT, plugins.length) + const isMutating = installMutation.isPending + const showEmptyState = !isLoading && visiblePlugins.length === 0 + + return ( +
+ + + {!isCollapsed && ( + <> + {isLoading && ( +
+ +
+ )} + + {showEmptyState && ( +

+ + {t('workflow.tabs.noFeaturedPlugins')} + +

+ )} + + {!showEmptyState && !isLoading && ( + <> + {installedProviders.length > 0 && ( + + )} + + {uninstalledPlugins.length > 0 && ( +
+ {uninstalledPlugins.map(plugin => ( + { + if (isMutating) + return + setInstallingIdentifier(plugin.latest_package_identifier) + installMutation.mutate(plugin.latest_package_identifier) + }} + t={t} + /> + ))} +
+ )} + + )} + + {!isLoading && visiblePlugins.length > 0 && showMore && ( +
{ + setVisibleCount(count => Math.min(count + INITIAL_VISIBLE_COUNT, MAX_RECOMMENDED_COUNT, plugins.length)) + }} + > +
+ +
+
+ {t('common.operation.more')} +
+
+ )} + + )} +
+ ) +} + +type FeaturedToolUninstalledItemProps = { + plugin: Plugin + language: string + installing: boolean + onInstall: () => void + t: (key: string, options?: Record) => string +} + +function FeaturedToolUninstalledItem({ + plugin, + language, + installing, + onInstall, + t, +}: FeaturedToolUninstalledItemProps) { + const label = plugin.label?.[language] || plugin.name + const description = typeof plugin.brief === 'object' ? plugin.brief[language] : plugin.brief + const installCountLabel = t('plugin.install', { num: plugin.install_count?.toLocaleString() ?? 0 }) + + return ( +
+
+ +
+
{label}
+ {description && ( +
{description}
+ )} +
+
+
+ {installCountLabel} + +
+
+ ) +} + +export default FeaturedTools diff --git a/web/app/components/workflow/block-selector/tabs.tsx b/web/app/components/workflow/block-selector/tabs.tsx index 871ab3405a..17de4b74d2 100644 --- a/web/app/components/workflow/block-selector/tabs.tsx +++ b/web/app/components/workflow/block-selector/tabs.tsx @@ -1,6 +1,6 @@ import type { Dispatch, FC, SetStateAction } from 'react' import { memo } from 'react' -import { useAllBuiltInTools, useAllCustomTools, useAllMCPTools, useAllWorkflowTools } from '@/service/use-tools' +import { useAllBuiltInTools, useAllCustomTools, useAllMCPTools, useAllWorkflowTools, useInvalidateAllBuiltInTools } from '@/service/use-tools' import type { BlockEnum, NodeDefault, @@ -13,6 +13,8 @@ import AllStartBlocks from './all-start-blocks' import AllTools from './all-tools' import DataSources from './data-sources' import cn from '@/utils/classnames' +import { useFeaturedToolsRecommendations } from '@/service/use-plugins' +import { useGlobalPublicStore } from '@/context/global-public-context' export type TabsProps = { activeTab: TabsEnum @@ -53,6 +55,13 @@ const Tabs: FC = ({ const { data: customTools } = useAllCustomTools() const { data: workflowTools } = useAllWorkflowTools() const { data: mcpTools } = useAllMCPTools() + const invalidateBuiltInTools = useInvalidateAllBuiltInTools() + const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures) + const inRAGPipeline = dataSources.length > 0 + const { + plugins: featuredPlugins = [], + isLoading: isFeaturedLoading, + } = useFeaturedToolsRecommendations(enable_marketplace && !inRAGPipeline) return (
e.stopPropagation()}> @@ -127,7 +136,13 @@ const Tabs: FC = ({ mcpTools={mcpTools || []} canChooseMCPTool onTagsChange={onTagsChange} - isInRAGPipeline={dataSources.length > 0} + isInRAGPipeline={inRAGPipeline} + featuredPlugins={featuredPlugins} + featuredLoading={isFeaturedLoading} + showFeatured={enable_marketplace && !inRAGPipeline} + onFeaturedInstallSuccess={async () => { + invalidateBuiltInTools() + }} /> ) } diff --git a/web/app/components/workflow/block-selector/tool-picker.tsx b/web/app/components/workflow/block-selector/tool-picker.tsx index ae4b0d4f02..c438e45042 100644 --- a/web/app/components/workflow/block-selector/tool-picker.tsx +++ b/web/app/components/workflow/block-selector/tool-picker.tsx @@ -23,7 +23,9 @@ import { } from '@/service/tools' import type { CustomCollectionBackend } from '@/app/components/tools/types' import Toast from '@/app/components/base/toast' -import { useAllBuiltInTools, useAllCustomTools, useAllMCPTools, useAllWorkflowTools, useInvalidateAllCustomTools } from '@/service/use-tools' +import { useAllBuiltInTools, useAllCustomTools, useAllMCPTools, useAllWorkflowTools, useInvalidateAllBuiltInTools, useInvalidateAllCustomTools } from '@/service/use-tools' +import { useFeaturedToolsRecommendations } from '@/service/use-plugins' +import { useGlobalPublicStore } from '@/context/global-public-context' import cn from '@/utils/classnames' type Props = { @@ -61,11 +63,18 @@ const ToolPicker: FC = ({ const [searchText, setSearchText] = useState('') const [tags, setTags] = useState([]) + const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures) const { data: buildInTools } = useAllBuiltInTools() const { data: customTools } = useAllCustomTools() const invalidateCustomTools = useInvalidateAllCustomTools() const { data: workflowTools } = useAllWorkflowTools() const { data: mcpTools } = useAllMCPTools() + const invalidateBuiltInTools = useInvalidateAllBuiltInTools() + + const { + plugins: featuredPlugins = [], + isLoading: isFeaturedLoading, + } = useFeaturedToolsRecommendations(enable_marketplace) const { builtinToolList, customToolList, workflowToolList } = useMemo(() => { if (scope === 'plugins') { @@ -179,6 +188,12 @@ const ToolPicker: FC = ({ selectedTools={selectedTools} canChooseMCPTool={canChooseMCPTool} onTagsChange={setTags} + featuredPlugins={featuredPlugins} + featuredLoading={isFeaturedLoading} + showFeatured={scope === 'all' && enable_marketplace} + onFeaturedInstallSuccess={async () => { + invalidateBuiltInTools() + }} />
diff --git a/web/i18n/de-DE/workflow.ts b/web/i18n/de-DE/workflow.ts index 8b1bb5dd5a..fd9ad92dae 100644 --- a/web/i18n/de-DE/workflow.ts +++ b/web/i18n/de-DE/workflow.ts @@ -309,7 +309,7 @@ const translation = { }, panel: { userInputField: 'Benutzereingabefeld', - helpLink: 'Hilfelink', + helpLink: 'Hilfe', about: 'Über', createdBy: 'Erstellt von ', nextStep: 'Nächster Schritt', diff --git a/web/i18n/en-US/common.ts b/web/i18n/en-US/common.ts index bbed96e3b7..adf2ec98f4 100644 --- a/web/i18n/en-US/common.ts +++ b/web/i18n/en-US/common.ts @@ -181,7 +181,7 @@ const translation = { emailSupport: 'Email Support', workspace: 'Workspace', createWorkspace: 'Create Workspace', - helpCenter: 'Docs', + helpCenter: 'View Docs', support: 'Support', compliance: 'Compliance', communityFeedback: 'Feedback', diff --git a/web/i18n/en-US/workflow.ts b/web/i18n/en-US/workflow.ts index a967a82bb2..6f5cb1d355 100644 --- a/web/i18n/en-US/workflow.ts +++ b/web/i18n/en-US/workflow.ts @@ -277,6 +277,13 @@ const translation = { 'addAll': 'Add all', 'sources': 'Sources', 'searchDataSource': 'Search Data Source', + 'featuredTools': 'Featured', + 'showMoreFeatured': 'Show more', + 'installed': 'Installed', + 'pluginByAuthor': 'By {{author}}', + 'usePlugin': 'Select tool', + 'hideActions': 'Hide tools', + 'noFeaturedPlugins': 'Discover more tools in Marketplace', }, blocks: { 'start': 'User Input', @@ -366,7 +373,7 @@ const translation = { panel: { userInputField: 'User Input Field', changeBlock: 'Change Node', - helpLink: 'Help Link', + helpLink: 'View Docs', about: 'About', createdBy: 'Created By ', nextStep: 'Next Step', diff --git a/web/i18n/es-ES/workflow.ts b/web/i18n/es-ES/workflow.ts index 05bee4516e..cd66ac112a 100644 --- a/web/i18n/es-ES/workflow.ts +++ b/web/i18n/es-ES/workflow.ts @@ -309,7 +309,7 @@ const translation = { }, panel: { userInputField: 'Campo de entrada del usuario', - helpLink: 'Enlace de ayuda', + helpLink: 'Ayuda', about: 'Acerca de', createdBy: 'Creado por ', nextStep: 'Siguiente paso', diff --git a/web/i18n/fa-IR/workflow.ts b/web/i18n/fa-IR/workflow.ts index 0efe0752fe..c449de8d87 100644 --- a/web/i18n/fa-IR/workflow.ts +++ b/web/i18n/fa-IR/workflow.ts @@ -309,7 +309,7 @@ const translation = { }, panel: { userInputField: 'فیلد ورودی کاربر', - helpLink: 'لینک کمک', + helpLink: 'راهنما', about: 'درباره', createdBy: 'ساخته شده توسط', nextStep: 'مرحله بعدی', diff --git a/web/i18n/fr-FR/workflow.ts b/web/i18n/fr-FR/workflow.ts index 696f691fc7..41548d5556 100644 --- a/web/i18n/fr-FR/workflow.ts +++ b/web/i18n/fr-FR/workflow.ts @@ -309,7 +309,7 @@ const translation = { }, panel: { userInputField: 'Champ de saisie de l\'utilisateur', - helpLink: 'Lien d\'aide', + helpLink: 'Aide', about: 'À propos', createdBy: 'Créé par', nextStep: 'Étape suivante', diff --git a/web/i18n/hi-IN/workflow.ts b/web/i18n/hi-IN/workflow.ts index 97ccdb2d06..1f076d3306 100644 --- a/web/i18n/hi-IN/workflow.ts +++ b/web/i18n/hi-IN/workflow.ts @@ -320,7 +320,7 @@ const translation = { }, panel: { userInputField: 'उपयोगकर्ता इनपुट फ़ील्ड', - helpLink: 'सहायता लिंक', + helpLink: 'सहायता', about: 'के बारे में', createdBy: 'द्वारा बनाया गया ', nextStep: 'अगला कदम', diff --git a/web/i18n/id-ID/workflow.ts b/web/i18n/id-ID/workflow.ts index b2e250993a..50aafe4510 100644 --- a/web/i18n/id-ID/workflow.ts +++ b/web/i18n/id-ID/workflow.ts @@ -321,7 +321,7 @@ const translation = { userInputField: 'Bidang Input Pengguna', checklistResolved: 'Semua masalah terselesaikan', createdBy: 'Dibuat oleh', - helpLink: 'Tautan Bantuan', + helpLink: 'Docs', changeBlock: 'Ubah Node', runThisStep: 'Jalankan langkah ini', maximize: 'Maksimalkan Kanvas', diff --git a/web/i18n/it-IT/workflow.ts b/web/i18n/it-IT/workflow.ts index 875e6230d9..4d18fd3c6e 100644 --- a/web/i18n/it-IT/workflow.ts +++ b/web/i18n/it-IT/workflow.ts @@ -323,7 +323,7 @@ const translation = { }, panel: { userInputField: 'Campo di Input Utente', - helpLink: 'Link di Aiuto', + helpLink: 'Aiuto', about: 'Informazioni', createdBy: 'Creato da ', nextStep: 'Prossimo Passo', diff --git a/web/i18n/ja-JP/common.ts b/web/i18n/ja-JP/common.ts index 60e95ef10d..21abb3f5fc 100644 --- a/web/i18n/ja-JP/common.ts +++ b/web/i18n/ja-JP/common.ts @@ -171,7 +171,7 @@ const translation = { emailSupport: 'サポート', workspace: 'ワークスペース', createWorkspace: 'ワークスペースを作成', - helpCenter: 'ヘルプ', + helpCenter: 'ドキュメントを見る', support: 'サポート', compliance: 'コンプライアンス', communityFeedback: 'フィードバック', diff --git a/web/i18n/ja-JP/workflow.ts b/web/i18n/ja-JP/workflow.ts index cd78ec16b6..444242c2c7 100644 --- a/web/i18n/ja-JP/workflow.ts +++ b/web/i18n/ja-JP/workflow.ts @@ -349,7 +349,7 @@ const translation = { panel: { userInputField: 'ユーザー入力欄', changeBlock: 'ノード変更', - helpLink: 'ヘルプリンク', + helpLink: 'ドキュメントを見る', about: '詳細', createdBy: '作成者', nextStep: '次のステップ', diff --git a/web/i18n/ko-KR/workflow.ts b/web/i18n/ko-KR/workflow.ts index 6ea64df36b..be6edfc655 100644 --- a/web/i18n/ko-KR/workflow.ts +++ b/web/i18n/ko-KR/workflow.ts @@ -330,7 +330,7 @@ const translation = { }, panel: { userInputField: '사용자 입력 필드', - helpLink: '도움말 링크', + helpLink: '도움말 센터', about: '정보', createdBy: '작성자 ', nextStep: '다음 단계', diff --git a/web/i18n/pl-PL/workflow.ts b/web/i18n/pl-PL/workflow.ts index 0dd6020498..0b8329aa0c 100644 --- a/web/i18n/pl-PL/workflow.ts +++ b/web/i18n/pl-PL/workflow.ts @@ -309,7 +309,7 @@ const translation = { }, panel: { userInputField: 'Pole wprowadzania użytkownika', - helpLink: 'Link do pomocy', + helpLink: 'Pomoc', about: 'O', createdBy: 'Stworzone przez ', nextStep: 'Następny krok', diff --git a/web/i18n/pt-BR/workflow.ts b/web/i18n/pt-BR/workflow.ts index 36ae2a95d2..fecac8509f 100644 --- a/web/i18n/pt-BR/workflow.ts +++ b/web/i18n/pt-BR/workflow.ts @@ -309,7 +309,7 @@ const translation = { }, panel: { userInputField: 'Campo de entrada do usuário', - helpLink: 'Link de ajuda', + helpLink: 'Ajuda', about: 'Sobre', createdBy: 'Criado por ', nextStep: 'Próximo passo', diff --git a/web/i18n/ro-RO/workflow.ts b/web/i18n/ro-RO/workflow.ts index b22cd44634..e4af9b501e 100644 --- a/web/i18n/ro-RO/workflow.ts +++ b/web/i18n/ro-RO/workflow.ts @@ -309,7 +309,7 @@ const translation = { }, panel: { userInputField: 'Câmp de introducere utilizator', - helpLink: 'Link de ajutor', + helpLink: 'Ajutor', about: 'Despre', createdBy: 'Creat de ', nextStep: 'Pasul următor', diff --git a/web/i18n/ru-RU/workflow.ts b/web/i18n/ru-RU/workflow.ts index 6ae8b6d542..31f1b96b41 100644 --- a/web/i18n/ru-RU/workflow.ts +++ b/web/i18n/ru-RU/workflow.ts @@ -309,7 +309,7 @@ const translation = { }, panel: { userInputField: 'Поле ввода пользователя', - helpLink: 'Ссылка на справку', + helpLink: 'Помощь', about: 'О программе', createdBy: 'Создано ', nextStep: 'Следующий шаг', diff --git a/web/i18n/sl-SI/workflow.ts b/web/i18n/sl-SI/workflow.ts index 9e0201b1b3..1eba5c8520 100644 --- a/web/i18n/sl-SI/workflow.ts +++ b/web/i18n/sl-SI/workflow.ts @@ -324,7 +324,7 @@ const translation = { addNextStep: 'Dodajte naslednji korak v ta delovni potek', checklistTip: 'Prepričajte se, da so vse težave rešene, preden objavite.', selectNextStep: 'Izberi naslednji korak', - helpLink: 'Pomočna povezava', + helpLink: 'Pomoč', checklist: 'Kontrolni seznam', checklistResolved: 'Vse težave so rešene', createdBy: 'Ustvarjeno z', diff --git a/web/i18n/th-TH/workflow.ts b/web/i18n/th-TH/workflow.ts index 97bb63952d..42eadb1789 100644 --- a/web/i18n/th-TH/workflow.ts +++ b/web/i18n/th-TH/workflow.ts @@ -309,7 +309,7 @@ const translation = { }, panel: { userInputField: 'ฟิลด์ป้อนข้อมูลของผู้ใช้', - helpLink: 'ลิงค์ช่วยเหลือ', + helpLink: 'วิธีใช้', about: 'ประมาณ', createdBy: 'สร้างโดย', nextStep: 'ขั้นตอนถัดไป', diff --git a/web/i18n/tr-TR/workflow.ts b/web/i18n/tr-TR/workflow.ts index f68d081bba..442ce26567 100644 --- a/web/i18n/tr-TR/workflow.ts +++ b/web/i18n/tr-TR/workflow.ts @@ -309,7 +309,7 @@ const translation = { }, panel: { userInputField: 'Kullanıcı Giriş Alanı', - helpLink: 'Yardım Linki', + helpLink: 'Yardım', about: 'Hakkında', createdBy: 'Oluşturan: ', nextStep: 'Sonraki Adım', diff --git a/web/i18n/uk-UA/workflow.ts b/web/i18n/uk-UA/workflow.ts index 91978d6402..a4d3a2f8f3 100644 --- a/web/i18n/uk-UA/workflow.ts +++ b/web/i18n/uk-UA/workflow.ts @@ -309,7 +309,7 @@ const translation = { }, panel: { userInputField: 'Поле введення користувача', - helpLink: 'Посилання на допомогу', + helpLink: 'Довідковий центр', about: 'Про', createdBy: 'Створено ', nextStep: 'Наступний крок', diff --git a/web/i18n/vi-VN/workflow.ts b/web/i18n/vi-VN/workflow.ts index 9367dae8f9..a5f348f586 100644 --- a/web/i18n/vi-VN/workflow.ts +++ b/web/i18n/vi-VN/workflow.ts @@ -309,7 +309,7 @@ const translation = { }, panel: { userInputField: 'Trường đầu vào của người dùng', - helpLink: 'Liên kết trợ giúp', + helpLink: 'Trung tâm trợ giúp', about: 'Giới thiệu', createdBy: 'Tạo bởi ', nextStep: 'Bước tiếp theo', diff --git a/web/i18n/zh-Hans/common.ts b/web/i18n/zh-Hans/common.ts index 6175a36bc2..d46f4a813a 100644 --- a/web/i18n/zh-Hans/common.ts +++ b/web/i18n/zh-Hans/common.ts @@ -181,7 +181,7 @@ const translation = { emailSupport: '邮件支持', workspace: '工作空间', createWorkspace: '创建工作空间', - helpCenter: '帮助文档', + helpCenter: '查看帮助文档', support: '支持', compliance: '合规', communityFeedback: '用户反馈', diff --git a/web/i18n/zh-Hans/workflow.ts b/web/i18n/zh-Hans/workflow.ts index a7206db9df..416c9da6ae 100644 --- a/web/i18n/zh-Hans/workflow.ts +++ b/web/i18n/zh-Hans/workflow.ts @@ -263,6 +263,13 @@ const translation = { 'sources': '数据源', 'searchDataSource': '搜索数据源', 'start': '开始', + 'featuredTools': '精选推荐', + 'showMoreFeatured': '查看更多', + 'installed': '已安装', + 'pluginByAuthor': '来自 {{author}}', + 'usePlugin': '选择工具', + 'hideActions': '收起工具', + 'noFeaturedPlugins': '前往插件市场查看更多工具', }, blocks: { 'start': '用户输入', @@ -352,7 +359,7 @@ const translation = { panel: { userInputField: '用户输入字段', changeBlock: '更改节点', - helpLink: '帮助链接', + helpLink: '查看帮助文档', about: '关于', createdBy: '作者', nextStep: '下一步', diff --git a/web/i18n/zh-Hant/common.ts b/web/i18n/zh-Hant/common.ts index 273ecb010f..89933ba968 100644 --- a/web/i18n/zh-Hant/common.ts +++ b/web/i18n/zh-Hant/common.ts @@ -160,7 +160,7 @@ const translation = { emailSupport: '電子郵件支援', workspace: '工作空間', createWorkspace: '建立工作空間', - helpCenter: '幫助文件', + helpCenter: '查看幫助文件', communityFeedback: '使用者反饋', roadmap: '路線圖', community: '社群', diff --git a/web/i18n/zh-Hant/workflow.ts b/web/i18n/zh-Hant/workflow.ts index d31e6f3b60..d855911719 100644 --- a/web/i18n/zh-Hant/workflow.ts +++ b/web/i18n/zh-Hant/workflow.ts @@ -310,7 +310,7 @@ const translation = { panel: { userInputField: '用戶輸入字段', changeBlock: '更改節點', - helpLink: '幫助鏈接', + helpLink: '查看幫助文件', about: '關於', createdBy: '作者', nextStep: '下一步', diff --git a/web/service/use-plugins.ts b/web/service/use-plugins.ts index 21a3cc00fd..5904da6d27 100644 --- a/web/service/use-plugins.ts +++ b/web/service/use-plugins.ts @@ -43,6 +43,7 @@ import useReferenceSetting from '@/app/components/plugins/plugin-page/use-refere import { uninstallPlugin } from '@/service/plugins' import useRefreshPluginList from '@/app/components/plugins/install-plugin/hooks/use-refresh-plugin-list' import { cloneDeep } from 'lodash-es' +import { getFormattedPlugin } from '@/app/components/plugins/marketplace/utils' const NAME_SPACE = 'plugins' @@ -66,6 +67,50 @@ export const useCheckInstalled = ({ }) } +const useRecommendedMarketplacePluginsKey = [NAME_SPACE, 'recommendedMarketplacePlugins'] +export const useRecommendedMarketplacePlugins = ({ + category = PluginCategoryEnum.tool, + enabled = true, + limit = 15, +}: { + category?: string + enabled?: boolean + limit?: number +} = {}) => { + return useQuery({ + queryKey: [...useRecommendedMarketplacePluginsKey, category, limit], + queryFn: async () => { + const response = await postMarketplace<{ data: { plugins: Plugin[] } }>( + '/collections/__recommended-plugins-overall/plugins', + { + body: { + category, + limit, + }, + }, + ) + return response.data.plugins.map(plugin => getFormattedPlugin(plugin)) + }, + enabled, + staleTime: 60 * 1000, + }) +} + +export const useFeaturedToolsRecommendations = (enabled: boolean, limit = 15) => { + const { + data: plugins = [], + isLoading, + } = useRecommendedMarketplacePlugins({ + enabled, + limit, + }) + + return { + plugins, + isLoading, + } +} + export const useInstalledPluginList = (disable?: boolean, pageSize = 100) => { const fetchPlugins = async ({ pageParam = 1 }) => { const response = await get(