7 Commits

Author SHA1 Message Date
v-zhangjc9
a2a8c39145 feat(ai-web): 输出节点改为多变量输出 2025-07-21 18:33:45 +08:00
v-zhangjc9
82ad3e7854 fix(web): 修复api引用错误 2025-07-21 10:08:37 +08:00
1e3e14c590 refactor(ai-web): 优化inputSchema可以为空,方便各处判断流程是否有入参 2025-07-21 00:05:28 +08:00
267eecbf45 refactor(ai-web): 使用输入节点代替inputSchema
将输入表单放在流程图中一起定义,方便统一参数的处理,不需要单独为输入表单的变量进行合并和操作
2025-07-20 19:27:09 +08:00
a5282762ed refractor(web): 增加输入节点 2025-07-20 17:17:22 +08:00
v-zhangjc9
77a09472aa feat(ai-web): 完成代码节点的执行 2025-07-18 15:48:35 +08:00
v-zhangjc9
4cfa110f2f feat(web): 增加数组的判断 2025-07-18 11:07:48 +08:00
43 changed files with 751 additions and 565 deletions

View File

@@ -45,7 +45,7 @@ create table hudi_collect_build_b12.service_ai_flow_task
status enum ('ERROR','FINISHED','RUNNING') not null comment '任务运行状态', status enum ('ERROR','FINISHED','RUNNING') not null comment '任务运行状态',
template_description varchar(255) comment '任务对应的模板功能、内容说明', template_description varchar(255) comment '任务对应的模板功能、内容说明',
template_flow_graph longtext not null comment '任务对应的模板前端流程图数据', template_flow_graph longtext not null comment '任务对应的模板前端流程图数据',
template_input_schema longtext not null comment '任务对应的模板入参Schema', template_input_schema longtext comment '任务对应的模板入参Schema',
template_name varchar(255) not null comment '任务对应的模板名称', template_name varchar(255) not null comment '任务对应的模板名称',
primary key (id) primary key (id)
) comment ='流程任务记录' charset = utf8mb4; ) comment ='流程任务记录' charset = utf8mb4;
@@ -57,7 +57,7 @@ create table hudi_collect_build_b12.service_ai_flow_task_template
modified_time datetime(6) comment '记录更新时间', modified_time datetime(6) comment '记录更新时间',
description varchar(255) comment '模板功能、内容说明', description varchar(255) comment '模板功能、内容说明',
flow_graph longtext not null comment '前端流程图数据', flow_graph longtext not null comment '前端流程图数据',
input_schema longtext not null comment '模板入参Schema', input_schema longtext comment '模板入参Schema',
name varchar(255) not null comment '模板名称', name varchar(255) not null comment '模板名称',
primary key (id) primary key (id)
) comment ='流程任务模板' charset = utf8mb4; ) comment ='流程任务模板' charset = utf8mb4;

View File

@@ -89,7 +89,10 @@
<dependency> <dependency>
<groupId>com.yomahub</groupId> <groupId>com.yomahub</groupId>
<artifactId>liteflow-script-graaljs</artifactId> <artifactId>liteflow-script-graaljs</artifactId>
<version>${liteflow.version}</version> </dependency>
<dependency>
<groupId>com.yomahub</groupId>
<artifactId>liteflow-script-python</artifactId>
</dependency> </dependency>
<dependency> <dependency>

View File

@@ -1,5 +1,7 @@
package com.lanyuanxiaoyao.service.ai.web.controller.task; package com.lanyuanxiaoyao.service.ai.web.controller.task;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.lanyuanxiaoyao.service.ai.core.entity.amis.AmisResponse; import com.lanyuanxiaoyao.service.ai.core.entity.amis.AmisResponse;
@@ -14,6 +16,7 @@ import java.util.Map;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers; import org.mapstruct.factory.Mappers;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
@@ -39,12 +42,18 @@ public class TaskController extends SimpleControllerSupport<FlowTask, TaskContro
@GetMapping("input_data/{id}") @GetMapping("input_data/{id}")
public AmisResponse<?> getInputData(@PathVariable("id") Long id) throws JsonProcessingException { public AmisResponse<?> getInputData(@PathVariable("id") Long id) throws JsonProcessingException {
var task = flowTaskService.detailOrThrow(id); var task = flowTaskService.detailOrThrow(id);
if (ObjectUtil.isEmpty(task.getInput())) {
return AmisResponse.responseSuccess();
}
return AmisResponse.responseSuccess(mapper.readValue(task.getInput(), Map.class)); return AmisResponse.responseSuccess(mapper.readValue(task.getInput(), Map.class));
} }
@GetMapping("input_schema/{id}") @GetMapping("input_schema/{id}")
public AmisResponse<?> getInputSchema(@PathVariable("id") Long id) throws JsonProcessingException { public AmisResponse<?> getInputSchema(@PathVariable("id") Long id) throws JsonProcessingException {
var task = flowTaskService.detailOrThrow(id); var task = flowTaskService.detailOrThrow(id);
if (ObjectUtil.isEmpty(task.getTemplateInputSchema())) {
return AmisResponse.responseSuccess();
}
return AmisResponse.responseSuccess(mapper.readValue(task.getTemplateInputSchema(), Map.class)); return AmisResponse.responseSuccess(mapper.readValue(task.getTemplateInputSchema(), Map.class));
} }
@@ -63,7 +72,7 @@ public class TaskController extends SimpleControllerSupport<FlowTask, TaskContro
task.setTemplateDescription(template.getDescription()); task.setTemplateDescription(template.getDescription());
task.setTemplateInputSchema(template.getInputSchema()); task.setTemplateInputSchema(template.getInputSchema());
task.setTemplateFlowGraph(template.getFlowGraph()); task.setTemplateFlowGraph(template.getFlowGraph());
task.setInput(mapper.writeValueAsString(item.getInput())); task.setInput(ObjectUtil.isEmpty(item.getInput()) ? null : mapper.writeValueAsString(item.getInput()));
return task; return task;
}; };
} }
@@ -91,9 +100,13 @@ public class TaskController extends SimpleControllerSupport<FlowTask, TaskContro
public static class ListItem extends SimpleItem { public static class ListItem extends SimpleItem {
private String templateName; private String templateName;
private FlowTask.Status status; private FlowTask.Status status;
private Boolean hasInput;
@org.mapstruct.Mapper @org.mapstruct.Mapper(imports = {
StrUtil.class
})
public static abstract class Mapper { public static abstract class Mapper {
@Mapping(target = "hasInput", expression = "java(StrUtil.isNotBlank(task.getInput()))")
public abstract ListItem from(FlowTask task); public abstract ListItem from(FlowTask task);
} }
} }
@@ -104,8 +117,11 @@ public class TaskController extends SimpleControllerSupport<FlowTask, TaskContro
private String error; private String error;
private String result; private String result;
@org.mapstruct.Mapper @org.mapstruct.Mapper(imports = {
StrUtil.class
})
public static abstract class Mapper extends ListItem.Mapper { public static abstract class Mapper extends ListItem.Mapper {
@Mapping(target = "hasInput", expression = "java(StrUtil.isNotBlank(task.getInput()))")
public abstract DetailItem from(FlowTask task); public abstract DetailItem from(FlowTask task);
} }
} }

View File

@@ -1,5 +1,6 @@
package com.lanyuanxiaoyao.service.ai.web.controller.task; package com.lanyuanxiaoyao.service.ai.web.controller.task;
import cn.hutool.core.util.ObjectUtil;
import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
@@ -38,9 +39,18 @@ public class TaskTemplateController extends SimpleControllerSupport<FlowTaskTemp
@GetMapping("input_schema/{id}") @GetMapping("input_schema/{id}")
public AmisResponse<?> getInputSchema(@PathVariable("id") Long id) throws JsonProcessingException { public AmisResponse<?> getInputSchema(@PathVariable("id") Long id) throws JsonProcessingException {
var template = flowTaskTemplateService.detailOrThrow(id); var template = flowTaskTemplateService.detailOrThrow(id);
if (ObjectUtil.isEmpty(template.getInputSchema())) {
return AmisResponse.responseSuccess();
}
return AmisResponse.responseSuccess(mapper.readValue(template.getInputSchema(), Map.class)); return AmisResponse.responseSuccess(mapper.readValue(template.getInputSchema(), Map.class));
} }
@GetMapping("flow_graph/{id}")
public AmisResponse<?> getFlowGraph(@PathVariable("id") Long id) throws JsonProcessingException {
var template = flowTaskTemplateService.detailOrThrow(id);
return AmisResponse.responseSuccess(mapper.readValue(template.getFlowGraph(), Map.class));
}
@PostMapping("update_flow_graph") @PostMapping("update_flow_graph")
public AmisResponse<?> updateFlowGraph(@RequestBody UpdateGraphItem item) throws JsonProcessingException { public AmisResponse<?> updateFlowGraph(@RequestBody UpdateGraphItem item) throws JsonProcessingException {
flowTaskTemplateService.updateFlowGraph(item.getId(), mapper.writeValueAsString(item.getGraph())); flowTaskTemplateService.updateFlowGraph(item.getId(), mapper.writeValueAsString(item.getGraph()));
@@ -49,8 +59,7 @@ public class TaskTemplateController extends SimpleControllerSupport<FlowTaskTemp
@Override @Override
protected SaveItemMapper<FlowTaskTemplate, SaveItem> saveItemMapper() { protected SaveItemMapper<FlowTaskTemplate, SaveItem> saveItemMapper() {
var map = Mappers.getMapper(SaveItem.Mapper.class); return Mappers.getMapper(SaveItem.Mapper.class);
return item -> map.from(item, mapper);
} }
@Override @Override
@@ -69,15 +78,9 @@ public class TaskTemplateController extends SimpleControllerSupport<FlowTaskTemp
private Long id; private Long id;
private String name; private String name;
private String description; private String description;
private Map<String, Object> inputSchema;
@org.mapstruct.Mapper @org.mapstruct.Mapper
public static abstract class Mapper { public interface Mapper extends SaveItemMapper<FlowTaskTemplate, SaveItem> {
public abstract FlowTaskTemplate from(SaveItem saveItem, @Context ObjectMapper mapper) throws Exception;
protected String mapInputSchema(Map<String, Object> inputSchema, @Context ObjectMapper mapper) throws JsonProcessingException {
return mapper.writeValueAsString(inputSchema);
}
} }
} }
@@ -105,6 +108,9 @@ public class TaskTemplateController extends SimpleControllerSupport<FlowTaskTemp
public abstract DetailItem from(FlowTaskTemplate template, @Context ObjectMapper mapper) throws Exception; public abstract DetailItem from(FlowTaskTemplate template, @Context ObjectMapper mapper) throws Exception;
public Map<String, Object> mapJson(String source, @Context ObjectMapper mapper) throws Exception { public Map<String, Object> mapJson(String source, @Context ObjectMapper mapper) throws Exception {
if (ObjectUtil.isNull(source)) {
return null;
}
return mapper.readValue(source, new TypeReference<>() { return mapper.readValue(source, new TypeReference<>() {
}); });
} }

View File

@@ -6,6 +6,7 @@ import cn.hutool.extra.template.TemplateEngine;
import cn.hutool.extra.template.TemplateUtil; import cn.hutool.extra.template.TemplateUtil;
import com.lanyuanxiaoyao.service.ai.web.engine.entity.FlowContext; import com.lanyuanxiaoyao.service.ai.web.engine.entity.FlowContext;
import java.util.Map; import java.util.Map;
import lombok.extern.slf4j.Slf4j;
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.ImmutableMap;
@@ -13,6 +14,7 @@ import org.eclipse.collections.api.map.ImmutableMap;
* @author lanyuanxiaoyao * @author lanyuanxiaoyao
* @version 20250711 * @version 20250711
*/ */
@Slf4j
public class FlowHelper { public class FlowHelper {
private static final TemplateEngine TEMPLATE_ENGINE = TemplateUtil.createEngine(); private static final TemplateEngine TEMPLATE_ENGINE = TemplateUtil.createEngine();
@@ -45,11 +47,6 @@ public class FlowHelper {
throw new RuntimeException(StrUtil.format("Target node variable not found: {}.{}", targetNodeId, targetVariableName)); throw new RuntimeException(StrUtil.format("Target node variable not found: {}.{}", targetNodeId, targetVariableName));
} }
return targetNodeData.get(targetVariableName); return targetNodeData.get(targetVariableName);
} else if (context.getInput().containsKey(expression)) {
if (!context.getInput().containsKey(expression)) {
throw new RuntimeException(StrUtil.format("Target variable not found in input {}", expression));
}
return context.getInput().get(expression);
} }
return null; return null;
} }

View File

@@ -2,12 +2,10 @@ 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) {

View File

@@ -2,10 +2,13 @@ package com.lanyuanxiaoyao.service.ai.web.engine.node;
import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.lanyuanxiaoyao.service.ai.web.configuration.SpringBeanGetter;
import com.lanyuanxiaoyao.service.ai.web.engine.FlowHelper; import com.lanyuanxiaoyao.service.ai.web.engine.FlowHelper;
import com.lanyuanxiaoyao.service.ai.web.engine.FlowNodeRunner; import com.lanyuanxiaoyao.service.ai.web.engine.FlowNodeRunner;
import com.lanyuanxiaoyao.service.ai.web.engine.node.code.CodeExecutor; import com.lanyuanxiaoyao.service.ai.web.engine.node.code.CodeExecutor;
import com.lanyuanxiaoyao.service.ai.web.engine.node.code.JavaScriptCodeExecutor; import com.lanyuanxiaoyao.service.ai.web.engine.node.code.JavaScriptCodeExecutor;
import com.lanyuanxiaoyao.service.ai.web.engine.node.code.PythonCodeExecutor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
/** /**
@@ -16,14 +19,15 @@ import lombok.extern.slf4j.Slf4j;
public class CodeNode extends FlowNodeRunner { public class CodeNode extends FlowNodeRunner {
@Override @Override
public void run() { public void run() {
var mapper = SpringBeanGetter.getBean(ObjectMapper.class);
var inputVariablesMap = FlowHelper.generateInputVariablesMap(getNodeId(), getContext()); var inputVariablesMap = FlowHelper.generateInputVariablesMap(getNodeId(), getContext());
var type = this.<String>getData("type"); var type = this.<String>getData("type");
var script = this.<String>getData("content"); var script = this.<String>getData("content");
CodeExecutor executor = null; CodeExecutor executor = switch (type) {
switch (type) { case "javascript" -> new JavaScriptCodeExecutor(mapper);
case "javascript": case "python" -> new PythonCodeExecutor(mapper);
executor = new JavaScriptCodeExecutor(); default -> null;
} };
if (ObjectUtil.isNull(executor)) { if (ObjectUtil.isNull(executor)) {
throw new RuntimeException(StrUtil.format("Unsupported type: {}", type)); throw new RuntimeException(StrUtil.format("Unsupported type: {}", type));
} }

View File

@@ -0,0 +1,25 @@
package com.lanyuanxiaoyao.service.ai.web.engine.node;
import com.lanyuanxiaoyao.service.ai.web.engine.FlowNodeRunner;
import java.util.Map;
import lombok.extern.slf4j.Slf4j;
/**
* @author lanyuanxiaoyao
* @version 20250711
*/
@Slf4j
public class InputNode extends FlowNodeRunner {
public static final String KEY = "flow_inputs";
@Override
public void run() {
var inputData = getContext().getData().get(KEY);
var inputs = this.<Map<String, Object>>getData("inputs");
for (String variable : inputs.keySet()) {
if (inputData.containsKey(variable)) {
setData(variable, inputData.get(variable));
}
}
}
}

View File

@@ -16,7 +16,6 @@ public class LlmNode extends FlowNodeRunner {
@Override @Override
public void run() { public void run() {
var variableMap = FlowHelper.generateInputVariablesMap(getNodeId(), getContext()); var variableMap = FlowHelper.generateInputVariablesMap(getNodeId(), getContext());
log.info("Variable map: {}", variableMap);
var sourcePrompt = (String) getData("systemPrompt"); var sourcePrompt = (String) getData("systemPrompt");
if (StrUtil.isNotBlank(sourcePrompt)) { if (StrUtil.isNotBlank(sourcePrompt)) {
var prompt = FlowHelper.renderTemplateText(sourcePrompt, variableMap.toMap()); var prompt = FlowHelper.renderTemplateText(sourcePrompt, variableMap.toMap());

View File

@@ -10,10 +10,11 @@ import lombok.extern.slf4j.Slf4j;
*/ */
@Slf4j @Slf4j
public class OutputNode extends FlowNodeRunner { public class OutputNode extends FlowNodeRunner {
private static final String KEY = "flow_outputs";
@Override @Override
public void run() { public void run() {
String expression = getData("output"); var variableMap = FlowHelper.generateInputVariablesMap(getNodeId(), getContext());
var targetVariable = FlowHelper.generateVariable(expression, getContext()); getContext().getData().put(KEY, variableMap.toMap());
log.info("Target: {}", targetVariable);
} }
} }

View File

@@ -1,10 +1,13 @@
package com.lanyuanxiaoyao.service.ai.web.engine.node; package com.lanyuanxiaoyao.service.ai.web.engine.node;
import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.NumberUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import com.lanyuanxiaoyao.service.ai.web.engine.FlowHelper; import com.lanyuanxiaoyao.service.ai.web.engine.FlowHelper;
import com.lanyuanxiaoyao.service.ai.web.engine.FlowNodeOptionalRunner; import com.lanyuanxiaoyao.service.ai.web.engine.FlowNodeOptionalRunner;
import java.math.BigDecimal;
import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@@ -44,22 +47,41 @@ public class SwitchNode extends FlowNodeOptionalRunner {
var left = FlowHelper.generateVariable(leftVariable, getContext()); var left = FlowHelper.generateVariable(leftVariable, getContext());
var operator = (String) condition.get("op"); var operator = (String) condition.get("op");
var right = condition.get("right"); var right = condition.get("right");
if (right instanceof Map<?, ?>) { if (left instanceof CharSequence || left instanceof Boolean) {
var rightVariable = (String) BeanUtil.getProperty(condition, "right.field"); String source = StrUtil.toStringOrNull(left);
if (StrUtil.isNotBlank(rightVariable)) { String target = StrUtil.toStringOrNull(right);
right = FlowHelper.generateVariable(rightVariable, getContext()); return switch (operator) {
} case "equal" -> StrUtil.equals(source, target);
case "not_equal" -> !StrUtil.equals(source, target);
case "is_empty" -> StrUtil.isBlank(source);
case "is_not_empty" -> StrUtil.isNotBlank(source);
case "like" -> StrUtil.contains(source, target);
case "not_like" -> !StrUtil.contains(source, target);
case "starts_with" -> StrUtil.startWith(source, target);
case "ends_with" -> StrUtil.endWith(source, target);
default -> false;
};
} else if (left instanceof Number source) {
var sourceNumber = new BigDecimal(StrUtil.toString(source));
var targetNumber = new BigDecimal(StrUtil.toString(right));
return switch (operator) {
case "equal" -> NumberUtil.equals(sourceNumber, targetNumber);
case "not_equal" -> !NumberUtil.equals(sourceNumber, targetNumber);
case "greater" -> NumberUtil.isGreater(sourceNumber, targetNumber);
case "greater_equal" -> NumberUtil.isGreaterOrEqual(sourceNumber, targetNumber);
case "less" -> NumberUtil.isLess(sourceNumber, targetNumber);
case "less_equal" -> NumberUtil.isLessOrEqual(sourceNumber, targetNumber);
default -> false;
};
} else if (left instanceof Collection<?> source) {
return switch (operator) {
case "is_empty" -> CollectionUtil.isEmpty(source);
case "is_not_empty" -> CollectionUtil.isNotEmpty(source);
case "contain" -> CollectionUtil.safeContains(source, right);
case "not_contain" -> !CollectionUtil.safeContains(source, right);
default -> false;
};
} }
return switch (operator) { return false;
case "equal" -> ObjectUtil.equals(left, right);
case "not_equal" -> ObjectUtil.notEqual(left, right);
case "is_empty" -> ObjectUtil.isEmpty(left);
case "is_not_empty" -> ObjectUtil.isNotEmpty(left);
case "like" -> StrUtil.contains((String) left, (String) right);
case "not_like" -> !StrUtil.contains((String) left, (String) right);
case "starts_with" -> StrUtil.startWith((String) left, (String) right);
case "ends_with" -> StrUtil.endWith((String) left, (String) right);
default -> false;
};
} }
} }

View File

@@ -1,18 +1,29 @@
package com.lanyuanxiaoyao.service.ai.web.engine.node.code; package com.lanyuanxiaoyao.service.ai.web.engine.node.code;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
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.ImmutableMap;
import org.graalvm.polyglot.Context; import org.graalvm.polyglot.Context;
import org.graalvm.polyglot.Engine; import org.graalvm.polyglot.Engine;
import org.graalvm.polyglot.Source; import org.graalvm.polyglot.Source;
import org.graalvm.polyglot.TypeLiteral;
/** /**
* @author lanyuanxiaoyao * @author lanyuanxiaoyao
* @version 20250717 * @version 20250717
*/ */
public class JavaScriptCodeExecutor implements CodeExecutor{ @Slf4j
public class JavaScriptCodeExecutor implements CodeExecutor {
private final ObjectMapper mapper;
public JavaScriptCodeExecutor(ObjectMapper mapper) {
this.mapper = mapper;
}
@SneakyThrows
@Override @Override
public ImmutableMap<String, Object> execute(String script, ImmutableMap<String, Object> inputVariablesMap) { public ImmutableMap<String, Object> execute(String script, ImmutableMap<String, Object> inputVariablesMap) {
if (StrUtil.isBlank(script)) { if (StrUtil.isBlank(script)) {
@@ -26,7 +37,7 @@ public class JavaScriptCodeExecutor implements CodeExecutor{
.build() .build()
) { ) {
var bindings = context.getBindings("js"); var bindings = context.getBindings("js");
inputVariablesMap.forEachKeyValue(bindings::putMember); bindings.putMember("context", inputVariablesMap);
var result = context.eval( var result = context.eval(
Source.create( Source.create(
"js", "js",
@@ -34,11 +45,12 @@ public class JavaScriptCodeExecutor implements CodeExecutor{
function process() { function process() {
%s %s
} }
process(); var result = process();
JSON.stringify(result? result: {})
""".formatted(script) """.formatted(script)
) )
); );
return Maps.immutable.ofAll(result.as(new TypeLiteral<>() {})); return mapper.readValue(result.asString(), new TypeReference<>() {});
} }
} }
} }

View File

@@ -0,0 +1,50 @@
package com.lanyuanxiaoyao.service.ai.web.engine.node.code;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.Arrays;
import java.util.stream.Collectors;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.collections.api.map.ImmutableMap;
import org.python.core.PySystemState;
import org.python.util.PythonInterpreter;
/**
* @author lanyuanxiaoyao
* @version 20250718
*/
@Slf4j
public class PythonCodeExecutor implements CodeExecutor {
private final ObjectMapper mapper;
public PythonCodeExecutor(ObjectMapper mapper) {
this.mapper = mapper;
}
@SneakyThrows
@Override
public ImmutableMap<String, Object> execute(String script, ImmutableMap<String, Object> inputVariablesMap) {
try (var systemState = new PySystemState()) {
systemState.setdefaultencoding("UTF-8");
var interpreter = new PythonInterpreter(null, systemState);
script = Arrays.stream(script.split("\n"))
.map(line -> " " + line)
.collect(Collectors.joining("\n"));
interpreter.set("context", inputVariablesMap);
var pythonScript = interpreter.compile(
"""
import json
def process():
%s
result = json.dumps(process())
""".formatted(script)
);
interpreter.exec(pythonScript);
var result = interpreter.get("result");
return mapper.readValue((String) result.__tojava__(String.class), new TypeReference<>() {});
}
}
}

View File

@@ -32,7 +32,7 @@ public class FlowTask extends SimpleEntity {
@Comment("任务对应的模板功能、内容说明") @Comment("任务对应的模板功能、内容说明")
private String templateDescription; private String templateDescription;
@Comment("任务对应的模板入参Schema") @Comment("任务对应的模板入参Schema")
@Column(nullable = false, columnDefinition = "longtext") @Column(columnDefinition = "longtext")
private String templateInputSchema; private String templateInputSchema;
@Comment("任务对应的模板前端流程图数据") @Comment("任务对应的模板前端流程图数据")
@Column(nullable = false, columnDefinition = "longtext") @Column(nullable = false, columnDefinition = "longtext")

View File

@@ -28,7 +28,7 @@ public class FlowTaskTemplate extends SimpleEntity {
@Comment("模板功能、内容说明") @Comment("模板功能、内容说明")
private String description; private String description;
@Comment("模板入参Schema") @Comment("模板入参Schema")
@Column(nullable = false, columnDefinition = "longtext") @Column(columnDefinition = "longtext")
private String inputSchema; private String inputSchema;
@Comment("前端流程图数据") @Comment("前端流程图数据")
@Column(nullable = false, columnDefinition = "longtext") @Column(nullable = false, columnDefinition = "longtext")

View File

@@ -0,0 +1,14 @@
package com.lanyuanxiaoyao.service.ai.web.entity.vo;
import com.lanyuanxiaoyao.service.ai.web.engine.entity.FlowEdge;
import com.lanyuanxiaoyao.service.ai.web.engine.entity.FlowNode;
import lombok.Data;
import org.eclipse.collections.api.map.MutableMap;
import org.eclipse.collections.api.set.ImmutableSet;
@Data
public class FlowGraphVo {
private ImmutableSet<FlowNode> nodes;
private ImmutableSet<FlowEdge> edges;
private MutableMap<String, MutableMap<String, Object>> data;
}

View File

@@ -7,9 +7,7 @@ import com.lanyuanxiaoyao.service.ai.web.entity.context.FeedbackContext;
import com.lanyuanxiaoyao.service.ai.web.repository.FeedbackRepository; import com.lanyuanxiaoyao.service.ai.web.repository.FeedbackRepository;
import com.yomahub.liteflow.core.FlowExecutor; import com.yomahub.liteflow.core.FlowExecutor;
import java.util.List; import java.util.List;
import java.util.concurrent.TimeUnit;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@@ -24,7 +22,7 @@ import org.springframework.transaction.annotation.Transactional;
this.executor = executor; this.executor = executor;
} }
@Scheduled(initialDelay = 1, fixedDelay = 1, timeUnit = TimeUnit.MINUTES) // @Scheduled(initialDelay = 1, fixedDelay = 1, timeUnit = TimeUnit.MINUTES)
public void analysis() { public void analysis() {
List<Feedback> feedbacks = repository.findAll( List<Feedback> feedbacks = repository.findAll(
builder -> builder builder -> builder

View File

@@ -1,30 +1,28 @@
package com.lanyuanxiaoyao.service.ai.web.service.task; package com.lanyuanxiaoyao.service.ai.web.service.task;
import cn.hutool.core.util.IdUtil; import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.ObjectUtil;
import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper; 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.FlowExecutor;
import com.lanyuanxiaoyao.service.ai.web.engine.entity.FlowContext; 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.FlowGraph;
import com.lanyuanxiaoyao.service.ai.web.engine.entity.FlowNode;
import com.lanyuanxiaoyao.service.ai.web.engine.node.CodeNode; import com.lanyuanxiaoyao.service.ai.web.engine.node.CodeNode;
import com.lanyuanxiaoyao.service.ai.web.engine.node.InputNode;
import com.lanyuanxiaoyao.service.ai.web.engine.node.LlmNode; import com.lanyuanxiaoyao.service.ai.web.engine.node.LlmNode;
import com.lanyuanxiaoyao.service.ai.web.engine.node.LoopNode; import com.lanyuanxiaoyao.service.ai.web.engine.node.LoopNode;
import com.lanyuanxiaoyao.service.ai.web.engine.node.OutputNode; import com.lanyuanxiaoyao.service.ai.web.engine.node.OutputNode;
import com.lanyuanxiaoyao.service.ai.web.engine.node.SwitchNode; import com.lanyuanxiaoyao.service.ai.web.engine.node.SwitchNode;
import com.lanyuanxiaoyao.service.ai.web.engine.store.InMemoryFlowStore; 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.entity.vo.FlowGraphVo;
import com.lanyuanxiaoyao.service.ai.web.repository.FlowTaskRepository; import com.lanyuanxiaoyao.service.ai.web.repository.FlowTaskRepository;
import java.lang.reflect.InvocationTargetException; import java.lang.reflect.InvocationTargetException;
import java.util.Map; import java.util.Map;
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.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.http.converter.json.Jackson2ObjectMapperBuilder;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -42,7 +40,6 @@ public class FlowTaskService extends SimpleServiceSupport<FlowTask> {
var flowTask = detailOrThrow(id); var flowTask = detailOrThrow(id);
var graphVo = mapper.readValue(flowTask.getTemplateFlowGraph(), FlowGraphVo.class); var graphVo = mapper.readValue(flowTask.getTemplateFlowGraph(), FlowGraphVo.class);
var flowGraph = new FlowGraph(IdUtil.fastUUID(), graphVo.getNodes(), graphVo.getEdges()); var flowGraph = new FlowGraph(IdUtil.fastUUID(), graphVo.getNodes(), graphVo.getEdges());
log.info("Graph: {}", flowGraph);
var store = new InMemoryFlowStore(); var store = new InMemoryFlowStore();
var executor = new FlowExecutor( var executor = new FlowExecutor(
@@ -52,19 +49,18 @@ public class FlowTaskService extends SimpleServiceSupport<FlowTask> {
"switch-node", SwitchNode.class, "switch-node", SwitchNode.class,
"code-node", CodeNode.class, "code-node", CodeNode.class,
"llm-node", LlmNode.class, "llm-node", LlmNode.class,
"input-node", InputNode.class,
"output-node", OutputNode.class "output-node", OutputNode.class
)) ))
); );
FlowContext context = new FlowContext(); FlowContext context = new FlowContext();
context.setInput(mapper.readValue(flowTask.getInput(), new TypeReference<>() {}));
context.setData(graphVo.getData()); context.setData(graphVo.getData());
if (ObjectUtil.isNotEmpty(flowTask.getInput())) {
context.getData().put(InputNode.KEY, mapper.readValue(flowTask.getInput(), new TypeReference<>() {
}));
}
executor.execute(flowGraph, context); 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;
}
} }

View File

@@ -1,22 +1,45 @@
package com.lanyuanxiaoyao.service.ai.web.service.task; package com.lanyuanxiaoyao.service.ai.web.service.task;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.fasterxml.jackson.core.JsonProcessingException;
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.entity.FlowTaskTemplate; import com.lanyuanxiaoyao.service.ai.web.entity.FlowTaskTemplate;
import com.lanyuanxiaoyao.service.ai.web.entity.vo.FlowGraphVo;
import com.lanyuanxiaoyao.service.ai.web.repository.FlowTaskTemplateRepository; import com.lanyuanxiaoyao.service.ai.web.repository.FlowTaskTemplateRepository;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@Slf4j @Slf4j
@Service @Service
public class FlowTaskTemplateService extends SimpleServiceSupport<FlowTaskTemplate> { public class FlowTaskTemplateService extends SimpleServiceSupport<FlowTaskTemplate> {
public FlowTaskTemplateService(FlowTaskTemplateRepository flowTaskTemplateRepository) { private final ObjectMapper mapper;
public FlowTaskTemplateService(FlowTaskTemplateRepository flowTaskTemplateRepository, Jackson2ObjectMapperBuilder builder) {
super(flowTaskTemplateRepository); super(flowTaskTemplateRepository);
this.mapper = builder.build();
} }
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public void updateFlowGraph(Long id, String flowGraph) { public void updateFlowGraph(Long id, String flowGraph) throws JsonProcessingException {
var template = detailOrThrow(id); var template = detailOrThrow(id);
var graph = mapper.readValue(flowGraph, FlowGraphVo.class);
// 如果发现输入节点,就单独提取出来
var inputNode = graph.getNodes()
.detectOptional(node -> StrUtil.equals(node.type(), "input-node") && ObjectUtil.isEmpty(node.parentId()))
.orElse(null);
if (ObjectUtil.isNotNull(inputNode)) {
var nodeId = inputNode.id();
var nodeData = graph.getData().getOrDefault(nodeId, null);
if (ObjectUtil.isNotNull(nodeData) && nodeData.containsKey("inputs")) {
template.setInputSchema(mapper.writeValueAsString(nodeData.get("inputs")));
}
}
template.setFlowGraph(flowGraph); template.setFlowGraph(flowGraph);
save(template); save(template);
} }

View File

@@ -0,0 +1,29 @@
package com.lanyuanxiaoyao.service.ai.web;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.eclipsecollections.EclipseCollectionsModule;
import com.lanyuanxiaoyao.service.ai.web.engine.node.code.JavaScriptCodeExecutor;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.collections.api.factory.Maps;
/**
* @author lanyuanxiaoyao
* @version 20250717
*/
@Slf4j
public class TestCodeExecutor {
public static void main(String[] args) {
var mapper = new ObjectMapper();
mapper.registerModule(new EclipseCollectionsModule());
var executor = new JavaScriptCodeExecutor(mapper);
var result = executor.execute(
"""
return {'code': 1, 'text': context.get('text')}
""",
Maps.immutable.of(
"text", "hello world"
)
);
log.info("Result: {}", result);
}
}

View File

@@ -1,57 +0,0 @@
package com.lanyuanxiaoyao.service.ai.web;
import cn.hutool.core.util.StrUtil;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.collections.api.factory.Maps;
import org.eclipse.collections.api.map.ImmutableMap;
import org.graalvm.polyglot.Context;
import org.graalvm.polyglot.Engine;
import org.graalvm.polyglot.Source;
import org.graalvm.polyglot.TypeLiteral;
/**
* @author lanyuanxiaoyao
* @version 20250717
*/
@Slf4j
public class TestJsExecutor {
public static void main(String[] args) {
var result = executeJavascript(
// language=JavaScript
"return 'hello'",
Maps.immutable.of(
"code", 1
)
);
log.info("Result: {}", result);
}
private static ImmutableMap<String, Object> executeJavascript(String script, ImmutableMap<String, Object> inputVariablesMap) {
if (StrUtil.isBlank(script)) {
return Maps.immutable.empty();
}
try (var engin = Engine.create()) {
try (
var context = Context.newBuilder()
.allowAllAccess(true)
.engine(engin)
.build()
) {
var bindings = context.getBindings("js");
inputVariablesMap.forEachKeyValue(bindings::putMember);
var result = context.eval(
Source.create(
"js",
"""
function process() {
%s
}
process();
""".formatted(script)
)
);
return Maps.immutable.ofAll(result.as(new TypeLiteral<>() {}));
}
}
}
}

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)) 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 // @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, '连线起始节点未找到') export const sourceNodeNotFoundError = () => new CheckError(200, '连线起始节点未找到')
@@ -70,18 +83,17 @@ 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 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 // @ts-ignore
export const checkSave: (inputSchema: Record<string, Record<string, any>>, nodes: Node[], edges: Edge[], data: any) => void = (inputSchema, nodes, edges, data) => { export const checkSave: (nodes: Node[], edges: Edge[], data: any) => void = (nodes, edges, data) => {
if (isEmpty(nodes)) { if (isEmpty(nodes)) {
throw atLeastOneNode() throw atLeastOneNode()
} }
for (let node of nodes) { for (let node of nodes) {
let nodeId = node.id if (!has(data, node.id) || !data[node.id]?.finished) {
if (!has(data, nodeId) || !data[nodeId]?.finished) { throw hasUnfinishedNode(node.id)
throw hasUnfinishedNode(nodeId)
} }
if (!has(node, 'type')) { if (!has(node, 'type')) {
@@ -89,10 +101,10 @@ export const checkSave: (inputSchema: Record<string, Record<string, any>>, nodes
} }
let nodeType = node.type! let nodeType = node.type!
let nodeDefine = NodeRegistryMap[nodeType] let nodeDefine = NodeRegistryMap[nodeType]
for (let checker of nodeDefine.checkers) { for (let checker of nodeDefine.checkers.save) {
let checkResult = checker(nodeId, inputSchema, nodes, edges, data) let checkResult = checker(node.id, node.parentId, nodes, edges, data)
if (checkResult.error) { if (checkResult.error) {
throw nodeError(nodeId, checkResult.message) throw saveNodeError(node.id, checkResult.message)
} }
} }
} }

View File

@@ -11,7 +11,6 @@ import AddNodeButton from './component/AddNodeButton.tsx'
import {checkAddConnection, checkSave} from './FlowChecker.tsx' import {checkAddConnection, checkSave} from './FlowChecker.tsx'
import {useNodeDrag} from './Helper.tsx' import {useNodeDrag} from './Helper.tsx'
import {NodeRegistryMap} from './NodeRegistry.tsx' import {NodeRegistryMap} from './NodeRegistry.tsx'
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 {flowDotColor, type FlowEditorProps} from './types.ts' import {flowDotColor, type FlowEditorProps} from './types.ts'
@@ -46,7 +45,6 @@ const FlowableDiv = styled.div`
function FlowEditor(props: FlowEditorProps) { function FlowEditor(props: FlowEditorProps) {
const navigate = useNavigate() const navigate = useNavigate()
const [messageApi, contextHolder] = message.useMessage()
const {data, setData} = useDataStore() const {data, setData} = useDataStore()
const { const {
@@ -59,8 +57,6 @@ function FlowEditor(props: FlowEditorProps) {
onConnect, onConnect,
} = useFlowStore() } = useFlowStore()
const {inputSchema, setInputSchema} = useContextStore()
useEffect(() => { useEffect(() => {
// language=JSON // language=JSON
// let initialData = JSON.parse('{"nodes":[{"id":"TCxPixrdkI","type":"start-node","position":{"x":-256,"y":109.5},"data":{},"measured":{"width":256,"height":83},"selected":false,"dragging":false},{"id":"tGs78_ietp","type":"llm-node","position":{"x":108,"y":-2.5},"data":{},"measured":{"width":256,"height":105},"selected":false,"dragging":false},{"id":"OeZdaU7LpY","type":"llm-node","position":{"x":111,"y":196},"data":{},"measured":{"width":256,"height":105},"selected":false,"dragging":false},{"id":"LjfoCYZo-E","type":"knowledge-node","position":{"x":497.62196259607214,"y":-10.792497317791003},"data":{},"measured":{"width":256,"height":75},"selected":false,"dragging":false},{"id":"sQM_22GYB5","type":"end-node","position":{"x":874.3164534765615,"y":151.70316541496913},"data":{},"measured":{"width":256,"height":75},"selected":false,"dragging":false},{"id":"KpMH_xc3ZZ","type":"llm-node","position":{"x":529.6286840434341,"y":150.4721376669937},"data":{},"measured":{"width":256,"height":75},"selected":false,"dragging":false},{"id":"pOrR6EMVbe","type":"switch-node","position":{"x":110.33793030183864,"y":373.9551529987239},"data":{},"measured":{"width":256,"height":157},"selected":false,"dragging":false}],"edges":[{"source":"TCxPixrdkI","sourceHandle":"source","target":"tGs78_ietp","targetHandle":"target","id":"xy-edge__TCxPixrdkIsource-tGs78_ietptarget"},{"source":"TCxPixrdkI","sourceHandle":"source","target":"OeZdaU7LpY","targetHandle":"target","id":"xy-edge__TCxPixrdkIsource-OeZdaU7LpYtarget"},{"source":"tGs78_ietp","sourceHandle":"source","target":"LjfoCYZo-E","targetHandle":"target","id":"xy-edge__tGs78_ietpsource-LjfoCYZo-Etarget"},{"source":"LjfoCYZo-E","sourceHandle":"source","target":"KpMH_xc3ZZ","targetHandle":"target","id":"xy-edge__LjfoCYZo-Esource-KpMH_xc3ZZtarget"},{"source":"OeZdaU7LpY","sourceHandle":"source","target":"KpMH_xc3ZZ","targetHandle":"target","id":"xy-edge__OeZdaU7LpYsource-KpMH_xc3ZZtarget"},{"source":"KpMH_xc3ZZ","sourceHandle":"source","target":"sQM_22GYB5","targetHandle":"target","id":"xy-edge__KpMH_xc3ZZsource-sQM_22GYB5target"},{"source":"TCxPixrdkI","sourceHandle":"source","target":"pOrR6EMVbe","id":"xy-edge__TCxPixrdkIsource-pOrR6EMVbe"},{"source":"pOrR6EMVbe","sourceHandle":"3","target":"sQM_22GYB5","targetHandle":"target","id":"xy-edge__pOrR6EMVbe3-sQM_22GYB5target"},{"source":"pOrR6EMVbe","sourceHandle":"1","target":"KpMH_xc3ZZ","targetHandle":"target","id":"xy-edge__pOrR6EMVbe1-KpMH_xc3ZZtarget"}],"data":{"tGs78_ietp":{"model":"qwen3","outputs":{"text":{"type":"string"}},"systemPrompt":"你是个聪明人"},"OeZdaU7LpY":{"model":"qwen3","outputs":{"text":{"type":"string"}},"systemPrompt":"你也是个聪明人"}}}') // let initialData = JSON.parse('{"nodes":[{"id":"TCxPixrdkI","type":"start-node","position":{"x":-256,"y":109.5},"data":{},"measured":{"width":256,"height":83},"selected":false,"dragging":false},{"id":"tGs78_ietp","type":"llm-node","position":{"x":108,"y":-2.5},"data":{},"measured":{"width":256,"height":105},"selected":false,"dragging":false},{"id":"OeZdaU7LpY","type":"llm-node","position":{"x":111,"y":196},"data":{},"measured":{"width":256,"height":105},"selected":false,"dragging":false},{"id":"LjfoCYZo-E","type":"knowledge-node","position":{"x":497.62196259607214,"y":-10.792497317791003},"data":{},"measured":{"width":256,"height":75},"selected":false,"dragging":false},{"id":"sQM_22GYB5","type":"end-node","position":{"x":874.3164534765615,"y":151.70316541496913},"data":{},"measured":{"width":256,"height":75},"selected":false,"dragging":false},{"id":"KpMH_xc3ZZ","type":"llm-node","position":{"x":529.6286840434341,"y":150.4721376669937},"data":{},"measured":{"width":256,"height":75},"selected":false,"dragging":false},{"id":"pOrR6EMVbe","type":"switch-node","position":{"x":110.33793030183864,"y":373.9551529987239},"data":{},"measured":{"width":256,"height":157},"selected":false,"dragging":false}],"edges":[{"source":"TCxPixrdkI","sourceHandle":"source","target":"tGs78_ietp","targetHandle":"target","id":"xy-edge__TCxPixrdkIsource-tGs78_ietptarget"},{"source":"TCxPixrdkI","sourceHandle":"source","target":"OeZdaU7LpY","targetHandle":"target","id":"xy-edge__TCxPixrdkIsource-OeZdaU7LpYtarget"},{"source":"tGs78_ietp","sourceHandle":"source","target":"LjfoCYZo-E","targetHandle":"target","id":"xy-edge__tGs78_ietpsource-LjfoCYZo-Etarget"},{"source":"LjfoCYZo-E","sourceHandle":"source","target":"KpMH_xc3ZZ","targetHandle":"target","id":"xy-edge__LjfoCYZo-Esource-KpMH_xc3ZZtarget"},{"source":"OeZdaU7LpY","sourceHandle":"source","target":"KpMH_xc3ZZ","targetHandle":"target","id":"xy-edge__OeZdaU7LpYsource-KpMH_xc3ZZtarget"},{"source":"KpMH_xc3ZZ","sourceHandle":"source","target":"sQM_22GYB5","targetHandle":"target","id":"xy-edge__KpMH_xc3ZZsource-sQM_22GYB5target"},{"source":"TCxPixrdkI","sourceHandle":"source","target":"pOrR6EMVbe","id":"xy-edge__TCxPixrdkIsource-pOrR6EMVbe"},{"source":"pOrR6EMVbe","sourceHandle":"3","target":"sQM_22GYB5","targetHandle":"target","id":"xy-edge__pOrR6EMVbe3-sQM_22GYB5target"},{"source":"pOrR6EMVbe","sourceHandle":"1","target":"KpMH_xc3ZZ","targetHandle":"target","id":"xy-edge__pOrR6EMVbe1-KpMH_xc3ZZtarget"}],"data":{"tGs78_ietp":{"model":"qwen3","outputs":{"text":{"type":"string"}},"systemPrompt":"你是个聪明人"},"OeZdaU7LpY":{"model":"qwen3","outputs":{"text":{"type":"string"}},"systemPrompt":"你也是个聪明人"}}}')
@@ -73,7 +69,6 @@ function FlowEditor(props: FlowEditorProps) {
setNodes(initialNodes) setNodes(initialNodes)
setEdges(initialEdges) setEdges(initialEdges)
setInputSchema(props.inputSchema)
}, [props.graphData]) }, [props.graphData])
const { const {
@@ -84,7 +79,6 @@ function FlowEditor(props: FlowEditorProps) {
return ( return (
<FlowableDiv className="h-full w-full"> <FlowableDiv className="h-full w-full">
{contextHolder}
<ReactFlow <ReactFlow
className="rounded-xl" className="rounded-xl"
nodes={nodes} nodes={nodes}
@@ -100,7 +94,7 @@ function FlowEditor(props: FlowEditorProps) {
onConnect(connection) onConnect(connection)
} catch (e) { } catch (e) {
// @ts-ignore // @ts-ignore
messageApi.error(e.toString()) message.error(e.toString())
} }
}} }}
// @ts-ignore // @ts-ignore
@@ -129,11 +123,11 @@ 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(inputSchema, nodes, edges, data) checkSave(nodes, edges, data)
props.onGraphDataChange({nodes, edges, data}) props.onGraphDataChange({nodes, edges, data})
} catch (e) { } catch (e) {
// @ts-ignore // @ts-ignore
messageApi.error(e.toString()) message.error(e.toString())
} }
}}> }}>
<SaveFilled/> <SaveFilled/>

View File

@@ -3,7 +3,6 @@ import type {Option} from 'amis/lib/Schema'
import {contain, find, has, isEqual, max, min, unique} from 'licia' import {contain, find, has, isEqual, max, min, unique} from 'licia'
import {type DependencyList, type MouseEvent as ReactMouseEvent, useCallback, useRef} from 'react' import {type DependencyList, type MouseEvent as ReactMouseEvent, useCallback, useRef} from 'react'
import Queue from 'yocto-queue' import Queue from 'yocto-queue'
import {originTypeMap} from '../../pages/ai/task/InputSchema.tsx'
import {useFlowStore} from './store/FlowStore.ts' import {useFlowStore} from './store/FlowStore.ts'
import {type OutputVariable, type OutputVariableType} from './types.ts' import {type OutputVariable, type OutputVariableType} from './types.ts'
@@ -21,17 +20,10 @@ export const getAllIncomerNodeById: (id: string, nodes: Node[], edges: Edge[]) =
return unique(result, (a, b) => isEqual(a, b)) return unique(result, (a, b) => isEqual(a, b))
} }
export const getAllIncomerNodeOutputVariables: (id: string, inputSchema: Record<string, Record<string, any>>, nodes: Node[], edges: Edge[], data: any) => OutputVariable[] = (id, inputSchema, nodes, edges, data) => { export const getAllIncomerNodeOutputVariables: (id: string, nodes: Node[], edges: Edge[], data: any) => OutputVariable[] = (id, nodes, edges, data) => {
let inputSchemaVariables: OutputVariable[] = Object.keys(inputSchema).map(key => ({
group: '流程入参',
name: `${key}${inputSchema[key]?.label ? ` (${inputSchema[key].label})` : ''}`,
type: originTypeMap[inputSchema[key]?.type ?? ''],
variable: key,
}))
let currentNode = find(nodes, n => isEqual(id, n.id)) let currentNode = find(nodes, n => isEqual(id, n.id))
if (!currentNode) { if (!currentNode) {
return inputSchemaVariables return []
} }
let incomerIds = getAllIncomerNodeById(id, nodes, edges) let incomerIds = getAllIncomerNodeById(id, nodes, edges)
@@ -63,7 +55,6 @@ export const getAllIncomerNodeOutputVariables: (id: string, inputSchema: Record<
} }
return [ return [
...inputSchemaVariables,
...(currentNode.parentId ? [ ...(currentNode.parentId ? [
{ {
group: '循环入参', group: '循环入参',
@@ -82,9 +73,9 @@ export const getAllIncomerNodeOutputVariables: (id: string, inputSchema: Record<
] ]
} }
export const generateAllIncomerOutputVariablesFormOptions: (id: string, inputSchema: Record<string, Record<string, any>>, nodes: Node[], edges: Edge[], data: any, targetTypes?: OutputVariableType[]) => Option[] = (id, inputSchema, nodes, edges, data, targetTypes) => { export const generateAllIncomerOutputVariablesFormOptions: (id: string, nodes: Node[], edges: Edge[], data: any, targetTypes?: OutputVariableType[]) => Option[] = (id, nodes, edges, data, targetTypes) => {
let optionMap: Record<string, Option[]> = {} let optionMap: Record<string, Option[]> = {}
for (const item of getAllIncomerNodeOutputVariables(id, inputSchema, nodes, edges, data)) { for (const item of getAllIncomerNodeOutputVariables(id, nodes, edges, data)) {
if (targetTypes && !contain(targetTypes, item.type)) { if (targetTypes && !contain(targetTypes, item.type)) {
continue continue
} }
@@ -117,10 +108,12 @@ const numberOperators: ConditionOperator[] = [
{label: '小于或等于', value: 'less_equal'}, {label: '小于或等于', value: 'less_equal'},
] ]
const numberDefaultOperator: string = 'equal' const numberDefaultOperator: string = 'equal'
const arrayOperators: ConditionOperator[] = ['is_empty', 'is_not_empty']
const arrayDefaultOperator: string = 'is_empty'
export const generateAllIncomerOutputVariablesConditions: (id: string, inputSchema: Record<string, Record<string, any>>, nodes: Node[], edges: Edge[], data: any) => Option[] = (id, inputSchema, nodes, edges, data) => { export const generateAllIncomerOutputVariablesConditions: (id: string, nodes: Node[], edges: Edge[], data: any) => Option[] = (id, nodes, edges, data) => {
let optionMap: Record<string, Option[]> = {} let optionMap: Record<string, Option[]> = {}
for (const item of getAllIncomerNodeOutputVariables(id, inputSchema, nodes, edges, data)) { for (const item of getAllIncomerNodeOutputVariables(id, nodes, edges, data)) {
if (!optionMap[item.group]) { if (!optionMap[item.group]) {
optionMap[item.group] = [] optionMap[item.group] = []
} }
@@ -159,6 +152,10 @@ export const generateAllIncomerOutputVariablesConditions: (id: string, inputSche
defaultOp: numberDefaultOperator, defaultOp: numberDefaultOperator,
operators: numberOperators, operators: numberOperators,
} : {}), } : {}),
...((item.type === 'array-text' || item.type === 'array-object') ? {
defaultOp: arrayDefaultOperator,
operators: arrayOperators,
} : {}),
}) })
} }
return Object.keys(optionMap) return Object.keys(optionMap)

View File

@@ -1,4 +1,4 @@
import {has, isEmpty} from 'licia' import {has, isEmpty, isEqual} from 'licia'
import {getAllIncomerNodeOutputVariables} from './Helper.tsx' import {getAllIncomerNodeOutputVariables} from './Helper.tsx'
import CodeNode from './node/CodeNode.tsx' import CodeNode from './node/CodeNode.tsx'
import KnowledgeNode from './node/KnowledgeNode.tsx' import KnowledgeNode from './node/KnowledgeNode.tsx'
@@ -7,15 +7,16 @@ import LoopNode from './node/LoopNode.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 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 => { const inputSingleVariableChecker: (field: string) => SaveNodeChecker = field => {
return (id, inputSchema, nodes, edges, data) => { return (id, _parentId, nodes, edges, data) => {
let nodeData = data[id] ?? {} let nodeData = data[id] ?? {}
if (has(nodeData, field)) { if (has(nodeData, field)) {
let expression = nodeData?.[field] ?? '' let expression = nodeData?.[field] ?? ''
if (!isEmpty(expression)) { if (!isEmpty(expression)) {
let outputVariables = new Set(getAllIncomerNodeOutputVariables(id, inputSchema, nodes, edges, data).map(i => i.variable)) let outputVariables = new Set(getAllIncomerNodeOutputVariables(id, nodes, edges, data).map(i => i.variable))
if (!outputVariables.has(expression)) { if (!outputVariables.has(expression)) {
return { return {
error: true, error: true,
@@ -28,12 +29,12 @@ const inputSingleVariableChecker: (field: string) => NodeChecker = field => {
} }
} }
const inputMultiVariableChecker: NodeChecker = (id, inputSchema, nodes, edges, data) => { const inputMultiVariableChecker: SaveNodeChecker = (id, _parentId, 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 ?? {}
if (!isEmpty(inputs)) { if (!isEmpty(inputs)) {
let outputVariables = new Set(getAllIncomerNodeOutputVariables(id, inputSchema, nodes, edges, data).map(i => i.variable)) let outputVariables = new Set(getAllIncomerNodeOutputVariables(id, nodes, edges, data).map(i => i.variable))
for (const key of Object.keys(inputs)) { for (const key of Object.keys(inputs)) {
let variable = inputs[key]?.variable ?? '' let variable = inputs[key]?.variable ?? ''
if (!outputVariables.has(variable)) { if (!outputVariables.has(variable)) {
@@ -48,6 +49,13 @@ const inputMultiVariableChecker: NodeChecker = (id, inputSchema, nodes, edges, d
return {error: false} return {error: false}
} }
const noMoreThanOneNodeType: AddNodeChecker = (type, parentId, nodes) => {
return {
error: nodes.filter(n => isEqual(n.parentId, parentId) && isEqual(n.type, type)).length > 0,
message: `同一个流程(子流程)中类型为 ${type} 的节点至多有一个`
}
}
export const NodeRegistry: NodeDefine[] = [ export const NodeRegistry: NodeDefine[] = [
{ {
key: 'llm-node', key: 'llm-node',
@@ -56,7 +64,10 @@ export const NodeRegistry: NodeDefine[] = [
icon: <i className="fa fa-message"/>, icon: <i className="fa fa-message"/>,
description: '使用大模型对话', description: '使用大模型对话',
component: LlmNode, component: LlmNode,
checkers: [inputMultiVariableChecker], checkers: {
add: [],
save: [inputMultiVariableChecker]
},
}, },
{ {
key: 'knowledge-node', key: 'knowledge-node',
@@ -65,7 +76,10 @@ 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: [inputMultiVariableChecker], checkers: {
add: [],
save: [inputMultiVariableChecker]
},
}, },
{ {
key: 'code-node', key: 'code-node',
@@ -74,7 +88,10 @@ export const NodeRegistry: NodeDefine[] = [
icon: <i className="fa fa-code"/>, icon: <i className="fa fa-code"/>,
description: '执行自定义的处理代码', description: '执行自定义的处理代码',
component: CodeNode, component: CodeNode,
checkers: [inputMultiVariableChecker], checkers: {
add: [],
save: [inputMultiVariableChecker]
},
}, },
{ {
key: 'template-node', key: 'template-node',
@@ -83,7 +100,10 @@ export const NodeRegistry: NodeDefine[] = [
icon: <i className="fa fa-pen-nib"/>, icon: <i className="fa fa-pen-nib"/>,
description: '使用模板聚合转换变量表示', description: '使用模板聚合转换变量表示',
component: TemplateNode, component: TemplateNode,
checkers: [inputMultiVariableChecker], checkers: {
add: [],
save: [inputMultiVariableChecker]
},
}, },
{ {
key: 'switch-node', key: 'switch-node',
@@ -92,7 +112,10 @@ export const NodeRegistry: NodeDefine[] = [
icon: <i className="fa fa-code-fork"/>, icon: <i className="fa fa-code-fork"/>,
description: '根据不同的情况前往不同的分支', description: '根据不同的情况前往不同的分支',
component: SwitchNode, component: SwitchNode,
checkers: [], checkers: {
add: [],
save: [],
},
}, },
{ {
key: 'loop-node', key: 'loop-node',
@@ -101,16 +124,35 @@ export const NodeRegistry: NodeDefine[] = [
icon: <i className="fa fa-repeat"/>, icon: <i className="fa fa-repeat"/>,
description: '实现循环执行流程', description: '实现循环执行流程',
component: LoopNode, 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', key: 'output-node',
group: '输出节点', group: '数据节点',
name: '输出', name: '输出',
icon: <i className="fa fa-file"/>, icon: <i className="fa fa-file"/>,
description: '定义输出变量', description: '定义流程输出变量',
component: OutputNode, component: OutputNode,
checkers: [inputSingleVariableChecker('output')], checkers: {
add: [noMoreThanOneNodeType],
save: [inputSingleVariableChecker('output')]
},
}, },
] ]

View File

@@ -1,5 +1,5 @@
import {PlusCircleFilled} from '@ant-design/icons' import {PlusCircleFilled} from '@ant-design/icons'
import {Button, Dropdown} from 'antd' import {Button, Dropdown, message} from 'antd'
import type {ButtonProps} from 'antd/lib' import type {ButtonProps} from 'antd/lib'
import {isEqual, randomId, unique} from 'licia' import {isEqual, randomId, unique} from 'licia'
import {commonInfo} from '../../../util/amis.tsx' import {commonInfo} from '../../../util/amis.tsx'
@@ -34,7 +34,7 @@ const AddNodeButton = (props: AddNodeButtonProps) => {
if (commonInfo.debug) { if (commonInfo.debug) {
console.info('Add', key, JSON.stringify({nodes, edges, data})) 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 nodeId = randomId(10, 'qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM')
let define = NodeRegistryMap[key] let define = NodeRegistryMap[key]
@@ -62,7 +62,7 @@ const AddNodeButton = (props: AddNodeButtonProps) => {
}) })
} catch (e) { } catch (e) {
// @ts-ignore // @ts-ignore
messageApi.error(e.toString()) message.error(e.toString())
} }
}, },
}} }}

View File

@@ -8,11 +8,10 @@ import {amisRender, commonInfo, horizontalFormOptions} from '../../../util/amis.
import {generateAllIncomerOutputVariablesFormOptions} 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 {OutputVariableTypeMap} from '../types.ts' import {type FormSchema, OutputVariableTypeMap} from '../types.ts'
export function inputsFormColumns( export function inputsFormColumns(
nodeId: string, nodeId: string,
inputSchema: Record<string, Record<string, any>>,
nodes: Node[], nodes: Node[],
edges: Edge[], edges: Edge[],
data: any, data: any,
@@ -38,7 +37,6 @@ export function inputsFormColumns(
selectMode: 'group', selectMode: 'group',
options: generateAllIncomerOutputVariablesFormOptions( options: generateAllIncomerOutputVariablesFormOptions(
nodeId, nodeId,
inputSchema,
nodes, nodes,
edges, edges,
data, data,
@@ -88,7 +86,7 @@ type AmisNodeProps = {
nodeProps: NodeProps nodeProps: NodeProps
extraNodeDescription?: JSX.Element extraNodeDescription?: JSX.Element
handler: JSX.Element handler: JSX.Element
columnSchema?: () => Schema[] formSchema?: () => FormSchema,
resize?: { minWidth: number, minHeight: number } resize?: { minWidth: number, minHeight: number }
} }
@@ -122,7 +120,7 @@ const AmisNode: (props: AmisNodeProps) => JSX.Element = ({
nodeProps, nodeProps,
extraNodeDescription, extraNodeDescription,
handler, handler,
columnSchema, formSchema,
resize, resize,
}) => { }) => {
const {removeNode} = useFlowStore() const {removeNode} = useFlowStore()
@@ -136,6 +134,7 @@ const AmisNode: (props: AmisNodeProps) => JSX.Element = ({
const [editDrawerOpen, setEditDrawerOpen] = useState(false) const [editDrawerOpen, setEditDrawerOpen] = useState(false)
const [editDrawerForm, setEditDrawerForm] = useState<JSX.Element>(<></>) const [editDrawerForm, setEditDrawerForm] = useState<JSX.Element>(<></>)
const onOpenEditDrawerClick = useCallback(() => { const onOpenEditDrawerClick = useCallback(() => {
const schema = formSchema?.()
setEditDrawerForm( setEditDrawerForm(
amisRender( amisRender(
{ {
@@ -166,6 +165,7 @@ const AmisNode: (props: AmisNodeProps) => JSX.Element = ({
}, },
], ],
}, },
...(schema?.events ?? {})
}, },
body: [ body: [
{ {
@@ -183,7 +183,7 @@ const AmisNode: (props: AmisNodeProps) => JSX.Element = ({
{ {
type: 'divider', type: 'divider',
}, },
...(columnSchema?.() ?? []), ...(schema?.columns ?? []),
{ {
type: 'wrapper', type: 'wrapper',
size: 'none', size: 'none',

View File

@@ -1,10 +1,10 @@
import type {NodeProps} from '@xyflow/react' import type {NodeProps} from '@xyflow/react'
import {Tag} from 'antd' import {Tag} from 'antd'
import React, {useCallback, useMemo} from 'react' import React, {useCallback, useMemo} from 'react'
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, nodeClassName, NormalNodeHandler, outputsFormColumns} from './AmisNode.tsx' import AmisNode, {inputsFormColumns, nodeClassName, NormalNodeHandler, outputsFormColumns} from './AmisNode.tsx'
import type {FormSchema} from '../types.ts'
const languageMap: Record<string, string> = { const languageMap: Record<string, string> = {
'javascript': 'Javascript', 'javascript': 'Javascript',
@@ -15,38 +15,39 @@ const languageMap: Record<string, string> = {
const CodeNode = (props: NodeProps) => { const CodeNode = (props: NodeProps) => {
const {getNodes, getEdges} = useFlowStore() const {getNodes, getEdges} = useFlowStore()
const {getData, getDataById} = useDataStore() const {getData, getDataById} = useDataStore()
const {getInputSchema} = useContextStore()
const nodeData = getDataById(props.id) const nodeData = getDataById(props.id)
const columnsSchema = useCallback(() => [ const formSchema: () => FormSchema = useCallback(() => ({
...inputsFormColumns(props.id, getInputSchema(), getNodes(), getEdges(), getData()), columns: [
{ ...inputsFormColumns(props.id, getNodes(), getEdges(), getData()),
type: 'divider', {
}, 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',
}, },
}, {
{ type: 'select',
type: 'divider', name: 'type',
}, label: '代码类型',
...outputsFormColumns(true, true), required: true,
], [props.id]) 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(() => { const extraNodeDescription = useMemo(() => {
return nodeData?.type return nodeData?.type
@@ -62,7 +63,7 @@ const CodeNode = (props: NodeProps) => {
className={nodeClassName('code')} className={nodeClassName('code')}
nodeProps={props} nodeProps={props}
extraNodeDescription={extraNodeDescription} extraNodeDescription={extraNodeDescription}
columnSchema={columnsSchema} formSchema={formSchema}
handler={<NormalNodeHandler/>} 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

@@ -1,15 +1,14 @@
import type {NodeProps} from '@xyflow/react' import type {NodeProps} from '@xyflow/react'
import React, {useCallback, useEffect} from 'react' import React, {useCallback, useEffect} from 'react'
import {commonInfo} from '../../../util/amis.tsx' import {commonInfo} from '../../../util/amis.tsx'
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, nodeClassName, NormalNodeHandler, outputsFormColumns} from './AmisNode.tsx' import AmisNode, {inputsFormColumns, nodeClassName, NormalNodeHandler, outputsFormColumns} from './AmisNode.tsx'
import type {FormSchema} from '../types.ts'
const KnowledgeNode = (props: NodeProps) => { const KnowledgeNode = (props: NodeProps) => {
const {getNodes, getEdges} = useFlowStore() const {getNodes, getEdges} = useFlowStore()
const {getData, mergeDataById} = useDataStore() const {getData, mergeDataById} = useDataStore()
const {getInputSchema} = useContextStore()
useEffect(() => { useEffect(() => {
mergeDataById( mergeDataById(
@@ -24,64 +23,66 @@ const KnowledgeNode = (props: NodeProps) => {
) )
}, [props.id]) }, [props.id])
const columnsSchema = useCallback(() => [ const formSchema: () => FormSchema = useCallback(() => ({
...inputsFormColumns(props.id, getInputSchema(), getNodes(), getEdges(), getData()), columns: [
{ ...inputsFormColumns(props.id, getNodes(), getEdges(), getData()),
type: 'divider', {
}, type: 'divider',
{ },
type: 'select', {
name: 'knowledgeId', type: 'select',
label: '知识库', name: 'knowledgeId',
required: true, label: '知识库',
options: [], required: true,
source: { options: [],
method: 'get', source: {
url: `${commonInfo.baseAiUrl}/knowledge/list`, method: 'get',
// @ts-ignore url: `${commonInfo.baseAiUrl}/knowledge/list`,
adaptor: (payload, response, api, context) => { // @ts-ignore
return { adaptor: (payload, response, api, context) => {
...payload, return {
data: { ...payload,
items: payload.data.items.map((item: any) => ({value: item['id'], label: item['name']})), data: {
}, items: payload.data.items.map((item: any) => ({value: item['id'], label: item['name']})),
} },
}
},
}, },
}, },
}, {
{ type: 'input-text',
type: 'input-text', name: 'query',
name: 'query', label: '查询文本',
label: '查询文本', required: true,
required: true, },
}, {
{ type: 'input-range',
type: 'input-range', name: 'count',
name: 'count', label: '返回数量',
label: '返回数量', required: true,
required: true, value: 3,
value: 3, max: 10,
max: 10, },
}, {
{ type: 'input-range',
type: 'input-range', name: 'score',
name: 'score', label: '匹配阀值',
label: '匹配阀值', required: true,
required: true, value: 0.6,
value: 0.6, max: 1,
max: 1, step: 0.05,
step: 0.05, },
}, {
{ type: 'divider',
type: 'divider', },
}, ...outputsFormColumns(false, true),
...outputsFormColumns(false, true), ]
], [props.id]) }), [props.id])
return ( return (
<AmisNode <AmisNode
className={nodeClassName('knowledge')} className={nodeClassName('knowledge')}
nodeProps={props} nodeProps={props}
columnSchema={columnsSchema} formSchema={formSchema}
handler={<NormalNodeHandler/>} handler={<NormalNodeHandler/>}
/> />
) )

View File

@@ -1,10 +1,10 @@
import type {NodeProps} from '@xyflow/react' import type {NodeProps} from '@xyflow/react'
import {Tag} from 'antd' import {Tag} from 'antd'
import React, {useCallback, useEffect, useMemo} from 'react' import React, {useCallback, useEffect, useMemo} from 'react'
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, nodeClassName, NormalNodeHandler, outputsFormColumns} from './AmisNode.tsx' import AmisNode, {inputsFormColumns, nodeClassName, NormalNodeHandler, outputsFormColumns} from './AmisNode.tsx'
import type {FormSchema} from '../types.ts'
const modelMap: Record<string, string> = { const modelMap: Record<string, string> = {
qwen3: 'Qwen3', qwen3: 'Qwen3',
@@ -14,7 +14,6 @@ const modelMap: Record<string, string> = {
const LlmNode = (props: NodeProps) => { const LlmNode = (props: NodeProps) => {
const {getNodes, getEdges} = useFlowStore() const {getNodes, getEdges} = useFlowStore()
const {getData, mergeDataById, getDataById} = useDataStore() const {getData, mergeDataById, getDataById} = useDataStore()
const {getInputSchema} = useContextStore()
const nodeData = getDataById(props.id) const nodeData = getDataById(props.id)
@@ -31,30 +30,32 @@ const LlmNode = (props: NodeProps) => {
) )
}, [props.id]) }, [props.id])
const columnsSchema = useCallback(() => [ const formSchema: () => FormSchema = useCallback(() => ({
...inputsFormColumns(props.id, getInputSchema(), getNodes(), getEdges(), getData()), columns: [
{ ...inputsFormColumns(props.id, getNodes(), getEdges(), getData()),
type: 'divider', {
}, type: 'divider',
{ },
type: 'select', {
name: 'model', type: 'select',
label: '大模型', name: 'model',
required: true, label: '大模型',
selectFirst: true, required: true,
options: Object.keys(modelMap).map(key => ({label: modelMap[key], value: key})), selectFirst: true,
}, options: Object.keys(modelMap).map(key => ({label: modelMap[key], value: key})),
{ },
type: 'textarea', {
name: 'systemPrompt', type: 'textarea',
label: '系统提示词', name: 'systemPrompt',
required: true, label: '系统提示词',
}, required: true,
{ },
type: 'divider', {
}, type: 'divider',
...outputsFormColumns(false, true), },
], [props.id]) ...outputsFormColumns(false, true),
]
}), [props.id])
const extraNodeDescription = useMemo(() => { const extraNodeDescription = useMemo(() => {
return nodeData?.model return nodeData?.model
@@ -70,7 +71,7 @@ const LlmNode = (props: NodeProps) => {
className={nodeClassName('llm')} className={nodeClassName('llm')}
nodeProps={props} nodeProps={props}
extraNodeDescription={extraNodeDescription} extraNodeDescription={extraNodeDescription}
columnSchema={columnsSchema} formSchema={formSchema}
handler={<NormalNodeHandler/>} handler={<NormalNodeHandler/>}
/> />
) )

View File

@@ -3,16 +3,14 @@ import {classnames} from 'amis'
import React, {useCallback, useEffect, useMemo} from 'react' import React, {useCallback, useEffect, useMemo} from 'react'
import AddNodeButton from '../component/AddNodeButton.tsx' import AddNodeButton from '../component/AddNodeButton.tsx'
import {generateAllIncomerOutputVariablesFormOptions} from '../Helper.tsx' import {generateAllIncomerOutputVariablesFormOptions} from '../Helper.tsx'
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 {flowBackgroundColor, flowDotColor} from '../types.ts' import {flowBackgroundColor, flowDotColor, type FormSchema} from '../types.ts'
import AmisNode, {nodeClassName, NormalNodeHandler, outputsFormColumns} from './AmisNode.tsx' import AmisNode, {nodeClassName, NormalNodeHandler, outputsFormColumns} from './AmisNode.tsx'
const LoopNode = (props: NodeProps) => { const LoopNode = (props: NodeProps) => {
const {getNodes, getEdges} = useFlowStore() const {getNodes, getEdges} = useFlowStore()
const {getData, mergeDataById} = useDataStore() const {getData, mergeDataById} = useDataStore()
const {getInputSchema} = useContextStore()
useEffect(() => { useEffect(() => {
mergeDataById( mergeDataById(
@@ -31,87 +29,87 @@ const LoopNode = (props: NodeProps) => {
) )
}, [props.id]) }, [props.id])
const columnsSchema = useCallback(() => [ const formSchema: () => FormSchema = useCallback(() => ({
{ columns: [
type: 'switch', {
name: 'failFast', type: 'switch',
label: '快速失败', name: 'failFast',
required: true, label: '快速失败',
description: '执行过程中一旦出现错误,及时中断循环任务的执行', required: true,
}, description: '执行过程中一旦出现错误,及时中断循环任务的执行',
{ },
disabled: true, {
type: 'switch', disabled: true,
name: 'parallel', type: 'switch',
label: '并行执行', name: 'parallel',
required: true, label: '并行执行',
}, required: true,
{ },
type: 'select', {
name: 'type', type: 'select',
label: '循环模式', name: 'type',
required: true, label: '循环模式',
options: [ required: true,
{ options: [
label: '次数循环', {
value: 'for', label: '次数循环',
}, value: 'for',
{ },
label: '次数循环 (引用变量)', {
value: 'for-variable', label: '次数循环 (引用变量)',
}, value: 'for-variable',
{ },
label: '对象循环', {
value: 'for-object', label: '对象循环',
}, value: 'for-object',
], },
}, ],
{ },
visibleOn: '${type === \'for\'}', {
type: 'input-number', visibleOn: '${type === \'for\'}',
name: 'count', type: 'input-number',
label: '循环次数', name: 'count',
required: true, label: '循环次数',
min: 1, required: true,
precision: 0, min: 1,
}, precision: 0,
{ },
visibleOn: '${type === \'for-variable\'}', {
type: 'select', visibleOn: '${type === \'for-variable\'}',
name: 'countVariable', type: 'select',
label: '循环次数', name: 'countVariable',
required: true, label: '循环次数',
selectMode: 'group', required: true,
options: generateAllIncomerOutputVariablesFormOptions( selectMode: 'group',
props.id, options: generateAllIncomerOutputVariablesFormOptions(
getInputSchema(), props.id,
getNodes(), getNodes(),
getEdges(), getEdges(),
getData(), getData(),
['number'], ['number'],
), ),
}, },
{ {
visibleOn: '${type === \'for-object\'}', visibleOn: '${type === \'for-object\'}',
type: 'select', type: 'select',
name: 'countObject', name: 'countObject',
label: '循环对象', label: '循环对象',
required: true, required: true,
selectMode: 'group', selectMode: 'group',
options: generateAllIncomerOutputVariablesFormOptions( options: generateAllIncomerOutputVariablesFormOptions(
props.id, props.id,
getInputSchema(), getNodes(),
getNodes(), getEdges(),
getEdges(), getData(),
getData(), ['array-text', 'array-object'],
['array-text', 'array-object'], ),
), },
}, {
{ type: 'divider',
type: 'divider', },
}, ...outputsFormColumns(false, true),
...outputsFormColumns(false, true), ]
], [props.id]) }), [props.id])
const extraNodeDescription = useMemo(() => { const extraNodeDescription = useMemo(() => {
return ( return (
@@ -142,7 +140,7 @@ const LoopNode = (props: NodeProps) => {
}} }}
nodeProps={props} nodeProps={props}
extraNodeDescription={extraNodeDescription} extraNodeDescription={extraNodeDescription}
columnSchema={columnsSchema} formSchema={formSchema}
handler={<NormalNodeHandler/>} handler={<NormalNodeHandler/>}
resize={{ resize={{
minWidth: 350, minWidth: 350,

View File

@@ -1,39 +1,23 @@
import type {NodeProps} from '@xyflow/react' import type {NodeProps} from '@xyflow/react'
import React, {useCallback} from 'react' import React, {useCallback} from 'react'
import {generateAllIncomerOutputVariablesFormOptions} from '../Helper.tsx'
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, nodeClassName} from './AmisNode.tsx' import type {FormSchema} from '../types.ts'
import AmisNode, {EndNodeHandler, inputsFormColumns, nodeClassName} from './AmisNode.tsx'
const OutputNode = (props: NodeProps) => { const OutputNode = (props: NodeProps) => {
const {getNodes, getEdges} = useFlowStore() const {getNodes, getEdges} = useFlowStore()
const {getData} = useDataStore() const {getData} = useDataStore()
const {getInputSchema} = useContextStore()
const columnsSchema = useCallback( const formSchema: () => FormSchema = useCallback(() => ({
() => [ columns: inputsFormColumns(props.id, getNodes(), getEdges(), getData()),
{ }), [props.id])
type: 'select',
name: 'output',
label: '输出变量',
required: true,
selectMode: 'group',
options: generateAllIncomerOutputVariablesFormOptions(
props.id,
getInputSchema(),
getNodes(),
getEdges(),
getData(),
),
},
], [props.id])
return ( return (
<AmisNode <AmisNode
className={nodeClassName('output')} className={nodeClassName('output')}
nodeProps={props} nodeProps={props}
columnSchema={columnsSchema} formSchema={formSchema}
handler={<EndNodeHandler/>} handler={<EndNodeHandler/>}
/> />
) )

View File

@@ -4,46 +4,46 @@ import {Tag} from 'antd'
import {contain, isEqual} from 'licia' import {contain, isEqual} from 'licia'
import React, {useCallback, useMemo} from 'react' import React, {useCallback, useMemo} from 'react'
import {generateAllIncomerOutputVariablesConditions} from '../Helper.tsx' import {generateAllIncomerOutputVariablesConditions} from '../Helper.tsx'
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, {nodeClassName} from './AmisNode.tsx' import AmisNode, {nodeClassName} from './AmisNode.tsx'
import type {FormSchema} from '../types.ts'
const SwitchNode = (props: NodeProps) => { const SwitchNode = (props: NodeProps) => {
const {getNodes, getEdges, removeEdges} = useFlowStore() const {getNodes, getEdges, removeEdges} = useFlowStore()
const {getData, getDataById} = useDataStore() const {getData, getDataById} = useDataStore()
const {getInputSchema} = useContextStore()
const nodeData = getDataById(props.id) const nodeData = getDataById(props.id)
// @ts-ignore // @ts-ignore
const conditions: ConditionValue[] = nodeData?.conditions?.map(c => c.condition) ?? [] const conditions: ConditionValue[] = nodeData?.conditions?.map(c => c.condition) ?? []
const columnsSchema = useCallback(() => [ const formSchema: () => FormSchema = useCallback(() => ({
{ columns: [
type: 'combo', {
name: 'conditions', type: 'combo',
label: '分支', name: 'conditions',
multiple: true, label: '分支',
required: true, multiple: true,
items: [ required: true,
{ items: [
type: 'condition-builder', {
name: 'condition', type: 'condition-builder',
label: '条件', name: 'condition',
required: true, label: '条件',
builderMode: 'simple', required: true,
showANDOR: true, builderMode: 'simple',
fields: generateAllIncomerOutputVariablesConditions( showANDOR: true,
props.id, fields: generateAllIncomerOutputVariablesConditions(
getInputSchema(), props.id,
getNodes(), getNodes(),
getEdges(), getEdges(),
getData(), getData(),
), ),
}, },
], ],
}, },
], [props.id]) ]
}), [props.id])
const extraNodeDescription = useMemo(() => { const extraNodeDescription = useMemo(() => {
return ( return (
@@ -88,7 +88,7 @@ const SwitchNode = (props: NodeProps) => {
className={nodeClassName('switch')} className={nodeClassName('switch')}
nodeProps={props} nodeProps={props}
extraNodeDescription={extraNodeDescription} extraNodeDescription={extraNodeDescription}
columnSchema={columnsSchema} formSchema={formSchema}
handler={handler} handler={handler}
/> />
) )

View File

@@ -1,10 +1,10 @@
import type {NodeProps} from '@xyflow/react' import type {NodeProps} from '@xyflow/react'
import {Tag} from 'antd' import {Tag} from 'antd'
import React, {useCallback, useEffect, useMemo} from 'react' import React, {useCallback, useEffect, useMemo} from 'react'
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, nodeClassName, NormalNodeHandler, outputsFormColumns} from './AmisNode.tsx' import AmisNode, {inputsFormColumns, nodeClassName, NormalNodeHandler, outputsFormColumns} from './AmisNode.tsx'
import type {FormSchema} from '../types.ts'
const typeMap: Record<string, string> = { const typeMap: Record<string, string> = {
default: '默认', default: '默认',
@@ -16,7 +16,6 @@ const typeMap: Record<string, string> = {
const TemplateNode = (props: NodeProps) => { const TemplateNode = (props: NodeProps) => {
const {getNodes, getEdges} = useFlowStore() const {getNodes, getEdges} = useFlowStore()
const {getData, getDataById, mergeDataById} = useDataStore() const {getData, getDataById, mergeDataById} = useDataStore()
const {getInputSchema} = useContextStore()
const nodeData = getDataById(props.id) const nodeData = getDataById(props.id)
@@ -33,9 +32,9 @@ const TemplateNode = (props: NodeProps) => {
) )
}, [props.id]) }, [props.id])
const columnsSchema = useCallback( const formSchema: () => FormSchema = useCallback(() => ({
() => [ columns: [
...inputsFormColumns(props.id, getInputSchema(), getNodes(), getEdges(), getData()), ...inputsFormColumns(props.id, getNodes(), getEdges(), getData()),
{ {
type: 'divider', type: 'divider',
}, },
@@ -69,7 +68,8 @@ const TemplateNode = (props: NodeProps) => {
}, },
}, },
...outputsFormColumns(false, true), ...outputsFormColumns(false, true),
], [props.id]) ]
}), [props.id])
const extraNodeDescription = useMemo(() => { const extraNodeDescription = useMemo(() => {
return nodeData?.type return nodeData?.type
@@ -85,7 +85,7 @@ const TemplateNode = (props: NodeProps) => {
className={nodeClassName('template')} className={nodeClassName('template')}
nodeProps={props} nodeProps={props}
extraNodeDescription={extraNodeDescription} extraNodeDescription={extraNodeDescription}
columnSchema={columnsSchema} formSchema={formSchema}
handler={<NormalNodeHandler/>} handler={<NormalNodeHandler/>}
/> />
) )

View File

@@ -1,11 +0,0 @@
import {create} from 'zustand/react'
export const useContextStore = create<{
inputSchema: Record<string, Record<string, any>>,
getInputSchema: () => Record<string, Record<string, any>>,
setInputSchema: (inputSchema: Record<string, Record<string, any>>) => void,
}>((set, get) => ({
inputSchema: {},
getInputSchema: () => get().inputSchema,
setInputSchema: (inputSchema: Record<string, Record<string, any>>) => set({inputSchema}),
}))

View File

@@ -1,5 +1,6 @@
import type {Edge, Node} from '@xyflow/react' import type {Edge, Node} from '@xyflow/react'
import type {JSX} from 'react' import type {JSX} from 'react'
import type {ListenerAction, Schema} from 'amis'
export const flowBackgroundColor = '#fafafa' export const flowBackgroundColor = '#fafafa'
export const flowDotColor = '#dedede' export const flowDotColor = '#dedede'
@@ -19,12 +20,12 @@ export type NodeError = {
message?: string, 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, nodes: Node[], edges: Edge[], data: any) => NodeError
export type SaveNodeChecker = (id: string, parentId: string | undefined, nodes: Node[], edges: Edge[], data: any) => NodeError
export type GraphData = { nodes: Node[], edges: Edge[], data: any } export type GraphData = { nodes: Node[], edges: Edge[], data: any }
export type FlowEditorProps = { export type FlowEditorProps = {
inputSchema: Record<string, Record<string, any>>,
graphData: GraphData, graphData: GraphData,
onGraphDataChange: (graphData: GraphData) => void, onGraphDataChange: (graphData: GraphData) => void,
} }
@@ -47,7 +48,10 @@ export type NodeDefine = {
icon: JSX.Element, icon: JSX.Element,
description: string, description: string,
component: any, component: any,
checkers: NodeChecker[], checkers: {
add: AddNodeChecker[],
save: SaveNodeChecker[],
},
} }
export type OutputVariable = { export type OutputVariable = {
@@ -56,3 +60,8 @@ export type OutputVariable = {
type: OutputVariableType, type: OutputVariableType,
variable: string, variable: string,
} }
export type FormSchema = {
events?: Record<string, { actions: ListenerAction[] }>
columns: Schema[]
}

File diff suppressed because one or more lines are too long

View File

@@ -87,6 +87,7 @@ const FlowTask: React.FC = () => {
width: 200, width: 200,
buttons: [ buttons: [
{ {
visibleOn: 'hasInput',
type: 'action', type: 'action',
label: '查看', label: '查看',
level: 'link', level: 'link',
@@ -107,17 +108,18 @@ const FlowTask: React.FC = () => {
return { return {
...payload, ...payload,
data: { data: {
...generateInputForm(payload.data, undefined, false, true), ...generateInputForm(payload.data ?? {}, undefined, false, true),
id: 'db8a4d10-0c47-4e27-b1a4-d0f2e1c15992', id: 'db8a4d10-0c47-4e27-b1a4-d0f2e1c15992',
initApi: { initApi: {
method: 'get', method: 'get',
url: `${commonInfo.baseAiUrl}/flow_task/input_data/\${id}`, url: `${commonInfo.baseAiUrl}/flow_task/input_data/\${id}`,
// @ts-ignore // @ts-ignore
adaptor: (payload, response, api, context) => { adaptor: (payload, response, api, context) => {
console.log(payload)
return { return {
...payload, ...payload,
data: { data: {
inputData: payload.data, inputData: payload.data ?? {},
}, },
} }
}, },

View File

@@ -159,7 +159,7 @@ const FlowTaskAdd: React.FC = () => {
input: '${inputData|default:undefined}', input: '${inputData|default:undefined}',
} }
}, },
...generateInputForm(payload.data, undefined, false), ...generateInputForm(payload.data ?? {}, undefined, false),
}, },
} }
}, },

View File

@@ -1,5 +1,4 @@
import type {Schema} from 'amis' import type {Schema} from 'amis'
import type {OutputVariableType} from '../../../components/flow/types.ts'
import {commonInfo, formInputFileStaticColumns} from '../../../util/amis.tsx' import {commonInfo, formInputFileStaticColumns} from '../../../util/amis.tsx'
export const typeMap: Record<string, string> = { export const typeMap: Record<string, string> = {
@@ -9,13 +8,6 @@ export const typeMap: Record<string, string> = {
files: '文件', files: '文件',
} }
export const originTypeMap: Record<string, OutputVariableType> = {
text: 'text',
textarea: 'text',
number: 'number',
files: 'array-text',
}
export type InputField = { export type InputField = {
type: string type: string
label: string label: string

View File

@@ -1,9 +1,8 @@
import {isEmpty, isEqual} from 'licia' import {isEqual} from 'licia'
import React from 'react' import React from 'react'
import {useNavigate, 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'
const TemplateEditDiv = styled.div` const TemplateEditDiv = styled.div`
.antd-EditorControl { .antd-EditorControl {
@@ -39,31 +38,6 @@ const FlowTaskTemplateEdit: React.FC = () => {
wrapWithPanel: false, wrapWithPanel: false,
...horizontalFormOptions(), ...horizontalFormOptions(),
onEvent: { onEvent: {
change: {
actions: [
{
actionType: 'validate',
},
{
actionType: 'custom',
// @ts-ignore
script: (context, doAction, event) => {
let data = event?.data ?? {}
let inputSchema = data.inputSchema ?? []
if (!isEmpty(inputSchema) && isEmpty(data?.validateResult?.error ?? undefined)) {
doAction({
actionType: 'setValue',
args: {
value: {
inputPreview: generateInputForm(inputSchema, '入参表单预览'),
},
},
})
}
},
},
],
},
submitSucc: { submitSucc: {
actions: [ actions: [
{ {
@@ -99,71 +73,6 @@ const FlowTaskTemplateEdit: React.FC = () => {
maxLength: 500, maxLength: 500,
showCounter: true, showCounter: true,
}, },
{
type: 'group',
body: [
{
type: 'wrapper',
size: 'none',
body: [
{
type: 'input-kvs',
name: 'inputSchema',
label: '输入变量',
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: 'amis',
name: 'inputPreview',
},
],
},
{ {
type: 'button-toolbar', type: 'button-toolbar',
buttons: [ buttons: [

View File

@@ -13,7 +13,6 @@ const FlowTaskTemplateFlowEditDiv = styled.div`
const FlowTaskTemplateFlowEdit: React.FC = () => { const FlowTaskTemplateFlowEdit: React.FC = () => {
const navigate = useNavigate() const navigate = useNavigate()
const {template_id} = useParams() const {template_id} = useParams()
const [inputSchema, setInputSchema] = useState<Record<string, Record<string, any>>>({})
const [graphData, setGraphData] = useState<GraphData>({nodes: [], edges: [], data: {}}) const [graphData, setGraphData] = useState<GraphData>({nodes: [], edges: [], data: {}})
useMount(async () => { useMount(async () => {
@@ -23,14 +22,12 @@ const FlowTaskTemplateFlowEdit: React.FC = () => {
headers: commonInfo.authorizationHeaders, headers: commonInfo.authorizationHeaders,
}, },
) )
setInputSchema(data?.data?.inputSchema)
setGraphData(data?.data?.flowGraph) setGraphData(data?.data?.flowGraph)
}) })
return ( return (
<FlowTaskTemplateFlowEditDiv className="h-full w-full"> <FlowTaskTemplateFlowEditDiv className="h-full w-full">
<FlowEditor <FlowEditor
inputSchema={inputSchema}
graphData={graphData} graphData={graphData}
onGraphDataChange={async data => { onGraphDataChange={async data => {
await axios.post( await axios.post(