mirror of
https://github.com/wnlen/clash-for-linux.git
synced 2026-02-04 10:11:28 +08:00
Merge pull request #102 from wnlen/codex/add-clashctl-command-and-port-conflict-detection
新增 `clashctl` 管理命令并实现端口冲突自动检测与分配
This commit is contained in:
2
.env
2
.env
@ -15,3 +15,5 @@ export CLASH_ALLOW_LAN=true
|
|||||||
# External Controller (RESTful API) 配置
|
# External Controller (RESTful API) 配置
|
||||||
export EXTERNAL_CONTROLLER_ENABLED=true
|
export EXTERNAL_CONTROLLER_ENABLED=true
|
||||||
export EXTERNAL_CONTROLLER=0.0.0.0:9090
|
export EXTERNAL_CONTROLLER=0.0.0.0:9090
|
||||||
|
|
||||||
|
# 端口可设置为 auto(自动分配随机端口)
|
||||||
|
|||||||
21
README.md
21
README.md
@ -45,6 +45,7 @@ $ vim .env
|
|||||||
|
|
||||||
> **注意:** `.env` 文件中的变量 `CLASH_SECRET` 为自定义 Clash Secret,值为空时,脚本将自动生成随机字符串。
|
> **注意:** `.env` 文件中的变量 `CLASH_SECRET` 为自定义 Clash Secret,值为空时,脚本将自动生成随机字符串。
|
||||||
> 如需使用其它架构,请将对应 Clash 二进制放入 `bin/` 并在 `.env` 中设置 `CLASH_BIN`,或命名为 `clash-linux-<arch>`(如 `clash-linux-riscv64`)。
|
> 如需使用其它架构,请将对应 Clash 二进制放入 `bin/` 并在 `.env` 中设置 `CLASH_BIN`,或命名为 `clash-linux-<arch>`(如 `clash-linux-riscv64`)。
|
||||||
|
> 端口支持设置为 `auto`,脚本会自动检测冲突并随机分配可用端口。
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
@ -90,6 +91,26 @@ $ proxy_on
|
|||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
|
## clashctl 命令
|
||||||
|
|
||||||
|
统一管理入口,支持启动/停止/重启/状态/更新/修改订阅:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ sudo ./clashctl status
|
||||||
|
$ sudo ./clashctl start
|
||||||
|
$ sudo ./clashctl restart
|
||||||
|
$ sudo ./clashctl update
|
||||||
|
$ sudo ./clashctl set-url "https://example.com/your-subscribe"
|
||||||
|
```
|
||||||
|
|
||||||
|
安装脚本会将 `clashctl` 安装到 `/usr/local/bin/clashctl`,安装后可直接使用:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ sudo clashctl status
|
||||||
|
```
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
## 一键安装/卸载
|
## 一键安装/卸载
|
||||||
|
|
||||||
脚本会自动识别安装路径、创建低权限用户、检测端口冲突,并根据架构自动下载 Clash 内核(可通过 `CLASH_DOWNLOAD_URL_TEMPLATE` 自定义下载地址)。
|
脚本会自动识别安装路径、创建低权限用户、检测端口冲突,并根据架构自动下载 Clash 内核(可通过 `CLASH_DOWNLOAD_URL_TEMPLATE` 自定义下载地址)。
|
||||||
|
|||||||
146
clashctl
Executable file
146
clashctl
Executable file
@ -0,0 +1,146 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||||
|
SERVICE_NAME="clash-for-linux"
|
||||||
|
|
||||||
|
resolve_clash_home() {
|
||||||
|
local candidates
|
||||||
|
if [ -n "${CLASH_HOME:-}" ] && [ -d "$CLASH_HOME" ]; then
|
||||||
|
echo "$CLASH_HOME"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
if [ -f "/etc/default/${SERVICE_NAME}" ]; then
|
||||||
|
candidates=$(awk -F= '/^CLASH_HOME=/{print $2}' "/etc/default/${SERVICE_NAME}" | tr -d '"')
|
||||||
|
if [ -n "$candidates" ] && [ -d "$candidates" ]; then
|
||||||
|
echo "$candidates"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
if [ -f "$SCRIPT_DIR/.env" ]; then
|
||||||
|
echo "$SCRIPT_DIR"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
if [ -f "/opt/clash-for-linux/.env" ]; then
|
||||||
|
echo "/opt/clash-for-linux"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
echo "$SCRIPT_DIR"
|
||||||
|
}
|
||||||
|
|
||||||
|
CLASH_HOME=$(resolve_clash_home)
|
||||||
|
ENV_FILE="$CLASH_HOME/.env"
|
||||||
|
PID_FILE="$CLASH_HOME/temp/clash.pid"
|
||||||
|
|
||||||
|
use_systemd() {
|
||||||
|
command -v systemctl >/dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
action_with_systemd() {
|
||||||
|
local action="$1"
|
||||||
|
if use_systemd; then
|
||||||
|
if systemctl "$action" "${SERVICE_NAME}.service" >/dev/null 2>&1; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
print_usage() {
|
||||||
|
cat <<USAGE
|
||||||
|
Usage: clashctl <command> [args]
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
start Start Clash service
|
||||||
|
stop Stop Clash service
|
||||||
|
restart Restart Clash service
|
||||||
|
status Show Clash service status
|
||||||
|
update Refresh subscription config
|
||||||
|
set-url <url> Update CLASH_URL in .env
|
||||||
|
|
||||||
|
Environment:
|
||||||
|
CLASH_HOME Override Clash installation directory
|
||||||
|
USAGE
|
||||||
|
}
|
||||||
|
|
||||||
|
set_url() {
|
||||||
|
local url="$1"
|
||||||
|
if [ -z "$url" ]; then
|
||||||
|
echo "[ERROR] 请提供订阅地址" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [ ! -f "$ENV_FILE" ]; then
|
||||||
|
echo "[ERROR] 未找到 .env 文件: $ENV_FILE" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
local escaped
|
||||||
|
escaped=$(printf "%s" "$url" | sed "s/'/'\"'\"'/g")
|
||||||
|
if grep -q '^export CLASH_URL=' "$ENV_FILE"; then
|
||||||
|
sed -i "s@^export CLASH_URL=.*@export CLASH_URL='${escaped}'@" "$ENV_FILE"
|
||||||
|
else
|
||||||
|
echo "export CLASH_URL='${escaped}'" >> "$ENV_FILE"
|
||||||
|
fi
|
||||||
|
echo "[OK] 已更新 CLASH_URL"
|
||||||
|
}
|
||||||
|
|
||||||
|
status_fallback() {
|
||||||
|
if [ -f "$PID_FILE" ]; then
|
||||||
|
local pid
|
||||||
|
pid=$(cat "$PID_FILE")
|
||||||
|
if [ -n "$pid" ] && kill -0 "$pid" >/dev/null 2>&1; then
|
||||||
|
echo "[OK] Clash 进程运行中 (PID: $pid)"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
if pgrep -f "clash-linux-" >/dev/null 2>&1; then
|
||||||
|
echo "[OK] Clash 进程运行中"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
echo "[WARN] 未检测到 Clash 进程"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
run_script() {
|
||||||
|
local script="$1"
|
||||||
|
if [ ! -x "$CLASH_HOME/$script" ]; then
|
||||||
|
echo "[ERROR] 未找到脚本: $CLASH_HOME/$script" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
( cd "$CLASH_HOME" && bash "$CLASH_HOME/$script" )
|
||||||
|
}
|
||||||
|
|
||||||
|
command=${1:-}
|
||||||
|
case "$command" in
|
||||||
|
start)
|
||||||
|
action_with_systemd start || run_script start.sh
|
||||||
|
;;
|
||||||
|
stop)
|
||||||
|
action_with_systemd stop || run_script shutdown.sh
|
||||||
|
;;
|
||||||
|
restart)
|
||||||
|
action_with_systemd restart || run_script restart.sh
|
||||||
|
;;
|
||||||
|
status)
|
||||||
|
if use_systemd; then
|
||||||
|
if systemctl status "${SERVICE_NAME}.service" --no-pager; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
status_fallback
|
||||||
|
;;
|
||||||
|
update)
|
||||||
|
run_script update.sh
|
||||||
|
;;
|
||||||
|
set-url)
|
||||||
|
set_url "${2:-}"
|
||||||
|
;;
|
||||||
|
-h|--help|help|'')
|
||||||
|
print_usage
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "[ERROR] 未知命令: $command" >&2
|
||||||
|
print_usage
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
27
install.sh
27
install.sh
@ -30,10 +30,12 @@ fi
|
|||||||
chmod +x "$Install_Dir"/*.sh 2>/dev/null || true
|
chmod +x "$Install_Dir"/*.sh 2>/dev/null || true
|
||||||
chmod +x "$Install_Dir"/scripts/* 2>/dev/null || true
|
chmod +x "$Install_Dir"/scripts/* 2>/dev/null || true
|
||||||
chmod +x "$Install_Dir"/bin/* 2>/dev/null || true
|
chmod +x "$Install_Dir"/bin/* 2>/dev/null || true
|
||||||
|
chmod +x "$Install_Dir"/clashctl 2>/dev/null || true
|
||||||
|
|
||||||
source "$Install_Dir/.env"
|
source "$Install_Dir/.env"
|
||||||
source "$Install_Dir/scripts/get_cpu_arch.sh"
|
source "$Install_Dir/scripts/get_cpu_arch.sh"
|
||||||
source "$Install_Dir/scripts/resolve_clash.sh"
|
source "$Install_Dir/scripts/resolve_clash.sh"
|
||||||
|
source "$Install_Dir/scripts/port_utils.sh"
|
||||||
|
|
||||||
if [[ -z "${CpuArch:-}" ]]; then
|
if [[ -z "${CpuArch:-}" ]]; then
|
||||||
echo -e "\033[31m[ERROR] 无法识别 CPU 架构\033[0m"
|
echo -e "\033[31m[ERROR] 无法识别 CPU 架构\033[0m"
|
||||||
@ -51,22 +53,11 @@ parse_port() {
|
|||||||
echo "$raw"
|
echo "$raw"
|
||||||
}
|
}
|
||||||
|
|
||||||
is_port_in_use() {
|
|
||||||
local port="$1"
|
|
||||||
if command -v ss >/dev/null 2>&1; then
|
|
||||||
ss -lnt | awk '{print $4}' | grep -E "(:|\.)${port}$" >/dev/null 2>&1
|
|
||||||
elif command -v netstat >/dev/null 2>&1; then
|
|
||||||
netstat -lnt | awk '{print $4}' | grep -E "(:|\.)${port}$" >/dev/null 2>&1
|
|
||||||
elif command -v lsof >/dev/null 2>&1; then
|
|
||||||
lsof -iTCP -sTCP:LISTEN -P -n | awk '{print $9}' | grep -E "(:|\.)${port}$" >/dev/null 2>&1
|
|
||||||
else
|
|
||||||
echo -e "\033[33m[WARN] 未找到端口检测工具,跳过端口冲突检测\033[0m"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
Port_Conflicts=()
|
Port_Conflicts=()
|
||||||
for port in "$CLASH_HTTP_PORT" "$CLASH_SOCKS_PORT" "$CLASH_REDIR_PORT" "$(parse_port "$EXTERNAL_CONTROLLER")"; do
|
for port in "$CLASH_HTTP_PORT" "$CLASH_SOCKS_PORT" "$CLASH_REDIR_PORT" "$(parse_port "$EXTERNAL_CONTROLLER")"; do
|
||||||
|
if [ "$port" = "auto" ] || [ -z "$port" ]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
if [[ "$port" =~ ^[0-9]+$ ]]; then
|
if [[ "$port" =~ ^[0-9]+$ ]]; then
|
||||||
if is_port_in_use "$port"; then
|
if is_port_in_use "$port"; then
|
||||||
Port_Conflicts+=("$port")
|
Port_Conflicts+=("$port")
|
||||||
@ -75,9 +66,7 @@ for port in "$CLASH_HTTP_PORT" "$CLASH_SOCKS_PORT" "$CLASH_REDIR_PORT" "$(parse_
|
|||||||
done
|
done
|
||||||
|
|
||||||
if [ "${#Port_Conflicts[@]}" -ne 0 ]; then
|
if [ "${#Port_Conflicts[@]}" -ne 0 ]; then
|
||||||
echo -e "\033[31m[ERROR] 检测到端口冲突: ${Port_Conflicts[*]}\033[0m"
|
echo -e "\033[33m[WARN] 检测到端口冲突: ${Port_Conflicts[*]},运行时将自动分配可用端口\033[0m"
|
||||||
echo -e "请修改 .env 中的端口设置后重新安装。"
|
|
||||||
exit 1
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if ! getent group "$Service_Group" >/dev/null 2>&1; then
|
if ! getent group "$Service_Group" >/dev/null 2>&1; then
|
||||||
@ -108,5 +97,9 @@ else
|
|||||||
echo -e "\033[33m[WARN] 未检测到 systemd,已跳过服务单元生成\033[0m"
|
echo -e "\033[33m[WARN] 未检测到 systemd,已跳过服务单元生成\033[0m"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [ -f "$Install_Dir/clashctl" ]; then
|
||||||
|
install -m 0755 "$Install_Dir/clashctl" /usr/local/bin/clashctl
|
||||||
|
fi
|
||||||
|
|
||||||
echo -e "\033[32m[OK] Clash for Linux 已安装至: ${Install_Dir}\033[0m"
|
echo -e "\033[32m[OK] Clash for Linux 已安装至: ${Install_Dir}\033[0m"
|
||||||
echo -e "请编辑 ${Install_Dir}/.env 配置订阅地址后启动服务。"
|
echo -e "请编辑 ${Install_Dir}/.env 配置订阅地址后启动服务。"
|
||||||
|
|||||||
102
scripts/port_utils.sh
Normal file
102
scripts/port_utils.sh
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
PORT_CHECK_WARNED=${PORT_CHECK_WARNED:-0}
|
||||||
|
|
||||||
|
is_port_in_use() {
|
||||||
|
local port="$1"
|
||||||
|
if command -v ss >/dev/null 2>&1; then
|
||||||
|
ss -lnt | awk '{print $4}' | grep -E "(:|\.)${port}$" >/dev/null 2>&1
|
||||||
|
return $?
|
||||||
|
fi
|
||||||
|
if command -v netstat >/dev/null 2>&1; then
|
||||||
|
netstat -lnt | awk '{print $4}' | grep -E "(:|\.)${port}$" >/dev/null 2>&1
|
||||||
|
return $?
|
||||||
|
fi
|
||||||
|
if command -v lsof >/dev/null 2>&1; then
|
||||||
|
lsof -iTCP -sTCP:LISTEN -P -n | awk '{print $9}' | grep -E "(:|\.)${port}$" >/dev/null 2>&1
|
||||||
|
return $?
|
||||||
|
fi
|
||||||
|
if [ "$PORT_CHECK_WARNED" -eq 0 ]; then
|
||||||
|
echo -e "\033[33m[WARN] 未找到端口检测工具,端口冲突检测可能不准确\033[0m" >&2
|
||||||
|
PORT_CHECK_WARNED=1
|
||||||
|
fi
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
find_available_port() {
|
||||||
|
local start_port=${1:-20000}
|
||||||
|
local end_port=${2:-65000}
|
||||||
|
local port
|
||||||
|
|
||||||
|
if command -v shuf >/dev/null 2>&1; then
|
||||||
|
for _ in {1..50}; do
|
||||||
|
port=$(shuf -i "${start_port}-${end_port}" -n 1)
|
||||||
|
if ! is_port_in_use "$port"; then
|
||||||
|
echo "$port"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
for port in $(seq "$start_port" "$end_port"); do
|
||||||
|
if ! is_port_in_use "$port"; then
|
||||||
|
echo "$port"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve_port_value() {
|
||||||
|
local name="$1"
|
||||||
|
local value="$2"
|
||||||
|
local resolved
|
||||||
|
|
||||||
|
if [ -z "$value" ] || [ "$value" = "auto" ]; then
|
||||||
|
resolved=$(find_available_port)
|
||||||
|
if [ -z "$resolved" ]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
echo -e "\033[33m[WARN] ${name} 端口已自动分配为 ${resolved}\033[0m" >&2
|
||||||
|
echo "$resolved"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$value" =~ ^[0-9]+$ ]]; then
|
||||||
|
if is_port_in_use "$value"; then
|
||||||
|
resolved=$(find_available_port)
|
||||||
|
if [ -n "$resolved" ]; then
|
||||||
|
echo -e "\033[33m[WARN] ${name} 端口 ${value} 已被占用,已自动切换为 ${resolved}\033[0m" >&2
|
||||||
|
echo "$resolved"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "$value"
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve_host_port() {
|
||||||
|
local name="$1"
|
||||||
|
local raw="$2"
|
||||||
|
local default_host="$3"
|
||||||
|
local host
|
||||||
|
local port
|
||||||
|
|
||||||
|
if [ "$raw" = "auto" ] || [ -z "$raw" ]; then
|
||||||
|
host="$default_host"
|
||||||
|
port="auto"
|
||||||
|
else
|
||||||
|
if [[ "$raw" == *:* ]]; then
|
||||||
|
host="${raw%:*}"
|
||||||
|
port="${raw##*:}"
|
||||||
|
else
|
||||||
|
host="$default_host"
|
||||||
|
port="$raw"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
port=$(resolve_port_value "$name" "$port") || return 1
|
||||||
|
echo "${host}:${port}"
|
||||||
|
}
|
||||||
6
start.sh
6
start.sh
@ -43,6 +43,12 @@ EXTERNAL_CONTROLLER_ENABLED=${EXTERNAL_CONTROLLER_ENABLED:-true}
|
|||||||
EXTERNAL_CONTROLLER=${EXTERNAL_CONTROLLER:-127.0.0.1:9090}
|
EXTERNAL_CONTROLLER=${EXTERNAL_CONTROLLER:-127.0.0.1:9090}
|
||||||
ALLOW_INSECURE_TLS=${ALLOW_INSECURE_TLS:-false}
|
ALLOW_INSECURE_TLS=${ALLOW_INSECURE_TLS:-false}
|
||||||
|
|
||||||
|
source "$Server_Dir/scripts/port_utils.sh"
|
||||||
|
CLASH_HTTP_PORT=$(resolve_port_value "HTTP" "$CLASH_HTTP_PORT")
|
||||||
|
CLASH_SOCKS_PORT=$(resolve_port_value "SOCKS" "$CLASH_SOCKS_PORT")
|
||||||
|
CLASH_REDIR_PORT=$(resolve_port_value "REDIR" "$CLASH_REDIR_PORT")
|
||||||
|
EXTERNAL_CONTROLLER=$(resolve_host_port "External Controller" "$EXTERNAL_CONTROLLER" "0.0.0.0")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#################### 函数定义 ####################
|
#################### 函数定义 ####################
|
||||||
|
|||||||
@ -27,10 +27,13 @@ if [ -f "/etc/profile.d/clash-for-linux.sh" ]; then
|
|||||||
rm -f "/etc/profile.d/clash-for-linux.sh"
|
rm -f "/etc/profile.d/clash-for-linux.sh"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [ -f "/usr/local/bin/clashctl" ]; then
|
||||||
|
rm -f "/usr/local/bin/clashctl"
|
||||||
|
fi
|
||||||
|
|
||||||
if [ -d "$Install_Dir" ]; then
|
if [ -d "$Install_Dir" ]; then
|
||||||
rm -rf "$Install_Dir"
|
rm -rf "$Install_Dir"
|
||||||
echo -e "\033[32m[OK] 已移除安装目录: ${Install_Dir}\033[0m"
|
echo -e "\033[32m[OK] 已移除安装目录: ${Install_Dir}\033[0m"
|
||||||
else
|
else
|
||||||
echo -e "\033[33m[WARN] 未找到安装目录: ${Install_Dir}\033[0m"
|
echo -e "\033[33m[WARN] 未找到安装目录: ${Install_Dir}\033[0m"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@ -36,6 +36,12 @@ EXTERNAL_CONTROLLER_ENABLED=${EXTERNAL_CONTROLLER_ENABLED:-true}
|
|||||||
EXTERNAL_CONTROLLER=${EXTERNAL_CONTROLLER:-127.0.0.1:9090}
|
EXTERNAL_CONTROLLER=${EXTERNAL_CONTROLLER:-127.0.0.1:9090}
|
||||||
ALLOW_INSECURE_TLS=${ALLOW_INSECURE_TLS:-false}
|
ALLOW_INSECURE_TLS=${ALLOW_INSECURE_TLS:-false}
|
||||||
|
|
||||||
|
source "$Server_Dir/scripts/port_utils.sh"
|
||||||
|
CLASH_HTTP_PORT=$(resolve_port_value "HTTP" "$CLASH_HTTP_PORT")
|
||||||
|
CLASH_SOCKS_PORT=$(resolve_port_value "SOCKS" "$CLASH_SOCKS_PORT")
|
||||||
|
CLASH_REDIR_PORT=$(resolve_port_value "REDIR" "$CLASH_REDIR_PORT")
|
||||||
|
EXTERNAL_CONTROLLER=$(resolve_host_port "External Controller" "$EXTERNAL_CONTROLLER" "0.0.0.0")
|
||||||
|
|
||||||
#################### 函数定义 ####################
|
#################### 函数定义 ####################
|
||||||
|
|
||||||
# 自定义action函数,实现通用action功能
|
# 自定义action函数,实现通用action功能
|
||||||
|
|||||||
Reference in New Issue
Block a user