Merge pull request #167 from wnlen/codex/refactor-to-active-only-architecture-qpp4w8

Add local Dashboard deployment, controller secret & shell-proxy persistence; improve diagnostics
This commit is contained in:
Arvin
2026-04-09 13:58:15 +08:00
committed by GitHub
8 changed files with 572 additions and 37 deletions
+5 -1
View File
@@ -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
print_install_summary
+48 -1
View File
@@ -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" <<EOF
SHELL_PROXY_PERSIST_ENABLED="${enabled}"
SHELL_PROXY_PERSIST_TIME="$(date '+%Y-%m-%d %H:%M:%S' 2>/dev/null || true)"
EOF
}
_clash_alias_persist_enabled() {
local state_file enabled
state_file="$(_clash_alias_state_file)"
[ -f "$state_file" ] || return 1
enabled="$(sed -nE 's/^SHELL_PROXY_PERSIST_ENABLED=\"?([^\"\r\n]+)\"?$/\1/p' "$state_file" | head -n 1)"
[ "${enabled:-false}" = "true" ]
}
_clash_alias_print_sep() {
echo
}
@@ -37,6 +68,7 @@ _clash_alias_prepare_on() {
}
_clash_alias_after_on() {
_clash_alias_set_persist_enabled "true"
_clash_alias_proxy_on || return $?
_clash_alias_print_sep
@@ -46,9 +78,11 @@ _clash_alias_after_on() {
}
_clash_alias_after_off() {
_clash_alias_set_persist_enabled "false"
_clash_alias_print_sep
echo "🔴 已关闭代理环境"
echo "🧹 当前 Shell 代理变量已清理"
echo "🧭 新终端默认代理:已关闭"
echo "👉 下一步:clashctl status"
}
@@ -62,7 +96,18 @@ _clash_alias_run_on() {
_clash_alias_run_off() {
_clash_alias_proxy_off
_clash_alias_set_persist_enabled "false"
_clashctl_real off "$@" || return $?
_clash_alias_after_off
}
_clash_alias_auto_restore_proxy() {
[ "${CLASH_FOR_LINUX_PROXY_AUTO_RESTORED:-0}" = "1" ] && return 0
export CLASH_FOR_LINUX_PROXY_AUTO_RESTORED="1"
if _clash_alias_persist_enabled; then
_clash_alias_proxy_on >/dev/null 2>&1 || true
fi
}
clashctl() {
@@ -195,4 +240,6 @@ clashmixin() {
esac
}
# shell 被 source 时不自动执行任何代理动作
# shell 被 source 后做轻量恢复:
# 若上次 clashon 持久化开启,则新终端自动恢复当前 shell 代理变量。
_clash_alias_auto_restore_proxy
+243 -15
View File
@@ -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 <url> [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
+195 -8
View File
@@ -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" <<EOF
SHELL_PROXY_PERSIST_ENABLED="${enabled}"
SHELL_PROXY_PERSIST_TIME="$(now_datetime)"
EOF
}
clear_shell_proxy_persist_state() {
rm -f "$(shell_proxy_persist_state_file)" 2>/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)
+55 -4
View File
@@ -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
+14 -4
View File
@@ -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"
}
}
+4 -1
View File
@@ -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 "更新完成"
}
}
+8 -3
View File
@@ -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 "🟢 卸载完成"
echo "🟢 卸载完成"