支持 curl 管道打开部署菜单。

服务器可直接拉取 deploy/manage.sh,克隆仓库后选择部署、卸载、更新或退出。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-10-10 21:58:06 +08:00
parent 2bbdb00a4d
commit 2a847263f2
4 changed files with 365 additions and 284 deletions
+2 -263
View File
@@ -1,264 +1,3 @@
#!/usr/bin/env bash
# HyperNode-AI 交互式部署脚本,适用于 Ubuntu 22.04。
# 不安装 Nginx。反代由你自己配置,服务监听 0.0.0.0:33333。
#
# sudo bash deploy/ubuntu22.sh
set -euo pipefail
APP_USER="hypernode"
APP_DIR="/opt/hypernode"
DATA_DIR="/var/lib/hypernode"
ENV_FILE="${APP_DIR}/.env"
SERVICE_NAME="hypernode"
BIND_HOST="0.0.0.0"
BIND_PORT="33333"
SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
if [[ "${EUID}" -ne 0 ]]; then
echo "请用 root 运行:sudo bash deploy/ubuntu22.sh" >&2
exit 1
fi
if [[ ! -t 0 ]]; then
echo "请在交互式终端中运行本脚本。" >&2
exit 1
fi
if [[ ! -r /etc/os-release ]]; then
echo "无法识别操作系统,这个脚本用于 Ubuntu 22.04。" >&2
exit 1
fi
# 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
if [[ ! -f "${SOURCE_DIR}/requirements.txt" || ! -f "${SOURCE_DIR}/app/main.py" ]]; then
echo "未在 ${SOURCE_DIR} 找到项目文件。" >&2
exit 1
fi
log() {
printf '\n==> %s\n' "$*"
}
pause() {
read -r -p "按回车返回菜单..." _ || exit 0
}
sync_code() {
mkdir -p "${APP_DIR}" "${DATA_DIR}"
if [[ "$(realpath "${SOURCE_DIR}")" != "$(realpath "${APP_DIR}")" ]]; then
rsync -a --delete \
--exclude '.venv/' \
--exclude '.env' \
--exclude '.git/' \
--exclude '__pycache__/' \
--exclude '*.db' \
--exclude '.pytest_cache/' \
"${SOURCE_DIR}/" "${APP_DIR}/"
fi
}
write_env() {
if [[ -f "${ENV_FILE}" ]]; then
log "保留已有配置 ${ENV_FILE}"
return
fi
log "写入 ${ENV_FILE}"
cat >"${ENV_FILE}" <<EOF
DATABASE_URL=sqlite:////${DATA_DIR}/hypernode.db
YIELD_TIMEZONE=Asia/Shanghai
YIELD_HOUR=0
YIELD_MINUTE=5
DISABLE_SCHEDULER=false
EOF
}
install_python_deps() {
if [[ ! -x "${APP_DIR}/.venv/bin/python" ]]; then
python3 -m venv "${APP_DIR}/.venv"
fi
"${APP_DIR}/.venv/bin/pip" install --upgrade pip
"${APP_DIR}/.venv/bin/pip" install -r "${APP_DIR}/requirements.txt"
}
write_service() {
cat >"/etc/systemd/system/${SERVICE_NAME}.service" <<EOF
[Unit]
Description=HyperNode-AI control plane
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=${APP_USER}
Group=${APP_USER}
WorkingDirectory=${APP_DIR}
EnvironmentFile=${ENV_FILE}
Environment=PYTHONUNBUFFERED=1
Environment=PYTHONDONTWRITEBYTECODE=1
ExecStart=${APP_DIR}/.venv/bin/uvicorn app.main:app --host ${BIND_HOST} --port ${BIND_PORT} --workers 1
Restart=always
RestartSec=3
TimeoutStopSec=20
KillSignal=SIGINT
NoNewPrivileges=true
PrivateTmp=true
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
}
fix_permissions() {
chown -R "${APP_USER}:${APP_USER}" "${APP_DIR}" "${DATA_DIR}"
chmod 750 "${APP_DIR}" "${DATA_DIR}"
chmod 640 "${ENV_FILE}"
}
wait_until_ready() {
local ready=0
local _i
for _i in 1 2 3 4 5 6 7 8 9 10; do
if curl -fsS "http://127.0.0.1:${BIND_PORT}/health" >/dev/null; then
ready=1
break
fi
sleep 1
done
if [[ "${ready}" -ne 1 ]]; then
echo "服务未在 10 秒内就绪。最近日志:" >&2
journalctl -u "${SERVICE_NAME}.service" -n 50 --no-pager >&2 || true
return 1
fi
}
ensure_user() {
if ! id -u "${APP_USER}" >/dev/null 2>&1; then
useradd --system --home-dir "${APP_DIR}" --shell /usr/sbin/nologin "${APP_USER}"
fi
}
install_packages() {
export DEBIAN_FRONTEND=noninteractive
apt-get update
apt-get install -y python3 python3-venv python3-pip rsync ca-certificates curl
python3 -c 'import sys; raise SystemExit(0 if sys.version_info >= (3, 10) else 1)'
}
cmd_deploy() {
log "一键部署"
install_packages
ensure_user
sync_code
write_env
fix_permissions
install_python_deps
fix_permissions
write_service
systemctl enable "${SERVICE_NAME}.service"
systemctl restart "${SERVICE_NAME}.service"
wait_until_ready
log "部署完成"
echo "监听地址:${BIND_HOST}:${BIND_PORT}"
echo "健康检查:curl http://127.0.0.1:${BIND_PORT}/health"
echo "接口文档:http://<服务器 IP>:${BIND_PORT}/docs"
echo "程序目录:${APP_DIR}"
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
sync_code
install_python_deps
fix_permissions
write_service
systemctl restart "${SERVICE_NAME}.service"
wait_until_ready
log "更新完成,配置和数据库已保留"
}
cmd_uninstall() {
local answer
echo "即将停止服务并删除 ${APP_DIR} 与 systemd 单元。"
read -r -p "确认卸载请输入 yes:" answer || exit 0
if [[ "${answer}" != "yes" ]]; then
echo "已取消卸载。"
return 0
fi
log "卸载服务"
systemctl stop "${SERVICE_NAME}.service" || true
systemctl disable "${SERVICE_NAME}.service" || true
rm -f "/etc/systemd/system/${SERVICE_NAME}.service"
systemctl daemon-reload
rm -rf "${APP_DIR}"
read -r -p "同时删除数据库 ${DATA_DIR} ?输入 yes 删除,其他键保留:" answer || exit 0
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 "卸载完成"
}
show_menu() {
cat <<EOF
========================================
HyperNode-AI 部署菜单 端口 ${BIND_PORT}
========================================
1) 一键部署
2) 一键卸载
3) 更新
4) 退出
========================================
EOF
}
while true; do
show_menu
read -r -p "请选择 [1-4]: " choice || exit 0
case "${choice}" in
1)
cmd_deploy || echo "部署失败。"
pause
;;
2)
cmd_uninstall || echo "卸载失败。"
pause
;;
3)
cmd_update || echo "更新失败。"
pause
;;
4)
echo "已退出。"
exit 0
;;
*)
echo "无效选项:${choice}"
;;
esac
done
# 兼容旧入口,转到 deploy/manage.sh。
exec bash "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/manage.sh" "$@"