#!/usr/bin/env bash
# ============================================================================
# SSPanel 节点一键安装脚本 (XrayR + Caddy 轻量方案)
# 基于 hulisang/v2ray-sspanel-v3-mod_Uim-plugin 重制 (2026-08)
#
# 与原版差异:
#   1. 镜像来自私有仓库 download.myidc.de:5000 (xrayr:latest / caddy:v2ray),
#      不再依赖 GitHub / Docker Hub / ghcr.io
#   2. 后端使用 XrayR 0.9.4 (实测 3000+ 用户仅 ~40MB 内存, CPU 0%,
#      替代原 hulisang/v2ray_v3:go 的 OOM 崩溃循环)
#   3. 内置 api_proxy 适配代理(systemd 服务): 规范化面板 server 字段
#      (去 tls 段/alterId=0/无 host= 项), XrayR 经 127.0.0.1:18080 连面板
#   4. 自动安装 Docker + Compose v2 插件 + insecure-registries
#   5. 新增彻底卸载功能 (恢复新装系统环境)
#
# 用法:
#   交互安装:     bash install.sh
#   非交互安装:   bash install.sh --install \
#                   --panel-url=http://myladder.pro --panel-key=xxxx \
#                   --node-id=1234 --domain=xxx.com --path=/download \
#                   --local-port=10550 --listen-port=443 [--mode=1]
#   更新配置:     bash install.sh --update-config
#   更新镜像:     bash install.sh --update-image
#   查看日志:     bash install.sh --logs
#   彻底卸载:     bash install.sh --uninstall [--purge-docker] [--yes]
#
# 所有参数也可用同名环境变量注入 (PANEL_URL/PANEL_KEY/PANEL_NODE_ID/...)
# ============================================================================

PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin
export PATH

red='\033[0;31m'; green='\033[0;32m'; yellow='\033[0;33m'; plain='\033[0m'

BASE_DIR="/usr/local/v2ray-agent"
COMPOSE_FILE="${BASE_DIR}/docker-compose.yml"
XRAYR_DIR="${BASE_DIR}/xrayr"
REGISTRY="download.myidc.de:5000"
IMAGE_XRAYR="${REGISTRY}/xrayr:latest"
IMAGE_CADDY="${REGISTRY}/caddy:v2ray"
SCRIPT_URL="https://download.myidc.de/Server/SSPanel/Node/install.sh"
GETDOCKER_URL="https://download.myidc.de/Server/SSPanel/Node/get-docker.sh"

software=(Docker_Caddy Docker_Caddy_cloudflare Docker_only)
operation=(install update_config update_image logs uninstall)

# ---------------- 参数收集 ----------------
ACTION=""
MODE=1
MODE_SET=0
PURGE_DOCKER=0
FORCE=0

for arg in "$@"; do
    case "${arg}" in
        --install) ACTION="install" ;;
        --update-config) ACTION="update_config" ;;
        --update-image) ACTION="update_image" ;;
        --logs) ACTION="logs" ;;
        --uninstall) ACTION="uninstall" ;;
        --purge-docker) PURGE_DOCKER=1 ;;
        --yes|-y) FORCE=1 ;;
        --mode=*) MODE="${arg#*=}"; MODE_SET=1 ;;
        --panel-url=*) PANEL_URL="${arg#*=}" ;;
        --panel-key=*) PANEL_KEY="${arg#*=}" ;;
        --node-id=*) PANEL_NODE_ID="${arg#*=}" ;;
        --api-port=*) API_PORT="${arg#*=}" ;;
        --down-with-panel=*) DOWN_WITH_PANEL="${arg#*=}" ;;
        --domain=*) V2RAY_DOMAIN="${arg#*=}" ;;
        --path=*) V2RAY_PATH="${arg#*=}" ;;
        --email=*) V2RAY_EMAIL="${arg#*=}" ;;
        --local-port=*) V2RAY_LOCAL_PORT="${arg#*=}" ;;
        --listen-port=*) CADDY_LISTEN_PORT="${arg#*=}" ;;
        --cf-email=*) CLOUDFLARE_EMAIL="${arg#*=}" ;;
        --cf-key=*) CLOUDFLARE_KEY="${arg#*=}" ;;
        --speedtest=*) PANEL_SPEEDTEST="${arg#*=}" ;;
        *) echo -e "[${red}Error${plain}] 未知参数: ${arg}"; exit 1 ;;
    esac
done

# 环境变量通道 (兼容 export 方式)
PANEL_URL="${PANEL_URL:-${SSRPANEL_URL:-}}"
PANEL_KEY="${PANEL_KEY:-${SSRPANEL_KEY:-}}"
PANEL_NODE_ID="${PANEL_NODE_ID:-${SSRPANEL_NODE_ID:-}}"

get_char(){
    # 非终端(自动化调用)直接跳过按键等待
    [ -t 0 ] || return 0
    SAVEDSTTY=`stty -g`
    stty -echo
    stty cbreak
    dd if=/dev/tty bs=1 count=1 2> /dev/null
    stty -raw
    stty echo
    stty $SAVEDSTTY
}

confirm_or_abort(){
    [ "${FORCE}" = "1" ] && return 0
    echo -e "[${yellow}Warning${plain}] ${1}"
    read -p "确认请按 y (其他任意键取消): " ans
    [ "${ans}" = "y" ] || { echo "已取消"; exit 1; }
}

check_root(){
    [[ $EUID -ne 0 ]] && echo -e "[${red}Error${plain}] 必须以 root 运行!" && exit 1
}

check_sys(){
    local checkType=$1
    local value=$2
    local release=''
    local systemPackage=''
    if [[ -f /etc/redhat-release ]]; then
        release="centos"; systemPackage="yum"
    elif grep -Eqi "debian|raspbian" /etc/os-release 2>/dev/null; then
        release="debian"; systemPackage="apt"
    elif grep -Eqi "ubuntu" /etc/os-release 2>/dev/null; then
        release="ubuntu"; systemPackage="apt"
    elif grep -Eqi "centos|red hat|redhat" /etc/os-release 2>/dev/null; then
        release="centos"; systemPackage="yum"
    fi
    if [[ "${checkType}" == "sysRelease" ]]; then
        [ "${value}" == "${release}" ] && return 0 || return 1
    elif [[ "${checkType}" == "packageManager" ]]; then
        [ "${value}" == "${systemPackage}" ] && return 0 || return 1
    fi
    return 1
}

get_pkg(){
    if check_sys packageManager yum; then
        echo "yum"
    elif check_sys packageManager apt; then
        echo "apt"
    fi
}

install_dependencies(){
    local pkg=$(get_pkg)
    if [ "${pkg}" = "apt" ]; then
        apt-get -y update > /dev/null 2>&1
        apt-get -y install curl > /dev/null 2>&1
    elif [ "${pkg}" = "yum" ]; then
        yum -y install curl > /dev/null 2>&1
    fi
    ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
    echo -e "[${green}Info${plain}] 系统依赖检查完成"
}

# ---------------- 交互式提问 ----------------
ask_param(){
    local desc="$1" var="$2" default="$3" required="${4:-1}"
    local cur=""
    eval "cur=\${${var}}"
    if [ -z "${cur}" ]; then
        if [ -n "${default}" ]; then
            read -p "${desc} (默认: ${default}):" cur
            [ -z "${cur}" ] && cur="${default}"
        elif [ "${required}" = "1" ]; then
            read -p "${desc} (必填):" cur
            [ -z "${cur}" ] && { echo -e "[${red}Error${plain}] ${desc} 不能为空"; exit 1; }
        else
            read -p "${desc} (可空):" cur
        fi
        eval "${var}='${cur}'"
    fi
    echo -e "  ${desc} = ${!var}"
}

pre_install(){
    echo -e "---------------------------"
    echo "请确认以下参数 (非交互模式下自动跳过提问)"
    echo -e "---------------------------"
    ask_param "面板地址 ssrpanel_url" PANEL_URL ""
    ask_param "面板通信密钥 ssrpanel_key" PANEL_KEY ""
    ask_param "面板测速间隔(小时)" PANEL_SPEEDTEST 6
    ask_param "节点 ID ssrpanel_node_id" PANEL_NODE_ID 0
    ask_param "V2ray API Listen port" API_PORT 2333
    ask_param "面板不可用时是否随之下线 downWithPanel" DOWN_WITH_PANEL 1
    if [ "${MODE}" = "1" ] || [ "${MODE}" = "2" ]; then
        ask_param "节点域名 v2ray_domain" V2RAY_DOMAIN ""
        ask_param "WebSocket 路径 v2ray_path" V2RAY_PATH /v2ray
        ask_param "TLS 申请邮箱 v2ray_email(可留空)" V2RAY_EMAIL "" 0
        ask_param "后端本地监听端口 v2ray_local_port" V2RAY_LOCAL_PORT 10550
        ask_param "caddy 对外监听端口 caddy_listen_port" CADDY_LISTEN_PORT 443
    fi
    if [ "${MODE}" = "2" ]; then
        ask_param "Cloudflare 账号邮箱" CLOUDFLARE_EMAIL ""
        ask_param "Cloudflare API Key" CLOUDFLARE_KEY ""
    fi
}

# ---------------- 模板生成 ----------------
write_compose(){
    cat > "${COMPOSE_FILE}" <<'EOF'
version: '2'

services:
  xrayr:
    image: __IMAGE_XRAYR__
    # 轻量后端 XrayR 0.9.4: 实测 3000+ 用户仅 ~40MB 内存, CPU 0%,
    # 替代原 hulisang/v2ray_v3:go(2019) 的 OOM 崩溃循环
    # 面板 server 字段经 xrayr-api-proxy(127.0.0.1:18080) 规范化
    restart: always
    network_mode: "host"
    volumes:
      - ./xrayr:/etc/XrayR
    logging:
      options:
        max-size: "10m"
        max-file: "3"

  caddy:
    image: __IMAGE_CADDY__
    restart: always
    environment:
      - ACME_AGREE=true
#      if u want to use cloudflare (for DNS challenge authentication)
#      - CLOUDFLARE_EMAIL=xxxxxx@out.look.com
#      - CLOUDFLARE_API_KEY=xxxxxxx
      - V2RAY_DOMAIN=__V2RAY_DOMAIN__
      - V2RAY_PATH=__V2RAY_PATH__
      - V2RAY_EMAIL=__V2RAY_EMAIL__
      - V2RAY_PORT=__V2RAY_LOCAL_PORT__
      - V2RAY_OUTSIDE_PORT=__CADDY_LISTEN_PORT__
      - TZ=Asia/Shanghai
      # 关闭 caddy 遥测上报(8.8.8.8 DNS不通会反复重试刷日志)
      - ENABLE_TELEMETRY=false
    network_mode: "host"
    # 文件描述符上限: 默认1024太低(caddy自身警告生产环境需>=8192)
    ulimits:
      nofile:
        soft: 65535
        hard: 65535
    volumes:
      - ./.caddy:/root/.caddy
      - ./Caddyfile:/etc/Caddyfile
    logging:
      options:
        max-size: "10m"
        max-file: "3"
EOF
    sed -i "s|__IMAGE_XRAYR__|${IMAGE_XRAYR}|g" "${COMPOSE_FILE}"
    sed -i "s|__IMAGE_CADDY__|${IMAGE_CADDY}|g" "${COMPOSE_FILE}"
    sed -i "s|__V2RAY_DOMAIN__|${V2RAY_DOMAIN}|g" "${COMPOSE_FILE}"
    sed -i "s|__V2RAY_PATH__|${V2RAY_PATH}|g" "${COMPOSE_FILE}"
    sed -i "s|__V2RAY_EMAIL__|${V2RAY_EMAIL}|g" "${COMPOSE_FILE}"
    sed -i "s|__V2RAY_LOCAL_PORT__|${V2RAY_LOCAL_PORT}|g" "${COMPOSE_FILE}"
    sed -i "s|__CADDY_LISTEN_PORT__|${CADDY_LISTEN_PORT}|g" "${COMPOSE_FILE}"
    if [ "${MODE}" = "2" ]; then
        sed -i "s|#      - CLOUDFLARE_EMAIL=xxxxxx@out.look.com|      - CLOUDFLARE_EMAIL=${CLOUDFLARE_EMAIL}|" "${COMPOSE_FILE}"
        sed -i "s|#      - CLOUDFLARE_API_KEY=xxxxxxx|      - CLOUDFLARE_API_KEY=${CLOUDFLARE_KEY}|" "${COMPOSE_FILE}"
    fi
    # 仅 XrayR 模式: 移除 caddy 服务(caddy 为模板最后一个服务, 直接截断)
    if [ "${MODE}" = "3" ]; then
        python3 - "${COMPOSE_FILE}" <<'PYEOF'
import sys
content = open(sys.argv[1]).read()
idx = content.find("\n  caddy:")
if idx != -1:
    content = content[:idx] + "\n"
    open(sys.argv[1], "w").write(content)
PYEOF
    fi
    echo -e "[${green}Info${plain}] docker-compose.yml 已生成"
}

write_caddyfile(){
    [ "${MODE}" = "3" ] && return 0
    cat > "${BASE_DIR}/Caddyfile" <<'EOF'
{$V2RAY_DOMAIN}:{$V2RAY_OUTSIDE_PORT}
{
  root /srv/www
  log ./caddy.log
  proxy {$V2RAY_PATH} 127.0.0.1:{$V2RAY_PORT} {
    websocket
    header_upstream -Origin
  }
  gzip
  tls {$V2RAY_EMAIL} {
    protocols tls1.2 tls1.3
    # remove comment if u want to use cloudflare (for DNS challenge authentication)
    # dns cloudflare
  }
}
EOF
    if [ "${MODE}" = "2" ]; then
        sed -i "s|# dns cloudflare|dns cloudflare|" "${BASE_DIR}/Caddyfile"
    fi
    echo -e "[${green}Info${plain}] Caddyfile 已生成"
}

write_config(){
    cat > "${XRAYR_DIR}/config.yml" <<'EOF'
Log:
  Level: debug
  AccessPath: /etc/XrayR/access.log
  ErrorPath: /etc/XrayR/error.log
DnsConfigPath: /etc/XrayR/dns.json
RouteConfigPath: /etc/XrayR/route.json
InboundConfig: []
OutboundConfig:
  - Protocol: Freedom
    Tag: direct
Nodes:
  - PanelType: "SSpanel"
    ApiConfig:
      ApiHost: "http://127.0.0.1:18080"
      ApiKey: "__PANEL_KEY__"
      NodeID: __PANEL_NODE_ID__
      NodeType: V2ray
      Timeout: 30
      EnableVless: false
      EnableXTLS: false
      SpeedLimit: 0
      DeviceLimit: 0
      RuleListPath: /etc/XrayR/rulelist
    ControllerConfig:
      ListenIP: 0.0.0.0
      SendIP: 0.0.0.0
      UpdatePeriodic: 60
      EnableDNS: false
      DNSType: AsIs
      EnableProxyProtocol: false
      EnableFallback: false
      FallBackConfigs: []
      CertConfig:
        CertMode: none
        CertDomain: ""
        CertFile: ""
        KeyFile: ""
        Provider: ""
        Email: ""
      BufferSize: 64
      MasterDisable: false
      DisableSniffing: false
EOF
    sed -i "s|__PANEL_KEY__|${PANEL_KEY}|g" "${XRAYR_DIR}/config.yml"
    sed -i "s|__PANEL_NODE_ID__|${PANEL_NODE_ID}|g" "${XRAYR_DIR}/config.yml"
    echo -e "[${green}Info${plain}] XrayR config.yml 已生成 (NodeID=${PANEL_NODE_ID})"
}

write_api_proxy(){
    cat > "${XRAYR_DIR}/api_proxy.py" <<'EOF'
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
本地 API 适配代理: 将面板返回的 v2ray 节点 server 字段
规范化为 XrayR 0.9.4 可解析的格式, 其余请求原样透传。
"""
import json
import urllib.request
import urllib.error
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer

UPSTREAM = "__PANEL_URL__"
LISTEN = ("127.0.0.1", 18080)


def rewrite_server(raw):
    """面板格式: host;0;16;tls;ws;path=/download|裸host|inside_port=10550|outside_port=46923
    XrayR格式: host;port;alterId;tls;network;path=...|host=...|inside_port=...|outside_port=...
    改写时去掉 tls 段(由 caddy 终止 TLS, 后端走明文 ws)。失败时原样返回。"""
    try:
        parts = raw.split("|")
        seg = parts[0].split(";")
        if len(seg) < 6:
            return raw
        host = seg[0]
        network = seg[4]
        path_kv = seg[5]
        path = path_kv.split("=", 1)[1] if "=" in path_kv else ""
        inside = "10550"
        outside = "46923"
        bare_host = host
        for item in parts[1:]:
            if "=" in item:
                k, v = item.split("=", 1)
                if k == "inside_port":
                    inside = v
                elif k == "outside_port":
                    outside = v
            else:
                bare_host = item
        # 注意: 不能带 host= 项, 否则 XrayR 会启用 ws Host 校验,
        # caddy 转发后 Host 为 127.0.0.1:10550, 校验失败返回 404
        # alterId 固定 0: 面板 server 字段第3段(16)实为 traffic_rate,
        # Uim 面板 v2ray 客户端订阅默认 alterId=0, 否则报 invalid user
        return "{};{};0;none;{};path={}|inside_port={}|outside_port={}".format(
            host, inside, network, path, inside, outside)
    except Exception:
        return raw


class Handler(BaseHTTPRequestHandler):
    protocol_version = "HTTP/1.1"

    def _do(self):
        length = int(self.headers.get("Content-Length") or 0)
        body = self.rfile.read(length) if length else None
        req = urllib.request.Request(UPSTREAM + self.path, data=body, method=self.command)
        for k, v in self.headers.items():
            if k.lower() not in ("host", "content-length", "connection"):
                req.add_header(k, v)
        try:
            resp = urllib.request.urlopen(req, timeout=15)
        except urllib.error.HTTPError as e:
            resp = e
        data = resp.read()
        ctype = resp.headers.get("Content-Type", "")
        if "/mod_mu/nodes/" in self.path:
            try:
                obj = json.loads(data)
                if isinstance(obj, dict) and obj.get("ret") == 1 and isinstance(obj.get("data"), dict):
                    if "server" in obj["data"]:
                        obj["data"]["server"] = rewrite_server(obj["data"]["server"])
                        data = json.dumps(obj, ensure_ascii=False).encode("utf-8")
            except Exception:
                pass
        self.send_response(resp.status)
        for k, v in resp.headers.items():
            if k.lower() not in ("content-length", "transfer-encoding", "connection", "keep-alive"):
                self.send_header(k, v)
        self.send_header("Content-Length", str(len(data)))
        self.end_headers()
        self.wfile.write(data)

    do_GET = _do
    do_POST = _do
    do_PUT = _do
    do_DELETE = _do


if __name__ == "__main__":
    ThreadingHTTPServer(LISTEN, Handler).serve_forever()
EOF
    sed -i "s|__PANEL_URL__|${PANEL_URL}|g" "${XRAYR_DIR}/api_proxy.py"
    chmod +x "${XRAYR_DIR}/api_proxy.py"
    echo -e "[${green}Info${plain}] api_proxy.py 已生成"
}

write_systemd(){
    cat > /etc/systemd/system/xrayr-api-proxy.service <<'EOF'
[Unit]
Description=XrayR API adaptor proxy for SSPanel
After=network-online.target

[Service]
Type=simple
ExecStart=/usr/bin/python3 /usr/local/v2ray-agent/xrayr/api_proxy.py
Restart=always
RestartSec=3

[Install]
WantedBy=multi-user.target
EOF
    systemctl daemon-reload
    systemctl enable xrayr-api-proxy > /dev/null 2>&1
    systemctl restart xrayr-api-proxy
    sleep 1
    if systemctl is-active xrayr-api-proxy > /dev/null; then
        echo -e "[${green}Info${plain}] 适配代理 xrayr-api-proxy 已启动 (127.0.0.1:18080)"
    else
        echo -e "[${red}Error${plain}] 适配代理启动失败, 请检查 python3 是否安装"
        exit 1
    fi
}

# ---------------- Docker 安装 ----------------
setup_docker(){
    if command -v docker > /dev/null 2>&1; then
        echo -e "[${green}Info${plain}] Docker 已安装: $(docker --version | head -1)"
    else
        echo -e "[${green}Info${plain}] 开始安装 Docker..."
        if curl -fsSL --max-time 60 "${GETDOCKER_URL}" -o /tmp/get-docker.sh 2>/dev/null; then
            echo "  使用本地源 get-docker.sh"
        else
            echo "  本地源不可用, 回退官方源"
            curl -fsSL --max-time 60 https://get.docker.com -o /tmp/get-docker.sh
        fi
        bash /tmp/get-docker.sh > /tmp/docker-install.log 2>&1
        if ! command -v docker > /dev/null 2>&1; then
            echo -e "[${red}Error${plain}] Docker 安装失败, 日志尾部:"
            tail -5 /tmp/docker-install.log
            exit 1
        fi
        systemctl enable docker > /dev/null 2>&1
        systemctl start docker
    fi
    # Compose v2 插件
    if ! docker compose version > /dev/null 2>&1; then
        echo -e "[${green}Info${plain}] 安装 docker-compose-plugin..."
        if check_sys packageManager apt; then
            apt-get -y install docker-compose-plugin > /dev/null 2>&1
        else
            yum -y install docker-compose-plugin > /dev/null 2>&1
        fi
    fi
    docker compose version | head -1
    # insecure-registries (http 私有仓库)
    if ! docker info 2>/dev/null | grep -q "${REGISTRY}"; then
        echo -e "[${green}Info${plain}] 配置 insecure-registries: ${REGISTRY}"
        if command -v python3 > /dev/null 2>&1; then
            python3 - "${REGISTRY}" <<'PYEOF'
import json, sys
reg = sys.argv[1]
path = "/etc/docker/daemon.json"
import os
data = {}
if os.path.exists(path):
    try:
        data = json.load(open(path))
    except Exception:
        data = {}
irs = data.get("insecure-registries", [])
if reg not in irs:
    irs.append(reg)
    data["insecure-registries"] = irs
    json.dump(data, open(path, "w"), indent=2)
PYEOF
        else
            echo "{\"insecure-registries\":[\"${REGISTRY}\"]}" > /etc/docker/daemon.json
        fi
        systemctl restart docker
        sleep 4
    fi
    echo -e "[${green}Info${plain}] Docker 环境就绪"
}

# ---------------- 部署 ----------------
deploy(){
    mkdir -p "${BASE_DIR}" "${XRAYR_DIR}"
    cd "${BASE_DIR}" || exit 1
    # 辅助文件 (XrayR 启动时必须能解析 dns/route 配置, 空文件会 panic)
    cat > "${XRAYR_DIR}/dns.json" <<'EOF'
{
  "servers": [
    "localhost"
  ]
}
EOF
    cat > "${XRAYR_DIR}/route.json" <<'EOF'
{
  "domainStrategy": "AsIs",
  "rules": [
    {
      "type": "field",
      "inboundTag": ["api"],
      "outboundTag": "api"
    }
  ]
}
EOF
    printf '' > "${XRAYR_DIR}/rulelist"
    touch "${XRAYR_DIR}/access.log" "${XRAYR_DIR}/error.log"
    # 生成配置
    write_compose
    write_caddyfile
    write_config
    write_api_proxy
    write_systemd
    # 启动
    echo -e "[${green}Info${plain}] 拉取镜像并启动服务..."
    docker compose up -d
    echo -e "[${green}Info${plain}] 等待服务就绪..."
    sleep 10
    local xrayr_ok=0 caddy_ok=0
    if docker ps --format '{{.Names}}' | grep -q 'xrayr'; then xrayr_ok=1; fi
    if docker ps --format '{{.Names}}' | grep -q 'caddy'; then caddy_ok=1; fi
    [ "${MODE}" = "3" ] && caddy_ok=1
    if [ "${xrayr_ok}" = "1" ] && [ "${caddy_ok}" = "1" ]; then
        echo -e "[${green}Info${plain}] 容器状态:"
        docker ps --filter "name=xrayr" --filter "name=caddy" --format '  {{.Names}}  {{.Status}}' 2>/dev/null | head -5
    else
        echo -e "[${red}Warning${plain}] 容器未全部就绪, 等待 20s 后重查..."
        sleep 20
        docker ps -a --format '{{.Names}} {{.Status}}' | grep -E 'xrayr|caddy' || true
    fi
    # 验证面板用户拉取
    echo -e "[${green}Info${plain}] 验证面板用户拉取..."
    local users=""
    for i in 1 2 3 4 5 6; do
        users=$(curl -s --max-time 8 "http://127.0.0.1:18080/mod_mu/users?key=${PANEL_KEY}&node_id=${PANEL_NODE_ID}" 2>/dev/null)
        if echo "${users}" | grep -q '"ret":1'; then
            break
        fi
        sleep 10
    done
    if echo "${users}" | grep -q '"ret":1'; then
        local n
        n=$(echo "${users}" | python3 -c "import json,sys; print(len(json.load(sys.stdin)['data']))" 2>/dev/null)
        echo -e "[${green}Info${plain}] ✅ 节点 ${PANEL_NODE_ID} 已从面板加载 ${n} 个用户"
    else
        echo -e "[${red}Warning${plain}] 用户拉取验证未通过, 请检查面板地址/密钥/节点ID, 日志:"
        docker logs $(docker ps -q --filter name=xrayr | head -1) --tail 5 2>&1 || true
    fi
    echo
    echo -e "${green}============================================${plain}"
    echo -e "${green}  恭喜! 节点安装完成!${plain}"
    echo -e "  节点 ID:   ${PANEL_NODE_ID}"
    if [ "${MODE}" != "3" ]; then
        echo -e "  订阅域名:  ${V2RAY_DOMAIN}:${CADDY_LISTEN_PORT}${V2RAY_PATH}"
    fi
    echo -e "  下载脚本:  curl -o install.sh ${SCRIPT_URL}"
    echo -e "  查看日志:  bash install.sh --logs"
    echo -e "  彻底卸载:  bash install.sh --uninstall [--purge-docker]"
    echo -e "${green}============================================${plain}"
}

# ---------------- 更新/日志 ----------------
update_config(){
    [ -f "${COMPOSE_FILE}" ] || { echo -e "[${red}Error${plain}] 未找到 ${COMPOSE_FILE}, 请先安装"; exit 1; }
    docker compose -f "${COMPOSE_FILE}" down 2>/dev/null
    pre_install
    write_compose
    write_caddyfile
    write_config
    write_api_proxy
    write_systemd
    docker compose -f "${COMPOSE_FILE}" up -d
    echo -e "[${green}Info${plain}] 配置更新完成"
}

update_image(){
    [ -f "${COMPOSE_FILE}" ] || { echo -e "[${red}Error${plain}] 未找到 ${COMPOSE_FILE}, 请先安装"; exit 1; }
    cd "${BASE_DIR}" || exit 1
    echo "停止服务..."
    docker compose down
    echo "拉取新镜像..."
    docker compose pull
    echo "启动服务..."
    docker compose up -d
    echo -e "[${green}Info${plain}] 镜像更新完成"
}

logs_v2ray(){
    [ -f "${COMPOSE_FILE}" ] || { echo -e "[${red}Error${plain}] 未找到 ${COMPOSE_FILE}, 请先安装"; exit 1; }
    docker compose -f "${COMPOSE_FILE}" logs --tail 100
}

# ---------------- 彻底卸载 ----------------
uninstall_v2ray(){
    echo -e "${red}========== 彻底卸载节点环境 ==========${plain}"
    confirm_or_abort "此操作将删除节点全部服务/容器/镜像/配置文件!"
    echo -e "[${yellow}1/6${plain}] 停止并删除容器..."
    if [ -f "${COMPOSE_FILE}" ]; then
        cd "${BASE_DIR}" && docker compose down 2>/dev/null
    fi
    docker rm -f $(docker ps -aq --filter name=xrayr) 2>/dev/null
    docker rm -f $(docker ps -aq --filter name=caddy) 2>/dev/null
    docker rm -f $(docker ps -aq --filter name=v2ray) 2>/dev/null
    echo -e "[${yellow}2/6${plain}] 停止并删除适配代理服务..."
    systemctl stop xrayr-api-proxy 2>/dev/null
    systemctl disable xrayr-api-proxy 2>/dev/null
    rm -f /etc/systemd/system/xrayr-api-proxy.service
    systemctl daemon-reload
    echo -e "[${yellow}3/6${plain}] 删除本地镜像..."
    docker rmi "${IMAGE_XRAYR}" "${IMAGE_CADDY}" 2>/dev/null
    docker rmi ghcr.io/xrayr-project/xrayr:latest hulisang/v2ray_v3:caddy hulisang/v2ray_v3:go 2>/dev/null
    echo -e "[${yellow}4/6${plain}] 删除配置目录..."
    rm -rf "${BASE_DIR}" /tmp/get-docker.sh /tmp/docker-install.log
    echo -e "[${yellow}5/6${plain}] 移除 insecure-registries 配置..."
    if [ -f /etc/docker/daemon.json ] && command -v python3 > /dev/null 2>&1; then
        python3 - "${REGISTRY}" <<'PYEOF'
import json, sys, os
reg = sys.argv[1]
path = "/etc/docker/daemon.json"
try:
    data = json.load(open(path))
except Exception:
    data = {}
irs = data.get("insecure-registries", [])
if reg in irs:
    irs.remove(reg)
    if irs:
        data["insecure-registries"] = irs
    else:
        data.pop("insecure-registries", None)
    json.dump(data, open(path, "w"), indent=2)
PYEOF
        rm -f /etc/docker/daemon.json.bak
        echo "  insecure-registries 已移除"
    fi
    echo -e "[${yellow}6/6${plain}] 清理 Docker 与数据..."
    if [ "${PURGE_DOCKER}" = "1" ]; then
        confirm_or_abort "即将卸载 Docker 本体并删除 /var/lib/docker 全部数据!"
        systemctl stop docker 2>/dev/null
        if check_sys packageManager apt; then
            apt-get -y purge docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin docker-registry > /dev/null 2>&1
            apt-get -y autoremove > /dev/null 2>&1
        else
            yum -y remove docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin docker-registry > /dev/null 2>&1
        fi
        rm -rf /var/lib/docker /etc/docker /var/lib/containerd
        groupdel docker 2>/dev/null
        echo -e "[${green}Info${plain}] Docker 已彻底卸载"
    else
        echo -e "[${green}Info${plain}] 保留 Docker (如需彻底移除 Docker 请加 --purge-docker)"
    fi
    rm -rf /tmp/xrayr-deploy /root/.caddy 2>/dev/null
    echo
    echo -e "${green}========== 卸载完成, 系统已恢复 ==========${plain}"
}

# ---------------- 主流程 ----------------
main(){
    check_root
    install_dependencies

    if [ -z "${ACTION}" ]; then
        clear
        while true; do
            echo "请选择操作:"
            for ((i=1;i<=${#operation[@]};i++)); do
                hint="${operation[$i-1]}"
                echo -e "${green}${i}${plain}) ${hint}"
            done
            read -p "请输入数字 (默认 1):" selected
            [ -z "${selected}" ] && selected="1"
            case "${selected}" in
                1|2|3|4|5) ACTION="${operation[${selected}-1]}"; break ;;
                *) echo -e "[${red}Error${plain}] 请输入 1-5" ;;
            esac
        done
    fi

    case "${ACTION}" in
        install)
            # 未显式指定模式时, 交互选择安装模式
            if [ "${MODE_SET}" != "1" ]; then
                clear
                while true; do
                    echo "请选择安装模式:"
                    for ((i=1;i<=${#software[@]};i++)); do
                        hint="${software[$i-1]}"
                        desc=""
                        case "$i" in
                            1) desc="  (XrayR + Caddy TLS, 推荐)" ;;
                            2) desc="  (XrayR + Caddy + Cloudflare DNS 验证)" ;;
                            3) desc="  (仅 XrayR, 无 caddy, 需自行反代)" ;;
                        esac
                        echo -e "${green}${i}${plain}) ${hint}${desc}"
                    done
                    read -p "请输入数字 (默认 1):" selected
                    [ -z "${selected}" ] && selected="1"
                    case "${selected}" in
                        1|2|3) MODE="${selected}"; break ;;
                        *) echo -e "[${red}Error${plain}] 请输入 1-3" ;;
                    esac
                done
            fi
            pre_install
            echo
            echo -e "---------------------------"
            echo "开始安装, 按任意键继续...或 Ctrl+C 取消"
            get_char
            setup_docker
            deploy
            ;;
        update_config)
            update_config
            ;;
        update_image)
            update_image
            ;;
        logs)
            logs_v2ray
            ;;
        uninstall)
            uninstall_v2ray
            ;;
        *)
            echo -e "[${red}Error${plain}] 未知操作: ${ACTION}"
            exit 1
            ;;
    esac
}

main "$@"
