Merge pull request #192 from wnlen/dev

Dev
This commit is contained in:
Arvin
2026-04-14 00:43:02 +08:00
committed by GitHub
4 changed files with 511 additions and 37 deletions
+18
View File
@@ -157,6 +157,24 @@ clashctl sub rename <旧名称> <新名称>
clashctl sub remove <名称>
```
WSL / 普通用户如果无权写入 `/etc/environment`,`clashon` 会自动降级:运行时照常启动,当前 Shell 代理变量生效;系统代理持久接管和开机代理保持不可用。
### 本地订阅
可以先手动下载 Clash 格式的订阅 YAML 文件到本地,再通过 `file://` 添加:
格式:`clashctl add "file:///绝对路径/xxx.yaml" 名称`
```bash
mkdir -p ~/.config/clash-for-linux/subscriptions
curl -L "https://example.com/sub.yaml" -o ~/.config/clash-for-linux/subscriptions/home.yaml
clashctl add "file://$HOME/.config/clash-for-linux/subscriptions/home.yaml" home
clashctl use home
clashon
```
`file://` 只支持 Clash 本地 YAML 配置,不支持 convert 格式订阅。如果订阅源需要转换,请先转换为 Clash YAML 后再保存到本地文件。
------
## 🏗️ 架构设计架构简述
+58 -3
View File
@@ -46,6 +46,14 @@ _clash_alias_state_file() {
echo "$(_clash_alias_project_dir)/runtime/shell-proxy.env"
}
_clash_alias_runtime_config_file() {
echo "$(_clash_alias_project_dir)/runtime/config.yaml"
}
_clash_alias_yq_bin() {
echo "$(_clash_alias_project_dir)/runtime/bin/yq"
}
_clash_alias_set_persist_enabled() {
local enabled="$1"
local state_file
@@ -72,6 +80,11 @@ _clash_alias_print_sep() {
}
_clash_alias_proxy_on() {
_clashctl_real proxy on >/dev/null || return $?
_clash_alias_export_proxy || return $?
}
_clash_alias_proxy_on_system() {
_clashctl_real proxy on >/dev/null || return $?
_clash_alias_export_system_proxy || return $?
}
@@ -102,6 +115,44 @@ _clash_alias_export_system_proxy() {
[ -n "${all_url:-}" ] || all_url="${http_url/http:\/\//socks5://}"
[ -n "${no_proxy:-}" ] || no_proxy="127.0.0.1,localhost,::1"
_clash_alias_export_proxy_values "$http_url" "$https_url" "$all_url" "$no_proxy"
}
_clash_alias_runtime_proxy_port() {
local config_file yq_bin port
config_file="$(_clash_alias_runtime_config_file)"
[ -s "$config_file" ] || return 1
yq_bin="$(_clash_alias_yq_bin)"
if [ -x "$yq_bin" ]; then
port="$("$yq_bin" eval '.["mixed-port"] // .port // ""' "$config_file" 2>/dev/null | head -n 1)"
else
port="$(sed -nE 's/^[[:space:]]*(mixed-port|port):[[:space:]]*"?([0-9]+)"?[[:space:]]*$/\2/p' "$config_file" | head -n 1)"
fi
[ -n "${port:-}" ] && [ "$port" != "null" ] || return 1
echo "$port"
}
_clash_alias_export_runtime_proxy() {
local port host http_url all_url no_proxy
port="$(_clash_alias_runtime_proxy_port)" || return $?
host="${CLASH_PROXY_HOST:-127.0.0.1}"
http_url="http://${host}:${port}"
all_url="socks5://${host}:${port}"
no_proxy="${NO_PROXY_DEFAULT:-127.0.0.1,localhost,::1}"
_clash_alias_export_proxy_values "$http_url" "$http_url" "$all_url" "$no_proxy"
}
_clash_alias_export_proxy_values() {
local http_url="$1"
local https_url="$2"
local all_url="$3"
local no_proxy="$4"
export http_proxy="$http_url"
export https_proxy="$https_url"
export HTTP_PROXY="$http_url"
@@ -112,6 +163,10 @@ _clash_alias_export_system_proxy() {
export NO_PROXY="$no_proxy"
}
_clash_alias_export_proxy() {
_clash_alias_export_system_proxy || _clash_alias_export_runtime_proxy
}
_clash_alias_unset_shell_proxy() {
unset \
http_proxy https_proxy HTTP_PROXY HTTPS_PROXY \
@@ -131,7 +186,7 @@ _clash_alias_prepare_on() {
_clash_alias_after_on() {
_clash_alias_set_persist_enabled "true"
_clash_alias_export_system_proxy || return $?
_clash_alias_export_proxy || return $?
}
_clash_alias_run_on() {
@@ -165,7 +220,7 @@ _clash_alias_run_on() {
if [ -s "$on_output" ]; then
sed 's/^/ /' "$on_output" >&2
fi
elif _clash_alias_proxy_on; then
elif _clash_alias_proxy_on_system; then
echo "🚨 clashctl on 返回非 0,已通过 proxy on 继续同步当前 Shell(底层返回码:$on_rc)" >&2
if [ -s "$on_output" ]; then
sed 's/^/ /' "$on_output" >&2
@@ -201,7 +256,7 @@ _clash_alias_run_off() {
_clash_alias_auto_restore_proxy() {
_clash_alias_persist_enabled || return 0
_clash_alias_export_system_proxy || return 0
_clash_alias_export_proxy || return 0
echo "♻️ 已恢复当前 shell 代理环境(来自持久化状态)"
return 0
+435 -24
View File
@@ -213,6 +213,7 @@ print_on_feedback() {
cmd_on() {
local relay_switch
local relay_switch_file relay_err_file relay_rc
local system_proxy_rc system_proxy_degraded="false"
local already_on="false"
trap 'rc=$?; ui_error "开启代理失败:cmd_on 在第 ${LINENO} 行执行失败:${BASH_COMMAND}(返回码:${rc})"; ui_next "clashctl logs service"; exit "$rc"' ERR
@@ -260,16 +261,32 @@ cmd_on() {
rm -f "$relay_switch_file" 2>/dev/null || true
fi
if ! system_proxy_enable; then
service_stop >/dev/null 2>&1 || true
die_state "当前环境不支持系统代理接管(仅支持可写 /etc/environment)" "clashctl doctor"
if system_proxy_enable; then
system_proxy_degraded="false"
else
system_proxy_rc=$?
system_proxy_degraded="true"
write_runtime_value "RUNTIME_BOOT_PROXY_KEEP" "false" 2>/dev/null || true
if [ "$system_proxy_rc" -eq 2 ]; then
ui_warn "当前环境不支持系统代理持久接管,仅当前 Shell 生效"
ui_next "开机代理保持不可用;如需持久接管,请使用可写的 $(system_proxy_env_file)"
else
ui_warn "系统代理持久接管写入失败,仅当前 Shell 生效"
ui_next "clashctl doctor"
fi
ui_blank
fi
load_system_state
print_on_feedback
if [ "${CLASH_ALIAS_CALL:-0}" != "1" ]; then
ui_warn "当前通过 clashctl 子进程执行,已开启系统代理,但不会修改当前 Shell 代理变量"
if [ "$system_proxy_degraded" = "true" ]; then
ui_warn "当前通过 clashctl 子进程执行,不能修改当前 Shell 代理变量"
else
ui_warn "当前通过 clashctl 子进程执行,已开启系统代理,但不会修改当前 Shell 代理变量"
fi
ui_next "需要当前 Shell 立即生效:source shell 入口后执行 clashon,或重新打开终端"
ui_blank
fi
@@ -285,11 +302,22 @@ cmd_on() {
}
cmd_off() {
local system_proxy_rc
prepare
service_stop
if ! system_proxy_disable; then
die_state "当前环境不支持系统代理关闭(仅支持可写 /etc/environment)" "clashctl doctor"
if system_proxy_disable; then
:
else
system_proxy_rc=$?
write_runtime_value "RUNTIME_BOOT_PROXY_KEEP" "false" 2>/dev/null || true
if [ "$system_proxy_rc" -eq 2 ]; then
ui_warn "当前环境不支持清理系统代理持久块,已继续关闭运行时"
else
ui_warn "系统代理持久块清理失败,已继续关闭运行时"
ui_next "clashctl doctor"
fi
fi
ui_blank
echo "🧹 系统代理已关闭"
@@ -578,6 +606,215 @@ status_build_effective_status() {
system_state_build_status 2>/dev/null || true
}
runtime_mixed_port_bind_failure_line() {
local log_file="$LOG_DIR/mihomo.out.log"
local mixed_port line
runtime_config_exists 2>/dev/null || return 1
status_is_running 2>/dev/null && return 1
[ -f "$log_file" ] || return 1
mixed_port="$(status_read_mixed_port 2>/dev/null || true)"
if [ -n "${mixed_port:-}" ] && [ "$mixed_port" != "null" ]; then
line="$(grep -Ei "Start Mixed.*server error: listen tcp .*:${mixed_port}:.*(operation not permitted|permission denied|address already in use)" "$log_file" 2>/dev/null | tail -n 1 || true)"
fi
if [ -z "${line:-}" ]; then
line="$(grep -Ei 'Start Mixed.*server error: listen tcp .*:.*(operation not permitted|permission denied|address already in use)' "$log_file" 2>/dev/null | tail -n 1 || true)"
fi
[ -n "${line:-}" ] || return 1
printf '%s\n' "$line"
}
runtime_mixed_port_bind_failure_kind() {
local line lower
line="$(runtime_mixed_port_bind_failure_line 2>/dev/null || true)"
[ -n "${line:-}" ] || return 1
lower="$(printf '%s' "$line" | tr '[:upper:]' '[:lower:]')"
case "$lower" in
*"address already in use"*)
echo "address_in_use"
;;
*"operation not permitted"*|*"permission denied"*)
echo "bind_denied"
;;
*)
echo "bind_failed"
;;
esac
}
runtime_mixed_port_bind_failure_text() {
case "$(runtime_mixed_port_bind_failure_kind 2>/dev/null || true)" in
address_in_use) echo "mixed-port 端口被占用" ;;
bind_denied) echo "mixed-port 绑定被拒绝" ;;
bind_failed) echo "mixed-port 绑定失败" ;;
*) return 1 ;;
esac
}
runtime_mixed_port_bind_failure_port() {
local port line
port="$(status_read_mixed_port 2>/dev/null || true)"
if [ -n "${port:-}" ] && [ "$port" != "null" ]; then
echo "$port"
return 0
fi
line="$(runtime_mixed_port_bind_failure_line 2>/dev/null || true)"
[ -n "${line:-}" ] || return 1
printf '%s\n' "$line" | grep -Eo ':[0-9]+:' | tail -n 1 | tr -d ':'
}
runtime_mihomo_log_port_listened() {
local keyword="$1"
local port="$2"
local log_file="$LOG_DIR/mihomo.out.log"
local line
[ -n "${keyword:-}" ] || return 1
[ -n "${port:-}" ] || return 1
[ -f "$log_file" ] || return 1
line="$(grep -Ei "(${keyword}).*(listen|listening|start|started).*[:.]${port}([^0-9]|$)|[:.]${port}([^0-9]|$).*(listen|listening|start|started).*(${keyword})" "$log_file" 2>/dev/null | tail -n 1 || true)"
[ -n "${line:-}" ]
}
runtime_mixed_port_controller_listening() {
local port
proxy_controller_reachable 2>/dev/null && return 0
port="$(runtime_config_controller_port 2>/dev/null || true)"
[ -n "${port:-}" ] || return 1
runtime_mihomo_log_port_listened "RESTful API|controller" "$port"
}
runtime_mixed_port_dns_listening() {
local port
port="$(runtime_config_dns_port 2>/dev/null || true)"
[ -n "${port:-}" ] || return 1
runtime_mihomo_log_port_listened "DNS|dns" "$port" && return 0
status_is_running 2>/dev/null && is_port_in_use "$port"
}
mixed_port_bind_environment_text() {
local scope backend container
scope="$(install_env_scope 2>/dev/null || true)"
[ -n "${scope:-}" ] || scope="${INSTALL_SCOPE:-unknown}"
backend="$(runtime_backend 2>/dev/null || true)"
[ -n "${backend:-}" ] || backend="$(install_plan_backend 2>/dev/null || true)"
[ -n "${backend:-}" ] || backend="unknown"
container="$(container_env_type 2>/dev/null || true)"
[ -n "${container:-}" ] || container="$(install_env_container 2>/dev/null || true)"
[ -n "${container:-}" ] || container="unknown"
echo "安装范围 ${scope},后端 ${backend},容器环境 ${container}"
}
mixed_port_bind_observation_line() {
local port dns_port
port="$(runtime_mixed_port_bind_failure_port 2>/dev/null || status_read_mixed_port 2>/dev/null || true)"
dns_port="$(runtime_config_dns_port 2>/dev/null || true)"
if runtime_mixed_port_controller_listening 2>/dev/null; then
echo "配置已加载,控制器已启动,仅代理端口 ${port:-unknown} 绑定失败;不是订阅/配置主链失败"
fi
if [ -n "${dns_port:-}" ] && runtime_mixed_port_dns_listening 2>/dev/null; then
echo "DNS 端口 ${dns_port} 已监听,可先聚焦 mixed-port"
fi
}
mixed_port_bind_recommendation_emit() {
local style="$1"
local number="$2"
local text="$3"
case "$style" in
numbered)
echo "${number}. ${text}"
;;
icon)
echo "💡 ${text}"
;;
*)
echo "$text"
;;
esac
}
mixed_port_bind_recommendation_lines() {
local style="${1:-icon}"
local kind port env_text n line
kind="$(runtime_mixed_port_bind_failure_kind 2>/dev/null || true)"
[ -n "${kind:-}" ] || return 1
port="$(runtime_mixed_port_bind_failure_port 2>/dev/null || status_read_mixed_port 2>/dev/null || true)"
env_text="$(mixed_port_bind_environment_text 2>/dev/null || echo "当前环境")"
n=1
while IFS= read -r line; do
[ -n "${line:-}" ] || continue
mixed_port_bind_recommendation_emit "$style" "$n" "$line"
n=$((n + 1))
done <<EOF
$(mixed_port_bind_observation_line 2>/dev/null || true)
EOF
case "$kind" in
bind_denied)
mixed_port_bind_recommendation_emit "$style" "$n" "当前 ${env_text} 对端口 ${port:-unknown} 的可绑定性受限;优先修改 .env 中 MIXED_PORT 后执行 clashctl config regen"
n=$((n + 1))
;;
address_in_use)
mixed_port_bind_recommendation_emit "$style" "$n" "mixed-port ${port:-unknown} 存在端口冲突;先检查占用进程,或修改 .env 中 MIXED_PORT 后执行 clashctl config regen"
n=$((n + 1))
;;
*)
mixed_port_bind_recommendation_emit "$style" "$n" "mixed-port ${port:-unknown} 绑定失败;先查看内核日志确认端口错误"
n=$((n + 1))
;;
esac
mixed_port_bind_recommendation_emit "$style" "$n" "保留日志线索:clashctl logs mihomo"
}
mixed_port_bind_next_action() {
local kind port
kind="$(runtime_mixed_port_bind_failure_kind 2>/dev/null || true)"
port="$(runtime_mixed_port_bind_failure_port 2>/dev/null || status_read_mixed_port 2>/dev/null || true)"
case "$kind" in
bind_denied)
echo "修改 .env 中 MIXED_PORT 后执行 clashctl config regen"
;;
address_in_use)
echo "检查 ${port:-mixed-port} 占用,或修改 .env 中 MIXED_PORT 后执行 clashctl config regen"
;;
bind_failed)
echo "clashctl logs mihomo"
;;
*)
return 1
;;
esac
}
system_state_runtime_status() {
if status_is_running; then
if proxy_controller_reachable 2>/dev/null; then
@@ -620,6 +857,11 @@ system_state_subscription_status() {
system_state_risk_level() {
local risk
if runtime_mixed_port_bind_failure_kind >/dev/null 2>&1; then
echo "high"
return 0
fi
if status_is_running 2>/dev/null && ! proxy_controller_reachable 2>/dev/null; then
echo "high"
return 0
@@ -646,7 +888,7 @@ system_state_summary() {
local runtime_status build_status subscription_status risk_level
local config_source fallback_used build_applied
local tun_enabled tun_effective tun_container_mode tun_kernel_support
local overall
local bind_failure_kind overall
runtime_status="$(system_state_runtime_status)"
build_status="$(system_state_build_status)"
@@ -655,6 +897,7 @@ system_state_summary() {
config_source="$(status_runtime_config_source)"
fallback_used="$(runtime_last_fallback_used 2>/dev/null || true)"
build_applied="$(status_runtime_build_applied)"
bind_failure_kind="$(runtime_mixed_port_bind_failure_kind 2>/dev/null || true)"
tun_enabled="$(status_tun_enabled)"
tun_effective="$(status_tun_effective_status)"
@@ -665,6 +908,8 @@ system_state_summary() {
&& [ "$build_status" = "success" ] \
&& [ "$subscription_status" = "healthy" ]; then
overall="ready"
elif [ "$runtime_status" = "stopped" ] && [ -n "${bind_failure_kind:-}" ]; then
overall="broken"
elif [ "$runtime_status" = "stopped" ]; then
overall="stopped"
elif [ "$build_status" = "failed" ] || [ "$build_status" = "blocked" ] || [ "$subscription_status" = "invalid" ] || [ "$subscription_status" = "missing" ]; then
@@ -679,6 +924,7 @@ RUNTIME_STATE=$runtime_status
BUILD_STATE=$build_status
SUBSCRIPTION_STATE=$subscription_status
RISK_LEVEL=$risk_level
BIND_FAILURE_STATE=${bind_failure_kind:-none}
CONFIG_SOURCE=${config_source:-unknown}
FALLBACK_USED=${fallback_used:-false}
BUILD_APPLIED=${build_applied:-unknown}
@@ -710,7 +956,11 @@ system_state_connectivity_text() {
echo "未连接(代理未启动)"
;;
broken)
echo "不可用(配置或订阅异常)"
if [ "${BIND_FAILURE_STATE:-none}" != "none" ]; then
echo "不可用(mixed-port 绑定失败)"
else
echo "不可用(配置或订阅异常)"
fi
;;
*)
echo "未知"
@@ -735,6 +985,11 @@ system_state_default_action() {
case "$SYSTEM_STATE" in
stopped)
if [ "${BIND_FAILURE_STATE:-none}" != "none" ]; then
mixed_port_bind_next_action 2>/dev/null || echo "clashctl logs mihomo"
return 0
fi
if [ "$SUBSCRIPTION_STATE" = "missing" ]; then
echo "clashctl add <订阅链接>"
else
@@ -742,6 +997,11 @@ system_state_default_action() {
fi
;;
broken)
if [ "${BIND_FAILURE_STATE:-none}" != "none" ]; then
mixed_port_bind_next_action 2>/dev/null || echo "clashctl logs mihomo"
return 0
fi
if [ "$SUBSCRIPTION_STATE" = "missing" ]; then
echo "clashctl add <订阅链接>"
else
@@ -765,14 +1025,19 @@ system_state_default_action() {
}
system_state_problem_lines() {
local mixed_port
load_system_state
mixed_port="$(runtime_mixed_port_bind_failure_port 2>/dev/null || status_read_mixed_port 2>/dev/null || true)"
case "$SYSTEM_STATE" in
ready)
return 0
;;
stopped)
if [ "$SUBSCRIPTION_STATE" = "missing" ]; then
if [ "${BIND_FAILURE_STATE:-none}" != "none" ]; then
echo "• $(runtime_mixed_port_bind_failure_text)${mixed_port:+:${mixed_port}}"
elif [ "$SUBSCRIPTION_STATE" = "missing" ]; then
echo "• 当前没有可用订阅"
else
echo "• 代理内核未启动"
@@ -783,6 +1048,7 @@ system_state_problem_lines() {
[ "$SUBSCRIPTION_STATE" = "degraded" ] && echo "• 当前主订阅健康异常"
;;
broken)
[ "${BIND_FAILURE_STATE:-none}" != "none" ] && echo "• $(runtime_mixed_port_bind_failure_text)${mixed_port:+:${mixed_port}}"
[ "$BUILD_STATE" = "failed" ] && echo "• 最近一次编译失败"
[ "$SUBSCRIPTION_STATE" = "missing" ] && echo "• 当前没有主订阅"
[ "$SUBSCRIPTION_STATE" = "invalid" ] && echo "• 当前主订阅无效"
@@ -795,6 +1061,11 @@ system_state_problem_lines() {
system_state_recommendation_lines() {
load_system_state
if [ "${BIND_FAILURE_STATE:-none}" != "none" ]; then
mixed_port_bind_recommendation_lines numbered
return 0
fi
case "$SYSTEM_STATE" in
ready)
echo "1. clashctl select"
@@ -1457,11 +1728,14 @@ status_risk_reason_lines() {
local build_block_reason build_block_time
local fallback_used fallback_time fallback_reason
local tun_enabled tun_effective tun_container_mode tun_kernel_support tun_verify_reason
local bind_failure_text mixed_port
build_status="$(status_build_last_status 2>/dev/null || true)"
active="$(active_subscription_name 2>/dev/null || true)"
controller_ok="false"
proxy_controller_reachable 2>/dev/null && controller_ok="true"
bind_failure_text="$(runtime_mixed_port_bind_failure_text 2>/dev/null || true)"
mixed_port="$(runtime_mixed_port_bind_failure_port 2>/dev/null || status_read_mixed_port 2>/dev/null || true)"
build_block_reason="$(status_runtime_last_build_block_reason 2>/dev/null || true)"
build_block_time="$(status_runtime_last_build_block_time 2>/dev/null || true)"
@@ -1475,7 +1749,12 @@ status_risk_reason_lines() {
tun_verify_reason="$(status_tun_last_verify_reason 2>/dev/null || true)"
if ! status_is_running; then
echo "• 代理内核未启动"
if [ -n "${bind_failure_text:-}" ]; then
echo "• ${bind_failure_text}${mixed_port:+:${mixed_port}}"
mixed_port_bind_observation_line | sed 's/^/• /'
else
echo "• 代理内核未启动"
fi
fi
if [ "$controller_ok" = "false" ] && status_is_running; then
@@ -1535,8 +1814,15 @@ status_recommendation_lines() {
status_current_proxy_brief() {
local default_group default_current fallback_group fallback_current
local bind_failure_text
if ! status_is_running; then
bind_failure_text="$(runtime_mixed_port_bind_failure_text 2>/dev/null || true)"
if [ -n "${bind_failure_text:-}" ]; then
echo "$bind_failure_text"
return 0
fi
echo "未启动"
return 0
fi
@@ -1576,8 +1862,25 @@ system_proxy_supported_state() {
connectivity_issue_code() {
local active group_count
local bind_failure_kind
if ! status_is_running; then
bind_failure_kind="$(runtime_mixed_port_bind_failure_kind 2>/dev/null || true)"
case "${bind_failure_kind:-}" in
bind_denied)
echo "mixed_port_bind_denied"
return 0
;;
address_in_use)
echo "mixed_port_address_in_use"
return 0
;;
bind_failed)
echo "mixed_port_bind_failed"
return 0
;;
esac
echo "runtime_stopped"
return 0
fi
@@ -1630,8 +1933,16 @@ connectivity_issue_code() {
}
connectivity_issue_text() {
case "$(connectivity_issue_code)" in
local issue port
issue="$(connectivity_issue_code)"
port="$(runtime_mixed_port_bind_failure_port 2>/dev/null || status_read_mixed_port 2>/dev/null || true)"
case "$issue" in
ok) echo "可用(代理链路已闭环)" ;;
mixed_port_bind_denied) echo "不可用(mixed-port ${port:-unknown} 绑定被拒绝)" ;;
mixed_port_address_in_use) echo "不可用(mixed-port ${port:-unknown} 端口被占用)" ;;
mixed_port_bind_failed) echo "不可用(mixed-port ${port:-unknown} 绑定失败)" ;;
runtime_stopped) echo "不可用(代理内核未启动)" ;;
controller_unreachable) echo "异常(内核已运行,但控制器不可访问)" ;;
config_invalid) echo "异常(当前运行配置不可用)" ;;
@@ -1652,6 +1963,9 @@ connectivity_next_action() {
runtime_stopped)
echo "clashon"
;;
mixed_port_bind_denied|mixed_port_address_in_use|mixed_port_bind_failed)
mixed_port_bind_next_action 2>/dev/null || echo "clashctl logs mihomo"
;;
controller_unreachable)
echo "clashctl doctor"
;;
@@ -1683,6 +1997,7 @@ connectivity_evidence_lines() {
local runtime_running controller_ok build_status subscription_status
local group_count expected_proxy actual_proxy active config_source
local system_proxy_state system_proxy_supported_text
local bind_failure_kind bind_failure_line
if status_is_running; then
runtime_running="true"
@@ -1705,9 +2020,13 @@ connectivity_evidence_lines() {
actual_proxy="$(system_proxy_http_value 2>/dev/null || true)"
system_proxy_state="$(system_proxy_status 2>/dev/null || echo off)"
system_proxy_supported_text="$(system_proxy_supported_state)"
bind_failure_kind="$(runtime_mixed_port_bind_failure_kind 2>/dev/null || true)"
bind_failure_line="$(runtime_mixed_port_bind_failure_line 2>/dev/null || true)"
echo "• runtime_running = ${runtime_running:-false}"
echo "• controller_reachable = ${controller_ok:-false}"
[ -n "${bind_failure_kind:-}" ] && echo "• mixed_port_bind_failure = ${bind_failure_kind}"
[ -n "${bind_failure_line:-}" ] && echo "• mixed_port_bind_log = ${bind_failure_line}"
echo "• build_status = ${build_status:-unknown}"
echo "• subscription_status = ${subscription_status:-unknown}"
echo "• active_subscription = ${active:-unset}"
@@ -1904,7 +2223,7 @@ print_status_summary_compact() {
local profile mixed_port controller controller_lan controller_public
local running_text user_connectivity user_risk current_proxy_brief system_proxy_text
local current_active dashboard_text dashboard_source_text dashboard_policy_text secret_text
local tun_text
local tun_text bind_failure_text next_action
profile="$(show_active_profile 2>/dev/null || true)"
[ -n "${profile:-}" ] || profile="default"
@@ -1915,9 +2234,12 @@ print_status_summary_compact() {
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)"
bind_failure_text="$(runtime_mixed_port_bind_failure_text 2>/dev/null || true)"
if status_is_running; then
running_text="🐱 已开启"
elif [ -n "${bind_failure_text:-}" ]; then
running_text="❗ ${bind_failure_text}"
else
running_text="❗ 未开启"
fi
@@ -1925,6 +2247,7 @@ print_status_summary_compact() {
user_connectivity="$(connectivity_issue_text)"
user_risk="$(status_user_risk_text)"
current_proxy_brief="$(status_current_proxy_brief)"
next_action="$(system_state_default_action 2>/dev/null || echo 'clashctl status --verbose')"
if system_proxy_supported; then
system_proxy_text="$(system_proxy_status 2>/dev/null || echo off)"
else
@@ -1961,7 +2284,11 @@ print_status_summary_compact() {
echo "📡 当前订阅:${current_active:-未设置}"
echo "🚀 当前节点:$current_proxy_brief"
echo "🚨 当前风险:$user_risk"
echo "👉 clashctl select 切换节点"
if [ "$next_action" = "clashctl select" ]; then
echo "👉 clashctl select 切换节点"
else
echo "👉 下一步:$next_action"
fi
echo
echo "【核心入口】"
@@ -2013,6 +2340,7 @@ print_status_summary_verbose() {
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 system_proxy_text dashboard_text dashboard_source_text dashboard_policy_text secret_text
local bind_failure_text next_action
profile="$(show_active_profile 2>/dev/null || true)"
[ -n "${profile:-}" ] || profile="default"
@@ -2043,9 +2371,12 @@ print_status_summary_verbose() {
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)"
bind_failure_text="$(runtime_mixed_port_bind_failure_text 2>/dev/null || true)"
if status_is_running; then
running_text="🐱 已开启"
elif [ -n "${bind_failure_text:-}" ]; then
running_text="❗ ${bind_failure_text}"
else
running_text="❗ 未开启"
fi
@@ -2060,6 +2391,7 @@ print_status_summary_verbose() {
user_connectivity="$(connectivity_issue_text)"
user_risk="$(status_user_risk_text)"
current_proxy_brief="$(status_current_proxy_brief)"
next_action="$(system_state_default_action 2>/dev/null || echo 'clashctl status --verbose')"
install_backend_text="$(status_runtime_backend_text)"
install_container_text="$(status_container_mode_text)"
install_verify_text="$(status_install_verify_brief)"
@@ -2109,7 +2441,11 @@ print_status_summary_verbose() {
echo "📡 当前订阅:${current_active:-未设置}"
echo "🚀 当前节点:$current_proxy_brief"
echo "🚨 当前风险:$user_risk"
echo "👉 clashctl select 切换节点"
if [ "$next_action" = "clashctl select" ]; then
echo "👉 clashctl select 切换节点"
else
echo "👉 下一步:$next_action"
fi
echo
echo "【核心入口】"
@@ -3055,7 +3391,7 @@ doctor_service() {
}
doctor_ports() {
local config_file mixed_port controller controller_port
local config_file mixed_port controller controller_port dns_port bind_failure_kind
doctor_print_title "端口检查"
@@ -3081,9 +3417,17 @@ doctor_ports() {
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##*:}"
dns_port="$(runtime_config_dns_port 2>/dev/null || true)"
bind_failure_kind="$(runtime_mixed_port_bind_failure_kind 2>/dev/null || true)"
if [ -n "${mixed_port:-}" ] && [ "$mixed_port" != "null" ]; then
if is_port_in_use "$mixed_port"; then
if [ "$bind_failure_kind" = "bind_denied" ]; then
doctor_fail "mixed-port 绑定被拒绝:$mixed_port"
elif [ "$bind_failure_kind" = "address_in_use" ]; then
doctor_fail "mixed-port 端口被占用:$mixed_port"
elif [ -n "${bind_failure_kind:-}" ]; then
doctor_fail "mixed-port 绑定失败:$mixed_port"
elif is_port_in_use "$mixed_port"; then
doctor_ok "代理端口已监听:$mixed_port"
else
doctor_warn "代理端口未监听:$mixed_port"
@@ -3102,6 +3446,16 @@ doctor_ports() {
doctor_warn "无法检查控制器端口"
fi
if [ -n "${dns_port:-}" ] && [ "$dns_port" != "null" ]; then
if is_port_in_use "$dns_port"; then
doctor_ok "DNS 端口已监听:$dns_port"
else
doctor_warn "DNS 端口未监听:$dns_port"
fi
else
doctor_warn "无法检查 DNS 端口"
fi
if [ -x "$(subconverter_bin)" ]; then
if is_port_in_use "$(subconverter_port)"; then
doctor_ok "subconverter 端口已监听:$(subconverter_port)"
@@ -3277,11 +3631,12 @@ doctor_active_switch() {
}
doctor_controller() {
local controller group_count current_examples
local controller group_count current_examples bind_failure_text
doctor_print_title "控制器检查"
controller="$(status_read_controller 2>/dev/null || true)"
bind_failure_text="$(runtime_mixed_port_bind_failure_text 2>/dev/null || true)"
if [ -z "${controller:-}" ] || [ "$controller" = "null" ]; then
doctor_fail "未解析到 external-controller"
@@ -3291,7 +3646,11 @@ doctor_controller() {
doctor_ok "控制器地址:$(display_controller_local_addr "$controller" 2>/dev/null || echo "$controller")"
if ! status_is_running; then
doctor_warn "内核未运行,无法检查控制器 API"
if [ -n "${bind_failure_text:-}" ]; then
doctor_warn "${bind_failure_text},无法检查控制器 API"
else
doctor_warn "内核未运行,无法检查控制器 API"
fi
return 0
fi
@@ -3928,6 +4287,11 @@ doctor_risk_level() {
proxy_controller_reachable 2>/dev/null && controller_ok="true"
runtime_config_exists && config_ok="true"
if runtime_mixed_port_bind_failure_kind >/dev/null 2>&1; then
echo "high"
return
fi
if [ "$running" = "true" ] && [ "$controller_ok" = "true" ] && [ "$config_ok" = "true" ]; then
echo "low"
return
@@ -3956,15 +4320,19 @@ doctor_risk_text() {
}
doctor_problem_lines() {
local active_sub
local active_sub bind_failure_text mixed_port
active_sub="$(active_subscription_name 2>/dev/null || true)"
bind_failure_text="$(runtime_mixed_port_bind_failure_text 2>/dev/null || true)"
mixed_port="$(runtime_mixed_port_bind_failure_port 2>/dev/null || status_read_mixed_port 2>/dev/null || true)"
if ! runtime_config_exists; then
echo "🚨 运行配置缺失"
fi
if ! status_is_running; then
if [ -n "${bind_failure_text:-}" ]; then
echo "🚨 ${bind_failure_text}${mixed_port:+:${mixed_port}}"
elif ! status_is_running; then
echo "🚨 代理内核未运行"
fi
@@ -3988,6 +4356,24 @@ doctor_primary_conclusion() {
fi
if ! status_is_running; then
case "$(runtime_mixed_port_bind_failure_kind 2>/dev/null || true)" in
bind_denied)
echo "❗ 当前不可用:mixed-port 绑定被拒绝"
mixed_port_bind_observation_line | sed -n '1p' | sed 's/^/ /'
return 0
;;
address_in_use)
echo "❗ 当前不可用:mixed-port 端口被占用"
mixed_port_bind_observation_line | sed -n '1p' | sed 's/^/ /'
return 0
;;
bind_failed)
echo "❗ 当前不可用:mixed-port 绑定失败"
mixed_port_bind_observation_line | sed -n '1p' | sed 's/^/ /'
return 0
;;
esac
echo "🟡 当前未连接:代理内核未启动"
return 0
fi
@@ -4001,9 +4387,10 @@ doctor_primary_conclusion() {
}
doctor_recommendation_lines() {
local active_sub
local active_sub bind_failure_kind
active_sub="$(active_subscription_name 2>/dev/null || true)"
bind_failure_kind="$(runtime_mixed_port_bind_failure_kind 2>/dev/null || true)"
if ! runtime_config_exists; then
if [ -n "$(subscription_url 2>/dev/null || true)" ]; then
@@ -4014,7 +4401,10 @@ doctor_recommendation_lines() {
return 0
fi
if ! status_is_running; then
if [ -n "${bind_failure_kind:-}" ]; then
mixed_port_bind_recommendation_lines icon
return 0
elif ! status_is_running; then
echo "💡 clashon"
return 0
fi
@@ -4042,11 +4432,14 @@ doctor_recommendation_lines() {
}
doctor_evidence_lines() {
local active_sub mixed_port controller
local active_sub mixed_port controller bind_failure_kind bind_failure_line dns_port
active_sub="$(active_subscription_name 2>/dev/null || true)"
mixed_port="$(status_read_mixed_port 2>/dev/null || true)"
mixed_port="$(runtime_mixed_port_bind_failure_port 2>/dev/null || status_read_mixed_port 2>/dev/null || true)"
controller="$(status_read_controller 2>/dev/null || true)"
bind_failure_kind="$(runtime_mixed_port_bind_failure_kind 2>/dev/null || true)"
bind_failure_line="$(runtime_mixed_port_bind_failure_line 2>/dev/null || true)"
dns_port="$(runtime_config_dns_port 2>/dev/null || true)"
if runtime_config_exists; then
echo "🔍 运行配置:存在"
@@ -4066,6 +4459,24 @@ doctor_evidence_lines() {
echo "🔍 控制器状态:不可访问"
fi
if [ -n "${bind_failure_kind:-}" ]; then
echo "🔍 mixed-port 绑定错误:${bind_failure_kind}"
echo "🔍 mixed-port 环境:$(mixed_port_bind_environment_text 2>/dev/null || echo unknown)"
if runtime_mixed_port_controller_listening 2>/dev/null; then
echo "🔍 控制器监听:已确认"
else
echo "🔍 控制器监听:未确认"
fi
if [ -n "${dns_port:-}" ]; then
if runtime_mixed_port_dns_listening 2>/dev/null; then
echo "🔍 DNS 监听:${dns_port}(已确认)"
else
echo "🔍 DNS 监听:${dns_port}(未确认)"
fi
fi
[ -n "${bind_failure_line:-}" ] && echo "🔍 日志证据:${bind_failure_line}"
fi
if [ -n "${active_sub:-}" ]; then
if active_subscription_enabled 2>/dev/null; then
echo "🔍 当前订阅:${active_sub}(可用)"
-10
View File
@@ -812,13 +812,8 @@ 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_arg" \
--show-error \
@@ -936,13 +931,8 @@ 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_arg" \
--show-error \