Compare commits
6 Commits
fad190567b
...
528e66c497
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
528e66c497 | ||
|
|
707f538213 | ||
|
|
eae0d8dacd | ||
|
|
bf37c163fb | ||
|
|
ac2b6b1611 | ||
|
|
863638deaa |
@@ -9,6 +9,7 @@ import com.lanyuanxiaoyao.service.ai.web.entity.FlowTask;
|
|||||||
import com.lanyuanxiaoyao.service.ai.web.entity.FlowTaskTemplate;
|
import com.lanyuanxiaoyao.service.ai.web.entity.FlowTaskTemplate;
|
||||||
import com.lanyuanxiaoyao.service.ai.web.service.task.FlowTaskService;
|
import com.lanyuanxiaoyao.service.ai.web.service.task.FlowTaskService;
|
||||||
import com.lanyuanxiaoyao.service.ai.web.service.task.FlowTaskTemplateService;
|
import com.lanyuanxiaoyao.service.ai.web.service.task.FlowTaskTemplateService;
|
||||||
|
import java.lang.reflect.InvocationTargetException;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
@@ -47,6 +48,12 @@ public class TaskController extends SimpleControllerSupport<FlowTask, TaskContro
|
|||||||
return AmisResponse.responseSuccess(mapper.readValue(task.getTemplateInputSchema(), Map.class));
|
return AmisResponse.responseSuccess(mapper.readValue(task.getTemplateInputSchema(), Map.class));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("execute/{id}")
|
||||||
|
public AmisResponse<?> execute(@PathVariable("id") Long id) throws JsonProcessingException, InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException {
|
||||||
|
flowTaskService.execute(id);
|
||||||
|
return AmisResponse.responseSuccess();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected SaveItemMapper<FlowTask, SaveItem> saveItemMapper() {
|
protected SaveItemMapper<FlowTask, SaveItem> saveItemMapper() {
|
||||||
return item -> {
|
return item -> {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.lanyuanxiaoyao.service.ai.web.engine;
|
package com.lanyuanxiaoyao.service.ai.web.engine;
|
||||||
|
|
||||||
|
import com.lanyuanxiaoyao.service.ai.web.engine.entity.FlowContext;
|
||||||
import com.lanyuanxiaoyao.service.ai.web.engine.entity.FlowGraph;
|
import com.lanyuanxiaoyao.service.ai.web.engine.entity.FlowGraph;
|
||||||
import com.lanyuanxiaoyao.service.ai.web.engine.store.FlowStore;
|
import com.lanyuanxiaoyao.service.ai.web.engine.store.FlowStore;
|
||||||
import java.lang.reflect.InvocationTargetException;
|
import java.lang.reflect.InvocationTargetException;
|
||||||
@@ -21,7 +22,11 @@ public class FlowExecutor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void execute(FlowGraph graph) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException {
|
public void execute(FlowGraph graph) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException {
|
||||||
var runner = new FlowGraphRunner(graph, flowStore, runnerMap);
|
execute(graph, new FlowContext());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void execute(FlowGraph graph, FlowContext context) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException {
|
||||||
|
var runner = new FlowGraphRunner(graph, context, flowStore, runnerMap);
|
||||||
runner.run();
|
runner.run();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import org.eclipse.collections.api.multimap.set.ImmutableSetMultimap;
|
|||||||
*/
|
*/
|
||||||
public final class FlowGraphRunner {
|
public final class FlowGraphRunner {
|
||||||
private final FlowGraph flowGraph;
|
private final FlowGraph flowGraph;
|
||||||
|
private final FlowContext flowContext;
|
||||||
private final FlowStore flowStore;
|
private final FlowStore flowStore;
|
||||||
private final ImmutableMap<String, Class<? extends FlowNodeRunner>> nodeRunnerClass;
|
private final ImmutableMap<String, Class<? extends FlowNodeRunner>> nodeRunnerClass;
|
||||||
private final Queue<FlowNode> executionQueue = new LinkedList<>();
|
private final Queue<FlowNode> executionQueue = new LinkedList<>();
|
||||||
@@ -27,8 +28,9 @@ public final class FlowGraphRunner {
|
|||||||
private final ImmutableSetMultimap<String, FlowEdge> nodeOutputMap;
|
private final ImmutableSetMultimap<String, FlowEdge> nodeOutputMap;
|
||||||
private final ImmutableMap<String, FlowNode> nodeMap;
|
private final ImmutableMap<String, FlowNode> nodeMap;
|
||||||
|
|
||||||
public FlowGraphRunner(FlowGraph flowGraph, FlowStore flowStore, ImmutableMap<String, Class<? extends FlowNodeRunner>> nodeRunnerClass) {
|
public FlowGraphRunner(FlowGraph flowGraph, FlowContext flowContext, FlowStore flowStore, ImmutableMap<String, Class<? extends FlowNodeRunner>> nodeRunnerClass) {
|
||||||
this.flowGraph = flowGraph;
|
this.flowGraph = flowGraph;
|
||||||
|
this.flowContext = flowContext;
|
||||||
this.flowStore = flowStore;
|
this.flowStore = flowStore;
|
||||||
this.nodeRunnerClass = nodeRunnerClass;
|
this.nodeRunnerClass = nodeRunnerClass;
|
||||||
|
|
||||||
@@ -40,13 +42,12 @@ public final class FlowGraphRunner {
|
|||||||
public void run() throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
|
public void run() throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
|
||||||
flowStore.init(flowGraph);
|
flowStore.init(flowGraph);
|
||||||
|
|
||||||
var context = new FlowContext();
|
|
||||||
for (FlowNode node : flowGraph.nodes()) {
|
for (FlowNode node : flowGraph.nodes()) {
|
||||||
executionQueue.offer(node);
|
executionQueue.offer(node);
|
||||||
}
|
}
|
||||||
while (!executionQueue.isEmpty()) {
|
while (!executionQueue.isEmpty()) {
|
||||||
var node = executionQueue.poll();
|
var node = executionQueue.poll();
|
||||||
process(node, context);
|
process(node, flowContext);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package com.lanyuanxiaoyao.service.ai.web.engine;
|
||||||
|
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
import com.lanyuanxiaoyao.service.ai.web.engine.entity.FlowContext;
|
||||||
|
import java.util.Map;
|
||||||
|
import org.eclipse.collections.api.factory.Maps;
|
||||||
|
import org.eclipse.collections.api.map.ImmutableMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author lanyuanxiaoyao
|
||||||
|
* @version 20250711
|
||||||
|
*/
|
||||||
|
public class FlowHelper {
|
||||||
|
public static ImmutableMap<String, Object> generateInputVariablesMap(String nodeId, FlowContext context) {
|
||||||
|
var variableMap = Maps.mutable.<String, Object>empty();
|
||||||
|
var currentNodeData = context.get(nodeId);
|
||||||
|
if (currentNodeData.containsKey("inputs")) {
|
||||||
|
var inputsMap = (Map<String, Map<String, String>>) currentNodeData.get("inputs");
|
||||||
|
for (String variableName : inputsMap.keySet()) {
|
||||||
|
var expression = inputsMap.get(variableName).get("variable");
|
||||||
|
if (StrUtil.contains(expression, ".")) {
|
||||||
|
var splits = StrUtil.splitTrim(expression, ".", 2);
|
||||||
|
var targetNodeId = splits.get(0);
|
||||||
|
var targetVariableName = splits.get(1);
|
||||||
|
if (!context.getData().containsKey(targetNodeId)) {
|
||||||
|
throw new RuntimeException(StrUtil.format("Target node id not found: {}", targetNodeId));
|
||||||
|
}
|
||||||
|
var targetNodeData = context.getData().get(targetNodeId);
|
||||||
|
if (!targetNodeData.containsKey(targetVariableName)) {
|
||||||
|
throw new RuntimeException(StrUtil.format("Target node variable not found: {}.{}", targetNodeId, targetVariableName));
|
||||||
|
}
|
||||||
|
var targetVariable = targetNodeData.get(targetVariableName);
|
||||||
|
variableMap.put(variableName, targetVariable);
|
||||||
|
} else if (context.getInput().containsKey(expression)) {
|
||||||
|
if (!context.getInput().containsKey(variableName)) {
|
||||||
|
throw new RuntimeException(StrUtil.format("Target variable not found in input {}", variableName));
|
||||||
|
}
|
||||||
|
variableMap.put(variableName, context.getInput().get(variableName));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return variableMap.toImmutable();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ package com.lanyuanxiaoyao.service.ai.web.engine;
|
|||||||
|
|
||||||
import com.lanyuanxiaoyao.service.ai.web.engine.entity.FlowContext;
|
import com.lanyuanxiaoyao.service.ai.web.engine.entity.FlowContext;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
|
import org.eclipse.collections.api.map.MutableMap;
|
||||||
|
|
||||||
public abstract class FlowNodeRunner {
|
public abstract class FlowNodeRunner {
|
||||||
@Getter
|
@Getter
|
||||||
@@ -19,6 +20,10 @@ public abstract class FlowNodeRunner {
|
|||||||
this.context = context;
|
this.context = context;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected MutableMap<String, Object> getData() {
|
||||||
|
return context.get(nodeId);
|
||||||
|
}
|
||||||
|
|
||||||
protected <T> T getData(String key) {
|
protected <T> T getData(String key) {
|
||||||
var data = context.get(nodeId);
|
var data = context.get(nodeId);
|
||||||
return (T) data.get(key);
|
return (T) data.get(key);
|
||||||
|
|||||||
@@ -2,10 +2,12 @@ package com.lanyuanxiaoyao.service.ai.web.engine.entity;
|
|||||||
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import org.eclipse.collections.api.factory.Maps;
|
import org.eclipse.collections.api.factory.Maps;
|
||||||
|
import org.eclipse.collections.api.map.ImmutableMap;
|
||||||
import org.eclipse.collections.api.map.MutableMap;
|
import org.eclipse.collections.api.map.MutableMap;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
public class FlowContext {
|
public class FlowContext {
|
||||||
|
private ImmutableMap<String, Object> input = Maps.immutable.empty();
|
||||||
private MutableMap<String, MutableMap<String, Object>> data = Maps.mutable.<String, MutableMap<String, Object>>empty().asSynchronized();
|
private MutableMap<String, MutableMap<String, Object>> data = Maps.mutable.<String, MutableMap<String, Object>>empty().asSynchronized();
|
||||||
|
|
||||||
public MutableMap<String, Object> get(String key) {
|
public MutableMap<String, Object> get(String key) {
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package com.lanyuanxiaoyao.service.ai.web.engine.node;
|
||||||
|
|
||||||
|
import com.lanyuanxiaoyao.service.ai.web.engine.FlowHelper;
|
||||||
|
import com.lanyuanxiaoyao.service.ai.web.engine.FlowNodeRunner;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author lanyuanxiaoyao
|
||||||
|
* @version 20250711
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public class LlmNode extends FlowNodeRunner {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
var variableMap = FlowHelper.generateInputVariablesMap(getNodeId(), getContext());
|
||||||
|
log.info("Variable map: {}", variableMap);
|
||||||
|
setData("text", "llm");
|
||||||
|
|
||||||
|
/* var prompt = (String) getData("systemPrompt");
|
||||||
|
if (StrUtil.isNotBlank(prompt)) {
|
||||||
|
var builder = SpringBeanGetter.getBean("chat", ChatClient.Builder.class);
|
||||||
|
var client = builder.build();
|
||||||
|
var content = client.prompt()
|
||||||
|
.user(prompt)
|
||||||
|
.call()
|
||||||
|
.content();
|
||||||
|
setData("text", content);
|
||||||
|
}*/
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package com.lanyuanxiaoyao.service.ai.web.engine.node;
|
||||||
|
|
||||||
|
import com.lanyuanxiaoyao.service.ai.web.engine.FlowHelper;
|
||||||
|
import com.lanyuanxiaoyao.service.ai.web.engine.FlowNodeRunner;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author lanyuanxiaoyao
|
||||||
|
* @version 20250711
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public class OutputNode extends FlowNodeRunner {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
var variableMap = FlowHelper.generateInputVariablesMap(getNodeId(), getContext());
|
||||||
|
log.info("Variable map: {}", variableMap);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,15 +1,62 @@
|
|||||||
package com.lanyuanxiaoyao.service.ai.web.service.task;
|
package com.lanyuanxiaoyao.service.ai.web.service.task;
|
||||||
|
|
||||||
|
import cn.hutool.core.util.IdUtil;
|
||||||
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.lanyuanxiaoyao.service.ai.web.base.service.SimpleServiceSupport;
|
import com.lanyuanxiaoyao.service.ai.web.base.service.SimpleServiceSupport;
|
||||||
|
import com.lanyuanxiaoyao.service.ai.web.engine.FlowExecutor;
|
||||||
|
import com.lanyuanxiaoyao.service.ai.web.engine.entity.FlowContext;
|
||||||
|
import com.lanyuanxiaoyao.service.ai.web.engine.entity.FlowEdge;
|
||||||
|
import com.lanyuanxiaoyao.service.ai.web.engine.entity.FlowGraph;
|
||||||
|
import com.lanyuanxiaoyao.service.ai.web.engine.entity.FlowNode;
|
||||||
|
import com.lanyuanxiaoyao.service.ai.web.engine.node.LlmNode;
|
||||||
|
import com.lanyuanxiaoyao.service.ai.web.engine.node.OutputNode;
|
||||||
|
import com.lanyuanxiaoyao.service.ai.web.engine.store.InMemoryFlowStore;
|
||||||
import com.lanyuanxiaoyao.service.ai.web.entity.FlowTask;
|
import com.lanyuanxiaoyao.service.ai.web.entity.FlowTask;
|
||||||
import com.lanyuanxiaoyao.service.ai.web.repository.FlowTaskRepository;
|
import com.lanyuanxiaoyao.service.ai.web.repository.FlowTaskRepository;
|
||||||
|
import java.lang.reflect.InvocationTargetException;
|
||||||
|
import lombok.Data;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.eclipse.collections.api.factory.Maps;
|
||||||
|
import org.eclipse.collections.api.map.MutableMap;
|
||||||
|
import org.eclipse.collections.api.set.ImmutableSet;
|
||||||
|
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
public class FlowTaskService extends SimpleServiceSupport<FlowTask> {
|
public class FlowTaskService extends SimpleServiceSupport<FlowTask> {
|
||||||
public FlowTaskService(FlowTaskRepository flowTaskRepository) {
|
private final ObjectMapper mapper;
|
||||||
|
|
||||||
|
public FlowTaskService(FlowTaskRepository flowTaskRepository, Jackson2ObjectMapperBuilder builder) {
|
||||||
super(flowTaskRepository);
|
super(flowTaskRepository);
|
||||||
|
this.mapper = builder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void execute(Long id) throws JsonProcessingException, InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException {
|
||||||
|
var flowTask = detailOrThrow(id);
|
||||||
|
var graphVo = mapper.readValue(flowTask.getTemplateFlowGraph(), FlowGraphVo.class);
|
||||||
|
var flowGraph = new FlowGraph(IdUtil.fastUUID(), graphVo.getNodes(), graphVo.getEdges());
|
||||||
|
|
||||||
|
var store = new InMemoryFlowStore();
|
||||||
|
var executor = new FlowExecutor(
|
||||||
|
store,
|
||||||
|
Maps.immutable.of(
|
||||||
|
"output-node", OutputNode.class,
|
||||||
|
"llm-node", LlmNode.class
|
||||||
|
)
|
||||||
|
);
|
||||||
|
FlowContext context = new FlowContext();
|
||||||
|
context.setInput(mapper.readValue(flowTask.getInput(), new TypeReference<>() {}));
|
||||||
|
context.setData(graphVo.getData());
|
||||||
|
executor.execute(flowGraph, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static final class FlowGraphVo {
|
||||||
|
private ImmutableSet<FlowNode> nodes;
|
||||||
|
private ImmutableSet<FlowEdge> edges;
|
||||||
|
private MutableMap<String, MutableMap<String, Object>> data;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import {type Connection, type Edge, getOutgoers, type Node} from '@xyflow/react'
|
import {type Connection, type Edge, getOutgoers, type Node} from '@xyflow/react'
|
||||||
import {find, isEmpty, isEqual, lpad, toStr} from 'licia'
|
import {find, has, isEmpty, isEqual, lpad, toStr} from 'licia'
|
||||||
|
import NodeRegistry from './NodeRegistry.tsx'
|
||||||
|
|
||||||
export class CheckError extends Error {
|
export class CheckError extends Error {
|
||||||
readonly id: string
|
readonly id: string
|
||||||
@@ -63,16 +64,31 @@ export const checkAddConnection: (connection: Connection, nodes: Node[], edges:
|
|||||||
|
|
||||||
export const atLeastOneNode = () => new CheckError(300, '至少包含一个节点')
|
export const atLeastOneNode = () => new CheckError(300, '至少包含一个节点')
|
||||||
export const hasUnfinishedNode = (nodeId: string) => new CheckError(301, `存在尚未配置完成的节点: ${nodeId}`)
|
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}`)
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
export const checkSave: (nodes: Node[], edges: Edge[], data: any) => void = (nodes, edges, data) => {
|
export const checkSave: (inputSchema: Record<string, Record<string, any>>, nodes: Node[], edges: Edge[], data: any) => void = (inputSchema, nodes, edges, data) => {
|
||||||
if (isEmpty(nodes)) {
|
if (isEmpty(nodes)) {
|
||||||
throw atLeastOneNode()
|
throw atLeastOneNode()
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let node of nodes) {
|
for (let node of nodes) {
|
||||||
if (!data[node.id] || !data[node.id]?.finished) {
|
let nodeId = node.id
|
||||||
throw hasUnfinishedNode(node.id)
|
if (!has(data, nodeId) || !data[nodeId]?.finished) {
|
||||||
|
throw hasUnfinishedNode(nodeId)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!has(node, 'type')) {
|
||||||
|
throw nodeTypeNotFound()
|
||||||
|
}
|
||||||
|
let nodeType = node.type!
|
||||||
|
let nodeDefine = NodeRegistry[nodeType]
|
||||||
|
for (let checker of nodeDefine.checkers) {
|
||||||
|
let checkResult = checker(nodeId, inputSchema, nodes, edges, data)
|
||||||
|
if (checkResult.error) {
|
||||||
|
throw nodeError(nodeId, checkResult.message)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import {PlusCircleFilled, RollbackOutlined, SaveFilled} from '@ant-design/icons'
|
import {PlusCircleFilled, RollbackOutlined, SaveFilled} from '@ant-design/icons'
|
||||||
import {Background, BackgroundVariant, Controls, type Edge, MiniMap, type Node, ReactFlow} from '@xyflow/react'
|
import {Background, BackgroundVariant, Controls, MiniMap, ReactFlow} from '@xyflow/react'
|
||||||
import {Button, Dropdown, message, Popconfirm, Space} from 'antd'
|
import {Button, Dropdown, message, Popconfirm, Space} from 'antd'
|
||||||
import {arrToMap, randomId} from 'licia'
|
import {arrToMap, randomId} from 'licia'
|
||||||
import {useEffect} from 'react'
|
import {useEffect} from 'react'
|
||||||
@@ -8,14 +8,11 @@ import styled from 'styled-components'
|
|||||||
import '@xyflow/react/dist/style.css'
|
import '@xyflow/react/dist/style.css'
|
||||||
import {commonInfo} from '../../util/amis.tsx'
|
import {commonInfo} from '../../util/amis.tsx'
|
||||||
import {checkAddConnection, checkAddNode, checkSave} from './FlowChecker.tsx'
|
import {checkAddConnection, checkAddNode, checkSave} from './FlowChecker.tsx'
|
||||||
import CodeNode from './node/CodeNode.tsx'
|
import NodeRegistry from './NodeRegistry.tsx'
|
||||||
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 {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 type {FlowEditorProps} from './types.ts'
|
||||||
|
|
||||||
const FlowableDiv = styled.div`
|
const FlowableDiv = styled.div`
|
||||||
.react-flow__node.selectable {
|
.react-flow__node.selectable {
|
||||||
@@ -52,54 +49,6 @@ const FlowableDiv = styled.div`
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
type NodeDefine = {
|
|
||||||
key: string,
|
|
||||||
name: string,
|
|
||||||
description: string,
|
|
||||||
component: any,
|
|
||||||
}
|
|
||||||
|
|
||||||
const nodeDefine: Record<string, NodeDefine> = {
|
|
||||||
'output-node': {
|
|
||||||
key: 'output-node',
|
|
||||||
name: '输出',
|
|
||||||
description: '定义输出变量',
|
|
||||||
component: OutputNode,
|
|
||||||
},
|
|
||||||
'llm-node': {
|
|
||||||
key: 'llm-node',
|
|
||||||
name: '大模型',
|
|
||||||
description: '使用大模型对话',
|
|
||||||
component: LlmNode,
|
|
||||||
},
|
|
||||||
'knowledge-node': {
|
|
||||||
key: 'knowledge-node',
|
|
||||||
name: '知识库',
|
|
||||||
description: '',
|
|
||||||
component: KnowledgeNode,
|
|
||||||
},
|
|
||||||
'code-node': {
|
|
||||||
key: 'code-node',
|
|
||||||
name: '代码执行',
|
|
||||||
description: '执行自定义的处理代码',
|
|
||||||
component: CodeNode,
|
|
||||||
},
|
|
||||||
'switch-node': {
|
|
||||||
key: 'switch-node',
|
|
||||||
name: '分支节点',
|
|
||||||
description: '根据不同的情况前往不同的分支',
|
|
||||||
component: SwitchNode,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
export type GraphData = { nodes: Node[], edges: Edge[], data: any }
|
|
||||||
|
|
||||||
export type FlowEditorProps = {
|
|
||||||
inputSchema: Record<string, Record<string, any>>,
|
|
||||||
graphData: GraphData,
|
|
||||||
onGraphDataChange: (graphData: GraphData) => void,
|
|
||||||
}
|
|
||||||
|
|
||||||
function FlowEditor(props: FlowEditorProps) {
|
function FlowEditor(props: FlowEditorProps) {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const [messageApi, contextHolder] = message.useMessage()
|
const [messageApi, contextHolder] = message.useMessage()
|
||||||
@@ -116,7 +65,7 @@ function FlowEditor(props: FlowEditorProps) {
|
|||||||
onConnect,
|
onConnect,
|
||||||
} = useFlowStore()
|
} = useFlowStore()
|
||||||
|
|
||||||
const {setInputSchema} = useContextStore()
|
const {inputSchema, setInputSchema} = useContextStore()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// language=JSON
|
// language=JSON
|
||||||
@@ -139,7 +88,7 @@ function FlowEditor(props: FlowEditorProps) {
|
|||||||
<Space className="toolbar">
|
<Space className="toolbar">
|
||||||
<Dropdown
|
<Dropdown
|
||||||
menu={{
|
menu={{
|
||||||
items: Object.keys(nodeDefine).map(key => ({key: key, label: nodeDefine[key]!.name})),
|
items: Object.keys(NodeRegistry).map(key => ({key: key, label: NodeRegistry[key]!.name})),
|
||||||
onClick: ({key}) => {
|
onClick: ({key}) => {
|
||||||
try {
|
try {
|
||||||
if (commonInfo.debug) {
|
if (commonInfo.debug) {
|
||||||
@@ -148,7 +97,7 @@ function FlowEditor(props: FlowEditorProps) {
|
|||||||
checkAddNode(key, nodes, edges)
|
checkAddNode(key, nodes, edges)
|
||||||
|
|
||||||
let nodeId = randomId(10, 'qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM')
|
let nodeId = randomId(10, 'qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM')
|
||||||
let define = nodeDefine[key]
|
let define = NodeRegistry[key]
|
||||||
|
|
||||||
setDataById(
|
setDataById(
|
||||||
nodeId,
|
nodeId,
|
||||||
@@ -193,7 +142,7 @@ function FlowEditor(props: FlowEditorProps) {
|
|||||||
if (commonInfo.debug) {
|
if (commonInfo.debug) {
|
||||||
console.info('Save', JSON.stringify({nodes, edges, data}))
|
console.info('Save', JSON.stringify({nodes, edges, data}))
|
||||||
}
|
}
|
||||||
checkSave(nodes, edges, data)
|
checkSave(inputSchema, nodes, edges, data)
|
||||||
props.onGraphDataChange({nodes, edges, data})
|
props.onGraphDataChange({nodes, edges, data})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@@ -222,7 +171,7 @@ function FlowEditor(props: FlowEditorProps) {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
nodeTypes={arrToMap(Object.keys(nodeDefine), key => nodeDefine[key]!.component)}
|
nodeTypes={arrToMap(Object.keys(NodeRegistry), key => NodeRegistry[key]!.component)}
|
||||||
>
|
>
|
||||||
<Controls/>
|
<Controls/>
|
||||||
<MiniMap/>
|
<MiniMap/>
|
||||||
|
|||||||
38
service-web/client/src/components/flow/Helper.tsx
Normal file
38
service-web/client/src/components/flow/Helper.tsx
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import {type Edge, getIncomers, type Node} from '@xyflow/react'
|
||||||
|
import {find, has, isEqual, unique} from 'licia'
|
||||||
|
import Queue from 'yocto-queue'
|
||||||
|
|
||||||
|
export const getAllIncomerNodeById: (id: string, nodes: Node[], edges: Edge[]) => string[] = (id, nodes, edges) => {
|
||||||
|
let queue = new Queue<Node>()
|
||||||
|
queue.enqueue(find(nodes, node => isEqual(node.id, id))!)
|
||||||
|
let result: string[] = []
|
||||||
|
while (queue.size !== 0) {
|
||||||
|
let currentNode = queue.dequeue()!
|
||||||
|
for (const incomer of getIncomers(currentNode, nodes, edges)) {
|
||||||
|
result.push(incomer.id)
|
||||||
|
queue.enqueue(incomer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return unique(result, (a, b) => isEqual(a, b))
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getAllIncomerNodeOutputVariables: (id: string, nodes: Node[], edges: Edge[], data: any) => {
|
||||||
|
id: string,
|
||||||
|
variable: string
|
||||||
|
}[] = (id, nodes, edges, data) => {
|
||||||
|
let incomerIds = getAllIncomerNodeById(id, nodes, edges)
|
||||||
|
let incomerVariables: { id: string, variable: string }[] = []
|
||||||
|
for (const incomerId of incomerIds) {
|
||||||
|
let nodeData = data[incomerId] ?? {}
|
||||||
|
if (has(nodeData, 'outputs')) {
|
||||||
|
let outputs = nodeData?.outputs ?? []
|
||||||
|
for (const output of Object.keys(outputs)) {
|
||||||
|
incomerVariables.push({
|
||||||
|
id: incomerId,
|
||||||
|
variable: output,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return incomerVariables
|
||||||
|
}
|
||||||
73
service-web/client/src/components/flow/NodeRegistry.tsx
Normal file
73
service-web/client/src/components/flow/NodeRegistry.tsx
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import {has, isEmpty} from 'licia'
|
||||||
|
import {getAllIncomerNodeOutputVariables} from './Helper.tsx'
|
||||||
|
import CodeNode from './node/CodeNode.tsx'
|
||||||
|
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 type {NodeChecker} from './types.ts'
|
||||||
|
|
||||||
|
const inputVariableChecker: NodeChecker = (id, inputSchema, nodes, edges, data) => {
|
||||||
|
let nodeData = data[id] ?? {}
|
||||||
|
if (has(nodeData, 'inputs')) {
|
||||||
|
let inputs = nodeData?.inputs ?? {}
|
||||||
|
if (!isEmpty(inputs)) {
|
||||||
|
let outputVariables = new Set([
|
||||||
|
...getAllIncomerNodeOutputVariables(id, nodes, edges, data).map(i => `${i.id}.${i.variable}`),
|
||||||
|
...Object.keys(inputSchema)
|
||||||
|
])
|
||||||
|
for (const key of Object.keys(inputs)) {
|
||||||
|
let variable = inputs[key]?.variable ?? ''
|
||||||
|
if (!outputVariables.has(variable)) {
|
||||||
|
return {
|
||||||
|
error: true,
|
||||||
|
message: `节点 ${id} 存在错误:变量 ${variable} 不存在`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {error: false}
|
||||||
|
}
|
||||||
|
|
||||||
|
type NodeDefine = {
|
||||||
|
name: string,
|
||||||
|
description: string,
|
||||||
|
component: any,
|
||||||
|
checkers: NodeChecker[],
|
||||||
|
}
|
||||||
|
|
||||||
|
const NodeRegistry: Record<string, NodeDefine> = {
|
||||||
|
'output-node': {
|
||||||
|
name: '输出',
|
||||||
|
description: '定义输出变量',
|
||||||
|
component: OutputNode,
|
||||||
|
checkers: [inputVariableChecker],
|
||||||
|
},
|
||||||
|
'llm-node': {
|
||||||
|
name: '大模型',
|
||||||
|
description: '使用大模型对话',
|
||||||
|
component: LlmNode,
|
||||||
|
checkers: [inputVariableChecker],
|
||||||
|
},
|
||||||
|
'knowledge-node': {
|
||||||
|
name: '知识库',
|
||||||
|
description: '',
|
||||||
|
component: KnowledgeNode,
|
||||||
|
checkers: [inputVariableChecker],
|
||||||
|
},
|
||||||
|
'code-node': {
|
||||||
|
name: '代码执行',
|
||||||
|
description: '执行自定义的处理代码',
|
||||||
|
component: CodeNode,
|
||||||
|
checkers: [inputVariableChecker],
|
||||||
|
},
|
||||||
|
'switch-node': {
|
||||||
|
name: '分支节点',
|
||||||
|
description: '根据不同的情况前往不同的分支',
|
||||||
|
component: SwitchNode,
|
||||||
|
checkers: [],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default NodeRegistry
|
||||||
@@ -1,38 +1,15 @@
|
|||||||
import {CopyFilled, DeleteFilled, EditFilled} from '@ant-design/icons'
|
import {CopyFilled, DeleteFilled, EditFilled} from '@ant-design/icons'
|
||||||
import {type Edge, getIncomers, 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 {find, has, isEmpty, isEqual, unique} from 'licia'
|
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 Queue from 'yocto-queue'
|
|
||||||
import {amisRender, commonInfo, horizontalFormOptions} from '../../../util/amis.tsx'
|
import {amisRender, commonInfo, horizontalFormOptions} from '../../../util/amis.tsx'
|
||||||
|
import {getAllIncomerNodeById} 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 type InputFormOptions = {
|
|
||||||
label: string
|
|
||||||
value: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export type InputFormOptionsGroup = {
|
|
||||||
group: string,
|
|
||||||
variables: InputFormOptions[],
|
|
||||||
}
|
|
||||||
|
|
||||||
const getAllIncomerNodeById: (id: string, nodes: Node[], edges: Edge[]) => string[] = (id, nodes, edges) => {
|
|
||||||
let queue = new Queue<Node>()
|
|
||||||
queue.enqueue(find(nodes, node => isEqual(node.id, id))!)
|
|
||||||
let result: string[] = []
|
|
||||||
while (queue.size !== 0) {
|
|
||||||
let currentNode = queue.dequeue()!
|
|
||||||
for (const incomer of getIncomers(currentNode, nodes, edges)) {
|
|
||||||
result.push(incomer.id)
|
|
||||||
queue.enqueue(incomer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return unique(result, (a, b) => isEqual(a, b))
|
|
||||||
}
|
|
||||||
|
|
||||||
export function inputsFormColumns(
|
export function inputsFormColumns(
|
||||||
nodeId: string,
|
nodeId: string,
|
||||||
@@ -91,7 +68,7 @@ export function inputsFormColumns(
|
|||||||
{
|
{
|
||||||
...horizontalFormOptions(),
|
...horizontalFormOptions(),
|
||||||
type: 'select',
|
type: 'select',
|
||||||
name: 'type',
|
name: 'variable',
|
||||||
label: '变量',
|
label: '变量',
|
||||||
required: true,
|
required: true,
|
||||||
selectMode: 'group',
|
selectMode: 'group',
|
||||||
|
|||||||
@@ -1,15 +1,24 @@
|
|||||||
import type {NodeProps} from '@xyflow/react'
|
import type {NodeProps} from '@xyflow/react'
|
||||||
|
import {Tag} from 'antd'
|
||||||
import React, {useCallback, useEffect} from 'react'
|
import React, {useCallback, useEffect} from 'react'
|
||||||
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, {inputsFormColumns, NormalNodeHandler, outputsFormColumns} from './AmisNode.tsx'
|
import AmisNode, {inputsFormColumns, NormalNodeHandler, outputsFormColumns} from './AmisNode.tsx'
|
||||||
|
|
||||||
|
const languageMap: Record<string, string> = {
|
||||||
|
'javascript': 'Javascript',
|
||||||
|
'python': 'Python',
|
||||||
|
'Lua': 'lua',
|
||||||
|
}
|
||||||
|
|
||||||
const CodeNode = (props: NodeProps) => {
|
const CodeNode = (props: NodeProps) => {
|
||||||
const {getNodes, getEdges} = useFlowStore()
|
const {getNodes, getEdges} = useFlowStore()
|
||||||
const {getData, mergeDataById} = useDataStore()
|
const {getData, mergeDataById, getDataById} = useDataStore()
|
||||||
const {getInputSchema} = useContextStore()
|
const {getInputSchema} = useContextStore()
|
||||||
|
|
||||||
|
const nodeData = getDataById(props.id)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
mergeDataById(
|
mergeDataById(
|
||||||
props.id,
|
props.id,
|
||||||
@@ -33,20 +42,8 @@ const CodeNode = (props: NodeProps) => {
|
|||||||
name: 'type',
|
name: 'type',
|
||||||
label: '代码类型',
|
label: '代码类型',
|
||||||
required: true,
|
required: true,
|
||||||
options: [
|
selectFirst: true,
|
||||||
{
|
options: Object.keys(languageMap).map(key => ({label: languageMap[key], value: key})),
|
||||||
value: 'javascript',
|
|
||||||
label: 'JavaScript',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'python',
|
|
||||||
label: 'Python',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'lua',
|
|
||||||
label: 'Lua',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'editor',
|
type: 'editor',
|
||||||
@@ -66,6 +63,14 @@ const CodeNode = (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">{languageMap[nodeData.type]}</Tag>
|
||||||
|
</div>
|
||||||
|
: <></>
|
||||||
|
}
|
||||||
columnSchema={columnsSchema}
|
columnSchema={columnsSchema}
|
||||||
handler={<NormalNodeHandler/>}
|
handler={<NormalNodeHandler/>}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ const LlmNode = (props: NodeProps) => {
|
|||||||
extraNodeDescription={
|
extraNodeDescription={
|
||||||
nodeData?.model
|
nodeData?.model
|
||||||
? <div className="mt-2 flex justify-between">
|
? <div className="mt-2 flex justify-between">
|
||||||
<span>大模型</span>
|
<span>模型名称</span>
|
||||||
<Tag className="m-0" color="blue">{modelMap[nodeData.model]}</Tag>
|
<Tag className="m-0" color="blue">{modelMap[nodeData.model]}</Tag>
|
||||||
</div>
|
</div>
|
||||||
: <></>
|
: <></>
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
import {create} from 'zustand/react'
|
import {create} from 'zustand/react'
|
||||||
|
|
||||||
export type ContextStoreState = {
|
export const useContextStore = create<{
|
||||||
inputSchema: Record<string, Record<string, any>>,
|
inputSchema: Record<string, Record<string, any>>,
|
||||||
getInputSchema: () => Record<string, Record<string, any>>,
|
getInputSchema: () => Record<string, Record<string, any>>,
|
||||||
setInputSchema: (inputSchema: Record<string, Record<string, any>>) => void,
|
setInputSchema: (inputSchema: Record<string, Record<string, any>>) => void,
|
||||||
}
|
}>((set, get) => ({
|
||||||
|
|
||||||
export const useContextStore = create<ContextStoreState>((set, get) => ({
|
|
||||||
inputSchema: {},
|
inputSchema: {},
|
||||||
getInputSchema: () => get().inputSchema,
|
getInputSchema: () => get().inputSchema,
|
||||||
setInputSchema: (inputSchema: Record<string, Record<string, any>>) => set({inputSchema}),
|
setInputSchema: (inputSchema: Record<string, Record<string, any>>) => set({inputSchema}),
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import {create} from 'zustand/react'
|
import {create} from 'zustand/react'
|
||||||
|
|
||||||
export type DataStoreState = {
|
export const useDataStore = create<{
|
||||||
data: Record<string, any>,
|
data: Record<string, any>,
|
||||||
getData: () => Record<string, any>,
|
getData: () => Record<string, any>,
|
||||||
setData: (data: Record<string, any>) => void,
|
setData: (data: Record<string, any>) => void,
|
||||||
@@ -8,9 +8,7 @@ export type DataStoreState = {
|
|||||||
setDataById: (id: string, data: any) => void,
|
setDataById: (id: string, data: any) => void,
|
||||||
mergeDataById: (id: string, data: any) => void,
|
mergeDataById: (id: string, data: any) => void,
|
||||||
removeDataById: (id: string) => void,
|
removeDataById: (id: string) => void,
|
||||||
}
|
}>((set, get) => ({
|
||||||
|
|
||||||
export const useDataStore = create<DataStoreState>((set, get) => ({
|
|
||||||
data: {},
|
data: {},
|
||||||
getData: () => get().data,
|
getData: () => get().data,
|
||||||
setData: (data) => set({
|
setData: (data) => set({
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
import {filter, find, isEqual} from 'licia'
|
import {filter, find, isEqual} from 'licia'
|
||||||
import {create} from 'zustand/react'
|
import {create} from 'zustand/react'
|
||||||
|
|
||||||
export type FlowStoreState = {
|
export const useFlowStore = create<{
|
||||||
nodes: Node[],
|
nodes: Node[],
|
||||||
getNodes: () => Node[],
|
getNodes: () => Node[],
|
||||||
onNodesChange: OnNodesChange,
|
onNodesChange: OnNodesChange,
|
||||||
@@ -26,9 +26,7 @@ export type FlowStoreState = {
|
|||||||
setEdges: (edges: Edge[]) => void,
|
setEdges: (edges: Edge[]) => void,
|
||||||
|
|
||||||
onConnect: OnConnect,
|
onConnect: OnConnect,
|
||||||
}
|
}>((set, get) => ({
|
||||||
|
|
||||||
export const useFlowStore = create<FlowStoreState>((set, get) => ({
|
|
||||||
nodes: [],
|
nodes: [],
|
||||||
getNodes: () => get().nodes,
|
getNodes: () => get().nodes,
|
||||||
onNodesChange: changes => {
|
onNodesChange: changes => {
|
||||||
|
|||||||
26
service-web/client/src/components/flow/types.ts
Normal file
26
service-web/client/src/components/flow/types.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import type {Edge, Node} from '@xyflow/react'
|
||||||
|
|
||||||
|
export type InputFormOptions = {
|
||||||
|
label: string
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type InputFormOptionsGroup = {
|
||||||
|
group: string,
|
||||||
|
variables: InputFormOptions[],
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NodeError = {
|
||||||
|
error: boolean,
|
||||||
|
message?: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NodeChecker = (id: string, inputSchema: Record<string, Record<string, any>>, nodes: Node[], edges: Edge[], data: any) => NodeError
|
||||||
|
|
||||||
|
export type GraphData = { nodes: Node[], edges: Edge[], data: any }
|
||||||
|
|
||||||
|
export type FlowEditorProps = {
|
||||||
|
inputSchema: Record<string, Record<string, any>>,
|
||||||
|
graphData: GraphData,
|
||||||
|
onGraphDataChange: (graphData: GraphData) => void,
|
||||||
|
}
|
||||||
@@ -1,12 +1,10 @@
|
|||||||
import {useState} from 'react'
|
import {useState} from 'react'
|
||||||
import FlowEditor, {type GraphData} from '../components/flow/FlowEditor.tsx'
|
import FlowEditor from '../components/flow/FlowEditor.tsx'
|
||||||
|
import type {GraphData} from '../components/flow/types.ts'
|
||||||
|
|
||||||
function Test() {
|
function Test() {
|
||||||
const [graphData] = useState<GraphData>({
|
// language=JSON
|
||||||
nodes: [],
|
const [graphData] = useState<GraphData>(JSON.parse('{\n "nodes": [\n {\n "id": "MzEitlOusl",\n "type": "llm-node",\n "position": {\n "x": 47,\n "y": 92\n },\n "data": {},\n "measured": {\n "width": 256,\n "height": 130\n },\n "selected": false,\n "dragging": false\n },\n {\n "id": "bivXSpiLaI",\n "type": "code-node",\n "position": {\n "x": 381,\n "y": 181\n },\n "data": {},\n "measured": {\n "width": 256,\n "height": 130\n },\n "selected": true,\n "dragging": false\n }\n ],\n "edges": [\n {\n "source": "MzEitlOusl",\n "sourceHandle": "source",\n "target": "bivXSpiLaI",\n "targetHandle": "target",\n "id": "xy-edge__MzEitlOuslsource-bivXSpiLaItarget"\n }\n ],\n "data": {\n "MzEitlOusl": {\n "node": {\n "name": "大模型",\n "description": "使用大模型对话"\n },\n "outputs": {\n "text": {\n "type": "string"\n }\n },\n "model": "qwen3",\n "systemPrompt": "你是个好人",\n "finished": true\n },\n "bivXSpiLaI": {\n "node": {\n "name": "代码执行",\n "description": "执行自定义的处理代码"\n },\n "outputs": {\n "result": {\n "type": "string"\n }\n },\n "type": "javascript",\n "content": "console.log(\'hello\')",\n "inputs": {\n "text": {\n "variable": "MzEitlOusl.text"\n }\n },\n "finished": true\n }\n }\n}'))
|
||||||
edges: [],
|
|
||||||
data: {},
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-screen">
|
<div className="h-screen">
|
||||||
|
|||||||
@@ -118,9 +118,9 @@ const FlowTask: React.FC = () => {
|
|||||||
...payload,
|
...payload,
|
||||||
data: {
|
data: {
|
||||||
inputData: payload.data,
|
inputData: payload.data,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
|
||||||
},
|
},
|
||||||
static: true,
|
static: true,
|
||||||
},
|
},
|
||||||
@@ -131,6 +131,14 @@ const FlowTask: React.FC = () => {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: 'action',
|
||||||
|
label: '执行',
|
||||||
|
level: 'link',
|
||||||
|
size: 'sm',
|
||||||
|
actionType: 'ajax',
|
||||||
|
api: `get:${commonInfo.baseAiUrl}/flow_task/execute/\${id}`,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
type: 'action',
|
type: 'action',
|
||||||
label: '删除',
|
label: '删除',
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {commonInfo, formInputFileStaticColumns} from '../../../util/amis.tsx'
|
|||||||
|
|
||||||
export const typeMap: Record<string, string> = {
|
export const typeMap: Record<string, string> = {
|
||||||
text: '文本',
|
text: '文本',
|
||||||
|
textarea: '文本段',
|
||||||
number: '数字',
|
number: '数字',
|
||||||
files: '文件',
|
files: '文件',
|
||||||
}
|
}
|
||||||
@@ -30,6 +31,12 @@ export const generateInputForm: (inputSchema: Record<string, InputField>, title?
|
|||||||
clearValueOnEmpty: true,
|
clearValueOnEmpty: true,
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
|
case 'textarea':
|
||||||
|
items.push({
|
||||||
|
...commonMeta,
|
||||||
|
type: 'textarea',
|
||||||
|
})
|
||||||
|
break
|
||||||
case 'number':
|
case 'number':
|
||||||
commonMeta.type = 'input-number'
|
commonMeta.type = 'input-number'
|
||||||
break
|
break
|
||||||
@@ -42,7 +49,7 @@ export const generateInputForm: (inputSchema: Record<string, InputField>, title?
|
|||||||
type: 'crud',
|
type: 'crud',
|
||||||
api: `${commonInfo.baseAiUrl}/upload/detail?ids=\${JOIN(inputData.${name}, ',')}`,
|
api: `${commonInfo.baseAiUrl}/upload/detail?ids=\${JOIN(inputData.${name}, ',')}`,
|
||||||
columns: formInputFileStaticColumns,
|
columns: formInputFileStaticColumns,
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
items.push({
|
items.push({
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import {isEmpty, isEqual} from 'licia'
|
import {isEmpty, isEqual} from 'licia'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {useParams} from 'react-router'
|
import {useNavigate, useParams} from 'react-router'
|
||||||
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 {generateInputForm, typeMap} from '../InputSchema.tsx'
|
import {generateInputForm, typeMap} from '../InputSchema.tsx'
|
||||||
@@ -12,6 +12,7 @@ const TemplateEditDiv = styled.div`
|
|||||||
`
|
`
|
||||||
|
|
||||||
const FlowTaskTemplateEdit: React.FC = () => {
|
const FlowTaskTemplateEdit: React.FC = () => {
|
||||||
|
const navigate = useNavigate()
|
||||||
const {template_id} = useParams()
|
const {template_id} = useParams()
|
||||||
const preloadTemplateId = isEqual(template_id, '-1') ? undefined : template_id
|
const preloadTemplateId = isEqual(template_id, '-1') ? undefined : template_id
|
||||||
return (
|
return (
|
||||||
@@ -63,6 +64,17 @@ const FlowTaskTemplateEdit: React.FC = () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
submitSucc: {
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
actionType: 'custom',
|
||||||
|
// @ts-ignore
|
||||||
|
script: (context, doAction, event) => {
|
||||||
|
navigate(-1)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
body: [
|
body: [
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user