#!/bin/sh # bolt 在线更新脚本(POSIX sh,需 root;Linux 使用 systemd,macOS 使用 launchd) # # 只替换二进制并重启服务,保留现有配置(/etc/bolt.env 或 launchd plist); # 新版本启动失败时自动回滚到旧版本。 # # 用法(在目标设备上): # IPv4: curl -fsSL https://git.jocayn.qzz.io/jocay/bolt/raw/branch/main/deploy/update.sh | sudo sh # IPv6: curl -fsSL https://git.draws.qzz.io/jocay/bolt/raw/branch/main/deploy/update.sh | sudo sh # 传参: ... | sudo sh -s -- --version v0.2.0 # # 选项: # --version 安装指定版本(默认 Release 上的最新版本) # --binary 使用本地二进制或 http(s) URL,跳过 Release 下载 # --force 版本相同或更旧也照样安装 # # 环境变量: # BOLT_GIT_HOST 指定下载站点(如 https://git.jocayn.qzz.io),默认先试 IPv4 站点再试 IPv6 站点 # BOLT_DL_TOKEN 仓库为私有时的下载令牌 set -eu REPO=jocay/bolt HOST4=https://git.jocayn.qzz.io HOST6=https://git.draws.qzz.io BIN_PATH=/usr/local/bin/bolt LABEL=io.jocay.bolt VERSION="${BOLT_VERSION:-}" BINARY_SRC="" FORCE=0 while [ $# -gt 0 ]; do case "$1" in --version) VERSION="${2:-}"; shift 2 ;; --binary) BINARY_SRC="${2:-}"; shift 2 ;; --force) FORCE=1; shift ;; *) echo "未知参数: $1"; echo "用法见脚本头部注释"; exit 1 ;; esac done say() { printf '\033[1;32m[bolt]\033[0m %s\n' "$*"; } warn() { printf '\033[1;33m[bolt]\033[0m %s\n' "$*"; } die() { printf '\033[1;31m[bolt] 错误:\033[0m %s\n' "$*"; exit 1; } [ "$(id -u)" -eq 0 ] || die "需要 root 权限(请用 sudo 运行)" [ -x "$BIN_PATH" ] || die "未找到已安装的 $BIN_PATH,请先用 install.sh 安装" # ---------- 平台 ---------- OS=$(uname -s) ARCH=$(uname -m) case "$OS" in Linux) PLATFORM=linux ;; Darwin) PLATFORM=darwin # Rosetta 下 uname 可能返回 x86_64,优先识别实际的 Apple Silicon 硬件 if [ "$(sysctl -n hw.optional.arm64 2>/dev/null || true)" = 1 ]; then ARCH=arm64 fi ;; *) die "不支持的操作系统: $OS" ;; esac case "$ARCH" in x86_64|amd64) ARCH=amd64 ;; aarch64|arm64) ARCH=arm64 ;; *) die "不支持的架构: $ARCH" ;; esac # ---------- 下载工具 ---------- # fetch <输出文件> [-4|-6] fetch() { if command -v curl >/dev/null 2>&1; then if [ -n "${BOLT_DL_TOKEN:-}" ]; then curl -fsSL --retry 3 ${3:-} -H "Authorization: token $BOLT_DL_TOKEN" -o "$2" "$1" else curl -fsSL --retry 3 ${3:-} -o "$2" "$1" fi elif command -v wget >/dev/null 2>&1; then if [ -n "${BOLT_DL_TOKEN:-}" ]; then wget -q ${3:-} --header="Authorization: token $BOLT_DL_TOKEN" -O "$2" "$1" else wget -q ${3:-} -O "$2" "$1" fi else die "需要 curl 或 wget" fi } # 探测可用站点:先 IPv4 站点,再 IPv6 站点 IPFLAG="" pick_host() { if [ -n "${BOLT_GIT_HOST:-}" ]; then HOST="${BOLT_GIT_HOST%/}" return fi for cand in "$HOST4 -4" "$HOST6 -6"; do h=${cand% *} f=${cand#* } if fetch "$h/api/v1/version" "$TMP/probe" "$f" 2>/dev/null; then HOST=$h IPFLAG=$f return fi done die "无法连接 $HOST4(IPv4)或 $HOST6(IPv6),可用 BOLT_GIT_HOST 指定站点" } # ver_cmp a b:输出 1 / 0 / -1 ver_cmp() { awk -v a="$1" -v b="$2" 'BEGIN { n = split(a, x, "."); m = split(b, y, "."); k = n > m ? n : m for (i = 1; i <= k; i++) { if (x[i] + 0 > y[i] + 0) { print 1; exit } if (x[i] + 0 < y[i] + 0) { print -1; exit } } print 0 }' } bin_version() { "$1" --help 2>/dev/null | head -1 | awk '{print $2}'; } TMP=$(mktemp -d) trap 'rm -rf "$TMP"' EXIT CUR=$(bin_version "$BIN_PATH") [ -n "$CUR" ] || CUR=unknown say "当前版本: $CUR 平台: $PLATFORM/$ARCH" # ---------- 1. 获取新二进制 ---------- NEW_BIN="$TMP/bolt" if [ -n "$BINARY_SRC" ]; then if [ -f "$BINARY_SRC" ]; then cp "$BINARY_SRC" "$NEW_BIN" elif echo "$BINARY_SRC" | grep -q '^https\?://'; then say "下载: $BINARY_SRC" fetch "$BINARY_SRC" "$NEW_BIN" || die "下载失败" else die "二进制来源不存在: $BINARY_SRC" fi else pick_host say "下载站点: $HOST" if [ -z "$VERSION" ]; then fetch "$HOST/api/v1/repos/$REPO/releases/latest" "$TMP/latest.json" "$IPFLAG" || die "查询最新版本失败" VERSION=$(sed -n 's/.*"tag_name":"\([^"]*\)".*/\1/p' "$TMP/latest.json") [ -n "$VERSION" ] || die "无法解析最新版本号" EXPLICIT=0 else EXPLICIT=1 fi TARGET=${VERSION#v} if [ "$FORCE" -eq 0 ] && [ "$CUR" != unknown ]; then c=$(ver_cmp "$TARGET" "$CUR") if [ "$c" = 0 ]; then say "已是最新版本 $CUR,无需更新(--force 可强制重装)" exit 0 fi if [ "$c" = -1 ] && [ "$EXPLICIT" -eq 0 ]; then say "Release 上最新的是 $VERSION,比已安装的 $CUR 旧,不降级(需要降级请用 --version 指定)" exit 0 fi fi BASE="$HOST/$REPO/releases/download/$VERSION" say "下载: $BASE/bolt-$PLATFORM-$ARCH" fetch "$BASE/bolt-$PLATFORM-$ARCH" "$NEW_BIN" "$IPFLAG" || die "下载失败,请确认 Release $VERSION 有 bolt-$PLATFORM-$ARCH 附件" # 有 SHA256SUMS 附件时校验 if fetch "$BASE/SHA256SUMS" "$TMP/SHA256SUMS" "$IPFLAG" 2>/dev/null; then want=$(awk -v f="bolt-$PLATFORM-$ARCH" '$2 == f || $2 == "*" f {print $1}' "$TMP/SHA256SUMS") if command -v sha256sum >/dev/null 2>&1; then got=$(sha256sum "$NEW_BIN" | awk '{print $1}') else got=$(shasum -a 256 "$NEW_BIN" | awk '{print $1}') fi [ -n "$want" ] && [ "$want" = "$got" ] || die "SHA-256 校验失败(期望 $want,实际 $got)" say "SHA-256 校验通过" fi fi chmod 0755 "$NEW_BIN" if [ "$PLATFORM" = darwin ]; then # Apple Silicon 要求可执行文件带签名;没有签名时补 ad-hoc 签名 codesign -v "$NEW_BIN" 2>/dev/null || codesign -s - -f "$NEW_BIN" >/dev/null 2>&1 || true fi NEW_VER=$(bin_version "$NEW_BIN") [ -n "$NEW_VER" ] || die "新二进制无法在本机运行" if [ "$FORCE" -eq 0 ] && [ "$NEW_VER" = "$CUR" ]; then say "已是版本 $CUR,无需更新(--force 可强制重装)" exit 0 fi say "新版本: $NEW_VER" # ---------- 2. 备份并原子替换 ---------- cp -p "$BIN_PATH" "$BIN_PATH.bak" cp "$NEW_BIN" "$BIN_PATH.new" chmod 0755 "$BIN_PATH.new" mv -f "$BIN_PATH.new" "$BIN_PATH" say "已替换 $BIN_PATH(旧版本备份为 $BIN_PATH.bak)" # ---------- 3. 重启服务(独立进程执行,失败自动回滚) ---------- if [ "$PLATFORM" = linux ] && command -v systemctl >/dev/null 2>&1 && systemctl cat bolt >/dev/null 2>&1; then RESTART="systemctl restart bolt" MAINPID="systemctl show -p MainPID --value bolt" ACTIVE="systemctl is-active --quiet bolt" elif [ "$PLATFORM" = darwin ] && launchctl print "system/$LABEL" >/dev/null 2>&1; then RESTART="launchctl kickstart -k system/$LABEL" MAINPID="launchctl print system/$LABEL | awk '/^[[:space:]]*pid = /{print \$3; exit}'" ACTIVE="launchctl print system/$LABEL | grep -q 'state = running'" else say "未发现已注册的 bolt 服务,只替换了二进制(stdio 模式下由 MCP 客户端重新拉起即可)" exit 0 fi # 重启必须由独立进程执行:如果本脚本是通过 bolt 自己运行的,重启 bolt 会连带结束本脚本 WORK=$(mktemp -d /tmp/bolt-update.XXXXXX) STATUS="$WORK/status" cat > "$WORK/restart.sh" < "$STATUS" else cp -p "$BIN_PATH.bak" "$BIN_PATH.rollback" && mv -f "$BIN_PATH.rollback" "$BIN_PATH" $RESTART echo rolled-back > "$STATUS" fi EOF if [ "$PLATFORM" = linux ] && command -v systemd-run >/dev/null 2>&1; then # 临时服务不在 bolt 的 cgroup 里,停止 bolt 时不会被一起杀掉 systemd-run --quiet --collect --unit="bolt-update-$$" /bin/sh "$WORK/restart.sh" else (trap '' HUP; sh "$WORK/restart.sh" /dev/null 2>&1 &) fi say "正在重启服务…" i=0 while [ $i -lt 30 ]; do if [ -f "$STATUS" ]; then break fi sleep 1 i=$((i + 1)) done case "$(cat "$STATUS" 2>/dev/null || echo timeout)" in ok) say "更新完成: $CUR -> $(bin_version "$BIN_PATH"),服务运行正常" rm -rf "$WORK" ;; rolled-back) rm -rf "$WORK" die "新版本服务启动失败,已回滚到 $CUR。查看日志: $([ "$PLATFORM" = linux ] && echo 'journalctl -u bolt -n 50' || echo '/var/log/bolt/stderr.log')" ;; *) warn "等待超时,重启结果写在 $STATUS(ok / rolled-back)" ;; esac