From 97dadb55e882de79737f67db3265bcd53335f636 Mon Sep 17 00:00:00 2001 From: ling Date: Thu, 18 Jun 2026 10:48:23 +0800 Subject: [PATCH 1/2] fix: normalize shipyard shell exec response --- astrbot/core/computer/booters/shipyard.py | 17 +++++++++-- tests/unit/test_computer.py | 36 +++++++++++++++++++++++ 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/astrbot/core/computer/booters/shipyard.py b/astrbot/core/computer/booters/shipyard.py index a8375544da..fbc710f02a 100644 --- a/astrbot/core/computer/booters/shipyard.py +++ b/astrbot/core/computer/booters/shipyard.py @@ -61,10 +61,23 @@ async def exec( cwd=cwd, ) payload = _maybe_model_dump(result) + data = payload.get("data") + if isinstance(data, dict): + payload.update( + {key: value for key, value in data.items() if value is not None} + ) - stdout = payload.get("output", payload.get("stdout", "")) or "" - stderr = payload.get("error", payload.get("stderr", "")) or "" + stdout = payload.get("output") + if not stdout and payload.get("stdout") is not None: + stdout = payload.get("stdout") + stdout = stdout or "" + stderr = payload.get("error") + if not stderr and payload.get("stderr") is not None: + stderr = payload.get("stderr") + stderr = stderr or "" exit_code = payload.get("exit_code") + if exit_code is None: + exit_code = payload.get("return_code") if background: pid: int | None = None try: diff --git a/tests/unit/test_computer.py b/tests/unit/test_computer.py index e667f98a6c..1bd963a7c6 100644 --- a/tests/unit/test_computer.py +++ b/tests/unit/test_computer.py @@ -376,6 +376,42 @@ def test_base_class_is_protocol(self): class TestShipyardBooter: """Tests for ShipyardBooter.""" + @pytest.mark.asyncio + async def test_shipyard_shell_unwraps_bay_exec_response(self): + """Bay exec responses keep shell output under the data field.""" + from astrbot.core.computer.booters.shipyard import ShipyardShellWrapper + + raw_shell = AsyncMock() + raw_shell.exec = AsyncMock( + return_value={ + "success": True, + "data": { + "success": True, + "return_code": 0, + "stdout": "hello_from_shipyard_bay\n/workspace\n", + "stderr": "", + "pid": 16, + "process_id": None, + "error": None, + }, + "error": None, + } + ) + + result = await ShipyardShellWrapper(raw_shell).exec( + "echo hello_from_shipyard_bay && pwd" + ) + + assert result == { + "stdout": "hello_from_shipyard_bay\n/workspace\n", + "stderr": "", + "exit_code": 0, + "success": True, + "execution_id": None, + "execution_time_ms": None, + "command": None, + } + @pytest.mark.asyncio async def test_shipyard_booter_init(self): """Test ShipyardBooter initialization.""" From 2ad710330b785fd039e6d6fd9bf381ebc7ae2934 Mon Sep 17 00:00:00 2001 From: ling Date: Thu, 18 Jun 2026 11:03:00 +0800 Subject: [PATCH 2/2] fix: normalize shipyard shell exec responses --- astrbot/core/computer/booters/shipyard.py | 10 ++----- tests/unit/test_computer.py | 32 +++++++++++++++++++++++ 2 files changed, 34 insertions(+), 8 deletions(-) diff --git a/astrbot/core/computer/booters/shipyard.py b/astrbot/core/computer/booters/shipyard.py index fbc710f02a..99fda4da4a 100644 --- a/astrbot/core/computer/booters/shipyard.py +++ b/astrbot/core/computer/booters/shipyard.py @@ -67,14 +67,8 @@ async def exec( {key: value for key, value in data.items() if value is not None} ) - stdout = payload.get("output") - if not stdout and payload.get("stdout") is not None: - stdout = payload.get("stdout") - stdout = stdout or "" - stderr = payload.get("error") - if not stderr and payload.get("stderr") is not None: - stderr = payload.get("stderr") - stderr = stderr or "" + stdout = payload.get("output") or payload.get("stdout") or "" + stderr = payload.get("error") or payload.get("stderr") or "" exit_code = payload.get("exit_code") if exit_code is None: exit_code = payload.get("return_code") diff --git a/tests/unit/test_computer.py b/tests/unit/test_computer.py index 1bd963a7c6..940f8fbfa7 100644 --- a/tests/unit/test_computer.py +++ b/tests/unit/test_computer.py @@ -412,6 +412,38 @@ async def test_shipyard_shell_unwraps_bay_exec_response(self): "command": None, } + @pytest.mark.asyncio + async def test_shipyard_shell_keeps_flat_exec_response_compatible(self): + """Flat shell responses remain compatible with ShipyardShellWrapper.""" + from astrbot.core.computer.booters.shipyard import ShipyardShellWrapper + + raw_shell = AsyncMock() + raw_shell.exec = AsyncMock( + return_value={ + "success": True, + "stdout": "hello_from_legacy_shell\n/workspace\n", + "stderr": "", + "exit_code": 0, + "execution_id": "exec-legacy", + "execution_time_ms": 45, + "command": "echo hello_from_legacy_shell && pwd", + } + ) + + result = await ShipyardShellWrapper(raw_shell).exec( + "echo hello_from_legacy_shell && pwd" + ) + + assert result == { + "stdout": "hello_from_legacy_shell\n/workspace\n", + "stderr": "", + "exit_code": 0, + "success": True, + "execution_id": "exec-legacy", + "execution_time_ms": 45, + "command": "echo hello_from_legacy_shell && pwd", + } + @pytest.mark.asyncio async def test_shipyard_booter_init(self): """Test ShipyardBooter initialization."""