| import asyncio |
| import random |
| from typing import Any |
|
|
| import aiohttp |
| import boxlite |
| from shipyard.filesystem import FileSystemComponent as ShipyardFileSystemComponent |
| from shipyard.python import PythonComponent as ShipyardPythonComponent |
| from shipyard.shell import ShellComponent as ShipyardShellComponent |
|
|
| from astrbot.api import logger |
|
|
| from ..olayer import FileSystemComponent, PythonComponent, ShellComponent |
| from .base import ComputerBooter |
|
|
|
|
| class MockShipyardSandboxClient: |
| def __init__(self, sb_url: str) -> None: |
| self.sb_url = sb_url.rstrip("/") |
|
|
| async def _exec_operation( |
| self, |
| ship_id: str, |
| operation_type: str, |
| payload: dict[str, Any], |
| session_id: str, |
| ) -> dict[str, Any]: |
| async with aiohttp.ClientSession() as session: |
| headers = {"X-SESSION-ID": session_id} |
| async with session.post( |
| f"{self.sb_url}/{operation_type}", |
| json=payload, |
| headers=headers, |
| ) as response: |
| if response.status == 200: |
| return await response.json() |
| else: |
| error_text = await response.text() |
| raise Exception( |
| f"Failed to exec operation: {response.status} {error_text}" |
| ) |
|
|
| async def upload_file(self, path: str, remote_path: str) -> dict: |
| """Upload a file to the sandbox""" |
| url = f"http://{self.sb_url}/upload" |
|
|
| try: |
| |
| with open(path, "rb") as f: |
| file_content = f.read() |
|
|
| |
| data = aiohttp.FormData() |
| data.add_field( |
| "file", |
| file_content, |
| filename=remote_path.split("/")[-1], |
| content_type="application/octet-stream", |
| ) |
| data.add_field("file_path", remote_path) |
|
|
| timeout = aiohttp.ClientTimeout(total=120) |
|
|
| async with aiohttp.ClientSession(timeout=timeout) as session: |
| async with session.post(url, data=data) as response: |
| if response.status == 200: |
| logger.info( |
| "[Computer] File uploaded to Boxlite sandbox: %s", |
| remote_path, |
| ) |
| return { |
| "success": True, |
| "message": "File uploaded successfully", |
| "file_path": remote_path, |
| } |
| else: |
| error_text = await response.text() |
| return { |
| "success": False, |
| "error": f"Server returned {response.status}: {error_text}", |
| "message": "File upload failed", |
| } |
|
|
| except aiohttp.ClientError as e: |
| logger.error(f"Failed to upload file: {e}") |
| return { |
| "success": False, |
| "error": f"Connection error: {str(e)}", |
| "message": "File upload failed", |
| } |
| except asyncio.TimeoutError: |
| return { |
| "success": False, |
| "error": "File upload timeout", |
| "message": "File upload failed", |
| } |
| except FileNotFoundError: |
| logger.error(f"File not found: {path}") |
| return { |
| "success": False, |
| "error": f"File not found: {path}", |
| "message": "File upload failed", |
| } |
| except Exception as e: |
| logger.error(f"Unexpected error uploading file: {e}") |
| return { |
| "success": False, |
| "error": f"Internal error: {str(e)}", |
| "message": "File upload failed", |
| } |
|
|
| async def wait_healthy(self, ship_id: str, session_id: str) -> None: |
| """Mock wait healthy""" |
| loop = 60 |
| while loop > 0: |
| try: |
| logger.info( |
| f"Checking health for sandbox {ship_id} on {self.sb_url}..." |
| ) |
| url = f"{self.sb_url}/health" |
| async with aiohttp.ClientSession() as session: |
| async with session.get(url) as response: |
| if response.status == 200: |
| logger.info(f"Sandbox {ship_id} is healthy") |
| return |
| except Exception: |
| await asyncio.sleep(1) |
| loop -= 1 |
|
|
|
|
| class BoxliteBooter(ComputerBooter): |
| async def boot(self, session_id: str) -> None: |
| logger.info( |
| f"Booting(Boxlite) for session: {session_id}, this may take a while..." |
| ) |
| random_port = random.randint(20000, 30000) |
| self.box = boxlite.SimpleBox( |
| image="soulter/shipyard-ship", |
| memory_mib=512, |
| cpus=1, |
| ports=[ |
| { |
| "host_port": random_port, |
| "guest_port": 8123, |
| } |
| ], |
| ) |
| await self.box.start() |
| logger.info(f"Boxlite booter started for session: {session_id}") |
| self.mocked = MockShipyardSandboxClient( |
| sb_url=f"http://127.0.0.1:{random_port}" |
| ) |
| self._fs = ShipyardFileSystemComponent( |
| client=self.mocked, |
| ship_id=self.box.id, |
| session_id=session_id, |
| ) |
| self._python = ShipyardPythonComponent( |
| client=self.mocked, |
| ship_id=self.box.id, |
| session_id=session_id, |
| ) |
| self._shell = ShipyardShellComponent( |
| client=self.mocked, |
| ship_id=self.box.id, |
| session_id=session_id, |
| ) |
|
|
| await self.mocked.wait_healthy(self.box.id, session_id) |
|
|
| async def shutdown(self) -> None: |
| logger.info(f"Shutting down Boxlite booter for ship: {self.box.id}") |
| self.box.shutdown() |
| logger.info(f"Boxlite booter for ship: {self.box.id} stopped") |
|
|
| @property |
| def fs(self) -> FileSystemComponent: |
| return self._fs |
|
|
| @property |
| def python(self) -> PythonComponent: |
| return self._python |
|
|
| @property |
| def shell(self) -> ShellComponent: |
| return self._shell |
|
|
| async def upload_file(self, path: str, file_name: str) -> dict: |
| """Upload file to sandbox""" |
| return await self.mocked.upload_file(path, file_name) |
|
|