Clash API 節點自動切換:進階腳本與容錯設定
從 Clash 的控制器介面與策略組運作方式開始,本文示範如何撰寫可執行的自動選點腳本,建立包含延遲檢測、節點容錯、紀錄追蹤與權限保護的進階代理管理流程。
先理解控制器介面與策略組
Clash API 並不是另一套代理協定,而是用戶端與 mihomo 核心之間的管理介面。核心啟動後,會在指定的 external-controller 位址監聽 HTTP API,圖形介面、命令列工具或自訂腳本都可以透過這個介面讀取節點狀態、切換策略組、查詢連線與取得流量資訊。常見設定如下:
external-controller: 127.0.0.1:9090
secret: "change-this-to-a-long-random-secret"
proxy-groups:
- name: "自動選擇"
type: url-test
proxies:
- "HK-01"
- "JP-01"
- "SG-01"
url: "https://www.gstatic.com/generate_204"
interval: 300
tolerance: 50
external-controller 只代表 API 的監聽位址與埠號,不等於代理流量使用的 mixed-port。例如代理埠可能是 7890,控制器則是 127.0.0.1:9090,兩者用途完全不同。secret 是 API 的驗證密鑰,啟用後請求通常要在 HTTP 標頭加入 Authorization: Bearer 你的密鑰。
策略組則是流量選擇的抽象層。規則不必直接指向某個節點,只要指向「自動選擇」或「代理」這類策略組,腳本切換組內目前選中的節點後,所有引用該組的規則便會一併生效。這種分層很重要:腳本負責選點,規則負責決定哪些流量使用這個組,兩者不要混寫。
控制器不要直接暴露到公網
external-controller: 0.0.0.0:9090 會讓區域網路甚至更大範圍的裝置嘗試存取管理介面,一旦密鑰外流,對方可能切換節點、讀取連線資訊或修改執行狀態。除非有明確的防火牆與反向代理隔離方案,建議固定綁定 127.0.0.1。需要跨裝置管理時,優先使用作業系統的 SSH 隧道或 VPN,不要直接把 9090 埠映射到網際網路。
選擇內建測速還是外部自動化腳本
mihomo 已經提供 url-test、fallback 與 load-balance 等策略組類型。若需求只是定期測試節點延遲並選出較快者,優先使用內建 url-test:核心會按照 interval 定期測試,在延遲差距超過 tolerance 時切換。這種方式不需要額外程序,也不會因 API 腳本停止而失效。
fallback 的重點不是選最低延遲,而是依照列表順序使用第一個可用節點。它適合工作帳號、固定出口或對節點順序有要求的情境。load-balance 則用於分散連線,不應誤當成「一定選最快」的策略。只有當你需要自訂測試網址、連續失敗計數、排除特定名稱、紀錄結果或在切換前後執行通知時,才值得使用外部 API 腳本。
| 方案 | 適合需求 | 主要限制 |
|---|---|---|
| url-test | 週期性選擇延遲較低的節點 | 判斷邏輯由核心固定處理 |
| fallback | 主節點失效後依序切換備援 | 不以最低延遲為選擇條件 |
| load-balance | 在多個節點之間分散連線 | 連線分散不等於單一節點最快 |
| API 腳本 | 自訂檢測、紀錄、告警與容錯規則 | 需要自行管理權限、錯誤與排程 |
實作時建議保留一個穩定的策略組名稱,例如「自動選擇」,而不是讓腳本直接修改規則內容。腳本只需要取得該組的候選節點,逐一呼叫延遲 API,最後向 /proxies/<策略組名稱> 發送 PUT 請求即可。若策略組本身是 url-test,也可以只在內建結果長時間不理想時進行人工或腳本介入,避免頻繁切換造成連線抖動。
撰寫可執行的延遲選點腳本
下面的 Python 範例只使用標準函式庫,不需要安裝第三方套件。它會從控制器讀取指定策略組,對組內節點呼叫延遲測試 API,忽略測試失敗的節點,在延遲最低且低於門檻的候選中切換。範例假設控制器只在本機監聽,請按實際用戶端的策略組名稱修改 GROUP。
#!/usr/bin/env python3
import json
import os
import sys
import time
import urllib.parse
import urllib.request
from pathlib import Path
CONTROLLER = os.getenv("CLASH_CONTROLLER", "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", "5000"))
MAX_LATENCY_MS = int(os.getenv("CLASH_MAX_LATENCY_MS", "2500"))
LOG_FILE = Path(os.getenv("CLASH_LOG_FILE", "clash-switch.log"))
def request(method, path, payload=None):
url = CONTROLLER.rstrip("/") + path
headers = {"Accept": "application/json"}
if SECRET:
headers["Authorization"] = "Bearer " + SECRET
data = None
if payload is not None:
data = json.dumps(payload, ensure_ascii=False).encode("utf-8")
headers["Content-Type"] = "application/json"
req = urllib.request.Request(url, data=data, headers=headers, method=method)
with urllib.request.urlopen(req, timeout=(TIMEOUT_MS / 1000) + 2) as response:
raw = response.read()
return json.loads(raw.decode("utf-8")) if raw else {}
def log(message):
stamp = time.strftime("%Y-%m-%d %H:%M:%S")
with LOG_FILE.open("a", encoding="utf-8") as stream:
stream.write(f"{stamp} {message}\n")
def main():
if not SECRET:
raise RuntimeError("請先設定 CLASH_SECRET,不要把密鑰直接寫入腳本")
group_path = "/proxies/" + urllib.parse.quote(GROUP, safe="")
group = request("GET", group_path)
candidates = group.get("all", [])
if not candidates:
raise RuntimeError("策略組沒有可測試的節點")
results = []
test_query = urllib.parse.urlencode({
"url": TEST_URL,
"timeout": str(TIMEOUT_MS)
})
for name in candidates:
if name in ("DIRECT", "REJECT"):
continue
node_path = "/proxies/" + urllib.parse.quote(name, safe="")
try:
result = request("GET", node_path + "/delay?" + test_query)
delay = int(result.get("delay", 0))
if delay > 0 and delay <= MAX_LATENCY_MS:
results.append((delay, name))
log(f"delay={delay}ms node={name}")
else:
log(f"unusable delay={delay}ms node={name}")
except Exception as error:
log(f"failed node={name} error={error}")
if not results:
raise RuntimeError("沒有通過門檻的節點,保留目前選擇")
results.sort(key=lambda item: item[0])
delay, selected = results[0]
request("PUT", group_path, {"name": selected})
log(f"switched group={GROUP} node={selected} delay={delay}ms")
print(f"{GROUP}: {selected} ({delay} ms)")
if __name__ == "__main__":
try:
main()
except Exception as error:
print("switch failed:", error, file=sys.stderr)
sys.exit(1)
執行前在終端機設定密鑰,不要把密鑰直接放進 Shell 歷史、公開腳本或排程檔的命令列參數中:
export CLASH_SECRET='請替換成控制器密鑰'
python3 clash-switch.py腳本使用 /proxies/<group> 取得策略組資料。回應中的 all 是候選節點名稱陣列,而 now 是目前選中的節點。測速使用 /proxies/<node>/delay,測試成功後回應會包含延遲值。最後對策略組發送 PUT,請求內容是 {"name":"節點名稱"}。如果節點名稱含有空格、斜線或中文,一定要使用 URL 編碼,不能自行拼接原始字串。
調整門檻與避免頻繁切換
延遲最低不代表實際體驗一定最好。測試網址回應快,只能說明該節點到測試站點的這條路徑較快,不能代表所有網站、串流服務或遊戲伺服器都一樣。CLASH_MAX_LATENCY_MS 可先設定為 2000 至 3000 毫秒,再依日誌觀察調整。若節點延遲只差 10 毫秒,不值得立刻切換,可以加入「新節點必須比目前節點快 20%」的條件,或連續兩次測試都勝出後才執行 PUT。
排程頻率也要保守。內建 url-test 已有定時檢測時,外部腳本可每 10 至 30 分鐘執行一次;若每幾秒切換一次,現有 TCP 連線不會因此變快,反而可能導致新連線反覆重建。測試網址應選擇穩定、回應內容小的 HTTPS 位址,並避免使用需要登入、會改變內容或容易觸發頻率限制的網站。
建立節點容錯與紀錄追蹤
可靠的自動切換流程不能只看一次測速結果,至少要區分「暫時超時」「核心拒絕測試」「節點本身不可用」與「控制器無法連線」。測試失敗時不要把策略組切到 DIRECT,也不要清空候選節點;較安全的處理是保留目前選擇,等待下一輪重試。若目前節點連續多輪失敗,再從通過測試的備援中選擇一個。
- 保留目前節點:新一輪測試全部失敗時,不要執行 PUT,讓核心維持原狀,避免暫時的 DNS 或測試站點故障造成意外直連。
- 設定最低門檻:延遲為零、超過逾時值或回應格式不完整,都應視為不可用,不可把錯誤值當成最快結果。
- 加入冷卻時間:切換後至少等待數分鐘再允許下一次切換,避免多個排程程序同時改寫策略組。
- 記錄前後狀態:至少保存時間、策略組、節點名稱、測試延遲、錯誤原因與腳本退出碼,方便判斷是節點問題還是 API 問題。
- 單一執行鎖:使用系統排程時,要避免上一輪尚未完成,下一輪又同時啟動。可使用作業系統的 lock 檔或排程器提供的互斥選項。
若需要進一步的故障轉移,可以把節點分成「主要」與「備援」兩個策略組。主要組由腳本依延遲選擇,備援組使用 fallback 按順序保留兩至三個穩定節點,規則只引用主要組。當主要組沒有任何通過測試的節點時,腳本可以把主要組暫時切到備援組名稱;恢復後再切回。不過這種跨組切換會改變使用者的手動選擇,實作前應把狀態寫入日誌,並提供明確的還原命令。
先觀察再自動化
建議先讓腳本只測速與寫入日誌,暫時不執行 PUT。連續觀察幾天後,確認測試網址、延遲分布、節點命名與排程頻率都合理,再開啟自動切換。這樣可以先排除「測試結果很好但實際服務不可用」或「節點名稱與策略組不一致」等問題。
mihomo 控制器也能查詢目前代理與連線狀態。常用的唯讀端點包括 GET /proxies、GET /proxies/<name>、GET /connections 與 GET /traffic。監控腳本應先使用這些唯讀端點確認狀態,只有在確定需要變更時才呼叫 PUT。頻繁查詢流量資訊時也要控制間隔,不要把控制器當成高頻指標資料庫。
權限保護與切換後驗證
控制器密鑰的保護等級應與訂閱連結相同。訂閱 URL 常含有可識別帳戶的 token,控制器密鑰則可能直接改變本機代理狀態,兩者都不應放入公開貼文、螢幕截圖或可被其他帳戶讀取的共用目錄。Linux 與 macOS 可以把密鑰放在權限為 600 的環境檔;Windows 則可使用僅限目前使用者讀取的排程工作或系統環境變數。
防火牆規則只允許本機存取控制器。若用戶端提供「外部控制器」或「允許區域網路控制」開關,確認沒有誤開後者。控制器埠與代理埠要分開管理:即使代理埠需要用 allow-lan: true 分享給手機,也不代表控制器 9090 應該對區域網路開放。
切換後可做三層驗證。第一層讀取策略組的 now,確認核心接受了新的節點名稱;第二層查看連線面板,確認新建立的連線使用預期節點;第三層使用一個遵循系統代理的測試命令驗證實際出口。不要只看「PUT 回應 204」就認定網路一定正常,204 只能代表控制器接受了這次設定變更。
# 查詢策略組目前選擇
curl -H "Authorization: Bearer $CLASH_SECRET" \
"http://127.0.0.1:9090/proxies/%E8%87%AA%E5%8B%95%E9%81%B8%E6%93%87"
# 透過本機混合埠檢查代理請求
curl -x http://127.0.0.1:7890 https://www.gstatic.com/generate_204 -I
如果查詢策略組成功但代理請求失敗,問題通常在節點本身、規則匹配、DNS 或 TUN 路由,不一定是 API 腳本。若連策略組查詢都失敗,先檢查控制器位址、埠號、密鑰與核心是否正在執行。若腳本回報策略組不存在,請直接呼叫 GET /proxies 查看實際名稱,中文標點、大小寫與空格都必須完全一致。
不要用 API 腳本繞過安全邊界
腳本可以自動選擇節點,但不能替代合法的訂閱權限、服務商規則或作業系統安全設定。不要把控制器密鑰寫進前端網頁,不要在不可信任的遠端主機上執行能直接連回本機控制器的程式,也不要為了排查問題而長期關閉防火牆與 TUN 權限保護。
部署前檢查清單
正式啟用前,可以按以下順序確認整套流程。先確認 mihomo 核心能正常載入設定,再確認控制器只監聽本機並啟用密鑰;接著用唯讀 API 讀取策略組與候選節點,手動測試一個節點的 /delay 回應;最後才允許腳本透過 PUT 切換。
- 確認策略組名稱、候選節點名稱與實際設定檔一致,特別注意空格、括號與全形字元。
- 確認測試網址可從目前網路環境連線,並設定合理的逾時值與最低延遲門檻。
- 確認 API 密鑰由環境變數或受保護的秘密儲存提供,腳本檔案本身不含明文密鑰。
- 確認測試失敗時保留原節點,而不是自動切換到直連或刪除整個策略組。
- 確認排程只有一個執行個體,並保留足夠的日誌供後續追蹤。
- 確認切換後會讀取
now並執行一次實際代理驗證。
對大多數使用者而言,先使用內建 url-test 或 fallback 已能完成穩定的自動選點。外部 API 腳本的價值在於把企業內網、家庭閘道或多裝置代理管理中的特殊條件納入流程:例如指定測試站點、限制節點地區、記錄延遲趨勢、連續故障後通知,以及在控制器變更前後保留可追溯紀錄。只要把讀取、判斷、切換、容錯與權限保護分開設計,就能在不破壞原有規則與 TUN 設定的前提下,建立可維護的節點自動化管理流程。
DOWNLINK READY
下載 Clash 客戶端
需要使用 mihomo 核心、策略組與控制器介面的桌面或行動客戶端,可依平台選擇合適版本。
DOWNLINK READY
下載 Clash 客戶端
涵蓋 Windows、macOS、Linux、Android 與 iOS 的 Clash 客戶端下載入口,含 mihomo 核心與圖形介面版本。