cdp-tunnel 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.
Files changed (46) hide show
  1. package/.github/workflows/publish.yml +92 -0
  2. package/.github/workflows/release-assets.yml +50 -0
  3. package/LICENSE +81 -0
  4. package/PUBLISH.md +65 -0
  5. package/README.md +228 -0
  6. package/cli/guide.html +753 -0
  7. package/cli/icon.svg +13 -0
  8. package/cli/icon128.png +0 -0
  9. package/cli/index.js +357 -0
  10. package/docs/README_CN.md +204 -0
  11. package/docs/config-page-screenshot.png +0 -0
  12. package/extension-new/background.js +294 -0
  13. package/extension-new/cdp/handler/forward.js +44 -0
  14. package/extension-new/cdp/handler/local.js +233 -0
  15. package/extension-new/cdp/handler/special.js +442 -0
  16. package/extension-new/cdp/index.js +104 -0
  17. package/extension-new/cdp/response.js +49 -0
  18. package/extension-new/config-page-preview.html +769 -0
  19. package/extension-new/config-page.js +318 -0
  20. package/extension-new/core/debugger.js +310 -0
  21. package/extension-new/core/state.js +384 -0
  22. package/extension-new/core/websocket.js +326 -0
  23. package/extension-new/features/automation-badge.js +113 -0
  24. package/extension-new/features/screencast.js +221 -0
  25. package/extension-new/icons/icon128.png +0 -0
  26. package/extension-new/icons/icon16.png +0 -0
  27. package/extension-new/icons/icon48.png +0 -0
  28. package/extension-new/manifest.json +39 -0
  29. package/extension-new/popup.html +72 -0
  30. package/extension-new/popup.js +34 -0
  31. package/extension-new/utils/config.js +20 -0
  32. package/extension-new/utils/diagnostics.js +560 -0
  33. package/extension-new/utils/helpers.js +25 -0
  34. package/extension-new/utils/logger.js +64 -0
  35. package/package.json +42 -0
  36. package/server/modules/config.js +28 -0
  37. package/server/modules/logger.js +197 -0
  38. package/server/proxy-server.js +1431 -0
  39. package/tests/playwright-demo.js +45 -0
  40. package/tests/playwright-interactive.js +261 -0
  41. package/tests/playwright-multi-demo.js +60 -0
  42. package/tests/playwright-multi.js +85 -0
  43. package/tests/playwright-single.js +41 -0
  44. package/tests/screenshot-config.js +35 -0
  45. package/tests/test-client.js +89 -0
  46. package/tests/test-multi-client.js +129 -0
@@ -0,0 +1,769 @@
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>CDP Bridge - 配置管理</title>
7
+ <style>
8
+ * {
9
+ margin: 0;
10
+ padding: 0;
11
+ box-sizing: border-box;
12
+ }
13
+
14
+ html, body {
15
+ height: 100%;
16
+ overflow: hidden;
17
+ }
18
+
19
+ body {
20
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
21
+ background: #f0f2f5;
22
+ display: flex;
23
+ flex-direction: column;
24
+ }
25
+
26
+ .navbar {
27
+ background: white;
28
+ border-bottom: 1px solid #e5e7eb;
29
+ padding: 0 24px;
30
+ height: 56px;
31
+ display: flex;
32
+ align-items: center;
33
+ justify-content: space-between;
34
+ flex-shrink: 0;
35
+ }
36
+
37
+ .navbar-brand {
38
+ display: flex;
39
+ align-items: center;
40
+ gap: 10px;
41
+ }
42
+
43
+ .navbar-brand .logo {
44
+ width: 32px;
45
+ height: 32px;
46
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
47
+ border-radius: 8px;
48
+ display: flex;
49
+ align-items: center;
50
+ justify-content: center;
51
+ font-size: 18px;
52
+ }
53
+
54
+ .navbar-brand h1 {
55
+ font-size: 18px;
56
+ font-weight: 600;
57
+ color: #1f2937;
58
+ }
59
+
60
+ .status-badge {
61
+ display: flex;
62
+ align-items: center;
63
+ gap: 6px;
64
+ padding: 6px 12px;
65
+ background: #ecfdf5;
66
+ border-radius: 16px;
67
+ font-size: 13px;
68
+ color: #059669;
69
+ }
70
+
71
+ .status-badge .dot {
72
+ width: 6px;
73
+ height: 6px;
74
+ background: #10b981;
75
+ border-radius: 50%;
76
+ animation: pulse 2s infinite;
77
+ }
78
+
79
+ .status-badge.disconnected {
80
+ background: #fef2f2;
81
+ color: #dc2626;
82
+ }
83
+
84
+ .status-badge.disconnected .dot {
85
+ background: #ef4444;
86
+ animation: none;
87
+ }
88
+
89
+ @keyframes pulse {
90
+ 0%, 100% { opacity: 1; }
91
+ 50% { opacity: 0.5; }
92
+ }
93
+
94
+ .main-container {
95
+ flex: 1;
96
+ display: flex;
97
+ flex-direction: column;
98
+ padding: 16px 24px;
99
+ gap: 16px;
100
+ min-height: 0;
101
+ }
102
+
103
+ .stats-row {
104
+ display: grid;
105
+ grid-template-columns: repeat(4, 1fr);
106
+ gap: 12px;
107
+ flex-shrink: 0;
108
+ }
109
+
110
+ .stat-card {
111
+ background: white;
112
+ border-radius: 10px;
113
+ padding: 14px 16px;
114
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
115
+ display: flex;
116
+ align-items: center;
117
+ gap: 12px;
118
+ }
119
+
120
+ .stat-card .icon {
121
+ font-size: 20px;
122
+ width: 40px;
123
+ height: 40px;
124
+ background: #f3f4f6;
125
+ border-radius: 8px;
126
+ display: flex;
127
+ align-items: center;
128
+ justify-content: center;
129
+ }
130
+
131
+ .stat-card .info .value {
132
+ font-size: 22px;
133
+ font-weight: 700;
134
+ color: #1f2937;
135
+ }
136
+
137
+ .stat-card .info .label {
138
+ font-size: 12px;
139
+ color: #6b7280;
140
+ }
141
+
142
+ .stat-card.highlight {
143
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
144
+ }
145
+
146
+ .stat-card.highlight .icon {
147
+ background: rgba(255, 255, 255, 0.2);
148
+ }
149
+
150
+ .stat-card.highlight .info .value,
151
+ .stat-card.highlight .info .label {
152
+ color: white;
153
+ }
154
+
155
+ .content-grid {
156
+ flex: 1;
157
+ display: grid;
158
+ grid-template-columns: 1fr 380px;
159
+ gap: 16px;
160
+ min-height: 0;
161
+ }
162
+
163
+ .left-panel {
164
+ display: flex;
165
+ flex-direction: column;
166
+ gap: 16px;
167
+ min-height: 0;
168
+ }
169
+
170
+ .left-panel .card {
171
+ flex: 1;
172
+ min-height: 0;
173
+ }
174
+
175
+ .card {
176
+ background: white;
177
+ border-radius: 10px;
178
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
179
+ display: flex;
180
+ flex-direction: column;
181
+ min-height: 0;
182
+ }
183
+
184
+ .card-header {
185
+ padding: 14px 16px;
186
+ border-bottom: 1px solid #e5e7eb;
187
+ display: flex;
188
+ align-items: center;
189
+ justify-content: space-between;
190
+ flex-shrink: 0;
191
+ }
192
+
193
+ .card-header h3 {
194
+ font-size: 14px;
195
+ font-weight: 600;
196
+ color: #1f2937;
197
+ display: flex;
198
+ align-items: center;
199
+ gap: 8px;
200
+ }
201
+
202
+ .card-header .badge {
203
+ font-size: 11px;
204
+ padding: 3px 8px;
205
+ background: #dbeafe;
206
+ color: #2563eb;
207
+ border-radius: 10px;
208
+ font-weight: 500;
209
+ }
210
+
211
+ .card-body {
212
+ padding: 12px 16px;
213
+ flex: 1;
214
+ overflow-y: auto;
215
+ min-height: 0;
216
+ }
217
+
218
+ .connection-list {
219
+ display: flex;
220
+ flex-direction: column;
221
+ gap: 8px;
222
+ }
223
+
224
+ .connection-item {
225
+ display: flex;
226
+ align-items: center;
227
+ padding: 10px 12px;
228
+ background: #f9fafb;
229
+ border-radius: 8px;
230
+ transition: all 0.2s;
231
+ border: 2px solid transparent;
232
+ }
233
+
234
+ .connection-item:hover {
235
+ background: #f3f4f6;
236
+ }
237
+
238
+ .connection-item.active {
239
+ border-color: #10b981;
240
+ background: rgba(16, 185, 129, 0.05);
241
+ }
242
+
243
+ .connection-item .status-indicator {
244
+ width: 8px;
245
+ height: 8px;
246
+ border-radius: 50%;
247
+ margin-right: 10px;
248
+ flex-shrink: 0;
249
+ }
250
+
251
+ .connection-item .status-indicator.online {
252
+ background: #10b981;
253
+ box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.2);
254
+ }
255
+
256
+ .connection-item .status-indicator.busy {
257
+ background: #f59e0b;
258
+ box-shadow: 0 0 0 2px rgba(245, 158, 11, 0.2);
259
+ animation: blink 0.5s infinite;
260
+ }
261
+
262
+ .connection-item .status-indicator.offline {
263
+ background: #9ca3af;
264
+ }
265
+
266
+ @keyframes blink {
267
+ 0%, 100% { opacity: 1; }
268
+ 50% { opacity: 0.5; }
269
+ }
270
+
271
+ .connection-item .connection-info {
272
+ flex: 1;
273
+ min-width: 0;
274
+ }
275
+
276
+ .connection-item .connection-header {
277
+ display: flex;
278
+ align-items: center;
279
+ gap: 8px;
280
+ margin-bottom: 3px;
281
+ }
282
+
283
+ .connection-item .connection-name {
284
+ font-weight: 600;
285
+ color: #1f2937;
286
+ font-size: 13px;
287
+ }
288
+
289
+ .connection-item .connection-tag {
290
+ font-size: 10px;
291
+ padding: 2px 6px;
292
+ border-radius: 3px;
293
+ font-weight: 500;
294
+ }
295
+
296
+ .connection-item .connection-tag.playwright {
297
+ background: #dbeafe;
298
+ color: #2563eb;
299
+ }
300
+
301
+ .connection-item .connection-tag.puppeteer {
302
+ background: #fef3c7;
303
+ color: #d97706;
304
+ }
305
+
306
+ .connection-item .connection-tag.cdp {
307
+ background: #e0e7ff;
308
+ color: #4f46e5;
309
+ }
310
+
311
+ .connection-item .connection-details {
312
+ display: flex;
313
+ align-items: center;
314
+ gap: 12px;
315
+ font-size: 11px;
316
+ color: #6b7280;
317
+ }
318
+
319
+ .connection-item .page-info {
320
+ display: flex;
321
+ align-items: center;
322
+ gap: 4px;
323
+ max-width: 200px;
324
+ overflow: hidden;
325
+ }
326
+
327
+ .connection-item .page-title {
328
+ color: #1f2937;
329
+ white-space: nowrap;
330
+ overflow: hidden;
331
+ text-overflow: ellipsis;
332
+ }
333
+
334
+ .connection-item .connection-meta {
335
+ text-align: right;
336
+ flex-shrink: 0;
337
+ font-size: 11px;
338
+ color: #9ca3af;
339
+ }
340
+
341
+ .connection-item .connection-actions {
342
+ display: flex;
343
+ gap: 4px;
344
+ margin-left: 8px;
345
+ opacity: 0;
346
+ transition: opacity 0.2s;
347
+ }
348
+
349
+ .connection-item:hover .connection-actions {
350
+ opacity: 1;
351
+ }
352
+
353
+ .connection-item .action-btn {
354
+ width: 26px;
355
+ height: 26px;
356
+ border: none;
357
+ background: white;
358
+ border-radius: 4px;
359
+ cursor: pointer;
360
+ display: flex;
361
+ align-items: center;
362
+ justify-content: center;
363
+ color: #6b7280;
364
+ transition: all 0.2s;
365
+ font-size: 12px;
366
+ }
367
+
368
+ .connection-item .action-btn:hover {
369
+ background: #e5e7eb;
370
+ color: #1f2937;
371
+ }
372
+
373
+ .connection-item .action-btn.danger:hover {
374
+ background: #fef2f2;
375
+ color: #dc2626;
376
+ }
377
+
378
+ .sidebar {
379
+ display: flex;
380
+ flex-direction: column;
381
+ gap: 16px;
382
+ min-height: 0;
383
+ }
384
+
385
+ .form-group {
386
+ margin-bottom: 14px;
387
+ }
388
+
389
+ .form-group:last-child {
390
+ margin-bottom: 0;
391
+ }
392
+
393
+ .form-group label {
394
+ display: block;
395
+ font-size: 12px;
396
+ font-weight: 500;
397
+ color: #374151;
398
+ margin-bottom: 6px;
399
+ }
400
+
401
+ .form-group input {
402
+ width: 100%;
403
+ padding: 10px 12px;
404
+ border: 1px solid #d1d5db;
405
+ border-radius: 6px;
406
+ font-size: 13px;
407
+ transition: all 0.2s;
408
+ outline: none;
409
+ }
410
+
411
+ .form-group input:focus {
412
+ border-color: #667eea;
413
+ box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.1);
414
+ }
415
+
416
+ .form-group .description {
417
+ font-size: 11px;
418
+ color: #6b7280;
419
+ margin-top: 6px;
420
+ line-height: 1.5;
421
+ }
422
+
423
+ .btn {
424
+ padding: 10px 16px;
425
+ border: none;
426
+ border-radius: 6px;
427
+ font-size: 13px;
428
+ font-weight: 500;
429
+ cursor: pointer;
430
+ transition: all 0.2s;
431
+ display: inline-flex;
432
+ align-items: center;
433
+ justify-content: center;
434
+ gap: 6px;
435
+ }
436
+
437
+ .btn-primary {
438
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
439
+ color: white;
440
+ }
441
+
442
+ .btn-primary:hover {
443
+ transform: translateY(-1px);
444
+ box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3);
445
+ }
446
+
447
+ .btn-secondary {
448
+ background: #f3f4f6;
449
+ color: #374151;
450
+ }
451
+
452
+ .btn-block {
453
+ width: 100%;
454
+ }
455
+
456
+ .btn-sm {
457
+ padding: 6px 10px;
458
+ font-size: 12px;
459
+ }
460
+
461
+ .divider {
462
+ height: 1px;
463
+ background: #e5e7eb;
464
+ margin: 16px 0;
465
+ }
466
+
467
+ .usage-guide {
468
+ background: #f9fafb;
469
+ border-radius: 8px;
470
+ padding: 14px;
471
+ }
472
+
473
+ .usage-guide h4 {
474
+ font-size: 12px;
475
+ font-weight: 600;
476
+ color: #1f2937;
477
+ margin-bottom: 10px;
478
+ }
479
+
480
+ .usage-guide p {
481
+ font-size: 11px;
482
+ color: #6b7280;
483
+ line-height: 1.6;
484
+ margin-bottom: 8px;
485
+ }
486
+
487
+ .usage-guide code {
488
+ background: #1f2937;
489
+ color: #86efac;
490
+ padding: 2px 6px;
491
+ border-radius: 3px;
492
+ font-size: 11px;
493
+ font-family: 'Monaco', 'Menlo', monospace;
494
+ }
495
+
496
+ .usage-guide .example {
497
+ background: #1f2937;
498
+ border-radius: 6px;
499
+ padding: 12px;
500
+ margin-top: 10px;
501
+ }
502
+
503
+ .usage-guide .example pre {
504
+ margin: 0;
505
+ font-family: 'Monaco', 'Menlo', monospace;
506
+ font-size: 11px;
507
+ color: #e5e7eb;
508
+ line-height: 1.6;
509
+ }
510
+
511
+ .activity-log {
512
+ display: flex;
513
+ flex-direction: column;
514
+ gap: 6px;
515
+ }
516
+
517
+ .log-item {
518
+ display: flex;
519
+ align-items: flex-start;
520
+ gap: 8px;
521
+ padding: 8px 0;
522
+ border-bottom: 1px solid #f3f4f6;
523
+ }
524
+
525
+ .log-item:last-child {
526
+ border-bottom: none;
527
+ }
528
+
529
+ .log-item .log-icon {
530
+ width: 20px;
531
+ height: 20px;
532
+ border-radius: 50%;
533
+ display: flex;
534
+ align-items: center;
535
+ justify-content: center;
536
+ font-size: 10px;
537
+ flex-shrink: 0;
538
+ }
539
+
540
+ .log-item .log-icon.connect { background: #dcfce7; color: #16a34a; }
541
+ .log-item .log-icon.disconnect { background: #fef2f2; color: #dc2626; }
542
+ .log-item .log-icon.action { background: #dbeafe; color: #2563eb; }
543
+ .log-item .log-icon.page { background: #fef3c7; color: #d97706; }
544
+
545
+ .log-item .log-content {
546
+ flex: 1;
547
+ min-width: 0;
548
+ }
549
+
550
+ .log-item .log-message {
551
+ font-size: 12px;
552
+ color: #1f2937;
553
+ margin-bottom: 1px;
554
+ }
555
+
556
+ .log-item .log-message code {
557
+ background: #f3f4f6;
558
+ padding: 1px 4px;
559
+ border-radius: 3px;
560
+ font-size: 11px;
561
+ }
562
+
563
+ .log-item .log-time {
564
+ font-size: 10px;
565
+ color: #9ca3af;
566
+ }
567
+
568
+ .empty-state {
569
+ text-align: center;
570
+ padding: 40px 20px;
571
+ color: #6b7280;
572
+ }
573
+
574
+ .empty-state .icon {
575
+ font-size: 48px;
576
+ margin-bottom: 12px;
577
+ }
578
+
579
+ .empty-state .title {
580
+ font-size: 14px;
581
+ font-weight: 500;
582
+ color: #374151;
583
+ margin-bottom: 4px;
584
+ }
585
+
586
+ .empty-state .desc {
587
+ font-size: 12px;
588
+ }
589
+
590
+ .toast {
591
+ position: fixed;
592
+ bottom: 16px;
593
+ right: 16px;
594
+ background: #1f2937;
595
+ color: white;
596
+ padding: 10px 16px;
597
+ border-radius: 6px;
598
+ font-size: 13px;
599
+ display: flex;
600
+ align-items: center;
601
+ gap: 8px;
602
+ transform: translateY(100px);
603
+ opacity: 0;
604
+ transition: all 0.3s;
605
+ z-index: 1001;
606
+ }
607
+
608
+ .toast.show {
609
+ transform: translateY(0);
610
+ opacity: 1;
611
+ }
612
+
613
+ .toast.success { background: #059669; }
614
+ .toast.error { background: #dc2626; }
615
+
616
+ @media (max-width: 1200px) {
617
+ .content-grid {
618
+ grid-template-columns: 1fr;
619
+ }
620
+
621
+ .stats-row {
622
+ grid-template-columns: repeat(2, 1fr);
623
+ }
624
+ }
625
+ </style>
626
+ </head>
627
+ <body>
628
+ <nav class="navbar">
629
+ <div class="navbar-brand">
630
+ <div class="logo">🔌</div>
631
+ <h1>CDP Bridge</h1>
632
+ </div>
633
+ <div class="status-badge disconnected" id="statusBadge">
634
+ <div class="dot"></div>
635
+ <span id="statusText">未连接</span>
636
+ </div>
637
+ </nav>
638
+
639
+ <div class="main-container">
640
+ <div class="stats-row">
641
+ <div class="stat-card highlight">
642
+ <div class="icon">🔗</div>
643
+ <div class="info">
644
+ <div class="value" id="activeConnections">0</div>
645
+ <div class="label">活跃连接</div>
646
+ </div>
647
+ </div>
648
+ <div class="stat-card">
649
+ <div class="icon">📄</div>
650
+ <div class="info">
651
+ <div class="value" id="controlledPages">0</div>
652
+ <div class="label">受控页面</div>
653
+ </div>
654
+ </div>
655
+ <div class="stat-card">
656
+ <div class="icon">⚡</div>
657
+ <div class="info">
658
+ <div class="value" id="cdpCommands">0</div>
659
+ <div class="label">CDP 命令/分</div>
660
+ </div>
661
+ </div>
662
+ <div class="stat-card">
663
+ <div class="icon">⏱️</div>
664
+ <div class="info">
665
+ <div class="value" id="uptime">0s</div>
666
+ <div class="label">运行时长</div>
667
+ </div>
668
+ </div>
669
+ </div>
670
+
671
+ <div class="content-grid">
672
+ <div class="left-panel">
673
+ <div class="card">
674
+ <div class="card-header">
675
+ <h3>
676
+ CDP 客户端
677
+ <span class="badge" id="clientCount">0 个</span>
678
+ </h3>
679
+ </div>
680
+ <div class="card-body">
681
+ <div class="connection-list" id="clientList">
682
+ <div class="empty-state">
683
+ <div class="icon">🔌</div>
684
+ <div class="title">暂无客户端</div>
685
+ <div class="desc">等待 Playwright/Puppeteer 连接...</div>
686
+ </div>
687
+ </div>
688
+ </div>
689
+ </div>
690
+
691
+ <div class="card">
692
+ <div class="card-header">
693
+ <h3>
694
+ 受控页面
695
+ <span class="badge" id="pageCount">0 个</span>
696
+ </h3>
697
+ <button class="btn btn-secondary btn-sm" id="refreshBtn">🔄 刷新</button>
698
+ </div>
699
+ <div class="card-body">
700
+ <div class="connection-list" id="pageList">
701
+ <div class="empty-state">
702
+ <div class="icon">📄</div>
703
+ <div class="title">暂无页面</div>
704
+ <div class="desc">客户端创建页面后会显示在这里</div>
705
+ </div>
706
+ </div>
707
+ </div>
708
+ </div>
709
+ </div>
710
+
711
+ <div class="sidebar">
712
+ <div class="card">
713
+ <div class="card-header">
714
+ <h3>插件配置</h3>
715
+ </div>
716
+ <div class="card-body">
717
+ <div class="form-group">
718
+ <label>服务器地址</label>
719
+ <input type="text" id="serverAddress" value="ws://localhost:9221/plugin" placeholder="ws://your-server:9221/plugin">
720
+ <div class="description">WebSocket 服务器地址,插件将连接到此服务器</div>
721
+ </div>
722
+ <button class="btn btn-primary btn-block" id="connectBtn">🔗 保存并连接</button>
723
+
724
+ <div class="divider"></div>
725
+
726
+ <div class="usage-guide">
727
+ <h4>📖 客户端连接方式</h4>
728
+ <p>插件连接后,客户端通过 CDP 端点连接:</p>
729
+ <div class="example">
730
+ <pre><span class="comment">// Playwright</span>
731
+ <span class="keyword">const</span> browser = <span class="keyword">await</span> chromium.connectOverCDP(
732
+ <span class="string">'http://localhost:9221'</span>
733
+ );
734
+
735
+ <span class="comment">// Puppeteer</span>
736
+ <span class="keyword">const</span> browser = <span class="keyword">await</span> puppeteer.connect({
737
+ browserWSEndpoint: <span class="string">'ws://localhost:9221'</span>
738
+ });</pre>
739
+ </div>
740
+ </div>
741
+ </div>
742
+ </div>
743
+
744
+ <div class="card" style="flex: 1; min-height: 0;">
745
+ <div class="card-header">
746
+ <h3>活动日志</h3>
747
+ </div>
748
+ <div class="card-body">
749
+ <div class="activity-log" id="activityLog">
750
+ <div class="empty-state">
751
+ <div class="icon">📋</div>
752
+ <div class="title">暂无日志</div>
753
+ <div class="desc">活动将显示在这里</div>
754
+ </div>
755
+ </div>
756
+ </div>
757
+ </div>
758
+ </div>
759
+ </div>
760
+ </div>
761
+
762
+ <div class="toast" id="toast">
763
+ <span class="icon">✓</span>
764
+ <span class="message">操作成功</span>
765
+ </div>
766
+
767
+ <script src="config-page.js"></script>
768
+ </body>
769
+ </html>