#!/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 装指定版本的 Release,如 v0.1.0;也用于回滚 --mirror 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 -> 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" < "$1" </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