feat: 快捷命令 mihomo-up
装一个启动器到 PATH,每次执行都拉取最新脚本再运行,仓库更新后机器上 无需手动更新。 它同时落一份缓存,并在拉取失败、或拉到的内容 bash -n 不过时回落到缓存。 这个脚本管的是代理,最需要它的时候往往正是网络不正常的时候。 顺带按 think-twice 过了一遍提示文案,并让子命令失败时干净退出, 不再在人话提示后面追加一句「第 N 行出错」。
This commit is contained in:
+139
-26
@@ -25,7 +25,13 @@ BACKUP_DIR="${MIHOMO_BACKUP_DIR:-/var/backups/mihomo}"
|
||||
UNIT_FILE="/etc/systemd/system/${SERVICE}.service"
|
||||
KEEP=3
|
||||
|
||||
# GitHub 下载前缀。国内机器 GitHub 不通时填个反代,例如 https://ghfast.top/
|
||||
# 快捷命令: 装一个 mihomo-up 到 PATH 里,省得每次都记那条 curl 长链接。
|
||||
# 它是个启动器,每次执行先拉最新脚本,拉不到就回落到 CACHE_DIR 里的缓存。
|
||||
SCRIPT_URL="${MIHOMO_UP_URL:-https://git.im0o.top/im0o/mihomo-up/raw/branch/main/mihomo-up.sh}"
|
||||
LAUNCHER="${MIHOMO_UP_LAUNCHER:-/usr/local/bin/mihomo-up}"
|
||||
CACHE_DIR="${MIHOMO_UP_CACHE:-/var/lib/mihomo-up}"
|
||||
|
||||
# GitHub 下载前缀。机器直连 GitHub 不通或太慢时填个反代,例如 https://ghfast.top/
|
||||
MIRROR="${MIHOMO_MIRROR:-}"
|
||||
|
||||
ARCH_OVERRIDE=""; VARIANT=""; WANT_VERSION=""
|
||||
@@ -65,6 +71,7 @@ mihomo-up.sh — 安装 / 配置 / 升级 / 卸载 mihomo 内核
|
||||
mihomo-up.sh update [--force] 只换内核二进制,不碰配置和服务定义
|
||||
mihomo-up.sh config 进入配置向导,重新生成 config.yaml
|
||||
mihomo-up.sh service 只写入/刷新 systemd 服务文件
|
||||
mihomo-up.sh shortcut 只装/刷新 mihomo-up 快捷命令
|
||||
mihomo-up.sh rollback 回滚到上一个备份的内核
|
||||
mihomo-up.sh list 列出本地内核备份
|
||||
mihomo-up.sh uninstall [--purge] 卸载内核和服务;--purge 连配置和备份一起删
|
||||
@@ -81,6 +88,7 @@ mihomo-up.sh — 安装 / 配置 / 升级 / 卸载 mihomo 内核
|
||||
--arch <a> amd64 | arm64 | armv7
|
||||
--variant <v> amd64 微架构: v1 | v2 | v3 | compatible
|
||||
--mirror <url> GitHub 下载前缀,如 https://ghfast.top/
|
||||
--script-url <u> 快捷命令每次去拉的脚本地址(fork 之后指到你自己那份)
|
||||
--bin <path> 内核路径,默认 /usr/local/bin/mihomo
|
||||
--conf-dir <dir> 配置目录,默认 /etc/mihomo
|
||||
--service <name> systemd 服务名,默认 mihomo
|
||||
@@ -248,7 +256,7 @@ do_update() { # $1 = 目标版本;只换二进制,不
|
||||
ok "现有配置在 $target 下校验通过"
|
||||
else
|
||||
warn "$target 校验现有配置失败:"; sed 's/^/ /' <<<"$out" >&2
|
||||
die "没有替换任何文件。先把配置改好(菜单「配置管理 → 校验配置」可以反复验),或加 --skip-conf-check 跳过这道检查"
|
||||
die "没有替换任何文件。先按上面的报错改配置(改完用 $BIN -t -d $CONF_DIR 复验),或加 --skip-conf-check 只换内核不管配置"
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -376,6 +384,78 @@ start_service() {
|
||||
fi
|
||||
}
|
||||
|
||||
# ───────────────────────── 快捷命令 ─────────────────────────
|
||||
# 设计取舍: 启动器每次跑都拉最新脚本,所以你改了仓库、客户那边下次执行就用上了,
|
||||
# 不需要谁去手动更新。但它落一份缓存并在拉取失败时回落 —— 这个脚本管的是代理,
|
||||
# 你最需要它的时候常常正是网络不正常的时候,那时候必须还能进菜单重启服务、看日志。
|
||||
write_launcher() {
|
||||
local out="$1"
|
||||
{
|
||||
printf '#!/usr/bin/env bash\n'
|
||||
printf '# mihomo-up 启动器 — 由 mihomo-up.sh v%s 生成,别手动改,重装会覆盖\n' "$SELF_VERSION"
|
||||
printf '# 先拉最新脚本跑;拉不到 (网络断了 / 仓库挂了) 就用本地缓存。\n'
|
||||
printf 'SRC="${MIHOMO_UP_URL:-%s}"\n' "$SCRIPT_URL"
|
||||
printf 'CACHE="%s/mihomo-up.sh"\n' "$CACHE_DIR"
|
||||
cat <<'EOF'
|
||||
TMP="$(mktemp)"; trap 'rm -f "$TMP"' EXIT
|
||||
RUN=""
|
||||
|
||||
# --connect-timeout / --max-time: 网络卡住时别让菜单一直转,那正是要用它的时候
|
||||
if curl -fsSL --connect-timeout 5 --max-time 20 "$SRC" -o "$TMP" && [[ -s "$TMP" ]]; then
|
||||
# 拿到了不等于能跑: 上游推坏了、或者中间设备塞回来一个门户登录页,
|
||||
# 都不该以 root 执行。语法过不了就当这次没拉到, 回落缓存。
|
||||
if bash -n "$TMP" 2>/dev/null; then
|
||||
mkdir -p "${CACHE%/*}" 2>/dev/null || true
|
||||
if install -m 0755 "$TMP" "$CACHE" 2>/dev/null; then RUN="$CACHE"; else RUN="$TMP"; fi
|
||||
else
|
||||
printf '! 拉到的内容没通过语法检查,已丢弃 (上游坏了? 返回的根本不是脚本?)\n' >&2
|
||||
fi
|
||||
else
|
||||
printf '! 拉取最新脚本失败: %s\n' "$SRC" >&2
|
||||
fi
|
||||
|
||||
if [[ -z "$RUN" && -x "$CACHE" ]]; then
|
||||
printf ' 改用本地缓存 (%s)\n' "$(date -r "$CACHE" '+%F %T')" >&2
|
||||
RUN="$CACHE"
|
||||
fi
|
||||
[[ -n "$RUN" ]] || { printf '✗ 远端拉不到,本地也没有缓存,没得跑了\n' >&2; exit 1; }
|
||||
|
||||
bash "$RUN" "$@"
|
||||
EOF
|
||||
} > "$out"
|
||||
}
|
||||
|
||||
seed_cache() { # 先放一份缓存,这样装完立刻断网也能用
|
||||
local cache="$CACHE_DIR/mihomo-up.sh"
|
||||
mkdir -p "$CACHE_DIR"
|
||||
# bash <(curl ...) 跑的时候 $0 是条管道,内容已被 bash 读掉一截,复制不出完整脚本,
|
||||
# 只有 $0 是真实文件时才能直接拷,否则老老实实重新下一份。
|
||||
if [[ -f "$0" && "$0" != /dev/fd/* && "$0" != /proc/* ]]; then
|
||||
install -m 0755 "$0" "$cache"; return 0
|
||||
fi
|
||||
local tmp; tmp="$(mktemp)"
|
||||
if curl -fsSL --retry 2 --connect-timeout 10 "$SCRIPT_URL" -o "$tmp" \
|
||||
&& [[ -s "$tmp" ]] && bash -n "$tmp" 2>/dev/null; then
|
||||
install -m 0755 "$tmp" "$cache"; rm -f "$tmp"; return 0
|
||||
fi
|
||||
rm -f "$tmp"
|
||||
warn "缓存没建立: 从 $SCRIPT_URL 下载失败"
|
||||
warn "快捷命令照样能用,但这台机器断网时它既拉不到脚本、也没有缓存可回落,会直接报错"
|
||||
return 1
|
||||
}
|
||||
|
||||
install_launcher() {
|
||||
require_root "写入 $LAUNCHER"
|
||||
if (( DRY_RUN )); then ok "[dry-run] 会写入快捷命令 $LAUNCHER"; return 0; fi
|
||||
|
||||
local tmp; tmp="$(mktemp)"
|
||||
write_launcher "$tmp"
|
||||
bash -n "$tmp" || { rm -f "$tmp"; die "生成的启动器语法有问题,没有安装(这是脚本自身的 bug,请反馈)"; }
|
||||
install -m 0755 "$tmp" "$LAUNCHER"; rm -f "$tmp"
|
||||
seed_cache || true
|
||||
ok "快捷命令已装好: 以后直接敲 ${W}$(basename "$LAUNCHER")${N} 就能进菜单"
|
||||
}
|
||||
|
||||
# ───────────────────────── 配置 ─────────────────────────
|
||||
# 说明:下面这组函数只改 config.yaml 顶层的简单键(形如 `mixed-port: 7890` 这种
|
||||
# 顶格写、值在同一行的),改之前一定先备份、改完一定校验,失败就还原。
|
||||
@@ -397,7 +477,7 @@ cfg_restore() { # cfg_restore <备份路径>
|
||||
|
||||
cfg_require() { # 没有配置文件时,给一句能照着做的提示
|
||||
[[ -f "$(conf_file)" ]] && return 0
|
||||
warn "还没有配置文件 $(conf_file),先用「生成配置」跑一遍向导"
|
||||
warn "还没有配置文件 $(conf_file),先跑一遍配置向导生成(菜单「配置管理 → 生成配置」,命令行是 $0 config)"
|
||||
return 1
|
||||
}
|
||||
|
||||
@@ -450,7 +530,7 @@ cfg_wizard() { # 生成一份全新的 config.yaml
|
||||
|
||||
log ""
|
||||
log "${W} 配置向导${N} ${D}— 回车用方括号里的默认值${N}"
|
||||
log "${D} 生成的是一份最小可用配置:一个订阅、一组选择/自动测速分组、国内直连其余走代理。${N}"
|
||||
log "${D} 生成的是一份最小可用配置:一个订阅、一组选择/自动测速分组、内网直连其余走代理。${N}"
|
||||
log ""
|
||||
|
||||
local sub port lan ctrl_mode ctrl secret ui rules_mode
|
||||
@@ -491,7 +571,18 @@ cfg_wizard() { # 生成一份全新的 config.yaml
|
||||
ui="no"
|
||||
fi
|
||||
|
||||
ask_default " 分流规则: 1=国内直连、其余走代理 2=全部走代理" "1" rules_mode
|
||||
log ""
|
||||
log " ${D}分流规则。内网地址在两种模式下都是直连。${N}"
|
||||
ask_default " 1=全部走代理 2=指定国家/地区直连,其余走代理" "1" rules_mode
|
||||
local direct_cc=""
|
||||
if [[ "$rules_mode" == "2" ]]; then
|
||||
ask_default " 哪个国家/地区直连(两位 ISO 代码,如 CN / US / JP / DE)" "CN" direct_cc
|
||||
direct_cc="$(printf '%s' "$direct_cc" | tr '[:lower:]' '[:upper:]')"
|
||||
if [[ ! "$direct_cc" =~ ^[A-Z]{2}$ ]]; then
|
||||
warn " «$direct_cc» 不像两位国家代码,这条直连规则就先不写了,需要的话之后手动加 GEOIP 规则"
|
||||
direct_cc=""
|
||||
fi
|
||||
fi
|
||||
|
||||
# 先写到临时文件,确认无误再落盘
|
||||
local tmp; tmp="$(mktemp)"
|
||||
@@ -569,14 +660,11 @@ EOF
|
||||
- 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
|
||||
if [[ -n "$direct_cc" ]]; then
|
||||
printf ' # 目标 IP 属于 %s 就直连;用到 GEOIP 的话内核首次启动会去下载 GeoIP 数据库\n' "$direct_cc"
|
||||
printf ' - GEOIP,%s,DIRECT\n' "$direct_cc"
|
||||
fi
|
||||
printf ' - MATCH,PROXY\n'
|
||||
} > "$tmp"
|
||||
|
||||
local bak=""
|
||||
@@ -638,7 +726,7 @@ cfg_toggle_lan() {
|
||||
local bak; bak="$(cfg_backup)"
|
||||
cfg_set 'allow-lan' "$new"
|
||||
if [[ "$new" == "true" ]]; then
|
||||
ok "已允许局域网设备连本机代理(记得放行防火墙端口)"
|
||||
ok "已允许局域网设备连本机代理。还要在防火墙放行 $(cfg_get 'mixed-port') 端口,否则外面照样连不进来"
|
||||
else
|
||||
ok "已改回只允许本机使用"
|
||||
fi
|
||||
@@ -658,7 +746,8 @@ cfg_change_ctrl() {
|
||||
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}" ;;
|
||||
ok "管理接口 → 0.0.0.0:${p},密钥 ${s}"
|
||||
warn "0.0.0.0 意味着任何能连到这台机器的人都能访问它,请用防火墙只放行你信任的来源" ;;
|
||||
*) local p; ask_default " 端口" "9090" p
|
||||
cfg_set 'external-controller' "127.0.0.1:${p}"
|
||||
local s; ask_default " 访问密钥" "${cur_secret:-$(gen_secret)}" s
|
||||
@@ -727,7 +816,7 @@ do_install() {
|
||||
fi
|
||||
|
||||
log ""
|
||||
log "${W} 一键安装${N} ${D}— 依次做四件事:装内核、写 systemd 服务、准备配置、启动${N}"
|
||||
log "${W} 一键安装${N} ${D}— 依次做五件事:装内核、准备配置、写 systemd 服务、装快捷命令、启动${N}"
|
||||
log " ${D}内核 ${N}$BIN"
|
||||
log " ${D}配置 ${N}$(conf_file)"
|
||||
log " ${D}服务 ${N}${UNIT_FILE}"
|
||||
@@ -735,7 +824,7 @@ do_install() {
|
||||
log ""
|
||||
|
||||
if (( DRY_RUN )); then
|
||||
ok "[dry-run] 会安装 $target、写入 $UNIT_FILE、并在缺配置时启动向导"; return 0
|
||||
ok "[dry-run] 会安装 $target、写入 $UNIT_FILE 和快捷命令 $LAUNCHER、并在缺配置时启动向导"; return 0
|
||||
fi
|
||||
|
||||
# 1. 内核
|
||||
@@ -772,7 +861,16 @@ do_install() {
|
||||
# 3. systemd
|
||||
install_unit
|
||||
|
||||
# 4. 启动
|
||||
# 4. 快捷命令
|
||||
if [[ -x "$LAUNCHER" ]]; then
|
||||
install_launcher # 已经有了就顺手刷新成当前版本
|
||||
elif confirm_default_yes " 装个 $(basename "$LAUNCHER") 快捷命令,省得每次记 curl 地址?"; then
|
||||
install_launcher
|
||||
else
|
||||
info "跳过。下次还用那条 bash <(curl ...) 进菜单"
|
||||
fi
|
||||
|
||||
# 5. 启动
|
||||
if (( NO_RESTART )); then
|
||||
warn "--no-restart: 没有启动服务。手动启动: systemctl start $SERVICE"
|
||||
elif (( has_conf )); then
|
||||
@@ -806,6 +904,8 @@ print_summary() {
|
||||
fi
|
||||
log " 服务状态 $(systemctl is-active "$SERVICE" 2>/dev/null || echo '未运行') ${D}开机自启: $(svc_enabled)${N}"
|
||||
log " 看日志 ${D}journalctl -u $SERVICE -f${N}"
|
||||
[[ -x "$LAUNCHER" ]] && \
|
||||
log " 再进菜单 ${W}$(basename "$LAUNCHER")${N} ${D}(每次自动拉最新脚本,拉不到就用本地缓存)${N}"
|
||||
log "${G} ───────────────────────────────────────${N}"
|
||||
}
|
||||
|
||||
@@ -817,6 +917,7 @@ do_uninstall() {
|
||||
log "${W} 卸载${N} ${D}— 下面这些会被删掉:${N}"
|
||||
[[ -x "$BIN" ]] && log " 内核 $BIN" || log " ${D}内核 $BIN(不存在)${N}"
|
||||
[[ -f "$UNIT_FILE" ]] && log " 服务 $UNIT_FILE" || log " ${D}服务 $UNIT_FILE(不存在)${N}"
|
||||
[[ -x "$LAUNCHER" ]] && log " 快捷命令 $LAUNCHER 及缓存 $CACHE_DIR"
|
||||
log " ${D}配置目录 $CONF_DIR 和备份目录 $BACKUP_DIR 会单独问你${N}"
|
||||
log ""
|
||||
|
||||
@@ -837,6 +938,11 @@ do_uninstall() {
|
||||
|
||||
if [[ -e "$BIN" ]]; then rm -f "$BIN"; ok "已删除 $BIN"; fi
|
||||
|
||||
# 快捷命令和它的缓存。注意: 你很可能正是从缓存里那份脚本跑起来的,删掉没关系 ——
|
||||
# Linux 上文件被删除后 inode 还在,bash 手里的 fd 仍然有效,这一趟能跑完。
|
||||
[[ -e "$LAUNCHER" ]] && { rm -f "$LAUNCHER"; ok "已删除快捷命令 $LAUNCHER"; }
|
||||
[[ -d "$CACHE_DIR" ]] && { rm -rf "$CACHE_DIR"; ok "已删除脚本缓存 $CACHE_DIR"; }
|
||||
|
||||
if [[ -d "$CONF_DIR" ]]; then
|
||||
if (( PURGE )) || confirm " 连配置目录 $CONF_DIR 一起删?(订阅、节点选择、面板文件都在里面)"; then
|
||||
rm -rf "$CONF_DIR"; ok "已删除 $CONF_DIR"
|
||||
@@ -953,7 +1059,7 @@ EOF
|
||||
8) svc_menu ;;
|
||||
9) do_uninstall || true; pause ;;
|
||||
0|q|"") log " ${D}bye~${N}"; exit 0 ;;
|
||||
*) warn "没这个选项啦"; sleep 1 ;;
|
||||
*) warn "没有「$c」这个选项,输入上面列出的数字,或 0 退出/返回"; sleep 1 ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
@@ -994,7 +1100,7 @@ EOF
|
||||
8) cfg_edit || true; pause ;;
|
||||
9) cfg_backup_menu || true; pause ;;
|
||||
0|q|"") return 0 ;;
|
||||
*) warn "没这个选项啦"; sleep 1 ;;
|
||||
*) warn "没有「$c」这个选项,输入上面列出的数字,或 0 退出/返回"; sleep 1 ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
@@ -1005,6 +1111,7 @@ svc_menu() {
|
||||
${C}1.${N} ▶️ 启动 ${C}2.${N} ⏹️ 停止 ${C}3.${N} 🔄 重启
|
||||
${C}4.${N} 📊 状态 ${C}5.${N} 📜 最近日志
|
||||
${C}6.${N} ⏻ 开机自启 开/关 ${C}7.${N} 🧾 重写服务文件
|
||||
${C}8.${N} 🔗 装/刷新 $(basename "$LAUNCHER") 快捷命令
|
||||
${C}0.${N} ↩️ 返回
|
||||
EOF
|
||||
local c; ask "$(printf '\n %s>%s ' "$W" "$N")" c
|
||||
@@ -1020,6 +1127,7 @@ EOF
|
||||
systemctl enable "$SERVICE" && ok "已打开开机自启"
|
||||
fi ;;
|
||||
7) install_unit && ok "服务文件已刷新;生效需要重启服务" || true ;;
|
||||
8) install_launcher || true ;;
|
||||
*) return 0 ;;
|
||||
esac
|
||||
pause
|
||||
@@ -1032,7 +1140,7 @@ if [[ $# -eq 0 ]]; then
|
||||
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 ;;
|
||||
check|update|install|uninstall|config|service|shortcut|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" ;;
|
||||
@@ -1053,6 +1161,7 @@ while (( $# )); do
|
||||
--arch) ARCH_OVERRIDE="${2:?--arch 后面要跟架构}"; shift ;;
|
||||
--variant) VARIANT="${2:?--variant 后面要跟 v1/v2/v3/compatible}"; shift ;;
|
||||
--mirror) MIRROR="${2:?--mirror 后面要跟下载前缀}"; shift ;;
|
||||
--script-url) SCRIPT_URL="${2:?--script-url 后面要跟脚本地址}"; 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 ;;
|
||||
@@ -1065,14 +1174,18 @@ done
|
||||
need curl; need gzip; need install
|
||||
|
||||
# ───────────────────────── 分发 ─────────────────────────
|
||||
# 这些动作失败时自己已经打印过原因了,用 || exit 1 收尾:
|
||||
# 直接让它们返回非零会撞上 ERR trap,在人话提示后面再补一句「第 N 行出错」,
|
||||
# 看起来像脚本崩了,其实只是"没做成"。
|
||||
case "$CMD" in
|
||||
menu) menu ;;
|
||||
install) do_install ;;
|
||||
uninstall) do_uninstall ;;
|
||||
config) cfg_wizard ;;
|
||||
service) install_unit ;;
|
||||
install) do_install || exit 1 ;;
|
||||
uninstall) do_uninstall || exit 1 ;;
|
||||
config) cfg_wizard || exit 1 ;;
|
||||
service) install_unit || exit 1 ;;
|
||||
shortcut) install_launcher || exit 1 ;;
|
||||
list) do_list ;;
|
||||
rollback) do_rollback ;;
|
||||
rollback) do_rollback || exit 1 ;;
|
||||
check)
|
||||
CUR="$(current_version)"; TARGET="${WANT_VERSION:-$(latest_version)}"
|
||||
(( JSON )) && printf '{"current":"%s","latest":"%s","outdated":%s}\n' \
|
||||
@@ -1085,5 +1198,5 @@ case "$CMD" in
|
||||
TARGET="${WANT_VERSION:-$(latest_version)}"
|
||||
CUR="$(current_version)"
|
||||
log "当前: ${CUR:-${Y}未安装${N}} 目标: $TARGET"
|
||||
do_update "$TARGET" ;;
|
||||
do_update "$TARGET" || exit 1 ;;
|
||||
esac
|
||||
|
||||
Reference in New Issue
Block a user