#!/usr/bin/env bash # cloud-mcp 一键安装/升级脚本(用户侧,Linux)。 # # curl -fsSL https://git.lingyu.org.cn/lingyu-note/releases/raw/branch/main/miiot/install.sh | sudo bash # # 行为: # - 自动检测架构(amd64/arm64),从公开发布仓下载最新版,SHA256 校验 # - 首次:装到 /opt/miiot,生成 .env(随机 token),注册 systemd 并启动 # - 再次运行:原地升级(保留 .env 与数据;旧二进制自动备份,健康检查失败自动回滚) # # 可选: # VERSION=1.2.0 bash install.sh # 装指定版本(默认最新) # bash install.sh --uninstall # 卸载(保留 /opt/miiot/data 与 .env) set -euo pipefail APP="cloud-mcp" GITEA="https://git.lingyu.org.cn" REL_REPO="lingyu-note/releases" APP_DIR="/opt/${APP}" SERVICE="${APP}" PORT=8550 HEALTH_URL="http://127.0.0.1:${PORT}/healthz" KEEP_BACKUPS=5 [[ "$(id -u)" == "0" ]] || { echo "❌ 请用 root / sudo 运行" >&2; exit 1; } [[ "$(uname -s)" == "Linux" ]] || { echo "❌ 仅支持 Linux" >&2; exit 1; } case "$(uname -m)" in x86_64) ARCH=amd64 ;; aarch64|arm64) ARCH=arm64 ;; *) echo "❌ 不支持的架构: $(uname -m)" >&2; exit 1 ;; esac command -v curl >/dev/null || { echo "❌ 需要 curl" >&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}/bin" echo "✓ 已卸载(${APP_DIR}/data 与 .env 保留,需要请手动删除)" exit 0 fi # ---- 解析版本:VERSION 环境变量,或发布仓里 tag 前缀 miiot-v 的最新 release ---- 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}_linux_${ARCH}.tar.gz" BASE="${GITEA}/${REL_REPO}/releases/download/${TAG}" # 已是该版本且服务健康 → 幂等退出 if [[ -x "${APP_DIR}/bin/${APP}" && -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}/bin/backup" "${APP_DIR}/data" # ---- 首次:生成 .env + systemd unit ---- 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 # ---- 替换二进制(旧的备份,保留最近 N 个)---- if [[ -f "${APP_DIR}/bin/${APP}" ]]; then cp "${APP_DIR}/bin/${APP}" "${APP_DIR}/bin/backup/${APP}.$(date +%Y%m%d%H%M%S)" ls -t "${APP_DIR}/bin/backup/" | tail -n +$((KEEP_BACKUPS+1)) \ | xargs -r -I{} rm -f "${APP_DIR}/bin/backup/{}" fi install -m 755 "${TMP}/${APP}" "${APP_DIR}/bin/${APP}" echo "${VER}" > "${APP_DIR}/.version" echo "▶ 重启 + 健康检查 ..." systemctl restart "${SERVICE}" ok="" for i in $(seq 1 10); 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 -t "${APP_DIR}/bin/backup/" 2>/dev/null | head -1 || true) if [[ -n "$latest" ]]; then echo " 回滚到 ${latest}" >&2 cp "${APP_DIR}/bin/backup/${latest}" "${APP_DIR}/bin/${APP}" systemctl restart "${SERVICE}" fi exit 1 fi echo "✅ ${APP} v${VER} 运行中 — 端口 ${PORT},配置 ${APP_DIR}/.env" echo " 日志:journalctl -u ${SERVICE} -f 升级:重跑本脚本即可"