#!/bin/bash #=========================================================================== # AIOps 智能运维平台 - 统一启动脚本 # 支持三种运行模式:本地开发 / Docker容器部署 / 独立二进制 # # 用法: # ./run.sh dev 本地开发模式(前后端热重载) # ./run.sh docker up Docker Compose 容器部署 # ./run.sh build 编译独立二进制 ./aiops # ./run.sh binary 编译并启动独立二进制 # ./run.sh help 查看帮助 #=========================================================================== set -e # ============================================================ # 配置区 # ============================================================ PROJECT_NAME="AIOps 智能运维平台" BACKEND_PORT=8080 FRONTEND_PORT=5173 BACKEND_DIR="backend" FRONTEND_DIR="frontend" BACKEND_BIN="aiops-server" STANDALONE_BIN="aiops" # 日志配置 MAX_LOG_FILES=3 MAX_LOG_SIZE=$((50 * 1024 * 1024)) # 50MB # ============================================================ # 通用函数 # ============================================================ RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' CYAN='\033[0;36m' NC='\033[0m' info() { echo -e "${BLUE}[INFO]${NC} $1"; } success() { echo -e "${GREEN}[OK]${NC} $1"; } warning() { echo -e "${YELLOW}[WARN]${NC} $1"; } error() { echo -e "${RED}[ERR]${NC} $1"; } banner() { echo -e "${CYAN}$1${NC}"; } SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" cd "$SCRIPT_DIR" # PID 文件 BACKEND_PID_FILE="$SCRIPT_DIR/$BACKEND_DIR/.backend.pid" FRONTEND_PID_FILE="$SCRIPT_DIR/$FRONTEND_DIR/.frontend.pid" # 检测操作系统 detect_os() { case "$(uname -s)" in Darwin) echo "macos" ;; Linux) echo "linux" ;; *) echo "unknown" ;; esac } OS=$(detect_os) # 检查端口是否被占用 check_port() { if [ "$OS" = "macos" ]; then lsof -ti:$1 2>/dev/null | head -1 else ss -tlnp 2>/dev/null | grep -q ":$1 " && echo "in-use" || echo "" fi } # 释放端口 kill_port() { local port=$1 if [ "$OS" = "macos" ]; then local pids=$(lsof -ti:$port 2>/dev/null || echo "") [ -n "$pids" ] && echo "$pids" | xargs kill -9 2>/dev/null || true else fuser -k ${port}/tcp 2>/dev/null || true fi sleep 0.5 } # 日志轮转 rotate_log() { local log_file=$1 [ ! -f "$log_file" ] && return 0 local file_size if [ "$OS" = "macos" ]; then file_size=$(stat -f%z "$log_file" 2>/dev/null || echo "0") else file_size=$(stat -c%s "$log_file" 2>/dev/null || echo "0") fi if [ "$file_size" -gt "$MAX_LOG_SIZE" ]; then [ -f "${log_file}.${MAX_LOG_FILES}" ] && rm -f "${log_file}.${MAX_LOG_FILES}" for i in $(seq $((MAX_LOG_FILES - 1)) -1 1); do [ -f "${log_file}.${i}" ] && mv "${log_file}.${i}" "${log_file}.$((i + 1))" done mv "$log_file" "${log_file}.1" info "日志已轮转: $log_file" fi } # ============================================================ # 后端服务(本地开发模式) # ============================================================ start_backend() { info "启动后端服务..." cd "$SCRIPT_DIR/$BACKEND_DIR" mkdir -p logs rotate_log "logs/server.log" kill_port $BACKEND_PORT export GIN_MODE=debug info "编译 Go 程序..." CGO_ENABLED=1 go build -o "$BACKEND_BIN" ./cmd/ 2>&1 || { error "编译失败" return 1 } nohup ./"$BACKEND_BIN" >> logs/server.log 2>&1 & echo $! > "$BACKEND_PID_FILE" # 等待服务启动 local retries=0 while ! curl -s "http://localhost:$BACKEND_PORT/health" > /dev/null 2>&1; do retries=$((retries + 1)) if [ $retries -ge 30 ]; then error "后端启动超时,查看日志: $BACKEND_DIR/logs/server.log" tail -20 logs/server.log 2>/dev/null return 1 fi sleep 1 done success "后端已启动 (PID: $(cat "$BACKEND_PID_FILE"), 端口: $BACKEND_PORT)" return 0 } stop_backend() { if [ -f "$BACKEND_PID_FILE" ]; then local pid=$(cat "$BACKEND_PID_FILE" 2>/dev/null || echo "") if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then kill "$pid" 2>/dev/null || true success "后端已停止" fi rm -f "$BACKEND_PID_FILE" fi kill_port $BACKEND_PORT } # ============================================================ # 前端服务(本地开发模式) # ============================================================ start_frontend() { info "启动前端服务..." cd "$SCRIPT_DIR/$FRONTEND_DIR" mkdir -p logs rotate_log "logs/dev.log" kill_port $FRONTEND_PORT if [ ! -d "node_modules" ]; then warning "安装前端依赖..." npm install fi nohup npm run dev >> logs/dev.log 2>&1 & echo $! > "$FRONTEND_PID_FILE" local retries=0 while ! curl -s "http://localhost:$FRONTEND_PORT" > /dev/null 2>&1; do retries=$((retries + 1)) if [ $retries -ge 30 ]; then error "前端启动超时,查看日志: $FRONTEND_DIR/logs/dev.log" tail -20 logs/dev.log 2>/dev/null return 1 fi sleep 1 done success "前端已启动 (PID: $(cat "$FRONTEND_PID_FILE"), 端口: $FRONTEND_PORT)" return 0 } stop_frontend() { if [ -f "$FRONTEND_PID_FILE" ]; then local pid=$(cat "$FRONTEND_PID_FILE" 2>/dev/null || echo "") if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then kill "$pid" 2>/dev/null || true success "前端已停止" fi rm -f "$FRONTEND_PID_FILE" fi kill_port $FRONTEND_PORT } # ============================================================ # 本地开发命令 # ============================================================ cmd_dev() { banner "========== $PROJECT_NAME (本地开发) ==========" # 检查依赖 if ! command -v go &> /dev/null; then error "Go 未安装,请先安装 Go 1.21+" exit 1 fi if ! command -v npm &> /dev/null; then error "npm 未安装,请先安装 Node.js 20+" exit 1 fi if start_backend && start_frontend; then echo "" banner "========== 所有服务已启动 ==========" info "前端地址: http://localhost:$FRONTEND_PORT" info "后端地址: http://localhost:$BACKEND_PORT" info "健康检查: http://localhost:$BACKEND_PORT/health" info "数据库: SQLite (backend/sqlite3.db)" echo "" info "使用 './run.sh stop' 停止服务" else error "服务启动失败" exit 1 fi } cmd_stop() { info "停止本地服务..." stop_backend stop_frontend success "所有本地服务已停止" } cmd_restart() { cmd_stop sleep 1 cmd_dev } cmd_status() { info "本地服务状态:" if [ -n "$(check_port $BACKEND_PORT)" ]; then success "后端: 运行中 (端口 $BACKEND_PORT)" else warning "后端: 未运行" fi if [ -n "$(check_port $FRONTEND_PORT)" ]; then success "前端: 运行中 (端口 $FRONTEND_PORT)" else warning "前端: 未运行" fi if [ -f "$SCRIPT_DIR/$BACKEND_DIR/sqlite3.db" ]; then local db_size=$(du -h "$SCRIPT_DIR/$BACKEND_DIR/sqlite3.db" | cut -f1) info "数据库: SQLite (sqlite3.db, $db_size)" fi } cmd_logs() { info "实时日志 (Ctrl+C 退出):" echo "" local pids=() if [ -f "$SCRIPT_DIR/$BACKEND_DIR/logs/server.log" ]; then echo "=== 后端日志 ===" tail -f "$SCRIPT_DIR/$BACKEND_DIR/logs/server.log" & pids+=($!) fi if [ -f "$SCRIPT_DIR/$FRONTEND_DIR/logs/dev.log" ]; then echo "=== 前端日志 ===" tail -f "$SCRIPT_DIR/$FRONTEND_DIR/logs/dev.log" & pids+=($!) fi trap "kill ${pids[*]} 2>/dev/null; exit 0" INT TERM wait } # ============================================================ # Docker Compose 部署 # ============================================================ cmd_docker() { local action=${1:-help} if ! command -v docker &> /dev/null; then error "Docker 未安装,请先安装 Docker" exit 1 fi case "$action" in up) banner "========== Docker Compose 启动 ==========" docker compose up -d --build echo "" info "等待服务就绪..." sleep 5 docker compose ps echo "" info "前端地址: http://localhost:80" info "后端地址: http://localhost:8080" info "健康检查: http://localhost:8080/health" ;; down) info "停止 Docker Compose..." docker compose down success "容器已停止" ;; build) info "构建 Docker 镜像..." docker compose build success "镜像构建完成" ;; restart) info "重启 Docker Compose..." docker compose restart success "容器已重启" ;; logs) info "Docker 实时日志 (Ctrl+C 退出):" docker compose logs -f ;; ps|status) docker compose ps ;; help|--help|-h) echo "Docker Compose 部署命令:" echo " ./run.sh docker up 构建并启动所有容器" echo " ./run.sh docker down 停止并移除容器" echo " ./run.sh docker build 仅构建镜像" echo " ./run.sh docker restart 重启容器" echo " ./run.sh docker logs 查看实时日志" echo " ./run.sh docker ps 查看容器状态" ;; *) error "未知 docker 子命令: $action" cmd_docker help exit 1 ;; esac } # ============================================================ # 编译独立二进制 # ============================================================ cmd_build() { local target=${1:-binary} case "$target" in binary|aiops) banner "========== 编译独立二进制 ==========" cd "$SCRIPT_DIR/$BACKEND_DIR" # 确保 config.yaml 存在(二进制模式默认 SQLite) if [ ! -f "config.yaml" ]; then info "生成默认 config.yaml (SQLite + 无Redis)..." cat > config.yaml << 'YAML' mode: "release" port: "8080" log_level: "info" database: driver: "sqlite" host: "localhost" port: 3306 user: "" password: "" dbname: "data/aiops.db" charset: "utf8mb4" max_conn: 100 max_idle: 10 redis: host: "localhost" port: 6379 password: "" db: 0 jwt: secret: "CHANGE_ME_JWT_SECRET" # ⚠️ 生产环境必须替换 expire_hour: 24 websocket: read_buffer_size: 1024 write_buffer_size: 1024 YAML fi # 静态编译(CGO_ENABLED=1 因为 SQLite 需要 CGO) info "编译中..." CGO_ENABLED=1 GOPROXY=https://goproxy.cn,direct GONOSUMCHECK='*' GOTOOLCHAIN=go1.22.2 go build -ldflags="-w -s" -o "$STANDALONE_BIN" ./cmd/ if [ -f "$STANDALONE_BIN" ]; then local bin_size=$(du -h "$STANDALONE_BIN" | cut -f1) success "编译完成: $BACKEND_DIR/$STANDALONE_BIN ($bin_size)" echo "" info "使用方法:" echo " ./$BACKEND_DIR/$STANDALONE_BIN # 默认启动 (SQLite, 端口 8080)" echo " ./$BACKEND_DIR/$STANDALONE_BIN --port 9090 # 自定义端口" echo " ./$BACKEND_DIR/$STANDALONE_BIN --db-driver mysql \\" echo " --db-host localhost --db-user root \\" echo " --db-password pass --db-name aiops # MySQL 模式" echo " ./$BACKEND_DIR/$STANDALONE_BIN --config config.yaml # 使用配置文件" else error "编译失败" exit 1 fi ;; agent) banner "========== 编译 Agent ==========" cd "$SCRIPT_DIR/agent" info "编译 Linux AMD64..." GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -ldflags="-w -s" -o aiops-agent-linux-amd64 . success "aiops-agent-linux-amd64 ($(du -h aiops-agent-linux-amd64 | cut -f1))" info "编译 Linux ARM64..." GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build -ldflags="-w -s" -o aiops-agent-linux-arm64 . success "aiops-agent-linux-arm64 ($(du -h aiops-agent-linux-arm64 | cut -f1))" if [ "$OS" = "macos" ]; then info "编译 macOS (当前平台)..." CGO_ENABLED=0 go build -ldflags="-w -s" -o aiops-agent . success "aiops-agent ($(du -h aiops-agent | cut -f1))" fi ;; frontend) info "构建前端生产包..." cd "$SCRIPT_DIR/$FRONTEND_DIR" npm run build success "前端构建完成: $FRONTEND_DIR/dist/" ;; help|--help|-h) echo "编译命令:" echo " ./run.sh build 编译独立二进制 aiops (默认 SQLite)" echo " ./run.sh build agent 交叉编译 Agent (amd64+arm64)" echo " ./run.sh build frontend 构建前端生产包" ;; *) error "未知构建目标: $target" cmd_build help exit 1 ;; esac } # ============================================================ # 独立二进制模式 # ============================================================ cmd_binary() { cd "$SCRIPT_DIR/$BACKEND_DIR" # 先编译(如果不存在) if [ ! -f "$STANDALONE_BIN" ]; then info "二进制不存在,先编译..." cmd_build binary fi banner "========== 启动独立二进制 ==========" mkdir -p data logs uploads # 传递额外参数 ./"$STANDALONE_BIN" "$@" } # ============================================================ # 帮助 # ============================================================ cmd_help() { echo "" banner " $PROJECT_NAME 启动脚本" echo "" echo -e " ${GREEN}本地开发${NC}" echo " ./run.sh dev 启动前后端开发服务 (SQLite, 端口 8080+5173)" echo " ./run.sh stop 停止本地服务" echo " ./run.sh restart 重启本地服务" echo " ./run.sh status 查看本地服务状态" echo " ./run.sh logs 查看实时日志" echo "" echo -e " ${CYAN}Docker 容器部署${NC}" echo " ./run.sh docker up 构建并启动所有容器 (MySQL+Redis+Nginx)" echo " ./run.sh docker down 停止并移除容器" echo " ./run.sh docker build 仅构建镜像" echo " ./run.sh docker restart 重启容器" echo " ./run.sh docker logs 查看容器日志" echo " ./run.sh docker ps 查看容器状态" echo "" echo -e " ${YELLOW}编译 & 独立二进制${NC}" echo " ./run.sh build 编译独立二进制 ./$BACKEND_DIR/$STANDALONE_BIN" echo " ./run.sh binary 编译+启动独立二进制" echo " ./run.sh build agent 交叉编译 Agent (amd64+arm64)" echo " ./run.sh build frontend 构建前端生产包" echo "" echo -e " ${BLUE}独立二进制用法${NC}" echo " ./$BACKEND_DIR/$STANDALONE_BIN 默认启动 (SQLite, 端口 8080)" echo " ./$BACKEND_DIR/$STANDALONE_BIN --port 9090 自定义端口" echo " ./$BACKEND_DIR/$STANDALONE_BIN --config config.yaml 使用配置文件" echo " ./$BACKEND_DIR/$STANDALONE_BIN --db-driver mysql \\" echo " --db-host 127.0.0.1 --db-user root \\" echo " --db-password pass --db-name aiops MySQL 模式" echo "" echo -e " ${GREEN}快捷操作${NC}" echo " ./run.sh 等同于 ./run.sh dev" echo " ./run.sh help 显示此帮助" echo "" } # ============================================================ # 主入口 # ============================================================ main() { local cmd=${1:-dev} case "$cmd" in # 本地开发 dev|start) cmd_dev ;; stop) cmd_stop ;; restart) cmd_restart ;; status) cmd_status ;; logs) cmd_logs ;; # Docker Compose docker|compose) shift cmd_docker "$@" ;; # 编译 build) shift cmd_build "$@" ;; # 独立二进制 binary|bin) shift cmd_binary "$@" ;; # 帮助 help|--help|-h) cmd_help ;; *) error "未知命令: $cmd" cmd_help exit 1 ;; esac } main "$@"