refine clashctl main path UX and runtime config flow

This commit is contained in:
Arvin
2026-04-12 15:59:06 +08:00
parent 9025dfb7ff
commit b11b3bffef
10 changed files with 1049 additions and 223 deletions
+3 -3
View File
@@ -77,7 +77,7 @@ bash install.sh
```bash
$ clashui
╔═══════════════════════════════════════════════╗
║ 😼 Web 控制台 ║
║ 🐱 Web 控制台 ║
║═══════════════════════════════════════════════║
║ ║
║ 🔓 注意放行端口:9090 ║
@@ -88,10 +88,10 @@ $ clashui
╚═══════════════════════════════════════════════╝
$ clashctl secret mysecret
😼 密钥更新成功,已重启生效
🐱 密钥更新成功,已重启生效
$ clashctl secret
😼 当前密钥:mysecret
🐱 当前密钥:mysecret
```
- 可通过浏览器打开 `Web` 控制台进行可视化操作,例如切换节点、查看日志等。
+15 -1
View File
@@ -3,6 +3,20 @@ allow-lan: true
mode: rule
log-level: info
external-controller: 127.0.0.1:9090
tun:
enable: false
stack: system
auto-route: true
auto-detect-interface: true
auto-redirect: false
strict-route: false
dns-hijack:
- any:53
- tcp://any:53
dns:
enable: true
enhanced-mode: fake-ip
listen: 0.0.0.0:1053
proxies: []
proxy-groups: []
rules: []
rules: []
+1 -1
View File
@@ -19,7 +19,6 @@ init_layout
ensure_required_commands
ensure_dashboard_deploy_prerequisites
ui_download "正在准备安装依赖"
resolve_runtime_kernel
resolve_yq
resolve_subconverter
@@ -40,6 +39,7 @@ prompt_subscription_if_needed
if [ -n "$(subscription_url 2>/dev/null || true)" ]; then
if generate_config; then
echo "📡 订阅已生效"
post_install_verify
else
write_runtime_value "INSTALL_VERIFY_CONFIG_READY" "false"
+4 -4
View File
@@ -142,7 +142,7 @@ _clash_alias_after_on() {
_clash_alias_proxy_show
if [ "${CLASH_WRAPPER_EXEC:-0}" = "1" ]; then
echo "⚠️ 当前通过独立命令执行,Shell 变量不会自动回写到父终端"
echo "🚨 当前通过独立命令执行,Shell 变量不会自动回写到父终端"
echo "💡 如需当前终端立即生效,请重新打开终端或 source shell 入口后执行 clashon"
fi
@@ -178,12 +178,12 @@ _clash_alias_run_on() {
if [ "$on_rc" -ne 0 ]; then
if _clash_alias_export_system_proxy; then
echo "⚠️ clashctl on 返回非 0,但系统代理已写入,继续同步当前 Shell(底层返回码:$on_rc)" >&2
echo "🚨 clashctl on 返回非 0,但系统代理已写入,继续同步当前 Shell(底层返回码:$on_rc)" >&2
if [ -s "$on_output" ]; then
sed 's/^/ /' "$on_output" >&2
fi
elif _clash_alias_proxy_on; then
echo "⚠️ clashctl on 返回非 0,已通过 proxy on 继续同步当前 Shell(底层返回码:$on_rc)" >&2
echo "🚨 clashctl on 返回非 0,已通过 proxy on 继续同步当前 Shell(底层返回码:$on_rc)" >&2
if [ -s "$on_output" ]; then
sed 's/^/ /' "$on_output" >&2
fi
@@ -304,7 +304,7 @@ clashproxy() {
clashctl proxy select "$@"
;;
*)
echo "🧭 用法:clashproxy [show|on|off|groups|current|nodes|select]"
echo "📜 用法:clashproxy [show|on|off|groups|current|nodes|select]"
echo "💡 主路径切节点请使用:clashselect 或 clashctl select"
return 2
;;
+822 -186
View File
File diff suppressed because it is too large Load Diff
+87 -7
View File
@@ -93,7 +93,7 @@ ui_info() {
}
ui_warn() {
printf '⚠️ %s\n' "$*"
printf '🚨 %s\n' "$*"
}
ui_error() {
@@ -763,9 +763,15 @@ download_candidate_probe() {
download_candidate_fetch() {
local url="$1"
local out="$2"
local asset_name="${3:-}"
local progress_arg="--progress-bar"
if [ "$asset_name" = "subscription" ]; then
progress_arg="--silent"
fi
curl_download \
--progress-bar \
"$progress_arg" \
--show-error \
--fail \
--location \
@@ -795,7 +801,7 @@ download_file() {
if ! github_url_is_mirrorable "$url"; then
ui_download "正在下载:${asset_name}"
if download_candidate_fetch "$url" "$fetch_tmp"; then
if download_candidate_fetch "$url" "$fetch_tmp" "$asset_name"; then
mv -f "$fetch_tmp" "$out"
download_cache_store "$url" "$out" "$url"
return 0
@@ -845,7 +851,7 @@ download_file() {
ui_download "正在下载:${asset_name} [${label}]"
fi
if download_candidate_fetch "$candidate_url" "$fetch_tmp"; then
if download_candidate_fetch "$candidate_url" "$fetch_tmp" "$asset_name"; then
mv -f "$fetch_tmp" "$out"
download_cache_store "$url" "$out" "$candidate_url"
record_download_mirror_success "$label" "$candidate_url"
@@ -881,9 +887,15 @@ download_http_fetch_to_file() {
local ua="${3:-}"
local connect_timeout="${4:-10}"
local max_time="${5:-300}"
local asset_name="${6:-}"
local progress_arg="--progress-bar"
if [ "$asset_name" = "subscription" ]; then
progress_arg="--silent"
fi
curl_download \
--progress-bar \
"$progress_arg" \
--show-error \
--fail \
--location \
@@ -922,7 +934,7 @@ download_text_file() {
fi
ui_download "正在下载:${asset_name}"
if download_http_fetch_to_file "$url" "$fetch_tmp" "$ua" "$connect_timeout" "$max_time"; then
if download_http_fetch_to_file "$url" "$fetch_tmp" "$ua" "$connect_timeout" "$max_time" "$asset_name"; then
mv -f "$fetch_tmp" "$out"
download_cache_store "$url" "$out" "$url"
return 0
@@ -1638,6 +1650,14 @@ runtime_config_tun_auto_route() {
"$(yq_bin)" eval '.tun."auto-route" // false' "$file" 2>/dev/null | head -n 1
}
runtime_config_tun_auto_redirect() {
local file
file="$(runtime_config_file)"
[ -s "$file" ] || return 1
"$(yq_bin)" eval '.tun."auto-redirect" // false' "$file" 2>/dev/null | head -n 1
}
runtime_config_tun_auto_detect_interface() {
local file
file="$(runtime_config_file)"
@@ -2578,7 +2598,7 @@ install_runtime_brief_line() {
;;
verifying)
echo "🟡 当前状态:verifying"
echo "🧭 正在确认运行状态,可继续观察或手动启动"
echo "📜 正在确认运行状态,可继续观察或手动启动"
if [ -n "${mixed_port:-}" ]; then
echo "🌐 本地代理:http://127.0.0.1:${mixed_port}"
fi
@@ -2597,8 +2617,68 @@ install_runtime_brief_line() {
print_install_summary() {
local clashctl_file
local kernel_text project_path arch_text install_actor install_scope_text
local env_mode env_mode_text backend_text subscription_text node_count runtime_file
clashctl_file="$(clashctl_source)"
kernel_text="$(runtime_kernel_type 2>/dev/null || echo "unknown")"
project_path="${PROJECT_DIR:-unknown}"
arch_text="$(install_env_arch 2>/dev/null || true)"
[ -n "${arch_text:-}" ] || arch_text="$(get_arch 2>/dev/null || echo "unknown")"
if [ "$(install_env_is_root 2>/dev/null || echo false)" = "true" ]; then
install_actor="root"
else
install_actor="user"
fi
install_scope_text="$(install_env_scope 2>/dev/null || true)"
[ -n "${install_scope_text:-}" ] || install_scope_text="${INSTALL_SCOPE:-unknown}"
env_mode="$(install_env_container 2>/dev/null || true)"
[ -n "${env_mode:-}" ] || env_mode="$(container_env_type 2>/dev/null || echo "unknown")"
case "${env_mode:-unknown}" in
host)
env_mode_text="主机"
;;
container|docker|lxc)
env_mode_text="容器"
;;
*)
env_mode_text="${env_mode:-unknown}"
;;
esac
backend_text="$(install_plan_backend 2>/dev/null || true)"
[ -n "${backend_text:-}" ] || backend_text="$(runtime_backend 2>/dev/null || echo "unknown")"
if install_has_subscription 2>/dev/null; then
subscription_text="已配置"
else
subscription_text="未配置"
fi
runtime_file="$(runtime_config_file)"
if [ -s "$runtime_file" ]; then
node_count="$("$(yq_bin)" eval '(.proxies // []) | length' "$runtime_file" 2>/dev/null | head -n 1)"
case "${node_count:-}" in
''|*[!0-9]*)
node_count=""
;;
esac
fi
echo
echo "🎉 安装完成"
echo
echo "🚀 当前内核:$kernel_text"
echo "🧬 系统架构:$arch_text"
echo "💻 环境模式:${env_mode_text:-unknown}"
echo "📁 安装路径:$project_path"
echo "👤 安装方式:${install_actor} / ${install_scope_text:-unknown}"
echo "🔧 运行后端:${backend_text:-unknown}"
echo "📦 订阅:$subscription_text"
[ -n "${node_count:-}" ] && echo "🔢 节点数量:$node_count"
if [ -f "$clashctl_file" ]; then
CLASH_UI_BOX_ONLY=1 bash "$clashctl_file" ui || true
+80 -9
View File
@@ -174,6 +174,59 @@ tun_stack() {
esac
}
config_bool_env_value() {
local key="$1"
local default_value="$2"
local value
value="${!key:-}"
[ -n "${value:-}" ] || value="$(read_env_value "$key" 2>/dev/null || true)"
[ -n "${value:-}" ] || value="$default_value"
case "$value" in
true|1|yes|on)
echo "true"
;;
false|0|no|off)
echo "false"
;;
*)
echo "$default_value"
;;
esac
}
tun_auto_route() {
config_bool_env_value "CLASH_TUN_AUTO_ROUTE" "true"
}
tun_auto_redirect_default() {
if [ "$(get_os 2>/dev/null || echo unknown)" = "linux" ] \
&& [ "$(container_env_type 2>/dev/null || echo unknown)" = "host" ] \
&& [ "$(runtime_kernel_type 2>/dev/null || echo mihomo)" = "mihomo" ]; then
echo "true"
return 0
fi
echo "false"
}
tun_auto_redirect() {
config_bool_env_value "CLASH_TUN_AUTO_REDIRECT" "$(tun_auto_redirect_default)"
}
tun_strict_route() {
config_bool_env_value "CLASH_TUN_STRICT_ROUTE" "false"
}
tun_dns_hijack() {
local value
value="${CLASH_TUN_DNS_HIJACK:-}"
[ -n "${value:-}" ] || value="$(read_env_value "CLASH_TUN_DNS_HIJACK" 2>/dev/null || true)"
[ -n "${value:-}" ] || value="any:53,tcp://any:53"
echo "$value"
}
ensure_config_files() {
mkdir -p "$CONFIG_DIR" "$(config_tmp_dir)"
@@ -190,6 +243,11 @@ tun:
stack: system
auto-route: true
auto-detect-interface: true
auto-redirect: false
strict-route: false
dns-hijack:
- any:53
- tcp://any:53
dns:
enable: true
@@ -432,6 +490,7 @@ clear_subscription_cache() {
normalize_runtime_config() {
local file="$1"
local mixed_port controller tun_enable_value tun_stack_value dns_port_value controller_secret_value
local tun_auto_route_value tun_auto_redirect_value tun_strict_route_value tun_dns_hijack_value
local dashboard_dir_value
local resolved_ports
@@ -444,6 +503,10 @@ normalize_runtime_config() {
controller="$EXTERNAL_CONTROLLER_RESOLVED"
tun_enable_value="$(tun_enabled)"
tun_stack_value="$(tun_stack)"
tun_auto_route_value="$(tun_auto_route)"
tun_auto_redirect_value="$(tun_auto_redirect)"
tun_strict_route_value="$(tun_strict_route)"
tun_dns_hijack_value="$(tun_dns_hijack)"
dns_port_value="$CLASH_DNS_PORT_RESOLVED"
controller_secret_value="$(ensure_controller_secret)"
dashboard_dir_value="$(runtime_dashboard_dir)"
@@ -452,6 +515,10 @@ normalize_runtime_config() {
controller="$controller" \
tun_enable_value="$tun_enable_value" \
tun_stack_value="$tun_stack_value" \
tun_auto_route_value="$tun_auto_route_value" \
tun_auto_redirect_value="$tun_auto_redirect_value" \
tun_strict_route_value="$tun_strict_route_value" \
tun_dns_hijack_value="$tun_dns_hijack_value" \
controller_secret_value="$controller_secret_value" \
dashboard_dir_value="$dashboard_dir_value" \
dns_listen_value="0.0.0.0:${dns_port_value}" \
@@ -467,8 +534,11 @@ normalize_runtime_config() {
.tun.enable = (env(tun_enable_value) == "true") |
.tun.stack = env(tun_stack_value) |
.tun["auto-route"] = (.tun["auto-route"] // true) |
.tun["auto-route"] = (env(tun_auto_route_value) == "true") |
.tun["auto-detect-interface"] = (.tun["auto-detect-interface"] // true) |
.tun["auto-redirect"] = (env(tun_auto_redirect_value) == "true") |
.tun["strict-route"] = (env(tun_strict_route_value) == "true") |
.tun["dns-hijack"] = (env(tun_dns_hijack_value) | split(",") | map(select(. != ""))) |
.dns.enable = (.dns.enable // true) |
.dns["enhanced-mode"] = (.dns["enhanced-mode"] // "fake-ip") |
@@ -736,7 +806,7 @@ print_subscription_health_summary() {
risk_marker=""
if subscription_auto_disabled "$name"; then
risk_marker="⚠️"
risk_marker="🚨"
fi
if [ "$name" = "$active" ]; then
@@ -788,14 +858,14 @@ print_subscription_health_one() {
url_text="$(subscription_url_by_name "$name" 2>/dev/null || true)"
echo "📡 订阅名称:$name"
echo "⚙️ 订阅类型:$type_text"
echo "🔧 订阅类型:$type_text"
echo "🟢 启用状态:$enabled_text"
echo "❤️ 健康状态:${status:-unknown}"
echo "⚠️ 失败次数:${fail_count:-0}"
echo "🚨 失败次数:${fail_count:-0}"
[ -n "${last_success:-}" ] && echo "🕒 最近成功:$last_success"
[ -n "${last_failure:-}" ] && echo "🕒 最近失败:$last_failure"
[ -n "${last_error:-}" ] && echo "❌ 最近错误:$last_error"
[ -n "${auto_disabled:-}" ] && echo "⚠️ 风险标记:$auto_disabled"
[ -n "${auto_disabled:-}" ] && echo "🚨 风险标记:$auto_disabled"
[ -n "${auto_disabled_at:-}" ] && echo "🕒 风险时间:$auto_disabled_at"
[ -n "${url_text:-}" ] && echo "🔗 订阅地址:$url_text"
}
@@ -2400,9 +2470,9 @@ subscription_list_overview_lines() {
recommended="$(recommended_subscription_name 2>/dev/null || true)"
if [ -n "${active:-}" ]; then
echo "📡 当前主订阅:$active"
echo "🚩 当前主订阅:$active"
else
echo "📡 当前主订阅:未设置"
echo "🚩 当前主订阅:未设置"
fi
if [ -n "${recommended:-}" ] && [ "${recommended:-}" != "${active:-}" ]; then
@@ -3207,7 +3277,8 @@ generate_config() {
included_csv="$active_source"
cp -f "$source_file" "$out_file"
cp -f "$source_file" "$RUNTIME_DIR/config.last.yaml"
apply_runtime_mixin "$out_file"
cp -f "$out_file" "$RUNTIME_DIR/config.last.yaml"
save_build_stage_snapshot "active-source-ready" "$source_file"
record_build_success \
@@ -3219,7 +3290,7 @@ generate_config() {
"$failed_csv"
mark_runtime_config_source "build-active"
success "配置文件已生成:$out_file"
# success "配置文件已生成:$out_file"
return 0
}
+2 -2
View File
@@ -150,12 +150,12 @@ print_proxy_show() {
status="$(system_proxy_status)"
echo
echo "😼 当前代理环境"
echo "🐱 当前代理环境"
echo
echo "🌐 HTTP:$(proxy_http_url)"
echo "🧦 SOCKS5:$(proxy_socks_url)"
echo "🚫 NO_PROXY:$(proxy_no_proxy_value)"
echo "🧭 系统代理:${status}($(system_proxy_env_file))"
echo "📜 系统代理:${status}($(system_proxy_env_file))"
echo
}
-7
View File
@@ -243,28 +243,23 @@ resolve_subconverter() {
extract_dir="$tmp_dir/extract"
download_file "$url" "$tmp_file" "subconverter"
info "subconverter 下载文件:$tmp_file"
package_type="$(detect_subconverter_package_type "$tmp_file")"
info "subconverter 下载文件类型:$package_type"
rm -rf "$target_dir"
mkdir -p "$target_dir" "$extract_dir"
case "$package_type" in
tar.gz)
info "subconverter 解压目录:$extract_dir"
tar -xzf "$tmp_file" -C "$extract_dir"
found_bin="$(find_subconverter_binary "$extract_dir" 2>/dev/null || true)"
;;
zip)
command -v unzip >/dev/null 2>&1 || die "解压 subconverter zip 失败:系统缺少 unzip"
info "subconverter 解压目录:$extract_dir"
unzip -oq "$tmp_file" -d "$extract_dir"
found_bin="$(find_subconverter_binary "$extract_dir" 2>/dev/null || true)"
;;
binary)
info "subconverter 解压目录:无需解压"
found_bin="$tmp_file"
;;
*)
@@ -280,10 +275,8 @@ resolve_subconverter() {
die "解压后未找到 subconverter 文件"
}
info "subconverter 实际文件:$found_bin"
cp -f "$found_bin" "$target_bin"
chmod +x "$target_bin"
info "subconverter 最终落盘:$target_bin"
[ -x "$target_bin" ] || {
rm -rf "$tmp_dir"
+35 -3
View File
@@ -130,6 +130,31 @@ service_restart() {
esac
}
service_is_running() {
local backend pid
backend="$(runtime_backend)"
case "$backend" in
systemd)
systemctl is-active --quiet "$(service_unit_name)"
;;
systemd-user)
systemctl --user is-active --quiet "$(service_unit_name)"
;;
script)
if [ -f "$RUNTIME_DIR/mihomo.pid" ]; then
pid="$(cat "$RUNTIME_DIR/mihomo.pid" 2>/dev/null || true)"
[ -n "${pid:-}" ] && kill -0 "$pid" 2>/dev/null
else
return 1
fi
;;
*)
return 1
;;
esac
}
service_status_text() {
local backend
backend="$(runtime_backend)"
@@ -177,7 +202,7 @@ wait_install_runtime_ready() {
local controller_ready="false"
while [ "$i" -lt "$max_try" ]; do
if status_is_running 2>/dev/null; then
if service_is_running 2>/dev/null; then
runtime_ready="true"
fi
@@ -221,7 +246,14 @@ post_install_verify() {
fi
if [ "$has_subscription" = "true" ]; then
service_start >/dev/null 2>&1 || true
if service_is_running 2>/dev/null; then
service_restart >/dev/null 2>&1 || {
service_stop >/dev/null 2>&1 || true
service_start >/dev/null 2>&1 || true
}
else
service_start >/dev/null 2>&1 || true
fi
wait_install_runtime_ready 4
else
write_runtime_value "INSTALL_VERIFY_RUNTIME_READY" "false"
@@ -238,4 +270,4 @@ post_install_verify() {
else
write_runtime_event_value "RUNTIME_LAST_INSTALL_READY" "false"
fi
}
}