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

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