BG5 commited on
Commit
c4bdf8c
·
verified ·
1 Parent(s): ca0d33d

Upload 7 files

Browse files
Files changed (3) hide show
  1. README_HF.md +74 -0
  2. app.py +69 -273
  3. templates/terminal.html +74 -22
README_HF.md ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # WebShell - Hugging Face Spaces版本
2
+
3
+ 这是一个基于Flask和WebSocket的Web终端和文件管理系统,专为Hugging Face Spaces部署优化。
4
+
5
+ ## 功能特性
6
+
7
+ ### 🗂️ 文件管理
8
+ - 📁 文件/文件夹浏览、创建、删除、重命名
9
+ - 📤 文件上传下载
10
+ - ✏️ 在线代码编辑器(支持语法高亮)
11
+ - 👀 文件预览功能
12
+
13
+ ### 💻 终端模拟器
14
+ - 🖥️ 实时终端交互(基于WebSocket)
15
+ - 🐧 支持Linux bash命令
16
+ - 📜 命令历史记录(↑↓键浏览)
17
+ - 🔄 多终端会话支持
18
+
19
+ ### 🛡️ 安全特性
20
+ - 🔒 路径遍历攻击防护
21
+ - 📁 访问权限限制在应用目录内
22
+ - 🚫 恶意命令执行防护
23
+
24
+ ## 技术栈
25
+
26
+ **后端:**
27
+ - Flask + Flask-SocketIO
28
+ - Python subprocess
29
+ - 多线程处理
30
+
31
+ **前端:**
32
+ - Bootstrap 5 响应式设计
33
+ - CodeMirror 代码编辑器
34
+ - Socket.IO 实时通信
35
+ - Font Awesome 图标
36
+
37
+ ## 部署说明
38
+
39
+ 这个版本已经针对Hugging Face Spaces进行了优化:
40
+
41
+ 1. **端口配置**:使用环境变量 `PORT`(默认7860)
42
+ 2. **Linux兼容**:支持Linux bash终端
43
+ 3. **Docker化**:包含完整的Dockerfile
44
+ 4. **依赖管理**:requirements.txt包含所有必需的包
45
+
46
+ ## 环境变量
47
+
48
+ - `PORT`: 服务端口(默认7860)
49
+ - `HOST`: 服务主机(默认0.0.0.0)
50
+ - `DEBUG`: 调试模式(默认False)
51
+
52
+ ## 使用方法
53
+
54
+ 部署到Hugging Face Spaces后,访问以下功能:
55
+
56
+ 1. **主页**:`/` - 功能选择页面
57
+ 2. **文件管理**:`/files` - 文件浏览和编辑
58
+ 3. **终端**:`/terminal` - 命令行界面
59
+
60
+ ## 安全提醒
61
+
62
+ ⚠️ **重要安全提醒**:
63
+ - 此工具仅用于开发和学习目的
64
+ - 请勿在生产环境中暴露于公网
65
+ - 定期检查和更新依赖包
66
+ - 谨慎使用终端功能
67
+
68
+ ## 许可证
69
+
70
+ MIT License
71
+
72
+ ---
73
+
74
+ **注意**:本项目仅供学习和开发使用,请遵守相关法律法规和平台使用条款。
app.py CHANGED
@@ -19,209 +19,53 @@ app = Flask(__name__)
19
  app.config['SECRET_KEY'] = 'webshell-secret-key-2024'
20
  socketio = SocketIO(app, cors_allowed_origins="*")
21
 
22
- # 全局变量存储终端进程
23
- terminals = {}
24
 
25
- class Terminal:
26
- def __init__(self, session_id):
27
- self.session_id = session_id
28
- self.process = None
29
- self.is_windows = os.name == 'nt'
30
- self.output_queue = queue.Queue()
31
- self.input_queue = queue.Queue()
32
- self.running = False
33
- logger.info(f"创建终端实例: {session_id}, Windows: {self.is_windows}")
34
 
35
- def spawn(self):
36
- """启动终端进程"""
37
- try:
38
- logger.info(f"启动终端进程 {self.session_id}")
39
- if self.is_windows:
40
- # Windows环境使用cmd,使用简化的参数
41
- self.process = subprocess.Popen(
42
- ['cmd.exe', '/Q'], # /Q 参数关闭回显
43
- stdin=subprocess.PIPE,
44
- stdout=subprocess.PIPE,
45
- stderr=subprocess.STDOUT,
46
- text=True,
47
- shell=False,
48
- bufsize=1, # 行缓冲
49
- universal_newlines=True,
50
- cwd=os.getcwd(),
51
- env=os.environ.copy()
52
- )
53
- logger.info(f"CMD进程已启动,PID: {self.process.pid}")
54
- else:
55
- # Unix/Linux环境使用bash
56
- import pty
57
- self.child_pid, self.fd = pty.fork()
58
- if self.child_pid == 0:
59
- # 子进程
60
- os.execvp('/bin/bash', ['/bin/bash'])
61
- else:
62
- # 父进程
63
- self.set_winsize(24, 80)
64
- logger.info(f"PTY进程已启动,PID: {self.child_pid}")
65
-
66
- self.running = True
67
-
68
- # 启动输出读取线程
69
- if self.is_windows:
70
- self._start_output_thread()
71
- self._start_input_thread()
72
- else:
73
- self._start_pty_thread()
74
 
75
- logger.info(f"终端 {self.session_id} 启动成功")
76
- return True
77
- except Exception as e:
78
- logger.error(f"启动终端失败: {e}")
79
- return False
80
-
81
- def _start_output_thread(self):
82
- """启动输出读取线程(Windows)"""
83
- def read_output():
84
- logger.info(f"输出读取线程启动 {self.session_id}")
85
- while self.running and self.process and self.process.poll() is None:
86
- try:
87
- # 检查进程是否还活着
88
- if self.process.poll() is not None:
89
- logger.warning(f"进程已退出: {self.process.poll()}")
90
- break
91
-
92
- # 尝试读取一行
93
- try:
94
- line = self.process.stdout.readline()
95
- if line:
96
- logger.debug(f"读取到输出: {repr(line)}")
97
- self.output_queue.put(line)
98
- else:
99
- # 没有数据时短暂等待
100
- time.sleep(0.05)
101
- except Exception as read_err:
102
- logger.error(f"读取stdout错误: {read_err}")
103
- time.sleep(0.1)
104
-
105
- except Exception as e:
106
- logger.error(f"输出线程异常: {e}")
107
- break
108
-
109
- logger.info(f"输出读取线程结束 {self.session_id}")
110
-
111
- threading.Thread(target=read_output, daemon=True).start()
112
-
113
- def _start_input_thread(self):
114
- """启动输入处理线程(Windows)"""
115
- def process_input():
116
- logger.info(f"输入处理线程启动 {self.session_id}")
117
- while self.running and self.process and self.process.poll() is None:
118
- try:
119
- if not self.input_queue.empty():
120
- data = self.input_queue.get(timeout=0.1)
121
- logger.debug(f"写入命令: {repr(data)}")
122
- if self.process.stdin:
123
- self.process.stdin.write(data)
124
- self.process.stdin.flush()
125
- logger.debug(f"命令已写入并刷新缓冲区")
126
-
127
- # 立即发送回显
128
- if not data.endswith('\n'):
129
- # 如果不是完整命令,发送回显
130
- self.output_queue.put(data)
131
- except Exception as e:
132
- logger.error(f"处理输入错误: {e}")
133
- time.sleep(0.01)
134
- logger.info(f"输入处理线程结束 {self.session_id}")
135
-
136
- threading.Thread(target=process_input, daemon=True).start()
137
-
138
- def _start_pty_thread(self):
139
- """启动PTY线程(Linux)"""
140
- def pty_thread():
141
- logger.info(f"PTY线程启动 {self.session_id}")
142
- while self.running:
143
- try:
144
- import select
145
- ready, _, _ = select.select([self.fd], [], [], 0.1)
146
- if ready:
147
- data = os.read(self.fd, 1024)
148
- if data:
149
- output = data.decode('utf-8', errors='ignore')
150
- logger.debug(f"PTY输出: {repr(output)}")
151
- self.output_queue.put(output)
152
- except Exception as e:
153
- logger.error(f"PTY线程错误: {e}")
154
- break
155
- logger.info(f"PTY线程结束 {self.session_id}")
156
-
157
- threading.Thread(target=pty_thread, daemon=True).start()
158
 
159
- def set_winsize(self, rows, cols):
160
- """设置终端窗口大小"""
161
- if not self.is_windows and hasattr(self, 'fd'):
162
- try:
163
- import termios
164
- import struct
165
- import fcntl
166
- winsize = struct.pack("HHHH", rows, cols, 0, 0)
167
- fcntl.ioctl(self.fd, termios.TIOCSWINSZ, winsize)
168
- logger.debug(f"设置终端窗口大小: {rows}x{cols}")
169
- except Exception as e:
170
- logger.error(f"设置窗口大小失败: {e}")
171
-
172
- def read(self):
173
- """读取终端输出"""
174
- try:
175
- output = ""
176
- count = 0
177
- while not self.output_queue.empty() and count < 10: # 限制一次读取的数量
178
- try:
179
- data = self.output_queue.get_nowait()
180
- output += data
181
- count += 1
182
- except queue.Empty:
183
- break
184
- if output:
185
- logger.debug(f"读取到输出 {self.session_id}: {repr(output[:100])}")
186
  return output
187
- except Exception as e:
188
- logger.error(f"读取终端输出错误: {e}")
189
- return ''
190
 
191
- def write(self, data):
192
- """写入终端输入"""
193
- try:
194
- logger.info(f"接收到输入 {self.session_id}: {repr(data)}")
195
- if self.is_windows:
196
- self.input_queue.put(data)
197
- logger.debug(f"命令已放入输入队列,队列大小: {self.input_queue.qsize()}")
198
- else:
199
- # Unix/Linux
200
- if hasattr(self, 'fd'):
201
- os.write(self.fd, data.encode('utf-8'))
202
- logger.debug(f"命令已写入PTY")
203
- except Exception as e:
204
- logger.error(f"写入终端输入错误: {e}")
205
-
206
- def close(self):
207
- """关闭终端"""
208
- try:
209
- logger.info(f"关闭终端 {self.session_id}")
210
- self.running = False
211
- if self.is_windows:
212
- if self.process:
213
- self.process.terminate()
214
- self.process = None
215
- else:
216
- if hasattr(self, 'fd'):
217
- os.close(self.fd)
218
- if hasattr(self, 'child_pid'):
219
- try:
220
- os.kill(self.child_pid, signal.SIGTERM)
221
- except:
222
- pass
223
- except Exception as e:
224
- logger.error(f"关闭终端错误: {e}")
225
 
226
  @app.route('/')
227
  def index():
@@ -393,91 +237,36 @@ def on_connect():
393
 
394
  @socketio.on('disconnect')
395
  def on_disconnect():
396
- session_id = request.sid
397
- logger.info(f'客户端断开: {session_id}')
398
-
399
- # 离开房间
400
- leave_room(session_id)
401
-
402
- # 清理终端进程
403
- if session_id in terminals:
404
- terminals[session_id].close()
405
- del terminals[session_id]
406
 
407
  @socketio.on('start_terminal')
408
  def on_start_terminal():
409
- session_id = request.sid
410
- logger.info(f'启动终端请求: {session_id}')
411
-
412
- # 将客户端加入专用房间
413
- join_room(session_id)
414
- logger.info(f'客户端 {session_id} 加入房间')
415
-
416
- terminal = Terminal(session_id)
417
-
418
- if terminal.spawn():
419
- terminals[session_id] = terminal
420
-
421
- # 立即发送测试消息
422
- emit('terminal_output', {'data': '=== WebShell Terminal Started ===\n'})
423
- emit('terminal_output', {'data': f'Session ID: {session_id}\n'})
424
- emit('terminal_output', {'data': f'Working Directory: {os.getcwd()}\n'})
425
- emit('terminal_output', {'data': 'Type commands below:\n'})
426
-
427
- if os.name == 'nt':
428
- emit('terminal_output', {'data': 'C:\\> '})
429
- else:
430
- emit('terminal_output', {'data': '$ '})
431
-
432
- # 启动读取线程
433
- def read_thread():
434
- logger.info(f'读取线程启动: {session_id}')
435
- while session_id in terminals and terminals[session_id].running:
436
- try:
437
- terminal_instance = terminals[session_id]
438
- output = terminal_instance.read()
439
- if output:
440
- logger.info(f'发送输出到客户端 {session_id}: {repr(output)}')
441
- # 发送到特定房间
442
- socketio.emit('terminal_output', {'data': output}, room=session_id)
443
- time.sleep(0.05) # 减少延迟
444
- except Exception as e:
445
- logger.error(f"读取线程错误: {e}")
446
- break
447
- logger.info(f'读取线程结束: {session_id}')
448
-
449
- threading.Thread(target=read_thread, daemon=True).start()
450
- emit('terminal_ready')
451
- logger.info(f'终端就绪: {session_id}')
452
- else:
453
- emit('terminal_error', {'error': '无法启动终端'})
454
- logger.error(f'终端启动失败: {session_id}')
455
 
456
- @socketio.on('terminal_input')
457
  def on_terminal_input(data):
458
- session_id = request.sid
459
- command = data['data']
460
- logger.info(f"收到终端输入 {session_id}: {repr(command)}")
461
 
462
- if session_id in terminals:
463
- terminal = terminals[session_id]
464
- terminal.write(command)
465
-
466
- # 检查进程状态
467
- if terminal.is_windows and terminal.process:
468
- poll_result = terminal.process.poll()
469
- logger.debug(f"进程状态 {session_id}: {poll_result}, PID: {terminal.process.pid}")
470
- if poll_result is not None:
471
- logger.warning(f"进程已终止 {session_id}: 退出码 {poll_result}")
472
- else:
473
- logger.warning(f"终端不存在: {session_id}")
474
 
475
  @socketio.on('terminal_resize')
476
  def on_terminal_resize(data):
477
- session_id = request.sid
478
- logger.debug(f'终端调整大小: {session_id}, {data["rows"]}x{data["cols"]}')
479
- if session_id in terminals:
480
- terminals[session_id].set_winsize(data['rows'], data['cols'])
481
 
482
  # 错误处理
483
  @app.errorhandler(404)
@@ -495,4 +284,11 @@ if __name__ == '__main__':
495
  debug = os.environ.get('DEBUG', 'False').lower() == 'true'
496
 
497
  logger.info(f"🚀 WebShell 启动在 {host}:{port}")
 
 
 
 
 
 
 
498
  socketio.run(app, host=host, port=port, debug=debug, allow_unsafe_werkzeug=True)
 
19
  app.config['SECRET_KEY'] = 'webshell-secret-key-2024'
20
  socketio = SocketIO(app, cors_allowed_origins="*")
21
 
22
+ # 全局终端变量
23
+ shell_process = None
24
 
25
+ def execute_command(command):
26
+ """执行单个命令并返回输出"""
27
+ try:
28
+ logger.info(f"执行命令: {repr(command)}")
 
 
 
 
 
29
 
30
+ if os.name == 'nt':
31
+ # Windows环境
32
+ result = subprocess.run(
33
+ command,
34
+ shell=True,
35
+ capture_output=True,
36
+ text=True,
37
+ cwd=os.getcwd(),
38
+ timeout=30
39
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
 
41
+ output = result.stdout + result.stderr
42
+ logger.debug(f"命令输出: {repr(output)}")
43
+ return output
44
+ else:
45
+ # Unix/Linux环境
46
+ result = subprocess.run(
47
+ command,
48
+ shell=True,
49
+ capture_output=True,
50
+ text=True,
51
+ cwd=os.getcwd(),
52
+ timeout=30
53
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
 
55
+ output = result.stdout + result.stderr
56
+ logger.debug(f"命令输出: {repr(output)}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
  return output
 
 
 
58
 
59
+ except subprocess.TimeoutExpired:
60
+ return "命令执行超时\n"
61
+ except Exception as e:
62
+ logger.error(f"执行命令错误: {e}")
63
+ return f"错误: {e}\n"
64
+
65
+ def init_terminal():
66
+ """初始化全局终端"""
67
+ logger.info("终端初���化")
68
+ return True
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
69
 
70
  @app.route('/')
71
  def index():
 
237
 
238
  @socketio.on('disconnect')
239
  def on_disconnect():
240
+ logger.info(f'客户端断开: {request.sid}')
 
 
 
 
 
 
 
 
 
241
 
242
  @socketio.on('start_terminal')
243
  def on_start_terminal():
244
+ logger.info('启动终端请求')
245
+ emit('output', '=== WebShell Terminal Started ===\n')
246
+ emit('output', f'Working Directory: {os.getcwd()}\n')
247
+ emit('output', 'Type commands below:\n')
248
+ emit('terminal_ready')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
249
 
250
+ @socketio.on('input')
251
  def on_terminal_input(data):
252
+ command = data # 成功案例直接发送字符串,不是对象
253
+ logger.info(f"收到终端输入: {repr(command)}")
 
254
 
255
+ try:
256
+ output = execute_command(command)
257
+ # 确保输出末尾有换行符,以区分命令和输出
258
+ if output and not output.endswith('\n'):
259
+ output += '\n'
260
+ emit('output', output) # 改为'output'事件
261
+ except Exception as e:
262
+ logger.error(f"执行命令错误: {e}")
263
+ emit('output', f'错误: {e}\n')
 
 
 
264
 
265
  @socketio.on('terminal_resize')
266
  def on_terminal_resize(data):
267
+ logger.debug(f'终端调整大小: {data["rows"]}x{data["cols"]}')
268
+ # 对于全局终端,暂时忽略调整大小
269
+ pass
 
270
 
271
  # 错误处理
272
  @app.errorhandler(404)
 
284
  debug = os.environ.get('DEBUG', 'False').lower() == 'true'
285
 
286
  logger.info(f"🚀 WebShell 启动在 {host}:{port}")
287
+
288
+ # 初始化终端
289
+ if init_terminal():
290
+ logger.info("终端初始化成功")
291
+ else:
292
+ logger.error("终端初始化失败")
293
+
294
  socketio.run(app, host=host, port=port, debug=debug, allow_unsafe_werkzeug=True)
templates/terminal.html CHANGED
@@ -78,6 +78,22 @@
78
  background: #0c0c0c;
79
  white-space: pre-wrap;
80
  word-wrap: break-word;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81
  }
82
 
83
  .terminal-input-line {
@@ -315,15 +331,15 @@
315
  updatePrompt();
316
  });
317
 
318
- socket.on('terminal_output', function(data) {
319
- console.log('收到terminal_output事件:', data);
320
- if (data && data.data) {
321
- // 确保文本正确显示
322
- console.log('输出内容:', JSON.stringify(data.data));
323
- addOutput(data.data, '#cccccc', false);
324
  updatePrompt();
325
  } else {
326
- console.error('terminal_output数据格式错误:', data);
327
  }
328
  });
329
 
@@ -377,6 +393,28 @@
377
  }
378
  });
379
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
380
  // 输入内容变化时
381
  input.addEventListener('input', function() {
382
  currentCommand = this.value;
@@ -389,13 +427,18 @@
389
 
390
  // 点击其他地方时隐藏历史
391
  document.addEventListener('click', function(e) {
 
 
 
 
 
392
  if (!e.target.closest('.history-dropdown') && !e.target.closest('.terminal-input')) {
393
  hideHistoryDropdown();
394
  }
395
  });
396
 
397
- // 阻止右键菜单
398
- document.addEventListener('contextmenu', function(e) {
399
  e.preventDefault();
400
  });
401
 
@@ -427,11 +470,11 @@
427
  hideHistoryDropdown();
428
 
429
  // 显示命令
430
- addOutput(document.getElementById('terminalPrompt').textContent + ' ' + command, '#569cd6');
431
 
432
  // 发送命令到服务器
433
  if (socket && isConnected) {
434
- socket.emit('terminal_input', {data: command + '\n'});
435
  }
436
 
437
  // 清空输入
@@ -449,16 +492,17 @@
449
  return;
450
  }
451
 
452
- // 创建文本节点而不是div,保持原始格式
453
- const textNode = document.createTextNode(text);
454
- const span = document.createElement('span');
455
-
456
- if (color) {
457
- span.style.color = color;
458
- }
459
 
460
- span.appendChild(textNode);
461
- output.appendChild(span);
 
462
 
463
  // 自动滚动到底部
464
  output.scrollTop = output.scrollHeight;
@@ -585,10 +629,18 @@
585
  }
586
  });
587
 
588
- // 保持输入框焦点
589
  setInterval(function() {
590
  const input = document.getElementById('terminalInput');
591
- if (document.activeElement !== input) {
 
 
 
 
 
 
 
 
592
  input.focus();
593
  }
594
  }, 100);
 
78
  background: #0c0c0c;
79
  white-space: pre-wrap;
80
  word-wrap: break-word;
81
+ /* 启用文本选择和复制 */
82
+ user-select: text !important;
83
+ -webkit-user-select: text !important;
84
+ -moz-user-select: text !important;
85
+ -ms-user-select: text !important;
86
+ /* 确保可以正常交互 */
87
+ pointer-events: auto;
88
+ cursor: text;
89
+ }
90
+
91
+ /* 确保span元素也可以选择 */
92
+ .terminal-output span {
93
+ user-select: text !important;
94
+ -webkit-user-select: text !important;
95
+ -moz-user-select: text !important;
96
+ -ms-user-select: text !important;
97
  }
98
 
99
  .terminal-input-line {
 
331
  updatePrompt();
332
  });
333
 
334
+ socket.on('output', function(data) {
335
+ console.log('收到output事件:', data);
336
+ if (data) {
337
+ // 直接显示输出,与成功案例一致
338
+ console.log('输出内容:', JSON.stringify(data));
339
+ addOutput(data, '#cccccc', false);
340
  updatePrompt();
341
  } else {
342
+ console.error('output数据为空:', data);
343
  }
344
  });
345
 
 
393
  }
394
  });
395
 
396
+ // 添加全局快捷键支持
397
+ document.addEventListener('keydown', function(e) {
398
+ // Ctrl+A 全选终端输出
399
+ if (e.ctrlKey && e.key === 'a') {
400
+ const output = document.getElementById('terminalOutput');
401
+ if (window.getSelection().toString() === '') {
402
+ const range = document.createRange();
403
+ range.selectNodeContents(output);
404
+ const selection = window.getSelection();
405
+ selection.removeAllRanges();
406
+ selection.addRange(range);
407
+ e.preventDefault();
408
+ }
409
+ }
410
+
411
+ // Ctrl+L 清空终端
412
+ if (e.ctrlKey && e.key === 'l') {
413
+ clearTerminal();
414
+ e.preventDefault();
415
+ }
416
+ });
417
+
418
  // 输入内容变化时
419
  input.addEventListener('input', function() {
420
  currentCommand = this.value;
 
427
 
428
  // 点击其他地方时隐藏历史
429
  document.addEventListener('click', function(e) {
430
+ // 如果点击的是终端输出区域,不要隐藏历史,允许文本选择
431
+ if (e.target.closest('.terminal-output')) {
432
+ return;
433
+ }
434
+
435
  if (!e.target.closest('.history-dropdown') && !e.target.closest('.terminal-input')) {
436
  hideHistoryDropdown();
437
  }
438
  });
439
 
440
+ // 只在输入区域阻止右键菜单,输出区域允许右键复制
441
+ document.getElementById('terminalInput').addEventListener('contextmenu', function(e) {
442
  e.preventDefault();
443
  });
444
 
 
470
  hideHistoryDropdown();
471
 
472
  // 显示命令
473
+ addOutput(document.getElementById('terminalPrompt').textContent + ' ' + command + '\n', '#569cd6');
474
 
475
  // 发送命令到服务器
476
  if (socket && isConnected) {
477
+ socket.emit('input', command + '\n'); // 直接发送字符串
478
  }
479
 
480
  // 清空输入
 
492
  return;
493
  }
494
 
495
+ // 转义HTML特殊字符但保留换行
496
+ const escapedText = text
497
+ .replace(/&/g, '&amp;')
498
+ .replace(/</g, '&lt;')
499
+ .replace(/>/g, '&gt;')
500
+ .replace(/"/g, '&quot;')
501
+ .replace(/'/g, '&#39;');
502
 
503
+ // 直接使用innerHTML添加,这样文本更容易选择
504
+ const colorStyle = color ? `color: ${color};` : '';
505
+ output.innerHTML += `<span style="${colorStyle}">${escapedText}</span>`;
506
 
507
  // 自动滚动到底部
508
  output.scrollTop = output.scrollHeight;
 
629
  }
630
  });
631
 
632
+ // 保持输入框焦点,但不要在用户选择文本时干扰
633
  setInterval(function() {
634
  const input = document.getElementById('terminalInput');
635
+ const selection = window.getSelection();
636
+
637
+ // 如果用户正在选择文本,不要强制聚焦
638
+ if (selection.toString().length > 0) {
639
+ return;
640
+ }
641
+
642
+ // 如果当前焦点不在输入框且没有其他元素获得焦点
643
+ if (document.activeElement !== input && document.activeElement === document.body) {
644
  input.focus();
645
  }
646
  }, 100);