#!/bin/bash # Jingrow 统一开发脚本 set -e # 颜色定义 RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # 打印函数 info() { echo -e "${BLUE}ℹ${NC} $1"; } success() { echo -e "${GREEN}✓${NC} $1"; } warn() { echo -e "${YELLOW}⚠${NC} $1"; } error() { echo -e "${RED}✗${NC} $1"; } # 检查命令是否存在 check_cmd() { if ! command -v "$1" &> /dev/null; then error "$1 未安装" return 1 fi } # 安装 uv(官方安装脚本) install_uv() { info "尝试自动安装 uv..." # 使用仓库内脚本进行安装 if [ -f ./install_uv.sh ]; then bash ./install_uv.sh || { warn "install_uv.sh 执行失败" return 1 } else warn "install_uv.sh 不存在" return 1 fi # 尝试把本次会话的 PATH 补上 export PATH="$HOME/.local/bin:$PATH" # 如果 uv 安装脚本生成了会话环境文件,则立刻激活 if [ -f "$HOME/.local/bin/env" ]; then # shellcheck disable=SC1090 source "$HOME/.local/bin/env" fi if command -v uv &> /dev/null; then success "uv 安装完成" return 0 fi return 1 } # 清理进程 cleanup() { info "清理现有进程..." pkill -f "uvicorn.*jingrow.main:app" 2>/dev/null || true pkill -f "concurrently" 2>/dev/null || true pkill -f "dramatiq.*jingrow.services.queue" 2>/dev/null || true pkill -f "vite" 2>/dev/null || true sleep 1 # 强制释放端口 if lsof -ti:9001 &> /dev/null; then warn "释放端口 9001..." lsof -ti:9001 | xargs kill -9 2>/dev/null || true fi if lsof -ti:3002 &> /dev/null; then warn "释放端口 3002..." lsof -ti:3002 | xargs kill -9 2>/dev/null || true fi success "清理完成" } # 确保 uv 可用:最先执行 ensure_uv() { if command -v uv &> /dev/null; then return 0 fi warn "uv 未安装,将尝试自动安装" install_uv || { error "自动安装 uv 失败,请手动安装: curl -fsSL https://astral.sh/uv/install.sh | sh" exit 1 } # 再次兜底激活环境,确保当前会话可用 if [ -f "$HOME/.local/bin/env" ]; then # shellcheck disable=SC1090 source "$HOME/.local/bin/env" fi } # 检查Redis check_redis() { if redis-cli ping &> /dev/null; then success "Redis 已运行" return 0 fi warn "Redis 未运行,尝试启动..." if command -v redis-server &> /dev/null; then redis-server ./redis.conf &> /dev/null & sleep 2 if redis-cli ping &> /dev/null; then success "Redis 启动成功" return 0 fi fi error "Redis 未运行,请手动启动: redis-server" return 1 } # 检查依赖 check_deps() { info "检查依赖..." check_cmd node || exit 1 # 这里假设 ensure_uv 已经在更早阶段被调用 check_cmd uv || exit 1 # 检查前端依赖 if [ ! -d "apps/jingrow/frontend/node_modules" ]; then warn "前端依赖未安装" info "安装前端依赖..." cd apps/jingrow/frontend && npm install && cd ../.. fi # 即使 node_modules 存在,也校验 vite 是否可用(可能之前安装不完整) if [ ! -x "apps/jingrow/frontend/node_modules/.bin/vite" ]; then warn "未检测到 vite,可执行文件缺失,重新安装前端依赖..." (cd apps/jingrow/frontend && npm install) fi # 检查后端依赖(优先使用 uv) info "同步后端依赖 (uv)..." if [ -f "apps/jingrow/pyproject.toml" ]; then (cd apps/jingrow && uv sync) || { error "uv 同步失败" exit 1 } elif [ -f "apps/jingrow/requirements.txt" ]; then warn "未发现 pyproject.toml,使用 requirements.txt 安装" (cd apps/jingrow && uv pip install -r requirements.txt) || { error "uv pip 安装失败" exit 1 } else warn "未发现依赖清单(pyproject.toml 或 requirements.txt)" fi success "依赖检查完成" } # 构建热重载目录参数(返回热重载参数,日志输出到stderr) # 使用 --reload-include 模式,自动监听 apps 目录下所有 .py 文件,支持动态安装 build_reload_dirs() { local RELOAD_DIRS="" local APPS_DIR="apps" if [ -d "$APPS_DIR" ]; then # 监听整个 apps 目录,自动包含新安装的 app(支持热重载) RELOAD_DIRS="--reload-dir $(cd "$APPS_DIR" && pwd)" info "已启用 apps 目录热重载(自动监听所有 app)" >&2 else warn "apps 目录不存在" >&2 fi echo "$RELOAD_DIRS" } # 启动所有服务 start_all() { info "启动开发环境..." ensure_uv cleanup check_redis check_deps # 加载环境变量 if [ -f .env ]; then export $(grep -v '^#' .env | xargs) fi # 构建热重载目录参数 RELOAD_DIRS=$(build_reload_dirs) info "启动所有服务..." # 使用延迟启动,让后端先启动3秒 npx --yes concurrently \ --names "BACKEND,WORKER,FRONTEND" \ --prefix-colors "blue,yellow,green" \ "cd apps/jingrow && uv run python -m jingrow.app" \ "cd apps/jingrow && uv run python -m jingrow.dramatiq" \ "sleep 3 && cd apps/jingrow/frontend && npm run dev" } # 只启动前端 start_frontend() { info "启动前端..." ensure_uv cleanup check_deps cd apps/jingrow/frontend && npm run dev } # 只启动后端 start_backend() { info "启动后端..." ensure_uv cleanup check_redis check_deps if [ -f .env ]; then export $(grep -v '^#' .env | xargs) fi # 构建热重载目录参数 RELOAD_DIRS=$(build_reload_dirs) npx --yes concurrently \ --names "BACKEND,WORKER" \ --prefix-colors "blue,yellow" \ "cd apps/jingrow && uv run python -m jingrow.app" \ "cd apps/jingrow && uv run python -m jingrow.dramatiq" } # 停止服务 stop_all() { info "停止所有服务..." cleanup success "已停止" } # 重启服务 restart() { stop_all sleep 2 start_all } # 主函数 main() { case "${1:-start}" in start) start_all ;; stop) stop_all ;; restart) restart ;; frontend) start_frontend ;; backend) start_backend ;; *) echo "用法: ./dev.sh [start|stop|restart|frontend|backend]" echo "" echo "命令:" echo " start 启动所有服务(默认)" echo " stop 停止所有服务" echo " restart 重启所有服务" echo " frontend 只启动前端" echo " backend 只启动后端" exit 1 ;; esac } main "$@"