原本只换内核二进制。现在补上从零到能用的完整链路:装内核 → 写 systemd 服务 → 向导生成 config.yaml → 启动,以及反过来的卸载清理。 所有配置改动统一走「先备份 → 改 → 用内核 -t 校验 → 不过就问是否还原」。
1090 lines
42 KiB
Bash
1090 lines
42 KiB
Bash
#!/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
|