#!/bin/bash PROJECT_DIR="$(cd "$(dirname "$0")" && pwd)" FRONTEND_DIR="$PROJECT_DIR/legal-platform-frontend" BACKEND_DIR="$PROJECT_DIR" WORKER_SCRIPT="$PROJECT_DIR/scripts/start_worker.sh" BEAT_SCRIPT="$PROJECT_DIR/scripts/start_beat.sh" LOG_DIR="$PROJECT_DIR/.codex-run" BACKEND_PID_FILE="$LOG_DIR/backend.pid" FRONTEND_PID_FILE="$LOG_DIR/frontend.pid" WORKER_PID_FILE="$LOG_DIR/worker.pid" BEAT_PID_FILE="$LOG_DIR/beat.pid" BACKEND_LOG="$LOG_DIR/backend.log" FRONTEND_LOG="$LOG_DIR/frontend.log" WORKER_LOG="$LOG_DIR/worker.log" BEAT_LOG="$LOG_DIR/beat.log" RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' CYAN='\033[0;36m' NC='\033[0m' mkdir -p "$LOG_DIR" log_info() { echo -e "${BLUE}[INFO]${NC} $1"; } log_success() { echo -e "${GREEN}[OK]${NC} $1"; } log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } log_error() { echo -e "${RED}[ERROR]${NC} $1"; } resolve_python() { local -a candidates=( "$PROJECT_DIR/.venv/bin/python" "$PROJECT_DIR/.venv/bin/python3" "$(command -v python3 2>/dev/null)" "$(command -v python 2>/dev/null)" ) local candidate="" for candidate in "${candidates[@]}"; do if [ -n "$candidate" ] && [ -x "$candidate" ]; then echo "$candidate" return 0 fi done return 1 } read_env_value() { local file=$1 local key=$2 if [ -f "$file" ]; then awk -F= -v key="$key" '$1 == key {print substr($0, index($0, "=") + 1); exit}' "$file" fi } FRONTEND_PUBLIC_PORT="$(read_env_value "$FRONTEND_DIR/.env" "PORT")" FRONTEND_PUBLIC_PORT="${FRONTEND_PUBLIC_PORT:-5173}" FRONTEND_DEV_PORT=5193 BACKEND_PYTHON="$(resolve_python || true)" BACKEND_PORT="$("${BACKEND_PYTHON:-python3}" - <<'PY' 2>/dev/null || true from fastapi_admin.config import APP_PORT print(APP_PORT) PY )" BACKEND_PORT="${BACKEND_PORT:-8000}" pid_alive() { local pid=$1 [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null } service_pid() { local pid_file=$1 if [ -f "$pid_file" ]; then tr -d '[:space:]' < "$pid_file" fi } cleanup_pid_file() { local pid_file=$1 local pid pid=$(service_pid "$pid_file") if [ -n "$pid" ] && ! pid_alive "$pid"; then rm -f "$pid_file" fi } port_pid() { local port=$1 local pid="" pid=$(lsof -tiTCP:"$port" -sTCP:LISTEN 2>/dev/null | head -n 1) if [ -n "$pid" ]; then echo "$pid" return 0 fi pid=$(ss -ltnp 2>/dev/null | awk -v port=":$port" ' index($0, port) { if (match($0, /pid=[0-9]+/)) { value = substr($0, RSTART + 4, RLENGTH - 4) print value exit } } ') [ -n "$pid" ] && echo "$pid" } pid_command() { local pid=$1 ps -p "$pid" -o args= 2>/dev/null | xargs } kill_process_tree() { local pid=$1 [ -n "$pid" ] || return 0 local children children=$(pgrep -P "$pid" 2>/dev/null || true) if [ -n "$children" ]; then local child for child in $children; do kill_process_tree "$child" done fi kill "$pid" 2>/dev/null || true } ensure_port_free() { local name=$1 local port=$2 local expected_pid=${3:-} local running_pid running_pid=$(port_pid "$port") if [ -n "$running_pid" ] && [ "$running_pid" != "$expected_pid" ]; then local cmd cmd="$(pid_command "$running_pid")" log_error "$name 目标端口 $port 已被其他进程占用 (PID: $running_pid)" [ -n "$cmd" ] && log_error "占用进程命令: $cmd" return 1 fi return 0 } start_backend() { cleanup_pid_file "$BACKEND_PID_FILE" local pid pid=$(service_pid "$BACKEND_PID_FILE") if pid_alive "$pid"; then log_warn "后端已在运行 (PID: $pid)" return 0 fi ensure_port_free "后端" "$BACKEND_PORT" || return 1 if [ -z "$BACKEND_PYTHON" ]; then log_error "未找到可用 Python" return 1 fi log_info "启动后端服务 (端口: $BACKEND_PORT)..." : > "$BACKEND_LOG" ( cd "$BACKEND_DIR" exec "$BACKEND_PYTHON" run.py ) >> "$BACKEND_LOG" 2>&1 & pid=$! sleep 2 local listening_pid listening_pid=$(port_pid "$BACKEND_PORT") if pid_alive "$pid" || [ -n "$listening_pid" ]; then echo "${listening_pid:-$pid}" > "$BACKEND_PID_FILE" log_success "后端启动成功 (PID: $pid, 端口: $BACKEND_PORT)" return 0 fi log_error "后端启动失败,查看日志: $BACKEND_LOG" tail -20 "$BACKEND_LOG" 2>/dev/null || true rm -f "$BACKEND_PID_FILE" return 1 } start_frontend() { cleanup_pid_file "$FRONTEND_PID_FILE" local pid pid=$(service_pid "$FRONTEND_PID_FILE") if pid_alive "$pid"; then log_warn "前端已在运行 (PID: $pid)" return 0 fi ensure_port_free "前端开发服务" "$FRONTEND_DEV_PORT" || return 1 log_info "启动前端开发服务 (端口: $FRONTEND_DEV_PORT,代理入口: $FRONTEND_PUBLIC_PORT)..." : > "$FRONTEND_LOG" ( cd "$FRONTEND_DIR" exec npm run dev:dev ) >> "$FRONTEND_LOG" 2>&1 & pid=$! sleep 4 local listening_pid listening_pid=$(port_pid "$FRONTEND_DEV_PORT") if pid_alive "$pid" || [ -n "$listening_pid" ]; then echo "${listening_pid:-$pid}" > "$FRONTEND_PID_FILE" log_success "前端启动成功 (PID: $pid, 开发端口: $FRONTEND_DEV_PORT, 访问端口: $FRONTEND_PUBLIC_PORT)" return 0 fi log_error "前端启动失败,查看日志: $FRONTEND_LOG" tail -20 "$FRONTEND_LOG" 2>/dev/null || true rm -f "$FRONTEND_PID_FILE" return 1 } start_worker() { cleanup_pid_file "$WORKER_PID_FILE" local pid pid=$(service_pid "$WORKER_PID_FILE") if pid_alive "$pid"; then log_warn "Worker 已在运行 (PID: $pid)" return 0 fi if [ ! -x "$WORKER_SCRIPT" ]; then log_error "Worker 启动脚本不存在或不可执行: $WORKER_SCRIPT" return 1 fi log_info "启动 Worker 服务..." : > "$WORKER_LOG" ( cd "$PROJECT_DIR" exec "$WORKER_SCRIPT" ) >> "$WORKER_LOG" 2>&1 & pid=$! echo "$pid" > "$WORKER_PID_FILE" sleep 2 if pid_alive "$pid"; then log_success "Worker 启动成功 (PID: $pid)" return 0 fi log_error "Worker 启动失败,查看日志: $WORKER_LOG" tail -20 "$WORKER_LOG" 2>/dev/null || true rm -f "$WORKER_PID_FILE" return 1 } start_beat() { cleanup_pid_file "$BEAT_PID_FILE" local pid pid=$(service_pid "$BEAT_PID_FILE") if pid_alive "$pid"; then log_warn "Beat 已在运行 (PID: $pid)" return 0 fi if [ ! -x "$BEAT_SCRIPT" ]; then log_error "Beat 启动脚本不存在或不可执行: $BEAT_SCRIPT" return 1 fi log_info "启动 Beat 调度服务..." : > "$BEAT_LOG" ( cd "$PROJECT_DIR" exec "$BEAT_SCRIPT" ) >> "$BEAT_LOG" 2>&1 & pid=$! echo "$pid" > "$BEAT_PID_FILE" sleep 2 if pid_alive "$pid"; then log_success "Beat 启动成功 (PID: $pid)" return 0 fi log_error "Beat 启动失败,查看日志: $BEAT_LOG" tail -20 "$BEAT_LOG" 2>/dev/null || true rm -f "$BEAT_PID_FILE" return 1 } stop_service() { local name=$1 local pid_file=$2 local port=${3:-} local pid pid=$(service_pid "$pid_file") if ! pid_alive "$pid"; then if [ -n "$port" ]; then local stray_pid stray_pid=$(port_pid "$port") if [ -n "$stray_pid" ]; then log_warn "$name PID 文件已失效,正在清理端口 $port 上的残留进程 (PID: $stray_pid)..." kill_process_tree "$stray_pid" sleep 1 if pid_alive "$stray_pid"; then kill -9 "$stray_pid" 2>/dev/null || true fi rm -f "$pid_file" log_success "$name 残留进程已停止" return 0 fi fi rm -f "$pid_file" log_warn "$name 未运行" return 0 fi log_info "停止 $name (PID: $pid)..." kill_process_tree "$pid" for _ in $(seq 1 10); do local current_port_pid="" if [ -n "$port" ]; then current_port_pid=$(port_pid "$port") fi if ! pid_alive "$pid" && [ -z "$current_port_pid" ]; then break fi sleep 0.5 done local stubborn_pid="" if [ -n "$port" ]; then stubborn_pid=$(port_pid "$port") fi if pid_alive "$pid" || [ -n "$stubborn_pid" ]; then log_warn "$name 未响应,强制终止..." kill -9 "$pid" 2>/dev/null || true [ -n "$stubborn_pid" ] && kill -9 "$stubborn_pid" 2>/dev/null || true fi rm -f "$pid_file" log_success "$name 已停止" } do_start() { echo "" echo -e "${CYAN}============================================${NC}" echo -e "${CYAN} 启动 LeAudit 前后端${NC}" echo -e "${CYAN}============================================${NC}" echo "" start_backend || return 1 start_frontend || return 1 start_worker || return 1 start_beat || return 1 echo "" echo -e "${GREEN}============================================${NC}" echo -e "${GREEN} 前端: http://localhost:$FRONTEND_PUBLIC_PORT (开发服务: $FRONTEND_DEV_PORT)${NC}" echo -e "${GREEN} 后端: http://localhost:$BACKEND_PORT${NC}" echo -e "${GREEN} Worker: $WORKER_SCRIPT${NC}" echo -e "${GREEN} Beat: $BEAT_SCRIPT${NC}" echo -e "${GREEN} 日志目录: $LOG_DIR${NC}" echo -e "${GREEN}============================================${NC}" echo "" } do_stop() { echo "" echo -e "${YELLOW}============================================${NC}" echo -e "${YELLOW} 停止 LeAudit 前后端${NC}" echo -e "${YELLOW}============================================${NC}" echo "" stop_service "Beat" "$BEAT_PID_FILE" stop_service "Worker" "$WORKER_PID_FILE" stop_service "前端" "$FRONTEND_PID_FILE" "$FRONTEND_DEV_PORT" stop_service "后端" "$BACKEND_PID_FILE" "$BACKEND_PORT" echo "" } do_restart() { do_stop sleep 1 do_start } do_status() { cleanup_pid_file "$BACKEND_PID_FILE" cleanup_pid_file "$FRONTEND_PID_FILE" cleanup_pid_file "$WORKER_PID_FILE" cleanup_pid_file "$BEAT_PID_FILE" echo "" echo -e "${CYAN}============================================${NC}" echo -e "${CYAN} 服务状态${NC}" echo -e "${CYAN}============================================${NC}" echo "" local pid pid=$(service_pid "$BACKEND_PID_FILE") if pid_alive "$pid"; then echo -e " 后端: ${GREEN}● 运行中${NC} PID=$pid 端口=$BACKEND_PORT" else echo -e " 后端: ${RED}○ 已停止${NC}" fi pid=$(service_pid "$FRONTEND_PID_FILE") if pid_alive "$pid"; then echo -e " 前端: ${GREEN}● 运行中${NC} PID=$pid 开发端口=$FRONTEND_DEV_PORT 访问端口=$FRONTEND_PUBLIC_PORT" else echo -e " 前端: ${RED}○ 已停止${NC}" fi pid=$(service_pid "$WORKER_PID_FILE") if pid_alive "$pid"; then echo -e " Worker: ${GREEN}● 运行中${NC} PID=$pid" else echo -e " Worker: ${RED}○ 已停止${NC}" fi pid=$(service_pid "$BEAT_PID_FILE") if pid_alive "$pid"; then echo -e " Beat: ${GREEN}● 运行中${NC} PID=$pid" else echo -e " Beat: ${RED}○ 已停止${NC}" fi echo "" echo " 后端日志: $BACKEND_LOG" echo " 前端日志: $FRONTEND_LOG" echo " Worker日志: $WORKER_LOG" echo " Beat日志: $BEAT_LOG" echo "" } do_logs() { local target=${2:-all} local lines=${3:-100} case "$target" in backend) [ -f "$BACKEND_LOG" ] || touch "$BACKEND_LOG" echo -e "${CYAN}--- 后端日志 (最近 $lines 行) ---${NC}" tail -n "$lines" "$BACKEND_LOG" echo -e "${CYAN}--- Ctrl+C 退出 ---${NC}" tail -f "$BACKEND_LOG" ;; frontend) [ -f "$FRONTEND_LOG" ] || touch "$FRONTEND_LOG" echo -e "${CYAN}--- 前端日志 (最近 $lines 行) ---${NC}" tail -n "$lines" "$FRONTEND_LOG" echo -e "${CYAN}--- Ctrl+C 退出 ---${NC}" tail -f "$FRONTEND_LOG" ;; worker) [ -f "$WORKER_LOG" ] || touch "$WORKER_LOG" echo -e "${CYAN}--- Worker 日志 (最近 $lines 行) ---${NC}" tail -n "$lines" "$WORKER_LOG" echo -e "${CYAN}--- Ctrl+C 退出 ---${NC}" tail -f "$WORKER_LOG" ;; beat) [ -f "$BEAT_LOG" ] || touch "$BEAT_LOG" echo -e "${CYAN}--- Beat 日志 (最近 $lines 行) ---${NC}" tail -n "$lines" "$BEAT_LOG" echo -e "${CYAN}--- Ctrl+C 退出 ---${NC}" tail -f "$BEAT_LOG" ;; all) [ -f "$BACKEND_LOG" ] || touch "$BACKEND_LOG" [ -f "$FRONTEND_LOG" ] || touch "$FRONTEND_LOG" [ -f "$WORKER_LOG" ] || touch "$WORKER_LOG" [ -f "$BEAT_LOG" ] || touch "$BEAT_LOG" echo -e "${CYAN}--- 后端日志 (最近 $lines 行) ---${NC}" tail -n "$lines" "$BACKEND_LOG" echo "" echo -e "${CYAN}--- 前端日志 (最近 $lines 行) ---${NC}" tail -n "$lines" "$FRONTEND_LOG" echo "" echo -e "${CYAN}--- Worker 日志 (最近 $lines 行) ---${NC}" tail -n "$lines" "$WORKER_LOG" echo "" echo -e "${CYAN}--- Beat 日志 (最近 $lines 行) ---${NC}" tail -n "$lines" "$BEAT_LOG" echo "" echo -e "${CYAN}--- Ctrl+C 退出 ---${NC}" tail -n 0 -f "$BACKEND_LOG" "$FRONTEND_LOG" "$WORKER_LOG" "$BEAT_LOG" 2>/dev/null | awk ' /^==> .*backend\.log <==$/ { current="[backend]"; next } /^==> .*frontend\.log <==$/ { current="[frontend]"; next } /^==> .*worker\.log <==$/ { current="[worker]"; next } /^==> .*beat\.log <==$/ { current="[beat]"; next } { print current " " $0; fflush() } ' ;; *) echo "用法: ./leaudit.sh logs [backend|frontend|worker|beat|all] [行数]" exit 1 ;; esac } do_doctor() { echo "" echo -e "${CYAN}============================================${NC}" echo -e "${CYAN} 运行环境检查${NC}" echo -e "${CYAN}============================================${NC}" echo "" local pid cmd pid=$(port_pid "$FRONTEND_PUBLIC_PORT") if [ -n "$pid" ]; then cmd="$(pid_command "$pid")" echo -e " 代理端口 5173: ${GREEN}● 已监听${NC} PID=$pid" [ -n "$cmd" ] && echo " 命令: $cmd" else echo -e " 代理端口 5173: ${RED}○ 未监听${NC}" fi pid=$(port_pid "$FRONTEND_DEV_PORT") if [ -n "$pid" ]; then cmd="$(pid_command "$pid")" echo -e " 前端开发 5193: ${GREEN}● 已监听${NC} PID=$pid" [ -n "$cmd" ] && echo " 命令: $cmd" else echo -e " 前端开发 5193: ${RED}○ 未监听${NC}" fi pid=$(port_pid "$BACKEND_PORT") if [ -n "$pid" ]; then cmd="$(pid_command "$pid")" echo -e " 后端服务 $BACKEND_PORT: ${GREEN}● 已监听${NC} PID=$pid" [ -n "$cmd" ] && echo " 命令: $cmd" else echo -e " 后端服务 $BACKEND_PORT: ${RED}○ 未监听${NC}" fi local worker_pid worker_pid=$(service_pid "$WORKER_PID_FILE") if pid_alive "$worker_pid"; then cmd="$(pid_command "$worker_pid")" echo -e " Worker 进程: ${GREEN}● 运行中${NC} PID=$worker_pid" [ -n "$cmd" ] && echo " 命令: $cmd" else echo -e " Worker 进程: ${RED}○ 未运行${NC}" fi local beat_pid beat_pid=$(service_pid "$BEAT_PID_FILE") if pid_alive "$beat_pid"; then cmd="$(pid_command "$beat_pid")" echo -e " Beat 进程: ${GREEN}● 运行中${NC} PID=$beat_pid" [ -n "$cmd" ] && echo " 命令: $cmd" else echo -e " Beat 进程: ${RED}○ 未运行${NC}" fi echo "" } do_open() { echo "" echo -e "${CYAN}============================================${NC}" echo -e "${CYAN} 访问地址与日志摘要${NC}" echo -e "${CYAN}============================================${NC}" echo "" echo " 前端访问: http://localhost:$FRONTEND_PUBLIC_PORT" echo " 前端开发: http://127.0.0.1:$FRONTEND_DEV_PORT" echo " 后端访问: http://localhost:$BACKEND_PORT" echo " Worker脚本: $WORKER_SCRIPT" echo " Beat脚本: $BEAT_SCRIPT" echo "" echo -e "${CYAN}--- 后端最近 5 行 ---${NC}" tail -n 5 "$BACKEND_LOG" 2>/dev/null || echo "(无后端日志)" echo "" echo -e "${CYAN}--- 前端最近 5 行 ---${NC}" tail -n 5 "$FRONTEND_LOG" 2>/dev/null || echo "(无前端日志)" echo "" echo -e "${CYAN}--- Worker最近 5 行 ---${NC}" tail -n 5 "$WORKER_LOG" 2>/dev/null || echo "(无Worker日志)" echo "" echo -e "${CYAN}--- Beat最近 5 行 ---${NC}" tail -n 5 "$BEAT_LOG" 2>/dev/null || echo "(无Beat日志)" echo "" } case "${1:-help}" in start) do_start ;; stop) do_stop ;; restart) do_restart ;; status) do_status ;; logs) do_logs "$@" ;; doctor) do_doctor ;; open) do_open ;; help|--help|-h) echo "用法: ./leaudit.sh <命令>" echo "" echo "命令说明:" echo " start 启动前后端、Worker、Beat" echo " stop 停止前后端、Worker、Beat" echo " restart 重启前后端、Worker、Beat" echo " status 查看前后端、Worker、Beat 运行状态" echo " logs 查看前后端、Worker、Beat 日志并持续跟踪" echo " logs backend 只看后端日志" echo " logs frontend 50 看前端最近 50 行日志并持续跟踪" echo " logs worker 50 看 Worker 最近 50 行日志并持续跟踪" echo " logs beat 50 看 Beat 最近 50 行日志并持续跟踪" echo " doctor 检查 5173 / 5193 / 8096 端口占用情况" echo " open 打印访问地址和最近日志摘要" echo "" echo "示例:" echo " ./leaudit.sh start" echo " ./leaudit.sh status" echo " ./leaudit.sh logs" echo " ./leaudit.sh doctor" echo " ./leaudit.sh open" ;; *) log_error "未知命令: $1" echo "运行 ./leaudit.sh help 查看帮助" exit 1 ;; esac