From b8b1a3d5f65e75ac337e0e8628970faedb7e3b88 Mon Sep 17 00:00:00 2001 From: attacker Date: Thu, 18 Jun 2026 07:56:10 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20=E8=BF=9C=E7=A8=8B=E5=91=BD=E4=BB=A4?= =?UTF-8?q?=E7=9B=B4=E8=BF=9ESSH(=E5=8E=BB=E8=BD=AE=E8=AF=A2)=20+=20?= =?UTF-8?q?=E5=8E=BB=E6=8E=89=E6=97=A0=E7=94=A8=E5=91=BD=E4=BB=A4=E5=88=86?= =?UTF-8?q?=E7=B1=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../internal/api/handlers/agent_install.go | 51 ++++++++++++ backend/internal/api/routes/routes.go | 3 + .../src/components/agent/AgentMonitor.tsx | 82 ++++--------------- frontend/src/services/api.ts | 2 + 4 files changed, 70 insertions(+), 68 deletions(-) diff --git a/backend/internal/api/handlers/agent_install.go b/backend/internal/api/handlers/agent_install.go index 5d21722..97a040b 100644 --- a/backend/internal/api/handlers/agent_install.go +++ b/backend/internal/api/handlers/agent_install.go @@ -254,3 +254,54 @@ func (h *AgentInstallHandler) BatchInstallAgent(c *gin.Context) { }, }) } + +// RemoteExec 直连 SSH 执行命令(即时返回,无轮询) +// POST /api/v1/agent/exec +func (h *AgentInstallHandler) RemoteExec(c *gin.Context) { + var req struct { + Hostname string `json:"hostname"` + Command string `json:"command"` + Timeout int `json:"timeout"` + } + if err := c.ShouldBindJSON(&req); err != nil || req.Hostname == "" || req.Command == "" { + c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "请提供 hostname 和 command"}) + return + } + if req.Timeout <= 0 { + req.Timeout = 30 + } + + var server models.Server + if err := h.db.Where("hostname = ? OR ip = ?", req.Hostname, req.Hostname).First(&server).Error; err != nil { + c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "未找到服务器: " + req.Hostname}) + return + } + + client, err := sshDial(server) + if err != nil { + c.JSON(http.StatusOK, models.Response{Code: 500, Message: "SSH连接失败: " + err.Error()}) + return + } + defer client.Close() + + type result struct { + out string + err error + } + done := make(chan result, 1) + go func() { + out, err := runSSHCommand(client, req.Command) + done <- result{out, err} + }() + + select { + case r := <-done: + if r.err != nil { + c.JSON(http.StatusOK, models.Response{Code: 200, Message: "执行完成", Data: gin.H{"output": r.out, "error": r.err.Error()}}) + return + } + c.JSON(http.StatusOK, models.Response{Code: 200, Message: "执行成功", Data: gin.H{"output": r.out}}) + case <-time.After(time.Duration(req.Timeout) * time.Second): + c.JSON(http.StatusOK, models.Response{Code: 408, Message: fmt.Sprintf("命令执行超时(%ds)", req.Timeout)}) + } +} diff --git a/backend/internal/api/routes/routes.go b/backend/internal/api/routes/routes.go index 6124c09..0a6def1 100644 --- a/backend/internal/api/routes/routes.go +++ b/backend/internal/api/routes/routes.go @@ -142,6 +142,9 @@ func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config, c agentInstallHandler := handlers.NewAgentInstallHandler(db) servers.POST("/:id/install-agent", agentInstallHandler.InstallAgent) servers.POST("/install-agent", agentInstallHandler.BatchInstallAgent) + + // Agent 远程命令执行 + agentGroup.POST("/exec", agentInstallHandler.RemoteExec) } // 云账号路由 diff --git a/frontend/src/components/agent/AgentMonitor.tsx b/frontend/src/components/agent/AgentMonitor.tsx index 550972d..b2850ea 100644 --- a/frontend/src/components/agent/AgentMonitor.tsx +++ b/frontend/src/components/agent/AgentMonitor.tsx @@ -55,13 +55,10 @@ export function AgentMonitor() { // 远程命令 const [cmdOpen, setCmdOpen] = useState(false); const [cmdHost, setCmdHost] = useState(''); - const [cmdType, setCmdType] = useState('command'); const [cmdCommand, setCmdCommand] = useState(''); - const [cmdTimeout, setCmdTimeout] = useState('30'); + const [cmdTimeout, setCmdTimeout] = useState(30); const [cmdSubmitting, setCmdSubmitting] = useState(false); const [cmdResult, setCmdResult] = useState(null); - const [cmdPolling, setCmdPolling] = useState(false); - const pollTimerRef = useRef | null>(null); const loadLatest = useCallback(async (silent = false) => { if (!silent) setRefreshing(true); @@ -95,14 +92,13 @@ export function AgentMonitor() { // 打开远程命令弹窗 const openCommandDialog = (hostname: string) => { setCmdHost(hostname); - setCmdType('command'); setCmdCommand(''); - setCmdTimeout('30'); + setCmdTimeout(30); setCmdResult(null); setCmdOpen(true); }; - // 提交远程命令并轮询结果 + // 提交远程命令(直连 SSH 即时返回) const submitCommand = async () => { if (!cmdCommand.trim()) { toast.error('请输入要执行的命令'); @@ -111,62 +107,26 @@ export function AgentMonitor() { setCmdSubmitting(true); setCmdResult(null); try { - const res: any = await agentApi.createTask({ + const res: any = await agentApi.execCommand({ hostname: cmdHost, - type: cmdType, command: cmdCommand.trim(), timeout: Number(cmdTimeout) || 30, }); - const taskId = res?.id ?? res?.task_id; - toast.success('任务已下发'); - setCmdSubmitting(false); - - // 轮询任务结果 - if (taskId != null) { - setCmdPolling(true); - let attempts = 0; - pollTimerRef.current = setInterval(async () => { - attempts++; - try { - const tasks: any = await agentApi.listTasks({ hostname: cmdHost }); - const list = asList(tasks); - const t = list.find((x: any) => String(x.id) === String(taskId)); - if (t && t.status && t.status !== 'pending' && t.status !== 'running') { - setCmdResult(t); - setCmdPolling(false); - if (pollTimerRef.current) { - clearInterval(pollTimerRef.current); - pollTimerRef.current = null; - } - loadLatest(true); - } - } catch { - /* ignore */ - } - if (attempts > 15) { - // 30s 超时 - setCmdPolling(false); - setCmdResult({ status: 'timeout', result: '等待结果超时,请稍后在任务列表查看' }); - if (pollTimerRef.current) { - clearInterval(pollTimerRef.current); - pollTimerRef.current = null; - } - } - }, 2000); - } + setCmdResult({ + status: res?.code === 200 ? 'completed' : 'failed', + result: res?.data?.output || res?.message || '无输出', + error: res?.data?.error, + }); } catch (err: any) { - toast.error(err?.message || '任务下发失败'); + setCmdResult({ status: 'failed', result: err?.message || '执行失败' }); + } finally { setCmdSubmitting(false); + loadLatest(true); } }; const closeCommandDialog = () => { setCmdOpen(false); - if (pollTimerRef.current) { - clearInterval(pollTimerRef.current); - pollTimerRef.current = null; - } - setCmdPolling(false); }; const onlineCount = hosts.filter((h) => isOnline(h.last_report || h.last_reported_at || h.updated_at)).length; @@ -358,18 +318,6 @@ export function AgentMonitor() { className="h-9 bg-slate-800 border-slate-700 text-slate-200" /> -
- - -
@@ -423,13 +371,11 @@ export function AgentMonitor() {