feat(web): workflow check list

This commit is contained in:
zhaoying
2026-04-09 18:58:21 +08:00
parent 33a1c178ff
commit 5adff38bda
19 changed files with 475 additions and 20 deletions

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>参与</title>
<g id="空间里层页面优化" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="应用管理-工作流-配置-开始" transform="translate(-1173, -24)" fill="#000000" fill-rule="nonzero">
<g id="编组-11" transform="translate(1166, 17)">
<g id="参与" transform="translate(7, 7)">
<g id="编组" transform="translate(1.5, 1)">
<path d="M9.66581309,0 C11.5071324,0 12.9999203,1.50297946 12.9999203,3.35712964 L12.9999203,6.99997709 C12.9999203,7.34514783 12.7220975,7.62497504 12.3793991,7.62497504 C12.0367007,7.62497504 11.7588778,7.34514783 11.7588778,6.99997709 L11.7588778,3.35712964 C11.7588778,2.19344595 10.8218287,1.24999591 9.66581309,1.24999591 L3.33410726,1.24999591 C2.17807615,1.24999591 1.24104252,2.19344595 1.24104252,3.35712964 L1.24104252,10.6428245 C1.24104252,11.8065082 2.17809167,12.7499583 3.33410726,12.7499583 L6.04769325,12.7499583 C6.39040715,12.7499583 6.66821451,13.0297855 6.66821451,13.3749562 C6.66821451,13.720127 6.39040715,13.9999542 6.04769325,13.9999542 L3.33410726,13.9999542 C1.49278799,13.9999542 0,12.4969747 0,10.6428245 L0,3.35712964 C0,1.50297946 1.49278799,0 3.33410726,0 L9.66581309,0 Z" id="路径"></path>
<path d="M11.8585646,8.937002 C12.0448761,8.6472842 12.4290718,8.56453447 12.7167144,8.75215885 C13.0043726,8.93981449 13.0865296,9.3267976 12.9002336,9.6165154 L10.2649729,13.7147051 C10.0576723,14.0370947 9.61342558,14.0966257 9.3296457,13.8400641 L7.8566058,12.5082872 C7.60157156,12.2777254 7.58041179,11.8825705 7.80932208,11.6256963 C8.03824788,11.3688222 8.43057245,11.3475097 8.68560669,11.5780715 L9.61814154,12.4211937 L11.8585646,8.93698637 L11.8585646,8.937002 Z M9.21354617,4.09820534 C9.55624455,4.09820534 9.83406743,4.37801692 9.83406743,4.72320329 C9.83406743,5.06837404 9.55624455,5.34820125 9.21354617,5.34820125 L3.78637417,5.34820125 C3.4436758,5.34820125 3.16585292,5.06837404 3.16585292,4.72320329 C3.16585292,4.37801692 3.4436758,4.09820534 3.78637417,4.09820534 L9.21354617,4.09820534 Z M9.21354617,7.74105279 C9.55624455,7.74105279 9.83406743,8.02086437 9.83406743,8.36605074 C9.83406743,8.71122149 9.55624455,8.9910487 9.21354617,8.9910487 L3.78637417,8.9910487 C3.4436758,8.9910487 3.16585292,8.71122149 3.16585292,8.36605074 C3.16585292,8.02086437 3.4436758,7.74105279 3.78637417,7.74105279 L9.21354617,7.74105279 Z" id="形状"></path>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@@ -1,12 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>参与</title>
<g id="空间里层页面优化" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="应用管理-工作流-配置-开始" transform="translate(-1211, -24)" fill="#171719" fill-rule="nonzero">
<g id="空间里层页面优化" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round">
<g id="应用管理-工作流-配置-开始" transform="translate(-1211, -24)" stroke="#171719" stroke-width="1.2">
<g id="编组-11" transform="translate(1204, 17)">
<g id="参与" transform="translate(7, 7)">
<g id="编组-35" transform="translate(0.5, 1.5)">
<path d="M13.3524137,3.04473843 C13.7876396,3.04473843 14.1979604,3.21975634 14.507738,3.53746403 C14.8173619,3.85501246 14.9923132,4.28005597 15,4.73408333 L15,10.2997805 C15,10.7391566 14.8365789,11.1556006 14.5400225,11.472512 C14.2665266,11.7647393 13.9083222,11.9416683 13.5224454,11.9771815 L13.5155273,13.3373525 C13.5155273,13.6047366 13.3547197,13.8526919 13.1155068,13.9536577 C13.021728,13.9861451 12.9450138,14 12.8673773,14 C12.6896587,14 12.521318,13.9261071 12.40494,13.797113 L10.6609614,11.9676263 L8.48098801,11.9676263 C8.12370606,11.9676263 7.83314543,11.6666401 7.83314543,11.2965385 C7.83314543,10.926437 8.12370606,10.6254507 8.48098801,10.6254507 L10.9635134,10.6254507 C11.1415394,10.6459942 11.2911243,10.7176576 11.3904376,10.8283378 L12.2272215,11.7015163 L12.2272215,11.2966978 C12.2272215,10.9371068 12.5239315,10.6334133 12.8750641,10.6334133 L13.3674798,10.6334133 C13.5491954,10.6334133 13.6969355,10.4803722 13.6969355,10.2921364 L13.6969355,4.72819101 C13.6969355,4.53995518 13.5491954,4.38691404 13.3674798,4.38691404 C13.0060469,4.38691404 12.7121041,4.08592781 12.7121041,3.71582623 C12.7121041,3.34588391 12.9994363,3.04473843 13.3524137,3.04473843 Z M10.4203649,0 C11.3164907,0 12.0455058,0.755172845 12.0455058,1.68345258 L12.0455058,8.25976271 C12.0455058,9.18804245 11.3131085,9.94305605 10.4129855,9.94305605 L5.13154658,9.94305605 L2.58091627,12.7683453 C2.45792764,12.9048242 2.29081685,12.9799911 2.11017731,12.9799911 C2.03346315,12.9799911 1.94675618,12.9634289 1.87234806,12.9344451 C1.62268115,12.8328423 1.462181,12.5875943 1.462181,12.3089033 L1.46940658,9.93604896 C1.08614328,9.89719148 0.730552424,9.71962553 0.459055037,9.42946844 C0.163113662,9.11319403 0,8.69770563 0,8.25960346 L0,1.68345258 C0,0.755172845 0.729015066,0 1.62514092,0 L10.4203649,0 Z M10.4208261,1.33453151 L1.62560213,1.33453151 C1.44388644,1.33453151 1.29614636,1.48757266 1.29614636,1.67580849 L1.29614636,8.25976271 C1.29614636,8.44799854 1.44388644,8.60103969 1.62560213,8.60103969 L2.14030952,8.60103969 C2.31449216,8.60103969 2.48329405,8.67588811 2.60320795,8.80663398 C2.72066209,8.93451331 2.78092651,9.10061312 2.77323973,9.27467552 C2.77323973,9.29060072 2.77293225,9.31273675 2.76570667,9.34156135 L2.75924977,10.5990149 L4.38623552,8.79532709 C4.40698985,8.76475071 4.43450856,8.74850701 4.45295685,8.73879264 L4.47217382,8.72557473 C4.48293533,8.71872689 4.49246695,8.71171981 4.50276724,8.70407571 C4.52905606,8.68464697 4.56180178,8.66028142 4.60638516,8.64483398 L4.65634929,8.63161607 L4.67326022,8.63161607 C4.70446859,8.62078693 4.73721431,8.61362059 4.76857641,8.60788752 L4.78302757,8.59291784 L10.4208261,8.59291784 C10.6025418,8.59291784 10.7502818,8.43987669 10.7502818,8.25164086 L10.7502818,1.67580849 C10.7502818,1.48757266 10.6025418,1.33453151 10.4208261,1.33453151 Z M3.1173004,4.58263471 C3.34559803,4.58263471 3.55975197,4.70939928 3.67612996,4.9132418 C3.85062007,5.22505716 4.19252844,5.51999181 4.63943835,5.74469634 C5.09157528,5.9719489 5.59213898,6.09712095 6.04904171,6.09712095 C6.99467049,6.09712095 8.05882956,5.56983768 8.42149226,4.92168216 C8.53833145,4.71688412 8.7524854,4.59011955 8.98078303,4.59011955 C9.1026955,4.59011955 9.21599877,4.62181069 9.30870145,4.68184869 C9.45797889,4.76513747 9.56497899,4.908942 9.61033104,5.08714495 C9.64815005,5.27283275 9.62339859,5.4460989 9.53976632,5.59356622 C8.85594957,6.7876375 7.23096239,7.43165247 6.04904171,7.43165247 C4.85989546,7.43165247 3.22968125,6.78477096 2.55078405,5.58608138 C2.46745926,5.43256248 2.4448601,5.24862645 2.48775238,5.07966011 C2.53125961,4.90830499 2.63549247,4.76816326 2.78892077,4.67468235 C2.89146254,4.61273333 2.99877011,4.58263471 3.1173004,4.58263471 Z" id="形状结合"></path>
<g id="编组-35" transform="translate(1, 2)">
<path d="M1.5,0 L9.5,0 C10.3284271,-2.22044605e-16 11,0.671572875 11,1.5 L11,7.5 C11,8.32842712 10.3284271,9 9.5,9 L4.16268077,9 L4.16268077,9 L1.61845449,11.7671841 L1.61845449,9 L1.5,9 C0.671572875,9 2.22044605e-16,8.32842712 2.22044605e-16,7.5 L0,1.5 C0,0.671572875 0.671572875,-2.22044605e-16 1.5,-2.22044605e-16 Z" id="矩形" stroke-linejoin="round"></path>
<path d="M14,10.7913467 L14,10.7913467 L11.889042,10.7913467 L9.95430953,12.7671841 L9.95430953,10.7913467 L9.33585504,10.7913467 C8.78357029,10.7913467 8.33585504,10.3436314 8.33585504,9.79134668 L8.33585504,4.21772464 C8.33585504,3.66543989 8.78357029,3.21772464 9.33585504,3.21772464 L9.35694395,3.21772464 L9.35694395,3.21772464" id="路径" stroke-linejoin="round" transform="translate(11.1679, 7.9925) scale(-1, 1) translate(-11.1679, -7.9925)"></path>
<path d="M2.57312498,4.7624569 C3.0405519,5.21866557 5.40029607,7.88187743 8.56554211,4.7624569" id="路径-22"></path>
</g>
</g>
</g>

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -1396,6 +1396,43 @@ export const en = {
pleaseUploadFile: 'Please upload file',
setting: 'Settings',
features: 'Conversation Features',
checkList: 'Check List',
checkListDesc: 'Ensure all issues are resolved before publishing',
checkListEmpty: 'No issues found',
notConnected: 'This node is not connected to other nodes',
goto: 'Go to',
cannotBeEmpty: 'cannot be empty',
checkListErrors: {
'llm.model_id': 'Model',
'llm.messages': 'Messages',
'end.output': 'Output',
'knowledge-retrieval.knowledge_retrieval': 'Knowledge bases',
'parameter-extractor.model_id': 'Model',
'parameter-extractor.text': 'Input variable',
'parameter-extractor.params': 'Params',
'memory-read.message': 'Message',
'memory-read.config_id': 'Memory config',
'memory-read.search_switch': 'Search mode',
'memory-write.messages': 'Messages',
'memory-write.config_id': 'Memory config',
'if-else.cases': 'Condition',
'question-classifier.model_id': 'Model',
'question-classifier.input_variable': 'Input variable',
'question-classifier.categories': 'Categories',
'iteration.input': 'Input variable',
'iteration.output': 'Output variable',
'var-aggregator.group_variables': 'Variables',
'assigner.assignments': 'Variables',
'http-request.url': 'API URL',
'http-request.body.data': 'Binary file variable',
'code.input_variables': 'Input variables',
'code.code': 'Code',
'code.output_variables': 'Output variables',
'jinja-render.mapping': 'Input variables',
'jinja-render.template': 'Template',
'document-extractor.file_selector': 'File variable',
'list-operator.input_list': 'Input list',
},
file_upload: 'File Upload',
file_upload_desc: 'The chat input box supports file uploads. Types include images, documents, and other types',
settings: 'File Upload Settings',
@@ -2442,7 +2479,45 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
iteration: 'Iteration',
input_cycle_vars: 'Initial Loop Variables',
output_cycle_vars: 'Final Loop Variables',
}
},
sureReplace: '确认替换',
checkList: 'Check List',
checkListDesc: 'Ensure all issues are resolved before publishing',
checkListEmpty: 'No issues found',
notConnected: 'This node is not connected to other nodes',
goto: 'Go to',
cannotBeEmpty: 'cannot be empty',
checkListErrors: {
'llm.model_id': 'Model',
'llm.messages': 'Messages',
'end.output': 'Output',
'knowledge-retrieval.knowledge_retrieval': 'Knowledge bases',
'parameter-extractor.model_id': 'Model',
'parameter-extractor.text': 'Input variable',
'parameter-extractor.params': 'Params',
'memory-read.message': 'Message',
'memory-read.config_id': 'Memory config',
'memory-read.search_switch': 'Search mode',
'memory-write.messages': 'Messages',
'memory-write.config_id': 'Memory config',
'if-else.cases': 'Condition',
'question-classifier.model_id': 'Model',
'question-classifier.input_variable': 'Input variable',
'question-classifier.categories': 'Categories',
'iteration.input': 'Input variable',
'iteration.output': 'Output variable',
'var-aggregator.group_variables': 'Variables',
'assigner.assignments': 'Variables',
'http-request.url': 'API URL',
'http-request.body.data': 'Binary file variable',
'code.input_variables': 'Input variables',
'code.code': 'Code',
'code.output_variables': 'Output variables',
'jinja-render.mapping': 'Input variables',
'jinja-render.template': 'Template',
'document-extractor.file_selector': 'File variable',
'list-operator.input_list': 'Input list',
},
},
emotionEngine: {
emotionEngineConfig: 'Emotion Engine Configuration',

View File

@@ -2445,6 +2445,43 @@ export const zh = {
output_cycle_vars: '最终循环变量',
},
sureReplace: '确认替换',
checkList: '检查清单',
checkListDesc: '发布前确保所有问题均已解决',
checkListEmpty: '没有发现问题',
notConnected: '此节点尚未连接到其他节点',
goto: '转到',
cannotBeEmpty: '不能为空',
checkListErrors: {
'llm.model_id': '模型',
'llm.messages': '提示词',
'end.output': '回复',
'knowledge-retrieval.knowledge_retrieval': '知识库',
'parameter-extractor.model_id': '模型',
'parameter-extractor.text': '输入变量',
'parameter-extractor.params': '提取参数',
'memory-read.message': '消息',
'memory-read.config_id': '记忆配置',
'memory-read.search_switch': '检索模式',
'memory-write.messages': '消息',
'memory-write.config_id': '记忆配置',
'if-else.cases': '条件',
'question-classifier.model_id': '模型',
'question-classifier.input_variable': '输入变量',
'question-classifier.categories': '分类',
'iteration.input': '输入变量',
'iteration.output': '输出变量',
'var-aggregator.group_variables': '变量',
'assigner.assignments': '变量',
'http-request.url': 'API URL',
'http-request.body.data': 'binary文件类型变量',
'code.input_variables': '输入变量',
'code.code': '代码',
'code.output_variables': '输出变量',
'jinja-render.mapping': '输入变量',
'jinja-render.template': '模板',
'document-extractor.file_selector': '文件变量',
'list-operator.input_list': '输入变量',
},
},
emotionEngine: {
emotionEngineConfig: '情感引擎配置',

View File

@@ -4,7 +4,7 @@
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-04-07 16:28:33
*/
import { type FC, useRef, useMemo, useCallback } from 'react';
import { type FC, useRef, useMemo } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { Tabs, Dropdown, Flex, Popover } from 'antd';
import type { MenuProps } from 'antd';
@@ -18,6 +18,7 @@ import type { CopyModalRef, AgentRef, ClusterRef, WorkflowRef, FeaturesConfigFor
import { deleteApplication, appExport } from '@/api/application'
import CopyModal from './CopyModal'
import PageHeader from '@/components/Layout/PageHeader'
import CheckList from '@/views/Workflow/components/CheckList'
/**
* Tab keys for application configuration
@@ -206,6 +207,7 @@ const ConfigHeader: FC<ConfigHeaderProps> = ({
</Flex>}
extra={application?.type === 'workflow' && source !== 'sharing' && activeTab === 'arrangement'
? <Flex align="center" justify="end" gap={10} className="rb:h-8">
<CheckList workflowRef={workflowRef} />
<Popover content={t('application.features')} classNames={{ body: 'rb:py-0.5! rb:px-1! rb:rounded-[6px]! rb:text-[12px]!' }}>
<div
className="rb:cursor-pointer rb:size-7.5 rb:border rb:border-[#EBEBEB] rb:hover:bg-[#F6F6F6] rb:rounded-[10px] rb:bg-[url('@/assets/images/workflow/features.svg')] rb:bg-size-[16px_16px] rb:bg-center rb:bg-no-repeat"

View File

@@ -12,6 +12,7 @@ import type { ChatVariable, GraphRef, WorkflowConfig } from '@/views/Workflow/ty
import type { ApiKey } from '@/views/ApiKeyManagement/types'
import type { SkillConfigForm } from './components/Skill/types'
import type { Capability } from '@/views/ModelManagement/types'
import { Node } from '@antv/x6';
/**
* Model configuration parameters
@@ -170,6 +171,7 @@ export interface WorkflowRef {
features: WorkflowConfig['features'];
handleFeaturesConfig?: () => void;
handleSaveFeaturesConfig?: (value: FeaturesConfigForm) => void;
nodeClick: ({ node }: { node: Node }) => void;
}
/**

View File

@@ -0,0 +1,285 @@
import { type FC, useState, useCallback, useEffect, useRef } from 'react'
import { Popover, Flex } from 'antd'
import { WarningFilled } from '@ant-design/icons'
import { useTranslation } from 'react-i18next'
import { Node } from '@antv/x6';
import type { WorkflowRef } from '@/views/ApplicationConfig/types'
import { nodeLibrary } from '../../constant'
import { getToolMethods } from '@/api/tools'
import RbDrawer from '@/components/RbDrawer'
interface CheckListProps {
workflowRef: React.RefObject<WorkflowRef>
}
interface CheckError {
key: string
message: string
}
interface NodeCheckResult {
id: string
name: string
type: string
icon: string
errors: CheckError[]
}
const allNodes = nodeLibrary.flatMap(c => c.nodes)
const nodeIconMap: Record<string, string> = Object.fromEntries(allNodes.map(n => [n.type, n.icon]))
const nodeConfigMap: Record<string, Record<string, any>> = Object.fromEntries(
allNodes.filter(n => n.config).map(n => [n.type, n.config!])
)
// Special validators for fields that need deeper checks beyond simple empty check
const specialValidators: Record<string, (val: any) => boolean> = {
// llm.messages: at least one message with non-empty content
'llm.messages': (val: any[]) => !Array.isArray(val) || !val.some(m => m?.content && String(m.content).trim()),
// knowledge-retrieval.knowledge_retrieval: knowledge_bases array must be non-empty
'knowledge-retrieval.knowledge_retrieval': (val: any) => !(val?.knowledge_bases?.length > 0),
'memory-write.messages': (val: any[]) => !Array.isArray(val) || !val.some(m => m?.content && String(m.content).trim()),
// if-else.cases: every case must have at least one expression, and every expression must be fully set
'if-else.cases': (val: any[]) => {
if (!Array.isArray(val) || !val.length) return true
return val.some(c => {
if (!c?.expressions?.length) return true
return c.expressions.some((expr: any) => {
if (!expr?.left) return true
if (['not_empty', 'empty'].includes(expr.operator)) return false
return !(!!expr.left && (!!expr.right || typeof expr.right === 'boolean' || typeof expr.right === 'number'))
})
})
},
// question-classifier.categories: every category must have a value
'question-classifier.categories': (val: any[]) => !Array.isArray(val) || !val.some(c => c?.class_name && String(c.class_name).trim()),
// var-aggregator.group_variables: must be non-empty array
'var-aggregator.group_variables': (val: any[]) => !Array.isArray(val) || !val.length,
// assigner.assignments: every item needs variable_selector + operation; value required unless operation is 'clear'
'assigner.assignments': (val: any[]) => {
if (!Array.isArray(val) || !val.length) return false
return val.some(a => {
if (!a?.variable_selector || !a?.operation) return true
if (a.operation === 'clear') return false
return a.value === undefined || a.value === null || a.value === ''
})
},
// http-request.body: binary content_type requires data
'http-request.body': (val: any) => val?.content_type === 'binary' && !val?.data,
// tool.tool_parameters: validated async via API, placeholder always returns false
'tool.tool_parameters': () => false,
// code.input_variables: if non-empty, every item must have both name and variable
'code.input_variables': (val: any[]) => Array.isArray(val) && val.length > 0 && val.some(v => !v?.name || !v?.variable),
// code.output_variables: must be non-empty
'code.output_variables': (val: any[]) => !Array.isArray(val) || !val.length,
// jinja-render.mapping: if non-empty, every item must have a name
'jinja-render.mapping': (val: any[]) => Array.isArray(val) && val.length > 0 && val.some(v => !v?.name || !v?.value),
}
function isEmpty(val: any): boolean {
console.log('validateNode isEmpty', val, val === undefined || val === null || val === '')
if (val === undefined || val === null || val === '') return true
if (Array.isArray(val)) return val.length === 0
return false
}
function validateNode(type: string, config: Record<string, any>): CheckError[] {
const errors: CheckError[] = []
const nodeConfig = nodeConfigMap[type]
if (!nodeConfig) return errors
const get = (key: string) => config[key]?.defaultValue
Object.entries(nodeConfig).forEach(([field, fieldConfig]) => {
if (!fieldConfig?.required) return
const val = get(field)
const specialKey = `${type}.${field}`
const specialValidator = specialValidators[specialKey]
const isInvalid = specialValidator ? specialValidator(val) : isEmpty(val)
console.log('validateNode', val, specialKey, specialValidator, isEmpty(val))
if (isInvalid) errors.push({ key: specialKey, message: '' })
})
// http-request body.data (binary) — not a top-level required field, check separately
if (type === 'http-request') {
const body = get('body')
if (body?.content_type === 'binary' && !body?.data) {
errors.push({ key: 'http-request.body.data', message: '' })
}
}
// console.log('nodeConfig', nodeConfigMap, nodeConfig, errors)
return errors
}
const CheckList: FC<CheckListProps> = ({ workflowRef }) => {
const { t } = useTranslation()
const [open, setOpen] = useState(false)
const [results, setResults] = useState<NodeCheckResult[]>([])
const timerRef = useRef<ReturnType<typeof setTimeout>>()
const runCheck = useCallback(async () => {
const graph = workflowRef.current?.graphRef?.current
if (!graph) return []
const nodes = graph.getNodes()
const edges = graph.getEdges()
const sourceIds = new Set<string>()
const targetIds = new Set<string>()
// child-to-child edges within same parent (cycle)
const childTargetIds = new Set<string>()
edges.forEach(e => {
sourceIds.add(e.getSourceCellId())
targetIds.add(e.getTargetCellId())
const srcData = graph.getCellById(e.getSourceCellId())?.getData()
const tgtData = graph.getCellById(e.getTargetCellId())?.getData()
if (srcData?.cycle && tgtData?.cycle && srcData.cycle === tgtData.cycle) {
childTargetIds.add(e.getTargetCellId())
}
})
const checked: NodeCheckResult[] = []
for (const node of nodes) {
const data = node.getData()
if (!data || ['add-node', 'notes', 'cycle-start', 'break'].includes(data.type)) continue
const errors: CheckError[] = []
// Check connectivity
const isChildNode = !!data.cycle
const hasIncoming = isChildNode ? childTargetIds.has(node.id) : !['start', 'cycle-start'].includes(data.type) ? targetIds.has(node.id) : true
if (!hasIncoming) {
errors.push({ key: 'notConnected', message: t('workflow.notConnected') })
}
// Validate config
const configErrors = validateNode(data.type, data.config ?? {})
configErrors.forEach(e => {
errors.push({ key: e.key, message: `${t(`workflow.checkListErrors.${e.key}`)} ${t('workflow.cannotBeEmpty')}`.trim() })
})
// Tool node: fetch parameters via API and check required fields
if (data.type === 'tool') {
const toolId = data.config?.tool_id?.defaultValue ?? data.config?.tool_id
const toolParameters = data.config?.tool_parameters?.defaultValue ?? data.config?.tool_parameters ?? {}
if (toolId) {
try {
const methods = await getToolMethods(toolId) as Array<{ name: string; parameters: Array<{ name: string; required: boolean }> }>
const operation = toolParameters?.operation
const method = operation ? methods.find(m => m.name === operation) : methods[0]
if (method) {
const missingParams = method.parameters.filter(p => p.required && (toolParameters[p.name] === undefined || toolParameters[p.name] === null || toolParameters[p.name] === ''))
missingParams.forEach(p => errors.push({ key: 'tool.tool_parameters', message: `${p.name} ${t('workflow.cannotBeEmpty')}` }))
}
} catch {
// ignore API errors
}
}
}
if (errors.length) {
checked.push({
id: node.id,
name: data.name || t(`workflow.${data.type}`),
type: data.type,
icon: nodeIconMap[data.type] ?? '',
errors,
})
}
}
return checked
}, [workflowRef.current?.graphRef?.current, t])
const scheduleCheck = useCallback(() => {
clearTimeout(timerRef.current)
timerRef.current = setTimeout(async () => {
setResults(await runCheck())
}, 500)
}, [runCheck])
useEffect(() => {
const graph = workflowRef.current?.graphRef?.current
if (!graph) return
const events = ['node:added', 'node:removed', 'node:change:data', 'edge:added', 'edge:removed']
events.forEach(e => graph.on(e, scheduleCheck))
scheduleCheck()
return () => {
events.forEach(e => graph.off(e, scheduleCheck))
clearTimeout(timerRef.current)
}
}, [workflowRef.current?.graphRef?.current])
const handleOpen = () => {
setOpen(true)
}
const focusNode = (id: string) => {
const graph = workflowRef.current?.graphRef?.current
if (!graph) return
const node = graph.getCellById(id)
if (node) {
workflowRef.current?.nodeClick({node} as { node: Node })
}
setOpen(false)
}
return (
<>
<Popover content={t('workflow.checkList')} classNames={{ body: 'rb:py-0.5! rb:px-1! rb:rounded-[6px]! rb:text-[12px]!' }}>
<div className="rb:relative rb:cursor-pointer rb:size-7.5" onClick={handleOpen}>
<div className="rb:size-7.5 rb:border rb:border-[#EBEBEB] rb:hover:bg-[#F6F6F6] rb:rounded-[10px] rb:bg-[url('@/assets/images/workflow/checkList.svg')] rb:bg-size-[16px_16px] rb:bg-center rb:bg-no-repeat" />
{results.length > 0 && (
<span className="rb:absolute rb:-top-1 rb:-right-1 rb:min-w-3.5 rb:h-3.5 rb:px-0.5 rb:bg-[#F04438] rb:text-white rb:text-[9px] rb:leading-3.5 rb:rounded-full rb:flex rb:items-center rb:justify-center">
{results.reduce((sum, n) => sum + n.errors.length, 0)}
</span>
)}
</div>
</Popover>
<RbDrawer
title={
<span className="rb:text-[16px] rb:font-semibold">
{t('workflow.checkList')}{results.length > 0 ? `(${results.reduce((sum, n) => sum + n.errors.length, 0)})` : ''}
</span>
}
open={open}
onClose={() => setOpen(false)}
width={360}
styles={{ body: { padding: '12px 16px' } }}
>
<p className="rb:text-[12px] rb:text-[#5B6167] rb:mb-3">{t('workflow.checkListDesc')}</p>
{results.length === 0
? <div className="rb:text-center rb:text-[#5B6167] rb:text-[13px] rb:py-8">{t('workflow.checkListEmpty')}</div>
: <Flex vertical gap={8} className="rb:pb-3!">
{results.map(node => (
<div key={node.id} className="rb-border rb:rounded-lg">
<Flex align="center" gap={8} className="rb:px-3! rb:py-2.5! rb-border-b">
<div className={`rb:size-5 rb:rounded-md rb:bg-size-[14px_14px] rb:bg-center rb:bg-no-repeat ${node.icon}`} />
<span className="rb:text-[13px] rb:font-medium rb:flex-1 rb:truncate">{node.name}</span>
<span
className="rb:text-[12px] rb:text-[#155EEF] rb:cursor-pointer rb:whitespace-nowrap"
onClick={() => focusNode(node.id)}
>
{t('workflow.goto')}
</span>
</Flex>
<Flex vertical gap={4} className="rb:px-3! rb:py-2!">
{node.errors.map((err, i) => (
<Flex key={i} align="center" gap={6}>
<WarningFilled className="rb:text-[#FF5D34]! rb:text-[12px] rb:shrink-0" />
<span className="rb:text-[12px] rb:text-[#5B6167]">{err.message}</span>
</Flex>
))}
</Flex>
</div>
))}
</Flex>
}
</RbDrawer>
</>
)
}
export default CheckList

View File

@@ -27,7 +27,8 @@ const OutputList: FC<OutputListProps> = ({ label, name, extra }) => {
<>
<Flex align="center" justify="space-between" className="rb:mb-2!">
<div className="rb:text-[12px] rb:font-medium rb:leading-4.5">
{label}
<span className="rb:text-[#ff5d34] rb:text-[14px] rb:font-[SimSun,sans-serif] rb:mr-1">*</span>{label}
</div>
<Space size={8}>

View File

@@ -58,7 +58,7 @@ const ConditionList: FC<CaseListProps> = ({
const { t } = useTranslation();
const form = Form.useFormInstance();
const handleLeftFieldChange = (index: number, newValue: string) => {
const handleLeftFieldChange = (index: number, newValue?: string | string[]) => {
form.setFieldsValue({
[parentName]: {
expressions: {

View File

@@ -87,7 +87,9 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an
return (
<>
<Flex align="center" justify="space-between" className="rb:mb-1!">
<div className="rb:font-medium rb:text-[12px] rb:leading-4.5">API</div>
<div className="rb:font-medium rb:text-[12px] rb:leading-4.5">
<span className="rb:text-[#ff5d34] rb:text-[14px] rb:font-[SimSun,sans-serif] rb:mr-1">*</span>API
</div>
<Button onClick={handleChangeAuth}
size="small"
type="text"
@@ -145,7 +147,7 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an
/>
</Form.Item>
<Form.Item label="BODY" className="rb:mb-0!">
<Form.Item label="BODY" className="rb:mb-0!" required>
<Form.Item name={['body', 'content_type']} className="rb:mb-3!">
<Radio.Group
size="small"

View File

@@ -114,7 +114,7 @@ const Knowledge: FC<{value?: KnowledgeConfig; onChange?: (config: KnowledgeConfi
<div>
<Flex align="center" justify="space-between" className="rb:mb-2!">
<div className="rb:text-[12px] rb:font-medium rb:leading-4.5">
{t('application.knowledgeBaseAssociation')}
<span className="rb:text-[#ff5d34] rb:text-[14px] rb:font-[SimSun,sans-serif] rb:mr-1">*</span>{t('application.knowledgeBaseAssociation')}
</div>
<Button

View File

@@ -18,6 +18,7 @@ const ModelConfig: FC = () => {
name="model_id"
label={t('workflow.config.llm.model_id')}
className={model_id ? 'rb:mb-2!' : 'rb:mb-4!'}
required
>
<ModelSelect
placeholder={t('common.pleaseSelect')}

View File

@@ -41,7 +41,7 @@ const ParamsList: FC<ParamsListProps> = ({
return (
<div>
<div className="rb:leading-4.25 rb:text-[12px] rb:font-medium rb:mb-2">
{label}
<span className="rb:text-[#ff5d34] rb:text-[14px] rb:font-[SimSun,sans-serif] rb:mr-1">*</span>{label}
</div>
<Flex gap={10} vertical>

View File

@@ -186,7 +186,7 @@ const VariableSelect: FC<VariableSelectProps> = ({
const nodeData = (parentOfSelected ?? selectedSuggestion)?.nodeData;
return (
<div ref={containerRef} className="rb:relative rb:w-full">
<div ref={containerRef} className={`rb:relative rb:w-full ${className}`}>
{/* Trigger */}
<div
className={clsx(

View File

@@ -585,7 +585,7 @@ const Properties: FC<PropertiesProps> = ({
if (config.type === 'messageEditor') {
return (
<Form.Item key={key} name={key} label={selectedNode?.data?.type === 'memory-write' ? t(`workflow.config.${selectedNode?.data?.type}.${key}`) : undefined}>
<Form.Item key={key} name={key} required={config.required} label={selectedNode?.data?.type === 'memory-write' ? t(`workflow.config.${selectedNode?.data?.type}.${key}`) : undefined}>
<MessageEditor
title={t(`workflow.config.${selectedNode?.data?.type}.${key}`)}
placeholder={t(config.placeholder || 'common.pleaseEnter')}
@@ -733,6 +733,7 @@ const Properties: FC<PropertiesProps> = ({
: ''
}
hidden={Boolean(config.hidden)}
required={config.required}
>
{config.type === 'input'
? <Input placeholder={t('common.pleaseEnter')} />

View File

@@ -64,11 +64,11 @@ export const nodeLibrary: NodeLibrary[] = [
}
}
},
{
type: "end", icon: 'rb:bg-[url("@/assets/images/workflow/end.svg")]',
{ type: "end", icon: 'rb:bg-[url("@/assets/images/workflow/end.svg")]',
config: {
output: {
type: 'editor'
type: 'editor',
required: true,
}
}
},
@@ -82,6 +82,7 @@ export const nodeLibrary: NodeLibrary[] = [
config: {
model_id: {
type: 'define',
required: true,
params: { type: 'llm,chat' }, // llm/chat
valueKey: 'id',
labelKey: 'name',
@@ -106,6 +107,7 @@ export const nodeLibrary: NodeLibrary[] = [
},
messages: {
type: 'define',
required: true,
defaultValue: [
{
role: 'SYSTEM',
@@ -138,7 +140,8 @@ export const nodeLibrary: NodeLibrary[] = [
type: 'variableList',
},
knowledge_retrieval: {
type: 'knowledge'
type: 'knowledge',
required: true,
}
}
},
@@ -146,15 +149,18 @@ export const nodeLibrary: NodeLibrary[] = [
config: {
model_id: {
type: 'modelSelect',
required: true,
params: { type: 'llm,chat' }, // llm/chat
},
text: {
type: 'variableList',
required: true,
filterLoopIterationVars: true,
placeholder: 'workflow.config.parameter-extractor.textPlaceholder'
},
params: {
type: 'paramList',
required: true,
},
prompt: {
type: 'messageEditor',
@@ -173,16 +179,19 @@ export const nodeLibrary: NodeLibrary[] = [
config: {
message: {
type: 'editor',
required: true,
isArray: false
},
config_id: {
type: 'customSelect',
required: true,
url: memoryConfigListUrl,
valueKey: 'config_id',
labelKey: 'config_name'
},
search_switch: {
type: 'select',
required: true,
options: [
{ value: '0', label: 'memoryConversation.deepThinking' },
{ value: '1', label: 'memoryConversation.normalReply' },
@@ -201,12 +210,14 @@ export const nodeLibrary: NodeLibrary[] = [
},
messages: {
type: 'messageEditor',
required: true,
defaultValue: [],
placeholder: 'workflow.config.llm.messagesPlaceholder',
isArray: true
},
config_id: {
type: 'customSelect',
required: true,
url: memoryConfigListUrl,
valueKey: 'config_id',
labelKey: 'config_name'
@@ -222,6 +233,7 @@ export const nodeLibrary: NodeLibrary[] = [
config: {
cases: {
type: 'caseList',
required: true,
defaultValue: [
{
logical_operator: 'and',
@@ -235,13 +247,16 @@ export const nodeLibrary: NodeLibrary[] = [
config: {
model_id: {
type: 'modelSelect',
required: true,
params: { type: 'llm,chat' }, // llm/chat
},
input_variable: {
type: 'variableList',
required: true,
},
categories: {
type: 'categoryList',
required: true,
defaultValue: [
{},
{}
@@ -259,6 +274,7 @@ export const nodeLibrary: NodeLibrary[] = [
config: {
input: {
type: 'variableList',
required: true,
filterNodeTypes: ['knowledge-retrieval', 'iteration', 'loop', 'parameter-extractor', 'code', 'CONVERSATION'],
filterVariableNames: ['message']
},
@@ -281,6 +297,7 @@ export const nodeLibrary: NodeLibrary[] = [
},
output: {
type: 'variableList',
required: true,
filterChildNodes: true
},
output_type: {
@@ -321,6 +338,7 @@ export const nodeLibrary: NodeLibrary[] = [
},
group_variables: {
type: 'groupVariableList',
required: true,
defaultValue: [],
},
group_type: {
@@ -332,6 +350,7 @@ export const nodeLibrary: NodeLibrary[] = [
config: {
assignments: {
type: 'assignmentList',
required: true,
filterLoopIterationVars: true
}
}
@@ -357,6 +376,7 @@ export const nodeLibrary: NodeLibrary[] = [
},
url: {
type: 'messageEditor',
required: true,
isArray: false,
},
auth: {
@@ -415,6 +435,7 @@ export const nodeLibrary: NodeLibrary[] = [
config: {
input_variables: {
type: 'inputList',
required: true,
defaultValue: [{ name: 'arg1' }, { name: 'arg2' }]
},
language: {
@@ -423,6 +444,7 @@ export const nodeLibrary: NodeLibrary[] = [
},
code: {
type: 'messageEditor',
required: true,
isArray: false,
language: ['python3', 'javascript'],
titleVariant: 'borderless',
@@ -433,6 +455,7 @@ export const nodeLibrary: NodeLibrary[] = [
},
output_variables: {
type: 'outputList',
required: true,
defaultValue: [{name: 'result', type: 'string'}]
},
}
@@ -441,10 +464,12 @@ export const nodeLibrary: NodeLibrary[] = [
config: {
mapping: {
type: 'mappingList',
required: true,
defaultValue: [{name: 'arg1'}]
},
template: {
type: 'messageEditor',
required: true,
isArray: false,
language: 'jinja2',
titleVariant: 'borderless',
@@ -456,6 +481,7 @@ export const nodeLibrary: NodeLibrary[] = [
config: {
file_selector: {
type: 'variableList',
required: true,
placeholder: 'common.pleaseSelect',
onFilterVariableType: ['array[file]', 'file']
}
@@ -465,6 +491,7 @@ export const nodeLibrary: NodeLibrary[] = [
config: {
input_list: {
type: 'variableList',
required: true,
},
filter_by: {
type: 'define',

View File

@@ -18,7 +18,6 @@ import { getWorkflowConfig, saveWorkflowConfig } from '@/api/application'
import { useUser } from '@/store/user';
import type { FeaturesConfigForm } from '@/views/ApplicationConfig/types'
import { calcConditionNodeTotalHeight, getConditionNodeCasePortY } from '../utils'
import type { Suggestion } from '../components/Editor/plugin/AutocompletePlugin';
/**
* Props for useWorkflowGraph hook
@@ -76,6 +75,7 @@ export interface UseWorkflowGraphReturn {
features?: FeaturesConfigForm;
/** Get start node output variable list (user-defined + system variables) */
getStartNodeVariables: () => Array<{ name: string; type: string; readonly?: boolean }>;
nodeClick: ({ node }: { node: Node }) => void;
}
/**
@@ -1494,6 +1494,7 @@ export const useWorkflowGraph = ({
setIsHandMode,
onDrop,
blankClick,
nodeClick,
deleteEvent,
copyEvent,
parseEvent,

View File

@@ -28,6 +28,7 @@ const Workflow = forwardRef<WorkflowRef, { onFeaturesLoad?: (features: FeaturesC
setIsHandMode,
onDrop,
blankClick,
nodeClick,
deleteEvent,
copyEvent,
parseEvent,
@@ -71,7 +72,8 @@ const Workflow = forwardRef<WorkflowRef, { onFeaturesLoad?: (features: FeaturesC
config,
features: features,
handleFeaturesConfig,
handleSaveFeaturesConfig
handleSaveFeaturesConfig,
nodeClick
}))
return (
<div className="rb:h-full rb:relative">

View File

@@ -31,6 +31,7 @@ export interface NodeConfig {
group_variables?: Array<{ key: string, value: string[] }>
cycle?: string;
cycle_vars?: Array<{ name: string; type: string; value: string; input_type: string; }>
required?: boolean;
[key: string]: unknown;
}