Clash API 自动切换节点的工作原理与适用场景
在 Clash、Clash Verge Rev、Mihomo Party 等客户端中,节点自动切换并不只是把策略组类型改成 url-test 这么简单。Clash external-controller 提供了一套本地 HTTP API,脚本可以通过它读取当前配置、查询代理列表、发起延迟测试、切换策略组成员,甚至在必要时重新加载配置。把这些接口串起来,就能形成一条可观测的自动化链路:先取得候选节点,再逐个检测可用性与延迟,按照阈值过滤异常节点,最后把最合适的节点写入目标策略组。
这种方案适合几类实际需求。第一,你的订阅里节点很多,但内置测速规则无法表达「延迟低于 300 毫秒、连续成功两次、同时排除某些地区」这样的条件。第二,你需要在家庭服务器、软路由或无人值守电脑上定时维护节点,不方便每天打开客户端手动切换。第三,某些业务需要固定策略组名称,例如下载、开发工具和流媒体分别使用不同的出口,而你又希望每个组内部自动避开失效节点。脚本可以把这些业务判断写成明确规则,比凭界面状态猜测节点是否正常更容易审计。
需要先区分三种不同机制。url-test 由内核按配置周期测试并选择较快成员,优点是零代码、维护成本低;fallback 更强调主备顺序,只要首选不可用就尝试下一个;而 API 脚本适合加入自定义逻辑,例如根据节点名称过滤倍率、保存上一次成功节点、在不同时间段使用不同策略组,或在连续失败后执行告警。脚本不是所有用户都必须采用的高级装饰,它的价值在于把内置策略组无法表达的业务条件变成可重复执行的流程。
url-test;如果你需要跨多个策略组、保存检测结果、执行异常回退或接入定时任务,再考虑使用 external-controller API。
API 的基本地址通常是 http://127.0.0.1:9090,但端口完全取决于你的 YAML 配置。控制器监听在本机回环地址时,外部设备无法直接访问,安全边界相对清晰;如果写成 0.0.0.0:9090,局域网中其他设备也可能连接,必须同时设置强认证密钥并配合防火墙限制。Mihomo 常见配置如下,端口和密钥请替换成自己的值:
external-controller: 127.0.0.1:9090
secret: "replace-with-a-long-random-token"
proxy-groups:
- name: AUTO_WORK
type: select
proxies:
- node-a
- node-b
- DIRECT
external-controller 只决定 API 从哪里监听,secret 则用于验证请求。不要把密钥写成账号名、生日或简单的 123456,也不要将包含密钥的脚本提交到公开仓库。若客户端界面提供控制器设置,保存配置后应重新启动内核,随后再用本机请求测试控制器是否响应。API 返回 401 Unauthorized 通常说明认证头缺失或密钥不一致;返回连接拒绝则优先检查端口、核心状态与监听地址。
读取代理、检测延迟与更新策略组
自动切换流程至少会用到三个 API 能力。第一是读取代理资源,常见接口为 GET /proxies,返回当前内核已加载的代理、策略组和直连选项。第二是对指定代理发起延迟测试,例如 GET /proxies/{name}/delay,并提供 url 与 timeout 参数。第三是更新策略组当前选择,通常向 PUT /proxies/{group} 发送 JSON 数据,例如 {"name":"node-a"}。不同 Clash 分支、核心版本和客户端包装层可能存在字段差异,实际使用前应以当前核心返回的状态码和响应体为准。
延迟测试地址要尽量稳定、轻量,并且与目标业务有一定相关性。使用一个永远可访问但与实际服务完全无关的站点,只能说明节点可以访问该站点,不能证明它适合视频、代码仓库或 API 长连接。脚本通常会选择一个 HTTPS 地址,例如 https://www.gstatic.com/generate_204,但企业网络、地区策略或 DNS 分流可能让它并不适合你的环境。更稳妥的做法是先用连接日志观察目标业务,分别选取一个稳定探针和一个业务探针,并将超时、证书失败、HTTP 非预期状态都视为检测失败。
策略组名称和节点名称是自动化中最容易出错的部分。策略组必须是当前配置真实存在的组,节点名称必须与 API 返回的对象键完全一致,包含空格、括号、地区后缀和表情符号。不要直接假定订阅一定会生成 PROXY 或 AUTO,因为不同服务商的命名方式可能是「自动选择」「节点选择」或带有图标的自定义名称。建议脚本启动时先读取 /proxies,打印目标组的实际候选列表;如果目标组不存在,脚本应立即退出,而不是盲目向错误 URL 发起更新。
API 认证、URL 编码与请求错误处理
认证请求通常在 HTTP 头中加入 Authorization: Bearer 密钥。节点名称可能含有空格、斜杠或非 ASCII 字符,拼接路径时必须进行 URL 编码,否则某些名称会被服务器解析成错误路径。更新策略组时则建议使用 JSON 请求体,并明确设置 Content-Type: application/json。脚本不应把任何 2xx 响应都当成成功,还需要解析响应内容,并在日志中记录目标组、选中节点、延迟数值和时间戳,方便日后确认到底是检测成功还是切换成功。
下面的 Python 示例使用标准库,不依赖第三方包。它会读取指定策略组的候选成员,过滤 DIRECT、其他策略组和名称黑名单,通过 API 检测延迟,选择低于阈值且延迟最低的节点,然后更新策略组。示例中的策略组名称、探针地址、端口和密钥都应按实际环境修改:
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_WORK")
TEST_URL = os.getenv(
"CLASH_TEST_URL",
"https://www.gstatic.com/generate_204"
)
TIMEOUT_MS = int(os.getenv("CLASH_TIMEOUT_MS", "5000"))
MAX_DELAY_MS = int(os.getenv("CLASH_MAX_DELAY_MS", "800"))
HEADERS = {
"Authorization": "Bearer " + SECRET,
"Accept": "application/json",
}
def request_json(path, method="GET", payload=None):
url = API.rstrip("/") + path
data = None
headers = dict(HEADERS)
if payload is not None:
data = json.dumps(payload).encode("utf-8")
headers["Content-Type"] = "application/json"
request = urllib.request.Request(
url, data=data, headers=headers, method=method
)
with urllib.request.urlopen(request, timeout=10) as response:
body = response.read().decode("utf-8")
return response.status, json.loads(body) if body else {}
def main():
_, proxies = request_json("/proxies")
groups = proxies.get("proxies", {})
group_info = groups.get(GROUP)
if not isinstance(group_info, dict):
raise RuntimeError("strategy group not found: " + GROUP)
candidates = group_info.get("all", [])
proxy_names = set(groups.keys())
results = []
for name in candidates:
if name in {"DIRECT", "REJECT"}:
continue
if name not in proxy_names:
continue
if any(word in name.lower() for word in ("test", "expired")):
continue
encoded = urllib.parse.quote(name, safe="")
query = (
"?url=" + urllib.parse.quote(TEST_URL, safe="")
+ "&timeout=" + str(TIMEOUT_MS)
)
try:
_, result = request_json(
"/proxies/" + encoded + "/delay" + query
)
delay = int(result.get("delay", 999999))
if delay <= MAX_DELAY_MS:
results.append((delay, name))
print(name, delay, "ms")
except (urllib.error.HTTPError, urllib.error.URLError,
ValueError, TimeoutError) as error:
print(name, "failed:", error)
if not results:
raise RuntimeError("no healthy proxy matched the threshold")
results.sort(key=lambda item: item[0])
delay, selected = results[0]
request_json(
"/proxies/" + urllib.parse.quote(GROUP, safe=""),
method="PUT",
payload={"name": selected},
)
print("selected:", selected, delay, "ms")
if __name__ == "__main__":
main()
这个示例有意保持保守:检测失败的节点不会立刻从配置中删除,只有检测成功并低于阈值的节点才进入排序。这样做能避免临时 DNS 抖动或一次网络丢包导致节点永久消失。实际部署时可以把结果保存到本地 JSON 文件,记录每个节点最近几次的成功次数、平均延迟和最后失败时间,再采用「连续两次成功才启用、连续三次失败才降级」的策略,避免单次测量结果造成频繁切换。
定时执行、稳定性控制与异常回退
自动切换最忌讳频繁抖动。假设两个节点的延迟分别是 210 毫秒和 220 毫秒,脚本每分钟运行一次,那么轻微的网络波动就可能让策略组不断来回切换。除了设置最低延迟阈值,还应增加切换滞后:只有新节点比当前节点快出一定幅度,例如 80 毫秒,或者当前节点连续检测失败,才执行切换。对于需要登录状态、长连接或上传任务的应用,过于频繁的切换可能比延迟偏高更影响体验。
建议将检测周期与业务类型匹配。普通桌面使用可以每 10 至 30 分钟运行一次;家庭服务器可以每 5 分钟检测一次,但应避免同时对几十个节点发起请求;高频任务则应采用缓存结果,而不是每次业务启动都完整扫描。节点数量较多时,可以先依据名称、地区、协议或订阅标签筛选,再检测剩余候选。还可以把并发检测限制在 3 至 5 个请求,既缩短总耗时,也避免本地控制器和远端节点被瞬间打满。
Linux、macOS 与 Windows 的定时任务
在 Linux 或 macOS 上,推荐使用专用虚拟环境之外的标准 Python 解释器,并通过环境变量提供密钥。不要把密钥直接写进全局命令历史或公开的 crontab 备份。一个简单的运行方式是先创建权限为仅当前用户可读的环境文件,再让定时任务加载它:
CLASH_API=http://127.0.0.1:9090
CLASH_SECRET=replace-with-a-long-random-token
CLASH_GROUP=AUTO_WORK
CLASH_MAX_DELAY_MS=800
使用 cron 时,注意它的工作目录、PATH 和图形会话环境通常与终端不同。命令应使用 Python 的绝对路径,脚本输出重定向到专用日志文件,并在运行前确认 Clash 核心已经启动。如果电脑刚开机时定时任务先于客户端执行,API 会返回连接失败;可以在脚本外层增加重试,也可以让任务延迟几分钟启动。macOS 用户还要留意应用的沙盒、登录项和系统休眠行为,电脑睡眠期间任务不一定会按计划执行。
Windows 可以使用任务计划程序,在「启动程序」中指定 python.exe 和脚本路径,并把起始目录设置为脚本所在文件夹。触发器可选择登录时运行或按固定间隔运行;如果启用了「无论用户是否登录都运行」,应重新检查环境变量和文件权限,因为任务可能使用不同的用户上下文。任务失败后不要自动无限重试,建议设置有限次数,并让脚本在日志中区分 API 不可达、所有节点失败和更新策略组失败三类错误。
当前节点保护、备用节点与恢复策略
健康检查没有合格结果时,脚本不能简单地把策略组改成列表第一项。更安全的处理顺序是:先读取策略组当前选择;如果当前节点仍能通过一个较宽松的探针,就保持不动;如果当前节点确认失效,再查找上一次成功记录;最后才考虑使用一个明确配置的备用节点。备用节点最好来自不同线路或不同地区,否则主节点和备用节点可能共享同一故障点。
还可以把「自动组」和「手动组」分开。脚本只更新 AUTO_WORK,而用户临时选中的 MANUAL_WORK 不受影响;规则层面再决定开发域名使用哪个策略组。这样既保留自动化能力,又不会因为定时任务突然覆盖用户正在进行的手动测试。若多个脚本负责不同业务组,必须明确每个脚本的锁文件或执行间隔,防止它们同时更新同一策略组,产生难以复现的竞态。
安全部署、日志审计与常见故障排查
external-controller 本质上是一个可以操作运行中内核的管理接口,不能把它当成普通测速端口。最安全的默认值是只监听 127.0.0.1,脚本也在同一台设备上运行。如果必须从局域网管理,应使用防火墙只允许指定管理主机访问,并设置足够长的随机密钥;不要为了方便直接暴露到公网,也不要把控制器端口映射到路由器 WAN。API 密钥应通过环境变量、系统密钥环或权限受限的配置文件读取,日志中不得打印完整的 Authorization 头。
日志要记录足够的信息,但不能泄露订阅链接、认证密钥和完整用户配置。推荐记录执行时间、目标策略组、候选数量、成功数量、被选节点名称、延迟和最终 HTTP 状态;节点名称如果包含账号、邮箱或专属标识,可在日志中做脱敏。保留最近 7 至 30 天的日志通常足够定位问题,长期运行的服务器应设置轮转,避免脚本日志把磁盘写满。若脚本需要通过远程 SSH 执行,也应把控制器继续限制在代理主机本地,通过 SSH 隧道访问,而不是修改监听地址来迁就远程操作。
当 API 返回 401 时,检查密钥是否属于当前运行核心,以及请求头是否确实使用了 Bearer 前缀;当返回 404 时,重点核对 URL 编码后的策略组或节点名称;当返回 400 时,查看请求体字段是否符合当前内核版本要求。若 /proxies 能读取但延迟接口全部失败,问题可能来自探针地址、DNS、证书校验或节点本身,而不是认证。若延迟测试成功但 PUT 更新失败,则要确认目标名称是策略组而非普通节点,并检查当前策略组是否允许选择该成员。
另一个常见现象是脚本显示「已切换」,但界面仍显示旧节点。此时先重新读取 /proxies 验证内核内存中的当前选择,再判断是否是客户端界面缓存或配置被订阅更新覆盖。订阅自动更新可能重建策略组,导致脚本原先使用的组名或成员列表失效;因此脚本启动时应重新发现组结构,而不是永久缓存节点名称。对于被远程配置完全接管的环境,更稳妥的方案是使用客户端支持的覆写、代理提供者或本地补丁机制,把自动化目标组稳定地保留下来。
还应为脚本增加单实例锁,避免系统网络恢复、定时任务重试和用户手动运行在同一时间并发执行。Linux 可以使用锁文件或 flock,Windows 则可以使用命名互斥体或简单的进程检查;无论采用哪种方式,都要在异常退出后清理过期锁。上线前建议先使用「只检测、不切换」模式运行一到两天,观察延迟分布、失败比例与候选名称,再开启 PUT 更新。这样可以把 YAML、认证和 API 路径问题在不会影响日常流量的情况下提前暴露。
与只依赖图形客户端手动点选相比,Clash for Windows 的旧版策略界面在无人值守场景下缺少细粒度阈值和审计记录,单纯使用固定 url-test 又难以处理黑名单、连续失败和业务分组;而 Clash Verge Rev 或 Mihomo Party 虽然提供更完整的内核能力,仍需要用户自己设计安全的控制器访问边界。Clash V.CORE 可以把 API 自动化、策略组管理、节点检测与日志排障放在同一套稳定的内核工作流中,减少反复手动切换带来的误判;如果你已经明确了策略组和安全要求,可以前往前往下载,再按本文的脚本与回退逻辑逐步部署。
// 编辑推荐
Clash V.CORE:让 API 自动化更易维护
从 external-controller 到策略组切换,使用清晰的配置与日志流程构建可靠的节点自动选择。
- 便于核对 API 控制器状态
- 支持复杂策略组与节点管理
- 适合定时检测和异常回退
- 清晰记录延迟与切换结果
- 支持桌面与长期运行场景