#!/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 看不见)。查找顺序: # 1. 显式 NODE_BIN 环境变量(自升级路径:worker 传自己的 process.execPath) # 2. PATH;3. 已装 unit 的 ExecStart(升级场景,上次装机时已解析对);4. SUDO_USER 登录环境 NODE_BIN="${NODE_BIN:-}" [[ -n "${NODE_BIN}" && -x "${NODE_BIN}" ]] || NODE_BIN="$(command -v node || true)" if [[ -z "${NODE_BIN}" && -f "/etc/systemd/system/${SERVICE}.service" ]]; then NODE_BIN="$(awk -F= '/^ExecStart=/{print $2}' "/etc/systemd/system/${SERVICE}.service" | awk '{print $1}')" [[ -x "${NODE_BIN}" ]] || NODE_BIN="" fi 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(NODE_BIN/PATH/unit/SUDO_USER 均未找到 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" < "/etc/systemd/system/${SERVICE}.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 升级:重跑本脚本即可"