#!/usr/bin/env bash set -euo pipefail # 剧核工厂 Linux 一键部署脚本。 # 负责安装 Nginx/PostgreSQL/Redis/FFmpeg、执行迁移、创建 systemd 服务, # 并按需使用本地 API/acme/acme.sh 申请或安装 HTTPS 证书。 RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'; RESET='\033[0m' info(){ echo -e "${CYAN} -> $*${RESET}"; } ok(){ echo -e "${GREEN} OK $*${RESET}"; } warn(){ echo -e "${YELLOW} !! $*${RESET}"; } fail(){ echo -e "${RED} ERR $*${RESET}" >&2; exit 1; } SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" API_ROOT="$(realpath "${1:-$SCRIPT_DIR/..}")" PROJECT_ROOT="$(realpath "$API_ROOT/..")" CONFIG_FILE="$SCRIPT_DIR/.deploy.conf" SYSTEM_ENV_FILE="/etc/juyou_ai/juyou_ai.env" [ -f "$SCRIPT_DIR/prepare_server.sh" ] || fail "缺少服务器初始化脚本。" chmod +x "$API_ROOT/bin/jcf-api" 2>/dev/null || true [ -x "$API_ROOT/bin/jcf-api" ] || fail "缺少 Linux API 二进制 $API_ROOT/bin/jcf-api,请先执行 push.bat。" [ -f "$API_ROOT/static/web/index.html" ] || fail "缺少 WEB 构建产物。" [ -f "$API_ROOT/static/admin/index.html" ] || fail "缺少 ADMIN 构建产物。" if [ -s "$CONFIG_FILE" ]; then # shellcheck disable=SC1090 source "$CONFIG_FILE" fi SERVICE_NAME="juyou_ai" APP_PORT="8123" DOMAIN_INPUT="${SAVED_DOMAIN_INPUT:-_}" USER_EMAIL="${SAVED_USER_EMAIL:-}" DOMAINS=() if [ "$DOMAIN_INPUT" != "_" ]; then for domain in $DOMAIN_INPUT; do [[ "$domain" =~ ^([A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?\.)+[A-Za-z]{2,63}$ ]] || fail "域名格式无效:$domain" DOMAINS+=("$domain") done [ "${#DOMAINS[@]}" -gt 0 ] || fail "至少需要一个有效域名。" DOMAIN_INPUT="${DOMAINS[*]}" FIRST_DOMAIN="${DOMAINS[0]}" fi FIRST_DOMAIN="${FIRST_DOMAIN:-_}" command -v sudo >/dev/null 2>&1 || fail "系统缺少 sudo。" sudo env SAVED_DOMAIN_INPUT="$DOMAIN_INPUT" JCF_ENV_SOURCE="${JCF_ENV_SOURCE:-}" bash "$SCRIPT_DIR/prepare_server.sh" "$API_ROOT" [ -f "$SYSTEM_ENV_FILE" ] || fail "服务器初始化后仍缺少 $SYSTEM_ENV_FILE。" SOCKET_PATH="/run/$SERVICE_NAME/app.sock" CERT_DIR="/etc/nginx/ssl" NGINX_FILE="/etc/nginx/conf.d/$SERVICE_NAME.conf" SERVICE_FILE="/etc/systemd/system/$SERVICE_NAME.service" LOG_DIR="/var/log/$SERVICE_NAME" LOG_FILE="$LOG_DIR/$SERVICE_NAME.log" LOGROTATE_FILE="/etc/logrotate.d/$SERVICE_NAME" # 只读取迁移所需的数据库变量,不直接 source .env。 # APP_NAME 等值可能包含空格,直接 source 会被 Bash 误解析。 env_value() { local key="$1" value value="$(sudo sed -n -E "s/^${key}=//p" "$SYSTEM_ENV_FILE" | head -n 1)" value="${value#\"}"; value="${value%\"}" printf '%s' "$value" } DATABASE_URL="$(env_value DATABASE_URL)" DATABASE_NAME="$(env_value DATABASE_NAME)" DATABASE_HOST="$(env_value DATABASE_HOST)" DATABASE_PORT="$(env_value DATABASE_PORT)" DATABASE_USER="$(env_value DATABASE_USER)" DATABASE_PASSWORD="$(env_value DATABASE_PASSWORD)" PSQL_ARGS=() if [ -n "${DATABASE_URL:-}" ]; then PSQL_TARGET="$DATABASE_URL" else PSQL_TARGET="dbname=${DATABASE_NAME:-juchuang_factory} host=${DATABASE_HOST:-127.0.0.1} port=${DATABASE_PORT:-5432} user=${DATABASE_USER:-postgres}" export PGPASSWORD="${DATABASE_PASSWORD:-}" fi # 使用独立记录表避免重复执行同一个 SQL 迁移。 psql "$PSQL_TARGET" -v ON_ERROR_STOP=1 -c 'CREATE TABLE IF NOT EXISTS jcf_schema_migrations (version varchar(128) PRIMARY KEY, applied_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP);' >/dev/null for migration in "$API_ROOT"/migrations/*.up.sql; do [ -f "$migration" ] || continue version="$(basename "$migration")" already="$(psql "$PSQL_TARGET" -At -v ON_ERROR_STOP=1 -c "SELECT 1 FROM jcf_schema_migrations WHERE version='${version//\'/\'\'}' LIMIT 1")" if [ "$already" = "1" ]; then info "跳过已执行迁移:$version" continue fi info "执行数据库迁移:$version" psql "$PSQL_TARGET" -v ON_ERROR_STOP=1 -f "$migration" >/dev/null psql "$PSQL_TARGET" -v ON_ERROR_STOP=1 -c "INSERT INTO jcf_schema_migrations(version) VALUES ('${version//\'/\'\'}')" >/dev/null done unset PGPASSWORD sudo chown -R www-data:www-data "$API_ROOT" sudo rm -f "$API_ROOT/.env" sudo mkdir -p "$(dirname "$SOCKET_PATH")" "$CERT_DIR" /var/www/jcf-acme sudo install -d -m 0750 -o www-data -g www-data "$LOG_DIR" sudo touch "$LOG_FILE" sudo chown www-data:www-data "$LOG_FILE" sudo chmod 0640 "$LOG_FILE" # 每天轮转 API 与 Worker 的统一日志,并在单个文件过大时提前轮转。 sudo tee "$LOGROTATE_FILE" >/dev/null </dev/null </dev/null </dev/null </dev/null <