Files
AstrBot/astrbot/core/computer/booters/boxlite.py
Soulter 2472a12671 feat: filesystem grep, read, write, edit file and workspace support (#7402)
* feat: filesystem grep, read, edit file

* feat: add file write tool and enhance file read functionality

* feat: enhance tool prompt formatting and add escaped text decoding for file editing

* feat: remove redundant safe path tests from security restrictions

* feat: implement file read tool with support for text and image files, including validation for large files

* feat: add file read utilities and integrate with filesystem tools

* refactor: move computer tools to builtin tools registry

* refactor: remove unused plugin_context parameter from _apply_sandbox_tools

* feat: supports to display enabled builtin tools in configs

* feat: add tooltip for disabled builtin tools and update localization strings

* feat: add workspace extra prompt handling in message processing

* feat: add ripgrep installation to Dockerfile

* perf: shell executed in workspace dir in local env

* feat: enhance file reading capabilities to support PDF and DOCX parsing, including workspace storage for long documents

* feat: update converted text notice to suggest using grep for large files

* feat: implement handling for large tool results with overflow file writing and read tool integration

* fix: test

* feat: enhance onboarding steps to include computer access configuration and related help information

* feat: add support for additional temporary path in restricted environment checks

* feat: update computer access hints and add detailed configuration instructions
2026-04-11 17:01:54 +08:00

195 lines
6.7 KiB
Python

import asyncio
import random
from typing import Any
import aiohttp
import boxlite
from shipyard 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
from .shipyard import ShipyardFileSystemWrapper
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:
# Read file content
with open(path, "rb") as f:
file_content = f.read()
# Create multipart form data
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) # 2 minutes for file upload
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._python = ShipyardPythonComponent(
client=self.mocked, # type: ignore
ship_id=self.box.id,
session_id=session_id,
)
self._shell = ShipyardShellComponent(
client=self.mocked, # type: ignore
ship_id=self.box.id,
session_id=session_id,
)
self._ship_fs = ShipyardFileSystemComponent(
client=self.mocked, # type: ignore
ship_id=self.box.id,
session_id=session_id,
)
self._fs = ShipyardFileSystemWrapper(
_shipyard_fs=self._ship_fs, _shipyard_shell=self._shell
)
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)