#!/bin/bash set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" cd "$ROOT_DIR" VENV_DIR=".venv" VENV_PYTHON="$VENV_DIR/bin/python" LOG_DIR="logs" API_PID_FILE="$LOG_DIR/api.pid" FRONTEND_PID_FILE="$LOG_DIR/frontend.pid" API_LOG_FILE="$LOG_DIR/api.log" FRONTEND_LOG_FILE="$LOG_DIR/frontend.log" DOCKER_CONTAINERS="milvus minio redis postgres" RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' CYAN='\033[0;36m' NC='\033[0m' load_env() { load_env_file ".env" load_env_file ".env.development" API_HOST="${API_HOST:-0.0.0.0}" API_PORT="${API_PORT:-8000}" FRONTEND_PORT="${FRONTEND_PORT:-5173}" FRONTEND_MODE="${FRONTEND_MODE:-dev}" } load_env_file() { local env_file="$1" if [ -f "$env_file" ]; then while IFS='=' read -r key value; do key="${key%$'\r'}" value="${value%$'\r'}" case "$key" in ""|\#*) continue ;; API_HOST|API_PORT|FRONTEND_PORT|FRONTEND_MODE) export "$key=$value" ;; esac done < "$env_file" fi } ensure_log_dir() { mkdir -p "$LOG_DIR" } print_header() { echo "" echo -e "${CYAN}========================================${NC}" echo -e "${CYAN} $1${NC}" echo -e "${CYAN}========================================${NC}" echo "" } info() { echo -e "${CYAN}$1${NC}" } success() { echo -e "${GREEN}$1${NC}" } warn() { echo -e "${YELLOW}$1${NC}" } error() { echo -e "${RED}$1${NC}" >&2 } die() { error "$1" exit 1 } is_pid_running() { local pid="$1" [ -n "$pid" ] && ps -p "$pid" > /dev/null 2>&1 } read_pid() { local pid_file="$1" if [ -f "$pid_file" ]; then cat "$pid_file" fi } cleanup_stale_pid() { local pid_file="$1" local pid pid="$(read_pid "$pid_file")" if [ -n "$pid" ] && ! is_pid_running "$pid"; then rm -f "$pid_file" fi } port_pid() { local port="$1" if command -v lsof > /dev/null 2>&1; then lsof -ti tcp:"$port" 2>/dev/null | head -n 1 || true return 0 fi if command -v ss > /dev/null 2>&1; then ss -lptn "sport = :$port" 2>/dev/null | awk -F 'pid=' 'NR>1 && NF>1 {split($2,a,/,/); print a[1]; exit}' || true return 0 fi if command -v netstat > /dev/null 2>&1; then netstat -lntp 2>/dev/null | awk -v port=":$port" '$4 ~ port {split($7,a,"/"); if (a[1] != "-") {print a[1]; exit}}' || true fi return 0 } require_python_bootstrap() { if command -v python3 > /dev/null 2>&1; then PYTHON_BOOTSTRAP="python3" elif command -v python > /dev/null 2>&1; then PYTHON_BOOTSTRAP="python" else die "未找到 Python,请先安装 Python 3.10+。" fi } require_venv() { [ -x "$VENV_PYTHON" ] || die "虚拟环境不存在,请先运行 ./dev.sh setup" } ensure_frontend_deps() { if [ ! -d "frontend" ]; then die "前端目录不存在: frontend" fi if [ ! -d "frontend/node_modules" ] || [ ! -d "frontend/node_modules/vite" ]; then warn "前端依赖不存在或不完整,正在执行 npm install..." npm --prefix frontend install fi } validate_frontend_mode() { case "$1" in dev|static) ;; *) die "前端模式仅支持 dev 或 static,当前值: $1" ;; esac } check_python_version() { local python_cmd="$1" "$python_cmd" -c 'import sys; raise SystemExit(0 if sys.version_info >= (3, 10) else 1)' } run_setup() { load_env ensure_log_dir print_header "AI+合规智能中枢 - 环境初始化" require_python_bootstrap info "[1/4] 检查 Python 版本" check_python_version "$PYTHON_BOOTSTRAP" || die "需要 Python 3.10+" success "Python 版本检查通过" echo "" info "[2/4] 准备虚拟环境" if [ ! -d "$VENV_DIR" ]; then "$PYTHON_BOOTSTRAP" -m venv "$VENV_DIR" success "已创建虚拟环境: $VENV_DIR" else success "虚拟环境已存在: $VENV_DIR" fi "$VENV_PYTHON" -m pip install --upgrade pip "$VENV_PYTHON" -m pip install -r backend/requirements.txt success "后端依赖安装完成" echo "" info "[3/4] 准备前端依赖" if ! command -v npm > /dev/null 2>&1; then die "未找到 npm,请先安装 Node.js 20+。" fi npm --prefix frontend install success "前端依赖安装完成" echo "" info "[4/4] 检查 Docker 基础服务" if command -v docker > /dev/null 2>&1; then local container for container in $DOCKER_CONTAINERS; do if docker ps --format '{{.Names}}' | grep -q "^${container}$"; then success "${container}: 运行中" elif docker ps -a --format '{{.Names}}' | grep -q "^${container}$"; then warn "${container}: 已创建但未运行" else warn "${container}: 未找到容器" fi done else warn "未检测到 Docker,已跳过容器检查" fi echo "" success "环境初始化完成" echo "后续常用命令:" echo " ./dev.sh start" echo " ./dev.sh status" echo " ./dev.sh logs api --follow" } api_health_ok() { if command -v curl > /dev/null 2>&1; then curl -fsS "http://localhost:$API_PORT/health" > /dev/null 2>&1 return fi require_venv "$VENV_PYTHON" - < /dev/null 2>&1 import sys from urllib.request import urlopen try: with urlopen("http://localhost:${API_PORT}/health", timeout=3) as response: body = response.read().decode("utf-8", errors="ignore") sys.exit(0 if "healthy" in body.lower() else 1) except Exception: sys.exit(1) PY } start_api() { local mode="${1:-background}" load_env ensure_log_dir require_venv cleanup_stale_pid "$API_PID_FILE" local pid pid="$(read_pid "$API_PID_FILE")" if is_pid_running "$pid"; then warn "API 已在运行 (PID: $pid)" return fi export PYTHONPATH="backend${PYTHONPATH:+:$PYTHONPATH}" if [ "$mode" = "foreground" ]; then print_header "AI+合规智能中枢 - 启动 API" echo "运行模式: 前台调试(带 --reload)" echo "服务地址: http://localhost:$API_PORT" echo "文档地址: http://localhost:$API_PORT/docs" echo "健康检查: http://localhost:$API_PORT/health" echo "" exec "$VENV_PYTHON" -m uvicorn app.main:app --host "$API_HOST" --port "$API_PORT" --reload fi nohup "$VENV_PYTHON" -m uvicorn app.main:app --host "$API_HOST" --port "$API_PORT" > "$API_LOG_FILE" 2>&1 & pid=$! echo "$pid" > "$API_PID_FILE" sleep 3 if is_pid_running "$pid"; then success "API 启动成功 (PID: $pid)" echo " 地址: http://localhost:$API_PORT" echo " 文档: http://localhost:$API_PORT/docs" echo " 日志: $API_LOG_FILE" else rm -f "$API_PID_FILE" die "API 启动失败,请查看日志: $API_LOG_FILE" fi } start_frontend() { local mode="${1:-$FRONTEND_MODE}" load_env ensure_log_dir cleanup_stale_pid "$FRONTEND_PID_FILE" local pid pid="$(read_pid "$FRONTEND_PID_FILE")" if is_pid_running "$pid"; then warn "前端已在运行 (PID: $pid)" return fi if ! command -v npm > /dev/null 2>&1; then die "未找到 npm,请先安装 Node.js 20+。" fi ensure_frontend_deps if [ "$mode" = "static" ]; then require_python_bootstrap npm --prefix frontend run build nohup "$PYTHON_BOOTSTRAP" -m http.server "$FRONTEND_PORT" --bind 0.0.0.0 --directory frontend/dist > "$FRONTEND_LOG_FILE" 2>&1 & else nohup npm --prefix frontend run dev -- --host 0.0.0.0 --port "$FRONTEND_PORT" > "$FRONTEND_LOG_FILE" 2>&1 & fi pid=$! echo "$pid" > "$FRONTEND_PID_FILE" sleep 4 if is_pid_running "$pid"; then success "前端启动成功 (PID: $pid)" echo " 地址: http://localhost:$FRONTEND_PORT" echo " 模式: $mode" echo " 日志: $FRONTEND_LOG_FILE" else rm -f "$FRONTEND_PID_FILE" die "前端启动失败,请查看日志: $FRONTEND_LOG_FILE" fi } kill_pid_file_process() { local pid_file="$1" local label="$2" local pid pid="$(read_pid "$pid_file")" if ! is_pid_running "$pid"; then rm -f "$pid_file" warn "$label 未通过 PID 文件发现运行中的进程" return 1 fi kill "$pid" 2>/dev/null || true sleep 2 if is_pid_running "$pid"; then kill -9 "$pid" 2>/dev/null || true fi rm -f "$pid_file" success "$label 已停止" return 0 } stop_api() { load_env ensure_log_dir if kill_pid_file_process "$API_PID_FILE" "API"; then return fi local port_listener port_listener="$(port_pid "$API_PORT")" if [ -n "$port_listener" ]; then kill "$port_listener" 2>/dev/null || true sleep 1 if is_pid_running "$port_listener"; then kill -9 "$port_listener" 2>/dev/null || true fi success "已停止监听 API 端口 $API_PORT 的进程 (PID: $port_listener)" else warn "未发现运行中的 API 服务" fi rm -f "$API_PID_FILE" } stop_frontend() { load_env ensure_log_dir if kill_pid_file_process "$FRONTEND_PID_FILE" "前端"; then return fi local port_listener port_listener="$(port_pid "$FRONTEND_PORT")" if [ -n "$port_listener" ]; then kill "$port_listener" 2>/dev/null || true sleep 1 if is_pid_running "$port_listener"; then kill -9 "$port_listener" 2>/dev/null || true fi success "已停止监听前端端口 $FRONTEND_PORT 的进程 (PID: $port_listener)" else warn "未发现运行中的前端服务" fi rm -f "$FRONTEND_PID_FILE" } run_status() { load_env ensure_log_dir print_header "AI+合规智能中枢 - 服务状态" cleanup_stale_pid "$API_PID_FILE" cleanup_stale_pid "$FRONTEND_PID_FILE" local api_running=false local frontend_running=false local pid local port_listener echo -e "${YELLOW}API 服务:${NC}" pid="$(read_pid "$API_PID_FILE")" if is_pid_running "$pid"; then api_running=true success " 状态: 运行中" echo " PID: $pid" else port_listener="$(port_pid "$API_PORT")" if [ -n "$port_listener" ]; then api_running=true success " 状态: 运行中 (无 PID 文件)" echo " PID: $port_listener" else error " 状态: 已停止" fi fi if [ "$api_running" = true ]; then if api_health_ok; then success " 健康检查: 正常" else warn " 健康检查: 未通过" fi fi echo " 地址: http://localhost:$API_PORT" echo " 文档: http://localhost:${API_PORT}/docs" echo "" echo -e "${YELLOW}前端服务:${NC}" pid="$(read_pid "$FRONTEND_PID_FILE")" if is_pid_running "$pid"; then frontend_running=true success " 状态: 运行中" echo " PID: $pid" else port_listener="$(port_pid "$FRONTEND_PORT")" if [ -n "$port_listener" ]; then frontend_running=true success " 状态: 运行中 (无 PID 文件)" echo " PID: $port_listener" else error " 状态: 已停止" fi fi echo " 模式: $FRONTEND_MODE" echo " 地址: http://localhost:$FRONTEND_PORT" echo "" echo -e "${YELLOW}Docker 服务:${NC}" if command -v docker > /dev/null 2>&1; then local container for container in $DOCKER_CONTAINERS; do if docker ps --format '{{.Names}}' | grep -q "^${container}$"; then success " ${container}: 运行中" elif docker ps -a --format '{{.Names}}' | grep -q "^${container}$"; then warn " ${container}: 已停止" else warn " ${container}: 未创建" fi done else warn " Docker 未安装,已跳过" fi echo "" if [ "$api_running" = true ] && [ "$frontend_running" = true ]; then success "所有核心服务均在运行" else warn "存在未运行的服务,可使用 ./dev.sh start 启动" fi } run_logs() { local target="${1:-}" local follow="${2:-}" local log_file case "$target" in api) log_file="$API_LOG_FILE" ;; frontend) log_file="$FRONTEND_LOG_FILE" ;; *) die "请指定日志类型: api 或 frontend" ;; esac [ -f "$log_file" ] || die "日志文件不存在: $log_file" if [ "$follow" = "--follow" ]; then tail -f "$log_file" else tail -n 50 "$log_file" fi } show_help() { cat <<'EOF' AI+合规智能中枢统一脚本 用法: ./dev.sh help ./dev.sh setup ./dev.sh start [all|api|frontend] [--foreground] [--mode dev|static] ./dev.sh stop [all|api|frontend] ./dev.sh restart [all|api|frontend] [--mode dev|static] ./dev.sh status ./dev.sh logs [--follow] 命令说明: help 输出完整帮助信息、默认端口、日志目录和常见示例。 setup 进行一次性的本地初始化。 包含 Python 版本检查、.venv 虚拟环境创建、后端依赖安装、前端 npm install、 以及 Docker 基础容器状态检查。 start 启动服务。默认行为等同于 ./dev.sh start all。 可选目标: all 同时启动 API 和前端。 api 只启动后端 API。 frontend 只启动前端。 可选参数: --foreground 仅对 start api 生效,前台运行并开启 --reload,便于调试。 --mode dev 前端使用 Vite 开发服务器,默认端口 5173。 --mode static 前端先执行 npm run build,再以静态文件服务器方式启动。 stop 停止服务。默认行为等同于 ./dev.sh stop all。 会优先读取 logs/*.pid,PID 文件失效时会回退到端口探测。 restart 先停止再启动,支持 all/api/frontend。 restart frontend --mode static 可直接切换前端启动模式。 status 查看 API、前端、Docker 基础容器的状态。 API 状态包含健康检查;前端状态包含当前模式和访问地址。 logs 查看日志。默认输出最后 50 行。 追加 --follow 后会持续跟踪日志输出。 默认约定: API_HOST 默认 0.0.0.0 API_PORT 默认 8000 FRONTEND_PORT 默认 5173 FRONTEND_MODE 默认 dev 日志目录 logs/ PID 文件 logs/api.pid, logs/frontend.pid 常用示例: ./dev.sh setup ./dev.sh start ./dev.sh start api --foreground ./dev.sh start frontend --mode static ./dev.sh restart frontend --mode dev ./dev.sh status ./dev.sh logs api --follow ./dev.sh logs frontend EOF } parse_target() { local default_target="$1" local candidate="${2:-}" case "$candidate" in all|api|frontend) echo "$candidate" ;; *) echo "$default_target" ;; esac } main() { local command="${1:-help}" local target="all" local mode="" local foreground=false local log_target="" shift || true load_env case "$command" in help|-h|--help) show_help ;; setup) run_setup ;; start) target="$(parse_target all "${1:-}")" if [ "$target" != "all" ]; then shift || true fi while [ $# -gt 0 ]; do case "$1" in --foreground) foreground=true ;; --mode) shift || die "--mode 需要指定 dev 或 static" mode="$1" validate_frontend_mode "$mode" ;; *) die "未知参数: $1" ;; esac shift || true done case "$target" in all) [ "$foreground" = false ] || die "start all 不支持 --foreground,请使用 start api --foreground" print_header "AI+合规智能中枢 - 启动服务" start_api background start_frontend "${mode:-$FRONTEND_MODE}" ;; api) if [ "$foreground" = true ]; then start_api foreground else print_header "AI+合规智能中枢 - 启动 API" start_api background fi ;; frontend) print_header "AI+合规智能中枢 - 启动前端" start_frontend "${mode:-$FRONTEND_MODE}" ;; esac ;; stop) target="$(parse_target all "${1:-}")" print_header "AI+合规智能中枢 - 停止服务" case "$target" in all) stop_frontend stop_api ;; api) stop_api ;; frontend) stop_frontend ;; esac ;; restart) target="$(parse_target all "${1:-}")" if [ "$target" != "all" ]; then shift || true fi while [ $# -gt 0 ]; do case "$1" in --mode) shift || die "--mode 需要指定 dev 或 static" mode="$1" validate_frontend_mode "$mode" ;; *) die "未知参数: $1" ;; esac shift || true done print_header "AI+合规智能中枢 - 重启服务" case "$target" in all) stop_frontend stop_api start_api background start_frontend "${mode:-$FRONTEND_MODE}" ;; api) stop_api start_api background ;; frontend) stop_frontend start_frontend "${mode:-$FRONTEND_MODE}" ;; esac ;; status) run_status ;; logs) log_target="${1:-}" run_logs "$log_target" "${2:-}" ;; *) die "未知命令: $command。可使用 ./dev.sh help 查看帮助。" ;; esac } main "$@"