@yangrunchi/a_6 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.
@@ -0,0 +1,1248 @@
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, user-scalable=no">
6
+ <title>BAT脚本管理器 - 远程执行控制台</title>
7
+ <style>
8
+ * {
9
+ box-sizing: border-box;
10
+ margin: 0;
11
+ padding: 0;
12
+ }
13
+ body {
14
+ background: linear-gradient(135deg, #0a0f1e 0%, #0c1222 100%);
15
+ font-family: 'Segoe UI', 'Fira Code', 'JetBrains Mono', monospace;
16
+ min-height: 100vh;
17
+ padding: 20px;
18
+ }
19
+
20
+ .auth-overlay {
21
+ position: fixed;
22
+ top: 0;
23
+ left: 0;
24
+ width: 100%;
25
+ height: 100%;
26
+ background: rgba(0,0,0,0.92);
27
+ backdrop-filter: blur(10px);
28
+ display: flex;
29
+ justify-content: center;
30
+ align-items: center;
31
+ z-index: 1000;
32
+ }
33
+ .auth-card {
34
+ background: #0e121f;
35
+ border-radius: 32px;
36
+ width: 420px;
37
+ max-width: 90%;
38
+ padding: 32px;
39
+ border: 1px solid #2a3a66;
40
+ box-shadow: 0 25px 50px rgba(0,0,0,0.5);
41
+ }
42
+ .auth-card h2 {
43
+ color: #b3f0ff;
44
+ text-align: center;
45
+ margin-bottom: 20px;
46
+ }
47
+ .auth-tabs {
48
+ display: flex;
49
+ gap: 10px;
50
+ margin-bottom: 24px;
51
+ }
52
+ .tab-btn {
53
+ flex: 1;
54
+ background: transparent;
55
+ border: none;
56
+ padding: 10px;
57
+ color: #8ba0c0;
58
+ font-weight: bold;
59
+ cursor: pointer;
60
+ border-radius: 30px;
61
+ transition: 0.2s;
62
+ }
63
+ .tab-btn.active {
64
+ background: #1f3b5c;
65
+ color: #b3f0ff;
66
+ }
67
+ .input-group {
68
+ margin-bottom: 18px;
69
+ }
70
+ .input-group label {
71
+ color: #b8c7e7;
72
+ font-size: 0.75rem;
73
+ display: block;
74
+ margin-bottom: 6px;
75
+ }
76
+ .input-group input {
77
+ width: 100%;
78
+ background: #010101aa;
79
+ border: 1px solid #2d4c7a;
80
+ border-radius: 40px;
81
+ padding: 12px 18px;
82
+ color: #eef5ff;
83
+ font-family: monospace;
84
+ outline: none;
85
+ }
86
+ .input-group input:focus {
87
+ border-color: #3fa7ff;
88
+ }
89
+ .input-group input.input-error {
90
+ border-color: #ff5c5c;
91
+ }
92
+ .input-group input.input-valid {
93
+ border-color: #5f9e6e;
94
+ }
95
+ .input-hint {
96
+ font-size: 0.65rem;
97
+ margin-top: 4px;
98
+ margin-left: 12px;
99
+ }
100
+ .input-hint.error {
101
+ color: #ff9f8f;
102
+ }
103
+ .input-hint.success {
104
+ color: #8bffb0;
105
+ }
106
+ .auth-btn {
107
+ width: 100%;
108
+ background: #0f5b8c;
109
+ border: none;
110
+ padding: 12px;
111
+ border-radius: 40px;
112
+ font-weight: bold;
113
+ color: white;
114
+ cursor: pointer;
115
+ margin-top: 8px;
116
+ transition: all 0.2s;
117
+ }
118
+ .auth-btn:hover:not(:disabled) {
119
+ background: #1976b0;
120
+ }
121
+ .auth-btn:disabled {
122
+ background: #3a5a7a;
123
+ cursor: not-allowed;
124
+ opacity: 0.6;
125
+ }
126
+ .error-msg {
127
+ color: #ff9f8f;
128
+ text-align: center;
129
+ margin-top: 12px;
130
+ font-size: 0.8rem;
131
+ }
132
+ .success-msg {
133
+ color: #8bffb0;
134
+ text-align: center;
135
+ margin-top: 12px;
136
+ }
137
+
138
+ .app-container {
139
+ max-width: 1600px;
140
+ margin: 0 auto;
141
+ }
142
+ .main-header {
143
+ background: #03060ecc;
144
+ backdrop-filter: blur(8px);
145
+ border-radius: 28px;
146
+ padding: 16px 24px;
147
+ margin-bottom: 20px;
148
+ border: 1px solid #2a3a66;
149
+ display: flex;
150
+ justify-content: space-between;
151
+ align-items: center;
152
+ flex-wrap: wrap;
153
+ gap: 15px;
154
+ }
155
+ .title-area {
156
+ display: flex;
157
+ align-items: center;
158
+ gap: 12px;
159
+ color: #b3f0ff;
160
+ font-weight: bold;
161
+ font-size: 1.3rem;
162
+ }
163
+ .status-area {
164
+ display: flex;
165
+ gap: 20px;
166
+ align-items: center;
167
+ background: #00000055;
168
+ padding: 6px 18px;
169
+ border-radius: 40px;
170
+ }
171
+ .led {
172
+ width: 12px;
173
+ height: 12px;
174
+ border-radius: 50%;
175
+ background: #ff4d4d;
176
+ box-shadow: 0 0 6px red;
177
+ }
178
+ .led.connected {
179
+ background: #2eff7a;
180
+ box-shadow: 0 0 12px #00ff88;
181
+ }
182
+ .led.busy {
183
+ background: #ffaa33;
184
+ box-shadow: 0 0 8px #ffaa33;
185
+ animation: pulse 1s infinite;
186
+ }
187
+ @keyframes pulse {
188
+ 0% { opacity: 0.5; }
189
+ 50% { opacity: 1; }
190
+ 100% { opacity: 0.5; }
191
+ }
192
+ .user-badge {
193
+ background: #1e2a3e;
194
+ padding: 4px 12px;
195
+ border-radius: 24px;
196
+ font-size: 0.8rem;
197
+ }
198
+ .logout-btn {
199
+ background: #4a2e2e;
200
+ border: none;
201
+ padding: 6px 16px;
202
+ border-radius: 30px;
203
+ color: white;
204
+ cursor: pointer;
205
+ }
206
+ .logout-btn:hover {
207
+ background: #6d3e3e;
208
+ }
209
+
210
+ .task-status {
211
+ display: flex;
212
+ align-items: center;
213
+ gap: 8px;
214
+ background: #1a2a3a;
215
+ padding: 4px 12px;
216
+ border-radius: 20px;
217
+ font-size: 0.7rem;
218
+ }
219
+ .task-status .busy-text {
220
+ color: #ffaa33;
221
+ }
222
+ .task-status .idle-text {
223
+ color: #6f8faa;
224
+ }
225
+
226
+ .dashboard {
227
+ display: flex;
228
+ gap: 20px;
229
+ flex-wrap: wrap;
230
+ }
231
+ .scripts-panel {
232
+ flex: 2;
233
+ min-width: 350px;
234
+ background: #0e121fcc;
235
+ backdrop-filter: blur(4px);
236
+ border-radius: 28px;
237
+ border: 1px solid #2a3a66;
238
+ padding: 20px;
239
+ height: fit-content;
240
+ }
241
+ .console-panel {
242
+ flex: 3;
243
+ min-width: 450px;
244
+ background: #0e121fcc;
245
+ border-radius: 28px;
246
+ border: 1px solid #2a3a66;
247
+ display: flex;
248
+ flex-direction: column;
249
+ overflow: hidden;
250
+ }
251
+ .panel-title {
252
+ font-size: 1rem;
253
+ color: #8db5e0;
254
+ padding-bottom: 12px;
255
+ border-bottom: 1px solid #253450;
256
+ margin-bottom: 16px;
257
+ display: flex;
258
+ justify-content: space-between;
259
+ align-items: center;
260
+ }
261
+ .script-grid {
262
+ display: flex;
263
+ flex-direction: column;
264
+ gap: 12px;
265
+ max-height: 55vh;
266
+ overflow-y: auto;
267
+ padding-right: 6px;
268
+ margin-bottom: 24px;
269
+ }
270
+ .script-card {
271
+ background: #070c16;
272
+ border-radius: 16px;
273
+ padding: 14px 16px;
274
+ border-left: 4px solid #3f8eff;
275
+ transition: 0.1s;
276
+ }
277
+ .script-card:hover {
278
+ background: #0c1422;
279
+ }
280
+ .script-name {
281
+ font-size: 0.95rem;
282
+ color: #c0dcff;
283
+ margin-bottom: 6px;
284
+ font-weight: bold;
285
+ }
286
+ .script-path {
287
+ font-size: 0.7rem;
288
+ color: #6f8faa;
289
+ margin-bottom: 10px;
290
+ word-break: break-all;
291
+ }
292
+ .script-actions {
293
+ display: flex;
294
+ gap: 10px;
295
+ flex-wrap: wrap;
296
+ align-items: center;
297
+ }
298
+ .btn-exec {
299
+ background: #1f5c3c;
300
+ border: none;
301
+ padding: 6px 20px;
302
+ border-radius: 30px;
303
+ color: white;
304
+ cursor: pointer;
305
+ }
306
+ .btn-exec:hover:not(:disabled) {
307
+ background: #2a7a4e;
308
+ }
309
+ .btn-exec:disabled, .btn-delete:disabled, .btn-add:disabled, #sendFreeCmdBtn:disabled, #refreshScriptsBtn:disabled {
310
+ opacity: 0.5;
311
+ cursor: not-allowed;
312
+ }
313
+ .btn-delete {
314
+ background: #6d2e2e;
315
+ border: none;
316
+ padding: 6px 16px;
317
+ border-radius: 30px;
318
+ color: white;
319
+ cursor: pointer;
320
+ }
321
+ .btn-delete:hover:not(:disabled) {
322
+ background: #8a3a3a;
323
+ }
324
+ .btn-add {
325
+ background: #2c5a6e;
326
+ border: none;
327
+ padding: 8px 20px;
328
+ border-radius: 30px;
329
+ color: white;
330
+ cursor: pointer;
331
+ }
332
+ .btn-add:hover:not(:disabled) {
333
+ background: #3a7a8e;
334
+ }
335
+
336
+ .add-script-form {
337
+ display: flex;
338
+ gap: 10px;
339
+ margin-top: 10px;
340
+ flex-wrap: wrap;
341
+ background: #070c16aa;
342
+ padding: 16px;
343
+ border-radius: 20px;
344
+ border: 1px solid #253450;
345
+ }
346
+ .add-script-form input {
347
+ flex: 1;
348
+ min-width: 120px;
349
+ background: #010101aa;
350
+ border: 1px solid #2d4c7a;
351
+ border-radius: 30px;
352
+ padding: 8px 14px;
353
+ color: white;
354
+ outline: none;
355
+ }
356
+ .add-script-form input:focus {
357
+ border-color: #3fa7ff;
358
+ }
359
+
360
+ .console-output {
361
+ flex: 1;
362
+ background: #050a12;
363
+ margin: 16px;
364
+ border-radius: 20px;
365
+ padding: 16px;
366
+ overflow-y: auto;
367
+ font-family: 'Consolas', 'Fira Code', monospace;
368
+ font-size: 0.8rem;
369
+ color: #cfddee;
370
+ min-height: 400px;
371
+ max-height: 50vh;
372
+ }
373
+ .log-line {
374
+ border-left: 2px solid #2c6f9b;
375
+ padding-left: 10px;
376
+ margin: 5px 0;
377
+ white-space: pre-wrap;
378
+ word-break: break-all;
379
+ }
380
+ .log-error {
381
+ border-left-color: #ff5c5c;
382
+ color: #ffbcac;
383
+ }
384
+ .log-warning {
385
+ border-left-color: #ffaa33;
386
+ color: #ffe0aa;
387
+ }
388
+ .cmd-row {
389
+ display: flex;
390
+ gap: 10px;
391
+ padding: 16px;
392
+ border-top: 1px solid #253450;
393
+ }
394
+ .free-input {
395
+ flex: 1;
396
+ background: #010101aa;
397
+ border: 1px solid #2d4c7a;
398
+ border-radius: 40px;
399
+ padding: 10px 18px;
400
+ color: white;
401
+ font-family: monospace;
402
+ outline: none;
403
+ }
404
+ .free-input:disabled {
405
+ opacity: 0.6;
406
+ }
407
+
408
+ button {
409
+ cursor: pointer;
410
+ transition: 0.1s;
411
+ }
412
+ button:active {
413
+ transform: scale(0.97);
414
+ }
415
+ .badge-count {
416
+ background: #2a4a6a;
417
+ border-radius: 30px;
418
+ padding: 2px 10px;
419
+ font-size: 0.7rem;
420
+ }
421
+
422
+ ::-webkit-scrollbar {
423
+ width: 6px;
424
+ }
425
+ ::-webkit-scrollbar-track {
426
+ background: #0c1222;
427
+ }
428
+ ::-webkit-scrollbar-thumb {
429
+ background: #2f4f7a;
430
+ border-radius: 8px;
431
+ }
432
+
433
+ .section-title {
434
+ font-size: 0.8rem;
435
+ color: #8ea7c5;
436
+ margin: 16px 0 8px 0;
437
+ letter-spacing: 0.5px;
438
+ }
439
+
440
+ .kick-toast {
441
+ position: fixed;
442
+ bottom: 30px;
443
+ left: 50%;
444
+ transform: translateX(-50%);
445
+ background: #ff5c5ccc;
446
+ backdrop-filter: blur(10px);
447
+ color: white;
448
+ padding: 12px 24px;
449
+ border-radius: 40px;
450
+ font-size: 0.9rem;
451
+ z-index: 1001;
452
+ animation: fadeInUp 0.3s ease;
453
+ font-weight: bold;
454
+ border: 1px solid #ff8c8c;
455
+ }
456
+ @keyframes fadeInUp {
457
+ from {
458
+ opacity: 0;
459
+ transform: translateX(-50%) translateY(20px);
460
+ }
461
+ to {
462
+ opacity: 1;
463
+ transform: translateX(-50%) translateY(0);
464
+ }
465
+ }
466
+ </style>
467
+ </head>
468
+ <body>
469
+
470
+ <div id="authOverlay" class="auth-overlay">
471
+ <div class="auth-card">
472
+ <h2>🔐 BAT 脚本管理器</h2>
473
+ <div class="auth-tabs">
474
+ <button id="tabLogin" class="tab-btn active">登录</button>
475
+ <button id="tabRegister" class="tab-btn">注册</button>
476
+ </div>
477
+
478
+ <div id="loginForm">
479
+ <div class="input-group">
480
+ <label>👤 用户名</label>
481
+ <input type="text" id="loginUsername" placeholder="用户名" autocomplete="off">
482
+ </div>
483
+ <div class="input-group">
484
+ <label>🔒 密码</label>
485
+ <input type="password" id="loginPassword" placeholder="密码" autocomplete="off">
486
+ </div>
487
+ <button id="doLoginBtn" class="auth-btn">▶ 登录</button>
488
+ <div id="loginMsg" class="error-msg"></div>
489
+ </div>
490
+
491
+ <div id="registerForm" style="display:none">
492
+ <div class="input-group">
493
+ <label>👤 用户名 (3~20位字母数字下划线)</label>
494
+ <input type="text" id="regUsername" placeholder="用户名" autocomplete="off">
495
+ <div id="usernameHint" class="input-hint"></div>
496
+ </div>
497
+ <div class="input-group">
498
+ <label>🔒 密码 (至少4位)</label>
499
+ <input type="password" id="regPassword" placeholder="密码" autocomplete="off">
500
+ <div id="passwordHint" class="input-hint"></div>
501
+ </div>
502
+ <div class="input-group">
503
+ <label>🔒 确认密码</label>
504
+ <input type="password" id="regConfirmPwd" placeholder="确认密码" autocomplete="off">
505
+ <div id="confirmHint" class="input-hint"></div>
506
+ </div>
507
+ <button id="doRegisterBtn" class="auth-btn">📝 注册新账号</button>
508
+ <div id="regMsg" class="error-msg"></div>
509
+ </div>
510
+
511
+ <div style="font-size:0.65rem; text-align:center; margin-top:18px; color:#5f7392;">
512
+ 💡 默认账号: admin / admin123
513
+ </div>
514
+ </div>
515
+ </div>
516
+
517
+ <div id="mainApp" style="display:none">
518
+ <div class="app-container">
519
+ <div class="main-header">
520
+ <div class="title-area">
521
+ <span>📜 BAT 脚本管理器</span>
522
+ <span class="badge-count" id="scriptCount">0</span>
523
+ </div>
524
+ <div class="status-area">
525
+ <div class="led" id="wsLed"></div>
526
+ <span id="wsStatus">未连接</span>
527
+ <div class="task-status">
528
+ <div class="led" id="taskLed"></div>
529
+ <span id="taskStatusText">空闲</span>
530
+ </div>
531
+ <div class="user-badge">👤 <span id="currentUser">--</span></div>
532
+ <button id="logoutBtn" class="logout-btn">退出登录</button>
533
+ </div>
534
+ </div>
535
+
536
+ <div class="dashboard">
537
+ <div class="scripts-panel">
538
+ <div class="panel-title">
539
+ <span>📂 脚本按钮列表</span>
540
+ <button id="refreshScriptsBtn" style="background:#2c3e5a; padding:4px 12px;">🔄 刷新</button>
541
+ </div>
542
+
543
+ <div id="scriptListContainer" class="script-grid">
544
+ <div style="color:#6f8faa; text-align:center; padding:20px;">✨ 暂无脚本,请使用下方表单添加</div>
545
+ </div>
546
+
547
+ <div class="section-title">➕ 添加新脚本</div>
548
+ <div class="add-script-form">
549
+ <input type="text" id="newScriptName" placeholder="脚本名称 (例如: 启动服务)" autocomplete="off">
550
+ <input type="text" id="newScriptPath" placeholder="脚本路径 (例如: D:/server/start.bat)" autocomplete="off">
551
+ <button id="addScriptBtn" class="btn-add">➕ 添加脚本</button>
552
+ </div>
553
+ <div style="font-size:0.7rem; color:#5f7a9a; margin-top: 12px; padding-left: 6px;">
554
+ 💡 提示:点击执行按钮即可运行对应的 BAT 脚本,同一时间只能执行一个任务
555
+ </div>
556
+ </div>
557
+
558
+ <div class="console-panel">
559
+ <div class="panel-title" style="padding:16px 16px 0 16px;">
560
+ <span>🖥️ 执行输出控制台</span>
561
+ <button id="clearConsoleBtn" style="background:#2c5a6e; padding:4px 14px;">🗑 清屏</button>
562
+ </div>
563
+ <div class="console-output" id="consoleOutput">
564
+ <pre id="logArea">━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
565
+ ✅ 欢迎使用 BAT 脚本管理器
566
+ 🔌 等待 WebSocket 连接...
567
+ 📌 左侧可添加/管理多个脚本按钮
568
+ 🔒 任务互斥锁:同一时间只能执行一个任务
569
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━</pre>
570
+ </div>
571
+ <div class="cmd-row">
572
+ <input type="text" id="freeCmdInput" class="free-input" placeholder="自由命令模式 (例如: ping 127.0.0.1)">
573
+ <button id="sendFreeCmdBtn" class="btn-add">▶ 执行命令</button>
574
+ </div>
575
+ </div>
576
+ </div>
577
+ </div>
578
+ </div>
579
+
580
+ <script>
581
+ (function() {
582
+ let ws = null;
583
+ let currentUser = null;
584
+ let scripts = [];
585
+ let reconnectTimer = null;
586
+ let isKicked = false;
587
+
588
+ // ========== 全局任务状态(由后端广播同步)==========
589
+ let globalTaskRunning = false; // 后端是否有任务在执行
590
+ let globalTaskUser = null;
591
+ let globalTaskName = null;
592
+
593
+ // ========== 本地任务执行状态 ==========
594
+ let localTaskRunning = false; // 本地发起任务是否在执行
595
+ let pendingEndTimer = null;
596
+
597
+ const WS_URL = `ws://${window.location.hostname}:8080`;
598
+
599
+ // DOM 元素
600
+ const authOverlay = document.getElementById('authOverlay');
601
+ const mainApp = document.getElementById('mainApp');
602
+ const loginForm = document.getElementById('loginForm');
603
+ const registerForm = document.getElementById('registerForm');
604
+ const tabLogin = document.getElementById('tabLogin');
605
+ const tabRegister = document.getElementById('tabRegister');
606
+ const loginUsername = document.getElementById('loginUsername');
607
+ const loginPassword = document.getElementById('loginPassword');
608
+ const doLoginBtn = document.getElementById('doLoginBtn');
609
+ const loginMsg = document.getElementById('loginMsg');
610
+ const regUsername = document.getElementById('regUsername');
611
+ const regPassword = document.getElementById('regPassword');
612
+ const regConfirmPwd = document.getElementById('regConfirmPwd');
613
+ const doRegisterBtn = document.getElementById('doRegisterBtn');
614
+ const regMsg = document.getElementById('regMsg');
615
+ const usernameHint = document.getElementById('usernameHint');
616
+ const passwordHint = document.getElementById('passwordHint');
617
+ const confirmHint = document.getElementById('confirmHint');
618
+ const currentUserSpan = document.getElementById('currentUser');
619
+ const logoutBtn = document.getElementById('logoutBtn');
620
+ const scriptListContainer = document.getElementById('scriptListContainer');
621
+ const scriptCountSpan = document.getElementById('scriptCount');
622
+ const refreshScriptsBtn = document.getElementById('refreshScriptsBtn');
623
+ const newScriptName = document.getElementById('newScriptName');
624
+ const newScriptPath = document.getElementById('newScriptPath');
625
+ const addScriptBtn = document.getElementById('addScriptBtn');
626
+ const logArea = document.getElementById('logArea');
627
+ const clearConsoleBtn = document.getElementById('clearConsoleBtn');
628
+ const freeCmdInput = document.getElementById('freeCmdInput');
629
+ const sendFreeCmdBtn = document.getElementById('sendFreeCmdBtn');
630
+ const wsLed = document.getElementById('wsLed');
631
+ const wsStatus = document.getElementById('wsStatus');
632
+ const taskLed = document.getElementById('taskLed');
633
+ const taskStatusText = document.getElementById('taskStatusText');
634
+
635
+ // 更新UI上的任务状态显示(基于全局后端状态)
636
+ function updateGlobalTaskUI() {
637
+ if (globalTaskRunning) {
638
+ taskLed.classList.add('busy');
639
+ taskLed.classList.remove('connected');
640
+ taskStatusText.innerHTML = `<span class="busy-text">⏳ 执行中: ${globalTaskName || '任务'} (${globalTaskUser || '?'})</span>`;
641
+ // 全局有任务时,禁用所有操作按钮
642
+ document.querySelectorAll('.btn-exec, .btn-delete, .btn-add, #sendFreeCmdBtn, #refreshScriptsBtn').forEach(btn => {
643
+ if (btn) btn.disabled = true;
644
+ });
645
+ if (freeCmdInput) freeCmdInput.disabled = true;
646
+ } else {
647
+ taskLed.classList.remove('busy');
648
+ taskLed.classList.add('connected');
649
+ taskStatusText.innerHTML = `<span class="idle-text">✅ 空闲</span>`;
650
+ // 全局空闲时,启用所有按钮
651
+ document.querySelectorAll('.btn-exec, .btn-delete, .btn-add, #sendFreeCmdBtn, #refreshScriptsBtn').forEach(btn => {
652
+ if (btn) btn.disabled = false;
653
+ });
654
+ if (freeCmdInput) freeCmdInput.disabled = false;
655
+ }
656
+ }
657
+
658
+ // 本地任务开始(只在用户自己发起任务时调用)
659
+ function startLocalTask(user, taskName) {
660
+ if (globalTaskRunning) {
661
+ addLog(`⚠️ [任务冲突] 当前有任务 [${globalTaskName}] 正在执行 (${globalTaskUser}),请稍后再试`, 'warning');
662
+ return false;
663
+ }
664
+ if (pendingEndTimer) {
665
+ clearTimeout(pendingEndTimer);
666
+ pendingEndTimer = null;
667
+ }
668
+ localTaskRunning = true;
669
+ addLog(`🔒 [开始] ${taskName} (${user})`, 'info');
670
+ return true;
671
+ }
672
+
673
+ // 本地任务结束
674
+ function endLocalTask() {
675
+ if (pendingEndTimer) {
676
+ clearTimeout(pendingEndTimer);
677
+ pendingEndTimer = null;
678
+ }
679
+ pendingEndTimer = setTimeout(() => {
680
+ if (localTaskRunning) {
681
+ addLog(`🔓 [完成] 本地任务结束`, 'success');
682
+ localTaskRunning = false;
683
+ }
684
+ pendingEndTimer = null;
685
+ }, 300);
686
+ }
687
+
688
+ // 强制重置本地任务状态
689
+ function forceResetLocalTask() {
690
+ if (pendingEndTimer) {
691
+ clearTimeout(pendingEndTimer);
692
+ pendingEndTimer = null;
693
+ }
694
+ localTaskRunning = false;
695
+ }
696
+
697
+ // 从后端同步任务状态
698
+ function syncTaskStatusFromBackend(isRunning, user, task) {
699
+ globalTaskRunning = isRunning;
700
+ globalTaskUser = user;
701
+ globalTaskName = task;
702
+ updateGlobalTaskUI();
703
+
704
+ // 如果后端任务结束,且本地任务标记还在,也重置本地标记
705
+ if (!isRunning) {
706
+ forceResetLocalTask();
707
+ }
708
+ }
709
+
710
+ function showToast(message, isError = true) {
711
+ const toast = document.createElement('div');
712
+ toast.className = 'kick-toast';
713
+ toast.style.background = isError ? '#ff5c5ccc' : '#2c5a6ecc';
714
+ toast.innerText = message;
715
+ document.body.appendChild(toast);
716
+ setTimeout(() => toast.remove(), 3000);
717
+ }
718
+
719
+ function redirectToLogin(reason) {
720
+ if (ws) { try { ws.close(); } catch(e) {} ws = null; }
721
+ forceResetLocalTask();
722
+ syncTaskStatusFromBackend(false, null, null);
723
+ currentUser = null;
724
+ scripts = [];
725
+ mainApp.style.display = 'none';
726
+ authOverlay.style.display = 'flex';
727
+ loginUsername.value = '';
728
+ loginPassword.value = '';
729
+ loginMsg.innerText = '';
730
+ regMsg.innerText = '';
731
+
732
+ if (regUsername) {
733
+ regUsername.value = '';
734
+ regPassword.value = '';
735
+ regConfirmPwd.value = '';
736
+ usernameHint.innerHTML = '';
737
+ passwordHint.innerHTML = '';
738
+ confirmHint.innerHTML = '';
739
+ regValidation = { usernameValid: false, passwordValid: false, confirmValid: false };
740
+ if (doRegisterBtn) doRegisterBtn.disabled = false;
741
+ }
742
+
743
+ if (reason === 'kicked') {
744
+ showToast('⚠️ 您的账号在其他地方登录,您已被踢下线!');
745
+ loginMsg.innerText = '⚠️ 您的账号在其他地方登录,请重新登录';
746
+ loginMsg.style.color = '#ffcc88';
747
+ } else if (reason === 'logout') {
748
+ showToast('👋 已安全退出登录', false);
749
+ }
750
+ updateWsStatus(false);
751
+ isKicked = false;
752
+ }
753
+
754
+ function addLog(msg, type) {
755
+ type = type || 'info';
756
+ const div = document.createElement('div');
757
+ div.className = 'log-line';
758
+ if (type === 'error') div.classList.add('log-error');
759
+ if (type === 'warning') div.classList.add('log-warning');
760
+ const time = new Date().toLocaleTimeString();
761
+ let prefix = '';
762
+ if (type === 'error') prefix = '❌ ';
763
+ else if (type === 'success') prefix = '✅ ';
764
+ else if (type === 'warning') prefix = '⚠️ ';
765
+ else prefix = 'ℹ️ ';
766
+ div.innerHTML = '<span style="color:#7f8faa;">[' + time + ']</span> ' + prefix + (msg || '').replace(/[&<>]/g, function(m) {
767
+ return {'&':'&amp;','<':'&lt;','>':'&gt;'}[m] || m;
768
+ });
769
+ logArea.appendChild(div);
770
+ const consoleDiv = document.getElementById('consoleOutput');
771
+ if (consoleDiv) consoleDiv.scrollTop = logArea.scrollHeight;
772
+ }
773
+
774
+ function clearConsole() {
775
+ logArea.innerHTML = '';
776
+ addLog('控制台已清空', 'success');
777
+ }
778
+
779
+ function updateWsStatus(connected) {
780
+ if (connected) {
781
+ wsLed.classList.add('connected');
782
+ wsStatus.innerText = '已连接';
783
+ wsStatus.style.color = '#a0ffc0';
784
+ } else {
785
+ wsLed.classList.remove('connected');
786
+ wsStatus.innerText = '未连接';
787
+ wsStatus.style.color = '#ffa098';
788
+ }
789
+ }
790
+
791
+ function sendRequest(req) {
792
+ if (ws && ws.readyState === WebSocket.OPEN) {
793
+ ws.send(JSON.stringify(req));
794
+ } else {
795
+ addLog('⚠️ WebSocket 未连接,无法发送请求', 'error');
796
+ }
797
+ }
798
+
799
+ let regValidation = { usernameValid: false, passwordValid: false, confirmValid: false };
800
+
801
+ function updateRegisterButtonState() {
802
+ const canRegister = regValidation.usernameValid && regValidation.passwordValid && regValidation.confirmValid;
803
+ doRegisterBtn.disabled = !canRegister;
804
+ }
805
+
806
+ function validateUsernameFormat(username) {
807
+ const regex = /^[a-zA-Z0-9_]{3,20}$/;
808
+ if (!username) return { valid: false, msg: '' };
809
+ if (!regex.test(username)) {
810
+ return { valid: false, msg: '❌ 用户名须为3~20位字母、数字或下划线' };
811
+ }
812
+ return { valid: true, msg: '✅ 格式正确' };
813
+ }
814
+
815
+ function validatePassword(password) {
816
+ if (!password) return { valid: false, msg: '' };
817
+ if (password.length < 4) {
818
+ return { valid: false, msg: '❌ 密码至少需要4位' };
819
+ }
820
+ return { valid: true, msg: '✅ 密码强度足够' };
821
+ }
822
+
823
+ function validateConfirm(password, confirm) {
824
+ if (!confirm) return { valid: false, msg: '' };
825
+ if (password !== confirm) {
826
+ return { valid: false, msg: '❌ 两次输入的密码不一致' };
827
+ }
828
+ return { valid: true, msg: '✅ 密码一致' };
829
+ }
830
+
831
+ regUsername.addEventListener('input', function() {
832
+ const username = this.value.trim();
833
+ const format = validateUsernameFormat(username);
834
+ regValidation.usernameValid = format.valid;
835
+ if (username && format.valid) {
836
+ usernameHint.innerHTML = format.msg;
837
+ usernameHint.className = 'input-hint success';
838
+ this.classList.remove('input-error');
839
+ this.classList.add('input-valid');
840
+ } else if (username && !format.valid) {
841
+ usernameHint.innerHTML = format.msg;
842
+ usernameHint.className = 'input-hint error';
843
+ this.classList.add('input-error');
844
+ this.classList.remove('input-valid');
845
+ } else {
846
+ usernameHint.innerHTML = '';
847
+ this.classList.remove('input-error', 'input-valid');
848
+ }
849
+ updateRegisterButtonState();
850
+ });
851
+
852
+ regPassword.addEventListener('input', function() {
853
+ const pwd = this.value;
854
+ const validation = validatePassword(pwd);
855
+ regValidation.passwordValid = validation.valid;
856
+ if (pwd && validation.valid) {
857
+ passwordHint.innerHTML = validation.msg;
858
+ passwordHint.className = 'input-hint success';
859
+ this.classList.add('input-valid');
860
+ this.classList.remove('input-error');
861
+ } else if (pwd && !validation.valid) {
862
+ passwordHint.innerHTML = validation.msg;
863
+ passwordHint.className = 'input-hint error';
864
+ this.classList.add('input-error');
865
+ this.classList.remove('input-valid');
866
+ } else {
867
+ passwordHint.innerHTML = '';
868
+ this.classList.remove('input-error', 'input-valid');
869
+ }
870
+ const confirm = regConfirmPwd.value;
871
+ if (confirm) {
872
+ const confirmValidation = validateConfirm(pwd, confirm);
873
+ regValidation.confirmValid = confirmValidation.valid;
874
+ if (confirmValidation.valid) {
875
+ confirmHint.innerHTML = confirmValidation.msg;
876
+ confirmHint.className = 'input-hint success';
877
+ regConfirmPwd.classList.add('input-valid');
878
+ regConfirmPwd.classList.remove('input-error');
879
+ } else {
880
+ confirmHint.innerHTML = confirmValidation.msg;
881
+ confirmHint.className = 'input-hint error';
882
+ regConfirmPwd.classList.add('input-error');
883
+ regConfirmPwd.classList.remove('input-valid');
884
+ }
885
+ }
886
+ updateRegisterButtonState();
887
+ });
888
+
889
+ regConfirmPwd.addEventListener('input', function() {
890
+ const pwd = regPassword.value;
891
+ const confirm = this.value;
892
+ const validation = validateConfirm(pwd, confirm);
893
+ regValidation.confirmValid = validation.valid;
894
+ if (confirm && validation.valid) {
895
+ confirmHint.innerHTML = validation.msg;
896
+ confirmHint.className = 'input-hint success';
897
+ this.classList.add('input-valid');
898
+ this.classList.remove('input-error');
899
+ } else if (confirm && !validation.valid) {
900
+ confirmHint.innerHTML = validation.msg;
901
+ confirmHint.className = 'input-hint error';
902
+ this.classList.add('input-error');
903
+ this.classList.remove('input-valid');
904
+ } else {
905
+ confirmHint.innerHTML = '';
906
+ this.classList.remove('input-error', 'input-valid');
907
+ }
908
+ updateRegisterButtonState();
909
+ });
910
+
911
+ function renderScripts() {
912
+ if (!scripts || scripts.length === 0) {
913
+ scriptListContainer.innerHTML = '<div style="color:#6f8faa;text-align:center;padding:20px;">📭 暂无脚本,请使用上方表单添加</div>';
914
+ scriptCountSpan.innerText = '0';
915
+ return;
916
+ }
917
+ scriptCountSpan.innerText = scripts.length;
918
+ let html = '';
919
+ for (let i = 0; i < scripts.length; i++) {
920
+ var s = scripts[i];
921
+ html += '<div class="script-card">' +
922
+ '<div class="script-name">📄 ' + escapeHtml(s.name) + '</div>' +
923
+ '<div class="script-path">📁 ' + escapeHtml(s.path) + '</div>' +
924
+ '<div class="script-actions">' +
925
+ '<button class="btn-exec" data-path="' + escapeHtml(s.path) + '" data-name="' + escapeHtml(s.name) + '">⚡ 执行</button>' +
926
+ '<button class="btn-delete" data-id="' + s.id + '">🗑 删除</button>' +
927
+ '</div></div>';
928
+ }
929
+ scriptListContainer.innerHTML = html;
930
+
931
+ document.querySelectorAll('.btn-exec').forEach(btn => {
932
+ btn.addEventListener('click', function(e) {
933
+ var path = this.getAttribute('data-path');
934
+ var name = this.getAttribute('data-name');
935
+ // 先检查全局后端任务状态
936
+ if (globalTaskRunning) {
937
+ addLog(`⚠️ [任务冲突] 当前有任务 [${globalTaskName}] 正在执行 (${globalTaskUser}),请稍后再试`, 'warning');
938
+ return;
939
+ }
940
+ // 再检查本地任务状态
941
+ if (!startLocalTask(currentUser, name)) {
942
+ return;
943
+ }
944
+ sendRequest({ type: 'exec_bat', path: path, args: '' });
945
+ addLog(`🚀 执行脚本: ${path}`, 'info');
946
+ });
947
+ });
948
+
949
+ document.querySelectorAll('.btn-delete').forEach(btn => {
950
+ btn.addEventListener('click', function(e) {
951
+ if (globalTaskRunning) {
952
+ addLog(`⚠️ [任务冲突] 当前有任务 [${globalTaskName}] 正在执行,无法删除脚本`, 'warning');
953
+ return;
954
+ }
955
+ var id = this.getAttribute('data-id');
956
+ sendRequest({ type: 'delete_script', id: id });
957
+ addLog(`🗑 请求删除脚本 ID: ${id}`, 'info');
958
+ });
959
+ });
960
+ updateGlobalTaskUI();
961
+ }
962
+
963
+ function escapeHtml(str) {
964
+ if (!str) return '';
965
+ return str.replace(/[&<>]/g, function(m) {
966
+ if (m === '&') return '&amp;';
967
+ if (m === '<') return '&lt;';
968
+ if (m === '>') return '&gt;';
969
+ return m;
970
+ });
971
+ }
972
+
973
+ function handleMessage(data) {
974
+ try {
975
+ var json = JSON.parse(data);
976
+
977
+ // 处理踢下线
978
+ if (json.type === 'kicked') {
979
+ addLog('⚠️ ' + json.message, 'error');
980
+ isKicked = true;
981
+ forceResetLocalTask();
982
+ syncTaskStatusFromBackend(false, null, null);
983
+ redirectToLogin('kicked');
984
+ return;
985
+ }
986
+
987
+ // 处理后端广播的任务状态
988
+ if (json.type === 'task_status') {
989
+ syncTaskStatusFromBackend(json.isRunning, json.currentUser, json.currentTask);
990
+ return;
991
+ }
992
+
993
+ // 处理任务开始通知(自己的任务)
994
+ if (json.type === 'task_start') {
995
+ // 后端确认任务开始,同步状态
996
+ syncTaskStatusFromBackend(true, currentUser, json.taskDesc);
997
+ return;
998
+ }
999
+
1000
+ // 处理任务结束通知
1001
+ if (json.type === 'task_end') {
1002
+ syncTaskStatusFromBackend(false, null, null);
1003
+ if (json.message) {
1004
+ addLog(json.message, 'success');
1005
+ }
1006
+ return;
1007
+ }
1008
+
1009
+ // 处理任务繁忙(后端拒绝)
1010
+ if (json.type === 'task_busy') {
1011
+ addLog(json.message, 'warning');
1012
+ forceResetLocalTask();
1013
+ // 从后端同步最新状态
1014
+ if (json.currentUser && json.currentTask) {
1015
+ syncTaskStatusFromBackend(true, json.currentUser, json.currentTask);
1016
+ }
1017
+ return;
1018
+ }
1019
+
1020
+ if (json.type === 'login_result') {
1021
+ if (json.success) {
1022
+ currentUser = json.username;
1023
+ scripts = json.scripts || [];
1024
+ currentUserSpan.innerText = json.username;
1025
+ authOverlay.style.display = 'none';
1026
+ mainApp.style.display = 'block';
1027
+ renderScripts();
1028
+ addLog(`✅ 登录成功,欢迎 ${json.username}`, 'success');
1029
+ isKicked = false;
1030
+ forceResetLocalTask();
1031
+ // 登录后主动查询当前任务状态
1032
+ sendRequest({ type: 'check_task_status' });
1033
+ } else {
1034
+ loginMsg.innerText = json.message || '登录失败';
1035
+ }
1036
+ return;
1037
+ }
1038
+
1039
+ if (json.type === 'register_result') {
1040
+ doRegisterBtn.disabled = false;
1041
+ doRegisterBtn.innerText = '📝 注册新账号';
1042
+ if (json.success) {
1043
+ regMsg.className = 'success-msg';
1044
+ regMsg.innerText = '✅ 注册成功!请登录';
1045
+ regUsername.value = '';
1046
+ regPassword.value = '';
1047
+ regConfirmPwd.value = '';
1048
+ usernameHint.innerHTML = '';
1049
+ passwordHint.innerHTML = '';
1050
+ confirmHint.innerHTML = '';
1051
+ regValidation = { usernameValid: false, passwordValid: false, confirmValid: false };
1052
+ updateRegisterButtonState();
1053
+ setTimeout(() => tabLogin.click(), 1500);
1054
+ } else {
1055
+ regMsg.className = 'error-msg';
1056
+ regMsg.innerText = json.message || '注册失败';
1057
+ if (json.message && json.message.includes('已存在')) {
1058
+ regUsername.classList.add('input-error');
1059
+ usernameHint.innerHTML = '❌ 该用户名已被占用,请换一个';
1060
+ usernameHint.className = 'input-hint error';
1061
+ regValidation.usernameValid = false;
1062
+ updateRegisterButtonState();
1063
+ }
1064
+ }
1065
+ return;
1066
+ }
1067
+
1068
+ if (json.type === 'script_added' || json.type === 'script_deleted' || json.type === 'script_list') {
1069
+ if (json.scripts) {
1070
+ scripts = json.scripts;
1071
+ renderScripts();
1072
+ let actionMsg = '';
1073
+ if (json.type === 'script_added') actionMsg = '脚本添加成功';
1074
+ else if (json.type === 'script_deleted') actionMsg = '脚本已删除';
1075
+ else actionMsg = '脚本列表已刷新';
1076
+ addLog(actionMsg, 'success');
1077
+ }
1078
+ return;
1079
+ }
1080
+
1081
+ if (json.type === 'exec_result') {
1082
+ if (json.output) {
1083
+ addLog(`📢 执行输出:\n${json.output}`, 'output');
1084
+ } else if (json.message) {
1085
+ addLog(json.message, json.success ? 'success' : 'error');
1086
+ }
1087
+ endLocalTask();
1088
+ return;
1089
+ }
1090
+
1091
+ if (json.type === 'cmd_result') {
1092
+ if (json.output) {
1093
+ addLog(`💻 命令输出:\n${json.output}`, 'output');
1094
+ } else {
1095
+ addLog(json.message || '命令执行完成', 'info');
1096
+ }
1097
+ endLocalTask();
1098
+ return;
1099
+ }
1100
+
1101
+ if (json.message) {
1102
+ addLog(json.message, json.type === 'error' ? 'error' : 'info');
1103
+ } else {
1104
+ addLog(data, 'output');
1105
+ }
1106
+
1107
+ } catch(e) {
1108
+ addLog(data, 'output');
1109
+ if (typeof data === 'string' && (data.includes('脚本执行完成') || data.includes('所有操作完成'))) {
1110
+ endLocalTask();
1111
+ }
1112
+ }
1113
+ }
1114
+
1115
+ function connectWebSocket() {
1116
+ if (ws && ws.readyState === WebSocket.OPEN) return;
1117
+
1118
+ addLog(`🔌 连接 WebSocket ${WS_URL}...`, 'info');
1119
+ ws = new WebSocket(WS_URL);
1120
+
1121
+ ws.onopen = function() {
1122
+ addLog('🔗 WebSocket 已连接', 'success');
1123
+ updateWsStatus(true);
1124
+ if (reconnectTimer) clearInterval(reconnectTimer);
1125
+ if (currentUser && !isKicked) {
1126
+ setTimeout(() => {
1127
+ sendRequest({ type: 'get_scripts' });
1128
+ sendRequest({ type: 'check_task_status' });
1129
+ }, 300);
1130
+ }
1131
+ };
1132
+
1133
+ ws.onmessage = function(e) { handleMessage(e.data); };
1134
+ ws.onerror = function() { addLog('⚠️ WebSocket 错误', 'error'); updateWsStatus(false); };
1135
+ ws.onclose = function() {
1136
+ addLog('🔌 连接已断开', 'error');
1137
+ updateWsStatus(false);
1138
+ forceResetLocalTask();
1139
+ if (!isKicked && currentUser) {
1140
+ if (reconnectTimer) clearInterval(reconnectTimer);
1141
+ reconnectTimer = setInterval(() => {
1142
+ if ((!ws || ws.readyState !== WebSocket.OPEN) && !isKicked) {
1143
+ connectWebSocket();
1144
+ }
1145
+ }, 5000);
1146
+ }
1147
+ };
1148
+ }
1149
+
1150
+ function doLogin() {
1151
+ var user = loginUsername.value.trim();
1152
+ var pwd = loginPassword.value;
1153
+ if (!user || !pwd) { loginMsg.innerText = '请输入用户名和密码'; return; }
1154
+ connectWebSocket();
1155
+ setTimeout(() => sendRequest({ type: 'login', username: user, password: pwd }), 200);
1156
+ }
1157
+
1158
+ function doRegister() {
1159
+ var user = regUsername.value.trim();
1160
+ var pwd = regPassword.value;
1161
+ var confirm = regConfirmPwd.value;
1162
+ regMsg.innerText = '';
1163
+ if (!user || !pwd) { regMsg.innerText = '请填写用户名和密码'; return; }
1164
+ if (pwd !== confirm) { regMsg.innerText = '两次密码不一致'; return; }
1165
+ if (!/^[a-zA-Z0-9_]{3,20}$/.test(user)) { regMsg.innerText = '用户名须为3~20位字母数字下划线'; return; }
1166
+ if (pwd.length < 4) { regMsg.innerText = '密码至少4位'; return; }
1167
+ doRegisterBtn.disabled = true;
1168
+ doRegisterBtn.innerText = '⏳ 注册中...';
1169
+ connectWebSocket();
1170
+ setTimeout(() => sendRequest({ type: 'register', username: user, password: pwd }), 200);
1171
+ }
1172
+
1173
+ function doLogout() { redirectToLogin('logout'); }
1174
+
1175
+ function refreshScripts() {
1176
+ if (!currentUser) { addLog('请先登录', 'error'); return; }
1177
+ if (globalTaskRunning) {
1178
+ addLog(`⚠️ [任务冲突] 当前有任务 [${globalTaskName}] 正在执行,请稍后再刷新`, 'warning');
1179
+ return;
1180
+ }
1181
+ sendRequest({ type: 'get_scripts' });
1182
+ }
1183
+
1184
+ function addScript() {
1185
+ if (!currentUser) { addLog('请先登录', 'error'); return; }
1186
+ if (globalTaskRunning) {
1187
+ addLog(`⚠️ [任务冲突] 当前有任务 [${globalTaskName}] 正在执行,请稍后再添加`, 'warning');
1188
+ return;
1189
+ }
1190
+ var name = newScriptName.value.trim();
1191
+ var path = newScriptPath.value.trim();
1192
+ if (!name || !path) { addLog('❌ 请填写脚本名称和路径', 'error'); return; }
1193
+ sendRequest({ type: 'add_script', name: name, path: path });
1194
+ newScriptName.value = '';
1195
+ newScriptPath.value = '';
1196
+ }
1197
+
1198
+ function sendFreeCmd() {
1199
+ if (!currentUser) { addLog('请先登录', 'error'); return; }
1200
+ var cmd = freeCmdInput.value.trim();
1201
+ if (!cmd) { addLog('请输入要执行的命令', 'error'); return; }
1202
+ if (globalTaskRunning) {
1203
+ addLog(`⚠️ [任务冲突] 当前有任务 [${globalTaskName}] 正在执行 (${globalTaskUser}),请稍后再试`, 'warning');
1204
+ return;
1205
+ }
1206
+ if (!startLocalTask(currentUser, `自由命令: ${cmd.substring(0, 30)}`)) return;
1207
+ sendRequest({ type: 'free_cmd', command: cmd });
1208
+ freeCmdInput.value = '';
1209
+ }
1210
+
1211
+ tabLogin.onclick = () => {
1212
+ tabLogin.classList.add('active');
1213
+ tabRegister.classList.remove('active');
1214
+ loginForm.style.display = 'block';
1215
+ registerForm.style.display = 'none';
1216
+ loginMsg.innerText = '';
1217
+ regMsg.innerText = '';
1218
+ doRegisterBtn.disabled = false;
1219
+ doRegisterBtn.innerText = '📝 注册新账号';
1220
+ };
1221
+
1222
+ tabRegister.onclick = () => {
1223
+ tabRegister.classList.add('active');
1224
+ tabLogin.classList.remove('active');
1225
+ loginForm.style.display = 'none';
1226
+ registerForm.style.display = 'block';
1227
+ loginMsg.innerText = '';
1228
+ regMsg.innerText = '';
1229
+ doRegisterBtn.disabled = false;
1230
+ doRegisterBtn.innerText = '📝 注册新账号';
1231
+ };
1232
+
1233
+ doLoginBtn.onclick = doLogin;
1234
+ doRegisterBtn.onclick = doRegister;
1235
+ logoutBtn.onclick = doLogout;
1236
+ refreshScriptsBtn.onclick = refreshScripts;
1237
+ addScriptBtn.onclick = addScript;
1238
+ clearConsoleBtn.onclick = clearConsole;
1239
+ sendFreeCmdBtn.onclick = sendFreeCmd;
1240
+
1241
+ loginPassword.addEventListener('keypress', e => { if (e.key === 'Enter') doLogin(); });
1242
+ freeCmdInput.addEventListener('keypress', e => { if (e.key === 'Enter') sendFreeCmd(); });
1243
+
1244
+ connectWebSocket();
1245
+ })();
1246
+ </script>
1247
+ </body>
1248
+ </html>