From 3a8d460de7625e3f59aeaa101727d7a79c7dcb64 Mon Sep 17 00:00:00 2001 From: Arvin <62139570+wnlen@users.noreply.github.com> Date: Thu, 9 Apr 2026 13:55:05 +0800 Subject: [PATCH] Add configurable mihomo download source overrides --- install.sh | 6 +- scripts/core/alias.sh | 49 +++++- scripts/core/clashctl.sh | 359 ++++++++++++++++++++++++++++++++++----- scripts/core/common.sh | 298 ++++++++++++++++++++++++++++---- scripts/core/config.sh | 283 +++++++++++++++--------------- scripts/core/runtime.sh | 18 +- scripts/core/update.sh | 5 +- uninstall.sh | 11 +- 8 files changed, 797 insertions(+), 232 deletions(-) diff --git a/install.sh b/install.sh index 5b70cce..c344e2b 100755 --- a/install.sh +++ b/install.sh @@ -17,6 +17,7 @@ detect_install_scope "${1:-auto}" init_layout ensure_required_commands +ensure_dashboard_deploy_prerequisites ui_download "正在准备安装依赖" resolve_runtime_kernel @@ -30,6 +31,9 @@ mark_install_port_plan || true install_clashctl_entry install_shell_alias_entry install_runtime_entry +install_local_dashboard_assets +ensure_controller_secret >/dev/null +set_shell_proxy_persist_enabled "false" ensure_subscription_bootstrap_for_install "default" prompt_subscription_if_needed @@ -56,4 +60,4 @@ if [ -n "$(subscription_url 2>/dev/null || true)" ]; then fi fi -print_install_summary \ No newline at end of file +print_install_summary diff --git a/scripts/core/alias.sh b/scripts/core/alias.sh index a6913dd..38bc083 100644 --- a/scripts/core/alias.sh +++ b/scripts/core/alias.sh @@ -9,6 +9,37 @@ _clashctl_real() { command clashctl "$@" } +_clash_alias_project_dir() { + local self_dir + self_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" + echo "$self_dir" +} + +_clash_alias_state_file() { + echo "$(_clash_alias_project_dir)/runtime/shell-proxy.env" +} + +_clash_alias_set_persist_enabled() { + local enabled="$1" + local state_file + state_file="$(_clash_alias_state_file)" + + mkdir -p "$(dirname "$state_file")" + cat > "$state_file" </dev/null 2>&1 || true + fi } clashctl() { @@ -195,4 +240,6 @@ clashmixin() { esac } -# shell 被 source 时不自动执行任何代理动作 \ No newline at end of file +# shell 被 source 后做轻量恢复: +# 若上次 clashon 持久化开启,则新终端自动恢复当前 shell 代理变量。 +_clash_alias_auto_restore_proxy diff --git a/scripts/core/clashctl.sh b/scripts/core/clashctl.sh index 51160a4..6fbee83 100644 --- a/scripts/core/clashctl.sh +++ b/scripts/core/clashctl.sh @@ -100,7 +100,7 @@ usage_advanced() { 💡 Notes: 当前编译链固定为 active-only - 只处理当前主订阅,不再提供 merge / policy / select 编译集合 + 只处理当前 active 主订阅 Tun 模式属于高级能力,开启前建议先执行:clashctl tun doctor EOF } @@ -111,6 +111,23 @@ prepare() { detect_install_scope auto } +ensure_add_use_prerequisites() { + if [ ! -x "$(yq_bin)" ]; then + die_state "依赖未就绪:缺少 yq($(yq_bin))" \ + "请先执行 bash install.sh,或运行 clashctl doctor 查看缺失项" + fi + + if [ ! -d "$RUNTIME_DIR" ]; then + die_state "运行环境未初始化:缺少 runtime 目录" \ + "请先执行 bash install.sh" + fi + + if [ ! -d "$CONFIG_DIR" ]; then + die_state "运行环境未初始化:缺少 config 目录" \ + "请先执行 bash install.sh" + fi +} + ensure_runtime_ports_ready() { local mixed_port controller_port dns_port local new_mixed_port="" new_controller_port="" new_dns_port="" @@ -190,10 +207,12 @@ ensure_on_path_ready() { } print_on_feedback() { - local mixed_port controller next_action + local mixed_port controller controller_lan controller_public next_action mixed_port="$(status_read_mixed_port 2>/dev/null || true)" controller="$(status_read_controller 2>/dev/null || true)" + controller_lan="$(status_read_controller_lan 2>/dev/null || true)" + controller_public="$(status_read_controller_public 2>/dev/null || true)" load_system_state next_action="$(system_state_default_action 2>/dev/null || echo 'clashctl status')" @@ -209,6 +228,12 @@ print_on_feedback() { if [ -n "${controller:-}" ] && [ "$controller" != "null" ]; then echo "🖥️ 控制台:http://${controller}/ui" + [ -n "${controller_lan:-}" ] && echo "🏠 局域网:http://${controller_lan}/ui" + if [ -n "${controller_public:-}" ]; then + echo "🌍 公网:http://${controller_public}/ui" + else + echo "🌍 公网:需公网 IP / 端口映射后可访问" + fi else echo "🖥️ 控制台:未知" fi @@ -243,11 +268,18 @@ cmd_off() { } ui_internal_url() { - local controller + local controller host port controller="$(status_read_controller 2>/dev/null || true)" [ -n "${controller:-}" ] && [ "$controller" != "null" ] || return 1 - echo "http://${controller}/ui" + host="${controller%:*}" + port="${controller##*:}" + + if [ "${host:-}" = "0.0.0.0" ]; then + host="127.0.0.1" + fi + + echo "http://${host}:${port}/ui" } ui_public_url() { @@ -276,6 +308,7 @@ cmd_ui() { local controller_addr="" local internal_url="" lan_url="" public_url="" public_fixed_url="" local current_secret="" controller_port="" controller_status="" + local dashboard_source="" dashboard_ready_text="" prepare runtime_config_exists || die "🧩 运行时配置不存在,请先生成配置" @@ -289,6 +322,16 @@ cmd_ui() { 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)" + dashboard_source="$(read_runtime_value "DASHBOARD_ASSET_SOURCE" 2>/dev/null || echo none)" + case "${dashboard_source:-none}" in + dir|zip|none) ;; + *) dashboard_source="none" ;; + esac + if runtime_dashboard_ready; then + dashboard_ready_text="有效" + else + dashboard_ready_text="无效" + fi if [ -z "${current_secret:-}" ] || [ "$current_secret" = "null" ]; then current_secret="未设置" @@ -313,6 +356,12 @@ cmd_ui() { "$current_secret" \ "$controller_port" + ui_kv "🧩" "Dashboard 来源" "$dashboard_source" + ui_kv "🧩" "Dashboard 部署" "$dashboard_ready_text" + if [ "$dashboard_ready_text" != "有效" ]; then + ui_warn "本地 Dashboard 部署无效,请先修复 assets 后重试 install/update" + fi + case "$controller_status" in 可访问) ui_next "浏览器打开上面的任一地址" @@ -340,27 +389,33 @@ cmd_ui() { esac } -status_build_mode() { - read_build_value "BUILD_MODE" 2>/dev/null || true -} - status_build_active_source() { read_build_value "BUILD_ACTIVE_SOURCE" 2>/dev/null || true } -status_build_selected_sources() { - read_build_value "BUILD_SELECTED_SOURCES" 2>/dev/null || true -} +status_build_active_sources() { + local value -status_build_included_sources() { + 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_min_success_sources() { - read_build_value "BUILD_MIN_SUCCESS_SOURCES" 2>/dev/null || true -} +status_build_failed_active_sources() { + local value -status_build_failed_sources() { + 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 } @@ -1664,9 +1719,9 @@ status_port_adjustment_brief() { } 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 profile mixed_port controller controller_lan controller_public + local running_text user_connectivity user_risk current_proxy_brief next_action shell_persist_text + local current_active dashboard_text dashboard_source_text dashboard_policy_text secret_text local tun_text profile="$(show_active_profile 2>/dev/null || true)" @@ -1674,6 +1729,8 @@ print_status_summary_compact() { mixed_port="$(status_read_mixed_port 2>/dev/null || true)" controller="$(status_read_controller 2>/dev/null || true)" + controller_lan="$(status_read_controller_lan 2>/dev/null || true)" + controller_public="$(status_read_controller_public 2>/dev/null || true)" current_active="$(active_subscription_name 2>/dev/null || true)" tun_text="$(status_tun_effective_text)" @@ -1687,6 +1744,31 @@ print_status_summary_compact() { 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')" + if shell_proxy_persist_enabled 2>/dev/null; then + shell_persist_text="开启" + else + shell_persist_text="关闭" + fi + if [ -f "$(runtime_dashboard_dir)/index.html" ]; then + dashboard_text="已部署" + else + dashboard_text="未部署" + fi + dashboard_source_text="$(read_runtime_value "DASHBOARD_ASSET_SOURCE" 2>/dev/null || echo none)" + case "${dashboard_source_text:-none}" in + dir|zip|none) ;; + *) dashboard_source_text="none" ;; + esac + if [ "$dashboard_source_text" = "none" ]; then + dashboard_policy_text="默认策略:Dashboard 资产无效将阻断 install/update" + else + dashboard_policy_text="默认策略:Dashboard 资产需保持可部署" + fi + if [ -n "$(read_env_value "CLASH_CONTROLLER_SECRET" 2>/dev/null || true)" ]; then + secret_text="已设置" + else + secret_text="未设置" + fi echo echo "😼 Clash 状态总览" @@ -1706,6 +1788,10 @@ print_status_summary_compact() { echo "⚙️ 运行后端:$(status_runtime_backend_text)" echo "🧪 环境模式:$(status_container_mode_text)" echo "🧪 Tun 状态:${tun_text:-未知}" + echo "🧭 新终端代理继承:${shell_persist_text}" + echo "🧩 Dashboard:${dashboard_text}(来源:${dashboard_source_text})" + echo "🧩 Dashboard 策略:${dashboard_policy_text}" + echo "🔐 控制器密钥:${secret_text}" if [ -n "${mixed_port:-}" ] && [ "$mixed_port" != "null" ]; then echo "🌐 本地代理:http://127.0.0.1:${mixed_port}" @@ -1715,6 +1801,12 @@ print_status_summary_compact() { if [ -n "${controller:-}" ] && [ "$controller" != "null" ]; then echo "🖥️ 控制台:http://${controller}/ui" + [ -n "${controller_lan:-}" ] && echo "🏠 局域网:http://${controller_lan}/ui" + if [ -n "${controller_public:-}" ]; then + echo "🌍 公网:http://${controller_public}/ui" + else + echo "🌍 公网:需公网 IP / 端口映射后可访问" + fi else echo "🖥️ 控制台:未知" fi @@ -1726,8 +1818,8 @@ print_status_summary_compact() { } print_status_summary_verbose() { - local running_text profile mixed_port controller - local build_mode current_active build_included build_failed build_status build_time + local running_text profile mixed_port controller controller_lan controller_public + 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 @@ -1736,18 +1828,20 @@ print_status_summary_verbose() { 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 + local shell_persist_text dashboard_text dashboard_source_text dashboard_policy_text secret_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)" + controller_lan="$(status_read_controller_lan 2>/dev/null || true)" + controller_public="$(status_read_controller_public 2>/dev/null || true)" - build_mode="$(status_build_mode)" current_active="$(active_subscription_name 2>/dev/null || true)" - build_included="$(status_build_included_sources)" - build_failed="$(status_build_failed_sources)" + 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)" @@ -1796,6 +1890,31 @@ print_status_summary_verbose() { 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)" + if shell_proxy_persist_enabled 2>/dev/null; then + shell_persist_text="开启" + else + shell_persist_text="关闭" + fi + if [ -f "$(runtime_dashboard_dir)/index.html" ]; then + dashboard_text="已部署" + else + dashboard_text="未部署" + fi + dashboard_source_text="$(read_runtime_value "DASHBOARD_ASSET_SOURCE" 2>/dev/null || echo none)" + case "${dashboard_source_text:-none}" in + dir|zip|none) ;; + *) dashboard_source_text="none" ;; + esac + if [ "$dashboard_source_text" = "none" ]; then + dashboard_policy_text="默认策略:Dashboard 资产无效将阻断 install/update" + else + dashboard_policy_text="默认策略:Dashboard 资产需保持可部署" + fi + if [ -n "$(read_env_value "CLASH_CONTROLLER_SECRET" 2>/dev/null || true)" ]; then + secret_text="已设置" + else + secret_text="未设置" + fi echo echo "😼 Clash 状态总览" @@ -1820,6 +1939,12 @@ print_status_summary_verbose() { if [ -n "${controller:-}" ] && [ "$controller" != "null" ]; then echo "🖥️ 控制台:http://${controller}/ui" + [ -n "${controller_lan:-}" ] && echo "🏠 局域网:http://${controller_lan}/ui" + if [ -n "${controller_public:-}" ]; then + echo "🌍 公网:http://${controller_public}/ui" + else + echo "🌍 公网:需公网 IP / 端口映射后可访问" + fi else echo "🖥️ 控制台:未知" fi @@ -1831,10 +1956,14 @@ print_status_summary_verbose() { echo "🧪 环境模式:${install_container_text:-unknown}" echo "🧩 安装验证:${install_verify_text:-unknown}" echo "🧭 端口裁决:${port_adjustment_text:-unknown}" + echo "🧭 新终端代理继承:${shell_persist_text}" + echo "🧩 Dashboard:${dashboard_text}(来源:${dashboard_source_text})" + echo "🧩 Dashboard 策略:${dashboard_policy_text}" + echo "🔐 控制器密钥:${secret_text}" echo if [ -n "$(install_plan_controller 2>/dev/null || true)" ]; then - echo "🖥️ 安装期控制器:$(install_plan_controller 2>/dev/null || true)" + echo "🖥️ 安装期控制器:$(display_controller_local_addr "$(install_plan_controller 2>/dev/null || true)" 2>/dev/null || install_plan_controller 2>/dev/null || true)" fi if [ -n "$(install_plan_mixed_port 2>/dev/null || true)" ]; then @@ -1876,8 +2005,8 @@ print_status_summary_verbose() { echo "🧩 最近编译:未知" fi - [ -n "${build_included:-}" ] && echo "🟢 实际参与编译:$build_included" - [ -n "${build_failed:-}" ] && echo "❌ 编译失败源:$build_failed" + [ -n "${build_active_sources:-}" ] && echo "🟢 实际参与编译:$build_active_sources" + [ -n "${build_failed_active_sources:-}" ] && echo "❌ 编译失败源:$build_failed_active_sources" echo echo "【当前订阅健康】" @@ -2109,6 +2238,16 @@ 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 "安装环境检查" @@ -2229,15 +2368,46 @@ doctor_container_tun() { } doctor_dependencies() { + local dashboard_source + 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)" + + dashboard_source="$(dashboard_asset_source)" + case "$dashboard_source" in + dir) + doctor_ok "Dashboard 来源:dir(当前部署不依赖 unzip)" + ;; + zip) + if command -v unzip >/dev/null 2>&1; then + if dashboard_archive_valid; then + doctor_ok "Dashboard 来源:zip(unzip 可用,压缩包可解压)" + else + doctor_fail "Dashboard 来源:zip(压缩包损坏或不可解压,将阻断 install/update)" + fi + else + doctor_fail "Dashboard 来源:zip(缺少 unzip,无法部署,将阻断 install/update)" + fi + ;; + *) + doctor_warn "Dashboard 来源:none(dist/ 与 dist.zip 均不可用,将阻断 install/update)" + ;; + esac + + if command -v openssl >/dev/null 2>&1; then + doctor_ok "Secret 生成:openssl 可用" + elif [ -r /dev/urandom ] && command -v od >/dev/null 2>&1 && command -v tr >/dev/null 2>&1 && command -v head >/dev/null 2>&1; then + doctor_ok "Secret 生成:fallback 可用(/dev/urandom + od/tr/head)" + else + doctor_fail "Secret 生成:缺少 openssl 且 fallback 不可用" + fi } doctor_config() { - local config_file active_profile mixed_port controller + local config_file active_profile mixed_port controller controller_secret_value external_ui_path dashboard_source doctor_print_title "配置检查" @@ -2254,6 +2424,16 @@ doctor_config() { [ -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)" @@ -2264,11 +2444,30 @@ doctor_config() { fi if [ -n "${controller:-}" ] && [ "$controller" != "null" ]; then - doctor_ok "控制器地址:$controller" + doctor_ok "控制器地址:$(display_controller_local_addr "$controller" 2>/dev/null || echo "$controller")" else doctor_warn "未解析到控制器地址" fi + controller_secret_value="$("$(yq_bin)" eval '.secret // ""' "$config_file" 2>/dev/null | head -n 1)" + if [ -n "${controller_secret_value:-}" ] && [ "$controller_secret_value" != "null" ]; then + doctor_ok "控制器密钥:已设置" + else + doctor_fail "控制器密钥:未设置" + fi + + external_ui_path="$("$(yq_bin)" eval '.["external-ui"] // ""' "$config_file" 2>/dev/null | head -n 1)" + dashboard_source="$(read_runtime_value "DASHBOARD_ASSET_SOURCE" 2>/dev/null || echo none)" + case "${dashboard_source:-none}" in + dir|zip|none) ;; + *) dashboard_source="none" ;; + esac + if [ -n "${external_ui_path:-}" ] && [ "$external_ui_path" != "null" ] && [ -f "${external_ui_path%/}/index.html" ]; then + doctor_ok "Dashboard 已接入:${external_ui_path}(来源:${dashboard_source})" + else + doctor_warn "Dashboard 未接入或目录无效(来源:${dashboard_source})" + fi + if test_runtime_config "$config_file" >/dev/null 2>&1; then doctor_ok "配置校验通过" else @@ -2287,6 +2486,13 @@ doctor_subscription() { 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)" @@ -2303,7 +2509,6 @@ doctor_subscription() { )" auto_disabled_count="$(printf '%s\n' "${auto_disabled_count:-}" | awk 'NF{c++} END{print c+0}')" - doctor_ok "订阅策略:active-only" [ -n "${active:-}" ] && doctor_ok "当前主订阅:$active" || doctor_warn "当前主订阅为空" doctor_ok "订阅源总数:$total" @@ -2325,15 +2530,15 @@ doctor_subscription() { } doctor_build() { - local active included failed last_status last_time + 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)" - included="$(read_build_value "BUILD_INCLUDED_SOURCES" 2>/dev/null || true)" - failed="$(read_build_value "BUILD_FAILED_SOURCES" 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)" @@ -2342,7 +2547,7 @@ doctor_build() { 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:-}${included:-}${failed:-}${last_status:-}${last_time:-}${error_summary:-}${error_stage:-}${block_reason:-}" ]; then + 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 @@ -2355,14 +2560,14 @@ doctor_build() { doctor_warn "当前主订阅为空" fi - if [ -n "${included:-}" ]; then - doctor_ok "实际参与编译:$included" + if [ -n "${active_sources:-}" ]; then + doctor_ok "实际参与编译:$active_sources" else doctor_warn "没有任何订阅参与编译" fi - if [ -n "${failed:-}" ]; then - doctor_warn "失败订阅源:$failed" + if [ -n "${failed_active_sources:-}" ]; then + doctor_warn "失败订阅源:$failed_active_sources" else doctor_ok "失败订阅源:无" fi @@ -2462,6 +2667,18 @@ doctor_ports() { 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##*:}" @@ -2529,6 +2746,7 @@ doctor_install_ports() { doctor_runtime_events() { local fallback_used fallback_time fallback_reason risk_level local config_source build_applied build_applied_time build_applied_reason + local shell_persist_text dashboard_status dashboard_source secret_status doctor_print_title "运行事件检查" @@ -2540,9 +2758,32 @@ doctor_runtime_events() { 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 shell_proxy_persist_enabled 2>/dev/null; then + shell_persist_text="开启" + else + shell_persist_text="关闭" + fi + if [ -f "$(runtime_dashboard_dir)/index.html" ]; then + dashboard_status="已部署" + else + dashboard_status="未部署" + fi + dashboard_source="$(read_runtime_value "DASHBOARD_ASSET_SOURCE" 2>/dev/null || echo none)" + case "${dashboard_source:-none}" in + dir|zip|none) ;; + *) dashboard_source="none" ;; + esac + if [ -n "$(read_env_value "CLASH_CONTROLLER_SECRET" 2>/dev/null || true)" ]; then + secret_status="已设置" + else + secret_status="未设置" + fi doctor_ok "当前风险等级:${risk_level:-unknown}" doctor_ok "当前配置来源:${config_source:-unknown}" + doctor_ok "新终端代理继承:${shell_persist_text}" + doctor_ok "Dashboard 运行目录:${dashboard_status}(来源:${dashboard_source})" + doctor_ok ".env 控制器密钥:${secret_status}" case "${build_applied:-}" in true) @@ -2648,7 +2889,7 @@ doctor_controller() { return 0 fi - doctor_ok "控制器地址:$controller" + doctor_ok "控制器地址:$(display_controller_local_addr "$controller" 2>/dev/null || echo "$controller")" if ! status_is_running; then doctor_warn "内核未运行,无法检查控制器 API" @@ -2798,7 +3039,7 @@ cmd_config() { echo echo "🧩 说明:" echo " 当前编译链固定为 active-only" - echo " 只处理当前主订阅,不再提供 merge / policy / select 编译集合" + echo " 只处理当前 active 主订阅" echo ui_next "clashctl config show" ui_blank @@ -3226,7 +3467,7 @@ doctor_evidence_lines() { fi if [ -n "${controller:-}" ] && [ "$controller" != "null" ]; then - echo "🔍 控制器地址:${controller}" + echo "🔍 控制器地址:$(display_controller_local_addr "$controller" 2>/dev/null || echo "$controller")" fi } @@ -3240,6 +3481,8 @@ set_controller_secret() { SECRET_VALUE="$secret" "$(yq_bin)" eval -i ' .secret = strenv(SECRET_VALUE) ' "$file" + + write_env_value "CLASH_CONTROLLER_SECRET" "$secret" } cmd_secret() { @@ -3841,6 +4084,7 @@ cmd_add() { local sub_name prepare + ensure_add_use_prerequisites [ -n "${1:-}" ] || die "用法:clashctl add [clash|convert] [name]" @@ -3855,6 +4099,7 @@ cmd_use() { local recommended active prepare + ensure_add_use_prerequisites case "${1:-}" in --recommend|-r) @@ -4641,10 +4886,38 @@ status_read_mixed_port() { runtime_config_mixed_port 2>/dev/null || true } -status_read_controller() { +status_read_controller_raw() { runtime_config_controller_addr 2>/dev/null || true } +status_read_controller() { + local controller + controller="$(status_read_controller_raw 2>/dev/null || true)" + display_controller_local_addr "$controller" 2>/dev/null || echo "$controller" +} + +status_read_controller_lan() { + local controller lan_ip port + controller="$(status_read_controller_raw 2>/dev/null || true)" + [ -n "${controller:-}" ] && [ "$controller" != "null" ] || return 1 + + port="${controller##*:}" + lan_ip="$(ui_lan_ip 2>/dev/null || true)" + [ -n "${lan_ip:-}" ] || return 1 + echo "${lan_ip}:${port}" +} + +status_read_controller_public() { + local controller public_ip port + controller="$(status_read_controller_raw 2>/dev/null || true)" + [ -n "${controller:-}" ] && [ "$controller" != "null" ] || return 1 + + port="${controller##*:}" + public_ip="$(ui_public_ip 2>/dev/null || true)" + [ -n "${public_ip:-}" ] || return 1 + echo "${public_ip}:${port}" +} + cmd="${1:-}" shift || true @@ -4682,4 +4955,4 @@ case "$cmd" in esac ;; *) die "未知命令:$cmd" ;; -esac \ No newline at end of file +esac diff --git a/scripts/core/common.sh b/scripts/core/common.sh index 8a2abd4..9daff6f 100644 --- a/scripts/core/common.sh +++ b/scripts/core/common.sh @@ -136,7 +136,7 @@ install_arch_text() { } install_state_text() { - local has_subscription install_ready runtime_ready controller_ready + local has_subscription install_ready runtime_ready controller_ready build_status if install_has_subscription; then has_subscription="true" @@ -147,35 +147,43 @@ install_state_text() { install_ready="$(read_runtime_event_value "RUNTIME_LAST_INSTALL_READY" 2>/dev/null || true)" runtime_ready="$(install_verify_runtime_ready 2>/dev/null || true)" controller_ready="$(install_verify_controller_ready 2>/dev/null || true)" - - if [ "${install_ready:-false}" = "true" ]; then - echo "ready" - return 0 - fi + build_status="$(read_build_value "BUILD_LAST_STATUS" 2>/dev/null || true)" if [ "${has_subscription:-false}" != "true" ]; then echo "stopped" return 0 fi - if [ "${runtime_ready:-false}" = "true" ] || [ "${controller_ready:-false}" = "true" ]; then - echo "degraded" + if [ "${build_status:-}" = "failed" ]; then + echo "broken" return 0 fi - echo "degraded" + if [ "${install_ready:-false}" = "true" ] \ + || { [ "${runtime_ready:-false}" = "true" ] && [ "${controller_ready:-false}" = "true" ]; }; then + echo "ready" + return 0 + fi + + echo "verifying" } install_build_result_text() { - local command_ready config_ready + local command_ready config_ready build_status command_ready="$(install_verify_command_ready 2>/dev/null || true)" config_ready="$(install_verify_config_ready 2>/dev/null || true)" + build_status="$(read_build_value "BUILD_LAST_STATUS" 2>/dev/null || true)" - if [ "${command_ready:-false}" = "true" ] && [ "${config_ready:-false}" = "true" ]; then + if [ "${build_status:-}" = "failed" ]; then + echo "failed" + return 0 + fi + + if [ "${command_ready:-false}" = "true" ] && { [ "${config_ready:-false}" = "true" ] || ! install_has_subscription; }; then echo "success" else - echo "failed" + echo "pending" fi } @@ -184,7 +192,14 @@ install_next_step_text() { ready) echo "clashctl select" ;; - degraded) + verifying) + if install_has_subscription; then + echo "clashon" + else + echo "clashctl add <订阅链接>" + fi + ;; + broken) echo "clashctl doctor" ;; stopped) @@ -219,6 +234,9 @@ init_project_context() { } load_env_if_exists() { + local env_file + env_file="$PROJECT_DIR/.env" + if [ -f "$PROJECT_DIR/.env" ]; then set -a # shellcheck disable=SC1090 @@ -227,6 +245,7 @@ load_env_if_exists() { fi normalize_env_compat + cleanup_env_legacy_compat_fields "$env_file" } normalize_env_compat() { @@ -262,9 +281,33 @@ normalize_env_compat() { CLASH_SUBSCRIPTION_UA="$CLASH_SUB_UA" fi + # 旧默认值迁移:公网默认监听 + if [ "${EXTERNAL_CONTROLLER:-}" = "127.0.0.1:9090" ]; then + EXTERNAL_CONTROLLER="0.0.0.0:9090" + fi + + # 已废弃:active-only 主链不再消费该字段 + unset BUILD_MIN_SUCCESS_SOURCES 2>/dev/null || true + return 0 } +cleanup_env_legacy_compat_fields() { + local file="$1" + + [ -n "${file:-}" ] || return 0 + [ -f "$file" ] || return 0 + + awk ' + $0 ~ /^[[:space:]]*(export[[:space:]]+)?BUILD_MIN_SUCCESS_SOURCES=/ { next } + $0 ~ /^[[:space:]]*(export[[:space:]]+)?EXTERNAL_CONTROLLER="?127\.0\.0\.1:9090"?$/ { + print "export EXTERNAL_CONTROLLER=\"0.0.0.0:9090\"" + next + } + { print } + ' "$file" > "${file}.tmp" && mv "${file}.tmp" "$file" +} + github_proxy_prefix() { local prefix="${CLASH_GH_PROXY:-}" prefix="${prefix%/}" @@ -721,7 +764,8 @@ download_file() { fi rm -f "$fetch_tmp" 2>/dev/null || true - die "下载失败:${asset_name}" + die_state "下载失败:${asset_name}" \ + "请检查网络连通性,或在 .env 中配置下载源后重试;也可先执行 clashctl doctor" fi ordered_entries="$( @@ -781,7 +825,8 @@ EOF done rm -f "$fetch_tmp" 2>/dev/null || true - die "下载失败:${asset_name}" + die_state "下载失败:${asset_name}" \ + "请检查网络连通性,或在 .env 中配置下载源后重试;也可先执行 clashctl doctor" } download_text_tmp_file() { @@ -895,6 +940,7 @@ init_layout() { "$PROJECT_DIR/scripts/core" \ "$PROJECT_DIR/scripts/init" \ "$RUNTIME_DIR" \ + "$RUNTIME_DIR/dashboard" \ "$BIN_DIR" \ "$LOG_DIR" @@ -908,6 +954,150 @@ ensure_required_commands() { command -v readlink >/dev/null 2>&1 || die "当前系统缺少 readlink" } +dashboard_archive_file() { + echo "$RESOURCE_DIR/dashboard/dist.zip" +} + +dashboard_dir_file() { + echo "$RESOURCE_DIR/dashboard/dist" +} + +runtime_dashboard_dir() { + echo "$RUNTIME_DIR/dashboard" +} + +runtime_dashboard_ready() { + [ -f "$(runtime_dashboard_dir)/index.html" ] +} + +dashboard_archive_valid() { + local archive + archive="$(dashboard_archive_file)" + [ -f "$archive" ] || return 1 + unzip -tq "$archive" >/dev/null 2>&1 +} + +dashboard_dir_valid() { + local dir + dir="$(dashboard_dir_file)" + [ -d "$dir" ] || return 1 + [ -f "$dir/index.html" ] +} + +dashboard_asset_source() { + local archive + archive="$(dashboard_archive_file)" + + if dashboard_dir_valid; then + echo "dir" + return 0 + fi + + if [ -f "$archive" ]; then + echo "zip" + return 0 + fi + + echo "none" +} + +ensure_dashboard_deploy_prerequisites() { + local archive source_type + archive="$(dashboard_archive_file)" + source_type="$(dashboard_asset_source)" + + case "$source_type" in + dir) + return 0 + ;; + zip) + command -v unzip >/dev/null 2>&1 || die_state \ + "检测到 Dashboard 仅可从 dist.zip 部署,但系统缺少 unzip" \ + "请安装 unzip,或提供 resources/dashboard/dist/index.html(默认策略:本地 Dashboard 资产无效将阻断 install/update)" + dashboard_archive_valid || die_state \ + "Dashboard 压缩包不可用:$archive" \ + "请修复 dist.zip,或提供 resources/dashboard/dist/index.html(默认策略:本地 Dashboard 资产无效将阻断 install/update)" + return 0 + ;; + *) + die_state "本地 Dashboard 资产不可用(dist/ 与 dist.zip 均无效)" \ + "请提供 resources/dashboard/dist/index.html 或可解压的 resources/dashboard/dist.zip(默认策略:本地 Dashboard 资产无效将阻断 install/update)" + ;; + esac +} + +shell_proxy_persist_state_file() { + echo "$RUNTIME_DIR/shell-proxy.env" +} + +shell_proxy_persist_enabled() { + local file enabled + file="$(shell_proxy_persist_state_file)" + [ -f "$file" ] || return 1 + + enabled="$(sed -nE 's/^SHELL_PROXY_PERSIST_ENABLED=\"?([^\"\r\n]+)\"?$/\1/p' "$file" | head -n 1)" + [ "${enabled:-false}" = "true" ] +} + +set_shell_proxy_persist_enabled() { + local enabled="${1:-false}" + local file + file="$(shell_proxy_persist_state_file)" + + mkdir -p "$(dirname "$file")" + cat > "$file" </dev/null || true +} + +install_local_dashboard_assets() { + local archive source_dir target source_type + archive="$(dashboard_archive_file)" + source_dir="$(dashboard_dir_file)" + target="$(runtime_dashboard_dir)" + source_type="$(dashboard_asset_source)" + + rm -rf "$target" 2>/dev/null || true + mkdir -p "$target" + + case "$source_type" in + dir) + cp -a "$source_dir"/. "$target"/ 2>/dev/null || { + write_runtime_value "DASHBOARD_ASSET_SOURCE" "dir" + write_runtime_value "DASHBOARD_DEPLOY_READY" "false" + die "复制 Dashboard 目录失败:$source_dir" + } + ;; + zip) + unzip -oq "$archive" -d "$target" || { + write_runtime_value "DASHBOARD_ASSET_SOURCE" "zip" + write_runtime_value "DASHBOARD_DEPLOY_READY" "false" + die "解压 Dashboard 失败:$archive" + } + ;; + *) + write_runtime_value "DASHBOARD_ASSET_SOURCE" "none" + write_runtime_value "DASHBOARD_DEPLOY_READY" "false" + die_state "本地 Dashboard 资产不可用(dist/ 与 dist.zip 均无效)" \ + "请提供 resources/dashboard/dist/index.html 或可解压的 resources/dashboard/dist.zip" + ;; + esac + + if ! runtime_dashboard_ready; then + write_runtime_value "DASHBOARD_ASSET_SOURCE" "$source_type" + write_runtime_value "DASHBOARD_DEPLOY_READY" "false" + die "Dashboard 部署不完整:缺少 $target/index.html" + fi + + write_runtime_value "DASHBOARD_ASSET_SOURCE" "$source_type" + write_runtime_value "DASHBOARD_DEPLOY_READY" "true" +} + get_os() { local os os="$(uname -s | tr '[:upper:]' '[:lower:]')" @@ -962,6 +1152,17 @@ read_env_value() { sed -nE "s/^[[:space:]]*(export[[:space:]]+)?${key}=['\"]?([^'\"]*)['\"]?$/\2/p" "$file" | head -n 1 } +unset_env_value() { + local key="$1" + local file="$PROJECT_DIR/.env" + [ -f "$file" ] || return 0 + + awk -v k="$key" ' + $0 ~ "^[[:space:]]*(export[[:space:]]+)?" k "=" { next } + { print } + ' "$file" > "${file}.tmp" && mv "${file}.tmp" "$file" +} + subscription_auto_update_enabled() { case "${CLASH_AUTO_UPDATE_SUBSCRIPTIONS:-true}" in true|1|yes|on) @@ -1287,6 +1488,24 @@ runtime_config_controller_port() { echo "${addr##*:}" } +display_controller_local_addr() { + local controller="$1" + local host port + + [ -n "${controller:-}" ] || return 1 + [ "$controller" != "null" ] || return 1 + printf '%s' "$controller" | grep -q ':' || return 1 + + host="${controller%:*}" + port="${controller##*:}" + + if [ "$host" = "0.0.0.0" ]; then + host="127.0.0.1" + fi + + echo "${host}:${port}" +} + runtime_config_dns_listen() { local file file="$(runtime_config_file)" @@ -2175,7 +2394,7 @@ remove_alias_command_wrappers() { } install_status_text() { - local has_subscription install_ready runtime_ready controller_ready + local has_subscription install_ready runtime_ready controller_ready build_status local live_runtime="false" local live_controller="false" @@ -2188,6 +2407,7 @@ install_status_text() { install_ready="$(read_runtime_event_value "RUNTIME_LAST_INSTALL_READY" 2>/dev/null || true)" runtime_ready="$(install_verify_runtime_ready 2>/dev/null || true)" controller_ready="$(install_verify_controller_ready 2>/dev/null || true)" + build_status="$(read_build_value "BUILD_LAST_STATUS" 2>/dev/null || true)" if status_is_running 2>/dev/null; then live_runtime="true" @@ -2202,6 +2422,11 @@ install_status_text() { return 0 fi + if [ "${build_status:-}" = "failed" ]; then + echo "broken" + return 0 + fi + if [ "${live_runtime:-false}" = "true" ] && [ "${live_controller:-false}" = "true" ]; then echo "ready" return 0 @@ -2216,18 +2441,18 @@ install_status_text() { if [ "${live_runtime:-false}" = "true" ] \ || [ "${runtime_ready:-false}" = "true" ] \ || [ "${controller_ready:-false}" = "true" ]; then - echo "degraded" + echo "verifying" return 0 fi - echo "degraded" + echo "verifying" } install_status_label() { case "$(install_status_text)" in ready) echo "ready" ;; stopped) echo "stopped" ;; - degraded) echo "degraded" ;; + verifying) echo "verifying" ;; broken) echo "broken" ;; *) echo "unknown" ;; esac @@ -2245,7 +2470,14 @@ install_default_next_action() { echo "clashctl add <订阅链接>" fi ;; - degraded|broken) + verifying) + if status_is_running 2>/dev/null; then + echo "clashctl status" + else + echo "clashon" + fi + ;; + broken) if install_has_subscription; then echo "clashctl doctor" else @@ -2259,7 +2491,7 @@ install_default_next_action() { } install_runtime_brief_line() { - local status_text mixed_port controller + local status_text mixed_port controller controller_display status_text="$(install_status_text)" mixed_port="$(install_plan_mixed_port 2>/dev/null || true)" @@ -2267,23 +2499,25 @@ install_runtime_brief_line() { controller="$(install_plan_controller 2>/dev/null || true)" [ -n "${controller:-}" ] || controller="$(read_env_value "EXTERNAL_CONTROLLER" 2>/dev/null || echo "127.0.0.1:9090")" + controller_display="$(display_controller_local_addr "$controller" 2>/dev/null || true)" case "$status_text" in ready) echo "🟢 当前状态:ready" echo "🌐 本地代理:http://127.0.0.1:${mixed_port}" - echo "🖥️ 控制台:http://${controller}/ui" + echo "🖥️ 控制台:http://${controller_display:-$controller}/ui" ;; stopped) echo "🔴 当前状态:stopped" ;; - degraded) - echo "🟡 当前状态:degraded" + verifying) + echo "🟡 当前状态:verifying" + echo "🧭 正在确认运行状态,可继续观察或手动启动" if [ -n "${mixed_port:-}" ]; then echo "🌐 本地代理:http://127.0.0.1:${mixed_port}" fi if [ -n "${controller:-}" ]; then - echo "🖥️ 控制台:http://${controller}/ui" + echo "🖥️ 控制台:http://${controller_display:-$controller}/ui" fi ;; broken) @@ -2296,7 +2530,7 @@ install_runtime_brief_line() { } print_install_summary() { - local backend mixed_port controller + local backend mixed_port controller controller_display local has_subscription final_state next_action backend="$(install_plan_backend 2>/dev/null || true)" @@ -2307,6 +2541,7 @@ print_install_summary() { controller="$(install_plan_controller 2>/dev/null || true)" [ -n "${controller:-}" ] || controller="$(read_env_value "EXTERNAL_CONTROLLER" 2>/dev/null || echo "127.0.0.1:9090")" + controller_display="$(display_controller_local_addr "$controller" 2>/dev/null || true)" if install_has_subscription; then has_subscription="true" @@ -2327,18 +2562,19 @@ print_install_summary() { ready) echo "🚦 当前状态:🟢 ready" echo "🌐 本地代理:http://127.0.0.1:${mixed_port}" - echo "🖥️ 控制台:http://${controller}/ui" + echo "🖥️ 控制台:http://${controller_display:-$controller}/ui" ;; stopped) echo "🚦 当前状态:⚪ stopped" ;; - degraded) - echo "🚦 当前状态:🟡 degraded" + verifying) + echo "🚦 当前状态:🟡 verifying" + echo "🧭 安装已完成,运行状态仍在确认中" if [ -n "${mixed_port:-}" ]; then echo "🌐 本地代理:http://127.0.0.1:${mixed_port}" fi if [ -n "${controller:-}" ]; then - echo "🖥️ 控制台:http://${controller}/ui" + echo "🖥️ 控制台:http://${controller_display:-$controller}/ui" fi ;; broken) @@ -2351,4 +2587,4 @@ print_install_summary() { echo "👉 下一步:${next_action}" echo -} \ No newline at end of file +} diff --git a/scripts/core/config.sh b/scripts/core/config.sh index 6229f1d..4132700 100644 --- a/scripts/core/config.sh +++ b/scripts/core/config.sh @@ -60,18 +60,39 @@ subscriptions_file() { echo "$CONFIG_DIR/subscriptions.yaml" } +migrate_subscriptions_legacy_fields() { + local file="$1" + [ -f "$file" ] || return 0 + + if [ -x "$(yq_bin 2>/dev/null || true)" ]; then + if "$(yq_bin)" eval '(.mode != null) or (.selected != null) or (.policy != null)' "$file" 2>/dev/null | grep -qx 'true'; then + "$(yq_bin)" eval -i 'del(.mode) | del(.selected) | del(.policy)' "$file" 2>/dev/null || true + fi + return 0 + fi + + # 无 yq 时做最小兜底:仅移除顶层旧字段,避免旧语义继续扩散 + awk ' + $0 ~ /^[[:space:]]*mode:[[:space:]]*/ { next } + $0 ~ /^[[:space:]]*selected:[[:space:]]*/ { next } + $0 ~ /^[[:space:]]*policy:[[:space:]]*/ { next } + { print } + ' "$file" > "${file}.tmp" && mv "${file}.tmp" "$file" +} + ensure_subscriptions_file() { local file file="$(subscriptions_file)" - [ -f "$file" ] && return 0 + if [ -f "$file" ]; then + migrate_subscriptions_legacy_fields "$file" + return 0 + fi mkdir -p "$CONFIG_DIR" cat > "$file" <<'EOF' -mode: single active: default -selected: [] sources: default: @@ -79,10 +100,8 @@ sources: url: "" enabled: true EOF -} -build_min_success_sources() { - echo "1" + migrate_subscriptions_legacy_fields "$file" } csv_count() { @@ -163,7 +182,7 @@ mixed-port: 7890 allow-lan: true mode: rule log-level: info -external-controller: 127.0.0.1:9090 +external-controller: 0.0.0.0:9090 secret: "" tun: @@ -412,7 +431,8 @@ clear_subscription_cache() { normalize_runtime_config() { local file="$1" - local mixed_port controller tun_enable_value tun_stack_value dns_port_value + local mixed_port controller tun_enable_value tun_stack_value dns_port_value controller_secret_value + local dashboard_dir_value local resolved_ports [ -s "$file" ] || die "待规范化的配置文件不存在:$file" @@ -425,15 +445,22 @@ normalize_runtime_config() { tun_enable_value="$(tun_enabled)" tun_stack_value="$(tun_stack)" dns_port_value="$CLASH_DNS_PORT_RESOLVED" + controller_secret_value="$(ensure_controller_secret)" + dashboard_dir_value="$(runtime_dashboard_dir)" mixed_port="$mixed_port" \ controller="$controller" \ tun_enable_value="$tun_enable_value" \ tun_stack_value="$tun_stack_value" \ + controller_secret_value="$controller_secret_value" \ + dashboard_dir_value="$dashboard_dir_value" \ dns_listen_value="0.0.0.0:${dns_port_value}" \ "$(yq_bin)" eval -i ' .["mixed-port"] = (env(mixed_port) | tonumber) | .["external-controller"] = env(controller) | + .secret = env(controller_secret_value) | + .["external-ui"] = env(dashboard_dir_value) | + .["external-ui-url"] = "/ui" | .["allow-lan"] = (.["allow-lan"] // true) | .mode = (.mode // "rule") | .["log-level"] = (.["log-level"] // "info") | @@ -453,6 +480,49 @@ normalize_runtime_config() { ' "$file" } +generate_secure_secret() { + if command -v openssl >/dev/null 2>&1; then + openssl rand -hex 24 2>/dev/null | head -n 1 + return 0 + fi + + head -c 32 /dev/urandom 2>/dev/null | od -An -tx1 | tr -d ' \n' +} + +is_valid_controller_secret() { + local value="${1:-}" + case "${value}" in + ""|null|NULL|undefined|UNDEFINED|\"\"|\'\') + return 1 + ;; + *) + return 0 + ;; + esac +} + +ensure_controller_secret() { + local value + + value="${CLASH_CONTROLLER_SECRET:-}" + if ! is_valid_controller_secret "$value"; then + value="$(read_env_value "CLASH_CONTROLLER_SECRET" 2>/dev/null || true)" + fi + + if ! is_valid_controller_secret "$value"; then + value="$(generate_secure_secret)" + [ -n "${value:-}" ] || die "无法生成控制器密钥" + write_env_value "CLASH_CONTROLLER_SECRET" "$value" + fi + + echo "$value" +} + +clear_controller_secret() { + unset_env_value "CLASH_CONTROLLER_SECRET" || true + unset CLASH_CONTROLLER_SECRET || true +} + first_available_proxy_name() { local file="$1" @@ -490,19 +560,15 @@ record_build_failure() { local selected="$4" local included="$5" local failed="$6" - local min_required - min_required="$(build_min_success_sources)" - - write_build_value "BUILD_MODE" "$mode" - write_build_value "BUILD_POLICY" "$policy" + # active-only 元数据:只记录当前实际参与编译的 active 订阅集合 + # 为避免旧状态文件混淆,同时清空旧 key write_build_value "BUILD_ACTIVE_SOURCE" "$active" - write_build_value "BUILD_SELECTED_SOURCES" "$selected" - write_build_value "BUILD_INCLUDED_SOURCES" "$included" - write_build_value "BUILD_FAILED_SOURCES" "$failed" - write_build_value "BUILD_SOURCE_COUNT" "$(csv_count "$selected")" - write_build_value "BUILD_INCLUDED_COUNT" "$(csv_count "$included")" - write_build_value "BUILD_MIN_SUCCESS_SOURCES" "$min_required" + write_build_value "BUILD_ACTIVE_SOURCES" "$included" + write_build_value "BUILD_FAILED_ACTIVE_SOURCES" "$failed" + write_build_value "BUILD_SELECTED_SOURCES" "" + write_build_value "BUILD_INCLUDED_SOURCES" "" + write_build_value "BUILD_FAILED_SOURCES" "" write_build_value "BUILD_LAST_STATUS" "failed" write_build_value "BUILD_LAST_TIME" "$(now_datetime)" } @@ -514,19 +580,13 @@ record_build_success() { local selected="$4" local included="$5" local failed="$6" - local min_required - min_required="$(build_min_success_sources)" - - write_build_value "BUILD_MODE" "$mode" - write_build_value "BUILD_POLICY" "$policy" write_build_value "BUILD_ACTIVE_SOURCE" "$active" - write_build_value "BUILD_SELECTED_SOURCES" "$selected" - write_build_value "BUILD_INCLUDED_SOURCES" "$included" - write_build_value "BUILD_FAILED_SOURCES" "$failed" - write_build_value "BUILD_SOURCE_COUNT" "$(csv_count "$selected")" - write_build_value "BUILD_INCLUDED_COUNT" "$(csv_count "$included")" - write_build_value "BUILD_MIN_SUCCESS_SOURCES" "$min_required" + write_build_value "BUILD_ACTIVE_SOURCES" "$included" + write_build_value "BUILD_FAILED_ACTIVE_SOURCES" "$failed" + write_build_value "BUILD_SELECTED_SOURCES" "" + write_build_value "BUILD_INCLUDED_SOURCES" "" + write_build_value "BUILD_FAILED_SOURCES" "" write_build_value "BUILD_LAST_STATUS" "success" write_build_value "BUILD_LAST_TIME" "$(now_datetime)" clear_build_error_detail @@ -937,7 +997,7 @@ print_build_explain() { error_detail="$(status_build_last_error_detail 2>/dev/null || true)" ui_title "🧩 编译说明" - echo "- 当前编译链只处理 active 主订阅,不再 merge 多订阅" + echo "- 当前编译链只处理 active 主订阅" echo "- 当前主订阅:${active:-未设置}" case "${last_status:-unknown}" in @@ -962,16 +1022,30 @@ print_build_explain() { ui_blank } -build_selected_sources_csv() { - read_build_value "BUILD_SELECTED_SOURCES" 2>/dev/null || true +build_active_sources_csv() { + local value + value="$(read_build_value "BUILD_ACTIVE_SOURCES" 2>/dev/null || true)" + if [ -n "${value:-}" ]; then + echo "$value" + return 0 + fi + + # 兼容历史 build.env + value="$(read_build_value "BUILD_INCLUDED_SOURCES" 2>/dev/null || true)" + [ -n "${value:-}" ] && echo "$value" } -build_included_sources_csv() { - read_build_value "BUILD_INCLUDED_SOURCES" 2>/dev/null || true -} +build_failed_active_sources_csv() { + local value + value="$(read_build_value "BUILD_FAILED_ACTIVE_SOURCES" 2>/dev/null || true)" + if [ -n "${value:-}" ]; then + echo "$value" + return 0 + fi -build_failed_sources_csv() { - read_build_value "BUILD_FAILED_SOURCES" 2>/dev/null || true + # 兼容历史 build.env + value="$(read_build_value "BUILD_FAILED_SOURCES" 2>/dev/null || true)" + [ -n "${value:-}" ] && echo "$value" } build_last_status() { @@ -995,66 +1069,36 @@ build_last_error_detail() { } build_success_explanation_lines() { - local mode policy included_csv failed_csv included_count min_required + local included_csv included_count - mode="$(subscription_mode 2>/dev/null || true)" - policy="$(subscription_policy 2>/dev/null || true)" - included_csv="$(build_included_sources_csv)" - failed_csv="$(build_failed_sources_csv)" + included_csv="$(build_active_sources_csv)" included_count="$(csv_count "$included_csv")" - min_required="$(build_min_success_sources)" - case "$mode" in - single) - echo "- single 模式下仅当前主订阅参与编译" - ;; - esac - - echo "- 实际成功源数量 ${included_count},达到最少成功源要求 ${min_required}" + echo "- active-only 编译链仅处理当前主订阅" + echo "- 当前成功源数量:${included_count}" } build_failure_explanation_lines() { - local mode policy included_csv failed_csv included_count min_required error_stage + local included_csv included_count error_stage - mode="$(subscription_mode 2>/dev/null || true)" - policy="$(subscription_policy 2>/dev/null || true)" - included_csv="$(build_included_sources_csv)" - failed_csv="$(build_failed_sources_csv)" + included_csv="$(build_active_sources_csv)" + failed_csv="$(build_failed_active_sources_csv)" included_count="$(csv_count "$included_csv")" - min_required="$(build_min_success_sources)" error_stage="$(build_last_error_stage)" - case "$mode" in - single) - echo "- single 模式下仅当前主订阅参与编译" - ;; - merge) - echo "- merge 模式下 selected 集合参与编译" - ;; - esac - - case "$policy" in - strict) - if [ -n "${failed_csv:-}" ]; then - echo "- strict 策略下任一订阅失败都会导致整体失败" - fi - ;; - tolerant) - if [ "$error_stage" = "min-success-threshold" ]; then - echo "- tolerant 虽允许跳过失败源,但仍必须满足最少成功源要求" - elif [ -n "${failed_csv:-}" ]; then - echo "- tolerant 已跳过失败源,但后续校验仍未通过" - fi - ;; - esac - - echo "- 实际成功源数量 ${included_count},最少成功源要求 ${min_required}" + echo "- active-only 编译链仅处理当前主订阅" + if [ "$error_stage" = "resolve-active-source" ]; then + echo "- 当前未找到可用的 active 主订阅" + elif [ -n "${failed_csv:-}" ]; then + echo "- 当前 active 主订阅拉取或校验失败" + fi + echo "- 当前成功源数量:${included_count}" } print_build_failed_source_lines() { local failed_csv name last_error - failed_csv="$(build_failed_sources_csv)" + failed_csv="$(build_failed_active_sources_csv)" [ -n "${failed_csv:-}" ] || return 0 IFS=',' read -r -a _failed_names <<< "$failed_csv" @@ -1073,7 +1117,7 @@ build_explain_next_steps() { local last_status failed_csv last_status="$(build_last_status)" - failed_csv="$(build_failed_sources_csv)" + failed_csv="$(build_failed_active_sources_csv)" if [ "${last_status:-}" = "success" ]; then echo "👉 clashctl health" @@ -1085,10 +1129,6 @@ build_explain_next_steps() { echo "👉 clashctl health" fi - if [ "$(subscription_mode 2>/dev/null || true)" = "merge" ]; then - echo "👉 clashctl config select list" - fi - echo "👉 clashctl doctor" } @@ -1289,38 +1329,6 @@ subscription_format_by_name() { "$(yq_bin)" eval ".sources.${name}.type // \"clash\"" "$file" 2>/dev/null | head -n 1 } -merge_subscription_sources() { - local base_file="$1" - local sub_file="$2" - local out_file="$3" - local tmp_file - - [ -s "$base_file" ] || die "基础配置不存在:$base_file" - [ -s "$sub_file" ] || die "订阅配置不存在:$sub_file" - - tmp_file="$(mktemp)" - cp -f "$base_file" "$tmp_file" - - BASE_FILE="$base_file" \ - SUB_FILE="$sub_file" \ - "$(yq_bin)" eval -i ' - .proxies = (load(strenv(SUB_FILE)).proxies // []) | - .["proxy-groups"] = (load(strenv(SUB_FILE)).["proxy-groups"] // (.["proxy-groups"] // [])) | - .["rule-providers"] = ((.["rule-providers"] // {}) * (load(strenv(SUB_FILE)).["rule-providers"] // {})) | - .rules = (load(strenv(SUB_FILE)).rules // (.rules // [])) - ' "$tmp_file" || { - rm -f "$tmp_file" 2>/dev/null || true - die "合并订阅配置失败" - } - - [ -s "$tmp_file" ] || { - rm -f "$tmp_file" 2>/dev/null || true - die "合并订阅配置失败:输出为空" - } - - mv -f "$tmp_file" "$out_file" -} - is_valid_port_number() { local port="$1" @@ -1491,7 +1499,7 @@ resolve_runtime_ports() { local used_ports="" preferred_mixed="${MIXED_PORT:-7890}" - preferred_controller="${EXTERNAL_CONTROLLER:-127.0.0.1:9090}" + preferred_controller="${EXTERNAL_CONTROLLER:-0.0.0.0:9090}" preferred_dns="${CLASH_DNS_PORT:-1053}" is_valid_port_number "$preferred_mixed" || die "MIXED_PORT 不合法:$preferred_mixed" @@ -1523,7 +1531,7 @@ mark_install_port_plan() { eval "$resolved" preferred_mixed="${MIXED_PORT:-7890}" - preferred_controller="${EXTERNAL_CONTROLLER:-127.0.0.1:9090}" + preferred_controller="${EXTERNAL_CONTROLLER:-0.0.0.0:9090}" preferred_dns="${CLASH_DNS_PORT:-1053}" if [ "$MIXED_PORT_RESOLVED" = "$preferred_mixed" ]; then @@ -1817,8 +1825,7 @@ remove_subscription() { active="$(active_subscription_name)" NAME="$name" "$(yq_bin)" eval -i ' - del(.sources[env(NAME)]) | - .selected = ((.selected // []) | map(select(. != env(NAME)))) + del(.sources[env(NAME)]) ' "$file" if [ "$active" = "$name" ]; then @@ -1861,7 +1868,6 @@ rename_subscription() { OLD_NAME="$old_name" NEW_NAME="$new_name" "$(yq_bin)" eval -i ' .sources[env(NEW_NAME)] = .sources[env(OLD_NAME)] | del(.sources[env(OLD_NAME)]) | - .selected = ((.selected // []) | map(if . == env(OLD_NAME) then env(NEW_NAME) else . end)) | .active = (if .active == env(OLD_NAME) then env(NEW_NAME) else .active end) ' "$file" @@ -2356,11 +2362,10 @@ list_subscriptions_verbose() { } subscription_list_overview_lines() { - local active recommended mode + local active recommended active="$(active_subscription_name 2>/dev/null || true)" recommended="$(recommended_subscription_name 2>/dev/null || true)" - mode="$(subscription_mode 2>/dev/null || true)" if [ -n "${active:-}" ]; then echo "📡 当前主订阅:$active" @@ -2376,17 +2381,14 @@ subscription_list_overview_lines() { echo "💡 推荐订阅:暂无" fi - if [ -n "${mode:-}" ]; then - echo "🧩 当前模式:$mode" - fi + echo "🧩 编译模式:active-only" } subscription_list_recommendation_lines() { - local active recommended mode + local active recommended active="$(active_subscription_name 2>/dev/null || true)" recommended="$(recommended_subscription_name 2>/dev/null || true)" - mode="$(subscription_mode 2>/dev/null || true)" if [ -n "${active:-}" ] && ! active_subscription_enabled 2>/dev/null; then echo "👉 clashctl use" @@ -2402,13 +2404,6 @@ subscription_list_recommendation_lines() { return 0 fi - if [ "${mode:-}" = "merge" ]; then - echo "👉 clashctl config show" - echo "👉 clashctl health" - echo "👉 clashctl ls --verbose" - return 0 - fi - echo "👉 clashctl use" echo "👉 clashctl health" echo "👉 clashctl ls --verbose" @@ -2964,14 +2959,6 @@ profile_set_value() { # ===== FINAL OVERRIDE: active-only runtime pipeline ===== -subscription_mode() { - echo "single" -} - -subscription_policy() { - echo "active-only" -} - resolve_build_sources() { local active @@ -3153,4 +3140,4 @@ generate_config() { regenerate_config() { generate_config -} \ No newline at end of file +} diff --git a/scripts/core/runtime.sh b/scripts/core/runtime.sh index a381316..53fbc59 100644 --- a/scripts/core/runtime.sh +++ b/scripts/core/runtime.sh @@ -44,10 +44,12 @@ resolve_yq() { } resolve_mihomo() { - local arch version file url tmp_file + local arch version file url tmp_file url_base custom_url arch="$(get_arch)" version="${MIHOMO_VERSION:-$DEFAULT_MIHOMO_VERSION}" + url_base="${MIHOMO_DOWNLOAD_BASE:-https://github.com/MetaCubeX/mihomo/releases/download}" + custom_url="${MIHOMO_DOWNLOAD_URL:-}" if [ -x "$(mihomo_bin)" ]; then return 0 @@ -60,10 +62,14 @@ resolve_mihomo() { *) die "暂不支持的 Mihomo 架构:$arch" ;; esac - url="https://github.com/MetaCubeX/mihomo/releases/download/${version}/${file}" + if [ -n "${custom_url:-}" ]; then + url="$custom_url" + else + url="${url_base%/}/${version}/${file}" + fi tmp_file="$(mktemp)" - download_file "$url" "$tmp_file" "mihomo" + download_file "$url" "$tmp_file" "mihomo(可在 .env 中设置 MIHOMO_DOWNLOAD_BASE / MIHOMO_DOWNLOAD_URL)" gzip -dc "$tmp_file" > "$(mihomo_bin)" chmod +x "$(mihomo_bin)" rm -f "$tmp_file" @@ -373,8 +379,10 @@ clean_runtime_state() { stop_subconverter || true stop_runtime || true clear_build_meta || true + clear_runtime_event_file || true rm -f "$RUNTIME_DIR/config.yaml" 2>/dev/null || true + rm -f "$(runtime_meta_file)" 2>/dev/null || true rm -f "$RUNTIME_DIR"/*.pid 2>/dev/null || true rm -f "$RUNTIME_DIR"/*.lock 2>/dev/null || true @@ -384,6 +392,8 @@ clean_runtime_state() { rm -rf "$RUNTIME_DIR/profiles" 2>/dev/null || true rm -rf "$RUNTIME_DIR/generated" 2>/dev/null || true rm -rf "$RUNTIME_DIR/tmp" 2>/dev/null || true + rm -rf "$(runtime_dashboard_dir)" 2>/dev/null || true + clear_shell_proxy_persist_state || true mkdir -p "$RUNTIME_DIR" "$BIN_DIR" "$LOG_DIR" -} \ No newline at end of file +} diff --git a/scripts/core/update.sh b/scripts/core/update.sh index 4c56e5e..95251ba 100644 --- a/scripts/core/update.sh +++ b/scripts/core/update.sh @@ -39,9 +39,12 @@ git_remote_name() { } sync_runtime_dependencies() { + ensure_dashboard_deploy_prerequisites resolve_yq resolve_runtime_kernel resolve_subconverter + install_local_dashboard_assets + ensure_controller_secret >/dev/null } remove_mihomo_binary() { @@ -143,4 +146,4 @@ update_project_code() { fi success "更新完成" -} \ No newline at end of file +} diff --git a/uninstall.sh b/uninstall.sh index e69295a..49de1c6 100755 --- a/uninstall.sh +++ b/uninstall.sh @@ -25,6 +25,8 @@ done source "$PROJECT_DIR/scripts/core/common.sh" # shellcheck source=scripts/core/runtime.sh source "$PROJECT_DIR/scripts/core/runtime.sh" +# shellcheck source=scripts/core/config.sh +source "$PROJECT_DIR/scripts/core/config.sh" # shellcheck source=scripts/init/systemd.sh source "$PROJECT_DIR/scripts/init/systemd.sh" # shellcheck source=scripts/init/systemd-user.sh @@ -40,11 +42,13 @@ service_stop || true remove_runtime_entry || true remove_clashctl_entry || true remove_shell_alias_entry || true +clear_shell_proxy_persist_state || true if [ "$PURGE_RUNTIME" = "true" ]; then rm -rf "$RUNTIME_DIR" + clear_controller_secret || true echo "🗑️ 已删除运行目录:$RUNTIME_DIR" - echo "🧩 保留内容:项目目录仍在" + echo "🧩 保留内容:项目目录仍在(已清理 controller secret)" elif [ "$DEV_RESET" = "true" ]; then cache_backup_dir="$(mktemp -d)" cache_restore_needed="false" @@ -75,12 +79,13 @@ elif [ "$DEV_RESET" = "true" ]; then fi rm -rf "$cache_backup_dir" 2>/dev/null || true + clear_controller_secret || true echo "🧪 已清理安装状态:$RUNTIME_DIR" - echo "🧩 保留内容:subscriptions.yaml、下载缓存与项目目录仍在" + echo "🧩 保留内容:subscriptions.yaml、下载缓存与项目目录仍在(已清理 controller secret)" else echo "📦 已卸载安装入口,保留运行目录:$RUNTIME_DIR" echo "🧩 保留内容:runtime 数据仍在" fi -echo "🟢 卸载完成" \ No newline at end of file +echo "🟢 卸载完成"