feat: run.sh四模式(dev/docker/build/binary) + CLI flags + Redka内嵌Redis + Agent复制修复 + 服务器编辑IP字段 + 安装Agent按钮
This commit is contained in:
1 parent
d0b94d39c0
commit
2354959811
14 files changed
+1368
-588
No files matched your search
+101
-82
@@ -1,82 +1,101 @@
|
||||
# ============================================================
|
||||
# AIOps Backend + Agent 多阶段 Dockerfile
|
||||
# Stage 1: 编译后端 (Go 1.21)
|
||||
# Stage 2: 交叉编译 Agent (Go 1.24, linux-amd64/arm64 + darwin-arm64)
|
||||
# Stage 3: 运行时 (Alpine)
|
||||
# 署名 Logan.Li
|
||||
# ============================================================
|
||||
|
||||
# ==================== Stage 1: Backend Builder ====================
|
||||
FROM golang:1.21-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN apk add --no-cache git ca-certificates tzdata
|
||||
|
||||
ENV TZ=Asia/Shanghai
|
||||
ENV GOPROXY=https://goproxy.cn,direct
|
||||
ENV GOSUMDB=off
|
||||
|
||||
# 复制后端 go.mod(利用 Docker 层缓存)
|
||||
COPY backend/go.mod backend/go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
# 复制后端源码
|
||||
COPY backend/ ./
|
||||
|
||||
# 构建后端二进制
|
||||
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \
|
||||
-ldflags="-w -s" \
|
||||
-o ezops ./cmd/
|
||||
|
||||
# ==================== Stage 2: Agent Builder ====================
|
||||
FROM golang:1.24-alpine AS agent-builder
|
||||
|
||||
WORKDIR /agent
|
||||
|
||||
ENV GOPROXY=https://goproxy.cn,direct
|
||||
ENV GOSUMDB=off
|
||||
|
||||
# 复制 agent go.mod
|
||||
COPY agent/go.mod agent/go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
# 复制 agent 源码
|
||||
COPY agent/ ./
|
||||
|
||||
# 交叉编译三个平台的 agent 二进制
|
||||
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o bin/aiops-agent-linux-amd64 . && \
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags="-s -w" -o bin/aiops-agent-linux-arm64 . && \
|
||||
CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -ldflags="-s -w" -o bin/aiops-agent-darwin-arm64 .
|
||||
|
||||
# ==================== Stage 3: Runtime ====================
|
||||
FROM alpine:latest
|
||||
|
||||
# 安装运行时依赖(curl=健康检查,openssh-client=SSH远程安装agent)
|
||||
RUN apk --no-cache add ca-certificates tzdata curl openssh-client
|
||||
|
||||
ENV TZ=Asia/Shanghai
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# 复制后端二进制
|
||||
COPY --from=builder /app/ezops .
|
||||
|
||||
# 创建运行时目录
|
||||
RUN mkdir -p /app/logs /app/uploads /app/agent/bin /app/agent/deploy
|
||||
|
||||
# 复制 agent 二进制(三个平台)
|
||||
COPY --from=agent-builder /agent/bin/aiops-agent-linux-amd64 /app/agent/bin/
|
||||
COPY --from=agent-builder /agent/bin/aiops-agent-linux-arm64 /app/agent/bin/
|
||||
COPY --from=agent-builder /agent/bin/aiops-agent-darwin-arm64 /app/agent/bin/
|
||||
|
||||
# 复制 agent 安装脚本和 systemd service 模板
|
||||
COPY agent/deploy/install.sh /app/agent/deploy/
|
||||
COPY agent/deploy/aiops-agent.service /app/agent/deploy/
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
|
||||
CMD curl -f http://localhost:8080/health || exit 1
|
||||
|
||||
CMD ["./ezops"]
|
||||
1|# ============================================================
|
||||
2|# AIOps Backend + Agent 多阶段 Dockerfile
|
||||
3|# Stage 1: 编译后端 (Go 1.21)
|
||||
4|# Stage 2: 交叉编译 Agent (Go 1.24, linux-amd64/arm64 + darwin-arm64)
|
||||
5|# Stage 3: 运行时 (Alpine)
|
||||
6|# 署名 Logan.Li
|
||||
7|# ============================================================
|
||||
8|
|
||||
9|# ==================== Stage 1: Backend Builder ====================
|
||||
10|FROM golang:1.21-alpine AS builder
|
||||
11|
|
||||
12|WORKDIR /app
|
||||
13|
|
||||
14|<<<<<<< HEAD
|
||||
15|=======
|
||||
16|# 使用国内 Alpine 镜像源加速
|
||||
17|RUN sed -i 's|dl-cdn.alpinelinux.org|mirrors.aliyun.com|g' /etc/apk/repositories
|
||||
18|
|
||||
19|# 安装构建依赖
|
||||
20|>>>>>>> ca18ead (feat: run.sh四模式(dev/docker/build/binary) + CLI flags + Redka内嵌Redis + Agent复制修复 + 服务器编辑IP字段 + 安装Agent按钮)
|
||||
21|RUN apk add --no-cache git ca-certificates tzdata
|
||||
22|
|
||||
23|ENV TZ=Asia/Shanghai
|
||||
24|ENV GOPROXY=https://goproxy.cn,direct
|
||||
25|# 跳过 toolchain 校验(GOTOOLCHAIN 自动下载 Go 1.22)
|
||||
26|ENV GONOSUMCHECK=*
|
||||
27|# 自动下载 Go 1.22 工具链(Redka 需要)
|
||||
28|ENV GOTOOLCHAIN=go1.22.2
|
||||
29|
|
||||
30|# 复制后端 go.mod(利用 Docker 层缓存)
|
||||
31|COPY backend/go.mod backend/go.sum ./
|
||||
32|RUN go mod download
|
||||
33|
|
||||
34|# 复制后端源码
|
||||
35|COPY backend/ ./
|
||||
36|
|
||||
37|# 构建后端二进制
|
||||
38|RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \
|
||||
39| -ldflags="-w -s" \
|
||||
40| -o ezops ./cmd/
|
||||
41|
|
||||
42|# ==================== Stage 2: Agent Builder ====================
|
||||
43|FROM golang:1.24-alpine AS agent-builder
|
||||
44|
|
||||
45|WORKDIR /agent
|
||||
46|
|
||||
47|ENV GOPROXY=https://goproxy.cn,direct
|
||||
48|ENV GOSUMDB=off
|
||||
49|
|
||||
50|# 复制 agent go.mod
|
||||
51|COPY agent/go.mod agent/go.sum ./
|
||||
52|RUN go mod download
|
||||
53|
|
||||
54|# 复制 agent 源码
|
||||
55|COPY agent/ ./
|
||||
56|
|
||||
57|# 交叉编译三个平台的 agent 二进制
|
||||
58|RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o bin/aiops-agent-linux-amd64 . && \
|
||||
59| CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags="-s -w" -o bin/aiops-agent-linux-arm64 . && \
|
||||
60| CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -ldflags="-s -w" -o bin/aiops-agent-darwin-arm64 .
|
||||
61|
|
||||
62|# ==================== Stage 3: Runtime ====================
|
||||
63|FROM alpine:latest
|
||||
64|
|
||||
65|<<<<<<< HEAD
|
||||
66|# 安装运行时依赖(curl=健康检查,openssh-client=SSH远程安装agent)
|
||||
67|RUN apk --no-cache add ca-certificates tzdata curl openssh-client
|
||||
68|=======
|
||||
69|# 使用国内 Alpine 镜像源加速
|
||||
70|RUN sed -i 's|dl-cdn.alpinelinux.org|mirrors.aliyun.com|g' /etc/apk/repositories
|
||||
71|
|
||||
72|# 安装运行时依赖
|
||||
73|RUN apk --no-cache add ca-certificates tzdata curl
|
||||
74|>>>>>>> ca18ead (feat: run.sh四模式(dev/docker/build/binary) + CLI flags + Redka内嵌Redis + Agent复制修复 + 服务器编辑IP字段 + 安装Agent按钮)
|
||||
75|
|
||||
76|ENV TZ=Asia/Shanghai
|
||||
77|
|
||||
78|WORKDIR /app
|
||||
79|
|
||||
80|# 复制后端二进制
|
||||
81|COPY --from=builder /app/ezops .
|
||||
82|
|
||||
83|# 创建运行时目录
|
||||
84|RUN mkdir -p /app/logs /app/uploads /app/agent/bin /app/agent/deploy
|
||||
85|
|
||||
86|# 复制 agent 二进制(三个平台)
|
||||
87|COPY --from=agent-builder /agent/bin/aiops-agent-linux-amd64 /app/agent/bin/
|
||||
88|COPY --from=agent-builder /agent/bin/aiops-agent-linux-arm64 /app/agent/bin/
|
||||
89|COPY --from=agent-builder /agent/bin/aiops-agent-darwin-arm64 /app/agent/bin/
|
||||
90|
|
||||
91|# 复制 agent 安装脚本和 systemd service 模板
|
||||
92|COPY agent/deploy/install.sh /app/agent/deploy/
|
||||
93|COPY agent/deploy/aiops-agent.service /app/agent/deploy/
|
||||
94|
|
||||
95|EXPOSE 8080
|
||||
96|
|
||||
97|HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
|
||||
98| CMD curl -f http://localhost:8080/health || exit 1
|
||||
99|
|
||||
100|CMD ["./ezops"]
|
||||
101|
|
||||
+189
-13
@@ -2,6 +2,8 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
@@ -12,18 +14,50 @@ import (
|
||||
"ezops/internal/config"
|
||||
"ezops/pkg/database"
|
||||
"ezops/pkg/logger"
|
||||
ezredis "ezops/pkg/redis"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/spf13/viper"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// 版本信息(通过编译时 ldflags 注入)
|
||||
// 版本信息(通过编译时 ldflags 注入)// Logan.Li
|
||||
var (
|
||||
Version = "dev"
|
||||
BuildTime = "unknown"
|
||||
GitCommit = "unknown"
|
||||
)
|
||||
|
||||
// CLI 参数定义 // Logan.Li
|
||||
var (
|
||||
flagPort = flag.String("port", "", "服务端口 (默认: 8080)")
|
||||
flagConfig = flag.String("config", "", "配置文件路径 (默认: config.yaml)")
|
||||
flagMode = flag.String("mode", "", "运行模式: development, production, release")
|
||||
flagLogLevel = flag.String("log-level", "", "日志级别: debug, info, warn, error")
|
||||
|
||||
// 数据库参数
|
||||
flagDBDriver = flag.String("db-driver", "", "数据库驱动: sqlite, mysql, postgres (默认: sqlite)")
|
||||
flagDBHost = flag.String("db-host", "", "数据库主机")
|
||||
flagDBPort = flag.Int("db-port", 0, "数据库端口")
|
||||
flagDBUser = flag.String("db-user", "", "数据库用户名")
|
||||
flagDBPassword = flag.String("db-password", "", "数据库密码")
|
||||
flagDBName = flag.String("db-name", "", "数据库名 / SQLite 文件路径")
|
||||
flagDBCharset = flag.String("db-charset", "", "数据库字符集")
|
||||
|
||||
// Redis 参数
|
||||
flagRedisHost = flag.String("redis-host", "", "Redis 主机")
|
||||
flagRedisPort = flag.Int("redis-port", 0, "Redis 端口")
|
||||
flagRedisPassword = flag.String("redis-password", "", "Redis 密码")
|
||||
flagRedisDB = flag.Int("redis-db", 0, "Redis 数据库编号")
|
||||
|
||||
// JWT 参数
|
||||
flagJWTSecret = flag.String("jwt-secret", "", "JWT 密钥")
|
||||
flagJWTExpire = flag.Int("jwt-expire", 0, "JWT 过期时间 (小时)")
|
||||
|
||||
// 显示版本
|
||||
flagVersion = flag.Bool("version", false, "显示版本信息")
|
||||
)
|
||||
|
||||
// @title AIOps运维平台 API
|
||||
// @version 1.0
|
||||
// @description AIOps智能运维平台后端API文档
|
||||
@@ -45,32 +79,67 @@ var (
|
||||
// @description Type "Bearer" followed by a space and JWT token.
|
||||
|
||||
func main() {
|
||||
// 加载配置
|
||||
cfg := config.Load()
|
||||
// 解析 CLI 参数 // Logan.Li
|
||||
flag.Parse()
|
||||
|
||||
// 显示版本信息 // Logan.Li
|
||||
if *flagVersion {
|
||||
fmt.Printf("AIOps 智能运维平台\n")
|
||||
fmt.Printf(" 版本: %s\n", Version)
|
||||
fmt.Printf(" 构建: %s\n", BuildTime)
|
||||
fmt.Printf(" 提交: %s\n", GitCommit)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// 加载配置 // Logan.Li
|
||||
cfg := loadConfig()
|
||||
|
||||
// 初始化日志
|
||||
log := logger.New(cfg.LogLevel)
|
||||
defer log.Sync()
|
||||
|
||||
log.Info("AIOps 启动中",
|
||||
zap.String("version", Version),
|
||||
zap.String("mode", cfg.Mode),
|
||||
zap.String("port", cfg.Port),
|
||||
)
|
||||
|
||||
// 设置Gin模式
|
||||
if cfg.Mode == "production" {
|
||||
if cfg.Mode == "production" || cfg.Mode == "release" {
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
}
|
||||
|
||||
// 初始化数据库
|
||||
db, err := database.New(cfg.Database)
|
||||
if err != nil {
|
||||
log.Fatal("failed to connect database", zap.Error(err))
|
||||
log.Fatal("数据库连接失败", zap.Error(err))
|
||||
}
|
||||
|
||||
// 自动迁移
|
||||
if err := database.AutoMigrate(db); err != nil {
|
||||
log.Fatal("failed to migrate database", zap.Error(err))
|
||||
log.Fatal("数据库迁移失败", zap.Error(err))
|
||||
}
|
||||
|
||||
// 初始化种子数据
|
||||
seedData(db)
|
||||
|
||||
// 初始化缓存(Redka 内嵌 / Redis 外部)// Logan.Li
|
||||
cache, err := ezredis.New(ezredis.Config{
|
||||
Driver: "redka", // 默认内嵌 Redka,零依赖
|
||||
Host: cfg.Redis.Host,
|
||||
Port: cfg.Redis.Port,
|
||||
Password: cfg.Redis.Password,
|
||||
DB: cfg.Redis.DB,
|
||||
DataDir: "data/redka",
|
||||
})
|
||||
if err != nil {
|
||||
log.Warn("缓存初始化失败,继续启动", zap.Error(err))
|
||||
cache = nil
|
||||
} else {
|
||||
log.Info("缓存已就绪", zap.String("driver", "redka"))
|
||||
defer cache.Close()
|
||||
}
|
||||
|
||||
// 创建Gin实例
|
||||
r := gin.New()
|
||||
|
||||
@@ -85,11 +154,13 @@ func main() {
|
||||
"version": Version,
|
||||
"build_time": BuildTime,
|
||||
"git_commit": GitCommit,
|
||||
"mode": cfg.Mode,
|
||||
"db_driver": cfg.Database.Driver,
|
||||
})
|
||||
})
|
||||
|
||||
// 注册路由
|
||||
routes.Register(r, db, log, cfg)
|
||||
routes.Register(r, db, log, cfg, cache)
|
||||
|
||||
// 创建HTTP服务器
|
||||
srv := &http.Server{
|
||||
@@ -99,28 +170,133 @@ func main() {
|
||||
|
||||
// 优雅关闭
|
||||
go func() {
|
||||
log.Info("服务已启动", zap.String("addr", srv.Addr))
|
||||
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
log.Fatal("failed to start server", zap.Error(err))
|
||||
log.Fatal("服务启动失败", zap.Error(err))
|
||||
}
|
||||
}()
|
||||
|
||||
log.Info("server started", zap.String("port", cfg.Port))
|
||||
|
||||
// 等待中断信号
|
||||
quit := make(chan os.Signal, 1)
|
||||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-quit
|
||||
|
||||
log.Info("shutting down server...")
|
||||
log.Info("正在关闭服务...")
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if err := srv.Shutdown(ctx); err != nil {
|
||||
log.Fatal("server forced to shutdown", zap.Error(err))
|
||||
log.Fatal("强制关闭服务", zap.Error(err))
|
||||
}
|
||||
|
||||
log.Info("server exited")
|
||||
log.Info("服务已退出")
|
||||
}
|
||||
|
||||
// loadConfig 加载配置(CLI flags 优先于 config.yaml)// Logan.Li
|
||||
func loadConfig() *config.Config {
|
||||
viper.SetConfigName("config")
|
||||
viper.SetConfigType("yaml")
|
||||
viper.AddConfigPath("./configs")
|
||||
viper.AddConfigPath(".")
|
||||
|
||||
// 如果指定了配置文件路径,使用它 // Logan.Li
|
||||
if *flagConfig != "" {
|
||||
viper.SetConfigFile(*flagConfig)
|
||||
}
|
||||
|
||||
// 默认值(独立二进制模式:SQLite + 无 Redis)// Logan.Li
|
||||
viper.SetDefault("mode", "release")
|
||||
viper.SetDefault("port", "8080")
|
||||
viper.SetDefault("log_level", "info")
|
||||
|
||||
viper.SetDefault("database.driver", "sqlite")
|
||||
viper.SetDefault("database.host", "localhost")
|
||||
viper.SetDefault("database.port", 3306)
|
||||
viper.SetDefault("database.user", "")
|
||||
viper.SetDefault("database.password", "")
|
||||
viper.SetDefault("database.dbname", "data/aiops.db")
|
||||
viper.SetDefault("database.charset", "utf8mb4")
|
||||
viper.SetDefault("database.max_conn", 100)
|
||||
viper.SetDefault("database.max_idle", 10)
|
||||
|
||||
viper.SetDefault("redis.host", "localhost")
|
||||
viper.SetDefault("redis.port", 6379)
|
||||
viper.SetDefault("redis.password", "")
|
||||
viper.SetDefault("redis.db", 0)
|
||||
|
||||
viper.SetDefault("jwt.secret", "aiops-jwt-secret-change-in-production")
|
||||
viper.SetDefault("jwt.expire_hour", 24)
|
||||
|
||||
viper.SetDefault("websocket.read_buffer_size", 1024)
|
||||
viper.SetDefault("websocket.write_buffer_size", 1024)
|
||||
|
||||
// 读取配置文件(不存在也不报错)// Logan.Li
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
// 配置文件不存在是正常的(独立二进制模式)
|
||||
if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
|
||||
fmt.Fprintf(os.Stderr, "警告: 读取配置文件失败: %v,使用默认配置\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 环境变量覆盖 // Logan.Li
|
||||
viper.AutomaticEnv()
|
||||
|
||||
// CLI flags 覆盖(最高优先级)// Logan.Li
|
||||
applyFlags()
|
||||
|
||||
// 解析配置 // Logan.Li
|
||||
var cfg config.Config
|
||||
if err := viper.Unmarshal(&cfg); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "配置解析失败: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
return &cfg
|
||||
}
|
||||
|
||||
// applyFlags 将 CLI 参数映射到 viper(覆盖 config.yaml 和默认值)// Logan.Li
|
||||
func applyFlags() {
|
||||
// 检查是否有任何 flag 被显式设置 // Logan.Li
|
||||
flag.Visit(func(f *flag.Flag) {
|
||||
switch f.Name {
|
||||
case "port":
|
||||
viper.Set("port", *flagPort)
|
||||
case "mode":
|
||||
viper.Set("mode", *flagMode)
|
||||
case "log-level":
|
||||
viper.Set("log_level", *flagLogLevel)
|
||||
|
||||
case "db-driver":
|
||||
viper.Set("database.driver", *flagDBDriver)
|
||||
case "db-host":
|
||||
viper.Set("database.host", *flagDBHost)
|
||||
case "db-port":
|
||||
viper.Set("database.port", *flagDBPort)
|
||||
case "db-user":
|
||||
viper.Set("database.user", *flagDBUser)
|
||||
case "db-password":
|
||||
viper.Set("database.password", *flagDBPassword)
|
||||
case "db-name":
|
||||
viper.Set("database.dbname", *flagDBName)
|
||||
case "db-charset":
|
||||
viper.Set("database.charset", *flagDBCharset)
|
||||
|
||||
case "redis-host":
|
||||
viper.Set("redis.host", *flagRedisHost)
|
||||
case "redis-port":
|
||||
viper.Set("redis.port", *flagRedisPort)
|
||||
case "redis-password":
|
||||
viper.Set("redis.password", *flagRedisPassword)
|
||||
case "redis-db":
|
||||
viper.Set("redis.db", *flagRedisDB)
|
||||
|
||||
case "jwt-secret":
|
||||
viper.Set("jwt.secret", *flagJWTSecret)
|
||||
case "jwt-expire":
|
||||
viper.Set("jwt.expire_hour", *flagJWTExpire)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// CORS 跨域中间件
|
||||
|
||||
+8
-2
@@ -1,12 +1,17 @@
|
||||
module ezops
|
||||
|
||||
go 1.21
|
||||
go 1.22
|
||||
|
||||
toolchain go1.22.2
|
||||
|
||||
require (
|
||||
github.com/gin-gonic/gin v1.9.1
|
||||
github.com/go-ldap/ldap/v3 v3.4.10
|
||||
github.com/golang-jwt/jwt/v5 v5.2.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/nalgeon/redka v0.2.0
|
||||
github.com/redis/go-redis/v9 v9.7.0
|
||||
github.com/spf13/viper v1.18.2
|
||||
go.uber.org/zap v1.26.0
|
||||
golang.org/x/crypto v0.31.0
|
||||
@@ -19,7 +24,9 @@ require (
|
||||
require (
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
|
||||
github.com/bytedance/sonic v1.9.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
@@ -29,7 +36,6 @@ require (
|
||||
github.com/go-playground/validator/v10 v10.16.0 // indirect
|
||||
github.com/go-sql-driver/mysql v1.7.0 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
||||
|
||||
@@ -2,9 +2,15 @@ github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
|
||||
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI=
|
||||
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
|
||||
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
|
||||
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
|
||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
|
||||
@@ -12,6 +18,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||
@@ -102,11 +110,15 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/nalgeon/redka v0.2.0 h1:9Hjp5aHPHPLE44T1m41LKzx+b/MquMXt8ggw69eVu6U=
|
||||
github.com/nalgeon/redka v0.2.0/go.mod h1:vLxjY3XS9IwBID2YEFWeeMiN4Ar/DtKd4JW62JTAxuU=
|
||||
github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
|
||||
github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E=
|
||||
github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw=
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
|
||||
@@ -207,6 +219,7 @@ golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
|
||||
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
|
||||
@@ -236,6 +236,9 @@ func (h *ServerHandler) UpdateServer(c *gin.Context) {
|
||||
if req.Hostname != "" {
|
||||
updates["hostname"] = req.Hostname
|
||||
}
|
||||
if req.IP != "" {
|
||||
updates["ip"] = req.IP
|
||||
}
|
||||
if req.PublicIP != "" {
|
||||
updates["public_ip"] = req.PublicIP
|
||||
}
|
||||
|
||||
@@ -12,8 +12,9 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Register 注册所有路由
|
||||
func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config) {
|
||||
// Register 注册所有路由 // Logan.Li
|
||||
// cache 参数为可选 Redis 缓存实例(支持 redka/redis 双后端)
|
||||
func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config, cache interface{}) {
|
||||
// API版本前缀
|
||||
api := r.Group("/api/v1")
|
||||
|
||||
|
||||
@@ -155,6 +155,7 @@ type CreateServerRequest struct {
|
||||
// UpdateServerRequest 更新服务器请求
|
||||
type UpdateServerRequest struct {
|
||||
Hostname string `json:"hostname"`
|
||||
IP string `json:"ip" binding:"omitempty,ip"`
|
||||
PublicIP string `json:"public_ip" binding:"omitempty,ip"`
|
||||
Port int `json:"port" binding:"omitempty,min=1,max=65535"`
|
||||
Username string `json:"username"`
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
// go-redis 外部 Redis 服务器实现
|
||||
// 用于 Docker Compose / 生产环境部署 // Logan.Li
|
||||
package redis
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
goredis "github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
// goRedisCache 基于 go-redis 的缓存实现 // Logan.Li
|
||||
type goRedisCache struct {
|
||||
client *goredis.Client
|
||||
}
|
||||
|
||||
func newGoRedis(cfg Config) (Cache, error) {
|
||||
client := goredis.NewClient(&goredis.Options{
|
||||
Addr: fmt.Sprintf("%s:%d", cfg.Host, cfg.Port),
|
||||
Password: cfg.Password,
|
||||
DB: cfg.DB,
|
||||
})
|
||||
|
||||
// 连接测试
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if err := client.Ping(ctx).Err(); err != nil {
|
||||
return nil, fmt.Errorf("redis: 连接失败 %s:%d: %w", cfg.Host, cfg.Port, err)
|
||||
}
|
||||
|
||||
return &goRedisCache{client: client}, nil
|
||||
}
|
||||
|
||||
func (r *goRedisCache) Get(ctx context.Context, key string) (string, error) {
|
||||
val, err := r.client.Get(ctx, key).Result()
|
||||
if err == goredis.Nil {
|
||||
return "", nil
|
||||
}
|
||||
return val, err
|
||||
}
|
||||
|
||||
func (r *goRedisCache) Set(ctx context.Context, key string, value string, expiration time.Duration) error {
|
||||
return r.client.Set(ctx, key, value, expiration).Err()
|
||||
}
|
||||
|
||||
func (r *goRedisCache) Del(ctx context.Context, keys ...string) error {
|
||||
return r.client.Del(ctx, keys...).Err()
|
||||
}
|
||||
|
||||
func (r *goRedisCache) Exists(ctx context.Context, keys ...string) (int64, error) {
|
||||
return r.client.Exists(ctx, keys...).Result()
|
||||
}
|
||||
|
||||
func (r *goRedisCache) Expire(ctx context.Context, key string, expiration time.Duration) error {
|
||||
return r.client.Expire(ctx, key, expiration).Err()
|
||||
}
|
||||
|
||||
func (r *goRedisCache) Ping(ctx context.Context) error {
|
||||
return r.client.Ping(ctx).Err()
|
||||
}
|
||||
|
||||
func (r *goRedisCache) Close() error {
|
||||
return r.client.Close()
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
// Package redis 提供统一的 Redis 缓存抽象层
|
||||
// 支持两种后端:
|
||||
// - redka: 内嵌 Redis 兼容存储(零依赖,独立二进制模式)// Logan.Li
|
||||
// - redis: 外部 Redis 服务器(Docker / 生产环境)
|
||||
//
|
||||
// 使用方式:
|
||||
//
|
||||
// cache, err := redis.New(redis.Config{
|
||||
// Driver: "redka", // 或 "redis"
|
||||
// Host: "localhost",
|
||||
// Port: 6379,
|
||||
// Password: "",
|
||||
// DB: 0,
|
||||
// DataDir: "data/redka", // redka 数据目录(仅 redka 驱动使用)
|
||||
// })
|
||||
//
|
||||
// cache.Set(ctx, "key", "value", 0)
|
||||
// val, _ := cache.Get(ctx, "key")
|
||||
package redis
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Config Redis 配置 // Logan.Li
|
||||
type Config struct {
|
||||
Driver string // "redka" 或 "redis"
|
||||
Host string
|
||||
Port int
|
||||
Password string
|
||||
DB int
|
||||
DataDir string // redka 数据目录
|
||||
}
|
||||
|
||||
// Cache Redis 缓存接口 // Logan.Li
|
||||
type Cache interface {
|
||||
Get(ctx context.Context, key string) (string, error)
|
||||
Set(ctx context.Context, key string, value string, expiration time.Duration) error
|
||||
Del(ctx context.Context, keys ...string) error
|
||||
Exists(ctx context.Context, keys ...string) (int64, error)
|
||||
Expire(ctx context.Context, key string, expiration time.Duration) error
|
||||
Ping(ctx context.Context) error
|
||||
Close() error
|
||||
}
|
||||
|
||||
// New 创建缓存实例(根据驱动自动选择后端)// Logan.Li
|
||||
func New(cfg Config) (Cache, error) {
|
||||
switch cfg.Driver {
|
||||
case "redka", "":
|
||||
return newRedka(cfg)
|
||||
case "redis":
|
||||
return newGoRedis(cfg)
|
||||
default:
|
||||
return nil, fmt.Errorf("redis: 不支持的驱动: %s (支持: redka, redis)", cfg.Driver)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
// redka 内嵌 Redis 兼容存储实现(v0.2.x API)
|
||||
// 使用 github.com/nalgeon/redka — 基于 SQLite 的 Redis 替代方案 // Logan.Li
|
||||
// 零外部依赖,数据存在本地文件中,非常适合独立二进制部署
|
||||
package redis
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/nalgeon/redka"
|
||||
)
|
||||
|
||||
// redkaCache 基于 Redka v0.2.x 的缓存实现 // Logan.Li
|
||||
type redkaCache struct {
|
||||
db *redka.DB
|
||||
}
|
||||
|
||||
func newRedka(cfg Config) (Cache, error) {
|
||||
dataDir := cfg.DataDir
|
||||
if dataDir == "" {
|
||||
dataDir = "data/redka"
|
||||
}
|
||||
|
||||
// Redka v0.2.x: Open(path, opts) // Logan.Li
|
||||
db, err := redka.Open(dataDir+"/redka.db", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &redkaCache{db: db}, nil
|
||||
}
|
||||
|
||||
func (r *redkaCache) Get(ctx context.Context, key string) (string, error) {
|
||||
// Redka v0.2.x: Str().Get() 返回 core.Value // Logan.Li
|
||||
val, err := r.db.Str().Get(key)
|
||||
if err == redka.ErrNotFound {
|
||||
return "", nil
|
||||
}
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return val.String(), nil
|
||||
}
|
||||
|
||||
func (r *redkaCache) Set(ctx context.Context, key string, value string, expiration time.Duration) error {
|
||||
// Redka v0.2.x: Str().Set() 返回 error // Logan.Li
|
||||
if err := r.db.Str().Set(key, value); err != nil {
|
||||
return err
|
||||
}
|
||||
if expiration > 0 {
|
||||
_, err := r.db.Key().Expire(key, expiration)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *redkaCache) Del(ctx context.Context, keys ...string) error {
|
||||
for _, key := range keys {
|
||||
if _, err := r.db.Key().Delete(key); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *redkaCache) Exists(ctx context.Context, keys ...string) (int64, error) {
|
||||
var count int64
|
||||
for _, key := range keys {
|
||||
ok, err := r.db.Key().Exists(key)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if ok {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func (r *redkaCache) Expire(ctx context.Context, key string, expiration time.Duration) error {
|
||||
ok, err := r.db.Key().Expire(key, expiration)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !ok {
|
||||
return redka.ErrNotFound
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *redkaCache) Ping(ctx context.Context) error {
|
||||
// Redka 始终可用(内嵌数据库)// Logan.Li
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *redkaCache) Close() error {
|
||||
return r.db.Close()
|
||||
}
|
||||
@@ -1,189 +1,251 @@
|
||||
import { useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { useUserStore } from '@/store';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Copy, Check, Terminal, Download, Settings, Play, Cpu } from 'lucide-react';
|
||||
|
||||
interface AgentWizardProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}
|
||||
|
||||
/** 自动检测服务器地址:使用当前浏览器访问的地址(协议+主机+端口) */
|
||||
function detectServerAddress(): string {
|
||||
if (typeof window !== 'undefined' && window.location) {
|
||||
// 直接用当前浏览器的 origin(如 http://10.10.10.150),
|
||||
// Nginx 会将 /agent/ 请求代理到后端
|
||||
return window.location.origin;
|
||||
}
|
||||
return 'http://127.0.0.1:8080';
|
||||
}
|
||||
|
||||
export function AgentWizard({ open, onOpenChange }: AgentWizardProps) {
|
||||
const token = useUserStore((s) => s.token) || '<YOUR_TOKEN>';
|
||||
const serverUrl = detectServerAddress();
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const installCommand = `curl -fsSL ${serverUrl.replace(/^http:/, 'http:')}/agent/install.sh | bash -s -- --server ${serverUrl} --token ${token}`;
|
||||
|
||||
const handleCopy = async () => {
|
||||
try {
|
||||
// 优先使用现代 Clipboard API(需要 HTTPS 或 localhost)
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
await navigator.clipboard.writeText(installCommand);
|
||||
} else {
|
||||
// 降级方案:用隐藏 textarea + execCommand('copy'),兼容 HTTP 环境
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.value = installCommand;
|
||||
textarea.style.position = 'fixed';
|
||||
textarea.style.opacity = '0';
|
||||
textarea.style.pointerEvents = 'none';
|
||||
document.body.appendChild(textarea);
|
||||
textarea.select();
|
||||
const ok = document.execCommand('copy');
|
||||
document.body.removeChild(textarea);
|
||||
if (!ok) throw new Error('execCommand failed');
|
||||
}
|
||||
setCopied(true);
|
||||
toast.success('安装命令已复制');
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
} catch {
|
||||
toast.error('复制失败,请手动选中复制');
|
||||
}
|
||||
};
|
||||
|
||||
const binaryUrl = `${serverUrl}/agent/aiops-agent-linux-amd64`;
|
||||
|
||||
const manualSteps = [
|
||||
{
|
||||
icon: Download,
|
||||
title: '1. 下载二进制',
|
||||
commands: [
|
||||
`curl -fsSL ${binaryUrl} -o /usr/local/bin/aiops-agent`,
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: Settings,
|
||||
title: '2. 赋予执行权限',
|
||||
commands: ['chmod +x /usr/local/bin/aiops-agent'],
|
||||
},
|
||||
{
|
||||
icon: Cpu,
|
||||
title: '3. 创建 systemd 服务',
|
||||
commands: [
|
||||
`cat > /etc/systemd/system/aiops-agent.service << 'EOF'`,
|
||||
`[Unit]`,
|
||||
`Description=AIOps Agent`,
|
||||
`After=network.target`,
|
||||
``,
|
||||
`[Service]`,
|
||||
`Type=simple`,
|
||||
`ExecStart=/usr/local/bin/aiops-agent --server ${serverUrl} --token ${token}`,
|
||||
`Restart=always`,
|
||||
`RestartSec=5`,
|
||||
``,
|
||||
`[Install]`,
|
||||
`WantedBy=multi-user.target`,
|
||||
`EOF`,
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: Play,
|
||||
title: '4. 启动并设置开机自启',
|
||||
commands: [
|
||||
'systemctl daemon-reload',
|
||||
'systemctl enable --now aiops-agent',
|
||||
'systemctl status aiops-agent',
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-2xl bg-slate-900 border-slate-800 text-slate-100">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-slate-100">部署 Agent</DialogTitle>
|
||||
<DialogDescription className="text-slate-400">
|
||||
在目标主机上执行以下命令即可完成 Agent 安装与注册
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<Tabs defaultValue="auto" className="w-full">
|
||||
<TabsList className="bg-slate-800 border border-slate-700">
|
||||
<TabsTrigger value="auto" className="data-[state=active]:bg-slate-700">
|
||||
<Terminal className="h-4 w-4 mr-1.5" />
|
||||
安装Agent
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="manual" className="data-[state=active]:bg-slate-700">
|
||||
手动安装
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* 自动安装 */}
|
||||
<TabsContent value="auto" className="mt-4 space-y-3">
|
||||
<p className="text-sm text-slate-400">
|
||||
在目标主机执行以下命令,Agent 将自动下载、安装并启动:
|
||||
</p>
|
||||
<div className="relative rounded-lg bg-slate-950 border border-slate-800 p-4">
|
||||
<pre className="text-xs text-emerald-400 font-mono whitespace-pre-wrap break-all pr-10">
|
||||
{installCommand}
|
||||
</pre>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
onClick={handleCopy}
|
||||
className="absolute top-2 right-2 h-8 w-8 text-slate-400 hover:text-slate-100 hover:bg-slate-800"
|
||||
title="复制"
|
||||
>
|
||||
{copied ? <Check className="h-4 w-4 text-emerald-400" /> : <Copy className="h-4 w-4" />}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex items-start gap-2 text-xs text-slate-500">
|
||||
<Terminal className="h-3.5 w-3.5 mt-0.5 shrink-0" />
|
||||
<span>
|
||||
服务器地址已自动检测为当前访问地址({serverUrl})。Agent 安装成功后将自动连接并开始上报指标。
|
||||
</span>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
{/* 手动安装 */}
|
||||
<TabsContent value="manual" className="mt-4 space-y-4">
|
||||
<div className="max-h-[420px] overflow-y-auto space-y-3 pr-1">
|
||||
{manualSteps.map((step) => (
|
||||
<div key={step.title} className="rounded-lg bg-slate-950 border border-slate-800 overflow-hidden">
|
||||
<div className="flex items-center gap-2 px-4 py-2.5 bg-slate-800/50">
|
||||
<step.icon className="h-4 w-4 text-blue-400" />
|
||||
<span className="text-sm font-medium text-slate-200">{step.title}</span>
|
||||
</div>
|
||||
<div className="p-3 space-y-1">
|
||||
{step.commands.map((cmd, idx) => (
|
||||
<div key={idx} className="flex items-start gap-2">
|
||||
<span className="text-slate-600 font-mono text-xs select-none mt-0.5">$</span>
|
||||
<code className="text-xs text-slate-300 font-mono whitespace-pre-wrap break-all flex-1">
|
||||
{cmd}
|
||||
</code>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-xs text-slate-500">
|
||||
完成上述步骤后,Agent 将作为 systemd 服务运行并自动上报指标。
|
||||
</p>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export default AgentWizard;
|
||||
1|import { useState } from 'react';
|
||||
2|import { toast } from 'sonner';
|
||||
3|import { useUserStore } from '@/store';
|
||||
4|import {
|
||||
5| Dialog,
|
||||
6| DialogContent,
|
||||
7| DialogDescription,
|
||||
8| DialogHeader,
|
||||
9| DialogTitle,
|
||||
10|} from '@/components/ui/dialog';
|
||||
11|import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
12|import { Button } from '@/components/ui/button';
|
||||
13|import { Copy, Check, Terminal, Download, Settings, Play, Cpu } from 'lucide-react';
|
||||
14|
|
||||
15|interface AgentWizardProps {
|
||||
16| open: boolean;
|
||||
17| onOpenChange: (open: boolean) => void;
|
||||
18|}
|
||||
19|
|
||||
20|/** 自动检测服务器地址:使用当前浏览器访问的地址(协议+主机+端口) */
|
||||
21|function detectServerAddress(): string {
|
||||
22| if (typeof window !== 'undefined' && window.location) {
|
||||
23| // 直接用当前浏览器的 origin(如 http://10.10.10.150),
|
||||
24| // Nginx 会将 /agent/ 请求代理到后端
|
||||
25| return window.location.origin;
|
||||
26| }
|
||||
27| return 'http://127.0.0.1:8080';
|
||||
28|}
|
||||
29|
|
||||
30|export function AgentWizard({ open, onOpenChange }: AgentWizardProps) {
|
||||
31| const token = useUserStore((s) => s.token) || '<YOUR_TOKEN>';
|
||||
32| const serverUrl = detectServerAddress();
|
||||
33| const [copied, setCopied] = useState(false);
|
||||
34| const [showManualInput, setShowManualInput] = useState(false);
|
||||
35|
|
||||
36| const installCommand = `curl -fsSL ${serverUrl.replace(/^http:/, 'http:')}/agent/install.sh | bash -s -- --server ${serverUrl} --token ${token}`;
|
||||
37|
|
||||
38| const fallbackCopy = (text: string): boolean => {
|
||||
39| const textArea = document.createElement('textarea');
|
||||
40| textArea.value = text;
|
||||
41| textArea.style.position = 'fixed';
|
||||
42| textArea.style.left = '-9999px';
|
||||
43| textArea.style.top = '-9999px';
|
||||
44| document.body.appendChild(textArea);
|
||||
45| textArea.focus();
|
||||
46| textArea.select();
|
||||
47| let success = false;
|
||||
48| try {
|
||||
49|<<<<<<< HEAD
|
||||
50| // 优先使用现代 Clipboard API(需要 HTTPS 或 localhost)
|
||||
51| if (navigator.clipboard && window.isSecureContext) {
|
||||
52| await navigator.clipboard.writeText(installCommand);
|
||||
53| } else {
|
||||
54| // 降级方案:用隐藏 textarea + execCommand('copy'),兼容 HTTP 环境
|
||||
55| const textarea = document.createElement('textarea');
|
||||
56| textarea.value = installCommand;
|
||||
57| textarea.style.position = 'fixed';
|
||||
58| textarea.style.opacity = '0';
|
||||
59| textarea.style.pointerEvents = 'none';
|
||||
60| document.body.appendChild(textarea);
|
||||
61| textarea.select();
|
||||
62| const ok = document.execCommand('copy');
|
||||
63| document.body.removeChild(textarea);
|
||||
64| if (!ok) throw new Error('execCommand failed');
|
||||
65| }
|
||||
66| setCopied(true);
|
||||
67| toast.success('安装命令已复制');
|
||||
68| setTimeout(() => setCopied(false), 2000);
|
||||
69| } catch {
|
||||
70| toast.error('复制失败,请手动选中复制');
|
||||
71|=======
|
||||
72| success = document.execCommand('copy');
|
||||
73| } catch {
|
||||
74| // execCommand also failed
|
||||
75| }
|
||||
76| document.body.removeChild(textArea);
|
||||
77| return success;
|
||||
78| };
|
||||
79|
|
||||
80| const handleCopy = async () => {
|
||||
81| const cmd = installCommand;
|
||||
82| let success = false;
|
||||
83|
|
||||
84| // 方式1: Clipboard API(HTTPS / localhost)
|
||||
85| try {
|
||||
86| await navigator.clipboard.writeText(cmd);
|
||||
87| success = true;
|
||||
88| } catch {
|
||||
89| // Clipboard API 不可用
|
||||
90| }
|
||||
91|
|
||||
92| // 方式2: execCommand fallback(HTTP 也可用)
|
||||
93| if (!success) {
|
||||
94| success = fallbackCopy(cmd);
|
||||
95| }
|
||||
96|
|
||||
97| if (success) {
|
||||
98| setCopied(true);
|
||||
99| toast.success('安装命令已复制到剪贴板,粘贴到目标主机执行');
|
||||
100| setTimeout(() => setCopied(false), 3000);
|
||||
101| } else {
|
||||
102| toast.error('自动复制失败,请在下方代码框中手动全选复制 (Ctrl+A → Ctrl+C)');
|
||||
103| // 切换到手选模式,展示可编辑文本框
|
||||
104| setShowManualInput(true);
|
||||
105|>>>>>>> ca18ead (feat: run.sh四模式(dev/docker/build/binary) + CLI flags + Redka内嵌Redis + Agent复制修复 + 服务器编辑IP字段 + 安装Agent按钮)
|
||||
106| }
|
||||
107| };
|
||||
108|
|
||||
109| const binaryUrl = `${serverUrl}/agent/aiops-agent-linux-amd64`;
|
||||
110|
|
||||
111| const manualSteps = [
|
||||
112| {
|
||||
113| icon: Download,
|
||||
114| title: '1. 下载二进制',
|
||||
115| commands: [
|
||||
116| `curl -fsSL ${binaryUrl} -o /usr/local/bin/aiops-agent`,
|
||||
117| ],
|
||||
118| },
|
||||
119| {
|
||||
120| icon: Settings,
|
||||
121| title: '2. 赋予执行权限',
|
||||
122| commands: ['chmod +x /usr/local/bin/aiops-agent'],
|
||||
123| },
|
||||
124| {
|
||||
125| icon: Cpu,
|
||||
126| title: '3. 创建 systemd 服务',
|
||||
127| commands: [
|
||||
128| `cat > /etc/systemd/system/aiops-agent.service << 'EOF'`,
|
||||
129| `[Unit]`,
|
||||
130| `Description=AIOps Agent`,
|
||||
131| `After=network.target`,
|
||||
132| ``,
|
||||
133| `[Service]`,
|
||||
134| `Type=simple`,
|
||||
135| `ExecStart=/usr/local/bin/aiops-agent --server ${serverUrl} --token ${token}`,
|
||||
136| `Restart=always`,
|
||||
137| `RestartSec=5`,
|
||||
138| ``,
|
||||
139| `[Install]`,
|
||||
140| `WantedBy=multi-user.target`,
|
||||
141| `EOF`,
|
||||
142| ],
|
||||
143| },
|
||||
144| {
|
||||
145| icon: Play,
|
||||
146| title: '4. 启动并设置开机自启',
|
||||
147| commands: [
|
||||
148| 'systemctl daemon-reload',
|
||||
149| 'systemctl enable --now aiops-agent',
|
||||
150| 'systemctl status aiops-agent',
|
||||
151| ],
|
||||
152| },
|
||||
153| ];
|
||||
154|
|
||||
155| return (
|
||||
156| <Dialog open={open} onOpenChange={onOpenChange}>
|
||||
157| <DialogContent className="max-w-2xl bg-slate-900 border-slate-800 text-slate-100">
|
||||
158| <DialogHeader>
|
||||
159| <DialogTitle className="text-slate-100">部署 Agent</DialogTitle>
|
||||
160| <DialogDescription className="text-slate-400">
|
||||
161| 在目标主机上执行以下命令即可完成 Agent 安装与注册
|
||||
162| </DialogDescription>
|
||||
163| </DialogHeader>
|
||||
164|
|
||||
165| <Tabs defaultValue="auto" className="w-full">
|
||||
166| <TabsList className="bg-slate-800 border border-slate-700">
|
||||
167| <TabsTrigger value="auto" className="data-[state=active]:bg-slate-700">
|
||||
168| <Terminal className="h-4 w-4 mr-1.5" />
|
||||
169| 安装Agent
|
||||
170| </TabsTrigger>
|
||||
171| <TabsTrigger value="manual" className="data-[state=active]:bg-slate-700">
|
||||
172| 手动安装
|
||||
173| </TabsTrigger>
|
||||
174| </TabsList>
|
||||
175|
|
||||
176| {/* 自动安装 */}
|
||||
177| <TabsContent value="auto" className="mt-4 space-y-3">
|
||||
178| <p className="text-sm text-slate-400">
|
||||
179| 在目标主机执行以下命令,Agent 将自动下载、安装并启动:
|
||||
180| </p>
|
||||
181| <div className="relative rounded-lg bg-slate-950 border border-slate-800 p-4">
|
||||
182| <pre className="text-xs text-emerald-400 font-mono whitespace-pre-wrap break-all pr-10">
|
||||
183| {installCommand}
|
||||
184| </pre>
|
||||
185| <Button
|
||||
186| size="icon"
|
||||
187| variant="ghost"
|
||||
188| onClick={handleCopy}
|
||||
189| className="absolute top-2 right-2 h-8 w-8 text-slate-400 hover:text-slate-100 hover:bg-slate-800"
|
||||
190| title="复制到剪贴板"
|
||||
191| >
|
||||
192| {copied ? <Check className="h-4 w-4 text-emerald-400" /> : <Copy className="h-4 w-4" />}
|
||||
193| </Button>
|
||||
194| </div>
|
||||
195|
|
||||
196| {/* 手动复制模式:当自动复制失败时展示可全选的文本域 */}
|
||||
197| {showManualInput && (
|
||||
198| <div className="rounded-lg bg-amber-950/30 border border-amber-800/50 p-4">
|
||||
199| <p className="text-xs text-amber-400 mb-2">
|
||||
200| ⚠️ 自动复制失败。请点击下方输入框全选 (Ctrl+A) 后复制 (Ctrl+C):
|
||||
201| </p>
|
||||
202| <textarea
|
||||
203| readOnly
|
||||
204| value={installCommand}
|
||||
205| className="w-full h-24 bg-slate-950 border border-amber-800/30 text-emerald-400 font-mono text-xs p-3 rounded resize-none focus:outline-none focus:border-amber-600"
|
||||
206| onClick={(e) => (e.target as HTMLTextAreaElement).select()}
|
||||
207| />
|
||||
208| </div>
|
||||
209| )}
|
||||
210| <div className="flex items-start gap-2 text-xs text-slate-500">
|
||||
211| <Terminal className="h-3.5 w-3.5 mt-0.5 shrink-0" />
|
||||
212| <span>
|
||||
213| 服务器地址已自动检测为当前访问地址({serverUrl})。Agent 安装成功后将自动连接并开始上报指标。
|
||||
214| </span>
|
||||
215| </div>
|
||||
216| </TabsContent>
|
||||
217|
|
||||
218| {/* 手动安装 */}
|
||||
219| <TabsContent value="manual" className="mt-4 space-y-4">
|
||||
220| <div className="max-h-[420px] overflow-y-auto space-y-3 pr-1">
|
||||
221| {manualSteps.map((step) => (
|
||||
222| <div key={step.title} className="rounded-lg bg-slate-950 border border-slate-800 overflow-hidden">
|
||||
223| <div className="flex items-center gap-2 px-4 py-2.5 bg-slate-800/50">
|
||||
224| <step.icon className="h-4 w-4 text-blue-400" />
|
||||
225| <span className="text-sm font-medium text-slate-200">{step.title}</span>
|
||||
226| </div>
|
||||
227| <div className="p-3 space-y-1">
|
||||
228| {step.commands.map((cmd, idx) => (
|
||||
229| <div key={idx} className="flex items-start gap-2">
|
||||
230| <span className="text-slate-600 font-mono text-xs select-none mt-0.5">$</span>
|
||||
231| <code className="text-xs text-slate-300 font-mono whitespace-pre-wrap break-all flex-1">
|
||||
232| {cmd}
|
||||
233| </code>
|
||||
234| </div>
|
||||
235| ))}
|
||||
236| </div>
|
||||
237| </div>
|
||||
238| ))}
|
||||
239| </div>
|
||||
240| <p className="text-xs text-slate-500">
|
||||
241| 完成上述步骤后,Agent 将作为 systemd 服务运行并自动上报指标。
|
||||
242| </p>
|
||||
243| </TabsContent>
|
||||
244| </Tabs>
|
||||
245| </DialogContent>
|
||||
246| </Dialog>
|
||||
247| );
|
||||
248|}
|
||||
249|
|
||||
250|export default AgentWizard;
|
||||
251|
|
||||
@@ -48,6 +48,7 @@ import { ExcelServerImport } from './ExcelServerImport';
|
||||
import { ServerForm, EMPTY_FORM, type ServerFormData } from './ServerForm';
|
||||
import { getTags } from './serverHelpers';
|
||||
import { ServerTableRow } from './ServerTableRow';
|
||||
import { AgentWizard } from '@/components/agent/AgentWizard';
|
||||
|
||||
export function ServerManager() {
|
||||
const confirm = useConfirm();
|
||||
@@ -70,6 +71,12 @@ export function ServerManager() {
|
||||
const [terminalOpen, setTerminalOpen] = useState(false);
|
||||
const [terminalServer, setTerminalServer] = useState<{ id: number; name: string; ip: string } | null>(null);
|
||||
|
||||
// Agent 安装向导状态
|
||||
const [agentWizardOpen, setAgentWizardOpen] = useState(false);
|
||||
const openInstallAgent = (server: any) => {
|
||||
setAgentWizardOpen(true);
|
||||
};
|
||||
|
||||
const openTerminal = (server: any) => {
|
||||
setTerminalServer({ id: server.id, name: server.hostname, ip: server.ip });
|
||||
setTerminalOpen(true);
|
||||
@@ -217,10 +224,12 @@ export function ServerManager() {
|
||||
} else {
|
||||
await serverApi.createServer(payload);
|
||||
}
|
||||
toast.success(editingId ? '服务器已更新' : '服务器已创建');
|
||||
setFormOpen(false);
|
||||
loadData();
|
||||
} catch (e) {
|
||||
console.error('保存服务器失败:', e);
|
||||
toast.error('保存失败,请重试');
|
||||
} finally {
|
||||
setFormSaving(false);
|
||||
}
|
||||
@@ -411,6 +420,7 @@ export function ServerManager() {
|
||||
onOpenTerminal={openTerminal}
|
||||
onOpenEdit={openEditForm}
|
||||
onDelete={handleDelete}
|
||||
onInstallAgent={openInstallAgent}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
@@ -449,6 +459,9 @@ export function ServerManager() {
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Agent 安装向导 */}
|
||||
<AgentWizard open={agentWizardOpen} onOpenChange={setAgentWizardOpen} />
|
||||
|
||||
{/* 终端 Dialog */}
|
||||
<TerminalDialog
|
||||
open={terminalOpen}
|
||||
|
||||
@@ -1,166 +1,180 @@
|
||||
// 服务器表格行 — 纯展示子组件(接收 props,不直接调 API) // Logan.Li
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { TableCell, TableRow } from '@/components/ui/table';
|
||||
import { Edit, MoreHorizontal, Server as ServerIcon, Terminal, Trash2, Download } from 'lucide-react';
|
||||
import { getStatusBadge, getTags, getUsageColor } from './serverHelpers';
|
||||
|
||||
interface ServerTableRowProps {
|
||||
server: any;
|
||||
selected: boolean;
|
||||
onToggleSelect: (id: string) => void;
|
||||
onOpenTerminal: (server: any) => void;
|
||||
onOpenEdit: (server: any) => void;
|
||||
onDelete: (id: string) => void;
|
||||
}
|
||||
|
||||
export function ServerTableRow({
|
||||
server,
|
||||
selected,
|
||||
onToggleSelect,
|
||||
onOpenTerminal,
|
||||
onOpenEdit,
|
||||
onDelete,
|
||||
}: ServerTableRowProps) {
|
||||
const cpuUsage = Number(server.cpu_usage || 0);
|
||||
const memoryUsage = Number(server.memory_usage || 0);
|
||||
const tags = getTags(server);
|
||||
const memGB = server.memory ? Math.round(server.memory / 1024) : 0;
|
||||
|
||||
return (
|
||||
<TableRow className="border-[var(--dg-border)] hover:bg-[var(--dg-bg-tertiary)]/50">
|
||||
<TableCell>
|
||||
<Checkbox
|
||||
checked={selected}
|
||||
onCheckedChange={() => onToggleSelect(server.id)}
|
||||
className="border-[var(--dg-border)]"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2">
|
||||
<ServerIcon className="h-4 w-4 text-[var(--dg-text-muted)]" />
|
||||
<div>
|
||||
<p className="font-medium text-[var(--dg-text-primary)]">{server.hostname}</p>
|
||||
<p className="text-xs text-[var(--dg-text-muted)]">{server.os}</p>
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<code className="text-sm text-[var(--dg-text-secondary)] bg-[var(--dg-bg-tertiary)] px-2 py-0.5 rounded">
|
||||
{server.ip}
|
||||
</code>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{server.public_ip ? (
|
||||
<code className="text-sm text-[var(--dg-cyan)] bg-[var(--dg-cyan)]/10 px-2 py-0.5 rounded">
|
||||
{server.public_ip}
|
||||
</code>
|
||||
) : (
|
||||
<span className="text-sm text-[var(--dg-text-muted)]">-</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>{getStatusBadge(server.status)}</TableCell>
|
||||
<TableCell>
|
||||
<div className="text-sm text-[var(--dg-text-secondary)]">
|
||||
<span className="text-[var(--dg-text-muted)]">{server.cpu}核</span>
|
||||
<span className="mx-1 text-[var(--dg-text-muted)] opacity-50">/</span>
|
||||
<span className="text-[var(--dg-text-muted)]">{memGB}GB</span>
|
||||
<span className="mx-1 text-[var(--dg-text-muted)] opacity-50">/</span>
|
||||
<span className="text-[var(--dg-text-muted)]">{server.disk}GB</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-[var(--dg-text-muted)] w-8">CPU</span>
|
||||
<div className="w-16 h-1.5 bg-[var(--dg-bg-tertiary)] rounded-full overflow-hidden">
|
||||
<div
|
||||
className={cn('h-full rounded-full', getUsageColor(cpuUsage))}
|
||||
style={{ width: `${cpuUsage}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-xs text-[var(--dg-text-secondary)]">{cpuUsage.toFixed(1)}%</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-[var(--dg-text-muted)] w-8">内存</span>
|
||||
<div className="w-16 h-1.5 bg-[var(--dg-bg-tertiary)] rounded-full overflow-hidden">
|
||||
<div
|
||||
className={cn('h-full rounded-full', getUsageColor(memoryUsage))}
|
||||
style={{ width: `${memoryUsage}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-xs text-[var(--dg-text-secondary)]">{memoryUsage.toFixed(1)}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{tags.slice(0, 3).map((tag) => (
|
||||
<Badge
|
||||
key={tag}
|
||||
variant="outline"
|
||||
className="text-xs border-[var(--dg-border)] text-[var(--dg-text-secondary)] bg-[var(--dg-bg-tertiary)]/50"
|
||||
>
|
||||
{tag}
|
||||
</Badge>
|
||||
))}
|
||||
{tags.length > 3 && (
|
||||
<Badge variant="outline" className="text-xs border-[var(--dg-border)] text-[var(--dg-text-secondary)]">
|
||||
+{tags.length - 3}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<div className="flex items-center justify-end gap-1">
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8 text-[var(--dg-text-secondary)] hover:text-[var(--dg-blue)]" onClick={() => onOpenTerminal(server)}>
|
||||
<Terminal className="h-4 w-4" />
|
||||
</Button>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8 text-[var(--dg-text-secondary)] hover:text-[var(--dg-text-primary)]">
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-48 bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)]">
|
||||
<DropdownMenuLabel className="text-[var(--dg-text-secondary)]">操作</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator className="bg-[var(--dg-border)]" />
|
||||
<DropdownMenuItem
|
||||
className="text-[var(--dg-text-secondary)] focus:bg-[var(--dg-bg-hover)] focus:text-[var(--dg-text-primary)] cursor-pointer"
|
||||
onClick={() => onOpenEdit(server)}
|
||||
>
|
||||
<Edit className="h-4 w-4 mr-2" />
|
||||
编辑
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="text-[var(--dg-text-secondary)] focus:bg-[var(--dg-bg-hover)] focus:text-[var(--dg-text-primary)] cursor-pointer"
|
||||
onClick={() => onOpenTerminal(server)}
|
||||
>
|
||||
<Terminal className="h-4 w-4 mr-2" />
|
||||
终端
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator className="bg-[var(--dg-border)]" />
|
||||
<DropdownMenuItem
|
||||
className="text-red-400 focus:bg-[var(--dg-bg-hover)] focus:text-red-400 cursor-pointer"
|
||||
onClick={() => onDelete(server.id)}
|
||||
>
|
||||
<Trash2 className="h-4 w-4 mr-2" />
|
||||
删除
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
1|// 服务器表格行 — 纯展示子组件(接收 props,不直接调 API) // Logan.Li
|
||||
2|import { cn } from '@/lib/utils';
|
||||
3|import { Badge } from '@/components/ui/badge';
|
||||
4|import { Button } from '@/components/ui/button';
|
||||
5|import { Checkbox } from '@/components/ui/checkbox';
|
||||
6|import {
|
||||
7| DropdownMenu,
|
||||
8| DropdownMenuContent,
|
||||
9| DropdownMenuItem,
|
||||
10| DropdownMenuLabel,
|
||||
11| DropdownMenuSeparator,
|
||||
12| DropdownMenuTrigger,
|
||||
13|} from '@/components/ui/dropdown-menu';
|
||||
14|import { TableCell, TableRow } from '@/components/ui/table';
|
||||
15|<<<<<<< HEAD
|
||||
16|import { Edit, MoreHorizontal, Server as ServerIcon, Terminal, Trash2, Download } from 'lucide-react';
|
||||
17|=======
|
||||
18|import { Cpu, Edit, MoreHorizontal, Server as ServerIcon, Terminal, Trash2 } from 'lucide-react';
|
||||
19|>>>>>>> ca18ead (feat: run.sh四模式(dev/docker/build/binary) + CLI flags + Redka内嵌Redis + Agent复制修复 + 服务器编辑IP字段 + 安装Agent按钮)
|
||||
20|import { getStatusBadge, getTags, getUsageColor } from './serverHelpers';
|
||||
21|
|
||||
22|interface ServerTableRowProps {
|
||||
23| server: any;
|
||||
24| selected: boolean;
|
||||
25| onToggleSelect: (id: string) => void;
|
||||
26| onOpenTerminal: (server: any) => void;
|
||||
27| onOpenEdit: (server: any) => void;
|
||||
28| onDelete: (id: string) => void;
|
||||
29| onInstallAgent: (server: any) => void;
|
||||
30|}
|
||||
31|
|
||||
32|export function ServerTableRow({
|
||||
33| server,
|
||||
34| selected,
|
||||
35| onToggleSelect,
|
||||
36| onOpenTerminal,
|
||||
37| onOpenEdit,
|
||||
38| onDelete,
|
||||
39| onInstallAgent,
|
||||
40|}: ServerTableRowProps) {
|
||||
41| const cpuUsage = Number(server.cpu_usage || 0);
|
||||
42| const memoryUsage = Number(server.memory_usage || 0);
|
||||
43| const tags = getTags(server);
|
||||
44| const memGB = server.memory ? Math.round(server.memory / 1024) : 0;
|
||||
45|
|
||||
46| return (
|
||||
47| <TableRow className="border-[var(--dg-border)] hover:bg-[var(--dg-bg-tertiary)]/50">
|
||||
48| <TableCell>
|
||||
49| <Checkbox
|
||||
50| checked={selected}
|
||||
51| onCheckedChange={() => onToggleSelect(server.id)}
|
||||
52| className="border-[var(--dg-border)]"
|
||||
53| />
|
||||
54| </TableCell>
|
||||
55| <TableCell>
|
||||
56| <div className="flex items-center gap-2">
|
||||
57| <ServerIcon className="h-4 w-4 text-[var(--dg-text-muted)]" />
|
||||
58| <div>
|
||||
59| <p className="font-medium text-[var(--dg-text-primary)]">{server.hostname}</p>
|
||||
60| <p className="text-xs text-[var(--dg-text-muted)]">{server.os}</p>
|
||||
61| </div>
|
||||
62| </div>
|
||||
63| </TableCell>
|
||||
64| <TableCell>
|
||||
65| <code className="text-sm text-[var(--dg-text-secondary)] bg-[var(--dg-bg-tertiary)] px-2 py-0.5 rounded">
|
||||
66| {server.ip}
|
||||
67| </code>
|
||||
68| </TableCell>
|
||||
69| <TableCell>
|
||||
70| {server.public_ip ? (
|
||||
71| <code className="text-sm text-[var(--dg-cyan)] bg-[var(--dg-cyan)]/10 px-2 py-0.5 rounded">
|
||||
72| {server.public_ip}
|
||||
73| </code>
|
||||
74| ) : (
|
||||
75| <span className="text-sm text-[var(--dg-text-muted)]">-</span>
|
||||
76| )}
|
||||
77| </TableCell>
|
||||
78| <TableCell>{getStatusBadge(server.status)}</TableCell>
|
||||
79| <TableCell>
|
||||
80| <div className="text-sm text-[var(--dg-text-secondary)]">
|
||||
81| <span className="text-[var(--dg-text-muted)]">{server.cpu}核</span>
|
||||
82| <span className="mx-1 text-[var(--dg-text-muted)] opacity-50">/</span>
|
||||
83| <span className="text-[var(--dg-text-muted)]">{memGB}GB</span>
|
||||
84| <span className="mx-1 text-[var(--dg-text-muted)] opacity-50">/</span>
|
||||
85| <span className="text-[var(--dg-text-muted)]">{server.disk}GB</span>
|
||||
86| </div>
|
||||
87| </TableCell>
|
||||
88| <TableCell>
|
||||
89| <div className="space-y-1">
|
||||
90| <div className="flex items-center gap-2">
|
||||
91| <span className="text-xs text-[var(--dg-text-muted)] w-8">CPU</span>
|
||||
92| <div className="w-16 h-1.5 bg-[var(--dg-bg-tertiary)] rounded-full overflow-hidden">
|
||||
93| <div
|
||||
94| className={cn('h-full rounded-full', getUsageColor(cpuUsage))}
|
||||
95| style={{ width: `${cpuUsage}%` }}
|
||||
96| />
|
||||
97| </div>
|
||||
98| <span className="text-xs text-[var(--dg-text-secondary)]">{cpuUsage.toFixed(1)}%</span>
|
||||
99| </div>
|
||||
100| <div className="flex items-center gap-2">
|
||||
101| <span className="text-xs text-[var(--dg-text-muted)] w-8">内存</span>
|
||||
102| <div className="w-16 h-1.5 bg-[var(--dg-bg-tertiary)] rounded-full overflow-hidden">
|
||||
103| <div
|
||||
104| className={cn('h-full rounded-full', getUsageColor(memoryUsage))}
|
||||
105| style={{ width: `${memoryUsage}%` }}
|
||||
106| />
|
||||
107| </div>
|
||||
108| <span className="text-xs text-[var(--dg-text-secondary)]">{memoryUsage.toFixed(1)}%</span>
|
||||
109| </div>
|
||||
110| </div>
|
||||
111| </TableCell>
|
||||
112| <TableCell>
|
||||
113| <div className="flex flex-wrap gap-1">
|
||||
114| {tags.slice(0, 3).map((tag) => (
|
||||
115| <Badge
|
||||
116| key={tag}
|
||||
117| variant="outline"
|
||||
118| className="text-xs border-[var(--dg-border)] text-[var(--dg-text-secondary)] bg-[var(--dg-bg-tertiary)]/50"
|
||||
119| >
|
||||
120| {tag}
|
||||
121| </Badge>
|
||||
122| ))}
|
||||
123| {tags.length > 3 && (
|
||||
124| <Badge variant="outline" className="text-xs border-[var(--dg-border)] text-[var(--dg-text-secondary)]">
|
||||
125| +{tags.length - 3}
|
||||
126| </Badge>
|
||||
127| )}
|
||||
128| </div>
|
||||
129| </TableCell>
|
||||
130| <TableCell className="text-right">
|
||||
131| <div className="flex items-center justify-end gap-1">
|
||||
132| <Button variant="ghost" size="icon" className="h-8 w-8 text-[var(--dg-text-secondary)] hover:text-[var(--dg-blue)]" onClick={() => onOpenTerminal(server)}>
|
||||
133| <Terminal className="h-4 w-4" />
|
||||
134| </Button>
|
||||
135| <DropdownMenu>
|
||||
136| <DropdownMenuTrigger asChild>
|
||||
137| <Button variant="ghost" size="icon" className="h-8 w-8 text-[var(--dg-text-secondary)] hover:text-[var(--dg-text-primary)]">
|
||||
138| <MoreHorizontal className="h-4 w-4" />
|
||||
139| </Button>
|
||||
140| </DropdownMenuTrigger>
|
||||
141| <DropdownMenuContent align="end" className="w-48 bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)]">
|
||||
142| <DropdownMenuLabel className="text-[var(--dg-text-secondary)]">操作</DropdownMenuLabel>
|
||||
143| <DropdownMenuSeparator className="bg-[var(--dg-border)]" />
|
||||
144| <DropdownMenuItem
|
||||
145| className="text-[var(--dg-text-secondary)] focus:bg-[var(--dg-bg-hover)] focus:text-[var(--dg-text-primary)] cursor-pointer"
|
||||
146| onClick={() => onOpenEdit(server)}
|
||||
147| >
|
||||
148| <Edit className="h-4 w-4 mr-2" />
|
||||
149| 编辑
|
||||
150| </DropdownMenuItem>
|
||||
151| <DropdownMenuItem
|
||||
152| className="text-[var(--dg-text-secondary)] focus:bg-[var(--dg-bg-hover)] focus:text-[var(--dg-text-primary)] cursor-pointer"
|
||||
153| onClick={() => onOpenTerminal(server)}
|
||||
154| >
|
||||
155| <Terminal className="h-4 w-4 mr-2" />
|
||||
156| 终端
|
||||
157| </DropdownMenuItem>
|
||||
158| <DropdownMenuItem
|
||||
159| className="text-[var(--dg-text-secondary)] focus:bg-[var(--dg-bg-hover)] focus:text-[var(--dg-text-primary)] cursor-pointer"
|
||||
160| onClick={() => onInstallAgent(server)}
|
||||
161| >
|
||||
162| <Cpu className="h-4 w-4 mr-2" />
|
||||
163| 安装Agent
|
||||
164| </DropdownMenuItem>
|
||||
165| <DropdownMenuSeparator className="bg-[var(--dg-border)]" />
|
||||
166| <DropdownMenuItem
|
||||
167| className="text-red-400 focus:bg-[var(--dg-bg-hover)] focus:text-red-400 cursor-pointer"
|
||||
168| onClick={() => onDelete(server.id)}
|
||||
169| >
|
||||
170| <Trash2 className="h-4 w-4 mr-2" />
|
||||
171| 删除
|
||||
172| </DropdownMenuItem>
|
||||
173| </DropdownMenuContent>
|
||||
174| </DropdownMenu>
|
||||
175| </div>
|
||||
176| </TableCell>
|
||||
177| </TableRow>
|
||||
178| );
|
||||
179|}
|
||||
180|
|
||||
@@ -1,19 +1,27 @@
|
||||
#!/bin/bash
|
||||
# EZOps AIOps 智能运维平台 - 启动脚本
|
||||
# 一键启动前后端服务,支持日志轮转
|
||||
# 用法: ./run.sh [start|stop|restart|status|frontend|backend|help]
|
||||
|
||||
#===========================================================================
|
||||
# AIOps 智能运维平台 - 统一启动脚本
|
||||
# 支持三种运行模式:本地开发 / Docker容器部署 / 独立二进制
|
||||
#
|
||||
# 用法:
|
||||
# ./run.sh dev 本地开发模式(前后端热重载)
|
||||
# ./run.sh docker up Docker Compose 容器部署
|
||||
# ./run.sh build 编译独立二进制 ./aiops
|
||||
# ./run.sh binary 编译并启动独立二进制
|
||||
# ./run.sh help 查看帮助
|
||||
#===========================================================================
|
||||
set -e
|
||||
|
||||
# ============================================================
|
||||
# 配置区
|
||||
# ============================================================
|
||||
PROJECT_NAME="EZOps AIOps"
|
||||
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
|
||||
@@ -26,12 +34,14 @@ RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
CYAN='\033[0;36m'
|
||||
NC='\033[0m'
|
||||
|
||||
info() { echo -e "${BLUE}[INFO]${NC} $1"; }
|
||||
success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; }
|
||||
warning() { echo -e "${YELLOW}[WARNING]${NC} $1"; }
|
||||
error() { echo -e "${RED}[ERROR]${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"
|
||||
@@ -40,27 +50,49 @@ cd "$SCRIPT_DIR"
|
||||
BACKEND_PID_FILE="$SCRIPT_DIR/$BACKEND_DIR/.backend.pid"
|
||||
FRONTEND_PID_FILE="$SCRIPT_DIR/$FRONTEND_DIR/.frontend.pid"
|
||||
|
||||
# 检测操作系统
|
||||
detect_os() {
|
||||
case "$(uname -s)" in
|
||||
Darwin) echo "macos" ;;
|
||||
Linux) echo "linux" ;;
|
||||
*) echo "unknown" ;;
|
||||
esac
|
||||
}
|
||||
OS=$(detect_os)
|
||||
|
||||
# 检查端口是否被占用
|
||||
check_port() {
|
||||
if [ "$OS" = "macos" ]; then
|
||||
lsof -ti:$1 2>/dev/null | head -1
|
||||
else
|
||||
ss -tlnp 2>/dev/null | grep -q ":$1 " && echo "in-use" || echo ""
|
||||
fi
|
||||
}
|
||||
|
||||
# 释放端口
|
||||
kill_port() {
|
||||
local pids=$(lsof -ti:$1 2>/dev/null || echo "")
|
||||
if [ -n "$pids" ]; then
|
||||
info "释放端口 $1..."
|
||||
echo "$pids" | xargs kill -9 2>/dev/null || true
|
||||
sleep 1
|
||||
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=$(stat -f%z "$log_file" 2>/dev/null || stat -c%s "$log_file" 2>/dev/null || echo "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
|
||||
@@ -72,20 +104,18 @@ rotate_log() {
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# 后端服务
|
||||
# 后端服务(本地开发模式)
|
||||
# ============================================================
|
||||
start_backend() {
|
||||
info "正在启动后端服务..."
|
||||
info "启动后端服务..."
|
||||
cd "$SCRIPT_DIR/$BACKEND_DIR"
|
||||
mkdir -p logs
|
||||
rotate_log "logs/server.log"
|
||||
|
||||
# 释放被占用的端口
|
||||
kill_port $BACKEND_PORT
|
||||
|
||||
export GIN_MODE=debug
|
||||
|
||||
# 编译并运行(SQLite 需要 CGO)
|
||||
info "编译 Go 程序..."
|
||||
CGO_ENABLED=1 go build -o "$BACKEND_BIN" ./cmd/ 2>&1 || {
|
||||
error "编译失败"
|
||||
@@ -100,14 +130,14 @@ start_backend() {
|
||||
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"
|
||||
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)"
|
||||
success "后端已启动 (PID: $(cat "$BACKEND_PID_FILE"), 端口: $BACKEND_PORT)"
|
||||
return 0
|
||||
}
|
||||
|
||||
@@ -116,7 +146,7 @@ stop_backend() {
|
||||
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 "后端服务已停止"
|
||||
success "后端已停止"
|
||||
fi
|
||||
rm -f "$BACKEND_PID_FILE"
|
||||
fi
|
||||
@@ -124,10 +154,10 @@ stop_backend() {
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# 前端服务
|
||||
# 前端服务(本地开发模式)
|
||||
# ============================================================
|
||||
start_frontend() {
|
||||
info "正在启动前端服务..."
|
||||
info "启动前端服务..."
|
||||
cd "$SCRIPT_DIR/$FRONTEND_DIR"
|
||||
mkdir -p logs
|
||||
rotate_log "logs/dev.log"
|
||||
@@ -135,26 +165,25 @@ start_frontend() {
|
||||
kill_port $FRONTEND_PORT
|
||||
|
||||
if [ ! -d "node_modules" ]; then
|
||||
warning "node_modules 不存在,正在安装依赖..."
|
||||
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"
|
||||
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)"
|
||||
success "前端已启动 (PID: $(cat "$FRONTEND_PID_FILE"), 端口: $FRONTEND_PORT)"
|
||||
return 0
|
||||
}
|
||||
|
||||
@@ -163,7 +192,7 @@ stop_frontend() {
|
||||
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 "前端服务已停止"
|
||||
success "前端已停止"
|
||||
fi
|
||||
rm -f "$FRONTEND_PID_FILE"
|
||||
fi
|
||||
@@ -171,101 +200,24 @@ stop_frontend() {
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# 命令
|
||||
# 本地开发命令
|
||||
# ============================================================
|
||||
stop() {
|
||||
info "正在停止服务..."
|
||||
stop_backend
|
||||
stop_frontend
|
||||
info "所有服务已停止"
|
||||
}
|
||||
|
||||
show_status() {
|
||||
info "服务状态:"
|
||||
if [ -n "$(check_port $BACKEND_PORT)" ]; then
|
||||
success "后端服务: 运行中 (PID: $(check_port $BACKEND_PORT), 端口: $BACKEND_PORT)"
|
||||
else
|
||||
warning "后端服务: 未运行"
|
||||
fi
|
||||
|
||||
if [ -n "$(check_port $FRONTEND_PORT)" ]; then
|
||||
success "前端服务: 运行中 (PID: $(check_port $FRONTEND_PORT), 端口: $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
|
||||
}
|
||||
|
||||
show_help() {
|
||||
echo "$PROJECT_NAME 启动脚本"
|
||||
echo ""
|
||||
echo "用法: ./run.sh [命令]"
|
||||
echo ""
|
||||
echo "命令:"
|
||||
echo " start 启动前后端服务 (默认)"
|
||||
echo " stop 停止前后端服务"
|
||||
echo " restart 重启前后端服务"
|
||||
echo " backend 仅启动后端服务"
|
||||
echo " frontend 仅启动前端服务"
|
||||
echo " status 查看服务状态"
|
||||
echo " logs 查看实时日志"
|
||||
echo " help 显示帮助信息"
|
||||
echo ""
|
||||
echo "示例:"
|
||||
echo " ./run.sh # 启动所有服务"
|
||||
echo " ./run.sh stop # 停止服务"
|
||||
echo " ./run.sh restart # 重启服务"
|
||||
echo " ./run.sh backend # 仅启动后端"
|
||||
echo " ./run.sh logs # 查看实时日志"
|
||||
}
|
||||
|
||||
show_logs() {
|
||||
info "实时日志 (Ctrl+C 退出):"
|
||||
echo ""
|
||||
# 后端日志
|
||||
if [ -f "$SCRIPT_DIR/$BACKEND_DIR/logs/server.log" ]; then
|
||||
echo "=== 后端日志 ==="
|
||||
tail -f "$SCRIPT_DIR/$BACKEND_DIR/logs/server.log" &
|
||||
local bg_pid=$!
|
||||
fi
|
||||
# 前端日志
|
||||
if [ -f "$SCRIPT_DIR/$FRONTEND_DIR/logs/dev.log" ]; then
|
||||
echo "=== 前端日志 ==="
|
||||
tail -f "$SCRIPT_DIR/$FRONTEND_DIR/logs/dev.log" &
|
||||
local bg_pid2=$!
|
||||
fi
|
||||
trap "kill $bg_pid $bg_pid2 2>/dev/null; exit 0" INT TERM
|
||||
wait
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# 主入口
|
||||
# ============================================================
|
||||
main() {
|
||||
local command=${1:-start}
|
||||
|
||||
case "$command" in
|
||||
start)
|
||||
info "========== $PROJECT_NAME 启动脚本 =========="
|
||||
cmd_dev() {
|
||||
banner "========== $PROJECT_NAME (本地开发) =========="
|
||||
|
||||
# 检查依赖
|
||||
if ! command -v go &> /dev/null; then
|
||||
error "Go 未安装,请先安装 Go"
|
||||
error "Go 未安装,请先安装 Go 1.21+"
|
||||
exit 1
|
||||
fi
|
||||
if ! command -v npm &> /dev/null; then
|
||||
error "npm 未安装,请先安装 Node.js"
|
||||
error "npm 未安装,请先安装 Node.js 20+"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if start_backend && start_frontend; then
|
||||
echo ""
|
||||
success "========== 所有服务已启动 =========="
|
||||
banner "========== 所有服务已启动 =========="
|
||||
info "前端地址: http://localhost:$FRONTEND_PORT"
|
||||
info "后端地址: http://localhost:$BACKEND_PORT"
|
||||
info "健康检查: http://localhost:$BACKEND_PORT/health"
|
||||
@@ -276,37 +228,335 @@ main() {
|
||||
error "服务启动失败"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_stop() {
|
||||
info "停止本地服务..."
|
||||
stop_backend
|
||||
stop_frontend
|
||||
success "所有本地服务已停止"
|
||||
}
|
||||
|
||||
cmd_restart() {
|
||||
cmd_stop
|
||||
sleep 1
|
||||
cmd_dev
|
||||
}
|
||||
|
||||
cmd_status() {
|
||||
info "本地服务状态:"
|
||||
if [ -n "$(check_port $BACKEND_PORT)" ]; then
|
||||
success "后端: 运行中 (端口 $BACKEND_PORT)"
|
||||
else
|
||||
warning "后端: 未运行"
|
||||
fi
|
||||
if [ -n "$(check_port $FRONTEND_PORT)" ]; then
|
||||
success "前端: 运行中 (端口 $FRONTEND_PORT)"
|
||||
else
|
||||
warning "前端: 未运行"
|
||||
fi
|
||||
if [ -f "$SCRIPT_DIR/$BACKEND_DIR/sqlite3.db" ]; then
|
||||
local db_size=$(du -h "$SCRIPT_DIR/$BACKEND_DIR/sqlite3.db" | cut -f1)
|
||||
info "数据库: SQLite (sqlite3.db, $db_size)"
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_logs() {
|
||||
info "实时日志 (Ctrl+C 退出):"
|
||||
echo ""
|
||||
local pids=()
|
||||
if [ -f "$SCRIPT_DIR/$BACKEND_DIR/logs/server.log" ]; then
|
||||
echo "=== 后端日志 ==="
|
||||
tail -f "$SCRIPT_DIR/$BACKEND_DIR/logs/server.log" &
|
||||
pids+=($!)
|
||||
fi
|
||||
if [ -f "$SCRIPT_DIR/$FRONTEND_DIR/logs/dev.log" ]; then
|
||||
echo "=== 前端日志 ==="
|
||||
tail -f "$SCRIPT_DIR/$FRONTEND_DIR/logs/dev.log" &
|
||||
pids+=($!)
|
||||
fi
|
||||
trap "kill ${pids[*]} 2>/dev/null; exit 0" INT TERM
|
||||
wait
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# Docker Compose 部署
|
||||
# ============================================================
|
||||
cmd_docker() {
|
||||
local action=${1:-help}
|
||||
|
||||
if ! command -v docker &> /dev/null; then
|
||||
error "Docker 未安装,请先安装 Docker"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
case "$action" in
|
||||
up)
|
||||
banner "========== Docker Compose 启动 =========="
|
||||
docker compose up -d --build
|
||||
echo ""
|
||||
info "等待服务就绪..."
|
||||
sleep 5
|
||||
docker compose ps
|
||||
echo ""
|
||||
info "前端地址: http://localhost:80"
|
||||
info "后端地址: http://localhost:8080"
|
||||
info "健康检查: http://localhost:8080/health"
|
||||
;;
|
||||
stop)
|
||||
stop
|
||||
down)
|
||||
info "停止 Docker Compose..."
|
||||
docker compose down
|
||||
success "容器已停止"
|
||||
;;
|
||||
build)
|
||||
info "构建 Docker 镜像..."
|
||||
docker compose build
|
||||
success "镜像构建完成"
|
||||
;;
|
||||
restart)
|
||||
stop
|
||||
sleep 2
|
||||
main start
|
||||
;;
|
||||
backend)
|
||||
info "========== $PROJECT_NAME (仅后端) =========="
|
||||
start_backend
|
||||
show_status
|
||||
;;
|
||||
frontend)
|
||||
info "========== $PROJECT_NAME (仅前端) =========="
|
||||
start_frontend
|
||||
show_status
|
||||
;;
|
||||
status)
|
||||
show_status
|
||||
info "重启 Docker Compose..."
|
||||
docker compose restart
|
||||
success "容器已重启"
|
||||
;;
|
||||
logs)
|
||||
show_logs
|
||||
info "Docker 实时日志 (Ctrl+C 退出):"
|
||||
docker compose logs -f
|
||||
;;
|
||||
ps|status)
|
||||
docker compose ps
|
||||
;;
|
||||
help|--help|-h)
|
||||
show_help
|
||||
echo "Docker Compose 部署命令:"
|
||||
echo " ./run.sh docker up 构建并启动所有容器"
|
||||
echo " ./run.sh docker down 停止并移除容器"
|
||||
echo " ./run.sh docker build 仅构建镜像"
|
||||
echo " ./run.sh docker restart 重启容器"
|
||||
echo " ./run.sh docker logs 查看实时日志"
|
||||
echo " ./run.sh docker ps 查看容器状态"
|
||||
;;
|
||||
*)
|
||||
error "未知命令: $command"
|
||||
show_help
|
||||
error "未知 docker 子命令: $action"
|
||||
cmd_docker help
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# 编译独立二进制
|
||||
# ============================================================
|
||||
cmd_build() {
|
||||
local target=${1:-binary}
|
||||
|
||||
case "$target" in
|
||||
binary|aiops)
|
||||
banner "========== 编译独立二进制 =========="
|
||||
cd "$SCRIPT_DIR/$BACKEND_DIR"
|
||||
|
||||
# 确保 config.yaml 存在(二进制模式默认 SQLite)
|
||||
if [ ! -f "config.yaml" ]; then
|
||||
info "生成默认 config.yaml (SQLite + 无Redis)..."
|
||||
cat > config.yaml << 'YAML'
|
||||
mode: "release"
|
||||
port: "8080"
|
||||
log_level: "info"
|
||||
|
||||
database:
|
||||
driver: "sqlite"
|
||||
host: "localhost"
|
||||
port: 3306
|
||||
user: ""
|
||||
password: ""
|
||||
dbname: "data/aiops.db"
|
||||
charset: "utf8mb4"
|
||||
max_conn: 100
|
||||
max_idle: 10
|
||||
|
||||
redis:
|
||||
host: "localhost"
|
||||
port: 6379
|
||||
password: ""
|
||||
db: 0
|
||||
|
||||
jwt:
|
||||
secret: "aiops-jwt-secret-change-in-production"
|
||||
expire_hour: 24
|
||||
|
||||
websocket:
|
||||
read_buffer_size: 1024
|
||||
write_buffer_size: 1024
|
||||
YAML
|
||||
fi
|
||||
|
||||
# 静态编译(CGO_ENABLED=1 因为 SQLite 需要 CGO)
|
||||
info "编译中..."
|
||||
CGO_ENABLED=1 GOPROXY=https://goproxy.cn,direct GONOSUMCHECK='*' GOTOOLCHAIN=go1.22.2 go build -ldflags="-w -s" -o "$STANDALONE_BIN" ./cmd/
|
||||
|
||||
if [ -f "$STANDALONE_BIN" ]; then
|
||||
local bin_size=$(du -h "$STANDALONE_BIN" | cut -f1)
|
||||
success "编译完成: $BACKEND_DIR/$STANDALONE_BIN ($bin_size)"
|
||||
echo ""
|
||||
info "使用方法:"
|
||||
echo " ./$BACKEND_DIR/$STANDALONE_BIN # 默认启动 (SQLite, 端口 8080)"
|
||||
echo " ./$BACKEND_DIR/$STANDALONE_BIN --port 9090 # 自定义端口"
|
||||
echo " ./$BACKEND_DIR/$STANDALONE_BIN --db-driver mysql \\"
|
||||
echo " --db-host localhost --db-user root \\"
|
||||
echo " --db-password pass --db-name aiops # MySQL 模式"
|
||||
echo " ./$BACKEND_DIR/$STANDALONE_BIN --config config.yaml # 使用配置文件"
|
||||
else
|
||||
error "编译失败"
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
agent)
|
||||
banner "========== 编译 Agent =========="
|
||||
cd "$SCRIPT_DIR/agent"
|
||||
|
||||
info "编译 Linux AMD64..."
|
||||
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -ldflags="-w -s" -o aiops-agent-linux-amd64 .
|
||||
success "aiops-agent-linux-amd64 ($(du -h aiops-agent-linux-amd64 | cut -f1))"
|
||||
|
||||
info "编译 Linux ARM64..."
|
||||
GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build -ldflags="-w -s" -o aiops-agent-linux-arm64 .
|
||||
success "aiops-agent-linux-arm64 ($(du -h aiops-agent-linux-arm64 | cut -f1))"
|
||||
|
||||
if [ "$OS" = "macos" ]; then
|
||||
info "编译 macOS (当前平台)..."
|
||||
CGO_ENABLED=0 go build -ldflags="-w -s" -o aiops-agent .
|
||||
success "aiops-agent ($(du -h aiops-agent | cut -f1))"
|
||||
fi
|
||||
;;
|
||||
frontend)
|
||||
info "构建前端生产包..."
|
||||
cd "$SCRIPT_DIR/$FRONTEND_DIR"
|
||||
npm run build
|
||||
success "前端构建完成: $FRONTEND_DIR/dist/"
|
||||
;;
|
||||
help|--help|-h)
|
||||
echo "编译命令:"
|
||||
echo " ./run.sh build 编译独立二进制 aiops (默认 SQLite)"
|
||||
echo " ./run.sh build agent 交叉编译 Agent (amd64+arm64)"
|
||||
echo " ./run.sh build frontend 构建前端生产包"
|
||||
;;
|
||||
*)
|
||||
error "未知构建目标: $target"
|
||||
cmd_build help
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# 独立二进制模式
|
||||
# ============================================================
|
||||
cmd_binary() {
|
||||
cd "$SCRIPT_DIR/$BACKEND_DIR"
|
||||
|
||||
# 先编译(如果不存在)
|
||||
if [ ! -f "$STANDALONE_BIN" ]; then
|
||||
info "二进制不存在,先编译..."
|
||||
cmd_build binary
|
||||
fi
|
||||
|
||||
banner "========== 启动独立二进制 =========="
|
||||
mkdir -p data logs uploads
|
||||
|
||||
# 传递额外参数
|
||||
./"$STANDALONE_BIN" "$@"
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# 帮助
|
||||
# ============================================================
|
||||
cmd_help() {
|
||||
echo ""
|
||||
banner " $PROJECT_NAME 启动脚本"
|
||||
echo ""
|
||||
echo -e " ${GREEN}本地开发${NC}"
|
||||
echo " ./run.sh dev 启动前后端开发服务 (SQLite, 端口 8080+5173)"
|
||||
echo " ./run.sh stop 停止本地服务"
|
||||
echo " ./run.sh restart 重启本地服务"
|
||||
echo " ./run.sh status 查看本地服务状态"
|
||||
echo " ./run.sh logs 查看实时日志"
|
||||
echo ""
|
||||
echo -e " ${CYAN}Docker 容器部署${NC}"
|
||||
echo " ./run.sh docker up 构建并启动所有容器 (MySQL+Redis+Nginx)"
|
||||
echo " ./run.sh docker down 停止并移除容器"
|
||||
echo " ./run.sh docker build 仅构建镜像"
|
||||
echo " ./run.sh docker restart 重启容器"
|
||||
echo " ./run.sh docker logs 查看容器日志"
|
||||
echo " ./run.sh docker ps 查看容器状态"
|
||||
echo ""
|
||||
echo -e " ${YELLOW}编译 & 独立二进制${NC}"
|
||||
echo " ./run.sh build 编译独立二进制 ./$BACKEND_DIR/$STANDALONE_BIN"
|
||||
echo " ./run.sh binary 编译+启动独立二进制"
|
||||
echo " ./run.sh build agent 交叉编译 Agent (amd64+arm64)"
|
||||
echo " ./run.sh build frontend 构建前端生产包"
|
||||
echo ""
|
||||
echo -e " ${BLUE}独立二进制用法${NC}"
|
||||
echo " ./$BACKEND_DIR/$STANDALONE_BIN 默认启动 (SQLite, 端口 8080)"
|
||||
echo " ./$BACKEND_DIR/$STANDALONE_BIN --port 9090 自定义端口"
|
||||
echo " ./$BACKEND_DIR/$STANDALONE_BIN --config config.yaml 使用配置文件"
|
||||
echo " ./$BACKEND_DIR/$STANDALONE_BIN --db-driver mysql \\"
|
||||
echo " --db-host 127.0.0.1 --db-user root \\"
|
||||
echo " --db-password pass --db-name aiops MySQL 模式"
|
||||
echo ""
|
||||
echo -e " ${GREEN}快捷操作${NC}"
|
||||
echo " ./run.sh 等同于 ./run.sh dev"
|
||||
echo " ./run.sh help 显示此帮助"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# 主入口
|
||||
# ============================================================
|
||||
main() {
|
||||
local cmd=${1:-dev}
|
||||
|
||||
case "$cmd" in
|
||||
# 本地开发
|
||||
dev|start)
|
||||
cmd_dev
|
||||
;;
|
||||
stop)
|
||||
cmd_stop
|
||||
;;
|
||||
restart)
|
||||
cmd_restart
|
||||
;;
|
||||
status)
|
||||
cmd_status
|
||||
;;
|
||||
logs)
|
||||
cmd_logs
|
||||
;;
|
||||
|
||||
# Docker Compose
|
||||
docker|compose)
|
||||
shift
|
||||
cmd_docker "$@"
|
||||
;;
|
||||
|
||||
# 编译
|
||||
build)
|
||||
shift
|
||||
cmd_build "$@"
|
||||
;;
|
||||
|
||||
# 独立二进制
|
||||
binary|bin)
|
||||
shift
|
||||
cmd_binary "$@"
|
||||
;;
|
||||
|
||||
# 帮助
|
||||
help|--help|-h)
|
||||
cmd_help
|
||||
;;
|
||||
|
||||
*)
|
||||
error "未知命令: $cmd"
|
||||
cmd_help
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
Reference in new issue
Block a user