diff --git a/README.md b/README.md index 64b4ce4..66ac815 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,30 @@ # mihomo-up -一个 bash 脚本,在 Linux 服务器上**装好、配好、管住** [mihomo](https://github.com/MetaCubeX/mihomo) 内核(Clash.Meta 的继任者,MetaCubeX 维护的代理内核)。 +一个 bash 脚本,在 Linux 服务器上装好、配好、管住 [mihomo](https://github.com/MetaCubeX/mihomo) 内核(Clash.Meta 的继任者,MetaCubeX 维护的代理内核)。 一条命令从零到能用:下载内核 → 写 systemd 服务 → 用向导生成 `config.yaml` → 启动。之后改订阅、换端口、升级、回滚、卸载也都在同一个菜单里。 -它管这三样东西,别的不碰: +装完,这台机器上会多出这几样东西——**卸载时它们全都能一键清掉**: | | 默认位置 | |---|---| | 内核二进制 | `/usr/local/bin/mihomo` | -| systemd 服务 | `/etc/systemd/system/mihomo.service` | | 配置文件 | `/etc/mihomo/config.yaml` | +| systemd 服务 | `/etc/systemd/system/mihomo.service` | +| 快捷命令 | `/usr/local/bin/mihomo-up` | +| 脚本缓存 | `/var/lib/mihomo-up/` | +| 内核备份 | `/var/backups/mihomo/` | + +除此之外它不碰你系统里的任何东西。 + +## 环境要求 + +- Linux + bash 4 以上 +- 依赖命令:`curl`、`gzip`、`install`、`awk`、`sed`(一般发行版自带) +- 安装、升级、改配置、卸载都需要 **root**。非 root 也能进菜单,会先提醒一句 +- 没有 systemd 的机器(容器等)也能用:脚本跳过服务相关步骤,只管内核和配置,你自己 `mihomo -d /etc/mihomo` 拉起来 + +交互输入读的是 `/dev/tty` 而不是标准输入,所以 `bash <(curl ...)` 这种 stdin 已被占用的跑法,菜单照样能按键。 ## 快速开始 @@ -20,7 +34,9 @@ bash <(curl -fsSL https://git.im0o.top/im0o/mihomo-up/raw/branch/main/mihomo-up.sh) ``` -菜单第 1 项就是一键安装。跑完你会拿到:本机的 HTTP/SOCKS5 代理端口、一个 Web 面板地址和它的访问密钥,以及一个开机自启的 systemd 服务。 +菜单第 1 项就是一键安装。跑完你会拿到:本机的 HTTP/SOCKS5 代理端口、一个 Web 面板地址和它的访问密钥、一个开机自启的 systemd 服务。 + +还会拿到一个 `mihomo-up` 命令——**以后直接敲它就能进菜单**,不用再翻上面这条 URL。 **命令行**: @@ -33,6 +49,7 @@ sudo ./mihomo-up.sh update # 只换内核二进制,配置和服务定 sudo ./mihomo-up.sh config # 重新跑配置向导 sudo ./mihomo-up.sh rollback # 内核回滚到上一个备份 sudo ./mihomo-up.sh uninstall # 卸载,会逐项问你配置和备份删不删 +sudo ./mihomo-up.sh shortcut # 只装/刷新 mihomo-up 快捷命令 ``` ## 菜单里有什么 @@ -50,12 +67,35 @@ sudo ./mihomo-up.sh uninstall # 卸载,会逐项问你配置和备份删 1. **下内核**:按机器架构从 GitHub Releases 取对应的 `.gz` 并解压 2. **验内核**:新二进制必须能执行,且 `-v` 自报的版本号和目标一致,否则不落盘 -3. **配文件**:没有 `config.yaml` 就跑配置向导(下面详述);已经有了就保持不动,只问一句要不要重做 -4. **写服务**:按 [MetaCubeX 官方文档](https://wiki.metacubex.one/startup/service/)的推荐写 `mihomo.service`;已存在且内容不同的话,旧文件先备份成 `.bak-<时间戳>` -5. **起服务**:`daemon-reload` → `enable`(开机自启)→ `restart`,起不来就提示你看 `journalctl` +3. **配文件**:没有 `config.yaml` 就跑配置向导;已经有了就保持不动,只问一句要不要重做 +4. **写服务**:按 [MetaCubeX 官方文档](https://wiki.metacubex.one/startup/service/)的推荐写 `mihomo.service`。已存在且内容不同的话,旧文件先备份成 `.bak-<时间戳>` +5. **装快捷命令**:问一句,可以拒绝。它是什么见「快捷命令 `mihomo-up`」一节 +6. **起服务**:`daemon-reload` → `enable`(开机自启)→ `restart`,起不来就提示你看 `journalctl` + +每一步都是「缺什么补什么」:已经装过的机器再跑一遍,不会覆盖你现有的配置。 装完会打印一段总结:代理端口怎么用、面板地址和密钥是什么、日志怎么看。 +## 快捷命令 `mihomo-up` + +一键安装会往 `/usr/local/bin/mihomo-up` 放一个启动器。它**不是脚本本体,而是每次执行都去拉最新脚本再跑**——你更新了仓库,机器上下次敲 `mihomo-up` 就自动用上新版,不需要谁去手动更新。 + +```bash +mihomo-up # 进菜单,参数原样透传 +mihomo-up check --json # 子命令照样能用,退出码也透传 +``` + +和纯粹的"每次重新下载"不同的是,它会把拉到的脚本落一份缓存到 `/var/lib/mihomo-up/`,并在这两种情况下回落到缓存: + +- **拉不到**(网络断了、仓库挂了)。这条对代理管理脚本尤其要紧:你最需要它的时候,往往正是代理挂了、网络不正常的时候,那时候还得能进菜单重启服务、看日志。 +- **拉到的东西没通过 `bash -n` 语法检查**(上游推了个坏版本,或者中间设备塞回来一个门户登录页)。宁可用旧的,也不以 root 执行来路不明的半个脚本。 + +抓取带 5 秒连接 / 20 秒总超时,网络卡住不会让菜单一直转着等。 + +fork 之后想让快捷命令指向你自己那份,装的时候加 `--script-url <你的 raw 地址>`,或者设环境变量 `MIHOMO_UP_URL`。 + +> **cron 里别用它**:每次执行都联网拉脚本,等于给定时任务多加一个失败点。定时任务直接指向缓存那份 `/var/lib/mihomo-up/mihomo-up.sh`,或者你自己固定的副本。 + ## 配置向导问什么 向导生成的是一份**最小可用配置**,够跑,不臃肿: @@ -64,25 +104,30 @@ sudo ./mihomo-up.sh uninstall # 卸载,会逐项问你配置和备份删 |---|---| | 订阅链接 | 机场给的 http(s) 地址,写成 `proxy-providers`,每 24 小时自动更新一次 | | 本地代理端口 | `mixed-port`,HTTP 和 SOCKS5 共用,默认 7890 | -| 局域网共享 | `allow-lan`,开了同网段其它设备也能连(记得放行防火墙) | +| 局域网共享 | `allow-lan`,开了同网段其它设备也能连(还要放行防火墙) | | 管理接口 | 只允许本机 / 局域网公网可访问 / 不开。面板和 API 都连这里 | | 访问密钥 | 回车即自动生成随机串;管理接口开在 `0.0.0.0` 时务必设 | | Web 面板 | 选是就写 `external-ui-url`,内核首次启动自己去下载 metacubexd | -| 分流规则 | 国内直连、其余走代理(默认),或者全部走代理 | +| 分流规则 | 全部走代理(默认),或者指定某个国家/地区直连、其余走代理 | -生成的分组是 `PROXY`(手动选,默认跟着 AUTO)和 `AUTO`(自动挑延迟最低的),规则里内网地址一律直连。 +生成的分组是 `PROXY`(手动选,默认跟着 AUTO)和 `AUTO`(自动挑延迟最低的)。规则里内网地址一律直连。 -**改配置不用手写 YAML**:菜单「配置管理」能直接换订阅、改端口、开关局域网共享、改面板密钥。每次改动之前自动备份成 `config.yaml.bak-<时间戳>`,改完立刻用内核 `-t` 校验,**校验不过会问你要不要还原**,服务在跑的话再问一句要不要重启生效。 +选了「指定国家/地区直连」会额外写一条 `GEOIP,<两位国家代码>,DIRECT`,代码填 `CN`、`US`、`JP`、`DE` 都行,小写会自动转大写。 + +**改配置不用手写 YAML**:菜单「配置管理」能直接换订阅、改端口、开关局域网共享、改面板密钥。 + +每次改动之前自动备份成 `config.yaml.bak-<时间戳>`,改完立刻用内核 `-t` 校验。**校验不过会问你要不要还原**,服务在跑的话再问一句要不要重启生效。 需要手写的复杂配置(DNS、TUN、自定义规则)请用菜单里的「手动编辑配置」,或者用「从 URL 导入完整配置」整份换掉——这两条路同样有备份、校验、还原兜底。 ## 升级和回滚 -`update` 只换内核二进制,不动你的配置和服务定义。两道闸门: +`update` 只换内核二进制,不动你的配置和服务定义。替换前有两道闸门,任一不过就中止: -1. 新二进制能跑起来,且版本号对得上 -2. 用**新版本**去 `-t` 校验你**现有的**配置,通不过就中止,一个文件都不动 - (配置本来就有问题、但你只想换内核时,加 `--skip-conf-check` 跳过这道) +1. 新二进制能跑起来,且 `-v` 自报的版本号和目标一致 +2. 用**新版本**去 `-t` 校验你**现有的**配置 + +第二道是为了挡住「新内核不认旧配置」这种升完才发现的问题。配置本来就有毛病、但你只想换内核时,加 `--skip-conf-check` 跳过它。 替换前旧内核备份到 `/var/backups/mihomo/`(保留最近 3 份)。升级后服务起不来会**自动回滚**到旧版并重启,然后提示你看 `journalctl -u mihomo -n 50`。 @@ -95,9 +140,9 @@ sudo ./mihomo-up.sh uninstall # 逐项确认 sudo ./mihomo-up.sh uninstall --purge # 全删,不问 ``` -直接删掉:内核二进制、`mihomo.service` 及其备份、`mihomo.service.d/` drop-in 目录,并 `daemon-reload` + `reset-failed` 清干净 systemd 状态(服务会先 stop 再 disable)。 +直接删掉:内核二进制、`mihomo.service` 及其备份、`mihomo.service.d/` drop-in 目录、快捷命令和它的缓存。systemd 那边会先 stop 再 disable,然后 `daemon-reload` + `reset-failed` 把状态清干净。 -单独问你:配置目录 `/etc/mihomo`(订阅、节点选择记录、面板文件都在里面)、内核备份目录 `/var/backups/mihomo`。留着的话下次重装能直接接上。 +会单独问你的两项:配置目录 `/etc/mihomo`(订阅、节点选择记录、面板文件都在里面)、内核备份目录 `/var/backups/mihomo`。留着的话下次重装能直接接上。 ## 在 cron / CI 里用 @@ -124,6 +169,16 @@ mihomo-up.sh check && exit 0 {"current":"v1.19.27","latest":"v1.19.28","outdated":true} ``` +## GitHub 直连不通时 + +内核、版本号、Web 面板都从 GitHub 下载。机器直连 GitHub 失败或太慢时,用 `--mirror` 指定一个反代前缀,脚本会把它拼在所有 GitHub 地址前面: + +```bash +sudo ./mihomo-up.sh install --mirror https://ghfast.top/ +``` + +带 `--mirror` 跑配置向导时,生成的配置里会额外写上走同一个镜像的 `geox-url`,免得内核首次启动下载 GeoIP 数据库时卡住。 + ## 选项一览 | 选项 | 作用 | @@ -138,22 +193,13 @@ mihomo-up.sh check && exit 0 | `--json` | `check` 时额外输出一行 JSON | | `--arch ` | 覆盖自动识别的架构:`amd64` / `arm64` / `armv7` | | `--variant ` | amd64 微架构变体:`v1` / `v2` / `v3` / `compatible` | -| `--mirror ` | GitHub 下载前缀,见下 | +| `--mirror ` | GitHub 反代前缀,用法见上一节 | +| `--script-url ` | 快捷命令每次去拉的脚本地址(fork 后指到你自己那份) | | `--bin ` | 内核路径,默认 `/usr/local/bin/mihomo` | | `--conf-dir ` | 配置目录,默认 `/etc/mihomo` | | `--service ` | systemd 服务名,默认 `mihomo` | | `-v` | 打印脚本自身版本 | -## GitHub 不通的机器 - -内核、版本号、Web 面板都从 GitHub 下载。国内机器直连失败时,用 `--mirror` 指定一个反代前缀,脚本会把它拼在所有 GitHub 地址前面: - -```bash -sudo ./mihomo-up.sh install --mirror https://ghfast.top/ -``` - -带 `--mirror` 跑配置向导时,生成的配置里会额外写上走同一个镜像的 `geox-url`,免得内核首次启动下载 GeoIP 数据库时卡住。 - ## 默认值与环境变量 命令行选项优先级最高,其次是环境变量,最后是下面这些默认值: @@ -165,17 +211,14 @@ sudo ./mihomo-up.sh install --mirror https://ghfast.top/ | `mihomo`(systemd 服务名) | `MIHOMO_SERVICE` | | `/var/backups/mihomo` | `MIHOMO_BACKUP_DIR` | | 空(不用镜像) | `MIHOMO_MIRROR` | +| `/usr/local/bin/mihomo-up`(快捷命令) | `MIHOMO_UP_LAUNCHER` | +| `/var/lib/mihomo-up`(脚本缓存) | `MIHOMO_UP_CACHE` | +| 仓库 raw 地址(快捷命令拉取源) | `MIHOMO_UP_URL` | 内核备份最多保留 3 份(脚本内常量 `KEEP=3`)。配置备份不自动清理,攒多了自己删 `config.yaml.bak-*`。 -## 环境要求 - -- Linux + bash 4 以上 -- 依赖命令:`curl`、`gzip`、`install`、`awk`、`sed`(一般发行版自带) -- 安装、升级、改配置、卸载都需要 **root**,非 root 进菜单会先提醒一句 -- 没有 systemd 的机器(容器等)也能用:脚本会跳过服务相关步骤,只管内核和配置,你自己 `mihomo -d /etc/mihomo` 拉起来 -- 交互输入读 `/dev/tty`,所以 `bash <(curl ...)` 这种 stdin 被占用的跑法照样能按键 - ## 参考 -systemd 服务写法来自 [MetaCubeX 官方文档](https://wiki.metacubex.one/startup/service/)。一键安装和卸载的思路参考了 [ForLoveIcu/mihomo-for-linux-install](https://github.com/ForLoveIcu/mihomo-for-linux-install) 和 [nelvko/clash-for-linux-install](https://github.com/nelvko/clash-for-linux-install)。 +systemd 服务写法来自 [MetaCubeX 官方文档](https://wiki.metacubex.one/startup/service/)。 + +一键安装和卸载的思路参考了 [ForLoveIcu/mihomo-for-linux-install](https://github.com/ForLoveIcu/mihomo-for-linux-install) 和 [nelvko/clash-for-linux-install](https://github.com/nelvko/clash-for-linux-install)。 diff --git a/mihomo-up.sh b/mihomo-up.sh index b77b5fd..93ba789 100644 --- a/mihomo-up.sh +++ b/mihomo-up.sh @@ -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 amd64 | arm64 | armv7 --variant amd64 微架构: v1 | v2 | v3 | compatible --mirror GitHub 下载前缀,如 https://ghfast.top/ + --script-url 快捷命令每次去拉的脚本地址(fork 之后指到你自己那份) --bin 内核路径,默认 /usr/local/bin/mihomo --conf-dir 配置目录,默认 /etc/mihomo --service 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