refractor(web): 增加输入节点

This commit is contained in:
2025-07-20 17:17:22 +08:00
parent 77a09472aa
commit a5282762ed
14 changed files with 462 additions and 257 deletions

View File

@@ -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<string, Record<string, any>>, nodes: Node[], edges: Edge[], data: any) => void = (inputSchema, nodes, edges, data) => {
@@ -79,9 +92,8 @@ export const checkSave: (inputSchema: Record<string, Record<string, any>>, 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<string, Record<string, any>>, 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)
}
}
}

View File

@@ -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 (
<FlowableDiv className="h-full w-full">
{contextHolder}
<ReactFlow
className="rounded-xl"
nodes={nodes}
@@ -133,7 +131,7 @@ function FlowEditor(props: FlowEditorProps) {
props.onGraphDataChange({nodes, edges, data})
} catch (e) {
// @ts-ignore
messageApi.error(e.toString())
message.error(e.toString())
}
}}>
<SaveFilled/>

View File

@@ -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: <i className="fa fa-message"/>,
description: '使用大模型对话',
component: LlmNode,
checkers: [inputMultiVariableChecker],
checkers: {
add: [],
save: [inputMultiVariableChecker]
},
},
{
key: 'knowledge-node',
@@ -65,7 +76,10 @@ export const NodeRegistry: NodeDefine[] = [
icon: <i className="fa fa-book-bookmark"/>,
description: '',
component: KnowledgeNode,
checkers: [inputMultiVariableChecker],
checkers: {
add: [],
save: [inputMultiVariableChecker]
},
},
{
key: 'code-node',
@@ -74,7 +88,10 @@ export const NodeRegistry: NodeDefine[] = [
icon: <i className="fa fa-code"/>,
description: '执行自定义的处理代码',
component: CodeNode,
checkers: [inputMultiVariableChecker],
checkers: {
add: [],
save: [inputMultiVariableChecker]
},
},
{
key: 'template-node',
@@ -83,7 +100,10 @@ export const NodeRegistry: NodeDefine[] = [
icon: <i className="fa fa-pen-nib"/>,
description: '使用模板聚合转换变量表示',
component: TemplateNode,
checkers: [inputMultiVariableChecker],
checkers: {
add: [],
save: [inputMultiVariableChecker]
},
},
{
key: 'switch-node',
@@ -92,7 +112,10 @@ export const NodeRegistry: NodeDefine[] = [
icon: <i className="fa fa-code-fork"/>,
description: '根据不同的情况前往不同的分支',
component: SwitchNode,
checkers: [],
checkers: {
add: [],
save: [],
},
},
{
key: 'loop-node',
@@ -101,16 +124,35 @@ export const NodeRegistry: NodeDefine[] = [
icon: <i className="fa fa-repeat"/>,
description: '实现循环执行流程',
component: LoopNode,
checkers: [],
checkers: {
add: [],
save: [],
},
},
// 特殊节点特殊判断
{
key: 'input-node',
group: '数据节点',
name: '输入',
icon: <i className="fa fa-file"/>,
description: '定义流程输入变量',
component: InputNode,
checkers: {
add: [noMoreThanOneNodeType],
save: [],
},
},
{
key: 'output-node',
group: '输出节点',
group: '数据节点',
name: '输出',
icon: <i className="fa fa-file"/>,
description: '定义输出变量',
description: '定义流程输出变量',
component: OutputNode,
checkers: [inputSingleVariableChecker('output')],
checkers: {
add: [noMoreThanOneNodeType],
save: [inputSingleVariableChecker('output')]
},
},
]

View File

@@ -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())
}
},
}}

View File

@@ -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<JSX.Element>(<></>)
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',

View File

@@ -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<string, string> = {
'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={<NormalNodeHandler/>}
/>
)

View File

@@ -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<string, OutputVariableType> = {
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<string, { type: OutputVariableType }> = {}
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 (
<AmisNode
className={nodeClassName('input')}
nodeProps={props}
formSchema={formSchema}
handler={<StartNodeHandler/>}
/>
)
}
export default React.memo(InputNode)

View File

@@ -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 (
<AmisNode
className={nodeClassName('knowledge')}
nodeProps={props}
columnSchema={columnsSchema}
formSchema={formSchema}
handler={<NormalNodeHandler/>}
/>
)

View File

@@ -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<string, string> = {
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={<NormalNodeHandler/>}
/>
)

View File

@@ -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={<NormalNodeHandler/>}
resize={{
minWidth: 350,

View File

@@ -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 (
<AmisNode
className={nodeClassName('output')}
nodeProps={props}
columnSchema={columnsSchema}
formSchema={formSchema}
handler={<EndNodeHandler/>}
/>
)

View File

@@ -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}
/>
)

View File

@@ -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<string, string> = {
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={<NormalNodeHandler/>}
/>
)

View File

@@ -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<string, Record<string, any>>, nodes: Node[], edges: Edge[], data: any) => NodeError
export type AddNodeChecker = (type: string, parentId: string | undefined, inputSchema: Record<string, Record<string, any>>, nodes: Node[], edges: Edge[], data: any) => NodeError
export type SaveNodeChecker = (id: string, parentId: string | undefined, inputSchema: Record<string, Record<string, any>>, 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<string, { actions: ListenerAction[] }>
columns: Schema[]
}