fix(aiops): Agent CLI 连接 ssh-key 主机永久转圈修复

- buildSSHConfig 放宽认证判定:SSHKey 非空即走密钥认证,不再强依赖 AuthType=="key",
  兼容编辑表单/历史数据未正确设置 AuthType 的 ssh-key 主机(与 WebShell 行为对齐)
- agent_cli WS 握手:Upgrade 提前到预检之前,预检失败经 writeWSCloseError 推具体
  error 帧;写 error 后 sleep 120ms 再发 close,避免 TCP 提前关闭导致 error 帧丢失
- 前端 AgentCLI 连接兜底:新增 8s 连接超时 + onclose 区分"从未连上"(push 明确
  原因提示)与"连上后断开",杜绝预检失败时"正在连接主机"永久转圈、看不到原因
- 引擎配置补传 ReasoningEnabled

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
LoganandClaude committed 2026-06-30 23:25:04 +08:00
1 parent 01967bc52b
commit 3b19ae6e98
3 files changed
+73 -25

No files matched your search

+32 -15
View File
@@ -59,6 +59,18 @@ type wsControl struct {
ConfirmID string `json:"confirm_id,omitempty"` // confirm/deny 时
}
// writeWSCloseError 在 agentConn 建立前,直接通过 conn 推送一条 error 事件再关闭连接 // Logan.Li
// 用于 WS 升级后的预检失败(SSH 凭据/LLM 配置),让前端能拿到具体原因而非"一直转圈"
func writeWSCloseError(conn *websocket.Conn, msg string) {
_ = conn.WriteJSON(wsEvent{Type: "error", Message: msg})
// 留出短暂窗口让浏览器接收 error 帧,再发送 close 帧:否则 close 帧紧随其后、叠加
// defer conn.Close() 触发的 TCP 关闭,可能令 error 帧被丢弃,前端表现就是"正在连接主机"
// 永久转圈、看不到真实原因 // Logan.Li
time.Sleep(120 * time.Millisecond)
_ = conn.WriteMessage(websocket.CloseMessage,
websocket.FormatCloseMessage(websocket.CloseNormalClosure, msg))
}
// agentConn 单个 WS 连接的运行态 // Logan.Li
type agentConn struct {
conn *websocket.Conn
@@ -84,24 +96,28 @@ func (h *AgentCLIHandler) Run(c *gin.Context) {
c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "主机不存在"})
return
}
sshCfg, err := buildSSHConfig(h.db, server)
if err != nil {
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: err.Error()})
return
}
// LLM 配置
var llmCfg model.LLMConfig
if err := h.db.First(&llmCfg).Error; err != nil || !llmCfg.Enabled {
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "LLM 服务未配置或未启用,请先在「大模型设置」中配置"})
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 服务未配置或未启用,请先在「大模型设置」中配置")
return
}
ac := &agentConn{
conn: conn, db: h.db,
userID: middleware.GetUserID(c), server: server,
@@ -166,10 +182,11 @@ func (ac *agentConn) startTask(sshCfg remoteexec.SSHConfig, llmCfg model.LLMConf
// 构建引擎
engine, err := agentcli.NewEngine(agentcli.EngineConfig{
LLM: llm.Config{
Protocol: resolveProtocol(llmCfg.Provider),
Endpoint: resolveEndpoint(llmCfg.Provider, llmCfg.Endpoint),
APIKey: llmCfg.APIKey, Model: llmCfg.Model,
Temperature: llmCfg.Temperature, MaxTokens: llmCfg.MaxTokens,
Protocol: resolveProtocol(llmCfg.Provider),
Endpoint: resolveEndpoint(llmCfg.Provider, llmCfg.Endpoint),
APIKey: llmCfg.APIKey, Model: llmCfg.Model,
Temperature: llmCfg.Temperature, MaxTokens: llmCfg.MaxTokens,
ReasoningEnabled: llmCfg.ReasoningEnabled,
},
SSH: sshCfg,
SafetyMode: safety.ModeBalanced,
+8 -7
View File
@@ -289,10 +289,15 @@ func buildSSHConfig(db *gorm.DB, server model.Server) (remoteexec.SSHConfig, err
cfg.User = "root"
}
// 优先使用 server 自带的认证信息
if server.AuthType == "key" && server.SSHKey != "" {
// 优先 SSH 密钥:只要 server.SSHKey 有值就用密钥认证,不依赖 AuthType 严格等于 "key",
// 兼容历史数据/编辑表单未正确设置 AuthType 的 ssh-key 主机(与 WebShell 行为对齐)// Logan.Li
if server.SSHKey != "" {
cfg.AuthType = remoteexec.AuthKey
cfg.KeyData = server.SSHKey
// 同时带上 password 作为兜底:remoteexec 在密钥解析失败时会回退用密码
if server.Password != "" {
cfg.Password = server.Password
}
return cfg, nil
}
if server.Password != "" {
@@ -301,11 +306,7 @@ func buildSSHConfig(db *gorm.DB, server model.Server) (remoteexec.SSHConfig, err
return cfg, nil
}
// 从凭证表查找关联的 SSH 凭据
// Server 模型目前没有 CredentialID 字段,通过 tags 或 name 匹配
// 未来可扩展 server.credential_id FK
return cfg, fmt.Errorf("服务器 %s 未配置 SSH 认证(密码或密钥)", server.Hostname)
return cfg, fmt.Errorf("服务器 %s 未配置 SSH 认证(密码或密钥),请到服务器详情关联 SSH 凭据", server.Hostname)
}
// ==================== 定时任务远程执行(对接 CronJob target_type) ====================