From 76f6fe32f59cbaa0df959142021773be0ffad71c Mon Sep 17 00:00:00 2001 From: jocay Date: Sun, 18 Oct 2026 10:46:21 +0800 Subject: [PATCH] =?UTF-8?q?deploy:=20=E4=B8=80=E9=94=AE=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=20/=20=E5=8D=B8=E8=BD=BD=E8=84=9A=E6=9C=AC,=E5=AE=89=E8=A3=85?= =?UTF-8?q?=E8=84=9A=E6=9C=AC=E8=87=AA=E5=8A=A8=E9=80=89=E6=8B=A9=20IPv4/I?= =?UTF-8?q?Pv6=20=E7=AB=99=E7=82=B9=E4=B8=8E=E6=9C=80=E6=96=B0=E7=89=88?= =?UTF-8?q?=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 deploy/update.sh:只替换二进制并重启服务,保留配置;已是最新时跳过、不自动降级; 重启由独立进程执行(通过 bolt 自身运行也安全),新版本起不来时自动回滚 - 新增 deploy/uninstall.sh:删除服务、二进制与备份,默认同时删除配置,--keep-config 保留 - install.sh:先试 IPv4 站点(git.jocayn.qzz.io)再试 IPv6 站点(git.draws.qzz.io), 通过 Release API 获取最新版本,支持 SHA256SUMS 校验与 --version; 已安装时拒绝覆盖配置(--reinstall 强制);macOS 缺少签名时补 ad-hoc 签名 - .gitattributes 强制 *.sh 使用 LF - README / CLAUDE.md 更新部署说明与发布流程 Co-Authored-By: Claude Opus 5.5 --- .gitattributes | 1 + CLAUDE.md | 7 +- README.md | 81 +++++++------- deploy/install.sh | 125 +++++++++++++++------ deploy/uninstall.sh | 94 ++++++++++++++++ deploy/update.sh | 263 ++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 499 insertions(+), 72 deletions(-) create mode 100644 .gitattributes create mode 100644 deploy/uninstall.sh create mode 100644 deploy/update.sh diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dfdb8b7 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.sh text eol=lf diff --git a/CLAUDE.md b/CLAUDE.md index 7db27c3..923ce11 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -40,6 +40,11 @@ cargo check --release --all-targets --target x86_64-pc-windows-gnu # 在 Linux ## 部署(deploy/) -- `install.sh` 是需要 root 的 POSIX sh 安装脚本。自动识别 Linux(systemd:`/usr/local/bin/bolt`、`/etc/bolt.env`、`bolt.service`)或 macOS(launchd 标签 `io.jocay.bolt`,plist 放在 `/Library/Application Support/bolt/`,启用开机自启时复制到 `/Library/LaunchDaemons/`)。默认从 Gitea Release 下载 `bolt--`(版本由 `BOLT_VERSION` 指定,默认 `v0.1.0`;可用 `BOLT_DL_URL` 或 `--binary` 覆盖)。`BOLT_DL_TOKEN`(下载认证)与 `--token`(服务器的 Bearer 令牌)相互独立。 +- 三个独立的 POSIX sh 脚本(需要 root,设计为 `curl … | sudo sh` 直接运行,所以每个脚本都自带下载/探测函数,不能互相引用): + - `install.sh`:首次安装。自动识别 Linux(systemd:`/usr/local/bin/bolt`、`/etc/bolt.env`、`bolt.service`)或 macOS(launchd 标签 `io.jocay.bolt`,plist 放在 `/Library/Application Support/bolt/`,启用开机自启时复制到 `/Library/LaunchDaemons/`)。已安装时拒绝执行,除非加 `--reinstall`,因为它会按本次参数重写配置。 + - `update.sh`:只替换二进制,保留配置。重启和健康检查交给独立进程执行(Linux 用 `systemd-run`,macOS 用 `(trap '' HUP; sh … &)`;macOS 上 `nohup … &` 启动的进程会在调用返回后消失),失败时从 `/usr/local/bin/bolt.bak` 回滚。 + - `uninstall.sh`:先删文件,最后才停服务,因为通过 bolt 自身执行时,停服务会结束脚本本身。 +- 下载站点:IPv4 用 `https://git.jocayn.qzz.io`,IPv6 用 `https://git.draws.qzz.io`(同一个 Gitea,仓库 `jocay/bolt` 已公开)。脚本先用 `-4` 试 IPv4 站点,再用 `-6` 试 IPv6 站点;最新版本通过 `/api/v1/repos/jocay/bolt/releases/latest` 获取(这个 Gitea 不支持 `releases/latest/download/` 直链),附件命名为 `bolt--`,可选附带 `SHA256SUMS`。`BOLT_GIT_HOST`、`BOLT_VERSION`、`BOLT_DL_URL`、`--binary` 可以覆盖;`BOLT_DL_TOKEN`(下载认证)与 `--token`(服务器的 Bearer 令牌)相互独立。 +- 发布用的二进制用 cargo-zigbuild 编译:`x86_64-unknown-linux-gnu.2.28`(普通 `cargo build` 产物依赖构建机的 glibc 版本,较老的系统上跑不了)和 `aarch64-apple-darwin`。 - `bolt.service` 和 `bolt.env.example` 用于手动 systemd 部署。监听地址写在 unit 文件的 `ExecStart` 里,其余配置来自 `/etc/bolt.env` 中的 `BOLT_*` 变量。 - 修改 CLI 参数时,需同步更新 `parse_args`/`print_help`、`install.sh`(参数解析及其生成的 env/plist)、`bolt.env.example` 和 README 中的参数表。 diff --git a/README.md b/README.md index 808119f..47f6daf 100644 --- a/README.md +++ b/README.md @@ -139,34 +139,56 @@ python3 docs/bolt_http_test.py # HTTP 端到端(含 Mcp-Session-Id、/files - Linux / macOS(完整功能:进程组杀、信号处理) - Windows(基础功能:`cmd /C`、`taskkill /T` 杀进程树、Ctrl+C 清理退出) -## 在其他设备上部署(最快方式) -一键脚本支持 Linux(systemd)和 macOS(launchd),自动识别 amd64 / arm64;Apple Silicon 即使在 Rosetta 终端中也优先选择 arm64。安装需要 root 权限。 +## 在其他设备上部署(一键脚本) -默认下载 `v0.1.0` 的 `bolt-<平台>-<架构>` 附件,例如 `bolt-linux-amd64`、`bolt-darwin-arm64`。对应附件需要事先发布到 Gitea Release;可通过 `BOLT_VERSION` 指定版本,或通过 `BOLT_DL_URL` / `--binary` 指定其他来源。 +脚本支持 Linux(systemd)和 macOS(launchd),自动识别 amd64 / arm64(Apple Silicon 即使在 Rosetta 终端中也优先选择 arm64),需要 root 权限。仓库公开,无需令牌;IPv4 网络用 `git.jocayn.qzz.io`,IPv6 网络用 `git.draws.qzz.io`(脚本下载二进制时也会自动先试 IPv4 站点、再试 IPv6 站点)。 + +| 操作 | IPv4 | IPv6 | +|---|---|---| +| 安装 | `curl -fsSL https://git.jocayn.qzz.io/jocay/bolt/raw/branch/main/deploy/install.sh \| sudo sh -s -- --http 0.0.0.0:8080 --token 你的令牌` | `curl -fsSL https://git.draws.qzz.io/jocay/bolt/raw/branch/main/deploy/install.sh \| sudo sh -s -- --http 0.0.0.0:8080 --token 你的令牌` | +| 更新 | `curl -fsSL https://git.jocayn.qzz.io/jocay/bolt/raw/branch/main/deploy/update.sh \| sudo sh` | `curl -fsSL https://git.draws.qzz.io/jocay/bolt/raw/branch/main/deploy/update.sh \| sudo sh` | +| 卸载 | `curl -fsSL https://git.jocayn.qzz.io/jocay/bolt/raw/branch/main/deploy/uninstall.sh \| sudo sh` | `curl -fsSL https://git.draws.qzz.io/jocay/bolt/raw/branch/main/deploy/uninstall.sh \| sudo sh` | + +- **安装**(`install.sh`):下载 Release 上最新版本的 `bolt-<平台>-<架构>` 附件(有 `SHA256SUMS` 附件时自动校验)→ 装到 `/usr/local/bin` → 写配置与服务 → 开机自启 → 启动。已安装的设备会拒绝执行,避免按新参数重写配置时丢掉令牌;确实要重装加 `--reinstall`。 +- **更新**(`update.sh`):只替换二进制并重启服务,**保留现有配置**;已是最新版本时直接跳过,不会自动降级。新版本服务起不来时自动回滚到旧版本(旧版本备份在 `/usr/local/bin/bolt.bak`)。重启由独立进程完成,所以通过 bolt 自身执行更新也安全(只是本次调用的连接会断开)。 + - `--version v0.2.0` 安装指定版本,`--binary ./bolt` 使用本地二进制,`--force` 同版本也重装 +- **卸载**(`uninstall.sh`):停止并删除服务、二进制和备份;默认同时删除配置与日志,加 `--keep-config` 保留。 +- 环境变量:`BOLT_GIT_HOST` 指定下载站点,`BOLT_VERSION` 指定版本,`BOLT_DL_URL` 直接指定二进制地址,`BOLT_DL_TOKEN` 用于私有仓库下载。 + +传参写在 `sh -s --` 之后,例如: ```bash -# 1. 下载并执行安装脚本(私有仓库下载附件需先 export 下载令牌) -export BOLT_DL_TOKEN=你的Gitea令牌 -curl -fsSL -H "Authorization: token $BOLT_DL_TOKEN" https://git.draws.qzz.io/jocay/bolt/raw/branch/main/deploy/install.sh -o install.sh -sudo BOLT_DL_TOKEN="$BOLT_DL_TOKEN" sh install.sh --http 127.0.0.1:8080 - -# 2. 完成(脚本自动:下载二进制 → 装到 /usr/local/bin → 写 /etc/bolt.env 与 -# systemd 单元 → 开机自启 → 启动服务)。看状态: -systemctl status bolt +curl -fsSL https://git.jocayn.qzz.io/jocay/bolt/raw/branch/main/deploy/install.sh | sudo sh -s -- \ + --http 0.0.0.0:8080 --token 你的令牌 --allow "git,docker" --file-root /srv/data +curl -fsSL https://git.jocayn.qzz.io/jocay/bolt/raw/branch/main/deploy/update.sh | sudo sh -s -- --version v0.2.0 +curl -fsSL https://git.jocayn.qzz.io/jocay/bolt/raw/branch/main/deploy/uninstall.sh | sudo sh -s -- --keep-config ``` -### macOS arm64(Apple Silicon) +安装参数(全部可选): -在 Mac 上运行上面的安装命令,脚本会下载 `bolt-darwin-arm64`,安装到 `/usr/local/bin/bolt`,并创建以 root 运行的 launchd 系统服务 `io.jocay.bolt`。 +| 参数 | 含义 | +|---|---| +| `--http ` | 监听地址,默认 `127.0.0.1:8080`;对外用 `0.0.0.0:8080` | +| `--token ` | Bearer 认证令牌(公网必填) | +| `--allow ` / `--deny ` | 命令白/黑名单 | +| `--timeout-ms ` | 默认命令超时 | +| `--file-root ` | 文件功能允许访问的目录(配置 `--allow` 时必填,否则文件功能禁用) | +| `--max-upload ` | `PUT /files` 单次上传上限(默认 2G) | +| `--binary <路径或URL>` | 跳过 Release 下载,用本地二进制(如 `scp` 过去的)或指定 URL | +| `--version ` | 安装指定版本(默认最新) | +| `--reinstall` | 已安装时按本次参数重写配置 | +| `--no-start` / `--no-enable` | 只安装不启动 / 不设开机自启 | -`v0.1.0` Release 提供 `bolt-darwin-arm64` 附件。也可以将该二进制下载或拷到 Mac 后安装: +### 发布新版本 -```bash -sudo sh deploy/install.sh --binary ./bolt-darwin-arm64 --http 127.0.0.1:8080 --token 你的令牌 -``` +在 Gitea 上新建 Release(tag 如 `v0.2.0`),上传附件 `bolt-linux-amd64`、`bolt-darwin-arm64`(以及需要的其他平台)和 `SHA256SUMS`(`sha256sum bolt-* > SHA256SUMS`)。脚本按 `/api/v1/repos/jocay/bolt/releases/latest` 识别最新版本,发布后各设备执行更新命令即可。 -配置保存在 `/Library/Application Support/bolt/io.jocay.bolt.plist`,包含监听地址、认证令牌、命令规则和超时。启用开机自启时,脚本还会复制配置到 `/Library/LaunchDaemons/io.jocay.bolt.plist`。两份配置均仅 root 可读写;更改参数建议重新运行安装脚本,以保持一致。 +Linux 附件建议用 `cargo zigbuild --release --target x86_64-unknown-linux-gnu.2.28` 编译(只依赖 glibc 2.28,Debian 10 及以上都能运行);macOS 附件用 `cargo zigbuild --release --target aarch64-apple-darwin` 交叉编译,脚本安装时会在缺少签名时自动补 ad-hoc 签名。 + +### macOS(launchd) + +脚本会创建以 root 运行的 launchd 系统服务 `io.jocay.bolt`。配置保存在 `/Library/Application Support/bolt/io.jocay.bolt.plist`,包含监听地址、认证令牌、命令规则和超时;启用开机自启时,脚本还会复制到 `/Library/LaunchDaemons/io.jocay.bolt.plist`。两份配置均仅 root 可读写。 `--no-enable` 只保留手动启动配置,取消本服务的开机配置,仍可立即启动;`--no-start` 本次不加载服务,已运行的本服务也会卸载,下次开机是否启动由 `--no-enable` 决定。两个参数可一起使用。 @@ -183,28 +205,7 @@ sudo launchctl bootstrap system '/Library/Application Support/bolt/io.jocay.bolt sudo tail -f /var/log/bolt/stdout.log /var/log/bolt/stderr.log ``` -macOS 服务的工作目录为 `/var/root`,PATH 包含 `/opt/homebrew/bin` 和 `/opt/homebrew/sbin`,不会加载个人 shell 配置。认证下载使用 `BOLT_DL_TOKEN`,HTTP 服务认证使用 `--token`,两者独立。 - -该附件交叉编译为 `aarch64-apple-darwin`,最低目标系统为 macOS 11.0,未进行 Mac 实机运行验证。SHA-256: - -```text -39274c7d1f965ae924ee51d4e9e4d8a260a84d083971cd50dc6ce645245cc4b8 bolt-darwin-arm64 -``` - -常用安装参数(全部可选): - -| 参数 | 含义 | -|---|---| -| `--http ` | 监听地址,默认 `127.0.0.1:8080`;对外用 `0.0.0.0:8080` | -| `--token ` | Bearer 认证令牌(公网必填) | -| `--allow ` / `--deny ` | 命令白/黑名单 | -| `--timeout-ms ` | 默认命令超时 | -| `--file-root ` | 文件功能允许访问的目录(配置 `--allow` 时必填,否则文件功能禁用) | -| `--max-upload ` | `PUT /files` 单次上传上限(默认 2G) | -| `--binary <路径或URL>` | 跳过下载,用本地已拷贝的二进制(如 `scp` 过去的) | -| `--no-start` / `--no-enable` | 只安装不启动 / 不设开机自启 | - -注意:仓库是私有时,Release 附件下载需要认证 —— `export BOLT_DL_TOKEN=你的令牌` 后再跑脚本;若目标设备与开发机同网,直接 `scp root@开发机:/usr/local/bin/bolt ./bolt` 后用 `--binary ./bolt` 最省事。也可以把仓库设公开,附件即可匿名下载。 +macOS 服务的工作目录为 `/var/root`,PATH 包含 `/opt/homebrew/bin` 和 `/opt/homebrew/sbin`,不会加载个人 shell 配置。 ## systemd 服务部署(手动) diff --git a/deploy/install.sh b/deploy/install.sh index 1f403a4..9ebfefc 100755 --- a/deploy/install.sh +++ b/deploy/install.sh @@ -2,12 +2,14 @@ # bolt 一键安装脚本(POSIX sh,需 root; Linux 使用 systemd,macOS 使用 launchd) # # 用法(在目标设备上): -# 方式 A - 从 Gitea Release 下载二进制并安装: -# curl -fsSL https://git.draws.qzz.io/jocay/bolt/raw/branch/main/deploy/install.sh | sh -s -- --http 0.0.0.0:8080 --token 你的令牌 -# (私有仓库下载附件需要认证:先 export BOLT_DL_TOKEN=xxx,与 --token 分开配置) +# 方式 A - 从 Gitea Release 下载最新版二进制并安装: +# IPv4: curl -fsSL https://git.jocayn.qzz.io/jocay/bolt/raw/branch/main/deploy/install.sh | sudo sh -s -- --http 0.0.0.0:8080 --token 你的令牌 +# IPv6: curl -fsSL https://git.draws.qzz.io/jocay/bolt/raw/branch/main/deploy/install.sh | sudo sh -s -- --http 0.0.0.0:8080 --token 你的令牌 # 方式 B - 已把二进制拷到目标设备(如 scp): # ./install.sh --binary ./bolt --http 0.0.0.0:8080 --token xxx --allow "git,docker" # +# 已安装的设备升级请用 update.sh(保留配置);卸载用 uninstall.sh。 +# # 选项: # --http 监听地址,默认 127.0.0.1:8080 # --token HTTP Bearer 认证令牌(监听公网时必填) @@ -17,8 +19,15 @@ # --file-root 文件工具与 /files 只能访问这些目录(配置 --allow 时必须设置,否则文件功能禁用) # --max-upload PUT /files 单次上传上限,支持 K/M/G(默认 2G) # --binary 二进制来源:本地路径 或 http(s) URL(默认从 Gitea Release 下载) +# --version 安装指定版本(默认 Release 上的最新版本) +# --reinstall 已安装时按本次参数重写配置(不加则拒绝覆盖,避免丢失令牌等配置) # --no-start 只安装不启动 # --no-enable 不设置开机自启 +# +# 环境变量: +# BOLT_GIT_HOST 指定下载站点(如 https://git.jocayn.qzz.io),默认先试 IPv4 站点再试 IPv6 站点 +# BOLT_DL_URL 直接指定二进制下载地址 +# BOLT_DL_TOKEN 仓库为私有时的下载令牌(与 --token 无关) set -eu HTTP_ADDR="${HTTP_ADDR:-127.0.0.1:8080}" @@ -29,6 +38,8 @@ TIMEOUT_MS="${BOLT_TIMEOUT_MS:-30000}" FILE_ROOT="" MAX_UPLOAD="" BINARY_SRC="" +VERSION="${BOLT_VERSION:-}" +REINSTALL=0 NO_START=0 NO_ENABLE=0 @@ -42,12 +53,17 @@ while [ $# -gt 0 ]; do --file-root) FILE_ROOT="${2:-}"; shift 2 ;; --max-upload) MAX_UPLOAD="${2:-}"; shift 2 ;; --binary) BINARY_SRC="${2:-}"; shift 2 ;; + --version) VERSION="${2:-}"; shift 2 ;; + --reinstall) REINSTALL=1; shift ;; --no-start) NO_START=1; shift ;; --no-enable) NO_ENABLE=1; shift ;; *) echo "未知参数: $1"; echo "用法见脚本头部注释"; exit 1 ;; esac done +REPO=jocay/bolt +HOST4=https://git.jocayn.qzz.io +HOST6=https://git.draws.qzz.io BIN_PATH=/usr/local/bin/bolt ENV_FILE=/etc/bolt.env UNIT_FILE=/etc/systemd/system/bolt.service @@ -78,50 +94,97 @@ case "$ARCH" in aarch64|arm64) ARCH=arm64 ;; *) die "不支持的架构: $ARCH" ;; esac -DL_URL="${BOLT_DL_URL:-https://git.draws.qzz.io/jocay/bolt/releases/download/${BOLT_VERSION:-v0.1.0}/bolt-$PLATFORM-$ARCH}" say "目标平台: $PLATFORM/$ARCH" + +# 已安装时默认拒绝:重写配置会丢掉原来的令牌、白名单等 +if [ "$REINSTALL" -eq 0 ] && [ -x "$BIN_PATH" ] && { [ -e "$ENV_FILE" ] || [ -e "/Library/Application Support/bolt/io.jocay.bolt.plist" ]; }; then + die "检测到已安装的 bolt。升级请用 update.sh(保留配置);确实要按本次参数重写配置请加 --reinstall" +fi + +# 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 +} + +TMP=$(mktemp -d) +trap 'rm -rf "$TMP"' EXIT mkdir -p /usr/local/bin # ---------- 1. 获取二进制 ---------- if [ -n "$BINARY_SRC" ]; then if [ -f "$BINARY_SRC" ]; then say "使用本地二进制: $BINARY_SRC" - install -m 0755 "$BINARY_SRC" "$BIN_PATH" + cp "$BINARY_SRC" "$TMP/bolt" elif echo "$BINARY_SRC" | grep -q '^https\?://'; then say "下载二进制: $BINARY_SRC" - rm -f /tmp/bolt-dl.$$ - if command -v curl >/dev/null 2>&1; then - if [ -n "${BOLT_DL_TOKEN:-}" ]; then - curl -fsSL --retry 3 -H "Authorization:token ${BOLT_DL_TOKEN:-}" -o /tmp/bolt-dl.$$ "$BINARY_SRC" || die "下载失败" - else - curl -fsSL --retry 3 -o /tmp/bolt-dl.$$ "$BINARY_SRC" || die "下载失败" - fi - elif command -v wget >/dev/null 2>&1; then - wget -qO /tmp/bolt-dl.$$ "$BINARY_SRC" || die "下载失败" - else - die "需要 curl 或 wget" - fi - install -m 0755 /tmp/bolt-dl.$$ "$BIN_PATH" && rm -f /tmp/bolt-dl.$$ + fetch "$BINARY_SRC" "$TMP/bolt" || die "下载失败" else die "二进制来源不存在: $BINARY_SRC" fi +elif [ -n "${BOLT_DL_URL:-}" ]; then + say "下载二进制: $BOLT_DL_URL" + fetch "$BOLT_DL_URL" "$TMP/bolt" || die "下载失败" else - say "从 Release 下载: $DL_URL" - rm -f /tmp/bolt-dl.$$ - if command -v curl >/dev/null 2>&1; then - if [ -n "${BOLT_DL_TOKEN:-}" ]; then - curl -fsSL --retry 3 -H "Authorization:token ${BOLT_DL_TOKEN:-}" -o /tmp/bolt-dl.$$ "$DL_URL" || die "下载失败,请确认 Release 附件存在且 BOLT_DL_TOKEN 有效" - else - curl -fsSL --retry 3 -o /tmp/bolt-dl.$$ "$DL_URL" || die "下载失败,请确认 Release 附件存在;私有仓库需设置 BOLT_DL_TOKEN" - fi - elif command -v wget >/dev/null 2>&1; then - wget -qO /tmp/bolt-dl.$$ "$DL_URL" || die "下载失败" + # 选择站点:BOLT_GIT_HOST 优先,否则先试 IPv4 站点再试 IPv6 站点 + HOST="" + IPFLAG="" + if [ -n "${BOLT_GIT_HOST:-}" ]; then + HOST="${BOLT_GIT_HOST%/}" else - die "需要 curl 或 wget" + for cand in "$HOST4 -4" "$HOST6 -6"; do + if fetch "${cand% *}/api/v1/version" "$TMP/probe" "${cand#* }" 2>/dev/null; then + HOST=${cand% *} + IPFLAG=${cand#* } + break + fi + done + [ -n "$HOST" ] || die "无法连接 $HOST4(IPv4)或 $HOST6(IPv6),可用 BOLT_GIT_HOST 指定站点" + fi + 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 "无法解析最新版本号" + fi + BASE="$HOST/$REPO/releases/download/$VERSION" + say "从 Release 下载: $BASE/bolt-$PLATFORM-$ARCH" + fetch "$BASE/bolt-$PLATFORM-$ARCH" "$TMP/bolt" "$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 "$TMP/bolt" | awk '{print $1}') + else + got=$(shasum -a 256 "$TMP/bolt" | awk '{print $1}') + fi + [ -n "$want" ] && [ "$want" = "$got" ] || die "SHA-256 校验失败(期望 $want,实际 $got)" + say "SHA-256 校验通过" fi - install -m 0755 /tmp/bolt-dl.$$ "$BIN_PATH" && rm -f /tmp/bolt-dl.$$ fi -"$BIN_PATH" --help >/dev/null 2>&1 || die "二进制不可执行: $BIN_PATH" +chmod 0755 "$TMP/bolt" +if [ "$PLATFORM" = darwin ]; then + # Apple Silicon 要求可执行文件带签名;没有签名时补 ad-hoc 签名 + codesign -v "$TMP/bolt" 2>/dev/null || codesign -s - -f "$TMP/bolt" >/dev/null 2>&1 || true +fi +"$TMP/bolt" --help >/dev/null 2>&1 || die "二进制无法在本机运行" +# 先写临时文件再 rename,替换正在运行的旧版本也安全 +cp "$TMP/bolt" "$BIN_PATH.new" +chmod 0755 "$BIN_PATH.new" +mv -f "$BIN_PATH.new" "$BIN_PATH" say "二进制就绪: $BIN_PATH ($("$BIN_PATH" --help | head -1))" # macOS 将环境配置直接写入 plist,不通过 shell 解释令牌或命令规则。 diff --git a/deploy/uninstall.sh b/deploy/uninstall.sh new file mode 100644 index 0000000..6af56b5 --- /dev/null +++ b/deploy/uninstall.sh @@ -0,0 +1,94 @@ +#!/bin/sh +# bolt 卸载脚本(POSIX sh,需 root;Linux 使用 systemd,macOS 使用 launchd) +# +# 用法(在目标设备上): +# IPv4: curl -fsSL https://git.jocayn.qzz.io/jocay/bolt/raw/branch/main/deploy/uninstall.sh | sudo sh +# IPv6: curl -fsSL https://git.draws.qzz.io/jocay/bolt/raw/branch/main/deploy/uninstall.sh | sudo sh +# 保留配置: ... | sudo sh -s -- --keep-config +# +# 选项: +# --keep-config 保留配置(Linux: /etc/bolt.env;macOS: /Library/Application Support/bolt 与 /var/log/bolt) +# +# 注意:如果是通过 bolt 自己执行本脚本,最后停止服务时连接会断开,这是正常现象。 +set -eu + +BIN_PATH=/usr/local/bin/bolt +LABEL=io.jocay.bolt +KEEP_CONFIG=0 + +while [ $# -gt 0 ]; do + case "$1" in + --keep-config) KEEP_CONFIG=1; shift ;; + *) echo "未知参数: $1"; echo "用法见脚本头部注释"; exit 1 ;; + esac +done + +say() { printf '\033[1;32m[bolt]\033[0m %s\n' "$*"; } +die() { printf '\033[1;31m[bolt] 错误:\033[0m %s\n' "$*"; exit 1; } + +[ "$(id -u)" -eq 0 ] || die "需要 root 权限(请用 sudo 运行)" + +# 二进制及更新脚本留下的备份(正在运行的进程不受影响,最后再停服务) +rm_bins() { + for f in "$BIN_PATH" "$BIN_PATH.bak" "$BIN_PATH.new" "$BIN_PATH.rollback" "$BIN_PATH".v*.bak; do + if [ -e "$f" ]; then + rm -f "$f" + say "已删除 $f" + fi + done + rm -rf /tmp/bolt-update.* /tmp/bolt-upgrade 2>/dev/null || true +} + +case "$(uname -s)" in + Linux) + UNIT=/etc/systemd/system/bolt.service + HAS_SYSTEMD=0 + command -v systemctl >/dev/null 2>&1 && HAS_SYSTEMD=1 + if [ "$HAS_SYSTEMD" -eq 1 ]; then + systemctl disable bolt >/dev/null 2>&1 && say "已取消开机自启" || true + fi + rm_bins + if [ -e "$UNIT" ] || [ -d "$UNIT.d" ]; then + rm -rf "$UNIT" "$UNIT.d" + say "已删除服务单元 $UNIT" + fi + if [ "$KEEP_CONFIG" -eq 0 ]; then + if [ -e /etc/bolt.env ]; then + rm -f /etc/bolt.env + say "已删除配置 /etc/bolt.env" + fi + else + [ -e /etc/bolt.env ] && say "保留配置 /etc/bolt.env" + fi + say "卸载完成,正在停止服务…" + if [ "$HAS_SYSTEMD" -eq 1 ]; then + systemctl daemon-reload || true + systemctl stop bolt >/dev/null 2>&1 || true + systemctl reset-failed bolt >/dev/null 2>&1 || true + fi + pkill -x bolt 2>/dev/null || true + ;; + Darwin) + AUTO_PLIST="/Library/LaunchDaemons/$LABEL.plist" + PLIST_DIR="/Library/Application Support/bolt" + rm_bins + if [ -e "$AUTO_PLIST" ]; then + rm -f "$AUTO_PLIST" + say "已删除开机配置 $AUTO_PLIST" + fi + if [ "$KEEP_CONFIG" -eq 0 ]; then + for d in "$PLIST_DIR" /var/log/bolt; do + if [ -e "$d" ]; then + rm -rf "$d" + say "已删除 $d" + fi + done + else + say "保留配置 $PLIST_DIR 与日志 /var/log/bolt" + fi + say "卸载完成,正在停止服务…" + launchctl bootout "system/$LABEL" 2>/dev/null || true + launchctl disable "system/$LABEL" 2>/dev/null || true + ;; + *) die "不支持的操作系统: $(uname -s)" ;; +esac diff --git a/deploy/update.sh b/deploy/update.sh new file mode 100644 index 0000000..3df4feb --- /dev/null +++ b/deploy/update.sh @@ -0,0 +1,263 @@ +#!/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