feat: 支持 Python 代码节点 main 返回输出

- 自动传入节点输入并映射 main 返回字典,兼容历史 _result

- 限制脚本输出缓冲并补充运行时与编辑器测试

- 同步 Python 帮助、占位提示和独立可执行补全片段
This commit is contained in:
2026-08-03 11:16:03 +08:00
parent 51dbfd41b6
commit 1bf755f6c4
5 changed files with 267 additions and 17 deletions

View File

@@ -31,6 +31,106 @@ public class PythonRuntimeEngineTest {
Assert.assertEquals("dict", result.get("env_type"));
}
@Test
public void testExecuteMainSuccess() {
Assume.assumeTrue(PythonRuntimeEngine.probe(PYTHON_COMMAND, 1500L).isAvailable());
Chain chain = createChain();
CodeNode node = (CodeNode) chain.getDefinition().getNodeById("code-test");
chain.getState().getMemory().put("ambient", "memory-value");
PythonRuntimeEngine engine = new PythonRuntimeEngine(
PYTHON_COMMAND,
3000L,
65536,
System.getProperty("java.io.tmpdir"));
Map<String, Object> result = engine.execute(
String.join("\n",
"import math",
"def main(inputs):",
" return {",
" 'answer': inputs['input'],",
" 'score': 95,",
" 'sqrt': math.sqrt(16),",
" 'has_ambient': 'ambient' in inputs",
" }"),
node,
chain,
Map.of("input", "hello"));
Assert.assertEquals("hello", result.get("answer"));
Assert.assertEquals(95, ((Number) result.get("score")).intValue());
Assert.assertEquals(4D, ((Number) result.get("sqrt")).doubleValue(), 0D);
Assert.assertEquals(Boolean.FALSE, result.get("has_ambient"));
}
@Test
public void testLegacyResultTakesPrecedenceOverMain() {
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"));
Map<String, Object> result = engine.execute(
String.join("\n",
"_result['source'] = 'legacy'",
"def main(inputs):",
" return {'source': 'main'}"),
node,
chain,
Map.of("input", "hello"));
Assert.assertEquals("legacy", result.get("source"));
}
@Test
public void testMainMustReturnDict() {
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,
"def main(inputs):\n return 'invalid'",
"Python main 函数必须返回 dict");
}
@Test
public void testCapturedOutputIsBounded() {
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"));
Map<String, Object> result = engine.execute(
String.join("\n",
"import sys",
"print('a' * 2048)",
"_result['truncated'] = sys.stdout.truncated"),
node,
chain);
Assert.assertEquals(Boolean.TRUE, result.get("truncated"));
}
@Test
public void testSyntaxError() {
Assume.assumeTrue(PythonRuntimeEngine.probe(PYTHON_COMMAND, 1500L).isAvailable());