From 2202bbf7c5e4380491359e62bd30df1bbaba0dd8 Mon Sep 17 00:00:00 2001 From: Arvin <62139570+wnlen@users.noreply.github.com> Date: Fri, 24 Apr 2026 01:13:07 +0800 Subject: [PATCH] fix(select): block manual node picking for auto strategy groups --- scripts/core/clashctl.sh | 141 +++++++++++++++++++++++++++++++- scripts/core/proxy.sh | 168 +++++++++++++++++++++++++++++++++++---- 2 files changed, 288 insertions(+), 21 deletions(-) diff --git a/scripts/core/clashctl.sh b/scripts/core/clashctl.sh index ad845e3..aa7bff4 100644 --- a/scripts/core/clashctl.sh +++ b/scripts/core/clashctl.sh @@ -6191,7 +6191,7 @@ cmd_select() { if [ -z "${1:-}" ]; then print_select_context || return $? - proxy_select_interactive + proxy_select_interactive_guarded return 0 fi @@ -6217,7 +6217,7 @@ cmd_proxy_groups() { [ -n "${group:-}" ] || continue found="true" type="$(proxy_group_type "$group" 2>/dev/null || echo "unknown")" - current="$(proxy_group_current "$group" 2>/dev/null || echo "-")" + current="$(proxy_group_current "$group" 2>/dev/null || true)" printf ' 📦 %-20s %-12s %s\n' "$group" "$type" "$current" done < <(proxy_group_list) @@ -6333,14 +6333,14 @@ proxy_pick_index() { } proxy_pick_group_interactive() { - local idx count group current + local idx count group current type local -a groups=() local -a ordered_groups=() while IFS= read -r group; do [ -n "${group:-}" ] || continue groups+=("$group") - done < <(proxy_group_manual_list) + done < <(proxy_group_display_list) for group in "节点选择" "自动选择"; do if printf '%s\n' "${groups[@]}" | grep -Fxq "$group"; then @@ -6543,6 +6543,139 @@ proxy_select_interactive() { print_select_feedback "$group" } +proxy_pick_group_for_select() { + local idx count group current type + local -a groups=() + local -a ordered_groups=() + + while IFS= read -r group; do + [ -n "${group:-}" ] || continue + groups+=("$group") + done < <(proxy_group_display_list) + + for group in "节点选择" "自动选择"; do + if printf '%s\n' "${groups[@]}" | grep -Fxq "$group"; then + ordered_groups+=("$group") + fi + done + + for group in "${groups[@]}"; do + case "$group" in + 节点选择|自动选择) + continue + ;; + esac + ordered_groups+=("$group") + done + + groups=("${ordered_groups[@]}") + + count="${#groups[@]}" + [ "$count" -gt 0 ] || die "📭 暂无可切换策略组" + + echo "📦 请选择策略组:" >&2 + echo "💡 通常优先选择:节点选择" >&2 + idx=1 + for group in "${groups[@]}"; do + current="$(proxy_group_current "$group" 2>/dev/null || true)" + type="$(proxy_group_type_label "$group" 2>/dev/null || echo "unknown")" + [ -n "${current:-}" ] || current="" + printf ' %s) 📦 %s [%s] -> 🚀 %s\n' "$idx" "$group" "$type" "$current" >&2 + idx=$((idx + 1)) + done + echo " q) 退出" >&2 + echo >&2 + + idx="$(proxy_pick_index "$count")" || return 1 + echo "${groups[$((idx - 1))]}" +} + +print_proxy_group_manual_pick_blocked() { + local group="$1" + local current type message + + message="$(proxy_group_manual_pick_error_message "$group" 2>/dev/null || echo "该策略组不支持手动切换:$group")" + current="$(proxy_group_current "$group" 2>/dev/null || true)" + type="$(proxy_group_type_label "$group" 2>/dev/null || echo "unknown")" + + ui_warn "$message" + ui_kv "📦" "策略组" "$group" + ui_kv "🧭" "策略组类型" "$type" + if [ -n "${current:-}" ]; then + ui_kv "🚀" "当前节点" "$current" + else + ui_kv "🚀" "当前节点" "" + fi + ui_blank +} + +proxy_select_interactive_guarded() { + local group="${1:-}" + local current idx count total_count node selected_node + local -a nodes=() + + prepare + + if ! status_is_running; then + die_state "代理内核未运行" "clashon" + fi + + if ! proxy_controller_reachable 2>/dev/null; then + die_state "控制器不可访问" "clashctl doctor" + fi + + if [ -z "${group:-}" ]; then + ui_title "🚀 节点切换" + group="$(proxy_pick_group_for_select)" || return 0 + fi + + proxy_group_exists "$group" || die "策略组不存在:$group" + + if ! proxy_group_supports_manual_pick "$group"; then + print_proxy_group_manual_pick_blocked "$group" + return 0 + fi + + current="$(proxy_group_current "$group" 2>/dev/null || true)" + + while IFS= read -r node; do + [ -n "${node:-}" ] || continue + nodes+=("$node") + done < <(proxy_group_selectable_nodes "$group") + + count="${#nodes[@]}" + [ "$count" -gt 0 ] || die "📭 当前策略组没有候选节点:$group" + total_count="$(select_total_node_count 2>/dev/null || true)" + + echo + echo "📦 当前策略组:$group" + [ -n "${current:-}" ] && echo "🚀 当前节点:$current" + echo "📦 当前策略组候选数:$count" + [ -n "${total_count:-}" ] && echo "🔢 全部节点总数:$total_count" + echo "ℹ️ 以下仅显示当前策略组可切换节点" + echo + echo "🚀 请选择节点:" + + idx=1 + for node in "${nodes[@]}"; do + if [ "$node" = "$current" ]; then + printf ' %s) 🚀 %s [当前]\n' "$idx" "$node" + else + printf ' %s) 🚀 %s\n' "$idx" "$node" + fi + idx=$((idx + 1)) + done + + echo " q) 退出" + echo + + idx="$(proxy_pick_index "$count")" || return 0 + selected_node="${nodes[$((idx - 1))]}" + + proxy_group_select "$group" "$selected_node" + print_select_feedback "$group" +} + cmd_proxy() { echo "⚠ 当前版本不提供 proxy 子命令" echo "👉 使用 clashon / clashoff 控制代理" diff --git a/scripts/core/proxy.sh b/scripts/core/proxy.sh index 13e9510..536c110 100644 --- a/scripts/core/proxy.sh +++ b/scripts/core/proxy.sh @@ -278,13 +278,50 @@ proxy_group_type() { proxy_groups_json | "$(yq_bin)" -p=json eval ".proxies.\"$group\".type // \"\"" - 2>/dev/null } -proxy_group_is_selector() { +proxy_group_type_key() { local type - type="$(proxy_group_type "$1")" + type="$(proxy_group_type "$1" 2>/dev/null || true)" + printf '%s' "${type:-}" | tr '[:upper:]' '[:lower:]' | tr -d '[:space:]_-' +} - case "$type" in - Selector|URLTest|Fallback|LoadBalance) +proxy_group_type_label() { + local group="$1" + local type_key raw_type + + type_key="$(proxy_group_type_key "$group")" + + case "$type_key" in + selector) + echo "select" + ;; + urltest) + echo "自动选择 / url-test" + ;; + fallback) + echo "故障转移 / fallback" + ;; + loadbalance) + echo "负载均衡 / load-balance" + ;; + *) + raw_type="$(proxy_group_type "$group" 2>/dev/null || true)" + if [ -n "${raw_type:-}" ] && [ "$raw_type" != "null" ]; then + echo "$raw_type" + else + echo "unknown" + fi + ;; + esac +} + +proxy_group_can_show_candidates() { + local type_key + + type_key="$(proxy_group_type_key "$1")" + + case "$type_key" in + selector|urltest|fallback|loadbalance) return 0 ;; *) @@ -293,22 +330,25 @@ proxy_group_is_selector() { esac } +proxy_group_is_selector() { + proxy_group_can_show_candidates "$1" +} + proxy_group_is_manual_selector() { proxy_group_supports_manual_pick "$1" } proxy_group_is_auto_managed() { local group="$1" - local type normalized_type + local type_key [ -n "${group:-}" ] || return 0 proxy_group_exists "$group" || return 0 - type="$(proxy_group_type "$group" 2>/dev/null || true)" - normalized_type="$(printf '%s' "${type:-}" | tr '[:upper:]' '[:lower:]')" + type_key="$(proxy_group_type_key "$group")" - case "$normalized_type" in - urltest|url-test|fallback|loadbalance|load-balance) + case "$type_key" in + urltest|fallback|loadbalance) return 0 ;; esac @@ -389,14 +429,54 @@ proxy_group_selectable_nodes() { done < <(proxy_group_nodes "$group") } -proxy_group_supports_manual_pick() { +proxy_group_has_selectable_candidates() { local group="$1" local node - local has_now="" [ -n "${group:-}" ] || return 1 proxy_group_exists "$group" || return 1 - proxy_group_is_auto_managed "$group" && return 1 + proxy_group_can_show_candidates "$group" || return 1 + + while IFS= read -r node; do + [ -n "${node:-}" ] || continue + return 0 + done < <(proxy_group_selectable_nodes "$group") + + return 1 +} + +proxy_group_manual_pick_error_message() { + local group="$1" + local type_key type_label + + [ -n "${group:-}" ] || { + echo "该策略组不支持手动切换" + return 0 + } + + type_key="$(proxy_group_type_key "$group")" + type_label="$(proxy_group_type_label "$group")" + + case "$type_key" in + urltest|fallback|loadbalance) + echo "该策略组为${type_label}类型,不支持手动切换:$group" + ;; + *) + echo "该策略组不支持手动切换:$group" + ;; + esac +} + +proxy_group_supports_manual_pick() { + local group="$1" + local has_now="" + local type_key + + [ -n "${group:-}" ] || return 1 + proxy_group_exists "$group" || return 1 + + type_key="$(proxy_group_type_key "$group")" + [ "$type_key" = "selector" ] || return 1 has_now="$( proxy_groups_json \ @@ -405,12 +485,17 @@ proxy_group_supports_manual_pick() { )" [ "${has_now:-false}" = "true" ] || return 1 - while IFS= read -r node; do - [ -n "${node:-}" ] || continue - return 0 - done < <(proxy_group_selectable_nodes "$group") + proxy_group_has_selectable_candidates "$group" +} - return 1 +proxy_group_display_list() { + local group + + while IFS= read -r group; do + [ -n "${group:-}" ] || continue + proxy_group_has_selectable_candidates "$group" || continue + echo "$group" + done < <(proxy_group_list) } proxy_group_manual_list() { @@ -472,6 +557,55 @@ proxy_group_select() { rm -f "$response_file" 2>/dev/null || true } +proxy_group_select() { + local group="$1" + local node="$2" + local base secret + local code response_file response_body + local available_node found + + [ -n "${group:-}" ] || die "策略组名称不能为空" + [ -n "${node:-}" ] || die "节点名称不能为空" + + proxy_group_exists "$group" || die "策略组不存在:$group" + proxy_group_supports_manual_pick "$group" || die "$(proxy_group_manual_pick_error_message "$group")" + + found=false + while IFS= read -r available_node; do + [ -n "${available_node:-}" ] || continue + if [ "$available_node" = "$node" ]; then + found=true + break + fi + done < <(proxy_group_selectable_nodes "$group") + + if [ "$found" != "true" ]; then + die "节点不存在于策略组中:$group -> $node" + fi + + base="$(controller_api_base)" + secret="$(controller_secret)" + response_file="$(mktemp)" + code="$( + curl -sS -o "$response_file" -w "%{http_code}" -X PUT \ + -H "Content-Type: application/json" \ + ${secret:+-H "Authorization: Bearer $secret"} \ + --data "{\"name\":\"$node\"}" \ + "$base/proxies/$group" + )" + + if [ "${code:-000}" -lt 200 ] || [ "${code:-000}" -ge 300 ]; then + response_body="$(cat "$response_file" 2>/dev/null || true)" + rm -f "$response_file" 2>/dev/null || true + if [ -n "${response_body:-}" ]; then + die "节点切换失败:$response_body" + fi + die "节点切换失败:controller 返回 HTTP $code" + fi + + rm -f "$response_file" 2>/dev/null || true +} + proxy_group_count() { proxy_group_list 2>/dev/null | awk 'NF{c++} END{print c+0}' }