This commit is contained in:
Lihatoo 2026-02-10 01:43:20 +08:00
commit 1907ba9685
13 changed files with 5801 additions and 0 deletions

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
# private folders
tnt/
history/

192
README.md Normal file
View file

@ -0,0 +1,192 @@
# mh (mihomo wrapper)
基于 mihomo 的纯终端控制梯子工具,适合简单全局控制与分享。
A terminal-only proxy wrapper based on mihomo, designed for simple global control and sharing.
A small Bash wrapper around `mihomo` to manage profiles, start/stop the service, switch nodes, and toggle TUN.
重要:必须使用 `sudo` 启动。
Important: You must run this script with `sudo`.
Repo / 仓库:
```
https://github.com/Lihatoo/mh
```
## What it does / 功能
- Creates per-profile directories under `MIHOMO_ROOT/<name>/` with generated `config.yaml`
- Starts/stops `mihomo` and records PID in `run.pid`
- Switches nodes by keyword and optionally ranks by delay
- Toggles TUN in the current profile and restarts
- Exports/unsets HTTP proxy environment variables
- 在 `MIHOMO_ROOT/<name>/` 下创建 profile 目录并生成 `config.yaml`
- 启动/停止 `mihomo`PID 记录在 `run.pid`
- 通过关键词切换节点,并可按延迟排序
- 切换 TUN 并重启
- 导出/取消 HTTP 代理环境变量
## Requirements / 依赖
- Linux with `bash`
- `mihomo` binary in `MIHOMO_ROOT/mihomo`
- Tools: `ss`, `yq`, `jq`, `curl`, `find`, `ip`
- Linux + `bash`
- `mihomo` 二进制放在 `MIHOMO_ROOT/mihomo`
- 工具:`ss`, `yq`, `jq`, `curl`, `find`, `ip`
## Quick start / 快速开始
1. Set the root path / 设置根路径
Open `mh` and update this line to the absolute path of this repo:
打开 `mh`,把下面这行改成仓库的绝对路径:
```
MIHOMO_ROOT=/path/to/this/repo
```
2. Make the script executable / 授权可执行
```
chmod +x mh
```
3. Put `mh` in `/usr/local/bin` for direct use / 放到 `/usr/local/bin` 以便直接使用
```
sudo cp mh /usr/local/bin/mh
```
4. Start with `sudo` (required) / 必须用 `sudo` 启动
```
sudo mh
```
## Usage / 用法
Create a profile from a subscription URL (quote the URL is recommended):
从订阅 URL 创建 profile建议加双引号
```
sudo mh add <name> "<url>"
```
Create a profile from a local file:
从本地文件创建 profile
```
sudo mh add <name> /path/to/subscription.yaml
```
Start/select a profile:
启动/切换 profile
```
sudo mh select
sudo mh select <name>
```
Stop:
停止:
```
sudo mh end
```
Toggle proxy env vars (current shell only):
导出/取消代理环境变量(只影响当前 shell
```
sudo -E mh on
sudo -E mh off
```
List nodes in the main group:
列出主组节点:
```
sudo mh -l
```
Switch node by keyword and rank by delay:
按关键词切换并按延迟排序:
```
sudo mh 美国
sudo mh HK
```
Toggle TUN (requires sudo):
切换 TUN需要 sudo
```
sudo mh tnu on
sudo mh tnu off
sudo mh tnu toggle
```
Run connectivity test:
网络连通性测试:
```
sudo mh test
```
Show help:
查看帮助:
```
sudo mh --help
```
## Notes / 说明
- Default ports: `mixed-port=7890`, API `external-controller=127.0.0.1:9090`.
- If you use `sudo` and want `HTTP_PROXY/HTTPS_PROXY` to persist, use `sudo -E`.
- When you run `mh` with no arguments, it checks ports, environment, TUN status, and current node delay.
- 默认端口:`mixed-port=7890`API `external-controller=127.0.0.1:9090`
- 若想保留 `HTTP_PROXY/HTTPS_PROXY`,请用 `sudo -E`
- 不带参数运行 `mh` 会检查端口、环境、TUN 状态和当前节点延迟。
## Template / 模板
Use `profile_template/` as a sanitized example of the basic profile format.
可参考 `profile_template/` 作为去隐私后的基础配置格式示例。
## Privacy / 隐私
Private folders are ignored and should not be uploaded: `history/`, `tnt/`.
私有目录已忽略且不应上传:`history/`, `tnt/`.
## Files created / 生成文件
- `current.profile`: name of the active profile / 当前 profile 名称
- `run.pid`: PID of the running `mihomo` process / 运行中的 PID
- `<profile>/mihomo.log`: log output for each profile / 日志文件
## Example session / 示例
```
# add profile
sudo mh add test "https://example.com/sub.yaml"
# start/select
sudo mh select test
# switch node by keyword
sudo mh 日本
```
## Links / 相关链接
Official mihomo repo:
官方 mihomo 仓库:
```
https://github.com/MetaCubeX/mihomo
```

1
current.profile Normal file
View file

@ -0,0 +1 @@
tnt

821
mh Executable file
View file

@ -0,0 +1,821 @@
#!/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}
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
start_end "start" "$name"
echo "$name" > "${MIHOMO_ROOT}/current.profile"
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 "$@"

BIN
mihomo Executable file

Binary file not shown.

View file

@ -0,0 +1,42 @@
mixed-port: 7890
allow-lan: false
bind-address: 127.0.0.1
mode: global
log-level: info
external-controller: 127.0.0.1:9090
secret: ""
proxy-providers:
example:
type: http
url: "https://example.com/your-subscription.yaml"
interval: 3600
path: ./providers/example.yaml
proxy-groups:
- name: Proxy
type: select
use:
- example
# Optional TUN + DNS (enabled via `mh tnu on`):
# tun:
# enable: true
# stack: system
# device: mihomo
# auto-route: true
# auto-detect-interface: true
# dns-hijack:
# - any:53
#
# dns:
# enable: true
# listen: 127.0.0.1:1053
# enhanced-mode: fake-ip
# nameserver:
# - 223.5.5.5
# - 119.29.29.29
# fallback:
# - 1.1.1.1
# - 8.8.8.8

View file

@ -0,0 +1,15 @@
# Example provider file (no real nodes)
proxies:
- name: "Example-Node"
type: ss
server: example.com
port: 443
cipher: aes-128-gcm
password: "your-password"
udp: true
proxy-groups:
- name: Proxy
type: select
proxies:
- "Example-Node"

1
run.pid Normal file
View file

@ -0,0 +1 @@
2240968

BIN
wechat/cache.db Normal file

Binary file not shown.

36
wechat/config.yaml Normal file
View file

@ -0,0 +1,36 @@
mixed-port: 7890
allow-lan: false
bind-address: 127.0.0.1
mode: global
log-level: info
external-controller: 127.0.0.1:9090
secret: ''
proxy-providers:
wechat:
type: http
url: https://1da5d8a2743d.ai-163cdn.com/sub?target=clash&new_name=true&emoji=true&clash.doh=true&filename=YToo_SS&udp=false&url=https%3A%2F%2Fapi.ytoo.xyz%2Fosubscribe.php%3Fsid%3D130335%26token%3DG9MmudNIRh1PtQPnY6Ix%26sip002%3D1
interval: 3600
path: /home/lht/bfile/mihomo/wechat/providers/wechat.yaml
proxy-groups:
- name: Proxy
type: select
use:
- wechat
tun:
enable: true
stack: system
device: mihomo
auto-route: true
auto-detect-interface: true
dns-hijack:
- any:53
dns:
enable: true
listen: 127.0.0.1:1053
enhanced-mode: fake-ip
nameserver:
- 223.5.5.5
- 119.29.29.29
fallback:
- 1.1.1.1
- 8.8.8.8

BIN
wechat/geoip.metadb Normal file

Binary file not shown.

35
wechat/mihomo.log Normal file
View file

@ -0,0 +1,35 @@
time="2026-02-10T01:42:15.229830089+08:00" level=info msg="Start initial configuration in progress"
time="2026-02-10T01:42:15.230019440+08:00" level=info msg="Geodata Loader mode: memconservative"
time="2026-02-10T01:42:15.230035320+08:00" level=info msg="Geosite Matcher implementation: succinct"
time="2026-02-10T01:42:15.230339230+08:00" level=info msg="Initial configuration complete, total time: 0ms"
time="2026-02-10T01:42:15.230915900+08:00" level=info msg="RESTful API listening at: 127.0.0.1:9090"
time="2026-02-10T01:42:15.250411285+08:00" level=info msg="Sniffer is closed"
time="2026-02-10T01:42:15.250645392+08:00" level=info msg="Mixed(http+socks) proxy listening at: 127.0.0.1:7890"
time="2026-02-10T01:42:15.250769338+08:00" level=info msg="DNS server(UDP) listening at: 127.0.0.1:1053"
time="2026-02-10T01:42:15.250925646+08:00" level=info msg="DNS server(TCP) listening at: 127.0.0.1:1053"
time="2026-02-10T01:42:15.251735371+08:00" level=warning msg="[TUN] default interface changed by monitor, => enp193s0"
time="2026-02-10T01:42:15.257619548+08:00" level=info msg="[TUN] Tun adapter listening at: mihomo([198.18.0.1/30],[fdfe:dcba:9876::1/126]), mtu: 9000, auto route: true, auto redir: false, ip stack: System"
time="2026-02-10T01:42:15.257742793+08:00" level=info msg="Start initial compatible provider default"
time="2026-02-10T01:42:15.257910603+08:00" level=info msg="Start initial provider wechat"
time="2026-02-10T01:42:16.264584363+08:00" level=info msg="Load MMDB file: /home/lht/bfile/mihomo/wechat/geoip.metadb"
time="2026-02-10T01:42:16.358315129+08:00" level=info msg="[TCP] 127.0.0.1:56680 --> detectportal.firefox.com:80 using GLOBAL"
time="2026-02-10T01:42:16.359565885+08:00" level=info msg="[TCP] 127.0.0.1:56696 --> detectportal.firefox.com:80 using GLOBAL"
time="2026-02-10T01:42:16.359902397+08:00" level=info msg="[TCP] 127.0.0.1:56692 --> detectportal.firefox.com:80 using GLOBAL"
time="2026-02-10T01:42:16.397214163+08:00" level=info msg="[TCP] 127.0.0.1:56708 --> detectportal.firefox.com:80 using GLOBAL"
time="2026-02-10T01:42:16.397626741+08:00" level=info msg="[TCP] 127.0.0.1:56712 --> detectportal.firefox.com:80 using GLOBAL"
time="2026-02-10T01:42:21.678364627+08:00" level=info msg="[UDP] 198.18.0.1:34035 --> 209.250.254.15:21116 using GLOBAL"
time="2026-02-10T01:42:23.206387009+08:00" level=warning msg="[TCP] dial GLOBAL 127.0.0.1:56728 --> chatgpt.com:443 error: connect failed: dial tcp 168.143.162.42:443: i/o timeout\nconnect failed: dial tcp [2a03:2880:f11a:83:face:b00c:0:25de]:443: connect: network is unreachable"
time="2026-02-10T01:42:23.490021436+08:00" level=warning msg="[TCP] dial GLOBAL 127.0.0.1:56732 --> chatgpt.com:443 error: connect failed: dial tcp 168.143.162.42:443: i/o timeout\nconnect failed: dial tcp [2a03:2880:f11a:83:face:b00c:0:25de]:443: connect: network is unreachable"
time="2026-02-10T01:42:24.638725022+08:00" level=warning msg="[TCP] dial GLOBAL 127.0.0.1:56744 --> chat.openai.com:443 error: connect failed: dial tcp 185.60.218.50:443: i/o timeout\nconnect failed: dial tcp [2a03:2880:f117:83:face:b00c:0:25de]:443: connect: network is unreachable"
time="2026-02-10T01:42:28.428410400+08:00" level=warning msg="[TCP] dial GLOBAL 127.0.0.1:48850 --> chatgpt.com:443 error: connect failed: dial tcp 168.143.162.42:443: i/o timeout\nconnect failed: dial tcp [2a03:2880:f11a:83:face:b00c:0:25de]:443: connect: network is unreachable"
time="2026-02-10T01:42:28.759206645+08:00" level=warning msg="[TCP] dial GLOBAL 127.0.0.1:48854 --> chatgpt.com:443 error: connect failed: dial tcp 168.143.162.42:443: i/o timeout\nconnect failed: dial tcp [2a03:2880:f11a:83:face:b00c:0:25de]:443: connect: network is unreachable"
time="2026-02-10T01:42:34.286527122+08:00" level=warning msg="[TCP] dial GLOBAL 127.0.0.1:48858 --> chatgpt.com:443 error: connect failed: dial tcp 168.143.162.42:443: i/o timeout\nconnect failed: dial tcp [2a03:2880:f11a:83:face:b00c:0:25de]:443: connect: network is unreachable"
time="2026-02-10T01:42:34.352971658+08:00" level=warning msg="[TCP] dial GLOBAL 127.0.0.1:48864 --> chatgpt.com:443 error: connect failed: dial tcp 168.143.162.42:443: i/o timeout\nconnect failed: dial tcp [2a03:2880:f11a:83:face:b00c:0:25de]:443: connect: network is unreachable"
time="2026-02-10T01:42:36.506324666+08:00" level=warning msg="[TCP] dial GLOBAL 127.0.0.1:48872 --> www.google.com:443 error: connect failed: dial tcp 199.59.148.96:443: i/o timeout\nconnect failed: dial tcp [2001::1]:443: connect: network is unreachable"
time="2026-02-10T01:42:36.587725061+08:00" level=info msg="[TCP] 127.0.0.1:49034 --> www.trae.cn:443 using GLOBAL"
time="2026-02-10T01:42:37.043857799+08:00" level=info msg="[ICMP] icmp 198.18.0.1:0 --> 198.18.0.23:0 using fake ping echo"
time="2026-02-10T01:42:37.240907912+08:00" level=info msg="[TCP] 127.0.0.1:49042 --> mcs.zijieapi.com:443 using GLOBAL"
time="2026-02-10T01:42:39.483942199+08:00" level=warning msg="[TCP] dial GLOBAL 127.0.0.1:49008 --> chatgpt.com:443 error: connect failed: dial tcp 168.143.162.42:443: i/o timeout\nconnect failed: dial tcp [2a03:2880:f11a:83:face:b00c:0:25de]:443: connect: network is unreachable"
time="2026-02-10T01:42:40.415803105+08:00" level=warning msg="[TCP] dial GLOBAL 127.0.0.1:49024 --> chatgpt.com:443 error: connect failed: dial tcp 168.143.162.42:443: i/o timeout\nconnect failed: dial tcp [2a03:2880:f11a:83:face:b00c:0:25de]:443: connect: network is unreachable"
time="2026-02-10T01:42:41.508098732+08:00" level=warning msg="[TCP] dial GLOBAL 127.0.0.1:49036 --> www.google.com:443 error: connect failed: dial tcp 199.59.148.96:443: i/o timeout\nconnect failed: dial tcp [2001::1]:443: connect: network is unreachable"
time="2026-02-10T01:42:42.839932611+08:00" level=warning msg="Mihomo shutting down"

4655
wechat/providers/wechat.yaml Normal file

File diff suppressed because it is too large Load diff