BG5 commited on
Commit
a4d706b
·
verified ·
1 Parent(s): 64014f3

Upload 7 files

Browse files
Files changed (7) hide show
  1. Dockerfile +37 -0
  2. app.py +407 -0
  3. requirements.txt +5 -0
  4. run.py +78 -0
  5. templates/files.html +646 -0
  6. templates/index.html +154 -0
  7. templates/terminal.html +553 -0
Dockerfile ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 使用Python 3.11作为基础镜像
2
+ FROM python:3.11-slim
3
+
4
+ # 设置工作目录
5
+ WORKDIR /app
6
+
7
+ # 设置环境变量
8
+ ENV PYTHONUNBUFFERED=1
9
+ ENV PYTHONDONTWRITEBYTECODE=1
10
+
11
+ # 安装系统依赖
12
+ RUN apt-get update && apt-get install -y \
13
+ curl \
14
+ vim \
15
+ git \
16
+ procps \
17
+ net-tools \
18
+ && rm -rf /var/lib/apt/lists/*
19
+
20
+ # 复制requirements文件
21
+ COPY requirements.txt .
22
+
23
+ # 安装Python依赖
24
+ RUN pip install --no-cache-dir -r requirements.txt
25
+
26
+ # 复制应用代码
27
+ COPY . .
28
+
29
+ # 创建非root用户
30
+ RUN useradd -m -u 1000 user && chown -R user:user /app
31
+ USER user
32
+
33
+ # 暴露端口(Hugging Face Spaces使用7860端口)
34
+ EXPOSE 7860
35
+
36
+ # 设置启动命令
37
+ CMD ["python", "app.py"]
app.py ADDED
@@ -0,0 +1,407 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import subprocess
3
+ import json
4
+ import threading
5
+ import time
6
+ import signal
7
+ import queue
8
+ from datetime import datetime
9
+ from flask import Flask, render_template, request, jsonify, send_file, abort, redirect, url_for
10
+ from flask_socketio import SocketIO, emit
11
+ import sys
12
+
13
+ app = Flask(__name__)
14
+ app.config['SECRET_KEY'] = 'webshell-secret-key-2024'
15
+ socketio = SocketIO(app, cors_allowed_origins="*")
16
+
17
+ # 全局变量存储终端进程
18
+ terminals = {}
19
+
20
+ class Terminal:
21
+ def __init__(self, session_id):
22
+ self.session_id = session_id
23
+ self.process = None
24
+ self.is_windows = os.name == 'nt'
25
+ self.output_queue = queue.Queue()
26
+ self.input_queue = queue.Queue()
27
+ self.running = False
28
+
29
+ def spawn(self):
30
+ """启动终端进程"""
31
+ try:
32
+ if self.is_windows:
33
+ # Windows环境使用cmd
34
+ self.process = subprocess.Popen(
35
+ 'cmd',
36
+ stdin=subprocess.PIPE,
37
+ stdout=subprocess.PIPE,
38
+ stderr=subprocess.STDOUT,
39
+ text=True,
40
+ shell=True,
41
+ bufsize=0
42
+ )
43
+ else:
44
+ # Unix/Linux环境使用bash
45
+ import pty
46
+ self.child_pid, self.fd = pty.fork()
47
+ if self.child_pid == 0:
48
+ # 子进程
49
+ os.execvp('/bin/bash', ['/bin/bash'])
50
+ else:
51
+ # 父进程
52
+ self.set_winsize(24, 80)
53
+
54
+ self.running = True
55
+
56
+ # 启动输出读取线程
57
+ if self.is_windows:
58
+ self._start_output_thread()
59
+ self._start_input_thread()
60
+
61
+ return True
62
+ except Exception as e:
63
+ print(f"启动终端失败: {e}")
64
+ return False
65
+
66
+ def _start_output_thread(self):
67
+ """启动输出读取线程(Windows)"""
68
+ def read_output():
69
+ while self.running and self.process and self.process.poll() is None:
70
+ try:
71
+ line = self.process.stdout.readline()
72
+ if line:
73
+ self.output_queue.put(line)
74
+ except Exception as e:
75
+ print(f"读取输出错误: {e}")
76
+ break
77
+
78
+ threading.Thread(target=read_output, daemon=True).start()
79
+
80
+ def _start_input_thread(self):
81
+ """启动输入处理线程(Windows)"""
82
+ def process_input():
83
+ while self.running and self.process and self.process.poll() is None:
84
+ try:
85
+ if not self.input_queue.empty():
86
+ data = self.input_queue.get(timeout=0.1)
87
+ if self.process.stdin:
88
+ self.process.stdin.write(data)
89
+ self.process.stdin.flush()
90
+ except:
91
+ pass
92
+ time.sleep(0.01)
93
+
94
+ threading.Thread(target=process_input, daemon=True).start()
95
+
96
+ def set_winsize(self, rows, cols):
97
+ """设置终端窗口大小"""
98
+ if not self.is_windows and hasattr(self, 'fd'):
99
+ try:
100
+ import termios
101
+ import struct
102
+ import fcntl
103
+ winsize = struct.pack("HHHH", rows, cols, 0, 0)
104
+ fcntl.ioctl(self.fd, termios.TIOCSWINSZ, winsize)
105
+ except:
106
+ pass
107
+
108
+ def read(self):
109
+ """读取终端输出"""
110
+ try:
111
+ if self.is_windows:
112
+ output = ""
113
+ while not self.output_queue.empty():
114
+ try:
115
+ line = self.output_queue.get_nowait()
116
+ output += line
117
+ except queue.Empty:
118
+ break
119
+ return output
120
+ else:
121
+ # Unix/Linux
122
+ if hasattr(self, 'fd'):
123
+ try:
124
+ import select
125
+ ready, _, _ = select.select([self.fd], [], [], 0.1)
126
+ if ready:
127
+ data = os.read(self.fd, 1024)
128
+ return data.decode('utf-8', errors='ignore')
129
+ except:
130
+ pass
131
+ return ''
132
+ except Exception as e:
133
+ print(f"读取终端输出错误: {e}")
134
+ return ''
135
+
136
+ def write(self, data):
137
+ """写入终端输入"""
138
+ try:
139
+ if self.is_windows:
140
+ self.input_queue.put(data)
141
+ else:
142
+ # Unix/Linux
143
+ if hasattr(self, 'fd'):
144
+ os.write(self.fd, data.encode('utf-8'))
145
+ except Exception as e:
146
+ print(f"写入终端输入错误: {e}")
147
+
148
+ def close(self):
149
+ """关闭终端"""
150
+ try:
151
+ self.running = False
152
+ if self.is_windows:
153
+ if self.process:
154
+ self.process.terminate()
155
+ self.process = None
156
+ else:
157
+ if hasattr(self, 'fd'):
158
+ os.close(self.fd)
159
+ if hasattr(self, 'child_pid'):
160
+ try:
161
+ os.kill(self.child_pid, signal.SIGTERM)
162
+ except:
163
+ pass
164
+ except Exception as e:
165
+ print(f"关闭终端错误: {e}")
166
+
167
+ @app.route('/')
168
+ def index():
169
+ return render_template('index.html')
170
+
171
+ @app.route('/files')
172
+ def file_manager():
173
+ return render_template('files.html')
174
+
175
+ @app.route('/terminal')
176
+ def terminal():
177
+ return render_template('terminal.html')
178
+
179
+ @app.route('/api/files')
180
+ def list_files():
181
+ path = request.args.get('path', '.')
182
+ try:
183
+ # 安全检查:防止路径遍历攻击
184
+ abs_path = os.path.abspath(path)
185
+ if not abs_path.startswith(os.getcwd()):
186
+ # 限制在当前工作目录内
187
+ abs_path = os.getcwd()
188
+
189
+ items = []
190
+ for item in os.listdir(abs_path):
191
+ item_path = os.path.join(abs_path, item)
192
+ try:
193
+ stat = os.stat(item_path)
194
+ items.append({
195
+ 'name': item,
196
+ 'type': 'directory' if os.path.isdir(item_path) else 'file',
197
+ 'size': stat.st_size,
198
+ 'modified': datetime.fromtimestamp(stat.st_mtime).strftime('%Y-%m-%d %H:%M:%S'),
199
+ 'path': os.path.relpath(item_path, os.getcwd()).replace('\\', '/')
200
+ })
201
+ except:
202
+ continue
203
+
204
+ return jsonify({
205
+ 'success': True,
206
+ 'path': os.path.relpath(abs_path, os.getcwd()).replace('\\', '/'),
207
+ 'items': sorted(items, key=lambda x: (x['type'] != 'directory', x['name'].lower()))
208
+ })
209
+ except Exception as e:
210
+ return jsonify({'success': False, 'error': str(e)})
211
+
212
+ @app.route('/api/files/create', methods=['POST'])
213
+ def create_file():
214
+ data = request.json
215
+ try:
216
+ file_path = data['path']
217
+ # 安全检查
218
+ abs_path = os.path.abspath(file_path)
219
+ if not abs_path.startswith(os.getcwd()):
220
+ return jsonify({'success': False, 'error': '路径不被允许'})
221
+
222
+ if data['type'] == 'directory':
223
+ os.makedirs(abs_path, exist_ok=True)
224
+ else:
225
+ os.makedirs(os.path.dirname(abs_path), exist_ok=True)
226
+ with open(abs_path, 'w', encoding='utf-8') as f:
227
+ f.write(data.get('content', ''))
228
+ return jsonify({'success': True})
229
+ except Exception as e:
230
+ return jsonify({'success': False, 'error': str(e)})
231
+
232
+ @app.route('/api/files/delete', methods=['POST'])
233
+ def delete_file():
234
+ data = request.json
235
+ try:
236
+ file_path = data['path']
237
+ abs_path = os.path.abspath(file_path)
238
+ if not abs_path.startswith(os.getcwd()):
239
+ return jsonify({'success': False, 'error': '路径不被允许'})
240
+
241
+ if os.path.isdir(abs_path):
242
+ import shutil
243
+ shutil.rmtree(abs_path)
244
+ else:
245
+ os.remove(abs_path)
246
+ return jsonify({'success': True})
247
+ except Exception as e:
248
+ return jsonify({'success': False, 'error': str(e)})
249
+
250
+ @app.route('/api/files/rename', methods=['POST'])
251
+ def rename_file():
252
+ data = request.json
253
+ try:
254
+ old_path = os.path.abspath(data['oldPath'])
255
+ new_path = os.path.abspath(data['newPath'])
256
+
257
+ if not old_path.startswith(os.getcwd()) or not new_path.startswith(os.getcwd()):
258
+ return jsonify({'success': False, 'error': '路径不被允许'})
259
+
260
+ os.rename(old_path, new_path)
261
+ return jsonify({'success': True})
262
+ except Exception as e:
263
+ return jsonify({'success': False, 'error': str(e)})
264
+
265
+ @app.route('/api/files/read')
266
+ def read_file():
267
+ file_path = request.args.get('path')
268
+ try:
269
+ abs_path = os.path.abspath(file_path)
270
+ if not abs_path.startswith(os.getcwd()):
271
+ return jsonify({'success': False, 'error': '路径不被允许'})
272
+
273
+ with open(abs_path, 'r', encoding='utf-8') as f:
274
+ content = f.read()
275
+ return jsonify({'success': True, 'content': content})
276
+ except UnicodeDecodeError:
277
+ try:
278
+ with open(abs_path, 'r', encoding='gbk') as f:
279
+ content = f.read()
280
+ return jsonify({'success': True, 'content': content})
281
+ except:
282
+ return jsonify({'success': False, 'error': '无法读取文件,可能是二进制文件'})
283
+ except Exception as e:
284
+ return jsonify({'success': False, 'error': str(e)})
285
+
286
+ @app.route('/api/files/write', methods=['POST'])
287
+ def write_file():
288
+ data = request.json
289
+ try:
290
+ file_path = data['path']
291
+ content = data['content']
292
+ abs_path = os.path.abspath(file_path)
293
+ if not abs_path.startswith(os.getcwd()):
294
+ return jsonify({'success': False, 'error': '路径不被允许'})
295
+
296
+ with open(abs_path, 'w', encoding='utf-8') as f:
297
+ f.write(content)
298
+ return jsonify({'success': True})
299
+ except Exception as e:
300
+ return jsonify({'success': False, 'error': str(e)})
301
+
302
+ @app.route('/api/files/download')
303
+ def download_file():
304
+ file_path = request.args.get('path')
305
+ try:
306
+ abs_path = os.path.abspath(file_path)
307
+ if not abs_path.startswith(os.getcwd()):
308
+ abort(403)
309
+ return send_file(abs_path, as_attachment=True)
310
+ except Exception as e:
311
+ abort(404)
312
+
313
+ @app.route('/api/files/upload', methods=['POST'])
314
+ def upload_file():
315
+ try:
316
+ file = request.files['file']
317
+ path = request.form.get('path', '.')
318
+ abs_path = os.path.abspath(path)
319
+ if not abs_path.startswith(os.getcwd()):
320
+ return jsonify({'success': False, 'error': '路径不被允许'})
321
+
322
+ file_path = os.path.join(abs_path, file.filename)
323
+ file.save(file_path)
324
+ return jsonify({'success': True})
325
+ except Exception as e:
326
+ return jsonify({'success': False, 'error': str(e)})
327
+
328
+ # WebSocket事件处理
329
+ @socketio.on('connect')
330
+ def on_connect():
331
+ print(f'客户端连接: {request.sid}')
332
+
333
+ @socketio.on('disconnect')
334
+ def on_disconnect():
335
+ print(f'客户端断开: {request.sid}')
336
+ # 清理终端进程
337
+ if request.sid in terminals:
338
+ terminals[request.sid].close()
339
+ del terminals[request.sid]
340
+
341
+ @socketio.on('start_terminal')
342
+ def on_start_terminal():
343
+ session_id = request.sid
344
+ terminal = Terminal(session_id)
345
+
346
+ if terminal.spawn():
347
+ terminals[session_id] = terminal
348
+
349
+ # 发送初始提示
350
+ if os.name == 'nt':
351
+ socketio.emit('terminal_output', {'data': f'Microsoft Windows [版本 {os.environ.get("OS", "Windows")}]\n'}, room=session_id)
352
+ socketio.emit('terminal_output', {'data': f'(c) Microsoft Corporation. 保留所有权利。\n\n'}, room=session_id)
353
+ socketio.emit('terminal_output', {'data': f'{os.getcwd()}>'}, room=session_id)
354
+ else:
355
+ socketio.emit('terminal_output', {'data': f'WebShell Terminal - Linux\n'}, room=session_id)
356
+ socketio.emit('terminal_output', {'data': f'Current directory: {os.getcwd()}\n'}, room=session_id)
357
+ socketio.emit('terminal_output', {'data': f'$ '}, room=session_id)
358
+
359
+ # 启动读取线程
360
+ def read_thread():
361
+ while session_id in terminals and terminals[session_id].running:
362
+ try:
363
+ terminal_instance = terminals[session_id]
364
+ output = terminal_instance.read()
365
+ if output:
366
+ socketio.emit('terminal_output', {'data': output}, room=session_id)
367
+ time.sleep(0.1)
368
+ except Exception as e:
369
+ print(f"读取线程错误: {e}")
370
+ break
371
+
372
+ threading.Thread(target=read_thread, daemon=True).start()
373
+ emit('terminal_ready')
374
+ else:
375
+ emit('terminal_error', {'error': '无法启动终端'})
376
+
377
+ @socketio.on('terminal_input')
378
+ def on_terminal_input(data):
379
+ session_id = request.sid
380
+ if session_id in terminals:
381
+ command = data['data']
382
+ print(f"收到命令: {repr(command)}")
383
+ terminals[session_id].write(command)
384
+
385
+ @socketio.on('terminal_resize')
386
+ def on_terminal_resize(data):
387
+ session_id = request.sid
388
+ if session_id in terminals:
389
+ terminals[session_id].set_winsize(data['rows'], data['cols'])
390
+
391
+ # 错误处理
392
+ @app.errorhandler(404)
393
+ def not_found(error):
394
+ return jsonify({'success': False, 'error': '页面未找到'}), 404
395
+
396
+ @app.errorhandler(500)
397
+ def internal_error(error):
398
+ return jsonify({'success': False, 'error': '内部服务器错误'}), 500
399
+
400
+ if __name__ == '__main__':
401
+ # 从环境变量获取端口,默认7860(Hugging Face Spaces标准端口)
402
+ port = int(os.environ.get('PORT', 7860))
403
+ host = os.environ.get('HOST', '0.0.0.0')
404
+ debug = os.environ.get('DEBUG', 'False').lower() == 'true'
405
+
406
+ print(f"🚀 WebShell 启动在 {host}:{port}")
407
+ socketio.run(app, host=host, port=port, debug=debug, allow_unsafe_werkzeug=True)
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ Flask==2.3.3
2
+ Flask-SocketIO==5.3.6
3
+ python-socketio==5.8.0
4
+ eventlet==0.33.3
5
+ Werkzeug==2.3.7
run.py ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ WebShell 系统启动脚本
5
+ """
6
+
7
+ import os
8
+ import sys
9
+ import logging
10
+ from app import app, socketio
11
+
12
+ def setup_logging():
13
+ """设置日志"""
14
+ logging.basicConfig(
15
+ level=logging.INFO,
16
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
17
+ handlers=[
18
+ logging.StreamHandler(sys.stdout),
19
+ logging.FileHandler('webshell.log', encoding='utf-8')
20
+ ]
21
+ )
22
+
23
+ def check_dependencies():
24
+ """检查依赖"""
25
+ try:
26
+ import flask
27
+ import flask_socketio
28
+ print("✓ 依赖检查通过")
29
+ return True
30
+ except ImportError as e:
31
+ print(f"✗ 缺少依赖: {e}")
32
+ print("请运行: pip install -r requirements.txt")
33
+ return False
34
+
35
+ def main():
36
+ """主函数"""
37
+ print("="*50)
38
+ print("🚀 WebShell 管理系统")
39
+ print("="*50)
40
+
41
+ # 检查依赖
42
+ if not check_dependencies():
43
+ sys.exit(1)
44
+
45
+ # 设置日志
46
+ setup_logging()
47
+
48
+ # 获取配置
49
+ host = os.environ.get('HOST', '0.0.0.0')
50
+ port = int(os.environ.get('PORT', 5000))
51
+ debug = os.environ.get('DEBUG', 'True').lower() == 'true'
52
+
53
+ print(f"📡 服务器地址: http://{host}:{port}")
54
+ print(f"🔧 调试模式: {'开启' if debug else '关闭'}")
55
+ print(f"📁 工作目录: {os.getcwd()}")
56
+ print("="*50)
57
+ print("💡 使用说明:")
58
+ print(" - 在浏览器中访问上述地址")
59
+ print(" - 按 Ctrl+C 停止服务器")
60
+ print("="*50)
61
+
62
+ try:
63
+ # 启动服务器
64
+ socketio.run(
65
+ app,
66
+ host=host,
67
+ port=port,
68
+ debug=debug,
69
+ allow_unsafe_werkzeug=True
70
+ )
71
+ except KeyboardInterrupt:
72
+ print("\n👋 服务器已停止")
73
+ except Exception as e:
74
+ print(f"❌ 启动失败: {e}")
75
+ sys.exit(1)
76
+
77
+ if __name__ == '__main__':
78
+ main()
templates/files.html ADDED
@@ -0,0 +1,646 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>文件管理 - WebShell</title>
7
+ <link href="https://cdn.bootcdn.net/ajax/libs/bootstrap/5.3.0/css/bootstrap.min.css" rel="stylesheet">
8
+ <link href="https://cdn.bootcdn.net/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
9
+ <link href="https://cdn.bootcdn.net/ajax/libs/codemirror/5.65.2/codemirror.min.css" rel="stylesheet">
10
+ <link href="https://cdn.bootcdn.net/ajax/libs/codemirror/5.65.2/theme/monokai.min.css" rel="stylesheet">
11
+ <style>
12
+ body {
13
+ background: #f8f9fa;
14
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
15
+ }
16
+
17
+ .navbar {
18
+ box-shadow: 0 2px 4px rgba(0,0,0,.1);
19
+ }
20
+
21
+ .file-item {
22
+ cursor: pointer;
23
+ transition: background-color 0.2s;
24
+ }
25
+
26
+ .file-item:hover {
27
+ background-color: #f8f9fa;
28
+ }
29
+
30
+ .file-item.selected {
31
+ background-color: #e3f2fd;
32
+ }
33
+
34
+ .breadcrumb {
35
+ background: white;
36
+ border-radius: 8px;
37
+ box-shadow: 0 1px 3px rgba(0,0,0,.1);
38
+ }
39
+
40
+ .file-icon {
41
+ width: 20px;
42
+ text-align: center;
43
+ }
44
+
45
+ .CodeMirror {
46
+ border: 1px solid #ddd;
47
+ border-radius: 4px;
48
+ height: 400px;
49
+ }
50
+
51
+ .toolbar {
52
+ background: white;
53
+ border-radius: 8px;
54
+ box-shadow: 0 1px 3px rgba(0,0,0,.1);
55
+ padding: 1rem;
56
+ margin-bottom: 1rem;
57
+ }
58
+ </style>
59
+ </head>
60
+ <body>
61
+ <!-- 导航栏 -->
62
+ <nav class="navbar navbar-expand-lg navbar-dark bg-dark">
63
+ <div class="container-fluid">
64
+ <a class="navbar-brand" href="/">
65
+ <i class="fas fa-folder-open me-2"></i>文件管理
66
+ </a>
67
+ <div class="navbar-nav ms-auto">
68
+ <a class="nav-link" href="/terminal">
69
+ <i class="fas fa-terminal me-2"></i>终端
70
+ </a>
71
+ <a class="nav-link" href="/">
72
+ <i class="fas fa-home me-2"></i>主页
73
+ </a>
74
+ </div>
75
+ </div>
76
+ </nav>
77
+
78
+ <div class="container-fluid py-3">
79
+ <div class="row">
80
+ <!-- 文件列表 -->
81
+ <div class="col-md-8">
82
+ <!-- 路径导航 -->
83
+ <nav aria-label="breadcrumb" class="mb-3">
84
+ <ol class="breadcrumb p-3 mb-0" id="breadcrumb">
85
+ <li class="breadcrumb-item"><a href="#" onclick="loadFiles('.')">根目录</a></li>
86
+ </ol>
87
+ </nav>
88
+
89
+ <!-- 工具栏 -->
90
+ <div class="toolbar d-flex justify-content-between align-items-center">
91
+ <div>
92
+ <button class="btn btn-primary btn-sm" onclick="showCreateDialog()">
93
+ <i class="fas fa-plus me-2"></i>新建
94
+ </button>
95
+ <button class="btn btn-success btn-sm" onclick="document.getElementById('uploadFile').click()">
96
+ <i class="fas fa-upload me-2"></i>上传
97
+ </button>
98
+ <button class="btn btn-warning btn-sm" onclick="renameSelected()" id="renameBtn" disabled>
99
+ <i class="fas fa-edit me-2"></i>重命名
100
+ </button>
101
+ <button class="btn btn-danger btn-sm" onclick="deleteSelected()" id="deleteBtn" disabled>
102
+ <i class="fas fa-trash me-2"></i>删除
103
+ </button>
104
+ </div>
105
+ <div>
106
+ <button class="btn btn-outline-secondary btn-sm" onclick="refreshFiles()">
107
+ <i class="fas fa-sync me-2"></i>刷新
108
+ </button>
109
+ </div>
110
+ </div>
111
+
112
+ <!-- 文件列表 -->
113
+ <div class="card">
114
+ <div class="table-responsive">
115
+ <table class="table table-hover mb-0">
116
+ <thead class="table-light">
117
+ <tr>
118
+ <th width="40"></th>
119
+ <th>名称</th>
120
+ <th width="100">大小</th>
121
+ <th width="150">修改时间</th>
122
+ <th width="100">操作</th>
123
+ </tr>
124
+ </thead>
125
+ <tbody id="fileList">
126
+ <!-- 文件列表将在这里动态生成 -->
127
+ </tbody>
128
+ </table>
129
+ </div>
130
+ </div>
131
+ </div>
132
+
133
+ <!-- 预览/编辑面板 -->
134
+ <div class="col-md-4">
135
+ <div class="card">
136
+ <div class="card-header">
137
+ <h6 class="mb-0" id="previewTitle">选择文件进行预览</h6>
138
+ </div>
139
+ <div class="card-body" id="previewContent">
140
+ <div class="text-center text-muted">
141
+ <i class="fas fa-file fa-3x mb-3"></i>
142
+ <p>选择一个文件来预览或编辑其内容</p>
143
+ </div>
144
+ </div>
145
+ </div>
146
+ </div>
147
+ </div>
148
+ </div>
149
+
150
+ <!-- 上传文件隐藏input -->
151
+ <input type="file" id="uploadFile" style="display: none" multiple onchange="uploadFiles(this.files)">
152
+
153
+ <!-- 新建文件/文件夹模态框 -->
154
+ <div class="modal fade" id="createModal" tabindex="-1">
155
+ <div class="modal-dialog">
156
+ <div class="modal-content">
157
+ <div class="modal-header">
158
+ <h5 class="modal-title">新建</h5>
159
+ <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
160
+ </div>
161
+ <div class="modal-body">
162
+ <div class="mb-3">
163
+ <label class="form-label">类型</label>
164
+ <select class="form-select" id="createType">
165
+ <option value="file">文件</option>
166
+ <option value="directory">文件夹</option>
167
+ </select>
168
+ </div>
169
+ <div class="mb-3">
170
+ <label class="form-label">名称</label>
171
+ <input type="text" class="form-control" id="createName" placeholder="输入名称">
172
+ </div>
173
+ </div>
174
+ <div class="modal-footer">
175
+ <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
176
+ <button type="button" class="btn btn-primary" onclick="createItem()">创建</button>
177
+ </div>
178
+ </div>
179
+ </div>
180
+ </div>
181
+
182
+ <!-- 编辑器模态框 -->
183
+ <div class="modal fade" id="editorModal" tabindex="-1">
184
+ <div class="modal-dialog modal-xl">
185
+ <div class="modal-content">
186
+ <div class="modal-header">
187
+ <h5 class="modal-title" id="editorTitle">编辑文件</h5>
188
+ <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
189
+ </div>
190
+ <div class="modal-body p-0">
191
+ <textarea id="fileEditor"></textarea>
192
+ </div>
193
+ <div class="modal-footer">
194
+ <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
195
+ <button type="button" class="btn btn-primary" onclick="saveFile()">保存</button>
196
+ </div>
197
+ </div>
198
+ </div>
199
+ </div>
200
+
201
+ <script src="https://cdn.bootcdn.net/ajax/libs/bootstrap/5.3.0/js/bootstrap.bundle.min.js"></script>
202
+ <script src="https://cdn.bootcdn.net/ajax/libs/codemirror/5.65.2/codemirror.min.js"></script>
203
+ <script src="https://cdn.bootcdn.net/ajax/libs/codemirror/5.65.2/mode/javascript/javascript.min.js"></script>
204
+ <script src="https://cdn.bootcdn.net/ajax/libs/codemirror/5.65.2/mode/python/python.min.js"></script>
205
+ <script src="https://cdn.bootcdn.net/ajax/libs/codemirror/5.65.2/mode/xml/xml.min.js"></script>
206
+ <script src="https://cdn.bootcdn.net/ajax/libs/codemirror/5.65.2/mode/css/css.min.js"></script>
207
+ <script src="https://cdn.bootcdn.net/ajax/libs/codemirror/5.65.2/mode/htmlmixed/htmlmixed.min.js"></script>
208
+
209
+ <script>
210
+ let currentPath = '.';
211
+ let selectedFile = null;
212
+ let editor = null;
213
+ let currentEditFile = null;
214
+
215
+ // 页面加载时初始化
216
+ document.addEventListener('DOMContentLoaded', function() {
217
+ loadFiles('.');
218
+ initEditor();
219
+ });
220
+
221
+ // 初始化编辑器
222
+ function initEditor() {
223
+ editor = CodeMirror.fromTextArea(document.getElementById('fileEditor'), {
224
+ theme: 'monokai',
225
+ lineNumbers: true,
226
+ mode: 'text/plain',
227
+ indentUnit: 4,
228
+ lineWrapping: true
229
+ });
230
+ }
231
+
232
+ // 加载文件列表
233
+ function loadFiles(path) {
234
+ currentPath = path;
235
+ fetch(`/api/files?path=${encodeURIComponent(path)}`)
236
+ .then(response => response.json())
237
+ .then(data => {
238
+ if (data.success) {
239
+ renderFileList(data.items);
240
+ updateBreadcrumb(data.path);
241
+ } else {
242
+ alert('加载文件失败: ' + data.error);
243
+ }
244
+ })
245
+ .catch(error => {
246
+ alert('网络错误: ' + error.message);
247
+ });
248
+ }
249
+
250
+ // 渲染文件列表
251
+ function renderFileList(items) {
252
+ const fileList = document.getElementById('fileList');
253
+ fileList.innerHTML = '';
254
+
255
+ // 添加上级目录
256
+ if (currentPath !== '.') {
257
+ const row = document.createElement('tr');
258
+ row.className = 'file-item';
259
+ row.innerHTML = `
260
+ <td><i class="fas fa-level-up-alt file-icon"></i></td>
261
+ <td><a href="#" onclick="loadFiles('${getParentPath(currentPath)}')">..</a></td>
262
+ <td>-</td>
263
+ <td>-</td>
264
+ <td>-</td>
265
+ `;
266
+ fileList.appendChild(row);
267
+ }
268
+
269
+ // 添加文件和文件夹
270
+ items.forEach(item => {
271
+ const row = document.createElement('tr');
272
+ row.className = 'file-item';
273
+ row.onclick = () => selectFile(row, item);
274
+ row.ondblclick = () => {
275
+ if (item.type === 'directory') {
276
+ loadFiles(item.path);
277
+ } else {
278
+ editFile(item.path);
279
+ }
280
+ };
281
+
282
+ const icon = item.type === 'directory' ? 'fa-folder' : getFileIcon(item.name);
283
+ const size = item.type === 'directory' ? '-' : formatFileSize(item.size);
284
+
285
+ row.innerHTML = `
286
+ <td><i class="fas ${icon} file-icon"></i></td>
287
+ <td>${item.name}</td>
288
+ <td>${size}</td>
289
+ <td>${item.modified}</td>
290
+ <td>
291
+ ${item.type === 'file' ? `
292
+ <button class="btn btn-sm btn-outline-primary" onclick="event.stopPropagation(); editFile('${item.path}')">
293
+ <i class="fas fa-edit"></i>
294
+ </button>
295
+ <button class="btn btn-sm btn-outline-success" onclick="event.stopPropagation(); downloadFile('${item.path}')">
296
+ <i class="fas fa-download"></i>
297
+ </button>
298
+ ` : ''}
299
+ </td>
300
+ `;
301
+ fileList.appendChild(row);
302
+ });
303
+ }
304
+
305
+ // 选择文件
306
+ function selectFile(row, item) {
307
+ // 移除之前的选中状态
308
+ document.querySelectorAll('.file-item').forEach(r => r.classList.remove('selected'));
309
+ row.classList.add('selected');
310
+
311
+ selectedFile = item;
312
+ updateButtons();
313
+
314
+ if (item.type === 'file') {
315
+ previewFile(item.path);
316
+ }
317
+ }
318
+
319
+ // 更新按钮状态
320
+ function updateButtons() {
321
+ const renameBtn = document.getElementById('renameBtn');
322
+ const deleteBtn = document.getElementById('deleteBtn');
323
+
324
+ if (selectedFile) {
325
+ renameBtn.disabled = false;
326
+ deleteBtn.disabled = false;
327
+ } else {
328
+ renameBtn.disabled = true;
329
+ deleteBtn.disabled = true;
330
+ }
331
+ }
332
+
333
+ // 预览文件
334
+ function previewFile(filePath) {
335
+ const previewTitle = document.getElementById('previewTitle');
336
+ const previewContent = document.getElementById('previewContent');
337
+
338
+ previewTitle.textContent = `预览: ${filePath.split('/').pop()}`;
339
+
340
+ fetch(`/api/files/read?path=${encodeURIComponent(filePath)}`)
341
+ .then(response => response.json())
342
+ .then(data => {
343
+ if (data.success) {
344
+ const content = data.content;
345
+ if (content.length > 1000) {
346
+ previewContent.innerHTML = `
347
+ <div class="alert alert-info">
348
+ <p><strong>文件太大,只显示前1000个字符</strong></p>
349
+ <pre style="max-height: 300px; overflow-y: auto;">${escapeHtml(content.substring(0, 1000))}...</pre>
350
+ <button class="btn btn-primary btn-sm mt-2" onclick="editFile('${filePath}')">
351
+ <i class="fas fa-edit me-2"></i>编辑完整文件
352
+ </button>
353
+ </div>
354
+ `;
355
+ } else {
356
+ previewContent.innerHTML = `
357
+ <pre style="max-height: 400px; overflow-y: auto; background: #f8f9fa; padding: 1rem; border-radius: 4px;">${escapeHtml(content)}</pre>
358
+ <button class="btn btn-primary btn-sm mt-2" onclick="editFile('${filePath}')">
359
+ <i class="fas fa-edit me-2"></i>编辑文件
360
+ </button>
361
+ `;
362
+ }
363
+ } else {
364
+ previewContent.innerHTML = `
365
+ <div class="alert alert-warning">
366
+ 无法预览此文件: ${data.error}
367
+ </div>
368
+ `;
369
+ }
370
+ })
371
+ .catch(error => {
372
+ previewContent.innerHTML = `
373
+ <div class="alert alert-danger">
374
+ 预览失败: ${error.message}
375
+ </div>
376
+ `;
377
+ });
378
+ }
379
+
380
+ // 编辑文件
381
+ function editFile(filePath) {
382
+ currentEditFile = filePath;
383
+
384
+ fetch(`/api/files/read?path=${encodeURIComponent(filePath)}`)
385
+ .then(response => response.json())
386
+ .then(data => {
387
+ if (data.success) {
388
+ editor.setValue(data.content);
389
+ editor.setOption('mode', getEditorMode(filePath));
390
+ document.getElementById('editorTitle').textContent = `编辑: ${filePath.split('/').pop()}`;
391
+ new bootstrap.Modal(document.getElementById('editorModal')).show();
392
+ } else {
393
+ alert('读取文件失败: ' + data.error);
394
+ }
395
+ })
396
+ .catch(error => {
397
+ alert('网络错误: ' + error.message);
398
+ });
399
+ }
400
+
401
+ // 保存文件
402
+ function saveFile() {
403
+ if (!currentEditFile) return;
404
+
405
+ const content = editor.getValue();
406
+
407
+ fetch('/api/files/write', {
408
+ method: 'POST',
409
+ headers: {
410
+ 'Content-Type': 'application/json',
411
+ },
412
+ body: JSON.stringify({
413
+ path: currentEditFile,
414
+ content: content
415
+ })
416
+ })
417
+ .then(response => response.json())
418
+ .then(data => {
419
+ if (data.success) {
420
+ bootstrap.Modal.getInstance(document.getElementById('editorModal')).hide();
421
+ refreshFiles();
422
+ } else {
423
+ alert('保存失败: ' + data.error);
424
+ }
425
+ })
426
+ .catch(error => {
427
+ alert('网络错误: ' + error.message);
428
+ });
429
+ }
430
+
431
+ // 显示创建对话框
432
+ function showCreateDialog() {
433
+ new bootstrap.Modal(document.getElementById('createModal')).show();
434
+ }
435
+
436
+ // 创建文件或文件夹
437
+ function createItem() {
438
+ const type = document.getElementById('createType').value;
439
+ const name = document.getElementById('createName').value.trim();
440
+
441
+ if (!name) {
442
+ alert('请输入名称');
443
+ return;
444
+ }
445
+
446
+ const path = currentPath === '.' ? name : `${currentPath}/${name}`;
447
+
448
+ fetch('/api/files/create', {
449
+ method: 'POST',
450
+ headers: {
451
+ 'Content-Type': 'application/json',
452
+ },
453
+ body: JSON.stringify({
454
+ path: path,
455
+ type: type
456
+ })
457
+ })
458
+ .then(response => response.json())
459
+ .then(data => {
460
+ if (data.success) {
461
+ bootstrap.Modal.getInstance(document.getElementById('createModal')).hide();
462
+ document.getElementById('createName').value = '';
463
+ refreshFiles();
464
+ } else {
465
+ alert('创建失败: ' + data.error);
466
+ }
467
+ })
468
+ .catch(error => {
469
+ alert('网络错误: ' + error.message);
470
+ });
471
+ }
472
+
473
+ // 上传文件
474
+ function uploadFiles(files) {
475
+ Array.from(files).forEach(file => {
476
+ const formData = new FormData();
477
+ formData.append('file', file);
478
+ formData.append('path', currentPath);
479
+
480
+ fetch('/api/files/upload', {
481
+ method: 'POST',
482
+ body: formData
483
+ })
484
+ .then(response => response.json())
485
+ .then(data => {
486
+ if (data.success) {
487
+ refreshFiles();
488
+ } else {
489
+ alert(`上传 ${file.name} 失败: ${data.error}`);
490
+ }
491
+ })
492
+ .catch(error => {
493
+ alert(`上传 ${file.name} 网络错误: ${error.message}`);
494
+ });
495
+ });
496
+ }
497
+
498
+ // 重命名选中文件
499
+ function renameSelected() {
500
+ if (!selectedFile) return;
501
+
502
+ const newName = prompt('请输入新名称:', selectedFile.name);
503
+ if (!newName || newName === selectedFile.name) return;
504
+
505
+ const newPath = selectedFile.path.replace(/[^/\\]*$/, newName);
506
+
507
+ fetch('/api/files/rename', {
508
+ method: 'POST',
509
+ headers: {
510
+ 'Content-Type': 'application/json',
511
+ },
512
+ body: JSON.stringify({
513
+ oldPath: selectedFile.path,
514
+ newPath: newPath
515
+ })
516
+ })
517
+ .then(response => response.json())
518
+ .then(data => {
519
+ if (data.success) {
520
+ refreshFiles();
521
+ selectedFile = null;
522
+ updateButtons();
523
+ } else {
524
+ alert('重命名失败: ' + data.error);
525
+ }
526
+ })
527
+ .catch(error => {
528
+ alert('网络错误: ' + error.message);
529
+ });
530
+ }
531
+
532
+ // 删除选中文件
533
+ function deleteSelected() {
534
+ if (!selectedFile) return;
535
+
536
+ if (!confirm(`确定要删除 "${selectedFile.name}" 吗?`)) return;
537
+
538
+ fetch('/api/files/delete', {
539
+ method: 'POST',
540
+ headers: {
541
+ 'Content-Type': 'application/json',
542
+ },
543
+ body: JSON.stringify({
544
+ path: selectedFile.path
545
+ })
546
+ })
547
+ .then(response => response.json())
548
+ .then(data => {
549
+ if (data.success) {
550
+ refreshFiles();
551
+ selectedFile = null;
552
+ updateButtons();
553
+ } else {
554
+ alert('删除失败: ' + data.error);
555
+ }
556
+ })
557
+ .catch(error => {
558
+ alert('网络错误: ' + error.message);
559
+ });
560
+ }
561
+
562
+ // 下载文件
563
+ function downloadFile(filePath) {
564
+ window.open(`/api/files/download?path=${encodeURIComponent(filePath)}`);
565
+ }
566
+
567
+ // 刷新文件列表
568
+ function refreshFiles() {
569
+ loadFiles(currentPath);
570
+ }
571
+
572
+ // 工具函数
573
+ function getParentPath(path) {
574
+ const parts = path.split('/').filter(p => p && p !== '.');
575
+ parts.pop();
576
+ return parts.length > 0 ? parts.join('/') : '.';
577
+ }
578
+
579
+ function getFileIcon(filename) {
580
+ const ext = filename.split('.').pop().toLowerCase();
581
+ const iconMap = {
582
+ 'txt': 'fa-file-alt',
583
+ 'js': 'fa-file-code',
584
+ 'html': 'fa-file-code',
585
+ 'css': 'fa-file-code',
586
+ 'py': 'fa-file-code',
587
+ 'jpg': 'fa-file-image',
588
+ 'png': 'fa-file-image',
589
+ 'gif': 'fa-file-image',
590
+ 'pdf': 'fa-file-pdf',
591
+ 'zip': 'fa-file-archive',
592
+ 'rar': 'fa-file-archive'
593
+ };
594
+ return iconMap[ext] || 'fa-file';
595
+ }
596
+
597
+ function formatFileSize(bytes) {
598
+ if (bytes === 0) return '0 B';
599
+ const k = 1024;
600
+ const sizes = ['B', 'KB', 'MB', 'GB'];
601
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
602
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
603
+ }
604
+
605
+ function getEditorMode(filename) {
606
+ const ext = filename.split('.').pop().toLowerCase();
607
+ const modeMap = {
608
+ 'js': 'javascript',
609
+ 'html': 'htmlmixed',
610
+ 'css': 'css',
611
+ 'py': 'python',
612
+ 'xml': 'xml',
613
+ 'json': 'javascript'
614
+ };
615
+ return modeMap[ext] || 'text/plain';
616
+ }
617
+
618
+ function updateBreadcrumb(path) {
619
+ const breadcrumb = document.getElementById('breadcrumb');
620
+ breadcrumb.innerHTML = '<li class="breadcrumb-item"><a href="#" onclick="loadFiles(\'.\')">���目录</a></li>';
621
+
622
+ if (path !== '.' && path !== '') {
623
+ const parts = path.split('/').filter(p => p);
624
+ let currentBreadcrumbPath = '';
625
+
626
+ parts.forEach((part, index) => {
627
+ currentBreadcrumbPath += (currentBreadcrumbPath ? '/' : '') + part;
628
+ const isLast = index === parts.length - 1;
629
+
630
+ if (isLast) {
631
+ breadcrumb.innerHTML += `<li class="breadcrumb-item active">${part}</li>`;
632
+ } else {
633
+ breadcrumb.innerHTML += `<li class="breadcrumb-item"><a href="#" onclick="loadFiles('${currentBreadcrumbPath}')">${part}</a></li>`;
634
+ }
635
+ });
636
+ }
637
+ }
638
+
639
+ function escapeHtml(text) {
640
+ const div = document.createElement('div');
641
+ div.textContent = text;
642
+ return div.innerHTML;
643
+ }
644
+ </script>
645
+ </body>
646
+ </html>
templates/index.html ADDED
@@ -0,0 +1,154 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>WebShell 系统</title>
7
+ <link href="https://cdn.bootcdn.net/ajax/libs/bootstrap/5.3.0/css/bootstrap.min.css" rel="stylesheet">
8
+ <link href="https://cdn.bootcdn.net/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
9
+ <style>
10
+ body {
11
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
12
+ min-height: 100vh;
13
+ font-family: 'Arial', sans-serif;
14
+ }
15
+
16
+ .card {
17
+ border: none;
18
+ border-radius: 15px;
19
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
20
+ transition: transform 0.3s ease, box-shadow 0.3s ease;
21
+ }
22
+
23
+ .card:hover {
24
+ transform: translateY(-5px);
25
+ box-shadow: 0 20px 40px rgba(0, 0, 0, 0.2);
26
+ }
27
+
28
+ .feature-icon {
29
+ font-size: 4rem;
30
+ margin-bottom: 1rem;
31
+ background: linear-gradient(45deg, #667eea, #764ba2);
32
+ -webkit-background-clip: text;
33
+ -webkit-text-fill-color: transparent;
34
+ background-clip: text;
35
+ }
36
+
37
+ .btn-custom {
38
+ background: linear-gradient(45deg, #667eea, #764ba2);
39
+ border: none;
40
+ border-radius: 25px;
41
+ padding: 12px 30px;
42
+ color: white;
43
+ font-weight: 600;
44
+ transition: all 0.3s ease;
45
+ }
46
+
47
+ .btn-custom:hover {
48
+ transform: translateY(-2px);
49
+ box-shadow: 0 10px 20px rgba(0, 0, 0, 0.2);
50
+ color: white;
51
+ }
52
+
53
+ .main-title {
54
+ color: white;
55
+ text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
56
+ margin-bottom: 3rem;
57
+ }
58
+ </style>
59
+ </head>
60
+ <body>
61
+ <div class="container py-5">
62
+ <div class="row justify-content-center">
63
+ <div class="col-md-10">
64
+ <h1 class="text-center main-title mb-5">
65
+ <i class="fas fa-terminal me-3"></i>WebShell 管理系统
66
+ </h1>
67
+
68
+ <div class="row g-4">
69
+ <!-- 文件管理卡片 -->
70
+ <div class="col-md-6">
71
+ <div class="card h-100 text-center p-4">
72
+ <div class="card-body">
73
+ <i class="fas fa-folder-open feature-icon"></i>
74
+ <h3 class="card-title mb-3">文件管理</h3>
75
+ <p class="card-text text-muted mb-4">
76
+ 浏览、上传、下载、编辑和管理服务器文件。支持创建文件夹、重命名、删除等操作。
77
+ </p>
78
+ <ul class="list-unstyled text-start mb-4">
79
+ <li><i class="fas fa-check text-success me-2"></i>文件浏览和导航</li>
80
+ <li><i class="fas fa-check text-success me-2"></i>文件上传下载</li>
81
+ <li><i class="fas fa-check text-success me-2"></i>在线编辑器</li>
82
+ <li><i class="fas fa-check text-success me-2"></i>文件操作管理</li>
83
+ </ul>
84
+ <a href="/files" class="btn btn-custom">
85
+ <i class="fas fa-folder me-2"></i>进入文件管理
86
+ </a>
87
+ </div>
88
+ </div>
89
+ </div>
90
+
91
+ <!-- 终端模拟器卡片 -->
92
+ <div class="col-md-6">
93
+ <div class="card h-100 text-center p-4">
94
+ <div class="card-body">
95
+ <i class="fas fa-terminal feature-icon"></i>
96
+ <h3 class="card-title mb-3">终端模拟器</h3>
97
+ <p class="card-text text-muted mb-4">
98
+ 实时交互式终端,支持命令执行、进程管理。提供完整的Shell体验。
99
+ </p>
100
+ <ul class="list-unstyled text-start mb-4">
101
+ <li><i class="fas fa-check text-success me-2"></i>实时终端交互</li>
102
+ <li><i class="fas fa-check text-success me-2"></i>命令历史记录</li>
103
+ <li><i class="fas fa-check text-success me-2"></i>多终端会话</li>
104
+ <li><i class="fas fa-check text-success me-2"></i>窗口大小调整</li>
105
+ </ul>
106
+ <a href="/terminal" class="btn btn-custom">
107
+ <i class="fas fa-terminal me-2"></i>进入终端
108
+ </a>
109
+ </div>
110
+ </div>
111
+ </div>
112
+ </div>
113
+
114
+ <!-- 系统信息 -->
115
+ <div class="row mt-5">
116
+ <div class="col-12">
117
+ <div class="card">
118
+ <div class="card-body text-center">
119
+ <h5 class="card-title mb-3">
120
+ <i class="fas fa-info-circle me-2"></i>系统信息
121
+ </h5>
122
+ <div class="row text-center">
123
+ <div class="col-md-3">
124
+ <i class="fas fa-server text-primary mb-2" style="font-size: 2rem;"></i>
125
+ <p class="mb-0"><strong>服务器</strong></p>
126
+ <small class="text-muted">Flask + SocketIO</small>
127
+ </div>
128
+ <div class="col-md-3">
129
+ <i class="fas fa-shield-alt text-success mb-2" style="font-size: 2rem;"></i>
130
+ <p class="mb-0"><strong>安全</strong></p>
131
+ <small class="text-muted">Token 认证</small>
132
+ </div>
133
+ <div class="col-md-3">
134
+ <i class="fas fa-bolt text-warning mb-2" style="font-size: 2rem;"></i>
135
+ <p class="mb-0"><strong>性能</strong></p>
136
+ <small class="text-muted">实时响应</small>
137
+ </div>
138
+ <div class="col-md-3">
139
+ <i class="fas fa-mobile-alt text-info mb-2" style="font-size: 2rem;"></i>
140
+ <p class="mb-0"><strong>兼容</strong></p>
141
+ <small class="text-muted">移动端适配</small>
142
+ </div>
143
+ </div>
144
+ </div>
145
+ </div>
146
+ </div>
147
+ </div>
148
+ </div>
149
+ </div>
150
+ </div>
151
+
152
+ <script src="https://cdn.bootcdn.net/ajax/libs/bootstrap/5.3.0/js/bootstrap.bundle.min.js"></script>
153
+ </body>
154
+ </html>
templates/terminal.html ADDED
@@ -0,0 +1,553 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>终端 - WebShell</title>
7
+ <link href="https://cdn.bootcdn.net/ajax/libs/bootstrap/5.3.0/css/bootstrap.min.css" rel="stylesheet">
8
+ <link href="https://cdn.bootcdn.net/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
9
+ <style>
10
+ body {
11
+ background: #1e1e1e;
12
+ font-family: 'Cascadia Code', 'Consolas', 'Monaco', monospace;
13
+ overflow: hidden;
14
+ }
15
+
16
+ .navbar {
17
+ background: #2d2d30 !important;
18
+ border-bottom: 1px solid #3e3e42;
19
+ }
20
+
21
+ .navbar-brand, .nav-link {
22
+ color: #cccccc !important;
23
+ }
24
+
25
+ .navbar-brand:hover, .nav-link:hover {
26
+ color: #ffffff !important;
27
+ }
28
+
29
+ .terminal-container {
30
+ height: calc(100vh - 60px);
31
+ background: #0c0c0c;
32
+ border: 1px solid #3e3e42;
33
+ border-radius: 8px;
34
+ overflow: hidden;
35
+ position: relative;
36
+ }
37
+
38
+ .terminal-header {
39
+ background: #2d2d30;
40
+ padding: 8px 16px;
41
+ border-bottom: 1px solid #3e3e42;
42
+ display: flex;
43
+ justify-content: between;
44
+ align-items: center;
45
+ }
46
+
47
+ .terminal-title {
48
+ color: #cccccc;
49
+ font-size: 14px;
50
+ font-weight: 500;
51
+ }
52
+
53
+ .terminal-controls {
54
+ display: flex;
55
+ gap: 8px;
56
+ }
57
+
58
+ .terminal-btn {
59
+ width: 12px;
60
+ height: 12px;
61
+ border-radius: 50%;
62
+ border: none;
63
+ cursor: pointer;
64
+ }
65
+
66
+ .btn-close { background: #ff5f56; }
67
+ .btn-minimize { background: #ffbd2e; }
68
+ .btn-maximize { background: #27ca3f; }
69
+
70
+ .terminal-output {
71
+ height: calc(100% - 50px);
72
+ padding: 16px;
73
+ overflow-y: auto;
74
+ font-family: 'Cascadia Code', 'Consolas', 'Monaco', monospace;
75
+ font-size: 14px;
76
+ line-height: 1.4;
77
+ color: #cccccc;
78
+ background: #0c0c0c;
79
+ white-space: pre-wrap;
80
+ word-wrap: break-word;
81
+ }
82
+
83
+ .terminal-input-line {
84
+ display: flex;
85
+ align-items: center;
86
+ padding: 0 16px 16px;
87
+ background: #0c0c0c;
88
+ border-top: 1px solid #3e3e42;
89
+ }
90
+
91
+ .terminal-prompt {
92
+ color: #569cd6;
93
+ margin-right: 8px;
94
+ font-family: 'Cascadia Code', 'Consolas', 'Monaco', monospace;
95
+ font-size: 14px;
96
+ }
97
+
98
+ .terminal-input {
99
+ flex: 1;
100
+ background: transparent;
101
+ border: none;
102
+ color: #cccccc;
103
+ font-family: 'Cascadia Code', 'Consolas', 'Monaco', monospace;
104
+ font-size: 14px;
105
+ outline: none;
106
+ padding: 4px 0;
107
+ }
108
+
109
+ .cursor {
110
+ display: inline-block;
111
+ width: 2px;
112
+ height: 18px;
113
+ background: #cccccc;
114
+ animation: blink 1s infinite;
115
+ margin-left: 2px;
116
+ }
117
+
118
+ @keyframes blink {
119
+ 0%, 50% { opacity: 1; }
120
+ 51%, 100% { opacity: 0; }
121
+ }
122
+
123
+ .terminal-status {
124
+ position: absolute;
125
+ bottom: 8px;
126
+ right: 16px;
127
+ background: rgba(45, 45, 48, 0.9);
128
+ color: #cccccc;
129
+ padding: 4px 8px;
130
+ border-radius: 4px;
131
+ font-size: 12px;
132
+ backdrop-filter: blur(5px);
133
+ }
134
+
135
+ .status-connected {
136
+ color: #27ca3f;
137
+ }
138
+
139
+ .status-disconnected {
140
+ color: #ff5f56;
141
+ }
142
+
143
+ .status-connecting {
144
+ color: #ffbd2e;
145
+ }
146
+
147
+ /* 滚动条样式 */
148
+ .terminal-output::-webkit-scrollbar {
149
+ width: 8px;
150
+ }
151
+
152
+ .terminal-output::-webkit-scrollbar-track {
153
+ background: #1e1e1e;
154
+ }
155
+
156
+ .terminal-output::-webkit-scrollbar-thumb {
157
+ background: #3e3e42;
158
+ border-radius: 4px;
159
+ }
160
+
161
+ .terminal-output::-webkit-scrollbar-thumb:hover {
162
+ background: #555555;
163
+ }
164
+
165
+ /* 命令历史下拉 */
166
+ .history-dropdown {
167
+ position: absolute;
168
+ bottom: 60px;
169
+ left: 16px;
170
+ right: 16px;
171
+ background: #2d2d30;
172
+ border: 1px solid #3e3e42;
173
+ border-radius: 4px;
174
+ max-height: 200px;
175
+ overflow-y: auto;
176
+ z-index: 1000;
177
+ display: none;
178
+ }
179
+
180
+ .history-item {
181
+ padding: 8px 12px;
182
+ color: #cccccc;
183
+ cursor: pointer;
184
+ border-bottom: 1px solid #3e3e42;
185
+ font-family: 'Cascadia Code', 'Consolas', 'Monaco', monospace;
186
+ font-size: 14px;
187
+ }
188
+
189
+ .history-item:hover {
190
+ background: #404040;
191
+ }
192
+
193
+ .history-item:last-child {
194
+ border-bottom: none;
195
+ }
196
+ </style>
197
+ </head>
198
+ <body>
199
+ <!-- 导航栏 -->
200
+ <nav class="navbar navbar-expand-lg navbar-dark">
201
+ <div class="container-fluid">
202
+ <a class="navbar-brand" href="/">
203
+ <i class="fas fa-terminal me-2"></i>WebShell 终端
204
+ </a>
205
+ <div class="navbar-nav ms-auto">
206
+ <a class="nav-link" href="/files">
207
+ <i class="fas fa-folder me-2"></i>文件管理
208
+ </a>
209
+ <a class="nav-link" href="/">
210
+ <i class="fas fa-home me-2"></i>主页
211
+ </a>
212
+ </div>
213
+ </div>
214
+ </nav>
215
+
216
+ <div class="container-fluid p-3">
217
+ <div class="terminal-container">
218
+ <!-- 终端标题栏 -->
219
+ <div class="terminal-header">
220
+ <div class="terminal-controls">
221
+ <button class="terminal-btn btn-close" onclick="clearTerminal()"></button>
222
+ <button class="terminal-btn btn-minimize" onclick="minimizeTerminal()"></button>
223
+ <button class="terminal-btn btn-maximize" onclick="toggleFullscreen()"></button>
224
+ </div>
225
+ <div class="terminal-title">
226
+ <i class="fas fa-terminal me-2"></i>终端会话
227
+ </div>
228
+ <div>
229
+ <button class="btn btn-sm btn-outline-light" onclick="newTerminal()">
230
+ <i class="fas fa-plus me-1"></i>新建
231
+ </button>
232
+ </div>
233
+ </div>
234
+
235
+ <!-- 终端输出区域 -->
236
+ <div class="terminal-output" id="terminalOutput">
237
+ <div style="color: #569cd6;">WebShell 终端 v1.0</div>
238
+ <div style="color: #6a9955;">正在连接到服务器...</div>
239
+ <div style="color: #6a9955;">输入命令开始使用,按 Ctrl+C 可中断当前命令</div>
240
+ <div style="color: #6a9955;">按 ↑/↓ 键浏览命令历史</div>
241
+ <br>
242
+ </div>
243
+
244
+ <!-- 命令输入区域 -->
245
+ <div class="terminal-input-line">
246
+ <span class="terminal-prompt" id="terminalPrompt">$</span>
247
+ <input type="text" class="terminal-input" id="terminalInput"
248
+ placeholder="输入命令..." autocomplete="off" spellcheck="false">
249
+ <span class="cursor"></span>
250
+ </div>
251
+
252
+ <!-- 命令历史下拉 -->
253
+ <div class="history-dropdown" id="historyDropdown"></div>
254
+
255
+ <!-- 连接状态 -->
256
+ <div class="terminal-status">
257
+ <span id="connectionStatus" class="status-connecting">
258
+ <i class="fas fa-circle me-1"></i>连接中...
259
+ </span>
260
+ </div>
261
+ </div>
262
+ </div>
263
+
264
+ <script src="https://cdn.bootcdn.net/ajax/libs/bootstrap/5.3.0/js/bootstrap.bundle.min.js"></script>
265
+ <script src="https://cdn.bootcdn.net/ajax/libs/socket.io/4.7.2/socket.io.min.js"></script>
266
+
267
+ <script>
268
+ let socket = null;
269
+ let commandHistory = [];
270
+ let historyIndex = -1;
271
+ let isConnected = false;
272
+ let currentCommand = '';
273
+
274
+ // 页面加载时初始化
275
+ document.addEventListener('DOMContentLoaded', function() {
276
+ initializeTerminal();
277
+ setupEventListeners();
278
+ });
279
+
280
+ // 初始化终端连接
281
+ function initializeTerminal() {
282
+ socket = io();
283
+
284
+ socket.on('connect', function() {
285
+ updateConnectionStatus('connected');
286
+ addOutput('已连接到服务器', '#27ca3f');
287
+ socket.emit('start_terminal');
288
+ });
289
+
290
+ socket.on('disconnect', function() {
291
+ updateConnectionStatus('disconnected');
292
+ addOutput('与服务器断开连接', '#ff5f56');
293
+ });
294
+
295
+ socket.on('terminal_ready', function() {
296
+ addOutput('终端已准备就绪', '#6a9955');
297
+ updatePrompt();
298
+ });
299
+
300
+ socket.on('terminal_output', function(data) {
301
+ addOutput(data.data, '#cccccc', false);
302
+ updatePrompt();
303
+ });
304
+
305
+ socket.on('error', function(error) {
306
+ addOutput('错误: ' + error, '#ff5f56');
307
+ });
308
+ }
309
+
310
+ // 设置事件监听器
311
+ function setupEventListeners() {
312
+ const input = document.getElementById('terminalInput');
313
+
314
+ // 命令输入处理
315
+ input.addEventListener('keydown', function(e) {
316
+ switch(e.key) {
317
+ case 'Enter':
318
+ executeCommand();
319
+ break;
320
+ case 'ArrowUp':
321
+ e.preventDefault();
322
+ navigateHistory(-1);
323
+ break;
324
+ case 'ArrowDown':
325
+ e.preventDefault();
326
+ navigateHistory(1);
327
+ break;
328
+ case 'Tab':
329
+ e.preventDefault();
330
+ // TODO: 实现命令自动补全
331
+ break;
332
+ case 'Escape':
333
+ hideHistoryDropdown();
334
+ break;
335
+ }
336
+ });
337
+
338
+ // 输入内容变化时
339
+ input.addEventListener('input', function() {
340
+ currentCommand = this.value;
341
+ if (currentCommand.length > 0) {
342
+ showMatchingHistory();
343
+ } else {
344
+ hideHistoryDropdown();
345
+ }
346
+ });
347
+
348
+ // 点击其他地方时隐藏历史
349
+ document.addEventListener('click', function(e) {
350
+ if (!e.target.closest('.history-dropdown') && !e.target.closest('.terminal-input')) {
351
+ hideHistoryDropdown();
352
+ }
353
+ });
354
+
355
+ // 阻止右键菜单
356
+ document.addEventListener('contextmenu', function(e) {
357
+ e.preventDefault();
358
+ });
359
+
360
+ // 窗口大小变化时调整终端
361
+ window.addEventListener('resize', function() {
362
+ if (socket && isConnected) {
363
+ const rows = Math.floor(window.innerHeight / 20);
364
+ const cols = Math.floor(window.innerWidth / 10);
365
+ socket.emit('terminal_resize', {rows: rows, cols: cols});
366
+ }
367
+ });
368
+ }
369
+
370
+ // 执行命令
371
+ function executeCommand() {
372
+ const input = document.getElementById('terminalInput');
373
+ const command = input.value.trim();
374
+
375
+ if (!command) return;
376
+
377
+ // 添加到历史记录
378
+ if (command !== commandHistory[commandHistory.length - 1]) {
379
+ commandHistory.push(command);
380
+ if (commandHistory.length > 100) {
381
+ commandHistory.shift();
382
+ }
383
+ }
384
+ historyIndex = -1;
385
+ hideHistoryDropdown();
386
+
387
+ // 显示命令
388
+ addOutput(document.getElementById('terminalPrompt').textContent + ' ' + command, '#569cd6');
389
+
390
+ // 发送命令到服务器
391
+ if (socket && isConnected) {
392
+ socket.emit('terminal_input', {data: command + '\n'});
393
+ }
394
+
395
+ // 清空输入
396
+ input.value = '';
397
+ currentCommand = '';
398
+ }
399
+
400
+ // 添加输出
401
+ function addOutput(text, color = '#cccccc', addNewline = true) {
402
+ const output = document.getElementById('terminalOutput');
403
+ const div = document.createElement('div');
404
+
405
+ if (color) {
406
+ div.style.color = color;
407
+ }
408
+
409
+ div.textContent = text;
410
+ if (addNewline && !text.endsWith('\n')) {
411
+ div.appendChild(document.createElement('br'));
412
+ }
413
+
414
+ output.appendChild(div);
415
+
416
+ // 自动滚动到底部
417
+ output.scrollTop = output.scrollHeight;
418
+ }
419
+
420
+ // 更新连接状态
421
+ function updateConnectionStatus(status) {
422
+ const statusElement = document.getElementById('connectionStatus');
423
+
424
+ switch(status) {
425
+ case 'connected':
426
+ statusElement.className = 'status-connected';
427
+ statusElement.innerHTML = '<i class="fas fa-circle me-1"></i>已连接';
428
+ isConnected = true;
429
+ break;
430
+ case 'disconnected':
431
+ statusElement.className = 'status-disconnected';
432
+ statusElement.innerHTML = '<i class="fas fa-circle me-1"></i>已断开';
433
+ isConnected = false;
434
+ break;
435
+ case 'connecting':
436
+ statusElement.className = 'status-connecting';
437
+ statusElement.innerHTML = '<i class="fas fa-circle me-1"></i>连接中...';
438
+ isConnected = false;
439
+ break;
440
+ }
441
+ }
442
+
443
+ // 更新提示符
444
+ function updatePrompt() {
445
+ // 这里可以根据当前目录等信息更新提示符
446
+ // 暂时保持简单的 $ 提示符
447
+ }
448
+
449
+ // 浏览命令历史
450
+ function navigateHistory(direction) {
451
+ if (commandHistory.length === 0) return;
452
+
453
+ historyIndex += direction;
454
+
455
+ if (historyIndex < -1) {
456
+ historyIndex = -1;
457
+ } else if (historyIndex >= commandHistory.length) {
458
+ historyIndex = commandHistory.length - 1;
459
+ }
460
+
461
+ const input = document.getElementById('terminalInput');
462
+ if (historyIndex === -1) {
463
+ input.value = currentCommand;
464
+ } else {
465
+ input.value = commandHistory[commandHistory.length - 1 - historyIndex];
466
+ }
467
+ }
468
+
469
+ // 显示匹配的历史命令
470
+ function showMatchingHistory() {
471
+ const dropdown = document.getElementById('historyDropdown');
472
+ const matching = commandHistory.filter(cmd =>
473
+ cmd.toLowerCase().includes(currentCommand.toLowerCase())
474
+ ).slice(-10); // 最多显示10个
475
+
476
+ if (matching.length === 0) {
477
+ hideHistoryDropdown();
478
+ return;
479
+ }
480
+
481
+ dropdown.innerHTML = '';
482
+ matching.reverse().forEach(cmd => {
483
+ const item = document.createElement('div');
484
+ item.className = 'history-item';
485
+ item.textContent = cmd;
486
+ item.onclick = function() {
487
+ document.getElementById('terminalInput').value = cmd;
488
+ hideHistoryDropdown();
489
+ };
490
+ dropdown.appendChild(item);
491
+ });
492
+
493
+ dropdown.style.display = 'block';
494
+ }
495
+
496
+ // 隐藏历史下拉
497
+ function hideHistoryDropdown() {
498
+ document.getElementById('historyDropdown').style.display = 'none';
499
+ }
500
+
501
+ // 清空终端
502
+ function clearTerminal() {
503
+ document.getElementById('terminalOutput').innerHTML = '';
504
+ }
505
+
506
+ // 最小化终端(模拟)
507
+ function minimizeTerminal() {
508
+ const container = document.querySelector('.terminal-container');
509
+ container.style.height = '60px';
510
+ setTimeout(() => {
511
+ container.style.height = 'calc(100vh - 60px)';
512
+ }, 300);
513
+ }
514
+
515
+ // 切换全屏
516
+ function toggleFullscreen() {
517
+ if (!document.fullscreenElement) {
518
+ document.documentElement.requestFullscreen();
519
+ } else {
520
+ document.exitFullscreen();
521
+ }
522
+ }
523
+
524
+ // 新建终端
525
+ function newTerminal() {
526
+ if (socket) {
527
+ socket.emit('start_terminal');
528
+ addOutput('启动新的终端会话...', '#6a9955');
529
+ }
530
+ }
531
+
532
+ // 页面卸载时断开连接
533
+ window.addEventListener('beforeunload', function() {
534
+ if (socket) {
535
+ socket.disconnect();
536
+ }
537
+ });
538
+
539
+ // 保持输入框焦点
540
+ setInterval(function() {
541
+ const input = document.getElementById('terminalInput');
542
+ if (document.activeElement !== input) {
543
+ input.focus();
544
+ }
545
+ }, 100);
546
+
547
+ // 初始化时设置焦点
548
+ setTimeout(function() {
549
+ document.getElementById('terminalInput').focus();
550
+ }, 500);
551
+ </script>
552
+ </body>
553
+ </html>