From 68f7f2f19bceea2a2df4fdbeff47492070d6d717 Mon Sep 17 00:00:00 2001 From: zhsama Date: Sun, 8 Feb 2026 02:59:06 +0800 Subject: [PATCH] feat: Unify sandbox detection and apply Agent icon override --- .../hooks/use-available-nodes-meta-data.ts | 10 +++- .../hooks/use-workflow-template.ts | 11 +++- web/app/components/workflow-app/index.tsx | 27 ++++++++-- web/app/components/workflow/block-icon.tsx | 53 +++++++++++++++++-- .../workflow/block-selector/blocks.tsx | 4 +- .../_base/components/next-step/index.tsx | 2 +- .../nodes/_base/components/next-step/item.tsx | 2 +- .../_base/components/workflow-panel/index.tsx | 2 +- .../components/workflow/nodes/_base/node.tsx | 7 +-- web/app/components/workflow/types.ts | 3 ++ 10 files changed, 100 insertions(+), 21 deletions(-) diff --git a/web/app/components/workflow-app/hooks/use-available-nodes-meta-data.ts b/web/app/components/workflow-app/hooks/use-available-nodes-meta-data.ts index dc85598dbb..eec5addee3 100644 --- a/web/app/components/workflow-app/hooks/use-available-nodes-meta-data.ts +++ b/web/app/components/workflow-app/hooks/use-available-nodes-meta-data.ts @@ -4,6 +4,7 @@ import type { DocPathWithoutLang } from '@/types/doc-paths' import { useMemo } from 'react' import { useTranslation } from 'react-i18next' import { useStore as useAppStore } from '@/app/components/app/store' +import { useFeatures } from '@/app/components/base/features/hooks' import { WORKFLOW_COMMON_NODES } from '@/app/components/workflow/constants/node' import AnswerDefault from '@/app/components/workflow/nodes/answer/default' import EndDefault from '@/app/components/workflow/nodes/end/default' @@ -18,7 +19,9 @@ import { useIsChatMode } from './use-is-chat-mode' export const useAvailableNodesMetaData = () => { const { t } = useTranslation() const isChatMode = useIsChatMode() - const isSandboxed = useAppStore(s => s.appDetail?.runtime_type === 'sandboxed') + const isSandboxFeatureEnabled = useFeatures(s => s.features.sandbox?.enabled) ?? false + const isSandboxRuntime = useAppStore(s => s.appDetail?.runtime_type === 'sandboxed') + const isSandboxed = isSandboxFeatureEnabled || isSandboxRuntime const docLink = useDocLink() const startNodeMetaData = useMemo(() => ({ @@ -76,10 +79,14 @@ export const useAvailableNodesMetaData = () => { const title = isSandboxed && metaData.type === BlockEnum.LLM ? t('blocks.agent', { ns: 'workflow' }) : t(`blocks.${metaData.type}` as const, { ns: 'workflow' }) + const iconTypeOverride = isSandboxed && metaData.type === BlockEnum.LLM + ? BlockEnum.Agent + : undefined const description = t(`blocksAbout.${metaData.type}`, { ns: 'workflow' }) const helpLinkPath = `/use-dify/nodes/${metaData.helpLinkUri}` as DocPathWithoutLang return toNodeDefaultBase(typedNode, { ...metaData, + iconType: iconTypeOverride, title, description, helpLinkUri: docLink(helpLinkPath), @@ -87,6 +94,7 @@ export const useAvailableNodesMetaData = () => { ...typedNode.defaultValue, type: metaData.type, title, + _iconTypeOverride: iconTypeOverride, }) }) }, [mergedNodesMetaData, t, docLink, isSandboxed]) diff --git a/web/app/components/workflow-app/hooks/use-workflow-template.ts b/web/app/components/workflow-app/hooks/use-workflow-template.ts index 7dfd4f0f9d..d838f9c7ff 100644 --- a/web/app/components/workflow-app/hooks/use-workflow-template.ts +++ b/web/app/components/workflow-app/hooks/use-workflow-template.ts @@ -8,12 +8,20 @@ import { import answerDefault from '@/app/components/workflow/nodes/answer/default' import llmDefault from '@/app/components/workflow/nodes/llm/default' import startDefault from '@/app/components/workflow/nodes/start/default' +import { BlockEnum } from '@/app/components/workflow/types' import { generateNewNode } from '@/app/components/workflow/utils' +import { STORAGE_KEYS } from '@/config/storage-keys' +import { storage } from '@/utils/storage' import { useIsChatMode } from './use-is-chat-mode' export const useWorkflowTemplate = () => { const isChatMode = useIsChatMode() - const isSandboxed = useAppStore(s => s.appDetail?.runtime_type === 'sandboxed') + const appDetail = useAppStore(s => s.appDetail) + const isSandboxedByType = appDetail?.runtime_type === 'sandboxed' + const isSandboxedBySelection = appDetail?.id + ? storage.getBoolean(`${STORAGE_KEYS.LOCAL.WORKFLOW.SANDBOX_RUNTIME_PREFIX}${appDetail.id}`) === true + : false + const isSandboxed = isSandboxedByType || isSandboxedBySelection const { t } = useTranslation() const { newNode: startNode } = generateNewNode({ @@ -39,6 +47,7 @@ export const useWorkflowTemplate = () => { query_prompt_template: '{{#sys.query#}}\n\n{{#sys.files#}}', }, selected: true, + _iconTypeOverride: isSandboxed ? BlockEnum.Agent : undefined, type: llmDefault.metaData.type, title: llmTitle, }, diff --git a/web/app/components/workflow-app/index.tsx b/web/app/components/workflow-app/index.tsx index 3422c2baad..6f3c68aa9e 100644 --- a/web/app/components/workflow-app/index.tsx +++ b/web/app/components/workflow-app/index.tsx @@ -28,6 +28,7 @@ import OnlineUsers from '@/app/components/workflow/header/online-users' import { useStore, useWorkflowStore } from '@/app/components/workflow/store' import { useTriggerStatusStore } from '@/app/components/workflow/store/trigger-status' import { + BlockEnum, SupportUploadFileTypes, ViewType, } from '@/app/components/workflow/types' @@ -221,14 +222,32 @@ const WorkflowAppWithAdditionalContext = () => { } }, [workflowStore]) + const isSandboxRuntime = appDetail?.runtime_type === 'sandboxed' + const isSandboxFeatureEnabled = data?.features?.sandbox?.enabled === true + const isSandboxed = isSandboxRuntime || isSandboxFeatureEnabled + const nodesData = useMemo(() => { if (data) { const processedNodes = initialNodes(data.graph.nodes, data.graph.edges) - collaborationManager.setNodes([], processedNodes) - return processedNodes + const resolvedNodes = isSandboxed + ? processedNodes.map((node) => { + if (node.data.type !== BlockEnum.LLM) + return node + + return { + ...node, + data: { + ...node.data, + _iconTypeOverride: BlockEnum.Agent, + }, + } + }) + : processedNodes + collaborationManager.setNodes([], resolvedNodes) + return resolvedNodes } return [] - }, [data]) + }, [data, isSandboxed]) const edgesData = useMemo(() => { if (data) { @@ -304,7 +323,7 @@ const WorkflowAppWithAdditionalContext = () => { }, [replayRunId, workflowStore, getWorkflowRunAndTraceUrl]) const isDataReady = !(!data || isLoading || isLoadingCurrentWorkspace || !currentWorkspace.id) - const sandboxEnabled = data?.features?.sandbox?.enabled === true + const sandboxEnabled = isSandboxFeatureEnabled useEffect(() => { if (!isDataReady || !appId) diff --git a/web/app/components/workflow/block-icon.tsx b/web/app/components/workflow/block-icon.tsx index 980f86cd80..0fa13d5acd 100644 --- a/web/app/components/workflow/block-icon.tsx +++ b/web/app/components/workflow/block-icon.tsx @@ -1,6 +1,13 @@ import type { FC } from 'react' -import { memo } from 'react' +import { + memo, + useCallback, + useMemo, + useSyncExternalStore, +} from 'react' +import { useStore as useAppStore } from '@/app/components/app/store' import AppIcon from '@/app/components/base/app-icon' +import { useFeaturesStore } from '@/app/components/base/features/hooks' import { Folder as FolderLine } from '@/app/components/base/icons/src/vender/line/files' import { Agent, @@ -28,7 +35,9 @@ import { WebhookLine, WindowCursor, } from '@/app/components/base/icons/src/vender/workflow' +import { STORAGE_KEYS } from '@/config/storage-keys' import { cn } from '@/utils/classnames' +import { storage } from '@/utils/storage' import { BlockEnum } from './types' type BlockIconProps = { @@ -114,13 +123,45 @@ const ICON_CONTAINER_BG_COLOR_MAP: Record = { [BlockEnum.TriggerWebhook]: 'bg-util-colors-blue-blue-500', [BlockEnum.TriggerPlugin]: 'bg-util-colors-blue-blue-500', } + +const useDisplayBlockType = (type: BlockEnum) => { + const appDetail = useAppStore(s => s.appDetail) + const featuresStore = useFeaturesStore() + + const subscribe = useCallback((listener: () => void) => { + if (!featuresStore) + return () => {} + return featuresStore.subscribe(listener) + }, [featuresStore]) + + const getSnapshot = useCallback(() => { + if (!featuresStore) + return false + return featuresStore.getState().features.sandbox?.enabled ?? false + }, [featuresStore]) + + const isSandboxFeatureEnabled = useSyncExternalStore(subscribe, getSnapshot, () => false) + const isSandboxRuntime = appDetail?.runtime_type === 'sandboxed' + const isSandboxSelection = useMemo(() => { + if (!appDetail?.id) + return false + return storage.getBoolean(`${STORAGE_KEYS.LOCAL.WORKFLOW.SANDBOX_RUNTIME_PREFIX}${appDetail.id}`) === true + }, [appDetail?.id]) + + const isSandboxed = isSandboxRuntime || isSandboxFeatureEnabled || isSandboxSelection + return isSandboxed && type === BlockEnum.LLM + ? BlockEnum.Agent + : type +} + const BlockIcon: FC = ({ type, size = 'sm', className, toolIcon, }) => { - const isToolOrDataSourceOrTriggerPlugin = type === BlockEnum.Tool || type === BlockEnum.DataSource || type === BlockEnum.TriggerPlugin + const displayType = useDisplayBlockType(type) + const isToolOrDataSourceOrTriggerPlugin = displayType === BlockEnum.Tool || displayType === BlockEnum.DataSource || displayType === BlockEnum.TriggerPlugin const showDefaultIcon = !isToolOrDataSourceOrTriggerPlugin || !toolIcon return ( @@ -128,7 +169,7 @@ const BlockIcon: FC = ({ cn( 'flex items-center justify-center border-[0.5px] border-white/2 text-white', ICON_CONTAINER_CLASSNAME_SIZE_MAP[size], - showDefaultIcon && ICON_CONTAINER_BG_COLOR_MAP[type], + showDefaultIcon && ICON_CONTAINER_BG_COLOR_MAP[displayType], toolIcon && '!shadow-none', className, ) @@ -136,7 +177,7 @@ const BlockIcon: FC = ({ > { showDefaultIcon && ( - getIcon(type, (type === BlockEnum.TriggerSchedule || type === BlockEnum.TriggerWebhook) + getIcon(displayType, (displayType === BlockEnum.TriggerSchedule || displayType === BlockEnum.TriggerWebhook) ? (size === 'xs' ? 'w-4 h-4' : 'w-4.5 h-4.5') : (size === 'xs' ? 'w-3 h-3' : 'w-3.5 h-3.5')) ) @@ -175,9 +216,11 @@ export const VarBlockIcon: FC = ({ type, className, }) => { + const displayType = useDisplayBlockType(type) + return ( <> - {getIcon(type, `w-3 h-3 ${className}`)} + {getIcon(displayType, `w-3 h-3 ${className}`)} ) } diff --git a/web/app/components/workflow/block-selector/blocks.tsx b/web/app/components/workflow/block-selector/blocks.tsx index 5a9e09eced..36657d00a1 100644 --- a/web/app/components/workflow/block-selector/blocks.tsx +++ b/web/app/components/workflow/block-selector/blocks.tsx @@ -103,7 +103,7 @@ const Blocks = ({
{block.metaData.title}
{block.metaData.description}
@@ -117,7 +117,7 @@ const Blocks = ({ >
{block.metaData.title}
{ diff --git a/web/app/components/workflow/nodes/_base/components/next-step/index.tsx b/web/app/components/workflow/nodes/_base/components/next-step/index.tsx index 8081f09b0c..1acdfeccea 100644 --- a/web/app/components/workflow/nodes/_base/components/next-step/index.tsx +++ b/web/app/components/workflow/nodes/_base/components/next-step/index.tsx @@ -100,7 +100,7 @@ const NextStep = ({
diff --git a/web/app/components/workflow/nodes/_base/components/next-step/item.tsx b/web/app/components/workflow/nodes/_base/components/next-step/item.tsx index 1019b995b6..fbcc18d02d 100644 --- a/web/app/components/workflow/nodes/_base/components/next-step/item.tsx +++ b/web/app/components/workflow/nodes/_base/components/next-step/item.tsx @@ -42,7 +42,7 @@ const Item = ({ className="group relative flex h-9 cursor-pointer items-center rounded-lg border-[0.5px] border-divider-regular bg-background-default px-2 text-xs text-text-secondary shadow-xs last-of-type:mb-0 hover:bg-background-default-hover" > diff --git a/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx b/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx index 650fcae94d..78613dbeb4 100644 --- a/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx +++ b/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx @@ -527,7 +527,7 @@ const BasePanel: FC = ({
diff --git a/web/app/components/workflow/nodes/_base/node.tsx b/web/app/components/workflow/nodes/_base/node.tsx index d22ce21bc6..cdaff9d831 100644 --- a/web/app/components/workflow/nodes/_base/node.tsx +++ b/web/app/components/workflow/nodes/_base/node.tsx @@ -69,11 +69,8 @@ const BaseNode: FC = ({ const { t } = useTranslation() const nodeRef = useRef(null) const { nodesReadOnly } = useNodesReadOnly() - const { _subGraphEntry, _iconTypeOverride } = data as { - _subGraphEntry?: boolean - _iconTypeOverride?: BlockEnum - } - const iconType = _iconTypeOverride ?? data.type + const { _subGraphEntry } = data + const iconType = data._iconTypeOverride ?? data.type const { handleNodeIterationChildSizeChange } = useNodeIterationInteractions() const { handleNodeLoopChildSizeChange } = useNodeLoopInteractions() diff --git a/web/app/components/workflow/types.ts b/web/app/components/workflow/types.ts index b7e094a46c..78cf69a039 100644 --- a/web/app/components/workflow/types.ts +++ b/web/app/components/workflow/types.ts @@ -91,6 +91,8 @@ export type CommonNodeType = { _retryIndex?: number _dataSourceStartToAdd?: boolean _isTempNode?: boolean + _subGraphEntry?: boolean + _iconTypeOverride?: BlockEnum isInIteration?: boolean iteration_id?: string selected?: boolean @@ -369,6 +371,7 @@ export type NodeDefaultBase = { classification: BlockClassificationEnum sort: number type: BlockEnum + iconType?: BlockEnum title: string author: string description?: string