From f1206d987bf7dff50663d0fc0c5b31975f6d8c26 Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Mon, 27 Apr 2026 21:59:49 +0800 Subject: [PATCH] feat(shell): update timeout parameter to be optional in shell execution methods --- astrbot/core/computer/booters/local.py | 2 +- astrbot/core/computer/booters/shipyard.py | 91 ++++++++++++++++++- astrbot/core/computer/booters/shipyard_neo.py | 14 ++- astrbot/core/computer/olayer/shell.py | 2 +- astrbot/core/tools/computer_tools/shell.py | 7 +- 5 files changed, 102 insertions(+), 14 deletions(-) diff --git a/astrbot/core/computer/booters/local.py b/astrbot/core/computer/booters/local.py index 44122361d..289ed19e9 100644 --- a/astrbot/core/computer/booters/local.py +++ b/astrbot/core/computer/booters/local.py @@ -90,7 +90,7 @@ class LocalShellComponent(ShellComponent): command: str, cwd: str | None = None, env: dict[str, str] | None = None, - timeout: int | None = 30, + timeout: int | None = None, shell: bool = True, background: bool = False, ) -> dict[str, Any]: diff --git a/astrbot/core/computer/booters/shipyard.py b/astrbot/core/computer/booters/shipyard.py index bed1b0653..4adfde1da 100644 --- a/astrbot/core/computer/booters/shipyard.py +++ b/astrbot/core/computer/booters/shipyard.py @@ -1,5 +1,6 @@ from __future__ import annotations +import shlex from typing import Any from shipyard import FileSystemComponent as ShipyardFileSystemComponent @@ -12,6 +13,91 @@ from .base import ComputerBooter from .shipyard_search_file_util import search_files_via_shell +def _maybe_model_dump(value: Any) -> dict[str, Any]: + if isinstance(value, dict): + return value + if hasattr(value, "model_dump"): + dumped = value.model_dump() + if isinstance(dumped, dict): + return dumped + return {} + + +class ShipyardShellWrapper: + def __init__(self, _shipyard_shell: ShellComponent): + self._shell = _shipyard_shell + + async def exec( + self, + command: str, + cwd: str | None = None, + env: dict[str, str] | None = None, + timeout: int | None = None, + shell: bool = True, + background: bool = False, + ) -> dict[str, Any]: + if not shell: + return { + "stdout": "", + "stderr": "error: only shell mode is supported in shipyard booter.", + "exit_code": 2, + "success": False, + } + + run_command = command + if env: + env_prefix = " ".join( + f"{k}={shlex.quote(str(v))}" for k, v in sorted(env.items()) + ) + run_command = f"{env_prefix} {run_command}" + + if background: + run_command = ( + f"nohup sh -lc {shlex.quote(run_command)} >/dev/null 2>&1 & echo $!" + ) + + result = await self._shell.exec( + run_command, + timeout=timeout, + cwd=cwd, + ) + payload = _maybe_model_dump(result) + + stdout = payload.get("output", payload.get("stdout", "")) or "" + stderr = payload.get("error", payload.get("stderr", "")) or "" + exit_code = payload.get("exit_code") + if background: + pid: int | None = None + try: + pid = int(str(stdout).strip().splitlines()[-1]) + except Exception: + pid = None + return { + "pid": pid, + "stdout": ( + f"Command is running in the background. pid={pid}" + if pid is not None + else "Command was submitted in the background." + ), + "stderr": stderr, + "exit_code": exit_code, + "success": bool(payload.get("success", not stderr)), + "execution_id": payload.get("execution_id"), + "execution_time_ms": payload.get("execution_time_ms"), + "command": payload.get("command"), + } + + return { + "stdout": stdout, + "stderr": stderr, + "exit_code": exit_code, + "success": bool(payload.get("success", not stderr)), + "execution_id": payload.get("execution_id"), + "execution_time_ms": payload.get("execution_time_ms"), + "command": payload.get("command"), + } + + class ShipyardFileSystemWrapper: def __init__( self, _shipyard_fs: ShipyardFileSystemComponent, _shipyard_shell: ShellComponent @@ -107,7 +193,8 @@ class ShipyardBooter(ComputerBooter): ) logger.info(f"Got sandbox ship: {ship.id} for session: {session_id}") self._ship = ship - self._fs = ShipyardFileSystemWrapper(self._ship.fs, self._ship.shell) + self._shell = ShipyardShellWrapper(self._ship.shell) + self._fs = ShipyardFileSystemWrapper(self._ship.fs, self._shell) async def shutdown(self) -> None: logger.info("[Computer] Shipyard booter shutdown.") @@ -122,7 +209,7 @@ class ShipyardBooter(ComputerBooter): @property def shell(self) -> ShellComponent: - return self._ship.shell + return self._shell async def upload_file(self, path: str, file_name: str) -> dict: """Upload file to sandbox""" diff --git a/astrbot/core/computer/booters/shipyard_neo.py b/astrbot/core/computer/booters/shipyard_neo.py index f3bb7e7b5..b141f918a 100644 --- a/astrbot/core/computer/booters/shipyard_neo.py +++ b/astrbot/core/computer/booters/shipyard_neo.py @@ -96,7 +96,7 @@ class NeoShellComponent(ShellComponent): command: str, cwd: str | None = None, env: dict[str, str] | None = None, - timeout: int | None = 30, + timeout: int | None = None, shell: bool = True, background: bool = False, ) -> dict[str, Any]: @@ -116,11 +116,13 @@ class NeoShellComponent(ShellComponent): run_command = f"{env_prefix} {run_command}" if background: - run_command = f"nohup sh -lc {shlex.quote(run_command)} >/tmp/astrbot_bg.log 2>&1 & echo $!" + run_command = ( + f"nohup sh -lc {shlex.quote(run_command)} >/dev/null 2>&1 & echo $!" + ) result = await self._sandbox.shell.exec( run_command, - timeout=timeout or 30, + timeout=timeout, cwd=cwd, ) payload = _maybe_model_dump(result) @@ -136,7 +138,11 @@ class NeoShellComponent(ShellComponent): pid = None return { "pid": pid, - "stdout": stdout, + "stdout": ( + f"Command is running in the background. pid={pid}" + if pid is not None + else "Command was submitted in the background." + ), "stderr": stderr, "exit_code": exit_code, "success": bool(payload.get("success", not stderr)), diff --git a/astrbot/core/computer/olayer/shell.py b/astrbot/core/computer/olayer/shell.py index df2263b65..c1c587534 100644 --- a/astrbot/core/computer/olayer/shell.py +++ b/astrbot/core/computer/olayer/shell.py @@ -13,7 +13,7 @@ class ShellComponent(Protocol): command: str, cwd: str | None = None, env: dict[str, str] | None = None, - timeout: int | None = 30, + timeout: int | None = None, shell: bool = True, background: bool = False, ) -> dict[str, Any]: diff --git a/astrbot/core/tools/computer_tools/shell.py b/astrbot/core/tools/computer_tools/shell.py index bfde5b8fd..b307ed261 100644 --- a/astrbot/core/tools/computer_tools/shell.py +++ b/astrbot/core/tools/computer_tools/shell.py @@ -58,10 +58,6 @@ class ExecuteShellTool(FunctionTool): "type": "string", "description": "The shell command to execute in the current runtime shell (for example, cmd.exe on Windows). Equal to 'cd {working_dir} && {your_command}'.", }, - "timeout": { - "type": "integer", - "description": "Optional timeout in seconds for the command execution.", - }, "background": { "type": "boolean", "description": "Run the command in the background. Use the file read tool to read the output later.", @@ -84,7 +80,6 @@ class ExecuteShellTool(FunctionTool): command: str, background: bool = False, env: dict = {}, - timeout: int | None = None, ) -> ToolExecResult: if permission_error := check_admin_permission(context, "Shell execution"): return permission_error @@ -119,7 +114,7 @@ class ExecuteShellTool(FunctionTool): cwd=cwd, background=background, env=env, - timeout=timeout or context.tool_call_timeout or 30, + timeout=None, ) if stdout_file: result["stdout_file"] = stdout_file