Files
Logan.Li f3a4fb569f
AIOps CI / Backend (test, vet, build) (push) Canceled after 0s
AIOps CI / Agent (test, vet, build) (push) Canceled after 0s
AIOps CI / Frontend (typecheck, lint, build) (push) Canceled after 0s
AIOps CI / API integration (SQLite + redka) (push) Canceled after 0s
fix(deploy): 全新部署链路健壮性(0004 幂等 + 迁移前置 + 空库守卫)
- 0004_metric_aggregate_unique 改为幂等:新库基线已由模型 uniqueIndex 建出
  uniq_metric_agg,再次 CREATE 会以 1061 重复键名中断 migrate up,
  导致 0005_agent_settings 建不出来(全新部署实测复现)
- 新增 database.IndexExists:跨 SQLite/MySQL/PostgreSQL 判断索引是否存在
- 新增 database.EnsureSchemaInitialized:服务启动遇到空库时明确失败并给出
  执行 aiops migrate up 的指引,避免「服务起来了但页面全是空数据」
- run.sh docker up/upgrade/update 先跑一次 aiops migrate up 再启动服务

// Logan.Li
2026-09-14 09:02:42 +00:00

1584 lines
54 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/bash
#===========================================================================
# AIOps 智能运维平台 - 统一启动脚本
# 支持三种部署方式 + 本地开发模式:
#
# 方式 1 — Docker Compose (embed 单应用容器):
# ./run.sh docker up 构建+启动 (MySQL+Redis+AIOps)
#
# 方式 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"
STANDALONE_BIN="aiops"
DOCKERHUB_IMAGE="lghost/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"
STANDALONE_BIN_PATH="$SCRIPT_DIR/$STANDALONE_BIN"
BINARY_PID_FILE="$SCRIPT_DIR/.aiops.pid"
BINARY_ARGS_FILE="$SCRIPT_DIR/.aiops.args"
LOGS_DIR="$SCRIPT_DIR/logs"
# 检测操作系统
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
}
# 显式指定根目录配置文件,避免 backend/ 目录下没有 config.yaml 时落到默认配置 // Logan.Li
nohup ./"$BACKEND_BIN" --config "$SCRIPT_DIR/config.yaml" >> 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 (./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/sqlite3.db" ]; then
local db_size=$(du -h "$SCRIPT_DIR/sqlite3.db" | cut -f1)
info "数据库: SQLite (./sqlite3.db, $db_size)"
elif [ -f "$SCRIPT_DIR/$BACKEND_DIR/sqlite3.db" ]; then
local db_size=$(du -h "$SCRIPT_DIR/$BACKEND_DIR/sqlite3.db" | cut -f1)
info "数据库: SQLite (backend/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/.vite"
rm -rf "$SCRIPT_DIR/$FRONTEND_DIR/node_modules/.vite"
rm -rf "$SCRIPT_DIR/$FRONTEND_DIR/node_modules/.cache"
rm -rf "$SCRIPT_DIR/$FRONTEND_DIR/node_modules/.tmp"
find "$SCRIPT_DIR/$FRONTEND_DIR" -maxdepth 3 -name "*.tsbuildinfo" -delete 2>/dev/null || true
}
clean_embed_static() {
info "清理 embed static 旧文件..."
# rm -rf + mkdir 确保 dotfiles 也被清除 // Logan.Li
rm -rf "$SCRIPT_DIR/$BACKEND_DIR/static"
mkdir -p "$SCRIPT_DIR/$BACKEND_DIR/static"
}
clean_go_artifacts() {
info "清理 Go 旧编译产物..."
rm -f "$STANDALONE_BIN_PATH"
rm -f "$SCRIPT_DIR/$BACKEND_DIR/$STANDALONE_BIN"
rm -f "$SCRIPT_DIR/$BACKEND_DIR/$BACKEND_BIN"
rm -f "$SCRIPT_DIR/$BACKEND_DIR"/aiops-*
}
# 全量清理(docker/build 前调用,确保零残留)// Logan.Li
clean_all_artifacts() {
clean_frontend_artifacts
clean_embed_static
clean_go_artifacts
success "构建产物清理完成"
}
# 构建前端并同步到 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 "${STANDALONE_BIN_PATH}" 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 "二进制启动超时,查看日志: $LOGS_DIR/aiops.log"
tail -20 "$LOGS_DIR/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/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: "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
}
# 判断参数是否像版本号 (v1.0.1 / 1.0.1) // Logan.Li
looks_like_version() {
local v=$1
[[ "$v" =~ ^v[0-9] ]] || [[ "$v" =~ ^[0-9]+\.[0-9] ]]
}
# 统一版本解析:显式参数 > VERSION 环境变量 > git describe > dev // Logan.Li
resolve_version() {
local explicit="${1:-}"
if [ -n "$explicit" ]; then
BUILD_VERSION="$explicit"
elif [ -n "${VERSION:-}" ]; then
BUILD_VERSION="$VERSION"
elif git describe --tags --always 2>/dev/null | grep -q .; then
BUILD_VERSION=$(git describe --tags --always 2>/dev/null)
else
BUILD_VERSION="dev"
fi
BUILD_TIME=$(date -u '+%Y-%m-%dT%H:%M:%SZ')
GIT_COMMIT=$(git rev-parse --short HEAD 2>/dev/null || echo "unknown")
EMBED_IMAGE="${DOCKERHUB_IMAGE}:${BUILD_VERSION}"
export VERSION="$BUILD_VERSION"
export BUILD_TIME
export GIT_COMMIT
}
# 兼容旧函数名 // Logan.Li
resolve_build_version() {
resolve_version "$@"
}
# 跨平台 sed -i // Logan.Li
sed_inplace() {
local expr=$1
local file=$2
if [ "$OS" = "macos" ]; then
sed -i '' "$expr" "$file"
else
sed -i "$expr" "$file"
fi
}
# 构建 embed Docker 镜像(二进制/K8s/Docker Compose 共用)// Logan.Li
build_embed_image() {
local version_tag="${1:-}"
local no_cache="${2:-0}"
local do_push="${3:-0}"
if ! command -v docker &>/dev/null; then
error "Docker 未安装"
return 1
fi
resolve_version "$version_tag"
banner "========== 构建 embed 镜像 ($EMBED_IMAGE) =========="
clean_frontend_artifacts
clean_embed_static
export DOCKERHUB_IMAGE
export DOCKER_BUILDKIT=1
export COMPOSE_DOCKER_CLI_BUILD=1
local compose_cmd
compose_cmd=$(detect_compose_cmd)
if [ -z "$compose_cmd" ]; then
error "Docker Compose 未安装,无法构建 embed 镜像"
return 1
fi
info "使用 $compose_cmd build(version=$BUILD_VERSION)..."
if [ "$no_cache" -eq 1 ]; then
info "无缓存全量构建 (--no-cache)..."
$compose_cmd build --no-cache aiops || { error "镜像构建失败"; return 1; }
else
$compose_cmd build aiops || { error "镜像构建失败"; return 1; }
fi
docker tag "$EMBED_IMAGE" "${DOCKERHUB_IMAGE}:dev" 2>/dev/null || true
local img_size
img_size=$(docker images "$EMBED_IMAGE" --format "{{.Size}}" | head -1)
success "镜像构建完成: $EMBED_IMAGE ($img_size)"
KUBERNETES_BUILT_IMAGE="$EMBED_IMAGE"
if [ "$do_push" -eq 1 ]; then
push_embed_image "$BUILD_VERSION" || return 1
fi
return 0
}
# 推送镜像到 Docker Hub // Logan.Li
push_embed_image() {
local version_tag="${1:-}"
resolve_version "$version_tag"
info "推送镜像到 Docker Hub..."
docker push "$EMBED_IMAGE" || {
error "推送失败: $EMBED_IMAGE(请先 docker login)"
return 1
}
if [ "$BUILD_VERSION" != "dev" ]; then
docker push "${DOCKERHUB_IMAGE}:dev" 2>/dev/null || true
fi
success "已推送: $EMBED_IMAGE"
return 0
}
# 更新 K8s deployment 中的镜像坐标 // Logan.Li
kubernetes_update_manifest_image() {
local image=$1
local files=(
"$KUBERNETES_MANIFEST_DIR/deployment.yaml"
"$KUBERNETES_MANIFEST_DIR/quick-deploy.yaml"
)
local f
for f in "${files[@]}"; do
[ -f "$f" ] || continue
sed_inplace "s|image: ${DOCKERHUB_IMAGE}:.*|image: ${image}|g" "$f"
sed_inplace "s|image: .*/aiops:.*|image: ${image}|g" "$f"
done
info "K8s manifest 镜像已更新为: $image"
}
# 编译独立二进制(含 embed 前端,输出到项目根目录 ./aiops)// Logan.Li
build_standalone_binary() {
local version_tag="${1:-}"
ensure_go_path || return 1
# 独立部署依赖 agent/bin 下载资源,主程序构建时一并生成。 // Logan.Li
build_agent_binaries || return 1
build_frontend_embed
ensure_binary_config
clean_go_artifacts
resolve_build_version "$version_tag"
cd "$SCRIPT_DIR/$BACKEND_DIR"
info "编译中 (version=$BUILD_VERSION)..."
local ldflags="-w -s"
ldflags+=" -X main.Version=${BUILD_VERSION}"
ldflags+=" -X main.BuildTime=${BUILD_TIME}"
ldflags+=" -X main.GitCommit=${GIT_COMMIT}"
CGO_ENABLED=1 GOPROXY=https://goproxy.cn,direct GOSUMDB=sum.golang.org GONOSUMCHECK='*' \
go build -ldflags="$ldflags" -o "$STANDALONE_BIN_PATH" .
if [ ! -f "$STANDALONE_BIN_PATH" ]; then
error "编译失败"
return 1
fi
local bin_size
bin_size=$(du -h "$STANDALONE_BIN_PATH" | cut -f1)
success "编译完成: ./$STANDALONE_BIN ($bin_size, $BUILD_VERSION)"
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
# macOS 默认 Bash 3 不支持 local -n,这里使用全局数组承载启动参数。// Logan.Li
BINARY_ARGS=()
if [ -f "$BINARY_ARGS_FILE" ]; then
while IFS= read -r line || [ -n "$line" ]; do
[ -n "$line" ] && BINARY_ARGS+=("$line")
done < "$BINARY_ARGS_FILE"
fi
if [ ${#BINARY_ARGS[@]} -eq 0 ]; then
BINARY_ARGS=(--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"
mkdir -p "$LOGS_DIR" uploads
rotate_log "$LOGS_DIR/aiops.log"
stop_binary_process "$port"
nohup ./"$STANDALONE_BIN" "${args[@]}" </dev/null >> "$LOGS_DIR/aiops.log" 2>&1 &
local pid=$!
disown "$pid" 2>/dev/null || true
echo "$pid" > "$BINARY_PID_FILE"
save_binary_args "${args[@]}"
if wait_binary_ready "$port"; then
success "二进制已启动 (PID: $(cat "$BINARY_PID_FILE"), 端口: $port)"
info "日志: $LOGS_DIR/aiops.log"
info "数据: $SCRIPT_DIR/sqlite3.db, $SCRIPT_DIR/redka.db"
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}
shift || true
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
# 解析可选版本号参数 (up v1.0.1 / push v1.0.1) // Logan.Li
local version_arg=""
local fresh_build=0
while [ $# -gt 0 ]; do
case "$1" in
fresh) fresh_build=1; shift ;;
tag)
shift
version_arg=${1:-}
shift
;;
*)
if looks_like_version "$1"; then
version_arg=$1
fi
shift
;;
esac
done
resolve_version "$version_arg"
export DOCKERHUB_IMAGE
info "使用: $DOCKER_COMPOSE"
info "版本: $BUILD_VERSION | 镜像: $EMBED_IMAGE"
export DOCKER_BUILDKIT=1
export COMPOSE_DOCKER_CLI_BUILD=1
info "BuildKit: 已启用 (增量编译 + 缓存复用)"
case "$action" in
up|upgrade|update)
banner "========== Docker Compose 启动/升级 ($BUILD_VERSION) =========="
# 全新部署时库是空的(服务启动刻意不建表),先显式跑一次结构迁移再启动服务,
# 避免容器先起来面对空库;已初始化的库此步幂等,只会输出「迁移已是最新状态」。 // Logan.Li
info "数据库结构迁移 (aiops migrate up)..."
if ! $DOCKER_COMPOSE run --rm --build aiops ./aiops migrate up; then
error "数据库迁移失败:请检查上方输出(历史库缺少迁移记录时需先执行 aiops migrate baseline)"
exit 1
fi
# Docker 在容器内构建,无需清理宿主机产物;每次 prune 缓存会导致下次全量编译 // Logan.Li
if [ "$fresh_build" -eq 1 ]; then
info "全量重建镜像 (--no-cache)..."
$DOCKER_COMPOSE build --no-cache aiops
$DOCKER_COMPOSE up -d --force-recreate --remove-orphans
elif [ "$action" = "upgrade" ] || [ "$action" = "update" ]; then
info "升级应用:仅重建 aiops 容器..."
$DOCKER_COMPOSE build aiops
$DOCKER_COMPOSE up -d --force-recreate --no-deps aiops
else
info "增量构建 + 启动容器(保留 BuildKit 缓存)..."
$DOCKER_COMPOSE up -d --build --remove-orphans
fi
echo ""
info "等待服务就绪..."
sleep 5
$DOCKER_COMPOSE ps
echo ""
info "访问地址: http://localhost:8080"
info "版本检查: http://localhost:8080/version"
;;
push)
build_embed_image "$version_arg" 0 1 || exit 1
;;
down)
info "停止 Docker Compose..."
$DOCKER_COMPOSE down
success "容器已停止"
;;
build)
info "构建 Docker 镜像..."
clean_all_artifacts
$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 ""
clean_all_artifacts
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 "清理完成"
;;
fast|start)
banner "========== Docker Compose 快速启动 (不构建镜像) =========="
$DOCKER_COMPOSE up -d --remove-orphans
sleep 3
$DOCKER_COMPOSE ps
info "访问地址: http://localhost:8080"
;;
fresh)
cmd_docker up fresh
;;
help|--help|-h)
echo "Docker Compose 部署命令 (embed 单容器 + MySQL + Redis):"
echo " ./run.sh docker up 增量构建 + 启动"
echo " ./run.sh docker fast 跳过构建,直接启动(低负载)"
echo " ./run.sh docker up v1.0.1 指定版本构建"
echo " ./run.sh docker up fresh 无缓存全量重建"
echo " ./run.sh docker push v1.0.1 构建并推送到 $DOCKERHUB_IMAGE"
echo " ./run.sh docker down 停止并移除容器"
echo " ./run.sh docker build 仅构建镜像"
echo " ./run.sh docker logs 查看实时日志"
echo " ./run.sh docker ps 查看容器状态"
;;
*)
error "未知 docker 子命令: $action"
cmd_docker help
exit 1
;;
esac
}
# ============================================================
# 编译独立二进制
# ============================================================
build_agent_binaries() {
cd "$SCRIPT_DIR/agent"
mkdir -p bin
info "编译 Agent Linux AMD64..."
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -ldflags="-w -s" -o bin/aiops-agent-linux-amd64 .
info "编译 Agent Linux ARM64..."
GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build -ldflags="-w -s" -o bin/aiops-agent-linux-arm64 .
info "编译 Agent Windows AMD64..."
GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build -ldflags="-w -s" -o bin/aiops-agent-windows-amd64.exe .
success "Agent 多平台二进制编译完成: agent/bin/"
}
cmd_build() {
local target=${1:-binary}
case "$target" in
binary|aiops)
banner "========== 编译独立二进制 =========="
build_standalone_binary || exit 1
echo ""
info "使用方法:"
echo " ./$STANDALONE_BIN version # 查看版本"
echo " ./run.sh binary upgrade # 停服重编译并后台启动"
echo " ./$STANDALONE_BIN --port 8080 # 前台启动"
echo " ./$STANDALONE_BIN --mode local # 本地模式 (SQLite + redka)"
echo " ./$STANDALONE_BIN --config config.yaml # 使用配置文件"
;;
tag)
local version_tag=${2:-}
if [ -z "$version_tag" ]; then
error "请指定版本号,例如: ./run.sh build tag v1.0.1"
exit 1
fi
banner "========== 编译独立二进制 ($version_tag) =========="
build_standalone_binary "$version_tag" || exit 1
echo ""
success "版本已写入二进制,验证: ./$STANDALONE_BIN version"
;;
agent)
banner "========== 编译 Agent =========="
build_agent_binaries
;;
frontend)
info "构建前端生产包..."
cd "$SCRIPT_DIR/$FRONTEND_DIR"
npm run build
success "前端构建完成: $FRONTEND_DIR/dist/"
;;
help|--help|-h)
echo "编译命令:"
echo " ./run.sh build 编译独立二进制 ./aiops (输出到项目根目录)"
echo " ./run.sh build tag v1.0.1 指定版本号编译 (./aiops version 可查看)"
echo " ./run.sh build agent 交叉编译 Agent (amd64+arm64)"
echo " ./run.sh build frontend 构建前端生产包"
;;
*)
error "未知构建目标: $target"
cmd_build help
exit 1
;;
esac
}
# ============================================================
# 本地质量门禁(内部协作 / 发版前共用)// Logan.Li
# ============================================================
cmd_check() {
local mode=${1:-standard}
banner "========== 本地质量检查 =========="
ensure_go_path || exit 1
# 受限环境可能无法写 /root/.cache,检查命令统一使用项目内 Go 缓存。// Logan.Li
export GOCACHE="$SCRIPT_DIR/.cache/go-build"
mkdir -p "$GOCACHE"
info "后端 go vet..."
cd "$SCRIPT_DIR/$BACKEND_DIR"
go vet ./...
info "后端 go test..."
go test ./...
info "后端 go build..."
CGO_ENABLED=1 go build -o "$BACKEND_BIN" .
info "Agent go test..."
cd "$SCRIPT_DIR/agent"
go test ./...
info "前端 typecheck..."
cd "$SCRIPT_DIR/$FRONTEND_DIR"
if [ ! -d "node_modules" ]; then
npm install
fi
npm run check
if [ "$mode" = "strict" ]; then
info "前端 lint(严格模式)..."
npm run lint
fi
cd "$SCRIPT_DIR"
success "本地质量检查通过"
}
# ============================================================
# 独立二进制模式
# ============================================================
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=("${BINARY_ARGS[@]}")
fi
local port
port=$(binary_listen_port "${args[@]}")
info "启动参数: ${args[*]}"
stop_binary_process "$port"
if [ -f "$STANDALONE_BIN_PATH" ]; then
local backup="$SCRIPT_DIR/${STANDALONE_BIN}.bak.$(date +%Y%m%d%H%M%S)"
cp "$STANDALONE_BIN_PATH" "$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=("${BINARY_ARGS[@]}")
fi
if [ ! -f "$STANDALONE_BIN_PATH" ]; 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=("${BINARY_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|*)
if [ ! -f "$STANDALONE_BIN_PATH" ]; then
info "二进制不存在,先编译..."
build_standalone_binary || exit 1
fi
banner "========== 前台启动独立二进制 =========="
mkdir -p "$LOGS_DIR" uploads
if [ $# -gt 0 ]; then
save_binary_args "$@"
fi
cd "$SCRIPT_DIR"
./"$STANDALONE_BIN" "$@"
;;
esac
}
# ============================================================
# Kubernetes 部署 (embed 单容器模式)
# ============================================================
KUBERNETES_NAMESPACE="devops"
KUBERNETES_MANIFEST_DIR="$SCRIPT_DIR/deploy/kubernetes"
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 <registry>/$image"
}
# Kubernetes 构建镜像 // Logan.Li
cmd_kubernetes_build() {
local version_tag=""
local no_cache=0
local do_push=0
while [ $# -gt 0 ]; do
case "$1" in
1|--no-cache|fresh) no_cache=1; shift ;;
push) do_push=1; shift ;;
tag)
shift
version_tag=${1:-}
shift
;;
*)
if looks_like_version "$1"; then
version_tag=$1
fi
shift
;;
esac
done
build_embed_image "$version_tag" "$no_cache" "$do_push" || return 1
load_image_to_cluster "$KUBERNETES_BUILT_IMAGE"
load_image_to_cluster "${DOCKERHUB_IMAGE}:dev"
}
# 读取 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. 构建镜像(支持 deploy v1.0.1)
shift
cmd_kubernetes_build "$@"
kubernetes_update_manifest_image "$KUBERNETES_BUILT_IMAGE"
# 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:-<NODE_IP>}:$KUBERNETES_NODE_PORT"
info "健康检查: http://${node_ip:-<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"
shift
cmd_kubernetes_build fresh "$@"
kubernetes_update_manifest_image "$KUBERNETES_BUILT_IMAGE"
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)
shift
cmd_kubernetes_build "$@"
;;
push)
shift
cmd_kubernetes_build push "$@"
;;
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 v1.0.1 构建 embed 镜像"
echo " ./run.sh kubernetes push v1.0.1 构建并推送到 $DOCKERHUB_IMAGE"
echo ""
echo " 镜像仓库: $DOCKERHUB_IMAGE:<tag>"
echo " 命名空间: $KUBERNETES_NAMESPACE"
echo " NodePort: $KUBERNETES_NODE_PORT"
;;
*)
error "未知 kubernetes 子命令: $action"
cmd_kubernetes help
exit 1
;;
esac
}
# ============================================================
# 统一发版:git tag + 二进制 + Docker 镜像推送 + K8s manifest
# ============================================================
cmd_release() {
local version_tag=${1:-}
local skip_tag=${2:-}
if [ -z "$version_tag" ]; then
error "用法: ./run.sh release v1.0.1"
echo " 将依次: 打 git tag → 编译二进制 → 构建镜像 → 推送 $DOCKERHUB_IMAGE → 更新 K8s manifest"
exit 1
fi
if ! looks_like_version "$version_tag"; then
error "版本号格式无效: $version_tag (示例: v1.0.1)"
exit 1
fi
banner "========== 发版 $version_tag =========="
resolve_version "$version_tag"
if [ "$skip_tag" != "--skip-tag" ]; then
if git rev-parse "$version_tag" >/dev/null 2>&1; then
warning "git tag 已存在: $version_tag(跳过创建)"
else
git tag -a "$version_tag" -m "release $version_tag"
success "已创建 git tag: $version_tag"
fi
fi
build_standalone_binary "$version_tag" || exit 1
info "二进制版本:"
./"$STANDALONE_BIN" version
build_embed_image "$version_tag" 0 1 || exit 1
kubernetes_update_manifest_image "$EMBED_IMAGE"
echo ""
success "发版完成: $version_tag"
echo " 二进制: ./$STANDALONE_BIN version"
echo " 镜像: $EMBED_IMAGE"
echo " 推送: docker push $EMBED_IMAGE"
echo " Compose: ./run.sh docker up $version_tag"
echo " K8s: ./run.sh kubernetes upgrade $version_tag"
echo ""
if [ "$skip_tag" != "--skip-tag" ]; then
info "如需推送 git tag: git push origin $version_tag"
fi
}
# ============================================================
# 帮助
# ============================================================
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 容器部署 (embed 单容器)${NC}"
echo " ./run.sh docker up 构建+启动 (MySQL+Redis+AIOps)"
echo " ./run.sh docker up v1.0.1 指定版本"
echo " ./run.sh docker push v1.0.1 构建并推送到 $DOCKERHUB_IMAGE"
echo " ./run.sh docker down 停止容器"
echo " ./run.sh docker logs 查看日志"
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 check 本地质量检查: Go vet/test/build + 前端 typecheck"
echo " ./run.sh check strict 严格检查: 在 check 基础上追加前端 lint"
echo " ./run.sh build 编译独立二进制 ./$STANDALONE_BIN (仅根目录)"
echo " ./run.sh build tag v1.0.1 指定版本号编译"
echo " ./run.sh release v1.0.1 发版:tag+二进制+镜像推送+K8s"
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 " ./$STANDALONE_BIN 默认启动 (SQLite, 端口 8080)"
echo " ./$STANDALONE_BIN --port 9090 自定义端口"
echo " ./$STANDALONE_BIN --mode local 本地模式 (SQLite + redka)"
echo " ./$STANDALONE_BIN --config config.yaml 使用配置文件"
echo " ./$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 "$@"
;;
# 本地质量检查
check)
shift
cmd_check "$@"
;;
# 独立二进制
binary|bin)
shift
cmd_binary "$@"
;;
# Kubernetes 部署 (k8s 为别名)
kubernetes|k8s)
shift
cmd_kubernetes "$@"
;;
# 统一发版
release)
shift
cmd_release "$@"
;;
# 帮助
help|--help|-h)
cmd_help
;;
# 清理构建产物
clean)
clean_all_artifacts
;;
*)
error "未知命令: $cmd"
cmd_help
exit 1
;;
esac
}
main "$@"