Files
aiops/run.sh
T
attacker 2e179ef7f4 feat(i18n): U3 国际化 — react-i18next 双语框架 + 侧边栏/Header/Login 全量翻译 + 语言切换器
- 安装 i18next + react-i18next,创建 src/i18n/ 配置实例
- 创建 zh-CN.json (120+ 键) + en-US.json 全量英文翻译
- 侧边栏 42+ 菜单项全量 labelKey 国际化
- Header 通知/主题/用户菜单全量翻译
- Login 页面表单/按钮/主题+语言切换器
- zustand useLanguageStore 持久化语言选择
- HTML lang 属性自动同步
- tsc -b=0, npm build=✅, 17/17 测试通过
2026-06-27 16:54:07 +08:00

1493 lines
50 KiB
Bash
Executable File
Raw 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-server"
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
}
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 (./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/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 "$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-*
}
# 构建前端并同步到 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 DOCKER_BUILDKIT=1
local build_args=(
-f Dockerfile.embed
--build-arg "VERSION=${BUILD_VERSION}"
--build-arg "BUILD_TIME=${BUILD_TIME}"
--build-arg "GIT_COMMIT=${GIT_COMMIT}"
--label "org.opencontainers.image.version=${BUILD_VERSION}"
-t "$EMBED_IMAGE"
-t "${DOCKERHUB_IMAGE}:dev"
.
)
if [ "$no_cache" -eq 1 ]; then
build_args=(--no-cache --pull "${build_args[@]}")
info "无缓存全量构建 (--no-cache --pull)"
fi
info "构建中(前端 + 后端 embed)..."
docker build "${build_args[@]}" || {
error "镜像构建失败"
return 1
}
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
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 GONOSUMCHECK='*' GOTOOLCHAIN=go1.24.0 \
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
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"
mkdir -p "$LOGS_DIR" uploads
rotate_log "$LOGS_DIR/aiops.log"
stop_binary_process "$port"
nohup ./"$STANDALONE_BIN" "${args[@]}" >> "$LOGS_DIR/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 "日志: $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) =========="
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 ""
info "清理历史构建缓存..."
docker builder prune -f 2>/dev/null || true
docker image prune -f 2>/dev/null || true
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 镜像..."
$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 部署命令 (embed 单容器 + MySQL + Redis):"
echo " ./run.sh docker up 增量构建 + 启动"
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
}
# ============================================================
# 编译独立二进制
# ============================================================
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 =========="
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 (输出到项目根目录)"
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
}
# ============================================================
# 独立二进制模式
# ============================================================
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 "$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
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
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 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 "$@"
;;
# 独立二进制
binary|bin)
shift
cmd_binary "$@"
;;
# Kubernetes 部署 (k8s 为别名)
kubernetes|k8s)
shift
cmd_kubernetes "$@"
;;
# 统一发版
release)
shift
cmd_release "$@"
;;
# 帮助
help|--help|-h)
cmd_help
;;
*)
error "未知命令: $cmd"
cmd_help
exit 1
;;
esac
}
main "$@"