#!/usr/bin/env bash set -euo pipefail # ===== 本地调试启动脚本:后端 reload + 前端热更新 ===== ROOT_DIR="$(cd "$(dirname "$0")" && pwd)" cd "$ROOT_DIR" if [ -f "$ROOT_DIR/.env" ]; then set -a # 调试模式复用 Docker 部署的环境变量,尤其是 JWT 和敏感字段加密密钥。 # shellcheck disable=SC1091 source "$ROOT_DIR/.env" set +a fi BACKEND_HOST="${BACKEND_HOST:-0.0.0.0}" BACKEND_PORT="${BACKEND_PORT:-8800}" FRONTEND_HOST="${FRONTEND_HOST:-0.0.0.0}" FRONTEND_PORT="${FRONTEND_PORT:-5174}" LOG_LEVEL="${LOG_LEVEL:-DEBUG}" MYSQL_IMAGE="${MYSQL_IMAGE:-mysql:8.4}" MYSQL_BIND_HOST="${MYSQL_BIND_HOST:-127.0.0.1}" MYSQL_HOST_PORT="${MYSQL_HOST_PORT:-3307}" MYSQL_DATABASE="${MYSQL_DATABASE:-douyu_login}" MYSQL_USER="${MYSQL_USER:-douyu_login}" DEV_DB_HOST="${DEV_DB_HOST:-127.0.0.1}" DEV_DB_PORT="${DEV_DB_PORT:-$MYSQL_HOST_PORT}" DB_HOST="$DEV_DB_HOST" DB_PORT="$DEV_DB_PORT" DB_NAME="${DB_NAME:-$MYSQL_DATABASE}" DB_USER="${DB_USER:-$MYSQL_USER}" DB_PASSWORD="${DB_PASSWORD:-${MYSQL_PASSWORD:-}}" DB_POOL_SIZE="${DB_POOL_SIZE:-20}" DB_MAX_OVERFLOW="${DB_MAX_OVERFLOW:-20}" DB_POOL_TIMEOUT="${DB_POOL_TIMEOUT:-30}" DB_POOL_RECYCLE="${DB_POOL_RECYCLE:-1800}" YYB_WORKER_PORT="${YYB_WORKER_PORT:-8810}" DEV_YYB_WORKER_URL="${DEV_YYB_WORKER_URL:-http://127.0.0.1:${YYB_WORKER_PORT}}" YYB_WORKER_DATA_DIR="${YYB_WORKER_DATA_DIR:-$ROOT_DIR/data/yyb-worker-dev}" YYB_WORKER_NODE_MODULES="$ROOT_DIR/services/yyb-worker/node_modules" LOG_DIR="$ROOT_DIR/logs" LOG_DATE="$(date +%Y-%m-%d)" BACKEND_LOG="$LOG_DIR/backend-$LOG_DATE.log" FRONTEND_LOG="$LOG_DIR/frontend-$LOG_DATE.log" WORKER_LOG="$LOG_DIR/yyb-worker-$LOG_DATE.log" BACKEND_PID="" FRONTEND_PID="" WORKER_PID="" stop_tree() { local pid="$1" local children if [ -z "$pid" ] || ! kill -0 "$pid" 2>/dev/null; then return fi children="$(pgrep -P "$pid" 2>/dev/null || true)" for child in $children; do stop_tree "$child" done kill "$pid" 2>/dev/null || true } cleanup() { trap - EXIT INT TERM stop_tree "$FRONTEND_PID" stop_tree "$BACKEND_PID" stop_tree "$WORKER_PID" } require_cmd() { if ! command -v "$1" >/dev/null 2>&1; then echo "缺少命令: $1" exit 1 fi } require_cmd uv require_cmd npm require_cmd docker detect_compose() { if docker compose version >/dev/null 2>&1; then echo "docker compose" elif command -v docker-compose >/dev/null 2>&1; then echo "docker-compose" else echo "" fi } detect_backend_proxy_host() { if [ "$BACKEND_HOST" != "0.0.0.0" ] && [ "$BACKEND_HOST" != "::" ]; then echo "$BACKEND_HOST" return fi # 绑定 0.0.0.0 时,Vite 代理不要固定连 127.0.0.1;某些本地工具会抢占 loopback 端口。 local ip ip="$(ipconfig getifaddr en0 2>/dev/null || ipconfig getifaddr en1 2>/dev/null || true)" if [ -z "$ip" ] && command -v hostname >/dev/null 2>&1; then ip="$(hostname -I 2>/dev/null | awk '{print $1}' || true)" fi echo "${ip:-127.0.0.1}" } if [ -n "${DATABASE_URL:-}" ] && [[ "$DATABASE_URL" != mysql+* ]]; then echo "dev.sh 只支持 MySQL,请移除 SQLite DATABASE_URL: $DATABASE_URL" exit 1 fi if [ -z "${MYSQL_PASSWORD:-}" ] || [ -z "${MYSQL_ROOT_PASSWORD:-}" ]; then echo "请先在 .env 中设置 MYSQL_PASSWORD 和 MYSQL_ROOT_PASSWORD" exit 1 fi if [ -z "$DB_PASSWORD" ]; then echo "请先在 .env 中设置 DB_PASSWORD 或 MYSQL_PASSWORD" exit 1 fi if [ -z "${YYB_WORKER_KEY:-}" ]; then echo "请先在 .env 中设置 YYB_WORKER_KEY" exit 1 fi COMPOSE=$(detect_compose) if [ -z "$COMPOSE" ]; then echo "缺少 docker compose" exit 1 fi BACKEND_PROXY_HOST="${BACKEND_PROXY_HOST:-$(detect_backend_proxy_host)}" BACKEND_PROXY_TARGET="${VITE_BACKEND_TARGET:-http://${BACKEND_PROXY_HOST}:${BACKEND_PORT}}" mkdir -p data "$LOG_DIR" mkdir -p "$YYB_WORKER_DATA_DIR" trap cleanup EXIT INT TERM start_yyb_worker() { if curl -fsS "${DEV_YYB_WORKER_URL}/health" >/dev/null 2>&1; then echo "本地 YYB Worker 已运行: ${DEV_YYB_WORKER_URL}(复用现有进程,如需重启请先停掉该进程)" return fi if [ ! -d "$YYB_WORKER_NODE_MODULES/jsdom" ]; then echo "正在安装本地 YYB Worker Node 依赖..." (cd "$ROOT_DIR/services/yyb-worker" && npm ci) fi echo "正在启动本地 YYB Worker..." ( cd "$ROOT_DIR/services/yyb-worker/runtime" YYB_WORKER_KEY="$YYB_WORKER_KEY" \ uv run --project "$ROOT_DIR" python scripts/yyb-worker.py \ --host 127.0.0.1 \ --port "$YYB_WORKER_PORT" \ --data-dir "$YYB_WORKER_DATA_DIR" 2>&1 | tee -a "$WORKER_LOG" ) & WORKER_PID=$! echo "等待本地 YYB Worker 就绪..." for i in $(seq 1 15); do if curl -fsS "${DEV_YYB_WORKER_URL}/health" >/dev/null 2>&1; then echo "YYB Worker 已就绪" return fi if [ "$i" = "15" ]; then echo "YYB Worker 启动超时,查看日志:" tail -n 100 "$WORKER_LOG" 2>/dev/null || true exit 1 fi sleep 1 done } start_yyb_worker echo "正在启动本地 MySQL..." MYSQL_IMAGE="$MYSQL_IMAGE" \ MYSQL_BIND_HOST="$MYSQL_BIND_HOST" \ MYSQL_HOST_PORT="$MYSQL_HOST_PORT" \ $COMPOSE up -d mysql echo "等待 MySQL 就绪..." for i in $(seq 1 30); do if $COMPOSE exec -T mysql sh -c 'mysqladmin ping -h localhost -uroot -p"$MYSQL_ROOT_PASSWORD" --silent' >/dev/null 2>&1; then echo "MySQL 已就绪" break fi if [ "$i" = "30" ]; then echo "MySQL 启动超时,查看日志:" $COMPOSE logs --tail 50 mysql exit 1 fi sleep 2 done echo "" echo "==============================" echo " 本地调试模式" echo "==============================" echo " 后端: http://${BACKEND_HOST}:${BACKEND_PORT}" echo " 前端: http://localhost:${FRONTEND_PORT}" echo " API 代理: ${BACKEND_PROXY_TARGET}" echo " MySQL: ${DB_HOST}:${DB_PORT}/${DB_NAME}" echo " YYB Worker: ${DEV_YYB_WORKER_URL}" echo " 后端日志: ${BACKEND_LOG}" echo " 前端日志: ${FRONTEND_LOG}" echo " Worker 日志: ${WORKER_LOG}" echo " 退出: Ctrl+C" echo "==============================" echo "" ( export DB_HOST DB_PORT DB_NAME DB_USER DB_PASSWORD export DB_POOL_SIZE DB_MAX_OVERFLOW DB_POOL_TIMEOUT DB_POOL_RECYCLE export YYB_WORKER_KEY LOG_LEVEL export YYB_WORKER_URL="$DEV_YYB_WORKER_URL" uv run uvicorn web.backend.main:app \ --host "$BACKEND_HOST" \ --port "$BACKEND_PORT" \ --reload \ --reload-dir core \ --reload-dir utils \ --reload-dir web/backend 2>&1 | tee -a "$BACKEND_LOG" ) & BACKEND_PID=$! ( cd web/frontend VITE_BACKEND_TARGET="$BACKEND_PROXY_TARGET" \ npm run dev -- --host "$FRONTEND_HOST" --port "$FRONTEND_PORT" 2>&1 | tee -a "$FRONTEND_LOG" ) & FRONTEND_PID=$! while true; do if ! kill -0 "$BACKEND_PID" 2>/dev/null; then wait "$BACKEND_PID" exit $? fi if ! kill -0 "$FRONTEND_PID" 2>/dev/null; then wait "$FRONTEND_PID" exit $? fi sleep 1 done