#!/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 </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 < ${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 <> ${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 < /root/home_assistant_data/config/configuration.yaml <> ${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 <> ${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 <> ${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 <> ${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 <> ${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 <> ${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 <> ${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 <> ${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 <> ${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 < /root/vless_data/config.json <> ${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 <> ${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 < /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 < "$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 < "$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 <> "/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="

服务器每日管家报告

主机名: $HOSTNAME

报告时间: $CURRENT_TIME


核心状态摘要:

  • 已持续运行: $UPTIME
  • 最近启动于: $LAST_REBOOT
  • SSH 登录失败次数 (今日): $FAILED_LOGINS 次

今日网络流量报告 (由 vnstat 提供):

$TRAFFIC_INFO

提示: 如果 vnstat 报告显示 'Not enough data available yet',这是正常的,请等待24小时后它才能收集到完整数据。

" 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 '/true' ~/.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 <设置->图形用户界面] 设置用户名和密码!${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','')) except: print('') " 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 " 精确匹配挂载点字段,避免路径被当成 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 <> ${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 <> ${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 <> ${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 <> ${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 <> ${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 <> ${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 <> ${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 <> ${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 < /root/grafana_data/docker-compose.yml <> ${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 "$@"