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 @@
|
|||||||
# ============================================================
|
1|# ============================================================
|
||||||
# AIOps Backend + Agent 多阶段 Dockerfile
|
2|# AIOps Backend + Agent 多阶段 Dockerfile
|
||||||
# Stage 1: 编译后端 (Go 1.21)
|
3|# Stage 1: 编译后端 (Go 1.21)
|
||||||
# Stage 2: 交叉编译 Agent (Go 1.24, linux-amd64/arm64 + darwin-arm64)
|
4|# Stage 2: 交叉编译 Agent (Go 1.24, linux-amd64/arm64 + darwin-arm64)
|
||||||
# Stage 3: 运行时 (Alpine)
|
5|# Stage 3: 运行时 (Alpine)
|
||||||
# 署名 Logan.Li
|
6|# 署名 Logan.Li
|
||||||
# ============================================================
|
7|# ============================================================
|
||||||
|
8|
|
||||||
# ==================== Stage 1: Backend Builder ====================
|
9|# ==================== Stage 1: Backend Builder ====================
|
||||||
FROM golang:1.21-alpine AS builder
|
10|FROM golang:1.21-alpine AS builder
|
||||||
|
11|
|
||||||
WORKDIR /app
|
12|WORKDIR /app
|
||||||
|
13|
|
||||||
RUN apk add --no-cache git ca-certificates tzdata
|
14|<<<<<<< HEAD
|
||||||
|
15|=======
|
||||||
ENV TZ=Asia/Shanghai
|
16|# 使用国内 Alpine 镜像源加速
|
||||||
ENV GOPROXY=https://goproxy.cn,direct
|
17|RUN sed -i 's|dl-cdn.alpinelinux.org|mirrors.aliyun.com|g' /etc/apk/repositories
|
||||||
ENV GOSUMDB=off
|
18|
|
||||||
|
19|# 安装构建依赖
|
||||||
# 复制后端 go.mod(利用 Docker 层缓存)
|
20|>>>>>>> ca18ead (feat: run.sh四模式(dev/docker/build/binary) + CLI flags + Redka内嵌Redis + Agent复制修复 + 服务器编辑IP字段 + 安装Agent按钮)
|
||||||
COPY backend/go.mod backend/go.sum ./
|
21|RUN apk add --no-cache git ca-certificates tzdata
|
||||||
RUN go mod download
|
22|
|
||||||
|
23|ENV TZ=Asia/Shanghai
|
||||||
# 复制后端源码
|
24|ENV GOPROXY=https://goproxy.cn,direct
|
||||||
COPY backend/ ./
|
25|# 跳过 toolchain 校验(GOTOOLCHAIN 自动下载 Go 1.22)
|
||||||
|
26|ENV GONOSUMCHECK=*
|
||||||
# 构建后端二进制
|
27|# 自动下载 Go 1.22 工具链(Redka 需要)
|
||||||
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \
|
28|ENV GOTOOLCHAIN=go1.22.2
|
||||||
-ldflags="-w -s" \
|
29|
|
||||||
-o ezops ./cmd/
|
30|# 复制后端 go.mod(利用 Docker 层缓存)
|
||||||
|
31|COPY backend/go.mod backend/go.sum ./
|
||||||
# ==================== Stage 2: Agent Builder ====================
|
32|RUN go mod download
|
||||||
FROM golang:1.24-alpine AS agent-builder
|
33|
|
||||||
|
34|# 复制后端源码
|
||||||
WORKDIR /agent
|
35|COPY backend/ ./
|
||||||
|
36|
|
||||||
ENV GOPROXY=https://goproxy.cn,direct
|
37|# 构建后端二进制
|
||||||
ENV GOSUMDB=off
|
38|RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \
|
||||||
|
39| -ldflags="-w -s" \
|
||||||
# 复制 agent go.mod
|
40| -o ezops ./cmd/
|
||||||
COPY agent/go.mod agent/go.sum ./
|
41|
|
||||||
RUN go mod download
|
42|# ==================== Stage 2: Agent Builder ====================
|
||||||
|
43|FROM golang:1.24-alpine AS agent-builder
|
||||||
# 复制 agent 源码
|
44|
|
||||||
COPY agent/ ./
|
45|WORKDIR /agent
|
||||||
|
46|
|
||||||
# 交叉编译三个平台的 agent 二进制
|
47|ENV GOPROXY=https://goproxy.cn,direct
|
||||||
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o bin/aiops-agent-linux-amd64 . && \
|
48|ENV GOSUMDB=off
|
||||||
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags="-s -w" -o bin/aiops-agent-linux-arm64 . && \
|
49|
|
||||||
CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -ldflags="-s -w" -o bin/aiops-agent-darwin-arm64 .
|
50|# 复制 agent go.mod
|
||||||
|
51|COPY agent/go.mod agent/go.sum ./
|
||||||
# ==================== Stage 3: Runtime ====================
|
52|RUN go mod download
|
||||||
FROM alpine:latest
|
53|
|
||||||
|
54|# 复制 agent 源码
|
||||||
# 安装运行时依赖(curl=健康检查,openssh-client=SSH远程安装agent)
|
55|COPY agent/ ./
|
||||||
RUN apk --no-cache add ca-certificates tzdata curl openssh-client
|
56|
|
||||||
|
57|# 交叉编译三个平台的 agent 二进制
|
||||||
ENV TZ=Asia/Shanghai
|
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 . && \
|
||||||
WORKDIR /app
|
60| CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -ldflags="-s -w" -o bin/aiops-agent-darwin-arm64 .
|
||||||
|
61|
|
||||||
# 复制后端二进制
|
62|# ==================== Stage 3: Runtime ====================
|
||||||
COPY --from=builder /app/ezops .
|
63|FROM alpine:latest
|
||||||
|
64|
|
||||||
# 创建运行时目录
|
65|<<<<<<< HEAD
|
||||||
RUN mkdir -p /app/logs /app/uploads /app/agent/bin /app/agent/deploy
|
66|# 安装运行时依赖(curl=健康检查,openssh-client=SSH远程安装agent)
|
||||||
|
67|RUN apk --no-cache add ca-certificates tzdata curl openssh-client
|
||||||
# 复制 agent 二进制(三个平台)
|
68|=======
|
||||||
COPY --from=agent-builder /agent/bin/aiops-agent-linux-amd64 /app/agent/bin/
|
69|# 使用国内 Alpine 镜像源加速
|
||||||
COPY --from=agent-builder /agent/bin/aiops-agent-linux-arm64 /app/agent/bin/
|
70|RUN sed -i 's|dl-cdn.alpinelinux.org|mirrors.aliyun.com|g' /etc/apk/repositories
|
||||||
COPY --from=agent-builder /agent/bin/aiops-agent-darwin-arm64 /app/agent/bin/
|
71|
|
||||||
|
72|# 安装运行时依赖
|
||||||
# 复制 agent 安装脚本和 systemd service 模板
|
73|RUN apk --no-cache add ca-certificates tzdata curl
|
||||||
COPY agent/deploy/install.sh /app/agent/deploy/
|
74|>>>>>>> ca18ead (feat: run.sh四模式(dev/docker/build/binary) + CLI flags + Redka内嵌Redis + Agent复制修复 + 服务器编辑IP字段 + 安装Agent按钮)
|
||||||
COPY agent/deploy/aiops-agent.service /app/agent/deploy/
|
75|
|
||||||
|
76|ENV TZ=Asia/Shanghai
|
||||||
EXPOSE 8080
|
77|
|
||||||
|
78|WORKDIR /app
|
||||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
|
79|
|
||||||
CMD curl -f http://localhost:8080/health || exit 1
|
80|# 复制后端二进制
|
||||||
|
81|COPY --from=builder /app/ezops .
|
||||||
CMD ["./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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
@@ -12,18 +14,50 @@ import (
|
|||||||
"ezops/internal/config"
|
"ezops/internal/config"
|
||||||
"ezops/pkg/database"
|
"ezops/pkg/database"
|
||||||
"ezops/pkg/logger"
|
"ezops/pkg/logger"
|
||||||
|
ezredis "ezops/pkg/redis"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/spf13/viper"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
// 版本信息(通过编译时 ldflags 注入)
|
// 版本信息(通过编译时 ldflags 注入)// Logan.Li
|
||||||
var (
|
var (
|
||||||
Version = "dev"
|
Version = "dev"
|
||||||
BuildTime = "unknown"
|
BuildTime = "unknown"
|
||||||
GitCommit = "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
|
// @title AIOps运维平台 API
|
||||||
// @version 1.0
|
// @version 1.0
|
||||||
// @description AIOps智能运维平台后端API文档
|
// @description AIOps智能运维平台后端API文档
|
||||||
@@ -45,32 +79,67 @@ var (
|
|||||||
// @description Type "Bearer" followed by a space and JWT token.
|
// @description Type "Bearer" followed by a space and JWT token.
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// 加载配置
|
// 解析 CLI 参数 // Logan.Li
|
||||||
cfg := config.Load()
|
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)
|
log := logger.New(cfg.LogLevel)
|
||||||
defer log.Sync()
|
defer log.Sync()
|
||||||
|
|
||||||
|
log.Info("AIOps 启动中",
|
||||||
|
zap.String("version", Version),
|
||||||
|
zap.String("mode", cfg.Mode),
|
||||||
|
zap.String("port", cfg.Port),
|
||||||
|
)
|
||||||
|
|
||||||
// 设置Gin模式
|
// 设置Gin模式
|
||||||
if cfg.Mode == "production" {
|
if cfg.Mode == "production" || cfg.Mode == "release" {
|
||||||
gin.SetMode(gin.ReleaseMode)
|
gin.SetMode(gin.ReleaseMode)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化数据库
|
// 初始化数据库
|
||||||
db, err := database.New(cfg.Database)
|
db, err := database.New(cfg.Database)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal("failed to connect database", zap.Error(err))
|
log.Fatal("数据库连接失败", zap.Error(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 自动迁移
|
// 自动迁移
|
||||||
if err := database.AutoMigrate(db); err != nil {
|
if err := database.AutoMigrate(db); err != nil {
|
||||||
log.Fatal("failed to migrate database", zap.Error(err))
|
log.Fatal("数据库迁移失败", zap.Error(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化种子数据
|
// 初始化种子数据
|
||||||
seedData(db)
|
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实例
|
// 创建Gin实例
|
||||||
r := gin.New()
|
r := gin.New()
|
||||||
|
|
||||||
@@ -85,11 +154,13 @@ func main() {
|
|||||||
"version": Version,
|
"version": Version,
|
||||||
"build_time": BuildTime,
|
"build_time": BuildTime,
|
||||||
"git_commit": GitCommit,
|
"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服务器
|
// 创建HTTP服务器
|
||||||
srv := &http.Server{
|
srv := &http.Server{
|
||||||
@@ -99,28 +170,133 @@ func main() {
|
|||||||
|
|
||||||
// 优雅关闭
|
// 优雅关闭
|
||||||
go func() {
|
go func() {
|
||||||
|
log.Info("服务已启动", zap.String("addr", srv.Addr))
|
||||||
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
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)
|
quit := make(chan os.Signal, 1)
|
||||||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||||
<-quit
|
<-quit
|
||||||
|
|
||||||
log.Info("shutting down server...")
|
log.Info("正在关闭服务...")
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
if err := srv.Shutdown(ctx); err != nil {
|
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 跨域中间件
|
// CORS 跨域中间件
|
||||||
|
|||||||
+8
-2
@@ -1,12 +1,17 @@
|
|||||||
module ezops
|
module ezops
|
||||||
|
|
||||||
go 1.21
|
go 1.22
|
||||||
|
|
||||||
|
toolchain go1.22.2
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/gin-gonic/gin v1.9.1
|
github.com/gin-gonic/gin v1.9.1
|
||||||
github.com/go-ldap/ldap/v3 v3.4.10
|
github.com/go-ldap/ldap/v3 v3.4.10
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.0
|
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/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
|
github.com/spf13/viper v1.18.2
|
||||||
go.uber.org/zap v1.26.0
|
go.uber.org/zap v1.26.0
|
||||||
golang.org/x/crypto v0.31.0
|
golang.org/x/crypto v0.31.0
|
||||||
@@ -19,7 +24,9 @@ require (
|
|||||||
require (
|
require (
|
||||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
|
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
|
||||||
github.com/bytedance/sonic v1.9.1 // 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/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/fsnotify/fsnotify v1.7.0 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
||||||
github.com/gin-contrib/sse v0.1.0 // 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-playground/validator/v10 v10.16.0 // indirect
|
||||||
github.com/go-sql-driver/mysql v1.7.0 // indirect
|
github.com/go-sql-driver/mysql v1.7.0 // indirect
|
||||||
github.com/goccy/go-json v0.10.2 // 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/hashicorp/hcl v1.0.0 // indirect
|
||||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // 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/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 h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI=
|
||||||
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
|
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.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 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
|
||||||
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
|
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-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 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
|
||||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
|
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.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 h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
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 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
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/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 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
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 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
|
||||||
github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
|
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.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 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
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 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||||
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
|
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.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
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.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/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.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
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 != "" {
|
if req.Hostname != "" {
|
||||||
updates["hostname"] = req.Hostname
|
updates["hostname"] = req.Hostname
|
||||||
}
|
}
|
||||||
|
if req.IP != "" {
|
||||||
|
updates["ip"] = req.IP
|
||||||
|
}
|
||||||
if req.PublicIP != "" {
|
if req.PublicIP != "" {
|
||||||
updates["public_ip"] = req.PublicIP
|
updates["public_ip"] = req.PublicIP
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,8 +12,9 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Register 注册所有路由
|
// Register 注册所有路由 // Logan.Li
|
||||||
func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config) {
|
// cache 参数为可选 Redis 缓存实例(支持 redka/redis 双后端)
|
||||||
|
func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config, cache interface{}) {
|
||||||
// API版本前缀
|
// API版本前缀
|
||||||
api := r.Group("/api/v1")
|
api := r.Group("/api/v1")
|
||||||
|
|
||||||
|
|||||||
@@ -155,6 +155,7 @@ type CreateServerRequest struct {
|
|||||||
// UpdateServerRequest 更新服务器请求
|
// UpdateServerRequest 更新服务器请求
|
||||||
type UpdateServerRequest struct {
|
type UpdateServerRequest struct {
|
||||||
Hostname string `json:"hostname"`
|
Hostname string `json:"hostname"`
|
||||||
|
IP string `json:"ip" binding:"omitempty,ip"`
|
||||||
PublicIP string `json:"public_ip" binding:"omitempty,ip"`
|
PublicIP string `json:"public_ip" binding:"omitempty,ip"`
|
||||||
Port int `json:"port" binding:"omitempty,min=1,max=65535"`
|
Port int `json:"port" binding:"omitempty,min=1,max=65535"`
|
||||||
Username string `json:"username"`
|
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';
|
1|import { useState } from 'react';
|
||||||
import { toast } from 'sonner';
|
2|import { toast } from 'sonner';
|
||||||
import { useUserStore } from '@/store';
|
3|import { useUserStore } from '@/store';
|
||||||
import {
|
4|import {
|
||||||
Dialog,
|
5| Dialog,
|
||||||
DialogContent,
|
6| DialogContent,
|
||||||
DialogDescription,
|
7| DialogDescription,
|
||||||
DialogHeader,
|
8| DialogHeader,
|
||||||
DialogTitle,
|
9| DialogTitle,
|
||||||
} from '@/components/ui/dialog';
|
10|} from '@/components/ui/dialog';
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
11|import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||||
import { Button } from '@/components/ui/button';
|
12|import { Button } from '@/components/ui/button';
|
||||||
import { Copy, Check, Terminal, Download, Settings, Play, Cpu } from 'lucide-react';
|
13|import { Copy, Check, Terminal, Download, Settings, Play, Cpu } from 'lucide-react';
|
||||||
|
14|
|
||||||
interface AgentWizardProps {
|
15|interface AgentWizardProps {
|
||||||
open: boolean;
|
16| open: boolean;
|
||||||
onOpenChange: (open: boolean) => void;
|
17| onOpenChange: (open: boolean) => void;
|
||||||
}
|
18|}
|
||||||
|
19|
|
||||||
/** 自动检测服务器地址:使用当前浏览器访问的地址(协议+主机+端口) */
|
20|/** 自动检测服务器地址:使用当前浏览器访问的地址(协议+主机+端口) */
|
||||||
function detectServerAddress(): string {
|
21|function detectServerAddress(): string {
|
||||||
if (typeof window !== 'undefined' && window.location) {
|
22| if (typeof window !== 'undefined' && window.location) {
|
||||||
// 直接用当前浏览器的 origin(如 http://10.10.10.150),
|
23| // 直接用当前浏览器的 origin(如 http://10.10.10.150),
|
||||||
// Nginx 会将 /agent/ 请求代理到后端
|
24| // Nginx 会将 /agent/ 请求代理到后端
|
||||||
return window.location.origin;
|
25| return window.location.origin;
|
||||||
}
|
26| }
|
||||||
return 'http://127.0.0.1:8080';
|
27| return 'http://127.0.0.1:8080';
|
||||||
}
|
28|}
|
||||||
|
29|
|
||||||
export function AgentWizard({ open, onOpenChange }: AgentWizardProps) {
|
30|export function AgentWizard({ open, onOpenChange }: AgentWizardProps) {
|
||||||
const token = useUserStore((s) => s.token) || '<YOUR_TOKEN>';
|
31| const token = useUserStore((s) => s.token) || '<YOUR_TOKEN>';
|
||||||
const serverUrl = detectServerAddress();
|
32| const serverUrl = detectServerAddress();
|
||||||
const [copied, setCopied] = useState(false);
|
33| const [copied, setCopied] = useState(false);
|
||||||
|
34| const [showManualInput, setShowManualInput] = useState(false);
|
||||||
const installCommand = `curl -fsSL ${serverUrl.replace(/^http:/, 'http:')}/agent/install.sh | bash -s -- --server ${serverUrl} --token ${token}`;
|
35|
|
||||||
|
36| const installCommand = `curl -fsSL ${serverUrl.replace(/^http:/, 'http:')}/agent/install.sh | bash -s -- --server ${serverUrl} --token ${token}`;
|
||||||
const handleCopy = async () => {
|
37|
|
||||||
try {
|
38| const fallbackCopy = (text: string): boolean => {
|
||||||
// 优先使用现代 Clipboard API(需要 HTTPS 或 localhost)
|
39| const textArea = document.createElement('textarea');
|
||||||
if (navigator.clipboard && window.isSecureContext) {
|
40| textArea.value = text;
|
||||||
await navigator.clipboard.writeText(installCommand);
|
41| textArea.style.position = 'fixed';
|
||||||
} else {
|
42| textArea.style.left = '-9999px';
|
||||||
// 降级方案:用隐藏 textarea + execCommand('copy'),兼容 HTTP 环境
|
43| textArea.style.top = '-9999px';
|
||||||
const textarea = document.createElement('textarea');
|
44| document.body.appendChild(textArea);
|
||||||
textarea.value = installCommand;
|
45| textArea.focus();
|
||||||
textarea.style.position = 'fixed';
|
46| textArea.select();
|
||||||
textarea.style.opacity = '0';
|
47| let success = false;
|
||||||
textarea.style.pointerEvents = 'none';
|
48| try {
|
||||||
document.body.appendChild(textarea);
|
49|<<<<<<< HEAD
|
||||||
textarea.select();
|
50| // 优先使用现代 Clipboard API(需要 HTTPS 或 localhost)
|
||||||
const ok = document.execCommand('copy');
|
51| if (navigator.clipboard && window.isSecureContext) {
|
||||||
document.body.removeChild(textarea);
|
52| await navigator.clipboard.writeText(installCommand);
|
||||||
if (!ok) throw new Error('execCommand failed');
|
53| } else {
|
||||||
}
|
54| // 降级方案:用隐藏 textarea + execCommand('copy'),兼容 HTTP 环境
|
||||||
setCopied(true);
|
55| const textarea = document.createElement('textarea');
|
||||||
toast.success('安装命令已复制');
|
56| textarea.value = installCommand;
|
||||||
setTimeout(() => setCopied(false), 2000);
|
57| textarea.style.position = 'fixed';
|
||||||
} catch {
|
58| textarea.style.opacity = '0';
|
||||||
toast.error('复制失败,请手动选中复制');
|
59| textarea.style.pointerEvents = 'none';
|
||||||
}
|
60| document.body.appendChild(textarea);
|
||||||
};
|
61| textarea.select();
|
||||||
|
62| const ok = document.execCommand('copy');
|
||||||
const binaryUrl = `${serverUrl}/agent/aiops-agent-linux-amd64`;
|
63| document.body.removeChild(textarea);
|
||||||
|
64| if (!ok) throw new Error('execCommand failed');
|
||||||
const manualSteps = [
|
65| }
|
||||||
{
|
66| setCopied(true);
|
||||||
icon: Download,
|
67| toast.success('安装命令已复制');
|
||||||
title: '1. 下载二进制',
|
68| setTimeout(() => setCopied(false), 2000);
|
||||||
commands: [
|
69| } catch {
|
||||||
`curl -fsSL ${binaryUrl} -o /usr/local/bin/aiops-agent`,
|
70| toast.error('复制失败,请手动选中复制');
|
||||||
],
|
71|=======
|
||||||
},
|
72| success = document.execCommand('copy');
|
||||||
{
|
73| } catch {
|
||||||
icon: Settings,
|
74| // execCommand also failed
|
||||||
title: '2. 赋予执行权限',
|
75| }
|
||||||
commands: ['chmod +x /usr/local/bin/aiops-agent'],
|
76| document.body.removeChild(textArea);
|
||||||
},
|
77| return success;
|
||||||
{
|
78| };
|
||||||
icon: Cpu,
|
79|
|
||||||
title: '3. 创建 systemd 服务',
|
80| const handleCopy = async () => {
|
||||||
commands: [
|
81| const cmd = installCommand;
|
||||||
`cat > /etc/systemd/system/aiops-agent.service << 'EOF'`,
|
82| let success = false;
|
||||||
`[Unit]`,
|
83|
|
||||||
`Description=AIOps Agent`,
|
84| // 方式1: Clipboard API(HTTPS / localhost)
|
||||||
`After=network.target`,
|
85| try {
|
||||||
``,
|
86| await navigator.clipboard.writeText(cmd);
|
||||||
`[Service]`,
|
87| success = true;
|
||||||
`Type=simple`,
|
88| } catch {
|
||||||
`ExecStart=/usr/local/bin/aiops-agent --server ${serverUrl} --token ${token}`,
|
89| // Clipboard API 不可用
|
||||||
`Restart=always`,
|
90| }
|
||||||
`RestartSec=5`,
|
91|
|
||||||
``,
|
92| // 方式2: execCommand fallback(HTTP 也可用)
|
||||||
`[Install]`,
|
93| if (!success) {
|
||||||
`WantedBy=multi-user.target`,
|
94| success = fallbackCopy(cmd);
|
||||||
`EOF`,
|
95| }
|
||||||
],
|
96|
|
||||||
},
|
97| if (success) {
|
||||||
{
|
98| setCopied(true);
|
||||||
icon: Play,
|
99| toast.success('安装命令已复制到剪贴板,粘贴到目标主机执行');
|
||||||
title: '4. 启动并设置开机自启',
|
100| setTimeout(() => setCopied(false), 3000);
|
||||||
commands: [
|
101| } else {
|
||||||
'systemctl daemon-reload',
|
102| toast.error('自动复制失败,请在下方代码框中手动全选复制 (Ctrl+A → Ctrl+C)');
|
||||||
'systemctl enable --now aiops-agent',
|
103| // 切换到手选模式,展示可编辑文本框
|
||||||
'systemctl status aiops-agent',
|
104| setShowManualInput(true);
|
||||||
],
|
105|>>>>>>> ca18ead (feat: run.sh四模式(dev/docker/build/binary) + CLI flags + Redka内嵌Redis + Agent复制修复 + 服务器编辑IP字段 + 安装Agent按钮)
|
||||||
},
|
106| }
|
||||||
];
|
107| };
|
||||||
|
108|
|
||||||
return (
|
109| const binaryUrl = `${serverUrl}/agent/aiops-agent-linux-amd64`;
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
110|
|
||||||
<DialogContent className="max-w-2xl bg-slate-900 border-slate-800 text-slate-100">
|
111| const manualSteps = [
|
||||||
<DialogHeader>
|
112| {
|
||||||
<DialogTitle className="text-slate-100">部署 Agent</DialogTitle>
|
113| icon: Download,
|
||||||
<DialogDescription className="text-slate-400">
|
114| title: '1. 下载二进制',
|
||||||
在目标主机上执行以下命令即可完成 Agent 安装与注册
|
115| commands: [
|
||||||
</DialogDescription>
|
116| `curl -fsSL ${binaryUrl} -o /usr/local/bin/aiops-agent`,
|
||||||
</DialogHeader>
|
117| ],
|
||||||
|
118| },
|
||||||
<Tabs defaultValue="auto" className="w-full">
|
119| {
|
||||||
<TabsList className="bg-slate-800 border border-slate-700">
|
120| icon: Settings,
|
||||||
<TabsTrigger value="auto" className="data-[state=active]:bg-slate-700">
|
121| title: '2. 赋予执行权限',
|
||||||
<Terminal className="h-4 w-4 mr-1.5" />
|
122| commands: ['chmod +x /usr/local/bin/aiops-agent'],
|
||||||
安装Agent
|
123| },
|
||||||
</TabsTrigger>
|
124| {
|
||||||
<TabsTrigger value="manual" className="data-[state=active]:bg-slate-700">
|
125| icon: Cpu,
|
||||||
手动安装
|
126| title: '3. 创建 systemd 服务',
|
||||||
</TabsTrigger>
|
127| commands: [
|
||||||
</TabsList>
|
128| `cat > /etc/systemd/system/aiops-agent.service << 'EOF'`,
|
||||||
|
129| `[Unit]`,
|
||||||
{/* 自动安装 */}
|
130| `Description=AIOps Agent`,
|
||||||
<TabsContent value="auto" className="mt-4 space-y-3">
|
131| `After=network.target`,
|
||||||
<p className="text-sm text-slate-400">
|
132| ``,
|
||||||
在目标主机执行以下命令,Agent 将自动下载、安装并启动:
|
133| `[Service]`,
|
||||||
</p>
|
134| `Type=simple`,
|
||||||
<div className="relative rounded-lg bg-slate-950 border border-slate-800 p-4">
|
135| `ExecStart=/usr/local/bin/aiops-agent --server ${serverUrl} --token ${token}`,
|
||||||
<pre className="text-xs text-emerald-400 font-mono whitespace-pre-wrap break-all pr-10">
|
136| `Restart=always`,
|
||||||
{installCommand}
|
137| `RestartSec=5`,
|
||||||
</pre>
|
138| ``,
|
||||||
<Button
|
139| `[Install]`,
|
||||||
size="icon"
|
140| `WantedBy=multi-user.target`,
|
||||||
variant="ghost"
|
141| `EOF`,
|
||||||
onClick={handleCopy}
|
142| ],
|
||||||
className="absolute top-2 right-2 h-8 w-8 text-slate-400 hover:text-slate-100 hover:bg-slate-800"
|
143| },
|
||||||
title="复制"
|
144| {
|
||||||
>
|
145| icon: Play,
|
||||||
{copied ? <Check className="h-4 w-4 text-emerald-400" /> : <Copy className="h-4 w-4" />}
|
146| title: '4. 启动并设置开机自启',
|
||||||
</Button>
|
147| commands: [
|
||||||
</div>
|
148| 'systemctl daemon-reload',
|
||||||
<div className="flex items-start gap-2 text-xs text-slate-500">
|
149| 'systemctl enable --now aiops-agent',
|
||||||
<Terminal className="h-3.5 w-3.5 mt-0.5 shrink-0" />
|
150| 'systemctl status aiops-agent',
|
||||||
<span>
|
151| ],
|
||||||
服务器地址已自动检测为当前访问地址({serverUrl})。Agent 安装成功后将自动连接并开始上报指标。
|
152| },
|
||||||
</span>
|
153| ];
|
||||||
</div>
|
154|
|
||||||
</TabsContent>
|
155| return (
|
||||||
|
156| <Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
{/* 手动安装 */}
|
157| <DialogContent className="max-w-2xl bg-slate-900 border-slate-800 text-slate-100">
|
||||||
<TabsContent value="manual" className="mt-4 space-y-4">
|
158| <DialogHeader>
|
||||||
<div className="max-h-[420px] overflow-y-auto space-y-3 pr-1">
|
159| <DialogTitle className="text-slate-100">部署 Agent</DialogTitle>
|
||||||
{manualSteps.map((step) => (
|
160| <DialogDescription className="text-slate-400">
|
||||||
<div key={step.title} className="rounded-lg bg-slate-950 border border-slate-800 overflow-hidden">
|
161| 在目标主机上执行以下命令即可完成 Agent 安装与注册
|
||||||
<div className="flex items-center gap-2 px-4 py-2.5 bg-slate-800/50">
|
162| </DialogDescription>
|
||||||
<step.icon className="h-4 w-4 text-blue-400" />
|
163| </DialogHeader>
|
||||||
<span className="text-sm font-medium text-slate-200">{step.title}</span>
|
164|
|
||||||
</div>
|
165| <Tabs defaultValue="auto" className="w-full">
|
||||||
<div className="p-3 space-y-1">
|
166| <TabsList className="bg-slate-800 border border-slate-700">
|
||||||
{step.commands.map((cmd, idx) => (
|
167| <TabsTrigger value="auto" className="data-[state=active]:bg-slate-700">
|
||||||
<div key={idx} className="flex items-start gap-2">
|
168| <Terminal className="h-4 w-4 mr-1.5" />
|
||||||
<span className="text-slate-600 font-mono text-xs select-none mt-0.5">$</span>
|
169| 安装Agent
|
||||||
<code className="text-xs text-slate-300 font-mono whitespace-pre-wrap break-all flex-1">
|
170| </TabsTrigger>
|
||||||
{cmd}
|
171| <TabsTrigger value="manual" className="data-[state=active]:bg-slate-700">
|
||||||
</code>
|
172| 手动安装
|
||||||
</div>
|
173| </TabsTrigger>
|
||||||
))}
|
174| </TabsList>
|
||||||
</div>
|
175|
|
||||||
</div>
|
176| {/* 自动安装 */}
|
||||||
))}
|
177| <TabsContent value="auto" className="mt-4 space-y-3">
|
||||||
</div>
|
178| <p className="text-sm text-slate-400">
|
||||||
<p className="text-xs text-slate-500">
|
179| 在目标主机执行以下命令,Agent 将自动下载、安装并启动:
|
||||||
完成上述步骤后,Agent 将作为 systemd 服务运行并自动上报指标。
|
180| </p>
|
||||||
</p>
|
181| <div className="relative rounded-lg bg-slate-950 border border-slate-800 p-4">
|
||||||
</TabsContent>
|
182| <pre className="text-xs text-emerald-400 font-mono whitespace-pre-wrap break-all pr-10">
|
||||||
</Tabs>
|
183| {installCommand}
|
||||||
</DialogContent>
|
184| </pre>
|
||||||
</Dialog>
|
185| <Button
|
||||||
);
|
186| size="icon"
|
||||||
}
|
187| variant="ghost"
|
||||||
|
188| onClick={handleCopy}
|
||||||
export default AgentWizard;
|
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 { ServerForm, EMPTY_FORM, type ServerFormData } from './ServerForm';
|
||||||
import { getTags } from './serverHelpers';
|
import { getTags } from './serverHelpers';
|
||||||
import { ServerTableRow } from './ServerTableRow';
|
import { ServerTableRow } from './ServerTableRow';
|
||||||
|
import { AgentWizard } from '@/components/agent/AgentWizard';
|
||||||
|
|
||||||
export function ServerManager() {
|
export function ServerManager() {
|
||||||
const confirm = useConfirm();
|
const confirm = useConfirm();
|
||||||
@@ -70,6 +71,12 @@ export function ServerManager() {
|
|||||||
const [terminalOpen, setTerminalOpen] = useState(false);
|
const [terminalOpen, setTerminalOpen] = useState(false);
|
||||||
const [terminalServer, setTerminalServer] = useState<{ id: number; name: string; ip: string } | null>(null);
|
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) => {
|
const openTerminal = (server: any) => {
|
||||||
setTerminalServer({ id: server.id, name: server.hostname, ip: server.ip });
|
setTerminalServer({ id: server.id, name: server.hostname, ip: server.ip });
|
||||||
setTerminalOpen(true);
|
setTerminalOpen(true);
|
||||||
@@ -217,10 +224,12 @@ export function ServerManager() {
|
|||||||
} else {
|
} else {
|
||||||
await serverApi.createServer(payload);
|
await serverApi.createServer(payload);
|
||||||
}
|
}
|
||||||
|
toast.success(editingId ? '服务器已更新' : '服务器已创建');
|
||||||
setFormOpen(false);
|
setFormOpen(false);
|
||||||
loadData();
|
loadData();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('保存服务器失败:', e);
|
console.error('保存服务器失败:', e);
|
||||||
|
toast.error('保存失败,请重试');
|
||||||
} finally {
|
} finally {
|
||||||
setFormSaving(false);
|
setFormSaving(false);
|
||||||
}
|
}
|
||||||
@@ -411,6 +420,7 @@ export function ServerManager() {
|
|||||||
onOpenTerminal={openTerminal}
|
onOpenTerminal={openTerminal}
|
||||||
onOpenEdit={openEditForm}
|
onOpenEdit={openEditForm}
|
||||||
onDelete={handleDelete}
|
onDelete={handleDelete}
|
||||||
|
onInstallAgent={openInstallAgent}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
@@ -449,6 +459,9 @@ export function ServerManager() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Agent 安装向导 */}
|
||||||
|
<AgentWizard open={agentWizardOpen} onOpenChange={setAgentWizardOpen} />
|
||||||
|
|
||||||
{/* 终端 Dialog */}
|
{/* 终端 Dialog */}
|
||||||
<TerminalDialog
|
<TerminalDialog
|
||||||
open={terminalOpen}
|
open={terminalOpen}
|
||||||
|
|||||||
@@ -1,166 +1,180 @@
|
|||||||
// 服务器表格行 — 纯展示子组件(接收 props,不直接调 API) // Logan.Li
|
1|// 服务器表格行 — 纯展示子组件(接收 props,不直接调 API) // Logan.Li
|
||||||
import { cn } from '@/lib/utils';
|
2|import { cn } from '@/lib/utils';
|
||||||
import { Badge } from '@/components/ui/badge';
|
3|import { Badge } from '@/components/ui/badge';
|
||||||
import { Button } from '@/components/ui/button';
|
4|import { Button } from '@/components/ui/button';
|
||||||
import { Checkbox } from '@/components/ui/checkbox';
|
5|import { Checkbox } from '@/components/ui/checkbox';
|
||||||
import {
|
6|import {
|
||||||
DropdownMenu,
|
7| DropdownMenu,
|
||||||
DropdownMenuContent,
|
8| DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
9| DropdownMenuItem,
|
||||||
DropdownMenuLabel,
|
10| DropdownMenuLabel,
|
||||||
DropdownMenuSeparator,
|
11| DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger,
|
12| DropdownMenuTrigger,
|
||||||
} from '@/components/ui/dropdown-menu';
|
13|} from '@/components/ui/dropdown-menu';
|
||||||
import { TableCell, TableRow } from '@/components/ui/table';
|
14|import { TableCell, TableRow } from '@/components/ui/table';
|
||||||
import { Edit, MoreHorizontal, Server as ServerIcon, Terminal, Trash2, Download } from 'lucide-react';
|
15|<<<<<<< HEAD
|
||||||
import { getStatusBadge, getTags, getUsageColor } from './serverHelpers';
|
16|import { Edit, MoreHorizontal, Server as ServerIcon, Terminal, Trash2, Download } from 'lucide-react';
|
||||||
|
17|=======
|
||||||
interface ServerTableRowProps {
|
18|import { Cpu, Edit, MoreHorizontal, Server as ServerIcon, Terminal, Trash2 } from 'lucide-react';
|
||||||
server: any;
|
19|>>>>>>> ca18ead (feat: run.sh四模式(dev/docker/build/binary) + CLI flags + Redka内嵌Redis + Agent复制修复 + 服务器编辑IP字段 + 安装Agent按钮)
|
||||||
selected: boolean;
|
20|import { getStatusBadge, getTags, getUsageColor } from './serverHelpers';
|
||||||
onToggleSelect: (id: string) => void;
|
21|
|
||||||
onOpenTerminal: (server: any) => void;
|
22|interface ServerTableRowProps {
|
||||||
onOpenEdit: (server: any) => void;
|
23| server: any;
|
||||||
onDelete: (id: string) => void;
|
24| selected: boolean;
|
||||||
}
|
25| onToggleSelect: (id: string) => void;
|
||||||
|
26| onOpenTerminal: (server: any) => void;
|
||||||
export function ServerTableRow({
|
27| onOpenEdit: (server: any) => void;
|
||||||
server,
|
28| onDelete: (id: string) => void;
|
||||||
selected,
|
29| onInstallAgent: (server: any) => void;
|
||||||
onToggleSelect,
|
30|}
|
||||||
onOpenTerminal,
|
31|
|
||||||
onOpenEdit,
|
32|export function ServerTableRow({
|
||||||
onDelete,
|
33| server,
|
||||||
}: ServerTableRowProps) {
|
34| selected,
|
||||||
const cpuUsage = Number(server.cpu_usage || 0);
|
35| onToggleSelect,
|
||||||
const memoryUsage = Number(server.memory_usage || 0);
|
36| onOpenTerminal,
|
||||||
const tags = getTags(server);
|
37| onOpenEdit,
|
||||||
const memGB = server.memory ? Math.round(server.memory / 1024) : 0;
|
38| onDelete,
|
||||||
|
39| onInstallAgent,
|
||||||
return (
|
40|}: ServerTableRowProps) {
|
||||||
<TableRow className="border-[var(--dg-border)] hover:bg-[var(--dg-bg-tertiary)]/50">
|
41| const cpuUsage = Number(server.cpu_usage || 0);
|
||||||
<TableCell>
|
42| const memoryUsage = Number(server.memory_usage || 0);
|
||||||
<Checkbox
|
43| const tags = getTags(server);
|
||||||
checked={selected}
|
44| const memGB = server.memory ? Math.round(server.memory / 1024) : 0;
|
||||||
onCheckedChange={() => onToggleSelect(server.id)}
|
45|
|
||||||
className="border-[var(--dg-border)]"
|
46| return (
|
||||||
/>
|
47| <TableRow className="border-[var(--dg-border)] hover:bg-[var(--dg-bg-tertiary)]/50">
|
||||||
</TableCell>
|
48| <TableCell>
|
||||||
<TableCell>
|
49| <Checkbox
|
||||||
<div className="flex items-center gap-2">
|
50| checked={selected}
|
||||||
<ServerIcon className="h-4 w-4 text-[var(--dg-text-muted)]" />
|
51| onCheckedChange={() => onToggleSelect(server.id)}
|
||||||
<div>
|
52| className="border-[var(--dg-border)]"
|
||||||
<p className="font-medium text-[var(--dg-text-primary)]">{server.hostname}</p>
|
53| />
|
||||||
<p className="text-xs text-[var(--dg-text-muted)]">{server.os}</p>
|
54| </TableCell>
|
||||||
</div>
|
55| <TableCell>
|
||||||
</div>
|
56| <div className="flex items-center gap-2">
|
||||||
</TableCell>
|
57| <ServerIcon className="h-4 w-4 text-[var(--dg-text-muted)]" />
|
||||||
<TableCell>
|
58| <div>
|
||||||
<code className="text-sm text-[var(--dg-text-secondary)] bg-[var(--dg-bg-tertiary)] px-2 py-0.5 rounded">
|
59| <p className="font-medium text-[var(--dg-text-primary)]">{server.hostname}</p>
|
||||||
{server.ip}
|
60| <p className="text-xs text-[var(--dg-text-muted)]">{server.os}</p>
|
||||||
</code>
|
61| </div>
|
||||||
</TableCell>
|
62| </div>
|
||||||
<TableCell>
|
63| </TableCell>
|
||||||
{server.public_ip ? (
|
64| <TableCell>
|
||||||
<code className="text-sm text-[var(--dg-cyan)] bg-[var(--dg-cyan)]/10 px-2 py-0.5 rounded">
|
65| <code className="text-sm text-[var(--dg-text-secondary)] bg-[var(--dg-bg-tertiary)] px-2 py-0.5 rounded">
|
||||||
{server.public_ip}
|
66| {server.ip}
|
||||||
</code>
|
67| </code>
|
||||||
) : (
|
68| </TableCell>
|
||||||
<span className="text-sm text-[var(--dg-text-muted)]">-</span>
|
69| <TableCell>
|
||||||
)}
|
70| {server.public_ip ? (
|
||||||
</TableCell>
|
71| <code className="text-sm text-[var(--dg-cyan)] bg-[var(--dg-cyan)]/10 px-2 py-0.5 rounded">
|
||||||
<TableCell>{getStatusBadge(server.status)}</TableCell>
|
72| {server.public_ip}
|
||||||
<TableCell>
|
73| </code>
|
||||||
<div className="text-sm text-[var(--dg-text-secondary)]">
|
74| ) : (
|
||||||
<span className="text-[var(--dg-text-muted)]">{server.cpu}核</span>
|
75| <span className="text-sm text-[var(--dg-text-muted)]">-</span>
|
||||||
<span className="mx-1 text-[var(--dg-text-muted)] opacity-50">/</span>
|
76| )}
|
||||||
<span className="text-[var(--dg-text-muted)]">{memGB}GB</span>
|
77| </TableCell>
|
||||||
<span className="mx-1 text-[var(--dg-text-muted)] opacity-50">/</span>
|
78| <TableCell>{getStatusBadge(server.status)}</TableCell>
|
||||||
<span className="text-[var(--dg-text-muted)]">{server.disk}GB</span>
|
79| <TableCell>
|
||||||
</div>
|
80| <div className="text-sm text-[var(--dg-text-secondary)]">
|
||||||
</TableCell>
|
81| <span className="text-[var(--dg-text-muted)]">{server.cpu}核</span>
|
||||||
<TableCell>
|
82| <span className="mx-1 text-[var(--dg-text-muted)] opacity-50">/</span>
|
||||||
<div className="space-y-1">
|
83| <span className="text-[var(--dg-text-muted)]">{memGB}GB</span>
|
||||||
<div className="flex items-center gap-2">
|
84| <span className="mx-1 text-[var(--dg-text-muted)] opacity-50">/</span>
|
||||||
<span className="text-xs text-[var(--dg-text-muted)] w-8">CPU</span>
|
85| <span className="text-[var(--dg-text-muted)]">{server.disk}GB</span>
|
||||||
<div className="w-16 h-1.5 bg-[var(--dg-bg-tertiary)] rounded-full overflow-hidden">
|
86| </div>
|
||||||
<div
|
87| </TableCell>
|
||||||
className={cn('h-full rounded-full', getUsageColor(cpuUsage))}
|
88| <TableCell>
|
||||||
style={{ width: `${cpuUsage}%` }}
|
89| <div className="space-y-1">
|
||||||
/>
|
90| <div className="flex items-center gap-2">
|
||||||
</div>
|
91| <span className="text-xs text-[var(--dg-text-muted)] w-8">CPU</span>
|
||||||
<span className="text-xs text-[var(--dg-text-secondary)]">{cpuUsage.toFixed(1)}%</span>
|
92| <div className="w-16 h-1.5 bg-[var(--dg-bg-tertiary)] rounded-full overflow-hidden">
|
||||||
</div>
|
93| <div
|
||||||
<div className="flex items-center gap-2">
|
94| className={cn('h-full rounded-full', getUsageColor(cpuUsage))}
|
||||||
<span className="text-xs text-[var(--dg-text-muted)] w-8">内存</span>
|
95| style={{ width: `${cpuUsage}%` }}
|
||||||
<div className="w-16 h-1.5 bg-[var(--dg-bg-tertiary)] rounded-full overflow-hidden">
|
96| />
|
||||||
<div
|
97| </div>
|
||||||
className={cn('h-full rounded-full', getUsageColor(memoryUsage))}
|
98| <span className="text-xs text-[var(--dg-text-secondary)]">{cpuUsage.toFixed(1)}%</span>
|
||||||
style={{ width: `${memoryUsage}%` }}
|
99| </div>
|
||||||
/>
|
100| <div className="flex items-center gap-2">
|
||||||
</div>
|
101| <span className="text-xs text-[var(--dg-text-muted)] w-8">内存</span>
|
||||||
<span className="text-xs text-[var(--dg-text-secondary)]">{memoryUsage.toFixed(1)}%</span>
|
102| <div className="w-16 h-1.5 bg-[var(--dg-bg-tertiary)] rounded-full overflow-hidden">
|
||||||
</div>
|
103| <div
|
||||||
</div>
|
104| className={cn('h-full rounded-full', getUsageColor(memoryUsage))}
|
||||||
</TableCell>
|
105| style={{ width: `${memoryUsage}%` }}
|
||||||
<TableCell>
|
106| />
|
||||||
<div className="flex flex-wrap gap-1">
|
107| </div>
|
||||||
{tags.slice(0, 3).map((tag) => (
|
108| <span className="text-xs text-[var(--dg-text-secondary)]">{memoryUsage.toFixed(1)}%</span>
|
||||||
<Badge
|
109| </div>
|
||||||
key={tag}
|
110| </div>
|
||||||
variant="outline"
|
111| </TableCell>
|
||||||
className="text-xs border-[var(--dg-border)] text-[var(--dg-text-secondary)] bg-[var(--dg-bg-tertiary)]/50"
|
112| <TableCell>
|
||||||
>
|
113| <div className="flex flex-wrap gap-1">
|
||||||
{tag}
|
114| {tags.slice(0, 3).map((tag) => (
|
||||||
</Badge>
|
115| <Badge
|
||||||
))}
|
116| key={tag}
|
||||||
{tags.length > 3 && (
|
117| variant="outline"
|
||||||
<Badge variant="outline" className="text-xs border-[var(--dg-border)] text-[var(--dg-text-secondary)]">
|
118| className="text-xs border-[var(--dg-border)] text-[var(--dg-text-secondary)] bg-[var(--dg-bg-tertiary)]/50"
|
||||||
+{tags.length - 3}
|
119| >
|
||||||
</Badge>
|
120| {tag}
|
||||||
)}
|
121| </Badge>
|
||||||
</div>
|
122| ))}
|
||||||
</TableCell>
|
123| {tags.length > 3 && (
|
||||||
<TableCell className="text-right">
|
124| <Badge variant="outline" className="text-xs border-[var(--dg-border)] text-[var(--dg-text-secondary)]">
|
||||||
<div className="flex items-center justify-end gap-1">
|
125| +{tags.length - 3}
|
||||||
<Button variant="ghost" size="icon" className="h-8 w-8 text-[var(--dg-text-secondary)] hover:text-[var(--dg-blue)]" onClick={() => onOpenTerminal(server)}>
|
126| </Badge>
|
||||||
<Terminal className="h-4 w-4" />
|
127| )}
|
||||||
</Button>
|
128| </div>
|
||||||
<DropdownMenu>
|
129| </TableCell>
|
||||||
<DropdownMenuTrigger asChild>
|
130| <TableCell className="text-right">
|
||||||
<Button variant="ghost" size="icon" className="h-8 w-8 text-[var(--dg-text-secondary)] hover:text-[var(--dg-text-primary)]">
|
131| <div className="flex items-center justify-end gap-1">
|
||||||
<MoreHorizontal className="h-4 w-4" />
|
132| <Button variant="ghost" size="icon" className="h-8 w-8 text-[var(--dg-text-secondary)] hover:text-[var(--dg-blue)]" onClick={() => onOpenTerminal(server)}>
|
||||||
</Button>
|
133| <Terminal className="h-4 w-4" />
|
||||||
</DropdownMenuTrigger>
|
134| </Button>
|
||||||
<DropdownMenuContent align="end" className="w-48 bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)]">
|
135| <DropdownMenu>
|
||||||
<DropdownMenuLabel className="text-[var(--dg-text-secondary)]">操作</DropdownMenuLabel>
|
136| <DropdownMenuTrigger asChild>
|
||||||
<DropdownMenuSeparator className="bg-[var(--dg-border)]" />
|
137| <Button variant="ghost" size="icon" className="h-8 w-8 text-[var(--dg-text-secondary)] hover:text-[var(--dg-text-primary)]">
|
||||||
<DropdownMenuItem
|
138| <MoreHorizontal className="h-4 w-4" />
|
||||||
className="text-[var(--dg-text-secondary)] focus:bg-[var(--dg-bg-hover)] focus:text-[var(--dg-text-primary)] cursor-pointer"
|
139| </Button>
|
||||||
onClick={() => onOpenEdit(server)}
|
140| </DropdownMenuTrigger>
|
||||||
>
|
141| <DropdownMenuContent align="end" className="w-48 bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)]">
|
||||||
<Edit className="h-4 w-4 mr-2" />
|
142| <DropdownMenuLabel className="text-[var(--dg-text-secondary)]">操作</DropdownMenuLabel>
|
||||||
编辑
|
143| <DropdownMenuSeparator className="bg-[var(--dg-border)]" />
|
||||||
</DropdownMenuItem>
|
144| <DropdownMenuItem
|
||||||
<DropdownMenuItem
|
145| className="text-[var(--dg-text-secondary)] focus:bg-[var(--dg-bg-hover)] focus:text-[var(--dg-text-primary)] cursor-pointer"
|
||||||
className="text-[var(--dg-text-secondary)] focus:bg-[var(--dg-bg-hover)] focus:text-[var(--dg-text-primary)] cursor-pointer"
|
146| onClick={() => onOpenEdit(server)}
|
||||||
onClick={() => onOpenTerminal(server)}
|
147| >
|
||||||
>
|
148| <Edit className="h-4 w-4 mr-2" />
|
||||||
<Terminal className="h-4 w-4 mr-2" />
|
149| 编辑
|
||||||
终端
|
150| </DropdownMenuItem>
|
||||||
</DropdownMenuItem>
|
151| <DropdownMenuItem
|
||||||
<DropdownMenuSeparator className="bg-[var(--dg-border)]" />
|
152| className="text-[var(--dg-text-secondary)] focus:bg-[var(--dg-bg-hover)] focus:text-[var(--dg-text-primary)] cursor-pointer"
|
||||||
<DropdownMenuItem
|
153| onClick={() => onOpenTerminal(server)}
|
||||||
className="text-red-400 focus:bg-[var(--dg-bg-hover)] focus:text-red-400 cursor-pointer"
|
154| >
|
||||||
onClick={() => onDelete(server.id)}
|
155| <Terminal className="h-4 w-4 mr-2" />
|
||||||
>
|
156| 终端
|
||||||
<Trash2 className="h-4 w-4 mr-2" />
|
157| </DropdownMenuItem>
|
||||||
删除
|
158| <DropdownMenuItem
|
||||||
</DropdownMenuItem>
|
159| className="text-[var(--dg-text-secondary)] focus:bg-[var(--dg-bg-hover)] focus:text-[var(--dg-text-primary)] cursor-pointer"
|
||||||
</DropdownMenuContent>
|
160| onClick={() => onInstallAgent(server)}
|
||||||
</DropdownMenu>
|
161| >
|
||||||
</div>
|
162| <Cpu className="h-4 w-4 mr-2" />
|
||||||
</TableCell>
|
163| 安装Agent
|
||||||
</TableRow>
|
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
|
#!/bin/bash
|
||||||
# EZOps AIOps 智能运维平台 - 启动脚本
|
#===========================================================================
|
||||||
# 一键启动前后端服务,支持日志轮转
|
# AIOps 智能运维平台 - 统一启动脚本
|
||||||
# 用法: ./run.sh [start|stop|restart|status|frontend|backend|help]
|
# 支持三种运行模式:本地开发 / Docker容器部署 / 独立二进制
|
||||||
|
#
|
||||||
|
# 用法:
|
||||||
|
# ./run.sh dev 本地开发模式(前后端热重载)
|
||||||
|
# ./run.sh docker up Docker Compose 容器部署
|
||||||
|
# ./run.sh build 编译独立二进制 ./aiops
|
||||||
|
# ./run.sh binary 编译并启动独立二进制
|
||||||
|
# ./run.sh help 查看帮助
|
||||||
|
#===========================================================================
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# 配置区
|
# 配置区
|
||||||
# ============================================================
|
# ============================================================
|
||||||
PROJECT_NAME="EZOps AIOps"
|
PROJECT_NAME="AIOps 智能运维平台"
|
||||||
BACKEND_PORT=8080
|
BACKEND_PORT=8080
|
||||||
FRONTEND_PORT=5173
|
FRONTEND_PORT=5173
|
||||||
BACKEND_DIR="backend"
|
BACKEND_DIR="backend"
|
||||||
FRONTEND_DIR="frontend"
|
FRONTEND_DIR="frontend"
|
||||||
BACKEND_BIN="aiops-server"
|
BACKEND_BIN="aiops-server"
|
||||||
|
STANDALONE_BIN="aiops"
|
||||||
|
|
||||||
# 日志配置
|
# 日志配置
|
||||||
MAX_LOG_FILES=3
|
MAX_LOG_FILES=3
|
||||||
@@ -26,12 +34,14 @@ RED='\033[0;31m'
|
|||||||
GREEN='\033[0;32m'
|
GREEN='\033[0;32m'
|
||||||
YELLOW='\033[1;33m'
|
YELLOW='\033[1;33m'
|
||||||
BLUE='\033[0;34m'
|
BLUE='\033[0;34m'
|
||||||
|
CYAN='\033[0;36m'
|
||||||
NC='\033[0m'
|
NC='\033[0m'
|
||||||
|
|
||||||
info() { echo -e "${BLUE}[INFO]${NC} $1"; }
|
info() { echo -e "${BLUE}[INFO]${NC} $1"; }
|
||||||
success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; }
|
success() { echo -e "${GREEN}[OK]${NC} $1"; }
|
||||||
warning() { echo -e "${YELLOW}[WARNING]${NC} $1"; }
|
warning() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
||||||
error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
error() { echo -e "${RED}[ERR]${NC} $1"; }
|
||||||
|
banner() { echo -e "${CYAN}$1${NC}"; }
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
cd "$SCRIPT_DIR"
|
cd "$SCRIPT_DIR"
|
||||||
@@ -40,27 +50,49 @@ cd "$SCRIPT_DIR"
|
|||||||
BACKEND_PID_FILE="$SCRIPT_DIR/$BACKEND_DIR/.backend.pid"
|
BACKEND_PID_FILE="$SCRIPT_DIR/$BACKEND_DIR/.backend.pid"
|
||||||
FRONTEND_PID_FILE="$SCRIPT_DIR/$FRONTEND_DIR/.frontend.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() {
|
check_port() {
|
||||||
|
if [ "$OS" = "macos" ]; then
|
||||||
lsof -ti:$1 2>/dev/null | head -1
|
lsof -ti:$1 2>/dev/null | head -1
|
||||||
|
else
|
||||||
|
ss -tlnp 2>/dev/null | grep -q ":$1 " && echo "in-use" || echo ""
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# 释放端口
|
# 释放端口
|
||||||
kill_port() {
|
kill_port() {
|
||||||
local pids=$(lsof -ti:$1 2>/dev/null || echo "")
|
local port=$1
|
||||||
if [ -n "$pids" ]; then
|
if [ "$OS" = "macos" ]; then
|
||||||
info "释放端口 $1..."
|
local pids=$(lsof -ti:$port 2>/dev/null || echo "")
|
||||||
echo "$pids" | xargs kill -9 2>/dev/null || true
|
[ -n "$pids" ] && echo "$pids" | xargs kill -9 2>/dev/null || true
|
||||||
sleep 1
|
else
|
||||||
|
fuser -k ${port}/tcp 2>/dev/null || true
|
||||||
fi
|
fi
|
||||||
|
sleep 0.5
|
||||||
}
|
}
|
||||||
|
|
||||||
# 日志轮转(仅启动时调用)
|
# 日志轮转
|
||||||
rotate_log() {
|
rotate_log() {
|
||||||
local log_file=$1
|
local log_file=$1
|
||||||
[ ! -f "$log_file" ] && return 0
|
[ ! -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
|
if [ "$file_size" -gt "$MAX_LOG_SIZE" ]; then
|
||||||
[ -f "${log_file}.${MAX_LOG_FILES}" ] && rm -f "${log_file}.${MAX_LOG_FILES}"
|
[ -f "${log_file}.${MAX_LOG_FILES}" ] && rm -f "${log_file}.${MAX_LOG_FILES}"
|
||||||
for i in $(seq $((MAX_LOG_FILES - 1)) -1 1); do
|
for i in $(seq $((MAX_LOG_FILES - 1)) -1 1); do
|
||||||
@@ -72,20 +104,18 @@ rotate_log() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# 后端服务
|
# 后端服务(本地开发模式)
|
||||||
# ============================================================
|
# ============================================================
|
||||||
start_backend() {
|
start_backend() {
|
||||||
info "正在启动后端服务..."
|
info "启动后端服务..."
|
||||||
cd "$SCRIPT_DIR/$BACKEND_DIR"
|
cd "$SCRIPT_DIR/$BACKEND_DIR"
|
||||||
mkdir -p logs
|
mkdir -p logs
|
||||||
rotate_log "logs/server.log"
|
rotate_log "logs/server.log"
|
||||||
|
|
||||||
# 释放被占用的端口
|
|
||||||
kill_port $BACKEND_PORT
|
kill_port $BACKEND_PORT
|
||||||
|
|
||||||
export GIN_MODE=debug
|
export GIN_MODE=debug
|
||||||
|
|
||||||
# 编译并运行(SQLite 需要 CGO)
|
|
||||||
info "编译 Go 程序..."
|
info "编译 Go 程序..."
|
||||||
CGO_ENABLED=1 go build -o "$BACKEND_BIN" ./cmd/ 2>&1 || {
|
CGO_ENABLED=1 go build -o "$BACKEND_BIN" ./cmd/ 2>&1 || {
|
||||||
error "编译失败"
|
error "编译失败"
|
||||||
@@ -100,14 +130,14 @@ start_backend() {
|
|||||||
while ! curl -s "http://localhost:$BACKEND_PORT/health" > /dev/null 2>&1; do
|
while ! curl -s "http://localhost:$BACKEND_PORT/health" > /dev/null 2>&1; do
|
||||||
retries=$((retries + 1))
|
retries=$((retries + 1))
|
||||||
if [ $retries -ge 30 ]; then
|
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
|
tail -20 logs/server.log 2>/dev/null
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
sleep 1
|
sleep 1
|
||||||
done
|
done
|
||||||
|
|
||||||
success "后端服务已启动 (PID: $(cat "$BACKEND_PID_FILE"), 端口: $BACKEND_PORT)"
|
success "后端已启动 (PID: $(cat "$BACKEND_PID_FILE"), 端口: $BACKEND_PORT)"
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,7 +146,7 @@ stop_backend() {
|
|||||||
local pid=$(cat "$BACKEND_PID_FILE" 2>/dev/null || echo "")
|
local pid=$(cat "$BACKEND_PID_FILE" 2>/dev/null || echo "")
|
||||||
if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
|
if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
|
||||||
kill "$pid" 2>/dev/null || true
|
kill "$pid" 2>/dev/null || true
|
||||||
success "后端服务已停止"
|
success "后端已停止"
|
||||||
fi
|
fi
|
||||||
rm -f "$BACKEND_PID_FILE"
|
rm -f "$BACKEND_PID_FILE"
|
||||||
fi
|
fi
|
||||||
@@ -124,10 +154,10 @@ stop_backend() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# 前端服务
|
# 前端服务(本地开发模式)
|
||||||
# ============================================================
|
# ============================================================
|
||||||
start_frontend() {
|
start_frontend() {
|
||||||
info "正在启动前端服务..."
|
info "启动前端服务..."
|
||||||
cd "$SCRIPT_DIR/$FRONTEND_DIR"
|
cd "$SCRIPT_DIR/$FRONTEND_DIR"
|
||||||
mkdir -p logs
|
mkdir -p logs
|
||||||
rotate_log "logs/dev.log"
|
rotate_log "logs/dev.log"
|
||||||
@@ -135,26 +165,25 @@ start_frontend() {
|
|||||||
kill_port $FRONTEND_PORT
|
kill_port $FRONTEND_PORT
|
||||||
|
|
||||||
if [ ! -d "node_modules" ]; then
|
if [ ! -d "node_modules" ]; then
|
||||||
warning "node_modules 不存在,正在安装依赖..."
|
warning "安装前端依赖..."
|
||||||
npm install
|
npm install
|
||||||
fi
|
fi
|
||||||
|
|
||||||
nohup npm run dev >> logs/dev.log 2>&1 &
|
nohup npm run dev >> logs/dev.log 2>&1 &
|
||||||
echo $! > "$FRONTEND_PID_FILE"
|
echo $! > "$FRONTEND_PID_FILE"
|
||||||
|
|
||||||
# 等待服务启动
|
|
||||||
local retries=0
|
local retries=0
|
||||||
while ! curl -s "http://localhost:$FRONTEND_PORT" > /dev/null 2>&1; do
|
while ! curl -s "http://localhost:$FRONTEND_PORT" > /dev/null 2>&1; do
|
||||||
retries=$((retries + 1))
|
retries=$((retries + 1))
|
||||||
if [ $retries -ge 30 ]; then
|
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
|
tail -20 logs/dev.log 2>/dev/null
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
sleep 1
|
sleep 1
|
||||||
done
|
done
|
||||||
|
|
||||||
success "前端服务已启动 (PID: $(cat "$FRONTEND_PID_FILE"), 端口: $FRONTEND_PORT)"
|
success "前端已启动 (PID: $(cat "$FRONTEND_PID_FILE"), 端口: $FRONTEND_PORT)"
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,7 +192,7 @@ stop_frontend() {
|
|||||||
local pid=$(cat "$FRONTEND_PID_FILE" 2>/dev/null || echo "")
|
local pid=$(cat "$FRONTEND_PID_FILE" 2>/dev/null || echo "")
|
||||||
if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
|
if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
|
||||||
kill "$pid" 2>/dev/null || true
|
kill "$pid" 2>/dev/null || true
|
||||||
success "前端服务已停止"
|
success "前端已停止"
|
||||||
fi
|
fi
|
||||||
rm -f "$FRONTEND_PID_FILE"
|
rm -f "$FRONTEND_PID_FILE"
|
||||||
fi
|
fi
|
||||||
@@ -171,101 +200,24 @@ stop_frontend() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# 命令
|
# 本地开发命令
|
||||||
# ============================================================
|
# ============================================================
|
||||||
stop() {
|
cmd_dev() {
|
||||||
info "正在停止服务..."
|
banner "========== $PROJECT_NAME (本地开发) =========="
|
||||||
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 启动脚本 =========="
|
|
||||||
|
|
||||||
# 检查依赖
|
# 检查依赖
|
||||||
if ! command -v go &> /dev/null; then
|
if ! command -v go &> /dev/null; then
|
||||||
error "Go 未安装,请先安装 Go"
|
error "Go 未安装,请先安装 Go 1.21+"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
if ! command -v npm &> /dev/null; then
|
if ! command -v npm &> /dev/null; then
|
||||||
error "npm 未安装,请先安装 Node.js"
|
error "npm 未安装,请先安装 Node.js 20+"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if start_backend && start_frontend; then
|
if start_backend && start_frontend; then
|
||||||
echo ""
|
echo ""
|
||||||
success "========== 所有服务已启动 =========="
|
banner "========== 所有服务已启动 =========="
|
||||||
info "前端地址: http://localhost:$FRONTEND_PORT"
|
info "前端地址: http://localhost:$FRONTEND_PORT"
|
||||||
info "后端地址: http://localhost:$BACKEND_PORT"
|
info "后端地址: http://localhost:$BACKEND_PORT"
|
||||||
info "健康检查: http://localhost:$BACKEND_PORT/health"
|
info "健康检查: http://localhost:$BACKEND_PORT/health"
|
||||||
@@ -276,37 +228,335 @@ main() {
|
|||||||
error "服务启动失败"
|
error "服务启动失败"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
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)
|
down)
|
||||||
stop
|
info "停止 Docker Compose..."
|
||||||
|
docker compose down
|
||||||
|
success "容器已停止"
|
||||||
|
;;
|
||||||
|
build)
|
||||||
|
info "构建 Docker 镜像..."
|
||||||
|
docker compose build
|
||||||
|
success "镜像构建完成"
|
||||||
;;
|
;;
|
||||||
restart)
|
restart)
|
||||||
stop
|
info "重启 Docker Compose..."
|
||||||
sleep 2
|
docker compose restart
|
||||||
main start
|
success "容器已重启"
|
||||||
;;
|
|
||||||
backend)
|
|
||||||
info "========== $PROJECT_NAME (仅后端) =========="
|
|
||||||
start_backend
|
|
||||||
show_status
|
|
||||||
;;
|
|
||||||
frontend)
|
|
||||||
info "========== $PROJECT_NAME (仅前端) =========="
|
|
||||||
start_frontend
|
|
||||||
show_status
|
|
||||||
;;
|
|
||||||
status)
|
|
||||||
show_status
|
|
||||||
;;
|
;;
|
||||||
logs)
|
logs)
|
||||||
show_logs
|
info "Docker 实时日志 (Ctrl+C 退出):"
|
||||||
|
docker compose logs -f
|
||||||
|
;;
|
||||||
|
ps|status)
|
||||||
|
docker compose ps
|
||||||
;;
|
;;
|
||||||
help|--help|-h)
|
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"
|
error "未知 docker 子命令: $action"
|
||||||
show_help
|
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
|
exit 1
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|||||||
Reference in new issue
Block a user