Compare commits

...

11 Commits

Author SHA1 Message Date
公明 3cea834036 Update config.yaml 2026-03-25 03:29:53 +08:00
公明 e1b594f875 Add files via upload 2026-03-25 03:26:13 +08:00
公明 4b105e0bb7 Add files via upload 2026-03-25 03:24:33 +08:00
公明 93f0a46d6e Add files via upload 2026-03-25 03:08:10 +08:00
公明 314cd005c8 Add files via upload 2026-03-25 03:06:37 +08:00
公明 c68b72ead2 Add files via upload 2026-03-25 03:05:13 +08:00
公明 60846b2152 Add files via upload 2026-03-25 02:17:01 +08:00
公明 f6525674d2 Add files via upload 2026-03-25 02:15:37 +08:00
公明 9c04b0db40 Add files via upload 2026-03-25 01:22:27 +08:00
公明 907b87494d Add files via upload 2026-03-25 01:00:29 +08:00
公明 97b7b4b932 Add files via upload 2026-03-24 23:54:38 +08:00
10 changed files with 2299 additions and 130 deletions
+1 -1
View File
@@ -10,7 +10,7 @@
# ============================================
# 前端显示的版本号(可选,不填则显示默认版本)
version: "v1.4.1"
version: "v1.4.2"
# 服务器配置
server:
host: 0.0.0.0 # 监听地址,0.0.0.0 表示监听所有网络接口
+1 -1
View File
@@ -8,6 +8,7 @@ go 1.24.0
toolchain go1.24.4
require (
github.com/bytedance/sonic v1.15.0
github.com/cloudwego/eino v0.8.4
github.com/cloudwego/eino-ext/components/model/openai v0.1.10
github.com/creack/pty v1.1.24
@@ -30,7 +31,6 @@ require (
github.com/bmatcuk/doublestar/v4 v4.10.0 // indirect
github.com/buger/jsonparser v1.1.1 // indirect
github.com/bytedance/gopkg v0.1.3 // indirect
github.com/bytedance/sonic v1.15.0 // indirect
github.com/bytedance/sonic/loader v0.5.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/cloudwego/eino-ext/libs/acl/openai v0.1.14 // indirect
+2
View File
@@ -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)
+14
View File
@@ -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))
+36
View File
@@ -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 := `
+85 -5
View File
@@ -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 时目标路径
+617 -3
View File
@@ -8860,6 +8860,71 @@ header {
border-radius: 10px;
border: 1px solid var(--border-color);
}
.webshell-terminal-sessions {
display: flex;
align-items: center;
gap: 2px;
padding: 0 8px;
height: 34px;
background: #0b0f14;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
overflow-x: auto;
}
.webshell-terminal-session {
display: inline-flex;
align-items: center;
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.12);
border-bottom: none;
border-radius: 6px 6px 0 0;
height: 30px;
color: #c9d1d9;
}
.webshell-terminal-session.active {
background: #0d1117;
border-color: rgba(88, 166, 255, 0.45);
color: #e6edf3;
}
.webshell-terminal-session-main {
border: 0;
background: transparent;
color: inherit;
font-size: 12px;
height: 100%;
padding: 0 10px;
cursor: pointer;
white-space: nowrap;
}
.webshell-terminal-session-close {
border: 0;
background: transparent;
color: #8b949e;
width: 20px;
height: 100%;
cursor: pointer;
font-size: 12px;
}
.webshell-terminal-session-close:hover {
color: #f85149;
background: rgba(248, 81, 73, 0.08);
}
.webshell-terminal-session-add {
border: 1px solid rgba(255, 255, 255, 0.16);
border-bottom: none;
background: rgba(255, 255, 255, 0.03);
color: #8b949e;
height: 30px;
width: 28px;
border-radius: 6px 6px 0 0;
cursor: pointer;
font-size: 16px;
line-height: 1;
flex-shrink: 0;
}
.webshell-terminal-session-add:hover {
color: #e6edf3;
background: rgba(255, 255, 255, 0.08);
}
.webshell-quick-label {
font-size: 12px;
font-weight: 500;
@@ -8869,20 +8934,48 @@ header {
.webshell-terminal-toolbar .btn-ghost {
font-size: 12px;
}
.webshell-terminal-status {
display: inline-flex;
align-items: center;
height: 24px;
padding: 0 10px;
border-radius: 999px;
font-size: 12px;
font-weight: 600;
border: 1px solid transparent;
}
.webshell-terminal-status.idle {
color: #166534;
background: rgba(34, 197, 94, 0.1);
border-color: rgba(34, 197, 94, 0.25);
}
.webshell-terminal-status.running {
color: #9a3412;
background: rgba(251, 146, 60, 0.14);
border-color: rgba(251, 146, 60, 0.28);
}
#webshell-pane-terminal {
flex-direction: column;
}
/* 仅外框圆角,内部不做额外装饰,避免挡住文字 */
.webshell-terminal-container {
.webshell-terminal-shell {
flex: 1;
min-height: 360px;
padding: 0;
display: flex;
flex-direction: column;
background: #0d1117;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
.webshell-terminal-container {
flex: 1;
min-height: 0;
padding: 0;
background: transparent;
overflow: hidden;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
transform: translateZ(0);
@@ -8941,6 +9034,128 @@ header {
background: rgba(139, 148, 158, 0.7);
}
.webshell-file-layout {
display: flex;
gap: 12px;
min-height: 0;
flex: 1;
}
.webshell-file-sidebar {
width: 280px;
min-width: 260px;
max-width: 320px;
border: 1px solid var(--border-color);
border-radius: 10px;
background: linear-gradient(180deg, var(--bg-secondary) 0%, var(--bg-primary) 100%);
display: flex;
flex-direction: column;
min-height: 0;
box-shadow: var(--shadow-sm);
}
.webshell-file-sidebar-title {
padding: 12px 14px;
border-bottom: 1px solid var(--border-color);
font-weight: 600;
color: var(--text-primary);
letter-spacing: 0.2px;
}
.webshell-dir-tree {
padding: 10px 8px;
overflow: auto;
min-height: 0;
}
.webshell-tree-node {
position: relative;
}
.webshell-tree-row {
display: flex;
align-items: center;
border-radius: 8px;
margin: 2px 0;
}
.webshell-tree-row.active {
background: rgba(0, 102, 255, 0.11);
}
.webshell-tree-toggle {
width: 18px;
min-width: 18px;
height: 24px;
margin-left: 2px;
border: none;
background: transparent;
color: var(--text-secondary);
cursor: pointer;
padding: 0;
font-size: 12px;
}
.webshell-tree-toggle.empty {
cursor: default;
opacity: 0.6;
}
.webshell-tree-children {
margin-left: 14px;
border-left: 1px dashed rgba(128, 128, 128, 0.28);
padding-left: 6px;
}
.webshell-dir-item {
display: inline-flex;
align-items: center;
gap: 6px;
flex: 1;
text-align: left;
border: none;
background: transparent;
color: var(--text-primary);
border-radius: 6px;
padding: 4px 8px;
margin: 0;
cursor: pointer;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.webshell-tree-icon {
flex: 0 0 auto;
font-size: 0.92rem;
opacity: 0.92;
}
.webshell-tree-name {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.webshell-dir-item:hover {
background: rgba(0, 102, 255, 0.08);
}
.webshell-tree-row.active .webshell-dir-item {
color: var(--accent-color);
font-weight: 600;
}
.webshell-file-main {
display: flex;
flex-direction: column;
min-width: 0;
min-height: 0;
flex: 1;
}
.webshell-file-toolbar {
display: flex;
align-items: center;
@@ -8948,7 +9163,7 @@ header {
margin-bottom: 12px;
flex-wrap: wrap;
padding: 12px 14px;
background: var(--bg-secondary);
background: linear-gradient(180deg, var(--bg-secondary) 0%, var(--bg-primary) 100%);
border-radius: 10px;
border: 1px solid var(--border-color);
width: 100%;
@@ -9093,6 +9308,19 @@ header {
width: 100%;
}
@media (max-width: 1200px) {
.webshell-file-layout {
flex-direction: column;
}
.webshell-file-sidebar {
width: 100%;
min-width: 0;
max-width: none;
max-height: 220px;
}
}
.webshell-file-table {
width: 100%;
border-collapse: collapse;
@@ -9154,6 +9382,22 @@ header {
color: var(--accent-hover);
}
.webshell-file-link.is-dir::before,
.webshell-file-link.is-file::before {
display: inline-block;
margin-right: 6px;
font-size: 0.95rem;
opacity: 0.95;
}
.webshell-file-link.is-dir::before {
content: "📁";
}
.webshell-file-link.is-file::before {
content: "📄";
}
.webshell-file-table .webshell-file-read {
color: var(--accent-color);
margin-right: 8px;
@@ -9426,6 +9670,112 @@ header {
flex-direction: column;
min-height: 0;
}
.webshell-pane-memo {
padding: 14px;
background: linear-gradient(180deg, rgba(248, 250, 252, 0.55) 0%, rgba(241, 245, 249, 0.28) 100%);
}
.webshell-memo-layout {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
border: 1px solid rgba(15, 23, 42, 0.08);
border-radius: 14px;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.96) 0%, rgba(248, 250, 252, 0.9) 100%);
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.06), inset 0 1px 0 rgba(255, 255, 255, 0.85);
overflow: hidden;
}
.webshell-memo-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
padding: 10px 14px;
border-bottom: 1px solid rgba(15, 23, 42, 0.08);
font-size: 0.92rem;
font-weight: 600;
color: var(--text-primary);
background: rgba(255, 255, 255, 0.75);
backdrop-filter: blur(6px);
}
.webshell-memo-input {
flex: 1;
min-height: 0;
margin: 12px 14px 8px;
padding: 12px 13px;
border-radius: 10px;
border: 1px solid rgba(15, 23, 42, 0.14);
background: #fff;
font-size: 0.9rem;
line-height: 1.55;
font-family: "JetBrains Mono", "Fira Code", "SFMono-Regular", Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
color: var(--text-primary);
resize: none;
transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
.webshell-memo-input:focus {
border-color: rgba(37, 99, 235, 0.5);
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.15);
outline: none;
}
.webshell-memo-status {
margin: 0 14px 12px auto;
padding: 3px 10px;
border-radius: 999px;
font-size: 0.74rem;
color: var(--text-secondary);
background: rgba(148, 163, 184, 0.14);
border: 1px solid rgba(148, 163, 184, 0.25);
}
.webshell-memo-status.error {
color: #b91c1c;
background: rgba(239, 68, 68, 0.14);
border-color: rgba(239, 68, 68, 0.28);
}
.webshell-ai-memo {
flex-shrink: 0;
width: 300px;
min-width: 220px;
display: flex;
flex-direction: column;
border-left: 1px solid var(--border-color);
background: var(--bg-secondary);
min-height: 0;
}
.webshell-ai-memo-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
padding: 10px 12px;
border-bottom: 1px solid var(--border-color);
font-size: 0.9rem;
color: var(--text-primary);
}
.webshell-ai-memo-input {
flex: 1;
min-height: 0;
margin: 10px 12px 8px;
resize: none;
}
.webshell-ai-memo-status {
padding: 0 12px 10px;
font-size: 0.75rem;
color: var(--text-secondary);
}
.webshell-ai-memo-status.error {
color: #dc2626;
}
@media (max-width: 1280px) {
.webshell-ai-memo {
width: 260px;
}
}
@media (max-width: 980px) {
.webshell-ai-memo {
display: none;
}
}
.webshell-ai-hint {
flex-shrink: 0;
padding: 10px 14px;
@@ -9793,6 +10143,222 @@ header {
background: linear-gradient(180deg, rgba(2, 6, 23, 0.015) 0%, rgba(2, 6, 23, 0.03) 100%);
border-radius: 10px;
}
.webshell-db-profiles-bar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
border: 1px solid rgba(15, 23, 42, 0.08);
border-radius: 10px;
padding: 6px;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.96) 0%, rgba(248, 250, 252, 0.92) 100%);
}
.webshell-db-profiles {
display: flex;
align-items: center;
gap: 6px;
overflow-x: auto;
min-width: 0;
flex: 1;
}
.webshell-db-profile-actions {
flex-shrink: 0;
}
.webshell-db-profile-tab {
display: inline-flex;
align-items: center;
border: 1px solid rgba(15, 23, 42, 0.12);
border-radius: 8px;
overflow: hidden;
background: #fff;
}
.webshell-db-profile-tab.active {
border-color: rgba(0, 102, 255, 0.36);
box-shadow: 0 0 0 1px rgba(0, 102, 255, 0.12);
}
.webshell-db-profile-main,
.webshell-db-profile-menu {
border: 0;
background: transparent;
color: var(--text-secondary);
cursor: pointer;
}
.webshell-db-profile-main {
padding: 5px 10px;
max-width: 200px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-size: 0.8rem;
}
.webshell-db-profile-tab.active .webshell-db-profile-main {
color: var(--text-primary);
font-weight: 600;
}
.webshell-db-profile-menu {
padding: 5px 7px;
border-left: 1px solid rgba(15, 23, 42, 0.1);
font-size: 0.78rem;
}
.webshell-db-profile-menu:hover {
background: rgba(15, 23, 42, 0.06);
}
.webshell-db-layout {
flex: 1;
min-height: 0;
display: grid;
grid-template-columns: 300px minmax(0, 1fr);
gap: 12px;
}
.webshell-db-sidebar {
border: 1px solid rgba(15, 23, 42, 0.08);
border-radius: 12px;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.98) 0%, rgba(248, 250, 252, 0.96) 100%);
box-shadow: 0 6px 20px rgba(15, 23, 42, 0.06), inset 0 1px 0 rgba(255, 255, 255, 0.9);
display: flex;
flex-direction: column;
min-height: 0;
}
.webshell-db-sidebar-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
padding: 10px 12px;
border-bottom: 1px solid var(--border-color);
}
.webshell-db-sidebar-head span {
font-size: 0.82rem;
font-weight: 700;
color: var(--text-primary);
}
.webshell-db-schema-tree {
flex: 1;
min-height: 0;
overflow-y: auto;
overflow-x: hidden;
padding: 10px;
}
.webshell-db-sidebar-hint {
border-top: 1px solid var(--border-color);
padding: 8px 12px;
font-size: 0.76rem;
color: var(--text-secondary);
background: rgba(2, 6, 23, 0.02);
}
.webshell-db-group {
border: 1px solid rgba(15, 23, 42, 0.08);
border-radius: 8px;
background: #fff;
margin-bottom: 8px;
}
.webshell-db-group-title {
cursor: pointer;
display: flex;
align-items: center;
gap: 6px;
list-style: none;
padding: 8px 10px;
font-size: 0.82rem;
color: var(--text-primary);
min-width: 0;
white-space: nowrap;
overflow: hidden;
}
.webshell-db-group-title::-webkit-details-marker {
display: none;
}
.webshell-db-count {
margin-left: auto;
font-size: 0.74rem;
color: var(--text-secondary);
flex: 0 0 auto;
}
.webshell-db-group-items {
border-top: 1px solid var(--border-color);
display: flex;
flex-direction: column;
max-height: 260px;
overflow-y: auto;
overflow-x: hidden;
}
.webshell-db-table-node {
border-bottom: 1px solid rgba(15, 23, 42, 0.06);
min-width: 0;
}
.webshell-db-table-node:last-child {
border-bottom: 0;
}
.webshell-db-table-item {
border: 0;
background: transparent;
text-align: left;
display: flex;
align-items: center;
gap: 6px;
padding: 7px 10px;
cursor: pointer;
color: var(--text-secondary);
font-size: 0.8rem;
list-style: none;
min-width: 0;
white-space: nowrap;
overflow: hidden;
}
.webshell-db-table-item::-webkit-details-marker {
display: none;
}
.webshell-db-table-item:hover {
background: rgba(0, 102, 255, 0.06);
color: var(--text-primary);
}
.webshell-db-column-list {
display: flex;
flex-direction: column;
margin: 0 0 4px;
}
.webshell-db-column-item {
border: 0;
background: transparent;
text-align: left;
display: flex;
align-items: center;
gap: 6px;
padding: 6px 10px 6px 24px;
cursor: pointer;
color: var(--text-secondary);
font-size: 0.78rem;
min-width: 0;
white-space: nowrap;
overflow: hidden;
}
.webshell-db-column-item:hover {
background: rgba(0, 102, 255, 0.06);
color: var(--text-primary);
}
.webshell-db-column-empty {
padding: 4px 10px 8px 24px;
font-size: 0.74rem;
color: var(--text-secondary);
}
.webshell-db-icon {
opacity: 0.85;
flex: 0 0 auto;
}
.webshell-db-label {
min-width: 0;
flex: 1 1 auto;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.webshell-db-main {
min-width: 0;
min-height: 0;
display: flex;
flex-direction: column;
gap: 10px;
}
.webshell-db-toolbar {
display: grid;
grid-template-columns: repeat(4, minmax(160px, 1fr));
@@ -9844,6 +10410,10 @@ header {
#webshell-db-sqlite-row {
grid-column: 1 / -1;
}
.webshell-db-sql-tools {
display: flex;
gap: 8px;
}
.webshell-db-sql {
width: 100%;
min-height: 140px;
@@ -9905,6 +10475,41 @@ header {
.webshell-db-output.error {
color: var(--error-color);
}
.webshell-db-result-table {
border-bottom: 1px solid var(--border-color);
overflow: auto;
max-height: 46%;
}
.webshell-db-table {
width: 100%;
border-collapse: collapse;
font-size: 0.8rem;
}
.webshell-db-table th,
.webshell-db-table td {
padding: 7px 8px;
border-bottom: 1px solid rgba(148, 163, 184, 0.24);
border-right: 1px solid rgba(148, 163, 184, 0.24);
white-space: nowrap;
}
.webshell-db-table th:last-child,
.webshell-db-table td:last-child {
border-right: none;
}
.webshell-db-table thead th {
position: sticky;
top: 0;
z-index: 1;
background: rgba(248, 250, 252, 0.98);
font-weight: 700;
}
.webshell-db-table-meta {
padding: 6px 8px;
font-size: 0.74rem;
color: var(--text-secondary);
border-top: 1px solid var(--border-color);
background: rgba(248, 250, 252, 0.9);
}
.webshell-db-hint {
border-top: 1px solid var(--border-color);
font-size: 0.76rem;
@@ -9913,11 +10518,20 @@ header {
background: rgba(2, 6, 23, 0.02);
}
@media (max-width: 1280px) {
.webshell-db-layout {
grid-template-columns: 240px minmax(0, 1fr);
}
.webshell-db-toolbar {
grid-template-columns: repeat(3, minmax(140px, 1fr));
}
}
@media (max-width: 980px) {
.webshell-db-layout {
grid-template-columns: 1fr;
}
.webshell-db-sidebar {
min-height: 200px;
}
.webshell-db-toolbar {
grid-template-columns: repeat(2, minmax(140px, 1fr));
}
+30
View File
@@ -374,6 +374,7 @@
"tabFileManager": "File manager",
"tabAiAssistant": "AI Assistant",
"tabDbManager": "Database Manager",
"tabMemo": "Memo",
"dbType": "Database type",
"dbHost": "Host",
"dbPort": "Port",
@@ -390,6 +391,23 @@
"dbRunning": "Database command is running, please wait",
"dbCliHint": "If command not found appears, install mysql/psql/sqlite3/sqlcmd on the target host first",
"dbExecFailed": "Database execution failed",
"dbSchema": "Database Schema",
"dbLoadSchema": "Load Schema",
"dbNoSchema": "No schema yet, click Load Schema",
"dbSelectTableHint": "Click a table to expand columns and generate SQL",
"dbNoColumns": "No column details",
"dbResultTable": "Result Table",
"dbClearSql": "Clear SQL",
"dbTemplateSql": "SQL Template",
"dbRows": "rows",
"dbColumns": "columns",
"dbSchemaFailed": "Failed to load schema",
"dbAddProfile": "Add connection",
"dbRenameProfile": "Rename",
"dbDeleteProfile": "Delete connection",
"dbDeleteProfileConfirm": "Delete this database connection profile?",
"dbProfileNamePrompt": "Enter profile name",
"dbProfiles": "Database connections",
"aiSystemReadyMessage": "System is ready. Please enter your test requirements, and the system will automatically perform the corresponding security tests.",
"aiNewConversation": "New conversation",
"aiPreviousConversation": "Previous conversation",
@@ -397,6 +415,11 @@
"aiDeleteConversationConfirm": "Delete this conversation?",
"aiPlaceholder": "e.g. List files in the current directory",
"aiSend": "Send",
"aiMemo": "Memo",
"aiMemoPlaceholder": "Save key commands, testing ideas, and repro steps...",
"aiMemoClear": "Clear",
"aiMemoSaving": "Saving...",
"aiMemoSaved": "Saved locally",
"quickCommands": "Quick commands",
"downloadFile": "Download",
"terminalWelcome": "WebShell virtual terminal — type a command and press Enter (Ctrl+L clear)",
@@ -414,6 +437,13 @@
"testFailed": "Connectivity test failed",
"testNoExpectedOutput": "Shell responded but expected output was not found. Check password and command parameter name.",
"clearScreen": "Clear",
"copyTerminalLog": "Copy log",
"terminalIdle": "Idle",
"terminalRunning": "Running",
"terminalCopyOk": "Log copied",
"terminalCopyFail": "Copy failed",
"terminalNewWindow": "New terminal",
"terminalWindowPrefix": "Terminal",
"running": "Running…",
"waitFinish": "Please wait for the current command to finish",
"newDir": "New directory",
+30
View File
@@ -374,6 +374,7 @@
"tabFileManager": "文件管理",
"tabAiAssistant": "AI 助手",
"tabDbManager": "数据库管理",
"tabMemo": "备忘录",
"dbType": "数据库类型",
"dbHost": "主机",
"dbPort": "端口",
@@ -390,6 +391,23 @@
"dbRunning": "数据库命令执行中,请稍候",
"dbCliHint": "如果提示命令不存在,请先在目标主机安装对应客户端(mysql/psql/sqlite3/sqlcmd",
"dbExecFailed": "数据库执行失败",
"dbSchema": "数据库结构",
"dbLoadSchema": "加载结构",
"dbNoSchema": "暂无数据库结构,请先加载",
"dbSelectTableHint": "点击表名可展开列信息并生成查询 SQL",
"dbNoColumns": "暂无列信息",
"dbResultTable": "结果表格",
"dbClearSql": "清空 SQL",
"dbTemplateSql": "示例 SQL",
"dbRows": "行",
"dbColumns": "列",
"dbSchemaFailed": "加载数据库结构失败",
"dbAddProfile": "新增连接",
"dbRenameProfile": "重命名",
"dbDeleteProfile": "删除连接",
"dbDeleteProfileConfirm": "确定删除该数据库连接配置吗?",
"dbProfileNamePrompt": "请输入连接名称",
"dbProfiles": "数据库连接",
"aiSystemReadyMessage": "系统已就绪。请输入您的测试需求,系统将自动执行相应的安全测试。",
"aiNewConversation": "新对话",
"aiPreviousConversation": "之前的对话",
@@ -397,6 +415,11 @@
"aiDeleteConversationConfirm": "确定删除当前对话记录?",
"aiPlaceholder": "例如:列出当前目录下的文件",
"aiSend": "发送",
"aiMemo": "备忘录",
"aiMemoPlaceholder": "记录关键命令、测试思路、复现步骤...",
"aiMemoClear": "清空",
"aiMemoSaving": "保存中...",
"aiMemoSaved": "已保存到本地",
"quickCommands": "快捷命令",
"downloadFile": "下载",
"terminalWelcome": "WebShell 虚拟终端 — 输入命令后按回车执行(Ctrl+L 清屏)",
@@ -414,6 +437,13 @@
"testFailed": "连通性测试失败",
"testNoExpectedOutput": "Shell 返回了响应但未得到预期输出,请检查连接密码与命令参数名",
"clearScreen": "清屏",
"copyTerminalLog": "复制日志",
"terminalIdle": "空闲",
"terminalRunning": "执行中",
"terminalCopyOk": "日志已复制",
"terminalCopyFail": "复制失败",
"terminalNewWindow": "新终端",
"terminalWindowPrefix": "终端",
"running": "执行中…",
"waitFinish": "请等待当前命令执行完成",
"newDir": "新建目录",
+1483 -120
View File
File diff suppressed because it is too large Load Diff