根因:mihomo 退出后 0.0.0.0/1 + 128.0.0.0/1 黑洞路由未清掉, 所有流量指向已消失的 TUN 网关 198.18.0.1 → 断网。 macOS (clash): - start 解析 mihomo 真实 PID 写入 PID 文件(原记的是 sudo PID) - cmd_stop 直接 pkill -TERM/-9 mihomo,不再依赖 sudo PID - network_restore 无条件删除 0.0.0.0/1 + 128.0.0.0/1 黑洞路由, 并放宽 TUN 识别到整个 198.18.x fake-IP 段(不再只认 198.18.0.1) - network_leftovers 检测同步放宽 Windows (clash.ps1): - NetworkRestore 删除 wintun 网卡上的全部路由(不限网段)+ 清理 网关落在 198.18.x 的所有路由 - Test-NetworkLeftovers 路由检测放宽到 198.18.x - CmdStop 强制终止时增加按进程名强杀兜底(防止 PID 文件不准杀不掉) 两版已做语法检查与入口冒烟测试。
690 lines
22 KiB
Bash
Executable File
690 lines
22 KiB
Bash
Executable File
#!/bin/zsh
|
||
# ═══════════════════════════════════════════════════════════
|
||
# Mihomo Kernel Manager — macOS
|
||
# clash <command> | clash 进入交互菜单 | clash help
|
||
# ═══════════════════════════════════════════════════════════
|
||
|
||
CONFIG_DIR="${HOME}/.config/mihomo"
|
||
CONFIG_FILE="${CONFIG_DIR}/config.yaml"
|
||
PID_FILE="${CONFIG_DIR}/mihomo.pid"
|
||
LOG_FILE="${CONFIG_DIR}/mihomo.log"
|
||
LINK_FILE="${CONFIG_DIR}/source.txt" # 源 YAML 路径
|
||
SOURCE_DIR="${HOME}/Mihomo" # 源配置目录(自动检测最新 YAML)
|
||
MIXED_PORT="7890"
|
||
API_PORT="9090"
|
||
MIHOMO_BIN="/opt/homebrew/bin/mihomo"
|
||
|
||
[[ -d "$CONFIG_DIR" ]] || mkdir -p "$CONFIG_DIR"
|
||
|
||
# ─── 颜色 ───
|
||
BOLD='\033[1m'; DIM='\033[2m'
|
||
GREEN='\033[0;32m'; BOLD_GREEN='\033[1;32m'
|
||
RED='\033[0;31m'; BOLD_RED='\033[1;31m'
|
||
YELLOW='\033[0;33m'; BOLD_YELLOW='\033[1;33m'
|
||
BLUE='\033[0;34m'; BOLD_BLUE='\033[1;34m'
|
||
PURPLE='\033[0;35m'; BOLD_PURPLE='\033[1;35m'
|
||
CYAN='\033[0;36m'; BOLD_CYAN='\033[1;36m'
|
||
WHITE='\033[1;37m'
|
||
NC='\033[0m'
|
||
SEP="${DIM}──────────────────────────────────────────${NC}"
|
||
|
||
# ─── 输出辅助 ───
|
||
info() { echo " ${CYAN}ℹ${NC} $*"; }
|
||
ok() { echo " ${GREEN}✓${NC} $*"; }
|
||
warn() { echo " ${YELLOW}⚠${NC} $*"; }
|
||
err() { echo " ${RED}✗${NC} $*"; }
|
||
dim() { echo " ${DIM}$*${NC}"; }
|
||
section(){ echo ""; echo " ${BOLD}── $* ──${NC}"; }
|
||
|
||
# ─── 运行状态 ───
|
||
is_running() {
|
||
[[ -f "$PID_FILE" ]] && pgrep -F "$PID_FILE" &>/dev/null
|
||
}
|
||
|
||
# ========== 源文件自动检测 ==========
|
||
# 优先级:link 固定路径 > SOURCE_DIR 最新 .yaml/.yml
|
||
auto_detect_source() {
|
||
if [[ -f "$LINK_FILE" ]]; then
|
||
local linked=$(cat "$LINK_FILE")
|
||
if [[ -f "$linked" ]]; then
|
||
echo "$linked"
|
||
return 0
|
||
fi
|
||
warn "链接文件不存在: $linked,尝试自动检测..."
|
||
fi
|
||
if [[ -d "$SOURCE_DIR" ]]; then
|
||
local latest=$(ls -t "$SOURCE_DIR"/*.yaml(N) "$SOURCE_DIR"/*.yml(N) 2>/dev/null | head -1)
|
||
if [[ -n "$latest" && -f "$latest" ]]; then
|
||
echo "$latest"
|
||
return 0
|
||
fi
|
||
fi
|
||
return 1
|
||
}
|
||
|
||
# ========== macOS 适配:复制源文件并做兼容转换(不改原件)==========
|
||
macos_adapt_config() {
|
||
local src="${1:-}"
|
||
if [[ -z "$src" ]]; then
|
||
src=$(auto_detect_source)
|
||
fi
|
||
if [[ -z "$src" || ! -f "$src" ]]; then
|
||
err "未找到 YAML 配置文件"
|
||
dim "请把 .yaml 放到 $SOURCE_DIR,或用: clash link /path/to/file.yaml"
|
||
return 1
|
||
fi
|
||
|
||
echo -n " 适配 macOS 配置 ... "
|
||
cp "$src" "$CONFIG_FILE"
|
||
|
||
# macOS 兼容修复
|
||
sed -i '' '/^[[:space:]]*device: nikki[[:space:]]*$/d' "$CONFIG_FILE"
|
||
sed -i '' '/^[[:space:]]*device: nikki$/d' "$CONFIG_FILE"
|
||
sed -i '' 's/^\([[:space:]]*stack:\) system/\1 gvisor/' "$CONFIG_FILE"
|
||
sed -i '' '/^[[:space:]]*auto-redirect: true/d' "$CONFIG_FILE"
|
||
sed -i '' 's/ - tcp:\/\// - /g' "$CONFIG_FILE"
|
||
sed -i '' 's/ - udp:\/\// - /g' "$CONFIG_FILE"
|
||
|
||
ok "完成"
|
||
dim "源文件 $src"
|
||
dim "适配后 $CONFIG_FILE"
|
||
return 0
|
||
}
|
||
|
||
# ========== 管理面板 ==========
|
||
# /ui 下可能有多个面板子目录(如 zashboard),自动定位并生成首页跳转
|
||
ui_url() {
|
||
local ui_dir="$CONFIG_DIR/ui"
|
||
if [[ -d "$ui_dir" ]]; then
|
||
local dash
|
||
dash=$(find "$ui_dir" -maxdepth 1 -mindepth 1 -type d 2>/dev/null | sort | head -1)
|
||
if [[ -n "$dash" ]]; then
|
||
echo "http://127.0.0.1:${API_PORT}/ui/$(basename "$dash")/"
|
||
return 0
|
||
fi
|
||
fi
|
||
echo "http://127.0.0.1:${API_PORT}/ui"
|
||
}
|
||
|
||
ensure_ui_index() {
|
||
local ui_dir="$CONFIG_DIR/ui"
|
||
[[ -d "$ui_dir" ]] || return 0
|
||
local dash
|
||
dash=$(find "$ui_dir" -maxdepth 1 -mindepth 1 -type d 2>/dev/null | sort | head -1)
|
||
[[ -z "$dash" ]] && return 0
|
||
local dash_name=$(basename "$dash")
|
||
local index_file="$ui_dir/index.html"
|
||
if [[ ! -f "$index_file" ]]; then
|
||
cat > "$index_file" <<EOF
|
||
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta http-equiv="refresh" content="0; url=./${dash_name}/">
|
||
<title>Mihomo Dashboard</title>
|
||
</head>
|
||
<body>
|
||
<p>正在打开面板 <a href="./${dash_name}/">${dash_name}</a> ...</p>
|
||
</body>
|
||
</html>
|
||
EOF
|
||
info "已生成面板首页跳转: $index_file"
|
||
fi
|
||
}
|
||
|
||
# ========== 残留网络状态检测 ==========
|
||
# 返回 0 = 存在 mihomo 残留进程 / 残留 TUN 劫持 / 系统代理 / DNS 劫持
|
||
network_leftovers() {
|
||
pgrep -l mihomo >/dev/null 2>&1 && return 0
|
||
|
||
local dev
|
||
for dev in $(ifconfig -l 2>/dev/null); do
|
||
[[ "$dev" != utun* ]] && continue
|
||
local idx="${dev#utun}"
|
||
[[ "$idx" != <-> ]] && continue
|
||
if ifconfig "$dev" 2>/dev/null | grep -qE "inet 198\.18\."; then
|
||
return 0
|
||
fi
|
||
if netstat -rn -f inet 2>/dev/null | grep -qE "198\.18\.[[:space:]]+.*[[:space:]]+$dev"; then
|
||
return 0
|
||
fi
|
||
(( idx >= 1024 )) && return 0
|
||
done
|
||
|
||
local hijacked=0
|
||
while IFS= read -r service; do
|
||
local clean=$(printf '%s' "$service" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
|
||
[[ -z "$clean" ]] && continue
|
||
local wp=$(networksetup -getwebproxy "$clean" 2>/dev/null)
|
||
local sp=$(networksetup -getsocksfirewallproxy "$clean" 2>/dev/null)
|
||
if echo "$wp$sp" | grep -qE "Server: 127\.0\.0\.1" && echo "$wp$sp" | grep -qE "Port: ${MIXED_PORT}"; then
|
||
hijacked=1
|
||
break
|
||
fi
|
||
done < <(networksetup -listallnetworkservices 2>/dev/null | tail -n +2)
|
||
[[ $hijacked -eq 1 ]] && return 0
|
||
|
||
scutil --dns 2>/dev/null | grep -qE "nameserver\[[0-9]+\].*(127\.0\.0\.1|198\.18\.)" && return 0
|
||
return 1
|
||
}
|
||
|
||
# ========== 网络恢复(TUN/路由/代理/DNS 全面清理)==========
|
||
network_restore() {
|
||
local verbose=${1:-true}
|
||
|
||
$verbose && section "网络恢复"
|
||
|
||
# 1. 清理残留 mihomo 进程
|
||
if pgrep -l mihomo >/dev/null 2>&1; then
|
||
$verbose && warn "发现残留进程,正在清理..."
|
||
sudo pkill -9 mihomo 2>/dev/null
|
||
rm -f "$PID_FILE"
|
||
fi
|
||
|
||
# 2. 清理 mihomo 残留 TUN / 全量劫持路由
|
||
# 放宽识别:fake-IP 网关只要是 198.18.x 段都算(不限于 198.18.0.1),
|
||
# 并无条件删除 0.0.0.0/1 + 128.0.0.0/1 这两条黑洞路由,防止残留导致断网
|
||
local tun_list=$(ifconfig -l 2>/dev/null)
|
||
# fake-IP 网关段(默认 198.18.x,放宽不再只认 198.18.0.1)
|
||
local fake_gws=$(netstat -rn -f inet 2>/dev/null | awk '$4 ~ /^utun[0-9]+$/ && $2 ~ /^198\.18\./ {print $2}' | sort -u)
|
||
local route_devs=$(netstat -rn -f inet 2>/dev/null | awk '$4 ~ /^utun[0-9]+$/ && $2 ~ /^198\.18\./ {print $4}' | sort -u)
|
||
local mihomo_tuns=""
|
||
for dev in ${(s: :)tun_list}; do
|
||
[[ "$dev" != utun* ]] && continue
|
||
local idx="${dev#utun}"
|
||
[[ "$idx" != <-> ]] && continue
|
||
if ifconfig "$dev" 2>/dev/null | grep -qE "inet 198\.18\."; then
|
||
mihomo_tuns+=" $dev"
|
||
continue
|
||
fi
|
||
if [[ -n "$route_devs" ]] && echo "$route_devs" | grep -qx "$dev"; then
|
||
mihomo_tuns+=" $dev"
|
||
continue
|
||
fi
|
||
(( idx >= 1024 )) && mihomo_tuns+=" $dev"
|
||
done
|
||
|
||
# 无条件清理全量劫持路由(0.0.0.0/1 + 128.0.0.0/1),几乎必定是 mihomo 创建
|
||
for gw in ${(s: :)fake_gws}; do
|
||
sudo route -n delete -net 0.0.0.0/1 "$gw" 2>/dev/null
|
||
sudo route -n delete -net 128.0.0.0/1 "$gw" 2>/dev/null
|
||
sudo route -n delete -net 198.18.0.0/16 "$gw" 2>/dev/null
|
||
done
|
||
# 兜底:常见默认网关组合,防止识别遗漏留下黑洞
|
||
sudo route -n delete -net 0.0.0.0/1 198.18.0.1 2>/dev/null
|
||
sudo route -n delete -net 128.0.0.0/1 198.18.0.1 2>/dev/null
|
||
|
||
for dev in ${(s: :)mihomo_tuns}; do
|
||
$verbose && echo -n " 清理残留 TUN $dev ... "
|
||
sudo ifconfig "$dev" down 2>/dev/null
|
||
sudo ifconfig "$dev" destroy 2>/dev/null
|
||
$verbose && echo "${GREEN}✓${NC}"
|
||
done
|
||
|
||
# 3. 关闭系统 HTTP/HTTPS/SOCKS 代理(遍历所有网络服务)
|
||
while IFS= read -r service; do
|
||
local clean=$(printf '%s' "$service" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
|
||
[[ -z "$clean" ]] && continue
|
||
local wp=$(networksetup -getwebproxy "$clean" 2>/dev/null)
|
||
local sp=$(networksetup -getsocksfirewallproxy "$clean" 2>/dev/null)
|
||
if echo "$wp$sp" | grep -qE "Server: 127\.0\.0\.1" && echo "$wp$sp" | grep -qE "Port: ${MIXED_PORT}"; then
|
||
$verbose && echo -n " 关闭系统代理 ($clean) ... "
|
||
sudo networksetup -setwebproxystate "$clean" off 2>/dev/null
|
||
sudo networksetup -setsecurewebproxystate "$clean" off 2>/dev/null
|
||
sudo networksetup -setsocksfirewallproxystate "$clean" off 2>/dev/null
|
||
$verbose && echo "${GREEN}✓${NC}"
|
||
fi
|
||
done < <(networksetup -listallnetworkservices 2>/dev/null | tail -n +2)
|
||
|
||
# 4. 刷新 DNS 缓存
|
||
$verbose && echo -n " 刷新 DNS 缓存 ... "
|
||
sudo dscacheutil -flushcache 2>/dev/null
|
||
sudo killall -HUP mDNSResponder 2>/dev/null
|
||
$verbose && echo "${GREEN}✓${NC}"
|
||
|
||
# 5. 还原系统 DNS(如果被 mihomo 指到 127.0.0.1 / 198.18.x)
|
||
if scutil --dns 2>/dev/null | grep -qE "nameserver\[[0-9]+\].*(127\.0\.0\.1|198\.18\.)"; then
|
||
$verbose && echo -n " 还原系统 DNS ... "
|
||
while IFS= read -r service; do
|
||
local clean=$(printf '%s' "$service" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
|
||
[[ -z "$clean" ]] && continue
|
||
if networksetup -getdnsservers "$clean" 2>/dev/null | grep -qE "^(127\.0\.0\.1|198\.18\.)"; then
|
||
sudo networksetup -setdnsservers "$clean" empty 2>/dev/null
|
||
fi
|
||
done < <(networksetup -listallnetworkservices 2>/dev/null | tail -n +2)
|
||
sudo dscacheutil -flushcache 2>/dev/null
|
||
sudo killall -HUP mDNSResponder 2>/dev/null
|
||
$verbose && echo "${GREEN}✓${NC}"
|
||
fi
|
||
|
||
$verbose && echo ""
|
||
$verbose && ok "网络已恢复"
|
||
}
|
||
|
||
# ========== 网络诊断 ==========
|
||
network_health_check() {
|
||
local issues=0
|
||
section "网络诊断"
|
||
|
||
if nslookup baidu.com >/dev/null 2>&1; then
|
||
ok "DNS 解析正常 (baidu.com)"
|
||
else
|
||
err "DNS 解析失败"
|
||
issues=1
|
||
fi
|
||
|
||
if ping -c 1 -W 2 8.8.8.8 >/dev/null 2>&1; then
|
||
ok "外网连通 (8.8.8.8)"
|
||
else
|
||
err "外网不可达"
|
||
issues=1
|
||
fi
|
||
|
||
local tun_count=$(ifconfig -l 2>/dev/null | tr ' ' '\n' | grep -c "^utun")
|
||
local tun_high=0
|
||
local dev
|
||
for dev in $(ifconfig -l 2>/dev/null); do
|
||
[[ "$dev" != utun* ]] && continue
|
||
local idx="${dev#utun}"
|
||
[[ "$idx" != <-> ]] && continue
|
||
(( idx >= 1024 )) && (( tun_high++ ))
|
||
done
|
||
if [[ $tun_high -eq 0 ]]; then
|
||
ok "TUN 接口 ${tun_count} 个,无残留"
|
||
else
|
||
warn "TUN ${tun_count} 个(${tun_high} 个高编号残留)"
|
||
issues=1
|
||
fi
|
||
|
||
if [[ $issues -eq 1 ]]; then
|
||
warn "网络可能有异常,试试: clash cleanup"
|
||
else
|
||
ok "网络一切正常"
|
||
fi
|
||
}
|
||
|
||
# ========== 启动 ==========
|
||
cmd_start() {
|
||
local src="${1:-}"
|
||
local do_adapt=true
|
||
|
||
if [[ -n "$src" ]]; then
|
||
if [[ "$src" == "$CONFIG_FILE" ]] || [[ "$src" == "$(cat "$LINK_FILE" 2>/dev/null)" ]]; then
|
||
do_adapt=false
|
||
fi
|
||
fi
|
||
if [[ -z "$src" ]]; then
|
||
src=$(auto_detect_source)
|
||
fi
|
||
|
||
if $do_adapt && [[ "$src" != "$CONFIG_FILE" ]]; then
|
||
macos_adapt_config "$src" || return 1
|
||
elif [[ ! -f "$CONFIG_FILE" ]]; then
|
||
err "配置文件不存在: $CONFIG_FILE"
|
||
dim "试试: clash link /path/to/your.yaml"
|
||
return 1
|
||
fi
|
||
|
||
if is_running; then
|
||
warn "mihomo 已经在运行了"
|
||
return 0
|
||
fi
|
||
|
||
# 启动前清理上次异常退出残留(旧 TUN/路由会继续劫持全部流量)
|
||
if network_leftovers; then
|
||
echo ""
|
||
info "检测到上次残留网络状态,先清理 ..."
|
||
network_restore false
|
||
fi
|
||
|
||
echo ""
|
||
echo -n " 启动中 ... "
|
||
nohup sudo "$MIHOMO_BIN" -d "$CONFIG_DIR" -f "$CONFIG_FILE" > "$LOG_FILE" 2>&1 &
|
||
local sudo_pid=$!
|
||
sleep 1
|
||
# 解析 mihomo 真实 PID(sudo 的子进程),PID 文件记它,stop 才能精确杀
|
||
local mihomo_pid=$(pgrep -P "$sudo_pid" 2>/dev/null | head -1)
|
||
echo "${mihomo_pid:-$sudo_pid}" > "$PID_FILE"
|
||
|
||
if is_running; then
|
||
ok "已就绪"
|
||
ensure_ui_index
|
||
echo ""
|
||
printf " ${DIM}%-10s${NC} ${CYAN}127.0.0.1:${MIXED_PORT}${NC} ${DIM}(HTTP/SOCKS5)${NC}\n" "代理端口"
|
||
printf " ${DIM}%-10s${NC} ${CYAN}127.0.0.1:${API_PORT}${NC}\n" "API 控制"
|
||
printf " ${DIM}%-10s${NC} ${CYAN}$(ui_url)${NC}\n" "管理面板"
|
||
echo ""
|
||
else
|
||
err "启动失败"
|
||
dim "查看日志: tail $LOG_FILE"
|
||
rm -f "$PID_FILE"
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
# ========== 停止 ==========
|
||
cmd_stop() {
|
||
if ! is_running; then
|
||
warn "mihomo 没有在运行"
|
||
# 进程没了但可能残留 TUN/路由/代理(例如被强杀),顺手清理
|
||
if network_leftovers; then
|
||
echo ""
|
||
info "检测到残留网络状态,正在清理 ..."
|
||
network_restore true
|
||
echo ""
|
||
network_health_check
|
||
fi
|
||
return 0
|
||
fi
|
||
|
||
local pid=$(cat "$PID_FILE")
|
||
echo ""
|
||
echo -n " 停止中 ... "
|
||
# 直接给 mihomo 发 SIGTERM(PID 文件记的是 sudo,按进程名杀最稳)
|
||
sudo pkill -TERM -x mihomo 2>/dev/null
|
||
sudo kill "$pid" 2>/dev/null # 兜底:也通知 sudo 进程
|
||
|
||
for i in {1..10}; do
|
||
if ! pgrep -x mihomo >/dev/null 2>&1; then
|
||
ok "已停止"
|
||
rm -f "$PID_FILE"
|
||
echo ""
|
||
network_restore false
|
||
network_health_check
|
||
return 0
|
||
fi
|
||
sleep 0.5
|
||
done
|
||
|
||
echo ""
|
||
warn "超时,强制终止..."
|
||
sudo pkill -9 -x mihomo 2>/dev/null
|
||
sleep 0.5
|
||
rm -f "$PID_FILE"
|
||
|
||
if pgrep -x mihomo >/dev/null 2>&1; then
|
||
err "停止失败,残留进程仍在运行"
|
||
dim "试试: sudo pkill -9 mihomo"
|
||
return 1
|
||
fi
|
||
|
||
warn "已强制停止"
|
||
info "正在恢复网络 ..."
|
||
network_restore true
|
||
echo ""
|
||
network_health_check
|
||
}
|
||
|
||
# ========== 重启 / 热重载 ==========
|
||
cmd_restart() {
|
||
cmd_stop
|
||
sleep 1
|
||
cmd_start "$@"
|
||
}
|
||
|
||
cmd_reload() {
|
||
local cfg="${1:-$CONFIG_FILE}"
|
||
if ! is_running; then
|
||
warn "mihomo 没在运行,直接启动"
|
||
cmd_start "$cfg"
|
||
return $?
|
||
fi
|
||
if [[ ! -f "$cfg" ]]; then
|
||
err "配置文件不存在: $cfg"
|
||
return 1
|
||
fi
|
||
echo -n " 热重载配置 ... "
|
||
if curl -s -X PUT "http://127.0.0.1:${API_PORT}/configs" \
|
||
-H "Content-Type: application/json" \
|
||
-d "{\"path\":\"${cfg}\"}" > /dev/null 2>&1; then
|
||
ok "成功"
|
||
else
|
||
err "API 不可达,尝试重启..."
|
||
cmd_restart "$cfg"
|
||
fi
|
||
}
|
||
|
||
# ========== 状态 ==========
|
||
cmd_status() {
|
||
if is_running; then
|
||
local pid=$(cat "$PID_FILE")
|
||
echo ""
|
||
echo " ${BOLD_GREEN}● 运行中${NC} ${DIM}PID: $pid${NC}"
|
||
echo " $SEP"
|
||
printf " ${DIM}%-10s${NC} ${CYAN}127.0.0.1:${MIXED_PORT}${NC} (HTTP/SOCKS5)\n" "代理端口"
|
||
printf " ${DIM}%-10s${NC} ${CYAN}127.0.0.1:${API_PORT}${NC}\n" "API 端口"
|
||
printf " ${DIM}%-10s${NC} ${CYAN}$(ui_url)${NC}\n" "管理面板"
|
||
printf " ${DIM}%-10s${NC} %s\n" "配置文件" "$CONFIG_FILE"
|
||
printf " ${DIM}%-10s${NC} %s\n" "日志文件" "$LOG_FILE"
|
||
local src=$(cat "$LINK_FILE" 2>/dev/null || auto_detect_source 2>/dev/null)
|
||
[[ -n "$src" ]] && printf " ${DIM}%-10s${NC} %s\n" "源文件" "$src"
|
||
echo ""
|
||
else
|
||
echo ""
|
||
echo " ${BOLD_RED}○ 已停止${NC}"
|
||
echo " $SEP"
|
||
dim "mihomo 当前未运行"
|
||
echo ""
|
||
rm -f "$PID_FILE"
|
||
fi
|
||
}
|
||
|
||
# ========== 链接源配置(查看/设置,合并原 import)==========
|
||
cmd_link() {
|
||
local src="${1:-}"
|
||
if [[ -z "$src" ]]; then
|
||
echo ""
|
||
section "源文件链接"
|
||
if [[ -f "$LINK_FILE" ]]; then
|
||
info "当前链接: $(cat "$LINK_FILE")"
|
||
else
|
||
local detected=$(auto_detect_source)
|
||
if [[ -n "$detected" ]]; then
|
||
info "自动检测: $detected"
|
||
else
|
||
warn "未检测到源文件"
|
||
fi
|
||
fi
|
||
echo ""
|
||
echo -n " 请输入 YAML 文件路径 ${DIM}(Enter 取消)${NC}: "
|
||
read src
|
||
[[ -z "$src" ]] && { dim "已取消"; return 0; }
|
||
src=$(echo "$src" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//;s/^"//;s/"$//')
|
||
fi
|
||
|
||
if [[ ! -f "$src" ]]; then
|
||
err "文件不存在: $src"
|
||
return 1
|
||
fi
|
||
|
||
echo "$src" > "$LINK_FILE"
|
||
ok "已链接"
|
||
dim "源文件 $src"
|
||
echo ""
|
||
echo -n " 立即适配并启动? (y/n) "
|
||
read -q ans
|
||
echo ""
|
||
if [[ "$ans" == "y" ]]; then
|
||
macos_adapt_config "$src" || return 1
|
||
if is_running; then
|
||
cmd_restart
|
||
else
|
||
cmd_start
|
||
fi
|
||
fi
|
||
}
|
||
|
||
# ========== 日志 / 编辑 / 面板 / 修复 / 诊断 ==========
|
||
cmd_log() {
|
||
if [[ -f "$LOG_FILE" ]]; then
|
||
echo ""
|
||
section "实时日志 (Ctrl+C 退出)"
|
||
tail -f "$LOG_FILE"
|
||
else
|
||
warn "日志文件不存在"
|
||
fi
|
||
}
|
||
|
||
cmd_edit() {
|
||
info "打开编辑器: $CONFIG_FILE"
|
||
${EDITOR:-vim} "$CONFIG_FILE"
|
||
}
|
||
|
||
cmd_open() {
|
||
if is_running; then
|
||
ensure_ui_index
|
||
local url=$(ui_url)
|
||
open "$url"
|
||
ok "已打开管理面板"
|
||
dim "$url"
|
||
else
|
||
warn "mihomo 未运行,请先启动"
|
||
fi
|
||
}
|
||
|
||
cmd_cleanup() {
|
||
network_restore true
|
||
}
|
||
|
||
cmd_doctor() {
|
||
network_health_check
|
||
}
|
||
|
||
# ========== 关闭 Clash Verge 后台服务 ==========
|
||
cmd_verge_off() {
|
||
echo ""
|
||
section "关闭 Clash Verge"
|
||
local svc_pid=$(pgrep -f "clash-verge-service" 2>/dev/null)
|
||
if [[ -n "$svc_pid" ]]; then
|
||
info "发现 Clash Verge 服务 (PID: $svc_pid)"
|
||
echo -n " 正在停止 ... "
|
||
sudo kill "$svc_pid" 2>/dev/null
|
||
sleep 1
|
||
if pgrep -f "clash-verge-service" >/dev/null 2>&1; then
|
||
sudo kill -9 "$svc_pid" 2>/dev/null
|
||
fi
|
||
ok "已停止"
|
||
else
|
||
ok "Clash Verge 服务未运行"
|
||
fi
|
||
|
||
echo ""
|
||
echo -n " 是否同时卸载开机自启? (y/n) "
|
||
read -q ans
|
||
echo ""
|
||
if [[ "$ans" == "y" ]]; then
|
||
sudo /bin/launchctl unload /Library/LaunchDaemons/io.github.clash-verge-rev.clash-verge-rev.plist 2>/dev/null
|
||
ok "已卸载开机自启"
|
||
fi
|
||
echo ""
|
||
ok "现在可以纯用 clash 脚本管理了"
|
||
echo ""
|
||
}
|
||
|
||
# ========== 帮助 ==========
|
||
show_help() {
|
||
echo ""
|
||
echo " ${BOLD_PURPLE}Mihomo Kernel Manager${NC} ${DIM}— macOS${NC}"
|
||
echo " $SEP"
|
||
echo ""
|
||
echo " ${BOLD}用法:${NC}"
|
||
echo " ${CYAN}clash <command>${NC} 执行子命令"
|
||
echo " ${CYAN}clash${NC} 进入交互菜单"
|
||
echo " ${CYAN}clash help${NC} 显示本帮助"
|
||
echo ""
|
||
echo " ${BOLD}核心:${NC}"
|
||
printf " ${CYAN}%-14s${NC} %s\n" "start [file]" "启动(自动适配 macOS 配置)"
|
||
printf " ${CYAN}%-14s${NC} %s\n" "stop" "停止并恢复网络"
|
||
printf " ${CYAN}%-14s${NC} %s\n" "restart" "重启"
|
||
printf " ${CYAN}%-14s${NC} %s\n" "status" "查看运行状态"
|
||
printf " ${CYAN}%-14s${NC} %s\n" "reload [file]" "热重载配置"
|
||
printf " ${CYAN}%-14s${NC} %s\n" "open" "打开管理面板"
|
||
printf " ${CYAN}%-14s${NC} %s\n" "log" "实时日志"
|
||
echo ""
|
||
echo " ${BOLD}配置:${NC}"
|
||
printf " ${CYAN}%-14s${NC} %s\n" "link [file]" "查看/设置源 YAML(含原 import)"
|
||
printf " ${CYAN}%-14s${NC} %s\n" "edit" "编辑当前配置"
|
||
echo ""
|
||
echo " ${BOLD}网络:${NC}"
|
||
printf " ${CYAN}%-14s${NC} %s\n" "cleanup" "网络修复(清理残留 TUN/路由/代理/DNS)"
|
||
printf " ${CYAN}%-14s${NC} %s\n" "doctor" "网络诊断"
|
||
echo ""
|
||
echo " ${BOLD}系统:${NC}"
|
||
printf " ${CYAN}%-14s${NC} %s\n" "verge-off" "关闭 Clash Verge 后台服务"
|
||
echo ""
|
||
echo " ${BOLD}示例:${NC}"
|
||
dim "clash start"
|
||
dim "clash link ~/Mihomo/config.yaml"
|
||
dim "clash cleanup"
|
||
echo ""
|
||
}
|
||
|
||
# ========== 交互菜单 ==========
|
||
show_menu() {
|
||
clear
|
||
echo ""
|
||
echo " ${BOLD_PURPLE}Mihomo Kernel Manager${NC} ${DIM}— macOS${NC}"
|
||
echo " $SEP"
|
||
|
||
if is_running; then
|
||
printf " ${BOLD_GREEN}● 运行中${NC} ${DIM}PID: %s${NC} ${DIM}代理: %s${NC} ${DIM}API: %s${NC}\n" \
|
||
"$(cat "$PID_FILE" 2>/dev/null)" "${MIXED_PORT}" "${API_PORT}"
|
||
else
|
||
echo " ${BOLD_RED}○ 已停止${NC}"
|
||
fi
|
||
echo ""
|
||
|
||
echo " ${BOLD}1${NC} 启动 ${BOLD}5${NC} 状态"
|
||
echo " ${BOLD}2${NC} 停止 ${BOLD}6${NC} 实时日志"
|
||
echo " ${BOLD}3${NC} 打开面板 ${BOLD}7${NC} 热重载"
|
||
echo " ${BOLD}4${NC} 重启 ${BOLD}8${NC} 网络修复"
|
||
echo " ${BOLD}0${NC} 退出"
|
||
echo ""
|
||
echo -n " ${BOLD}❯${NC} "
|
||
}
|
||
|
||
interactive_menu() {
|
||
while true; do
|
||
show_menu
|
||
read choice
|
||
echo ""
|
||
case "$choice" in
|
||
1) cmd_start ;;
|
||
2) cmd_stop ;;
|
||
3) cmd_open ;;
|
||
4) cmd_restart ;;
|
||
5) cmd_status ;;
|
||
6) cmd_log ;;
|
||
7) cmd_reload ;;
|
||
8) cmd_cleanup ;;
|
||
0|q|Q)
|
||
echo " ${DIM}再见 👋${NC}"
|
||
echo ""
|
||
break
|
||
;;
|
||
*) warn "无效选项,请输入 0-8" ;;
|
||
esac
|
||
echo ""
|
||
echo -n " 按 Enter 返回菜单..."
|
||
read
|
||
done
|
||
}
|
||
|
||
# ========== 入口 ==========
|
||
case "${1:-}" in
|
||
start|s) cmd_start "$2" ;;
|
||
stop) cmd_stop ;;
|
||
restart|r) cmd_restart "$2" ;;
|
||
reload|rl) cmd_reload "$2" ;;
|
||
status|st) cmd_status ;;
|
||
log|l) cmd_log ;;
|
||
link|lk) cmd_link "$2" ;;
|
||
open|o) cmd_open ;;
|
||
edit|e) cmd_edit ;;
|
||
cleanup|c) cmd_cleanup ;;
|
||
doctor|d) cmd_doctor ;;
|
||
verge-off) cmd_verge_off ;;
|
||
help|-h|--help|h) show_help ;;
|
||
*) interactive_menu ;;
|
||
esac
|