#!/bin/sh # bolt 一键安装脚本(POSIX sh,需 root; Linux 使用 systemd,macOS 使用 launchd) # # 用法(在目标设备上): # 方式 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 认证令牌(监听公网时必填) # --allow 命令白名单 # --deny 命令黑名单 # --timeout-ms 命令默认超时(默认 30000) # --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}" TOKEN="${BOLT_TOKEN:-}" ALLOW="" DENY="" TIMEOUT_MS="${BOLT_TIMEOUT_MS:-30000}" FILE_ROOT="" MAX_UPLOAD="" BINARY_SRC="" VERSION="${BOLT_VERSION:-}" REINSTALL=0 NO_START=0 NO_ENABLE=0 while [ $# -gt 0 ]; do case "$1" in --http) HTTP_ADDR="${2:-}"; shift 2 ;; --token) TOKEN="${2:-}"; shift 2 ;; --allow) ALLOW="${2:-}"; shift 2 ;; --deny) DENY="${2:-}"; shift 2 ;; --timeout-ms) TIMEOUT_MS="${2:-}"; shift 2 ;; --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 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 权限" OS=$(uname -s) ARCH=$(uname -m) case "$OS" in Linux) command -v systemctl >/dev/null 2>&1 || die "未检测到 systemd" PLATFORM=linux ;; Darwin) command -v launchctl >/dev/null 2>&1 || die "未检测到 launchd" 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 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" cp "$BINARY_SRC" "$TMP/bolt" elif echo "$BINARY_SRC" | grep -q '^https\?://'; then say "下载二进制: $BINARY_SRC" 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 # 选择站点:BOLT_GIT_HOST 优先,否则先试 IPv4 站点再试 IPv6 站点 HOST="" IPFLAG="" if [ -n "${BOLT_GIT_HOST:-}" ]; then HOST="${BOLT_GIT_HOST%/}" else 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 fi 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 解释令牌或命令规则。 if [ "$OS" = Darwin ]; then LABEL=io.jocay.bolt PLIST_DIR="/Library/Application Support/bolt" PLIST_FILE="$PLIST_DIR/$LABEL.plist" AUTO_PLIST="/Library/LaunchDaemons/$LABEL.plist" xml_escape() { printf '%s' "$1" | sed 's/&/\&/g; s//\>/g; s/"/\"/g; s/'"'"'/\'/g' } mkdir -p "$PLIST_DIR" /Library/LaunchDaemons /var/log/bolt chmod 700 "$PLIST_DIR" /var/log/bolt # 令牌所在配置仅供 root 读取。 umask 077 say "写入 launchd 配置: $PLIST_FILE" cat > "$PLIST_FILE" < Label$LABEL ProgramArguments $BIN_PATH --http$(xml_escape "$HTTP_ADDR") EnvironmentVariables PATH/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin BOLT_TOKEN$(xml_escape "$TOKEN") BOLT_ALLOW$(xml_escape "$ALLOW") BOLT_DENY$(xml_escape "$DENY") BOLT_TIMEOUT_MS$(xml_escape "$TIMEOUT_MS") BOLT_FILE_ROOT$(xml_escape "$FILE_ROOT") BOLT_MAX_UPLOAD$(xml_escape "$MAX_UPLOAD") WorkingDirectory/var/root RunAtLoad KeepAliveSuccessfulExit ThrottleInterval2 ExitTimeOut10 StandardOutPath/var/log/bolt/stdout.log StandardErrorPath/var/log/bolt/stderr.log PLISTEOF chown root:wheel "$PLIST_FILE" chmod 600 "$PLIST_FILE" if [ "$NO_ENABLE" -eq 0 ]; then cp "$PLIST_FILE" "$AUTO_PLIST" chown root:wheel "$AUTO_PLIST" chmod 600 "$AUTO_PLIST" say "已设置开机自启" else # 仅移除本脚本管理的开机配置,保留手动启动配置。 rm -f "$AUTO_PLIST" fi launchctl enable "system/$LABEL" if launchctl print "system/$LABEL" >/dev/null 2>&1; then launchctl bootout "system/$LABEL" fi if [ "$NO_START" -eq 0 ]; then launchctl bootstrap system "$PLIST_FILE" || die "服务注册失败" say "已提交服务启动: http://$HTTP_ADDR/mcp" else say "已安装(未启动),执行: sudo launchctl bootstrap system '$PLIST_FILE'" fi say "管理: sudo launchctl print system/$LABEL | sudo launchctl kickstart -k system/$LABEL" say "日志: /var/log/bolt/stdout.log 与 /var/log/bolt/stderr.log" exit 0 fi # ---------- 2. 写入环境配置 ---------- say "写入配置: $ENV_FILE" printf '# bolt 服务配置,修改后 systemctl restart bolt\n' > "$ENV_FILE" printf 'BOLT_TIMEOUT_MS=%s\n' "$TIMEOUT_MS" >> "$ENV_FILE" if [ -n "$TOKEN" ]; then printf 'BOLT_TOKEN=%s\n' "$TOKEN" >> "$ENV_FILE" fi if [ -n "$ALLOW" ]; then printf 'BOLT_ALLOW=%s\n' "$ALLOW" >> "$ENV_FILE" fi if [ -n "$DENY" ]; then printf 'BOLT_DENY=%s\n' "$DENY" >> "$ENV_FILE" fi if [ -n "$FILE_ROOT" ]; then printf 'BOLT_FILE_ROOT=%s\n' "$FILE_ROOT" >> "$ENV_FILE" fi if [ -n "$MAX_UPLOAD" ]; then printf 'BOLT_MAX_UPLOAD=%s\n' "$MAX_UPLOAD" >> "$ENV_FILE" fi [ -z "$TOKEN" ] && [ -z "$ALLOW" ] && [ -z "$DENY" ] && \ printf '# 默认无认证无白名单;监听公网时请设置 BOLT_TOKEN 与 BOLT_ALLOW\n' >> "$ENV_FILE" # ---------- 3. 写入 systemd 单元 ---------- say "写入服务单元: $UNIT_FILE" cat > "$UNIT_FILE" </dev/null 2>&1 && say "已设置开机自启" fi if [ "$NO_START" -eq 0 ]; then systemctl restart bolt sleep 1 systemctl is-active bolt >/dev/null 2>&1 || { systemctl status bolt --no-pager -l | tail -15; die "服务启动失败"; } say "服务运行中: http://$HTTP_ADDR/mcp $(systemctl show -p MainPID --value bolt)" [ -z "$TOKEN" ] || say "已启用 Bearer 认证" else say "已安装(未启动),执行 systemctl start bolt 启动" fi say "完成!管理: systemctl status bolt | journalctl -u bolt -f | systemctl restart bolt"