diff --git a/astrbot/core/computer/booters/local.py b/astrbot/core/computer/booters/local.py index 1fb7b5cf7a..30de507ba1 100644 --- a/astrbot/core/computer/booters/local.py +++ b/astrbot/core/computer/booters/local.py @@ -143,13 +143,16 @@ async def exec( kernel_id: str | None = None, timeout: int = 30, silent: bool = False, + cwd: str | None = None, ) -> dict[str, Any]: def _run() -> dict[str, Any]: try: + working_dir = os.path.abspath(cwd) if cwd else get_astrbot_root() result = subprocess.run( [os.environ.get("PYTHON", sys.executable), "-c", code], timeout=timeout, capture_output=True, + cwd=working_dir, ) stdout = "" if silent else _decode_shell_output(result.stdout) stderr = ( diff --git a/astrbot/core/computer/olayer/python.py b/astrbot/core/computer/olayer/python.py index 6255041463..2b86b11530 100644 --- a/astrbot/core/computer/olayer/python.py +++ b/astrbot/core/computer/olayer/python.py @@ -14,6 +14,7 @@ async def exec( kernel_id: str | None = None, timeout: int = 30, silent: bool = False, + cwd: str | None = None, ) -> dict[str, Any]: """Execute Python code""" ... diff --git a/astrbot/core/tools/computer_tools/python.py b/astrbot/core/tools/computer_tools/python.py index be909f6d26..f9500ff7e8 100644 --- a/astrbot/core/tools/computer_tools/python.py +++ b/astrbot/core/tools/computer_tools/python.py @@ -11,7 +11,7 @@ from astrbot.core.message.message_event_result import MessageChain from ..registry import builtin_tool -from .util import check_admin_permission +from .util import check_admin_permission, workspace_root _OS_NAME = platform.system() _SANDBOX_PYTHON_TOOL_CONFIG = { @@ -137,10 +137,15 @@ async def call( else context.tool_call_timeout ) try: + current_workspace_root = workspace_root( + context.context.event.unified_msg_origin + ) + current_workspace_root.mkdir(parents=True, exist_ok=True) result = await sb.python.exec( code, timeout=effective_timeout, silent=silent, + cwd=str(current_workspace_root), ) return await handle_result(result, context.context.event) except Exception as e: diff --git a/tests/unit/test_python_tools.py b/tests/unit/test_python_tools.py index deabc0db34..c1f450b079 100644 --- a/tests/unit/test_python_tools.py +++ b/tests/unit/test_python_tools.py @@ -1,5 +1,10 @@ import platform +from types import SimpleNamespace +from unittest.mock import AsyncMock +import pytest + +from astrbot.core.agent.run_context import ContextWrapper from astrbot.core.tools.computer_tools.python import LocalPythonTool, PythonTool @@ -18,3 +23,48 @@ def test_local_python_tool_description_contains_os(): assert current_os in tool.description assert "Python environment" in tool.description assert "system-compatible" in tool.description + + +@pytest.mark.asyncio +async def test_local_python_tool_uses_session_workspace(tmp_path, monkeypatch): + """Local Python execution should use the same workspace as local shell.""" + tool = LocalPythonTool() + python_exec = AsyncMock( + return_value={"data": {"output": {"text": "ok", "images": []}, "error": ""}} + ) + monkeypatch.setattr( + "astrbot.core.tools.computer_tools.python.get_local_booter", + lambda: SimpleNamespace(python=SimpleNamespace(exec=python_exec)), + ) + monkeypatch.setattr( + "astrbot.core.tools.computer_tools.python.workspace_root", + lambda umo: tmp_path / umo.replace(":", "_"), + ) + + event = SimpleNamespace( + unified_msg_origin="onebot:GroupMessage:12345", + role="admin", + get_platform_name=lambda: "onebot", + ) + context = ContextWrapper( + context=SimpleNamespace( + event=event, + context=SimpleNamespace( + get_config=lambda **_kwargs: { + "provider_settings": {"computer_use_require_admin": True} + } + ), + ), + tool_call_timeout=60, + ) + + await tool.call(context, code="print('ok')", timeout=30) + + workspace = tmp_path / "onebot_GroupMessage_12345" + assert workspace.is_dir() + python_exec.assert_awaited_once_with( + "print('ok')", + timeout=30, + silent=False, + cwd=str(workspace.resolve(strict=False)), + )