Created
May 20, 2026 07:11
-
-
Save growvv/0c5e62750a31a0da8e54aaf3c1059ebf to your computer and use it in GitHub Desktop.
服务端管理Clash的TUI
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| set -euo pipefail | |
| API="${API:-http://127.0.0.1:9092}" | |
| GROUP="${GROUP:-三毛机场}" | |
| TEST_URL="${TEST_URL:-http://www.apple.com/library/test/success.html}" | |
| TIMEOUT_MS="${TIMEOUT_MS:-5000}" | |
| ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" | |
| GROUP_ENC="$(python3 -c 'import sys, urllib.parse; print(urllib.parse.quote(sys.argv[1], safe=""))' "$GROUP")" | |
| need() { | |
| command -v "$1" >/dev/null 2>&1 || { | |
| echo "Missing required command: $1" >&2 | |
| exit 1 | |
| } | |
| } | |
| need curl | |
| need python3 | |
| need whiptail | |
| api_get() { | |
| curl -fsS --connect-timeout 5 "$API$1" | |
| } | |
| api_put_json() { | |
| curl -fsS --connect-timeout 5 -X PUT -H 'Content-Type: application/json' --data-binary @- "$API$1" | |
| } | |
| message() { | |
| whiptail --title "Clash TUI" --msgbox "$1" 18 78 | |
| } | |
| current_node() { | |
| api_get "/proxies/$GROUP_ENC" | python3 -c 'import sys,json; print(json.load(sys.stdin).get("now", ""))' | |
| } | |
| group_json() { | |
| api_get "/proxies/$GROUP_ENC" | |
| } | |
| delay_json() { | |
| api_get "/group/$GROUP_ENC/delay?timeout=$TIMEOUT_MS&url=$(python3 -c 'import sys, urllib.parse; print(urllib.parse.quote(sys.argv[1], safe=""))' "$TEST_URL | |
| ")" | |
| } | |
| show_current() { | |
| local now | |
| now="$(current_node)" | |
| message "当前策略组: $GROUP | |
| 当前节点: $now | |
| 代理地址: | |
| http_proxy=http://127.0.0.1:7892 | |
| https_proxy=http://127.0.0.1:7892" | |
| } | |
| show_nodes() { | |
| local tmp | |
| tmp="$(mktemp)" | |
| trap 'rm -f "$tmp"' RETURN | |
| group_json | python3 -c ' | |
| import sys, json | |
| d = json.load(sys.stdin) | |
| now = d.get("now", "") | |
| for i, name in enumerate(d.get("all", []), 1): | |
| mark = "*" if name == now else " " | |
| print(f"{mark} {i:02d}. {name}") | |
| ' > "$tmp" | |
| whiptail --title "所有节点 (* 为当前)" --textbox "$tmp" 30 100 | |
| } | |
| test_nodes() { | |
| local tmp | |
| tmp="$(mktemp)" | |
| trap 'rm -f "$tmp"' RETURN | |
| { | |
| echo "正在测试节点延迟,timeout=${TIMEOUT_MS}ms ..." | |
| echo | |
| delay_json | python3 -c ' | |
| import sys, json | |
| d = json.load(sys.stdin) | |
| for name, delay in sorted(d.items(), key=lambda item: item[1]): | |
| print(f"{delay:>5} ms {name}") | |
| ' | |
| } > "$tmp" | |
| whiptail --title "节点延迟" --textbox "$tmp" 30 100 | |
| } | |
| choose_node() { | |
| local delays now menu tmp group_tmp choice | |
| tmp="$(mktemp)" | |
| group_tmp="$(mktemp)" | |
| trap 'rm -f "$tmp" "$group_tmp"' RETURN | |
| now="$(current_node)" | |
| if whiptail --title "测试延迟" --yesno "是否先测试所有节点延迟并按延迟排序?" 10 60; then | |
| delays="$(delay_json)" | |
| else | |
| delays='{}' | |
| fi | |
| group_json > "$group_tmp" | |
| python3 - "$now" "$delays" "$group_tmp" > "$tmp" <<'PY' | |
| import sys, json | |
| now = sys.argv[1] | |
| delays = json.loads(sys.argv[2]) | |
| with open(sys.argv[3], encoding="utf-8") as f: | |
| d = json.load(f) | |
| def is_info(name): | |
| markers = ("剩余流量", "距离下次重置", "套餐到期", "官网更新软件", "每天更新一次订阅") | |
| return any(m in name for m in markers) | |
| names = d.get("all", []) | |
| names.sort(key=lambda n: (is_info(n), delays.get(n, 10**9), n)) | |
| for name in names: | |
| delay = delays.get(name) | |
| prefix = "* " if name == now else " " | |
| suffix = "info" if is_info(name) else ("--" if delay is None else f"{delay} ms") | |
| print(name) | |
| print(f"{prefix}{suffix}") | |
| PY | |
| mapfile -t menu < "$tmp" | |
| choice="$(whiptail --title "选择节点" --menu "当前节点: $now" 30 100 20 "${menu[@]}" 3>&1 1>&2 2>&3)" || return 0 | |
| python3 - "$choice" <<'PY' | api_put_json "/proxies/$GROUP_ENC" >/dev/null | |
| import sys, json | |
| print(json.dumps({"name": sys.argv[1]}, ensure_ascii=False)) | |
| PY | |
| message "已切换到: | |
| $choice" | |
| } | |
| update_subscription() { | |
| if ! whiptail --title "更新订阅" --yesno "将执行 update-subscription.sh 并重启 mihomo 服务,继续?" 10 70; then | |
| return 0 | |
| fi | |
| local tmp | |
| tmp="$(mktemp)" | |
| trap 'rm -f "$tmp"' RETURN | |
| if "$ROOT_DIR/update-subscription.sh" >"$tmp" 2>&1 && systemctl --user restart mihomo >>"$tmp" 2>&1; then | |
| echo >>"$tmp" | |
| echo "Done." >>"$tmp" | |
| else | |
| echo >>"$tmp" | |
| echo "Failed." >>"$tmp" | |
| fi | |
| whiptail --title "更新订阅结果" --textbox "$tmp" 20 90 | |
| } | |
| main() { | |
| api_get /version >/dev/null || { | |
| echo "Cannot connect to Mihomo API: $API" >&2 | |
| echo "Try: systemctl --user start mihomo" >&2 | |
| exit 1 | |
| } | |
| while true; do | |
| local action | |
| action="$(whiptail --title "Clash/Mihomo TUI" --menu "策略组: $GROUP" 18 72 8 \ | |
| current "查看当前节点" \ | |
| list "查看所有节点" \ | |
| test "测试所有节点延迟" \ | |
| choose "手动选择节点" \ | |
| update "更新订阅并重启" \ | |
| quit "退出" \ | |
| 3>&1 1>&2 2>&3)" || break | |
| case "$action" in | |
| current) show_current ;; | |
| list) show_nodes ;; | |
| test) test_nodes ;; | |
| choose) choose_node ;; | |
| update) update_subscription ;; | |
| quit) break ;; | |
| esac | |
| done | |
| } | |
| main "$@" |
growvv
commented
May 20, 2026
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment