7513 lines
372 KiB
Bash
7513 lines
372 KiB
Bash
#!/bin/bash
|
||
# ================================================================
|
||
# 作者 : 鸡公头面板 jigongtou.uepopo.com
|
||
# 项目地址 : https://jigongtou.uepopo.com
|
||
# 全局帮助:https://claude.ai/
|
||
# ================================================================
|
||
|
||
# --- 全局函数与配置 ---
|
||
|
||
# ================================================================
|
||
# ⚙️ 统一配置区 — 端口/镜像/路径全部在此修改,无需搜索全文
|
||
# ================================================================
|
||
|
||
# ── 数据目录 ────────────────────────────────────────────────────
|
||
readonly DATA_ROOT="/root"
|
||
readonly MNT_ROOT="/mnt"
|
||
|
||
# ── 服务端口(宿主机端口,容器内端口不变)──────────────────────
|
||
readonly PORT_NEXTCLOUD=8888
|
||
readonly PORT_ONLYOFFICE=8889
|
||
readonly PORT_WORDPRESS=8890
|
||
readonly PORT_OLLAMA_UI=3001
|
||
readonly PORT_JELLYFIN=8096
|
||
readonly PORT_NAVIDROME=4533
|
||
readonly PORT_MINIFLUX=8091
|
||
readonly PORT_GITEA=3000
|
||
readonly PORT_GITEA_SSH=2222
|
||
readonly PORT_MEMOS=8080
|
||
readonly PORT_QBIT=6881
|
||
readonly PORT_JDOWNLOADER=5800
|
||
readonly PORT_METUBE=8999
|
||
readonly PORT_DRAWIO=8082
|
||
readonly PORT_N8N=5678
|
||
readonly PORT_UPTIME_KUMA=3002
|
||
readonly PORT_HA=8123
|
||
readonly PORT_SILLYTAVERN=8000
|
||
readonly PORT_GLANCES=61208
|
||
readonly PORT_VLESS=40001
|
||
readonly PORT_IMMICH=2283
|
||
readonly PORT_CALIBRE=8083
|
||
readonly PORT_KAVITA=5000
|
||
readonly PORT_GOTIFY=8085
|
||
readonly PORT_ACTUAL=5006
|
||
readonly PORT_EXCALIDRAW=8086
|
||
readonly PORT_GRAFANA=3003
|
||
readonly PORT_JOPLIN=22300
|
||
readonly PORT_WEBDAV=6086
|
||
readonly PORT_TRILIUM=8087
|
||
|
||
# ── Docker 镜像版本(需要锁定版本时在此修改)──────────────────
|
||
readonly IMG_NEXTCLOUD="nextcloud:latest"
|
||
readonly IMG_MARIADB="mariadb:11.4"
|
||
readonly IMG_REDIS="redis:alpine"
|
||
readonly IMG_ONLYOFFICE="onlyoffice/documentserver:latest"
|
||
readonly IMG_WORDPRESS="wordpress:latest"
|
||
readonly IMG_OLLAMA="ollama/ollama:latest"
|
||
readonly IMG_OPEN_WEBUI="ghcr.io/open-webui/open-webui:main"
|
||
readonly IMG_JELLYFIN="jellyfin/jellyfin:latest"
|
||
readonly IMG_NAVIDROME="deluan/navidrome:latest"
|
||
readonly IMG_MINIFLUX="miniflux/miniflux:latest"
|
||
readonly IMG_POSTGRES="postgres:15-alpine"
|
||
readonly IMG_GITEA="gitea/gitea:latest"
|
||
readonly IMG_MEMOS="ghcr.io/usememos/memos:latest"
|
||
readonly IMG_QBIT="lscr.io/linuxserver/qbittorrent:latest"
|
||
readonly IMG_JDOWNLOADER="jlesage/jdownloader-2"
|
||
readonly IMG_METUBE="ghcr.io/alexta69/metube:latest"
|
||
readonly IMG_DRAWIO="jgraph/drawio"
|
||
readonly IMG_N8N="n8nio/n8n:latest"
|
||
readonly IMG_UPTIME_KUMA="louislam/uptime-kuma:1"
|
||
readonly IMG_HA="ghcr.io/home-assistant/home-assistant:stable"
|
||
readonly IMG_SILLYTAVERN="ghcr.io/sillytavern/sillytavern:latest"
|
||
readonly IMG_GLANCES="nicolargo/glances:latest-full"
|
||
readonly IMG_XRAY="teddysun/xray:latest"
|
||
readonly IMG_IMMICH_SERVER="ghcr.io/immich-app/immich-server:release"
|
||
readonly IMG_IMMICH_ML="ghcr.io/immich-app/immich-machine-learning:release"
|
||
readonly IMG_CALIBRE="lscr.io/linuxserver/calibre-web:latest"
|
||
readonly IMG_KAVITA="jvmilazz0/kavita:latest"
|
||
readonly IMG_GOTIFY="gotify/server:latest"
|
||
readonly IMG_ACTUAL="actualbudget/actual-server:latest"
|
||
readonly IMG_EXCALIDRAW="excalidraw/excalidraw:latest"
|
||
readonly IMG_GRAFANA="grafana/grafana:latest"
|
||
readonly IMG_PROMETHEUS="prom/prometheus:latest"
|
||
readonly IMG_NODE_EXPORTER="prom/node-exporter:latest"
|
||
readonly IMG_CADVISOR="gcr.io/cadvisor/cadvisor:latest"
|
||
readonly IMG_JOPLIN_SERVER="joplin/server:latest"
|
||
readonly IMG_JOPLIN_DB="postgres:15-alpine"
|
||
readonly IMG_TRILIUM="zadam/trilium:latest"
|
||
|
||
# ── 时区 ──────────────────────────────────────────────────────
|
||
readonly TZ_DEFAULT="Asia/Shanghai"
|
||
|
||
# ================================================================
|
||
|
||
STATE_FILE="/root/.vps_setup_credentials" # 用于存储密码和配置的凭证文件
|
||
TUNNEL_CONFIG_FILE="/root/.cloudflared/config.yml"
|
||
RCLONE_CONFIG_FILE="/root/.config/rclone/rclone.conf"
|
||
RCLONE_LOG_FILE="/var/log/rclone.log"
|
||
|
||
# --- 环境检测全局变量 (由 detect_environment 填充) ---
|
||
SYS_ARCH="" # amd64 / arm64 / armv7
|
||
NET_MODE="" # dual / ipv4only / ipv6only
|
||
SYS_DISTRO="" # ubuntu / debian
|
||
SYS_CODENAME="" # focal / jammy / bookworm 等
|
||
IS_IPV6_ONLY=false # 纯IPv6环境快速判断标志
|
||
|
||
|
||
# ================================================================
|
||
# --- 环境自动检测 (网络模式 / 系统发行版 / CPU架构) ---
|
||
# ================================================================
|
||
detect_environment() {
|
||
# --- 检测 CPU 架构 ---
|
||
local raw_arch
|
||
raw_arch=$(dpkg --print-architecture 2>/dev/null || uname -m)
|
||
case "$raw_arch" in
|
||
amd64|x86_64) SYS_ARCH="amd64" ;;
|
||
arm64|aarch64) SYS_ARCH="arm64" ;;
|
||
armhf|armv7*) SYS_ARCH="armhf" ;;
|
||
*) SYS_ARCH="$raw_arch" ;;
|
||
esac
|
||
|
||
# --- 检测发行版与代号 ---
|
||
if [ -f /etc/os-release ]; then
|
||
SYS_DISTRO=$(grep ^ID= /etc/os-release | cut -d= -f2 | tr -d '"' | tr '[:upper:]' '[:lower:]')
|
||
SYS_CODENAME=$(grep ^VERSION_CODENAME= /etc/os-release | cut -d= -f2 | tr -d '"')
|
||
# 兜底:部分精简镜像没有 VERSION_CODENAME
|
||
if [ -z "$SYS_CODENAME" ]; then
|
||
SYS_CODENAME=$(lsb_release -cs 2>/dev/null || echo "unknown")
|
||
fi
|
||
fi
|
||
|
||
# --- 检测网络模式 ---
|
||
# 用超短超时避免卡顿,测试点选 Cloudflare (对IPv4/IPv6都友好)
|
||
local has_ipv4=false
|
||
local has_ipv6=false
|
||
|
||
if curl -4 -s --max-time 5 --connect-timeout 4 https://1.1.1.1/cdn-cgi/trace -o /dev/null 2>/dev/null; then
|
||
has_ipv4=true
|
||
fi
|
||
if curl -6 -s --max-time 5 --connect-timeout 4 https://[2606:4700:4700::1111]/cdn-cgi/trace -o /dev/null 2>/dev/null; then
|
||
has_ipv6=true
|
||
fi
|
||
|
||
if $has_ipv4 && $has_ipv6; then
|
||
NET_MODE="dual"
|
||
IS_IPV6_ONLY=false
|
||
elif $has_ipv6 && ! $has_ipv4; then
|
||
NET_MODE="ipv6only"
|
||
IS_IPV6_ONLY=true
|
||
elif $has_ipv4 && ! $has_ipv6; then
|
||
NET_MODE="ipv4only"
|
||
IS_IPV6_ONLY=false
|
||
else
|
||
# 两个都探测失败,保守当作IPv4处理,避免误判
|
||
NET_MODE="ipv4only"
|
||
IS_IPV6_ONLY=false
|
||
fi
|
||
}
|
||
|
||
# ================================================================
|
||
# --- 纯IPv6环境:替换apt源为支持IPv6的镜像 ---
|
||
# ================================================================
|
||
setup_ipv6_apt_sources() {
|
||
# 已经替换过就跳过
|
||
if grep -q "mirrors.ustc.edu.cn\|mirrors.tuna.tsinghua.edu.cn" /etc/apt/sources.list 2>/dev/null; then
|
||
return 0
|
||
fi
|
||
|
||
echo -e "${YELLOW}[IPv6模式] 正在将 apt 源替换为支持 IPv6 的 USTC 镜像...${NC}"
|
||
|
||
# 备份原始sources.list
|
||
sudo cp /etc/apt/sources.list /etc/apt/sources.list.bak.$(date +%Y%m%d)
|
||
|
||
if [ "$SYS_DISTRO" = "ubuntu" ]; then
|
||
sudo tee /etc/apt/sources.list > /dev/null << EOF
|
||
deb https://mirrors.ustc.edu.cn/ubuntu/ ${SYS_CODENAME} main restricted universe multiverse
|
||
deb https://mirrors.ustc.edu.cn/ubuntu/ ${SYS_CODENAME}-updates main restricted universe multiverse
|
||
deb https://mirrors.ustc.edu.cn/ubuntu/ ${SYS_CODENAME}-backports main restricted universe multiverse
|
||
deb https://mirrors.ustc.edu.cn/ubuntu/ ${SYS_CODENAME}-security main restricted universe multiverse
|
||
EOF
|
||
elif [ "$SYS_DISTRO" = "debian" ]; then
|
||
sudo tee /etc/apt/sources.list > /dev/null << EOF
|
||
deb https://mirrors.ustc.edu.cn/debian/ ${SYS_CODENAME} main contrib non-free non-free-firmware
|
||
deb https://mirrors.ustc.edu.cn/debian/ ${SYS_CODENAME}-updates main contrib non-free non-free-firmware
|
||
deb https://mirrors.ustc.edu.cn/debian/ ${SYS_CODENAME}-backports main contrib non-free non-free-firmware
|
||
deb https://mirrors.ustc.edu.cn/debian-security/ ${SYS_CODENAME}-security main contrib non-free non-free-firmware
|
||
EOF
|
||
fi
|
||
|
||
echo -e "${GREEN}✅ apt 源已切换至 USTC IPv6 镜像 (原始备份于 sources.list.bak.$(date +%Y%m%d))${NC}"
|
||
}
|
||
|
||
# ================================================================
|
||
# --- 纯IPv6环境:下载 cloudflared 二进制 ---
|
||
# ================================================================
|
||
install_cloudflared_ipv6() {
|
||
echo -e "${YELLOW}[IPv6模式] 通过直链下载 cloudflared 二进制 (架构: ${SYS_ARCH})...${NC}"
|
||
|
||
# Cloudflare 官方 release 链接,cloudflare.com 本身支持 IPv6
|
||
local CF_BASE="https://github.com/cloudflare/cloudflared/releases/latest/download"
|
||
local BIN_NAME=""
|
||
|
||
case "$SYS_ARCH" in
|
||
amd64) BIN_NAME="cloudflared-linux-amd64" ;;
|
||
arm64) BIN_NAME="cloudflared-linux-arm64" ;;
|
||
armhf) BIN_NAME="cloudflared-linux-arm" ;;
|
||
*)
|
||
echo -e "${RED}❌ 不支持的架构: ${SYS_ARCH},无法自动下载 cloudflared。${NC}"
|
||
return 1 ;;
|
||
esac
|
||
|
||
# GitHub 不直接支持IPv6,使用 ghproxy 中转(国内IPv6友好的代理)
|
||
# 备用:尝试多个镜像
|
||
local DOWNLOAD_SUCCESS=false
|
||
local MIRRORS=(
|
||
"https://gh.con.sh/${CF_BASE}/${BIN_NAME}"
|
||
"https://ghproxy.net/${CF_BASE}/${BIN_NAME}"
|
||
"https://mirror.ghproxy.com/${CF_BASE}/${BIN_NAME}"
|
||
"${CF_BASE}/${BIN_NAME}"
|
||
)
|
||
|
||
for mirror_url in "${MIRRORS[@]}"; do
|
||
echo -e "${DIM} 尝试下载源: ${mirror_url}${NC}"
|
||
if curl -6L --max-time 60 --connect-timeout 10 -o /tmp/cloudflared_bin "$mirror_url" 2>/dev/null; then
|
||
# 验证下载的是真实二进制而非HTML错误页
|
||
if file /tmp/cloudflared_bin 2>/dev/null | grep -qiE "ELF|executable"; then
|
||
DOWNLOAD_SUCCESS=true
|
||
break
|
||
fi
|
||
fi
|
||
done
|
||
|
||
if ! $DOWNLOAD_SUCCESS; then
|
||
echo -e "${RED}❌ cloudflared 下载失败!所有镜像均不可用。${NC}"
|
||
echo -e "${YELLOW}请手动下载后放到 /usr/local/bin/cloudflared 并赋予执行权限。${NC}"
|
||
return 1
|
||
fi
|
||
|
||
sudo mv /tmp/cloudflared_bin /usr/local/bin/cloudflared
|
||
sudo chmod +x /usr/local/bin/cloudflared
|
||
|
||
echo -e "${GREEN}✅ cloudflared 安装完毕!(版本: $(cloudflared --version 2>/dev/null | head -1))${NC}"
|
||
return 0
|
||
}
|
||
|
||
# ================================================================
|
||
# --- 纯IPv6环境:安装 Docker ---
|
||
# ================================================================
|
||
install_docker_ipv6() {
|
||
echo -e "${YELLOW}[IPv6模式] 使用 USTC 镜像安装 Docker...${NC}"
|
||
|
||
sudo apt-get update
|
||
sudo apt-get install -y ca-certificates curl gnupg
|
||
|
||
# 添加 Docker GPG key (走USTC镜像,支持IPv6)
|
||
sudo install -m 0755 -d /etc/apt/keyrings
|
||
|
||
if [ "$SYS_DISTRO" = "ubuntu" ]; then
|
||
curl -6fsSL "https://mirrors.ustc.edu.cn/docker-ce/linux/ubuntu/gpg" \
|
||
| sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg 2>/dev/null
|
||
echo "deb [arch=${SYS_ARCH} signed-by=/etc/apt/keyrings/docker.gpg] \
|
||
https://mirrors.ustc.edu.cn/docker-ce/linux/ubuntu ${SYS_CODENAME} stable" \
|
||
| sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
|
||
elif [ "$SYS_DISTRO" = "debian" ]; then
|
||
curl -6fsSL "https://mirrors.ustc.edu.cn/docker-ce/linux/debian/gpg" \
|
||
| sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg 2>/dev/null
|
||
echo "deb [arch=${SYS_ARCH} signed-by=/etc/apt/keyrings/docker.gpg] \
|
||
https://mirrors.ustc.edu.cn/docker-ce/linux/debian ${SYS_CODENAME} stable" \
|
||
| sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
|
||
fi
|
||
|
||
sudo apt-get update
|
||
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
|
||
|
||
sudo systemctl enable docker
|
||
sudo systemctl start docker
|
||
|
||
if command -v docker &>/dev/null && docker compose version &>/dev/null; then
|
||
echo -e "${GREEN}✅ Docker 安装成功!(版本: $(docker --version))${NC}"
|
||
return 0
|
||
else
|
||
echo -e "${RED}❌ Docker 安装失败,请检查上方日志。${NC}"
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
|
||
# --- 颜色定义 ---
|
||
# 配色方案:骚气黄绿 + 白色 + 灰色,去掉蓝色/紫色
|
||
|
||
# ================================================================
|
||
# 🛠️ 通用工具函数
|
||
# ================================================================
|
||
|
||
# 统一日志输出
|
||
_log_info() { echo -e "${GREEN}[INFO]${NC} $*"; }
|
||
_log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
||
_log_error() { echo -e "${RED}[ERROR]${NC} $*"; }
|
||
_log_step() { echo -e "${CYAN}[STEP]${NC} $*"; }
|
||
|
||
# 通用 Docker Compose 启动函数
|
||
# 用法: _compose_up <服务名> <数据目录>
|
||
# 失败时打印日志尾部,方便诊断
|
||
_compose_up() {
|
||
local svc_name="$1"
|
||
local data_dir="$2"
|
||
_log_step "正在启动 ${svc_name}..."
|
||
if (cd "${data_dir}" && sudo docker compose up -d 2>&1); then
|
||
_log_info "${svc_name} 容器已启动"
|
||
return 0
|
||
else
|
||
_log_error "${svc_name} 启动失败!最近日志:"
|
||
(cd "${data_dir}" && sudo docker compose logs --tail 20 2>/dev/null) || true
|
||
_log_warn "可手动排查: cd ${data_dir} && docker compose logs"
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
# 通用安装前置检查(已安装则提示并返回)
|
||
# 用法: _check_not_installed <标志路径> <服务名>
|
||
_check_not_installed() {
|
||
local flag_path="$1"
|
||
local svc_name="$2"
|
||
if [ -d "${flag_path}" ] || [ -f "${flag_path}" ]; then
|
||
_log_warn "${svc_name} 已安装,跳过。如需重装请先从卸载中心删除。"
|
||
sleep 2; return 1
|
||
fi
|
||
return 0
|
||
}
|
||
|
||
# 生成随机密码
|
||
_gen_password() {
|
||
local len="${1:-16}"
|
||
head /dev/urandom | tr -dc A-Za-z0-9 | head -c "${len}"
|
||
}
|
||
|
||
# 保存凭证到 STATE_FILE
|
||
_save_credential() {
|
||
local key="$1" val="$2"
|
||
sed -i.bak "/^${key}=/d" "${STATE_FILE}" 2>/dev/null
|
||
echo "${key}=${val}" >> "${STATE_FILE}"
|
||
rm -f "${STATE_FILE}.bak"
|
||
}
|
||
|
||
# ================================================================
|
||
setup_colors() {
|
||
if [[ -t 1 ]];
|
||
then
|
||
GREEN='\033[1;32m' # 亮绿(骚气色主色)
|
||
YELLOW='\033[1;33m' # 亮黄(骚气色副色)
|
||
CYAN='\033[1;32m' # 原青色 → 改为亮绿
|
||
BLUE='\033[1;33m' # 原蓝色 → 改为亮黄
|
||
PURPLE='\033[1;32m' # 原紫色 → 改为亮绿
|
||
RED='\033[0;31m' # 红色保留(报错用)
|
||
WHITE='\033[1;37m' # 白色
|
||
DIM='\033[2;37m' # 灰色(暗淡白)
|
||
NC='\033[0m'
|
||
else
|
||
GREEN=''
|
||
YELLOW=''
|
||
CYAN=''
|
||
BLUE=''
|
||
PURPLE=''
|
||
RED=''
|
||
WHITE=''
|
||
DIM=''
|
||
NC=''
|
||
fi
|
||
}
|
||
|
||
# --- 美化对齐输出函数 (解决中文宽度问题 - 终极兼容版) ---
|
||
# --- 统一菜单行输出函数 ---
|
||
# $1=编号(不含括号) $2=文字 $3=状态字符串 $4=编号颜色(可选)
|
||
print_row() {
|
||
local num="$1"
|
||
local text="$2"
|
||
local status="$3"
|
||
local num_color="${4:-$YELLOW}"
|
||
|
||
# 编号+括号,固定7字符(左对齐)
|
||
local num_field
|
||
printf -v num_field "%-7s" "${num})"
|
||
|
||
# 计算文字视觉宽度(正确处理中文/emoji双宽字符)
|
||
local visual_text_width
|
||
visual_text_width=$(echo -n "$text" | perl -CS -Mutf8 -nlE '
|
||
use Encode;
|
||
my $w = 0;
|
||
for my $c (split //, $_) {
|
||
my $b = length(Encode::encode("utf8", $c));
|
||
$w += ($b >= 3 ? 2 : 1);
|
||
}
|
||
print $w;
|
||
')
|
||
|
||
# 文字列目标宽度42,右侧对齐状态栏
|
||
local text_target=42
|
||
local text_pad=$((text_target - visual_text_width))
|
||
[ $text_pad -lt 1 ] && text_pad=1
|
||
|
||
printf " ${num_color}%s${NC}%s" "$num_field" "$text"
|
||
printf "%*s" $text_pad ""
|
||
echo -e "$status"
|
||
}
|
||
|
||
print_menu_item() {
|
||
local text="$1"
|
||
local status="$2"
|
||
|
||
# 先剥离 ANSI 转义序列,再计算视觉宽度(解决颜色代码被误计入宽度的问题)
|
||
local plain_text
|
||
plain_text=$(echo -e "$text" | sed 's/\x1b\[[0-9;]*m//g')
|
||
|
||
local visual_width=$(echo -n "$plain_text" | perl -CS -Mutf8 -nlE '
|
||
use Encode;
|
||
my $width = 0;
|
||
for my $char (split //, $_) {
|
||
my $b = length(Encode::encode("utf8", $char));
|
||
$width += ($b >= 3 ? 2 : 1);
|
||
}
|
||
print $width;
|
||
')
|
||
|
||
local target_column=62
|
||
local padding_needed=$((target_column - visual_width))
|
||
if [ $padding_needed -lt 1 ]; then
|
||
padding_needed=1
|
||
fi
|
||
echo -e -n "${text}"
|
||
printf "%*s" $padding_needed ""
|
||
echo -e "${status}"
|
||
}
|
||
|
||
# --- 首次运行自安装快捷命令 ---
|
||
setup_shortcut() {
|
||
local install_path="/usr/local/bin/jigongtou"
|
||
local script_path
|
||
script_path=$(realpath "${BASH_SOURCE[0]}" 2>/dev/null)
|
||
|
||
# 判断当前脚本是否已经就是安装位置本身,是则无需操作
|
||
if [[ "$script_path" == "$install_path" ]]; then
|
||
return 0
|
||
fi
|
||
|
||
# 检查已安装版本是否完整有效
|
||
if [[ -f "$install_path" && -x "$install_path" ]]; then
|
||
return 0
|
||
fi
|
||
|
||
echo -e "${GREEN}为方便您使用,正在创建快捷命令 'jigongtou'...${NC}"
|
||
|
||
# 优先:当前脚本有实体路径,直接复制过去
|
||
if [[ -n "$script_path" && -f "$script_path" && \
|
||
"$script_path" != "/bin/bash" && "$script_path" != "/usr/bin/bash" ]]; then
|
||
sudo cp -f "${script_path}" "${install_path}"
|
||
else
|
||
# 兜底:管道/source 执行时 BASH_SOURCE 无效,把自身内容写到目标路径
|
||
# ($0 也可能是 bash,用子 shell 读 /proc/self/fd/255 尝试获取)
|
||
local pipe_src
|
||
pipe_src=$(readlink /proc/$$/fd/255 2>/dev/null)
|
||
if [[ -n "$pipe_src" && -f "$pipe_src" ]]; then
|
||
sudo cp -f "${pipe_src}" "${install_path}"
|
||
else
|
||
echo -e "${YELLOW} ⚠️ 无法自动创建快捷命令(管道执行模式)。${NC}"
|
||
echo -e "${YELLOW} 请将脚本保存到本地后运行:bash 鸡公头-正式版.sh${NC}"
|
||
echo -e "${YELLOW} 之后即可直接输入 jigongtou 调出面板。${NC}"
|
||
sleep 4
|
||
return 0
|
||
fi
|
||
fi
|
||
|
||
if sudo chmod +x "${install_path}" && [[ -x "${install_path}" ]]; then
|
||
echo -e "${GREEN} ✅ 快捷命令创建成功!现在起随时输入 'jigongtou' 即可启动此面板。${NC}"
|
||
else
|
||
echo -e "${RED} ❌ 快捷命令创建失败,请手动执行:sudo cp 鸡公头-正式版.sh /usr/local/bin/jigongtou && sudo chmod +x /usr/local/bin/jigongtou${NC}"
|
||
fi
|
||
sleep 3
|
||
}
|
||
|
||
# --- 核心依赖检查函数 ---
|
||
check_core_dependencies() {
|
||
if ! perl -e 'use Encode;' 2>/dev/null; then
|
||
echo -e "${YELLOW}--- 检测到核心组件 Perl::Encode 缺失,正在自动安装 ---${NC}"
|
||
echo -e "${YELLOW}这是确保菜单正确对齐所必需的。${NC}"
|
||
sleep 2
|
||
if $IS_IPV6_ONLY; then setup_ipv6_apt_sources; fi
|
||
sudo apt-get update >/dev/null 2>&1
|
||
sudo apt-get install -y libencode-perl
|
||
if ! perl -e 'use Encode;' 2>/dev/null; then
|
||
echo -e "${RED}错误:核心组件自动安装失败。${NC}"
|
||
echo -e "${RED}请手动执行 'sudo apt-get install libencode-perl' 后重试。${NC}"
|
||
exit 1
|
||
fi
|
||
echo -e "${GREEN}✅ 核心组件已成功安装!脚本将继续运行。${NC}"
|
||
sleep 2
|
||
fi
|
||
}
|
||
|
||
# --- 核心环境检查函数 ---
|
||
ensure_docker_installed() {
|
||
if ! command -v docker &> /dev/null || ! docker compose version &> /dev/null; then
|
||
echo -e "${YELLOW}--- 检查到 Docker 或 Docker Compose 插件未安装,现在开始自动安装 ---${NC}"
|
||
sleep 2
|
||
|
||
if ! command -v docker &> /dev/null; then
|
||
if $IS_IPV6_ONLY; then
|
||
# 纯IPv6环境:先换源,再用USTC镜像安装Docker
|
||
setup_ipv6_apt_sources
|
||
install_docker_ipv6
|
||
else
|
||
# 正常IPv4/双栈环境:使用官方脚本
|
||
sudo apt-get update
|
||
sudo apt-get install -y ca-certificates curl gnupg
|
||
echo -e "${YELLOW}正在安装 Docker Engine (包含 Compose 插件)...${NC}"
|
||
curl -fsSL https://get.docker.com -o get-docker.sh
|
||
sudo sh get-docker.sh && rm get-docker.sh
|
||
sudo systemctl restart docker
|
||
sudo systemctl enable docker
|
||
fi
|
||
fi
|
||
|
||
if ! docker compose version &> /dev/null; then
|
||
echo -e "${YELLOW}Docker Compose 插件未找到, 正在尝试补充安装...${NC}"
|
||
sudo apt-get update
|
||
sudo apt-get install -y docker-compose-plugin
|
||
fi
|
||
|
||
if ! command -v docker &> /dev/null || ! docker compose version &> /dev/null; then
|
||
echo -e "${RED}错误:Docker 环境自动安装失败,请检查网络或手动安装后重试。${NC}"
|
||
sleep 5
|
||
return 1
|
||
else
|
||
echo -e "${GREEN}✅ Docker 环境已成功安装并准备就绪!${NC}"
|
||
sleep 2
|
||
fi
|
||
fi
|
||
|
||
if ! sudo docker info >/dev/null 2>&1; then
|
||
echo -e "${YELLOW}检测到 Docker 服务未运行,正在尝试启动...${NC}"
|
||
sudo systemctl start docker
|
||
sleep 3
|
||
if ! sudo docker info >/dev/null 2>&1; then
|
||
echo -e "${RED}错误:无法启动 Docker 服务!请手动检查 'sudo systemctl status docker'。${NC}"
|
||
return 1
|
||
fi
|
||
echo -e "${GREEN}✅ Docker 服务已成功启动!${NC}"
|
||
fi
|
||
return 0
|
||
}
|
||
|
||
# --- 系统基础功能函数 ---
|
||
update_system() {
|
||
clear
|
||
echo -e "${BLUE}--- 更新系统与软件 (APT 更新) ---${NC}"
|
||
echo -e "${YELLOW}即将开始更新系统软件包列表并升级所有已安装的软件 ...${NC}"
|
||
if $IS_IPV6_ONLY; then setup_ipv6_apt_sources; fi
|
||
sudo apt-get update && sudo apt-get upgrade -y
|
||
echo -e "\n${GREEN}✅ 系统更新完成!${NC}"
|
||
echo -e "\n${GREEN}按任意键返回主菜单 ...${NC}"; read -n 1 -s
|
||
}
|
||
|
||
run_unminimize() {
|
||
clear
|
||
echo -e "${BLUE}--- 恢复至标准系统 (unminimize) ---${NC}"
|
||
if grep -q -i "ubuntu" /etc/os-release; then
|
||
echo -e "${YELLOW}此操作将为您的最小化 Ubuntu 系统安装完整的标准系统包。${NC}"
|
||
echo -e "${YELLOW}它会增加一些磁盘占用,但可以解决某些软件的兼容性问题。${NC}"
|
||
read -p "您确定要继续吗? (y/n): " confirm
|
||
if [[ "$confirm" == "y" || "$confirm" == "Y" ]]; then
|
||
echo -e "${GREEN}正在执行 unminimize ,请稍候 ...${NC}"
|
||
sudo unminimize
|
||
echo -e "\n${GREEN}✅ 操作完成!${NC}"
|
||
else
|
||
echo -e "${GREEN}操作已取消。${NC}"
|
||
fi
|
||
else
|
||
echo -e "${RED}此功能专为 Ubuntu 系统设计,您当前的系统似乎不是 Ubuntu 。${NC}"
|
||
fi
|
||
echo -e "\n${GREEN}按任意键返回主菜单 ...${NC}"; read -n 1 -s
|
||
}
|
||
|
||
manage_swap() {
|
||
clear
|
||
echo -e "${BLUE}--- 配置虚拟内存 (Swap) ---${NC}"
|
||
local swap_active=false
|
||
local swapfile_exists=false
|
||
swapon --show --noheadings 2>/dev/null | grep -q '/swapfile' && swap_active=true
|
||
[ -f "/swapfile" ] && swapfile_exists=true
|
||
|
||
if $swap_active; then
|
||
local swap_size_cur
|
||
swap_size_cur=$(swapon --show --noheadings --bytes 2>/dev/null | awk '/swapfile/{printf "%.0fG", $3/1024/1024/1024}')
|
||
echo -e "${GREEN}检测到虚拟内存已启用:/swapfile(${swap_size_cur})${NC}"
|
||
free -h
|
||
echo ""
|
||
read -p "您想移除现有的虚拟内存吗? (y/n): " confirm_remove
|
||
if [[ "$confirm_remove" == "y" || "$confirm_remove" == "Y" ]]; then
|
||
echo -e "${YELLOW}正在停止并移除虚拟内存...${NC}"
|
||
sudo swapoff /swapfile
|
||
sudo sed -i '/\/swapfile/d' /etc/fstab
|
||
sudo rm -f /swapfile
|
||
echo -e "${GREEN}✅ 虚拟内存已成功移除!${NC}"
|
||
free -h
|
||
else
|
||
echo -e "${GREEN}操作已取消,虚拟内存保持不变。${NC}"
|
||
fi
|
||
elif $swapfile_exists; then
|
||
echo -e "${YELLOW}检测到 /swapfile 文件存在但未挂载。${NC}"
|
||
echo -e " 1) 重新启用它"
|
||
echo -e " 2) 删除并重新创建"
|
||
echo -e " 3) 取消"
|
||
read -p "请选择 (1/2/3): " choice
|
||
case "$choice" in
|
||
1)
|
||
sudo swapon /swapfile
|
||
if ! grep -q "/swapfile" /etc/fstab; then
|
||
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
|
||
fi
|
||
echo -e "${GREEN}✅ 虚拟内存已重新启用!${NC}"; free -h ;;
|
||
2)
|
||
sudo rm -f /swapfile
|
||
sudo sed -i '/\/swapfile/d' /etc/fstab
|
||
read -p "请输入新的 Swap 大小 (例如: 4G, 8G): " swap_size
|
||
[ -z "$swap_size" ] && { echo -e "${RED}输入为空,取消。${NC}"; sleep 2
|
||
echo -e "\n${GREEN}按任意键返回主菜单 ...${NC}"; read -n 1 -s; return; }
|
||
sudo fallocate -l ${swap_size} /swapfile
|
||
sudo chmod 600 /swapfile; sudo mkswap /swapfile; sudo swapon /swapfile
|
||
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
|
||
echo -e "${GREEN}✅ 虚拟内存已重新创建!${NC}"; free -h ;;
|
||
*) echo -e "${GREEN}操作已取消。${NC}" ;;
|
||
esac
|
||
else
|
||
echo -e "${YELLOW}未检测到虚拟内存,现在为您创建。${NC}"
|
||
read -p "请输入 Swap 大小 (例如: 4G, 8G, 10G) [建议为内存的1-2倍]: " swap_size
|
||
if [ -z "$swap_size" ]; then
|
||
echo -e "${RED}输入为空,操作取消。${NC}"; sleep 2; return
|
||
fi
|
||
sudo fallocate -l ${swap_size} /swapfile
|
||
sudo chmod 600 /swapfile; sudo mkswap /swapfile; sudo swapon /swapfile
|
||
if ! grep -q "/swapfile" /etc/fstab; then
|
||
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
|
||
fi
|
||
echo -e "\n${GREEN}✅ 虚拟内存创建并启用成功!${NC}"
|
||
free -h
|
||
fi
|
||
echo -e "\n${GREEN}按任意键返回主菜单 ...${NC}"; read -n 1 -s
|
||
}
|
||
|
||
# --- 检查与菜单显示函数 ---
|
||
check_and_display() {
|
||
local option_num="$1"
|
||
local text="$2"
|
||
local check_type="$3"
|
||
local check_path="$4"
|
||
local status_string="[ ${RED}❌ 未安装${NC} ]"
|
||
|
||
# SSHFS 专项检测:看实际挂载数量,而非工具是否存在
|
||
if [[ "$text" == *"SSHFS"* || "$text" == *"远程VPS"* || "$text" == *"挂载远程"* ]]; then
|
||
local mount_count
|
||
mount_count=$(mount 2>/dev/null | grep -cE "fuse\.sshfs|type fuse[^.]" 2>/dev/null || true)
|
||
mount_count=${mount_count:-0}
|
||
if [ "$mount_count" -gt 0 ]; then
|
||
status_string="[ ${GREEN}✅ 已挂载 ${mount_count} 个${NC} ]"
|
||
elif command -v sshfs &>/dev/null; then
|
||
status_string="[ ${YELLOW}⚠️ 已安装/无挂载${NC} ]"
|
||
fi
|
||
print_row "$option_num" "$text" "$status_string"
|
||
return
|
||
fi
|
||
|
||
local is_installed=false
|
||
case "$check_type" in
|
||
"dir")
|
||
[ -d "$check_path" ] && is_installed=true
|
||
;;
|
||
"file")
|
||
[ -f "$check_path" ] && is_installed=true
|
||
;;
|
||
"service")
|
||
if systemctl is-active --quiet "$check_path" 2>/dev/null; then
|
||
is_installed=true
|
||
elif systemctl list-unit-files 2>/dev/null | grep -q "^${check_path}.service"; then
|
||
status_string="[ ${YELLOW}⚠️ 已安装/未运行${NC} ]"
|
||
print_row "$option_num" "$text" "$status_string"
|
||
return
|
||
fi
|
||
;;
|
||
"docker")
|
||
if docker inspect "$check_path" &>/dev/null 2>&1; then
|
||
if docker inspect --format='{{.State.Running}}' "$check_path" 2>/dev/null | grep -q "true"; then
|
||
is_installed=true
|
||
else
|
||
status_string="[ ${YELLOW}⚠️ 已安装/未运行${NC} ]"
|
||
print_row "$option_num" "$text" "$status_string"
|
||
return
|
||
fi
|
||
fi
|
||
;;
|
||
"cmd")
|
||
command -v "$check_path" &>/dev/null && is_installed=true
|
||
;;
|
||
"ai_model")
|
||
# 检测 AI 大脑是否已安装且 ollama 容器内有模型
|
||
if [ -d "$check_path" ] && docker inspect ollama &>/dev/null 2>&1; then
|
||
local model_count
|
||
model_count=$(docker exec ollama ollama list 2>/dev/null | tail -n +2 | grep -c "." 2>/dev/null || true)
|
||
model_count=${model_count:-0}
|
||
if [ "$model_count" -gt 0 ]; then
|
||
status_string="[ ${GREEN}✅ 已安装 ${model_count} 个模型${NC} ]"
|
||
else
|
||
status_string="[ ${YELLOW}⚠️ 已安装/无模型${NC} ]"
|
||
fi
|
||
print_row "$option_num" "$text" "$status_string"
|
||
return
|
||
fi
|
||
[ -d "$check_path" ] && is_installed=true
|
||
;;
|
||
esac
|
||
|
||
if $is_installed; then
|
||
status_string="[ ${GREEN}✅ 已安装${NC} ]"
|
||
fi
|
||
|
||
# Rclone:直接读 rclone listremotes 显示所有已配置 remote
|
||
if [[ "$text" == *"Rclone"* && -f "$check_path" ]]; then
|
||
local remotes
|
||
remotes=$(rclone listremotes --config "${RCLONE_CONFIG_FILE}" 2>/dev/null | sed 's/:$//' | tr '\n' ',' | sed 's/,$//')
|
||
if [ -n "$remotes" ]; then
|
||
status_string="[ ${GREEN}✅ 已配置: ${remotes}${NC} ]"
|
||
else
|
||
status_string="[ ${GREEN}✅ 已安装, 未配置${NC} ]"
|
||
fi
|
||
fi
|
||
|
||
print_row "$option_num" "$text" "$status_string"
|
||
}
|
||
|
||
# --- 动态 LOGO 显示函数 ---
|
||
GREEN2='\033[1;32m'
|
||
display_animated_logo() {
|
||
local G="${GREEN2}"
|
||
local NC_="\033[0m"
|
||
echo -e "${G} ███████ ████████"
|
||
echo " █████████████ ███████████"
|
||
echo " ████ ████ █████ ████"
|
||
echo " ████ ███ ███ ████"
|
||
echo " █████ █████████ ███"
|
||
echo " █████ ██████ ███ ██████████"
|
||
echo " █████████████ ████ ██████████████████"
|
||
echo " █████ █████████ ███ ████████ ████"
|
||
echo " ████ █████ ███ █████ ██████"
|
||
echo " ██████ ███ ████████████ ██████"
|
||
echo " ███████ ████████████████████ ██████"
|
||
echo " ███████████████ ███████████"
|
||
echo " ███████ ██████"
|
||
echo " ████ ██████"
|
||
echo " █████ ███"
|
||
echo " █████████████████████ █████████████████████"
|
||
echo " ███████ ███████ ███████████"
|
||
echo " ███ ███████ ███ ████"
|
||
echo " ███ ███ ███ ███ ████"
|
||
echo " ████ ███ ████ ████ ████"
|
||
echo " ███████████████████████████ ████"
|
||
echo " ████████████████████ ████"
|
||
echo " ████████████████████████ ████"
|
||
echo " ████████████████████████████ ████"
|
||
echo " ██████████████████████████████ ████"
|
||
echo " ████████████████████████████████ ████"
|
||
echo " █████████████████████████████████ ████"
|
||
echo " ██████████████████████████████ ████"
|
||
echo " █████████████████████ ████"
|
||
echo " ██████████ ██████"
|
||
echo " ██████ ████████"
|
||
echo " ██████████████████████████"
|
||
echo -e " ████████████████████${NC_}"
|
||
echo ""
|
||
echo -e "${G}╔═════════════════════════════════════════════════════════════════════╗${NC_}"
|
||
echo -e "${G}║ 🐓 J I G O N G T O U ║${NC_}"
|
||
echo -e "${G}║ 鸡公头 VPS 全面部署安装脚本 ║${NC_}"
|
||
echo -e "${DIM}║ jigongtou.uepopo.com ║${NC_}"
|
||
echo -e "${G}╚═════════════════════════════════════════════════════════════════════╝${NC_}"
|
||
echo ""
|
||
}
|
||
|
||
# --- 菜单显示 ---
|
||
show_main_menu() {
|
||
clear
|
||
display_animated_logo
|
||
|
||
echo -e "${YELLOW}══════════════════════════════════════════════════════════════════════${NC}"
|
||
echo -e "${YELLOW} 🐓 鸡公头面板 Cloudflare Tunnel版 v7.0.9${NC}"
|
||
echo -e "${DIM} 适用于 Ubuntu/Debian · 输入 jigongtou 可随时调出本面板${NC}"
|
||
# --- 动态环境信息栏 ---
|
||
local net_label arch_label distro_label
|
||
case "$NET_MODE" in
|
||
dual) net_label="${GREEN}双栈 IPv4+IPv6${NC}" ;;
|
||
ipv4only) net_label="${WHITE}仅 IPv4${NC}" ;;
|
||
ipv6only) net_label="${YELLOW}纯 IPv6 模式 · 已启用兼容方案${NC}" ;;
|
||
*) net_label="${DIM}检测中...${NC}" ;;
|
||
esac
|
||
arch_label="${WHITE}${SYS_ARCH:-未知}${NC}"
|
||
distro_label="${WHITE}${SYS_DISTRO:-未知} ${SYS_CODENAME:-}${NC}"
|
||
echo -e "${DIM} 🌐 网络: ${net_label} ${DIM}| 💻 架构: ${arch_label} ${DIM}| 🐧 系统: ${distro_label}${NC}"
|
||
echo -e "${YELLOW}══════════════════════════════════════════════════════════════════════${NC}"
|
||
echo ""
|
||
echo -e "${GREEN} ── 地基与系统 ──────────────────────────────────────────────────────${NC}"
|
||
print_row "u" "更新系统软件包" "[ APT 更新 ]"
|
||
print_row "m" "恢复至标准系统" "[ unminimize ]"
|
||
print_row "s" "配置虚拟内存 Swap" "[ 增强低配VPS性能 ]"
|
||
echo ""
|
||
echo -e "${YELLOW} ── 基础设施 ────────────────────────────────────────────────────────${NC}"
|
||
check_and_display "1" "部署 Cloudflare Tunnel" "service" "cloudflared"
|
||
check_and_display "2" "配置 Rclone 数据同步桥" "file" "${RCLONE_CONFIG_FILE}"
|
||
check_and_display "3" "挂载远程VPS为本机网盘" "file" "/usr/bin/sshfs"
|
||
check_and_display "3.1" "将本机做成 WebDAV 网盘" "docker" "webdav_app"
|
||
check_and_display "3.2" "🗂️ 网盘矩阵联合挂载 (mergerfs)" "dir" "/mnt/union"
|
||
echo ""
|
||
echo -e "${GREEN} ── 家庭数据中心 ────────────────────────────────────────────────────${NC}"
|
||
check_and_display "4.1" "部署 Nextcloud 网盘" "dir" "/root/nextcloud_data"
|
||
check_and_display "4.2" "部署 OnlyOffice 在线文档" "dir" "/root/onlyoffice_data"
|
||
check_and_display "4.3" "部署 Home Assistant 智能家居" "dir" "/root/home_assistant_data"
|
||
check_and_display "4.4" "部署 WordPress 博客" "dir" "/root/wordpress_data"
|
||
echo ""
|
||
echo -e "${YELLOW} ── AI 与媒体 ───────────────────────────────────────────────────────${NC}"
|
||
check_and_display "5.1" "部署 AI 大脑 (Ollama + WebUI)" "dir" "/root/ai_stack"
|
||
check_and_display "5.2" "为 AI 大脑安装本地模型" "ai_model" "/root/ai_stack"
|
||
check_and_display "5.3" "部署 Jellyfin 家庭影院" "dir" "/root/jellyfin_data"
|
||
check_and_display "5.4" "部署 Navidrome 音乐服务器" "dir" "/root/navidrome_data"
|
||
check_and_display "5.5" "部署 Immich 私人相册" "dir" "/root/immich_data"
|
||
check_and_display "5.6" "部署 Calibre-Web 电子书库" "dir" "/root/calibre_web_data"
|
||
check_and_display "5.7" "部署 Kavita 漫画阅读器" "dir" "/root/kavita_data"
|
||
echo ""
|
||
echo -e "${GREEN} ── 效率工具 ────────────────────────────────────────────────────────${NC}"
|
||
check_and_display "6.1" "部署 Miniflux RSS 阅读器" "dir" "/root/miniflux_data"
|
||
check_and_display "6.2" "部署 Gitea 代码仓库" "dir" "/root/gitea_data"
|
||
check_and_display "6.3" "部署 qBittorrent 下载器" "dir" "/root/qbittorrent_data"
|
||
check_and_display "6.4" "部署 JDownloader 下载器" "dir" "/root/jdownloader_data"
|
||
check_and_display "6.5" "部署 yt-dlp 视频下载器" "dir" "/root/ytdlp_data"
|
||
check_and_display "6.6" "部署 Draw.io 绘图工具" "dir" "/root/drawio_data"
|
||
check_and_display "6.7" "部署 N8N 工作流自动化" "dir" "/root/n8n_data"
|
||
check_and_display "6.8" "部署 Gotify 消息推送" "docker" "gotify_app"
|
||
check_and_display "6.9" "部署 Actual Budget 记账" "dir" "/root/actual_budget_data"
|
||
check_and_display "6.10" "部署 Excalidraw 在线白板" "dir" "/root/excalidraw_data"
|
||
check_and_display "6.11" "部署 Joplin Server 私人笔记" "dir" "/root/joplin_data"
|
||
check_and_display "6.12" "部署 Trilium Notes 知识库" "dir" "/root/trilium_data"
|
||
echo ""
|
||
echo -e "${YELLOW} ── 安防与监控 ──────────────────────────────────────────────────────${NC}"
|
||
check_and_display "8.1" "部署远程桌面 (Xfce+XRDP)" "file" "/etc/xrdp/xrdp.ini"
|
||
print_row "8.2" "为远程桌面添加中文字体" "[ 可选 ]"
|
||
print_row "8.3" "为远程桌面添加浏览器/更改RDP端口" "[ 可选 ]"
|
||
check_and_display "8.4" "部署 Fail2ban 防暴力破解" "service" "fail2ban"
|
||
check_and_display "8.5" "部署 Uptime Kuma 服务监控" "dir" "/root/uptime_kuma_data"
|
||
check_and_display "8.6" "部署 Glances 资源监控" "docker" "glances_app"
|
||
check_and_display "8.7" "部署 Syncthing 文件同步" "service" "syncthing@root"
|
||
check_and_display "8.8" "部署 Grafana 监控面板" "docker" "grafana_app"
|
||
echo ""
|
||
echo -e "${GREEN} ── 娱乐与创作 ──────────────────────────────────────────────────────${NC}"
|
||
check_and_display "7.1" "部署 SillyTavern AI酒馆" "dir" "/root/sillytavern_data"
|
||
check_and_display "7.2" "🐓 部署 PopoDash 监控面板 【鸡公头出品】" "dir" "/root/popodash"
|
||
check_and_display "7.3" "🐓 部署 MIMI 电报 AI 小秘书 【鸡公头出品】" "dir" "/root/mimi"
|
||
echo ""
|
||
echo -e "${YELLOW} ── SSH 安全 ────────────────────────────────────────────────────────${NC}"
|
||
print_row "9.1" "添加 SSH 公钥" "[ 公钥登录 ]"
|
||
local ssh_pass_status
|
||
if grep -qE "^\s*PasswordAuthentication\s+no" /etc/ssh/sshd_config 2>/dev/null; then
|
||
ssh_pass_status="[ ${RED}已禁用${NC} ]"
|
||
else
|
||
ssh_pass_status="[ ${GREEN}已启用${NC} ]"
|
||
fi
|
||
print_row "9.2" "切换 SSH 密码登录" "${ssh_pass_status}"
|
||
echo ""
|
||
echo -e "${GREEN} ── 高级维护 ────────────────────────────────────────────────────────${NC}"
|
||
print_row "10.1" "Nextcloud 性能精装修" "[ 优化配置 ]"
|
||
print_row "10.2" "Nextcloud 高速风景版" "[ 视频封面+BBR ]"
|
||
print_row "10.3" "Nextcloud 外部存储管理" "[一键重挂/扫描 ]"
|
||
print_row "10.4" "🖼️ Nextcloud 预览图终极调教" "[ 新文件自动生成 ]"
|
||
print_row "10.5" "查看密码与路径" "[ 重要凭证 ]"
|
||
print_row "10.6" "查看VPS信息" "[ 系统状态 ]"
|
||
print_row "10.7" "收尾优化脚本" "[ 清理收尾 ]"
|
||
print_row "10.8" "甲骨文开机助手 (oci-helper)" "[ Oracle 专用 ]"
|
||
print_row "10.9" "服务器每日管家邮件报告" "[ 定时报告 ]"
|
||
echo ""
|
||
echo -e "${YELLOW} ── 一键部署 ────────────────────────────────────────────────────────${NC}"
|
||
print_row "20.1" "🚀 甲骨文满配一键全装" "[ 高配置 VPS 专用 ]"
|
||
print_row "20.2" "進入應用卸載中心" "[ ${RED}按需刪除${NC} ]"
|
||
echo ""
|
||
echo -e "${YELLOW}══════════════════════════════════════════════════════════════════════${NC}"
|
||
echo -e "${RED} ── 科学上网 ────────────────────────────────────────────────────────${NC}"
|
||
check_and_display "30.1" "打開科學上網工具箱 (Vless節點)" "dir" "/root/vless_data"
|
||
check_and_display "30.2" "部署 Hysteria 2 (暴力加速)" "service" "hysteria-server"
|
||
echo ""
|
||
echo -e "${YELLOW}══════════════════════════════════════════════════════════════════════${NC}"
|
||
print_row "40" "🎛️ 服务控制中心" "[ ${GREEN}管理已部署服务${NC} ]" "$CYAN"
|
||
print_row "41" "💾 整机冷备份" "[ ${GREEN}一键打包所有数据 → 上传网盘${NC} ]" "$YELLOW"
|
||
echo -e "${YELLOW}══════════════════════════════════════════════════════════════════════${NC}"
|
||
echo ""
|
||
print_row "X" "一键深度清理 (清理垃圾与缓存)" "[ ${CYAN}让小鸡更丝滑${NC} ]"
|
||
print_row "99" "一键卸载鸡公头" "[ ${RED}此操作不可逆!${NC} ]" "$RED"
|
||
print_row "q" "退出面板" ""
|
||
echo ""
|
||
echo -e "${YELLOW}══════════════════════════════════════════════════════════════════════${NC}"
|
||
}
|
||
|
||
update_tunnel_config() {
|
||
local hostname="$1"
|
||
local service_url="$2"
|
||
local comment="$3"
|
||
local extra_param="$4"
|
||
|
||
if [ ! -f "$TUNNEL_CONFIG_FILE" ]; then
|
||
echo -e "${RED}错误: Tunnel 配置文件不存在,请先部署 Tunnel。${NC}"; return 1
|
||
fi
|
||
|
||
sudo sed -i '/- service: http_status:404/d' "$TUNNEL_CONFIG_FILE"
|
||
|
||
if grep -q "hostname: $hostname" "$TUNNEL_CONFIG_FILE"; then
|
||
echo -e "${YELLOW}域名 ${hostname} 的配置已存在,跳过添加。${NC}"
|
||
else
|
||
echo -e "\n # ${comment}" | sudo tee -a "$TUNNEL_CONFIG_FILE" > /dev/null
|
||
echo -e " - hostname: $hostname\n service: $service_url" | sudo tee -a "$TUNNEL_CONFIG_FILE" > /dev/null
|
||
# 如果传入了 "access" 参数,就添加 Cloudflare Access 规则
|
||
if [[ "$extra_param" == "access" ]]; then
|
||
echo -e " # Cloudflare Access 保护" | sudo tee -a "$TUNNEL_CONFIG_FILE" > /dev/null
|
||
echo -e " access:\n required: true\n teamsName: \"$(echo $hostname | cut -d. -f2-).cloudflareaccess.com\"" | sudo tee -a "$TUNNEL_CONFIG_FILE" > /dev/null
|
||
fi
|
||
fi
|
||
echo -e " - service: http_status:404" | sudo tee -a "$TUNNEL_CONFIG_FILE" > /dev/null
|
||
|
||
echo -e "\n${CYAN}--- Tunnel 配置预览 ---${NC}"
|
||
echo -e "${YELLOW}已将以下规则添加/更新至 ${TUNNEL_CONFIG_FILE}:${NC}"
|
||
echo "--------------------------------------------------"
|
||
grep -A 2 "hostname: $hostname" "$TUNNEL_CONFIG_FILE" | sed 's/^/ /'
|
||
echo "--------------------------------------------------"
|
||
|
||
echo -e "${YELLOW}正在应用新的网络配置...${NC}"
|
||
sudo systemctl restart cloudflared
|
||
sleep 2
|
||
if systemctl is-active --quiet cloudflared; then
|
||
echo -e "${GREEN}✅ Cloudflare Tunnel 已为 ${hostname} 更新并重启成功!${NC}"
|
||
echo -e "${GREEN} 您现在应该可以通过 https://${hostname} 访问您的服务了。${NC}"
|
||
else
|
||
echo -e "${RED}❌ Cloudflare Tunnel 重启失败!请执行 'sudo journalctl -u cloudflared -f' 查看日志。${NC}"
|
||
fi
|
||
|
||
}
|
||
|
||
# 1. 部署Cloudflare Tunnel
|
||
install_cloudflare_tunnel() {
|
||
clear
|
||
echo -e "${BLUE}--- “Cloudflare Tunnel 总线”开始施工! ---${NC}"
|
||
|
||
if ! command -v cloudflared &> /dev/null; then
|
||
echo -e "${YELLOW}正在安装 Cloudflare Tunnel (cloudflared)...${NC}"
|
||
if $IS_IPV6_ONLY; then
|
||
# 纯IPv6环境:先换apt源,再直接下载二进制
|
||
setup_ipv6_apt_sources
|
||
install_cloudflared_ipv6 || { echo -e "\n${GREEN}按任意键返回主菜单...${NC}"; read -n 1 -s; return 1; }
|
||
else
|
||
# 正常IPv4/双栈环境:使用官方apt源
|
||
sudo mkdir -p --mode=0755 /usr/share/keyrings
|
||
curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg | sudo tee /usr/share/keyrings/cloudflare-main.gpg >/dev/null
|
||
echo "deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/cloudflared.list
|
||
sudo apt-get update
|
||
sudo apt-get install -y cloudflared
|
||
fi
|
||
|
||
if ! command -v cloudflared &> /dev/null; then
|
||
echo -e "${RED}❌ cloudflared 安装失败!无法找到命令。请检查上面的日志。${NC}"
|
||
echo -e "\n${GREEN}按任意键返回主菜单...${NC}"; read -n 1 -s; return 1
|
||
fi
|
||
echo -e "${GREEN}✅ cloudflared 安装完毕!${NC}"
|
||
fi
|
||
|
||
echo -e "\n${CYAN}接下来,您需要提供 Cloudflare Tunnel 的令牌 (Token)。${NC}"
|
||
echo -e "${CYAN}请登录 Cloudflare Zero Trust Dashboard, 找到 Access -> Tunnels。${NC}"
|
||
echo -e "${CYAN}创建一个新的 Tunnel,选择 'Connector' 类型,复制页面上生成的 Token。${NC}"
|
||
read -p "请在此处粘贴您的 Tunnel Token: " tunnel_token
|
||
if [ -z "$tunnel_token" ]; then
|
||
echo -e "${RED}Token 不能为空,安装中止。${NC}"; sleep 3; return
|
||
fi
|
||
|
||
# 如果旧的 cloudflared 服务文件残留,先清理,避免 "already installed" 报错
|
||
if [ -f "/etc/systemd/system/cloudflared.service" ]; then
|
||
echo -e "${YELLOW}检测到旧的 cloudflared 服务残留,正在清理...${NC}"
|
||
sudo cloudflared service uninstall 2>/dev/null || true
|
||
sudo systemctl daemon-reload
|
||
sleep 1
|
||
echo -e "${GREEN} ✅ 旧服务已清理,继续安装...${NC}"
|
||
fi
|
||
|
||
if sudo cloudflared service install "$tunnel_token"; then
|
||
echo "CLOUDFLARE_TOKEN=${tunnel_token}" > ${STATE_FILE}
|
||
sudo mkdir -p "$(dirname "$TUNNEL_CONFIG_FILE")"
|
||
sudo tee "$TUNNEL_CONFIG_FILE" > /dev/null <<EOF
|
||
# 由鸡公头面板自动生成
|
||
# tunnel: UUID (这一行由cloudflared自动管理,请勿手动修改)
|
||
# credentials-file: /root/.cloudflared/UUID.json (这一行也由cloudflared自动管理)
|
||
|
||
ingress:
|
||
- service: http_status:404
|
||
EOF
|
||
|
||
echo -e "${GREEN}✅ Cloudflare Tunnel 服务安装成功!${NC}"
|
||
echo -e "${YELLOW}正在启动服务...${NC}"
|
||
sudo systemctl start cloudflared
|
||
sleep 2
|
||
|
||
if systemctl is-active --quiet cloudflared; then
|
||
echo -e "${GREEN}✅ Tunnel 服务已成功启动!现在您可以部署其他应用了。${NC}"
|
||
else
|
||
echo -e "${RED}❌ Tunnel 服务启动失败!请检查日志排查问题。${NC}"
|
||
echo -e "${RED} 您可以尝试执行 'sudo journalctl -u cloudflared -f' 来查看实时日志。${NC}"
|
||
fi
|
||
else
|
||
echo -e "${RED}❌ Tunnel 服务安装失败!请检查您的 Token 是否正确,或查看 cloudflared 的错误输出。${NC}"
|
||
fi
|
||
|
||
echo -e "\n${GREEN}按任意键返回主菜单...${NC}"; read -n 1 -s
|
||
}
|
||
|
||
# --- 前置检查 ---
|
||
check_tunnel_installed() {
|
||
if ! systemctl is-active --quiet cloudflared 2>/dev/null; then
|
||
echo -e "${RED}错误:此功能依赖“Cloudflare Tunnel”,请先执行选项 1 进行安装和配置!${NC}"
|
||
sleep 3; return 1
|
||
fi
|
||
return 0
|
||
}
|
||
|
||
# 2. Rclone 数据同步桥
|
||
# 删除单个已配置的 Rclone remote(停服务、卸挂载、清配置)
|
||
_delete_rclone_remote() {
|
||
local remote_name="$1"
|
||
if [ -z "$remote_name" ]; then return; fi
|
||
|
||
local service_name="rclone-${remote_name}-mount"
|
||
local mount_path
|
||
mount_path=$(grep "^RCLONE_MOUNT_${remote_name}=" "${STATE_FILE}" 2>/dev/null | cut -d'=' -f2)
|
||
|
||
echo -e "\n${YELLOW}正在删除 remote: ${CYAN}${remote_name}${NC}"
|
||
|
||
# 1. 停止并禁用 systemd 服务
|
||
if systemctl is-active --quiet "${service_name}.service" 2>/dev/null; then
|
||
echo -e " ${YELLOW}→ 停止挂载服务 ${service_name}...${NC}"
|
||
sudo systemctl stop "${service_name}.service"
|
||
fi
|
||
if systemctl is-enabled --quiet "${service_name}.service" 2>/dev/null; then
|
||
sudo systemctl disable "${service_name}.service"
|
||
fi
|
||
if [ -f "/etc/systemd/system/${service_name}.service" ]; then
|
||
sudo rm -f "/etc/systemd/system/${service_name}.service"
|
||
sudo systemctl daemon-reload
|
||
echo -e " ${GREEN}✅ 已删除 systemd 服务${NC}"
|
||
fi
|
||
|
||
# 2. 卸载挂载点(如果还在挂载中)
|
||
if [ -n "$mount_path" ] && mount | grep -q "${mount_path}"; then
|
||
sudo fusermount -u "${mount_path}" 2>/dev/null || sudo umount -l "${mount_path}" 2>/dev/null
|
||
echo -e " ${GREEN}✅ 已卸载挂载点 ${mount_path}${NC}"
|
||
fi
|
||
|
||
# 3. 从 rclone.conf 删除该 remote 的配置段
|
||
if [ -f "${RCLONE_CONFIG_FILE}" ]; then
|
||
python3 -c "
|
||
import re
|
||
with open('${RCLONE_CONFIG_FILE}', 'r') as fh:
|
||
content = fh.read()
|
||
pattern = r'\[${remote_name}\][^\[]*'
|
||
content = re.sub(pattern, '', content).strip() + '\n'
|
||
with open('${RCLONE_CONFIG_FILE}', 'w') as fh:
|
||
fh.write(content)
|
||
" 2>/dev/null \
|
||
&& echo -e " ${GREEN}✅ 已从 rclone.conf 删除 [${remote_name}] 配置${NC}" \
|
||
|| echo -e " ${YELLOW}⚠️ 无法自动清理 rclone.conf,请手动删除 [${remote_name}] 段落${NC}"
|
||
fi
|
||
|
||
# 4. 清除 STATE_FILE 中的凭证记录
|
||
sudo sed -i "/^RCLONE_REMOTE_${remote_name}=/d" "${STATE_FILE}" 2>/dev/null
|
||
sudo sed -i "/^RCLONE_MOUNT_${remote_name}=/d" "${STATE_FILE}" 2>/dev/null
|
||
# 若删除的是旧版兼容记录的第一个 remote,同步更新
|
||
local compat_remote
|
||
compat_remote=$(grep "^RCLONE_REMOTE=" "${STATE_FILE}" 2>/dev/null | cut -d'=' -f2)
|
||
if [ "$compat_remote" = "$remote_name" ]; then
|
||
sudo sed -i "/^RCLONE_REMOTE=/d" "${STATE_FILE}" 2>/dev/null
|
||
sudo sed -i "/^RCLONE_MOUNT_PATH=/d" "${STATE_FILE}" 2>/dev/null
|
||
# 若仍有其他 remote,将第一个补回兼容记录
|
||
local next_remote
|
||
next_remote=$(grep "^RCLONE_REMOTE_" "${STATE_FILE}" 2>/dev/null | head -1 | cut -d'=' -f2)
|
||
if [ -n "$next_remote" ]; then
|
||
local next_mount
|
||
next_mount=$(grep "^RCLONE_MOUNT_${next_remote}=" "${STATE_FILE}" 2>/dev/null | cut -d'=' -f2)
|
||
echo "RCLONE_REMOTE=${next_remote}" >> "${STATE_FILE}"
|
||
[ -n "$next_mount" ] && echo "RCLONE_MOUNT_PATH=${next_mount}" >> "${STATE_FILE}"
|
||
fi
|
||
fi
|
||
|
||
# 5. 删除日志文件
|
||
sudo rm -f "/var/log/rclone-${remote_name}.log"
|
||
|
||
echo -e " ${GREEN}✅ Remote [${remote_name}] 已完全删除!${NC}"
|
||
}
|
||
|
||
configure_rclone_engine() {
|
||
clear
|
||
echo -e "${BLUE}--- \"Rclone 数据同步桥\"配置向导 (全盘跃遷模式) ---${NC}"
|
||
if ! command -v rclone &> /dev/null; then
|
||
echo -e "\n${YELLOW}🚀 正在为您安装 Rclone 主程序...${NC}"
|
||
curl https://rclone.org/install.sh | sudo bash; sudo apt-get install -y fuse3
|
||
echo -e "${GREEN}✅ Rclone 已安装完毕!${NC}"; sleep 2
|
||
fi
|
||
if [ ! -f "${RCLONE_CONFIG_FILE}" ]; then
|
||
echo -e "\n${YELLOW}未检测到 Rclone 配置文件。${NC}\n${CYAN}即将启动 Rclone 官方交互式配置工具...${NC}"
|
||
read -p "准备好后,请按任意键继续..." -n 1 -s; echo -e "\n"; rclone config
|
||
if [ ! -f "${RCLONE_CONFIG_FILE}" ]; then
|
||
echo -e "\n${RED}错误:配置似乎未成功保存。请重新尝试。${NC}"; sleep 3; return
|
||
fi
|
||
echo -e "\n${GREEN}✅ 检测到 Rclone 配置文件已成功创建!${NC}"; sleep 2
|
||
fi
|
||
echo -e "\n${CYAN}--- 设置 Rclone 全盘自动挂载 ---${NC}"
|
||
|
||
# 读取已有 remote 列表(存入数组,供编号选择使用)
|
||
local remotes=()
|
||
while IFS= read -r r; do
|
||
remotes+=("$r")
|
||
done < <(rclone listremotes --config "${RCLONE_CONFIG_FILE}" 2>/dev/null | sed 's/:$//')
|
||
|
||
echo -e "${YELLOW}当前 Rclone 已配置的 remote:${NC}"
|
||
if [ ${#remotes[@]} -eq 0 ]; then
|
||
echo -e " ${YELLOW}(暂无已配置的 remote)${NC}"
|
||
else
|
||
local i=1
|
||
for r in "${remotes[@]}"; do
|
||
local svc_status=""
|
||
if systemctl is-active --quiet "rclone-${r}-mount.service" 2>/dev/null; then
|
||
svc_status="${GREEN}[运行中]${NC}"
|
||
elif [ -f "/etc/systemd/system/rclone-${r}-mount.service" ]; then
|
||
svc_status="${YELLOW}[已配置/未运行]${NC}"
|
||
fi
|
||
echo -e " ${CYAN}${i})${NC} ${GREEN}${r}${NC} ${svc_status}"
|
||
(( i++ ))
|
||
done
|
||
fi
|
||
echo ""
|
||
echo -e " ${GREEN}a)${NC} 挂载新的 remote(添加)"
|
||
[ ${#remotes[@]} -gt 0 ] && echo -e " ${RED}d)${NC} 删除已配置的 remote"
|
||
echo -e " ${YELLOW}q)${NC} 返回主菜单"
|
||
echo ""
|
||
read -p "请选择操作 (a/d/q): " rclone_action
|
||
|
||
case "$rclone_action" in
|
||
d|D)
|
||
if [ ${#remotes[@]} -eq 0 ]; then
|
||
echo -e "${RED}没有可删除的 remote。${NC}"; sleep 2; configure_rclone_engine; return
|
||
fi
|
||
echo ""
|
||
echo -e "${RED}--- 删除 Rclone Remote ---${NC}"
|
||
echo -e "${YELLOW}请输入要删除的 remote 编号:${NC}"
|
||
local i=1
|
||
for r in "${remotes[@]}"; do
|
||
echo -e " ${CYAN}${i})${NC} ${r}"
|
||
(( i++ ))
|
||
done
|
||
echo ""
|
||
read -p "请输入编号 (1-${#remotes[@]}),或按 q 取消: " del_choice
|
||
if [[ "$del_choice" == "q" || "$del_choice" == "Q" ]]; then
|
||
configure_rclone_engine; return
|
||
fi
|
||
if ! [[ "$del_choice" =~ ^[0-9]+$ ]] || [ "$del_choice" -lt 1 ] || [ "$del_choice" -gt "${#remotes[@]}" ]; then
|
||
echo -e "${RED}❌ 无效编号,操作取消。${NC}"; sleep 2; configure_rclone_engine; return
|
||
fi
|
||
local target_remote="${remotes[$((del_choice - 1))]}"
|
||
echo ""
|
||
echo -e "${RED}⚠️ 即将删除 remote: ${CYAN}${target_remote}${NC}"
|
||
echo -e "${RED} 将执行:停止挂载服务 / 卸载挂载点 / 清除 rclone.conf 配置${NC}"
|
||
read -p "确认删除?(y/n,默认n): " del_confirm
|
||
if [[ "$del_confirm" == "y" || "$del_confirm" == "Y" ]]; then
|
||
_delete_rclone_remote "$target_remote"
|
||
else
|
||
echo -e "${GREEN}已取消。${NC}"
|
||
fi
|
||
echo -e "\n${GREEN}按任意键返回...${NC}"; read -n 1 -s
|
||
configure_rclone_engine; return
|
||
;;
|
||
q|Q)
|
||
return
|
||
;;
|
||
*)
|
||
# a 或其他输入 → 继续走挂载新 remote 的流程
|
||
:
|
||
;;
|
||
esac
|
||
|
||
read -p "请输入您要挂载的 remote 名称 (例如 onedrive / GoogleDrive / S3): " rclone_remote_name
|
||
if [ -z "$rclone_remote_name" ]; then echo -e "${RED}remote 名称不能为空,配置中止。${NC}"; sleep 3; return; fi
|
||
|
||
# 检查输入的 remote 是否已在配置文件中,若没有则先运行 rclone config 添加
|
||
if ! rclone listremotes --config "${RCLONE_CONFIG_FILE}" 2>/dev/null | sed 's/:$//' | grep -qx "${rclone_remote_name}"; then
|
||
echo -e "${YELLOW}⚠️ 未找到名为 "${rclone_remote_name}" 的 remote,需要先配置它。${NC}"
|
||
echo -e "${CYAN}即将启动 rclone config,请按以下步骤操作:${NC}"
|
||
echo -e " 1. 选 ${GREEN}n${NC} 新建 remote"
|
||
echo -e " 2. name 填 ${GREEN}${rclone_remote_name}${NC}"
|
||
echo -e " 3. 完成认证后选 ${GREEN}q${NC} 退出"
|
||
read -p "准备好后按任意键继续..." -n 1 -s; echo ""
|
||
rclone config
|
||
# 再次确认是否配置成功
|
||
if ! rclone listremotes --config "${RCLONE_CONFIG_FILE}" 2>/dev/null | sed 's/:$//' | grep -qx "${rclone_remote_name}"; then
|
||
echo -e "${RED}❌ "${rclone_remote_name}" 仍未配置成功,配置中止。${NC}"; sleep 3; return
|
||
fi
|
||
echo -e "${GREEN}✅ "${rclone_remote_name}" 配置成功!${NC}"; sleep 2
|
||
fi
|
||
|
||
# 挂载路径默认与 remote 名称保持一致(大小写原样保留),用户可自定义
|
||
local default_mount_path="/mnt/${rclone_remote_name}"
|
||
read -p "请输入本机挂载路径 (默认 ${default_mount_path}): " rclone_mount_path
|
||
[ -z "$rclone_mount_path" ] && rclone_mount_path="${default_mount_path}"
|
||
|
||
# systemd 服务名用 remote 名称,支持同机多个 remote 并存
|
||
local service_name="rclone-${rclone_remote_name}-mount"
|
||
local log_file="/var/log/rclone-${rclone_remote_name}.log"
|
||
|
||
# 保存凭证(支持多条记录,用 remote 名称做 key 区分)
|
||
sed -i.bak "/^RCLONE_REMOTE_${rclone_remote_name}=/d" ${STATE_FILE} 2>/dev/null
|
||
sed -i.bak "/^RCLONE_MOUNT_${rclone_remote_name}=/d" ${STATE_FILE} 2>/dev/null
|
||
rm -f "${STATE_FILE}.bak"
|
||
# 同时保留兼容旧版的 RCLONE_REMOTE / RCLONE_MOUNT_PATH(供其他模块读取第一个 remote)
|
||
grep -q "^RCLONE_REMOTE=" ${STATE_FILE} 2>/dev/null || echo "RCLONE_REMOTE=${rclone_remote_name}" >> ${STATE_FILE}
|
||
grep -q "^RCLONE_MOUNT_PATH=" ${STATE_FILE} 2>/dev/null || echo "RCLONE_MOUNT_PATH=${rclone_mount_path}" >> ${STATE_FILE}
|
||
echo "RCLONE_REMOTE_${rclone_remote_name}=${rclone_remote_name}" >> ${STATE_FILE}
|
||
echo "RCLONE_MOUNT_${rclone_remote_name}=${rclone_mount_path}" >> ${STATE_FILE}
|
||
|
||
echo -e "\n${YELLOW}正在为 ${rclone_remote_name} 创建全盘挂载通道 → ${rclone_mount_path} ...${NC}"
|
||
sudo mkdir -p "${rclone_mount_path}"
|
||
|
||
sudo tee "/etc/systemd/system/${service_name}.service" > /dev/null <<EOF
|
||
[Unit]
|
||
Description=Rclone Mount Service for ${rclone_remote_name} -> ${rclone_mount_path}
|
||
Wants=network-online.target
|
||
After=network-online.target
|
||
[Service]
|
||
Type=simple
|
||
User=root
|
||
Group=root
|
||
RestartSec=10
|
||
Restart=on-failure
|
||
ExecStart=/usr/bin/rclone mount ${rclone_remote_name}: ${rclone_mount_path} --config ${RCLONE_CONFIG_FILE} --uid 33 --gid 33 --umask 022 --allow-other --allow-non-empty --vfs-cache-mode off --log-level INFO --log-file ${log_file}
|
||
ExecStop=/bin/fusermount -u ${rclone_mount_path}
|
||
[Install]
|
||
WantedBy=default.target
|
||
EOF
|
||
|
||
sudo systemctl daemon-reload
|
||
sudo systemctl enable --now "${service_name}.service"
|
||
sleep 3
|
||
|
||
if systemctl is-active --quiet "${service_name}.service"; then
|
||
echo -e "${GREEN}✅ Rclone 挂载通道已激活!${NC}"
|
||
echo -e " Remote : ${CYAN}${rclone_remote_name}${NC}"
|
||
echo -e " 挂载路径: ${CYAN}${rclone_mount_path}${NC}"
|
||
echo -e " 服务名 : ${CYAN}${service_name}.service${NC}"
|
||
echo -e " 日志 : ${CYAN}${log_file}${NC}"
|
||
ls "${rclone_mount_path}" 2>/dev/null | head -5 && echo -e "${GREEN} ✅ 挂载内容可读${NC}" || echo -e "${YELLOW} ⚠️ 挂载点为空或读取需要时间${NC}"
|
||
|
||
# 若 Nextcloud 在运行,询问是否注册为外部存储
|
||
if docker ps 2>/dev/null | grep -q "nextcloud_app"; then
|
||
echo ""
|
||
read -p "检测到 Nextcloud 正在运行,是否自动注册为外部存储?(y/n,默认y): " NC_REG
|
||
if [ "$NC_REG" != "n" ] && [ "$NC_REG" != "N" ]; then
|
||
local mount_label=$(basename "${rclone_mount_path}")
|
||
docker exec -u www-data nextcloud_app php occ files_external:create \
|
||
"/${mount_label}" local null::null \
|
||
--config=datadir="${rclone_mount_path}" 2>/dev/null \
|
||
&& echo -e "${GREEN}✅ 已注册到 Nextcloud 外部存储 → /${mount_label}${NC}" \
|
||
|| echo -e "${YELLOW}⚠️ 自动注册失败,请手动在 Nextcloud → 设置 → 外部存储 添加 ${rclone_mount_path}${NC}"
|
||
# 清锁再扫描
|
||
local _DB_PASS
|
||
_DB_PASS=$(grep "MYSQL_PASSWORD" /root/nextcloud_data/docker-compose.yml 2>/dev/null | head -1 | sed 's/.*MYSQL_PASSWORD: //' | tr -d ' ')
|
||
docker exec -u www-data nextcloud_app php occ maintenance:mode --on 2>/dev/null
|
||
[ -n "$_DB_PASS" ] && docker exec nextcloud_db mariadb -u nextclouduser -p"${_DB_PASS}" nextclouddb \
|
||
-e "DELETE FROM oc_file_locks WHERE 1;" 2>/dev/null
|
||
docker exec -u www-data nextcloud_app php occ maintenance:mode --off 2>/dev/null
|
||
docker exec -u www-data nextcloud_app php occ files:scan --all --quiet 2>/dev/null
|
||
echo -e "${GREEN}✅ 扫描完成!${NC}"
|
||
fi
|
||
fi
|
||
else
|
||
echo -e "${RED}❌ 挂载通道启动失败!请检查日志:${NC}"
|
||
echo -e " journalctl -u ${service_name}.service -n 20"
|
||
echo -e " cat ${log_file}"
|
||
fi
|
||
|
||
echo -e "\n${GREEN}Rclone 数据同步桥配置完成!按任意键返回主菜单...${NC}"; read -n 1 -s
|
||
}
|
||
|
||
# 3.1. 部署 Nextcloud
|
||
install_nextcloud() {
|
||
ensure_docker_installed || return; check_tunnel_installed || return
|
||
_check_not_installed "/root/nextcloud_data" "Nextcloud" || return
|
||
read -p "请输入您的主域名 (例如 example.com): " MAIN_DOMAIN
|
||
if [ -z "$MAIN_DOMAIN" ]; then echo -e "${RED}错误:主域名不能为空!${NC}"; sleep 2; return; fi
|
||
local NEXTCLOUD_DOMAIN="nextcloud.${MAIN_DOMAIN}"
|
||
local DB_PASSWORD="NcDb-pW_$(_gen_password 12)"
|
||
clear; echo -e "${BLUE}--- "Nextcloud 核心"部署计划启动! ---${NC}"
|
||
|
||
mkdir -p /root/nextcloud_data
|
||
|
||
# 确保 php-opcache.ini 是文件而不是目录(已知 bug 修复)
|
||
[ -d "/root/nextcloud_data/php-opcache.ini" ] && rm -rf /root/nextcloud_data/php-opcache.ini
|
||
cat > /root/nextcloud_data/php-opcache.ini << 'OPCEOF'
|
||
opcache.enable=1
|
||
opcache.memory_consumption=128
|
||
opcache.interned_strings_buffer=16
|
||
opcache.max_accelerated_files=10000
|
||
opcache.save_comments=1
|
||
opcache.revalidate_freq=60
|
||
OPCEOF
|
||
|
||
# 写入 docker-compose(含 /mnt rshared 映射,让 FUSE/SSHFS 挂载穿透容器)
|
||
cat > /root/nextcloud_data/docker-compose.yml << NCEOF
|
||
services:
|
||
db:
|
||
image: mariadb:11.4
|
||
container_name: nextcloud_db
|
||
restart: unless-stopped
|
||
command: [--transaction-isolation=READ-COMMITTED, --binlog-format=ROW, --character-set-server=utf8mb4, --collation-server=utf8mb4_unicode_ci]
|
||
volumes:
|
||
- './db:/var/lib/mysql'
|
||
environment:
|
||
MYSQL_DATABASE: nextclouddb
|
||
MYSQL_USER: nextclouduser
|
||
MYSQL_PASSWORD: ${DB_PASSWORD}
|
||
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}_root
|
||
redis:
|
||
image: redis:alpine
|
||
container_name: nextcloud_redis
|
||
restart: unless-stopped
|
||
app:
|
||
image: nextcloud:latest
|
||
container_name: nextcloud_app
|
||
restart: unless-stopped
|
||
ports:
|
||
- "127.0.0.1:8888:80"
|
||
volumes:
|
||
- './html:/var/www/html'
|
||
- './php-opcache.ini:/usr/local/etc/php/conf.d/opcache-recommended.ini'
|
||
- type: bind
|
||
source: /mnt
|
||
target: /mnt
|
||
bind:
|
||
propagation: rshared
|
||
depends_on:
|
||
- db
|
||
- redis
|
||
NCEOF
|
||
|
||
echo -e "${YELLOW}[1/5] 正在启动容器...${NC}"
|
||
_compose_up "Nextcloud" "/root/nextcloud_data" || { read -n 1 -s -r -p "按任意键返回..."; return 1; }
|
||
echo -e "${GREEN}✅ 容器已启动,等待服务稳定...${NC}"; sleep 10
|
||
|
||
# 保存凭证
|
||
echo -e "\n## Nextcloud 凭证 (部署于: $(date))" >> ${STATE_FILE}
|
||
echo "NEXTCLOUD_DOMAIN=${NEXTCLOUD_DOMAIN}" >> ${STATE_FILE}
|
||
echo "DB_USER=nextclouduser" >> ${STATE_FILE}
|
||
echo "DB_NAME=nextclouddb" >> ${STATE_FILE}
|
||
echo "DB_HOST=db" >> ${STATE_FILE}
|
||
echo "DB_PASSWORD=${DB_PASSWORD}" >> ${STATE_FILE}
|
||
|
||
# 修复 /mnt 权限(仅设置已存在目录的权限,不主动批量创建)
|
||
echo -e "${YELLOW}[2/5] 正在初始化 /mnt 目录权限...${NC}"
|
||
sudo chown -R www-data:www-data /mnt 2>/dev/null || true
|
||
sudo chmod -R 755 /mnt 2>/dev/null || true
|
||
|
||
# 配置 Tunnel
|
||
echo -e "${YELLOW}[3/5] 正在配置 Cloudflare Tunnel...${NC}"
|
||
update_tunnel_config "${NEXTCLOUD_DOMAIN}" "http://127.0.0.1:8888" "Nextcloud"
|
||
|
||
# 设置定时任务
|
||
echo -e "${YELLOW}[4/5] 正在配置 Nextcloud 定时任务 (Cron)...${NC}"
|
||
(crontab -l 2>/dev/null | grep -v "nextcloud_app php -f"; echo "*/5 * * * * /usr/bin/docker exec --user www-data nextcloud_app php -f /var/www/html/cron.php > /dev/null 2>&1") | crontab -
|
||
(crontab -l 2>/dev/null | grep -v "files:scan"; echo "*/15 * * * * /usr/bin/docker exec --user www-data nextcloud_app php occ files:scan --all > /dev/null 2>&1") | crontab -
|
||
echo -e "${GREEN}✅ Cron 定时任务已配置!${NC}"
|
||
|
||
echo -e "${YELLOW}[5/5] 部署完成!${NC}"
|
||
echo ""
|
||
echo -e "${GREEN}╔══════════════════════════════════════════════════════════╗${NC}"
|
||
echo -e "${GREEN}║ ✅ Nextcloud 部署完成!首次安装指引 ║${NC}"
|
||
echo -e "${GREEN}╠══════════════════════════════════════════════════════════╣${NC}"
|
||
echo -e "${GREEN}║ 访问地址:https://${NEXTCLOUD_DOMAIN} ${NC}"
|
||
echo -e "${GREEN}║ ║${NC}"
|
||
echo -e "${GREEN}║ 在网页中填写以下数据库信息完成初始化: ║${NC}"
|
||
echo -e "${GREEN}║ 数据库类型 :MySQL/MariaDB ║${NC}"
|
||
echo -e "${GREEN}║ 数据库用户 :nextclouduser ║${NC}"
|
||
echo -e "${CYAN}║ 数据库密码 :${DB_PASSWORD} ${NC}"
|
||
echo -e "${GREEN}║ 数据库名 :nextclouddb ║${NC}"
|
||
echo -e "${GREEN}║ 数据库地址 :db ║${NC}"
|
||
echo -e "${GREEN}╠══════════════════════════════════════════════════════════╣${NC}"
|
||
echo -e "${GREEN}║ 初始化后请继续执行: ║${NC}"
|
||
echo -e "${GREEN}║ 22) Nextcloud 精装修(性能优化) ║${NC}"
|
||
echo -e "${GREEN}║ 22.1) Nextcloud 高速风景版(视频封面+BBR) ║${NC}"
|
||
echo -e "${GREEN}╚══════════════════════════════════════════════════════════╝${NC}"
|
||
echo -e "\n${GREEN}按任意键返回主菜单...${NC}"; read -n 1 -s
|
||
}
|
||
# 3.2. 部署 OnlyOffice
|
||
install_onlyoffice() {
|
||
ensure_docker_installed || return; check_tunnel_installed || return
|
||
_check_not_installed "/root/onlyoffice_data" "OnlyOffice" || return
|
||
read -p "请输入您的主域名 (例如 example.com): " MAIN_DOMAIN
|
||
if [ -z "$MAIN_DOMAIN" ]; then echo -e "${RED}错误:主域名不能为空!${NC}"; sleep 2; return; fi
|
||
local ONLYOFFICE_DOMAIN="onlyoffice.${MAIN_DOMAIN}"
|
||
local ONLYOFFICE_JWT_SECRET="JwtS3cr3t-$(_gen_password 16)"
|
||
clear; echo -e "${BLUE}--- “OnlyOffice 办公套件”部署计划启动! ---${NC}"
|
||
|
||
mkdir -p /root/onlyoffice_data; cat > /root/onlyoffice_data/docker-compose.yml <<EOF
|
||
services:
|
||
onlyoffice:
|
||
image: onlyoffice/documentserver:latest
|
||
container_name: onlyoffice_app
|
||
restart: always
|
||
ports:
|
||
- "127.0.0.1:8889:80"
|
||
volumes:
|
||
- './data:/var/www/onlyoffice/Data'
|
||
- './logs:/var/log/onlyoffice'
|
||
environment:
|
||
JWT_ENABLED: 'true'
|
||
JWT_SECRET: ${ONLYOFFICE_JWT_SECRET}
|
||
EOF
|
||
_compose_up "OnlyOffice" "/root/onlyoffice_data" || { read -n 1 -s -r -p "按任意键返回..."; return 1; }
|
||
echo -e "${GREEN}✅ OnlyOffice 已启动,等待服务稳定...${NC}"; sleep 5
|
||
echo -e "\n## OnlyOffice 凭证 (部署于: $(date))" >> ${STATE_FILE}
|
||
echo "ONLYOFFICE_DOMAIN=${ONLYOFFICE_DOMAIN}" >> ${STATE_FILE}
|
||
echo "ONLYOFFICE_JWT_SECRET=${ONLYOFFICE_JWT_SECRET}" >> ${STATE_FILE}
|
||
|
||
echo -e "${GREEN}正在为您配置网络...${NC}"
|
||
update_tunnel_config "${ONLYOFFICE_DOMAIN}" "http://127.0.0.1:8889" "OnlyOffice"
|
||
|
||
echo -e "\n${YELLOW}提示:OnlyOffice 已部署。请登录您的 Nextcloud,在设置中找到 'ONLYOFFICE',${NC}"
|
||
echo -e "${YELLOW}填入此域名 (https://${ONLYOFFICE_DOMAIN}) 和 JWT 密钥 (见选项24) 以完成集成。${NC}"
|
||
echo -e "\n${GREEN}按任意键返回主菜单...${NC}"; read -n 1 -s
|
||
}
|
||
|
||
# 3.3. 部署 Home Assistant
|
||
install_home_assistant() {
|
||
ensure_docker_installed || return; check_tunnel_installed || return
|
||
read -p "请输入您为 Home Assistant 规划的子域名 (例如 ha.example.com): " HA_DOMAIN
|
||
if [ -z "$HA_DOMAIN" ]; then echo -e "${RED}错误:域名不能为空!${NC}"; sleep 2; return; fi
|
||
clear; echo -e "${BLUE}--- “Home Assistant 智能家居中枢”部署计划启动! ---${NC}"
|
||
|
||
# 【修复】创建 config 目录,为预配置做准备
|
||
mkdir -p /root/home_assistant_data/config
|
||
|
||
cat > /root/home_assistant_data/docker-compose.yml <<EOF
|
||
services:
|
||
homeassistant:
|
||
image: ghcr.io/home-assistant/home-assistant:stable
|
||
container_name: home_assistant_app
|
||
restart: always
|
||
ports:
|
||
- "127.0.0.1:8123:8123"
|
||
volumes:
|
||
- './config:/config'
|
||
- '/etc/localtime:/etc/localtime:ro'
|
||
environment:
|
||
- 'TZ=Asia/Shanghai'
|
||
EOF
|
||
|
||
# ==================== 【关键修复开始】 ====================
|
||
# 在容器启动前,预先创建配置文件,并添加信任代理的设置
|
||
echo -e "${YELLOW}正在为 Home Assistant 预配置受信任的代理...${NC}"
|
||
cat > /root/home_assistant_data/config/configuration.yaml <<EOF
|
||
# 由鸡公头面板自动添加,用于信任 Cloudflare Tunnel
|
||
default_config:
|
||
|
||
http:
|
||
use_x_forwarded_for: true
|
||
trusted_proxies:
|
||
- 127.0.0.1 # 信任来自本机的请求
|
||
- 172.16.0.0/12 # 信任 Docker 的默认网段
|
||
- 10.0.0.0/8 # 信任其他常见的内部网段
|
||
- 192.168.0.0/16
|
||
EOF
|
||
# ==================== 【关键修复结束】 ====================
|
||
|
||
echo -e "${YELLOW}正在启动 Home Assistant 服务... (首次启动可能需要几分钟时间初始化,请稍候...)${NC}"
|
||
if _compose_up "Home Assistant" "/root/home_assistant_data"; then
|
||
echo -e "${GREEN}✅ Home Assistant 已启动,正在进行初始化...${NC}"; sleep 10
|
||
echo -e "\n## Home Assistant 凭证 (部署于: $(date))" >> ${STATE_FILE}
|
||
echo "HA_DOMAIN=${HA_DOMAIN}" >> ${STATE_FILE}
|
||
|
||
echo -e "${GREEN}正在为您配置网络...${NC}"
|
||
update_tunnel_config "${HA_DOMAIN}" "http://127.0.0.1:8123" "Home Assistant"
|
||
|
||
echo -e "\n${GREEN}✅ Home Assistant 部署成功!${NC}"
|
||
echo -e "${YELLOW}请访问 https://${HA_DOMAIN} 来创建您的管理员账户并开始配置。${NC}"
|
||
else
|
||
echo -e "${RED}❌ Home Assistant 部署失败!请检查 Docker 是否正常运行。${NC}"
|
||
fi
|
||
echo -e "\n${GREEN}按任意键返回主菜单...${NC}"; read -n 1 -s
|
||
}
|
||
|
||
# 4. WordPress
|
||
install_wordpress() {
|
||
ensure_docker_installed || return; check_tunnel_installed || return
|
||
read -p "请输入您的 WordPress 域名 (例如 blog.example.com): " WP_DOMAIN
|
||
if [ -z "$WP_DOMAIN" ]; then echo -e "${RED}错误:域名不能为空!${NC}"; sleep 2; return; fi
|
||
local WP_DB_PASS="WpDb-pW_$(_gen_password 12)"; local WP_DB_ROOT_PASS="WpRoot-pW_$(_gen_password 12)"
|
||
clear; echo -e "${BLUE}--- “WordPress 个人博客”建造计划启动! ---${NC}"
|
||
mkdir -p /root/wordpress_data; cat > /root/wordpress_data/docker-compose.yml <<EOF
|
||
services:
|
||
db:
|
||
image: mariadb:11.4
|
||
container_name: wordpress_db
|
||
restart: unless-stopped
|
||
volumes:
|
||
- './db_data:/var/lib/mysql'
|
||
environment:
|
||
MYSQL_ROOT_PASSWORD: ${WP_DB_ROOT_PASS}
|
||
MYSQL_DATABASE: wordpress
|
||
MYSQL_USER: wordpress
|
||
MYSQL_PASSWORD: ${WP_DB_PASS}
|
||
wordpress:
|
||
image: wordpress:latest
|
||
container_name: wordpress_app
|
||
restart: unless-stopped
|
||
ports:
|
||
- "127.0.0.1:8890:80"
|
||
volumes:
|
||
- './html:/var/www/html'
|
||
environment:
|
||
WORDPRESS_DB_HOST: db
|
||
WORDPRESS_DB_USER: wordpress
|
||
WORDPRESS_DB_PASSWORD: ${WP_DB_PASS}
|
||
WORDPRESS_DB_NAME: wordpress
|
||
depends_on:
|
||
- db
|
||
EOF
|
||
if _compose_up "WordPress" "/root/wordpress_data"; then
|
||
echo -e "${GREEN}✅ WordPress 已启动,等待服务稳定...${NC}"; sleep 5
|
||
echo -e "\n## WordPress 凭证 (部署于: $(date))" >> ${STATE_FILE}; echo "WORDPRESS_DOMAIN=${WP_DOMAIN}" >> ${STATE_FILE}
|
||
echo -e "${GREEN}正在为您配置网络...${NC}"; update_tunnel_config "${WP_DOMAIN}" "http://127.0.0.1:8890" "WordPress"
|
||
else echo -e "${RED}❌ WordPress 部署失败!${NC}"; fi
|
||
echo -e "\n${GREEN}按任意键返回主菜单...${NC}"; read -n 1 -s
|
||
}
|
||
|
||
# 5. AI 核心
|
||
install_ai_suite() {
|
||
ensure_docker_installed || return; check_tunnel_installed || return
|
||
read -p "请输入您为 AI 规划的子域名 (例如 ai.example.com): " AI_DOMAIN
|
||
if [ -z "$AI_DOMAIN" ]; then echo -e "${RED}错误:AI 域名不能为空!${NC}"; sleep 2; return; fi
|
||
clear; echo -e "${BLUE}--- “AI 大脑”激活计划启动! ---${NC}"
|
||
mkdir -p /root/ai_stack; cat > /root/ai_stack/docker-compose.yml <<EOF
|
||
services:
|
||
ollama:
|
||
image: ollama/ollama
|
||
container_name: ollama
|
||
restart: unless-stopped
|
||
volumes:
|
||
- './ollama_data:/root/.ollama'
|
||
open-webui:
|
||
image: 'ghcr.io/open-webui/open-webui:main'
|
||
container_name: open_webui_app
|
||
restart: unless-stopped
|
||
ports:
|
||
- "127.0.0.1:3001:8080"
|
||
environment:
|
||
- 'OLLAMA_BASE_URL=http://ollama:11434'
|
||
depends_on:
|
||
- ollama
|
||
EOF
|
||
if _compose_up "AI 大脑" "/root/ai_stack"; then
|
||
echo -e "${GREEN}✅ AI 核心已启动,等待服务稳定...${NC}"; sleep 5
|
||
echo -e "\n## AI 核心凭证 (部署于: $(date))" >> ${STATE_FILE}; echo "AI_DOMAIN=${AI_DOMAIN}" >> ${STATE_FILE}
|
||
echo -e "${GREEN}正在为您配置网络...${NC}"; update_tunnel_config "${AI_DOMAIN}" "http://127.0.0.1:3001" "AI WebUI"
|
||
echo -e "${YELLOW}强烈建议立即执行选项 17 安装一个知识库 (模型)!${NC}"
|
||
else echo -e "${RED}❌ AI 核心部署失败!${NC}"; fi
|
||
echo -e "\n${GREEN}按任意键返回主菜单...${NC}"; read -n 1 -s
|
||
}
|
||
|
||
# 6. Jellyfin
|
||
install_jellyfin() {
|
||
ensure_docker_installed || return; check_tunnel_installed || return
|
||
_check_not_installed "/root/jellyfin_data" "Jellyfin" || return
|
||
clear
|
||
read -p "请输入您为 Jellyfin 规划的子域名 (例如 jellyfin.example.com): " JELLYFIN_DOMAIN
|
||
if [ -z "$JELLYFIN_DOMAIN" ]; then echo -e "${RED}域名不能为空!${NC}"; sleep 2; return; fi
|
||
echo -e "${BLUE}--- “Jellyfin 家庭影院”建造计划启动! ---${NC}"
|
||
mkdir -p /root/jellyfin_data/config
|
||
cat > /root/jellyfin_data/docker-compose.yml <<EOF
|
||
services:
|
||
jellyfin:
|
||
image: jellyfin/jellyfin:latest
|
||
container_name: jellyfin_app
|
||
restart: unless-stopped
|
||
ports:
|
||
- "127.0.0.1:8096:8096"
|
||
environment:
|
||
- 'PUID=0'
|
||
- 'PGID=0'
|
||
- 'TZ=Asia/Shanghai'
|
||
volumes:
|
||
- './config:/config'
|
||
- type: bind
|
||
source: /mnt
|
||
target: /mnt
|
||
bind:
|
||
propagation: rshared
|
||
EOF
|
||
echo -e "${CYAN}提示:Jellyfin 已挂载整个 /mnt,添加媒体库时路径填 /mnt/Movies 或 /mnt/Frankfurt/Movies 等即可。${NC}"
|
||
if _compose_up "Jellyfin" "/root/jellyfin_data"; then
|
||
echo -e "${GREEN}✅ Jellyfin 已启动,等待服务稳定...${NC}"; sleep 5
|
||
echo -e "\n## Jellyfin 凭证 (部署于: $(date))" >> ${STATE_FILE}; echo "JELLYFIN_DOMAIN=${JELLYFIN_DOMAIN}" >> ${STATE_FILE}
|
||
echo -e "${GREEN}正在为您配置网络...${NC}"; update_tunnel_config "${JELLYFIN_DOMAIN}" "http://127.0.0.1:8096" "Jellyfin"
|
||
else echo -e "${RED}❌ Jellyfin 部署失败!${NC}"; fi
|
||
echo -e "\n${GREEN}按任意键返回主菜单...${NC}"; read -n 1 -s
|
||
}
|
||
|
||
# 7. Navidrome
|
||
install_navidrome() {
|
||
ensure_docker_installed || return; check_tunnel_installed || return
|
||
_check_not_installed "/root/navidrome_data" "Navidrome" || return
|
||
clear
|
||
read -p "请输入您为 Navidrome 规划的子域名 (例如 music.example.com): " NAVIDROME_DOMAIN
|
||
if [ -z "$NAVIDROME_DOMAIN" ]; then echo -e "${RED}域名不能为空!${NC}"; sleep 2; return; fi
|
||
echo -e "${BLUE}--- “Navidrome 音乐服务器”部署计划启动! ---${NC}"
|
||
mkdir -p /root/navidrome_data
|
||
echo ""
|
||
echo -e "${CYAN}请输入音乐库路径(支持任意 /mnt 下的路径,例如 /mnt/Music 或 /mnt/Frankfurt/Music)${NC}"
|
||
read -p "音乐库路径 (默认 /mnt/Music): " ND_MUSIC_PATH
|
||
[ -z "$ND_MUSIC_PATH" ] && ND_MUSIC_PATH="/mnt/Music"
|
||
sudo mkdir -p "$ND_MUSIC_PATH"
|
||
cat > /root/navidrome_data/docker-compose.yml <<EOF
|
||
services:
|
||
navidrome:
|
||
image: deluan/navidrome:latest
|
||
container_name: navidrome_app
|
||
restart: unless-stopped
|
||
ports:
|
||
- "127.0.0.1:4533:4533"
|
||
volumes:
|
||
- '${ND_MUSIC_PATH}:/music'
|
||
- './data:/data'
|
||
environment:
|
||
- 'PUID=1000'
|
||
- 'PGID=1000'
|
||
- 'TZ=Asia/Shanghai'
|
||
EOF
|
||
if _compose_up "Navidrome" "/root/navidrome_data"; then
|
||
echo -e "${GREEN}✅ Navidrome 已启动,等待服务稳定...${NC}"; sleep 5
|
||
echo -e "\n## Navidrome 凭证 (部署于: $(date))" >> ${STATE_FILE}; echo "NAVIDROME_DOMAIN=${NAVIDROME_DOMAIN}" >> ${STATE_FILE}
|
||
echo -e "${GREEN}正在为您配置网络...${NC}"; update_tunnel_config "${NAVIDROME_DOMAIN}" "http://127.0.0.1:4533" "Navidrome"
|
||
else echo -e "${RED}❌ Navidrome 部署失败!${NC}"; fi
|
||
echo -e "\n${GREEN}按任意键返回主菜单...${NC}"; read -n 1 -s
|
||
}
|
||
|
||
# 8. Miniflux RSS 阅读器
|
||
install_miniflux() {
|
||
ensure_docker_installed || return; check_tunnel_installed || return
|
||
_check_not_installed "/root/miniflux_data" "Miniflux" || return
|
||
clear
|
||
read -p "请输入您为 Miniflux 规划的子域名 (例如 miniflux.example.com): " MINIFLUX_DOMAIN
|
||
if [ -z "$MINIFLUX_DOMAIN" ]; then echo -e "${RED}域名不能为空!${NC}"; sleep 2; return; fi
|
||
local DB_PASSWORD="miniflux_secret_$(_gen_password 12)"
|
||
local ADMIN_PASSWORD_INITIAL="admin123456"
|
||
|
||
echo -e "${BLUE}--- “Miniflux RSS 阅读器”部署计划启动! ---${NC}";
|
||
mkdir -p /root/miniflux_data
|
||
cat > /root/miniflux_data/docker-compose.yml <<EOF
|
||
services:
|
||
db:
|
||
image: postgres:15-alpine
|
||
container_name: miniflux_db
|
||
restart: unless-stopped
|
||
environment:
|
||
POSTGRES_USER: miniflux
|
||
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
||
POSTGRES_DB: miniflux
|
||
volumes:
|
||
- ./db_data:/var/lib/postgresql/data
|
||
healthcheck:
|
||
test: ["CMD", "pg_isready", "-U", "miniflux"]
|
||
interval: 10s
|
||
start_period: 30s
|
||
app:
|
||
image: miniflux/miniflux:latest
|
||
container_name: miniflux_app
|
||
restart: unless-stopped
|
||
ports:
|
||
- "127.0.0.1:8091:8080"
|
||
depends_on:
|
||
db:
|
||
condition: service_healthy
|
||
environment:
|
||
DATABASE_URL: postgres://miniflux:${DB_PASSWORD}@db/miniflux?sslmode=disable
|
||
RUN_MIGRATIONS: "1"
|
||
CREATE_ADMIN: "1"
|
||
ADMIN_USERNAME: admin
|
||
ADMIN_PASSWORD: ${ADMIN_PASSWORD_INITIAL}
|
||
BASE_URL: https://${MINIFLUX_DOMAIN}
|
||
EOF
|
||
|
||
if _compose_up "Miniflux" "/root/miniflux_data"; then
|
||
echo -e "${GREEN}✅ Miniflux 已启动,等待服务稳定...${NC}"; sleep 10
|
||
echo -e "\n## Miniflux 凭证 (部署于: $(date))" >> ${STATE_FILE}
|
||
echo "MINIFLUX_DOMAIN=${MINIFLUX_DOMAIN}" >> ${STATE_FILE}
|
||
echo "MINIFLUX_ADMIN_USER=admin" >> ${STATE_FILE}
|
||
echo "MINIFLUX_ADMIN_PASSWORD_INITIAL=${ADMIN_PASSWORD_INITIAL}" >> ${STATE_FILE}
|
||
echo -e "${GREEN}正在为您配置网络...${NC}";
|
||
update_tunnel_config "${MINIFLUX_DOMAIN}" "http://127.0.0.1:8091" "Miniflux"
|
||
echo -e "\n${YELLOW}首次登录 https://${MINIFLUX_DOMAIN} 后,请立即修改密码!${NC}"
|
||
else
|
||
echo -e "${RED}❌ Miniflux 部署失败!${NC}";
|
||
fi
|
||
echo -e "\n${GREEN}按任意键返回主菜单...${NC}"; read -n 1 -s
|
||
}
|
||
|
||
# 9. Gitea
|
||
install_gitea() {
|
||
ensure_docker_installed || return; check_tunnel_installed || return
|
||
_check_not_installed "/root/gitea_data" "Gitea" || return
|
||
clear
|
||
read -p "请输入您为 Gitea 规划的子域名 (例如 git.example.com): " GITEA_DOMAIN
|
||
if [ -z "$GITEA_DOMAIN" ]; then echo -e "${RED}域名不能为空!${NC}"; sleep 2; return; fi
|
||
echo -e "${BLUE}--- “Gitea 代码仓库”部署计划启动! ---${NC}"; mkdir -p /root/gitea_data
|
||
cat >/root/gitea_data/docker-compose.yml <<EOF
|
||
services:
|
||
server:
|
||
image: gitea/gitea:latest
|
||
container_name: gitea_app
|
||
restart: unless-stopped
|
||
ports:
|
||
- "127.0.0.1:3000:3000"
|
||
environment:
|
||
- 'USER_UID=1000'
|
||
- 'USER_GID=1000'
|
||
volumes:
|
||
- './gitea:/data'
|
||
- '/etc/timezone:/etc/timezone:ro'
|
||
- '/etc/localtime:/etc/localtime:ro'
|
||
EOF
|
||
if _compose_up "Gitea" "/root/gitea_data"; then
|
||
echo -e "${GREEN}✅ Gitea 已启动,等待服务稳定...${NC}"; sleep 5
|
||
echo -e "\n## Gitea 凭证 (部署于: $(date))" >> ${STATE_FILE}; echo "GITEA_DOMAIN=${GITEA_DOMAIN}" >> ${STATE_FILE}
|
||
echo -e "${GREEN}正在为您配置网络...${NC}"; update_tunnel_config "${GITEA_DOMAIN}" "http://127.0.0.1:3000" "Gitea"
|
||
else echo -e "${RED}❌ Gitea 部署失败!${NC}"; fi
|
||
echo -e "\n${GREEN}按任意键返回主菜单...${NC}"; read -n 1 -s
|
||
}
|
||
|
||
# 10. Memos (已集成 Mortis 修复 App API)
|
||
install_memos() {
|
||
ensure_docker_installed || return; check_tunnel_installed || return
|
||
_check_not_installed "/root/memos_data" "Memos" || return
|
||
clear
|
||
echo -e "${BLUE}=== 📝 部署 Memos 轻量笔记 (Immortal 永久稳定版) ===${NC}"
|
||
echo -e "${CYAN}使用 zhangcaiduo/Memos-Immortal-Backup 维护的稳定镜像,集成 Mortis gRPC 桥${NC}"
|
||
echo ""
|
||
|
||
read -p "请输入 Memos 网页版域名 (例如 memos.example.com): " MEMOS_DOMAIN
|
||
if [ -z "$MEMOS_DOMAIN" ]; then echo -e "${RED}域名不能为空!${NC}"; sleep 2; return; fi
|
||
|
||
read -p "请输入 App gRPC 接口域名 (例如 grpc.memos.example.com): " MORTIS_DOMAIN
|
||
if [ -z "$MORTIS_DOMAIN" ]; then echo -e "${RED}gRPC 域名不能为空!${NC}"; sleep 2; return; fi
|
||
|
||
read -p "请输入版本号 [默认 0.22.4]: " MEMOS_VER
|
||
MEMOS_VER=${MEMOS_VER:-"0.22.4"}
|
||
MEMOS_VER=${MEMOS_VER#v}
|
||
|
||
mkdir -p /root/memos_data/data
|
||
|
||
cat > /root/memos_data/docker-compose.yml << MEMOSEOF
|
||
services:
|
||
memos:
|
||
image: ghcr.io/zhangcaiduo/memos-immortal-backup:${MEMOS_VER}
|
||
container_name: memos_app
|
||
restart: always
|
||
ports:
|
||
- "127.0.0.1:5230:5230"
|
||
volumes:
|
||
- './data:/var/opt/memos'
|
||
command: ["--addr", "0.0.0.0", "--port", "5230", "--data", "/var/opt/memos"]
|
||
|
||
mortis:
|
||
image: ghcr.io/mudkipme/mortis:latest
|
||
container_name: memos_mortis_translator
|
||
restart: always
|
||
ports:
|
||
- "127.0.0.1:5231:5231"
|
||
entrypoint: ["/app/mortis"]
|
||
command: ["-grpc-addr=memos:5230"]
|
||
depends_on:
|
||
- memos
|
||
MEMOSEOF
|
||
|
||
echo -e "${YELLOW}正在拉取 Memos v${MEMOS_VER} 镜像(来自 Immortal 稳定备份)...${NC}"
|
||
if _compose_up "Memos" "/root/memos_data"; then
|
||
echo -e "${GREEN}✅ Memos 和 Mortis 已启动,等待服务稳定...${NC}"; sleep 8
|
||
echo -e "\n## Memos 凭证 (部署于: $(date))" >> ${STATE_FILE}
|
||
echo "MEMOS_DOMAIN=${MEMOS_DOMAIN}" >> ${STATE_FILE}
|
||
echo "MORTIS_DOMAIN=${MORTIS_DOMAIN}" >> ${STATE_FILE}
|
||
echo "MEMOS_VERSION=${MEMOS_VER}" >> ${STATE_FILE}
|
||
|
||
echo -e "${GREEN}正在配置 Cloudflare Tunnel(Web + gRPC 双路)...${NC}"
|
||
update_tunnel_config "${MEMOS_DOMAIN}" "http://127.0.0.1:5230" "Memos (WebUI)"
|
||
|
||
# gRPC 路由特殊处理
|
||
sudo sed -i "/- service: http_status:404/d" "$TUNNEL_CONFIG_FILE"
|
||
printf "\n # Memos (gRPC App)\n - hostname: %s\n service: grpc://127.0.0.1:5231\n" "${MORTIS_DOMAIN}" | sudo tee -a "$TUNNEL_CONFIG_FILE" > /dev/null
|
||
echo " - service: http_status:404" | sudo tee -a "$TUNNEL_CONFIG_FILE" > /dev/null
|
||
sudo systemctl restart cloudflared
|
||
|
||
echo ""
|
||
echo -e "${GREEN}╔══════════════════════════════════════════════════════════╗${NC}"
|
||
echo -e "${GREEN}║ ✅ Memos v${MEMOS_VER} 部署成功! ║${NC}"
|
||
echo -e "${GREEN}╠══════════════════════════════════════════════════════════╣${NC}"
|
||
echo -e "${GREEN}║ 网页版: ${CYAN}https://${MEMOS_DOMAIN}${NC}"
|
||
echo -e "${GREEN}║ App接口: ${CYAN}${MORTIS_DOMAIN}${NC} (填入手机App服务器地址)"
|
||
echo -e "${GREEN}║ 版本: v${MEMOS_VER} (Immortal 永久稳定版) ║${NC}"
|
||
echo -e "${GREEN}╚══════════════════════════════════════════════════════════╝${NC}"
|
||
else
|
||
echo -e "${RED}❌ Memos 部署失败!请检查版本号 ${MEMOS_VER} 是否存在。${NC}"
|
||
echo -e "${YELLOW}可用版本: https://github.com/zhangcaiduo/Memos-Immortal-Backup/releases${NC}"
|
||
fi
|
||
echo -e "\n${GREEN}按任意键返回主菜单...${NC}"; read -n 1 -s
|
||
}
|
||
|
||
# 11. qBittorrent
|
||
install_qbittorrent() {
|
||
ensure_docker_installed || return; check_tunnel_installed || return
|
||
_check_not_installed "/root/qbittorrent_data" "qBittorrent" || return
|
||
clear
|
||
read -p "请输入您为 qBittorrent 规划的子域名 (例如 qb.example.com): " QB_DOMAIN
|
||
if [ -z "$QB_DOMAIN" ]; then echo -e "${RED}域名不能为空!${NC}"; sleep 2; return; fi
|
||
echo -e "${BLUE}--- “qBittorrent 下载器”部署计划启动! ---${NC}"; mkdir -p /root/qbittorrent_data /mnt/Downloads
|
||
cat > /root/qbittorrent_data/docker-compose.yml <<EOF
|
||
services:
|
||
qbittorrent:
|
||
image: lscr.io/linuxserver/qbittorrent:latest
|
||
container_name: qbittorrent_app
|
||
restart: unless-stopped
|
||
ports:
|
||
- "127.0.0.1:8080:8080"
|
||
- "127.0.0.1:6881:6881"
|
||
- "127.0.0.1:6881:6881/udp"
|
||
environment:
|
||
- 'PUID=1000'
|
||
- 'PGID=1000'
|
||
- 'TZ=Asia/Shanghai'
|
||
- 'WEBUI_PORT=8080'
|
||
volumes:
|
||
- './config:/config'
|
||
- '/mnt/Downloads:/downloads'
|
||
EOF
|
||
if _compose_up "qBittorrent" "/root/qbittorrent_data"; then
|
||
echo -e "${GREEN}✅ qBittorrent 已启动,等待服务稳定...${NC}"; sleep 5
|
||
echo -e "\n## qBittorrent 凭证 (部署于: $(date))" >> ${STATE_FILE}; echo "QB_DOMAIN=${QB_DOMAIN}" >> ${STATE_FILE}
|
||
echo -e "${GREEN}正在为您配置网络...${NC}"; update_tunnel_config "${QB_DOMAIN}" "http://127.0.0.1:8080" "qBittorrent"
|
||
else echo -e "${RED}❌ qBittorrent 部署失败!${NC}"; fi
|
||
echo -e "\n${GREEN}按任意键返回主菜单...${NC}"; read -n 1 -s
|
||
}
|
||
|
||
# 12. JDownloader
|
||
install_jdownloader() {
|
||
ensure_docker_installed || return; check_tunnel_installed || return
|
||
_check_not_installed "/root/jdownloader_data" "JDownloader" || return
|
||
clear
|
||
read -p "请输入您为 JDownloader 规划的子域名 (例如 jd.example.com): " JD_DOMAIN
|
||
if [ -z "$JD_DOMAIN" ]; then echo -e "${RED}域名不能为空!${NC}"; sleep 2; return; fi
|
||
local JDOWNLOADER_PASS="VNC-Pass-$(_gen_password 8)"
|
||
echo -e "${BLUE}--- “JDownloader 下载器”部署计划启动! ---${NC}"; mkdir -p /root/jdownloader_data /mnt/Downloads
|
||
cat > /root/jdownloader_data/docker-compose.yml <<EOF
|
||
services:
|
||
jdownloader-2:
|
||
image: jlesage/jdownloader-2
|
||
container_name: jdownloader_app
|
||
restart: unless-stopped
|
||
ports:
|
||
- "127.0.0.1:5800:5800"
|
||
volumes:
|
||
- './config:/config'
|
||
- '/mnt/Downloads:/output'
|
||
environment:
|
||
- 'USER_ID=1000'
|
||
- 'GROUP_ID=1000'
|
||
- 'TZ=Asia/Shanghai'
|
||
- 'VNC_PASSWORD=${JDOWNLOADER_PASS}'
|
||
EOF
|
||
if _compose_up "JDownloader" "/root/jdownloader_data"; then
|
||
echo -e "${GREEN}✅ JDownloader 已启动,等待服务稳定...${NC}"; sleep 5
|
||
echo -e "\n## JDownloader 凭证 (部署于: $(date))" >> ${STATE_FILE}; echo "JD_DOMAIN=${JD_DOMAIN}" >> ${STATE_FILE}
|
||
echo "JDOWNLOADER_VNC_PASSWORD=${JDOWNLOADER_PASS}" >> ${STATE_FILE}
|
||
echo -e "${GREEN}正在为您配置网络...${NC}"; update_tunnel_config "${JD_DOMAIN}" "http://127.0.0.1:5800" "JDownloader"
|
||
else echo -e "${RED}❌ JDownloader 部署失败!${NC}"; fi
|
||
echo -e "\n${GREEN}按任意键返回主菜单...${NC}"; read -n 1 -s
|
||
}
|
||
|
||
# 13. yt-dlp (MeTube)
|
||
install_ytdlp() {
|
||
ensure_docker_installed || return; check_tunnel_installed || return
|
||
_check_not_installed "/root/ytdlp_data" "MeTube" || return
|
||
clear
|
||
echo -e "${BLUE}=== 📥 部署 MeTube 视频下载器 ===${NC}"
|
||
echo -e "${CYAN}基于 yt-dlp,支持 YouTube、B站、Twitter 等数百个网站${NC}"
|
||
echo ""
|
||
|
||
read -p "请输入 MeTube 的访问域名 (例如 dl.example.com): " YTDL_DOMAIN
|
||
if [ -z "$YTDL_DOMAIN" ]; then echo -e "${RED}域名不能为空!${NC}"; sleep 2; return; fi
|
||
|
||
# 询问下载目录
|
||
echo -e "${YELLOW}下载文件保存位置:${NC}"
|
||
echo " 1) 默认路径 /mnt/Downloads(推荐,可被 Nextcloud 外部存储访问)"
|
||
echo " 2) 自定义路径"
|
||
read -p "请选择 (1/2,默认1): " DL_CHOICE
|
||
if [ "$DL_CHOICE" = "2" ]; then
|
||
read -p "请输入自定义下载路径: " YTDL_DOWNLOAD_DIR
|
||
[ -z "$YTDL_DOWNLOAD_DIR" ] && YTDL_DOWNLOAD_DIR="/mnt/Downloads"
|
||
else
|
||
YTDL_DOWNLOAD_DIR="/mnt/Downloads"
|
||
fi
|
||
sudo mkdir -p "${YTDL_DOWNLOAD_DIR}"
|
||
echo -e "${GREEN}下载路径: ${YTDL_DOWNLOAD_DIR}${NC}"
|
||
|
||
# 询问是否配置 cookies(甲骨文等机器IP被YouTube要求验证时必须配置)
|
||
echo ""
|
||
echo -e "${YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||
echo -e "${YELLOW} YouTube Cookies 配置(解决"Sign in to confirm"报错)${NC}"
|
||
echo -e "${YELLOW} 甲骨文/某些机房IP需要此配置才能下载 YouTube 视频${NC}"
|
||
echo -e "${YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||
echo -e " 1) 现在配置 cookies(推荐,特别是甲骨文机器)"
|
||
echo -e " 2) 跳过,稍后手动上传 cookies.txt 到 /root/ytdlp_data/cookies.txt"
|
||
read -p "请选择 (1/2,默认2): " COOKIE_CHOICE
|
||
|
||
mkdir -p /root/ytdlp_data
|
||
|
||
if [ "$COOKIE_CHOICE" = "1" ]; then
|
||
echo ""
|
||
echo -e "${CYAN}请按以下步骤获取 cookies.txt:${NC}"
|
||
echo -e " 1. 在您自己的电脑浏览器安装扩展 ${YELLOW}"Get cookies.txt LOCALLY"${NC}"
|
||
echo -e " 2. 登录 YouTube 账号,停留在 youtube.com 页面"
|
||
echo -e " 3. 点击扩展图标 → Export → 保存为 cookies.txt"
|
||
echo -e " 4. 用 SFTP(WindTerm/Tabby)上传到 ${YELLOW}/root/ytdlp_data/cookies.txt${NC}"
|
||
echo ""
|
||
read -p "上传完成后请按回车继续(或按 s 跳过cookies配置): " COOKIE_WAIT
|
||
if [ "$COOKIE_WAIT" != "s" ] && [ "$COOKIE_WAIT" != "S" ]; then
|
||
if [ -f "/root/ytdlp_data/cookies.txt" ]; then
|
||
echo -e "${GREEN}✅ 检测到 cookies.txt,将自动挂载!${NC}"
|
||
USE_COOKIES=true
|
||
else
|
||
echo -e "${YELLOW}⚠️ 未检测到 cookies.txt,将不挂载(可稍后手动添加)${NC}"
|
||
USE_COOKIES=false
|
||
fi
|
||
else
|
||
USE_COOKIES=false
|
||
fi
|
||
else
|
||
USE_COOKIES=false
|
||
fi
|
||
|
||
# 生成 docker-compose.yml
|
||
if [ "$USE_COOKIES" = true ]; then
|
||
cat > /root/ytdlp_data/docker-compose.yml << YTEOF
|
||
services:
|
||
ytdlp-ui:
|
||
image: ghcr.io/alexta69/metube:latest
|
||
container_name: ytdlp_app
|
||
restart: unless-stopped
|
||
ports:
|
||
- "127.0.0.1:8999:8081"
|
||
volumes:
|
||
- '${YTDL_DOWNLOAD_DIR}:/downloads'
|
||
- '/root/ytdlp_data/cookies.txt:/cookies.txt:ro'
|
||
environment:
|
||
- TZ=Asia/Shanghai
|
||
- YTDL_OPTIONS={"cookiefile":"/cookies.txt"}
|
||
YTEOF
|
||
else
|
||
cat > /root/ytdlp_data/docker-compose.yml << YTEOF
|
||
services:
|
||
ytdlp-ui:
|
||
image: ghcr.io/alexta69/metube:latest
|
||
container_name: ytdlp_app
|
||
restart: unless-stopped
|
||
ports:
|
||
- "127.0.0.1:8999:8081"
|
||
volumes:
|
||
- '${YTDL_DOWNLOAD_DIR}:/downloads'
|
||
environment:
|
||
- TZ=Asia/Shanghai
|
||
YTEOF
|
||
fi
|
||
|
||
if _compose_up "MeTube" "/root/ytdlp_data"; then
|
||
echo -e "${GREEN}✅ MeTube 已启动,等待服务稳定...${NC}"; sleep 5
|
||
echo -e "\n## MeTube/yt-dlp 凭证 (部署于: $(date))" >> ${STATE_FILE}
|
||
echo "YTDL_DOMAIN=${YTDL_DOMAIN}" >> ${STATE_FILE}
|
||
echo "YTDL_DOWNLOAD_DIR=${YTDL_DOWNLOAD_DIR}" >> ${STATE_FILE}
|
||
echo -e "${GREEN}正在配置 Cloudflare Tunnel...${NC}"
|
||
update_tunnel_config "${YTDL_DOMAIN}" "http://127.0.0.1:8999" "MeTube 下载器"
|
||
|
||
echo ""
|
||
echo -e "${GREEN}╔══════════════════════════════════════════════════════════╗${NC}"
|
||
echo -e "${GREEN}║ ✅ MeTube 部署成功! ║${NC}"
|
||
echo -e "${GREEN}╠══════════════════════════════════════════════════════════╣${NC}"
|
||
echo -e "${GREEN}║ 访问地址: ${CYAN}https://${YTDL_DOMAIN}${NC}"
|
||
echo -e "${GREEN}║ 下载目录: ${YTDL_DOWNLOAD_DIR}${NC}"
|
||
if [ "$USE_COOKIES" = true ]; then
|
||
echo -e "${GREEN}║ Cookies: ✅ 已配置(可下载需登录的 YouTube 视频) ║${NC}"
|
||
else
|
||
echo -e "${GREEN}║ Cookies: ⚠️ 未配置(遇到Sign in报错时需要添加) ║${NC}"
|
||
echo -e "${GREEN}║ 添加方法: 上传 cookies.txt 到 /root/ytdlp_data/ ║${NC}"
|
||
echo -e "${GREEN}║ 然后执行: cd /root/ytdlp_data ║${NC}"
|
||
echo -e "${GREEN}║ 编辑 docker-compose.yml 加入 cookies 挂载 ║${NC}"
|
||
echo -e "${GREEN}║ 执行: docker compose up -d --force-recreate ║${NC}"
|
||
fi
|
||
echo -e "${GREEN}╚══════════════════════════════════════════════════════════╝${NC}"
|
||
else
|
||
echo -e "${RED}❌ MeTube 部署失败!${NC}"
|
||
fi
|
||
echo -e "\n${GREEN}按任意键返回主菜单...${NC}"; read -n 1 -s
|
||
}
|
||
|
||
# 14. Draw.io 绘图工具
|
||
install_drawio() {
|
||
ensure_docker_installed || return; check_tunnel_installed || return
|
||
_check_not_installed "/root/drawio_data" "Draw.io" || return
|
||
clear
|
||
read -p "请输入您为 Draw.io 规划的子域名 (例如 draw.example.com): " DRAWIO_DOMAIN
|
||
if [ -z "$DRAWIO_DOMAIN" ]; then echo -e "${RED}错误:域名不能为空!安装已中止。${NC}"; return 1; fi
|
||
|
||
# 【修复】移除了不再需要的用户名和密码输入
|
||
|
||
echo -e "${GREEN}正在创建 Draw.io 的配置文件...${NC}"
|
||
mkdir -p /root/drawio_data
|
||
|
||
# --- YAML 语法修正区 ---
|
||
# 注意:下面的 cat 命令使用了 <<-EOF,并且所有缩进都是空格,不是 TAB!
|
||
cat > /root/drawio_data/docker-compose.yml <<-EOF
|
||
services:
|
||
drawio:
|
||
# 【修复】使用了官方的 jgraph/drawio 镜像替换了已失效的 fuerst/draw.io-basic-auth
|
||
image: jgraph/drawio
|
||
container_name: drawio_app
|
||
restart: unless-stopped
|
||
ports:
|
||
- "127.0.0.1:8082:8080"
|
||
# 【修复】移除了不再需要的 BASIC_AUTH 环境变量
|
||
EOF
|
||
|
||
echo -e "${YELLOW}正在启动 Draw.io 服务...${NC}"
|
||
if _compose_up "Draw.io" "/root/drawio_data"; then
|
||
echo -e "${GREEN}✅ Draw.io 已启动,等待服务稳定...${NC}"; sleep 5
|
||
echo -e "\n## Draw.io 凭证 (部署于: $(date))" >> ${STATE_FILE}; echo "DRAWIO_DOMAIN=${DRAWIO_DOMAIN}" >> ${STATE_FILE}
|
||
# 【修复】移除了不再需要的用户名和密码保存
|
||
echo -e "${GREEN}正在为您配置网络...${NC}"; update_tunnel_config "${DRAWIO_DOMAIN}" "http://127.0.0.1:8082" "Draw.io" "access"
|
||
|
||
# 【修复】更新了提示信息
|
||
echo -e "\n${YELLOW}=======================【!重要!】=======================${NC}"
|
||
echo -e "${YELLOW}Draw.io 已部署成功!${NC}"
|
||
echo -e "${YELLOW}此应用已通过 Cloudflare Tunnel 的 ${CYAN}Access${YELLOW} 功能进行保护。${NC}"
|
||
echo -e "${YELLOW}您需要登录 Cloudflare Zero Trust 仪表盘, ${NC}"
|
||
echo -e "${YELLOW}在 Access -> Applications 中为 ${GREEN}${DRAWIO_DOMAIN}${YELLOW} 添加访问策略 (例如: 允许您的邮箱登录)。${NC}"
|
||
echo -e "${RED}===========================================================${NC}"
|
||
else
|
||
echo -e "${RED}❌ Draw.io 部署失败!请检查 Docker 是否正常运行。${NC}"
|
||
fi
|
||
echo -e "\n${GREEN}按任意键返回主菜单...${NC}"; read -n 1 -s
|
||
}
|
||
|
||
# 15. 远程工作台
|
||
install_desktop_env() {
|
||
ensure_docker_installed || return; check_tunnel_installed || return
|
||
clear
|
||
echo -e "${BLUE}--- “远程工作台”建造计划启动! ---${NC}";
|
||
export DEBIAN_FRONTEND=noninteractive
|
||
sudo apt-get update
|
||
sudo apt-get install -y xfce4 xfce4-goodies xrdp
|
||
if [ -f /etc/xrdp/sesman.ini ]; then
|
||
sudo sed -i 's/AllowRootLogin=true/AllowRootLogin=false/g' /etc/xrdp/sesman.ini
|
||
fi
|
||
sudo systemctl enable --now xrdp
|
||
echo xfce4-session > ~/.xsession
|
||
sudo adduser xrdp ssl-cert
|
||
sudo systemctl restart xrdp
|
||
|
||
read -p "请输入您想创建的远程桌面用户名 (例如 admin): " NEW_USER
|
||
if [ -z "$NEW_USER" ]; then echo -e "${RED}用户名不能为空,操作取消。${NC}"; sleep 2; return; fi
|
||
sudo adduser --gecos "" "$NEW_USER"
|
||
echo -e "${YELLOW}请为新账户 '$NEW_USER' 设置登录密码...${NC}"
|
||
sudo passwd "$NEW_USER"
|
||
|
||
echo -e "\n## 远程工作台 (Xfce) 凭证 (部署于: $(date))" >> ${STATE_FILE}
|
||
echo "RDP_USER=${NEW_USER}" >> ${STATE_FILE}
|
||
|
||
echo -e "\n${GREEN}✅ 远程工作台建造及隧道配置完毕!${NC}"
|
||
echo -e "\n${YELLOW}--- 如何连接 ---${NC}"
|
||
echo -e " 1. 打开您本地的远程桌面客户端 (Windows/Mac/手机)。"
|
||
echo -e " 2. 在计算机/服务器地址栏输入: ${CYAN}\$(curl -s4 https://ifconfig.me/ip)${NC}"
|
||
echo -e " 3. (如果提示端口,请输入: ${CYAN}3389${NC})"
|
||
echo -e " 4. 使用您刚才创建的用户名 ${GREEN}${NEW_USER}${NC} 和密码登录。"
|
||
echo -e " 5. ${RED}重要: 您的 3389 端口已公网暴露,请确保已部署 Fail2ban (选项 18)!${NC}"
|
||
echo -e "\n${GREEN}按任意键返回主菜单...${NC}"; read -n 1 -s
|
||
}
|
||
|
||
# 16. 为远程桌面安装中文字体
|
||
install_chinese_fonts() {
|
||
clear
|
||
echo -e "${BLUE}--- 为远程桌面安装中文字体 ---${NC}"
|
||
if [ ! -f "/etc/xrdp/xrdp.ini" ]; then
|
||
echo -e "${RED}错误:此功能依赖“远程工作台”,请先执行选项 15 进行安装。${NC}"; sleep 4; return 1
|
||
fi
|
||
echo -e "${YELLOW}此操作将为您的远程桌面环境安装文泉驿等常用中文字体。${NC}"
|
||
read -p "您确定要安装吗? (y/n): " confirm
|
||
if [[ "$confirm" == "y" || "$confirm" == "Y" ]]; then
|
||
echo -e "\n${CYAN}[1/3] 正在更新软件包列表...${NC}"; sudo apt-get update > /dev/null 2>&1
|
||
echo -e "\n${CYAN}[2/3] 正在安装中文字体包...${NC}"; sudo apt-get install -y fonts-wqy-microhei fonts-wqy-zenhei fonts-arphic-ukai fonts-arphic-uming
|
||
echo -e "\n${CYAN}[3/3] 正在刷新系统字体缓存...${NC}"; sudo fc-cache -fv
|
||
echo -e "\n${YELLOW}══════════════════════════════════════════════════════════════════════"
|
||
echo -e "✅ 中文字体安装完成!"
|
||
echo -e "================================================${NC}"
|
||
echo -e "${YELLOW}为了使字体完全生效,请注销后重新登录您的远程桌面会话。${NC}"
|
||
else
|
||
echo -e "${GREEN}操作已取消。${NC}"
|
||
fi
|
||
echo -e "\n${GREEN}按任意键返回主菜单...${NC}"; read -n 1 -s
|
||
}
|
||
|
||
_install_desktop_browser() {
|
||
echo -e "\n${CYAN}--- 步驟 2: 為遠程桌面安裝瀏覽器 ---${NC}"
|
||
if [ ! -f "/etc/xrdp/xrdp.ini" ]; then echo -e "${RED}錯誤:遠程工作台未安裝,無法安裝瀏覽器。${NC}"; sleep 3; return; fi
|
||
|
||
local ARCH; ARCH=$(dpkg --print-architecture)
|
||
echo -e "${GREEN}CPU 架構: ${ARCH} | 系統: ${SYS_DISTRO} ${SYS_CODENAME}${NC}"
|
||
sudo apt-get update > /dev/null 2>&1
|
||
|
||
# 探测当前系统实际可用的 chromium 包名(排除 snap 占位符)
|
||
_detect_chromium_pkg() {
|
||
for pkg in chromium chromium-browser google-chrome-stable; do
|
||
if apt-cache show "$pkg" 2>/dev/null | grep -q "^Package:"; then
|
||
local desc
|
||
desc=$(apt-cache show "$pkg" 2>/dev/null | grep "^Description" | head -1)
|
||
if ! echo "$desc" | grep -qi "snap"; then
|
||
echo "$pkg"; return
|
||
fi
|
||
fi
|
||
done
|
||
echo ""
|
||
}
|
||
|
||
# Firefox-ESR:优先 apt,Ubuntu 22.04+ snap 占位时自动切换 Mozilla PPA
|
||
_install_firefox() {
|
||
echo -e "${YELLOW}正在安裝 Firefox-ESR...${NC}"
|
||
if apt-cache show firefox-esr 2>/dev/null | grep -q "^Package:"; then
|
||
sudo apt-get install -y firefox-esr && return 0
|
||
fi
|
||
echo -e "${YELLOW}系統自帶 Firefox 是 snap 版,嘗試從 Mozilla PPA 安裝原生版...${NC}"
|
||
sudo apt-get install -y software-properties-common > /dev/null 2>&1
|
||
sudo add-apt-repository -y ppa:mozillateam/ppa > /dev/null 2>&1
|
||
printf "Package: firefox*\nPin: release o=LP-PPA-mozillateam\nPin-Priority: 1001\n" \
|
||
| sudo tee /etc/apt/preferences.d/mozilla-firefox > /dev/null
|
||
sudo apt-get update > /dev/null 2>&1
|
||
sudo apt-get install -y firefox-esr
|
||
}
|
||
|
||
# Chromium:apt 多包名回退 → Google Chrome 官方 deb(仅 amd64)
|
||
_install_chromium() {
|
||
local pkg; pkg=$(_detect_chromium_pkg)
|
||
if [ -n "$pkg" ]; then
|
||
echo -e "${YELLOW}正在安裝 ${pkg}...${NC}"
|
||
sudo apt-get install -y "$pkg" && return 0
|
||
fi
|
||
echo -e "${YELLOW}apt 中未找到可用 Chromium,嘗試安裝 Google Chrome Stable...${NC}"
|
||
if [ "$ARCH" = "amd64" ]; then
|
||
local chrome_deb="/tmp/google-chrome.deb"
|
||
wget -q --show-progress -O "$chrome_deb" \
|
||
"https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb" \
|
||
&& sudo apt-get install -y "$chrome_deb" && rm -f "$chrome_deb" && return 0
|
||
fi
|
||
echo -e "${RED}❌ 當前架構 (${ARCH}) 無法自動安裝 Chrome,請手動處理。${NC}"
|
||
return 1
|
||
}
|
||
|
||
echo ""
|
||
echo -e " ${YELLOW}1)${NC} Firefox-ESR (推薦,輕量穩定)"
|
||
echo -e " ${YELLOW}2)${NC} Chromium / Google Chrome (自動適配來源)"
|
||
echo -e " ${YELLOW}其他)${NC} 取消"
|
||
read -p "請選擇: " choice
|
||
|
||
local ok=false
|
||
case "$choice" in
|
||
1) _install_firefox && ok=true ;;
|
||
2) _install_chromium && ok=true ;;
|
||
*) echo -e "${GREEN}操作已取消。${NC}"; read -p "按 Enter 鍵繼續..."; return ;;
|
||
esac
|
||
|
||
if $ok; then
|
||
echo -e "\n${GREEN}✅ 瀏覽器安裝成功!${NC}"
|
||
echo -e "${YELLOW}提示:登入遠程桌面後可在終端執行 exo-preferred-applications 設為默認瀏覽器。${NC}"
|
||
else
|
||
echo -e "\n${RED}❌ 瀏覽器安裝失敗,請查看上方錯誤信息。${NC}"
|
||
echo -e "${YELLOW}提示:也可先執行菜單 m) 恢復標準系統後重試。${NC}"
|
||
fi
|
||
read -p "按 Enter 鍵繼續..."
|
||
}
|
||
# 17. 安装 AI 知识库
|
||
install_ai_model() {
|
||
ensure_docker_installed || return
|
||
if [ ! -d "/root/ai_stack" ]; then echo -e "${RED}错误:AI 大脑未安装!${NC}"; sleep 3; return; fi
|
||
clear
|
||
echo -e "${BLUE}--- 为 AI 大脑安装知识库 (安装大语言模型) ---${NC}"
|
||
echo " 1) qwen:1.8b (阿里通义千问), 2) gemma:2b (Google), 3) tinyllama (极限轻量)"
|
||
echo " 4) llama3:8b (Meta, 推荐), 5) qwen:4b (更强中文), 6) phi3 (微软)"
|
||
echo " 7) qwen:14b (准专业级), 8) llama3:70b (性能怪兽)"
|
||
read -p "请输入您的选择: " model_choice
|
||
local model_name=""
|
||
case $model_choice in
|
||
1) model_name="qwen:1.8b";; 2) model_name="gemma:2b";; 3) model_name="tinyllama";;
|
||
4) model_name="llama3:8b";; 5) model_name="qwen:4b";; 6) model_name="phi3";;
|
||
7) model_name="qwen:14b";; 8) model_name="llama3:70b";;
|
||
*) echo -e "${RED}无效选择!${NC}"; sleep 2; return;;
|
||
esac
|
||
echo -e "\n${YELLOW}即将开始下载模型: ${model_name},这可能需要一些时间,请耐心等待...${NC}"
|
||
sudo docker exec ollama ollama pull ${model_name}
|
||
echo -e "\n${GREEN}✅ 知识库 ${model_name} 安装完成!${NC}"
|
||
echo -e "\n${GREEN}按任意键返回主菜单...${NC}"; read -n 1 -s
|
||
}
|
||
|
||
# 18. 部署 Fail2ban (防暴力破解)
|
||
install_fail2ban() {
|
||
clear
|
||
echo -e "${BLUE}--- “Fail2ban 安防系统”部署计划启动! ---${NC}"
|
||
echo -e "${YELLOW}即将安装 Fail2ban 并自动配置 SSH 与 RDP 防护...${NC}"
|
||
|
||
sudo apt-get update >/dev/null 2>&1
|
||
sudo apt-get install -y fail2ban
|
||
sudo systemctl enable --now fail2ban
|
||
|
||
echo -e "${GREEN}✅ Fail2ban 已安装并启动。${NC}"
|
||
echo -e "${CYAN}正在配置 jail.local (设置 SSH 和 RDP 规则)...${NC}"
|
||
|
||
# --- 创建 jail.local 配置文件 ---
|
||
sudo tee /etc/fail2ban/jail.local > /dev/null <<'EOF'
|
||
[DEFAULT]
|
||
bantime = 3600
|
||
findtime = 600
|
||
maxretry = 3
|
||
|
||
[sshd]
|
||
enabled = true
|
||
|
||
[xrdp]
|
||
enabled = true
|
||
port = 3389
|
||
logpath = /var/log/xrdp.log
|
||
EOF
|
||
|
||
echo -e "${CYAN}正在配置 xrdp.conf (设置过滤器)...${NC}"
|
||
|
||
# --- 创建 xrdp 过滤器文件 ---
|
||
sudo tee /etc/fail2ban/filter.d/xrdp.conf > /dev/null <<'EOF'
|
||
[Definition]
|
||
failregex = ^.*\[WARN \].*A connection problem has occurred, login is denied.*$
|
||
EOF
|
||
|
||
echo -e "${CYAN}正在创建 RDP 日志文件...${NC}"
|
||
sudo touch /var/log/xrdp.log
|
||
|
||
echo -e "${YELLOW}正在重启 Fail2ban 以应用所有配置...${NC}"
|
||
sudo systemctl restart fail2ban
|
||
sleep 2
|
||
|
||
echo -e "\n${GREEN}--- 部署完成!正在检查 [xrdp] 规则状态 ---${NC}"
|
||
sudo fail2ban-client status xrdp
|
||
|
||
echo -e "\n${GREEN}✅ Fail2ban 已成功加载!您的 SSH 和 RDP 已受到保护。${NC}"
|
||
echo -e "\n${GREEN}按任意键返回主菜单...${NC}"; read -n 1 -s
|
||
}
|
||
|
||
# 19. 部署 Uptime Kuma 监控面板
|
||
install_uptime_kuma() {
|
||
ensure_docker_installed || return; check_tunnel_installed || return
|
||
_check_not_installed "/root/uptime_kuma_data" "Uptime Kuma" || return
|
||
clear
|
||
read -p "请输入您为 Uptime Kuma 规划的子域名 (例如 status.example.com): " KUMA_DOMAIN
|
||
if [ -z "$KUMA_DOMAIN" ]; then echo -e "${RED}错误:域名不能为空!${NC}"; sleep 2; return; fi
|
||
|
||
echo -e "${BLUE}--- “Uptime Kuma 监控面板”部署计划启动! ---${NC}";
|
||
mkdir -p /root/uptime_kuma_data
|
||
|
||
# --- YAML 语法修正区 ---
|
||
# 注意:下面的 cat 命令使用了 <<-EOF,并且所有缩进都是空格,不是 TAB!
|
||
# 端口 3001 已被 AI (Option 5) 占用,此处使用 3002
|
||
cat > /root/uptime_kuma_data/docker-compose.yml <<-EOF
|
||
services:
|
||
uptime-kuma:
|
||
image: louislam/uptime-kuma:1
|
||
container_name: uptime_kuma_app
|
||
restart: always
|
||
ports:
|
||
- "127.0.0.1:3002:3001"
|
||
volumes:
|
||
- './data:/app/data'
|
||
environment:
|
||
- 'TZ=Asia/Shanghai'
|
||
EOF
|
||
|
||
echo -e "${YELLOW}正在启动 Uptime Kuma 服务...${NC}"
|
||
if _compose_up "Uptime Kuma" "/root/uptime_kuma_data"; then
|
||
echo -e "${GREEN}✅ Uptime Kuma 已启动,等待服务稳定...${NC}"; sleep 5
|
||
echo -e "\n## Uptime Kuma 凭证 (部署于: $(date))" >> ${STATE_FILE};
|
||
echo "KUMA_DOMAIN=${KUMA_DOMAIN}" >> ${STATE_FILE}
|
||
echo -e "${GREEN}正在为您配置网络...${NC}";
|
||
# 注意:这里 service_url 必须指向本地端口 3002
|
||
update_tunnel_config "${KUMA_DOMAIN}" "http://127.0.0.1:3002" "Uptime Kuma"
|
||
else
|
||
echo -e "${RED}❌ Uptime Kuma 部署失败!请检查 Docker 是否正常运行。${NC}"
|
||
fi
|
||
echo -e "\n${GREEN}按任意键返回主菜单...${NC}"; read -n 1 -s
|
||
}
|
||
|
||
# 19.1 部署 Glances 实时资源监控
|
||
install_glances() {
|
||
ensure_docker_installed || return; check_tunnel_installed || return; clear
|
||
read -p "请输入您为 Glances 规划的子域名 (例如 glances.example.com): " GLANCES_DOMAIN
|
||
if [ -z "$GLANCES_DOMAIN" ]; then echo -e "${RED}错误:域名不能为空!${NC}"; sleep 2; return; fi
|
||
|
||
echo -e "${BLUE}--- “Glances 实时资源监控”部署计划启动! ---${NC}"
|
||
echo -e "${YELLOW}正在拉取镜像并启动 Web 模式...${NC}"
|
||
|
||
# 使用 Docker 直接运行,开启 -w 网页模式,并绑定本地 61208 端口
|
||
docker run -d \
|
||
--name glances_app \
|
||
--restart always \
|
||
-p 127.0.0.1:61208:61208 \
|
||
-v /var/run/docker.sock:/var/run/docker.sock:ro \
|
||
-v /:/host:ro \
|
||
--pid host \
|
||
nicolargo/glances:latest-full \
|
||
/usr/local/bin/glances -w
|
||
|
||
if [ $? -eq 0 ]; then
|
||
echo -e "${GREEN}✅ Glances 已启动!${NC}"
|
||
echo -e "\n## Glances 监控凭证 (部署于: $(date))" >> ${STATE_FILE}
|
||
echo "GLANCES_DOMAIN=${GLANCES_DOMAIN}" >> ${STATE_FILE}
|
||
echo -e "${GREEN}正在为您配置网络隧道...${NC}"
|
||
# 关联 Cloudflare Tunnel
|
||
update_tunnel_config "${GLANCES_DOMAIN}" "http://127.0.0.1:61208" "Glances Monitor"
|
||
else
|
||
echo -e "${RED}❌ Glances 部署失败!${NC}"
|
||
fi
|
||
echo -e "\n${GREEN}按任意键返回主菜单...${NC}"; read -n 1 -s
|
||
}
|
||
|
||
# 20. 添加 SSH 密钥
|
||
add_ssh_public_key() {
|
||
clear
|
||
echo -e "${BLUE}--- 添加 SSH 公钥 (密钥登录) ---${NC}"
|
||
echo -e "${YELLOW}此功能将允许您通过粘贴“公钥”来实现密钥登录。${NC}"
|
||
echo -e "${RED}警告:请确保您粘贴的是“公钥” (例如 id_rsa.pub 的内容),而不是“私钥”!${NC}"
|
||
|
||
read -p "请粘贴您的 SSH 公钥内容 (通常以 ssh-rsa, ssh-ed25519 等开头): " public_key
|
||
|
||
if [ -z "$public_key" ]; then
|
||
echo -e "${RED}输入为空,操作已取消。${NC}"; sleep 3; return
|
||
fi
|
||
|
||
if [[ ! "$public_key" == "ssh-"* ]]; then
|
||
echo -e "${RED}错误:公钥格式不正确。它应该以 'ssh-' 开头。${NC}"; sleep 3; return
|
||
fi
|
||
|
||
read -p "您想为哪个用户配置此密钥? [默认为 root]: " target_user
|
||
target_user=${target_user:-root}
|
||
|
||
if ! id "$target_user" &>/dev/null; then
|
||
echo -e "${RED}错误:用户 ${target_user} 不存在于系统中。${NC}"; sleep 3; return
|
||
fi
|
||
|
||
local target_home
|
||
target_home=$(getent passwd "$target_user" | cut -d: -f6)
|
||
if [ -z "$target_home" ]; then
|
||
echo -e "${RED}错误:无法找到用户 ${target_user} 的主目录。${NC}"; sleep 3; return
|
||
fi
|
||
|
||
local ssh_dir="${target_home}/.ssh"
|
||
local auth_keys_file="${ssh_dir}/authorized_keys"
|
||
|
||
echo -e "${YELLOW}正在为用户 ${target_user} 配置密钥 (路径: ${auth_keys_file})...${NC}"
|
||
|
||
# 创建目录和文件
|
||
mkdir -p "$ssh_dir"
|
||
|
||
# 检查密钥是否已存在
|
||
if grep -qF "$public_key" "$auth_keys_file" 2>/dev/null; then
|
||
echo -e "${YELLOW}此密钥已存在于 ${auth_keys_file} 中,无需重复添加。${NC}"
|
||
else
|
||
# 追加密钥
|
||
echo "$public_key" >> "$auth_keys_file"
|
||
echo -e "${GREEN}✅ 密钥已成功追加。${NC}"
|
||
fi
|
||
|
||
# 设置严格的权限
|
||
echo -e "${YELLOW}正在设置安全权限...${NC}"
|
||
chmod 700 "$ssh_dir"
|
||
chmod 600 "$auth_keys_file"
|
||
|
||
# 设置正确的所有者
|
||
if [ "$(stat -c '%U' "$ssh_dir")" != "$target_user" ]; then
|
||
chown -R "${target_user}:${target_user}" "$ssh_dir"
|
||
echo -e "${GREEN}✅ 目录所有者已修正为 ${target_user}。${NC}"
|
||
fi
|
||
|
||
echo -e "\n${YELLOW}══════════════════════════════════════════════════════════════════════${NC}"
|
||
echo -e "✅ SSH 密钥添加成功!"
|
||
echo -e "${YELLOW}请尝试在新终端中使用您的“私钥”登录。"
|
||
echo -e "${YELLOW}测试成功后,强烈建议您执行主菜单中的“切换 SSH 密码登录”选项。${NC}"
|
||
echo -e "${YELLOW}══════════════════════════════════════════════════════════════════════${NC}"
|
||
|
||
echo -e "\n${GREEN}按任意键返回主菜单...${NC}"; read -n 1 -s
|
||
}
|
||
|
||
# 21. 切换 SSH 密码登录
|
||
toggle_ssh_password_login() {
|
||
clear
|
||
echo -e "${BLUE}--- 切换 SSH 密码登录状态 ---${NC}"
|
||
|
||
local config_file="/etc/ssh/sshd_config"
|
||
local current_status
|
||
current_status=$(grep -E "^\s*#*\s*PasswordAuthentication" "$config_file" | tail -n 1 | awk '{print $2}')
|
||
|
||
if [[ "$current_status" == "yes" || -z "$current_status" ]]; then
|
||
echo -e "${YELLOW}当前状态:允许密码登录 (PasswordAuthentication is 'yes' or commented out)。${NC}"
|
||
read -p "您确定要【禁用】密码登录吗? (y/n): " confirm
|
||
if [[ "$confirm" == "y" || "$confirm" == "Y" ]]; then
|
||
echo -e "${YELLOW}正在禁用密码登录 (设置 PasswordAuthentication no)...${NC}"
|
||
sudo sed -i 's/^[ \t]*#*[ \t]*PasswordAuthentication .*/PasswordAuthentication no/g' "$config_file"
|
||
sudo sed -i 's/^[ \t]*#*[ \t]*ChallengeResponseAuthentication .*/ChallengeResponseAuthentication no/g' "$config_file"
|
||
echo -e "${YELLOW}正在重启 SSH 服务...${NC}"
|
||
sudo systemctl restart sshd
|
||
echo -e "\n${RED}================================================================${NC}"
|
||
echo -e "✅ 密码登录已禁用!请确保您的密钥可以正常登录!"
|
||
echo -e "${RED}如果密钥失效,您可能无法再次登录此服务器!${NC}"
|
||
echo -e "${RED}================================================================${NC}"
|
||
else
|
||
echo -e "${GREEN}操作已取消。${NC}"
|
||
fi
|
||
else
|
||
echo -e "${GREEN}当前状态:密码登录已被禁用 (PasswordAuthentication is 'no')。${NC}"
|
||
read -p "您确定要【启用】密码登录吗? (y/n): " confirm
|
||
if [[ "$confirm" == "y" || "$confirm" == "Y" ]]; then
|
||
echo -e "${YELLOW}正在启用密码登录 (设置 PasswordAuthentication yes)...${NC}"
|
||
sudo sed -i 's/^[ \t]*#*[ \t]*PasswordAuthentication .*/PasswordAuthentication yes/g' "$config_file"
|
||
echo -e "${YELLOW}正在重启 SSH 服务...${NC}"
|
||
sudo systemctl restart sshd
|
||
echo -e "\n${GREEN}✅ 密码登录已重新启用。${NC}"
|
||
else
|
||
echo -e "${GREEN}操作已取消。${NC}"
|
||
fi
|
||
fi
|
||
|
||
echo -e "\n${GREEN}按任意键返回主菜单...${NC}"; read -n 1 -s
|
||
}
|
||
|
||
# 22. Nextcloud 优化
|
||
run_nextcloud_optimization() {
|
||
ensure_docker_installed || return
|
||
if [ ! -d "/root/nextcloud_data" ]; then echo -e "${RED}错误:Nextcloud 套件未安装!${NC}"; sleep 3; return; fi
|
||
clear
|
||
echo -e "${BLUE}--- “Nextcloud 精装修”计划启动! ---${NC}";
|
||
local nc_domain=$(grep 'NEXTCLOUD_DOMAIN' ${STATE_FILE} | cut -d'=' -f2)
|
||
if [ -z "$nc_domain" ]; then echo -e "${RED}错误: 无法从凭证文件找到 Nextcloud 域名!${NC}"; sleep 3; return; fi
|
||
sudo docker exec --user www-data nextcloud_app php occ config:system:set trusted_proxies 0 --value='172.17.0.0/16'
|
||
sudo docker exec --user www-data nextcloud_app php occ config:system:set overwrite.cli.url --value="https://${nc_domain}"
|
||
sudo docker exec --user www-data nextcloud_app php occ config:system:set overwriteprotocol --value='https'
|
||
sudo docker exec --user www-data nextcloud_app php occ config:system:set memcache.local --value '\\OC\\Memcache\\Redis'
|
||
sudo docker exec --user www-data nextcloud_app php occ config:system:set memcache.locking --value '\\OC\\Memcache\\Redis'
|
||
sudo docker exec --user www-data nextcloud_app php occ config:system:set redis host --value 'nextcloud_redis'
|
||
sudo docker exec --user www-data nextcloud_app php occ db:add-missing-indices
|
||
sudo docker exec --user www-data nextcloud_app php occ maintenance:repair --include-expensive
|
||
sudo docker exec --user www-data nextcloud_app php occ config:system:set maintenance_window_start --type=integer --value=1
|
||
sudo docker exec --user www-data nextcloud_app php occ config:system:set default_phone_region --value="CN"
|
||
echo -e "\n${GREEN}✅ Nextcloud 精装修完成!${NC}"
|
||
echo -e "\n${GREEN}按任意键返回主菜单...${NC}"; read -n 1 -s
|
||
}
|
||
# 22.1 Nextcloud 高速风景版 (视听封面 + BBR + 大文件分块)
|
||
run_nextcloud_super_boost() {
|
||
ensure_docker_installed || return
|
||
if [ ! -d "/root/nextcloud_data" ]; then
|
||
echo -e "${RED}错误:Nextcloud 套件未安装! 请先执行 3.1 部署。${NC}"
|
||
sleep 3
|
||
return
|
||
fi
|
||
clear
|
||
echo -e "${BLUE}--- “Nextcloud 高速风景版” 深度优化计划启动! ---${NC}"
|
||
|
||
echo -e "\n${CYAN}[1/5] 正在强制 IPv4 优先 (解决甲骨文等机器的 IPv6 绕路/断连问题)...${NC}"
|
||
sudo sed -i 's/#precedence ::ffff:0:0\/96 100/precedence ::ffff:0:0\/96 100/' /etc/gai.conf
|
||
grep -q "precedence ::ffff:0:0/96 100" /etc/gai.conf || echo "precedence ::ffff:0:0/96 100" | sudo tee -a /etc/gai.conf >/dev/null
|
||
|
||
echo -e "\n${CYAN}[2/5] 正在进行内核网络调优 (开启 BBR 加速 + 扩展缓冲区)...${NC}"
|
||
sudo cat > /tmp/sysctl_nc_boost.conf << EOB
|
||
net.ipv4.ip_forward = 1
|
||
net.core.default_qdisc=fq
|
||
net.ipv4.tcp_congestion_control=bbr
|
||
net.core.rmem_max = 67108864
|
||
net.core.wmem_max = 67108864
|
||
net.ipv4.tcp_rmem = 4096 87380 67108864
|
||
net.ipv4.tcp_wmem = 4096 65536 67108864
|
||
net.core.netdev_max_backlog = 250000
|
||
net.ipv4.tcp_max_syn_backlog = 30000
|
||
net.ipv4.tcp_max_tw_buckets = 2000000
|
||
net.ipv4.tcp_tw_reuse = 1
|
||
net.ipv4.tcp_fin_timeout = 10
|
||
vm.swappiness = 10
|
||
EOB
|
||
sudo cat /tmp/sysctl_nc_boost.conf | sudo tee -a /etc/sysctl.conf >/dev/null
|
||
sudo sysctl -p >/dev/null 2>&1
|
||
rm -f /tmp/sysctl_nc_boost.conf
|
||
|
||
echo -e "\n${CYAN}[3/5] 正在安装多核调度器 (irqbalance)...${NC}"
|
||
export DEBIAN_FRONTEND=noninteractive
|
||
sudo apt-get update >/dev/null 2>&1
|
||
sudo apt-get install irqbalance -y >/dev/null 2>&1
|
||
sudo systemctl enable --now irqbalance >/dev/null 2>&1
|
||
unset DEBIAN_FRONTEND
|
||
|
||
echo -e "\n${CYAN}[4/5] 正在为 Nextcloud 容器内部安装视频解码器 (ffmpeg)...${NC}"
|
||
sudo docker exec -u root nextcloud_app apt-get update >/dev/null 2>&1
|
||
sudo docker exec -u root nextcloud_app apt-get install -y ffmpeg >/dev/null 2>&1
|
||
|
||
echo -e "\n${CYAN}[5/5] 正在注入视听封面引擎与大文件切片配置...${NC}"
|
||
sudo docker exec --user www-data nextcloud_app php occ config:system:set enable_previews --value=true --type=boolean
|
||
# 限制预览图最大尺寸(256px 节省资源,缩略图完全够用)
|
||
sudo docker exec --user www-data nextcloud_app php occ config:app:set preview max_x --value 256
|
||
sudo docker exec --user www-data nextcloud_app php occ config:app:set preview max_y --value 256
|
||
sudo docker exec --user www-data nextcloud_app php occ config:app:set preview jpeg_quality --value 50
|
||
sudo docker exec --user www-data nextcloud_app php occ config:system:set enabledPreviewProviders 0 --value="OC\Preview\Image"
|
||
sudo docker exec --user www-data nextcloud_app php occ config:system:set enabledPreviewProviders 1 --value="OC\Preview\Movie"
|
||
sudo docker exec --user www-data nextcloud_app php occ config:system:set enabledPreviewProviders 2 --value="OC\Preview\MP3"
|
||
sudo docker exec --user www-data nextcloud_app php occ config:system:set enabledPreviewProviders 3 --value="OC\Preview\TXT"
|
||
sudo docker exec --user www-data nextcloud_app php occ config:system:set enabledPreviewProviders 4 --value="OC\Preview\MarkDown"
|
||
sudo docker exec --user www-data nextcloud_app php occ config:app:set files max_chunk_size --value 524288000
|
||
|
||
echo -e "\n${GREEN}✅ 基础配置全部注入完成!新上传的视听文件将自动生成封面。${NC}"
|
||
|
||
# --- 历史文件缩略图生成模块 ---
|
||
echo -e "\n${YELLOW}================================================================${NC}"
|
||
echo -e "${YELLOW}检测到您可能需要为以前上传的“老文件”补齐缩略图。${NC}"
|
||
echo -e "${YELLOW}此操作需要安装官方的 Preview Generator 插件,并扫描整个网盘。${NC}"
|
||
echo -e "${RED}注意:如果您的网盘文件非常多,这一步可能会花费十几分钟甚至更久!${NC}"
|
||
echo -e "${YELLOW}================================================================${NC}"
|
||
read -p "您想现在立刻为所有历史文件生成封面吗?(y/n): " gen_choice
|
||
|
||
if [[ "$gen_choice" == "y" || "$gen_choice" == "Y" ]]; then
|
||
echo -e "\n${CYAN}正在下载并安装 Preview Generator 插件...${NC}"
|
||
sudo docker exec --user www-data nextcloud_app php occ app:install previewgenerator
|
||
echo -e "${CYAN}正在全盘扫描并生成历史缩略图,请耐心等待 (切勿关闭终端窗口)...${NC}"
|
||
sudo docker exec --user www-data nextcloud_app php occ preview:generate-all
|
||
echo -e "${GREEN}✅ 历史文件缩略图全盘生成完毕!${NC}"
|
||
else
|
||
echo -e "\n${GREEN}已跳过历史文件生成。您以后可以随时重新运行此选项来补齐封面。${NC}"
|
||
fi
|
||
|
||
echo -e "\n${GREEN}按任意键返回主菜单...${NC}"
|
||
read -n 1 -s
|
||
}
|
||
|
||
# 22.2 Nextcloud 外部存储管理(一键重挂/扫描)
|
||
manage_nextcloud_storage() {
|
||
clear
|
||
echo -e "${BLUE}=== 🗄️ Nextcloud 外部存储管理 ===${NC}"
|
||
echo ""
|
||
|
||
if ! docker ps 2>/dev/null | grep -q "nextcloud_app"; then
|
||
echo -e "${RED}❌ Nextcloud 未运行!${NC}"; sleep 2; return
|
||
fi
|
||
|
||
echo -e "${YELLOW}当前外部存储列表:${NC}"
|
||
docker exec -u www-data nextcloud_app php occ files_external:list 2>/dev/null
|
||
echo ""
|
||
echo -e " ${YELLOW}1)${NC} 🔄 重新挂载所有 SSHFS 并重建 Nextcloud 容器(重启后必做)"
|
||
echo -e " ${YELLOW}2)${NC} 🔍 扫描所有外部存储文件"
|
||
echo -e " ${YELLOW}3)${NC} 🔍 扫描指定外部存储(输入路径)"
|
||
echo -e " ${YELLOW}4)${NC} 🔒 清除文件锁(扫描报 is locked 时使用)"
|
||
echo -e " ${YELLOW}5)${NC} 📋 查看所有 SSHFS 挂载状态"
|
||
echo -e " ${YELLOW}6)${NC} ➕ 添加新的 SSHFS 外部存储"
|
||
echo -e " ${YELLOW}7)${NC} 🗑️ 删除指定外部存储"
|
||
echo -e " ${YELLOW}q)${NC} 返回主菜单"
|
||
echo ""
|
||
read -p "请选择: " sc
|
||
|
||
case $sc in
|
||
1)
|
||
echo -e "${YELLOW}正在重新挂载所有 SSHFS...${NC}"
|
||
# 启动所有未运行的 sshfs systemd 服务
|
||
for svc in $(systemctl list-units --type=service --state=inactive,failed 2>/dev/null | grep "^mnt-" | awk '{print $1}'); do
|
||
echo -e " 挂载: ${svc}"
|
||
sudo systemctl start "$svc" 2>/dev/null
|
||
sleep 2
|
||
done
|
||
# 列出已运行的挂载
|
||
for svc in $(systemctl list-units --type=service --state=active 2>/dev/null | grep "^mnt-" | awk '{print $1}'); do
|
||
echo -e " ${GREEN}✅ 已运行: ${svc}${NC}"
|
||
done
|
||
echo ""
|
||
echo -e "${YELLOW}正在重建 Nextcloud 容器(让容器读取最新 FUSE/SSHFS 挂载)...${NC}"
|
||
cd /root/nextcloud_data && docker compose down --remove-orphans && docker compose up -d
|
||
echo -e "${YELLOW}等待 Nextcloud 启动 (30s)...${NC}"; sleep 30
|
||
|
||
echo -e "${YELLOW}正在清除文件锁(防止扫描报错)...${NC}"
|
||
docker exec -u www-data nextcloud_app php occ maintenance:mode --on 2>/dev/null
|
||
# 用 mariadb 命令清锁(mariadb 镜像内置)
|
||
local _DB_PASS
|
||
_DB_PASS=$(grep "MYSQL_PASSWORD" /root/nextcloud_data/docker-compose.yml 2>/dev/null | head -1 | sed 's/.*MYSQL_PASSWORD: //' | tr -d ' ')
|
||
if [ -n "$_DB_PASS" ]; then
|
||
docker exec nextcloud_db mariadb -u nextclouduser -p"${_DB_PASS}" nextclouddb \
|
||
-e "DELETE FROM oc_file_locks WHERE 1;" 2>/dev/null \
|
||
&& echo -e "${GREEN} ✅ 文件锁已清除${NC}" \
|
||
|| echo -e "${YELLOW} ⚠️ 清锁失败(可能影响扫描,但不影响数据)${NC}"
|
||
fi
|
||
docker exec -u www-data nextcloud_app php occ maintenance:mode --off 2>/dev/null
|
||
|
||
echo -e "${YELLOW}扫描外部存储文件...${NC}"
|
||
docker exec -u www-data nextcloud_app php occ files:scan --all --quiet 2>/dev/null
|
||
echo -e "${GREEN}✅ 完成!刷新 Nextcloud 页面查看文件。${NC}"
|
||
;;
|
||
2)
|
||
echo -e "${YELLOW}正在扫描所有外部存储...${NC}"
|
||
docker exec -u www-data nextcloud_app php occ files:scan --all
|
||
;;
|
||
3)
|
||
read -p "请输入用户名 (默认: $(docker exec -u www-data nextcloud_app php occ user:list 2>/dev/null | head -1 | tr -d ' -')): " NC_USER
|
||
[ -z "$NC_USER" ] && NC_USER=$(docker exec -u www-data nextcloud_app php occ user:list 2>/dev/null | head -1 | tr -d ' -')
|
||
read -p "请输入存储名称 (例如 TOKYO): " STORE_NAME
|
||
docker exec -u www-data nextcloud_app php occ files:scan --path="/${NC_USER}/files/${STORE_NAME}"
|
||
;;
|
||
4)
|
||
echo -e "${YELLOW}正在清除 Nextcloud 文件锁...${NC}"
|
||
docker exec -u www-data nextcloud_app php occ maintenance:mode --on 2>/dev/null
|
||
local _DB_PASS
|
||
_DB_PASS=$(grep "MYSQL_PASSWORD" /root/nextcloud_data/docker-compose.yml 2>/dev/null | head -1 | sed 's/.*MYSQL_PASSWORD: //' | tr -d ' ')
|
||
if [ -n "$_DB_PASS" ]; then
|
||
docker exec nextcloud_db mariadb -u nextclouduser -p"${_DB_PASS}" nextclouddb \
|
||
-e "DELETE FROM oc_file_locks WHERE 1;" 2>/dev/null \
|
||
&& echo -e "${GREEN}✅ 文件锁已清除!${NC}" \
|
||
|| echo -e "${RED}❌ 清锁失败,请检查数据库容器状态${NC}"
|
||
else
|
||
echo -e "${RED}❌ 无法读取数据库密码,请检查 /root/nextcloud_data/docker-compose.yml${NC}"
|
||
fi
|
||
docker exec -u www-data nextcloud_app php occ maintenance:mode --off 2>/dev/null
|
||
echo -e "${YELLOW}重新扫描文件...${NC}"
|
||
docker exec -u www-data nextcloud_app php occ files:scan --all --quiet 2>/dev/null
|
||
echo -e "${GREEN}✅ 完成!${NC}"
|
||
;;
|
||
5)
|
||
echo -e "${YELLOW}=== 所有 SSHFS 挂载状态 ===${NC}"
|
||
echo ""
|
||
echo -e "${CYAN}--- systemd 服务状态 ---${NC}"
|
||
for svc in $(systemctl list-units --type=service 2>/dev/null | grep "^mnt-" | awk '{print $1}'); do
|
||
local _state
|
||
_state=$(systemctl is-active "$svc" 2>/dev/null)
|
||
if [ "$_state" == "active" ]; then
|
||
echo -e " ${GREEN}✅ $svc (运行中)${NC}"
|
||
else
|
||
echo -e " ${RED}❌ $svc ($_state)${NC}"
|
||
fi
|
||
done
|
||
echo ""
|
||
echo -e "${CYAN}--- 当前 FUSE/SSHFS 挂载点 ---${NC}"
|
||
mount | grep -E "fuse|sshfs" | while read -r line; do
|
||
echo -e " ${GREEN}$line${NC}"
|
||
done
|
||
echo ""
|
||
echo -e "${CYAN}--- /mnt 目录内容 ---${NC}"
|
||
ls -la /mnt/
|
||
;;
|
||
6)
|
||
install_sshfs_mount
|
||
;;
|
||
7)
|
||
echo -e "${YELLOW}当前外部存储列表:${NC}"
|
||
docker exec -u www-data nextcloud_app php occ files_external:list 2>/dev/null
|
||
echo ""
|
||
read -p "请输入要删除的 Mount ID: " DEL_ID
|
||
if [ -n "$DEL_ID" ]; then
|
||
docker exec -u www-data nextcloud_app php occ files_external:delete "$DEL_ID" 2>/dev/null \
|
||
&& echo -e "${GREEN}✅ 已删除外部存储 ID=$DEL_ID${NC}" \
|
||
|| echo -e "${RED}❌ 删除失败${NC}"
|
||
read -p "是否同时卸载本机挂载点?(y/n): " DO_UMOUNT
|
||
if [ "$DO_UMOUNT" == "y" ] || [ "$DO_UMOUNT" == "Y" ]; then
|
||
read -p "请输入本机挂载路径 (例如 /mnt/TOKYO): " UMOUNT_PATH
|
||
fusermount -u "$UMOUNT_PATH" 2>/dev/null || umount "$UMOUNT_PATH" 2>/dev/null
|
||
# 停用并删除 systemd 服务
|
||
local _UNAME="mnt-$(echo ${UMOUNT_PATH} | sed 's|^/||' | tr '/' '-')"
|
||
systemctl disable --now "${_UNAME}.service" 2>/dev/null
|
||
rm -f "/etc/systemd/system/${_UNAME}.service"
|
||
systemctl daemon-reload
|
||
echo -e "${GREEN}✅ 已卸载并清理 systemd 服务${NC}"
|
||
fi
|
||
fi
|
||
;;
|
||
*) return ;;
|
||
esac
|
||
|
||
read -n 1 -s -r -p "按任意键返回..."
|
||
}
|
||
|
||
# 22.2 Nextcloud PDF及画册封面引擎补丁
|
||
run_nextcloud_pdf_patch() {
|
||
if ! docker ps | grep -q nextcloud_app; then
|
||
echo -e "\n${RED}错误:未检测到运行中的 nextcloud_app 容器!请确认 Nextcloud 正常运行。${NC}"
|
||
sleep 3
|
||
return
|
||
fi
|
||
clear
|
||
echo -e "${BLUE}--- Nextcloud PDF/书籍封面引擎补丁启动 ---${NC}"
|
||
|
||
echo -e "\n${CYAN}[1/3] 正在为容器安装 PDF 渲染核心组件 (Ghostscript)...${NC}"
|
||
docker exec -u root nextcloud_app apt-get update >/dev/null 2>&1
|
||
docker exec -u root nextcloud_app apt-get install -y ghostscript >/dev/null 2>&1
|
||
|
||
echo -e "\n${CYAN}[2/3] 正在解除 ImageMagick 的 PDF 安全限制...${NC}"
|
||
# 将默认的 rights="none" 修改为 rights="read|write",释放 PDF 渲染权限
|
||
docker exec -u root nextcloud_app sed -i 's/rights="none" pattern="PDF"/rights="read|write" pattern="PDF"/g' /etc/ImageMagick-6/policy.xml
|
||
|
||
echo -e "\n${CYAN}[3/3] 正在向 Nextcloud 注入 PDF 预览提供程序...${NC}"
|
||
docker exec --user www-data nextcloud_app php occ config:system:set enabledPreviewProviders 5 --value="OC\Preview\PDF"
|
||
# 顺手把矢量图 SVG 的预览也加上
|
||
docker exec --user www-data nextcloud_app php occ config:system:set enabledPreviewProviders 6 --value="OC\Preview\SVG"
|
||
|
||
echo -e "\n${GREEN}✅ PDF 封面引擎物理配置完成!新上传的 PDF 将自动生成缩略图。${NC}"
|
||
|
||
# --- 历史文件缩略图生成模块 ---
|
||
echo -e "\n${YELLOW}================================================================${NC}"
|
||
read -p "您想现在立刻为网盘里【已经存在】的历史 PDF 文件生成封面吗?(y/n): " gen_choice
|
||
|
||
if [[ "$gen_choice" == "y" || "$gen_choice" == "Y" ]]; then
|
||
echo -e "\n${CYAN}正在全盘扫描并生成历史缩略图,请耐心等待 (切勿关闭终端窗口)...${NC}"
|
||
docker exec --user www-data nextcloud_app php occ preview:generate-all
|
||
echo -e "${GREEN}✅ 历史 PDF 及其他文件缩略图全盘生成完毕!${NC}"
|
||
else
|
||
echo -e "\n${GREEN}已跳过历史文件生成。${NC}"
|
||
fi
|
||
|
||
echo -e "\n${GREEN}按任意键返回主菜单...${NC}"
|
||
read -n 1 -s
|
||
}
|
||
|
||
# 23. 服务控制中心
|
||
show_service_control_panel() {
|
||
ensure_docker_installed || return
|
||
|
||
# ══════════════════════════════════════════════════════════
|
||
# 辅助:_svc_action_docker — 对 docker-compose 服务执行操作
|
||
# ══════════════════════════════════════════════════════════
|
||
_svc_action_docker() {
|
||
local s_name="$1" s_path="$2"
|
||
local compose_file="${s_path}/docker-compose.yml"
|
||
|
||
# ── 判断此服务是否支持媒体库关联 ──
|
||
local is_linkable=false
|
||
local container_paths=() path_labels=() default_local_paths=()
|
||
local menu_options=()
|
||
|
||
case "$s_name" in
|
||
"Jellyfin 影院")
|
||
menu_options=("启动" "停止" "重启" "查看日志" "查看可用媒体路径" "返回") ;;
|
||
"Navidrome 音乐")
|
||
is_linkable=true
|
||
container_paths=("/music"); path_labels=("音乐库"); default_local_paths=("/mnt/Music") ;;
|
||
"Immich 相册")
|
||
is_linkable=true
|
||
container_paths=("/usr/src/app/upload"); path_labels=("照片路径"); default_local_paths=("/mnt/Photos") ;;
|
||
"Calibre-Web 书库")
|
||
is_linkable=true
|
||
container_paths=("/books"); path_labels=("书库路径"); default_local_paths=("/mnt/Books") ;;
|
||
"Kavita 阅读器")
|
||
is_linkable=true
|
||
container_paths=("/manga"); path_labels=("书库路径"); default_local_paths=("/mnt/Books") ;;
|
||
"qBittorrent"|"JDownloader"|"MeTube 下载")
|
||
is_linkable=true
|
||
local dl_c="/downloads"
|
||
[[ "$s_name" == "JDownloader" ]] && dl_c="/output"
|
||
container_paths=("$dl_c"); path_labels=("下载目录"); default_local_paths=("/mnt/Downloads") ;;
|
||
"Nextcloud 数据中心")
|
||
menu_options=("启动" "停止" "重启" "查看日志" "添加Rclone跃遷盘(外部存储)" "配置Cron定时任务(文件盘点)" "返回") ;;
|
||
esac
|
||
|
||
# 若 case 未预设菜单,按通用规则生成
|
||
if [ ${#menu_options[@]} -eq 0 ]; then
|
||
if $is_linkable; then
|
||
menu_options=("启动" "停止" "重启" "查看日志" "设置媒体库/下载路径" "查看当前路径" "返回")
|
||
else
|
||
menu_options=("启动" "停止" "重启" "查看日志" "返回")
|
||
fi
|
||
fi
|
||
|
||
clear
|
||
echo -e "${BLUE}══════════════════════════════════════════════${NC}"
|
||
echo -e "${BLUE} 🎛️ ${s_name}${NC}"
|
||
echo -e "${BLUE}══════════════════════════════════════════════${NC}"
|
||
|
||
select opt in "${menu_options[@]}"; do
|
||
case $opt in
|
||
"启动") (cd "$s_path" && sudo docker compose up -d); break ;;
|
||
"停止") (cd "$s_path" && sudo docker compose stop); break ;;
|
||
"重启") (cd "$s_path" && sudo docker compose restart); break ;;
|
||
"查看日志") sudo docker compose -f "$compose_file" logs -f --tail 80; break ;;
|
||
|
||
# ── Jellyfin 专属 ──
|
||
"查看可用媒体路径")
|
||
echo ""
|
||
echo -e "${CYAN}Jellyfin 已挂载整个 /mnt,直接在 Jellyfin 界面填写以下路径:${NC}"
|
||
echo ""
|
||
find /mnt -maxdepth 2 -mindepth 1 -type d 2>/dev/null | sort | while read -r d; do
|
||
echo -e " ${GREEN}${d}${NC}"
|
||
done
|
||
echo ""
|
||
echo -e "${YELLOW}⚠️ 报"没有访问权"?选 y 一键重建容器修复权限:${NC}"
|
||
read -p "是否重建 Jellyfin 容器?(y/n): " fix_c
|
||
if [[ "$fix_c" == "y" || "$fix_c" == "Y" ]]; then
|
||
sudo sed -i "s/PUID=1000/PUID=0/g; s/PGID=1000/PGID=0/g" "$compose_file"
|
||
(cd "$s_path" && sudo docker compose up -d --force-recreate)
|
||
echo -e "${GREEN}✅ 重建完成!${NC}"
|
||
fi
|
||
read -n 1 -s -r -p "按任意键返回..."; break ;;
|
||
|
||
# ── Nextcloud 专属 ──
|
||
"添加Rclone跃遷盘(外部存储)")
|
||
local rclone_mount_path
|
||
rclone_mount_path=$(grep "^RCLONE_MOUNT_PATH=" "${STATE_FILE}" 2>/dev/null | cut -d'=' -f2)
|
||
if [ -z "$rclone_mount_path" ]; then echo -e "${RED}❌ Rclone 未配置${NC}"; sleep 3; break; fi
|
||
if ! mount | grep -q "${rclone_mount_path}"; then echo -e "${RED}❌ Rclone 挂载点未生效${NC}"; sleep 3; break; fi
|
||
if grep -q "${rclone_mount_path}:${rclone_mount_path}" "$compose_file"; then
|
||
echo -e "${YELLOW}跃遷盘已添加,无需重复操作。${NC}"; sleep 3; break; fi
|
||
local last_vol_line
|
||
last_vol_line=$(grep -n 'php-opcache.ini' "$compose_file" | cut -d: -f1)
|
||
if [ -n "$last_vol_line" ]; then
|
||
local ind
|
||
ind=$(grep 'php-opcache.ini' "$compose_file" | awk '{gsub(/[^ ].*/, ""); print}')
|
||
sudo sed -i "${last_vol_line}a ${ind}- '${rclone_mount_path}:${rclone_mount_path}' # 由控制中心添加" "$compose_file"
|
||
(cd "$s_path" && sudo docker compose up -d --force-recreate)
|
||
echo -e "${GREEN}✅ 已添加并重启!进入 Nextcloud → 设置 → 外部存储 添加路径。${NC}"
|
||
else
|
||
echo -e "${RED}❌ 未找到锚点,请检查 docker-compose.yml${NC}"
|
||
fi
|
||
sleep 5; break ;;
|
||
|
||
"配置Cron定时任务(文件盘点)")
|
||
local cur_cron; cur_cron=$(crontab -l 2>/dev/null)
|
||
local cron_scan="*/15 * * * * /usr/bin/docker exec --user www-data nextcloud_app php occ files:scan --all > /dev/null 2>&1"
|
||
local cron_php="*/5 * * * * /usr/bin/docker exec --user www-data nextcloud_app php -f /var/www/html/cron.php > /dev/null 2>&1"
|
||
echo "$cur_cron" | grep -q "files:scan --all" || { (echo "$cur_cron"; echo "$cron_scan") | crontab -; echo -e "${GREEN}✅ 文件扫描任务已添加${NC}"; }
|
||
cur_cron=$(crontab -l 2>/dev/null)
|
||
echo "$cur_cron" | grep -q "cron.php" || { (echo "$cur_cron"; echo "$cron_php") | crontab -; echo -e "${GREEN}✅ 后台任务已添加${NC}"; }
|
||
echo -e "${CYAN}当前 Nextcloud cron:${NC}"; crontab -l | grep 'nextcloud_app'
|
||
sleep 5; break ;;
|
||
|
||
# ── 媒体库/下载路径关联(Navidrome / qBittorrent / JDownloader / MeTube)──
|
||
"设置媒体库/下载路径")
|
||
echo ""
|
||
echo -e "${CYAN}/mnt 下可用目录:${NC}"
|
||
find /mnt -maxdepth 2 -mindepth 1 -type d 2>/dev/null | sort | while read -r d; do echo -e " ${GREEN}${d}${NC}"; done
|
||
echo ""
|
||
local any_changed=false
|
||
for idx in "${!container_paths[@]}"; do
|
||
local c_path="${container_paths[$idx]}" label="${path_labels[$idx]}" def="${default_local_paths[$idx]}"
|
||
local cur_host
|
||
cur_host=$(grep ":${c_path}" "$compose_file" 2>/dev/null | head -1 \
|
||
| sed "s|.*['\"]\\?\\([^'\"]*\\)['\"]\\?:${c_path}.*|\\1|" \
|
||
| sed "s/^[ \t-]*//" | sed "s/['\"]//g")
|
||
echo -e "${YELLOW}[${label}] 当前: ${GREEN}${cur_host:-未知}${NC} 默认: ${CYAN}${def}${NC}"
|
||
read -p " 输入新路径(回车=默认)> " user_in
|
||
local new_host; [ -z "$user_in" ] && new_host="$def" || new_host="$user_in"
|
||
[ ! -d "$new_host" ] && { echo -e " ${YELLOW}路径不存在,正在创建...${NC}"; sudo mkdir -p "$new_host"; }
|
||
local match_line; match_line=$(grep ":${c_path}" "$compose_file" | head -1)
|
||
if [ -z "$match_line" ]; then
|
||
echo -e " ${RED}❌ 未找到 ${c_path} 的映射行${NC}"; continue; fi
|
||
local ind; ind=$(echo "$match_line" | sed 's/[^ ].*//')
|
||
local new_line="${ind}- '${new_host}:${c_path}'"
|
||
if sudo sed -i "s|${match_line}|${new_line}|" "$compose_file" 2>/dev/null; then
|
||
echo -e " ${GREEN}✅ [${label}] → ${new_host}${NC}"; any_changed=true
|
||
else
|
||
echo -e " ${RED}❌ 替换失败${NC}"; fi
|
||
echo ""
|
||
done
|
||
if $any_changed; then
|
||
(cd "$s_path" && sudo docker compose up -d --force-recreate)
|
||
echo -e "${GREEN}✅ 已重启,新路径生效!${NC}"
|
||
fi
|
||
read -n 1 -s -r -p "按任意键返回..."; break ;;
|
||
|
||
"查看当前路径")
|
||
echo ""
|
||
for idx in "${!container_paths[@]}"; do
|
||
local c_path="${container_paths[$idx]}" label="${path_labels[$idx]}"
|
||
local cur_host
|
||
cur_host=$(grep ":${c_path}" "$compose_file" 2>/dev/null | head -1 \
|
||
| sed "s|.*['\"]\\?\\([^'\"]*\\)['\"]\\?:${c_path}.*|\\1|" \
|
||
| sed "s/^[ \t-]*//" | sed "s/['\"]//g")
|
||
echo -e " ${label}: ${GREEN}${cur_host:-未找到}${NC} → 容器内 ${c_path}"
|
||
done
|
||
read -n 1 -s -r -p "按任意键返回..."; break ;;
|
||
|
||
"返回") return ;;
|
||
*) echo "无效选项" ;;
|
||
esac
|
||
done
|
||
}
|
||
|
||
# ══════════════════════════════════════════════════════════
|
||
# 辅助:_svc_action_systemd — 对 systemd 服务执行操作
|
||
# ══════════════════════════════════════════════════════════
|
||
_svc_action_systemd() {
|
||
local s_name="$1" svc_unit="$2"
|
||
clear
|
||
echo -e "${BLUE}══════════════════════════════════════════════${NC}"
|
||
echo -e "${BLUE} 🎛️ ${s_name}${NC}"
|
||
echo -e "${BLUE}══════════════════════════════════════════════${NC}"
|
||
select opt in "启动" "停止" "重启" "查看日志" "查看状态" "返回"; do
|
||
case $opt in
|
||
"启动") sudo systemctl start "$svc_unit"; echo -e "${GREEN}✅ 已启动${NC}"; sleep 2; break ;;
|
||
"停止") sudo systemctl stop "$svc_unit"; echo -e "${YELLOW}⏹️ 已停止${NC}"; sleep 2; break ;;
|
||
"重启") sudo systemctl restart "$svc_unit"; echo -e "${GREEN}✅ 已重启${NC}"; sleep 2; break ;;
|
||
"查看日志") sudo journalctl -u "$svc_unit" -f --no-pager -n 80; break ;;
|
||
"查看状态") sudo systemctl status "$svc_unit"; read -n 1 -s -r -p "按任意键返回..."; break ;;
|
||
"返回") return ;;
|
||
*) echo "无效选项" ;;
|
||
esac
|
||
done
|
||
}
|
||
|
||
# ══════════════════════════════════════════════════════════
|
||
# 辅助:_svc_action_glances — Glances 无 compose,用 docker 直接管理
|
||
# ══════════════════════════════════════════════════════════
|
||
_svc_action_glances() {
|
||
clear
|
||
echo -e "${BLUE}══════════════════════════════════════════════${NC}"
|
||
echo -e "${BLUE} 🎛️ Glances 资源监控${NC}"
|
||
echo -e "${BLUE}══════════════════════════════════════════════${NC}"
|
||
select opt in "启动" "停止" "重启" "查看日志" "返回"; do
|
||
case $opt in
|
||
"启动") sudo docker start glances_app; echo -e "${GREEN}✅ 已启动${NC}"; sleep 2; break ;;
|
||
"停止") sudo docker stop glances_app; echo -e "${YELLOW}⏹️ 已停止${NC}"; sleep 2; break ;;
|
||
"重启") sudo docker restart glances_app; echo -e "${GREEN}✅ 已重启${NC}"; sleep 2; break ;;
|
||
"查看日志") sudo docker logs -f --tail 80 glances_app; break ;;
|
||
"返回") return ;;
|
||
*) echo "无效选项" ;;
|
||
esac
|
||
done
|
||
}
|
||
|
||
# ══════════════════════════════════════════════════════════
|
||
# 主循环
|
||
# ══════════════════════════════════════════════════════════
|
||
while true; do
|
||
clear
|
||
echo -e "${BLUE}══════════════════════════════════════════════════════════════════════════════════════════════════════${NC}"
|
||
echo -e "${BLUE} 🎛️ 服务控制中心 — 对已部署服务进行管理${NC}"
|
||
echo -e "${BLUE}══════════════════════════════════════════════════════════════════════════════════════════════════════${NC}"
|
||
|
||
# ── 状态检测辅助函数 ──
|
||
_docker_status() {
|
||
local path="$1"
|
||
if sudo docker compose -f "${path}/docker-compose.yml" ps -q 2>/dev/null | grep -q .; then
|
||
echo -e "${GREEN}[ 运行中 ]${NC}"
|
||
else
|
||
echo -e "${RED}[ 已停止 ]${NC}"
|
||
fi
|
||
}
|
||
_systemd_status() {
|
||
if systemctl is-active --quiet "$1" 2>/dev/null; then
|
||
echo -e "${GREEN}[ 运行中 ]${NC}"
|
||
else
|
||
echo -e "${RED}[ 已停止 ]${NC}"
|
||
fi
|
||
}
|
||
_docker_ctr_status() {
|
||
if docker inspect --format='{{.State.Running}}' "$1" 2>/dev/null | grep -q true; then
|
||
echo -e "${GREEN}[ 运行中 ]${NC}"
|
||
else
|
||
echo -e "${RED}[ 已停止 ]${NC}"
|
||
fi
|
||
}
|
||
|
||
# ── Docker 服务列表(name:path) ──
|
||
local docker_services=(
|
||
"Nextcloud 数据中心:/root/nextcloud_data"
|
||
"OnlyOffice 办公室:/root/onlyoffice_data"
|
||
"WordPress 博客:/root/wordpress_data"
|
||
"AI 大脑 (Ollama+WebUI):/root/ai_stack"
|
||
"Jellyfin 影院:/root/jellyfin_data"
|
||
"Navidrome 音乐:/root/navidrome_data"
|
||
"Immich 相册:/root/immich_data"
|
||
"Calibre-Web 书库:/root/calibre_web_data"
|
||
"Kavita 阅读器:/root/kavita_data"
|
||
"Gotify 推送:/root/gotify_data"
|
||
"Actual Budget 记账:/root/actual_budget_data"
|
||
"Excalidraw 白板:/root/excalidraw_data"
|
||
"Joplin Server 笔记:/root/joplin_data"
|
||
"Trilium Notes 知识库:/root/trilium_data"
|
||
"Grafana 监控:/root/grafana_data"
|
||
"Miniflux RSS:/root/miniflux_data"
|
||
"Gitea 代码仓库:/root/gitea_data"
|
||
"Memos 笔记:/root/memos_data"
|
||
"qBittorrent:/root/qbittorrent_data"
|
||
"JDownloader:/root/jdownloader_data"
|
||
"MeTube 下载:/root/ytdlp_data"
|
||
"Draw.io 绘图:/root/drawio_data"
|
||
"N8N 工作流:/root/n8n_data"
|
||
"SillyTavern AI酒馆:/root/sillytavern_data"
|
||
"Vless 节点:/root/vless_data"
|
||
"Uptime Kuma 监控:/root/uptime_kuma_data"
|
||
"Home Assistant:/root/home_assistant_data"
|
||
)
|
||
|
||
# ── systemd 服务列表(name:unit) ──
|
||
local systemd_services=(
|
||
"Syncthing 文件同步:syncthing@root"
|
||
"Fail2ban 防暴力破解:fail2ban"
|
||
"XRDP 远程桌面:xrdp"
|
||
"Hysteria 2 加速:hysteria-server"
|
||
"Cloudflare Tunnel:cloudflared"
|
||
)
|
||
|
||
local idx=1
|
||
declare -a slot_type=() # "docker" | "systemd" | "glances"
|
||
declare -a slot_name=()
|
||
declare -a slot_path=() # docker: compose dir; systemd: unit name
|
||
|
||
# ── 渲染 Docker 服务分组 ──
|
||
echo ""
|
||
echo -e "${GREEN} ── 🐳 Docker 服务 ──────────────────────────────────────────────────────────────${NC}"
|
||
for entry in "${docker_services[@]}"; do
|
||
local sname="${entry%%:*}" spath="${entry#*:}"
|
||
if [ -f "${spath}/docker-compose.yml" ]; then
|
||
local st; st=$(_docker_status "$spath")
|
||
printf " ${YELLOW}%2d)${NC} %-28s %b\n" "$idx" "$sname" "$st"
|
||
slot_type+=("docker"); slot_name+=("$sname"); slot_path+=("$spath")
|
||
idx=$((idx+1))
|
||
fi
|
||
done
|
||
|
||
# ── Glances 单独一行(无 compose,用 docker 容器直接管理)──
|
||
if docker inspect glances_app &>/dev/null 2>&1; then
|
||
local gst; gst=$(_docker_ctr_status "glances_app")
|
||
printf " ${YELLOW}%2d)${NC} %-28s %b\n" "$idx" "Glances 资源监控" "$gst"
|
||
slot_type+=("glances"); slot_name+=("Glances 资源监控"); slot_path+=("")
|
||
idx=$((idx+1))
|
||
fi
|
||
|
||
# ── 渲染 systemd 服务分组 ──
|
||
local has_systemd=false
|
||
local systemd_buf=""
|
||
for entry in "${systemd_services[@]}"; do
|
||
local sname="${entry%%:*}" sunit="${entry#*:}"
|
||
if systemctl list-unit-files 2>/dev/null | grep -q "^${sunit}"; then
|
||
local st; st=$(_systemd_status "$sunit")
|
||
systemd_buf+=$(printf " ${YELLOW}%2d)${NC} %-28s %b\n" "$idx" "$sname" "$st")
|
||
systemd_buf+="\n"
|
||
slot_type+=("systemd"); slot_name+=("$sname"); slot_path+=("$sunit")
|
||
idx=$((idx+1))
|
||
has_systemd=true
|
||
fi
|
||
done
|
||
if $has_systemd; then
|
||
echo ""
|
||
echo -e "${YELLOW} ── ⚙️ 系统服务 ─────────────────────────────────────────────────────────────────${NC}"
|
||
echo -e "$systemd_buf"
|
||
fi
|
||
|
||
# ── Rclone 挂载状态(只读展示,不可操作)──
|
||
local rclone_mounts
|
||
rclone_mounts=$(mount 2>/dev/null | grep "fuse.sshfs\|rclone" | wc -l)
|
||
if [ "$rclone_mounts" -gt 0 ]; then
|
||
echo ""
|
||
echo -e "${CYAN} ── 📡 网盘挂载 ($rclone_mounts 个活跃) — 管理请用菜单 10.3 ──────────────────────────────${NC}"
|
||
mount 2>/dev/null | grep "fuse.sshfs\|rclone" | while read -r line; do
|
||
local mp; mp=$(echo "$line" | awk '{print $3}')
|
||
echo -e " ${GREEN}✅ ${mp}${NC}"
|
||
done
|
||
fi
|
||
|
||
echo ""
|
||
echo -e "${BLUE}══════════════════════════════════════════════════════════════════════════════════════════════════════${NC}"
|
||
echo -e " ${YELLOW}b)${NC} 返回主菜单"
|
||
echo ""
|
||
read -p " 请输入编号选择服务: " svc_choice
|
||
|
||
[[ "$svc_choice" == "b" || "$svc_choice" == "B" ]] && break
|
||
|
||
if ! [[ "$svc_choice" =~ ^[0-9]+$ ]] || [ "$svc_choice" -lt 1 ] || [ "$svc_choice" -gt $((idx-1)) ]; then
|
||
echo -e "${RED}无效选择!${NC}"; sleep 2; continue
|
||
fi
|
||
|
||
local si=$((svc_choice-1))
|
||
local chosen_type="${slot_type[$si]}"
|
||
local chosen_name="${slot_name[$si]}"
|
||
local chosen_path="${slot_path[$si]}"
|
||
|
||
case "$chosen_type" in
|
||
"docker") _svc_action_docker "$chosen_name" "$chosen_path" ;;
|
||
"systemd") _svc_action_systemd "$chosen_name" "$chosen_path" ;;
|
||
"glances") _svc_action_glances ;;
|
||
esac
|
||
|
||
sleep 1
|
||
done
|
||
}
|
||
|
||
|
||
# 24. 查看密码与数据路径
|
||
show_credentials() {
|
||
if [ ! -f "${STATE_FILE}" ]; then echo -e "\n${YELLOW}尚未开始装修,没有凭证信息。${NC}"; sleep 2; return; fi
|
||
clear
|
||
echo -e "${RED}==================== 🔑 【重要凭证保险箱】 🔑 ====================${NC}"
|
||
|
||
# --- 特殊处理 Nextcloud ---
|
||
if grep -q "## Nextcloud 凭证" "${STATE_FILE}"; then
|
||
local nc_db_pass=$(grep 'DB_PASSWORD' ${STATE_FILE} | head -n 1 | cut -d'=' -f2)
|
||
local nc_domain=$(grep 'NEXTCLOUD_DOMAIN' ${STATE_FILE} | cut -d'=' -f2)
|
||
local oo_domain=$(grep 'ONLYOFFICE_DOMAIN' ${STATE_FILE} | cut -d'=' -f2)
|
||
local oo_jwt=$(grep 'ONLYOFFICE_JWT_SECRET' ${STATE_FILE} | cut -d'=' -f2)
|
||
|
||
echo -e "\n${CYAN}--- Nextcloud 首次安装配置信息 ---${NC}"
|
||
echo -e " 数据库用户: ${GREEN}nextclouduser${NC}"
|
||
echo -e " 数据库名称: ${GREEN}nextclouddb${NC}"
|
||
echo -e " 数据库主机: ${GREEN}db${NC}"
|
||
echo -e " 数据库密码: ${GREEN}${nc_db_pass}${NC}"
|
||
echo ""
|
||
echo -e "${CYAN}--- Nextcloud & OnlyOffice 访问与集成信息 ---${NC}"
|
||
echo -e " Nextcloud 访问域名: ${GREEN}https://${nc_domain}${NC}"
|
||
echo -e " OnlyOffice 访问域名: ${GREEN}https://${oo_domain}${NC}"
|
||
echo -e " OnlyOffice JWT Secret: ${GREEN}${oo_jwt}${NC}"
|
||
fi
|
||
|
||
# --- VLESS 节点信息 ---
|
||
if grep -q "VLESS_DOMAIN" "${STATE_FILE}"; then
|
||
local vless_domain=$(grep 'VLESS_DOMAIN' ${STATE_FILE} | head -n 1 | cut -d'=' -f2)
|
||
local vless_uuid=$(grep 'VLESS_UUID' ${STATE_FILE} | head -n 1 | cut -d'=' -f2)
|
||
|
||
if [ -n "$vless_domain" ] && [ -n "$vless_uuid" ]; then
|
||
local vless_link="vless://${vless_uuid}@${vless_domain}:443?encryption=none&security=tls&type=ws&host=${vless_domain}&path=%2F#VPS-Tunnel-VLESS"
|
||
echo -e "\n${CYAN}--- “科学上网” VLESS 节点信息 ---${NC}"
|
||
echo -e " 节点域名: ${GREEN}${vless_domain}${NC}"
|
||
echo -e " 节点 UUID: ${GREEN}${vless_uuid}${NC}"
|
||
echo -e " VLESS 链接: ${CYAN}${vless_link}${NC}"
|
||
fi
|
||
fi
|
||
|
||
# --- 循环显示其他凭证 ---
|
||
local current_section=""
|
||
while IFS= read -r line; do
|
||
if [[ "$line" == "## Nextcloud 凭证"* || "$line" == "## “科学上网” VLESS 节点"* ]]; then
|
||
continue
|
||
elif [[ "$line" == "## "* ]]; then
|
||
current_section=$(echo "$line" | sed -e 's/## //' -e 's/ (.*//')
|
||
echo -e "\n${CYAN}--- ${current_section} ---${NC}"
|
||
elif [[ -n "$line" && ! "$line" =~ ^# ]]; then
|
||
local key=$(echo "$line" | cut -d'=' -f1)
|
||
local value=$(echo "$line" | cut -d'=' -f2-)
|
||
if [[ "$key" == *"DOMAIN"* ]]; then
|
||
echo -e " 访问域名 : ${GREEN}https://${value}${NC}"
|
||
elif [[ "$key" == *"PORT"* ]]; then
|
||
echo -e " 本地端口 : ${DIM}${value}${NC}"
|
||
elif [[ "$key" == *"PASSWORD"* || "$key" == *"PASS"* || "$key" == *"SECRET"* || "$key" == *"KEY"* ]]; then
|
||
echo -e " 密码/密钥 : ${YELLOW}${value}${NC}"
|
||
elif [[ "$key" == *"USER"* || "$key" == *"ADMIN"* ]]; then
|
||
echo -e " 用户名 : ${value}"
|
||
elif [[ "$key" == *"NOTE"* ]]; then
|
||
echo -e " ${DIM}提示: ${value}${NC}"
|
||
elif [[ "$key" == "CLOUDFLARE_TOKEN" || "$key" == "VLESS_UUID" || "$key" == "N8N_ENCRYPTION_KEY" ]]; then
|
||
continue
|
||
elif [[ -n "$value" ]]; then
|
||
echo -e " ${key}: ${value}"
|
||
fi
|
||
fi
|
||
done < "${STATE_FILE}"
|
||
|
||
# --- qBittorrent 初始密码 ---
|
||
if [ -d "/root/qbittorrent_data" ]; then
|
||
echo -e "\n${CYAN}--- qBittorrent 初始密码 ---${NC}"
|
||
echo " qBittorrent 的初始密码在首次启动的日志中。请退出本面板后执行:"
|
||
echo -e " ${YELLOW}sudo docker logs qbittorrent_app${NC}"
|
||
echo " (在日志中找到 The Web UI administrator password is: xxxxxxxx 这一行)"
|
||
fi
|
||
|
||
# --- 应用数据目录 ---
|
||
echo -e "\n${CYAN}--- 应用数据目录 ---${NC}"
|
||
[ -d "/mnt/Music" ] && echo " 🎵 音乐库 (Navidrome/Jellyfin): /mnt/Music"
|
||
[ -d "/mnt/Movies" ] && echo " 🎬 电影库 (Jellyfin): /mnt/Movies"
|
||
[ -d "/mnt/TVShows" ] && echo " 📺 电视剧库 (Jellyfin): /mnt/TVShows"
|
||
[ -d "/mnt/Downloads" ] && echo " 🔽 默认下载目录: /mnt/Downloads"
|
||
if grep -q "RCLONE_MOUNT_PATH" "${STATE_FILE}"; then
|
||
echo " ☁️ Rclone 网盘挂载点: $(grep 'RCLONE_MOUNT_PATH' ${STATE_FILE} | cut -d'=' -f2)"
|
||
fi
|
||
|
||
echo -e "\n${RED}========================================================================${NC}"
|
||
read -n 1 -s -r -p "按任意键返回主菜单..."
|
||
}
|
||
|
||
# --- VPS信息查看函数 ---
|
||
show_vps_info() {
|
||
clear
|
||
echo -e "${BLUE}══════════════════════════════════════════════════════════════${NC}"
|
||
echo -e "${BLUE} 📊 VPS 服务器信息${NC}"
|
||
echo -e "${BLUE}══════════════════════════════════════════════════════════════${NC}"
|
||
|
||
local ipv4; ipv4=$(curl -s4 --max-time 5 https://ifconfig.me/ip 2>/dev/null || echo "获取失败")
|
||
local os_info; os_info=$(source /etc/os-release && echo "$PRETTY_NAME")
|
||
local cpu_info; cpu_info=$(grep "model name" /proc/cpuinfo | head -1 | cut -d: -f2 | sed 's/^[ \t]*//')
|
||
local cpu_cores; cpu_cores=$(nproc)
|
||
local uptime_info; uptime_info=$(uptime -p 2>/dev/null || uptime)
|
||
|
||
echo ""
|
||
echo -e "${CYAN} 🌐 公网 IP :${NC} ${GREEN}${ipv4}${NC}"
|
||
echo -e "${CYAN} 🐧 操作系统 :${NC} ${GREEN}${os_info}${NC}"
|
||
echo -e "${CYAN} 💻 CPU 型号 :${NC} ${GREEN}${cpu_info} (${cpu_cores} 核)${NC}"
|
||
echo -e "${CYAN} ⏱️ 运行时间 :${NC} ${GREEN}${uptime_info}${NC}"
|
||
|
||
echo ""
|
||
echo -e "${YELLOW} ── 内存使用 ──────────────────────────────────────────────${NC}"
|
||
free -h | sed 's/^/ /'
|
||
|
||
echo ""
|
||
echo -e "${YELLOW} ── 磁盘使用 ──────────────────────────────────────────────${NC}"
|
||
df -h | grep -v "tmpfs\|overlay\|udev" | sed 's/^/ /'
|
||
|
||
echo ""
|
||
echo -e "${YELLOW} ── 目录占用详情 ──────────────────────────────────────────${NC}"
|
||
echo -e " ${CYAN}Docker 镜像与容器:${NC}"
|
||
du -sh /var/lib/docker/ 2>/dev/null | sed 's/^/ /'
|
||
echo -e " ${CYAN}应用数据目录(从小到大):${NC}"
|
||
du -sh /root/*/ 2>/dev/null | sort -h | sed 's/^/ /'
|
||
|
||
echo ""
|
||
echo -e "${YELLOW} ── 网盘挂载状态 ──────────────────────────────────────────${NC}"
|
||
local mount_count; mount_count=$(mount 2>/dev/null | grep -c "fuse\|sshfs" || echo 0)
|
||
if [ "$mount_count" -gt 0 ]; then
|
||
mount | grep "fuse\|sshfs" | awk '{print " ✅ "$3}'
|
||
else
|
||
echo -e " ${DIM}无网盘挂载${NC}"
|
||
fi
|
||
|
||
echo ""
|
||
echo -e "${BLUE}══════════════════════════════════════════════════════════════${NC}"
|
||
echo -e "\n${GREEN}按任意键返回主菜单...${NC}"; read -n 1 -s
|
||
}
|
||
|
||
# 26. 科学上网工具箱
|
||
install_vless_node() {
|
||
ensure_docker_installed || return; check_tunnel_installed || return
|
||
clear; echo -e "${BLUE}--- “科学上网”工具箱 ---${NC}"
|
||
read -p "请输入您为VLESS节点准备的域名 (例如 v.example.com): " VLESS_DOMAIN
|
||
if [ -z "$VLESS_DOMAIN" ]; then echo -e "${RED}域名不能为空!${NC}"; sleep 2; return; fi
|
||
|
||
local UUID; UUID=$(cat /proc/sys/kernel/random/uuid)
|
||
|
||
mkdir -p /root/vless_data; cat > /root/vless_data/docker-compose.yml <<EOF
|
||
services:
|
||
vless:
|
||
image: teddysun/xray:latest
|
||
container_name: vless_app
|
||
restart: always
|
||
ports:
|
||
- "127.0.0.1:40001:40001"
|
||
command: xray -c /etc/xray/config.json
|
||
volumes:
|
||
- ./config.json:/etc/xray/config.json
|
||
EOF
|
||
cat > /root/vless_data/config.json <<EOF
|
||
{
|
||
"log": { "loglevel": "warning" },
|
||
"inbounds": [
|
||
{
|
||
"listen": "0.0.0.0",
|
||
"port": 40001,
|
||
"protocol": "vless",
|
||
"settings": {
|
||
"clients": [
|
||
{ "id": "${UUID}", "level": 0 }
|
||
],
|
||
"decryption": "none"
|
||
},
|
||
"streamSettings": { "network": "ws" }
|
||
}
|
||
],
|
||
"outbounds": [ { "protocol": "freedom" } ]
|
||
}
|
||
EOF
|
||
|
||
if _compose_up "Vless" "/root/vless_data"; then
|
||
echo -e "${GREEN}✅ VLESS 节点已启动,等待服务稳定...${NC}"; sleep 5
|
||
update_tunnel_config "${VLESS_DOMAIN}" "http://127.0.0.1:40001" "VLESS Node"
|
||
|
||
local VLESS_LINK="vless://${UUID}@${VLESS_DOMAIN}:443?encryption=none&security=tls&type=ws&host=${VLESS_DOMAIN}&path=%2F#VPS-Tunnel-VLESS"
|
||
echo -e "\n## “科学上网” VLESS 节点 (部署于: $(date))" >> ${STATE_FILE}
|
||
echo "VLESS_DOMAIN=${VLESS_DOMAIN}" >> ${STATE_FILE}
|
||
echo "VLESS_UUID=${UUID}" >> ${STATE_FILE}
|
||
echo -e "\n${GREEN}✅ VLESS 节点部署成功!${NC}"
|
||
echo -e "${YELLOW}请使用以下链接导入客户端:${NC}"
|
||
echo -e "${CYAN}${VLESS_LINK}${NC}"
|
||
else
|
||
echo -e "${RED}❌ VLESS 节点部署失败!请检查 Docker 是否正常运行。${NC}"
|
||
fi
|
||
echo -e "\n${GREEN}按任意键返回主菜单...${NC}"; read -n 1 -s
|
||
}
|
||
|
||
# 27. 部署 Socks5 代理
|
||
install_socks5_proxy() {
|
||
ensure_docker_installed || return; check_tunnel_installed || return
|
||
clear
|
||
echo -e "${BLUE}--- “Socks5 代理”部署計劃啟動! ---${NC}"
|
||
read -p "請輸入您為 Socks5 代理規劃的子域名 (例如 proxy.example.com): " SOCKS5_DOMAIN
|
||
if [ -z "$SOCKS5_DOMAIN" ]; then echo -e "${RED}錯誤:域名不能為空!${NC}"; sleep 2; return; fi
|
||
|
||
mkdir -p /root/socks5_proxy_data
|
||
cat > /root/socks5_proxy_data/docker-compose.yml <<EOF
|
||
services:
|
||
socks5:
|
||
# 【建材升级】已从 serjs/go-socks5-proxy 替换为维护更积极的 dockage/socks5
|
||
image: dockage/socks5:latest
|
||
container_name: socks5_proxy_app
|
||
restart: unless-stopped
|
||
ports:
|
||
- "127.0.0.1:1080:1080"
|
||
EOF
|
||
|
||
if (cd /root/socks5_proxy_data && sudo docker compose up -d); then
|
||
echo -e "${GREEN}✅ Socks5 代理服務已啟動,等待服務穩定...${NC}"; sleep 5
|
||
echo -e "\n## Socks5 代理憑證 (部署于: $(date))" >> ${STATE_FILE}
|
||
echo "SOCKS5_DOMAIN=${SOCKS5_DOMAIN}" >> ${STATE_FILE}
|
||
echo -e "${GREEN}正在為您配置網絡...${NC}"
|
||
update_tunnel_config "${SOCKS5_DOMAIN}" "socks://127.0.0.1:1080" "Socks5 Proxy"
|
||
|
||
echo -e "\n${GREEN}✅ Socks5 代理部署成功!${NC}"
|
||
echo -e "${YELLOW}請在您的客戶端中使用以下資訊配置代理:${NC}"
|
||
echo -e " 代理類型: ${CYAN}SOCKS5${NC}"
|
||
echo -e " 伺服器地址: ${CYAN}${SOCKS5_DOMAIN}${NC}"
|
||
echo -e " 端口: ${CYAN}443${NC}"
|
||
echo -e " ${YELLOW}注意:需要客戶端支援 Socks5 over TLS/SSL (例如 v2rayN, Clash 等)。${NC}"
|
||
else
|
||
echo -e "${RED}❌ Socks5 代理部署失敗!請檢查 Docker 是否正常運行。${NC}"
|
||
fi
|
||
echo -e "\n${GREEN}按任意键返回主選單...${NC}"; read -n 1 -s
|
||
}
|
||
# 28. Hysteria 2 暴力加速 (集成版 - IPv6 优先/WARP 修正版)
|
||
install_hysteria_node() {
|
||
clear
|
||
echo -e "${BLUE}--- “Hysteria 2 (歇斯底里)” 暴力加速部署启动! ---${NC}"
|
||
echo -e "${YELLOW}注意:此协议需要占用一个公网端口 (UDP),且不走 Cloudflare Tunnel。${NC}"
|
||
echo -e "${YELLOW}请确保您的 VPS 防火墙 (安全组) 已放行您设置的端口!${NC}"
|
||
|
||
# 检查是否已安装
|
||
if command -v hysteria &> /dev/null; then
|
||
echo -e "${RED}检测到 Hysteria 2 已安装!如需重装请先卸载。${NC}"; sleep 3; return
|
||
fi
|
||
|
||
# 1. 安装基础环境
|
||
echo -e "${CYAN}[1/4] 正在安装 Hysteria 2 核心组件...${NC}"
|
||
sudo apt-get update >/dev/null 2>&1
|
||
sudo apt-get install -y curl wget openssl >/dev/null 2>&1
|
||
bash <(curl -fsSL https://get.hy2.sh/)
|
||
|
||
# 2. 配置交互
|
||
echo -e "${CYAN}[2/4] 进行参数配置...${NC}"
|
||
read -p "请设置连接密码 (留空自动生成): " USER_PASS
|
||
[[ -z "$USER_PASS" ]] && USER_PASS=$(openssl rand -base64 16)
|
||
|
||
read -p "请设置端口号 (默认 54321): " USER_PORT
|
||
[[ -z "$USER_PORT" ]] && USER_PORT=54321
|
||
|
||
# 为防止与 Cloudflare Tunnel 的域名冲突,强制使用自签证书模式
|
||
echo -e "${YELLOW}为避免与您的 Tunnel 域名冲突,脚本自动采用“自签证书”模式。${NC}"
|
||
mkdir -p /etc/hysteria
|
||
local CERT_FILE="/etc/hysteria/server.crt"
|
||
local KEY_FILE="/etc/hysteria/server.key"
|
||
openssl req -x509 -nodes -newkey rsa:2048 -keyout $KEY_FILE -out $CERT_FILE -subj "/CN=bing.com" -days 3650 >/dev/null 2>&1
|
||
chmod 644 $CERT_FILE
|
||
chmod 600 $KEY_FILE
|
||
|
||
# 3. 写入配置文件
|
||
cat <<EOF > /etc/hysteria/config.yaml
|
||
listen: :$USER_PORT
|
||
tls:
|
||
cert: $CERT_FILE
|
||
key: $KEY_FILE
|
||
auth:
|
||
type: password
|
||
password: $USER_PASS
|
||
quic:
|
||
initStreamReceiveWindow: 8388608
|
||
maxStreamReceiveWindow: 8388608
|
||
initConnReceiveWindow: 20971520
|
||
maxConnReceiveWindow: 20971520
|
||
EOF
|
||
chown -R hysteria:hysteria /etc/hysteria
|
||
|
||
# 4. 开启 BBR (继承自原脚本)
|
||
echo -e "${CYAN}[3/4] 优化系统网络参数 (BBR)...${NC}"
|
||
if ! grep -q "net.ipv4.tcp_congestion_control=bbr" /etc/sysctl.conf; then
|
||
echo "net.core.default_qdisc=fq" >> /etc/sysctl.conf
|
||
echo "net.ipv4.tcp_congestion_control=bbr" >> /etc/sysctl.conf
|
||
sysctl -p >/dev/null 2>&1
|
||
fi
|
||
|
||
# 启动服务
|
||
systemctl enable --now hysteria-server.service
|
||
|
||
# --- 智能 IP 识别 (修正 WARP 干扰) ---
|
||
echo -e "${CYAN}[4/4] 正在智能检测真实公网 IP...${NC}"
|
||
|
||
# 尝试获取 IPv6 (设置5秒超时)
|
||
local IP_V6=$(curl -s6 -m 5 https://ifconfig.me/ip)
|
||
# 尝试获取 IPv4 (设置5秒超时)
|
||
local IP_V4=$(curl -s4 -m 5 https://ifconfig.me/ip)
|
||
|
||
local FINAL_IP=""
|
||
local SHARE_LINK=""
|
||
|
||
# 逻辑:只要有 IPv6,就强制优先使用 IPv6
|
||
# 原因:对于这就好像 EUserv/Hax 这种机器,IPv4 往往是 WARP 的假 IP,无法入站连接
|
||
if [[ -n "$IP_V6" ]]; then
|
||
echo -e "${GREEN}✔ 检测到 IPv6 地址,将优先使用以避开 WARP 干扰。${NC}"
|
||
FINAL_IP="$IP_V6"
|
||
# IPv6 在链接中必须加上 []
|
||
SHARE_LINK="hysteria2://$USER_PASS@[$FINAL_IP]:$USER_PORT/?sni=bing.com&insecure=1#Hysteria2-IPv6"
|
||
# 为了显示好看,把保存到凭证里的 IP 也加上括号
|
||
DISPLAY_IP="[$FINAL_IP]"
|
||
elif [[ -n "$IP_V4" ]]; then
|
||
echo -e "${YELLOW}未检测到 IPv6,将使用 IPv4 (请注意:如果是 WARP IP 将无法连接)。${NC}"
|
||
FINAL_IP="$IP_V4"
|
||
SHARE_LINK="hysteria2://$USER_PASS@$FINAL_IP:$USER_PORT/?sni=bing.com&insecure=1#Hysteria2-IPv4"
|
||
DISPLAY_IP="$FINAL_IP"
|
||
else
|
||
echo -e "${RED}错误:无法检测到任何公网 IP!${NC}"
|
||
FINAL_IP="<未知IP>"
|
||
DISPLAY_IP="<未知IP>"
|
||
fi
|
||
|
||
# 保存凭证
|
||
echo -e "\n## Hysteria 2 凭证 (部署于: $(date))" >> ${STATE_FILE}
|
||
echo "HY2_IP=${DISPLAY_IP}" >> ${STATE_FILE}
|
||
echo "HY2_PORT=${USER_PORT}" >> ${STATE_FILE}
|
||
echo "HY2_PASSWORD=${USER_PASS}" >> ${STATE_FILE}
|
||
echo "HY2_LINK=${SHARE_LINK}" >> ${STATE_FILE}
|
||
|
||
echo -e "\n${GREEN}✅ Hysteria 2 部署成功!${NC}"
|
||
echo -e "${YELLOW}分享链接已保存到凭证箱 (选项 24)。${NC}"
|
||
|
||
if [[ -n "$IP_V6" ]]; then
|
||
echo -e "${CYAN}提示:已为您自动适配 IPv6 格式 [$FINAL_IP]${NC}"
|
||
echo -e "${CYAN}小白请注意:您的本地网络(手机/电脑)必须支持 IPv6 才能连接此节点!${NC}"
|
||
fi
|
||
|
||
echo -e "\n${GREEN}按任意键返回主菜单...${NC}";
|
||
read -n 1 -s
|
||
}
|
||
|
||
# 41. 整机备份(独立入口)
|
||
run_backup_setup() {
|
||
run_wrap_up_tasks_inner_backup() {
|
||
local script_path="/usr/local/bin/vps_backup.sh"
|
||
echo -e "\n${CYAN}--- 创建终极安全的"冷备份"脚本 ---${NC}"
|
||
echo -e "${YELLOW}这会创建一个带安全停机机制的备份脚本,防止数据库损坏。${NC}"
|
||
read -p "您确定要创建吗? (y/n): " confirm
|
||
[[ "$confirm" != "y" && "$confirm" != "Y" ]] && echo -e "${YELLOW}已取消。${NC}" && return
|
||
local default_backup_path
|
||
# 自动检测已挂载的网盘作为默认备份路径
|
||
local first_mount; first_mount=$(mount | grep "fuse\|sshfs" | awk '{print $3}' | head -1)
|
||
[ -n "$first_mount" ] && default_backup_path="${first_mount}/VPS-Backups" || default_backup_path="/mnt/Backup/VPS-Backups"
|
||
echo -e "${CYAN}检测到可用挂载点,备份默认路径建议: ${GREEN}${default_backup_path}${NC}"
|
||
read -p "留空使用上方默认值: " custom_backup_path
|
||
local BACKUP_DEST_DIR="${custom_backup_path:-$default_backup_path}"
|
||
echo -e "${GREEN}备份目标: ${BACKUP_DEST_DIR}${NC}"; sleep 1
|
||
cat <<EOF > "$script_path"
|
||
#!/bin/bash
|
||
BACKUP_DEST_DIR="${BACKUP_DEST_DIR}"
|
||
LOG_FILE="/var/log/vps_backup.log"
|
||
TIMESTAMP=\$(date +"%Y-%m-%d_%H-%M-%S")
|
||
BACKUP_FILENAME="vps_data_backup_\${TIMESTAMP}.tar.gz"
|
||
LOCAL_TEMP_PATH="/tmp/\${BACKUP_FILENAME}"
|
||
BACKUP_PATHS="/etc /home /usr/local/bin /root/.vps_setup_credentials /root/.cloudflared /root/.config/rclone"
|
||
DATA_DIRS=\$(find /root -maxdepth 1 -type d -name "*_data" 2>/dev/null)
|
||
COMPOSE_FILES=\$(find /root -maxdepth 2 -name "docker-compose.yml" 2>/dev/null)
|
||
log_message() { echo "\$(date +"%Y-%m-%d %H:%M:%S") - \$1" | sudo tee -a \$LOG_FILE; }
|
||
log_message "================== 冷备份任务开始 =================="
|
||
mkdir -p "\$BACKUP_DEST_DIR"
|
||
log_message "正在清理旧备份..."
|
||
find "\$BACKUP_DEST_DIR" -name "vps_data_backup_*.tar.gz" -type f -delete
|
||
log_message "【安全机制】暂停所有容器..."
|
||
docker stop \$(docker ps -q) > /dev/null 2>&1
|
||
log_message "正在打包数据..."
|
||
tar -czf "\$LOCAL_TEMP_PATH" --exclude="*/.cache/*" --ignore-failed-read \$BACKUP_PATHS \$COMPOSE_FILES \$DATA_DIRS
|
||
log_message "【安全机制】唤醒所有容器..."
|
||
docker start \$(docker ps -a -q) > /dev/null 2>&1
|
||
if [ \$? -eq 0 ]; then
|
||
mv "\$LOCAL_TEMP_PATH" "\$BACKUP_DEST_DIR/"
|
||
log_message "✅ 备份完成: \${BACKUP_DEST_DIR}/\${BACKUP_FILENAME}"
|
||
else
|
||
log_message "❌ 打包失败!"; rm -f "\$LOCAL_TEMP_PATH"; exit 1
|
||
fi
|
||
log_message "================== 备份任务结束 =================="
|
||
EOF
|
||
chmod +x "$script_path"
|
||
echo -e "${GREEN}✔ 备份脚本已创建: $script_path${NC}"
|
||
echo ""
|
||
read -p "备份频率 (1=每小时, 2=每天凌晨4点) [默认2]: " freq_choice
|
||
[[ -z "$freq_choice" ]] && freq_choice="2"
|
||
local cron_job
|
||
[[ "$freq_choice" == "1" ]] && cron_job="5 * * * * /bin/bash $script_path" || cron_job="5 4 * * * /bin/bash $script_path"
|
||
(crontab -l 2>/dev/null | grep -v "$script_path"; echo "$cron_job") | crontab -
|
||
echo -e "${GREEN}✔ 定时备份已设置。手动测试: sudo $script_path${NC}"
|
||
read -p "按 Enter 键继续..."
|
||
}
|
||
run_wrap_up_tasks_inner_backup
|
||
echo -e "\n${GREEN}按任意键返回主菜单...${NC}"; read -n 1 -s
|
||
}
|
||
|
||
# 30. 收尾工作与优化脚本
|
||
run_wrap_up_tasks() {
|
||
_patch_vps_installer() {
|
||
local installer_path="/root/vps_installer.sh"
|
||
echo -e "\n${CYAN}--- 步骤 1: 修正 vps_installer.sh 的凭证覆盖 Bug ---${NC}"
|
||
if [ -f "$installer_path" ]; then
|
||
if grep -q '> ${STATE_FILE}' "$installer_path"; then
|
||
sed -i "s/> \${STATE_FILE}/>> \${STATE_FILE}/g" "$installer_path"
|
||
sed -i "s/echo \"## Nextcloud 套件憑證.*>>/>> \${STATE_FILE}/echo \"## Nextcloud 套件憑證 ( 部署于 : \$(date))\" > \${STATE_FILE}/" "$installer_path"
|
||
echo -e "${GREEN}✔ 已自動修補 $installer_path,解決了憑證文件被覆蓋的問題。${NC}"
|
||
else
|
||
echo -e "${YELLOW}ℹ️ 檢測到 vps_installer.sh 似乎已經被修正過,跳過此步驟。${NC}"
|
||
fi
|
||
else
|
||
echo -e "${YELLOW}⚠️ 未在 $installer_path 找到安裝腳本,跳過修補步驟。${NC}"
|
||
fi
|
||
read -p "按 Enter 鍵繼續..."
|
||
}
|
||
|
||
_create_ai_script() {
|
||
local script_path="/usr/local/bin/ask_ai_a_question.sh"; local cron_job="0 * * * * /bin/bash -c '/bin/sleep \$((RANDOM % 3600)) && $script_path'"
|
||
echo -e "\n${CYAN}--- 步驟 3: 創建並部署“AI 智能提問官”腳本 (日誌增強版) ---${NC}"
|
||
echo -e "${YELLOW}這將創建一個腳本並設置定時任務,每小時隨機向您的 AI 提問,並將對話記錄到日誌。${NC}"
|
||
read -p "您確定要創建嗎? (y/n): " confirm
|
||
if [[ "$confirm" == "y" || "$confirm" == "Y" ]]; then
|
||
cat <<'EOF' > "$script_path"
|
||
#!/bin/bash
|
||
QUESTIONS=( "请写一首关于悉尼歌剧院的诗。" "用一个简单的比喻解释一下什么是“量子计算”。" "给我讲一个关于程序员的简短冷笑话。" "如果AI统治了世界,人类最好的结局会是什么?" "推荐一道适合在办公室当午餐的、简单的食谱。" "澳大利亚历史上有什么不出名但却非常有趣的事件吗?" )
|
||
MODEL_NAME="llama3:8b"; LOG_FILE="/var/log/ask_ai.log"
|
||
RANDOM_QUESTION=${QUESTIONS[$((RANDOM % ${#QUESTIONS[@]}))]}
|
||
echo "==================================================" >> "$LOG_FILE"
|
||
echo "時間: $(date)" >> "$LOG_FILE"; echo "提問: $RANDOM_QUESTION" >> "$LOG_FILE"; echo "回答:" >> "$LOG_FILE"
|
||
echo "$RANDOM_QUESTION" | /usr/bin/docker exec -i ollama ollama run "$MODEL_NAME" >> "$LOG_FILE" 2>&1
|
||
echo "" >> "$LOG_FILE"
|
||
EOF
|
||
chmod +x "$script_path"; echo -e "${GREEN}✔ 已成功創建腳本: $script_path${NC}"
|
||
(crontab -l 2>/dev/null | grep -v "$script_path"; echo "$cron_job") | crontab -
|
||
echo -e "${GREEN}✔ 已成功設置定時任務 (crontab)。${NC}"; echo -e "${YELLOW}您可以通過 'sudo $script_path' 來手動測試它。${NC}"; echo -e "${YELLOW}測試後,使用 'cat /var/log/ask_ai.log' 來查看 AI 的回答。${NC}"
|
||
else echo -e "${YELLOW}已跳過創建 AI 腳本。${NC}"; fi
|
||
read -p "按 Enter 鍵繼續..."
|
||
}
|
||
_create_backup_script() {
|
||
local script_path="/usr/local/bin/vps_backup.sh"
|
||
echo -e "\n${CYAN}--- 步骤 4: 创建终极安全的“冷备份”脚本 ---${NC}"
|
||
echo -e "${YELLOW}这会创建一个带“安全停机机制”的备份脚本,彻底杜绝数据库损坏。${NC}"
|
||
read -p "您确定要创建吗? (y/n): " confirm
|
||
if [[ "$confirm" == "y" || "$confirm" == "Y" ]]; then
|
||
local default_backup_path="/mnt/onedrive/VPS-Backups"
|
||
echo -e "${CYAN}请输入您希望在 Rclone 网盘中存放备份的目标文件夹路径。${NC}"
|
||
read -p "留空将使用默认值 [${default_backup_path}]: " custom_backup_path
|
||
local BACKUP_DEST_DIR="${custom_backup_path:-$default_backup_path}"
|
||
echo -e "${GREEN}备份目标文件夹已成功设置为: ${BACKUP_DEST_DIR}${NC}"; sleep 2
|
||
|
||
cat <<EOF > "$script_path"
|
||
#!/bin/bash
|
||
BACKUP_DEST_DIR="${BACKUP_DEST_DIR}"
|
||
LOG_FILE="/var/log/vps_backup.log"; TIMESTAMP=\$(date +"%Y-%m-%d_%H-%M-%S")
|
||
BACKUP_FILENAME="vps_data_backup_\${TIMESTAMP}.tar.gz";
|
||
LOCAL_TEMP_PATH="/tmp/\${BACKUP_FILENAME}"
|
||
|
||
BACKUP_PATHS="/etc /home /usr/local/bin /root/.vps_setup_credentials /root/.cloudflared /root/.config/rclone"
|
||
DATA_DIRS=\$(find /root -maxdepth 1 -type d -name "*_data" 2>/dev/null)
|
||
COMPOSE_FILES=\$(find /root -maxdepth 2 -name "docker-compose.yml" 2>/dev/null)
|
||
|
||
log_message() { echo "\$(date +"%Y-%m-%d %H:%M:%S") - \$1" | sudo tee -a \$LOG_FILE; }
|
||
|
||
log_message "================== 应用数据冷备份任务开始 =================="
|
||
mkdir -p "\$BACKUP_DEST_DIR"
|
||
log_message "正在清理旧备份...";
|
||
find "\$BACKUP_DEST_DIR" -name "vps_data_backup_*.tar.gz" -type f -delete
|
||
|
||
# [核心优化] 备份前安全暂停所有 Docker 容器,防止数据写入产生脏数据
|
||
log_message "【安全机制】正在暂停所有容器运行..."
|
||
docker stop \$(docker ps -q) > /dev/null 2>&1
|
||
|
||
log_message "正在打包核心配置与应用数据..."
|
||
tar -czf "\$LOCAL_TEMP_PATH" \
|
||
--exclude="*/.cache/*" \
|
||
--exclude="*/.cache" \
|
||
--ignore-failed-read \
|
||
\$BACKUP_PATHS \$COMPOSE_FILES \$DATA_DIRS
|
||
|
||
# [核心优化] 打包完成后,立即唤醒所有容器
|
||
log_message "【安全机制】打包完成,正在唤醒所有容器恢复服务..."
|
||
docker start \$(docker ps -a -q) > /dev/null 2>&1
|
||
|
||
if [ \$? -eq 0 ]; then
|
||
log_message "正在上传到云盘..."
|
||
mv "\$LOCAL_TEMP_PATH" "\$BACKUP_DEST_DIR/";
|
||
if [ \$? -eq 0 ]; then
|
||
log_message "✅ 上传成功!文件位于: \${BACKUP_DEST_DIR}/\${BACKUP_FILENAME}";
|
||
else
|
||
log_message "❌ 错误:上传失败!请检查挂载点。"; rm -f "\$LOCAL_TEMP_PATH"; exit 1;
|
||
fi
|
||
else
|
||
log_message "❌ 错误:打包失败!"; rm -f "\$LOCAL_TEMP_PATH"; exit 1;
|
||
fi
|
||
log_message "================== 备份任务结束 =================="
|
||
EOF
|
||
chmod +x "$script_path";
|
||
echo -e "${GREEN}✔ 已成功创建安全的冷备份脚本: $script_path${NC}"
|
||
|
||
echo -e "${CYAN}--- 接下来, 设置自动备份定时任务 ---${NC}"
|
||
read -p "请选择备份频率 (1=每小时, 2=每天) [默认: 2]: " freq_choice
|
||
read -p "是否随机化执行时间 (y/n) [默认: y]: " rand_choice
|
||
|
||
[[ -z "$freq_choice" ]] && freq_choice="2"
|
||
[[ -z "$rand_choice" ]] && rand_choice="y"
|
||
|
||
local cron_job=""
|
||
local message=""
|
||
local full_command="/bin/bash $script_path"
|
||
|
||
if [[ "$freq_choice" == "1" ]]; then
|
||
if [[ "$rand_choice" == "y" || "$rand_choice" == "Y" ]]; then
|
||
cron_job="0 * * * * /bin/bash -c '/bin/sleep \$((RANDOM \\% 3540)) && $full_command'"
|
||
message="✔ 已设置定时备份 (每小时随机一次)。"
|
||
else
|
||
cron_job="5 * * * * $full_command"
|
||
message="✔ 已设置定时备份 (每小时的第5分钟)。"
|
||
fi
|
||
else
|
||
if [[ "$rand_choice" == "y" || "$rand_choice" == "Y" ]]; then
|
||
cron_job="0 0 * * * /bin/bash -c '/bin/sleep \$((RANDOM \\% 86000)) && $full_command'"
|
||
message="✔ 已设置定时备份 (每天随机时间)。"
|
||
else
|
||
cron_job="5 4 * * * $full_command"
|
||
message="✔ 已设置定时备份 (每天 4:05 AM)。"
|
||
fi
|
||
fi
|
||
|
||
(crontab -l 2>/dev/null | grep -v "$script_path"; echo "$cron_job") | crontab -
|
||
echo -e "${GREEN}${message}${NC}"
|
||
echo -e "${YELLOW}您可以随时输入 'sudo $script_path' 手动触发安全备份测试。${NC}"
|
||
else
|
||
echo -e "${YELLOW}已跳过创建备份脚本。${NC}";
|
||
fi
|
||
read -p "按 Enter 键继续..."
|
||
}
|
||
|
||
while true; do
|
||
clear
|
||
echo -e "${BLUE}══════════════════════════════════════════════════════════════${NC}"
|
||
echo -e "${BLUE} 🔧 收尾工作与优化脚本${NC}"
|
||
echo -e "${BLUE}══════════════════════════════════════════════════════════════${NC}"
|
||
echo ""
|
||
local ai_status="[ 依赖: AI 大脑 (选项 5.1) ]"
|
||
[ -d "/root/ai_stack" ] && ai_status="${GREEN}[ ✅ 依赖已满足 ]${NC}"
|
||
printf " ${YELLOW}3)${NC} %-42s %b\n" "部署 AI 智能提问官脚本" "${ai_status}"
|
||
echo -e " ${YELLOW}4)${NC} 创建整机冷备份脚本(推荐!)"
|
||
echo ""
|
||
echo -e " ${YELLOW}b)${NC} 返回主菜单"
|
||
echo ""
|
||
read -p " 请输入您的选择: " task_choice
|
||
case $task_choice in
|
||
3) if [ ! -d "/root/ai_stack" ]; then echo -e "${RED}错误:AI 大脑未安装。${NC}"; sleep 3; else _create_ai_script; fi ;;
|
||
4) _create_backup_script ;;
|
||
b|B) break ;; *) echo -e "${RED}无效的选项。${NC}"; sleep 2 ;;
|
||
esac
|
||
done
|
||
}
|
||
|
||
# 31. 甲骨文开机助手 (oci-helper)
|
||
install_oci_helper() {
|
||
clear
|
||
echo -e "${BLUE}--- “甲骨文开机助手 (oci-helper)” 部署向导 ---${NC}"
|
||
echo -e "${YELLOW}此功能将从 GitHub 下载并执行 oci-helper 的官方一键安装脚本。${NC}"
|
||
echo -e "${YELLOW}请确保您的 VPS 是甲骨文云 (Oracle Cloud Infrastructure) 服务器。${NC}"
|
||
read -p "您确定要继续吗? (y/n): " confirm
|
||
if [[ "$confirm" == "y" || "$confirm" == "Y" ]]; then
|
||
echo -e "${GREEN}正在运行 oci-helper 安装脚本...${NC}"
|
||
bash <(wget -qO- https://github.com/Yohann0617/oci-helper/releases/latest/download/sh_oci-helper_install.sh)
|
||
echo -e "\n${GREEN}✅ oci-helper 脚本执行完毕。${NC}"
|
||
echo -e "${CYAN}请根据其自身的提示进行后续操作。${NC}"
|
||
else
|
||
echo -e "${GREEN}操作已取消。${NC}"
|
||
fi
|
||
echo -e "\n${GREEN}按任意键返回主菜单...${NC}"; read -n 1 -s
|
||
}
|
||
|
||
# 32. 服务器每日管家报告系统
|
||
install_mail_report() {
|
||
clear
|
||
echo -e "${BLUE}--- “服务器每日管家”报告系统安装程序 ---${NC}"
|
||
echo -e "${CYAN}正在检查并安装专业邮件工具 (msmtp, vnstat, cron)...${NC}"
|
||
|
||
# 声明非交互模式,防止 mailutils/postfix 弹出配置界面导致静默安装卡死
|
||
export DEBIAN_FRONTEND=noninteractive
|
||
apt-get update >/dev/null 2>&1
|
||
apt-get install -y msmtp msmtp-mta vnstat mailutils cron >/dev/null 2>&1
|
||
unset DEBIAN_FRONTEND
|
||
|
||
echo -e "${GREEN}✅ 所需工具已安装完毕。${NC}"; echo "-------------------------------------------------"
|
||
echo -e "${CYAN}请输入您的发件邮箱配置 (用于发送报告):${NC}"
|
||
echo -e "${YELLOW}重要提示: 请务必使用您邮箱的“应用密码”或“授权码”!${NC}"
|
||
read -p "请输入您的邮箱地址 (例如: yourname@gmail.com): " mail_user
|
||
read -sp "请输入上面邮箱的“应用密码”或“授权码”: " mail_pass; echo
|
||
read -p "请输入邮箱的 SMTP 服务器地址 (例如: smtp.gmail.com): " mail_server
|
||
read -p "请输入 SMTP 端口号 (AOL/Gmail/QQ等通常为 587,默认为 587): " mail_port; mail_port=${mail_port:-587}
|
||
read -p "请输入接收报告的邮箱地址 (可以和上面相同): " to_email
|
||
echo -e "${CYAN}正在配置专业邮件发送服务 (msmtp)...${NC}"
|
||
|
||
# [修复] 恢复标准的换行格式,msmtprc 不支持用分号将多条配置写在同一行
|
||
cat > /etc/msmtprc <<EOF
|
||
defaults
|
||
auth on
|
||
tls on
|
||
tls_starttls on
|
||
tls_trust_file /etc/ssl/certs/ca-certificates.crt
|
||
logfile ~/.msmtp.log
|
||
account default
|
||
host $mail_server
|
||
port $mail_port
|
||
from $mail_user
|
||
user $mail_user
|
||
password $mail_pass
|
||
EOF
|
||
chmod 600 /etc/msmtprc; echo -e "${GREEN}✅ 邮件发送服务 (msmtp) 配置完毕。${NC}"; echo "-------------------------------------------------"
|
||
|
||
echo -e "${CYAN}正在任命新的邮件管家 (msmtp)...${NC}"
|
||
# [修复] 预先创建文件,消除 grep 找不到文件的报错警告
|
||
touch /etc/mail.rc
|
||
if ! grep -q "set mta=/usr/bin/msmtp" "/etc/mail.rc"; then echo "set mta=/usr/bin/msmtp" >> "/etc/mail.rc"; fi
|
||
echo -e "${GREEN}✅ 系统 mail 命令现在将由 msmtp 负责!${NC}"; echo "-------------------------------------------------"
|
||
|
||
echo -e "${CYAN}正在创建每日报告生成脚本...${NC}"
|
||
local report_script_path="/usr/local/bin/daily_server_report.sh"
|
||
cat > $report_script_path <<'EOF'
|
||
#!/bin/bash
|
||
HOSTNAME=$(hostname); CURRENT_TIME=$(date "+%Y-%m-%d %H:%M:%S"); UPTIME=$(uptime -p); LAST_REBOOT=$(who -b | awk '{print $3, $4}')
|
||
if [ -f "/var/log/auth.log" ]; then FAILED_LOGINS=$(grep -c "Failed password" /var/log/auth.log || echo "0"); else FAILED_LOGINS="N/A"; fi
|
||
[ -z "$FAILED_LOGINS" ] && FAILED_LOGINS=0
|
||
if ! systemctl is-active --quiet vnstat; then systemctl enable vnstat > /dev/null 2>&1 && systemctl start vnstat && sleep 5; fi
|
||
INTERFACE=$(ip -o -4 route show to default | awk '{print $5}'); ! vnstat --iflist | grep -q "$INTERFACE" && vnstat -u -i $INTERFACE
|
||
TRAFFIC_INFO=$(vnstat -d 1); SUBJECT="【服务器管家报告】来自 $HOSTNAME - $(date "+%Y-%m-%d")"
|
||
HTML_BODY="<html><body><h2 style='color:#2c3e50;'>服务器每日管家报告</h2><p><b>主机名:</b> $HOSTNAME</p><p><b>报告时间:</b> $CURRENT_TIME</p><hr><h3>核心状态摘要:</h3><ul><li><b>已持续运行:</b> $UPTIME</li><li><b>最近启动于:</b> $LAST_REBOOT</li><li><b>SSH 登录失败次数 (今日):</b> <strong style='color:red;'>$FAILED_LOGINS 次</strong></li></ul><hr><h3>今日网络流量报告 (由 vnstat 提供):</h3><pre style='background-color:#f5f5f5; padding:10px; border-radius:5px; font-family:monospace;'>$TRAFFIC_INFO</pre><p style='font-size:12px; color:#7f8c8d;'>提示: 如果 vnstat 报告显示 'Not enough data available yet',这是正常的,请等待24小时后它才能收集到完整数据。</p></body></html>"
|
||
echo "$HTML_BODY" | mail -s "$SUBJECT" -a "Content-Type: text/html" "$1"
|
||
EOF
|
||
chmod +x $report_script_path; echo -e "${GREEN}✅ 报告生成脚本已创建。${NC}"; echo "-------------------------------------------------"
|
||
|
||
echo -e "${CYAN}正在设置定时任务,让脚本在每天 23:30 自动运行...${NC}"
|
||
(crontab -l 2>/dev/null | grep -v "$report_script_path" ; echo "30 23 * * * $report_script_path $to_email") | crontab -
|
||
echo -e "${GREEN}✅ 定时任务已设置成功!${NC}"; echo "-------------------------------------------------"
|
||
|
||
echo -e "${CYAN}正在发送最终测试邮件到 $to_email...${NC}"
|
||
echo "这是一封来自【服务器每日管家】最终版脚本的安装成功测试邮件!" | mail -s "【服务器管家】安装成功测试" $to_email
|
||
if [ $? -eq 0 ]; then
|
||
echo -e "${GREEN}★★★★★ 系统安装完毕!一切完美!★★★★★${NC}"; echo -e "${CYAN}测试邮件已成功发送!请检查您的邮箱。${NC}"; echo -e "${CYAN}第一份正式报告将在今晚 23:30 发送给您。${NC}"
|
||
else
|
||
echo -e "${RED}最终测试邮件发送失败!请检查您的邮箱配置是否正确。${NC}"
|
||
echo -e "${YELLOW}如遇报错,请退出脚本后输入 'cat ~/.msmtp.log' 查看具体错误原因。${NC}"
|
||
fi
|
||
echo -e "\n${GREEN}按任意键返回主菜单...${NC}"; read -n 1 -s
|
||
}
|
||
|
||
# X. 一键深度清理
|
||
system_cleanup() {
|
||
clear; echo -e "${BLUE}--- 深度清理与系统优化 ---${NC}\n${YELLOW}即将开始一套大扫除...${NC}"; sleep 2
|
||
echo -e "\n${CYAN}🧹 [1/4] 正在清扫系统更新缓存...${NC}"; sudo apt-get clean; sudo apt-get autoremove -y > /dev/null 2>&1; echo -e "${GREEN}✅ 清理完毕!${NC}"; sleep 1
|
||
echo -e "\n${CYAN}📦 [2/4] 正在清理 Docker 环境...${NC}"; docker system prune -f > /dev/null 2>&1; echo -e "${GREEN}✅ 清理完毕!${NC}"; sleep 1
|
||
echo -e "\n${CYAN}📜 [3/4] 正在压缩系统日志...${NC}"; sudo journalctl --vacuum-size=50M > /dev/null 2>&1; echo -e "${GREEN}✅ 优化完毕!${NC}"; sleep 1
|
||
echo -e "\n${CYAN}💧 [4/4] 正在释放内存缓存...${NC}"; sudo sync && sudo sh -c 'echo 3 > /proc/sys/vm/drop_caches'; echo -e "${GREEN}✅ 释放完毕!${NC}"; sleep 1
|
||
echo -e "\n${GREEN}✨ 系统深度清理完成!${NC}"; echo -e "${YELLOW}当前内存状态:${NC}"; free -h
|
||
echo -e "\n${GREEN}按任意键返回主菜单 ...${NC}"; read -n 1 -s
|
||
}
|
||
|
||
# 99. 一键卸载鸡公头
|
||
uninstall_everything() {
|
||
clear
|
||
read -p "为确认执行此终极毁灭操作,请输入【yEs-i-aM-sUrE】: " confirmation
|
||
if [[ "$confirmation" != "yEs-i-aM-sUrE" ]]; then echo -e "${GREEN}操作已取消。${NC}"; sleep 3; return; fi
|
||
|
||
echo -e "${RED}正在拆除所有由本脚本安装的服务...${NC}"
|
||
if command -v docker &> /dev/null; then sudo docker system prune -a --volumes -f; fi
|
||
if systemctl is-active --quiet "rclone-vps-mount.service"; then sudo systemctl stop rclone-vps-mount.service; sudo systemctl disable rclone-vps-mount.service; sudo rm -f /etc/systemd/system/rclone-vps-mount.service; fi
|
||
if systemctl is-active --quiet "cloudflared"; then sudo systemctl stop cloudflared; sudo systemctl disable cloudflared; fi
|
||
|
||
echo -e "${RED}正在清理所有数据和配置文件...${NC}"
|
||
sudo rm -rf /root/*_data /root/ai_stack /root/vless_data /root/socks5_proxy_data /root/sillytavern_data /root/n8n_data /root/popodash /root/.config/rclone /root/.cloudflared /mnt/*
|
||
if [ -f "/etc/xrdp/xrdp.ini" ]; then
|
||
local desktop_user=$(grep 'RDP_USER' ${STATE_FILE} 2>/dev/null | cut -d'=' -f2)
|
||
if [ -n "$desktop_user" ] && id "$desktop_user" &>/dev/null; then sudo deluser --remove-home "$desktop_user" &>/dev/null; fi
|
||
sudo apt-get purge -y xrdp xfce4* &>/dev/null; sudo rm -f /root/.xsession
|
||
fi
|
||
sudo apt-get autoremove -y &>/dev/null
|
||
|
||
echo -e "${RED}正在清理脚本自身...${NC}"
|
||
sudo rm -f ${STATE_FILE} ${RCLONE_LOG_FILE} /usr/local/bin/jigongtou
|
||
|
||
echo -e "\n${GREEN}✅ 终极还原完成。建议重启服务器。${NC}"
|
||
rm -- "$0"
|
||
exit 0
|
||
}
|
||
|
||
|
||
# --- 新增功能:部署 Syncthing 同步引擎 ---
|
||
install_syncthing() {
|
||
clear
|
||
echo -e "${BLUE}--- “Syncthing 跨服同步引擎”部署计划启动! ---${NC}"
|
||
|
||
# 1. 安装软件
|
||
if ! command -v syncthing &> /dev/null; then
|
||
echo -e "${YELLOW}正在安装 Syncthing 主程序...${NC}"
|
||
sudo apt-get update && sudo apt-get install -y syncthing
|
||
fi
|
||
|
||
# 2. 预初始化配置
|
||
echo -e "${YELLOW}正在进行静默初始化...${NC}"
|
||
pkill syncthing
|
||
syncthing --no-browser > /dev/null 2>&1 &
|
||
sleep 3
|
||
pkill syncthing
|
||
|
||
# 3. 核心配置修改:允许通过域名访问、跳过主机检查、放行 127.0.0.1
|
||
sed -i 's/127.0.0.1:8384/127.0.0.1:8384/g' ~/.config/syncthing/config.xml
|
||
if ! grep -q "insecureSkipHostcheck" ~/.config/syncthing/config.xml; then
|
||
sed -i '/<gui/a \ <insecureSkipHostcheck>true</insecureSkipHostcheck>' ~/.config/syncthing/config.xml
|
||
fi
|
||
|
||
# 4. 自动配置网络隧道
|
||
check_tunnel_installed || return
|
||
read -p "请输入您为 Syncthing 面板规划的域名 (例如 sync.jigongtou.uepopo.com): " SYNC_DOMAIN
|
||
if [ -z "$SYNC_DOMAIN" ]; then echo -e "${RED}域名不能为空!${NC}"; return; fi
|
||
|
||
update_tunnel_config "${SYNC_DOMAIN}" "http://127.0.0.1:8384" "Syncthing GUI"
|
||
|
||
# 5. 放行传输端口 (22000)
|
||
echo -e "${YELLOW}正在放行 Syncthing 传输端口 (22000)...${NC}"
|
||
sudo ufw allow 22000/tcp > /dev/null 2>&1
|
||
sudo ufw allow 22000/udp > /dev/null 2>&1
|
||
|
||
# 6. 设置后台开机自启 (使用 Systemd)
|
||
echo -e "${YELLOW}正在配置 Systemd 服务以确保稳定运行...${NC}"
|
||
sudo tee /etc/systemd/system/syncthing@root.service > /dev/null <<EOF
|
||
[Unit]
|
||
Description=Syncthing - Open Source Continuous File Synchronization for %i
|
||
Documentation=man:syncthing(1)
|
||
After=network.target
|
||
|
||
[Service]
|
||
User=%i
|
||
ExecStart=/usr/bin/syncthing --no-browser --no-restart --logflags=0
|
||
Restart=on-failure
|
||
SuccessExitStatus=3 4
|
||
RestartForceExitStatus=3 4
|
||
|
||
[Install]
|
||
WantedBy=multi-user.target
|
||
EOF
|
||
|
||
sudo systemctl daemon-reload
|
||
sudo systemctl enable --now syncthing@root.service
|
||
|
||
echo -e "\n${GREEN}✅ Syncthing 部署成功!${NC}"
|
||
echo -e "${CYAN}访问域名: https://${SYNC_DOMAIN}${NC}"
|
||
echo -e "${YELLOW}提示:请立即在网页中 [操作->设置->图形用户界面] 设置用户名和密码!${NC}"
|
||
|
||
echo -e "\n## Syncthing 凭证 (部署于: $(date))" >> ${STATE_FILE}
|
||
echo "SYNCTHING_DOMAIN=${SYNC_DOMAIN}" >> ${STATE_FILE}
|
||
|
||
echo -e "\n${GREEN}按任意键返回主菜单...${NC}"; read -n 1 -s
|
||
}
|
||
|
||
# ================================================================
|
||
# --- ⬇️ ⬇️ ⬇️ 請將以下所有代碼添加到 main() 函數之前 ⬇️ ⬇️ ⬇️ ---
|
||
# ================================================================
|
||
|
||
# --- 應用按需卸載助手 (Docker 應用) ---
|
||
_uninstall_docker_app() {
|
||
local s_name="$1"
|
||
local s_path="$2"
|
||
local s_credential_header="$3"
|
||
local s_tunnel_comment="$4"
|
||
|
||
if [ ! -d "$s_path" ]; then
|
||
echo -e "\n${YELLOW}應用 【${s_name}】 未安裝,無需卸載。${NC}"; sleep 2; return
|
||
fi
|
||
|
||
echo -e "\n${RED}--- 警告 ---${NC}"
|
||
echo -e "${YELLOW}您確定要徹底刪除 【${s_name}】 嗎?${NC}"
|
||
echo -e "${YELLOW}這將會停止並刪除其 Docker 容器,並永久刪除其所有數據 (位於 ${s_path})。${NC}"
|
||
read -p "請確認 (y/n): " confirm
|
||
|
||
if [[ "$confirm" != "y" && "$confirm" != "Y" ]]; then
|
||
echo -e "${GREEN}操作已取消。${NC}"; sleep 2; return
|
||
fi
|
||
|
||
echo -e "\n${CYAN}1/4: 正在停止並刪除 ${s_name} 的 Docker 容器...${NC}"
|
||
(cd ${s_path} && sudo docker compose down -v) > /dev/null 2>&1
|
||
|
||
echo -e "${CYAN}2/4: 正在刪除 ${s_name} 的數據文件夾: ${s_path}${NC}"
|
||
sudo rm -rf ${s_path}
|
||
|
||
if [ -n "$s_credential_header" ] && [ -f "${STATE_FILE}" ]; then
|
||
echo -e "${CYAN}3/4: 正在清理 ${s_name} 的憑證...${NC}"
|
||
# 使用 awk 按标题块清理凭证:匹配到目标标题后开始删除,遇到下一个 ## 标题时停止
|
||
# p=1 默认保留;遇到目标头 p=0 开始删除(含该行);遇到其他 ## 头 p=1 恢复保留
|
||
sudo awk -v header="${s_credential_header}" '
|
||
$0 ~ header { p=0; next }
|
||
/^## / { p=1 }
|
||
p { print }
|
||
' "${STATE_FILE}" > "${STATE_FILE}.tmp" && sudo mv "${STATE_FILE}.tmp" "${STATE_FILE}"
|
||
# 清理可能残留的空行
|
||
sudo awk 'NF' "${STATE_FILE}" > "${STATE_FILE}.tmp" && sudo mv "${STATE_FILE}.tmp" "${STATE_FILE}"
|
||
else
|
||
echo -e "${YELLOW}3/4: 未找到憑證標頭,跳過憑證清理。${NC}"
|
||
fi
|
||
|
||
if [ -n "$s_tunnel_comment" ] && [ -f "$TUNNEL_CONFIG_FILE" ]; then
|
||
echo -e "${CYAN}4/4: 正在清理 ${s_name} 的 Tunnel 域名配置...${NC}"
|
||
# 刪除從註釋行到服務行的整個塊
|
||
sudo sed -i -e "/# ${s_tunnel_comment}/,/service: .*/{/./d;}" "$TUNNEL_CONFIG_FILE"
|
||
else
|
||
echo -e "${YELLOW}4/4: 未找到 Tunnel 註釋,跳過 Tunnel 清理。${NC}"
|
||
fi
|
||
|
||
echo -e "\n${GREEN}✅ 【${s_name}】 已徹底卸載。${NC}"
|
||
echo -e "${YELLOW}正在重啟 Cloudflare Tunnel 以使配置生效...${NC}"
|
||
sudo systemctl restart cloudflared
|
||
sleep 3
|
||
}
|
||
|
||
# --- 應用按需卸載助手 (系統服務) ---
|
||
_uninstall_system_service() {
|
||
local s_name="$1"
|
||
local s_service="$2"
|
||
local s_package="$3"
|
||
local s_config_files=($4) # 接收一個文件路徑數組
|
||
|
||
if ! systemctl is-active --quiet "$s_service" 2>/dev/null; then
|
||
echo -e "\n${YELLOW}服務 【${s_name}】 未安裝或未運行,無需卸載。${NC}"; sleep 2; return
|
||
fi
|
||
|
||
read -p "您確定要卸載 ${s_name} 嗎? (y/n): " confirm
|
||
if [[ "$confirm" != "y" && "$confirm" != "Y" ]]; then
|
||
echo -e "${GREEN}操作已取消。${NC}"; sleep 2; return
|
||
fi
|
||
|
||
echo -e "${YELLOW}正在停止並卸載 ${s_name} (包: ${s_package})...${NC}"
|
||
sudo systemctl stop "$s_service"
|
||
sudo systemctl disable "$s_service"
|
||
sudo apt-get purge -y "$s_package"
|
||
|
||
if [ ${#s_config_files[@]} -gt 0 ]; then
|
||
echo -e "${YELLOW}正在清理 ${s_name} 的殘留配置文件...${NC}"
|
||
for file in "${s_config_files[@]}"; do
|
||
sudo rm -f "$file"
|
||
done
|
||
fi
|
||
|
||
echo -e "\n${GREEN}✅ 【${s_name}】 已卸載。${NC}"
|
||
sleep 3
|
||
}
|
||
|
||
# --- 應用按需卸載助手 (Rclone) ---
|
||
_uninstall_rclone() {
|
||
if [ ! -f "${RCLONE_CONFIG_FILE}" ]; then
|
||
echo -e "\n${YELLOW}Rclone 未配置,無需卸載。${NC}"; sleep 2; return
|
||
fi
|
||
read -p "您確定要卸載 Rclone 嗎? (這將刪除您的配置和掛載服務) (y/n): " confirm
|
||
if [[ "$confirm" == "y" || "$confirm" == "Y" ]]; then
|
||
echo -e "${YELLOW}正在停止 Rclone 掛載服務...${NC}"
|
||
sudo systemctl stop rclone-vps-mount.service &>/dev/null
|
||
sudo systemctl disable rclone-vps-mount.service &>/dev/null
|
||
sudo rm -f /etc/systemd/system/rclone-vps-mount.service
|
||
echo -e "${YELLOW}正在刪除 Rclone 配置文件...${NC}"
|
||
sudo rm -rf "$(dirname "$RCLONE_CONFIG_FILE")"
|
||
sudo sed -i '/^RCLONE_REMOTE/d' ${STATE_FILE}
|
||
sudo sed -i '/^RCLONE_MOUNT_PATH/d' ${STATE_FILE}
|
||
echo -e "${GREEN}✅ Rclone 已卸載。${NC}"
|
||
sleep 3
|
||
else
|
||
echo -e "${GREEN}操作已取消。${NC}"; sleep 2
|
||
fi
|
||
}
|
||
|
||
# --- 應用按需卸載助手 (XFCE 遠程桌面) ---
|
||
_uninstall_xrdp() {
|
||
if [ ! -f "/etc/xrdp/xrdp.ini" ]; then
|
||
echo -e "\n${YELLOW}遠程工作台未安裝,無需卸載。${NC}"; sleep 2; return
|
||
fi
|
||
read -p "您確定要卸載 XFCE 遠程工作台嗎? (y/n): " confirm
|
||
if [[ "$confirm" == "y" || "$confirm" == "Y" ]]; then
|
||
echo -e "${YELLOW}正在停止並卸載 XFCE 和 XRDP...${NC}"
|
||
sudo systemctl stop xrdp
|
||
sudo apt-get purge -y xfce4* xrdp
|
||
local desktop_user=$(grep 'RDP_USER' ${STATE_FILE} 2>/dev/null | cut -d'=' -f2)
|
||
if [ -n "$desktop_user" ] && id "$desktop_user" &>/dev/null; then
|
||
echo -e "${YELLOW}正在刪除您之前創建的桌面用戶: ${desktop_user}...${NC}"
|
||
sudo deluser --remove-home "$desktop_user" &>/dev/null
|
||
fi
|
||
sudo rm -f /root/.xsession
|
||
sudo sed -i '/^## 遠程工作台/,+1d' ${STATE_FILE} # 刪除憑證
|
||
echo -e "${GREEN}✅ 遠程工作台已卸載。${NC}"
|
||
sleep 3
|
||
else
|
||
echo -e "${GREEN}操作已取消。${NC}"; sleep 2
|
||
fi
|
||
}
|
||
|
||
|
||
# --- “应用卸载中心”主菜单 ---
|
||
show_uninstall_menu() {
|
||
while true; do
|
||
clear
|
||
echo -e "${RED}==================== 💣 应用卸载中心 💣 ====================${NC}"
|
||
echo -e "${YELLOW}请选择您要卸载的应用。此操作将删除其所有数据且不可逆!${NC}"
|
||
echo -e "${YELLOW}══════════════════════════════════════════════════════════════════════${NC}"
|
||
|
||
# --- 显示部分 (只负责打印) ---
|
||
check_and_display "1" "Cloudflare Tunnel (核心)" "service" "cloudflared"
|
||
check_and_display "2" "Rclone 数据同步桥" "file" "${RCLONE_CONFIG_FILE}"
|
||
check_and_display "3" "挂载远程VPS网盘 (SSHFS)" "file" "/usr/bin/sshfs"
|
||
echo " ---"
|
||
check_and_display "4.1" "Nextcloud" "dir" "/root/nextcloud_data"
|
||
check_and_display "4.2" "OnlyOffice" "dir" "/root/onlyoffice_data"
|
||
check_and_display "4.3" "Home Assistant" "dir" "/root/home_assistant_data"
|
||
check_and_display "4.4" "WordPress 个人博客" "dir" "/root/wordpress_data"
|
||
check_and_display "5.1" "AI 大脑 (Ollama+WebUI)" "dir" "/root/ai_stack"
|
||
check_and_display "5.3" "Jellyfin 家庭影院" "dir" "/root/jellyfin_data"
|
||
check_and_display "5.4" "Navidrome 音乐服务器" "dir" "/root/navidrome_data"
|
||
check_and_display "5.5" "Immich 私人相册" "dir" "/root/immich_data"
|
||
check_and_display "5.6" "Calibre-Web 电子书库" "dir" "/root/calibre_web_data"
|
||
check_and_display "5.7" "Kavita 漫画阅读器" "dir" "/root/kavita_data"
|
||
check_and_display "6.1" "Miniflux RSS 阅读器" "dir" "/root/miniflux_data"
|
||
check_and_display "6.2" "Gitea 代码仓库" "dir" "/root/gitea_data"
|
||
check_and_display "6.3" "qBittorrent 下载器" "dir" "/root/qbittorrent_data"
|
||
check_and_display "6.4" "JDownloader 下载器" "dir" "/root/jdownloader_data"
|
||
check_and_display "6.5" "yt-dlp 视频下载器" "dir" "/root/ytdlp_data"
|
||
check_and_display "6.6" "Draw.io 绘图工具" "dir" "/root/drawio_data"
|
||
check_and_display "6.7" "N8N 工作流自动化" "dir" "/root/n8n_data"
|
||
check_and_display "6.8" "Gotify 消息推送" "docker" "gotify_app"
|
||
check_and_display "6.9" "Actual Budget 记账" "dir" "/root/actual_budget_data"
|
||
check_and_display "6.10" "Excalidraw 在线白板" "dir" "/root/excalidraw_data"
|
||
check_and_display "6.11" "Joplin Server 私人笔记" "dir" "/root/joplin_data"
|
||
check_and_display "6.12" "Trilium Notes 知识库" "dir" "/root/trilium_data"
|
||
echo " ---"
|
||
check_and_display "8.1" "远程工作台 (Xfce)" "file" "/etc/xrdp/xrdp.ini"
|
||
check_and_display "8.4" "Fail2ban (防暴力破解)" "service" "fail2ban"
|
||
check_and_display "8.5" "Uptime Kuma (服务监控)" "dir" "/root/uptime_kuma_data"
|
||
check_and_display "8.6" "Glances (实时资源监控)" "docker" "glances_app"
|
||
check_and_display "8.7" "Syncthing (跨服同步引擎)" "service" "syncthing@root"
|
||
check_and_display "8.8" "Grafana 监控面板" "docker" "grafana_app"
|
||
echo " ---"
|
||
check_and_display "7.1" "SillyTavern AI酒馆" "dir" "/root/sillytavern_data"
|
||
check_and_display "7.2" "PopoDash 监控面板" "dir" "/root/popodash"
|
||
check_and_display "7.3" "MIMI 电报AI小秘书" "dir" "/root/mimi"
|
||
check_and_display "30.1" "\"科学上网\" Vless 节点" "dir" "/root/vless_data"
|
||
check_and_display "30.2" "Hysteria 2 (暴力加速)" "service" "hysteria-server"
|
||
|
||
echo -e "${YELLOW}══════════════════════════════════════════════════════════════════════${NC}"
|
||
echo -e " b) 返回主菜单"
|
||
echo ""
|
||
read -p "请输入您要卸载的选项编号 (例如 4.1) 或 'b' 返回: " choice
|
||
|
||
# --- 逻辑处理部分 (处理你的按键) ---
|
||
case $choice in
|
||
4.1) _uninstall_docker_app "Nextcloud" "/root/nextcloud_data" "## Nextcloud 凭证" "Nextcloud" ;;
|
||
4.2) _uninstall_docker_app "OnlyOffice" "/root/onlyoffice_data" "## OnlyOffice 凭证" "OnlyOffice" ;;
|
||
4.3) _uninstall_docker_app "Home Assistant" "/root/home_assistant_data" "## Home Assistant 凭证" "Home Assistant" ;;
|
||
4.4) _uninstall_docker_app "WordPress" "/root/wordpress_data" "## WordPress 凭证" "WordPress" ;;
|
||
5.1) _uninstall_docker_app "AI 大脑" "/root/ai_stack" "## AI 核心凭证" "AI WebUI" ;;
|
||
5.3) _uninstall_docker_app "Jellyfin" "/root/jellyfin_data" "## Jellyfin 凭证" "Jellyfin" ;;
|
||
5.4) _uninstall_docker_app "Navidrome" "/root/navidrome_data" "## Navidrome 凭证" "Navidrome" ;;
|
||
5.5) _uninstall_docker_app "Immich" "/root/immich_data" "## Immich 凭证" "Immich" ;;
|
||
5.6) _uninstall_docker_app "Calibre-Web" "/root/calibre_web_data" "## Calibre-Web 凭证" "Calibre-Web" ;;
|
||
5.7) _uninstall_docker_app "Kavita" "/root/kavita_data" "## Kavita 凭证" "Kavita" ;;
|
||
6.1) _uninstall_docker_app "Miniflux" "/root/miniflux_data" "## Miniflux 凭证" "Miniflux" ;;
|
||
6.2) _uninstall_docker_app "Gitea" "/root/gitea_data" "## Gitea 凭证" "Gitea" ;;
|
||
6.3) _uninstall_docker_app "qBittorrent" "/root/qbittorrent_data" "## qBittorrent 凭证" "qBittorrent" ;;
|
||
6.4) _uninstall_docker_app "JDownloader" "/root/jdownloader_data" "## JDownloader 凭证" "JDownloader" ;;
|
||
6.5) _uninstall_docker_app "yt-dlp" "/root/ytdlp_data" "## yt-dlp 凭证" "yt-dlp" ;;
|
||
6.6) _uninstall_docker_app "Draw.io" "/root/drawio_data" "## Draw.io 凭证" "Draw.io" ;;
|
||
6.7) _uninstall_docker_app "N8N" "/root/n8n_data" "## N8N 凭证" "N8N" ;;
|
||
6.8) _uninstall_docker_app "Gotify" "/root/gotify_data" "## Gotify 凭证" "Gotify" ;;
|
||
6.9) _uninstall_docker_app "Actual Budget" "/root/actual_budget_data" "## Actual Budget 凭证" "Actual Budget" ;;
|
||
6.10) _uninstall_docker_app "Excalidraw" "/root/excalidraw_data" "## Excalidraw 凭证" "Excalidraw" ;;
|
||
6.11) _uninstall_docker_app "Joplin Server" "/root/joplin_data" "## Joplin Server 凭证" "Joplin" ;;
|
||
6.12) _uninstall_docker_app "Trilium Notes" "/root/trilium_data" "## Trilium Notes 凭证" "Trilium" ;;
|
||
8.1) _uninstall_xrdp ;;
|
||
8.4) _uninstall_system_service "Fail2ban" "fail2ban" "fail2ban" "/etc/fail2ban/jail.local /etc/fail2ban/filter.d/xrdp.conf" ;;
|
||
8.5) _uninstall_docker_app "Uptime Kuma" "/root/uptime_kuma_data" "## Uptime Kuma 凭证" "Uptime Kuma" ;;
|
||
8.6) docker rm -f glances_app && sudo sed -i -e "/# Glances Monitor/,/service: .*/{/./d;}" "$TUNNEL_CONFIG_FILE" && sudo systemctl restart cloudflared ;;
|
||
8.7) _uninstall_system_service "Syncthing" "syncthing@root" "syncthing" "/etc/systemd/system/syncthing@root.service /root/.config/syncthing" ;;
|
||
8.8) _uninstall_docker_app "Grafana" "/root/grafana_data" "## Grafana 凭证" "Grafana" ;;
|
||
7.1) _uninstall_docker_app "SillyTavern" "/root/sillytavern_data" "## SillyTavern 凭证" "SillyTavern" ;;
|
||
7.2) rm -rf /root/popodash; echo -e "${GREEN}✅ PopoDash 已卸载${NC}" ;;
|
||
7.3) rm -rf /root/mimi; echo -e "${GREEN}✅ MIMI 已卸载${NC}" ;;
|
||
30.1) _uninstall_docker_app "VLESS 节点" "/root/vless_data" "## \"科学上网\" VLESS 节点" "VLESS Node" ;;
|
||
30.2) _uninstall_system_service "Hysteria 2" "hysteria-server" "hysteria-2" "/etc/hysteria /usr/bin/hysteria" ;;
|
||
1) _uninstall_system_service "Cloudflare Tunnel" "cloudflared" "cloudflared" "/root/.cloudflared /etc/apt/sources.list.d/cloudflared.list ${STATE_FILE}" ;;
|
||
2) _uninstall_rclone ;;
|
||
b|B) break ;;
|
||
*) echo -e "${RED}无效的选项。${NC}"; sleep 2 ;;
|
||
esac
|
||
done
|
||
}
|
||
# ================================================================
|
||
# 33. 应用升级与维护中心
|
||
# ================================================================
|
||
show_upgrade_menu() {
|
||
while true; do
|
||
clear
|
||
echo -e "${BLUE}==================== 🚀 应用升级中心 🚀 ====================${NC}"
|
||
echo -e "${YELLOW}一键升级你的应用,专治各种电报机器人“催更”!${NC}"
|
||
echo -e "${YELLOW}══════════════════════════════════════════════════════════════════════${NC}"
|
||
echo -e " 1) 升级 甲骨文开机助手 (oci-helper)"
|
||
echo -e " 2) 升级 AI 大脑 (Open WebUI 保数据升级)"
|
||
echo -e " b) 返回主菜单"
|
||
echo -e "${YELLOW}══════════════════════════════════════════════════════════════════════${NC}"
|
||
read -p "请输入你要升级的选项: " up_choice
|
||
|
||
case $up_choice in
|
||
1)
|
||
echo -e "${CYAN}正在调用 oci-helper 官方脚本进行升级...${NC}"
|
||
echo -e "${YELLOW}提示:如果弹出官方菜单,请选择对应的【更新/Upgrade】选项。${NC}"
|
||
# 重新调用官方脚本,官方脚本自带更新逻辑
|
||
bash <(wget -qO- https://github.com/Yohann0617/oci-helper/releases/latest/download/sh_oci-helper_install.sh)
|
||
echo -e "\n${GREEN}按任意键继续...${NC}"; read -n 1 -s
|
||
;;
|
||
2)
|
||
if [ ! -d "/root/ai_stack" ]; then
|
||
echo -e "${RED}未检测到 AI 大脑安装目录!${NC}"; sleep 2; continue
|
||
fi
|
||
echo -e "${CYAN}正在安全升级 AI 大脑 (基于我们刚修复的保数据方案)...${NC}"
|
||
cd /root/ai_stack
|
||
sudo docker compose pull open-webui
|
||
sudo docker compose up -d
|
||
sudo docker image prune -f >/dev/null 2>&1
|
||
echo -e "${GREEN}✅ AI 大脑升级完成,旧镜像已清理!${NC}"
|
||
echo -e "\n${GREEN}按任意键继续...${NC}"; read -n 1 -s
|
||
;;
|
||
b|B) break ;;
|
||
*) echo -e "${RED}无效选项,请重新输入!${NC}"; sleep 1 ;;
|
||
esac
|
||
done
|
||
}
|
||
# ================================================================
|
||
|
||
|
||
# ================================================================
|
||
# --- 选项 35:甲骨文满配一键全装 (最终重构版) ---
|
||
# ================================================================
|
||
install_oracle_fullstack() {
|
||
clear
|
||
echo -e "${GREEN}╔══════════════════════════════════════════════════════════════╗${NC}"
|
||
echo -e "${GREEN}║ 🚀 甲骨文满配一键全装 (Oracle ARM 4C24G) ║${NC}"
|
||
echo -e "${GREEN}║ 全程只需输入一次主域名,安装完再去 Tunnel 添加子域名 ║${NC}"
|
||
echo -e "${GREEN}╚══════════════════════════════════════════════════════════════╝${NC}"
|
||
echo ""
|
||
|
||
# ── 前置检查 1:Cloudflare Tunnel 必须已运行 ──────────────────
|
||
echo -e "${CYAN}[前置检查] 正在确认 Cloudflare Tunnel 状态...${NC}"
|
||
if ! systemctl is-active --quiet cloudflared 2>/dev/null; then
|
||
echo ""
|
||
echo -e "${RED}╔══════════════════════════════════════════════════════════════╗${NC}"
|
||
echo -e "${RED}║ ❌ 未检测到正在运行的 Cloudflare Tunnel! ║${NC}"
|
||
echo -e "${RED}║ 请先返回主菜单,执行【选项 1】部署 Cloudflare Tunnel, ║${NC}"
|
||
echo -e "${RED}║ 确认 Tunnel 服务启动成功后,再来运行此选项。 ║${NC}"
|
||
echo -e "${RED}╚══════════════════════════════════════════════════════════════╝${NC}"
|
||
echo ""
|
||
echo -e "${GREEN}按任意键返回主菜单...${NC}"; read -n 1 -s
|
||
return
|
||
fi
|
||
echo -e "${GREEN} ✅ Cloudflare Tunnel 运行正常!${NC}"
|
||
echo ""
|
||
|
||
# ── 前置检查 2:自动安装 Docker ──────────────────────────────
|
||
echo -e "${CYAN}[前置检查] 正在确认 Docker 环境...${NC}"
|
||
ensure_docker_installed || return
|
||
echo ""
|
||
|
||
# ── 输入主域名 ────────────────────────────────────────────────
|
||
echo -e "${CYAN}请输入您的主域名(例如 example.com):${NC}"
|
||
read -p "主域名: " MAIN_DOMAIN
|
||
if [ -z "$MAIN_DOMAIN" ]; then
|
||
echo -e "${RED}域名不能为空!${NC}"; sleep 2; return
|
||
fi
|
||
|
||
echo ""
|
||
echo -e "${CYAN}以下所有服务将被安装,请选择要跳过的编号(多个用空格分隔),直接回车全装:${NC}"
|
||
echo ""
|
||
echo -e "${GREEN} 序号 服务名称 子域名 本地端口${NC}"
|
||
echo -e "${DIM} ──── ────────────────────── ───────────────────────────── ──────${NC}"
|
||
printf " ${YELLOW}%2s${NC} %-24s ${CYAN}%-30s${NC} %s\n" "1" "Nextcloud 网盘" "nextcloud.${MAIN_DOMAIN}" "8888"
|
||
printf " ${YELLOW}%2s${NC} %-24s ${CYAN}%-30s${NC} %s\n" "2" "OnlyOffice 文档" "onlyoffice.${MAIN_DOMAIN}" "8889"
|
||
printf " ${YELLOW}%2s${NC} %-24s ${CYAN}%-30s${NC} %s\n" "3" "Jellyfin 影院" "jellyfin.${MAIN_DOMAIN}" "8096"
|
||
printf " ${YELLOW}%2s${NC} %-24s ${CYAN}%-30s${NC} %s\n" "4" "Navidrome 音乐" "music.${MAIN_DOMAIN}" "4533"
|
||
printf " ${YELLOW}%2s${NC} %-24s ${CYAN}%-30s${NC} %s\n" "5" "Miniflux RSS" "rss.${MAIN_DOMAIN}" "8091"
|
||
printf " ${YELLOW}%2s${NC} %-24s ${CYAN}%-30s${NC} %s\n" "6" "Memos 笔记" "memos.${MAIN_DOMAIN}" "5230"
|
||
printf " ${YELLOW}%2s${NC} %-24s ${CYAN}%-30s${NC} %s\n" "7" "AI 大脑(Ollama+WebUI)" "ai.${MAIN_DOMAIN}" "3001"
|
||
printf " ${YELLOW}%2s${NC} %-24s ${CYAN}%-30s${NC} %s\n" "8" "Uptime Kuma 监控" "status.${MAIN_DOMAIN}" "3002"
|
||
printf " ${YELLOW}%2s${NC} %-24s ${CYAN}%-30s${NC} %s\n" "9" "qBittorrent 下载" "bt.${MAIN_DOMAIN}" "8080"
|
||
printf " ${YELLOW}%2s${NC} %-24s ${CYAN}%-30s${NC} %s\n" "10" "SillyTavern 酒馆" "st.${MAIN_DOMAIN}" "8000"
|
||
printf " ${YELLOW}%2s${NC} %-24s ${CYAN}%-30s${NC} %s\n" "11" "N8N 工作流自动化" "n8n.${MAIN_DOMAIN}" "5678"
|
||
printf " ${YELLOW}%2s${NC} %-24s ${CYAN}%-30s${NC} %s\n" "12" "Immich 私人相册" "photos.${MAIN_DOMAIN}" "2283"
|
||
printf " ${YELLOW}%2s${NC} %-24s ${CYAN}%-30s${NC} %s\n" "13" "Calibre-Web 电子书库" "books.${MAIN_DOMAIN}" "8083"
|
||
printf " ${YELLOW}%2s${NC} %-24s ${CYAN}%-30s${NC} %s\n" "14" "Kavita 漫画阅读器" "manga.${MAIN_DOMAIN}" "5000"
|
||
printf " ${YELLOW}%2s${NC} %-24s ${CYAN}%-30s${NC} %s\n" "15" "Gitea 代码仓库" "git.${MAIN_DOMAIN}" "3000"
|
||
printf " ${YELLOW}%2s${NC} %-24s ${CYAN}%-30s${NC} %s\n" "16" "JDownloader 下载器" "jd.${MAIN_DOMAIN}" "5800"
|
||
printf " ${YELLOW}%2s${NC} %-24s ${CYAN}%-30s${NC} %s\n" "17" "MeTube 视频下载" "dl.${MAIN_DOMAIN}" "8999"
|
||
printf " ${YELLOW}%2s${NC} %-24s ${CYAN}%-30s${NC} %s\n" "18" "Draw.io 绘图工具" "draw.${MAIN_DOMAIN}" "8082"
|
||
printf " ${YELLOW}%2s${NC} %-24s ${CYAN}%-30s${NC} %s\n" "19" "Gotify 消息推送" "push.${MAIN_DOMAIN}" "8085"
|
||
printf " ${YELLOW}%2s${NC} %-24s ${CYAN}%-30s${NC} %s\n" "20" "Actual Budget 记账" "money.${MAIN_DOMAIN}" "5006"
|
||
printf " ${YELLOW}%2s${NC} %-24s ${CYAN}%-30s${NC} %s\n" "21" "Excalidraw 白板" "board.${MAIN_DOMAIN}" "8086"
|
||
printf " ${YELLOW}%2s${NC} %-24s ${CYAN}%-30s${NC} %s\n" "22" "Joplin Server 笔记" "notes.${MAIN_DOMAIN}" "22300"
|
||
printf " ${YELLOW}%2s${NC} %-24s ${CYAN}%-30s${NC} %s\n" "23" "Grafana 监控面板" "monitor.${MAIN_DOMAIN}" "3003"
|
||
printf " ${YELLOW}%2s${NC} %-24s ${CYAN}%-30s${NC} %s\n" "24" "Home Assistant 智能家居" "ha.${MAIN_DOMAIN}" "8123"
|
||
printf " ${YELLOW}%2s${NC} %-24s ${CYAN}%-30s${NC} %s\n" "25" "WordPress 博客" "blog.${MAIN_DOMAIN}" "8890"
|
||
printf " ${YELLOW}%2s${NC} %-24s ${CYAN}%-30s${NC} %s\n" "26" "Glances 资源监控" "glances.${MAIN_DOMAIN}" "61208"
|
||
printf " ${YELLOW}%2s${NC} %-24s ${CYAN}%-30s${NC} %s\n" "27" "Fail2ban 防暴力破解" "(系统服务,无域名)" "-"
|
||
echo ""
|
||
echo -e "${YELLOW}⚠️ 甲骨文 ARM 4C24G 资源充足,理论上可以全装。如非满配机器建议跳过部分。${NC}"
|
||
echo ""
|
||
read -p "输入要跳过的编号(例如 24 25,留空全装,q 取消): " SKIP_INPUT
|
||
if [ "$SKIP_INPUT" == "q" ] || [ "$SKIP_INPUT" == "Q" ]; then
|
||
echo -e "${GREEN}操作已取消。${NC}"; sleep 2; return
|
||
fi
|
||
|
||
# 解析跳过列表
|
||
declare -A SKIP_MAP
|
||
for num in $SKIP_INPUT; do
|
||
SKIP_MAP[$num]=1
|
||
done
|
||
|
||
local START_TIME; START_TIME=$(date +%s)
|
||
local STEP=0; local TOTAL=27
|
||
|
||
_step() {
|
||
STEP=$((STEP+1))
|
||
echo ""
|
||
echo -e "${GREEN}════════════════════════════════════════════════${NC}"
|
||
echo -e "${GREEN} [${STEP}/${TOTAL}] $1${NC}"
|
||
echo -e "${GREEN}════════════════════════════════════════════════${NC}"
|
||
}
|
||
|
||
# ── 基础环境 ──────────────────────────────────────────────────
|
||
sudo chown -R www-data:www-data /mnt 2>/dev/null || true
|
||
sudo mkdir -p /mnt/Downloads /mnt/Music /mnt/Photos /mnt/Books
|
||
|
||
# ── 1. Nextcloud ──────────────────────────────────────────────
|
||
_step "部署 Nextcloud 家庭网盘"
|
||
if [[ -n "${SKIP_MAP[1]}" ]]; then echo -e "${DIM} ⏭️ 已跳过${NC}"
|
||
elif [ ! -d "/root/nextcloud_data" ]; then
|
||
local NC_DOMAIN="nextcloud.${MAIN_DOMAIN}"
|
||
local NC_DB_PASS="NcDb-pW_$(_gen_password 12)"
|
||
mkdir -p /root/nextcloud_data
|
||
[ -d "/root/nextcloud_data/php-opcache.ini" ] && rm -rf /root/nextcloud_data/php-opcache.ini
|
||
cat > /root/nextcloud_data/php-opcache.ini << 'OPCEOF'
|
||
opcache.enable=1
|
||
opcache.memory_consumption=128
|
||
opcache.interned_strings_buffer=16
|
||
opcache.max_accelerated_files=10000
|
||
opcache.save_comments=1
|
||
opcache.revalidate_freq=60
|
||
OPCEOF
|
||
cat > /root/nextcloud_data/docker-compose.yml << NCEOF
|
||
services:
|
||
db:
|
||
image: mariadb:11.4
|
||
container_name: nextcloud_db
|
||
restart: unless-stopped
|
||
command: [--transaction-isolation=READ-COMMITTED, --binlog-format=ROW, --character-set-server=utf8mb4, --collation-server=utf8mb4_unicode_ci]
|
||
volumes:
|
||
- './db:/var/lib/mysql'
|
||
environment:
|
||
MYSQL_DATABASE: nextclouddb
|
||
MYSQL_USER: nextclouduser
|
||
MYSQL_PASSWORD: ${NC_DB_PASS}
|
||
MYSQL_ROOT_PASSWORD: ${NC_DB_PASS}_root
|
||
redis:
|
||
image: redis:alpine
|
||
container_name: nextcloud_redis
|
||
restart: unless-stopped
|
||
app:
|
||
image: nextcloud:latest
|
||
container_name: nextcloud_app
|
||
restart: unless-stopped
|
||
ports:
|
||
- "127.0.0.1:8888:80"
|
||
volumes:
|
||
- './html:/var/www/html'
|
||
- './php-opcache.ini:/usr/local/etc/php/conf.d/opcache-recommended.ini'
|
||
- type: bind
|
||
source: /mnt
|
||
target: /mnt
|
||
bind:
|
||
propagation: rshared
|
||
depends_on:
|
||
- db
|
||
- redis
|
||
NCEOF
|
||
(cd /root/nextcloud_data && sudo docker compose up -d)
|
||
echo -e "\n## Nextcloud 凭证 (全装部署: $(date))" >> ${STATE_FILE}
|
||
echo "NEXTCLOUD_DOMAIN=${NC_DOMAIN}" >> ${STATE_FILE}
|
||
echo "NEXTCLOUD_PORT=8888" >> ${STATE_FILE}
|
||
echo "NEXTCLOUD_DB_USER=nextclouduser" >> ${STATE_FILE}
|
||
echo "NEXTCLOUD_DB_NAME=nextclouddb" >> ${STATE_FILE}
|
||
echo "NEXTCLOUD_DB_HOST=db" >> ${STATE_FILE}
|
||
echo "NEXTCLOUD_DB_PASSWORD=${NC_DB_PASS}" >> ${STATE_FILE}
|
||
update_tunnel_config "${NC_DOMAIN}" "http://127.0.0.1:8888" "Nextcloud"
|
||
(crontab -l 2>/dev/null | grep -v "nextcloud_app php -f"; echo "*/5 * * * * /usr/bin/docker exec --user www-data nextcloud_app php -f /var/www/html/cron.php > /dev/null 2>&1") | crontab -
|
||
(crontab -l 2>/dev/null | grep -v "files:scan"; echo "*/15 * * * * /usr/bin/docker exec --user www-data nextcloud_app php occ files:scan --all > /dev/null 2>&1") | crontab -
|
||
echo -e "${GREEN}✅ Nextcloud 部署完成${NC}"
|
||
else
|
||
echo -e "${YELLOW}⏭️ Nextcloud 已安装,跳过${NC}"
|
||
fi
|
||
|
||
# ── 2. OnlyOffice ─────────────────────────────────────────────
|
||
_step "部署 OnlyOffice 在线文档"
|
||
if [[ -n "${SKIP_MAP[2]}" ]]; then echo -e "${DIM} ⏭️ 已跳过${NC}"
|
||
elif [ ! -d "/root/onlyoffice_data" ]; then
|
||
local OO_DOMAIN="onlyoffice.${MAIN_DOMAIN}"
|
||
local OO_JWT="JwtS3cr3t-$(_gen_password 16)"
|
||
mkdir -p /root/onlyoffice_data
|
||
cat > /root/onlyoffice_data/docker-compose.yml << EOF
|
||
services:
|
||
onlyoffice:
|
||
image: onlyoffice/documentserver:latest
|
||
container_name: onlyoffice_app
|
||
restart: unless-stopped
|
||
ports:
|
||
- "127.0.0.1:8889:80"
|
||
volumes:
|
||
- './data:/var/www/onlyoffice/Data'
|
||
- './logs:/var/log/onlyoffice'
|
||
environment:
|
||
JWT_ENABLED: 'true'
|
||
JWT_SECRET: ${OO_JWT}
|
||
EOF
|
||
(cd /root/onlyoffice_data && sudo docker compose up -d)
|
||
echo -e "\n## OnlyOffice 凭证 (全装部署: $(date))" >> ${STATE_FILE}
|
||
echo "ONLYOFFICE_DOMAIN=${OO_DOMAIN}" >> ${STATE_FILE}
|
||
echo "ONLYOFFICE_JWT_SECRET=${OO_JWT}" >> ${STATE_FILE}
|
||
update_tunnel_config "${OO_DOMAIN}" "http://127.0.0.1:8889" "OnlyOffice"
|
||
echo -e "${GREEN}✅ OnlyOffice 部署完成${NC}"
|
||
else
|
||
echo -e "${YELLOW}⏭️ OnlyOffice 已安装,跳过${NC}"
|
||
fi
|
||
|
||
# ── 3. Jellyfin ───────────────────────────────────────────────
|
||
_step "部署 Jellyfin 家庭影院"
|
||
if [[ -n "${SKIP_MAP[3]}" ]]; then echo -e "${DIM} ⏭️ 已跳过${NC}"
|
||
elif [ ! -d "/root/jellyfin_data" ]; then
|
||
local JF_DOMAIN="jellyfin.${MAIN_DOMAIN}"
|
||
mkdir -p /root/jellyfin_data/config
|
||
cat > /root/jellyfin_data/docker-compose.yml << EOF
|
||
services:
|
||
jellyfin:
|
||
image: jellyfin/jellyfin:latest
|
||
container_name: jellyfin_app
|
||
restart: unless-stopped
|
||
ports:
|
||
- "127.0.0.1:8096:8096"
|
||
environment:
|
||
- 'PUID=0'
|
||
- 'PGID=0'
|
||
- 'TZ=Asia/Shanghai'
|
||
volumes:
|
||
- './config:/config'
|
||
- type: bind
|
||
source: /mnt
|
||
target: /mnt
|
||
bind:
|
||
propagation: rshared
|
||
EOF
|
||
(cd /root/jellyfin_data && sudo docker compose up -d)
|
||
echo -e "\n## Jellyfin 凭证 (全装部署: $(date))" >> ${STATE_FILE}
|
||
echo "JELLYFIN_DOMAIN=${JF_DOMAIN}" >> ${STATE_FILE}
|
||
update_tunnel_config "${JF_DOMAIN}" "http://127.0.0.1:8096" "Jellyfin"
|
||
echo -e "${GREEN}✅ Jellyfin 部署完成${NC}"
|
||
else
|
||
echo -e "${YELLOW}⏭️ Jellyfin 已安装,跳过${NC}"
|
||
fi
|
||
|
||
# ── 4. Navidrome ──────────────────────────────────────────────
|
||
_step "部署 Navidrome 音乐服务器"
|
||
if [[ -n "${SKIP_MAP[4]}" ]]; then echo -e "${DIM} ⏭️ 已跳过${NC}"
|
||
elif [ ! -d "/root/navidrome_data" ]; then
|
||
local ND_DOMAIN="music.${MAIN_DOMAIN}"
|
||
mkdir -p /root/navidrome_data/data
|
||
cat > /root/navidrome_data/docker-compose.yml << EOF
|
||
services:
|
||
navidrome:
|
||
image: deluan/navidrome:latest
|
||
container_name: navidrome_app
|
||
restart: unless-stopped
|
||
ports:
|
||
- "127.0.0.1:4533:4533"
|
||
environment:
|
||
ND_SCANSCHEDULE: 1h
|
||
ND_LOGLEVEL: info
|
||
ND_MUSICFOLDER: /music
|
||
volumes:
|
||
- './data:/data'
|
||
- '/mnt/Music:/music:ro'
|
||
EOF
|
||
(cd /root/navidrome_data && sudo docker compose up -d)
|
||
echo -e "\n## Navidrome 凭证 (全装部署: $(date))" >> ${STATE_FILE}
|
||
echo "NAVIDROME_DOMAIN=${ND_DOMAIN}" >> ${STATE_FILE}
|
||
update_tunnel_config "${ND_DOMAIN}" "http://127.0.0.1:4533" "Navidrome"
|
||
echo -e "${GREEN}✅ Navidrome 部署完成${NC}"
|
||
else
|
||
echo -e "${YELLOW}⏭️ Navidrome 已安装,跳过${NC}"
|
||
fi
|
||
|
||
# ── 5. Miniflux ───────────────────────────────────────────────
|
||
_step "部署 Miniflux RSS 阅读器"
|
||
if [[ -n "${SKIP_MAP[5]}" ]]; then echo -e "${DIM} ⏭️ 已跳过${NC}"
|
||
elif [ ! -d "/root/miniflux_data" ]; then
|
||
local MF_DOMAIN="rss.${MAIN_DOMAIN}"
|
||
local MF_PASS="mf_$(_gen_password 10)"
|
||
local MF_DB_PASS="mfDb_$(_gen_password 12)"
|
||
mkdir -p /root/miniflux_data/db_data
|
||
cat > /root/miniflux_data/docker-compose.yml << EOF
|
||
services:
|
||
db:
|
||
image: postgres:15-alpine
|
||
container_name: miniflux_db
|
||
restart: unless-stopped
|
||
environment:
|
||
POSTGRES_USER: miniflux
|
||
POSTGRES_PASSWORD: ${MF_DB_PASS}
|
||
POSTGRES_DB: miniflux
|
||
volumes:
|
||
- './db_data:/var/lib/postgresql/data'
|
||
healthcheck:
|
||
test: ["CMD", "pg_isready", "-U", "miniflux"]
|
||
interval: 10s
|
||
timeout: 5s
|
||
retries: 5
|
||
app:
|
||
image: miniflux/miniflux:latest
|
||
container_name: miniflux_app
|
||
restart: unless-stopped
|
||
depends_on:
|
||
db:
|
||
condition: service_healthy
|
||
ports:
|
||
- "127.0.0.1:8091:8080"
|
||
environment:
|
||
DATABASE_URL: postgres://miniflux:${MF_DB_PASS}@db/miniflux?sslmode=disable
|
||
RUN_MIGRATIONS: "1"
|
||
CREATE_ADMIN: "1"
|
||
ADMIN_USERNAME: admin
|
||
ADMIN_PASSWORD: ${MF_PASS}
|
||
BASE_URL: https://${MF_DOMAIN}
|
||
EOF
|
||
(cd /root/miniflux_data && sudo docker compose up -d)
|
||
echo -e "\n## Miniflux 凭证 (全装部署: $(date))" >> ${STATE_FILE}
|
||
echo "MINIFLUX_DOMAIN=${MF_DOMAIN}" >> ${STATE_FILE}
|
||
echo "MINIFLUX_ADMIN_USER=admin" >> ${STATE_FILE}
|
||
echo "MINIFLUX_ADMIN_PASSWORD=${MF_PASS}" >> ${STATE_FILE}
|
||
update_tunnel_config "${MF_DOMAIN}" "http://127.0.0.1:8091" "Miniflux"
|
||
echo -e "${GREEN}✅ Miniflux 部署完成(admin 密码已写入凭证文件)${NC}"
|
||
else
|
||
echo -e "${YELLOW}⏭️ Miniflux 已安装,跳过${NC}"
|
||
fi
|
||
|
||
# ── 6. Memos ──────────────────────────────────────────────────
|
||
_step "部署 Memos 轻量笔记"
|
||
if [[ -n "${SKIP_MAP[6]}" ]]; then echo -e "${DIM} ⏭️ 已跳过${NC}"
|
||
elif [ ! -d "/root/memos_data" ]; then
|
||
local MM_DOMAIN="memos.${MAIN_DOMAIN}"
|
||
mkdir -p /root/memos_data/data
|
||
cat > /root/memos_data/docker-compose.yml << EOF
|
||
services:
|
||
memos:
|
||
image: ghcr.io/usememos/memos:latest
|
||
container_name: memos_app
|
||
restart: unless-stopped
|
||
ports:
|
||
- "127.0.0.1:5230:5230"
|
||
volumes:
|
||
- './data:/var/opt/memos'
|
||
EOF
|
||
(cd /root/memos_data && sudo docker compose up -d)
|
||
echo -e "\n## Memos 凭证 (全装部署: $(date))" >> ${STATE_FILE}
|
||
echo "MEMOS_DOMAIN=${MM_DOMAIN}" >> ${STATE_FILE}
|
||
update_tunnel_config "${MM_DOMAIN}" "http://127.0.0.1:5230" "Memos"
|
||
echo -e "${GREEN}✅ Memos 部署完成${NC}"
|
||
else
|
||
echo -e "${YELLOW}⏭️ Memos 已安装,跳过${NC}"
|
||
fi
|
||
|
||
# ── 7. AI 大脑 ────────────────────────────────────────────────
|
||
_step "部署 AI 大脑 (Ollama + Open WebUI)"
|
||
if [[ -n "${SKIP_MAP[7]}" ]]; then echo -e "${DIM} ⏭️ 已跳过${NC}"
|
||
elif [ ! -d "/root/ai_stack" ]; then
|
||
local AI_DOMAIN="ai.${MAIN_DOMAIN}"
|
||
mkdir -p /root/ai_stack
|
||
cat > /root/ai_stack/docker-compose.yml << EOF
|
||
services:
|
||
ollama:
|
||
image: ollama/ollama:latest
|
||
container_name: ollama
|
||
restart: unless-stopped
|
||
volumes:
|
||
- './ollama_data:/root/.ollama'
|
||
open-webui:
|
||
image: ghcr.io/open-webui/open-webui:main
|
||
container_name: open_webui_app
|
||
restart: unless-stopped
|
||
ports:
|
||
- "127.0.0.1:3001:8080"
|
||
environment:
|
||
- OLLAMA_BASE_URL=http://ollama:11434
|
||
volumes:
|
||
- './webui_data:/app/backend/data'
|
||
depends_on:
|
||
- ollama
|
||
EOF
|
||
(cd /root/ai_stack && sudo docker compose up -d)
|
||
echo -e "\n## AI 核心凭证 (全装部署: $(date))" >> ${STATE_FILE}
|
||
echo "AI_DOMAIN=${AI_DOMAIN}" >> ${STATE_FILE}
|
||
update_tunnel_config "${AI_DOMAIN}" "http://127.0.0.1:3001" "Open WebUI"
|
||
echo -e "${GREEN}✅ AI 大脑部署完成(建议执行选项 5.2 安装语言模型)${NC}"
|
||
else
|
||
echo -e "${YELLOW}⏭️ AI 大脑已安装,跳过${NC}"
|
||
fi
|
||
|
||
# ── 8. Uptime Kuma ────────────────────────────────────────────
|
||
_step "部署 Uptime Kuma 服务监控"
|
||
if [[ -n "${SKIP_MAP[8]}" ]]; then echo -e "${DIM} ⏭️ 已跳过${NC}"
|
||
elif [ ! -d "/root/uptime_kuma_data" ]; then
|
||
local UK_DOMAIN="status.${MAIN_DOMAIN}"
|
||
mkdir -p /root/uptime_kuma_data
|
||
cat > /root/uptime_kuma_data/docker-compose.yml << EOF
|
||
services:
|
||
uptime-kuma:
|
||
image: louislam/uptime-kuma:1
|
||
container_name: uptime_kuma_app
|
||
restart: unless-stopped
|
||
ports:
|
||
- "127.0.0.1:3002:3001"
|
||
volumes:
|
||
- './data:/app/data'
|
||
environment:
|
||
- 'TZ=Asia/Shanghai'
|
||
EOF
|
||
(cd /root/uptime_kuma_data && sudo docker compose up -d)
|
||
echo -e "\n## Uptime Kuma 凭证 (全装部署: $(date))" >> ${STATE_FILE}
|
||
echo "KUMA_DOMAIN=${UK_DOMAIN}" >> ${STATE_FILE}
|
||
update_tunnel_config "${UK_DOMAIN}" "http://127.0.0.1:3002" "Uptime Kuma"
|
||
echo -e "${GREEN}✅ Uptime Kuma 部署完成${NC}"
|
||
else
|
||
echo -e "${YELLOW}⏭️ Uptime Kuma 已安装,跳过${NC}"
|
||
fi
|
||
|
||
# ── 9. qBittorrent ────────────────────────────────────────────
|
||
_step "部署 qBittorrent 下载器"
|
||
if [[ -n "${SKIP_MAP[9]}" ]]; then echo -e "${DIM} ⏭️ 已跳过${NC}"
|
||
elif [ ! -d "/root/qbittorrent_data" ]; then
|
||
local QB_DOMAIN="bt.${MAIN_DOMAIN}"
|
||
mkdir -p /root/qbittorrent_data/config
|
||
cat > /root/qbittorrent_data/docker-compose.yml << EOF
|
||
services:
|
||
qbittorrent:
|
||
image: lscr.io/linuxserver/qbittorrent:latest
|
||
container_name: qbittorrent_app
|
||
restart: unless-stopped
|
||
environment:
|
||
- PUID=1000
|
||
- PGID=1000
|
||
- WEBUI_PORT=8080
|
||
ports:
|
||
- "127.0.0.1:8080:8080"
|
||
- "127.0.0.1:6881:6881"
|
||
- "127.0.0.1:6881:6881/udp"
|
||
volumes:
|
||
- './config:/config'
|
||
- '/mnt:/downloads'
|
||
EOF
|
||
(cd /root/qbittorrent_data && sudo docker compose up -d)
|
||
echo -e "\n## qBittorrent 凭证 (全装部署: $(date))" >> ${STATE_FILE}
|
||
echo "QB_DOMAIN=${QB_DOMAIN}" >> ${STATE_FILE}
|
||
echo "QB_NOTE=初始密码请执行: docker logs qbittorrent_app | grep password" >> ${STATE_FILE}
|
||
update_tunnel_config "${QB_DOMAIN}" "http://127.0.0.1:8080" "qBittorrent"
|
||
echo -e "${GREEN}✅ qBittorrent 部署完成(初始密码:docker logs qbittorrent_app)${NC}"
|
||
else
|
||
echo -e "${YELLOW}⏭️ qBittorrent 已安装,跳过${NC}"
|
||
fi
|
||
|
||
# ── 10. SillyTavern ───────────────────────────────────────────
|
||
_step "部署 SillyTavern 角色扮演酒馆"
|
||
if [[ -n "${SKIP_MAP[10]}" ]]; then echo -e "${DIM} ⏭️ 已跳过${NC}"
|
||
elif ! docker ps -q -f name=sillytavern_app 2>/dev/null | grep -q .; then
|
||
local ST_DOMAIN="st.${MAIN_DOMAIN}"
|
||
mkdir -p /root/sillytavern_data
|
||
[ -d "/root/sillytavern_data/config.yaml" ] && rm -rf /root/sillytavern_data/config.yaml
|
||
cat > /root/sillytavern_data/config.yaml << 'STEOF'
|
||
dataRoot: ./data
|
||
listen: true
|
||
port: 8000
|
||
ssl:
|
||
enabled: false
|
||
whitelistMode: false
|
||
basicAuthMode: false
|
||
enableCorsProxy: true
|
||
securityOverride: true
|
||
STEOF
|
||
cat > /root/sillytavern_data/docker-compose.yml << 'STEOF'
|
||
services:
|
||
sillytavern:
|
||
container_name: sillytavern_app
|
||
image: ghcr.io/sillytavern/sillytavern:latest
|
||
restart: unless-stopped
|
||
ports:
|
||
- "127.0.0.1:8000:8000"
|
||
volumes:
|
||
- '/root/sillytavern_data/config.yaml:/home/node/app/config.yaml'
|
||
- '/root/sillytavern_data/data:/home/node/app/data'
|
||
STEOF
|
||
(cd /root/sillytavern_data && docker compose up -d)
|
||
echo -e "\n## SillyTavern 凭证 (全装部署: $(date))" >> ${STATE_FILE}
|
||
echo "SILLYTAVERN_DOMAIN=${ST_DOMAIN}" >> ${STATE_FILE}
|
||
update_tunnel_config "${ST_DOMAIN}" "http://127.0.0.1:8000" "SillyTavern 酒馆"
|
||
echo -e "${GREEN}✅ SillyTavern 部署完成${NC}"
|
||
else
|
||
echo -e "${YELLOW}⏭️ SillyTavern 已安装,跳过${NC}"
|
||
fi
|
||
|
||
# ── 11. N8N ───────────────────────────────────────────────────
|
||
_step "部署 N8N 工作流自动化"
|
||
if [[ -n "${SKIP_MAP[11]}" ]]; then echo -e "${DIM} ⏭️ 已跳过${NC}"
|
||
elif [ ! -d "/root/n8n_data" ]; then
|
||
local N8N_DOMAIN="n8n.${MAIN_DOMAIN}"
|
||
local N8N_ENCRYPTION_KEY="n8n-key-$(_gen_password 24)"
|
||
mkdir -p /root/n8n_data/data
|
||
chown -R 1000:1000 /root/n8n_data/data
|
||
cat > /root/n8n_data/docker-compose.yml << EOF
|
||
services:
|
||
n8n:
|
||
image: n8nio/n8n:latest
|
||
container_name: n8n_app
|
||
restart: unless-stopped
|
||
ports:
|
||
- "127.0.0.1:5678:5678"
|
||
environment:
|
||
- N8N_HOST=${N8N_DOMAIN}
|
||
- N8N_PORT=5678
|
||
- N8N_PROTOCOL=https
|
||
- NODE_ENV=production
|
||
- WEBHOOK_URL=https://${N8N_DOMAIN}/
|
||
- N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY}
|
||
- TZ=Asia/Shanghai
|
||
volumes:
|
||
- './data:/home/node/.n8n'
|
||
EOF
|
||
(cd /root/n8n_data && sudo docker compose up -d)
|
||
echo -e "\n## N8N 凭证 (全装部署: $(date))" >> ${STATE_FILE}
|
||
echo "N8N_DOMAIN=${N8N_DOMAIN}" >> ${STATE_FILE}
|
||
echo "N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY}" >> ${STATE_FILE}
|
||
update_tunnel_config "${N8N_DOMAIN}" "http://127.0.0.1:5678" "N8N"
|
||
echo -e "${GREEN}✅ N8N 部署完成(首次访问注册管理员账号)${NC}"
|
||
else
|
||
echo -e "${YELLOW}⏭️ N8N 已安装,跳过${NC}"
|
||
fi
|
||
|
||
# ── 12. Immich ────────────────────────────────────────────────
|
||
_step "部署 Immich 私人相册"
|
||
if [[ -n "${SKIP_MAP[12]}" ]]; then echo -e "${DIM} ⏭️ 已跳过${NC}"
|
||
elif [ ! -d "/root/immich_data" ]; then
|
||
local IM_DOMAIN="photos.${MAIN_DOMAIN}"
|
||
local IM_DB_PASS="immich_db_$(_gen_password 16)"
|
||
local IM_JWT="$(_gen_password 32)"
|
||
mkdir -p /root/immich_data
|
||
cat > /root/immich_data/docker-compose.yml << EOF
|
||
services:
|
||
immich-server:
|
||
image: ghcr.io/immich-app/immich-server:release
|
||
container_name: immich_server
|
||
restart: unless-stopped
|
||
ports:
|
||
- "127.0.0.1:2283:2283"
|
||
volumes:
|
||
- '/mnt/Photos:/usr/src/app/upload'
|
||
- /etc/localtime:/etc/localtime:ro
|
||
environment:
|
||
DB_HOSTNAME: immich_postgres
|
||
DB_USERNAME: immich
|
||
DB_PASSWORD: ${IM_DB_PASS}
|
||
DB_DATABASE_NAME: immich
|
||
REDIS_HOSTNAME: immich_redis
|
||
JWT_SECRET: ${IM_JWT}
|
||
TZ: Asia/Shanghai
|
||
depends_on:
|
||
- immich-redis
|
||
- immich-postgres
|
||
immich-machine-learning:
|
||
image: ghcr.io/immich-app/immich-machine-learning:release
|
||
container_name: immich_ml
|
||
restart: unless-stopped
|
||
volumes:
|
||
- immich_model_cache:/cache
|
||
immich-redis:
|
||
image: redis:7-alpine
|
||
container_name: immich_redis
|
||
restart: unless-stopped
|
||
immich-postgres:
|
||
image: tensorchord/pgvecto-rs:pg14-v0.2.0
|
||
container_name: immich_postgres
|
||
restart: unless-stopped
|
||
environment:
|
||
POSTGRES_USER: immich
|
||
POSTGRES_PASSWORD: ${IM_DB_PASS}
|
||
POSTGRES_DB: immich
|
||
volumes:
|
||
- immich_pgdata:/var/lib/postgresql/data
|
||
volumes:
|
||
immich_model_cache:
|
||
immich_pgdata:
|
||
EOF
|
||
(cd /root/immich_data && sudo docker compose up -d)
|
||
echo -e "\n## Immich 凭证 (全装部署: $(date))" >> ${STATE_FILE}
|
||
echo "IMMICH_DOMAIN=${IM_DOMAIN}" >> ${STATE_FILE}
|
||
echo "IMMICH_UPLOAD_PATH=/mnt/Photos" >> ${STATE_FILE}
|
||
update_tunnel_config "${IM_DOMAIN}" "http://127.0.0.1:2283" "Immich 相册"
|
||
echo -e "${GREEN}✅ Immich 部署完成(首次访问注册管理员账号)${NC}"
|
||
else
|
||
echo -e "${YELLOW}⏭️ Immich 已安装,跳过${NC}"
|
||
fi
|
||
|
||
# ── 13. Calibre-Web ───────────────────────────────────────────
|
||
_step "部署 Calibre-Web 电子书库"
|
||
if [[ -n "${SKIP_MAP[13]}" ]]; then echo -e "${DIM} ⏭️ 已跳过${NC}"
|
||
elif [ ! -d "/root/calibre_web_data" ]; then
|
||
local CW_DOMAIN="books.${MAIN_DOMAIN}"
|
||
mkdir -p /root/calibre_web_data/config
|
||
cat > /root/calibre_web_data/docker-compose.yml << EOF
|
||
services:
|
||
calibre-web:
|
||
image: lscr.io/linuxserver/calibre-web:latest
|
||
container_name: calibre_web_app
|
||
restart: unless-stopped
|
||
ports:
|
||
- "127.0.0.1:8083:8083"
|
||
volumes:
|
||
- './config:/config'
|
||
- '/mnt/Books:/books'
|
||
environment:
|
||
- PUID=0
|
||
- PGID=0
|
||
- TZ=Asia/Shanghai
|
||
- DOCKER_MODS=linuxserver/mods:universal-calibre
|
||
EOF
|
||
(cd /root/calibre_web_data && sudo docker compose up -d)
|
||
echo -e "\n## Calibre-Web 凭证 (全装部署: $(date))" >> ${STATE_FILE}
|
||
echo "CALIBRE_WEB_DOMAIN=${CW_DOMAIN}" >> ${STATE_FILE}
|
||
update_tunnel_config "${CW_DOMAIN}" "http://127.0.0.1:8083" "Calibre-Web"
|
||
echo -e "${GREEN}✅ Calibre-Web 部署完成(默认账号/密码: admin/admin123,登录后填书库路径 /books)${NC}"
|
||
else
|
||
echo -e "${YELLOW}⏭️ Calibre-Web 已安装,跳过${NC}"
|
||
fi
|
||
|
||
# ── 14. Kavita ────────────────────────────────────────────────
|
||
_step "部署 Kavita 漫画阅读器"
|
||
if [[ -n "${SKIP_MAP[14]}" ]]; then echo -e "${DIM} ⏭️ 已跳过${NC}"
|
||
elif [ ! -d "/root/kavita_data" ]; then
|
||
local KV_DOMAIN="manga.${MAIN_DOMAIN}"
|
||
mkdir -p /root/kavita_data/config
|
||
cat > /root/kavita_data/docker-compose.yml << EOF
|
||
services:
|
||
kavita:
|
||
image: jvmilazz0/kavita:latest
|
||
container_name: kavita_app
|
||
restart: unless-stopped
|
||
ports:
|
||
- "127.0.0.1:5000:5000"
|
||
volumes:
|
||
- './config:/kavita/config'
|
||
- '/mnt/Books:/manga'
|
||
environment:
|
||
- TZ=Asia/Shanghai
|
||
EOF
|
||
(cd /root/kavita_data && sudo docker compose up -d)
|
||
echo -e "\n## Kavita 凭证 (全装部署: $(date))" >> ${STATE_FILE}
|
||
echo "KAVITA_DOMAIN=${KV_DOMAIN}" >> ${STATE_FILE}
|
||
update_tunnel_config "${KV_DOMAIN}" "http://127.0.0.1:5000" "Kavita"
|
||
echo -e "${GREEN}✅ Kavita 部署完成(首次访问设置管理员,书库路径填 /manga)${NC}"
|
||
else
|
||
echo -e "${YELLOW}⏭️ Kavita 已安装,跳过${NC}"
|
||
fi
|
||
|
||
# ── 15. Gitea ─────────────────────────────────────────────────
|
||
_step "部署 Gitea 代码仓库"
|
||
if [[ -n "${SKIP_MAP[15]}" ]]; then echo -e "${DIM} ⏭️ 已跳过${NC}"
|
||
elif [ ! -d "/root/gitea_data" ]; then
|
||
local GT_DOMAIN="git.${MAIN_DOMAIN}"
|
||
mkdir -p /root/gitea_data
|
||
cat > /root/gitea_data/docker-compose.yml << EOF
|
||
services:
|
||
server:
|
||
image: gitea/gitea:latest
|
||
container_name: gitea_app
|
||
restart: unless-stopped
|
||
ports:
|
||
- "127.0.0.1:3000:3000"
|
||
environment:
|
||
- 'USER_UID=1000'
|
||
- 'USER_GID=1000'
|
||
volumes:
|
||
- './gitea:/data'
|
||
- '/etc/timezone:/etc/timezone:ro'
|
||
- '/etc/localtime:/etc/localtime:ro'
|
||
EOF
|
||
(cd /root/gitea_data && sudo docker compose up -d)
|
||
echo -e "\n## Gitea 凭证 (全装部署: $(date))" >> ${STATE_FILE}
|
||
echo "GITEA_DOMAIN=${GT_DOMAIN}" >> ${STATE_FILE}
|
||
update_tunnel_config "${GT_DOMAIN}" "http://127.0.0.1:3000" "Gitea"
|
||
echo -e "${GREEN}✅ Gitea 部署完成(首次访问完成初始化设置)${NC}"
|
||
else
|
||
echo -e "${YELLOW}⏭️ Gitea 已安装,跳过${NC}"
|
||
fi
|
||
|
||
# ── 16. JDownloader ───────────────────────────────────────────
|
||
_step "部署 JDownloader 下载器"
|
||
if [[ -n "${SKIP_MAP[16]}" ]]; then echo -e "${DIM} ⏭️ 已跳过${NC}"
|
||
elif [ ! -d "/root/jdownloader_data" ]; then
|
||
local JD_DOMAIN="jd.${MAIN_DOMAIN}"
|
||
local JD_VNC_PASS="VNC-$(_gen_password 8)"
|
||
mkdir -p /root/jdownloader_data
|
||
cat > /root/jdownloader_data/docker-compose.yml << EOF
|
||
services:
|
||
jdownloader-2:
|
||
image: jlesage/jdownloader-2
|
||
container_name: jdownloader_app
|
||
restart: unless-stopped
|
||
ports:
|
||
- "127.0.0.1:5800:5800"
|
||
volumes:
|
||
- './config:/config'
|
||
- '/mnt/Downloads:/output'
|
||
environment:
|
||
- 'USER_ID=1000'
|
||
- 'GROUP_ID=1000'
|
||
- 'TZ=Asia/Shanghai'
|
||
- 'VNC_PASSWORD=${JD_VNC_PASS}'
|
||
EOF
|
||
(cd /root/jdownloader_data && sudo docker compose up -d)
|
||
echo -e "\n## JDownloader 凭证 (全装部署: $(date))" >> ${STATE_FILE}
|
||
echo "JD_DOMAIN=${JD_DOMAIN}" >> ${STATE_FILE}
|
||
echo "JDOWNLOADER_VNC_PASSWORD=${JD_VNC_PASS}" >> ${STATE_FILE}
|
||
update_tunnel_config "${JD_DOMAIN}" "http://127.0.0.1:5800" "JDownloader"
|
||
echo -e "${GREEN}✅ JDownloader 部署完成${NC}"
|
||
else
|
||
echo -e "${YELLOW}⏭️ JDownloader 已安装,跳过${NC}"
|
||
fi
|
||
|
||
# ── 17. MeTube (yt-dlp) ───────────────────────────────────────
|
||
_step "部署 MeTube 视频下载器"
|
||
if [[ -n "${SKIP_MAP[17]}" ]]; then echo -e "${DIM} ⏭️ 已跳过${NC}"
|
||
elif [ ! -d "/root/ytdlp_data" ]; then
|
||
local DL_DOMAIN="dl.${MAIN_DOMAIN}"
|
||
mkdir -p /root/ytdlp_data
|
||
cat > /root/ytdlp_data/docker-compose.yml << EOF
|
||
services:
|
||
ytdlp-ui:
|
||
image: ghcr.io/alexta69/metube:latest
|
||
container_name: ytdlp_app
|
||
restart: unless-stopped
|
||
ports:
|
||
- "127.0.0.1:8999:8081"
|
||
volumes:
|
||
- '/mnt/Downloads:/downloads'
|
||
environment:
|
||
- TZ=Asia/Shanghai
|
||
EOF
|
||
(cd /root/ytdlp_data && sudo docker compose up -d)
|
||
echo -e "\n## MeTube/yt-dlp 凭证 (全装部署: $(date))" >> ${STATE_FILE}
|
||
echo "YTDL_DOMAIN=${DL_DOMAIN}" >> ${STATE_FILE}
|
||
echo "YTDL_DOWNLOAD_DIR=/mnt/Downloads" >> ${STATE_FILE}
|
||
update_tunnel_config "${DL_DOMAIN}" "http://127.0.0.1:8999" "MeTube 下载器"
|
||
echo -e "${GREEN}✅ MeTube 部署完成(如遇 YouTube 验证,可后续配置 cookies.txt)${NC}"
|
||
else
|
||
echo -e "${YELLOW}⏭️ MeTube 已安装,跳过${NC}"
|
||
fi
|
||
|
||
# ── 18. Draw.io ───────────────────────────────────────────────
|
||
_step "部署 Draw.io 绘图工具"
|
||
if [[ -n "${SKIP_MAP[18]}" ]]; then echo -e "${DIM} ⏭️ 已跳过${NC}"
|
||
elif [ ! -d "/root/drawio_data" ]; then
|
||
local DRAW_DOMAIN="draw.${MAIN_DOMAIN}"
|
||
mkdir -p /root/drawio_data
|
||
cat > /root/drawio_data/docker-compose.yml << EOF
|
||
services:
|
||
drawio:
|
||
image: jgraph/drawio
|
||
container_name: drawio_app
|
||
restart: unless-stopped
|
||
ports:
|
||
- "127.0.0.1:8082:8080"
|
||
EOF
|
||
(cd /root/drawio_data && sudo docker compose up -d)
|
||
echo -e "\n## Draw.io 凭证 (全装部署: $(date))" >> ${STATE_FILE}
|
||
echo "DRAWIO_DOMAIN=${DRAW_DOMAIN}" >> ${STATE_FILE}
|
||
update_tunnel_config "${DRAW_DOMAIN}" "http://127.0.0.1:8082" "Draw.io" "access"
|
||
echo -e "${GREEN}✅ Draw.io 部署完成(已启用 Cloudflare Access 保护)${NC}"
|
||
else
|
||
echo -e "${YELLOW}⏭️ Draw.io 已安装,跳过${NC}"
|
||
fi
|
||
|
||
# ── 19. Gotify ────────────────────────────────────────────────
|
||
_step "部署 Gotify 消息推送服务"
|
||
if [[ -n "${SKIP_MAP[19]}" ]]; then echo -e "${DIM} ⏭️ 已跳过${NC}"
|
||
elif [ ! -d "/root/gotify_data" ]; then
|
||
local GT2_DOMAIN="push.${MAIN_DOMAIN}"
|
||
local GT_PASS="gotify_$(_gen_password 12)"
|
||
mkdir -p /root/gotify_data/data
|
||
cat > /root/gotify_data/docker-compose.yml << EOF
|
||
services:
|
||
gotify:
|
||
image: gotify/server:latest
|
||
container_name: gotify_app
|
||
restart: unless-stopped
|
||
ports:
|
||
- "127.0.0.1:8085:80"
|
||
volumes:
|
||
- './data:/app/data'
|
||
environment:
|
||
- GOTIFY_DEFAULTUSER_PASS=${GT_PASS}
|
||
- TZ=Asia/Shanghai
|
||
EOF
|
||
(cd /root/gotify_data && sudo docker compose up -d)
|
||
echo -e "\n## Gotify 凭证 (全装部署: $(date))" >> ${STATE_FILE}
|
||
echo "GOTIFY_DOMAIN=${GT2_DOMAIN}" >> ${STATE_FILE}
|
||
echo "GOTIFY_PASSWORD=${GT_PASS}" >> ${STATE_FILE}
|
||
update_tunnel_config "${GT2_DOMAIN}" "http://127.0.0.1:8085" "Gotify"
|
||
echo -e "${GREEN}✅ Gotify 部署完成(账号: admin,密码已写入凭证)${NC}"
|
||
else
|
||
echo -e "${YELLOW}⏭️ Gotify 已安装,跳过${NC}"
|
||
fi
|
||
|
||
# ── 20. Actual Budget ─────────────────────────────────────────
|
||
_step "部署 Actual Budget 个人记账"
|
||
if [[ -n "${SKIP_MAP[20]}" ]]; then echo -e "${DIM} ⏭️ 已跳过${NC}"
|
||
elif [ ! -d "/root/actual_budget_data" ]; then
|
||
local AB_DOMAIN="money.${MAIN_DOMAIN}"
|
||
mkdir -p /root/actual_budget_data/data
|
||
cat > /root/actual_budget_data/docker-compose.yml << EOF
|
||
services:
|
||
actual:
|
||
image: actualbudget/actual-server:latest
|
||
container_name: actual_budget_app
|
||
restart: unless-stopped
|
||
ports:
|
||
- "127.0.0.1:5006:5006"
|
||
volumes:
|
||
- './data:/data'
|
||
environment:
|
||
- TZ=Asia/Shanghai
|
||
EOF
|
||
(cd /root/actual_budget_data && sudo docker compose up -d)
|
||
echo -e "\n## Actual Budget 凭证 (全装部署: $(date))" >> ${STATE_FILE}
|
||
echo "ACTUAL_BUDGET_DOMAIN=${AB_DOMAIN}" >> ${STATE_FILE}
|
||
update_tunnel_config "${AB_DOMAIN}" "http://127.0.0.1:5006" "Actual Budget"
|
||
echo -e "${GREEN}✅ Actual Budget 部署完成${NC}"
|
||
else
|
||
echo -e "${YELLOW}⏭️ Actual Budget 已安装,跳过${NC}"
|
||
fi
|
||
|
||
# ── 21. Excalidraw ────────────────────────────────────────────
|
||
_step "部署 Excalidraw 在线白板"
|
||
if [[ -n "${SKIP_MAP[21]}" ]]; then echo -e "${DIM} ⏭️ 已跳过${NC}"
|
||
elif [ ! -d "/root/excalidraw_data" ]; then
|
||
local ED_DOMAIN="board.${MAIN_DOMAIN}"
|
||
mkdir -p /root/excalidraw_data
|
||
cat > /root/excalidraw_data/docker-compose.yml << EOF
|
||
services:
|
||
excalidraw:
|
||
image: excalidraw/excalidraw:latest
|
||
container_name: excalidraw_app
|
||
restart: unless-stopped
|
||
ports:
|
||
- "127.0.0.1:8086:80"
|
||
environment:
|
||
- TZ=Asia/Shanghai
|
||
EOF
|
||
(cd /root/excalidraw_data && sudo docker compose up -d)
|
||
echo -e "\n## Excalidraw 凭证 (全装部署: $(date))" >> ${STATE_FILE}
|
||
echo "EXCALIDRAW_DOMAIN=${ED_DOMAIN}" >> ${STATE_FILE}
|
||
update_tunnel_config "${ED_DOMAIN}" "http://127.0.0.1:8086" "Excalidraw"
|
||
echo -e "${GREEN}✅ Excalidraw 部署完成(无需登录,打开即用)${NC}"
|
||
else
|
||
echo -e "${YELLOW}⏭️ Excalidraw 已安装,跳过${NC}"
|
||
fi
|
||
|
||
# ── 22. Joplin Server ─────────────────────────────────────────
|
||
_step "部署 Joplin Server 私人笔记服务器"
|
||
if [[ -n "${SKIP_MAP[22]}" ]]; then echo -e "${DIM} ⏭️ 已跳过${NC}"
|
||
elif [ ! -d "/root/joplin_data" ]; then
|
||
local JP_DOMAIN="notes.${MAIN_DOMAIN}"
|
||
local JP_DB_PASS; JP_DB_PASS="$(_gen_password 16)"
|
||
mkdir -p /root/joplin_data/postgres
|
||
cat > /root/joplin_data/docker-compose.yml << EOF
|
||
services:
|
||
joplin:
|
||
image: ${IMG_JOPLIN_SERVER}
|
||
container_name: joplin_app
|
||
restart: unless-stopped
|
||
depends_on:
|
||
joplin_db:
|
||
condition: service_healthy
|
||
ports:
|
||
- "127.0.0.1:${PORT_JOPLIN}:22300"
|
||
environment:
|
||
- APP_PORT=22300
|
||
- APP_BASE_URL=https://${JP_DOMAIN}
|
||
- DB_CLIENT=pg
|
||
- POSTGRES_PASSWORD=${JP_DB_PASS}
|
||
- POSTGRES_DATABASE=joplin
|
||
- POSTGRES_USER=joplin
|
||
- POSTGRES_PORT=5432
|
||
- POSTGRES_HOST=joplin_db
|
||
- MAILER_ENABLED=0
|
||
- TZ=${TZ_DEFAULT}
|
||
joplin_db:
|
||
image: ${IMG_JOPLIN_DB}
|
||
container_name: joplin_db
|
||
restart: unless-stopped
|
||
volumes:
|
||
- './postgres:/var/lib/postgresql/data'
|
||
environment:
|
||
- POSTGRES_PASSWORD=${JP_DB_PASS}
|
||
- POSTGRES_USER=joplin
|
||
- POSTGRES_DB=joplin
|
||
healthcheck:
|
||
test: ["CMD-SHELL", "pg_isready -U joplin"]
|
||
interval: 10s
|
||
timeout: 5s
|
||
retries: 5
|
||
EOF
|
||
(cd /root/joplin_data && sudo docker compose up -d)
|
||
echo -e "\n## Joplin Server 凭证 (全装部署: $(date))" >> ${STATE_FILE}
|
||
echo "JOPLIN_DOMAIN=${JP_DOMAIN}" >> ${STATE_FILE}
|
||
echo "JOPLIN_DB_PASSWORD=${JP_DB_PASS}" >> ${STATE_FILE}
|
||
update_tunnel_config "${JP_DOMAIN}" "http://127.0.0.1:${PORT_JOPLIN}" "Joplin Server"
|
||
echo -e "${GREEN}✅ Joplin Server 部署完成(首次登录: admin@localhost / admin,请立即修改密码)${NC}"
|
||
else
|
||
echo -e "${YELLOW}⏭️ Joplin Server 已安装,跳过${NC}"
|
||
fi
|
||
|
||
# ── 23. Grafana ───────────────────────────────────────────────
|
||
_step "部署 Grafana + Prometheus 监控面板"
|
||
if [[ -n "${SKIP_MAP[23]}" ]]; then echo -e "${DIM} ⏭️ 已跳过${NC}"
|
||
elif [ ! -d "/root/grafana_data" ]; then
|
||
local GF_DOMAIN="monitor.${MAIN_DOMAIN}"
|
||
local GF_PASS="grafana_$(_gen_password 12)"
|
||
mkdir -p /root/grafana_data/{grafana,prometheus}
|
||
sudo chown -R 472:472 /root/grafana_data/grafana
|
||
cat > /root/grafana_data/prometheus/prometheus.yml << EOF
|
||
global:
|
||
scrape_interval: 15s
|
||
scrape_configs:
|
||
- job_name: 'node'
|
||
static_configs:
|
||
- targets: ['node_exporter:9100']
|
||
- job_name: 'cadvisor'
|
||
static_configs:
|
||
- targets: ['cadvisor:8080']
|
||
EOF
|
||
cat > /root/grafana_data/docker-compose.yml << EOF
|
||
services:
|
||
grafana:
|
||
image: grafana/grafana:latest
|
||
container_name: grafana_app
|
||
restart: unless-stopped
|
||
ports:
|
||
- "127.0.0.1:3003:3000"
|
||
volumes:
|
||
- './grafana:/var/lib/grafana'
|
||
environment:
|
||
- GF_SECURITY_ADMIN_PASSWORD=${GF_PASS}
|
||
- GF_USERS_ALLOW_SIGN_UP=false
|
||
- TZ=Asia/Shanghai
|
||
prometheus:
|
||
image: prom/prometheus:latest
|
||
container_name: prometheus_app
|
||
restart: unless-stopped
|
||
volumes:
|
||
- './prometheus/prometheus.yml:/etc/prometheus/prometheus.yml'
|
||
- prometheus_data:/prometheus
|
||
command:
|
||
- '--config.file=/etc/prometheus/prometheus.yml'
|
||
- '--storage.tsdb.retention.time=30d'
|
||
node_exporter:
|
||
image: prom/node-exporter:latest
|
||
container_name: node_exporter
|
||
restart: unless-stopped
|
||
pid: host
|
||
volumes:
|
||
- '/proc:/host/proc:ro'
|
||
- '/sys:/host/sys:ro'
|
||
- '/:/rootfs:ro'
|
||
command:
|
||
- '--path.procfs=/host/proc'
|
||
- '--path.sysfs=/host/sys'
|
||
- '--collector.filesystem.ignored-mount-points=^/(sys|proc|dev|host|etc)($$|/)'
|
||
cadvisor:
|
||
image: gcr.io/cadvisor/cadvisor:latest
|
||
container_name: cadvisor
|
||
restart: unless-stopped
|
||
privileged: true
|
||
volumes:
|
||
- '/:/rootfs:ro'
|
||
- '/var/run:/var/run:ro'
|
||
- '/sys:/sys:ro'
|
||
- '/var/lib/docker/:/var/lib/docker:ro'
|
||
volumes:
|
||
prometheus_data:
|
||
EOF
|
||
(cd /root/grafana_data && sudo docker compose up -d)
|
||
echo -e "\n## Grafana 凭证 (全装部署: $(date))" >> ${STATE_FILE}
|
||
echo "GRAFANA_DOMAIN=${GF_DOMAIN}" >> ${STATE_FILE}
|
||
echo "GRAFANA_PASSWORD=${GF_PASS}" >> ${STATE_FILE}
|
||
update_tunnel_config "${GF_DOMAIN}" "http://127.0.0.1:3003" "Grafana"
|
||
echo -e "${GREEN}✅ Grafana 部署完成(账号: admin,密码已写入凭证。数据源填 http://prometheus_app:9090,推荐仪表盘 ID: 1860 / 193)${NC}"
|
||
else
|
||
echo -e "${YELLOW}⏭️ Grafana 已安装,跳过${NC}"
|
||
fi
|
||
|
||
# ── 24. Home Assistant ────────────────────────────────────────
|
||
_step "部署 Home Assistant 智能家居"
|
||
if [[ -n "${SKIP_MAP[24]}" ]]; then echo -e "${DIM} ⏭️ 已跳过${NC}"
|
||
elif [ ! -d "/root/home_assistant_data" ]; then
|
||
local HA_DOMAIN="ha.${MAIN_DOMAIN}"
|
||
mkdir -p /root/home_assistant_data/config
|
||
cat > /root/home_assistant_data/docker-compose.yml << EOF
|
||
services:
|
||
homeassistant:
|
||
image: ghcr.io/home-assistant/home-assistant:stable
|
||
container_name: home_assistant_app
|
||
restart: always
|
||
ports:
|
||
- "127.0.0.1:8123:8123"
|
||
volumes:
|
||
- './config:/config'
|
||
- '/etc/localtime:/etc/localtime:ro'
|
||
environment:
|
||
- 'TZ=Asia/Shanghai'
|
||
EOF
|
||
cat > /root/home_assistant_data/config/configuration.yaml << EOF
|
||
default_config:
|
||
|
||
http:
|
||
use_x_forwarded_for: true
|
||
trusted_proxies:
|
||
- 127.0.0.1
|
||
- 172.16.0.0/12
|
||
- 10.0.0.0/8
|
||
- 192.168.0.0/16
|
||
EOF
|
||
(cd /root/home_assistant_data && sudo docker compose up -d)
|
||
echo -e "\n## Home Assistant 凭证 (全装部署: $(date))" >> ${STATE_FILE}
|
||
echo "HA_DOMAIN=${HA_DOMAIN}" >> ${STATE_FILE}
|
||
update_tunnel_config "${HA_DOMAIN}" "http://127.0.0.1:8123" "Home Assistant"
|
||
echo -e "${GREEN}✅ Home Assistant 部署完成${NC}"
|
||
else
|
||
echo -e "${YELLOW}⏭️ Home Assistant 已安装,跳过${NC}"
|
||
fi
|
||
|
||
# ── 25. WordPress ─────────────────────────────────────────────
|
||
_step "部署 WordPress 博客"
|
||
if [[ -n "${SKIP_MAP[25]}" ]]; then echo -e "${DIM} ⏭️ 已跳过${NC}"
|
||
elif [ ! -d "/root/wordpress_data" ]; then
|
||
local WP_DOMAIN="blog.${MAIN_DOMAIN}"
|
||
local WP_DB_PASS="WpDb-pW_$(_gen_password 12)"
|
||
local WP_DB_ROOT_PASS="WpRoot-pW_$(_gen_password 12)"
|
||
mkdir -p /root/wordpress_data
|
||
cat > /root/wordpress_data/docker-compose.yml << EOF
|
||
services:
|
||
db:
|
||
image: mariadb:11.4
|
||
container_name: wordpress_db
|
||
restart: unless-stopped
|
||
volumes:
|
||
- './db_data:/var/lib/mysql'
|
||
environment:
|
||
MYSQL_ROOT_PASSWORD: ${WP_DB_ROOT_PASS}
|
||
MYSQL_DATABASE: wordpress
|
||
MYSQL_USER: wordpress
|
||
MYSQL_PASSWORD: ${WP_DB_PASS}
|
||
wordpress:
|
||
image: wordpress:latest
|
||
container_name: wordpress_app
|
||
restart: unless-stopped
|
||
ports:
|
||
- "127.0.0.1:8890:80"
|
||
volumes:
|
||
- './html:/var/www/html'
|
||
environment:
|
||
WORDPRESS_DB_HOST: db
|
||
WORDPRESS_DB_USER: wordpress
|
||
WORDPRESS_DB_PASSWORD: ${WP_DB_PASS}
|
||
WORDPRESS_DB_NAME: wordpress
|
||
depends_on:
|
||
- db
|
||
EOF
|
||
(cd /root/wordpress_data && sudo docker compose up -d)
|
||
echo -e "\n## WordPress 凭证 (全装部署: $(date))" >> ${STATE_FILE}
|
||
echo "WORDPRESS_DOMAIN=${WP_DOMAIN}" >> ${STATE_FILE}
|
||
echo "WORDPRESS_DB_PASSWORD=${WP_DB_PASS}" >> ${STATE_FILE}
|
||
update_tunnel_config "${WP_DOMAIN}" "http://127.0.0.1:8890" "WordPress"
|
||
echo -e "${GREEN}✅ WordPress 部署完成${NC}"
|
||
else
|
||
echo -e "${YELLOW}⏭️ WordPress 已安装,跳过${NC}"
|
||
fi
|
||
|
||
# ── 26. Glances ───────────────────────────────────────────────
|
||
_step "部署 Glances 实时资源监控"
|
||
if [[ -n "${SKIP_MAP[26]}" ]]; then echo -e "${DIM} ⏭️ 已跳过${NC}"
|
||
elif ! docker inspect glances_app &>/dev/null 2>&1; then
|
||
local GL_DOMAIN="glances.${MAIN_DOMAIN}"
|
||
docker run -d \
|
||
--name glances_app \
|
||
--restart always \
|
||
-p 127.0.0.1:61208:61208 \
|
||
-v /var/run/docker.sock:/var/run/docker.sock:ro \
|
||
-v /:/host:ro \
|
||
--pid host \
|
||
nicolargo/glances:latest-full \
|
||
/usr/local/bin/glances -w
|
||
echo -e "\n## Glances 凭证 (全装部署: $(date))" >> ${STATE_FILE}
|
||
echo "GLANCES_DOMAIN=${GL_DOMAIN}" >> ${STATE_FILE}
|
||
update_tunnel_config "${GL_DOMAIN}" "http://127.0.0.1:61208" "Glances Monitor"
|
||
echo -e "${GREEN}✅ Glances 部署完成${NC}"
|
||
else
|
||
echo -e "${YELLOW}⏭️ Glances 已安装,跳过${NC}"
|
||
fi
|
||
|
||
# ── 27. Fail2ban ──────────────────────────────────────────────
|
||
_step "部署 Fail2ban 防暴力破解"
|
||
if [[ -n "${SKIP_MAP[27]}" ]]; then echo -e "${DIM} ⏭️ 已跳过${NC}"
|
||
elif ! systemctl is-active --quiet fail2ban 2>/dev/null; then
|
||
sudo apt-get update >/dev/null 2>&1
|
||
sudo apt-get install -y fail2ban
|
||
sudo tee /etc/fail2ban/jail.local > /dev/null << 'EOF'
|
||
[DEFAULT]
|
||
bantime = 3600
|
||
findtime = 600
|
||
maxretry = 3
|
||
|
||
[sshd]
|
||
enabled = true
|
||
EOF
|
||
sudo systemctl enable --now fail2ban
|
||
echo -e "${GREEN}✅ Fail2ban 部署完成(SSH 防护已开启)${NC}"
|
||
else
|
||
echo -e "${YELLOW}⏭️ Fail2ban 已安装,跳过${NC}"
|
||
fi
|
||
|
||
# ── 完成汇总 ──────────────────────────────────────────────────
|
||
local END_TIME; END_TIME=$(date +%s)
|
||
local ELAPSED=$(( (END_TIME - START_TIME) / 60 ))
|
||
|
||
echo ""
|
||
echo -e "${GREEN}╔═══════════════════════════════════════════════════════════════════════════════╗${NC}"
|
||
printf "${GREEN}║${NC} 🎉 甲骨文满配全装完成!共 27 个服务,耗时约 ${ELAPSED} 分钟\n"
|
||
echo -e "${GREEN}╠═════════════════╦══════════════════════════════════════╦════════════════╣${NC}"
|
||
echo -e "${GREEN}║ 服务 ║ 访问域名 ║ 本地端口 ║${NC}"
|
||
echo -e "${GREEN}╠═════════════════╬══════════════════════════════════════╬════════════════╣${NC}"
|
||
printf "${GREEN}║${CYAN} %-15s${GREEN}║${NC} %-36s${GREEN}║${NC} %-14s${GREEN}║${NC}\n" "Nextcloud" "nextcloud.${MAIN_DOMAIN}" "8888"
|
||
printf "${GREEN}║${CYAN} %-15s${GREEN}║${NC} %-36s${GREEN}║${NC} %-14s${GREEN}║${NC}\n" "OnlyOffice" "onlyoffice.${MAIN_DOMAIN}" "8889"
|
||
printf "${GREEN}║${CYAN} %-15s${GREEN}║${NC} %-36s${GREEN}║${NC} %-14s${GREEN}║${NC}\n" "Jellyfin" "jellyfin.${MAIN_DOMAIN}" "8096"
|
||
printf "${GREEN}║${CYAN} %-15s${GREEN}║${NC} %-36s${GREEN}║${NC} %-14s${GREEN}║${NC}\n" "Navidrome" "music.${MAIN_DOMAIN}" "4533"
|
||
printf "${GREEN}║${CYAN} %-15s${GREEN}║${NC} %-36s${GREEN}║${NC} %-14s${GREEN}║${NC}\n" "Miniflux" "rss.${MAIN_DOMAIN}" "8091"
|
||
printf "${GREEN}║${CYAN} %-15s${GREEN}║${NC} %-36s${GREEN}║${NC} %-14s${GREEN}║${NC}\n" "Memos" "memos.${MAIN_DOMAIN}" "5230"
|
||
printf "${GREEN}║${CYAN} %-15s${GREEN}║${NC} %-36s${GREEN}║${NC} %-14s${GREEN}║${NC}\n" "AI 大脑" "ai.${MAIN_DOMAIN}" "3001"
|
||
printf "${GREEN}║${CYAN} %-15s${GREEN}║${NC} %-36s${GREEN}║${NC} %-14s${GREEN}║${NC}\n" "Uptime Kuma" "status.${MAIN_DOMAIN}" "3002"
|
||
printf "${GREEN}║${CYAN} %-15s${GREEN}║${NC} %-36s${GREEN}║${NC} %-14s${GREEN}║${NC}\n" "qBittorrent" "bt.${MAIN_DOMAIN}" "8080"
|
||
printf "${GREEN}║${CYAN} %-15s${GREEN}║${NC} %-36s${GREEN}║${NC} %-14s${GREEN}║${NC}\n" "SillyTavern" "st.${MAIN_DOMAIN}" "8000"
|
||
printf "${GREEN}║${CYAN} %-15s${GREEN}║${NC} %-36s${GREEN}║${NC} %-14s${GREEN}║${NC}\n" "N8N 工作流" "n8n.${MAIN_DOMAIN}" "5678"
|
||
printf "${GREEN}║${CYAN} %-15s${GREEN}║${NC} %-36s${GREEN}║${NC} %-14s${GREEN}║${NC}\n" "Immich 相册" "photos.${MAIN_DOMAIN}" "2283"
|
||
printf "${GREEN}║${CYAN} %-15s${GREEN}║${NC} %-36s${GREEN}║${NC} %-14s${GREEN}║${NC}\n" "Calibre-Web" "books.${MAIN_DOMAIN}" "8083"
|
||
printf "${GREEN}║${CYAN} %-15s${GREEN}║${NC} %-36s${GREEN}║${NC} %-14s${GREEN}║${NC}\n" "Kavita 阅读" "manga.${MAIN_DOMAIN}" "5000"
|
||
printf "${GREEN}║${CYAN} %-15s${GREEN}║${NC} %-36s${GREEN}║${NC} %-14s${GREEN}║${NC}\n" "Gitea 代码" "git.${MAIN_DOMAIN}" "3000"
|
||
printf "${GREEN}║${CYAN} %-15s${GREEN}║${NC} %-36s${GREEN}║${NC} %-14s${GREEN}║${NC}\n" "JDownloader" "jd.${MAIN_DOMAIN}" "5800"
|
||
printf "${GREEN}║${CYAN} %-15s${GREEN}║${NC} %-36s${GREEN}║${NC} %-14s${GREEN}║${NC}\n" "MeTube" "dl.${MAIN_DOMAIN}" "8999"
|
||
printf "${GREEN}║${CYAN} %-15s${GREEN}║${NC} %-36s${GREEN}║${NC} %-14s${GREEN}║${NC}\n" "Draw.io" "draw.${MAIN_DOMAIN}" "8082"
|
||
printf "${GREEN}║${CYAN} %-15s${GREEN}║${NC} %-36s${GREEN}║${NC} %-14s${GREEN}║${NC}\n" "Gotify 推送" "push.${MAIN_DOMAIN}" "8085"
|
||
printf "${GREEN}║${CYAN} %-15s${GREEN}║${NC} %-36s${GREEN}║${NC} %-14s${GREEN}║${NC}\n" "Actual Budget" "money.${MAIN_DOMAIN}" "5006"
|
||
printf "${GREEN}║${CYAN} %-15s${GREEN}║${NC} %-36s${GREEN}║${NC} %-14s${GREEN}║${NC}\n" "Excalidraw" "board.${MAIN_DOMAIN}" "8086"
|
||
printf "${GREEN}║${CYAN} %-15s${GREEN}║${NC} %-36s${GREEN}║${NC} %-14s${GREEN}║${NC}\n" "Joplin 笔记" "notes.${MAIN_DOMAIN}" "22300"
|
||
printf "${GREEN}║${CYAN} %-15s${GREEN}║${NC} %-36s${GREEN}║${NC} %-14s${GREEN}║${NC}\n" "Grafana" "monitor.${MAIN_DOMAIN}" "3003"
|
||
printf "${GREEN}║${CYAN} %-15s${GREEN}║${NC} %-36s${GREEN}║${NC} %-14s${GREEN}║${NC}\n" "Home Assistant" "ha.${MAIN_DOMAIN}" "8123"
|
||
printf "${GREEN}║${CYAN} %-15s${GREEN}║${NC} %-36s${GREEN}║${NC} %-14s${GREEN}║${NC}\n" "WordPress" "blog.${MAIN_DOMAIN}" "8890"
|
||
printf "${GREEN}║${CYAN} %-15s${GREEN}║${NC} %-36s${GREEN}║${NC} %-14s${GREEN}║${NC}\n" "Glances" "glances.${MAIN_DOMAIN}" "61208"
|
||
printf "${GREEN}║${CYAN} %-15s${GREEN}║${NC} %-36s${GREEN}║${NC} %-14s${GREEN}║${NC}\n" "Fail2ban" "(系统服务,无域名)" "-"
|
||
echo -e "${GREEN}╠═══════════════════════════════════════════════════════════════════════════════╣${NC}"
|
||
echo -e "${YELLOW}║ 📋 后续操作: ║${NC}"
|
||
echo -e "${YELLOW}║ 1. 前往 Cloudflare DNS 将以上子域名 CNAME 指向您的 Tunnel ║${NC}"
|
||
echo -e "${YELLOW}║ 2. 访问 Nextcloud 完成初始化(数据库密码用选项 10.5 查看) ║${NC}"
|
||
echo -e "${YELLOW}║ 3. 执行选项 10.1) Nextcloud 精装修,10.2) 高速风景版 ║${NC}"
|
||
echo -e "${YELLOW}║ 4. 执行选项 5.2) 为 AI 大脑安装语言模型 ║${NC}"
|
||
echo -e "${YELLOW}║ 5. 用选项 10.5) 随时查看所有密码、端口、凭证 ║${NC}"
|
||
echo -e "${GREEN}╚═══════════════════════════════════════════════════════════════════════════════╝${NC}"
|
||
echo ""
|
||
echo -e "${CYAN}所有凭证(密码/域名/端口)已统一保存至 ${STATE_FILE}${NC}"
|
||
|
||
# ── 打印 DNS 手动添加清单 ─────────────────────────────────────
|
||
local _t_id
|
||
_t_id=$(grep "CLOUDFLARE_TOKEN" "${STATE_FILE}" 2>/dev/null | cut -d'=' -f2 | python3 -c "
|
||
import json,sys,base64
|
||
t=sys.stdin.read().strip()
|
||
try:
|
||
d=json.loads(base64.b64decode(t + '==').decode())
|
||
print(d.get('t','<Tunnel-ID>'))
|
||
except:
|
||
print('<Tunnel-ID>')
|
||
" 2>/dev/null)
|
||
[ -z "$_t_id" ] && _t_id="<在 CF Zero Trust → Tunnels 找到您的 Tunnel ID>"
|
||
|
||
echo ""
|
||
echo -e "${CYAN}══════════════════════════════════════════════════════════════════════${NC}"
|
||
echo -e "${CYAN} 📋 Cloudflare DNS 手动添加清单(全部 CNAME,代理状态:橙色云朵)${NC}"
|
||
echo -e "${CYAN}══════════════════════════════════════════════════════════════════════${NC}"
|
||
echo -e "${DIM} 内容(目标)统一填写:${YELLOW}${_t_id}.cfargotunnel.com${NC}"
|
||
echo ""
|
||
local domains_list=(
|
||
"nextcloud:1" "onlyoffice:2" "jellyfin:3" "music:4" "rss:5"
|
||
"memos:6" "ai:7" "status:8" "bt:9" "st:10" "n8n:11"
|
||
"photos:12" "books:13" "manga:14" "git:15" "jd:16"
|
||
"dl:17" "draw:18" "push:19" "money:20" "board:21"
|
||
"notes:22" "monitor:23" "ha:24" "blog:25" "glances:26"
|
||
)
|
||
for item in "${domains_list[@]}"; do
|
||
local sub="${item%%:*}"
|
||
local num="${item##*:}"
|
||
[[ -z "${SKIP_MAP[$num]}" ]] && printf " ${YELLOW}%-22s${NC} %s\n" "${sub}" "${_t_id}.cfargotunnel.com"
|
||
done
|
||
echo -e "${CYAN}══════════════════════════════════════════════════════════════════════${NC}"
|
||
|
||
echo -e "\n${GREEN}按任意键返回主菜单...${NC}"; read -n 1 -s
|
||
}
|
||
|
||
# ================================================================
|
||
# --- 删除/卸载 SSHFS 远程VPS挂载 ---
|
||
# ================================================================
|
||
remove_sshfs_mount() {
|
||
clear
|
||
echo -e "${BLUE}=== 🗑️ 删除外挂VPS网盘 (SSHFS) ===${NC}"
|
||
echo ""
|
||
|
||
# 读取 STATE_FILE 中的挂载记录
|
||
local STATE_FILE="/root/.vps_setup_credentials"
|
||
local mounts=()
|
||
local units=()
|
||
local remotes=()
|
||
|
||
if [ -f "$STATE_FILE" ]; then
|
||
while IFS='=' read -r key val; do
|
||
[[ "$key" =~ ^SSHFS_.*_LOCAL$ ]] && mounts+=("$val")
|
||
[[ "$key" =~ ^SSHFS_.*_UNIT$ ]] && units+=("$val")
|
||
[[ "$key" =~ ^SSHFS_.*_REMOTE$ ]] && remotes+=("$val")
|
||
done < "$STATE_FILE"
|
||
fi
|
||
|
||
# 同时扫描当前实际 fuse.sshfs 挂载点(避免记录丢失时也能操作)
|
||
echo -e "${YELLOW}── 当前 SSHFS 挂载点 ──────────────────────────────${NC}"
|
||
local live_mounts=()
|
||
while IFS= read -r line; do
|
||
local mp; mp=$(echo "$line" | awk '{print $3}')
|
||
live_mounts+=("$mp")
|
||
done < <(mount 2>/dev/null | grep -E "fuse\.sshfs|type fuse[^.]")
|
||
|
||
if [ ${#live_mounts[@]} -eq 0 ] && [ ${#mounts[@]} -eq 0 ]; then
|
||
echo -e "${YELLOW} 当前没有任何 SSHFS 挂载记录。${NC}"
|
||
echo -e "\n${GREEN}按任意键返回...${NC}"; read -n 1 -s; return
|
||
fi
|
||
|
||
# 合并去重显示
|
||
local all_mounts=()
|
||
for m in "${live_mounts[@]}" "${mounts[@]}"; do
|
||
local found=false
|
||
for a in "${all_mounts[@]}"; do [ "$a" = "$m" ] && found=true && break; done
|
||
$found || all_mounts+=("$m")
|
||
done
|
||
|
||
local i=1
|
||
for mp in "${all_mounts[@]}"; do
|
||
local is_live=""
|
||
# mount 输出格式:`source on /mnt/NAME type fuse.sshfs (...)`
|
||
# 用 " on <mp> " 精确匹配挂载点字段,避免路径被当成 source 端误匹配
|
||
if mount 2>/dev/null | grep -qF " on ${mp} "; then
|
||
is_live="${GREEN}[已挂载]${NC}"
|
||
else
|
||
is_live="${RED}[未挂载/残骸]${NC}"
|
||
fi
|
||
# 从 STATE_FILE 读取对应 remote 信息
|
||
local remote_info=""
|
||
for j in "${!mounts[@]}"; do
|
||
[ "${mounts[$j]}" = "$mp" ] && remote_info=" ← ${remotes[$j]:-}" && break
|
||
done
|
||
echo -e " ${YELLOW}${i})${NC} ${mp}${remote_info} ${is_live}"
|
||
((i++))
|
||
done
|
||
|
||
echo ""
|
||
read -p "请输入要删除的编号 (q 取消): " SEL
|
||
[[ "$SEL" == "q" || "$SEL" == "Q" || -z "$SEL" ]] && return
|
||
|
||
if ! [[ "$SEL" =~ ^[0-9]+$ ]] || [ "$SEL" -lt 1 ] || [ "$SEL" -gt ${#all_mounts[@]} ]; then
|
||
echo -e "${RED}❌ 无效编号${NC}"; sleep 2; return
|
||
fi
|
||
|
||
local TARGET_MOUNT="${all_mounts[$((SEL-1))]}"
|
||
local UNIT_NAME="mnt-$(echo "${TARGET_MOUNT}" | sed 's|^/||' | tr '/' '-')"
|
||
|
||
echo ""
|
||
echo -e "${YELLOW}即将删除挂载点:${CYAN}${TARGET_MOUNT}${NC}"
|
||
echo -e " 对应 systemd 服务:${UNIT_NAME}.service"
|
||
echo ""
|
||
echo -e " ${YELLOW}1)${NC} 仅卸载(保留 systemd 服务,下次重启不再自动挂载但服务文件还在)"
|
||
echo -e " ${YELLOW}2)${NC} 完全清除(卸载 + 删除 systemd 服务 + 清除凭证记录)【推荐】"
|
||
echo -e " ${YELLOW}q)${NC} 取消"
|
||
echo ""
|
||
read -p "请选择: " MODE
|
||
|
||
case "$MODE" in
|
||
1|2)
|
||
# Step 1: 卸载挂载点
|
||
if mount 2>/dev/null | grep -qF " on ${TARGET_MOUNT} "; then
|
||
echo -e "${YELLOW}正在卸载 ${TARGET_MOUNT}...${NC}"
|
||
fusermount -u "${TARGET_MOUNT}" 2>/dev/null \
|
||
|| umount "${TARGET_MOUNT}" 2>/dev/null \
|
||
|| umount -l "${TARGET_MOUNT}" 2>/dev/null
|
||
if ! mount 2>/dev/null | grep -qF " on ${TARGET_MOUNT} "; then
|
||
echo -e "${GREEN}✅ 卸载成功${NC}"
|
||
else
|
||
echo -e "${RED}❌ 卸载失败,可能有进程正在使用该挂载点,请关闭后重试。${NC}"
|
||
read -n 1 -s -r -p "按任意键返回..."; return
|
||
fi
|
||
else
|
||
echo -e "${YELLOW} 该挂载点当前未挂载,跳过卸载步骤。${NC}"
|
||
fi
|
||
|
||
if [ "$MODE" == "2" ]; then
|
||
# Step 2: 停用并删除 systemd 服务
|
||
if systemctl list-units --all 2>/dev/null | grep -q "${UNIT_NAME}.service"; then
|
||
echo -e "${YELLOW}正在停用并删除 systemd 服务 ${UNIT_NAME}.service...${NC}"
|
||
systemctl disable --now "${UNIT_NAME}.service" 2>/dev/null
|
||
rm -f "/etc/systemd/system/${UNIT_NAME}.service"
|
||
systemctl daemon-reload
|
||
echo -e "${GREEN}✅ systemd 服务已清除${NC}"
|
||
else
|
||
echo -e "${YELLOW} 未找到对应 systemd 服务,跳过。${NC}"
|
||
fi
|
||
|
||
# Step 3: 删除私钥文件(如有)
|
||
local IP_PART
|
||
IP_PART=$(echo "${TARGET_MOUNT}" | sed 's|/mnt/||' | tr '-' '.')
|
||
local KEY_PATH="/root/.ssh/sshfs_key_$(echo "${TARGET_MOUNT}" | sed 's|.*/||' | tr '-' '_')"
|
||
# 尝试从 STATE_FILE 推断 IP 格式的私钥路径
|
||
for key_try in /root/.ssh/sshfs_key_*; do
|
||
[ -f "$key_try" ] || continue
|
||
done
|
||
|
||
# Step 4: 从 STATE_FILE 清除对应记录
|
||
if [ -f "$STATE_FILE" ]; then
|
||
# 找到匹配此 LOCAL 路径的 IP key 前缀,然后清除三行
|
||
local ESCAPED_MOUNT
|
||
ESCAPED_MOUNT=$(echo "${TARGET_MOUNT}" | sed 's|/|\\/|g')
|
||
# 先找出对应的 IP(通过 _LOCAL= 值反查 key)
|
||
local MATCH_IP=""
|
||
while IFS='=' read -r k v; do
|
||
if [[ "$k" =~ ^SSHFS_(.*)_LOCAL$ ]] && [ "$v" = "$TARGET_MOUNT" ]; then
|
||
MATCH_IP="${BASH_REMATCH[1]}"
|
||
break
|
||
fi
|
||
done < "$STATE_FILE"
|
||
if [ -n "$MATCH_IP" ]; then
|
||
sed -i "/^SSHFS_${MATCH_IP}_LOCAL=/d" "$STATE_FILE"
|
||
sed -i "/^SSHFS_${MATCH_IP}_REMOTE=/d" "$STATE_FILE"
|
||
sed -i "/^SSHFS_${MATCH_IP}_UNIT=/d" "$STATE_FILE"
|
||
echo -e "${GREEN}✅ 凭证记录已清除${NC}"
|
||
fi
|
||
fi
|
||
|
||
# Step 5: 询问是否同步从 Nextcloud 外部存储删除
|
||
if docker ps 2>/dev/null | grep -q "nextcloud_app"; then
|
||
echo ""
|
||
echo -e "${YELLOW}检测到 Nextcloud 正在运行。${NC}"
|
||
echo -e "当前外部存储列表:"
|
||
docker exec -u www-data nextcloud_app php occ files_external:list 2>/dev/null
|
||
echo ""
|
||
read -p "是否同时从 Nextcloud 外部存储中删除对应条目?(y/n,默认n): " DEL_NC
|
||
if [ "$DEL_NC" == "y" ] || [ "$DEL_NC" == "Y" ]; then
|
||
read -p "请输入上面列表中对应的 Mount ID: " DEL_ID
|
||
if [ -n "$DEL_ID" ]; then
|
||
docker exec -u www-data nextcloud_app php occ files_external:delete "$DEL_ID" 2>/dev/null \
|
||
&& echo -e "${GREEN}✅ Nextcloud 外部存储已删除${NC}" \
|
||
|| echo -e "${RED}❌ 删除失败,请手动在 Nextcloud 管理界面操作${NC}"
|
||
fi
|
||
fi
|
||
fi
|
||
|
||
# Step 6: 询问是否删除本地挂载目录(空目录才删)
|
||
echo ""
|
||
read -p "是否删除本地空目录 ${TARGET_MOUNT}?(y/n,默认n): " DEL_DIR
|
||
if [ "$DEL_DIR" == "y" ] || [ "$DEL_DIR" == "Y" ]; then
|
||
rmdir "${TARGET_MOUNT}" 2>/dev/null \
|
||
&& echo -e "${GREEN}✅ 目录已删除${NC}" \
|
||
|| echo -e "${YELLOW}⚠️ 目录非空或删除失败,已跳过(数据安全,不强制删除)${NC}"
|
||
fi
|
||
|
||
echo ""
|
||
echo -e "${GREEN}=== ✅ 外挂VPS「${TARGET_MOUNT}」已完全清除 ===${NC}"
|
||
else
|
||
echo ""
|
||
echo -e "${GREEN}=== ✅ 已卸载「${TARGET_MOUNT}」(systemd 服务保留,重启后不会自动挂载)===${NC}"
|
||
fi
|
||
;;
|
||
q|Q|*) return ;;
|
||
esac
|
||
|
||
echo -e "\n${GREEN}按任意键返回...${NC}"; read -n 1 -s
|
||
}
|
||
|
||
# ================================================================
|
||
# --- SSHFS 管理子菜单(安装 + 删除)---
|
||
# ================================================================
|
||
manage_sshfs_menu() {
|
||
clear
|
||
echo -e "${BLUE}=== 🌐 外挂VPS网盘管理 (SSHFS) ===${NC}"
|
||
echo ""
|
||
# 显示当前挂载状态
|
||
local count; count=$(mount 2>/dev/null | grep -cE "fuse\.sshfs|type fuse[^.]" || echo 0)
|
||
if [ "$count" -gt 0 ]; then
|
||
echo -e "${GREEN} 当前已挂载 ${count} 个远程VPS:${NC}"
|
||
mount 2>/dev/null | grep -E "fuse\.sshfs|type fuse[^.]" | awk '{print " ✅ "$1" → "$3}'
|
||
else
|
||
echo -e "${YELLOW} 当前没有活跃的 SSHFS 挂载。${NC}"
|
||
fi
|
||
echo ""
|
||
echo -e " ${YELLOW}1)${NC} ➕ 添加/挂载新的远程VPS"
|
||
echo -e " ${YELLOW}2)${NC} 🗑️ 删除/卸载已有的远程VPS"
|
||
echo -e " ${YELLOW}3)${NC} 📋 查看所有挂载状态"
|
||
echo -e " ${YELLOW}q)${NC} 返回主菜单"
|
||
echo ""
|
||
read -p "请选择: " sub
|
||
case "$sub" in
|
||
1) install_sshfs_mount ;;
|
||
2) remove_sshfs_mount ;;
|
||
3)
|
||
echo ""
|
||
echo -e "${YELLOW}=== systemd 服务状态 ===${NC}"
|
||
local found_svc=false
|
||
for svc in $(systemctl list-units --type=service --all 2>/dev/null | grep "^mnt-" | awk '{print $1}'); do
|
||
found_svc=true
|
||
local _state; _state=$(systemctl is-active "$svc" 2>/dev/null)
|
||
[ "$_state" == "active" ] \
|
||
&& echo -e " ${GREEN}✅ $svc (运行中)${NC}" \
|
||
|| echo -e " ${RED}❌ $svc ($_state)${NC}"
|
||
done
|
||
$found_svc || echo -e " ${YELLOW}未找到任何 mnt-*.service${NC}"
|
||
echo ""
|
||
echo -e "${YELLOW}=== 当前 FUSE/SSHFS 挂载点 ===${NC}"
|
||
mount | grep -E "fuse|sshfs" | while read -r l; do echo -e " ${GREEN}$l${NC}"; done
|
||
echo ""
|
||
echo -e "${YELLOW}=== /mnt 目录 ===${NC}"
|
||
ls -la /mnt/
|
||
echo ""
|
||
read -n 1 -s -r -p "按任意键返回..."
|
||
manage_sshfs_menu
|
||
;;
|
||
*) return ;;
|
||
esac
|
||
}
|
||
|
||
# ================================================================
|
||
# --- 新增功能:SSHFS 远程VPS挂载网盘 ---
|
||
install_sshfs_mount() {
|
||
clear
|
||
echo -e "${BLUE}=== 🌐 连接远程VPS文件夹为本机网盘 (SSHFS) ===${NC}"
|
||
echo -e "${CYAN}此功能将另一台VPS的指定文件夹挂载到本机,并自动注册到 Nextcloud 外部存储。${NC}"
|
||
echo ""
|
||
|
||
# 安装 sshfs
|
||
if ! command -v sshfs &> /dev/null; then
|
||
echo -e "${YELLOW}正在安装 SSHFS...${NC}"
|
||
sudo apt-get update && sudo apt-get install -y sshfs
|
||
fi
|
||
|
||
# 收集信息
|
||
read -p "请输入对方VPS的IP地址: " REMOTE_IP
|
||
[ -z "$REMOTE_IP" ] && echo -e "${RED}IP不能为空!${NC}" && return
|
||
|
||
read -p "请输入对方VPS的SSH用户名 (默认 ubuntu): " REMOTE_USER
|
||
[ -z "$REMOTE_USER" ] && REMOTE_USER="ubuntu"
|
||
|
||
read -p "请输入对方VPS的SSH端口 (默认 22): " REMOTE_PORT
|
||
[ -z "$REMOTE_PORT" ] && REMOTE_PORT="22"
|
||
|
||
read -p "请输入要挂载对方的哪个文件夹路径 (例如 /home/ubuntu/data): " REMOTE_PATH
|
||
[ -z "$REMOTE_PATH" ] && echo -e "${RED}远程路径不能为空!${NC}" && return
|
||
|
||
read -p "请输入挂载到本机的路径 (例如 /mnt/TOKYO,留空自动生成): " LOCAL_MOUNT
|
||
if [ -z "$LOCAL_MOUNT" ]; then
|
||
LOCAL_MOUNT="/mnt/remote_$(echo $REMOTE_IP | tr '.' '_')"
|
||
fi
|
||
|
||
# 挂载点名称(用于菜单显示)
|
||
MOUNT_NAME=$(basename "$LOCAL_MOUNT")
|
||
|
||
echo ""
|
||
echo -e "${YELLOW}SSH认证方式:${NC}"
|
||
echo " 1) 使用 SSH 私钥文件(推荐,更安全稳定)"
|
||
echo " 2) 使用密码(需要安装 sshpass)"
|
||
read -p "请选择 (1/2): " AUTH_CHOICE
|
||
|
||
SSH_KEY_OPT=""
|
||
SSH_PASS_OPT=""
|
||
KEY_FILE=""
|
||
if [ "$AUTH_CHOICE" == "1" ]; then
|
||
echo ""
|
||
echo -e "${CYAN}请将对方给你的私钥内容粘贴进来,粘贴完成后新起一行输入 END 回车:${NC}"
|
||
KEY_FILE="/root/.ssh/sshfs_key_$(echo $REMOTE_IP | tr '.' '_')"
|
||
> "$KEY_FILE"
|
||
while IFS= read -r line; do
|
||
[ "$line" == "END" ] && break
|
||
echo "$line" >> "$KEY_FILE"
|
||
done
|
||
chmod 600 "$KEY_FILE"
|
||
SSH_KEY_OPT="-o IdentityFile=$KEY_FILE"
|
||
echo -e "${GREEN}私钥已保存到 $KEY_FILE${NC}"
|
||
else
|
||
apt-get install -y sshpass > /dev/null 2>&1
|
||
read -s -p "请输入对方VPS的SSH密码: " REMOTE_PASS
|
||
echo ""
|
||
SSH_PASS_OPT="sshpass -p '${REMOTE_PASS}'"
|
||
fi
|
||
|
||
# 创建挂载点
|
||
sudo mkdir -p "$LOCAL_MOUNT"
|
||
|
||
# 执行挂载(加入 uid=33,gid=33 确保 Nextcloud www-data 用户可读)
|
||
echo -e "${YELLOW}正在挂载,请稍候...${NC}"
|
||
MOUNT_CMD="sshfs ${REMOTE_USER}@${REMOTE_IP}:${REMOTE_PATH} ${LOCAL_MOUNT} -p ${REMOTE_PORT} ${SSH_KEY_OPT} -o StrictHostKeyChecking=no -o reconnect -o ServerAliveInterval=15 -o ServerAliveCountMax=3 -o allow_other -o uid=33,gid=33,default_permissions"
|
||
|
||
if [ -n "$SSH_PASS_OPT" ]; then
|
||
eval "${SSH_PASS_OPT} ${MOUNT_CMD}" 2>/dev/null
|
||
else
|
||
eval "${MOUNT_CMD}" 2>/dev/null
|
||
fi
|
||
|
||
if mount | grep -q "$LOCAL_MOUNT"; then
|
||
echo -e "${GREEN}✅ 挂载成功!以下是对方文件夹内容:${NC}"
|
||
ls "$LOCAL_MOUNT" | head -8
|
||
else
|
||
echo -e "${RED}❌ 挂载失败,请检查IP、用户名、端口或密钥是否正确。${NC}"
|
||
read -n 1 -s -r -p "按任意键返回..."; return
|
||
fi
|
||
|
||
# 设置开机自动挂载(包含正确的 uid/gid)
|
||
read -p "是否设置开机自动挂载?(y/n,默认y): " AUTO_MOUNT
|
||
UNIT_NAME="mnt-$(echo ${LOCAL_MOUNT} | sed 's|^/||' | tr '/' '-')"
|
||
if [ "$AUTO_MOUNT" != "n" ] && [ "$AUTO_MOUNT" != "N" ]; then
|
||
if [ -n "$SSH_PASS_OPT" ]; then
|
||
# 密码认证:ExecStart 用 sshpass
|
||
sudo tee /etc/systemd/system/${UNIT_NAME}.service > /dev/null << UNITEOF
|
||
[Unit]
|
||
Description=SSHFS Mount ${REMOTE_IP}:${REMOTE_PATH} to ${LOCAL_MOUNT}
|
||
After=network-online.target
|
||
Wants=network-online.target
|
||
|
||
[Service]
|
||
Type=oneshot
|
||
RemainAfterExit=yes
|
||
ExecStart=/bin/bash -c 'sshpass -p "${REMOTE_PASS}" sshfs ${REMOTE_USER}@${REMOTE_IP}:${REMOTE_PATH} ${LOCAL_MOUNT} -p ${REMOTE_PORT} -o StrictHostKeyChecking=no -o reconnect -o ServerAliveInterval=15 -o ServerAliveCountMax=3 -o allow_other -o uid=33,gid=33,default_permissions'
|
||
ExecStop=/bin/fusermount -u ${LOCAL_MOUNT}
|
||
|
||
[Install]
|
||
WantedBy=multi-user.target
|
||
UNITEOF
|
||
else
|
||
sudo tee /etc/systemd/system/${UNIT_NAME}.service > /dev/null << UNITEOF
|
||
[Unit]
|
||
Description=SSHFS Mount ${REMOTE_IP}:${REMOTE_PATH} to ${LOCAL_MOUNT}
|
||
After=network-online.target
|
||
Wants=network-online.target
|
||
|
||
[Service]
|
||
Type=oneshot
|
||
RemainAfterExit=yes
|
||
ExecStart=/usr/bin/sshfs ${REMOTE_USER}@${REMOTE_IP}:${REMOTE_PATH} ${LOCAL_MOUNT} -p ${REMOTE_PORT} ${SSH_KEY_OPT} -o StrictHostKeyChecking=no -o reconnect -o ServerAliveInterval=15 -o ServerAliveCountMax=3 -o allow_other -o uid=33,gid=33,default_permissions
|
||
ExecStop=/bin/fusermount -u ${LOCAL_MOUNT}
|
||
|
||
[Install]
|
||
WantedBy=multi-user.target
|
||
UNITEOF
|
||
fi
|
||
sudo systemctl daemon-reload
|
||
sudo systemctl enable "${UNIT_NAME}.service"
|
||
echo -e "${GREEN}✅ 已设置开机自动挂载!${NC}"
|
||
fi
|
||
|
||
# 自动注册到 Nextcloud 外部存储
|
||
if docker ps 2>/dev/null | grep -q "nextcloud_app"; then
|
||
echo ""
|
||
read -p "检测到 Nextcloud 正在运行,是否自动注册为外部存储?(y/n,默认y): " NC_REG
|
||
if [ "$NC_REG" != "n" ] && [ "$NC_REG" != "N" ]; then
|
||
echo -e "${YELLOW}正在注册 Nextcloud 外部存储...${NC}"
|
||
docker exec -u www-data nextcloud_app php occ files_external:create \
|
||
"/${MOUNT_NAME}" local null::null \
|
||
--config=datadir="${LOCAL_MOUNT}" 2>/dev/null \
|
||
&& echo -e "${GREEN}✅ 已自动注册!在 Nextcloud 文件页面刷新即可看到 /${MOUNT_NAME}${NC}" \
|
||
|| echo -e "${YELLOW}⚠️ 自动注册失败,请手动在 Nextcloud → 设置 → 外部存储 中添加路径 ${LOCAL_MOUNT}${NC}"
|
||
|
||
# 清除文件锁再扫描,避免 is locked 报错
|
||
sleep 3
|
||
echo -e "${YELLOW}正在清除旧文件锁...${NC}"
|
||
docker exec -u www-data nextcloud_app php occ maintenance:mode --on 2>/dev/null
|
||
local _DB_PASS
|
||
_DB_PASS=$(grep "MYSQL_PASSWORD" /root/nextcloud_data/docker-compose.yml 2>/dev/null | head -1 | sed 's/.*MYSQL_PASSWORD: //' | tr -d ' ')
|
||
[ -n "$_DB_PASS" ] && docker exec nextcloud_db mariadb -u nextclouduser -p"${_DB_PASS}" nextclouddb \
|
||
-e "DELETE FROM oc_file_locks WHERE 1;" 2>/dev/null
|
||
docker exec -u www-data nextcloud_app php occ maintenance:mode --off 2>/dev/null
|
||
|
||
echo -e "${YELLOW}正在扫描外部存储文件...${NC}"
|
||
docker exec -u www-data nextcloud_app php occ files:scan --all --quiet 2>/dev/null
|
||
echo -e "${GREEN}✅ 扫描完成!${NC}"
|
||
fi
|
||
fi
|
||
|
||
# 保存记录
|
||
echo -e "\n## SSHFS 挂载记录 (添加于: $(date))" >> ${STATE_FILE}
|
||
echo "SSHFS_${REMOTE_IP}_LOCAL=${LOCAL_MOUNT}" >> ${STATE_FILE}
|
||
echo "SSHFS_${REMOTE_IP}_REMOTE=${REMOTE_USER}@${REMOTE_IP}:${REMOTE_PATH}" >> ${STATE_FILE}
|
||
echo "SSHFS_${REMOTE_IP}_UNIT=${UNIT_NAME}" >> ${STATE_FILE}
|
||
|
||
echo ""
|
||
echo -e "${GREEN}=== ✅ 挂载完成 ===${NC}"
|
||
echo -e " 远程地址 :${CYAN}${REMOTE_USER}@${REMOTE_IP}:${REMOTE_PATH}${NC}"
|
||
echo -e " 本机路径 :${CYAN}${LOCAL_MOUNT}${NC}"
|
||
echo -e " 权限配置 :${GREEN}uid=33,gid=33 (www-data,Nextcloud 可直接读写)${NC}"
|
||
echo -e "\n${GREEN}按任意键返回主菜单...${NC}"; read -n 1 -s
|
||
}
|
||
|
||
|
||
# ================================================================
|
||
# --- 新增功能:部署 SillyTavern 酒馆 ---
|
||
# ── install_immich ──
|
||
install_immich() {
|
||
ensure_docker_installed || return; check_tunnel_installed || return; clear
|
||
echo -e "${BLUE}=== 📸 部署 Immich 私人相册(Google Photos 替代)===${NC}"
|
||
echo -e "${CYAN}Immich 支持手机自动备份、AI人脸识别、地图、相册共享${NC}"
|
||
echo ""
|
||
|
||
read -p "请输入您为 Immich 规划的子域名 (例如 photos.example.com): " IMMICH_DOMAIN
|
||
if [ -z "$IMMICH_DOMAIN" ]; then echo -e "${RED}域名不能为空!${NC}"; sleep 2; return; fi
|
||
|
||
# 询问媒体库路径
|
||
echo ""
|
||
echo -e "${CYAN}当前 /mnt 下可用目录:${NC}"
|
||
find /mnt -maxdepth 2 -mindepth 1 -type d 2>/dev/null | sort | while read -r d; do
|
||
echo -e " ${GREEN}${d}${NC}"
|
||
done
|
||
echo ""
|
||
echo -e "${CYAN}请输入照片存储路径(建议存在远程VPS挂载点,不占本机硬盘)${NC}"
|
||
read -p "照片路径 (默认 /mnt/Photos): " IMMICH_UPLOAD_PATH
|
||
[ -z "$IMMICH_UPLOAD_PATH" ] && IMMICH_UPLOAD_PATH="/mnt/Photos"
|
||
sudo mkdir -p "$IMMICH_UPLOAD_PATH"
|
||
|
||
# 生成随机密钥
|
||
local DB_PASSWORD="immich_db_$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 16)"
|
||
local JWT_SECRET="$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 32)"
|
||
|
||
mkdir -p /root/immich_data
|
||
cat > /root/immich_data/docker-compose.yml <<EOF
|
||
services:
|
||
immich-server:
|
||
image: ghcr.io/immich-app/immich-server:release
|
||
container_name: immich_server
|
||
restart: unless-stopped
|
||
ports:
|
||
- "127.0.0.1:2283:2283"
|
||
volumes:
|
||
- ${IMMICH_UPLOAD_PATH}:/usr/src/app/upload
|
||
- /etc/localtime:/etc/localtime:ro
|
||
environment:
|
||
DB_HOSTNAME: immich_postgres
|
||
DB_USERNAME: immich
|
||
DB_PASSWORD: ${DB_PASSWORD}
|
||
DB_DATABASE_NAME: immich
|
||
REDIS_HOSTNAME: immich_redis
|
||
JWT_SECRET: ${JWT_SECRET}
|
||
TZ: Asia/Shanghai
|
||
depends_on:
|
||
- immich-redis
|
||
- immich-postgres
|
||
|
||
immich-machine-learning:
|
||
image: ghcr.io/immich-app/immich-machine-learning:release
|
||
container_name: immich_ml
|
||
restart: unless-stopped
|
||
volumes:
|
||
- immich_model_cache:/cache
|
||
environment:
|
||
TZ: Asia/Shanghai
|
||
|
||
immich-redis:
|
||
image: redis:7-alpine
|
||
container_name: immich_redis
|
||
restart: unless-stopped
|
||
|
||
immich-postgres:
|
||
image: tensorchord/pgvecto-rs:pg14-v0.2.0
|
||
container_name: immich_postgres
|
||
restart: unless-stopped
|
||
environment:
|
||
POSTGRES_USER: immich
|
||
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
||
POSTGRES_DB: immich
|
||
volumes:
|
||
- immich_pgdata:/var/lib/postgresql/data
|
||
|
||
volumes:
|
||
immich_model_cache:
|
||
immich_pgdata:
|
||
EOF
|
||
|
||
echo -e "${YELLOW}[1/3] 正在拉取镜像并启动(首次需要几分钟)...${NC}"
|
||
if (cd /root/immich_data && sudo docker compose up -d); then
|
||
echo -e "${GREEN}✅ Immich 已启动,等待服务稳定...${NC}"; sleep 10
|
||
|
||
echo -e "${YELLOW}[2/3] 正在配置 Cloudflare Tunnel...${NC}"
|
||
update_tunnel_config "${IMMICH_DOMAIN}" "http://127.0.0.1:2283" "Immich 相册"
|
||
|
||
echo -e "${YELLOW}[3/3] 保存凭证...${NC}"
|
||
echo -e "\n## Immich 凭证 (部署于: $(date))" >> ${STATE_FILE}
|
||
echo "IMMICH_DOMAIN=${IMMICH_DOMAIN}" >> ${STATE_FILE}
|
||
echo "IMMICH_UPLOAD_PATH=${IMMICH_UPLOAD_PATH}" >> ${STATE_FILE}
|
||
echo "IMMICH_DB_PASSWORD=${DB_PASSWORD}" >> ${STATE_FILE}
|
||
|
||
echo ""
|
||
echo -e "${GREEN}╔══════════════════════════════════════════════════════════╗${NC}"
|
||
echo -e "${GREEN}║ ✅ Immich 部署成功! ║${NC}"
|
||
echo -e "${GREEN}╠══════════════════════════════════════════════════════════╣${NC}"
|
||
echo -e "${GREEN}║ 访问地址:${CYAN}https://${IMMICH_DOMAIN}${NC}"
|
||
echo -e "${GREEN}║ 照片路径:${CYAN}${IMMICH_UPLOAD_PATH}${NC}"
|
||
echo -e "${GREEN}║${NC}"
|
||
echo -e "${GREEN}║ ${YELLOW}首次访问需注册管理员账号(第一个注册即为管理员)${NC}"
|
||
echo -e "${GREEN}║ ${YELLOW}手机端下载 Immich App,填入域名即可自动备份${NC}"
|
||
echo -e "${GREEN}╚══════════════════════════════════════════════════════════╝${NC}"
|
||
else
|
||
echo -e "${RED}❌ Immich 部署失败!请检查日志。${NC}"
|
||
echo -e "${YELLOW}提示:docker compose -f /root/immich_data/docker-compose.yml logs${NC}"
|
||
fi
|
||
echo -e "\n${GREEN}按任意键返回主菜单...${NC}"; read -n 1 -s
|
||
}
|
||
|
||
# ── install_calibre_web ──
|
||
install_calibre_web() {
|
||
ensure_docker_installed || return; check_tunnel_installed || return; clear
|
||
echo -e "${BLUE}=== 📚 部署 Calibre-Web 电子书库 ===${NC}"
|
||
echo -e "${CYAN}支持 EPUB/PDF/MOBI,网页阅读,可推送到 Kindle${NC}"
|
||
echo ""
|
||
read -p "请输入 Calibre-Web 的子域名 (例如 books.example.com): " CW_DOMAIN
|
||
[ -z "$CW_DOMAIN" ] && { echo -e "${RED}域名不能为空!${NC}"; sleep 2; return; }
|
||
|
||
echo ""
|
||
echo -e "${CYAN}当前 /mnt 下可用目录:${NC}"
|
||
find /mnt -maxdepth 2 -mindepth 1 -type d 2>/dev/null | sort | while read -r d; do echo -e " ${GREEN}${d}${NC}"; done
|
||
echo ""
|
||
read -p "书库路径 (默认 /mnt/Books): " CW_BOOKS_PATH
|
||
[ -z "$CW_BOOKS_PATH" ] && CW_BOOKS_PATH="/mnt/Books"
|
||
sudo mkdir -p "$CW_BOOKS_PATH"
|
||
|
||
mkdir -p /root/calibre_web_data/config
|
||
cat > /root/calibre_web_data/docker-compose.yml <<EOF
|
||
services:
|
||
calibre-web:
|
||
image: lscr.io/linuxserver/calibre-web:latest
|
||
container_name: calibre_web_app
|
||
restart: unless-stopped
|
||
ports:
|
||
- "127.0.0.1:8083:8083"
|
||
volumes:
|
||
- './config:/config'
|
||
- '${CW_BOOKS_PATH}:/books'
|
||
environment:
|
||
- PUID=0
|
||
- PGID=0
|
||
- TZ=Asia/Shanghai
|
||
- DOCKER_MODS=linuxserver/mods:universal-calibre
|
||
EOF
|
||
if (cd /root/calibre_web_data && sudo docker compose up -d); then
|
||
echo -e "${GREEN}✅ Calibre-Web 已启动...${NC}"; sleep 5
|
||
update_tunnel_config "${CW_DOMAIN}" "http://127.0.0.1:8083" "Calibre-Web"
|
||
echo -e "\n## Calibre-Web 凭证 (部署于: $(date))" >> ${STATE_FILE}
|
||
echo "CALIBRE_WEB_DOMAIN=${CW_DOMAIN}" >> ${STATE_FILE}
|
||
echo "CALIBRE_WEB_BOOKS_PATH=${CW_BOOKS_PATH}" >> ${STATE_FILE}
|
||
echo ""
|
||
echo -e "${GREEN}╔══════════════════════════════════════════════════════╗${NC}"
|
||
echo -e "${GREEN}║ ✅ Calibre-Web 部署成功! ║${NC}"
|
||
echo -e "${GREEN}║ 访问:${CYAN}https://${CW_DOMAIN}${NC}"
|
||
echo -e "${GREEN}║ 默认账号:${CYAN}admin${NC} 默认密码:${CYAN}admin123${NC}"
|
||
echo -e "${GREEN}║ 首次登录后进入 设置→书库 填写路径: /books ║${NC}"
|
||
echo -e "${GREEN}╚══════════════════════════════════════════════════════╝${NC}"
|
||
else
|
||
echo -e "${RED}❌ Calibre-Web 部署失败!${NC}"
|
||
fi
|
||
echo -e "\n${GREEN}按任意键返回...${NC}"; read -n 1 -s
|
||
}
|
||
|
||
# ── install_kavita ──
|
||
install_kavita() {
|
||
ensure_docker_installed || return; check_tunnel_installed || return; clear
|
||
echo -e "${BLUE}=== 📖 部署 Kavita 阅读器 ===${NC}"
|
||
echo -e "${CYAN}支持漫画(CBZ/CBR)、EPUB、PDF,阅读体验极佳${NC}"
|
||
echo ""
|
||
read -p "请输入 Kavita 的子域名 (例如 read.example.com): " KV_DOMAIN
|
||
[ -z "$KV_DOMAIN" ] && { echo -e "${RED}域名不能为空!${NC}"; sleep 2; return; }
|
||
|
||
echo ""
|
||
echo -e "${CYAN}当前 /mnt 下可用目录:${NC}"
|
||
find /mnt -maxdepth 2 -mindepth 1 -type d 2>/dev/null | sort | while read -r d; do echo -e " ${GREEN}${d}${NC}"; done
|
||
echo ""
|
||
read -p "书库路径 (默认 /mnt/Books): " KV_BOOKS_PATH
|
||
[ -z "$KV_BOOKS_PATH" ] && KV_BOOKS_PATH="/mnt/Books"
|
||
sudo mkdir -p "$KV_BOOKS_PATH"
|
||
|
||
mkdir -p /root/kavita_data/config
|
||
cat > /root/kavita_data/docker-compose.yml <<EOF
|
||
services:
|
||
kavita:
|
||
image: jvmilazz0/kavita:latest
|
||
container_name: kavita_app
|
||
restart: unless-stopped
|
||
ports:
|
||
- "127.0.0.1:5000:5000"
|
||
volumes:
|
||
- './config:/kavita/config'
|
||
- '${KV_BOOKS_PATH}:/manga'
|
||
environment:
|
||
- TZ=Asia/Shanghai
|
||
EOF
|
||
if (cd /root/kavita_data && sudo docker compose up -d); then
|
||
echo -e "${GREEN}✅ Kavita 已启动...${NC}"; sleep 5
|
||
update_tunnel_config "${KV_DOMAIN}" "http://127.0.0.1:5000" "Kavita"
|
||
echo -e "\n## Kavita 凭证 (部署于: $(date))" >> ${STATE_FILE}
|
||
echo "KAVITA_DOMAIN=${KV_DOMAIN}" >> ${STATE_FILE}
|
||
echo "KAVITA_BOOKS_PATH=${KV_BOOKS_PATH}" >> ${STATE_FILE}
|
||
echo ""
|
||
echo -e "${GREEN}╔══════════════════════════════════════════════════════╗${NC}"
|
||
echo -e "${GREEN}║ ✅ Kavita 部署成功! ║${NC}"
|
||
echo -e "${GREEN}║ 访问:${CYAN}https://${KV_DOMAIN}${NC}"
|
||
echo -e "${GREEN}║ 首次访问需设置管理员账号密码 ║${NC}"
|
||
echo -e "${GREEN}║ 登录后进入 设置→书库 添加路径: /manga ║${NC}"
|
||
echo -e "${GREEN}╚══════════════════════════════════════════════════════╝${NC}"
|
||
else
|
||
echo -e "${RED}❌ Kavita 部署失败!${NC}"
|
||
fi
|
||
echo -e "\n${GREEN}按任意键返回...${NC}"; read -n 1 -s
|
||
}
|
||
|
||
# ── install_gotify ──
|
||
install_gotify() {
|
||
ensure_docker_installed || return; check_tunnel_installed || return; clear
|
||
echo -e "${BLUE}=== 🔔 部署 Gotify 消息推送服务 ===${NC}"
|
||
echo -e "${CYAN}自建推送服务,N8N/服务器报警可推送到手机${NC}"
|
||
echo ""
|
||
read -p "请输入 Gotify 的子域名 (例如 push.example.com): " GT_DOMAIN
|
||
[ -z "$GT_DOMAIN" ] && { echo -e "${RED}域名不能为空!${NC}"; sleep 2; return; }
|
||
|
||
local GT_PASS="gotify_$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 12)"
|
||
|
||
mkdir -p /root/gotify_data/data
|
||
cat > /root/gotify_data/docker-compose.yml <<EOF
|
||
services:
|
||
gotify:
|
||
image: gotify/server:latest
|
||
container_name: gotify_app
|
||
restart: unless-stopped
|
||
ports:
|
||
- "127.0.0.1:8085:80"
|
||
volumes:
|
||
- './data:/app/data'
|
||
environment:
|
||
- GOTIFY_DEFAULTUSER_PASS=${GT_PASS}
|
||
- TZ=Asia/Shanghai
|
||
EOF
|
||
if (cd /root/gotify_data && sudo docker compose up -d); then
|
||
echo -e "${GREEN}✅ Gotify 已启动...${NC}"; sleep 3
|
||
update_tunnel_config "${GT_DOMAIN}" "http://127.0.0.1:8085" "Gotify"
|
||
echo -e "\n## Gotify 凭证 (部署于: $(date))" >> ${STATE_FILE}
|
||
echo "GOTIFY_DOMAIN=${GT_DOMAIN}" >> ${STATE_FILE}
|
||
echo "GOTIFY_PASSWORD=${GT_PASS}" >> ${STATE_FILE}
|
||
echo ""
|
||
echo -e "${GREEN}╔══════════════════════════════════════════════════════╗${NC}"
|
||
echo -e "${GREEN}║ ✅ Gotify 部署成功! ║${NC}"
|
||
echo -e "${GREEN}║ 访问:${CYAN}https://${GT_DOMAIN}${NC}"
|
||
echo -e "${GREEN}║ 账号:${CYAN}admin${NC} 密码:${CYAN}${GT_PASS}${NC}"
|
||
echo -e "${GREEN}║ 手机安装 Gotify App 填入域名即可收到推送 ║${NC}"
|
||
echo -e "${GREEN}╚══════════════════════════════════════════════════════╝${NC}"
|
||
else
|
||
echo -e "${RED}❌ Gotify 部署失败!${NC}"
|
||
fi
|
||
echo -e "\n${GREEN}按任意键返回...${NC}"; read -n 1 -s
|
||
}
|
||
|
||
# ── install_actual_budget ──
|
||
install_actual_budget() {
|
||
ensure_docker_installed || return; check_tunnel_installed || return; clear
|
||
echo -e "${BLUE}=== 💰 部署 Actual Budget 个人记账 ===${NC}"
|
||
echo -e "${CYAN}开源记账软件,预算管理,支持多设备同步${NC}"
|
||
echo ""
|
||
read -p "请输入 Actual Budget 的子域名 (例如 money.example.com): " AB_DOMAIN
|
||
[ -z "$AB_DOMAIN" ] && { echo -e "${RED}域名不能为空!${NC}"; sleep 2; return; }
|
||
|
||
mkdir -p /root/actual_budget_data/data
|
||
cat > /root/actual_budget_data/docker-compose.yml <<EOF
|
||
services:
|
||
actual:
|
||
image: actualbudget/actual-server:latest
|
||
container_name: actual_budget_app
|
||
restart: unless-stopped
|
||
ports:
|
||
- "127.0.0.1:5006:5006"
|
||
volumes:
|
||
- './data:/data'
|
||
environment:
|
||
- TZ=Asia/Shanghai
|
||
EOF
|
||
if (cd /root/actual_budget_data && sudo docker compose up -d); then
|
||
echo -e "${GREEN}✅ Actual Budget 已启动...${NC}"; sleep 3
|
||
update_tunnel_config "${AB_DOMAIN}" "http://127.0.0.1:5006" "Actual Budget"
|
||
echo -e "\n## Actual Budget 凭证 (部署于: $(date))" >> ${STATE_FILE}
|
||
echo "ACTUAL_BUDGET_DOMAIN=${AB_DOMAIN}" >> ${STATE_FILE}
|
||
echo ""
|
||
echo -e "${GREEN}╔══════════════════════════════════════════════════════╗${NC}"
|
||
echo -e "${GREEN}║ ✅ Actual Budget 部署成功! ║${NC}"
|
||
echo -e "${GREEN}║ 访问:${CYAN}https://${AB_DOMAIN}${NC}"
|
||
echo -e "${GREEN}║ 首次访问直接创建账号即可 ║${NC}"
|
||
echo -e "${GREEN}╚══════════════════════════════════════════════════════╝${NC}"
|
||
else
|
||
echo -e "${RED}❌ Actual Budget 部署失败!${NC}"
|
||
fi
|
||
echo -e "\n${GREEN}按任意键返回...${NC}"; read -n 1 -s
|
||
}
|
||
|
||
# ── install_excalidraw ──
|
||
install_excalidraw() {
|
||
ensure_docker_installed || return; check_tunnel_installed || return; clear
|
||
echo -e "${BLUE}=== ✏️ 部署 Excalidraw 在线白板 ===${NC}"
|
||
echo -e "${CYAN}手绘风格白板,流程图/思维导图,比 Draw.io 更轻量好看${NC}"
|
||
echo ""
|
||
read -p "请输入 Excalidraw 的子域名 (例如 draw.example.com): " ED_DOMAIN
|
||
[ -z "$ED_DOMAIN" ] && { echo -e "${RED}域名不能为空!${NC}"; sleep 2; return; }
|
||
|
||
mkdir -p /root/excalidraw_data
|
||
cat > /root/excalidraw_data/docker-compose.yml <<EOF
|
||
services:
|
||
excalidraw:
|
||
image: excalidraw/excalidraw:latest
|
||
container_name: excalidraw_app
|
||
restart: unless-stopped
|
||
ports:
|
||
- "127.0.0.1:8086:80"
|
||
environment:
|
||
- TZ=Asia/Shanghai
|
||
EOF
|
||
if (cd /root/excalidraw_data && sudo docker compose up -d); then
|
||
echo -e "${GREEN}✅ Excalidraw 已启动...${NC}"; sleep 3
|
||
update_tunnel_config "${ED_DOMAIN}" "http://127.0.0.1:8086" "Excalidraw"
|
||
echo -e "\n## Excalidraw 凭证 (部署于: $(date))" >> ${STATE_FILE}
|
||
echo "EXCALIDRAW_DOMAIN=${ED_DOMAIN}" >> ${STATE_FILE}
|
||
echo ""
|
||
echo -e "${GREEN}╔══════════════════════════════════════════════════════╗${NC}"
|
||
echo -e "${GREEN}║ ✅ Excalidraw 部署成功! ║${NC}"
|
||
echo -e "${GREEN}║ 访问:${CYAN}https://${ED_DOMAIN}${NC}"
|
||
echo -e "${GREEN}║ 无需登录,打开即用 ║${NC}"
|
||
echo -e "${GREEN}╚══════════════════════════════════════════════════════╝${NC}"
|
||
else
|
||
echo -e "${RED}❌ Excalidraw 部署失败!${NC}"
|
||
fi
|
||
echo -e "\n${GREEN}按任意键返回...${NC}"; read -n 1 -s
|
||
}
|
||
|
||
# ── install_joplin_server ──
|
||
install_joplin_server() {
|
||
ensure_docker_installed || return; check_tunnel_installed || return; clear
|
||
echo -e "${BLUE}=== 📓 部署 Joplin Server 私人笔记服务器 ===${NC}"
|
||
echo -e "${CYAN}替代 OneNote,原生支持 Windows/macOS/Linux/iOS/Android 客户端,端对端加密${NC}"
|
||
echo ""
|
||
read -p "请输入 Joplin Server 的子域名 (例如 notes.example.com): " JP_DOMAIN
|
||
[ -z "$JP_DOMAIN" ] && { echo -e "${RED}域名不能为空!${NC}"; sleep 2; return; }
|
||
|
||
local JP_DB_PASS; JP_DB_PASS="$(_gen_password 16)"
|
||
local JP_MAILER_ENABLED="${JP_MAILER_ENABLED:-0}"
|
||
|
||
mkdir -p /root/joplin_data/postgres
|
||
cat > /root/joplin_data/docker-compose.yml <<EOF
|
||
services:
|
||
joplin:
|
||
image: ${IMG_JOPLIN_SERVER}
|
||
container_name: joplin_app
|
||
restart: unless-stopped
|
||
depends_on:
|
||
joplin_db:
|
||
condition: service_healthy
|
||
ports:
|
||
- "127.0.0.1:${PORT_JOPLIN}:22300"
|
||
environment:
|
||
- APP_PORT=22300
|
||
- APP_BASE_URL=https://${JP_DOMAIN}
|
||
- DB_CLIENT=pg
|
||
- POSTGRES_PASSWORD=${JP_DB_PASS}
|
||
- POSTGRES_DATABASE=joplin
|
||
- POSTGRES_USER=joplin
|
||
- POSTGRES_PORT=5432
|
||
- POSTGRES_HOST=joplin_db
|
||
- MAILER_ENABLED=${JP_MAILER_ENABLED}
|
||
- TZ=${TZ_DEFAULT}
|
||
|
||
joplin_db:
|
||
image: ${IMG_JOPLIN_DB}
|
||
container_name: joplin_db
|
||
restart: unless-stopped
|
||
volumes:
|
||
- './postgres:/var/lib/postgresql/data'
|
||
environment:
|
||
- POSTGRES_PASSWORD=${JP_DB_PASS}
|
||
- POSTGRES_USER=joplin
|
||
- POSTGRES_DB=joplin
|
||
healthcheck:
|
||
test: ["CMD-SHELL", "pg_isready -U joplin"]
|
||
interval: 10s
|
||
timeout: 5s
|
||
retries: 5
|
||
EOF
|
||
if (cd /root/joplin_data && sudo docker compose up -d); then
|
||
echo -e "${GREEN}✅ Joplin Server 已启动,等待数据库初始化...${NC}"; sleep 8
|
||
update_tunnel_config "${JP_DOMAIN}" "http://127.0.0.1:${PORT_JOPLIN}" "Joplin Server"
|
||
echo -e "\n## Joplin Server 凭证 (部署于: $(date))" >> ${STATE_FILE}
|
||
echo "JOPLIN_DOMAIN=${JP_DOMAIN}" >> ${STATE_FILE}
|
||
echo "JOPLIN_DB_PASSWORD=${JP_DB_PASS}" >> ${STATE_FILE}
|
||
echo ""
|
||
echo -e "${GREEN}╔══════════════════════════════════════════════════════╗${NC}"
|
||
echo -e "${GREEN}║ ✅ Joplin Server 部署成功! ║${NC}"
|
||
echo -e "${GREEN}║ 访问:${CYAN}https://${JP_DOMAIN}${NC}"
|
||
echo -e "${GREEN}║ 首次登录账号:${CYAN}admin@localhost${NC} ║${NC}"
|
||
echo -e "${GREEN}║ 首次登录密码:${CYAN}admin${NC} ⚠️ 请立即修改! ║${NC}"
|
||
echo -e "${GREEN}║ ║${NC}"
|
||
echo -e "${GREEN}║ 📱 客户端配置教程: ║${NC}"
|
||
echo -e "${GREEN}║ 1) 下载 Joplin 客户端 (joplinapp.org) ║${NC}"
|
||
echo -e "${GREEN}║ 2) 同步 → Joplin Server → 填入域名 ║${NC}"
|
||
echo -e "${GREEN}║ 3) 填入邮箱和密码即可多端同步 ║${NC}"
|
||
echo -e "${GREEN}╚══════════════════════════════════════════════════════╝${NC}"
|
||
else
|
||
echo -e "${RED}❌ Joplin Server 部署失败,请检查日志:${NC}"
|
||
(cd /root/joplin_data && sudo docker compose logs --tail=30)
|
||
fi
|
||
echo -e "\n${GREEN}按任意键返回...${NC}"; read -n 1 -s
|
||
}
|
||
|
||
# ── install_trilium ──
|
||
install_trilium() {
|
||
ensure_docker_installed || return; check_tunnel_installed || return; clear
|
||
echo -e "${BLUE}=== 🌳 部署 Trilium Notes 个人知识库 ===${NC}"
|
||
echo -e "${CYAN}功能强大的层级笔记应用,支持脑图、代码高亮、关系图、WebDAV 同步${NC}"
|
||
echo ""
|
||
read -p "请输入 Trilium Notes 的子域名 (例如 notes.example.com): " TN_DOMAIN
|
||
[ -z "$TN_DOMAIN" ] && { echo -e "${RED}域名不能为空!${NC}"; sleep 2; return; }
|
||
|
||
mkdir -p /root/trilium_data
|
||
# 修复目录权限 (容器内以 node 用户 uid=1000 运行)
|
||
sudo chown -R 1000:1000 /root/trilium_data
|
||
|
||
cat > /root/trilium_data/docker-compose.yml <<EOF
|
||
services:
|
||
trilium:
|
||
image: ${IMG_TRILIUM}
|
||
container_name: trilium_app
|
||
restart: unless-stopped
|
||
ports:
|
||
- "127.0.0.1:${PORT_TRILIUM}:8080"
|
||
volumes:
|
||
- /root/trilium_data:/home/node/trilium-data
|
||
environment:
|
||
- TZ=${TZ_DEFAULT}
|
||
EOF
|
||
|
||
if (cd /root/trilium_data && sudo docker compose up -d); then
|
||
echo -e "${GREEN}✅ Trilium Notes 已启动,等待服务就绪...${NC}"; sleep 5
|
||
update_tunnel_config "${TN_DOMAIN}" "http://127.0.0.1:${PORT_TRILIUM}" "Trilium Notes"
|
||
echo -e "\n## Trilium Notes 凭证 (部署于: $(date))" >> ${STATE_FILE}
|
||
echo "TRILIUM_DOMAIN=${TN_DOMAIN}" >> ${STATE_FILE}
|
||
echo ""
|
||
echo -e "${GREEN}╔══════════════════════════════════════════════════════╗${NC}"
|
||
echo -e "${GREEN}║ ✅ Trilium Notes 部署成功! ║${NC}"
|
||
echo -e "${GREEN}║ 访问:${CYAN}https://${TN_DOMAIN}${NC}"
|
||
echo -e "${GREEN}║ ║${NC}"
|
||
echo -e "${GREEN}║ 📋 首次使用说明: ║${NC}"
|
||
echo -e "${GREEN}║ 1) 浏览器打开上方地址,按向导设置管理员密码 ║${NC}"
|
||
echo -e "${GREEN}║ 2) 数据存于 /root/trilium_data,请定期备份 ║${NC}"
|
||
echo -e "${GREEN}║ 3) 支持桌面客户端同步:trilium.github.io ║${NC}"
|
||
echo -e "${GREEN}╚══════════════════════════════════════════════════════╝${NC}"
|
||
else
|
||
echo -e "${RED}❌ Trilium Notes 部署失败,请检查日志:${NC}"
|
||
(cd /root/trilium_data && sudo docker compose logs --tail=30)
|
||
fi
|
||
echo -e "\n${GREEN}按任意键返回...${NC}"; read -n 1 -s
|
||
}
|
||
|
||
# ── install_grafana ──
|
||
install_grafana() {
|
||
ensure_docker_installed || return; check_tunnel_installed || return; clear
|
||
echo -e "${BLUE}=== 📊 部署 Grafana + Prometheus 监控面板 ===${NC}"
|
||
echo -e "${CYAN}可视化监控服务器CPU/内存/硬盘/网络,图表美观${NC}"
|
||
echo ""
|
||
read -p "请输入 Grafana 的子域名 (例如 monitor.example.com): " GF_DOMAIN
|
||
[ -z "$GF_DOMAIN" ] && { echo -e "${RED}域名不能为空!${NC}"; sleep 2; return; }
|
||
|
||
local GF_PASS="grafana_$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 12)"
|
||
|
||
mkdir -p /root/grafana_data/{grafana,prometheus}
|
||
# 修复 Grafana 数据目录权限(容器内 uid=472)
|
||
sudo chown -R 472:472 /root/grafana_data/grafana
|
||
sudo chmod -R 755 /root/grafana_data/grafana
|
||
|
||
# Prometheus 配置
|
||
cat > /root/grafana_data/prometheus/prometheus.yml <<EOF
|
||
global:
|
||
scrape_interval: 15s
|
||
scrape_configs:
|
||
- job_name: 'node'
|
||
static_configs:
|
||
- targets: ['node_exporter:9100']
|
||
- job_name: 'cadvisor'
|
||
static_configs:
|
||
- targets: ['cadvisor:8080']
|
||
EOF
|
||
|
||
cat > /root/grafana_data/docker-compose.yml <<EOF
|
||
services:
|
||
grafana:
|
||
image: grafana/grafana:latest
|
||
container_name: grafana_app
|
||
restart: unless-stopped
|
||
ports:
|
||
- "127.0.0.1:3003:3000"
|
||
volumes:
|
||
- './grafana:/var/lib/grafana'
|
||
environment:
|
||
- GF_SECURITY_ADMIN_PASSWORD=${GF_PASS}
|
||
- GF_USERS_ALLOW_SIGN_UP=false
|
||
- TZ=Asia/Shanghai
|
||
|
||
prometheus:
|
||
image: prom/prometheus:latest
|
||
container_name: prometheus_app
|
||
restart: unless-stopped
|
||
volumes:
|
||
- './prometheus/prometheus.yml:/etc/prometheus/prometheus.yml'
|
||
- prometheus_data:/prometheus
|
||
command:
|
||
- '--config.file=/etc/prometheus/prometheus.yml'
|
||
- '--storage.tsdb.retention.time=30d'
|
||
|
||
node_exporter:
|
||
image: prom/node-exporter:latest
|
||
container_name: node_exporter
|
||
restart: unless-stopped
|
||
pid: host
|
||
volumes:
|
||
- '/proc:/host/proc:ro'
|
||
- '/sys:/host/sys:ro'
|
||
- '/:/rootfs:ro'
|
||
command:
|
||
- '--path.procfs=/host/proc'
|
||
- '--path.sysfs=/host/sys'
|
||
- '--collector.filesystem.ignored-mount-points=^/(sys|proc|dev|host|etc)($$|/)'
|
||
|
||
cadvisor:
|
||
image: gcr.io/cadvisor/cadvisor:latest
|
||
container_name: cadvisor
|
||
restart: unless-stopped
|
||
privileged: true
|
||
volumes:
|
||
- '/:/rootfs:ro'
|
||
- '/var/run:/var/run:ro'
|
||
- '/sys:/sys:ro'
|
||
- '/var/lib/docker/:/var/lib/docker:ro'
|
||
|
||
volumes:
|
||
prometheus_data:
|
||
EOF
|
||
|
||
if (cd /root/grafana_data && sudo docker compose up -d); then
|
||
echo -e "${GREEN}✅ Grafana 已启动,等待服务稳定...${NC}"; sleep 8
|
||
update_tunnel_config "${GF_DOMAIN}" "http://127.0.0.1:3003" "Grafana"
|
||
echo -e "\n## Grafana 凭证 (部署于: $(date))" >> ${STATE_FILE}
|
||
echo "GRAFANA_DOMAIN=${GF_DOMAIN}" >> ${STATE_FILE}
|
||
echo "GRAFANA_PASSWORD=${GF_PASS}" >> ${STATE_FILE}
|
||
echo ""
|
||
echo -e "${GREEN}╔══════════════════════════════════════════════════════════╗${NC}"
|
||
echo -e "${GREEN}║ ✅ Grafana + Prometheus 部署成功! ║${NC}"
|
||
echo -e "${GREEN}║ 访问:${CYAN}https://${GF_DOMAIN}${NC}"
|
||
echo -e "${GREEN}║ 账号:${CYAN}admin${NC} 密码:${CYAN}${GF_PASS}${NC}"
|
||
echo -e "${GREEN}║ ║${NC}"
|
||
echo -e "${GREEN}║ ${YELLOW}首次登录后: ${NC}"
|
||
echo -e "${GREEN}║ ${YELLOW}1. 添加数据源 → Prometheus → URL填 http://prometheus_app:9090${NC}"
|
||
echo -e "${GREEN}║ ${YELLOW}2. 导入仪表盘 → ID填 1860 (Node Exporter 完整版) ${NC}"
|
||
echo -e "${GREEN}║ ${YELLOW}3. 导入仪表盘 → ID填 193 (Docker 容器监控) ${NC}"
|
||
echo -e "${GREEN}╚══════════════════════════════════════════════════════════╝${NC}"
|
||
else
|
||
echo -e "${RED}❌ Grafana 部署失败!${NC}"
|
||
fi
|
||
echo -e "\n${GREEN}按任意键返回...${NC}"; read -n 1 -s
|
||
}
|
||
|
||
install_sillytavern() {
|
||
clear
|
||
echo -e "${BLUE}=== 🍺 部署 SillyTavern 角色扮演酒馆 ===${NC}"
|
||
echo -e "${CYAN}SillyTavern 是强大的 AI 角色扮演前端,支持 Claude、GPT、本地模型等。${NC}"
|
||
echo ""
|
||
|
||
if docker ps -q -f name=sillytavern_app 2>/dev/null | grep -q .; then
|
||
echo -e "${YELLOW}⚠️ SillyTavern 已在运行!${NC}"
|
||
docker ps | grep sillytavern
|
||
read -n 1 -s -r -p "按任意键返回..."
|
||
return
|
||
fi
|
||
|
||
check_tunnel_installed || return
|
||
|
||
read -p "请输入酒馆的访问域名 (例如 jg.jigongtou.uepopo.com): " ST_DOMAIN
|
||
[ -z "$ST_DOMAIN" ] && echo -e "${RED}域名不能为空!${NC}" && return
|
||
|
||
# 修复已知问题:config.yaml 可能是空目录
|
||
mkdir -p /root/sillytavern_data
|
||
[ -d "/root/sillytavern_data/config.yaml" ] && rm -rf /root/sillytavern_data/config.yaml
|
||
[ -d "/root/sillytavern_data/config" ] && rm -rf /root/sillytavern_data/config
|
||
|
||
# 写入配置文件
|
||
cat > /root/sillytavern_data/config.yaml << 'STEOF'
|
||
dataRoot: ./data
|
||
listen: true
|
||
port: 8000
|
||
ssl:
|
||
enabled: false
|
||
whitelistMode: false
|
||
basicAuthMode: false
|
||
basicAuthUser:
|
||
username: user
|
||
password: password
|
||
enableCorsProxy: true
|
||
securityOverride: true
|
||
STEOF
|
||
|
||
# 写入 docker-compose
|
||
cat > /root/sillytavern_data/docker-compose.yml << 'STEOF'
|
||
services:
|
||
sillytavern:
|
||
container_name: sillytavern_app
|
||
image: ghcr.io/sillytavern/sillytavern:latest
|
||
restart: unless-stopped
|
||
ports:
|
||
- "127.0.0.1:8000:8000"
|
||
volumes:
|
||
- '/root/sillytavern_data/config.yaml:/home/node/app/config.yaml'
|
||
- '/root/sillytavern_data/data:/home/node/app/data'
|
||
STEOF
|
||
|
||
echo -e "${YELLOW}正在拉取酒馆镜像并启动(首次约需2-5分钟)...${NC}"
|
||
cd /root/sillytavern_data
|
||
docker compose up -d
|
||
|
||
echo -e "${YELLOW}等待服务启动...${NC}"
|
||
sleep 8
|
||
|
||
if docker ps | grep -q sillytavern_app; then
|
||
echo -e "${GREEN}✅ 酒馆已开业!${NC}"
|
||
else
|
||
echo -e "${RED}❌ 启动失败,请查看日志:docker logs sillytavern_app${NC}"
|
||
read -n 1 -s -r -p "按任意键返回..."
|
||
return
|
||
fi
|
||
|
||
# 配置 Cloudflare Tunnel
|
||
echo -e "${GREEN}正在为酒馆配置网络通道...${NC}"
|
||
update_tunnel_config "${ST_DOMAIN}" "http://127.0.0.1:8000" "SillyTavern 酒馆"
|
||
|
||
# 保存凭证
|
||
echo -e "
|
||
## SillyTavern 凭证 (部署于: $(date))" >> ${STATE_FILE}
|
||
echo "SILLYTAVERN_DOMAIN=${ST_DOMAIN}" >> ${STATE_FILE}
|
||
|
||
echo ""
|
||
echo -e "${YELLOW}══════════════════════════════════════════════════════════════════════${NC}"
|
||
echo -e "${GREEN}🍺 酒馆正式开业!${NC}"
|
||
echo -e " 访问地址 :${CYAN}https://${ST_DOMAIN}${NC}"
|
||
echo -e " ${YELLOW}进入后在左上角 API 处配置 AI 模型连接。${NC}"
|
||
echo -e " ${YELLOW}支持 Claude、GPT、Gemini、本地 Ollama 等。${NC}"
|
||
echo -e "${YELLOW}══════════════════════════════════════════════════════════════════════${NC}"
|
||
echo -e "
|
||
${GREEN}按任意键返回主菜单...${NC}"; read -n 1 -s
|
||
}
|
||
|
||
# ================================================================
|
||
# --- 新增功能:部署 N8N 工作流自动化 ---
|
||
# ================================================================
|
||
install_n8n() {
|
||
ensure_docker_installed || return; check_tunnel_installed || return
|
||
clear
|
||
echo -e "${BLUE}=== 🔄 部署 N8N 工作流自动化引擎 ===${NC}"
|
||
echo -e "${CYAN}N8N 是强大的开源工作流自动化平台,可连接数百种应用和服务。${NC}"
|
||
echo ""
|
||
|
||
if [ -d "/root/n8n_data" ]; then
|
||
echo -e "${YELLOW}⚠️ N8N 已安装!${NC}"
|
||
echo -e "${YELLOW}如需重装请先执行选项 40) 卸载 N8N 后重试。${NC}"
|
||
echo -e "\n${GREEN}按任意键返回主菜单...${NC}"; read -n 1 -s
|
||
return
|
||
fi
|
||
|
||
read -p "请输入您为 N8N 规划的子域名 (例如 n8n.example.com): " N8N_DOMAIN
|
||
if [ -z "$N8N_DOMAIN" ]; then echo -e "${RED}域名不能为空!${NC}"; sleep 2; return; fi
|
||
|
||
local N8N_ENCRYPTION_KEY="n8n-key-$(_gen_password 24)"
|
||
|
||
echo -e "${YELLOW}正在创建 N8N 配置...${NC}"
|
||
mkdir -p /root/n8n_data/data
|
||
# N8N 容器内以 uid=1000 (node用户) 运行,必须提前赋权否则启动崩溃
|
||
chown -R 1000:1000 /root/n8n_data/data
|
||
|
||
cat > /root/n8n_data/docker-compose.yml << EOF
|
||
services:
|
||
n8n:
|
||
image: n8nio/n8n:latest
|
||
container_name: n8n_app
|
||
restart: unless-stopped
|
||
ports:
|
||
- "127.0.0.1:5678:5678"
|
||
environment:
|
||
- N8N_HOST=${N8N_DOMAIN}
|
||
- N8N_PORT=5678
|
||
- N8N_PROTOCOL=https
|
||
- N8N_LISTEN_ADDRESS=0.0.0.0
|
||
- NODE_ENV=production
|
||
- WEBHOOK_URL=https://${N8N_DOMAIN}/
|
||
- N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY}
|
||
- TZ=Asia/Shanghai
|
||
- GENERIC_TIMEZONE=Asia/Shanghai
|
||
volumes:
|
||
- './data:/home/node/.n8n'
|
||
EOF
|
||
|
||
echo -e "${YELLOW}正在启动 N8N 服务...${NC}"
|
||
if _compose_up "N8N" "/root/n8n_data"; then
|
||
echo -e "${GREEN}✅ N8N 已启动,等待服务稳定(首次启动需要更多时间)...${NC}"; sleep 15
|
||
|
||
echo -e "\n## N8N 凭证 (部署于: $(date))" >> ${STATE_FILE}
|
||
echo "N8N_DOMAIN=${N8N_DOMAIN}" >> ${STATE_FILE}
|
||
echo "N8N_PORT=5678" >> ${STATE_FILE}
|
||
echo "N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY}" >> ${STATE_FILE}
|
||
|
||
echo -e "${GREEN}正在为您配置 Cloudflare Tunnel...${NC}"
|
||
update_tunnel_config "${N8N_DOMAIN}" "http://127.0.0.1:5678" "N8N"
|
||
|
||
echo ""
|
||
echo -e "${GREEN}╔══════════════════════════════════════════════════════════╗${NC}"
|
||
echo -e "${GREEN}║ ✅ N8N 部署成功! ║${NC}"
|
||
echo -e "${GREEN}╠══════════════════════════════════════════════════════════╣${NC}"
|
||
echo -e "${GREEN}║ 访问地址: ${CYAN}https://${N8N_DOMAIN}${NC}"
|
||
echo -e "${GREEN}║ 首次访问将引导您注册管理员账号 ║${NC}"
|
||
echo -e "${GREEN}╚══════════════════════════════════════════════════════════╝${NC}"
|
||
else
|
||
echo -e "${RED}❌ N8N 部署失败!请检查 Docker 是否正常运行。${NC}"
|
||
fi
|
||
echo -e "\n${GREEN}按任意键返回主菜单...${NC}"; read -n 1 -s
|
||
}
|
||
|
||
# --- ⬆️ ⬆️ ⬆️ 請將以上所有代碼添加到 main() 函數之前 ⬆️ ⬆️ ⬆️ ---
|
||
# ================================================================
|
||
|
||
# ================================================================
|
||
# --- Nextcloud 预览图终极调教 (10.4) ---
|
||
# ================================================================
|
||
run_nextcloud_preview_setup() {
|
||
clear
|
||
echo -e "${BLUE}================================================================${NC}"
|
||
echo -e "${BLUE} 🖼️ Nextcloud 预览图终极调教 ${NC}"
|
||
echo -e "${BLUE}================================================================${NC}"
|
||
echo ""
|
||
|
||
local CONTAINER="nextcloud_app"
|
||
|
||
# 检查容器是否运行
|
||
if ! docker inspect --format='{{.State.Running}}' "$CONTAINER" 2>/dev/null | grep -q "true"; then
|
||
echo -e "${RED}❌ 错误:nextcloud_app 容器未运行,请先启动 Nextcloud!${NC}"
|
||
echo -e "\n${GREEN}按任意键返回主菜单...${NC}"; read -n 1 -s
|
||
return
|
||
fi
|
||
|
||
echo -e "${YELLOW}本功能将完成以下操作:${NC}"
|
||
echo -e " ${GREEN}1)${NC} 启用 22 种文件格式预览(图片/PDF/视频/Office/音乐/字体)"
|
||
echo -e " ${GREEN}2)${NC} 设置预览图尺寸 256x256(够看又省空间)"
|
||
echo -e " ${GREEN}3)${NC} 配置 cron 每5分钟自动处理新上传文件(永久生效)"
|
||
echo -e " ${GREEN}4)${NC} 限速批量生成所有存量文件的预览图(后台慢慢跑)"
|
||
echo ""
|
||
read -p "$(echo -e ${YELLOW}"确认开始?(y/n): "${NC})" confirm
|
||
[[ "$confirm" != "y" && "$confirm" != "Y" ]] && return
|
||
|
||
# ── 第一步:启用所有预览格式 ──────────────────────────────────
|
||
echo ""
|
||
echo -e "${BLUE}[1/4] 正在启用所有预览格式...${NC}"
|
||
local providers=(
|
||
"OC\\Preview\\PNG" "OC\\Preview\\JPEG"
|
||
"OC\\Preview\\GIF" "OC\\Preview\\BMP"
|
||
"OC\\Preview\\WEBP" "OC\\Preview\\HEIC"
|
||
"OC\\Preview\\TIFF" "OC\\Preview\\SVG"
|
||
"OC\\Preview\\PDF" "OC\\Preview\\MP3"
|
||
"OC\\Preview\\MP4" "OC\\Preview\\MKV"
|
||
"OC\\Preview\\Movie" "OC\\Preview\\MSOfficeDoc"
|
||
"OC\\Preview\\MSOffice2003" "OC\\Preview\\MSOffice2007"
|
||
"OC\\Preview\\OpenDocument" "OC\\Preview\\Krita"
|
||
"OC\\Preview\\TXT" "OC\\Preview\\MarkDown"
|
||
"OC\\Preview\\XBitmap" "OC\\Preview\\Font"
|
||
)
|
||
for i in "${!providers[@]}"; do
|
||
docker exec "$CONTAINER" php occ config:system:set enabledPreviewProviders "$i" \
|
||
--value="${providers[$i]}" 2>/dev/null
|
||
done
|
||
echo -e "${GREEN} ✅ 已启用 ${#providers[@]} 种预览格式${NC}"
|
||
|
||
# ── 第二步:设置尺寸 ──────────────────────────────────────────
|
||
echo -e "${BLUE}[2/4] 正在设置预览图尺寸...${NC}"
|
||
docker exec "$CONTAINER" php occ config:system:set preview_max_x --value="256" 2>/dev/null
|
||
docker exec "$CONTAINER" php occ config:system:set preview_max_y --value="256" 2>/dev/null
|
||
docker exec "$CONTAINER" php occ config:system:set jpeg_quality --value="60" 2>/dev/null
|
||
docker exec "$CONTAINER" php occ config:system:set preview_max_scale_factor --value="1" 2>/dev/null
|
||
echo -e "${GREEN} ✅ 预览图尺寸:256x256,JPEG质量:60${NC}"
|
||
|
||
# ── 第三步:配置 cron 自动生成新文件预览 ──────────────────────
|
||
echo -e "${BLUE}[3/4] 正在配置新文件自动预览 (cron)...${NC}"
|
||
docker exec "$CONTAINER" php occ background:cron 2>/dev/null
|
||
local CRON_JOB="*/5 * * * * docker exec -u www-data ${CONTAINER} php occ preview:pre-generate >> /var/log/nextcloud_preview.log 2>&1"
|
||
if crontab -l 2>/dev/null | grep -q "preview:pre-generate"; then
|
||
echo -e "${YELLOW} ⚠️ cron 任务已存在,跳过重复添加${NC}"
|
||
else
|
||
(crontab -l 2>/dev/null; echo "$CRON_JOB") | crontab -
|
||
echo -e "${GREEN} ✅ 已添加 cron 任务:每5分钟自动处理新文件,永久生效${NC}"
|
||
fi
|
||
|
||
# ── 第四步:限速批量生成存量文件预览 ──────────────────────────
|
||
echo -e "${BLUE}[4/4] 开始限速批量生成存量预览图...${NC}"
|
||
echo -e "${YELLOW} 提示:正在后台限速运行,每50个文件暂停10秒,不影响其他服务${NC}"
|
||
echo -e "${YELLOW} 提示:可随时 Ctrl+C 中断,下次重跑会跳过已生成的文件${NC}"
|
||
echo -e "${YELLOW} 提示:日志记录在 /var/log/nextcloud_preview_batch.log${NC}"
|
||
echo ""
|
||
|
||
local LOG_FILE="/var/log/nextcloud_preview_batch.log"
|
||
local BATCH_SIZE=50
|
||
local SLEEP_SEC=10
|
||
echo "========== 开始时间: $(date) ==========" >> "$LOG_FILE"
|
||
|
||
local counter=0
|
||
local total=0
|
||
|
||
# 用 nice+ionice 双重限速,逐条输出进度
|
||
nice -n 19 ionice -c 3 \
|
||
docker exec -u www-data "$CONTAINER" php -d memory_limit=512M occ preview:generate-all -vvv 2>&1 | \
|
||
tee -a "$LOG_FILE" | \
|
||
grep --line-buffered -E "Generated|Generating|Error|Warning|generated" | \
|
||
while IFS= read -r line; do
|
||
counter=$((counter + 1))
|
||
total=$((total + 1))
|
||
echo -e " ${DIM}[${total}]${NC} $line"
|
||
if (( counter >= BATCH_SIZE )); then
|
||
counter=0
|
||
echo -e "${YELLOW} ⏸ 已处理 ${total} 个,暂停 ${SLEEP_SEC} 秒降温...${NC}"
|
||
sleep "$SLEEP_SEC"
|
||
fi
|
||
done
|
||
|
||
echo "========== 结束时间: $(date) ==========" >> "$LOG_FILE"
|
||
echo ""
|
||
echo -e "${GREEN}================================================================${NC}"
|
||
echo -e "${GREEN} ✅ 全部完成!${NC}"
|
||
echo -e "${GREEN} 📋 已启用:22种格式预览${NC}"
|
||
echo -e "${GREEN} 🤖 新文件:上传后5分钟内自动生成预览(永久)${NC}"
|
||
echo -e "${GREEN} 📦 存量文件:批量生成完毕${NC}"
|
||
echo -e "${GREEN} 📄 日志:${LOG_FILE}${NC}"
|
||
echo -e "${GREEN}================================================================${NC}"
|
||
echo -e "${YELLOW} ℹ️ 远程挂载(sshfs/rclone)的文件预览速度取决于网速,属正常现象${NC}"
|
||
echo -e "\n${GREEN}按任意键返回主菜单...${NC}"; read -n 1 -s
|
||
}
|
||
|
||
# ================================================================
|
||
# --- 部署 PopoDash 监控面板 【鸡公头出品】 ---
|
||
# ================================================================
|
||
install_popodash() {
|
||
clear
|
||
echo -e "${BLUE}--- 🐓 部署 PopoDash 监控面板 【鸡公头出品】 ---${NC}"
|
||
bash <(curl -sL "https://raw.githubusercontent.com/uepopo/popodash/main/install.sh?t=$RANDOM" | tr -d '\r')
|
||
# 写入安装标记,供菜单状态检测使用
|
||
mkdir -p /root/popodash && touch /root/popodash/.installed
|
||
echo -e "\n${GREEN}按任意键返回主菜单...${NC}"; read -n 1 -s
|
||
}
|
||
|
||
# ================================================================
|
||
# --- 部署 MIMI 电报 AI 小秘书 【鸡公头出品】 ---
|
||
# ================================================================
|
||
install_mimi() {
|
||
clear
|
||
echo -e "${BLUE}--- 🐓 部署 MIMI 电报 AI 小秘书 【鸡公头出品】 ---${NC}"
|
||
bash <(curl -sL "https://raw.githubusercontent.com/uepopo/mimi/refs/heads/main/install.sh?t=$RANDOM" | tr -d '\r')
|
||
# 写入安装标记,供菜单状态检测使用
|
||
mkdir -p /root/mimi && touch /root/mimi/.installed
|
||
echo -e "\n${GREEN}按任意键返回主菜单...${NC}"; read -n 1 -s
|
||
}
|
||
|
||
# ================================================================
|
||
# --- 部署 WebDAV 网盘 ---
|
||
# ================================================================
|
||
install_webdav() {
|
||
clear
|
||
echo -e "${BLUE}--- 🗂️ WebDAV 网盘管理 ---${NC}"
|
||
echo ""
|
||
|
||
local is_running=false
|
||
docker inspect webdav_app &>/dev/null && is_running=true
|
||
|
||
# ── 显示当前状态 ──────────────────────────────────────────────
|
||
if $is_running; then
|
||
local cur_dir cur_url cur_user
|
||
cur_dir=$(grep "^WEBDAV_DIR=" "${STATE_FILE}" 2>/dev/null | tail -1 | cut -d= -f2-)
|
||
cur_url=$(grep "^WEBDAV_URL=" "${STATE_FILE}" 2>/dev/null | tail -1 | cut -d= -f2-)
|
||
cur_user=$(grep "^WEBDAV_USER=" "${STATE_FILE}" 2>/dev/null | tail -1 | cut -d= -f2-)
|
||
echo -e " 状态:${GREEN}● 运行中${NC}"
|
||
echo -e " 地址:${CYAN}${cur_url:-未知}${NC}"
|
||
echo -e " 用户:${CYAN}${cur_user:-未知}${NC}"
|
||
echo -e " 目录:${CYAN}${cur_dir:-未知}${NC}"
|
||
else
|
||
echo -e " 状态:${YELLOW}○ 未安装${NC}"
|
||
fi
|
||
|
||
echo ""
|
||
echo -e " ${CYAN}1)${NC} 安装 WebDAV"
|
||
if $is_running; then
|
||
echo -e " ${CYAN}2)${NC} 更换共享目录(热更新,无需重新输入域名密码)"
|
||
echo -e " ${CYAN}3)${NC} ${RED}删除 WebDAV${NC}(容器和数据全部清除)"
|
||
fi
|
||
echo -e " ${CYAN}0)${NC} 返回"
|
||
echo ""
|
||
read -p " 请选择: " webdav_menu_choice
|
||
|
||
case "$webdav_menu_choice" in
|
||
1) _webdav_install ;;
|
||
2) $is_running && _webdav_change_dir || { echo -e "${RED}WebDAV 未运行。${NC}"; sleep 2; } ;;
|
||
3) $is_running && _webdav_delete || { echo -e "${RED}WebDAV 未运行。${NC}"; sleep 2; } ;;
|
||
0|"") return ;;
|
||
*) echo -e "${RED}无效选项。${NC}"; sleep 2 ;;
|
||
esac
|
||
}
|
||
|
||
# ================================================================
|
||
# --- WebDAV 实际安装逻辑(内部函数)---
|
||
# ================================================================
|
||
_webdav_install() {
|
||
if docker inspect webdav_app &>/dev/null; then
|
||
echo -e "${YELLOW}WebDAV 已在运行。如需重装请先选择删除。${NC}"
|
||
sleep 2; return
|
||
fi
|
||
|
||
check_tunnel_installed || return 1
|
||
ensure_docker_installed || return 1
|
||
|
||
echo ""
|
||
echo -e "${CYAN} WebDAV 可以让你的 VPS 变成一个网盘${NC}"
|
||
echo -e "${CYAN} 支持 Windows/Mac/手机直接挂载,能显示真实磁盘容量${NC}"
|
||
echo ""
|
||
|
||
# ── 选择共享目录 ──────────────────────────────────────────────
|
||
local webdav_dir
|
||
_webdav_pick_dir webdav_dir || return 1
|
||
echo -e "${GREEN} ✅ 共享目录:${webdav_dir}${NC}"
|
||
echo ""
|
||
|
||
# ── 用户名 ────────────────────────────────────────────────────
|
||
read -p " 用户名 [默认 admin]: " webdav_user
|
||
webdav_user="${webdav_user:-admin}"
|
||
|
||
# ── 密码 ──────────────────────────────────────────────────────
|
||
local default_pass
|
||
default_pass=$(_gen_password 16)
|
||
read -p " 密码 [回车自动生成]: " webdav_pass
|
||
webdav_pass="${webdav_pass:-$default_pass}"
|
||
|
||
# ── 域名 ──────────────────────────────────────────────────────
|
||
echo ""
|
||
echo -e "${YELLOW} 请输入 WebDAV 访问域名(例如:webdav.yourdomain.com):${NC}"
|
||
read -p " 域名: " webdav_domain
|
||
if [ -z "$webdav_domain" ]; then
|
||
echo -e "${RED}域名不能为空,安装中止。${NC}"; sleep 2; return
|
||
fi
|
||
|
||
# ── 创建 compose 文件 ─────────────────────────────────────────
|
||
local data_dir="/root/webdav_data"
|
||
mkdir -p "$data_dir"
|
||
|
||
cat > "${data_dir}/docker-compose.yml" << EOF
|
||
services:
|
||
webdav:
|
||
image: dgraziotin/nginx-webdav-nononsense
|
||
container_name: webdav_app
|
||
restart: unless-stopped
|
||
ports:
|
||
- "127.0.0.1:${PORT_WEBDAV}:80"
|
||
volumes:
|
||
- ${webdav_dir}:/data
|
||
environment:
|
||
- WEBDAV_USERNAME=${webdav_user}
|
||
- WEBDAV_PASSWORD=${webdav_pass}
|
||
- WEBDAV_HOSTNAME=${webdav_domain}
|
||
- UID=0
|
||
- GID=0
|
||
- TZ=${TZ_DEFAULT}
|
||
EOF
|
||
|
||
_compose_up "WebDAV" "$data_dir" || return 1
|
||
|
||
# 修复容器内写权限问题(UID/GID 环境变量在此镜像中不可靠)
|
||
echo -e "${YELLOW}正在修复 WebDAV 目录写权限...${NC}"
|
||
sleep 2 # 等容器完全启动
|
||
docker exec webdav_app chmod -R 777 /data 2>/dev/null && \
|
||
echo -e "${GREEN} ✅ 写权限修复完成${NC}" || \
|
||
echo -e "${YELLOW} ⚠️ 权限修复失败,如无法写入请手动执行:docker exec webdav_app chmod -R 777 /data${NC}"
|
||
|
||
echo -e "\n${YELLOW}正在配置 Cloudflare Tunnel...${NC}"
|
||
update_tunnel_config "$webdav_domain" "http://127.0.0.1:${PORT_WEBDAV}" "WebDAV 网盘"
|
||
|
||
_save_credential "WEBDAV_URL" "https://${webdav_domain}"
|
||
_save_credential "WEBDAV_USER" "$webdav_user"
|
||
_save_credential "WEBDAV_PASS" "$webdav_pass"
|
||
_save_credential "WEBDAV_DIR" "$webdav_dir"
|
||
|
||
echo ""
|
||
echo -e "${GREEN}╔══════════════════════════════════════════════════════════╗${NC}"
|
||
echo -e "${GREEN}║ 🎉 WebDAV 网盘部署成功! ║${NC}"
|
||
echo -e "${GREEN}╠══════════════════════════════════════════════════════════╣${NC}"
|
||
echo -e "${GREEN}║ 访问地址:${YELLOW}https://${webdav_domain}${NC}"
|
||
echo -e "${GREEN}║ 用户名: ${YELLOW}${webdav_user}${NC}"
|
||
echo -e "${GREEN}║ 密码: ${YELLOW}${webdav_pass}${NC}"
|
||
echo -e "${GREEN}║ 共享目录:${YELLOW}${webdav_dir}${NC}"
|
||
echo -e "${GREEN}╠══════════════════════════════════════════════════════════╣${NC}"
|
||
echo -e "${GREEN}║ 💻 RaiDrive:选 WebDAV → 填入以上信息即可 ║${NC}"
|
||
echo -e "${GREEN}║ 💻 Win11映射网络驱动器:填入访问地址即可 ║${NC}"
|
||
echo -e "${GREEN}║ 📱 手机 nPlayer/ES文件浏览器:填入以上信息 ║${NC}"
|
||
echo -e "${GREEN}╚══════════════════════════════════════════════════════════╝${NC}"
|
||
echo ""
|
||
echo -e "\n${GREEN}按任意键返回...${NC}"; read -n 1 -s
|
||
}
|
||
|
||
# ================================================================
|
||
# --- WebDAV 更换共享目录(热更新)---
|
||
# ================================================================
|
||
_webdav_change_dir() {
|
||
echo ""
|
||
local _cur; _cur=$(grep "^WEBDAV_DIR=" "${STATE_FILE}" 2>/dev/null | tail -1 | cut -d= -f2-)
|
||
echo -e "${CYAN} 当前共享目录:${_cur:-未知}${NC}"
|
||
echo ""
|
||
|
||
local new_dir
|
||
_webdav_pick_dir new_dir || return 1
|
||
|
||
echo -e "${YELLOW}正在更换共享目录为:${new_dir}${NC}"
|
||
|
||
# 修改 compose 文件中的 volumes 行
|
||
local data_dir="/root/webdav_data"
|
||
sed -i "s|^ *- .*:/data| - ${new_dir}:/data|" "${data_dir}/docker-compose.yml"
|
||
|
||
# 重启容器使新目录生效
|
||
cd "$data_dir" && docker compose up -d --force-recreate
|
||
sleep 3
|
||
|
||
# 确认容器真的起来了
|
||
if ! docker inspect webdav_app &>/dev/null; then
|
||
echo -e "${RED}❌ 容器启动失败,请检查日志:docker logs webdav_app${NC}"
|
||
echo -e "\n${GREEN}按任意键返回...${NC}"; read -n 1 -s
|
||
return 1
|
||
fi
|
||
|
||
# 修复权限
|
||
docker exec webdav_app chmod -R 777 /data 2>/dev/null && \
|
||
echo -e "${GREEN} ✅ 写权限修复完成${NC}" || true
|
||
|
||
# 更新凭证文件(_save_credential 会先删旧值再写,不会重复)
|
||
_save_credential "WEBDAV_DIR" "$new_dir"
|
||
|
||
echo -e "${GREEN}✅ 共享目录已切换为:${new_dir}${NC}"
|
||
echo -e "\n${GREEN}按任意键返回...${NC}"; read -n 1 -s
|
||
}
|
||
|
||
# ================================================================
|
||
# --- WebDAV 删除 ---
|
||
# ================================================================
|
||
_webdav_delete() {
|
||
echo ""
|
||
echo -e "${RED} ⚠️ 此操作将删除 WebDAV 容器及配置文件。${NC}"
|
||
echo -e "${YELLOW} 共享目录中的文件【不会】被删除,只是停止共享。${NC}"
|
||
echo ""
|
||
read -p " 确认删除?输入 yes 继续: " confirm
|
||
if [ "$confirm" != "yes" ]; then
|
||
echo -e "${YELLOW}已取消。${NC}"; sleep 1; return
|
||
fi
|
||
uninstall_webdav
|
||
echo -e "\n${GREEN}按任意键返回...${NC}"; read -n 1 -s
|
||
}
|
||
|
||
# ================================================================
|
||
# --- WebDAV 目录选择(复用逻辑,避免重复写两遍)---
|
||
# ================================================================
|
||
_webdav_pick_dir() {
|
||
# $1 = 要赋值的变量名(nameref写法)
|
||
local _result_var="$1"
|
||
|
||
echo -e "${YELLOW} 请选择要共享的目录:${NC}"
|
||
echo -e " ${CYAN}1)${NC} /mnt (所有挂载的网盘,推荐)"
|
||
echo -e " ${CYAN}2)${NC} /mnt/TOOL (仅 TOOL 网盘)"
|
||
echo -e " ${CYAN}3)${NC} /mnt/Media (仅 Media 媒体库)"
|
||
echo -e " ${CYAN}4)${NC} /root (VPS本机根目录)"
|
||
echo -e " ${CYAN}5)${NC} 自定义路径"
|
||
echo ""
|
||
local dir_choice picked_dir
|
||
read -p " 请选择 [1-5,默认1]: " dir_choice
|
||
dir_choice="${dir_choice:-1}"
|
||
|
||
case "$dir_choice" in
|
||
1) picked_dir="/mnt" ;;
|
||
2) picked_dir="/mnt/TOOL" ;;
|
||
3) picked_dir="/mnt/Media" ;;
|
||
4) picked_dir="/root" ;;
|
||
5)
|
||
read -p " 请输入自定义路径(例:/mnt/CC-00): " picked_dir
|
||
if [ -z "$picked_dir" ]; then
|
||
echo -e "${RED}路径不能为空,已取消。${NC}"; sleep 2; return 1
|
||
fi
|
||
;;
|
||
*) picked_dir="/mnt" ;;
|
||
esac
|
||
|
||
if [ ! -d "$picked_dir" ]; then
|
||
echo -e "${YELLOW}目录不存在,正在创建 ${picked_dir}...${NC}"
|
||
mkdir -p "$picked_dir"
|
||
fi
|
||
|
||
# 把结果写回调用方指定的变量
|
||
printf -v "$_result_var" '%s' "$picked_dir"
|
||
}
|
||
|
||
# ================================================================
|
||
# --- 卸载 WebDAV 网盘 ---
|
||
# ================================================================
|
||
uninstall_webdav() {
|
||
echo -e "${YELLOW}正在卸载 WebDAV 网盘...${NC}"
|
||
local data_dir="/root/webdav_data"
|
||
if [ -d "$data_dir" ]; then
|
||
cd "$data_dir" && docker compose down 2>/dev/null
|
||
cd /root && rm -rf "$data_dir"
|
||
fi
|
||
docker rm -f webdav_app 2>/dev/null || true
|
||
sed -i '/^WEBDAV_/d' "${STATE_FILE}" 2>/dev/null
|
||
echo -e "${GREEN}✅ WebDAV 已卸载${NC}"
|
||
sleep 2
|
||
}
|
||
|
||
# ================================================================
|
||
# --- 网盘矩阵联合挂载 (mergerfs) ---
|
||
# 用途:把多个已挂载的网盘/本地目录合并成一个统一目录(路径可自定义)
|
||
# 副手VPS 在此做联合挂载,主力VPS 再用 SSHFS 挂载副手的联合目录
|
||
# ================================================================
|
||
|
||
# 读取已保存的联合挂载出口路径(默认 /mnt/union)
|
||
_mergerfs_get_union_path() {
|
||
local p
|
||
p=$(grep "^MERGERFS_UNION_PATH=" "${STATE_FILE}" 2>/dev/null | cut -d'=' -f2-)
|
||
echo "${p:-/mnt/union}"
|
||
}
|
||
|
||
# 检测 mergerfs 状态用于菜单显示
|
||
_mergerfs_menu_status() {
|
||
local union_path; union_path=$(_mergerfs_get_union_path)
|
||
if mount 2>/dev/null | grep -q " ${union_path} "; then
|
||
local cnt
|
||
cnt=$(grep "^MERGERFS_BRANCHES=" "${STATE_FILE}" 2>/dev/null | cut -d'=' -f2- | tr ':' '\n' | grep -c '.')
|
||
echo "[ ${GREEN}✅ 已激活 · ${cnt} 个来源 · ${union_path}${NC} ]"
|
||
elif command -v mergerfs &>/dev/null; then
|
||
echo "[ ${YELLOW}⚠️ 已安装/未挂载${NC} ]"
|
||
else
|
||
echo "[ ${RED}❌ 未安装${NC} ]"
|
||
fi
|
||
}
|
||
|
||
# 扫描并列出系统上所有"有意义"的目录供用户选择
|
||
# 包括:/mnt/* 、已挂载的 rclone/sshfs 挂载点、/home 各用户目录、/root
|
||
_mergerfs_list_candidates() {
|
||
local union_path; union_path=$(_mergerfs_get_union_path)
|
||
local shown=()
|
||
|
||
echo -e "${CYAN}┌─ 可加入联合挂载的路径 ──────────────────────────────┐${NC}"
|
||
|
||
# 1. /mnt/* 下的目录(排除联合出口自身)
|
||
for d in /mnt/*/; do
|
||
d="${d%/}"
|
||
[ "$d" = "$union_path" ] && continue
|
||
[ -d "$d" ] || continue
|
||
local size; size=$(df -h "$d" 2>/dev/null | tail -1 | awk '{print $2}')
|
||
echo -e "${CYAN}│${NC} ${GREEN}▸${NC} $d ${DIM}[${size}]${NC}"
|
||
shown+=("$d")
|
||
done
|
||
|
||
# 2. /home/* 各用户的主目录(常见于 ubuntu 用户)
|
||
for d in /home/*/; do
|
||
d="${d%/}"
|
||
[ -d "$d" ] || continue
|
||
# 避免重复
|
||
local dup=false
|
||
for s in "${shown[@]}"; do [ "$s" = "$d" ] && dup=true && break; done
|
||
$dup && continue
|
||
local size; size=$(df -h "$d" 2>/dev/null | tail -1 | awk '{print $2}')
|
||
echo -e "${CYAN}│${NC} ${GREEN}▸${NC} $d ${DIM}[${size:-?}]${NC}"
|
||
shown+=("$d")
|
||
done
|
||
|
||
# 3. /root 本身(如果不在 /home 下)
|
||
if [ -d /root ]; then
|
||
local dup=false
|
||
for s in "${shown[@]}"; do [ "$s" = "/root" ] && dup=true && break; done
|
||
if ! $dup; then
|
||
local size; size=$(df -h /root 2>/dev/null | tail -1 | awk '{print $2}')
|
||
echo -e "${CYAN}│${NC} ${GREEN}▸${NC} /root ${DIM}[${size}]${NC}"
|
||
fi
|
||
fi
|
||
|
||
# 4. 已挂载的 rclone/sshfs(可能挂在非 /mnt 路径)
|
||
mount 2>/dev/null | grep -E "fuse\.rclone|fuse\.sshfs" | awk '{print $3}' | while read -r mp; do
|
||
local dup=false
|
||
for s in "${shown[@]}"; do [ "$s" = "$mp" ] && dup=true && break; done
|
||
$dup && continue
|
||
local size; size=$(df -h "$mp" 2>/dev/null | tail -1 | awk '{print $2}')
|
||
echo -e "${CYAN}│${NC} ${GREEN}▸${NC} $mp ${DIM}[${size}]${NC}"
|
||
done
|
||
|
||
echo -e "${CYAN}└─────────────────────────────────────────────────────┘${NC}"
|
||
echo -e "${DIM} 也可直接输入任意路径,例如 /home/ubuntu/TOOL${NC}"
|
||
echo ""
|
||
}
|
||
|
||
manage_mergerfs_menu() {
|
||
while true; do
|
||
clear
|
||
local union_path; union_path=$(_mergerfs_get_union_path)
|
||
echo -e "${BLUE}╔══════════════════════════════════════════════════════════╗${NC}"
|
||
echo -e "${BLUE}║ 🗂️ 网盘矩阵联合挂载管理 (mergerfs) ║${NC}"
|
||
echo -e "${BLUE}╠══════════════════════════════════════════════════════════╣${NC}"
|
||
echo -e "${BLUE}║ 原理:把多个网盘/目录合并为一个统一入口 ║${NC}"
|
||
echo -e "${BLUE}║ 副手VPS → 做联合挂载 → 主力VPS用SSHFS挂入,岂不美哉 ║${NC}"
|
||
echo -e "${BLUE}╚══════════════════════════════════════════════════════════╝${NC}"
|
||
echo ""
|
||
|
||
# 显示当前联合挂载状态
|
||
if mount 2>/dev/null | grep -q " ${union_path} "; then
|
||
local branches_raw
|
||
branches_raw=$(grep "^MERGERFS_BRANCHES=" "${STATE_FILE}" 2>/dev/null | cut -d'=' -f2-)
|
||
echo -e " ${GREEN}✅ 联合出口:${YELLOW}${union_path}${GREEN} 【当前激活】${NC}"
|
||
echo -e " ${CYAN}包含的来源:${NC}"
|
||
echo "$branches_raw" | tr ':' '\n' | while read -r b; do
|
||
[ -n "$b" ] || continue
|
||
local size; size=$(df -h "$b" 2>/dev/null | tail -1 | awk '{print $4}')
|
||
echo -e " ${GREEN}▸${NC} $b ${DIM}(可用: ${size})${NC}"
|
||
done
|
||
else
|
||
echo -e " ${YELLOW}⚠️ 联合出口 ${union_path} 当前未激活${NC}"
|
||
fi
|
||
echo ""
|
||
echo -e " ${YELLOW}1)${NC} ➕ 创建/重建联合挂载(选择来源、自定义出口路径)"
|
||
echo -e " ${YELLOW}2)${NC} ➕ 追加目录到现有联合挂载"
|
||
echo -e " ${YELLOW}3)${NC} 🗑️ 卸载联合挂载"
|
||
echo -e " ${YELLOW}4)${NC} 📋 查看详细状态与空间"
|
||
echo -e " ${YELLOW}5)${NC} 📡 主力VPS接入指引"
|
||
echo -e " ${YELLOW}q)${NC} 返回主菜单"
|
||
echo ""
|
||
read -p " 请选择: " sub
|
||
case "$sub" in
|
||
1) _mergerfs_create ;;
|
||
2) _mergerfs_append ;;
|
||
3) _mergerfs_umount ;;
|
||
4) _mergerfs_status_detail ;;
|
||
5) _mergerfs_guide ;;
|
||
q|Q) return ;;
|
||
*) echo -e "${RED}无效选项${NC}"; sleep 1 ;;
|
||
esac
|
||
done
|
||
}
|
||
|
||
# 安装 mergerfs
|
||
_install_mergerfs() {
|
||
if command -v mergerfs &>/dev/null; then
|
||
echo -e "${GREEN}mergerfs 已安装,跳过。${NC}"
|
||
return 0
|
||
fi
|
||
echo -e "${YELLOW}正在安装 mergerfs...${NC}"
|
||
if apt-cache show mergerfs &>/dev/null 2>&1; then
|
||
sudo apt-get update -qq && sudo apt-get install -y mergerfs
|
||
else
|
||
local ver="2.40.2"
|
||
local deb_name=""
|
||
case "$SYS_ARCH" in
|
||
amd64) deb_name="mergerfs_${ver}.ubuntu-focal_amd64.deb" ;;
|
||
arm64) deb_name="mergerfs_${ver}.ubuntu-focal_arm64.deb" ;;
|
||
*)
|
||
echo -e "${RED}❌ 暂不支持架构 ${SYS_ARCH} 的自动安装,请手动安装 mergerfs。${NC}"
|
||
return 1 ;;
|
||
esac
|
||
local url="https://github.com/trapexit/mergerfs/releases/download/${ver}/${deb_name}"
|
||
echo -e "${YELLOW}从 GitHub 下载 mergerfs ${ver}...${NC}"
|
||
curl -L --retry 3 -o /tmp/mergerfs.deb "$url" && sudo dpkg -i /tmp/mergerfs.deb && rm -f /tmp/mergerfs.deb
|
||
fi
|
||
if command -v mergerfs &>/dev/null; then
|
||
echo -e "${GREEN}✅ mergerfs 安装成功!${NC}"
|
||
else
|
||
echo -e "${RED}❌ mergerfs 安装失败,请检查网络后重试。${NC}"
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
# 创建/重建联合挂载
|
||
_mergerfs_create() {
|
||
clear
|
||
echo -e "${BLUE}=== ➕ 创建网盘矩阵联合挂载 ===${NC}"
|
||
echo ""
|
||
|
||
_install_mergerfs || { read -n 1 -s -r -p "按任意键返回..."; return; }
|
||
|
||
# ── 第一步:选择联合出口路径 ──────────────────────────────────
|
||
local old_union_path; old_union_path=$(_mergerfs_get_union_path)
|
||
echo -e "${YELLOW}【第一步】设置联合出口路径(所有网盘合并后的统一入口)${NC}"
|
||
echo -e "${DIM} 当前/上次路径:${old_union_path}${NC}"
|
||
echo -e "${DIM} 直接回车保留原路径,或输入新路径(如 /mnt/allcloud)${NC}"
|
||
read -p " 联合出口路径: " input_union
|
||
local union_path="${input_union:-$old_union_path}"
|
||
# 不允许把出口设在某个来源内部,简单检查
|
||
echo -e "${GREEN} ✅ 联合出口将设为:${union_path}${NC}"
|
||
echo ""
|
||
|
||
# ── 第二步:选择要合并的来源路径 ─────────────────────────────
|
||
echo -e "${YELLOW}【第二步】选择要合并进来的目录(每行输入一个路径,空行结束)${NC}"
|
||
echo ""
|
||
_mergerfs_list_candidates
|
||
|
||
local branches=()
|
||
while true; do
|
||
read -p " 路径(空行结束): " input_path
|
||
[ -z "$input_path" ] && break
|
||
# 去掉末尾斜杠
|
||
input_path="${input_path%/}"
|
||
if [ ! -d "$input_path" ]; then
|
||
echo -e " ${RED}❌ 路径不存在,请检查后重新输入:$input_path${NC}"
|
||
continue
|
||
fi
|
||
if [ "$input_path" = "$union_path" ]; then
|
||
echo -e " ${RED}❌ 来源路径不能和联合出口相同,跳过。${NC}"
|
||
continue
|
||
fi
|
||
branches+=("$input_path")
|
||
local sz; sz=$(df -h "$input_path" 2>/dev/null | tail -1 | awk '{print $4}')
|
||
echo -e " ${GREEN}✅ 已加入:$input_path ${DIM}(可用: ${sz})${NC}"
|
||
done
|
||
|
||
if [ ${#branches[@]} -eq 0 ]; then
|
||
echo -e "${RED}❌ 未选择任何路径,操作取消。${NC}"; sleep 2; return
|
||
fi
|
||
|
||
echo ""
|
||
echo -e "${YELLOW}即将合并以下 ${#branches[@]} 个来源:${NC}"
|
||
for b in "${branches[@]}"; do
|
||
local sz; sz=$(df -h "$b" 2>/dev/null | tail -1 | awk '{print $4}')
|
||
echo -e " ${GREEN}▸${NC} $b ${DIM}(可用: ${sz})${NC}"
|
||
done
|
||
echo -e " ${CYAN}→ 联合出口:${union_path}${NC}"
|
||
echo ""
|
||
read -p "确认创建?(y/n,默认y): " confirm
|
||
[ "$confirm" = "n" ] || [ "$confirm" = "N" ] && return
|
||
|
||
# 卸载旧的联合挂载(如路径变了也一并处理)
|
||
for old_mp in "$old_union_path" "$union_path"; do
|
||
if mount 2>/dev/null | grep -q " ${old_mp} "; then
|
||
echo -e "${YELLOW}正在卸载旧联合挂载 ${old_mp}...${NC}"
|
||
sudo systemctl stop mergerfs-union.service 2>/dev/null || true
|
||
sudo fusermount -u "$old_mp" 2>/dev/null || sudo umount -l "$old_mp" 2>/dev/null
|
||
fi
|
||
done
|
||
|
||
sudo mkdir -p "$union_path"
|
||
|
||
local branch_str
|
||
branch_str=$(IFS=':'; echo "${branches[*]}")
|
||
|
||
echo -e "${YELLOW}正在挂载联合目录...${NC}"
|
||
sudo mergerfs \
|
||
-o defaults,allow_other,use_ino,category.create=mfs,moveonenospc=true,minfreespace=1G \
|
||
"${branch_str}" "${union_path}"
|
||
|
||
if mount 2>/dev/null | grep -q " ${union_path} "; then
|
||
echo -e "${GREEN}✅ 联合挂载成功!目录内容预览:${NC}"
|
||
ls "${union_path}" 2>/dev/null | head -10
|
||
else
|
||
echo -e "${RED}❌ 挂载失败,请检查各来源路径是否正确且可访问。${NC}"
|
||
read -n 1 -s -r -p "按任意键返回..."; return
|
||
fi
|
||
|
||
# 保存配置
|
||
sed -i '/^MERGERFS_BRANCHES=/d; /^MERGERFS_UNION_PATH=/d' "${STATE_FILE}" 2>/dev/null
|
||
echo "MERGERFS_UNION_PATH=${union_path}" >> "${STATE_FILE}"
|
||
echo "MERGERFS_BRANCHES=${branch_str}" >> "${STATE_FILE}"
|
||
|
||
# 写 systemd 服务
|
||
sudo tee /etc/systemd/system/mergerfs-union.service > /dev/null << MFEOF
|
||
[Unit]
|
||
Description=mergerfs Union Mount -> ${union_path}
|
||
After=network-online.target remote-fs.target
|
||
Wants=network-online.target
|
||
|
||
[Service]
|
||
Type=oneshot
|
||
RemainAfterExit=yes
|
||
ExecStartPre=/bin/sleep 5
|
||
ExecStart=/usr/bin/mergerfs -o defaults,allow_other,use_ino,category.create=mfs,moveonenospc=true,minfreespace=1G ${branch_str} ${union_path}
|
||
ExecStop=/bin/fusermount -u ${union_path}
|
||
|
||
[Install]
|
||
WantedBy=multi-user.target
|
||
MFEOF
|
||
|
||
sudo systemctl daemon-reload
|
||
sudo systemctl enable mergerfs-union.service
|
||
echo -e "${GREEN}✅ 已设置开机自动联合挂载!${NC}"
|
||
|
||
# 自动注册 Nextcloud
|
||
if docker ps 2>/dev/null | grep -q "nextcloud_app"; then
|
||
echo ""
|
||
read -p "检测到 Nextcloud 运行中,是否将 ${union_path} 注册为外部存储?(y/n,默认y): " nc_reg
|
||
if [ "$nc_reg" != "n" ] && [ "$nc_reg" != "N" ]; then
|
||
docker exec -u www-data nextcloud_app php occ files_external:create \
|
||
"/union网盘矩阵" local null::null \
|
||
--config=datadir="${union_path}" 2>/dev/null \
|
||
&& echo -e "${GREEN}✅ 已注册到 Nextcloud 外部存储!${NC}" \
|
||
|| echo -e "${YELLOW}⚠️ 自动注册失败,请在 Nextcloud → 设置 → 外部存储 中手动添加路径 ${union_path}${NC}"
|
||
fi
|
||
fi
|
||
|
||
echo ""
|
||
echo -e "${GREEN}╔══════════════════════════════════════════════════════════╗${NC}"
|
||
echo -e "${GREEN}║ 🎉 网盘矩阵联合挂载创建成功! ║${NC}"
|
||
echo -e "${GREEN}╠══════════════════════════════════════════════════════════╣${NC}"
|
||
echo -e "${GREEN}║ 联合出口:${YELLOW}${union_path}${NC}"
|
||
echo -e "${GREEN}║ 包含来源:${YELLOW}${#branches[@]} 个${NC}"
|
||
echo -e "${GREEN}║ 开机自启:${YELLOW}已启用${NC}"
|
||
echo -e "${GREEN}╠══════════════════════════════════════════════════════════╣${NC}"
|
||
echo -e "${GREEN}║ 💡 主力VPS 接入方式:选菜单 3.2 → 选项 5 查看指引 ║${NC}"
|
||
echo -e "${GREEN}╚══════════════════════════════════════════════════════════╝${NC}"
|
||
echo ""
|
||
echo -e "\n${GREEN}按任意键返回...${NC}"; read -n 1 -s
|
||
}
|
||
|
||
# 追加新目录到现有联合挂载
|
||
_mergerfs_append() {
|
||
clear
|
||
echo -e "${BLUE}=== ➕ 追加目录到联合挂载 ===${NC}"
|
||
echo ""
|
||
local union_path; union_path=$(_mergerfs_get_union_path)
|
||
|
||
if ! mount 2>/dev/null | grep -q " ${union_path} "; then
|
||
echo -e "${RED}❌ ${union_path} 当前未挂载,请先选择「创建联合挂载」。${NC}"
|
||
sleep 3; return
|
||
fi
|
||
|
||
local current_branches
|
||
current_branches=$(grep "^MERGERFS_BRANCHES=" "${STATE_FILE}" 2>/dev/null | cut -d'=' -f2-)
|
||
echo -e "${CYAN}当前已包含的来源:${NC}"
|
||
echo "$current_branches" | tr ':' '\n' | while read -r b; do
|
||
[ -n "$b" ] || continue
|
||
local sz; sz=$(df -h "$b" 2>/dev/null | tail -1 | awk '{print $4}')
|
||
echo -e " ${GREEN}▸${NC} $b ${DIM}(可用: ${sz})${NC}"
|
||
done
|
||
echo ""
|
||
_mergerfs_list_candidates
|
||
|
||
read -p "请输入要追加的目录路径: " new_path
|
||
new_path="${new_path%/}"
|
||
[ -z "$new_path" ] && return
|
||
if [ ! -d "$new_path" ]; then
|
||
echo -e "${RED}❌ 路径不存在:$new_path${NC}"; sleep 2; return
|
||
fi
|
||
if [ "$new_path" = "$union_path" ]; then
|
||
echo -e "${RED}❌ 不能把联合出口自身加入来源。${NC}"; sleep 2; return
|
||
fi
|
||
if echo "$current_branches" | tr ':' '\n' | grep -qx "$new_path"; then
|
||
echo -e "${YELLOW}⚠️ 该路径已在联合挂载中,无需重复添加。${NC}"; sleep 2; return
|
||
fi
|
||
|
||
local new_branches="${current_branches}:${new_path}"
|
||
|
||
echo -e "${YELLOW}正在重建联合挂载(含新目录)...${NC}"
|
||
sudo fusermount -u "$union_path" 2>/dev/null || sudo umount -l "$union_path" 2>/dev/null
|
||
sleep 1
|
||
sudo mergerfs \
|
||
-o defaults,allow_other,use_ino,category.create=mfs,moveonenospc=true,minfreespace=1G \
|
||
"${new_branches}" "${union_path}"
|
||
|
||
if mount 2>/dev/null | grep -q " ${union_path} "; then
|
||
sed -i '/^MERGERFS_BRANCHES=/d' "${STATE_FILE}" 2>/dev/null
|
||
echo "MERGERFS_BRANCHES=${new_branches}" >> "${STATE_FILE}"
|
||
sudo sed -i "s|ExecStart=.*mergerfs.*|ExecStart=/usr/bin/mergerfs -o defaults,allow_other,use_ino,category.create=mfs,moveonenospc=true,minfreespace=1G ${new_branches} ${union_path}|" \
|
||
/etc/systemd/system/mergerfs-union.service 2>/dev/null
|
||
sudo systemctl daemon-reload
|
||
local sz; sz=$(df -h "$new_path" 2>/dev/null | tail -1 | awk '{print $4}')
|
||
echo -e "${GREEN}✅ 追加成功!已加入:${new_path} (可用: ${sz})${NC}"
|
||
else
|
||
echo -e "${RED}❌ 重建失败,请检查路径是否正确且可访问。${NC}"
|
||
fi
|
||
echo ""
|
||
echo -e "\n${GREEN}按任意键返回...${NC}"; read -n 1 -s
|
||
}
|
||
|
||
# 卸载联合挂载
|
||
_mergerfs_umount() {
|
||
clear
|
||
echo -e "${BLUE}=== 🗑️ 卸载联合挂载 ===${NC}"
|
||
echo ""
|
||
local union_path; union_path=$(_mergerfs_get_union_path)
|
||
|
||
if ! mount 2>/dev/null | grep -q " ${union_path} "; then
|
||
echo -e "${YELLOW}⚠️ ${union_path} 当前并未挂载。${NC}"; sleep 2; return
|
||
fi
|
||
echo -e "${YELLOW}此操作将卸载 ${union_path},底层各来源目录不受影响。${NC}"
|
||
read -p "确认卸载?(y/n): " confirm
|
||
[ "$confirm" != "y" ] && [ "$confirm" != "Y" ] && return
|
||
|
||
sudo systemctl stop mergerfs-union.service 2>/dev/null || true
|
||
sudo fusermount -u "$union_path" 2>/dev/null || sudo umount -l "$union_path" 2>/dev/null
|
||
|
||
read -p "是否同时禁用开机自启并清除配置?(y/n,默认n): " dis_auto
|
||
if [ "$dis_auto" = "y" ] || [ "$dis_auto" = "Y" ]; then
|
||
sudo systemctl disable mergerfs-union.service 2>/dev/null
|
||
sudo rm -f /etc/systemd/system/mergerfs-union.service
|
||
sudo systemctl daemon-reload
|
||
sed -i '/^MERGERFS_BRANCHES=/d; /^MERGERFS_UNION_PATH=/d' "${STATE_FILE}" 2>/dev/null
|
||
echo -e "${GREEN}✅ 已完全移除联合挂载及开机自启。${NC}"
|
||
else
|
||
echo -e "${GREEN}✅ 已卸载 ${union_path},配置保留,下次开机将自动重建。${NC}"
|
||
fi
|
||
sleep 2
|
||
}
|
||
|
||
# 查看详细状态
|
||
_mergerfs_status_detail() {
|
||
clear
|
||
echo -e "${BLUE}=== 📋 联合挂载详细状态 ===${NC}"
|
||
echo ""
|
||
local union_path; union_path=$(_mergerfs_get_union_path)
|
||
|
||
if mount 2>/dev/null | grep -q " ${union_path} "; then
|
||
echo -e "${GREEN}✅ ${union_path} 挂载中${NC}"
|
||
echo ""
|
||
echo -e "${CYAN}─── 各来源空间 ───────────────────────────────────────${NC}"
|
||
local branches_raw
|
||
branches_raw=$(grep "^MERGERFS_BRANCHES=" "${STATE_FILE}" 2>/dev/null | cut -d'=' -f2-)
|
||
echo "$branches_raw" | tr ':' '\n' | while read -r b; do
|
||
[ -n "$b" ] || continue
|
||
if [ -d "$b" ]; then
|
||
local info; info=$(df -h "$b" 2>/dev/null | tail -1)
|
||
local used avail pct
|
||
used=$(echo "$info" | awk '{print $3}')
|
||
avail=$(echo "$info" | awk '{print $4}')
|
||
pct=$(echo "$info" | awk '{print $5}')
|
||
echo -e " ${GREEN}▸${NC} $b"
|
||
echo -e " 已用:${YELLOW}${used}${NC} 可用:${GREEN}${avail}${NC} 占比:${pct}"
|
||
else
|
||
echo -e " ${RED}▸${NC} $b ${RED}【路径不可访问/未挂载】${NC}"
|
||
fi
|
||
done
|
||
echo ""
|
||
echo -e "${CYAN}─── 联合出口整体空间 ─────────────────────────────────${NC}"
|
||
df -h "${union_path}" 2>/dev/null | tail -1 | \
|
||
awk '{printf " 总容量: %-8s 已用: %-8s 可用: %-8s 占比: %s\n", $2, $3, $4, $5}'
|
||
echo ""
|
||
echo -e "${CYAN}─── systemd 服务状态 ─────────────────────────────────${NC}"
|
||
systemctl status mergerfs-union.service 2>/dev/null | head -8 | sed 's/^/ /'
|
||
else
|
||
echo -e "${RED}❌ ${union_path} 未挂载${NC}"
|
||
echo ""
|
||
local saved_branches
|
||
saved_branches=$(grep "^MERGERFS_BRANCHES=" "${STATE_FILE}" 2>/dev/null | cut -d'=' -f2-)
|
||
if [ -n "$saved_branches" ]; then
|
||
echo -e "${YELLOW}上次配置的来源路径:${NC}"
|
||
echo "$saved_branches" | tr ':' '\n' | while read -r b; do
|
||
[ -n "$b" ] && echo -e " ▸ $b"
|
||
done
|
||
echo ""
|
||
read -p "是否立即重建联合挂载?(y/n): " rebuild
|
||
if [ "$rebuild" = "y" ] || [ "$rebuild" = "Y" ]; then
|
||
sudo mkdir -p "$union_path"
|
||
sudo mergerfs \
|
||
-o defaults,allow_other,use_ino,category.create=mfs,moveonenospc=true,minfreespace=1G \
|
||
"${saved_branches}" "${union_path}" \
|
||
&& echo -e "${GREEN}✅ 重建成功!${NC}" \
|
||
|| echo -e "${RED}❌ 重建失败,请检查底层来源是否已挂载。${NC}"
|
||
fi
|
||
fi
|
||
fi
|
||
echo ""
|
||
echo -e "\n${GREEN}按任意键返回...${NC}"; read -n 1 -s
|
||
}
|
||
|
||
# 主力VPS接入指引
|
||
_mergerfs_guide() {
|
||
clear
|
||
local union_path; union_path=$(_mergerfs_get_union_path)
|
||
local my_ip
|
||
my_ip=$(curl -4 -s --max-time 5 ifconfig.me 2>/dev/null || curl -6 -s --max-time 5 ifconfig.me 2>/dev/null || echo "YOUR_VPS_IP")
|
||
echo -e "${BLUE}╔══════════════════════════════════════════════════════════╗${NC}"
|
||
echo -e "${BLUE}║ 📡 主力VPS接入副手联合挂载 — 操作指引 ║${NC}"
|
||
echo -e "${BLUE}╚══════════════════════════════════════════════════════════╝${NC}"
|
||
echo ""
|
||
echo -e "${CYAN}【当前副手VPS 联合出口】${NC}"
|
||
echo -e " 本机 IP : ${YELLOW}${my_ip}${NC}"
|
||
echo -e " 联合路径 : ${YELLOW}${union_path}${NC}"
|
||
echo ""
|
||
echo -e "${CYAN}【架构示意】${NC}"
|
||
echo -e " 副手VPS (${my_ip})"
|
||
echo -e " ├─ /mnt/onedrive ─┐"
|
||
echo -e " ├─ /mnt/gdrive ─┤ mergerfs"
|
||
echo -e " ├─ /home/ubuntu/… ─┤ ══════▶ ${YELLOW}${union_path}${NC}"
|
||
echo -e " └─ /mnt/… ─┘"
|
||
echo -e " 主力VPS"
|
||
echo -e " └─ SSHFS ══▶ ${union_path}@副手 ══▶ /mnt/fushou ✨ 透明读写所有网盘"
|
||
echo ""
|
||
echo -e "${CYAN}【主力VPS 操作步骤】${NC}"
|
||
echo -e " ${YELLOW}方式A:用鸡公头面板一键挂载(推荐)${NC}"
|
||
echo -e " ┌─────────────────────────────────────────────────────┐"
|
||
echo -e " │ 主力VPS 运行鸡公头面板 → 选 3(挂载远程VPS) │"
|
||
echo -e " │ 对方IP : ${my_ip}"
|
||
echo -e " │ 远程路径 : ${union_path}"
|
||
echo -e " │ 本机挂载 : /mnt/fushou(或任意你喜欢的名字) │"
|
||
echo -e " └─────────────────────────────────────────────────────┘"
|
||
echo ""
|
||
echo -e " ${YELLOW}方式B:手动命令${NC}"
|
||
echo -e " ┌─────────────────────────────────────────────────────┐"
|
||
echo -e " │ apt install sshfs -y │"
|
||
echo -e " │ mkdir -p /mnt/fushou │"
|
||
echo -e " │ sshfs root@${my_ip}:${union_path} /mnt/fushou \\ "
|
||
echo -e " │ -o reconnect,ServerAliveInterval=15,allow_other │"
|
||
echo -e " └─────────────────────────────────────────────────────┘"
|
||
echo ""
|
||
echo -e "${CYAN}【温馨提示】${NC}"
|
||
echo -e " ▸ mergerfs 写入策略 mfs = 优先写入剩余空间最大的来源"
|
||
echo -e " ▸ 本地目录(如 /home/ubuntu/TOOL)也可加入,190G 物尽其用"
|
||
echo -e " ▸ 某个来源断连,/union 对应部分变空,重挂来源即恢复"
|
||
echo -e " ▸ 主力VPS 看到的是一个统一目录,无需关心底层几个盘 🎉"
|
||
echo ""
|
||
echo -e "\n${GREEN}按任意键返回...${NC}"; read -n 1 -s
|
||
}
|
||
|
||
# --- 主循環 ---
|
||
main() {
|
||
# 最优先:检测系统环境(网络/架构/发行版),后续所有分支都依赖此结果
|
||
setup_colors # 先初始化颜色,让检测过程中的提示有颜色
|
||
echo -e "${YELLOW}⏳ 正在检测系统环境,请稍候...${NC}"
|
||
detect_environment
|
||
# 检测完成后给用户一个即时反馈
|
||
if $IS_IPV6_ONLY; then
|
||
echo -e "${YELLOW}🌐 检测到纯 IPv6 环境 (${SYS_DISTRO} ${SYS_CODENAME} / ${SYS_ARCH}),已自动启用兼容方案。${NC}"
|
||
sleep 2
|
||
fi
|
||
check_core_dependencies
|
||
setup_shortcut
|
||
|
||
while true; do
|
||
show_main_menu
|
||
read -p "請輸入您的選擇 (u, m, s, 1-30.2, X, 99, q): " choice
|
||
case $choice in
|
||
u|U) update_system ;;
|
||
m|M) run_unminimize ;;
|
||
s|S) manage_swap ;;
|
||
1) if systemctl is-active --quiet cloudflared 2>/dev/null; then
|
||
echo -e "\n${YELLOW}Cloudflare Tunnel 已安裝並運行中。如需重裝請先執行選項 40 卸載。${NC}"; sleep 3
|
||
elif [ -f "/etc/systemd/system/cloudflared.service" ]; then
|
||
echo -e "\n${YELLOW}检测到 cloudflared 服务文件存在但未运行,正在尝试启动...${NC}"
|
||
sudo systemctl start cloudflared; sleep 2
|
||
if systemctl is-active --quiet cloudflared; then
|
||
echo -e "${GREEN}✅ Cloudflare Tunnel 已成功启动!${NC}"
|
||
else
|
||
echo -e "${RED}启动失败,服务可能已损坏。正在清理旧服务后重新安装...${NC}"; sleep 2
|
||
sudo cloudflared service uninstall 2>/dev/null; sudo systemctl daemon-reload; sleep 1
|
||
install_cloudflare_tunnel
|
||
fi; sleep 2
|
||
else
|
||
install_cloudflare_tunnel
|
||
fi ;;
|
||
2) configure_rclone_engine ;;
|
||
3) manage_sshfs_menu ;;
|
||
3.1) install_webdav ;;
|
||
3.2) manage_mergerfs_menu ;;
|
||
4.1) [ -d "/root/nextcloud_data" ] && { echo -e "\n${YELLOW}Nextcloud 已安裝。${NC}"; sleep 2; } || install_nextcloud ;;
|
||
4.2) [ -d "/root/onlyoffice_data" ] && { echo -e "\n${YELLOW}OnlyOffice 已安裝。${NC}"; sleep 2; } || install_onlyoffice ;;
|
||
4.3) [ -d "/root/home_assistant_data" ] && { echo -e "\n${YELLOW}Home Assistant 已安裝。${NC}"; sleep 2; } || install_home_assistant ;;
|
||
4.4) [ -d "/root/wordpress_data" ] && { echo -e "\n${YELLOW}WordPress 已安裝。${NC}"; sleep 2; } || install_wordpress ;;
|
||
5.1) [ -d "/root/ai_stack" ] && { echo -e "\n${YELLOW}AI 大腦已安裝。${NC}"; sleep 2; } || install_ai_suite ;;
|
||
5.2) install_ai_model ;;
|
||
5.3) [ -d "/root/jellyfin_data" ] && { echo -e "\n${YELLOW}Jellyfin 已安裝。${NC}"; sleep 2; } || install_jellyfin ;;
|
||
5.4) [ -d "/root/navidrome_data" ] && { echo -e "\n${YELLOW}Navidrome 已安裝。${NC}"; sleep 2; } || install_navidrome ;;
|
||
5.5) [ -d "/root/immich_data" ] && { echo -e "\n${YELLOW}Immich 已安裝。${NC}"; sleep 2; } || install_immich ;;
|
||
5.6) [ -d "/root/calibre_web_data" ] && { echo -e "\n${YELLOW}Calibre-Web 已安裝。${NC}"; sleep 2; } || install_calibre_web ;;
|
||
5.7) [ -d "/root/kavita_data" ] && { echo -e "\n${YELLOW}Kavita 已安裝。${NC}"; sleep 2; } || install_kavita ;;
|
||
6.1) [ -d "/root/miniflux_data" ] && { echo -e "\n${YELLOW}Miniflux 已安裝。${NC}"; sleep 2; } || install_miniflux ;;
|
||
6.2) [ -d "/root/gitea_data" ] && { echo -e "\n${YELLOW}Gitea 已安裝。${NC}"; sleep 2; } || install_gitea ;;
|
||
6.3) [ -d "/root/qbittorrent_data" ] && { echo -e "\n${YELLOW}qBittorrent 已安裝。${NC}"; sleep 2; } || install_qbittorrent ;;
|
||
6.4) [ -d "/root/jdownloader_data" ] && { echo -e "\n${YELLOW}JDownloader 已安裝。${NC}"; sleep 2; } || install_jdownloader ;;
|
||
6.5) [ -d "/root/ytdlp_data" ] && { echo -e "\n${YELLOW}yt-dlp 已安裝。${NC}"; sleep 2; } || install_ytdlp ;;
|
||
6.6) [ -d "/root/drawio_data" ] && { echo -e "\n${YELLOW}Draw.io 已安裝。${NC}"; sleep 2; } || install_drawio ;;
|
||
6.7) [ -d "/root/n8n_data" ] && { echo -e "\n${YELLOW}N8N 已安裝。${NC}"; sleep 2; } || install_n8n ;;
|
||
6.8) docker inspect gotify_app &>/dev/null && { echo -e "\n${YELLOW}Gotify 已安裝。${NC}"; sleep 2; } || install_gotify ;;
|
||
6.9) [ -d "/root/actual_budget_data" ] && { echo -e "\n${YELLOW}Actual Budget 已安裝。${NC}"; sleep 2; } || install_actual_budget ;;
|
||
6.10) [ -d "/root/excalidraw_data" ] && { echo -e "\n${YELLOW}Excalidraw 已安裝。${NC}"; sleep 2; } || install_excalidraw ;;
|
||
6.11) [ -d "/root/joplin_data" ] && { echo -e "\n${YELLOW}Joplin Server 已安裝。${NC}"; sleep 2; } || install_joplin_server ;;
|
||
6.12) [ -d "/root/trilium_data" ] && { echo -e "\n${YELLOW}Trilium Notes 已安裝。${NC}"; sleep 2; } || install_trilium ;;
|
||
7.1) [ -d "/root/sillytavern_data" ] && { echo -e "\n${YELLOW}SillyTavern 已安裝。如需重裝請先執行選項 20.2 卸載。${NC}"; sleep 2; } || install_sillytavern ;;
|
||
7.2) install_popodash ;;
|
||
7.3) install_mimi ;;
|
||
8.1) [ -f "/etc/xrdp/xrdp.ini" ] && { echo -e "\n${YELLOW}遠程工作台已安裝。${NC}"; sleep 2; } || install_desktop_env ;;
|
||
8.2) install_chinese_fonts ;;
|
||
8.3)
|
||
if [ ! -f "/etc/xrdp/xrdp.ini" ]; then
|
||
echo -e "${RED}错误:远程工作台未安装。${NC}"; sleep 3
|
||
else
|
||
clear
|
||
echo -e "${BLUE} ── 远程桌面工具 ──────────────────────────────${NC}"
|
||
local cur_port; cur_port=$(grep "^port" /etc/xrdp/xrdp.ini 2>/dev/null | awk '{print $3}')
|
||
echo -e " 当前 RDP 端口: ${GREEN}${cur_port:-3389}${NC}"
|
||
echo ""
|
||
echo -e " ${YELLOW}1)${NC} 安装浏览器"
|
||
echo -e " ${YELLOW}2)${NC} 更改 RDP 端口(当前: ${cur_port:-3389})"
|
||
echo -e " ${YELLOW}其他)${NC} 取消"
|
||
read -p " 请选择: " sub83
|
||
case "$sub83" in
|
||
1) _install_desktop_browser ;;
|
||
2)
|
||
echo ""
|
||
echo -e " ${YELLOW}建议使用 10000-65535 之间的端口,避免 3389 被扫描。${NC}"
|
||
read -p " 请输入新端口号: " new_rdp_port
|
||
if [[ "$new_rdp_port" =~ ^[0-9]+$ ]] && [ "$new_rdp_port" -ge 1024 ] && [ "$new_rdp_port" -le 65535 ]; then
|
||
sudo sed -i "s/^port[[:space:]]*=.*/port=${new_rdp_port}/" /etc/xrdp/xrdp.ini
|
||
sudo systemctl restart xrdp
|
||
echo -e " ${GREEN}✅ RDP 端口已更改为 ${new_rdp_port},服务已重启。${NC}"
|
||
echo -e " ${YELLOW}提示:请确保防火墙已放行新端口,否则无法连接。${NC}"
|
||
_save_credential "XRDP_PORT" "${new_rdp_port}"
|
||
else
|
||
echo -e " ${RED}❌ 无效端口号,请输入 1024-65535 之间的数字。${NC}"
|
||
fi
|
||
read -n 1 -s -r -p " 按任意键返回..."
|
||
;;
|
||
esac
|
||
fi
|
||
;;
|
||
8.4) systemctl is-active --quiet fail2ban 2>/dev/null && { echo -e "\n${YELLOW}Fail2ban 已安裝並運行中。${NC}"; sleep 2; } || install_fail2ban ;;
|
||
8.5) [ -d "/root/uptime_kuma_data" ] && { echo -e "\n${YELLOW}Uptime Kuma 已安裝。${NC}"; sleep 2; } || install_uptime_kuma ;;
|
||
8.6) install_glances ;;
|
||
8.7) systemctl is-active --quiet "syncthing@root" 2>/dev/null && { echo -e "\n${YELLOW}Syncthing 已安裝並運行中。${NC}"; sleep 2; } || install_syncthing ;;
|
||
8.8) docker inspect grafana_app &>/dev/null && { echo -e "\n${YELLOW}Grafana 已安裝。${NC}"; sleep 2; } || install_grafana ;;
|
||
9.1) add_ssh_public_key ;;
|
||
9.2) toggle_ssh_password_login ;;
|
||
10.1) run_nextcloud_optimization ;;
|
||
10.2) run_nextcloud_super_boost ;;
|
||
10.3) manage_nextcloud_storage ;;
|
||
10.4) run_nextcloud_preview_setup ;;
|
||
40) show_service_control_panel ;;
|
||
41) run_backup_setup ;;
|
||
10.5) show_credentials ;;
|
||
10.6) show_vps_info ;;
|
||
10.7) run_wrap_up_tasks ;;
|
||
10.8) install_oci_helper ;;
|
||
10.9) install_mail_report ;;
|
||
20.1) install_oracle_fullstack ;;
|
||
20.2) show_uninstall_menu ;;
|
||
30.1) install_vless_node ;;
|
||
30.2) install_hysteria_node ;;
|
||
x|X) system_cleanup ;;
|
||
99) uninstall_everything ;;
|
||
q|Q) echo -e "${BLUE}裝修愉快,房主再見!${NC}"; exit 0 ;;
|
||
*) echo -e "${RED}無效的選項,請重新輸入。${NC}"; sleep 2 ;;
|
||
esac
|
||
done
|
||
}
|
||
|
||
main "$@"
|