1408 lines
42 KiB
Bash
Executable file
1408 lines
42 KiB
Bash
Executable file
#!/usr/bin/env bash
|
||
set -euo pipefail
|
||
shopt -s nullglob
|
||
|
||
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 # 指向二进制文件
|
||
PID_FILE=${MIHOMO_ROOT}/run.pid # 固定pid文件
|
||
THEME_FILE=${MIHOMO_ROOT}/theme.current
|
||
|
||
API_HOST="127.0.0.1"
|
||
API_PORT="9090"
|
||
MIXED_PORT="7890" # 默认main
|
||
SOCK_PORT="7891"
|
||
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
|
||
}
|
||
|
||
|
||
|
||
#===============测试小工具,下======================================
|
||
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() {
|
||
local level=$1
|
||
shift
|
||
echo "[$(date +%H:%M:%S)] [$level] $(theme_prefix "$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
|
||
}
|
||
|
||
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() {
|
||
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 "<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]"
|
||
}
|
||
|
||
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 "<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: ${SOCK_PORT}
|
||
|
||
allow-lan: false
|
||
bind-address: ${API_HOST}
|
||
mode: rule
|
||
log-level: error
|
||
|
||
external-controller: ${API_HOST}:${API_PORT}
|
||
secret: ""
|
||
|
||
proxy-providers:
|
||
${name}:
|
||
type: http
|
||
url: "${url}"
|
||
interval: 86400
|
||
path: "${MIHOMO_ROOT}/$name/providers/$name.yaml"
|
||
|
||
proxy-groups:
|
||
- name: Proxy
|
||
type: select
|
||
use:
|
||
- ${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
|
||
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配置,上====================================
|
||
print_mihomo_status() {
|
||
local cfg pjson
|
||
cfg="$(api_get "/configs" 2>/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"
|
||
ensure_profile_config "$cfg"
|
||
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"
|
||
# 等一下让它退出
|
||
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 <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/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
|
||
ensure_bypass_list "$cfg_root"
|
||
reset_basic_rules "$cfg_root/config.yaml"
|
||
# 启动这个配置
|
||
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}'
|
||
}
|
||
|
||
|
||
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() {
|
||
cat <<EOF
|
||
Usage:
|
||
mh [command] [args...]
|
||
|
||
Commands:
|
||
add <name> <url|file> 新建 profile
|
||
select [name] 切换/启动 profile,不带 name 时交互选择
|
||
start <name> 等价于 select <name>
|
||
end 停止当前 mihomo
|
||
env 输出代理 export,常用:eval "\$(mh env)"
|
||
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 显示帮助
|
||
|
||
Default:
|
||
mh 显示 TUN/路由/代理环境/当前节点延迟
|
||
mh <keyword> 搜索节点并按延迟排序选择切换
|
||
|
||
Examples:
|
||
mh add demo "https://example.com/sub.yaml"
|
||
mh add local "/path/to/sub.yaml"
|
||
mh select
|
||
mh select demo
|
||
mh 美国
|
||
eval "\$(mh env)"
|
||
|
||
Notes:
|
||
root=${MIHOMO_ROOT}
|
||
api=${API_BASE}
|
||
mixed-port=${MIXED_PORT}
|
||
mihomo=${MIHOMO_BIN}
|
||
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"
|
||
}
|
||
|
||
ensure_bypass_list() {
|
||
local profile_dir="$1"
|
||
local file="${profile_dir}/bypass.list"
|
||
[[ -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() {
|
||
local cfg="$1"
|
||
[[ -f "$cfg" ]] || return 1
|
||
|
||
local providers_json profile_dir bypass_rules rules_json
|
||
providers_json="$(yq -c '."proxy-providers" | keys' "$cfg" 2>/dev/null)"
|
||
[[ "$providers_json" != "null" && "$providers_json" != "[]" ]] || { warn "cannot detect proxy-provider names from $cfg"; return 1; }
|
||
|
||
profile_dir="$(dirname "$cfg")"
|
||
ensure_bypass_list "$profile_dir"
|
||
bypass_rules="$(compile_bypass_rules "${profile_dir}/bypass.list")"
|
||
rules_json="$({
|
||
printf "%s\n" "$bypass_rules"
|
||
printf "%s\n" \
|
||
"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"
|
||
} | rules_json_from_lines)"
|
||
|
||
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"
|
||
}
|
||
|
||
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() {
|
||
local kw="${1:-}"
|
||
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")"
|
||
|
||
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}"
|
||
[[ -n "$now" ]] && echo "⭐ Now: ${now}"
|
||
[[ -n "$kw" ]] && echo "🔎 Filter: ${kw}"
|
||
echo
|
||
|
||
local i
|
||
for i in "${!nodes[@]}"; do
|
||
printf " %d) %s%s\n" "$((i+1))" "$(if [[ "${nodes[$i]}" == "$now" ]]; then printf "⭐ "; else printf " "; fi)" "${nodes[$i]}"
|
||
done
|
||
|
||
[[ -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>"
|
||
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
|
||
}
|
||
|
||
|
||
|
||
#==============================搜索测速排序,上======================
|
||
|
||
#============================状态检查,下=====================
|
||
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 <<EOF
|
||
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"
|
||
EOF
|
||
}
|
||
|
||
net_test() {
|
||
# 常用网站连通性测试:输出 “URL -> 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|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/7890/7891 端口监听:"
|
||
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
|
||
}
|
||
|
||
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() {
|
||
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: <none>"
|
||
[[ -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|7890|7891|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
|
||
}
|
||
|
||
#=========================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 <name>"
|
||
select_one "select" "${2}";;
|
||
end)
|
||
start_end "$@";;
|
||
select)
|
||
select_one "$@";;
|
||
db)
|
||
debug_one "$@";;
|
||
doctor)
|
||
doctor ;;
|
||
env)
|
||
show_proxy_exports ;;
|
||
theme)
|
||
shift; theme_cmd "${1:-show}" ;;
|
||
sysproxy)
|
||
shift; sysproxy_cmd "${1:-status}" ;;
|
||
update)
|
||
shift; update_providers_cmd "${1:-}" ;;
|
||
mode)
|
||
shift; mode_set "${1:-}" ;;
|
||
rules)
|
||
shift; rules_cmd "${1:-show}" ;;
|
||
help|-h|--help) help ;;
|
||
-l|--list|list) shift; list_nodes "${1:-}" ;;
|
||
tun) shift; tun "${1:-toggle}" ;;
|
||
*) pick_switch_with_delay "${1:-}" 1200 ;;
|
||
esac
|
||
}
|
||
main "$@"
|