mihomo/mh
2026-02-10 02:08:04 +08:00

823 lines
25 KiB
Bash
Executable file
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
set -euo pipefail
shopt -s nullglob
MIHOMO_ROOT=/home/lht/bfile/mihomo
MIHOMO_BIN=${MIHOMO_ROOT}/mihomo # 指向二进制文件
PID_FILE=${MIHOMO_ROOT}/run.pid # 固定pid文件
API_HOST="127.0.0.1"
API_PORT="9090"
MIXED_PORT="7890" # 默认main
API_BASE="http://${API_HOST}:${API_PORT}"
API_SECRET="" # 这是密钥用于验证对应yaml中设置的secret字段保持一致即可如果不设置默认空字符串表示不验证
# 简单彩色(不喜欢颜色就把这些变量都设为空字符串)
c_reset=$'\e[0m'
c_title=$'\e[1;36m'
c_key=$'\e[1;33m'
c_ok=$'\e[1;32m'
c_warn=$'\e[1;31m'
c_dim=$'\e[2m'
urlenc() {
python3 - <<'PY' "$1"
import sys, urllib.parse
print(urllib.parse.quote(sys.argv[1], safe=''))
PY
}
#===============测试小工具,下======================================
log() {
local level=$1
shift
echo "[$(date +%H:%M:%S)] [$level] $*"
}
debug() { [[ "${DEBUG:-0}" == "1" ]] && log "DEBUG" "$@"; }
info() { log "INFO" "$@"; }
warn() { log "WARN" "$@"; }
error() { log "ERROR" "$@"; }
die() { error "$@"; exit 1; }
ok() { log "OK" "$@"; }
need() {
command -v "$1" >/dev/null 2>&1 || die "missing dependency: $1"
}
test_env() { # 测试一下环境
need ss
need yq
need jq
need curl
need find
[[ -x "$MIHOMO_BIN" ]] || die "mihomo not executable: $MIHOMO_BIN"
echo "环境正确 ; env is ok"
}
test_port() {
local port
for port in "$@"; do
if ss -lnt 2>/dev/null | awk '{print $4}' | grep -qE "(:|\\.)${port}\$"; then
die "port already in use: $port"
fi
done
}
api_get() {
if [[ -n "${API_SECRET}" ]]; then
curl -fsS -H "Authorization: Bearer ${API_SECRET}" "${API_BASE}$1"
else
curl -fsS "${API_BASE}$1"
fi
}
api_put() {
local path="$1"
local data="$2"
if [[ -n "${API_SECRET}" ]]; then
curl -fsS -X PUT -H "Authorization: Bearer ${API_SECRET}" -H 'Content-Type: application/json' \
-d "$data" "${API_BASE}${path}"
else
curl -fsS -X PUT -H 'Content-Type: application/json' -d "$data" "${API_BASE}${path}"
fi
}
api_delay() {
local name="$1"
local timeout="${2:-2000}"
local url="http://www.gstatic.com/generate_204"
local enc_name enc_url
enc_name="$(urlenc "$name")"
enc_url="$(urlenc "$url")"
local out
out="$(api_get "/proxies/${enc_name}/delay?timeout=${timeout}&url=${enc_url}" 2>/dev/null || true)"
jq -r 'if (.delay|type)=="number" then (.delay|tostring) else "-" end' <<<"$out" 2>/dev/null || echo "-"
}
switch_group_node() {
# 用法switch_group_node "<group>" "<node>"
local group="$1"
local node="$2"
[[ -n "$group" && -n "$node" ]] || return 1
local enc_group
enc_group="$(urlenc "$group")"
api_put "/proxies/${enc_group}" "$(jq -nc --arg name "$node" '{name:$name}')" >/dev/null
info "Switched group [$group] -> [$node]"
}
#===============测试小工具,上======================================
#==============================配置文件读取,下======================================
get_main_group() {
local pjson
pjson="$(api_get "/proxies" 2>/dev/null)" || return 1
# 1) prefer group named "Proxy"
if jq -e '.proxies["Proxy"]? != null and .proxies["Proxy"].all? != null' >/dev/null <<<"$pjson"; then
echo "Proxy"
return 0
fi
# 2) fallback to your heuristic
jq -r '
.proxies
| to_entries
| map(select(.value.all? != null))
| (map(select(.value.type?=="Selector" or .value.type?=="URLTest" or .value.type?=="Fallback" or .value.type?=="LoadBalance")) + .)
| .[0].key // empty
' <<<"$pjson"
}
get_now_node() {
# 输出“当前节点名”;失败输出空并返回非 0
local group pjson
group="$(get_main_group)" || return 1
[[ -n "$group" ]] || return 1
pjson="$(api_get "/proxies" 2>/dev/null)" || return 1
jq -r --arg g "$group" '.proxies[$g].now // empty' <<<"$pjson"
}
#==============================配置文件读取,上======================================
#============================测速、测速结果处理,下=================================
speedtest_node_delay() {
# 用 mihomo 内置 delay API 测某个节点,输出 ms 或 "-"
# 用法speedtest_node_delay "<node_name>" [timeout_ms]
local node="${1:-}"
local timeout_ms="${2:-1000}"
[[ -n "$node" ]] || { echo "-"; return 1; }
# DIRECT/REJECT 没必要测
if [[ "$node" == "DIRECT" || "$node" == "REJECT" ]]; then
echo "-"
return 0
fi
api_delay "$node" "$timeout_ms"
}
speedtest_now() {
# 用法speedtest_now [timeout_ms]
local timeout_ms="${1:-1000}"
local node delay
node="$(get_now_node || true)"
if [[ -z "$node" ]]; then
echo "⚠️ 无法获取当前节点mihomo API 不通或没有可用 group"
return 1
fi
delay="$(speedtest_node_delay "$node" "$timeout_ms")"
# 简单打个标
local badge="🟡"
if [[ "$delay" =~ ^[0-9]+$ ]]; then
if (( delay <= 150 )); then badge="🟢"
elif (( delay <= 400 )); then badge="🟡"
else badge="🔴"
fi
else
badge="⚪"
fi
echo "👉 当前节点:${node}"
echo "${badge} 延迟:${delay} ms (timeout=${timeout_ms})"
}
wait_api() {
local t="${1:-5}" # seconds
local i=0
while (( i < t*10 )); do
if api_get "/proxies" >/dev/null 2>&1; then
return 0
fi
sleep 0.1
i=$((i+1))
done
return 1
}
#============================测速、测速结果处理,上=================================
#============================mihomo配置下==================
write_cfg() { # name url file
local name="$1"
local url="$2"
cat > "${MIHOMO_ROOT}/$name/config.yaml" <<YAML # 直接使用这个即可
mixed-port: ${MIXED_PORT}
socks-port: ${MIXED_PORT}
allow-lan: false
bind-address: ${API_HOST}
mode: global
log-level: info
external-controller: ${API_HOST}:${API_PORT}
secret: ""
proxy-providers:
${name}:
type: http
url: "${url}"
interval: 3600
path: "${MIHOMO_ROOT}/$name/providers/$name.yaml"
proxy-groups:
- name: Proxy
type: select
use:
- ${name}
YAML
}
#=====================mihomo配置上====================================
print_mihomo_status() {
local timeout_ms="${1:-2000}"
# 1) configs运行态端口/LAN/mode
local cfg
if ! cfg="$(api_get "/configs")"; then
echo "${c_warn}✗ Cannot reach mihomo API at ${API_BASE}${c_reset}"
return 1
fi
local allow_lan bind_addr mode log_level mixed_port ec
allow_lan="$(jq -r '."allow-lan" // false' <<<"$cfg")"
bind_addr="$(jq -r '."bind-address" // "-"' <<<"$cfg")"
mode="$(jq -r '.mode // "-"' <<<"$cfg")"
log_level="$(jq -r '."log-level" // "-"' <<<"$cfg")"
mixed_port="$(jq -r '."mixed-port" // .port // "-"' <<<"$cfg")"
ec="$(jq -r '."external-controller" // "-"' <<<"$cfg")"
local lan_badge
if [[ "$allow_lan" == "true" ]]; then
lan_badge="${c_ok}ON${c_reset}"
else
lan_badge="${c_warn}OFF${c_reset}"
fi
# 2) proxies找主组 + now + all节点列表
local pjson
pjson="$(api_get "/proxies")" || { echo "${c_warn}✗ Cannot read /proxies${c_reset}"; return 1; }
# 自动选“主组”:优先 Selector/URLTest/Fallback/LoadBalance其次任意有 all 的
local group
group="$(jq -r '
.proxies
| to_entries
| map(select(.value.all? != null))
| (map(select(.value.type?=="Selector" or .value.type?=="URLTest" or .value.type?=="Fallback" or .value.type?=="LoadBalance")) + .)
| .[0].key // empty
' <<<"$pjson")"
if [[ -z "$group" ]]; then
echo "${c_warn}✗ No proxy groups found in /proxies (unexpected)${c_reset}"
return 1
fi
local now node_count
now="$(jq -r --arg g "$group" '.proxies[$g].now // "-"' <<<"$pjson")"
node_count="$(jq -r --arg g "$group" '.proxies[$g].all | length' <<<"$pjson" 2>/dev/null || echo "-")"
# 3) 当前节点 delayDIRECT/REJECT 不测)
local now_delay="-"
if [[ "$now" != "-" && "$now" != "DIRECT" && "$now" != "REJECT" ]]; then
now_delay="$(api_delay "$now" "$timeout_ms")"
fi
# 4) 好看输出
echo
echo "${c_title}╔══════════════════════════════ Mihomo Live Status ══════════════════════════════╗${c_reset}"
printf "${c_key}║%-16s${c_reset} %-58s ║\n" "API" "${API_BASE} ${c_dim}(external-controller=${ec})${c_reset}"
printf "${c_key}║%-16s${c_reset} %-58s ║\n" "Ports" "mixed-port=${mixed_port}"
printf "${c_key}║%-16s${c_reset} %-58s ║\n" "LAN" "allow-lan=${lan_badge} bind=${bind_addr}"
printf "${c_key}║%-16s${c_reset} %-58s ║\n" "Mode / Log" "mode=${mode} log-level=${log_level}"
printf "${c_key}║%-16s${c_reset} %-58s ║\n" "Main Group" "${group}"
printf "${c_key}║%-16s${c_reset} %-58s ║\n" "Nodes" "count=${node_count}"
printf "${c_key}║%-16s${c_reset} %-58s ║\n" "Now Using" "${now} ${c_dim}(delay=${now_delay}ms)${c_reset}"
echo "${c_title}╚═══════════════════════════════════════════════════════════════════════════════╝${c_reset}"
echo
}
on_off() { # 启动/停止代理,
if [[ "$1" == "on" ]]; then
export HTTP_PROXY="http://${API_HOST}:${MIXED_PORT}"
export HTTPS_PROXY="http://${API_HOST}:${MIXED_PORT}"
info "Proxy ON: HTTP_PROXY and HTTPS_PROXY set to ${API_HOST}:${MIXED_PORT}"
speedtest_now 1000
elif [[ "$1" == "off" ]]; then
unset HTTP_PROXY HTTPS_PROXY
info "Proxy OFF: HTTP_PROXY and HTTPS_PROXY unset"
else
die "usage: $0 on|off"
fi
}
start_end(){ # start/end name/none 文件
local start0Rend=$1
local name_file=${2:-}
local cfg=$MIHOMO_ROOT/$name_file
if [[ "$start0Rend" == "start" ]]; then
start_end "end" # 先停止当前
if [[ -d $cfg ]];then
# 进入的前提配置没有问题,可以直接运行
info "start mihomo with config=$cfg/config.yaml data=$cfg"
# 前台跑不方便,这里后台启动并记录 pid
test_port "$API_PORT" "$MIXED_PORT"
"$MIHOMO_BIN" -d "$cfg" >"$cfg/mihomo.log" 2>&1 & # 每次启动情况记录
# on_off on
echo $! >"$PID_FILE" # 保存到定点位置
fi
elif [[ "$start0Rend" == "end" ]]; then
if [[ -f "$PID_FILE" ]]; then # 存在就终止
local pid
pid="$(cat "$PID_FILE" || true)"
if [[ -n "${pid}" ]] && kill "$pid" 2>/dev/null; then
info "stop process pid=$pid"
kill "$pid" 2>/dev/null || true
# 等一下让它退出
for _ in {1..20}; do
kill -0 "$pid" 2>/dev/null || break
sleep 0.1
done
if kill -0 "$pid" 2>/dev/null; then
warn "force kill pid=$pid"
kill -9 "$pid" 2>/dev/null || true
fi
fi
rm -f "$PID_FILE"
# on_off off
fi
else
die "usage: $0 start|end"
fi
}
cfg_add() { # add name src
if [[ "$1" == "add" ]]; then
shift # 去掉 "add" 参数
fi
local name="$1"
local src="$2"
local cfg_root="${MIHOMO_ROOT}/${name}"
[[ -n "$name" ]] || die "usage: $0 add <name> <url|file>"
[[ -n "$src" ]] || die "usage: $0 add <name> <url|file>"
if [[ -d "$cfg_root" ]]; then
die "profile exists: $cfg_root ,请换一个名称"
fi
mkdir -p "$cfg_root"
mkdir -p "$cfg_root/providers" #
write_cfg "$name" "$src"
if [[ -f "$src" ]]; then #file: 直接复制,不用下载了
info "copy file -> ${cfg_root}/providers/$name.yaml"
cp -f "$src" "$cfg_root/providers/$name.yaml"
yq -y -i "
.[\"proxy-providers\"][\"$name\"].type = \"file\" |
del(.[\"proxy-providers\"][\"$name\"].url) |
del(.[\"proxy-providers\"][\"$name\"].interval)
" "$cfg_root/config.yaml"
info "saved: ${cfg_root}/providers/$name.yaml" # 有了文件,等会挂载
fi
# 启动这个配置
select_one "select" "$name" # 启动这个新的节点
print_mihomo_status 1000
}
get_group_nodes_json() {
local pjson group nodes
pjson="$(api_get "/proxies")" || return 1
group="$(get_main_group)" || return 1
[[ -n "$group" ]] || return 1
nodes="$(jq -c --arg g "$group" '.proxies[$g].all' <<<"$pjson")" || return 1
jq -nc --arg group "$group" --argjson nodes "$nodes" '{group:$group, nodes:$nodes}'
}
pick_and_switch_by_keyword() {
# 用法mh <keyword>
local kw="${1:-}"
[[ -n "$kw" ]] || { warn "usage: mh <keyword>"; return 1; }
wait_api 5 || { warn "mihomo API not ready"; return 1; }
local gj group
gj="$(get_group_nodes_json)" || { warn "cannot read proxies/groups"; return 1; }
group="$(jq -r '.group' <<<"$gj")"
# 纯子串匹配:中文直接 contains英文做 lower-case contains
local candidates
candidates="$(jq -r --arg kw "$kw" '
def lc: ascii_downcase;
.nodes
| map(select(. != "DIRECT" and . != "REJECT"))
| map(select(
(contains($kw)) # 中文/原样子串
or ((lc) | contains($kw | lc)) # 英文忽略大小写
))
| .[]
' <<<"$gj")"
if [[ -z "$candidates" ]]; then
warn "No nodes matched keyword: $kw"
info "Tip: try: 美国 / 香港 / 日本 / 狮城 / 韩国 / 台湾 / US / HK / JP / SG"
return 1
fi
mapfile -t arr <<<"$candidates"
echo "🔎 Matched nodes in group [$group] for keyword: [$kw]"
local i
for i in "${!arr[@]}"; do
printf " %d) %s\n" "$((i+1))" "${arr[$i]}"
done
local pick
while true; do
read -r -p "👉 choose (1-${#arr[@]}, 0 cancel): " pick
[[ "$pick" =~ ^[0-9]+$ ]] || { echo "❌ 请输入数字"; continue; }
(( pick==0 )) && { echo "cancelled"; return 0; }
(( pick>=1 && pick<=${#arr[@]} )) || { echo "❌ 超出范围"; continue; }
break
done
switch_group_node "$group" "${arr[$((pick-1))]}"
speedtest_now 1200 || true
}
help() {
cat <<EOF
Usage:
$0 add <name> <url|file> # 新建一个 profile 目录:${MIHOMO_ROOT}/<name>/
# 生成 config.yaml并把订阅作为 proxy-provider
# <url>在线订阅type=http
# <file>本地文件type=file会自动删 url/interval
$0 select [name] # 切换/启动某个 profile
# 不带 name列出目录输入序号选择
# 带 name直接启动 ${MIHOMO_ROOT}/<name>/
$0 start <name> # 直接启动指定 profile等价于 select name
$0 end # 停止当前 mihomo用 ${PID_FILE}
$0 on # 导出 HTTP_PROXY/HTTPS_PROXY 指向 127.0.0.1:${MIXED_PORT}
$0 off # 取消代理环境变量
$0 # 显示端口监听情况 + 当前节点测速(需要 API 就绪)
$0 tnu [on|off|toggle] Toggle TUN (writes tun: into current profile and restarts)
$0 -l | --list # 列出主组(优先 Proxy)的所有节点,并标记当前节点
$0 美国 # 模糊搜索节点并切换
Tips:
- 如果刚启动就查节点失败,加了 wait_api 会更稳(避免 race
- 当前节点显示 DIRECT 代表还没切到 provider 节点,或 provider 没拉到节点
EOF
}
select_one(){ # select name
shift
local name
if [[ -z "${1:-}" ]];then
local -a files
mapfile -t files < <(find "$MIHOMO_ROOT" -mindepth 1 -maxdepth 1 -type d -exec test -f '{}/config.yaml' ';' -print | sort)
if (( ${#files[@]} == 0 )); then
echo "⚠️ ${MIHOMO_ROOT} 下没有找到任何 目录"
return 1
fi
echo "📄 可用配置(输入序号选择,例如 1"
local i
for i in "${!files[@]}"; do
printf " %d) %s\n" "$((i+1))" "$(basename "${files[$i]}")"
done
local pick
while true; do
read -r -p "👉 请输入序号 (1-${#files[@]}): " pick
[[ "$pick" =~ ^[0-9]+$ ]] || { echo "❌ 请输入数字"; continue; }
(( pick>=1 && pick<=${#files[@]} )) || { echo "❌ 超出范围"; continue; }
name="$(basename "${files[$((pick-1))]}")"
break
done
else
local file_cfg="$MIHOMO_ROOT/$1"
if [[ -d $file_cfg ]];then
name="$1"
else
echo "❌ 不存在: $file_cfg"# 输入name有无
return 1
fi
fi
echo "$name" > "${MIHOMO_ROOT}/current.profile"
start_end "start" "$name"
wait_api 5 || warn "mihomo API not ready yet"
speedtest_now 1000
}
#====================tnu控制下=========================
current_profile() {
[[ -f "${MIHOMO_ROOT}/current.profile" ]] || return 1
cat "${MIHOMO_ROOT}/current.profile"
}
tnu() {
# 用法mh tnu on|off|toggle
local action="${1:-toggle}"
local prof
prof="$(current_profile)" || { warn "no current profile"; return 1; }
local cfg="${MIHOMO_ROOT}/${prof}/config.yaml"
[[ -f "$cfg" ]] || { warn "missing config: $cfg"; return 1; }
local cur
cur="$(yq -r '.tun.enable // false' "$cfg" 2>/dev/null || echo false)"
local next
case "$action" in
on) next=true ;;
off) next=false ;;
toggle) [[ "$cur" == "true" ]] && next=false || next=true ;;
*) warn "usage: mh tnu on|off|toggle"; return 1 ;;
esac
yq -y -i "
.tun.enable = ${next} |
.tun.stack = (.tun.stack // \"system\") |
.tun.device = (.tun.device // \"mihomo\") |
.tun[\"auto-route\"] = (.tun[\"auto-route\"] // true) |
.tun[\"auto-detect-interface\"] = (.tun[\"auto-detect-interface\"] // true) |
.tun[\"dns-hijack\"] = (.tun[\"dns-hijack\"] // []) |
( .tun[\"dns-hijack\"] |= ( . + [\"any:53\"] | unique ) ) |
.dns.enable = ${next} |
.dns.listen = (.dns.listen // \"127.0.0.1:1053\") |
.dns[\"enhanced-mode\"] = (.dns[\"enhanced-mode\"] // \"fake-ip\") |
.dns.nameserver = (.dns.nameserver // [\"223.5.5.5\",\"119.29.29.29\"]) |
.dns.fallback = (.dns.fallback // [\"1.1.1.1\",\"8.8.8.8\"])
" "$cfg"
info "TUN set to ${next} (profile=${prof}), restarting..."
start_end end
start_end start "$prof"
wait_api 5 || warn "mihomo API not ready"
print_mihomo_status 1200 || true
}
#=============================tnu控制上===================
list_nodes() {
# 用法mh -l
wait_api 5 || { warn "mihomo API not ready"; return 1; }
local pjson group now
pjson="$(api_get "/proxies")" || { warn "cannot read /proxies"; return 1; }
group="$(get_main_group)" || { warn "cannot detect main group"; return 1; }
now="$(jq -r --arg g "$group" '.proxies[$g].now // ""' <<<"$pjson")"
echo "📌 Group: ${group}"
[[ -n "$now" ]] && echo "⭐ Now: ${now}"
echo
# 列出 all 节点,排除 DIRECT/REJECT 的话可以加过滤;这里默认全列出并标记当前
jq -r --arg g "$group" --arg now "$now" '
.proxies[$g].all
| to_entries[]
| "\(.key + 1)) " + (if .value == $now then "⭐ " else " " end) + .value
' <<<"$pjson"
}
#===============================搜索测速排序,下======================
# 批量测延迟(默认并发 6输出"<delay>\t<node>"
batch_delay() {
# 用法batch_delay <timeout_ms> <max_jobs> <node1> <node2> ...
local timeout_ms="${1:-1200}"; shift || true
local max_jobs="${1:-6}"; shift || true
local -a nodes=( "$@" )
# 兼容:没有节点直接返回
(( ${#nodes[@]} == 0 )) && return 0
# 并发测
local tmp
tmp="$(mktemp)"
rm -f "$tmp"; : >"$tmp"
# 简单并发控制:后台跑,超过 max_jobs 就 wait 一个
local running=0
for n in "${nodes[@]}"; do
(
local d
d="$(api_delay "$n" "$timeout_ms")"
# 统一成数值:"-" 变成 999999便于排序
if [[ "$d" =~ ^[0-9]+$ ]]; then
printf "%s\t%s\n" "$d" "$n"
else
printf "999999\t%s\n" "$n"
fi
) >>"$tmp" &
running=$((running+1))
if (( running >= max_jobs )); then
wait -n 2>/dev/null || wait
running=$((running-1))
fi
done
wait
# 排序输出
sort -n "$tmp"
rm -f "$tmp"
}
# 关键词匹配 + 测速排序 + 选择切换
pick_switch_with_delay() {
# 用法pick_switch_with_delay "<keyword>" [timeout_ms]
local kw="${1:-}"
local timeout_ms="${2:-1200}"
[[ -n "$kw" ]] || { warn "usage: mh <keyword>"; return 1; }
wait_api 5 || { warn "mihomo API not ready"; return 1; }
local gj group
gj="$(get_group_nodes_json)" || { warn "cannot read proxies/groups"; return 1; }
group="$(jq -r '.group' <<<"$gj")"
# 匹配候选:中文用 contains英文用 lower-case contains
local candidates
candidates="$(jq -r --arg kw "$kw" '
def lc: ascii_downcase;
.nodes
| map(select(. != "DIRECT" and . != "REJECT"))
| map(select(
(contains($kw)) or ((lc) | contains($kw | lc))
))
| .[]
' <<<"$gj")"
if [[ -z "$candidates" ]]; then
warn "No nodes matched keyword: $kw"
info "Tip: try: 美国 / 香港 / 日本 / 狮城 / 韩国 / 台湾 / US / HK / JP / SG"
return 1
fi
mapfile -t arr <<<"$candidates"
info "Testing delay for ${#arr[@]} nodes (timeout=${timeout_ms}ms)..."
# 并发 6你可以改大/改小
local lines
lines="$(batch_delay "$timeout_ms" 6 "${arr[@]}")"
# 组装排序后的节点数组(按 delay asc
local -a sorted_nodes=()
local -a sorted_delay=()
while IFS=$'\t' read -r d n; do
sorted_nodes+=( "$n" )
sorted_delay+=( "$d" )
done <<<"$lines"
echo "🔎 Matched nodes in group [$group] for keyword: [$kw] (sorted by delay)"
local i showd
for i in "${!sorted_nodes[@]}"; do
showd="${sorted_delay[$i]}"
[[ "$showd" == "999999" ]] && showd="-" # 显示时还原
printf " %d) %s\t%s ms\n" "$((i+1))" "${sorted_nodes[$i]}" "$showd"
done
local pick
while true; do
read -r -p "👉 choose (1-${#sorted_nodes[@]}, 0 cancel): " pick
[[ "$pick" =~ ^[0-9]+$ ]] || { echo "❌ 请输入数字"; continue; }
(( pick==0 )) && { echo "cancelled"; return 0; }
(( pick>=1 && pick<=${#sorted_nodes[@]} )) || { echo "❌ 超出范围"; continue; }
break
done
switch_group_node "$group" "${sorted_nodes[$((pick-1))]}"
speedtest_now 1500 || true
}
#==============================搜索测速排序,上======================
#============================状态检查,下=====================
detect_tun_ifaces() {
# 输出检测到的 TUN/TAP 接口名;没有则空
local out=""
# 1) ip link匹配 tun/utun/tap忽略大小写
out="$(ip -o link show 2>/dev/null | awk -F': ' '{print $2}' \
| grep -iE '(^tun[0-9]*$|^utun[0-9]*$|^tap[0-9]*$|tun|utun|tap|mihomo|clash|meta)' || true)"
if [[ -n "$out" ]]; then
echo "$out" | sed '/^$/d' | sort -u
return 0
fi
# 2) ip tuntap更权威有些发行版默认不装 iproute2 的 tuntap 子命令,失败就忽略)
if ip tuntap show >/dev/null 2>&1; then
ip tuntap show | awk '{print $1}' | sed '/^$/d' | sort -u
return 0
fi
# 3) /sys/class/net兜底
ls /sys/class/net 2>/dev/null | grep -iE '(^tun[0-9]*$|^utun[0-9]*$|^tap[0-9]*$|tun|utun|tap|mihomo|clash|meta)' || true
}
tun_status() {
local prof cfg enabled
prof="$(current_profile 2>/dev/null || true)"
if [[ -z "$prof" ]]; then
echo "TUN: unknown (no current profile)"
return 0
fi
cfg="${MIHOMO_ROOT}/${prof}/config.yaml"
if [[ ! -f "$cfg" ]]; then
echo "TUN: unknown (missing config)"
return 0
fi
enabled="$(yq -r '.tun.enable // false' "$cfg" 2>/dev/null || echo false)"
# 配置状态
if [[ "$enabled" != "true" ]]; then
echo "TUN: OFF (profile=${prof})"
return 0
fi
# 运行态:接口
local ifs
ifs="$(detect_tun_ifaces || true)"
if [[ -z "$ifs" ]]; then
echo "TUN: ON in config, but NO tun/tap iface found (profile=${prof})"
else
echo "TUN: ON (profile=${prof})"
echo "TUN ifaces:"
echo "$ifs" | sed 's/^/ - /'
fi
# 运行态:默认路由(是否被接管)
echo "Default route:"
ip route show default 2>/dev/null | sed 's/^/ /' || true
}
proxy_env_status() {
if [[ -n "${HTTP_PROXY:-}" || -n "${HTTPS_PROXY:-}" ]]; then
echo "Proxy ENV: ON (HTTP_PROXY=${HTTP_PROXY:-<unset>} HTTPS_PROXY=${HTTPS_PROXY:-<unset>})"
else
echo "Proxy ENV: OFF"
fi
}
net_test() {
# 验证:环境变量代理是否生效 + 通过代理访问 google
# 1) 走系统环境(如果你 mh on 了)
echo "== Test via ENV proxy (if enabled) =="
curl -I -m 10 https://www.cloudflare.com | head -n 1 || echo "❌ ENV proxy path failed"
# 2) 强制走 mihomo mixed-port不依赖你是否 export
echo
echo "== Test via explicit proxy http://127.0.0.1:${MIXED_PORT} =="
curl -I -m 10 -x "http://127.0.0.1:${MIXED_PORT}" https://www.cloudflare.com | head -n 1 || echo "❌ explicit proxy failed"
echo
echo "== Current node delay (mihomo API) =="
speedtest_now 1500 || true
}
#============================状态检查,上=====================
main() {
if [[ -z "${1:-}" ]]; then
ss -lntp | grep -E ':(9090|7890)'|| echo "❌ 未找到代理服务"
test_env
tun_status
proxy_env_status
speedtest_now 1000
# print_mihomo_status
exit 0
fi
case "${1:-}" in
add)
cfg_add "$@";;
on|off)
on_off "$@";;
start|end)
start_end "$@";;
select)
select_one "$@";;
help|-h|--help) help ;;
-l|--list|list) list_nodes ;;
tnu) shift; tnu "${1:-toggle}" ;;
test) net_test ;;
# *) pick_and_switch_by_keyword "${1:-}" ;;
*) pick_switch_with_delay "${1:-}" 1200 ;;
esac
}
main "$@"