网站搭建在本地,通过 frp 内网穿透分别连接到主线 VPS1 和备线 VPS2,本地监控脚本探测 frpc 状态,主线故障时自动切换 Cloudflare DNS 到备线 VPS2,实现服务故障转移。
脚本本身提供安装、配置、查看日志、卸载等功能。 MIT green

🎉 脚本界面

========================================
  frpc 双线监控 + CF DNS 自动切换管理器
========================================
  1. 安装/配置监控脚本 (首次安装)
  2. 修改配置
  3. 查看实时日志
  4. 卸载并清理所有文件
  0. 退出
========================================
请输入选项编号: 

✨ 功能特性

功能模块 说明 状态
🎯双线热备 主线+备线 VPS 自动切换
🔄DNS 自动故障转移 基于 Cloudflare API 实时切换
🐳Docker 原生支持 自动检测和管理 frpc 容器
📊Dashboard 健康检查 隧道状态实时监控
🔧交互式配置向导 首次安装零门槛
📝灵活配置管理 支持单项修改和批量管理
📈日志轮转 自动管理日志文件
🚦Systemd 集成 开机自启 + 守护进程

🏗️ 架构原理

20260518102247_6a0a77f7ee984.jpg

📋 工作流程

  1. 健康检查 🔍
  • 每 30 秒检查两个 frpc 容器的 Dashboard API
  • 验证隧道状态是否为 running
  • 自动重启无响应的 frpc 容器
  1. 故障判断 🧠
  • 主线宕机 → 立即切换所有域名到备线 IP
  • 主线恢复 → 自动切回主线 IP
  • 双线均宕 → 保持最后状态,等待恢复
  1. DNS 更新 ☁️
  • 调用 Cloudflare API 批量更新 A 记录
  • TTL=60 秒,实现快速收敛
  • 支持代理模式开关

🚀 快速开始

  • 操作系统: Linux (CentOS 7+, Ubuntu 16.04+, Debian 9+)
  • 权限: Root 或 sudo 权限
  • 依赖: Docker 已安装并运行
  • 网络: 能够访问 Cloudflare API

⚙️ 安装步骤

1️⃣ 创建 frpc-failover.sh 文件

sudo nano /opt/frpc-failover.sh

2️⃣ 粘贴 👇脚本代码 后 Ctrl+XYEnter 保存退出

#!/bin/bash
# =============================================================
# frpc 双线监控 + Cloudflare DNS 自动切换 - 管理脚本 v1.0
# =============================================================

# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
NC='\033[0m'

# 路径定义
INSTALL_DEPS_SCRIPT="/opt/install-deps.sh"
MONITOR_SCRIPT="/opt/frpc-monitor.sh"
SERVICE_FILE="/etc/systemd/system/frpc-monitor.service"
LOGROTATE_FILE="/etc/logrotate.d/frpc-monitor"
LOG_FILE="/var/log/frpc-monitor.log"
STATE_FILE="/tmp/frpc_monitor_state"

# 检查权限
if [[ $EUID -ne 0 ]]; then
    echo -e "${RED}请使用 root 用户或 sudo 运行此脚本${NC}"
    exit 1
fi

# ================================================================
# 主菜单
# ================================================================
print_menu() {
    clear
    echo -e "${CYAN}========================================${NC}"
    echo -e "${CYAN}  frpc 双线监控 + CF DNS 自动切换管理器${NC}"
    echo -e "${CYAN}========================================${NC}"
    echo "  1. 安装/配置监控脚本 (首次安装)"
    echo -e "  ${YELLOW}2. 修改配置${NC}"
    echo "  3. 查看实时日志"
    echo "  4. 卸载并清理所有文件"
    echo -e "  ${YELLOW}0. 退出${NC}"
    echo -e "${CYAN}========================================${NC}"
}

# ================================================================
# -------- 安装流程 --------
# ================================================================

# -------- 1. 安装依赖(curl / jq / docker)--------
install_deps() {
    echo -e "${GREEN}[1/5] 检查并安装系统依赖 (curl, jq, docker)...${NC}"

    cat <<'DEPS_EOF' > "$INSTALL_DEPS_SCRIPT"
#!/bin/bash
if command -v apt-get &>/dev/null; then
    PKG_MANAGER="apt-get"; UPDATE_CMD="apt-get update -y"; INSTALL_CMD="apt-get install -y"
elif command -v dnf &>/dev/null; then
    PKG_MANAGER="dnf";     UPDATE_CMD="dnf makecache -y";  INSTALL_CMD="dnf install -y"
elif command -v yum &>/dev/null; then
    PKG_MANAGER="yum";     UPDATE_CMD="yum makecache -y";  INSTALL_CMD="yum install -y"
else
    echo "未识别的包管理器,请手动安装:curl、jq"; exit 1
fi
 $UPDATE_CMD &>/dev/null

install_pkg() {
    local name=$1 cmd=$2
    if command -v "$cmd" &>/dev/null; then
        echo "  ${name} 已安装"
    else
        echo "  安装 ${name}..."
        $INSTALL_CMD "$name" &>/dev/null \
            && echo "  ${name} 安装成功" \
            || { echo "  ${name} 安装失败"; exit 1; }
    fi
}

install_pkg "curl"   "curl"
install_pkg "jq"     "jq"
install_pkg "docker" "docker"
DEPS_EOF

    chmod +x "$INSTALL_DEPS_SCRIPT"
    bash "$INSTALL_DEPS_SCRIPT"
}

# -------- 辅助:读取必填字符串,不允许为空 --------
# 用法: _read_required VAR "提示语"
_read_required() {
    local _var=$1 _prompt=$2 _val
    while true; do
        read -p "$_prompt" _val
        if [[ -n "$_val" ]]; then
            printf -v "$_var" '%s' "$_val"
            return
        fi
        echo -e "  ${RED}不能为空,请重新输入${NC}"
    done
}

# -------- 辅助:读取端口(1-65535),允许回车使用默认值 --------
# 用法: _read_port VAR "提示语" 默认值
_read_port() {
    local _var=$1 _prompt=$2 _default=$3 _val
    while true; do
        read -p "$_prompt" _val
        _val=${_val:-$_default}
        if [[ "$_val" =~ ^[0-9]+$ ]] && (( _val >= 1 && _val <= 65535 )); then
            printf -v "$_var" '%s' "$_val"
            return
        fi
        echo -e "  ${RED}请输入 1~65535 的整数${NC}"
    done
}

# -------- 2. 交互式采集配置 --------
# 返回值:0=成功,1=用户取消(未输入域名)
collect_config() {
    echo -e "\n${GREEN}[2/5] 检测 frpc 容器...${NC}"
    mapfile -t FRPC_CONTAINERS < <(docker ps --format "{{.Names}}" 2>/dev/null | grep frpc | sort)

    if [[ ${#FRPC_CONTAINERS[@]} -eq 0 ]]; then
        echo -e "${YELLOW}未检测到运行中的 frpc 容器,请确保 Docker 已启动且容器名包含 frpc${NC}"
    else
        echo -e "${GREEN}检测到以下 frpc 容器:${NC}"
        for c in "${FRPC_CONTAINERS[@]}"; do echo "  - $c"; done
    fi

    echo -e "\n${CYAN}[3/5] 请输入配置信息:${NC}"

    _read_required CF_API_TOKEN "Cloudflare API Token: "
    _read_required CF_ZONE_ID   "Cloudflare Zone ID: "
    _read_required VPS1_IP      "主线 VPS1 IP: "
    _read_required VPS2_IP      "备线 VPS2 IP: "

    echo -e "\n${CYAN}--- frpc 容器名 ---${NC}"
    if [[ ${#FRPC_CONTAINERS[@]} -ge 2 ]]; then
        read -p "frpc01 容器名 (默认 ${FRPC_CONTAINERS[0]}): " C1
        FRPC1_CONTAINER=${C1:-${FRPC_CONTAINERS[0]}}
        read -p "frpc02 容器名 (默认 ${FRPC_CONTAINERS[1]}): " C2
        FRPC2_CONTAINER=${C2:-${FRPC_CONTAINERS[1]}}
    else
        echo -e "${YELLOW}未自动检测到足够容器,请手动输入${NC}"
        _read_required FRPC1_CONTAINER "frpc01 容器名: "
        _read_required FRPC2_CONTAINER "frpc02 容器名: "
    fi

    echo -e "\n${CYAN}--- frpc Dashboard 端口 ---${NC}"
    _read_port FRPC1_DASH_PORT "frpc01 Dashboard 端口 (默认 7401): " 7401
    _read_port FRPC2_DASH_PORT "frpc02 Dashboard 端口 (默认 7402): " 7402

    echo -e "\n${CYAN}--- frpc Dashboard 认证 ---${NC}"
    _read_required FRPC_USER "Dashboard 用户名: "
    _read_required FRPC_PASS "Dashboard 密码: "

    echo -e "\n${CYAN}--- 需要切换的域名 ---${NC}"
    DOMAIN_LIST=()
    _read_domains_input
    DOMAINS=("${DOMAIN_LIST[@]}")

    if [[ ${#DOMAINS[@]} -eq 0 ]]; then
        echo -e "${RED}未输入任何域名,操作取消${NC}"
        return 1
    fi

    echo -e "\n${GREEN}已录入 ${#DOMAINS[@]} 个域名:${NC}"
    for d in "${DOMAINS[@]}"; do echo "  - $d"; done
    return 0
}

# -------- 3. 生成主监控脚本 --------
generate_monitor_script() {
    echo -e "\n${GREEN}[4/5] 生成监控脚本...${NC}"

    # heredoc 使用单引号定界符,防止变量展开。
    # FRPC1_URL / FRPC2_URL 使用占位符,由后续 sed 替换为静态字符串,
    # 避免 heredoc 内 ${FRPC1_DASH_PORT} 因未赋值而展开为空。
    cat <<'SCRIPT_EOF' > "$MONITOR_SCRIPT"
#!/bin/bash
# =============================================================
# frpc 双线监控 + Cloudflare DNS 自动切换脚本
# 依赖:curl / jq / docker
# =============================================================

CF_API_TOKEN="CF_TOKEN_PLACEHOLDER"
CF_ZONE_ID="ZONE_ID_PLACEHOLDER"

VPS1_IP="VPS1_IP_PLACEHOLDER"
VPS2_IP="VPS2_IP_PLACEHOLDER"

DOMAINS=(
DOMAIN_ARRAY_PLACEHOLDER
)

FRPC1_DASH_PORT=FRPC1_DASH_PORT_PLACEHOLDER
FRPC2_DASH_PORT=FRPC2_DASH_PORT_PLACEHOLDER
FRPC1_URL="FRPC1_URL_PLACEHOLDER"
FRPC2_URL="FRPC2_URL_PLACEHOLDER"
FRPC_USER="USER_PLACEHOLDER"
FRPC_PASS="PASS_PLACEHOLDER"

FRPC1_CONTAINER="CON1_PLACEHOLDER"
FRPC2_CONTAINER="CON2_PLACEHOLDER"

DNS_TTL=60
CHECK_INTERVAL=30
RESTART_WAIT=15
CF_PROXIED=false

LOG_FILE="LOG_FILE_PLACEHOLDER"
STATE_FILE="STATE_FILE_PLACEHOLDER"

log()        { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE" >&2; }
log_ok()     { log "[OK] $1"; }
log_warn()   { log "[!!] $1"; }
log_err()    { log "[XX] $1"; }
log_info()   { log "[--] $1"; }
load_state() { [[ -f "$STATE_FILE" ]] && source "$STATE_FILE" || LAST_STATE="UNKNOWN"; }
save_state() { echo "LAST_STATE=\"$1\"" > "$STATE_FILE"; }

check_dashboard() {
    local code
    code=$(curl -s -o /dev/null -w "%{http_code}" \
        --connect-timeout 5 --max-time 8 \
        -u "${FRPC_USER}:${FRPC_PASS}" "$1" 2>/dev/null)
    [[ "$code" == "200" ]]
}

restart_frpc() {
    local container=$1
    log_info "  重启容器: ${container}"
    if docker restart "$container" &>/dev/null; then
        log_ok "  ${container} 重启成功,等待 ${RESTART_WAIT}s"
        sleep "$RESTART_WAIT"
        return 0
    else
        log_err "  ${container} 重启失败"
        return 1
    fi
}

get_vps_status() {
    local dashboard_url=$1 container=$2

    if ! check_dashboard "$dashboard_url"; then
        log_warn "${container}: Dashboard 无响应,尝试重启"
        if restart_frpc "$container" && check_dashboard "$dashboard_url"; then
            log_ok "${container} 重启后 Dashboard 恢复"
        else
            log_err "${container} 重启后仍无响应,按宕机处理"
            echo "VPS_DOWN"; return
        fi
    fi

    local result
    result=$(curl -s --connect-timeout 5 --max-time 8 \
        -u "${FRPC_USER}:${FRPC_PASS}" "$dashboard_url" 2>/dev/null)

    if echo "$result" | jq -e '.. | objects | select(.status == "running")' &>/dev/null; then
        echo "OK"
    else
        log_err "${container}: Dashboard 正常但隧道全部离线,按宕机处理"
        echo "VPS_DOWN"
    fi
}

get_record_info() {
    local domain=$1 response
    response=$(curl -s -X GET \
        "https://api.cloudflare.com/client/v4/zones/${CF_ZONE_ID}/dns_records?type=A&name=${domain}" \
        -H "Authorization: Bearer ${CF_API_TOKEN}" \
        -H "Content-Type: application/json")
    local record_id current_ip
    record_id=$(echo "$response"  | jq -r '.result[0].id      // ""')
    current_ip=$(echo "$response" | jq -r '.result[0].content // ""')
    echo "${record_id}|${current_ip}"
}

update_record() {
    local domain=$1 target_ip=$2
    [[ -z "$domain" ]] && return 2

    local info record_id current_ip
    local max_retries=3 attempt delay

    # ---------- 第一步:获取记录 ID(含重试)----------
    for (( attempt=1; attempt<=max_retries; attempt++ )); do
        info=$(get_record_info "$domain")
        record_id=$(echo "$info" | cut -d'|' -f1)
        current_ip=$(echo "$info" | cut -d'|' -f2)
        [[ -n "$record_id" ]] && break
        delay=$(( attempt * 2 ))
        log_warn "  | ${domain} -> 获取记录失败 (第${attempt}/${max_retries}次),${delay}s 后重试"
        sleep "$delay"
    done

    if [[ -z "$record_id" ]]; then
        log_err "  | ${domain} -> 获取记录失败,已重试 ${max_retries} 次,放弃"
        return 1
    fi

    if [[ "$current_ip" == "$target_ip" ]]; then
        log_info "  | ${domain} -> 已是 ${target_ip},无需更新"
        return 0
    fi

    # ---------- 第二步:更新记录(含重试)----------
    local response success
    for (( attempt=1; attempt<=max_retries; attempt++ )); do
        response=$(curl -s -X PUT \
            "https://api.cloudflare.com/client/v4/zones/${CF_ZONE_ID}/dns_records/${record_id}" \
            -H "Authorization: Bearer ${CF_API_TOKEN}" \
            -H "Content-Type: application/json" \
            --data "{\"type\":\"A\",\"name\":\"${domain}\",\"content\":\"${target_ip}\",\"ttl\":${DNS_TTL},\"proxied\":${CF_PROXIED}}")
        success=$(echo "$response" | jq -r '.success')
        if [[ "$success" == "true" ]]; then
            log_ok "  | ${domain} -> ${current_ip} => ${target_ip}"
            return 0
        fi
        delay=$(( attempt * 2 ))
        log_warn "  | ${domain} -> 更新失败 (第${attempt}/${max_retries}次),${delay}s 后重试"
        sleep "$delay"
    done

    log_err "  | ${domain} -> 更新失败,已重试 ${max_retries} 次,放弃: $(echo "$response" | jq -r '.errors[0].message // "未知错误"')"
    return 1
}

switch_all() {
    local target_ip=$1 label=$2 ok=0 fail=0
    local failed_domains=()

    log "========================================"
    log "[>>] 批量切换 -> ${target_ip}  (${label})"
    log "----------------------------------------"

    for domain in "${DOMAINS[@]}"; do
        update_record "$domain" "$target_ip"
        case $? in
            0) ((ok++)) ;;
            1) ((fail++)); failed_domains+=("$domain") ;;
        esac
        sleep 0.5
    done

    # ---------- 对失败域名做一轮补偿重试 ----------
    if [[ ${#failed_domains[@]} -gt 0 ]]; then
        log "----------------------------------------"
        log_warn "对 ${#failed_domains[@]} 个失败域名进行补偿重试 (等待 5s)..."
        sleep 5
        local retry_ok=0 retry_fail=0
        for domain in "${failed_domains[@]}"; do
            update_record "$domain" "$target_ip"
            case $? in
                0) ((retry_ok++)); ((ok++)); ((fail--)) ;;
                1) ((retry_fail++)) ;;
            esac
            sleep 1
        done
        log_info "补偿结果: 补救成功 ${retry_ok} 个,仍失败 ${retry_fail} 个"
    fi

    log "----------------------------------------"
    if [[ $fail -eq 0 ]]; then
        log_ok "完成:全部 ${ok} 个成功"
    else
        log_warn "完成:成功 ${ok} 个,最终失败 ${fail} 个"
    fi
    log "========================================"
}

log "========================================"
log "[>>] frpc 监控启动 | 域名数:${#DOMAINS[@]} | VPS1:${VPS1_IP} | VPS2:${VPS2_IP} | 间隔:${CHECK_INTERVAL}s"
log "========================================"

load_state

while true; do
    VPS1_STATUS=$(get_vps_status "$FRPC1_URL" "$FRPC1_CONTAINER")
    VPS2_STATUS=$(get_vps_status "$FRPC2_URL" "$FRPC2_CONTAINER")

    log "[--] VPS1:${VPS1_STATUS} VPS2:${VPS2_STATUS} 上次:${LAST_STATE}"

    case "${VPS1_STATUS}|${VPS2_STATUS}" in
        "OK|OK")
            CURRENT_STATE="BOTH_OK"
            if [[ "$LAST_STATE" == "VPS1_FAIL" || "$LAST_STATE" == "BOTH_FAIL" ]]; then
                log_ok "主线 VPS1 恢复,切回主线"
                switch_all "$VPS1_IP" "恢复主线"
            else
                log_ok "双线正常,DNS 保持主线 VPS1"
            fi
            ;;
        "VPS_DOWN|OK")
            CURRENT_STATE="VPS1_FAIL"
            if [[ "$LAST_STATE" != "VPS1_FAIL" ]]; then
                log_warn "主线 VPS1 宕机,切换备线 VPS2"
                switch_all "$VPS2_IP" "切换备线"
            else
                log_warn "主线 VPS1 仍宕机,DNS 保持备线 VPS2"
            fi
            ;;
        "OK|VPS_DOWN")
            CURRENT_STATE="VPS2_FAIL"
            if [[ "$LAST_STATE" != "VPS2_FAIL" ]]; then
                log_info "备线 VPS2 宕机,确保 DNS 指向主线 VPS1"
                switch_all "$VPS1_IP" "确保主线"
            else
                log_info "备线 VPS2 仍宕机,DNS 正常指向主线 VPS1"
            fi
            ;;
        "VPS_DOWN|VPS_DOWN")
            CURRENT_STATE="BOTH_FAIL"
            log_err "双线均宕机!DNS 保持不变,等待恢复"
            ;;
    esac

    save_state "$CURRENT_STATE"
    LAST_STATE="$CURRENT_STATE"
    sleep "$CHECK_INTERVAL"
done
SCRIPT_EOF

    # ---------- 替换占位符 ----------

    # 普通字符串占位符(sed -e 批量处理)
    sed -i \
        -e "s|CF_TOKEN_PLACEHOLDER|${CF_API_TOKEN}|g" \
        -e "s|ZONE_ID_PLACEHOLDER|${CF_ZONE_ID}|g"    \
        -e "s|VPS1_IP_PLACEHOLDER|${VPS1_IP}|g"       \
        -e "s|VPS2_IP_PLACEHOLDER|${VPS2_IP}|g"       \
        -e "s|LOG_FILE_PLACEHOLDER|${LOG_FILE}|g"     \
        -e "s|STATE_FILE_PLACEHOLDER|${STATE_FILE}|g" \
        -e "s|CON1_PLACEHOLDER|${FRPC1_CONTAINER}|g"  \
        -e "s|CON2_PLACEHOLDER|${FRPC2_CONTAINER}|g"  \
        "$MONITOR_SCRIPT"

    # 数值变量(不带引号)
    sed -i \
        -e "s|FRPC1_DASH_PORT_PLACEHOLDER|${FRPC1_DASH_PORT}|g" \
        -e "s|FRPC2_DASH_PORT_PLACEHOLDER|${FRPC2_DASH_PORT}|g" \
        "$MONITOR_SCRIPT"

    # URL 静态写入(含端口数字,用 sed 安全)
    sed -i \
        -e "s|FRPC1_URL_PLACEHOLDER|http://127.0.0.1:${FRPC1_DASH_PORT}/api/status|g" \
        -e "s|FRPC2_URL_PLACEHOLDER|http://127.0.0.1:${FRPC2_DASH_PORT}/api/status|g" \
        "$MONITOR_SCRIPT"

    # 用户名、密码可能含特殊字符,用 perl \Q...\E 逐字面替换
    perl -i -pe "s|USER_PLACEHOLDER|\Q${FRPC_USER}\E|g" "$MONITOR_SCRIPT"
    perl -i -pe "s|PASS_PLACEHOLDER|\Q${FRPC_PASS}\E|g" "$MONITOR_SCRIPT"

    # 域名数组通过环境变量传给 perl,彻底避免域名中特殊字符被解释
    local DOMAIN_BLOCK
    DOMAIN_BLOCK=$(printf '    "%s"\n' "${DOMAINS[@]}")
    DOMAIN_BLOCK="$DOMAIN_BLOCK" perl -i -0pe '
        my $block = $ENV{"DOMAIN_BLOCK"};
        s/DOMAIN_ARRAY_PLACEHOLDER/$block/;
    ' "$MONITOR_SCRIPT"

    chmod +x "$MONITOR_SCRIPT"
    echo -e "  ${GREEN}监控脚本生成完毕${NC}"
}

# -------- 4. 配置 systemd 和 logrotate --------
setup_services() {
    echo -e "${GREEN}[5/5] 配置 Systemd 服务和 Logrotate...${NC}"

    cat > "$SERVICE_FILE" <<EOF
[Unit]
Description=frpc Monitor with CF DNS Failover
After=network.target docker.service
Requires=docker.service

[Service]
Type=simple
ExecStart=/bin/bash ${MONITOR_SCRIPT}
Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target
EOF

    cat > "$LOGROTATE_FILE" <<EOF
 ${LOG_FILE} {
    daily
    rotate 3
    compress
    delaycompress
    missingok
    notifempty
    copytruncate
}
EOF

    systemctl daemon-reload
    systemctl enable frpc-monitor
    systemctl restart frpc-monitor
    sleep 2

    local STATUS
    STATUS=$(systemctl is-active frpc-monitor)
    echo -e "\n${CYAN}========================================${NC}"
    if [[ "$STATUS" == "active" ]]; then
        echo -e "${GREEN}安装配置完成,服务运行正常!${NC}"
    else
        echo -e "${RED}服务启动异常,请选择「3. 查看实时日志」排查${NC}"
    fi
    echo -e "${CYAN}========================================${NC}"
    echo -e "脚本位置 : ${MONITOR_SCRIPT}"
    echo -e "服务状态 : ${STATUS}"
    echo -e "实时日志 : tail -f ${LOG_FILE}"
    echo ""
    read -p "按回车键返回主菜单..."
}

# -------- 安装主流程 --------
install_script() {
    install_deps
    collect_config || { read -p "按回车键返回主菜单..."; return; }
    generate_monitor_script
    setup_services
}

# ================================================================
# -------- 修改配置 --------
# ================================================================

# 从监控脚本中读取带双引号字符串变量值(兼容值含任意特殊字符)
_cfg_get() {
    local key=$1
    KEY="$key" perl -ne '
        if (/^\Q$ENV{KEY}\E="(.*)"$/) { print $1; exit }
    ' "$MONITOR_SCRIPT"
}

# 安全替换带双引号的字符串变量(perl 逐字面匹配,兼容特殊字符)
_cfg_set() {
    local key=$1 val=$2 old_val
    old_val=$(_cfg_get "$key")
    OLD_VAL="$old_val" NEW_VAL="$val" KEY="$key" perl -i -0pe '
        my ($k, $o, $n) = ($ENV{KEY}, $ENV{OLD_VAL}, $ENV{NEW_VAL});
        s/^\Q$k\E="\Q$o\E"/$k="$n"/m;
    ' "$MONITOR_SCRIPT"
}

# 替换不带引号的数值变量;若为 Dashboard 端口则同步更新对应 URL
_cfg_set_num() {
    local key=$1 val=$2
    sed -i "s|^${key}=.*|${key}=${val}|" "$MONITOR_SCRIPT"

    local url_key old_url new_url
    case "$key" in
        FRPC1_DASH_PORT) url_key="FRPC1_URL" ;;
        FRPC2_DASH_PORT) url_key="FRPC2_URL" ;;
        *) return ;;
    esac
    old_url=$(_cfg_get "$url_key")
    new_url="http://127.0.0.1:${val}/api/status"
    OLD_VAL="$old_url" NEW_VAL="$new_url" KEY="$url_key" perl -i -0pe '
        my ($k, $o, $n) = ($ENV{KEY}, $ENV{OLD_VAL}, $ENV{NEW_VAL});
        s/^\Q$k\E="\Q$o\E"/$k="$n"/m;
    ' "$MONITOR_SCRIPT"
}

# 修改后重启服务
_restart_service() {
    echo -e "${GREEN}正在重启 frpc-monitor 服务以应用配置...${NC}"
    systemctl restart frpc-monitor
    sleep 2
    local status
    status=$(systemctl is-active frpc-monitor)
    if [[ "$status" == "active" ]]; then
        echo -e "${GREEN}服务已重启,配置生效${NC}"
    else
        echo -e "${RED}服务重启异常,请选择「3. 查看实时日志」排查${NC}"
    fi
}

# 获取域名数组到全局变量 DOMAIN_LIST
_load_domains() {
    DOMAIN_LIST=()
    while IFS= read -r line; do
        local d
        d=$(echo "$line" | tr -d ' "')
        [[ -n "$d" ]] && DOMAIN_LIST+=("$d")
    done < <(awk '/^DOMAINS=\(/{found=1; next} found && /^\)/{exit} found{print}' "$MONITOR_SCRIPT")
}

# 将 DOMAIN_LIST 写回监控脚本
_save_domains() {
    if [[ ${#DOMAIN_LIST[@]} -eq 0 ]]; then
        echo -e "${RED}域名列表为空,不允许保存(至少保留一个域名)${NC}"
        return 1
    fi
    local DOMAIN_BLOCK
    DOMAIN_BLOCK=$(printf '    "%s"\n' "${DOMAIN_LIST[@]}")
    DOMAIN_BLOCK="$DOMAIN_BLOCK" perl -i -0pe '
        my $block = $ENV{"DOMAIN_BLOCK"};
        s/DOMAINS=\(.*?\)/DOMAINS=(\n$block\n)/s;
    ' "$MONITOR_SCRIPT"
}

# 辅助:读取多个域名(支持空格分隔),追加到 DOMAIN_LIST
_read_domains_input() {
    echo -e "${YELLOW}每行可输入一个或多个域名(空格分隔),空行回车结束:${NC}"
    while true; do
        read -p "域名: " domain_line
        [[ -z "$domain_line" ]] && break
        for d in $domain_line; do
            DOMAIN_LIST+=("$d")
            echo -e "  ${GREEN}已添加: ${d}${NC}"
        done
    done
}

# 辅助:读取并更新字符串配置项,不允许为空,自动重启服务
_update_cfg() {
    local key=$1 prompt=$2 v
    read -p "$prompt" v
    if [[ -z "$v" ]]; then
        echo -e "${RED}输入不能为空,已取消${NC}"
        read -p "按回车键继续..."; return 1
    fi
    _cfg_set "$key" "$v"
    echo -e "${GREEN}已更新${NC}"
    _restart_service
    read -p "按回车键继续..."
}

# 辅助:读取并更新数值配置项,自动重启服务
# 用法: _update_num KEY "提示语" min [max](max=0 表示不限上界)
_update_num() {
    local key=$1 prompt=$2 min=${3:-1} max=${4:-0} v valid=false
    read -p "$prompt" v
    if [[ "$v" =~ ^[0-9]+$ ]]; then
        if (( max > 0 )); then
            (( v >= min && v <= max )) && valid=true
        else
            (( v >= min )) && valid=true
        fi
    fi
    if [[ "$valid" == true ]]; then
        _cfg_set_num "$key" "$v"
        echo -e "${GREEN}已更新${NC}"
        _restart_service
    else
        (( max > 0 )) \
            && echo -e "${RED}请输入 ${min}~${max} 的整数${NC}" \
            || echo -e "${RED}请输入不小于 ${min} 的整数${NC}"
    fi
    read -p "按回车键继续..."
}

# -------- 子菜单:域名管理 --------
modify_domains() {
    while true; do
        clear
        _load_domains
        echo -e "${CYAN}==============================${NC}"
        echo -e "${CYAN}  域名管理${NC}"
        echo -e "${CYAN}==============================${NC}"
        local i=1
        for d in "${DOMAIN_LIST[@]}"; do echo "  ${i}. ${d}"; ((i++)); done
        echo ""
        echo "  a. 添加域名"
        echo "  d. 删除域名"
        echo "  0. 返回上级菜单"
        echo -e "${CYAN}==============================${NC}"
        read -p "请输入操作: " op
        case $op in
            a|A)
                local before=${#DOMAIN_LIST[@]}
                _read_domains_input
                if [[ ${#DOMAIN_LIST[@]} -gt $before ]]; then
                    _save_domains && _restart_service
                else
                    echo -e "${YELLOW}未输入任何域名,已取消${NC}"
                fi
                read -p "按回车键继续..."
                ;;
            d|D)
                if [[ ${#DOMAIN_LIST[@]} -eq 0 ]]; then
                    echo -e "${RED}当前没有域名可删除${NC}"
                    read -p "按回车键继续..."; continue
                fi
                if [[ ${#DOMAIN_LIST[@]} -eq 1 ]]; then
                    echo -e "${RED}至少需要保留一个域名,无法删除最后一项${NC}"
                    read -p "按回车键继续..."; continue
                fi
                local idx
                read -p "请输入要删除的域名序号 (1-${#DOMAIN_LIST[@]}): " idx
                if ! [[ "$idx" =~ ^[0-9]+$ ]] || (( idx < 1 || idx > ${#DOMAIN_LIST[@]} )); then
                    echo -e "${RED}无效序号${NC}"; sleep 2; continue
                fi
                local removed="${DOMAIN_LIST[$((idx-1))]}"
                DOMAIN_LIST=("${DOMAIN_LIST[@]:0:$((idx-1))}" "${DOMAIN_LIST[@]:$idx}")
                _save_domains \
                    && echo -e "${GREEN}已删除: ${removed}${NC}" \
                    && _restart_service
                read -p "按回车键继续..."
                ;;
            0) break ;;
            *) echo -e "${RED}无效操作${NC}"; sleep 1 ;;
        esac
    done
}

# -------- 子菜单:运行参数 --------
modify_runtime_params() {
    while true; do
        clear
        local cur_interval cur_ttl cur_proxied cur_rwait
        cur_interval=$(grep -m1 "^CHECK_INTERVAL=" "$MONITOR_SCRIPT" | cut -d= -f2)
        cur_ttl=$(grep -m1      "^DNS_TTL="        "$MONITOR_SCRIPT" | cut -d= -f2)
        cur_proxied=$(grep -m1  "^CF_PROXIED="     "$MONITOR_SCRIPT" | cut -d= -f2)
        cur_rwait=$(grep -m1    "^RESTART_WAIT="   "$MONITOR_SCRIPT" | cut -d= -f2)

        echo -e "${CYAN}==============================${NC}"
        echo -e "${CYAN}  运行参数设置${NC}"
        echo -e "${CYAN}==============================${NC}"
        echo "  1. 检测间隔 (CHECK_INTERVAL) : ${cur_interval}s"
        echo "  2. DNS TTL  (DNS_TTL)         : ${cur_ttl}s"
        echo "  3. CF 代理  (CF_PROXIED)      : ${cur_proxied}"
        echo "  4. 重启等待 (RESTART_WAIT)    : ${cur_rwait}s"
        echo "  0. 返回上级菜单"
        echo -e "${CYAN}==============================${NC}"
        read -p "请选择参数编号: " p
        case $p in
            1) _update_num "CHECK_INTERVAL" "新的检测间隔(秒,当前 ${cur_interval},建议 ≥10): " 10 ;;
            2) _update_num "DNS_TTL"        "新的 DNS TTL(秒,当前 ${cur_ttl}): " 1 ;;
            3)
                read -p "CF_PROXIED (true/false,当前 ${cur_proxied}): " v
                if [[ "$v" == "true" || "$v" == "false" ]]; then
                    _cfg_set_num "CF_PROXIED" "$v"
                    echo -e "${GREEN}已更新${NC}"; _restart_service
                else
                    echo -e "${RED}只能输入 true 或 false${NC}"
                fi
                read -p "按回车键继续..." ;;
            4) _update_num "RESTART_WAIT" "新的重启等待时间(秒,当前 ${cur_rwait}): " 1 ;;
            0) break ;;
            *) echo -e "${RED}无效选项${NC}"; sleep 1 ;;
        esac
    done
}

# -------- 子菜单:单项配置 --------
modify_single_item() {
    while true; do
        clear
        local cur_token cur_zone cur_vps1 cur_vps2 cur_con1 cur_con2
        local cur_dash1 cur_dash2 cur_user cur_pass_raw cur_pass stars
        cur_token=$(_cfg_get "CF_API_TOKEN")
        cur_zone=$(_cfg_get  "CF_ZONE_ID")
        cur_vps1=$(_cfg_get  "VPS1_IP")
        cur_vps2=$(_cfg_get  "VPS2_IP")
        cur_con1=$(_cfg_get  "FRPC1_CONTAINER")
        cur_con2=$(_cfg_get  "FRPC2_CONTAINER")
        cur_user=$(_cfg_get  "FRPC_USER")
        cur_dash1=$(grep -m1 "^FRPC1_DASH_PORT=" "$MONITOR_SCRIPT" | cut -d= -f2)
        cur_dash2=$(grep -m1 "^FRPC2_DASH_PORT=" "$MONITOR_SCRIPT" | cut -d= -f2)
        cur_pass_raw=$(_cfg_get "FRPC_PASS")
        stars="${cur_pass_raw//?/*}"
        if [[ -z "$cur_pass_raw" ]]; then
            cur_pass="(未设置)"
        else
            cur_pass="${cur_pass_raw:0:2}${stars:2}"
        fi

        echo -e "${CYAN}==============================${NC}"
        echo -e "${CYAN}  单项配置修改${NC}"
        echo -e "${CYAN}==============================${NC}"
        echo "   1. CF API Token          : ${cur_token:0:8}…(已隐藏)"
        echo "   2. CF Zone ID            : ${cur_zone}"
        echo "   3. 主线 VPS1 IP          : ${cur_vps1}"
        echo "   4. 备线 VPS2 IP          : ${cur_vps2}"
        echo "   5. frpc01 容器名         : ${cur_con1}"
        echo "   6. frpc02 容器名         : ${cur_con2}"
        echo "   7. frpc01 Dashboard 端口 : ${cur_dash1}"
        echo "   8. frpc02 Dashboard 端口 : ${cur_dash2}"
        echo "   9. Dashboard 用户名      : ${cur_user}"
        echo "  10. Dashboard 密码        : ${cur_pass}"
        echo "   0. 返回上级菜单"
        echo -e "${CYAN}==============================${NC}"
        read -p "请选择项目编号: " item
        case $item in
            1)  _update_cfg "CF_API_TOKEN"    "新的 CF API Token: " ;;
            2)  _update_cfg "CF_ZONE_ID"      "新的 CF Zone ID: " ;;
            3)  _update_cfg "VPS1_IP"         "新的 VPS1 IP: " ;;
            4)  _update_cfg "VPS2_IP"         "新的 VPS2 IP: " ;;
            5)  _update_cfg "FRPC1_CONTAINER" "新的 frpc01 容器名: " ;;
            6)  _update_cfg "FRPC2_CONTAINER" "新的 frpc02 容器名: " ;;
            7)  _update_num "FRPC1_DASH_PORT" "新的 frpc01 Dashboard 端口(当前 ${cur_dash1}): " 1 65535 ;;
            8)  _update_num "FRPC2_DASH_PORT" "新的 frpc02 Dashboard 端口(当前 ${cur_dash2}): " 1 65535 ;;
            9)  _update_cfg "FRPC_USER"       "新的 Dashboard 用户名: " ;;
            10) _update_cfg "FRPC_PASS"       "新的 Dashboard 密码: " ;;
            0) break ;;
            *) echo -e "${RED}无效选项${NC}"; sleep 1 ;;
        esac
    done
}

# -------- 修改配置入口 --------
modify_config() {
    if [[ ! -f "$MONITOR_SCRIPT" ]]; then
        echo -e "${RED}未找到监控脚本 ${MONITOR_SCRIPT},请先执行「1. 安装/配置监控脚本」${NC}"
        read -p "按回车键返回主菜单..."; return
    fi

    while true; do
        clear
        echo -e "${CYAN}========================================${NC}"
        echo -e "${CYAN}  修改配置${NC}"
        echo -e "${CYAN}========================================${NC}"
        echo "  1. 修改单项配置   (Token / IP / 容器名 / 端口 / 密码等)"
        echo "  2. 管理域名列表   (添加 / 删除域名)"
        echo "  3. 修改运行参数   (检测间隔 / TTL / 代理模式等)"
        echo "  4. 重新向导配置   (覆盖所有配置,重新完整填写)"
        echo "  0. 返回主菜单"
        echo -e "${CYAN}========================================${NC}"
        read -p "请输入选项编号: " sub
        case $sub in
            1) modify_single_item ;;
            2) modify_domains ;;
            3) modify_runtime_params ;;
            4)
                echo -e "${YELLOW}此操作将覆盖当前所有配置,重新引导填写!${NC}"
                read -p "确认继续?(y/n): " confirm
                if [[ "$confirm" == "y" ]]; then
                    if collect_config; then
                        generate_monitor_script
                        _restart_service
                        echo -e "${GREEN}配置已全部更新${NC}"
                    fi
                    read -p "按回车键继续..."
                fi
                ;;
            0) break ;;
            *) echo -e "${RED}无效选项${NC}"; sleep 1 ;;
        esac
    done
}

# ================================================================
# -------- 查看日志 --------
# ================================================================
view_logs() {
    if [[ ! -f "$LOG_FILE" ]]; then
        echo -e "${RED}日志文件不存在,服务可能尚未启动${NC}"
        read -p "按回车键返回主菜单..."; return
    fi
    echo -e "${GREEN}实时日志输出中,按 Ctrl+C 退出查看...${NC}"
    echo ""
    
    # 使用 sed -u 实时替换英文状态码为中文
    # UNKNOWN -> 未知
    # BOTH_OK -> 双线正常
    # VPS1_FAIL -> 主线故障
    # VPS2_FAIL -> 备线故障
    # BOTH_FAIL -> 双线故障
    # OK -> 正常
    # VPS_DOWN -> 连接中断
    tail -f "$LOG_FILE" | sed -u \
        -e 's/\[OK\]/[√]/g' \
        -e 's/\[!!\]/[!]/g' \
        -e 's/\[XX\]/[✗]/g' \
        -e 's/\[--\]/[-]/g' \
        -e 's/\[>>\]/[>]/g' \
        -e 's/\[\.\.\]/[.]/g' \
        -e 's/UNKNOWN/未知/g' \
        -e 's/BOTH_OK/双线正常/g' \
        -e 's/VPS1_FAIL/主线故障/g' \
        -e 's/VPS2_FAIL/备线故障/g' \
        -e 's/BOTH_FAIL/双线故障/g' \
        -e 's/\bOK\b/正常/g' \
        -e 's/VPS_DOWN/连接中断/g'
}

# ================================================================
# -------- 卸载 --------
# ================================================================
uninstall_script() {
    echo -e "${YELLOW}此操作将停止服务并删除所有相关文件(包括本脚本)!${NC}"
    echo ""
    read -p "确认卸载?(y/n): " confirm
    [[ "$confirm" != "y" ]] && return

    echo -e "${RED}正在卸载...${NC}"
    systemctl stop    frpc-monitor 2>/dev/null
    systemctl disable frpc-monitor 2>/dev/null
    rm -f "$SERVICE_FILE"
    systemctl daemon-reload
    systemctl reset-failed 2>/dev/null

    rm -f "$MONITOR_SCRIPT" "$INSTALL_DEPS_SCRIPT" "${LOG_FILE}"* "$STATE_FILE" "$LOGROTATE_FILE"

    echo -e "\n${CYAN}--- 验证清理结果 ---${NC}"
    for f in "$MONITOR_SCRIPT" "$INSTALL_DEPS_SCRIPT" "$SERVICE_FILE" "$LOG_FILE" "$LOGROTATE_FILE" "$STATE_FILE"; do
        if [[ ! -e "$f" ]]; then
            echo -e "  ${GREEN}$f 已删除${NC}"
        else
            echo -e "  ${RED}$f 仍存在${NC}"
        fi
    done

    echo ""
    echo -e "  服务状态: $(systemctl is-active frpc-monitor 2>&1)"
    echo ""
    echo -e "${GREEN}卸载完成${NC}"
    echo -e "${YELLOW}本脚本将在退出后自动删除:$0${NC}"
    echo ""
    read -p "按回车键退出并完成清理..."

    # 后台延迟删除脚本自身,留出主进程退出时间
    (sleep 1 && rm -f "$0") &
    exit 0
}

# ================================================================
# 主循环
# ================================================================
while true; do
    print_menu
    read -p "请输入选项编号: " choice
    case $choice in
        1) install_script ;;
        2) modify_config ;;
        3) view_logs ;;
        4) uninstall_script ;;
        0) echo -e "退出"; exit 0 ;;
        *) echo -e "${RED}无效选项${NC}"; sleep 2 ;;
    esac
done
#!/bin/bash
# =============================================================
# frpc 双线监控 + Cloudflare DNS 自动切换 - 管理脚本 v1.1
# =============================================================

# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
NC='\033[0m'

# 路径定义
INSTALL_DEPS_SCRIPT="/opt/install-deps.sh"
MONITOR_SCRIPT="/opt/frpc-monitor.sh"
SERVICE_FILE="/etc/systemd/system/frpc-monitor.service"
LOGROTATE_FILE="/etc/logrotate.d/frpc-monitor"
LOG_FILE="/var/log/frpc-monitor.log"
STATE_FILE="/tmp/frpc_monitor_state"

# 检查权限
if [[ $EUID -ne 0 ]]; then
    echo -e "${RED}请使用 root 用户或 sudo 运行此脚本${NC}"
    exit 1
fi

# ================================================================
# 主菜单
# ================================================================
print_menu() {
    clear
    echo -e "${CYAN}========================================${NC}"
    echo -e "${CYAN}  frpc 双线监控 + CF DNS 自动切换管理器${NC}"
    echo -e "${CYAN}========================================${NC}"
    echo "  1. 安装/配置监控脚本 (首次安装)"
    echo -e "  ${YELLOW}2. 修改配置${NC}"
    echo "  3. 查看实时日志"
    echo "  4. 卸载并清理所有文件"
    echo -e "  ${YELLOW}0. 退出${NC}"
    echo -e "${CYAN}========================================${NC}"
}

# ================================================================
# -------- 安装流程 --------
# ================================================================

# -------- 1. 安装依赖(curl / jq / docker)--------
install_deps() {
    echo -e "${GREEN}[1/5] 检查并安装系统依赖 (curl, jq, docker)...${NC}"

    cat <<'DEPS_EOF' > "$INSTALL_DEPS_SCRIPT"
#!/bin/bash
if command -v apt-get &>/dev/null; then
    PKG_MANAGER="apt-get"; UPDATE_CMD="apt-get update -y"; INSTALL_CMD="apt-get install -y"
elif command -v dnf &>/dev/null; then
    PKG_MANAGER="dnf";     UPDATE_CMD="dnf makecache -y";  INSTALL_CMD="dnf install -y"
elif command -v yum &>/dev/null; then
    PKG_MANAGER="yum";     UPDATE_CMD="yum makecache -y";  INSTALL_CMD="yum install -y"
else
    echo "未识别的包管理器,请手动安装:curl、jq"; exit 1
fi
 $UPDATE_CMD &>/dev/null

install_pkg() {
    local name=$1 cmd=$2
    if command -v "$cmd" &>/dev/null; then
        echo "  ${name} 已安装"
    else
        echo "  安装 ${name}..."
        $INSTALL_CMD "$name" &>/dev/null \
            && echo "  ${name} 安装成功" \
            || { echo "  ${name} 安装失败"; exit 1; }
    fi
}

install_pkg "curl"   "curl"
install_pkg "jq"     "jq"
install_pkg "docker" "docker"
DEPS_EOF

    chmod +x "$INSTALL_DEPS_SCRIPT"
    bash "$INSTALL_DEPS_SCRIPT"
}

# -------- 辅助:读取必填字符串,不允许为空 --------
# 用法: _read_required VAR "提示语"
_read_required() {
    local _var=$1 _prompt=$2 _val
    while true; do
        read -p "$_prompt" _val
        if [[ -n "$_val" ]]; then
            printf -v "$_var" '%s' "$_val"
            return
        fi
        echo -e "  ${RED}不能为空,请重新输入${NC}"
    done
}

# -------- 辅助:读取端口(1-65535),允许回车使用默认值 --------
# 用法: _read_port VAR "提示语" 默认值
_read_port() {
    local _var=$1 _prompt=$2 _default=$3 _val
    while true; do
        read -p "$_prompt" _val
        _val=${_val:-$_default}
        if [[ "$_val" =~ ^[0-9]+$ ]] && (( _val >= 1 && _val <= 65535 )); then
            printf -v "$_var" '%s' "$_val"
            return
        fi
        echo -e "  ${RED}请输入 1~65535 的整数${NC}"
    done
}

# -------- 2. 交互式采集配置 --------
# 返回值:0=成功,1=用户取消(未输入域名)
collect_config() {
    echo -e "\n${GREEN}[2/5] 检测 frpc 容器...${NC}"
    mapfile -t FRPC_CONTAINERS < <(docker ps --format "{{.Names}}" 2>/dev/null | grep frpc | sort)

    if [[ ${#FRPC_CONTAINERS[@]} -eq 0 ]]; then
        echo -e "${YELLOW}未检测到运行中的 frpc 容器,请确保 Docker 已启动且容器名包含 frpc${NC}"
    else
        echo -e "${GREEN}检测到以下 frpc 容器:${NC}"
        for c in "${FRPC_CONTAINERS[@]}"; do echo "  - $c"; done
    fi

    echo -e "\n${CYAN}[3/5] 请输入配置信息:${NC}"

    _read_required CF_API_TOKEN "Cloudflare API Token: "
    _read_required CF_ZONE_ID   "Cloudflare Zone ID: "
    _read_required VPS1_IP      "主线 VPS1 IP: "
    _read_required VPS2_IP      "备线 VPS2 IP: "

    echo -e "\n${CYAN}--- frpc 容器名 ---${NC}"
    if [[ ${#FRPC_CONTAINERS[@]} -ge 2 ]]; then
        read -p "frpc01 容器名 (默认 ${FRPC_CONTAINERS[0]}): " C1
        FRPC1_CONTAINER=${C1:-${FRPC_CONTAINERS[0]}}
        read -p "frpc02 容器名 (默认 ${FRPC_CONTAINERS[1]}): " C2
        FRPC2_CONTAINER=${C2:-${FRPC_CONTAINERS[1]}}
    else
        echo -e "${YELLOW}未自动检测到足够容器,请手动输入${NC}"
        _read_required FRPC1_CONTAINER "frpc01 容器名: "
        _read_required FRPC2_CONTAINER "frpc02 容器名: "
    fi

    echo -e "\n${CYAN}--- frpc Dashboard 端口 ---${NC}"
    _read_port FRPC1_DASH_PORT "frpc01 Dashboard 端口 (默认 7401): " 7401
    _read_port FRPC2_DASH_PORT "frpc02 Dashboard 端口 (默认 7402): " 7402

    echo -e "\n${CYAN}--- frpc Dashboard 认证 ---${NC}"
    _read_required FRPC_USER "Dashboard 用户名: "
    _read_required FRPC_PASS "Dashboard 密码: "

    echo -e "\n${CYAN}--- 需要切换的域名 ---${NC}"
    DOMAIN_LIST=()
    _read_domains_input
    DOMAINS=("${DOMAIN_LIST[@]}")

    if [[ ${#DOMAINS[@]} -eq 0 ]]; then
        echo -e "${RED}未输入任何域名,操作取消${NC}"
        return 1
    fi

    echo -e "\n${GREEN}已录入 ${#DOMAINS[@]} 个域名:${NC}"
    for d in "${DOMAINS[@]}"; do echo "  - $d"; done
    return 0
}

# -------- 3. 生成主监控脚本 --------
generate_monitor_script() {
    echo -e "\n${GREEN}[4/5] 生成监控脚本...${NC}"

    # heredoc 使用单引号定界符,防止变量展开。
    # FRPC1_URL / FRPC2_URL 使用占位符,由后续 sed 替换为静态字符串,
    # 避免 heredoc 内 ${FRPC1_DASH_PORT} 因未赋值而展开为空。
    cat <<'SCRIPT_EOF' > "$MONITOR_SCRIPT"
#!/bin/bash
# =============================================================
# frpc 双线监控 + Cloudflare DNS 自动切换脚本
# 依赖:curl / jq / docker
# =============================================================

CF_API_TOKEN="CF_TOKEN_PLACEHOLDER"
CF_ZONE_ID="ZONE_ID_PLACEHOLDER"

VPS1_IP="VPS1_IP_PLACEHOLDER"
VPS2_IP="VPS2_IP_PLACEHOLDER"

DOMAINS=(
DOMAIN_ARRAY_PLACEHOLDER
)

FRPC1_DASH_PORT=FRPC1_DASH_PORT_PLACEHOLDER
FRPC2_DASH_PORT=FRPC2_DASH_PORT_PLACEHOLDER
FRPC1_URL="FRPC1_URL_PLACEHOLDER"
FRPC2_URL="FRPC2_URL_PLACEHOLDER"
FRPC_USER="USER_PLACEHOLDER"
FRPC_PASS="PASS_PLACEHOLDER"

FRPC1_CONTAINER="CON1_PLACEHOLDER"
FRPC2_CONTAINER="CON2_PLACEHOLDER"

DNS_TTL=60
CHECK_INTERVAL=30
RESTART_WAIT=15
CF_PROXIED=false

LOG_FILE="LOG_FILE_PLACEHOLDER"
STATE_FILE="STATE_FILE_PLACEHOLDER"

log()        { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE" >&2; }
log_ok()     { log "[OK] $1"; }
log_warn()   { log "[!!] $1"; }
log_err()    { log "[XX] $1"; }
log_info()   { log "[--] $1"; }
log_status() { log "[ST] $1"; }
load_state() { [[ -f "$STATE_FILE" ]] && source "$STATE_FILE" || LAST_STATE="UNKNOWN"; }
save_state() { echo "LAST_STATE=\"$1\"" > "$STATE_FILE"; }

check_dashboard() {
    local code
    code=$(curl -s -o /dev/null -w "%{http_code}" \
        --connect-timeout 5 --max-time 8 \
        -u "${FRPC_USER}:${FRPC_PASS}" "$1" 2>/dev/null)
    [[ "$code" == "200" ]]
}

restart_frpc() {
    local container=$1
    log_info "  重启容器: ${container}"
    if docker restart "$container" &>/dev/null; then
        log_ok "  ${container} 重启成功,等待 ${RESTART_WAIT}s"
        sleep "$RESTART_WAIT"
        return 0
    else
        log_err "  ${container} 重启失败"
        return 1
    fi
}

get_vps_status() {
    local dashboard_url=$1 container=$2

    if ! check_dashboard "$dashboard_url"; then
        log_warn "${container}: Dashboard 无响应,尝试重启"
        if restart_frpc "$container" && check_dashboard "$dashboard_url"; then
            log_ok "${container} 重启后 Dashboard 恢复"
        else
            log_err "${container} 重启后仍无响应,按宕机处理"
            echo "VPS_DOWN"; return
        fi
    fi

    local result
    result=$(curl -s --connect-timeout 5 --max-time 8 \
        -u "${FRPC_USER}:${FRPC_PASS}" "$dashboard_url" 2>/dev/null)

    if echo "$result" | jq -e '.. | objects | select(.status == "running")' &>/dev/null; then
        echo "OK"
    else
        log_err "${container}: Dashboard 正常但隧道全部离线,按宕机处理"
        echo "VPS_DOWN"
    fi
}

get_record_info() {
    local domain=$1 response
    response=$(curl -s -X GET \
        "https://api.cloudflare.com/client/v4/zones/${CF_ZONE_ID}/dns_records?type=A&name=${domain}" \
        -H "Authorization: Bearer ${CF_API_TOKEN}" \
        -H "Content-Type: application/json")
    local record_id current_ip
    record_id=$(echo "$response"  | jq -r '.result[0].id      // ""')
    current_ip=$(echo "$response" | jq -r '.result[0].content // ""')
    echo "${record_id}|${current_ip}"
}

update_record() {
    local domain=$1 target_ip=$2
    [[ -z "$domain" ]] && return 2

    local info record_id current_ip
    local max_retries=3 attempt delay

    # ---------- 第一步:获取记录 ID(含重试)----------
    for (( attempt=1; attempt<=max_retries; attempt++ )); do
        info=$(get_record_info "$domain")
        record_id=$(echo "$info" | cut -d'|' -f1)
        current_ip=$(echo "$info" | cut -d'|' -f2)
        [[ -n "$record_id" ]] && break
        delay=$(( attempt * 2 ))
        log_warn "  | ${domain} -> 获取记录失败 (第${attempt}/${max_retries}次),${delay}s 后重试"
        sleep "$delay"
    done

    if [[ -z "$record_id" ]]; then
        log_err "  | ${domain} -> 获取记录失败,已重试 ${max_retries} 次,放弃"
        return 1
    fi

    if [[ "$current_ip" == "$target_ip" ]]; then
        log_info "  | ${domain} -> 已是 ${target_ip},无需更新"
        return 0
    fi

    # ---------- 第二步:更新记录(含重试)----------
    local response success
    for (( attempt=1; attempt<=max_retries; attempt++ )); do
        response=$(curl -s -X PUT \
            "https://api.cloudflare.com/client/v4/zones/${CF_ZONE_ID}/dns_records/${record_id}" \
            -H "Authorization: Bearer ${CF_API_TOKEN}" \
            -H "Content-Type: application/json" \
            --data "{\"type\":\"A\",\"name\":\"${domain}\",\"content\":\"${target_ip}\",\"ttl\":${DNS_TTL},\"proxied\":${CF_PROXIED}}")
        success=$(echo "$response" | jq -r '.success')
        if [[ "$success" == "true" ]]; then
            log_ok "  | ${domain} -> ${current_ip} => ${target_ip}"
            return 0
        fi
        delay=$(( attempt * 2 ))
        log_warn "  | ${domain} -> 更新失败 (第${attempt}/${max_retries}次),${delay}s 后重试"
        sleep "$delay"
    done

    log_err "  | ${domain} -> 更新失败,已重试 ${max_retries} 次,放弃: $(echo "$response" | jq -r '.errors[0].message // "未知错误"')"
    return 1
}

switch_all() {
    local target_ip=$1 label=$2 ok=0 fail=0
    local failed_domains=()

    log "========================================"
    log "[>>] 批量切换 -> ${target_ip}  (${label})"
    log "----------------------------------------"

    for domain in "${DOMAINS[@]}"; do
        update_record "$domain" "$target_ip"
        case $? in
            0) ((ok++)) ;;
            1) ((fail++)); failed_domains+=("$domain") ;;
        esac
        sleep 0.5
    done

    # ---------- 对失败域名做一轮补偿重试 ----------
    if [[ ${#failed_domains[@]} -gt 0 ]]; then
        log "----------------------------------------"
        log_warn "对 ${#failed_domains[@]} 个失败域名进行补偿重试 (等待 5s)..."
        sleep 5
        local retry_ok=0 retry_fail=0
        for domain in "${failed_domains[@]}"; do
            update_record "$domain" "$target_ip"
            case $? in
                0) ((retry_ok++)); ((ok++)); ((fail--)) ;;
                1) ((retry_fail++)) ;;
            esac
            sleep 1
        done
        log_info "补偿结果: 补救成功 ${retry_ok} 个,仍失败 ${retry_fail} 个"
    fi

    log "----------------------------------------"
    if [[ $fail -eq 0 ]]; then
        log_ok "完成:全部 ${ok} 个成功"
    else
        log_warn "完成:成功 ${ok} 个,最终失败 ${fail} 个"
    fi
    log "========================================"
}

log "========================================"
log "[>>] frpc 监控启动 | 域名数:${#DOMAINS[@]} | VPS1:${VPS1_IP} | VPS2:${VPS2_IP} | 间隔:${CHECK_INTERVAL}s"
log "========================================"

load_state

while true; do
    VPS1_STATUS=$(get_vps_status "$FRPC1_URL" "$FRPC1_CONTAINER")
    VPS2_STATUS=$(get_vps_status "$FRPC2_URL" "$FRPC2_CONTAINER")

    log_status "VPS1:${VPS1_STATUS} VPS2:${VPS2_STATUS} 上次:${LAST_STATE}"

    case "${VPS1_STATUS}|${VPS2_STATUS}" in
        "OK|OK")
            CURRENT_STATE="BOTH_OK"
            if [[ "$LAST_STATE" == "VPS1_FAIL" || "$LAST_STATE" == "BOTH_FAIL" ]]; then
                log_ok "主线 VPS1 恢复,切回主线"
                switch_all "$VPS1_IP" "恢复主线"
            else
                log_ok "双线正常,DNS 保持主线 VPS1"
            fi
            ;;
        "VPS_DOWN|OK")
            CURRENT_STATE="VPS1_FAIL"
            if [[ "$LAST_STATE" != "VPS1_FAIL" ]]; then
                log_warn "主线 VPS1 宕机,切换备线 VPS2"
                switch_all "$VPS2_IP" "切换备线"
            else
                log_warn "主线 VPS1 仍宕机,DNS 保持备线 VPS2"
            fi
            ;;
        "OK|VPS_DOWN")
            CURRENT_STATE="VPS2_FAIL"
            if [[ "$LAST_STATE" != "VPS2_FAIL" ]]; then
                log_info "备线 VPS2 宕机,确保 DNS 指向主线 VPS1"
                switch_all "$VPS1_IP" "确保主线"
            else
                log_info "备线 VPS2 仍宕机,DNS 正常指向主线 VPS1"
            fi
            ;;
        "VPS_DOWN|VPS_DOWN")
            CURRENT_STATE="BOTH_FAIL"
            log_err "双线均宕机!DNS 保持不变,等待恢复"
            ;;
    esac

    save_state "$CURRENT_STATE"
    LAST_STATE="$CURRENT_STATE"
    sleep "$CHECK_INTERVAL"
done
SCRIPT_EOF

    # ---------- 替换占位符 ----------

    # 普通字符串占位符(sed -e 批量处理)
    sed -i \
        -e "s|CF_TOKEN_PLACEHOLDER|${CF_API_TOKEN}|g" \
        -e "s|ZONE_ID_PLACEHOLDER|${CF_ZONE_ID}|g"    \
        -e "s|VPS1_IP_PLACEHOLDER|${VPS1_IP}|g"       \
        -e "s|VPS2_IP_PLACEHOLDER|${VPS2_IP}|g"       \
        -e "s|LOG_FILE_PLACEHOLDER|${LOG_FILE}|g"     \
        -e "s|STATE_FILE_PLACEHOLDER|${STATE_FILE}|g" \
        -e "s|CON1_PLACEHOLDER|${FRPC1_CONTAINER}|g"  \
        -e "s|CON2_PLACEHOLDER|${FRPC2_CONTAINER}|g"  \
        "$MONITOR_SCRIPT"

    # 数值变量(不带引号)
    sed -i \
        -e "s|FRPC1_DASH_PORT_PLACEHOLDER|${FRPC1_DASH_PORT}|g" \
        -e "s|FRPC2_DASH_PORT_PLACEHOLDER|${FRPC2_DASH_PORT}|g" \
        "$MONITOR_SCRIPT"

    # URL 静态写入(含端口数字,用 sed 安全)
    sed -i \
        -e "s|FRPC1_URL_PLACEHOLDER|http://127.0.0.1:${FRPC1_DASH_PORT}/api/status|g" \
        -e "s|FRPC2_URL_PLACEHOLDER|http://127.0.0.1:${FRPC2_DASH_PORT}/api/status|g" \
        "$MONITOR_SCRIPT"

    # 用户名、密码可能含特殊字符,用 perl \Q...\E 逐字面替换
    perl -i -pe "s|USER_PLACEHOLDER|\Q${FRPC_USER}\E|g" "$MONITOR_SCRIPT"
    perl -i -pe "s|PASS_PLACEHOLDER|\Q${FRPC_PASS}\E|g" "$MONITOR_SCRIPT"

    # 域名数组通过环境变量传给 perl,彻底避免域名中特殊字符被解释
    local DOMAIN_BLOCK
    DOMAIN_BLOCK=$(printf '    "%s"\n' "${DOMAINS[@]}")
    DOMAIN_BLOCK="$DOMAIN_BLOCK" perl -i -0pe '
        my $block = $ENV{"DOMAIN_BLOCK"};
        s/DOMAIN_ARRAY_PLACEHOLDER/$block/;
    ' "$MONITOR_SCRIPT"

    chmod +x "$MONITOR_SCRIPT"
    echo -e "  ${GREEN}监控脚本生成完毕${NC}"
}

# -------- 4. 配置 systemd 和 logrotate --------
setup_services() {
    echo -e "${GREEN}[5/5] 配置 Systemd 服务和 Logrotate...${NC}"

    cat > "$SERVICE_FILE" <<EOF
[Unit]
Description=frpc Monitor with CF DNS Failover
After=network.target docker.service
Requires=docker.service

[Service]
Type=simple
ExecStart=/bin/bash ${MONITOR_SCRIPT}
Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target
EOF

    cat > "$LOGROTATE_FILE" <<EOF
 ${LOG_FILE} {
    daily
    rotate 3
    compress
    delaycompress
    missingok
    notifempty
    copytruncate
}
EOF

    systemctl daemon-reload
    systemctl enable frpc-monitor
    systemctl restart frpc-monitor
    sleep 2

    local STATUS
    STATUS=$(systemctl is-active frpc-monitor)
    echo -e "\n${CYAN}========================================${NC}"
    if [[ "$STATUS" == "active" ]]; then
        echo -e "${GREEN}安装配置完成,服务运行正常!${NC}"
    else
        echo -e "${RED}服务启动异常,请选择「3. 查看实时日志」排查${NC}"
    fi
    echo -e "${CYAN}========================================${NC}"
    echo -e "脚本位置 : ${MONITOR_SCRIPT}"
    echo -e "服务状态 : ${STATUS}"
    echo -e "实时日志 : tail -f ${LOG_FILE}"
    echo ""
    read -p "按回车键返回主菜单..."
}

# -------- 安装主流程 --------
install_script() {
    install_deps
    collect_config || { read -p "按回车键返回主菜单..."; return; }
    generate_monitor_script
    setup_services
}

# ================================================================
# -------- 修改配置 --------
# ================================================================

# 从监控脚本中读取带双引号字符串变量值(兼容值含任意特殊字符)
_cfg_get() {
    local key=$1
    KEY="$key" perl -ne '
        if (/^\Q$ENV{KEY}\E="(.*)"$/) { print $1; exit }
    ' "$MONITOR_SCRIPT"
}

# 安全替换带双引号的字符串变量(perl 逐字面匹配,兼容特殊字符)
_cfg_set() {
    local key=$1 val=$2 old_val
    old_val=$(_cfg_get "$key")
    OLD_VAL="$old_val" NEW_VAL="$val" KEY="$key" perl -i -0pe '
        my ($k, $o, $n) = ($ENV{KEY}, $ENV{OLD_VAL}, $ENV{NEW_VAL});
        s/^\Q$k\E="\Q$o\E"/$k="$n"/m;
    ' "$MONITOR_SCRIPT"
}

# 替换不带引号的数值变量;若为 Dashboard 端口则同步更新对应 URL
_cfg_set_num() {
    local key=$1 val=$2
    sed -i "s|^${key}=.*|${key}=${val}|" "$MONITOR_SCRIPT"

    local url_key old_url new_url
    case "$key" in
        FRPC1_DASH_PORT) url_key="FRPC1_URL" ;;
        FRPC2_DASH_PORT) url_key="FRPC2_URL" ;;
        *) return ;;
    esac
    old_url=$(_cfg_get "$url_key")
    new_url="http://127.0.0.1:${val}/api/status"
    OLD_VAL="$old_url" NEW_VAL="$new_url" KEY="$url_key" perl -i -0pe '
        my ($k, $o, $n) = ($ENV{KEY}, $ENV{OLD_VAL}, $ENV{NEW_VAL});
        s/^\Q$k\E="\Q$o\E"/$k="$n"/m;
    ' "$MONITOR_SCRIPT"
}

# 修改后重启服务
_restart_service() {
    echo -e "${GREEN}正在重启 frpc-monitor 服务以应用配置...${NC}"
    systemctl restart frpc-monitor
    sleep 2
    local status
    status=$(systemctl is-active frpc-monitor)
    if [[ "$status" == "active" ]]; then
        echo -e "${GREEN}服务已重启,配置生效${NC}"
    else
        echo -e "${RED}服务重启异常,请选择「3. 查看实时日志」排查${NC}"
    fi
}

# 获取域名数组到全局变量 DOMAIN_LIST
_load_domains() {
    DOMAIN_LIST=()
    while IFS= read -r line; do
        local d
        d=$(echo "$line" | tr -d ' "')
        [[ -n "$d" ]] && DOMAIN_LIST+=("$d")
    done < <(awk '/^DOMAINS=\(/{found=1; next} found && /^\)/{exit} found{print}' "$MONITOR_SCRIPT")
}

# 将 DOMAIN_LIST 写回监控脚本
_save_domains() {
    if [[ ${#DOMAIN_LIST[@]} -eq 0 ]]; then
        echo -e "${RED}域名列表为空,不允许保存(至少保留一个域名)${NC}"
        return 1
    fi
    local DOMAIN_BLOCK
    DOMAIN_BLOCK=$(printf '    "%s"\n' "${DOMAIN_LIST[@]}")
    DOMAIN_BLOCK="$DOMAIN_BLOCK" perl -i -0pe '
        my $block = $ENV{"DOMAIN_BLOCK"};
        s/DOMAINS=\(.*?\)/DOMAINS=(\n$block\n)/s;
    ' "$MONITOR_SCRIPT"
}

# 辅助:读取多个域名(支持空格分隔),追加到 DOMAIN_LIST
_read_domains_input() {
    echo -e "${YELLOW}每行可输入一个或多个域名(空格分隔),空行回车结束:${NC}"
    while true; do
        read -p "域名: " domain_line
        [[ -z "$domain_line" ]] && break
        for d in $domain_line; do
            DOMAIN_LIST+=("$d")
            echo -e "  ${GREEN}已添加: ${d}${NC}"
        done
    done
}

# 辅助:读取并更新字符串配置项,不允许为空,自动重启服务
_update_cfg() {
    local key=$1 prompt=$2 v
    read -p "$prompt" v
    if [[ -z "$v" ]]; then
        echo -e "${RED}输入不能为空,已取消${NC}"
        read -p "按回车键继续..."; return 1
    fi
    _cfg_set "$key" "$v"
    echo -e "${GREEN}已更新${NC}"
    _restart_service
    read -p "按回车键继续..."
}

# 辅助:读取并更新数值配置项,自动重启服务
# 用法: _update_num KEY "提示语" min [max](max=0 表示不限上界)
_update_num() {
    local key=$1 prompt=$2 min=${3:-1} max=${4:-0} v valid=false
    read -p "$prompt" v
    if [[ "$v" =~ ^[0-9]+$ ]]; then
        if (( max > 0 )); then
            (( v >= min && v <= max )) && valid=true
        else
            (( v >= min )) && valid=true
        fi
    fi
    if [[ "$valid" == true ]]; then
        _cfg_set_num "$key" "$v"
        echo -e "${GREEN}已更新${NC}"
        _restart_service
    else
        (( max > 0 )) \
            && echo -e "${RED}请输入 ${min}~${max} 的整数${NC}" \
            || echo -e "${RED}请输入不小于 ${min} 的整数${NC}"
    fi
    read -p "按回车键继续..."
}

# -------- 子菜单:域名管理 --------
modify_domains() {
    while true; do
        clear
        _load_domains
        echo -e "${CYAN}==============================${NC}"
        echo -e "${CYAN}  域名管理${NC}"
        echo -e "${CYAN}==============================${NC}"
        local i=1
        for d in "${DOMAIN_LIST[@]}"; do echo "  ${i}. ${d}"; ((i++)); done
        echo ""
        echo "  a. 添加域名"
        echo "  d. 删除域名"
        echo "  0. 返回上级菜单"
        echo -e "${CYAN}==============================${NC}"
        read -p "请输入操作: " op
        case $op in
            a|A)
                local before=${#DOMAIN_LIST[@]}
                _read_domains_input
                if [[ ${#DOMAIN_LIST[@]} -gt $before ]]; then
                    _save_domains && _restart_service
                else
                    echo -e "${YELLOW}未输入任何域名,已取消${NC}"
                fi
                read -p "按回车键继续..."
                ;;
            d|D)
                if [[ ${#DOMAIN_LIST[@]} -eq 0 ]]; then
                    echo -e "${RED}当前没有域名可删除${NC}"
                    read -p "按回车键继续..."; continue
                fi
                if [[ ${#DOMAIN_LIST[@]} -eq 1 ]]; then
                    echo -e "${RED}至少需要保留一个域名,无法删除最后一项${NC}"
                    read -p "按回车键继续..."; continue
                fi
                local idx
                read -p "请输入要删除的域名序号 (1-${#DOMAIN_LIST[@]}): " idx
                if ! [[ "$idx" =~ ^[0-9]+$ ]] || (( idx < 1 || idx > ${#DOMAIN_LIST[@]} )); then
                    echo -e "${RED}无效序号${NC}"; sleep 2; continue
                fi
                local removed="${DOMAIN_LIST[$((idx-1))]}"
                DOMAIN_LIST=("${DOMAIN_LIST[@]:0:$((idx-1))}" "${DOMAIN_LIST[@]:$idx}")
                _save_domains \
                    && echo -e "${GREEN}已删除: ${removed}${NC}" \
                    && _restart_service
                read -p "按回车键继续..."
                ;;
            0) break ;;
            *) echo -e "${RED}无效操作${NC}"; sleep 1 ;;
        esac
    done
}

# -------- 子菜单:运行参数 --------
modify_runtime_params() {
    while true; do
        clear
        local cur_interval cur_ttl cur_proxied cur_rwait
        cur_interval=$(grep -m1 "^CHECK_INTERVAL=" "$MONITOR_SCRIPT" | cut -d= -f2)
        cur_ttl=$(grep -m1      "^DNS_TTL="        "$MONITOR_SCRIPT" | cut -d= -f2)
        cur_proxied=$(grep -m1  "^CF_PROXIED="     "$MONITOR_SCRIPT" | cut -d= -f2)
        cur_rwait=$(grep -m1    "^RESTART_WAIT="   "$MONITOR_SCRIPT" | cut -d= -f2)

        echo -e "${CYAN}==============================${NC}"
        echo -e "${CYAN}  运行参数设置${NC}"
        echo -e "${CYAN}==============================${NC}"
        echo "  1. 检测间隔 (CHECK_INTERVAL) : ${cur_interval}s"
        echo "  2. DNS TTL  (DNS_TTL)         : ${cur_ttl}s"
        echo "  3. CF 代理  (CF_PROXIED)      : ${cur_proxied}"
        echo "  4. 重启等待 (RESTART_WAIT)    : ${cur_rwait}s"
        echo "  0. 返回上级菜单"
        echo -e "${CYAN}==============================${NC}"
        read -p "请选择参数编号: " p
        case $p in
            1) _update_num "CHECK_INTERVAL" "新的检测间隔(秒,当前 ${cur_interval},建议 ≥10): " 10 ;;
            2) _update_num "DNS_TTL"        "新的 DNS TTL(秒,当前 ${cur_ttl}): " 1 ;;
            3)
                read -p "CF_PROXIED (true/false,当前 ${cur_proxied}): " v
                if [[ "$v" == "true" || "$v" == "false" ]]; then
                    _cfg_set_num "CF_PROXIED" "$v"
                    echo -e "${GREEN}已更新${NC}"; _restart_service
                else
                    echo -e "${RED}只能输入 true 或 false${NC}"
                fi
                read -p "按回车键继续..." ;;
            4) _update_num "RESTART_WAIT" "新的重启等待时间(秒,当前 ${cur_rwait}): " 1 ;;
            0) break ;;
            *) echo -e "${RED}无效选项${NC}"; sleep 1 ;;
        esac
    done
}

# -------- 子菜单:单项配置 --------
modify_single_item() {
    while true; do
        clear
        local cur_token cur_zone cur_vps1 cur_vps2 cur_con1 cur_con2
        local cur_dash1 cur_dash2 cur_user cur_pass_raw cur_pass stars
        cur_token=$(_cfg_get "CF_API_TOKEN")
        cur_zone=$(_cfg_get  "CF_ZONE_ID")
        cur_vps1=$(_cfg_get  "VPS1_IP")
        cur_vps2=$(_cfg_get  "VPS2_IP")
        cur_con1=$(_cfg_get  "FRPC1_CONTAINER")
        cur_con2=$(_cfg_get  "FRPC2_CONTAINER")
        cur_user=$(_cfg_get  "FRPC_USER")
        cur_dash1=$(grep -m1 "^FRPC1_DASH_PORT=" "$MONITOR_SCRIPT" | cut -d= -f2)
        cur_dash2=$(grep -m1 "^FRPC2_DASH_PORT=" "$MONITOR_SCRIPT" | cut -d= -f2)
        cur_pass_raw=$(_cfg_get "FRPC_PASS")
        stars="${cur_pass_raw//?/*}"
        if [[ -z "$cur_pass_raw" ]]; then
            cur_pass="(未设置)"
        else
            cur_pass="${cur_pass_raw:0:2}${stars:2}"
        fi

        echo -e "${CYAN}==============================${NC}"
        echo -e "${CYAN}  单项配置修改${NC}"
        echo -e "${CYAN}==============================${NC}"
        echo "   1. CF API Token          : ${cur_token:0:8}…(已隐藏)"
        echo "   2. CF Zone ID            : ${cur_zone}"
        echo "   3. 主线 VPS1 IP          : ${cur_vps1}"
        echo "   4. 备线 VPS2 IP          : ${cur_vps2}"
        echo "   5. frpc01 容器名         : ${cur_con1}"
        echo "   6. frpc02 容器名         : ${cur_con2}"
        echo "   7. frpc01 Dashboard 端口 : ${cur_dash1}"
        echo "   8. frpc02 Dashboard 端口 : ${cur_dash2}"
        echo "   9. Dashboard 用户名      : ${cur_user}"
        echo "  10. Dashboard 密码        : ${cur_pass}"
        echo "   0. 返回上级菜单"
        echo -e "${CYAN}==============================${NC}"
        read -p "请选择项目编号: " item
        case $item in
            1)  _update_cfg "CF_API_TOKEN"    "新的 CF API Token: " ;;
            2)  _update_cfg "CF_ZONE_ID"      "新的 CF Zone ID: " ;;
            3)  _update_cfg "VPS1_IP"         "新的 VPS1 IP: " ;;
            4)  _update_cfg "VPS2_IP"         "新的 VPS2 IP: " ;;
            5)  _update_cfg "FRPC1_CONTAINER" "新的 frpc01 容器名: " ;;
            6)  _update_cfg "FRPC2_CONTAINER" "新的 frpc02 容器名: " ;;
            7)  _update_num "FRPC1_DASH_PORT" "新的 frpc01 Dashboard 端口(当前 ${cur_dash1}): " 1 65535 ;;
            8)  _update_num "FRPC2_DASH_PORT" "新的 frpc02 Dashboard 端口(当前 ${cur_dash2}): " 1 65535 ;;
            9)  _update_cfg "FRPC_USER"       "新的 Dashboard 用户名: " ;;
            10) _update_cfg "FRPC_PASS"       "新的 Dashboard 密码: " ;;
            0) break ;;
            *) echo -e "${RED}无效选项${NC}"; sleep 1 ;;
        esac
    done
}

# -------- 修改配置入口 --------
modify_config() {
    if [[ ! -f "$MONITOR_SCRIPT" ]]; then
        echo -e "${RED}未找到监控脚本 ${MONITOR_SCRIPT},请先执行「1. 安装/配置监控脚本」${NC}"
        read -p "按回车键返回主菜单..."; return
    fi

    while true; do
        clear
        echo -e "${CYAN}========================================${NC}"
        echo -e "${CYAN}  修改配置${NC}"
        echo -e "${CYAN}========================================${NC}"
        echo "  1. 修改单项配置   (Token / IP / 容器名 / 端口 / 密码等)"
        echo "  2. 管理域名列表   (添加 / 删除域名)"
        echo "  3. 修改运行参数   (检测间隔 / TTL / 代理模式等)"
        echo "  4. 重新向导配置   (覆盖所有配置,重新完整填写)"
        echo "  0. 返回主菜单"
        echo -e "${CYAN}========================================${NC}"
        read -p "请输入选项编号: " sub
        case $sub in
            1) modify_single_item ;;
            2) modify_domains ;;
            3) modify_runtime_params ;;
            4)
                echo -e "${YELLOW}此操作将覆盖当前所有配置,重新引导填写!${NC}"
                read -p "确认继续?(y/n): " confirm
                if [[ "$confirm" == "y" ]]; then
                    if collect_config; then
                        generate_monitor_script
                        _restart_service
                        echo -e "${GREEN}配置已全部更新${NC}"
                    fi
                    read -p "按回车键继续..."
                fi
                ;;
            0) break ;;
            *) echo -e "${RED}无效选项${NC}"; sleep 1 ;;
        esac
    done
}

# ================================================================
# -------- 查看日志 --------
# ================================================================
view_logs() {
    if [[ ! -f "$LOG_FILE" ]]; then
        echo -e "${RED}日志文件不存在,服务可能尚未启动${NC}"
        read -p "按回车键返回主菜单..."; return
    fi
    echo -e "${GREEN}实时日志输出中,按 Ctrl+C 退出查看...${NC}"
    echo ""
    
    # 使用 sed -u 实时替换英文状态码为中文
    # UNKNOWN -> 未知
    # BOTH_OK -> 双线正常
    # VPS1_FAIL -> 主线故障
    # VPS2_FAIL -> 备线故障
    # BOTH_FAIL -> 双线故障
    # OK -> 正常
    # VPS_DOWN -> 连接中断
    tail -f "$LOG_FILE" | sed -u \
        -e 's/\[OK\]/[成功]/g' \
        -e 's/\[!!\]/[警告]/g' \
        -e 's/\[XX\]/[失败]/g' \
        -e 's/\[--\]/[提示]/g' \
        -e 's/\[>>\]/[执行]/g' \
        -e 's/\[\.\.\]/[补偿]/g' \
        -e 's/\[ST\]/[巡检]/g' \
        -e 's/UNKNOWN/未知/g' \
        -e 's/BOTH_OK/双线正常/g' \
        -e 's/VPS1_FAIL/主线故障/g' \
        -e 's/VPS2_FAIL/备线故障/g' \
        -e 's/BOTH_FAIL/双线故障/g' \
        -e 's/\bOK\b/正常/g' \
        -e 's/VPS_DOWN/连接中断/g'
}

# ================================================================
# -------- 卸载 --------
# ================================================================
uninstall_script() {
    echo -e "${YELLOW}此操作将停止服务并删除所有相关文件(包括本脚本)!${NC}"
    echo ""
    read -p "确认卸载?(y/n): " confirm
    [[ "$confirm" != "y" ]] && return

    echo -e "${RED}正在卸载...${NC}"
    systemctl stop    frpc-monitor 2>/dev/null
    systemctl disable frpc-monitor 2>/dev/null
    rm -f "$SERVICE_FILE"
    systemctl daemon-reload
    systemctl reset-failed 2>/dev/null

    rm -f "$MONITOR_SCRIPT" "$INSTALL_DEPS_SCRIPT" "${LOG_FILE}"* "$STATE_FILE" "$LOGROTATE_FILE"

    echo -e "\n${CYAN}--- 验证清理结果 ---${NC}"
    for f in "$MONITOR_SCRIPT" "$INSTALL_DEPS_SCRIPT" "$SERVICE_FILE" "$LOG_FILE" "$LOGROTATE_FILE" "$STATE_FILE"; do
        if [[ ! -e "$f" ]]; then
            echo -e "  ${GREEN}$f 已删除${NC}"
        else
            echo -e "  ${RED}$f 仍存在${NC}"
        fi
    done

    echo ""
    echo -e "  服务状态: $(systemctl is-active frpc-monitor 2>&1)"
    echo ""
    echo -e "${GREEN}卸载完成${NC}"
    echo -e "${YELLOW}本脚本将在退出后自动删除:$0${NC}"
    echo ""
    read -p "按回车键退出并完成清理..."

    # 后台延迟删除脚本自身,留出主进程退出时间
    (sleep 1 && rm -f "$0") &
    exit 0
}

# ================================================================
# 主循环
# ================================================================
while true; do
    print_menu
    read -p "请输入选项编号: " choice
    case $choice in
        1) install_script ;;
        2) modify_config ;;
        3) view_logs ;;
        4) uninstall_script ;;
        0) echo -e "退出"; exit 0 ;;
        *) echo -e "${RED}无效选项${NC}"; sleep 2 ;;
    esac
done

3️⃣ 赋予文件执行权限并执行文件

sudo chmod +x /opt/frpc-failover.sh && sudo bash /opt/frpc-failover.sh

首次配置向导示例

脚本会自动引导完成以下配置:

1️⃣ 检测 frpc 容器

检测到以下 frpc 容器:
  - frpc-office
  - frpc-backup

2️⃣ Cloudflare 配置

Cloudflare API Token: ************************
Cloudflare Zone ID: 1234567890abcdef

3️⃣ VPS 配置

主线 VPS1 IP: 1.2.3.4
备线 VPS2 IP: 5.6.7.8
FRPS 端口 (默认 7000): 7000

4️⃣ 容器配置

frpc01 容器名: frpc-office
frpc02 容器名: frpc-backup
frpc Dashboard 用户名: admin
frpc Dashboard 密码: ******

5️⃣ 域名配置

需要切换的域名:
  - example.com
  - api.example.com
  - www.example.com

安装完成后,服务会自动启动!🎉

⚙️ 配置管理

主菜单功能

========================================
  frpc 双线监控 + CF DNS 自动切换管理器
========================================
  1. 安装/配置监控脚本 (首次安装)
  2. 修改配置
  3. 查看实时日志
  4. 卸载并清理所有文件
  0. 退出
========================================

配置项详解

注意:脚本中 Dashboard 的默认访问地址分别为:
http://127.0.0.1:7401/api/statushttp://127.0.0.1:7402/api/status
需确保两个 frpc 容器映射了不同的主机端口(例:7401、7402)

配置类别 参数 说明 默认值
API 凭证 CF_API_TOKEN Cloudflare API Token 必填
CF_ZONE_ID 域名所在 Zone ID 必填
网络配置 VPS1_IP 主线服务器 IP 必填
VPS2_IP 备线服务器 IP 必填
FRPS_PORT frps 服务端口 7000
容器配置 FRPC1_CONTAINER 主线 frpc 容器名 自动检测
FRPC2_CONTAINER 备线 frpc 容器名 自动检测
FRPC_USER Dashboard 用户名 必填
FRPC_PASS Dashboard 密码 必填
运行参数 CHECK_INTERVAL 健康检查间隔 30 秒
DNS_TTL DNS 记录 TTL 60 秒
RESTART_WAIT 容器重启等待时间 15 秒
CF_PROXIED Cloudflare 代理 false
域名管理 DOMAINS 需要切换的域名列表 至少 1 个

修改配置示例

添加新域名

[主菜单] → 2 (修改配置) → 2 (管理域名列表) → a (添加域名)
域名: new-service.example.com
✅ 已添加: new-service.example.com

调整检测间隔

[主菜单] → 2 (修改配置) → 3 (修改运行参数) → 1
新的检测间隔(秒,当前 30,建议 ≥10): 15
✅ 服务已重启,配置生效

🗑️ 卸载清理

自动卸载

在主菜单中选择 4. 卸载并清理所有文件,脚本会自动:

  1. 停止并禁用 systemd 服务
  2. 删除监控脚本 (/opt/frpc-monitor.sh)
  3. 删除依赖安装脚本 (/opt/install-deps.sh)
  4. 删除日志文件及轮转配置
  5. 删除状态文件
  6. 自动删除自身脚本

🔐 本站配置信息

此区域为 ✱✱✱ 内容,请输入密码验证身份。

出处:https://xblog.aigo.hidns.co/article.php?id=57
版权:本文采用 CC BY-NC-SA 4.0 协议,完整转载请注明来源。