Clash 外部控制器自動換節點:故障切換與腳本實作

節點失效或延遲飆高時,靠手動更換代理既慢又容易中斷工作。本篇以外部控制器為核心,建立可持續執行的節點健康檢查與自動換線流程,並說明策略組更新、API 驗證和服務化部署的實務細節。

外部控制器能做什麼

Clash 的外部控制器是一組供面板與自動化程式使用的 HTTP API。它不負責建立代理節點,而是把目前已載入的設定、代理列表、策略組狀態與連線資訊暴露出來,讓外部程式可以讀取狀態或發出切換指令。Clash Verge、Clash Verge Rev、Mihomo Party 與其他基於 mihomo 的用戶端,通常都能在設定頁看到外部控制器位址與密鑰。

節點自動切換的核心流程可以拆成四個動作:取得策略組中的節點名稱,逐一對節點執行延遲或連線測試,依結果選出可用節點,最後透過 API 把策略組切換到新節點。這個流程和用戶端畫面上的手動點選相同,只是把判斷條件與執行頻率固定下來。

要先分清楚「節點」與「策略組」不是同一個物件。節點是實際的代理出口,例如某個伺服器或某條代理設定;策略組則是把多個節點包在一起的選擇器,名稱可能是 PROXY🚀 節點選擇 或訂閱服務商自訂的名稱。API 切換時,請求的目標通常是策略組,請求內容才是要選中的節點。

先保護外部控制器

外部控制器等同於代理核心的管理入口。不要把它綁定在公網位址,也不要把密鑰放進公開腳本、聊天紀錄或版本庫。單機腳本優先使用 127.0.0.1:9090;只有確實需要區域網路控制時,才設定區域網路位址並搭配防火牆限制來源。

開啟 API 與確認策略組

在 mihomo 設定中,外部控制器由 external-controller 指定監聽位址,密鑰由 secret 指定。下面是一份只供本機腳本使用的示例設定,其中密鑰是刻意編寫的假值,實際使用時請換成長度足夠的隨機字串。

external-controller: 127.0.0.1:9090
secret: "demo-local-secret-change-me"

修改設定後要重新載入設定檔或重啟核心,再確認埠是否真的在監聽。Windows 可以用 netstat -ano | findstr 9090,macOS 與 Linux 可以用 lsof -iTCP:9090 -sTCP:LISTEN。若用戶端是 Clash Verge 或 Clash Verge Rev,也要注意圖形介面顯示的 API 埠可能由當前核心設定檔覆蓋,不能只看全域偏好設定。

API 驗證使用 HTTP 標頭 Authorization: Bearer 密鑰。先呼叫根端點確認控制器可連線,再查詢全部代理。查詢結果中的 proxies 是物件,鍵名就是節點或策略組名稱;策略組則會包含 allnowtype 等欄位。

curl -H "Authorization: Bearer demo-local-secret-change-me" \
  http://127.0.0.1:9090/proxies

不要把策略組名稱寫死成英文 PROXY 就直接部署。訂閱設定經常使用表情符號、空格或全形標點,例如 🚀 節點選擇;名稱只要有一個字元不同,API 就會回傳找不到代理。最穩定的做法是先列出所有名稱,再透過設定檔中的固定名稱、環境變數或明確關鍵字選定目標。

端點用途常見方法
/確認控制器是否可連線GET
/proxies取得節點、策略組與目前選擇GET
/proxies/{name}取得單一節點或策略組詳情GET
/proxies/{group}切換策略組目前選中的節點PUT
/proxies/{name}/delay測試節點延遲GET

健康檢查與自動選線邏輯

延遲測試不能只看數字最小的節點。單次測試可能受到瞬間擁塞、DNS 解析或目標網址不可達影響,所以腳本至少要同時考慮測試成功、延遲上限、連續失敗次數與切換冷卻時間。若每次輪詢都切換到當下最快的節點,網路稍有抖動就會頻繁換線,反而造成連線中斷與登入狀態失效。

測試網址應選擇穩定、回應內容不重要的 HTTPS 目標,並設定合理的逾時時間。這裡使用 https://www.example.com/generate_204 作為格式示例;實際部署時請換成你能穩定存取的測試網址。若策略組中混有不支援該網址協定的節點,測試失敗只能代表「這個節點無法完成本次測試」,不能直接推論節點永久失效。

  1. 讀取策略組的 all 清單,排除策略組、DIRECTREJECT 與不希望自動選用的名稱。
  2. 對每個候選節點呼叫延遲端點,傳入經 URL 編碼的測試網址與逾時毫秒數。
  3. 捨棄逾時、HTTP 錯誤或回傳格式不完整的結果,再套用延遲上限,例如 800 毫秒。
  4. 保留目前節點的黏著時間,只有目前節點失敗或候選節點明顯快很多時才切換。
  5. 切換後等待下一輪確認,不要在同一輪連續 PUT 多次,避免核心與上游連線同時重建。

下面的 Python 範例使用標準函式庫,不依賴第三方套件。它假設策略組名稱由環境變數提供,每輪先讀取策略組,再逐一測試候選節點。requests 等第三方套件雖然寫法較短,但在服務化部署時需要額外管理虛擬環境;標準函式庫更容易搬到 Windows 工作排程或 Linux systemd。

#!/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", "PROXY")
TEST_URL = os.getenv("TEST_URL", "https://www.example.com/generate_204")
TIMEOUT_MS = int(os.getenv("TEST_TIMEOUT_MS", "5000"))
MAX_DELAY = int(os.getenv("MAX_DELAY_MS", "800"))
INTERVAL = int(os.getenv("CHECK_INTERVAL_SEC", "60"))
SWITCH_MARGIN = int(os.getenv("SWITCH_MARGIN_MS", "120"))

def request(path, method="GET", body=None):
    data = None if body is None else json.dumps(body).encode()
    req = urllib.request.Request(API + path, data=data, method=method)
    req.add_header("Authorization", "Bearer " + SECRET)
    req.add_header("Content-Type", "application/json")
    with urllib.request.urlopen(req, timeout=TIMEOUT_MS / 1000 + 2) as res:
        return json.loads(res.read().decode())

def delay(node):
    name = urllib.parse.quote(node, safe="")
    target = urllib.parse.quote(TEST_URL, safe="")
    path = f"/proxies/{name}/delay?timeout={TIMEOUT_MS}&url={target}"
    try:
        result = request(path)
        value = int(result.get("delay", 0))
        return value if value > 0 else None
    except (urllib.error.HTTPError, urllib.error.URLError,
            TimeoutError, ValueError, json.JSONDecodeError):
        return None

def check_once():
    group_name = urllib.parse.quote(GROUP, safe="")
    info = request("/proxies/" + group_name)
    candidates = info.get("all", [])
    current = info.get("now")
    results = {}
    for node in candidates:
        if node in ("DIRECT", "REJECT") or node == GROUP:
            continue
        value = delay(node)
        if value is not None and value <= MAX_DELAY:
            results[node] = value

    if not results:
        print("沒有可用節點,保留目前選擇:", current, flush=True)
        return

    best, best_delay = min(results.items(), key=lambda item: item[1])
    current_delay = results.get(current)
    should_switch = current not in results
    if current_delay is not None:
        should_switch = best_delay + SWITCH_MARGIN < current_delay

    if should_switch and best != current:
        request("/proxies/" + group_name, "PUT", {"name": best})
        print(f"已切換: {current} -> {best}, {best_delay} ms", flush=True)
    else:
        print(f"維持: {current}, 最佳 {best} {best_delay} ms", flush=True)

while True:
    try:
        check_once()
    except Exception as exc:
        print("本輪檢查失敗:", exc, flush=True)
    time.sleep(INTERVAL)

避免誤切換的實務細節

API 可以很快完成切換,但「能切換」不代表「應該立刻切換」。最常見的錯誤是把一次超時當成節點死亡,下一輪又把節點切回來,最後形成來回震盪。建議設定至少兩輪失敗才標記為不可用,並讓成功節點維持一段最短使用時間。例如檢查週期設為 60 秒,冷卻時間設為 5 分鐘,就能避開短暫封包遺失造成的頻繁換線。

  • 設定延遲門檻:只接受小於 800 或 1000 毫秒的結果。門檻應依所在地網路與服務用途調整,串流與互動服務不適合沿用同一個數值。
  • 設定切換幅度:最佳節點只比目前節點快 10 毫秒時不要切換。使用 100 至 200 毫秒的差值,能減少延遲測量誤差帶來的抖動。
  • 保留目前節點:若目前節點仍能通過測試,優先維持連線。自動化的目標是恢復可用性,不是每分鐘追逐最低數字。
  • 限制候選範圍:可用名稱前綴、地區標籤或固定清單排除高流量、限時與特殊用途節點,避免腳本選到不適合日常使用的出口。
  • 記錄每輪結果:至少記錄時間、策略組、目前節點、最佳節點、延遲與錯誤原因。沒有紀錄時,很難分辨是 API 失效、節點逾時還是測試網址不可達。

不要覆寫訂閱設定

PUT /proxies/{group} 通常只改變目前執行中的策略組選擇,不會把選擇永久寫回遠端訂閱。用戶端重新載入設定或訂閱更新後,策略組可能恢復預設值,因此腳本必須能在核心重載後重新判斷,不能把一次切換當成永久修改。

若需要驗證整條代理鏈,可以把測試網址與實際使用的協定分開設計。延遲端點主要反映核心經該節點建立請求的時間,不等同於影片播放、檔案下載或長連線穩定度。對重要工作環境,可在切換後另外執行一次應用層健康檢查,但不要把過多探測流量送進節點。

服務化部署與故障排查

手動在終端機執行腳本適合初次驗證,長期使用則應交給作業系統的服務管理器。Linux 上可用 systemd,Windows 可用工作排程器或把腳本包成服務。無論平台為何,密鑰都不應直接寫在腳本裡,建議透過環境檔、系統密鑰儲存區或只有服務帳號可讀的檔案注入。

Linux 的 systemd 服務可以使用以下形式。路徑請按照實際安裝位置修改,並確保執行帳號有權讀取腳本與環境檔。

[Unit]
Description=Clash node auto switch
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=clashbot
WorkingDirectory=/opt/clash-autoswitch
EnvironmentFile=/etc/clash-autoswitch.env
ExecStart=/usr/bin/python3 /opt/clash-autoswitch/autoswitch.py
Restart=on-failure
RestartSec=10

[Install]
WantedBy=multi-user.target

環境檔可以只保留必要值,例如 CLASH_API=http://127.0.0.1:9090CLASH_GROUP=PROXYCLASH_SECRET=demo-local-secret-change-me。建立後將權限限制為服務帳號或 root 可讀,再執行 systemctl daemon-reloadsystemctl enable --now clash-autoswitch。查看 journalctl -u clash-autoswitch -f 能即時確認每輪檢查與切換結果。

排查時按照「核心、認證、名稱、節點」的順序最有效。先用瀏覽器或 curl 確認 API 埠可達;接著確認 Authorization 標頭與密鑰完全一致;再查 /proxies 中是否真的存在策略組與候選節點;最後才分析延遲測試失敗。若回傳 401,通常是密鑰或標頭格式錯誤;若回傳 404,常見原因是策略組名稱未經 URL 編碼或名稱已因訂閱更新而改變;若所有節點都超時,應先用用戶端手動測試,排除測試網址、系統防火牆與核心本身的問題。

完成部署後,不要只驗證「腳本有在執行」。可以暫時把延遲門檻調低,或手動讓目前節點不可用,觀察腳本是否只切換一次、日誌是否記錄原因、用戶端畫面是否同步更新。測試結束後恢復正常門檻,再確認重啟核心、更新訂閱與重啟腳本三種情況都能自動恢復。這樣建立的流程,才是可長期維持的故障切換,而不是一個只在終端機裡偶爾成功的示範程式。

外部控制器故障切換自動化腳本

先確認核心與控制器

開始寫腳本前,先安裝仍在維護的 Clash 用戶端,確認 mihomo 核心、外部控制器埠與策略組名稱,再依本文流程逐步測試。

下載 Clash 用戶端

規則分流的前提是用戶端先接管流量。前往下載中心依平台選擇用戶端,再回到教學文章完成系統代理或 TUN 接管設定。

下載Clash