Files
releases/sandbox-worker/install.sh
T
2026-07-08 15:28:39 +08:00

139 lines
5.4 KiB
Bash

#!/usr/bin/env bash
# sandbox-worker 一键安装/升级脚本(用户侧,Linux)。
#
# curl -fsSL https://git.lingyu.org.cn/lingyu-note/releases/raw/branch/main/sandbox-worker/install.sh | sudo bash
#
# 前置:Node ≥ 22、Docker(worker 通过 docker API 起沙箱容器)。
# 行为:下载最新发布包(dist+依赖,平台无关)→ /opt/sandbox-worker,首次生成 .env
# + systemd 并启动;再次运行 = 升级(保留 .env,旧版本备份,失败自动回滚)。
# (仓内旧 deploy/install.sh 是内网 .95 首装脚本,与本脚本无关。)
#
# 可选:VERSION=1.0.0 指定版本;--uninstall 卸载(保留 .env)。
set -euo pipefail
APP="sandbox-worker"
GITEA="https://git.lingyu.org.cn"
REL_REPO="lingyu-note/releases"
APP_DIR="/opt/${APP}"
SERVICE="${APP}"
PORT=8443
HEALTH_URL="http://127.0.0.1:${PORT}/v1/health"
KEEP_BACKUPS=3
[[ "$(id -u)" == "0" ]] || { echo "❌ 请用 root / sudo 运行" >&2; exit 1; }
[[ "$(uname -s)" == "Linux" ]] || { echo "❌ 仅支持 Linux" >&2; exit 1; }
command -v curl >/dev/null || { echo "❌ 需要 curl" >&2; exit 1; }
# node 可能装在普通用户的 nvm 下(root PATH 看不见)—— 先 PATH 找,再借 SUDO_USER 的登录环境找
NODE_BIN="$(command -v node || true)"
if [[ -z "${NODE_BIN}" && -n "${SUDO_USER:-}" ]]; then
NODE_BIN="$(su - "${SUDO_USER}" -c 'command -v node' 2>/dev/null || true)"
fi
[[ -n "${NODE_BIN}" ]] || { echo "❌ 需要 Node.js ≥ 22(root 与 ${SUDO_USER:-当前用户} 的 PATH 均未找到 node)" >&2; exit 1; }
NODE_MAJOR=$("${NODE_BIN}" -e 'console.log(process.versions.node.split(".")[0])')
[[ "${NODE_MAJOR}" -ge 22 ]] || { echo "❌ Node ≥ 22(当前 $("${NODE_BIN}" -v))" >&2; exit 1; }
command -v docker >/dev/null || { echo "❌ 需要 Docker(worker 用它起沙箱容器)" >&2; exit 1; }
if [[ "${1:-}" == "--uninstall" ]]; then
systemctl disable --now "${SERVICE}" 2>/dev/null || true
rm -f "/etc/systemd/system/${SERVICE}.service"; systemctl daemon-reload
rm -rf "${APP_DIR}/current" "${APP_DIR}/backup"
echo "✓ 已卸载(${APP_DIR}/.env 保留,需要请手动删除)"
exit 0
fi
# ---- 解析版本 ----
if [[ -n "${VERSION:-}" ]]; then
VER="${VERSION#v}"
else
VER=$(curl -fsSL "${GITEA}/api/v1/repos/${REL_REPO}/releases?limit=50" \
| python3 -c "
import json,sys
rels=[r for r in json.load(sys.stdin) if r['tag_name'].startswith('${APP}-v')]
print(rels[0]['tag_name'].removeprefix('${APP}-v') if rels else '')" 2>/dev/null) || VER=""
[[ -n "$VER" ]] || { echo "❌ 查询最新版本失败(发布仓暂无 ${APP} release?)" >&2; exit 1; }
fi
TAG="${APP}-v${VER}"
PKG="${APP}_${VER}_node.tar.gz"
BASE="${GITEA}/${REL_REPO}/releases/download/${TAG}"
if [[ -d "${APP_DIR}/current" && -f "${APP_DIR}/.version" ]] \
&& [[ "$(cat "${APP_DIR}/.version")" == "${VER}" ]] \
&& curl -sf -o /dev/null "${HEALTH_URL}" 2>/dev/null; then
echo "✓ 已是最新版本 v${VER},无需升级"; exit 0
fi
echo "▶ 下载 ${PKG} ..."
TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT
curl -fsSL -o "${TMP}/${PKG}" "${BASE}/${PKG}"
curl -fsSL -o "${TMP}/SHA256SUMS" "${BASE}/SHA256SUMS"
(cd "${TMP}" && grep " ${PKG}\$" SHA256SUMS | sha256sum -c - >/dev/null) \
|| { echo "❌ SHA256 校验失败" >&2; exit 1; }
tar -xzf "${TMP}/${PKG}" -C "${TMP}"
mkdir -p "${APP_DIR}/backup"
# ---- 首次:.env + systemd ----
if [[ ! -f "${APP_DIR}/.env" ]]; then
echo "▶ 首次安装:生成 ${APP_DIR}/.env(请按需修改后 systemctl restart ${SERVICE})"
rnd() { head -c 32 /dev/urandom | od -An -tx1 | tr -d ' \n'; }
cat > "${APP_DIR}/.env" <<ENV
# sandbox-worker 配置(完整项见项目 .env.example)
PORT=${PORT}
# 调用方 x-api-key(已随机生成;server 侧需配同一值)
WORKER_API_KEY=$(rnd)
# Web UI 登录(/ui,查看连接信息 + 一键升级;首次安装随机密码,请尽快修改)
SANDBOX_UI_USER=admin
SANDBOX_UI_PASSWORD=$(rnd | head -c 16)
ENV
chmod 600 "${APP_DIR}/.env"
fi
if [[ ! -f "/etc/systemd/system/${SERVICE}.service" ]]; then
cat > "/etc/systemd/system/${SERVICE}.service" <<UNIT
[Unit]
Description=Lingyu Sandbox Worker (Fastify + dockerode)
After=network-online.target docker.service
Wants=network-online.target
[Service]
EnvironmentFile=${APP_DIR}/.env
ExecStart=${NODE_BIN} ${APP_DIR}/current/dist/server.js
WorkingDirectory=${APP_DIR}/current
Restart=always
RestartSec=3
[Install]
WantedBy=multi-user.target
UNIT
systemctl daemon-reload
systemctl enable "${SERVICE}" >/dev/null
fi
# ---- 替换 current(旧版备份)----
if [[ -d "${APP_DIR}/current" ]]; then
mv "${APP_DIR}/current" "${APP_DIR}/backup/current.$(date +%Y%m%d%H%M%S)"
ls -dt "${APP_DIR}"/backup/current.* 2>/dev/null | tail -n +$((KEEP_BACKUPS+1)) | xargs -r rm -rf
fi
mv "${TMP}/${APP}" "${APP_DIR}/current"
echo "${VER}" > "${APP_DIR}/.version"
echo "▶ 重启 + 健康检查 ..."
systemctl restart "${SERVICE}"
ok=""
for i in $(seq 1 12); do
sleep 1
curl -sf -o /dev/null "${HEALTH_URL}" && { ok=1; break; }
done
if [[ -z "$ok" ]]; then
echo "✗ 健康检查失败" >&2
journalctl -u "${SERVICE}" -n 20 --no-pager || true
latest=$(ls -dt "${APP_DIR}"/backup/current.* 2>/dev/null | head -1 || true)
if [[ -n "$latest" ]]; then
echo " 回滚到 $(basename "$latest")" >&2
rm -rf "${APP_DIR}/current"; mv "$latest" "${APP_DIR}/current"
systemctl restart "${SERVICE}"
fi
exit 1
fi
echo "✅ ${APP} v${VER} 运行中 — 端口 ${PORT},配置 ${APP_DIR}/.env"
echo " 日志:journalctl -u ${SERVICE} -f 升级:重跑本脚本即可"