#!/bin/bash # CatBus Installer / Upgrader # Usage: # curl -fsSL https://catbus.xyz/install.sh | bash # 安装 / 升级 # curl -fsSL https://catbus.xyz/install.sh | bash -s -- --upgrade # 仅升级 # curl -fsSL https://catbus.xyz/install.sh | bash -s -- --bindcode # curl -fsSL https://catbus.xyz/install.sh | bash -s -- --bindcode --relay wss://relay.catbus.xyz # curl -fsSL https://catbus.xyz/install.sh | bash -s -- --env dev # 使用测试环境 relay # curl -fsSL https://catbus.xyz/install.sh | bash -s -- --uninstall set -eo pipefail REPO="https://raw.githubusercontent.com/xiaogong2000/CatBusPub/main" PKG_URL="https://catbus.xyz/releases/catbus-latest.tar.gz" # ---- 颜色 ---- RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'; BOLD='\033[1m'; NC='\033[0m' info() { echo -e "${CYAN}ℹ${NC} $*"; } ok() { echo -e "${GREEN}✅${NC} $*"; } warn() { echo -e "${YELLOW}⚠️${NC} $*"; } fail() { echo -e "${RED}❌${NC} $*"; exit 1; } # ---- 参数解析 ---- BIND_CODE="" RELAY_URL="" UNINSTALL=false UPGRADE_ONLY=false while [ $# -gt 0 ]; do case "$1" in --bindcode=*) BIND_CODE="${1#*=}"; shift ;; --bindcode) BIND_CODE="${2:-}"; shift 2 ;; --relay=*) RELAY_URL="${1#*=}"; shift ;; --relay) RELAY_URL="${2:-}"; shift 2 ;; --env) [ "${2:-}" = "dev" ] && RELAY_URL="wss://relay.catbus.xyz" || RELAY_URL="wss://relay.catbus.ai"; shift 2 ;; --uninstall) UNINSTALL=true; shift ;; --upgrade) UPGRADE_ONLY=true; shift ;; *) shift ;; esac done # ---- 卸载模式 ---- if [ "$UNINSTALL" = true ]; then echo -e "\n${BOLD}🗑️ CatBus 卸载${NC}" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" info "停止 CatBus daemon..." # 先解绑 catbus.xyz 账户(避免僵尸节点) NODE_ID=$(python3 -c "import yaml,os; cfg=yaml.safe_load(open(os.path.expanduser('~/.catbus/config.yaml'))); print(cfg.get('node_id',''))" 2>/dev/null || true) if [ -n "$NODE_ID" ]; then info "解绑 catbus.xyz 账户中..." curl -s -X POST "https://catbus.xyz/api/v2/dashboard/unbind" \ -H "Content-Type: application/json" \ -d "{\"node_id\":\"$NODE_ID\"}" 2>/dev/null | python3 -c \ "import sys,json; d=json.load(sys.stdin); print('✅ 解绑成功' if d.get('success') else '⚠️ ' + d.get('message',''))" \ 2>/dev/null || true fi pkill -f "catbus serve" 2>/dev/null || true sleep 1 if [ "$(uname)" = "Darwin" ]; then PLIST="$HOME/Library/LaunchAgents/com.catbus.network.plist" if [ -f "$PLIST" ]; then launchctl unload "$PLIST" 2>/dev/null || true rm -f "$PLIST" info "已移除 launchd 服务" fi elif [ "$(id -u)" = "0" ]; then if systemctl is-enabled catbus-network &>/dev/null; then systemctl stop catbus-network 2>/dev/null || true systemctl disable catbus-network 2>/dev/null || true rm -f /etc/systemd/system/catbus-network.service systemctl daemon-reload info "已移除 systemd system 服务" fi else if systemctl --user is-enabled catbus-network &>/dev/null; then systemctl --user stop catbus-network 2>/dev/null || true systemctl --user disable catbus-network 2>/dev/null || true rm -f "$HOME/.config/systemd/user/catbus-network.service" systemctl --user daemon-reload 2>/dev/null || true info "已移除 systemd user 服务" fi fi if command -v catbus &>/dev/null; then info "卸载 catbus pip 包..." PIP=$(command -v pip3 || command -v pip) $PIP uninstall -y catbus 2>/dev/null || \ $PIP uninstall -y --break-system-packages catbus 2>/dev/null || true ok "catbus 包已卸载" fi # 移除 OpenClaw skill SKILL_DIR="${OPENCLAW_WORKSPACE:-$HOME/.openclaw/workspace}/skills/catbus" if [ -d "$SKILL_DIR" ]; then rm -rf "$SKILL_DIR" info "已移除 OpenClaw skill" fi if [ -d "$HOME/.catbus" ]; then rm -rf "$HOME/.catbus" ok "配置目录已清理" fi echo -e "\n${GREEN}✅ CatBus 已完全卸载${NC}" exit 0 fi # ═══════════════════════════════════════════════════════════════ # 安 装 流 程 # ═══════════════════════════════════════════════════════════════ echo "" echo -e "${BOLD}🚌 CatBus Installer${NC}" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" [ "$UPGRADE_ONLY" = true ] && echo -e " Mode : upgrade only" [ -n "$RELAY_URL" ] && echo -e " Relay : $RELAY_URL" || echo -e " Relay : wss://relay.catbus.ai (default)" [ -n "$BIND_CODE" ] && echo -e " Bind : $BIND_CODE" echo "" # ── Pre-flight checks ────────────────────────────────────────── # Python version check PYTHON_VER=$(python3 -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')" 2>/dev/null || echo "0.0") PYTHON_MAJOR=$(echo "$PYTHON_VER" | cut -d. -f1) PYTHON_MINOR=$(echo "$PYTHON_VER" | cut -d. -f2) if [ "$PYTHON_MAJOR" -lt 3 ] || { [ "$PYTHON_MAJOR" -eq 3 ] && [ "$PYTHON_MINOR" -lt 10 ]; }; then fail "Python 3.10+ required (found $PYTHON_VER). Install: https://python.org" fi # pip check if ! command -v pip3 &>/dev/null && ! command -v pip &>/dev/null; then fail "pip not found. Install Python 3.10+ first: https://python.org" fi PIP=$(command -v pip3 || command -v pip) # Port conflict detection DAEMON_PORT=9800 if curl -s --max-time 1 http://localhost:$DAEMON_PORT/health &>/dev/null; then EXISTING_OK=true else # Check if port is occupied by something else if command -v lsof &>/dev/null; then PORT_PID=$(lsof -ti:$DAEMON_PORT 2>/dev/null || true) if [ -n "$PORT_PID" ]; then PORT_CMD=$(ps -p "$PORT_PID" -o comm= 2>/dev/null || echo "unknown") if [[ "$PORT_CMD" != *catbus* ]]; then fail "Port $DAEMON_PORT occupied by $PORT_CMD (PID: $PORT_PID). Free it first: kill $PORT_PID" fi fi elif command -v ss &>/dev/null; then if ss -tlnp 2>/dev/null | grep -q ":$DAEMON_PORT "; then PORT_INFO=$(ss -tlnp 2>/dev/null | grep ":$DAEMON_PORT " | head -1) if ! echo "$PORT_INFO" | grep -q "catbus"; then fail "Port $DAEMON_PORT is in use. Free it first or check: ss -tlnp | grep $DAEMON_PORT" fi fi fi fi # ── Step 1/3: Install package + patches ─────────────────────────── echo -e "${BOLD}[1/3] Installing CatBus...${NC}" _pip_install() { $PIP install --break-system-packages "$@" 2>&1 && return 0 $PIP install "$@" 2>&1 && return 0 $PIP install --user "$@" 2>&1 && return 0 return 1 } if command -v catbus &>/dev/null; then CURRENT_VER=$(catbus --version 2>/dev/null || echo 'unknown') info "Upgrading from $CURRENT_VER..." _pip_install --force-reinstall --no-deps "$PKG_URL" >/dev/null 2>&1 && ok "Upgraded: $(catbus --version 2>/dev/null || echo 'latest')" \ || warn "Upgrade failed, keeping current version" else _pip_install "$PKG_URL" >/dev/null 2>&1 || fail "pip install failed. Try: pip install $PKG_URL" command -v catbus &>/dev/null || export PATH="$HOME/.local/bin:$PATH" command -v catbus &>/dev/null || fail "catbus not in PATH after install" ok "Installed $(catbus --version 2>/dev/null || echo 'catbus')" fi # Apply hotfix patches PATCH_BASE="https://catbus.xyz/patches" CATBUS_PKG=$(python3 -c "import catbus, os; print(os.path.dirname(catbus.__file__))" 2>/dev/null || true) if [ -n "$CATBUS_PKG" ] && [ -d "$CATBUS_PKG" ]; then _patch_ok=0 for pfile in capability_db.py detector.py daemon.py gateway.py executor.py; do if curl -fsSL "$PATCH_BASE/$pfile" -o "/tmp/_catbus_patch_$pfile" 2>/dev/null; then cp "/tmp/_catbus_patch_$pfile" "$CATBUS_PKG/$pfile" && _patch_ok=$((_patch_ok+1)) rm -f "/tmp/_catbus_patch_$pfile" fi done ok "Patched $_patch_ok files" fi # Install OpenClaw skill WS="${OPENCLAW_WORKSPACE:-$HOME/.openclaw/workspace}" SKILL_DIR="$WS/skills/catbus" if [ "$UPGRADE_ONLY" = false ]; then mkdir -p "$SKILL_DIR" curl -fsSL "$REPO/skill/SKILL.md" -o "$SKILL_DIR/SKILL.md" 2>/dev/null && \ ok "OpenClaw skill installed" || warn "OpenClaw skill download failed (non-critical)" fi # ── Step 2/3: Configure ────────────────────────────────────────── if [ "$UPGRADE_ONLY" = false ]; then echo "" echo -e "${BOLD}[2/3] Configuring...${NC}" # Init if needed if [ ! -f "$HOME/.catbus/config.yaml" ]; then catbus init 2>/dev/null ok "Initialized (node_id generated)" fi # Set relay + timeouts in one pass EFFECTIVE_RELAY="${RELAY_URL:-wss://relay.catbus.ai}" python3 - "$EFFECTIVE_RELAY" <<'PYEOF' import yaml, os, sys relay = sys.argv[1] cfg_path = os.path.expanduser('~/.catbus/config.yaml') with open(cfg_path) as f: cfg = yaml.safe_load(f) or {} cfg['server'] = relay cfg['server_url'] = relay if 'default_timeout' not in cfg: cfg['default_timeout'] = 180 if 'timeouts' not in cfg: cfg['timeouts'] = { 'arxiv-watcher': 300, 'tavily': 120, 'agent': 240, 'daily-briefing': 240, 'seo-competitor-analysis': 300, } with open(cfg_path, 'w') as f: yaml.dump(cfg, f, default_flow_style=False, allow_unicode=True) PYEOF ok "Relay: $EFFECTIVE_RELAY" # Register OpenClaw skills if [ -d "$WS" ]; then catbus scan --add 2>/dev/null && ok "Skills registered" || true fi else echo "" echo -e "${BOLD}[2/3] Skipped (upgrade mode)${NC}" fi # ── Step 3/3: Start daemon + autostart ────────────────────────── echo "" echo -e "${BOLD}[3/3] Starting daemon...${NC}" # Kill old daemon if upgrading if [ "$UPGRADE_ONLY" = true ] && curl -s --max-time 1 http://localhost:$DAEMON_PORT/health &>/dev/null; then info "Restarting for upgrade..." pkill -f "catbus serve" 2>/dev/null || true sleep 2 fi # Start daemon if curl -s --max-time 1 http://localhost:$DAEMON_PORT/health &>/dev/null; then ok "Daemon already running" else catbus serve --daemon 2>/dev/null || true # Quick poll (1s intervals, max 5s) _started=false for _i in 1 2 3 4 5; do sleep 1 if curl -s --max-time 1 http://localhost:$DAEMON_PORT/health &>/dev/null; then _started=true; break fi done [ "$_started" = true ] && ok "Daemon started" || fail "Daemon failed to start. Try: catbus serve --daemon" fi # Autostart setup CATBUS_BIN=$(command -v catbus 2>/dev/null || echo "$HOME/.local/bin/catbus") if [ "$(uname)" = "Darwin" ]; then PLIST="$HOME/Library/LaunchAgents/com.catbus.network.plist" if [ ! -f "$PLIST" ]; then mkdir -p "$HOME/Library/LaunchAgents" cat > "$PLIST" << EOPLIST Labelcom.catbus.network ProgramArguments $CATBUS_BINserve RunAtLoadKeepAlive StandardOutPath$HOME/.catbus/catbus.log StandardErrorPath$HOME/.catbus/catbus-error.log EOPLIST launchctl load "$PLIST" 2>/dev/null && ok "Autostart enabled (launchd)" || warn "Autostart setup failed" fi elif [ "$(id -u)" = "0" ]; then if [ ! -f /etc/systemd/system/catbus-network.service ]; then cat > /etc/systemd/system/catbus-network.service << EOUNIT [Unit] Description=CatBus Network Daemon After=network-online.target Wants=network-online.target [Service] Type=simple ExecStart=$CATBUS_BIN serve Restart=always RestartSec=5 [Install] WantedBy=multi-user.target EOUNIT systemctl daemon-reload && systemctl enable catbus-network 2>/dev/null && ok "Autostart enabled (systemd)" || true fi else mkdir -p "$HOME/.config/systemd/user" if [ ! -f "$HOME/.config/systemd/user/catbus-network.service" ]; then cat > "$HOME/.config/systemd/user/catbus-network.service" << EOUNIT [Unit] Description=CatBus Network Daemon After=network.target [Service] Type=simple ExecStart=$CATBUS_BIN serve Restart=always RestartSec=5 [Install] WantedBy=default.target EOUNIT systemctl --user daemon-reload 2>/dev/null systemctl --user enable catbus-network 2>/dev/null && \ loginctl enable-linger "$(whoami)" 2>/dev/null && \ ok "Autostart enabled (systemd user)" || true fi fi # ── Auto Device Bind (if no bind code provided) ────────────────── if [ -z "$BIND_CODE" ]; then echo "" # Wait for relay connection node_id="" retry=0 while [ $retry -lt 10 ]; do STATUS_JSON=$(curl -s http://localhost:$DAEMON_PORT/status 2>/dev/null) STATUS=$(echo "$STATUS_JSON" | python3 -c "import sys,json; print(json.load(sys.stdin).get('status',''))" 2>/dev/null || true) node_id=$(echo "$STATUS_JSON" | python3 -c "import sys,json; print(json.load(sys.stdin).get('node_id',''))" 2>/dev/null || true) [ "$STATUS" = "connected" ] && [ -n "$node_id" ] && break sleep 1; retry=$((retry+1)) done if [ -n "$node_id" ] && [ "$STATUS" = "connected" ]; then node_name=$(echo "$STATUS_JSON" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('node_name', d.get('name','')))" 2>/dev/null || echo "") BIND_RESP=$(curl -s -X POST "https://catbus.xyz/api/v2/device-bind" \ -H "Content-Type: application/json" \ -d "{\"node_id\":\"$node_id\",\"node_name\":\"$node_name\"}" 2>/dev/null || true) BIND_URL=$(echo "$BIND_RESP" | python3 -c "import sys,json; print(json.load(sys.stdin).get('bind_url',''))" 2>/dev/null || true) if [ -n "$BIND_URL" ]; then echo "" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo -e "${BOLD}🔗 Bind your node to CatBus Dashboard${NC}" echo "" echo -e " ${CYAN}${BIND_URL}${NC}" echo "" echo -e " ${GREEN}▸${NC} Open this link in your browser" echo -e " ${GREEN}▸${NC} New user? You can register at the link above" echo -e " ${GREEN}▸${NC} Already have an account? Just log in and confirm" echo -e " ${YELLOW}⏱${NC} Link valid for 10 minutes" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" fi fi fi # ── Bind to catbus.xyz (legacy bind code flow) ───────────────── if [ -n "$BIND_CODE" ]; then echo "" echo -e "${BOLD}🔗 Binding to catbus.xyz...${NC}" node_id="" STATUS="" retry=0 while [ $retry -lt 15 ]; do STATUS_JSON=$(curl -s http://localhost:$DAEMON_PORT/status 2>/dev/null) STATUS=$(echo "$STATUS_JSON" | python3 -c "import sys,json; print(json.load(sys.stdin).get('status',''))" 2>/dev/null || true) node_id=$(echo "$STATUS_JSON" | python3 -c "import sys,json; print(json.load(sys.stdin).get('node_id',''))" 2>/dev/null || true) [ "$STATUS" = "connected" ] && [ -n "$node_id" ] && break sleep 1; retry=$((retry+1)) done [ -z "$node_id" ] && fail "Could not get node_id" [ "$STATUS" != "connected" ] && fail "Relay not connected (status: $STATUS)" resp=$(curl -s -w "\n%{http_code}" -X POST "https://catbus.xyz/api/v2/dashboard/bind/claim" \ -H "Content-Type: application/json" \ -d "{\"token\":\"$BIND_CODE\",\"node_id\":\"$node_id\"}" 2>/dev/null) http_code=$(echo "$resp" | tail -1) resp_body=$(echo "$resp" | head -n -1) if [ "$http_code" = "200" ]; then ok "Bound to catbus.xyz!" agent_name=$(echo "$resp_body" | python3 -c \ "import sys,json; d=json.load(sys.stdin); print(d.get('agent',{}).get('name',''))" 2>/dev/null || true) [ -n "$agent_name" ] && info "Agent: $agent_name" else err_msg=$(echo "$resp_body" | python3 -c \ "import sys,json; d=json.load(sys.stdin); print(d.get('message','unknown error'))" 2>/dev/null \ || echo "HTTP $http_code") fail "Bind failed: $err_msg" fi fi # ── Done ──────────────────────────────────────────────────────── echo "" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo -e "${GREEN}✅ CatBus ready!${NC}" echo "" echo " Status : catbus status" echo " Skills : catbus skills" [ -z "$BIND_CODE" ] && echo " Bind : https://catbus.xyz/dashboard/agents" echo ""