#!/usr/bin/env bash # ────────────────────────────────────────────────────────────── # start-with-neo.sh — 一键启动 Shipyard Neo Bay + AstrBot # # Usage: # bash scripts/start-with-neo.sh # 默认 Bay :8114 # BAY_PORT=9000 bash scripts/start-with-neo.sh # 自定义端口 # ────────────────────────────────────────────────────────────── set -euo pipefail # ── 路径 ────────────────────────────────────────────────────── SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" ASTRBOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" # shipyard-neo mono-repo root is one level above AstrBot NEO_ROOT="$(cd "$ASTRBOT_DIR/.." && pwd)" BAY_DIR="$NEO_ROOT/pkgs/bay" BAY_PORT="${BAY_PORT:-8114}" BAY_HOST="0.0.0.0" BAY_PID="" BAY_API_KEY="" # Populated after Bay starts from credentials.json # ── 颜色 ────────────────────────────────────────────────────── RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' CYAN='\033[0;36m' NC='\033[0m' # No Color log() { echo -e "${CYAN}[neo]${NC} $*"; } ok() { echo -e "${GREEN}[neo]${NC} $*"; } warn() { echo -e "${YELLOW}[neo]${NC} $*"; } err() { echo -e "${RED}[neo]${NC} $*" >&2; } # ── 清理函数 ────────────────────────────────────────────────── cleanup() { log "Shutting down..." if [[ -n "$BAY_PID" ]] && kill -0 "$BAY_PID" 2>/dev/null; then log "Stopping Bay (PID $BAY_PID)..." kill "$BAY_PID" 2>/dev/null || true wait "$BAY_PID" 2>/dev/null || true fi ok "All services stopped." } trap cleanup EXIT INT TERM # ── 检查前置条件 ────────────────────────────────────────────── check_prerequisites() { log "Checking prerequisites..." if [[ ! -d "$BAY_DIR" ]]; then err "Bay directory not found: $BAY_DIR" err "Expected shipyard-neo mono-repo at: $NEO_ROOT" exit 1 fi if ! command -v uv &>/dev/null; then err "'uv' is not installed. Please install it first." exit 1 fi # Check Docker access (try without sudo first, then with sudo) if docker info &>/dev/null 2>&1; then ok "Docker is accessible." elif sudo docker info &>/dev/null 2>&1; then warn "Docker requires sudo. Bay may need socket permissions." warn "If Bay fails to connect to Docker, run: sudo chmod 666 /var/run/docker.sock" else err "Docker is not accessible. Please install Docker or fix permissions." exit 1 fi # Check Bay venv if [[ ! -d "$BAY_DIR/.venv" ]]; then log "Bay venv not found. Running 'uv sync' in $BAY_DIR ..." (cd "$BAY_DIR" && uv sync) fi ok "Prerequisites OK." } # ── 生成 Bay config.yaml(如不存在)──────────────────────────── ensure_bay_config() { local config_file="$BAY_DIR/config.yaml" if [[ -f "$config_file" ]]; then ok "Bay config.yaml already exists." return fi log "Generating Bay config.yaml for local development..." cat > "$config_file" << 'BAYCONFIG' # Bay Local Development Config (auto-generated by start-with-neo.sh) # For full reference see config.yaml.example server: host: "0.0.0.0" port: 8114 database: url: "sqlite+aiosqlite:///./bay.db" echo: false driver: type: docker image_pull_policy: if_not_present docker: socket: "unix:///var/run/docker.sock" connect_mode: host_port host_address: "127.0.0.1" publish_ports: true host_port: null network: null cargo: root_path: "/var/lib/bay/cargos" default_size_limit_mb: 1024 mount_path: "/workspace" # Security: auto-provision mode # Bay generates sk-bay-* key on first boot → credentials.json security: allow_anonymous: false profiles: - id: python-default description: "Standard Python sandbox" image: "ghcr.io/astrbotdevs/shipyard-neo-ship:latest" runtime_type: ship runtime_port: 8123 resources: cpus: 1.0 memory: "1g" capabilities: - filesystem - shell - python idle_timeout: 1800 env: {} gc: enabled: true run_on_startup: true interval_seconds: 300 idle_session: enabled: true expired_sandbox: enabled: true orphan_cargo: enabled: true orphan_container: enabled: false BAYCONFIG ok "Bay config.yaml created at $config_file" } # ── 拉取 Ship 镜像 ─────────────────────────────────────────── ensure_ship_image() { local image="ghcr.io/astrbotdevs/shipyard-neo-ship:latest" log "Checking Ship image: $image ..." if docker image inspect "$image" &>/dev/null 2>&1 || \ sudo docker image inspect "$image" &>/dev/null 2>&1; then ok "Ship image is available locally." else log "Pulling Ship image (this may take a while)..." if docker pull "$image" 2>/dev/null || sudo docker pull "$image" 2>/dev/null; then ok "Ship image pulled successfully." else warn "Failed to pull Ship image. Bay will try to pull it on first sandbox creation." fi fi } # ── 启动 Bay ────────────────────────────────────────────────── start_bay() { log "Starting Bay on :$BAY_PORT ..." (cd "$BAY_DIR" && BAY_DATA_DIR="$BAY_DIR" uv run uvicorn app.main:app \ --host "$BAY_HOST" \ --port "$BAY_PORT" \ --reload \ 2>&1 | sed "s/^/ ${CYAN}[bay]${NC} /") & BAY_PID=$! log "Bay started (PID $BAY_PID), waiting for health check..." # Wait for Bay to become healthy local max_wait=30 local waited=0 while [[ $waited -lt $max_wait ]]; do if curl -sf "http://127.0.0.1:$BAY_PORT/health" &>/dev/null; then ok "Bay is healthy at http://127.0.0.1:$BAY_PORT" return fi # Check if process is still alive if ! kill -0 "$BAY_PID" 2>/dev/null; then err "Bay process died unexpectedly. Check the output above." exit 1 fi sleep 1 waited=$((waited + 1)) done err "Bay did not become healthy within ${max_wait}s." err "It may still be starting — check http://127.0.0.1:$BAY_PORT/health" } # ── 读取 Bay 自动生成的凭证 ─────────────────────────────────── read_bay_credentials() { local cred_file="$BAY_DIR/credentials.json" # Wait briefly for credentials.json to appear (Bay writes it during startup) local max_wait=5 local waited=0 while [[ $waited -lt $max_wait ]]; do if [[ -f "$cred_file" ]]; then break fi sleep 1 waited=$((waited + 1)) done if [[ -f "$cred_file" ]]; then # Extract api_key using python (always available) — no jq dependency BAY_API_KEY=$(python3 -c " import json, sys try: d = json.load(open('$cred_file')) print(d.get('api_key', '')) except Exception: print('') " 2>/dev/null || echo "") if [[ -n "$BAY_API_KEY" ]]; then ok "Auto-provisioned API key loaded from credentials.json" else warn "credentials.json found but api_key is empty" fi else warn "credentials.json not found — Bay may be using an existing key or anonymous mode" warn "Check Bay logs above for the API key, or look at: $cred_file" fi } # ── 打印 AstrBot 配置提示 ──────────────────────────────────── print_astrbot_config_hint() { echo "" echo -e "${GREEN}════════════════════════════════════════════════════════════${NC}" echo -e "${GREEN} Shipyard Neo Bay is running at http://127.0.0.1:$BAY_PORT ${NC}" echo -e "${GREEN}════════════════════════════════════════════════════════════${NC}" echo "" if [[ -n "$BAY_API_KEY" ]]; then echo -e " ${CYAN}Bay API Key (auto-generated):${NC}" echo -e " ${YELLOW}$BAY_API_KEY${NC}" echo "" fi echo -e " ${CYAN}AstrBot Dashboard 配置指引:${NC}" echo -e " 1. AI 配置 → Agent Computer Use" echo -e " • Computer Use Runtime → ${YELLOW}沙箱${NC}" echo -e " • 沙箱环境驱动器 → ${YELLOW}Shipyard Neo${NC}" echo -e " • Shipyard Neo API Endpoint → ${YELLOW}http://127.0.0.1:$BAY_PORT${NC}" if [[ -n "$BAY_API_KEY" ]]; then echo -e " • Shipyard Neo Access Token → ${YELLOW}$BAY_API_KEY${NC}" else echo -e " • Shipyard Neo Access Token → ${YELLOW}(查看 Bay 日志获取 key)${NC}" fi echo -e " • Shipyard Neo Profile → ${YELLOW}python-default${NC}" echo "" } # ── 启动 AstrBot ────────────────────────────────────────────── start_astrbot() { log "Starting AstrBot..." cd "$ASTRBOT_DIR" uv run main.py } # ── 主流程 ──────────────────────────────────────────────────── main() { echo "" echo -e "${CYAN}╔══════════════════════════════════════════╗${NC}" echo -e "${CYAN}║ Shipyard Neo + AstrBot Quick Start ║${NC}" echo -e "${CYAN}╚══════════════════════════════════════════╝${NC}" echo "" check_prerequisites ensure_bay_config ensure_ship_image start_bay read_bay_credentials print_astrbot_config_hint start_astrbot } main "$@"