feat(rdp): 新增 RDP 远程桌面/SMB/账单同步能力 + 清理编译产物

主要变更:
- RDP 远程桌面:新增 backend/pkg/rdp(RDP 引擎 + credssp/display/rle/trace)、
  backend/controller/rdp*(连接/凭证/文件/会话/实时)、frontend RDP 组件与 API
- Windows SMB 文件传输:backend/pkg/winsmb
- 云账单同步:backend/pkg/billingsync + billing_sync/aliyun_billing,前端账单详情
- Agent CLI Kubernetes 工具:kubernetes_action/get、pod_execute/logs
- 服务器导入助手:server_import_helpers、ImportCredentialSettings
- 国际化与组件重构(cloud/deploy/monitor/server/system 等多模块)
- 清理编译产物二进制:移除 backend/server、backend/agent/bin/aiops-agent-*
- .gitignore:新增 backend/agent/ 与 .cache/(避免 334MB go-build 缓存误入库)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
LoganandClaude committed 2026-07-04 01:50:36 +08:00
1 parent 777d9e4e71
commit 76ec9387f0
202 files changed
+16555 -2543

No files matched your search

+133
View File
@@ -0,0 +1,133 @@
package router
import (
"fmt"
"net/http"
"os"
"path/filepath"
"strings"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
type agentStaticPaths struct {
binDir string
deployDir string
}
var agentBinaryFiles = map[string]string{
"linux-amd64": "aiops-agent-linux-amd64",
"linux-arm64": "aiops-agent-linux-arm64",
"darwin-arm64": "aiops-agent-darwin-arm64",
}
// registerAgentDownloads 注册 Agent 安装脚本和二进制下载路由,避免维护 backend/agent 重复目录。 // Logan.Li
func registerAgentDownloads(r *gin.Engine, log *zap.Logger) {
paths := resolveAgentStaticPaths(log)
r.GET("/agent/arch", func(c *gin.Context) {
base := requestBaseURL(c)
downloads := availableAgentDownloads(paths.binDir)
c.JSON(http.StatusOK, gin.H{
"version": "1.0.0",
"downloads": downloads,
"install": fmt.Sprintf("curl -fsSL %s/agent/install.sh | sudo bash -s -- --server %s --token <token>", base, base),
})
})
r.GET("/agent/install.sh", func(c *gin.Context) {
serveAgentStaticFile(c, paths.deployDir, "install.sh")
})
for _, fileName := range agentBinaryFiles {
name := fileName
r.GET("/agent/"+name, func(c *gin.Context) {
serveAgentStaticFile(c, paths.binDir, name)
})
}
r.GET("/agent/bin/:file", func(c *gin.Context) {
serveAgentStaticFile(c, paths.binDir, c.Param("file"))
})
r.GET("/api/agent/bin/:file", func(c *gin.Context) {
serveAgentStaticFile(c, paths.binDir, c.Param("file"))
})
}
// resolveAgentStaticPaths 按部署形态寻找唯一 Agent 资源目录:容器优先,其次项目根目录。 // Logan.Li
func resolveAgentStaticPaths(log *zap.Logger) agentStaticPaths {
paths := agentStaticPaths{
binDir: firstExistingDir([]string{
"/app/agent/bin",
"./agent/bin",
"../agent/bin",
}),
deployDir: firstExistingDir([]string{
"/app/agent/deploy",
"./agent/deploy",
"../agent/deploy",
}),
}
if log != nil {
log.Info("Agent 静态资源目录已解析",
zap.String("bin_dir", paths.binDir),
zap.String("deploy_dir", paths.deployDir),
)
}
return paths
}
func firstExistingDir(candidates []string) string {
for _, candidate := range candidates {
info, err := os.Stat(candidate)
if err == nil && info.IsDir() {
return candidate
}
}
return ""
}
func availableAgentDownloads(binDir string) gin.H {
downloads := gin.H{}
for platform, fileName := range agentBinaryFiles {
if binDir == "" {
continue
}
fullPath := filepath.Join(binDir, fileName)
if info, err := os.Stat(fullPath); err == nil && !info.IsDir() {
downloads[platform] = "/agent/" + fileName
}
}
return downloads
}
func requestBaseURL(c *gin.Context) string {
scheme := "http"
if c.Request.TLS != nil {
scheme = "https"
}
if proto := c.GetHeader("X-Forwarded-Proto"); proto != "" {
scheme = proto
}
host := c.Request.Host
if host == "" {
host = "localhost:8080"
}
return fmt.Sprintf("%s://%s", scheme, host)
}
func serveAgentStaticFile(c *gin.Context, dir string, fileName string) {
if dir == "" {
c.JSON(http.StatusNotFound, gin.H{"error": "agent static directory not found"})
return
}
if strings.Contains(fileName, "/") || strings.Contains(fileName, "\\") || fileName == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid agent file"})
return
}
fullPath := filepath.Join(dir, fileName)
if info, err := os.Stat(fullPath); err != nil || info.IsDir() {
c.JSON(http.StatusNotFound, gin.H{"error": "agent file not found"})
return
}
c.File(fullPath)
}
+56 -58
View File
@@ -1,17 +1,16 @@
package router
import (
"ezops/config"
"ezops/controller"
"ezops/middleware"
"ezops/config"
"ezops/model"
"ezops/service"
ezredis "ezops/pkg/redis"
"ezops/pkg/siteprobe"
"ezops/pkg/systask"
"ezops/pkg/toolinstaller"
"ezops/pkg/wshub"
"fmt"
"ezops/service"
"strconv"
"github.com/gin-gonic/gin"
@@ -35,35 +34,7 @@ func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config, c
})
// Agent 二进制下载 + 一键安装脚本(无需鉴权,方便 curl|bash)
r.Static("/agent/bin", "./agent/bin")
r.StaticFile("/agent/install.sh", "./agent/deploy/install.sh")
r.StaticFile("/agent/aiops-agent-linux-amd64", "./agent/bin/aiops-agent-linux-amd64")
r.StaticFile("/agent/aiops-agent-linux-arm64", "./agent/bin/aiops-agent-linux-arm64")
r.StaticFile("/agent/aiops-agent-darwin-arm64", "./agent/bin/aiops-agent-darwin-arm64") // macOS Apple Silicon // Logan.Li
r.GET("/agent/arch", func(c *gin.Context) {
// 动态构建安装命令 URL(根据请求来源,兼容 nginx 反代)
scheme := "http"
if c.Request.TLS != nil {
scheme = "https"
}
if proto := c.GetHeader("X-Forwarded-Proto"); proto != "" {
scheme = proto
}
host := c.Request.Host
if host == "" {
host = "localhost:8080"
}
base := fmt.Sprintf("%s://%s", scheme, host)
c.JSON(200, gin.H{
"version": "1.0.0",
"downloads": gin.H{
"linux-amd64": "/agent/aiops-agent-linux-amd64",
"linux-arm64": "/agent/aiops-agent-linux-arm64",
"darwin-arm64": "/agent/aiops-agent-darwin-arm64",
},
"install": fmt.Sprintf("curl -fsSL %s/agent/install.sh | sudo bash -s -- --server %s --token <token>", base, base),
})
})
registerAgentDownloads(r, log)
// === A3: 统一 WebSocket 连接管理器 === // Logan.Li
// 所有 WebSocket handler 共享同一个 WSManager(心跳、连接注册、监控、优雅关闭)
@@ -73,6 +44,9 @@ func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config, c
auditHub := controller.NewAuditHub(wsMgr)
webShellHandler := controller.NewWebShellHandler(db, auditHub, wsMgr)
r.GET("/ws/terminal", middleware.JWTAuth(&cfg.JWT, db), webShellHandler.Terminal)
rdpHandler := controller.NewRDPHandler(db, cfg.JWT.Secret, wsMgr)
r.GET("/ws/rdp", middleware.JWTAuth(&cfg.JWT, db), rdpHandler.Serve)
r.GET("/ws/rdp-audit/:sessionId", middleware.JWTAuth(&cfg.JWT, db), rdpHandler.ServeLive)
// WebSocket 审计实时流 — 在线会话实时查看 // Logan.Li
r.GET("/ws/audit/:sessionId", middleware.JWTAuth(&cfg.JWT, db), func(c *gin.Context) {
@@ -151,6 +125,10 @@ func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config, c
api.POST("/auth/login", authHandler.Login)
api.POST("/auth/register", authHandler.Register)
// RDP 暂存文件拉取(token 鉴权,供 Windows iwr 回退下载)// Logan.Li
rdpFilePublic := controller.NewRDPFileHandler(db, cfg.JWT.Secret)
api.GET("/rdp-staged/:token", rdpFilePublic.ServeStaged)
// Webhook 触发路由(无需 JWT,由 Webhook Token 签名验证)
pipelineHandler := controller.NewPipelineHandler(db, redisCache, ti)
webhookGroup := api.Group("/webhook")
@@ -272,18 +250,28 @@ func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config, c
servers.POST("/batch-delete", serverHandlerV2.BatchDeleteServers)
servers.POST("/batch", serverHandlerV2.BatchCreateServers)
// Agent 远程安装 // Logan.Li
servers.POST("/:id/install-agent", agentInstallHandler.InstallAgent)
servers.POST("/install-agent", agentInstallHandler.BatchInstallAgent)
// Agent 远程安装 // Logan.Li
servers.POST("/:id/install-agent", agentInstallHandler.InstallAgent)
servers.POST("/install-agent", agentInstallHandler.BatchInstallAgent)
// SFTP 文件浏览 // Logan.Li
sftpHandler := controller.NewSFTPHandler(db)
servers.GET("/:id/files", sftpHandler.ListFiles)
servers.GET("/:id/files/download", sftpHandler.DownloadFile)
servers.POST("/:id/files/upload", sftpHandler.UploadFile)
servers.DELETE("/:id/files", sftpHandler.DeleteFile)
servers.POST("/:id/files/mkdir", sftpHandler.Mkdir)
}
// SFTP 文件浏览 // Logan.Li
sftpHandler := controller.NewSFTPHandler(db)
servers.GET("/:id/files", sftpHandler.ListFiles)
servers.GET("/:id/files/download", sftpHandler.DownloadFile)
servers.POST("/:id/files/upload", sftpHandler.UploadFile)
servers.DELETE("/:id/files", sftpHandler.DeleteFile)
servers.POST("/:id/files/mkdir", sftpHandler.Mkdir)
// RDP 虚拟 Download 盘(SMB 管理共享,一键上传下载)// Logan.Li
rdpFileHandler := controller.NewRDPFileHandler(db, cfg.JWT.Secret)
servers.GET("/:id/rdp-files", rdpFileHandler.ListFiles)
servers.POST("/:id/rdp-files/upload", rdpFileHandler.UploadFile)
servers.GET("/:id/rdp-files/download", rdpFileHandler.DownloadFile)
servers.POST("/:id/rdp-files/stage", rdpFileHandler.StageUpload)
}
// 云账单处理器(云账号同步账单路由共用) // Logan.Li
billingHandler := controller.NewBillingHandler(db)
// 云账号路由
cloudAccountHandler := controller.NewCloudAccountHandler(db)
@@ -295,11 +283,12 @@ func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config, c
cloudAccounts.DELETE("/:id", cloudAccountHandler.DeleteCloudAccount)
// 云同步路由 — 区域同步 / 主机发现 / 导入主机 // Logan.Li
syncHandler := controller.NewCloudSyncHandler(db)
syncHandler := controller.NewCloudSyncHandler(db, cfg.JWT.Secret)
cloudAccounts.POST("/:id/sync-regions", syncHandler.SyncRegions)
cloudAccounts.GET("/:id/regions", syncHandler.GetAccountRegions)
cloudAccounts.POST("/:id/discover-instances", syncHandler.DiscoverInstances)
cloudAccounts.POST("/:id/import-instances", syncHandler.ImportInstances)
cloudAccounts.POST("/:id/sync-billing", billingHandler.SyncAccountBilling)
}
// 域名路由
@@ -334,7 +323,6 @@ func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config, c
}
// 云账单路由
billingHandler := controller.NewBillingHandler(db)
billings := authorized.Group("/cloud-billings")
{
billings.GET("", billingHandler.ListBillings)
@@ -343,6 +331,7 @@ func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config, c
billings.DELETE("/:id", billingHandler.DeleteBilling)
billings.GET("/stats", billingHandler.GetBillingStats)
billings.GET("/providers", billingHandler.GetProviderOptions)
billings.POST("/sync", billingHandler.SyncYesterdayBillings)
}
billingAlerts := authorized.Group("/billing-alerts")
{
@@ -376,15 +365,15 @@ func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config, c
clusters.POST("", k8sHandler.CreateCluster)
clusters.PUT("/:id", k8sHandler.UpdateCluster)
clusters.DELETE("/:id", k8sHandler.DeleteCluster)
clusters.POST("/:id/sync", k8sHandler.SyncCluster) // 同步集群资源 // Logan.Li
clusters.GET("/:id/nodes", k8sHandler.ListNodes) // Deprecated: use GET /kubernetes/:clusterCode/nodes
clusters.GET("/:id/nodes/:name", k8sHandler.GetNodeDetail) // M10 节点详情 // Logan.Li
clusters.GET("/:id/pods", k8sHandler.ListPods) // Deprecated: use GET /kubernetes/:clusterCode/pods
clusters.GET("/:id/deployments", k8sHandler.ListDeployments) // Deprecated: use GET /kubernetes/:clusterCode/deployments
clusters.GET("/:id/services", k8sHandler.ListServices) // Deprecated: use GET /kubernetes/:clusterCode/services
clusters.GET("/:id/ingresses", k8sHandler.ListIngresses) // Deprecated: use GET /kubernetes/:clusterCode/ingresses
clusters.GET("/:id/topology", k8sHandler.GetTopology) // 获取拓扑布局 // Logan.Li
clusters.PUT("/:id/topology", k8sHandler.SaveTopology) // 保存拓扑布局 // Logan.Li
clusters.POST("/:id/sync", k8sHandler.SyncCluster) // 同步集群资源 // Logan.Li
clusters.GET("/:id/nodes", k8sHandler.ListNodes) // Deprecated: use GET /kubernetes/:clusterCode/nodes
clusters.GET("/:id/nodes/:name", k8sHandler.GetNodeDetail) // M10 节点详情 // Logan.Li
clusters.GET("/:id/pods", k8sHandler.ListPods) // Deprecated: use GET /kubernetes/:clusterCode/pods
clusters.GET("/:id/deployments", k8sHandler.ListDeployments) // Deprecated: use GET /kubernetes/:clusterCode/deployments
clusters.GET("/:id/services", k8sHandler.ListServices) // Deprecated: use GET /kubernetes/:clusterCode/services
clusters.GET("/:id/ingresses", k8sHandler.ListIngresses) // Deprecated: use GET /kubernetes/:clusterCode/ingresses
clusters.GET("/:id/topology", k8sHandler.GetTopology) // 获取拓扑布局 // Logan.Li
clusters.PUT("/:id/topology", k8sHandler.SaveTopology) // 保存拓扑布局 // Logan.Li
// Pod 日志 // Logan.Li
clusters.GET("/:id/namespaces/:namespace/pods/:podName/logs", k8sHandler.GetPodLogs)
@@ -681,7 +670,7 @@ func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config, c
releases.GET("", pipelineHandler.ListReleases)
releases.POST("", pipelineHandler.CreateRelease)
releases.POST("/:id/approve", pipelineHandler.ApproveRelease)
releases.POST("/:id/reject", pipelineHandler.RejectRelease) // 驳回发布 // Logan.Li
releases.POST("/:id/reject", pipelineHandler.RejectRelease) // 驳回发布 // Logan.Li
releases.POST("/:id/deploy", pipelineHandler.DeployRelease)
releases.POST("/:id/rollback", pipelineHandler.RollbackRelease)
}
@@ -870,6 +859,18 @@ func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config, c
ts.GET("/:id/stream", terminalSessionHandler.StreamSession)
}
// Windows RDP 图形会话审计路由(只做录屏帧回放,不做命令审计) // Logan.Li
rdpSessionHandler := controller.NewRDPSessionHandler(db, rdpHandler.KillSession)
rs := authorized.Group("/rdp-sessions")
{
rs.GET("", rdpSessionHandler.ListRDPSessions)
rs.GET("/:id", rdpSessionHandler.GetRDPSession)
rs.GET("/:id/record", rdpSessionHandler.GetRDPRecord)
rs.POST("/:id/kill", rdpSessionHandler.KillRDPSession)
rs.GET("/:id/download", rdpSessionHandler.DownloadRDPRecord)
rs.POST("/:id/replay-view", rdpSessionHandler.LogRDPReplayView)
}
// 会话命令审计路由(JumpServer 式命令审计) // Logan.Li
scHandler := controller.NewSessionCommandHandler(db)
sc := authorized.Group("/session-commands")
@@ -938,9 +939,6 @@ func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config, c
agentQuery.PUT("/port-policies/:hostname", agentHandler.SavePortPolicy)
}
// Agent 二进制下载(无需鉴权,Agent 升级时使用)
r.Static("/api/agent/bin", "/app/agent/bin")
// AI 智能助手 - LLM 配置 + 对话代理(OpenAI 兼容) // Logan.Li
llmHandler := controller.NewLLMHandler(db)
aiopsGroup := authorized.Group("/aiops")