Add files via upload

This commit is contained in:
公明
2026-03-14 00:49:25 +08:00
committed by GitHub
parent 226f9b79e2
commit 5e8fef0ad4
11 changed files with 1147 additions and 25 deletions
+162
View File
@@ -320,6 +320,7 @@ func New(cfg *config.Config, log *logger.Logger) (*App, error) {
attackChainHandler := handler.NewAttackChainHandler(db, &cfg.OpenAI, log.Logger)
vulnerabilityHandler := handler.NewVulnerabilityHandler(db, log.Logger)
webshellHandler := handler.NewWebShellHandler(log.Logger, db)
registerWebshellTools(mcpServer, db, webshellHandler, log.Logger)
configHandler := handler.NewConfigHandler(configPath, cfg, mcpServer, executor, agent, attackChainHandler, externalMCPMgr, log.Logger)
externalMCPHandler := handler.NewExternalMCPHandler(externalMCPMgr, cfg, configPath, log.Logger)
roleHandler := handler.NewRoleHandler(cfg, configPath, log.Logger)
@@ -365,6 +366,13 @@ func New(cfg *config.Config, log *logger.Logger) (*App, error) {
}
configHandler.SetVulnerabilityToolRegistrar(vulnerabilityRegistrar)
// 设置 WebShell 工具注册器(ApplyConfig 时重新注册)
webshellRegistrar := func() error {
registerWebshellTools(mcpServer, db, webshellHandler, log.Logger)
return nil
}
configHandler.SetWebshellToolRegistrar(webshellRegistrar)
// 设置Skills工具注册器(内置工具,必须设置)
skillsRegistrar := func() error {
// 创建一个适配器,将database.DB适配为SkillStatsStorage接口
@@ -823,6 +831,8 @@ func setupRoutes(
// WebShell 管理(代理执行 + 连接配置存 SQLite)
protected.GET("/webshell/connections", webshellHandler.ListConnections)
protected.POST("/webshell/connections", webshellHandler.CreateConnection)
protected.GET("/webshell/connections/:id/ai-history", webshellHandler.GetAIHistory)
protected.GET("/webshell/connections/:id/ai-conversations", webshellHandler.ListAIConversations)
protected.PUT("/webshell/connections/:id", webshellHandler.UpdateConnection)
protected.DELETE("/webshell/connections/:id", webshellHandler.DeleteConnection)
protected.POST("/webshell/exec", webshellHandler.Exec)
@@ -1067,6 +1077,158 @@ func registerVulnerabilityTool(mcpServer *mcp.Server, db *database.DB, logger *z
logger.Info("漏洞记录工具注册成功")
}
// registerWebshellTools 注册 WebShell 相关 MCP 工具,供 AI 助手在指定连接上执行命令与文件操作
func registerWebshellTools(mcpServer *mcp.Server, db *database.DB, webshellHandler *handler.WebShellHandler, logger *zap.Logger) {
if db == nil || webshellHandler == nil {
logger.Warn("跳过 WebShell 工具注册:db 或 webshellHandler 为空")
return
}
// webshell_exec
execTool := mcp.Tool{
Name: builtin.ToolWebshellExec,
Description: "在指定的 WebShell 连接上执行一条系统命令,返回命令的标准输出。connection_id 由用户在 AI 助手上下文中选定。",
ShortDescription: "在 WebShell 连接上执行命令",
InputSchema: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"connection_id": map[string]interface{}{
"type": "string",
"description": "WebShell 连接 ID(如 ws_xxx",
},
"command": map[string]interface{}{
"type": "string",
"description": "要执行的系统命令",
},
},
"required": []string{"connection_id", "command"},
},
}
execHandler := func(ctx context.Context, args map[string]interface{}) (*mcp.ToolResult, error) {
cid, _ := args["connection_id"].(string)
cmd, _ := args["command"].(string)
if cid == "" || cmd == "" {
return &mcp.ToolResult{Content: []mcp.Content{{Type: "text", Text: "connection_id 和 command 均为必填"}}, IsError: true}, nil
}
conn, err := db.GetWebshellConnection(cid)
if err != nil || conn == nil {
return &mcp.ToolResult{Content: []mcp.Content{{Type: "text", Text: "未找到该 WebShell 连接或查询失败"}}, IsError: true}, nil
}
output, ok, errMsg := webshellHandler.ExecWithConnection(conn, cmd)
if errMsg != "" {
return &mcp.ToolResult{Content: []mcp.Content{{Type: "text", Text: errMsg}}, IsError: true}, nil
}
if !ok {
return &mcp.ToolResult{Content: []mcp.Content{{Type: "text", Text: "HTTP 非 200,输出:\n" + output}}, IsError: false}, nil
}
return &mcp.ToolResult{Content: []mcp.Content{{Type: "text", Text: output}}, IsError: false}, nil
}
mcpServer.RegisterTool(execTool, execHandler)
// webshell_file_list
listTool := mcp.Tool{
Name: builtin.ToolWebshellFileList,
Description: "在指定 WebShell 连接上列出目录内容。path 默认为当前目录(.)。",
ShortDescription: "在 WebShell 上列出目录",
InputSchema: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"connection_id": map[string]interface{}{"type": "string", "description": "WebShell 连接 ID"},
"path": map[string]interface{}{"type": "string", "description": "目录路径,默认 ."},
},
"required": []string{"connection_id"},
},
}
listHandler := func(ctx context.Context, args map[string]interface{}) (*mcp.ToolResult, error) {
cid, _ := args["connection_id"].(string)
path, _ := args["path"].(string)
if cid == "" {
return &mcp.ToolResult{Content: []mcp.Content{{Type: "text", Text: "connection_id 必填"}}, IsError: true}, nil
}
conn, err := db.GetWebshellConnection(cid)
if err != nil || conn == nil {
return &mcp.ToolResult{Content: []mcp.Content{{Type: "text", Text: "未找到该 WebShell 连接"}}, IsError: true}, nil
}
output, ok, errMsg := webshellHandler.FileOpWithConnection(conn, "list", path, "", "")
if errMsg != "" {
return &mcp.ToolResult{Content: []mcp.Content{{Type: "text", Text: errMsg}}, IsError: true}, nil
}
return &mcp.ToolResult{Content: []mcp.Content{{Type: "text", Text: output}}, IsError: !ok}, nil
}
mcpServer.RegisterTool(listTool, listHandler)
// webshell_file_read
readTool := mcp.Tool{
Name: builtin.ToolWebshellFileRead,
Description: "在指定 WebShell 连接上读取文件内容。",
ShortDescription: "在 WebShell 上读取文件",
InputSchema: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"connection_id": map[string]interface{}{"type": "string", "description": "WebShell 连接 ID"},
"path": map[string]interface{}{"type": "string", "description": "文件路径"},
},
"required": []string{"connection_id", "path"},
},
}
readHandler := func(ctx context.Context, args map[string]interface{}) (*mcp.ToolResult, error) {
cid, _ := args["connection_id"].(string)
path, _ := args["path"].(string)
if cid == "" || path == "" {
return &mcp.ToolResult{Content: []mcp.Content{{Type: "text", Text: "connection_id 和 path 必填"}}, IsError: true}, nil
}
conn, err := db.GetWebshellConnection(cid)
if err != nil || conn == nil {
return &mcp.ToolResult{Content: []mcp.Content{{Type: "text", Text: "未找到该 WebShell 连接"}}, IsError: true}, nil
}
output, ok, errMsg := webshellHandler.FileOpWithConnection(conn, "read", path, "", "")
if errMsg != "" {
return &mcp.ToolResult{Content: []mcp.Content{{Type: "text", Text: errMsg}}, IsError: true}, nil
}
return &mcp.ToolResult{Content: []mcp.Content{{Type: "text", Text: output}}, IsError: !ok}, nil
}
mcpServer.RegisterTool(readTool, readHandler)
// webshell_file_write
writeTool := mcp.Tool{
Name: builtin.ToolWebshellFileWrite,
Description: "在指定 WebShell 连接上写入文件内容(会覆盖已有文件)。",
ShortDescription: "在 WebShell 上写入文件",
InputSchema: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"connection_id": map[string]interface{}{"type": "string", "description": "WebShell 连接 ID"},
"path": map[string]interface{}{"type": "string", "description": "文件路径"},
"content": map[string]interface{}{"type": "string", "description": "要写入的内容"},
},
"required": []string{"connection_id", "path", "content"},
},
}
writeHandler := func(ctx context.Context, args map[string]interface{}) (*mcp.ToolResult, error) {
cid, _ := args["connection_id"].(string)
path, _ := args["path"].(string)
content, _ := args["content"].(string)
if cid == "" || path == "" {
return &mcp.ToolResult{Content: []mcp.Content{{Type: "text", Text: "connection_id 和 path 必填"}}, IsError: true}, nil
}
conn, err := db.GetWebshellConnection(cid)
if err != nil || conn == nil {
return &mcp.ToolResult{Content: []mcp.Content{{Type: "text", Text: "未找到该 WebShell 连接"}}, IsError: true}, nil
}
output, ok, errMsg := webshellHandler.FileOpWithConnection(conn, "write", path, content, "")
if errMsg != "" {
return &mcp.ToolResult{Content: []mcp.Content{{Type: "text", Text: errMsg}}, IsError: true}, nil
}
if !ok {
return &mcp.ToolResult{Content: []mcp.Content{{Type: "text", Text: "写入可能失败,输出:\n" + output}}, IsError: false}, nil
}
return &mcp.ToolResult{Content: []mcp.Content{{Type: "text", Text: "写入成功\n" + output}}, IsError: false}, nil
}
mcpServer.RegisterTool(writeTool, writeHandler)
logger.Info("WebShell 工具注册成功")
}
// initializeKnowledge 初始化知识库组件(用于动态初始化)
func initializeKnowledge(
cfg *config.Config,
+97 -4
View File
@@ -33,13 +33,26 @@ type Message struct {
// CreateConversation 创建新对话
func (db *DB) CreateConversation(title string) (*Conversation, error) {
return db.CreateConversationWithWebshell("", title)
}
// CreateConversationWithWebshell 创建新对话,可选绑定 WebShell 连接 ID(为空则普通对话)
func (db *DB) CreateConversationWithWebshell(webshellConnectionID, title string) (*Conversation, error) {
id := uuid.New().String()
now := time.Now()
_, err := db.Exec(
"INSERT INTO conversations (id, title, created_at, updated_at) VALUES (?, ?, ?, ?)",
id, title, now, now,
)
var err error
if webshellConnectionID != "" {
_, err = db.Exec(
"INSERT INTO conversations (id, title, created_at, updated_at, webshell_connection_id) VALUES (?, ?, ?, ?, ?)",
id, title, now, now, webshellConnectionID,
)
} else {
_, err = db.Exec(
"INSERT INTO conversations (id, title, created_at, updated_at) VALUES (?, ?, ?, ?)",
id, title, now, now,
)
}
if err != nil {
return nil, fmt.Errorf("创建对话失败: %w", err)
}
@@ -52,6 +65,86 @@ func (db *DB) CreateConversation(title string) (*Conversation, error) {
}, nil
}
// GetConversationByWebshellConnectionID 根据 WebShell 连接 ID 获取该连接下最近一条对话(用于 AI 助手持久化)
func (db *DB) GetConversationByWebshellConnectionID(connectionID string) (*Conversation, error) {
if connectionID == "" {
return nil, fmt.Errorf("connectionID is empty")
}
var conv Conversation
var createdAt, updatedAt string
var pinned int
err := db.QueryRow(
"SELECT id, title, pinned, created_at, updated_at FROM conversations WHERE webshell_connection_id = ? ORDER BY updated_at DESC LIMIT 1",
connectionID,
).Scan(&conv.ID, &conv.Title, &pinned, &createdAt, &updatedAt)
if err != nil {
if err == sql.ErrNoRows {
return nil, nil
}
return nil, fmt.Errorf("查询对话失败: %w", err)
}
conv.Pinned = pinned != 0
if t, e := time.Parse("2006-01-02 15:04:05.999999999-07:00", createdAt); e == nil {
conv.CreatedAt = t
} else if t, e := time.Parse("2006-01-02 15:04:05", createdAt); e == nil {
conv.CreatedAt = t
} else {
conv.CreatedAt, _ = time.Parse(time.RFC3339, createdAt)
}
if t, e := time.Parse("2006-01-02 15:04:05.999999999-07:00", updatedAt); e == nil {
conv.UpdatedAt = t
} else if t, e := time.Parse("2006-01-02 15:04:05", updatedAt); e == nil {
conv.UpdatedAt = t
} else {
conv.UpdatedAt, _ = time.Parse(time.RFC3339, updatedAt)
}
messages, err := db.GetMessages(conv.ID)
if err != nil {
return nil, fmt.Errorf("加载消息失败: %w", err)
}
conv.Messages = messages
return &conv, nil
}
// WebShellConversationItem 用于侧边栏列表,不含消息
type WebShellConversationItem struct {
ID string `json:"id"`
Title string `json:"title"`
UpdatedAt time.Time `json:"updatedAt"`
}
// ListConversationsByWebshellConnectionID 列出该 WebShell 连接下的所有对话(按更新时间倒序),供侧边栏展示
func (db *DB) ListConversationsByWebshellConnectionID(connectionID string) ([]WebShellConversationItem, error) {
if connectionID == "" {
return nil, nil
}
rows, err := db.Query(
"SELECT id, title, updated_at FROM conversations WHERE webshell_connection_id = ? ORDER BY updated_at DESC",
connectionID,
)
if err != nil {
return nil, fmt.Errorf("查询对话列表失败: %w", err)
}
defer rows.Close()
var list []WebShellConversationItem
for rows.Next() {
var item WebShellConversationItem
var updatedAt string
if err := rows.Scan(&item.ID, &item.Title, &updatedAt); err != nil {
continue
}
if t, e := time.Parse("2006-01-02 15:04:05.999999999-07:00", updatedAt); e == nil {
item.UpdatedAt = t
} else if t, e := time.Parse("2006-01-02 15:04:05", updatedAt); e == nil {
item.UpdatedAt = t
} else {
item.UpdatedAt, _ = time.Parse(time.RFC3339, updatedAt)
}
list = append(list, item)
}
return list, rows.Err()
}
// GetConversation 获取对话
func (db *DB) GetConversation(id string) (*Conversation, error) {
var conv Conversation
+15
View File
@@ -415,6 +415,21 @@ func (db *DB) migrateConversationsTable() error {
}
}
// 检查 webshell_connection_id 字段是否存在(WebShell AI 助手对话关联)
err = db.QueryRow("SELECT COUNT(*) FROM pragma_table_info('conversations') WHERE name='webshell_connection_id'").Scan(&count)
if err != nil {
if _, addErr := db.Exec("ALTER TABLE conversations ADD COLUMN webshell_connection_id TEXT"); addErr != nil {
errMsg := strings.ToLower(addErr.Error())
if !strings.Contains(errMsg, "duplicate column") && !strings.Contains(errMsg, "already exists") {
db.logger.Warn("添加webshell_connection_id字段失败", zap.Error(addErr))
}
}
} else if count == 0 {
if _, err := db.Exec("ALTER TABLE conversations ADD COLUMN webshell_connection_id TEXT"); err != nil {
db.logger.Warn("添加webshell_connection_id字段失败", zap.Error(err))
}
}
return nil
}
+49 -19
View File
@@ -121,10 +121,11 @@ type ChatAttachment struct {
// ChatRequest 聊天请求
type ChatRequest struct {
Message string `json:"message" binding:"required"`
ConversationID string `json:"conversationId,omitempty"`
Role string `json:"role,omitempty"` // 角色名称
Attachments []ChatAttachment `json:"attachments,omitempty"`
Message string `json:"message" binding:"required"`
ConversationID string `json:"conversationId,omitempty"`
Role string `json:"role,omitempty"` // 角色名称
Attachments []ChatAttachment `json:"attachments,omitempty"`
WebShellConnectionID string `json:"webshellConnectionId,omitempty"` // WebShell 管理 - AI 助手:当前选中的连接 ID,仅使用 webshell_* 工具
}
const (
@@ -316,7 +317,24 @@ func (h *AgentHandler) AgentLoop(c *gin.Context) {
finalMessage := req.Message
var roleTools []string // 角色配置的工具列表
var roleSkills []string // 角色配置的skills列表(用于提示AI,但不硬编码内容)
if req.Role != "" && req.Role != "默认" {
// WebShell AI 助手模式:绑定当前连接,仅开放 webshell_* 工具并注入 connection_id
if req.WebShellConnectionID != "" {
conn, err := h.db.GetWebshellConnection(strings.TrimSpace(req.WebShellConnectionID))
if err != nil || conn == nil {
h.logger.Warn("WebShell AI 助手:未找到连接", zap.String("id", req.WebShellConnectionID), zap.Error(err))
c.JSON(http.StatusBadRequest, gin.H{"error": "未找到该 WebShell 连接"})
return
}
remark := conn.Remark
if remark == "" {
remark = conn.URL
}
finalMessage = fmt.Sprintf("[WebShell 助手上下文] 当前连接 ID:%s,备注:%s。可用工具(仅在该连接上操作时使用,connection_id 填 \"%s\"):webshell_exec、webshell_file_list、webshell_file_read、webshell_file_write。请根据用户输入决定下一步:若仅为问候、闲聊或简单问题,直接简短回复即可,不必调用工具;当用户明确需要执行命令、列目录、读写字件等操作时再调用上述工具。\n\n用户请求:%s",
conn.ID, remark, conn.ID, req.Message)
roleTools = []string{builtin.ToolWebshellExec, builtin.ToolWebshellFileList, builtin.ToolWebshellFileRead, builtin.ToolWebshellFileWrite}
roleSkills = nil
} else if req.Role != "" && req.Role != "默认" {
if h.config.Roles != nil {
if role, exists := h.config.Roles[req.Role]; exists && role.Enabled {
// 应用用户提示词
@@ -712,11 +730,17 @@ func (h *AgentHandler) AgentLoopStream(c *gin.Context) {
}
}
// 如果没有对话ID,创建新对话
// 如果没有对话ID,创建新对话(WebShell 助手模式下关联连接 ID 以便持久化展示)
conversationID := req.ConversationID
if conversationID == "" {
title := safeTruncateString(req.Message, 50)
conv, err := h.db.CreateConversation(title)
var conv *database.Conversation
var err error
if req.WebShellConnectionID != "" {
conv, err = h.db.CreateConversationWithWebshell(strings.TrimSpace(req.WebShellConnectionID), title)
} else {
conv, err = h.db.CreateConversation(title)
}
if err != nil {
h.logger.Error("创建对话失败", zap.Error(err))
sendEvent("error", "创建对话失败: "+err.Error(), nil)
@@ -769,7 +793,22 @@ func (h *AgentHandler) AgentLoopStream(c *gin.Context) {
// 应用角色用户提示词和工具配置
finalMessage := req.Message
var roleTools []string // 角色配置的工具列表
if req.Role != "" && req.Role != "默认" {
var roleSkills []string
if req.WebShellConnectionID != "" {
conn, errConn := h.db.GetWebshellConnection(strings.TrimSpace(req.WebShellConnectionID))
if errConn != nil || conn == nil {
h.logger.Warn("WebShell AI 助手:未找到连接", zap.String("id", req.WebShellConnectionID), zap.Error(errConn))
sendEvent("error", "未找到该 WebShell 连接", nil)
return
}
remark := conn.Remark
if remark == "" {
remark = conn.URL
}
finalMessage = fmt.Sprintf("[WebShell 助手上下文] 当前连接 ID:%s,备注:%s。可用工具(仅在该连接上操作时使用,connection_id 填 \"%s\"):webshell_exec、webshell_file_list、webshell_file_read、webshell_file_write。请根据用户输入决定下一步:若仅为问候、闲聊或简单问题,直接简短回复即可,不必调用工具;当用户明确需要执行命令、列目录、读写字件等操作时再调用上述工具。\n\n用户请求:%s",
conn.ID, remark, conn.ID, req.Message)
roleTools = []string{builtin.ToolWebshellExec, builtin.ToolWebshellFileList, builtin.ToolWebshellFileRead, builtin.ToolWebshellFileWrite}
} else if req.Role != "" && req.Role != "默认" {
if h.config.Roles != nil {
if role, exists := h.config.Roles[req.Role]; exists && role.Enabled {
// 应用用户提示词
@@ -788,6 +827,7 @@ func (h *AgentHandler) AgentLoopStream(c *gin.Context) {
}
// 注意:角色配置的skills不再硬编码注入,AI可以通过list_skills和read_skill工具按需调用
if len(role.Skills) > 0 {
roleSkills = role.Skills
h.logger.Info("角色配置了skillsAI可通过工具按需调用", zap.String("role", req.Role), zap.Int("skillCount", len(role.Skills)), zap.Strings("skills", role.Skills))
}
}
@@ -886,17 +926,7 @@ func (h *AgentHandler) AgentLoopStream(c *gin.Context) {
// 执行Agent Loop,传入独立的上下文,确保任务不会因客户端断开而中断(使用包含角色提示词的finalMessage和角色工具列表)
sendEvent("progress", "正在分析您的请求...", nil)
// 注意:skills不会硬编码注入,但会在系统提示词中提示AI这个角色推荐使用哪些skills
var roleSkills []string // 角色配置的skills列表(用于提示AI,但不硬编码内容)
if req.Role != "" && req.Role != "默认" {
if h.config.Roles != nil {
if role, exists := h.config.Roles[req.Role]; exists && role.Enabled {
if len(role.Skills) > 0 {
roleSkills = role.Skills
}
}
}
}
// 注意:roleSkills 已在上方根据 req.Role 或 WebShell 模式设置
result, err := h.agent.AgentLoopWithProgress(taskCtx, finalMessage, agentHistoryMessages, conversationID, progressCallback, roleTools, roleSkills)
if err != nil {
h.logger.Error("Agent Loop执行失败", zap.Error(err))
+21
View File
@@ -28,6 +28,9 @@ type KnowledgeToolRegistrar func() error
// VulnerabilityToolRegistrar 漏洞工具注册器接口
type VulnerabilityToolRegistrar func() error
// WebshellToolRegistrar WebShell 工具注册器接口(ApplyConfig 时重新注册)
type WebshellToolRegistrar func() error
// SkillsToolRegistrar Skills工具注册器接口
type SkillsToolRegistrar func() error
@@ -60,6 +63,7 @@ type ConfigHandler struct {
externalMCPMgr *mcp.ExternalMCPManager // 外部MCP管理器
knowledgeToolRegistrar KnowledgeToolRegistrar // 知识库工具注册器(可选)
vulnerabilityToolRegistrar VulnerabilityToolRegistrar // 漏洞工具注册器(可选)
webshellToolRegistrar WebshellToolRegistrar // WebShell 工具注册器(可选)
skillsToolRegistrar SkillsToolRegistrar // Skills工具注册器(可选)
retrieverUpdater RetrieverUpdater // 检索器更新器(可选)
knowledgeInitializer KnowledgeInitializer // 知识库初始化器(可选)
@@ -120,6 +124,13 @@ func (h *ConfigHandler) SetVulnerabilityToolRegistrar(registrar VulnerabilityToo
h.vulnerabilityToolRegistrar = registrar
}
// SetWebshellToolRegistrar 设置 WebShell 工具注册器
func (h *ConfigHandler) SetWebshellToolRegistrar(registrar WebshellToolRegistrar) {
h.mu.Lock()
defer h.mu.Unlock()
h.webshellToolRegistrar = registrar
}
// SetSkillsToolRegistrar 设置Skills工具注册器
func (h *ConfigHandler) SetSkillsToolRegistrar(registrar SkillsToolRegistrar) {
h.mu.Lock()
@@ -792,6 +803,16 @@ func (h *ConfigHandler) ApplyConfig(c *gin.Context) {
}
}
// 重新注册 WebShell 工具(内置工具,必须注册)
if h.webshellToolRegistrar != nil {
h.logger.Info("重新注册 WebShell 工具")
if err := h.webshellToolRegistrar(); err != nil {
h.logger.Error("重新注册 WebShell 工具失败", zap.Error(err))
} else {
h.logger.Info("WebShell 工具已重新注册")
}
}
// 重新注册Skills工具(内置工具,必须注册)
if h.skillsToolRegistrar != nil {
h.logger.Info("重新注册Skills工具")
+152
View File
@@ -197,6 +197,53 @@ func (h *WebShellHandler) DeleteConnection(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"ok": true})
}
// GetAIHistory 获取指定 WebShell 连接的 AI 助手对话历史(GET /api/webshell/connections/:id/ai-history
func (h *WebShellHandler) GetAIHistory(c *gin.Context) {
if h.db == nil {
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "database not available"})
return
}
id := strings.TrimSpace(c.Param("id"))
if id == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "id is required"})
return
}
conv, err := h.db.GetConversationByWebshellConnectionID(id)
if err != nil {
h.logger.Warn("获取 WebShell AI 对话失败", zap.String("connectionId", id), zap.Error(err))
c.JSON(http.StatusOK, gin.H{"conversationId": nil, "messages": []database.Message{}})
return
}
if conv == nil {
c.JSON(http.StatusOK, gin.H{"conversationId": nil, "messages": []database.Message{}})
return
}
c.JSON(http.StatusOK, gin.H{"conversationId": conv.ID, "messages": conv.Messages})
}
// ListAIConversations 列出该 WebShell 连接下的所有 AI 对话(供侧边栏)
func (h *WebShellHandler) ListAIConversations(c *gin.Context) {
if h.db == nil {
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "database not available"})
return
}
id := strings.TrimSpace(c.Param("id"))
if id == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "id is required"})
return
}
list, err := h.db.ListConversationsByWebshellConnectionID(id)
if err != nil {
h.logger.Warn("列出 WebShell AI 对话失败", zap.String("connectionId", id), zap.Error(err))
c.JSON(http.StatusOK, []database.WebShellConversationItem{})
return
}
if list == nil {
list = []database.WebShellConversationItem{}
}
c.JSON(http.StatusOK, list)
}
// ExecRequest 执行命令请求(前端传入连接信息 + 命令)
type ExecRequest struct {
URL string `json:"url" binding:"required"`
@@ -472,3 +519,108 @@ func (h *WebShellHandler) escapeForEcho(s string) string {
// 仅用于 write:base64 写入更安全,这里简单用单引号包裹
return "'" + strings.ReplaceAll(s, "'", "'\"'\"'") + "'"
}
// ExecWithConnection 在指定 WebShell 连接上执行命令(供 MCP/Agent 等非 HTTP 调用)
func (h *WebShellHandler) ExecWithConnection(conn *database.WebShellConnection, command string) (output string, ok bool, errMsg string) {
if conn == nil {
return "", false, "connection is nil"
}
command = strings.TrimSpace(command)
if command == "" {
return "", false, "command is required"
}
useGET := strings.ToUpper(strings.TrimSpace(conn.Method)) == "GET"
cmdParam := strings.TrimSpace(conn.CmdParam)
if cmdParam == "" {
cmdParam = "cmd"
}
var httpReq *http.Request
var err error
if useGET {
targetURL := h.buildExecURL(conn.URL, conn.Type, conn.Password, cmdParam, command)
httpReq, err = http.NewRequest(http.MethodGet, targetURL, nil)
} else {
body := h.buildExecBody(conn.Type, conn.Password, cmdParam, command)
httpReq, err = http.NewRequest(http.MethodPost, conn.URL, bytes.NewReader(body))
httpReq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
}
if err != nil {
return "", false, err.Error()
}
httpReq.Header.Set("User-Agent", "Mozilla/5.0 (compatible; CyberStrikeAI-WebShell/1.0)")
resp, err := h.client.Do(httpReq)
if err != nil {
return "", false, err.Error()
}
defer resp.Body.Close()
out, _ := io.ReadAll(resp.Body)
return string(out), resp.StatusCode == http.StatusOK, ""
}
// FileOpWithConnection 在指定 WebShell 连接上执行文件操作(供 MCP/Agent 调用),支持 list / read / write
func (h *WebShellHandler) FileOpWithConnection(conn *database.WebShellConnection, action, path, content, targetPath string) (output string, ok bool, errMsg string) {
if conn == nil {
return "", false, "connection is nil"
}
action = strings.ToLower(strings.TrimSpace(action))
shellType := strings.ToLower(strings.TrimSpace(conn.Type))
if shellType == "" {
shellType = "php"
}
var command string
switch action {
case "list":
if path == "" {
path = "."
}
if shellType == "asp" || shellType == "aspx" {
command = "dir " + h.escapePath(strings.TrimSpace(path))
} else {
command = "ls -la " + h.escapePath(strings.TrimSpace(path))
}
case "read":
path = strings.TrimSpace(path)
if path == "" {
return "", false, "path is required for read"
}
if shellType == "asp" || shellType == "aspx" {
command = "type " + h.escapePath(path)
} else {
command = "cat " + h.escapePath(path)
}
case "write":
path = strings.TrimSpace(path)
if path == "" {
return "", false, "path is required for write"
}
command = "echo " + h.escapeForEcho(content) + " > " + h.escapePath(path)
default:
return "", false, "unsupported action: " + action + " (supported: list, read, write)"
}
useGET := strings.ToUpper(strings.TrimSpace(conn.Method)) == "GET"
cmdParam := strings.TrimSpace(conn.CmdParam)
if cmdParam == "" {
cmdParam = "cmd"
}
var httpReq *http.Request
var err error
if useGET {
targetURL := h.buildExecURL(conn.URL, conn.Type, conn.Password, cmdParam, command)
httpReq, err = http.NewRequest(http.MethodGet, targetURL, nil)
} else {
body := h.buildExecBody(conn.Type, conn.Password, cmdParam, command)
httpReq, err = http.NewRequest(http.MethodPost, conn.URL, bytes.NewReader(body))
httpReq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
}
if err != nil {
return "", false, err.Error()
}
httpReq.Header.Set("User-Agent", "Mozilla/5.0 (compatible; CyberStrikeAI-WebShell/1.0)")
resp, err := h.client.Do(httpReq)
if err != nil {
return "", false, err.Error()
}
defer resp.Body.Close()
out, _ := io.ReadAll(resp.Body)
return string(out), resp.StatusCode == http.StatusOK, ""
}
+15 -1
View File
@@ -13,6 +13,12 @@ const (
// Skills工具
ToolListSkills = "list_skills"
ToolReadSkill = "read_skill"
// WebShell 助手工具(AI 在 WebShell 管理 - AI 助手 中使用)
ToolWebshellExec = "webshell_exec"
ToolWebshellFileList = "webshell_file_list"
ToolWebshellFileRead = "webshell_file_read"
ToolWebshellFileWrite = "webshell_file_write"
)
// IsBuiltinTool 检查工具名称是否是内置工具
@@ -22,7 +28,11 @@ func IsBuiltinTool(toolName string) bool {
ToolListKnowledgeRiskTypes,
ToolSearchKnowledgeBase,
ToolListSkills,
ToolReadSkill:
ToolReadSkill,
ToolWebshellExec,
ToolWebshellFileList,
ToolWebshellFileRead,
ToolWebshellFileWrite:
return true
default:
return false
@@ -37,5 +47,9 @@ func GetAllBuiltinTools() []string {
ToolSearchKnowledgeBase,
ToolListSkills,
ToolReadSkill,
ToolWebshellExec,
ToolWebshellFileList,
ToolWebshellFileRead,
ToolWebshellFileWrite,
}
}