#!/usr/bin/env bash # HyperNode-AI 部署管理器 — 一键部署 / 卸载 / 更新 # # 新服务器: # curl -fsSL https://git.bz121.com/dekun/HyperNode-AI/raw/branch/main/deploy/manage.sh | bash # # 已安装: # bash /opt/hypernode/deploy/manage.sh # set -e if [[ -n "${BASH_VERSION:-}" ]]; then set -o pipefail fi INSTALL_ROOT="${INSTALL_ROOT:-/opt/hypernode}" DATA_DIR="${DATA_DIR:-/var/lib/hypernode}" GIT_URL="${GIT_URL:-https://git.bz121.com/dekun/HyperNode-AI.git}" GIT_BRANCH="${GIT_BRANCH:-main}" APP_USER="hypernode" SERVICE_NAME="hypernode" BIND_HOST="0.0.0.0" BIND_PORT="33333" ENV_FILE="${INSTALL_ROOT}/.env" GENERATED_ADMIN_PASSWORD="" _script_src="${BASH_SOURCE[0]:-}" if [[ -n "${_script_src}" && -f "${_script_src}" ]]; then REPO_ROOT="$(cd "$(dirname "${_script_src}")/.." && pwd)" else REPO_ROOT="" fi unset _script_src set -u log() { printf '\n==> %s\n' "$*" } ask() { local value="" if [[ ! -r /dev/tty ]]; then echo "请在终端中运行。通过 curl 管道执行时,也需要当前会话有终端。" >&2 exit 1 fi if ! read -r -p "$1" value &2 echo "或:curl -fsSL https://git.bz121.com/dekun/HyperNode-AI/raw/branch/main/deploy/manage.sh | sudo bash" >&2 exit 1 fi } ensure_git() { if command -v git >/dev/null 2>&1; then return 0 fi if ! command -v apt-get >/dev/null 2>&1; then echo "未找到 git,且当前系统没有 apt-get。" >&2 exit 1 fi export DEBIAN_FRONTEND=noninteractive apt-get update -qq apt-get install -y git ca-certificates curl } bootstrap_repo() { if repo_ready "${INSTALL_ROOT}"; then REPO_ROOT="${INSTALL_ROOT}" return 0 fi if [[ -n "${REPO_ROOT}" ]] && repo_ready "${REPO_ROOT}"; then return 0 fi echo "HyperNode-AI 部署管理器 — 首次自举" echo "将克隆到: ${INSTALL_ROOT}" require_root ensure_git if [[ -e "${INSTALL_ROOT}" ]]; then echo "错误: ${INSTALL_ROOT} 已存在,但不是有效的 HyperNode-AI 目录。" >&2 echo "请先处理该目录,或设置 INSTALL_ROOT 指向其他路径。" >&2 exit 1 fi git clone --branch "${GIT_BRANCH}" "${GIT_URL}" "${INSTALL_ROOT}" exec bash "${INSTALL_ROOT}/deploy/manage.sh" "$@" = (3, 10) else 1)' } ensure_user() { if ! id -u "${APP_USER}" >/dev/null 2>&1; then useradd --system --home-dir "${INSTALL_ROOT}" --shell /usr/sbin/nologin "${APP_USER}" fi } ensure_install_tree() { if repo_ready "${INSTALL_ROOT}"; then return 0 fi if [[ -n "${REPO_ROOT}" && "${REPO_ROOT}" != "${INSTALL_ROOT}" ]] && repo_ready "${REPO_ROOT}"; then mkdir -p "${INSTALL_ROOT}" rsync -a --delete \ --exclude '.venv/' \ --exclude '.env' \ --exclude '__pycache__/' \ --exclude '*.db' \ "${REPO_ROOT}/" "${INSTALL_ROOT}/" return 0 fi ensure_git git clone --branch "${GIT_BRANCH}" "${GIT_URL}" "${INSTALL_ROOT}" } write_env() { mkdir -p "${DATA_DIR}" if [[ -f "${ENV_FILE}" ]]; then log "保留已有配置 ${ENV_FILE}" return 0 fi local jwt_secret admin_password jwt_secret="$(openssl rand -hex 32)" admin_password="$(openssl rand -hex 12)" GENERATED_ADMIN_PASSWORD="${admin_password}" log "写入 ${ENV_FILE}" cat >"${ENV_FILE}" <"/etc/systemd/system/${SERVICE_NAME}.service" </dev/null 2>&1; then ready=1 break fi sleep 1 done if [[ "${ready}" -ne 1 ]]; then echo "服务未在 40 秒内就绪。状态和最近日志:" >&2 systemctl status "${SERVICE_NAME}.service" --no-pager >&2 || true journalctl -u "${SERVICE_NAME}.service" -n 80 --no-pager >&2 || true return 1 fi echo "健康检查通过:http://127.0.0.1:${BIND_PORT}/health" } verify_app() { log "检查程序能否启动" if ! /usr/sbin/runuser -u "${APP_USER}" -- bash -c "cd '${INSTALL_ROOT}' && exec '${INSTALL_ROOT}/.venv/bin/python' -c 'import app.main'"; then echo "程序导入失败,没有重启服务。请把上面的报错发出来。" >&2 return 1 fi } cmd_deploy() { log "一键部署" install_packages ensure_user ensure_install_tree write_env install_python_deps fix_permissions write_service verify_app || return 1 systemctl enable "${SERVICE_NAME}.service" systemctl restart "${SERVICE_NAME}.service" wait_until_ready || return 1 log "部署完成" echo "监听地址:${BIND_HOST}:${BIND_PORT}" echo "健康检查:curl http://127.0.0.1:${BIND_PORT}/health" echo "控制台:http://<服务器 IP>:${BIND_PORT}/" echo "接口文档:http://<服务器 IP>:${BIND_PORT}/docs" if [[ -n "${GENERATED_ADMIN_PASSWORD}" ]]; then echo "管理员账号:admin" echo "管理员密码:${GENERATED_ADMIN_PASSWORD}" echo "密码写在 ${ENV_FILE},请立刻记下。" fi echo "程序目录:${INSTALL_ROOT}" echo "数据库:${DATA_DIR}/hypernode.db" echo "日志:journalctl -u ${SERVICE_NAME}.service -f" echo "反代请自行配置,上游地址使用 127.0.0.1:${BIND_PORT}" } cmd_update() { if [[ ! -f "/etc/systemd/system/${SERVICE_NAME}.service" ]]; then echo "尚未部署。请先选择「一键部署」。" >&2 return 1 fi log "更新程序" install_packages if [[ -d "${INSTALL_ROOT}/.git" ]]; then if ! git_in_install diff --quiet -- web; then log "控制台文件与仓库不一致,按仓库版本覆盖 web/" git_in_install checkout -- web fi git_in_install pull --ff-only origin "${GIT_BRANCH}" else ensure_install_tree fi install_python_deps fix_permissions write_service verify_app || return 1 systemctl restart "${SERVICE_NAME}.service" wait_until_ready || return 1 log "更新完成,配置和数据库已保留" } cmd_uninstall() { local answer echo "即将停止服务并删除 ${INSTALL_ROOT} 与 systemd 单元。" answer="$(ask "确认卸载请输入 yes:")" if [[ "${answer}" != "yes" ]]; then echo "已取消卸载。" return 0 fi log "卸载服务" systemctl stop "${SERVICE_NAME}.service" >/dev/null 2>&1 || true systemctl disable "${SERVICE_NAME}.service" >/dev/null 2>&1 || true rm -f "/etc/systemd/system/${SERVICE_NAME}.service" systemctl daemon-reload rm -rf "${INSTALL_ROOT}" answer="$(ask "同时删除数据库 ${DATA_DIR} ?输入 yes 删除,其他键保留:")" if [[ "${answer}" == "yes" ]]; then rm -rf "${DATA_DIR}" echo "数据库已删除。" else echo "数据库已保留在 ${DATA_DIR}" fi if id -u "${APP_USER}" >/dev/null 2>&1; then userdel "${APP_USER}" || echo "系统用户 ${APP_USER} 未删除,可稍后执行 userdel ${APP_USER}" fi log "卸载完成" } detect_ip() { hostname -I 2>/dev/null | awk '{print $1}' } show_banner() { local ip path ip="$(detect_ip)" path="${INSTALL_ROOT}" [[ -n "${ip}" ]] || ip="?" echo "" echo "========================================" echo " HyperNode-AI 部署管理器" echo " 仓库: git.bz121.com/dekun/HyperNode-AI" echo " 路径: ${path}" echo " 端口: ${BIND_PORT}" echo " IP: ${ip}" echo "========================================" } show_menu() { echo " 1) 一键部署" echo " 2) 一键卸载" echo " 3) 更新" echo " 0) 退出" echo "" } main_menu() { local choice bootstrap_repo "$@" require_root if [[ -r /etc/os-release ]]; then # shellcheck disable=SC1091 source /etc/os-release if [[ "${ID:-}" != "ubuntu" ]]; then echo "当前系统是 ${ID:-unknown},这个脚本用于 Ubuntu 22.04。" >&2 exit 1 fi if [[ "${VERSION_ID:-}" != "22.04" ]]; then echo "警告:当前是 Ubuntu ${VERSION_ID:-unknown},脚本按 22.04 编写,将继续。" >&2 fi fi while true; do show_banner show_menu choice="$(ask "请选择 [0-3]: ")" case "${choice}" in 1) cmd_deploy || echo "部署失败。" ;; 2) cmd_uninstall || echo "卸载失败。" ;; 3) cmd_update || echo "更新失败。" ;; 0) echo "已退出。" exit 0 ;; *) echo "无效选项:${choice}" ;; esac ask "按 Enter 返回菜单..." >/dev/null done } main_menu "$@"