#!/usr/bin/env bash set -Eeuo pipefail set -m ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" BACKEND_DIR="$ROOT_DIR/backend" FRONTEND_DIR="$ROOT_DIR/frontend" BACKEND_VENV="$BACKEND_DIR/.venv" BACKEND_PYTHON="$BACKEND_VENV/bin/python" DO_SETUP=false CHECK_ONLY=false BACKEND_PID="" FRONTEND_PID="" CONFIG_PATH="" FRONTEND_PORT="" BACKEND_PORT="" usage() { cat <<'EOF' 用法: ./scripts/start-dev.sh 启动前端和后端 ./scripts/start-dev.sh --setup 同步本地依赖后启动 ./scripts/start-dev.sh --check 只检查依赖、配置和数据库连接 环境变量: PYTHON_BIN 创建虚拟环境时使用的 Python 3.12+ 可执行文件 BACKEND_CONFIG_FILE 指定其他后端 YAML 配置文件 FRONTEND_PORT 覆盖 YAML 中的前端端口 BACKEND_PORT 覆盖 YAML 中的后端端口 EOF } fail() { echo "❌ $*" >&2 exit 1 } process_group_alive() { local pid="$1" [[ -n "$pid" ]] && kill -0 -- "-$pid" 2>/dev/null } stop_process_group() { local pid="$1" local attempt=0 [[ -n "$pid" ]] || return 0 if process_group_alive "$pid"; then kill -TERM -- "-$pid" 2>/dev/null || true while process_group_alive "$pid" && [[ $attempt -lt 50 ]]; do sleep 0.1 attempt=$((attempt + 1)) done if process_group_alive "$pid"; then kill -KILL -- "-$pid" 2>/dev/null || true fi elif kill -0 "$pid" 2>/dev/null; then kill -TERM "$pid" 2>/dev/null || true fi wait "$pid" 2>/dev/null || true } cleanup() { trap - EXIT stop_process_group "$FRONTEND_PID" stop_process_group "$BACKEND_PID" } handle_signal() { echo echo "🛑 正在停止前端和后端……" exit 130 } port_in_use() { local port="$1" lsof -nP -iTCP:"$port" -sTCP:LISTEN >/dev/null 2>&1 } select_base_python() { if [[ -n "${PYTHON_BIN:-}" ]]; then printf '%s\n' "$PYTHON_BIN" elif [[ -x /opt/miniconda3/bin/python3 ]]; then printf '%s\n' /opt/miniconda3/bin/python3 else command -v python3 || true fi } setup_dependencies() { local base_python base_python="$(select_base_python)" [[ -n "$base_python" ]] || fail "未找到 Python 3,请通过 PYTHON_BIN 指定 Python 3.12+。" "$base_python" -c \ 'import sys; raise SystemExit(0 if sys.version_info >= (3, 12) else 1)' \ || fail "后端要求 Python 3.12+,当前为 $($base_python --version 2>&1)。" if [[ ! -x "$BACKEND_PYTHON" ]]; then echo "📦 创建后端虚拟环境……" "$base_python" -m venv "$BACKEND_VENV" fi echo "📦 同步后端依赖……" "$BACKEND_PYTHON" -m pip install -r "$BACKEND_DIR/requirements.txt" echo "📦 同步前端依赖……" (cd "$FRONTEND_DIR" && npm ci) if [[ -z "${BACKEND_CONFIG_FILE:-}" && ! -f "$BACKEND_DIR/config.yaml" ]]; then cp "$BACKEND_DIR/config.example.yaml" "$BACKEND_DIR/config.yaml" echo "📝 已根据 config.example.yaml 创建本地 config.yaml,请填写数据库密码。" fi } resolve_and_check_config() { local port_values CONFIG_PATH="$( cd "$BACKEND_DIR" "$BACKEND_PYTHON" -c \ 'from app.core.config import _resolve_config_path; print(_resolve_config_path()[0])' )" || fail "无法解析后端配置文件路径。" [[ -f "$CONFIG_PATH" ]] || fail "配置文件不存在:$CONFIG_PATH" port_values="$( cd "$BACKEND_DIR" "$BACKEND_PYTHON" -c \ 'from app.core.config import get_settings; settings = get_settings(); print(settings.frontend_port, settings.backend_port)' )" || fail "配置文件无法加载:$CONFIG_PATH" read -r FRONTEND_PORT BACKEND_PORT <<< "$port_values" [[ -n "$FRONTEND_PORT" && -n "$BACKEND_PORT" ]] \ || fail "无法从配置文件读取前端和后端端口:$CONFIG_PATH" } check_dependencies() { command -v node >/dev/null 2>&1 || fail "未找到 Node.js。" command -v npm >/dev/null 2>&1 || fail "未找到 npm。" command -v curl >/dev/null 2>&1 || fail "未找到 curl。" command -v lsof >/dev/null 2>&1 || fail "未找到 lsof。" [[ -x "$FRONTEND_DIR/node_modules/.bin/vite" ]] \ || fail "前端依赖不完整,请先运行 ./scripts/start-dev.sh --setup。" [[ -x "$BACKEND_PYTHON" ]] \ || fail "后端虚拟环境不存在,请先运行 ./scripts/start-dev.sh --setup。" "$BACKEND_PYTHON" -c 'import fastapi, psycopg, sqlalchemy, uvicorn, yaml' \ || fail "后端依赖不完整,请先运行 ./scripts/start-dev.sh --setup。" resolve_and_check_config } check_database() { echo "🔍 检查 PostgreSQL 连接:$CONFIG_PATH" ( cd "$BACKEND_DIR" "$BACKEND_PYTHON" -c ' import psycopg from app.core.config import get_settings url = get_settings().database_url prefix = "postgresql+psycopg://" if url.startswith(prefix): url = "postgresql://" + url[len(prefix):] connection = psycopg.connect(url, connect_timeout=3) connection.close() ' ) || fail "PostgreSQL 无法连接,请检查配置文件中的地址、用户名和密码:$CONFIG_PATH" } wait_for_url() { local url="$1" local service_name="$2" local pid="$3" local attempt=0 local max_attempts=30 while [[ $attempt -lt $max_attempts ]]; do if curl -fsS --max-time 1 "$url" >/dev/null 2>&1; then return 0 fi if ! kill -0 "$pid" 2>/dev/null; then echo "❌ $service_name 进程已提前退出。" >&2 return 1 fi sleep 1 attempt=$((attempt + 1)) done echo "❌ 等待 $service_name 就绪超时:$url" >&2 return 1 } while [[ $# -gt 0 ]]; do case "$1" in --setup) DO_SETUP=true ;; --check) CHECK_ONLY=true ;; -h|--help) usage exit 0 ;; *) usage >&2 fail "未知参数:$1" ;; esac shift done if [[ "$DO_SETUP" == true ]]; then setup_dependencies fi check_dependencies check_database if [[ "$CHECK_ONLY" == true ]]; then echo "✅ 前端、后端依赖、配置和 PostgreSQL 连接均正常。" exit 0 fi port_in_use "$BACKEND_PORT" \ && fail "后端端口 $BACKEND_PORT 已被占用,后端可能已经启动。" port_in_use "$FRONTEND_PORT" \ && fail "前端端口 $FRONTEND_PORT 已被占用,前端可能已经启动。" trap cleanup EXIT trap handle_signal INT TERM echo "🚀 启动后端:http://127.0.0.1:$BACKEND_PORT" ( cd "$BACKEND_DIR" exec "$BACKEND_PYTHON" -m uvicorn app.main:app \ --host 0.0.0.0 \ --port "$BACKEND_PORT" \ --reload ) & BACKEND_PID=$! wait_for_url "http://127.0.0.1:$BACKEND_PORT/modelTF/health" "后端" "$BACKEND_PID" \ || fail "后端启动失败。" echo "🚀 启动前端:http://127.0.0.1:$FRONTEND_PORT" ( cd "$FRONTEND_DIR" export FRONTEND_PORT BACKEND_PORT exec npm run dev -- --host 0.0.0.0 --port "$FRONTEND_PORT" --strictPort ) & FRONTEND_PID=$! wait_for_url "http://127.0.0.1:$FRONTEND_PORT" "前端" "$FRONTEND_PID" \ || fail "前端启动失败。" echo "✅ 前端和后端均已就绪,按 Ctrl+C 可同时停止。" while kill -0 "$BACKEND_PID" 2>/dev/null && kill -0 "$FRONTEND_PID" 2>/dev/null; do sleep 1 done set +e if ! kill -0 "$BACKEND_PID" 2>/dev/null; then wait "$BACKEND_PID" EXIT_CODE=$? echo "❌ 后端已退出,正在停止前端。" >&2 else wait "$FRONTEND_PID" EXIT_CODE=$? echo "❌ 前端已退出,正在停止后端。" >&2 fi set -e exit "$EXIT_CODE"