From cee46f40fa0eb8ac32f2d89dc232ab530cd18f96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=85=AC=E6=98=8E?= <83812544+Ed1s0nZ@users.noreply.github.com> Date: Thu, 23 Jul 2026 19:07:27 +0800 Subject: [PATCH] Add files via upload --- internal/handler/agent.go | 14 +++++ internal/handler/c2.go | 87 ++++++++++++++++++++++++++- internal/handler/config.go | 81 ++++++++++++++++++++++++- internal/handler/eino_single_agent.go | 22 +++++-- internal/handler/multi_agent.go | 19 ++++-- internal/handler/webshell.go | 81 +++++++++++++++++-------- 6 files changed, 267 insertions(+), 37 deletions(-) diff --git a/internal/handler/agent.go b/internal/handler/agent.go index 779aa1d0..6d16ef2b 100644 --- a/internal/handler/agent.go +++ b/internal/handler/agent.go @@ -341,12 +341,26 @@ type ChatRequest struct { Role string `json:"role,omitempty"` // 角色名称 Attachments []ChatAttachment `json:"attachments,omitempty"` WebShellConnectionID string `json:"webshellConnectionId,omitempty"` // WebShell 管理 - AI 助手:当前选中的连接 ID,仅使用 webshell_* 工具 + AIChannelID string `json:"aiChannelId,omitempty"` // 会话级 AI 通道;空则使用 ai.default_channel Hitl *HITLRequest `json:"hitl,omitempty"` Reasoning *ChatReasoningRequest `json:"reasoning,omitempty"` // Orchestration 仅对 /api/multi-agent、/api/multi-agent/stream:deep | plan_execute | supervisor;空则等同 deep。机器人/批量等无请求体时由服务端默认 deep。/api/eino-agent* 不使用此字段。 Orchestration string `json:"orchestration,omitempty"` } +func (h *AgentHandler) configForAIChannel(channelID string) (*config.Config, string, error) { + if h == nil || h.config == nil { + return nil, "", fmt.Errorf("服务器配置未加载") + } + oa, resolvedID, ok := h.config.ResolveAIChannel(channelID) + if !ok { + return nil, resolvedID, fmt.Errorf("AI 通道不存在: %s", resolvedID) + } + cfgCopy := *h.config + cfgCopy.OpenAI = oa + return &cfgCopy, resolvedID, nil +} + func chatReasoningToClientIntent(r *ChatReasoningRequest) *reasoning.ClientIntent { if r == nil { return nil diff --git a/internal/handler/c2.go b/internal/handler/c2.go index 9e156eca..aa7d7fb9 100644 --- a/internal/handler/c2.go +++ b/internal/handler/c2.go @@ -60,7 +60,7 @@ func (h *C2Handler) SetManager(m *c2.Manager) { // ListListeners 获取监听器列表 func (h *C2Handler) ListListeners(c *gin.Context) { - listeners, err := h.mgr().DB().ListC2ListenersForAccess(c2AccessFromContext(c)) + listeners, err := h.mgr().DB().ListC2ListenersForAccess(c2AccessFromContext(c), c.Query("project_id")) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return @@ -77,6 +77,7 @@ func (h *C2Handler) ListListeners(c *gin.Context) { func (h *C2Handler) CreateListener(c *gin.Context) { var req struct { Name string `json:"name"` + ProjectID string `json:"project_id,omitempty"` Type string `json:"type"` BindHost string `json:"bind_host"` BindPort int `json:"bind_port"` @@ -92,6 +93,7 @@ func (h *C2Handler) CreateListener(c *gin.Context) { input := c2.CreateListenerInput{ Name: req.Name, + ProjectID: req.ProjectID, Type: req.Type, BindHost: req.BindHost, BindPort: req.BindPort, @@ -100,6 +102,10 @@ func (h *C2Handler) CreateListener(c *gin.Context) { Config: req.Config, CallbackHost: strings.TrimSpace(req.CallbackHost), } + if !h.canAccessProject(c, input.ProjectID) { + c.JSON(http.StatusForbidden, gin.H{"error": "project access denied"}) + return + } listener, err := h.mgr().CreateListener(input) if err != nil { @@ -158,6 +164,7 @@ func (h *C2Handler) UpdateListener(c *gin.Context) { var req struct { Name string `json:"name"` + ProjectID string `json:"project_id"` BindHost string `json:"bind_host"` BindPort int `json:"bind_port"` ProfileID string `json:"profile_id"` @@ -179,6 +186,7 @@ func (h *C2Handler) UpdateListener(c *gin.Context) { } listener.Name = req.Name + listener.ProjectID = strings.TrimSpace(req.ProjectID) listener.BindHost = req.BindHost listener.BindPort = req.BindPort listener.ProfileID = req.ProfileID @@ -187,6 +195,10 @@ func (h *C2Handler) UpdateListener(c *gin.Context) { cfgJSON, _ := json.Marshal(req.Config) listener.ConfigJSON = string(cfgJSON) } + if !h.canAccessProject(c, listener.ProjectID) { + c.JSON(http.StatusForbidden, gin.H{"error": "project access denied"}) + return + } if req.CallbackHost != nil { cfg := &c2.ListenerConfig{} raw := strings.TrimSpace(listener.ConfigJSON) @@ -275,6 +287,7 @@ func (h *C2Handler) StopListener(c *gin.Context) { func (h *C2Handler) ListSessions(c *gin.Context) { filter := database.ListC2SessionsFilter{ ListenerID: c.Query("listener_id"), + ProjectID: c.Query("project_id"), Status: c.Query("status"), OS: c.Query("os"), Search: c.Query("search"), @@ -404,6 +417,47 @@ func (h *C2Handler) SetSessionSleep(c *gin.Context) { c.JSON(http.StatusOK, out) } +// SetSessionNote 更新会话备注(仅服务端元数据,不下发植入体) +func (h *C2Handler) SetSessionNote(c *gin.Context) { + id := c.Param("id") + var req struct { + Note string `json:"note"` + } + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + note := strings.TrimSpace(req.Note) + if len(note) > 2000 { + c.JSON(http.StatusBadRequest, gin.H{"error": "note too long (max 2000 characters)"}) + return + } + + session, err := h.mgr().DB().GetC2Session(id) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + if session == nil { + c.JSON(http.StatusNotFound, gin.H{"error": "session not found"}) + return + } + + if err := h.mgr().DB().SetC2SessionNote(id, note); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + if h.audit != nil { + h.audit.RecordOK(c, "c2", "session_note", "更新 C2 会话备注", "c2_session", id, map[string]interface{}{ + "note_len": len(note), + }) + } + c.JSON(http.StatusOK, gin.H{ + "updated": true, + "note": note, + }) +} + // ============================================================================ // 任务 API // ============================================================================ @@ -412,7 +466,14 @@ func (h *C2Handler) SetSessionSleep(c *gin.Context) { func (h *C2Handler) ListTasks(c *gin.Context) { filter := database.ListC2TasksFilter{ SessionID: c.Query("session_id"), + ProjectID: c.Query("project_id"), Status: c.Query("status"), + TaskType: c.Query("task_type"), + } + if since := c.Query("since"); since != "" { + if t, err := database.ParseRFC3339Time(since); err == nil { + filter.Since = &t + } } paginated := false @@ -447,7 +508,7 @@ func (h *C2Handler) ListTasks(c *gin.Context) { } // 仪表盘「待审任务」为全局 queued/pending 数量,与列表 session 过滤无关 - pendingN, _ := h.mgr().DB().CountC2TasksQueuedOrPendingForAccess("", access) + pendingN, _ := h.mgr().DB().CountC2TasksQueuedOrPendingForAccess("", filter.ProjectID, access) if !paginated { c.JSON(http.StatusOK, gin.H{ @@ -462,9 +523,15 @@ func (h *C2Handler) ListTasks(c *gin.Context) { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } + statusCounts, err := h.mgr().DB().CountC2TasksByStatusForAccess(filter, access) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } c.JSON(http.StatusOK, gin.H{ "tasks": tasks, "total": total, + "status_counts": statusCounts, "page": page, "page_size": pageSize, "pending_queued_count": pendingN, @@ -784,6 +851,7 @@ func (h *C2Handler) ListEvents(c *gin.Context) { filter := database.ListC2EventsFilter{ Level: c.Query("level"), Category: c.Query("category"), + ProjectID: c.Query("project_id"), SessionID: c.Query("session_id"), TaskID: c.Query("task_id"), } @@ -1121,6 +1189,21 @@ func c2AccessFromContext(c *gin.Context) database.RBACListAccess { return database.RBACListAccess{UserID: session.UserID, Scope: session.Scope} } +func (h *C2Handler) canAccessProject(c *gin.Context, projectID string) bool { + projectID = strings.TrimSpace(projectID) + if projectID == "" { + return true + } + session, ok := security.CurrentSession(c) + if !ok { + return false + } + if session.Scope == database.RBACScopeAll { + return true + } + return h.mgr().DB().UserCanAccessResource(session.UserID, session.Scope, "project", projectID) +} + func (h *C2Handler) c2ResourceAllowed(c *gin.Context, resourceType, resourceID string) bool { session, ok := security.CurrentSession(c) if !ok { diff --git a/internal/handler/config.go b/internal/handler/config.go index 566038f2..e3c26e5c 100644 --- a/internal/handler/config.go +++ b/internal/handler/config.go @@ -258,6 +258,7 @@ func (h *ConfigHandler) ApplyWechatRobotBinding(wc config.RobotWechatConfig) err // GetConfigResponse 获取配置响应 type GetConfigResponse struct { + AI config.AIConfig `json:"ai"` OpenAI config.OpenAIConfig `json:"openai"` Vision config.VisionConfig `json:"vision"` FOFA config.FofaConfig `json:"fofa"` @@ -363,6 +364,7 @@ func (h *ConfigHandler) GetConfig(c *gin.Context) { } c.JSON(http.StatusOK, GetConfigResponse{ + AI: h.config.AI, OpenAI: h.config.OpenAI, Vision: h.config.Vision, FOFA: h.config.FOFA, @@ -706,6 +708,7 @@ func (h *ConfigHandler) GetTools(c *gin.Context) { // UpdateConfigRequest 更新配置请求 type UpdateConfigRequest struct { + AI *config.AIConfig `json:"ai,omitempty"` OpenAI *config.OpenAIConfig `json:"openai,omitempty"` Vision *config.VisionConfig `json:"vision,omitempty"` FOFA *config.FofaConfig `json:"fofa,omitempty"` @@ -785,8 +788,20 @@ func (h *ConfigHandler) UpdateConfig(c *gin.Context) { defer h.mu.Unlock() // 更新OpenAI配置 + if req.AI != nil { + h.config.AI = *req.AI + h.config.ApplyDefaultAIChannel() + h.logger.Info("更新 AI 通道配置", + zap.String("default_channel", h.config.AI.DefaultChannel), + zap.Int("channels", len(h.config.AI.Channels)), + ) + } if req.OpenAI != nil { h.config.OpenAI = *req.OpenAI + h.config.AI.EnsureDefaultFromOpenAI(h.config.OpenAI) + if def := config.NormalizeAIChannelID(h.config.AI.DefaultChannel); def != "" { + h.config.AI.Channels[def] = config.AIChannelFromOpenAI(def, "Default", h.config.OpenAI) + } h.logger.Info("更新OpenAI配置", zap.String("base_url", h.config.OpenAI.BaseURL), zap.String("model", h.config.OpenAI.Model), @@ -1683,7 +1698,8 @@ func (h *ConfigHandler) saveConfig() error { updateAgentConfig(root, h.config.Agent) updateMCPConfig(root, h.config.MCP) - updateOpenAIConfig(root, h.config.OpenAI) + updateAIConfig(root, h.config.AI) + removeKeyFromMap(root.Content[0], "openai") updateVisionConfig(root, h.config.Vision) updateFOFAConfig(root, h.config.FOFA) updateSpaceSearchConfig(root, "zoomeye", h.config.ZoomEye) @@ -1877,6 +1893,69 @@ func updateOpenAIConfig(doc *yaml.Node, cfg config.OpenAIConfig) { } } +func updateAIConfig(doc *yaml.Node, cfg config.AIConfig) { + root := doc.Content[0] + aiNode := ensureMap(root, "ai") + if strings.TrimSpace(cfg.DefaultChannel) != "" { + setStringInMap(aiNode, "default_channel", config.NormalizeAIChannelID(cfg.DefaultChannel)) + } + channelsNode := ensureMap(aiNode, "channels") + channelsNode.Content = nil + normalized := make(map[string]config.AIChannelConfig, len(cfg.Channels)) + ids := make([]string, 0, len(cfg.Channels)) + for id, ch := range cfg.Channels { + nid := config.NormalizeAIChannelID(id) + if nid == "" { + continue + } + if _, exists := normalized[nid]; !exists { + ids = append(ids, nid) + } + normalized[nid] = ch + } + sort.Strings(ids) + seen := make(map[string]bool, len(ids)) + for _, id := range ids { + if seen[id] { + continue + } + seen[id] = true + ch := normalized[id] + keyNode := &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: id} + channelNode := &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"} + channelsNode.Content = append(channelsNode.Content, keyNode, channelNode) + setStringInMap(channelNode, "name", ch.Name) + if strings.TrimSpace(ch.Provider) != "" { + setStringInMap(channelNode, "provider", ch.Provider) + } + setStringInMap(channelNode, "api_key", ch.APIKey) + setStringInMap(channelNode, "base_url", ch.BaseURL) + setStringInMap(channelNode, "model", ch.Model) + if ch.MaxTotalTokens > 0 { + setIntInMap(channelNode, "max_total_tokens", ch.MaxTotalTokens) + } + if ch.MaxCompletionTokens > 0 { + setIntInMap(channelNode, "max_completion_tokens", ch.MaxCompletionTokens) + } + rn := ensureMap(channelNode, "reasoning") + if strings.TrimSpace(ch.Reasoning.Mode) != "" { + setStringInMap(rn, "mode", ch.Reasoning.Mode) + } + if strings.TrimSpace(ch.Reasoning.Effort) != "" { + setStringInMap(rn, "effort", ch.Reasoning.Effort) + } + if ch.Reasoning.AllowClientReasoning != nil { + setBoolInMap(rn, "allow_client_reasoning", *ch.Reasoning.AllowClientReasoning) + } + if strings.TrimSpace(ch.Reasoning.Profile) != "" { + setStringInMap(rn, "profile", ch.Reasoning.Profile) + } + if len(rn.Content) == 0 { + removeKeyFromMap(channelNode, "reasoning") + } + } +} + func updateFOFAConfig(doc *yaml.Node, cfg config.FofaConfig) { root := doc.Content[0] fofaNode := ensureMap(root, "fofa") diff --git a/internal/handler/eino_single_agent.go b/internal/handler/eino_single_agent.go index 074e6cbd..762861dd 100644 --- a/internal/handler/eino_single_agent.go +++ b/internal/handler/eino_single_agent.go @@ -149,6 +149,14 @@ func (h *AgentHandler) EinoSingleAgentLoopStream(c *gin.Context) { sendEvent("done", "", map[string]interface{}{"conversationId": conversationID}) return } + runCfg, resolvedAIChannelID, err := h.configForAIChannel(req.AIChannelID) + if err != nil { + taskStatus = "failed" + h.tasks.UpdateTaskStatus(conversationID, taskStatus) + sendEvent("error", err.Error(), nil) + sendEvent("done", "", map[string]interface{}{"conversationId": conversationID}) + return + } var result *multiagent.RunResult var runErr error @@ -222,8 +230,8 @@ func (h *AgentHandler) EinoSingleAgentLoopStream(c *gin.Context) { result, runErr = multiagent.RunEinoSingleChatModelAgent( taskCtxLoop, - h.config, - &h.config.MultiAgent, + runCfg, + &runCfg.MultiAgent, h.agent, h.db, h.logger, @@ -236,6 +244,7 @@ func (h *AgentHandler) EinoSingleAgentLoopStream(c *gin.Context) { chatReasoningToClientIntent(req.Reasoning), h.agentSessionContextBlock(conversationID), ) + _ = resolvedAIChannelID if result != nil && len(result.MCPExecutionIDs) > 0 { cumulativeMCPExecutionIDs = mergeMCPExecutionIDLists(cumulativeMCPExecutionIDs, result.MCPExecutionIDs) @@ -410,6 +419,11 @@ func (h *AgentHandler) EinoSingleAgentLoop(c *gin.Context) { c.JSON(http.StatusInternalServerError, gin.H{"error": "服务器配置未加载"}) return } + runCfg, _, err := h.configForAIChannel(req.AIChannelID) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } curHist := prep.History curMsg := prep.FinalMessage @@ -418,8 +432,8 @@ func (h *AgentHandler) EinoSingleAgentLoop(c *gin.Context) { for { result, runErr = multiagent.RunEinoSingleChatModelAgent( taskCtx, - h.config, - &h.config.MultiAgent, + runCfg, + &runCfg.MultiAgent, h.agent, h.db, h.logger, diff --git a/internal/handler/multi_agent.go b/internal/handler/multi_agent.go index 03e6b4a8..f2a3a22d 100644 --- a/internal/handler/multi_agent.go +++ b/internal/handler/multi_agent.go @@ -158,6 +158,12 @@ func (h *AgentHandler) MultiAgentLoopStream(c *gin.Context) { stopKeepalive := runSSEKeepalive(c, &sseWriteMu) defer stopKeepalive() + runCfg, _, err := h.configForAIChannel(req.AIChannelID) + if err != nil { + sendEvent("error", err.Error(), nil) + sendEvent("done", "", map[string]interface{}{"conversationId": conversationID}) + return + } var result *multiagent.RunResult var runErr error @@ -232,8 +238,8 @@ func (h *AgentHandler) MultiAgentLoopStream(c *gin.Context) { result, runErr = multiagent.RunDeepAgent( taskCtxLoop, - h.config, - &h.config.MultiAgent, + runCfg, + &runCfg.MultiAgent, h.agent, h.db, h.logger, @@ -421,6 +427,11 @@ func (h *AgentHandler) MultiAgentLoop(c *gin.Context) { taskCtx = multiagent.WithHITLToolInterceptor(taskCtx, func(ctx context.Context, toolName, arguments string) (string, error) { return h.interceptHITLForEinoTool(ctx, cancelWithCause, prep.ConversationID, prep.AssistantMessageID, nil, toolName, arguments) }) + runCfg, _, err := h.configForAIChannel(req.AIChannelID) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } curHist := prep.History curMsg := prep.FinalMessage @@ -429,8 +440,8 @@ func (h *AgentHandler) MultiAgentLoop(c *gin.Context) { for { result, runErr = multiagent.RunDeepAgent( taskCtx, - h.config, - &h.config.MultiAgent, + runCfg, + &runCfg.MultiAgent, h.agent, h.db, h.logger, diff --git a/internal/handler/webshell.go b/internal/handler/webshell.go index d6d828df..90eb3ca8 100644 --- a/internal/handler/webshell.go +++ b/internal/handler/webshell.go @@ -352,26 +352,28 @@ func NewWebShellHandler(logger *zap.Logger, db *database.DB) *WebShellHandler { // CreateConnectionRequest 创建连接请求 type CreateConnectionRequest struct { - URL string `json:"url" binding:"required"` - Password string `json:"password"` - Type string `json:"type"` - Method string `json:"method"` - CmdParam string `json:"cmd_param"` - Remark string `json:"remark"` - Encoding string `json:"encoding"` - OS string `json:"os"` + ProjectID string `json:"project_id"` + URL string `json:"url" binding:"required"` + Password string `json:"password"` + Type string `json:"type"` + Method string `json:"method"` + CmdParam string `json:"cmd_param"` + Remark string `json:"remark"` + Encoding string `json:"encoding"` + OS string `json:"os"` } // UpdateConnectionRequest 更新连接请求 type UpdateConnectionRequest struct { - URL string `json:"url" binding:"required"` - Password string `json:"password"` - Type string `json:"type"` - Method string `json:"method"` - CmdParam string `json:"cmd_param"` - Remark string `json:"remark"` - Encoding string `json:"encoding"` - OS string `json:"os"` + ProjectID string `json:"project_id"` + URL string `json:"url" binding:"required"` + Password string `json:"password"` + Type string `json:"type"` + Method string `json:"method"` + CmdParam string `json:"cmd_param"` + Remark string `json:"remark"` + Encoding string `json:"encoding"` + OS string `json:"os"` } // ListConnections 列出所有 WebShell 连接(GET /api/webshell/connections) @@ -381,7 +383,7 @@ func (h *WebShellHandler) ListConnections(c *gin.Context) { return } session, _ := security.CurrentSession(c) - list, err := h.db.ListWebshellConnectionsForAccess(session.UserID, session.Scope) + list, err := h.db.ListWebshellConnectionsForAccess(session.UserID, session.Scope, c.Query("project_id")) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return @@ -412,6 +414,11 @@ func (h *WebShellHandler) CreateConnection(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid url"}) return } + projectID := strings.TrimSpace(req.ProjectID) + if !h.canAccessProject(c, projectID) { + c.JSON(http.StatusForbidden, gin.H{"error": "project access denied"}) + return + } method := strings.ToLower(strings.TrimSpace(req.Method)) if method != "get" && method != "post" { method = "post" @@ -422,6 +429,7 @@ func (h *WebShellHandler) CreateConnection(c *gin.Context) { } conn := &database.WebShellConnection{ ID: "ws_" + strings.ReplaceAll(uuid.New().String(), "-", "")[:12], + ProjectID: projectID, URL: req.URL, Password: strings.TrimSpace(req.Password), Type: shellType, @@ -477,6 +485,11 @@ func (h *WebShellHandler) UpdateConnection(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid url"}) return } + projectID := strings.TrimSpace(req.ProjectID) + if !h.canAccessProject(c, projectID) { + c.JSON(http.StatusForbidden, gin.H{"error": "project access denied"}) + return + } method := strings.ToLower(strings.TrimSpace(req.Method)) if method != "get" && method != "post" { method = "post" @@ -486,15 +499,16 @@ func (h *WebShellHandler) UpdateConnection(c *gin.Context) { shellType = "php" } conn := &database.WebShellConnection{ - ID: id, - URL: req.URL, - Password: strings.TrimSpace(req.Password), - Type: shellType, - Method: method, - CmdParam: strings.TrimSpace(req.CmdParam), - Remark: strings.TrimSpace(req.Remark), - Encoding: normalizeWebshellEncoding(req.Encoding), - OS: normalizeWebshellOS(req.OS), + ID: id, + ProjectID: projectID, + URL: req.URL, + Password: strings.TrimSpace(req.Password), + Type: shellType, + Method: method, + CmdParam: strings.TrimSpace(req.CmdParam), + Remark: strings.TrimSpace(req.Remark), + Encoding: normalizeWebshellEncoding(req.Encoding), + OS: normalizeWebshellOS(req.OS), } if err := h.db.UpdateWebshellConnection(conn); err != nil { if err == sql.ErrNoRows { @@ -940,6 +954,21 @@ func (h *WebShellHandler) authorizedWebshellConnection(c *gin.Context, connectio return conn, true } +func (h *WebShellHandler) canAccessProject(c *gin.Context, projectID string) bool { + projectID = strings.TrimSpace(projectID) + if projectID == "" || h.db == nil { + return true + } + session, ok := security.CurrentSession(c) + if !ok { + return false + } + if session.Scope == database.RBACScopeAll { + return true + } + return h.db.UserCanAccessResource(session.UserID, session.Scope, "project", projectID) +} + // ExecWithConnection 在指定 WebShell 连接上执行命令(供 MCP/Agent 等非 HTTP 调用) func (h *WebShellHandler) ExecWithConnection(conn *database.WebShellConnection, command string) (output string, ok bool, errMsg string) { if conn == nil {