refactor: rebuild project architecture and codebase

This commit is contained in:
wnlen
2026-04-08 15:53:52 +08:00
parent 69e4d8bc36
commit bc97b0a2cd
1684 changed files with 11768 additions and 427871 deletions
+198
View File
@@ -0,0 +1,198 @@
#!/usr/bin/env bash
_clashctl_real() {
if command -v clashctl-bin >/dev/null 2>&1; then
clashctl-bin "$@"
return $?
fi
command clashctl "$@"
}
_clash_alias_print_sep() {
echo
}
_clash_alias_proxy_on() {
eval "$(_clashctl_real proxy on)" || return $?
}
_clash_alias_proxy_off() {
eval "$(_clashctl_real proxy off)" || true
}
_clash_alias_proxy_show() {
_clashctl_real proxy show 2>/dev/null || true
}
_clash_alias_status_next() {
_clashctl_real status-next 2>/dev/null || echo "clashctl status"
}
_clash_alias_prepare_on() {
# 这里不直接自己做 regenerate / restart / fallback,
# 而是统一交给 clashctl on 主链处理,避免双执行链。
# shell 层只负责“闭环体验”,不抢 runtime / build 的职责。
return 0
}
_clash_alias_after_on() {
_clash_alias_proxy_on || return $?
_clash_alias_print_sep
_clash_alias_proxy_show
echo "👉 下一步:$(_clash_alias_status_next)"
}
_clash_alias_after_off() {
_clash_alias_print_sep
echo "🔴 已关闭代理环境"
echo "🧹 当前 Shell 代理变量已清理"
echo "👉 下一步:clashctl status"
}
_clash_alias_run_on() {
_clash_alias_prepare_on || return $?
_clashctl_real on "$@" || return $?
_clash_alias_after_on || return $?
}
_clash_alias_run_off() {
_clash_alias_proxy_off
_clashctl_real off "$@" || return $?
}
clashctl() {
case "${1:-}" in
on)
shift || true
_clash_alias_run_on "$@"
;;
off)
shift || true
_clash_alias_run_off "$@"
;;
proxy)
case "${2:-}" in
on)
_clash_alias_proxy_on || return $?
_clash_alias_print_sep
_clash_alias_proxy_show
;;
off)
_clash_alias_proxy_off
_clash_alias_print_sep
echo "🧹 当前 Shell 代理变量已清理"
;;
*)
_clashctl_real "$@"
;;
esac
;;
ui)
shift || true
# ui 前如果 runtime 已运行但当前 shell 没代理,不强制注入;
# 保持 UI 行为纯粹,只走原命令。
_clashctl_real ui "$@"
;;
status)
shift || true
_clashctl_real status "$@"
;;
*)
_clashctl_real "$@"
;;
esac
}
# 快捷入口全部收敛到 clashctl 函数
clashon() {
clashctl on "$@" || return $?
}
clashoff() {
clashctl off "$@" || return $?
}
clashproxy() {
case "${1:-show}" in
on)
clashctl proxy on
;;
off)
clashctl proxy off
;;
show|status)
clashctl proxy show
;;
groups)
clashctl proxy groups
;;
current)
shift || true
clashctl proxy current "$@"
;;
nodes)
shift || true
clashctl proxy nodes "$@"
;;
select)
shift || true
clashctl proxy select "$@"
;;
*)
echo "🧭 用法:clashproxy [show|on|off|groups|current|nodes|select]"
echo "💡 主路径切节点请使用:clashselect 或 clashctl select"
return 2
;;
esac
}
clashls() {
clashctl ls "$@"
}
clashselect() {
clashctl select "$@"
}
clashui() {
clashctl ui "$@"
}
clashsecret() {
clashctl secret "$@"
}
clashtun() {
clashctl tun "$@"
}
clashupgrade() {
clashctl upgrade "$@"
}
clashmixin() {
case "${1:-}" in
-e|--edit)
clashctl mixin edit
;;
-c|--raw)
clashctl mixin raw
;;
-r|--runtime)
clashctl mixin runtime
;;
"")
clashctl mixin
;;
*)
clashctl mixin "$@"
;;
esac
}
# shell 被 source 时不自动执行任何代理动作
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+273
View File
@@ -0,0 +1,273 @@
#!/usr/bin/env bash
# shellcheck source=scripts/core/common.sh
source "${PROJECT_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}/scripts/core/common.sh"
proxy_host() {
echo "${CLASH_PROXY_HOST:-127.0.0.1}"
}
proxy_port() {
local config_file="${1:-$RUNTIME_DIR/config.yaml}"
local port
[ -s "$config_file" ] || die "配置文件不存在:$config_file"
port="$("$(yq_bin)" eval '.["mixed-port"] // .port // ""' "$config_file" 2>/dev/null | head -n 1)"
[ -n "${port:-}" ] && [ "$port" != "null" ] || die "未在配置文件中找到代理端口"
echo "$port"
}
proxy_http_url() {
echo "http://$(proxy_host):$(proxy_port)"
}
proxy_socks_url() {
echo "socks5://$(proxy_host):$(proxy_port)"
}
proxy_no_proxy_value() {
echo "${NO_PROXY_DEFAULT:-127.0.0.1,localhost,::1}"
}
print_proxy_show() {
echo
echo "😼 当前代理环境"
echo
echo "🌐 HTTP:$(proxy_http_url)"
echo "🧦 SOCKS5:$(proxy_socks_url)"
echo "🚫 NO_PROXY:$(proxy_no_proxy_value)"
echo
}
print_proxy_on_script() {
local http_url socks_url no_proxy
http_url="$(proxy_http_url)"
socks_url="$(proxy_socks_url)"
no_proxy="$(proxy_no_proxy_value)"
cat <<EOF
export http_proxy="$http_url"
export https_proxy="$http_url"
export HTTP_PROXY="$http_url"
export HTTPS_PROXY="$http_url"
export all_proxy="$socks_url"
export ALL_PROXY="$socks_url"
export no_proxy="$no_proxy"
export NO_PROXY="$no_proxy"
EOF
}
print_proxy_off_script() {
cat <<'EOF'
unset http_proxy
unset https_proxy
unset HTTP_PROXY
unset HTTPS_PROXY
unset all_proxy
unset ALL_PROXY
unset no_proxy
unset NO_PROXY
EOF
}
controller_addr() {
local config_file="${1:-$RUNTIME_DIR/config.yaml}"
[ -s "$config_file" ] || die "配置文件不存在:$config_file"
"$(yq_bin)" eval '.["external-controller"] // ""' "$config_file" 2>/dev/null | head -n 1
}
controller_secret() {
local config_file="${1:-$RUNTIME_DIR/config.yaml}"
[ -s "$config_file" ] || die "配置文件不存在:$config_file"
"$(yq_bin)" eval '.secret // ""' "$config_file" 2>/dev/null | head -n 1
}
controller_api_base() {
local addr
addr="$(controller_addr)"
[ -n "${addr:-}" ] || die "未找到 external-controller"
[ "$addr" != "null" ] || die "未找到 external-controller"
echo "http://$addr"
}
controller_curl() {
local method="$1"
local path="$2"
local data="${3:-}"
local base secret
base="$(controller_api_base)"
secret="$(controller_secret)"
if [ -n "${data:-}" ]; then
curl -fsSL -X "$method" \
-H "Content-Type: application/json" \
${secret:+-H "Authorization: Bearer $secret"} \
--data "$data" \
"$base$path"
else
curl -fsSL -X "$method" \
${secret:+-H "Authorization: Bearer $secret"} \
"$base$path"
fi
}
proxy_controller_reachable() {
controller_curl GET "/version" >/dev/null 2>&1
}
proxy_groups_json() {
controller_curl GET "/proxies"
}
proxy_group_exists() {
local group="$1"
[ -n "${group:-}" ] || return 1
[ "$(proxy_groups_json | "$(yq_bin)" -p=json eval ".proxies | has(\"$group\")" - 2>/dev/null)" = "true" ]
}
proxy_group_type() {
local group="$1"
[ -n "${group:-}" ] || die "策略组名称不能为空"
proxy_group_exists "$group" || die "策略组不存在:$group"
proxy_groups_json | "$(yq_bin)" -p=json eval ".proxies.\"$group\".type // \"\"" - 2>/dev/null
}
proxy_group_is_selector() {
local type
type="$(proxy_group_type "$1")"
case "$type" in
Selector|URLTest|Fallback|LoadBalance)
return 0
;;
*)
return 1
;;
esac
}
proxy_group_list() {
proxy_groups_json \
| "$(yq_bin)" -p=json eval '
.proxies
| to_entries
| map(select(.value.all != null))
| .[].key
' - 2>/dev/null
}
proxy_group_current() {
local group="$1"
[ -n "${group:-}" ] || die "策略组名称不能为空"
proxy_group_exists "$group" || die "策略组不存在:$group"
proxy_groups_json \
| "$(yq_bin)" -p=json eval ".proxies.\"$group\".now // \"\"" - 2>/dev/null
}
proxy_group_nodes() {
local group="$1"
[ -n "${group:-}" ] || die "策略组名称不能为空"
proxy_group_exists "$group" || die "策略组不存在:$group"
proxy_groups_json \
| "$(yq_bin)" -p=json eval ".proxies.\"$group\".all[] // \"\"" - 2>/dev/null
}
proxy_group_select() {
local group="$1"
local node="$2"
[ -n "${group:-}" ] || die "策略组名称不能为空"
[ -n "${node:-}" ] || die "节点名称不能为空"
proxy_group_exists "$group" || die "策略组不存在:$group"
proxy_group_is_selector "$group" || die "该策略组不支持手动切换:$group"
if ! proxy_group_nodes "$group" | grep -Fxq "$node"; then
die "节点不存在于策略组中:$group -> $node"
fi
controller_curl PUT "/proxies/$group" "{\"name\":\"$node\"}" >/dev/null
}
proxy_group_count() {
proxy_group_list 2>/dev/null | awk 'NF{c++} END{print c+0}'
}
default_proxy_group_name() {
local config_file="${1:-$RUNTIME_DIR/config.yaml}"
local group_name
[ -s "$config_file" ] || return 1
group_name="$("$(yq_bin)" eval '
.["proxy-groups"][] |
select(
(.type == "select") or
(.type == "url-test") or
(.type == "fallback") or
(.type == "load-balance")
) |
.name
' "$config_file" 2>/dev/null | head -n 1)"
[ -n "${group_name:-}" ] || return 1
echo "$group_name"
}
default_proxy_group_current() {
local group
group="$(default_proxy_group_name 2>/dev/null || true)"
[ -n "${group:-}" ] || return 1
proxy_group_current "$group" 2>/dev/null || return 1
}
print_proxy_groups_status() {
local group current
while IFS= read -r group; do
[ -n "${group:-}" ] || continue
current="$(proxy_group_current "$group" 2>/dev/null || true)"
if [ -n "${current:-}" ]; then
echo "$group -> $current"
else
echo "$group -> <unknown>"
fi
done < <(proxy_group_list)
}
print_proxy_groups_summary() {
local group current
while IFS= read -r group; do
[ -n "${group:-}" ] || continue
current="$(proxy_group_current "$group" 2>/dev/null || true)"
if [ -n "${current:-}" ]; then
echo "$group -> $current"
else
echo "$group"
fi
done < <(proxy_group_list)
}
+389
View File
@@ -0,0 +1,389 @@
#!/usr/bin/env bash
# shellcheck source=scripts/core/common.sh
source "${PROJECT_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}/scripts/core/common.sh"
# shellcheck source=scripts/core/config.sh
source "${PROJECT_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}/scripts/core/config.sh"
resolve_yq() {
local arch version file url tmp_dir tmp_file
arch="$(get_arch)"
version="${YQ_VERSION:-$DEFAULT_YQ_VERSION}"
if [ -x "$(yq_bin)" ]; then
return 0
fi
case "$arch" in
amd64) file="yq_linux_amd64.tar.gz" ;;
arm64) file="yq_linux_arm64.tar.gz" ;;
armv7) file="yq_linux_arm.tar.gz" ;;
*) die "暂不支持的 yq 架构:$arch" ;;
esac
url="https://github.com/mikefarah/yq/releases/download/${version}/${file}"
tmp_dir="$(mktemp -d)"
tmp_file="$tmp_dir/$file"
download_file "$url" "$tmp_file" "yq"
tar -xzf "$tmp_file" -C "$tmp_dir"
if [ -f "$tmp_dir/yq_linux_${arch}" ]; then
install -m 0755 "$tmp_dir/yq_linux_${arch}" "$(yq_bin)"
elif [ -f "$tmp_dir/yq_linux_arm" ]; then
install -m 0755 "$tmp_dir/yq_linux_arm" "$(yq_bin)"
elif [ -f "$tmp_dir/yq" ]; then
install -m 0755 "$tmp_dir/yq" "$(yq_bin)"
else
rm -rf "$tmp_dir"
die "解压后未找到 yq 可执行文件"
fi
rm -rf "$tmp_dir"
}
resolve_mihomo() {
local arch version file url tmp_file
arch="$(get_arch)"
version="${MIHOMO_VERSION:-$DEFAULT_MIHOMO_VERSION}"
if [ -x "$(mihomo_bin)" ]; then
return 0
fi
case "$arch" in
amd64) file="mihomo-linux-amd64-compatible-${version}.gz" ;;
arm64) file="mihomo-linux-arm64-${version}.gz" ;;
armv7) file="mihomo-linux-armv7-${version}.gz" ;;
*) die "暂不支持的 Mihomo 架构:$arch" ;;
esac
url="https://github.com/MetaCubeX/mihomo/releases/download/${version}/${file}"
tmp_file="$(mktemp)"
download_file "$url" "$tmp_file" "mihomo"
gzip -dc "$tmp_file" > "$(mihomo_bin)"
chmod +x "$(mihomo_bin)"
rm -f "$tmp_file"
}
resolve_clash() {
local arch version version_no_v url_base tmp_file url downloaded="false"
local candidates file
arch="$(get_arch)"
version="${CLASH_VERSION:-$DEFAULT_CLASH_VERSION}"
version_no_v="${version#v}"
url_base="${CLASH_DOWNLOAD_BASE:-https://github.com/WindSpiritSR/clash/releases/download}"
if [ -x "$(clash_bin)" ] \
&& [ "$(read_runtime_value "KERNEL_TYPE_INSTALLED" 2>/dev/null || true)" = "clash" ] \
&& [ "$(read_runtime_value "CLASH_VERSION_INSTALLED" 2>/dev/null || true)" = "$version" ]; then
return 0
fi
case "$arch" in
amd64)
candidates="
clash-linux-amd64-${version}.gz
clash-linux-amd64-v${version_no_v}.gz
"
;;
arm64)
candidates="
clash-linux-arm64-${version}.gz
clash-linux-arm64-v${version_no_v}.gz
clash-linux-armv8-${version}.gz
clash-linux-armv8-v${version_no_v}.gz
"
;;
armv7)
candidates="
clash-linux-armv7-${version}.gz
clash-linux-armv7-v${version_no_v}.gz
"
;;
*)
die "暂不支持的 Clash 架构:$arch"
;;
esac
tmp_file="$(mktemp)"
rm -f "$tmp_file"
for file in $candidates; do
url="${url_base}/${version}/${file}"
if download_file "$url" "$tmp_file" "clash"; then
downloaded="true"
break
fi
done
[ "$downloaded" = "true" ] || {
rm -f "$tmp_file"
die "Clash 内核下载失败,请检查版本或在 .env 中覆盖 CLASH_DOWNLOAD_BASE / CLASH_VERSION"
}
gzip -dc "$tmp_file" > "$(clash_bin)"
chmod +x "$(clash_bin)"
rm -f "$tmp_file"
write_runtime_value "KERNEL_TYPE_INSTALLED" "clash"
write_runtime_value "CLASH_VERSION_INSTALLED" "$version"
}
resolve_runtime_kernel() {
case "$(runtime_kernel_type)" in
mihomo)
resolve_mihomo
write_runtime_value "KERNEL_TYPE_INSTALLED" "mihomo"
write_runtime_value "MIHOMO_VERSION_INSTALLED" "${MIHOMO_VERSION:-$DEFAULT_MIHOMO_VERSION}"
;;
clash)
resolve_clash
;;
*)
die "未知内核类型:$(runtime_kernel_type)"
;;
esac
}
subconverter_version_file_value() {
read_runtime_value "SUBCONVERTER_VERSION_INSTALLED" 2>/dev/null || true
}
mark_subconverter_version_installed() {
local version="$1"
write_runtime_value "SUBCONVERTER_VERSION_INSTALLED" "$version"
}
resolve_subconverter() {
local arch version file url tmp_dir tmp_file target_dir installed_version
arch="$(get_arch)"
version="${SUBCONVERTER_VERSION:-$DEFAULT_SUBCONVERTER_VERSION}"
target_dir="$(subconverter_home)"
installed_version="$(subconverter_version_file_value)"
if [ -x "$(subconverter_bin)" ] && [ "$installed_version" = "$version" ]; then
return 0
fi
case "$arch" in
amd64) file="subconverter_linux64.tar.gz" ;;
arm64) file="subconverter_aarch64.tar.gz" ;;
armv7) file="subconverter_armv7.tar.gz" ;;
*) die "暂不支持的 subconverter 架构:$arch" ;;
esac
url="https://github.com/tindy2013/subconverter/releases/download/${version}/${file}"
tmp_dir="$(mktemp -d)"
tmp_file="$tmp_dir/$file"
download_file "$url" "$tmp_file" "subconverter"
rm -rf "$target_dir"
mkdir -p "$target_dir"
tar -xzf "$tmp_file" -C "$target_dir"
[ -x "$(subconverter_bin)" ] || {
rm -rf "$tmp_dir"
die "解压后未找到 subconverter 可执行文件"
}
chmod +x "$(subconverter_bin)"
mark_subconverter_version_installed "$version"
rm -rf "$tmp_dir"
}
ensure_runtime_config_ready() {
local config_file last_file now reason
config_file="$RUNTIME_DIR/config.yaml"
last_file="$RUNTIME_DIR/config.last.yaml"
if [ -s "$config_file" ] && test_runtime_config "$config_file" >/dev/null 2>&1; then
write_runtime_event_value "RUNTIME_LAST_FALLBACK_USED" "false"
write_runtime_event_value "RUNTIME_LAST_FALLBACK_TIME" ""
write_runtime_event_value "RUNTIME_LAST_FALLBACK_REASON" ""
write_runtime_event_value "RUNTIME_LAST_RISK_LEVEL" "$(calculate_runtime_risk_level)"
mark_runtime_config_source "runtime"
return 0
fi
reason="当前运行配置不可用"
warn "当前运行配置不可用,尝试回退到上一次成功配置"
[ -s "$last_file" ] || die "当前配置不可用,且不存在可回退的最后成功配置:$last_file"
if test_runtime_config "$last_file" >/dev/null 2>&1; then
cp -f "$last_file" "$config_file"
now="$(now_datetime)"
write_runtime_event_value "RUNTIME_LAST_FALLBACK_USED" "true"
write_runtime_event_value "RUNTIME_LAST_FALLBACK_TIME" "$now"
write_runtime_event_value "RUNTIME_LAST_FALLBACK_REASON" "$reason"
write_runtime_event_value "RUNTIME_LAST_RISK_LEVEL" "$(calculate_runtime_risk_level)"
mark_runtime_config_source "last_good"
success "已回退到最后成功配置"
return 0
fi
die "最后成功配置也不可用:$last_file"
}
tun_effective_check() {
local config_tun_enabled auto_route controller_ok route_ok
config_tun_enabled="$(runtime_config_tun_enabled 2>/dev/null || true)"
if [ "$(tun_enabled)" != "true" ]; then
echo "disabled-in-state"
return 1
fi
if [ "${config_tun_enabled:-false}" != "true" ]; then
echo "disabled-in-runtime-config"
return 1
fi
if ! status_is_running 2>/dev/null; then
echo "runtime-not-running"
return 1
fi
controller_ok="false"
if proxy_controller_reachable 2>/dev/null; then
controller_ok="true"
fi
if [ "$controller_ok" != "true" ]; then
echo "controller-unreachable"
return 1
fi
auto_route="$(runtime_config_tun_auto_route 2>/dev/null || true)"
route_ok="unknown"
if [ "${auto_route:-false}" = "true" ]; then
if default_route_is_tun_like; then
route_ok="true"
else
route_ok="false"
fi
if [ "$route_ok" != "true" ]; then
echo "default-route-not-tun"
return 1
fi
fi
echo "ok"
return 0
}
tun_disable_check() {
local config_tun_enabled
config_tun_enabled="$(runtime_config_tun_enabled 2>/dev/null || true)"
if [ "$(tun_enabled)" = "true" ]; then
echo "state-still-enabled"
return 1
fi
if [ "${config_tun_enabled:-false}" = "true" ]; then
echo "runtime-config-still-enabled"
return 1
fi
if status_is_running 2>/dev/null; then
if ! proxy_controller_reachable 2>/dev/null; then
echo "runtime-restarted-but-controller-unreachable"
return 1
fi
fi
echo "ok"
return 0
}
start_runtime() {
local config_file="$RUNTIME_DIR/config.yaml"
resolve_runtime_kernel
ensure_runtime_config_ready
if [ -f "$RUNTIME_DIR/mihomo.pid" ]; then
local old_pid
old_pid="$(cat "$RUNTIME_DIR/mihomo.pid" 2>/dev/null || true)"
if [ -n "${old_pid:-}" ] && kill -0 "$old_pid" 2>/dev/null; then
warn "Mihomo 已在运行:pid=$old_pid"
return 0
fi
rm -f "$RUNTIME_DIR/mihomo.pid"
fi
nohup "$(runtime_kernel_bin)" -f "$config_file" -d "$RUNTIME_DIR" \
> "$LOG_DIR/mihomo.out.log" 2>&1 &
echo $! > "$RUNTIME_DIR/mihomo.pid"
success "$(runtime_kernel_name) 已启动:pid=$(cat "$RUNTIME_DIR/mihomo.pid")"
}
stop_runtime() {
local pid_file="$RUNTIME_DIR/mihomo.pid"
[ -f "$pid_file" ] || {
warn "Mihomo 当前未运行"
return 0
}
local pid
pid="$(cat "$pid_file" 2>/dev/null || true)"
if [ -n "${pid:-}" ] && kill -0 "$pid" 2>/dev/null; then
kill "$pid" 2>/dev/null || true
sleep 1
kill -9 "$pid" 2>/dev/null || true
fi
rm -f "$pid_file"
success "Mihomo 已停止"
}
runtime_status_text() {
if [ -f "$RUNTIME_DIR/mihomo.pid" ]; then
local pid
pid="$(cat "$RUNTIME_DIR/mihomo.pid" 2>/dev/null || true)"
if [ -n "${pid:-}" ] && kill -0 "$pid" 2>/dev/null; then
echo "运行中"
echo "进程号:$pid"
return 0
fi
fi
echo "未运行"
}
clean_runtime_state() {
stop_subconverter || true
stop_runtime || true
clear_build_meta || true
rm -f "$RUNTIME_DIR/config.yaml" 2>/dev/null || true
rm -f "$RUNTIME_DIR"/*.pid 2>/dev/null || true
rm -f "$RUNTIME_DIR"/*.lock 2>/dev/null || true
rm -rf "$LOG_DIR" 2>/dev/null || true
mkdir -p "$LOG_DIR"
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
mkdir -p "$RUNTIME_DIR" "$BIN_DIR" "$LOG_DIR"
}
+146
View File
@@ -0,0 +1,146 @@
#!/usr/bin/env bash
# shellcheck source=scripts/core/common.sh
source "${PROJECT_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}/scripts/core/common.sh"
# shellcheck source=scripts/core/runtime.sh
source "${PROJECT_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}/scripts/core/runtime.sh"
# shellcheck source=scripts/core/config.sh
source "${PROJECT_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}/scripts/core/config.sh"
ensure_git_ready() {
command -v git >/dev/null 2>&1 || die "当前系统缺少 git"
git -C "$PROJECT_DIR" rev-parse --is-inside-work-tree >/dev/null 2>&1 \
|| die "当前目录不是 Git 仓库:$PROJECT_DIR"
}
current_git_branch() {
git -C "$PROJECT_DIR" rev-parse --abbrev-ref HEAD 2>/dev/null
}
update_branch() {
local branch
branch="$(read_env_value "CLASH_UPDATE_BRANCH" 2>/dev/null || true)"
if [ -n "${branch:-}" ]; then
echo "$branch"
return 0
fi
current_git_branch
}
git_has_local_changes() {
[ -n "$(git -C "$PROJECT_DIR" status --porcelain --untracked-files=no 2>/dev/null)" ]
}
git_remote_name() {
echo "${CLASH_GIT_REMOTE:-origin}"
}
sync_runtime_dependencies() {
resolve_yq
resolve_runtime_kernel
resolve_subconverter
}
remove_mihomo_binary() {
rm -f "$(mihomo_bin)" 2>/dev/null || true
}
remove_clash_binary() {
rm -f "$(clash_bin)" 2>/dev/null || true
}
upgrade_runtime_kernel() {
local target_kernel="${1:-}"
local verbose="${2:-false}"
local was_running="false"
target_kernel="$(normalize_kernel_type "${target_kernel:-$(runtime_kernel_type)}")"
if status_is_running 2>/dev/null; then
was_running="true"
fi
write_runtime_kernel_type "$target_kernel"
if [ "$verbose" = "true" ]; then
info "当前架构:$(get_arch)"
info "目标内核:$target_kernel"
case "$target_kernel" in
mihomo)
info "目标版本:${MIHOMO_VERSION:-$DEFAULT_MIHOMO_VERSION}"
;;
clash)
info "目标版本:${CLASH_VERSION:-$DEFAULT_CLASH_VERSION}"
;;
esac
fi
case "$target_kernel" in
mihomo)
info "正在升级 mihomo 内核"
remove_mihomo_binary
resolve_mihomo
[ -x "$(mihomo_bin)" ] || die "mihomo 内核升级失败"
;;
clash)
info "正在升级 clash 内核"
remove_clash_binary
resolve_clash
[ -x "$(clash_bin)" ] || die "clash 内核升级失败"
;;
*)
die "未知内核类型:$target_kernel"
;;
esac
if [ "$was_running" = "true" ]; then
info "检测到当前内核正在运行,正在重启服务"
service_restart
success "内核升级成功,已重启生效"
else
success "内核升级成功"
fi
}
update_project_code() {
local force_mode="$1"
local regenerate_mode="$2"
local remote_name branch
ensure_git_ready
remote_name="$(git_remote_name)"
branch="$(update_branch)"
[ -n "${branch:-}" ] || die "无法识别当前分支"
info "远程仓库:$remote_name"
info "更新分支:$branch"
if git_has_local_changes && [ "$force_mode" != "true" ]; then
die "检测到本地有未提交改动,请先提交,或使用 clashctl update --force"
fi
info "正在获取最新代码"
git -C "$PROJECT_DIR" fetch "$remote_name" "$branch" || die "获取远程代码失败"
if git_has_local_changes && [ "$force_mode" = "true" ]; then
warn "检测到本地改动,正在强制覆盖"
git -C "$PROJECT_DIR" reset --hard "$remote_name/$branch" || die "强制覆盖失败"
else
git -C "$PROJECT_DIR" reset --hard "$remote_name/$branch" || die "更新失败"
fi
success "代码已更新到最新版本"
sync_runtime_dependencies
if [ "$regenerate_mode" = "true" ]; then
regenerate_config
fi
success "更新完成"
}