Files
kejilion/kejilion.sh
T
eddy e31ab788ed chore: 移除过时的 VPS 管理功能
- 移除 WARP、Python 安装、留言板、重启和集群管理菜单
- 删除相关辅助函数和远程执行逻辑
- 将脚本版本提升至 3.0.0,以体现破坏性变更
- 重写功能文档并清理过时说明
2026-08-01 23:10:27 +08:00

2406 lines
89 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/bash
sh_v="3.0.0"
huang='\033[33m'
bai='\033[0m'
lv='\033[0;32m'
hong='\033[31m'
kjlan='\033[96m'
hui='\e[37m'
# 统一使用 GitHub 官方 Raw 地址,可通过环境变量 GITHUB_RAW_URL 覆盖
GITHUB_RAW_URL="${GITHUB_RAW_URL:-https://raw.githubusercontent.com}"
GITHUB_RAW_URL="${GITHUB_RAW_URL%/}"
# 本脚本自身的更新源
UPDATE_RAW_URL="${UPDATE_RAW_URL:-https://gitea.tohub.top/Share/kejilion/raw/branch/main}"
UPDATE_RAW_URL="${UPDATE_RAW_URL%/}"
XANMOD_SOURCE_FILE=/etc/apt/sources.list.d/kejilion-xanmod.list
if [ ! -r /etc/os-release ]; then
echo "无法识别操作系统,仅支持 Debian 和 Ubuntu。" >&2
exit 1
fi
OS_ID=$(. /etc/os-release && printf '%s' "$ID")
case "$OS_ID" in
debian|ubuntu) ;;
*)
echo "当前系统不受支持,仅支持 Debian 和 Ubuntu。" >&2
exit 1
;;
esac
# 记录脚本自身的绝对路径,供重启自身和安装快捷命令使用
SCRIPT_PATH=$(readlink -f "$0" 2>/dev/null)
[ -f "$SCRIPT_PATH" ] || SCRIPT_PATH="$0"
if [ ! -f "$SCRIPT_PATH" ]; then
echo "无法定位脚本自身路径,请先将脚本保存到本地再运行。" >&2
exit 1
fi
cp "$SCRIPT_PATH" /usr/local/bin/k > /dev/null 2>&1
# 统一的错误提示
err_msg() {
echo -e "${hong}$*${bai}" >&2
}
# 新建账户用:与 adduser 默认 NAME_REGEX 一致,只允许小写
valid_username() {
[ "${#1}" -le 32 ] && [[ "$1" =~ ^[a-z_][a-z0-9_-]*[$]?$ ]]
}
# 仅校验字符集:useradd 允许含大写字母的用户名,操作已有账户时沿用新建账户
# 的严格规则会让这类账户无法通过菜单管理。白名单仍要保留,避免把任意字符串
# 写进 sudoers 或拼进文件路径。
valid_username_chars() {
[ -n "$1" ] && [ "${#1}" -le 32 ] && [[ "$1" =~ ^[A-Za-z0-9_][A-Za-z0-9_.-]*[$]?$ ]]
}
# 授予权限等场景用:额外要求账户真实存在。比对 id -un 而非只看 id 的退出码,
# 是为了挡掉直接传 UID 数字的情况(id 0 会成功并解析成 root)。
valid_existing_username() {
valid_username_chars "$1" && [ "$(id -un "$1" 2>/dev/null)" = "$1" ]
}
valid_hostname() {
local hostname="$1" label
local hostname_labels=()
[ -n "$hostname" ] && [ "${#hostname}" -le 253 ] || return 1
[[ "$hostname" != .* && "$hostname" != *. && "$hostname" != *..* ]] || return 1
IFS=. read -r -a hostname_labels <<< "$hostname"
for label in "${hostname_labels[@]}"; do
[ -n "$label" ] && [ "${#label}" -le 63 ] &&
[[ "$label" =~ ^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$ ]] || return 1
done
}
grant_sudo() {
local username="$1" sudoers_file="/etc/sudoers.d/kejilion-$1" tmp_sudoers
if ! valid_existing_username "$username"; then
err_msg "用户不存在或用户名格式不合法: $username"
return 1
fi
if ! command -v visudo &>/dev/null; then
install sudo || return 1
fi
mkdir -p /etc/sudoers.d || return 1
tmp_sudoers=$(mktemp /etc/sudoers.d/.kejilion.XXXXXX) || return 1
if ! printf '%s ALL=(ALL:ALL) ALL\n' "$username" > "$tmp_sudoers" ||
! chmod 440 "$tmp_sudoers" ||
! visudo -cf "$tmp_sudoers" >/dev/null; then
rm -f "$tmp_sudoers"
err_msg "sudoers 配置校验失败,未授予 sudo 权限。"
return 1
fi
if ! mv -f "$tmp_sudoers" "$sudoers_file"; then
rm -f "$tmp_sudoers"
err_msg "写入 $sudoers_file 失败。"
return 1
fi
}
revoke_sudo() {
local username="$1" sudoers_file="/etc/sudoers.d/kejilion-$1" tmp_sudoers legacy_rule
# 这里只校验字符集,不要求账户仍然存在:账户已被删除、只剩 sudoers
# 残留规则时,仍应允许清理。
if ! valid_username_chars "$username"; then
err_msg "用户名格式不合法: $username"
return 1
fi
if ! command -v visudo &>/dev/null; then
err_msg "未找到 visudo,无法安全修改 sudoers。"
return 1
fi
# 兼容旧版本直接追加到 /etc/sudoers 的授权;没有旧规则时不重写主配置。
legacy_rule="$username ALL=(ALL:ALL) ALL"
if grep -Fqx -- "$legacy_rule" /etc/sudoers; then
tmp_sudoers=$(mktemp /etc/.sudoers.kejilion.XXXXXX) || return 1
awk -v rule="$legacy_rule" '$0 != rule' /etc/sudoers > "$tmp_sudoers" || {
rm -f "$tmp_sudoers"
return 1
}
if ! chmod --reference=/etc/sudoers "$tmp_sudoers" 2>/dev/null ||
! chown --reference=/etc/sudoers "$tmp_sudoers" 2>/dev/null ||
! visudo -cf "$tmp_sudoers" >/dev/null ||
! mv -f "$tmp_sudoers" /etc/sudoers; then
rm -f "$tmp_sudoers"
err_msg "sudoers 配置校验或写入失败,未修改主配置。"
return 1
fi
fi
rm -f "$sudoers_file" || {
err_msg "删除 $sudoers_file 失败。"
return 1
}
}
# 下载文件,失败时给出明确提示并返回非零
download_file() {
local url="$1" dest="$2"
if curl -fsSL --retry 2 --max-time 60 -o "$dest" "$url"; then
return 0
fi
err_msg "下载失败: $url"
return 1
}
update_limit_cron() {
local action="$1" limit_script="$2" current_cron new_cron cron_error
current_cron=$(mktemp) || return 1
new_cron=$(mktemp) || {
rm -f "$current_cron"
return 1
}
cron_error=$(mktemp) || {
rm -f "$current_cron" "$new_cron"
return 1
}
if ! crontab -l > "$current_cron" 2> "$cron_error"; then
if grep -qi 'no crontab' "$cron_error"; then
: > "$current_cron"
else
rm -f "$current_cron" "$new_cron" "$cron_error"
err_msg "无法读取现有 crontab,已取消修改。"
return 1
fi
fi
rm -f "$cron_error"
if ! awk -v script="$limit_script" '
$0 == "# KEJILION_LIMIT_BEGIN" {
if (in_block) exit 2
in_block = 1
next
}
$0 == "# KEJILION_LIMIT_END" {
if (!in_block) exit 2
in_block = 0
next
}
in_block { next }
$0 == "* * * * * ~/Limiting_Shut_down.sh" { next }
$0 == "* * * * * " script { next }
$0 == "0 1 1 * * reboot" { next }
{ print }
END { if (in_block) exit 2 }
' "$current_cron" > "$new_cron"; then
rm -f "$current_cron" "$new_cron"
err_msg "检测到不完整的限流 crontab 标记,已保留原定时任务。"
return 1
fi
if [ "$action" = "enable" ]; then
{
echo "# KEJILION_LIMIT_BEGIN"
printf '* * * * * /bin/bash %q\n' "$limit_script"
echo "0 1 1 * * /sbin/reboot"
echo "# KEJILION_LIMIT_END"
} >> "$new_cron"
elif [ "$action" != "disable" ]; then
rm -f "$current_cron" "$new_cron"
err_msg "未知的限流定时任务操作: $action"
return 1
fi
if crontab "$new_cron"; then
rm -f "$current_cron" "$new_cron"
return 0
fi
rm -f "$current_cron" "$new_cron"
return 1
}
# 版本号比较:$1 严格大于 $2 时返回 0,用于避免自更新把脚本降级
version_gt() {
[ "$1" = "$2" ] && return 1
local IFS=. i x y
local a=($1) b=($2)
for ((i = 0; i < ${#a[@]} || i < ${#b[@]}; i++)); do
x=${a[i]:-0}; y=${b[i]:-0}
x=${x//[!0-9]/}; y=${y//[!0-9]/}
x=${x:-0}; y=${y:-0}
[ "$x" -gt "$y" ] && return 0
[ "$x" -lt "$y" ] && return 1
done
return 1
}
ip_address() {
ipv4_address=$(curl -s ipv4.ip.sb)
ipv6_address=$(curl -s --max-time 1 ipv6.ip.sb)
}
package_installed() {
[ "$(dpkg-query -W -f='${db:Status-Abbrev}' "$1" 2>/dev/null)" = "ii " ]
}
install() {
if [ $# -eq 0 ]; then
echo "未提供软件包参数!"
return 1
fi
local package
local missing_packages=()
for package in "$@"; do
if package_installed "$package"; then
echo "$package 已经安装。"
else
missing_packages+=("$package")
fi
done
[ "${#missing_packages[@]}" -gt 0 ] || return 0
if ! command -v apt-get &>/dev/null; then
err_msg "未找到 apt-get,当前系统环境不受支持。"
return 1
fi
echo "正在安装: ${missing_packages[*]}"
if apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y -- "${missing_packages[@]}"; then
return 0
fi
err_msg "安装失败: ${missing_packages[*]}"
return 1
}
remove() {
if [ $# -eq 0 ]; then
echo "未提供软件包参数!"
return 1
fi
if ! command -v apt-get &>/dev/null; then
err_msg "未找到 apt-get,当前系统环境不受支持。"
return 1
fi
echo "正在卸载: $*"
DEBIAN_FRONTEND=noninteractive apt-get purge -y -- "$@"
}
break_end() {
echo -e "${lv}操作完成${bai}"
echo "按任意键继续..."
read -n 1 -s -r -p ""
echo ""
clear
}
# 用当前脚本自身替换进程,回到主菜单。
# 用 exec 而非再开子进程,既不依赖 /usr/local/bin/k 是否存在(非 root 时 cp 会失败),
# 也避免每次“返回主菜单”都叠加一层进程。
kejilion() {
exec bash "$SCRIPT_PATH"
}
install_add_docker() {
local installer
installer=$(mktemp) || return 1
if ! download_file "https://get.docker.com" "$installer" ||
! sh "$installer"; then
rm -f "$installer"
err_msg "Docker 安装失败。"
return 1
fi
rm -f "$installer"
if ! systemctl enable --now docker; then
err_msg "Docker 服务启动失败。"
return 1
fi
sleep 2
}
install_docker() {
if ! command -v docker &>/dev/null; then
install_add_docker || return 1
elif ! docker info >/dev/null 2>&1; then
if ! systemctl enable --now docker || ! docker info >/dev/null 2>&1; then
err_msg "Docker 已安装,但守护进程无法启动。"
return 1
fi
fi
echo "Docker环境已经就绪"
}
add_swap() {
# 先校验大小,避免把已有的 /swapfile 删掉之后才发现参数不合法
if ! [[ "$new_swap" =~ ^[0-9]+$ ]] || [ "$new_swap" -le 0 ]; then
err_msg "虚拟内存大小必须是大于 0 的整数(单位 MB),当前输入: '${new_swap}'"
return 1
fi
# 先完整创建新文件,成功后再替换旧 swapfile,避免磁盘不足时丢失原配置
local new_file=/swapfile.new old_file=/swapfile.old old_swap_active=0 fstab_tmp
if [ -e "$old_file" ]; then
err_msg "检测到未处理的恢复文件 $old_file,请先人工确认后再重试。"
return 1
fi
if [ -e /swapfile ] && [ ! -f /swapfile ]; then
err_msg "检测到 /swapfile 不是普通文件,已取消调整。"
return 1
fi
rm -f "$new_file"
if ! dd if=/dev/zero of="$new_file" bs=1M count="$new_swap" status=none ||
! chmod 600 "$new_file" ||
! mkswap "$new_file" >/dev/null; then
err_msg "创建新 swapfile 失败,请检查磁盘剩余空间。"
rm -f "$new_file"
return 1
fi
if awk 'NR > 1 && $1 == "/swapfile" { found = 1 } END { exit !found }' /proc/swaps; then
old_swap_active=1
if ! swapoff /swapfile; then
err_msg "无法停用当前 swapfile,已保留原配置。"
rm -f "$new_file"
return 1
fi
fi
if [ -f /swapfile ] && ! mv /swapfile "$old_file"; then
err_msg "无法备份当前 swapfile,已保留原配置。"
rm -f "$new_file"
[ "$old_swap_active" -eq 0 ] || swapon /swapfile 2>/dev/null
return 1
fi
if mv "$new_file" /swapfile && swapon /swapfile; then
:
else
err_msg "启用新 swapfile 失败,正在恢复原文件。"
rm -f "$new_file"
swapoff /swapfile 2>/dev/null
rm -f /swapfile
if [ -f "$old_file" ]; then
mv "$old_file" /swapfile
[ "$old_swap_active" -eq 0 ] || swapon /swapfile 2>/dev/null
fi
return 1
fi
# 原子更新 fstab,并把重复的 /swapfile 记录归并为一条。
fstab_tmp=$(mktemp /etc/.fstab.kejilion.XXXXXX) || true
if [ -z "$fstab_tmp" ] ||
! awk '
$1 == "/swapfile" {
if (!seen) print "/swapfile swap swap defaults 0 0"
seen = 1
next
}
{ print }
END { if (!seen) print "/swapfile swap swap defaults 0 0" }
' /etc/fstab > "$fstab_tmp" ||
! chmod --reference=/etc/fstab "$fstab_tmp" 2>/dev/null ||
! chown --reference=/etc/fstab "$fstab_tmp" 2>/dev/null ||
! mv -f "$fstab_tmp" /etc/fstab; then
[ -z "$fstab_tmp" ] || rm -f "$fstab_tmp"
err_msg "写入 /etc/fstab 失败,正在恢复原 swapfile。"
swapoff /swapfile 2>/dev/null
rm -f /swapfile
if [ -f "$old_file" ]; then
mv "$old_file" /swapfile
[ "$old_swap_active" -eq 0 ] || swapon /swapfile 2>/dev/null
fi
return 1
fi
if ! rm -f "$old_file"; then
err_msg "新 swapfile 已启用,但无法删除恢复文件 $old_file。"
return 1
fi
echo -e "swapfile 已设置为${huang}${new_swap}${bai}MB(若系统另有 swap 分区,总量会大于该值)"
}
# 兜底场景专用:只在现有 swap 不足时才调整。
# 跑测试/装内核前的 swap 只是给低内存机器补位,直接调 add_swap 会把已有的
# 4G swap 缩成 1G。菜单里用户显式指定大小的地方仍应直接调 add_swap,
# 否则就无法主动调小。
ensure_swap() {
local current
current=$(LC_ALL=C free -m | awk '/^Swap:/{print $2}')
current=${current:-0}
if [[ "$current" =~ ^[0-9]+$ ]] && [[ "$new_swap" =~ ^[0-9]+$ ]] && [ "$current" -ge "$new_swap" ]; then
echo "当前虚拟内存 ${current}MB 已满足需求(目标 ${new_swap}MB),跳过调整。"
return 0
fi
add_swap
}
f2b_status() {
docker restart fail2ban
sleep 3
docker exec -it fail2ban fail2ban-client status
}
f2b_status_xxx() {
docker exec -it fail2ban fail2ban-client status "$xxx"
}
# 等待容器把配置目录创建出来,原先固定 sleep 3 在慢机器上会导致
# cd 失败、配置文件被写到错误的目录
f2b_wait_config_dir() {
local dir="$1" i
for i in $(seq 1 30); do
[ -d "$dir" ] && return 0
sleep 1
done
err_msg "等待 fail2ban 配置目录超时: $dir"
return 1
}
f2b_install_sshd() {
docker run -d \
--name=fail2ban \
--net=host \
--cap-add=NET_ADMIN \
--cap-add=NET_RAW \
-e PUID=1000 \
-e PGID=1000 \
-e TZ=Etc/UTC \
-e VERBOSITY=-vv \
-v /path/to/fail2ban/config:/config \
-v /var/log:/var/log:ro \
--restart unless-stopped \
lscr.io/linuxserver/fail2ban:latest || {
err_msg "Fail2Ban 容器启动失败。"
return 1
}
local f2b_url="${GITHUB_RAW_URL}/kejilion/config/main/fail2ban"
local jail_d=/path/to/fail2ban/config/fail2ban/jail.d
f2b_wait_config_dir "$jail_d" || return 1
install rsyslog || return 1
systemctl enable --now rsyslog || return 1
download_file "${f2b_url}/linux-ssh.conf" "${jail_d}/linux-ssh.conf" || return 1
}
f2b_sshd() {
xxx=linux-sshd
f2b_status_xxx
}
server_reboot() {
read -p "$(echo -e "${huang}现在重启服务器吗?(Y/N): ${bai}")" rboot
case "$rboot" in
[Yy])
echo "已重启"
reboot
;;
[Nn])
echo "已取消"
;;
*)
echo "无效的选择,请输入 Y 或 N。"
;;
esac
}
output_status() {
# 跳过 lo 等回环/虚拟接口,否则本机内部流量会被计入总量,
# 导致“限流自动关机”提前触发
output=$(awk 'BEGIN { rx_total = 0; tx_total = 0 }
NR > 2 {
# 以冒号切分,兼容接口名较长时数值与冒号粘连的内核输出格式
idx = index($0, ":");
if (idx == 0) next;
iface = substr($0, 1, idx - 1); gsub(/[ \t]/, "", iface);
if (iface == "lo" || iface ~ /^(docker|veth|br-|virbr|tun|tap|wg|tailscale|zt)/) next;
split(substr($0, idx + 1), f, " ");
rx_total += f[1]; tx_total += f[9]
}
END {
rx_units = "Bytes";
tx_units = "Bytes";
if (rx_total > 1024) { rx_total /= 1024; rx_units = "KB"; }
if (rx_total > 1024) { rx_total /= 1024; rx_units = "MB"; }
if (rx_total > 1024) { rx_total /= 1024; rx_units = "GB"; }
if (tx_total > 1024) { tx_total /= 1024; tx_units = "KB"; }
if (tx_total > 1024) { tx_total /= 1024; tx_units = "MB"; }
if (tx_total > 1024) { tx_total /= 1024; tx_units = "GB"; }
printf("总接收: %.2f %s\n总发送: %.2f %s\n", rx_total, rx_units, tx_total, tx_units);
}' /proc/net/dev)
}
current_timezone() {
timedatectl show --property=Timezone --value 2>/dev/null
}
set_timedate() {
local shiqu="$1"
timedatectl set-timezone "$shiqu"
}
linux_update() {
if ! command -v apt-get &>/dev/null; then
err_msg "未找到 apt-get,当前系统环境不受支持。"
return 1
fi
apt-get update && DEBIAN_FRONTEND=noninteractive apt-get full-upgrade -y
}
linux_clean() {
local old_pkgs
if ! command -v apt-get &>/dev/null; then
err_msg "未找到 apt-get,当前系统环境不受支持。"
return 1
fi
DEBIAN_FRONTEND=noninteractive apt-get autoremove --purge -y
apt-get clean
old_pkgs=$(dpkg -l | awk '/^rc/ {print $2}')
[ -n "$old_pkgs" ] && DEBIAN_FRONTEND=noninteractive apt-get purge -y $old_pkgs
journalctl --rotate
journalctl --vacuum-time=1s
journalctl --vacuum-size=50M
# 不自动批量删除内核,保留当前内核和可启动的回退版本
}
# 只增量修改指定的内核参数,不再用 > 整体覆盖 /etc/sysctl.conf
# 否则用户原有的全部内核调优配置都会丢失
set_sysctl() {
local key="$1" value="$2" conf=/etc/sysctl.conf escaped_key
touch "$conf" || return 1
escaped_key=${key//./\\.}
if grep -qE "^[[:space:]]*#?[[:space:]]*${escaped_key}[[:space:]]*=" "$conf"; then
sed -i "s|^[[:space:]]*#\?[[:space:]]*${escaped_key}[[:space:]]*=.*|${key}=${value}|" "$conf"
else
printf '%s=%s\n' "$key" "$value" >> "$conf"
fi
}
set_dns() {
# 检查机器是否有IPv6地址
ipv6_available=0
if [[ $(ip -6 addr | grep -c "inet6") -gt 0 ]]; then
ipv6_available=1
fi
echo "nameserver $dns1_ipv4" > /etc/resolv.conf
echo "nameserver $dns2_ipv4" >> /etc/resolv.conf
if [[ $ipv6_available -eq 1 ]]; then
echo "nameserver $dns1_ipv6" >> /etc/resolv.conf
echo "nameserver $dns2_ipv6" >> /etc/resolv.conf
fi
echo "DNS地址已更新"
echo "------------------------"
cat /etc/resolv.conf
echo "------------------------"
}
root_use() {
clear
[ "$EUID" -ne 0 ] && echo -e "${huang}请注意,该功能需要root用户才能运行!${bai}" && break_end && kejilion
}
# 探测 CPU 的 x86-64 微架构等级(v1~v4),决定安装哪个 xanmod 包。
# 下载到临时目录,避免在当前工作目录留下文件、或被 rm -f 误删同名文件
xanmod_psabi_version() {
local tmpdir ver
tmpdir=$(mktemp -d) || return 1
if ! download_file "${GITHUB_RAW_URL}/kejilion/sh/main/check_x86-64_psabi.sh" "${tmpdir}/check.sh"; then
rm -rf "$tmpdir"
return 1
fi
chmod +x "${tmpdir}/check.sh"
# 用 -oE 而非 -oPbusybox grep 不支持 PCRE
ver=$("${tmpdir}/check.sh" | grep -oE 'x86-64-v[0-9]+' | grep -oE '[0-9]+$' | head -n 1)
rm -rf "$tmpdir"
[ -n "$ver" ] || return 1
echo "$ver"
}
xanmod_add_repo() {
local key_file key_tmp source_tmp
install gnupg || return 1
key_file=$(mktemp) || return 1
key_tmp=$(mktemp /usr/share/keyrings/.xanmod-archive-keyring.XXXXXX) || {
rm -f "$key_file"
return 1
}
if ! download_file "${GITHUB_RAW_URL}/kejilion/sh/main/archive.key" "$key_file" ||
! gpg --dearmor --yes -o "$key_tmp" "$key_file" ||
! chmod 644 "$key_tmp" ||
! mv -f "$key_tmp" /usr/share/keyrings/xanmod-archive-keyring.gpg; then
rm -f "$key_file" "$key_tmp"
err_msg "XanMod 签名密钥下载或导入失败。"
return 1
fi
rm -f "$key_file"
source_tmp=$(mktemp "${XANMOD_SOURCE_FILE}.XXXXXX") || return 1
if ! printf '%s\n' 'deb [signed-by=/usr/share/keyrings/xanmod-archive-keyring.gpg] https://deb.xanmod.org releases main' > "$source_tmp" ||
! chmod 644 "$source_tmp" ||
! mv -f "$source_tmp" "$XANMOD_SOURCE_FILE"; then
rm -f "$source_tmp"
err_msg "XanMod 软件源写入失败。"
return 1
fi
}
xanmod_install_kernel() {
local version
if ! version=$(xanmod_psabi_version); then
err_msg "无法探测 CPU 微架构等级,已中止内核安装。"
rm -f "$XANMOD_SOURCE_FILE"
return 1
fi
case "$version" in
1|2|3|4) ;;
*)
err_msg "CPU 微架构等级异常: $version"
rm -f "$XANMOD_SOURCE_FILE"
return 1
;;
esac
if ! apt-get update; then
err_msg "XanMod 软件源更新失败。"
rm -f "$XANMOD_SOURCE_FILE"
return 1
fi
if ! DEBIAN_FRONTEND=noninteractive apt-get install -y "linux-xanmod-x64v${version}"; then
err_msg "XanMod 内核安装失败(linux-xanmod-x64v${version})。"
rm -f "$XANMOD_SOURCE_FILE"
return 1
fi
rm -f "$XANMOD_SOURCE_FILE"
return 0
}
list_xanmod_packages() {
dpkg-query -W -f='${binary:Package}\t${db:Status-Abbrev}\n' 2>/dev/null |
awk -F '\t' '$2 == "ii " && $1 ~ /xanmod/ { print $1 }'
}
bbrv3() {
local fallback_kernel os_id arch
local -a xanmod_packages
root_use
if command -v dpkg-query &>/dev/null && list_xanmod_packages | grep -q .; then
while true; do
kernel_version=$(uname -r)
echo "您已安装xanmod的BBRv3内核"
echo "当前内核版本: $kernel_version"
echo ""
echo "内核管理"
echo "------------------------"
echo "1. 更新BBRv3内核 2. 卸载BBRv3内核"
echo "------------------------"
echo "0. 返回上一级选单"
echo "------------------------"
read -p "请输入你的选择: " sub_choice
case $sub_choice in
1)
# 先安装并写入启动菜单,避免更新失败后没有可启动的旧内核
if xanmod_add_repo && xanmod_install_kernel && update-grub; then
echo "XanMod内核已更新,旧内核已保留作为回退。重启后生效"
server_reboot
else
err_msg "XanMod 内核更新失败,旧内核保持不变。"
fi
;;
2)
xanmod_packages=()
case "$OS_ID" in
ubuntu) fallback_kernel=linux-generic ;;
debian) fallback_kernel=linux-image-amd64 ;;
esac
mapfile -t xanmod_packages < <(list_xanmod_packages)
if [ "${#xanmod_packages[@]}" -eq 0 ]; then
err_msg "未找到已安装的 XanMod 软件包。"
elif apt-get update &&
DEBIAN_FRONTEND=noninteractive apt-get install -y -- "$fallback_kernel" &&
DEBIAN_FRONTEND=noninteractive apt-get purge -y -- "${xanmod_packages[@]}" &&
update-grub; then
rm -f "$XANMOD_SOURCE_FILE"
echo "XanMod内核已卸载,并已确认系统回退内核可用。重启后生效"
server_reboot
else
err_msg "无法确认回退内核或卸载失败,已取消自动重启。"
fi
;;
0)
break # 跳出循环,退出菜单
;;
*)
break # 跳出循环,退出菜单
;;
esac
done
else
clear
echo "请备份数据,将为你升级Linux内核开启BBR3"
echo "官网介绍: https://xanmod.org/"
echo "------------------------------------------------"
echo "仅支持Debian/Ubuntu 仅支持x86_64架构"
echo "VPS是512M内存的,请提前添加1G虚拟内存,防止因内存不足失联!"
echo "------------------------------------------------"
read -p "确定继续吗?(Y/N): " choice
case "$choice" in
[Yy])
# 注意:这里必须用 return,不能用 break —— 当前不在任何循环内,
# break 只会报错并继续往下执行,导致在不支持的系统上照样改内核和 sysctl
if [ -r /etc/os-release ]; then
# 放到子 shell 里取值,避免污染当前 shell 的 ID/VERSION 等变量
os_id=$(. /etc/os-release && echo "$ID")
if [ "$os_id" != "debian" ] && [ "$os_id" != "ubuntu" ]; then
err_msg "当前环境不支持,仅支持Debian和Ubuntu系统"
return 1
fi
else
err_msg "无法确定操作系统类型"
return 1
fi
# 检查系统架构
arch=$(dpkg --print-architecture 2>/dev/null)
if [ "$arch" != "amd64" ]; then
err_msg "当前环境不支持,仅支持x86_64架构"
return 1
fi
new_swap=1024
ensure_swap || return 1
xanmod_add_repo || return 1
xanmod_install_kernel || return 1
update-grub || {
err_msg "更新启动菜单失败,已取消自动重启。"
return 1
}
# 步骤5:启用BBR3
if ! set_sysctl net.core.default_qdisc fq_pie ||
! set_sysctl net.ipv4.tcp_congestion_control bbr; then
err_msg "写入 BBR3 内核参数失败,已取消自动重启。"
return 1
fi
echo "XanMod内核安装并BBR3启用成功。重启后生效"
server_reboot
;;
[Nn])
echo "已取消"
;;
*)
echo "无效的选择,请输入 Y 或 N。"
;;
esac
fi
}
kejilion_sh() {
while true; do
clear
echo -e "${kjlan}_ _ ____ _ _ _ _ ____ _ _ "
echo "|_/ |___ | | | | | | |\ | "
echo "| \_ |___ _| | |___ | |__| | \| "
echo " "
echo -e "${kjlan}科技lion一键脚本工具 v$sh_v (仅支持 Ubuntu / Debian${bai}"
echo -e "${kjlan}-输入${huang}k${kjlan}可快速启动此脚本-${bai}"
echo "------------------------"
echo "1. 系统信息查询"
echo "2. 系统更新"
echo "3. 系统清理"
echo "5. BBR管理 ▶"
echo "8. 测试脚本合集 ▶ "
echo "13. 系统工具 ▶ "
echo "------------------------"
echo "00. 脚本更新"
echo "------------------------"
echo "0. 退出脚本"
echo "------------------------"
read -p "请输入你的选择: " choice
case $choice in
1)
clear
# 函数: 获取IPv4和IPv6地址
ip_address
if [ "$(uname -m)" == "x86_64" ]; then
cpu_info=$(cat /proc/cpuinfo | grep 'model name' | uniq | sed -e 's/model name[[:space:]]*: //')
else
cpu_info=$(lscpu | grep 'BIOS Model name' | awk -F': ' '{print $2}' | sed 's/^[ \t]*//')
fi
cpu_usage_percent=$(awk '{u=$2+$4; t=$2+$4+$5; if (NR==1){u1=u; t1=t;} else printf "%.0f\n", (($2+$4-u1) * 100 / (t-t1))}' \
<(grep 'cpu ' /proc/stat) <(sleep 1; grep 'cpu ' /proc/stat))
cpu_cores=$(nproc)
mem_info=$(free -b | awk 'NR==2{printf "%.2f/%.2f MB (%.2f%%)", $3/1024/1024, $2/1024/1024, $3*100/$2}')
disk_info=$(df -h | awk '$NF=="/"{printf "%s/%s (%s)", $3, $2, $5}')
# 加超时,避免无外网时整个信息页卡住
country=$(curl -s --max-time 5 ipinfo.io/country)
city=$(curl -s --max-time 5 ipinfo.io/city)
isp_info=$(curl -s --max-time 5 ipinfo.io/org)
cpu_arch=$(uname -m)
hostname=$(hostname)
kernel_version=$(uname -r)
congestion_algorithm=$(sysctl -n net.ipv4.tcp_congestion_control)
queue_algorithm=$(sysctl -n net.core.default_qdisc)
# 尝试使用 lsb_release 获取系统信息
os_info=$(lsb_release -ds 2>/dev/null)
# 如果 lsb_release 命令失败,则尝试其他方法
if [ -z "$os_info" ]; then
# 检查常见的发行文件
if [ -f "/etc/os-release" ]; then
os_info=$(source /etc/os-release && echo "$PRETTY_NAME")
elif [ -f "/etc/debian_version" ]; then
os_info="Debian $(cat /etc/debian_version)"
else
os_info="Unknown"
fi
fi
output_status
current_time=$(date "+%Y-%m-%d %I:%M %p")
# 用 Swap 行定位而非固定行号;强制 C locale,否则中文系统下 free 的行标签
# 会被本地化,/^Swap:/ 匹配不到而恒显示 0MB/0MB
swap_used=$(LC_ALL=C free -m | awk '/^Swap:/{print $3}')
swap_total=$(LC_ALL=C free -m | awk '/^Swap:/{print $2}')
swap_used=${swap_used:-0}
swap_total=${swap_total:-0}
if [ "$swap_total" -eq 0 ]; then
swap_percentage=0
else
swap_percentage=$((swap_used * 100 / swap_total))
fi
swap_info="${swap_used}MB/${swap_total}MB (${swap_percentage}%)"
runtime=$(cat /proc/uptime | awk -F. '{run_days=int($1 / 86400);run_hours=int(($1 % 86400) / 3600);run_minutes=int(($1 % 3600) / 60); if (run_days > 0) printf("%d天 ", run_days); if (run_hours > 0) printf("%d时 ", run_hours); printf("%d分\n", run_minutes)}')
timezone=$(current_timezone)
echo ""
echo "系统信息查询"
echo "------------------------"
echo "主机名: $hostname"
echo "运营商: $isp_info"
echo "------------------------"
echo "系统版本: $os_info"
echo "Linux版本: $kernel_version"
echo "------------------------"
echo "CPU架构: $cpu_arch"
echo "CPU型号: $cpu_info"
echo "CPU核心数: $cpu_cores"
echo "------------------------"
echo "CPU占用: $cpu_usage_percent%"
echo "物理内存: $mem_info"
echo "虚拟内存: $swap_info"
echo "硬盘占用: $disk_info"
echo "------------------------"
echo "$output"
echo "------------------------"
echo "网络拥堵算法: $congestion_algorithm $queue_algorithm"
echo "------------------------"
echo "公网IPv4地址: $ipv4_address"
echo "公网IPv6地址: $ipv6_address"
echo "------------------------"
echo "地理位置: $country $city"
echo "系统时区: $timezone"
echo "系统时间: $current_time"
echo "------------------------"
echo "系统运行时长: $runtime"
echo
;;
2)
clear
linux_update
;;
3)
clear
linux_clean
;;
5)
clear
tcpx_dir=$(mktemp -d) || {
err_msg "无法创建临时目录。"
echo "按任意键继续..."
read -n 1 -s -r -p ""
continue
}
if download_file "${GITHUB_RAW_URL}/ylx2016/Linux-NetSpeed/master/tcpx.sh" "${tcpx_dir}/tcpx.sh"; then
bash "${tcpx_dir}/tcpx.sh"
fi
rm -rf "$tcpx_dir"
;;
8)
while true; do
clear
echo "▶ 测试脚本合集"
echo ""
echo "----IP及解锁状态检测-----------"
echo "1. ChatGPT解锁状态检测"
echo "2. Region流媒体解锁测试"
echo "3. yeahwu流媒体解锁检测"
echo "4. xykt_IP质量体检脚本"
echo ""
echo "----网络线路测速-----------"
echo "11. besttrace三网回程延迟路由测试"
echo "12. mtr_trace三网回程线路测试"
echo "13. Superspeed三网测速"
echo "14. nxtrace快速回程测试脚本"
echo "15. nxtrace指定IP回程测试脚本"
echo "16. ludashi2020三网线路测试"
echo "17. i-abc多功能测速脚本"
echo ""
echo "----硬件性能测试----------"
echo "21. yabs性能测试"
echo "22. GB5 CPU性能测试脚本"
echo ""
echo "----综合性测试-----------"
echo "31. bench性能测试"
echo "32. spiritysdx融合怪测评"
echo ""
echo "------------------------"
echo "0. 返回主菜单"
echo "------------------------"
read -p "请输入你的选择: " sub_choice
case $sub_choice in
1)
clear
bash <(curl -Ls https://cdn.jsdelivr.net/gh/missuo/OpenAI-Checker/openai.sh)
;;
2)
clear
bash <(curl -L -s check.unlock.media)
;;
3)
clear
install wget
wget -qO- https://github.com/yeahwu/check/raw/main/check.sh | bash
;;
4)
clear
bash <(curl -Ls IP.Check.Place)
;;
11)
clear
install wget
wget -qO- git.io/besttrace | bash
;;
12)
clear
curl "${GITHUB_RAW_URL}/zhucaidan/mtr_trace/main/mtr_trace.sh" | bash
;;
13)
clear
bash <(curl -Lso- https://git.io/superspeed_uxh)
;;
14)
clear
curl nxtrace.org/nt |bash
nexttrace --fast-trace --tcp
;;
15)
clear
echo "可参考的IP列表"
echo "------------------------"
echo "北京电信: 219.141.136.12"
echo "北京联通: 202.106.50.1"
echo "北京移动: 221.179.155.161"
echo "上海电信: 202.96.209.133"
echo "上海联通: 210.22.97.1"
echo "上海移动: 211.136.112.200"
echo "广州电信: 58.60.188.222"
echo "广州联通: 210.21.196.6"
echo "广州移动: 120.196.165.24"
echo "成都电信: 61.139.2.69"
echo "成都联通: 119.6.6.6"
echo "成都移动: 211.137.96.205"
echo "湖南电信: 36.111.200.100"
echo "湖南联通: 42.48.16.100"
echo "湖南移动: 39.134.254.6"
echo "------------------------"
read -p "输入一个指定IP: " testip
curl nxtrace.org/nt |bash
nexttrace $testip
;;
16)
clear
curl "${GITHUB_RAW_URL}/ludashi2020/backtrace/main/install.sh" -sSf | sh
;;
17)
clear
bash <(curl -sL "${GITHUB_RAW_URL}/i-abc/Speedtest/main/speedtest.sh")
;;
21)
clear
# swap 只是低内存机器的兜底,失败不应中断测试
new_swap=1024
ensure_swap || echo -e "${huang}未能调整虚拟内存,将直接开始测试。${bai}"
curl -sL yabs.sh | bash -s -- -i -5
;;
22)
clear
new_swap=1024
ensure_swap || echo -e "${huang}未能调整虚拟内存,将直接开始测试。${bai}"
bash <(curl -sL "${GITHUB_RAW_URL}/i-abc/GB5/main/gb5-test.sh")
;;
31)
clear
curl -Lso- bench.sh | bash
;;
32)
clear
curl -L https://gitlab.com/spiritysdx/za/-/raw/main/ecs.sh -o ecs.sh && chmod +x ecs.sh && bash ecs.sh
;;
0)
kejilion
;;
*)
echo "无效的输入!"
;;
esac
break_end
done
;;
13)
while true; do
clear
echo "▶ 系统工具"
echo "------------------------"
echo "1. 设置脚本启动快捷键 2. 修改登录密码"
echo "7. 优化DNS地址 10. 切换优先ipv4/ipv6"
echo "------------------------"
echo "11. 查看端口占用状态 12. 修改虚拟内存大小"
echo "13. 用户管理 14. 用户/密码生成器"
echo "15. 系统时区调整 16. 设置BBR3加速"
echo "18. 修改主机名"
echo "19. 切换系统更新源 20. 定时任务管理"
echo "------------------------"
echo "21. 本机host解析 22. fail2banSSH防御程序"
echo "23. 限流自动关机"
echo "------------------------"
echo "0. 返回主菜单"
echo "------------------------"
read -p "请输入你的选择: " sub_choice
case $sub_choice in
1)
clear
read -r -p "请输入你的快捷按键: " kuaijiejian
if ! [[ "$kuaijiejian" =~ ^[a-zA-Z_][a-zA-Z0-9_]*$ ]]; then
err_msg "快捷键只能包含字母、数字和下划线,且不能以数字开头。"
else
sed -i "/^alias ${kuaijiejian}=/d" "$HOME/.bashrc" 2>/dev/null
printf 'alias %s=%q\n' "$kuaijiejian" "bash $SCRIPT_PATH" >> "$HOME/.bashrc"
echo "快捷键已设置,请重新登录终端或执行 source ~/.bashrc 后生效"
fi
;;
2)
clear
echo "设置你的登录密码"
passwd
;;
7)
root_use
echo "当前DNS地址"
echo "------------------------"
cat /etc/resolv.conf
echo "------------------------"
echo ""
# 询问用户是否要优化DNS设置
read -p "是否要设置DNS地址?(y/n): " choice
if [ "$choice" == "y" ]; then
read -p "1. 国外DNS优化 2. 国内DNS优化 0. 退出 : " Limiting
case "$Limiting" in
1)
dns1_ipv4="1.1.1.1"
dns2_ipv4="8.8.8.8"
dns1_ipv6="2606:4700:4700::1111"
dns2_ipv6="2001:4860:4860::8888"
set_dns
;;
2)
dns1_ipv4="223.5.5.5"
dns2_ipv4="183.60.83.19"
dns1_ipv6="2400:3200::1"
dns2_ipv6="2400:da00::6666"
set_dns
;;
0)
echo "已取消"
;;
*)
echo "无效的选择,请输入 0、1 或 2。"
;;
esac
else
echo "DNS设置未更改"
fi
;;
10)
root_use
# 内核未编译 IPv6 时该 sysctl 不存在,取值为空会导致下面的整数比较报错
ipv6_disabled=$(sysctl -n net.ipv6.conf.all.disable_ipv6 2>/dev/null)
if [ -z "$ipv6_disabled" ]; then
err_msg "当前内核未启用 IPv6,无法切换网络优先级。"
break
fi
echo ""
if [ "$ipv6_disabled" -eq 1 ]; then
echo "当前网络优先级设置: IPv4 优先"
else
echo "当前网络优先级设置: IPv6 优先"
fi
echo "------------------------"
echo ""
echo "切换的网络优先级"
echo "------------------------"
echo "1. IPv4 优先 2. IPv6 优先"
echo "------------------------"
read -p "选择优先的网络: " choice
case $choice in
1)
sysctl -w net.ipv6.conf.all.disable_ipv6=1 > /dev/null 2>&1
echo "已切换为 IPv4 优先"
;;
2)
sysctl -w net.ipv6.conf.all.disable_ipv6=0 > /dev/null 2>&1
echo "已切换为 IPv6 优先"
;;
*)
echo "无效的选择"
;;
esac
;;
11)
clear
if command -v ss &>/dev/null; then
ss -tulnape
elif command -v netstat &>/dev/null; then
netstat -tulnape
else
install iproute2 && ss -tulnape
fi
;;
12)
root_use
# 获取当前交换空间信息(强制 C locale,避免中文系统下标签被本地化)
swap_used=$(LC_ALL=C free -m | awk '/^Swap:/{print $3}')
swap_total=$(LC_ALL=C free -m | awk '/^Swap:/{print $2}')
swap_used=${swap_used:-0}
swap_total=${swap_total:-0}
if [ "$swap_total" -eq 0 ]; then
swap_percentage=0
else
swap_percentage=$((swap_used * 100 / swap_total))
fi
swap_info="${swap_used}MB/${swap_total}MB (${swap_percentage}%)"
echo "当前虚拟内存: $swap_info"
read -p "是否调整大小?(Y/N): " choice
case "$choice" in
[Yy])
# 输入新的虚拟内存大小
read -p "请输入虚拟内存大小MB: " new_swap
add_swap
;;
[Nn])
echo "已取消"
;;
*)
echo "无效的选择,请输入 Y 或 N。"
;;
esac
;;
13)
while true; do
root_use
# 显示所有用户、用户权限、用户组和是否在sudoers中
echo "用户列表"
echo "----------------------------------------------------------------------------"
printf "%-20s %-30s %-20s %-10s\n" "用户名" "主目录" "用户组" "sudo权限"
# /etc/passwd 共 7 个字段: 用户名:密码:UID:GID:备注:主目录:登录shell
while IFS=: read -r username _ userid groupid _ homedir shell; do
user_groups=$(groups "$username" 2>/dev/null | cut -d: -f2- | sed 's/^ *//')
if sudo -n -lU "$username" 2>/dev/null | grep -q '(ALL'; then
sudo_status="Yes"
else
sudo_status="No"
fi
printf "%-20s %-30s %-20s %-10s\n" "$username" "$homedir" "$user_groups" "$sudo_status"
done < /etc/passwd
echo ""
echo "账户操作"
echo "------------------------"
echo "1. 创建普通账户 2. 创建高级账户"
echo "------------------------"
echo "3. 授予 sudo 权限 4. 撤销脚本授予的 sudo 权限"
echo "------------------------"
echo "5. 删除账号"
echo "------------------------"
echo "0. 返回上一级选单"
echo "------------------------"
read -p "请输入你的选择: " sub_choice
case $sub_choice in
1)
# 提示用户输入新用户名
read -p "请输入新用户名: " new_username
if ! valid_username "$new_username"; then
err_msg "用户名格式不合法。"
elif id "$new_username" &>/dev/null; then
err_msg "用户 $new_username 已存在。"
elif useradd -m -s /bin/bash "$new_username"; then
if passwd "$new_username"; then
echo "操作已完成。"
else
if userdel -r "$new_username" >/dev/null 2>&1; then
err_msg "密码设置失败,已回滚新建账户。"
else
err_msg "密码设置失败,且新建账户无法自动回滚,请人工检查: $new_username"
fi
fi
else
err_msg "账户创建失败。"
fi
;;
2)
# 提示用户输入新用户名
read -p "请输入新用户名: " new_username
if ! valid_username "$new_username"; then
err_msg "用户名格式不合法。"
elif id "$new_username" &>/dev/null; then
err_msg "用户 $new_username 已存在。"
elif useradd -m -s /bin/bash "$new_username"; then
if passwd "$new_username" && grant_sudo "$new_username"; then
echo "操作已完成。"
else
rm -f "/etc/sudoers.d/kejilion-$new_username"
if userdel -r "$new_username" >/dev/null 2>&1; then
err_msg "密码设置或 sudo 授权失败,已回滚新建账户。"
else
err_msg "密码设置或 sudo 授权失败,且新建账户无法自动回滚,请人工检查: $new_username"
fi
fi
else
err_msg "账户创建失败,未授予 sudo 权限。"
fi
;;
3)
read -p "请输入用户名: " username
if ! id "$username" &>/dev/null; then
err_msg "用户 $username 不存在。"
elif ! valid_existing_username "$username"; then
err_msg "用户名格式不合法。"
elif grant_sudo "$username"; then
echo "操作已完成。"
fi
;;
4)
read -p "请输入用户名: " username
if revoke_sudo "$username"; then
echo "操作已完成。"
fi
;;
5)
read -p "请输入要删除的用户名: " username
if [ "$username" = "root" ]; then
err_msg "不允许删除 root 用户。"
elif ! id "$username" &>/dev/null; then
err_msg "用户 $username 不存在。"
elif ! valid_existing_username "$username"; then
err_msg "用户名格式不合法。"
elif [ "$username" = "$(id -un)" ]; then
err_msg "不允许删除当前正在运行脚本的用户。"
else
sudo_rule="$username ALL=(ALL:ALL) ALL"
if { [ ! -e "/etc/sudoers.d/kejilion-$username" ] &&
! grep -Fqx -- "$sudo_rule" /etc/sudoers; } || revoke_sudo "$username"; then
if userdel -r "$username"; then
echo "用户 $username 已删除。"
else
err_msg "删除用户 $username 失败。"
fi
else
err_msg "无法安全清理用户的 sudo 规则,已取消删除。"
fi
fi
;;
0)
break # 跳出循环,退出菜单
;;
*)
break # 跳出循环,退出菜单
;;
esac
done
;;
14)
clear
echo "随机用户名"
echo "------------------------"
for i in {1..5}; do
username="user$(< /dev/urandom tr -dc _a-z0-9 | head -c6)"
echo "随机用户名 $i: $username"
done
echo ""
echo "随机姓名"
echo "------------------------"
first_names=("John" "Jane" "Michael" "Emily" "David" "Sophia" "William" "Olivia" "James" "Emma" "Ava" "Liam" "Mia" "Noah" "Isabella")
last_names=("Smith" "Johnson" "Brown" "Davis" "Wilson" "Miller" "Jones" "Garcia" "Martinez" "Williams" "Lee" "Gonzalez" "Rodriguez" "Hernandez")
# 生成5个随机用户姓名
for i in {1..5}; do
first_name_index=$((RANDOM % ${#first_names[@]}))
last_name_index=$((RANDOM % ${#last_names[@]}))
user_name="${first_names[$first_name_index]} ${last_names[$last_name_index]}"
echo "随机用户姓名 $i: $user_name"
done
echo ""
echo "随机UUID"
echo "------------------------"
for i in {1..5}; do
uuid=$(cat /proc/sys/kernel/random/uuid)
echo "随机UUID $i: $uuid"
done
echo ""
echo "16位随机密码"
echo "------------------------"
for i in {1..5}; do
password=$(< /dev/urandom tr -dc _A-Z-a-z-0-9 | head -c16)
echo "随机密码 $i: $password"
done
echo ""
echo "32位随机密码"
echo "------------------------"
for i in {1..5}; do
password=$(< /dev/urandom tr -dc _A-Z-a-z-0-9 | head -c32)
echo "随机密码 $i: $password"
done
echo ""
;;
15)
root_use
while true; do
echo "系统时间信息"
# 获取当前系统时区
timezone=$(current_timezone)
# 获取当前系统时间
current_time=$(date +"%Y-%m-%d %H:%M:%S")
# 显示时区和时间
echo "当前系统时区:$timezone"
echo "当前系统时间:$current_time"
echo ""
echo "时区切换"
echo "亚洲------------------------"
echo "1. 中国上海时间 2. 中国香港时间"
echo "3. 日本东京时间 4. 韩国首尔时间"
echo "5. 新加坡时间 6. 印度加尔各答时间"
echo "7. 阿联酋迪拜时间 8. 澳大利亚悉尼时间"
echo "欧洲------------------------"
echo "11. 英国伦敦时间 12. 法国巴黎时间"
echo "13. 德国柏林时间 14. 俄罗斯莫斯科时间"
echo "15. 荷兰尤特赖赫特时间 16. 西班牙马德里时间"
echo "美洲------------------------"
echo "21. 美国西部时间 22. 美国东部时间"
echo "23. 加拿大时间 24. 墨西哥时间"
echo "25. 巴西时间 26. 阿根廷时间"
echo "------------------------"
echo "0. 返回上一级选单"
echo "------------------------"
read -p "请输入你的选择: " sub_choice
case $sub_choice in
1) set_timedate Asia/Shanghai ;;
2) set_timedate Asia/Hong_Kong ;;
3) set_timedate Asia/Tokyo ;;
4) set_timedate Asia/Seoul ;;
5) set_timedate Asia/Singapore ;;
6) set_timedate Asia/Kolkata ;;
7) set_timedate Asia/Dubai ;;
8) set_timedate Australia/Sydney ;;
11) set_timedate Europe/London ;;
12) set_timedate Europe/Paris ;;
13) set_timedate Europe/Berlin ;;
14) set_timedate Europe/Moscow ;;
15) set_timedate Europe/Amsterdam ;;
16) set_timedate Europe/Madrid ;;
21) set_timedate America/Los_Angeles ;;
22) set_timedate America/New_York ;;
23) set_timedate America/Vancouver ;;
24) set_timedate America/Mexico_City ;;
25) set_timedate America/Sao_Paulo ;;
26) set_timedate America/Argentina/Buenos_Aires ;;
0) break ;; # 跳出循环,退出菜单
*) break ;; # 跳出循环,退出菜单
esac
done
;;
16)
bbrv3
;;
18)
root_use
current_hostname=$(hostname)
echo "当前主机名: $current_hostname"
read -p "是否要更改主机名?(y/n): " answer
if [[ "${answer,,}" == "y" ]]; then
# 获取新的主机名
read -p "请输入新的主机名: " new_hostname
if valid_hostname "$new_hostname"; then
if hostnamectl set-hostname "$new_hostname"; then
systemctl restart systemd-hostnamed
echo "主机名已更改为: $new_hostname"
else
err_msg "主机名修改失败。"
fi
else
err_msg "主机名格式不合法。未更改主机名。"
fi
else
echo "未更改主机名。"
fi
;;
19)
root_use
# 直接复用脚本开头已解析好的 OS_ID,避免 source /etc/os-release
# 把 ID/VERSION/PRETTY_NAME 等一并注入当前 shell
case "$(dpkg --print-architecture 2>/dev/null)" in
amd64|i386)
aliyun_ubuntu_source="https://mirrors.aliyun.com/ubuntu/"
official_ubuntu_source="https://archive.ubuntu.com/ubuntu/"
official_ubuntu_security_source="https://security.ubuntu.com/ubuntu/"
;;
*)
aliyun_ubuntu_source="https://mirrors.aliyun.com/ubuntu-ports/"
official_ubuntu_source="https://ports.ubuntu.com/ubuntu-ports/"
official_ubuntu_security_source="$official_ubuntu_source"
;;
esac
aliyun_debian_source="https://mirrors.aliyun.com/debian/"
official_debian_source="https://deb.debian.org/debian/"
list_apt_source_files() {
local file pattern
case "$OS_ID" in
ubuntu) pattern='(([^./[:space:]]+\.)?archive\.ubuntu\.com|security\.ubuntu\.com|ports\.ubuntu\.com|mirrors\.aliyun\.com)/(ubuntu|ubuntu-ports)' ;;
debian) pattern='(debian\.org|debian\.net|aliyun\.com)/(debian|debian-security)' ;;
*) return 1 ;;
esac
for file in /etc/apt/sources.list /etc/apt/sources.list.d/*.list /etc/apt/sources.list.d/*.sources; do
[ -f "$file" ] || continue
case "$file" in
*.sources)
grep -qE '^[[:space:]]*URIs:[[:space:]]*' "$file" &&
grep -qE "$pattern" "$file" && printf '%s\n' "$file"
;;
*)
grep -qE "^[[:space:]]*(deb|deb-src)[[:space:]].*${pattern}" "$file" && printf '%s\n' "$file"
;;
esac
done
}
mapfile -t apt_source_files < <(list_apt_source_files)
if [ "${#apt_source_files[@]}" -eq 0 ]; then
err_msg "未找到可识别的 APT 更新源配置。"
break
fi
backup_sources() {
local file backup created=()
for file in "${apt_source_files[@]}"; do
backup="${file}.kejilion.bak"
if [ -f "$backup" ] && [ ! -L "$backup" ]; then
# 已存在是预期结果而非错误:本函数只保存初始源,不覆盖
echo "初始备份已存在,跳过: $backup"
continue
elif [ -e "$backup" ] || [ -L "$backup" ]; then
err_msg "备份路径不是普通文件,已取消操作: $backup"
rm -f "${created[@]}"
return 1
fi
if cp -- "$file" "$backup"; then
created+=("$backup")
else
rm -f "$backup" "${created[@]}"
err_msg "备份更新源失败,已清理本次生成的备份。"
return 1
fi
done
echo "初始更新源备份已就绪。"
}
ensure_source_backup() {
local file
for file in "${apt_source_files[@]}"; do
if [ ! -f "${file}.kejilion.bak" ] || [ -L "${file}.kejilion.bak" ]; then
backup_sources || return 1
break
fi
done
for file in "${apt_source_files[@]}"; do
[ -f "${file}.kejilion.bak" ] && [ ! -L "${file}.kejilion.bak" ] || return 1
done
}
restore_initial_source() {
local file saved rollback_dir ready=1
rollback_dir=$(mktemp -d) || return 1
for file in "${apt_source_files[@]}"; do
saved="$rollback_dir/$(printf '%s' "$file" | sed 's|/|_|g')"
if [ ! -f "${file}.kejilion.bak" ] || [ -L "${file}.kejilion.bak" ] ||
! cp -- "$file" "$saved"; then
ready=0
break
fi
done
if [ "$ready" -eq 1 ]; then
for file in "${apt_source_files[@]}"; do
cp -- "${file}.kejilion.bak" "$file" || { ready=0; break; }
done
fi
if [ "$ready" -eq 1 ] && apt-get update; then
rm -rf "$rollback_dir"
echo "已还原初始更新源并验证成功。"
return 0
fi
local rollback_ok=1
for file in "${apt_source_files[@]}"; do
saved="$rollback_dir/$(printf '%s' "$file" | sed 's|/|_|g')"
if [ -f "$saved" ] && ! cp -- "$saved" "$file"; then
rollback_ok=0
fi
done
if [ "$rollback_ok" -eq 1 ]; then
rm -rf "$rollback_dir"
err_msg "还原或验证失败,已恢复还原前的配置。"
else
err_msg "还原失败且部分配置无法回滚,事务快照保留在: $rollback_dir"
fi
return 1
}
switch_source() {
local mode="$1" file tmp_file rollback_dir saved failed=0 processed=0 is_deb822
case "$mode" in
mirror|official) ;;
*)
err_msg "未知的更新源切换模式: $mode"
return 1
;;
esac
rollback_dir=$(mktemp -d) || return 1
# 先完成所有快照,再开始修改,避免快照中途失败时留下半套新配置。
for file in "${apt_source_files[@]}"; do
saved="$rollback_dir/$(printf '%s' "$file" | sed 's|/|_|g')"
if ! cp -- "$file" "$saved"; then
failed=1
break
fi
done
if [ "$failed" -eq 0 ]; then
for file in "${apt_source_files[@]}"; do
tmp_file=$(mktemp "${file}.XXXXXX") || { failed=1; break; }
case "$file" in
*.sources) is_deb822=1 ;;
*) is_deb822=0 ;;
esac
if ! awk -v mode="$mode" -v distro="$OS_ID" \
-v ubuntu_archive="$official_ubuntu_source" -v ubuntu_mirror="$aliyun_ubuntu_source" \
-v ubuntu_security="$official_ubuntu_security_source" -v deb822="$is_deb822" \
-v debian_archive="$official_debian_source" -v debian_mirror="$aliyun_debian_source" '
BEGIN {
FS = "\n"
if (deb822) {
RS = ""
}
}
function replacement(uri, security) {
if (distro == "ubuntu" && uri ~ "^https?://mirrors[.]aliyun[.]com/(ubuntu|ubuntu-ports)/?$")
return mode == "mirror" ? ubuntu_mirror : (security ? ubuntu_security : ubuntu_archive)
if (distro == "ubuntu" && uri ~ "^https?://([^/]+[.])?archive[.]ubuntu[.]com/ubuntu/?$")
return mode == "mirror" ? ubuntu_mirror : ubuntu_archive
if (distro == "ubuntu" && uri ~ "^https?://security[.]ubuntu[.]com/ubuntu/?$")
return mode == "mirror" ? ubuntu_mirror : ubuntu_security
if (distro == "ubuntu" && uri ~ "^https?://ports[.]ubuntu[.]com/ubuntu-ports/?$")
return mode == "mirror" ? ubuntu_mirror : ubuntu_archive
if (distro == "debian" && uri ~ "^https?://[^/]*(debian[.]org|debian[.]net|aliyun[.]com)/debian-security/?$")
return mode == "mirror" ? "https://mirrors.aliyun.com/debian-security/" : "https://security.debian.org/debian-security/"
if (distro == "debian" && uri ~ "^https?://[^/]*(debian[.]org|debian[.]net|aliyun[.]com)/debian/?$")
return mode == "mirror" ? debian_mirror : debian_archive
return uri
}
function rewrite(line, security, fields, count, i, result) {
sub(/^[[:space:]]+/, "", line)
count = split(line, fields, /[[:space:]]+/)
result = fields[1]
for (i = 2; i <= count; i++)
result = result " " replacement(fields[i], security)
return result
}
function rewrite_continuation(line, security, fields, count, i, result) {
sub(/^[[:space:]]+/, "", line)
count = split(line, fields, /[[:space:]]+/)
result = ""
for (i = 1; i <= count; i++)
result = result " " replacement(fields[i], security)
return result
}
{
# FS="\n" 下空行的 NF 为 0,末尾输出循环不会执行,
# 不特判会导致换源后原文件里的空行被静默删除
if (NF == 0) {
print ""
next
}
security = 0
if (deb822) {
in_suites = 0
for (i = 1; i <= NF; i++) {
if ($i ~ /^[[:space:]]*Suites:/) {
in_suites = 1
if ($i ~ /-security([[:space:]]|$)/) security = 1
} else if ($i ~ /^[[:space:]]+/) {
if (in_suites && $i ~ /-security([[:space:]]|$)/) security = 1
} else {
in_suites = 0
}
}
} else if ($0 ~ /-security([[:space:]]|$)/) {
security = 1
}
in_uris = 0
for (i = 1; i <= NF; i++) {
if ($i ~ /^[[:space:]]*URIs:[[:space:]]*/) {
$i = rewrite($i, security)
in_uris = 1
} else if (deb822 && in_uris && $i ~ /^[[:space:]]+/) {
$i = rewrite_continuation($i, security)
} else {
in_uris = 0
if ($i ~ /^[[:space:]]*deb(-src)?[[:space:]]/)
$i = rewrite($i, security)
}
}
for (i = 1; i <= NF; i++)
printf "%s%s", $i, (i < NF ? "\n" : (deb822 ? "\n\n" : "\n"))
}
' "$file" > "$tmp_file"; then
rm -f "$tmp_file"
failed=1
break
fi
if ! chmod --reference="$file" "$tmp_file" 2>/dev/null ||
! chown --reference="$file" "$tmp_file" 2>/dev/null ||
! mv -f "$tmp_file" "$file"; then
rm -f "$tmp_file"
failed=1
break
fi
processed=$((processed + 1))
done
fi
if [ "$failed" -eq 0 ] && [ "$processed" -eq "${#apt_source_files[@]}" ] && apt-get update; then
rm -rf "$rollback_dir"
return 0
fi
local rollback_ok=1
for file in "${apt_source_files[@]}"; do
saved="$rollback_dir/$(printf '%s' "$file" | sed 's|/|_|g')"
if [ -f "$saved" ] && ! cp -- "$saved" "$file"; then
rollback_ok=0
fi
done
if [ "$rollback_ok" -eq 1 ]; then
rm -rf "$rollback_dir"
apt-get update >/dev/null 2>&1
err_msg "新更新源验证失败,已恢复原配置。"
else
err_msg "新源验证失败且部分配置无法回滚,事务快照保留在: $rollback_dir"
fi
return 1
}
# 主菜单
while true; do
case "$OS_ID" in
ubuntu)
echo "Ubuntu 更新源切换脚本"
echo "------------------------"
;;
debian)
echo "Debian 更新源切换脚本"
echo "------------------------"
;;
*)
err_msg "未知系统,无法执行脚本"
break
;;
esac
echo "1. 切换到阿里云源"
echo "2. 切换到官方源"
echo "------------------------"
echo "3. 备份初始更新源(已备份则跳过)"
echo "4. 还原初始更新源"
echo "------------------------"
echo "0. 返回上一级"
echo "------------------------"
read -p "请选择操作: " choice
case $choice in
1)
if ensure_source_backup && switch_source mirror; then
echo "已切换到阿里云源"
fi
;;
2)
if ensure_source_backup && switch_source official; then
echo "已切换到官方源"
fi
;;
3)
backup_sources
;;
4)
restore_initial_source
;;
0)
break
;;
*)
echo "无效的选择,请重新输入"
;;
esac
break_end
done
;;
20)
while true; do
clear
echo "定时任务列表"
crontab -l 2>/dev/null
echo ""
echo "操作"
echo "------------------------"
echo "1. 添加定时任务 2. 删除定时任务 3. 编辑定时任务"
echo "------------------------"
echo "0. 返回上一级选单"
echo "------------------------"
read -p "请输入你的选择: " sub_choice
case $sub_choice in
1)
read -p "请输入新任务的执行命令: " newquest
if [ -z "$newquest" ]; then
err_msg "执行命令不能为空。"
echo "按任意键继续..."
read -n 1 -s -r -p ""
continue
fi
echo "------------------------"
echo "1. 每月任务 2. 每周任务"
echo "3. 每天任务 4. 每小时任务"
echo "------------------------"
read -p "请输入你的选择: " dingshi
case $dingshi in
1)
read -p "选择每月的几号执行任务? (1-28): " day
if [[ "$day" =~ ^[0-9]+$ ]] && [ "$day" -ge 1 ] && [ "$day" -le 28 ]; then
(crontab -l 2>/dev/null ; echo "0 0 $day * * $newquest") | crontab -
else
err_msg "日期必须是 1 到 28 之间的整数。"
fi
;;
2)
read -p "选择周几执行任务? (0-60代表星期日): " weekday
if [[ "$weekday" =~ ^[0-9]+$ ]] && [ "$weekday" -ge 0 ] && [ "$weekday" -le 6 ]; then
(crontab -l 2>/dev/null ; echo "0 0 * * $weekday $newquest") | crontab -
else
err_msg "星期必须是 0 到 6 之间的整数。"
fi
;;
3)
read -p "选择每天几点执行任务?(小时,0-23): " hour
if [[ "$hour" =~ ^[0-9]+$ ]] && [ "$hour" -ge 0 ] && [ "$hour" -le 23 ]; then
(crontab -l 2>/dev/null ; echo "0 $hour * * * $newquest") | crontab -
else
err_msg "小时必须是 0 到 23 之间的整数。"
fi
;;
4)
read -p "输入每小时的第几分钟执行任务?(分钟,0-59): " minute
if [[ "$minute" =~ ^[0-9]+$ ]] && [ "$minute" -ge 0 ] && [ "$minute" -le 59 ]; then
(crontab -l 2>/dev/null ; echo "$minute * * * * $newquest") | crontab -
else
err_msg "分钟必须是 0 到 59 之间的整数。"
fi
;;
*)
break # 跳出
;;
esac
;;
2)
read -p "请输入需要删除任务的关键字: " kquest
# 关键字为空时 grep -v "" 会匹配所有行,导致整份 crontab 被清空
if [ -z "$kquest" ]; then
err_msg "关键字不能为空,已取消删除。"
else
crontab -l 2>/dev/null | grep -vF "$kquest" | crontab - > /dev/null 2>&1
fi
;;
3)
crontab -e
;;
0)
break # 跳出循环,退出菜单
;;
*)
break # 跳出循环,退出菜单
;;
esac
done
;;
21)
root_use
while true; do
echo "本机host解析列表"
echo "如果你在这里添加解析匹配,将不再使用动态解析了"
cat /etc/hosts
echo ""
echo "操作"
echo "------------------------"
echo "1. 添加新的解析 2. 删除解析地址"
echo "------------------------"
echo "0. 返回上一级选单"
echo "------------------------"
read -p "请输入你的选择: " host_dns
case $host_dns in
1)
read -p "请输入新的解析记录 格式: 110.25.5.33 kejilion.pro : " addhost
if [ -z "$addhost" ]; then
err_msg "解析记录不能为空。"
else
echo "$addhost" >> /etc/hosts
fi
;;
2)
read -p "请输入需要删除的解析内容关键字: " delhost
# 关键字为空时 sed //d 会清空整个 /etc/hosts
if [ -z "$delhost" ]; then
err_msg "关键字不能为空,已取消删除。"
else
hosts_tmp=$(mktemp /etc/.hosts.kejilion.XXXXXX) || {
err_msg "无法创建 hosts 临时文件。"
continue
}
if awk -v keyword="$delhost" 'index($0, keyword) == 0' /etc/hosts > "$hosts_tmp" &&
chmod --reference=/etc/hosts "$hosts_tmp" 2>/dev/null &&
chown --reference=/etc/hosts "$hosts_tmp" 2>/dev/null &&
mv -f "$hosts_tmp" /etc/hosts; then
echo "匹配的解析记录已删除。"
else
rm -f "$hosts_tmp"
err_msg "更新 /etc/hosts 失败,已保留原文件。"
fi
fi
;;
0)
break # 跳出循环,退出菜单
;;
*)
break # 跳出循环,退出菜单
;;
esac
done
;;
22)
root_use
if docker inspect fail2ban &>/dev/null ; then
while true; do
clear
echo "SSH防御程序已启动"
echo "------------------------"
echo "1. 查看SSH拦截记录"
echo "2. 日志实时监控"
echo "------------------------"
echo "9. 卸载防御程序"
echo "------------------------"
echo "0. 退出"
echo "------------------------"
read -p "请输入你的选择: " sub_choice
case $sub_choice in
1)
echo "------------------------"
f2b_sshd
echo "------------------------"
;;
2)
tail -f /path/to/fail2ban/config/log/fail2ban/fail2ban.log
break
;;
9)
docker rm -f fail2ban
rm -rf /path/to/fail2ban
echo "Fail2Ban防御程序已卸载"
break
;;
0)
break
;;
*)
echo "无效的选择,请重新输入。"
;;
esac
break_end
done
elif [ -x "$(command -v fail2ban-client)" ] ; then
clear
echo "卸载旧版fail2ban"
read -p "确定继续吗?(Y/N): " choice
case "$choice" in
[Yy])
remove fail2ban
rm -rf /etc/fail2ban
echo "Fail2Ban防御程序已卸载"
;;
[Nn])
echo "已取消"
;;
*)
echo "无效的选择,请输入 Y 或 N。"
;;
esac
else
clear
echo "fail2ban是一个SSH防止暴力破解工具"
echo "官网介绍: https://github.com/fail2ban/fail2ban"
echo "------------------------------------------------"
echo "工作原理:研判非法IP恶意高频访问SSH端口,自动进行IP封锁"
echo "------------------------------------------------"
read -p "确定继续吗?(Y/N): " choice
case "$choice" in
[Yy])
clear
if ! install_docker; then
err_msg "Docker 环境准备失败。"
break
fi
# 只有安装/配置失败才回滚:慢机器上 fail2ban socket 尚未就绪时
# 状态查询会非零退出,此时容器其实是好的,不该被强删
if f2b_install_sshd; then
f2b_status || err_msg "Fail2Ban 已安装,但状态查询失败,可稍后用菜单查看运行状态确认。"
echo "Fail2Ban防御程序已开启"
else
docker rm -f fail2ban >/dev/null 2>&1
err_msg "Fail2Ban 安装或配置失败,已清理未完成的容器。"
fi
;;
[Nn])
echo "已取消"
;;
*)
echo "无效的选择,请输入 Y 或 N。"
;;
esac
fi
;;
23)
root_use
echo "当前流量使用情况,重启服务器流量计算会清零!"
output_status
echo "$output"
# 检查是否存在 Limiting_Shut_down.sh 文件
if [ -f ~/Limiting_Shut_down.sh ]; then
# 获取 threshold_gb 的值(使用 POSIX ERE,不依赖 PCRE
threshold_gb=$(grep -oE '^threshold_gb=[0-9]+' ~/Limiting_Shut_down.sh | head -n 1 | cut -d= -f2)
if [ -n "$threshold_gb" ]; then
echo -e "当前设置的限流阈值为 ${huang}${threshold_gb}${bai}GB"
else
err_msg "现有限流脚本的阈值配置异常。"
fi
else
echo -e "${hui}当前未启用限流关机功能${bai}"
fi
echo
echo "------------------------------------------------"
echo "系统每分钟检测一次流量,接收或发送任一方向到达阈值就会自动关闭服务器!每月1日重置流量重启服务器。"
read -p "1. 开启限流关机功能 2. 停用限流关机功能 0. 退出 : " Limiting
case "$Limiting" in
1)
# 输入新的流量阈值
echo "按单向流量计算(多数 VPS 只计出站)。如果实际服务器就100G流量,可设置阈值为95G,提前关机,以免出现流量误差或溢出."
read -p "请输入流量阈值(单位为GB: " threshold_gb
if ! [[ "$threshold_gb" =~ ^[0-9]+$ ]] || [ "$threshold_gb" -le 0 ]; then
err_msg "流量阈值必须是大于 0 的整数(单位 GB),当前输入: '${threshold_gb}'"
elif ! command -v crontab &>/dev/null; then
err_msg "未找到 crontab,无法启用限流关机功能。"
else
limit_script="$HOME/Limiting_Shut_down.sh"
tmp_limit_script=$(mktemp "$HOME/.Limiting_Shut_down.sh.XXXXXX") || {
err_msg "无法创建限流脚本临时文件。"
break
}
limit_backup=""
limit_script_existed=0
if [ -e "$limit_script" ]; then
limit_backup=$(mktemp "$HOME/.Limiting_Shut_down.sh.backup.XXXXXX") || {
rm -f "$tmp_limit_script"
err_msg "无法创建现有限流脚本的备份。"
break
}
if ! cp -p -- "$limit_script" "$limit_backup"; then
rm -f "$tmp_limit_script" "$limit_backup"
err_msg "无法备份现有限流脚本,已取消更新。"
break
fi
limit_script_existed=1
fi
if download_file "${UPDATE_RAW_URL}/Limiting_Shut_down.sh" "$tmp_limit_script" &&
sed -i "s/^threshold_gb=.*/threshold_gb=${threshold_gb}/" "$tmp_limit_script" &&
grep -q "^threshold_gb=${threshold_gb}$" "$tmp_limit_script" &&
bash -n "$tmp_limit_script" &&
chmod +x "$tmp_limit_script" &&
mv -f "$tmp_limit_script" "$limit_script"; then
if update_limit_cron enable "$limit_script"; then
[ -z "$limit_backup" ] || rm -f "$limit_backup"
echo "限流关机已设置"
else
if [ "$limit_script_existed" -eq 1 ]; then
if ! mv -f "$limit_backup" "$limit_script"; then
err_msg "定时任务写入失败,且旧限流脚本恢复失败: $limit_backup"
break
fi
else
rm -f "$limit_script"
fi
err_msg "定时任务写入失败,已恢复原限流脚本。"
fi
else
rm -f "$tmp_limit_script"
[ -z "$limit_backup" ] || rm -f "$limit_backup"
err_msg "限流关机脚本下载或校验失败,已保留原配置。"
fi
fi
;;
0)
echo "已取消"
;;
2)
if ! command -v crontab &>/dev/null; then
err_msg "未找到 crontab,无法安全停用定时任务。"
elif update_limit_cron disable "$HOME/Limiting_Shut_down.sh"; then
rm -f "$HOME/Limiting_Shut_down.sh"
echo "已关闭限流关机功能"
else
err_msg "定时任务更新失败,限流脚本已保留。"
fi
;;
*)
echo "无效的选择,请输入 0、1 或 2。"
;;
esac
;;
0)
kejilion
;;
*)
echo "无效的输入!"
;;
esac
break_end
done
;;
00)
clear
echo "更新日志"
echo "------------------------"
changelog=$(curl -fsSL --max-time 20 "${UPDATE_RAW_URL}/kejilion_sh_log.txt")
if [ -n "$changelog" ]; then
echo "全部日志: ${UPDATE_RAW_URL}/kejilion_sh_log.txt"
echo "------------------------"
echo "$changelog" | tail -n 35
else
echo "(当前更新源未提供更新日志)"
fi
echo "------------------------"
sh_v_new=$(curl -fsSL --max-time 20 "${UPDATE_RAW_URL}/kejilion.sh" | grep -o 'sh_v="[0-9.]*"' | head -n 1 | cut -d '"' -f 2)
if [ -z "$sh_v_new" ]; then
err_msg "无法获取远程版本信息,请检查网络或更新源: ${UPDATE_RAW_URL}"
elif [ "$sh_v" = "$sh_v_new" ]; then
echo -e "${lv}你已经是最新版本!${huang}v$sh_v${bai}"
elif ! version_gt "$sh_v_new" "$sh_v"; then
echo -e "${lv}本地版本 ${huang}v$sh_v${lv} 高于更新源版本 ${huang}v$sh_v_new${lv},已跳过更新。${bai}"
else
echo "发现新版本!"
echo -e "当前版本 v$sh_v 最新版本 ${huang}v$sh_v_new${bai}"
echo "------------------------"
read -p "确定更新脚本吗?(Y/N): " choice
case "$choice" in
[Yy])
clear
# 在脚本所在目录下载并原子替换,避免更新失败时截断当前脚本
script_dir=$(dirname "$SCRIPT_PATH")
tmp_sh=$(mktemp "${script_dir}/.kejilion.sh.XXXXXX") || {
err_msg "创建更新临时文件失败。"
echo "按任意键继续..."
read -n 1 -s -r -p ""
continue
}
if download_file "${UPDATE_RAW_URL}/kejilion.sh" "$tmp_sh" &&
bash -n "$tmp_sh" &&
downloaded_version=$(grep -m1 -oE '^sh_v="[0-9]+([.][0-9]+)*"$' "$tmp_sh" | cut -d '"' -f 2) &&
[ "$downloaded_version" = "$sh_v_new" ] &&
version_gt "$downloaded_version" "$sh_v" &&
chmod +x "$tmp_sh" &&
mv -f "$tmp_sh" "$SCRIPT_PATH"; then
cp "$SCRIPT_PATH" /usr/local/bin/k > /dev/null 2>&1
echo -e "${lv}脚本已更新到最新版本!${huang}v$sh_v_new${bai}"
break_end
exec bash "$SCRIPT_PATH"
else
rm -f "$tmp_sh"
err_msg "更新失败,已保留当前版本。"
fi
;;
[Nn])
echo "已取消"
;;
*)
;;
esac
fi
;;
0)
clear
exit
;;
*)
echo "无效的输入!"
;;
esac
break_end
done
}
if [ "$#" -eq 0 ]; then
# 如果没有参数,运行交互式逻辑
kejilion_sh
else
# 如果有参数,执行相应函数
case $1 in
install|add|安装)
shift
install "$@"
;;
remove|del|卸载)
shift
remove "$@"
;;
update|更新)
linux_update
;;
clean|清理)
linux_clean
;;
bbr3|bbrv3)
bbrv3
;;
*)
echo "无效参数"
;;
esac
fi