#!/usr/bin/env bash set -euo pipefail SCRIPT_PATH="$(readlink -f "${BASH_SOURCE[0]}")" PROJECT_DIR="$(cd "$(dirname "$SCRIPT_PATH")/../.." && pwd)" source "$PROJECT_DIR/scripts/core/common.sh" source "$PROJECT_DIR/scripts/core/runtime.sh" source "$PROJECT_DIR/scripts/core/config.sh" source "$PROJECT_DIR/scripts/core/proxy.sh" source "$PROJECT_DIR/scripts/core/update.sh" source "$PROJECT_DIR/scripts/init/systemd.sh" source "$PROJECT_DIR/scripts/init/systemd-user.sh" source "$PROJECT_DIR/scripts/init/script.sh" usage() { cat < 🚀 Main Path: add 📡 添加订阅 use 🔁 切换当前订阅 select 🚀 切换节点 on 🟢 开启代理环境 off 🔴 关闭代理环境 status 😼 查看状态总览 📦 Subscription: ls 📡 查看订阅列表 health ❤️ 查看订阅健康 🕹️ Control: proxy 🌎️ 高级代理与策略组控制 ui 🕹️ 查看 Web 控制台 secret 🔑 查看或设置 Web 密钥 🧭 Diagnose: doctor 🧭 诊断环境与运行状态 logs 📜 查看日志 💡 更多高级能力:clashctl help advanced EOF } usage_advanced() { cat < clashctl use clashon clashctl select clashctl status 💡 Notes: 当前编译链固定为 active-only 只处理当前 active 主订阅 Tun 模式属于高级能力,开启前建议先执行:clashctl tun doctor EOF } prepare() { init_project_context "$PROJECT_DIR" load_env_if_exists detect_install_scope auto } ensure_runtime_ports_ready() { local mixed_port controller_port dns_port local new_mixed_port="" new_controller_port="" new_dns_port="" local changed="false" local repair_detail="" # 运行中不做端口改写,避免把当前已经跑起来的 runtime 搅乱 if status_is_running 2>/dev/null; then return 0 fi # 当前没有 runtime/config.yaml 时,不在这里修;后面 generate_config 会生成 [ -s "$RUNTIME_DIR/config.yaml" ] || return 0 mixed_port="$(runtime_config_mixed_port 2>/dev/null || true)" controller_port="$(runtime_config_controller_port 2>/dev/null || true)" dns_port="$(runtime_config_dns_port 2>/dev/null || true)" if [ -n "${mixed_port:-}" ] && [ "$mixed_port" != "null" ] && is_port_in_use "$mixed_port"; then new_mixed_port="$(resolve_free_port 7890 7999)" write_env_value "MIXED_PORT" "$new_mixed_port" write_runtime_value "INSTALL_PLAN_MIXED_PORT" "$new_mixed_port" write_runtime_value "INSTALL_PLAN_MIXED_PORT_AUTO_CHANGED" "true" changed="true" repair_detail="${repair_detail} mixed-port:${mixed_port}->${new_mixed_port};" info "检测到 mixed-port 冲突:${mixed_port} -> ${new_mixed_port}" fi if [ -n "${controller_port:-}" ] && [ "$controller_port" != "null" ] && is_port_in_use "$controller_port"; then new_controller_port="$(resolve_free_port 9000 9199)" write_env_value "EXTERNAL_CONTROLLER" "127.0.0.1:${new_controller_port}" write_runtime_value "INSTALL_PLAN_CONTROLLER" "127.0.0.1:${new_controller_port}" write_runtime_value "INSTALL_PLAN_CONTROLLER_AUTO_CHANGED" "true" changed="true" repair_detail="${repair_detail} external-controller:${controller_port}->${new_controller_port};" info "检测到 external-controller 冲突:${controller_port} -> ${new_controller_port}" fi if [ -n "${dns_port:-}" ] && [ "$dns_port" != "null" ] && is_port_in_use "$dns_port"; then new_dns_port="$(resolve_free_port 1053 1999)" write_env_value "CLASH_DNS_PORT" "$new_dns_port" write_runtime_value "INSTALL_PLAN_DNS_PORT" "$new_dns_port" write_runtime_value "INSTALL_PLAN_DNS_PORT_AUTO_CHANGED" "true" changed="true" repair_detail="${repair_detail} dns:${dns_port}->${new_dns_port};" info "检测到 DNS 端口冲突:${dns_port} -> ${new_dns_port}" fi if [ "$changed" = "true" ]; then mark_runtime_port_repair_result "true" "$repair_detail" info "检测到端口占用,正在自动重建运行配置" regenerate_config success "端口冲突已自动修复" else mark_runtime_port_repair_result "false" "no-change" fi } ensure_on_path_ready() { load_system_state if [ "$SUBSCRIPTION_STATE" = "missing" ]; then die_state "当前没有可用订阅" "clashctl add <订阅链接>" fi if [ ! -s "$RUNTIME_DIR/config.yaml" ]; then info "检测到运行配置缺失,正在自动生成" regenerate_config || true load_system_state fi if [ ! -s "$RUNTIME_DIR/config.yaml" ] && [ ! -s "$RUNTIME_DIR/config.last.yaml" ]; then die_state "当前没有可启动的运行配置" "clashctl doctor" fi ensure_runtime_ports_ready } print_on_feedback() { local mixed_port controller next_action mixed_port="$(status_read_mixed_port 2>/dev/null || true)" controller="$(status_read_controller 2>/dev/null || true)" load_system_state next_action="$(system_state_default_action 2>/dev/null || echo 'clashctl status')" echo echo "🟢 已开启代理环境" echo if [ -n "${mixed_port:-}" ] && [ "$mixed_port" != "null" ]; then echo "🌐 本地代理:http://127.0.0.1:${mixed_port}" else echo "🌐 本地代理:未知" fi if [ -n "${controller:-}" ] && [ "$controller" != "null" ]; then echo "🖥️ 控制台:http://${controller}/ui" else echo "🖥️ 控制台:未知" fi echo "👉 下一步:$next_action" echo } cmd_on() { prepare ensure_on_path_ready service_start load_system_state print_on_feedback if [ "$RUNTIME_STATE" = "degraded" ]; then ui_warn "代理内核已启动,但控制器暂不可访问" ui_next "clashctl doctor" ui_blank fi } cmd_off() { prepare service_stop ui_title "🔴 代理环境已关闭" ui_info "当前 Shell 代理变量已清理(函数系统已生效)" ui_next "clashctl status" ui_blank } ui_internal_url() { local controller controller="$(status_read_controller 2>/dev/null || true)" [ -n "${controller:-}" ] && [ "$controller" != "null" ] || return 1 echo "http://${controller}/ui" } ui_public_url() { local ip port ip="$(ui_public_ip 2>/dev/null || true)" port="$(ui_controller_port 2>/dev/null || true)" [ -n "${ip:-}" ] || return 1 [ -n "${port:-}" ] || return 1 echo "http://${ip}:${port}/ui" } ui_lan_url() { local ip port ip="$(ui_lan_ip 2>/dev/null || true)" port="$(ui_controller_port 2>/dev/null || true)" [ -n "${ip:-}" ] || return 1 [ -n "${port:-}" ] || return 1 echo "http://${ip}:${port}/ui" } cmd_ui() { local controller_addr="" local internal_url="" lan_url="" public_url="" public_fixed_url="" local current_secret="" controller_port="" controller_status="" prepare runtime_config_exists || die "🧩 运行时配置不存在,请先生成配置" controller_addr="$(status_read_controller 2>/dev/null || true)" [ -n "${controller_addr:-}" ] && [ "$controller_addr" != "null" ] || die_state "未解析到控制器地址" "clashctl doctor" internal_url="$(ui_internal_url 2>/dev/null || true)" lan_url="$(ui_lan_url 2>/dev/null || true)" public_url="$(ui_public_url 2>/dev/null || true)" public_fixed_url="${CLASH_PUBLIC_UI_URL:-http://board.zash.run.place}" current_secret="$(controller_secret 2>/dev/null || true)" controller_port="$(ui_controller_port 2>/dev/null || true)" if [ -z "${current_secret:-}" ] || [ "$current_secret" = "null" ]; then current_secret="未设置" fi if status_is_running; then if proxy_controller_reachable 2>/dev/null; then controller_status="可访问" else controller_status="不可访问" fi else controller_status="未运行" fi cmd_ui_box \ "$controller_status" \ "$internal_url" \ "$lan_url" \ "$public_url" \ "$public_fixed_url" \ "$current_secret" \ "$controller_port" case "$controller_status" in 可访问) ui_next "浏览器打开上面的任一地址" ui_blank return 0 ;; 不可访问) ui_warn "控制器当前不可访问" ui_next "clashctl doctor" ui_blank return 1 ;; 未运行) ui_warn "当前代理未启动" ui_next "clashon" ui_blank return 1 ;; *) ui_warn "控制器状态未知" ui_next "clashctl doctor" ui_blank return 1 ;; esac } status_build_active_source() { read_build_value "BUILD_ACTIVE_SOURCE" 2>/dev/null || true } status_build_active_sources() { local value value="$(read_build_value "BUILD_ACTIVE_SOURCES" 2>/dev/null || true)" if [ -n "${value:-}" ]; then echo "$value" return 0 fi # 兼容历史 build.env read_build_value "BUILD_INCLUDED_SOURCES" 2>/dev/null || true } status_build_failed_active_sources() { local value value="$(read_build_value "BUILD_FAILED_ACTIVE_SOURCES" 2>/dev/null || true)" if [ -n "${value:-}" ]; then echo "$value" return 0 fi # 兼容历史 build.env read_build_value "BUILD_FAILED_SOURCES" 2>/dev/null || true } status_build_last_status() { read_build_value "BUILD_LAST_STATUS" 2>/dev/null || true } status_build_last_time() { read_build_value "BUILD_LAST_TIME" 2>/dev/null || true } status_runtime_last_active_switch_from() { read_runtime_event_value "RUNTIME_LAST_ACTIVE_SWITCH_FROM" 2>/dev/null || true } status_runtime_last_active_switch_to() { read_runtime_event_value "RUNTIME_LAST_ACTIVE_SWITCH_TO" 2>/dev/null || true } status_runtime_last_active_switch_time() { read_runtime_event_value "RUNTIME_LAST_ACTIVE_SWITCH_TIME" 2>/dev/null || true } status_runtime_last_build_block_reason() { read_runtime_event_value "RUNTIME_LAST_BUILD_BLOCK_REASON" 2>/dev/null || true } status_runtime_last_build_block_time() { read_runtime_event_value "RUNTIME_LAST_BUILD_BLOCK_TIME" 2>/dev/null || true } status_runtime_config_source() { read_runtime_event_value "RUNTIME_LAST_CONFIG_SOURCE" 2>/dev/null || true } status_runtime_config_source_time() { read_runtime_event_value "RUNTIME_LAST_CONFIG_SOURCE_TIME" 2>/dev/null || true } status_runtime_build_applied() { read_runtime_event_value "RUNTIME_LAST_BUILD_APPLIED" 2>/dev/null || true } status_runtime_build_applied_time() { read_runtime_event_value "RUNTIME_LAST_BUILD_APPLIED_TIME" 2>/dev/null || true } status_runtime_build_applied_reason() { read_runtime_event_value "RUNTIME_LAST_BUILD_APPLIED_REASON" 2>/dev/null || true } status_tun_enabled() { tun_enabled 2>/dev/null || echo false } status_tun_stack() { tun_stack 2>/dev/null || echo system } status_tun_container_mode() { tun_container_mode 2>/dev/null || echo unknown } status_tun_container_mode_text() { tun_container_mode_text 2>/dev/null || echo 未知 } status_tun_kernel_support_level() { tun_kernel_support_level 2>/dev/null || echo unknown } status_tun_kernel_support_text() { tun_kernel_support_text 2>/dev/null || echo 未知 } status_tun_last_verify_result() { read_tun_last_verify_result 2>/dev/null || true } status_tun_last_verify_reason() { read_tun_last_verify_reason 2>/dev/null || true } status_tun_last_verify_time() { read_tun_last_verify_time 2>/dev/null || true } status_tun_effective_status() { local enabled result enabled="$(status_tun_enabled)" if [ "$enabled" != "true" ]; then echo "off" return 0 fi result="$(tun_effective_check 2>/dev/null || true)" case "${result:-unknown}" in ok) echo "effective" ;; "") echo "unknown" ;; *) echo "partial" ;; esac } status_tun_effective_text() { local s s="$(status_tun_effective_status)" case "$s" in off) echo "未开启" ;; effective) echo "已生效" ;; partial) echo "已开启但未完全确认" ;; *) echo "未知" ;; esac } system_state_build_status() { local last_status local block_reason last_status="$(status_build_last_status 2>/dev/null || true)" block_reason="$(status_runtime_last_build_block_reason 2>/dev/null || true)" if [ -n "${block_reason:-}" ]; then echo "blocked" return 0 fi case "${last_status:-}" in success) echo "success" ;; failed) echo "failed" ;; *) echo "unknown" ;; esac } status_build_effective_status() { system_state_build_status 2>/dev/null || true } system_state_runtime_status() { if status_is_running; then if proxy_controller_reachable 2>/dev/null; then echo "running" else echo "degraded" fi else echo "stopped" fi } system_state_subscription_status() { local active active="$(active_subscription_name 2>/dev/null || true)" if [ -z "${active:-}" ]; then echo "missing" return 0 fi if ! subscription_exists "$active"; then echo "invalid" return 0 fi if ! subscription_enabled "$active"; then echo "disabled" return 0 fi case "$(subscription_health_status "$active" 2>/dev/null || echo unknown)" in success) echo "healthy" ;; failed) echo "degraded" ;; *) echo "unknown" ;; esac } system_state_risk_level() { local risk risk="$(calculate_runtime_risk_level 2>/dev/null || true)" case "${risk:-unknown}" in low|medium|high|critical) echo "$risk" ;; *) echo "unknown" ;; esac } system_state_summary() { local runtime_status build_status subscription_status risk_level local config_source fallback_used build_applied local tun_enabled tun_effective tun_container_mode tun_kernel_support local overall runtime_status="$(system_state_runtime_status)" build_status="$(system_state_build_status)" subscription_status="$(system_state_subscription_status)" risk_level="$(system_state_risk_level)" config_source="$(status_runtime_config_source)" fallback_used="$(runtime_last_fallback_used 2>/dev/null || true)" build_applied="$(status_runtime_build_applied)" tun_enabled="$(status_tun_enabled)" tun_effective="$(status_tun_effective_status)" tun_container_mode="$(status_tun_container_mode)" tun_kernel_support="$(status_tun_kernel_support_level)" if [ "$runtime_status" = "running" ] \ && [ "$build_status" = "success" ] \ && [ "$subscription_status" = "healthy" ]; then overall="ready" elif [ "$runtime_status" = "stopped" ]; then overall="stopped" elif [ "$build_status" = "failed" ] || [ "$build_status" = "blocked" ] || [ "$subscription_status" = "invalid" ] || [ "$subscription_status" = "missing" ]; then overall="broken" else overall="degraded" fi cat <" else echo "clashon" fi ;; broken) if [ "$SUBSCRIPTION_STATE" = "missing" ]; then echo "clashctl add <订阅链接>" else echo "clashctl doctor" fi ;; degraded) if [ "$RUNTIME_STATE" = "degraded" ]; then echo "clashctl doctor" else echo "clashctl status --verbose" fi ;; ready) echo "clashctl select" ;; *) echo "clashctl status --verbose" ;; esac } system_state_problem_lines() { load_system_state case "$SYSTEM_STATE" in ready) return 0 ;; stopped) if [ "$SUBSCRIPTION_STATE" = "missing" ]; then echo "• 当前没有可用订阅" else echo "• 代理内核未启动" fi ;; degraded) [ "$RUNTIME_STATE" = "degraded" ] && echo "• 代理内核已启动,但控制器不可访问" [ "$SUBSCRIPTION_STATE" = "degraded" ] && echo "• 当前主订阅健康异常" ;; broken) [ "$BUILD_STATE" = "failed" ] && echo "• 最近一次编译失败" [ "$SUBSCRIPTION_STATE" = "missing" ] && echo "• 当前没有主订阅" [ "$SUBSCRIPTION_STATE" = "invalid" ] && echo "• 当前主订阅无效" [ "$SUBSCRIPTION_STATE" = "disabled" ] && echo "• 当前主订阅已禁用" [ "$BUILD_STATE" = "blocked" ] && echo "• 最近一次编译被阻断" ;; esac } system_state_recommendation_lines() { load_system_state case "$SYSTEM_STATE" in ready) echo "1. clashctl select" echo "2. clashctl health" ;; stopped) if [ "$SUBSCRIPTION_STATE" = "missing" ]; then echo "1. clashctl add <订阅链接>" else echo "1. clashon" echo "2. clashctl status" fi ;; degraded) echo "1. clashctl doctor" echo "2. clashctl status --verbose" ;; broken) if [ "$SUBSCRIPTION_STATE" = "missing" ]; then echo "1. clashctl add <订阅链接>" else echo "1. clashctl doctor" echo "2. clashctl health" fi ;; *) echo "1. clashctl status --verbose" ;; esac } ui_box_width=47 compute_box_width() { local max_len=0 local line for line in "$@"; do [ ${#line} -gt $max_len ] && max_len=${#line} done ui_box_width=$((max_len + 8)) } box_border_top() { printf "╔%0.s═" $(seq 1 $((ui_box_width-2))) echo "╗" } box_border_mid() { printf "╠%0.s═" $(seq 1 $((ui_box_width-2))) echo "╣" } box_border_bottom() { printf "╚%0.s═" $(seq 1 $((ui_box_width-2))) echo "╝" } box_center_line() { local text="$1" local inner_width=$((ui_box_width - 2)) local text_len=${#text} local left_pad=$(( (inner_width - text_len) / 2 )) local right_pad=$(( inner_width - left_pad - text_len )) printf "║%*s%s%*s║\n" "$left_pad" "" "$text" "$right_pad" "" } box_empty() { printf "║%*s║\n" $((ui_box_width-2)) "" } box_section_line() { local text="$1" printf "║ %-*s║\n" $((ui_box_width-6)) "$text" } box_title_line() { box_center_line "$1" } status_is_running() { local backend pid backend="$(runtime_backend)" case "$backend" in systemd) systemctl is-active --quiet "$(service_unit_name)" ;; systemd-user) systemctl --user is-active --quiet "$(service_unit_name)" ;; script) if [ -f "$RUNTIME_DIR/mihomo.pid" ]; then pid="$(cat "$RUNTIME_DIR/mihomo.pid" 2>/dev/null || true)" [ -n "${pid:-}" ] && kill -0 "$pid" 2>/dev/null else return 1 fi ;; *) return 1 ;; esac } apply_runtime_change_after_config_mutation() { if status_is_running; then info "检测到代理正在运行,正在自动重启以应用新配置" service_restart success "新配置已自动重启生效" else info "新配置已写入,当前代理未运行,下次启动自动生效" fi } print_config_regen_feedback() { echo echo "🧩 配置已重新生成" echo } print_config_apply_feedback() { local build_status build_applied config_source next_action build_status="$(status_build_effective_status 2>/dev/null || true)" build_applied="$(status_runtime_build_applied 2>/dev/null || true)" config_source="$(status_runtime_config_source 2>/dev/null || true)" next_action="$(system_state_default_action 2>/dev/null || echo 'clashctl status')" echo echo "🧩 配置变更已处理" case "${build_status:-unknown}" in success) echo "🟢 构建结果:success" ;; failed) echo "🔴 构建结果:failed" ;; blocked) echo "⚠️ 构建结果:blocked" ;; *) echo "⚪ 构建结果:unknown" ;; esac case "${build_applied:-unknown}" in true) echo "🟢 是否应用:true" ;; false) echo "⚠️ 是否应用:false" ;; *) echo "⚪ 是否应用:unknown" ;; esac echo "🧩 配置来源:${config_source:-unknown}" echo "👉 下一步:$next_action" echo } main_feedback_runtime_state() { if status_is_running; then ui_kv "🟢" "运行状态" "已运行" else ui_kv "🔴" "运行状态" "未运行" fi } main_feedback_build_mode() { ui_kv "🧩" "编译模式" "active-only" } main_feedback_active_subscription() { local active active="$(active_subscription_name 2>/dev/null || true)" if [ -n "${active:-}" ]; then ui_kv "📡" "当前订阅" "$active" fi } main_feedback_subscription_selected_state() { local name="$1" local active [ -n "${name:-}" ] || return 0 active="$(active_subscription_name 2>/dev/null || true)" if [ "$name" = "$active" ]; then echo "✅ 参与状态:当前主订阅" else echo "📦 参与状态:已保存,但当前未启用" fi } print_add_feedback() { local name="$1" ui_title "📡 订阅添加完成" main_feedback_active_subscription main_feedback_build_mode main_feedback_subscription_selected_state "$name" main_feedback_runtime_state ui_next "clashctl status" ui_blank } print_use_context() { local active recommended active="$(active_subscription_name 2>/dev/null || true)" recommended="$(recommended_subscription_name 2>/dev/null || true)" ui_title "🔁 切换当前订阅" if [ -n "${active:-}" ]; then ui_kv "📡" "当前主订阅" "$active" else ui_kv "📡" "当前主订阅" "未设置" fi if [ -n "${recommended:-}" ] && [ "${recommended:-}" != "${active:-}" ]; then ui_kv "💡" "推荐订阅" "$recommended" else ui_kv "💡" "推荐订阅" "保持当前" fi ui_blank } print_use_feedback() { local name="$1" health fail_count enabled_text ui_title "🔁 订阅切换完成" if [ -n "${name:-}" ]; then if subscription_enabled "$name"; then enabled_text="enabled" else enabled_text="disabled" fi health="$(subscription_health_status "$name" 2>/dev/null || echo "unknown")" fail_count="$(subscription_fail_count "$name" 2>/dev/null || echo "0")" ui_kv "📡" "当前主订阅" "$name" ui_kv "❤️" "订阅状态" "$enabled_text / $health / fail=$fail_count" fi main_feedback_build_mode main_feedback_runtime_state ui_next "clashctl status" ui_blank } print_select_context() { local current_proxy ui_title "🚀 切换节点" if ! status_is_running; then ui_kv "🔴" "代理状态" "未运行" ui_next "clashon" ui_blank return 1 fi if ! proxy_controller_reachable 2>/dev/null; then ui_kv "🔴" "当前节点" "控制器不可访问" ui_next "clashctl doctor" ui_blank return 1 fi if [ "$(proxy_group_count 2>/dev/null || echo 0)" -le 0 ]; then ui_kv "🔴" "当前节点" "暂无可切换策略组" ui_next "clashctl status --verbose" ui_blank return 1 fi current_proxy="$(status_current_proxy_brief 2>/dev/null || true)" if [ -n "${current_proxy:-}" ]; then ui_kv "🚀" "当前节点" "$current_proxy" else ui_kv "🚀" "当前节点" "未知" fi ui_next "请选择要切换的策略组和节点" ui_blank return 0 } print_select_feedback() { local group="$1" local current current="$(proxy_group_current "$group" 2>/dev/null || true)" ui_title "🚀 节点切换完成" if [ -n "${group:-}" ]; then ui_kv "📦" "已切换策略组" "$group" fi if [ -n "${current:-}" ]; then ui_kv "🚀" "当前节点" "$current" fi main_feedback_runtime_state ui_next "clashctl status" ui_blank } print_sub_enable_feedback() { local name="$1" local health fail_count health="$(subscription_health_status "$name" 2>/dev/null || echo "unknown")" fail_count="$(subscription_fail_count "$name" 2>/dev/null || echo "0")" ui_title "📡 订阅已启用" ui_kv "📡" "订阅名称" "$name" ui_kv "❤️" "当前健康" "$health / fail=$fail_count" main_feedback_build_mode main_feedback_runtime_state ui_next "clashctl health ${name}" ui_blank } print_sub_disable_feedback() { local name="$1" local active active="$(active_subscription_name 2>/dev/null || true)" ui_title "📡 订阅已禁用" ui_kv "📡" "订阅名称" "$name" if [ "$name" = "$active" ]; then ui_kv "⚠️" "当前主订阅" "已被禁用" ui_next "clashctl use" else ui_kv "🧩" "当前主订阅" "${active:-未设置}" ui_next "clashctl status" fi main_feedback_build_mode main_feedback_runtime_state ui_blank } print_sub_rename_feedback() { local old_name="$1" local new_name="$2" ui_title "📡 订阅已重命名" ui_kv "📡" "原名称" "$old_name" ui_kv "📡" "新名称" "$new_name" main_feedback_build_mode main_feedback_runtime_state ui_next "clashctl ls --verbose" ui_blank } print_sub_remove_feedback() { local name="$1" local active active="$(active_subscription_name 2>/dev/null || true)" ui_title "📡 订阅已删除" ui_kv "📡" "已删除" "$name" ui_kv "📡" "当前主订阅" "${active:-未设置}" main_feedback_build_mode main_feedback_runtime_state ui_next "clashctl ls" ui_blank } print_config_kernel_feedback() { local kernel="$1" ui_title "🚀 运行内核已切换" ui_kv "🚀" "当前内核" "$kernel" main_feedback_runtime_state ui_next "clashctl status" ui_blank } print_tun_container_gate_feedback() { local mode="$1" local reason="${2:-}" echo ui_title "🧪 Tun 裁决" ui_kv "🚀" "当前内核" "$(runtime_kernel_type 2>/dev/null || echo unknown)" ui_kv "🧩" "Tun 支持等级" "$(tun_kernel_support_text 2>/dev/null || echo 未知)" case "$mode" in host) ui_kv "🖥️" "环境模式" "主机环境" ui_kv "🟢" "容器裁决" "允许正常开启" ;; container-safe) ui_kv "🖥️" "环境模式" "容器环境" ui_kv "⚠️" "容器裁决" "允许开启,但属于保守通过" [ -n "${reason:-}" ] && ui_kv "⚠️" "注意事项" "$reason" ;; container-risky) ui_kv "🖥️" "环境模式" "容器环境" ui_kv "🔴" "容器裁决" "高风险,已阻断开启" [ -n "${reason:-}" ] && ui_kv "🔴" "阻断原因" "$reason" ui_next "clashctl tun doctor" ui_blank ;; *) ui_kv "⚪" "容器裁决" "未知" [ -n "${reason:-}" ] && ui_kv "⚠️" "原因" "$reason" ;; esac if ! tun_kernel_is_recommended 2>/dev/null; then ui_warn "$(tun_kernel_support_reason 2>/dev/null || echo '当前内核不适合作为 Tun 主支持内核')" ui_next "如需最稳妥 Tun 体验,先执行:clashctl config kernel mihomo" fi if [ "$mode" != "container-risky" ]; then ui_next "clashctl tun doctor" ui_blank fi } print_tun_on_feedback() { local verify_result stack auto_route route_dev verify_result="$1" stack="$(runtime_config_tun_stack 2>/dev/null || tun_stack 2>/dev/null || echo unknown)" auto_route="$(runtime_config_tun_auto_route 2>/dev/null || echo false)" route_dev="$(default_route_dev 2>/dev/null || true)" echo ui_title "🧪 Tun 模式已处理" ui_kv "🧪" "目标状态" "开启" ui_kv "⚙️" "Tun stack" "${stack:-unknown}" ui_kv "🧭" "auto-route" "${auto_route:-false}" case "$verify_result" in ok) ui_kv "🟢" "验证结果" "Tun 接管已生效" [ -n "${route_dev:-}" ] && ui_kv "🌐" "默认路由设备" "$route_dev" ;; *) ui_kv "⚠️" "验证结果" "Tun 已开启配置,但系统接管未完全确认" ui_kv "⚠️" "原因" "$verify_result" ;; esac if [ "$(container_env_type)" != "host" ]; then ui_warn "当前处于容器环境,Tun 可能受宿主机权限或设备映射限制" fi ui_next "clashctl tun doctor" ui_blank } print_tun_off_feedback() { local verify_result route_dev verify_result="$1" route_dev="$(default_route_dev 2>/dev/null || true)" echo ui_title "🧪 Tun 模式已处理" ui_kv "🧪" "目标状态" "关闭" case "$verify_result" in ok) ui_kv "🟢" "验证结果" "Tun 已关闭并完成回滚检查" [ -n "${route_dev:-}" ] && ui_kv "🌐" "当前默认路由设备" "$route_dev" ;; *) ui_kv "⚠️" "验证结果" "Tun 关闭后仍存在残留或运行异常" ui_kv "⚠️" "原因" "$verify_result" ;; esac ui_next "clashctl tun doctor" ui_blank } status_subscription_health_summary() { local active health fail_count auto_disabled active="$(active_subscription_name 2>/dev/null || true)" [ -n "${active:-}" ] || return 0 health="$(subscription_health_status "$active" 2>/dev/null || echo "unknown")" fail_count="$(subscription_fail_count "$active" 2>/dev/null || echo "0")" if subscription_auto_disabled "$active"; then auto_disabled="yes" else auto_disabled="no" fi echo "❤️ 健康状态:$health" echo "⚠️ 失败次数:$fail_count" echo "⚠️ 阈值命中:$auto_disabled" if [ -n "$(subscription_last_success "$active" 2>/dev/null || true)" ]; then echo "🕒 最近成功:$(subscription_last_success "$active" 2>/dev/null || true)" fi if [ -n "$(subscription_last_failure "$active" 2>/dev/null || true)" ]; then echo "🕒 最近失败:$(subscription_last_failure "$active" 2>/dev/null || true)" fi } status_proxy_summary_lines() { print_proxy_groups_status 2>/dev/null | head -n 5 || true } status_user_risk_text() { system_state_risk_text } status_risk_reason_lines() { local build_status active controller_ok local last_risk_name last_risk_reason last_risk_fail_count last_risk_threshold local build_block_reason build_block_time local fallback_used fallback_time fallback_reason local tun_enabled tun_effective tun_container_mode tun_kernel_support tun_verify_reason build_status="$(status_build_last_status 2>/dev/null || true)" active="$(active_subscription_name 2>/dev/null || true)" controller_ok="false" proxy_controller_reachable 2>/dev/null && controller_ok="true" build_block_reason="$(status_runtime_last_build_block_reason 2>/dev/null || true)" build_block_time="$(status_runtime_last_build_block_time 2>/dev/null || true)" fallback_used="$(runtime_last_fallback_used 2>/dev/null || true)" fallback_time="$(runtime_last_fallback_time 2>/dev/null || true)" fallback_reason="$(runtime_last_fallback_reason 2>/dev/null || true)" tun_enabled="$(status_tun_enabled 2>/dev/null || echo false)" tun_effective="$(status_tun_effective_status 2>/dev/null || echo unknown)" tun_container_mode="$(status_tun_container_mode 2>/dev/null || echo unknown)" tun_kernel_support="$(status_tun_kernel_support_level 2>/dev/null || echo unknown)" tun_verify_reason="$(status_tun_last_verify_reason 2>/dev/null || true)" if ! status_is_running; then echo "• 代理内核未启动" fi if [ "$controller_ok" = "false" ] && status_is_running; then echo "• 控制器不可访问" fi if [ "${build_status:-}" = "failed" ]; then echo "• 最近一次编译失败" fi if [ -n "${build_block_reason:-}" ]; then if [ -n "${build_block_time:-}" ]; then echo "• 最近一次编译被阻断:${build_block_reason} @ ${build_block_time}" else echo "• 最近一次编译被阻断:${build_block_reason}" fi fi if [ "${fallback_used:-false}" = "true" ]; then if [ -n "${fallback_time:-}" ]; then echo "• 最近一次启动触发配置回退:${fallback_time}" else echo "• 最近一次启动触发配置回退" fi [ -n "${fallback_reason:-}" ] && echo "• 回退原因:${fallback_reason}" fi if [ -n "${active:-}" ] && ! active_subscription_enabled 2>/dev/null; then echo "• 当前主订阅已禁用或不可用" fi last_risk_name="$(read_runtime_event_value "RUNTIME_LAST_SUBSCRIPTION_RISK_NAME" 2>/dev/null || true)" last_risk_reason="$(read_runtime_event_value "RUNTIME_LAST_SUBSCRIPTION_RISK_REASON" 2>/dev/null || true)" last_risk_fail_count="$(read_runtime_event_value "RUNTIME_LAST_SUBSCRIPTION_RISK_FAIL_COUNT" 2>/dev/null || true)" last_risk_threshold="$(read_runtime_event_value "RUNTIME_LAST_SUBSCRIPTION_RISK_THRESHOLD" 2>/dev/null || true)" if [ -n "${last_risk_name:-}" ] && [ "${last_risk_reason:-}" = "fail-threshold-reached" ]; then echo "• 订阅 ${last_risk_name} 连续失败 ${last_risk_fail_count:-?} 次(阈值 ${last_risk_threshold:-?})" fi if [ "$tun_enabled" = "true" ] && [ "$tun_effective" != "effective" ]; then echo "• Tun 已开启,但系统级接管未完全确认:${tun_verify_reason:-unknown}" fi if [ "$tun_container_mode" = "container-risky" ]; then echo "• 当前 Tun 运行环境属于高风险容器场景" fi if [ "$tun_enabled" = "true" ] && [ "$tun_kernel_support" = "limited" ]; then echo "• 当前 Tun 运行在 clash 内核上,仅按降级支持处理" fi } status_recommendation_lines() { system_state_recommendation_lines } status_current_proxy_brief() { local default_group default_current fallback_group fallback_current if ! status_is_running; then echo "未启动" return 0 fi if ! proxy_controller_reachable 2>/dev/null; then echo "控制器不可访问" return 0 fi default_group="$(default_proxy_group_name 2>/dev/null || true)" default_current="$(default_proxy_group_current 2>/dev/null || true)" if [ -n "${default_group:-}" ] && [ -n "${default_current:-}" ]; then echo "${default_group} -> ${default_current}" return 0 fi fallback_group="$(proxy_group_list 2>/dev/null | sed -n '1p')" if [ -n "${fallback_group:-}" ]; then fallback_current="$(proxy_group_current "$fallback_group" 2>/dev/null || true)" if [ -n "${fallback_current:-}" ]; then echo "${fallback_group} -> ${fallback_current}" return 0 fi fi echo "暂无可切换策略组" } shell_proxy_enabled() { [ -n "${http_proxy:-}" ] \ || [ -n "${https_proxy:-}" ] \ || [ -n "${HTTP_PROXY:-}" ] \ || [ -n "${HTTPS_PROXY:-}" ] \ || [ -n "${all_proxy:-}" ] \ || [ -n "${ALL_PROXY:-}" ] } shell_proxy_http_value() { if [ -n "${http_proxy:-}" ]; then echo "$http_proxy" return 0 fi if [ -n "${HTTP_PROXY:-}" ]; then echo "$HTTP_PROXY" return 0 fi echo "" } shell_proxy_matches_runtime() { local expected actual expected="$(proxy_http_url 2>/dev/null || true)" actual="$(shell_proxy_http_value)" [ -n "${expected:-}" ] || return 1 [ -n "${actual:-}" ] || return 1 [ "$expected" = "$actual" ] } connectivity_issue_code() { local active group_count if ! status_is_running; then echo "runtime_stopped" return 0 fi if ! proxy_controller_reachable 2>/dev/null; then echo "controller_unreachable" return 0 fi case "$(system_state_build_status 2>/dev/null || echo unknown)" in failed|blocked) echo "config_invalid" return 0 ;; esac case "$(system_state_subscription_status 2>/dev/null || echo unknown)" in missing|invalid|disabled|degraded) echo "subscription_unhealthy" return 0 ;; esac group_count="$(proxy_group_count 2>/dev/null || echo 0)" case "${group_count:-0}" in ''|*[!0-9]*) group_count=0 ;; esac if [ "$group_count" -le 0 ]; then echo "proxy_control_broken" return 0 fi if ! shell_proxy_enabled; then echo "shell_proxy_missing" return 0 fi if ! shell_proxy_matches_runtime; then echo "shell_proxy_mismatch" return 0 fi echo "ok" } connectivity_issue_text() { case "$(connectivity_issue_code)" in ok) echo "可用(代理链路已闭环)" ;; runtime_stopped) echo "不可用(代理内核未启动)" ;; controller_unreachable) echo "异常(内核已运行,但控制器不可访问)" ;; config_invalid) echo "异常(当前运行配置不可用)" ;; subscription_unhealthy) echo "异常(当前主订阅不可用)" ;; proxy_control_broken) echo "异常(当前无可用策略组或节点控制面异常)" ;; shell_proxy_missing) echo "未接管(当前 Shell 未注入代理环境)" ;; shell_proxy_mismatch) echo "异常(当前 Shell 代理与运行时端口不一致)" ;; *) echo "未知" ;; esac } connectivity_next_action() { case "$(connectivity_issue_code)" in ok) echo "clashctl select" ;; runtime_stopped) echo "clashon" ;; controller_unreachable) echo "clashctl doctor" ;; config_invalid) echo "clashctl doctor" ;; subscription_unhealthy) echo "clashctl health" ;; proxy_control_broken) echo "clashctl status --verbose" ;; shell_proxy_missing) echo 'eval "$(clashctl proxy on)"' ;; shell_proxy_mismatch) echo 'eval "$(clashctl proxy off)" && eval "$(clashctl proxy on)"' ;; *) echo "clashctl doctor" ;; esac } connectivity_evidence_lines() { local runtime_running controller_ok build_status subscription_status local group_count expected_proxy actual_proxy active config_source if status_is_running; then runtime_running="true" else runtime_running="false" fi if proxy_controller_reachable 2>/dev/null; then controller_ok="true" else controller_ok="false" fi build_status="$(system_state_build_status 2>/dev/null || echo unknown)" subscription_status="$(system_state_subscription_status 2>/dev/null || echo unknown)" group_count="$(proxy_group_count 2>/dev/null || echo 0)" active="$(active_subscription_name 2>/dev/null || true)" config_source="$(status_runtime_config_source 2>/dev/null || true)" expected_proxy="$(proxy_http_url 2>/dev/null || true)" actual_proxy="$(shell_proxy_http_value)" echo "• runtime_running = ${runtime_running:-false}" echo "• controller_reachable = ${controller_ok:-false}" echo "• build_status = ${build_status:-unknown}" echo "• subscription_status = ${subscription_status:-unknown}" echo "• active_subscription = ${active:-unset}" echo "• config_source = ${config_source:-unknown}" echo "• proxy_group_count = ${group_count:-0}" if shell_proxy_enabled; then echo "• shell_proxy_enabled = true" echo "• shell_proxy_http = ${actual_proxy:-unknown}" else echo "• shell_proxy_enabled = false" fi if [ -n "${expected_proxy:-}" ]; then echo "• runtime_proxy_http = $expected_proxy" fi echo "• tun_enabled = $(status_tun_enabled 2>/dev/null || echo false)" echo "• tun_effective = $(status_tun_effective_status 2>/dev/null || echo unknown)" echo "• tun_container_mode = $(status_tun_container_mode 2>/dev/null || echo unknown)" echo "• tun_kernel_support = $(status_tun_kernel_support_level 2>/dev/null || echo unknown)" } status_runtime_backend_text() { local backend backend="$(install_plan_backend 2>/dev/null || true)" [ -n "${backend:-}" ] || backend="$(runtime_backend 2>/dev/null || true)" case "${backend:-unknown}" in systemd) echo "systemd" ;; systemd-user) echo "systemd-user" ;; script) echo "script" ;; *) echo "unknown" ;; esac } status_runtime_backend_reason_text() { local backend container_type backend="$(runtime_backend 2>/dev/null || echo unknown)" container_type="$(install_env_container 2>/dev/null || echo unknown)" case "$backend" in systemd) echo "root + systemd 可用" ;; systemd-user) echo "普通用户 + user systemd 可用" ;; script) if [ "$container_type" != "host" ] && [ -n "${container_type:-}" ]; then echo "容器环境默认回退 script" else echo "systemd 不可用,自动回退 script" fi ;; *) echo "未知" ;; esac } status_container_mode_text() { local container_mode env_container container_mode="$(install_plan_container_mode 2>/dev/null || true)" env_container="$(install_env_container 2>/dev/null || true)" if [ "${container_mode:-}" = "true" ]; then echo "容器兼容模式" return 0 fi case "${env_container:-host}" in host) echo "主机模式" ;; "") echo "未知" ;; *) echo "容器环境(未启用兼容模式)" ;; esac } status_install_verify_brief() { local runtime_ready controller_ready local live_runtime="false" local live_controller="false" runtime_ready="$(install_verify_runtime_ready 2>/dev/null || true)" controller_ready="$(install_verify_controller_ready 2>/dev/null || true)" if status_is_running 2>/dev/null; then live_runtime="true" fi if proxy_controller_reachable 2>/dev/null; then live_controller="true" fi if [ "$live_runtime" = "true" ] && [ "$live_controller" = "true" ]; then echo "安装验证通过" return 0 fi if [ "$runtime_ready" = "true" ] && [ "$controller_ready" = "true" ]; then echo "安装验证通过" return 0 fi if [ "$live_runtime" = "true" ] && [ "$live_controller" != "true" ]; then echo "运行已就绪,控制器未就绪" return 0 fi if [ "$runtime_ready" = "true" ] && [ "$controller_ready" != "true" ]; then echo "运行已就绪,控制器未就绪" return 0 fi if [ "$live_runtime" != "true" ] && [ "$live_controller" = "true" ]; then echo "控制器异常记录" return 0 fi if [ "$runtime_ready" != "true" ] && [ "$controller_ready" = "true" ]; then echo "控制器异常记录" return 0 fi echo "安装验证未完成" } status_port_adjustment_brief() { local changed=0 [ "$(install_plan_mixed_port_auto_changed 2>/dev/null || echo false)" = "true" ] && changed=1 [ "$(install_plan_controller_auto_changed 2>/dev/null || echo false)" = "true" ] && changed=1 [ "$(install_plan_dns_port_auto_changed 2>/dev/null || echo false)" = "true" ] && changed=1 if [ "$changed" -eq 1 ]; then echo "安装期发生过端口自动避让" else echo "安装期端口保持默认" fi } print_status_summary_compact() { local profile mixed_port controller local running_text user_connectivity user_risk current_proxy_brief next_action local current_active local tun_text profile="$(show_active_profile 2>/dev/null || true)" [ -n "${profile:-}" ] || profile="default" mixed_port="$(status_read_mixed_port 2>/dev/null || true)" controller="$(status_read_controller 2>/dev/null || true)" current_active="$(active_subscription_name 2>/dev/null || true)" tun_text="$(status_tun_effective_text)" if status_is_running; then running_text="🟢 已开启" else running_text="🔴 未开启" fi user_connectivity="$(connectivity_issue_text)" user_risk="$(status_user_risk_text)" current_proxy_brief="$(status_current_proxy_brief)" next_action="$(system_state_default_action 2>/dev/null || echo 'clashctl status')" echo echo "😼 Clash 状态总览" echo echo "【当前结果】" echo "🟢 代理状态:$running_text" echo "🌐 当前可用性:$user_connectivity" echo "📡 当前订阅:${current_active:-未设置}" echo "🚀 当前节点:$current_proxy_brief" echo "⚠️ 当前风险:$user_risk" echo "👉 下一步:$next_action" echo echo "【核心入口】" echo "⚙️ Profile:$profile" echo "⚙️ 运行后端:$(status_runtime_backend_text)" echo "🧪 环境模式:$(status_container_mode_text)" echo "🧪 Tun 状态:${tun_text:-未知}" if [ -n "${mixed_port:-}" ] && [ "$mixed_port" != "null" ]; then echo "🌐 本地代理:http://127.0.0.1:${mixed_port}" else echo "🌐 本地代理:未知" fi if [ -n "${controller:-}" ] && [ "$controller" != "null" ]; then echo "🖥️ 控制台:http://${controller}/ui" else echo "🖥️ 控制台:未知" fi echo echo "🧩 安装验证:$(status_install_verify_brief)" echo "💡 更多细节:clashctl status --verbose" echo } print_status_summary_verbose() { local running_text profile mixed_port controller local current_active build_active_sources build_failed_active_sources build_status build_time local build_block_reason build_block_time local last_switch_from last_switch_to last_switch_time local controller_ok current_proxy_lines local user_connectivity user_risk current_proxy_brief next_action local fallback_used fallback_time fallback_reason local config_source config_source_time build_applied build_applied_time build_applied_reason local install_backend_text install_container_text install_verify_text port_adjustment_text local tun_enabled tun_effective tun_stack tun_container_text tun_kernel_text tun_verify_result tun_verify_reason tun_verify_time profile="$(show_active_profile 2>/dev/null || true)" [ -n "${profile:-}" ] || profile="default" mixed_port="$(status_read_mixed_port 2>/dev/null || true)" controller="$(status_read_controller 2>/dev/null || true)" current_active="$(active_subscription_name 2>/dev/null || true)" build_active_sources="$(status_build_active_sources)" build_failed_active_sources="$(status_build_failed_active_sources)" build_status="$(status_build_last_status)" build_time="$(status_build_last_time)" build_block_reason="$(status_runtime_last_build_block_reason)" build_block_time="$(status_runtime_last_build_block_time)" last_switch_from="$(status_runtime_last_active_switch_from)" last_switch_to="$(status_runtime_last_active_switch_to)" last_switch_time="$(status_runtime_last_active_switch_time)" fallback_used="$(runtime_last_fallback_used 2>/dev/null || true)" fallback_time="$(runtime_last_fallback_time 2>/dev/null || true)" fallback_reason="$(runtime_last_fallback_reason 2>/dev/null || true)" config_source="$(status_runtime_config_source 2>/dev/null || true)" config_source_time="$(status_runtime_config_source_time 2>/dev/null || true)" build_applied="$(status_runtime_build_applied 2>/dev/null || true)" build_applied_time="$(status_runtime_build_applied_time 2>/dev/null || true)" build_applied_reason="$(status_runtime_build_applied_reason 2>/dev/null || true)" if status_is_running; then running_text="🟢 已开启" else running_text="🔴 未开启" fi controller_ok="false" current_proxy_lines="" if status_is_running && proxy_controller_reachable 2>/dev/null; then controller_ok="true" current_proxy_lines="$(status_proxy_summary_lines)" fi user_connectivity="$(connectivity_issue_text)" user_risk="$(status_user_risk_text)" current_proxy_brief="$(status_current_proxy_brief)" next_action="$(system_state_default_action 2>/dev/null || echo 'clashctl status')" install_backend_text="$(status_runtime_backend_text)" install_container_text="$(status_container_mode_text)" install_verify_text="$(status_install_verify_brief)" port_adjustment_text="$(status_port_adjustment_brief)" tun_enabled="$(status_tun_enabled 2>/dev/null || echo false)" tun_effective="$(status_tun_effective_text 2>/dev/null || echo 未知)" tun_stack="$(status_tun_stack 2>/dev/null || echo system)" tun_container_text="$(status_tun_container_mode_text 2>/dev/null || echo 未知)" tun_kernel_text="$(status_tun_kernel_support_text 2>/dev/null || echo 未知)" tun_verify_result="$(status_tun_last_verify_result 2>/dev/null || true)" tun_verify_reason="$(status_tun_last_verify_reason 2>/dev/null || true)" tun_verify_time="$(status_tun_last_verify_time 2>/dev/null || true)" echo echo "😼 Clash 状态总览" echo echo "【当前结果】" echo "🟢 代理状态:$running_text" echo "🌐 当前可用性:$user_connectivity" echo "📡 当前订阅:${current_active:-未设置}" echo "🚀 当前节点:$current_proxy_brief" echo "⚠️ 当前风险:$user_risk" echo "👉 下一步:$next_action" echo echo "【核心入口】" echo "⚙️ Profile:$profile" if [ -n "${mixed_port:-}" ] && [ "$mixed_port" != "null" ]; then echo "🌐 本地代理:http://127.0.0.1:${mixed_port}" else echo "🌐 本地代理:未知" fi if [ -n "${controller:-}" ] && [ "$controller" != "null" ]; then echo "🖥️ 控制台:http://${controller}/ui" else echo "🖥️ 控制台:未知" fi echo echo "【安装上下文】" echo "⚙️ 运行后端:${install_backend_text:-unknown}" echo "💡 后端原因:$(status_runtime_backend_reason_text 2>/dev/null || echo unknown)" echo "🧪 环境模式:${install_container_text:-unknown}" echo "🧩 安装验证:${install_verify_text:-unknown}" echo "🧭 端口裁决:${port_adjustment_text:-unknown}" echo if [ -n "$(install_plan_controller 2>/dev/null || true)" ]; then echo "🖥️ 安装期控制器:$(install_plan_controller 2>/dev/null || true)" fi if [ -n "$(install_plan_mixed_port 2>/dev/null || true)" ]; then echo "🌐 安装期代理端口:$(install_plan_mixed_port 2>/dev/null || true)" fi echo echo "【Tun 状态】" if [ "$tun_enabled" = "true" ]; then echo "🟢 Tun 开关:已开启" else echo "🔴 Tun 开关:未开启" fi echo "🧪 Tun 生效:${tun_effective:-未知}" echo "⚙️ Tun stack:${tun_stack:-unknown}" echo "🖥️ 容器裁决:${tun_container_text:-未知}" echo "🚀 内核支持:${tun_kernel_text:-未知}" if [ -n "${tun_verify_result:-}" ]; then echo "🔍 最近验证:${tun_verify_result}" fi if [ -n "${tun_verify_reason:-}" ]; then echo "🧾 最近原因:${tun_verify_reason}" fi if [ -n "${tun_verify_time:-}" ]; then echo "🕒 最近验证时间:${tun_verify_time}" fi echo echo "【编译结果】" echo "🧩 编译模式:active-only" [ -n "$(status_build_active_source 2>/dev/null || true)" ] && echo "📦 编译主订阅:$(status_build_active_source 2>/dev/null || true)" if [ -n "${build_status:-}" ]; then echo "🧩 最近编译:${build_status} @ ${build_time:-unknown}" else echo "🧩 最近编译:未知" fi [ -n "${build_active_sources:-}" ] && echo "🟢 实际参与编译:$build_active_sources" [ -n "${build_failed_active_sources:-}" ] && echo "❌ 编译失败源:$build_failed_active_sources" echo echo "【当前订阅健康】" if status_subscription_health_summary | grep -q .; then status_subscription_health_summary | sed 's/^/ /' else echo " 暂无健康数据" fi echo echo "【系统事件】" echo "🧩 当前配置来源:${config_source:-unknown}${config_source_time:+ @ ${config_source_time}}" case "${build_applied:-}" in true) echo "🟢 最近构建应用:true @ ${build_applied_time:-unknown}" ;; false) echo "⚠️ 最近构建应用:false @ ${build_applied_time:-unknown}" [ -n "${build_applied_reason:-}" ] && echo "未应用原因:${build_applied_reason}" ;; *) echo "⚪ 最近构建应用:unknown" ;; esac if [ -n "${build_block_reason:-}" ]; then echo "⚠️ 最近阻断:${build_block_reason} @ ${build_block_time:-unknown}" else echo "⚠️ 最近阻断:无" fi if [ -n "${last_switch_to:-}" ]; then echo "🤖 订阅切换建议记录:${last_switch_from:-unknown} -> ${last_switch_to} @ ${last_switch_time:-unknown}" else echo "🤖 订阅切换建议记录:无" fi if [ "${fallback_used:-false}" = "true" ]; then echo "⚠️ 最近回退:true @ ${fallback_time:-unknown}" [ -n "${fallback_reason:-}" ] && echo "回退原因:${fallback_reason}" else echo "⚠️ 最近回退:false" fi echo echo "【风险原因】" if status_risk_reason_lines | grep -q .; then status_risk_reason_lines | sed 's/^/ /' else echo " 无明显风险原因" fi echo echo "【网络闭环】" echo "🧭 问题裁决:$(connectivity_issue_text)" echo "👉 下一步:$(connectivity_next_action)" echo "🔍 关键证据:" connectivity_evidence_lines | sed 's/^/ /' echo echo "【推荐操作】" status_recommendation_lines | sed 's/^/ /' echo echo "【策略组摘要】" if [ "$controller_ok" = "true" ] && [ -n "${current_proxy_lines:-}" ]; then printf '%s\n' "$current_proxy_lines" | sed 's/^/ /' else echo " 控制器不可访问" fi echo } cmd_status() { prepare case "${1:-}" in --verbose|-v) print_status_summary_verbose ;; "") print_status_summary_compact ;; *) die_usage "未知的 status 参数:$1" "clashctl status [--verbose]" ;; esac } cmd_status_next() { prepare system_state_default_action } ui_controller_port() { local controller controller="$(controller_addr 2>/dev/null || true)" [ -n "${controller:-}" ] && [ "$controller" != "null" ] || return 1 echo "${controller##*:}" } ui_lan_ip() { ip route get 1.1.1.1 2>/dev/null | awk ' { for (i = 1; i <= NF; i++) { if ($i == "src") { print $(i+1) exit } } } ' } ui_public_ip() { curl -fsSL --connect-timeout 2 --max-time 3 https://api64.ipify.org 2>/dev/null || true } cmd_ui_box() { local line1="" line2="" line3="" line4="" line5="" line6="" [ -n "${1:-}" ] && line1="📶 状态:$1" [ -n "${2:-}" ] && line2="💻 本机:$2" [ -n "${3:-}" ] && line3="🏠 局域网:$3" [ -n "${4:-}" ] && line4="🌏 公网:$4" [ -n "${5:-}" ] && line5="☁️ 公共:$5" [ -n "${6:-}" ] && line6="🔑 密钥:$6" compute_box_width \ "🖥️ Web 控制台" \ "$line1" "$line2" "$line3" "$line4" "$line5" "$line6" \ "🔓 注意放行端口:${7:-unknown}" echo box_border_top box_title_line "🖥️ Web 控制台" box_border_mid box_empty [ -n "$line1" ] && box_section_line "$line1" [ -n "$line2" ] && box_section_line "$line2" [ -n "$line3" ] && box_section_line "$line3" [ -n "$line4" ] && box_section_line "$line4" [ -n "$line5" ] && box_section_line "$line5" [ -n "$line6" ] && box_section_line "$line6" box_empty box_section_line "🔓 注意放行端口:${7:-unknown}" box_border_bottom echo } logs_mihomo() { if [ ! -f "$LOG_DIR/mihomo.out.log" ]; then echo "Mihomo 日志文件不存在" return 0 fi tail -n 200 "$LOG_DIR/mihomo.out.log" } logs_subconverter() { local file file="$(subconverter_log_file)" if [ ! -f "$file" ]; then echo "subconverter 日志文件不存在" return 0 fi tail -n 200 "$file" } logs_service() { local backend backend="$(runtime_backend)" case "$backend" in systemd) systemd_service_logs ;; systemd-user) systemd_user_service_logs ;; script) echo "当前为脚本运行模式,服务日志即 Mihomo 日志" logs_mihomo ;; *) die "未知运行后端:$backend" ;; esac } cmd_logs() { prepare case "${1:-mihomo}" in mihomo) logs_mihomo ;; subconverter) logs_subconverter ;; service) logs_service ;; *) die "用法:clashctl logs [mihomo|subconverter|service]" ;; esac } doctor_print_title() { echo echo "🧰【$1】" } doctor_ok() { echo " ✔ $1" } doctor_warn() { echo " ⚠ $1" } doctor_fail() { echo " ✘ $1" } doctor_yq_available() { local yq_path yq_path="$(yq_bin 2>/dev/null || true)" [ -n "${yq_path:-}" ] && [ -x "$yq_path" ] } doctor_warn_skip_yq_parse() { doctor_warn "缺少 yq,跳过配置解析类检查" } doctor_install_context() { doctor_print_title "安装环境检查" if [ -n "$(install_env_os 2>/dev/null || true)" ]; then doctor_ok "操作系统:$(install_env_os 2>/dev/null || echo unknown)" else doctor_warn "未记录安装期操作系统" fi if [ -n "$(install_env_arch 2>/dev/null || true)" ]; then doctor_ok "系统架构:$(install_env_arch 2>/dev/null || echo unknown)" else doctor_warn "未记录安装期系统架构" fi if [ -n "$(install_env_scope 2>/dev/null || true)" ]; then doctor_ok "安装范围:$(install_env_scope 2>/dev/null || echo unknown)" else doctor_warn "未记录安装范围" fi if [ -n "$(install_env_container 2>/dev/null || true)" ]; then doctor_ok "容器环境:$(install_env_container 2>/dev/null || echo unknown)" else doctor_warn "未记录容器环境" fi if [ -n "$(install_plan_backend 2>/dev/null || true)" ]; then doctor_ok "安装期选择后端:$(install_plan_backend 2>/dev/null || echo unknown)" else doctor_warn "未记录安装期运行后端" fi if [ -n "$(install_plan_container_mode 2>/dev/null || true)" ]; then doctor_ok "容器兼容模式:$(install_plan_container_mode 2>/dev/null || echo unknown)" else doctor_warn "未记录容器兼容模式" fi if [ -n "$(install_plan_port_policy 2>/dev/null || true)" ]; then doctor_ok "端口策略:$(install_plan_port_policy 2>/dev/null || echo unknown)" else doctor_warn "未记录端口策略" fi } doctor_container_tun() { doctor_print_title "容器与 Tun 检查" case "$(install_env_container 2>/dev/null || echo unknown)" in host) doctor_ok "当前运行在主机环境" ;; docker) doctor_warn "检测到 Docker 环境" ;; container) doctor_warn "检测到容器环境" ;; *) doctor_warn "容器环境未知" ;; esac case "$(runtime_backend 2>/dev/null || echo unknown)" in systemd) doctor_ok "当前运行后端:systemd" ;; systemd-user) doctor_ok "当前运行后端:systemd-user" ;; script) doctor_warn "当前运行后端:script" ;; *) doctor_warn "当前运行后端未知" ;; esac if tun_device_exists; then doctor_ok "/dev/net/tun 存在" else doctor_warn "/dev/net/tun 不存在" fi if tun_device_readable; then doctor_ok "/dev/net/tun 可读写" else doctor_warn "/dev/net/tun 不可直接读写" fi case "$(install_env_tun_safe 2>/dev/null || true)" in true) doctor_ok "安装期判定:Tun 可安全管理" ;; false) doctor_warn "安装期判定:Tun 不可安全管理" ;; *) doctor_warn "安装期未记录 Tun 安全性" ;; esac if has_ip_command; then doctor_ok "ip 命令可用" else doctor_warn "缺少 ip 命令,Tun/路由能力受限" fi case "$(tun_enabled 2>/dev/null || echo false)" in true) doctor_warn "当前 Tun 状态:开启" ;; *) doctor_ok "当前 Tun 状态:关闭" ;; esac } doctor_dependencies() { doctor_print_title "依赖检查" [ -x "$(mihomo_bin)" ] && doctor_ok "Mihomo 已安装:$(mihomo_bin)" || doctor_fail "Mihomo 缺失:$(mihomo_bin)" [ -x "$(subconverter_bin)" ] && doctor_ok "subconverter 已安装:$(subconverter_bin)" || doctor_fail "subconverter 缺失:$(subconverter_bin)" [ -x "$(yq_bin)" ] && doctor_ok "yq 已安装:$(yq_bin)" || doctor_fail "yq 缺失:$(yq_bin)" } doctor_config() { local config_file active_profile mixed_port controller doctor_print_title "配置检查" config_file="$RUNTIME_DIR/config.yaml" if [ -s "$config_file" ]; then doctor_ok "运行配置存在:$config_file" else doctor_fail "运行配置缺失:$config_file" return 0 fi active_profile="$(show_active_profile 2>/dev/null || true)" [ -n "${active_profile:-}" ] || active_profile="default" doctor_ok "当前 Profile:$active_profile" if ! doctor_yq_available; then doctor_warn_skip_yq_parse if test_runtime_config "$config_file" >/dev/null 2>&1; then doctor_ok "配置校验通过" else doctor_fail "配置校验失败" fi return 0 fi mixed_port="$("$(yq_bin)" eval '.["mixed-port"] // .port // ""' "$config_file" 2>/dev/null | head -n 1)" controller="$("$(yq_bin)" eval '.["external-controller"] // ""' "$config_file" 2>/dev/null | head -n 1)" if [ -n "${mixed_port:-}" ] && [ "$mixed_port" != "null" ]; then doctor_ok "代理端口:$mixed_port" else doctor_warn "未解析到代理端口" fi if [ -n "${controller:-}" ] && [ "$controller" != "null" ]; then doctor_ok "控制器地址:$controller" else doctor_warn "未解析到控制器地址" fi if test_runtime_config "$config_file" >/dev/null 2>&1; then doctor_ok "配置校验通过" else doctor_fail "配置校验失败" fi } doctor_subscription() { local active file total enabled convert_count auto_disabled_count name doctor_print_title "订阅检查" file="$(subscriptions_file 2>/dev/null || true)" if [ -z "${file:-}" ] || [ ! -f "$file" ]; then doctor_warn "订阅配置文件不存在" return 0 fi doctor_ok "订阅策略:active-only" if ! doctor_yq_available; then doctor_warn_skip_yq_parse return 0 fi active="$(active_subscription_name 2>/dev/null || true)" total="$("$(yq_bin)" eval '.sources | keys | length' "$file" 2>/dev/null || echo 0)" enabled="$("$(yq_bin)" eval '[.sources[] | select(.enabled == true)] | length' "$file" 2>/dev/null || echo 0)" convert_count="$("$(yq_bin)" eval '[.sources[] | select(.type == "convert")] | length' "$file" 2>/dev/null || echo 0)" auto_disabled_count="$( while IFS= read -r name; do [ -n "${name:-}" ] || continue if subscription_auto_disabled "$name"; then echo 1 fi done < <("$(yq_bin)" eval '.sources | keys | .[]' "$file") )" auto_disabled_count="$(printf '%s\n' "${auto_disabled_count:-}" | awk 'NF{c++} END{print c+0}')" [ -n "${active:-}" ] && doctor_ok "当前主订阅:$active" || doctor_warn "当前主订阅为空" doctor_ok "订阅源总数:$total" doctor_ok "已启用订阅源:$enabled" doctor_ok "convert 订阅源:$convert_count" doctor_ok "自动禁用订阅源:$auto_disabled_count" if [ -n "${active:-}" ] && subscription_exists "$active"; then if subscription_enabled "$active"; then doctor_ok "当前主订阅可用:$active" else doctor_warn "当前主订阅已禁用:$active" fi if subscription_auto_disabled "$active"; then doctor_warn "当前主订阅曾被自动禁用:$active" fi fi } doctor_build() { local active active_sources failed_active_sources last_status last_time local block_reason block_time local error_summary error_detail error_stage doctor_print_title "编译状态检查" active="$(read_build_value "BUILD_ACTIVE_SOURCE" 2>/dev/null || true)" active_sources="$(status_build_active_sources 2>/dev/null || true)" failed_active_sources="$(status_build_failed_active_sources 2>/dev/null || true)" last_status="$(read_build_value "BUILD_LAST_STATUS" 2>/dev/null || true)" last_time="$(read_build_value "BUILD_LAST_TIME" 2>/dev/null || true)" error_summary="$(read_build_value "BUILD_LAST_ERROR_SUMMARY" 2>/dev/null || true)" error_detail="$(read_build_value "BUILD_LAST_ERROR_DETAIL" 2>/dev/null || true)" error_stage="$(read_build_value "BUILD_LAST_ERROR_STAGE" 2>/dev/null || true)" block_reason="$(read_runtime_event_value "RUNTIME_LAST_BUILD_BLOCK_REASON" 2>/dev/null || true)" block_time="$(read_runtime_event_value "RUNTIME_LAST_BUILD_BLOCK_TIME" 2>/dev/null || true)" if [ -z "${active:-}${active_sources:-}${failed_active_sources:-}${last_status:-}${last_time:-}${error_summary:-}${error_stage:-}${block_reason:-}" ]; then doctor_warn "未找到编译元数据(build.env)" return 0 fi doctor_ok "编译模式:active-only" if [ -n "${active:-}" ]; then doctor_ok "当前主订阅:$active" else doctor_warn "当前主订阅为空" fi if [ -n "${active_sources:-}" ]; then doctor_ok "实际参与编译:$active_sources" else doctor_warn "没有任何订阅参与编译" fi if [ -n "${failed_active_sources:-}" ]; then doctor_warn "失败订阅源:$failed_active_sources" else doctor_ok "失败订阅源:无" fi if [ -n "${last_status:-}" ]; then if [ "$last_status" = "success" ]; then doctor_ok "最近一次编译状态:$last_status" else doctor_warn "最近一次编译状态:$last_status" fi else doctor_warn "最近一次编译状态未知" fi if [ -n "${last_time:-}" ]; then doctor_ok "最近一次编译时间:$last_time" else doctor_warn "最近一次编译时间未知" fi if [ -n "${block_reason:-}" ]; then if [ -n "${block_time:-}" ]; then doctor_warn "最近一次编译阻断原因:${block_reason} @ ${block_time}" else doctor_warn "最近一次编译阻断原因:$block_reason" fi fi if [ -n "${error_summary:-}" ]; then doctor_warn "最近一次错误摘要:$error_summary" fi if [ -n "${error_stage:-}" ]; then doctor_warn "最近一次错误阶段:$error_stage" fi if [ -n "${error_detail:-}" ]; then echo " 详细错误:" printf '%s\n' "$error_detail" | sed 's/^/ /' fi } doctor_service() { local backend doctor_print_title "服务检查" backend="$(runtime_backend)" doctor_ok "运行后端:$backend" case "$backend" in systemd) if systemctl is-active --quiet "$(service_unit_name)"; then doctor_ok "systemd 服务运行中" systemctl show "$(service_unit_name)" --property MainPID --value 2>/dev/null | awk '{print " 进程号:" $1}' else doctor_warn "systemd 服务未运行" fi ;; systemd-user) if systemctl --user is-active --quiet "$(service_unit_name)"; then doctor_ok "用户级 systemd 服务运行中" systemctl --user show "$(service_unit_name)" --property MainPID --value 2>/dev/null | awk '{print " 进程号:" $1}' else doctor_warn "用户级 systemd 服务未运行" fi ;; script) if [ -f "$RUNTIME_DIR/mihomo.pid" ]; then local pid pid="$(cat "$RUNTIME_DIR/mihomo.pid" 2>/dev/null || true)" if [ -n "${pid:-}" ] && kill -0 "$pid" 2>/dev/null; then doctor_ok "脚本模式进程运行中" echo " 进程号:$pid" else doctor_warn "脚本模式 PID 文件存在,但进程未运行" fi else doctor_warn "脚本模式当前未运行" fi ;; *) doctor_fail "未知运行后端:$backend" ;; esac } doctor_ports() { local config_file mixed_port controller controller_port doctor_print_title "端口检查" config_file="$RUNTIME_DIR/config.yaml" if [ ! -s "$config_file" ]; then doctor_warn "运行配置不存在,跳过端口检查" return 0 fi if ! doctor_yq_available; then doctor_warn_skip_yq_parse if [ -x "$(subconverter_bin)" ]; then if is_port_in_use "$(subconverter_port)"; then doctor_ok "subconverter 端口已监听:$(subconverter_port)" else doctor_warn "subconverter 端口未监听:$(subconverter_port)" fi fi return 0 fi mixed_port="$("$(yq_bin)" eval '.["mixed-port"] // .port // ""' "$config_file" 2>/dev/null | head -n 1)" controller="$("$(yq_bin)" eval '.["external-controller"] // ""' "$config_file" 2>/dev/null | head -n 1)" controller_port="${controller##*:}" if [ -n "${mixed_port:-}" ] && [ "$mixed_port" != "null" ]; then if is_port_in_use "$mixed_port"; then doctor_ok "代理端口已监听:$mixed_port" else doctor_warn "代理端口未监听:$mixed_port" fi else doctor_warn "无法检查代理端口" fi if [ -n "${controller_port:-}" ] && [ "$controller_port" != "$controller" ] && [ "$controller_port" != "null" ]; then if is_port_in_use "$controller_port"; then doctor_ok "控制器端口已监听:$controller_port" else doctor_warn "控制器端口未监听:$controller_port" fi else doctor_warn "无法检查控制器端口" fi if [ -x "$(subconverter_bin)" ]; then if is_port_in_use "$(subconverter_port)"; then doctor_ok "subconverter 端口已监听:$(subconverter_port)" else doctor_warn "subconverter 端口未监听:$(subconverter_port)" fi fi } doctor_install_ports() { doctor_print_title "安装端口裁决检查" if [ -n "$(install_plan_mixed_port 2>/dev/null || true)" ]; then doctor_ok "安装期代理端口:$(install_plan_mixed_port 2>/dev/null || echo unknown)" if [ "$(install_plan_mixed_port_auto_changed 2>/dev/null || echo false)" = "true" ]; then doctor_warn "代理端口在安装期发生自动避让" fi else doctor_warn "未记录安装期代理端口" fi if [ -n "$(install_plan_controller 2>/dev/null || true)" ]; then doctor_ok "安装期控制器:$(install_plan_controller 2>/dev/null || echo unknown)" if [ "$(install_plan_controller_auto_changed 2>/dev/null || echo false)" = "true" ]; then doctor_warn "控制器端口在安装期发生自动避让" fi else doctor_warn "未记录安装期控制器地址" fi if [ -n "$(install_plan_dns_port 2>/dev/null || true)" ]; then doctor_ok "安装期 DNS 端口:$(install_plan_dns_port 2>/dev/null || echo unknown)" if [ "$(install_plan_dns_port_auto_changed 2>/dev/null || echo false)" = "true" ]; then doctor_warn "DNS 端口在安装期发生自动避让" fi else doctor_warn "未记录安装期 DNS 端口" fi } doctor_runtime_events() { local fallback_used fallback_time fallback_reason risk_level local config_source build_applied build_applied_time build_applied_reason doctor_print_title "运行事件检查" fallback_used="$(runtime_last_fallback_used)" fallback_time="$(runtime_last_fallback_time)" fallback_reason="$(runtime_last_fallback_reason)" risk_level="$(calculate_runtime_risk_level)" config_source="$(status_runtime_config_source 2>/dev/null || true)" build_applied="$(status_runtime_build_applied 2>/dev/null || true)" build_applied_time="$(status_runtime_build_applied_time 2>/dev/null || true)" build_applied_reason="$(status_runtime_build_applied_reason 2>/dev/null || true)" doctor_ok "当前风险等级:${risk_level:-unknown}" doctor_ok "当前配置来源:${config_source:-unknown}" case "${build_applied:-}" in true) doctor_ok "最近一次构建已应用" [ -n "${build_applied_time:-}" ] && echo " 应用时间:$build_applied_time" ;; false) doctor_warn "最近一次构建未应用" [ -n "${build_applied_time:-}" ] && echo " 记录时间:$build_applied_time" [ -n "${build_applied_reason:-}" ] && echo " 未应用原因:$build_applied_reason" ;; *) doctor_warn "最近一次构建应用状态未知" ;; esac if [ "${fallback_used:-}" = "true" ]; then doctor_warn "最近一次启动触发了配置回退" [ -n "${fallback_time:-}" ] && echo " 回退时间:$fallback_time" [ -n "${fallback_reason:-}" ] && echo " 回退原因:$fallback_reason" else doctor_ok "最近一次启动未触发配置回退" fi } doctor_install_verify() { local live_runtime="false" local live_controller="false" doctor_print_title "安装验证检查" case "$(install_verify_command_ready 2>/dev/null || true)" in true) doctor_ok "clashctl 命令入口可用" ;; false) doctor_fail "clashctl 命令入口不可用" ;; *) doctor_warn "未记录命令入口验证结果" ;; esac case "$(install_verify_config_ready 2>/dev/null || true)" in true) doctor_ok "安装后配置已就绪" ;; false) doctor_fail "安装后配置未就绪" ;; *) doctor_warn "未记录配置验证结果" ;; esac if status_is_running 2>/dev/null; then live_runtime="true" fi if proxy_controller_reachable 2>/dev/null; then live_controller="true" fi if [ "$live_runtime" = "true" ]; then doctor_ok "安装后运行态已就绪" else case "$(install_verify_runtime_ready 2>/dev/null || true)" in true) doctor_ok "安装后运行态已就绪" ;; false) doctor_warn "安装后运行态未就绪" ;; *) doctor_warn "未记录运行态验证结果" ;; esac fi if [ "$live_controller" = "true" ]; then doctor_ok "安装后控制器可访问" else case "$(install_verify_controller_ready 2>/dev/null || true)" in true) doctor_ok "安装后控制器可访问" ;; false) doctor_warn "安装后控制器不可访问" ;; *) doctor_warn "未记录控制器验证结果" ;; esac fi } doctor_active_switch() { local from to at reason doctor_print_title "主订阅自动切换检查" from="$(read_runtime_event_value "RUNTIME_LAST_ACTIVE_SWITCH_FROM" 2>/dev/null || true)" to="$(read_runtime_event_value "RUNTIME_LAST_ACTIVE_SWITCH_TO" 2>/dev/null || true)" at="$(read_runtime_event_value "RUNTIME_LAST_ACTIVE_SWITCH_TIME" 2>/dev/null || true)" reason="$(read_runtime_event_value "RUNTIME_LAST_ACTIVE_SWITCH_REASON" 2>/dev/null || true)" if [ -n "${to:-}" ]; then doctor_warn "检测到最近一次主订阅自动切换" [ -n "${from:-}" ] && echo " from : $from" echo " to : $to" [ -n "${at:-}" ] && echo " time : $at" [ -n "${reason:-}" ] && echo " reason : $reason" else doctor_ok "最近未发生主订阅自动切换" fi } doctor_controller() { local controller group_count current_examples doctor_print_title "控制器检查" controller="$(status_read_controller 2>/dev/null || true)" if [ -z "${controller:-}" ] || [ "$controller" = "null" ]; then doctor_fail "未解析到 external-controller" return 0 fi doctor_ok "控制器地址:$controller" if ! status_is_running; then doctor_warn "内核未运行,无法检查控制器 API" return 0 fi if proxy_controller_reachable 2>/dev/null; then doctor_ok "控制器 API 可访问" group_count="$(proxy_group_count 2>/dev/null || echo 0)" doctor_ok "可切换策略组数量:$group_count" if [ "${group_count:-0}" -gt 0 ]; then echo " 当前策略组摘要:" print_proxy_groups_status | head -n 5 | sed 's/^/ /' fi else doctor_fail "控制器 API 不可访问" fi } cmd_doctor() { prepare load_system_state ui_title "🧭 系统诊断" ui_section "总体结论" doctor_primary_conclusion ui_kv "⚠️" "风险等级" "$(doctor_risk_text)" ui_blank ui_section "发现的问题" if doctor_problem_lines | grep -q .; then doctor_problem_lines | sed 's/^/ /' else echo " 🟢 未发现明显问题" fi ui_blank ui_section "关键证据" doctor_evidence_lines | sed 's/^/ /' ui_blank ui_section "修复建议" doctor_recommendation_lines | sed 's/^/ /' ui_blank ui_section "详细检查" doctor_install_context doctor_dependencies doctor_container_tun doctor_config doctor_subscription doctor_build doctor_service doctor_ports doctor_install_ports doctor_runtime_events doctor_install_verify doctor_active_switch doctor_controller } cmd_dev() { prepare case "${1:-}" in reset) echo echo "🧪 正在恢复到安装前状态(保留项目目录与下载文件) ..." echo bash "$PROJECT_DIR/uninstall.sh" --dev-reset echo echo "🟢 开发重置完成" echo "🧩 保留内容:项目目录、已下载依赖、调试环境" echo "👉 下一步:重新执行 install.sh 或 clashctl status" echo ;; "") echo "🧭 用法:clashctl dev reset" ;; *) die_usage "未知的 dev 子命令:$1" "clashctl dev reset" ;; esac } cmd_config_show() { local active kernel build_status build_time config_source prepare active="$(active_subscription_name 2>/dev/null || true)" kernel="$(runtime_kernel_type 2>/dev/null || echo mihomo)" build_status="$(status_build_last_status 2>/dev/null || true)" build_time="$(status_build_last_time 2>/dev/null || true)" config_source="$(status_runtime_config_source 2>/dev/null || true)" ui_title "🧩 配置编译管理" ui_kv "📡" "当前主订阅" "${active:-未设置}" ui_kv "🚀" "当前内核" "${kernel:-mihomo}" ui_kv "🧩" "编译模式" "active-only" ui_kv "🟢" "最近构建" "${build_status:-unknown}${build_time:+ @ ${build_time}}" ui_kv "🧩" "配置来源" "${config_source:-unknown}" ui_blank ui_next "clashctl status" ui_blank } cmd_config() { prepare case "${1:-}" in show) cmd_config_show ;; explain) print_build_explain ;; regen) regenerate_config apply_runtime_change_after_config_mutation print_config_regen_feedback print_config_apply_feedback ;; kernel) shift || true [ -n "${1:-}" ] || die_usage "内核类型不能为空" "clashctl config kernel " write_runtime_kernel_type "$1" resolve_runtime_kernel regenerate_config apply_runtime_change_after_config_mutation print_config_kernel_feedback "$(runtime_kernel_type)" print_config_apply_feedback ;; "") ui_title "🧩 配置编译管理" echo "🧭 用法:" echo " clashctl config show" echo " clashctl config explain" echo " clashctl config regen" echo " clashctl config kernel " echo echo "🧩 说明:" echo " 当前编译链固定为 active-only" echo " 只处理当前 active 主订阅" echo ui_next "clashctl config show" ui_blank ;; *) die_usage "未知的 config 子命令:$1" "clashctl config" ;; esac } print_profile_use_feedback() { local profile="$1" ui_title "⚙️ Profile 已切换" ui_kv "⚙️" "当前 Profile" "$profile" main_feedback_runtime_state ui_next "clashctl status" ui_blank } cmd_profile() { prepare case "${1:-}" in list) print_profile_list ;; use) shift || true [ -n "${1:-}" ] || die "用法:clashctl profile use <名称>" set_active_profile "$1" regenerate_config apply_runtime_change_after_config_mutation print_profile_use_feedback "$1" print_config_apply_feedback ;; add) shift || true [ -n "${1:-}" ] || die "用法:clashctl profile add <名称>" add_profile "$1" ;; del) shift || true [ -n "${1:-}" ] || die "用法:clashctl profile del <名称>" delete_profile "$1" ;; set) shift || true [ -n "${1:-}" ] || die "用法:clashctl profile set <名称> <键> <值>" [ -n "${2:-}" ] || die "用法:clashctl profile set <名称> <键> <值>" [ -n "${3:-}" ] || die "用法:clashctl profile set <名称> <键> <值>" profile_set_value "$1" "$2" "$3" ;; "") ui_title "⚙️ Profile 管理" echo "🧭 用法:" echo " clashctl profile list" echo " clashctl profile use <名称>" echo " clashctl profile add <名称>" echo " clashctl profile del <名称>" echo " clashctl profile set <名称> <键> <值>" echo echo "🧩 说明:" echo " Profile 用于管理模板级配置差异" echo " 当前 active Profile 会参与运行时配置编译" echo echo "💡 常用动作:" echo " clashctl profile list" echo " clashctl profile use default" echo ui_next "clashctl status" ui_blank ;; *) die_usage "未知的 profile 子命令:$1" "clashctl profile" ;; esac } mixin_config_file() { echo "$CONFIG_DIR/mixin.yaml" } active_subscription_runtime_raw_file() { local name="$1" echo "$RUNTIME_DIR/tmp/source-${name}.yaml" } ensure_mixin_runtime_prepared() { local active source_file active="$(active_subscription_name 2>/dev/null || true)" [ -n "${active:-}" ] || die "当前没有活动订阅" subscription_exists "$active" || die "当前活动订阅不存在:$active" source_file="$(active_subscription_runtime_raw_file "$active")" mkdir -p "$RUNTIME_DIR/tmp" if [ -s "$source_file" ]; then echo "$source_file" return 0 fi if fetch_subscription_source "$active" "$source_file"; then echo "$source_file" return 0 fi rm -f "$source_file" 2>/dev/null || true die "无法获取当前活动订阅原始配置:$active" } open_editor_for_file() { local file="$1" local editor [ -n "${file:-}" ] || die "文件路径不能为空" touch "$file" if [ -n "${EDITOR:-}" ]; then editor="$EDITOR" elif command -v nano >/dev/null 2>&1; then editor="nano" elif command -v vim >/dev/null 2>&1; then editor="vim" elif command -v vi >/dev/null 2>&1; then editor="vi" else die "未找到可用编辑器,请先设置 EDITOR 环境变量" fi "$editor" "$file" } cmd_mixin_show() { local file prepare ensure_config_files file="$(mixin_config_file)" ui_title "🧩 Mixin 配置" ui_kv "🧩" "作用" "对原始订阅配置进行补充、覆盖或前后置合并" ui_kv "⚙️" "配置文件" "$file" ui_blank cat "$file" ui_blank ui_next "clashctl mixin edit" ui_blank } cmd_mixin_edit() { local file prepare ensure_config_files file="$(mixin_config_file)" open_editor_for_file "$file" echo echo "ℹ️ 正在重新生成配置 ..." regenerate_config if status_is_running; then service_restart echo "🟢 Mixin 已生效(已自动重启)" else echo "🟡 Mixin 已写入(下次启动生效)" fi echo "👉 下一步:clashctl mixin runtime" echo } cmd_mixin_raw() { local file prepare file="$(ensure_mixin_runtime_prepared)" echo echo "📡 原始订阅配置" echo echo "📡 说明:这是当前活动订阅拉取到的原始配置,不包含 Mixin 最终合并结果" echo "⚙️ 来源文件:$file" echo cat "$file" echo echo "👉 下一步:clashctl mixin runtime" echo } cmd_mixin_runtime() { local file prepare file="$RUNTIME_DIR/config.yaml" [ -s "$file" ] || die "🧩 运行时配置不存在:$file" echo echo "🧩 运行时配置" echo echo "🧩 说明:这是当前内核真正加载的最终配置" echo "⚙️ 配置文件:$file" echo cat "$file" echo echo "👉 下一步:clashctl status" echo } cmd_mixin() { case "${1:-}" in "") cmd_mixin_show ;; edit|-e|--edit) cmd_mixin_edit ;; raw|-c|--raw) cmd_mixin_raw ;; runtime|-r|--runtime) cmd_mixin_runtime ;; help|-h|--help) ui_title "🧩 Mixin 配置管理" echo "🧭 用法:" echo " clashctl mixin" echo " clashctl mixin edit" echo " clashctl mixin raw" echo " clashctl mixin runtime" echo echo "🧩 说明:" echo " mixin 用于补充、覆盖或前后置合并原始订阅配置" echo " runtime 展示当前内核真正加载的最终配置" echo echo "💡 常用动作:" echo " clashctl mixin" echo " clashctl mixin edit" echo " clashctl mixin runtime" echo echo "⚡ 快捷参数:" echo " clashctl mixin -e" echo " clashctl mixin -c" echo " clashctl mixin -r" echo ui_next "clashctl mixin" ui_blank ;; *) die_usage "未知的 mixin 子命令:$1" "clashctl mixin" ;; esac } runtime_config_exists() { [ -s "$RUNTIME_DIR/config.yaml" ] } doctor_risk_level() { local running controller_ok config_ok running="false" controller_ok="false" config_ok="false" status_is_running && running="true" proxy_controller_reachable 2>/dev/null && controller_ok="true" runtime_config_exists && config_ok="true" if [ "$running" = "true" ] && [ "$controller_ok" = "true" ] && [ "$config_ok" = "true" ]; then echo "low" return fi if [ "$running" = "false" ]; then echo "medium" return fi if [ "$controller_ok" = "false" ]; then echo "high" return fi echo "medium" } doctor_risk_text() { case "$(doctor_risk_level)" in low) echo "🟢 低" ;; medium) echo "🟡 中" ;; high) echo "🔴 高" ;; *) echo "⚪ 未知" ;; esac } doctor_problem_lines() { local active_sub active_sub="$(active_subscription_name 2>/dev/null || true)" if ! runtime_config_exists; then echo "⚠️ 运行配置缺失" fi if ! status_is_running; then echo "⚠️ 代理内核未运行" fi if status_is_running && ! proxy_controller_reachable 2>/dev/null; then echo "⚠️ 控制器不可访问" fi if [ -n "${active_sub:-}" ] && ! active_subscription_enabled 2>/dev/null; then echo "⚠️ 当前主订阅不可用" fi if [ "$(status_build_last_status 2>/dev/null || true)" = "failed" ]; then echo "⚠️ 最近一次编译失败" fi } doctor_primary_conclusion() { if ! runtime_config_exists; then echo "🔴 当前不可用:缺少运行配置" return 0 fi if ! status_is_running; then echo "🟡 当前未连接:代理内核未启动" return 0 fi if ! proxy_controller_reachable 2>/dev/null; then echo "🔴 当前异常:内核已启动,但控制器不可访问" return 0 fi echo "🟢 当前基本可用:代理内核与控制器均正常" } doctor_recommendation_lines() { local active_sub active_sub="$(active_subscription_name 2>/dev/null || true)" if ! runtime_config_exists; then if [ -n "$(subscription_url 2>/dev/null || true)" ]; then echo "💡 clashctl config regen" else echo "💡 clashctl add <订阅链接>" fi return 0 fi if ! status_is_running; then echo "💡 clashon" return 0 fi if ! proxy_controller_reachable 2>/dev/null; then echo "💡 clashctl logs service" echo "💡 clashctl off && clashon" return 0 fi if [ -n "${active_sub:-}" ] && ! active_subscription_enabled 2>/dev/null; then echo "💡 clashctl use" echo "💡 clashctl health" return 0 fi if [ "$(status_build_last_status 2>/dev/null || true)" = "failed" ]; then echo "💡 clashctl health" echo "💡 clashctl config regen" return 0 fi echo "💡 clashctl status" echo "💡 clashctl select" } doctor_evidence_lines() { local active_sub mixed_port controller active_sub="$(active_subscription_name 2>/dev/null || true)" mixed_port="$(status_read_mixed_port 2>/dev/null || true)" controller="$(status_read_controller 2>/dev/null || true)" if runtime_config_exists; then echo "🔍 运行配置:存在" else echo "🔍 运行配置:缺失" fi if status_is_running; then echo "🔍 服务状态:运行中" else echo "🔍 服务状态:未运行" fi if proxy_controller_reachable 2>/dev/null; then echo "🔍 控制器状态:可访问" else echo "🔍 控制器状态:不可访问" fi if [ -n "${active_sub:-}" ]; then if active_subscription_enabled 2>/dev/null; then echo "🔍 当前订阅:${active_sub}(可用)" else echo "🔍 当前订阅:${active_sub}(不可用)" fi else echo "🔍 当前订阅:未设置" fi if [ -n "${mixed_port:-}" ] && [ "$mixed_port" != "null" ]; then if is_port_in_use "$mixed_port"; then echo "🔍 代理端口:${mixed_port}(已监听)" else echo "🔍 代理端口:${mixed_port}(未监听)" fi fi if [ -n "${controller:-}" ] && [ "$controller" != "null" ]; then echo "🔍 控制器地址:${controller}" fi } set_controller_secret() { local secret="$1" local file="$RUNTIME_DIR/config.yaml" [ -n "${secret:-}" ] || die "密钥不能为空" [ -s "$file" ] || die "运行时配置不存在:$file" SECRET_VALUE="$secret" "$(yq_bin)" eval -i ' .secret = strenv(SECRET_VALUE) ' "$file" } cmd_secret() { local current_secret new_secret prepare runtime_config_exists || die_state "运行时配置不存在" "clashctl add <订阅链接> 或 clashctl config regen" case "${1:-}" in "") current_secret="$(controller_secret 2>/dev/null || true)" echo ui_title "🔑 Web 密钥" if [ -n "${current_secret:-}" ] && [ "$current_secret" != "null" ]; then ui_kv "🔑" "当前密钥" "$current_secret" else ui_kv "⚠️" "当前密钥" "未设置" fi ui_kv "🖥️" "用途" "用于访问 Clash Web 控制台" ui_next "clashctl ui" ui_blank echo ;; *) new_secret="$1" set_controller_secret "$new_secret" echo ui_title "🔑 密钥已更新" ui_kv "🔑" "新密钥" "$new_secret" if status_is_running; then service_restart ui_kv "🟢" "生效状态" "已重启并生效" else ui_kv "🟡" "生效状态" "将在下次启动时生效" fi ui_next "clashctl ui" ui_blank echo ;; esac } cmd_tun_status() { local enabled stack env_type can_enable verify_result verify_time verify_reason prepare enabled="$(tun_enabled)" stack="$(tun_stack)" env_type="$(container_env_type)" verify_result="$(read_tun_last_verify_result 2>/dev/null || true)" verify_time="$(read_tun_last_verify_time 2>/dev/null || true)" verify_reason="$(read_tun_last_verify_reason 2>/dev/null || true)" echo echo "🧪 Tun 状态" echo if [ "$enabled" = "true" ]; then echo "🟢 当前状态:已开启" else echo "🔴 当前状态:未开启" fi echo "⚙️ Tun stack:$stack" echo "🖥️ 环境类型:$env_type" if can_manage_tun_safely; then echo "🟢 环境检查:满足基础开启条件" else echo "⚠️ 环境检查:当前不满足基础开启条件" fi if [ -n "${verify_result:-}" ]; then echo "🔍 最近验证:$verify_result" [ -n "${verify_reason:-}" ] && echo "🧾 最近原因:$verify_reason" [ -n "${verify_time:-}" ] && echo "🕒 最近验证时间:$verify_time" fi if [ "$enabled" = "true" ]; then echo "👉 下一步:clashctl tun doctor" else echo "👉 下一步:clashctl tun on" fi echo } cmd_tun_on() { local verify_result local container_mode risk_reason prepare container_mode="$(tun_container_mode 2>/dev/null || echo unknown)" risk_reason="$(tun_container_risk_reason 2>/dev/null || true)" case "$container_mode" in host) ;; container-safe) ui_warn "当前处于容器环境,Tun 将按保守模式尝试开启" [ -n "${risk_reason:-}" ] && ui_warn "$risk_reason" ;; container-risky) mark_tun_last_action "on" "blocked" "${risk_reason:-container-risky}" mark_tun_last_verification "blocked" "${risk_reason:-container-risky}" print_tun_container_gate_feedback "$container_mode" "${risk_reason:-容器环境不满足 Tun 基础条件}" return 1 ;; *) ui_warn "无法识别当前 Tun 容器模式,按高风险处理" mark_tun_last_action "on" "blocked" "unknown-container-mode" mark_tun_last_verification "blocked" "unknown-container-mode" print_tun_container_gate_feedback "container-risky" "unknown-container-mode" return 1 ;; esac if ! can_manage_tun_safely; then echo echo "🔴 Tun 模式无法开启" echo "⚠️ 原因:当前环境不满足基础 Tun 条件" echo "👉 下一步:clashctl tun doctor" echo return 1 fi case "$(tun_kernel_support_level 2>/dev/null || echo unknown)" in full) ;; limited) ui_warn "$(tun_kernel_support_reason 2>/dev/null || echo '当前内核仅对 Tun 提供降级支持')" ;; *) ui_warn "当前内核的 Tun 支持等级未知,请谨慎开启" ;; esac set_tun_enabled "true" regenerate_config if status_is_running; then service_restart fi verify_result="$(tun_effective_check 2>/dev/null || true)" [ -n "${verify_result:-}" ] || verify_result="unknown" if [ "$verify_result" = "ok" ]; then mark_tun_last_action "on" "success" "effective" mark_tun_last_verification "success" "effective" else mark_tun_last_action "on" "partial" "$verify_result" mark_tun_last_verification "partial" "$verify_result" fi print_tun_container_gate_feedback "$container_mode" "$risk_reason" print_tun_on_feedback "$verify_result" } cmd_tun_off() { local verify_result prepare set_tun_enabled "false" regenerate_config if status_is_running; then service_restart fi verify_result="$(tun_disable_check 2>/dev/null || true)" [ -n "${verify_result:-}" ] || verify_result="unknown" if [ "$verify_result" = "ok" ]; then mark_tun_last_action "off" "success" "rolled-back" mark_tun_last_verification "success" "disabled" else mark_tun_last_action "off" "partial" "$verify_result" mark_tun_last_verification "partial" "$verify_result" fi print_tun_off_feedback "$verify_result" } tun_runtime_status_text() { if [ "$(tun_enabled)" = "true" ]; then echo "已开启" else echo "未开启" fi } doctor_tun_checks() { local env_type stack runtime_tun_status backend dns_listen_value local runtime_tun_enabled runtime_tun_stack runtime_tun_auto_route local effective_result disable_result route_dev prepare echo echo "😼 Tun 诊断" echo runtime_tun_status="$(tun_runtime_status_text)" stack="$(tun_stack)" backend="$(runtime_backend)" env_type="$(container_env_type)" runtime_tun_enabled="$(runtime_config_tun_enabled 2>/dev/null || echo false)" runtime_tun_stack="$(runtime_config_tun_stack 2>/dev/null || echo "")" runtime_tun_auto_route="$(runtime_config_tun_auto_route 2>/dev/null || echo false)" route_dev="$(default_route_dev 2>/dev/null || true)" echo "【总体结论】" if can_manage_tun_safely; then echo "🟢 当前环境满足基础 Tun 开启条件" else echo "🔴 当前环境不满足基础 Tun 开启条件" fi echo "🧪 当前 Tun 状态:$runtime_tun_status" echo "⚙️ Tun stack:$stack" echo "⚙️ 运行后端:$backend" echo "🚀 当前内核:$(runtime_kernel_type 2>/dev/null || echo unknown)" echo "🧩 Tun 支持等级:$(tun_kernel_support_text 2>/dev/null || echo 未知)" echo "🖥️ 环境类型:$env_type" echo "🧭 容器裁决:$(tun_container_mode_text 2>/dev/null || echo 未知)" echo "💡 内核说明:$(tun_kernel_support_reason 2>/dev/null || echo 未知)" if [ "$(tun_container_mode 2>/dev/null || echo unknown)" = "container-risky" ]; then local gate_reason gate_reason="$(tun_container_risk_reason 2>/dev/null || true)" [ -n "${gate_reason:-}" ] && echo "🔴 阻断原因:$gate_reason" fi echo echo "【发现的问题】" if tun_problem_lines | grep -q .; then tun_problem_lines | sed 's/^/ /' else echo " 🟢 未发现明显问题" fi echo echo "【关键证据】" if is_root_user; then echo " 🟢 当前为 root 用户" else echo " ⚠️ 当前不是 root 用户" fi if tun_device_exists; then echo " 🟢 /dev/net/tun:存在" else echo " 🔴 /dev/net/tun:不存在" fi if tun_device_exists; then if tun_device_readable; then echo " 🟢 /dev/net/tun:可读写" else echo " ⚠️ /dev/net/tun:存在但不可正常读写" fi fi case "$(has_cap_net_admin; echo $?)" in 0) echo " 🟢 CAP_NET_ADMIN:已检测到" ;; 2) echo " ⚠️ CAP_NET_ADMIN:无法精确判断(缺少 capsh)" ;; *) echo " ⚠️ CAP_NET_ADMIN:未检测到" ;; esac if has_ip_command; then echo " 🟢 ip 命令:可用" else echo " ⚠️ ip 命令:缺失" fi if runtime_config_exists; then dns_listen_value="$("$(yq_bin)" eval '.dns.listen // ""' "$RUNTIME_DIR/config.yaml" 2>/dev/null | head -n 1)" if [ -n "${dns_listen_value:-}" ] && [ "$dns_listen_value" != "null" ]; then echo " 🟢 DNS 监听:$dns_listen_value" else echo " ⚠️ DNS 监听:未解析到" fi echo " 🧩 runtime tun.enable:${runtime_tun_enabled:-false}" if [ -n "${runtime_tun_stack:-}" ] && [ "$runtime_tun_stack" != "null" ]; then echo " 🧩 runtime tun.stack:$runtime_tun_stack" fi echo " 🧭 runtime tun.auto-route:${runtime_tun_auto_route:-false}" else echo " ⚠️ 运行时配置:不存在" fi if [ -n "${route_dev:-}" ]; then echo " 🌐 默认路由设备:$route_dev" else echo " ⚠️ 默认路由设备:未解析到" fi echo echo "【生效验证】" if [ "$(tun_enabled 2>/dev/null || echo false)" = "true" ]; then effective_result="$(tun_effective_check 2>/dev/null || true)" if [ "$effective_result" = "ok" ]; then echo " 🟢 Tun 已完成系统级接管验证" if [ "${runtime_tun_auto_route:-false}" = "true" ]; then if default_route_is_tun_like; then echo " 🟢 默认路由已切换到 Tun 类设备" else echo " ⚠️ auto-route 已开启,但默认路由未识别为 Tun 类设备" fi fi else echo " ⚠️ Tun 已开启,但接管验证未完全通过:${effective_result:-unknown}" fi else disable_result="$(tun_disable_check 2>/dev/null || true)" if [ "$disable_result" = "ok" ]; then echo " 🟢 Tun 当前处于关闭态,回滚检查通过" else echo " ⚠️ Tun 当前虽为关闭态,但仍存在残留:${disable_result:-unknown}" fi fi echo echo "【建议操作】" tun_recommendation_lines | sed 's/^/ /' echo } tun_recommendation_lines() { local enabled env_type can_enable local effective_result disable_result local config_tun_enabled auto_route local kernel_support kernel_support="$(tun_kernel_support_level 2>/dev/null || echo unknown)" enabled="$(tun_enabled 2>/dev/null || echo false)" env_type="$(container_env_type 2>/dev/null || echo unknown)" config_tun_enabled="$(runtime_config_tun_enabled 2>/dev/null || echo false)" auto_route="$(runtime_config_tun_auto_route 2>/dev/null || echo false)" can_enable="false" if can_manage_tun_safely 2>/dev/null; then can_enable="true" fi if [ "$kernel_support" = "limited" ] && [ "$enabled" != "true" ]; then echo "1. 当前内核为 clash,Tun 仅按降级支持处理" echo "2. 如需最稳妥 Tun 体验,建议先执行:clashctl config kernel mihomo" echo "3. 再执行:clashctl tun on" return 0 fi if [ "$enabled" = "true" ]; then effective_result="$(tun_effective_check 2>/dev/null || true)" [ -n "${effective_result:-}" ] || effective_result="unknown" if [ "$kernel_support" = "limited" ]; then echo "1. 当前 Tun 运行在 clash 内核上,建议重点关注稳定性" echo "2. 如需最稳妥体验,建议切换内核:clashctl config kernel mihomo" echo "3. 再执行:clashctl tun doctor" return 0 fi case "$effective_result" in ok) echo "1. Tun 已生效,可继续使用" echo "2. 如需恢复普通代理模式,执行:clashctl tun off" ;; runtime-not-running) echo "1. 先重新启动代理:clashon" echo "2. 再执行:clashctl tun doctor" ;; controller-unreachable) echo "1. 先执行:clashctl doctor" echo "2. 若服务异常,执行:clashoff && clashon" ;; default-route-not-tun) if [ "$env_type" != "host" ]; then echo "1. 当前是容器环境,优先检查宿主机是否映射 /dev/net/tun 并授予 NET_ADMIN" echo "2. 若只是想保守启用 Tun,后续可考虑关闭 auto-route" else echo "1. 检查当前系统路由是否允许 Tun 接管" echo "2. 执行:ip route show default" echo "3. 再执行:clashctl tun doctor" fi ;; disabled-in-state|disabled-in-runtime-config) echo "1. 当前 Tun 状态与配置不一致" echo "2. 建议执行:clashctl tun off" echo "3. 再重新执行:clashctl tun on" ;; *) echo "1. Tun 已开启但未完全生效,先执行:clashctl tun doctor" echo "2. 如仍异常,执行:clashctl tun off" echo "3. 再执行:clashctl tun on" ;; esac return 0 fi disable_result="$(tun_disable_check 2>/dev/null || true)" [ -n "${disable_result:-}" ] || disable_result="unknown" if [ "$disable_result" != "ok" ]; then echo "1. Tun 关闭后仍有残留,建议执行:clashctl tun off" echo "2. 如仍异常,执行:clashoff && clashon" echo "3. 再执行:clashctl tun doctor" return 0 fi if [ "$can_enable" != "true" ]; then case "$container_mode" in container-risky) echo "1. 当前容器环境已被裁决为高风险:${risk_reason:-容器条件不足}" echo "2. 检查宿主机是否映射 /dev/net/tun" echo "3. 检查是否授予 CAP_NET_ADMIN / --cap-add=NET_ADMIN" echo "4. 检查容器内是否具备 ip 命令" echo "5. 条件满足后再执行:clashctl tun on" ;; *) echo "1. 当前环境不满足 Tun 基础条件" echo "2. 优先检查:/dev/net/tun、CAP_NET_ADMIN、ip 命令" echo "3. 条件满足后再执行:clashctl tun on" ;; esac return 0 fi if [ "${config_tun_enabled:-false}" = "true" ] && [ "$enabled" != "true" ]; then echo "1. 当前运行配置仍保留 Tun 开启状态" echo "2. 建议先执行:clashctl tun off" echo "3. 再观察:clashctl tun doctor" return 0 fi case "$container_mode" in container-safe) echo "1. 当前容器环境已通过保守裁决,可尝试开启 Tun:clashctl tun on" echo "2. 开启后立即执行:clashctl tun doctor" echo "3. 若接管异常,优先检查默认路由、宿主机权限与设备映射" return 0 ;; container-risky) echo "1. 当前容器环境属于高风险,不建议直接开启 Tun" echo "2. 先修复:${risk_reason:-容器条件不足}" echo "3. 修复后再执行:clashctl tun on" return 0 ;; esac echo "1. 当前环境可尝试开启 Tun:clashctl tun on" echo "2. 开启后建议立即执行:clashctl tun doctor" if [ "${auto_route:-false}" = "true" ]; then echo "3. 如需验证系统接管,可检查默认路由是否变化" fi } tun_problem_lines() { local enabled env_type config_tun_enabled auto_route local effective_result disable_result route_dev local container_mode risk_reason local kernel_support kernel_support="$(tun_kernel_support_level 2>/dev/null || echo unknown)" container_mode="$(tun_container_mode 2>/dev/null || echo unknown)" risk_reason="$(tun_container_risk_reason 2>/dev/null || true)" enabled="$(tun_enabled 2>/dev/null || echo false)" env_type="$(container_env_type 2>/dev/null || echo unknown)" config_tun_enabled="$(runtime_config_tun_enabled 2>/dev/null || echo false)" auto_route="$(runtime_config_tun_auto_route 2>/dev/null || echo false)" route_dev="$(default_route_dev 2>/dev/null || true)" if ! tun_device_exists 2>/dev/null; then echo "• /dev/net/tun 不存在" fi if tun_device_exists 2>/dev/null && ! tun_device_readable 2>/dev/null; then echo "• /dev/net/tun 存在但不可正常读写" fi if ! has_ip_command 2>/dev/null; then echo "• 缺少 ip 命令,无法进行完整路由校验" fi if ! can_manage_tun_safely 2>/dev/null; then echo "• 当前环境不满足 Tun 安全开启条件" fi if [ "$env_type" != "host" ]; then case "$container_mode" in container-safe) echo "• 当前处于容器环境,Tun 虽可尝试开启,但建议重点关注路由接管结果" ;; container-risky) echo "• 当前处于高风险容器环境:${risk_reason:-容器条件不足}" ;; *) echo "• 当前处于容器环境,Tun 状态未知" ;; esac fi case "$kernel_support" in limited) echo "• 当前内核为 clash,Tun 仅按降级支持处理,稳定性可能弱于 mihomo" ;; unknown) echo "• 当前内核的 Tun 支持等级未知" ;; esac if ! runtime_config_exists 2>/dev/null; then echo "• 运行时配置不存在" return 0 fi if [ "$enabled" = "true" ] && [ "${config_tun_enabled:-false}" != "true" ]; then echo "• Tun 状态文件已开启,但 runtime/config.yaml 未启用 tun.enable" fi if [ "$enabled" != "true" ] && [ "${config_tun_enabled:-false}" = "true" ]; then echo "• Tun 状态文件已关闭,但 runtime/config.yaml 仍启用 tun.enable" fi if [ "$enabled" = "true" ]; then effective_result="$(tun_effective_check 2>/dev/null || true)" if [ "$effective_result" != "ok" ]; then echo "• Tun 已开启,但系统级接管验证未通过:${effective_result:-unknown}" fi if [ "${auto_route:-false}" = "true" ] && ! default_route_is_tun_like 2>/dev/null; then echo "• auto-route 已开启,但默认路由未识别为 Tun 类设备:${route_dev:-unknown}" fi else disable_result="$(tun_disable_check 2>/dev/null || true)" if [ "$disable_result" != "ok" ]; then echo "• Tun 当前虽为关闭态,但仍存在残留:${disable_result:-unknown}" fi fi } cmd_tun() { case "${1:-}" in ""|status) cmd_tun_status ;; on) cmd_tun_on ;; off) cmd_tun_off ;; doctor) doctor_tun_checks ;; *) ui_title "🧪 Tun 模式管理" echo "🧭 用法:" echo " clashctl tun" echo " clashctl tun status" echo " clashctl tun on" echo " clashctl tun off" echo " clashctl tun doctor" echo echo "🧩 说明:" echo " Tun 属于高级接管能力" echo " 开启前应确认环境支持 /dev/net/tun、权限与网络能力" echo echo "💡 常用动作:" echo " clashctl tun status" echo " clashctl tun doctor" echo " clashtun on" echo ui_next "clashctl tun doctor" ui_blank ;; esac } cmd_add() { local sub_name prepare [ -n "${1:-}" ] || die "用法:clashctl add [clash|convert] [name]" sub_name="${3:-default}" set_subscription "${1:-}" "${2:-clash}" "$sub_name" apply_runtime_change_after_config_mutation print_add_feedback "$sub_name" } cmd_use() { local recommended active prepare case "${1:-}" in --recommend|-r) recommended="$(recommended_subscription_name 2>/dev/null || true)" active="$(active_subscription_name 2>/dev/null || true)" if [ -z "${recommended:-}" ]; then if [ -n "${active:-}" ]; then ui_title "🟢 当前无更优推荐,保持当前" ui_kv "📡" "当前主订阅" "$active" ui_next "clashctl status" ui_blank return 0 fi ui_title "🔴 当前没有可推荐的订阅" ui_next "clashctl ls --verbose" ui_blank return 1 fi if [ "${recommended:-}" = "${active:-}" ]; then ui_title "🟢 当前已是推荐订阅,无需切换" [ -n "${active:-}" ] && ui_kv "📡" "当前主订阅" "$active" ui_next "clashctl status" ui_blank return 0 fi set_active_subscription "$recommended" apply_runtime_change_after_config_mutation print_use_feedback "$recommended" return 0 ;; "") print_use_context use_subscription_interactive ;; *) set_active_subscription "$1" apply_runtime_change_after_config_mutation print_use_feedback "$1" ;; esac } subscription_pick_index() { local count="$1" local input while true; do printf "> " IFS= read -r input || return 1 case "${input:-}" in q|Q) return 1 ;; esac if printf '%s' "$input" | grep -Eq '^[0-9]+$' && [ "$input" -ge 1 ] && [ "$input" -le "$count" ]; then echo "$input" return 0 fi echo "请输入 1-$count 之间的编号,或输入 q 退出" done } use_subscription_interactive() { local active idx count selected_name current_enabled current_health current_fail local -a names active="$(active_subscription_name 2>/dev/null || true)" while IFS= read -r selected_name; do [ -n "${selected_name:-}" ] || continue names+=("$selected_name") done < <("$(yq_bin)" eval '.sources | keys | .[]' "$(subscriptions_file)" 2>/dev/null) count="${#names[@]}" [ "$count" -gt 0 ] || die "当前没有任何订阅" echo echo "😼 请选择订阅" echo if [ -n "${active:-}" ] && subscription_exists "$active"; then if subscription_enabled "$active"; then current_enabled="enabled" else current_enabled="disabled" fi current_health="$(subscription_health_status "$active" 2>/dev/null || echo "unknown")" current_fail="$(subscription_fail_count "$active" 2>/dev/null || echo "0")" echo "当前主订阅:$active ($current_enabled / $current_health / fail=$current_fail)" echo fi echo "编号 名称 类型 启用状态 健康状态 失败次数" idx=1 for selected_name in "${names[@]}"; do print_subscription_pick_line "$idx" "$selected_name" idx=$((idx + 1)) done echo echo " q) 退出" echo idx="$(subscription_pick_index "$count")" || return 0 selected_name="${names[$((idx - 1))]}" set_active_subscription "$selected_name" apply_runtime_change_after_config_mutation print_use_feedback "$selected_name" } health_overview_lines() { local active recommended active_health active_fail active_enabled active="$(active_subscription_name 2>/dev/null || true)" recommended="$(recommended_subscription_name 2>/dev/null || true)" if [ -n "${active:-}" ]; then if subscription_enabled "$active"; then active_enabled="enabled" else active_enabled="disabled" fi active_health="$(subscription_health_status "$active" 2>/dev/null || echo "unknown")" active_fail="$(subscription_fail_count "$active" 2>/dev/null || echo "0")" echo "📡 当前主订阅:$active" echo "❤️ 当前健康状态:$active_health" echo "⚠️ 当前失败次数:$active_fail" echo "⚙️ 当前启用状态:$active_enabled" else echo "📡 当前主订阅:未设置" fi if [ -n "${recommended:-}" ] && [ "${recommended:-}" != "${active:-}" ]; then echo "💡 推荐订阅:$recommended" elif [ -n "${active:-}" ]; then echo "💡 推荐订阅:保持当前" else echo "💡 推荐订阅:暂无" fi } health_recommendation_lines() { load_system_state case "$SYSTEM_STATE" in ready) echo "💡 clashctl status" echo "💡 clashctl select" ;; stopped) if [ "$SUBSCRIPTION_STATE" = "missing" ]; then echo "💡 clashctl add <订阅链接>" else echo "💡 clashon" fi ;; degraded|broken) echo "💡 clashctl doctor" echo "💡 clashctl status --verbose" ;; *) echo "💡 clashctl status" ;; esac } cmd_ls() { local verbose_mode="false" prepare case "${1:-}" in --verbose|-v) verbose_mode="true" ;; "") ;; *) die_usage "ls 参数不合法" "clashctl ls [--verbose]" ;; esac if [ "$verbose_mode" = "true" ]; then ui_title "📡 订阅列表(详细)" ui_section "当前推荐" subscription_list_overview_lines | sed 's/^/ /' ui_blank ui_section "逐订阅详情" list_subscriptions_verbose | sed 's/^/ /' ui_blank ui_section "下一步建议" echo " 👉 clashctl use" echo " 👉 clashctl health --verbose" echo " 👉 clashctl config show" ui_blank return 0 fi ui_title "📡 订阅列表" ui_section "当前推荐" subscription_list_overview_lines | sed 's/^/ /' ui_blank ui_section "可直接使用" list_subscriptions ui_blank ui_section "下一步建议" subscription_list_recommendation_lines | sed 's/^/ /' ui_blank } cmd_health() { local verbose_mode="false" local target_name="" prepare while [ $# -gt 0 ]; do case "$1" in --verbose|-v) verbose_mode="true" ;; *) if [ -z "${target_name:-}" ]; then target_name="$1" else die_usage "health 参数不合法" "clashctl health [名称] [--verbose]" fi ;; esac shift done if [ "$verbose_mode" = "true" ]; then ui_title "❤️ 订阅健康状态(详细)" if [ -n "${target_name:-}" ]; then ui_section "单订阅详情" print_subscription_health_one "$target_name" | sed 's/^/ /' ui_blank ui_section "建议操作" echo " 💡 clashctl use ${target_name}" echo " 💡 clashctl ls --verbose" ui_blank return 0 fi ui_section "当前主订阅" health_overview_lines | sed 's/^/ /' ui_blank ui_section "逐订阅详情" print_subscription_health_verbose | sed 's/^/ /' ui_blank ui_section "系统解释" health_verbose_explanation_lines | sed 's/^/ /' ui_blank ui_section "建议操作" health_recommendation_lines | sed 's/^/ /' ui_blank return 0 fi ui_title "❤️ 订阅健康状态" if [ -n "${target_name:-}" ]; then ui_section "单订阅详情" print_subscription_health_one "$target_name" | sed 's/^/ /' ui_blank ui_section "建议操作" echo " 💡 clashctl use ${target_name}" echo " 💡 clashctl ls --verbose" ui_blank return 0 fi ui_section "总览" health_overview_lines | sed 's/^/ /' ui_blank ui_section "订阅简表" print_subscription_health_summary ui_blank ui_section "建议操作" health_recommendation_lines | sed 's/^/ /' ui_blank } cmd_select() { prepare if [ -z "${1:-}" ]; then print_select_context || return $? proxy_select_interactive return 0 fi proxy_select_direct "$@" } cmd_proxy_groups() { local group current type found="false" prepare if ! status_is_running; then die_state "代理内核未运行" "clashon" fi if ! proxy_controller_reachable 2>/dev/null; then die_state "控制器不可访问" "clashctl doctor" fi ui_title "📦 策略组列表" echo " 📦 名称 类型 当前节点" while IFS= read -r group; do [ -n "${group:-}" ] || continue found="true" type="$(proxy_group_type "$group" 2>/dev/null || echo "unknown")" current="$(proxy_group_current "$group" 2>/dev/null || echo "-")" printf ' 📦 %-20s %-12s %s\n' "$group" "$type" "$current" done < <(proxy_group_list) if [ "$found" != "true" ]; then echo " 📭 暂无可切换策略组" fi ui_blank ui_next "clashctl select" ui_blank } cmd_proxy_current() { local group current found="false" prepare if ! status_is_running; then die_state "代理内核未运行" "clashon" fi if ! proxy_controller_reachable 2>/dev/null; then die_state "控制器不可访问" "clashctl doctor" fi if [ -n "${1:-}" ]; then group="$1" current="$(proxy_group_current "$group" 2>/dev/null || true)" ui_title "🚀 当前节点" ui_kv "📦" "策略组" "$group" ui_kv "🚀" "当前节点" "${current:-未知}" ui_blank return 0 fi ui_title "🚀 当前节点总览" while IFS= read -r group; do [ -n "${group:-}" ] || continue found="true" current="$(proxy_group_current "$group" 2>/dev/null || echo "-")" printf ' 🚀 %-20s %s\n' "$group" "$current" done < <(proxy_group_list) if [ "$found" != "true" ]; then echo " 📭 暂无可切换策略组" fi ui_blank } cmd_proxy_nodes() { local group="$1" local current node found="false" prepare [ -n "${group:-}" ] || die "🧭 用法:clashctl proxy nodes <策略组>" if ! status_is_running; then die_state "代理内核未运行" "clashon" fi if ! proxy_controller_reachable 2>/dev/null; then die_state "控制器不可访问" "clashctl doctor" fi current="$(proxy_group_current "$group" 2>/dev/null || true)" ui_title "🚀 候选节点列表" ui_kv "📦" "策略组" "$group" [ -n "${current:-}" ] && ui_kv "🚀" "当前节点" "$current" ui_blank while IFS= read -r node; do [ -n "${node:-}" ] || continue found="true" if [ "$node" = "$current" ]; then printf ' * 🚀 %s\n' "$node" else printf ' 🚀 %s\n' "$node" fi done < <(proxy_group_nodes "$group") if [ "$found" != "true" ]; then echo " 📭 当前策略组没有候选节点" fi ui_blank ui_next "clashctl proxy select ${group} <节点>" ui_blank } proxy_pick_index() { local count="$1" local input while true; do printf "> " IFS= read -r input || return 1 case "${input:-}" in q|Q) return 1 ;; esac if printf '%s' "$input" | grep -Eq '^[0-9]+$' && [ "$input" -ge 1 ] && [ "$input" -le "$count" ]; then echo "$input" return 0 fi echo "⚠️ 请输入 1-$count 之间的编号,或输入 q 退出" done } proxy_pick_group_interactive() { local idx count group current local -a groups while IFS= read -r group; do [ -n "${group:-}" ] || continue groups+=("$group") done < <(proxy_group_list) count="${#groups[@]}" [ "$count" -gt 0 ] || die "📭 暂无可切换策略组" echo "📦 请选择策略组:" idx=1 for group in "${groups[@]}"; do current="$(proxy_group_current "$group" 2>/dev/null || echo "-")" printf ' %s) 📦 %s -> 🚀 %s\n' "$idx" "$group" "$current" idx=$((idx + 1)) done echo " q) 退出" echo idx="$(proxy_pick_index "$count")" || return 1 echo "${groups[$((idx - 1))]}" } cmd_sub() { prepare case "${1:-}" in list) shift || true cmd_ls "$@" ;; use) shift || true [ -n "${1:-}" ] || die "用法:clashctl sub use <名称>" set_active_subscription "$1" apply_runtime_change_after_config_mutation ;; set) shift || true [ -n "${1:-}" ] || die "用法:clashctl sub set [clash|convert] [name](推荐使用:clashctl add <订阅链接>)" set_subscription "${1:-}" "${2:-clash}" "${3:-default}" apply_runtime_change_after_config_mutation ;; enable) shift || true [ -n "${1:-}" ] || die "🧭 用法:clashctl sub enable <名称>" enable_subscription "$1" regenerate_config apply_runtime_change_after_config_mutation print_sub_enable_feedback "$1" ;; disable) shift || true [ -n "${1:-}" ] || die "🧭 用法:clashctl sub disable <名称>" disable_subscription "$1" regenerate_config apply_runtime_change_after_config_mutation print_sub_disable_feedback "$1" ;; rename) shift || true [ -n "${1:-}" ] || die "🧭 用法:clashctl sub rename <旧名称> <新名称>" [ -n "${2:-}" ] || die "🧭 用法:clashctl sub rename <旧名称> <新名称>" rename_subscription "$1" "$2" regenerate_config apply_runtime_change_after_config_mutation print_sub_rename_feedback "$1" "$2" ;; remove|rm|del) shift || true [ -n "${1:-}" ] || die "🧭 用法:clashctl sub remove <名称>" remove_subscription "$1" regenerate_config apply_runtime_change_after_config_mutation print_sub_remove_feedback "$1" ;; health) shift || true cmd_health "$@" ;; "") ui_title "📡 订阅高级管理" echo "🧭 用法:" echo " clashctl sub list [--verbose]" echo " clashctl sub use <名称>" echo " clashctl sub enable <名称>" echo " clashctl sub disable <名称>" echo " clashctl sub rename <旧名称> <新名称>" echo " clashctl sub remove <名称>" echo " clashctl sub health [名称]" echo echo "🧩 说明:" echo " add / use / ls / health 属于主路径" echo " sub 仅用于高级维护操作" echo echo "💡 常用动作:" echo " clashctl sub list --verbose" echo " clashctl sub health" echo " clashctl sub enable <名称>" echo ui_next "clashctl ls" ui_blank ;; *) die_usage "未知的 sub 子命令:$1" "clashctl sub" ;; esac } proxy_select_interactive() { local group="${1:-}" local current idx count node selected_node local -a nodes prepare if ! status_is_running; then die_state "代理内核未运行" "clashon" fi if ! proxy_controller_reachable 2>/dev/null; then die_state "控制器不可访问" "clashctl doctor" fi if [ -z "${group:-}" ]; then ui_title "🚀 节点切换" group="$(proxy_pick_group_interactive)" || return 0 fi current="$(proxy_group_current "$group" 2>/dev/null || true)" while IFS= read -r node; do [ -n "${node:-}" ] || continue nodes+=("$node") done < <(proxy_group_nodes "$group") count="${#nodes[@]}" [ "$count" -gt 0 ] || die "📭 当前策略组没有候选节点:$group" echo echo "📦 当前策略组:$group" [ -n "${current:-}" ] && echo "🚀 当前节点:$current" echo echo "🚀 请选择节点:" idx=1 for node in "${nodes[@]}"; do if [ "$node" = "$current" ]; then printf ' %s) 🚀 %s [当前]\n' "$idx" "$node" else printf ' %s) 🚀 %s\n' "$idx" "$node" fi idx=$((idx + 1)) done echo " q) 退出" echo idx="$(proxy_pick_index "$count")" || return 0 selected_node="${nodes[$((idx - 1))]}" proxy_group_select "$group" "$selected_node" print_select_feedback "$group" } cmd_proxy() { prepare case "${1:-}" in show) print_proxy_show ;; on) print_proxy_on_script ;; off) print_proxy_off_script ;; groups) cmd_proxy_groups ;; current) shift || true cmd_proxy_current "$@" ;; nodes) shift || true cmd_proxy_nodes "$@" ;; select) shift || true if [ -n "${1:-}" ] && [ -n "${2:-}" ]; then proxy_group_select "$1" "$2" print_select_feedback "$1" else proxy_select_interactive "${1:-}" fi ;; "") ui_title "🌐 系统代理与策略组" echo "🧭 用法:" echo " clashctl proxy show" echo " clashctl proxy on" echo " clashctl proxy off" echo " clashctl proxy groups" echo " clashctl proxy current" echo " clashctl proxy current <策略组>" echo " clashctl proxy nodes <策略组>" echo " clashctl proxy select" echo " clashctl proxy select <策略组>" echo " clashctl proxy select <策略组> <节点>" echo echo "🧩 说明:" echo " on/off 输出当前 Shell 代理变量脚本" echo " groups 查看可切换策略组" echo " current 查看当前节点" echo " nodes 查看某策略组候选节点" echo " select 交互式或直接切换节点" echo echo "💡 常用动作:" echo " clashctl proxy show" echo " clashctl proxy groups" echo " clashctl proxy select" echo echo '🌐 注入当前 Shell:eval "$(clashctl proxy on)"' echo '🧹 清理当前 Shell:eval "$(clashctl proxy off)"' echo ui_next "clashctl select" ui_blank ;; *) die_usage "未知的 proxy 子命令:$1" "clashctl proxy" ;; esac } cmd_upgrade() { local verbose="false" local target_kernel="" prepare while [ $# -gt 0 ]; do case "$1" in mihomo|clash) target_kernel="$1" ;; -v|--verbose) verbose="true" ;; *) die_usage "upgrade 参数不合法" "clashctl upgrade [mihomo|clash] [-v|--verbose]" ;; esac shift done [ -n "${target_kernel:-}" ] || target_kernel="$(runtime_kernel_type)" ui_title "🚀 正在升级 ${target_kernel} 内核 ..." upgrade_runtime_kernel "$target_kernel" "$verbose" ui_title "🟢 内核升级完成" ui_kv "🚀" "当前内核" "$(runtime_kernel_type)" ui_kv "🧩" "影响范围" "仅更新代理内核,不更新项目脚本" ui_next "clashctl status" ui_blank } cmd_update() { local force_mode="false" local regenerate_mode="false" prepare while [ $# -gt 0 ]; do case "$1" in --force) force_mode="true" ;; --regenerate) regenerate_mode="true" ;; *) die_usage "update 参数不合法" "clashctl update [--force] [--regenerate]" ;; esac shift done ui_title "🔄 正在更新项目代码 ..." update_project_code "$force_mode" "$regenerate_mode" ui_title "🟢 项目代码已更新" ui_kv "🧩" "影响范围" "脚本、CLI、配置处理逻辑可能已变化" if [ "$regenerate_mode" = "true" ]; then ui_kv "🧩" "配置状态" "已重新生成" else ui_kv "⚠️" "配置状态" "未自动重新生成" fi ui_next "clashctl status" ui_blank } cmd_start_direct() { prepare start_runtime } cmd_stop_direct() { prepare stop_runtime } cmd_restart_direct() { prepare stop_runtime || true start_runtime } status_read_mixed_port() { runtime_config_mixed_port 2>/dev/null || true } status_read_controller() { runtime_config_controller_addr 2>/dev/null || true } cmd="${1:-}" shift || true case "$cmd" in add) cmd_add "$@" ;; use) cmd_use "$@" ;; ls) cmd_ls "$@" ;; health) cmd_health "$@" ;; select) cmd_select "$@" ;; on) cmd_on "$@" ;; off) cmd_off "$@" ;; status) cmd_status "$@" ;; status-next) cmd_status_next "$@" ;; logs) cmd_logs "$@" ;; doctor) cmd_doctor "$@" ;; ui) cmd_ui "$@" ;; secret) cmd_secret "$@" ;; tun) cmd_tun "$@" ;; dev) cmd_dev "$@" ;; config) cmd_config "$@" ;; mixin) cmd_mixin "$@" ;; profile) cmd_profile "$@" ;; sub) cmd_sub "$@" ;; proxy) cmd_proxy "$@" ;; upgrade) cmd_upgrade "$@" ;; update) cmd_update "$@" ;; start-direct) cmd_start_direct "$@" ;; stop-direct) cmd_stop_direct "$@" ;; restart-direct) cmd_restart_direct "$@" ;; -h|--help|"") usage ;; help) case "${1:-}" in advanced) usage_advanced ;; *) usage ;; esac ;; *) die "未知命令:$cmd" ;; esac