termites 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/server.js ADDED
@@ -0,0 +1,625 @@
1
+ #!/usr/bin/env node
2
+ const http = require('http');
3
+ const WebSocket = require('ws');
4
+ const crypto = require('crypto');
5
+
6
+ const PORT = process.env.PORT || 3456;
7
+
8
+ class WebShellServer {
9
+ constructor(port) {
10
+ this.port = port;
11
+ this.clients = new Map(); // clientId -> { ws, info, outputBuffer }
12
+ this.browsers = new Set(); // browser WebSocket connections
13
+ this.selectedClient = null; // currently selected client for each browser
14
+
15
+ this.startServer();
16
+ }
17
+
18
+ startServer() {
19
+ this.httpServer = http.createServer((req, res) => {
20
+ this.handleHttpRequest(req, res);
21
+ });
22
+
23
+ this.wss = new WebSocket.Server({ server: this.httpServer });
24
+
25
+ this.wss.on('connection', (ws, req) => {
26
+ const isClient = req.url === '/client';
27
+
28
+ if (isClient) {
29
+ this.handleClientConnection(ws);
30
+ } else {
31
+ this.handleBrowserConnection(ws);
32
+ }
33
+ });
34
+
35
+ this.httpServer.listen(this.port, () => {
36
+ console.log(`WebShell Server 启动: http://localhost:${this.port}`);
37
+ console.log(`客户端连接地址: ws://localhost:${this.port}/client`);
38
+ });
39
+ }
40
+
41
+ // Handle shell client connections
42
+ handleClientConnection(ws) {
43
+ const clientId = crypto.randomUUID();
44
+ console.log(`新客户端连接: ${clientId}`);
45
+
46
+ const clientData = {
47
+ ws,
48
+ info: null,
49
+ outputBuffer: []
50
+ };
51
+ this.clients.set(clientId, clientData);
52
+
53
+ ws.on('message', (message) => {
54
+ try {
55
+ const data = JSON.parse(message);
56
+ this.handleClientMessage(clientId, data);
57
+ } catch (e) {
58
+ console.error('解析客户端消息失败:', e);
59
+ }
60
+ });
61
+
62
+ ws.on('close', () => {
63
+ console.log(`客户端断开: ${clientId}`);
64
+ this.clients.delete(clientId);
65
+ this.broadcastToBrowsers({
66
+ type: 'client-disconnected',
67
+ clientId
68
+ });
69
+ });
70
+ }
71
+
72
+ handleClientMessage(clientId, data) {
73
+ const client = this.clients.get(clientId);
74
+ if (!client) return;
75
+
76
+ switch (data.type) {
77
+ case 'register':
78
+ client.info = data.info;
79
+ console.log(`客户端注册: ${data.info.username}@${data.info.hostname}`);
80
+ this.broadcastToBrowsers({
81
+ type: 'client-connected',
82
+ client: { id: clientId, ...data.info }
83
+ });
84
+ this.broadcastClientList();
85
+ break;
86
+
87
+ case 'output':
88
+ client.outputBuffer.push(data.data);
89
+ if (client.outputBuffer.length > 1000) {
90
+ client.outputBuffer = client.outputBuffer.slice(-500);
91
+ }
92
+ this.broadcastToBrowsers({
93
+ type: 'output',
94
+ clientId,
95
+ data: data.data
96
+ });
97
+ break;
98
+
99
+ case 'exit':
100
+ console.log(`客户端 shell 退出: ${clientId}`);
101
+ break;
102
+ }
103
+ }
104
+
105
+ // Handle browser connections
106
+ handleBrowserConnection(ws) {
107
+ console.log('新浏览器连接');
108
+ this.browsers.add(ws);
109
+
110
+ // Send current client list
111
+ ws.send(JSON.stringify({
112
+ type: 'clients',
113
+ list: this.getClientList()
114
+ }));
115
+
116
+ ws.on('message', (message) => {
117
+ try {
118
+ const data = JSON.parse(message);
119
+ this.handleBrowserMessage(ws, data);
120
+ } catch (e) {
121
+ console.error('解析浏览器消息失败:', e);
122
+ }
123
+ });
124
+
125
+ ws.on('close', () => {
126
+ this.browsers.delete(ws);
127
+ });
128
+ }
129
+
130
+ handleBrowserMessage(browserWs, data) {
131
+ switch (data.type) {
132
+ case 'select':
133
+ const client = this.clients.get(data.clientId);
134
+ if (client) {
135
+ // Send buffered output to browser
136
+ client.outputBuffer.forEach(output => {
137
+ browserWs.send(JSON.stringify({
138
+ type: 'output',
139
+ clientId: data.clientId,
140
+ data: output
141
+ }));
142
+ });
143
+ }
144
+ break;
145
+
146
+ case 'input':
147
+ if (data.clientId) {
148
+ const targetClient = this.clients.get(data.clientId);
149
+ if (targetClient && targetClient.ws.readyState === WebSocket.OPEN) {
150
+ targetClient.ws.send(JSON.stringify({
151
+ type: 'input',
152
+ text: data.text
153
+ }));
154
+ }
155
+ }
156
+ break;
157
+
158
+ case 'resize':
159
+ if (data.clientId) {
160
+ const targetClient = this.clients.get(data.clientId);
161
+ if (targetClient && targetClient.ws.readyState === WebSocket.OPEN) {
162
+ targetClient.ws.send(JSON.stringify({
163
+ type: 'resize',
164
+ cols: data.cols,
165
+ rows: data.rows
166
+ }));
167
+ }
168
+ }
169
+ break;
170
+ }
171
+ }
172
+
173
+ getClientList() {
174
+ const list = [];
175
+ this.clients.forEach((client, id) => {
176
+ if (client.info) {
177
+ list.push({ id, ...client.info });
178
+ }
179
+ });
180
+ return list;
181
+ }
182
+
183
+ broadcastClientList() {
184
+ this.broadcastToBrowsers({
185
+ type: 'clients',
186
+ list: this.getClientList()
187
+ });
188
+ }
189
+
190
+ broadcastToBrowsers(data) {
191
+ const msg = JSON.stringify(data);
192
+ this.browsers.forEach(ws => {
193
+ if (ws.readyState === WebSocket.OPEN) {
194
+ ws.send(msg);
195
+ }
196
+ });
197
+ }
198
+
199
+ handleHttpRequest(req, res) {
200
+ if (req.url === '/' || req.url === '/index.html') {
201
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
202
+ res.end(this.getHtmlPage());
203
+ } else {
204
+ res.writeHead(404);
205
+ res.end('Not Found');
206
+ }
207
+ }
208
+
209
+ getHtmlPage() {
210
+ return `<!DOCTYPE html>
211
+ <html lang="zh-CN">
212
+ <head>
213
+ <meta charset="UTF-8">
214
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
215
+ <title>WebShell</title>
216
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm@5.3.0/css/xterm.css">
217
+ <style>
218
+ * { margin: 0; padding: 0; box-sizing: border-box; }
219
+ body { height: 100vh; display: flex; flex-direction: column; transition: background 0.3s; font-family: monospace; }
220
+ .header {
221
+ padding: 10px 12px; display: flex; align-items: center; gap: 12px;
222
+ border-bottom: 1px solid; transition: all 0.3s; flex-shrink: 0;
223
+ }
224
+ .menu-btn {
225
+ background: none; border: none; padding: 6px; cursor: pointer;
226
+ display: flex; flex-direction: column; gap: 4px; color: inherit;
227
+ }
228
+ .menu-btn span { display: block; width: 20px; height: 2px; background: currentColor; border-radius: 1px; }
229
+ .header-title { flex: 1; font-size: 14px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
230
+ .header-title .user { font-weight: bold; color: var(--user-color, #859900); }
231
+ .header-title .host { color: var(--host-color, #859900); }
232
+ .header-title .sep { color: var(--sep-color, #657b83); }
233
+ .no-client { color: #888; font-style: italic; }
234
+ #terminal-container { flex: 1; padding: 4px; transition: background 0.3s; overflow: hidden; }
235
+ .xterm { height: 100%; }
236
+ .overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.4); z-index: 99; opacity: 0; visibility: hidden; transition: all 0.3s; }
237
+ .overlay.open { opacity: 1; visibility: visible; }
238
+ .drawer {
239
+ position: fixed; top: 0; left: 0; width: 280px; max-width: 85vw; height: 100%;
240
+ transform: translateX(-100%); transition: transform 0.3s; z-index: 100;
241
+ display: flex; flex-direction: column; box-shadow: 2px 0 10px rgba(0,0,0,0.2);
242
+ }
243
+ .drawer.open { transform: translateX(0); }
244
+ .drawer-header {
245
+ padding: 16px; border-bottom: 1px solid; display: flex; justify-content: space-between; align-items: center;
246
+ }
247
+ .drawer-header h3 { font-size: 16px; font-weight: bold; }
248
+ .drawer-close { background: none; border: none; font-size: 24px; cursor: pointer; color: inherit; padding: 0 4px; }
249
+ .drawer-content { flex: 1; overflow-y: auto; }
250
+ .drawer-section { border-bottom: 1px solid; }
251
+ .drawer-section-header {
252
+ padding: 12px 16px; font-size: 12px; font-weight: bold; text-transform: uppercase;
253
+ opacity: 0.6; display: flex; align-items: center; gap: 8px;
254
+ }
255
+ .client-list { }
256
+ .client-item { padding: 12px 16px; cursor: pointer; transition: background 0.2s; }
257
+ .client-item:hover { opacity: 0.8; }
258
+ .client-item.selected { font-weight: bold; }
259
+ .client-item .client-name { font-size: 13px; margin-bottom: 3px; display: flex; align-items: center; }
260
+ .client-item .client-info { font-size: 11px; opacity: 0.6; padding-left: 14px; }
261
+ .status-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 6px; flex-shrink: 0; }
262
+ .status-dot.online { background: #22c55e; }
263
+ .settings-section { padding: 12px 16px; }
264
+ .setting-group { margin-bottom: 16px; }
265
+ .setting-group:last-child { margin-bottom: 0; }
266
+ .setting-group label { display: block; margin-bottom: 6px; font-size: 12px; font-weight: bold; }
267
+ .setting-group select, .setting-group input[type="range"] {
268
+ width: 100%; padding: 8px; border: 1px solid; border-radius: 4px;
269
+ font-size: 13px; background: inherit; color: inherit;
270
+ }
271
+ .font-size-row { display: flex; align-items: center; gap: 8px; }
272
+ .font-size-row input { flex: 1; }
273
+ .font-size-row span { font-size: 12px; min-width: 36px; }
274
+ .empty-clients { padding: 16px; font-size: 12px; opacity: 0.5; text-align: center; }
275
+ </style>
276
+ </head>
277
+ <body>
278
+ <div class="overlay" id="overlay"></div>
279
+ <div class="drawer" id="drawer">
280
+ <div class="drawer-header">
281
+ <h3>WebShell</h3>
282
+ <button class="drawer-close" id="drawer-close">&times;</button>
283
+ </div>
284
+ <div class="drawer-content">
285
+ <div class="drawer-section">
286
+ <div class="drawer-section-header">⚙ 设置</div>
287
+ <div class="settings-section">
288
+ <div class="setting-group">
289
+ <label>主题</label>
290
+ <select id="theme-select">
291
+ <option value="solarized-light">Solarized Light</option>
292
+ <option value="solarized-dark">Solarized Dark</option>
293
+ <option value="monokai">Monokai</option>
294
+ <option value="dracula">Dracula</option>
295
+ <option value="nord">Nord</option>
296
+ <option value="github-dark">GitHub Dark</option>
297
+ </select>
298
+ </div>
299
+ <div class="setting-group">
300
+ <label>字体</label>
301
+ <select id="font-select">
302
+ <option value="Monaco, Menlo, monospace">Monaco</option>
303
+ <option value="'Fira Code', monospace">Fira Code</option>
304
+ <option value="'JetBrains Mono', monospace">JetBrains Mono</option>
305
+ <option value="'Source Code Pro', monospace">Source Code Pro</option>
306
+ <option value="Consolas, monospace">Consolas</option>
307
+ </select>
308
+ </div>
309
+ <div class="setting-group">
310
+ <label>字体大小</label>
311
+ <div class="font-size-row">
312
+ <input type="range" id="font-size" min="10" max="24" value="14">
313
+ <span id="font-size-value">14px</span>
314
+ </div>
315
+ </div>
316
+ </div>
317
+ </div>
318
+ <div class="drawer-section">
319
+ <div class="drawer-section-header">◉ 客户端</div>
320
+ <div class="client-list" id="client-list">
321
+ <div class="empty-clients">等待客户端连接...</div>
322
+ </div>
323
+ </div>
324
+ </div>
325
+ </div>
326
+ <div class="header">
327
+ <button class="menu-btn" id="menu-btn"><span></span><span></span><span></span></button>
328
+ <div class="header-title" id="header-title"><span class="no-client">未连接</span></div>
329
+ </div>
330
+ <div id="terminal-container"></div>
331
+
332
+ <script src="https://cdn.jsdelivr.net/npm/xterm@5.3.0/lib/xterm.min.js"></script>
333
+ <script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.min.js"></script>
334
+ <script>
335
+ let ws, term, fitAddon;
336
+ let clients = [];
337
+ let selectedClientId = null;
338
+
339
+ const themes = {
340
+ 'solarized-light': {
341
+ background: '#fdf6e3', foreground: '#657b83', cursor: '#657b83',
342
+ cursorAccent: '#fdf6e3', selectionBackground: '#93a1a1',
343
+ black: '#073642', red: '#dc322f', green: '#859900', yellow: '#b58900',
344
+ blue: '#268bd2', magenta: '#d33682', cyan: '#2aa198', white: '#eee8d5',
345
+ brightBlack: '#002b36', brightRed: '#cb4b16', brightGreen: '#586e75',
346
+ brightYellow: '#657b83', brightBlue: '#839496', brightMagenta: '#6c71c4',
347
+ brightCyan: '#93a1a1', brightWhite: '#fdf6e3',
348
+ headerBg: '#eee8d5', headerBorder: '#93a1a1', sidebarBg: '#eee8d5',
349
+ sidebarSelected: '#fdf6e3', userColor: '#859900', hostColor: '#859900',
350
+ sepColor: '#657b83', pathColor: '#268bd2'
351
+ },
352
+ 'solarized-dark': {
353
+ background: '#002b36', foreground: '#839496', cursor: '#839496',
354
+ cursorAccent: '#002b36', selectionBackground: '#073642',
355
+ black: '#073642', red: '#dc322f', green: '#859900', yellow: '#b58900',
356
+ blue: '#268bd2', magenta: '#d33682', cyan: '#2aa198', white: '#eee8d5',
357
+ brightBlack: '#002b36', brightRed: '#cb4b16', brightGreen: '#586e75',
358
+ brightYellow: '#657b83', brightBlue: '#839496', brightMagenta: '#6c71c4',
359
+ brightCyan: '#93a1a1', brightWhite: '#fdf6e3',
360
+ headerBg: '#073642', headerBorder: '#586e75', sidebarBg: '#073642',
361
+ sidebarSelected: '#002b36', userColor: '#859900', hostColor: '#859900',
362
+ sepColor: '#839496', pathColor: '#268bd2'
363
+ },
364
+ 'monokai': {
365
+ background: '#272822', foreground: '#f8f8f2', cursor: '#f8f8f2',
366
+ cursorAccent: '#272822', selectionBackground: '#49483e',
367
+ black: '#272822', red: '#f92672', green: '#a6e22e', yellow: '#f4bf75',
368
+ blue: '#66d9ef', magenta: '#ae81ff', cyan: '#a1efe4', white: '#f8f8f2',
369
+ brightBlack: '#75715e', brightRed: '#f92672', brightGreen: '#a6e22e',
370
+ brightYellow: '#f4bf75', brightBlue: '#66d9ef', brightMagenta: '#ae81ff',
371
+ brightCyan: '#a1efe4', brightWhite: '#f9f8f5',
372
+ headerBg: '#1e1f1c', headerBorder: '#49483e', sidebarBg: '#1e1f1c',
373
+ sidebarSelected: '#272822', userColor: '#a6e22e', hostColor: '#a6e22e',
374
+ sepColor: '#f8f8f2', pathColor: '#66d9ef'
375
+ },
376
+ 'dracula': {
377
+ background: '#282a36', foreground: '#f8f8f2', cursor: '#f8f8f2',
378
+ cursorAccent: '#282a36', selectionBackground: '#44475a',
379
+ black: '#21222c', red: '#ff5555', green: '#50fa7b', yellow: '#f1fa8c',
380
+ blue: '#bd93f9', magenta: '#ff79c6', cyan: '#8be9fd', white: '#f8f8f2',
381
+ brightBlack: '#6272a4', brightRed: '#ff6e6e', brightGreen: '#69ff94',
382
+ brightYellow: '#ffffa5', brightBlue: '#d6acff', brightMagenta: '#ff92df',
383
+ brightCyan: '#a4ffff', brightWhite: '#ffffff',
384
+ headerBg: '#21222c', headerBorder: '#44475a', sidebarBg: '#21222c',
385
+ sidebarSelected: '#282a36', userColor: '#50fa7b', hostColor: '#50fa7b',
386
+ sepColor: '#f8f8f2', pathColor: '#bd93f9'
387
+ },
388
+ 'nord': {
389
+ background: '#2e3440', foreground: '#d8dee9', cursor: '#d8dee9',
390
+ cursorAccent: '#2e3440', selectionBackground: '#434c5e',
391
+ black: '#3b4252', red: '#bf616a', green: '#a3be8c', yellow: '#ebcb8b',
392
+ blue: '#81a1c1', magenta: '#b48ead', cyan: '#88c0d0', white: '#e5e9f0',
393
+ brightBlack: '#4c566a', brightRed: '#bf616a', brightGreen: '#a3be8c',
394
+ brightYellow: '#ebcb8b', brightBlue: '#81a1c1', brightMagenta: '#b48ead',
395
+ brightCyan: '#8fbcbb', brightWhite: '#eceff4',
396
+ headerBg: '#3b4252', headerBorder: '#4c566a', sidebarBg: '#3b4252',
397
+ sidebarSelected: '#2e3440', userColor: '#a3be8c', hostColor: '#a3be8c',
398
+ sepColor: '#d8dee9', pathColor: '#81a1c1'
399
+ },
400
+ 'github-dark': {
401
+ background: '#0d1117', foreground: '#c9d1d9', cursor: '#c9d1d9',
402
+ cursorAccent: '#0d1117', selectionBackground: '#3b5070',
403
+ black: '#0d1117', red: '#ff7b72', green: '#7ee787', yellow: '#d29922',
404
+ blue: '#79c0ff', magenta: '#d2a8ff', cyan: '#a5d6ff', white: '#c9d1d9',
405
+ brightBlack: '#484f58', brightRed: '#ffa198', brightGreen: '#aff5b4',
406
+ brightYellow: '#e3b341', brightBlue: '#a5d6ff', brightMagenta: '#d2a8ff',
407
+ brightCyan: '#b6e3ff', brightWhite: '#f0f6fc',
408
+ headerBg: '#161b22', headerBorder: '#30363d', sidebarBg: '#161b22',
409
+ sidebarSelected: '#0d1117', userColor: '#7ee787', hostColor: '#7ee787',
410
+ sepColor: '#c9d1d9', pathColor: '#79c0ff'
411
+ }
412
+ };
413
+
414
+ let currentTheme = 'solarized-light';
415
+ let currentFont = 'Monaco, Menlo, monospace';
416
+ let currentFontSize = 14;
417
+
418
+ function loadSettings() {
419
+ const saved = localStorage.getItem('webshell-settings');
420
+ if (saved) {
421
+ const s = JSON.parse(saved);
422
+ currentTheme = s.theme || currentTheme;
423
+ currentFont = s.font || currentFont;
424
+ currentFontSize = s.fontSize || currentFontSize;
425
+ }
426
+ document.getElementById('theme-select').value = currentTheme;
427
+ document.getElementById('font-select').value = currentFont;
428
+ document.getElementById('font-size').value = currentFontSize;
429
+ document.getElementById('font-size-value').textContent = currentFontSize + 'px';
430
+ }
431
+
432
+ function saveSettings() {
433
+ localStorage.setItem('webshell-settings', JSON.stringify({
434
+ theme: currentTheme, font: currentFont, fontSize: currentFontSize
435
+ }));
436
+ }
437
+
438
+ function applyTheme(themeName) {
439
+ const t = themes[themeName];
440
+ if (!t) return;
441
+ currentTheme = themeName;
442
+ document.body.style.background = t.background;
443
+ document.getElementById('terminal-container').style.background = t.background;
444
+ const header = document.querySelector('.header');
445
+ header.style.background = t.headerBg;
446
+ header.style.borderColor = t.headerBorder;
447
+ header.style.color = t.foreground;
448
+ const drawer = document.getElementById('drawer');
449
+ drawer.style.background = t.sidebarBg;
450
+ drawer.style.color = t.foreground;
451
+ document.querySelectorAll('.drawer-section').forEach(el => el.style.borderColor = t.headerBorder);
452
+ document.querySelector('.drawer-header').style.borderColor = t.headerBorder;
453
+ const style = document.documentElement.style;
454
+ style.setProperty('--user-color', t.userColor);
455
+ style.setProperty('--host-color', t.hostColor);
456
+ style.setProperty('--sep-color', t.sepColor);
457
+ style.setProperty('--path-color', t.pathColor);
458
+ style.setProperty('--sidebar-selected', t.sidebarSelected);
459
+ if (term) term.options.theme = t;
460
+ updateClientList();
461
+ saveSettings();
462
+ }
463
+
464
+ function applyFont(font) {
465
+ currentFont = font;
466
+ if (term) {
467
+ term.options.fontFamily = font;
468
+ fitAddon.fit();
469
+ }
470
+ saveSettings();
471
+ }
472
+
473
+ function applyFontSize(size) {
474
+ currentFontSize = size;
475
+ document.getElementById('font-size-value').textContent = size + 'px';
476
+ if (term) {
477
+ term.options.fontSize = size;
478
+ fitAddon.fit();
479
+ }
480
+ saveSettings();
481
+ }
482
+
483
+ function setupDrawer() {
484
+ const menuBtn = document.getElementById('menu-btn');
485
+ const drawer = document.getElementById('drawer');
486
+ const overlay = document.getElementById('overlay');
487
+ const closeBtn = document.getElementById('drawer-close');
488
+ const openDrawer = () => { drawer.classList.add('open'); overlay.classList.add('open'); };
489
+ const closeDrawer = () => { drawer.classList.remove('open'); overlay.classList.remove('open'); };
490
+ menuBtn.onclick = openDrawer;
491
+ closeBtn.onclick = closeDrawer;
492
+ overlay.onclick = closeDrawer;
493
+ document.getElementById('theme-select').onchange = e => applyTheme(e.target.value);
494
+ document.getElementById('font-select').onchange = e => applyFont(e.target.value);
495
+ document.getElementById('font-size').oninput = e => applyFontSize(parseInt(e.target.value));
496
+ }
497
+
498
+ function updateClientList() {
499
+ const listEl = document.getElementById('client-list');
500
+ const t = themes[currentTheme];
501
+ if (clients.length === 0) {
502
+ listEl.innerHTML = '<div class="empty-clients">等待客户端连接...</div>';
503
+ return;
504
+ }
505
+ listEl.innerHTML = clients.map(c => {
506
+ const isSelected = c.id === selectedClientId;
507
+ const bgColor = isSelected ? t.sidebarSelected : 'transparent';
508
+ return '<div class="client-item' + (isSelected ? ' selected' : '') + '" ' +
509
+ 'style="background: ' + bgColor + ';" ' +
510
+ 'onclick="selectClient(\\'' + c.id + '\\')">' +
511
+ '<div class="client-name"><span class="status-dot online"></span>' +
512
+ c.username + '@' + c.hostname + '</div>' +
513
+ '<div class="client-info">' + (c.cwd || '') + ' · ' + (c.platform || '') + '</div></div>';
514
+ }).join('');
515
+ }
516
+
517
+ function selectClient(clientId) {
518
+ if (selectedClientId === clientId) {
519
+ document.getElementById('drawer').classList.remove('open');
520
+ document.getElementById('overlay').classList.remove('open');
521
+ return;
522
+ }
523
+ selectedClientId = clientId;
524
+ const client = clients.find(c => c.id === clientId);
525
+ if (client) {
526
+ document.getElementById('header-title').innerHTML =
527
+ '<span class="user">' + client.username + '</span>' +
528
+ '<span class="sep">@</span><span class="host">' + client.hostname + '</span>';
529
+ }
530
+ term.clear();
531
+ ws.send(JSON.stringify({ type: 'select', clientId }));
532
+ updateClientList();
533
+ document.getElementById('drawer').classList.remove('open');
534
+ document.getElementById('overlay').classList.remove('open');
535
+ term.focus();
536
+ }
537
+
538
+ function init() {
539
+ loadSettings();
540
+ setupDrawer();
541
+ term = new Terminal({
542
+ theme: themes[currentTheme], fontFamily: currentFont,
543
+ fontSize: currentFontSize, cursorBlink: true
544
+ });
545
+ fitAddon = new FitAddon.FitAddon();
546
+ term.loadAddon(fitAddon);
547
+ term.open(document.getElementById('terminal-container'));
548
+ fitAddon.fit();
549
+ term.onData(data => {
550
+ if (ws?.readyState === WebSocket.OPEN && selectedClientId) {
551
+ ws.send(JSON.stringify({ type: 'input', clientId: selectedClientId, text: data }));
552
+ }
553
+ });
554
+ window.addEventListener('resize', () => {
555
+ fitAddon.fit();
556
+ if (ws?.readyState === WebSocket.OPEN && selectedClientId) {
557
+ ws.send(JSON.stringify({
558
+ type: 'resize', clientId: selectedClientId,
559
+ cols: term.cols, rows: term.rows
560
+ }));
561
+ }
562
+ });
563
+ connect();
564
+ applyTheme(currentTheme);
565
+ }
566
+
567
+ function connect() {
568
+ ws = new WebSocket('ws://' + location.host);
569
+ ws.onopen = () => {
570
+ if (selectedClientId) {
571
+ ws.send(JSON.stringify({
572
+ type: 'resize', clientId: selectedClientId,
573
+ cols: term.cols, rows: term.rows
574
+ }));
575
+ }
576
+ };
577
+ ws.onmessage = e => {
578
+ const d = JSON.parse(e.data);
579
+ switch (d.type) {
580
+ case 'clients':
581
+ clients = d.list;
582
+ updateClientList();
583
+ if (!selectedClientId && clients.length > 0) {
584
+ selectClient(clients[0].id);
585
+ }
586
+ break;
587
+ case 'client-connected':
588
+ if (!clients.find(c => c.id === d.client.id)) {
589
+ clients.push(d.client);
590
+ updateClientList();
591
+ if (!selectedClientId) selectClient(d.client.id);
592
+ }
593
+ break;
594
+ case 'client-disconnected':
595
+ clients = clients.filter(c => c.id !== d.clientId);
596
+ updateClientList();
597
+ if (selectedClientId === d.clientId) {
598
+ selectedClientId = null;
599
+ term.clear();
600
+ document.getElementById('header-title').innerHTML =
601
+ '<span class="no-client">已断开</span>';
602
+ if (clients.length > 0) selectClient(clients[0].id);
603
+ }
604
+ break;
605
+ case 'output':
606
+ if (d.clientId === selectedClientId) {
607
+ term.write(d.data);
608
+ }
609
+ break;
610
+ }
611
+ };
612
+ ws.onclose = () => setTimeout(connect, 3000);
613
+ }
614
+
615
+ init();
616
+ </script>
617
+ </body>
618
+ </html>`;
619
+ }
620
+ }
621
+
622
+ new WebShellServer(PORT);
623
+
624
+ process.on('SIGINT', () => process.exit(0));
625
+ process.on('SIGTERM', () => process.exit(0));