fix: 远程命令直连SSH(去轮询) + 去掉无用命令分类
This commit is contained in:
1 parent
ed3bf59c3b
commit
b8b1a3d5f6
4 files changed
+70
-68
No files matched your search
@@ -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)})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -142,6 +142,9 @@ func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config, c
|
|||||||
agentInstallHandler := handlers.NewAgentInstallHandler(db)
|
agentInstallHandler := handlers.NewAgentInstallHandler(db)
|
||||||
servers.POST("/:id/install-agent", agentInstallHandler.InstallAgent)
|
servers.POST("/:id/install-agent", agentInstallHandler.InstallAgent)
|
||||||
servers.POST("/install-agent", agentInstallHandler.BatchInstallAgent)
|
servers.POST("/install-agent", agentInstallHandler.BatchInstallAgent)
|
||||||
|
|
||||||
|
// Agent 远程命令执行
|
||||||
|
agentGroup.POST("/exec", agentInstallHandler.RemoteExec)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 云账号路由
|
// 云账号路由
|
||||||
|
|||||||
@@ -55,13 +55,10 @@ export function AgentMonitor() {
|
|||||||
// 远程命令
|
// 远程命令
|
||||||
const [cmdOpen, setCmdOpen] = useState(false);
|
const [cmdOpen, setCmdOpen] = useState(false);
|
||||||
const [cmdHost, setCmdHost] = useState('');
|
const [cmdHost, setCmdHost] = useState('');
|
||||||
const [cmdType, setCmdType] = useState('command');
|
|
||||||
const [cmdCommand, setCmdCommand] = useState('');
|
const [cmdCommand, setCmdCommand] = useState('');
|
||||||
const [cmdTimeout, setCmdTimeout] = useState('30');
|
const [cmdTimeout, setCmdTimeout] = useState(30);
|
||||||
const [cmdSubmitting, setCmdSubmitting] = useState(false);
|
const [cmdSubmitting, setCmdSubmitting] = useState(false);
|
||||||
const [cmdResult, setCmdResult] = useState<any>(null);
|
const [cmdResult, setCmdResult] = useState<any>(null);
|
||||||
const [cmdPolling, setCmdPolling] = useState(false);
|
|
||||||
const pollTimerRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
||||||
|
|
||||||
const loadLatest = useCallback(async (silent = false) => {
|
const loadLatest = useCallback(async (silent = false) => {
|
||||||
if (!silent) setRefreshing(true);
|
if (!silent) setRefreshing(true);
|
||||||
@@ -95,14 +92,13 @@ export function AgentMonitor() {
|
|||||||
// 打开远程命令弹窗
|
// 打开远程命令弹窗
|
||||||
const openCommandDialog = (hostname: string) => {
|
const openCommandDialog = (hostname: string) => {
|
||||||
setCmdHost(hostname);
|
setCmdHost(hostname);
|
||||||
setCmdType('command');
|
|
||||||
setCmdCommand('');
|
setCmdCommand('');
|
||||||
setCmdTimeout('30');
|
setCmdTimeout(30);
|
||||||
setCmdResult(null);
|
setCmdResult(null);
|
||||||
setCmdOpen(true);
|
setCmdOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 提交远程命令并轮询结果
|
// 提交远程命令(直连 SSH 即时返回)
|
||||||
const submitCommand = async () => {
|
const submitCommand = async () => {
|
||||||
if (!cmdCommand.trim()) {
|
if (!cmdCommand.trim()) {
|
||||||
toast.error('请输入要执行的命令');
|
toast.error('请输入要执行的命令');
|
||||||
@@ -111,62 +107,26 @@ export function AgentMonitor() {
|
|||||||
setCmdSubmitting(true);
|
setCmdSubmitting(true);
|
||||||
setCmdResult(null);
|
setCmdResult(null);
|
||||||
try {
|
try {
|
||||||
const res: any = await agentApi.createTask({
|
const res: any = await agentApi.execCommand({
|
||||||
hostname: cmdHost,
|
hostname: cmdHost,
|
||||||
type: cmdType,
|
|
||||||
command: cmdCommand.trim(),
|
command: cmdCommand.trim(),
|
||||||
timeout: Number(cmdTimeout) || 30,
|
timeout: Number(cmdTimeout) || 30,
|
||||||
});
|
});
|
||||||
const taskId = res?.id ?? res?.task_id;
|
setCmdResult({
|
||||||
toast.success('任务已下发');
|
status: res?.code === 200 ? 'completed' : 'failed',
|
||||||
setCmdSubmitting(false);
|
result: res?.data?.output || res?.message || '无输出',
|
||||||
|
error: res?.data?.error,
|
||||||
// 轮询任务结果
|
});
|
||||||
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);
|
|
||||||
}
|
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
toast.error(err?.message || '任务下发失败');
|
setCmdResult({ status: 'failed', result: err?.message || '执行失败' });
|
||||||
|
} finally {
|
||||||
setCmdSubmitting(false);
|
setCmdSubmitting(false);
|
||||||
|
loadLatest(true);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const closeCommandDialog = () => {
|
const closeCommandDialog = () => {
|
||||||
setCmdOpen(false);
|
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;
|
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"
|
className="h-9 bg-slate-800 border-slate-700 text-slate-200"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1.5">
|
|
||||||
<Label className="text-slate-300 text-xs">类型</Label>
|
|
||||||
<Select value={cmdType} onValueChange={setCmdType}>
|
|
||||||
<SelectTrigger className="h-9 bg-slate-800 border-slate-700 text-slate-200">
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent className="bg-slate-800 border-slate-700 text-slate-200">
|
|
||||||
<SelectItem value="command">Shell 命令</SelectItem>
|
|
||||||
<SelectItem value="script">多行脚本</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
@@ -423,13 +371,11 @@ export function AgentMonitor() {
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={submitCommand}
|
onClick={submitCommand}
|
||||||
disabled={cmdSubmitting || cmdPolling || !cmdCommand.trim()}
|
disabled={cmdSubmitting || !cmdCommand.trim()}
|
||||||
className="bg-blue-600 hover:bg-blue-700 text-white"
|
className="bg-blue-600 hover:bg-blue-700 text-white"
|
||||||
>
|
>
|
||||||
{cmdSubmitting ? (
|
{cmdSubmitting ? (
|
||||||
<><Loader2 className="h-4 w-4 mr-1.5 animate-spin" />下发中</>
|
<><Loader2 className="h-4 w-4 mr-1.5 animate-spin" />执行中</>
|
||||||
) : cmdPolling ? (
|
|
||||||
<><Loader2 className="h-4 w-4 mr-1.5 animate-spin" />等待结果</>
|
|
||||||
) : (
|
) : (
|
||||||
<><Terminal className="h-4 w-4 mr-1.5" />执行</>
|
<><Terminal className="h-4 w-4 mr-1.5" />执行</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -489,6 +489,8 @@ export const agentApi = {
|
|||||||
getHistory: (hostname: string, hours = 1) => http.get(`/agent/history/${hostname}?hours=${hours}`),
|
getHistory: (hostname: string, hours = 1) => http.get(`/agent/history/${hostname}?hours=${hours}`),
|
||||||
createTask: (data: { hostname: string; type: string; command: string; timeout?: number }) => http.post('/agent/tasks', data),
|
createTask: (data: { hostname: string; type: string; command: string; timeout?: number }) => http.post('/agent/tasks', data),
|
||||||
listTasks: (params?: Record<string, string>) => http.get('/agent/tasks', params),
|
listTasks: (params?: Record<string, string>) => http.get('/agent/tasks', params),
|
||||||
|
// 直连 SSH 执行命令(即时返回)
|
||||||
|
execCommand: (data: { hostname: string; command: string; timeout?: number }) => http.post('/agent/exec', data),
|
||||||
listRules: () => http.get('/agent/rules'),
|
listRules: () => http.get('/agent/rules'),
|
||||||
createRule: (data: any) => http.post('/agent/rules', data),
|
createRule: (data: any) => http.post('/agent/rules', data),
|
||||||
updateRule: (id: number, data: any) => http.put(`/agent/rules/${id}`, data),
|
updateRule: (id: number, data: any) => http.put(`/agent/rules/${id}`, data),
|
||||||
|
|||||||
Reference in new issue
Block a user