diff --git a/service-web/client/src/components/flow/Helper.tsx b/service-web/client/src/components/flow/Helper.tsx index 504b0f1..7f69900 100644 --- a/service-web/client/src/components/flow/Helper.tsx +++ b/service-web/client/src/components/flow/Helper.tsx @@ -1,6 +1,8 @@ import {type Edge, getIncomers, type Node} from '@xyflow/react' -import {find, has, isEqual, unique} from 'licia' +import type {Option} from 'amis/lib/Schema' +import {find, has, isEmpty, isEqual, unique} from 'licia' import Queue from 'yocto-queue' +import type {InputFormOptions, InputFormOptionsGroup} from './types.ts' export const getAllIncomerNodeById: (id: string, nodes: Node[], edges: Edge[]) => string[] = (id, nodes, edges) => { let queue = new Queue() @@ -36,3 +38,47 @@ export const getAllIncomerNodeOutputVariables: (id: string, nodes: Node[], edges } return incomerVariables } + +export const generateAllIncomerOutputVariablesFormOptions: (id: string, inputSchema: Record>, nodes: Node[], edges: Edge[], data: any) => Option[] = (id, inputSchema, nodes, edges, data) => { + let inputSchemaVariables: InputFormOptions[] = Object.keys(inputSchema).map(key => ({ + label: `${key} (${inputSchema[key]?.label ?? ''})`, + value: key, + })) + + let incomerIds = getAllIncomerNodeById(id, nodes, edges) + let incomerVariables: InputFormOptionsGroup[] = [] + for (const incomerId of incomerIds) { + let nodeData = data[incomerId] ?? {} + let group = incomerId + if (has(nodeData, 'node') && has(nodeData.node, 'name')) { + group = `${nodeData.node.name} ${incomerId}` + } + if (has(nodeData, 'outputs')) { + let outputs = nodeData?.outputs ?? [] + incomerVariables.push({ + group: group, + variables: Object.keys(outputs).map(key => ({ + value: `${incomerId}.${key}`, + label: key, + })), + }) + } + } + + let inputVariables = [ + ...(isEmpty(inputSchemaVariables) ? [] : [ + { + group: '流程入参', + variables: inputSchemaVariables, + }, + ]), + ...incomerVariables, + ] + + return [ + ...inputVariables.map(item => ({ + label: item.group, + children: item.variables, + })), + ] +} diff --git a/service-web/client/src/components/flow/NodeRegistry.tsx b/service-web/client/src/components/flow/NodeRegistry.tsx index 8ad892a..43c40f6 100644 --- a/service-web/client/src/components/flow/NodeRegistry.tsx +++ b/service-web/client/src/components/flow/NodeRegistry.tsx @@ -6,9 +6,32 @@ import KnowledgeNode from './node/KnowledgeNode.tsx' import LlmNode from './node/LlmNode.tsx' import OutputNode from './node/OutputNode.tsx' import SwitchNode from './node/SwitchNode.tsx' +import TemplateNode from './node/TemplateNode.tsx' import type {NodeChecker} from './types.ts' -const inputVariableChecker: NodeChecker = (id, inputSchema, nodes, edges, data) => { +const inputSingleVariableChecker: (field: string) => NodeChecker = field => { + return (id, inputSchema, nodes, edges, data) => { + let nodeData = data[id] ?? {} + if (has(nodeData, field)) { + let expression = nodeData?.[field] ?? '' + if (!isEmpty(expression)) { + let outputVariables = new Set([ + ...getAllIncomerNodeOutputVariables(id, nodes, edges, data).map(i => `${i.id}.${i.variable}`), + ...Object.keys(inputSchema), + ]) + if (!outputVariables.has(expression)) { + return { + error: true, + message: `节点 ${id} 存在错误:变量 ${expression} 不存在`, + } + } + } + } + return {error: false} + } +} + +const inputMultiVariableChecker: NodeChecker = (id, inputSchema, nodes, edges, data) => { let nodeData = data[id] ?? {} if (has(nodeData, 'inputs')) { let inputs = nodeData?.inputs ?? {} @@ -49,7 +72,7 @@ export const NodeRegistry: NodeDefine[] = [ icon: , description: '使用大模型对话', component: LlmNode, - checkers: [inputVariableChecker], + checkers: [inputMultiVariableChecker], }, { key: 'knowledge-node', @@ -58,7 +81,7 @@ export const NodeRegistry: NodeDefine[] = [ icon: , description: '', component: KnowledgeNode, - checkers: [inputVariableChecker], + checkers: [inputMultiVariableChecker], }, { key: 'code-node', @@ -67,7 +90,16 @@ export const NodeRegistry: NodeDefine[] = [ icon: , description: '执行自定义的处理代码', component: CodeNode, - checkers: [inputVariableChecker], + checkers: [inputMultiVariableChecker], + }, + { + key: 'template-node', + group: '普通节点', + name: '模板替换', + icon: , + description: '使用模板聚合转换变量表示', + component: TemplateNode, + checkers: [inputMultiVariableChecker], }, { key: 'switch-node', @@ -85,7 +117,7 @@ export const NodeRegistry: NodeDefine[] = [ icon: , description: '定义输出变量', component: OutputNode, - checkers: [inputVariableChecker], + checkers: [inputSingleVariableChecker('output')], }, ] diff --git a/service-web/client/src/components/flow/node/AmisNode.tsx b/service-web/client/src/components/flow/node/AmisNode.tsx index cd34159..66f2bd0 100644 --- a/service-web/client/src/components/flow/node/AmisNode.tsx +++ b/service-web/client/src/components/flow/node/AmisNode.tsx @@ -2,14 +2,12 @@ import {CopyFilled, DeleteFilled, EditFilled} from '@ant-design/icons' import {type Edge, Handle, type Node, type NodeProps, Position} from '@xyflow/react' import type {Schema} from 'amis' import {Button, Card, Drawer} from 'antd' -import {has, isEmpty} from 'licia' import {type JSX, useCallback, useState} from 'react' import styled from 'styled-components' import {amisRender, commonInfo, horizontalFormOptions} from '../../../util/amis.tsx' -import {getAllIncomerNodeById} from '../Helper.tsx' +import {generateAllIncomerOutputVariablesFormOptions} from '../Helper.tsx' import {useDataStore} from '../store/DataStore.ts' import {useFlowStore} from '../store/FlowStore.ts' -import type {InputFormOptions, InputFormOptionsGroup} from '../types.ts' export function inputsFormColumns( nodeId: string, @@ -18,41 +16,6 @@ export function inputsFormColumns( edges: Edge[], data: any, ): Schema[] { - let inputSchemaVariables: InputFormOptions[] = Object.keys(inputSchema).map(key => ({ - label: `${key} (${inputSchema[key]?.label ?? ''})`, - value: key, - })) - - let incomerIds = getAllIncomerNodeById(nodeId, nodes, edges) - let incomerVariables: InputFormOptionsGroup[] = [] - for (const incomerId of incomerIds) { - let nodeData = data[incomerId] ?? {} - let group = incomerId - if (has(nodeData, 'node') && has(nodeData.node, 'name')) { - group = `${nodeData.node.name} ${incomerId}` - } - if (has(nodeData, 'outputs')) { - let outputs = nodeData?.outputs ?? [] - incomerVariables.push({ - group: group, - variables: Object.keys(outputs).map(key => ({ - value: `${incomerId}.${key}`, - label: key, - })), - }) - } - } - - let inputVariables = [ - ...(isEmpty(inputSchemaVariables) ? [] : [ - { - group: '流程入参', - variables: inputSchemaVariables, - }, - ]), - ...incomerVariables, - ] - return [ { type: 'input-kvs', @@ -72,12 +35,13 @@ export function inputsFormColumns( label: '变量', required: true, selectMode: 'group', - options: [ - ...inputVariables.map(item => ({ - label: item.group, - children: item.variables, - })), - ], + options: generateAllIncomerOutputVariablesFormOptions( + nodeId, + inputSchema, + nodes, + edges, + data, + ), }, ], }, diff --git a/service-web/client/src/components/flow/node/OutputNode.tsx b/service-web/client/src/components/flow/node/OutputNode.tsx index 13f957d..15d4687 100644 --- a/service-web/client/src/components/flow/node/OutputNode.tsx +++ b/service-web/client/src/components/flow/node/OutputNode.tsx @@ -1,59 +1,31 @@ import type {NodeProps} from '@xyflow/react' -import {Tag} from 'antd' import React, {useCallback} from 'react' +import {generateAllIncomerOutputVariablesFormOptions} from '../Helper.tsx' import {useContextStore} from '../store/ContextStore.ts' import {useDataStore} from '../store/DataStore.ts' import {useFlowStore} from '../store/FlowStore.ts' -import AmisNode, {EndNodeHandler, inputsFormColumns} from './AmisNode.tsx' - -const typeMap: Record = { - markdown: 'Markdown', - json: 'JSON', - 'template-markdown': 'Markdown 模板', - 'template-rich-text': '富文本模板', -} +import AmisNode, {EndNodeHandler} from './AmisNode.tsx' const OutputNode = (props: NodeProps) => { const {getNodes, getEdges} = useFlowStore() - const {getData, getDataById} = useDataStore() + const {getData} = useDataStore() const {getInputSchema} = useContextStore() - const nodeData = getDataById(props.id) - const columnsSchema = useCallback( () => [ - ...inputsFormColumns(props.id, getInputSchema(), getNodes(), getEdges(), getData()), - { - type: 'divider', - }, { type: 'select', - name: 'type', - label: '输出类型', + name: 'output', + label: '输出变量', required: true, - selectFirst: true, - options: Object.keys(typeMap).map(key => ({label: typeMap[key], value: key})), - }, - { - visibleOn: 'type === \'template-markdown\'', - type: 'editor', - required: true, - label: '模板内容', - name: 'template', - language: 'markdown', - options: { - wordWrap: 'bounded', - }, - }, - { - visibleOn: 'type === \'template-rich-text\'', - type: 'input-rich-text', - required: true, - name: 'template', - label: '模板内容', - options: { - min_height: 500, - }, + selectMode: 'group', + options: generateAllIncomerOutputVariablesFormOptions( + props.id, + getInputSchema(), + getNodes(), + getEdges(), + getData(), + ), }, ], [props.id], @@ -62,14 +34,6 @@ const OutputNode = (props: NodeProps) => { return ( - 输出类型 - {typeMap[nodeData.type]} - - : <> - } columnSchema={columnsSchema} handler={} /> diff --git a/service-web/client/src/components/flow/node/TemplateNode.tsx b/service-web/client/src/components/flow/node/TemplateNode.tsx new file mode 100644 index 0000000..bbe243b --- /dev/null +++ b/service-web/client/src/components/flow/node/TemplateNode.tsx @@ -0,0 +1,79 @@ +import type {NodeProps} from '@xyflow/react' +import {Tag} from 'antd' +import React, {useCallback} from 'react' +import {useContextStore} from '../store/ContextStore.ts' +import {useDataStore} from '../store/DataStore.ts' +import {useFlowStore} from '../store/FlowStore.ts' +import AmisNode, {EndNodeHandler, inputsFormColumns} from './AmisNode.tsx' + +const typeMap: Record = { + default: '默认', + json: 'JSON', + 'template-markdown': 'Markdown', + 'template-rich-text': '富文本', +} + +const TemplateNode = (props: NodeProps) => { + const {getNodes, getEdges} = useFlowStore() + const {getData, getDataById} = useDataStore() + const {getInputSchema} = useContextStore() + + 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(typeMap).map(key => ({label: typeMap[key], value: key})), + }, + { + visibleOn: 'type === \'template-markdown\'', + type: 'editor', + required: true, + label: '模板内容', + name: 'template', + language: 'markdown', + options: { + wordWrap: 'bounded', + }, + }, + { + visibleOn: 'type === \'template-rich-text\'', + type: 'input-rich-text', + required: true, + name: 'template', + label: '模板内容', + options: { + min_height: 500, + }, + }, + ], + [props.id], + ) + + return ( + + 模板类型 + {typeMap[nodeData.type]} + + : <> + } + columnSchema={columnsSchema} + handler={} + /> + ) +} + +export default React.memo(TemplateNode) \ No newline at end of file