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 74bd257..2df99dd 100644 --- a/scripts/core/clashctl.sh +++ b/scripts/core/clashctl.sh @@ -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 "浏览器打开上面的任一地址" @@ -359,13 +408,11 @@ status_build_active_sources() { 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 } @@ -1670,9 +1717,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)" @@ -1680,6 +1727,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)" @@ -1693,6 +1742,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 状态总览" @@ -1712,6 +1786,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}" @@ -1721,6 +1799,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 @@ -1732,7 +1816,7 @@ print_status_summary_compact() { } print_status_summary_verbose() { - local running_text profile mixed_port controller + 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 @@ -1742,12 +1826,15 @@ 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)" current_active="$(active_subscription_name 2>/dev/null || true)" @@ -1801,6 +1888,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 状态总览" @@ -1825,6 +1937,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 @@ -1836,10 +1954,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 @@ -2244,15 +2366,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 "配置检查" @@ -2289,11 +2442,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 @@ -2572,6 +2744,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 "运行事件检查" @@ -2583,9 +2756,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) @@ -2691,7 +2887,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" @@ -3269,7 +3465,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 } @@ -3283,6 +3479,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() { @@ -3884,6 +4082,7 @@ cmd_add() { local sub_name prepare + ensure_add_use_prerequisites [ -n "${1:-}" ] || die "用法:clashctl add [clash|convert] [name]" @@ -3898,6 +4097,7 @@ cmd_use() { local recommended active prepare + ensure_add_use_prerequisites case "${1:-}" in --recommend|-r) @@ -4684,10 +4884,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 diff --git a/scripts/core/common.sh b/scripts/core/common.sh index 58a6fa7..e3d2f7a 100644 --- a/scripts/core/common.sh +++ b/scripts/core/common.sh @@ -281,6 +281,11 @@ 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 @@ -295,6 +300,10 @@ cleanup_env_legacy_compat_fields() { 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" } @@ -755,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="$( @@ -815,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() { @@ -929,6 +940,7 @@ init_layout() { "$PROJECT_DIR/scripts/core" \ "$PROJECT_DIR/scripts/init" \ "$RUNTIME_DIR" \ + "$RUNTIME_DIR/dashboard" \ "$BIN_DIR" \ "$LOG_DIR" @@ -942,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:]')" @@ -996,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) @@ -1321,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)" @@ -2306,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)" @@ -2314,12 +2499,13 @@ 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" @@ -2331,7 +2517,7 @@ install_runtime_brief_line() { 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) @@ -2344,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)" @@ -2355,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" @@ -2375,7 +2562,7 @@ 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" @@ -2387,7 +2574,7 @@ print_install_summary() { 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) diff --git a/scripts/core/config.sh b/scripts/core/config.sh index 15cd427..4132700 100644 --- a/scripts/core/config.sh +++ b/scripts/core/config.sh @@ -182,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: @@ -431,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" @@ -444,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") | @@ -472,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" @@ -1448,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" @@ -1480,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 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 "🟢 卸载完成"