feat(ai-web): 完成代码节点的执行

This commit is contained in:
v-zhangjc9
2025-07-18 15:48:35 +08:00
parent 4cfa110f2f
commit 77a09472aa
7 changed files with 146 additions and 84 deletions

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();

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

@@ -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) { return switch (operator) {
case "equal" -> ObjectUtil.equals(left, right); case "equal" -> StrUtil.equals(source, target);
case "not_equal" -> ObjectUtil.notEqual(left, right); case "not_equal" -> !StrUtil.equals(source, target);
case "is_empty" -> ObjectUtil.isEmpty(left); case "is_empty" -> StrUtil.isBlank(source);
case "is_not_empty" -> ObjectUtil.isNotEmpty(left); case "is_not_empty" -> StrUtil.isNotBlank(source);
case "like" -> StrUtil.contains((String) left, (String) right); case "like" -> StrUtil.contains(source, target);
case "not_like" -> !StrUtil.contains((String) left, (String) right); case "not_like" -> !StrUtil.contains(source, target);
case "starts_with" -> StrUtil.startWith((String) left, (String) right); case "starts_with" -> StrUtil.startWith(source, target);
case "ends_with" -> StrUtil.endWith((String) left, (String) right); 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; default -> false;
}; };
} }
return 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

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