mirror of
https://github.com/langgenius/dify.git
synced 2026-08-28 23:16:50 +08:00
Merge remote-tracking branch 'origin/feat/trigger' into feat/trigger
This commit is contained in:
commit
d101a83be8
@ -196,7 +196,7 @@ class AsyncWorkflowConfig(BaseSettings):
|
|||||||
"to avoid this, workflow can be suspended if needed, to achieve"
|
"to avoid this, workflow can be suspended if needed, to achieve"
|
||||||
"this, a time-based checker is required, every granularity seconds, "
|
"this, a time-based checker is required, every granularity seconds, "
|
||||||
"the checker will check the workflow queue and suspend the workflow",
|
"the checker will check the workflow queue and suspend the workflow",
|
||||||
default=1,
|
default=60,
|
||||||
ge=1,
|
ge=1,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
from core.workflow.graph_engine.layers.base import GraphEngineLayer
|
from core.workflow.graph_engine.layers.base import GraphEngineLayer
|
||||||
from core.workflow.graph_events.base import GraphEngineEvent
|
from core.workflow.graph_events.base import GraphEngineEvent
|
||||||
|
from core.workflow.graph_events.graph import GraphRunPausedEvent
|
||||||
|
|
||||||
|
|
||||||
class SuspendLayer(GraphEngineLayer):
|
class SuspendLayer(GraphEngineLayer):
|
||||||
@ -9,7 +10,11 @@ class SuspendLayer(GraphEngineLayer):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
def on_event(self, event: GraphEngineEvent):
|
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):
|
def on_graph_end(self, error: Exception | None):
|
||||||
""" """
|
""" """
|
||||||
|
|||||||
@ -4,7 +4,6 @@ from typing import ClassVar
|
|||||||
|
|
||||||
from apscheduler.schedulers.background import BackgroundScheduler # type: ignore
|
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.entities.commands import CommandType, GraphEngineCommand
|
||||||
from core.workflow.graph_engine.layers.base import GraphEngineLayer
|
from core.workflow.graph_engine.layers.base import GraphEngineLayer
|
||||||
from core.workflow.graph_events.base import GraphEngineEvent
|
from core.workflow.graph_events.base import GraphEngineEvent
|
||||||
@ -13,7 +12,7 @@ from services.workflow.scheduler import CFSPlanScheduler, SchedulerCommand
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class TimesliceLayer(GraphEngineLayer):
|
class TimeSliceLayer(GraphEngineLayer):
|
||||||
"""
|
"""
|
||||||
CFS plan scheduler to control the timeslice of the workflow.
|
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.
|
CFS plan scheduler allows to control the timeslice of the workflow.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if not TimesliceLayer.scheduler.running:
|
if not TimeSliceLayer.scheduler.running:
|
||||||
TimesliceLayer.scheduler.start()
|
TimeSliceLayer.scheduler.start()
|
||||||
|
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.cfs_plan_scheduler = cfs_plan_scheduler
|
self.cfs_plan_scheduler = cfs_plan_scheduler
|
||||||
self.stopped = False
|
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):
|
def on_graph_start(self):
|
||||||
"""
|
"""
|
||||||
Start timer to check if the workflow need to be suspended.
|
Start timer to check if the workflow need to be suspended.
|
||||||
@ -39,39 +68,11 @@ class TimesliceLayer(GraphEngineLayer):
|
|||||||
|
|
||||||
schedule_id = uuid.uuid4().hex
|
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(
|
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):
|
def on_event(self, event: GraphEngineEvent):
|
||||||
|
|||||||
@ -11,7 +11,7 @@ from core.workflow.graph_events.graph import GraphRunFailedEvent, GraphRunPaused
|
|||||||
from models.engine import db
|
from models.engine import db
|
||||||
from models.enums import WorkflowTriggerStatus
|
from models.enums import WorkflowTriggerStatus
|
||||||
from repositories.sqlalchemy_workflow_trigger_log_repository import SQLAlchemyWorkflowTriggerLogRepository
|
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__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -29,7 +29,7 @@ class TriggerPostLayer(GraphEngineLayer):
|
|||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
cfs_plan_scheduler_entity: TriggerWorkflowCFSPlanEntity,
|
cfs_plan_scheduler_entity: AsyncWorkflowCFSPlanEntity,
|
||||||
start_time: datetime,
|
start_time: datetime,
|
||||||
trigger_log_id: str,
|
trigger_log_id: str,
|
||||||
):
|
):
|
||||||
|
|||||||
@ -12,8 +12,9 @@ from celery import shared_task
|
|||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from sqlalchemy.orm import Session, sessionmaker
|
from sqlalchemy.orm import Session, sessionmaker
|
||||||
|
|
||||||
|
from configs import dify_config
|
||||||
from core.app.apps.workflow.app_generator import WorkflowAppGenerator
|
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.engine_layers.trigger_post_layer import TriggerPostLayer
|
||||||
from core.app.entities.app_invoke_entities import InvokeFrom
|
from core.app.entities.app_invoke_entities import InvokeFrom
|
||||||
from extensions.ext_database import db
|
from extensions.ext_database import db
|
||||||
@ -29,7 +30,7 @@ from services.workflow.entities import (
|
|||||||
WorkflowScheduleCFSPlanEntity,
|
WorkflowScheduleCFSPlanEntity,
|
||||||
WorkflowTaskData,
|
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
|
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]):
|
def execute_workflow_professional(task_data_dict: dict[str, Any]):
|
||||||
"""Execute workflow for professional tier with highest priority"""
|
"""Execute workflow for professional tier with highest priority"""
|
||||||
task_data = WorkflowTaskData.model_validate(task_data_dict)
|
task_data = WorkflowTaskData.model_validate(task_data_dict)
|
||||||
cfs_plan_scheduler_entity = TriggerWorkflowCFSPlanEntity(
|
cfs_plan_scheduler_entity = AsyncWorkflowCFSPlanEntity(
|
||||||
queue=AsyncWorkflowQueue.PROFESSIONAL_QUEUE, schedule_strategy=WorkflowScheduleCFSPlanEntity.Strategy.TimeSlice
|
queue=AsyncWorkflowQueue.PROFESSIONAL_QUEUE,
|
||||||
|
schedule_strategy=WorkflowScheduleCFSPlanEntity.Strategy.TimeSlice,
|
||||||
|
granularity=dify_config.ASYNC_WORKFLOW_SCHEDULER_GRANULARITY,
|
||||||
)
|
)
|
||||||
_execute_workflow_common(
|
_execute_workflow_common(
|
||||||
task_data,
|
task_data,
|
||||||
TriggerCFSPlanScheduler(plan=cfs_plan_scheduler_entity),
|
AsyncWorkflowCFSPlanScheduler(plan=cfs_plan_scheduler_entity),
|
||||||
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]):
|
def execute_workflow_team(task_data_dict: dict[str, Any]):
|
||||||
"""Execute workflow for team tier"""
|
"""Execute workflow for team tier"""
|
||||||
task_data = WorkflowTaskData.model_validate(task_data_dict)
|
task_data = WorkflowTaskData.model_validate(task_data_dict)
|
||||||
cfs_plan_scheduler_entity = TriggerWorkflowCFSPlanEntity(
|
cfs_plan_scheduler_entity = AsyncWorkflowCFSPlanEntity(
|
||||||
queue=AsyncWorkflowQueue.TEAM_QUEUE, schedule_strategy=WorkflowScheduleCFSPlanEntity.Strategy.TimeSlice
|
queue=AsyncWorkflowQueue.TEAM_QUEUE,
|
||||||
|
schedule_strategy=WorkflowScheduleCFSPlanEntity.Strategy.TimeSlice,
|
||||||
|
granularity=dify_config.ASYNC_WORKFLOW_SCHEDULER_GRANULARITY,
|
||||||
)
|
)
|
||||||
_execute_workflow_common(
|
_execute_workflow_common(
|
||||||
task_data,
|
task_data,
|
||||||
TriggerCFSPlanScheduler(plan=cfs_plan_scheduler_entity),
|
AsyncWorkflowCFSPlanScheduler(plan=cfs_plan_scheduler_entity),
|
||||||
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]):
|
def execute_workflow_sandbox(task_data_dict: dict[str, Any]):
|
||||||
"""Execute workflow for free tier with lower retry limit"""
|
"""Execute workflow for free tier with lower retry limit"""
|
||||||
task_data = WorkflowTaskData.model_validate(task_data_dict)
|
task_data = WorkflowTaskData.model_validate(task_data_dict)
|
||||||
cfs_plan_scheduler_entity = TriggerWorkflowCFSPlanEntity(
|
cfs_plan_scheduler_entity = AsyncWorkflowCFSPlanEntity(
|
||||||
queue=AsyncWorkflowQueue.SANDBOX_QUEUE, schedule_strategy=WorkflowScheduleCFSPlanEntity.Strategy.TimeSlice
|
queue=AsyncWorkflowQueue.SANDBOX_QUEUE,
|
||||||
|
schedule_strategy=WorkflowScheduleCFSPlanEntity.Strategy.TimeSlice,
|
||||||
|
granularity=dify_config.ASYNC_WORKFLOW_SCHEDULER_GRANULARITY,
|
||||||
)
|
)
|
||||||
_execute_workflow_common(
|
_execute_workflow_common(
|
||||||
task_data,
|
task_data,
|
||||||
TriggerCFSPlanScheduler(plan=cfs_plan_scheduler_entity),
|
AsyncWorkflowCFSPlanScheduler(plan=cfs_plan_scheduler_entity),
|
||||||
cfs_plan_scheduler_entity,
|
cfs_plan_scheduler_entity,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _execute_workflow_common(
|
def _execute_workflow_common(
|
||||||
task_data: WorkflowTaskData,
|
task_data: WorkflowTaskData,
|
||||||
cfs_plan_scheduler: TriggerCFSPlanScheduler,
|
cfs_plan_scheduler: AsyncWorkflowCFSPlanScheduler,
|
||||||
cfs_plan_scheduler_entity: TriggerWorkflowCFSPlanEntity,
|
cfs_plan_scheduler_entity: AsyncWorkflowCFSPlanEntity,
|
||||||
):
|
):
|
||||||
"""Execute workflow with common logic and trigger log updates."""
|
"""Execute workflow with common logic and trigger log updates."""
|
||||||
|
|
||||||
@ -140,7 +147,7 @@ def _execute_workflow_common(
|
|||||||
triggered_from=trigger_data.trigger_type,
|
triggered_from=trigger_data.trigger_type,
|
||||||
root_node_id=trigger_data.root_node_id,
|
root_node_id=trigger_data.root_node_id,
|
||||||
layers=[
|
layers=[
|
||||||
TimesliceLayer(cfs_plan_scheduler),
|
TimeSliceLayer(cfs_plan_scheduler),
|
||||||
TriggerPostLayer(cfs_plan_scheduler_entity, start_time, trigger_log.id),
|
TriggerPostLayer(cfs_plan_scheduler_entity, start_time, trigger_log.id),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|||||||
@ -3,7 +3,7 @@ from services.workflow.scheduler import CFSPlanScheduler, SchedulerCommand
|
|||||||
from tasks.workflow_cfs_scheduler.entities import AsyncWorkflowQueue
|
from tasks.workflow_cfs_scheduler.entities import AsyncWorkflowQueue
|
||||||
|
|
||||||
|
|
||||||
class TriggerWorkflowCFSPlanEntity(WorkflowScheduleCFSPlanEntity):
|
class AsyncWorkflowCFSPlanEntity(WorkflowScheduleCFSPlanEntity):
|
||||||
"""
|
"""
|
||||||
Trigger workflow CFS plan entity.
|
Trigger workflow CFS plan entity.
|
||||||
"""
|
"""
|
||||||
@ -11,23 +11,22 @@ class TriggerWorkflowCFSPlanEntity(WorkflowScheduleCFSPlanEntity):
|
|||||||
queue: AsyncWorkflowQueue
|
queue: AsyncWorkflowQueue
|
||||||
|
|
||||||
|
|
||||||
class TriggerCFSPlanScheduler(CFSPlanScheduler):
|
class AsyncWorkflowCFSPlanScheduler(CFSPlanScheduler):
|
||||||
"""
|
"""
|
||||||
Trigger workflow CFS plan scheduler.
|
Trigger workflow CFS plan scheduler.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
plan: AsyncWorkflowCFSPlanEntity
|
||||||
|
|
||||||
def can_schedule(self) -> SchedulerCommand:
|
def can_schedule(self) -> SchedulerCommand:
|
||||||
"""
|
"""
|
||||||
Check if the workflow can be scheduled.
|
Check if the workflow can be scheduled.
|
||||||
"""
|
"""
|
||||||
assert isinstance(self.plan, TriggerWorkflowCFSPlanEntity)
|
|
||||||
|
|
||||||
if self.plan.queue in [AsyncWorkflowQueue.PROFESSIONAL_QUEUE, AsyncWorkflowQueue.TEAM_QUEUE]:
|
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
|
return SchedulerCommand.NONE
|
||||||
|
|
||||||
# FIXME: avoid the sandbox user's workflow at a running state for ever
|
# FIXME: avoid the sandbox user's workflow at a running state for ever
|
||||||
|
return SchedulerCommand.RESOURCE_LIMIT_REACHED
|
||||||
return SchedulerCommand.NONE
|
|
||||||
|
|||||||
@ -25,10 +25,12 @@ import { SearchMenu } from '@/app/components/base/icons/src/vender/line/general'
|
|||||||
import { useGetLanguage } from '@/context/i18n'
|
import { useGetLanguage } from '@/context/i18n'
|
||||||
import type { ListRef } from '@/app/components/workflow/block-selector/market-place-plugin/list'
|
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 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 { PluginCategoryEnum } from '../../plugins/types'
|
||||||
import { useMarketplacePlugins } from '../../plugins/marketplace/hooks'
|
import { useMarketplacePlugins } from '../../plugins/marketplace/hooks'
|
||||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||||
import RAGToolSuggestions from './rag-tool-suggestions'
|
import RAGToolSuggestions from './rag-tool-suggestions'
|
||||||
|
import FeaturedTools from './featured-tools'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
|
|
||||||
type AllToolsProps = {
|
type AllToolsProps = {
|
||||||
@ -47,6 +49,10 @@ type AllToolsProps = {
|
|||||||
canChooseMCPTool?: boolean
|
canChooseMCPTool?: boolean
|
||||||
onTagsChange?: Dispatch<SetStateAction<string[]>>
|
onTagsChange?: Dispatch<SetStateAction<string[]>>
|
||||||
isInRAGPipeline?: boolean
|
isInRAGPipeline?: boolean
|
||||||
|
featuredPlugins?: Plugin[]
|
||||||
|
featuredLoading?: boolean
|
||||||
|
showFeatured?: boolean
|
||||||
|
onFeaturedInstallSuccess?: () => Promise<void> | void
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_TAGS: AllToolsProps['tags'] = []
|
const DEFAULT_TAGS: AllToolsProps['tags'] = []
|
||||||
@ -67,6 +73,10 @@ const AllTools = ({
|
|||||||
canChooseMCPTool,
|
canChooseMCPTool,
|
||||||
onTagsChange,
|
onTagsChange,
|
||||||
isInRAGPipeline = false,
|
isInRAGPipeline = false,
|
||||||
|
featuredPlugins = [],
|
||||||
|
featuredLoading = false,
|
||||||
|
showFeatured = false,
|
||||||
|
onFeaturedInstallSuccess,
|
||||||
}: AllToolsProps) => {
|
}: AllToolsProps) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const language = useGetLanguage()
|
const language = useGetLanguage()
|
||||||
@ -80,6 +90,16 @@ const AllTools = ({
|
|||||||
const isMatchingKeywords = (text: string, keywords: string) => {
|
const isMatchingKeywords = (text: string, keywords: string) => {
|
||||||
return text.toLowerCase().includes(keywords.toLowerCase())
|
return text.toLowerCase().includes(keywords.toLowerCase())
|
||||||
}
|
}
|
||||||
|
const allProviders = useMemo(() => [...buildInTools, ...customTools, ...workflowTools, ...mcpTools], [buildInTools, customTools, workflowTools, mcpTools])
|
||||||
|
const providerMap = useMemo(() => {
|
||||||
|
const map = new Map<string, ToolWithProvider>()
|
||||||
|
allProviders.forEach((provider) => {
|
||||||
|
const key = provider.plugin_id || provider.id
|
||||||
|
if (key)
|
||||||
|
map.set(key, provider)
|
||||||
|
})
|
||||||
|
return map
|
||||||
|
}, [allProviders])
|
||||||
const tools = useMemo(() => {
|
const tools = useMemo(() => {
|
||||||
let mergedTools: ToolWithProvider[] = []
|
let mergedTools: ToolWithProvider[] = []
|
||||||
if (activeTab === ToolTypeEnum.All)
|
if (activeTab === ToolTypeEnum.All)
|
||||||
@ -136,6 +156,7 @@ const AllTools = ({
|
|||||||
} = useMarketplacePlugins()
|
} = useMarketplacePlugins()
|
||||||
|
|
||||||
const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures)
|
const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!enable_marketplace) return
|
if (!enable_marketplace) return
|
||||||
if (hasFilter) {
|
if (hasFilter) {
|
||||||
@ -155,6 +176,12 @@ const AllTools = ({
|
|||||||
const hasToolsContent = tools.length > 0
|
const hasToolsContent = tools.length > 0
|
||||||
const hasPluginContent = enable_marketplace && notInstalledPlugins.length > 0
|
const hasPluginContent = enable_marketplace && notInstalledPlugins.length > 0
|
||||||
const shouldShowEmptyState = hasFilter && !hasToolsContent && !hasPluginContent
|
const shouldShowEmptyState = hasFilter && !hasToolsContent && !hasPluginContent
|
||||||
|
const shouldShowFeatured = showFeatured
|
||||||
|
&& enable_marketplace
|
||||||
|
&& !isInRAGPipeline
|
||||||
|
&& activeTab === ToolTypeEnum.All
|
||||||
|
&& !hasFilter
|
||||||
|
&& (featuredLoading || featuredPlugins.length > 0)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn('min-w-[400px] max-w-[500px]', className)}>
|
<div className={cn('min-w-[400px] max-w-[500px]', className)}>
|
||||||
@ -193,6 +220,19 @@ const AllTools = ({
|
|||||||
onTagsChange={onTagsChange}
|
onTagsChange={onTagsChange}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{shouldShowFeatured && (
|
||||||
|
<FeaturedTools
|
||||||
|
plugins={featuredPlugins}
|
||||||
|
providerMap={providerMap}
|
||||||
|
onSelect={onSelect}
|
||||||
|
selectedTools={selectedTools}
|
||||||
|
canChooseMCPTool={canChooseMCPTool}
|
||||||
|
isLoading={featuredLoading}
|
||||||
|
onInstallSuccess={async () => {
|
||||||
|
await onFeaturedInstallSuccess?.()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<Tools
|
<Tools
|
||||||
className={toolContentClassName}
|
className={toolContentClassName}
|
||||||
tools={tools}
|
tools={tools}
|
||||||
|
|||||||
@ -92,10 +92,10 @@ const DataSources = ({
|
|||||||
}, [searchText, enable_marketplace])
|
}, [searchText, enable_marketplace])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn('min-w-[400px] max-w-[500px]', className)}>
|
<div className={cn('w-[400px] min-w-0 max-w-full', className)}>
|
||||||
<div
|
<div
|
||||||
ref={wrapElemRef}
|
ref={wrapElemRef}
|
||||||
className='max-h-[464px] overflow-y-auto'
|
className='max-h-[464px] overflow-y-auto overflow-x-hidden'
|
||||||
onScroll={pluginRef.current?.handleScroll}
|
onScroll={pluginRef.current?.handleScroll}
|
||||||
>
|
>
|
||||||
<Tools
|
<Tools
|
||||||
|
|||||||
231
web/app/components/workflow/block-selector/featured-tools.tsx
Normal file
231
web/app/components/workflow/block-selector/featured-tools.tsx
Normal file
@ -0,0 +1,231 @@
|
|||||||
|
'use client'
|
||||||
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { BlockEnum, type ToolWithProvider } from '../types'
|
||||||
|
import type { ToolDefaultValue, ToolValue } from './types'
|
||||||
|
import type { Plugin } from '@/app/components/plugins/types'
|
||||||
|
import { useGetLanguage } from '@/context/i18n'
|
||||||
|
import Button from '@/app/components/base/button'
|
||||||
|
import BlockIcon from '../block-icon'
|
||||||
|
import { RiArrowDownSLine, RiArrowRightSLine, RiLoader2Line, RiMoreLine } from '@remixicon/react'
|
||||||
|
import { useInstallPackageFromMarketPlace } from '@/service/use-plugins'
|
||||||
|
import Loading from '@/app/components/base/loading'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import { getMarketplaceUrl } from '@/utils/var'
|
||||||
|
import { ToolTypeEnum } from './types'
|
||||||
|
import { ViewType } from './view-type-select'
|
||||||
|
import Tools from './tools'
|
||||||
|
|
||||||
|
const MAX_RECOMMENDED_COUNT = 15
|
||||||
|
const INITIAL_VISIBLE_COUNT = 5
|
||||||
|
|
||||||
|
type FeaturedToolsProps = {
|
||||||
|
plugins: Plugin[]
|
||||||
|
providerMap: Map<string, ToolWithProvider>
|
||||||
|
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<string | null>(null)
|
||||||
|
const [isCollapsed, setIsCollapsed] = useState<boolean>(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 (
|
||||||
|
<div className='px-3 pb-3 pt-2'>
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
className='flex w-full items-center justify-between rounded-md px-0 py-1 text-left text-text-tertiary'
|
||||||
|
onClick={() => setIsCollapsed(prev => !prev)}
|
||||||
|
>
|
||||||
|
<span className='system-xs-medium'>{t('workflow.tabs.featuredTools')}</span>
|
||||||
|
{isCollapsed ? <RiArrowRightSLine className='size-3.5' /> : <RiArrowDownSLine className='size-3.5' />}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{!isCollapsed && (
|
||||||
|
<>
|
||||||
|
{isLoading && (
|
||||||
|
<div className='py-3'>
|
||||||
|
<Loading type='app' />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{showEmptyState && (
|
||||||
|
<p className='system-xs-regular py-2 text-text-tertiary'>
|
||||||
|
<Link className='text-text-accent' href={getMarketplaceUrl('', { category: 'tool' })} target='_blank' rel='noopener noreferrer'>
|
||||||
|
{t('workflow.tabs.noFeaturedPlugins')}
|
||||||
|
</Link>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!showEmptyState && !isLoading && (
|
||||||
|
<>
|
||||||
|
{installedProviders.length > 0 && (
|
||||||
|
<Tools
|
||||||
|
className='p-0'
|
||||||
|
tools={installedProviders}
|
||||||
|
onSelect={onSelect}
|
||||||
|
canNotSelectMultiple
|
||||||
|
toolType={ToolTypeEnum.All}
|
||||||
|
viewType={ViewType.flat}
|
||||||
|
hasSearchText={false}
|
||||||
|
selectedTools={selectedTools}
|
||||||
|
canChooseMCPTool={canChooseMCPTool}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{uninstalledPlugins.length > 0 && (
|
||||||
|
<div className='mt-1 flex flex-col gap-1'>
|
||||||
|
{uninstalledPlugins.map(plugin => (
|
||||||
|
<FeaturedToolUninstalledItem
|
||||||
|
key={plugin.plugin_id}
|
||||||
|
plugin={plugin}
|
||||||
|
language={language}
|
||||||
|
installing={isMutating && installingIdentifier === plugin.latest_package_identifier}
|
||||||
|
onInstall={() => {
|
||||||
|
if (isMutating)
|
||||||
|
return
|
||||||
|
setInstallingIdentifier(plugin.latest_package_identifier)
|
||||||
|
installMutation.mutate(plugin.latest_package_identifier)
|
||||||
|
}}
|
||||||
|
t={t}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isLoading && visiblePlugins.length > 0 && showMore && (
|
||||||
|
<div
|
||||||
|
className='mt-1 flex cursor-pointer items-center gap-x-2 py-1 pl-3 pr-2 text-text-tertiary hover:text-text-secondary'
|
||||||
|
onClick={() => {
|
||||||
|
setVisibleCount(count => Math.min(count + INITIAL_VISIBLE_COUNT, MAX_RECOMMENDED_COUNT, plugins.length))
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className='px-1'>
|
||||||
|
<RiMoreLine className='size-4' />
|
||||||
|
</div>
|
||||||
|
<div className='system-xs-regular'>
|
||||||
|
{t('common.operation.more')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type FeaturedToolUninstalledItemProps = {
|
||||||
|
plugin: Plugin
|
||||||
|
language: string
|
||||||
|
installing: boolean
|
||||||
|
onInstall: () => void
|
||||||
|
t: (key: string, options?: Record<string, any>) => 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 (
|
||||||
|
<div className='group flex items-center justify-between rounded-lg px-3 py-2 hover:bg-state-base-hover'>
|
||||||
|
<div className='flex min-w-0 items-center gap-3'>
|
||||||
|
<BlockIcon type={BlockEnum.Tool} toolIcon={plugin.icon} />
|
||||||
|
<div className='min-w-0'>
|
||||||
|
<div className='system-sm-medium truncate text-text-primary'>{label}</div>
|
||||||
|
{description && (
|
||||||
|
<div className='system-xs-regular truncate text-text-tertiary'>{description}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='ml-3 flex items-center'>
|
||||||
|
<span className='system-xs-regular text-text-tertiary group-hover:hidden'>{installCountLabel}</span>
|
||||||
|
<Button
|
||||||
|
size='small'
|
||||||
|
variant='secondary'
|
||||||
|
className='hidden items-center gap-1 group-hover:flex'
|
||||||
|
disabled={installing}
|
||||||
|
onClick={onInstall}
|
||||||
|
>
|
||||||
|
{installing ? t('workflow.nodes.agent.pluginInstaller.installing') : t('workflow.nodes.agent.pluginInstaller.install')}
|
||||||
|
{installing && <RiLoader2Line className='size-3 animate-spin' />}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FeaturedTools
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import type { Dispatch, FC, SetStateAction } from 'react'
|
import type { Dispatch, FC, SetStateAction } from 'react'
|
||||||
import { memo } 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 {
|
import type {
|
||||||
BlockEnum,
|
BlockEnum,
|
||||||
NodeDefault,
|
NodeDefault,
|
||||||
@ -13,6 +13,8 @@ import AllStartBlocks from './all-start-blocks'
|
|||||||
import AllTools from './all-tools'
|
import AllTools from './all-tools'
|
||||||
import DataSources from './data-sources'
|
import DataSources from './data-sources'
|
||||||
import cn from '@/utils/classnames'
|
import cn from '@/utils/classnames'
|
||||||
|
import { useFeaturedToolsRecommendations } from '@/service/use-plugins'
|
||||||
|
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||||
|
|
||||||
export type TabsProps = {
|
export type TabsProps = {
|
||||||
activeTab: TabsEnum
|
activeTab: TabsEnum
|
||||||
@ -53,6 +55,13 @@ const Tabs: FC<TabsProps> = ({
|
|||||||
const { data: customTools } = useAllCustomTools()
|
const { data: customTools } = useAllCustomTools()
|
||||||
const { data: workflowTools } = useAllWorkflowTools()
|
const { data: workflowTools } = useAllWorkflowTools()
|
||||||
const { data: mcpTools } = useAllMCPTools()
|
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 (
|
return (
|
||||||
<div onClick={e => e.stopPropagation()}>
|
<div onClick={e => e.stopPropagation()}>
|
||||||
@ -127,7 +136,13 @@ const Tabs: FC<TabsProps> = ({
|
|||||||
mcpTools={mcpTools || []}
|
mcpTools={mcpTools || []}
|
||||||
canChooseMCPTool
|
canChooseMCPTool
|
||||||
onTagsChange={onTagsChange}
|
onTagsChange={onTagsChange}
|
||||||
isInRAGPipeline={dataSources.length > 0}
|
isInRAGPipeline={inRAGPipeline}
|
||||||
|
featuredPlugins={featuredPlugins}
|
||||||
|
featuredLoading={isFeaturedLoading}
|
||||||
|
showFeatured={enable_marketplace && !inRAGPipeline}
|
||||||
|
onFeaturedInstallSuccess={async () => {
|
||||||
|
invalidateBuiltInTools()
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,7 +23,9 @@ import {
|
|||||||
} from '@/service/tools'
|
} from '@/service/tools'
|
||||||
import type { CustomCollectionBackend } from '@/app/components/tools/types'
|
import type { CustomCollectionBackend } from '@/app/components/tools/types'
|
||||||
import Toast from '@/app/components/base/toast'
|
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'
|
import cn from '@/utils/classnames'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@ -61,11 +63,18 @@ const ToolPicker: FC<Props> = ({
|
|||||||
const [searchText, setSearchText] = useState('')
|
const [searchText, setSearchText] = useState('')
|
||||||
const [tags, setTags] = useState<string[]>([])
|
const [tags, setTags] = useState<string[]>([])
|
||||||
|
|
||||||
|
const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures)
|
||||||
const { data: buildInTools } = useAllBuiltInTools()
|
const { data: buildInTools } = useAllBuiltInTools()
|
||||||
const { data: customTools } = useAllCustomTools()
|
const { data: customTools } = useAllCustomTools()
|
||||||
const invalidateCustomTools = useInvalidateAllCustomTools()
|
const invalidateCustomTools = useInvalidateAllCustomTools()
|
||||||
const { data: workflowTools } = useAllWorkflowTools()
|
const { data: workflowTools } = useAllWorkflowTools()
|
||||||
const { data: mcpTools } = useAllMCPTools()
|
const { data: mcpTools } = useAllMCPTools()
|
||||||
|
const invalidateBuiltInTools = useInvalidateAllBuiltInTools()
|
||||||
|
|
||||||
|
const {
|
||||||
|
plugins: featuredPlugins = [],
|
||||||
|
isLoading: isFeaturedLoading,
|
||||||
|
} = useFeaturedToolsRecommendations(enable_marketplace)
|
||||||
|
|
||||||
const { builtinToolList, customToolList, workflowToolList } = useMemo(() => {
|
const { builtinToolList, customToolList, workflowToolList } = useMemo(() => {
|
||||||
if (scope === 'plugins') {
|
if (scope === 'plugins') {
|
||||||
@ -179,6 +188,12 @@ const ToolPicker: FC<Props> = ({
|
|||||||
selectedTools={selectedTools}
|
selectedTools={selectedTools}
|
||||||
canChooseMCPTool={canChooseMCPTool}
|
canChooseMCPTool={canChooseMCPTool}
|
||||||
onTagsChange={setTags}
|
onTagsChange={setTags}
|
||||||
|
featuredPlugins={featuredPlugins}
|
||||||
|
featuredLoading={isFeaturedLoading}
|
||||||
|
showFeatured={scope === 'all' && enable_marketplace}
|
||||||
|
onFeaturedInstallSuccess={async () => {
|
||||||
|
invalidateBuiltInTools()
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</PortalToFollowElemContent>
|
</PortalToFollowElemContent>
|
||||||
|
|||||||
@ -309,7 +309,7 @@ const translation = {
|
|||||||
},
|
},
|
||||||
panel: {
|
panel: {
|
||||||
userInputField: 'Benutzereingabefeld',
|
userInputField: 'Benutzereingabefeld',
|
||||||
helpLink: 'Hilfelink',
|
helpLink: 'Hilfe',
|
||||||
about: 'Über',
|
about: 'Über',
|
||||||
createdBy: 'Erstellt von ',
|
createdBy: 'Erstellt von ',
|
||||||
nextStep: 'Nächster Schritt',
|
nextStep: 'Nächster Schritt',
|
||||||
|
|||||||
@ -181,7 +181,7 @@ const translation = {
|
|||||||
emailSupport: 'Email Support',
|
emailSupport: 'Email Support',
|
||||||
workspace: 'Workspace',
|
workspace: 'Workspace',
|
||||||
createWorkspace: 'Create Workspace',
|
createWorkspace: 'Create Workspace',
|
||||||
helpCenter: 'Docs',
|
helpCenter: 'View Docs',
|
||||||
support: 'Support',
|
support: 'Support',
|
||||||
compliance: 'Compliance',
|
compliance: 'Compliance',
|
||||||
communityFeedback: 'Feedback',
|
communityFeedback: 'Feedback',
|
||||||
|
|||||||
@ -277,6 +277,13 @@ const translation = {
|
|||||||
'addAll': 'Add all',
|
'addAll': 'Add all',
|
||||||
'sources': 'Sources',
|
'sources': 'Sources',
|
||||||
'searchDataSource': 'Search Data Source',
|
'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: {
|
blocks: {
|
||||||
'start': 'User Input',
|
'start': 'User Input',
|
||||||
@ -366,7 +373,7 @@ const translation = {
|
|||||||
panel: {
|
panel: {
|
||||||
userInputField: 'User Input Field',
|
userInputField: 'User Input Field',
|
||||||
changeBlock: 'Change Node',
|
changeBlock: 'Change Node',
|
||||||
helpLink: 'Help Link',
|
helpLink: 'View Docs',
|
||||||
about: 'About',
|
about: 'About',
|
||||||
createdBy: 'Created By ',
|
createdBy: 'Created By ',
|
||||||
nextStep: 'Next Step',
|
nextStep: 'Next Step',
|
||||||
|
|||||||
@ -309,7 +309,7 @@ const translation = {
|
|||||||
},
|
},
|
||||||
panel: {
|
panel: {
|
||||||
userInputField: 'Campo de entrada del usuario',
|
userInputField: 'Campo de entrada del usuario',
|
||||||
helpLink: 'Enlace de ayuda',
|
helpLink: 'Ayuda',
|
||||||
about: 'Acerca de',
|
about: 'Acerca de',
|
||||||
createdBy: 'Creado por ',
|
createdBy: 'Creado por ',
|
||||||
nextStep: 'Siguiente paso',
|
nextStep: 'Siguiente paso',
|
||||||
|
|||||||
@ -309,7 +309,7 @@ const translation = {
|
|||||||
},
|
},
|
||||||
panel: {
|
panel: {
|
||||||
userInputField: 'فیلد ورودی کاربر',
|
userInputField: 'فیلد ورودی کاربر',
|
||||||
helpLink: 'لینک کمک',
|
helpLink: 'راهنما',
|
||||||
about: 'درباره',
|
about: 'درباره',
|
||||||
createdBy: 'ساخته شده توسط',
|
createdBy: 'ساخته شده توسط',
|
||||||
nextStep: 'مرحله بعدی',
|
nextStep: 'مرحله بعدی',
|
||||||
|
|||||||
@ -309,7 +309,7 @@ const translation = {
|
|||||||
},
|
},
|
||||||
panel: {
|
panel: {
|
||||||
userInputField: 'Champ de saisie de l\'utilisateur',
|
userInputField: 'Champ de saisie de l\'utilisateur',
|
||||||
helpLink: 'Lien d\'aide',
|
helpLink: 'Aide',
|
||||||
about: 'À propos',
|
about: 'À propos',
|
||||||
createdBy: 'Créé par',
|
createdBy: 'Créé par',
|
||||||
nextStep: 'Étape suivante',
|
nextStep: 'Étape suivante',
|
||||||
|
|||||||
@ -320,7 +320,7 @@ const translation = {
|
|||||||
},
|
},
|
||||||
panel: {
|
panel: {
|
||||||
userInputField: 'उपयोगकर्ता इनपुट फ़ील्ड',
|
userInputField: 'उपयोगकर्ता इनपुट फ़ील्ड',
|
||||||
helpLink: 'सहायता लिंक',
|
helpLink: 'सहायता',
|
||||||
about: 'के बारे में',
|
about: 'के बारे में',
|
||||||
createdBy: 'द्वारा बनाया गया ',
|
createdBy: 'द्वारा बनाया गया ',
|
||||||
nextStep: 'अगला कदम',
|
nextStep: 'अगला कदम',
|
||||||
|
|||||||
@ -321,7 +321,7 @@ const translation = {
|
|||||||
userInputField: 'Bidang Input Pengguna',
|
userInputField: 'Bidang Input Pengguna',
|
||||||
checklistResolved: 'Semua masalah terselesaikan',
|
checklistResolved: 'Semua masalah terselesaikan',
|
||||||
createdBy: 'Dibuat oleh',
|
createdBy: 'Dibuat oleh',
|
||||||
helpLink: 'Tautan Bantuan',
|
helpLink: 'Docs',
|
||||||
changeBlock: 'Ubah Node',
|
changeBlock: 'Ubah Node',
|
||||||
runThisStep: 'Jalankan langkah ini',
|
runThisStep: 'Jalankan langkah ini',
|
||||||
maximize: 'Maksimalkan Kanvas',
|
maximize: 'Maksimalkan Kanvas',
|
||||||
|
|||||||
@ -323,7 +323,7 @@ const translation = {
|
|||||||
},
|
},
|
||||||
panel: {
|
panel: {
|
||||||
userInputField: 'Campo di Input Utente',
|
userInputField: 'Campo di Input Utente',
|
||||||
helpLink: 'Link di Aiuto',
|
helpLink: 'Aiuto',
|
||||||
about: 'Informazioni',
|
about: 'Informazioni',
|
||||||
createdBy: 'Creato da ',
|
createdBy: 'Creato da ',
|
||||||
nextStep: 'Prossimo Passo',
|
nextStep: 'Prossimo Passo',
|
||||||
|
|||||||
@ -171,7 +171,7 @@ const translation = {
|
|||||||
emailSupport: 'サポート',
|
emailSupport: 'サポート',
|
||||||
workspace: 'ワークスペース',
|
workspace: 'ワークスペース',
|
||||||
createWorkspace: 'ワークスペースを作成',
|
createWorkspace: 'ワークスペースを作成',
|
||||||
helpCenter: 'ヘルプ',
|
helpCenter: 'ドキュメントを見る',
|
||||||
support: 'サポート',
|
support: 'サポート',
|
||||||
compliance: 'コンプライアンス',
|
compliance: 'コンプライアンス',
|
||||||
communityFeedback: 'フィードバック',
|
communityFeedback: 'フィードバック',
|
||||||
|
|||||||
@ -349,7 +349,7 @@ const translation = {
|
|||||||
panel: {
|
panel: {
|
||||||
userInputField: 'ユーザー入力欄',
|
userInputField: 'ユーザー入力欄',
|
||||||
changeBlock: 'ノード変更',
|
changeBlock: 'ノード変更',
|
||||||
helpLink: 'ヘルプリンク',
|
helpLink: 'ドキュメントを見る',
|
||||||
about: '詳細',
|
about: '詳細',
|
||||||
createdBy: '作成者',
|
createdBy: '作成者',
|
||||||
nextStep: '次のステップ',
|
nextStep: '次のステップ',
|
||||||
|
|||||||
@ -330,7 +330,7 @@ const translation = {
|
|||||||
},
|
},
|
||||||
panel: {
|
panel: {
|
||||||
userInputField: '사용자 입력 필드',
|
userInputField: '사용자 입력 필드',
|
||||||
helpLink: '도움말 링크',
|
helpLink: '도움말 센터',
|
||||||
about: '정보',
|
about: '정보',
|
||||||
createdBy: '작성자 ',
|
createdBy: '작성자 ',
|
||||||
nextStep: '다음 단계',
|
nextStep: '다음 단계',
|
||||||
|
|||||||
@ -309,7 +309,7 @@ const translation = {
|
|||||||
},
|
},
|
||||||
panel: {
|
panel: {
|
||||||
userInputField: 'Pole wprowadzania użytkownika',
|
userInputField: 'Pole wprowadzania użytkownika',
|
||||||
helpLink: 'Link do pomocy',
|
helpLink: 'Pomoc',
|
||||||
about: 'O',
|
about: 'O',
|
||||||
createdBy: 'Stworzone przez ',
|
createdBy: 'Stworzone przez ',
|
||||||
nextStep: 'Następny krok',
|
nextStep: 'Następny krok',
|
||||||
|
|||||||
@ -309,7 +309,7 @@ const translation = {
|
|||||||
},
|
},
|
||||||
panel: {
|
panel: {
|
||||||
userInputField: 'Campo de entrada do usuário',
|
userInputField: 'Campo de entrada do usuário',
|
||||||
helpLink: 'Link de ajuda',
|
helpLink: 'Ajuda',
|
||||||
about: 'Sobre',
|
about: 'Sobre',
|
||||||
createdBy: 'Criado por ',
|
createdBy: 'Criado por ',
|
||||||
nextStep: 'Próximo passo',
|
nextStep: 'Próximo passo',
|
||||||
|
|||||||
@ -309,7 +309,7 @@ const translation = {
|
|||||||
},
|
},
|
||||||
panel: {
|
panel: {
|
||||||
userInputField: 'Câmp de introducere utilizator',
|
userInputField: 'Câmp de introducere utilizator',
|
||||||
helpLink: 'Link de ajutor',
|
helpLink: 'Ajutor',
|
||||||
about: 'Despre',
|
about: 'Despre',
|
||||||
createdBy: 'Creat de ',
|
createdBy: 'Creat de ',
|
||||||
nextStep: 'Pasul următor',
|
nextStep: 'Pasul următor',
|
||||||
|
|||||||
@ -309,7 +309,7 @@ const translation = {
|
|||||||
},
|
},
|
||||||
panel: {
|
panel: {
|
||||||
userInputField: 'Поле ввода пользователя',
|
userInputField: 'Поле ввода пользователя',
|
||||||
helpLink: 'Ссылка на справку',
|
helpLink: 'Помощь',
|
||||||
about: 'О программе',
|
about: 'О программе',
|
||||||
createdBy: 'Создано ',
|
createdBy: 'Создано ',
|
||||||
nextStep: 'Следующий шаг',
|
nextStep: 'Следующий шаг',
|
||||||
|
|||||||
@ -324,7 +324,7 @@ const translation = {
|
|||||||
addNextStep: 'Dodajte naslednji korak v ta delovni potek',
|
addNextStep: 'Dodajte naslednji korak v ta delovni potek',
|
||||||
checklistTip: 'Prepričajte se, da so vse težave rešene, preden objavite.',
|
checklistTip: 'Prepričajte se, da so vse težave rešene, preden objavite.',
|
||||||
selectNextStep: 'Izberi naslednji korak',
|
selectNextStep: 'Izberi naslednji korak',
|
||||||
helpLink: 'Pomočna povezava',
|
helpLink: 'Pomoč',
|
||||||
checklist: 'Kontrolni seznam',
|
checklist: 'Kontrolni seznam',
|
||||||
checklistResolved: 'Vse težave so rešene',
|
checklistResolved: 'Vse težave so rešene',
|
||||||
createdBy: 'Ustvarjeno z',
|
createdBy: 'Ustvarjeno z',
|
||||||
|
|||||||
@ -309,7 +309,7 @@ const translation = {
|
|||||||
},
|
},
|
||||||
panel: {
|
panel: {
|
||||||
userInputField: 'ฟิลด์ป้อนข้อมูลของผู้ใช้',
|
userInputField: 'ฟิลด์ป้อนข้อมูลของผู้ใช้',
|
||||||
helpLink: 'ลิงค์ช่วยเหลือ',
|
helpLink: 'วิธีใช้',
|
||||||
about: 'ประมาณ',
|
about: 'ประมาณ',
|
||||||
createdBy: 'สร้างโดย',
|
createdBy: 'สร้างโดย',
|
||||||
nextStep: 'ขั้นตอนถัดไป',
|
nextStep: 'ขั้นตอนถัดไป',
|
||||||
|
|||||||
@ -309,7 +309,7 @@ const translation = {
|
|||||||
},
|
},
|
||||||
panel: {
|
panel: {
|
||||||
userInputField: 'Kullanıcı Giriş Alanı',
|
userInputField: 'Kullanıcı Giriş Alanı',
|
||||||
helpLink: 'Yardım Linki',
|
helpLink: 'Yardım',
|
||||||
about: 'Hakkında',
|
about: 'Hakkında',
|
||||||
createdBy: 'Oluşturan: ',
|
createdBy: 'Oluşturan: ',
|
||||||
nextStep: 'Sonraki Adım',
|
nextStep: 'Sonraki Adım',
|
||||||
|
|||||||
@ -309,7 +309,7 @@ const translation = {
|
|||||||
},
|
},
|
||||||
panel: {
|
panel: {
|
||||||
userInputField: 'Поле введення користувача',
|
userInputField: 'Поле введення користувача',
|
||||||
helpLink: 'Посилання на допомогу',
|
helpLink: 'Довідковий центр',
|
||||||
about: 'Про',
|
about: 'Про',
|
||||||
createdBy: 'Створено ',
|
createdBy: 'Створено ',
|
||||||
nextStep: 'Наступний крок',
|
nextStep: 'Наступний крок',
|
||||||
|
|||||||
@ -309,7 +309,7 @@ const translation = {
|
|||||||
},
|
},
|
||||||
panel: {
|
panel: {
|
||||||
userInputField: 'Trường đầu vào của người dùng',
|
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',
|
about: 'Giới thiệu',
|
||||||
createdBy: 'Tạo bởi ',
|
createdBy: 'Tạo bởi ',
|
||||||
nextStep: 'Bước tiếp theo',
|
nextStep: 'Bước tiếp theo',
|
||||||
|
|||||||
@ -181,7 +181,7 @@ const translation = {
|
|||||||
emailSupport: '邮件支持',
|
emailSupport: '邮件支持',
|
||||||
workspace: '工作空间',
|
workspace: '工作空间',
|
||||||
createWorkspace: '创建工作空间',
|
createWorkspace: '创建工作空间',
|
||||||
helpCenter: '帮助文档',
|
helpCenter: '查看帮助文档',
|
||||||
support: '支持',
|
support: '支持',
|
||||||
compliance: '合规',
|
compliance: '合规',
|
||||||
communityFeedback: '用户反馈',
|
communityFeedback: '用户反馈',
|
||||||
|
|||||||
@ -263,6 +263,13 @@ const translation = {
|
|||||||
'sources': '数据源',
|
'sources': '数据源',
|
||||||
'searchDataSource': '搜索数据源',
|
'searchDataSource': '搜索数据源',
|
||||||
'start': '开始',
|
'start': '开始',
|
||||||
|
'featuredTools': '精选推荐',
|
||||||
|
'showMoreFeatured': '查看更多',
|
||||||
|
'installed': '已安装',
|
||||||
|
'pluginByAuthor': '来自 {{author}}',
|
||||||
|
'usePlugin': '选择工具',
|
||||||
|
'hideActions': '收起工具',
|
||||||
|
'noFeaturedPlugins': '前往插件市场查看更多工具',
|
||||||
},
|
},
|
||||||
blocks: {
|
blocks: {
|
||||||
'start': '用户输入',
|
'start': '用户输入',
|
||||||
@ -352,7 +359,7 @@ const translation = {
|
|||||||
panel: {
|
panel: {
|
||||||
userInputField: '用户输入字段',
|
userInputField: '用户输入字段',
|
||||||
changeBlock: '更改节点',
|
changeBlock: '更改节点',
|
||||||
helpLink: '帮助链接',
|
helpLink: '查看帮助文档',
|
||||||
about: '关于',
|
about: '关于',
|
||||||
createdBy: '作者',
|
createdBy: '作者',
|
||||||
nextStep: '下一步',
|
nextStep: '下一步',
|
||||||
|
|||||||
@ -160,7 +160,7 @@ const translation = {
|
|||||||
emailSupport: '電子郵件支援',
|
emailSupport: '電子郵件支援',
|
||||||
workspace: '工作空間',
|
workspace: '工作空間',
|
||||||
createWorkspace: '建立工作空間',
|
createWorkspace: '建立工作空間',
|
||||||
helpCenter: '幫助文件',
|
helpCenter: '查看幫助文件',
|
||||||
communityFeedback: '使用者反饋',
|
communityFeedback: '使用者反饋',
|
||||||
roadmap: '路線圖',
|
roadmap: '路線圖',
|
||||||
community: '社群',
|
community: '社群',
|
||||||
|
|||||||
@ -310,7 +310,7 @@ const translation = {
|
|||||||
panel: {
|
panel: {
|
||||||
userInputField: '用戶輸入字段',
|
userInputField: '用戶輸入字段',
|
||||||
changeBlock: '更改節點',
|
changeBlock: '更改節點',
|
||||||
helpLink: '幫助鏈接',
|
helpLink: '查看幫助文件',
|
||||||
about: '關於',
|
about: '關於',
|
||||||
createdBy: '作者',
|
createdBy: '作者',
|
||||||
nextStep: '下一步',
|
nextStep: '下一步',
|
||||||
|
|||||||
@ -43,6 +43,7 @@ import useReferenceSetting from '@/app/components/plugins/plugin-page/use-refere
|
|||||||
import { uninstallPlugin } from '@/service/plugins'
|
import { uninstallPlugin } from '@/service/plugins'
|
||||||
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 { cloneDeep } from 'lodash-es'
|
import { cloneDeep } from 'lodash-es'
|
||||||
|
import { getFormattedPlugin } from '@/app/components/plugins/marketplace/utils'
|
||||||
|
|
||||||
const NAME_SPACE = 'plugins'
|
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<Plugin[]>({
|
||||||
|
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) => {
|
export const useInstalledPluginList = (disable?: boolean, pageSize = 100) => {
|
||||||
const fetchPlugins = async ({ pageParam = 1 }) => {
|
const fetchPlugins = async ({ pageParam = 1 }) => {
|
||||||
const response = await get<InstalledPluginListWithTotalResponse>(
|
const response = await get<InstalledPluginListWithTotalResponse>(
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user