dsh-plugin-admin 0.2.1 → 0.3.1
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/README.md +6 -3
- package/cordis.patch.yml +7 -7
- package/lib/client.js +2211 -1898
- package/lib/index.js +2144 -2128
- package/package.json +2 -2
package/lib/client.js
CHANGED
|
@@ -1,1898 +1,2211 @@
|
|
|
1
|
-
window.__ModuleLoader__.load({ id: 'dsh-plugin-admin', factory: (require) => {
|
|
2
|
-
var module = { exports: {} }; var exports = module.exports;
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* dsh-plugin-admin browser half: unified "管理中心" (插件管理 & 会话管理)
|
|
6
|
-
* page inside the settings panel.
|
|
7
|
-
*
|
|
8
|
-
* The page rides the shell's `settings.section` slot (the shell owns
|
|
9
|
-
* the dialog chrome, navigation, and close); its component is assembled with
|
|
10
|
-
* the platform-shared React (module-table seed word, same instance the shell
|
|
11
|
-
* renders with), providing a tabbed modern UI to manage both profile plugin
|
|
12
|
-
* layers and session lifecycles over the /api RPC gateway.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
var React = require('react')
|
|
16
|
-
var createElement = React.createElement
|
|
17
|
-
var useState = React.useState
|
|
18
|
-
var useRef = React.useRef
|
|
19
|
-
var useEffect = React.useEffect
|
|
20
|
-
|
|
21
|
-
var CSS_TEXT = [
|
|
22
|
-
'@keyframes dsh-admin-spin { to { transform: rotate(360deg); } }',
|
|
23
|
-
'@keyframes dsh-admin-pulse { 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: 0.35; transform: scale(0.85); } }',
|
|
24
|
-
'[data-dsh-admin-section] { display: flex; flex-direction: column; width: 100%; gap: 14px; padding: 2px; font-size: 13px; line-height: 1.5; color: var(--dsw-alias-label-primary, #
|
|
25
|
-
'[data-dsh-admin-section] * { box-sizing: border-box; }',
|
|
26
|
-
'[data-dsh-admin-section] .tabs { display: flex; padding: 3px; background: var(--dsw-alias-interactive-bg-hover, rgba(
|
|
27
|
-
'[data-dsh-admin-section] .tab { flex: 1; border: 0; background: transparent; border-radius: 7px; padding: 7px 14px; cursor: pointer; display: flex; align-items: center; justify-content: center; gap: 7px; font-size: 12px; font-weight: 500; color: var(--dsw-alias-label-secondary, #666); transition: all 0.15s ease; font-family: inherit; }',
|
|
28
|
-
'[data-dsh-admin-section] .tab:hover:not(.active) { color: var(--dsw-alias-label-primary, #
|
|
29
|
-
'[data-dsh-admin-section] .tab.active { background: var(--dsw-alias-bg-base,
|
|
30
|
-
'[data-dsh-admin-section] .tab-count { font-size: 10px; font-weight: 600; padding: 1px 6px; border-radius: 10px; background: var(--dsw-alias-interactive-bg-hover,
|
|
31
|
-
'[data-dsh-admin-section] .tab.active .tab-count { background: var(--dsw-
|
|
32
|
-
'[data-dsh-admin-section] .toolbar { display: flex; gap: 8px; align-items: center; padding: 10px; border: 1px solid var(--dsw-alias-border-subtle, rgba(
|
|
33
|
-
'[data-dsh-admin-section] .
|
|
34
|
-
'[data-dsh-admin-section] .search-wrap
|
|
35
|
-
'[data-dsh-admin-section] .
|
|
36
|
-
'[data-dsh-admin-section] .
|
|
37
|
-
'[data-dsh-admin-section] .search
|
|
38
|
-
'[data-dsh-admin-section] .
|
|
39
|
-
'[data-dsh-admin-section] .input
|
|
40
|
-
'[data-dsh-admin-section] .
|
|
41
|
-
'[data-dsh-admin-section] .
|
|
42
|
-
'[data-dsh-admin-section] .
|
|
43
|
-
'[data-dsh-admin-section] .
|
|
44
|
-
'[data-dsh-admin-section] .
|
|
45
|
-
'[data-dsh-admin-section] .
|
|
46
|
-
'[data-dsh-admin-section] .
|
|
47
|
-
'[data-dsh-admin-section] .
|
|
48
|
-
'[data-dsh-admin-section] .btn:
|
|
49
|
-
'[data-dsh-admin-section] .btn:disabled {
|
|
50
|
-
'[data-dsh-admin-section] .btn.
|
|
51
|
-
'[data-dsh-admin-section] .btn.
|
|
52
|
-
'[data-dsh-admin-section] .btn
|
|
53
|
-
'[data-dsh-admin-section] .btn.
|
|
54
|
-
'[data-dsh-admin-section] .btn
|
|
55
|
-
'[data-dsh-admin-section] .btn.
|
|
56
|
-
'[data-dsh-admin-section] .btn.
|
|
57
|
-
'[data-dsh-admin-section] .
|
|
58
|
-
'[data-dsh-admin-section] .
|
|
59
|
-
'[data-dsh-admin-section] .
|
|
60
|
-
'[data-dsh-admin-section] .
|
|
61
|
-
'[data-dsh-admin-section] .
|
|
62
|
-
'[data-dsh-admin-section] .
|
|
63
|
-
'[data-dsh-admin-section] .
|
|
64
|
-
'[data-dsh-admin-section] .
|
|
65
|
-
'[data-dsh-admin-section] .
|
|
66
|
-
'[data-dsh-admin-section] .
|
|
67
|
-
'[data-dsh-admin-section] .
|
|
68
|
-
'[data-dsh-admin-section] .
|
|
69
|
-
'[data-dsh-admin-section] .
|
|
70
|
-
'[data-dsh-admin-section] .
|
|
71
|
-
'[data-dsh-admin-section] .
|
|
72
|
-
'[data-dsh-admin-section] .
|
|
73
|
-
'[data-dsh-admin-section] .
|
|
74
|
-
'[data-dsh-admin-section] .
|
|
75
|
-
'[data-dsh-admin-section] .
|
|
76
|
-
'[data-dsh-admin-section] .card
|
|
77
|
-
'[data-dsh-admin-section] .card
|
|
78
|
-
'[data-dsh-admin-section] .
|
|
79
|
-
'[data-dsh-admin-section] .
|
|
80
|
-
'[data-dsh-admin-section] .
|
|
81
|
-
'[data-dsh-admin-section] .
|
|
82
|
-
'[data-dsh-admin-section] .
|
|
83
|
-
'[data-dsh-admin-section] .
|
|
84
|
-
'[data-dsh-admin-section] .
|
|
85
|
-
'[data-dsh-admin-section] .
|
|
86
|
-
'[data-dsh-admin-section] .
|
|
87
|
-
'[data-dsh-admin-section] .tag
|
|
88
|
-
'[data-dsh-admin-section] .
|
|
89
|
-
'[data-dsh-admin-section] .
|
|
90
|
-
'[data-dsh-admin-section] .
|
|
91
|
-
'[data-dsh-admin-section] .
|
|
92
|
-
'[data-dsh-admin-section] .
|
|
93
|
-
'[data-dsh-admin-section] .
|
|
94
|
-
'[data-dsh-admin-section] .
|
|
95
|
-
'[data-dsh-admin-section] .
|
|
96
|
-
'[data-dsh-admin-section] .
|
|
97
|
-
'[data-dsh-admin-section] .
|
|
98
|
-
'[data-dsh-admin-section] .
|
|
99
|
-
'[data-dsh-admin-section] .
|
|
100
|
-
'[data-dsh-admin-section] .
|
|
101
|
-
'[data-dsh-admin-section] .
|
|
102
|
-
'[data-dsh-admin-section] .
|
|
103
|
-
'[data-dsh-admin-section] .
|
|
104
|
-
'[data-dsh-admin-section] .
|
|
105
|
-
'[data-dsh-admin-section] .
|
|
106
|
-
'[data-dsh-admin-section] .
|
|
107
|
-
'[data-dsh-admin-section] .
|
|
108
|
-
'
|
|
109
|
-
'
|
|
110
|
-
'
|
|
111
|
-
'
|
|
112
|
-
'
|
|
113
|
-
'
|
|
114
|
-
'
|
|
115
|
-
'
|
|
116
|
-
].
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
if (
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
function
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
var
|
|
251
|
-
if (
|
|
252
|
-
var
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
if (
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
// session
|
|
272
|
-
if (
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
var
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
if (
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
}
|
|
355
|
-
cb(
|
|
356
|
-
})
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
patchPlugin({ error: '
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
busy:
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
})
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
}
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
var
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
}
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
function
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
if (
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
),
|
|
1281
|
-
)
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
}),
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
),
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
}
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
//
|
|
1392
|
-
var
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
}
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
createElement('
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
var
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
createElement('
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
}
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
}
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
function
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
className: '
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
var
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
}
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
)
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
headerChildren
|
|
1792
|
-
}
|
|
1793
|
-
|
|
1794
|
-
}
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1
|
+
window.__ModuleLoader__.load({ id: 'dsh-plugin-admin', factory: (require) => {
|
|
2
|
+
var module = { exports: {} }; var exports = module.exports;
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* dsh-plugin-admin browser half: unified "管理中心" (插件管理 & 会话管理)
|
|
6
|
+
* page inside the settings panel.
|
|
7
|
+
*
|
|
8
|
+
* The page rides the shell's `settings.section` slot (the shell owns
|
|
9
|
+
* the dialog chrome, navigation, and close); its component is assembled with
|
|
10
|
+
* the platform-shared React (module-table seed word, same instance the shell
|
|
11
|
+
* renders with), providing a tabbed modern UI to manage both profile plugin
|
|
12
|
+
* layers and session lifecycles over the /api RPC gateway.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
var React = require('react')
|
|
16
|
+
var createElement = React.createElement
|
|
17
|
+
var useState = React.useState
|
|
18
|
+
var useRef = React.useRef
|
|
19
|
+
var useEffect = React.useEffect
|
|
20
|
+
|
|
21
|
+
var CSS_TEXT = [
|
|
22
|
+
'@keyframes dsh-admin-spin { to { transform: rotate(360deg); } }',
|
|
23
|
+
'@keyframes dsh-admin-pulse { 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: 0.35; transform: scale(0.85); } }',
|
|
24
|
+
'[data-dsh-admin-section] { display: flex; flex-direction: column; width: 100%; gap: 14px; padding: 2px; font-size: 13px; line-height: 1.5; color: var(--dsw-alias-label-primary, #222); max-height: calc(100vh - 140px); overflow: hidden; }',
|
|
25
|
+
'[data-dsh-admin-section] * { box-sizing: border-box; }',
|
|
26
|
+
'[data-dsh-admin-section] .tabs { display: flex; padding: 3px; background: var(--dsw-alias-interactive-bg-hover, rgba(200,200,210,0.2)); border-radius: 9px; border: 1px solid var(--dsw-alias-border-subtle, rgba(200,200,210,0.3)); gap: 3px; }',
|
|
27
|
+
'[data-dsh-admin-section] .tab { flex: 1; border: 0; background: transparent; border-radius: 7px; padding: 7px 14px; cursor: pointer; display: flex; align-items: center; justify-content: center; gap: 7px; font-size: 12px; font-weight: 500; color: var(--dsw-alias-label-secondary, #666); transition: all 0.15s ease; font-family: inherit; }',
|
|
28
|
+
'[data-dsh-admin-section] .tab:hover:not(.active) { color: var(--dsw-alias-label-primary, #222); background: var(--dsw-alias-interactive-bg-hover, rgba(200,200,210,0.3)); }',
|
|
29
|
+
'[data-dsh-admin-section] .tab.active { background: var(--dsw-alias-bg-elevated, var(--dsw-alias-bg-base, transparent)); color: var(--dsw-alias-label-primary, #333); font-weight: 600; box-shadow: 0 1px 3px rgba(0,0,0,0.08), 0 1px 2px rgba(0,0,0,0.04); }',
|
|
30
|
+
'[data-dsh-admin-section] .tab-count { font-size: 10px; font-weight: 600; padding: 1px 6px; border-radius: 10px; background: var(--dsw-alias-interactive-bg-hover, rgba(200,200,210,0.4)); color: var(--dsw-alias-label-secondary, #555); }',
|
|
31
|
+
'[data-dsh-admin-section] .tab.active .tab-count { background: var(--dsw-static-blue-500, #3b82f6); color: #fff; }',
|
|
32
|
+
'[data-dsh-admin-section] .toolbar { display: flex; gap: 8px; align-items: center; padding: 10px; border: 1px solid var(--dsw-alias-border-subtle, rgba(200,200,210,0.4)); border-radius: 12px; background: var(--dsw-alias-bg-elevated, var(--dsw-alias-bg-base, transparent)); box-shadow: 0 1px 2px rgba(0,0,0,0.02); }',
|
|
33
|
+
'[data-dsh-admin-section] .toolbar .input { height: 32px; padding: 0 12px; border-radius: 8px; border-color: var(--dsw-alias-border-subtle, rgba(200,200,210,0.5)); }',
|
|
34
|
+
'[data-dsh-admin-section] .search-wrap { flex: 1.6; min-width: 0; position: relative; display: flex; align-items: center; height: 32px; }',
|
|
35
|
+
'[data-dsh-admin-section] .search-wrap .input { flex: 1; min-width: 0; padding-left: 30px; font-size: 13px; }',
|
|
36
|
+
'[data-dsh-admin-section] .session-search { flex: 1; height: 32px; }',
|
|
37
|
+
'[data-dsh-admin-section] .session-search .input { height: 32px; padding: 0 12px; font-size: 13px; border-radius: 8px; border-color: var(--dsw-alias-border-subtle, rgba(200,200,210,0.5)); }',
|
|
38
|
+
'[data-dsh-admin-section] .install-wrap { flex: 1; min-width: 0; display: flex; align-items: center; height: 32px; }',
|
|
39
|
+
'[data-dsh-admin-section] .install-wrap .input { flex: 1; min-width: 0; font-size: 13px; border-radius: 8px; border-color: var(--dsw-alias-border-subtle, rgba(200,200,210,0.5)); }',
|
|
40
|
+
'[data-dsh-admin-section] .search-icon { position: absolute; left: 10px; top: 50%; transform: translateY(-50%); font-size: 12px; opacity: 0.7; pointer-events: none; z-index: 1; }',
|
|
41
|
+
'[data-dsh-admin-section] .input { width: 100%; padding: 7px 12px; border-radius: 9px; border: 1px solid var(--dsw-alias-border-subtle, rgba(200,200,210,0.5)); background: var(--dsw-alias-bg-base, transparent); color: inherit; font: inherit; outline: none; transition: border-color 0.15s, box-shadow 0.15s, background 0.15s; }',
|
|
42
|
+
'[data-dsh-admin-section] .input:hover { border-color: var(--dsw-alias-label-tertiary, rgba(180,180,195,0.6)); }',
|
|
43
|
+
'[data-dsh-admin-section] .input:focus { border-color: var(--dsw-static-blue-500, #3b82f6); box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.16); }',
|
|
44
|
+
'[data-dsh-admin-section] .filter-bar { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; }',
|
|
45
|
+
'[data-dsh-admin-section] .pill { font-size: 11px; padding: 2px 8px; border-radius: 6px; border: 1px solid var(--dsw-alias-border-subtle, rgba(200,200,210,0.45)); background: transparent; cursor: pointer; color: var(--dsw-alias-label-secondary, #555); transition: all 0.12s; font-family: inherit; }',
|
|
46
|
+
'[data-dsh-admin-section] .pill:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(200,200,210,0.3)); }',
|
|
47
|
+
'[data-dsh-admin-section] .pill.active { background: var(--dsw-alias-interactive-bg-hover, rgba(200,200,210,0.4)); color: var(--dsw-static-blue-500, #3b82f6); font-weight: 600; border-color: currentColor; }',
|
|
48
|
+
'[data-dsh-admin-section] .btn { display: inline-flex; align-items: center; justify-content: center; gap: 5px; height: 32px; padding: 0 14px; border-radius: 8px; font-size: 13px; font-weight: 500; cursor: pointer; font-family: inherit; transition: transform 0.15s ease, background 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease, color 0.15s ease; border: 1px solid var(--dsw-alias-border-subtle, rgba(200,200,210,0.5)); background: var(--dsw-alias-interactive-bg-hover, rgba(240,240,245,0.6)); color: var(--dsw-alias-label-secondary, #555); white-space: nowrap; flex: none; box-shadow: 0 1px 2px rgba(0,0,0,0.04); }',
|
|
49
|
+
'[data-dsh-admin-section] .btn:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover, rgba(220,220,230,0.7)); border-color: var(--dsw-alias-label-tertiary, rgba(180,180,195,0.65)); color: var(--dsw-alias-label-primary, #222); box-shadow: 0 2px 4px rgba(0,0,0,0.06); }',
|
|
50
|
+
'[data-dsh-admin-section] .search-wrap .btn.sm { background: transparent; border: none; box-shadow: none; padding: 2px 6px; color: var(--dsw-alias-label-tertiary, #888); }',
|
|
51
|
+
'[data-dsh-admin-section] .search-wrap .btn.sm:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover, rgba(200,200,210,0.4)); color: var(--dsw-alias-label-primary, #222); }',
|
|
52
|
+
'[data-dsh-admin-section] .btn:active:not(:disabled) { transform: translateY(1px); }',
|
|
53
|
+
'[data-dsh-admin-section] .btn:focus-visible, [data-dsh-admin-section] .pill:focus-visible { outline: 2px solid var(--dsw-static-blue-500, #3b82f6); outline-offset: 2px; }',
|
|
54
|
+
'[data-dsh-admin-section] .btn:disabled { opacity: 0.45; cursor: not-allowed; }',
|
|
55
|
+
'[data-dsh-admin-section] .btn.primary { background: var(--dsw-static-blue-500, #3b82f6); color: #fff; border-color: transparent; font-weight: 600; box-shadow: 0 1px 3px rgba(0,0,0,0.12); }',
|
|
56
|
+
'[data-dsh-admin-section] .btn.primary:hover:not(:disabled) { background: var(--dsw-static-blue-400, #60a5fa); color: #fff; box-shadow: 0 2px 6px rgba(0,0,0,0.14); }',
|
|
57
|
+
'[data-dsh-admin-section] .btn.primary:active:not(:disabled) { background: var(--dsw-static-blue-600, #2563eb); color: #fff; }',
|
|
58
|
+
'[data-dsh-admin-section] .btn.danger { color: var(--dsw-alias-state-error-primary, #dc2626); border-color: var(--dsw-alias-border-subtle, rgba(200,200,210,0.4)); }',
|
|
59
|
+
'[data-dsh-admin-section] .btn.danger:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover-danger, rgba(239,68,68,0.1)); border-color: rgba(220,38,38,0.35); color: #b91c1c; }',
|
|
60
|
+
'[data-dsh-admin-section] .btn.danger-solid { background: var(--dsw-alias-state-error-primary, #dc2626); color: #fff; border-color: transparent; }',
|
|
61
|
+
'[data-dsh-admin-section] .btn.danger-solid:hover:not(:disabled) { background: #b91c1c; color: #fff; }',
|
|
62
|
+
'[data-dsh-admin-section] .btn.danger-solid:active:not(:disabled) { background: #991b1b; color: #fff; }',
|
|
63
|
+
'[data-dsh-admin-section] .btn.sm { height: 28px; padding: 0 10px; font-size: 12px; border-radius: 6px; }',
|
|
64
|
+
'[data-dsh-admin-section] .spinner { width: 12px; height: 12px; border: 2px solid transparent; border-top-color: currentColor; border-radius: 50%; animation: dsh-admin-spin 0.7s linear infinite; display: inline-block; flex: none; }',
|
|
65
|
+
'[data-dsh-admin-section] .list { display: flex; flex-direction: column; gap: 10px; flex: 1 1 auto; min-height: 0; max-height: 560px; overflow-y: auto; padding: 2px 4px 2px 2px; scrollbar-width: thin; scrollbar-color: var(--dsw-alias-border-subtle, rgba(200,200,210,0.4)) transparent; }',
|
|
66
|
+
'[data-dsh-admin-section] .list::-webkit-scrollbar { width: 6px; }',
|
|
67
|
+
'[data-dsh-admin-section] .list::-webkit-scrollbar-thumb { border-radius: 99px; background: var(--dsw-alias-border-subtle, rgba(200,200,210,0.4)); }',
|
|
68
|
+
'[data-dsh-admin-section] .session-group { display: flex; flex-direction: column; gap: 6px; }',
|
|
69
|
+
'[data-dsh-admin-section] .group-header { display: flex; align-items: center; gap: 10px; padding: 10px 12px 8px 12px; font-size: 13px; font-weight: 600; color: var(--dsw-alias-label-primary); border: 1px solid var(--dsw-alias-border-subtle, rgba(200,200,210,0.4)); border-radius: 10px; background: var(--dsw-alias-bg-elevated, var(--dsw-alias-bg-base, transparent)); box-shadow: 0 1px 2px rgba(0,0,0,0.02); margin-bottom: 4px; }',
|
|
70
|
+
'[data-dsh-admin-section] .group-header:first-child { margin-top: 2px; }',
|
|
71
|
+
'[data-dsh-admin-section] .group-title { display: inline-flex; align-items: center; gap: 6px; }',
|
|
72
|
+
'[data-dsh-admin-section] .group-count { font-size: 11px; padding: 2px 8px; border-radius: 10px; background: var(--dsw-alias-interactive-bg-hover, rgba(200,200,210,0.3)); color: var(--dsw-alias-label-secondary, #555); font-weight: 500; }',
|
|
73
|
+
'[data-dsh-admin-section] .group-header .btn.sm { margin-left: auto; }',
|
|
74
|
+
'[data-dsh-admin-section] .group-header .btn.sm + .btn.sm { margin-left: 0; }',
|
|
75
|
+
'[data-dsh-admin-section] .group-path { font-family: monospace; font-size: 10px; color: var(--dsw-alias-label-tertiary, #888); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; flex: 1; min-width: 0; text-align: right; }',
|
|
76
|
+
'[data-dsh-admin-section] .card { display: flex; flex-direction: column; gap: 7px; padding: 12px; border-radius: 12px; border: 1px solid var(--dsw-alias-border-subtle, rgba(200,200,210,0.4)); background: var(--dsw-alias-bg-elevated, var(--dsw-alias-bg-base, transparent)); box-shadow: 0 1px 2px rgba(0,0,0,0.02); transition: transform 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease; }',
|
|
77
|
+
'[data-dsh-admin-section] .card:hover { transform: translateY(-1px); border-color: var(--dsw-alias-label-tertiary, rgba(180,180,195,0.6)); box-shadow: 0 8px 20px rgba(0,0,0,0.06); }',
|
|
78
|
+
'[data-dsh-admin-section] .card-header { display: flex; align-items: center; gap: 8px; min-width: 0; }',
|
|
79
|
+
'[data-dsh-admin-section] .card-title { font-size: 13px; font-weight: 600; color: var(--dsw-alias-label-primary); display: flex; align-items: center; gap: 7px; min-width: 0; flex: 1; }',
|
|
80
|
+
'[data-dsh-admin-section] .card-title-text { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-weight: 600; }',
|
|
81
|
+
'[data-dsh-admin-section] .card-summary { display: flex; align-items: flex-start; gap: 6px; padding: 6px 9px; border-radius: 6px; background: var(--dsw-alias-interactive-bg-hover, rgba(200,200,210,0.25)); border-left: 2px solid var(--dsw-static-blue-500, #3b82f6); font-size: 12px; line-height: 1.45; color: var(--dsw-alias-label-secondary, #555); margin-top: 1px; margin-bottom: 1px; }',
|
|
82
|
+
'[data-dsh-admin-section] .summary-icon { font-size: 11px; line-height: 1.45; flex: none; opacity: 0.85; }',
|
|
83
|
+
'[data-dsh-admin-section] .summary-text { flex: 1; min-width: 0; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; text-overflow: ellipsis; word-break: break-word; }',
|
|
84
|
+
'[data-dsh-admin-section] .card-sub { font-size: 11px; color: var(--dsw-alias-label-secondary, #666); display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }',
|
|
85
|
+
'[data-dsh-admin-section] .card-sub-item { display: inline-flex; align-items: center; gap: 3px; max-width: 100%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }',
|
|
86
|
+
'[data-dsh-admin-section] .card-actions { display: flex; gap: 5px; align-items: center; flex: none; }',
|
|
87
|
+
'[data-dsh-admin-section] .tag { font-size: 10px; font-weight: 500; padding: 1px 6px; border-radius: 4px; flex: none; display: inline-flex; align-items: center; gap: 4px; background: var(--dsw-alias-interactive-bg-hover, rgba(200,200,210,0.25)); color: var(--dsw-alias-label-secondary, #555); }',
|
|
88
|
+
'[data-dsh-admin-section] .tag.plugin { background: rgba(16, 185, 129, 0.12); color: var(--dsw-alias-state-success-primary, #10b981); }',
|
|
89
|
+
'[data-dsh-admin-section] .tag.local { background: rgba(59, 130, 246, 0.12); color: var(--dsw-static-blue-500, #3b82f6); }',
|
|
90
|
+
'[data-dsh-admin-section] .tag.update { background: rgba(245, 158, 11, 0.14); color: #d97706; font-weight: 600; border: 1px solid rgba(217, 119, 6, 0.25); }',
|
|
91
|
+
'[data-dsh-admin-section] .tag.update-error { background: rgba(239, 68, 68, 0.1); color: var(--dsw-alias-state-error-primary, #ef4444); }',
|
|
92
|
+
'[data-dsh-admin-section] .plugin-path { font-family: monospace; font-size: 11px; line-height: 1.4; color: var(--dsw-alias-label-secondary, #555); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; padding: 4px 9px; border-radius: 6px; background: var(--dsw-alias-interactive-bg-hover, rgba(200,200,210,0.25)); }',
|
|
93
|
+
'[data-dsh-admin-section] .tag.version { font-family: monospace; }',
|
|
94
|
+
'[data-dsh-admin-section] .tag.live { background: rgba(16, 185, 129, 0.12); color: var(--dsw-alias-state-success-primary, #10b981); font-weight: 600; }',
|
|
95
|
+
'[data-dsh-admin-section] .tag.archived { background: rgba(245, 158, 11, 0.12); color: var(--dsw-alias-state-warn-label, #f59e0b); }',
|
|
96
|
+
'[data-dsh-admin-section] .tag.turns { background: rgba(59, 130, 246, 0.12); color: var(--dsw-static-blue-500, #3b82f6); font-weight: 500; }',
|
|
97
|
+
'[data-dsh-admin-section] .dot { width: 7px; height: 7px; border-radius: 50%; flex: none; background: var(--dsw-alias-label-tertiary, #999); }',
|
|
98
|
+
'[data-dsh-admin-section] .dot.live { background: var(--dsw-alias-state-success-primary, #10b981); box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.25); animation: dsh-admin-pulse 2s infinite ease-in-out; }',
|
|
99
|
+
'[data-dsh-admin-section] .dot.archived { background: var(--dsw-alias-state-warn-label, #f59e0b); }',
|
|
100
|
+
'[data-dsh-admin-section] .confirm-bar { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 6px 10px; border-radius: 6px; background: var(--dsw-alias-interactive-bg-hover-danger, rgba(239,68,68,0.08)); border: 1px solid rgba(239,68,68,0.2); }',
|
|
101
|
+
'[data-dsh-admin-section] .confirm-text { font-size: 11px; color: var(--dsw-alias-state-error-primary, #ef4444); font-weight: 500; }',
|
|
102
|
+
'[data-dsh-admin-section] .confirm-actions { display: flex; gap: 5px; flex: none; }',
|
|
103
|
+
'[data-dsh-admin-section] .busy-banner { display: flex; align-items: center; gap: 8px; padding: 7px 11px; border-radius: 7px; background: rgba(59, 130, 246, 0.08); color: var(--dsw-static-blue-500, #3b82f6); font-size: 11px; }',
|
|
104
|
+
'[data-dsh-admin-section] .update-strip { display: flex; align-items: center; gap: 8px; padding: 8px 11px; border-radius: 8px; font-size: 12px; font-weight: 500; line-height: 1.4; }',
|
|
105
|
+
'[data-dsh-admin-section] .update-strip .btn.sm { margin-left: auto; flex: none; }',
|
|
106
|
+
'[data-dsh-admin-section] .update-strip.checking { background: rgba(59, 130, 246, 0.08); color: var(--dsw-static-blue-500, #3b82f6); }',
|
|
107
|
+
'[data-dsh-admin-section] .update-strip.ok { background: rgba(16, 185, 129, 0.1); color: var(--dsw-alias-state-success-primary, #10b981); }',
|
|
108
|
+
'[data-dsh-admin-section] .update-strip.has-updates { background: rgba(245, 158, 11, 0.12); color: #d97706; border: 1px solid rgba(217, 119, 6, 0.25); }',
|
|
109
|
+
'[data-dsh-admin-section] .update-strip.has-errors { background: rgba(239, 68, 68, 0.08); color: var(--dsw-alias-state-error-primary, #ef4444); }',
|
|
110
|
+
'[data-dsh-admin-section] .empty { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 28px 12px; color: var(--dsw-alias-label-tertiary, #999); gap: 4px; font-size: 12px; text-align: center; }',
|
|
111
|
+
'[data-dsh-admin-section] .error { padding: 7px 11px; border-radius: 7px; color: var(--dsw-alias-state-error-primary, #ef4444); background: var(--dsw-alias-interactive-bg-hover-danger, rgba(239,68,68,0.08)); font-size: 12px; white-space: pre-wrap; max-height: 100px; overflow-y: auto; }',
|
|
112
|
+
'[data-dsh-admin-section] .footer { display: flex; justify-content: space-between; align-items: center; padding-top: 4px; font-size: 11px; color: var(--dsw-alias-label-tertiary, #888); border-top: 1px solid var(--dsw-alias-border-subtle, rgba(200,200,210,0.3)); flex: none; }',
|
|
113
|
+
'[data-dsh-admin-section] .footer .path { max-width: 60%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }',
|
|
114
|
+
'[data-dsh-admin-section] .footer .hint { font-style: normal; }',
|
|
115
|
+
'[data-dsh-admin-section] .session-id-badge { font-family: monospace; font-size: 10px; opacity: 0.75; }',
|
|
116
|
+
'[data-dsh-admin-section] .mcp-test-row { padding: 6px 9px; border-radius: 6px; background: var(--dsw-alias-interactive-bg-hover, rgba(200,200,210,0.25)); font-size: 11px; line-height: 1.4; overflow-wrap: anywhere; }',
|
|
117
|
+
'[data-dsh-admin-section] .mcp-test { display: inline-flex; align-items: center; gap: 5px; }',
|
|
118
|
+
'[data-dsh-admin-section] .mcp-test.mcp-test-list { display: flex; flex-direction: column; align-items: flex-start; gap: 2px; }',
|
|
119
|
+
'[data-dsh-admin-section] .mcp-test-busy { color: var(--dsw-alias-label-secondary, #666); }',
|
|
120
|
+
'[data-dsh-admin-section] .mcp-test-ok { color: #16a34a; }',
|
|
121
|
+
'[data-dsh-admin-section] .mcp-test-fail { color: var(--dsw-alias-state-error-primary, #ef4444); }',
|
|
122
|
+
'[data-dsh-admin-section] .mcp-test-warn { color: #d97706; opacity: 0.95; }',
|
|
123
|
+
'[data-dsh-admin-section] .mcp-test-cached { font-size: 10px; color: var(--dsw-alias-label-tertiary, #999); }',
|
|
124
|
+
'@media (max-width: 520px) { [data-dsh-admin-section] { gap: 10px; } [data-dsh-admin-section] .toolbar { align-items: stretch; flex-wrap: wrap; padding: 8px; } [data-dsh-admin-section] .toolbar .input-wrap { flex-basis: 100%; } [data-dsh-admin-section] .card-header { align-items: flex-start; } [data-dsh-admin-section] .card-actions { flex-wrap: wrap; justify-content: flex-end; } [data-dsh-admin-section] .footer { gap: 6px; align-items: flex-start; flex-direction: column; } [data-dsh-admin-section] .footer .path { max-width: 100%; } }',
|
|
125
|
+
'@keyframes dsh-toast-in { 0% { opacity: 0; transform: translateY(-12px) scale(0.96); } 100% { opacity: 1; transform: translateY(0) scale(1); } }',
|
|
126
|
+
'@keyframes dsh-toast-out { 0% { opacity: 1; transform: translateY(0) scale(1); } 100% { opacity: 0; transform: translateY(-8px) scale(0.96); } }',
|
|
127
|
+
'.dsh-admin-toast { position: fixed; top: 16px; left: 50%; transform: translateX(-50%); z-index: 99999; padding: 10px 20px; border-radius: 10px; font-size: 13px; font-weight: 500; line-height: 1.4; box-shadow: 0 8px 30px rgba(0,0,0,0.15), 0 2px 8px rgba(0,0,0,0.08); display: flex; align-items: center; gap: 8px; pointer-events: none; animation: dsh-toast-in 0.25s ease-out forwards; max-width: 90vw; word-break: break-word; }',
|
|
128
|
+
'.dsh-admin-toast.success { background: rgba(16, 185, 129, 0.95); color: #fff; }',
|
|
129
|
+
'.dsh-admin-toast.error { background: rgba(239, 68, 68, 0.95); color: #fff; }',
|
|
130
|
+
'.dsh-admin-toast.info { background: rgba(22, 119, 255, 0.95); color: #fff; }',
|
|
131
|
+
'.dsh-admin-toast.leaving { animation: dsh-toast-out 0.2s ease-in forwards; }',
|
|
132
|
+
].join('\n')
|
|
133
|
+
|
|
134
|
+
function injectStyles() {
|
|
135
|
+
var tagId = 'dsh-plugin-admin/unified-section.css'
|
|
136
|
+
if (document.querySelector('style[data-plugin-css="' + tagId + '"]') === null) {
|
|
137
|
+
var tag = document.createElement('style')
|
|
138
|
+
tag.dataset.plugin = 'dsh-plugin-admin'
|
|
139
|
+
tag.dataset.pluginCss = tagId
|
|
140
|
+
tag.textContent = CSS_TEXT
|
|
141
|
+
document.head.appendChild(tag)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function baseName(path) {
|
|
146
|
+
if (path === null || path === '') return '(无工作目录)'
|
|
147
|
+
var parts = path.replace(/\\/g, '/').split('/')
|
|
148
|
+
var last = parts[parts.length - 1]
|
|
149
|
+
return last === '' ? parts[parts.length - 2] || path : last
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function formatDate(ms) {
|
|
153
|
+
if (typeof ms !== 'number' || !Number.isFinite(ms)) return ''
|
|
154
|
+
var d = new Date(ms)
|
|
155
|
+
var pad = function (n) { return (n < 10 ? '0' : '') + String(n) }
|
|
156
|
+
return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate())
|
|
157
|
+
+ ' ' + pad(d.getHours()) + ':' + pad(d.getMinutes())
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function messageOf(error) {
|
|
161
|
+
if (error !== null && typeof error === 'object' && typeof error.message === 'string') return error.message
|
|
162
|
+
if (typeof error === 'string') return error
|
|
163
|
+
return JSON.stringify(error)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Show a floating toast notification at the top of the viewport.
|
|
168
|
+
* Auto-dismisses after `duration` ms. Supports 'success', 'error', 'info'.
|
|
169
|
+
*/
|
|
170
|
+
function showToast(type, text, duration) {
|
|
171
|
+
if (typeof document === 'undefined') return
|
|
172
|
+
duration = duration || 3000
|
|
173
|
+
var toast = document.createElement('div')
|
|
174
|
+
toast.className = 'dsh-admin-toast ' + type
|
|
175
|
+
toast.textContent = text
|
|
176
|
+
document.body.appendChild(toast)
|
|
177
|
+
|
|
178
|
+
var timer = setTimeout(function () {
|
|
179
|
+
toast.classList.add('leaving')
|
|
180
|
+
setTimeout(function () {
|
|
181
|
+
if (toast.parentNode) toast.parentNode.removeChild(toast)
|
|
182
|
+
}, 200)
|
|
183
|
+
}, duration)
|
|
184
|
+
|
|
185
|
+
toast.addEventListener('click', function () {
|
|
186
|
+
clearTimeout(timer)
|
|
187
|
+
if (!toast.classList.contains('leaving')) {
|
|
188
|
+
toast.classList.add('leaving')
|
|
189
|
+
setTimeout(function () {
|
|
190
|
+
if (toast.parentNode) toast.parentNode.removeChild(toast)
|
|
191
|
+
}, 200)
|
|
192
|
+
}
|
|
193
|
+
})
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Copy text to the clipboard. Uses the async Clipboard API when available;
|
|
198
|
+
* falls back to a hidden textarea + execCommand for older browsers or
|
|
199
|
+
* non-secure contexts.
|
|
200
|
+
*/
|
|
201
|
+
function copyTextToClipboard(text) {
|
|
202
|
+
if (typeof navigator !== 'undefined' && navigator.clipboard && typeof navigator.clipboard.writeText === 'function') {
|
|
203
|
+
navigator.clipboard.writeText(text).then(function () {
|
|
204
|
+
showToast('success', '📋 会话 ID 已复制')
|
|
205
|
+
}, function () {
|
|
206
|
+
fallbackCopy(text)
|
|
207
|
+
})
|
|
208
|
+
return
|
|
209
|
+
}
|
|
210
|
+
fallbackCopy(text)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function fallbackCopy(text) {
|
|
214
|
+
try {
|
|
215
|
+
var ta = document.createElement('textarea')
|
|
216
|
+
ta.value = text
|
|
217
|
+
ta.style.cssText = 'position:fixed;top:-9999px;left:-9999px;opacity:0'
|
|
218
|
+
document.body.appendChild(ta)
|
|
219
|
+
ta.select()
|
|
220
|
+
ta.setSelectionRange(0, text.length)
|
|
221
|
+
document.execCommand('copy')
|
|
222
|
+
document.body.removeChild(ta)
|
|
223
|
+
} catch (e) {
|
|
224
|
+
showToast('error', '❌ 复制失败:' + messageOf(e))
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/* ========================================================================== */
|
|
229
|
+
/* Menu Popup Injection (Sessions & Workspaces) */
|
|
230
|
+
/* ========================================================================== */
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Inject plugin items into the existing three-dot menu popup. The workspace
|
|
234
|
+
* bundle renders a Menu component (@deepseek-ai/dsh-client-ui-primitives)
|
|
235
|
+
* with portal:true, so the popup lives as a div[role="menu"] inside
|
|
236
|
+
* document.body with position:fixed. A MutationObserver detects when this
|
|
237
|
+
* popup appears, identifies it as a session or workspace menu by checking
|
|
238
|
+
* the existing button labels, and injects additional items.
|
|
239
|
+
*/
|
|
240
|
+
function setupMenuInjection(call, refreshSessions) {
|
|
241
|
+
if (typeof document === 'undefined') return function () {}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Extract the session title from the row's aria-label. The anchor button
|
|
245
|
+
* aria-label is: 会话"<title>"的操作 (zh) / Session "<title>" actions (en).
|
|
246
|
+
* Extracting from the DOM keeps the injection synchronous — no RPC needed
|
|
247
|
+
* just to show the button, so the menu can't close before it appears.
|
|
248
|
+
*/
|
|
249
|
+
function sessionTitleFromRow(row) {
|
|
250
|
+
var btn = row.querySelector('button[aria-label*="会话" i][aria-label*="操作" i], button[aria-label*="session" i][aria-label*="actions" i]')
|
|
251
|
+
if (btn === null) return null
|
|
252
|
+
var label = btn.getAttribute('aria-label') || ''
|
|
253
|
+
// zh: 会话"xxx"的操作 → strip 会话"/"的操作
|
|
254
|
+
var zh = /会话["“]([^"”]+)["”]/.exec(label)
|
|
255
|
+
if (zh) return zh[1].trim()
|
|
256
|
+
// en: Session "xxx" actions
|
|
257
|
+
var en = /session\s*["“]([^"”]+)["”]/i.exec(label)
|
|
258
|
+
if (en) return en[1].trim()
|
|
259
|
+
// fallback: whole label minus known prefixes/suffixes
|
|
260
|
+
return label.replace(/^会话/, '').replace(/^session/i, '').replace(/["“”]|的操作|actions$/gi, '').trim() || null
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** Find the treeitem row for the anchor button nearest the menu. */
|
|
264
|
+
function rowForMenu(menuEl) {
|
|
265
|
+
var menuRect = menuEl ? menuEl.getBoundingClientRect() : null
|
|
266
|
+
var allRows = document.querySelectorAll('[role="treeitem"]')
|
|
267
|
+
if (allRows.length === 0) return null
|
|
268
|
+
var rows = []
|
|
269
|
+
for (var i = 0; i < allRows.length; i++) {
|
|
270
|
+
var r = allRows[i]
|
|
271
|
+
// Only rows that contain a session/workspace action button
|
|
272
|
+
if (r.querySelector('button[aria-label*="会话" i][aria-label*="操作" i], button[aria-label*="工作区" i][aria-label*="操作" i], button[aria-label*="session" i][aria-label*="actions" i], button[aria-label*="workspace" i][aria-label*="actions" i]')) {
|
|
273
|
+
rows.push(r)
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
if (rows.length === 0) return null
|
|
277
|
+
|
|
278
|
+
// The Menu component renders with side="bottom": menu top sits just
|
|
279
|
+
// below the anchor button (button.bottom + 4). Match the row whose
|
|
280
|
+
// center is closest to the MENU TOP, not the menu center — the menu
|
|
281
|
+
// can be tall (several items), so its center drifts far from the
|
|
282
|
+
// clicked row.
|
|
283
|
+
if (menuRect === null || menuRect.height === 0) {
|
|
284
|
+
// No reliable menu rect. With a single candidate row the choice is
|
|
285
|
+
// unambiguous, so return it (jsdom/test environments have zero rects);
|
|
286
|
+
// with several rows refuse to guess rather than act on the wrong
|
|
287
|
+
// session.
|
|
288
|
+
if (rows.length === 1) return rows[0]
|
|
289
|
+
return null
|
|
290
|
+
}
|
|
291
|
+
var closest = rows[0]
|
|
292
|
+
var closestDist = Infinity
|
|
293
|
+
for (var j = 0; j < rows.length; j++) {
|
|
294
|
+
var rect = rows[j].getBoundingClientRect()
|
|
295
|
+
if (rect.height === 0) continue
|
|
296
|
+
var rowCenter = rect.top + rect.height / 2
|
|
297
|
+
var dist = Math.abs(rowCenter - menuRect.top)
|
|
298
|
+
if (dist < closestDist) {
|
|
299
|
+
closestDist = dist
|
|
300
|
+
closest = rows[j]
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return closest
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function injectItems(menuEl) {
|
|
307
|
+
if (menuEl.querySelector('[data-dsh-admin-injected]')) return
|
|
308
|
+
|
|
309
|
+
var text = menuEl.textContent || ''
|
|
310
|
+
var isSession = text.indexOf('归档会话') !== -1 || text.indexOf('Archive session') !== -1
|
|
311
|
+
var isWorkspace = !isSession && (text.indexOf('删除') !== -1 || text.indexOf('Delete workspace') !== -1)
|
|
312
|
+
if (!isSession && !isWorkspace) return
|
|
313
|
+
|
|
314
|
+
var viewport = menuEl.querySelector('[role="presentation"]')
|
|
315
|
+
if (viewport === null) return
|
|
316
|
+
|
|
317
|
+
var row = rowForMenu(menuEl)
|
|
318
|
+
if (row === null) return
|
|
319
|
+
|
|
320
|
+
if (isSession) {
|
|
321
|
+
var title = sessionTitleFromRow(row)
|
|
322
|
+
if (title === null) return
|
|
323
|
+
// Resolve sessions by title lazily at click time (fresh RPC) so the
|
|
324
|
+
// match is always current. Copy-id keeps the fuzzy matcher (a wrong
|
|
325
|
+
// match only mis-copies an id); delete must never guess, so it
|
|
326
|
+
// resolves by exact title only and refuses duplicate titles — the
|
|
327
|
+
// panel's rows act on ids and can disambiguate.
|
|
328
|
+
function fetchSessions(done) {
|
|
329
|
+
call('sessionAdmin/list', {}).then(function (listResult) {
|
|
330
|
+
done((listResult && listResult.ok && listResult.value && listResult.value.sessions) || [])
|
|
331
|
+
}, function () { showToast('error', '❌ 无法加载会话列表') })
|
|
332
|
+
}
|
|
333
|
+
// Normalize a title for comparison: trim, collapse spaces, drop
|
|
334
|
+
// trailing ellipsis and truncation artifacts.
|
|
335
|
+
function norm(v) {
|
|
336
|
+
return (v || '').replace(/\s+/g, ' ').replace(/\.{3,}\s*$/, '').trim().toLowerCase()
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function resolveSessionFuzzy(cb) {
|
|
340
|
+
fetchSessions(function (sessions) {
|
|
341
|
+
var match = null
|
|
342
|
+
var nt = norm(title)
|
|
343
|
+
for (var i = 0; i < sessions.length; i++) {
|
|
344
|
+
var s = sessions[i]
|
|
345
|
+
var st = norm(s.title)
|
|
346
|
+
if (st === nt) { match = s; break }
|
|
347
|
+
// substring both ways (skip too-short needles)
|
|
348
|
+
if (nt.length > 3 && st.indexOf(nt) !== -1) { match = s; break }
|
|
349
|
+
if (st.length > 3 && nt.indexOf(st) !== -1) { match = s; break }
|
|
350
|
+
// fall back to cwd basename match
|
|
351
|
+
var cwdBase = s.cwd ? s.cwd.replace(/\\/g, '/').split('/').pop() : ''
|
|
352
|
+
if (cwdBase && (cwdBase === nt || nt.indexOf(cwdBase) !== -1 || cwdBase.indexOf(nt) !== -1)) { match = s; break }
|
|
353
|
+
}
|
|
354
|
+
if (match === null) { showToast('error', '❌ 未找到匹配的会话'); return }
|
|
355
|
+
cb(match)
|
|
356
|
+
})
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function resolveSessionExact(cb) {
|
|
360
|
+
fetchSessions(function (sessions) {
|
|
361
|
+
var nt = norm(title)
|
|
362
|
+
var exact = []
|
|
363
|
+
for (var i = 0; i < sessions.length; i++) {
|
|
364
|
+
if (norm(sessions[i].title) === nt) exact.push(sessions[i])
|
|
365
|
+
}
|
|
366
|
+
if (exact.length === 0) { showToast('error', '❌ 未找到匹配的会话'); return }
|
|
367
|
+
if (exact.length > 1) {
|
|
368
|
+
showToast('error', '❌ 存在 ' + String(exact.length) + ' 个同名会话,无法确定要删除的目标;请在 设置 → 管理中心 → 会话管理 中按会话 ID 删除')
|
|
369
|
+
return
|
|
370
|
+
}
|
|
371
|
+
cb(exact[0])
|
|
372
|
+
})
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Copy the session id to the clipboard.
|
|
376
|
+
appendMenuItem(viewport, '复制会话 ID', 'normal', function () {
|
|
377
|
+
resolveSessionFuzzy(function (session) {
|
|
378
|
+
copyTextToClipboard(session.id)
|
|
379
|
+
})
|
|
380
|
+
})
|
|
381
|
+
|
|
382
|
+
// Delete the session. Online sessions are now closable through the
|
|
383
|
+
// captured AgentHandle (closeSession disposes the live agent/session
|
|
384
|
+
// first, so removing the log cannot resurrect it) — this stops a
|
|
385
|
+
// running conversation in that session. The item arms on the first
|
|
386
|
+
// click and only fires on a second click within 4s (confirmText below)
|
|
387
|
+
// — deletion is physical, so one misclick must never be enough.
|
|
388
|
+
appendMenuItem(viewport, '删除会话', 'danger', function () {
|
|
389
|
+
resolveSessionExact(function (match) {
|
|
390
|
+
var method = match.live ? 'sessionAdmin/closeSession' : 'sessionAdmin/deleteSession'
|
|
391
|
+
call(method, { sessionId: match.id }).then(function (result) {
|
|
392
|
+
if (result && result.ok) {
|
|
393
|
+
showToast('success', '🗑️ 会话已删除')
|
|
394
|
+
// Nudge the sidebar so the removed session vanishes now
|
|
395
|
+
// instead of lingering in 未分组 until reload.
|
|
396
|
+
if (refreshSessions) refreshSessions()
|
|
397
|
+
} else {
|
|
398
|
+
showToast('error', '❌ 删除会话失败:' + messageOf(result && result.error))
|
|
399
|
+
}
|
|
400
|
+
}, function (err) { showToast('error', '❌ 删除会话失败:' + messageOf(err)) })
|
|
401
|
+
})
|
|
402
|
+
}, '⚠️ 再点一次确认删除(在线会话将先关停)')
|
|
403
|
+
} else {
|
|
404
|
+
// Workspace: reveal in file manager. Path is resolved lazily at click.
|
|
405
|
+
var wsTitle = (row.querySelector('[class*="_title"]') || {}).textContent || ''
|
|
406
|
+
wsTitle = wsTitle.trim()
|
|
407
|
+
if (wsTitle === '') return
|
|
408
|
+
appendMenuItem(viewport, '在资源管理器打开', 'normal', function () {
|
|
409
|
+
call('sessionAdmin/list', {}).then(function (listResult) {
|
|
410
|
+
var workspaces = (listResult && listResult.ok && listResult.value && listResult.value.workspaces) || []
|
|
411
|
+
var wsPath = null
|
|
412
|
+
for (var i = 0; i < workspaces.length; i++) {
|
|
413
|
+
var w = workspaces[i]
|
|
414
|
+
var wTitle = (w && w.title) || ''
|
|
415
|
+
if (wTitle === wsTitle || (wsTitle !== '' && wTitle !== '' && (wsTitle.indexOf(wTitle) !== -1 || wTitle.indexOf(wsTitle) !== -1))) { wsPath = w && w.path; break }
|
|
416
|
+
}
|
|
417
|
+
if (wsPath === null) { showToast('error', '❌ 打开失败:未找到工作区路径'); return }
|
|
418
|
+
call('fsAdmin/reveal', { path: wsPath }).then(function (result) {
|
|
419
|
+
// Success needs no toast — the Explorer window opening is the
|
|
420
|
+
// feedback itself. Only failures get a hint.
|
|
421
|
+
if (!(result && result.ok)) showToast('error', '❌ 打开失败:' + messageOf(result && result.error))
|
|
422
|
+
}, function (err) { showToast('error', '❌ 打开失败:' + messageOf(err)) })
|
|
423
|
+
}, function () { showToast('error', '❌ 打开失败:无法加载工作区列表') })
|
|
424
|
+
})
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function appendMenuItem(viewport, label, kind, onClick, confirmText) {
|
|
429
|
+
// Separator (only if there are already items — always, to visually group)
|
|
430
|
+
var sep = document.createElement('div')
|
|
431
|
+
sep.setAttribute('role', 'separator')
|
|
432
|
+
sep.setAttribute('data-dsh-admin-injected', '')
|
|
433
|
+
sep.style.cssText = 'margin:3px 0;border-top:1px solid var(--dsw-alias-border-subtle,rgba(200,200,210,0.3))'
|
|
434
|
+
viewport.appendChild(sep)
|
|
435
|
+
|
|
436
|
+
var btn = document.createElement('button')
|
|
437
|
+
btn.type = 'button'
|
|
438
|
+
btn.setAttribute('role', 'menuitem')
|
|
439
|
+
btn.setAttribute('data-dsh-admin-injected', '')
|
|
440
|
+
btn.textContent = label
|
|
441
|
+
var color = kind === 'danger' ? '#ef4444' : 'inherit'
|
|
442
|
+
var hoverBg = kind === 'danger' ? 'rgba(239,68,68,0.12)' : 'rgba(200,200,210,0.3)'
|
|
443
|
+
btn.style.cssText = 'display:flex;align-items:center;gap:8px;width:100%;padding:6px 10px;border:0;background:transparent;cursor:pointer;font:inherit;font-size:12px;color:' + color + ';border-radius:5px;text-align:left'
|
|
444
|
+
btn.addEventListener('mouseenter', function () { btn.style.background = hoverBg })
|
|
445
|
+
btn.addEventListener('mouseleave', function () { btn.style.background = 'transparent' })
|
|
446
|
+
// Optional two-step confirm (confirmText): the first click arms the
|
|
447
|
+
// item and relabels it; only a second click within 4s runs onClick —
|
|
448
|
+
// destructive items must never fire on a single misclick.
|
|
449
|
+
var armed = false
|
|
450
|
+
var disarmTimer = null
|
|
451
|
+
btn.addEventListener('click', function (e) {
|
|
452
|
+
e.stopPropagation()
|
|
453
|
+
if (confirmText === undefined) { onClick(); return }
|
|
454
|
+
if (!armed) {
|
|
455
|
+
armed = true
|
|
456
|
+
btn.textContent = confirmText
|
|
457
|
+
disarmTimer = setTimeout(function () {
|
|
458
|
+
armed = false
|
|
459
|
+
btn.textContent = label
|
|
460
|
+
}, 4000)
|
|
461
|
+
return
|
|
462
|
+
}
|
|
463
|
+
clearTimeout(disarmTimer)
|
|
464
|
+
armed = false
|
|
465
|
+
btn.textContent = label
|
|
466
|
+
onClick()
|
|
467
|
+
})
|
|
468
|
+
viewport.appendChild(btn)
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
var observer = new MutationObserver(function (mutations) {
|
|
472
|
+
for (var i = 0; i < mutations.length; i++) {
|
|
473
|
+
var added = mutations[i].addedNodes
|
|
474
|
+
if (!added || added.length === 0) continue
|
|
475
|
+
for (var j = 0; j < added.length; j++) {
|
|
476
|
+
var node = added[j]
|
|
477
|
+
if (node.nodeType !== 1) continue
|
|
478
|
+
var el = node
|
|
479
|
+
if (el.getAttribute && el.getAttribute('role') === 'menu') {
|
|
480
|
+
injectItems(el)
|
|
481
|
+
} else {
|
|
482
|
+
var menu = el.querySelector && el.querySelector('[role="menu"]')
|
|
483
|
+
if (menu) injectItems(menu)
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
})
|
|
488
|
+
|
|
489
|
+
observer.observe(document.body, { childList: true, subtree: true })
|
|
490
|
+
|
|
491
|
+
return function () {
|
|
492
|
+
observer.disconnect()
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Plugins management settings section (standalone, no internal tabs).
|
|
498
|
+
*/
|
|
499
|
+
function PluginsSection(props) {
|
|
500
|
+
var pairPlugins = useState({
|
|
501
|
+
profileDir: '',
|
|
502
|
+
plugins: [],
|
|
503
|
+
busy: false,
|
|
504
|
+
error: '',
|
|
505
|
+
spec: '',
|
|
506
|
+
confirming: null,
|
|
507
|
+
note: '',
|
|
508
|
+
output: '',
|
|
509
|
+
filter: 'all',
|
|
510
|
+
needle: '',
|
|
511
|
+
checkingUpdates: false,
|
|
512
|
+
updateChecked: false, // true once a check finished (even if no updates)
|
|
513
|
+
// Seed from persisted reminders so the ⬆ 有新版本 badge is visible the
|
|
514
|
+
// moment the panel mounts (before the fresh host check resolves) — the
|
|
515
|
+
// reminder is only dropped once a check confirms the upgrade completed.
|
|
516
|
+
updates: loadUpdateReminders(), // name -> { latest, updateAvailable, error?, at? }
|
|
517
|
+
})
|
|
518
|
+
var pView = pairPlugins[0]
|
|
519
|
+
var setPView = pairPlugins[1]
|
|
520
|
+
|
|
521
|
+
var alive = useRef(false)
|
|
522
|
+
|
|
523
|
+
function patchPlugin(partial) {
|
|
524
|
+
setPView(function (cur) {
|
|
525
|
+
var next = {}
|
|
526
|
+
for (var k in cur) next[k] = cur[k]
|
|
527
|
+
for (var pk in partial) next[pk] = partial[pk]
|
|
528
|
+
return next
|
|
529
|
+
})
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function callRemote(method, args) {
|
|
533
|
+
return props.call(method, args)
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function reloadPlugins() {
|
|
537
|
+
patchPlugin({ busy: true, error: '', note: '' })
|
|
538
|
+
callRemote('pluginAdmin/list', {}).then(function (result) {
|
|
539
|
+
if (!alive.current) return
|
|
540
|
+
if (result.ok) {
|
|
541
|
+
patchPlugin({
|
|
542
|
+
busy: false,
|
|
543
|
+
profileDir: (result.value && result.value.profileDir) || '',
|
|
544
|
+
plugins: (result.value && result.value.plugins) || [],
|
|
545
|
+
})
|
|
546
|
+
} else {
|
|
547
|
+
patchPlugin({ busy: false, error: '加载插件失败:' + messageOf(result.error) })
|
|
548
|
+
}
|
|
549
|
+
}, function (failure) {
|
|
550
|
+
if (!alive.current) return
|
|
551
|
+
patchPlugin({ busy: false, error: '调用失败:' + messageOf(failure) })
|
|
552
|
+
})
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* Query the npm registry for newer versions of registry-installed bundles
|
|
557
|
+
* and stash per-plugin results. The resolution MERGES the fresh authoritative
|
|
558
|
+
* results onto the previously persisted reminders:
|
|
559
|
+
* - newer version confirmed -> (re)arm & persist the reminder
|
|
560
|
+
* - confirmed up to date -> drop the reminder (更新完删除提醒)
|
|
561
|
+
* - per-plugin check error -> keep the known reminder (a transient
|
|
562
|
+
* registry failure must not erase it)
|
|
563
|
+
* - no longer a registry install -> drop stale reminders
|
|
564
|
+
* Runs concurrently with the list so the panel stays responsive; failures
|
|
565
|
+
* are per-plugin, never fatal.
|
|
566
|
+
*/
|
|
567
|
+
function checkUpdates() {
|
|
568
|
+
if (pView.checkingUpdates) return
|
|
569
|
+
patchPlugin({ checkingUpdates: true, note: '' })
|
|
570
|
+
callRemote('pluginAdmin/checkUpdates', {}).then(function (result) {
|
|
571
|
+
if (!alive.current) return
|
|
572
|
+
patchPlugin({ checkingUpdates: false })
|
|
573
|
+
if (!result.ok) {
|
|
574
|
+
patchPlugin({ error: '检查更新失败:' + messageOf(result.error) })
|
|
575
|
+
// A failed call must not wipe previously known reminders — they stay
|
|
576
|
+
// seeded from localStorage and keep rendering on the cards.
|
|
577
|
+
return
|
|
578
|
+
}
|
|
579
|
+
var list = (result.value && result.value.updates) || []
|
|
580
|
+
// Only the plugins the host actually checked are "live" registry
|
|
581
|
+
// installs; builtins / local-path installs never carry a reminder and a
|
|
582
|
+
// reminder for a removed plugin must be dropped too.
|
|
583
|
+
var liveNames = {}
|
|
584
|
+
for (var a = 0; a < list.length; a++) liveNames[list[a].name] = true
|
|
585
|
+
|
|
586
|
+
var prev = pView.updates || {}
|
|
587
|
+
var map = {}
|
|
588
|
+
for (var k in prev) {
|
|
589
|
+
if (prev[k] && liveNames[k]) map[k] = prev[k]
|
|
590
|
+
}
|
|
591
|
+
for (var i = 0; i < list.length; i++) {
|
|
592
|
+
var u = list[i]
|
|
593
|
+
if (u.updateAvailable === true) {
|
|
594
|
+
map[u.name] = { latest: u.latest, updateAvailable: true, error: '', at: Date.now() }
|
|
595
|
+
} else if (u.latest !== null && u.latest !== undefined && !u.error) {
|
|
596
|
+
// Authoritatively current — the newer version is now installed.
|
|
597
|
+
delete map[u.name]
|
|
598
|
+
} else if (map[u.name]) {
|
|
599
|
+
// Query failed for this entry: keep the prior reminder, surface the
|
|
600
|
+
// transient error alongside it.
|
|
601
|
+
map[u.name] = mergeReminder(map[u.name], { error: u.error || '' })
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
// Persist whatever the check decided so the reminder survives dialog
|
|
605
|
+
// close/reopen and dsh restarts (best-effort; never fatal).
|
|
606
|
+
saveUpdateReminders(map)
|
|
607
|
+
|
|
608
|
+
var updateCount = 0
|
|
609
|
+
var checkErrorCount = 0
|
|
610
|
+
for (var n in map) {
|
|
611
|
+
if (map[n].updateAvailable === true) updateCount++
|
|
612
|
+
if (map[n].error) checkErrorCount++
|
|
613
|
+
}
|
|
614
|
+
patchPlugin({
|
|
615
|
+
updates: map,
|
|
616
|
+
updateChecked: true,
|
|
617
|
+
note: updateCount > 0
|
|
618
|
+
? '发现 ' + updateCount + ' 个插件有新版本'
|
|
619
|
+
: (checkErrorCount > 0
|
|
620
|
+
? '已检查 ' + list.length + ' 个插件(' + checkErrorCount + ' 个查询失败),其余均为最新版本'
|
|
621
|
+
: (list.length > 0 ? '已检查 ' + list.length + ' 个插件,均为最新版本' : '')),
|
|
622
|
+
})
|
|
623
|
+
}, function (failure) {
|
|
624
|
+
if (!alive.current) return
|
|
625
|
+
patchPlugin({ checkingUpdates: false, error: '检查更新调用失败:' + messageOf(failure) })
|
|
626
|
+
})
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/** Upgrade one plugin to its latest version (registry install by name). */
|
|
630
|
+
function upgradePlugin(name) {
|
|
631
|
+
if (pView.busy) return
|
|
632
|
+
var spec = name + '@latest'
|
|
633
|
+
patchPlugin({ busy: true, error: '', confirming: null, note: '' })
|
|
634
|
+
callRemote('pluginAdmin/install', { spec: spec }).then(function (result) {
|
|
635
|
+
if (!alive.current) return
|
|
636
|
+
if (result.ok) {
|
|
637
|
+
patchPlugin({
|
|
638
|
+
busy: false,
|
|
639
|
+
note: '已更新 ' + name + '。更改在重启 dsh 后生效',
|
|
640
|
+
output: (result.value && result.value.output) || '',
|
|
641
|
+
profileDir: (result.value && result.value.profileDir) || '',
|
|
642
|
+
plugins: (result.value && result.value.plugins) || [],
|
|
643
|
+
})
|
|
644
|
+
// The upgrade consumed this reminder: clear it right away and persist
|
|
645
|
+
// so it does not linger after the update (更新完删除提醒); the refresh
|
|
646
|
+
// check below confirms the new version is current.
|
|
647
|
+
var nextReminders = {}
|
|
648
|
+
for (var rk in pView.updates) {
|
|
649
|
+
if (rk !== name) nextReminders[rk] = pView.updates[rk]
|
|
650
|
+
}
|
|
651
|
+
saveUpdateReminders(nextReminders)
|
|
652
|
+
patchPlugin({ updates: nextReminders })
|
|
653
|
+
// Refresh the update map so the upgraded entry stops flagging.
|
|
654
|
+
checkUpdates()
|
|
655
|
+
return
|
|
656
|
+
}
|
|
657
|
+
patchPlugin({ busy: false, error: '更新失败:' + messageOf(result.error) })
|
|
658
|
+
reloadPlugins()
|
|
659
|
+
}, function (failure) {
|
|
660
|
+
if (!alive.current) return
|
|
661
|
+
patchPlugin({ busy: false, error: '调用失败:' + messageOf(failure) })
|
|
662
|
+
})
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
function installPlugin() {
|
|
666
|
+
if (pView.busy || pView.spec.trim() === '') return
|
|
667
|
+
var spec = pView.spec.trim()
|
|
668
|
+
patchPlugin({ busy: true, error: '', confirming: null, note: '' })
|
|
669
|
+
callRemote('pluginAdmin/install', { spec: spec }).then(function (result) {
|
|
670
|
+
if (!alive.current) return
|
|
671
|
+
if (result.ok) {
|
|
672
|
+
patchPlugin({
|
|
673
|
+
busy: false,
|
|
674
|
+
note: '安装完成。更改在重启 dsh 后生效',
|
|
675
|
+
output: (result.value && result.value.output) || '',
|
|
676
|
+
profileDir: (result.value && result.value.profileDir) || '',
|
|
677
|
+
plugins: (result.value && result.value.plugins) || [],
|
|
678
|
+
spec: '',
|
|
679
|
+
})
|
|
680
|
+
return
|
|
681
|
+
}
|
|
682
|
+
patchPlugin({ busy: false, error: '安装失败:' + messageOf(result.error) })
|
|
683
|
+
reloadPlugins()
|
|
684
|
+
}, function (failure) {
|
|
685
|
+
if (!alive.current) return
|
|
686
|
+
patchPlugin({ busy: false, error: '调用失败:' + messageOf(failure) })
|
|
687
|
+
})
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
function removePlugin(name) {
|
|
691
|
+
patchPlugin({ busy: true, error: '', confirming: null, note: '' })
|
|
692
|
+
callRemote('pluginAdmin/remove', { name: name }).then(function (result) {
|
|
693
|
+
if (!alive.current) return
|
|
694
|
+
if (result.ok) {
|
|
695
|
+
patchPlugin({
|
|
696
|
+
busy: false,
|
|
697
|
+
note: '卸载完成。更改在重启 dsh 后生效',
|
|
698
|
+
output: (result.value && result.value.output) || '',
|
|
699
|
+
profileDir: (result.value && result.value.profileDir) || '',
|
|
700
|
+
plugins: (result.value && result.value.plugins) || [],
|
|
701
|
+
})
|
|
702
|
+
return
|
|
703
|
+
}
|
|
704
|
+
patchPlugin({ busy: false, error: '卸载失败:' + messageOf(result.error) })
|
|
705
|
+
reloadPlugins()
|
|
706
|
+
}, function (failure) {
|
|
707
|
+
if (!alive.current) return
|
|
708
|
+
patchPlugin({ busy: false, error: '调用失败:' + messageOf(failure) })
|
|
709
|
+
})
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
useEffect(function () {
|
|
713
|
+
alive.current = true
|
|
714
|
+
reloadPlugins()
|
|
715
|
+
// Auto-check for remote updates on mount (the panel loads lazily when
|
|
716
|
+
// the 插件管理 tab is opened, so this runs once per open). The host
|
|
717
|
+
// caches registry answers for 5 minutes, so re-opening the tab shortly
|
|
718
|
+
// after does not re-hit the registry. The manual ⬆ 检查更新 button stays
|
|
719
|
+
// for forcing a fresh check.
|
|
720
|
+
checkUpdates()
|
|
721
|
+
return function () { alive.current = false }
|
|
722
|
+
}, [])
|
|
723
|
+
|
|
724
|
+
return createElement('div', { 'data-dsh-admin-section': '' },
|
|
725
|
+
renderPluginsView(pView, patchPlugin, installPlugin, removePlugin, checkUpdates, upgradePlugin))
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
/**
|
|
729
|
+
* Sessions management settings section (standalone, no internal tabs).
|
|
730
|
+
*/
|
|
731
|
+
function SessionsSection(props) {
|
|
732
|
+
var pairSessions = useState({
|
|
733
|
+
sessions: [],
|
|
734
|
+
workspaces: [],
|
|
735
|
+
busy: false,
|
|
736
|
+
error: '',
|
|
737
|
+
needle: '',
|
|
738
|
+
confirming: null,
|
|
739
|
+
filter: 'all',
|
|
740
|
+
})
|
|
741
|
+
var sView = pairSessions[0]
|
|
742
|
+
var setSView = pairSessions[1]
|
|
743
|
+
|
|
744
|
+
var alive = useRef(false)
|
|
745
|
+
|
|
746
|
+
function patchSession(partial) {
|
|
747
|
+
setSView(function (cur) {
|
|
748
|
+
var next = {}
|
|
749
|
+
for (var k in cur) next[k] = cur[k]
|
|
750
|
+
for (var pk in partial) next[pk] = partial[pk]
|
|
751
|
+
return next
|
|
752
|
+
})
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
function callRemote(method, args) {
|
|
756
|
+
return props.call(method, args)
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
function reloadSessions() {
|
|
760
|
+
patchSession({ busy: true, error: '' })
|
|
761
|
+
callRemote('sessionAdmin/list', {}).then(function (result) {
|
|
762
|
+
if (!alive.current) return
|
|
763
|
+
if (result.ok) {
|
|
764
|
+
patchSession({
|
|
765
|
+
busy: false,
|
|
766
|
+
sessions: (result.value && result.value.sessions) || [],
|
|
767
|
+
workspaces: (result.value && result.value.workspaces) || [],
|
|
768
|
+
})
|
|
769
|
+
} else {
|
|
770
|
+
patchSession({ busy: false, error: '加载会话失败:' + messageOf(result.error) })
|
|
771
|
+
}
|
|
772
|
+
}, function (failure) {
|
|
773
|
+
if (!alive.current) return
|
|
774
|
+
patchSession({ busy: false, error: '调用失败:' + messageOf(failure) })
|
|
775
|
+
})
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
function actSession(method, sessionId) {
|
|
779
|
+
patchSession({ busy: true, error: '', confirming: null })
|
|
780
|
+
callRemote('sessionAdmin/' + method, { sessionId: sessionId }).then(function (result) {
|
|
781
|
+
if (!alive.current) return
|
|
782
|
+
if (result.ok) {
|
|
783
|
+
// Refresh this panel's own list, and nudge the sidebar (via the
|
|
784
|
+
// sessions service) so a deleted session disappears immediately
|
|
785
|
+
// instead of lingering until reload.
|
|
786
|
+
if ((method === 'deleteSession' || method === 'closeSession') && props.refreshSessions) props.refreshSessions()
|
|
787
|
+
return reloadSessions()
|
|
788
|
+
}
|
|
789
|
+
patchSession({ busy: false, error: '操作失败:' + messageOf(result.error) })
|
|
790
|
+
}, function (failure) {
|
|
791
|
+
if (!alive.current) return
|
|
792
|
+
patchSession({ busy: false, error: '调用失败:' + messageOf(failure) })
|
|
793
|
+
})
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
useEffect(function () {
|
|
797
|
+
alive.current = true
|
|
798
|
+
reloadSessions()
|
|
799
|
+
return function () { alive.current = false }
|
|
800
|
+
}, [])
|
|
801
|
+
|
|
802
|
+
return createElement('div', { 'data-dsh-admin-section': '' },
|
|
803
|
+
renderSessionsView(sView, patchSession, reloadSessions, actSession))
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
/**
|
|
807
|
+
* MCP configuration settings section (standalone, no internal tabs).
|
|
808
|
+
*/
|
|
809
|
+
|
|
810
|
+
/* ------------------------------------------------------------------------ */
|
|
811
|
+
/* MCP test-result cache */
|
|
812
|
+
/* */
|
|
813
|
+
/* Successful connectivity probes are persisted to localStorage so the */
|
|
814
|
+
/* panel shows the last known status after tab switches or reopening the */
|
|
815
|
+
/* dialog instead of an empty state. Only OK results are cached (failures */
|
|
816
|
+
/* are usually transient); a cached row is labelled with its probe time, */
|
|
817
|
+
/* and the cache entry is invalidated when the config is saved or removed, */
|
|
818
|
+
/* or pruned when the entry disappears from the host list. */
|
|
819
|
+
/* ------------------------------------------------------------------------ */
|
|
820
|
+
|
|
821
|
+
var MCP_TEST_CACHE_KEY = 'dsh-plugin-admin/mcp-test-results'
|
|
822
|
+
|
|
823
|
+
/**
|
|
824
|
+
* Touch localStorage safely: on some environments (jsdom with an opaque
|
|
825
|
+
* origin, hardened browsers, privacy modes) merely READING window.localStorage
|
|
826
|
+
* throws a SecurityError, so the access itself has to sit inside try/catch.
|
|
827
|
+
* Returns null when storage is unavailable.
|
|
828
|
+
*/
|
|
829
|
+
function safeLocalStorage() {
|
|
830
|
+
try {
|
|
831
|
+
if (typeof window === 'undefined' || !window.localStorage) return null
|
|
832
|
+
return window.localStorage
|
|
833
|
+
} catch (e) {
|
|
834
|
+
return null
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
function mcpTestStorage() {
|
|
839
|
+
return safeLocalStorage()
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
function loadMcpTestCache() {
|
|
843
|
+
var store = mcpTestStorage()
|
|
844
|
+
if (store === null) return {}
|
|
845
|
+
try {
|
|
846
|
+
var parsed = JSON.parse(store.getItem(MCP_TEST_CACHE_KEY))
|
|
847
|
+
return parsed !== null && typeof parsed === 'object' ? parsed : {}
|
|
848
|
+
} catch (e) {
|
|
849
|
+
return {}
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
/** Mirror settled, successful probes into localStorage (best-effort). */
|
|
854
|
+
function saveMcpTestCache(state) {
|
|
855
|
+
var store = mcpTestStorage()
|
|
856
|
+
if (store === null) return
|
|
857
|
+
try {
|
|
858
|
+
var out = {}
|
|
859
|
+
for (var id in state) {
|
|
860
|
+
var s = state[id]
|
|
861
|
+
// Busy rows keep the previous cached result; failures stay ephemeral.
|
|
862
|
+
if (!s || s.busy || s.result === null || s.result === undefined) continue
|
|
863
|
+
out[id] = { result: s.result, at: typeof s.at === 'number' ? s.at : Date.now() }
|
|
864
|
+
}
|
|
865
|
+
store.setItem(MCP_TEST_CACHE_KEY, JSON.stringify(out))
|
|
866
|
+
} catch (e) {
|
|
867
|
+
// Quota exceeded / storage blocked — the cache is optional, never fatal.
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
/** Drop one entry's cached probe (config saved or entry removed). */
|
|
872
|
+
function clearMcpTestCacheEntry(id) {
|
|
873
|
+
var store = mcpTestStorage()
|
|
874
|
+
if (store === null) return
|
|
875
|
+
try {
|
|
876
|
+
var state = loadMcpTestCache()
|
|
877
|
+
if (state[id] === undefined) return
|
|
878
|
+
delete state[id]
|
|
879
|
+
store.setItem(MCP_TEST_CACHE_KEY, JSON.stringify(state))
|
|
880
|
+
} catch (e) { /* ignore */ }
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
/** Drop test states whose entry no longer exists in the host list. */
|
|
884
|
+
function pruneMcpTestState(map, entries) {
|
|
885
|
+
var valid = {}
|
|
886
|
+
for (var i = 0; i < entries.length; i++) valid[entries[i].id] = true
|
|
887
|
+
var next = {}
|
|
888
|
+
var changed = false
|
|
889
|
+
for (var id in map) {
|
|
890
|
+
if (valid[id]) next[id] = map[id]
|
|
891
|
+
else changed = true
|
|
892
|
+
}
|
|
893
|
+
if (changed) saveMcpTestCache(next)
|
|
894
|
+
return next
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
/** Compact timestamp for a cached probe ("14:32" today, "6-1 14:32" older). */
|
|
898
|
+
function formatTestTime(ms) {
|
|
899
|
+
if (typeof ms !== 'number' || !Number.isFinite(ms)) return ''
|
|
900
|
+
var d = new Date(ms)
|
|
901
|
+
var pad = function (n) { return (n < 10 ? '0' : '') + String(n) }
|
|
902
|
+
var hm = pad(d.getHours()) + ':' + pad(d.getMinutes())
|
|
903
|
+
var now = new Date()
|
|
904
|
+
if (d.getFullYear() === now.getFullYear() && d.getMonth() === now.getMonth() && d.getDate() === now.getDate()) return hm
|
|
905
|
+
return (d.getMonth() + 1) + '-' + d.getDate() + ' ' + hm
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
/* Update-reminder persistence */
|
|
909
|
+
/* */
|
|
910
|
+
/* A "⬆ 有新版本" reminder is a durable fact, not a transient fetch result: */
|
|
911
|
+
/* once a newer version is detected it is kept in localStorage until a */
|
|
912
|
+
/* check confirms the installed version is current again (i.e. the update */
|
|
913
|
+
/* actually completed). This is what keeps the badge visible after closing */
|
|
914
|
+
/* and reopening the 管理中心, across dsh restarts, and even when a later */
|
|
915
|
+
/* re-check hits a transient network failure — and what clears it only once */
|
|
916
|
+
/* the plugin is upgraded (更新完删除提醒). */
|
|
917
|
+
/* ------------------------------------------------------------------------ */
|
|
918
|
+
|
|
919
|
+
var UPDATE_REMINDER_KEY = 'dsh-plugin-admin/update-reminders'
|
|
920
|
+
|
|
921
|
+
/** Shape-merge a reminder entry (keeps the original truthy fields). */
|
|
922
|
+
function mergeReminder(entry, partial) {
|
|
923
|
+
var next = {}
|
|
924
|
+
for (var k in entry) next[k] = entry[k]
|
|
925
|
+
for (var pk in partial) next[pk] = partial[pk]
|
|
926
|
+
return next
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
/**
|
|
930
|
+
* Load persisted update reminders as state entries:
|
|
931
|
+
* { name: { latest, at, updateAvailable, error } }. Storage-unavailable
|
|
932
|
+
* environments (opaque-origin jsdom, privacy modes) just get an empty map.
|
|
933
|
+
*/
|
|
934
|
+
function loadUpdateReminders() {
|
|
935
|
+
var store = safeLocalStorage()
|
|
936
|
+
if (store === null) return {}
|
|
937
|
+
try {
|
|
938
|
+
var parsed = JSON.parse(store.getItem(UPDATE_REMINDER_KEY))
|
|
939
|
+
var out = {}
|
|
940
|
+
if (parsed !== null && typeof parsed === 'object') {
|
|
941
|
+
for (var name in parsed) {
|
|
942
|
+
var r = parsed[name]
|
|
943
|
+
if (r && typeof r.latest === 'string' && r.latest.length > 0) {
|
|
944
|
+
out[name] = {
|
|
945
|
+
latest: r.latest,
|
|
946
|
+
at: typeof r.at === 'number' ? r.at : 0,
|
|
947
|
+
updateAvailable: true,
|
|
948
|
+
error: '',
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
return out
|
|
954
|
+
} catch (e) {
|
|
955
|
+
return {}
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
/** Mirror the current reminder set into localStorage (best-effort). */
|
|
960
|
+
function saveUpdateReminders(map) {
|
|
961
|
+
var store = safeLocalStorage()
|
|
962
|
+
if (store === null) return
|
|
963
|
+
try {
|
|
964
|
+
var out = {}
|
|
965
|
+
for (var name in map) {
|
|
966
|
+
var r = map[name]
|
|
967
|
+
if (r && r.updateAvailable === true && typeof r.latest === 'string' && r.latest.length > 0) {
|
|
968
|
+
out[name] = { latest: r.latest, at: typeof r.at === 'number' ? r.at : Date.now() }
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
store.setItem(UPDATE_REMINDER_KEY, JSON.stringify(out))
|
|
972
|
+
} catch (e) {
|
|
973
|
+
// Quota exceeded / storage blocked — the reminder is optional, never fatal.
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
function McpSection(props) {
|
|
978
|
+
var pairMcp = useState({
|
|
979
|
+
mcpEntries: [],
|
|
980
|
+
mcpBusy: false,
|
|
981
|
+
mcpError: '',
|
|
982
|
+
mcpEditorOpen: false,
|
|
983
|
+
mcpDraft: null,
|
|
984
|
+
// entryId -> { busy?, result?, error?, at? }; previous successful probes
|
|
985
|
+
// are restored from localStorage so statuses survive panel remounts.
|
|
986
|
+
mcpTestState: loadMcpTestCache(),
|
|
987
|
+
})
|
|
988
|
+
var mView = pairMcp[0]
|
|
989
|
+
var setMView = pairMcp[1]
|
|
990
|
+
|
|
991
|
+
var alive = useRef(false)
|
|
992
|
+
|
|
993
|
+
function patchMcp(partial) {
|
|
994
|
+
setMView(function (cur) {
|
|
995
|
+
var next = {}
|
|
996
|
+
for (var k in cur) next[k] = cur[k]
|
|
997
|
+
for (var pk in partial) next[pk] = partial[pk]
|
|
998
|
+
return next
|
|
999
|
+
})
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
// Merge a partial into the CURRENT mcpDraft via a functional update, so
|
|
1003
|
+
// rapid successive field edits (React batching) never lose earlier input.
|
|
1004
|
+
function patchDraft(partial) {
|
|
1005
|
+
setMView(function (cur) {
|
|
1006
|
+
var next = {}
|
|
1007
|
+
for (var k in cur) next[k] = cur[k]
|
|
1008
|
+
next.mcpDraft = mergeDraft(cur.mcpDraft, partial)
|
|
1009
|
+
return next
|
|
1010
|
+
})
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
function callRemote(method, args) {
|
|
1014
|
+
return props.call(method, args)
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
function reloadMcp() {
|
|
1018
|
+
patchMcp({ mcpBusy: true, mcpError: '' })
|
|
1019
|
+
callRemote('mcpAdmin/list', {}).then(function (result) {
|
|
1020
|
+
if (!alive.current) return
|
|
1021
|
+
if (result.ok) {
|
|
1022
|
+
var entries = (result.value && result.value.entries) || []
|
|
1023
|
+
setMView(function (cur) {
|
|
1024
|
+
var next = {}
|
|
1025
|
+
for (var k in cur) next[k] = cur[k]
|
|
1026
|
+
next.mcpBusy = false
|
|
1027
|
+
next.mcpEntries = entries
|
|
1028
|
+
// Drop cached probes whose entry no longer exists on the host.
|
|
1029
|
+
next.mcpTestState = pruneMcpTestState(cur.mcpTestState, entries)
|
|
1030
|
+
return next
|
|
1031
|
+
})
|
|
1032
|
+
} else {
|
|
1033
|
+
patchMcp({ mcpBusy: false, mcpError: '加载 MCP 配置失败:' + messageOf(result.error) })
|
|
1034
|
+
}
|
|
1035
|
+
}, function (failure) {
|
|
1036
|
+
if (!alive.current) return
|
|
1037
|
+
patchMcp({ mcpBusy: false, mcpError: '调用失败:' + messageOf(failure) })
|
|
1038
|
+
})
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
function openMcpEditor(entry) {
|
|
1042
|
+
if (entry !== null && entry !== undefined && (entry.config === null || entry.config === undefined)) {
|
|
1043
|
+
patchMcp({ mcpError: '该 MCP 配置无法安全解析,已禁止在此覆盖;请在 cordis.patch.yml 中手动编辑。' })
|
|
1044
|
+
return
|
|
1045
|
+
}
|
|
1046
|
+
var source = entry !== null && entry !== undefined ? entry.config : null
|
|
1047
|
+
var draft = source ? {
|
|
1048
|
+
id: entry.id,
|
|
1049
|
+
isNew: false,
|
|
1050
|
+
serverName: source.serverName || entry.id,
|
|
1051
|
+
transport: source.transport,
|
|
1052
|
+
command: source.command || '',
|
|
1053
|
+
url: source.url || '',
|
|
1054
|
+
args: (source.args || []).join(' '),
|
|
1055
|
+
argsOriginal: source.args || [],
|
|
1056
|
+
argsChanged: false,
|
|
1057
|
+
env: Object.keys(source.env || {}).map(function (key) { return key + '=' + source.env[key] }).join('\n'),
|
|
1058
|
+
envOriginal: source.env || {},
|
|
1059
|
+
envChanged: false,
|
|
1060
|
+
cwd: source.cwd || '',
|
|
1061
|
+
headers: Object.keys(source.headers || {}).map(function (k) { return k + '=' + source.headers[k] }).join('\n'),
|
|
1062
|
+
headersOriginal: source.headers || {},
|
|
1063
|
+
headersChanged: false,
|
|
1064
|
+
toolCallTimeoutMs: source.toolCallTimeoutMs,
|
|
1065
|
+
failOnStartupError: source.failOnStartupError === true,
|
|
1066
|
+
reconnectEnabled: source.reconnect ? source.reconnect.enabled === true : false,
|
|
1067
|
+
reconnectInitialDelayMs: source.reconnect ? source.reconnect.initialDelayMs : 1000,
|
|
1068
|
+
reconnectMaxDelayMs: source.reconnect ? source.reconnect.maxDelayMs : 30000,
|
|
1069
|
+
reconnectMaxAttempts: source.reconnect ? source.reconnect.maxAttempts : 10,
|
|
1070
|
+
} : {
|
|
1071
|
+
// New server: pre-fill a collision-free id so the user does not have to
|
|
1072
|
+
// invent one; they can still edit it or regenerate it.
|
|
1073
|
+
id: generateMcpId(mView.mcpEntries),
|
|
1074
|
+
isNew: true,
|
|
1075
|
+
serverName: '',
|
|
1076
|
+
transport: 'stdio',
|
|
1077
|
+
command: '',
|
|
1078
|
+
url: '',
|
|
1079
|
+
args: '',
|
|
1080
|
+
argsOriginal: [],
|
|
1081
|
+
argsChanged: false,
|
|
1082
|
+
env: '',
|
|
1083
|
+
envOriginal: {},
|
|
1084
|
+
envChanged: false,
|
|
1085
|
+
cwd: '',
|
|
1086
|
+
headers: '',
|
|
1087
|
+
headersOriginal: {},
|
|
1088
|
+
headersChanged: false,
|
|
1089
|
+
toolCallTimeoutMs: undefined,
|
|
1090
|
+
failOnStartupError: false,
|
|
1091
|
+
reconnectEnabled: false,
|
|
1092
|
+
reconnectInitialDelayMs: 1000,
|
|
1093
|
+
reconnectMaxDelayMs: 30000,
|
|
1094
|
+
reconnectMaxAttempts: 10,
|
|
1095
|
+
}
|
|
1096
|
+
patchMcp({ mcpEditorOpen: true, mcpDraft: draft, mcpError: '' })
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
function closeMcpEditor() {
|
|
1100
|
+
patchMcp({ mcpEditorOpen: false, mcpDraft: null, mcpError: '' })
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
function saveMcpDraft() {
|
|
1104
|
+
var draft = mView.mcpDraft
|
|
1105
|
+
if (draft === null) return
|
|
1106
|
+
var config
|
|
1107
|
+
if (draft.transport === 'streamable-http') {
|
|
1108
|
+
config = { transport: 'streamable-http', serverName: draft.serverName, url: draft.url }
|
|
1109
|
+
var headersStr = Object.keys(draft.headersOriginal || {}).map(function (k) { return k + '=' + draft.headersOriginal[k] }).join('\n')
|
|
1110
|
+
var headers = (draft.headersChanged && draft.headers !== headersStr) ? {} : draft.headersOriginal
|
|
1111
|
+
if (draft.headersChanged && draft.headers !== headersStr && draft.headers !== '') {
|
|
1112
|
+
// Newline-only split: header values legitimately contain ';'/','
|
|
1113
|
+
// (Cookie, Accept), which must never become pair separators.
|
|
1114
|
+
var pairs = draft.headers.split('\n').map(function (s) { return s.trim() }).filter(Boolean)
|
|
1115
|
+
for (var i = 0; i < pairs.length; i++) {
|
|
1116
|
+
var eq = pairs[i].indexOf('=')
|
|
1117
|
+
if (eq > 0) headers[pairs[i].slice(0, eq).trim()] = pairs[i].slice(eq + 1).trim()
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
if (Object.keys(headers).length > 0) config.headers = headers
|
|
1121
|
+
} else {
|
|
1122
|
+
config = { transport: 'stdio', serverName: draft.serverName, command: draft.command }
|
|
1123
|
+
var argsStr = (draft.argsOriginal || []).join(' ')
|
|
1124
|
+
var args = (draft.argsChanged && draft.args !== argsStr) ? draft.args.split(/\s+/).filter(Boolean) : draft.argsOriginal
|
|
1125
|
+
if (args.length > 0) config.args = args
|
|
1126
|
+
var envStr = Object.keys(draft.envOriginal || {}).map(function (k) { return k + '=' + draft.envOriginal[k] }).join('\n')
|
|
1127
|
+
var env = (draft.envChanged && draft.env !== envStr) ? {} : draft.envOriginal
|
|
1128
|
+
if (draft.envChanged && draft.env !== envStr && draft.env !== '') {
|
|
1129
|
+
// Newline-only split: values like PATH=C:\a;C:\b would lose their
|
|
1130
|
+
// tail (and Windows paths carry ';' everywhere).
|
|
1131
|
+
var pairs = draft.env.split('\n').map(function (s) { return s.trim() }).filter(Boolean)
|
|
1132
|
+
for (var i = 0; i < pairs.length; i++) {
|
|
1133
|
+
var eq = pairs[i].indexOf('=')
|
|
1134
|
+
if (eq > 0) env[pairs[i].slice(0, eq).trim()] = pairs[i].slice(eq + 1).trim()
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
if (Object.keys(env).length > 0) config.env = env
|
|
1138
|
+
if (draft.cwd !== '') config.cwd = draft.cwd
|
|
1139
|
+
}
|
|
1140
|
+
if (draft.toolCallTimeoutMs !== undefined) config.toolCallTimeoutMs = draft.toolCallTimeoutMs
|
|
1141
|
+
if (draft.failOnStartupError) config.failOnStartupError = true
|
|
1142
|
+
if (draft.reconnectEnabled) {
|
|
1143
|
+
config.reconnect = {
|
|
1144
|
+
enabled: true,
|
|
1145
|
+
initialDelayMs: Number(draft.reconnectInitialDelayMs),
|
|
1146
|
+
maxDelayMs: Number(draft.reconnectMaxDelayMs),
|
|
1147
|
+
maxAttempts: Number(draft.reconnectMaxAttempts),
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
patchMcp({ mcpBusy: true, mcpError: '' })
|
|
1151
|
+
callRemote('mcpAdmin/upsert', { entry: { id: draft.id, config: config } }).then(function (result) {
|
|
1152
|
+
if (!alive.current) return
|
|
1153
|
+
if (result.ok) {
|
|
1154
|
+
// The config changed, so any cached probe no longer describes this
|
|
1155
|
+
// server — drop it from storage and from the panel.
|
|
1156
|
+
clearMcpTestCacheEntry(draft.id)
|
|
1157
|
+
patchMcpTest(draft.id, { busy: false, result: null, error: null })
|
|
1158
|
+
patchMcp({ mcpBusy: false, mcpEntries: (result.value && result.value.entries) || [], mcpEditorOpen: false, mcpDraft: null })
|
|
1159
|
+
} else {
|
|
1160
|
+
patchMcp({ mcpBusy: false, mcpError: '保存 MCP 配置失败:' + messageOf(result.error) })
|
|
1161
|
+
}
|
|
1162
|
+
}, function (failure) {
|
|
1163
|
+
if (!alive.current) return
|
|
1164
|
+
patchMcp({ mcpBusy: false, mcpError: '调用失败:' + messageOf(failure) })
|
|
1165
|
+
})
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
function removeMcpEntry(id) {
|
|
1169
|
+
patchMcp({ mcpBusy: true, mcpError: '' })
|
|
1170
|
+
callRemote('mcpAdmin/remove', { id: id }).then(function (result) {
|
|
1171
|
+
if (!alive.current) return
|
|
1172
|
+
if (result.ok) {
|
|
1173
|
+
// The entry is gone — its cached probe must not linger in storage.
|
|
1174
|
+
clearMcpTestCacheEntry(id)
|
|
1175
|
+
patchMcp({ mcpBusy: false, mcpEntries: (result.value && result.value.entries) || [] })
|
|
1176
|
+
} else {
|
|
1177
|
+
patchMcp({ mcpBusy: false, mcpError: '移除 MCP 配置失败:' + messageOf(result.error) })
|
|
1178
|
+
}
|
|
1179
|
+
}, function (failure) {
|
|
1180
|
+
if (!alive.current) return
|
|
1181
|
+
patchMcp({ mcpBusy: false, mcpError: '调用失败:' + messageOf(failure) })
|
|
1182
|
+
})
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
/**
|
|
1186
|
+
* Set one entry's connectivity-test status against the CURRENT state.
|
|
1187
|
+
* Settled successful probes are mirrored into localStorage so the last
|
|
1188
|
+
* known status survives tab switches and reopening the settings dialog.
|
|
1189
|
+
*/
|
|
1190
|
+
function patchMcpTest(id, partial) {
|
|
1191
|
+
setMView(function (cur) {
|
|
1192
|
+
var nextTestState = mergeTestState(cur.mcpTestState, id, partial)
|
|
1193
|
+
saveMcpTestCache(nextTestState)
|
|
1194
|
+
var next = {}
|
|
1195
|
+
for (var k in cur) next[k] = cur[k]
|
|
1196
|
+
next.mcpTestState = nextTestState
|
|
1197
|
+
return next
|
|
1198
|
+
})
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
/** Run a host-side connectivity probe for one entry and stash the result. */
|
|
1202
|
+
function testMcpEntry(id) {
|
|
1203
|
+
patchMcpTest(id, { busy: true, result: null, error: null })
|
|
1204
|
+
callRemote('mcpAdmin/test', { id: id }).then(function (result) {
|
|
1205
|
+
if (!alive.current) return
|
|
1206
|
+
if (result.ok && result.value !== null && typeof result.value === 'object') {
|
|
1207
|
+
// `at` timestamps the probe; it is what the cached label renders.
|
|
1208
|
+
patchMcpTest(id, { busy: false, result: result.value, at: Date.now() })
|
|
1209
|
+
} else {
|
|
1210
|
+
patchMcpTest(id, { busy: false, result: null, error: messageOf(result.error) })
|
|
1211
|
+
}
|
|
1212
|
+
}, function (failure) {
|
|
1213
|
+
if (!alive.current) return
|
|
1214
|
+
patchMcpTest(id, { busy: false, result: null, error: messageOf(failure) })
|
|
1215
|
+
})
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
useEffect(function () {
|
|
1219
|
+
alive.current = true
|
|
1220
|
+
reloadMcp()
|
|
1221
|
+
return function () { alive.current = false }
|
|
1222
|
+
}, [])
|
|
1223
|
+
|
|
1224
|
+
return createElement('div', { 'data-dsh-admin-section': '' },
|
|
1225
|
+
renderMcpSection(mView, patchDraft, reloadMcp, openMcpEditor, closeMcpEditor, saveMcpDraft, removeMcpEntry, testMcpEntry))
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
/** Unified settings page; only the selected panel mounts and loads. */
|
|
1229
|
+
function AdminCenterSection(props) {
|
|
1230
|
+
var pairTab = useState('plugins')
|
|
1231
|
+
var activeTab = pairTab[0]
|
|
1232
|
+
var setActiveTab = pairTab[1]
|
|
1233
|
+
var tabs = [
|
|
1234
|
+
{ id: 'plugins', label: '插件管理', component: PluginsSection },
|
|
1235
|
+
{ id: 'sessions', label: '会话管理', component: SessionsSection },
|
|
1236
|
+
{ id: 'mcp', label: 'MCP 配置', component: McpSection },
|
|
1237
|
+
]
|
|
1238
|
+
var selected = tabs.find(function (tab) { return tab.id === activeTab }) || tabs[0]
|
|
1239
|
+
|
|
1240
|
+
return createElement('div', { 'data-dsh-admin-section': '' },
|
|
1241
|
+
createElement('div', { className: 'tabs', role: 'tablist', 'aria-label': '管理中心功能' },
|
|
1242
|
+
tabs.map(function (tab) {
|
|
1243
|
+
return createElement('button', {
|
|
1244
|
+
type: 'button',
|
|
1245
|
+
key: tab.id,
|
|
1246
|
+
className: 'tab' + (tab.id === selected.id ? ' active' : ''),
|
|
1247
|
+
role: 'tab',
|
|
1248
|
+
'aria-selected': tab.id === selected.id,
|
|
1249
|
+
onClick: function () { setActiveTab(tab.id) },
|
|
1250
|
+
}, tab.label)
|
|
1251
|
+
})
|
|
1252
|
+
),
|
|
1253
|
+
createElement(selected.component, { key: selected.id, call: props.call, refreshSessions: props.refreshSessions })
|
|
1254
|
+
)
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
/* ========================================================================== */
|
|
1258
|
+
/* Render Plugins View */
|
|
1259
|
+
/* ========================================================================== */
|
|
1260
|
+
|
|
1261
|
+
function renderPluginsView(view, patch, install, remove, checkUpdates, upgrade) {
|
|
1262
|
+
var elements = []
|
|
1263
|
+
|
|
1264
|
+
// Toolbar: search (primary, wide) + install input + actions in one row.
|
|
1265
|
+
elements.push(createElement('div', { className: 'toolbar', key: 'toolbar' },
|
|
1266
|
+
createElement('div', { className: 'search-wrap', key: 'search-wrap' },
|
|
1267
|
+
createElement('span', { className: 'search-icon', key: 'icon' }, '🔍'),
|
|
1268
|
+
createElement('input', {
|
|
1269
|
+
className: 'input',
|
|
1270
|
+
key: 'search-input',
|
|
1271
|
+
placeholder: '搜索插件(名称/版本/路径)...',
|
|
1272
|
+
value: view.needle,
|
|
1273
|
+
onChange: function (e) { patch({ needle: e.target.value }) },
|
|
1274
|
+
}),
|
|
1275
|
+
view.needle !== '' ? createElement('button', {
|
|
1276
|
+
type: 'button',
|
|
1277
|
+
key: 'btn-clear-search',
|
|
1278
|
+
className: 'btn sm',
|
|
1279
|
+
title: '清空搜索',
|
|
1280
|
+
onClick: function () { patch({ needle: '' }) },
|
|
1281
|
+
}, '✕') : null
|
|
1282
|
+
),
|
|
1283
|
+
createElement('div', { className: 'input-wrap install-wrap', key: 'install-wrap' },
|
|
1284
|
+
createElement('input', {
|
|
1285
|
+
className: 'input',
|
|
1286
|
+
placeholder: '安装包名/路径...',
|
|
1287
|
+
value: view.spec,
|
|
1288
|
+
disabled: view.busy,
|
|
1289
|
+
onChange: function (e) { patch({ spec: e.target.value }) },
|
|
1290
|
+
onKeyDown: function (e) { if (e.key === 'Enter') install() },
|
|
1291
|
+
})
|
|
1292
|
+
),
|
|
1293
|
+
createElement('button', {
|
|
1294
|
+
type: 'button',
|
|
1295
|
+
key: 'btn-install',
|
|
1296
|
+
className: 'btn primary',
|
|
1297
|
+
disabled: view.busy || view.spec.trim() === '',
|
|
1298
|
+
onClick: install,
|
|
1299
|
+
},
|
|
1300
|
+
view.busy ? createElement('span', { className: 'spinner', key: 'spin' }) : null,
|
|
1301
|
+
'安装'
|
|
1302
|
+
),
|
|
1303
|
+
createElement('button', {
|
|
1304
|
+
type: 'button',
|
|
1305
|
+
key: 'btn-check-updates',
|
|
1306
|
+
className: 'btn',
|
|
1307
|
+
disabled: view.busy || view.checkingUpdates,
|
|
1308
|
+
onClick: checkUpdates,
|
|
1309
|
+
},
|
|
1310
|
+
view.checkingUpdates ? createElement('span', { className: 'spinner', key: 'spin' }) : null,
|
|
1311
|
+
view.checkingUpdates ? '检查中...' : '⬆️ 检查更新'
|
|
1312
|
+
),
|
|
1313
|
+
))
|
|
1314
|
+
|
|
1315
|
+
// Filter Pills
|
|
1316
|
+
var totalPlugins = view.plugins.length
|
|
1317
|
+
var thirdPartyCount = 0
|
|
1318
|
+
var builtinCount = 0
|
|
1319
|
+
for (var i = 0; i < view.plugins.length; i++) {
|
|
1320
|
+
if (view.plugins[i].removable) thirdPartyCount++
|
|
1321
|
+
else builtinCount++
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
elements.push(createElement('div', { className: 'filter-bar', key: 'filters' },
|
|
1325
|
+
createElement('button', {
|
|
1326
|
+
type: 'button',
|
|
1327
|
+
key: 'filter-all',
|
|
1328
|
+
className: 'pill' + (view.filter === 'all' ? ' active' : ''),
|
|
1329
|
+
onClick: function () { patch({ filter: 'all' }) },
|
|
1330
|
+
}, '全部 (' + String(totalPlugins) + ')'),
|
|
1331
|
+
createElement('button', {
|
|
1332
|
+
type: 'button',
|
|
1333
|
+
key: 'filter-plugin',
|
|
1334
|
+
className: 'pill' + (view.filter === 'plugin' ? ' active' : ''),
|
|
1335
|
+
onClick: function () { patch({ filter: 'plugin' }) },
|
|
1336
|
+
}, '扩展插件 (' + String(thirdPartyCount) + ')'),
|
|
1337
|
+
createElement('button', {
|
|
1338
|
+
type: 'button',
|
|
1339
|
+
key: 'filter-builtin',
|
|
1340
|
+
className: 'pill' + (view.filter === 'builtin' ? ' active' : ''),
|
|
1341
|
+
onClick: function () { patch({ filter: 'builtin' }) },
|
|
1342
|
+
}, '系统内置 (' + String(builtinCount) + ')')
|
|
1343
|
+
))
|
|
1344
|
+
|
|
1345
|
+
// Busy banner / Error
|
|
1346
|
+
if (view.busy) {
|
|
1347
|
+
elements.push(createElement('div', { className: 'busy-banner', key: 'busy' },
|
|
1348
|
+
createElement('span', { className: 'spinner', key: 'spin' }),
|
|
1349
|
+
'正在执行 pnpm 操作(可能需要数秒至数分钟,请勿关闭窗口)...'
|
|
1350
|
+
))
|
|
1351
|
+
}
|
|
1352
|
+
if (view.error !== '') {
|
|
1353
|
+
elements.push(createElement('div', { className: 'error', key: 'error' }, view.error))
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
// Update-check status strip: gives the auto-check a visible outcome instead
|
|
1357
|
+
// of hiding it in the footer. Checking → spinner; done with updates →
|
|
1358
|
+
// orange; done without updates → green; per-plugin query failures surface
|
|
1359
|
+
// on the strip rather than vanishing.
|
|
1360
|
+
if (view.checkingUpdates) {
|
|
1361
|
+
elements.push(createElement('div', { className: 'update-strip checking', key: 'update-checking' },
|
|
1362
|
+
createElement('span', { className: 'spinner', key: 'spin' }),
|
|
1363
|
+
'正在检查插件版本更新…'
|
|
1364
|
+
))
|
|
1365
|
+
} else if (view.updateChecked) {
|
|
1366
|
+
var upCount = 0
|
|
1367
|
+
var errCount = 0
|
|
1368
|
+
for (var un in view.updates) {
|
|
1369
|
+
if (view.updates[un].updateAvailable === true) upCount++
|
|
1370
|
+
if (view.updates[un].error) errCount++
|
|
1371
|
+
}
|
|
1372
|
+
var stripClass = upCount > 0 ? 'update-strip has-updates'
|
|
1373
|
+
: (errCount > 0 ? 'update-strip has-errors' : 'update-strip ok')
|
|
1374
|
+
var stripText = upCount > 0
|
|
1375
|
+
? '⬆️ 发现 ' + upCount + ' 个插件有新版本,可点击卡片上的「更新」升级'
|
|
1376
|
+
: (errCount > 0
|
|
1377
|
+
? '⚠️ 有 ' + errCount + ' 个插件查询版本失败(网络或 registry 不可达)'
|
|
1378
|
+
: '✅ 已自动检查版本更新,全部为最新版本')
|
|
1379
|
+
elements.push(createElement('div', { className: stripClass, key: 'update-result' },
|
|
1380
|
+
createElement('span', { key: 't' }, stripText),
|
|
1381
|
+
createElement('button', {
|
|
1382
|
+
type: 'button',
|
|
1383
|
+
key: 'btn-recheck',
|
|
1384
|
+
className: 'btn sm',
|
|
1385
|
+
disabled: view.busy || view.checkingUpdates,
|
|
1386
|
+
onClick: checkUpdates,
|
|
1387
|
+
}, '🔄 重新检查')
|
|
1388
|
+
))
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
// Filter and render rows
|
|
1392
|
+
var needle = view.needle.trim().toLowerCase()
|
|
1393
|
+
var filtered = filterPlugins(view.plugins, view.filter, needle)
|
|
1394
|
+
var rows = []
|
|
1395
|
+
for (var j = 0; j < filtered.length; j++) {
|
|
1396
|
+
var p = filtered[j]
|
|
1397
|
+
rows.push(renderPluginCard(p, view, remove, patch, upgrade))
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
if (rows.length === 0) {
|
|
1401
|
+
rows.push(createElement('div', { className: 'empty', key: 'empty' },
|
|
1402
|
+
createElement('div', null, needle !== '' ? '🔍 无匹配的插件(试试其他关键词)' : '📦 暂无匹配的插件层')
|
|
1403
|
+
))
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
elements.push(createElement('div', { className: 'list', key: 'list' }, rows))
|
|
1407
|
+
|
|
1408
|
+
// Footer
|
|
1409
|
+
var hintText = view.note !== '' ? view.note : '更改在重启 dsh 后生效(关闭 dsh 进程后重新运行即可)'
|
|
1410
|
+
var hintTitle = view.output !== '' ? 'pnpm 输出:\n' + view.output : undefined
|
|
1411
|
+
elements.push(createElement('div', { className: 'footer', key: 'footer' },
|
|
1412
|
+
createElement('span', { className: 'path', key: 'profile-path', title: view.profileDir }, view.profileDir || 'Profile: 默认'),
|
|
1413
|
+
createElement('span', { className: 'hint', key: 'hint', title: hintTitle }, hintText)
|
|
1414
|
+
))
|
|
1415
|
+
|
|
1416
|
+
return createElement('div', { key: 'plugins-panel', style: { display: 'flex', flexDirection: 'column', gap: '10px' } }, elements)
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
|
|
1420
|
+
function renderMcpSection(view, patchDraft, reloadMcp, openMcpEditor, closeMcpEditor, saveMcpDraft, removeMcpEntry, testMcpEntry) {
|
|
1421
|
+
var children = []
|
|
1422
|
+
var header = createElement('div', { className: 'group-header', key: 'mcp-header', style: { marginTop: 0 } },
|
|
1423
|
+
createElement('span', { className: 'group-title', key: 't' }, '🔌 MCP 配置'),
|
|
1424
|
+
createElement('span', { className: 'group-count', key: 'c' }, String(view.mcpEntries.length) + ' 个'),
|
|
1425
|
+
createElement('button', {
|
|
1426
|
+
type: 'button',
|
|
1427
|
+
key: 'btn-refresh-mcp',
|
|
1428
|
+
className: 'btn',
|
|
1429
|
+
disabled: view.mcpBusy,
|
|
1430
|
+
onClick: reloadMcp,
|
|
1431
|
+
}, '🔄 刷新'),
|
|
1432
|
+
createElement('button', {
|
|
1433
|
+
type: 'button',
|
|
1434
|
+
key: 'btn-add-mcp',
|
|
1435
|
+
className: 'btn primary',
|
|
1436
|
+
disabled: view.mcpBusy,
|
|
1437
|
+
onClick: function () { openMcpEditor(null) },
|
|
1438
|
+
}, '➕ 添加服务器'),
|
|
1439
|
+
)
|
|
1440
|
+
children.push(header)
|
|
1441
|
+
|
|
1442
|
+
if (view.mcpError !== '') {
|
|
1443
|
+
children.push(createElement('div', { className: 'error', key: 'mcp-error' }, view.mcpError))
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
var rows = []
|
|
1447
|
+
for (var i = 0; i < view.mcpEntries.length; i++) {
|
|
1448
|
+
var entry = view.mcpEntries[i]
|
|
1449
|
+
var tag = createElement('span', { className: 'tag plugin', key: 'mcp-tag-' + entry.id },
|
|
1450
|
+
'MCP ' + String(entry.serverName || entry.id))
|
|
1451
|
+
var testState = (view.mcpTestState && view.mcpTestState[entry.id]) || null
|
|
1452
|
+
var testIndicator = null
|
|
1453
|
+
if (testState !== null && testState.busy) {
|
|
1454
|
+
testIndicator = createElement('span', { className: 'mcp-test mcp-test-busy', key: 'test-busy' },
|
|
1455
|
+
createElement('span', { className: 'spinner', key: 'sp' }),
|
|
1456
|
+
' 检测中...')
|
|
1457
|
+
} else if (testState !== null && testState.result !== null && testState.result !== undefined) {
|
|
1458
|
+
var r = testState.result
|
|
1459
|
+
var ok = r.ok === true
|
|
1460
|
+
var label = ok ? '✅ 连通'
|
|
1461
|
+
: '❌ 不通'
|
|
1462
|
+
var detail = ok
|
|
1463
|
+
? (r.serverInfo ? (r.serverInfo.name || '') + (r.serverInfo.version ? ' v' + r.serverInfo.version : '') : '') +
|
|
1464
|
+
(typeof r.toolCount === 'number' ? ' · ' + r.toolCount + ' 个工具' : '') +
|
|
1465
|
+
(r.pingOk === false ? ' · 初始化成功但 ping 失败' : '')
|
|
1466
|
+
: (r.error || '连接失败') + (r.ms !== undefined ? ' (' + r.ms + 'ms)' : '')
|
|
1467
|
+
var detailChildren = [
|
|
1468
|
+
createElement('span', { key: 'lbl' }, label + (detail !== '' ? ' ' + detail : '')),
|
|
1469
|
+
]
|
|
1470
|
+
if (ok && Array.isArray(r.tools) && r.tools.length > 0) {
|
|
1471
|
+
detailChildren.push(createElement('span', { key: 'tools-hint', style: { opacity: 0.75 } },
|
|
1472
|
+
'工具:' + r.tools.join('、')))
|
|
1473
|
+
}
|
|
1474
|
+
if (r.warning) {
|
|
1475
|
+
detailChildren.push(createElement('span', { key: 'warn', className: 'mcp-test-warn', title: r.warning },
|
|
1476
|
+
'⚠️ ' + r.warning))
|
|
1477
|
+
}
|
|
1478
|
+
// Cached probe: label when it was taken so a restored status is never
|
|
1479
|
+
// mistaken for a just-run check. Clicking 🔌 测试 refreshes it.
|
|
1480
|
+
if (typeof testState.at === 'number') {
|
|
1481
|
+
detailChildren.push(createElement('span', {
|
|
1482
|
+
key: 'cached-at',
|
|
1483
|
+
className: 'mcp-test-cached',
|
|
1484
|
+
title: '上次检测结果(本地缓存)。点击「🔌 测试」可重新检测。',
|
|
1485
|
+
}, '· 缓存于 ' + formatTestTime(testState.at)))
|
|
1486
|
+
}
|
|
1487
|
+
testIndicator = createElement('div', {
|
|
1488
|
+
className: 'mcp-test mcp-test-list ' + (ok ? 'mcp-test-ok' : 'mcp-test-fail'),
|
|
1489
|
+
key: 'test-result',
|
|
1490
|
+
title: detail,
|
|
1491
|
+
}, detailChildren)
|
|
1492
|
+
} else if (testState !== null && testState.error !== null && testState.error !== undefined) {
|
|
1493
|
+
testIndicator = createElement('span', {
|
|
1494
|
+
className: 'mcp-test mcp-test-fail',
|
|
1495
|
+
key: 'test-error',
|
|
1496
|
+
title: testState.error,
|
|
1497
|
+
}, '❌ ' + testState.error)
|
|
1498
|
+
}
|
|
1499
|
+
var rowChildren = [
|
|
1500
|
+
createElement('div', { className: 'card-header', key: 'h' },
|
|
1501
|
+
createElement('span', { className: 'card-title-text', key: 'name' }, entry.id),
|
|
1502
|
+
tag,
|
|
1503
|
+
createElement('div', { className: 'card-actions', key: 'a' },
|
|
1504
|
+
createElement('button', {
|
|
1505
|
+
type: 'button',
|
|
1506
|
+
className: 'btn sm',
|
|
1507
|
+
disabled: view.mcpBusy || (testState !== null && testState.busy) || entry.config === null || entry.config === undefined,
|
|
1508
|
+
title: entry.config === null || entry.config === undefined ? '该配置无法安全解析,请手动编辑 cordis.patch.yml' : (testState !== null && testState.busy ? '检测中...' : (testState !== null && typeof testState.at === 'number' ? '重新检测该服务器的连通性(当前显示的是缓存结果)' : '测试该服务器的连通性')),
|
|
1509
|
+
onClick: function (id) { return function () { testMcpEntry(id) } }(entry.id),
|
|
1510
|
+
}, testState !== null && testState.busy ? '检测中' : '🔌 测试'),
|
|
1511
|
+
createElement('button', {
|
|
1512
|
+
type: 'button',
|
|
1513
|
+
className: 'btn sm',
|
|
1514
|
+
disabled: view.mcpBusy || (testState !== null && testState.busy) || entry.config === null || entry.config === undefined,
|
|
1515
|
+
title: entry.config === null || entry.config === undefined ? '该配置无法安全解析,请手动编辑 cordis.patch.yml' : undefined,
|
|
1516
|
+
onClick: function (value) { return function () { openMcpEditor(value) } }(entry),
|
|
1517
|
+
}, '编辑'),
|
|
1518
|
+
createElement('button', {
|
|
1519
|
+
type: 'button',
|
|
1520
|
+
className: 'btn danger sm',
|
|
1521
|
+
disabled: view.mcpBusy,
|
|
1522
|
+
onClick: function (id) { return function () { removeMcpEntry(id) } }(entry.id),
|
|
1523
|
+
}, '移除'),
|
|
1524
|
+
),
|
|
1525
|
+
),
|
|
1526
|
+
]
|
|
1527
|
+
if (testIndicator !== null) {
|
|
1528
|
+
rowChildren.push(createElement('div', { className: 'mcp-test-row', key: 'test-row' }, testIndicator))
|
|
1529
|
+
}
|
|
1530
|
+
rows.push(createElement('div', { className: 'card', key: 'mcp-' + entry.id }, rowChildren))
|
|
1531
|
+
}
|
|
1532
|
+
if (rows.length === 0) {
|
|
1533
|
+
rows.push(createElement('div', { className: 'empty', key: 'mcp-empty' },
|
|
1534
|
+
createElement('div', null, '🔌 暂无 MCP 服务器配置')
|
|
1535
|
+
))
|
|
1536
|
+
}
|
|
1537
|
+
children.push(createElement('div', { className: 'list', key: 'mcp-list' }, rows))
|
|
1538
|
+
|
|
1539
|
+
// Editor form. patchDraft merges against the CURRENT draft in state so
|
|
1540
|
+
// typing across fields never clobbers earlier edits (stale-closure guard).
|
|
1541
|
+
if (view.mcpEditorOpen && view.mcpDraft !== null) {
|
|
1542
|
+
var d = view.mcpDraft
|
|
1543
|
+
var fieldStyle = { display: 'flex', flexDirection: 'column', gap: '4px', marginBottom: '8px' }
|
|
1544
|
+
var labelStyle = { fontSize: '11px', color: 'var(--dsw-alias-label-secondary, #666)' }
|
|
1545
|
+
var editor = createElement('div', { className: 'card', key: 'mcp-editor' },
|
|
1546
|
+
createElement('div', { className: 'card-header', key: 'h' },
|
|
1547
|
+
createElement('span', { className: 'card-title-text', key: 't' }, d.isNew ? '添加 MCP 服务器' : '编辑 ' + d.id),
|
|
1548
|
+
),
|
|
1549
|
+
createElement('div', { style: fieldStyle, key: 'f-id' },
|
|
1550
|
+
createElement('label', { style: labelStyle }, 'ID(唯一标识,[A-Za-z0-9_-])'),
|
|
1551
|
+
createElement('div', { style: { display: 'flex', gap: '6px' } },
|
|
1552
|
+
createElement('input', {
|
|
1553
|
+
className: 'input',
|
|
1554
|
+
value: d.id,
|
|
1555
|
+
disabled: !d.isNew,
|
|
1556
|
+
onChange: function (e) { patchDraft({ id: e.target.value }) },
|
|
1557
|
+
}),
|
|
1558
|
+
d.isNew ? createElement('button', {
|
|
1559
|
+
type: 'button',
|
|
1560
|
+
className: 'btn sm',
|
|
1561
|
+
title: '重新生成一个随机 ID',
|
|
1562
|
+
onClick: function () { patchDraft({ id: generateMcpId(view.mcpEntries) }) },
|
|
1563
|
+
}, '🎲') : null,
|
|
1564
|
+
),
|
|
1565
|
+
),
|
|
1566
|
+
createElement('div', { style: fieldStyle, key: 'f-server' },
|
|
1567
|
+
createElement('label', { style: labelStyle }, 'serverName(模型命名空间)'),
|
|
1568
|
+
createElement('input', {
|
|
1569
|
+
className: 'input',
|
|
1570
|
+
value: d.serverName,
|
|
1571
|
+
onChange: function (e) { patchDraft({ serverName: e.target.value }) },
|
|
1572
|
+
}),
|
|
1573
|
+
),
|
|
1574
|
+
createElement('div', { style: fieldStyle, key: 'f-transport' },
|
|
1575
|
+
createElement('label', { style: labelStyle }, '传输方式'),
|
|
1576
|
+
createElement('select', {
|
|
1577
|
+
className: 'input',
|
|
1578
|
+
value: d.transport,
|
|
1579
|
+
onChange: function (e) { patchDraft({ transport: e.target.value }) },
|
|
1580
|
+
},
|
|
1581
|
+
createElement('option', { value: 'stdio' }, 'stdio(子进程)'),
|
|
1582
|
+
createElement('option', { value: 'streamable-http' }, 'streamable-http(HTTP)'),
|
|
1583
|
+
),
|
|
1584
|
+
),
|
|
1585
|
+
d.transport === 'stdio' ? createElement('div', { style: fieldStyle, key: 'f-cmd' },
|
|
1586
|
+
createElement('label', { style: labelStyle }, 'command(启动命令)'),
|
|
1587
|
+
createElement('input', {
|
|
1588
|
+
className: 'input',
|
|
1589
|
+
value: d.command,
|
|
1590
|
+
placeholder: 'npx -y @modelcontextprotocol/server-github',
|
|
1591
|
+
onChange: function (e) { patchDraft({ command: e.target.value }) },
|
|
1592
|
+
}),
|
|
1593
|
+
) : createElement('div', { style: fieldStyle, key: 'f-url' },
|
|
1594
|
+
createElement('label', { style: labelStyle }, 'url(MCP 端点)'),
|
|
1595
|
+
createElement('input', {
|
|
1596
|
+
className: 'input',
|
|
1597
|
+
value: d.url,
|
|
1598
|
+
placeholder: 'http://localhost:3000/mcp',
|
|
1599
|
+
onChange: function (e) { patchDraft({ url: e.target.value }) },
|
|
1600
|
+
}),
|
|
1601
|
+
),
|
|
1602
|
+
d.transport === 'streamable-http' ? createElement('div', { style: fieldStyle, key: 'f-headers' },
|
|
1603
|
+
createElement('label', { style: labelStyle }, 'headers(每行 KEY=VALUE,可选)'),
|
|
1604
|
+
createElement('textarea', {
|
|
1605
|
+
className: 'input',
|
|
1606
|
+
style: { minHeight: '48px' },
|
|
1607
|
+
value: d.headers,
|
|
1608
|
+
onChange: function (e) { patchDraft({ headers: e.target.value, headersChanged: true }) },
|
|
1609
|
+
}),
|
|
1610
|
+
) : null,
|
|
1611
|
+
d.transport === 'stdio' ? createElement('div', { style: fieldStyle, key: 'f-args' },
|
|
1612
|
+
createElement('label', { style: labelStyle }, 'args(空格分隔,可选)'),
|
|
1613
|
+
createElement('input', {
|
|
1614
|
+
className: 'input',
|
|
1615
|
+
value: d.args,
|
|
1616
|
+
onChange: function (e) { patchDraft({ args: e.target.value, argsChanged: true }) },
|
|
1617
|
+
}),
|
|
1618
|
+
) : null,
|
|
1619
|
+
d.transport === 'stdio' ? createElement('div', { style: fieldStyle, key: 'f-env' },
|
|
1620
|
+
createElement('label', { style: labelStyle }, 'env(每行 KEY=VALUE,可选)'),
|
|
1621
|
+
createElement('textarea', {
|
|
1622
|
+
className: 'input',
|
|
1623
|
+
style: { minHeight: '48px' },
|
|
1624
|
+
value: d.env,
|
|
1625
|
+
onChange: function (e) { patchDraft({ env: e.target.value, envChanged: true }) },
|
|
1626
|
+
}),
|
|
1627
|
+
) : null,
|
|
1628
|
+
createElement('div', { style: { display: 'flex', alignItems: 'center', gap: '6px', padding: '4px 0', marginBottom: '4px', borderTop: '1px solid var(--dsw-alias-border-subtle, rgba(200,200,210,0.3))', paddingTop: '8px' }, key: 'f-reconnect-header' },
|
|
1629
|
+
createElement('input', {
|
|
1630
|
+
type: 'checkbox',
|
|
1631
|
+
id: 'reconnect-toggle',
|
|
1632
|
+
checked: d.reconnectEnabled,
|
|
1633
|
+
onChange: function (e) { patchDraft({ reconnectEnabled: e.target.checked }) },
|
|
1634
|
+
style: { margin: '0' },
|
|
1635
|
+
}),
|
|
1636
|
+
createElement('label', { htmlFor: 'reconnect-toggle', style: { fontSize: '12px', fontWeight: 500, cursor: 'pointer', color: 'var(--dsw-alias-label-primary, #333)' } }, '启用自动重连'),
|
|
1637
|
+
),
|
|
1638
|
+
d.reconnectEnabled ? createElement('div', { style: { display: 'flex', flexWrap: 'wrap', gap: '8px', marginBottom: '8px', paddingLeft: '4px' }, key: 'f-reconnect-fields' },
|
|
1639
|
+
createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: '2px', flex: '1 1 120px' }, key: 'f-reconnect-id' },
|
|
1640
|
+
createElement('label', { style: { fontSize: '10px', color: 'var(--dsw-alias-label-secondary, #666)' } }, 'initialDelayMs'),
|
|
1641
|
+
createElement('input', {
|
|
1642
|
+
className: 'input',
|
|
1643
|
+
type: 'number',
|
|
1644
|
+
min: 0,
|
|
1645
|
+
value: d.reconnectInitialDelayMs,
|
|
1646
|
+
onChange: function (e) { patchDraft({ reconnectInitialDelayMs: Number(e.target.value) }) },
|
|
1647
|
+
}),
|
|
1648
|
+
),
|
|
1649
|
+
createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: '2px', flex: '1 1 120px' }, key: 'f-reconnect-md' },
|
|
1650
|
+
createElement('label', { style: { fontSize: '10px', color: 'var(--dsw-alias-label-secondary, #666)' } }, 'maxDelayMs'),
|
|
1651
|
+
createElement('input', {
|
|
1652
|
+
className: 'input',
|
|
1653
|
+
type: 'number',
|
|
1654
|
+
min: 0,
|
|
1655
|
+
value: d.reconnectMaxDelayMs,
|
|
1656
|
+
onChange: function (e) { patchDraft({ reconnectMaxDelayMs: Number(e.target.value) }) },
|
|
1657
|
+
}),
|
|
1658
|
+
),
|
|
1659
|
+
createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: '2px', flex: '1 1 120px' }, key: 'f-reconnect-ma' },
|
|
1660
|
+
createElement('label', { style: { fontSize: '10px', color: 'var(--dsw-alias-label-secondary, #666)' } }, 'maxAttempts'),
|
|
1661
|
+
createElement('input', {
|
|
1662
|
+
className: 'input',
|
|
1663
|
+
type: 'number',
|
|
1664
|
+
min: 0,
|
|
1665
|
+
value: d.reconnectMaxAttempts,
|
|
1666
|
+
onChange: function (e) { patchDraft({ reconnectMaxAttempts: Number(e.target.value) }) },
|
|
1667
|
+
}),
|
|
1668
|
+
),
|
|
1669
|
+
) : null,
|
|
1670
|
+
createElement('div', { className: 'card-actions', key: 'f-actions', style: { justifyContent: 'flex-end', gap: '6px' } },
|
|
1671
|
+
createElement('button', {
|
|
1672
|
+
type: 'button',
|
|
1673
|
+
className: 'btn',
|
|
1674
|
+
disabled: view.mcpBusy,
|
|
1675
|
+
onClick: closeMcpEditor,
|
|
1676
|
+
}, '取消'),
|
|
1677
|
+
createElement('button', {
|
|
1678
|
+
type: 'button',
|
|
1679
|
+
className: 'btn primary',
|
|
1680
|
+
disabled: view.mcpBusy || d.id.trim() === '' || d.serverName.trim() === '' ||
|
|
1681
|
+
(d.transport === 'stdio' ? d.command.trim() === '' : d.url.trim() === ''),
|
|
1682
|
+
onClick: saveMcpDraft,
|
|
1683
|
+
}, view.mcpBusy ? '保存中...' : '保存'),
|
|
1684
|
+
),
|
|
1685
|
+
)
|
|
1686
|
+
children.push(editor)
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
return createElement('div', { key: 'mcp-section', style: { display: 'flex', flexDirection: 'column', gap: '10px' } }, children)
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
// Characters allowed in an MCP entry id: [A-Za-z0-9_-]. The random part uses
|
|
1693
|
+
// alphanumerics only so the generated id reads cleanly and always matches the
|
|
1694
|
+
// host-side validation regex.
|
|
1695
|
+
var MCP_ID_ALPHABET = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
|
|
1696
|
+
|
|
1697
|
+
/**
|
|
1698
|
+
* Generate a fresh MCP entry id that is not already used by any listed entry
|
|
1699
|
+
* (an id collision would silently overwrite the existing entry on upsert).
|
|
1700
|
+
*/
|
|
1701
|
+
function generateMcpId(entries) {
|
|
1702
|
+
var taken = {}
|
|
1703
|
+
for (var i = 0; i < entries.length; i++) taken[entries[i].id] = true
|
|
1704
|
+
var candidate
|
|
1705
|
+
do {
|
|
1706
|
+
candidate = 'mcp-'
|
|
1707
|
+
for (var j = 0; j < 8; j++) {
|
|
1708
|
+
candidate += MCP_ID_ALPHABET.charAt(Math.floor(Math.random() * MCP_ID_ALPHABET.length))
|
|
1709
|
+
}
|
|
1710
|
+
} while (taken[candidate])
|
|
1711
|
+
return candidate
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
function mergeDraft(draft, partial) {
|
|
1715
|
+
var next = {}
|
|
1716
|
+
for (var k in draft) next[k] = draft[k]
|
|
1717
|
+
for (var pk in partial) next[pk] = partial[pk]
|
|
1718
|
+
return next
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
/** Set one entry's test status inside the shared mcpTestState map. */
|
|
1722
|
+
function mergeTestState(map, id, partial) {
|
|
1723
|
+
var next = {}
|
|
1724
|
+
for (var k in map) next[k] = map[k]
|
|
1725
|
+
next[id] = {}
|
|
1726
|
+
var cur = map[id] || {}
|
|
1727
|
+
for (var ck in cur) next[id][ck] = cur[ck]
|
|
1728
|
+
for (var pk in partial) next[id][pk] = partial[pk]
|
|
1729
|
+
return next
|
|
1730
|
+
}
|
|
1731
|
+
function renderPluginCard(plugin, view, remove, patch, upgrade) {
|
|
1732
|
+
var isConfirming = view.confirming === plugin.name
|
|
1733
|
+
|
|
1734
|
+
var titleChildren = [
|
|
1735
|
+
createElement('span', { className: 'card-title-text', key: 'name' }, plugin.name),
|
|
1736
|
+
]
|
|
1737
|
+
if (plugin.version) {
|
|
1738
|
+
titleChildren.push(createElement('span', { className: 'tag version', key: 'version' }, 'v' + plugin.version))
|
|
1739
|
+
}
|
|
1740
|
+
var isLocal = Boolean(plugin.localPath)
|
|
1741
|
+
titleChildren.push(createElement('span', {
|
|
1742
|
+
className: plugin.removable ? (isLocal ? 'tag local' : 'tag plugin') : 'tag',
|
|
1743
|
+
key: 'type-tag',
|
|
1744
|
+
},
|
|
1745
|
+
plugin.removable ? (isLocal ? '本地安装' : '包安装') : '内置'
|
|
1746
|
+
))
|
|
1747
|
+
|
|
1748
|
+
// Remote update badge: only for registry-installed bundles (removable and
|
|
1749
|
+
// not a local path) after a check ran. `updates` is keyed by plugin name.
|
|
1750
|
+
var updateInfo = view.updates && view.updates[plugin.name]
|
|
1751
|
+
if (updateInfo && updateInfo.updateAvailable && updateInfo.latest) {
|
|
1752
|
+
titleChildren.push(createElement('span', {
|
|
1753
|
+
className: 'tag update',
|
|
1754
|
+
key: 'update-badge',
|
|
1755
|
+
title: '远程 registry 有新版本:v' + updateInfo.latest + '(当前 v' + plugin.version + ')'
|
|
1756
|
+
+ (typeof updateInfo.at === 'number' && updateInfo.at > 0 ? ',检测于 ' + formatTestTime(updateInfo.at) + ',更新后提醒自动消除' : ''),
|
|
1757
|
+
}, '⬆ 有新版本 v' + updateInfo.latest))
|
|
1758
|
+
} else if (updateInfo && updateInfo.error) {
|
|
1759
|
+
titleChildren.push(createElement('span', {
|
|
1760
|
+
className: 'tag update-error',
|
|
1761
|
+
key: 'update-error',
|
|
1762
|
+
title: updateInfo.error,
|
|
1763
|
+
}, '⚠ 更新检查失败'))
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
var headerChildren = [
|
|
1767
|
+
createElement('div', { className: 'card-title', key: 'title' }, titleChildren),
|
|
1768
|
+
]
|
|
1769
|
+
|
|
1770
|
+
if (plugin.removable && !isConfirming) {
|
|
1771
|
+
var actionChildren = []
|
|
1772
|
+
// 更新 button — only when an update is available and the plugin is a
|
|
1773
|
+
// registry install (not local path).
|
|
1774
|
+
if (updateInfo && updateInfo.updateAvailable && updateInfo.latest && !isLocal) {
|
|
1775
|
+
actionChildren.push(createElement('button', {
|
|
1776
|
+
type: 'button',
|
|
1777
|
+
key: 'btn-upgrade',
|
|
1778
|
+
className: 'btn sm',
|
|
1779
|
+
disabled: view.busy,
|
|
1780
|
+
title: '升级到 v' + updateInfo.latest + '(npm install ' + plugin.name + '@latest)',
|
|
1781
|
+
onClick: function () { upgrade(plugin.name) },
|
|
1782
|
+
}, '⬆ 更新'))
|
|
1783
|
+
}
|
|
1784
|
+
actionChildren.push(createElement('button', {
|
|
1785
|
+
type: 'button',
|
|
1786
|
+
key: 'btn-remove',
|
|
1787
|
+
className: 'btn danger sm',
|
|
1788
|
+
disabled: view.busy,
|
|
1789
|
+
onClick: function () { patch({ confirming: plugin.name }) },
|
|
1790
|
+
}, '卸载'))
|
|
1791
|
+
headerChildren.push(createElement('div', { className: 'card-actions', key: 'actions' }, actionChildren))
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
var header = createElement('div', { className: 'card-header', key: 'header' }, headerChildren)
|
|
1795
|
+
var cardChildren = [header]
|
|
1796
|
+
|
|
1797
|
+
if (plugin.localPath) {
|
|
1798
|
+
cardChildren.push(createElement('div', { className: 'plugin-path', key: 'path', title: plugin.localPath },
|
|
1799
|
+
'📂 ' + plugin.localPath
|
|
1800
|
+
))
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1803
|
+
if (isConfirming) {
|
|
1804
|
+
cardChildren.push(createElement('div', { className: 'confirm-bar', key: 'confirm' },
|
|
1805
|
+
createElement('span', { className: 'confirm-text', key: 'text' }, '⚠️ 确定要卸载该插件吗?'),
|
|
1806
|
+
createElement('div', { className: 'confirm-actions', key: 'actions' },
|
|
1807
|
+
createElement('button', {
|
|
1808
|
+
type: 'button',
|
|
1809
|
+
key: 'btn-confirm',
|
|
1810
|
+
className: 'btn danger-solid sm',
|
|
1811
|
+
disabled: view.busy,
|
|
1812
|
+
onClick: function () { remove(plugin.name) },
|
|
1813
|
+
}, '确认卸载'),
|
|
1814
|
+
createElement('button', {
|
|
1815
|
+
type: 'button',
|
|
1816
|
+
key: 'btn-cancel',
|
|
1817
|
+
className: 'btn sm',
|
|
1818
|
+
disabled: view.busy,
|
|
1819
|
+
onClick: function () { patch({ confirming: null }) },
|
|
1820
|
+
}, '取消')
|
|
1821
|
+
)
|
|
1822
|
+
))
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
return createElement('div', { key: plugin.name, className: 'card' }, cardChildren)
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
/* ========================================================================== */
|
|
1829
|
+
/* Render Sessions View */
|
|
1830
|
+
/* ========================================================================== */
|
|
1831
|
+
|
|
1832
|
+
/**
|
|
1833
|
+
* Live-session display hint. "Online" means the session is still mounted in
|
|
1834
|
+
* the dsh host's in-memory SessionStore (created or opened at least once in
|
|
1835
|
+
* this process) — NOT that a turn is running. Online sessions can now be
|
|
1836
|
+
* closed directly: the admin panel's "关停并删除" disposes the captured agent
|
|
1837
|
+
* handle (stopping any running turn), removes the session from the store,
|
|
1838
|
+
* and then deletes the log — no restart needed.
|
|
1839
|
+
*/
|
|
1840
|
+
var LIVE_HINT = '会话在线:仍挂载于 dsh host 内存(本进程内创建或打开过的会话保持在线,不代表正在运行);可直接"关停并删除"(会中断该会话正在进行的对话)'
|
|
1841
|
+
|
|
1842
|
+
/**
|
|
1843
|
+
* Canonical session status. Live takes precedence over archived (a session
|
|
1844
|
+
* that is both live and in the archived set is shown as 会话在线); otherwise
|
|
1845
|
+
* archived wins over ended. Used by both the filter pills and the pill
|
|
1846
|
+
* counts so they can never disagree about membership.
|
|
1847
|
+
*/
|
|
1848
|
+
function sessionStatus(s) {
|
|
1849
|
+
if (s.live) return 'live'
|
|
1850
|
+
if (s.archived) return 'archived'
|
|
1851
|
+
return 'ended'
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
function filterSessions(sessions, filter, needle) {
|
|
1855
|
+
var result = []
|
|
1856
|
+
for (var i = 0; i < sessions.length; i++) {
|
|
1857
|
+
var s = sessions[i]
|
|
1858
|
+
if (filter !== 'all' && sessionStatus(s) !== filter) continue
|
|
1859
|
+
if (needle !== '') {
|
|
1860
|
+
var hay = ((s.title || '') + ' ' + (s.summary || '') + ' ' + (s.cwd || '') + ' '
|
|
1861
|
+
+ (s.workspaceTitle || '') + ' ' + s.id).toLowerCase()
|
|
1862
|
+
if (hay.indexOf(needle) === -1) continue
|
|
1863
|
+
}
|
|
1864
|
+
result.push(s)
|
|
1865
|
+
}
|
|
1866
|
+
return result
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
/**
|
|
1870
|
+
* Filter plugin layers by the type pill and a fuzzy name/version/path query.
|
|
1871
|
+
* The needle matches case-insensitively against the package name, the
|
|
1872
|
+
* installed version, and the local source path, so "tool" finds
|
|
1873
|
+
* dsh-custom-tool and "0.2" finds version rows.
|
|
1874
|
+
*/
|
|
1875
|
+
function filterPlugins(plugins, filter, needle) {
|
|
1876
|
+
var result = []
|
|
1877
|
+
for (var i = 0; i < plugins.length; i++) {
|
|
1878
|
+
var p = plugins[i]
|
|
1879
|
+
if (filter === 'plugin' && !p.removable) continue
|
|
1880
|
+
if (filter === 'builtin' && p.removable) continue
|
|
1881
|
+
if (needle !== '') {
|
|
1882
|
+
var hay = ((p.name || '') + ' ' + (p.version || '') + ' ' + (p.localPath || '')).toLowerCase()
|
|
1883
|
+
if (hay.indexOf(needle) === -1) continue
|
|
1884
|
+
}
|
|
1885
|
+
result.push(p)
|
|
1886
|
+
}
|
|
1887
|
+
return result
|
|
1888
|
+
}/**
|
|
1889
|
+
* Group sessions by their accounting workspace in registry order; sessions
|
|
1890
|
+
* without a workspace trail under an "未分组" bucket. Empty workspaces are
|
|
1891
|
+
* omitted so the user only sees groups that currently hold a session.
|
|
1892
|
+
*/
|
|
1893
|
+
function buildSessionGroups(filtered, workspaceOrder) {
|
|
1894
|
+
var byWs = new Map()
|
|
1895
|
+
for (var i = 0; i < workspaceOrder.length; i++) {
|
|
1896
|
+
var ws = workspaceOrder[i]
|
|
1897
|
+
byWs.set(ws.workspaceId, {
|
|
1898
|
+
workspaceId: ws.workspaceId, title: ws.title, path: ws.path, sessions: [],
|
|
1899
|
+
})
|
|
1900
|
+
}
|
|
1901
|
+
var ungrouped = []
|
|
1902
|
+
for (var j = 0; j < filtered.length; j++) {
|
|
1903
|
+
var s = filtered[j]
|
|
1904
|
+
var g = s.workspaceId ? byWs.get(s.workspaceId) : undefined
|
|
1905
|
+
if (g !== undefined) g.sessions.push(s)
|
|
1906
|
+
else ungrouped.push(s)
|
|
1907
|
+
}
|
|
1908
|
+
var groups = []
|
|
1909
|
+
for (var k = 0; k < workspaceOrder.length; k++) {
|
|
1910
|
+
var bucket = byWs.get(workspaceOrder[k].workspaceId)
|
|
1911
|
+
if (bucket !== undefined && bucket.sessions.length > 0) groups.push(bucket)
|
|
1912
|
+
}
|
|
1913
|
+
if (ungrouped.length > 0) {
|
|
1914
|
+
groups.push({ workspaceId: null, title: '未分组', path: null, sessions: ungrouped })
|
|
1915
|
+
}
|
|
1916
|
+
return groups
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1919
|
+
function renderSessionsView(view, patch, reload, act) {
|
|
1920
|
+
var elements = []
|
|
1921
|
+
|
|
1922
|
+
// Toolbar: full-width search with inline icon; the refresh action sits at
|
|
1923
|
+
// the row's end so the search field gets the whole width.
|
|
1924
|
+
elements.push(createElement('div', { className: 'toolbar', key: 'toolbar' },
|
|
1925
|
+
createElement('div', { className: 'search-wrap session-search', key: 'wrap' },
|
|
1926
|
+
createElement('span', { className: 'search-icon', key: 'icon' }, '🔍'),
|
|
1927
|
+
createElement('input', {
|
|
1928
|
+
className: 'input',
|
|
1929
|
+
placeholder: '搜索标题、内容摘要、目录或 Session ID...',
|
|
1930
|
+
value: view.needle,
|
|
1931
|
+
onChange: function (e) { patch({ needle: e.target.value }) },
|
|
1932
|
+
}),
|
|
1933
|
+
view.needle !== '' ? createElement('button', {
|
|
1934
|
+
type: 'button',
|
|
1935
|
+
key: 'btn-clear-search',
|
|
1936
|
+
className: 'btn sm',
|
|
1937
|
+
title: '清空搜索',
|
|
1938
|
+
onClick: function () { patch({ needle: '' }) },
|
|
1939
|
+
}, '✕') : null
|
|
1940
|
+
),
|
|
1941
|
+
createElement('button', {
|
|
1942
|
+
type: 'button',
|
|
1943
|
+
key: 'btn-refresh',
|
|
1944
|
+
className: 'btn',
|
|
1945
|
+
disabled: view.busy,
|
|
1946
|
+
onClick: reload,
|
|
1947
|
+
},
|
|
1948
|
+
view.busy ? createElement('span', { className: 'spinner', key: 'spin' }) : null,
|
|
1949
|
+
'🔄 刷新'
|
|
1950
|
+
)
|
|
1951
|
+
))
|
|
1952
|
+
|
|
1953
|
+
// Filter Pills
|
|
1954
|
+
var totalSessions = view.sessions.length
|
|
1955
|
+
var liveCount = 0
|
|
1956
|
+
var archivedCount = 0
|
|
1957
|
+
var endedCount = 0
|
|
1958
|
+
for (var i = 0; i < view.sessions.length; i++) {
|
|
1959
|
+
var s = view.sessions[i]
|
|
1960
|
+
var st = sessionStatus(s)
|
|
1961
|
+
if (st === 'live') liveCount++
|
|
1962
|
+
else if (st === 'archived') archivedCount++
|
|
1963
|
+
else endedCount++
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1966
|
+
elements.push(createElement('div', { className: 'filter-bar', key: 'filters' },
|
|
1967
|
+
createElement('button', {
|
|
1968
|
+
type: 'button',
|
|
1969
|
+
key: 'filter-all',
|
|
1970
|
+
className: 'pill' + (view.filter === 'all' ? ' active' : ''),
|
|
1971
|
+
onClick: function () { patch({ filter: 'all' }) },
|
|
1972
|
+
}, '全部 (' + String(totalSessions) + ')'),
|
|
1973
|
+
createElement('button', {
|
|
1974
|
+
type: 'button',
|
|
1975
|
+
key: 'filter-live',
|
|
1976
|
+
className: 'pill' + (view.filter === 'live' ? ' active' : ''),
|
|
1977
|
+
title: LIVE_HINT,
|
|
1978
|
+
onClick: function () { patch({ filter: 'live' }) },
|
|
1979
|
+
}, '在线 (' + String(liveCount) + ')'),
|
|
1980
|
+
createElement('button', {
|
|
1981
|
+
type: 'button',
|
|
1982
|
+
key: 'filter-archived',
|
|
1983
|
+
className: 'pill' + (view.filter === 'archived' ? ' active' : ''),
|
|
1984
|
+
onClick: function () { patch({ filter: 'archived' }) },
|
|
1985
|
+
}, '已归档 (' + String(archivedCount) + ')'),
|
|
1986
|
+
createElement('button', {
|
|
1987
|
+
type: 'button',
|
|
1988
|
+
key: 'filter-ended',
|
|
1989
|
+
className: 'pill' + (view.filter === 'ended' ? ' active' : ''),
|
|
1990
|
+
onClick: function () { patch({ filter: 'ended' }) },
|
|
1991
|
+
}, '已结束 (' + String(endedCount) + ')')
|
|
1992
|
+
))
|
|
1993
|
+
|
|
1994
|
+
if (view.error !== '') {
|
|
1995
|
+
elements.push(createElement('div', { className: 'error', key: 'error' }, view.error))
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1998
|
+
// Filter then group by workspace (registry order); sessions without an
|
|
1999
|
+
// accounting workspace trail under "未分组", matching the sidebar model.
|
|
2000
|
+
var needle = view.needle.trim().toLowerCase()
|
|
2001
|
+
var filtered = filterSessions(view.sessions, view.filter, needle)
|
|
2002
|
+
var groups = buildSessionGroups(filtered, view.workspaces || [])
|
|
2003
|
+
var rows = []
|
|
2004
|
+
for (var gi = 0; gi < groups.length; gi++) {
|
|
2005
|
+
var g = groups[gi]
|
|
2006
|
+
var groupKey = g.workspaceId === null ? 'ungrouped' : 'ws-' + g.workspaceId
|
|
2007
|
+
rows.push(createElement('div', { className: 'group-header', key: 'header-' + groupKey },
|
|
2008
|
+
createElement('span', { className: 'group-title', key: 'title' },
|
|
2009
|
+
(g.workspaceId === null ? '📂 ' : '📁 ') + (g.title || '未命名')
|
|
2010
|
+
),
|
|
2011
|
+
createElement('span', { className: 'group-count', key: 'count' }, String(g.sessions.length) + ' 个'),
|
|
2012
|
+
g.path !== null
|
|
2013
|
+
? createElement('span', { className: 'group-path', key: 'path', title: g.path }, g.path)
|
|
2014
|
+
: null
|
|
2015
|
+
))
|
|
2016
|
+
for (var si = 0; si < g.sessions.length; si++) {
|
|
2017
|
+
rows.push(renderSessionCard(g.sessions[si], view, act, patch))
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
|
|
2021
|
+
if (rows.length === 0) {
|
|
2022
|
+
rows.push(createElement('div', { className: 'empty', key: 'empty' },
|
|
2023
|
+
createElement('div', null, needle !== '' ? '🔍 无匹配的会话内容' : '💬 没有已持久化的会话')
|
|
2024
|
+
))
|
|
2025
|
+
}
|
|
2026
|
+
|
|
2027
|
+
elements.push(createElement('div', { className: 'list', key: 'list' }, rows))
|
|
2028
|
+
|
|
2029
|
+
// Footer
|
|
2030
|
+
var summaryText = view.busy ? '加载中...'
|
|
2031
|
+
: '共 ' + String(totalSessions) + ' 个会话,当前展示 ' + String(filtered.length) + ' 个'
|
|
2032
|
+
|
|
2033
|
+
elements.push(createElement('div', { className: 'footer', key: 'footer' },
|
|
2034
|
+
createElement('span', { key: 'summary' }, summaryText),
|
|
2035
|
+
createElement('span', { className: 'hint', key: 'hint' }, '会话修改即时同步到侧边栏')
|
|
2036
|
+
))
|
|
2037
|
+
|
|
2038
|
+
return createElement('div', { key: 'sessions-panel', style: { display: 'flex', flexDirection: 'column', gap: '10px' } }, elements)
|
|
2039
|
+
}
|
|
2040
|
+
|
|
2041
|
+
function renderSessionCard(session, view, act, patch) {
|
|
2042
|
+
var isConfirming = view.confirming === session.id
|
|
2043
|
+
var dotClass = 'dot' + (session.live ? ' live' : session.archived ? ' archived' : '')
|
|
2044
|
+
var dotTitle = session.live ? LIVE_HINT : session.archived ? '已归档' : '已结束'
|
|
2045
|
+
|
|
2046
|
+
var actions = []
|
|
2047
|
+
if (!isConfirming) {
|
|
2048
|
+
if (!session.archived && !session.live) {
|
|
2049
|
+
actions.push(createElement('button', {
|
|
2050
|
+
type: 'button',
|
|
2051
|
+
className: 'btn sm',
|
|
2052
|
+
key: 'btn-archive',
|
|
2053
|
+
disabled: view.busy,
|
|
2054
|
+
onClick: function () { act('archive', session.id) },
|
|
2055
|
+
}, '归档'))
|
|
2056
|
+
}
|
|
2057
|
+
if (session.archived) {
|
|
2058
|
+
actions.push(createElement('button', {
|
|
2059
|
+
type: 'button',
|
|
2060
|
+
className: 'btn sm',
|
|
2061
|
+
key: 'btn-unarchive',
|
|
2062
|
+
disabled: view.busy,
|
|
2063
|
+
onClick: function () { act('unarchive', session.id) },
|
|
2064
|
+
}, '取消归档'))
|
|
2065
|
+
}
|
|
2066
|
+
if (!session.live) {
|
|
2067
|
+
actions.push(createElement('button', {
|
|
2068
|
+
type: 'button',
|
|
2069
|
+
className: 'btn danger sm',
|
|
2070
|
+
key: 'btn-delete',
|
|
2071
|
+
disabled: view.busy,
|
|
2072
|
+
onClick: function () { patch({ confirming: session.id }) },
|
|
2073
|
+
}, '删除'))
|
|
2074
|
+
} else {
|
|
2075
|
+
// Online sessions can now be closed through the captured AgentHandle
|
|
2076
|
+
// (closeSession disposes the live agent/session first, so the log can
|
|
2077
|
+
// be removed without resurrection). This stops a running conversation,
|
|
2078
|
+
// so it is a destructive action with its own confirm flow.
|
|
2079
|
+
actions.push(createElement('button', {
|
|
2080
|
+
type: 'button',
|
|
2081
|
+
className: 'btn danger sm',
|
|
2082
|
+
key: 'btn-close',
|
|
2083
|
+
disabled: view.busy,
|
|
2084
|
+
title: '关停该在线会话(停止其 agent 运行)并永久删除日志记录',
|
|
2085
|
+
onClick: function () { patch({ confirming: session.id }) },
|
|
2086
|
+
}, '关停并删除'))
|
|
2087
|
+
}
|
|
2088
|
+
}
|
|
2089
|
+
|
|
2090
|
+
var titleChildren = [
|
|
2091
|
+
createElement('span', { className: dotClass, title: dotTitle, key: 'dot' }),
|
|
2092
|
+
createElement('div', { className: 'card-title', key: 'title' },
|
|
2093
|
+
createElement('span', { className: 'card-title-text', key: 'name', title: session.title || session.cwd },
|
|
2094
|
+
session.title || (session.cwd ? baseName(session.cwd) : '未命名会话')
|
|
2095
|
+
),
|
|
2096
|
+
session.live ? createElement('span', { className: 'tag live', key: 'tag-live', title: LIVE_HINT }, '会话在线')
|
|
2097
|
+
: session.archived ? createElement('span', { className: 'tag archived', key: 'tag-archived' }, '已归档')
|
|
2098
|
+
: null,
|
|
2099
|
+
session.messageCount > 0 ? createElement('span', { className: 'tag turns', key: 'tag-turns' }, String(session.messageCount) + ' 条消息') : null
|
|
2100
|
+
),
|
|
2101
|
+
]
|
|
2102
|
+
var headerChildren
|
|
2103
|
+
if (actions.length > 0) {
|
|
2104
|
+
headerChildren = titleChildren.concat([createElement('div', { className: 'card-actions', key: 'actions' }, actions)])
|
|
2105
|
+
} else {
|
|
2106
|
+
headerChildren = titleChildren
|
|
2107
|
+
}
|
|
2108
|
+
|
|
2109
|
+
var header = createElement('div', { className: 'card-header', key: 'header' }, headerChildren)
|
|
2110
|
+
var cardChildren = [header]
|
|
2111
|
+
|
|
2112
|
+
// Content summary block
|
|
2113
|
+
if (session.summary) {
|
|
2114
|
+
cardChildren.push(createElement('div', { className: 'card-summary', key: 'summary', title: session.summary },
|
|
2115
|
+
createElement('span', { className: 'summary-icon', key: 'icon' }, '💬'),
|
|
2116
|
+
createElement('span', { className: 'summary-text', key: 'text' }, session.summary)
|
|
2117
|
+
))
|
|
2118
|
+
} else if (session.summaryError) {
|
|
2119
|
+
cardChildren.push(createElement('div', { className: 'error', key: 'summary-error' },
|
|
2120
|
+
'摘要读取失败:' + session.summaryError
|
|
2121
|
+
))
|
|
2122
|
+
}
|
|
2123
|
+
|
|
2124
|
+
// Sub metadata line
|
|
2125
|
+
var subChildren = [
|
|
2126
|
+
createElement('span', { className: 'card-sub-item', title: session.cwd || '无工作目录', key: 'path' },
|
|
2127
|
+
'📁 ' + (session.cwd ? baseName(session.cwd) + ' (' + session.cwd + ')' : '(无工作目录)')
|
|
2128
|
+
),
|
|
2129
|
+
createElement('span', { key: 'sep1' }, '·'),
|
|
2130
|
+
createElement('span', { className: 'card-sub-item', key: 'time' }, '🕒 ' + formatDate(session.createdAt)),
|
|
2131
|
+
]
|
|
2132
|
+
if (session.id) {
|
|
2133
|
+
subChildren.push(createElement('span', { key: 'sep2' }, '·'))
|
|
2134
|
+
subChildren.push(createElement('span', { className: 'card-sub-item session-id-badge', key: 'id', title: '会话 ID: ' + session.id },
|
|
2135
|
+
'🆔 ' + session.id.slice(0, 8)
|
|
2136
|
+
))
|
|
2137
|
+
}
|
|
2138
|
+
|
|
2139
|
+
var sub = createElement('div', { className: 'card-sub', key: 'sub' }, subChildren)
|
|
2140
|
+
cardChildren.push(sub)
|
|
2141
|
+
|
|
2142
|
+
if (isConfirming) {
|
|
2143
|
+
cardChildren.push(createElement('div', { className: 'confirm-bar', key: 'confirm' },
|
|
2144
|
+
createElement('span', { className: 'confirm-text', key: 'text' },
|
|
2145
|
+
session.live
|
|
2146
|
+
? '⚠️ 将关停该在线会话(正在运行则会中断)并永久删除记录与日志,确定?'
|
|
2147
|
+
: '⚠️ 确定永久删除该会话记录及日志文件?'),
|
|
2148
|
+
createElement('div', { className: 'confirm-actions', key: 'actions' },
|
|
2149
|
+
createElement('button', {
|
|
2150
|
+
type: 'button',
|
|
2151
|
+
key: 'btn-confirm',
|
|
2152
|
+
className: 'btn danger-solid sm',
|
|
2153
|
+
disabled: view.busy,
|
|
2154
|
+
onClick: function () { act(session.live ? 'closeSession' : 'deleteSession', session.id) },
|
|
2155
|
+
}, '确认删除'),
|
|
2156
|
+
createElement('button', {
|
|
2157
|
+
type: 'button',
|
|
2158
|
+
key: 'btn-cancel',
|
|
2159
|
+
className: 'btn sm',
|
|
2160
|
+
disabled: view.busy,
|
|
2161
|
+
onClick: function () { patch({ confirming: null }) },
|
|
2162
|
+
}, '取消')
|
|
2163
|
+
)
|
|
2164
|
+
))
|
|
2165
|
+
}
|
|
2166
|
+
|
|
2167
|
+
return createElement('div', { key: session.id, className: 'card' }, cardChildren)
|
|
2168
|
+
}
|
|
2169
|
+
|
|
2170
|
+
/* ========================================================================== */
|
|
2171
|
+
/* Plugin Entrypoint */
|
|
2172
|
+
/* ========================================================================== */
|
|
2173
|
+
|
|
2174
|
+
function apply(ctx) {
|
|
2175
|
+
injectStyles()
|
|
2176
|
+
|
|
2177
|
+
var call = function (method, args) {
|
|
2178
|
+
return ctx.connection.rpc.call('/api', method, { args: args })
|
|
2179
|
+
}
|
|
2180
|
+
|
|
2181
|
+
// Sidebar right-click menus: delete session (after archive) on session
|
|
2182
|
+
// rows, reveal in explorer on workspace rows. Mounted once for the page
|
|
2183
|
+
// lifetime; dispose when the plugin unmounts.
|
|
2184
|
+
// The sessions service lets us nudge the sidebar list after a delete so
|
|
2185
|
+
// the removed session disappears immediately instead of lingering in
|
|
2186
|
+
// "未分组" until the next reload.
|
|
2187
|
+
var refreshSessions = null
|
|
2188
|
+
try {
|
|
2189
|
+
var sessionsSvc = ctx.get && ctx.get('sessions')
|
|
2190
|
+
if (sessionsSvc && typeof sessionsSvc.refresh === 'function') {
|
|
2191
|
+
refreshSessions = function () { sessionsSvc.refresh().catch(function () {}) }
|
|
2192
|
+
}
|
|
2193
|
+
} catch (e) { refreshSessions = null }
|
|
2194
|
+
var disposeSidebar = ctx.effect(function () { return setupMenuInjection(call, refreshSessions) })
|
|
2195
|
+
|
|
2196
|
+
// One management-center entry keeps related local administration together;
|
|
2197
|
+
// its tab container mounts only the selected panel.
|
|
2198
|
+
ctx.slots.inject('settings.section', function () {
|
|
2199
|
+
return ctx.slots.register({
|
|
2200
|
+
name: 'settings.section',
|
|
2201
|
+
id: 'plugin-admin',
|
|
2202
|
+
order: 25,
|
|
2203
|
+
label: '管理中心',
|
|
2204
|
+
inject: function () { return { call: call, refreshSessions: refreshSessions } },
|
|
2205
|
+
}, AdminCenterSection)
|
|
2206
|
+
})
|
|
2207
|
+
}
|
|
2208
|
+
|
|
2209
|
+
module.exports = { apply: apply, inject: ['slots', 'connection'] }
|
|
2210
|
+
return module.exports
|
|
2211
|
+
} });
|