先理解 Mihomo API:自动切换不是客户端按钮魔法

很多用户搜索「Clash API 自动切换节点」时,第一反应是寻找一个能在节点失效后自动点击界面的脚本。实际上,真正稳定的方案不依赖鼠标操作,而是通过 Mihomo 的外部控制器 API读取代理组、修改当前选择,并结合健康检查结果完成故障转移。Clash Verge Rev、Mihomo Party、Clash for Android 等客户端只是提供了不同的界面,脚本最终连接的仍然是本机 Mihomo 内核暴露的 HTTP API。

典型链路可以拆成四层:第一层是控制器地址,通常监听在 127.0.0.1:9090 或客户端设置的其他端口;第二层是鉴权,由 secret 令牌保护接口;第三层是代理组状态,脚本通过 API 查询当前策略组、成员节点和延迟;第四层是切换动作,向指定代理组发送新的节点名称。只有这四层都确认无误,自动切换才不会变成「脚本运行了,但实际出口没有变化」。

Mihomo 常见的控制接口包括 /version/proxies/configs/connections。其中,/proxies 是自动切换最重要的入口:GET 请求可以取得全部代理组和节点状态,PUT 请求则可以修改某个可手动选择的代理组。需要注意,策略组名称、节点名称以及 API 路径都可能包含中文、空格或特殊字符,脚本不能简单地把字符串拼接到 URL 中,应该使用 URL 编码或标准 JSON 库处理。

先确认控制器:在客户端设置中找到「外部控制器」「External Controller」或「API 端口」;不要盲目假设端口一定是 9090。如果控制器绑定在 0.0.0.0,务必同时设置强密码,并用防火墙限制来源地址,避免把切换节点和查看连接记录的权限暴露到公网。

第一步:开启鉴权并用命令行验证 API

如果只在个人电脑上临时测试,可以先确认控制器是否可访问;如果脚本要部署到服务器、NAS 或长期运行的后台任务,建议从一开始就配置 secret。没有鉴权的 API 不仅能切换节点,还可能泄露访问目标、当前连接、DNS 请求和配置内容。更稳妥的做法是让控制器只监听回环地址,然后让脚本与 Mihomo 部署在同一台机器上。

配置文件中的写法通常类似下面这样。不同版本的字段位置可能略有差异,最终应以当前 Mihomo 文档和客户端生成的配置为准:

external-controller: 127.0.0.1:9090
secret: "change-this-to-a-long-random-token"

重载配置或重启内核后,先用 curl 验证版本接口。下面的命令不会修改任何状态,适合排查端口、路径和令牌问题:

export MIHOMO_API="http://127.0.0.1:9090"
export MIHOMO_SECRET="change-this-to-a-long-random-token"

curl --fail --silent \
  -H "Authorization: Bearer ${MIHOMO_SECRET}" \
  "${MIHOMO_API}/version"

如果返回 JSON 版本信息,说明基本鉴权链路已经打通;若返回 401,优先检查令牌是否与配置完全一致;若是连接拒绝,检查端口是否正确以及控制器是否已经启动;若返回 404,则可能连接到了其他服务,或者客户端使用了不同的 API 地址。不要一看到超时就立刻更换节点,因为这里的请求通常只发生在本机,和远端代理质量没有直接关系。

接着读取代理组。为了避免终端输出过长,可以先保存响应,再用 jq 查看组名:

curl --fail --silent \
  -H "Authorization: Bearer ${MIHOMO_SECRET}" \
  "${MIHOMO_API}/proxies" | jq -r '.proxies | keys[]'

你需要找到真正负责出站的组,例如 🚀 节点选择Proxy 或订阅服务商自定义的组名。不要直接把「全部节点」当作目标组:它可能是一个 URL 测速组、Fallback 组或 Select 组,不同类型支持的操作并不完全相同。自动选择通常优先使用 select 或具备明确切换语义的策略组,测速组则应让 Mihomo 自己按照其规则工作。

  1. 确认 API 端口:从客户端设置或配置文件读取 external-controller,不要凭经验填写。
  2. 确认鉴权:使用 Bearer Token 请求 /version,先证明身份验证没有问题。
  3. 确认目标组:/proxies 返回值中复制策略组的完整名称,保留空格、图标和标点。
  4. 确认候选节点:只使用该策略组实际包含的成员,不要手写一个组内不存在的名称。

第二步:用 Shell 构建轻量故障转移脚本

Shell 适合部署在 Linux 服务器、软路由或定时任务中。一个可维护的脚本至少要完成四件事:请求 API 时启用失败检测、从返回结果中确认目标节点存在、调用外部探测地址判断当前出口、切换后写入带时间戳的日志。只检测 API 自身是否返回成功是不够的,因为 API 在本机,节点即使全部失效,127.0.0.1:9090 仍然可能正常响应。

下面示例依赖 curljq。它读取指定策略组的当前节点,使用 Mihomo 的延迟测试接口逐个测试候选节点,并把第一个低于阈值的节点切换为当前节点。实际使用时,应根据服务商节点数量、服务器带宽和目标站点响应情况调整参数。

#!/usr/bin/env bash
set -Eeuo pipefail

API="${MIHOMO_API:-http://127.0.0.1:9090}"
SECRET="${MIHOMO_SECRET:?MIHOMO_SECRET is required}"
GROUP="${MIHOMO_GROUP:-Proxy}"
URL="${MIHOMO_TEST_URL:-https://www.gstatic.com/generate_204}"
TIMEOUT="${MIHOMO_TIMEOUT:-5000}"
MAX_LATENCY="${MIHOMO_MAX_LATENCY:-900}"
LOG="${MIHOMO_LOG:-$HOME/.local/state/mihomo-failover.log}"

mkdir -p "$(dirname "$LOG")"
auth=(-H "Authorization: Bearer ${SECRET}")
proxies="$(curl --fail --silent "${auth[@]}" "${API}/proxies")"

mapfile -t candidates < <(printf '%s' "$proxies" |
  jq -r --arg group "$GROUP" '.proxies[$group].all[]?')

if ((${#candidates[@]} == 0)); then
  echo "$(date -Is) no candidates in group: $GROUP" >> "$LOG"
  exit 2
fi

for node in "${candidates[@]}"; do
  encoded="$(jq -rn --arg value "$node" '$value|@uri')"
  delay="$(curl --fail --silent --max-time 8 \
    "${auth[@]}" \
    "${API}/proxies/${encoded}/delay?timeout=${TIMEOUT}&url=$(jq -rn --arg value "$URL" '$value|@uri')" |
    jq -r '.delay // 999999' || echo 999999)"

  if [[ "$delay" =~ ^[0-9]+$ ]] && ((delay <= MAX_LATENCY)); then
    jq -n --arg name "$node" '{name:$name}' |
      curl --fail --silent -X PUT "${auth[@]}" \
        -H "Content-Type: application/json" \
        --data-binary @- "${API}/proxies/$(jq -rn --arg value "$GROUP" '$value|@uri')"

    echo "$(date -Is) switched group=$GROUP node=$node delay=${delay}ms" >> "$LOG"
    exit 0
  fi
done

echo "$(date -Is) no healthy node found" >> "$LOG"
exit 1

这里有一个容易忽略的工程细节:jq@uri 用来编码组名和节点名,避免名称中出现空格时请求路径失效。不同 Mihomo 版本对延迟测试接口的参数和返回字段可能有细微差异,如果你的版本不接受节点名路径形式,可以先通过浏览器开发者工具或客户端 Dashboard 的网络请求,确认它实际使用的 API 端点,再调整脚本。

脚本还需要防止「频繁抖动」。如果每次测试都选择当前最低延迟节点,网络稍有波动就会在两个节点之间来回切换,长连接、下载任务和登录会话都会受到影响。更合理的策略是加入冷却时间、连续失败次数和回切阈值:当前节点连续三次失败才切换;新节点必须连续两次通过检查才被接受;原节点恢复后,也只有在延迟明显低于当前节点时才回切。

第三步:用 Python 加入重试、冷却与状态记录

当节点数量较多,或者需要将结果写入监控系统时,Python 比纯 Shell 更适合维护。建议把配置放到环境变量或权限受控的文件中,不要将 secret 直接提交到 Git 仓库。脚本还应区分三种失败:API 请求失败代表本地服务异常;延迟测试失败代表候选节点不可用;切换请求失败则可能是组类型不支持手动选择,不能把三者都记录成「节点挂了」。

#!/usr/bin/env python3
import json
import os
import time
from pathlib import Path

import requests

API = os.getenv("MIHOMO_API", "http://127.0.0.1:9090").rstrip("/")
SECRET = os.environ["MIHOMO_SECRET"]
GROUP = os.getenv("MIHOMO_GROUP", "Proxy")
TEST_URL = os.getenv("MIHOMO_TEST_URL", "https://www.gstatic.com/generate_204")
TIMEOUT_MS = int(os.getenv("MIHOMO_TIMEOUT", "5000"))
MAX_LATENCY = int(os.getenv("MIHOMO_MAX_LATENCY", "900"))
COOLDOWN = int(os.getenv("MIHOMO_COOLDOWN", "300"))
STATE_FILE = Path(os.getenv("MIHOMO_STATE", "/tmp/mihomo-failover.json"))

session = requests.Session()
session.headers.update({"Authorization": f"Bearer {SECRET}"})

def get_proxies():
    response = session.get(f"{API}/proxies", timeout=8)
    response.raise_for_status()
    return response.json()["proxies"]

def measure(node):
    response = session.get(
        f"{API}/proxies/{requests.utils.quote(node, safe='')}/delay",
        params={"timeout": TIMEOUT_MS, "url": TEST_URL},
        timeout=10,
    )
    response.raise_for_status()
    return int(response.json().get("delay") or 999999)

def switch(node):
    group_path = requests.utils.quote(GROUP, safe="")
    response = session.put(
        f"{API}/proxies/{group_path}",
        json={"name": node},
        timeout=8,
    )
    response.raise_for_status()

def main():
    state = json.loads(STATE_FILE.read_text()) if STATE_FILE.exists() else {}
    now = time.time()
    if now - state.get("changed_at", 0) < COOLDOWN:
        print("cooldown active")
        return

    groups = get_proxies()
    group = groups.get(GROUP)
    if not group:
        raise RuntimeError(f"proxy group not found: {GROUP}")

    current = group.get("now")
    candidates = group.get("all", [])
    ranked = []

    for node in candidates:
        try:
            delay = measure(node)
            if delay <= MAX_LATENCY:
                ranked.append((delay, node))
        except requests.RequestException as error:
            print(f"probe failed: {node}: {error}")

    if not ranked:
        raise RuntimeError("no healthy node found")

    ranked.sort()
    best_delay, best_node = ranked[0]
    if best_node != current:
        switch(best_node)
        state = {
            "changed_at": now,
            "node": best_node,
            "delay": best_delay,
        }
        STATE_FILE.write_text(json.dumps(state))
        print(f"switched {current} -> {best_node}, {best_delay} ms")
    else:
        print(f"keep {current}, {best_delay} ms")

if __name__ == "__main__":
    main()

这个版本用状态文件实现最基本的冷却窗口,避免定时任务每分钟都切换。生产环境还可以继续增加连续失败计数,把每个节点最近几次探测结果保存为环形队列;也可以为不同业务设置不同测试地址。例如,服务器主要访问代码托管平台时,测试地址应接近实际业务域名,而不是只测一个全球可达的静态页面。延迟低并不等于业务一定可用,HTTP 状态码、TLS 握手、响应体大小和下载速度都可能影响最终体验。

不要把测速结果当成唯一真相:节点可能对测速网址返回很快,但对你的实际 API、Git 仓库或视频 CDN 不稳定。更可靠的做法是使用「通用探测 + 业务探测」两级策略,并在切换日志中记录测试 URL、状态码、延迟和最终选择,方便事后判断是节点问题还是目标服务限流。

第四步:定时任务、日志与长期运行规范

脚本写完后,最常见的失败不是 API 逻辑错误,而是定时环境与交互式终端不同。Cron 通常没有你的完整 PATH、代理变量和用户目录;systemd 则可能以不同用户运行,无法读取原本放在桌面客户端目录中的配置。因此部署前应使用绝对路径,明确写入环境变量,并确认运行账号有权创建日志和状态文件。

Linux 上可以先手动运行成功,再创建 Cron 条目。例如每五分钟执行一次:

*/5 * * * * /usr/bin/env MIHOMO_API=http://127.0.0.1:9090 /usr/local/bin/mihomo-failover.sh >> /var/log/mihomo-failover-cron.log 2>&1

如果使用 systemd,建议采用独立的环境文件,并让服务在 Mihomo 启动后再执行。环境文件应设置为仅管理员可读,权限可使用 chmod 600。对于桌面客户端,不建议同时运行多个自动切换脚本,否则它们可能互相覆盖选择结果,导致日志显示「刚切换成功,几秒后又被切回」。

日志至少应包含时间、策略组、旧节点、新节点、探测目标、延迟、失败原因和脚本退出码。可以把日志按天轮换,避免服务器磁盘被无休止的探测记录占满。若接入 Prometheus、Uptime Kuma 或其他监控工具,还可以将「当前节点」「最近切换时间」「连续失败次数」作为指标;当短时间内连续切换超过阈值时,发送告警,而不是让脚本静默地不断换节点。

现象 优先检查位置 处理思路
401 Unauthorized secret 与请求头 确认使用 Authorization: Bearer,并检查令牌是否有多余空格。
策略组找不到 /proxies 返回值 复制完整组名,注意图标、空格和大小写,不要使用界面上的翻译名猜测。
切换返回 400 组类型与节点成员 确认目标是可手动选择的组,且节点确实属于该组。
测速全是超时 测试 URL 与内核日志 更换符合业务的 HTTPS 地址,并检查 DNS、TLS、路由及节点本身。
节点来回跳转 冷却和阈值逻辑 增加连续失败条件、冷却时间与回切门槛,避免单次抖动触发切换。

在 Windows 上,计划任务也可以运行 PowerShell、Python 或 WSL 中的 Shell 脚本,但要特别留意「仅在用户登录时运行」与「无论用户是否登录都运行」的区别。若 Mihomo 由桌面客户端启动,计划任务可能先于内核启动,从而连续记录连接失败。可以设置延迟启动,或让脚本在启动时先循环请求 /version,等待 API 在限定时间内出现,再进入节点检查流程。

最后,自动切换应该是故障转移手段,而不是替代配置质量的万能开关。先保证 DNS、规则集、TUN 或系统代理路径稳定,再让脚本处理节点层面的异常;否则规则写错、端口冲突和本地防火墙问题都会被误判成「节点失效」。对日常电脑,建议先在客户端日志中人工确认一次切换结果;对服务器,则应保留手动回退命令,确保自动化出现误判时能迅速恢复。

相比一些只提供图形化测速按钮的同类工具,很多方案无法细致控制 API 鉴权、失败次数、冷却时间和日志格式,换到服务器或多实例环境后就容易失去可观察性;而单纯复制一段脚本又常常忽略策略组名称编码、权限保护与计划任务上下文。Clash 官网 的优势在于把 Mihomo API、Shell/Python 自动化、健康检查和排障顺序放在同一套文档体系里,既方便新手按步骤验证,也便于进阶用户继续接入监控和自定义规则。如果你准备先搭建一个可控的 Clash 运行环境,再实践本文的自动切换方案,可以前往下载适合你平台的客户端。