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

本文从 Clash API 的请求流程与策略组机制出发,提供可运行的自动切换脚本和 YAML 配置示例,帮助技术用户构建具备健康检查、故障转移、日志记录与安全鉴权能力的代理节点自动化运维方案。

Clash API 自动切换节点的核心,不是让脚本直接修改订阅文件,而是通过外部控制器(External Controller)读取当前内核状态,再向指定策略组发送切换请求。这样做不会破坏订阅缓存,也不会覆盖配置文件中的节点定义,适合在服务器、家庭网关或长期运行的桌面客户端上做健康检查与故障转移。

本文以 mihomo(Clash Meta)兼容的 REST API 为例,使用 Python 标准库编写一个可以直接运行的轮询脚本。脚本会完成鉴权、读取策略组、检查候选节点延迟、切换到健康节点、记录日志和避免频繁抖动。不同客户端的面板名称可能略有差异,但只要底层核心启用了 External Controller,接口路径基本一致。

先理解 Clash API:控制器、策略组与节点状态

Clash API 通常监听在本机的 127.0.0.1:9090,也可以由配置文件中的 external-controller 指定其他地址。它不是一个公开的云端接口,而是客户端本机上的管理端口。脚本连接 API 后,可以读取所有代理节点、策略组当前选择、延迟测试结果以及内核运行信息。

自动切换通常涉及三个对象。第一是 proxies,它包含节点和策略组的实时状态;第二是 proxy-groups,其中的 select 策略组允许手动选择节点,url-test 会根据延迟自动选择,fallback 则按可用性进行故障转移;第三是切换接口,向具体策略组发送 PUT 请求即可改变当前选项。

用途请求作用
读取内核版本GET /version确认 API 可以访问,并记录核心版本
读取全部代理GET /proxies查看节点、策略组及延迟信息
读取策略组GET /proxies/{group}取得候选项与当前选择
切换策略组PUT /proxies/{group}请求体为 {"name":"节点名"}
测试节点延迟GET /proxies/{node}/delay按 URL 与 timeout 测量单个节点

策略组名称和节点名称可能包含空格、斜杠、括号或非 ASCII 字符,因此拼接 URL 时必须使用 URL 编码,不能直接把名称连接到路径后面。脚本还应区分普通节点与策略组:策略组的名称可能出现在 proxies 结果中,但它不能作为上游节点直接进行延迟测试。

先保护 External Controller

External Controller 拥有切换节点、修改配置和读取运行状态的权限。不要把它绑定到 0.0.0.0 后裸露在公网,也不要把带有 secret 的完整 URL 写进公开脚本或日志。远程管理时应优先使用 SSH 隧道、内网防火墙或 VPN,再配合 API 密钥。

YAML 基础配置:开启安全的 API 与策略组

下面是一份适合测试和小型自动化环境的配置片段。节点部分使用示例地址和凭据,实际使用时应替换为订阅生成的节点;如果客户端通过订阅管理节点,不要手工复制整份订阅,只需在覆写配置或全局扩展配置中补充 API、策略组和规则。

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

# 只允许本机脚本访问控制器
external-controller: 127.0.0.1:9090
secret: "change-this-to-a-long-random-secret"

proxies:
  - name: "HK-01"
    type: ss
    server: 203.0.113.10
    port: 443
    cipher: aes-256-gcm
    password: "replace-me"

  - name: "JP-01"
    type: trojan
    server: 198.51.100.20
    port: 443
    password: "replace-me"
    sni: example.com

proxy-groups:
  - name: "自动切换"
    type: select
    proxies:
      - "HK-01"
      - "JP-01"
      - DIRECT

  - name: "自动测速"
    type: url-test
    proxies:
      - "HK-01"
      - "JP-01"
    url: "https://www.gstatic.com/generate_204"
    interval: 300
    tolerance: 50

rules:
  - MATCH,自动切换

select 组适合由外部脚本决定节点,脚本切换后状态清晰,便于人工接管;url-test 组适合完全依赖内核定期测速的场景;fallback 适合强调可用性而不是最低延迟的场景。不要让多个脚本同时控制同一个策略组,否则一个脚本可能刚切到低延迟节点,另一个脚本又依据旧数据切回原节点。

API 端口最好只监听回环地址。若确实需要让独立服务器调用,应把监听地址限制在管理网卡,配置防火墙白名单,并使用足够长的随机密钥。修改配置后要在客户端中重载配置或重启核心,再用本机请求确认服务已经生效。

可运行的 Python 自动切换脚本

以下脚本不依赖第三方库,适用于 Python 3.9 及以上版本。它通过 /proxies/{node}/delay 测试候选节点,选择低于阈值且延迟最低的节点;当所有节点都失败时保持当前选择,不会因为一次临时网络抖动把策略组切到不可用状态。脚本采用连续失败计数和切换冷却时间,减少频繁切换对长连接的影响。

#!/usr/bin/env python3
import json
import logging
import os
import time
from urllib.error import HTTPError, URLError
from urllib.parse import quote, urlencode
from urllib.request import Request, urlopen

API = os.getenv("CLASH_API", "http://127.0.0.1:9090")
SECRET = os.getenv("CLASH_SECRET", "")
GROUP = os.getenv("CLASH_GROUP", "自动切换")
TEST_URL = os.getenv("CLASH_TEST_URL",
                     "https://www.gstatic.com/generate_204")
TIMEOUT_MS = int(os.getenv("CLASH_TIMEOUT_MS", "1800"))
INTERVAL = int(os.getenv("CLASH_INTERVAL", "60"))
COOLDOWN = int(os.getenv("CLASH_COOLDOWN", "300"))
MAX_FAILS = int(os.getenv("CLASH_MAX_FAILS", "2"))

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s %(levelname)s %(message)s"
)
last_switch = 0.0
fail_count = 0

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

def get_delay(node):
    path = "/proxies/" + quote(node, safe="")
    result = api("GET", path + "/delay", {
        "url": TEST_URL,
        "timeout": str(TIMEOUT_MS)
    })
    return int(result["delay"])

def choose_node():
    group = api("GET", "/proxies/" + quote(GROUP, safe=""))
    current = group.get("now", "")
    candidates = []
    for name in group.get("all", []):
        if name in ("DIRECT", "REJECT", current):
            continue
        try:
            delay = get_delay(name)
            logging.info("group=%s node=%s delay=%dms",
                         GROUP, name, delay)
            if delay <= TIMEOUT_MS:
                candidates.append((delay, name))
        except (HTTPError, URLError, KeyError, ValueError) as exc:
            logging.warning("node=%s test failed: %s", name, exc)
    if not candidates:
        return current, current
    candidates.sort()
    return current, candidates[0][1]

def switch_to(node):
    path = "/proxies/" + quote(GROUP, safe="")
    api("PUT", path, body={"name": node})
    logging.warning("switched group=%s to node=%s", GROUP, node)

def check_once():
    global last_switch, fail_count
    current, best = choose_node()
    if not best:
        logging.warning("group has no current selection")
        return
    if best == current:
        fail_count = 0
        logging.info("keep node=%s", current)
        return
    fail_count += 1
    logging.warning("candidate=%s current=%s failure-count=%d",
                    best, current, fail_count)
    if fail_count < MAX_FAILS:
        return
    if time.time() - last_switch < COOLDOWN:
        logging.info("switch suppressed by cooldown")
        return
    switch_to(best)
    last_switch = time.time()
    fail_count = 0

if __name__ == "__main__":
    logging.info("starting Clash API watcher for group=%s", GROUP)
    api("GET", "/version")
    while True:
        try:
            check_once()
        except (HTTPError, URLError, TimeoutError, ValueError) as exc:
            logging.error("API check failed: %s", exc)
        time.sleep(INTERVAL)

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

export CLASH_API="http://127.0.0.1:9090"
export CLASH_SECRET="change-this-to-a-long-random-secret"
export CLASH_GROUP="自动切换"
python3 clash-watcher.py

Windows PowerShell 的写法是 $env:CLASH_SECRET="...",然后执行 python .\clash-watcher.py。首次运行时先观察日志,不要立刻把它设置为系统服务。确认策略组名称、节点名称和测试地址都正确后,再调整轮询间隔。延迟测试会消耗请求和节点资源,节点较多时不建议把间隔设为几秒。

健康检查策略:延迟不是唯一指标

单次延迟最低的节点不一定是最适合长期使用的节点。网络质量包含连接成功率、响应时间、丢包、带宽和持续稳定性。脚本至少应设置超时、连续失败次数和冷却时间:超时防止某个节点阻塞整个轮询;连续失败避免一次偶发丢包触发切换;冷却时间避免多个节点在边界状态下来回跳转。

  • 测试地址要稳定。优先使用能快速返回状态码的 HTTPS 地址,避免把大文件下载或需要登录的页面作为探针。测试地址不可访问时,所有节点都会被误判为故障。
  • 阈值要按线路设置。跨地区线路的正常延迟可能高于本地线路,固定使用 100 毫秒会导致大量误切换。可以按历史日志设置 800 至 2000 毫秒的初始范围,再根据实际结果调整。
  • 保留人工选择入口。自动化策略组之外,建议保留一个手动 select 组,让用户在维护、流媒体地区或特殊业务场景下临时锁定节点。
  • 避免切断长连接。切换节点会影响 WebSocket、下载任务和登录会话。工作时间可以提高连续失败次数,夜间或网关场景则可以缩短故障恢复时间。
  • 记录切换原因。至少保存时间、旧节点、新节点、测试延迟和异常信息。只有日志完整,才能判断是节点故障、探针故障还是脚本参数不合理。

如果目标只是让内核自动选择延迟较低的节点,可以优先使用 YAML 原生的 url-test,不必额外部署脚本。脚本更适合需要自定义阈值、发送通知、跨多个策略组联动,或者在切换前后执行额外操作的环境。

关于 fallback 与 url-test

fallback 主要回答「当前节点不可用时选谁」,url-test 主要回答「哪些节点延迟更低」。它们都由 mihomo 内核负责维护状态,配置简单且不容易出现外部脚本与内核竞争。只有原生策略组无法满足业务条件时,才建议引入自定义轮询脚本。

鉴权、日志与生产部署注意事项

API 自动化脚本本身也是一个管理客户端,生产部署时应当按最小权限和最小暴露面处理。虽然 Clash API 通常只有一个全局密钥,没有细粒度的只读账号,但仍可以通过网络边界降低风险。

  1. external-controller 绑定到 127.0.0.1,脚本和 Clash 在同一台机器运行。
  2. 若必须跨机器访问,使用 SSH 本地端口转发,例如把远端的 9090 映射到管理机本地,再让脚本连接本地地址。
  3. 限制脚本配置文件和环境变量的读取权限,避免普通用户或共享日志系统读取 CLASH_SECRET
  4. 日志中只记录节点显示名和延迟,不输出 Authorization 请求头、订阅链接、节点密码或完整 API URL。
  5. 为脚本设置进程守护和退出重启策略,但不要设置过短的重启间隔,以免 API 异常时形成高频请求。

可以先用 curl 验证 API 和鉴权是否正常。请求头中的密钥不要带多余空格,策略组名称需要进行 URL 编码:

curl -H "Authorization: Bearer change-this-to-a-long-random-secret" \
  http://127.0.0.1:9090/version

curl -H "Authorization: Bearer change-this-to-a-long-random-secret" \
  "http://127.0.0.1:9090/proxies/%E8%87%AA%E5%8A%A8%E5%88%87%E6%8D%A2"

curl -X PUT \
  -H "Authorization: Bearer change-this-to-a-long-random-secret" \
  -H "Content-Type: application/json" \
  -d '{"name":"HK-01"}' \
  "http://127.0.0.1:9090/proxies/%E8%87%AA%E5%8A%A8%E5%88%87%E6%8D%A2"

如果返回 401,先检查 secret 与 Bearer 头是否一致;返回 404,重点检查 API 地址、策略组名称和核心是否真的支持该接口;返回 400,通常是请求体字段错误,或者目标节点不在该策略组的 all 列表中。切换成功后,用 GET /proxies/{group} 查看 now 字段确认状态,而不是只根据 PUT 请求没有报错就认定成功。

不要把订阅链接当普通配置公开

订阅地址、节点密码和 API 密钥都属于敏感凭据。示例中的域名、IP、密码仅用于说明字段结构,实际部署应使用服务商下发的真实配置并妥善保存。脚本仓库、工单系统和错误日志中不要粘贴完整订阅 URL。

最后,建议先在非关键设备上运行一段时间,观察节点切换频率、失败原因和网络恢复时间,再将参数复制到家庭网关或办公环境。合理的自动化不是切换次数越多越好,而是在确实发生故障时快速恢复,同时让正常连接尽量保持稳定。

DOWNLINK READY

下载 Clash 客户端

选择支持 mihomo 内核和 External Controller 的客户端,再结合本文的 API 配置与自动化脚本进行节点管理。

DOWNLINK READY

下载 Clash 客户端

覆盖 Windows、macOS、Linux、Android 与 iOS 的 Clash 客户端下载入口,含 mihomo 内核与图形界面版本。

Clash最新版下载