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:
1 parent
777d9e4e71
commit
76ec9387f0
202 files changed
+16555
-2543
No files matched your search
+128
-36
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user