From 2ccb20bf3a5944ee1f60285f05bdf8ab009dffa9 Mon Sep 17 00:00:00 2001 From: lyzno1 Date: Wed, 15 Oct 2025 20:26:12 +0800 Subject: [PATCH] =?UTF-8?q?fix(workflow):=20gate=20=E2=80=9Cpublish=20as?= =?UTF-8?q?=20tool=E2=80=9D=20on=20published=20user=20input=20node=20valid?= =?UTF-8?q?ity?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/app/app-publisher/index.tsx | 9 +++++++- .../tools/workflow-tool/configure-button.tsx | 21 ++++++++++++++----- .../workflow-header/features-trigger.tsx | 16 ++++++++++++-- .../workflow-app/hooks/use-workflow-init.ts | 16 ++++++++++++++ .../workflow/store/workflow/tool-slice.ts | 4 ++++ web/i18n/en-US/workflow.ts | 1 + web/i18n/ja-JP/workflow.ts | 1 + web/i18n/zh-Hans/workflow.ts | 1 + 8 files changed, 61 insertions(+), 8 deletions(-) diff --git a/web/app/components/app/app-publisher/index.tsx b/web/app/components/app/app-publisher/index.tsx index 253acddd81..ddd0459772 100644 --- a/web/app/components/app/app-publisher/index.tsx +++ b/web/app/components/app/app-publisher/index.tsx @@ -64,6 +64,7 @@ export type AppPublisherProps = { toolPublished?: boolean inputs?: InputVar[] onRefreshData?: () => void + workflowToolAvailable?: boolean } const PUBLISH_SHORTCUT = ['ctrl', '⇧', 'P'] @@ -82,6 +83,7 @@ const AppPublisher = ({ toolPublished, inputs, onRefreshData, + workflowToolAvailable = true, }: AppPublisherProps) => { const { t } = useTranslation() const [published, setPublished] = useState(false) @@ -178,6 +180,10 @@ const AppPublisher = ({ handlePublish() }, { exactMatch: true, useCapture: true }) + const hasPublishedVersion = !!publishedAt + const workflowToolDisabled = !hasPublishedVersion || !workflowToolAvailable + const workflowToolMessage = workflowToolDisabled ? t('workflow.common.workflowAsToolDisabledHint') : undefined + return ( <> {appDetail?.mode === 'workflow' && ( )} diff --git a/web/app/components/tools/workflow-tool/configure-button.tsx b/web/app/components/tools/workflow-tool/configure-button.tsx index 095ed369b2..bf0d789ff9 100644 --- a/web/app/components/tools/workflow-tool/configure-button.tsx +++ b/web/app/components/tools/workflow-tool/configure-button.tsx @@ -28,6 +28,7 @@ type Props = { inputs?: InputVar[] handlePublish: (params?: PublishWorkflowParams) => Promise onRefreshData?: () => void + disabledReason?: string } const WorkflowToolConfigureButton = ({ @@ -41,6 +42,7 @@ const WorkflowToolConfigureButton = ({ inputs, handlePublish, onRefreshData, + disabledReason, }: Props) => { const { t } = useTranslation() const router = useRouter() @@ -200,7 +202,8 @@ const WorkflowToolConfigureButton = ({ {t('workflow.common.configureRequired')} )} - ) + + ) : (
)} + {disabledReason && ( +
+ {disabledReason} +
+ )} {published && (
@@ -221,7 +229,7 @@ const WorkflowToolConfigureButton = ({ size='small' className='w-[140px]' onClick={() => setShowModal(true)} - disabled={!isCurrentWorkspaceManager} + disabled={!isCurrentWorkspaceManager || disabled} > {t('workflow.common.configure')} {outdated && } @@ -230,14 +238,17 @@ const WorkflowToolConfigureButton = ({ size='small' className='w-[140px]' onClick={() => router.push('/tools?category=workflow')} + disabled={disabled} > {t('workflow.common.manageInTools')}
- {outdated &&
- {t('workflow.common.workflowAsToolTip')} -
} + {outdated && ( +
+ {t('workflow.common.workflowAsToolTip')} +
+ )}
)} diff --git a/web/app/components/workflow-app/components/workflow-header/features-trigger.tsx b/web/app/components/workflow-app/components/workflow-header/features-trigger.tsx index 6fe3aa56af..9fed2bc07e 100644 --- a/web/app/components/workflow-app/components/workflow-header/features-trigger.tsx +++ b/web/app/components/workflow-app/components/workflow-header/features-trigger.tsx @@ -50,9 +50,12 @@ const FeaturesTrigger = () => { const publishedAt = useStore(s => s.publishedAt) const draftUpdatedAt = useStore(s => s.draftUpdatedAt) const toolPublished = useStore(s => s.toolPublished) + const lastPublishedHasUserInput = useStore(s => s.lastPublishedHasUserInput) const startVariables = useReactflowStore( s => s.getNodes().find(node => node.data.type === BlockEnum.Start)?.data.variables, ) + const nodes = useNodes() + const edges = useEdges() const fileSettings = useFeatures(s => s.features.file) const variables = useMemo(() => { const data = startVariables || [] @@ -74,6 +77,15 @@ const FeaturesTrigger = () => { const { handleCheckBeforePublish } = useChecklistBeforePublish() const { handleSyncWorkflowDraft } = useNodesSyncDraft() const { notify } = useToastContext() + const startNodeIds = useMemo( + () => nodes.filter(node => node.data.type === BlockEnum.Start).map(node => node.id), + [nodes], + ) + const hasUserInputNode = useMemo(() => { + if (!startNodeIds.length) + return false + return edges.some(edge => startNodeIds.includes(edge.source)) + }, [edges, startNodeIds]) const resetWorkflowVersionHistory = useResetWorkflowVersionHistory() const invalidateAppTriggers = useInvalidateAppTriggers() @@ -101,8 +113,6 @@ const FeaturesTrigger = () => { const { mutateAsync: publishWorkflow } = usePublishWorkflow() // const { validateBeforeRun } = useWorkflowRunValidation() - const nodes = useNodes() - const edges = useEdges() const needWarningNodes = useChecklist(nodes, edges) const updatePublishedWorkflow = useInvalidateAppWorkflow() @@ -130,6 +140,7 @@ const FeaturesTrigger = () => { updateAppDetail() invalidateAppTriggers(appID!) workflowStore.getState().setPublishedAt(res.created_at) + workflowStore.getState().setLastPublishedHasUserInput(hasUserInputNode) resetWorkflowVersionHistory() } } @@ -172,6 +183,7 @@ const FeaturesTrigger = () => { onRefreshData: handleToolConfigureUpdate, onPublish, onToggle: onPublisherToggle, + workflowToolAvailable: lastPublishedHasUserInput, crossAxisOffset: 4, }} /> diff --git a/web/app/components/workflow-app/hooks/use-workflow-init.ts b/web/app/components/workflow-app/hooks/use-workflow-init.ts index 2567f3d67c..f2524b8bc9 100644 --- a/web/app/components/workflow-app/hooks/use-workflow-init.ts +++ b/web/app/components/workflow-app/hooks/use-workflow-init.ts @@ -18,7 +18,18 @@ import { import type { FetchWorkflowDraftResponse } from '@/types/workflow' import { useWorkflowConfig } from '@/service/use-workflow' import type { FileUploadConfigResponse } from '@/models/common' +import { BlockEnum } from '@/app/components/workflow/types' +const hasConnectedUserInput = (nodes: any[] = [], edges: any[] = []) => { + const startNodeIds = nodes + .filter(node => node?.data?.type === BlockEnum.Start) + .map((node: any) => node.id) + + if (!startNodeIds.length) + return false + + return edges?.some((edge: any) => startNodeIds.includes(edge.source)) +} export const useWorkflowInit = () => { const workflowStore = useWorkflowStore() const { @@ -110,9 +121,14 @@ export const useWorkflowInit = () => { }, {} as Record), }) workflowStore.getState().setPublishedAt(publishedWorkflow?.created_at) + const graph = publishedWorkflow?.graph + workflowStore.getState().setLastPublishedHasUserInput( + hasConnectedUserInput(graph?.nodes, graph?.edges), + ) } catch (e) { console.error(e) + workflowStore.getState().setLastPublishedHasUserInput(false) } }, [workflowStore, appDetail]) diff --git a/web/app/components/workflow/store/workflow/tool-slice.ts b/web/app/components/workflow/store/workflow/tool-slice.ts index d6d89abcf0..04eb9f1ddc 100644 --- a/web/app/components/workflow/store/workflow/tool-slice.ts +++ b/web/app/components/workflow/store/workflow/tool-slice.ts @@ -14,6 +14,8 @@ export type ToolSliceShape = { setMcpTools: (tools: ToolWithProvider[]) => void toolPublished: boolean setToolPublished: (toolPublished: boolean) => void + lastPublishedHasUserInput: boolean + setLastPublishedHasUserInput: (hasUserInput: boolean) => void } export const createToolSlice: StateCreator = set => ({ @@ -27,4 +29,6 @@ export const createToolSlice: StateCreator = set => ({ setMcpTools: mcpTools => set(() => ({ mcpTools })), toolPublished: false, setToolPublished: toolPublished => set(() => ({ toolPublished })), + lastPublishedHasUserInput: false, + setLastPublishedHasUserInput: hasUserInput => set(() => ({ lastPublishedHasUserInput: hasUserInput })), }) diff --git a/web/i18n/en-US/workflow.ts b/web/i18n/en-US/workflow.ts index 6dba8e11b3..67b8ae5f2c 100644 --- a/web/i18n/en-US/workflow.ts +++ b/web/i18n/en-US/workflow.ts @@ -88,6 +88,7 @@ const translation = { configure: 'Configure', manageInTools: 'Manage in Tools', workflowAsToolTip: 'Tool reconfiguration is required after the workflow update.', + workflowAsToolDisabledHint: 'Publish the latest workflow and ensure a connected User Input node before configuring it as a tool.', viewDetailInTracingPanel: 'View details', syncingData: 'Syncing data, just a few seconds.', importDSL: 'Import DSL', diff --git a/web/i18n/ja-JP/workflow.ts b/web/i18n/ja-JP/workflow.ts index 418bd23545..f28f986b62 100644 --- a/web/i18n/ja-JP/workflow.ts +++ b/web/i18n/ja-JP/workflow.ts @@ -83,6 +83,7 @@ const translation = { configure: '設定', manageInTools: 'ツールページで管理', workflowAsToolTip: 'ワークフロー更新後はツールの再設定が必要です', + workflowAsToolDisabledHint: '最新のワークフローを公開し、接続済みの User Input ノードを用意してからツールとして設定してください。', viewDetailInTracingPanel: '詳細を表示', syncingData: 'データ同期中。。。', importDSL: 'DSL をインポート', diff --git a/web/i18n/zh-Hans/workflow.ts b/web/i18n/zh-Hans/workflow.ts index 309c21dd84..df63774ebd 100644 --- a/web/i18n/zh-Hans/workflow.ts +++ b/web/i18n/zh-Hans/workflow.ts @@ -86,6 +86,7 @@ const translation = { configure: '配置', manageInTools: '访问工具页', workflowAsToolTip: '工作流更新后需要重新配置工具参数', + workflowAsToolDisabledHint: '请先发布最新的工作流,并确保已连接的 User Input 节点后再配置为工具。', viewDetailInTracingPanel: '查看详细信息', syncingData: '同步数据中,只需几秒钟。', importDSL: '导入 DSL',