feat(web): 增加任务和流程图CRUD

This commit is contained in:
2025-07-03 22:21:31 +08:00
parent 8ebaf5de8e
commit abdbb5ed03
20 changed files with 71 additions and 105 deletions

View File

@@ -0,0 +1,199 @@
import {DeleteFilled, EditFilled} from '@ant-design/icons'
import {Handle, type HandleProps, type NodeProps, Position, useNodeConnections} from '@xyflow/react'
import type {Schema} from 'amis'
import {Card, Dropdown} from 'antd'
import {isEmpty, isEqual, isNil} from 'licia'
import {type JSX} from 'react'
import {horizontalFormOptions} from '../../../util/amis.tsx'
export type AmisNodeType = 'normal' | 'start' | 'end'
export function inputsFormColumns(required: boolean = false, preload?: any): Schema[] {
return [
{
type: 'input-kvs',
name: 'inputs',
label: '输入变量',
value: preload,
addButtonText: '新增输入',
draggable: false,
keyItem: {
...horizontalFormOptions(),
label: '参数名称',
},
required: required,
valueItems: [
{
...horizontalFormOptions(),
type: 'select',
name: 'type',
label: '变量',
required: true,
options: [],
},
],
},
]
}
export function outputsFormColumns(editable: boolean = false, required: boolean = false, preload?: any): Schema[] {
return [
{
disabled: !editable,
type: 'input-kvs',
name: 'outputs',
label: '输出变量',
value: preload,
addButtonText: '新增输出',
draggable: false,
keyItem: {
...horizontalFormOptions(),
label: '参数名称',
},
required: required,
valueItems: [
{
...horizontalFormOptions(),
type: 'select',
name: 'type',
label: '参数',
required: true,
selectFirst: true,
options: [
{
label: '文本',
value: 'string',
},
{
label: '数字',
value: 'number',
},
{
label: '文本数组',
value: 'array-string',
},
{
label: '对象数组',
value: 'array-object',
},
],
},
],
},
]
}
export const LimitHandler = (props: HandleProps & { limit: number }) => {
const connections = useNodeConnections({
handleType: props.type,
})
return (
<Handle
{...props}
isConnectable={connections.length < props.limit}
/>
)
}
type AmisNodeProps = {
nodeProps: NodeProps
type: AmisNodeType
defaultNodeName: String
defaultNodeDescription?: String
extraNodeDescription?: (nodeData: any) => JSX.Element
handlers?: (nodeData: any) => JSX.Element
columnSchema?: Schema[]
}
const AmisNode: (props: AmisNodeProps) => JSX.Element = ({
nodeProps,
type,
defaultNodeName,
defaultNodeDescription,
extraNodeDescription,
handlers,
columnSchema,
}) => {
const {id, data} = nodeProps
const {getDataById, removeNode, editNode} = data
// @ts-ignore
const nodeData = getDataById(id)
const nodeName = isEmpty(nodeData?.node?.name) ? defaultNodeName : nodeData.node.name
const nodeDescription = isEmpty(nodeData?.node?.description) ? defaultNodeDescription : nodeData.node?.description
return (
<div className="w-64">
<Dropdown
className="card-container"
trigger={['contextMenu']}
menu={{
items: [
{
key: 'edit',
label: '编辑',
icon: <EditFilled className="text-gray-600 hover:text-blue-500"/>,
},
{
key: 'remove',
label: '删除',
icon: <DeleteFilled className="text-red-500 hover:text-red-500"/>,
},
],
onClick: menu => {
switch (menu.key) {
case 'edit':
// @ts-ignore
editNode(
id,
[
{
type: 'input-text',
name: 'node.name',
label: '节点名称',
placeholder: nodeName,
},
{
type: 'textarea',
name: 'node.description',
label: '节点描述',
placeholder: nodeDescription,
},
{
type: 'divider',
},
...(columnSchema ?? []),
],
)
break
case 'remove':
// @ts-ignore
removeNode(id)
break
}
},
}}
>
<Card
className="node-card"
title={nodeName}
extra={<span className="text-gray-300 text-xs">{id}</span>}
size="small"
>
<div className="card-description p-2 text-secondary text-sm">
{nodeDescription}
{extraNodeDescription?.(nodeData)}
</div>
</Card>
</Dropdown>
{isNil(handlers)
? <>
{isEqual(type, 'start') || isEqual(type, 'normal')
? <Handle type="source" position={Position.Right} id="source"/> : undefined}
{isEqual(type, 'end') || isEqual(type, 'normal')
? <Handle type="target" position={Position.Left} id="target"/> : undefined}
</>
: handlers?.(nodeData)}
</div>
)
}
export default AmisNode

View File

@@ -0,0 +1,52 @@
import type {NodeProps} from '@xyflow/react'
import AmisNode, {inputsFormColumns, outputsFormColumns} from './AmisNode.tsx'
import React from 'react'
const CodeNode = (props: NodeProps) => AmisNode({
nodeProps: props,
type: 'normal',
defaultNodeName: '代码执行',
defaultNodeDescription: '执行自定义的处理代码',
columnSchema: [
...inputsFormColumns(),
{
type: 'divider',
},
{
type: 'select',
name: 'type',
label: '代码类型',
required: true,
options: [
{
value: 'javascript',
label: 'JavaScript',
},
{
value: 'python',
label: 'Python',
},
{
value: 'lua',
label: 'Lua',
},
],
},
{
type: 'editor',
required: true,
label: '代码内容',
name: 'content',
language: '${type}',
options: {
wordWrap: 'bounded',
},
},
{
type: 'divider',
},
...outputsFormColumns(true, true, {result: {type: 'string'}}),
],
})
export default React.memo(CodeNode)

View File

@@ -0,0 +1,13 @@
import type {NodeProps} from '@xyflow/react'
import AmisNode, {outputsFormColumns} from './AmisNode.tsx'
import React from 'react'
const EndNode = (props: NodeProps) => AmisNode({
nodeProps: props,
type: 'end',
defaultNodeName: '结束节点',
defaultNodeDescription: '定义输出变量',
columnSchema: outputsFormColumns(true),
})
export default React.memo(EndNode)

View File

@@ -0,0 +1,66 @@
import type {NodeProps} from '@xyflow/react'
import {commonInfo} from '../../../util/amis.tsx'
import AmisNode, {inputsFormColumns, outputsFormColumns} from './AmisNode.tsx'
import React from 'react'
const KnowledgeNode = (props: NodeProps) => AmisNode({
nodeProps: props,
type: 'normal',
defaultNodeName: '知识库',
defaultNodeDescription: '查询知识库获取外部知识',
columnSchema: [
...inputsFormColumns(),
{
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, {result: {type: 'array-string'}}),
],
})
export default React.memo(KnowledgeNode)

View File

@@ -0,0 +1,51 @@
import type {NodeProps} from '@xyflow/react'
import {Tag} from 'antd'
import AmisNode, {inputsFormColumns, outputsFormColumns} from './AmisNode.tsx'
import React from 'react'
const modelMap: Record<string, string> = {
qwen3: 'Qwen3',
deepseek: 'Deepseek',
}
const LlmNode = (props: NodeProps) => AmisNode({
nodeProps: props,
type: 'normal',
defaultNodeName: '大模型',
defaultNodeDescription: '使用大模型对话',
extraNodeDescription: nodeData => {
const model = nodeData?.model as string | undefined
return model
? <div className="mt-2 flex justify-between">
<span></span>
<Tag className="m-0" color="blue">{modelMap[model]}</Tag>
</div>
: <></>
},
columnSchema: [
...inputsFormColumns(),
{
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, {text: {type: 'string'}}),
],
})
export default React.memo(LlmNode)

View File

@@ -0,0 +1,68 @@
import type {NodeProps} from '@xyflow/react'
import {Tag} from 'antd'
import {each} from 'licia'
import React, {type JSX} from 'react'
import {horizontalFormOptions} from '../../../util/amis.tsx'
import AmisNode from './AmisNode.tsx'
const typeMap: Record<string, string> = {
text: '文本',
number: '数字',
files: '文件',
}
const StartNode = (props: NodeProps) => AmisNode({
nodeProps: props,
type: 'start',
defaultNodeName: '开始节点',
defaultNodeDescription: '定义输入变量',
extraNodeDescription: nodeData => {
const variables: JSX.Element[] = []
const inputs = (nodeData?.inputs ?? {}) as Record<string, { type: string, description: string }>
each(inputs, (value, key) => {
variables.push(
<div className="mt-1 flex justify-between" key={key}>
<span>{key}</span>
<Tag className="m-0" color="blue">{typeMap[value.type]}</Tag>
</div>,
)
})
return (
<div className="mt-2">
{...variables}
</div>
)
},
columnSchema: [
{
type: 'input-kvs',
name: 'inputs',
label: '输入变量',
addButtonText: '新增入参',
draggable: false,
keyItem: {
label: '参数名称',
...horizontalFormOptions(),
},
valueItems: [
{
...horizontalFormOptions(),
type: 'input-text',
name: 'description',
label: '参数描述',
},
{
...horizontalFormOptions(),
type: 'select',
name: 'type',
label: '参数类型',
required: true,
selectFirst: true,
options: Object.keys(typeMap).map(key => ({label: typeMap[key], value: key})),
},
],
},
],
})
export default React.memo(StartNode)

View File

@@ -0,0 +1,55 @@
import {Handle, type NodeProps, Position} from '@xyflow/react'
import {Tag} from 'antd'
import React from 'react'
import AmisNode from './AmisNode.tsx'
const cases = [
{
index: 1,
},
{
index: 2,
},
{
index: 3,
},
]
const SwitchNode = (props: NodeProps) => AmisNode({
nodeProps: props,
type: 'normal',
defaultNodeName: '分支节点',
defaultNodeDescription: '根据不同的情况前往不同的分支',
columnSchema: [],
// @ts-ignore
extraNodeDescription: nodeData => {
return (
<div className="mt-2">
{cases.map(item => (
<div key={item.index} className="mt-1">
<Tag className="m-0" color="blue"> {item.index}</Tag>
</div>
))}
</div>
)
},
// @ts-ignore
handlers: nodeData => {
return (
<>
<Handle type="target" position={Position.Left}/>
{cases.map((item, index) => (
<Handle
type="source"
position={Position.Right}
key={item.index}
id={`${item.index}`}
style={{top: 85 + (25 * index)}}
/>
))}
</>
)
},
})
export default React.memo(SwitchNode)