#!/usr/bin/env bash set -euo pipefail Server_Dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" Service_Name="clash-for-linux.service" PROFILED_FILE="/etc/profile.d/clash-for-linux.sh" ENV_FILE="$Server_Dir/.env" CONF_FILE="$Server_Dir/conf/config.yaml" TEMP_CONF_FILE="$Server_Dir/temp/config.yaml" log() { printf "%b\n" "$*"; } info() { log "\033[36m[INFO]\033[0m $*"; } ok() { log "\033[32m[OK]\033[0m $*"; } warn() { log "\033[33m[WARN]\033[0m $*"; } err() { log "\033[31m[ERROR]\033[0m $*"; } usage() { cat <<'EOF' Usage: clashctl COMMAND [OPTIONS] Commands: on 开启代理 off 关闭代理 status 内核状况 proxy 系统代理 ui Web 面板 secret Web 密钥 sub 订阅管理 upgrade 升级内核 tun Tun 模式 mixin Mixin 配置 Global Options: -h, --help 显示帮助信息 Examples: clashctl on clashctl off clashctl status clashctl proxy status clashctl ui clashctl secret clashctl sub show clashctl sub update clashctl tun status clashctl mixin status 💡 clashon 同 clashctl on,Tab 补全更方便! EOF } require_profiled() { if [ ! -f "$PROFILED_FILE" ]; then err "未安装 Shell 代理快捷命令:$PROFILED_FILE" exit 1 fi } has_systemd() { command -v systemctl >/dev/null 2>&1 } service_exists() { has_systemd || return 1 local load_state load_state="$(systemctl show "$Service_Name" --property=LoadState --value 2>/dev/null || true)" [ -n "$load_state" ] && [ "$load_state" != "not-found" ] } service_is_active() { service_exists || return 1 systemctl is-active --quiet "$Service_Name" } read_config_value() { local key="$1" local f for f in "$TEMP_CONF_FILE" "$CONF_FILE"; do if [ -f "$f" ]; then sed -nE "s/^[[:space:]]*${key}:[[:space:]]*//p" "$f" \ | head -n 1 \ | tr -d '\r' \ | sed -E 's/^"(.*)"$/\1/; s/^'\''(.*)'\''$/\1/' return 0 fi done return 1 } cmd_on() { require_profiled # shellcheck disable=SC1090 source "$PROFILED_FILE" proxy_on } cmd_off() { require_profiled # shellcheck disable=SC1090 source "$PROFILED_FILE" proxy_off } cmd_status() { local proc_info="" proc_info="$(ps -eo pid,ppid,cmd | grep -E '[c]lash-linux|[m]ihomo' || true)" echo "=== Clash Status ===" if service_exists; then local active_state enabled_state active_state="$(systemctl is-active "$Service_Name" 2>/dev/null || true)" enabled_state="$(systemctl is-enabled "$Service_Name" 2>/dev/null || true)" echo "Service: installed" echo "Active : ${active_state:-unknown}" echo "Enabled: ${enabled_state:-unknown}" if [ -n "$proc_info" ]; then echo echo "Process:" echo "$proc_info" fi return 0 fi if [ -n "$proc_info" ]; then warn "未检测到 systemd 服务,但发现 Clash 进程正在运行" echo echo "Process:" echo "$proc_info" return 0 fi warn "未检测到 systemd 服务,也未发现 Clash 进程" return 1 } cmd_proxy() { require_profiled # shellcheck disable=SC1090 source "$PROFILED_FILE" local sub="${1:-status}" case "$sub" in on) proxy_on ;; off) proxy_off ;; status) proxy_status ;; *) err "未知 proxy 子命令: $sub" echo "用法: clashctl proxy [on|off|status]" exit 1 ;; esac } cmd_ui() { local controller host port controller="$(read_config_value "external-controller" || true)" [ -n "${controller:-}" ] || controller="127.0.0.1:9090" host="${controller%:*}" port="${controller##*:}" case "$host" in 0.0.0.0|::|localhost) host="$(hostname -I 2>/dev/null | awk '{print $1}')" [ -n "$host" ] || host="127.0.0.1" ;; esac printf 'http://%s:%s/ui\n' "$host" "$port" } cmd_secret() { local secret secret="$(read_config_value "secret" || true)" if [ -n "${secret:-}" ]; then echo "$secret" else err "未读取到 secret" exit 1 fi } cmd_sub() { local subcmd="${1:-show}" case "$subcmd" in show) if [ -f "$ENV_FILE" ]; then grep -E '^[[:space:]]*(export[[:space:]]+)?CLASH_URL=' "$ENV_FILE" || true else err "未找到 .env" exit 1 fi ;; update) if service_exists; then info "重启服务以刷新订阅..." systemctl restart "$Service_Name" ok "订阅刷新完成" elif [ -f "$Server_Dir/restart.sh" ]; then bash "$Server_Dir/restart.sh" ok "订阅刷新完成" else err "未找到 restart.sh" exit 1 fi ;; *) err "未知 sub 子命令: $subcmd" echo "用法: clashctl sub [show|update]" exit 1 ;; esac } cmd_upgrade() { if [ -f "$Server_Dir/update.sh" ]; then bash "$Server_Dir/update.sh" else err "未找到 update.sh" exit 1 fi } write_env_bool() { local key="$1" local value="$2" if [ ! -f "$ENV_FILE" ]; then err "未找到 .env" exit 1 fi if grep -qE "^[[:space:]]*(export[[:space:]]+)?${key}=" "$ENV_FILE"; then sed -i -E "s|^[[:space:]]*(export[[:space:]]+)?${key}=.*$|export ${key}=\"${value}\"|g" "$ENV_FILE" else echo "export ${key}=\"${value}\"" >> "$ENV_FILE" fi } cmd_tun() { local subcmd="${1:-status}" case "$subcmd" in status) if [ -f "$ENV_FILE" ]; then grep -E '^[[:space:]]*(export[[:space:]]+)?CLASH_TUN=' "$ENV_FILE" || echo 'CLASH_TUN=未配置' else err "未找到 .env" exit 1 fi ;; on) write_env_bool "CLASH_TUN" "true" ok "已写入 CLASH_TUN=true" ;; off) write_env_bool "CLASH_TUN" "false" ok "已写入 CLASH_TUN=false" ;; *) err "未知 tun 子命令: $subcmd" echo "用法: clashctl tun [on|off|status]" exit 1 ;; esac } cmd_mixin() { local subcmd="${1:-status}" local mixin_file="$Server_Dir/conf/mixin.yaml" case "$subcmd" in status) if [ -f "$mixin_file" ]; then ok "Mixin 已存在: $mixin_file" else warn "Mixin 不存在: $mixin_file" fi ;; edit) ${EDITOR:-vi} "$mixin_file" ;; *) err "未知 mixin 子命令: $subcmd" echo "用法: clashctl mixin [status|edit]" exit 1 ;; esac } main() { local cmd="${1:-}" shift || true case "$cmd" in on) cmd_on "$@" ;; off) cmd_off "$@" ;; status) cmd_status "$@" ;; proxy) cmd_proxy "$@" ;; ui) cmd_ui "$@" ;; secret) cmd_secret "$@" ;; sub) cmd_sub "$@" ;; upgrade) cmd_upgrade "$@" ;; tun) cmd_tun "$@" ;; mixin) cmd_mixin "$@" ;; -h|--help|"") usage ;; *) err "未知命令: $cmd" echo usage exit 1 ;; esac } main "$@"