#!/bin/bash # # Cosmo 开发环境一键启动脚本 # # 先检查运行环境(Python / Node / 依赖 / 配置 / 数据库 / Redis / 端口), # 检查通过后启动: # - 后端 FastAPI http://localhost:8000 (API 前缀 /api) # - 前端 Vite http://localhost:5173 # # 用法: # ./scripts/run.sh # 检查环境并启动前后端(Ctrl+C 停止) # ./scripts/run.sh --check # 只检查环境,不启动服务 set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" BACKEND_DIR="$PROJECT_ROOT/backend" FRONTEND_DIR="$PROJECT_ROOT/frontend" BACKEND_HOST="${BACKEND_HOST:-0.0.0.0}" BACKEND_PORT="${BACKEND_PORT:-8000}" FRONTEND_PORT="${PORT:-5173}" RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' info() { echo -e "${BLUE}[INFO]${NC} $1"; } ok() { echo -e "${GREEN}[ OK ]${NC} $1"; } warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } fail() { echo -e "${RED}[FAIL]${NC} $1"; exit 1; } CHECK_ONLY=0 if [ "${1:-}" = "--check" ] || [ "${1:-}" = "-c" ]; then CHECK_ONLY=1 fi print_header() { echo "=================================================================" echo " Cosmo 开发环境启动脚本" echo " 项目目录: $PROJECT_ROOT" echo "=================================================================" } # ---------------------------------------------------------------- 后端环境 PYTHON="" check_backend_env() { info "检查后端环境..." if [ -x "$BACKEND_DIR/venv/bin/python" ]; then PYTHON="$BACKEND_DIR/venv/bin/python" else PYTHON="$(command -v python3 || true)" [ -n "$PYTHON" ] || fail "未找到 python3,请先安装 Python 3.11+" info "未找到 backend/venv,正在创建虚拟环境..." "$PYTHON" -m venv "$BACKEND_DIR/venv" || fail "创建虚拟环境失败" PYTHON="$BACKEND_DIR/venv/bin/python" fi "$PYTHON" -c 'import sys; sys.exit(0 if sys.version_info >= (3, 11) else 1)' \ || fail "Python 版本过低(需 3.11+): $("$PYTHON" --version 2>&1)" ok "Python: $("$PYTHON" --version 2>&1) ($PYTHON)" if ! "$PYTHON" -c 'import fastapi, uvicorn, sqlalchemy, asyncpg' &> /dev/null; then info "后端依赖不完整,正在安装 requirements.txt..." "$PYTHON" -m pip install -r "$BACKEND_DIR/requirements.txt" || fail "依赖安装失败" fi ok "后端依赖已就绪" if [ ! -f "$BACKEND_DIR/.env" ]; then if [ -f "$BACKEND_DIR/.env.example" ]; then cp "$BACKEND_DIR/.env.example" "$BACKEND_DIR/.env" warn "已从 .env.example 生成 backend/.env,请核对数据库等配置" else fail "缺少 backend/.env,且未找到 backend/.env.example" fi fi ok "配置文件: backend/.env" } # ---------------------------------------------------------------- 前端环境 check_frontend_env() { info "检查前端环境..." command -v node &> /dev/null || fail "未找到 node,请先安装 Node.js 20+" local node_major node_major="$(node -p 'process.versions.node.split(".")[0]')" if [ "$node_major" -lt 20 ]; then fail "Node.js 版本过低(需 20+): $(node --version)" fi ok "Node.js: $(node --version)" command -v yarn &> /dev/null || fail "未找到 yarn,请先安装 Yarn" ok "Yarn: $(yarn --version)" if [ ! -d "$FRONTEND_DIR/node_modules" ]; then info "未找到 frontend/node_modules,正在安装前端依赖..." (cd "$FRONTEND_DIR" && yarn install --ignore-engines) || fail "前端依赖安装失败" fi ok "前端依赖已就绪" } # ------------------------------------------------------- 数据库 / Redis / 端口 check_services() { info "检查数据库与 Redis 连接..." local output status set +e output="$(cd "$BACKEND_DIR" && "$PYTHON" - <<'PY' 2>&1 import socket import sys from app.config import settings def reachable(host: str, port: int, timeout: float = 3.0) -> bool: try: with socket.create_connection((host, port), timeout=timeout): return True except OSError: return False database_ok = reachable(settings.database_host, settings.database_port) redis_ok = reachable(settings.redis_host, settings.redis_port) print(f"database|{settings.database_host}:{settings.database_port}|{'ok' if database_ok else 'fail'}") print(f"redis|{settings.redis_host}:{settings.redis_port}|{'ok' if redis_ok else 'fail'}") sys.exit(0 if database_ok else 1) PY )" status=$? set -e echo "$output" | sed -u 's/^/ /' [ "$status" -eq 0 ] || fail "无法连接 PostgreSQL,请先启动数据库(如 docker compose up -d postgres redis)" ok "PostgreSQL 连接正常" if echo "$output" | grep -q '^redis|.*|fail$'; then warn "Redis 未连接,后端将降级为内存缓存" else ok "Redis 连接正常" fi } port_in_use() { "$PYTHON" - "$1" <<'PY' import socket import sys with socket.socket() as sock: sock.settimeout(1.0) sys.exit(0 if sock.connect_ex(("127.0.0.1", int(sys.argv[1]))) == 0 else 1) PY } # ---------------------------------------------------------------- 服务启动 PIDS=() start_backend() { info "启动后端: http://localhost:$BACKEND_PORT (API 文档 /api/docs)" (cd "$BACKEND_DIR" && exec "$PYTHON" -m uvicorn app.main:app \ --host "$BACKEND_HOST" --port "$BACKEND_PORT" --reload) \ > >(sed -u 's/^/[后端] /') 2>&1 & PIDS+=("$!") } start_frontend() { info "启动前端: http://localhost:$FRONTEND_PORT" (cd "$FRONTEND_DIR" && PORT="$FRONTEND_PORT" exec yarn dev) \ > >(sed -u 's/^/[前端] /') 2>&1 & PIDS+=("$!") } cleanup() { trap - INT TERM echo info "正在停止服务..." if [ "${#PIDS[@]}" -gt 0 ]; then for pid in "${PIDS[@]}"; do kill "$pid" 2> /dev/null || true done fi wait 2> /dev/null || true ok "服务已停止" } main() { print_header check_backend_env check_frontend_env check_services info "检查端口占用..." local backend_free=1 frontend_free=1 if port_in_use "$BACKEND_PORT"; then warn "端口 $BACKEND_PORT 已被占用,跳过启动后端" backend_free=0 fi if port_in_use "$FRONTEND_PORT"; then warn "端口 $FRONTEND_PORT 已被占用,跳过启动前端" frontend_free=0 fi if [ "$backend_free" = 1 ] && [ "$frontend_free" = 1 ]; then ok "端口 $BACKEND_PORT / $FRONTEND_PORT 均可用" fi if [ "$CHECK_ONLY" = 1 ]; then echo ok "环境检查完成" exit 0 fi echo if [ "$backend_free" = 1 ]; then start_backend fi if [ "$frontend_free" = 1 ]; then start_frontend fi if [ "${#PIDS[@]}" -eq 0 ]; then warn "前后端均已在运行,未启动新服务" exit 0 fi echo ok "启动完成,按 Ctrl+C 停止服务" trap cleanup INT TERM wait } main "$@"