Clash API自动切换节点脚本:进阶配置与工程实践

面向开发者与运维人员的 Clash API 进阶教程。通过 external-controller、节点延迟测试和自动化脚本构建可恢复的故障切换机制,让代理策略组能够根据网络质量自动选择可用节点。

先分清 API 控制、延迟测试与策略组

Clash API 自动切换节点,核心不是让脚本直接接管所有流量,而是通过 external-controller 读取当前配置和策略组状态,再对候选节点发起延迟测试,最后调用选择接口修改策略组当前使用的代理。客户端仍然负责启动 mihomo 内核、加载配置和维持系统代理,脚本只负责「观察—判断—切换」这一小段控制逻辑。

一个完整的切换流程通常包含四个环节:先确认控制端口可访问;再读取目标策略组下的节点列表;接着逐个测试节点到指定 URL 的连通性;最后把延迟最低且满足阈值的节点写回策略组。任何一个环节失败,都应该保留当前节点,而不是因为一次短暂超时就立即切换。

组件作用常见误区
external-controller 提供 HTTP API,读取运行状态并修改策略组 监听在公网地址,或忘记配置认证密钥
/proxies 获取代理、策略组及其当前状态 把策略组名称和节点名称混用
/delay 通过指定节点测试目标 URL 的响应时间 把一次 timeout 当成节点永久失效
PUT /proxies/{group} 修改指定策略组当前选中的代理 节点名包含特殊字符时没有进行 URL 编码

自动切换适合处理「某个策略组中的节点质量波动」和「无人值守环境下的基本故障恢复」,例如家庭服务器、远程开发机或定时任务。它不能替代订阅更新,也不能修复节点本身已经过期、认证失败或服务端下线的问题。如果候选列表全部失败,最安全的动作是记录日志并继续使用当前选择,避免脚本在多个坏节点之间循环跳转。

配置 external-controller 并限制访问范围

在配置文件中启用控制接口时,优先只监听本机回环地址。桌面端和同一台机器上的脚本使用 127.0.0.1 即可,不需要把 API 暴露给局域网。mihomo 的控制接口通常配置在 127.0.0.1:9090,并通过 secret 对请求进行认证。

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

mode: rule
log-level: info
mixed-port: 7890

proxy-groups:
  - name: Auto
    type: select
    proxies:
      - node-a
      - node-b
      - DIRECT

修改配置后要重新载入配置或重启内核,再用接口确认实际生效值。仅仅编辑磁盘上的 config.yaml,不代表已经运行的内核会立即读取新内容。

curl -s -H "Authorization: Bearer change-this-to-a-long-random-string" http://127.0.0.1:9090/proxies

接口返回 JSON 后,重点查看顶层 proxies 对象中的目标策略组。策略组通常包含 allnowtype 等字段:all 是可选节点列表,now 是当前选择。不同客户端的界面名称可能不同,但 API 读取的是内核配置中的真实策略组名称。

不要把控制端口暴露到公网

控制 API 不只是查询接口,还能切换代理、关闭连接,部分版本还提供配置重载等管理能力。不要使用 0.0.0.0:9090 作为默认方案,也不要把 secret 写进公开代码仓库、聊天记录或定时任务的可读命令行参数中。确实需要跨机器管理时,应先使用防火墙、SSH 隧道或内网访问控制收窄来源。

认证头与节点名称编码

启用了 secret 后,请求头使用 Authorization: Bearer secret值。脚本访问带有空格、斜杠、井号或中文的策略组名称时,不能直接把名称拼在 URL 中,应使用 URL 编码。比如策略组名为 自动选择,实际请求路径需要把它编码后再发送。

另一个需要注意的边界是:读取节点列表和执行切换必须针对策略组,而不是直接针对单个节点。先从 /proxies 找到组对象,再读取其 all 列表;切换时把组名放在路径中,把节点名放在 JSON 请求体中。

延迟测试不能只看一个数字

延迟测试接口需要提供目标 URL 和超时时间。常见做法是测试一个稳定、响应体较小的 HTTPS 地址,例如站点的健康检查地址,而不是测试一个会跳转、需要登录或经常改变响应状态的网页。timeout 单位通常是毫秒,设置为 50008000 比设置极短的 500 毫秒更适合普通公网链路。

# 把 Node-A 替换为实际节点名称
curl -s -G \
  -H "Authorization: Bearer change-this-to-a-long-random-string" \
  --data-urlencode "url=https://www.gstatic.com/generate_204" \
  --data-urlencode "timeout=5000" \
  "http://127.0.0.1:9090/proxies/Node-A/delay"

返回结果通常包含 delay 字段,单位为毫秒。请求失败、返回错误或没有有效的 delay 时,应把该节点标记为本轮不可用,但不要立刻从本地配置删除。节点可能只是当前线路抖动,下一轮测试仍有机会恢复。

过滤不可测试的成员

跳过 DIRECTREJECT 以及嵌套策略组,只对真实代理节点发起延迟请求。否则脚本可能把策略组名称再次当成节点测试,得到无法解释的错误。

设置单节点超时

每个节点使用 5000 毫秒左右的超时,并在脚本层设置请求异常捕获。不要让一个失效节点阻塞整轮检测,候选较多时可以进一步限制总运行时间。

使用阈值和迟滞

只有延迟低于阈值且明显优于当前节点时才切换,例如新节点至少快 100 毫秒,或当前节点连续两轮失败。这样可以避免两个相近节点之间来回抖动。

保留失败记录

记录时间、策略组、节点名、延迟和异常原因。日志要隐藏 secret,并避免把完整订阅链接写入文件。

实际选择时不要只按最低延迟排序。延迟最低的节点可能存在间歇性丢包,或者只对测试地址表现良好。可以采用「连续失败优先切换、成功节点按延迟排序、切换后至少保持一段时间」的策略。对于定时任务,检测间隔可从 60 秒到 300 秒开始,过短会增加 API 请求和节点探测压力。

用 Python 编写带保护条件的切换脚本

下面的示例只依赖 Python 标准库,目标策略组为 Auto。脚本每轮读取策略组成员,跳过直连和嵌套组,测试候选节点后选择延迟最低者。为了避免频繁切换,只有当前节点失败,或新节点比当前节点快出 100 毫秒以上时才执行 PUT 请求。

#!/usr/bin/env python3
import json
import os
import time
import urllib.parse
import urllib.request
import urllib.error

API = os.getenv("CLASH_API", "http://127.0.0.1:9090")
SECRET = os.environ["CLASH_SECRET"]
GROUP = os.getenv("CLASH_GROUP", "Auto")
TEST_URL = os.getenv("TEST_URL", "https://www.gstatic.com/generate_204")
TIMEOUT_MS = 5000
SWITCH_MARGIN_MS = 100
INTERVAL = 180

def request(method, path, payload=None, query=None):
    url = API.rstrip("/") + path
    if query:
        url += "?" + urllib.parse.urlencode(query)
    body = None
    headers = {
        "Authorization": "Bearer " + SECRET,
        "Accept": "application/json"
    }
    if payload is not None:
        body = json.dumps(payload).encode("utf-8")
        headers["Content-Type"] = "application/json"
    req = urllib.request.Request(url, data=body, headers=headers, method=method)
    with urllib.request.urlopen(req, timeout=8) as response:
        return json.loads(response.read().decode("utf-8"))

def get_group():
    data = request("GET", "/proxies")
    group = data["proxies"][GROUP]
    return group

def test_node(name):
    path = "/proxies/" + urllib.parse.quote(name, safe="")
    result = request("GET", path + "/delay", query={
        "url": TEST_URL,
        "timeout": TIMEOUT_MS
    })
    delay = result.get("delay")
    return int(delay) if delay is not None else None

def select_node(name):
    path = "/proxies/" + urllib.parse.quote(GROUP, safe="")
    request("PUT", path, {"name": name})

def run_once():
    group = get_group()
    current = group.get("now")
    candidates = []

    for name in group.get("all", []):
        if name in ("DIRECT", "REJECT") or name == GROUP:
            continue
        try:
            delay = test_node(name)
            if delay is not None:
                candidates.append((delay, name))
                print(f"{name}: {delay} ms")
        except (urllib.error.HTTPError, urllib.error.URLError,
                TimeoutError, ValueError, KeyError) as exc:
            print(f"{name}: failed ({exc})")

    if not candidates:
        print("no usable candidate; keep current:", current)
        return

    candidates.sort()
    best_delay, best_name = candidates[0]
    current_delay = None

    if current:
        try:
            current_delay = test_node(current)
        except Exception as exc:
            print(f"current node failed: {exc}")

    should_switch = (
        current != best_name and
        (current_delay is None or best_delay + SWITCH_MARGIN_MS < current_delay)
    )

    if should_switch:
        select_node(best_name)
        print(f"switched: {current} -> {best_name} ({best_delay} ms)")
    else:
        print(f"keep: {current}; best={best_name} ({best_delay} ms)")

if __name__ == "__main__":
    while True:
        try:
            run_once()
        except Exception as exc:
            print("round failed:", exc)
        time.sleep(INTERVAL)

运行前不要把密钥直接写进脚本,可以通过环境变量提供。Linux 或 macOS 中可以这样启动:

export CLASH_API="http://127.0.0.1:9090"
export CLASH_SECRET="change-this-to-a-long-random-string"
export CLASH_GROUP="Auto"
python3 clash_switch.py

这个示例把当前节点也测试一次,因此一轮最多会多发起一个请求。对于节点数量较多的配置,可以把当前节点的测试结果缓存到本轮,或者使用线程池并发测试,但并发数建议限制在 4 到 8 个,避免短时间内向控制接口和远端节点发起大量连接。

定时运行时加入状态、冷却与回滚

脚本能完成一次切换,不等于已经具备可靠的无人值守能力。工程实践中至少要加入连续失败计数、切换冷却时间和配置变化检测。连续失败计数可以防止一次网络抖动触发切换;冷却时间可以让新节点有机会稳定下来;配置变化检测则能应对订阅更新后节点列表被替换的情况。

保护机制建议做法解决的问题
连续失败 当前节点连续 2~3 轮失败后才切换 避免单次超时造成误切换
切换冷却 切换后至少等待 180~300 秒 避免节点之间反复来回跳转
最低质量阈值 延迟超过 3000~5000 毫秒的节点不选 避免选择虽然成功但实际不可用的慢节点
无候选时保持原状 所有测试失败时不执行 PUT 避免把策略组切到未知状态
日志轮转 记录摘要并限制文件大小 避免长期运行占满磁盘

如果使用 systemd,可以让脚本作为服务启动,并在服务异常退出后自动重启。不要用过短的重启间隔,否则 API 尚未恢复时会不断创建进程。Windows 可以使用任务计划程序按固定间隔运行单次脚本;这种方式比让多个常驻副本同时运行更安全,但要确保任务设置为「上一个实例仍在运行时不启动新实例」。

先做只读演练,再开放切换

上线前先把 select_node(best_name) 改成日志输出,只观察一到两天的延迟、失败率和候选排序。确认策略组名称、节点过滤条件、测试 URL 与阈值都符合预期后,再恢复 PUT 请求。首次启用时建议只绑定一个测试策略组,不要直接控制所有流量。

最后还要区分 API 正常和代理可用。/proxies 能返回数据,只能证明本地内核的控制接口可访问;延迟测试成功,只能证明测试 URL 经过该节点获得了响应。真正上线后仍应观察应用日志、DNS 请求、TUN 状态和系统代理端口。若出现「脚本显示已切换,但浏览器仍使用旧节点」,优先检查浏览器是否直连、目标流量是否命中另一个策略组,以及客户端是否在订阅更新后重载了不同的配置文件。

排查之前先确认客户端与内核版本

本文涉及的 proxy-server-nameserverunified-delaytcp-concurrent 三个字段需要 mihomo 内核支持。原版 Clash 内核已停止维护,字段不生效时先确认内核类型,再对照日志逐项排查。

前往下载页 查看安装教程

下载 Clash 客户端