Add files via upload

This commit is contained in:
公明
2026-07-23 19:07:27 +08:00
committed by GitHub
parent 4c55784dbc
commit cee46f40fa
6 changed files with 267 additions and 37 deletions
+14
View File
@@ -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/streamdeep | 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
+85 -2
View File
@@ -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 {
+80 -1
View File
@@ -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")
+18 -4
View File
@@ -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,
+15 -4
View File
@@ -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,
+55 -26
View File
@@ -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 {