feat(web): 增加模板节点
This commit is contained in:
@@ -1,6 +1,8 @@
|
|||||||
import {type Edge, getIncomers, type Node} from '@xyflow/react'
|
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 Queue from 'yocto-queue'
|
||||||
|
import type {InputFormOptions, InputFormOptionsGroup} from './types.ts'
|
||||||
|
|
||||||
export const getAllIncomerNodeById: (id: string, nodes: Node[], edges: Edge[]) => string[] = (id, nodes, edges) => {
|
export const getAllIncomerNodeById: (id: string, nodes: Node[], edges: Edge[]) => string[] = (id, nodes, edges) => {
|
||||||
let queue = new Queue<Node>()
|
let queue = new Queue<Node>()
|
||||||
@@ -36,3 +38,47 @@ export const getAllIncomerNodeOutputVariables: (id: string, nodes: Node[], edges
|
|||||||
}
|
}
|
||||||
return incomerVariables
|
return incomerVariables
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const generateAllIncomerOutputVariablesFormOptions: (id: string, inputSchema: Record<string, Record<string, any>>, 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,
|
||||||
|
})),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,9 +6,32 @@ import KnowledgeNode from './node/KnowledgeNode.tsx'
|
|||||||
import LlmNode from './node/LlmNode.tsx'
|
import LlmNode from './node/LlmNode.tsx'
|
||||||
import OutputNode from './node/OutputNode.tsx'
|
import OutputNode from './node/OutputNode.tsx'
|
||||||
import SwitchNode from './node/SwitchNode.tsx'
|
import SwitchNode from './node/SwitchNode.tsx'
|
||||||
|
import TemplateNode from './node/TemplateNode.tsx'
|
||||||
import type {NodeChecker} from './types.ts'
|
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] ?? {}
|
let nodeData = data[id] ?? {}
|
||||||
if (has(nodeData, 'inputs')) {
|
if (has(nodeData, 'inputs')) {
|
||||||
let inputs = nodeData?.inputs ?? {}
|
let inputs = nodeData?.inputs ?? {}
|
||||||
@@ -49,7 +72,7 @@ export const NodeRegistry: NodeDefine[] = [
|
|||||||
icon: <i className="fa fa-message"/>,
|
icon: <i className="fa fa-message"/>,
|
||||||
description: '使用大模型对话',
|
description: '使用大模型对话',
|
||||||
component: LlmNode,
|
component: LlmNode,
|
||||||
checkers: [inputVariableChecker],
|
checkers: [inputMultiVariableChecker],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'knowledge-node',
|
key: 'knowledge-node',
|
||||||
@@ -58,7 +81,7 @@ export const NodeRegistry: NodeDefine[] = [
|
|||||||
icon: <i className="fa fa-book-bookmark"/>,
|
icon: <i className="fa fa-book-bookmark"/>,
|
||||||
description: '',
|
description: '',
|
||||||
component: KnowledgeNode,
|
component: KnowledgeNode,
|
||||||
checkers: [inputVariableChecker],
|
checkers: [inputMultiVariableChecker],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'code-node',
|
key: 'code-node',
|
||||||
@@ -67,7 +90,16 @@ export const NodeRegistry: NodeDefine[] = [
|
|||||||
icon: <i className="fa fa-code"/>,
|
icon: <i className="fa fa-code"/>,
|
||||||
description: '执行自定义的处理代码',
|
description: '执行自定义的处理代码',
|
||||||
component: CodeNode,
|
component: CodeNode,
|
||||||
checkers: [inputVariableChecker],
|
checkers: [inputMultiVariableChecker],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'template-node',
|
||||||
|
group: '普通节点',
|
||||||
|
name: '模板替换',
|
||||||
|
icon: <i className="fa fa-pen-nib"/>,
|
||||||
|
description: '使用模板聚合转换变量表示',
|
||||||
|
component: TemplateNode,
|
||||||
|
checkers: [inputMultiVariableChecker],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'switch-node',
|
key: 'switch-node',
|
||||||
@@ -85,7 +117,7 @@ export const NodeRegistry: NodeDefine[] = [
|
|||||||
icon: <i className="fa fa-file"/>,
|
icon: <i className="fa fa-file"/>,
|
||||||
description: '定义输出变量',
|
description: '定义输出变量',
|
||||||
component: OutputNode,
|
component: OutputNode,
|
||||||
checkers: [inputVariableChecker],
|
checkers: [inputSingleVariableChecker('output')],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -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 Edge, Handle, type Node, type NodeProps, Position} from '@xyflow/react'
|
||||||
import type {Schema} from 'amis'
|
import type {Schema} from 'amis'
|
||||||
import {Button, Card, Drawer} from 'antd'
|
import {Button, Card, Drawer} from 'antd'
|
||||||
import {has, isEmpty} from 'licia'
|
|
||||||
import {type JSX, useCallback, useState} from 'react'
|
import {type JSX, useCallback, useState} from 'react'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
import {amisRender, commonInfo, horizontalFormOptions} from '../../../util/amis.tsx'
|
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 {useDataStore} from '../store/DataStore.ts'
|
||||||
import {useFlowStore} from '../store/FlowStore.ts'
|
import {useFlowStore} from '../store/FlowStore.ts'
|
||||||
import type {InputFormOptions, InputFormOptionsGroup} from '../types.ts'
|
|
||||||
|
|
||||||
export function inputsFormColumns(
|
export function inputsFormColumns(
|
||||||
nodeId: string,
|
nodeId: string,
|
||||||
@@ -18,41 +16,6 @@ export function inputsFormColumns(
|
|||||||
edges: Edge[],
|
edges: Edge[],
|
||||||
data: any,
|
data: any,
|
||||||
): Schema[] {
|
): 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 [
|
return [
|
||||||
{
|
{
|
||||||
type: 'input-kvs',
|
type: 'input-kvs',
|
||||||
@@ -72,12 +35,13 @@ export function inputsFormColumns(
|
|||||||
label: '变量',
|
label: '变量',
|
||||||
required: true,
|
required: true,
|
||||||
selectMode: 'group',
|
selectMode: 'group',
|
||||||
options: [
|
options: generateAllIncomerOutputVariablesFormOptions(
|
||||||
...inputVariables.map(item => ({
|
nodeId,
|
||||||
label: item.group,
|
inputSchema,
|
||||||
children: item.variables,
|
nodes,
|
||||||
})),
|
edges,
|
||||||
],
|
data,
|
||||||
|
),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,59 +1,31 @@
|
|||||||
import type {NodeProps} from '@xyflow/react'
|
import type {NodeProps} from '@xyflow/react'
|
||||||
import {Tag} from 'antd'
|
|
||||||
import React, {useCallback} from 'react'
|
import React, {useCallback} from 'react'
|
||||||
|
import {generateAllIncomerOutputVariablesFormOptions} from '../Helper.tsx'
|
||||||
import {useContextStore} from '../store/ContextStore.ts'
|
import {useContextStore} from '../store/ContextStore.ts'
|
||||||
import {useDataStore} from '../store/DataStore.ts'
|
import {useDataStore} from '../store/DataStore.ts'
|
||||||
import {useFlowStore} from '../store/FlowStore.ts'
|
import {useFlowStore} from '../store/FlowStore.ts'
|
||||||
import AmisNode, {EndNodeHandler, inputsFormColumns} from './AmisNode.tsx'
|
import AmisNode, {EndNodeHandler} from './AmisNode.tsx'
|
||||||
|
|
||||||
const typeMap: Record<string, string> = {
|
|
||||||
markdown: 'Markdown',
|
|
||||||
json: 'JSON',
|
|
||||||
'template-markdown': 'Markdown 模板',
|
|
||||||
'template-rich-text': '富文本模板',
|
|
||||||
}
|
|
||||||
|
|
||||||
const OutputNode = (props: NodeProps) => {
|
const OutputNode = (props: NodeProps) => {
|
||||||
const {getNodes, getEdges} = useFlowStore()
|
const {getNodes, getEdges} = useFlowStore()
|
||||||
const {getData, getDataById} = useDataStore()
|
const {getData} = useDataStore()
|
||||||
const {getInputSchema} = useContextStore()
|
const {getInputSchema} = useContextStore()
|
||||||
|
|
||||||
const nodeData = getDataById(props.id)
|
|
||||||
|
|
||||||
const columnsSchema = useCallback(
|
const columnsSchema = useCallback(
|
||||||
() => [
|
() => [
|
||||||
...inputsFormColumns(props.id, getInputSchema(), getNodes(), getEdges(), getData()),
|
|
||||||
{
|
|
||||||
type: 'divider',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
type: 'select',
|
type: 'select',
|
||||||
name: 'type',
|
name: 'output',
|
||||||
label: '输出类型',
|
label: '输出变量',
|
||||||
required: true,
|
required: true,
|
||||||
selectFirst: true,
|
selectMode: 'group',
|
||||||
options: Object.keys(typeMap).map(key => ({label: typeMap[key], value: key})),
|
options: generateAllIncomerOutputVariablesFormOptions(
|
||||||
},
|
props.id,
|
||||||
{
|
getInputSchema(),
|
||||||
visibleOn: 'type === \'template-markdown\'',
|
getNodes(),
|
||||||
type: 'editor',
|
getEdges(),
|
||||||
required: true,
|
getData(),
|
||||||
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],
|
[props.id],
|
||||||
@@ -62,14 +34,6 @@ const OutputNode = (props: NodeProps) => {
|
|||||||
return (
|
return (
|
||||||
<AmisNode
|
<AmisNode
|
||||||
nodeProps={props}
|
nodeProps={props}
|
||||||
extraNodeDescription={
|
|
||||||
nodeData?.type
|
|
||||||
? <div className="mt-2 flex justify-between">
|
|
||||||
<span>输出类型</span>
|
|
||||||
<Tag className="m-0" color="blue">{typeMap[nodeData.type]}</Tag>
|
|
||||||
</div>
|
|
||||||
: <></>
|
|
||||||
}
|
|
||||||
columnSchema={columnsSchema}
|
columnSchema={columnsSchema}
|
||||||
handler={<EndNodeHandler/>}
|
handler={<EndNodeHandler/>}
|
||||||
/>
|
/>
|
||||||
|
|||||||
79
service-web/client/src/components/flow/node/TemplateNode.tsx
Normal file
79
service-web/client/src/components/flow/node/TemplateNode.tsx
Normal file
@@ -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<string, string> = {
|
||||||
|
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 (
|
||||||
|
<AmisNode
|
||||||
|
nodeProps={props}
|
||||||
|
extraNodeDescription={
|
||||||
|
nodeData?.type
|
||||||
|
? <div className="mt-2 flex justify-between">
|
||||||
|
<span>模板类型</span>
|
||||||
|
<Tag className="m-0" color="blue">{typeMap[nodeData.type]}</Tag>
|
||||||
|
</div>
|
||||||
|
: <></>
|
||||||
|
}
|
||||||
|
columnSchema={columnsSchema}
|
||||||
|
handler={<EndNodeHandler/>}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default React.memo(TemplateNode)
|
||||||
Reference in New Issue
Block a user