#!/usr/bin/env bash # OOBO VPS Toolbox v2.2.0 set -o pipefail OOBO_VERSION=2.2.0 SINGBOX_URL='https://raw.githubusercontent.com/caigouzi121380/singbox-deploy/main/install-singbox-yyds.sh' KOMARI_URL='https://raw.githubusercontent.com/komari-monitor/komari-agent/refs/heads/main/install.sh' OOKLA_URL='https://packagecloud.io/install/repositories/ookla/speedtest-cli/script.deb.sh' KOMARI_ENDPOINT='https://eis.qzz.io' info(){ printf '[信息] %s\n' "$*"; } warn(){ printf '[注意] %s\n' "$*"; } err(){ printf '[错误] %s\n' "$*" >&2; } pause(){ printf '\n'; read -r -p '按回车继续...' _ || true; } confirm(){ local a; read -r -p "$1 [y/N]: " a || return 1; [[ "$a" == y || "$a" == Y ]]; } as_root(){ if (( EUID == 0 )); then "$@"; elif command -v sudo >/dev/null 2>&1; then sudo "$@"; else err '需要 root 权限或 sudo'; return 1; fi; } has(){ command -v "$1" >/dev/null 2>&1; } check_system(){ [[ -r /etc/os-release ]] || { err '找不到 /etc/os-release'; exit 1; } . /etc/os-release OS_ID="$ID"; OS_NAME="${PRETTY_NAME:-$ID}" case "$OS_ID" in debian|ubuntu) INIT=systemd; has systemctl || { err '当前环境没有 systemctl'; exit 1; } ;; alpine) INIT=openrc; has rc-service || { err '当前环境没有 OpenRC'; exit 1; } ;; *) err "不支持的系统: $OS_NAME"; exit 1 ;; esac } check_dependencies(){ if [[ "$OS_ID" == alpine ]]; then local missing=() has bash || missing+=(bash) has curl || missing+=(curl) if (( ${#missing[@]} )); then as_root apk add --no-cache "${missing[@]}" || return 1; fi else if ! has curl; then warn '缺少 curl,准备通过 APT 安装' as_root apt-get update && as_root apt-get install -y curl || return 1 fi fi } service_exists(){ if [[ "$INIT" == systemd ]]; then systemctl list-unit-files "$1.service" --no-legend --no-pager 2>/dev/null | grep -q "^$1.service " || [[ -f "/etc/systemd/system/$1.service" || -f "/lib/systemd/system/$1.service" || -f "/usr/lib/systemd/system/$1.service" ]] else [[ -f "/etc/init.d/$1" ]] fi } service_status(){ if ! service_exists "$1"; then warn "没有检测到 $1 服务"; return 0; fi if [[ "$INIT" == systemd ]]; then systemctl --no-pager --full status "$1" || true; else rc-service "$1" status || true; fi } service_restart(){ service_exists "$1" || { warn "没有检测到 $1 服务"; return 0; } confirm "确定重启 $1?" || return 0 if [[ "$INIT" == systemd ]]; then as_root systemctl restart "$1"; else as_root rc-service "$1" restart; fi } status_word(){ if "$@"; then printf '已安装'; else printf '未安装'; fi; } docker_installed(){ has docker; } compose_installed(){ has docker && docker compose version >/dev/null 2>&1; } singbox_installed(){ has sb || has sing-box || [[ -x /usr/local/bin/sing-box || -x /usr/bin/sing-box ]] || service_exists sing-box; } caddy_installed(){ has caddy; } speedtest_installed(){ has speedtest; } komari_installed(){ has komari-agent || [[ -x /opt/komari/agent ]] || service_exists komari-agent; } # 下载到临时文件,验 Bash 语法,显示 SHA256,明确确认后执行。 # 该流程不能保证远程脚本安全;执行前仍需信任和审查来源。 fetch_script(){ local url="$1" out="$2" curl -fsSL --connect-timeout 15 --max-time 120 "$url" -o "$out" || { err '下载失败'; return 1; } [[ -s "$out" ]] || { err '下载内容为空'; return 1; } bash -n "$out" || { err '脚本 Bash 语法检查失败'; return 1; } printf '\nSHA-256: '; sha256sum "$out" warn "来源:$url" if confirm '查看脚本内容?'; then if has less; then less "$out"; else cat "$out"; fi fi } install_docker(){ if docker_installed && compose_installed; then info 'Docker 和 Compose 均已安装,跳过'; return 0; fi if docker_installed; then warn 'Docker 已安装,但未检测到 Compose 插件。不会重复执行 Docker 安装脚本。' if ! apt-cache show docker-compose-plugin >/dev/null 2>&1; then warn 'APT 未找到 docker-compose-plugin;请先配置 Docker 官方 APT 仓库。' return 1 fi confirm '只安装 Docker Compose 插件?' || return 0 as_root apt-get update && as_root apt-get install -y docker-compose-plugin return $? fi warn '将使用 Docker 官方 get.docker.com 便捷安装脚本(适合测试环境)。' confirm '确认下载安装脚本?' || return 0 local tmp rc tmp=$(mktemp) || return 1 if ! fetch_script 'https://get.docker.com' "$tmp"; then rm -f "$tmp"; return 1; fi if confirm '确认以 root 执行 Docker 安装脚本?'; then as_root sh "$tmp"; rc=$?; else info '已取消'; rc=0; fi rm -f "$tmp" if (( rc == 0 )); then docker --version 2>/dev/null || true; docker compose version 2>/dev/null || true; fi return "$rc" } install_singbox(){ if singbox_installed; then info '检测到 sing-box 或 sb,跳过安装以保护现有配置'; return 0; fi local tmp rc tmp=$(mktemp) || return 1 if ! fetch_script "$SINGBOX_URL" "$tmp"; then rm -f "$tmp"; return 1; fi if confirm '确认以 root 执行第三方 sing-box 安装脚本?'; then as_root bash "$tmp"; rc=$?; else info '已取消'; rc=0; fi rm -f "$tmp"; return "$rc" } install_caddy(){ if caddy_installed; then info 'Caddy 已安装,跳过'; return 0; fi confirm '通过 Caddy 官方 APT 仓库安装?' || return 0 as_root apt-get update || return 1 as_root apt-get install -y debian-keyring debian-archive-keyring apt-transport-https curl gnupg ca-certificates || return 1 local key list key=$(mktemp) || return 1 list=$(mktemp) || { rm -f "$key"; return 1; } if ! curl -fsSL https://dl.cloudsmith.io/public/caddy/stable/gpg.key -o "$key" || ! curl -fsSL https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt -o "$list"; then rm -f "$key" "$list"; err '下载 Caddy 仓库配置失败'; return 1 fi if ! as_root gpg --batch --yes --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg "$key" || ! as_root install -m 644 "$list" /etc/apt/sources.list.d/caddy-stable.list; then rm -f "$key" "$list"; return 1 fi rm -f "$key" "$list" as_root chmod a+r /usr/share/keyrings/caddy-stable-archive-keyring.gpg || return 1 as_root apt-get update && as_root apt-get install -y caddy } install_speedtest(){ if speedtest_installed; then info 'Speedtest 已安装,跳过'; echo '测速命令:speedtest'; return 0; fi confirm '安装 Ookla Speedtest?' || return 0 as_root apt-get update || return 1 as_root apt-get install -y curl gnupg apt-transport-https ca-certificates || return 1 local tmp rc tmp=$(mktemp) || return 1 if ! fetch_script "$OOKLA_URL" "$tmp"; then rm -f "$tmp"; return 1; fi if confirm '确认以 root 执行 Ookla 仓库配置脚本?'; then as_root bash "$tmp"; rc=$?; else info '已取消'; rc=1; fi rm -f "$tmp" (( rc == 0 )) || return "$rc" as_root apt-get update && as_root apt-get install -y speedtest || return 1 info 'Speedtest 安装完成。需要测速时运行:speedtest(不会自动测速)' } install_komari(){ if komari_installed; then info 'Komari Agent 已安装,跳过以保护配置'; return 0; fi local secret day tmp rc local -a args printf 'Komari 面板:%s\n' "$KOMARI_ENDPOINT" read -r -s -p '请输入 Auto-Discovery 密钥:' secret; printf '\n' [[ -n "$secret" ]] || { err '密钥不能为空'; return 1; } read -r -p '每月流量重置日期(1-31,直接回车不设置):' day if [[ -n "$day" && ! "$day" =~ ^([1-9]|[12][0-9]|3[01])$ ]]; then err '请输入 1-31 或留空'; unset secret; return 1; fi args=(-e "$KOMARI_ENDPOINT" --auto-discovery "$secret") [[ -z "$day" ]] || args+=(--month-rotate "$day") printf '面板:%s\n密钥:已输入(隐藏)\n重置日期:%s\n' "$KOMARI_ENDPOINT" "${day:-不指定}" local tmp tmp=$(mktemp) || { unset secret; return 1; } if ! fetch_script "$KOMARI_URL" "$tmp"; then rm -f "$tmp"; unset secret; return 1; fi if confirm '确认以 root 执行 Komari 安装脚本?'; then # 安装器通过命令行参数接收密钥,运行时可能短暂出现在进程列表中。 as_root bash "$tmp" "${args[@]}"; rc=$? else info '已取消'; rc=0; fi rm -f "$tmp"; unset secret; unset args return "$rc" } show_logs(){ local svc="$1" if [[ "$INIT" == systemd ]]; then journalctl -u "$svc" -n 50 --no-pager || true else warn 'OpenRC 日志路径取决于安装器;下面显示服务定义和可能的日志文件。' if [[ -r "/etc/init.d/$svc" ]]; then grep -E 'log|output|error|supervisor|command=' "/etc/init.d/$svc" || true; fi for f in "/var/log/$svc.log" "/var/log/$svc/current"; do [[ ! -f "$f" ]] || tail -n 50 "$f"; done fi } docker_menu(){ local c while true; do printf '\n========== Docker 管理 ==========\n1. 安装 Docker + Compose\n2. 查看版本\n3. 查看运行状态\n4. 重启 Docker\n0. 返回主菜单\n' read -r -p '请选择:' c || return case "$c" in 1) install_docker; pause;; 2) if has docker; then docker --version; docker compose version || true; else warn '未安装 Docker'; fi; pause;; 3) service_status docker; pause;; 4) service_restart docker; pause;; 0) return;; *) warn '无效选项'; pause;; esac done } singbox_menu(){ local c while true; do printf '\n========== sing-box 管理 ==========\n1. 安装 sing-box(第三方脚本)\n2. 查看版本\n3. 查看运行状态\n4. 重启 sing-box\n5. 查看日志\n6. 打开 sb 管理界面\n0. 返回主菜单\n' read -r -p '请选择:' c || return case "$c" in 1) install_singbox; pause;; 2) if has sing-box; then sing-box version; else warn '未找到 sing-box 命令'; fi; pause;; 3) service_status sing-box; pause;; 4) service_restart sing-box; pause;; 5) show_logs sing-box; pause;; 6) if has sb; then sb; else warn '未找到 sb 命令'; fi; pause;; 0) return;; *) warn '无效选项'; pause;; esac done } caddy_menu(){ local c while true; do printf '\n========== Caddy 管理 ==========\n1. 安装 Caddy\n2. 查看版本\n3. 查看运行状态\n4. 重启 Caddy\n5. 验证 Caddyfile 配置\n0. 返回主菜单\n' read -r -p '请选择:' c || return case "$c" in 1) install_caddy; pause;; 2) if has caddy; then caddy version; else warn '未安装 Caddy'; fi; pause;; 3) service_status caddy; pause;; 4) service_restart caddy; pause;; 5) if has caddy; then as_root caddy validate --config /etc/caddy/Caddyfile; else warn '未安装 Caddy'; fi; pause;; 0) return;; *) warn '无效选项'; pause;; esac done } speedtest_menu(){ local c while true; do printf '\n========== Speedtest 管理 ==========\n1. 安装 Speedtest\n2. 开始测速\n3. 查看版本\n0. 返回主菜单\n' read -r -p '请选择:' c || return case "$c" in 1) install_speedtest; pause;; 2) if has speedtest; then speedtest; else warn '未安装 Speedtest'; fi; pause;; 3) if has speedtest; then speedtest --version; else warn '未安装 Speedtest'; fi; pause;; 0) return;; *) warn '无效选项'; pause;; esac done } komari_menu(){ local c while true; do printf '\n========== Komari Agent 管理 ==========\n1. 安装 Komari Agent\n2. 查看运行状态\n3. 重启 Komari Agent\n4. 查看日志\n0. 返回主菜单\n' read -r -p '请选择:' c || return case "$c" in 1) install_komari; pause;; 2) service_status komari-agent; pause;; 3) service_restart komari-agent; pause;; 4) show_logs komari-agent; pause;; 0) return;; *) warn '无效选项'; pause;; esac done } main(){ check_system check_dependencies || exit 1 local c while true; do printf '\nOOBO v%s\n==================================================\n系统:%s\n服务管理:%s\n\n' "$OOBO_VERSION" "$OS_NAME" "$INIT" if [[ "$OS_ID" == alpine ]]; then printf ' 1. sing-box 管理 [%s]\n' "$(status_word singbox_installed)" printf ' 2. Komari Agent 管理 [%s]\n' "$(status_word komari_installed)" printf ' 0. 退出\n\n' read -r -p '请选择 [0-2]:' c || break case "$c" in 1) singbox_menu;; 2) komari_menu;; 0) break;; *) warn '无效选项';; esac else printf ' 1. Docker 管理 [Docker %s / Compose %s]\n' "$(status_word docker_installed)" "$(status_word compose_installed)" printf ' 2. sing-box 管理 [%s]\n' "$(status_word singbox_installed)" printf ' 3. Caddy 管理 [%s]\n' "$(status_word caddy_installed)" printf ' 4. Ookla Speedtest 管理 [%s]\n' "$(status_word speedtest_installed)" printf ' 5. Komari Agent 管理 [%s]\n' "$(status_word komari_installed)" printf ' 0. 退出\n\n' read -r -p '请选择 [0-5]:' c || break case "$c" in 1) docker_menu;; 2) singbox_menu;; 3) caddy_menu;; 4) speedtest_menu;; 5) komari_menu;; 0) break;; *) warn '无效选项';; esac fi done info '已退出 OOBO' } main "$@"