先理解 external-controller 与自动切换边界
Clash API 自动切换节点的核心,不是修改订阅文件,而是通过正在运行的 mihomo 或 Clash 内核控制接口,读取代理组状态、测试候选节点延迟,再向指定代理组提交新的选择结果。客户端界面只是控制接口的一个调用方;脚本、监控程序和命令行工具也可以使用同一套 REST API。
常见控制接口地址是 127.0.0.1:9090。混合代理端口通常是 7890,两者用途完全不同:7890 用来转发 HTTP、HTTPS 或 SOCKS 流量,9090 用来查询内核运行状态和修改策略。把控制端口填写到浏览器代理设置中,或把代理端口当作 API 地址,都会得到连接失败或协议错误。
mixed-port: 7890
allow-lan: false
mode: rule
log-level: info
external-controller: 127.0.0.1:9090
secret: replace-with-a-long-random-secret
在 FlClash 或其他采用 mihomo 的客户端中,字段名称和菜单位置可能略有不同,但判断方法一致:先确认内核已经启动,再确认控制接口监听地址和端口,最后使用带有 Bearer 密钥的请求访问 /version。如果接口没有启用,客户端仍然可以正常打开,但自动化脚本无法建立控制连接。
curl -sS \
-H "Authorization: Bearer replace-with-a-long-random-secret" \
http://127.0.0.1:9090/version
正常响应通常是 JSON,包含版本号、发布日期或运行内核标识。401 Unauthorized 说明接口可达但鉴权失败;connection refused 通常表示内核未启动、端口写错,或 external-controller 没有监听该地址;404 Not Found 则可能是路径、客户端实现或请求方法不符合当前内核版本。
鉴权与暴露范围必须同时配置
Clash API 的密钥通过请求头传递,格式为 Authorization: Bearer 密钥。密钥不是订阅链接,也不能写进会被网页前端直接访问的公开 JavaScript。若脚本保存于个人电脑,可以放在环境变量、操作系统凭据存储或权限严格的配置文件中;不要把真实密钥提交到代码仓库、同步目录或公开日志。
- 本机脚本:使用
127.0.0.1:9090,通常不需要开启allow-lan。 - 局域网管理:只绑定明确的局域网地址,并在防火墙中限制来源 IP 和端口。
- 公网访问:不建议直接暴露控制端口。若必须远程操作,应放在 VPN、SSH 隧道或经过身份验证的反向代理之后。
- 日志处理:记录状态码、耗时和节点名称即可,避免打印完整的
Authorization请求头。
读取代理组与候选节点
自动切换前要先弄清楚代理组的层级。配置中的 proxy-groups 可能包含具体节点,也可能包含另一个代理组。例如“节点选择”下面有“自动测速”和多个节点,“流媒体”又引用“节点选择”。脚本如果直接把所有返回名称当作物理节点测试,可能会测试到 DIRECT、REJECT 或另一个策略组,最终选择结果并不符合预期。
请求 /proxies 可以取得当前内核识别到的代理和策略组。返回对象的键通常是名称,值中可能包含 type、now、all、history 等字段。不同版本的字段细节可能存在差异,因此脚本应优先根据 type 和 all 判断,不要假定每个对象都具备同样结构。
curl -sS \
-H "Authorization: Bearer replace-with-a-long-random-secret" \
http://127.0.0.1:9090/proxies
也可以只读取某一个代理组。名称中可能包含空格、斜杠、中文或特殊符号,拼接 URL 时必须进行百分号编码,不能直接把原始名称放进路径。下面的示例假定代理组名称为“节点选择”,实际使用时应替换为当前配置中的名称。
curl -G -sS \
-H "Authorization: Bearer replace-with-a-long-random-secret" \
--data-urlencode "name=节点选择" \
http://127.0.0.1:9090/proxies/节点选择
在 shell 中直接调用时,推荐使用 Python、Node.js 或其他 HTTP 库负责 URL 编码。手工处理中文名称不仅容易产生 404,也可能在不同操作系统的终端编码下出现难以复现的问题。
筛选真正可测试的节点
一个可维护的脚本需要明确候选节点规则。最简单的方式是在配置中建立一个专用代理组,只放入希望自动切换的节点;脚本只操作这个组,不扫描全部代理。这样可以排除备用链、直连、拒绝策略以及只为特殊域名准备的节点。
proxy-groups:
- name: 自动选择
type: select
proxies:
- 节点-日本
- 节点-新加坡
- 节点-美国
- DIRECT
如果代理组使用 select 类型,API 可以修改当前选择;如果使用 url-test 类型,内核本身已经具备按延迟选择节点的能力,通常不需要另写脚本。手动脚本更适合增加业务条件,例如排除高丢包节点、限制地区、设置最低剩余带宽,或在连续失败后暂时冷却节点。
| 代理组类型 | 适合的控制方式 | 注意事项 |
|---|---|---|
| select | 脚本测试后通过 API 指定当前节点 | 需要自己处理测试、排序和失败回退 |
| url-test | 由内核按测试 URL 和间隔自动选择 | 复杂业务条件较难表达 |
| fallback | 按可用性顺序自动回退 | 更关注成功与失败,不等同于最低延迟 |
| load-balance | 由内核在多个节点之间分配连接 | 不适合要求固定出口 IP 的场景 |
延迟测试、阈值与 API 切换
mihomo 常见的延迟测试接口是 GET /proxies/{name}/delay。请求时需要提供测试 URL 和超时时间,例如 http://www.gstatic.com/generate_204 或自己能够稳定访问的 HTTPS 探测地址。返回结果通常包含延迟数值;如果连接失败、证书验证失败、超时或节点不支持目标网络,接口会返回错误。
curl -G -sS \
-H "Authorization: Bearer replace-with-a-long-random-secret" \
--data-urlencode "url=http://www.gstatic.com/generate_204" \
--data-urlencode "timeout=5000" \
"http://127.0.0.1:9090/proxies/节点-日本/delay"
测试 URL 不应选择体积很大的网页或变化频繁的接口。延迟探测的目标是快速判断连接建立和少量响应是否正常,而不是测量完整下载速度。若所有节点都无法访问探测地址,先确认这个地址在当前网络中可达;否则脚本会把公共探测站故障误判为节点全部失效。
切换 select 代理组时,通常使用 PUT /proxies/{group-name},请求体中的 name 是候选节点名称:
curl -sS -X PUT \
-H "Authorization: Bearer replace-with-a-long-random-secret" \
-H "Content-Type: application/json" \
--data '{"name":"节点-日本"}' \
"http://127.0.0.1:9090/proxies/自动选择"
下面是一个使用 Python 标准库的完整简化示例。它只读取指定代理组的候选项,跳过 DIRECT 和 REJECT,测试延迟低于 5000 毫秒的节点,并在最佳节点与当前节点不同时提交切换。真实环境中应根据网络特征调整探测 URL、超时和阈值。
import json
import os
import urllib.parse
import urllib.request
API = "http://127.0.0.1:9090"
SECRET = os.environ["CLASH_API_SECRET"]
GROUP = "自动选择"
TEST_URL = "http://www.gstatic.com/generate_204"
TIMEOUT = 5000
MAX_DELAY = 1800
def request(path, method="GET", body=None):
headers = {"Authorization": f"Bearer {SECRET}"}
data = None
if body is not None:
headers["Content-Type"] = "application/json"
data = json.dumps(body, ensure_ascii=False).encode()
req = urllib.request.Request(API + path, data=data,
headers=headers, method=method)
with urllib.request.urlopen(req, timeout=8) as response:
return json.loads(response.read().decode())
group_path = "/proxies/" + urllib.parse.quote(GROUP, safe="")
group = request(group_path)
candidates = [
name for name in group.get("all", [])
if name not in {"DIRECT", "REJECT"}
]
results = []
for name in candidates:
node_path = "/proxies/" + urllib.parse.quote(name, safe="")
query = urllib.parse.urlencode({
"url": TEST_URL,
"timeout": str(TIMEOUT)
})
try:
result = request(node_path + "/delay?" + query)
delay = int(result["delay"])
if delay <= MAX_DELAY:
results.append((delay, name))
except Exception:
continue
if results:
results.sort()
best_delay, best_name = results[0]
if group.get("now") != best_name:
request(group_path, method="PUT", body={"name": best_name})
print(f"{best_name}: {best_delay} ms")
else:
print("没有通过阈值的节点,保留当前选择")
把密钥放入环境变量后再运行,例如 Linux 或 macOS 可以使用 export CLASH_API_SECRET='真实密钥'。Windows 则可以在 PowerShell 中使用 $env:CLASH_API_SECRET='真实密钥'。脚本不应在异常处理中无条件切换到 DIRECT,因为这可能让本应经过代理的请求意外直连。
增加冷却、滞后与失败回退
一个实用的切换策略通常包含三个条件。第一是最低质量阈值,例如延迟必须低于 1800 ms;第二是滞后值,例如新节点至少比当前节点快 150 ms 才切换;第三是冷却时间,例如两次切换间隔不短于 10 分钟。这些条件可以避免网络轻微波动导致策略组在两个节点之间来回跳转。
- 首次运行:测试全部候选节点,选择第一个通过阈值的节点。
- 日常轮询:先测试当前节点,再测试其他候选,减少不必要的 API 请求。
- 当前节点轻微变慢:只有新节点达到滞后条件时才切换。
- 当前节点连续失败:立即从健康候选中选择,同时记录失败原因。
- 全部节点失败:保留当前选择并报警,不要静默改成直连。
定时执行、日志与常见故障
桌面系统上可以用任务计划程序、cron 或 systemd timer 周期运行脚本。周期不宜设置得过短:延迟测试会产生额外连接,多个设备同时轮询还可能增加节点服务端压力。一般可从 5~15 分钟开始,根据节点稳定性和实际需求调整。若使用内核自身的 url-test,则应优先调整代理组的 interval,避免内核测试和外部脚本重复探测。
# 每 10 分钟执行一次,输出追加到本地日志
*/10 * * * * /usr/bin/python3 /opt/clash/health_switch.py \
>> /var/log/clash-health.log 2>&1
日志应记录执行时间、代理组、候选数量、每个节点的延迟或错误类型、最终选择以及是否发生切换。不要记录订阅 URL、Bearer 密钥和完整请求头。为了便于分析,可以使用脱敏后的节点名称,或者只记录名称哈希。
| 现象 | 优先检查 | 处理方向 |
|---|---|---|
| 所有请求都是 401 | secret 与 Bearer 内容 |
重新读取当前运行配置,确认没有多余空格或旧密钥 |
| 读取代理组返回 404 | 代理组名称编码与 API 路径 | 对中文、空格和特殊字符使用 URL 编码 |
| 延迟接口全部超时 | 探测 URL、本机网络和节点协议 | 更换可达测试地址,分别手动验证一个节点 |
| PUT 返回成功但界面未变化 | 操作的组是否为 select,以及客户端显示的是否是同一内核 |
重新 GET 代理组,确认 now 是否已更新 |
| 节点名称找不到 | 订阅更新是否重命名或移除了节点 | 每次运行动态读取 all,不要永久写死节点列表 |
| 频繁来回切换 | 切换阈值、轮询间隔和延迟波动 | 增加滞后值、冷却时间和连续失败计数 |
排查时建议按照“接口可达、鉴权正确、代理组存在、单节点测试、切换请求、规则实际生效”的顺序进行。先用 /version 验证基础连接,再用 /proxies 验证数据读取,随后单独测试一个节点的 /delay。只有这些步骤都成功后,才检查定时器和脚本逻辑。
最后还要验证流量是否真的经过了切换后的节点。API 返回 now 只代表策略组状态已经改变,不代表所有已有连接立即迁移。部分连接会继续使用原来的出站,新的连接才会按照新策略建立。可以在切换后重新发起测试请求,并结合 /connections 查看现有连接,必要时关闭异常的长连接后再复测。