Files
mihomo-patchbay/install.sh
T
im0o 944da9fee4
release / release (push) Failing after 1m11s
发布链路:tag 触发 CI 发 Release,install.sh 负责安装
产物是架构无关的 JS tarball(约 400 KB)而不是 bun 单文件:
单文件 94 MB 里几乎全是运行时,静态文件还得外置。架构差异
由安装侧的 bun 运行时吸收,版本钉死,和 CI 构建版本一起改。
2026-08-12 15:38:55 +08:00

391 lines
15 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
#
# install.sh — Patchbay 的安装 / 更新 / 卸载脚本
#
# 它管四样东西:
# 1. bun 运行时(私有副本,版本钉死) 默认 /opt/patchbay/bun/bun
# 2. 应用本体(来自 Gitea Release) 默认 /opt/patchbay/app/
# 3. 环境变量文件 /etc/patchbay/env
# 4. systemd 服务 /etc/systemd/system/patchbay.service
#
# bash <(curl -fsSL https://git.im0o.top/im0o/mihomo-patchbay/raw/branch/main/install.sh)
# └─ 安装;已装过就更新到最新 Release(环境变量文件保持不动)
# install.sh check # 只查询有没有新版,不改动任何东西,退出码见 --help
# install.sh uninstall # 卸载;--purge 连环境变量文件和访问令牌一起删除
#
# 前提:机器上已经用 mihomo-up.sh 装好了 mihomo(Patchbay 要读它的配置文件、
# 调它的二进制做校验、连它的管理接口)。还没装先去装那个。
#
set -Eeuo pipefail
SELF_VERSION="1.0.0"
REPO_URL="https://git.im0o.top/im0o/mihomo-patchbay"
# bun 版本钉死,避免运行时行为漂移;和 .gitea/workflows/release.yml 的构建版本一起改
BUN_VERSION="${PATCHBAY_BUN_VERSION:-1.3.14}"
PREFIX="${PATCHBAY_PREFIX:-/opt/patchbay}"
ENV_FILE="/etc/patchbay/env"
SERVICE="${PATCHBAY_SERVICE:-patchbay}"
UNIT_FILE="/etc/systemd/system/${SERVICE}.service"
# mihomo-up.sh 装出来的默认布局;mihomo 装在别处时用这两个环境变量覆盖
MIHOMO_BIN="${MIHOMO_BIN:-/usr/local/bin/mihomo}"
MIHOMO_CONF_DIR="${MIHOMO_CONF_DIR:-/etc/mihomo}"
# GitHub 下载前缀(bun 从 GitHub 下载)。直连不通时填个反代,例如 https://ghfast.top/
MIRROR="${MIHOMO_MIRROR:-}"
WANT_VERSION=""; FORCE=0; ASSUME_YES=0; PURGE=0
TMPDIR_=""
# ───────────────────────── 输出 ─────────────────────────
if [[ -t 1 ]]; then
R=$'\033[31m'; G=$'\033[32m'; Y=$'\033[33m'; W=$'\033[1m'; D=$'\033[2m'; N=$'\033[0m'
else
R=""; G=""; Y=""; W=""; D=""; N=""
fi
log() { printf '%s\n' "$*" >&2; }
info() { log "${D}··${N} $*"; }
ok() { log "${G}✓${N} $*"; }
warn() { log "${Y}!${N} $*"; }
die() { printf '%s✗%s %s\n' "$R" "$N" "$*" >&2; exit 1; }
cleanup() {
local rc=$?
trap - ERR
[[ -n "${TMPDIR_:-}" && -d "$TMPDIR_" ]] && rm -rf "$TMPDIR_"
return $rc
}
trap cleanup EXIT
trap 'die "第 $LINENO 行出错,已中止"' ERR
usage() {
cat <<'EOF'
install.sh — 安装 / 更新 / 卸载 Patchbay
install.sh 安装;已装过就更新到最新 Release
install.sh check 只查询有没有新版,不改动任何东西
install.sh uninstall [--purge]
选项:
-y, --yes 非交互确认,所有询问都当作同意(用默认值)
--force 版本相同也重新安装
--version <v> 装指定版本的 Release,如 v0.1.0;也用于回滚
--mirror <url> GitHub 下载前缀(下载 bun 用),如 https://ghfast.top/
--purge uninstall 时连环境变量文件和访问令牌一起删除
-v 打印本脚本自身的版本号
check 退出码: 0 = 已是最新 10 = 有新版或未安装 1 = 出错
EOF
exit "${1:-0}"
}
# 从 /dev/tty 读,这样 bash <(curl ...) 这种 stdin 被占用的场景也能正常按键
TTY_IN="/dev/stdin"
[[ -r /dev/tty ]] && TTY_IN="/dev/tty"
ask_default() { # ask_default "提示" "默认值" varname
local __adf_def="$2" __adf_var="$3" __adf_ans=""
if (( ASSUME_YES )); then printf -v "$__adf_var" '%s' "$__adf_def"; return 0; fi
printf '%s %s[%s]%s ' "$1" "$D" "$__adf_def" "$N" >&2
read -r __adf_ans < "$TTY_IN" || __adf_ans=""
[[ -z "$__adf_ans" ]] && __adf_ans="$__adf_def"
printf -v "$__adf_var" '%s' "$__adf_ans"
}
# ───────────────────────── 基础能力 ─────────────────────────
need() { command -v "$1" >/dev/null 2>&1 || die "缺少命令: $1"; }
require_root() {
[[ ${EUID:-$(id -u)} -eq 0 ]] || die "$1 需要 root 权限,请用 sudo 重新运行"
}
gh_url() { # gh_url "owner/repo/releases/..." -> 完整下载地址
local u="https://github.com/$1"
[[ -n "$MIRROR" ]] && u="${MIRROR%/}/$u"
printf '%s' "$u"
}
release_url() { # release_url <asset> -> Gitea Release 下载地址
if [[ -n "$WANT_VERSION" ]]; then
printf '%s/releases/download/%s/%s' "$REPO_URL" "$WANT_VERSION" "$1"
else
printf '%s/releases/latest/download/%s' "$REPO_URL" "$1"
fi
}
# 读 config.yaml 顶层的简单键(形如 `secret: "xxx"`),套路取自 mihomo-up.sh 的 cfg_get
conf_get() {
local key="$1" f="$MIHOMO_CONF_DIR/config.yaml"
[[ -f "$f" ]] || return 0
sed -n -E "s/^${key}:[[:space:]]*//p" "$f" | sed -n '1p' \
| sed -E 's/[[:space:]]*#.*$//; s/^"(.*)"$/\1/; s/^'"'"'(.*)'"'"'$/\1/'
}
installed_version() {
[[ -f "$PREFIX/app/version.txt" ]] && tr -d '[:space:]' < "$PREFIX/app/version.txt" || printf ''
}
latest_version() {
local v
v="$(curl -fsSL --retry 2 --connect-timeout 5 --max-time 15 "$(release_url version.txt)" \
| tr -d '[:space:]')" \
|| die "拉取版本号失败: $(release_url version.txt)(仓库还没发布过 Release?网络不通?)"
[[ "$v" =~ ^v[0-9]+\.[0-9]+ ]] || die "拿到的版本号格式不对: $v"
printf '%s' "$v"
}
svc_running() { systemctl is-active --quiet "$SERVICE" 2>/dev/null; }
# ───────────────────────── bun 运行时 ─────────────────────────
bun_asset() {
case "$(uname -m)" in
x86_64|amd64)
# bun 的常规 x64 构建要求 CPU 支持 AVX2,老机器要用 baseline 构建
if grep -qw avx2 /proc/cpuinfo 2>/dev/null; then echo "bun-linux-x64"
else echo "bun-linux-x64-baseline"; fi ;;
aarch64|arm64) echo "bun-linux-aarch64" ;;
*) die "未适配的架构: $(uname -m)(bun 只发布 linux x64 / aarch64)" ;;
esac
}
install_bun() {
local bun="$PREFIX/bun/bun"
if [[ -x "$bun" && "$("$bun" --version 2>/dev/null || true)" == "$BUN_VERSION" ]]; then
info "bun $BUN_VERSION 已就位,跳过"
return 0
fi
need unzip
local asset url
asset="$(bun_asset)"
url="$(gh_url "oven-sh/bun/releases/download/bun-v${BUN_VERSION}/${asset}.zip")"
info "下载 ${asset}.zip(约 30 MB)"
curl -fL -# --retry 3 --connect-timeout 15 --max-time 600 -o "$TMPDIR_/bun.zip" "$url" \
|| die "下载失败: $url(GitHub 不通就加 --mirror 换个下载源)"
unzip -q -o "$TMPDIR_/bun.zip" -d "$TMPDIR_" || die "解压失败,下载到的不是有效的 zip"
# 闸门: 新 bun 能运行起来,且自报版本号和目标一致(也能暴露架构选错)
local got
got="$("$TMPDIR_/$asset/bun" --version 2>/dev/null || true)"
[[ "$got" == "$BUN_VERSION" ]] \
|| die "bun 校验失败: 期望 $BUN_VERSION,实际 ${got:-执行不了}(多半是架构不对)"
mkdir -p "$PREFIX/bun"
install -m 0755 "$TMPDIR_/$asset/bun" "$bun"
ok "bun $BUN_VERSION → $bun"
}
# ───────────────────────── 应用本体 ─────────────────────────
install_app() { # $1 = 目标版本
local target="$1" url
url="$(release_url patchbay.tar.gz)"
info "下载 patchbay.tar.gz"
curl -fL -# --retry 3 --connect-timeout 15 --max-time 300 -o "$TMPDIR_/app.tar.gz" "$url" \
|| die "下载失败: $url"
mkdir -p "$TMPDIR_/unpack"
tar -xzf "$TMPDIR_/app.tar.gz" -C "$TMPDIR_/unpack" || die "解压失败,下载到的不是有效的 tar.gz"
# 闸门: 包里的东西齐不齐。server/ 和 web-dist/ 必须平级,服务端靠相对路径找前端
[[ -f "$TMPDIR_/unpack/patchbay/server/index.js" && -d "$TMPDIR_/unpack/patchbay/web-dist" ]] \
|| die "包内布局和预期不符(应有 patchbay/server/index.js 和 patchbay/web-dist/),已放弃"
# 应用目录无状态(配置和令牌都在别处),留一份旧版用于出问题时手动换回
rm -rf "$PREFIX/app.prev"
[[ -d "$PREFIX/app" ]] && mv "$PREFIX/app" "$PREFIX/app.prev"
mv "$TMPDIR_/unpack/patchbay" "$PREFIX/app"
ok "已安装 $target → $PREFIX/app"
}
# ───────────────────────── 环境变量文件 ─────────────────────────
# 各项的含义见仓库里的 server/env.ts;这里从 mihomo 的现有配置直接推导,
# 装完即可用,不用人工填一遍已经存在的信息。
write_env() {
if [[ -f "$ENV_FILE" ]]; then
info "环境变量文件已存在,保持不动: $ENV_FILE"
return 0
fi
local conf="$MIHOMO_CONF_DIR/config.yaml"
[[ -f "$conf" ]] || die "找不到 $conf。先用 mihomo-up.sh 装好 mihomo 再来装 Patchbay"
[[ -x "$MIHOMO_BIN" ]] || die "找不到 mihomo 二进制 $MIHOMO_BIN(装在别处就用 MIHOMO_BIN=路径 覆盖)"
local ctrl secret
ctrl="$(conf_get 'external-controller')"
secret="$(conf_get 'secret')"
[[ -n "$ctrl" && "$ctrl" != '""' ]] \
|| die "配置里没开管理接口(external-controller),Patchbay 靠它读线路和重载。先用 mihomo-up.sh 的配置向导开起来"
# Patchbay 和内核在同一台机器上,管理接口绑了 0.0.0.0 也从环回地址连
local api_host="${ctrl%:*}" api_port="${ctrl##*:}"
[[ "$api_host" == "0.0.0.0" || -z "$api_host" ]] && api_host="127.0.0.1"
local port
ask_default " Patchbay 监听端口" "7580" port
[[ "$port" =~ ^[0-9]+$ ]] || die "端口要是数字,收到的是「$port」"
mkdir -p "$(dirname "$ENV_FILE")"
cat > "$ENV_FILE" <<EOF
# Patchbay 环境变量 — 由 install.sh v${SELF_VERSION} 生成于 $(date '+%F %T')
# 各项含义见 ${REPO_URL}(server/env.ts)。改完 systemctl restart ${SERVICE} 生效。
MIHOMO_API=http://${api_host}:${api_port}
MIHOMO_SECRET=${secret}
PATCHBAY_CONFIG=${conf}
PATCHBAY_MIHOMO_BIN=${MIHOMO_BIN}
PATCHBAY_MIHOMO_HOME=${MIHOMO_CONF_DIR}
PATCHBAY_HOST=127.0.0.1
PATCHBAY_PORT=${port}
EOF
# 里面有管理接口的密钥,不让其它用户读
chmod 0640 "$ENV_FILE"
ok "已生成 $ENV_FILE ${D}(权限 0640,密钥在里面)${N}"
}
# ───────────────────────── systemd 服务 ─────────────────────────
write_unit() {
cat > "$1" <<EOF
[Unit]
Description=Patchbay — mihomo 配置的网页界面
Documentation=${REPO_URL}
# 只是排序不是依赖: 内核连不上时 Patchbay 照样能起来编辑配置
After=network-online.target mihomo.service
Wants=network-online.target
[Service]
Type=simple
EnvironmentFile=${ENV_FILE}
ExecStart=${PREFIX}/bun/bun ${PREFIX}/app/server/index.js
WorkingDirectory=${PREFIX}/app
Restart=always
RestartSec=3
[Install]
WantedBy=multi-user.target
EOF
}
install_unit() {
command -v systemctl >/dev/null 2>&1 \
|| die "这台机器没有 systemd。手动运行: $PREFIX/bun/bun $PREFIX/app/server/index.js(环境变量见 $ENV_FILE)"
local tmp; tmp="$(mktemp)"
write_unit "$tmp"
if [[ -f "$UNIT_FILE" ]] && cmp -s "$tmp" "$UNIT_FILE"; then
rm -f "$tmp"; info "服务文件已是最新,没改动"
else
install -m 0644 "$tmp" "$UNIT_FILE"; rm -f "$tmp"
ok "已写入 $UNIT_FILE"
fi
systemctl daemon-reload
}
start_service() {
systemctl enable "$SERVICE" >/dev/null 2>&1 && info "已设为开机自启"
systemctl restart "$SERVICE" || true
sleep 2
if svc_running; then
ok "$SERVICE 已启动"
else
warn "$SERVICE 没有启动成功。看具体原因: journalctl -u $SERVICE -n 50 --no-pager"
return 1
fi
}
show_access() {
local port token=""
port="$(sed -n -E 's/^PATCHBAY_PORT=//p' "$ENV_FILE" 2>/dev/null | sed -n '1p')"
# 访问令牌由服务端首次启动时生成,存在配置文件旁边(见 server/auth.ts)
[[ -f "$MIHOMO_CONF_DIR/.patchbay-token" ]] && token="$(tr -d '[:space:]' < "$MIHOMO_CONF_DIR/.patchbay-token")"
log ""
log " ${D}只监听本机。从你自己的电脑访问,先开一条 SSH 隧道:${N}"
log " ${W}ssh -N -L ${port:-7580}:127.0.0.1:${port:-7580} 用户名@这台机器${N}"
log " ${D}然后浏览器打开:${N}"
log " ${W}http://127.0.0.1:${port:-7580}${N}"
if [[ -n "$token" ]]; then
log " ${D}访问令牌${N} ${W}${token}${N}"
log " ${D}令牌存在 $MIHOMO_CONF_DIR/.patchbay-token,忘了随时回来看。${N}"
else
log " ${D}访问令牌在服务首次启动时生成: journalctl -u $SERVICE | grep 令牌${N}"
fi
}
# ───────────────────────── 命令 ─────────────────────────
do_install() {
require_root "安装到 $PREFIX 并写入 systemd 服务"
need curl; need tar
TMPDIR_="$(mktemp -d)"
local target cur
target="${WANT_VERSION:-$(latest_version)}"
cur="$(installed_version)"
if [[ "$cur" == "$target" && $FORCE -eq 0 ]]; then
ok "已是 $cur,无需操作(--force 可重装)"
return 0
fi
log "${W}Patchbay ${target}${N} ${D}${cur:+(当前 $cur)}→ $PREFIX${N}"
install_bun
install_app "$target"
write_env
install_unit
start_service || return 1
show_access
}
do_check() {
local cur latest
cur="$(installed_version)"
latest="$(latest_version)"
if [[ -z "$cur" ]]; then
log "未安装(最新 Release 是 $latest)"; exit 10
elif [[ "$cur" == "$latest" ]]; then
log "已是最新 ($cur)"; exit 0
else
log "有新版: $cur → $latest"; exit 10
fi
}
do_uninstall() {
require_root "卸载 Patchbay"
if command -v systemctl >/dev/null 2>&1; then
systemctl disable --now "$SERVICE" >/dev/null 2>&1 || true
rm -f "$UNIT_FILE"
systemctl daemon-reload
fi
rm -rf "$PREFIX"
ok "已删除 $PREFIX 和 systemd 服务"
if (( PURGE )); then
rm -f "$ENV_FILE" "$MIHOMO_CONF_DIR/.patchbay-token"
rmdir "$(dirname "$ENV_FILE")" 2>/dev/null || true
ok "已删除环境变量文件和访问令牌"
else
info "留下了 $ENV_FILE 和访问令牌,重装可以继续用;不要了加 --purge"
fi
}
# ───────────────────────── 入口 ─────────────────────────
CMD="install"
while (( $# )); do
case "$1" in
install|check|uninstall) CMD="$1" ;;
-y|--yes) ASSUME_YES=1 ;;
--force) FORCE=1 ;;
--purge) PURGE=1 ;;
--version) shift; WANT_VERSION="${1:-}"; [[ -n "$WANT_VERSION" ]] || die "--version 后面要跟版本号" ;;
--mirror) shift; MIRROR="${1:-}"; [[ -n "$MIRROR" ]] || die "--mirror 后面要跟地址" ;;
-h|--help) usage ;;
-v) printf '%s\n' "$SELF_VERSION"; exit 0 ;;
*) die "不认识的参数: $1(--help 看用法)" ;;
esac
shift
done
case "$CMD" in
install) do_install ;;
check) do_check ;;
uninstall) do_uninstall ;;
esac