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

+128 -36
View File
@@ -1,5 +1,5 @@
// agent_cli.go - Agent CLI WebSocket 控制器 // Logan.Li
// 驱动 pkg/agentcli 引擎:关联主机(model.Server 凭据)→ 构建引擎 → ReAct 循环,
// 驱动 pkg/agentcli 引擎:主机模式(SSH)或 K8s 模式(集群查询/Pod 执行)→ ReAct 循环,
// 通过 WebSocket 实时推送思考/工具调用/输出/结果,高危命令经前端确认,过程持久化到 DB。
package controller
@@ -78,22 +78,37 @@ type agentConn struct {
conn *websocket.Conn
db *gorm.DB
userID uint
mode string
server model.Server
sshCfg remoteexec.SSHConfig
kubernetesCtx *kubernetesAgentContext
session *model.AgentCLISession
taskCtx context.Context
cancel context.CancelFunc
totalTokens int
tokenMu sync.Mutex
writeMu sync.Mutex
confirms map[string]chan bool
cfMu sync.Mutex
convHistory []llm.Message // 多轮追问对话历史(不含 system) // Logan.Li
running bool
runningMu sync.Mutex
confirms map[string]chan bool
cfMu sync.Mutex
convHistory []llm.Message
running bool
runningMu sync.Mutex
}
// Run WebSocket 入口:GET /ws/agent-cli?id=<serverID>&token=<jwt> // Logan.Li
// Run WebSocket 入口 // Logan.Li
// 主机: GET /ws/agent-cli?mode=host&id=<serverID>&token=<jwt>
// Kubernetes: GET /ws/agent-cli?mode=kubernetes&cluster=<code>&namespace=<ns>&pod=<可选>&container=<可选>&token=<jwt>
func (h *AgentCLIHandler) Run(c *gin.Context) {
mode := agentcli.NormalizeRunMode(c.DefaultQuery("mode", agentcli.RunModeHost))
if mode == agentcli.RunModeKubernetes {
h.runKubernetesWS(c)
return
}
h.runHostWS(c)
}
// runHostWS 主机模式 WebSocket // Logan.Li
func (h *AgentCLIHandler) runHostWS(c *gin.Context) {
serverID := c.Query("id")
if serverID == "" {
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "缺少主机 id"})
@@ -105,21 +120,17 @@ func (h *AgentCLIHandler) Run(c *gin.Context) {
return
}
// 先升级 WebSocket:SSH 凭据 / LLM 配置预检放到升级之后,失败时通过 WS error 事件
// 把真实原因推给前端,避免握手 400 导致前端"一直转圈"看不到原因 // Logan.Li
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
return
}
defer conn.Close()
// SSH 凭据预检
sshCfg, err := buildSSHConfig(h.db, server)
if err != nil {
writeWSCloseError(conn, "SSH 连接失败: "+err.Error())
return
}
// LLM 配置预检
var llmCfg model.LLMConfig
if err := h.db.First(&llmCfg).Error; err != nil || !llmCfg.Enabled {
writeWSCloseError(conn, "LLM 服务未配置或未启用,请先在「大模型设置」中配置")
@@ -127,18 +138,57 @@ func (h *AgentCLIHandler) Run(c *gin.Context) {
}
ac := &agentConn{
conn: conn, db: h.db,
userID: middleware.GetUserID(c), server: server,
taskCtx: context.Background(),
confirms: make(map[string]chan bool),
conn: conn, db: h.db, mode: agentcli.RunModeHost,
userID: middleware.GetUserID(c), server: server, sshCfg: sshCfg,
taskCtx: context.Background(), confirms: make(map[string]chan bool),
}
// 推送主机上下文
ac.write(wsEvent{Type: "session", SessionID: 0,
Message: fmt.Sprintf("已连接主机 %s(%s),LLM: %s。请描述要执行的任务。", server.Hostname, server.IP, llmCfg.Model)})
ac.wsReadLoop()
}
// 读循环:处理前端控制消息
// runKubernetesWS Kubernetes 模式 WebSocket // Logan.Li
func (h *AgentCLIHandler) runKubernetesWS(c *gin.Context) {
clusterCode, namespace, podName, container := parseKubernetesWSParams(c)
if clusterCode == "" || namespace == "" {
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "缺少 cluster 或 namespace 参数"})
return
}
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
return
}
defer conn.Close()
kCtx, err := loadKubernetesAgentContext(h.db, clusterCode, namespace, podName)
if err != nil {
writeWSCloseError(conn, "Kubernetes 连接失败: "+err.Error())
return
}
kCtx.container = container
var llmCfg model.LLMConfig
if err := h.db.First(&llmCfg).Error; err != nil || !llmCfg.Enabled {
writeWSCloseError(conn, "LLM 服务未配置或未启用,请先在「大模型设置」中配置")
return
}
ac := &agentConn{
conn: conn, db: h.db, mode: agentcli.RunModeKubernetes,
userID: middleware.GetUserID(c), kubernetesCtx: kCtx,
taskCtx: context.Background(), confirms: make(map[string]chan bool),
}
target := buildKubernetesTargetInfo(kCtx)
ac.write(wsEvent{Type: "session", SessionID: 0,
Message: fmt.Sprintf("已连接 Kubernetes 集群,LLM: %s。\n%s\n请描述要执行的任务。", llmCfg.Model, target)})
ac.wsReadLoop()
}
// wsReadLoop 处理前端控制消息 // Logan.Li
func (ac *agentConn) wsReadLoop() {
for {
_, data, err := conn.ReadMessage()
_, data, err := ac.conn.ReadMessage()
if err != nil {
ac.abort()
return
@@ -153,13 +203,13 @@ func (h *AgentCLIHandler) Run(c *gin.Context) {
ac.write(wsEvent{Type: "error", Message: "任务描述不能为空"})
continue
}
ac.startTask(sshCfg, ctrl.Query, ctrl.SafetyMode)
ac.startTask(ctrl.Query, ctrl.SafetyMode)
case "continue":
if ctrl.Query == "" {
ac.write(wsEvent{Type: "error", Message: "追问内容不能为空"})
continue
}
ac.continueTask(sshCfg, ctrl.Query, ctrl.SafetyMode)
ac.continueTask(ctrl.Query, ctrl.SafetyMode)
case "resume":
if ctrl.SessionID == 0 {
ac.write(wsEvent{Type: "error", Message: "缺少 session_id"})
@@ -189,7 +239,7 @@ func parseSafetyMode(mode string) safety.Mode {
}
// startTask 启动新任务(新会话,清空对话历史) // Logan.Li
func (ac *agentConn) startTask(sshCfg remoteexec.SSHConfig, query, safetyMode string) {
func (ac *agentConn) startTask(query, safetyMode string) {
ac.abort()
var llmCfg model.LLMConfig
@@ -203,11 +253,20 @@ func (ac *agentConn) startTask(sshCfg remoteexec.SSHConfig, query, safetyMode st
title = string(r[:60]) + "..."
}
sess := &model.AgentCLISession{
UserID: ac.userID, ServerID: ac.server.ID,
ServerName: ac.server.Hostname, ServerHost: ac.server.IP,
Title: title, Status: "running",
UserID: ac.userID, Mode: ac.mode, Title: title, Status: "running",
Provider: llmCfg.Provider, Model: llmCfg.Model,
}
if ac.mode == agentcli.RunModeKubernetes && ac.kubernetesCtx != nil {
sess.ClusterID = ac.kubernetesCtx.cluster.ID
sess.ClusterName = ac.kubernetesCtx.cluster.Name
sess.Namespace = ac.kubernetesCtx.namespace
sess.PodName = ac.kubernetesCtx.podName
sess.Container = ac.kubernetesCtx.container
} else {
sess.ServerID = ac.server.ID
sess.ServerName = ac.server.Hostname
sess.ServerHost = ac.server.IP
}
if err := ac.db.Create(sess).Error; err != nil {
ac.write(wsEvent{Type: "error", Message: "创建会话失败: " + err.Error()})
return
@@ -215,7 +274,7 @@ func (ac *agentConn) startTask(sshCfg remoteexec.SSHConfig, query, safetyMode st
ac.session = sess
ac.convHistory = nil
ac.setTotalTokens(0)
ac.runTask(sshCfg, sess, query, safetyMode, llmCfg, false)
ac.runTask(sess, query, safetyMode, llmCfg, false)
}
// resumeSession 从历史会话恢复上下文,供继续追问 // Logan.Li
@@ -225,8 +284,16 @@ func (ac *agentConn) resumeSession(sessionID uint) {
return
}
var sess model.AgentCLISession
if err := ac.db.Where("user_id = ? AND server_id = ?", ac.userID, ac.server.ID).
First(&sess, sessionID).Error; err != nil {
q := ac.db.Where("user_id = ?", ac.userID)
if ac.mode == agentcli.RunModeKubernetes {
q = q.Where("mode IN ?", []string{agentcli.RunModeKubernetes, "k8s"})
if ac.kubernetesCtx != nil {
q = q.Where("cluster_id = ? AND namespace = ?", ac.kubernetesCtx.cluster.ID, ac.kubernetesCtx.namespace)
}
} else {
q = q.Where("mode = ? AND server_id = ?", ac.mode, ac.server.ID)
}
if err := q.First(&sess, sessionID).Error; err != nil {
ac.write(wsEvent{Type: "error", Message: "会话不存在或无权访问"})
return
}
@@ -246,7 +313,7 @@ func (ac *agentConn) resumeSession(sessionID uint) {
}
// continueTask 在同一会话内追问(保留上下文) // Logan.Li
func (ac *agentConn) continueTask(sshCfg remoteexec.SSHConfig, query, safetyMode string) {
func (ac *agentConn) continueTask(query, safetyMode string) {
if ac.isRunning() {
ac.write(wsEvent{Type: "error", Message: "任务进行中,请等待完成或停止"})
return
@@ -265,11 +332,11 @@ func (ac *agentConn) continueTask(sshCfg remoteexec.SSHConfig, query, safetyMode
ac.write(wsEvent{Type: "error", Message: "LLM 服务未配置或未启用,请先在「大模型设置」中配置"})
return
}
ac.runTask(sshCfg, ac.session, query, safetyMode, llmCfg, true)
ac.runTask(ac.session, query, safetyMode, llmCfg, true)
}
// runTask 执行一轮 ReAct(新建或追问共用) // Logan.Li
func (ac *agentConn) runTask(sshCfg remoteexec.SSHConfig, sess *model.AgentCLISession, query, safetyMode string, llmCfg model.LLMConfig, isContinue bool) {
func (ac *agentConn) runTask(sess *model.AgentCLISession, query, safetyMode string, llmCfg model.LLMConfig, isContinue bool) {
if ac.isRunning() {
ac.write(wsEvent{Type: "error", Message: "任务进行中,请等待完成或停止"})
return
@@ -287,14 +354,28 @@ func (ac *agentConn) runTask(sshCfg remoteexec.SSHConfig, sess *model.AgentCLISe
coreCfg := core.DefaultConfig()
coreCfg.RoundOffset = roundOffset
coreCfg.Mode = ac.mode
engine, err := agentcli.NewEngine(agentcli.EngineConfig{
engCfg := agentcli.EngineConfig{
Mode: ac.mode,
LLM: buildLLMConfigFromModel(llmCfg),
SSH: sshCfg,
SafetyMode: parseSafetyMode(safetyMode),
MemoryDir: fmt.Sprintf("data/agentcli/user_%d", ac.userID),
Core: coreCfg,
})
}
if ac.mode == agentcli.RunModeKubernetes {
if ac.kubernetesCtx == nil {
ac.write(wsEvent{Type: "error", Message: "Kubernetes 上下文丢失"})
return
}
engCfg.Kubernetes = &agentcli.KubernetesTarget{
Client: ac.kubernetesCtx.client, ClusterName: ac.kubernetesCtx.cluster.Name,
Namespace: ac.kubernetesCtx.namespace, PodName: ac.kubernetesCtx.podName, Container: ac.kubernetesCtx.container,
}
} else {
engCfg.SSH = ac.sshCfg
}
engine, err := agentcli.NewEngine(engCfg)
if err != nil {
ac.write(wsEvent{Type: "error", Message: "引擎初始化失败: " + err.Error()})
ac.finish(sess, "error", "引擎初始化失败: "+err.Error(), ac.lastRound(), 0)
@@ -311,8 +392,7 @@ func (ac *agentConn) runTask(sshCfg remoteexec.SSHConfig, sess *model.AgentCLISe
ac.persistMsg(sess.ID, 0, "user", "user", query, "", "", "", "", false, "", 0)
serverInfo := fmt.Sprintf("主机名: %s | IP: %s:%d | 用户: %s | OS: %s",
ac.server.Hostname, ac.server.IP, ac.server.Port, ac.server.Username, ac.server.OS)
targetInfo := ac.buildTargetInfo()
msg := "任务已开始"
if isContinue {
@@ -326,7 +406,7 @@ func (ac *agentConn) runTask(sshCfg remoteexec.SSHConfig, sess *model.AgentCLISe
engine.Close()
}()
start := time.Now()
result, err := engine.Run(ctx, query, serverInfo, prior, priorTokens)
result, err := engine.Run(ctx, query, targetInfo, prior, priorTokens)
if result != nil && len(result.History) > 0 {
ac.convHistory = result.History
ac.setTotalTokens(result.TotalTokens)
@@ -405,10 +485,22 @@ func (ac *agentConn) onEvent(e core.Event) {
}
}
// buildTargetInfo 构建 Prompt 目标上下文 // Logan.Li
func (ac *agentConn) buildTargetInfo() string {
if ac.mode == agentcli.RunModeKubernetes && ac.kubernetesCtx != nil {
return buildKubernetesTargetInfo(ac.kubernetesCtx)
}
return fmt.Sprintf("主机名: %s | IP: %s:%d | 用户: %s | OS: %s",
ac.server.Hostname, ac.server.IP, ac.server.Port, ac.server.Username, ac.server.OS)
}
// confirmFn 高危命令确认回调:推 confirm 事件并阻塞等前端回传 // Logan.Li
func (ac *agentConn) confirmFn(toolName string, args map[string]interface{}, risk tools.RiskLevel) (bool, error) {
confirmID := fmt.Sprintf("cf-%d", time.Now().UnixNano())
cmd, _ := args["command"].(string)
if cmd == "" && (toolName == "kubernetes_action" || toolName == "k8s_action") {
cmd = fmt.Sprintf("%v %v", args["action"], args["name"])
}
ac.write(wsEvent{Type: "confirm", ConfirmID: confirmID, ToolName: toolName, Command: cmd, Risk: risk.Display(), ToolArgs: args})
ch := make(chan bool, 1)