feat: 支持工作流代码节点 Python 执行引擎

- easyflow-module-ai: 新增 PythonRuntimeEngine、不可用引擎降级实现与引擎能力服务

- easyflow-module-ai: 在 TinyFlowConfigService 注册 python/py 引擎并增加启动探测与可用性日志

- easyflow-api: 新增 /api/v1/workflow/supportedCodeEngines 能力查询接口

- easyflow-starter: 增加 node.code-engine.python 配置项默认值

- Dockerfile: 安装 python3 运行时以支持容器内执行

- test: 增加 PythonRuntimeEngineTest 覆盖成功、语法错误、超时、输出限制、命令缺失场景

- chore(ui-admin): 更新 cspell 词典
This commit is contained in:
2026-03-01 19:59:53 +08:00
parent ac8de7dbb8
commit 29f82ed1f0
11 changed files with 631 additions and 2 deletions

View File

@@ -11,6 +11,9 @@ ENV EASYFLOW_LOG_FILE=/app/logs/app.log
WORKDIR /app WORKDIR /app
RUN useradd --system --create-home easyflow && \ 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 && \ mkdir -p /app/logs && \
chown -R easyflow:easyflow /app chown -R easyflow:easyflow /app

View File

@@ -12,6 +12,7 @@ import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import tech.easyflow.ai.entity.Workflow; import tech.easyflow.ai.entity.Workflow;
import tech.easyflow.ai.easyagentsflow.service.CodeEngineCapabilityService;
import tech.easyflow.ai.service.BotWorkflowService; import tech.easyflow.ai.service.BotWorkflowService;
import tech.easyflow.ai.service.ModelService; import tech.easyflow.ai.service.ModelService;
import tech.easyflow.ai.service.WorkflowService; import tech.easyflow.ai.service.WorkflowService;
@@ -55,6 +56,8 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
private ChainParser chainParser; private ChainParser chainParser;
@Resource @Resource
private TinyFlowService tinyFlowService; private TinyFlowService tinyFlowService;
@Resource
private CodeEngineCapabilityService codeEngineCapabilityService;
public WorkflowController(WorkflowService service, ModelService modelService) { public WorkflowController(WorkflowService service, ModelService modelService) {
super(service); super(service);
@@ -160,6 +163,12 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
return Result.ok(res); return Result.ok(res);
} }
@GetMapping("/supportedCodeEngines")
@SaCheckPermission("/api/v1/workflow/query")
public Result<?> supportedCodeEngines() {
return Result.ok(codeEngineCapabilityService.listSupportedCodeEngines());
}
@Override @Override
public Result<Workflow> detail(String id) { public Result<Workflow> detail(String id) {
Workflow workflow = service.getDetail(id); Workflow workflow = service.getDetail(id);
@@ -210,4 +219,4 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
} }
return null; return null;
} }
} }

View File

@@ -92,5 +92,12 @@
<groupId>com.easyagents</groupId> <groupId>com.easyagents</groupId>
<artifactId>easy-agents-mcp</artifactId> <artifactId>easy-agents-mcp</artifactId>
</dependency> </dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
</dependencies> </dependencies>
</project> </project>

View File

@@ -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;
}
}

View File

@@ -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<String, Object> 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<String, Object> context = buildContext(chain, node);
Map<String, Object> 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<String, Object> buildContext(Chain chain, CodeNode node) {
Map<String, Object> context = new HashMap<>();
Map<String, Object> all = chain.getState().getMemory();
all.forEach((key, value) -> {
if (!key.contains(".")) {
context.put(key, value);
}
});
Map<String, Object> 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);
}
}
}

View File

@@ -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<String, Object> execute(String code, CodeNode node, Chain chain) {
String engine = node == null ? "unknown" : node.getEngine();
throw new RuntimeException("代码执行引擎不可用: " + engine + ",原因: " + reason);
}
}

View File

@@ -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<Map<String, Object>> listSupportedCodeEngines() {
List<Map<String, Object>> result = new ArrayList<>();
result.add(toItem("js", "JavaScript", true, ""));
result.add(toItem("python", "Python", pythonAvailable, pythonAvailable ? "" : pythonReason));
return result;
}
private Map<String, Object> toItem(String value, String label, boolean available, String reason) {
Map<String, Object> item = new LinkedHashMap<>();
item.put("value", value);
item.put("label", label);
item.put("available", available);
item.put("reason", reason);
return item;
}
}

View File

@@ -1,5 +1,6 @@
package tech.easyflow.ai.easyagentsflow.service; 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.FileStorageManager;
import com.easyagents.flow.core.filestoreage.FileStorageProvider; import com.easyagents.flow.core.filestoreage.FileStorageProvider;
import com.easyagents.flow.core.knowledge.KnowledgeManager; 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.SearchEngineManager;
import com.easyagents.flow.core.searchengine.SearchEngineProvider; import com.easyagents.flow.core.searchengine.SearchEngineProvider;
import com.easyagents.flow.core.searchengine.impl.BochaaiSearchEngineImpl; import com.easyagents.flow.core.searchengine.impl.BochaaiSearchEngineImpl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import tech.easyflow.ai.config.BochaaiProps; 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.ai.node.*;
import tech.easyflow.common.util.StringUtil;
import javax.annotation.Resource; import javax.annotation.Resource;
import java.util.Arrays;
import java.util.List;
@Component @Component
public class TinyFlowConfigService { public class TinyFlowConfigService {
private static final Logger log = LoggerFactory.getLogger(TinyFlowConfigService.class);
@Resource @Resource
private LlmProvider llmProvider; private LlmProvider llmProvider;
@Resource @Resource
@@ -28,6 +39,10 @@ public class TinyFlowConfigService {
private BochaaiProps bochaaiProps; private BochaaiProps bochaaiProps;
@Resource @Resource
private KnowledgeProvider knowledgeProvider; private KnowledgeProvider knowledgeProvider;
@Resource
private CodeEngineProps codeEngineProps;
@Resource
private CodeEngineCapabilityService codeEngineCapabilityService;
public void initProvidersAndNodeParsers(ChainParser chainParser) { public void initProvidersAndNodeParsers(ChainParser chainParser) {
setExtraNodeParser(chainParser); setExtraNodeParser(chainParser);
@@ -35,6 +50,7 @@ public class TinyFlowConfigService {
setFileStorage(); setFileStorage();
setKnowledgeProvider(); setKnowledgeProvider();
setSearchEngineProvider(); setSearchEngineProvider();
setCodeRuntimeEngineProvider();
} }
private void setFileStorage() { private void setFileStorage() {
@@ -88,4 +104,69 @@ public class TinyFlowConfigService {
public void setKnowledgeProvider() { public void setKnowledgeProvider() {
KnowledgeManager.getInstance().registerProvider(knowledgeProvider); 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<String> 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);
}
} }

View File

@@ -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<String, Object> 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));
}
}
}

View File

@@ -119,6 +119,13 @@ node:
# 搜索引擎节点 - 目前只支持博查搜索 # 搜索引擎节点 - 目前只支持博查搜索
bochaai: bochaai:
apiKey: 'xxx' apiKey: 'xxx'
code-engine:
python:
enabled: true
command: python3
timeout-ms: 5000
max-output-bytes: 262144
working-dir: /tmp
jetcache: jetcache:
# 缓存类型可选值local/remote/both CacheConfig 类初始化 # 缓存类型可选值local/remote/both CacheConfig 类初始化

View File

@@ -42,6 +42,9 @@
"sortablejs", "sortablejs",
"styl", "styl",
"taze", "taze",
"xyflow",
"rerank",
"reranker",
"ui-kit", "ui-kit",
"uicons", "uicons",
"unplugin", "unplugin",
@@ -64,6 +67,7 @@
"**/*.log", "**/*.log",
"**/*.test.ts", "**/*.test.ts",
"**/*.spec.ts", "**/*.spec.ts",
"**/__tests__/**" "**/__tests__/**",
"packages/effects/common-ui/src/components/icon-picker/**"
] ]
} }