cosmo/scripts/run.sh

245 lines
7.0 KiB
Bash
Raw Normal View History

2026-09-20 05:48:43 +00:00
#!/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 "$@"