From 60846b21523c3f77248932117461681b250f984a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=85=AC=E6=98=8E?= <83812544+Ed1s0nZ@users.noreply.github.com> Date: Wed, 25 Mar 2026 02:17:01 +0800 Subject: [PATCH] Add files via upload --- internal/app/app.go | 2 + internal/database/database.go | 14 ++++++ internal/database/webshell.go | 36 ++++++++++++++ internal/handler/webshell.go | 90 +++++++++++++++++++++++++++++++++-- 4 files changed, 137 insertions(+), 5 deletions(-) diff --git a/internal/app/app.go b/internal/app/app.go index a82fca5f..bd58a0a5 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -862,7 +862,9 @@ func setupRoutes( 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.GET("/webshell/connections/:id/state", webshellHandler.GetConnectionState) protected.PUT("/webshell/connections/:id", webshellHandler.UpdateConnection) + protected.PUT("/webshell/connections/:id/state", webshellHandler.SaveConnectionState) protected.DELETE("/webshell/connections/:id", webshellHandler.DeleteConnection) protected.POST("/webshell/exec", webshellHandler.Exec) protected.POST("/webshell/file", webshellHandler.FileOp) diff --git a/internal/database/database.go b/internal/database/database.go index 1c48f5e0..14a3809a 100644 --- a/internal/database/database.go +++ b/internal/database/database.go @@ -240,6 +240,15 @@ func (db *DB) initTables() error { created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP );` + // 创建 WebShell 连接扩展状态表(前端工作区/终端状态持久化) + createWebshellConnectionStatesTable := ` + CREATE TABLE IF NOT EXISTS webshell_connection_states ( + connection_id TEXT PRIMARY KEY, + state_json TEXT NOT NULL DEFAULT '{}', + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (connection_id) REFERENCES webshell_connections(id) ON DELETE CASCADE + );` + // 创建索引 createIndexes := ` CREATE INDEX IF NOT EXISTS idx_messages_conversation_id ON messages(conversation_id); @@ -267,6 +276,7 @@ func (db *DB) initTables() error { CREATE INDEX IF NOT EXISTS idx_batch_task_queues_created_at ON batch_task_queues(created_at); CREATE INDEX IF NOT EXISTS idx_batch_task_queues_title ON batch_task_queues(title); CREATE INDEX IF NOT EXISTS idx_webshell_connections_created_at ON webshell_connections(created_at); + CREATE INDEX IF NOT EXISTS idx_webshell_connection_states_updated_at ON webshell_connection_states(updated_at); ` if _, err := db.Exec(createConversationsTable); err != nil { @@ -329,6 +339,10 @@ func (db *DB) initTables() error { return fmt.Errorf("创建webshell_connections表失败: %w", err) } + if _, err := db.Exec(createWebshellConnectionStatesTable); err != nil { + return fmt.Errorf("创建webshell_connection_states表失败: %w", err) + } + // 为已有表添加新字段(如果不存在)- 必须在创建索引之前 if err := db.migrateConversationsTable(); err != nil { db.logger.Warn("迁移conversations表失败", zap.Error(err)) diff --git a/internal/database/webshell.go b/internal/database/webshell.go index abd7b28d..2ea25da7 100644 --- a/internal/database/webshell.go +++ b/internal/database/webshell.go @@ -19,6 +19,42 @@ type WebShellConnection struct { CreatedAt time.Time `json:"createdAt"` } +// GetWebshellConnectionState 获取连接关联的持久化状态 JSON,不存在时返回 "{}" +func (db *DB) GetWebshellConnectionState(connectionID string) (string, error) { + var stateJSON string + err := db.QueryRow(`SELECT state_json FROM webshell_connection_states WHERE connection_id = ?`, connectionID).Scan(&stateJSON) + if err == sql.ErrNoRows { + return "{}", nil + } + if err != nil { + db.logger.Error("查询 WebShell 连接状态失败", zap.Error(err), zap.String("connectionID", connectionID)) + return "", err + } + if stateJSON == "" { + stateJSON = "{}" + } + return stateJSON, nil +} + +// UpsertWebshellConnectionState 保存连接关联的持久化状态 JSON +func (db *DB) UpsertWebshellConnectionState(connectionID, stateJSON string) error { + if stateJSON == "" { + stateJSON = "{}" + } + query := ` + INSERT INTO webshell_connection_states (connection_id, state_json, updated_at) + VALUES (?, ?, ?) + ON CONFLICT(connection_id) DO UPDATE SET + state_json = excluded.state_json, + updated_at = excluded.updated_at + ` + if _, err := db.Exec(query, connectionID, stateJSON, time.Now()); err != nil { + db.logger.Error("保存 WebShell 连接状态失败", zap.Error(err), zap.String("connectionID", connectionID)) + return err + } + return nil +} + // ListWebshellConnections 列出所有 WebShell 连接,按创建时间倒序 func (db *DB) ListWebshellConnections() ([]WebShellConnection, error) { query := ` diff --git a/internal/handler/webshell.go b/internal/handler/webshell.go index b32bcb05..06da5d61 100644 --- a/internal/handler/webshell.go +++ b/internal/handler/webshell.go @@ -3,6 +3,7 @@ package handler import ( "bytes" "database/sql" + "encoding/json" "io" "net/http" "net/url" @@ -104,10 +105,10 @@ func (h *WebShellHandler) CreateConnection(c *gin.Context) { ID: "ws_" + strings.ReplaceAll(uuid.New().String(), "-", "")[:12], URL: req.URL, Password: strings.TrimSpace(req.Password), - Type: shellType, - Method: method, + Type: shellType, + Method: method, CmdParam: strings.TrimSpace(req.CmdParam), - Remark: strings.TrimSpace(req.Remark), + Remark: strings.TrimSpace(req.Remark), CreatedAt: time.Now(), } if err := h.db.CreateWebshellConnection(conn); err != nil { @@ -197,6 +198,85 @@ func (h *WebShellHandler) DeleteConnection(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"ok": true}) } +// GetConnectionState 获取 WebShell 连接关联的前端持久化状态(GET /api/webshell/connections/:id/state) +func (h *WebShellHandler) GetConnectionState(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 + } + conn, err := h.db.GetWebshellConnection(id) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + if conn == nil { + c.JSON(http.StatusNotFound, gin.H{"error": "connection not found"}) + return + } + stateJSON, err := h.db.GetWebshellConnectionState(id) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + var state interface{} + if err := json.Unmarshal([]byte(stateJSON), &state); err != nil { + state = map[string]interface{}{} + } + c.JSON(http.StatusOK, gin.H{"state": state}) +} + +// SaveConnectionState 保存 WebShell 连接关联的前端持久化状态(PUT /api/webshell/connections/:id/state) +func (h *WebShellHandler) SaveConnectionState(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 + } + conn, err := h.db.GetWebshellConnection(id) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + if conn == nil { + c.JSON(http.StatusNotFound, gin.H{"error": "connection not found"}) + return + } + var req struct { + State json.RawMessage `json:"state"` + } + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + raw := req.State + if len(raw) == 0 { + raw = json.RawMessage(`{}`) + } + if len(raw) > 2*1024*1024 { + c.JSON(http.StatusBadRequest, gin.H{"error": "state payload too large (max 2MB)"}) + return + } + var anyJSON interface{} + if err := json.Unmarshal(raw, &anyJSON); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "state must be valid json"}) + return + } + if err := h.db.UpsertWebshellConnectionState(id, string(raw)); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + 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 { @@ -267,8 +347,8 @@ type FileOpRequest struct { URL string `json:"url" binding:"required"` Password string `json:"password"` Type string `json:"type"` - Method string `json:"method"` // GET 或 POST,空则默认 POST - CmdParam string `json:"cmd_param"` // 命令参数名,如 cmd/xxx,空则默认 cmd + Method string `json:"method"` // GET 或 POST,空则默认 POST + CmdParam string `json:"cmd_param"` // 命令参数名,如 cmd/xxx,空则默认 cmd Action string `json:"action" binding:"required"` // list, read, delete, write, mkdir, rename, upload, upload_chunk Path string `json:"path"` TargetPath string `json:"target_path"` // rename 时目标路径