diff --git a/Dockerfile b/Dockerfile index dcd49e1..51131f2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,6 +11,9 @@ ENV EASYFLOW_LOG_FILE=/app/logs/app.log WORKDIR /app RUN useradd --system --create-home easyflow && \ + apt-get update && \ + apt-get install -y --no-install-recommends python3 && \ + rm -rf /var/lib/apt/lists/* && \ mkdir -p /app/logs && \ chown -R easyflow:easyflow /app diff --git a/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/WorkflowController.java b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/WorkflowController.java index 46fcd59..e489c21 100644 --- a/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/WorkflowController.java +++ b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/WorkflowController.java @@ -12,6 +12,7 @@ import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import tech.easyflow.ai.entity.Workflow; +import tech.easyflow.ai.easyagentsflow.service.CodeEngineCapabilityService; import tech.easyflow.ai.service.BotWorkflowService; import tech.easyflow.ai.service.ModelService; import tech.easyflow.ai.service.WorkflowService; @@ -55,6 +56,8 @@ public class WorkflowController extends BaseCurdController supportedCodeEngines() { + return Result.ok(codeEngineCapabilityService.listSupportedCodeEngines()); + } + @Override public Result detail(String id) { Workflow workflow = service.getDetail(id); @@ -210,4 +219,4 @@ public class WorkflowController extends BaseCurdControllercom.easyagents easy-agents-mcp + + + junit + junit + ${junit.version} + test + diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/config/CodeEngineProps.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/config/CodeEngineProps.java new file mode 100644 index 0000000..986f448 --- /dev/null +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/config/CodeEngineProps.java @@ -0,0 +1,55 @@ +package tech.easyflow.ai.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConfigurationProperties(prefix = "node.code-engine.python") +public class CodeEngineProps { + + private boolean enabled = true; + private String command = "python3"; + private long timeoutMs = 5000; + private int maxOutputBytes = 262144; + private String workingDir = System.getProperty("java.io.tmpdir"); + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public String getCommand() { + return command; + } + + public void setCommand(String command) { + this.command = command; + } + + public long getTimeoutMs() { + return timeoutMs; + } + + public void setTimeoutMs(long timeoutMs) { + this.timeoutMs = timeoutMs; + } + + public int getMaxOutputBytes() { + return maxOutputBytes; + } + + public void setMaxOutputBytes(int maxOutputBytes) { + this.maxOutputBytes = maxOutputBytes; + } + + public String getWorkingDir() { + return workingDir; + } + + public void setWorkingDir(String workingDir) { + this.workingDir = workingDir; + } +} diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/easyagentsflow/code/PythonRuntimeEngine.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/easyagentsflow/code/PythonRuntimeEngine.java new file mode 100644 index 0000000..1d14423 --- /dev/null +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/easyagentsflow/code/PythonRuntimeEngine.java @@ -0,0 +1,309 @@ +package tech.easyflow.ai.easyagentsflow.code; + +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import com.easyagents.flow.core.chain.Chain; +import com.easyagents.flow.core.code.CodeRuntimeEngine; +import com.easyagents.flow.core.node.CodeNode; +import com.easyagents.flow.core.util.StringUtil; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +public class PythonRuntimeEngine implements CodeRuntimeEngine { + + private static final String PYTHON_HELPER_SCRIPT = String.join("\n", + "import contextlib", + "import io", + "import json", + "import sys", + "import traceback", + "", + "def _read_payload():", + " raw = sys.stdin.read()", + " if not raw:", + " return {}", + " return json.loads(raw)", + "", + "def _main():", + " payload = _read_payload()", + " code = payload.get('code') or ''", + " context = payload.get('context')", + " if not isinstance(context, dict):", + " context = {}", + "", + " local_ctx = dict(context)", + " local_ctx['_result'] = {}", + "", + " captured_stdout = io.StringIO()", + " captured_stderr = io.StringIO()", + "", + " try:", + " with contextlib.redirect_stdout(captured_stdout), contextlib.redirect_stderr(captured_stderr):", + " exec(code, {'__builtins__': __builtins__}, local_ctx)", + " except Exception:", + " sys.stderr.write(traceback.format_exc())", + " sys.exit(2)", + "", + " result = local_ctx.get('_result')", + " if not isinstance(result, dict):", + " sys.stderr.write('Python 脚本执行结束后,_result 必须是 dict\\n')", + " sys.exit(3)", + "", + " if captured_stdout.getvalue():", + " sys.stderr.write(captured_stdout.getvalue())", + " if captured_stderr.getvalue():", + " sys.stderr.write(captured_stderr.getvalue())", + "", + " try:", + " sys.stdout.write(json.dumps(result, ensure_ascii=False))", + " except Exception:", + " sys.stderr.write(traceback.format_exc())", + " sys.exit(4)", + "", + "if __name__ == '__main__':", + " _main()" + ); + + private final String command; + private final long timeoutMs; + private final int maxOutputBytes; + private final String workingDir; + + public PythonRuntimeEngine(String command, long timeoutMs, int maxOutputBytes, String workingDir) { + this.command = command; + this.timeoutMs = timeoutMs; + this.maxOutputBytes = maxOutputBytes; + this.workingDir = workingDir; + } + + public static ProbeResult probe(String command, long timeoutMs) { + if (StringUtil.noText(command)) { + return new ProbeResult(false, "未配置 python 命令"); + } + + Process process = null; + StreamCollector outputCollector = null; + Thread outputThread = null; + try { + process = new ProcessBuilder(command, "--version") + .redirectErrorStream(true) + .start(); + + outputCollector = new StreamCollector(process.getInputStream(), 4096); + outputThread = new Thread(outputCollector, "python-probe-output"); + outputThread.setDaemon(true); + outputThread.start(); + + boolean finished = process.waitFor(Math.max(timeoutMs, 1000), TimeUnit.MILLISECONDS); + waitCollector(outputThread); + + String output = outputCollector == null ? "" : outputCollector.getText().trim(); + if (!finished) { + process.destroyForcibly(); + return new ProbeResult(false, "执行 python --version 超时"); + } + + if (process.exitValue() != 0) { + return new ProbeResult(false, StringUtil.hasText(output) ? output : "python 命令返回非 0"); + } + + if (outputCollector != null && outputCollector.isTruncated()) { + return new ProbeResult(false, "python --version 输出过长"); + } + + return new ProbeResult(true, StringUtil.hasText(output) ? output : "Python 可用"); + } catch (Exception e) { + return new ProbeResult(false, e.getMessage()); + } finally { + if (process != null) { + process.destroy(); + } + } + } + + @Override + public Map execute(String code, CodeNode node, Chain chain) { + if (StringUtil.noText(command)) { + throw new RuntimeException("python 执行命令为空,请检查配置 node.code-engine.python.command"); + } + + Process process = null; + StreamCollector stdoutCollector = null; + StreamCollector stderrCollector = null; + Thread stdoutThread = null; + Thread stderrThread = null; + + try { + ProcessBuilder builder = new ProcessBuilder(command, "-c", PYTHON_HELPER_SCRIPT); + if (StringUtil.hasText(workingDir)) { + builder.directory(new File(workingDir)); + } + process = builder.start(); + + stdoutCollector = new StreamCollector(process.getInputStream(), maxOutputBytes); + stderrCollector = new StreamCollector(process.getErrorStream(), maxOutputBytes); + stdoutThread = new Thread(stdoutCollector, "python-runtime-stdout"); + stderrThread = new Thread(stderrCollector, "python-runtime-stderr"); + stdoutThread.setDaemon(true); + stderrThread.setDaemon(true); + stdoutThread.start(); + stderrThread.start(); + + Map context = buildContext(chain, node); + Map payload = new HashMap<>(); + payload.put("code", code); + payload.put("context", context); + + String payloadText = JSON.toJSONString(payload); + try (OutputStream stdin = process.getOutputStream()) { + stdin.write(payloadText.getBytes(StandardCharsets.UTF_8)); + stdin.flush(); + } + + boolean finished = process.waitFor(Math.max(timeoutMs, 1), TimeUnit.MILLISECONDS); + if (!finished) { + process.destroyForcibly(); + process.waitFor(1, TimeUnit.SECONDS); + } + + waitCollector(stdoutThread); + waitCollector(stderrThread); + + String stdout = stdoutCollector == null ? "" : stdoutCollector.getText().trim(); + String stderr = stderrCollector == null ? "" : stderrCollector.getText().trim(); + + if (stdoutCollector != null && stdoutCollector.isTruncated()) { + throw new RuntimeException("Python 执行输出超出限制(" + maxOutputBytes + " bytes)"); + } + if (stderrCollector != null && stderrCollector.isTruncated()) { + stderr = stderr + "\n...(stderr too large, truncated)"; + } + + if (!finished) { + throw new RuntimeException("Python 脚本执行超时(" + timeoutMs + "ms)" + (StringUtil.hasText(stderr) ? ",stderr: " + stderr : "")); + } + + int exitCode = process.exitValue(); + if (exitCode != 0) { + String errorMsg = StringUtil.hasText(stderr) ? stderr : (StringUtil.hasText(stdout) ? stdout : "无错误输出"); + throw new RuntimeException("Python 脚本执行失败(exit=" + exitCode + "): " + errorMsg); + } + + if (StringUtil.noText(stdout)) { + throw new RuntimeException("Python 脚本执行失败:stdout 为空,未返回 _result JSON"); + } + + Object parsed = JSON.parse(stdout); + if (!(parsed instanceof JSONObject)) { + throw new RuntimeException("Python 脚本执行失败:输出不是 JSON Object"); + } + + return ((JSONObject) parsed); + } catch (Exception e) { + throw new RuntimeException("Python 脚本执行失败: " + e.getMessage(), e); + } finally { + if (process != null) { + process.destroy(); + } + } + } + + private Map buildContext(Chain chain, CodeNode node) { + Map context = new HashMap<>(); + + Map all = chain.getState().getMemory(); + all.forEach((key, value) -> { + if (!key.contains(".")) { + context.put(key, value); + } + }); + + Map parameterValues = chain.getState().resolveParameters(node); + if (parameterValues != null && !parameterValues.isEmpty()) { + context.putAll(parameterValues); + } + + context.put("_env", chain.getState().getEnvMap()); + return context; + } + + private static void waitCollector(Thread collectorThread) { + if (collectorThread == null) { + return; + } + try { + collectorThread.join(2000L); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + public static class ProbeResult { + private final boolean available; + private final String message; + + public ProbeResult(boolean available, String message) { + this.available = available; + this.message = message; + } + + public boolean isAvailable() { + return available; + } + + public String getMessage() { + return message; + } + } + + private static class StreamCollector implements Runnable { + private final InputStream inputStream; + private final int maxBytes; + private final ByteArrayOutputStream output = new ByteArrayOutputStream(); + private volatile boolean truncated; + + private StreamCollector(InputStream inputStream, int maxBytes) { + this.inputStream = inputStream; + this.maxBytes = maxBytes; + } + + @Override + public void run() { + byte[] buffer = new byte[4096]; + int n; + int written = 0; + try { + while ((n = inputStream.read(buffer)) != -1) { + if (written < maxBytes) { + int canWrite = Math.min(maxBytes - written, n); + output.write(buffer, 0, canWrite); + written += canWrite; + if (canWrite < n) { + truncated = true; + } + } else { + truncated = true; + } + } + } catch (IOException ignored) { + // 忽略流读异常,最终由进程退出码和输出判断 + } + } + + public boolean isTruncated() { + return truncated; + } + + public String getText() { + return output.toString(StandardCharsets.UTF_8); + } + } +} diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/easyagentsflow/code/UnavailableCodeRuntimeEngine.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/easyagentsflow/code/UnavailableCodeRuntimeEngine.java new file mode 100644 index 0000000..b2a7d3d --- /dev/null +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/easyagentsflow/code/UnavailableCodeRuntimeEngine.java @@ -0,0 +1,22 @@ +package tech.easyflow.ai.easyagentsflow.code; + +import com.easyagents.flow.core.chain.Chain; +import com.easyagents.flow.core.code.CodeRuntimeEngine; +import com.easyagents.flow.core.node.CodeNode; + +import java.util.Map; + +public class UnavailableCodeRuntimeEngine implements CodeRuntimeEngine { + + private final String reason; + + public UnavailableCodeRuntimeEngine(String reason) { + this.reason = reason; + } + + @Override + public Map execute(String code, CodeNode node, Chain chain) { + String engine = node == null ? "unknown" : node.getEngine(); + throw new RuntimeException("代码执行引擎不可用: " + engine + ",原因: " + reason); + } +} diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/easyagentsflow/service/CodeEngineCapabilityService.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/easyagentsflow/service/CodeEngineCapabilityService.java new file mode 100644 index 0000000..0b5ce1a --- /dev/null +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/easyagentsflow/service/CodeEngineCapabilityService.java @@ -0,0 +1,36 @@ +package tech.easyflow.ai.easyagentsflow.service; + +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +@Component +public class CodeEngineCapabilityService { + + private volatile boolean pythonAvailable = false; + private volatile String pythonReason = "Python 引擎未初始化"; + + public void setPythonCapability(boolean available, String reason) { + this.pythonAvailable = available; + this.pythonReason = reason; + } + + public List> listSupportedCodeEngines() { + List> result = new ArrayList<>(); + result.add(toItem("js", "JavaScript", true, "")); + result.add(toItem("python", "Python", pythonAvailable, pythonAvailable ? "" : pythonReason)); + return result; + } + + private Map toItem(String value, String label, boolean available, String reason) { + Map item = new LinkedHashMap<>(); + item.put("value", value); + item.put("label", label); + item.put("available", available); + item.put("reason", reason); + return item; + } +} diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/easyagentsflow/service/TinyFlowConfigService.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/easyagentsflow/service/TinyFlowConfigService.java index b5c6f5b..840ae6c 100644 --- a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/easyagentsflow/service/TinyFlowConfigService.java +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/easyagentsflow/service/TinyFlowConfigService.java @@ -1,5 +1,6 @@ package tech.easyflow.ai.easyagentsflow.service; +import com.easyagents.flow.core.code.CodeRuntimeEngineManager; import com.easyagents.flow.core.filestoreage.FileStorageManager; import com.easyagents.flow.core.filestoreage.FileStorageProvider; import com.easyagents.flow.core.knowledge.KnowledgeManager; @@ -11,15 +12,25 @@ import com.easyagents.flow.core.searchengine.SearchEngine; import com.easyagents.flow.core.searchengine.SearchEngineManager; import com.easyagents.flow.core.searchengine.SearchEngineProvider; import com.easyagents.flow.core.searchengine.impl.BochaaiSearchEngineImpl; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import tech.easyflow.ai.config.BochaaiProps; +import tech.easyflow.ai.config.CodeEngineProps; +import tech.easyflow.ai.easyagentsflow.code.PythonRuntimeEngine; +import tech.easyflow.ai.easyagentsflow.code.UnavailableCodeRuntimeEngine; import tech.easyflow.ai.node.*; +import tech.easyflow.common.util.StringUtil; import javax.annotation.Resource; +import java.util.Arrays; +import java.util.List; @Component public class TinyFlowConfigService { + private static final Logger log = LoggerFactory.getLogger(TinyFlowConfigService.class); + @Resource private LlmProvider llmProvider; @Resource @@ -28,6 +39,10 @@ public class TinyFlowConfigService { private BochaaiProps bochaaiProps; @Resource private KnowledgeProvider knowledgeProvider; + @Resource + private CodeEngineProps codeEngineProps; + @Resource + private CodeEngineCapabilityService codeEngineCapabilityService; public void initProvidersAndNodeParsers(ChainParser chainParser) { setExtraNodeParser(chainParser); @@ -35,6 +50,7 @@ public class TinyFlowConfigService { setFileStorage(); setKnowledgeProvider(); setSearchEngineProvider(); + setCodeRuntimeEngineProvider(); } private void setFileStorage() { @@ -88,4 +104,69 @@ public class TinyFlowConfigService { public void setKnowledgeProvider() { KnowledgeManager.getInstance().registerProvider(knowledgeProvider); } + + public void setCodeRuntimeEngineProvider() { + CodeRuntimeEngineManager manager = CodeRuntimeEngineManager.getInstance(); + + PythonRuntimeEngine.ProbeResult probeResult; + String pythonCommand = codeEngineProps.getCommand(); + if (!codeEngineProps.isEnabled()) { + probeResult = new PythonRuntimeEngine.ProbeResult(false, "配置已关闭"); + } else { + probeResult = PythonRuntimeEngine.probe(pythonCommand, 2000L); + } + + boolean pythonAvailable = codeEngineProps.isEnabled() && probeResult.isAvailable(); + codeEngineCapabilityService.setPythonCapability(pythonAvailable, probeResult.getMessage()); + + if (pythonAvailable) { + PythonRuntimeEngine pythonRuntimeEngine = new PythonRuntimeEngine( + codeEngineProps.getCommand(), + codeEngineProps.getTimeoutMs(), + codeEngineProps.getMaxOutputBytes(), + codeEngineProps.getWorkingDir() + ); + manager.registerProvider(engineId -> { + if ("python".equals(engineId) || "py".equals(engineId)) { + return pythonRuntimeEngine; + } + return null; + }); + log.info("已启用 Python 代码执行引擎: command={}, timeoutMs={}, maxOutputBytes={}, workingDir={}", + codeEngineProps.getCommand(), codeEngineProps.getTimeoutMs(), codeEngineProps.getMaxOutputBytes(), codeEngineProps.getWorkingDir()); + } else { + final String reason = probeResult.getMessage(); + manager.registerProvider(engineId -> { + if ("python".equals(engineId) || "py".equals(engineId)) { + return new UnavailableCodeRuntimeEngine(reason); + } + return null; + }); + log.warn("Python 代码执行引擎不可用: enabled={}, command={}, reason={}", + codeEngineProps.isEnabled(), codeEngineProps.getCommand(), reason); + } + + manager.registerProvider(engineId -> { + if (!(engineId instanceof String)) { + return new UnavailableCodeRuntimeEngine("未配置代码执行引擎"); + } + String engine = ((String) engineId).trim(); + if (StringUtil.noText(engine)) { + return new UnavailableCodeRuntimeEngine("未配置代码执行引擎"); + } + return new UnavailableCodeRuntimeEngine("不支持的代码执行引擎: " + engine + ",当前支持: " + supportedEngineLabels()); + }); + } + + private String supportedEngineLabels() { + List engines = codeEngineCapabilityService.listSupportedCodeEngines() + .stream() + .filter(it -> Boolean.TRUE.equals(it.get("available"))) + .map(it -> (String) it.get("value")) + .toList(); + if (engines.isEmpty()) { + engines = Arrays.asList("js"); + } + return String.join(", ", engines); + } } diff --git a/easyflow-modules/easyflow-module-ai/src/test/java/tech/easyflow/ai/easyagentsflow/code/PythonRuntimeEngineTest.java b/easyflow-modules/easyflow-module-ai/src/test/java/tech/easyflow/ai/easyagentsflow/code/PythonRuntimeEngineTest.java new file mode 100644 index 0000000..4d38ddc --- /dev/null +++ b/easyflow-modules/easyflow-module-ai/src/test/java/tech/easyflow/ai/easyagentsflow/code/PythonRuntimeEngineTest.java @@ -0,0 +1,96 @@ +package tech.easyflow.ai.easyagentsflow.code; + +import com.easyagents.flow.core.chain.Chain; +import com.easyagents.flow.core.chain.ChainDefinition; +import com.easyagents.flow.core.chain.repository.InMemoryChainStateRepository; +import com.easyagents.flow.core.node.CodeNode; +import org.junit.Assert; +import org.junit.Assume; +import org.junit.Test; + +import java.util.Map; +import java.util.UUID; + +public class PythonRuntimeEngineTest { + + private static final String PYTHON_COMMAND = "python3"; + + @Test + public void testExecuteSuccess() { + Assume.assumeTrue(PythonRuntimeEngine.probe(PYTHON_COMMAND, 1500L).isAvailable()); + + Chain chain = createChain(); + CodeNode node = (CodeNode) chain.getDefinition().getNodeById("code-test"); + chain.getState().getMemory().put("a", 1); + chain.getState().getMemory().put("b", 2); + + PythonRuntimeEngine engine = new PythonRuntimeEngine(PYTHON_COMMAND, 3000L, 65536, System.getProperty("java.io.tmpdir")); + Map result = engine.execute("_result['sum'] = a + b\n_result['env_type'] = type(_env).__name__", node, chain); + + Assert.assertEquals(3, ((Number) result.get("sum")).intValue()); + Assert.assertEquals("dict", result.get("env_type")); + } + + @Test + public void testSyntaxError() { + Assume.assumeTrue(PythonRuntimeEngine.probe(PYTHON_COMMAND, 1500L).isAvailable()); + + Chain chain = createChain(); + CodeNode node = (CodeNode) chain.getDefinition().getNodeById("code-test"); + PythonRuntimeEngine engine = new PythonRuntimeEngine(PYTHON_COMMAND, 3000L, 65536, System.getProperty("java.io.tmpdir")); + + assertExecuteFail(engine, node, chain, "if True print('broken')", "执行失败"); + } + + @Test + public void testTimeout() { + Assume.assumeTrue(PythonRuntimeEngine.probe(PYTHON_COMMAND, 1500L).isAvailable()); + + Chain chain = createChain(); + CodeNode node = (CodeNode) chain.getDefinition().getNodeById("code-test"); + PythonRuntimeEngine engine = new PythonRuntimeEngine(PYTHON_COMMAND, 100L, 65536, System.getProperty("java.io.tmpdir")); + + assertExecuteFail(engine, node, chain, "import time\ntime.sleep(1)\n_result['ok'] = 1", "超时"); + } + + @Test + public void testOutputTooLarge() { + Assume.assumeTrue(PythonRuntimeEngine.probe(PYTHON_COMMAND, 1500L).isAvailable()); + + Chain chain = createChain(); + CodeNode node = (CodeNode) chain.getDefinition().getNodeById("code-test"); + PythonRuntimeEngine engine = new PythonRuntimeEngine(PYTHON_COMMAND, 3000L, 64, System.getProperty("java.io.tmpdir")); + + assertExecuteFail(engine, node, chain, "_result['text'] = 'a' * 2048", "输出超出限制"); + } + + @Test + public void testCommandNotFound() { + Chain chain = createChain(); + CodeNode node = (CodeNode) chain.getDefinition().getNodeById("code-test"); + PythonRuntimeEngine engine = new PythonRuntimeEngine("python3_not_exists_for_test", 1000L, 65536, System.getProperty("java.io.tmpdir")); + + assertExecuteFail(engine, node, chain, "_result['ok'] = 1", "执行失败"); + } + + private Chain createChain() { + ChainDefinition definition = new ChainDefinition(); + CodeNode node = new CodeNode(); + node.setId("code-test"); + node.setEngine("python"); + definition.addNode(node); + + Chain chain = new Chain(definition, UUID.randomUUID().toString()); + chain.setChainStateRepository(new InMemoryChainStateRepository()); + return chain; + } + + private void assertExecuteFail(PythonRuntimeEngine engine, CodeNode node, Chain chain, String code, String messageContains) { + try { + engine.execute(code, node, chain); + Assert.fail("expected execute fail"); + } catch (RuntimeException e) { + Assert.assertTrue(e.getMessage().contains(messageContains)); + } + } +} diff --git a/easyflow-starter/easyflow-starter-all/src/main/resources/application.yml b/easyflow-starter/easyflow-starter-all/src/main/resources/application.yml index 278129b..ef4c551 100644 --- a/easyflow-starter/easyflow-starter-all/src/main/resources/application.yml +++ b/easyflow-starter/easyflow-starter-all/src/main/resources/application.yml @@ -119,6 +119,13 @@ node: # 搜索引擎节点 - 目前只支持博查搜索 bochaai: apiKey: 'xxx' + code-engine: + python: + enabled: true + command: python3 + timeout-ms: 5000 + max-output-bytes: 262144 + working-dir: /tmp jetcache: # 缓存类型,可选值:local/remote/both CacheConfig 类初始化 diff --git a/easyflow-ui-admin/cspell.json b/easyflow-ui-admin/cspell.json index 710ecfe..0f0f731 100644 --- a/easyflow-ui-admin/cspell.json +++ b/easyflow-ui-admin/cspell.json @@ -42,6 +42,9 @@ "sortablejs", "styl", "taze", + "xyflow", + "rerank", + "reranker", "ui-kit", "uicons", "unplugin", @@ -64,6 +67,7 @@ "**/*.log", "**/*.test.ts", "**/*.spec.ts", - "**/__tests__/**" + "**/__tests__/**", + "packages/effects/common-ui/src/components/icon-picker/**" ] }