#!/usr/bin/env bash set -euo pipefail GH_REPO="${GH_REPO:-JetSprow/J-Board-Lite}" BRANCH="${BRANCH:-main}" APP_DIR="${APP_DIR:-}" REWRITE_ENV="${REWRITE_ENV:-}" SKIP_DOCKER_INSTALL="${SKIP_DOCKER_INSTALL:-0}" APP_PORT="" PUBLIC_URL="" SUBSCRIPTION_PUBLIC_URL="" SITE_NAME="" ADMIN_EMAIL="" ADMIN_PASSWORD="" ADMIN_NAME="" NEXTAUTH_SECRET="" ENCRYPTION_KEY="" ENV_REUSED="0" is_interactive() { [ -r /dev/tty ] && [ -w /dev/tty ] } prompt_print() { if is_interactive; then printf '%b' "$*" > /dev/tty else printf '%b' "$*" >&2 fi } prompt_read() { local __var="$1" if is_interactive; then IFS= read -r "$__var" < /dev/tty || true else return 1 fi } run_as_root() { if [ "$(id -u)" -eq 0 ]; then "$@" elif command -v sudo >/dev/null 2>&1; then sudo "$@" else echo "需要 root 权限。请使用 root 用户运行,或先安装 sudo。" >&2 exit 1 fi } line() { printf '%s\n' "------------------------------------------------------------" } section() { echo line printf '%s\n' "$1" line } need_command() { command -v "$1" >/dev/null 2>&1 } install_base_packages() { local missing=() for cmd in curl git openssl; do if ! need_command "$cmd"; then missing+=("$cmd") fi done if [ "${#missing[@]}" -eq 0 ]; then return fi section "安装基础依赖:${missing[*]}" if need_command apt-get; then run_as_root apt-get update run_as_root apt-get install -y ca-certificates curl git openssl elif need_command dnf; then run_as_root dnf install -y ca-certificates curl git openssl elif need_command yum; then run_as_root yum install -y ca-certificates curl git openssl elif need_command apk; then run_as_root apk add --no-cache ca-certificates curl git openssl else echo "无法识别包管理器,请先手动安装:curl git openssl" >&2 exit 1 fi } install_docker() { if [ "$SKIP_DOCKER_INSTALL" = "1" ]; then return fi if need_command docker && run_as_root docker compose version >/dev/null 2>&1; then return fi section "安装 Docker 与 Compose 插件" curl -fsSL https://get.docker.com -o /tmp/get-docker.sh run_as_root sh /tmp/get-docker.sh rm -f /tmp/get-docker.sh if need_command systemctl; then run_as_root systemctl enable --now docker || true elif need_command service; then run_as_root service docker start || true fi if ! run_as_root docker compose version >/dev/null 2>&1; then echo "Docker Compose 插件安装后仍不可用,请检查 Docker 安装状态。" >&2 exit 1 fi } random_hex() { openssl rand -hex "$1" } random_password() { openssl rand -hex 12 } server_ip() { curl -fsS --max-time 3 https://api.ipify.org 2>/dev/null || hostname -I 2>/dev/null | awk '{print $1}' || echo "127.0.0.1" } normalize_url() { local value="$1" value="${value%/}" if [ -z "$value" ]; then printf '%s' "$value" return fi case "$value" in http://*|https://*) printf '%s' "$value" ;; *) printf 'https://%s' "$value" ;; esac } prompt_value() { local label="$1" local default="$2" local help="${3:-}" local value="" if [ -n "$help" ]; then prompt_print "$help\n" fi if is_interactive; then prompt_print "$label [$default]: " prompt_read value || true fi if [ -z "$value" ]; then value="$default" fi printf '%s' "$value" } prompt_generated() { local label="$1" local default="$2" local help="${3:-}" local value="" if [ -n "$help" ]; then prompt_print "$help\n" fi if is_interactive; then prompt_print "$label [回车自动生成]: " prompt_read value || true fi if [ -z "$value" ]; then value="$default" fi printf '%s' "$value" } prompt_yes_no() { local label="$1" local default="$2" local value="" if ! is_interactive; then printf '%s' "$default" return fi prompt_print "$label [$default]: " prompt_read value || true value="${value:-$default}" case "$value" in y|Y|yes|YES|Yes) printf 'y' ;; *) printf 'n' ;; esac } resolve_default_app_dir() { local source="${BASH_SOURCE[0]:-}" local dir="" if [ -n "$source" ] && [ -f "$source" ]; then dir="$(cd -- "$(dirname -- "$source")" && pwd)" if [ -f "$dir/../package.json" ]; then cd -- "$dir/.." && pwd return fi fi echo "/opt/jboard" } git_in_repo() { if [ -w "$APP_DIR/.git" ] || [ -w "$APP_DIR" ]; then git -c safe.directory="$APP_DIR" "$@" else run_as_root git -c safe.directory="$APP_DIR" "$@" fi } load_resource_helpers() { local helper="$APP_DIR/scripts/lib-resource-profile.sh" if [ -f "$helper" ]; then # shellcheck disable=SC1090 . "$helper" else echo "未找到资源检测脚本:$helper,将使用 Docker 默认构建策略。" fi } prepare_repo() { section "准备 J-Board Lite 代码" local default_dir default_dir="$(resolve_default_app_dir)" APP_DIR="$(prompt_value "安装目录" "${APP_DIR:-$default_dir}" "如果你已经在仓库目录里运行脚本,直接回车即可。")" if [ -d "$APP_DIR/.git" ]; then echo "检测到已有仓库:$APP_DIR" cd "$APP_DIR" git_in_repo fetch origin "$BRANCH" git_in_repo checkout "$BRANCH" git_in_repo pull --ff-only origin "$BRANCH" elif [ -e "$APP_DIR" ] && [ "$(find "$APP_DIR" -mindepth 1 -maxdepth 1 2>/dev/null | wc -l | tr -d ' ')" != "0" ]; then echo "安装目录已存在且不为空:$APP_DIR" >&2 echo "请换一个目录,或设置 APP_DIR 指向空目录/已有 J-Board Lite 仓库。" >&2 exit 1 else run_as_root mkdir -p "$(dirname "$APP_DIR")" run_as_root git clone --branch "$BRANCH" "https://github.com/${GH_REPO}.git" "$APP_DIR" cd "$APP_DIR" fi } load_existing_env() { if [ -f .env ]; then if [ -r .env ]; then set -a # shellcheck disable=SC1091 . ./.env set +a else echo ".env 存在但当前用户不可读取;如需显示管理员密码,请使用 root 重新运行或查看 .env。" fi APP_PORT="${APP_PORT:-3000}" PUBLIC_URL="${NEXTAUTH_URL:-}" SUBSCRIPTION_PUBLIC_URL="${SUBSCRIPTION_URL:-}" SITE_NAME="${SITE_NAME:-J-Board Lite}" ADMIN_EMAIL="${ADMIN_EMAIL:-admin@jboard.local}" ADMIN_PASSWORD="${ADMIN_PASSWORD:-}" ADMIN_NAME="${ADMIN_NAME:-Admin}" fi } env_escape() { printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g; s/\$/\\$/g' } write_env() { local tmp tmp="$(mktemp)" { printf '# J-Board Lite Docker panel\n' printf 'APP_PORT="%s"\n' "$(env_escape "$APP_PORT")" printf 'SITE_NAME="%s"\n' "$(env_escape "$SITE_NAME")" printf '\n# SQLite for local tools and Docker\n' printf 'DATABASE_URL="file:./storage/jboard.db"\n' printf '\n# NextAuth\n' printf 'NEXTAUTH_SECRET="%s"\n' "$(env_escape "$NEXTAUTH_SECRET")" printf 'NEXTAUTH_URL="%s"\n' "$(env_escape "$PUBLIC_URL")" printf 'SUBSCRIPTION_URL="%s"\n' "$(env_escape "$SUBSCRIPTION_PUBLIC_URL")" printf '\n# Must be at least 32 bytes, used for AES-256-GCM encryption\n' printf 'ENCRYPTION_KEY="%s"\n' "$(env_escape "$ENCRYPTION_KEY")" printf '\n# Initial admin account, used by npm run db:seed on first install\n' printf 'ADMIN_EMAIL="%s"\n' "$(env_escape "$ADMIN_EMAIL")" printf 'ADMIN_PASSWORD="%s"\n' "$(env_escape "$ADMIN_PASSWORD")" printf 'ADMIN_NAME="%s"\n' "$(env_escape "$ADMIN_NAME")" } > "$tmp" if [ -f .env ]; then run_as_root cp .env ".env.backup.$(date +%Y%m%d%H%M%S)" fi run_as_root install -m 0600 "$tmp" .env rm -f "$tmp" } install_jetboard_command() { section "安装 JetBoard 管理命令" if [ ! -f "$APP_DIR/scripts/jetboard.sh" ]; then echo "未找到 $APP_DIR/scripts/jetboard.sh,跳过管理命令安装。" return fi local config_tmp wrapper_tmp config_tmp="$(mktemp)" wrapper_tmp="$(mktemp)" { printf '# JetBoard Docker command configuration\n' printf 'JBOARD_DEPLOY_MODE="docker"\n' printf 'JBOARD_APP_DIR="%s"\n' "$(env_escape "$APP_DIR")" printf 'GH_REPO="%s"\n' "$(env_escape "$GH_REPO")" printf 'BRANCH="%s"\n' "$(env_escape "$BRANCH")" printf 'COMPOSE="docker compose"\n' } > "$config_tmp" { printf '#!/usr/bin/env bash\n' printf 'set -euo pipefail\n' printf 'CONFIG_FILE="${JETBOARD_CONFIG:-/etc/jetboard.conf}"\n' printf 'if [ -r "$CONFIG_FILE" ]; then\n' printf ' # shellcheck disable=SC1090\n' printf ' . "$CONFIG_FILE"\n' printf 'fi\n' printf 'APP_DIR="${JBOARD_APP_DIR:-/opt/jboard}"\n' printf 'exec "$APP_DIR/scripts/jetboard.sh" "$@"\n' } > "$wrapper_tmp" run_as_root mkdir -p /usr/local/bin run_as_root install -m 0644 "$config_tmp" /etc/jetboard.conf run_as_root install -m 0755 "$wrapper_tmp" /usr/local/bin/jetboard run_as_root ln -sf /usr/local/bin/jetboard /usr/local/bin/JetBoard || true run_as_root chmod +x "$APP_DIR/scripts/jetboard.sh" || true rm -f "$config_tmp" "$wrapper_tmp" echo "已安装:jetboard(也可使用 JetBoard)" } configure_env() { section "生成 .env 配置" if [ -f .env ] && [ -z "$REWRITE_ENV" ]; then local answer answer="$(prompt_yes_no "检测到已有 .env,是否重新生成" "n")" if [ "$answer" = "n" ]; then ENV_REUSED="1" load_existing_env echo "沿用现有 .env。" return fi elif [ "${REWRITE_ENV:-0}" = "0" ] && [ -f .env ]; then ENV_REUSED="1" load_existing_env echo "沿用现有 .env。" return fi local ip default_url ip="$(server_ip)" default_url="http://${ip}:3000" SITE_NAME="$(prompt_value "站点名称" "J-Board Lite")" PUBLIC_URL="$(prompt_value "网站访问地址" "$default_url" "这里请填写你准备反向代理到本机 3000 端口的面板域名,例如 https://panel.example.com。没有域名时可先回车用 IP:3000 测试。")" PUBLIC_URL="$(normalize_url "$PUBLIC_URL")" SUBSCRIPTION_PUBLIC_URL="$(prompt_value "订阅访问地址" "$PUBLIC_URL" "用于生成客户端订阅链接。可以和网站地址相同,也可以填单独反代到本面板的订阅域名,例如 https://sub.example.com。")" SUBSCRIPTION_PUBLIC_URL="$(normalize_url "$SUBSCRIPTION_PUBLIC_URL")" APP_PORT="$(prompt_value "本机监听端口" "3000" "反向代理目标会是 http://127.0.0.1:端口,默认 3000。")" ADMIN_EMAIL="$(prompt_value "管理员邮箱" "admin@jboard.local")" ADMIN_PASSWORD="$(prompt_generated "管理员密码" "$(random_password)" "回车会生成一个安全密码,部署完成后会在结果中显示一次。")" ADMIN_NAME="$(prompt_value "管理员昵称" "Admin")" NEXTAUTH_SECRET="$(prompt_generated "NEXTAUTH_SECRET" "$(random_hex 32)")" ENCRYPTION_KEY="$(prompt_generated "ENCRYPTION_KEY" "$(random_hex 32)" "生产使用后不要更换 ENCRYPTION_KEY,否则已加密的面板密码、Token、凭据会无法解密。")" write_env echo ".env 已写入:$APP_DIR/.env" } docker_compose() { local env_args=() if [ -n "${COMPOSE_PARALLEL_LIMIT:-}" ]; then env_args+=("COMPOSE_PARALLEL_LIMIT=$COMPOSE_PARALLEL_LIMIT") fi if [ "$(id -u)" -eq 0 ]; then env "${env_args[@]}" docker compose "$@" elif command -v sudo >/dev/null 2>&1; then sudo env "${env_args[@]}" docker compose "$@" else echo "需要 root 权限。请使用 root 用户运行,或先安装 sudo。" >&2 exit 1 fi } start_panel() { section "构建并启动面板" if command -v jboard_prepare_docker_build_env >/dev/null 2>&1; then jboard_prepare_docker_build_env jboard_print_build_profile if jboard_is_low_resource_build; then docker_compose build "${JBOARD_DOCKER_BUILD_ARGS[@]}" init docker_compose build "${JBOARD_DOCKER_BUILD_ARGS[@]}" app else docker_compose build "${JBOARD_DOCKER_BUILD_ARGS[@]}" init app fi else docker_compose build init app fi docker_compose --profile setup run --rm init docker_compose up -d app } wait_for_app() { section "检查启动状态" local url="http://127.0.0.1:${APP_PORT:-3000}/api/public/app-info" local ok="0" for _ in $(seq 1 30); do if curl -fsS "$url" >/dev/null 2>&1; then ok="1" break fi sleep 2 done docker_compose ps echo if [ "$ok" = "1" ]; then echo "健康检查通过:$url" else echo "健康检查暂未通过,请查看日志:docker compose logs -f app" fi } print_summary() { local proxy_target="http://127.0.0.1:${APP_PORT:-3000}" local shown_password="$ADMIN_PASSWORD" if [ "$ENV_REUSED" = "1" ] && [ -z "$shown_password" ]; then shown_password="沿用已有数据库账号;如忘记可执行 jetboard reset" fi echo printf '%s\n' "============================================================" printf '%s\n' "J-Board Lite Docker 部署完成" printf '%s\n' "============================================================" printf '访问地址:%s\n' "${PUBLIC_URL:-http://127.0.0.1:${APP_PORT:-3000}}" printf '订阅地址:%s\n' "${SUBSCRIPTION_PUBLIC_URL:-${PUBLIC_URL:-http://127.0.0.1:${APP_PORT:-3000}}}" printf '反代目标:%s\n' "$proxy_target" printf '管理员邮箱:%s\n' "${ADMIN_EMAIL:-admin@jboard.local}" printf '管理员密码:%s\n' "$shown_password" echo printf '%s\n' "反向代理提示" printf ' 将你的面板域名解析到这台服务器,并把 Web 反向代理到 %s。\n' "$proxy_target" printf ' 如果使用独立订阅域名,也把它反向代理到同一个目标,并在后台系统设置中填写订阅 URL。\n' echo printf '%s\n' "可以展示给用户的入口" printf ' 首页 / 登录:%s/login\n' "${PUBLIC_URL%/}" printf ' 注册入口:%s/register\n' "${PUBLIC_URL%/}" printf ' 套餐商店:%s/store\n' "${PUBLIC_URL%/}" printf ' 用户中心:%s/dashboard\n' "${PUBLIC_URL%/}" echo printf '%s\n' "上线前建议完成" printf ' 1. 后台 /admin/settings:确认网站 URL、订阅 URL、SMTP 邮件服务、注册策略。\n' printf ' 2. 后台 /admin/payments:配置并启用支付方式。\n' printf ' 3. 后台 /admin/nodes:添加 3x-ui 节点并同步入站。\n' printf ' 4. 后台 /admin/plans:创建套餐并绑定入站或流媒体服务。\n' echo printf '%s\n' "常用命令" printf ' jetboard # 打开 JetBoard 管理菜单\n' printf ' jetboard status # 查看 Docker 服务状态\n' printf ' jetboard update # 拉取代码、备份数据库并按机器配置重建\n' printf ' jetboard reset # 重置或创建管理员账号密码\n' printf ' jetboard logs # 查看 app 日志\n' printf ' jetboard uninstall # 完整卸载 Docker 部署\n' printf '%s\n' "============================================================" } main() { section "J-Board Lite Docker 一键部署向导" echo "这个脚本会安装 Docker、准备配置、初始化数据库并启动面板。" echo "适合全新 Linux 服务器;已有环境会尽量保留现有 .env。" install_base_packages prepare_repo load_resource_helpers configure_env install_jetboard_command install_docker start_panel wait_for_app print_summary } main "$@"