#!/bin/bash # EasyAI 一键部署脚本 # 支持交互式问答配置,兼容 IP 与域名两种访问方式 # 一行命令: git clone --depth 1 https://git.51easyai.com/wangbo/easyai.git && cd easyai && chmod +x ./start.sh && ./start.sh set -e # 仅配置模式(验证用):DEPLOY_DRY_RUN=1 只生成配置文件,不执行 Docker 安装和启动 DEPLOY_DRY_RUN="${DEPLOY_DRY_RUN:-0}" # ==================== 项目初始化 ==================== init_project_dir() { local script_source script_source="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)" if [ -f "${script_source}/docker-compose.yml" ]; then echo "📁 项目目录: ${script_source}" cd "$script_source" return 0 fi echo "❌ 未找到 docker-compose.yml,请在 easyai 项目目录下运行 start.sh" echo " 启动命令: git clone --depth 1 https://git.51easyai.com/wangbo/easyai.git && cd easyai && chmod +x ./start.sh && ./start.sh" exit 1 } # ==================== 配置变量(支持环境变量非交互模式) ==================== DEPLOY_ACCESS="${DEPLOY_ACCESS:-}" # ip | domain DEPLOY_MODE="" # ip | domain | skip DEPLOY_IP="${DEPLOY_IP:-}" DEPLOY_DOMAIN="${DEPLOY_DOMAIN:-}" DEPLOY_HTTPS_ENV="${DEPLOY_HTTPS_INPUT:-${DEPLOY_HTTPS:-}}" DEPLOY_HTTPS=false SECURITY_ENV_MODE="upgrade" PUBLIC_API_BASE_URL_CANDIDATE="" is_non_interactive() { [ "${DEPLOY_NON_INTERACTIVE:-0}" = "1" ] || [ "${CI:-}" = "true" ] || [ ! -t 0 ] } require_interactive() { local missing_hint=$1 if is_non_interactive; then echo "❌ 当前为非交互环境,无法继续问答配置" echo " 请通过环境变量传入: ${missing_hint}" exit 1 fi } normalize_bool() { local value value="$(printf '%s' "$1" | tr '[:upper:]' '[:lower:]')" case "$value" in 1|true|yes|y|on) echo "true" ;; 0|false|no|n|off|"") echo "false" ;; *) echo "❌ 布尔变量值不合法: $1(支持 true/false/1/0/yes/no/y/n/on/off)" >&2 return 1 ;; esac } set_deploy_https_from_env_or_default() { local normalized if ! normalized="$(normalize_bool "${DEPLOY_HTTPS_ENV:-false}")"; then exit 1 fi DEPLOY_HTTPS="$normalized" } infer_deploy_access_from_env() { if [ -z "$DEPLOY_ACCESS" ]; then if [ -n "$DEPLOY_DOMAIN" ]; then DEPLOY_ACCESS="domain" elif [ -n "$DEPLOY_IP" ]; then DEPLOY_ACCESS="ip" fi fi } sanitize_domain() { local domain="$1" domain="${domain#http://}" domain="${domain#https://}" domain="${domain%%/*}" domain="${domain%;}" echo "$domain" } validate_domain() { local domain="$1" # 基础域名校验:仅允许字母、数字、连字符和点;必须包含至少一个点;不能以点或连字符开头结尾 if [[ ! "$domain" =~ ^[A-Za-z0-9.-]+$ ]]; then return 1 fi if [[ "$domain" != *.* ]]; then return 1 fi if [[ "$domain" =~ ^[.-] || "$domain" =~ [.-]$ ]]; then return 1 fi if [[ "$domain" == *".."* ]]; then return 1 fi return 0 } prompt_or_env() { local var_name=$1 local prompt_text=$2 local env_name=$3 local default=$4 if [ -n "${!env_name}" ]; then eval "$var_name=\"${!env_name}\"" return fi require_interactive "$env_name" if [ -n "$default" ]; then read -r -p "${prompt_text} [$default]: " input eval "$var_name=\"${input:-$default}\"" else read -r -p "${prompt_text}: " input eval "$var_name=\"$input\"" fi } run_deploy_questions() { echo "" echo "================================" echo " EasyAI 部署配置(问答模式)" echo "================================" echo "" infer_deploy_access_from_env # 非交互模式:环境变量已完整设置则直接使用(CI/自动化部署) if [ -n "$DEPLOY_ACCESS" ]; then if [ "$DEPLOY_ACCESS" = "ip" ] && [ -n "$DEPLOY_IP" ]; then DEPLOY_MODE="ip" echo "使用环境变量: IP 模式, IP=$DEPLOY_IP" return fi if [ "$DEPLOY_ACCESS" = "domain" ] && [ -n "$DEPLOY_DOMAIN" ]; then DEPLOY_DOMAIN="$(sanitize_domain "$DEPLOY_DOMAIN")" if ! validate_domain "$DEPLOY_DOMAIN"; then echo "❌ 环境变量 DEPLOY_DOMAIN 格式不合法: ${DEPLOY_DOMAIN}" exit 1 fi DEPLOY_MODE="domain" set_deploy_https_from_env_or_default echo "使用环境变量: 域名模式, 域名=$DEPLOY_DOMAIN" return fi if [ "$DEPLOY_ACCESS" != "ip" ] && [ "$DEPLOY_ACCESS" != "domain" ]; then echo "❌ DEPLOY_ACCESS 只能是 ip 或 domain,当前为: $DEPLOY_ACCESS" exit 1 fi fi # 1. IP 或域名访问 if [ -z "$DEPLOY_ACCESS" ]; then require_interactive "DEPLOY_ACCESS=ip DEPLOY_IP=<服务器IP> 或 DEPLOY_ACCESS=domain DEPLOY_DOMAIN=<域名>" echo "1. 通过 IP 地址还是域名访问?" echo " [1] IP 地址(需开放 3001、3002、3003 端口)" echo " [2] 域名" read -r -p "请选择 [1/2]: " choice case "$choice" in 1) DEPLOY_MODE="ip" ;; 2) DEPLOY_MODE="domain" ;; *) echo "❌ 无效选择"; exit 1 ;; esac else DEPLOY_MODE="$DEPLOY_ACCESS" fi if [ "$DEPLOY_MODE" = "ip" ]; then # 2. 输入服务器 IP prompt_or_env DEPLOY_IP "2. 请输入服务器 IP 地址" "DEPLOY_IP" "" if [ -z "$DEPLOY_IP" ]; then echo "❌ IP 不能为空" exit 1 fi echo " 请确保防火墙已放行 3001、3002、3003 端口" else # 3. 输入域名 prompt_or_env DEPLOY_DOMAIN "3. 请输入域名(不含 https:// 前缀,如 51easyai.com)" "DEPLOY_DOMAIN" "" DEPLOY_DOMAIN="$(sanitize_domain "$DEPLOY_DOMAIN")" if [ -z "$DEPLOY_DOMAIN" ]; then echo "❌ 域名不能为空" exit 1 fi if ! validate_domain "$DEPLOY_DOMAIN"; then echo "❌ 域名格式不合法: ${DEPLOY_DOMAIN}" echo " 请输入类似 51easyai.com 的域名,不要包含协议、路径或分号" exit 1 fi # 3.1 是否启用 HTTPS if [ -n "$DEPLOY_HTTPS_ENV" ]; then set_deploy_https_from_env_or_default else require_interactive "DEPLOY_HTTPS=true 或 DEPLOY_HTTPS=false" read -r -p "3.1 是否签名 HTTPS 证书?原来已经有 HTTPS 证书这里选择 N,第一次部署或者原来没有证书选 Y [y/N]: " https_choice DEPLOY_HTTPS=false if [[ "$https_choice" =~ ^[yY] ]]; then DEPLOY_HTTPS=true fi fi if [ "$DEPLOY_HTTPS" = true ]; then echo " 启用 HTTPS 需确保防火墙已放行 80、443 端口" fi fi } # ==================== 生成配置文件 ==================== setup_env_files() { echo "" echo "📝 配置环境文件..." # 6. 复制 .env.tools、.env.ASG、.env.AMS(无 example 后缀的从 .sample 生成) if [ ! -f .env.tools ]; then cp .env.tools.sample .env.tools echo " ✓ .env.tools" fi if [ ! -f .env.ASG ]; then cp .env.ASG.sample .env.ASG echo " ✓ .env.ASG" fi if [ ! -f .env.AMS ]; then cp .env.AMS.sample .env.AMS echo " ✓ .env.AMS" fi # 4/5. 配置 .env if [ ! -f .env ]; then cp .env.sample .env SECURITY_ENV_MODE="new" fi local web_port server_port public_scheme socket_scheme web_port="$(awk -F= '$1 == "WEB_PORT" { print $2; exit }' .env | tr -d '[:space:]')" web_port="${web_port:-3010}" server_port="$(awk -F= '$1 == "SERVER_HTTP_PORT" { print $2; exit }' .env | tr -d '[:space:]')" server_port="${server_port:-3001}" if [ "$DEPLOY_MODE" = "ip" ]; then # IP 模式 sed -i.bak "s|^NUXT_PUBLIC_BASE_APIURL=.*|NUXT_PUBLIC_BASE_APIURL=http://${DEPLOY_IP}:${server_port}|" .env sed -i.bak "s|^NUXT_PUBLIC_BASE_SOCKETURL=.*|NUXT_PUBLIC_BASE_SOCKETURL=ws://${DEPLOY_IP}:3002|" .env sed -i.bak "s|^NUXT_PUBLIC_SG_APIURL=.*|NUXT_PUBLIC_SG_APIURL=http://${DEPLOY_IP}:3003|" .env sed -i.bak "s|^CONFIG_SECURITY_ORIGIN=.*|CONFIG_SECURITY_ORIGIN=http://${DEPLOY_IP}:${web_port}|" .env PUBLIC_API_BASE_URL_CANDIDATE="${DEPLOY_PUBLIC_API_BASE_URL:-http://${DEPLOY_IP}:${server_port}}" echo " ✓ .env 已配置为 IP 模式 (${DEPLOY_IP})" else # 域名模式 public_scheme="http" socket_scheme="ws" if [ "$DEPLOY_HTTPS" = true ]; then public_scheme="https" socket_scheme="wss" fi sed -i.bak "s|^NUXT_PUBLIC_BASE_APIURL=.*|NUXT_PUBLIC_BASE_APIURL=/api|" .env sed -i.bak "s|^NUXT_PUBLIC_BASE_SOCKETURL=.*|NUXT_PUBLIC_BASE_SOCKETURL=${socket_scheme}://${DEPLOY_DOMAIN}/socket.io|" .env sed -i.bak "s|^NUXT_PUBLIC_SG_APIURL=.*|NUXT_PUBLIC_SG_APIURL=/asg-api|" .env # 域名模式同时允许同域 HTTP/HTTPS,兼容首次签证书和复用既有证书两种流程。 sed -i.bak "s|^CONFIG_SECURITY_ORIGIN=.*|CONFIG_SECURITY_ORIGIN=https://${DEPLOY_DOMAIN},http://${DEPLOY_DOMAIN}|" .env PUBLIC_API_BASE_URL_CANDIDATE="${DEPLOY_PUBLIC_API_BASE_URL:-${public_scheme}://${DEPLOY_DOMAIN}/api}" echo " ✓ .env 已配置为域名模式 (${DEPLOY_DOMAIN})" # 7. Nginx 配置(域名模式) PROXY_CONF="${DEPLOY_DOMAIN}.conf" if [ ! -f "$PROXY_CONF" ]; then local proxy_template="easyai-proxy.conf.sample" if [ ! -f "$proxy_template" ]; then echo "❌ 未找到 Nginx 配置模板: easyai-proxy.conf.sample" echo " 请确认仓库文件完整后重试" exit 1 fi sed "s/51easyai.com/${DEPLOY_DOMAIN}/g" "$proxy_template" > "$PROXY_CONF" echo " ✓ Nginx 配置已生成: $PROXY_CONF" fi fi rm -f .env.bak } # ==================== Docker 安装(复用原 start.sh 逻辑) ==================== install_docker() { echo "" echo "================================" echo " Docker 安装与检查" echo "================================" local host_os host_os="$(uname -s 2>/dev/null || echo Unknown)" if [[ "$host_os" == "Darwin" ]]; then OS_FAMILY="macOS" OS_VERSION_ID="0" OS_CODENAME="" elif [ -f /etc/os-release ]; then . /etc/os-release case "${ID:-}" in ubuntu|debian) OS_FAMILY="Ubuntu" ;; centos|rhel|fedora) OS_FAMILY="CentOS" ;; *) OS_FAMILY="${ID:-Unknown}" ;; esac OS_VERSION_ID=$(grep -oP '(?<=^VERSION_ID=")[0-9.]+' /etc/os-release 2>/dev/null | cut -d'.' -f1 || echo "0") OS_CODENAME="" if [[ "$OS_FAMILY" == "Ubuntu" ]]; then OS_CODENAME=$(lsb_release -cs 2>/dev/null || (grep VERSION_CODENAME /etc/os-release 2>/dev/null | cut -d= -f2 | tr -d '"')) fi else OS_FAMILY=$(hostnamectl 2>/dev/null | grep "Operating System" | awk '{print $3}' || echo "Unknown") OS_VERSION_ID="0" OS_CODENAME="" fi UBUNTU_DOCKER_MIRROR_URL="https://mirrors.ustc.edu.cn/docker-ce" UBUNTU_DOCKER_GPG_URL="https://mirrors.ustc.edu.cn/docker-ce/linux/ubuntu/gpg" CENTOS_DOCKER_MIRROR_URL="https://mirrors.tuna.tsinghua.edu.cn/docker-ce" check_network() { if curl -sSf --connect-timeout 5 https://download.docker.com/ &>/dev/null; then echo "✅ 网络连接正常" return 0 fi echo "⚠️ 将使用国内镜像源" return 0 } check_network install_docker_macos() { if ! command -v brew &>/dev/null; then echo "❌ 未检测到 Homebrew,无法自动安装 Docker Desktop。" echo " 请先安装 Homebrew: https://brew.sh/" return 1 fi if brew list --cask docker &>/dev/null; then echo "✅ Docker Desktop 已安装(Homebrew)" else echo "📦 正在通过 Homebrew 安装 Docker Desktop..." brew install --cask docker fi echo "🚀 正在尝试启动 Docker Desktop..." open -a Docker || true echo "⏳ 等待 Docker 引擎就绪(最多 120 秒)..." local i for i in $(seq 1 60); do if docker info &>/dev/null; then echo "✅ Docker Desktop 已启动并可用" return 0 fi sleep 2 done echo "⚠️ Docker Desktop 已安装,但尚未就绪。" echo " 请手动打开 Docker Desktop,待状态正常后重新运行 ./start.sh" return 1 } if command -v docker &>/dev/null; then echo "✅ Docker 已安装" else if [[ "$OS_FAMILY" == "Ubuntu" ]]; then sudo apt update -y sudo apt install -y apt-transport-https ca-certificates curl gnupg lsb-release sudo install -m 0755 -d /etc/apt/keyrings curl -fsSL "$UBUNTU_DOCKER_GPG_URL" | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg 2>/dev/null || \ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg sudo chmod a+r /etc/apt/keyrings/docker.gpg echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] $UBUNTU_DOCKER_MIRROR_URL/linux/ubuntu $OS_CODENAME stable" | sudo tee /etc/apt/sources.list.d/docker.list >/dev/null sudo apt-get update -y sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin elif [[ "$OS_FAMILY" == "CentOS" ]]; then sudo yum install -y yum-utils sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo sudo sed -i "s+https://download.docker.com+$CENTOS_DOCKER_MIRROR_URL+g" /etc/yum.repos.d/docker-ce.repo [[ "$OS_VERSION_ID" -ge "8" ]] && sudo yum module disable -y container-tools 2>/dev/null || true sudo yum install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin elif [[ "$OS_FAMILY" == "macOS" ]]; then echo "⚠️ 检测到 macOS,未找到 Docker。" mac_install_choice="${DEPLOY_MAC_DOCKER_INSTALL:-${DEPLOY_DOCKER_INSTALL:-}}" if [ -z "$mac_install_choice" ]; then require_interactive "DEPLOY_MAC_DOCKER_INSTALL=auto 或 DEPLOY_MAC_DOCKER_INSTALL=manual" echo "请选择安装方式:" echo " [1] 自动安装 Docker Desktop(需要 Homebrew)" echo " [2] 手动安装(默认)" read -r -p "请选择 [1/2]: " mac_install_choice fi mac_install_choice="$(printf '%s' "$mac_install_choice" | tr '[:upper:]' '[:lower:]')" case "${mac_install_choice:-2}" in 1|auto) if ! install_docker_macos; then exit 1 fi ;; 2|manual) echo " 请手动安装 Docker Desktop: https://www.docker.com/products/docker-desktop/" echo " 安装并启动 Docker Desktop 后,重新运行 ./start.sh" exit 1 ;; *) echo "❌ 无效选择" exit 1 ;; esac else echo "❌ 不支持的操作系统: $OS_FAMILY" exit 1 fi if [[ "$OS_FAMILY" != "macOS" ]]; then sudo systemctl enable docker sudo systemctl start docker getent group docker | grep -qw "$USER" || sudo usermod -aG docker "$USER" fi fi if docker compose version &>/dev/null; then echo "✅ Docker Compose 已就绪" elif command -v docker-compose &>/dev/null; then echo "✅ Docker Compose (兼容模式) 已就绪" else echo "❌ 请安装 docker-compose-plugin" exit 1 fi } # ==================== 启动服务 ==================== start_services() { echo "" echo "🚀 启动 EasyAI 服务..." if docker compose version &>/dev/null; then sudo docker compose pull && sudo docker compose up -d else sudo docker-compose pull && sudo docker-compose up -d fi echo "🎉 EasyAI 应用启动成功" } # ==================== 执行 HTTPS 配置 ==================== run_https_setup() { if [ "$DEPLOY_HTTPS" = true ] && [ -n "$DEPLOY_DOMAIN" ]; then echo "" echo "🔒 执行 HTTPS 配置(请确保服务器已放行 80、443 端口)..." if [ -f "./https.sh" ]; then # https.sh 依赖 easyai-proxy.conf,需使用生成的域名配置文件 export EASYAI_PROXY_CONF="${DEPLOY_DOMAIN}.conf" bash ./https.sh else echo "⚠️ 未找到 https.sh,请手动配置 HTTPS" fi fi } # ==================== 主流程 ==================== main() { init_project_dir infer_deploy_access_from_env # 检查是否已有 .env 且非强制重新配置 if [ -f .env ] && [ -z "$DEPLOY_FORCE_RECONFIG" ] && [ -z "$DEPLOY_ACCESS" ]; then echo "📁 检测到已有 .env 配置" if is_non_interactive; then echo "⏭️ 使用现有配置继续..." DEPLOY_MODE="skip" else read -r -p "是否重新配置部署方式?[y/N]: " reconfigure if [[ ! "$reconfigure" =~ ^[yY] ]]; then echo "⏭️ 使用现有配置继续..." DEPLOY_MODE="skip" fi fi fi if [ "$DEPLOY_MODE" != "skip" ]; then run_deploy_questions fi if [ "$DEPLOY_MODE" != "skip" ]; then setup_env_files else if [ ! -f .env.tools ]; then cp .env.tools.sample .env.tools fi if [ ! -f .env.ASG ]; then cp .env.ASG.sample .env.ASG fi if [ ! -f .env.AMS ]; then cp .env.AMS.sample .env.AMS fi fi # 空值或历史默认值只在首次发现时生成并写回 .env,后续启动保持稳定。 # 数据库中的后台 JWT 轮转配置由后端优先使用,不受此处影响。 # shellcheck source=scripts/init-security-env.sh . ./scripts/init-security-env.sh init_security_env .env "$SECURITY_ENV_MODE" # 在拉取镜像或重建服务前完成公开地址迁移;非法或无法推导时立即终止。 # shellcheck source=scripts/init-public-api-base-url.sh . ./scripts/init-public-api-base-url.sh if [ "$DEPLOY_MODE" = "skip" ]; then init_public_api_base_url .env upgrade "${DEPLOY_PUBLIC_API_BASE_URL:-}" else init_public_api_base_url .env configure "$PUBLIC_API_BASE_URL_CANDIDATE" fi if [ "$DEPLOY_DRY_RUN" = "1" ]; then echo "" echo "⚠️ dry-run 模式:跳过 Docker 安装和服务启动" echo " 配置文件已生成,可直接运行 ./start.sh 完成部署" else install_docker start_services export DEPLOY_DOMAIN run_https_setup fi echo "" echo "================================" echo " 部署完成" echo "================================" if [ "$DEPLOY_MODE" = "ip" ] && [ -n "$DEPLOY_IP" ]; then echo "访问地址: http://${DEPLOY_IP}:3010" elif [ "$DEPLOY_MODE" = "domain" ] && [ -n "$DEPLOY_DOMAIN" ]; then if [ "$DEPLOY_HTTPS" = true ]; then echo "访问地址: https://${DEPLOY_DOMAIN}" else echo "访问地址: http://${DEPLOY_DOMAIN} (配置 Nginx 后)" fi if [ "$DEPLOY_HTTPS" = true ]; then echo "HTTPS 已启用" fi fi echo "" } main "$@"