diff --git a/sandbox-worker/install.sh b/sandbox-worker/install.sh new file mode 100644 index 0000000..8c246aa --- /dev/null +++ b/sandbox-worker/install.sh @@ -0,0 +1,130 @@ +#!/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; } +command -v node >/dev/null || { echo "❌ 需要 Node.js ≥ 22" >&2; exit 1; } +NODE_MAJOR=$(node -e 'console.log(process.versions.node.split(".")[0])') +[[ "${NODE_MAJOR}" -ge 22 ]] || { echo "❌ Node ≥ 22(当前 $(node -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 升级:重跑本脚本即可"