Update mh wrapper features and docs

This commit is contained in:
Lihatoo 2026-07-01 12:18:49 +08:00
parent afd10467e5
commit 87060094d7
3 changed files with 516 additions and 375 deletions

165
README.md
View file

@ -1,98 +1,127 @@
# mh (mihomo wrapper) # mh
>基于 mihomo 的纯ubuntu终端控制梯子工具适合简单全局控制与 `mh` 是一个面向 Ubuntu 终端的 mihomo 包装脚本,用来管理 profile、启动/停止 mihomo、切换节点、输出 shell 代理环境变量,以及开关 TUN
mihomo: https://github.com/mihomo/mihomo mihomo: https://github.com/mihomo/mihomo
# 部署 ## 部署
将目录下的`mh`复制到`/usr/local/bin`下,并添加执行权限。 目录内需要包含:
```bash
sudo chmod +x mh # 给予执行权限
sudo setcap cap_net_admin,cap_net_raw+ep /home/lht/bfile/mihomo/mihomo # 给予网络tun权限
```
然后在任何终端都可以使用`mh`命令。
# 使用 - `mh`:控制脚本
- `mihomo`mihomo 可执行文件
## 导入 - `<profile>/config.yaml`profile 配置,通常由 `mh add` 生成
> 支持两种导入方法
```bash
mh add name path
mh add name url
mh select name # 选择一个配置
```
分别是订阅链接或者是某个已配置好的`yaml`文件
得到如下的项目结构
```json
tnt
├── cache.db
├── config.yaml # 这是mh生成的配置文件用于启动mihomo
├── geoip.metadb # 这是运行中生成的文件
├── mihomo.log # 这是日志可以用config.yaml 的log_file 指定error,warning,info,debug
└── providers
└── tnt.yaml # 这是提供节点的文件,来自你的订阅链接,tnt为你输入的名称
current.profile # 保存是否需要tun配置
run.pid # 运行时保存的进程id目前只能支持起一个mihomo
```
## 换节点
```bash ```bash
mh -l # 列出搜索可选择节点 chmod +x mh
mh name # 搜索并列表选择节点 sudo setcap cap_net_admin,cap_net_raw+ep "$(pwd)/mihomo"
``` ```
## 其他操作 如需全局使用,可以把 `mh` 放到 `PATH` 中。脚本会优先使用当前目录里的 `mihomo` 作为工作目录;如果当前目录没有 `mihomo`,才使用脚本所在目录。`/usr/local/bin` 不会被当作 profile 根目录;从其他目录运行时可通过 `MIHOMO_ROOT=/path/to/mihomo mh ...` 显式指定。
## 常用命令
```bash ```bash
mh tun on/off # 开启/关闭tun,可能需要sudo权限 mh add <name> <url|file> # 添加订阅或本地 provider
mh env # 输出当前 shell 可用的代理 export 片段 mh select [name] # 切换/启动 profile
mh mode rule/global/direct # 切换运行模式 mh start <name> # 等价于 select <name>
mh rules show/reset # 查看或重置内建基础规则 mh end # 停止 mihomo
mh -h # 帮助 mh env # 输出代理 export
mh # 快速预览信息 mh theme [name] # 查看或切换输出主题
mh db # 一些测试debug专用 mh sysproxy [on|off|status] # 开关 GNOME 系统代理
sudo -E apt install ...#如果下载没有网络,则是环境没有导入 mh update [all|provider] # 更新当前 profile 的 HTTP provider
mh mode rule|global|direct # 切换模式
mh rules show|reset # 查看/重置基础规则
mh tun [on|off|toggle] # 开关 TUN
mh list [keyword] # 列出节点并可输入序号切换
mh <keyword> # 搜索节点并按延迟排序选择
mh doctor # 诊断信息
``` ```
如果要让当前 shell 立刻走代理,不能只执行 `mh`,因为它是子进程,没法反向修改你的父 shell 环境。要这样用: 让当前 shell 立刻走代理:
```bash ```bash
eval "$(mh env)" eval "$(mh env)"
``` ```
默认输出的是 `127.0.0.1:7897`,和常见 GUI 代理端口保持一致。也可以把 `mh env` 的输出写进 `~/.bashrc` / `~/.zshrc`
## 基础规则 默认 mixed-port 是 `127.0.0.1:7890`socks-port 是 `127.0.0.1:7891`API 是 `127.0.0.1:9090`
现在新建的 profile 默认走一套很简单的规则: 输出主题可以切换:
- 局域网、本地回环直连
- 其他流量走 `Final`
- `Final` 默认选择 `Proxy`
可以直接调整:
```bash ```bash
mh mode rule # 规则模式 mh theme # 查看当前主题
mh mode global # 全局代理 mh theme list # 查看可选主题
mh mode direct # 全局直连 mh theme basic # 基础版,最稳
mh theme ganyu # 原神甘雨风格
mh rules show # 查看当前规则 mh theme kaomoji # 颜文字版,适合 emoji 显示不完整的终端
mh rules reset # 重置成内建基础规则 mh theme minimal # 极简版
``` ```
# 错误 GNOME/Ubuntu 桌面可以让 `mh` 帮你开系统代理:
> 主要错误都可以看看`mihomo.log`文件,从其中分析
## 网络dns解析失败
> 节点正常搜索,测速正常,可是无法使用(浏览器,网站)
> 查看日志,前面下载正常,就是网络解析不了
`mh db`选择7 ,看看有几个dns ```bash
mh sysproxy status
mh sysproxy on # HTTP/HTTPS -> 127.0.0.1:7890, SOCKS -> 127.0.0.1:7891
mh sysproxy off
```
问我这里显示有两个dns也就是说错误的ip解析可能来自另一个干扰的`tailscale0`(我这里是这个) ## Profile
`mh add name url` 会生成类似结构:
```text
name/
config.yaml
mihomo.log
providers/
name.yaml
current.profile
run.pid
```
新 profile 默认使用 `rule` 模式:本地/内网直连,其余流量走 `Final``Final` 默认选择 `Proxy`
`providers/<name>.yaml` 只保存订阅下发的节点;`config.yaml` 保存 mh 管理的端口、模式、代理组和规则。mihomo 启动时会读取 `config.yaml`,再按 `proxy-providers` 引用 provider 节点,两者是组合生效。更新订阅只更新 provider 节点文件,不会覆盖本地规则。
域名排除写在当前 profile 的 `bypass.list`,格式尽量贴近 FLClash
```text
*zhihu.com
*zhimg.com
*jd.com
100ime-iat-api.xfyun.cn
*360buyimg.com
localhost
*.local
127.*
10.*
```
执行 `mh rules reset` 后,`mh` 会把它编译进 `config.yaml``rules`,例如:
```yaml
rules:
- DOMAIN-SUFFIX,zhihu.com,DIRECT
- DOMAIN,100ime-iat-api.xfyun.cn,DIRECT
- IP-CIDR,127.0.0.0/8,DIRECT,no-resolve
- MATCH,Final
```
默认订阅更新间隔是 1 天:`interval: 86400`。默认日志级别是 `error`,减少 `mihomo.log` 输出。
## 排错
优先看:
```bash
mh doctor
tail -n 80 <profile>/mihomo.log
```
如果 TUN 启动失败,确认当前用户有 root 权限,或已经给 `mihomo` 设置 `cap_net_admin,cap_net_raw`
终端可用但浏览器不可用时,通常是浏览器没有使用 shell 里的 `http_proxy` 环境变量。mihomo core 本身只负责监听本地端口或 TUNFLClash 能“一键开”,是因为 GUI 额外帮你设置了系统代理或 TUN。处理方式三选一
- 开启 TUN`mh tun on`
- 开启 GNOME 系统代理:`mh sysproxy on`
- 在系统或浏览器代理里手动设置 HTTP/HTTPS 代理为 `127.0.0.1:7890`SOCKS 为 `127.0.0.1:7891`

718
mh
View file

@ -2,14 +2,30 @@
set -euo pipefail set -euo pipefail
shopt -s nullglob shopt -s nullglob
MIHOMO_ROOT=/home/lht/bfile/mihomo SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
resolve_mihomo_root() {
if [[ -n "${MIHOMO_ROOT:-}" ]]; then
printf "%s\n" "$MIHOMO_ROOT"
elif [[ -x "${PWD}/mihomo" ]]; then
printf "%s\n" "$PWD"
elif [[ "$SCRIPT_DIR" != "/usr/local/bin" && -x "${SCRIPT_DIR}/mihomo" ]]; then
printf "%s\n" "$SCRIPT_DIR"
else
echo "mh: cannot locate mihomo root; cd into the project dir or set MIHOMO_ROOT=/path/to/mihomo" >&2
exit 1
fi
}
MIHOMO_ROOT="$(resolve_mihomo_root)"
MIHOMO_BIN=${MIHOMO_ROOT}/mihomo # 指向二进制文件 MIHOMO_BIN=${MIHOMO_ROOT}/mihomo # 指向二进制文件
PID_FILE=${MIHOMO_ROOT}/run.pid # 固定pid文件 PID_FILE=${MIHOMO_ROOT}/run.pid # 固定pid文件
THEME_FILE=${MIHOMO_ROOT}/theme.current
API_HOST="127.0.0.1" API_HOST="127.0.0.1"
API_PORT="9090" API_PORT="9090"
MIXED_PORT="7897" # 默认main MIXED_PORT="7890" # 默认main
SOCK_PORT="7898" SOCK_PORT="7891"
API_BASE="http://${API_HOST}:${API_PORT}" API_BASE="http://${API_HOST}:${API_PORT}"
API_SECRET="" # 这是密钥用于验证对应yaml中设置的secret字段保持一致即可如果不设置默认空字符串表示不验证 API_SECRET="" # 这是密钥用于验证对应yaml中设置的secret字段保持一致即可如果不设置默认空字符串表示不验证
@ -24,10 +40,47 @@ PY
#===============测试小工具,下====================================== #===============测试小工具,下======================================
theme_current() {
local theme="basic"
if [[ -f "$THEME_FILE" ]]; then
theme="$(tr -d '[:space:]' < "$THEME_FILE")"
fi
case "$theme" in
basic|ganyu|kaomoji|minimal) printf '%s\n' "$theme" ;;
*) printf 'basic\n' ;;
esac
}
theme_prefix() {
local level="$1"
case "$(theme_current):$level" in
basic:DEBUG) printf '[debug] ' ;;
basic:INFO) printf '[info] ' ;;
basic:WARN) printf '[warn] ' ;;
basic:ERROR) printf '[error] ' ;;
basic:OK) printf '[ok] ' ;;
ganyu:DEBUG) printf '(甘雨观察中) ' ;;
ganyu:INFO) printf '❄️ 甘雨小秘书:' ;;
ganyu:WARN) printf '🍧 甘雨提醒:' ;;
ganyu:ERROR) printf '(甘雨皱眉) ' ;;
ganyu:OK) printf '🍡 甘雨点头:' ;;
kaomoji:DEBUG) printf '(눈_눈) ' ;;
kaomoji:INFO) printf '(。・ω・。) ' ;;
kaomoji:WARN) printf '(;´д`)ゞ ' ;;
kaomoji:ERROR) printf '(╯°□°)╯ ' ;;
kaomoji:OK) printf '(๑•̀ㅂ•́)و✧ ' ;;
minimal:*) printf '' ;;
*) printf '' ;;
esac
}
log() { log() {
local level=$1 local level=$1
shift shift
echo "[$(date +%H:%M:%S)] [$level] $*" echo "[$(date +%H:%M:%S)] [$level] $(theme_prefix "$level")$*"
} }
debug() { [[ "${DEBUG:-0}" == "1" ]] && log "DEBUG" "$@"; } debug() { [[ "${DEBUG:-0}" == "1" ]] && log "DEBUG" "$@"; }
info() { log "INFO" "$@"; } info() { log "INFO" "$@"; }
@ -80,6 +133,16 @@ api_put() {
fi fi
} }
api_put_empty() {
local path="$1"
if [[ -n "${API_SECRET}" ]]; then
curl -fsS --noproxy '*' -X PUT \
-H "Authorization: Bearer ${API_SECRET}" "${API_BASE}${path}"
else
curl -fsS --noproxy '*' -X PUT "${API_BASE}${path}"
fi
}
sync_global_group() { sync_global_group() {
local pjson local pjson
pjson="$(api_get "/proxies" 2>/dev/null)" || return 1 pjson="$(api_get "/proxies" 2>/dev/null)" || return 1
@ -119,19 +182,6 @@ switch_group_node() {
info "Switched group [$group] -> [$node]" info "Switched group [$group] -> [$node]"
} }
need_root() {
# 用法need_root "sudo -E $0 tun on"
if [[ "${EUID:-$(id -u)}" -ne 0 ]]; then
warn "此操作需要 sudo/root 权限"
warn "请用 sudo 运行:"
warn " $1"
warn "或给 mihomo 二进制加能力(不必整进程 root"
warn " sudo setcap cap_net_admin,cap_net_raw+ep \"$MIHOMO_BIN\""
return 1
fi
return 0
}
has_mihomo_cap_net_admin() { has_mihomo_cap_net_admin() {
command -v getcap >/dev/null 2>&1 || return 1 command -v getcap >/dev/null 2>&1 || return 1
getcap "$MIHOMO_BIN" 2>/dev/null | grep -q 'cap_net_admin' getcap "$MIHOMO_BIN" 2>/dev/null | grep -q 'cap_net_admin'
@ -249,7 +299,7 @@ socks-port: ${SOCK_PORT}
allow-lan: false allow-lan: false
bind-address: ${API_HOST} bind-address: ${API_HOST}
mode: rule mode: rule
log-level: info log-level: error
external-controller: ${API_HOST}:${API_PORT} external-controller: ${API_HOST}:${API_PORT}
secret: "" secret: ""
@ -258,7 +308,7 @@ proxy-providers:
${name}: ${name}:
type: http type: http
url: "${url}" url: "${url}"
interval: 3600 interval: 86400
path: "${MIHOMO_ROOT}/$name/providers/$name.yaml" path: "${MIHOMO_ROOT}/$name/providers/$name.yaml"
proxy-groups: proxy-groups:
@ -288,6 +338,65 @@ rules:
YAML YAML
} }
ensure_profile_config() {
local profile_dir="$1"
local cfg="${profile_dir}/config.yaml"
[[ -d "$profile_dir" ]] || die "profile not found: $profile_dir"
[[ -f "$cfg" ]] || die "missing config: $cfg"
mkdir -p "${profile_dir}/providers"
local -a providers
mapfile -t providers < <(yq -r '."proxy-providers" | keys[]?' "$cfg" 2>/dev/null)
(( ${#providers[@]} > 0 )) || die "missing proxy-providers in $cfg"
local provider ptype purl ppath
for provider in "${providers[@]}"; do
ptype="$(yq -r --arg p "$provider" '."proxy-providers"[$p].type // ""' "$cfg")"
case "$ptype" in
http)
purl="$(yq -r --arg p "$provider" '."proxy-providers"[$p].url // ""' "$cfg")"
[[ -n "$purl" ]] || die "provider [$provider] is http but url is empty"
yq -y -i --arg p "$provider" --arg path "${profile_dir}/providers/${provider}.yaml" '."proxy-providers"[$p].path = $path | ."proxy-providers"[$p].interval = 86400' "$cfg"
;;
file)
ppath="$(yq -r --arg p "$provider" '."proxy-providers"[$p].path // ""' "$cfg")"
[[ -n "$ppath" ]] || ppath="${profile_dir}/providers/${provider}.yaml"
yq -y -i --arg p "$provider" --arg path "$ppath" '."proxy-providers"[$p].path = $path' "$cfg"
[[ -f "$ppath" ]] || die "provider [$provider] file not found: $ppath"
;;
*)
die "provider [$provider] has unsupported or empty type: ${ptype:-<empty>}"
;;
esac
done
local mode
mode="$(yq -r '.mode // ""' "$cfg" 2>/dev/null)"
case "$mode" in
rule|global|direct) ;;
"") mode="rule" ;;
*) warn "invalid mode [$mode], reset to rule"; mode="rule" ;;
esac
yq -y -i "
.mode = \"${mode}\" |
.[\"mixed-port\"] = ${MIXED_PORT} |
.[\"socks-port\"] = ${SOCK_PORT} |
.[\"external-controller\"] = \"${API_HOST}:${API_PORT}\" |
.secret = (.secret // \"\") |
.[\"log-level\"] = \"error\" |
.[\"allow-lan\"] = (.[\"allow-lan\"] // false) |
.[\"bind-address\"] = \"${API_HOST}\"
" "$cfg"
if ! yq -e '([."proxy-groups"[]?.name] | index("Proxy") != null and index("Final") != null) and (.rules | type == "array" and length > 0)' "$cfg" >/dev/null 2>&1; then
warn "proxy-groups or rules missing, applying basic rule config"
reset_basic_rules "$cfg" || die "failed to initialize basic rules"
fi
yq -e '.mode as $mode | ($mode == "rule" or $mode == "global" or $mode == "direct") and (."mixed-port" | type == "number") and (."socks-port" | type == "number") and (."proxy-providers" | type == "object" and length > 0) and (."proxy-groups" | type == "array" and length > 0) and (.rules | type == "array" and length > 0)' "$cfg" >/dev/null || die "invalid mihomo config after normalization: $cfg"
}
#=====================mihomo配置上==================================== #=====================mihomo配置上====================================
print_mihomo_status() { print_mihomo_status() {
local cfg pjson local cfg pjson
@ -386,18 +495,7 @@ start_end(){ # start/end name/none 文件
start_end "end" # 先停止当前 start_end "end" # 先停止当前
[[ -d "$cfg" ]] || die "profile not found: $cfg" [[ -d "$cfg" ]] || die "profile not found: $cfg"
[[ -f "$cfg/config.yaml" ]] || die "missing config: $cfg/config.yaml" [[ -f "$cfg/config.yaml" ]] || die "missing config: $cfg/config.yaml"
local provider_name ensure_profile_config "$cfg"
provider_name="$(yq -r '."proxy-providers" | keys | .[0] // empty' "$cfg/config.yaml" 2>/dev/null || true)"
if [[ -n "$provider_name" ]]; then
yq -y -i "
.\"proxy-providers\".\"${provider_name}\".path = \"${cfg}/providers/${provider_name}.yaml\"
" "$cfg/config.yaml"
fi
yq -y -i "
.[\"mixed-port\"] = ${MIXED_PORT} |
.[\"socks-port\"] = ${SOCK_PORT} |
.[\"external-controller\"] = \"${API_HOST}:${API_PORT}\"
" "$cfg/config.yaml"
if [[ "$(yq -r '.tun.enable // false' "$cfg/config.yaml" 2>/dev/null || echo false)" != "true" ]]; then if [[ "$(yq -r '.tun.enable // false' "$cfg/config.yaml" 2>/dev/null || echo false)" != "true" ]]; then
disable_dns_fallback_geoip "$cfg/config.yaml" disable_dns_fallback_geoip "$cfg/config.yaml"
fi fi
@ -413,7 +511,6 @@ start_end(){ # start/end name/none 文件
pid="$(cat "$PID_FILE" || true)" pid="$(cat "$PID_FILE" || true)"
if [[ -n "${pid}" ]] && kill "$pid" 2>/dev/null; then if [[ -n "${pid}" ]] && kill "$pid" 2>/dev/null; then
info "stop process pid=$pid" info "stop process pid=$pid"
kill "$pid" 2>/dev/null || true
# 等一下让它退出 # 等一下让它退出
for _ in {1..20}; do for _ in {1..20}; do
kill -0 "$pid" 2>/dev/null || break kill -0 "$pid" 2>/dev/null || break
@ -443,8 +540,7 @@ cfg_add() { # add name src
if [[ -d "$cfg_root" ]]; then if [[ -d "$cfg_root" ]]; then
die "profile exists: $cfg_root ,请换一个名称" die "profile exists: $cfg_root ,请换一个名称"
fi fi
mkdir -p "$cfg_root" mkdir -p "$cfg_root/providers"
mkdir -p "$cfg_root/providers" #
write_cfg "$name" "$src" write_cfg "$name" "$src"
if [[ -f "$src" ]]; then #file: 直接复制,不用下载了 if [[ -f "$src" ]]; then #file: 直接复制,不用下载了
info "copy file -> ${cfg_root}/providers/$name.yaml" info "copy file -> ${cfg_root}/providers/$name.yaml"
@ -456,6 +552,8 @@ cfg_add() { # add name src
" "$cfg_root/config.yaml" " "$cfg_root/config.yaml"
info "saved: ${cfg_root}/providers/$name.yaml" # 有了文件,等会挂载 info "saved: ${cfg_root}/providers/$name.yaml" # 有了文件,等会挂载
fi fi
ensure_bypass_list "$cfg_root"
reset_basic_rules "$cfg_root/config.yaml"
# 启动这个配置 # 启动这个配置
select_one "select" "$name" # 启动这个新的节点 select_one "select" "$name" # 启动这个新的节点
print_mihomo_status 1000 print_mihomo_status 1000
@ -473,171 +571,146 @@ get_group_nodes_json() {
} }
provider_http_list() {
local cfg="$1"
yq -r '."proxy-providers" | to_entries[]? | select(.value.type == "http") | .key' "$cfg"
}
provider_in_list() {
local needle="$1"
shift
local item
for item in "$@"; do
[[ "$item" == "$needle" ]] && return 0
done
return 1
}
update_one_provider() {
local provider="$1"
local enc_provider
enc_provider="$(urlenc "$provider")"
api_put_empty "/providers/proxies/${enc_provider}" >/dev/null
info "updated provider: $provider"
}
update_providers_cmd() {
local target="${1:-}"
local prof cfg_dir cfg
prof="$(current_profile)" || die "no current profile"
cfg_dir="${MIHOMO_ROOT}/${prof}"
cfg="${cfg_dir}/config.yaml"
ensure_profile_config "$cfg_dir"
local -a providers selected
mapfile -t providers < <(provider_http_list "$cfg")
(( ${#providers[@]} > 0 )) || die "no http proxy-providers in profile=${prof}"
if ! wait_api 3; then
info "mihomo API not ready, starting current profile=${prof} first..."
start_end start "$prof"
wait_api 5 || die "mihomo API not ready after start"
fi
case "$target" in
"" )
echo "📦 HTTP providers (profile=${prof}):"
echo " 0) ALL"
local i pick
for i in "${!providers[@]}"; do
printf " %d) %s\n" "$((i+1))" "${providers[$i]}"
done
while true; do
read -r -p "👉 请输入序号 (0-${#providers[@]}): " pick
[[ "$pick" =~ ^[0-9]+$ ]] || { echo "❌ 请输入数字"; continue; }
(( pick>=0 && pick<=${#providers[@]} )) || { echo "❌ 超出范围"; continue; }
break
done
if (( pick == 0 )); then
selected=("${providers[@]}")
else
selected=("${providers[$((pick-1))]}")
fi
;;
all|ALL)
selected=("${providers[@]}")
;;
*)
provider_in_list "$target" "${providers[@]}" || die "provider not found or not http: $target"
selected=("$target")
;;
esac
local provider
for provider in "${selected[@]}"; do
update_one_provider "$provider"
done
}
theme_cmd() {
local action="${1:-show}"
case "$action" in
show|current)
echo "current theme: $(theme_current)"
echo "available: basic ganyu kaomoji minimal"
;;
list)
cat <<THEMES
basic - 稳定基础版ASCII 前缀,最不挑终端
ganyu - 原神甘雨风格,少量 emoji + 轻提示
kaomoji - 颜文字版,适合 emoji 显示不完整的终端
minimal - 极简版,不加额外表情
THEMES
;;
basic|ganyu|kaomoji|minimal)
printf '%s\n' "$action" > "$THEME_FILE"
info "theme set to ${action}"
;;
*)
die "usage: $0 theme [show|list|basic|ganyu|kaomoji|minimal]"
;;
esac
}
help() { help() {
cat <<EOF cat <<EOF
✅ 复制粘贴到你的 shell 启动文件(见下方“该改哪个文件”),或直接执行 mh env
# ====== MIHOMO PROXY ENV BEGIN ======
export http_proxy="http://127.0.0.1:${MIXED_PORT}"
export https_proxy="http://127.0.0.1:${MIXED_PORT}"
export all_proxy="socks5h://127.0.0.1:${MIXED_PORT}"
export HTTP_PROXY="\$http_proxy"
export HTTPS_PROXY="\$https_proxy"
export ALL_PROXY="\$all_proxy"
# 避免本地/内网/控制端口走代理(按需加减)
export NO_PROXY="127.0.0.1,localhost,::1,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16"
export no_proxy="\$NO_PROXY"
# ====== MIHOMO PROXY ENV END ======
应用方法(改完文件后执行其一):
source ~/.bashrc
source ~/.zshrc
exec \$SHELL -l
-------------------------------------------------------------------------------
该改哪个文件?(尽量准确 + 兜底)
1) 先看你当前是什么 shell
echo "\$SHELL"
ps -p \$\$ -o comm=
2) 尝试自动判断“这次会读哪个 rc 文件”(不保证 100% 准确,但很实用):
# bash看看是不是 login shell
shopt -q login_shell && echo "login shell" || echo "non-login shell"
# zsh看看 ZDOTDIR / 以及默认读 ~/.zshrc
echo "ZDOTDIR=\${ZDOTDIR:-<unset>}"
3) 常见规则(按你当前情况选一个文件写入):
- bash + 交互式非登录 shell多数终端 tab~/.bashrc
- bash + 登录 shellssh 登录 / 有些终端配置):~/.bash_profile 或 ~/.profile
- zshmac 常见):~/.zshrc
- 如果你不确定:
a) bash同时写 ~/.bashrc 和 ~/.bash_profilebash_profile 里最好加一行:[[ -f ~/.bashrc ]] && source ~/.bashrc
b) zsh写 ~/.zshrc
验证是否生效(新开终端或 source 后):
echo \$http_proxy
env | grep -iE 'http_proxy|https_proxy|all_proxy|no_proxy'
-------------------------------------------------------------------------------
Usage: Usage:
mh [command] [args...] mh [command] [args...]
Commands: Commands:
add <name> <url|file> add <name> <url|file> 新建 profile
新建一个 profile 目录:${MIHOMO_ROOT}/<name>/ select [name] 切换/启动 profile不带 name 时交互选择
- 生成 config.yaml start <name> 等价于 select <name>
- 把订阅作为 proxy-provider end 停止当前 mihomo
- <url> 在线订阅type=http env 输出代理 export常用eval "\$(mh env)"
- <file> 本地文件type=file会自动删 url/interval theme [name] 查看或切换输出主题
sysproxy [on|off|status] 开关 GNOME 系统代理
update [all|provider] 更新当前 profile 的 HTTP provider
mode rule|global|direct 切换运行模式并重启
rules show|reset 查看或重置基础规则
tun [on|off|toggle] 开关 TUN
list | -l | --list [kw] 列出节点并可输入序号切换
doctor 输出诊断信息
db 调试菜单
help | -h | --help 显示帮助
select [name] Default:
切换/启动某个 profile mh 显示 TUN/路由/代理环境/当前节点延迟
- 不带 name列出目录输入序号选择 mh <keyword> 搜索节点并按延迟排序选择切换
- 带 name直接启动 ${MIHOMO_ROOT}/<name>/
start <name>
直接启动指定 profile等价于select <name>
end
停止当前 mihomo通过 ${PID_FILE} 记录的 pid
env
输出代理环境变量 export 片段
用法示例:
eval "\$(mh env)"
mode [rule|global|direct]
切换当前 profile 的运行模式,并重启
- rule 使用内建基础规则
- global 全部走代理
- direct 全部直连
rules [show|reset]
查看或重置当前 profile 的基础规则
- show 显示当前规则
- reset 写回一套最简规则,并切回 rule 模式
doctor
输出当前 mh/mihomo 的诊断信息
- capability
- 端口监听
- TUN/模式/代理环境
- API / 路由 / DNS
- 最近日志
tun [on|off|toggle]
开关 TUN会写入当前 profile 的 config.yaml 并重启)
- 开启 TUN 通常需要 sudo/root或给 mihomo 加能力:
sudo setcap cap_net_admin,cap_net_raw+ep "<mihomo_bin>"
-l | --list | list
列出主组(优先 Proxy)的所有节点,并标记当前节点
<keyword>
模糊搜索节点名称并测速排序后让你选择切换
例如:
mh 美国
mh 香港
mh JP
mh SG
No-args (默认行为):
直接显示:
- TUN 状态
- 当前环境是否存在 HTTP_PROXY/HTTPS_PROXY
- 当前节点延迟测速(通过 mihomo API
-------------------------------------------------------------------------------
Examples: Examples:
# 添加订阅(在线) mh add demo "https://example.com/sub.yaml"
mh add tnt "https://example.com/sub.yaml"
# 添加订阅(本地文件)
mh add local "/path/to/sub.yaml" mh add local "/path/to/sub.yaml"
# 启动/切换 profile
mh select mh select
mh select tnt mh select demo
# 停止 mihomo
mh end
# 输出代理环境变量
mh env
# 切换模式
mh mode rule
mh mode global
mh mode direct
# 查看/重置基础规则
mh rules show
mh rules reset
# 一键诊断
mh doctor
# 查看节点列表
mh -l
# 按关键词筛选并测速排序后切换
mh 美国 mh 美国
eval "\$(mh env)"
# TUN 开关
sudo mh tun on
sudo mh tun off
mh tun toggle
Notes: Notes:
- API 不通时,很多功能会失败:确保 mihomo 正在运行且 external-controller 为 ${API_HOST}:${API_PORT} root=${MIHOMO_ROOT}
- 如果刚启动就查询失败wait_api 会稍等 mihomo API 就绪(避免 race api=${API_BASE}
- `mh` 是子进程,不能直接修改你当前 shell 的代理环境;需要 `eval "\$(mh env)"` 或写入 rc 文件 mixed-port=${MIXED_PORT}
mihomo=${MIHOMO_BIN}
EOF EOF
} }
@ -691,41 +764,95 @@ current_profile() {
cat "${MIHOMO_ROOT}/current.profile" cat "${MIHOMO_ROOT}/current.profile"
} }
current_profile_cfg() { ensure_bypass_list() {
local prof local profile_dir="$1"
prof="$(current_profile)" || return 1 local file="${profile_dir}/bypass.list"
printf '%s/%s/config.yaml\n' "$MIHOMO_ROOT" "$prof" [[ -f "$file" ]] && return 0
cat > "$file" <<BYPASS
*zhihu.com
*zhimg.com
*jd.com
100ime-iat-api.xfyun.cn
*360buyimg.com
localhost
*.local
127.*
10.*
172.*
192.168.*
BYPASS
}
compile_bypass_rules() {
local file="$1"
local line value prefix
[[ -f "$file" ]] || return 0
while IFS= read -r line || [[ -n "$line" ]]; do
line="${line%%#*}"
line="$(printf "%s" "$line" | sed -E "s/^[[:space:]]+|[[:space:]]+$//g")"
[[ -z "$line" ]] && continue
case "$line" in
*/*)
printf "%s\n" "IP-CIDR,${line},DIRECT,no-resolve"
;;
[0-9]*.*\*)
prefix="${line%%.*}"
case "$prefix" in
10) printf "%s\n" "IP-CIDR,10.0.0.0/8,DIRECT,no-resolve" ;;
127) printf "%s\n" "IP-CIDR,127.0.0.0/8,DIRECT,no-resolve" ;;
172) printf "%s\n" "IP-CIDR,172.16.0.0/12,DIRECT,no-resolve" ;;
192) printf "%s\n" "IP-CIDR,192.168.0.0/16,DIRECT,no-resolve" ;;
*) warn "unsupported bypass IP wildcard: $line" ;;
esac
;;
[0-9]*.[0-9]*.[0-9]*.[0-9]*)
printf "%s\n" "IP-CIDR,${line}/32,DIRECT,no-resolve"
;;
\**)
value="${line#\*}"
value="${value#.}"
[[ -n "$value" ]] && printf "%s\n" "DOMAIN-SUFFIX,${value},DIRECT"
;;
.*)
value="${line#.}"
[[ -n "$value" ]] && printf "%s\n" "DOMAIN-SUFFIX,${value},DIRECT"
;;
*)
printf "%s\n" "DOMAIN,${line},DIRECT"
;;
esac
done < "$file" | awk "!seen[\$0]++"
}
rules_json_from_lines() {
jq -R -s -c "split(\"\n\") | map(select(. != \"\"))"
} }
reset_basic_rules() { reset_basic_rules() {
local cfg="$1" local cfg="$1"
[[ -f "$cfg" ]] || return 1 [[ -f "$cfg" ]] || return 1
local provider_name local providers_json profile_dir bypass_rules rules_json
provider_name="$(yq -r '."proxy-providers" | keys | .[0] // empty' "$cfg" 2>/dev/null)" providers_json="$(yq -c '."proxy-providers" | keys' "$cfg" 2>/dev/null)"
[[ -n "$provider_name" ]] || { warn "cannot detect proxy-provider name from $cfg"; return 1; } [[ "$providers_json" != "null" && "$providers_json" != "[]" ]] || { warn "cannot detect proxy-provider names from $cfg"; return 1; }
yq -y -i " profile_dir="$(dirname "$cfg")"
.mode = \"rule\" | ensure_bypass_list "$profile_dir"
.[\"proxy-groups\"] = [ bypass_rules="$(compile_bypass_rules "${profile_dir}/bypass.list")"
{\"name\":\"Proxy\",\"type\":\"select\",\"use\":[\"${provider_name}\"]}, rules_json="$({
{\"name\":\"Final\",\"type\":\"select\",\"proxies\":[\"Proxy\",\"DIRECT\"]} printf "%s\n" "$bypass_rules"
] | printf "%s\n" \
.rules = [ "IP-CIDR,100.64.0.0/10,DIRECT,no-resolve" \
\"DOMAIN-SUFFIX,localhost,DIRECT\", "IP-CIDR,224.0.0.0/4,DIRECT,no-resolve" \
\"DOMAIN-SUFFIX,local,DIRECT\", "IP-CIDR6,::1/128,DIRECT,no-resolve" \
\"IP-CIDR,127.0.0.0/8,DIRECT,no-resolve\", "IP-CIDR6,fc00::/7,DIRECT,no-resolve" \
\"IP-CIDR,10.0.0.0/8,DIRECT,no-resolve\", "IP-CIDR6,fe80::/10,DIRECT,no-resolve" \
\"IP-CIDR,172.16.0.0/12,DIRECT,no-resolve\", "MATCH,Final"
\"IP-CIDR,192.168.0.0/16,DIRECT,no-resolve\", } | rules_json_from_lines)"
\"IP-CIDR,100.64.0.0/10,DIRECT,no-resolve\",
\"IP-CIDR,224.0.0.0/4,DIRECT,no-resolve\", yq -y -i --argjson providers "$providers_json" --argjson rules "$rules_json" ".mode = \"rule\" | .[\"proxy-groups\"] = [{\"name\":\"Proxy\",\"type\":\"select\",\"use\":\$providers},{\"name\":\"Final\",\"type\":\"select\",\"proxies\":[\"Proxy\",\"DIRECT\"]}] | .rules = \$rules" "$cfg"
\"IP-CIDR6,::1/128,DIRECT,no-resolve\",
\"IP-CIDR6,fc00::/7,DIRECT,no-resolve\",
\"IP-CIDR6,fe80::/10,DIRECT,no-resolve\",
\"MATCH,Final\"
]
" "$cfg"
} }
mode_set() { mode_set() {
@ -838,7 +965,7 @@ tun() {
#=============================tun控制上=================== #=============================tun控制上===================
list_nodes() { list_nodes() {
# 用法mh -l local kw="${1:-}"
wait_api 5 || { warn "mihomo API not ready"; return 1; } wait_api 5 || { warn "mihomo API not ready"; return 1; }
local pjson group now local pjson group now
@ -847,20 +974,44 @@ list_nodes() {
group="$(get_main_group)" || { warn "cannot detect main group"; return 1; } group="$(get_main_group)" || { warn "cannot detect main group"; return 1; }
now="$(jq -r --arg g "$group" '.proxies[$g].now // ""' <<<"$pjson")" now="$(jq -r --arg g "$group" '.proxies[$g].now // ""' <<<"$pjson")"
local -a nodes
if [[ -n "$kw" ]]; then
mapfile -t nodes < <(jq -r --arg g "$group" --arg kw "$kw" '
def lc: ascii_downcase;
.proxies[$g].all
| map(select((contains($kw)) or ((lc) | contains($kw | lc))))
| .[]
' <<<"$pjson")
else
mapfile -t nodes < <(jq -r --arg g "$group" '.proxies[$g].all[]' <<<"$pjson")
fi
(( ${#nodes[@]} > 0 )) || { warn "No nodes matched: ${kw:-<all>}"; return 1; }
echo "📌 Group: ${group}" echo "📌 Group: ${group}"
[[ -n "$now" ]] && echo "⭐ Now: ${now}" [[ -n "$now" ]] && echo "⭐ Now: ${now}"
[[ -n "$kw" ]] && echo "🔎 Filter: ${kw}"
echo echo
# 列出 all 节点,排除 DIRECT/REJECT 的话可以加过滤;这里默认全列出并标记当前 local i
jq -r --arg g "$group" --arg now "$now" ' for i in "${!nodes[@]}"; do
.proxies[$g].all printf " %d) %s%s\n" "$((i+1))" "$(if [[ "${nodes[$i]}" == "$now" ]]; then printf "⭐ "; else printf " "; fi)" "${nodes[$i]}"
| to_entries[] done
| "\(.key + 1)) " + (if .value == $now then "⭐ " else " " end) + .value
' <<<"$pjson" [[ -t 0 ]] || return 0
local pick
while true; do
read -r -p "👉 选择节点 (1-${#nodes[@]}, 0 cancel): " pick
[[ "$pick" =~ ^[0-9]+$ ]] || { echo "❌ 请输入数字"; continue; }
(( pick == 0 )) && { echo "cancelled"; return 0; }
(( pick>=1 && pick<=${#nodes[@]} )) || { echo "❌ 超出范围"; continue; }
break
done
switch_group_node "$group" "${nodes[$((pick-1))]}"
speedtest_now 1500 || true
} }
#===============================搜索测速排序,下====================== #===============================搜索测速排序,下======================
# 批量测延迟(默认并发 6输出"<delay>\t<node>" # 批量测延迟(默认并发 6输出"<delay>\t<node>"
batch_delay() { batch_delay() {
@ -1090,7 +1241,7 @@ debug_one() {
shift shift
local -a debug_code=() local -a debug_code=()
debug_code[1]="ss -lntp | grep -E ':(9090|9091|7897|7898|7890|7891)'|| echo \"❌ 未找到代理服务\"" debug_code[1]="ss -lntp | grep -E ':(9090|9091|7890|7891)'|| echo \"❌ 未找到代理服务\""
debug_code[2]="ps -fp \${1:-} -o pid,ppid,cmd" debug_code[2]="ps -fp \${1:-} -o pid,ppid,cmd"
debug_code[3]="net_test" debug_code[3]="net_test"
debug_code[4]="net_test_double" debug_code[4]="net_test_double"
@ -1099,7 +1250,7 @@ debug_one() {
debug_code[7]="resolvectl status 2>/dev/null | sed -n '1,120p' || cat /etc/resolv.conf" debug_code[7]="resolvectl status 2>/dev/null | sed -n '1,120p' || cat /etc/resolv.conf"
local -a debug_desc=() local -a debug_desc=()
debug_desc[1]="检查 9090/9091/7897/7898 端口监听:" debug_desc[1]="检查 9090/9091/7890/7891 端口监听:"
debug_desc[2]="查看指定 PID 的进程信息:" debug_desc[2]="查看指定 PID 的进程信息:"
debug_desc[3]="测试一下网络(仅proxy)" debug_desc[3]="测试一下网络(仅proxy)"
debug_desc[4]="测试一下网络(直连+proxy)" debug_desc[4]="测试一下网络(直连+proxy)"
@ -1127,6 +1278,38 @@ debug_one() {
return 1 return 1
} }
sysproxy_cmd() {
local action="${1:-status}"
command -v gsettings >/dev/null 2>&1 || die "gsettings not found; this sysproxy command is for GNOME/Ubuntu desktops"
case "$action" in
on)
gsettings set org.gnome.system.proxy mode 'manual'
gsettings set org.gnome.system.proxy.http host '127.0.0.1'
gsettings set org.gnome.system.proxy.http port "${MIXED_PORT}"
gsettings set org.gnome.system.proxy.https host '127.0.0.1'
gsettings set org.gnome.system.proxy.https port "${MIXED_PORT}"
gsettings set org.gnome.system.proxy.socks host '127.0.0.1'
gsettings set org.gnome.system.proxy.socks port "${SOCK_PORT}"
gsettings set org.gnome.system.proxy ignore-hosts "['localhost','127.0.0.0/8','::1','10.0.0.0/8','172.16.0.0/12','192.168.0.0/16']"
info "system proxy enabled: http=127.0.0.1:${MIXED_PORT}, socks=127.0.0.1:${SOCK_PORT}"
;;
off)
gsettings set org.gnome.system.proxy mode 'none'
info "system proxy disabled"
;;
status)
echo "mode=$(gsettings get org.gnome.system.proxy mode)"
echo "http=$(gsettings get org.gnome.system.proxy.http host):$(gsettings get org.gnome.system.proxy.http port)"
echo "https=$(gsettings get org.gnome.system.proxy.https host):$(gsettings get org.gnome.system.proxy.https port)"
echo "socks=$(gsettings get org.gnome.system.proxy.socks host):$(gsettings get org.gnome.system.proxy.socks port)"
;;
*)
die "usage: $0 sysproxy [on|off|status]"
;;
esac
}
doctor() { doctor() {
local prof cfg local prof cfg
prof="$(current_profile 2>/dev/null || true)" prof="$(current_profile 2>/dev/null || true)"
@ -1150,7 +1333,7 @@ doctor() {
echo echo
echo "== ports ==" echo "== ports =="
ss -lnt 2>/dev/null | grep -E ":(9090|7897|7898|1053)\\b" || echo "no mh ports listening" ss -lnt 2>/dev/null | grep -E ":(9090|7890|7891|1053)\\b" || echo "no mh ports listening"
echo echo
echo "== runtime ==" echo "== runtime =="
@ -1179,83 +1362,6 @@ doctor() {
fi fi
} }
proxy_on() {
export http_proxy="http://127.0.0.1:${MIXED_PORT}"
export https_proxy="http://127.0.0.1:${MIXED_PORT}"
export all_proxy="socks5h://127.0.0.1:${MIXED_PORT}"
export HTTP_PROXY="$http_proxy"
export HTTPS_PROXY="$https_proxy"
export ALL_PROXY="$all_proxy"
# 设置no是为了避免mihomo自己访问自己
export NO_PROXY="127.0.0.1,localhost,::1,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16"
export no_proxy="$NO_PROXY"
}
proxy_off() {
unset http_proxy https_proxy all_proxy HTTP_PROXY HTTPS_PROXY ALL_PROXY NO_PROXY no_proxy
}
# proxy_env() {
# local action="${1:-}"
# local bashrc="${HOME}/.bashrc"
# local port="${2:-${MIXED_PORT:-7890}}"
# [[ -f "$bashrc" ]] || { echo "❌ not found: $bashrc"; return 1; }
# # 备份
# local bak="${bashrc}.bak.$(date +%Y%m%d%H%M%S)"
# cp -a "$bashrc" "$bak" || { echo "❌ backup failed"; return 1; }
# # 1) 先清理:标记块 + 常见代理 export 行
# # - 标记块:# >>> MH PROXY BEGIN ... # <<< MH PROXY END
# # - 以及零散的 export http_proxy/HTTP_PROXY/NO_PROXY...(避免残留)
# perl -0777 -pe '
# s/\n?# >>> MH PROXY BEGIN\n.*?\n# <<< MH PROXY END\n?/\n/sg;
# ' -i "$bashrc"
# # 删除零散的代理行(只删我们关心的这些变量)
# sed -i \
# -e '/^[[:space:]]*export[[:space:]]\+\(http_proxy\|https_proxy\|all_proxy\|HTTP_PROXY\|HTTPS_PROXY\|ALL_PROXY\|NO_PROXY\|no_proxy\)=/d' \
# -e '/^[[:space:]]*\(http_proxy\|https_proxy\|all_proxy\|HTTP_PROXY\|HTTPS_PROXY\|ALL_PROXY\|NO_PROXY\|no_proxy\)=/d' \
# "$bashrc"
# case "$action" in
# on)
# cat >>"$bashrc" <<EOF
# # >>> MH PROXY BEGIN
# export http_proxy="http://127.0.0.1:${port}"
# export https_proxy="http://127.0.0.1:${port}"
# export all_proxy="socks5h://127.0.0.1:${port}"
# export HTTP_PROXY="\$http_proxy"
# export HTTPS_PROXY="\$https_proxy"
# export ALL_PROXY="\$all_proxy"
# export NO_PROXY="127.0.0.1,localhost,::1,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16"
# export no_proxy="\$NO_PROXY"
# # <<< MH PROXY END
# EOF
# echo "✅ proxy_env on: wrote proxy to $bashrc (port=${port})"
# echo " apply with: source ~/.bashrc"
# ;;
# off)
# echo "✅ proxy_env off: removed proxy lines from $bashrc"
# echo " apply with: source ~/.bashrc"
# ;;
# *)
# echo "usage:"
# echo " proxy_env on [port] # write proxy exports into ~/.bashrc (default port=\${MIXED_PORT:-7890})"
# echo " proxy_env off # remove proxy exports from ~/.bashrc"
# echo "backup:"
# echo " $bak"
# return 1
# ;;
# esac
# }
#=========================debug,上=============================== #=========================debug,上===============================
main() { main() {
@ -1283,12 +1389,18 @@ main() {
doctor ;; doctor ;;
env) env)
show_proxy_exports ;; show_proxy_exports ;;
theme)
shift; theme_cmd "${1:-show}" ;;
sysproxy)
shift; sysproxy_cmd "${1:-status}" ;;
update)
shift; update_providers_cmd "${1:-}" ;;
mode) mode)
shift; mode_set "${1:-}" ;; shift; mode_set "${1:-}" ;;
rules) rules)
shift; rules_cmd "${1:-show}" ;; shift; rules_cmd "${1:-show}" ;;
help|-h|--help) help ;; help|-h|--help) help ;;
-l|--list|list) list_nodes ;; -l|--list|list) shift; list_nodes "${1:-}" ;;
tun) shift; tun "${1:-toggle}" ;; tun) shift; tun "${1:-toggle}" ;;
*) pick_switch_with_delay "${1:-}" 1200 ;; *) pick_switch_with_delay "${1:-}" 1200 ;;
esac esac

View file

@ -3,11 +3,11 @@ set -euo pipefail
ROOT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" ROOT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
MH_BIN="${ROOT_DIR}/mh" MH_BIN="${ROOT_DIR}/mh"
PROFILE="${1:-tnt}" PROFILE="${1:-demo}"
TIMEOUT="${TIMEOUT:-10}" TIMEOUT="${TIMEOUT:-10}"
API_PORT="${API_PORT:-9090}" API_PORT="${API_PORT:-9090}"
MIXED_PORT="${MIXED_PORT:-7897}" MIXED_PORT="${MIXED_PORT:-7890}"
SOCKS_PORT="${SOCKS_PORT:-7898}" SOCKS_PORT="${SOCKS_PORT:-7891}"
OLD_PROXY_PORT="${OLD_PROXY_PORT:-}" OLD_PROXY_PORT="${OLD_PROXY_PORT:-}"
RUN_PID_FILE="${ROOT_DIR}/run.pid" RUN_PID_FILE="${ROOT_DIR}/run.pid"
LOG_FILE="${ROOT_DIR}/${PROFILE}/mihomo.log" LOG_FILE="${ROOT_DIR}/${PROFILE}/mihomo.log"
@ -90,7 +90,7 @@ print_env_snapshot() {
print_port_snapshot() { print_port_snapshot() {
print_section "Port Snapshot" print_section "Port Snapshot"
ss -lnt | grep -E ":(7897|7898|9090)\\b" || true ss -lnt | grep -E ":(7890|7891|9090)\\b" || true
} }
print_pid_snapshot() { print_pid_snapshot() {