Files
mihomo-up/mihomo-up.sh
T
im0oandClaude Opus 5 afa5ae42f1 feat: 一键安装 / 配置向导 / 卸载,并整体优化脚本
安装:
- 新增 install 子命令和菜单第 1 项「一键安装」: 装内核 → 写 systemd →
  跑配置向导 → enable + start,装完打印代理端口 / 面板地址 / 密钥 / 日志命令
- systemd unit 按 MetaCubeX 官方文档写入,旧 unit 内容不同时先备份

配置:
- 新增配置向导,问 7 个问题生成最小可用配置 (订阅 provider、mixed-port、
  allow-lan、external-controller、secret、Web 面板、分流规则)
- 新增「配置管理」菜单: 换订阅 / 改端口 / 局域网共享 / 面板密钥 / URL 导入 /
  校验 / 手动编辑 / 备份还原。每次改动前自动备份,改完 -t 校验,
  不过则询问还原,服务在跑再询问重启

卸载:
- 新增 uninstall 子命令和菜单项: stop + disable + 删 unit 和 drop-in +
  daemon-reload + reset-failed + 删二进制,配置目录和内核备份单独询问
- --purge 全删不问

其它:
- --mirror 支持 GitHub 反代前缀,带 mirror 时生成的配置一并写走镜像的 geox-url
- --service 自定义服务名、--skip-conf-check 跳过升级前的配置预校验
- 服务菜单新增开机自启开关和重写服务文件
- 修复 ask_default 与 ask 的局部变量重名: bash 动态作用域下 printf -v
  写进了 ask 自己的变量,导致向导除默认值外的输入全部丢失
- 修复输入流 EOF 时向导在「订阅不能空」处死循环
- 修复无配置时改端口直接 die 退出菜单、查版本失败 die 掉整个会话、
  --dry-run 被 root 检查提前挡掉等问题
- 生成的 find-process-mode 加引号,避免被解析成布尔值

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XpjLPV4JvAsuq9ACb3u6B4
2026-08-09 13:38:27 +08:00

1090 lines
42 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
#
# mihomo-up.sh — mihomo 内核的安装 / 配置 / 升级 / 卸载脚本(交互菜单 + 命令行双模式)
#
# 它管三样东西:
# 1. 内核二进制 默认 /usr/local/bin/mihomo
# 2. systemd 服务 默认 /etc/systemd/system/mihomo.service
# 3. 配置文件 默认 /etc/mihomo/config.yaml
#
# bash <(curl -fsSL https://git.im0o.top/im0o/mihomo-up/raw/branch/main/mihomo-up.sh)
# └─ 交互菜单;一键安装、改配置、升级、卸载都在里面
# ./mihomo-up.sh check # 非交互,给 cron/CI 用,退出码见 --help
#
set -Eeuo pipefail
SELF_VERSION="2.0.0"
REPO="MetaCubeX/mihomo"
UI_ZIP="https://github.com/MetaCubeX/metacubexd/archive/refs/heads/gh-pages.zip"
BIN="${MIHOMO_BIN:-/usr/local/bin/mihomo}"
CONF_DIR="${MIHOMO_CONF_DIR:-/etc/mihomo}"
SERVICE="${MIHOMO_SERVICE:-mihomo}"
BACKUP_DIR="${MIHOMO_BACKUP_DIR:-/var/backups/mihomo}"
UNIT_FILE="/etc/systemd/system/${SERVICE}.service"
KEEP=3
# GitHub 下载前缀。国内机器 GitHub 不通时填个反代,例如 https://ghfast.top/
MIRROR="${MIHOMO_MIRROR:-}"
ARCH_OVERRIDE=""; VARIANT=""; WANT_VERSION=""
FORCE=0; DRY_RUN=0; NO_RESTART=0; JSON=0; ASSUME_YES=0; PURGE=0; SKIP_CONF_CHECK=0
TMPDIR_=""; STAGED=""
# ───────────────────────── 输出 ─────────────────────────
if [[ -t 1 ]]; then
R=$'\033[31m'; G=$'\033[32m'; Y=$'\033[33m'; B=$'\033[34m'
C=$'\033[36m'; W=$'\033[1m'; D=$'\033[2m'; N=$'\033[0m'
else
R=""; G=""; Y=""; B=""; C=""; W=""; D=""; N=""
fi
log() { (( JSON )) || 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'
mihomo-up.sh — 安装 / 配置 / 升级 / 卸载 mihomo 内核
mihomo-up.sh 进入交互菜单(终端下默认)
mihomo-up.sh install 一键安装:装内核 + 写 systemd + 生成配置 + 启动
mihomo-up.sh check 只查,不改任何东西
mihomo-up.sh update [--force] 只换内核二进制,不碰配置和服务定义
mihomo-up.sh config 进入配置向导,重新生成 config.yaml
mihomo-up.sh service 只写入/刷新 systemd 服务文件
mihomo-up.sh rollback 回滚到上一个备份的内核
mihomo-up.sh list 列出本地内核备份
mihomo-up.sh uninstall [--purge] 卸载内核和服务;--purge 连配置和备份一起删
选项:
-y, --yes 非交互确认,所有询问都当作同意
-n, --dry-run 只打印会做什么,不实际改动
--force 版本相同也重装
--version <v> 指定内核版本,如 v1.19.28
--no-restart 装完不启动服务
--purge uninstall 时连配置目录和备份一起删除
--skip-conf-check 升级时不用新版本预校验现有配置(配置本来就有问题时用)
--json check 时额外输出一行 JSON
--arch <a> amd64 | arm64 | armv7
--variant <v> amd64 微架构: v1 | v2 | v3 | compatible
--mirror <url> GitHub 下载前缀,如 https://ghfast.top/
--bin <path> 内核路径,默认 /usr/local/bin/mihomo
--conf-dir <dir> 配置目录,默认 /etc/mihomo
--service <name> systemd 服务名,默认 mihomo
-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"
# 注意: 这两个函数用 printf -v 往调用方的变量里写值。bash 是动态作用域,局部变量
# 重名会把调用方的变量遮住,所以这里的内部变量名都加了 __ask_ / __adf_ 前缀。
ask() { # ask "提示" varname
local __ask_prompt="$1" __ask_var="$2" __ask_ans=""
printf '%s' "$__ask_prompt" >&2
read -r __ask_ans < "$TTY_IN" || __ask_ans=""
printf -v "$__ask_var" '%s' "$__ask_ans"
}
ask_default() { # ask_default "提示" "默认值" varname
local __adf_def="$2" __adf_var="$3" __adf_ans=""
ask "$(printf '%s %s[%s]%s ' "$1" "$D" "$__adf_def" "$N")" __adf_ans
[[ -z "$__adf_ans" ]] && __adf_ans="$__adf_def"
printf -v "$__adf_var" '%s' "$__adf_ans"
}
confirm() { # confirm "问题?" -> 0/1
(( ASSUME_YES )) && return 0
local a; ask "$1 ${D}[y/N]${N} " a
[[ "$a" =~ ^[Yy]$ ]]
}
confirm_default_yes() { # 默认同意版,回车即 yes
(( ASSUME_YES )) && return 0
local a; ask "$1 ${D}[Y/n]${N} " a
[[ ! "$a" =~ ^[Nn]$ ]]
}
pause() { local _x; ask "$(printf '\n%s按回车继续…%s' "$D" "$N")" _x; }
# ───────────────────────── 基础能力 ─────────────────────────
need() { command -v "$1" >/dev/null 2>&1 || die "缺少命令: $1"; }
require_root() {
[[ ${EUID:-$(id -u)} -eq 0 ]] || die "$1 需要 root 权限,请用 sudo 重跑"
}
has_systemd() { command -v systemctl >/dev/null 2>&1; }
gh_url() { # gh_url "owner/repo/releases/..." -> 完整下载地址
local u="https://github.com/$1"
[[ -n "$MIRROR" ]] && u="${MIRROR%/}/$u"
printf '%s' "$u"
}
gen_secret() {
local s=""
[[ -r /dev/urandom ]] && s="$(od -An -tx1 -N12 /dev/urandom 2>/dev/null | tr -d ' \n')" || s=""
[[ -n "$s" ]] || s="mihomo$(date +%s)"
printf '%s' "$s"
}
detect_arch() {
[[ -n "$ARCH_OVERRIDE" ]] && { echo "$ARCH_OVERRIDE"; return; }
case "$(uname -m)" in
x86_64|amd64) echo "amd64" ;;
aarch64|arm64) echo "arm64" ;;
armv7l|armv7) echo "armv7" ;;
*) die "未适配的架构: $(uname -m)(可用 --arch 手动指定 amd64/arm64/armv7)" ;;
esac
}
asset_name() {
local arch suffix=""; arch="$(detect_arch)"
[[ "$arch" == "amd64" && -n "$VARIANT" ]] && suffix="-${VARIANT}"
echo "mihomo-linux-${arch}${suffix}-${1}.gz"
}
version_line() { # 不用 head,避免 SIGPIPE 触发 ERR trap
local out; out="$("$1" -v 2>/dev/null || true)"
printf '%s' "${out%%$'\n'*}"
}
current_version() {
[[ -x "$BIN" ]] || { printf ''; return; }
local line; line="$(version_line "$BIN")"
[[ -n "$line" ]] && awk '{print $3}' <<<"$line" || printf ''
}
latest_version() {
local v
v="$(curl -fsSL --retry 3 --connect-timeout 10 \
"$(gh_url "${REPO}/releases/latest/download/version.txt")" | tr -d '[:space:]')" \
|| die "拉取最新版本号失败(GitHub 不通?可用 --mirror 指定反代前缀)"
[[ "$v" =~ ^v[0-9]+\.[0-9]+ ]] || die "拿到的版本号格式不对: $v"
echo "$v"
}
svc_state() {
has_systemd || { echo "无 systemd"; return; }
if systemctl is-active --quiet "$SERVICE" 2>/dev/null; then echo "running"
elif [[ -f "$UNIT_FILE" ]] || systemctl list-unit-files 2>/dev/null | grep -q "^${SERVICE}\.service"; then echo "stopped"
else echo "未安装"; fi
}
svc_enabled() { # 是否开机自启
has_systemd || { echo "-"; return; }
systemctl is-enabled "$SERVICE" 2>/dev/null || echo "disabled"
}
conf_file() { printf '%s/config.yaml' "$CONF_DIR"; }
# ───────────────────────── 内核二进制 ─────────────────────────
stage_binary() { # $1 = 目标版本;成功后 STAGED 指向校验过的新二进制
local target="$1" asset url
[[ -n "${TMPDIR_:-}" && -d "${TMPDIR_:-}" ]] || TMPDIR_="$(mktemp -d)"
asset="$(asset_name "$target")"
url="$(gh_url "${REPO}/releases/download/${target}/${asset}")"
info "下载 $asset"
curl -fL -# --retry 3 --connect-timeout 15 -o "$TMPDIR_/m.gz" "$url" \
|| die "下载失败: $url(网络不通就加 --mirror 换个下载源,版本号写错也会 404)"
gzip -dc "$TMPDIR_/m.gz" > "$TMPDIR_/mihomo" || die "解压失败,下载到的不是有效的 .gz 文件"
chmod +x "$TMPDIR_/mihomo"
# 闸门 1: 新二进制能跑起来,且自报版本号和目标一致
local line ver
line="$(version_line "$TMPDIR_/mihomo")"
[[ -n "$line" ]] || die "新二进制执行不了,多半是架构选错了(这次按 $(detect_arch) 下载,可用 --arch 覆盖)"
ver="$(awk '{print $3}' <<<"$line")"
[[ "$ver" == "$target" ]] || die "版本对不上: 期望 $target,实际 $ver"
ok "校验通过: $line"
STAGED="$TMPDIR_/mihomo"
}
test_conf_with() { # test_conf_with <二进制路径>;配置无效返回 1,输出错误
local b="$1"
[[ -f "$(conf_file)" ]] || return 0 # 没配置就没什么可校验的
"$b" -t -d "$CONF_DIR" 2>&1
}
do_update() { # $1 = 目标版本;只换二进制,不动配置和服务定义
local target="$1" cur; cur="$(current_version)"
if [[ "$cur" == "$target" && $FORCE -eq 0 ]]; then
ok "已是最新 ($cur),无需操作"; return 0
fi
require_root "替换 $BIN 并重启服务"
if (( DRY_RUN )); then
ok "[dry-run] 会下载 $(asset_name "$target") 并安装到 $BIN"; return 0
fi
stage_binary "$target"
# 闸门 2: 用新版本校验现有配置,配置在新版本下跑不通就别换
if [[ -f "$(conf_file)" ]] && (( SKIP_CONF_CHECK == 0 )); then
local out
if out="$(test_conf_with "$STAGED")"; then
ok "现有配置在 $target 下校验通过"
else
warn "$target 校验现有配置失败:"; sed 's/^/ /' <<<"$out" >&2
die "没有替换任何文件。先把配置改好(菜单「配置管理 → 校验配置」可以反复验),或加 --skip-conf-check 跳过这道检查"
fi
fi
mkdir -p "$BACKUP_DIR"
if [[ -x "$BIN" && -n "$cur" ]]; then
cp -a "$BIN" "$BACKUP_DIR/mihomo-$cur"; info "已备份旧版 → $BACKUP_DIR/mihomo-$cur"
fi
local was_active=0
[[ "$(svc_state)" == "running" ]] && was_active=1
trap - ERR # 下面自己处理失败并回滚
(( was_active )) && { info "停止 $SERVICE"; systemctl stop "$SERVICE"; }
install -m 0755 "$STAGED" "$BIN"
ok "已安装 $target → $BIN"
if (( NO_RESTART )); then
warn "--no-restart: 记得手动 systemctl start $SERVICE"
elif (( was_active )); then
systemctl start "$SERVICE" || true
sleep 2
if [[ "$(svc_state)" == "running" ]]; then
ok "$SERVICE 运行中"
else
warn "$SERVICE 启动失败,正在回滚到 $cur"
if [[ -f "$BACKUP_DIR/mihomo-$cur" ]]; then
install -m 0755 "$BACKUP_DIR/mihomo-$cur" "$BIN"
systemctl start "$SERVICE" || true
warn "已回滚到 $cur;看失败原因: journalctl -u $SERVICE -n 50"
else
warn "没有 $cur 的备份可回滚;看日志: journalctl -u $SERVICE -n 50"
fi
trap 'die "第 $LINENO 行出错,已中止"' ERR
return 1
fi
fi
compgen -G "$BACKUP_DIR/mihomo-*" >/dev/null && \
ls -1t "$BACKUP_DIR"/mihomo-* | tail -n +$((KEEP + 1)) | xargs -r rm -f
ok "完成: ${cur:-无} → $target"
trap 'die "第 $LINENO 行出错,已中止"' ERR
}
do_rollback() {
local b; b="$(ls -1t "$BACKUP_DIR"/mihomo-* 2>/dev/null | sed -n '1p' || true)"
[[ -n "$b" ]] || { warn "没有可回滚的备份(备份目录 $BACKUP_DIR 是空的)"; return 1; }
require_root "替换 $BIN"
info "回滚到 $(basename "$b")"
(( DRY_RUN )) && { ok "[dry-run] 到此为止"; return 0; }
systemctl stop "$SERVICE" 2>/dev/null || true
install -m 0755 "$b" "$BIN"
systemctl start "$SERVICE" 2>/dev/null || true
ok "已回滚: $(version_line "$BIN")"
}
do_list() {
if compgen -G "$BACKUP_DIR/mihomo-*" >/dev/null 2>&1; then
ls -1t "$BACKUP_DIR"/mihomo-* | while read -r f; do
printf ' %s %s%s%s\n' "$(basename "$f")" "$D" "$(date -r "$f" '+%F %T')" "$N"
done
else
log " ${D}还没有备份(备份目录 $BACKUP_DIR)${N}"
fi
}
# ───────────────────────── systemd 服务 ─────────────────────────
# 参考 MetaCubeX 官方文档 https://wiki.metacubex.one/startup/service/
write_unit() {
cat > "$1" <<EOF
[Unit]
Description=mihomo Daemon, Another Clash Kernel.
Documentation=https://wiki.metacubex.one
After=network-online.target nss-lookup.target
Wants=network-online.target
[Service]
Type=simple
LimitNPROC=500
LimitNOFILE=1000000
# 这些 capability 让 mihomo 不用完整 root 也能开 TUN、绑低端口、读系统时间
CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_TIME CAP_SYS_PTRACE CAP_DAC_READ_SEARCH CAP_DAC_OVERRIDE
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_TIME CAP_SYS_PTRACE CAP_DAC_READ_SEARCH CAP_DAC_OVERRIDE
Restart=always
RestartSec=3
ExecStart=${BIN} -d ${CONF_DIR}
ExecReload=/bin/kill -HUP \$MAINPID
[Install]
WantedBy=multi-user.target
EOF
}
install_unit() { # 写入或刷新 systemd 服务文件
has_systemd || { warn "这台机器没有 systemd,跳过服务安装(内核仍可手动运行: $BIN -d $CONF_DIR)"; return 0; }
require_root "写入 $UNIT_FILE"
if (( DRY_RUN )); then ok "[dry-run] 会写入 $UNIT_FILE"; return 0; fi
local tmp; tmp="$(mktemp)"
write_unit "$tmp"
if [[ -f "$UNIT_FILE" ]] && cmp -s "$tmp" "$UNIT_FILE"; then
rm -f "$tmp"; info "服务文件已是最新,没改动"
else
if [[ -f "$UNIT_FILE" ]]; then
local bak="${UNIT_FILE}.bak-$(date +%Y%m%d%H%M%S)"
cp -a "$UNIT_FILE" "$bak"; info "旧的服务文件已备份 → $bak"
fi
install -m 0644 "$tmp" "$UNIT_FILE"; rm -f "$tmp"
ok "已写入 $UNIT_FILE"
fi
systemctl daemon-reload
}
start_service() {
has_systemd || return 0
systemctl enable "$SERVICE" >/dev/null 2>&1 && info "已设为开机自启"
systemctl restart "$SERVICE" || true
sleep 2
if [[ "$(svc_state)" == "running" ]]; then
ok "$SERVICE 已启动"
else
warn "$SERVICE 没起来。看具体原因: journalctl -u $SERVICE -n 50 --no-pager"
return 1
fi
}
# ───────────────────────── 配置 ─────────────────────────
# 说明:下面这组函数只改 config.yaml 顶层的简单键(形如 `mixed-port: 7890` 这种
# 顶格写、值在同一行的),改之前一定先备份、改完一定校验,失败就还原。
# 缩进层级里的东西(比如 dns、proxy-groups)不碰,那种请用「编辑配置」手改。
cfg_backup() { # 备份当前配置,输出备份路径
local f; f="$(conf_file)"
[[ -f "$f" ]] || return 0
local bak="${f}.bak-$(date +%Y%m%d%H%M%S)"
cp -a "$f" "$bak"
printf '%s' "$bak"
}
cfg_restore() { # cfg_restore <备份路径>
[[ -n "${1:-}" && -f "$1" ]] || return 0
cp -a "$1" "$(conf_file)"
warn "已还原到改动前的配置"
}
cfg_require() { # 没有配置文件时,给一句能照着做的提示
[[ -f "$(conf_file)" ]] && return 0
warn "还没有配置文件 $(conf_file),先用「生成配置」跑一遍向导"
return 1
}
cfg_get() { # cfg_get <顶层键> -> 值(拿不到就是空)
local key="$1" f; f="$(conf_file)"
[[ -f "$f" ]] || return 0
sed -n -E "s/^${key}:[[:space:]]*//p" "$f" | sed -n '1p' | sed -E 's/[[:space:]]*#.*$//; s/^"(.*)"$/\1/; s/^'"'"'(.*)'"'"'$/\1/'
}
cfg_set() { # cfg_set <顶层键> <值>
local key="$1" val="$2" f; f="$(conf_file)"
[[ -f "$f" ]] || die "找不到 $f,先生成配置"
local esc; esc="$(printf '%s' "$val" | sed -e 's/[\\&|]/\\&/g')"
if grep -qE "^${key}:" "$f"; then
sed -i -E "0,/^${key}:.*/s|^${key}:.*|${key}: ${esc}|" "$f"
else
printf '%s: %s\n' "$key" "$val" >> "$f"
fi
}
cfg_validate() { # 校验配置;无效时打印错误并返回 1
local f; f="$(conf_file)"
[[ -f "$f" ]] || { warn "还没有配置文件 $f"; return 1; }
[[ -x "$BIN" ]] || { warn "内核还没装($BIN 不存在),没法校验,先「一键安装」"; return 1; }
local out
if out="$("$BIN" -t -d "$CONF_DIR" 2>&1)"; then
ok "配置有效"; return 0
else
warn "配置有问题:"; sed 's/^/ /' <<<"$out" >&2; return 1
fi
}
cfg_apply() { # cfg_apply <备份路径> — 校验并按需重启;失败给还原选项
local bak="${1:-}"
if cfg_validate; then
if [[ "$(svc_state)" == "running" ]]; then
confirm_default_yes " 重启 $SERVICE 让新配置生效?" && { systemctl restart "$SERVICE" && ok "$SERVICE 已重启"; }
fi
return 0
fi
if [[ -n "$bak" ]]; then
confirm_default_yes " 配置校验没过,还原到改动前?" && cfg_restore "$bak"
fi
return 1
}
cfg_wizard() { # 生成一份全新的 config.yaml
local f; f="$(conf_file)"
mkdir -p "$CONF_DIR"
log ""
log "${W} 配置向导${N} ${D}— 回车用方括号里的默认值${N}"
log "${D} 生成的是一份最小可用配置:一个订阅、一组选择/自动测速分组、国内直连其余走代理。${N}"
log ""
local sub port lan ctrl_mode ctrl secret ui rules_mode
local old_sub old_port old_ctrl old_secret
old_sub="$(cfg_sub_url || true)"; old_port="$(cfg_get 'mixed-port' || true)"
old_ctrl="$(cfg_get 'external-controller' || true)"; old_secret="$(cfg_get 'secret' || true)"
local tries=0
while :; do
ask_default " 订阅链接(机场给的那个 http(s) 地址)" "${old_sub:-}" sub
[[ -n "$sub" ]] && break
if (( ++tries >= 3 )); then
warn " 一直没拿到订阅链接,向导先退出了,配置没有改动"
return 1
fi
warn " 订阅链接不能空,没有节点 mihomo 起不来"
done
ask_default " 本地代理端口(HTTP 和 SOCKS5 共用)" "${old_port:-7890}" port
[[ "$port" =~ ^[0-9]+$ ]] || die "端口要是数字,收到的是: $port"
if confirm " 允许同局域网的其它设备用这个代理?"; then lan="true"; else lan="false"; fi
log ""
log " ${D}管理接口用来接 Web 面板和 API。开在 0.0.0.0 等于对外网暴露,务必设密钥。${N}"
ask_default " 管理接口: 1=只允许本机 2=局域网/公网可访问 3=不开" "1" ctrl_mode
case "$ctrl_mode" in
2) local cport; ask_default " 管理接口端口" "9090" cport; ctrl="0.0.0.0:${cport}" ;;
3) ctrl="" ;;
*) local cport; ask_default " 管理接口端口" "9090" cport; ctrl="127.0.0.1:${cport}" ;;
esac
secret=""
if [[ -n "$ctrl" ]]; then
ask_default " 管理接口密钥(回车=自动生成一个)" "${old_secret:-$(gen_secret)}" secret
if confirm_default_yes " 顺带装个 Web 面板(metacubexd,内核启动时自动下载)?"; then ui="yes"; else ui="no"; fi
else
ui="no"
fi
ask_default " 分流规则: 1=国内直连、其余走代理 2=全部走代理" "1" rules_mode
# 先写到临时文件,确认无误再落盘
local tmp; tmp="$(mktemp)"
{
cat <<EOF
# mihomo 配置 — 由 mihomo-up.sh v${SELF_VERSION} 生成于 $(date '+%F %T')
# 端口、订阅这类常用项可以回脚本菜单的「配置管理」里改;手改也行,改完记得校验。
mixed-port: ${port} # HTTP 和 SOCKS5 共用这一个端口
allow-lan: ${lan} # true = 同局域网的其它设备也能连这个代理
mode: rule
log-level: info
ipv6: false
unified-delay: true # 延迟测试扣掉握手耗时,数字更贴近真实体感
tcp-concurrent: true # 同一域名的多个 IP 一起连,谁先通用谁
find-process-mode: 'off' # 不做进程匹配,省一点开销(加引号防止被当成布尔值)
profile:
store-selected: true # 记住你在面板里选的节点,重启不丢
EOF
if [[ -n "$ctrl" ]]; then
printf '\nexternal-controller: %s\n' "$ctrl"
printf 'secret: "%s"\n' "$secret"
if [[ "$ui" == "yes" ]]; then
printf 'external-ui: ui\n'
printf 'external-ui-url: "%s"\n' "$UI_ZIP"
fi
fi
if [[ -n "$MIRROR" ]]; then
cat <<EOF
# GeoIP/GeoSite 数据库走镜像下载(因为你指定了 --mirror)
geox-url:
geoip: "${MIRROR%/}/https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.dat"
geosite: "${MIRROR%/}/https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geosite.dat"
mmdb: "${MIRROR%/}/https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/country.mmdb"
EOF
fi
cat <<EOF
proxy-providers:
subs:
type: http
url: "${sub}" # mihomo-up:sub
path: ./providers/subs.yaml
interval: 86400 # 每 24 小时自动拉一次订阅
health-check:
enable: true
url: https://www.gstatic.com/generate_204
interval: 300
proxy-groups:
- name: PROXY # 手动选节点,默认跟着 AUTO 走
type: select
proxies: [AUTO, DIRECT]
use: [subs]
- name: AUTO # 自动挑延迟最低的节点
type: url-test
use: [subs]
url: https://www.gstatic.com/generate_204
interval: 300
tolerance: 50
rules:
EOF
cat <<'EOF'
# 本机和内网地址一律直连,不然会绕出去再回来
- IP-CIDR,127.0.0.0/8,DIRECT,no-resolve
- IP-CIDR,10.0.0.0/8,DIRECT,no-resolve
- IP-CIDR,172.16.0.0/12,DIRECT,no-resolve
- IP-CIDR,192.168.0.0/16,DIRECT,no-resolve
EOF
if [[ "$rules_mode" == "2" ]]; then
printf ' - MATCH,PROXY\n'
else
cat <<'EOF'
- GEOIP,CN,DIRECT # 目标 IP 在国内就直连(首次启动会下载 GeoIP 库)
- MATCH,PROXY
EOF
fi
} > "$tmp"
local bak=""
if [[ -f "$f" ]]; then
confirm " $f 已存在,覆盖它?(旧的会先备份)" || { rm -f "$tmp"; warn " 已取消,配置没动"; return 1; }
bak="$(cfg_backup)"
[[ -n "$bak" ]] && info "旧配置已备份 → $bak"
fi
install -m 0644 "$tmp" "$f"; rm -f "$tmp"
ok "配置已写入 $f"
cfg_apply "$bak" || warn "配置写进去了但校验没过,可以用「编辑配置」改,或重新跑一遍向导"
}
cfg_sub_url() { # 读回本脚本写的订阅链接
local f; f="$(conf_file)"
[[ -f "$f" ]] || return 0
sed -n -E 's|^[[:space:]]*url:[[:space:]]*"?([^"#]*)"?[[:space:]]*#[[:space:]]*mihomo-up:sub.*|\1|p' "$f" \
| sed -n '1p' | sed -E 's/[[:space:]]+$//'
}
cfg_change_sub() {
local f; f="$(conf_file)"
[[ -f "$f" ]] || { warn "还没有配置文件,先用「生成配置」跑一遍向导"; return 1; }
local cur; cur="$(cfg_sub_url)"
if [[ -z "$cur" ]]; then
warn "在 $f 里没找到本脚本写的订阅行(标记是行尾的 # mihomo-up:sub)"
warn "这份配置多半是手写或外部导入的,改订阅请用「编辑配置」,或重跑向导生成新配置"
return 1
fi
local new; ask_default " 新的订阅链接" "$cur" new
[[ -n "$new" && "$new" != "$cur" ]] || { info "没有改动"; return 0; }
local bak; bak="$(cfg_backup)"
local esc; esc="$(printf '%s' "$new" | sed -e 's/[\\&|]/\\&/g')"
sed -i -E "s|^([[:space:]]*url:[[:space:]]*).*(#[[:space:]]*mihomo-up:sub.*)|\1\"${esc}\" \2|" "$f"
ok "订阅链接已更新"
# 订阅缓存留着会继续用旧节点,删掉让内核下次启动重新拉
rm -f "$CONF_DIR/providers/subs.yaml" 2>/dev/null || true
cfg_apply "$bak"
}
cfg_change_port() {
cfg_require || return 1
local cur; cur="$(cfg_get 'mixed-port')"
local new; ask_default " 本地代理端口" "${cur:-7890}" new
[[ "$new" =~ ^[0-9]+$ ]] || { warn "端口要是数字"; return 1; }
[[ "$new" == "$cur" ]] && { info "没有改动"; return 0; }
local bak; bak="$(cfg_backup)"
cfg_set 'mixed-port' "$new"
ok "代理端口 → $new"
cfg_apply "$bak"
}
cfg_toggle_lan() {
cfg_require || return 1
local cur; cur="$(cfg_get 'allow-lan')"
local new="true"; [[ "$cur" == "true" ]] && new="false"
local bak; bak="$(cfg_backup)"
cfg_set 'allow-lan' "$new"
if [[ "$new" == "true" ]]; then
ok "已允许局域网设备连本机代理(记得放行防火墙端口)"
else
ok "已改回只允许本机使用"
fi
cfg_apply "$bak"
}
cfg_change_ctrl() {
cfg_require || return 1
local cur_ctrl cur_secret
cur_ctrl="$(cfg_get 'external-controller')"; cur_secret="$(cfg_get 'secret')"
log " ${D}当前: ${cur_ctrl:-未开启} 密钥: ${cur_secret:-无}${N}"
local mode; ask_default " 管理接口: 1=只允许本机 2=局域网/公网可访问 3=不开" "1" mode
local bak; bak="$(cfg_backup)"
case "$mode" in
3) cfg_set 'external-controller' '""'; ok "已关闭管理接口(面板也就连不上了)" ;;
2) local p; ask_default " 端口" "9090" p
cfg_set 'external-controller' "0.0.0.0:${p}"
local s; ask_default " 访问密钥(对外暴露必须设)" "${cur_secret:-$(gen_secret)}" s
cfg_set 'secret' "\"$s\""
ok "管理接口 → 0.0.0.0:${p},密钥 ${s}" ;;
*) local p; ask_default " 端口" "9090" p
cfg_set 'external-controller' "127.0.0.1:${p}"
local s; ask_default " 访问密钥" "${cur_secret:-$(gen_secret)}" s
cfg_set 'secret' "\"$s\""
ok "管理接口 → 127.0.0.1:${p}(只有本机能连)" ;;
esac
cfg_apply "$bak"
}
cfg_import_url() {
local url; ask " 完整 config.yaml 的下载地址: " url
[[ -n "$url" ]] || { info "已取消"; return 0; }
mkdir -p "$CONF_DIR"
local tmp; tmp="$(mktemp)"
curl -fsSL --retry 2 --connect-timeout 15 -o "$tmp" "$url" || { rm -f "$tmp"; warn "下载失败: $url"; return 1; }
[[ -s "$tmp" ]] || { rm -f "$tmp"; warn "下载到的是空文件,没有覆盖任何东西"; return 1; }
local bak=""
[[ -f "$(conf_file)" ]] && { bak="$(cfg_backup)"; info "旧配置已备份 → $bak"; }
install -m 0644 "$tmp" "$(conf_file)"; rm -f "$tmp"
ok "已导入到 $(conf_file)"
cfg_apply "$bak"
}
cfg_edit() {
local f; f="$(conf_file)"
[[ -f "$f" ]] || { warn "还没有配置文件,先用「生成配置」跑一遍向导"; return 1; }
local ed="${EDITOR:-}"
[[ -n "$ed" ]] || { command -v nano >/dev/null 2>&1 && ed=nano || ed=vi; }
command -v "${ed%% *}" >/dev/null 2>&1 || { warn "找不到编辑器 $ed,可以先 export EDITOR=你的编辑器 再回来"; return 1; }
local bak; bak="$(cfg_backup)"
# 编辑器要直接接管终端,否则 bash <(curl ...) 这种场景下它读不到按键
if [[ -w /dev/tty ]]; then "$ed" "$f" </dev/tty >/dev/tty 2>&1 || true
else "$ed" "$f" || true; fi
cfg_apply "$bak"
}
cfg_backup_menu() {
local f; f="$(conf_file)"
log ""
log " ${D}配置备份(每次改动前自动生成):${N}"
if compgen -G "${f}.bak-*" >/dev/null 2>&1; then
ls -1t "${f}".bak-* | while read -r b; do
printf ' %s %s%s%s\n' "$(basename "$b")" "$D" "$(date -r "$b" '+%F %T')" "$N"
done
else
log " ${D}还没有备份${N}"; return 0
fi
confirm " 还原到最近的一份备份?" || return 0
local last; last="$(ls -1t "${f}".bak-* | sed -n '1p')"
cp -a "$last" "$f"
ok "已还原 $(basename "$last")"
cfg_apply ""
}
# ───────────────────────── 一键安装 ─────────────────────────
do_install() {
(( DRY_RUN )) || require_root "安装 mihomo"
local target cur
target="${WANT_VERSION:-$(latest_version 2>/dev/null || true)}"
cur="$(current_version)"
if [[ -z "$target" ]]; then
warn "查不到最新版本号,GitHub 大概不通"
warn "可以加 --mirror <反代前缀> 换下载源,或用 --version v1.19.28 指定版本再试"
return 1
fi
log ""
log "${W} 一键安装${N} ${D}— 依次做四件事:装内核、写 systemd 服务、准备配置、启动${N}"
log " ${D}内核 ${N}$BIN"
log " ${D}配置 ${N}$(conf_file)"
log " ${D}服务 ${N}${UNIT_FILE}"
log " ${D}版本 ${N}${cur:-未安装} → ${target}"
log ""
if (( DRY_RUN )); then
ok "[dry-run] 会安装 $target、写入 $UNIT_FILE、并在缺配置时启动向导"; return 0
fi
# 1. 内核
if [[ "$cur" == "$target" && $FORCE -eq 0 ]]; then
info "内核已是 $cur,跳过下载"
else
stage_binary "$target"
[[ "$(svc_state)" == "running" ]] && { info "先停掉正在跑的 $SERVICE"; systemctl stop "$SERVICE" || true; }
if [[ -x "$BIN" && -n "$cur" ]]; then
mkdir -p "$BACKUP_DIR"; cp -a "$BIN" "$BACKUP_DIR/mihomo-$cur"
info "旧内核已备份 → $BACKUP_DIR/mihomo-$cur"
fi
install -m 0755 "$STAGED" "$BIN"
ok "内核已安装: $(version_line "$BIN")"
fi
# 2. 配置
mkdir -p "$CONF_DIR"
local has_conf=0
if [[ -f "$(conf_file)" ]]; then
has_conf=1
ok "已有配置 $(conf_file),保持不动"
confirm " 要用向导重新生成一份吗?" && { cfg_wizard || true; }
else
log ""
warn "还没有配置文件,没有它 mihomo 起不来"
if confirm_default_yes " 现在用向导生成一份?"; then
cfg_wizard && has_conf=1 || has_conf=0
else
info "跳过。自己把 config.yaml 放到 $CONF_DIR/ 之后,再执行: systemctl start $SERVICE"
fi
fi
# 3. systemd
install_unit
# 4. 启动
if (( NO_RESTART )); then
warn "--no-restart: 没有启动服务。手动启动: systemctl start $SERVICE"
elif (( has_conf )); then
start_service || true
else
has_systemd && systemctl enable "$SERVICE" >/dev/null 2>&1 && info "已设为开机自启,但缺配置所以没启动"
fi
print_summary
}
print_summary() {
local port ctrl secret ui
port="$(cfg_get 'mixed-port')"; ctrl="$(cfg_get 'external-controller')"
secret="$(cfg_get 'secret')"; ui="$(cfg_get 'external-ui')"
log ""
log "${G} ── 装好了 ─────────────────────────────${N}"
[[ -n "$port" ]] && {
log " 本机代理 ${W}http://127.0.0.1:${port}${N} ${D}(HTTP 和 SOCKS5 同一个端口)${N}"
log " 临时用起来 ${D}export https_proxy=http://127.0.0.1:${port} http_proxy=http://127.0.0.1:${port}${N}"
}
if [[ -n "$ctrl" && "$ctrl" != '""' ]]; then
local host="${ctrl%%:*}" cport="${ctrl##*:}"
[[ "$host" == "0.0.0.0" ]] && host="<本机IP>"
if [[ -n "$ui" ]]; then
log " Web 面板 ${W}http://${host}:${cport}/ui${N} ${D}密钥 ${secret}${N}"
else
log " 管理接口 ${W}http://${host}:${cport}${N} ${D}密钥 ${secret}${N}"
fi
fi
log " 服务状态 $(systemctl is-active "$SERVICE" 2>/dev/null || echo '未运行') ${D}开机自启: $(svc_enabled)${N}"
log " 看日志 ${D}journalctl -u $SERVICE -f${N}"
log "${G} ───────────────────────────────────────${N}"
}
# ───────────────────────── 卸载 ─────────────────────────
do_uninstall() {
(( DRY_RUN )) || require_root "卸载 mihomo"
log ""
log "${W} 卸载${N} ${D}— 下面这些会被删掉:${N}"
[[ -x "$BIN" ]] && log " 内核 $BIN" || log " ${D}内核 $BIN(不存在)${N}"
[[ -f "$UNIT_FILE" ]] && log " 服务 $UNIT_FILE" || log " ${D}服务 $UNIT_FILE(不存在)${N}"
log " ${D}配置目录 $CONF_DIR 和备份目录 $BACKUP_DIR 会单独问你${N}"
log ""
if (( DRY_RUN )); then ok "[dry-run] 到此为止,什么都没删"; return 0; fi
(( PURGE )) || confirm " 确认卸载?" || { info "已取消"; return 0; }
if has_systemd; then
systemctl stop "$SERVICE" 2>/dev/null || true
systemctl disable "$SERVICE" 2>/dev/null || true
if [[ -f "$UNIT_FILE" ]]; then rm -f "$UNIT_FILE"; ok "已删除 $UNIT_FILE"; fi
rm -f "${UNIT_FILE}".bak-* 2>/dev/null || true
# drop-in 覆盖目录,别人加过 override.conf 的话留着会变孤儿
[[ -d "/etc/systemd/system/${SERVICE}.service.d" ]] && rm -rf "/etc/systemd/system/${SERVICE}.service.d"
systemctl daemon-reload
systemctl reset-failed "$SERVICE" 2>/dev/null || true
ok "systemd 已清理"
fi
if [[ -e "$BIN" ]]; then rm -f "$BIN"; ok "已删除 $BIN"; fi
if [[ -d "$CONF_DIR" ]]; then
if (( PURGE )) || confirm " 连配置目录 $CONF_DIR 一起删?(订阅、节点选择、面板文件都在里面)"; then
rm -rf "$CONF_DIR"; ok "已删除 $CONF_DIR"
else
info "配置保留在 $CONF_DIR,下次重装能直接接上"
fi
fi
if [[ -d "$BACKUP_DIR" ]]; then
if (( PURGE )) || confirm " 连内核备份 $BACKUP_DIR 一起删?"; then
rm -rf "$BACKUP_DIR"; ok "已删除 $BACKUP_DIR"
else
info "内核备份保留在 $BACKUP_DIR"
fi
fi
ok "卸载完成"
}
# ───────────────────────── 菜单 ─────────────────────────
banner() {
printf '%s\n' "" >&2
cat >&2 <<EOF
${C} ╭──────────────────────────────────────────╮
│ ${W}mihomo 内核管理${N}${C} ╭( ・ㅂ・)و ✧ │
╰──────────────────────────────────────────╯${N}
EOF
}
status_panel() {
local cur latest svc conf enabled
cur="$(current_version)"; svc="$(svc_state)"; enabled="$(svc_enabled)"
latest="$(latest_version 2>/dev/null || echo '查询失败')"
if [[ -x "$BIN" && -f "$(conf_file)" ]]; then
"$BIN" -t -d "$CONF_DIR" >/dev/null 2>&1 && conf="${G}有效${N}" || conf="${R}有错${N}"
elif [[ -f "$(conf_file)" ]]; then
conf="${D}有文件, 未校验${N}"
else
conf="${Y}未配置${N}"
fi
local cur_s="${cur:-${Y}未安装${N}}" latest_s="$latest"
[[ -n "$cur" && "$cur" != "$latest" && "$latest" != "查询失败" ]] && latest_s="${Y}${latest} ← 有新版${N}"
[[ -n "$cur" && "$cur" == "$latest" ]] && latest_s="${G}${latest}${N}"
case "$svc" in
running) svc="${G}● 运行中${N}" ;;
stopped) svc="${Y}○ 已停止${N}" ;;
未安装) svc="${D}未安装服务${N}" ;;
*) svc="${D}${svc}${N}" ;;
esac
[[ "$enabled" == "enabled" ]] && svc="$svc ${D}(开机自启)${N}"
printf ' %s当前版本%s %s\n' "$D" "$N" "$cur_s" >&2
printf ' %s最新版本%s %s\n' "$D" "$N" "$latest_s" >&2
printf ' %s服务状态%s %s\n' "$D" "$N" "$svc" >&2
printf ' %s配置文件%s %s\n' "$D" "$N" "$conf" >&2
printf ' %s架构 %s %s\n' "$D" "$N" "$(detect_arch)" >&2
}
menu() {
local warned_root=0
while true; do
clear 2>/dev/null || true
banner
status_panel
if [[ ${EUID:-$(id -u)} -ne 0 && $warned_root -eq 0 ]]; then
warned_root=1
log ""
warn "现在不是 root。查看类操作没问题,但安装/升级/改配置/卸载都会被拒绝,建议 sudo 重跑本脚本"
fi
local first="🚀 一键安装 ${D}(内核 + systemd + 配置)${N}"
[[ -x "$BIN" ]] && first="🚀 重新安装 ${D}(补齐 systemd / 配置)${N}"
cat >&2 <<EOF
${W} 选个操作吧:${N}
${C}1.${N} $first
${C}2.${N} 🔍 检查更新
${C}3.${N} ⬆️ 升级内核到最新版
${C}4.${N} 📌 安装指定版本内核
${C}5.${N} ⏪ 回滚到上一版内核
${C}6.${N} 📦 查看内核备份
${C}7.${N} ⚙️ 配置管理 ${D}(订阅 / 端口 / 面板 / 校验)${N}
${C}8.${N} 🎛️ 服务控制 ${D}(启停 / 日志 / 自启)${N}
${C}9.${N} 🗑️ 卸载
${C}0.${N} 👋 退出
EOF
local c; ask "$(printf '\n %s>%s ' "$W" "$N")" c
echo >&2
case "$c" in
1) do_install || warn "安装没走完,看上面的报错"; pause ;;
2) local cur latest; cur="$(current_version)"; latest="$(latest_version 2>/dev/null || true)"
if [[ -z "$latest" ]]; then warn "查不到最新版本号,GitHub 大概不通;可以退出后加 --mirror <反代前缀> 重跑"
elif [[ -z "$cur" ]]; then warn "内核还没装,选 1 一键安装"
elif [[ "$cur" == "$latest" ]]; then ok "已是最新 $cur"
else warn "有新版: $cur → $latest"
confirm " 现在升级?" && { do_update "$latest" || warn "升级未完成"; }; fi
pause ;;
3) local latest; latest="$(latest_version 2>/dev/null || true)"
if [[ -z "$latest" ]]; then warn "查不到最新版本号,GitHub 大概不通;可以退出后加 --mirror <反代前缀> 重跑"
else confirm " 升级/重装内核到 $latest ?" && { FORCE=1; do_update "$latest" || warn "升级未完成"; FORCE=0; }; fi
pause ;;
4) local v; ask " 输入版本号 (如 v1.19.28): " v
if [[ "$v" =~ ^v[0-9] ]]; then FORCE=1; do_update "$v" || warn "安装未完成"; FORCE=0
else warn "版本号要以 v 开头,比如 v1.19.28"; fi
pause ;;
5) confirm " 确认回滚到上一个备份?" && { do_rollback || true; }; pause ;;
6) echo >&2; do_list; pause ;;
7) conf_menu ;;
8) svc_menu ;;
9) do_uninstall || true; pause ;;
0|q|"") log " ${D}bye~${N}"; exit 0 ;;
*) warn "没这个选项啦"; sleep 1 ;;
esac
done
}
conf_menu() {
while true; do
local sub port lan ctrl
sub="$(cfg_sub_url || true)"; port="$(cfg_get 'mixed-port' || true)"
lan="$(cfg_get 'allow-lan' || true)"; ctrl="$(cfg_get 'external-controller' || true)"
cat >&2 <<EOF
${W} 配置管理${N} ${D}$(conf_file)${N}
${D}代理端口 ${N}${port:-—} ${D}局域网共享 ${N}${lan:-—} ${D}管理接口 ${N}${ctrl:-未开}
${D}订阅 ${N}${sub:-—}
${C}1.${N} 🧙 生成配置 ${D}(向导, 会覆盖现有配置)${N}
${C}2.${N} 🔗 更换订阅链接
${C}3.${N} 🔌 改代理端口
${C}4.${N} 📡 局域网共享 开/关
${C}5.${N} 🎚️ 管理接口 / 面板密钥
${C}6.${N} 📥 从 URL 导入完整配置
${C}7.${N} 🧪 校验配置
${C}8.${N} 📝 手动编辑配置
${C}9.${N} 💾 配置备份 / 还原
${C}0.${N} ↩️ 返回
EOF
local c; ask "$(printf '\n %s>%s ' "$W" "$N")" c
echo >&2
case "$c" in
1) cfg_wizard || true; pause ;;
2) cfg_change_sub || true; pause ;;
3) cfg_change_port || true; pause ;;
4) cfg_toggle_lan || true; pause ;;
5) cfg_change_ctrl || true; pause ;;
6) cfg_import_url || true; pause ;;
7) cfg_validate || true; pause ;;
8) cfg_edit || true; pause ;;
9) cfg_backup_menu || true; pause ;;
0|q|"") return 0 ;;
*) warn "没这个选项啦"; sleep 1 ;;
esac
done
}
svc_menu() {
cat >&2 <<EOF
${C}1.${N} ▶️ 启动 ${C}2.${N} ⏹️ 停止 ${C}3.${N} 🔄 重启
${C}4.${N} 📊 状态 ${C}5.${N} 📜 最近日志
${C}6.${N} ⏻ 开机自启 开/关 ${C}7.${N} 🧾 重写服务文件
${C}0.${N} ↩️ 返回
EOF
local c; ask "$(printf '\n %s>%s ' "$W" "$N")" c
case "$c" in
1) systemctl start "$SERVICE" && ok "已启动" || warn "启动失败,看日志: journalctl -u $SERVICE -n 50" ;;
2) systemctl stop "$SERVICE" && ok "已停止" ;;
3) systemctl restart "$SERVICE" && ok "已重启" || warn "重启失败,看日志: journalctl -u $SERVICE -n 50" ;;
4) systemctl status "$SERVICE" --no-pager || true ;;
5) journalctl -u "$SERVICE" -n 40 --no-pager || true ;;
6) if [[ "$(svc_enabled)" == "enabled" ]]; then
systemctl disable "$SERVICE" && ok "已关闭开机自启"
else
systemctl enable "$SERVICE" && ok "已打开开机自启"
fi ;;
7) install_unit && ok "服务文件已刷新;生效需要重启服务" || true ;;
*) return 0 ;;
esac
pause
}
# ───────────────────────── 参数解析 ─────────────────────────
CMD=""
if [[ $# -eq 0 ]]; then
# stdin 是终端就进菜单;被重定向/管道(cron、CI)就安静地跑 check
if [[ -t 0 ]] || { [[ -t 1 ]] && [[ -r /dev/tty ]]; }; then CMD="menu"; else CMD="check"; fi
else
case "$1" in
check|update|install|uninstall|config|service|rollback|list|menu) CMD="$1"; shift ;;
-h|--help|help) usage 0 ;;
-v|--version-self) echo "mihomo-up.sh v${SELF_VERSION}"; exit 0 ;;
-*) CMD="check" ;;
*) echo "未知子命令: $1" >&2; usage 1 ;;
esac
fi
while (( $# )); do
case "$1" in
--force) FORCE=1 ;;
-y|--yes) ASSUME_YES=1 ;;
-n|--dry-run) DRY_RUN=1 ;;
--no-restart) NO_RESTART=1 ;;
--purge) PURGE=1 ;;
--skip-conf-check) SKIP_CONF_CHECK=1 ;;
--json) JSON=1 ;;
--version) WANT_VERSION="${2:?--version 后面要跟版本号}"; shift ;;
--arch) ARCH_OVERRIDE="${2:?--arch 后面要跟架构}"; shift ;;
--variant) VARIANT="${2:?--variant 后面要跟 v1/v2/v3/compatible}"; shift ;;
--mirror) MIRROR="${2:?--mirror 后面要跟下载前缀}"; shift ;;
--bin) BIN="${2:?--bin 后面要跟路径}"; shift ;;
--conf-dir) CONF_DIR="${2:?--conf-dir 后面要跟目录}"; shift ;;
--service) SERVICE="${2:?--service 后面要跟服务名}"; UNIT_FILE="/etc/systemd/system/${SERVICE}.service"; shift ;;
-h|--help) usage 0 ;;
*) die "未知参数: $1(用 --help 看全部选项)" ;;
esac
shift
done
need curl; need gzip; need install
# ───────────────────────── 分发 ─────────────────────────
case "$CMD" in
menu) menu ;;
install) do_install ;;
uninstall) do_uninstall ;;
config) cfg_wizard ;;
service) install_unit ;;
list) do_list ;;
rollback) do_rollback ;;
check)
CUR="$(current_version)"; TARGET="${WANT_VERSION:-$(latest_version)}"
(( JSON )) && printf '{"current":"%s","latest":"%s","outdated":%s}\n' \
"${CUR:-null}" "$TARGET" "$([[ "$CUR" != "$TARGET" ]] && echo true || echo false)"
log "当前: ${CUR:-${Y}未安装${N}} 最新: $TARGET"
[[ -z "$CUR" ]] && { log "→ 还没装,跑 install 一键安装"; exit 10; }
[[ "$CUR" == "$TARGET" ]] && { ok "已是最新"; exit 0; }
warn "有新版本可升级"; exit 10 ;;
update)
TARGET="${WANT_VERSION:-$(latest_version)}"
CUR="$(current_version)"
log "当前: ${CUR:-${Y}未安装${N}} 目标: $TARGET"
do_update "$TARGET" ;;
esac