Files

448 lines
15 KiB
Bash
Executable File

#!/bin/sh
# ============================================================================
# hermes-env — Hermes Agent environment installation management script
# Usage: hermes-env setup | check | upgrade
# Environment variables: HERMES_VERSION — specify version to install
# ============================================================================
# ── Version and path constants ─────────────────────────────────────────────
HERMES_BASE="/opt/hermes"
HERMES_VENV="${HERMES_BASE}/venv"
HERMES_DATA="${HERMES_BASE}/data"
HERMES_BIN="${HERMES_VENV}/bin/hermes"
PYTHON_BIN="${HERMES_VENV}/bin/python3"
HERMES_CONFIG="/root/.hermes/config.yaml"
# PyPI mirrors
HERMES_PYPI_CN="https://mirrors.aliyun.com/pypi/simple/"
HERMES_PYPI_OFFICIAL="https://pypi.org/simple/"
HERMES_PYPI_TSINGHUA="https://pypi.tuna.tsinghua.edu.cn/simple/"
# Default API endpoint
HERMES_DEFAULT_API="https://api.boos.lat/v1"
HERMES_DEFAULT_MODEL="opus4.6"
export PATH="${HERMES_VENV}/bin:$PATH"
# ── Logging tools ──────────────────────────────────────────────────────────
log_ok() { echo " [✓] $*"; }
log_warn() { echo " [!] $*"; }
log_err() { echo " [✗] $*"; }
log_step() { echo ""; echo "=== $* ==="; }
# ── OverlayFS fix (iStoreOS Docker /opt conflict) ─────────────────────────
_fix_opt() {
mkdir -p /opt/hermes/.probe 2>/dev/null && { rmdir /opt/hermes/.probe 2>/dev/null; return 0; }
[ -d /overlay/upper/opt ] && mkdir -p /overlay/upper/opt/hermes 2>/dev/null && \
mount --bind /overlay/upper/opt /opt 2>/dev/null && return 0
return 1
}
_fix_opt
# ── Basic tools ────────────────────────────────────────────────────────────
ensure_dir() { [ -d "$1" ] && return 0; mkdir -p "$1" 2>/dev/null || { log_err "Cannot create directory: $1"; return 1; }; }
# Check available disk space
_df_avail() {
local p="$1"
while [ -n "$p" ] && [ "$p" != "/" ]; do
[ -e "$p" ] && break
p="${p%/*}"
done
[ -z "$p" ] && p="/"
df -m "$p" 2>/dev/null | tail -1 | awk '{print $(NF-2)}'
}
check_space() {
local path="$1" need_mb="$2" label="$3"
local avail
avail=$(_df_avail "$path")
[ -z "$avail" ] && return 0
if [ "$avail" -lt "$need_mb" ]; then
log_warn "${label} available space insufficient: ${avail}MB < ${need_mb}MB recommended"
else
log_ok "${label} available space: ${avail}MB"
fi
}
# Download: wget first, curl fallback
_download() {
local url="$1" out="$2"
if command -v wget >/dev/null 2>&1; then
wget -q --no-check-certificate -O "$out" "$url" 2>/dev/null
elif command -v curl >/dev/null 2>&1; then
curl -fsSL --connect-timeout 15 --max-time 120 -o "$out" "$url" 2>/dev/null
else
log_err "No download tool available (wget/curl)"; return 1
fi
}
# Try multiple mirrors, return first success
_download_first() {
local out="$1"; shift
for url in "$@"; do
[ -z "$url" ] && continue
if _download "$url" "$out"; then
local sz; sz=$(wc -c < "$out" 2>/dev/null || echo 0)
[ "$sz" -gt 1000 ] && return 0
fi
rm -f "$out"
done
return 1
}
# ── Architecture detection ─────────────────────────────────────────────────
detect_arch() {
case "$(uname -m)" in
x86_64) echo "x86_64" ;;
aarch64) echo "aarch64" ;;
armv7l) echo "armv7l" ;;
armv6l) echo "armv6l" ;;
*) log_err "Unsupported architecture: $(uname -m)"; exit 1 ;;
esac
}
detect_libc() {
[ -f /etc/openwrt_release ] && echo "musl" && return
for f in /lib/ld-musl-*.so.1; do [ -f "$f" ] && echo "musl" && return; done
echo "glibc"
}
# ── Install Python 3 ──────────────────────────────────────────────────────
install_python() {
# Check if system python3 is available
if command -v python3 >/dev/null 2>&1; then
local ver
ver=$(python3 --version 2>/dev/null | awk '{print $2}')
log_ok "System Python3 v${ver} found"
# Check if it's recent enough (3.8+)
local major minor
major=$(echo "$ver" | cut -d. -f1)
minor=$(echo "$ver" | cut -d. -f2)
if [ "$major" -ge 3 ] && [ "$minor" -ge 8 ] 2>/dev/null; then
log_ok "Python3 v${ver} meets requirements (>= 3.8)"
return 0
fi
log_warn "Python3 v${ver} is too old, need >= 3.8"
fi
log_step "Installing Python 3"
# Try opkg first
opkg update >/dev/null 2>&1 || true
opkg install python3 python3-pip python3-venv 2>/dev/null | tail -5 || true
if command -v python3 >/dev/null 2>&1; then
log_ok "Python3 installed via opkg: $(python3 --version 2>/dev/null)"
return 0
fi
# Fallback: try to install minimal python3
opkg install python3-base python3-light 2>/dev/null | tail -5 || true
if command -v python3 >/dev/null 2>&1; then
log_ok "Python3 (minimal) installed: $(python3 --version 2>/dev/null)"
# Also need pip
opkg install python3-pip 2>/dev/null || true
return 0
fi
log_err "Python3 installation failed"
log_err "Please install manually: opkg install python3 python3-pip python3-venv"
return 1
}
# ── Create virtual environment ─────────────────────────────────────────────
create_venv() {
log_step "Creating Python virtual environment"
ensure_dir "$HERMES_BASE" || exit 1
# Remove old venv if exists
if [ -d "$HERMES_VENV" ]; then
log_warn "Removing old virtual environment..."
rm -rf "$HERMES_VENV"
fi
# Create venv using system python3
local sys_python
sys_python=$(command -v python3 2>/dev/null)
if [ -z "$sys_python" ]; then
log_err "python3 not found in PATH"
return 1
fi
"$sys_python" -m venv "$HERMES_VENV" 2>/dev/null
if [ ! -x "$PYTHON_BIN" ]; then
# Try without -m venv (older python)
log_warn "python3 -m venv failed, trying virtualenv..."
pip3 install virtualenv 2>/dev/null || true
"$sys_python" -m virtualenv "$HERMES_VENV" 2>/dev/null
fi
if [ ! -x "$PYTHON_BIN" ]; then
log_err "Failed to create virtual environment"
return 1
fi
log_ok "Virtual environment created at $HERMES_VENV"
# Upgrade pip
log_ok "Upgrading pip..."
"$PYTHON_BIN" -m pip install --upgrade pip 2>/dev/null | tail -2 || true
log_ok "Python: $("$PYTHON_BIN" --version 2>/dev/null)"
}
# ── Install Hermes Agent ───────────────────────────────────────────────────
install_hermes() {
log_step "Installing Hermes Agent"
# Check if already installed
if [ -x "$HERMES_BIN" ]; then
local cur
cur=$("$HERMES_BIN" --version 2>/dev/null | head -1 | tr -d '[:space:]')
log_ok "Hermes Agent v${cur} already installed"
echo " To reinstall: hermes-env setup --force"
return 0
fi
ensure_dir "$HERMES_VENV" || return 1
# Try PyPI mirrors in order
local install_ok=false
for mirror in "$HERMES_PYPI_CN" "$HERMES_PYPI_TSINGHUA" "$HERMES_PYPI_OFFICIAL"; do
log_warn "Trying: ${mirror}"
if "$PYTHON_BIN" -m pip install hermes-agent \
-i "$mirror" \
--trusted-host "$(echo "$mirror" | sed 's|https://||;s|/.*||')" \
2>&1 | tail -10; then
install_ok=true
break
fi
done
if [ "$install_ok" = "false" ] || [ ! -x "$HERMES_BIN" ]; then
# Try without mirror flags
log_warn "Mirror install failed, trying default PyPI..."
"$PYTHON_BIN" -m pip install hermes-agent 2>&1 | tail -10 || true
[ -x "$HERMES_BIN" ] && install_ok=true
fi
if [ "$install_ok" = "false" ]; then
log_err "Hermes Agent installation failed"
log_err "Please check network connection and try again"
return 1
fi
log_ok "Hermes Agent installed: $("$HERMES_BIN" --version 2>/dev/null | head -1)"
}
# ── Initialize Hermes configuration ────────────────────────────────────────
init_hermes() {
log_step "Initializing Hermes configuration"
mkdir -p /root/.hermes 2>/dev/null
# Generate auth token
local token
token=$(uci -q get hermes.main.token 2>/dev/null || echo "")
if [ -z "$token" ]; then
token=$(head -c 24 /dev/urandom | hexdump -e '24/1 "%02x"' 2>/dev/null || openssl rand -hex 24 2>/dev/null || echo "hermes_$(date +%s)")
uci set hermes.main.token="$token" 2>/dev/null || true
uci commit hermes 2>/dev/null || true
fi
# Generate PTY token
local pty_token
pty_token=$(uci -q get hermes.main.pty_token 2>/dev/null || echo "")
if [ -z "$pty_token" ]; then
pty_token=$(head -c 16 /dev/urandom | hexdump -e '16/1 "%02x"' 2>/dev/null || openssl rand -hex 16 2>/dev/null || echo "pty_$(date +%s)")
uci set hermes.main.pty_token="$pty_token" 2>/dev/null || true
uci commit hermes 2>/dev/null || true
fi
# Write default config.yaml
local port bind api_endpoint api_key model
port=$(uci -q get hermes.main.port || echo "3000")
bind=$(uci -q get hermes.main.bind || echo "lan")
api_endpoint=$(uci -q get hermes.main.api_endpoint || echo "$HERMES_DEFAULT_API")
api_key=$(uci -q get hermes.main.api_key || echo "")
model=$(uci -q get hermes.main.model || echo "$HERMES_DEFAULT_MODEL")
local listen_host="0.0.0.0"
[ "$bind" = "loopback" ] && listen_host="127.0.0.1"
cat > /root/.hermes/config.yaml << YAMLEOF
# Hermes Agent configuration
# Initialized by luci-app-hermes on $(date '+%Y-%m-%d %H:%M:%S')
gateway:
port: ${port}
bind: ${listen_host}
auth:
mode: token
token: ${token}
api:
endpoint: "${api_endpoint}"
key: "${api_key}"
model:
primary: "${model}"
tools:
profile: coding
update:
checkOnStart: false
YAMLEOF
chmod 600 /root/.hermes/config.yaml 2>/dev/null || true
log_ok "Configuration written to /root/.hermes/config.yaml"
}
# ── Command: setup ─────────────────────────────────────────────────────────
cmd_setup() {
check_space /opt 100 "Install directory (/opt)"
check_space /tmp 100 "Temp directory (/tmp)"
# Already installed?
if [ -x "$HERMES_BIN" ]; then
echo "╔══════════════════════════════════════════════════════════════╗"
echo "║ [!] Hermes Agent already installed ║"
echo "║ To reinstall, run: hermes-env setup --force ║"
echo "╚══════════════════════════════════════════════════════════════╝"
echo " Hermes: $("$HERMES_BIN" --version 2>/dev/null | head -1)"
exit 0
fi
echo "╔══════════════════════════════════════════════════════════════╗"
echo "║ Hermes Agent Environment Setup ║"
echo "╚══════════════════════════════════════════════════════════════╝"
echo " Architecture: $(uname -m)"
echo " Libc: $(detect_libc)"
echo ""
install_python || exit 1
create_venv || exit 1
install_hermes || exit 1
init_hermes || exit 1
echo ""
echo "╔══════════════════════════════════════════════════════════════╗"
echo "║ Setup Complete! ║"
echo "╚══════════════════════════════════════════════════════════════╝"
echo " Hermes: $("$HERMES_BIN" --version 2>/dev/null | head -1)"
echo " Python: $("$PYTHON_BIN" --version 2>/dev/null)"
echo " Config: /root/.hermes/config.yaml"
echo ""
echo " Next steps:"
echo " 1. Edit /etc/config/hermes to set your API key"
echo " 2. Run: /etc/init.d/hermes enable"
echo " 3. Run: /etc/init.d/hermes start"
echo ""
echo " Or use the LuCI web interface to configure and start."
}
# ── Command: check ─────────────────────────────────────────────────────────
cmd_check() {
echo "=== Hermes Environment Check ==="
if [ -x "$PYTHON_BIN" ]; then
echo "Python: $("$PYTHON_BIN" --version 2>/dev/null)"
else
echo "Python: Not installed (virtual env)"
fi
if command -v python3 >/dev/null 2>&1; then
echo "System Py: $(python3 --version 2>/dev/null)"
else
echo "System Py: Not found"
fi
if [ -x "$HERMES_BIN" ]; then
echo "Hermes: $("$HERMES_BIN" --version 2>/dev/null | head -1)"
else
echo "Hermes: Not installed"
fi
echo "VENV: $HERMES_VENV"
echo "Config: $HERMES_CONFIG"
if [ -f "$HERMES_CONFIG" ]; then
echo "Config OK: Yes"
else
echo "Config OK: No (will be created on start)"
fi
}
# ── Command: upgrade ───────────────────────────────────────────────────────
cmd_upgrade() {
if [ ! -x "$HERMES_BIN" ]; then
log_err "Hermes Agent not installed. Run: hermes-env setup"
exit 1
fi
log_step "Upgrading Hermes Agent"
local upgrade_ok=false
for mirror in "$HERMES_PYPI_CN" "$HERMES_PYPI_TSINGHUA" "$HERMES_PYPI_OFFICIAL"; do
log_warn "Trying: ${mirror}"
if "$PYTHON_BIN" -m pip install --upgrade hermes-agent \
-i "$mirror" \
--trusted-host "$(echo "$mirror" | sed 's|https://||;s|/.*||')" \
2>&1 | tail -10; then
upgrade_ok=true
break
fi
done
if [ "$upgrade_ok" = "false" ]; then
log_warn "Mirror install failed, trying default PyPI..."
"$PYTHON_BIN" -m pip install --upgrade hermes-agent 2>&1 | tail -10 || true
[ -x "$HERMES_BIN" ] && upgrade_ok=true
fi
if [ "$upgrade_ok" = "true" ] && [ -x "$HERMES_BIN" ]; then
log_ok "Hermes upgraded: $("$HERMES_BIN" --version 2>/dev/null | head -1)"
else
log_err "Upgrade failed"
exit 1
fi
}
# ── Command: uninstall ─────────────────────────────────────────────────────
cmd_uninstall() {
log_step "Uninstalling Hermes Agent"
/etc/init.d/hermes stop 2>/dev/null || true
/etc/init.d/hermes disable 2>/dev/null || true
uci set hermes.main.enabled=0 2>/dev/null || true
uci commit hermes 2>/dev/null || true
[ -d "$HERMES_BASE" ] && { rm -rf "$HERMES_BASE" 2>/dev/null || true; }
[ -d "/root/.hermes" ] && { rm -rf "/root/.hermes" 2>/dev/null || true; }
log_ok "Hermes Agent uninstalled"
}
# ── Main entry ─────────────────────────────────────────────────────────────
case "${1:-setup}" in
setup) cmd_setup ;;
check) cmd_check ;;
upgrade) cmd_upgrade ;;
uninstall) cmd_uninstall ;;
*)
echo "Usage: hermes-env {setup|check|upgrade|uninstall}"
echo ""
echo " setup Install Python + Hermes Agent (default)"
echo " check Check installation status"
echo " upgrade Upgrade Hermes Agent to latest version"
echo " uninstall Remove all Hermes files"
exit 1
;;
esac