a8d6dd4b87
git.jocayn.qzz.io 在 Cloudflare 后面同时有 IPv4/IPv6 地址,部分网络(如 nas)IPv4 不通、 IPv6 可用;原来用 -4 强制 IPv4 且没有连接超时,会重试到卡死。改为由 curl 自动选择协议族, 探测 8 秒连接超时、15 秒总时长且不重试,下载加 15 秒连接超时。 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
326 lines
13 KiB
Bash
Executable File
326 lines
13 KiB
Bash
Executable File
#!/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 <addr> 监听地址,默认 127.0.0.1:8080
|
|
# --token <t> HTTP Bearer 认证令牌(监听公网时必填)
|
|
# --allow <a,b> 命令白名单
|
|
# --deny <a,b> 命令黑名单
|
|
# --timeout-ms <n> 命令默认超时(默认 30000)
|
|
# --file-root <d1,d2> 文件工具与 /files 只能访问这些目录(配置 --allow 时必须设置,否则文件功能禁用)
|
|
# --max-upload <n> PUT /files 单次上传上限,支持 K/M/G(默认 2G)
|
|
# --binary <src> 二进制来源:本地路径 或 http(s) URL(默认从 Gitea Release 下载)
|
|
# --version <tag> 安装指定版本(默认 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 <url> <输出文件> [probe]:probe 用于探测站点,连接最多等 8 秒、总共 15 秒且不重试
|
|
fetch() {
|
|
if [ "${3:-}" = probe ]; then
|
|
c_opt="--connect-timeout 8 -m 15"; w_opt="-T 15 -t 1"
|
|
else
|
|
c_opt="--connect-timeout 15 --retry 3"; w_opt="-T 60 -t 3"
|
|
fi
|
|
if command -v curl >/dev/null 2>&1; then
|
|
if [ -n "${BOLT_DL_TOKEN:-}" ]; then
|
|
curl -fsSL $c_opt -H "Authorization: token $BOLT_DL_TOKEN" -o "$2" "$1"
|
|
else
|
|
curl -fsSL $c_opt -o "$2" "$1"
|
|
fi
|
|
elif command -v wget >/dev/null 2>&1; then
|
|
if [ -n "${BOLT_DL_TOKEN:-}" ]; then
|
|
wget -q $w_opt --header="Authorization: token $BOLT_DL_TOKEN" -O "$2" "$1"
|
|
else
|
|
wget -q $w_opt -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=""
|
|
if [ -n "${BOLT_GIT_HOST:-}" ]; then
|
|
HOST="${BOLT_GIT_HOST%/}"
|
|
else
|
|
# 不强制协议族,由 curl 自动选择可用的 IPv4 / IPv6 地址
|
|
for h in "$HOST4" "$HOST6"; do
|
|
if fetch "$h/api/v1/version" "$TMP/probe" probe 2>/dev/null; then
|
|
HOST=$h
|
|
break
|
|
fi
|
|
done
|
|
[ -n "$HOST" ] || die "无法连接 $HOST4 或 $HOST6,可用 BOLT_GIT_HOST 指定站点"
|
|
fi
|
|
say "下载站点: $HOST"
|
|
if [ -z "$VERSION" ]; then
|
|
fetch "$HOST/api/v1/repos/$REPO/releases/latest" "$TMP/latest.json" || 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" || die "下载失败,请确认 Release $VERSION 有 bolt-$PLATFORM-$ARCH 附件"
|
|
# 有 SHA256SUMS 附件时校验
|
|
if fetch "$BASE/SHA256SUMS" "$TMP/SHA256SUMS" 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; 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" <<PLISTEOF
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
<plist version="1.0">
|
|
<dict>
|
|
<key>Label</key><string>$LABEL</string>
|
|
<key>ProgramArguments</key>
|
|
<array>
|
|
<string>$BIN_PATH</string>
|
|
<string>--http</string><string>$(xml_escape "$HTTP_ADDR")</string>
|
|
</array>
|
|
<key>EnvironmentVariables</key>
|
|
<dict>
|
|
<key>PATH</key><string>/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
|
|
<key>BOLT_TOKEN</key><string>$(xml_escape "$TOKEN")</string>
|
|
<key>BOLT_ALLOW</key><string>$(xml_escape "$ALLOW")</string>
|
|
<key>BOLT_DENY</key><string>$(xml_escape "$DENY")</string>
|
|
<key>BOLT_TIMEOUT_MS</key><string>$(xml_escape "$TIMEOUT_MS")</string>
|
|
<key>BOLT_FILE_ROOT</key><string>$(xml_escape "$FILE_ROOT")</string>
|
|
<key>BOLT_MAX_UPLOAD</key><string>$(xml_escape "$MAX_UPLOAD")</string>
|
|
</dict>
|
|
<key>WorkingDirectory</key><string>/var/root</string>
|
|
<key>RunAtLoad</key><true/>
|
|
<key>KeepAlive</key><dict><key>SuccessfulExit</key><false/></dict>
|
|
<key>ThrottleInterval</key><integer>2</integer>
|
|
<key>ExitTimeOut</key><integer>10</integer>
|
|
<key>StandardOutPath</key><string>/var/log/bolt/stdout.log</string>
|
|
<key>StandardErrorPath</key><string>/var/log/bolt/stderr.log</string>
|
|
</dict>
|
|
</plist>
|
|
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" <<UNITEOF
|
|
[Unit]
|
|
Description=bolt - 极速单文件 MCP 命令执行服务器
|
|
After=network-online.target
|
|
Wants=network-online.target
|
|
|
|
[Service]
|
|
Type=simple
|
|
ExecStart=$BIN_PATH --http $HTTP_ADDR
|
|
EnvironmentFile=-$ENV_FILE
|
|
Restart=on-failure
|
|
RestartSec=2
|
|
TimeoutStopSec=10
|
|
StandardOutput=journal
|
|
StandardError=journal
|
|
SyslogIdentifier=bolt
|
|
LimitNOFILE=65536
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
UNITEOF
|
|
|
|
# ---------- 4. 注册并启动 ----------
|
|
systemctl daemon-reload
|
|
if [ "$NO_ENABLE" -eq 0 ]; then
|
|
systemctl enable bolt >/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"
|