import os import shutil import uuid from pathlib import Path from astrbot.api import logger from astrbot.core.skills.skill_manager import SANDBOX_SKILLS_ROOT from astrbot.core.star.context import Context from astrbot.core.utils.astrbot_path import ( get_astrbot_skills_path, get_astrbot_temp_path, ) from .booters.base import ComputerBooter from .booters.local import LocalBooter session_booter: dict[str, ComputerBooter] = {} local_booter: ComputerBooter | None = None async def _sync_skills_to_sandbox(booter: ComputerBooter) -> None: skills_root = get_astrbot_skills_path() if not os.path.isdir(skills_root): return if not any(Path(skills_root).iterdir()): return temp_dir = get_astrbot_temp_path() os.makedirs(temp_dir, exist_ok=True) zip_base = os.path.join(temp_dir, "skills_bundle") zip_path = f"{zip_base}.zip" try: if os.path.exists(zip_path): os.remove(zip_path) shutil.make_archive(zip_base, "zip", skills_root) remote_zip = Path(SANDBOX_SKILLS_ROOT) / "skills.zip" logger.info("Uploading skills bundle to sandbox...") await booter.shell.exec(f"mkdir -p {SANDBOX_SKILLS_ROOT}") upload_result = await booter.upload_file(zip_path, str(remote_zip)) if not upload_result.get("success", False): raise RuntimeError("Failed to upload skills bundle to sandbox.") # Always overwrite with local source of truth to avoid stale skills in long-lived sandboxes. await booter.shell.exec( f"mkdir -p {SANDBOX_SKILLS_ROOT} && " f"rm -rf {SANDBOX_SKILLS_ROOT}/* && " f"(unzip -o {remote_zip} -d {SANDBOX_SKILLS_ROOT} || " f"python3 -c \"import zipfile; z=zipfile.ZipFile('{remote_zip}'); " f"z.extractall('{SANDBOX_SKILLS_ROOT}')\" || " f"python -c \"import zipfile; z=zipfile.ZipFile('{remote_zip}'); " f"z.extractall('{SANDBOX_SKILLS_ROOT}')\"); " f"rm -f {remote_zip}" ) finally: if os.path.exists(zip_path): try: os.remove(zip_path) except Exception: logger.warning(f"Failed to remove temp skills zip: {zip_path}") async def get_booter( context: Context, session_id: str, ) -> ComputerBooter: config = context.get_config(umo=session_id) sandbox_cfg = config.get("provider_settings", {}).get("sandbox", {}) booter_type = sandbox_cfg.get("booter", "shipyard_neo") if session_id in session_booter: booter = session_booter[session_id] if not await booter.available(): # rebuild session_booter.pop(session_id, None) if session_id not in session_booter: uuid_str = uuid.uuid5(uuid.NAMESPACE_DNS, session_id).hex if booter_type == "shipyard": from .booters.shipyard import ShipyardBooter ep = sandbox_cfg.get("shipyard_endpoint", "") token = sandbox_cfg.get("shipyard_access_token", "") ttl = sandbox_cfg.get("shipyard_ttl", 3600) max_sessions = sandbox_cfg.get("shipyard_max_sessions", 10) client = ShipyardBooter( endpoint_url=ep, access_token=token, ttl=ttl, session_num=max_sessions ) elif booter_type == "shipyard_neo": from .booters.shipyard_neo import ShipyardNeoBooter ep = sandbox_cfg.get("shipyard_neo_endpoint", "") token = sandbox_cfg.get("shipyard_neo_access_token", "") ttl = sandbox_cfg.get("shipyard_neo_ttl", 3600) profile = sandbox_cfg.get("shipyard_neo_profile", "python-default") client = ShipyardNeoBooter( endpoint_url=ep, access_token=token, profile=profile, ttl=ttl, ) elif booter_type == "boxlite": from .booters.boxlite import BoxliteBooter client = BoxliteBooter() else: raise ValueError(f"Unknown booter type: {booter_type}") try: await client.boot(uuid_str) await _sync_skills_to_sandbox(client) except Exception as e: logger.error(f"Error booting sandbox for session {session_id}: {e}") raise e session_booter[session_id] = client return session_booter[session_id] async def sync_skills_to_active_sandboxes() -> None: """Best-effort skills synchronization for all active sandbox sessions.""" for session_id, booter in list(session_booter.items()): try: if not await booter.available(): continue await _sync_skills_to_sandbox(booter) except Exception as e: logger.warning( "Failed to sync skills to sandbox for session %s: %s", session_id, e, ) def get_local_booter() -> ComputerBooter: global local_booter if local_booter is None: local_booter = LocalBooter() return local_booter