diff --git a/service-web/client/src/components/flow/FlowChecker.tsx b/service-web/client/src/components/flow/FlowChecker.tsx index f68ce7d..601d791 100644 --- a/service-web/client/src/components/flow/FlowChecker.tsx +++ b/service-web/client/src/components/flow/FlowChecker.tsx @@ -20,8 +20,21 @@ export class CheckError extends Error { const getNodeById = (id: string, nodes: Node[]) => find(nodes, (n: Node) => isEqual(n.id, id)) +export const typeNotFound = (type: string) => new CheckError(100, `类型 ${type} 不存在`) +export const addNodeError = (message?: string) => new CheckError(101, message ?? '无法添加节点') + // @ts-ignore -export const checkAddNode: (type: string, nodes: Node[], edges: Edge[]) => void = (type, nodes, edges) => { +export const checkAddNode: (type: string, parentId: string | undefined, nodes: Node[], edges: Edge[]) => void = (type, parentId, nodes, edges) => { + let nodeDefine = NodeRegistryMap[type] + if (!nodeDefine) { + throw typeNotFound(type) + } + for (const checker of nodeDefine.checkers.add) { + let checkResult = checker(type, parentId, {}, nodes, edges, undefined) + if (checkResult.error) { + throw addNodeError(checkResult.message) + } + } } export const sourceNodeNotFoundError = () => new CheckError(200, '连线起始节点未找到') @@ -70,7 +83,7 @@ export const checkAddConnection: (connection: Connection, nodes: Node[], edges: export const atLeastOneNode = () => new CheckError(300, '至少包含一个节点') export const hasUnfinishedNode = (nodeId: string) => new CheckError(301, `存在尚未配置完成的节点: ${nodeId}`) export const nodeTypeNotFound = () => new CheckError(302, '节点类型不存在') -export const nodeError = (nodeId: string, reason?: string) => new CheckError(303, reason ?? `节点配置存在错误:${nodeId}`) +export const saveNodeError = (nodeId: string, reason?: string) => new CheckError(303, reason ?? `节点配置存在错误:${nodeId}`) // @ts-ignore export const checkSave: (inputSchema: Record>, nodes: Node[], edges: Edge[], data: any) => void = (inputSchema, nodes, edges, data) => { @@ -79,9 +92,8 @@ export const checkSave: (inputSchema: Record>, nodes } for (let node of nodes) { - let nodeId = node.id - if (!has(data, nodeId) || !data[nodeId]?.finished) { - throw hasUnfinishedNode(nodeId) + if (!has(data, node.id) || !data[node.id]?.finished) { + throw hasUnfinishedNode(node.id) } if (!has(node, 'type')) { @@ -89,10 +101,10 @@ export const checkSave: (inputSchema: Record>, nodes } let nodeType = node.type! let nodeDefine = NodeRegistryMap[nodeType] - for (let checker of nodeDefine.checkers) { - let checkResult = checker(nodeId, inputSchema, nodes, edges, data) + for (let checker of nodeDefine.checkers.save) { + let checkResult = checker(node.id, node.parentId, inputSchema, nodes, edges, data) if (checkResult.error) { - throw nodeError(nodeId, checkResult.message) + throw saveNodeError(node.id, checkResult.message) } } } diff --git a/service-web/client/src/components/flow/FlowEditor.tsx b/service-web/client/src/components/flow/FlowEditor.tsx index ded628a..c7db2fd 100644 --- a/service-web/client/src/components/flow/FlowEditor.tsx +++ b/service-web/client/src/components/flow/FlowEditor.tsx @@ -46,7 +46,6 @@ const FlowableDiv = styled.div` function FlowEditor(props: FlowEditorProps) { const navigate = useNavigate() - const [messageApi, contextHolder] = message.useMessage() const {data, setData} = useDataStore() const { @@ -84,7 +83,6 @@ function FlowEditor(props: FlowEditorProps) { return ( - {contextHolder} diff --git a/service-web/client/src/components/flow/NodeRegistry.tsx b/service-web/client/src/components/flow/NodeRegistry.tsx index 097a0cc..f0d71c1 100644 --- a/service-web/client/src/components/flow/NodeRegistry.tsx +++ b/service-web/client/src/components/flow/NodeRegistry.tsx @@ -1,4 +1,4 @@ -import {has, isEmpty} from 'licia' +import {has, isEmpty, isEqual} from 'licia' import {getAllIncomerNodeOutputVariables} from './Helper.tsx' import CodeNode from './node/CodeNode.tsx' import KnowledgeNode from './node/KnowledgeNode.tsx' @@ -7,10 +7,11 @@ import LoopNode from './node/LoopNode.tsx' import OutputNode from './node/OutputNode.tsx' import SwitchNode from './node/SwitchNode.tsx' import TemplateNode from './node/TemplateNode.tsx' -import type {NodeChecker, NodeDefine} from './types.ts' +import type {AddNodeChecker, NodeDefine, SaveNodeChecker} from './types.ts' +import InputNode from './node/InputNode.tsx' -const inputSingleVariableChecker: (field: string) => NodeChecker = field => { - return (id, inputSchema, nodes, edges, data) => { +const inputSingleVariableChecker: (field: string) => SaveNodeChecker = field => { + return (id, _parentId, inputSchema, nodes, edges, data) => { let nodeData = data[id] ?? {} if (has(nodeData, field)) { let expression = nodeData?.[field] ?? '' @@ -28,7 +29,7 @@ const inputSingleVariableChecker: (field: string) => NodeChecker = field => { } } -const inputMultiVariableChecker: NodeChecker = (id, inputSchema, nodes, edges, data) => { +const inputMultiVariableChecker: SaveNodeChecker = (id, _parentId, inputSchema, nodes, edges, data) => { let nodeData = data[id] ?? {} if (has(nodeData, 'inputs')) { let inputs = nodeData?.inputs ?? {} @@ -48,6 +49,13 @@ const inputMultiVariableChecker: NodeChecker = (id, inputSchema, nodes, edges, d return {error: false} } +const noMoreThanOneNodeType: AddNodeChecker = (type, parentId, _inputSchema, nodes) => { + return { + error: nodes.filter(n => isEqual(n.parentId, parentId) && isEqual(n.type, type)).length > 0, + message: `同一个流程(子流程)中类型为 ${type} 的节点至多有一个` + } +} + export const NodeRegistry: NodeDefine[] = [ { key: 'llm-node', @@ -56,7 +64,10 @@ export const NodeRegistry: NodeDefine[] = [ icon: , description: '使用大模型对话', component: LlmNode, - checkers: [inputMultiVariableChecker], + checkers: { + add: [], + save: [inputMultiVariableChecker] + }, }, { key: 'knowledge-node', @@ -65,7 +76,10 @@ export const NodeRegistry: NodeDefine[] = [ icon: , description: '', component: KnowledgeNode, - checkers: [inputMultiVariableChecker], + checkers: { + add: [], + save: [inputMultiVariableChecker] + }, }, { key: 'code-node', @@ -74,7 +88,10 @@ export const NodeRegistry: NodeDefine[] = [ icon: , description: '执行自定义的处理代码', component: CodeNode, - checkers: [inputMultiVariableChecker], + checkers: { + add: [], + save: [inputMultiVariableChecker] + }, }, { key: 'template-node', @@ -83,7 +100,10 @@ export const NodeRegistry: NodeDefine[] = [ icon: , description: '使用模板聚合转换变量表示', component: TemplateNode, - checkers: [inputMultiVariableChecker], + checkers: { + add: [], + save: [inputMultiVariableChecker] + }, }, { key: 'switch-node', @@ -92,7 +112,10 @@ export const NodeRegistry: NodeDefine[] = [ icon: , description: '根据不同的情况前往不同的分支', component: SwitchNode, - checkers: [], + checkers: { + add: [], + save: [], + }, }, { key: 'loop-node', @@ -101,16 +124,35 @@ export const NodeRegistry: NodeDefine[] = [ icon: , description: '实现循环执行流程', component: LoopNode, - checkers: [], + checkers: { + add: [], + save: [], + }, + }, + // 特殊节点特殊判断 + { + key: 'input-node', + group: '数据节点', + name: '输入', + icon: , + description: '定义流程输入变量', + component: InputNode, + checkers: { + add: [noMoreThanOneNodeType], + save: [], + }, }, { key: 'output-node', - group: '输出节点', + group: '数据节点', name: '输出', icon: , - description: '定义输出变量', + description: '定义流程输出变量', component: OutputNode, - checkers: [inputSingleVariableChecker('output')], + checkers: { + add: [noMoreThanOneNodeType], + save: [inputSingleVariableChecker('output')] + }, }, ] diff --git a/service-web/client/src/components/flow/component/AddNodeButton.tsx b/service-web/client/src/components/flow/component/AddNodeButton.tsx index a668469..f603717 100644 --- a/service-web/client/src/components/flow/component/AddNodeButton.tsx +++ b/service-web/client/src/components/flow/component/AddNodeButton.tsx @@ -1,5 +1,5 @@ import {PlusCircleFilled} from '@ant-design/icons' -import {Button, Dropdown} from 'antd' +import {Button, Dropdown, message} from 'antd' import type {ButtonProps} from 'antd/lib' import {isEqual, randomId, unique} from 'licia' import {commonInfo} from '../../../util/amis.tsx' @@ -34,7 +34,7 @@ const AddNodeButton = (props: AddNodeButtonProps) => { if (commonInfo.debug) { console.info('Add', key, JSON.stringify({nodes, edges, data})) } - checkAddNode(key, nodes, edges) + checkAddNode(key, props.parent, nodes, edges) let nodeId = randomId(10, 'qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM') let define = NodeRegistryMap[key] @@ -62,7 +62,7 @@ const AddNodeButton = (props: AddNodeButtonProps) => { }) } catch (e) { // @ts-ignore - messageApi.error(e.toString()) + message.error(e.toString()) } }, }} diff --git a/service-web/client/src/components/flow/node/AmisNode.tsx b/service-web/client/src/components/flow/node/AmisNode.tsx index 01f3fe7..d3a44f5 100644 --- a/service-web/client/src/components/flow/node/AmisNode.tsx +++ b/service-web/client/src/components/flow/node/AmisNode.tsx @@ -8,7 +8,7 @@ import {amisRender, commonInfo, horizontalFormOptions} from '../../../util/amis. import {generateAllIncomerOutputVariablesFormOptions} from '../Helper.tsx' import {useDataStore} from '../store/DataStore.ts' import {useFlowStore} from '../store/FlowStore.ts' -import {OutputVariableTypeMap} from '../types.ts' +import {type FormSchema, OutputVariableTypeMap} from '../types.ts' export function inputsFormColumns( nodeId: string, @@ -88,7 +88,7 @@ type AmisNodeProps = { nodeProps: NodeProps extraNodeDescription?: JSX.Element handler: JSX.Element - columnSchema?: () => Schema[] + formSchema?: () => FormSchema, resize?: { minWidth: number, minHeight: number } } @@ -122,7 +122,7 @@ const AmisNode: (props: AmisNodeProps) => JSX.Element = ({ nodeProps, extraNodeDescription, handler, - columnSchema, + formSchema, resize, }) => { const {removeNode} = useFlowStore() @@ -136,6 +136,7 @@ const AmisNode: (props: AmisNodeProps) => JSX.Element = ({ const [editDrawerOpen, setEditDrawerOpen] = useState(false) const [editDrawerForm, setEditDrawerForm] = useState(<>) const onOpenEditDrawerClick = useCallback(() => { + const schema = formSchema?.() setEditDrawerForm( amisRender( { @@ -166,6 +167,7 @@ const AmisNode: (props: AmisNodeProps) => JSX.Element = ({ }, ], }, + ...(schema?.events ?? {}) }, body: [ { @@ -183,7 +185,7 @@ const AmisNode: (props: AmisNodeProps) => JSX.Element = ({ { type: 'divider', }, - ...(columnSchema?.() ?? []), + ...(schema?.columns ?? []), { type: 'wrapper', size: 'none', diff --git a/service-web/client/src/components/flow/node/CodeNode.tsx b/service-web/client/src/components/flow/node/CodeNode.tsx index 87fb721..028b37f 100644 --- a/service-web/client/src/components/flow/node/CodeNode.tsx +++ b/service-web/client/src/components/flow/node/CodeNode.tsx @@ -5,6 +5,7 @@ import {useContextStore} from '../store/ContextStore.ts' import {useDataStore} from '../store/DataStore.ts' import {useFlowStore} from '../store/FlowStore.ts' import AmisNode, {inputsFormColumns, nodeClassName, NormalNodeHandler, outputsFormColumns} from './AmisNode.tsx' +import type {FormSchema} from '../types.ts' const languageMap: Record = { 'javascript': 'Javascript', @@ -19,34 +20,36 @@ const CodeNode = (props: NodeProps) => { const nodeData = getDataById(props.id) - const columnsSchema = useCallback(() => [ - ...inputsFormColumns(props.id, getInputSchema(), getNodes(), getEdges(), getData()), - { - type: 'divider', - }, - { - type: 'select', - name: 'type', - label: '代码类型', - required: true, - selectFirst: true, - options: Object.keys(languageMap).map(key => ({label: languageMap[key], value: key})), - }, - { - type: 'editor', - required: true, - label: '代码内容', - name: 'content', - language: '${type}', - options: { - wordWrap: 'bounded', + const formSchema: () => FormSchema = useCallback(() => ({ + columns: [ + ...inputsFormColumns(props.id, getInputSchema(), getNodes(), getEdges(), getData()), + { + type: 'divider', }, - }, - { - type: 'divider', - }, - ...outputsFormColumns(true, false), - ], [props.id]) + { + type: 'select', + name: 'type', + label: '代码类型', + required: true, + selectFirst: true, + options: Object.keys(languageMap).map(key => ({label: languageMap[key], value: key})), + }, + { + type: 'editor', + required: true, + label: '代码内容', + name: 'content', + language: '${type}', + options: { + wordWrap: 'bounded', + }, + }, + { + type: 'divider', + }, + ...outputsFormColumns(true, false), + ] + }), [props.id]) const extraNodeDescription = useMemo(() => { return nodeData?.type @@ -62,7 +65,7 @@ const CodeNode = (props: NodeProps) => { className={nodeClassName('code')} nodeProps={props} extraNodeDescription={extraNodeDescription} - columnSchema={columnsSchema} + formSchema={formSchema} handler={} /> ) diff --git a/service-web/client/src/components/flow/node/InputNode.tsx b/service-web/client/src/components/flow/node/InputNode.tsx new file mode 100644 index 0000000..9fa64c8 --- /dev/null +++ b/service-web/client/src/components/flow/node/InputNode.tsx @@ -0,0 +1,123 @@ +import type {NodeProps} from '@xyflow/react' +import React, {useCallback} from 'react' +import AmisNode, {nodeClassName, outputsFormColumns, StartNodeHandler} from './AmisNode.tsx' +import {horizontalFormOptions} from '../../../util/amis.tsx' +import {typeMap} from '../../../pages/ai/task/InputSchema.tsx' +import type {FormSchema, OutputVariableType} from '../types.ts' +import {isEmpty} from 'licia' + +const originTypeMap: Record = { + text: 'text', + textarea: 'text', + number: 'number', + files: 'array-text', +} + +const InputNode = (props: NodeProps) => { + const formSchema: () => FormSchema = useCallback(() => ({ + events: { + change: { + actions: [ + { + actionType: 'validate', + }, + { + actionType: 'custom', + // @ts-ignore + script: (context, doAction, event) => { + let data = event?.data + console.log(data) + if (data && isEmpty(data?.validateResult?.error ?? undefined)) { + let inputs = data.validateResult?.payload?.inputs ?? {} + if (inputs) { + let outputs: Record = {} + for (let key of Object.keys(inputs)) { + outputs[key] = { + type: originTypeMap[inputs[key].type], + } + } + doAction({ + actionType: 'setValue', + args: { + value: { + outputs + }, + }, + }) + } + } + }, + }, + ] + } + }, + columns: [ + { + type: 'input-kvs', + name: 'inputs', + label: '输入变量', + required: true, + addButtonText: '新增入参', + draggable: false, + keyItem: { + label: '参数名称', + ...horizontalFormOptions(), + validations: { + isAlphanumeric: true, + }, + }, + valueItems: [ + { + ...horizontalFormOptions(), + type: 'input-text', + name: 'label', + required: true, + label: '中文名称', + clearValueOnEmpty: true, + clearable: true, + }, + { + ...horizontalFormOptions(), + type: 'input-text', + name: 'description', + label: '参数描述', + clearValueOnEmpty: true, + clearable: true, + }, + { + ...horizontalFormOptions(), + type: 'select', + name: 'type', + label: '参数类型', + required: true, + selectFirst: true, + options: Object.keys(typeMap).map(key => ({label: typeMap[key], value: key})), + }, + { + ...horizontalFormOptions(), + type: 'switch', + name: 'required', + label: '是否必填', + required: true, + value: true, + }, + ], + }, + { + type: 'divider', + }, + ...outputsFormColumns(false, false), + ] + }), [props.id]) + + return ( + } + /> + ) +} + +export default React.memo(InputNode) \ No newline at end of file diff --git a/service-web/client/src/components/flow/node/KnowledgeNode.tsx b/service-web/client/src/components/flow/node/KnowledgeNode.tsx index 00892c5..c0d1d04 100644 --- a/service-web/client/src/components/flow/node/KnowledgeNode.tsx +++ b/service-web/client/src/components/flow/node/KnowledgeNode.tsx @@ -5,6 +5,7 @@ import {useContextStore} from '../store/ContextStore.ts' import {useDataStore} from '../store/DataStore.ts' import {useFlowStore} from '../store/FlowStore.ts' import AmisNode, {inputsFormColumns, nodeClassName, NormalNodeHandler, outputsFormColumns} from './AmisNode.tsx' +import type {FormSchema} from '../types.ts' const KnowledgeNode = (props: NodeProps) => { const {getNodes, getEdges} = useFlowStore() @@ -24,64 +25,66 @@ const KnowledgeNode = (props: NodeProps) => { ) }, [props.id]) - const columnsSchema = useCallback(() => [ - ...inputsFormColumns(props.id, getInputSchema(), getNodes(), getEdges(), getData()), - { - type: 'divider', - }, - { - type: 'select', - name: 'knowledgeId', - label: '知识库', - required: true, - options: [], - source: { - method: 'get', - url: `${commonInfo.baseAiUrl}/knowledge/list`, - // @ts-ignore - adaptor: (payload, response, api, context) => { - return { - ...payload, - data: { - items: payload.data.items.map((item: any) => ({value: item['id'], label: item['name']})), - }, - } + const formSchema: () => FormSchema = useCallback(() => ({ + columns: [ + ...inputsFormColumns(props.id, getInputSchema(), getNodes(), getEdges(), getData()), + { + type: 'divider', + }, + { + type: 'select', + name: 'knowledgeId', + label: '知识库', + required: true, + options: [], + source: { + method: 'get', + url: `${commonInfo.baseAiUrl}/knowledge/list`, + // @ts-ignore + adaptor: (payload, response, api, context) => { + return { + ...payload, + data: { + items: payload.data.items.map((item: any) => ({value: item['id'], label: item['name']})), + }, + } + }, }, }, - }, - { - type: 'input-text', - name: 'query', - label: '查询文本', - required: true, - }, - { - type: 'input-range', - name: 'count', - label: '返回数量', - required: true, - value: 3, - max: 10, - }, - { - type: 'input-range', - name: 'score', - label: '匹配阀值', - required: true, - value: 0.6, - max: 1, - step: 0.05, - }, - { - type: 'divider', - }, - ...outputsFormColumns(false, true), - ], [props.id]) + { + type: 'input-text', + name: 'query', + label: '查询文本', + required: true, + }, + { + type: 'input-range', + name: 'count', + label: '返回数量', + required: true, + value: 3, + max: 10, + }, + { + type: 'input-range', + name: 'score', + label: '匹配阀值', + required: true, + value: 0.6, + max: 1, + step: 0.05, + }, + { + type: 'divider', + }, + ...outputsFormColumns(false, true), + ] + }), [props.id]) return ( } /> ) diff --git a/service-web/client/src/components/flow/node/LlmNode.tsx b/service-web/client/src/components/flow/node/LlmNode.tsx index 7bdbb25..343dfd2 100644 --- a/service-web/client/src/components/flow/node/LlmNode.tsx +++ b/service-web/client/src/components/flow/node/LlmNode.tsx @@ -5,6 +5,7 @@ import {useContextStore} from '../store/ContextStore.ts' import {useDataStore} from '../store/DataStore.ts' import {useFlowStore} from '../store/FlowStore.ts' import AmisNode, {inputsFormColumns, nodeClassName, NormalNodeHandler, outputsFormColumns} from './AmisNode.tsx' +import type {FormSchema} from '../types.ts' const modelMap: Record = { qwen3: 'Qwen3', @@ -31,30 +32,32 @@ const LlmNode = (props: NodeProps) => { ) }, [props.id]) - const columnsSchema = useCallback(() => [ - ...inputsFormColumns(props.id, getInputSchema(), getNodes(), getEdges(), getData()), - { - type: 'divider', - }, - { - type: 'select', - name: 'model', - label: '大模型', - required: true, - selectFirst: true, - options: Object.keys(modelMap).map(key => ({label: modelMap[key], value: key})), - }, - { - type: 'textarea', - name: 'systemPrompt', - label: '系统提示词', - required: true, - }, - { - type: 'divider', - }, - ...outputsFormColumns(false, true), - ], [props.id]) + const formSchema: () => FormSchema = useCallback(() => ({ + columns: [ + ...inputsFormColumns(props.id, getInputSchema(), getNodes(), getEdges(), getData()), + { + type: 'divider', + }, + { + type: 'select', + name: 'model', + label: '大模型', + required: true, + selectFirst: true, + options: Object.keys(modelMap).map(key => ({label: modelMap[key], value: key})), + }, + { + type: 'textarea', + name: 'systemPrompt', + label: '系统提示词', + required: true, + }, + { + type: 'divider', + }, + ...outputsFormColumns(false, true), + ] + }), [props.id]) const extraNodeDescription = useMemo(() => { return nodeData?.model @@ -70,7 +73,7 @@ const LlmNode = (props: NodeProps) => { className={nodeClassName('llm')} nodeProps={props} extraNodeDescription={extraNodeDescription} - columnSchema={columnsSchema} + formSchema={formSchema} handler={} /> ) diff --git a/service-web/client/src/components/flow/node/LoopNode.tsx b/service-web/client/src/components/flow/node/LoopNode.tsx index c34069e..4b3d7fd 100644 --- a/service-web/client/src/components/flow/node/LoopNode.tsx +++ b/service-web/client/src/components/flow/node/LoopNode.tsx @@ -6,7 +6,7 @@ import {generateAllIncomerOutputVariablesFormOptions} from '../Helper.tsx' import {useContextStore} from '../store/ContextStore.ts' import {useDataStore} from '../store/DataStore.ts' import {useFlowStore} from '../store/FlowStore.ts' -import {flowBackgroundColor, flowDotColor} from '../types.ts' +import {flowBackgroundColor, flowDotColor, type FormSchema} from '../types.ts' import AmisNode, {nodeClassName, NormalNodeHandler, outputsFormColumns} from './AmisNode.tsx' const LoopNode = (props: NodeProps) => { @@ -31,87 +31,89 @@ const LoopNode = (props: NodeProps) => { ) }, [props.id]) - const columnsSchema = useCallback(() => [ - { - type: 'switch', - name: 'failFast', - label: '快速失败', - required: true, - description: '执行过程中一旦出现错误,及时中断循环任务的执行', - }, - { - disabled: true, - type: 'switch', - name: 'parallel', - label: '并行执行', - required: true, - }, - { - type: 'select', - name: 'type', - label: '循环模式', - required: true, - options: [ - { - label: '次数循环', - value: 'for', - }, - { - label: '次数循环 (引用变量)', - value: 'for-variable', - }, - { - label: '对象循环', - value: 'for-object', - }, - ], - }, - { - visibleOn: '${type === \'for\'}', - type: 'input-number', - name: 'count', - label: '循环次数', - required: true, - min: 1, - precision: 0, - }, - { - visibleOn: '${type === \'for-variable\'}', - type: 'select', - name: 'countVariable', - label: '循环次数', - required: true, - selectMode: 'group', - options: generateAllIncomerOutputVariablesFormOptions( - props.id, - getInputSchema(), - getNodes(), - getEdges(), - getData(), - ['number'], - ), - }, - { - visibleOn: '${type === \'for-object\'}', - type: 'select', - name: 'countObject', - label: '循环对象', - required: true, - selectMode: 'group', - options: generateAllIncomerOutputVariablesFormOptions( - props.id, - getInputSchema(), - getNodes(), - getEdges(), - getData(), - ['array-text', 'array-object'], - ), - }, - { - type: 'divider', - }, - ...outputsFormColumns(false, true), - ], [props.id]) + const formSchema: () => FormSchema = useCallback(() => ({ + columns: [ + { + type: 'switch', + name: 'failFast', + label: '快速失败', + required: true, + description: '执行过程中一旦出现错误,及时中断循环任务的执行', + }, + { + disabled: true, + type: 'switch', + name: 'parallel', + label: '并行执行', + required: true, + }, + { + type: 'select', + name: 'type', + label: '循环模式', + required: true, + options: [ + { + label: '次数循环', + value: 'for', + }, + { + label: '次数循环 (引用变量)', + value: 'for-variable', + }, + { + label: '对象循环', + value: 'for-object', + }, + ], + }, + { + visibleOn: '${type === \'for\'}', + type: 'input-number', + name: 'count', + label: '循环次数', + required: true, + min: 1, + precision: 0, + }, + { + visibleOn: '${type === \'for-variable\'}', + type: 'select', + name: 'countVariable', + label: '循环次数', + required: true, + selectMode: 'group', + options: generateAllIncomerOutputVariablesFormOptions( + props.id, + getInputSchema(), + getNodes(), + getEdges(), + getData(), + ['number'], + ), + }, + { + visibleOn: '${type === \'for-object\'}', + type: 'select', + name: 'countObject', + label: '循环对象', + required: true, + selectMode: 'group', + options: generateAllIncomerOutputVariablesFormOptions( + props.id, + getInputSchema(), + getNodes(), + getEdges(), + getData(), + ['array-text', 'array-object'], + ), + }, + { + type: 'divider', + }, + ...outputsFormColumns(false, true), + ] + }), [props.id]) const extraNodeDescription = useMemo(() => { return ( @@ -142,7 +144,7 @@ const LoopNode = (props: NodeProps) => { }} nodeProps={props} extraNodeDescription={extraNodeDescription} - columnSchema={columnsSchema} + formSchema={formSchema} handler={} resize={{ minWidth: 350, diff --git a/service-web/client/src/components/flow/node/OutputNode.tsx b/service-web/client/src/components/flow/node/OutputNode.tsx index fd4d5cf..179073d 100644 --- a/service-web/client/src/components/flow/node/OutputNode.tsx +++ b/service-web/client/src/components/flow/node/OutputNode.tsx @@ -5,14 +5,15 @@ import {useContextStore} from '../store/ContextStore.ts' import {useDataStore} from '../store/DataStore.ts' import {useFlowStore} from '../store/FlowStore.ts' import AmisNode, {EndNodeHandler, nodeClassName} from './AmisNode.tsx' +import type {FormSchema} from '../types.ts' const OutputNode = (props: NodeProps) => { const {getNodes, getEdges} = useFlowStore() const {getData} = useDataStore() const {getInputSchema} = useContextStore() - const columnsSchema = useCallback( - () => [ + const formSchema: () => FormSchema = useCallback(() => ({ + columns: [ { type: 'select', name: 'output', @@ -27,13 +28,14 @@ const OutputNode = (props: NodeProps) => { getData(), ), }, - ], [props.id]) + ] + }), [props.id]) return ( } /> ) diff --git a/service-web/client/src/components/flow/node/SwitchNode.tsx b/service-web/client/src/components/flow/node/SwitchNode.tsx index f0d1b61..a037e35 100644 --- a/service-web/client/src/components/flow/node/SwitchNode.tsx +++ b/service-web/client/src/components/flow/node/SwitchNode.tsx @@ -8,6 +8,7 @@ import {useContextStore} from '../store/ContextStore.ts' import {useDataStore} from '../store/DataStore.ts' import {useFlowStore} from '../store/FlowStore.ts' import AmisNode, {nodeClassName} from './AmisNode.tsx' +import type {FormSchema} from '../types.ts' const SwitchNode = (props: NodeProps) => { const {getNodes, getEdges, removeEdges} = useFlowStore() @@ -18,32 +19,34 @@ const SwitchNode = (props: NodeProps) => { // @ts-ignore const conditions: ConditionValue[] = nodeData?.conditions?.map(c => c.condition) ?? [] - const columnsSchema = useCallback(() => [ - { - type: 'combo', - name: 'conditions', - label: '分支', - multiple: true, - required: true, - items: [ - { - type: 'condition-builder', - name: 'condition', - label: '条件', - required: true, - builderMode: 'simple', - showANDOR: true, - fields: generateAllIncomerOutputVariablesConditions( - props.id, - getInputSchema(), - getNodes(), - getEdges(), - getData(), - ), - }, - ], - }, - ], [props.id]) + const formSchema: () => FormSchema = useCallback(() => ({ + columns: [ + { + type: 'combo', + name: 'conditions', + label: '分支', + multiple: true, + required: true, + items: [ + { + type: 'condition-builder', + name: 'condition', + label: '条件', + required: true, + builderMode: 'simple', + showANDOR: true, + fields: generateAllIncomerOutputVariablesConditions( + props.id, + getInputSchema(), + getNodes(), + getEdges(), + getData(), + ), + }, + ], + }, + ] + }), [props.id]) const extraNodeDescription = useMemo(() => { return ( @@ -88,7 +91,7 @@ const SwitchNode = (props: NodeProps) => { className={nodeClassName('switch')} nodeProps={props} extraNodeDescription={extraNodeDescription} - columnSchema={columnsSchema} + formSchema={formSchema} handler={handler} /> ) diff --git a/service-web/client/src/components/flow/node/TemplateNode.tsx b/service-web/client/src/components/flow/node/TemplateNode.tsx index fee8271..d8005bb 100644 --- a/service-web/client/src/components/flow/node/TemplateNode.tsx +++ b/service-web/client/src/components/flow/node/TemplateNode.tsx @@ -5,6 +5,7 @@ import {useContextStore} from '../store/ContextStore.ts' import {useDataStore} from '../store/DataStore.ts' import {useFlowStore} from '../store/FlowStore.ts' import AmisNode, {inputsFormColumns, nodeClassName, NormalNodeHandler, outputsFormColumns} from './AmisNode.tsx' +import type {FormSchema} from '../types.ts' const typeMap: Record = { default: '默认', @@ -33,8 +34,8 @@ const TemplateNode = (props: NodeProps) => { ) }, [props.id]) - const columnsSchema = useCallback( - () => [ + const formSchema: () => FormSchema = useCallback(() => ({ + columns: [ ...inputsFormColumns(props.id, getInputSchema(), getNodes(), getEdges(), getData()), { type: 'divider', @@ -69,7 +70,8 @@ const TemplateNode = (props: NodeProps) => { }, }, ...outputsFormColumns(false, true), - ], [props.id]) + ] + }), [props.id]) const extraNodeDescription = useMemo(() => { return nodeData?.type @@ -85,7 +87,7 @@ const TemplateNode = (props: NodeProps) => { className={nodeClassName('template')} nodeProps={props} extraNodeDescription={extraNodeDescription} - columnSchema={columnsSchema} + formSchema={formSchema} handler={} /> ) diff --git a/service-web/client/src/components/flow/types.ts b/service-web/client/src/components/flow/types.ts index f009df1..5d3279b 100644 --- a/service-web/client/src/components/flow/types.ts +++ b/service-web/client/src/components/flow/types.ts @@ -1,5 +1,6 @@ import type {Edge, Node} from '@xyflow/react' import type {JSX} from 'react' +import type {ListenerAction, Schema} from 'amis' export const flowBackgroundColor = '#fafafa' export const flowDotColor = '#dedede' @@ -19,7 +20,8 @@ export type NodeError = { message?: string, } -export type NodeChecker = (id: string, inputSchema: Record>, nodes: Node[], edges: Edge[], data: any) => NodeError +export type AddNodeChecker = (type: string, parentId: string | undefined, inputSchema: Record>, nodes: Node[], edges: Edge[], data: any) => NodeError +export type SaveNodeChecker = (id: string, parentId: string | undefined, inputSchema: Record>, nodes: Node[], edges: Edge[], data: any) => NodeError export type GraphData = { nodes: Node[], edges: Edge[], data: any } @@ -47,7 +49,10 @@ export type NodeDefine = { icon: JSX.Element, description: string, component: any, - checkers: NodeChecker[], + checkers: { + add: AddNodeChecker[], + save: SaveNodeChecker[], + }, } export type OutputVariable = { @@ -56,3 +61,8 @@ export type OutputVariable = { type: OutputVariableType, variable: string, } + +export type FormSchema = { + events?: Record + columns: Schema[] +}