feat(shell): update timeout parameter to be optional in shell execution methods

This commit is contained in:
Soulter
2026-04-27 21:59:49 +08:00
parent 77040cabcd
commit f1206d987b
5 changed files with 102 additions and 14 deletions

View File

@@ -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]:

View File

@@ -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"""

View File

@@ -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)),

View File

@@ -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]:

View File

@@ -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