From 4833d39ab332b615a50a21358b5981195fab88e1 Mon Sep 17 00:00:00 2001
From: yangzheli <43645580+yangzheli@users.noreply.github.com>
Date: Thu, 20 Nov 2025 11:40:24 +0800
Subject: [PATCH] fix(workflow): validate node compatibility when importing dsl
between chatflows and workflows (#28012)
---
web/app/components/app-sidebar/app-info.tsx | 4 +--
.../components/workflow/update-dsl-modal.tsx | 36 ++++++++++++++++++-
web/package.json | 2 ++
web/pnpm-lock.yaml | 11 ++++++
4 files changed, 50 insertions(+), 3 deletions(-)
diff --git a/web/app/components/app-sidebar/app-info.tsx b/web/app/components/app-sidebar/app-info.tsx
index c2bda8d8fc..f143c2fcef 100644
--- a/web/app/components/app-sidebar/app-info.tsx
+++ b/web/app/components/app-sidebar/app-info.tsx
@@ -239,7 +239,7 @@ const AppInfo = ({ expand, onlyShowDetail = false, openState = false, onDetailEx
const secondaryOperations: Operation[] = [
// Import DSL (conditional)
- ...(appDetail.mode !== AppModeEnum.AGENT_CHAT && (appDetail.mode === AppModeEnum.ADVANCED_CHAT || appDetail.mode === AppModeEnum.WORKFLOW)) ? [{
+ ...(appDetail.mode === AppModeEnum.ADVANCED_CHAT || appDetail.mode === AppModeEnum.WORKFLOW) ? [{
id: 'import',
title: t('workflow.common.importDSL'),
icon: ,
@@ -271,7 +271,7 @@ const AppInfo = ({ expand, onlyShowDetail = false, openState = false, onDetailEx
]
// Keep the switch operation separate as it's not part of the main operations
- const switchOperation = (appDetail.mode !== AppModeEnum.AGENT_CHAT && (appDetail.mode === AppModeEnum.COMPLETION || appDetail.mode === AppModeEnum.CHAT)) ? {
+ const switchOperation = (appDetail.mode === AppModeEnum.COMPLETION || appDetail.mode === AppModeEnum.CHAT) ? {
id: 'switch',
title: t('app.switch'),
icon: ,
diff --git a/web/app/components/workflow/update-dsl-modal.tsx b/web/app/components/workflow/update-dsl-modal.tsx
index 00c36cce90..136c3d3455 100644
--- a/web/app/components/workflow/update-dsl-modal.tsx
+++ b/web/app/components/workflow/update-dsl-modal.tsx
@@ -9,6 +9,7 @@ import {
} from 'react'
import { useContext } from 'use-context-selector'
import { useTranslation } from 'react-i18next'
+import { load as yamlLoad } from 'js-yaml'
import {
RiAlertFill,
RiCloseLine,
@@ -16,8 +17,14 @@ import {
} from '@remixicon/react'
import { WORKFLOW_DATA_UPDATE } from './constants'
import {
+ BlockEnum,
SupportUploadFileTypes,
} from './types'
+import type {
+ CommonNodeType,
+ Node,
+} from './types'
+import { AppModeEnum } from '@/types/app'
import {
initialEdges,
initialNodes,
@@ -130,6 +137,33 @@ const UpdateDSLModal = ({
} as any)
}, [eventEmitter])
+ const validateDSLContent = (content: string): boolean => {
+ try {
+ const data = yamlLoad(content) as any
+ const nodes = data?.workflow?.graph?.nodes ?? []
+ const invalidNodes = appDetail?.mode === AppModeEnum.ADVANCED_CHAT
+ ? [
+ BlockEnum.End,
+ BlockEnum.TriggerWebhook,
+ BlockEnum.TriggerSchedule,
+ BlockEnum.TriggerPlugin,
+ ]
+ : [BlockEnum.Answer]
+ const hasInvalidNode = nodes.some((node: Node) => {
+ return invalidNodes.includes(node?.data?.type)
+ })
+ if (hasInvalidNode) {
+ notify({ type: 'error', message: t('workflow.common.importFailure') })
+ return false
+ }
+ return true
+ }
+ catch (err: any) {
+ notify({ type: 'error', message: t('workflow.common.importFailure') })
+ return false
+ }
+ }
+
const isCreatingRef = useRef(false)
const handleImport: MouseEventHandler = useCallback(async () => {
if (isCreatingRef.current)
@@ -138,7 +172,7 @@ const UpdateDSLModal = ({
if (!currentFile)
return
try {
- if (appDetail && fileContent) {
+ if (appDetail && fileContent && validateDSLContent(fileContent)) {
setLoading(true)
const response = await importDSL({ mode: DSLImportMode.YAML_CONTENT, yaml_content: fileContent, app_id: appDetail.id })
const { id, status, app_id, imported_dsl_version, current_dsl_version } = response
diff --git a/web/package.json b/web/package.json
index d10359f25d..0d267d7ee8 100644
--- a/web/package.json
+++ b/web/package.json
@@ -88,6 +88,7 @@
"immer": "^10.1.3",
"js-audio-recorder": "^1.0.7",
"js-cookie": "^3.0.5",
+ "js-yaml": "^4.1.0",
"jsonschema": "^1.5.0",
"katex": "^0.16.25",
"ky": "^1.12.0",
@@ -163,6 +164,7 @@
"@testing-library/react": "^16.3.0",
"@types/jest": "^29.5.14",
"@types/js-cookie": "^3.0.6",
+ "@types/js-yaml": "^4.0.9",
"@types/lodash-es": "^4.17.12",
"@types/negotiator": "^0.6.4",
"@types/node": "18.15.0",
diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml
index 8e638ed2df..7ef519a291 100644
--- a/web/pnpm-lock.yaml
+++ b/web/pnpm-lock.yaml
@@ -192,6 +192,9 @@ importers:
js-cookie:
specifier: ^3.0.5
version: 3.0.5
+ js-yaml:
+ specifier: ^4.1.0
+ version: 4.1.0
jsonschema:
specifier: ^1.5.0
version: 1.5.0
@@ -412,6 +415,9 @@ importers:
'@types/js-cookie':
specifier: ^3.0.6
version: 3.0.6
+ '@types/js-yaml':
+ specifier: ^4.0.9
+ version: 4.0.9
'@types/lodash-es':
specifier: ^4.17.12
version: 4.17.12
@@ -3240,6 +3246,9 @@ packages:
'@types/js-cookie@3.0.6':
resolution: {integrity: sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==}
+ '@types/js-yaml@4.0.9':
+ resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==}
+
'@types/json-schema@7.0.15':
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
@@ -11632,6 +11641,8 @@ snapshots:
'@types/js-cookie@3.0.6': {}
+ '@types/js-yaml@4.0.9': {}
+
'@types/json-schema@7.0.15': {}
'@types/katex@0.16.7': {}