File sing-box-wrapper.sh of Package network-sing-box-master-git

#!/bin/bash

# === 配置 ===
CONF_DIR="/etc/sing-box"
SUB_JSON="$CONF_DIR/subscriptions.json"
TEMPLATE="$CONF_DIR/config_template.json"
TARGET="$CONF_DIR/config.json"
MODE_FILE="$CONF_DIR/mode"
SERVICE="sing-box"
CONV_SERVICE="subconverter" 
API_URL=""

# 构建配置
REPO_DIR="/var/local/arch-repo"
DB_NAME="local-proxy.db.tar.gz"
REAL_USER=${SUDO_USER:-$USER}
SRC_ROOT="/home/$REAL_USER" 
PROJECTS=("clash2singbox-git" "subconverter-optimized-git" "sing-box-master-git")

# 颜色
C_RESET='\033[0m'
C_GREEN='\033[32m'
C_BLUE='\033[34m'
C_YELLOW='\033[33m'
C_RED='\033[31m'
C_GRAY='\033[90m'
C_BOLD='\033[1m'

# === 核心函数 ===
url_encode() {
    local string="${1}"; local strlen=${#string}; local encoded=""
    local pos c o
    for (( pos=0 ; pos<strlen ; pos++ )); do
        c=${string:$pos:1}; case "$c" in [-_.~a-zA-Z0-9] ) o="${c}" ;; * ) printf -v o '%%%02x' "'$c" ;; esac; encoded+="${o}"
    done
    echo "${encoded}"
}

init_files() {
    if [ ! -d "$CONF_DIR" ]; then sudo mkdir -p "$CONF_DIR"; fi
    if [ ! -f "$SUB_JSON" ] || [ ! -s "$SUB_JSON" ]; then echo '[]' | sudo tee "$SUB_JSON" >/dev/null; fi
    if [ ! -f "$MODE_FILE" ]; then echo "tun" | sudo tee "$MODE_FILE" >/dev/null; fi
    sudo chmod 666 "$SUB_JSON" "$MODE_FILE" 2>/dev/null
}

start_converter() {
    if ! systemctl is-active --quiet $CONV_SERVICE; then
        systemctl start $CONV_SERVICE
        for i in {1..50}; do if ss -tuln | grep -q :25500; then break; fi; sleep 0.1; done
    fi
}
stop_converter() { systemctl stop $CONV_SERVICE; }
get_current_mode() { if [ -f "$MODE_FILE" ]; then cat "$MODE_FILE"; else echo "tun"; fi; }

get_api_url() {
    local ctrl host port
    ctrl=$(jq -r '.experimental.clash_api.external_controller // .clash_api.external_controller // empty' "$TARGET" 2>/dev/null)
    if [ -n "$ctrl" ]; then
        host="${ctrl%%:*}"
        port="${ctrl##*:}"
        if [ "$host" = "0.0.0.0" ] || [ "$host" = "::" ] || [ "$host" = "[::]" ]; then
            host="127.0.0.1"
        fi
        echo "http://${host}:${port}"
        return
    fi

    # If config is unreadable for non-root user, probe common API ports.
    if curl --noproxy "*" -s -m 1 "http://127.0.0.1:9091/version" >/dev/null 2>&1; then
        echo "http://127.0.0.1:9091"
        return
    fi
    if curl --noproxy "*" -s -m 1 "http://127.0.0.1:9090/version" >/dev/null 2>&1; then
        echo "http://127.0.0.1:9090"
        return
    fi

    # Prefer 9091 as default for this wrapper.
    echo "http://127.0.0.1:9091"
}

_migrate_legacy_config_with_jq() {
    local input_file="$1"
    local output_file="$2"
    jq '
      def to_rcode($raw):
        ($raw | ascii_downcase) as $v |
        if $v == "success" then "NOERROR"
        elif $v == "format_error" then "FORMERR"
        elif $v == "server_failure" then "SERVFAIL"
        elif $v == "name_error" then "NXDOMAIN"
        elif $v == "not_implemented" then "NOTIMP"
        elif $v == "refused" then "REFUSED"
        else "NOERROR" end;

      def normalize_dns_server:
        if (.address? | type != "string") then .
        else
          . as $s |
          (if has("address_resolver") then .domain_resolver = .address_resolver else . end) |
          (if has("address_strategy") then .domain_strategy = .address_strategy else . end) |
          (if (has("strategy") and (has("domain_strategy") | not)) then .domain_strategy = .strategy else . end) |
          (
            if $s.address == "local" then .type = "local"
            elif ($s.address | startswith("tls://")) then .type = "tls" | .server = ($s.address | sub("^tls://"; ""))
            elif ($s.address | startswith("tcp://")) then .type = "tcp" | .server = ($s.address | sub("^tcp://"; ""))
            elif ($s.address | startswith("udp://")) then .type = "udp" | .server = ($s.address | sub("^udp://"; ""))
            elif ($s.address | startswith("https://")) then
              ($s.address | sub("^https://"; "")) as $raw |
              ($raw | split("/")) as $parts |
              .type = "https" |
              .server = ($parts[0]) |
              (if (($parts | length) > 1) then .path = ("/" + ($parts[1:] | join("/"))) else . end)
            elif ($s.address | startswith("h3://")) then
              ($s.address | sub("^h3://"; "")) as $raw |
              ($raw | split("/")) as $parts |
              .type = "h3" |
              .server = ($parts[0]) |
              (if (($parts | length) > 1) then .path = ("/" + ($parts[1:] | join("/"))) else . end)
            elif ($s.address | startswith("quic://")) then .type = "quic" | .server = ($s.address | sub("^quic://"; ""))
            else .type = "udp" | .server = $s.address
            end
          ) |
          del(.address, .address_resolver, .address_strategy, .strategy)
        end;

      (.dns.servers // []) as $servers |
      (
        $servers
        | map(select((.address? | type == "string") and (.address | startswith("rcode://"))))
        | map({key: (.tag // ""), value: to_rcode((.address | sub("^rcode://"; "")))})
        | map(select(.key != ""))
        | from_entries
      ) as $rcode_by_tag |

      .dns.servers = (
        $servers
        | map(
            if ((.address? | type == "string") and (.address | startswith("rcode://"))) then
              empty
            else
              normalize_dns_server
            end
          )
      ) |
      .dns.rules = (
        (.dns.rules // [])
        | map(
            if ((.server? != null) and ($rcode_by_tag[.server] != null)) then
              del(.server, .outbound) + {action: "predefined", rcode: $rcode_by_tag[.server]}
            else
              if has("outbound") then del(.outbound) else . end
            end
          )
      ) |

      (.outbounds // []) as $outs |
      ($outs | map(select((.type? == "dns") or (.type? == "block"))) | map(.tag) | map(select(type == "string" and . != ""))) as $legacy_out_tags |
      .outbounds = ($outs | map(select((.type? != "dns") and (.type? != "block")))) |
      .route.rules = (
        (.route.rules // [])
        | map(
            (.outbound // "") as $ob |
            if ($ob != "" and ($legacy_out_tags | index($ob) != null)) then
              if ((.protocol? == "dns") or ((.protocol? | type == "array") and (.protocol | index("dns") != null))) then
                del(.outbound) + {protocol: "dns", action: "hijack-dns"}
              else
                del(.outbound) + {action: "reject"}
              end
            else
              .
            end
          )
      ) |

      .route.default_domain_resolver = (.route.default_domain_resolver // "local")
    ' "$input_file" > "$output_file"
}

migrate_legacy_config_in_place() {
    local input_file="${1:-$TARGET}"
    [ -f "$input_file" ] || return 0
    local migrated_file
    migrated_file=$(mktemp)
    if ! _migrate_legacy_config_with_jq "$input_file" "$migrated_file"; then
        rm -f "$migrated_file"
        echo -e "${C_RED}❌ 配置迁移失败: $input_file${C_RESET}"
        return 1
    fi
    if ! cmp -s "$input_file" "$migrated_file"; then
        local backup_file="${input_file}.bak.$(date +%Y%m%d-%H%M%S)"
        cp -f "$input_file" "$backup_file" 2>/dev/null || true
        mv "$migrated_file" "$input_file"
        echo -e "${C_YELLOW}⚠ 发现旧版配置,已自动迁移 (备份: $backup_file)${C_RESET}"
    else
        rm -f "$migrated_file"
    fi
}

# === 节点选择 (小窗版) ===
select_node() {
    local target_node="$1"
    if ! command -v fzf &> /dev/null; then echo -e "${C_RED}❌ 请安装 fzf (sudo pacman -S fzf)${C_RESET}"; exit 1; fi
    
    # 检查服务,自动尝试启动
    if ! systemctl is-active --quiet $SERVICE; then
        echo -e "${C_YELLOW}⚠ 服务未运行,尝试启动...${C_RESET}"
        if [ "$EUID" -ne 0 ]; then
            if ! sudo "$0" on >/dev/null; then echo -e "${C_RED}❌ 启动失败${C_RESET}"; exit 1; fi
        else
            migrate_legacy_config_in_place "$TARGET" || exit 1
            systemctl start $SERVICE
        fi
    fi

    API_URL="$(get_api_url)"
    local retries=0
    while ! curl --noproxy "*" -s -m 1 "${API_URL}/version" >/dev/null; do
        sleep 0.2; ((retries++)); if [ "$retries" -gt 100 ]; then echo -e "${C_RED}❌ API 超时${C_RESET}"; exit 1; fi
    done

    local group="proxy"
    local json_data=$(curl --noproxy "*" -s -m 2 "${API_URL}/proxies/${group}")
    if [[ -z "$json_data" ]]; then echo -e "${C_RED}❌ API 无响应${C_RESET}"; exit 1; fi

    local current=$(echo "$json_data" | jq -r '.now')
    
    # 快捷切换
    if [ -n "$target_node" ]; then
        local exists=$(echo "$json_data" | jq -r --arg n "$target_node" '.all[] | select(. == $n)')
        if [ -z "$exists" ]; then echo -e "${C_RED}❌ 节点不存在: $target_node${C_RESET}"; exit 1; fi
        if [ "$target_node" == "$current" ]; then echo -e "${C_BLUE}⚡ 当前已是该节点${C_RESET}"; else
            curl --noproxy "*" -s -o /dev/null -X PUT "${API_URL}/proxies/${group}" -H "Content-Type: application/json" -d "{\"name\": \"$target_node\"}"
            echo -e "${C_GREEN}✅ 已切换至: $target_node${C_RESET}"
        fi; return
    fi

    # UI 界面
    local nodes=$(echo "$json_data" | jq -r '.all[]')
    local selected=$(echo "$nodes" | fzf \
        --prompt="🔥 $current > " --height=40% --layout=reverse --border=rounded --margin=5%,20% --info=hidden --no-scrollbar --cycle \
        --color=fg:white,bg:-1,hl:blue,fg+:green,bg+:-1,hl+:blue --bind 'q:abort,esc:abort')

    if [ -n "$selected" ] && [ "$selected" != "$current" ]; then
        curl --noproxy "*" -s -o /dev/null -X PUT "${API_URL}/proxies/${group}" -H "Content-Type: application/json" -d "{\"name\": \"$selected\"}"
        echo -e "${C_GREEN}✅ 已切换至: $selected${C_RESET}"
    fi
}

# === 系统升级 ===
system_upgrade() {
    if [ "$EUID" -ne 0 ]; then exec sudo "$0" upgrade; exit; fi
    echo -e "${C_BLUE}🏭 启动智能构建流水线...${C_RESET}"; local UPDATED=false
    for proj in "${PROJECTS[@]}"; do
        TARGET_DIR="$SRC_ROOT/$proj"; if [ ! -d "$TARGET_DIR" ]; then echo -e "${C_RED}❌ 找不到: $TARGET_DIR${C_RESET}"; continue; fi
        echo -e "${C_BLUE}>> 检查: $proj${C_RESET}"; GIT_DIR=$(find "$TARGET_DIR/src" -maxdepth 2 -name ".git" -type d 2>/dev/null | head -n 1 | xargs dirname)
        NEED_BUILD=false; if [ -z "$GIT_DIR" ]; then echo -e "${C_YELLOW}   未找到源码缓存${C_RESET}"; NEED_BUILD=true; else
            cd "$GIT_DIR"; git config --global --add safe.directory "$GIT_DIR" 2>/dev/null
            REMOTE_HASH=$(sudo -u "$REAL_USER" git ls-remote "$(git config --get remote.origin.url)" HEAD | awk '{print $1}')
            LOCAL_HASH=$(sudo -u "$REAL_USER" git rev-parse HEAD)
            if [ -z "$REMOTE_HASH" ]; then echo -e "${C_YELLOW}   ⚠ 网络查询失败${C_RESET}"; NEED_BUILD=true
            elif [ "$LOCAL_HASH" != "$REMOTE_HASH" ]; then echo -e "${C_GREEN}   🔥 发现更新 ($REMOTE_HASH)${C_RESET}"; NEED_BUILD=true
            else echo -e "${C_GREEN}   ✅ 已是最新${C_RESET}"; fi
        fi
        if [ "$NEED_BUILD" = true ]; then
            cd "$TARGET_DIR"; chown -R "$REAL_USER:$REAL_USER" "$TARGET_DIR"
            if sudo -u "$REAL_USER" makepkg -fsC --noconfirm; then
                echo -e "${C_GREEN}   🔨 编译成功${C_RESET}"; pkg_file=$(ls -t "$TARGET_DIR"/*.pkg.tar.zst | grep -v "debug" | head -n1)
                mv "$pkg_file" "$REPO_DIR/"; repo-add -n -R "$REPO_DIR/$DB_NAME" "$REPO_DIR/$(basename "$pkg_file")"; UPDATED=true
            else echo -e "${C_RED}❌ 失败${C_RESET}"; exit 1; fi
        fi
    done
    if [ "$UPDATED" = true ]; then echo -e "${C_BLUE}>> 安装更新...${C_RESET}"; pacman -Sy --noconfirm "${PROJECTS[@]}"; systemctl restart $SERVICE; echo -e "${C_GREEN}🎉 升级完成${C_RESET}"; else echo -e "${C_GREEN}☕ 无需更新${C_RESET}"; fi
}

# === 订阅管理 UI ===
manage_subs() {
    if [ "$EUID" -ne 0 ]; then exec sudo "$0" sub; exit; fi
    init_files
    while true; do
        clear; echo -e "${C_BOLD}${C_BLUE}=== 📡 订阅管理 ===${C_RESET}"
        local list=$(jq -r 'to_entries | .[] | "📄 \(.value.name)"' "$SUB_JSON")
        local menu="➕ 添加新订阅\n🔄 立即更新配置\n⚙️ 自动更新设置\n🚪 退出"
        if [ -n "$list" ]; then menu="$menu\n$list"; fi
        local selection=$(echo -e "$menu" | fzf --prompt="请选择 > " --height=50% --layout=reverse --border=rounded --info=hidden --no-scrollbar --header=" " --bind 'q:abort,esc:abort')
        if [ -z "$selection" ]; then break; fi
        case "$selection" in
            *"添加新订阅"*) add_sub_ui ;;
            *"立即更新"*) generate_config; read -p "按回车...";;
            *"自动更新"*) setup_timer_ui ;;
            *"退出"*) break ;;
            *"📄"*) local name=$(echo "$selection" | cut -d' ' -f2-); local id=$(jq -r --arg n "$name" 'to_entries | .[] | select(.value.name == $n) | .key' "$SUB_JSON" | head -n1); if [[ "$id" =~ ^[0-9]+$ ]]; then sub_detail_ui "$id"; fi ;;
        esac
    done
}

sub_detail_ui() {
    local id="$1"
    while true; do
        local name=$(jq -r ".[$id].name" "$SUB_JSON"); local url=$(jq -r ".[$id].url" "$SUB_JSON"); local auto=$(jq -r ".[$id].auto_update" "$SUB_JSON")
        local status_icon="✅"; if [ "$auto" != "true" ]; then status_icon="⛔"; fi
        clear; echo -e "${C_BOLD}${C_BLUE}📄 $name${C_RESET}\n"; echo -e "🔗 URL:  ${C_GRAY}${url:0:50}...${C_RESET}"; echo -e "⏰ 自动更新: $status_icon"; echo -e "──────────────────────────────"
        local op=$(echo -e "↻ 立即更新此订阅\n✏️ 重命名\n🔗 修改链接\n🔄 切换自动更新\n🗑️ 删除此订阅\n🔙 返回上级" | fzf --prompt="操作 > " --height=40% --layout=reverse --border=rounded --info=hidden)
        local tmp=$(mktemp)
        case "$op" in
            "↻"*) update_single_sub "$name"; read -p "按回车..." ;;
            "✏️"*) echo -ne "\n新名称: "; read nn; if [ -n "$nn" ]; then jq ".[$id].name=\"$nn\"" "$SUB_JSON" > "$tmp" && mv "$tmp" "$SUB_JSON"; fi ;;
            "🔗"*) echo -ne "\n新链接: "; read nu; if [ -n "$nu" ]; then jq ".[$id].url=\"$nu\"" "$SUB_JSON" > "$tmp" && mv "$tmp" "$SUB_JSON"; fi ;;
            "🔄"*) jq ".[$id].auto_update = (if .[$id].auto_update then false else true end)" "$SUB_JSON" > "$tmp" && mv "$tmp" "$SUB_JSON" ;;
            "🗑️"*) echo -ne "\n${C_RED}确定删除? (y/n): ${C_RESET}"; read confirm; if [[ "$confirm" == "y" ]]; then jq "del(.[$id])" "$SUB_JSON" > "$tmp" && mv "$tmp" "$SUB_JSON"; return; fi ;;
            "🔙"*|"") return ;;
        esac; sudo chmod 666 "$SUB_JSON"
    done
}

add_sub_ui() {
    clear; echo -e "${C_BLUE}=== ➕ 添加订阅 ===${C_RESET}"
    echo -ne "名称: "; read name; if [[ -z "$name" ]]; then return; fi
    echo -ne "链接: "; read url; if [[ -z "$url" ]]; then return; fi
    echo -ne "自动更新? (y/n) [y]: "; read auto_up; local auto_bool="true"; if [[ "$auto_up" == "n" ]]; then auto_bool="false"; fi
    init_files; if [ -f "$url" ]; then url="file://$(realpath "$url")"; fi
    local tmp=$(mktemp); jq --arg n "$name" --arg u "$url" --argjson a "$auto_bool" '. += [{"name": $n, "url": $u, "auto_update": $a}]' "$SUB_JSON" > "$tmp" && mv "$tmp" "$SUB_JSON"
    sudo chmod 666 "$SUB_JSON"; echo -e "\n${C_GREEN}✅ 已添加${C_RESET}"; read -p "按回车返回..."
}

setup_timer_ui() {
    clear; echo -e "${C_BOLD}${C_YELLOW}⚙  自动更新设置${C_RESET}\n"
    local is_active="no"; if systemctl is-enabled sing-box-update.timer &>/dev/null; then is_active="yes"; fi
    if [ "$is_active" == "yes" ]; then
        echo -e "当前状态: ${C_GREEN}已启用${C_RESET}"; echo -ne "是否关闭? (y/n): "; read confirm
        if [[ "$confirm" == "y" ]]; then systemctl disable --now sing-box-update.timer; echo -e "${C_RED}已关闭${C_RESET}"; fi
    else
        echo -e "当前状态: ${C_GRAY}未启用${C_RESET}"; echo -ne "是否开启? (y/n): "; read confirm
        if [[ "$confirm" == "y" ]]; then
            echo -ne "每天几点? (HH:MM): "; read time_val; if [[ ! "$time_val" =~ ^[0-9]{2}:[0-9]{2}$ ]]; then echo "格式错误"; sleep 1; return; fi
            mkdir -p /etc/systemd/system; cat > /etc/systemd/system/sing-box-update.timer <<EOF
[Unit]
Description=Daily update
[Timer]
OnCalendar=*-*-* $time_val:00
Persistent=true
[Install]
WantedBy=timers.target
EOF
            cat > /etc/systemd/system/sing-box-update.service <<EOF
[Unit]
Description=Update Sing-box
After=network.target
[Service]
Type=oneshot
ExecStart=/usr/bin/sing-box update-auto
EOF
            systemctl daemon-reload; systemctl enable --now sing-box-update.timer; echo -e "${C_GREEN}已启用${C_RESET}"
        fi
    fi; read -p "按回车..."
}

add_subscription_cli() {
    local url="$1"; local name="${2:-Default}"; init_files
    if [ -f "$url" ]; then url="file://$(realpath "$url")"; fi
    local exists=$(jq --arg u "$url" 'map(select(.url == $u)) | length' "$SUB_JSON")
    local tmp=$(mktemp)
    if [ "$exists" -gt 0 ]; then jq --arg u "$url" --arg n "$name" 'map(if .url == $u then .name = $n else . end)' "$SUB_JSON" > "$tmp"
    else jq --arg u "$url" --arg n "$name" '. += [{"name": $n, "url": $u, "auto_update": true}]' "$SUB_JSON" > "$tmp"; fi
    sudo mv "$tmp" "$SUB_JSON"; sudo chmod 666 "$SUB_JSON"; generate_config
}

update_single_sub() {
    local name="$1"
    local id=$(jq -r --arg n "$name" 'to_entries | .[] | select(.value.name == $n) | .key' "$SUB_JSON" | head -n1)
    if [ -z "$id" ]; then echo -e "${C_RED}❌ 找不到: $name${C_RESET}"; return 1; fi
    echo -e "${C_BLUE}>> 更新: $name ...${C_RESET}"; generate_config
}

generate_config() {
    local auto_only="$1"
    if [ "$EUID" -ne 0 ]; then exec sudo "$0" update; exit; fi
    init_files; local mode=$(get_current_mode); local all_nodes_file="/tmp/all_nodes.json"; local temp_target="/tmp/config_gen_temp.json"
    
    echo "[]" > "$all_nodes_file"
    local count=$(jq '. | length' "$SUB_JSON")
    if [ "$count" -eq 0 ]; then echo -e "${C_RED}❌ 无订阅${C_RESET}"; return 1; fi

    [ -z "$auto_only" ] && echo -e "${C_BLUE}>> [1/3] 处理订阅 ($count 个)...${C_RESET}"
    local valid_count=0
    local curl_opts=(-L --retry 3 -s -A "Clash.Meta/2.11" --noproxy "*")
    for ((i=0; i<count; i++)); do
        local name=$(jq -r ".[$i].name" "$SUB_JSON"); local url=$(jq -r ".[$i].url" "$SUB_JSON"); local auto=$(jq -r ".[$i].auto_update" "$SUB_JSON")
        if [[ "$auto_only" == "true" && "$auto" != "true" ]]; then continue; fi
        local tmp_yaml="/tmp/sub_raw_$i.yaml"; local tmp_json="/tmp/sub_out_$i.json"
        
        [ -z "$auto_only" ] && echo -e "   ⬇️  $name"
        if [[ "$url" == file://* ]]; then local path=${url#file://}; if [ -f "$path" ]; then cp "$path" "$tmp_yaml"; else echo "      ❌ 丢失"; continue; fi
        else
            local orig_url="$url"
            curl "${curl_opts[@]}" -o "$tmp_yaml" "$orig_url"
            if ! grep -q "proxies:" "$tmp_yaml" || grep -q "^proxies:[[:space:]]*\[\]" "$tmp_yaml"; then
                if [[ "$orig_url" != *"flag="* ]]; then
                    if [[ "$orig_url" == *"?"* ]]; then
                        url="${orig_url}&flag=clash"
                    else
                        url="${orig_url}?flag=clash"
                    fi
                    curl "${curl_opts[@]}" -o "$tmp_yaml" "$url"
                fi
            else
                url="$orig_url"
            fi
        fi
        
        if ! grep -q "proxies:" "$tmp_yaml"; then
            [ -z "$auto_only" ] && echo "      ⚠️  清洗..."
            start_converter; local enc=$(url_encode "$url")
            curl -s --noproxy "*" "http://127.0.0.1:9090/sub?target=clash&url=${enc}&insert=false&emoji=true&list=true" -o "$tmp_yaml.clean" || curl -s --noproxy "*" "http://127.0.0.1:25500/sub?target=clash&url=${enc}&insert=false&emoji=true&list=true" -o "$tmp_yaml.clean"
            stop_converter; mv "$tmp_yaml.clean" "$tmp_yaml"
        fi
        
        if grep -q "proxies:" "$tmp_yaml"; then
            clash2singbox -i "$tmp_yaml" -o "$tmp_json"
            if [ -s "$tmp_json" ]; then
                local tmp_merge=$(mktemp)
                if jq -s '.[0] + (.[1].outbounds | map(select(.type != "selector" and .type != "urltest" and .type != "direct" and .type != "block" and .type != "dns")))' "$all_nodes_file" "$tmp_json" > "$tmp_merge"; then
                    mv "$tmp_merge" "$all_nodes_file"; ((valid_count++))
                else echo -e "${C_RED}      ❌ 格式错误${C_RESET}"; fi
            fi
        else echo -e "${C_RED}      ❌ 无效${C_RESET}"; fi
    done

    if [ "$valid_count" -eq 0 ]; then echo -e "${C_RED}❌ 无有效节点${C_RESET}"; return 1; fi
    [ -z "$auto_only" ] && echo -e "${C_BLUE}>> [2/3] 生成配置...${C_RESET}"
    
    jq -s --arg mode "$mode" '
      (.[0] | del(.experimental.clash_api.cache_file)) as $tmpl | .[1] as $nodes |
      ($nodes | map(.tag)) as $node_tags |
      ($nodes | map(.server? // empty) | map(select(type == "string" and . != "")) | unique) as $server_hosts |
      $tmpl | (if $mode == "proxy" then del(.inbounds[] | select(.type == "tun")) else . end) |
      .outbounds += $nodes |
      .outbounds |= map(if .tag == "proxy" then .outbounds = (["auto"] + $node_tags) elif .tag == "auto" then .outbounds = $node_tags else . end) |
      (if ($server_hosts | length) > 0 then .dns.rules = ([ $server_hosts[] | {domain: [.] , server: "local"} ] + (.dns.rules // [])) else . end)
    ' "$TEMPLATE" "$all_nodes_file" > "$temp_target"
    
    if [ $? -eq 0 ] && [ -s "$temp_target" ]; then
        local migrated_temp
        migrated_temp=$(mktemp)
        if ! _migrate_legacy_config_with_jq "$temp_target" "$migrated_temp"; then
            rm -f "$migrated_temp" "$temp_target"
            echo -e "${C_RED}❌ 配置兼容迁移失败${C_RESET}"
            return 1
        fi
        mv "$migrated_temp" "$temp_target"
        mv "$temp_target" "$TARGET"; systemctl restart $SERVICE
        [ -z "$auto_only" ] && echo -e "${C_GREEN}✅ 更新成功 ($valid_count 个订阅)${C_RESET}"
    else echo -e "${C_RED}❌ 失败${C_RESET}"; exit 1; fi
}

# --- CLI 入口 ---
if [[ "$1" =~ ^(select|s|status|logs|help)$ ]] || [ -z "$1" ]; then
    case "$1" in status) systemctl status $SERVICE --no-pager ;; logs) journalctl -u $SERVICE -f -n 20 ;; *) select_node "$2" ;; esac; exit 0
fi
if [ "$EUID" -ne 0 ]; then exec sudo "$0" "$@"; exit; fi
case "$1" in
    on|start)
        if [[ "$2" == "tun" || "$2" == "proxy" ]]; then "$0" mode "$2"; exit $?; fi
        migrate_legacy_config_in_place "$TARGET" || exit 1
        systemctl start $SERVICE
        if systemctl is-active --quiet $SERVICE; then CUR=$(get_current_mode); echo -e "${C_GREEN}✅ 已启动 [ ${CUR^^} ]${C_RESET}"; else echo -e "${C_RED}❌ 启动失败${C_RESET}"; fi ;;
    off|stop) systemctl stop $SERVICE; echo -e "${C_RED}🛑 已停止${C_RESET}" ;;
    restart) "$0" on ;; upgrade) system_upgrade ;; sub|manager) manage_subs ;; update-auto) generate_config "true" ;;
    mode)
        if [ -n "$3" ]; then echo -e "${C_RED}❌ 参数错误${C_RESET}"; exit 1; fi
        if [[ "$2" == "tun" || "$2" == "proxy" ]]; then
            echo "$2" > "$MODE_FILE"; echo -e "${C_GREEN}>> 模式: $2${C_RESET}"; 
            if [ -s "$SUB_JSON" ] && [ "$(jq 'length' "$SUB_JSON")" -gt 0 ]; then generate_config; else echo -e "${C_YELLOW}⚠ 模式已存(无订阅)${C_RESET}"; migrate_legacy_config_in_place "$TARGET" || exit 1; systemctl restart $SERVICE; fi
        else echo "用法: sing-box mode [tun|proxy]"; fi ;;
    add)
        case "$2" in -l|-link|--link) if [ -n "$3" ]; then add_subscription_cli "$3" "$4"; else echo "缺少链接"; fi ;; -f|-file|--file) if [ -n "$3" ]; then add_subscription_cli "$3" "$4"; else echo "缺少路径"; fi ;; *) manage_subs ;; esac ;;
    update) generate_config ;;
    *) if [ -z "$1" ]; then select_node; else exec /usr/lib/sing-box/sing-box-core "$@"; fi ;;
esac
openSUSE Build Service is sponsored by