#!/bin/bash #=========================================================================== # AIOps 智能运维平台 - 统一启动脚本 # 支持三种部署方式 + 本地开发模式: # # 方式 1 — Docker Compose: # ./run.sh docker up 构建+启动 (MySQL+Redis+Backend+Nginx) # # 方式 2 — Kubernetes (embed 单容器): # ./run.sh kubernetes deploy 构建镜像+全量部署 (embed 前端嵌入后端) # ./run.sh kubernetes upgrade 重新构建+滚动更新 # ./run.sh kubernetes status 查看资源状态 # ./run.sh kubernetes logs/exec 日志/进入容器 # # 方式 3 — 独立二进制 (默认 --mode local, SQLite + redka): # ./run.sh build 编译独立二进制 (embed 前端) # ./run.sh binary upgrade 停服 → 清旧产物 → 重编译 → 后台启动 # ./run.sh binary start|stop|status # # 本地开发: # ./run.sh dev 前后端热重载 # ./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' MAGENTA='\033[0;35m' 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" BINARY_PID_FILE="$SCRIPT_DIR/$BACKEND_DIR/.aiops.pid" BINARY_ARGS_FILE="$SCRIPT_DIR/$BACKEND_DIR/.aiops.args" # 检测操作系统 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() { ensure_go_path || return 1 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" . 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 } # ============================================================ # 构建清理(三种部署方式共用,避免旧代码/旧静态资源残留)// Logan.Li # ============================================================ clean_frontend_artifacts() { info "清理前端旧构建产物..." rm -rf "$SCRIPT_DIR/$FRONTEND_DIR/dist" rm -rf "$SCRIPT_DIR/$FRONTEND_DIR/node_modules/.vite" } clean_embed_static() { info "清理 embed static 旧文件..." mkdir -p "$SCRIPT_DIR/$BACKEND_DIR/static" find "$SCRIPT_DIR/$BACKEND_DIR/static" -mindepth 1 -delete 2>/dev/null || rm -rf "$SCRIPT_DIR/$BACKEND_DIR/static"/* } clean_go_artifacts() { info "清理 Go 旧编译产物..." rm -f "$SCRIPT_DIR/$BACKEND_DIR/$STANDALONE_BIN" rm -f "$SCRIPT_DIR/$BACKEND_DIR/$BACKEND_BIN" } # 构建前端并同步到 backend/static(二进制 / embed 镜像共用)// Logan.Li build_frontend_embed() { clean_frontend_artifacts cd "$SCRIPT_DIR/$FRONTEND_DIR" if [ ! -d "node_modules" ]; then npm install --registry=https://registry.npmmirror.com fi npm run build 2>&1 | tail -8 cd "$SCRIPT_DIR" clean_embed_static cp -r "$FRONTEND_DIR/dist/"* "$BACKEND_DIR/static/" local fe_files fe_files=$(find "$BACKEND_DIR/static" -type f | wc -l | tr -d ' ') success "前端已嵌入 static/ ($fe_files 个文件)" } # 停止独立二进制进程(PID 文件 + 端口双保险)// Logan.Li stop_binary_process() { local port=${1:-8080} if [ -f "$BINARY_PID_FILE" ]; then local pid pid=$(cat "$BINARY_PID_FILE" 2>/dev/null || echo "") if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then kill "$pid" 2>/dev/null || true sleep 1 kill -9 "$pid" 2>/dev/null || true success "已停止二进制进程 (PID: $pid)" fi rm -f "$BINARY_PID_FILE" fi # 兜底:按端口和进程名清理残留 if [ "$OS" = "macos" ]; then local pids 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 pkill -f "${SCRIPT_DIR}/${BACKEND_DIR}/${STANDALONE_BIN}" 2>/dev/null || true pkill -f "./${STANDALONE_BIN} --port" 2>/dev/null || true fi sleep 0.5 } wait_binary_ready() { local port=${1:-8080} local retries=0 while ! curl -sf "http://localhost:${port}/version" > /dev/null 2>&1; do retries=$((retries + 1)) if [ $retries -ge 30 ]; then error "二进制启动超时,查看日志: $BACKEND_DIR/logs/aiops.log" tail -20 "$SCRIPT_DIR/$BACKEND_DIR/logs/aiops.log" 2>/dev/null || true return 1 fi sleep 1 done return 0 } # 确保二进制默认 config.yaml 存在 // Logan.Li ensure_binary_config() { local cfg="$SCRIPT_DIR/$BACKEND_DIR/config.yaml" if [ -f "$cfg" ]; then return 0 fi info "生成默认 config.yaml (local 模式: SQLite + redka 本地缓存)..." cat > "$cfg" << 'YAML' mode: "local" port: "8080" log_level: "info" database: driver: "sqlite" host: "localhost" port: 3306 user: "" password: "" dbname: "data/sqlite3.db" charset: "utf8mb4" max_conn: 100 max_idle: 10 redis: host: "localhost" port: 6379 driver: "redka" password: "" db: 0 jwt: secret: "aiops-local-dev-secret-change-in-production" expire_hour: 24 websocket: read_buffer_size: 1024 write_buffer_size: 1024 YAML } # 非交互 shell 常缺 Go PATH,补充常见安装目录 // Logan.Li ensure_go_path() { if command -v go &>/dev/null; then return 0 fi export PATH="/usr/local/go/bin:$HOME/go/bin:/usr/lib/go/bin:$PATH" if ! command -v go &>/dev/null; then error "Go 未安装或不在 PATH,请先安装 Go 1.24+" return 1 fi } # 编译独立二进制(含 embed 前端)// Logan.Li build_standalone_binary() { ensure_go_path || return 1 build_frontend_embed ensure_binary_config clean_go_artifacts cd "$SCRIPT_DIR/$BACKEND_DIR" info "编译中..." CGO_ENABLED=1 GOPROXY=https://goproxy.cn,direct GONOSUMCHECK='*' GOTOOLCHAIN=go1.24.0 \ go build -ldflags="-w -s" -o "$STANDALONE_BIN" . if [ ! -f "$STANDALONE_BIN" ]; then error "编译失败" return 1 fi local bin_size bin_size=$(du -h "$STANDALONE_BIN" | cut -f1) success "编译完成: $BACKEND_DIR/$STANDALONE_BIN ($bin_size)" cd "$SCRIPT_DIR" return 0 } # 解析/持久化二进制启动参数 // Logan.Li save_binary_args() { printf '%s\n' "$@" > "$BINARY_ARGS_FILE" } # 将旧版 --db-driver sqlite 参数迁移为 --mode local // Logan.Li migrate_binary_args_file() { [ -f "$BINARY_ARGS_FILE" ] || return 0 if grep -qE '^--mode ' "$BINARY_ARGS_FILE" 2>/dev/null; then return 0 fi if ! grep -qE '^--db-driver' "$BINARY_ARGS_FILE" 2>/dev/null; then return 0 fi local port="8080" local lines=() while IFS= read -r line || [ -n "$line" ]; do [ -n "$line" ] && lines+=("$line") done < "$BINARY_ARGS_FILE" local i=0 while [ $i -lt ${#lines[@]} ]; do if [ "${lines[$i]}" = "--port" ] && [ $((i + 1)) -lt ${#lines[@]} ]; then port="${lines[$((i + 1))]}" break fi if [[ "${lines[$i]}" == --port=* ]]; then port="${lines[$i]#--port=}" break fi i=$((i + 1)) done info "迁移启动参数: --db-driver → --mode local --port $port" save_binary_args --mode local --port "$port" } load_binary_args() { migrate_binary_args_file local -n _out=$1 _out=() if [ -f "$BINARY_ARGS_FILE" ]; then while IFS= read -r line || [ -n "$line" ]; do [ -n "$line" ] && _out+=("$line") done < "$BINARY_ARGS_FILE" fi if [ ${#_out[@]} -eq 0 ]; then _out=(--mode local --port 8080) fi } # 升级后校验 /version 驱动信息 // Logan.Li verify_binary_version() { local port=${1:-8080} local body body=$(curl -sf "http://localhost:${port}/version" 2>/dev/null) || { warning "/version 未响应" return 1 } info "版本探测: $body" echo "$body" | grep -q '"mode":"local"' && success "mode=local 已生效" || warning "mode 非 local" echo "$body" | grep -q '"db_driver":"sqlite"' && success "db_driver=sqlite 已生效" || true echo "$body" | grep -q '"redis_driver":"redka"' && success "redis_driver=redka 已生效" || true return 0 } binary_listen_port() { local args=("$@") local i=0 while [ $i -lt ${#args[@]} ]; do if [ "${args[$i]}" = "--port" ] && [ $((i + 1)) -lt ${#args[@]} ]; then echo "${args[$((i + 1))]}" return 0 fi i=$((i + 1)) done echo "8080" } # 后台启动独立二进制 // Logan.Li start_binary_daemon() { local args=("$@") local port port=$(binary_listen_port "${args[@]}") cd "$SCRIPT_DIR/$BACKEND_DIR" mkdir -p data logs uploads rotate_log "logs/aiops.log" stop_binary_process "$port" nohup ./"$STANDALONE_BIN" "${args[@]}" >> logs/aiops.log 2>&1 & echo $! > "$BINARY_PID_FILE" save_binary_args "${args[@]}" if wait_binary_ready "$port"; then success "二进制已启动 (PID: $(cat "$BINARY_PID_FILE"), 端口: $port)" info "日志: $BACKEND_DIR/logs/aiops.log" verify_binary_version "$port" || true return 0 fi return 1 } # ============================================================ # Docker Compose 部署 # ============================================================ # 检测 Docker Compose 命令 (兼容 docker compose 插件和 docker-compose 独立二进制) detect_compose_cmd() { if docker compose version &>/dev/null 2>&1; then echo "docker compose" elif command -v docker-compose &>/dev/null; then echo "docker-compose" else echo "" fi } cmd_docker() { local action=${1:-help} if ! command -v docker &> /dev/null; then error "Docker 未安装,请先安装 Docker" exit 1 fi DOCKER_COMPOSE=$(detect_compose_cmd) if [ -z "$DOCKER_COMPOSE" ]; then error "Docker Compose 未安装 (需要 docker compose 插件或 docker-compose)" exit 1 fi # 自动从 git tag 获取版本号 GIT_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "") BUILD_TIME=$(date -u +%Y-%m-%dT%H:%M:%SZ) export VERSION="${GIT_TAG:-dev}" export BUILD_TIME info "使用: $DOCKER_COMPOSE" info "版本: $VERSION (构建时间: $BUILD_TIME)" # 启用 BuildKit 加速构建(cache mount 跨构建复用 go mod/apk 缓存)// Logan.Li export DOCKER_BUILDKIT=1 export COMPOSE_DOCKER_CLI_BUILD=1 info "BuildKit: 已启用 (增量编译 + 缓存复用)" case "$action" in up|upgrade|update) local fresh_build=0 if [ "${2:-}" = "fresh" ] || [ "$action" = "fresh" ]; then fresh_build=1 fi banner "========== Docker Compose 启动/升级 ==========" clean_frontend_artifacts if [ "$fresh_build" -eq 1 ]; then info "全量重建镜像 (--no-cache)..." $DOCKER_COMPOSE build --no-cache $DOCKER_COMPOSE up -d --force-recreate --remove-orphans else info "增量构建 + 强制重建容器..." $DOCKER_COMPOSE up -d --build --force-recreate --remove-orphans fi echo "" # 清理历史 build cache 和悬空镜像,防止占满磁盘 info "清理历史构建缓存..." local before_prune=$(df -h / | awk 'NR==2{print $5}') docker builder prune -f 2>/dev/null || true docker image prune -f 2>/dev/null || true local after_prune=$(df -h / | awk 'NR==2{print $5}') info "磁盘使用: ${before_prune} → ${after_prune}" 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 ;; clean|prune) banner "========== Docker 缓存清理 ==========" info "清理前磁盘使用:" echo " $(df -h / | awk 'NR==2{print $3, "used /", $2, "total ("$5")"}')" echo "" info "清理 dangling 镜像..." docker image prune -f 2>/dev/null || true info "清理构建缓存..." docker builder prune -f 2>/dev/null || true info "清理未使用的网络..." docker network prune -f 2>/dev/null || true echo "" info "清理后磁盘使用:" echo " $(df -h / | awk 'NR==2{print $3, "used /", $2, "total ("$5")"}')" success "清理完成" ;; fresh) cmd_docker up fresh ;; help|--help|-h) echo "Docker Compose 部署命令:" echo " ./run.sh docker up 增量构建 + 强制重建容器" echo " ./run.sh docker up fresh 无缓存全量重建镜像并启动" echo " ./run.sh docker upgrade 同 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 查看容器状态" echo " ./run.sh docker clean 清理构建缓存/无用镜像" ;; *) error "未知 docker 子命令: $action" cmd_docker help exit 1 ;; esac } # ============================================================ # 编译独立二进制 # ============================================================ cmd_build() { local target=${1:-binary} case "$target" in binary|aiops) banner "========== 编译独立二进制 ==========" build_standalone_binary || exit 1 echo "" info "使用方法:" echo " ./run.sh binary upgrade # 停服重编译并后台启动" echo " ./$BACKEND_DIR/$STANDALONE_BIN --port 8080 # 前台启动" echo " ./$BACKEND_DIR/$STANDALONE_BIN --mode local # 本地模式 (SQLite + redka)" echo " ./$BACKEND_DIR/$STANDALONE_BIN --config config.yaml # 使用配置文件" ;; 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() { local action="run" if [ $# -gt 0 ] && [[ "$1" =~ ^(upgrade|start|stop|status|help|--help|-h)$ ]]; then action=$1 shift fi case "$action" in upgrade|update) banner "========== 二进制升级部署 ==========" local args=() if [ $# -gt 0 ]; then args=("$@") else load_binary_args args fi local port port=$(binary_listen_port "${args[@]}") info "启动参数: ${args[*]}" stop_binary_process "$port" if [ -f "$SCRIPT_DIR/$BACKEND_DIR/$STANDALONE_BIN" ]; then local backup="$SCRIPT_DIR/$BACKEND_DIR/${STANDALONE_BIN}.bak.$(date +%Y%m%d%H%M%S)" cp "$SCRIPT_DIR/$BACKEND_DIR/$STANDALONE_BIN" "$backup" info "已备份旧二进制: $backup" fi build_standalone_binary || exit 1 start_binary_daemon "${args[@]}" || exit 1 ;; start) local args=() if [ $# -gt 0 ]; then args=("$@") else load_binary_args args fi if [ ! -f "$SCRIPT_DIR/$BACKEND_DIR/$STANDALONE_BIN" ]; then info "二进制不存在,先编译..." build_standalone_binary || exit 1 fi start_binary_daemon "${args[@]}" || exit 1 ;; stop) local port=${1:-8080} stop_binary_process "$port" success "二进制已停止" ;; status) local args=() load_binary_args args local port port=$(binary_listen_port "${args[@]}") info "监听端口: $port" if [ -f "$BINARY_PID_FILE" ]; then local pid pid=$(cat "$BINARY_PID_FILE" 2>/dev/null || echo "") if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then success "进程运行中 (PID: $pid)" else warning "PID 文件存在但进程未运行" fi else warning "未找到 PID 文件" fi if curl -sf "http://localhost:${port}/version" > /dev/null 2>&1; then success "HTTP 就绪: http://localhost:${port}/version" else warning "HTTP 未响应" fi ;; help|--help|-h) echo "独立二进制命令:" echo " ./run.sh binary upgrade [--port 8080] 停服→清旧→重编译→启动 (默认 --mode local)" echo " ./run.sh binary start [args...] 后台启动(参数会持久化)" echo " ./run.sh binary stop [port] 停止进程" echo " ./run.sh binary status 查看状态" echo " ./run.sh binary [--mode local --port 8080 ...] 前台运行 (SQLite + redka)" ;; run|*) cd "$SCRIPT_DIR/$BACKEND_DIR" if [ ! -f "$STANDALONE_BIN" ]; then info "二进制不存在,先编译..." build_standalone_binary || exit 1 fi banner "========== 前台启动独立二进制 ==========" mkdir -p data logs uploads if [ $# -gt 0 ]; then save_binary_args "$@" fi ./"$STANDALONE_BIN" "$@" ;; esac } # ============================================================ # Kubernetes 部署 (embed 单容器模式) # ============================================================ KUBERNETES_NAMESPACE="devops" KUBERNETES_MANIFEST_DIR="$SCRIPT_DIR/deploy/kubernetes" KUBERNETES_IMAGE="aiops:embed" KUBERNETES_NODE_PORT=30080 KUBERNETES_BUILT_IMAGE="" # 检测 kubectl 和 Docker check_kubernetes_deps() { if ! command -v kubectl &>/dev/null; then error "kubectl 未安装,请先安装 kubectl" exit 1 fi if ! kubectl cluster-info &>/dev/null 2>&1; then error "无法连接 Kubernetes 集群,请检查 kubeconfig" kubectl cluster-info 2>&1 | tail -3 exit 1 fi } # 检测容器运行时并加载镜像到集群 load_image_to_cluster() { local image=$1 info "加载镜像到集群..." # minikube if command -v minikube &>/dev/null && minikube status &>/dev/null 2>&1; then minikube image load "$image" 2>/dev/null && success "镜像已加载到 minikube" && return 0 fi # kind if command -v kind &>/dev/null && kind get clusters &>/dev/null 2>&1 | grep -q .; then kind load docker-image "$image" 2>/dev/null && success "镜像已加载到 kind" && return 0 fi # k3d if command -v k3d &>/dev/null; then k3d image import "$image" 2>/dev/null && success "镜像已加载到 k3d" && return 0 fi # Docker Desktop K8s / 远程仓库 — 镜像通过 registry 共享 warning "未检测到 minikube/kind/k3d,假设镜像通过 registry 共享" warning "如集群无法拉取镜像,请手动推送: docker push /$image" } # Kubernetes 构建镜像(upgrade 时无缓存 + 时间戳 tag 避免旧层残留)// Logan.Li cmd_kubernetes_build() { local no_cache=${1:-0} if ! command -v docker &>/dev/null; then error "Docker 未安装" exit 1 fi banner "========== 构建 embed 镜像 ==========" clean_frontend_artifacts clean_embed_static local version version=$(git describe --tags --abbrev=0 2>/dev/null || echo "dev") local build_time build_time=$(date -u +%Y-%m-%dT%H:%M:%SZ) local image_tag image_tag="${KUBERNETES_IMAGE}-$(date +%Y%m%d%H%M%S)" info "镜像: $KUBERNETES_IMAGE (+ $image_tag)" local build_args=( -f Dockerfile.embed --build-arg VERSION="$version" --build-arg BUILD_TIME="$build_time" -t "$KUBERNETES_IMAGE" -t "$image_tag" ) if [ "$no_cache" -eq 1 ]; then build_args+=(--no-cache --pull) info "无缓存全量构建 (--no-cache --pull)" fi build_args+=(.) info "构建中(前端 + 后端 embed)..." docker build "${build_args[@]}" || { error "镜像构建失败" exit 1 } local img_size img_size=$(docker images "$KUBERNETES_IMAGE" --format "{{.Size}}" | head -1) success "镜像构建完成: $KUBERNETES_IMAGE ($img_size)" load_image_to_cluster "$KUBERNETES_IMAGE" load_image_to_cluster "$image_tag" KUBERNETES_BUILT_IMAGE="$image_tag" } # 读取 deployment.yaml 中的 replicas(默认 1)// Logan.Li kubernetes_manifest_replicas() { local replicas replicas=$(awk '/^ replicas:/{print $2; exit}' "$KUBERNETES_MANIFEST_DIR/deployment.yaml" 2>/dev/null) echo "${replicas:-1}" } # 读取集群中 aiops Deployment 当前副本数 // Logan.Li kubernetes_live_replicas() { kubectl -n "$KUBERNETES_NAMESPACE" get deployment aiops -o jsonpath='{.spec.replicas}' 2>/dev/null } # 多副本时 SQLite 不共享,强制切换 MODE=release + 集群内 MySQL/Redis // Logan.Li ensure_cluster_mode_for_replicas() { local replicas=${1:-1} if [ "$replicas" -le 1 ]; then return 0 fi if ! kubectl -n "$KUBERNETES_NAMESPACE" get deployment aiops &>/dev/null; then warning "Deployment 尚未创建,跳过多副本模式切换(apply 后会自动处理)" return 0 fi local current_mode current_mode=$(kubectl -n "$KUBERNETES_NAMESPACE" get deployment aiops \ -o jsonpath='{.spec.template.spec.containers[0].env[?(@.name=="MODE")].value}' 2>/dev/null) if [ "$current_mode" = "release" ]; then info "副本 $replicas,已为 release 模式" return 0 fi warning "副本 $replicas > 1,SQLite 数据不共享,自动切换 MODE=release (mysql + redis)" kubectl -n "$KUBERNETES_NAMESPACE" set env deployment/aiops \ MODE=release \ DATABASE_DRIVER=mysql \ DATABASE_HOST=mysql \ DATABASE_PORT=3306 \ DATABASE_USER=aiops \ DATABASE_PASSWORD=aiops_pass_2026 \ DATABASE_DBNAME=aiops \ REDIS_DRIVER=redis \ REDIS_HOST=redis \ REDIS_PORT=6379 \ --overwrite success "已切换 release 模式,使用集群 MySQL/Redis" } # Kubernetes 部署 cmd_kubernetes() { local action=${1:-help} case "$action" in deploy|up) check_kubernetes_deps banner "========== Kubernetes 部署 ($KUBERNETES_NAMESPACE) ==========" # 1. 构建镜像 cmd_kubernetes_build # 2. 部署基础设施 info "部署 Namespace + Secret + ConfigMap..." kubectl apply -f "$KUBERNETES_MANIFEST_DIR/namespace.yaml" kubectl apply -f "$KUBERNETES_MANIFEST_DIR/secret.yaml" kubectl apply -f "$KUBERNETES_MANIFEST_DIR/configmap.yaml" info "部署 local 模式数据卷 (PVC)..." kubectl apply -f "$KUBERNETES_MANIFEST_DIR/pvc-aiops-data.yaml" info "部署 MySQL + Redis..." kubectl apply -f "$KUBERNETES_MANIFEST_DIR/mysql.yaml" kubectl apply -f "$KUBERNETES_MANIFEST_DIR/redis.yaml" # 3. 等待数据库就绪 info "等待 MySQL 就绪..." kubectl -n "$KUBERNETES_NAMESPACE" rollout status statefulset/mysql --timeout=120s 2>/dev/null || { warning "MySQL 启动较慢,继续部署应用(可在后台自行完成初始化)" } info "等待 Redis 就绪..." kubectl -n "$KUBERNETES_NAMESPACE" rollout status statefulset/redis --timeout=60s 2>/dev/null || true # 4. 部署应用 info "部署 AIOps 应用..." local target_replicas target_replicas=$(kubernetes_manifest_replicas) if [ "$target_replicas" -gt 1 ]; then warning "manifest replicas=$target_replicas,将自动切换 MODE=release" fi kubectl apply -f "$KUBERNETES_MANIFEST_DIR/deployment.yaml" ensure_cluster_mode_for_replicas "$target_replicas" kubectl -n "$KUBERNETES_NAMESPACE" rollout status deployment/aiops --timeout=180s 2>/dev/null || { warning "应用正在启动,查看日志: ./run.sh kubernetes logs" } # 5. Ingress(可选) if [ -f "$KUBERNETES_MANIFEST_DIR/ingress.yaml" ]; then info "部署 Ingress(如集群无 Ingress Controller 会跳过)..." kubectl apply -f "$KUBERNETES_MANIFEST_DIR/ingress.yaml" 2>/dev/null || true fi echo "" success "部署完成!" echo "" kubectl -n "$KUBERNETES_NAMESPACE" get pods,svc echo "" local node_ip=$(kubectl get nodes -o jsonpath='{.items[0].status.addresses[?(@.type=="InternalIP")].address}' 2>/dev/null) info "访问地址: http://${node_ip:-}:$KUBERNETES_NODE_PORT" info "健康检查: http://${node_ip:-}:$KUBERNETES_NODE_PORT/health" ;; upgrade|update) check_kubernetes_deps banner "========== Kubernetes 滚动更新 ==========" local live_replicas live_replicas=$(kubernetes_live_replicas) live_replicas=${live_replicas:-1} ensure_cluster_mode_for_replicas "$live_replicas" cmd_kubernetes_build 1 info "更新 Deployment 镜像: $KUBERNETES_BUILT_IMAGE" kubectl -n "$KUBERNETES_NAMESPACE" set image deployment/aiops aiops="$KUBERNETES_BUILT_IMAGE" --record kubectl -n "$KUBERNETES_NAMESPACE" rollout status deployment/aiops --timeout=180s info "清理悬空镜像..." docker image prune -f 2>/dev/null || true success "滚动更新完成" ;; delete|down|remove) check_kubernetes_deps banner "========== Kubernetes 清理 ($KUBERNETES_NAMESPACE) ==========" warning "这将删除 $KUBERNETES_NAMESPACE 命名空间中的所有资源" warning "MySQL/Redis 持久卷(PVC)需手动删除" echo "" read -p "确认删除?[y/N] " confirm if [ "$confirm" = "y" ] || [ "$confirm" = "Y" ]; then kubectl delete -f "$KUBERNETES_MANIFEST_DIR/" --ignore-not-found 2>/dev/null echo "" info "PVC 状态(需手动清理):" kubectl -n "$KUBERNETES_NAMESPACE" get pvc 2>/dev/null || echo " 无 PVC" success "清理完成" else info "已取消" fi ;; build|image) cmd_kubernetes_build ;; status|ps) check_kubernetes_deps banner "========== Kubernetes 状态 ($KUBERNETES_NAMESPACE) ==========" echo "" echo "--- Pods ---" kubectl -n "$KUBERNETES_NAMESPACE" get pods -o wide 2>/dev/null echo "" echo "--- Services ---" kubectl -n "$KUBERNETES_NAMESPACE" get svc 2>/dev/null echo "" echo "--- PVC ---" kubectl -n "$KUBERNETES_NAMESPACE" get pvc 2>/dev/null echo "" echo "--- Ingress ---" kubectl -n "$KUBERNETES_NAMESPACE" get ingress 2>/dev/null ;; logs|log) check_kubernetes_deps local follow=${2:--f} info "应用日志 (Ctrl+C 退出):" kubectl -n "$KUBERNETES_NAMESPACE" logs $follow deployment/aiops --tail=100 ;; exec|shell|sh) check_kubernetes_deps info "进入容器 Shell..." kubectl -n "$KUBERNETES_NAMESPACE" exec -it deployment/aiops -- bash || \ kubectl -n "$KUBERNETES_NAMESPACE" exec -it deployment/aiops -- sh ;; mysql|db) check_kubernetes_deps info "进入 MySQL..." kubectl -n "$KUBERNETES_NAMESPACE" exec -it mysql-0 -- mysql -uaiops -paiops_pass_2026 aiops ;; redis) check_kubernetes_deps info "进入 Redis CLI..." kubectl -n "$KUBERNETES_NAMESPACE" exec -it redis-0 -- redis-cli ;; scale) check_kubernetes_deps local replicas=${2:-1} if [ "$replicas" -gt 1 ]; then ensure_cluster_mode_for_replicas "$replicas" fi info "扩缩容到 $replicas 副本..." kubectl -n "$KUBERNETES_NAMESPACE" scale deployment/aiops --replicas="$replicas" ;; port-forward|pf) check_kubernetes_deps local local_port=${2:-8080} info "端口转发 localhost:$local_port → aiops:8080 (Ctrl+C 退出):" kubectl -n "$KUBERNETES_NAMESPACE" port-forward svc/aiops "$local_port":8080 ;; help|--help|-h) echo "Kubernetes 部署命令 (embed 单容器模式):" echo "" echo " ${GREEN}部署${NC}" echo " ./run.sh kubernetes deploy 构建镜像 + 全量部署" echo " ./run.sh kubernetes upgrade 重新构建镜像 + 滚动更新" echo " ./run.sh kubernetes delete 删除所有 Kubernetes 资源(保留 PVC)" echo "" echo " ${CYAN}运维${NC}" echo " ./run.sh kubernetes status 查看资源状态" echo " ./run.sh kubernetes logs 查看应用日志(默认 follow)" echo " ./run.sh kubernetes exec 进入容器 Shell" echo " ./run.sh kubernetes mysql 进入 MySQL CLI" echo " ./run.sh kubernetes redis 进入 Redis CLI" echo " ./run.sh kubernetes scale 3 扩缩容到 3 副本" echo " ./run.sh kubernetes port-forward 端口转发到本机 8080" echo "" echo " ${YELLOW}镜像${NC}" echo " ./run.sh kubernetes build 仅构建 embed 镜像" echo "" echo " 镜像: $KUBERNETES_IMAGE" echo " 命名空间: $KUBERNETES_NAMESPACE" echo " NodePort: $KUBERNETES_NODE_PORT" ;; *) error "未知 kubernetes 子命令: $action" cmd_kubernetes help exit 1 ;; esac } # ============================================================ # 帮助 # ============================================================ 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 增量构建 + 强制重建容器" echo " ./run.sh docker up fresh 无缓存全量重建并启动" echo " ./run.sh docker upgrade 同 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 查看容器状态" echo "" echo -e " ${MAGENTA}Kubernetes 部署 (embed 单容器)${NC}" echo " ./run.sh kubernetes deploy 构建镜像 + 全量部署到 Kubernetes" echo " ./run.sh kubernetes upgrade 重新构建 + 滚动更新" echo " ./run.sh kubernetes delete 删除所有 Kubernetes 资源" echo " ./run.sh kubernetes status 查看资源状态" echo " ./run.sh kubernetes logs 查看应用日志" echo " ./run.sh kubernetes exec 进入容器 Shell" echo " ./run.sh kubernetes mysql 进入 MySQL CLI" echo " ./run.sh kubernetes redis 进入 Redis CLI" echo " ./run.sh kubernetes scale 3 扩缩容" echo " ./run.sh kubernetes port-forward 端口转发到本机" echo " ./run.sh kubernetes build 仅构建 embed 镜像" echo "" echo -e " ${YELLOW}编译 & 独立二进制${NC}" echo " ./run.sh build 编译独立二进制 ./$BACKEND_DIR/$STANDALONE_BIN" echo " ./run.sh binary upgrade 停服→清旧→重编译→后台启动(生产升级)" echo " ./run.sh binary start|stop|status 进程管理" echo " ./run.sh binary [args] 前台启动(调试)" 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 "$@" ;; # Kubernetes 部署 (k8s 为别名) kubernetes|k8s) shift cmd_kubernetes "$@" ;; # 帮助 help|--help|-h) cmd_help ;; *) error "未知命令: $cmd" cmd_help exit 1 ;; esac } main "$@"