#!/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="7897" # 默认main SOCK_PORT="7898" API_BASE="http://${API_HOST}:${API_PORT}" API_SECRET="" # 这是密钥,用于验证;对应yaml中设置的secret字段,保持一致即可;如果不设置,默认空字符串,表示不验证 # 简单彩色(不喜欢颜色就把这些变量都设为空字符串) 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() { local path="$1" if [[ -n "${API_SECRET}" ]]; then curl -fsS --noproxy '*' -H "Authorization: Bearer ${API_SECRET}" "${API_BASE}${path}" else curl -fsS --noproxy '*' "${API_BASE}${path}" fi } api_put() { local path="$1" local data="$2" if [[ -n "${API_SECRET}" ]]; then curl -fsS --noproxy '*' -X PUT \ -H "Authorization: Bearer ${API_SECRET}" \ -H 'Content-Type: application/json' \ -d "$data" "${API_BASE}${path}" else curl -fsS --noproxy '*' -X PUT \ -H 'Content-Type: application/json' \ -d "$data" "${API_BASE}${path}" fi } sync_global_group() { local pjson pjson="$(api_get "/proxies" 2>/dev/null)" || return 1 jq -e '.proxies["GLOBAL"]?.all? | index("Proxy") != null' >/dev/null <<<"$pjson" || return 0 local now now="$(jq -r '.proxies["GLOBAL"].now // empty' <<<"$pjson")" if [[ "$now" != "Proxy" ]]; then api_put "/proxies/GLOBAL" '{"name":"Proxy"}' >/dev/null info "Synced global selector [GLOBAL] -> [Proxy]" 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 "" "" 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]" } 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() { command -v getcap >/dev/null 2>&1 || return 1 getcap "$MIHOMO_BIN" 2>/dev/null | grep -q 'cap_net_admin' } disable_dns_fallback_geoip() { local cfg="$1" [[ -f "$cfg" ]] || return 1 yq -y -i ' .dns.enable = false | .dns.fallback = [] | .dns."fallback-filter" = null ' "$cfg" } #===============测试小工具,上====================================== #==============================配置文件读取,下====================================== 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 "" [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" </dev/null)" || { echo "❌ API 不通: ${API_BASE}"; return 1; } pjson="$(api_get "/proxies" 2>/dev/null)" || { echo "❌ 读取 /proxies 失败"; return 1; } local allow_lan bind_addr mode mixed_port ec allow_lan="$(jq -r '."allow-lan" // false' <<<"$cfg")" bind_addr="$(jq -r '."bind-address" // "-"' <<<"$cfg")" mode="$(jq -r '.mode // "-"' <<<"$cfg")" mixed_port="$(jq -r '."mixed-port" // .port // "-"' <<<"$cfg")" ec="$(jq -r '."external-controller" // "-"' <<<"$cfg")" local group now node_count group="$(get_main_group 2>/dev/null || true)" [[ -n "$group" ]] || { echo "❌ 找不到主组(/proxies 里没有可用 group)"; return 1; } 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 "-")" local lan_icon="🔒" [[ "$allow_lan" == "true" ]] && lan_icon="📡" echo "🧠 Mihomo Status" echo "🌐 API: ${API_BASE} (EC=${ec})" echo "🚪 Port: mixed-port=${mixed_port}" echo "${lan_icon} LAN: allow-lan=${allow_lan} bind=${bind_addr}" echo "🧭 Mode: ${mode}" echo "🧩 Group: ${group}" echo "📦 Nodes: ${node_count}" echo "✅ Now: ${now}" } port_open_local() { local port="$1" ss -lnt 2>/dev/null | awk '{print $4}' | grep -qE "(:|\\.)${port}\$" } show_runtime_routing_status() { local cfg pjson cfg="$(api_get "/configs" 2>/dev/null)" || { echo "Routing: API unavailable" return 0 } pjson="$(api_get "/proxies" 2>/dev/null)" || { echo "Routing: /proxies unavailable" return 0 } local mode global_now final_now proxy_now mode="$(jq -r '.mode // "-"' <<<"$cfg")" global_now="$(jq -r '.proxies["GLOBAL"].now // empty' <<<"$pjson")" final_now="$(jq -r '.proxies["Final"].now // empty' <<<"$pjson")" proxy_now="$(jq -r '.proxies["Proxy"].now // empty' <<<"$pjson")" echo "Mode: ${mode}" [[ -n "$global_now" ]] && echo "GLOBAL -> ${global_now}" [[ -n "$final_now" ]] && echo "Final -> ${final_now}" [[ -n "$proxy_now" ]] && echo "Proxy -> ${proxy_now}" } extract_proxy_port() { local value="${1:-}" [[ -n "$value" ]] || return 1 sed -E 's#^[[:alpha:]][[:alnum:]+.-]*://[^:/]+:([0-9]+)/*$#\1#' <<<"$value" } proxy_env_status_line() { local name="$1" local value="$2" if [[ -z "$value" ]]; then echo "${name}: OFF" return 0 fi local port port="$(extract_proxy_port "$value" 2>/dev/null || true)" if [[ -n "$port" ]] && port_open_local "$port"; then echo "${name}: ON (${value}, port ${port} reachable)" elif [[ -n "$port" ]]; then echo "${name}: STALE (${value}, port ${port} not listening)" else echo "${name}: ON (${value})" fi } start_end(){ # start/end name/none 文件 local start0Rend="${1:-}" local name_file=${2:-} local cfg=$MIHOMO_ROOT/$name_file [[ -n "$start0Rend" ]] || die "usage: $0 start|end" if [[ "$start0Rend" == "start" ]]; then start_end "end" # 先停止当前 [[ -d "$cfg" ]] || die "profile not found: $cfg" [[ -f "$cfg/config.yaml" ]] || die "missing config: $cfg/config.yaml" local provider_name 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 disable_dns_fallback_geoip "$cfg/config.yaml" fi # 进入的前提配置没有问题,可以直接运行 info "start mihomo with config=$cfg/config.yaml data=$cfg" # 前台跑不方便,这里后台启动并记录 pid test_port "$API_PORT" "$MIXED_PORT" "$SOCK_PORT" "$MIHOMO_BIN" -d "$cfg" -f "$cfg/config.yaml" >"$cfg/mihomo.log" 2>&1 & # 每次启动情况记录 echo $! >"$PID_FILE" # 保存到定点位置 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" 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 " [[ -n "$src" ]] || die "usage: $0 add " 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}' } help() { cat <}" 3) 常见规则(按你当前情况选一个文件写入): - bash + 交互式非登录 shell(多数终端 tab):~/.bashrc - bash + 登录 shell(ssh 登录 / 有些终端配置):~/.bash_profile 或 ~/.profile - zsh(mac 常见):~/.zshrc - 如果你不确定: a) bash:同时写 ~/.bashrc 和 ~/.bash_profile(bash_profile 里最好加一行:[[ -f ~/.bashrc ]] && source ~/.bashrc) b) zsh:写 ~/.zshrc 验证是否生效(新开终端或 source 后): echo \$http_proxy env | grep -iE 'http_proxy|https_proxy|all_proxy|no_proxy' ------------------------------------------------------------------------------- Usage: mh [command] [args...] Commands: add 新建一个 profile 目录:${MIHOMO_ROOT}// - 生成 config.yaml - 把订阅作为 proxy-provider - 在线订阅(type=http) - 本地文件(type=file,会自动删 url/interval) select [name] 切换/启动某个 profile - 不带 name:列出目录,输入序号选择 - 带 name:直接启动 ${MIHOMO_ROOT}// start 直接启动指定 profile(等价于:select ) 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 "" -l | --list | list 列出主组(优先 Proxy)的所有节点,并标记当前节点 模糊搜索节点名称并测速排序后让你选择切换 例如: mh 美国 mh 香港 mh JP mh SG No-args (默认行为): 直接显示: - TUN 状态 - 当前环境是否存在 HTTP_PROXY/HTTPS_PROXY - 当前节点延迟测速(通过 mihomo API) ------------------------------------------------------------------------------- Examples: # 添加订阅(在线) mh add tnt "https://example.com/sub.yaml" # 添加订阅(本地文件) mh add local "/path/to/sub.yaml" # 启动/切换 profile mh select mh select tnt # 停止 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 美国 # TUN 开关 sudo mh tun on sudo mh tun off mh tun toggle Notes: - API 不通时,很多功能会失败:确保 mihomo 正在运行且 external-controller 为 ${API_HOST}:${API_PORT} - 如果刚启动就查询失败,wait_api 会稍等 mihomo API 就绪(避免 race) - `mh` 是子进程,不能直接修改你当前 shell 的代理环境;需要 `eval "\$(mh env)"` 或写入 rc 文件 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" sync_global_group || warn "failed to sync GLOBAL -> Proxy" speedtest_now 1000 } #====================tun控制,下========================= current_profile() { [[ -f "${MIHOMO_ROOT}/current.profile" ]] || return 1 cat "${MIHOMO_ROOT}/current.profile" } current_profile_cfg() { local prof prof="$(current_profile)" || return 1 printf '%s/%s/config.yaml\n' "$MIHOMO_ROOT" "$prof" } reset_basic_rules() { local cfg="$1" [[ -f "$cfg" ]] || return 1 local provider_name provider_name="$(yq -r '."proxy-providers" | keys | .[0] // empty' "$cfg" 2>/dev/null)" [[ -n "$provider_name" ]] || { warn "cannot detect proxy-provider name from $cfg"; return 1; } yq -y -i " .mode = \"rule\" | .[\"proxy-groups\"] = [ {\"name\":\"Proxy\",\"type\":\"select\",\"use\":[\"${provider_name}\"]}, {\"name\":\"Final\",\"type\":\"select\",\"proxies\":[\"Proxy\",\"DIRECT\"]} ] | .rules = [ \"DOMAIN-SUFFIX,localhost,DIRECT\", \"DOMAIN-SUFFIX,local,DIRECT\", \"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\", \"IP-CIDR,100.64.0.0/10,DIRECT,no-resolve\", \"IP-CIDR,224.0.0.0/4,DIRECT,no-resolve\", \"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() { local next_mode="${1:-}" case "$next_mode" in rule|global|direct) ;; *) die "usage: $0 mode rule|global|direct" ;; esac local prof cfg prof="$(current_profile)" || die "no current profile" cfg="${MIHOMO_ROOT}/${prof}/config.yaml" [[ -f "$cfg" ]] || die "missing config: $cfg" if [[ "$next_mode" == "rule" ]] && ! yq -e '.rules | length > 0' "$cfg" >/dev/null 2>&1; then info "No rules found in profile=${prof}, applying basic rules first..." reset_basic_rules "$cfg" || die "failed to initialize basic rules" fi yq -y -i ".mode = \"${next_mode}\"" "$cfg" info "Mode set to ${next_mode} (profile=${prof}), restarting..." start_end end start_end start "$prof" wait_api 5 || warn "mihomo API not ready" if [[ "$next_mode" == "global" ]]; then sync_global_group || warn "failed to sync GLOBAL -> Proxy" fi print_mihomo_status || true } rules_cmd() { local action="${1:-show}" local cfg prof prof="$(current_profile)" || die "no current profile" cfg="${MIHOMO_ROOT}/${prof}/config.yaml" [[ -f "$cfg" ]] || die "missing config: $cfg" case "$action" in show) echo "Rules (profile=${prof}):" yq -r '.rules[]?' "$cfg" ;; reset) reset_basic_rules "$cfg" || die "failed to reset basic rules" info "Basic rules reset (profile=${prof}), restarting..." start_end end start_end start "$prof" wait_api 5 || warn "mihomo API not ready" print_mihomo_status || true ;; *) die "usage: $0 rules show|reset" ;; esac } tun() { # 用法:sudo mh tun 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 tun on|off|toggle"; return 1 ;; esac if [[ "$next" == "true" && "${EUID:-$(id -u)}" -ne 0 ]] && ! has_mihomo_cap_net_admin; then warn "开启 TUN 通常需要 root,或提前给 mihomo 设置 cap_net_admin/cap_net_raw" warn "继续尝试启动;若失败,请执行:sudo setcap cap_net_admin,cap_net_raw+ep \"$MIHOMO_BIN\"" fi 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-redir\"] = (.tun[\"auto-redir\"] // 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\"] = \"redir-host\" | .dns.nameserver = (.dns.nameserver // [\"223.5.5.5\",\"119.29.29.29\"]) | .dns.fallback = (.dns.fallback // [\"1.1.1.1\",\"8.8.8.8\"]) | .dns[\"fallback-filter\"].geoip = false " "$cfg" if [[ "$next" != "true" ]]; then disable_dns_fallback_geoip "$cfg" fi info "TUN set to ${next} (profile=${prof}), restarting..." start_end end start_end start "$prof" wait_api 5 || warn "mihomo API not ready" sync_global_group || warn "failed to sync GLOBAL -> Proxy" print_mihomo_status 1200 || true } #=============================tun控制,上=================== 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),输出:"\t" batch_delay() { # 用法:batch_delay ... 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 "" [timeout_ms] local kw="${1:-}" local timeout_ms="${2:-1200}" [[ -n "$kw" ]] || { warn "usage: mh "; 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 } #==============================搜索测速排序,上====================== #============================状态检查,下===================== tun_status() { local prof cfg enabled dev prof="$(current_profile 2>/dev/null || true)" [[ -n "$prof" ]] || { echo "TUN: unknown (no current profile)"; return 0; } cfg="${MIHOMO_ROOT}/${prof}/config.yaml" [[ -f "$cfg" ]] || { echo "TUN: unknown (missing config)"; return 0; } enabled="$(yq -r '.tun.enable // false' "$cfg" 2>/dev/null || echo false)" dev="$(yq -r '.tun.device // "mihomo"' "$cfg" 2>/dev/null || echo mihomo)" if [[ "$enabled" != "true" ]]; then echo "TUN: OFF (profile=${prof})" return 0 fi echo "TUN: ON (profile=${prof})" echo "Expect iface: ${dev}" if ip link show "$dev" >/dev/null 2>&1; then echo "Iface: ✅ ${dev} exists" else echo "Iface: ❌ ${dev} not found (mihomo tun may not be running/privileged)" fi echo "Default route:" ip route show default 2>/dev/null | sed 's/^/ /' || true } proxy_env_status() { proxy_env_status_line "HTTP_PROXY" "${HTTP_PROXY:-}" proxy_env_status_line "HTTPS_PROXY" "${HTTPS_PROXY:-}" proxy_env_status_line "ALL_PROXY" "${ALL_PROXY:-}" } show_proxy_exports() { cat < OK/FAIL” local -a urls=( "https://www.google.com" "https://www.cloudflare.com" "https://www.github.com" "https://www.youtube.com" "https://www.baidu.com" "https://www.bing.com" ) echo "== Net Test (timeout=10s) ==" local url for url in "${urls[@]}"; do if curl -fsS -I -m 10 "$url" >/dev/null 2>&1; then printf "%s -> ✅ OK\n" "$url" else printf "%s -> ❌ FAIL\n" "$url" fi done } net_test_double() { local -a urls=( "https://www.google.com" "https://www.cloudflare.com" "https://www.github.com" "https://www.youtube.com" "https://www.baidu.com" "https://www.bing.com" ) echo "== Net Test (timeout=10s) ==" echo "Format: URL -> DIRECT | PROXY(mixed-port=${MIXED_PORT})" local url ok1 ok2 for url in "${urls[@]}"; do # 直连(完全忽略环境代理) if curl -fsS -I -m 10 --noproxy '*' "$url" >/dev/null 2>&1; then ok1="✅" else ok1="❌" fi # 强制走 mihomo mixed-port(明确指定代理) if curl -fsS -I -m 10 -x "http://127.0.0.1:${MIXED_PORT}" "$url" >/dev/null 2>&1; then ok2="✅" else ok2="❌" fi printf "%-28s -> DIRECT:%s PROXY:%s\n" "$url" "$ok1" "$ok2" done } #============================状态检查,上===================== #=========================debug,下=============================== debug_one() { shift local -a debug_code=() debug_code[1]="ss -lntp | grep -E ':(9090|9091|7897|7898|7890|7891)'|| echo \"❌ 未找到代理服务\"" debug_code[2]="ps -fp \${1:-} -o pid,ppid,cmd" debug_code[3]="net_test" debug_code[4]="net_test_double" debug_code[5]="test_env" debug_code[6]="ip -o link show" debug_code[7]="resolvectl status 2>/dev/null | sed -n '1,120p' || cat /etc/resolv.conf" local -a debug_desc=() debug_desc[1]="检查 9090/9091/7897/7898 端口监听:" debug_desc[2]="查看指定 PID 的进程信息:" debug_desc[3]="测试一下网络(仅proxy)" debug_desc[4]="测试一下网络(直连+proxy)" debug_desc[5]="测试需要的软件是否安装:" debug_desc[6]="看看是否有虚拟网卡 mihomo:" debug_desc[7]="dns debug" echo "== debug list ==" local i for i in "${!debug_code[@]}"; do printf " [%s] %s\n %s\n" "$i" "${debug_desc[$i]:-}" "${debug_code[$i]}" done local idx read -r -p "select index: " idx for i in "${!debug_code[@]}"; do if [[ "$i" == "$idx" ]]; then echo "== run [$i] ==" eval "${debug_code[$i]}" return $? fi done echo "invalid index: $idx" return 1 } doctor() { local prof cfg prof="$(current_profile 2>/dev/null || true)" cfg="" [[ -n "$prof" ]] && cfg="${MIHOMO_ROOT}/${prof}/config.yaml" echo "== mh doctor ==" echo "root: ${MIHOMO_ROOT}" echo "bin: ${MIHOMO_BIN}" echo "pid: ${PID_FILE}" [[ -n "$prof" ]] && echo "profile: ${prof}" || echo "profile: " [[ -n "$cfg" ]] && echo "config: ${cfg}" echo echo "== capability ==" if command -v getcap >/dev/null 2>&1; then getcap "$MIHOMO_BIN" 2>/dev/null || true else echo "getcap not found" fi echo echo "== ports ==" ss -lnt 2>/dev/null | grep -E ":(9090|7897|7898|1053)\\b" || echo "no mh ports listening" echo echo "== runtime ==" tun_status show_runtime_routing_status proxy_env_status echo echo "== api ==" api_get "/configs" 2>/dev/null | jq -r '{mode, "mixed-port": ."mixed-port", "socks-port": ."socks-port", "external-controller": ."external-controller"}' 2>/dev/null || echo "api unavailable" echo echo "== route ==" ip route show default 2>/dev/null || true echo echo "== dns ==" resolvectl status 2>/dev/null | sed -n '1,120p' || cat /etc/resolv.conf echo echo "== log tail ==" if [[ -n "$prof" && -f "${MIHOMO_ROOT}/${prof}/mihomo.log" ]]; then tail -n 40 "${MIHOMO_ROOT}/${prof}/mihomo.log" else echo "no log file" 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" <>> 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,上=============================== main() { if [[ -z "${1:-}" ]]; then tun_status show_runtime_routing_status proxy_env_status speedtest_now 1000 || true # print_mihomo_status exit 0 fi case "${1:-}" in add) cfg_add "$@";; start) [[ -n "${2:-}" ]] || die "usage: $0 start " select_one "select" "${2}";; end) start_end "$@";; select) select_one "$@";; db) debug_one "$@";; doctor) doctor ;; env) show_proxy_exports ;; mode) shift; mode_set "${1:-}" ;; rules) shift; rules_cmd "${1:-show}" ;; help|-h|--help) help ;; -l|--list|list) list_nodes ;; tun) shift; tun "${1:-toggle}" ;; *) pick_switch_with_delay "${1:-}" 1200 ;; esac } main "$@"