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 对象中的目标策略组。策略组通常包含 all、now、type 等字段:all 是可选节点列表,now 是当前选择。不同客户端的界面名称可能不同,但 API 读取的是内核配置中的真实策略组名称。
控制 API 不只是查询接口,还能切换代理、关闭连接,部分版本还提供配置重载等管理能力。不要使用 0.0.0.0:9090 作为默认方案,也不要把 secret 写进公开代码仓库、聊天记录或定时任务的可读命令行参数中。确实需要跨机器管理时,应先使用防火墙、SSH 隧道或内网访问控制收窄来源。
认证头与节点名称编码
启用了 secret 后,请求头使用 Authorization: Bearer secret值。脚本访问带有空格、斜杠、井号或中文的策略组名称时,不能直接把名称拼在 URL 中,应使用 URL 编码。比如策略组名为 自动选择,实际请求路径需要把它编码后再发送。
另一个需要注意的边界是:读取节点列表和执行切换必须针对策略组,而不是直接针对单个节点。先从 /proxies 找到组对象,再读取其 all 列表;切换时把组名放在路径中,把节点名放在 JSON 请求体中。
延迟测试不能只看一个数字
延迟测试接口需要提供目标 URL 和超时时间。常见做法是测试一个稳定、响应体较小的 HTTPS 地址,例如站点的健康检查地址,而不是测试一个会跳转、需要登录或经常改变响应状态的网页。timeout 单位通常是毫秒,设置为 5000 或 8000 比设置极短的 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 时,应把该节点标记为本轮不可用,但不要立刻从本地配置删除。节点可能只是当前线路抖动,下一轮测试仍有机会恢复。
过滤不可测试的成员
跳过 DIRECT、REJECT 以及嵌套策略组,只对真实代理节点发起延迟请求。否则脚本可能把策略组名称再次当成节点测试,得到无法解释的错误。
设置单节点超时
每个节点使用 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 状态和系统代理端口。若出现「脚本显示已切换,但浏览器仍使用旧节点」,优先检查浏览器是否直连、目标流量是否命中另一个策略组,以及客户端是否在订阅更新后重载了不同的配置文件。